From 6b61567b5abb2c3335b1681d8fc1eba4db1b2e2f Mon Sep 17 00:00:00 2001 From: Robin Scholtes Date: Thu, 11 May 2023 20:24:09 +1200 Subject: [PATCH] setting up mobile tests --- nightwatch.json | 4 +- src/core/ChefWorker.js | 3 +- src/core/Recipe.mjs | 4 +- src/core/lib/Magic.mjs | 3 +- src/web/html/index.html | 1 - tests/browser/mobile/00_nightwatch.js | 260 ++++++++++ tests/browser/mobile/01_io.js | 695 ++++++++++++++++++++++++++ tests/browser/mobile/02_ops.js | 487 ++++++++++++++++++ tests/browser/mobile/browserUtils.js | 247 +++++++++ 9 files changed, 1694 insertions(+), 10 deletions(-) create mode 100644 tests/browser/mobile/00_nightwatch.js create mode 100644 tests/browser/mobile/01_io.js create mode 100644 tests/browser/mobile/02_ops.js create mode 100644 tests/browser/mobile/browserUtils.js diff --git a/nightwatch.json b/nightwatch.json index 95359f44..ba0bbef5 100644 --- a/nightwatch.json +++ b/nightwatch.json @@ -1,6 +1,6 @@ { - "src_folders": ["tests/browser"], - "exclude": ["tests/browser/browserUtils.js"], + "src_folders": ["tests/browser/mobile"], + "exclude": ["tests/browser/mobile/browserUtils.js"], "output_folder": "tests/browser/output", "test_settings": { diff --git a/src/core/ChefWorker.js b/src/core/ChefWorker.js index fd92a89b..a43993f9 100644 --- a/src/core/ChefWorker.js +++ b/src/core/ChefWorker.js @@ -7,8 +7,7 @@ */ import Chef from "./Chef.mjs"; -// @TODO: fix import OperationConfig from "./config/OperationConfig.json" assert {type: "json"}; -import OperationConfig from "./config/OperationConfig.json"; +import OperationConfig from "./config/OperationConfig.json" assert {type: "json"}; import OpModules from "./config/modules/OpModules.mjs"; import loglevelMessagePrefix from "loglevel-message-prefix"; diff --git a/src/core/Recipe.mjs b/src/core/Recipe.mjs index e2c4362f..d92968fb 100755 --- a/src/core/Recipe.mjs +++ b/src/core/Recipe.mjs @@ -4,8 +4,7 @@ * @license Apache-2.0 */ -// @TODO: fix import OperationConfig from "./config/OperationConfig.json" assert {type: "json"}; -import OperationConfig from "./config/OperationConfig.json"; +import OperationConfig from "./config/OperationConfig.json" assert {type: "json"}; import OperationError from "./errors/OperationError.mjs"; import Operation from "./Operation.mjs"; import DishError from "./errors/DishError.mjs"; @@ -19,7 +18,6 @@ let modules = null; * The Recipe controls a list of Operations and the Dish they operate on. */ class Recipe { - /** * Recipe constructor * diff --git a/src/core/lib/Magic.mjs b/src/core/lib/Magic.mjs index d55ca6ca..921fc3f6 100644 --- a/src/core/lib/Magic.mjs +++ b/src/core/lib/Magic.mjs @@ -1,5 +1,4 @@ -// @TODO: fix import OperationConfig from "../config/OperationConfig.json" assert {type: "json"}; -import OperationConfig from "../config/OperationConfig.json"; +import OperationConfig from "../config/OperationConfig.json" assert {type: "json"}; import Utils, { isWorkerEnvironment } from "../Utils.mjs"; import Recipe from "../Recipe.mjs"; import Dish from "../Dish.mjs"; diff --git a/src/web/html/index.html b/src/web/html/index.html index 7eab9dbe..49b9d924 100755 --- a/src/web/html/index.html +++ b/src/web/html/index.html @@ -135,7 +135,6 @@ -
diff --git a/tests/browser/mobile/00_nightwatch.js b/tests/browser/mobile/00_nightwatch.js new file mode 100644 index 00000000..e9640909 --- /dev/null +++ b/tests/browser/mobile/00_nightwatch.js @@ -0,0 +1,260 @@ +const utils = require("./browserUtils.js"); + +module.exports = { + before: browser => { + browser + .resizeWindow(412, 915) // Galaxy S20 Ultra-ish size + .url(browser.launchUrl); + }, + + // "Loading screen": browser => { + // // Check that the loading screen appears and then disappears within a reasonable time + // browser + // .waitForElementVisible("#preloader", 300) + // .waitForElementNotPresent("#preloader", 30000); + // }, + + "App loaded": browser => { + browser.useCss(); + // Check that various important elements are loaded + browser.expect.element("#operations").to.be.visible; + browser.expect.element("#search").to.be.visible; + browser.expect.element("#recipe").to.be.visible; + browser.expect.element("#input").to.be.present; + browser.expect.element("#output").to.be.present; + browser.expect.element("#rec-list").to.be.visible; + browser.expect.element("#controls").to.be.visible; + browser.expect.element("#input-text").to.be.visible; + browser.expect.element("#output-text").to.be.visible; + }, + + "Operations dropdown loaded": browser => { + browser.click("#search"); + + browser.expect.element("#operations-dropdown").to.be.visible; + browser.expect.element("#categories").to.be.visible; + browser.expect.element(".op-list").to.be.present; + }, + + "Operations loaded": browser => { + // Check that an operation in every category has been populated + browser.expect.element("//li[contains(@class, 'operation') and text()='To Base64']").to.be.present; + browser.expect.element("//li[contains(@class, 'operation') and text()='To Binary']").to.be.present; + browser.expect.element("//li[contains(@class, 'operation') and text()='AES Decrypt']").to.be.present; + browser.expect.element("//li[contains(@class, 'operation') and text()='PEM to Hex']").to.be.present; + browser.expect.element("//li[contains(@class, 'operation') and text()='Power Set']").to.be.present; + browser.expect.element("//li[contains(@class, 'operation') and text()='Parse IP range']").to.be.present; + browser.expect.element("//li[contains(@class, 'operation') and text()='Remove Diacritics']").to.be.present; + browser.expect.element("//li[contains(@class, 'operation') and text()='Sort']").to.be.present; + browser.expect.element("//li[contains(@class, 'operation') and text()='To UNIX Timestamp']").to.be.present; + browser.expect.element("//li[contains(@class, 'operation') and text()='Extract dates']").to.be.present; + browser.expect.element("//li[contains(@class, 'operation') and text()='Gzip']").to.be.present; + browser.expect.element("//li[contains(@class, 'operation') and text()='Keccak']").to.be.present; + browser.expect.element("//li[contains(@class, 'operation') and text()='JSON Beautify']").to.be.present; + browser.expect.element("//li[contains(@class, 'operation') and text()='Detect File Type']").to.be.present; + browser.expect.element("//li[contains(@class, 'operation') and text()='Play Media']").to.be.present; + browser.expect.element("//li[contains(@class, 'operation') and text()='Disassemble x86']").to.be.present; + browser.expect.element("//li[contains(@class, 'operation') and text()='Register']").to.be.present; + }, + + "Recipe can be run": browser => { + const toHex = "//li[contains(@class, 'operation') and text()='To Hex']"; + const op = "#rec-list .operation .op-title"; + + // Check that operation is visible + browser + .useXpath() + .expect.element(toHex).to.be.visible; + + // Add it to the recipe by double clicking + browser + .useXpath() + .moveToElement(toHex, 10, 10) + .useCss() + .waitForElementVisible(".popover-body", 1000) + .doubleClick("xpath", toHex); + + // Confirm that it has been added to the recipe + browser + .useCss() + .waitForElementVisible(op, 100) + .expect.element(op).text.to.contain("To Hex"); + + // Enter input + browser + .useCss() + .sendKeys("#input-text .cm-content", "Don't Panic.") + .pause(1000) + .click("#bake"); + + // Check output + browser + .useCss() + .waitForElementNotVisible("#stale-indicator", 1000) + .expect.element("#output-text .cm-content").text.that.equals("44 6f 6e 27 74 20 50 61 6e 69 63 2e"); + + // Clear recipe + browser + .useCss() + .moveToElement(op, 10, 10) + .waitForElementNotPresent(".popover-body", 1000) + .click("#clr-recipe") + .waitForElementNotPresent(op); + }, + + "Test every module": browser => { + browser.useCss(); + + // BSON + loadOp("BSON deserialise", browser) + .waitForElementNotVisible("#output-loader", 5000); + + // Charts + loadOp("Entropy", browser) + .waitForElementNotVisible("#output-loader", 5000); + + // Ciphers + loadOp("AES Encrypt", browser) + .waitForElementNotVisible("#output-loader", 5000); + + // Code + loadOp("XPath expression", browser) + .waitForElementNotVisible("#output-loader", 5000); + + // Compression + loadOp("Gzip", browser) + .waitForElementNotVisible("#output-loader", 5000); + + // Crypto + loadOp("MD5", browser) + .waitForElementNotVisible("#output-loader", 5000); + + // Default + loadOp("Fork", browser) + .waitForElementNotVisible("#output-loader", 5000); + + // Diff + loadOp("Diff", browser) + .waitForElementNotVisible("#output-loader", 5000); + + // Encodings + loadOp("Encode text", browser) + .waitForElementNotVisible("#output-loader", 5000); + + // Hashing + loadOp("Streebog", browser) + .waitForElementNotVisible("#output-loader", 5000); + + // Image + loadOp("Extract EXIF", browser) + .waitForElementNotVisible("#output-loader", 5000); + + // PGP + loadOp("PGP Encrypt", browser) + .waitForElementNotVisible("#output-loader", 5000); + + // PublicKey + loadOp("Hex to PEM", browser) + .waitForElementNotVisible("#output-loader", 5000); + + // Regex + loadOp("Strings", browser) + .waitForElementNotVisible("#output-loader", 5000); + + // Shellcode + loadOp("Disassemble x86", browser) + .waitForElementNotVisible("#output-loader", 5000); + + // URL + loadOp("URL Encode", browser) + .waitForElementNotVisible("#output-loader", 5000); + + // UserAgent + loadOp("Parse User Agent", browser) + .waitForElementNotVisible("#output-loader", 5000); + + // YARA + loadOp("YARA Rules", browser) + .waitForElementNotVisible("#output-loader", 5000); + + browser.click("#clr-recipe"); + }, + + "Move around the UI": browser => { + const otherCat = "//a[contains(@class, 'category-title') and contains(@data-target, '#catOther')]", + genUUID = "//li[contains(@class, 'operation') and text()='Generate UUID']"; + + browser.useXpath(); + + // Scroll to a lower category + browser + .getLocationInView(otherCat) + .expect.element(otherCat).to.be.visible; + + // Open category + browser + .click(otherCat) + .expect.element(genUUID).to.be.visible; + + // Add op to recipe + /* mouseButtonUp drops wherever the actual cursor is, not necessarily in the right place, + so we can't test Sortable.js properly using Nightwatch. html-dnd doesn't work either. + Instead of relying on drag and drop, we double click on the op to load it. */ + browser + .getLocationInView(genUUID) + .moveToElement(genUUID, 10, 10) + .doubleClick("xpath", genUUID) + .useCss() + .waitForElementVisible(".operation .op-title", 1000) + .waitForElementNotVisible("#stale-indicator", 1000) + .expect.element("#output-text .cm-content").text.which.matches(/[\da-f-]{36}/); + + browser.click("#clr-recipe"); + }, + + "Search": browser => { + // Search for an op + browser + .useCss() + .clearValue("#search") + .setValue("#search", "md5") + .useXpath() + .waitForElementVisible("//ul[@id='search-results']//b[text()='MD5']", 1000); + }, + + "Alert bar": browser => { + // Bake nothing to create an empty output which can be copied + utils.clear(browser); + utils.bake(browser); + + // Alert bar shows and contains correct content + browser + .click("#copy-output") + .waitForElementVisible("#snackbar-container") + .waitForElementVisible("#snackbar-container .snackbar-content") + .expect.element("#snackbar-container .snackbar-content").text.to.equal("Copied raw output successfully."); + + // Alert bar disappears after the correct amount of time + // Should disappear after 2000ms + browser + .waitForElementNotPresent("#snackbar-container .snackbar-content", 2500) + .waitForElementNotVisible("#snackbar-container"); + }, + + after: browser => { + browser.end(); + } +}; + +/** + * Clears the current recipe and loads a new operation. + * + * @param {string} opName + * @param {Browser} browser + */ +function loadOp(opName, browser) { + return browser + .useCss() + .click("#clr-recipe") + .urlHash("op=" + opName); +} diff --git a/tests/browser/mobile/01_io.js b/tests/browser/mobile/01_io.js new file mode 100644 index 00000000..a3715e16 --- /dev/null +++ b/tests/browser/mobile/01_io.js @@ -0,0 +1,695 @@ +// import { +// clear, +// utils.setInput, +// bake, +// setChrEnc, +// setEOLSeq, +// copy, +// paste, +// loadRecipe, +// expectOutput, +// uploadFile, +// uploadFolder +// } from "./browserUtils.js"; + +const utils = require("./browserUtils.js"); + +const SPECIAL_CHARS = [ + "\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007\u0008\u0009\u000a\u000b\u000c\u000d\u000e\u000f", + "\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017\u0018\u0019\u001a\u001b\u001c\u001d\u001e\u001f", + "\u007f", + "\u0080\u0081\u0082\u0083\u0084\u0085\u0086\u0087\u0088\u0089\u008a\u008b\u008c\u008d\u008e\u008f", + "\u0090\u0091\u0092\u0093\u0094\u0095\u0096\u0097\u0098\u0099\u009a\u009b\u009c\u009d\u009e\u009f", + "\u00ad\u061c\u200b\u200e\u200f\u2028\u2029\u202d\u202e\u2066\u2067\u2069\ufeff\ufff9\ufffa\ufffb\ufffc" +].join(""); + +const ALL_BYTES = [ + "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f", + "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f", + "\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f", + "\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f", + "\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f", + "\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f", + "\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f", + "\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f", + "\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f", + "\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f", + "\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf", + "\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf", + "\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf", + "\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf", + "\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef", + "\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff", +].join(""); + +const PUA_CHARS = "\ue000\ue001\uf8fe\uf8ff"; + +const MULTI_LINE_STRING =`"You know," said Arthur, "it's at times like this, when I'm trapped in a Vogon airlock with a man from Betelgeuse, and about to die of asphyxiation in deep space that I really wish I'd listened to what my mother told me when I was young." +"Why, what did she tell you?" +"I don't know, I didn't listen."`; + +const SELECTABLE_STRING = `ONE +two +ONE +three +ONE +four +ONE`; + +// Descriptions for named control characters +const CONTROL_CHAR_NAMES = { + 0: "null", + 7: "bell", + 8: "backspace", + 10: "line feed", + 11: "vertical tab", + 13: "carriage return", + 27: "escape", + 8203: "zero width space", + 8204: "zero width non-joiner", + 8205: "zero width joiner", + 8206: "left-to-right mark", + 8207: "right-to-left mark", + 8232: "line separator", + 8237: "left-to-right override", + 8238: "right-to-left override", + 8294: "left-to-right isolate", + 8295: "right-to-left isolate", + 8297: "pop directional isolate", + 8233: "paragraph separator", + 65279: "zero width no-break space", + 65532: "object replacement" +}; + +module.exports = { + before: browser => { + browser + .resizeWindow(1280, 800) + .url(browser.launchUrl) + .useCss() + .waitForElementNotPresent("#preloader", 10000) + .click("#auto-bake-label"); + }, + + "CodeMirror has loaded correctly": browser => { + /* Editor has initialised */ + browser + .useCss() + // Input + .waitForElementVisible("#input-text") + .waitForElementVisible("#input-text .cm-editor") + .waitForElementVisible("#input-text .cm-editor .cm-scroller") + .waitForElementVisible("#input-text .cm-editor .cm-scroller .cm-content") + .waitForElementVisible("#input-text .cm-editor .cm-scroller .cm-content .cm-line") + // Output + .waitForElementVisible("#output-text") + .waitForElementVisible("#output-text .cm-editor") + .waitForElementVisible("#output-text .cm-editor .cm-scroller") + .waitForElementVisible("#output-text .cm-editor .cm-scroller .cm-content") + .waitForElementVisible("#output-text .cm-editor .cm-scroller .cm-content .cm-line"); + + /* Status bar is showing and has correct values */ + browser // Input + .waitForElementVisible("#input-text .cm-status-bar") + .waitForElementVisible("#input-text .cm-status-bar .stats-length-value") + .expect.element("#input-text .cm-status-bar .stats-length-value").text.to.equal("0"); + browser.waitForElementVisible("#input-text .cm-status-bar .stats-lines-value") + .expect.element("#input-text .cm-status-bar .stats-lines-value").text.to.equal("1"); + browser.waitForElementVisible("#input-text .cm-status-bar .chr-enc-value") + .expect.element("#input-text .cm-status-bar .chr-enc-value").text.to.equal("Raw Bytes"); + browser.waitForElementVisible("#input-text .cm-status-bar .eol-value") + .expect.element("#input-text .cm-status-bar .eol-value").text.to.equal("LF"); + + browser // Output + .waitForElementVisible("#output-text .cm-status-bar") + .waitForElementVisible("#output-text .cm-status-bar .stats-length-value") + .expect.element("#output-text .cm-status-bar .stats-length-value").text.to.equal("0"); + browser.waitForElementVisible("#output-text .cm-status-bar .stats-lines-value") + .expect.element("#output-text .cm-status-bar .stats-lines-value").text.to.equal("1"); + browser.waitForElementVisible("#output-text .cm-status-bar .baking-time-info") + .expect.element("#output-text .cm-status-bar .baking-time-info").text.to.contain("ms"); + browser.waitForElementVisible("#output-text .cm-status-bar .chr-enc-value") + .expect.element("#output-text .cm-status-bar .chr-enc-value").text.to.equal("Raw Bytes"); + browser.waitForElementVisible("#output-text .cm-status-bar .eol-value") + .expect.element("#output-text .cm-status-bar .eol-value").text.to.equal("LF"); + }, + + "Adding content": browser => { + /* Status bar updates correctly */ + utils.setInput(browser, MULTI_LINE_STRING); + + browser.expect.element("#input-text .cm-status-bar .stats-length-value").text.to.equal("301"); + browser.expect.element("#input-text .cm-status-bar .stats-lines-value").text.to.equal("3"); + browser.expect.element("#input-text .cm-status-bar .chr-enc-value").text.to.equal("Raw Bytes"); + browser.expect.element("#input-text .cm-status-bar .eol-value").text.to.equal("LF"); + + browser.expect.element("#output-text .cm-status-bar .stats-length-value").text.to.equal("0"); + browser.expect.element("#output-text .cm-status-bar .stats-lines-value").text.to.equal("1"); + browser.expect.element("#output-text .cm-status-bar .baking-time-info").text.to.contain("ms"); + browser.expect.element("#output-text .cm-status-bar .chr-enc-value").text.to.equal("Raw Bytes"); + browser.expect.element("#output-text .cm-status-bar .eol-value").text.to.equal("LF"); + + /* Output updates correctly */ + utils.bake(browser); + browser.expect.element("#output-text .cm-status-bar .stats-length-value").text.to.equal("301"); + browser.expect.element("#output-text .cm-status-bar .stats-lines-value").text.to.equal("3"); + browser.expect.element("#output-text .cm-status-bar .baking-time-info").text.to.contain("ms"); + browser.expect.element("#output-text .cm-status-bar .chr-enc-value").text.to.equal("Raw Bytes"); + browser.expect.element("#output-text .cm-status-bar .eol-value").text.to.equal("LF"); + }, + + "Special content": browser => { + /* Special characters are rendered correctly */ + utils.setInput(browser, SPECIAL_CHARS, false); + + // First line + for (let i = 0x0; i <= 0x8; i++) { + browser.expect.element(`#input-text .cm-line:nth-of-type(1) .cm-specialChar:nth-of-type(${i+1})`) + .to.have.property("title").equals(`Control character ${CONTROL_CHAR_NAMES[i] || "0x" + i.toString(16)}`); + browser.expect.element(`#input-text .cm-line:nth-of-type(1) .cm-specialChar:nth-of-type(${i+1})`) + .text.to.equal(String.fromCharCode(0x2400 + i)); + } + + // Tab \u0009 + browser.expect.element(`#input-text .cm-line:nth-of-type(1)`).to.have.property("textContent").match(/\u0009$/); + + // Line feed \u000a + browser.expect.element(`#input-text .cm-line:nth-of-type(1)`).to.have.property("textContent").match(/^.{10}$/); + browser.expect.element("#input-text .cm-status-bar .stats-lines-value").text.to.equal("2"); + + // Second line + for (let i = 0x0b; i < SPECIAL_CHARS.length; i++) { + const index = SPECIAL_CHARS.charCodeAt(i); + const name = CONTROL_CHAR_NAMES[index] || "0x" + index.toString(16); + const value = index >= 32 ? "\u2022" : String.fromCharCode(0x2400 + index); + + browser.expect.element(`#input-text .cm-line:nth-of-type(2) .cm-specialChar:nth-of-type(${i-10})`) + .to.have.property("title").equals(`Control character ${name}`); + browser.expect.element(`#input-text .cm-line:nth-of-type(2) .cm-specialChar:nth-of-type(${i-10})`) + .text.to.equal(value); + } + + /* Output renders correctly */ + utils.setChrEnc(browser, "output", "UTF-8"); + utils.bake(browser); + + // First line + for (let i = 0x0; i <= 0x8; i++) { + browser.expect.element(`#output-text .cm-line:nth-of-type(1) .cm-specialChar:nth-of-type(${i+1})`) + .to.have.property("title").equals(`Control character ${CONTROL_CHAR_NAMES[i] || "0x" + i.toString(16)}`); + browser.expect.element(`#output-text .cm-line:nth-of-type(1) .cm-specialChar:nth-of-type(${i+1})`) + .text.to.equal(String.fromCharCode(0x2400 + i)); + } + + // Tab \u0009 + browser.expect.element(`#output-text .cm-line:nth-of-type(1)`).to.have.property("textContent").match(/\u0009$/); + + // Line feed \u000a + browser.expect.element(`#output-text .cm-line:nth-of-type(1)`).to.have.property("textContent").match(/^.{10}$/); + browser.expect.element("#output-text .cm-status-bar .stats-lines-value").text.to.equal("2"); + + // Second line + for (let i = 0x0b; i < SPECIAL_CHARS.length; i++) { + const index = SPECIAL_CHARS.charCodeAt(i); + const name = CONTROL_CHAR_NAMES[index] || "0x" + index.toString(16); + const value = index >= 32 ? "\u2022" : String.fromCharCode(0x2400 + index); + + browser.expect.element(`#output-text .cm-content .cm-line:nth-of-type(2) .cm-specialChar:nth-of-type(${i-10})`) + .to.have.property("title").equals(`Control character ${name}`); + browser.expect.element(`#output-text .cm-content .cm-line:nth-of-type(2) .cm-specialChar:nth-of-type(${i-10})`) + .text.to.equal(value); + } + + /* Bytes are rendered correctly */ + utils.setInput(browser, ALL_BYTES, false); + // Expect length to be 255, since one character is creating a newline + browser.expect.element(`#input-text .cm-content`).to.have.property("textContent").match(/^.{255}$/); + browser.expect.element("#input-text .cm-status-bar .stats-length-value").text.to.equal("256"); + browser.expect.element("#input-text .cm-status-bar .stats-lines-value").text.to.equal("2"); + + + /* PUA \ue000-\uf8ff */ + utils.setInput(browser, PUA_CHARS, false); + utils.setChrEnc(browser, "output", "UTF-8"); + utils.bake(browser); + + // Confirm input and output as expected + /* In order to render whitespace characters as control character pictures in the output, even + when they are the designated line separator, CyberChef sometimes chooses to represent them + internally using the Unicode Private Use Area (https://en.wikipedia.org/wiki/Private_Use_Areas). + See `Utils.escapeWhitespace()` for an example of this. + Therefore, PUA characters should be rendered normally in the Input but as control character + pictures in the output. + */ + browser.expect.element(`#input-text .cm-content`).to.have.property("textContent").match(/^\ue000\ue001\uf8fe\uf8ff$/); + browser.expect.element(`#output-text .cm-content`).to.have.property("textContent").match(/^\u2400\u2401\u3cfe\u3cff$/); + + /* Can be copied */ + utils.setInput(browser, SPECIAL_CHARS, false); + utils.setChrEnc(browser, "output", "UTF-8"); + utils.bake(browser); + + // Manual copy + browser + .doubleClick("#output-text .cm-content .cm-line:nth-of-type(1) .cm-specialChar:nth-of-type(1)") + .waitForElementVisible("#output-text .cm-selectionBackground"); + utils.copy(browser); + utils.paste(browser, "#search"); // Paste into search box as this won't mess with the values + + // Ensure that the values are as expected + browser.expect.element("#search").to.have.value.that.equals("\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007\u0008"); + browser.clearValue("#search"); + + // Raw copy + browser + .click("#copy-output") + .pause(100); + utils.paste(browser, "#search"); // Paste into search box as this won't mess with the values + + // Ensure that the values are as expected + browser.expect.element("#search").to.have.value.that.matches(/^\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007\u0008\u0009/); + browser.clearValue("#search"); + }, + + "HTML output": browser => { + /* Displays correctly */ + utils.loadRecipe(browser, "Entropy", ALL_BYTES); + utils.bake(browser); + + browser + .waitForElementVisible("#output-html") + .waitForElementVisible("#output-html #chart-area"); + + /* Status bar widgets are disabled */ + browser.expect.element("#output-text .cm-status-bar .disabled .stats-length-value").to.be.visible; + browser.expect.element("#output-text .cm-status-bar .disabled .stats-lines-value").to.be.visible; + browser.expect.element("#output-text .cm-status-bar .disabled .chr-enc-value").to.be.visible; + browser.expect.element("#output-text .cm-status-bar .disabled .eol-value").to.be.visible; + + /* Displays special chars correctly */ + utils.loadRecipe(browser, "To Table", ",\u0000\u0001\u0002\u0003\u0004", [",", "\\r\\n", false, "HTML"]); + utils.bake(browser); + + for (let i = 0x0; i <= 0x4; i++) { + browser.expect.element(`#output-html .cm-specialChar:nth-of-type(${i+1})`) + .to.have.property("title").equals(`Control character ${CONTROL_CHAR_NAMES[i] || "0x" + i.toString(16)}`); + browser.expect.element(`#output-html .cm-specialChar:nth-of-type(${i+1})`) + .text.to.equal(String.fromCharCode(0x2400 + i)); + } + + /* Can be copied */ + // Raw copy + browser + .click("#copy-output") + .pause(100); + utils.paste(browser, "#search"); // Paste into search box as this won't mess with the values + + // Ensure that the values are as expected + browser.expect.element("#search").to.have.value.that.matches(/\u0000\u0001\u0002\u0003\u0004/); + browser.clearValue("#search"); + }, + + "Highlighting": browser => { + utils.setInput(browser, SELECTABLE_STRING); + utils.bake(browser); + + /* Selecting input text also selects other instances in input and output */ + browser // Input + .click("#auto-bake-label") + .doubleClick("#input-text .cm-content .cm-line:nth-of-type(1)") + .waitForElementVisible("#input-text .cm-selectionLayer .cm-selectionBackground") + .waitForElementNotPresent("#input-text .cm-content .cm-line:nth-of-type(1) .cm-selectionMatch") + .waitForElementNotPresent("#input-text .cm-content .cm-line:nth-of-type(2) .cm-selectionMatch") + .waitForElementVisible("#input-text .cm-content .cm-line:nth-of-type(3) .cm-selectionMatch") + .waitForElementNotPresent("#input-text .cm-content .cm-line:nth-of-type(4) .cm-selectionMatch") + .waitForElementVisible("#input-text .cm-content .cm-line:nth-of-type(5) .cm-selectionMatch") + .waitForElementNotPresent("#input-text .cm-content .cm-line:nth-of-type(6) .cm-selectionMatch") + .waitForElementVisible("#input-text .cm-content .cm-line:nth-of-type(7) .cm-selectionMatch"); + + browser // Output + .waitForElementVisible("#output-text .cm-selectionLayer .cm-selectionBackground") + .waitForElementNotPresent("#output-text .cm-content .cm-line:nth-of-type(1) .cm-selectionMatch") + .waitForElementNotPresent("#output-text .cm-content .cm-line:nth-of-type(2) .cm-selectionMatch") + .waitForElementVisible("#output-text .cm-content .cm-line:nth-of-type(3) .cm-selectionMatch") + .waitForElementNotPresent("#output-text .cm-content .cm-line:nth-of-type(4) .cm-selectionMatch") + .waitForElementVisible("#output-text .cm-content .cm-line:nth-of-type(5) .cm-selectionMatch") + .waitForElementNotPresent("#output-text .cm-content .cm-line:nth-of-type(6) .cm-selectionMatch") + .waitForElementVisible("#output-text .cm-content .cm-line:nth-of-type(7) .cm-selectionMatch"); + + /* Selecting output text highlights in input */ + browser // Output + .click("#output-text") + .waitForElementNotPresent("#input-text .cm-selectionLayer .cm-selectionBackground") + .waitForElementNotPresent("#output-text .cm-selectionLayer .cm-selectionBackground") + .waitForElementNotPresent("#input-text .cm-content .cm-line .cm-selectionMatch") + .waitForElementNotPresent("#output-text .cm-content .cm-line .cm-selectionMatch") + .doubleClick("#output-text .cm-content .cm-line:nth-of-type(7)") + .waitForElementVisible("#output-text .cm-selectionLayer .cm-selectionBackground") + .waitForElementVisible("#output-text .cm-content .cm-line:nth-of-type(1) .cm-selectionMatch") + .waitForElementNotPresent("#output-text .cm-content .cm-line:nth-of-type(2) .cm-selectionMatch") + .waitForElementVisible("#output-text .cm-content .cm-line:nth-of-type(3) .cm-selectionMatch") + .waitForElementNotPresent("#output-text .cm-content .cm-line:nth-of-type(4) .cm-selectionMatch") + .waitForElementVisible("#output-text .cm-content .cm-line:nth-of-type(5) .cm-selectionMatch") + .waitForElementNotPresent("#output-text .cm-content .cm-line:nth-of-type(6) .cm-selectionMatch") + .waitForElementNotPresent("#output-text .cm-content .cm-line:nth-of-type(7) .cm-selectionMatch"); + + browser // Input + .waitForElementVisible("#input-text .cm-selectionLayer .cm-selectionBackground") + .waitForElementVisible("#input-text .cm-content .cm-line:nth-of-type(1) .cm-selectionMatch") + .waitForElementNotPresent("#input-text .cm-content .cm-line:nth-of-type(2) .cm-selectionMatch") + .waitForElementVisible("#input-text .cm-content .cm-line:nth-of-type(3) .cm-selectionMatch") + .waitForElementNotPresent("#input-text .cm-content .cm-line:nth-of-type(4) .cm-selectionMatch") + .waitForElementVisible("#input-text .cm-content .cm-line:nth-of-type(5) .cm-selectionMatch") + .waitForElementNotPresent("#input-text .cm-content .cm-line:nth-of-type(6) .cm-selectionMatch") + .waitForElementNotPresent("#input-text .cm-content .cm-line:nth-of-type(7) .cm-selectionMatch"); + + // Turn autobake off again + browser.click("#auto-bake-label"); + }, + + "Character encoding": browser => { + const CHINESE_CHARS = "不要恐慌。"; + /* Dropup works */ + /* Selecting changes output correctly */ + utils.setInput(browser, CHINESE_CHARS, false); + utils.setChrEnc(browser, "input", "UTF-8"); + utils.bake(browser); + utils.expectOutput(browser, "\u00E4\u00B8\u008D\u00E8\u00A6\u0081\u00E6\u0081\u0090\u00E6\u0085\u008C\u00E3\u0080\u0082"); + + /* Changing output to match input works as expected */ + utils.setChrEnc(browser, "output", "UTF-8"); + utils.bake(browser); + utils.expectOutput(browser, CHINESE_CHARS); + + /* Encodings appear in the URL */ + browser.assert.urlContains("ienc=65001"); + browser.assert.urlContains("oenc=65001"); + + /* Preserved when changing tabs */ + browser + .click("#btn-new-tab") + .waitForElementVisible("#input-tabs li:nth-of-type(2).active-input-tab"); + browser.expect.element("#input-text .chr-enc-value").text.that.equals("Raw Bytes"); + browser.expect.element("#output-text .chr-enc-value").text.that.equals("Raw Bytes"); + + utils.setChrEnc(browser, "input", "UTF-7"); + utils.setChrEnc(browser, "output", "UTF-7"); + + browser + .click("#input-tabs li:nth-of-type(1)") + .waitForElementVisible("#input-tabs li:nth-of-type(1).active-input-tab"); + browser.expect.element("#input-text .chr-enc-value").text.that.equals("UTF-8"); + browser.expect.element("#output-text .chr-enc-value").text.that.equals("UTF-8"); + + /* Try various encodings */ + // These are not meant to be realistic encodings for this data + utils.setInput(browser, CHINESE_CHARS, false); + utils.setChrEnc(browser, "input", "UTF-8"); + utils.setChrEnc(browser, "output", "UTF-16LE"); + utils.bake(browser); + utils.expectOutput(browser, "\uB8E4\uE88D\u81A6\u81E6\uE690\u8C85\u80E3"); + + utils.setChrEnc(browser, "output", "Simplified Chinese GBK"); + utils.bake(browser); + utils.expectOutput(browser, "\u6D93\u5D88\uFDFF\u93AD\u612D\u53A1\u9286\u0000"); + + utils.setChrEnc(browser, "input", "UTF-7"); + utils.bake(browser); + utils.expectOutput(browser, "+Tg0-+iYE-+YFA-+YUw-"); + + utils.setChrEnc(browser, "input", "Traditional Chinese Big5"); + utils.bake(browser); + utils.expectOutput(browser, "\u3043\u74B6\uFDFF\u7A3A\uFDFF"); + + utils.setChrEnc(browser, "output", "Windows-1251 Cyrillic"); + utils.bake(browser); + utils.expectOutput(browser, "\u00A4\u0408\u00ADn\u00AE\u0408\u00B7W\u040EC"); + }, + + "Line endings": browser => { + /* Dropup works */ + /* Selecting changes view in input */ + utils.setInput(browser, MULTI_LINE_STRING); + + // Line endings: LF + + // Input + browser + .waitForElementPresent("#input-text .cm-content .cm-line:nth-of-type(3)") + .waitForElementNotPresent("#input-text .cm-content .cm-line:nth-of-type(4)") + .waitForElementNotPresent("#input-text .cm-content .cm-specialChar"); + browser.expect.element("#input-text .cm-status-bar .stats-length-value").text.to.equal("301"); + browser.expect.element("#input-text .cm-status-bar .stats-lines-value").text.to.equal("3"); + + // Output + utils.bake(browser); + browser + .waitForElementPresent("#output-text .cm-content .cm-line:nth-of-type(3)") + .waitForElementNotPresent("#output-text .cm-content .cm-line:nth-of-type(4)") + .waitForElementNotPresent("#output-text .cm-content .cm-specialChar"); + browser.expect.element("#output-text .cm-status-bar .stats-length-value").text.to.equal("301"); + browser.expect.element("#output-text .cm-status-bar .stats-lines-value").text.to.equal("3"); + + // Input EOL: VT + utils.setEOLSeq(browser, "input", "VT"); + + // Input + browser + .waitForElementPresent("#input-text .cm-content .cm-line:nth-of-type(1)") + .waitForElementNotPresent("#input-text .cm-content .cm-line:nth-of-type(2)") + .waitForElementPresent("#input-text .cm-content .cm-specialChar"); + browser.expect.element("#input-text .cm-content .cm-specialChar").text.to.equal("␊"); + browser.expect.element("#input-text .cm-status-bar .stats-length-value").text.to.equal("301"); + browser.expect.element("#input-text .cm-status-bar .stats-lines-value").text.to.equal("1"); + + // Output + utils.bake(browser); + browser + .waitForElementPresent("#output-text .cm-content .cm-line:nth-of-type(3)") + .waitForElementNotPresent("#output-text .cm-content .cm-line:nth-of-type(4)") + .waitForElementNotPresent("#output-text .cm-content .cm-specialChar"); + browser.expect.element("#output-text .cm-status-bar .stats-length-value").text.to.equal("301"); + browser.expect.element("#output-text .cm-status-bar .stats-lines-value").text.to.equal("3"); + + // Output EOL: VT + utils.setEOLSeq(browser, "output", "VT"); + + // Input + browser + .waitForElementPresent("#input-text .cm-content .cm-line:nth-of-type(1)") + .waitForElementNotPresent("#input-text .cm-content .cm-line:nth-of-type(2)") + .waitForElementPresent("#input-text .cm-content .cm-specialChar"); + browser.expect.element("#input-text .cm-content .cm-specialChar").text.to.equal("␊"); + browser.expect.element("#input-text .cm-status-bar .stats-length-value").text.to.equal("301"); + browser.expect.element("#input-text .cm-status-bar .stats-lines-value").text.to.equal("1"); + + // Output + browser + .waitForElementPresent("#output-text .cm-content .cm-line:nth-of-type(1)") + .waitForElementNotPresent("#output-text .cm-content .cm-line:nth-of-type(2)") + .waitForElementPresent("#output-text .cm-content .cm-specialChar"); + browser.expect.element("#output-text .cm-content .cm-specialChar").text.to.equal("␊"); + browser.expect.element("#output-text .cm-status-bar .stats-length-value").text.to.equal("301"); + browser.expect.element("#output-text .cm-status-bar .stats-lines-value").text.to.equal("1"); + + /* Adding new line ending changes output correctly */ + browser.sendKeys("#input-text .cm-content", browser.Keys.RETURN); + + // Input + browser + .waitForElementPresent("#input-text .cm-content .cm-line:nth-of-type(2)") + .waitForElementNotPresent("#input-text .cm-content .cm-line:nth-of-type(3)"); + browser.expect.element("#input-text .cm-status-bar .stats-length-value").text.to.equal("302"); + browser.expect.element("#input-text .cm-status-bar .stats-lines-value").text.to.equal("2"); + + // Output + utils.bake(browser); + browser + .waitForElementPresent("#output-text .cm-content .cm-line:nth-of-type(2)") + .waitForElementNotPresent("#output-text .cm-content .cm-line:nth-of-type(3)"); + browser.expect.element("#output-text .cm-status-bar .stats-length-value").text.to.equal("302"); + browser.expect.element("#output-text .cm-status-bar .stats-lines-value").text.to.equal("2"); + + // Input EOL: CRLF + utils.setEOLSeq(browser, "input", "CRLF"); + // Output EOL: CR + utils.setEOLSeq(browser, "output", "CR"); + browser.sendKeys("#input-text .cm-content", browser.Keys.RETURN); + + // Input + browser + .waitForElementPresent("#input-text .cm-content .cm-line:nth-of-type(2)") + .waitForElementNotPresent("#input-text .cm-content .cm-line:nth-of-type(3)") + .waitForElementPresent("#input-text .cm-content .cm-line:nth-of-type(1) .cm-specialChar:nth-of-type(3)"); + browser.expect.element("#input-text .cm-content .cm-line:nth-of-type(1) .cm-specialChar:nth-of-type(3)").text.to.equal("␋"); + browser.expect.element("#input-text .cm-status-bar .stats-length-value").text.to.equal("304"); + browser.expect.element("#input-text .cm-status-bar .stats-lines-value").text.to.equal("2"); + + // Output + utils.bake(browser); + browser + .waitForElementPresent("#output-text .cm-content .cm-line:nth-of-type(2)") + .waitForElementNotPresent("#output-text .cm-content .cm-line:nth-of-type(3)") + .waitForElementPresent("#output-text .cm-content .cm-line:nth-of-type(2) .cm-specialChar"); + browser.expect.element("#output-text .cm-content .cm-line:nth-of-type(2) .cm-specialChar").text.to.equal("␊"); + browser.expect.element("#output-text .cm-status-bar .stats-length-value").text.to.equal("304"); + browser.expect.element("#output-text .cm-status-bar .stats-lines-value").text.to.equal("2"); + + /* Line endings appear in the URL */ + browser.assert.urlContains("ieol=%0D%0A"); + browser.assert.urlContains("oeol=%0D"); + + /* Preserved when changing tabs */ + browser + .click("#btn-new-tab") + .waitForElementVisible("#input-tabs li:nth-of-type(2).active-input-tab"); + browser.expect.element("#input-text .eol-value").text.that.equals("LF"); + browser.expect.element("#output-text .eol-value").text.that.equals("LF"); + + utils.setEOLSeq(browser, "input", "FF"); + utils.setEOLSeq(browser, "output", "LS"); + + browser + .click("#input-tabs li:nth-of-type(1)") + .waitForElementVisible("#input-tabs li:nth-of-type(1).active-input-tab"); + browser.expect.element("#input-text .eol-value").text.that.equals("CRLF"); + browser.expect.element("#output-text .eol-value").text.that.equals("CR"); + }, + + "File inputs": browser => { + utils.clear(browser); + + /* Side panel displays correct info */ + utils.uploadFile(browser, "files/TowelDay.jpeg"); + + browser + .waitForElementVisible("#input-text .cm-file-details") + .waitForElementVisible("#input-text .cm-file-details .file-details-toggle-shown") + .waitForElementVisible("#input-text .cm-file-details .file-details-thumbnail") + .waitForElementVisible("#input-text .cm-file-details .file-details-name") + .waitForElementVisible("#input-text .cm-file-details .file-details-size") + .waitForElementVisible("#input-text .cm-file-details .file-details-type") + .waitForElementVisible("#input-text .cm-file-details .file-details-loaded"); + browser.expect.element("#input-text .cm-file-details .file-details-name").text.that.equals("TowelDay.jpeg"); + browser.expect.element("#input-text .cm-file-details .file-details-size").text.that.equals("61,379 bytes"); + browser.expect.element("#input-text .cm-file-details .file-details-type").text.that.equals("image/jpeg"); + browser.expect.element("#input-text .cm-file-details .file-details-loaded").text.that.equals("100%"); + + /* Side panel can be hidden */ + browser + .click("#input-text .cm-file-details .file-details-toggle-shown") + .waitForElementNotPresent("#input-text .cm-file-details .file-details-toggle-shown") + .waitForElementVisible("#input-text .cm-file-details .file-details-toggle-hidden") + .expect.element("#input-text .cm-file-details").to.have.css("width").which.equals("1px"); + + browser + .click("#input-text .cm-file-details .file-details-toggle-hidden") + .waitForElementNotPresent("#input-text .cm-file-details .file-details-toggle-hidden") + .waitForElementVisible("#input-text .cm-file-details .file-details-toggle-shown") + .expect.element("#input-text .cm-file-details").to.have.css("width").which.equals("200px"); + }, + + "Folder inputs": browser => { + utils.clear(browser); + + /* Side panel displays correct info */ + utils.uploadFolder(browser, "files"); + + // Loop through tabs + for (let i = 1; i < 3; i++) { + browser + .click(`#input-tabs li:nth-of-type(${i})`) + .waitForElementVisible(`#input-tabs li:nth-of-type(${i}).active-input-tab`); + + browser + .waitForElementVisible("#input-text .cm-file-details") + .waitForElementVisible("#input-text .cm-file-details .file-details-toggle-shown") + .waitForElementVisible("#input-text .cm-file-details .file-details-thumbnail") + .waitForElementVisible("#input-text .cm-file-details .file-details-name") + .waitForElementVisible("#input-text .cm-file-details .file-details-size") + .waitForElementVisible("#input-text .cm-file-details .file-details-type") + .waitForElementVisible("#input-text .cm-file-details .file-details-loaded"); + + browser.getText("#input-text .cm-file-details .file-details-name", function(result) { + switch (result.value) { + case "TowelDay.jpeg": + browser.expect.element("#input-text .cm-file-details .file-details-name").text.that.equals("TowelDay.jpeg"); + browser.expect.element("#input-text .cm-file-details .file-details-size").text.that.equals("61,379 bytes"); + browser.expect.element("#input-text .cm-file-details .file-details-type").text.that.equals("image/jpeg"); + browser.expect.element("#input-text .cm-file-details .file-details-loaded").text.that.equals("100%"); + break; + case "Hitchhikers_Guide.jpeg": + browser.expect.element("#input-text .cm-file-details .file-details-name").text.that.equals("Hitchhikers_Guide.jpeg"); + browser.expect.element("#input-text .cm-file-details .file-details-size").text.that.equals("36,595 bytes"); + browser.expect.element("#input-text .cm-file-details .file-details-type").text.that.equals("image/jpeg"); + browser.expect.element("#input-text .cm-file-details .file-details-loaded").text.that.equals("100%"); + break; + default: + break; + } + }); + } + }, + + "Loading from URL": browser => { + /* Complex deep link populates the input correctly (encoding, eol, input) */ + browser + .urlHash("recipe=To_Base64('A-Za-z0-9%2B/%3D')&input=VGhlIHNoaXBzIGh1bmcgaW4gdGhlIHNreSBpbiBtdWNoIHRoZSBzYW1lIHdheSB0aGF0IGJyaWNrcyBkb24ndC4M&ienc=21866&oenc=1201&ieol=%0C&oeol=%E2%80%A9") + .waitForElementVisible("#rec-list li.operation"); + + browser.expect.element(`#input-text .cm-content`).to.have.property("textContent").match(/^.{65}$/); + browser.expect.element("#input-text .cm-status-bar .stats-length-value").text.to.equal("66"); + browser.expect.element("#input-text .cm-status-bar .stats-lines-value").text.to.equal("2"); + + browser.expect.element("#input-text .chr-enc-value").text.that.equals("KOI8-U Ukrainian Cyrillic"); + browser.expect.element("#output-text .chr-enc-value").text.that.equals("UTF-16BE"); + + browser.expect.element("#input-text .eol-value").text.that.equals("FF"); + browser.expect.element("#output-text .eol-value").text.that.equals("PS"); + + utils.bake(browser); + + browser.expect.element(`#output-text .cm-content`).to.have.property("textContent").match(/^.{44}$/); + browser.expect.element("#output-text .cm-status-bar .stats-length-value").text.to.equal("44"); + browser.expect.element("#output-text .cm-status-bar .stats-lines-value").text.to.equal("1"); + }, + + "Replace input with output": browser => { + /* Input is correctly populated */ + utils.loadRecipe(browser, "XOR", "The ships hung in the sky in much the same way that bricks don't.", [{ "option": "Hex", "string": "65" }, "Standard", false]); + utils.setChrEnc(browser, "input", "UTF-32LE"); + utils.setChrEnc(browser, "output", "UTF-7"); + utils.setEOLSeq(browser, "input", "CRLF"); + utils.setEOLSeq(browser, "output", "LS"); + + browser + .sendKeys("#input-text .cm-content", browser.Keys.RETURN) + .expect.element("#input-text .cm-status-bar .stats-lines-value").text.to.equal("2"); + utils.bake(browser); + + browser.expect.element("#input-text .cm-status-bar .stats-length-value").text.to.equal("67"); + browser.expect.element("#input-text .cm-status-bar .stats-lines-value").text.to.equal("2"); + browser.expect.element("#input-text .chr-enc-value").text.that.equals("UTF-32LE"); + browser.expect.element("#input-text .eol-value").text.that.equals("CRLF"); + browser.expect.element("#output-text .cm-status-bar .stats-length-value").text.to.equal("268"); + + browser + .click("#switch") + .waitForElementVisible("#stale-indicator"); + + browser.expect.element("#input-text .cm-status-bar .stats-length-value").text.to.equal("268"); + + /* Special characters, encodings and line endings all as expected */ + browser.expect.element("#input-text .cm-status-bar .stats-lines-value").text.to.equal("1"); + browser.expect.element("#input-text .chr-enc-value").text.that.equals("UTF-7"); + browser.expect.element("#input-text .eol-value").text.that.equals("LS"); + browser.expect.element("#input-text .cm-line:nth-of-type(1) .cm-specialChar:nth-of-type(1)").text.to.equal("␍"); + browser.expect.element("#input-text .cm-line:nth-of-type(1) .cm-specialChar:nth-of-type(49)").text.to.equal("␑"); + browser.waitForElementNotPresent("#input-text .cm-line:nth-of-type(1) .cm-specialChar:nth-of-type(50)"); + }, + + + after: browser => { + browser.end(); + } +}; diff --git a/tests/browser/mobile/02_ops.js b/tests/browser/mobile/02_ops.js new file mode 100644 index 00000000..f7e447c2 --- /dev/null +++ b/tests/browser/mobile/02_ops.js @@ -0,0 +1,487 @@ +const utils = require("./browserUtils.js"); + +module.exports = { + before: browser => { + browser + .resizeWindow(1280, 800) + .url(browser.launchUrl) + .useCss() + .waitForElementNotPresent("#preloader", 10000) + .click("#auto-bake-label"); + }, + + "Sanity check operations": async browser => { + const Images = await import("../samples/Images.mjs"); + testOp(browser, "A1Z26 Cipher Decode", "20 5 19 20 15 21 20 16 21 20", "testoutput"); + testOp(browser, "A1Z26 Cipher Encode", "test input", "20 5 19 20 9 14 16 21 20"); + testOp(browser, "ADD", "test input", "Ê»ÉÊv¿ÄÆËÊ", [{ "option": "Hex", "string": "56" }]); + testOp(browser, "AES Decrypt", "b443f7f7c16ac5396a34273f6f639caa", "test output", [{ "option": "Hex", "string": "00112233445566778899aabbccddeeff" }, { "option": "Hex", "string": "00000000000000000000000000000000" }, "CBC", "Hex", "Raw", { "option": "Hex", "string": "" }]); + testOp(browser, "AES Encrypt", "test input", "e42eb8fbfb7a98fff061cd2c1a794d92", [{"option": "Hex", "string": "00112233445566778899aabbccddeeff"}, {"option": "Hex", "string": "00000000000000000000000000000000"}, "CBC", "Raw", "Hex"]); + testOp(browser, "AND", "test input", "4$04 $044", [{ "option": "Hex", "string": "34" }]); + testOp(browser, "Add line numbers", "test input", "1 test input"); + testOp(browser, ["From Hex", "Add Text To Image", "To Base64"], Images.PNG_HEX, Images.PNG_CHEF_B64, [[], ["Chef", "Center", "Middle", 0, 0, 16], []]); + testOp(browser, "Adler-32 Checksum", "test input", "16160411"); + testOp(browser, "Affine Cipher Decode", "test input", "rcqr glnsr", [1, 2]); + testOp(browser, "Affine Cipher Encode", "test input", "njln rbfpn", [2, 1]); + testOp(browser, "AMF Decode", "\u000A\u0013\u0001\u0003a\u0006\u0009test", /"\$value": "test"/); + testOp(browser, "AMF Encode", '{"a": "test"}', "\u000A\u0013\u0001\u0003a\u0006\u0009test"); + testOp(browser, "Analyse hash", "0123456789abcdef", /CRC-64/); + testOp(browser, "Atbash Cipher", "test input", "gvhg rmkfg"); + // testOp(browser, "Avro to JSON", "test input", "test_output"); + testOp(browser, "BLAKE2b", "test input", "33ebdc8f38177f3f3f334eeb117a84e11f061bbca4db6b8923e5cec85103f59f415551a5d5a933fdb6305dc7bf84671c2540b463dbfa08ee1895cfaa5bd780b5", ["512", "Hex", { "option": "UTF8", "string": "pass" }]); + testOp(browser, "BLAKE2s", "test input", "defe73d61dfa6e5807e4f9643e159a09ccda6be3c26dcd65f8a9bb38bfc973a7", ["256", "Hex", { "option": "UTF8", "string": "pass" }]); + testOp(browser, "BSON deserialise", "\u0011\u0000\u0000\u0000\u0002a\u0000\u0005\u0000\u0000\u0000test\u0000\u0000", '{\u000A "a": "test"\u000A}'); + testOp(browser, "BSON serialise", '{"a":"test"}', "\u0011\u0000\u0000\u0000\u0002a\u0000\u0005\u0000\u0000\u0000test\u0000\u0000"); + // testOp(browser, "Bacon Cipher Decode", "test input", "test_output"); + // testOp(browser, "Bacon Cipher Encode", "test input", "test_output"); + testOp(browser, "Bcrypt", "test input", /^\$2a\$06\$.{53}$/, [6]); + testOp(browser, "Bcrypt compare", "test input", "Match: test input", ["$2a$05$FCfBSVX7OeRkK.9kQVFCiOYu9XtwtIbePqUiroD1lkASW9q5QClzG"]); + testOp(browser, "Bcrypt parse", "$2a$05$kXWtAIGB/R8VEzInoM5ocOTBtyc0m2YTIwFiBU/0XoW032f9QrkWW", /Rounds: 5/); + testOp(browser, "Bifid Cipher Decode", "qblb tfovy", "test input", ["pass"]); + testOp(browser, "Bifid Cipher Encode", "test input", "qblb tfovy", ["pass"]); + testOp(browser, "Bit shift left", "test input", "\u00E8\u00CA\u00E6\u00E8@\u00D2\u00DC\u00E0\u00EA\u00E8"); + testOp(browser, "Bit shift right", "test input", ":29:\u0010478::"); + testOp(browser, "Blowfish Decrypt", "10884e15427dd84ec35204e9c8e921ae", "test_output", [{"option": "Hex", "string": "1234567801234567"}, {"option": "Hex", "string": "0011223344556677"}, "CBC", "Hex", "Raw"]); + testOp(browser, "Blowfish Encrypt", "test input", "f0fadbd1d90d774f714248cf26b96410", [{"option": "Hex", "string": "1234567801234567"}, {"option": "Hex", "string": "0011223344556677"}, "CBC", "Raw", "Hex"]); + testOp(browser, ["From Hex", "Blur Image", "To Base64"], Images.PNG_HEX, Images.PNG_BLUR_B64); + testOpHtml(browser, "Bombe", "XTSYN WAEUG EZALY NRQIM AMLZX MFUOD AWXLY LZCUZ QOQBQ JLCPK NDDRW F", "table tr:last-child td:first-child", "ECG", ["3-rotor", "LEYJVCNIXWPBQMDRTAKZGFUHOS", "BDFHJLCPRTXVZNYEIWGAKMUSQO { + browser.end(); + } +}; + + +/** @function + * Clears the current recipe and bakes a new operation. + * + * @param {Browser} browser - Nightwatch client + * @param {string|Array} opName - name of operation to be tested, array for multiple ops + * @param {string} input - input text for test + * @param {Array|Array>} args - arguments, nested if multiple ops + */ +function bakeOp(browser, opName, input, args=[]) { + browser.perform(function() { + console.log(`Current test: ${opName}`); + }); + utils.loadRecipe(browser, opName, input, args); + browser.waitForElementVisible("#stale-indicator", 5000); + utils.bake(browser); +} + +/** @function + * Clears the current recipe and tests a new operation. + * + * @param {Browser} browser - Nightwatch client + * @param {string|Array} opName - name of operation to be tested, array for multiple ops + * @param {string} input - input text + * @param {string} output - expected output + * @param {Array|Array>} args - arguments, nested if multiple ops + */ +function testOp(browser, opName, input, output, args=[]) { + bakeOp(browser, opName, input, args); + utils.expectOutput(browser, output); +} + +/** @function + * Clears the current recipe and tests a new operation with HTML output. + * + * @param {Browser} browser - Nightwatch client + * @param {string|Array} opName - name of operation to be tested array for multiple ops + * @param {string} input - input text + * @param {string} cssSelector - CSS selector for HTML output + * @param {string} output - expected output + * @param {Array|Array>} args - arguments, nested if multiple ops + */ +function testOpHtml(browser, opName, input, cssSelector, output, args=[]) { + bakeOp(browser, opName, input, args); + + if (typeof output === "string") { + browser.expect.element("#output-html " + cssSelector).text.that.equals(output); + } else if (output instanceof RegExp) { + browser.expect.element("#output-html " + cssSelector).text.that.matches(output); + } +} + +/** @function + * Clears the current recipe and tests a new Image-based operation. + * + * @param {Browser} browser - Nightwatch client + * @param {string|Array} opName - name of operation to be tested array for multiple ops + * @param {string} filename - filename of image file from samples directory + * @param {Array|Array>} args - arguments, nested if multiple ops + */ +function testOpImage(browser, opName, filename, args) { + browser.perform(function() { + console.log(`Current test: ${opName}`); + }); + utils.loadRecipe(browser, opName, "", args); + utils.uploadFile(browser, filename); + browser.waitForElementVisible("#stale-indicator", 5000); + utils.bake(browser); + + browser + .waitForElementVisible("#output-html img") + .expect.element("#output-html img").to.have.css("width").which.matches(/^[^0]\d*px/); +} + +/** @function + * Clears the current recipe and tests a new File-based operation. + * + * @param {Browser} browser - Nightwatch client + * @param {string|Array} opName - name of operation to be tested array for multiple ops + * @param {string} filename - filename of file from samples directory + * @param {string} cssSelector - CSS selector for HTML output + * @param {string} output - expected output + * @param {Array|Array>} args - arguments, nested if multiple ops + */ +function testOpFile(browser, opName, filename, cssSelector, output, args) { + browser.perform(function() { + console.log(`Current test: ${opName}`); + }); + utils.loadRecipe(browser, opName, "", args); + utils.uploadFile(browser, filename); + browser.pause(100).waitForElementVisible("#stale-indicator", 5000); + utils.bake(browser); + + if (typeof output === "string") { + browser.expect.element("#output-html " + cssSelector).text.that.equals(output); + } else if (output instanceof RegExp) { + browser.expect.element("#output-html " + cssSelector).text.that.matches(output); + } +} diff --git a/tests/browser/mobile/browserUtils.js b/tests/browser/mobile/browserUtils.js new file mode 100644 index 00000000..b73dca91 --- /dev/null +++ b/tests/browser/mobile/browserUtils.js @@ -0,0 +1,247 @@ +/** + * Utility functions for browser tests. + * + * @author n1474335 [n1474335@gmail.com] + * @copyright Crown Copyright 2023 + * @license Apache-2.0 + */ + +/** @function + * Clears the recipe and input + * + * @param {Browser} browser - Nightwatch client + */ +function clear(browser) { + browser + .useCss() + .click("#clr-recipe") + .click("#clr-io") + .waitForElementNotPresent("#rec-list li.operation") + .expect.element("#input-text .cm-content").text.that.equals(""); +} + +/** @function + * Sets the input to the desired string + * + * @param {Browser} browser - Nightwatch client + * @param {string} input - The text to populate the input with + * @param {boolean} [type=true] - Whether to type the characters in by using sendKeys, + * or to set the value of the editor directly (useful for special characters) + */ +function setInput(browser, input, type=true) { + clear(browser); + if (type) { + browser + .useCss() + .sendKeys("#input-text .cm-content", input) + .pause(100); + } else { + browser.execute(text => { + window.app.setInput(text); + }, [input]); + } +} + +/** @function + * Triggers a bake + * + * @param {Browser} browser - Nightwatch client + */ +function bake(browser) { + browser + .click("#bake") + .waitForElementNotVisible("#stale-indicator", 5000) + .waitForElementNotVisible("#output-loader", 5000); +} + +/** @function + * Sets the character encoding in the input or output + * + * @param {Browser} browser - Nightwatch client + * @param {string} io - Either "input" or "output" + * @param {string} enc - The encoding to be set + */ +function setChrEnc(browser, io, enc) { + io = `#${io}-text`; + browser + .useCss() + .click(io + " .chr-enc-value") + .waitForElementVisible(io + " .chr-enc-select .cm-status-bar-select-scroll") + .click("link text", enc) + .waitForElementNotVisible(io + " .chr-enc-select .cm-status-bar-select-scroll") + .expect.element(io + " .chr-enc-value").text.that.equals(enc); +} + +/** @function + * Sets the end of line sequence in the input or output + * + * @param {Browser} browser - Nightwatch client + * @param {string} io - Either "input" or "output" + * @param {string} eol - The sequence to set + */ +function setEOLSeq(browser, io, eol) { + io = `#${io}-text`; + browser + .useCss() + .click(io + " .eol-value") + .waitForElementVisible(io + " .eol-select .cm-status-bar-select-content") + .click(`${io} .cm-status-bar-select-content a[data-val=${eol}]`) + .waitForElementNotVisible(io + " .eol-select .cm-status-bar-select-content") + .expect.element(io + " .eol-value").text.that.equals(eol); +} + +/** @function + * Copies whatever is currently selected + * + * @param {Browser} browser - Nightwatch client + */ +function copy(browser) { + browser.perform(function() { + const actions = this.actions({async: true}); + + // Ctrl + Ins used as this works on Windows, Linux and Mac + return actions + .keyDown(browser.Keys.CONTROL) + .keyDown(browser.Keys.INSERT) + .keyUp(browser.Keys.INSERT) + .keyUp(browser.Keys.CONTROL); + }); +} + +/** @function + * Pastes into the target element + * + * @param {Browser} browser - Nightwatch client + * @param {string} el - Target element selector + */ +function paste(browser, el) { + browser + .click(el) + .perform(function() { + const actions = this.actions({async: true}); + + // Shift + Ins used as this works on Windows, Linux and Mac + return actions + .keyDown(browser.Keys.SHIFT) + .keyDown(browser.Keys.INSERT) + .keyUp(browser.Keys.INSERT) + .keyUp(browser.Keys.SHIFT); + }) + .pause(100); +} + +/** @function + * Loads a recipe and input + * + * @param {Browser} browser - Nightwatch client + * @param {string|Array} opName - name of operation to be loaded, array for multiple ops + * @param {string} input - input text for test + * @param {Array|Array>} args - arguments, nested if multiple ops + */ +function loadRecipe(browser, opName, input, args) { + let recipeConfig; + + if (typeof(opName) === "string") { + recipeConfig = JSON.stringify([{ + "op": opName, + "args": args + }]); + } else if (opName instanceof Array) { + recipeConfig = JSON.stringify( + opName.map((op, i) => { + return { + op: op, + args: args.length ? args[i] : [] + }; + }) + ); + } else { + throw new Error("Invalid operation type. Must be string or array of strings. Received: " + typeof(opName)); + } + + clear(browser); + setInput(browser, input, false); + browser + .urlHash("recipe=" + recipeConfig) + .waitForElementPresent("#rec-list li.operation"); +} + +/** @function + * Tests whether the output matches a given value + * + * @param {Browser} browser - Nightwatch client + * @param {string|RegExp} expected - The expected output value + */ +function expectOutput(browser, expected) { + browser.execute(expected => { + const output = window.app.manager.output.outputEditorView.state.doc.toString(); + if (expected instanceof RegExp) { + return expected.test(output); + } else { + return expected === output; + } + }, [expected]); +} + +/** @function + * Uploads a file using the #open-file input + * + * @param {Browser} browser - Nightwatch client + * @param {string} filename - A path to a file in the samples directory + */ +function uploadFile(browser, filename) { + const filepath = require("path").resolve(__dirname + "/../samples/" + filename); + + // The file input cannot be interacted with by nightwatch while it is hidden, + // so we temporarily expose it for the purposes of this test. + browser.execute(() => { + document.getElementById("open-file").style.display = "block"; + }); + browser + .pause(100) + .setValue("#open-file", filepath) + .pause(100); + browser.execute(() => { + document.getElementById("open-file").style.display = "none"; + }); + browser.waitForElementVisible("#input-text .cm-file-details"); +} + +/** @function + * Uploads a folder using the #open-folder input + * + * @param {Browser} browser - Nightwatch client + * @param {string} foldername - A path to a folder in the samples directory + */ +function uploadFolder(browser, foldername) { + const folderpath = require("path").resolve(__dirname + "/../samples/" + foldername); + + // The folder input cannot be interacted with by nightwatch while it is hidden, + // so we temporarily expose it for the purposes of this test. + browser.execute(() => { + document.getElementById("open-folder").style.display = "block"; + }); + browser + .pause(100) + .setValue("#open-folder", folderpath) + .pause(500); + browser.execute(() => { + document.getElementById("open-folder").style.display = "none"; + }); + browser.waitForElementVisible("#input-text .cm-file-details"); +} + + +module.exports = { + clear: clear, + setInput: setInput, + bake: bake, + setChrEnc: setChrEnc, + setEOLSeq: setEOLSeq, + copy: copy, + paste: paste, + loadRecipe: loadRecipe, + expectOutput: expectOutput, + uploadFile: uploadFile, + uploadFolder: uploadFolder +};