From 05160227a3e5dd18d6df2b0b22624ad2ffc88f1e Mon Sep 17 00:00:00 2001 From: n1474335 Date: Thu, 2 Mar 2023 18:10:52 +0000 Subject: [PATCH] Added initial input and output UI tests --- src/core/Utils.mjs | 15 +- src/web/utils/editorUtils.mjs | 7 +- tests/browser/io.js | 486 ++++++++++++++++++++++++++++++++++ tests/browser/ops.js | 2 +- 4 files changed, 503 insertions(+), 7 deletions(-) create mode 100644 tests/browser/io.js diff --git a/src/core/Utils.mjs b/src/core/Utils.mjs index bd955a4b..43b4ec67 100755 --- a/src/core/Utils.mjs +++ b/src/core/Utils.mjs @@ -835,6 +835,11 @@ class Utils { * Escapes HTML tags in a string to stop them being rendered. * https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet * + * Null bytes are a special case and are converted to a character from the Unicode + * Private Use Area, which CyberChef will display as a control character picture. + * This is done due to null bytes not being rendered or stored correctly in HTML + * DOM building. + * * @param {string} str * @returns string * @@ -849,10 +854,11 @@ class Utils { ">": ">", '"': """, "'": "'", // ' not recommended because it's not in the HTML spec - "`": "`" + "`": "`", + "\u0000": "\ue000" }; - return str ? str.replace(/[&<>"'`]/g, function (match) { + return str ? str.replace(/[&<>"'`\u0000]/g, function (match) { return HTML_CHARS[match]; }) : str; } @@ -876,10 +882,11 @@ class Utils { """: '"', "'": "'", "/": "/", - "`": "`" + "`": "`", + "\ue000": "\u0000" }; - return str.replace(/&#?x?[a-z0-9]{2,4};/ig, function (match) { + return str.replace(/(&#?x?[a-z0-9]{2,4};|\ue000)/ig, function (match) { return HTML_CHARS[match] || match; }); } diff --git a/src/web/utils/editorUtils.mjs b/src/web/utils/editorUtils.mjs index cb0ebed1..e02e692b 100644 --- a/src/web/utils/editorUtils.mjs +++ b/src/web/utils/editorUtils.mjs @@ -25,6 +25,9 @@ const Names = { 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" @@ -32,13 +35,13 @@ const Names = { // Regex for Special Characters to be replaced const UnicodeRegexpSupport = /x/.unicode != null ? "gu" : "g"; -const Specials = new RegExp("[\u0000-\u0008\u000a-\u001f\u007f-\u009f\u00ad\u061c\u200b\u200e\u200f\u2028\u2029\u202d\u202e\ufeff\ufff9-\ufffc\ue000-\uf8ff]", UnicodeRegexpSupport); +const Specials = new RegExp("[\u0000-\u0008\u000a-\u001f\u007f-\u009f\u00ad\u061c\u200b\u200e\u200f\u2028\u2029\u202d\u202e\u2066\u2067\u2069\ufeff\ufff9-\ufffc\ue000-\uf8ff]", UnicodeRegexpSupport); /** * Override for rendering special characters. * Should mirror the toDOM function in - * https://github.com/codemirror/view/blob/main/src/special-chars.ts#L150 + * https://github.com/codemirror/view/blob/main/src/special-chars.ts#L153 * But reverts the replacement of line feeds with newline control pictures. * * @param {number} code diff --git a/tests/browser/io.js b/tests/browser/io.js new file mode 100644 index 00000000..34c019ab --- /dev/null +++ b/tests/browser/io.js @@ -0,0 +1,486 @@ +/** + * Tests for input and output of various types to ensure the editors work as expected + * and retain data integrity, especially when it comes to special characters. + * + * @author n1474335 [n1474335@gmail.com] + * @copyright Crown Copyright 2023 + * @license Apache-2.0 + */ + +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"; + +// 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 */ + setInput(browser, `"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."`); + + 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 */ + 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 */ + 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 */ + setChrEnc(browser, "output", "UTF-8"); + 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 */ + 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 */ + setInput(browser, PUA_CHARS, false); + setChrEnc(browser, "output", "UTF-8"); + 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 */ + setInput(browser, SPECIAL_CHARS, false); + setChrEnc(browser, "output", "UTF-8"); + 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"); + copy(browser); + 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); + 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 */ + loadRecipe(browser, "Entropy", ALL_BYTES); + 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 */ + loadRecipe(browser, "To Table", ",\u0000\u0001\u0002\u0003\u0004", [",", "\\r\\n", false, "HTML"]); + 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); + 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 => { + /* Selecting text also selects other instances */ + /* Selecting input text highlights in output */ + /* Selecting output text highlights in input */ + }, + + "Character encoding": browser => { + /* Dropup works */ + /* Selecting changes output correctly */ + /* Changing output to match input works as expected */ + /* Encodings appear in the URL */ + /* Preserved when changing tabs */ + }, + + "Line endings": browser => { + /* Dropup works */ + /* Selecting changes view in input */ + /* Adding new line ending changes output correctly */ + /* Other EOL characters are displayed correctly when not being used to end a line */ + /* Changing in output has the correct effect */ + /* Line endings appear in the URL */ + /* Preserved when changing tabs */ + }, + + "File inputs": browser => { + /* By button */ + /* By drag and drop */ + /* Side panel displays correct info */ + /* Side panel can be hidden */ + }, + + "Folder inputs": browser => { + /* By button */ + /* By drag and drop */ + }, + + "Loading from URL": browser => { + /* Complex deep link populates the input correctly (encoding, eol, input) */ + }, + + "Replace input with output": browser => { + /* Input is correctly populated */ + /* Special characters, encodings and line endings all as expected */ + }, + + + after: browser => { + browser.end(); + } +}; + +/** @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") + .pause(100) + .waitForElementPresent("#stale-indicator.hidden", 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 + " .cm-status-bar-select-scroll") + .click("link text", enc) + .waitForElementNotVisible(io + " .cm-status-bar-select-scroll") + .expect.element(io + " .chr-enc-value").text.that.equals(enc); +} + +/** @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"); +} diff --git a/tests/browser/ops.js b/tests/browser/ops.js index e3d53f4c..f3f5b85e 100644 --- a/tests/browser/ops.js +++ b/tests/browser/ops.js @@ -382,7 +382,7 @@ module.exports = { * @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 - aarguments, nested if multiple ops + * @param {Array|Array>} args - arguments, nested if multiple ops */ function bakeOp(browser, opName, input, args=[]) { let recipeConfig;