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
+};