From 9d060256dbbee9f94dbea5dd1dec63d2889bc0b3 Mon Sep 17 00:00:00 2001 From: Erika Miguel Date: Mon, 29 Apr 2024 15:21:42 -0400 Subject: [PATCH 1/6] Add support in import test script for importing test harness --- .gitignore | 2 + client/resources/aria-at-test-io-format.mjs | 2 +- client/resources/keys.json | 117 ---- client/resources/support.json | 28 + client/resources/types/aria-at-test-result.js | 4 +- .../20211116172219-commandSequences.js | 2 +- server/resolvers/helpers/retrieveCommands.js | 2 +- server/resources/commandsV1.json | 530 ++++++++++++++++++ server/scripts/import-tests/index.js | 36 +- 9 files changed, 600 insertions(+), 123 deletions(-) delete mode 100644 client/resources/keys.json create mode 100644 server/resources/commandsV1.json diff --git a/.gitignore b/.gitignore index 50447732f..50eb2d5dc 100644 --- a/.gitignore +++ b/.gitignore @@ -125,3 +125,5 @@ server/migrations/test_plan_target_id.csv # Private Key files (installed by deploy) jwt-signing-key.pem + +client/resources diff --git a/client/resources/aria-at-test-io-format.mjs b/client/resources/aria-at-test-io-format.mjs index 8121c4133..31991c975 100644 --- a/client/resources/aria-at-test-io-format.mjs +++ b/client/resources/aria-at-test-io-format.mjs @@ -1455,7 +1455,7 @@ export class TestRunInputOutput { output: command.atOutput.value, assertionResults: command.assertions.map(assertion => ({ assertion: { - priority: assertion.priority === 1 ? 'MUST' : 'SHOULD', + priority: assertion.priority === 1 ? 'REQUIRED' : 'OPTIONAL', text: assertion.description, }, passed: assertion.result === 'pass', diff --git a/client/resources/keys.json b/client/resources/keys.json deleted file mode 100644 index 640724f58..000000000 --- a/client/resources/keys.json +++ /dev/null @@ -1,117 +0,0 @@ -{ - "CTRL_HOME": "Control+Home", - "CTRL_OPT_HOME": "Control+Option+Home", - "CTRL_END": "Control+End", - "CTRL_OPT_END": "Control+Option+End", - "CTRL_HOME_THEN_DOWN": "Control+Home followed by Down Arrow", - "DELETE": "Delete", - "ALT_DELETE": "Alt+Delete", - "ALT_DOWN": "Alt+Down", - "CTRL_ALT_DOWN": "Control+Alt+Down", - "ALT_UP": "Alt+Up", - "C_AND_SHIFT_C": "C / Shift+C", - "SHIFT_C": "Shift+C", - "CTRL_INS_X": "Control+Insert+X", - "OPT_DOWN": "Option+Down", - "OPT_UP": "Option+Up", - "CTRL_OPT_LEFT": "Ctrl+Option+Left", - "CTRL_ALT_LEFT": "Control+Alt+Left", - "CTRL_OPT_RIGHT": "Control+Option+Right", - "CTRL_ALT_RIGHT": "Control+Alt+Right", - "CTRL_OPT_UP": "Control+Option+Up", - "CTRL_OPT_DOWN": "Control+Option+Down", - "CTRL_OPT_RIGHT_AND_CTRL_OPT_LEFT": "Control+Option+Right / Ctrl+Option+Left", - "CTRL_OPT_A": "Control+Option+A", - "CTRL_OPT_CMD_J": "Control+Option+Command+J", - "CTRL_OPT_CMD_L": "Control+Option+Command+L", - "CTRL_OPT_CMD_P": "Control+Option+Command+P", - "SHIFT_CTRL_OPT_CMD_J": "Shift+Control+Option+Command+J", - "SHIFT_CTRL_OPT_CMD_L": "Shift+Control+Option+Command+L", - "CTRL_OPT_CMD_J_AND_SHIFT_CTRL_OPT_CMD_J": "Control+Option+Command+J / Shift+Control+Option+Command+J", - "CTRL_OPT_CMD_C_AND_SHIFT_CTRL_OPT_CMD_C": "Control+Option+Command+C / Shift+Control+Option+Command+C", - "CTRL_OPT_F3": "Control+Option+F3", - "CTRL_OPT_F4": "Control+Option+F4", - "CTRL_OPT_SPACE": "Control+Option+Space", - "CTRL_OPT_SPACE_THEN_CTRL_OPT_RIGHT": "Control+Option+Space followed by Control+Option+Right", - "CTRL_U": "Control+U", - "CMD": "Command", - "CMD_LEFT": "Command+Left", - "CMD_RIGHT": "Command+Right", - "CMD_DOWN": "Command+Down", - "CMD_UP": "Command+Up", - "DOWN": "Down Arrow", - "END": "End", - "ENTER": "Enter", - "E_AND_SHIFT_E": "E / Shift+E", - "ESC": "Escape", - "F_AND_SHIFT_F": "F / Shift+F", - "HOME": "Home", - "INS_DOWN_OR_CAPS_DOWN": "Insert+Down (or CapsLock+Down)", - "INS_F7_OR_CAPS_F7": "Insert+F7 (or CapsLock+F7)", - "INS_SPACE": "Insert+Space", - "INS_TAB": "Insert+Tab", - "INS_TAB_OR_CAPS_TAB": "Insert+Tab (or CapsLock+Tab)", - "INS_UP_OR_CAPS_I": "Insert+Up (or CapsLock+I)", - "INS_UP": "Insert+Up", - "INS_UP_OR_CAPS_UP": "Insert+Up (or CapsLock+Up)", - "INS_Z": "Insert+Z", - "LEFT_AND_RIGHT": "Left Arrow / Right Arrow", - "LEFT": "Left Arrow", - "NUMPAD_5": "Numpad 5", - "INS_NUMPAD_5": "Insert+Numpad 5 (or CapsLock+Numpad 5)", - "INS_NUMPAD_6": "Insert+Numpad 6 (or CapsLock+Numpad 6)", - "NUMPAD_PLUS": "Numpad Plus", - "RIGHT": "Right Arrow", - "SPACE": "Space", - "TAB": "Tab", - "SHIFT_TAB": "Shift+Tab", - "TAB_AND_SHIFT_TAB": "Tab / Shift+Tab", - "UP": "Up Arrow", - "CTRL_ALT_UP": "Control+Alt+Up", - "UP_AND_DOWN": "Up Arrow / Down Arrow", - "SHIFT_X": "Shift+X", - "X_AND_SHIFT_X": "X / Shift+X", - "A": "A", - "SHIFT_A": "Shift+A", - "B": "B", - "SHIFT_B": "Shift+B", - "C": "C", - "D": "D", - "E": "E", - "SHIFT_E": "Shift+E", - "F": "F", - "SHIFT_F": "Shift+F", - "G": "G", - "H": "H", - "I": "I", - "SHIFT_I": "Shift+I", - "J": "J", - "K": "K", - "SHIFT_K": "Shift+K", - "L": "L", - "SHIFT_L": "Shift+L", - "M": "M", - "N": "N", - "O": "O", - "P": "P", - "Q": "Q", - "R": "R", - "SHIFT_R": "Shift+R", - "S": "S", - "T": "T", - "SHIFT_T": "Shift+T", - "CTRL_OPT_CMD_T": "Control+Option+Command+T", - "T_THEN_DOWN": "T followed by Down Arrow", - "SHIFT_T_THEN_DOWN": "Shift+T followed by Down Arrow", - "U": "U", - "SHIFT_U": "Shift+U", - "V": "V", - "W": "W", - "X": "X", - "Y": "Y", - "CTRL_OPT_CMD_Y": "Control+Option+Command+Y", - "SHIFT_CTRL_OPT_CMD_Y": "Shift+Control+Option+Command+Y", - "Z": "Z", - "PAGE_DOWN": "Page Down", - "PAGE_UP": "Page Up" -} diff --git a/client/resources/support.json b/client/resources/support.json index 4b91b7f71..7f8fa85b1 100644 --- a/client/resources/support.json +++ b/client/resources/support.json @@ -70,6 +70,34 @@ "Simultaneously press <kbd>Left Arrow</kbd> and <kbd>Right Arrow</kbd>.", "If VoiceOver said 'quick nav on', press <kbd>Left Arrow</kbd> and <kbd>Right Arrow</kbd> again to turn it back off." ] + }, + "arrowQuickKeyNavOn": { + "screenText": "arrow quick key nav on", + "instructions": [ + "Simultaneously press <kbd>Left Arrow</kbd> and <kbd>Right Arrow</kbd>.", + "If VoiceOver said 'arrow quick key nav off', press <kbd>Left Arrow</kbd> and <kbd>Right Arrow</kbd> again to turn it back on." + ] + }, + "arrowQuickKeyNavOff": { + "screenText": "arrow quick key nav off", + "instructions": [ + "Simultaneously press <kbd>Left Arrow</kbd> and <kbd>Right Arrow</kbd>.", + "If VoiceOver said 'arrow quick key nav on', press <kbd>Left Arrow</kbd> and <kbd>Right Arrow</kbd> again to turn it back off." + ] + }, + "singleQuickKeyNavOn": { + "screenText": "single quick key nav on", + "instructions": [ + "Press <kbd>Control</kbd>+<kbd>Option</kbd>+<kbd>q</kbd>.", + "If VoiceOver said 'single quick key nav off', press <kbd>Control</kbd>+<kbd>Option</kbd>+<kbd>q</kbd> again to turn it back on." + ] + }, + "singleQuickKeyNavOff": { + "screenText": "single quick key nav off", + "instructions": [ + "Press <kbd>Control</kbd>+<kbd>Option</kbd>+<kbd>q</kbd>.", + "If VoiceOver said 'single quick key nav on', press <kbd>Control</kbd>+<kbd>Option</kbd>+<kbd>q</kbd> again to turn it back off." + ] } } } diff --git a/client/resources/types/aria-at-test-result.js b/client/resources/types/aria-at-test-result.js index ec8eab55d..c989191b9 100644 --- a/client/resources/types/aria-at-test-result.js +++ b/client/resources/types/aria-at-test-result.js @@ -4,8 +4,8 @@ */ /** - * @typedef {"MUST" - * | "SHOULD"} AriaATTestResult.AssertionPriorityJSON + * @typedef {"REQUIRED" + * | "OPTIONAL"} AriaATTestResult.AssertionPriorityJSON */ /** diff --git a/server/migrations/20211116172219-commandSequences.js b/server/migrations/20211116172219-commandSequences.js index 85d6243ac..610924c71 100644 --- a/server/migrations/20211116172219-commandSequences.js +++ b/server/migrations/20211116172219-commandSequences.js @@ -1,6 +1,6 @@ const { omit } = require('lodash'); const { TestPlanVersion } = require('../models'); -const commandList = require('../resources/commands.json'); +const commandList = require('../resources/commandsV1.json'); module.exports = { up: async queryInterface => { diff --git a/server/resolvers/helpers/retrieveCommands.js b/server/resolvers/helpers/retrieveCommands.js index a7c42515b..8f035a2ec 100644 --- a/server/resolvers/helpers/retrieveCommands.js +++ b/server/resolvers/helpers/retrieveCommands.js @@ -1,4 +1,4 @@ -const commands = require('../../resources/commands.json'); +const commands = require('../../resources/commandsV1.json'); const commandsV2 = require('../../resources/commandsV2.json'); function findValueByKey(keyMappings, keyToFindText) { diff --git a/server/resources/commandsV1.json b/server/resources/commandsV1.json new file mode 100644 index 000000000..ede2590ac --- /dev/null +++ b/server/resources/commandsV1.json @@ -0,0 +1,530 @@ +[ + { + "id": "A", + "text": "A" + }, + { + "id": "ALT_DELETE", + "text": "Alt+Delete" + }, + { + "id": "ALT_DOWN", + "text": "Alt+Down" + }, + { + "id": "ALT_UP", + "text": "Alt+Up" + }, + { + "id": "B", + "text": "B" + }, + { + "id": "C", + "text": "C" + }, + { + "id": "CMD", + "text": "Command" + }, + { + "id": "CMD_DOWN", + "text": "Command+Down" + }, + { + "id": "CMD_LEFT", + "text": "Command+Left" + }, + { + "id": "CMD_RIGHT", + "text": "Command+Right" + }, + { + "id": "CMD_UP", + "text": "Command+Up" + }, + { + "id": "COMMA", + "text": "Comma" + }, + { + "id": "CTRL_ALT_DOWN", + "text": "Control+Alt+Down" + }, + { + "id": "CTRL_ALT_LEFT", + "text": "Control+Alt+Left" + }, + { + "id": "CTRL_ALT_RIGHT", + "text": "Control+Alt+Right" + }, + { + "id": "CTRL_ALT_UP", + "text": "Control+Alt+Up" + }, + { + "id": "CTRL_END", + "text": "Control+End" + }, + { + "id": "CTRL_HOME", + "text": "Control+Home" + }, + { + "id": "CTRL_HOME_THEN_DOWN", + "text": "Control+Home followed by Down Arrow" + }, + { + "id": "CTRL_INS_X", + "text": "Control+Insert+X" + }, + { + "id": "CTRL_OPT_A", + "text": "Control+Option+A" + }, + { + "id": "CTRL_OPT_CMD_C_AND_SHIFT_CTRL_OPT_CMD_C", + "text": "Control+Option+Command+C / Shift+Control+Option+Command+C" + }, + { + "id": "CTRL_OPT_CMD_G", + "text": "Control+Option+Command+G" + }, + { + "id": "CTRL_OPT_CMD_H", + "text": "Control+Option+Command+H" + }, + { + "id": "CTRL_OPT_CMD_J", + "text": "Control+Option+Command+J" + }, + { + "id": "CTRL_OPT_CMD_J_AND_SHIFT_CTRL_OPT_CMD_J", + "text": "Control+Option+Command+J / Shift+Control+Option+Command+J" + }, + { + "id": "CTRL_OPT_CMD_L", + "text": "Control+Option+Command+L" + }, + { + "id": "CTRL_OPT_CMD_P", + "text": "Control+Option+Command+P" + }, + { + "id": "CTRL_OPT_CMD_T", + "text": "Control+Option+Command+T" + }, + { + "id": "CTRL_OPT_CMD_X", + "text": "Control+Option+Command+X" + }, + { + "id": "CTRL_OPT_CMD_Y", + "text": "Control+Option+Command+Y" + }, + { + "id": "CTRL_OPT_DOWN", + "text": "Control+Option+Down" + }, + { + "id": "CTRL_OPT_END", + "text": "Control+Option+End" + }, + { + "id": "CTRL_OPT_F3", + "text": "Control+Option+F3" + }, + { + "id": "CTRL_OPT_F4", + "text": "Control+Option+F4" + }, + { + "id": "CTRL_OPT_HOME", + "text": "Control+Option+Home" + }, + { + "id": "CTRL_OPT_LEFT", + "text": "Ctrl+Option+Left" + }, + { + "id": "CTRL_OPT_RIGHT", + "text": "Control+Option+Right" + }, + { + "id": "CTRL_OPT_RIGHT_AND_CTRL_OPT_LEFT", + "text": "Control+Option+Right / Ctrl+Option+Left" + }, + { + "id": "CTRL_OPT_SPACE", + "text": "Control+Option+Space" + }, + { + "id": "CTRL_OPT_SPACE_THEN_CTRL_OPT_RIGHT", + "text": "Control+Option+Space followed by Control+Option+Right" + }, + { + "id": "CTRL_OPT_UP", + "text": "Control+Option+Up" + }, + { + "id": "CTRL_U", + "text": "Control+U" + }, + { + "id": "C_AND_SHIFT_C", + "text": "C / Shift+C" + }, + { + "id": "D", + "text": "D" + }, + { + "id": "DELETE", + "text": "Delete" + }, + { + "id": "DOWN", + "text": "Down Arrow" + }, + { + "id": "E", + "text": "E" + }, + { + "id": "END", + "text": "End" + }, + { + "id": "ENTER", + "text": "Enter" + }, + { + "id": "ESC", + "text": "Escape" + }, + { + "id": "E_AND_SHIFT_E", + "text": "E / Shift+E" + }, + { + "id": "F", + "text": "F" + }, + { + "id": "F_AND_SHIFT_F", + "text": "F / Shift+F" + }, + { + "id": "G", + "text": "G" + }, + { + "id": "H", + "text": "H" + }, + { + "id": "HOME", + "text": "Home" + }, + { + "id": "I", + "text": "I" + }, + { + "id": "INS_DOWN_OR_CAPS_DOWN", + "text": "Insert+Down (or CapsLock+Down)" + }, + { + "id": "INS_F7_OR_CAPS_F7", + "text": "Insert+F7 (or CapsLock+F7)" + }, + { + "id": "INS_NUMPAD_5", + "text": "Insert+Numpad 5 (or CapsLock+Numpad 5)" + }, + { + "id": "INS_NUMPAD_6", + "text": "Insert+Numpad 6 (or CapsLock+Numpad 6)" + }, + { + "id": "INS_SPACE", + "text": "Insert+Space" + }, + { + "id": "INS_TAB", + "text": "Insert+Tab" + }, + { + "id": "INS_TAB_OR_CAPS_TAB", + "text": "Insert+Tab (or CapsLock+Tab)" + }, + { + "id": "INS_UP", + "text": "Insert+Up" + }, + { + "id": "INS_UP_OR_CAPS_I", + "text": "Insert+Up (or CapsLock+I)" + }, + { + "id": "INS_UP_OR_CAPS_UP", + "text": "Insert+Up (or CapsLock+Up)" + }, + { + "id": "INS_Z", + "text": "Insert+Z" + }, + { + "id": "J", + "text": "J" + }, + { + "id": "K", + "text": "K" + }, + { + "id": "L", + "text": "L" + }, + { + "id": "LEFT", + "text": "Left Arrow" + }, + { + "id": "LEFT_AND_RIGHT", + "text": "Left Arrow / Right Arrow" + }, + { + "id": "M", + "text": "M" + }, + { + "id": "N", + "text": "N" + }, + { + "id": "NUMPAD_5", + "text": "Numpad 5" + }, + { + "id": "NUMPAD_PLUS", + "text": "Numpad Plus" + }, + { + "id": "O", + "text": "O" + }, + { + "id": "ONE", + "text": "1" + }, + { + "id": "OPT_DOWN", + "text": "Option+Down" + }, + { + "id": "OPT_UP", + "text": "Option+Up" + }, + { + "id": "P", + "text": "P" + }, + { + "id": "PAGE_DOWN", + "text": "Page Down" + }, + { + "id": "PAGE_UP", + "text": "Page Up" + }, + { + "id": "Q", + "text": "Q" + }, + { + "id": "R", + "text": "R" + }, + { + "id": "RIGHT", + "text": "Right Arrow" + }, + { + "id": "S", + "text": "S" + }, + { + "id": "SHIFT_A", + "text": "Shift+A" + }, + { + "id": "SHIFT_B", + "text": "Shift+B" + }, + { + "id": "SHIFT_C", + "text": "Shift+C" + }, + { + "id": "SHIFT_CTRL_OPT_CMD_G", + "text": "Shift+Control+Option+Command+G" + }, + { + "id": "SHIFT_CTRL_OPT_CMD_H", + "text": "Shift+Control+Option+Command+H" + }, + { + "id": "SHIFT_CTRL_OPT_CMD_J", + "text": "Shift+Control+Option+Command+J" + }, + { + "id": "SHIFT_CTRL_OPT_CMD_L", + "text": "Shift+Control+Option+Command+L" + }, + { + "id": "SHIFT_CTRL_OPT_CMD_P", + "text": "Shift+Control+Option+Command+P" + }, + { + "id": "SHIFT_CTRL_OPT_CMD_X", + "text": "Shift+Control+Option+Command+X" + }, + { + "id": "SHIFT_CTRL_OPT_CMD_Y", + "text": "Shift+Control+Option+Command+Y" + }, + { + "id": "SHIFT_D", + "text": "Shift+D" + }, + { + "id": "SHIFT_E", + "text": "Shift+E" + }, + { + "id": "SHIFT_F", + "text": "Shift+F" + }, + { + "id": "SHIFT_G", + "text": "Shift+G" + }, + { + "id": "SHIFT_H", + "text": "Shift+H" + }, + { + "id": "SHIFT_I", + "text": "Shift+I" + }, + { + "id": "SHIFT_K", + "text": "Shift+K" + }, + { + "id": "SHIFT_L", + "text": "Shift+L" + }, + { + "id": "SHIFT_ONE", + "text": "Shift+1" + }, + { + "id": "SHIFT_P", + "text": "Shift+P" + }, + { + "id": "SHIFT_PERIOD", + "text": "Shift+Period" + }, + { + "id": "SHIFT_R", + "text": "Shift+R" + }, + { + "id": "SHIFT_T", + "text": "Shift+T" + }, + { + "id": "SHIFT_TAB", + "text": "Shift+Tab" + }, + { + "id": "SHIFT_TWO", + "text": "Shift+2" + }, + { + "id": "SHIFT_T_THEN_DOWN", + "text": "Shift+T followed by Down Arrow" + }, + { + "id": "SHIFT_U", + "text": "Shift+U" + }, + { + "id": "SHIFT_X", + "text": "Shift+X" + }, + { + "id": "SPACE", + "text": "Space" + }, + { + "id": "T", + "text": "T" + }, + { + "id": "TAB", + "text": "Tab" + }, + { + "id": "TAB_AND_SHIFT_TAB", + "text": "Tab / Shift+Tab" + }, + { + "id": "TWO", + "text": "2" + }, + { + "id": "T_THEN_DOWN", + "text": "T followed by Down Arrow" + }, + { + "id": "U", + "text": "U" + }, + { + "id": "UP", + "text": "Up Arrow" + }, + { + "id": "UP_AND_DOWN", + "text": "Up Arrow / Down Arrow" + }, + { + "id": "V", + "text": "V" + }, + { + "id": "W", + "text": "W" + }, + { + "id": "X", + "text": "X" + }, + { + "id": "X_AND_SHIFT_X", + "text": "X / Shift+X" + }, + { + "id": "Y", + "text": "Y" + }, + { + "id": "Z", + "text": "Z" + } +] \ No newline at end of file diff --git a/server/scripts/import-tests/index.js b/server/scripts/import-tests/index.js index 17860e9c3..2c9e5f853 100644 --- a/server/scripts/import-tests/index.js +++ b/server/scripts/import-tests/index.js @@ -74,6 +74,8 @@ const importTestPlanVersions = async transaction => { }); console.log('`npm run build` output', buildOutput.stdout.toString()); + importHarness(); + const { support } = await updateJsons(); const ats = await At.findAll(); @@ -251,6 +253,38 @@ const readDirectoryGitInfo = directoryPath => { return { gitSha, gitMessage, gitCommitDate }; }; +const importHarness = () => { + const sourceFolder = path.resolve(`${testsDirectory}/resources`); + const targetFolder = path.resolve('../', 'client/resources'); + console.info('Clearing harness directory ...'); + fse.rmSync(targetFolder, { recursive: true, force: true }); + + // Copy source folder + console.info('Importing harness ...'); + fse.copySync(sourceFolder, targetFolder, { filter: (src) => { + if (fse.lstatSync(src).isDirectory()) { + return true; + } + if (!src.includes('.html')) { + return true; + } + }, + }); + + // Copy files + const commandsJson = 'commands.json'; + const supportJson = 'support.json'; + fse.copyFileSync( + `${testsDirectory}/${commandsJson}`, + `${targetFolder}/${commandsJson}` + ); + fse.copyFileSync( + `${testsDirectory}/${supportJson}`, + `${targetFolder}/${supportJson}` + ); + console.info('Import harness complete.'); +}; + const getAppUrl = (directoryRelativePath, { gitSha, directoryPath }) => { return path.join( '/', @@ -318,7 +352,7 @@ const updateJsons = async () => { // Write commands for v1 format await fse.writeFile( - path.resolve(__dirname, '../../resources/commands.json'), + path.resolve(__dirname, '../../resources/commandsV1.json'), JSON.stringify(commands, null, 4) ); From f303ffa3f74038d374ec8a591541d6f3bdbbef53 Mon Sep 17 00:00:00 2001 From: Erika Miguel Date: Wed, 1 May 2024 15:04:32 -0400 Subject: [PATCH 2/6] Support older version commits --- server/scripts/import-tests/index.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/server/scripts/import-tests/index.js b/server/scripts/import-tests/index.js index 2c9e5f853..6d9b8df84 100644 --- a/server/scripts/import-tests/index.js +++ b/server/scripts/import-tests/index.js @@ -261,23 +261,26 @@ const importHarness = () => { // Copy source folder console.info('Importing harness ...'); - fse.copySync(sourceFolder, targetFolder, { filter: (src) => { + fse.copySync(sourceFolder, targetFolder, { + filter: src => { if (fse.lstatSync(src).isDirectory()) { return true; } if (!src.includes('.html')) { return true; } - }, + } }); // Copy files const commandsJson = 'commands.json'; const supportJson = 'support.json'; - fse.copyFileSync( - `${testsDirectory}/${commandsJson}`, - `${targetFolder}/${commandsJson}` - ); + if (fse.existsSync(`${testsDirectory}/${commandsJson}`)) { + fse.copyFileSync( + `${testsDirectory}/${commandsJson}`, + `${targetFolder}/${commandsJson}` + ); + } fse.copyFileSync( `${testsDirectory}/${supportJson}`, `${targetFolder}/${supportJson}` From 240d91709e583dc458daace97d2dfbdfc2612a10 Mon Sep 17 00:00:00 2001 From: Erika Miguel Date: Wed, 1 May 2024 15:12:22 -0400 Subject: [PATCH 3/6] Updating logging messages --- server/scripts/import-tests/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/scripts/import-tests/index.js b/server/scripts/import-tests/index.js index 6d9b8df84..b432916ba 100644 --- a/server/scripts/import-tests/index.js +++ b/server/scripts/import-tests/index.js @@ -256,11 +256,11 @@ const readDirectoryGitInfo = directoryPath => { const importHarness = () => { const sourceFolder = path.resolve(`${testsDirectory}/resources`); const targetFolder = path.resolve('../', 'client/resources'); - console.info('Clearing harness directory ...'); + console.info(`Updating harness directory, ${targetFolder} ...`); fse.rmSync(targetFolder, { recursive: true, force: true }); // Copy source folder - console.info('Importing harness ...'); + console.info('Importing latest harness files ...'); fse.copySync(sourceFolder, targetFolder, { filter: src => { if (fse.lstatSync(src).isDirectory()) { @@ -285,7 +285,7 @@ const importHarness = () => { `${testsDirectory}/${supportJson}`, `${targetFolder}/${supportJson}` ); - console.info('Import harness complete.'); + console.info('Harness files update complete.'); }; const getAppUrl = (directoryRelativePath, { gitSha, directoryPath }) => { From 4dd9a42d43571142dcff3336b350104349a4a87a Mon Sep 17 00:00:00 2001 From: Erika Miguel Date: Wed, 1 May 2024 15:16:00 -0400 Subject: [PATCH 4/6] Remove commands.json from server resources and update commands to commandsV1 --- server/resolvers/helpers/retrieveCommands.js | 4 +- server/resources/commands.json | 530 ------------------- 2 files changed, 2 insertions(+), 532 deletions(-) delete mode 100644 server/resources/commands.json diff --git a/server/resolvers/helpers/retrieveCommands.js b/server/resolvers/helpers/retrieveCommands.js index 8f035a2ec..846a404de 100644 --- a/server/resolvers/helpers/retrieveCommands.js +++ b/server/resolvers/helpers/retrieveCommands.js @@ -1,4 +1,4 @@ -const commands = require('../../resources/commandsV1.json'); +const commandsV1 = require('../../resources/commandsV1.json'); const commandsV2 = require('../../resources/commandsV2.json'); function findValueByKey(keyMappings, keyToFindText) { @@ -110,7 +110,7 @@ function findValuesByKeys(commandsMapping, keysToFind = []) { } const getCommandV1 = commandId => { - return commands.find(command => command.id === commandId); + return commandsV1.find(command => command.id === commandId); }; const getCommandV2 = commandId => { diff --git a/server/resources/commands.json b/server/resources/commands.json deleted file mode 100644 index ede2590ac..000000000 --- a/server/resources/commands.json +++ /dev/null @@ -1,530 +0,0 @@ -[ - { - "id": "A", - "text": "A" - }, - { - "id": "ALT_DELETE", - "text": "Alt+Delete" - }, - { - "id": "ALT_DOWN", - "text": "Alt+Down" - }, - { - "id": "ALT_UP", - "text": "Alt+Up" - }, - { - "id": "B", - "text": "B" - }, - { - "id": "C", - "text": "C" - }, - { - "id": "CMD", - "text": "Command" - }, - { - "id": "CMD_DOWN", - "text": "Command+Down" - }, - { - "id": "CMD_LEFT", - "text": "Command+Left" - }, - { - "id": "CMD_RIGHT", - "text": "Command+Right" - }, - { - "id": "CMD_UP", - "text": "Command+Up" - }, - { - "id": "COMMA", - "text": "Comma" - }, - { - "id": "CTRL_ALT_DOWN", - "text": "Control+Alt+Down" - }, - { - "id": "CTRL_ALT_LEFT", - "text": "Control+Alt+Left" - }, - { - "id": "CTRL_ALT_RIGHT", - "text": "Control+Alt+Right" - }, - { - "id": "CTRL_ALT_UP", - "text": "Control+Alt+Up" - }, - { - "id": "CTRL_END", - "text": "Control+End" - }, - { - "id": "CTRL_HOME", - "text": "Control+Home" - }, - { - "id": "CTRL_HOME_THEN_DOWN", - "text": "Control+Home followed by Down Arrow" - }, - { - "id": "CTRL_INS_X", - "text": "Control+Insert+X" - }, - { - "id": "CTRL_OPT_A", - "text": "Control+Option+A" - }, - { - "id": "CTRL_OPT_CMD_C_AND_SHIFT_CTRL_OPT_CMD_C", - "text": "Control+Option+Command+C / Shift+Control+Option+Command+C" - }, - { - "id": "CTRL_OPT_CMD_G", - "text": "Control+Option+Command+G" - }, - { - "id": "CTRL_OPT_CMD_H", - "text": "Control+Option+Command+H" - }, - { - "id": "CTRL_OPT_CMD_J", - "text": "Control+Option+Command+J" - }, - { - "id": "CTRL_OPT_CMD_J_AND_SHIFT_CTRL_OPT_CMD_J", - "text": "Control+Option+Command+J / Shift+Control+Option+Command+J" - }, - { - "id": "CTRL_OPT_CMD_L", - "text": "Control+Option+Command+L" - }, - { - "id": "CTRL_OPT_CMD_P", - "text": "Control+Option+Command+P" - }, - { - "id": "CTRL_OPT_CMD_T", - "text": "Control+Option+Command+T" - }, - { - "id": "CTRL_OPT_CMD_X", - "text": "Control+Option+Command+X" - }, - { - "id": "CTRL_OPT_CMD_Y", - "text": "Control+Option+Command+Y" - }, - { - "id": "CTRL_OPT_DOWN", - "text": "Control+Option+Down" - }, - { - "id": "CTRL_OPT_END", - "text": "Control+Option+End" - }, - { - "id": "CTRL_OPT_F3", - "text": "Control+Option+F3" - }, - { - "id": "CTRL_OPT_F4", - "text": "Control+Option+F4" - }, - { - "id": "CTRL_OPT_HOME", - "text": "Control+Option+Home" - }, - { - "id": "CTRL_OPT_LEFT", - "text": "Ctrl+Option+Left" - }, - { - "id": "CTRL_OPT_RIGHT", - "text": "Control+Option+Right" - }, - { - "id": "CTRL_OPT_RIGHT_AND_CTRL_OPT_LEFT", - "text": "Control+Option+Right / Ctrl+Option+Left" - }, - { - "id": "CTRL_OPT_SPACE", - "text": "Control+Option+Space" - }, - { - "id": "CTRL_OPT_SPACE_THEN_CTRL_OPT_RIGHT", - "text": "Control+Option+Space followed by Control+Option+Right" - }, - { - "id": "CTRL_OPT_UP", - "text": "Control+Option+Up" - }, - { - "id": "CTRL_U", - "text": "Control+U" - }, - { - "id": "C_AND_SHIFT_C", - "text": "C / Shift+C" - }, - { - "id": "D", - "text": "D" - }, - { - "id": "DELETE", - "text": "Delete" - }, - { - "id": "DOWN", - "text": "Down Arrow" - }, - { - "id": "E", - "text": "E" - }, - { - "id": "END", - "text": "End" - }, - { - "id": "ENTER", - "text": "Enter" - }, - { - "id": "ESC", - "text": "Escape" - }, - { - "id": "E_AND_SHIFT_E", - "text": "E / Shift+E" - }, - { - "id": "F", - "text": "F" - }, - { - "id": "F_AND_SHIFT_F", - "text": "F / Shift+F" - }, - { - "id": "G", - "text": "G" - }, - { - "id": "H", - "text": "H" - }, - { - "id": "HOME", - "text": "Home" - }, - { - "id": "I", - "text": "I" - }, - { - "id": "INS_DOWN_OR_CAPS_DOWN", - "text": "Insert+Down (or CapsLock+Down)" - }, - { - "id": "INS_F7_OR_CAPS_F7", - "text": "Insert+F7 (or CapsLock+F7)" - }, - { - "id": "INS_NUMPAD_5", - "text": "Insert+Numpad 5 (or CapsLock+Numpad 5)" - }, - { - "id": "INS_NUMPAD_6", - "text": "Insert+Numpad 6 (or CapsLock+Numpad 6)" - }, - { - "id": "INS_SPACE", - "text": "Insert+Space" - }, - { - "id": "INS_TAB", - "text": "Insert+Tab" - }, - { - "id": "INS_TAB_OR_CAPS_TAB", - "text": "Insert+Tab (or CapsLock+Tab)" - }, - { - "id": "INS_UP", - "text": "Insert+Up" - }, - { - "id": "INS_UP_OR_CAPS_I", - "text": "Insert+Up (or CapsLock+I)" - }, - { - "id": "INS_UP_OR_CAPS_UP", - "text": "Insert+Up (or CapsLock+Up)" - }, - { - "id": "INS_Z", - "text": "Insert+Z" - }, - { - "id": "J", - "text": "J" - }, - { - "id": "K", - "text": "K" - }, - { - "id": "L", - "text": "L" - }, - { - "id": "LEFT", - "text": "Left Arrow" - }, - { - "id": "LEFT_AND_RIGHT", - "text": "Left Arrow / Right Arrow" - }, - { - "id": "M", - "text": "M" - }, - { - "id": "N", - "text": "N" - }, - { - "id": "NUMPAD_5", - "text": "Numpad 5" - }, - { - "id": "NUMPAD_PLUS", - "text": "Numpad Plus" - }, - { - "id": "O", - "text": "O" - }, - { - "id": "ONE", - "text": "1" - }, - { - "id": "OPT_DOWN", - "text": "Option+Down" - }, - { - "id": "OPT_UP", - "text": "Option+Up" - }, - { - "id": "P", - "text": "P" - }, - { - "id": "PAGE_DOWN", - "text": "Page Down" - }, - { - "id": "PAGE_UP", - "text": "Page Up" - }, - { - "id": "Q", - "text": "Q" - }, - { - "id": "R", - "text": "R" - }, - { - "id": "RIGHT", - "text": "Right Arrow" - }, - { - "id": "S", - "text": "S" - }, - { - "id": "SHIFT_A", - "text": "Shift+A" - }, - { - "id": "SHIFT_B", - "text": "Shift+B" - }, - { - "id": "SHIFT_C", - "text": "Shift+C" - }, - { - "id": "SHIFT_CTRL_OPT_CMD_G", - "text": "Shift+Control+Option+Command+G" - }, - { - "id": "SHIFT_CTRL_OPT_CMD_H", - "text": "Shift+Control+Option+Command+H" - }, - { - "id": "SHIFT_CTRL_OPT_CMD_J", - "text": "Shift+Control+Option+Command+J" - }, - { - "id": "SHIFT_CTRL_OPT_CMD_L", - "text": "Shift+Control+Option+Command+L" - }, - { - "id": "SHIFT_CTRL_OPT_CMD_P", - "text": "Shift+Control+Option+Command+P" - }, - { - "id": "SHIFT_CTRL_OPT_CMD_X", - "text": "Shift+Control+Option+Command+X" - }, - { - "id": "SHIFT_CTRL_OPT_CMD_Y", - "text": "Shift+Control+Option+Command+Y" - }, - { - "id": "SHIFT_D", - "text": "Shift+D" - }, - { - "id": "SHIFT_E", - "text": "Shift+E" - }, - { - "id": "SHIFT_F", - "text": "Shift+F" - }, - { - "id": "SHIFT_G", - "text": "Shift+G" - }, - { - "id": "SHIFT_H", - "text": "Shift+H" - }, - { - "id": "SHIFT_I", - "text": "Shift+I" - }, - { - "id": "SHIFT_K", - "text": "Shift+K" - }, - { - "id": "SHIFT_L", - "text": "Shift+L" - }, - { - "id": "SHIFT_ONE", - "text": "Shift+1" - }, - { - "id": "SHIFT_P", - "text": "Shift+P" - }, - { - "id": "SHIFT_PERIOD", - "text": "Shift+Period" - }, - { - "id": "SHIFT_R", - "text": "Shift+R" - }, - { - "id": "SHIFT_T", - "text": "Shift+T" - }, - { - "id": "SHIFT_TAB", - "text": "Shift+Tab" - }, - { - "id": "SHIFT_TWO", - "text": "Shift+2" - }, - { - "id": "SHIFT_T_THEN_DOWN", - "text": "Shift+T followed by Down Arrow" - }, - { - "id": "SHIFT_U", - "text": "Shift+U" - }, - { - "id": "SHIFT_X", - "text": "Shift+X" - }, - { - "id": "SPACE", - "text": "Space" - }, - { - "id": "T", - "text": "T" - }, - { - "id": "TAB", - "text": "Tab" - }, - { - "id": "TAB_AND_SHIFT_TAB", - "text": "Tab / Shift+Tab" - }, - { - "id": "TWO", - "text": "2" - }, - { - "id": "T_THEN_DOWN", - "text": "T followed by Down Arrow" - }, - { - "id": "U", - "text": "U" - }, - { - "id": "UP", - "text": "Up Arrow" - }, - { - "id": "UP_AND_DOWN", - "text": "Up Arrow / Down Arrow" - }, - { - "id": "V", - "text": "V" - }, - { - "id": "W", - "text": "W" - }, - { - "id": "X", - "text": "X" - }, - { - "id": "X_AND_SHIFT_X", - "text": "X / Shift+X" - }, - { - "id": "Y", - "text": "Y" - }, - { - "id": "Z", - "text": "Z" - } -] \ No newline at end of file From 1920ec106cdc3260738fbff4774e355bdb67a481 Mon Sep 17 00:00:00 2001 From: Erika Miguel Date: Wed, 1 May 2024 15:19:32 -0400 Subject: [PATCH 5/6] Adding note in local development --- docs/local-development.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/local-development.md b/docs/local-development.md index 33fb84e45..5b494a9d5 100644 --- a/docs/local-development.md +++ b/docs/local-development.md @@ -17,6 +17,8 @@ yarn install ``` 2. Set up local database using the instructions provided in [database.md](database.md). + - Note: You must run `yarn db-import-tests:dev` after setting up your database to import the latest test harness into + your project. 3. Run the server ``` yarn dev From d9ee5a6b01db86c34775dfed13a95d8cb17ec0e8 Mon Sep 17 00:00:00 2001 From: Erika Miguel Date: Tue, 7 May 2024 15:43:19 -0400 Subject: [PATCH 6/6] Remove client resources from cache --- client/resources/aria-at-harness.mjs | 675 ------ client/resources/aria-at-test-io-format.mjs | 1878 ----------------- client/resources/aria-at-test-run.mjs | 1336 ------------ client/resources/aria-at-test-window.mjs | 73 - client/resources/at-commands.mjs | 273 --- client/resources/commands.json | 115 - client/resources/keys.mjs | 133 -- client/resources/support.json | 438 ---- client/resources/types/aria-at-test-result.js | 39 - client/resources/types/aria-at-test-run.js | 100 - client/resources/vrender.mjs | 987 --------- 11 files changed, 6047 deletions(-) delete mode 100644 client/resources/aria-at-harness.mjs delete mode 100644 client/resources/aria-at-test-io-format.mjs delete mode 100644 client/resources/aria-at-test-run.mjs delete mode 100644 client/resources/aria-at-test-window.mjs delete mode 100644 client/resources/at-commands.mjs delete mode 100644 client/resources/commands.json delete mode 100644 client/resources/keys.mjs delete mode 100644 client/resources/support.json delete mode 100644 client/resources/types/aria-at-test-result.js delete mode 100644 client/resources/types/aria-at-test-run.js delete mode 100644 client/resources/vrender.mjs diff --git a/client/resources/aria-at-harness.mjs b/client/resources/aria-at-harness.mjs deleted file mode 100644 index bc52077be..000000000 --- a/client/resources/aria-at-harness.mjs +++ /dev/null @@ -1,675 +0,0 @@ -import { - element, - fragment, - property, - attribute, - className, - style, - focus, - render, -} from './vrender.mjs'; -import { - AssertionResultMap, - userCloseWindow, - userOpenWindow, - WhitespaceStyleMap, - UnexpectedBehaviorImpactMap, -} from './aria-at-test-run.mjs'; -import { TestRunExport, TestRunInputOutput } from './aria-at-test-io-format.mjs'; -import { TestWindow } from './aria-at-test-window.mjs'; - -const PAGE_STYLES = ` - table { - border-collapse: collapse; - margin-bottom: 1em; - } - - table, td, th { - border: 1px solid black; - } - - td { - padding: .5em; - } - - table.record-results tr:first-child { - font-weight: bold; - } - - textarea { - width: 100% - } - - fieldset.problem-select { - margin-top: 1em; - margin-left: 1em; - } - - div.problem-option-container.enabled { - margin-bottom: 0.5em; - } - - div.problem-option-container:last-child { - margin-bottom: 0; - } - - fieldset.assertions { - margin-bottom: 1em; - } - - label.assertion { - display: block; - } - - .required:not(.highlight-required) { - display: none; - } - - .required-other:not(.highlight-required) { - display: none; - } - - .required.highlight-required { - color: red; - } - - fieldset.highlight-required { - border-color: red; - } - - fieldset .highlight-required { - color: red; - } - - .off-screen { - position: absolute !important; - height: 1px; - width: 1px; - overflow: hidden; - clip: rect(1px, 1px, 1px, 1px); - white-space: nowrap; - } -`; - -let testRunIO = new TestRunInputOutput(); -testRunIO.setTitleInputFromTitle(document.title); -testRunIO.setUnexpectedInputFromBuiltin(); -testRunIO.setScriptsInputFromMap(typeof scripts === 'object' ? scripts : {}); - -/** - * @param {SupportJSON} newSupport - * @param {CommandsJSON} newCommandsData - * @param {AllCommandsJSON} allCommands - */ -export function initialize(newSupport, newCommandsData, allCommands) { - testRunIO.setSupportInputFromJSON(newSupport); - testRunIO.setAllCommandsInputFromJSON(allCommands); - testRunIO.setConfigInputFromQueryParamsAndSupport( - Array.from(new URL(document.location).searchParams) - ); - testRunIO.setKeysInputFromBuiltinAndConfig(); - testRunIO.setCommandsInputFromJSONAndConfigKeys(newCommandsData); -} - -/** - * @param {BehaviorJSON} atBehavior - */ -export function verifyATBehavior(atBehavior) { - if (testRunIO.behaviorInput !== null) { - throw new Error('Test files should only contain one verifyATBehavior call.'); - } - - testRunIO.setBehaviorInputFromJSONAndCommandsConfigKeysTitleUnexpected(atBehavior); -} - -export async function loadCollectedTestAsync(testRoot, testFileName) { - const collectedTestResponse = await fetch(`${testRoot}/${testFileName}`); - const collectedTestJson = await collectedTestResponse.json(); - - // v2 commands.json - const commandsJsonResponse = await fetch('../commands.json'); - if (commandsJsonResponse.ok) { - const commandsJson = await commandsJsonResponse.json(); - testRunIO.setAllCommandsInputFromJSON(commandsJson); - } - - await testRunIO.setInputsFromCollectedTestAsync(collectedTestJson, testRoot); - testRunIO.setConfigInputFromQueryParamsAndSupport([ - ['at', collectedTestJson.target.at.key], - ...Array.from(new URL(document.location).searchParams), - ]); - - displayInstructionsForBehaviorTest(); -} - -export function displayTestPageAndInstructions(testPage) { - if (document.readyState !== 'complete') { - window.setTimeout(() => { - displayTestPageAndInstructions(testPage); - }, 100); - return; - } - - testRunIO.setPageUriInputFromPageUri(testPage); - - document.querySelector('html').setAttribute('lang', 'en'); - var style = document.createElement('style'); - style.innerHTML = PAGE_STYLES; - document.head.appendChild(style); - - displayInstructionsForBehaviorTest(); -} - -function displayInstructionsForBehaviorTest() { - const windowManager = new TestWindow({ - ...testRunIO.testWindowOptions(), - hooks: { - windowOpened() { - app.dispatch(userOpenWindow()); - }, - windowClosed() { - app.dispatch(userCloseWindow()); - }, - }, - }); - - // First, execute necesary set up script in test page if the test page is open from a previous behavior test - windowManager.prepare(); - - const app = new TestRunExport({ - hooks: { - openTestPage() { - windowManager.open(); - }, - closeTestPage() { - windowManager.close(); - }, - postResults: () => postResults(testRunIO.resultJSON(app.state)), - }, - state: testRunIO.testRunState(), - resultsJSON: state => testRunIO.resultJSON(state), - }); - app.observe(() => { - render(document.body, renderVirtualTestPage(app.testPageAndResults())); - }); - render(document.body, renderVirtualTestPage(app.testPageAndResults())); - - // if test is loaded in iFrame - if (window.parent && window.parent.postMessage) { - // results can be submitted by parent posting a message to the - // iFrame with a data.type property of 'submit' - window.addEventListener('message', function (message) { - if (!validateMessage(message, 'submit')) return; - app.hooks.submit(); - }); - - // send message to parent that test has loaded - window.parent.postMessage( - { - type: 'loaded', - data: { - testPageUri: windowManager.pageUri, - }, - }, - '*' - ); - } -} - -function validateMessage(message, type) { - if (window.location.origin !== message.origin) { - return false; - } - if (!message.data || typeof message.data !== 'object') { - return false; - } - if (message.data.type !== type) { - return false; - } - return true; -} - -/** - * @param {resultsJSON} resultsJSON - */ -function postResults(resultsJSON) { - // send message to parent if test is loaded in iFrame - if (window.parent && window.parent.postMessage) { - window.parent.postMessage( - { - type: 'results', - data: resultsJSON, - }, - '*' - ); - } -} - -function bind(fn, ...args) { - return (...moreArgs) => fn(...args, ...moreArgs); -} - -const a = bind(element, 'a'); -const br = bind(element, 'br'); -const button = bind(element, 'button'); -const div = bind(element, 'div'); -const em = bind(element, 'em'); -const kbd = bind(element, 'kbd'); -const fieldset = bind(element, 'fieldset'); -const h1 = bind(element, 'h1'); -const h2 = bind(element, 'h2'); -const h3 = bind(element, 'h3'); -const hr = bind(element, 'hr'); -const input = bind(element, 'input'); -const label = bind(element, 'label'); -const select = bind(element, 'select'); -const option = bind(element, 'option'); -const legend = bind(element, 'legend'); -const li = bind(element, 'li'); -const ol = bind(element, 'ol'); -const p = bind(element, 'p'); -const script = bind(element, 'script'); -const section = bind(element, 'section'); -const span = bind(element, 'span'); -const table = bind(element, 'table'); -const td = bind(element, 'td'); -const textarea = bind(element, 'textarea'); -const th = bind(element, 'th'); -const tr = bind(element, 'tr'); -const ul = bind(element, 'ul'); - -const forInput = bind(attribute, 'for'); -const href = bind(attribute, 'href'); -const id = bind(attribute, 'id'); -const name = bind(attribute, 'name'); -const tabIndex = bind(attribute, 'tabindex'); -const textContent = bind(attribute, 'textContent'); -const type = bind(attribute, 'type'); -const ariaLabel = bind(attribute, 'aria-label'); -const ariaHidden = bind(attribute, 'aria-hidden'); - -const value = bind(property, 'value'); -const checked = bind(property, 'checked'); -const disabled = bind(property, 'disabled'); - -/** @type {(cb: (ev: MouseEvent) => void) => any} */ -const onclick = bind(property, 'onclick'); -/** @type {(cb: (ev: InputEvent) => void) => any} */ -const onchange = bind(property, 'onchange'); -/** @type {(cb: (ev: KeyboardEvent) => void) => any} */ -const onkeydown = bind(property, 'onkeydown'); - -/** - * @param {Description} value - */ -function rich(value) { - if (typeof value === 'string') { - return value; - } else if (Array.isArray(value)) { - return fragment(...value.map(rich)); - } else if (value.kbd) { - return kbd.bind(value.kbd)(rich(value.kbd)); - } else { - if ('whitespace' in value) { - if (value.whitespace === WhitespaceStyleMap.LINE_BREAK) { - return br(); - } - return null; - } - return (value.href ? a.bind(null, href(value.href)) : span)( - className([ - value.offScreen ? 'off-screen' : '', - value.required ? 'required' : '', - value.highlightRequired ? 'highlight-required' : '', - ]), - rich(value.description) - ); - } -} - -/** - * @param {TestPageAndResultsDocument} doc - */ -function renderVirtualTestPage(doc) { - return fragment( - 'instructions' in doc - ? div( - section( - id('errors'), - style({ display: doc.errors && doc.errors.visible ? 'block' : 'none' }), - h2(doc.errors ? doc.errors.header : ''), - ul( - ...(doc.errors && doc.errors.errors ? doc.errors.errors.map(error => li(error)) : []) - ), - hr() - ), - section(id('instructions'), renderVirtualInstructionDocument(doc.instructions)), - section(id('record-results')) - ) - : null, - 'results' in doc ? renderVirtualResultsTable(doc.results) : null, - doc.resultsJSON - ? script( - type('text/json'), - id('__ariaatharness__results__'), - textContent(JSON.stringify(doc.resultsJSON)) - ) - : null - ); -} - -/** - * @param doc {InstructionDocument} - */ -function renderVirtualInstructionDocument(doc) { - function compose(...fns) { - return around => fns.reduceRight((carry, fn) => fn(carry), around); - } - - const map = (ary, el) => ary.map(item => el(item)); - - return div( - instructionHeader(doc.instructions), - - instructCommands(doc.instructions.instructions), - - instructAssertions(doc.instructions.assertions), - - button( - disabled(!doc.instructions.openTestPage.enabled), - onclick(doc.instructions.openTestPage.click), - rich(doc.instructions.openTestPage.button) - ), - - resultHeader(doc.results.header), - - section(...doc.results.commands.map(commandResult)), - - doc.submit ? button(onclick(doc.submit.click), rich(doc.submit.button)) : null - ); - - /** - * @param {InstructionDocumentResultsHeader} param0 - */ - function resultHeader({ header, description }) { - return fragment(h2(rich(header)), p(rich(description))); - } - - /** - * @param {InstructionDocumentResultsCommand} command - * @param {number} commandIndex - */ - function commandResult(command, commandIndex) { - return fragment( - h3(rich(command.header)), - p( - label(rich(command.atOutput.description)), - textarea( - value(command.atOutput.value), - focus(command.atOutput.focus), - onchange(ev => - command.atOutput.change(/** @type {HTMLInputElement} */ (ev.currentTarget).value) - ) - ) - ), - fieldset( - className(['assertions']), - legend(rich(command.assertionsHeader.descriptionHeader)), - ...command.assertions.map(bind(commandResultAssertion, commandIndex)) - ), - ...[command.unexpectedBehaviors].map(bind(commandResultUnexpectedBehavior, commandIndex)) - ); - } - - /** - * @param {number} commandIndex - * @param {InstructionDocumentResultsCommandsUnexpected} unexpected - */ - function commandResultUnexpectedBehavior(commandIndex, unexpected) { - return fieldset( - id(`cmd-${commandIndex}-problem`), - rich(unexpected.description), - div( - radioChoice( - `problem-${commandIndex}-true`, - `problem-${commandIndex}`, - unexpected.passChoice - ) - ), - div( - radioChoice( - `problem-${commandIndex}-false`, - `problem-${commandIndex}`, - unexpected.failChoice - ) - ), - fieldset( - className(['problem-select']), - id(`cmd-${commandIndex}-problem-checkboxes`), - legend(rich(unexpected.failChoice.options.header)), - ...unexpected.failChoice.options.options.map(failOption => { - const failOptionId = failOption.description - .toLowerCase() - .replace(/[.,]/g, '') - .replace(/\s+/g, '-'); - - const undesirableBehaviorCheckbox = div( - input( - type('checkbox'), - value(failOption.description), - id(`${failOptionId}-${commandIndex}-checkbox`), - className([`undesirable-${commandIndex}`]), - disabled(!failOption.enabled), - checked(failOption.checked), - focus(failOption.focus), - onchange(ev => - failOption.change(/** @type {HTMLInputElement} */ (ev.currentTarget).checked) - ), - onkeydown(ev => { - if (failOption.keydown(ev.key)) { - ev.stopPropagation(); - ev.preventDefault(); - } - }) - ), - label( - id(`${failOptionId}-${commandIndex}-label`), - forInput(`${failOptionId}-${commandIndex}-checkbox`), - rich(`${failOption.description} behavior occurred`) - ) - ); - - const impactSelect = div( - className([!failOption.checked && 'off-screen']), - ariaHidden(!failOption.checked), - label(forInput(`${failOptionId}-${commandIndex}-impact`), rich('Impact:')), - select( - id(`${failOptionId}-${commandIndex}-impact`), - ariaLabel(`Impact for ${failOption.description}`), - option(UnexpectedBehaviorImpactMap.MODERATE), - option(UnexpectedBehaviorImpactMap.SEVERE), - disabled(!failOption.checked), - onchange(ev => - failOption.impactchange(/** @type {HTMLInputElement} */ (ev.currentTarget).value) - ) - ) - ); - - const detailsTextInput = div( - className([!failOption.checked && 'off-screen']), - ariaHidden(!failOption.checked), - label( - forInput(`${failOptionId}-${commandIndex}-details`), - rich(failOption.more.description) - ), - input( - type('text'), - id(`${failOptionId}-${commandIndex}-details`), - ariaLabel(`Details for ${failOption.description}`), - className(['undesirable-other-input']), - disabled(!failOption.more.enabled), - value(failOption.more.value), - onchange(ev => - failOption.more.change(/** @type {HTMLInputElement} */ (ev.currentTarget).value) - ) - ) - ); - - return div( - className(['problem-option-container', failOption.checked && 'enabled']), - undesirableBehaviorCheckbox, - impactSelect, - detailsTextInput - ); - }) - ) - ); - } - - /** - * @param {number} commandIndex - * @param {InstructionDocumentResultsCommandsAssertion} assertion - * @param {number} assertionIndex - */ - function commandResultAssertion(commandIndex, assertion, assertionIndex) { - return label( - className(['assertion']), - input( - type('checkbox'), - id(`cmd-${commandIndex}-${assertionIndex}`), - checked(assertion.passed === AssertionResultMap.PASS), - onclick(assertion.click) - ), - rich(assertion.description) - ); - } - - /** - * @param {string} idKey - * @param {string} nameKey - * @param {InstructionDocumentAssertionChoice} choice - */ - function radioChoice(idKey, nameKey, choice) { - return fragment( - input( - type('radio'), - id(idKey), - name(nameKey), - checked(choice.checked), - focus(choice.focus), - onclick(choice.click) - ), - label(id(`${idKey}-label`), forInput(`${idKey}`), rich(choice.label)) - ); - } - - /** - * @param {InstructionDocumentInstructionsInstructions} param0 - * @returns - */ - function instructCommands({ - header, - instructions, - strongInstructions: boldInstructions, - commands, - }) { - return fragment( - h2(rich(header)), - ol( - ...map(instructions, compose(li, rich)), - ...map(boldInstructions, compose(li, em, rich)), - li(rich(commands.description), ul(...map(commands.commands, compose(li, em, rich)))) - ) - ); - } - - /** - * @param {InstructionDocumentInstructions} param0 - */ - function instructionHeader({ header, description }) { - return fragment( - h1(id('behavior-header'), tabIndex('0'), focus(header.focus), rich(header.header)), - p(rich(description)) - ); - } - - /** - * @param {InstructionDocumentInstructionsAssertions} param0 - */ - function instructAssertions({ header, description, assertions }) { - return fragment( - h2(rich(header)), - p(rich(description)), - ol(...map(assertions, compose(li, em, rich))) - ); - } -} - -/** - * @param {ResultsTableDocument} results - */ -function renderVirtualResultsTable(results) { - return fragment( - h1(rich(results.header)), - h2(id('overallstatus'), rich(results.status.header)), - - table( - (({ description, support, details }) => tr(th(description), th(support), th(details)))( - results.table.headers - ), - results.table.commands.map( - ({ - description, - support, - details: { output, passingAssertions, failingAssertions, unexpectedBehaviors }, - }) => - fragment( - tr( - td(rich(description)), - td(rich(support)), - td( - p(rich(output)), - commandDetailsList(passingAssertions), - commandDetailsList(failingAssertions), - commandDetailsList(unexpectedBehaviors) - ) - ) - ) - ) - ) - ); - - /** - * @param {object} list - * @param {Description} list.description - * @param {Description[]} list.items - */ - function commandDetailsList({ description, items }) { - return div(description, ul(...items.map(description => li(rich(description))))); - } -} - -/** @typedef {import('./aria-at-test-io-format.mjs').SupportJSON} SupportJSON */ -/** @typedef {import('./aria-at-test-io-format.mjs').AllCommandsJSON} AllCommandsJSON */ -/** @typedef {import('./aria-at-test-io-format.mjs').CommandsJSON} CommandsJSON */ -/** @typedef {import('./aria-at-test-io-format.mjs').BehaviorJSON} BehaviorJSON */ - -/** @typedef {import('./aria-at-test-run.mjs').TestRunState} TestRunState */ - -/** @typedef {import('./aria-at-test-run.mjs').Description} Description */ - -/** @typedef {import('./aria-at-test-run.mjs').InstructionDocument} InstructionDocument */ -/** @typedef {import('./aria-at-test-run.mjs').InstructionDocumentInstructions} InstructionDocumentInstructions */ -/** @typedef {import('./aria-at-test-run.mjs').InstructionDocumentInstructionsAssertions} InstructionDocumentInstructionsAssertions */ -/** @typedef {import('./aria-at-test-run.mjs').InstructionDocumentResultsHeader} InstructionDocumentResultsHeader */ -/** @typedef {import('./aria-at-test-run.mjs').InstructionDocumentResultsCommand} InstructionDocumentResultsCommand */ -/** @typedef {import('./aria-at-test-run.mjs').InstructionDocumentResultsCommandsUnexpected} InstructionDocumentResultsCommandsUnexpected */ -/** @typedef {import("./aria-at-test-run.mjs").InstructionDocumentResultsCommandsAssertion} InstructionDocumentResultsCommandsAssertion */ -/** @typedef {import("./aria-at-test-run.mjs").InstructionDocumentAssertionChoice} InstructionDocumentAssertionChoice */ -/** @typedef {import("./aria-at-test-run.mjs").InstructionDocumentInstructionsInstructions} InstructionDocumentInstructionsInstructions */ - -/** @typedef {import('./aria-at-test-run.mjs').ResultsTableDocument} ResultsTableDocument */ - -/** - * @typedef {import('./aria-at-test-io-format.mjs').TestPageAndResultsDocument} TestPageAndResultsDocument - */ diff --git a/client/resources/aria-at-test-io-format.mjs b/client/resources/aria-at-test-io-format.mjs deleted file mode 100644 index 31991c975..000000000 --- a/client/resources/aria-at-test-io-format.mjs +++ /dev/null @@ -1,1878 +0,0 @@ -/// -/// -/// - -import { - AssertionResultMap, - CommonResultMap, - createEnumMap, - HasUnexpectedBehaviorMap, - TestRun, - UserActionMap, -} from './aria-at-test-run.mjs'; -import * as keysModule from './keys.mjs'; - -const UNEXPECTED_BEHAVIORS = [ - 'Output is excessively verbose, e.g., includes redundant and/or irrelevant speech', - 'Reading cursor position changed in an unexpected manner', - 'Screen reader became extremely sluggish', - 'Screen reader crashed', - 'Browser crashed', -]; - -/** Depends on ConfigInput. */ -class KeysInput { - /** - * @param {object} value - * @param {string} value.origin - * @param {{[KEY_ID: string]: string}} value.keys - * @param {ATJSON} value.at - * @param {{[atMode in ATMode]: string}} value.modeInstructions - * @private - */ - constructor(value) { - this.errors = []; - - /** @private */ - this._value = value; - } - - origin() { - return this._value.origin; - } - - /** - * @param {string} keyId - * @returns {string} - */ - keysForCommand(keyId) { - return this._value.keys[keyId]; - } - - /** - * @param {ATMode} atMode - */ - modeInstructions(atMode) { - if (this._value.modeInstructions[atMode]) { - return this._value.modeInstructions[atMode]; - } - return ''; - } - - /** - * @param {object} data - * @param {ConfigInput} data.configInput - */ - static fromBuiltinAndConfig({ configInput }) { - const keys = keysModule; - const atKey = configInput.at().key; - - invariant( - ['jaws', 'nvda', 'voiceover_macos'].includes(atKey), - '%s is one of "jaws", "nvda", or "voiceover_macos"', - atKey - ); - - return new KeysInput({ - origin: 'resources/keys.mjs', - keys, - at: atKey, - modeInstructions: { - reading: { - jaws: `Verify the Virtual Cursor is active by pressing ${keys.ALT_DELETE}. If it is not, exit Forms Mode to activate the Virtual Cursor by pressing ${keys.ESC}.`, - nvda: `Ensure NVDA is in browse mode by pressing ${keys.ESC}. Note: This command has no effect if NVDA is already in browse mode.`, - voiceover_macos: `Toggle Quick Nav ON by pressing the ${keys.LEFT} and ${keys.RIGHT} keys at the same time.`, - }[atKey], - interaction: { - jaws: `Verify the PC Cursor is active by pressing ${keys.ALT_DELETE}. If it is not, turn off the Virtual Cursor by pressing ${keys.INS_Z}.`, - nvda: `If NVDA did not make the focus mode sound when the test page loaded, press ${keys.INS_SPACE} to turn focus mode on.`, - voiceover_macos: `Toggle Quick Nav OFF by pressing the ${keys.LEFT} and ${keys.RIGHT} keys at the same time.`, - }[atKey], - }, - }); - } - - /** @param {AriaATFile.CollectedTest} collectedTest */ - static fromCollectedTest(collectedTest) { - return new KeysInput({ - origin: 'test.collected.json', - keys: collectedTest.commands.reduce((carry, { keypresses }) => { - return keypresses.reduce((carry, { id, keystroke }) => { - carry[id] = keystroke; - return carry; - }, carry); - }, {}), - at: collectedTest.target.at.key, - modeInstructions: collectedTest.instructions.mode, - }); - } -} - -class SupportInput { - /** - * @param {SupportJSON} value - * @private - */ - constructor(value) { - this.errors = []; - - /** @private */ - this._value = value; - } - - defaultAT() { - return this._value.ats[0]; - } - - /** - * @param {string} atKey - * @returns {ATJSON | undefined} - */ - findAT(atKey) { - const lowercaseATKey = atKey.toLowerCase(); - return this._value.ats.find(({ key }) => key === lowercaseATKey); - } - - /** - * @param {SupportJSON} json - */ - static fromJSON(json) { - return new SupportInput(json); - } - - /** - * @param {AriaATFile.CollectedTest} collectedTest - */ - static fromCollectedTest(collectedTest) { - return new SupportInput({ - ats: [ - typeof collectedTest.target.at.raw === 'object' - ? collectedTest.target.at.raw - : { key: collectedTest.target.at.key, name: collectedTest.target.at.name }, - ], - applies_to: {}, - examples: [], - }); - } -} - -class AllCommandsInput { - /** - * @param {AllCommandsJSON} value - * @private - */ - constructor(value) { - this.errors = []; - - /** @private */ - this._value = value; - - /** @private */ - this._flattened = this.flattenObject(this._value); - } - - flattenObject(obj, parentKey) { - const flattened = {}; - - for (const key in obj) { - if (typeof obj[key] === 'object') { - const subObject = this.flattenObject(obj[key], parentKey + key + '.'); - Object.assign(flattened, subObject); - } else { - flattened[parentKey + key] = obj[key]; - } - } - - return flattened; - } - - findValueByKey(keyToFind) { - const keys = Object.keys(this._flattened); - - // Need to specially handle VO modifier key combination - if (keyToFind === 'vo') - return this.findValuesByKeys([this._flattened['modifierAliases.vo']])[0]; - - if (keyToFind.includes('modifiers.') || keyToFind.includes('keys.')) { - const parts = keyToFind.split('.'); - const keyToCheck = parts[parts.length - 1]; // value after the '.' - - if (this._flattened[keyToFind]) - return { - value: this._flattened[keyToFind], - key: keyToCheck, - }; - - return null; - } - - for (const key of keys) { - const parts = key.split('.'); - const parentKey = parts[0]; - const keyToCheck = parts[parts.length - 1]; // value after the '.' - - if (keyToCheck === keyToFind) { - if (parentKey === 'modifierAliases') { - return this.findValueByKey(`modifiers.${this._flattened[key]}`); - } else if (parentKey === 'keyAliases') { - return this.findValueByKey(`keys.${this._flattened[key]}`); - } - - return { - value: this._flattened[key], - key: keyToCheck, - }; - } - } - - // Return null if the key is not found - return null; - } - - findValuesByKeys(keysToFind = []) { - const result = []; - - const patternSepWithReplacement = (keyToFind, pattern, replacement) => { - if (keyToFind.includes(pattern)) { - let value = ''; - let validKeys = true; - const keys = keyToFind.split(pattern); - - for (const key of keys) { - const keyResult = this.findValueByKey(key); - if (keyResult) - value = value ? `${value}${replacement}${keyResult.value}` : keyResult.value; - else validKeys = false; - } - if (validKeys) return { value, key: keyToFind }; - } - - return null; - }; - - const patternSepHandler = keyToFind => { - let value = ''; - - if (keyToFind.includes(' ') && keyToFind.includes('+')) { - const keys = keyToFind.split(' '); - for (let [index, key] of keys.entries()) { - const keyToFindResult = this.findValueByKey(key); - if (keyToFindResult) keys[index] = keyToFindResult.value; - if (key.includes('+')) keys[index] = patternSepWithReplacement(key, '+', '+').value; - } - value = keys.join(' then '); - - return { value, key: keyToFind }; - } else if (keyToFind.includes(' ')) - return patternSepWithReplacement(keyToFind, ' ', ' then '); - else if (keyToFind.includes('+')) return patternSepWithReplacement(keyToFind, '+', '+'); - }; - - for (const keyToFind of keysToFind) { - if (keyToFind.includes(' ') || keyToFind.includes('+')) { - result.push(patternSepHandler(keyToFind)); - } else { - const keyToFindResult = this.findValueByKey(keyToFind); - if (keyToFindResult) result.push(keyToFindResult); - } - } - - return result; - } - - static fromJSON(json) { - return new AllCommandsInput(json); - } -} - -/** Depends on ConfigInput and KeysInput. */ -class CommandsInput { - /** - * @param {object} value - * @param {CommandsJSON} value.commands - * @param {ATJSON} value.at - * @param {KeysInput} keysInput - * @param {AllCommandsInput} allCommandsInput - * @private - */ - constructor(value, keysInput, allCommandsInput) { - this.errors = []; - - /** @private */ - this._value = value; - - /** @private */ - this._keysInput = keysInput; - - this._allCommandsInput = allCommandsInput; - } - - /** - * @param {object} config - * @param {string} config.task - * @param {ATMode} mode - * @returns {string[]} - */ - getCommands({ task }, mode) { - if (mode === 'reading' || mode === 'interaction') { - const v1Commands = this.getCommandsV1(task, mode); - return { - commands: v1Commands, - commandsAndSettings: v1Commands.map(command => ({ command })), - }; - } else { - return this.getCommandsV2({ task }, mode); - } - } - - getCommandsV1(task, mode) { - const assistiveTech = this._value.at; - - if (!this._value.commands[task]) { - throw new Error( - `Task "${task}" does not exist, please add to at-commands or correct your spelling.` - ); - } else if (!this._value.commands[task][mode]) { - throw new Error( - `Mode "${mode}" instructions for task "${task}" does not exist, please add to at-commands or correct your spelling.` - ); - } - - let commandsData = this._value.commands[task][mode][assistiveTech.key] || []; - let commands = []; - - for (let c of commandsData) { - let innerCommands = []; - let commandSequence = c[0].split(','); - for (let command of commandSequence) { - command = this._keysInput.keysForCommand(command); - if (typeof command === 'undefined') { - throw new Error( - `Key instruction identifier "${c}" for AT "${assistiveTech.name}", mode "${mode}", task "${task}" is not an available identified. Update you commands.json file to the correct identifier or add your identifier to resources/keys.mjs.` - ); - } - - let furtherInstruction = c[1]; - command = furtherInstruction ? `${command} ${furtherInstruction}` : command; - innerCommands.push(command); - } - commands.push(innerCommands.join(', then ')); - } - - return commands; - } - - getCommandsV2({ task }, mode) { - const assistiveTech = this._value.at; - let commandsAndSettings = []; - let commands = []; - - // Mode could be in the format of mode1_mode2 - // If they are from the same AT, this needs to return the function in the format of [ [[commands], settings], [[commands], settings], ... ] - for (const _atMode of mode.split('_')) { - if (assistiveTech.settings[_atMode] || _atMode === 'defaultMode') { - const [atMode] = deriveModeWithTextAndInstructions(_atMode, assistiveTech); - - if (!this._value.commands[task]) { - throw new Error( - `Task "${task}" does not exist, please add to at-commands or correct your spelling.` - ); - } else if (!this._value.commands[task][atMode]) { - throw new Error( - `Mode "${atMode}" instructions for task "${task}" does not exist, please add to at-commands or correct your spelling.` - ); - } - - let commandsData = this._value.commands[task][atMode][assistiveTech.key] || []; - for (let commandSequence of commandsData) { - for (const commandWithPresentationNumber of commandSequence) { - const [commandId, presentationNumber] = commandWithPresentationNumber.split('|'); - - let command; - const foundCommandKV = this._allCommandsInput.findValuesByKeys([commandId]); - if (!foundCommandKV.length) command = undefined; - else { - const { value } = this._allCommandsInput.findValuesByKeys([commandId])[0]; - command = value; - } - - if (typeof command === 'undefined') { - throw new Error( - `Key instruction identifier "${commandSequence}" for AT "${assistiveTech.name}", mode "${atMode}", task "${task}" is not an available identified. Update your commands.json file to the correct identifier or add your identifier to resources/keys.mjs.` - ); - } - - commands.push(command); - commandsAndSettings.push({ - command, - commandId, - presentationNumber: Number(presentationNumber), - settings: _atMode, - settingsText: assistiveTech.settings?.[_atMode]?.screenText || 'default mode active', - settingsInstructions: assistiveTech.settings?.[_atMode]?.instructions || [ - assistiveTech.defaultConfigurationInstructionsHTML, - ], - }); - } - } - } - } - - return { commands, commandsAndSettings }; - } - - /** - * @param {CommandsJSON} json - * @param {object} data - * @param {ConfigInput} data.configInput - * @param {KeysInput} data.keysInput - */ - static fromJSONAndConfigKeys(json, { configInput, keysInput, allCommandsInput }) { - return new CommandsInput({ commands: json, at: configInput.at() }, keysInput, allCommandsInput); - } - - /** - * @param {AriaATFile.CollectedTest} collectedTest - * @param {object} data - * @param {KeysInput} data.keysInput - */ - static fromCollectedTestKeys(collectedTest, { keysInput, allCommandsInput }) { - let settingsForTest = {}; - - // For v2 test format - const settings = collectedTest.target.at.settings; - if (settings) { - for (const _atMode of settings.split('_')) { - settingsForTest[_atMode] = { - // Use settings attribute to verify in filter if available - [collectedTest.target.at.key]: collectedTest.commands - .filter(({ settings }) => (settings ? settings === _atMode : true)) - .map(({ id, extraInstruction }) => (extraInstruction ? [id, extraInstruction] : [id])), - }; - } - } else { - settingsForTest = { - [collectedTest.target.mode]: { - [collectedTest.target.at.key]: collectedTest.commands.map(({ id, extraInstruction }) => - extraInstruction ? [id, extraInstruction] : [id] - ), - }, - }; - } - - return new CommandsInput( - { - commands: { - [collectedTest.info.task || collectedTest.info.testId]: settingsForTest, - }, - at: - typeof collectedTest.target.at.raw === 'object' - ? collectedTest.target.at.raw - : collectedTest.target.at, - }, - keysInput, - allCommandsInput - ); - } -} - -/** - * Depends on SupportInput. - */ -class ConfigInput { - /** - * @param {string[]} errors - * @param {object} value - * @param {ATJSON} value.at - * @param {boolean} value.displaySubmitButton - * @param {boolean} value.renderResultsAfterSubmit - * @param {"SubmitResultsJSON" | "TestResultJSON"} value.resultFormat - * @param {AriaATTestResult.JSON | null} value.resultJSON - * @private - */ - constructor(errors, value) { - this.errors = errors; - - /** @private */ - this._value = value; - } - - at() { - return this._value.at; - } - - displaySubmitButton() { - return this._value.displaySubmitButton; - } - - renderResultsAfterSubmit() { - return this._value.renderResultsAfterSubmit; - } - - resultFormat() { - return this._value.resultFormat; - } - - resultJSON() { - return this._value.resultJSON; - } - - /** - * @param {ConfigQueryParams} queryParams - * @param {object} data - * @param {SupportInput} data.supportInput - */ - static fromQueryParamsAndSupport(queryParams, { supportInput }) { - const errors = []; - - let at = supportInput.defaultAT(); - let displaySubmitButton = true; - let renderResultsAfterSubmit = true; - let resultFormat = 'SubmitResultsJSON'; - let resultJSON = null; - - for (const [key, value] of queryParams) { - if (key === 'at') { - const requestedAT = value; - const knownAt = supportInput.findAT(requestedAT); - if (knownAt) { - at = knownAt; - } else { - errors.push( - `Harness does not have commands for the requested assistive technology ('${requestedAT}'), showing commands for assistive technology '${at.name}' instead. To test '${requestedAT}', please contribute command mappings to this project.` - ); - } - } else if (key === 'showResults') { - displaySubmitButton = decodeBooleanParam(value, displaySubmitButton); - } else if (key === 'showSubmitButton') { - renderResultsAfterSubmit = decodeBooleanParam(value, renderResultsAfterSubmit); - } else if (key === 'resultFormat') { - if (value !== 'SubmitResultsJSON' && value !== 'TestResultJSON') { - errors.push( - `resultFormat can be 'SubmitResultsJSON' or 'TestResultJSON'. '${value}' is not supported.` - ); - continue; - } - resultFormat = value; - } else if (key === 'resultJSON') { - try { - resultJSON = JSON.parse(value); - } catch (error) { - errors.push(`Failed to parse resultJSON: ${error.message}`); - } - } - } - - if (resultJSON && resultFormat !== 'TestResultJSON') { - errors.push(`resultJSON requires resultFormat to be set to 'TestResultJSON'.`); - resultJSON = null; - } - - return new ConfigInput(errors, { - at, - displaySubmitButton, - renderResultsAfterSubmit, - resultFormat, - resultJSON, - }); - - /** - * @param {string} param - * @param {boolean} defaultValue - * @returns {boolean} - */ - function decodeBooleanParam(param, defaultValue) { - if (param === 'true') { - return true; - } else if (param === 'false') { - return false; - } - return defaultValue; - } - } -} - -class ScriptsInput { - /** - * @param {object} value - * @param {SetupScripts} value.scripts - * @private - */ - constructor(value) { - this.errors = []; - - /** @private */ - this._value = value; - } - - scripts() { - return this._value.scripts; - } - - /** - * @param {SetupScripts} scripts - */ - static fromScriptsMap(scripts) { - return new ScriptsInput({ scripts }); - } - - /** - * @param {{source: string}} script - * @private - */ - static scriptsFromSource(script) { - return { [script.name]: new Function('testPageDocument', script.source) }; - } - - /** - * @param {{modulePath: string}} script - * @param {string} dataUrl - * @private - */ - static async scriptsFromModuleAsync(script, dataUrl) { - return await import(`${dataUrl}/${script.modulePath}`); - } - - /** - * @param {{jsonpPath: string}} script - * @param {string} dataUrl - * @private - */ - static async scriptsFromJsonpAsync(script, dataUrl) { - return await Promise.race([ - new Promise(resolve => { - window.scriptsJsonpLoaded = resolve; - const scriptTag = document.createElement('script'); - scriptTag.src = script.jsonpPath; - document.body.appendChild(scriptTag); - }), - new Promise((_, reject) => - setTimeout(() => reject(new Error('Loading scripts timeout error')), 10000) - ), - ]); - } - - /** - * @param {AriaATFile.CollectedTest} collectedAsync - * @param {string} dataUrl url to directory where CollectedTest was loaded from - */ - static async fromCollectedTestAsync({ target: { setupScript } }, dataUrl) { - if (!setupScript) { - return new ScriptsInput({ scripts: {} }); - } - try { - return new ScriptsInput({ scripts: ScriptsInput.scriptsFromSource(setupScript) }); - } catch (error) { - try { - return new ScriptsInput({ - scripts: await ScriptsInput.scriptsFromModuleAsync(setupScript, dataUrl), - }); - } catch (error2) { - try { - return new ScriptsInput({ - scripts: await ScriptsInput.scriptsFromJsonpAsync(setupScript, dataUrl), - }); - } catch (error3) { - throw new Error( - [error, error2, error3].map(error => error.stack || error.message).join('\n\n') - ); - } - } - } - } -} - -class UnexpectedInput { - /** - * @param {object} value - * @param {BehaviorUnexpectedItem[]} value.behaviors - * @private - */ - constructor(value) { - this.errors = []; - - this._value = value; - } - - behaviors() { - return this._value.behaviors; - } - - static fromBuiltin() { - return new UnexpectedInput({ - behaviors: [ - ...UNEXPECTED_BEHAVIORS.map(description => ({ description })), - { description: 'Other' }, - ], - }); - } -} - -class TitleInput { - /** - * @param {object} value - * @param {string} value.title - * @private - */ - constructor(value) { - this.errors = []; - - /** @private */ - this._value = value; - } - - title() { - return this._value.title; - } - - /** @param {string} title */ - static fromTitle(title) { - return new TitleInput({ - title, - }); - } -} - -/** Depends on CommandsInput, ConfigInput, KeysInput, TitleInput, and UnexpectedInput. */ -class BehaviorInput { - /** - * @param {object} value - * @param {Behavior} value.behavior - * @private - */ - constructor(value) { - this.errors = []; - - /** @private */ - this._value = value; - } - - behavior() { - return this._value.behavior; - } - - /** - * @param {BehaviorJSON} json - * @param {object} data - * @param {KeysInput} data.keysInput - * @param {CommandsInput} data.commandsInput - * @param {ConfigInput} data.configInput - * @param {UnexpectedInput} data.unexpectedInput - * @param {TitleInput} data.titleInput - */ - static fromJSONCommandsConfigKeysTitleUnexpected( - json, - { commandsInput, configInput, keysInput, titleInput, unexpectedInput } - ) { - const mode = Array.isArray(json.mode) ? json.mode[0] : json.mode; - const at = configInput.at(); - - const { commandsAndSettings } = commandsInput.getCommands({ task: json.task }, mode); - - // Use to determine assertionExceptions - const commandsInfo = json.commandsInfo?.[at.key]; - - return new BehaviorInput({ - behavior: { - description: titleInput.title(), - task: json.task, - mode, - modeInstructions: keysInput.modeInstructions(mode), - appliesTo: json.applies_to, - specificUserInstruction: json.specific_user_instruction, - setupScriptDescription: json.setup_script_description, - setupTestPage: json.setupTestPage, - assertionResponseQuestion: json.assertionResponseQuestion, - commands: commandsAndSettings.map(cs => { - const foundCommandInfo = commandsInfo?.find( - c => - cs.commandId === c.command && - cs.presentationNumber === c.presentationNumber && - cs.settings === c.settings - ); - if (!foundCommandInfo || !foundCommandInfo.assertionExceptions) return cs; - - // Only works for v2 - let assertionExceptions = json.output_assertions.map(each => each.assertionId); - foundCommandInfo.assertionExceptions.split(' ').forEach(each => { - let [priority, assertionId] = each.split(':'); - const index = assertionExceptions.findIndex(each => each === assertionId); - - priority = Number(priority); - assertionExceptions[index] = priority; - }); - // Preserve default priority or update with exception - assertionExceptions = assertionExceptions.map((each, index) => - isNaN(each) ? json.output_assertions[index].priority : each - ); - - return { ...cs, assertionExceptions }; - }), - assertions: (json.output_assertions ? json.output_assertions : []).map(assertion => { - // Tuple array [ priorityNumber, assertionText ] - if (Array.isArray(assertion)) { - return { - priority: Number(assertion[0]), - assertion: assertion[1], - }; - } - - // { assertionId, priority, assertionStatement, assertionPhrase, refIds, tokenizedAssertionStatements, tokenizedAssertionPhrases } - return { - priority: assertion.priority, - assertion: - assertion.tokenizedAssertionStatements?.[at.key] || assertion.assertionStatement, - }; - }), - additionalAssertions: (json.additional_assertions - ? json.additional_assertions[at.key] || [] - : [] - ).map(assertionTuple => ({ - priority: Number(assertionTuple[0]), - assertion: assertionTuple[1], - })), - unexpectedBehaviors: unexpectedInput.behaviors(), - }, - }); - } - - /** - * @param {AriaATFile.CollectedTest} collectedTest - * @param {object} data - * @param {CommandsInput} data.commandsInput - * @param {KeysInput} data.keysInput - * @param {UnexpectedInput} data.unexpectedInput - */ - static fromCollectedTestCommandsKeysUnexpected( - { info, target, instructions, assertions, commands }, - { commandsInput, keysInput, unexpectedInput } - ) { - // v1:info.task, v2: info.testId | v1:target.mode, v2:target.at.settings - const { commandsAndSettings } = commandsInput.getCommands( - { task: info.task || info.testId }, - target.mode || target.at.settings - ); - - return new BehaviorInput({ - behavior: { - description: info.title, - task: info.task || info.testId, - mode: target.mode || target.at.settings, - modeInstructions: instructions.mode, - appliesTo: [target.at.name], - specificUserInstruction: instructions.raw || instructions.instructions, - setupScriptDescription: target.setupScript ? target.setupScript.description : '', - setupTestPage: target.setupScript ? target.setupScript.name : undefined, - commands: commandsAndSettings.map(cs => { - const foundCommandInfo = commands.find( - c => cs.commandId === c.id && cs.settings === c.settings - ); - if (!foundCommandInfo || !foundCommandInfo.assertionExceptions) return cs; - - // Only works for v2 - let assertionExceptions = assertions.map(each => each.assertionId); - foundCommandInfo.assertionExceptions.forEach(each => { - let { priority, assertionId } = each; - const index = assertionExceptions.findIndex(each => each === assertionId); - - priority = Number(priority); - assertionExceptions[index] = priority; - }); - // Preserve default priority or update with exception - assertionExceptions = assertionExceptions.map((each, index) => - isNaN(each) ? assertions[index].priority : each - ); - - return { ...cs, assertionExceptions }; - }), - assertions: assertions.map( - ({ priority, expectation, assertionStatement, tokenizedAssertionStatements }) => { - let assertion = tokenizedAssertionStatements - ? tokenizedAssertionStatements[target.at.key] - : null; - assertion = assertion || expectation || assertionStatement; - - return { - priority, - assertion, - }; - } - ), - additionalAssertions: [], - unexpectedBehaviors: unexpectedInput.behaviors(), - }, - }); - } -} - -class PageUriInput { - /** - * @param {object} value - * @param {string} value.pageUri - * @private - */ - constructor(value) { - this._errors = []; - this._value = value; - } - - pageUri() { - return this._value.pageUri; - } - - /** - * @param {string} pageUri - */ - static fromPageUri(pageUri) { - return new PageUriInput({ pageUri }); - } -} - -export class TestRunInputOutput { - constructor() { - /** @type {BehaviorInput} */ - this.behaviorInput = null; - /** @type {CommandsInput} */ - this.commandsInput = null; - /** @type {ConfigInput} */ - this.configInput = null; - /** @type {KeysInput} */ - this.keysInput = null; - /** @type {PageUriInput} */ - this.pageUriInput = null; - /** @type {ScriptsInput} */ - this.scriptsInput = null; - /** @type {SupportInput} */ - this.supportInput = null; - /** @type {AllCommandsInput} */ - this.allCommandsInput = null; - /** @type {TitleInput} */ - this.titleInput = null; - /** @type {UnexpectedInput} */ - this.unexpectedInput = null; - } - - /** @param {BehaviorInput} behaviorInput */ - setBehaviorInput(behaviorInput) { - this.behaviorInput = behaviorInput; - } - - /** @param {BehaviorJSON} behaviorJSON */ - setBehaviorInputFromJSONAndCommandsConfigKeysTitleUnexpected(behaviorJSON) { - invariant( - this.commandsInput !== null, - 'Call %s or %s before calling %s.', - this.setCommandsInput.name, - this.setCommandsInputFromJSONAndConfigKeys.name, - this.setBehaviorInputFromJSONAndCommandsConfigKeysTitleUnexpected.name - ); - invariant( - this.configInput !== null, - 'Call %s or %s before calling %s.', - this.setConfigInput.name, - this.setConfigInputFromQueryParamsAndSupport.name, - this.setBehaviorInputFromJSONAndCommandsConfigKeysTitleUnexpected.name - ); - invariant( - this.keysInput !== null, - 'Call %s or %s before calling %s.', - this.setKeysInput.name, - this.setKeysInputFromBuiltinAndConfig.name, - this.setBehaviorInputFromJSONAndCommandsConfigKeysTitleUnexpected.name - ); - invariant( - this.titleInput !== null, - 'Call %s or %s before calling %s.', - this.setTitleInput.name, - this.setTitleInputFromTitle.name, - this.setBehaviorInputFromJSONAndCommandsConfigKeysTitleUnexpected.name - ); - invariant( - this.unexpectedInput !== null, - 'Call %s or %s before calling %s.', - this.setUnexpectedInput.name, - this.setUnexpectedInputFromBuiltin.name, - this.setBehaviorInputFromJSONAndCommandsConfigKeysTitleUnexpected.name - ); - - this.setBehaviorInput( - BehaviorInput.fromJSONCommandsConfigKeysTitleUnexpected(behaviorJSON, { - commandsInput: this.commandsInput, - configInput: this.configInput, - keysInput: this.keysInput, - titleInput: this.titleInput, - unexpectedInput: this.unexpectedInput, - }) - ); - } - - /** - * Set all inputs but ConfigInput. - * @param {AriaATFile.CollectedTest} collectedTest - * @param {string} dataUrl url to directory where CollectedTest was loaded from - */ - async setInputsFromCollectedTestAsync(collectedTest, dataUrl) { - const pageUriInput = PageUriInput.fromPageUri(collectedTest.target.referencePage); - const titleInput = TitleInput.fromTitle(collectedTest.info.title); - const supportInput = SupportInput.fromCollectedTest(collectedTest); - const scriptsInput = await ScriptsInput.fromCollectedTestAsync(collectedTest, dataUrl); - - const unexpectedInput = UnexpectedInput.fromBuiltin(); - const keysInput = KeysInput.fromCollectedTest(collectedTest); - const allCommandsInput = this.allCommandsInput; - const commandsInput = CommandsInput.fromCollectedTestKeys(collectedTest, { - keysInput, - allCommandsInput, - }); - const behaviorInput = BehaviorInput.fromCollectedTestCommandsKeysUnexpected(collectedTest, { - commandsInput, - keysInput, - unexpectedInput, - }); - - this.setTitleInput(titleInput); - this.setPageUriInput(pageUriInput); - this.setSupportInput(supportInput); - this.setScriptsInput(scriptsInput); - - this.setUnexpectedInput(unexpectedInput); - this.setKeysInput(keysInput); - this.setCommandsInput(commandsInput); - this.setBehaviorInput(behaviorInput); - } - - /** @param {CommandsInput} commandsInput */ - setCommandsInput(commandsInput) { - this.commandsInput = commandsInput; - } - - /** @param {CommandsJSON} commandsJSON */ - setCommandsInputFromJSONAndConfigKeys(commandsJSON) { - invariant( - this.configInput !== null, - 'Call %s or %s before calling %s.', - this.setConfigInput.name, - this.setConfigInputFromQueryParamsAndSupport.name, - this.setCommandsInputFromJSONAndConfigKeys.name - ); - invariant( - this.keysInput !== null, - 'Call %s or %s before calling %s.', - this.setKeysInput.name, - this.setKeysInputFromBuiltinAndConfig.name, - this.setCommandsInputFromJSONAndConfigKeys.name - ); - - this.setCommandsInput( - CommandsInput.fromJSONAndConfigKeys(commandsJSON, { - configInput: this.configInput, - keysInput: this.keysInput, - allCommandsInput: this.allCommandsInput, - }) - ); - } - - /** @param {ConfigInput} configInput */ - setConfigInput(configInput) { - this.configInput = configInput; - } - - /** @param {ConfigQueryParams} queryParams */ - setConfigInputFromQueryParamsAndSupport(queryParams) { - invariant( - this.supportInput !== null, - 'Call %s or %s before calling %s.', - this.setSupportInput.name, - this.setSupportInputFromJSON.name, - this.setConfigInputFromQueryParamsAndSupport.name - ); - - this.setConfigInput( - ConfigInput.fromQueryParamsAndSupport(queryParams, { - supportInput: this.supportInput, - }) - ); - } - - /** @param {KeysInput} keysInput */ - setKeysInput(keysInput) { - this.keysInput = keysInput; - } - - setKeysInputFromBuiltinAndConfig() { - invariant( - this.configInput !== null, - 'Call %s or %s before calling %s.', - this.setConfigInput.name, - this.setConfigInputFromQueryParamsAndSupport.name, - this.setCommandsInputFromJSONAndConfigKeys.name - ); - - this.setKeysInput(KeysInput.fromBuiltinAndConfig({ configInput: this.configInput })); - } - - /** @param {PageUriInput} pageUriInput */ - setPageUriInput(pageUriInput) { - this.pageUriInput = pageUriInput; - } - - /** @param {string} pageUri */ - setPageUriInputFromPageUri(pageUri) { - this.setPageUriInput(PageUriInput.fromPageUri(pageUri)); - } - - /** @param {ScriptsInput} scriptsInput */ - setScriptsInput(scriptsInput) { - this.scriptsInput = scriptsInput; - } - - /** @param {SetupScripts} scriptsMap */ - setScriptsInputFromMap(scriptsMap) { - this.setScriptsInput(ScriptsInput.fromScriptsMap(scriptsMap)); - } - - /** @param {SupportInput} supportInput */ - setSupportInput(supportInput) { - this.supportInput = supportInput; - } - - /** @param {SupportJSON} supportJSON */ - setSupportInputFromJSON(supportJSON) { - this.setSupportInput(SupportInput.fromJSON(supportJSON)); - } - - /** @param {AllCommandsInput} allCommandsInput */ - setAllCommandsInput(allCommandsInput) { - this.allCommandsInput = allCommandsInput; - } - - /** @param {AllCommandsJSON} allCommandsJSON */ - setAllCommandsInputFromJSON(allCommandsJSON) { - this.setAllCommandsInput(AllCommandsInput.fromJSON(allCommandsJSON)); - } - - /** @param {TitleInput} titleInput */ - setTitleInput(titleInput) { - this.titleInput = titleInput; - } - - /** @param {string} title */ - setTitleInputFromTitle(title) { - this.setTitleInput(TitleInput.fromTitle(title)); - } - - /** @param {UnexpectedInput} unexpectedInput */ - setUnexpectedInput(unexpectedInput) { - this.unexpectedInput = unexpectedInput; - } - - setUnexpectedInputFromBuiltin() { - this.setUnexpectedInput(UnexpectedInput.fromBuiltin()); - } - - /** @returns {AriaATTestRun.State} */ - testRunState() { - invariant( - this.behaviorInput !== null, - 'Call %s or %s before calling %s.', - this.setBehaviorInput.name, - this.setBehaviorInputFromJSONAndCommandsConfigKeysTitleUnexpected.name, - this.testRunState.name - ); - invariant( - this.configInput !== null, - 'Call %s or %s before calling %s.', - this.setConfigInput.name, - this.setConfigInputFromQueryParamsAndSupport.name, - this.testRunState.name - ); - - const errors = [ - ...this.behaviorInput.errors, - ...this.commandsInput.errors, - ...this.configInput.errors, - ]; - const test = this.behaviorInput.behavior(); - const config = this.configInput; - - function unescapeHTML(input) { - const textarea = document.createElement('textarea'); - textarea.innerHTML = input; - return textarea.value; - } - - const [atMode, screenText, instructions] = deriveModeWithTextAndInstructions( - test.mode, - config.at() - ); - - let state = { - errors, - info: { - description: test.description, - task: test.task, - mode: screenText || atMode, - modeInstructions: Array.isArray(instructions) - ? unescapeHTML(`${instructions[0]} ${instructions[1]}`) - : test.modeInstructions, - userInstructions: test.specificUserInstruction.split('|'), - setupScriptDescription: test.setupScriptDescription, - }, - config: { - at: config.at(), - displaySubmitButton: config.displaySubmitButton(), - renderResultsAfterSubmit: config.renderResultsAfterSubmit(), - }, - currentUserAction: UserActionMap.LOAD_PAGE, - openTest: { - enabled: true, - }, - assertionResponseQuestion: test.assertionResponseQuestion, - commands: test.commands.map( - command => - /** @type {import("./aria-at-test-run.mjs").TestRunCommand} */ ({ - description: command.command, - commandSettings: { - command: command.command, - description: command.settings, - text: command.settingsText, - instructions: command.settingsInstructions, - assertionExceptions: command.assertionExceptions, - }, - atOutput: { - highlightRequired: false, - value: '', - }, - assertions: test.assertions.map(assertion => ({ - description: assertion.assertion, - highlightRequired: false, - priority: assertion.priority, - result: CommonResultMap.NOT_SET, - })), - additionalAssertions: test.additionalAssertions.map(assertion => ({ - description: assertion.assertion, - highlightRequired: false, - priority: assertion.priority, - result: CommonResultMap.NOT_SET, - })), - unexpected: { - highlightRequired: false, - hasUnexpected: HasUnexpectedBehaviorMap.NOT_SET, - tabbedBehavior: 0, - behaviors: test.unexpectedBehaviors.map(({ description }) => ({ - description, - checked: false, - impact: UnexpectedBehaviorImpactMap.MODERATE, - more: { highlightRequired: false, value: '' }, - })), - }, - }) - ), - }; - - if (this.configInput.resultJSON()) { - state = this.testRunStateFromTestResultJSON(this.configInput.resultJSON(), state); - } - - return state; - } - - testWindowOptions() { - invariant( - this.behaviorInput !== null, - 'Call %s or %s before calling %s.', - this.setBehaviorInput.name, - this.setBehaviorInputFromJSONAndCommandsConfigKeysTitleUnexpected.name, - this.testWindowOptions.name - ); - invariant( - this.pageUriInput !== null, - 'Call %s or %s before calling %s.', - this.setPageUriInput.name, - this.setPageUriInputFromPageUri.name, - this.testWindowOptions.name - ); - invariant( - this.scriptsInput !== null, - 'Call %s or %s before calling %s.', - this.setScriptsInput.name, - this.setScriptsInputFromMap.name, - this.testWindowOptions.name - ); - - return { - pageUri: this.pageUriInput.pageUri(), - setupScriptName: this.behaviorInput.behavior().setupTestPage, - scripts: this.scriptsInput.scripts(), - }; - } - - /** - * @param {AriaATTestRun.State} state - * @returns {import("./aria-at-harness.mjs").SubmitResultJSON} - */ - submitResultsJSON(state) { - invariant( - this.behaviorInput !== null, - 'Call %s or %s before calling %s.', - this.setBehaviorInput.name, - this.setBehaviorInputFromJSONAndCommandsConfigKeysTitleUnexpected.name, - this.submitResultsJSON.name - ); - - const behavior = this.behaviorInput.behavior(); - - /** @type {SubmitResultDetailsJSON} */ - const details = { - name: state.info.description, - task: state.info.task, - specific_user_instruction: behavior.specificUserInstruction, - summary: { - 1: { - pass: countAssertions( - ({ priority, result }) => priority === 1 && result === CommonResultMap.PASS - ), - fail: countAssertions( - ({ priority, result }) => priority === 1 && result !== CommonResultMap.PASS - ), - }, - 2: { - pass: countAssertions( - ({ priority, result }) => priority === 2 && result === CommonResultMap.PASS - ), - fail: countAssertions( - ({ priority, result }) => priority === 2 && result !== CommonResultMap.PASS - ), - }, - unexpectedCount: countUnexpectedBehaviors(({ checked }) => checked), - }, - commands: state.commands.map(command => ({ - command: command.description, - output: command.atOutput.value, - support: commandSupport(command), - assertions: [...command.assertions, ...command.additionalAssertions].map( - assertionToAssertion - ), - unexpected_behaviors: command.unexpected.behaviors - .filter(({ checked }) => checked) - .map(({ description, more }) => (more ? more.value : description)), - })), - }; - - /** @type {SubmitResultStatusJSON} */ - const status = state.commands - .map(commandSupport) - .some(support => support === CommandSupportJSONMap.FAILING) - ? StatusJSONMap.FAIL - : StatusJSONMap.PASS; - - return { - test: state.info.description, - details, - status, - }; - - function commandSupport(command) { - const allAssertions = [...command.assertions, ...command.additionalAssertions]; - return allAssertions.some( - ({ priority, result }) => priority === 1 && result !== CommonResultMap.PASS - ) || command.unexpected.behaviors.some(({ checked }) => checked) - ? CommandSupportJSONMap.FAILING - : allAssertions.some( - ({ priority, result }) => priority === 2 && result !== CommonResultMap.PASS - ) - ? CommandSupportJSONMap.ALL_REQUIRED - : CommandSupportJSONMap.FULL; - } - - /** - * @param {(assertion: TestRunAssertion | TestRunAdditionalAssertion) => boolean} filter - * @returns {number} - */ - function countAssertions(filter) { - return state.commands.reduce( - (carry, command) => - carry + [...command.assertions, ...command.additionalAssertions].filter(filter).length, - 0 - ); - } - - /** - * @param {(behavior: TestRunUnexpected) => boolean} filter - * @returns {number} - */ - function countUnexpectedBehaviors(filter) { - return state.commands.reduce( - (carry, command) => carry + command.unexpected.behaviors.filter(filter).length, - 0 - ); - } - - /** - * @param {TestRunAssertion | TestRunAdditionalAssertion} assertion - * @returns {SubmitResultAssertionsJSON} - */ - function assertionToAssertion(assertion) { - return assertion.result === CommonResultMap.PASS - ? { - assertion: assertion.description, - priority: assertion.priority.toString(), - pass: AssertionPassJSONMap.GOOD_OUTPUT, - } - : { - assertion: assertion.description, - priority: assertion.priority.toString(), - fail: - assertion.result === AssertionResultMap.FAIL_MISSING - ? AssertionFailJSONMap.NO_OUTPUT - : assertion.result === AssertionResultMap.FAIL_INCORRECT - ? AssertionFailJSONMap.INCORRECT_OUTPUT - : AssertionFailJSONMap.NO_SUPPORT, - }; - } - } - - /** - * Transform a test run state into a test result json for serialization. - * @param {AriaATTestRun.State} state - * @returns {AriaATTestResult.JSON} - */ - testResultJSON(state) { - return { - test: { - title: state.info.description, - at: { - id: state.config.at.key, - }, - atMode: state.info.mode, - }, - scenarioResults: state.commands.map(command => ({ - scenario: { - command: { - id: command.description, - }, - }, - output: command.atOutput.value, - assertionResults: command.assertions.map(assertion => ({ - assertion: { - priority: assertion.priority === 1 ? 'REQUIRED' : 'OPTIONAL', - text: assertion.description, - }, - passed: assertion.result === 'pass', - failedReason: - assertion.result === 'failIncorrect' - ? 'INCORRECT_OUTPUT' - : assertion.result === 'failMissing' - ? 'NO_OUTPUT' - : null, - })), - unexpectedBehaviors: command.unexpected.behaviors - .map(behavior => - behavior.checked - ? { - text: behavior.description, - impact: behavior.impact, - details: behavior.more.value, - } - : null - ) - .filter(Boolean), - })), - }; - } - - /** - * @param {AriaATTestRun.State} state - * @returns {SubmitResultJSON | AriaATTestResult.JSON} - */ - resultJSON(state) { - // If ConfigInput is available and resultFormat is TestResultJSON return result in that format. - if (this.configInput !== null) { - const resultFormat = this.configInput.resultFormat(); - if (resultFormat === 'TestResultJSON') { - return this.testResultJSON(state); - } - } - - return this.submitResultsJSON(state); - } - - /** - * Set a default or given test run state with the recorded results json. Intermediate state not stored into - * testResult, like highlightRequired, is to the default. - * @param {AriaATTestResult.JSON} testResult - * @param {AriaATTestRun.State} [state] - * @returns {AriaATTestRun.State} - */ - testRunStateFromTestResultJSON(testResult, state = this.testRunState()) { - return { - ...state, - commands: state.commands.map((command, commandIndex) => { - const scenarioResult = testResult.scenarioResults[commandIndex]; - return { - ...command, - atOutput: { highlightRequired: false, value: scenarioResult.output }, - assertions: command.assertions.map((assertion, assertionIndex) => { - const assertionResult = scenarioResult.assertionResults[assertionIndex]; - return { - ...assertion, - highlightRequired: false, - result: assertionResult.passed - ? 'pass' - : assertionResult.failedReason === 'INCORRECT_OUTPUT' - ? 'failIncorrect' - : assertionResult.failedReason === 'NO_OUTPUT' - ? 'failMissing' - : 'fail', - }; - }), - unexpected: { - ...command.unexpected, - highlightRequired: false, - hasUnexpected: - scenarioResult.unexpectedBehaviors.length > 0 - ? 'hasUnexpected' - : 'doesNotHaveUnexpected', - tabbedBehavior: 0, - behaviors: command.unexpected.behaviors.map(behavior => { - const behaviorResult = scenarioResult.unexpectedBehaviors.find( - unexpectedResult => unexpectedResult.text === behavior.description - ); - return { - ...behavior, - checked: behaviorResult ? true : false, - more: behavior.more - ? { - highlightRequired: false, - impact: behaviorResult - ? behavior.impact - : UnexpectedBehaviorImpactMap.MODERATE, - value: behaviorResult ? behaviorResult.details : '', - } - : behavior.more, - }; - }), - }, - }; - }), - }; - } -} - -/** - * Extended TestRun that can access methods to turn the TestRun["state"] into - * the desired output format. - */ -export class TestRunExport extends TestRun { - /** - * @param {TestRunOptions & TestRunExportOptions} options - */ - constructor({ resultsJSON, ...parentOptions }) { - super(parentOptions); - - this.resultsJSON = resultsJSON; - } - - testPageAndResults() { - const testPage = this.testPage(); - if ('results' in testPage) { - return { - ...testPage, - resultsJSON: this.resultsJSON(this.state), - }; - } - return { - ...testPage, - resultsJSON: - this.state.currentUserAction === UserActionMap.CLOSE_TEST_WINDOW - ? this.resultsJSON(this.state) - : null, - }; - } -} - -/** - * @typedef SubmitResultDetailsCommandsAssertionsPass - * @property {string} assertion - * @property {string} priority - * @property {AssertionPassJSON} pass - */ - -/** - * Passing assertion values submitted from the tester result form. - * - * In the submitted json object the values contain spaces and are title cased. - * @typedef {EnumValues} AssertionPassJSON - */ - -const AssertionPassJSONMap = createEnumMap({ - GOOD_OUTPUT: 'Good Output', - PASS: 'Pass', -}); - -/** - * @typedef SubmitResultDetailsCommandsAssertionsFail - * @property {string} assertion - * @property {string} priority - * @property {AssertionFailJSON} fail - */ - -/** - * Failing assertion values from the tester result form as are submitted in the - * JSON result object. - * - * In the submitted json object the values contain spaces and are title cased. - * @typedef {EnumValues} AssertionFailJSON - */ - -const AssertionFailJSONMap = createEnumMap({ - NO_OUTPUT: 'No Output', - INCORRECT_OUTPUT: 'Incorrect Output', - NO_SUPPORT: 'No Support', - FAIL: 'Fail', -}); - -const UnexpectedBehaviorImpactMap = createEnumMap({ - MODERATE: 'Moderate', - SEVERE: 'Severe', -}); - -/** @typedef {SubmitResultDetailsCommandsAssertionsPass | SubmitResultDetailsCommandsAssertionsFail} SubmitResultAssertionsJSON */ - -/** - * Command result derived from priority 1 and 2 assertions. - * - * Support is "FAILING" is priority 1 assertions fail. Support is "ALL REQUIRED" - * if priority 2 assertions fail. - * - * In the submitted json object values may contain spaces and are in ALL CAPS. - * - * @typedef {EnumValues} CommandSupportJSON - */ - -const CommandSupportJSONMap = createEnumMap({ - FULL: 'FULL', - FAILING: 'FAILING', - ALL_REQUIRED: 'ALL REQUIRED', -}); - -/** - * Highest level status submitted from test result. - * - * In the submitted json object values are in ALL CAPS. - * - * @typedef {EnumValues} SubmitResultStatusJSON - */ - -const StatusJSONMap = createEnumMap({ - PASS: 'PASS', - FAIL: 'FAIL', -}); - -/** - * - * @param {ATMode} mode - * @param {ATJSON} at - * @returns {[ATMode, string, [string]]} - */ -function deriveModeWithTextAndInstructions(mode, at) { - let atMode = mode; - let screenText = ''; - let instructions = []; - - if (mode.includes('_')) { - const atModes = mode.split('_'); - for (const _atMode of atModes) { - if (at.settings[_atMode]) { - atMode = _atMode; - screenText = at.settings[_atMode].screenText; - instructions = at.settings[_atMode].instructions; - } - } - } else { - if (at.settings && at.settings[atMode]) { - screenText = at.settings[atMode]?.screenText; - instructions = at.settings[atMode]?.instructions; - } - } - - return [atMode, screenText, instructions]; -} - -/** - * @param {boolean} test - * @param {string} message - * @param {any[]} args - * @returns {asserts test} - */ -function invariant(test, message, ...args) { - if (!test) { - let index = 0; - throw new Error(message.replace(/%%|%\w/g, match => (match[0] !== '%%' ? args[index++] : '%'))); - } -} - -/** @typedef {ConstructorParameters[0]} TestRunOptions */ -/** - * @typedef TestRunExportOptions - * @property {(state: AriaATTestRun.State) => SubmitResultJSON} resultsJSON - */ - -/** - * @typedef ATJSON - * @property {string} name - * @property {string} key - * @property {string} defaultConfigurationInstructionsHTML - * @property {object} settings - */ - -/** - * @typedef SupportJSON - * @property {ATJSON[]} ats - * @property {object} applies_to - * @property {object[]} examples - * @property {string} examples[].directory - * @property {string} examples[].name - */ - -/** - * @typedef AllCommandsJSON - * @property {object} modifiers - * @property {object} modifierAliases - * @property {object} keys - * @property {object} keyAliases - */ - -/** - * @typedef {([string] | [string, string])[]} CommandATJSON - */ - -/** - * @typedef {{[atMode: string]: CommandATJSON}} CommandModeJSON - */ - -/** - * @typedef CommandJSON - * @property {CommandModeJSON} [reading] - * @property {CommandModeJSON} [interaction] - */ - -/** - * @typedef {{[commandDescription: string]: CommandJSON}} CommandsJSON - */ - -/** - * @typedef {["at" | "showSubmitButton" | "showResults" | string, string][]} ConfigQueryParams - */ - -/** @typedef {"reading" | "interaction" | "virtualCursor", "pcCursor", "browseMode" | "focusMode" | "quickNavOn" | "quickNavOff" | "defaultMode"} ATMode */ - -/** @typedef OutputAssertion - * @property {string} assertionId - * @property {Number} priority - * @property {string} assertionStatement - * @property {string} assertionPhrase - * @property {string} refIds - */ - -/** - * @typedef BehaviorJSON - * @property {string} setup_script_description - * @property {string} setupTestPage - * @property {string[]} applies_to - * @property {ATMode | ATMode[]} mode - * @property {string} task - * @property {string} specific_user_instruction - * @property {[string, string][] | [OutputAssertion]} [output_assertions] - * @property {{[atKey: string]: [number, string][]}} [additional_assertions] - */ - -/** - * @typedef BehaviorAssertion - * @property {number} priority - * @property {string} assertion - */ - -/** - * @typedef BehaviorUnexpectedItem - * @property {string} description - */ - -/** - * @typedef Behavior - * @property {string} description - * @property {string} task - * @property {ATMode} mode - * @property {string} modeInstructions - * @property {string[]} appliesTo - * @property {string} specificUserInstruction - * @property {string} setupScriptDescription - * @property {string} setupTestPage - * @property {string[]} commands - * @property {BehaviorAssertion[]} assertions - * @property {BehaviorAssertion[]} additionalAssertions - * @property {BehaviorUnexpectedItem[]} unexpectedBehaviors - */ - -/** @typedef {{[key: string]: (document: Document) => void}} SetupScripts */ - -/** - * @typedef SubmitResultJSON - * @property {string} test - * @property {SubmitResultDetailsJSON} details - * @property {SubmitResultStatusJSON} status - */ - -/** - * @typedef SubmitResultSummaryPriorityJSON - * @property {number} pass - * @property {number} fail - */ - -/** - * @typedef {{[key in "1" | "2"]: SubmitResultSummaryPriorityJSON}} SubmitResultSummaryPriorityMapJSON - */ - -/** - * @typedef SubmitResultSummaryPropsJSON - * @property {number} unexpectedCount - */ - -/** - * @typedef {SubmitResultSummaryPriorityMapJSON & SubmitResultSummaryPropsJSON} SubmitResultSummaryJSON - */ - -/** - * @typedef SubmitResultDetailsJSON - * @property {string} name - * @property {string} specific_user_instruction - * @property {string} task - * @property {object[]} commands - * @property {string} commands[].command - * @property {string} commands[].output - * @property {string[]} commands[].unexpected_behaviors - * @property {CommandSupportJSON} commands[].support - * @property {SubmitResultAssertionsJSON[]} commands[].assertions - * @property {SubmitResultSummaryJSON} summary - */ - -/** - * @typedef ResultJSONDocument - * @property {SubmitResultJSON | null} resultsJSON - */ - -/** - * @typedef {TestPageDocument & ResultJSONDocument} TestPageAndResultsDocument - */ - -/** - * @typedef {import('./aria-at-test-run.mjs').EnumValues} EnumValues - * @template T - */ - -/** @typedef {import('./aria-at-test-run.mjs').TestRunAssertion} TestRunAssertion */ -/** @typedef {import('./aria-at-test-run.mjs').TestRunAdditionalAssertion} TestRunAdditionalAssertion */ -/** @typedef {import('./aria-at-test-run.mjs').TestRunCommand} TestRunCommand */ -/** @typedef {import("./aria-at-test-run.mjs").TestRunUnexpectedBehavior} TestRunUnexpected */ - -/** @typedef {import('./aria-at-test-run.mjs').TestPageDocument} TestPageDocument */ diff --git a/client/resources/aria-at-test-run.mjs b/client/resources/aria-at-test-run.mjs deleted file mode 100644 index c45a3e179..000000000 --- a/client/resources/aria-at-test-run.mjs +++ /dev/null @@ -1,1336 +0,0 @@ -export class TestRun { - /** - * @param {object} param0 - * @param {Partial} [param0.hooks] - * @param {TestRunState} param0.state - */ - constructor({ hooks, state }) { - /** @type {TestRunState} */ - this.state = state; - - const bindDispatch = transform => arg => this.dispatch(transform(arg)); - /** @type {TestRunHooks} */ - this.hooks = { - closeTestPage: bindDispatch(userCloseWindow), - focusCommandUnexpectedBehavior: bindDispatch(userFocusCommandUnexpectedBehavior), - openTestPage: bindDispatch(userOpenWindow), - postResults: () => {}, - setCommandAdditionalAssertion: bindDispatch(userChangeCommandAdditionalAssertion), - setCommandAssertion: bindDispatch(userChangeCommandAssertion), - setCommandHasUnexpectedBehavior: bindDispatch(userChangeCommandHasUnexpectedBehavior), - setCommandUnexpectedBehavior: bindDispatch(userChangeCommandUnexpectedBehavior), - setCommandUnexpectedBehaviorImpact: bindDispatch(userChangeCommandUnexpectedBehaviorImpact), - setCommandUnexpectedBehaviorMore: bindDispatch(userChangeCommandUnexpectedBehaviorMore), - setCommandOutput: bindDispatch(userChangeCommandOutput), - submit: () => submitResult(this), - ...hooks, - }; - - this.observers = []; - - this.dispatch = this.dispatch.bind(this); - } - - /** - * @param {(state: TestRunState) => TestRunState} updateMethod - */ - dispatch(updateMethod) { - this.state = updateMethod(this.state); - this.observers.forEach(subscriber => subscriber(this)); - } - - /** - * @param {(app: TestRun) => void} subscriber - * @returns {() => void} - */ - observe(subscriber) { - this.observers.push(subscriber); - return () => { - const index = this.observers.indexOf(subscriber); - if (index > -1) { - this.observers.splice(index, 1); - } - }; - } - - testPage() { - return testPageDocument(this.state, this.hooks); - } - - instructions() { - return instructionDocument(this.state, this.hooks); - } - - resultsTable() { - return resultsTableDocument(this.state); - } -} - -/** - * @param {U} map - * @returns {Readonly} - * @template {string} T - * @template {{[key: string]: T}} U - */ -export function createEnumMap(map) { - return Object.freeze(map); -} - -export const WhitespaceStyleMap = createEnumMap({ - LINE_BREAK: 'lineBreak', -}); - -function bind(fn, ...args) { - return (...moreArgs) => fn(...args, ...moreArgs); -} - -/** - * @param {TestRunState} resultState - * @param {TestRunHooks} hooks - * @returns {InstructionDocument} - */ -export function instructionDocument(resultState, hooks) { - const mode = resultState.info.mode; - const modeInstructions = resultState.info.modeInstructions; - const userInstructions = resultState.info.userInstructions; - const lastInstruction = userInstructions[userInstructions.length - 1]; - const setupScriptDescription = resultState.info.setupScriptDescription - ? ` and runs a script that ${resultState.info.setupScriptDescription}.` - : resultState.info.setupScriptDescription; - // As a hack, special case mode instructions for VoiceOver for macOS until we - // support modeless tests. - const modePhrase = - resultState.config.at.name === 'VoiceOver for macOS' - ? 'Describe ' - : `With ${resultState.config.at.name} in ${mode} mode, describe `; - - // TODO: Wrap each command token in - const commands = resultState.commands.map(({ description }) => description); - const commandSettings = resultState.commands.map(({ commandSettings }) => commandSettings); - const assertions = resultState.commands[0].assertions.map(({ description }) => description); - const additionalAssertions = resultState.commands[0].additionalAssertions.map( - ({ description }) => description - ); - - let firstRequired = true; - function focusFirstRequired() { - if (firstRequired) { - firstRequired = false; - return true; - } - return false; - } - - function convertModeInstructionsToKbdArray(inputString) { - const container = document.createElement('div'); - container.innerHTML = inputString; - - const resultArray = []; - for (const node of container.childNodes) { - if (node.nodeName === 'KBD') { - // Handle elements - resultArray.push({ kbd: node.innerText.trim() }); - } else { - // Handle text nodes - resultArray.push(node.textContent); - } - } - - return resultArray.length ? resultArray : null; - } - - const convertedModeInstructions = - modeInstructions !== undefined && !modeInstructions.includes('undefined') - ? convertModeInstructionsToKbdArray(modeInstructions) - : null; - - let strongInstructions = [...userInstructions]; - if (convertedModeInstructions) - strongInstructions = [convertedModeInstructions, ...strongInstructions]; - - return { - errors: { - visible: resultState.errors && resultState.errors.length > 0 ? true : false, - header: 'Test cannot be performed due to error(s)!', - errors: resultState.errors || [], - }, - instructions: { - header: { - header: `Testing task: ${resultState.info.description}`, - focus: resultState.currentUserAction === UserActionMap.LOAD_PAGE, - }, - description: `${modePhrase} how ${resultState.config.at.name} behaves when performing task "${lastInstruction}"`, - instructions: { - header: 'Test instructions', - instructions: [ - [ - `Restore default settings for ${resultState.config.at.name}. For help, read `, - { - href: 'https://github.com/w3c/aria-at/wiki/Configuring-Screen-Readers-for-Testing', - description: 'Configuring Screen Readers for Testing', - }, - `.`, - ], - `Activate the "Open test page" button below, which opens the example to test in a new window${setupScriptDescription}`, - ], - strongInstructions: strongInstructions.filter(el => el), - commands: { - description: `Using the following commands, ${lastInstruction}`, - commands: commands.map((command, index) => { - const { description: settings, text: settingsText } = commandSettings[index]; - return `${command}${ - settingsText && settings !== 'defaultMode' ? ` (${settingsText})` : '' - }`; - }), - }, - }, - assertions: { - header: 'Success Criteria', - description: `To pass this test, ${resultState.config.at.name} needs to meet all the following assertions when each specified command is executed:`, - assertions, - }, - openTestPage: { - button: 'Open Test Page', - enabled: resultState.openTest.enabled, - click: hooks.openTestPage, - }, - }, - results: { - header: { - header: 'Record Results', - description: `${resultState.info.description}`, - }, - commands: commands.map(commandResult), - }, - submit: resultState.config.displaySubmitButton - ? { - button: 'Submit Results', - click: hooks.submit, - } - : null, - }; - - /** - * @param {string} command - * @param {number} commandIndex - * @returns {InstructionDocumentResultsCommand} - */ - function commandResult(command, commandIndex) { - const resultStateCommand = resultState.commands[commandIndex]; - const resultUnexpectedBehavior = resultStateCommand.unexpected; - - const { - commandSettings: { description: settings, text: settingsText, assertionExceptions }, - } = resultStateCommand; - - return { - header: `After '${command}'${ - settingsText && settings !== 'defaultMode' ? ` (${settingsText})` : '' - }`, - atOutput: { - description: [ - `${resultState.config.at.name} output after ${command}`, - { - required: true, - highlightRequired: resultStateCommand.atOutput.highlightRequired, - description: '(required)', - }, - ], - value: resultStateCommand.atOutput.value, - focus: - resultState.currentUserAction === 'validateResults' && - resultStateCommand.atOutput.highlightRequired && - focusFirstRequired(), - change: atOutput => hooks.setCommandOutput({ commandIndex, atOutput }), - }, - assertionsHeader: { - descriptionHeader: `${resultState.assertionResponseQuestion} ${command}${ - settingsText && settings !== 'defaultMode' ? ` (${settingsText})` : '' - }?`, - }, - assertions: [ - ...assertions - // Ignore assertion if level 0 priority exception found for assertion's command - .filter((each, index) => (assertionExceptions ? assertionExceptions[index] !== 0 : each)) - .map(each => - assertionResult( - commandIndex, - each, - assertions.findIndex(e => e === each) - ) - ), - ...additionalAssertions.map(bind(additionalAssertionResult, commandIndex)), - ], - unexpectedBehaviors: { - description: [ - 'Were there additional undesirable behaviors?', - { - required: true, - highlightRequired: resultStateCommand.unexpected.highlightRequired, - description: '(required)', - }, - ], - passChoice: { - label: 'No, there were no additional undesirable behaviors.', - checked: - resultUnexpectedBehavior.hasUnexpected === - HasUnexpectedBehaviorMap.DOES_NOT_HAVE_UNEXPECTED, - focus: - resultState.currentUserAction === 'validateResults' && - resultUnexpectedBehavior.highlightRequired && - resultUnexpectedBehavior.hasUnexpected === HasUnexpectedBehaviorMap.NOT_SET && - focusFirstRequired(), - click: () => - hooks.setCommandHasUnexpectedBehavior({ - commandIndex, - hasUnexpected: HasUnexpectedBehaviorMap.DOES_NOT_HAVE_UNEXPECTED, - }), - }, - failChoice: { - label: 'Yes, there were additional undesirable behaviors', - checked: - resultUnexpectedBehavior.hasUnexpected === HasUnexpectedBehaviorMap.HAS_UNEXPECTED, - focus: - resultState.currentUserAction === 'validateResults' && - resultUnexpectedBehavior.highlightRequired && - resultUnexpectedBehavior.hasUnexpected === HasUnexpectedBehaviorMap.NOT_SET && - focusFirstRequired(), - click: () => - hooks.setCommandHasUnexpectedBehavior({ - commandIndex, - hasUnexpected: HasUnexpectedBehaviorMap.HAS_UNEXPECTED, - }), - options: { - header: 'Undesirable behaviors', - options: resultUnexpectedBehavior.behaviors.map((behavior, unexpectedIndex) => { - return { - description: behavior.description, - impact: behavior.impact, - enabled: - resultUnexpectedBehavior.hasUnexpected === - HasUnexpectedBehaviorMap.HAS_UNEXPECTED, - tabbable: resultUnexpectedBehavior.tabbedBehavior === unexpectedIndex, - checked: behavior.checked, - focus: - typeof resultState.currentUserAction === 'object' && - resultState.currentUserAction.action === UserObjectActionMap.FOCUS_UNDESIRABLE - ? resultState.currentUserAction.commandIndex === commandIndex && - resultUnexpectedBehavior.tabbedBehavior === unexpectedIndex - : resultState.currentUserAction === UserActionMap.VALIDATE_RESULTS && - resultUnexpectedBehavior.hasUnexpected === - HasUnexpectedBehaviorMap.HAS_UNEXPECTED && - resultUnexpectedBehavior.behaviors.every(({ checked }) => !checked) && - focusFirstRequired(), - change: checked => - hooks.setCommandUnexpectedBehavior({ commandIndex, unexpectedIndex, checked }), - impactchange: impact => - hooks.setCommandUnexpectedBehaviorImpact({ - commandIndex, - unexpectedIndex, - impact, - }), - keydown: key => { - const increment = keyToFocusIncrement(key); - if (increment) { - hooks.focusCommandUnexpectedBehavior({ - commandIndex, - unexpectedIndex, - increment, - }); - return true; - } - return false; - }, - more: { - description: /** @type {Description[]} */ ([ - `Details:`, - { - required: true, - highlightRequired: behavior.more.highlightRequired, - description: '(required)', - }, - ]), - enabled: behavior.checked, - value: behavior.more.value, - focus: - resultState.currentUserAction === 'validateResults' && - behavior.more.highlightRequired && - focusFirstRequired(), - change: value => - hooks.setCommandUnexpectedBehaviorMore({ - commandIndex, - unexpectedIndex, - more: value, - }), - }, - }; - }), - }, - }, - }, - }; - } - - /** - * @param {number} commandIndex - * @param {string} assertion - * @param {number} assertionIndex - */ - function assertionResult(commandIndex, assertion, assertionIndex) { - const resultAssertion = resultState.commands[commandIndex].assertions[assertionIndex]; - return /** @type {InstructionDocumentResultsCommandsAssertion} */ ({ - description: [assertion], - passed: resultAssertion.result === AssertionResultMap.PASS, - click: () => - hooks.setCommandAssertion({ - commandIndex, - assertionIndex, - result: - resultAssertion.result === AssertionResultMap.PASS - ? AssertionResultMap.FAIL - : AssertionResultMap.PASS, - }), - }); - } - - /** - * @param {number} commandIndex - * @param {string} assertion - * @param {number} assertionIndex - */ - function additionalAssertionResult(commandIndex, assertion, assertionIndex) { - const resultAdditionalAssertion = - resultState.commands[commandIndex].additionalAssertions[assertionIndex]; - return /** @type {InstructionDocumentResultsCommandsAssertion} */ ({ - description: [assertion], - passed: resultAdditionalAssertion.result === CommonResultMap.PASS, - click: () => - hooks.setCommandAssertion({ - commandIndex, - assertionIndex, - result: - resultAdditionalAssertion.result === AssertionResultMap.PASS - ? AssertionResultMap.FAIL - : AssertionResultMap.PASS, - }), - }); - } -} - -/** - * @typedef {typeof UserActionMap[keyof typeof UserActionMap]} UserAction - */ - -export const UserActionMap = createEnumMap({ - LOAD_PAGE: 'loadPage', - OPEN_TEST_WINDOW: 'openTestWindow', - CLOSE_TEST_WINDOW: 'closeTestWindow', - VALIDATE_RESULTS: 'validateResults', - CHANGE_TEXT: 'changeText', - CHANGE_SELECTION: 'changeSelection', - SHOW_RESULTS: 'showResults', -}); - -/** - * @typedef {typeof UserObjectActionMap[keyof typeof UserObjectActionMap]} UserObjectAction - */ - -export const UserObjectActionMap = createEnumMap({ - FOCUS_UNDESIRABLE: 'focusUndesirable', -}); - -/** - * @typedef {UserAction | UserActionFocusUnexpected} TestRunUserAction - */ - -/** - * @typedef {EnumValues} HasUnexpectedBehavior - */ - -export const HasUnexpectedBehaviorMap = createEnumMap({ - NOT_SET: 'notSet', - HAS_UNEXPECTED: 'hasUnexpected', - DOES_NOT_HAVE_UNEXPECTED: 'doesNotHaveUnexpected', -}); - -export const CommonResultMap = createEnumMap({ - NOT_SET: 'notSet', - PASS: 'pass', -}); - -/** - * @typedef {EnumValues} AdditionalAssertionResult - */ - -export const AdditionalAssertionResultMap = createEnumMap({ - ...CommonResultMap, - FAIL_SUPPORT: 'failSupport', -}); - -/** - * @typedef {EnumValues} AssertionResult - */ - -export const AssertionResultMap = createEnumMap({ - ...CommonResultMap, - FAIL_MISSING: 'failMissing', - FAIL_INCORRECT: 'failIncorrect', - FAIL: 'fail', -}); - -/** - * @typedef {EnumValues} UnexpectedBehaviorImpact - */ - -export const UnexpectedBehaviorImpactMap = createEnumMap({ - MODERATE: 'Moderate', - SEVERE: 'Severe', -}); - -/** - * @param {object} props - * @param {number} props.commandIndex - * @param {string} props.atOutput - * @returns {(state: TestRunState) => TestRunState} - */ -export function userChangeCommandOutput({ commandIndex, atOutput }) { - return function (state) { - return { - ...state, - currentUserAction: UserActionMap.CHANGE_TEXT, - commands: state.commands.map((commandState, index) => - index !== commandIndex - ? commandState - : { - ...commandState, - atOutput: { - ...commandState.atOutput, - value: atOutput, - }, - } - ), - }; - }; -} - -/** - * @param {object} props - * @param {number} props.commandIndex - * @param {number} props.assertionIndex - * @param {AssertionResult} props.result - * @returns {(state: TestRunState) => TestRunState} - */ -export function userChangeCommandAssertion({ commandIndex, assertionIndex, result }) { - return function (state) { - return { - ...state, - currentUserAction: UserActionMap.CHANGE_SELECTION, - commands: state.commands.map((command, commandI) => - commandI !== commandIndex - ? command - : { - ...command, - assertions: command.assertions.map((assertion, assertionI) => - assertionI !== assertionIndex ? assertion : { ...assertion, result } - ), - } - ), - }; - }; -} - -/** - * @param {object} props - * @param {number} props.commandIndex - * @param {number} props.additionalAssertionIndex - * @param {AdditionalAssertionResult} props.result - * @returns {(state: TestRunState) => TestRunState} - */ -export function userChangeCommandAdditionalAssertion({ - commandIndex, - additionalAssertionIndex, - result, -}) { - return function (state) { - return { - ...state, - currentUserAction: UserActionMap.CHANGE_SELECTION, - commands: state.commands.map((command, commandI) => - commandI !== commandIndex - ? command - : { - ...command, - additionalAssertions: command.additionalAssertions.map((assertion, assertionI) => - assertionI !== additionalAssertionIndex ? assertion : { ...assertion, result } - ), - } - ), - }; - }; -} - -/** - * @param {object} props - * @param {number} props.commandIndex - * @param {HasUnexpectedBehavior} props.hasUnexpected - * @returns {(state: TestRunState) => TestRunState} - */ -export function userChangeCommandHasUnexpectedBehavior({ commandIndex, hasUnexpected }) { - return function (state) { - return { - ...state, - currentUserAction: UserActionMap.CHANGE_SELECTION, - commands: state.commands.map((command, commandI) => - commandI !== commandIndex - ? command - : { - ...command, - unexpected: { - ...command.unexpected, - hasUnexpected: hasUnexpected, - tabbedBehavior: hasUnexpected === HasUnexpectedBehaviorMap.HAS_UNEXPECTED ? 0 : -1, - behaviors: command.unexpected.behaviors.map(behavior => ({ - ...behavior, - checked: false, - more: behavior.more ? { ...behavior.more, value: '' } : null, - })), - }, - } - ), - }; - }; -} - -/** - * @param {object} props - * @param {number} props.commandIndex - * @param {number} props.unexpectedIndex - * @param {boolean} props.checked - * @returns {(state: TestRunState) => TestRunState} - */ -export function userChangeCommandUnexpectedBehavior({ commandIndex, unexpectedIndex, checked }) { - return function (state) { - return { - ...state, - currentUserAction: UserActionMap.CHANGE_SELECTION, - commands: state.commands.map((command, commandI) => - commandI !== commandIndex - ? command - : { - ...command, - unexpected: { - ...command.unexpected, - behaviors: command.unexpected.behaviors.map((unexpected, unexpectedI) => - unexpectedI !== unexpectedIndex - ? unexpected - : { - ...unexpected, - checked, - } - ), - }, - } - ), - }; - }; -} - -/** - * @param {object} props - * @param {number} props.commandIndex - * @param {number} props.unexpectedIndex - * @param {string} props.impact - * @returns {(state: TestRunState) => TestRunState} - */ -export function userChangeCommandUnexpectedBehaviorImpact({ - commandIndex, - unexpectedIndex, - impact, -}) { - return function (state) { - return { - ...state, - currentUserAction: UserActionMap.CHANGE_TEXT, - commands: state.commands.map((command, commandI) => - commandI !== commandIndex - ? command - : /** @type {TestRunCommand} */ ({ - ...command, - unexpected: { - ...command.unexpected, - behaviors: command.unexpected.behaviors.map((unexpected, unexpectedI) => - unexpectedI !== unexpectedIndex - ? unexpected - : /** @type {TestRunUnexpectedBehavior} */ ({ - ...unexpected, - impact: impact, - }) - ), - }, - }) - ), - }; - }; -} - -/** - * @param {object} props - * @param {number} props.commandIndex - * @param {number} props.unexpectedIndex - * @param {string} props.more - * @returns {(state: TestRunState) => TestRunState} - */ -export function userChangeCommandUnexpectedBehaviorMore({ commandIndex, unexpectedIndex, more }) { - return function (state) { - return { - ...state, - currentUserAction: UserActionMap.CHANGE_TEXT, - commands: state.commands.map((command, commandI) => - commandI !== commandIndex - ? command - : /** @type {TestRunCommand} */ ({ - ...command, - unexpected: { - ...command.unexpected, - behaviors: command.unexpected.behaviors.map((unexpected, unexpectedI) => - unexpectedI !== unexpectedIndex - ? unexpected - : /** @type {TestRunUnexpectedBehavior} */ ({ - ...unexpected, - more: { - ...unexpected.more, - value: more, - }, - }) - ), - }, - }) - ), - }; - }; -} - -/** - * @param {string} key - * @returns {TestRunFocusIncrement} - */ -function keyToFocusIncrement(key) { - switch (key) { - case 'Up': - case 'ArrowUp': - case 'Left': - case 'ArrowLeft': - return 'previous'; - - case 'Down': - case 'ArrowDown': - case 'Right': - case 'ArrowRight': - return 'next'; - } -} - -/** - * @param {TestRunState} state - * @param {TestRunHooks} hooks - * @returns {TestPageDocument} - */ -function testPageDocument(state, hooks) { - if (state.currentUserAction === UserActionMap.SHOW_RESULTS) { - return { - results: resultsTableDocument(state), - }; - } - const instructions = instructionDocument(state, hooks); - return { - errors: instructions.errors, - instructions, - }; -} - -/** - * @param {TestRun} app - */ -function submitResult(app) { - app.dispatch(userValidateState()); - - if (isSomeFieldRequired(app.state)) { - return; - } - - app.hooks.postResults(); - - app.hooks.closeTestPage(); - - if (app.state.config.renderResultsAfterSubmit) { - app.dispatch(userShowResults()); - } -} - -export function userShowResults() { - return function (/** @type {TestRunState} */ state) { - return /** @type {TestRunState} */ ({ - ...state, - currentUserAction: UserActionMap.SHOW_RESULTS, - }); - }; -} - -/** - * @param {TestRunState} state - * @returns - */ -function isSomeFieldRequired(state) { - return state.commands.some( - command => - command.atOutput.value.trim() === '' || - command.unexpected.hasUnexpected === HasUnexpectedBehaviorMap.NOT_SET || - (command.unexpected.hasUnexpected === HasUnexpectedBehaviorMap.HAS_UNEXPECTED && - (command.unexpected.behaviors.every(({ checked }) => !checked) || - command.unexpected.behaviors.some( - behavior => behavior.checked && behavior.more && behavior.more.value.trim() === '' - ))) - ); -} - -/** - * @param {TestRunState} state - * @returns {ResultsTableDocument} - */ -function resultsTableDocument(state) { - return { - header: state.info.description, - status: { - header: [ - 'Test result: ', - state.commands.some( - ({ - assertions, - additionalAssertions, - unexpected, - commandSettings: { assertionExceptions }, - }) => - [ - // Ignore assertion if level 0 priority exception found for assertion's command - ...assertions.filter((each, index) => - assertionExceptions ? assertionExceptions[index] !== 0 : each - ), - ...additionalAssertions, - ].some(({ priority, result }) => priority === 1 && result !== CommonResultMap.PASS) || - unexpected.behaviors.some(({ checked }) => checked) - ) - ? 'FAIL' - : 'PASS', - ], - }, - table: { - headers: { - description: 'Command', - support: 'Support', - details: 'Details', - }, - commands: state.commands.map(command => { - const { - commandSettings: { assertionExceptions }, - } = command; - const allAssertions = [ - // Ignore assertion if level 0 priority exception found for assertion's command - ...command.assertions.filter((each, index) => - assertionExceptions ? assertionExceptions[index] !== 0 : each - ), - ...command.additionalAssertions, - ]; - - let passingAssertions = ['No passing assertions']; - let failingAssertions = ['No failing assertions']; - let unexpectedBehaviors = ['None']; - - if (allAssertions.some(({ result }) => result === CommonResultMap.PASS)) { - passingAssertions = allAssertions - .filter(({ result }) => result === CommonResultMap.PASS) - .map(({ description }) => description); - } - if (allAssertions.some(({ result }) => result !== CommonResultMap.PASS)) { - failingAssertions = allAssertions - .filter(({ result }) => result !== CommonResultMap.PASS) - .map(({ description }) => description); - } - if (command.unexpected.behaviors.some(({ checked }) => checked)) { - unexpectedBehaviors = command.unexpected.behaviors - .filter(({ checked }) => checked) - .map(({ description, more, impact }) => { - let result = `${description} (`; - if (more) result = `${result}Details: ${more.value}, `; - result = `${result}Impact: ${impact})`; - return result; - }); - } - - return { - description: command.description, - support: - allAssertions.some( - ({ priority, result }) => priority === 1 && result !== CommonResultMap.PASS - ) || command.unexpected.behaviors.some(({ checked }) => checked) - ? 'FAILING' - : allAssertions.some( - ({ priority, result }) => priority === 2 && result !== CommonResultMap.PASS - ) - ? 'ALL_REQUIRED' - : 'FULL', - details: { - output: /** @type {Description} */ [ - 'Output:', - /** @type {DescriptionWhitespace} */ ({ whitespace: WhitespaceStyleMap.LINE_BREAK }), - ' ', - ...command.atOutput.value.split(/(\r\n|\r|\n)/g).map(output => - /\r\n|\r|\n/.test(output) - ? /** @type {DescriptionWhitespace} */ ({ - whitespace: WhitespaceStyleMap.LINE_BREAK, - }) - : output - ), - ], - passingAssertions: { - description: 'Passing Assertions:', - items: passingAssertions, - }, - failingAssertions: { - description: 'Failing Assertions:', - items: failingAssertions, - }, - unexpectedBehaviors: { - description: 'Other behaviors that create negative impact:', - items: unexpectedBehaviors, - }, - }, - }; - }), - }, - }; -} - -export function userOpenWindow() { - return (/** @type {TestRunState} */ state) => - /** @type {TestRunState} */ ({ - ...state, - currentUserAction: UserActionMap.OPEN_TEST_WINDOW, - openTest: { ...state.openTest, enabled: false }, - }); -} - -export function userCloseWindow() { - return (/** @type {TestRunState} */ state) => - /** @type {TestRunState} */ ({ - ...state, - currentUserAction: UserActionMap.CLOSE_TEST_WINDOW, - openTest: { ...state.openTest, enabled: true }, - }); -} - -/** - * @param {object} props - * @param {number} props.commandIndex - * @param {number} props.unexpectedIndex - * @param {TestRunFocusIncrement} props.increment - * @returns {(state: TestRunState) => TestRunState} - */ -export function userFocusCommandUnexpectedBehavior({ commandIndex, unexpectedIndex, increment }) { - return function (state) { - const unexpectedLength = state.commands[commandIndex].unexpected.behaviors.length; - const incrementValue = increment === 'next' ? 1 : -1; - const newUnexpectedIndex = - (unexpectedIndex + incrementValue + unexpectedLength) % unexpectedLength; - - return { - ...state, - currentUserAction: { - action: UserObjectActionMap.FOCUS_UNDESIRABLE, - commandIndex, - unexpectedIndex: newUnexpectedIndex, - }, - commands: state.commands.map((command, commandI) => { - const tabbed = command.unexpected.tabbedBehavior; - const unexpectedLength = command.unexpected.behaviors.length; - const newTabbed = - (tabbed + (increment === 'next' ? 1 : -1) + unexpectedLength) % unexpectedLength; - return commandI !== commandIndex - ? command - : { - ...command, - unexpected: { - ...command.unexpected, - tabbedBehavior: newTabbed, - }, - }; - }), - }; - }; -} - -/** - * @returns {(state: TestRunState) => TestRunState} - */ -export function userValidateState() { - return function (state) { - return { - ...state, - currentUserAction: UserActionMap.VALIDATE_RESULTS, - commands: state.commands.map(command => { - return { - ...command, - atOutput: { - ...command.atOutput, - highlightRequired: !command.atOutput.value.trim(), - }, - unexpected: { - ...command.unexpected, - highlightRequired: - command.unexpected.hasUnexpected === HasUnexpectedBehaviorMap.NOT_SET || - (command.unexpected.hasUnexpected === HasUnexpectedBehaviorMap.HAS_UNEXPECTED && - command.unexpected.behaviors.every(({ checked }) => !checked)), - behaviors: command.unexpected.behaviors.map(unexpected => { - return unexpected.more - ? { - ...unexpected, - more: { - ...unexpected.more, - highlightRequired: unexpected.checked && !unexpected.more.value.trim(), - }, - } - : unexpected; - }), - }, - }; - }), - }; - }; -} - -/** - * @typedef AT - * @property {string} name - * @property {string} key - */ - -/** - * @typedef Behavior - * @property {string} description - * @property {string} task - * @property {string} mode - * @property {string} modeInstructions - * @property {string[]} appliesTo - * @property {string} specificUserInstruction - * @property {string} setupScriptDescription - * @property {string} setupTestPage - * @property {string[]} commands - * @property {[string, string][]} outputAssertions - * @property {[number, string][]} additionalAssertions - */ - -/** - * @typedef {"previous" | "next"} TestRunFocusIncrement - */ - -/** - * @typedef {(action: (state: TestRunState) => TestRunState) => void} Dispatcher - */ - -/** - * @typedef InstructionDocumentButton - * @property {Description} button - * @property {boolean} [enabled] - * @property {() => void} click - */ - -/** - * @typedef InstructionDocumentAssertionChoiceOptionsOptionsMore - * @property {Description} description - * @property {string} value - * @property {boolean} enabled - * @property {boolean} [focus] - * @property {(value: string) => void} change - */ - -/** - * @typedef InstructionDocumentAssertionChoiceOptionsOption - * @property {Description} description - * @property {boolean} checked - * @property {boolean} enabled - * @property {boolean} tabbable - * @property {boolean} [focus] - * @property {(checked: boolean) => void} change - * @property {(key: string) => boolean} keydown - * @property {InstructionDocumentAssertionChoiceOptionsOptionsMore} [more] - */ - -/** - * @typedef InstructionDocumentAssertionChoiceOptions - * @property {Description} header - * @property {InstructionDocumentAssertionChoiceOptionsOption[]} options - */ - -/** - * @typedef InstructionDocumentAssertionChoice - * @property {Description} label - * @property {boolean} checked - * @property {boolean} [focus] - * @property {() => void} click - * @property {InstructionDocumentAssertionChoiceOptions} [options] - */ - -/** - * @typedef DescriptionRich - * @property {string} [href] - * @property {boolean} [required] - * @property {boolean} [highlightRequired] - * @property {boolean} [offScreen] - * @property {Description} description - */ - -/** - * @typedef DescriptionWhitespace - * @property {typeof WhitespaceStyleMap["LINE_BREAK"]} whitespace - */ - -/** @typedef {string | DescriptionRich | DescriptionWhitespace | DescriptionArray} Description */ - -/** @typedef {Description[]} DescriptionArray */ - -/** - * @typedef InstructionDocumentResultsCommandsAssertion - * @property {Description} description - * @property {Boolean} passed - * @property {boolean} [focus] - * @property {() => void} click - */ - -/** - * @typedef InstructionDocumentResultsCommandsAssertionsHeader - * @property {Description} descriptionHeader - */ - -/** - * @typedef InstructionDocumentResultsCommandsATOutput - * @property {Description} description - * @property {string} value - * @property {boolean} focus - * @property {(value: string) => void} change - */ - -/** - * @typedef InstructionDocumentResultsCommandsUnexpected - * @property {Description} description - * @property {InstructionDocumentAssertionChoice} passChoice - * @property {InstructionDocumentAssertionChoice} failChoice - */ - -/** - * @typedef InstructionDocumentResultsCommand - * @property {Description} header - * @property {InstructionDocumentResultsCommandsATOutput} atOutput - * @property {InstructionDocumentResultsCommandsAssertionsHeader} assertionsHeader - * @property {InstructionDocumentResultsCommandsAssertion[]} assertions - * @property {InstructionDocumentResultsCommandsUnexpected} unexpectedBehaviors - */ - -/** - * @typedef InstructionDocumentResultsHeader - * @property {Description} header - * @property {Description} description - */ - -/** - * @typedef InstructionDocumentResults - * @property {InstructionDocumentResultsHeader} header - * @property {InstructionDocumentResultsCommand[]} commands - */ - -/** - * @typedef InstructionDocumentInstructionsInstructionsCommands - * @property {Description} description - * @property {Description[]} commands - */ - -/** - * @typedef InstructionDocumentInstructionsInstructions - * @property {Description} header - * @property {Description[]} instructions - * @property {Description[]} strongInstructions - * @property {InstructionDocumentInstructionsInstructionsCommands} commands - */ - -/** - * @typedef InstructionDocumentErrors - * @property {boolean} visible - * @property {Description} header - * @property {Description[]} errors - */ - -/** - * @typedef InstructionDocumentInstructionsHeader - * @property {Description} header - * @property {boolean} focus - */ - -/** - * @typedef InstructionDocumentInstructionsAssertions - * @property {Description} header - * @property {Description} description - * @property {Description[]} assertions - */ - -/** - * @typedef InstructionDocumentInstructions - * @property {InstructionDocumentInstructionsHeader} header - * @property {Description} description - * @property {InstructionDocumentInstructionsInstructions} instructions - * @property {InstructionDocumentInstructionsAssertions} assertions - * @property {InstructionDocumentButton} openTestPage - */ - -/** - * @typedef InstructionDocument - * @property {InstructionDocumentErrors} errors - * @property {InstructionDocumentInstructions} instructions - * @property {InstructionDocumentResults} results - * @property {InstructionDocumentButton} submit - */ - -/** - * @typedef TestRunHooks - * @property {() => void} closeTestPage - * @property {(options: {commandIndex: number, unexpectedIndex: number, increment: TestRunFocusIncrement}) => void} focusCommandUnexpectedBehavior - * @property {() => void} openTestPage - * @property {() => void} postResults - * @property {(options: {commandIndex: number, additionalAssertionIndex: number, result: AdditionalAssertionResult}) => void} setCommandAdditionalAssertion - * @property {(options: {commandIndex: number, assertionIndex: number, result: AssertionResult}) => void} setCommandAssertion - * @property {(options: {commandIndex: number, hasUnexpected: HasUnexpectedBehavior}) => void } setCommandHasUnexpectedBehavior - * @property {(options: {commandIndex: number, atOutput: string}) => void} setCommandOutput - * @property {(options: {commandIndex: number, unexpectedIndex: number, checked}) => void } setCommandUnexpectedBehavior - * @property {(options: {commandIndex: number, unexpectedIndex: number, impact: string}) => void } setCommandUnexpectedBehaviorImpact - * @property {(options: {commandIndex: number, unexpectedIndex: number, more: string}) => void } setCommandUnexpectedBehaviorMore - * @property {() => void} submit - */ - -/** - * @typedef UserActionFocusUnexpected - * @property {typeof UserObjectActionMap["FOCUS_UNDESIRABLE"]} action - * @property {number} commandIndex - * @property {number} unexpectedIndex - */ - -/** - * @typedef {T[keyof T]} EnumValues - * @template {{[key: string]: string}} T - */ - -/** - * @typedef TestRunAssertion - * @property {string} description - * @property {boolean} highlightRequired - * @property {number} priority - * @property {AssertionResult} result - */ - -/** - * @typedef TestRunAdditionalAssertion - * @property {string} description - * @property {boolean} highlightRequired - * @property {number} priority - * @property {AdditionalAssertionResult} result - */ - -/** - * @typedef TestRunUnexpectedBehavior - * @property {string} description - * @property {boolean} checked - * @property {object} [more] - * @property {boolean} more.highlightRequired - * @property {string} more.value - * @property {string} impact - */ - -/** - * @typedef TestRunUnexpectedGroup - * @property {boolean} highlightRequired - * @property {HasUnexpectedBehavior} hasUnexpected - * @property {number} tabbedBehavior - * @property {TestRunUnexpectedBehavior[]} behaviors - */ - -/** - * @typedef TestRunCommand - * @property {string} description - * @property {object} atOutput - * @property {boolean} atOutput.highlightRequired - * @property {string} atOutput.value - * @property {TestRunAssertion[]} assertions - * @property {TestRunAdditionalAssertion[]} additionalAssertions - * @property {TestRunUnexpectedGroup} unexpected - */ - -/** - * @typedef TestRunState - * This state contains all the serializable values that are needed to render any - * of the documents (InstructionDocument, ResultsTableDocument, and - * TestPageDocument) from this module. - * - * @property {string[] | null} errors - * @property {object} info - * @property {string} info.description - * @property {string} info.task - * @property {ATMode} info.mode - * @property {string} info.modeInstructions - * @property {string[]} info.userInstructions - * @property {string} info.setupScriptDescription - * @property {object} config - * @property {AT} config.at - * @property {boolean} config.renderResultsAfterSubmit - * @property {boolean} config.displaySubmitButton - * @property {TestRunUserAction} currentUserAction - * @property {TestRunCommand[]} commands - * @property {object} openTest - * @property {boolean} openTest.enabled - */ - -/** - * @typedef ResultsTableDetailsList - * @property {Description} description - * @property {Description[]} items - */ - -/** - * @typedef ResultsTableDocument - * @property {string} header - * @property {object} status - * @property {Description} status.header - * @property {object} table - * @property {object} table.headers - * @property {string} table.headers.description - * @property {string} table.headers.support - * @property {string} table.headers.details - * @property {object[]} table.commands - * @property {string} table.commands[].description - * @property {Description} table.commands[].support - * @property {object} table.commands[].details - * @property {Description} table.commands[].details.output - * @property {ResultsTableDetailsList} table.commands[].details.passingAssertions - * @property {ResultsTableDetailsList} table.commands[].details.failingAssertions - * @property {ResultsTableDetailsList} table.commands[].details.unexpectedBehaviors - */ - -/** - * @typedef TestPageDocumentResults - * @property {ResultsTableDocument} results - */ - -/** - * @typedef TestPageDocumentInstructions - * @property {string[] | null} errors - * @property {InstructionDocument} instructions - */ - -/** @typedef {TestPageDocumentInstructions | TestPageDocumentResults} TestPageDocument */ - -/** @typedef {"reading" | "interaction"} ATMode */ diff --git a/client/resources/aria-at-test-window.mjs b/client/resources/aria-at-test-window.mjs deleted file mode 100644 index c60b8c828..000000000 --- a/client/resources/aria-at-test-window.mjs +++ /dev/null @@ -1,73 +0,0 @@ -export class TestWindow { - /** - * @param {object} options - * @param {Window | null} [options.window] - * @param {string} options.pageUri - * @param {TestWindowHooks} [options.hooks] - */ - constructor({ window = null, pageUri, hooks }) { - /** @type {Window | null} */ - this.window = window; - - /** @type {string} */ - this.pageUri = pageUri; - - /** @type {TestWindowHooks} */ - this.hooks = { - windowOpened: () => {}, - windowClosed: () => {}, - ...hooks, - }; - } - - open() { - this.window = window.open( - this.pageUri, - '_blank', - 'toolbar=0,location=0,menubar=0,width=800,height=800' - ); - - this.hooks.windowOpened(); - - this.prepare(); - } - - prepare() { - if (!this.window) { - return; - } - - if (this.window.closed) { - this.window = undefined; - this.hooks.windowClosed(); - return; - } - - if ( - this.window.location.origin !== window.location.origin || // make sure the origin is the same, and prevent this from firing on an 'about' page - this.window.document.readyState !== 'complete' - ) { - window.setTimeout(() => { - this.prepare(); - }, 100); - return; - } - - // If the window is closed, re-enable open popup button - this.window.onunload = () => { - window.setTimeout(() => this.prepare(), 100); - }; - } - - close() { - if (this.window) { - this.window.close(); - } - } -} - -/** - * @typedef TestWindowHooks - * @property {() => void} windowOpened - * @property {() => void} windowClosed - */ diff --git a/client/resources/at-commands.mjs b/client/resources/at-commands.mjs deleted file mode 100644 index 6bbb5b776..000000000 --- a/client/resources/at-commands.mjs +++ /dev/null @@ -1,273 +0,0 @@ -/** @deprecated See aria-at-test-io-format.mjs */ - -import * as keys from './keys.mjs'; - -/** - * Class for getting AT-specific instructions for a test against a design pattern. - * @deprecated See aria-at-test-io-format.mjs:CommandsInput - */ -export class commandsAPI { - /** - * Creates an API to get AT-specific instructions for a design pattern. - * @param {object} commands - A data structure which is a nested object with the following format: - * { - * task: { - * mode: { - * at: [ - * key-command (string corresponding to export in keys.mjs), - * optional additional instructions to list after key command (string), - * ] - * } - * } - * } - * @param {object} supportJson - The data object found in `tests/support.json` - * @param {object} commandsJson - The data object found in `tests/commands.json` - */ - constructor(commands, supportJson, commandsJson) { - if (!commands) { - throw new Error('You must initialize commandsAPI with a commands data object'); - } - - if (!supportJson) { - throw new Error('You must initialize commandsAPI with a supportJson data object'); - } - - if (!commandsJson) { - throw new Error('You must initialize commandsAPI with a commandsJson data object'); - } - - this.AT_COMMAND_MAP = commands; - - this.MODE_INSTRUCTIONS = { - reading: { - jaws: `Verify the Virtual Cursor is active by pressing ${keys.ALT_DELETE}. If it is not, exit Forms Mode to activate the Virtual Cursor by pressing ${keys.ESC}.`, - nvda: `Ensure NVDA is in browse mode by pressing ${keys.ESC}. Note: This command has no effect if NVDA is already in browse mode.`, - voiceover_macos: `Toggle Quick Nav ON by pressing the ${keys.LEFT} and ${keys.RIGHT} keys at the same time.`, - }, - interaction: { - jaws: `Verify the PC Cursor is active by pressing ${keys.ALT_DELETE}. If it is not, turn off the Virtual Cursor by pressing ${keys.INS_Z}.`, - nvda: `If NVDA did not make the focus mode sound when the test page loaded, press ${keys.INS_SPACE} to turn focus mode on.`, - voiceover_macos: `Toggle Quick Nav OFF by pressing the ${keys.LEFT} and ${keys.RIGHT} keys at the same time.`, - }, - }; - - this.supportJson = supportJson; - this.commandsJson = this.flattenObject(commandsJson); - } - - /** - * Get AT-specific instruction - * @param {string} mode - The mode of the screen reader, "reading" or "interaction" - * @param {string} task - The task of the test. - * @param {object} assistiveTech - The assistive technology. - * @return {Array} - A list of commands (strings) - */ - getATCommands(mode, task, assistiveTech) { - let commands = []; - - for (const _atMode of mode.split('_')) { - if (this.AT_COMMAND_MAP[task][_atMode][assistiveTech.key]) { - mode = _atMode; - - if (!this.AT_COMMAND_MAP[task]) { - throw new Error( - `Task "${task}" does not exist, please add to at-commands or correct your spelling.` - ); - } - - if (!this.AT_COMMAND_MAP[task][mode]) { - throw new Error( - `Mode "${mode}" instructions for task "${task}" does not exist, please add to at-commands or correct your spelling.` - ); - } - - let commandsData = this.AT_COMMAND_MAP[task][mode][assistiveTech.key] || []; - - // V1 - if (mode === 'reading' || mode === 'interaction') { - for (let c of commandsData) { - let innerCommands = []; - let commandSequence = c[0].split(','); - for (let command of commandSequence) { - command = keys[command]; - if (typeof command === 'undefined') { - throw new Error( - `Key instruction identifier "${c}" for AT "${assistiveTech.name}", mode "${mode}", task "${task}" is not an available identifier. Update your commands.json file to the correct identifier or add your identifier to resources/keys.mjs.` - ); - } - - let furtherInstruction = c[1]; - command = furtherInstruction ? `${command} ${furtherInstruction}` : command; - innerCommands.push(command); - } - commands.push(innerCommands.join(', then ')); - } - } else { - // V2 - for (let c of commandsData) { - const commandWithPresentationNumber = c[0]; - const [commandId, presentationNumber] = commandWithPresentationNumber.split('|'); - - const commandKVs = this.findValuesByKeys([commandId]); - if (!commandKVs.length) { - throw new Error( - `Key instruction identifier "${commandId}" for AT "${assistiveTech.name}", mode "${mode}", task "${task}" is not an available identifier. Update your commands.json file to the correct identifier or add your identifier to tests/commands.json.` - ); - } - - commands.push( - ...commandKVs.map(({ value, key }) => { - value = assistiveTech.settings[mode].screenText - ? `${value} (${assistiveTech.settings[mode].screenText})` - : value; - return { - value, - key, - settings: mode, - }; - }) - ); - } - } - } - } - - return commands; - } - - /** - * Get AT-specific mode switching instructions - * @param {string} mode - The mode of the screen reader, "reading" or "interaction" - * @param {string} assistiveTech - The assistive technology. - * @return {string} - Instructions for switching into the correct mode. - */ - getModeInstructions(mode, assistiveTech) { - if (this.MODE_INSTRUCTIONS[mode] && this.MODE_INSTRUCTIONS[mode][assistiveTech.key]) { - return this.MODE_INSTRUCTIONS[mode][assistiveTech.key]; - } - return ''; - } - - /** - * Get AT-specific instruction - * @param {string} at - an assitve technology with any capitalization - * @return {string} - if this API knows instructions for `at`, it will return the `at` with proper capitalization - */ - isKnownAT(at) { - return this.supportJson.ats.find(o => o.key === at.toLowerCase()); - } - - defaultConfigurationInstructions(at) { - return this.supportJson.ats.find(o => o.key === at.toLowerCase()) - .defaultConfigurationInstructionsHTML; - } - - flattenObject(obj, parentKey) { - const flattened = {}; - - for (const key in obj) { - if (typeof obj[key] === 'object') { - const subObject = this.flattenObject(obj[key], parentKey + key + '.'); - Object.assign(flattened, subObject); - } else { - flattened[parentKey + key] = obj[key]; - } - } - - return flattened; - } - - findValueByKey(keyToFind) { - const keys = Object.keys(this.commandsJson); - - // Need to specially handle VO modifier key combination - if (keyToFind === 'vo') - return this.findValuesByKeys([this.commandsJson['modifierAliases.vo']])[0]; - - if (keyToFind.includes('modifiers.') || keyToFind.includes('keys.')) { - const parts = keyToFind.split('.'); - const keyToCheck = parts[parts.length - 1]; // value after the '.' - - if (this.commandsJson[keyToFind]) - return { - value: this.commandsJson[keyToFind], - key: keyToCheck, - }; - - return null; - } - - for (const key of keys) { - const parts = key.split('.'); - const parentKey = parts[0]; - const keyToCheck = parts[parts.length - 1]; // value after the '.' - - if (keyToCheck === keyToFind) { - if (parentKey === 'modifierAliases') { - return this.findValueByKey(`modifiers.${this.commandsJson[key]}`); - } else if (parentKey === 'keyAliases') { - return this.findValueByKey(`keys.${this.commandsJson[key]}`); - } - - return { - value: this.commandsJson[key], - key: keyToCheck, - }; - } - } - - // Return null if the key is not found - return null; - } - - findValuesByKeys(keysToFind = []) { - const result = []; - - const patternSepWithReplacement = (keyToFind, pattern, replacement) => { - if (keyToFind.includes(pattern)) { - let value = ''; - let validKeys = true; - const keys = keyToFind.split(pattern); - - for (const key of keys) { - const keyResult = this.findValueByKey(key); - if (keyResult) - value = value ? `${value}${replacement}${keyResult.value}` : keyResult.value; - else validKeys = false; - } - if (validKeys) return { value, key: keyToFind }; - } - - return null; - }; - - const patternSepHandler = keyToFind => { - let value = ''; - - if (keyToFind.includes(' ') && keyToFind.includes('+')) { - const keys = keyToFind.split(' '); - for (let [index, key] of keys.entries()) { - const keyToFindResult = this.findValueByKey(key); - if (keyToFindResult) keys[index] = keyToFindResult.value; - if (key.includes('+')) keys[index] = patternSepWithReplacement(key, '+', '+').value; - } - value = keys.join(' then '); - - return { value, key: keyToFind }; - } else if (keyToFind.includes(' ')) - return patternSepWithReplacement(keyToFind, ' ', ' then '); - else if (keyToFind.includes('+')) return patternSepWithReplacement(keyToFind, '+', '+'); - }; - - for (const keyToFind of keysToFind) { - if (keyToFind.includes(' ') || keyToFind.includes('+')) { - result.push(patternSepHandler(keyToFind)); - } else { - const keyToFindResult = this.findValueByKey(keyToFind); - if (keyToFindResult) result.push(keyToFindResult); - } - } - - return result; - } -} diff --git a/client/resources/commands.json b/client/resources/commands.json deleted file mode 100644 index 9f5e3cf94..000000000 --- a/client/resources/commands.json +++ /dev/null @@ -1,115 +0,0 @@ -{ - "modifiers": { - "alt": "Alt", - "opt": "Option", - "shift": "Shift", - "ctrl": "Control", - "cmd": "Command", - "win": "Windows", - "ins": "Insert" - }, - "modifierAliases": { - "jaws": "ins", - "nvda": "ins", - "vo": "ctrl+opt" - }, - "keys": { - "a": "a", - "b": "b", - "c": "c", - "d": "d", - "e": "e", - "f": "f", - "g": "g", - "h": "h", - "i": "i", - "j": "j", - "k": "k", - "l": "l", - "m": "m", - "n": "n", - "o": "o", - "p": "p", - "q": "q", - "r": "r", - "s": "s", - "t": "t", - "u": "u", - "v": "v", - "w": "w", - "x": "x", - "y": "y", - "z": "z", - "1": "1", - "2": "2", - "3": "3", - "4": "4", - "5": "5", - "6": "6", - "7": "7", - "8": "8", - "9": "9", - "0": "0", - "dash": "Dash", - "equals": "Equals", - "grave": "Grave", - "leftBracket": "Left Bracket", - "rightBracket": "Right Bracket", - "backslash": "Backslash", - "semicolon": "Semicolon", - "apostrophe": "Apostrophe", - "comma": "Comma", - "period": "Period", - "slash": "Slash", - "esc": "Escape", - "backspace": "Backspace", - "tab": "Tab", - "capsLock": "Caps Lock", - "enter": "Enter", - "space": "Space", - "f1": "F1", - "f2": "F2", - "f3": "F3", - "f4": "F4", - "f5": "F5", - "f6": "F6", - "f7": "F7", - "f8": "F8", - "f9": "F9", - "f10": "F10", - "f11": "F11", - "f12": "F12", - "scrollLock": "Scroll Lock", - "pause": "Pause", - "home": "Home", - "end": "End", - "pageUp": "Page Up", - "pageDown": "Page Down", - "del": "Delete", - "left": "Left Arrow", - "right": "Right Arrow", - "up": "Up Arrow", - "down": "Down Arrow", - "numLock": "Num Lock", - "numpadSlash": "Numpad Slash", - "numpadAsterisk": "Numpad Asterisk", - "numpadMinus": "Numpad Minus", - "numpadPlus": "Numpad Plus", - "numpadEnter": "Numpad Enter", - "numpad1": "Numpad 1", - "numpad2": "Numpad 2", - "numpad3": "Numpad 3", - "numpad4": "Numpad 4", - "numpad5": "Numpad 5", - "numpad6": "Numpad 6", - "numpad7": "Numpad 7", - "numpad8": "Numpad 8", - "numpad9": "Numpad 9", - "numpad0": "Numpad 0", - "numpadPeriod": "Numpad Period" - }, - "keyAliases": { - "delete": "del", - "escape": "esc" - } -} \ No newline at end of file diff --git a/client/resources/keys.mjs b/client/resources/keys.mjs deleted file mode 100644 index bfeff2bde..000000000 --- a/client/resources/keys.mjs +++ /dev/null @@ -1,133 +0,0 @@ -// Keys -export const CTRL_HOME = "Control+Home"; -export const CTRL_OPT_HOME = "Control+Option+Home"; -export const CTRL_END = "Control+End"; -export const CTRL_OPT_END = "Control+Option+End"; -export const CTRL_HOME_THEN_DOWN = "Control+Home followed by Down Arrow"; -export const DELETE = "Delete"; -export const ALT_DELETE = "Alt+Delete"; -export const ALT_DOWN = "Alt+Down"; -export const CTRL_ALT_DOWN = "Control+Alt+Down"; -export const ALT_UP = "Alt+Up"; -export const C_AND_SHIFT_C = "C / Shift+C"; -export const SHIFT_C = "Shift+C"; -export const CTRL_INS_X = "Control+Insert+X"; -export const OPT_DOWN = "Option+Down"; -export const OPT_UP = "Option+Up"; -export const CTRL_OPT_LEFT = "Ctrl+Option+Left"; -export const CTRL_ALT_LEFT = "Control+Alt+Left"; -export const CTRL_OPT_RIGHT = "Control+Option+Right"; -export const CTRL_ALT_RIGHT = "Control+Alt+Right"; -export const CTRL_OPT_UP = "Control+Option+Up"; -export const CTRL_OPT_DOWN = "Control+Option+Down"; -export const CTRL_OPT_RIGHT_AND_CTRL_OPT_LEFT = "Control+Option+Right / Ctrl+Option+Left"; -export const CTRL_OPT_A = "Control+Option+A"; -export const CTRL_OPT_CMD_J = "Control+Option+Command+J"; -export const CTRL_OPT_CMD_L = "Control+Option+Command+L"; -export const CTRL_OPT_CMD_P = "Control+Option+Command+P"; -export const SHIFT_CTRL_OPT_CMD_J = "Shift+Control+Option+Command+J"; -export const SHIFT_CTRL_OPT_CMD_L = "Shift+Control+Option+Command+L"; -export const CTRL_OPT_CMD_J_AND_SHIFT_CTRL_OPT_CMD_J = "Control+Option+Command+J / Shift+Control+Option+Command+J"; -export const CTRL_OPT_CMD_C_AND_SHIFT_CTRL_OPT_CMD_C = "Control+Option+Command+C / Shift+Control+Option+Command+C"; -export const CTRL_OPT_F3 = "Control+Option+F3"; -export const CTRL_OPT_F4 = "Control+Option+F4"; -export const CTRL_OPT_SPACE = "Control+Option+Space"; -export const CTRL_OPT_SPACE_THEN_CTRL_OPT_RIGHT = "Control+Option+Space followed by Control+Option+Right"; -export const CTRL_U = "Control+U"; -export const CMD = "Command"; -export const CMD_LEFT = "Command+Left"; -export const CMD_RIGHT = "Command+Right"; -export const CMD_DOWN = "Command+Down"; -export const CMD_UP = "Command+Up"; -export const DOWN = "Down Arrow"; -export const END = "End"; -export const ENTER = "Enter"; -export const E_AND_SHIFT_E = "E / Shift+E"; -export const ESC = "Escape"; -export const F_AND_SHIFT_F = "F / Shift+F"; -export const HOME = "Home"; -export const INS_DOWN_OR_CAPS_DOWN = "Insert+Down (or CapsLock+Down)"; -export const INS_F7_OR_CAPS_F7 = "Insert+F7 (or CapsLock+F7)"; -export const INS_SPACE = "Insert+Space"; -export const INS_TAB = "Insert+Tab"; -export const INS_TAB_OR_CAPS_TAB = "Insert+Tab (or CapsLock+Tab)"; -export const INS_UP_OR_CAPS_I = "Insert+Up (or CapsLock+I)"; -export const INS_UP = "Insert+Up"; -export const INS_UP_OR_CAPS_UP = "Insert+Up (or CapsLock+Up)"; -export const INS_Z = "Insert+Z"; -export const LEFT_AND_RIGHT = "Left Arrow / Right Arrow"; -export const LEFT = "Left Arrow"; -export const NUMPAD_5 = "Numpad 5"; -export const INS_NUMPAD_5 = "Insert+Numpad 5 (or CapsLock+Numpad 5)"; -export const INS_NUMPAD_6 = "Insert+Numpad 6 (or CapsLock+Numpad 6)"; -export const NUMPAD_PLUS = "Numpad Plus"; -export const RIGHT = "Right Arrow"; -export const SPACE = "Space"; -export const TAB = "Tab"; -export const SHIFT_TAB = "Shift+Tab"; -export const TAB_AND_SHIFT_TAB = "Tab / Shift+Tab"; -export const UP = "Up Arrow"; -export const CTRL_ALT_UP = "Control+Alt+Up"; -export const UP_AND_DOWN = "Up Arrow / Down Arrow"; -export const SHIFT_X = "Shift+X"; -export const X_AND_SHIFT_X = "X / Shift+X"; -export const A = "A"; -export const SHIFT_A = "Shift+A"; -export const B = "B"; -export const SHIFT_B = "Shift+B"; -export const C = "C"; -export const D = "D"; -export const E = "E"; -export const SHIFT_E = "Shift+E"; -export const F = "F"; -export const SHIFT_F = "Shift+F"; -export const G = "G"; -export const H = "H"; -export const I = "I"; -export const SHIFT_I = "Shift+I"; -export const J = "J"; -export const K = "K"; -export const SHIFT_K = "Shift+K"; -export const L = "L"; -export const SHIFT_L = "Shift+L"; -export const M = "M"; -export const N = "N"; -export const O = "O"; -export const P = "P"; -export const Q = "Q"; -export const R = "R"; -export const SHIFT_R = "Shift+R"; -export const S = "S"; -export const T = "T"; -export const SHIFT_T = "Shift+T"; -export const CTRL_OPT_CMD_T = "Control+Option+Command+T"; -export const T_THEN_DOWN = "T followed by Down Arrow"; -export const SHIFT_T_THEN_DOWN = "Shift+T followed by Down Arrow"; -export const U = "U"; -export const SHIFT_U = "Shift+U"; -export const V = "V"; -export const W = "W"; -export const X = "X"; -export const Y = "Y"; -export const CTRL_OPT_CMD_Y = "Control+Option+Command+Y"; -export const SHIFT_CTRL_OPT_CMD_Y = "Shift+Control+Option+Command+Y"; -export const Z = "Z"; -export const PAGE_DOWN = "Page Down"; -export const PAGE_UP = "Page Up"; -export const SHIFT_D = "Shift+D"; -export const CTRL_OPT_CMD_G = "Control+Option+Command+G"; -export const CTRL_OPT_CMD_H = "Control+Option+Command+H"; -export const CTRL_OPT_CMD_X = "Control+Option+Command+X"; -export const SHIFT_CTRL_OPT_CMD_X = "Shift+Control+Option+Command+X"; -export const SHIFT_CTRL_OPT_CMD_G = "Shift+Control+Option+Command+G"; -export const SHIFT_CTRL_OPT_CMD_H = "Shift+Control+Option+Command+H"; -export const SHIFT_CTRL_OPT_CMD_P = "Shift+Control+Option+Command+P"; -export const SHIFT_G = "Shift+G"; -export const SHIFT_H = "Shift+H"; -export const ONE = "1"; -export const TWO = "2"; -export const SHIFT_ONE = "Shift+1"; -export const SHIFT_TWO = "Shift+2"; -export const SHIFT_P = "Shift+P"; -export const COMMA = "Comma"; -export const SHIFT_PERIOD = "Shift+Period"; diff --git a/client/resources/support.json b/client/resources/support.json deleted file mode 100644 index 7f8fa85b1..000000000 --- a/client/resources/support.json +++ /dev/null @@ -1,438 +0,0 @@ -{ - "ats": [ - { - "name": "JAWS", - "key": "jaws", - "defaultConfigurationInstructionsHTML": "Configure JAWS with default settings. For help, read <a href="https://github.com/w3c/aria-at/wiki/Configuring-Screen-Readers-for-Testing">Configuring Screen Readers for Testing</a>.", - "assertionTokens": { - "screenReader": "JAWS", - "readingMode": "virtual cursor active", - "interactionMode": "PC cursor active" - }, - "settings": { - "virtualCursor": { - "screenText": "virtual cursor active", - "instructions": [ - "Press <kbd>Alt</kbd>+<kbd>Delete</kbd> to determine which cursor is active.", - "If the PC cursor is active, press <kbd>Escape</kbd> to activate the virtual cursor." - ] - }, - "pcCursor": { - "screenText": "PC cursor active", - "instructions": [ - "Press <kbd>Alt</kbd>+<kbd>Delete</kbd> to determine which cursor is active.", - "If the virtual cursor is active, press <kbd>Insert</kbd>+<kbd>z</kbd> to disable the virtual cursor." - ] - } - } - }, - { - "name": "NVDA", - "key": "nvda", - "defaultConfigurationInstructionsHTML": "Configure NVDA with default settings. For help, read <a href="https://github.com/w3c/aria-at/wiki/Configuring-Screen-Readers-for-Testing">Configuring Screen Readers for Testing</a>.", - "assertionTokens": { - "screenReader": "NVDA", - "readingMode": "browse mode", - "interactionMode": "focus mode" - }, - "settings": { - "browseMode": { - "screenText": "browse mode on", - "instructions": [ - "Press <kbd>Insert</kbd>+<kbd>Space</kbd>.", - "If NVDA made the focus mode sound, press <kbd>Insert</kbd>+<kbd>Space</kbd> again to turn browse mode back on." - ] - }, - "focusMode": { - "screenText": "focus mode on", - "instructions": [ - "Press <kbd>Insert</kbd>+<kbd>Space</kbd>.", - "If NVDA made the browse mode sound, press <kbd>Insert</kbd>+<kbd>Space</kbd> again to turn focus mode back on." - ] - } - } - }, - { - "name": "VoiceOver for macOS", - "key": "voiceover_macos", - "defaultConfigurationInstructionsHTML": "Configure VoiceOver with default settings. For help, read <a href="https://github.com/w3c/aria-at/wiki/Configuring-Screen-Readers-for-Testing">Configuring Screen Readers for Testing</a>.", - "settings": { - "quickNavOn": { - "screenText": "quick nav on", - "instructions": [ - "Simultaneously press <kbd>Left Arrow</kbd> and <kbd>Right Arrow</kbd>.", - "If VoiceOver said 'quick nav off', press <kbd>Left Arrow</kbd> and <kbd>Right Arrow</kbd> again to turn it back on." - ] - }, - "quickNavOff": { - "screenText": "quick nav off", - "instructions": [ - "Simultaneously press <kbd>Left Arrow</kbd> and <kbd>Right Arrow</kbd>.", - "If VoiceOver said 'quick nav on', press <kbd>Left Arrow</kbd> and <kbd>Right Arrow</kbd> again to turn it back off." - ] - }, - "arrowQuickKeyNavOn": { - "screenText": "arrow quick key nav on", - "instructions": [ - "Simultaneously press <kbd>Left Arrow</kbd> and <kbd>Right Arrow</kbd>.", - "If VoiceOver said 'arrow quick key nav off', press <kbd>Left Arrow</kbd> and <kbd>Right Arrow</kbd> again to turn it back on." - ] - }, - "arrowQuickKeyNavOff": { - "screenText": "arrow quick key nav off", - "instructions": [ - "Simultaneously press <kbd>Left Arrow</kbd> and <kbd>Right Arrow</kbd>.", - "If VoiceOver said 'arrow quick key nav on', press <kbd>Left Arrow</kbd> and <kbd>Right Arrow</kbd> again to turn it back off." - ] - }, - "singleQuickKeyNavOn": { - "screenText": "single quick key nav on", - "instructions": [ - "Press <kbd>Control</kbd>+<kbd>Option</kbd>+<kbd>q</kbd>.", - "If VoiceOver said 'single quick key nav off', press <kbd>Control</kbd>+<kbd>Option</kbd>+<kbd>q</kbd> again to turn it back on." - ] - }, - "singleQuickKeyNavOff": { - "screenText": "single quick key nav off", - "instructions": [ - "Press <kbd>Control</kbd>+<kbd>Option</kbd>+<kbd>q</kbd>.", - "If VoiceOver said 'single quick key nav on', press <kbd>Control</kbd>+<kbd>Option</kbd>+<kbd>q</kbd> again to turn it back off." - ] - } - } - } - ], - "applies_to": { - "Desktop Screen Readers": [ - "VoiceOver for macOS", - "NVDA", - "JAWS" - ], - "Screen Readers": [ - "VoiceOver for macOS", - "NVDA", - "JAWS" - ] - }, - "testPlanStrings": { - "ariaSpecsPreface": "Tested ARIA features:", - "openExampleInstruction": "Activate the "Open test page" button, which opens the example to test in a new window and runs a script that", - "commandListPreface": "Do this with each of the following commands or command sequences.", - "commandListSettingsPreface": "If any settings are specified in parentheses, ensure the settings are active before executing the command or command sequence.", - "settingInstructionsPreface": "To perform a task with", - "assertionResponseQuestion": "Which statements are true about the response to" - }, - "references": { - "aria": { - "baseUrl": "https://www.w3.org/TR/wai-aria/", - "linkText": "ARIA Specification", - "fragmentIds": { - "alert": "#alert", - "alertdialog": "#alertdialog", - "application": "#application", - "article": "#article", - "associationlist": "#associationlist", - "associationlistitemkey": "#associationlistitemkey", - "associationlistitemvalue": "#associationlistitemvalue", - "banner": "#banner", - "blockquote": "#blockquote", - "button": "#button", - "caption": "#caption", - "cell": "#cell", - "checkbox": "#checkbox", - "code": "#code", - "columnheader": "#columnheader", - "combobox": "#combobox", - "command": "#command", - "comment": "#comment", - "complementary": "#complementary", - "composite": "#composite", - "contentinfo": "#contentinfo", - "definition": "#definition", - "deletion": "#deletion", - "dialog": "#dialog", - "directory": "#directory", - "document": "#document", - "emphasis": "#emphasis", - "feed": "#feed", - "figure": "#figure", - "form": "#form", - "generic": "#generic", - "grid": "#grid", - "gridcell": "#gridcell", - "group": "#group", - "heading": "#heading", - "image": "#image", - "img": "#img", - "input": "#input", - "insertion": "#insertion", - "landmark": "#landmark", - "link": "#link", - "list": "#list", - "listbox": "#listbox", - "listitem": "#listitem", - "log": "#log", - "main": "#main", - "mark": "#mark", - "marquee": "#marquee", - "math": "#math", - "menu": "#menu", - "menubar": "#menubar", - "menuitem": "#menuitem", - "menuitemcheckbox": "#menuitemcheckbox", - "menuitemradio": "#menuitemradio", - "meter": "#meter", - "navigation": "#navigation", - "none": "#none", - "note": "#note", - "option": "#option", - "paragraph": "#paragraph", - "presentation": "#presentation", - "progressbar": "#progressbar", - "radio": "#radio", - "radiogroup": "#radiogroup", - "range": "#range", - "region": "#region", - "roletype": "#roletype", - "row": "#row", - "rowgroup": "#rowgroup", - "rowheader": "#rowheader", - "scrollbar": "#scrollbar", - "search": "#search", - "searchbox": "#searchbox", - "section": "#section", - "sectionhead": "#sectionhead", - "select": "#select", - "separator": "#separator", - "slider": "#slider", - "spinbutton": "#spinbutton", - "status": "#status", - "strong": "#strong", - "structure": "#structure", - "subscript": "#subscript", - "suggestion": "#suggestion", - "superscript": "#superscript", - "switch": "#switch", - "tab": "#tab", - "table": "#table", - "tablist": "#tablist", - "tabpanel": "#tabpanel", - "term": "#term", - "textbox": "#textbox", - "time": "#time", - "timer": "#timer", - "toolbar": "#toolbar", - "tooltip": "#tooltip", - "tree": "#tree", - "treegrid": "#treegrid", - "treeitem": "#treeitem", - "widget": "#widget", - "window": "#window", - "aria-activedescendant": "#aria-activedescendant", - "aria-atomic": "#aria-atomic", - "aria-autocomplete": "#aria-autocomplete", - "aria-braillelabel": "#aria-braillelabel", - "aria-brailleroledescription": "#aria-brailleroledescription", - "aria-busy": "#aria-busy", - "aria-checked": "#aria-checked", - "aria-colcount": "#aria-colcount", - "aria-colindex": "#aria-colindex", - "aria-colindextext": "#aria-colindextext", - "aria-colspan": "#aria-colspan", - "aria-controls": "#aria-controls", - "aria-current": "#aria-current", - "aria-describedby": "#aria-describedby", - "aria-description": "#aria-description", - "aria-details": "#aria-details", - "aria-disabled": "#aria-disabled", - "aria-errormessage": "#aria-errormessage", - "aria-expanded": "#aria-expanded", - "aria-flowto": "#aria-flowto", - "aria-haspopup": "#aria-haspopup", - "aria-hidden": "#aria-hidden", - "aria-invalid": "#aria-invalid", - "aria-keyshortcuts": "#aria-keyshortcuts", - "aria-label": "#aria-label", - "aria-labelledby": "#aria-labelledby", - "aria-level": "#aria-level", - "aria-live": "#aria-live", - "aria-modal": "#aria-modal", - "aria-multiline": "#aria-multiline", - "aria-multiselectable": "#aria-multiselectable", - "aria-orientation": "#aria-orientation", - "aria-owns": "#aria-owns", - "aria-placeholder": "#aria-placeholder", - "aria-posinset": "#aria-posinset", - "aria-pressed": "#aria-pressed", - "aria-readonly": "#aria-readonly", - "aria-relevant": "#aria-relevant", - "aria-required": "#aria-required", - "aria-roledescription": "#aria-roledescription", - "aria-rowcount": "#aria-rowcount", - "aria-rowindex": "#aria-rowindex", - "aria-rowindextext": "#aria-rowindextext", - "aria-rowspan": "#aria-rowspan", - "aria-selected": "#aria-selected", - "aria-setsize": "#aria-setsize", - "aria-sort": "#aria-sort", - "aria-valuemax": "#aria-valuemax", - "aria-valuemin": "#aria-valuemin", - "aria-valuenow": "#aria-valuenow", - "aria-valuetext": "#aria-valuetext" - } - }, - "htmlAam": { - "baseUrl": "https://www.w3.org/TR/html-aam-1.0/", - "linkText": "Accessibility API Mapping", - "fragmentIds": { - "a": "#el-a", - "aNoHref": "#el-a-no-href", - "abbr": "#el-abbr", - "address": "#el-address", - "area": "#el-area", - "areaNoHref": "#el-area-no-href", - "article": "#el-article", - "asideBodyOrMainScope": "#el-aside-ancestorbodymain", - "asideSectionScope": "#el-aside", - "audio": "#el-audio", - "autonomous custom element": "#el-autonomous-custom-element", - "b": "#el-b", - "base": "#el-base", - "bdi": "#el-bdi", - "bdo": "#el-bdo", - "blockquote": "#el-blockquote", - "body": "#el-body", - "br": "#el-br", - "button": "#el-button", - "canvas": "#el-canvas", - "caption": "#el-caption", - "cite": "#el-cite", - "code": "#el-code", - "col": "#el-col", - "colgroup": "#el-colgroup", - "data": "#el-data", - "datalist": "#el-datalist", - "dd": "#el-dd", - "del": "#el-del", - "details": "#el-details", - "dfn": "#el-dfn", - "dialog": "#el-dialog", - "div": "#el-div", - "dl": "#el-dl", - "dt": "#el-dt", - "em": "#el-em", - "embed": "#el-embed", - "fieldset": "#el-fieldset", - "figcaption": "#el-figcaption", - "figure": "#el-figure", - "footerBodyScope": "#el-footer-ancestorbody", - "footerMainScope": "#el-footer", - "form": "#el-form", - "formAssociatedCustomElement": "#el-form-associated-custom-element", - "heading": "#el-h1-h6", - "head": "#el-head", - "headerBodyScope": "#el-header-ancestorbody", - "headerMainScope": "#el-header", - "hgroup": "#el-hgroup", - "hr": "#el-hr", - "html": "#el-html", - "i": "#el-i", - "iframe": "#el-iframe", - "img": "#el-img", - "imgEmptyAlt": "#el-img-empty-alt", - "inputTypeButton": "#el-input-button", - "inputTypeCheckbox": "#el-input-checkbox", - "inputTypeColor": "#el-input-color", - "inputTypeDate": "#el-input-date", - "inputTypeDateTime": "#el-input-datetime-local", - "inputTypeEmail": "#el-input-email", - "inputTypeFile": "#el-input-file", - "inputTypeHidden": "#el-input-hidden", - "inputTypeImage": "#el-input-image", - "inputTypeMonth": "#el-input-month", - "inputTypeNumber": "#el-input-number", - "inputTypePassword": "#el-input-password", - "inputTypeRadio": "#el-input-radio", - "inputTypeRange": "#el-input-range", - "inputTypeReset": "#el-input-reset", - "inputTypeSearch": "#el-input-search", - "inputTypeSubmit": "#el-input-submit", - "inputTypeTelephone": "#el-input-tel", - "inputTypeText": "#el-input-text", - "inputTypeTextAutocomplete": "#el-input-textetc-autocomplete", - "inputTypeTime": "#el-input-time", - "inputTypeUrl": "#el-input-url", - "inputTypeWeek": "#el-input-week", - "ins": "#el-ins", - "kbd": "#el-kbd", - "label": "#el-label", - "legend": "#el-legend", - "li": "#el-li", - "link": "#el-link", - "main": "#el-main", - "map": "#el-map", - "mark": "#el-mark", - "math": "#el-math", - "menu": "#el-menu", - "meta": "#el-meta", - "meter": "#el-meter", - "nav": "#el-nav", - "noscript": "#el-noscript", - "object": "#el-object", - "ol": "#el-ol", - "optgroup": "#el-optgroup", - "option": "#el-option", - "output": "#el-output", - "p": "#el-p", - "param": "#el-param", - "picture": "#el-picture", - "pre": "#el-pre", - "progress": "#el-progress", - "q": "#el-q", - "rb": "#el-rb", - "rp": "#el-rp", - "rt": "#el-rt", - "rtc": "#el-rtc", - "ruby": "#el-ruby", - "s": "#el-s", - "samp": "#el-samp", - "script": "#el-script", - "search": "#el-search", - "section": "#el-section", - "select": "#el-select-listbox", - "selectSize1": "#el-select-combobox", - "slot": "#el-slot", - "small": "#el-small", - "source": "#el-source", - "span": "#el-span", - "strong": "#el-strong", - "style": "#el-style", - "sub": "#el-sub", - "summary": "#el-summary", - "sup": "#el-sup", - "svg": "#el-svg", - "table": "#el-table", - "tbody": "#el-tbody", - "td": "#el-td", - "tdGridcell": "#el-td-gridcell", - "template": "#el-template", - "textarea": "#el-textarea", - "tfoot": "#el-tfoot", - "th": "#el-th", - "thGridcell": "#el-th-gridcell", - "thColgroupHeader": "#el-th-columnheader", - "thRowgroupHeader": "#el-th-rowheader", - "thead": "#el-thead", - "time": "#el-time", - "title": "#el-title", - "tr": "#el-tr", - "track": "#el-track", - "u": "#el-u", - "ul": "#el-ul", - "var": "#el-var", - "video": "#el-video", - "wbr": "#el-wbr" - } - } - } -} diff --git a/client/resources/types/aria-at-test-result.js b/client/resources/types/aria-at-test-result.js deleted file mode 100644 index c989191b9..000000000 --- a/client/resources/types/aria-at-test-result.js +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Types of a format of a test result submitted to or received from aria-at-app. - * @namespace AriaATTestResult - */ - -/** - * @typedef {"REQUIRED" - * | "OPTIONAL"} AriaATTestResult.AssertionPriorityJSON - */ - -/** - * @typedef {"INCORRECT_OUTPUT" - * | "NO_OUTPUT"} AriaATTestResult.AssertionFailedReasonJSON - */ - -/** - * @typedef AriaATTestResult.JSON - * @property {object} test - * @property {string} test.title - * @property {object} test.at - * @property {string} test.at.id - * @property {string} test.atMode - * @property {object[]} scenarioResults - * @property {object} scenarioResults[].scenario - * @property {object} scenarioResults[].scenario.command - * @property {string} scenarioResults[].scenario.command.id - * @property {string} scenarioResults[].output - * @property {object[]} scenarioResults[].assertionResults - * @property {object} scenarioResults[].assertionResults[].assertion - * @property {AriaATTestResult.AssertionPriorityJSON} scenarioResults[].assertionResults[].assertion.priority - * @property {string} scenarioResults[].assertionResults[].assertion.text - * @property {boolean} scenarioResults[].assertionResults[].passed - * @property {AriaATTestResult.AssertionFailedReasonJSON | null} [scenarioResults[].assertionResults[].failedReason] - * @property {object[]} scenarioResults[].unexpectedBehaviors - * @property {string} scenarioResults[].unexpectedBehaviors[].id - * @property {string} scenarioResults[].unexpectedBehaviors[].text - * @property {string} scenarioResults[].unexpectedBehaviors[].impact - * @property {string} scenarioResults[].unexpectedBehaviors[].details - */ diff --git a/client/resources/types/aria-at-test-run.js b/client/resources/types/aria-at-test-run.js deleted file mode 100644 index cdeceb90e..000000000 --- a/client/resources/types/aria-at-test-run.js +++ /dev/null @@ -1,100 +0,0 @@ -/** @namespace AriaATTestRun */ - -/** - * @typedef {"reading" - * | "interaction"} AriaATTestRun.ATMode - */ - -/** - * @typedef {"loadPage" - * | "openTestWindow" - * | "closeTestWindow" - * | "validateResults" - * | "changeText" - * | "changeSelection" - * | "showResults"} AriaATTestRun.UserActionName - */ - -/** - * @typedef {"focusUndesirable"} AriaATTestRun.UserActionObjectName - */ - -/** - * @typedef AriaATTestRun.UserActionFocusUnexpected - * @property {"focusUndesirable"} action - * @property {number} commandIndex - * @property {number} unexpectedIndex - */ - -/** - * @typedef {AriaATTestRun.UserActionName - * | AriaATTestRun.UserActionFocusUnexpected} AriaATTestRun.UserAction - */ - -/** - * @typedef {"notSet" - * | "pass" - * | "failMissing" - * | "failIncorrect"} AriaATTestRun.AssertionResult - */ - -/** - * @typedef {"notSet" - * | "pass" - * | "failSupport"} AriaATTestRun.AdditionalAssertionResult - */ - -/** - * @typedef {"notSet" - * | "hasUnexpected" - * | "doesNotHaveUnexpected"} AriaATTestRun.HasUnexpectedBehavior - */ - -/** - * @typedef AriaATTestRun.State - * This state contains all the serializable values that are needed to render any of the documents (InstructionDocument, - * ResultsTableDocument, and TestPageDocument) from the test-run module. - * - * @property {string[] | null} errors - * @property {object} info - * @property {string} info.description - * @property {string} info.task - * @property {AriaATTestRun.ATMode} info.mode - * @property {string} info.modeInstructions - * @property {string[]} info.userInstructions - * @property {string} info.setupScriptDescription - * @property {object} config - * @property {object} config.at - * @property {string} config.at.key - * @property {string} config.at.name - * @property {boolean} config.renderResultsAfterSubmit - * @property {boolean} config.displaySubmitButton - * @property {AriaATTestRun.UserAction} currentUserAction - * @property {object[]} commands - * @property {string} commands[].description - * @property {object} commands[].atOutput - * @property {boolean} commands[].atOutput.highlightRequired - * @property {string} commands[].atOutput.value - * @property {object[]} commands[].assertions - * @property {string} commands[].assertions[].description - * @property {boolean} commands[].assertions[].highlightRequired - * @property {number} commands[].assertions[].priority - * @property {AriaATTestRun.AssertionResult} commands[].assertions[].result - * @property {object[]} commands[].additionalAssertions - * @property {string} commands[].additionalAssertions[].description - * @property {boolean} commands[].additionalAssertions[].highlightRequired - * @property {number} commands[].additionalAssertions[].priority - * @property {AriaATTestRun.AdditionalAssertionResult} commands[].additionalAssertions[].result - * @property {object} commands[].unexpected - * @property {boolean} commands[].unexpected.highlightRequired - * @property {AriaATTestRun.HasUnexpectedBehavior} commands[].unexpected.hasUnexpected - * @property {number} commands[].unexpected.tabbedBehavior - * @property {object[]} commands[].unexpected.behaviors - * @property {string} commands[].unexpected.behaviors[].description - * @property {boolean} commands[].unexpected.behaviors[].checked - * @property {object} [commands[].unexpected.behaviors[].more] - * @property {boolean} commands[].unexpected.behaviors[].more.highlightRequired - * @property {string} commands[].unexpected.behaviors[].more.value - * @property {object} openTest - * @property {boolean} openTest.enabled - */ diff --git a/client/resources/vrender.mjs b/client/resources/vrender.mjs deleted file mode 100644 index f21b6b2dc..000000000 --- a/client/resources/vrender.mjs +++ /dev/null @@ -1,987 +0,0 @@ -const mounts = new WeakMap(); - -/** - * @param {HTMLElement} mount - * @param {NodeNode} newValue - */ -export function render(mount, newValue) { - let lastValue = mounts.get(mount); - if (!lastValue) { - lastValue = ElementType.init(newQueue(), null, null, element(mount.tagName)); - lastValue.ref = mount; - mounts.set(mount, lastValue); - } - const queue = newQueue(); - lastValue.type.diff(queue, lastValue, element(mount.tagName, newValue)); - runQueue(queue); - if (!newValue) { - mounts.delete(mount); - } -} - -/** - * @param {string} shape - * @param {...NodeNode} content - * @returns {ElementNode} - */ -export function element(shape, ...content) { - return { - type: ELEMENT_TYPE_NAME, - shape, - content: content.map(asNode), - }; -} - -/** - * @param {...NodeNode} content - * @returns {FragmentNode} - */ -export function fragment(...content) { - return { - type: FRAGMENT_TYPE_NAME, - shape: FRAGMENT_TYPE_NAME, - content: content.map(asNode), - }; -} - -/** - * @param {string} content - * @returns {TextNode} - */ -export function text(content) { - return { - type: TEXT_TYPE_NAME, - shape: TEXT_TYPE_NAME, - content, - }; -} - -/** - * @param {function} shape - * @param {...NodeNode} content - * @returns {ComponentNode} - */ -export function component(shape, ...content) { - return { - type: COMPONENT_TYPE_NAME, - shape, - content, - }; -} - -/** - * @param {{[key: string]: string}} styleMap - * @returns {MemberNode} - */ -export function style(styleMap) { - return attribute( - 'style', - Object.keys(styleMap) - .map(key => `${key}: ${styleMap[key]};`) - .join(' ') - ); -} - -/** - * @param {string[]} names - * @returns {MemberNode} - */ -export function className(names) { - return attribute('class', names.filter(Boolean).join(' ')); -} - -/** - * @param {string} name - * @param {string | boolean} value - * @returns {MemberNode} - */ -export function attribute(name, value) { - return { - type: ATTRIBUTE_TYPE_NAME, - name, - value, - }; -} - -/** - * @param {string} name - * @param {any} value - * @returns {MemberNode} - */ -export function property(name, value) { - return { - type: PROPERTY_TYPE_NAME, - name, - value, - }; -} - -/** - * @param {string} name - * @param {any} value - * @returns {MemberNode} - */ -export function meta(name, value) { - return { - type: META_TYPE_NAME, - name, - value, - }; -} - -const refMap = new WeakMap(); - -/** - * @param {{ref: HTMLElement | null}} value - * @returns {MemberNode} - */ -export function ref(value) { - let refHook = refMap.get(value); - if (!refHook) { - refHook = (/** @type {HTMLElement} */ element) => { - value.ref = element; - }; - refMap.set(value, refHook); - } - return { - type: REF_TYPE_NAME, - name: 'ref', - value: refHook, - }; -} - -const noop = function () {}; - -/** - * @param {boolean} shouldFocus - */ -export function focus(shouldFocus) { - return { - type: REF_TYPE_NAME, - name: 'focus', - value: shouldFocus ? element => element.focus() : noop, - }; -} - -function asNode(item) { - if (typeof item === 'string') { - return text(item); - } else if (Array.isArray(item)) { - return fragment(...item); - } else if (item === null || item === undefined) { - return fragment(); - } - return item; -} - -const ELEMENT_TYPE_NAME = 'element'; -const FRAGMENT_TYPE_NAME = 'fragment'; -const COMPONENT_TYPE_NAME = 'component'; -const TEXT_TYPE_NAME = 'text'; -const ATTRIBUTE_TYPE_NAME = 'attribute'; -const PROPERTY_TYPE_NAME = 'property'; -const REF_TYPE_NAME = 'ref'; -const META_TYPE_NAME = 'meta'; - -/** @type ElementStateType */ -const ElementType = { - name: ELEMENT_TYPE_NAME, - diffEntry: /** @type {DiffEntryFunction} */ (diffChildEntry), - init: /** @type {InitNodeFunction} */ ( - function (queue, parent, after, /** @type {ElementNode} */ node) { - const state = { - type: ElementType, - parent, - after, - shape: node.shape, - content: null, - ref: null, - refHooks: null, - rewriteChildIndex: 0, - children: null, - rewriteMemberIndex: 0, - members: null, - }; - enqueueChange(queue, addElement, state); - return state; - } - ), - diff: /** @type {DiffFunction} */ ( - function (queue, /** @type {ElementState} */ lastValue, /** @type {ElementNode} */ newValue) { - lastValue.rewriteMemberIndex = 0; - diffFragment(queue, lastValue, newValue); - if (lastValue.members !== null) { - const group = lastValue.members; - let index; - for (index = lastValue.rewriteMemberIndex; index < group.length; index++) { - const node = group[index]; - node.type.teardown(queue, node); - } - if (lastValue.rewriteMemberIndex === 0) { - lastValue.members = null; - } else { - group.length = lastValue.rewriteMemberIndex; - } - } - } - ), - teardown: /** @type {TeardownFunction} */ ( - function (queue, /** @type {ElementState} */ state) { - enqueueChange(queue, removeElement, state); - const { children } = state; - if (children !== null) { - for (let i = 0; i < children.length; i++) { - children[i].type.softTeardown(children[i]); - } - } - } - ), - softTeardown: /** @type {SoftTeardownFunction} */ (softTeardownElement), -}; - -/** @type {FragmentStateType} */ -const FragmentType = { - name: FRAGMENT_TYPE_NAME, - diffEntry: /** @type {DiffEntryFunction} */ (diffChildEntry), - init(queue, parent, after) { - return { - type: FragmentType, - parent, - after, - shape: FRAGMENT_TYPE_NAME, - content: null, - rewriteChildIndex: 0, - children: null, - }; - }, - diff: /** @type {DiffFunction} */ (diffFragment), - teardown: /** @type {TeardownFunction} */ ( - function (queue, /** @type {FragmentState} */ state) { - const { children } = state; - if (children !== null) { - for (let i = 0; i < children.length; i++) { - children[i].type.teardown(queue, children[i]); - } - } - } - ), - softTeardown: /** @type {SoftTeardownFunction} */ (softTeardownElement), -}; - -/** @type ComponentStateType */ -const ComponentType = { - name: COMPONENT_TYPE_NAME, - diffEntry: /** @type {DiffEntryFunction} */ (diffChildEntry), - init: /** @type {InitNodeFunction} */ ( - function (queue, parent, after, /** @type {ComponentNode} */ node) { - return { - type: ComponentType, - parent, - after, - shape: node.shape, - content: null, - rendered: null, - }; - } - ), - diff: /** @type {DiffFunction} */ ( - function (queue, /** @type {ComponentState} */ lastChild, /** @type {ComponentNode} */ node) { - /** @type {MemberNode} */ - const componentOptionsMeta = node.content.find(isOptions) || null; - const componentOptions = componentOptionsMeta ? componentOptionsMeta.value : null; - if (shallowEquals(lastChild.content, componentOptions) === false) { - lastChild.content = componentOptions; - queue.prepare.push(lastChild); - } - } - ), - teardown: /** @type {TeardownFunction} */ ( - function (queue, /** @type {ComponentState} */ state) { - if (state.rendered !== null) { - state.rendered.type.teardown(queue, state.rendered); - } - } - ), - softTeardown: /** @type {SoftTeardownFunction} */ ( - function (/** @type {ComponentState} */ state) { - if (state.rendered !== null) { - state.rendered.type.softTeardown(state.rendered); - } - } - ), -}; - -/** @type TextStateType */ -const TextType = { - name: TEXT_TYPE_NAME, - diffEntry: /** @type {DiffEntryFunction} */ (diffChildEntry), - init: /** @type {InitNodeFunction} */ ( - function (queue, parent, after, /** @type {TextNode} */ node) { - /** @type {TextState} */ - const state = { - type: TextType, - parent, - after, - shape: TEXT_TYPE_NAME, - content: node.content, - ref: null, - }; - enqueueChange(queue, addText, state); - return state; - } - ), - diff: /** @type {DiffFunction} */ ( - function (queue, /** @type {TextState} */ lastChild, /** @type {TextNode} */ node) { - if (lastChild.content !== node.content) { - lastChild.content = node.content; - enqueueChange(queue, changeText, lastChild); - } - } - ), - teardown: /** @type {TeardownFunction} */ ( - function (queue, /** @type {TextState} */ state) { - enqueueChange(queue, removeText, state); - } - ), - softTeardown() {}, -}; - -/** @type {MemberStateType} */ -const AttributeType = { - name: ATTRIBUTE_TYPE_NAME, - diffEntry: /** @type {DiffEntryFunction} */ (diffMemberEntry), - init(parent, name) { - return { - type: AttributeType, - parent, - name, - value: null, - }; - }, - diff: /** @type {DiffFunction} */ ( - function (queue, /** @type {MemberState} */ old, /** @type {MemberNode} */ memberNode) { - if (old.value !== memberNode.value) { - old.value = memberNode.value; - if (old.value === false) { - enqueueChange(queue, removeAttribute, old); - } else { - enqueueChange(queue, changeAttribute, old); - } - } - } - ), - teardown(queue, state) { - enqueueChange(queue, removeAttribute, state); - }, -}; - -/** @type {MemberStateType} */ -const PropertyType = { - name: PROPERTY_TYPE_NAME, - diffEntry: /** @type {DiffEntryFunction} */ (diffMemberEntry), - init(parent, name) { - return { - type: PropertyType, - parent, - name, - value: null, - }; - }, - diff: /** @type {DiffFunction} */ ( - function (queue, /** @type {MemberState} */ old, /** @type {MemberNode} */ memberNode) { - if (old.value !== memberNode.value) { - old.value = memberNode.value; - enqueueChange(queue, changeProperty, old); - } - } - ), - teardown(queue, state) { - state.value = undefined; - enqueueChange(queue, changeProperty, state); - }, -}; - -/** @type {MemberStateType} */ -const RefType = { - name: REF_TYPE_NAME, - diffEntry: /** @type {DiffEntryFunction} */ (diffMemberEntry), - init(parent, name) { - return { - type: RefType, - parent, - name, - value: null, - }; - }, - diff: /** @type {DiffFunction} */ ( - function (queue, /** @type {MemberState} */ state, /** @type {MemberNode} */ node) { - if (state.value !== node.value) { - state.value = node.value; - if (state.parent.refHooks === null) { - state.parent.refHooks = []; - } - if (state.parent.refHooks.indexOf(state) === -1) { - state.parent.refHooks.push(state); - } - enqueuePost(queue, updateRef, state); - } - } - ), - teardown(queue, state) { - const index = state.parent.refHooks.indexOf(state); - state.parent.refHooks.splice(index, 1); - if (state.parent.refHooks.length === 0) { - state.parent.refHooks = null; - } - enqueuePost(queue, unsetRef, state); - }, -}; - -const typeMap = { - [ELEMENT_TYPE_NAME]: ElementType, - [FRAGMENT_TYPE_NAME]: FragmentType, - [COMPONENT_TYPE_NAME]: ComponentType, - [TEXT_TYPE_NAME]: TextType, - [ATTRIBUTE_TYPE_NAME]: AttributeType, - [PROPERTY_TYPE_NAME]: PropertyType, - [REF_TYPE_NAME]: RefType, -}; - -/** @type DiffEntryFunction */ -function diffChildEntry( - queue, - parent, - /** @type NodeStateType */ metaType, - /** @type NodeNode */ element -) { - if (!parent.children) { - parent.children = []; - } - const index = parent.rewriteChildIndex++; - let state = parent.children[index]; - if (!state || state.shape !== element.shape) { - if (state) { - state.type.teardown(queue, state); - } - state = /** @type {NodeState} */ ( - metaType.init(queue, parent, parent.children[index - 1] || null, element) - ); - parent.children[index] = state; - - const sibling = parent.children[index + 1]; - if (sibling) { - sibling.after = state; - } - } - state.type.diff(queue, state, element); -} - -/** @type {DiffFunction} */ -function diffFragment( - queue, - /** @type {ElementState | FragmentState} */ lastValue, - /** @type {ElementNode | FragmentNode} */ newValue -) { - lastValue.rewriteChildIndex = 0; - const { content } = newValue; - for (let i = 0; i < content.length; i++) { - const node = content[i]; - const metaType = typeMap[node.type]; - metaType.diffEntry(queue, lastValue, metaType, node); - } - const children = lastValue.children; - if (children !== null) { - const childIndex = lastValue.rewriteChildIndex; - for (let i = childIndex; i < children.length; i++) { - const node = children[i]; - node.type.teardown(queue, node); - } - if (childIndex === 0) { - lastValue.children = null; - } else { - children.length = childIndex; - } - } -} - -/** @type {DiffEntryFunction} */ -function diffMemberEntry( - queue, - /** @type {ElementState} */ element, - /** @type {MemberStateType} */ nodeType, - /** @type {MemberNode} */ node -) { - if (element.members === null) { - element.members = []; - } - const group = element.members; - - const writeIndex = element.rewriteMemberIndex++; - let index; - for (index = writeIndex; index < group.length; index++) { - const item = group[index]; - if (item.type.name === node.type && item.name === node.name) { - break; - } - } - - let old = group[index]; - if (index !== writeIndex) { - group[index] = group[writeIndex]; - } - if (!old) { - old = nodeType.init(element, node.name); - group[writeIndex] = old; - } - old.type.diff(queue, old, node); -} - -/** @type {SoftTeardownFunction} */ -function softTeardownElement(/** @type {ElementState} */ state) { - const { children } = state; - if (children !== null) { - for (let i = 0; i < children.length; i++) { - children[i].type.softTeardown(children[i]); - } - } -} - -/** - * @param {Data} node - * @returns {node is MemberNode} - */ -function isOptions(node) { - return node.type === META_TYPE_NAME && node.name === 'options'; -} - -/** - * @param {Queue} queue - */ -function runQueue(queue) { - const { prepare, changes: apply, post } = queue; - for (let i = 0; i < prepare.length; i++) { - changeViewRender(prepare[i], queue); - } - for (let i = 0; i < apply.length; i += 2) { - apply[i](apply[i + 1]); - } - for (let i = 0; i < post.length; i += 2) { - post[i](post[i + 1]); - } -} - -/** - * @param {ComponentState} componentState - * @param {Queue} queue - */ -function changeViewRender(componentState, queue) { - const newRender = componentState.shape(componentState.content) || null; - if (newRender) { - const metaType = typeMap[newRender.type]; - let lastRender = componentState.rendered; - if (!lastRender || lastRender.shape !== newRender.shape) { - if (lastRender) { - lastRender.type.teardown(queue, lastRender); - } - lastRender = metaType.init(queue, componentState, null, newRender); - componentState.rendered = lastRender; - } - lastRender.type.diff(queue, lastRender, newRender); - } else if (componentState.rendered) { - componentState.rendered.type.teardown(queue, componentState.rendered); - componentState.rendered = null; - } -} - -/** - * @param {MemberState} state - */ -function changeProperty(state) { - state.parent.ref[state.name] = state.value; -} - -/** - * @param {MemberState} state - */ -function removeAttribute(state) { - state.parent.ref.removeAttribute(state.name); -} - -/** - * @param {MemberState} state - */ -function changeAttribute(state) { - state.parent.ref.setAttribute(state.name, state.value); -} - -/** - * @param {TextState} state - */ -function removeText(state) { - state.ref.parentNode.removeChild(state.ref); -} - -/** - * @param {TextState} state - */ -function changeText(state) { - state.ref.textContent = state.content; -} - -/** - * @param {TextState} state - */ -function addText(state) { - state.ref = document.createTextNode(state.content); - state.ref.textContent = state.content; - insertAfterSibling(state); -} - -/** - * @param {NodeState} after - * @returns {NodeState} - */ -function deepestDescendant(after) { - if (after === null) { - return after; - } else if (after.type === FragmentType) { - const { children } = /** @type {FragmentState} */ (after); - if (children !== null) { - return deepestDescendant(children[children.length - 1]); - } - } else if (after.type === ComponentType) { - const { rendered } = /** @type {ComponentState} */ (after); - if (rendered !== null) { - return deepestDescendant(rendered); - } - } - return after; -} - -/** - * @param {ElementState | TextState} element - */ -function insertAfterSibling(element) { - /** @type {NodeState} */ - let state = element; - let after = deepestDescendant(state.after); - do { - if (after === null) { - if (state.parent.type === ElementType) { - break; - } - state = state.parent; - after = state; - } - if (after.type !== ElementType && after.type !== TextType) { - after = deepestDescendant(after.after); - } - } while (after === null || (after.type !== ElementType && after.type !== TextType)); - - if (after !== null) { - const { ref } = /** @type {ElementState | TextState} */ (after); - ref.parentNode.insertBefore(element.ref, ref.nextSibling); - } else { - const { ref } = /** @type {ElementState} */ (state.parent); - if (ref.childNodes.length) { - ref.insertBefore(element.ref, ref.childNodes[0]); - } else { - ref.appendChild(element.ref); - } - } -} - -/** - * @param {ElementState} state - */ -function removeElement(state) { - state.ref.parentNode.removeChild(state.ref); - state.ref = null; -} - -/** - * @param {ElementState} state - */ -function addElement(state) { - state.ref = document.createElement(state.shape); - insertAfterSibling(state); -} - -/** - * @param {MemberState} state - */ -function updateRef(state) { - state.value(state.parent.ref); -} - -/** - * @param {MemberState} state - */ -function unsetRef(state) { - state.value(null); -} - -function shallowEquals(a, b) { - if (a === null || b === null) { - return a === b; - } - for (const key of Object.keys(a)) { - if (key in b) { - if (a[key] !== b[key]) { - return false; - } - } else { - return false; - } - } - for (const key of Object.keys(b)) { - if (!(key in a)) { - return false; - } - } - return true; -} - -/** - * @callback StateAction - * @param {S} state - * @template {State} S - */ - -/** - * @returns {Queue} - */ -function newQueue() { - return { prepare: [], changes: [], post: [] }; -} - -/** - * @param {Queue} queue - * @param {StateAction} fn - * @param {S} state - * @template {State} S - */ -function enqueueChange(queue, fn, state) { - queue.changes.push(fn, state); -} - -/** - * @param {Queue} queue - * @param {StateAction} fn - * @param {S} state - * @template {State} S - */ -function enqueuePost(queue, fn, state) { - queue.post.push(fn, state); -} - -/** - * @typedef Queue - * @property {Array} prepare - * @property {Array} changes - * @property {Array} post - */ - -/** - * @callback DiffEntryFunction - * @param {Queue} queue - * @param {ElementState | FragmentState} parent - * @param {StateType} nodeType - * @param {Data} node - * @returns {void} - */ - -/** - * @callback InitNodeFunction - * @param {Queue} queue - * @param {NodeState} parent - * @param {NodeState | null} after - * @param {NodeNode} node - * @returns {NodeState} - */ - -/** - * @callback DiffFunction - * @param {Queue} queue - * @param {State} state - * @param {Data} node - * @returns {void} - */ - -/** - * @callback TeardownFunction - * @param {Queue} queue - * @param {NodeState} state - * @returns {void} - */ - -/** - * @callback SoftTeardownFunction - * @param {NodeState} state - * @returns {void} - */ - -/** - * @typedef NodeStateTypeGeneric - * @property {Name} name - * @property {DiffEntryFunction} diffEntry - * @property {DiffFunction} diff - * @property {InitNodeFunction} init - * @property {TeardownFunction} teardown - * @property {SoftTeardownFunction} softTeardown - * @template {string | symbol} Name - */ - -/** - * @typedef NodeStateTypeCreate - * @property {Name} name - * @property {(queue: Queue, parent: ElementState | FragmentState, meta: StateType, node: Node) => void} diffEntry - * @property {(queue: Queue, state: S, node: Node) => void} diff - * @property {(queue: Queue, parent: ElementState | FragmentState, after: NodeState, node: Node) => void} init - * @property {(queue: Queue, state: S) => void} teardown - * @property {(state: S) => void} softTeardown - * @template {string | symbol} Name - * @template {State} S - * @template {Data} Node - */ - -/** @typedef {NodeStateTypeGeneric} ElementStateType */ -/** @typedef {NodeStateTypeGeneric} FragmentStateType */ -/** @typedef {NodeStateTypeGeneric} ComponentStateType */ -/** @typedef {NodeStateTypeGeneric} TextStateType */ - -/** @typedef {ElementStateType | FragmentStateType| ComponentStateType | TextStateType} NodeStateType */ - -/** - * @callback InitMemberFunction - * @param {ElementState} parent - * @param {string} name - * @returns {MemberState} - */ - -/** - * @callback TeardownMemberFunction - * @param {Queue} queue - * @param {MemberState} member - * @returns {void} - */ - -/** - * @typedef MemberStateType - * @property {string | symbol} name - * @property {DiffEntryFunction} diffEntry - * @property {DiffFunction} diff - * @property {InitMemberFunction} init - * @property {TeardownMemberFunction} teardown - */ - -/** - * @typedef {NodeStateType | MemberStateType} StateType - */ - -/** - * @typedef ElementState - * @property {ElementStateType} type - * @property {NodeState} parent - * @property {NodeState | null} after - * @property {string} shape - * @property {null} content - * @property {HTMLElement | null} ref - * @property {MemberState[] | null} refHooks - * @property {number} rewriteChildIndex - * @property {NodeState[] | null} children - * @property {number} rewriteMemberIndex - * @property {MemberState[] | null} members - */ - -/** - * @typedef FragmentState - * @property {FragmentStateType} type - * @property {NodeState} parent - * @property {NodeState | null} after - * @property {typeof FRAGMENT_TYPE_NAME} shape - * @property {null} content - * @property {number} rewriteChildIndex - * @property {NodeState[] | null} children - */ - -/** @typedef {ElementState | FragmentState} ParentState */ - -/** - * @typedef ComponentState - * @property {ComponentStateType} type - * @property {NodeState} parent - * @property {NodeState | null} after - * @property {function} shape - * @property {object} content - * @property {NodeState | null} rendered - */ - -/** - * @typedef TextState - * @property {TextStateType} type - * @property {NodeState} parent - * @property {NodeState | null} after - * @property {typeof TEXT_TYPE_NAME} shape - * @property {string} content - * @property {Text | null} ref - */ - -/** - * @typedef {ElementState | FragmentState | ComponentState | TextState} NodeState - */ - -/** - * @typedef MemberState - * @property {MemberStateType} type - * @property {ElementState} parent - * @property {string} name - * @property {*} value - */ - -/** @typedef {NodeState | MemberState} State */ - -/** @typedef {typeof ELEMENT_TYPE_NAME | typeof FRAGMENT_TYPE_NAME | typeof COMPONENT_TYPE_NAME | typeof TEXT_TYPE_NAME} NodeNodeType */ - -/** @typedef {typeof ATTRIBUTE_TYPE_NAME | typeof PROPERTY_TYPE_NAME | typeof REF_TYPE_NAME | typeof META_TYPE_NAME} MemberNodeType */ - -/** - * @typedef ElementNode - * @property {typeof ELEMENT_TYPE_NAME} type - * @property {string} shape - * @property {Data[]} content - */ - -/** - * @typedef FragmentNode - * @property {typeof FRAGMENT_TYPE_NAME} type - * @property {typeof FRAGMENT_TYPE_NAME} shape - * @property {Data[]} content - */ - -/** - * @typedef TextNode - * @property {typeof TEXT_TYPE_NAME} type - * @property {typeof TEXT_TYPE_NAME} shape - * @property {string} content - */ - -/** - * @typedef ComponentNode - * @property {typeof COMPONENT_TYPE_NAME} type - * @property {function} shape - * @property {Data[]} content - */ - -/** - * @typedef {ElementNode | FragmentNode | TextNode | ComponentNode} NodeNode - */ - -/** - * @typedef MemberNode - * @property {MemberNodeType} type - * @property {string} name - * @property {*} value - */ - -/** @typedef {NodeNode | MemberNode} Data */