From e59a00ef66c2cd9439dc6e5479cfec7b93215c67 Mon Sep 17 00:00:00 2001 From: "Mx. Corey Frang" Date: Mon, 15 Jul 2024 14:56:17 -0400 Subject: [PATCH 1/8] configurable timeout and delay values --- .../create-safari-apple-script-driver.js | 6 +-- src/agent/cli.js | 3 ++ src/agent/driver-test-runner.js | 23 +++------- src/host/cli-run-plan.js | 2 + src/shared/times-option.js | 42 +++++++++++++++++++ 5 files changed, 55 insertions(+), 21 deletions(-) create mode 100644 src/shared/times-option.js diff --git a/src/agent/browser-driver/create-safari-apple-script-driver.js b/src/agent/browser-driver/create-safari-apple-script-driver.js index 98039ea..ac83e12 100644 --- a/src/agent/browser-driver/create-safari-apple-script-driver.js +++ b/src/agent/browser-driver/create-safari-apple-script-driver.js @@ -1,7 +1,5 @@ import { execFile } from 'child_process'; - -/** Number of milliseconds to wait for document to be ready before giving up. */ -const DOCUMENT_READY_TIMEOUT = 2000; +import { timesOption } from '../../shared/times-option.js'; /** * @param {string} source @@ -60,7 +58,7 @@ export default async () => { async documentReady() { const start = Date.now(); - while (Date.now() - start < DOCUMENT_READY_TIMEOUT) { + while (Date.now() - start < timesOption.docReady) { const readyState = await evalJavaScript('document.readyState'); if (readyState === 'complete') { return; diff --git a/src/agent/cli.js b/src/agent/cli.js index bfdb09a..c476cca 100644 --- a/src/agent/cli.js +++ b/src/agent/cli.js @@ -12,6 +12,7 @@ import { iterateEmitter } from '../shared/iterate-emitter.js'; import { createRunner } from './create-test-runner.js'; import { agentMain } from './main.js'; import { AgentMessage, createAgentLogger } from './messages.js'; +import { timesArgs, timesOptionDescription } from '../shared/times-option.js'; /** @param {yargs} args */ export function buildAgentCliOptions(args = yargs) { @@ -76,6 +77,7 @@ export function buildAgentCliOptions(args = yargs) { choices: ['request', 'skip'], hidden: true, }, + times: timesOptionDescription, }) .showHidden('show-hidden'); } @@ -86,6 +88,7 @@ export function buildAgentCliOptions(args = yargs) { */ export function agentCliArgsFromOptionsMap(options) { const args = []; + args.push('--times', timesArgs()); for (const key of Object.keys(options)) { const value = options[key]; switch (key) { diff --git a/src/agent/driver-test-runner.js b/src/agent/driver-test-runner.js index 6c2c9fa..c9fd4c3 100644 --- a/src/agent/driver-test-runner.js +++ b/src/agent/driver-test-runner.js @@ -3,6 +3,7 @@ /// import { startJob } from '../shared/job.js'; +import { timesOption } from '../shared/times-option.js'; import { ATDriver, ATKey, webDriverCodePoints } from './at-driver.js'; import { AgentMessage } from './messages.js'; @@ -11,10 +12,6 @@ import { AgentMessage } from './messages.js'; * @module agent */ -const AFTER_NAVIGATION_DELAY = 1000; -const AFTER_KEYS_DELAY = 5000; -const RUN_TEST_SETUP_BUTTON_TIMEOUT = 1000; - export class DriverTestRunner { /** * @param {object} options @@ -49,10 +46,7 @@ export class DriverTestRunner { await this.browserDriver.documentReady(); try { - await this.browserDriver.clickWhenPresent( - '.button-run-test-setup', - RUN_TEST_SETUP_BUTTON_TIMEOUT - ); + await this.browserDriver.clickWhenPresent('.button-run-test-setup', timesOption.testSetup); } catch ({}) { await this.log(AgentMessage.NO_RUN_TEST_SETUP, { referencePage }); } @@ -71,15 +65,10 @@ export class DriverTestRunner { * @param {string} desiredResponse */ async pressKeysToToggleSetting(sequence, desiredResponse) { - // This timeout may be reached as many as two times for every test. - // Delays of over 500ms have been observed during local testing in a - // Windows virtual machine. - const MODE_SWITCH_SPEECH_TIMEOUT = 750; - let unknownCollected = ''; // there are 2 modes, so we will try pressing mode switch up to twice for (let triesRemain = 2; triesRemain > 0; triesRemain--) { - const speechResponse = await this._collectSpeech(MODE_SWITCH_SPEECH_TIMEOUT, () => + const speechResponse = await this._collectSpeech(timesOption.modeSwitch, () => this.sendKeys(sequence) ); while (speechResponse.length) { @@ -202,7 +191,7 @@ export class DriverTestRunner { const { value: validCommand, errors } = validateKeysFromCommand(command); if (validCommand) { - await this._collectSpeech(AFTER_NAVIGATION_DELAY, () => + await this._collectSpeech(timesOption.afterNav, () => this.openPage({ url: this._appendBaseUrl(test.target.referencePage), referencePage: test.target.referencePage, @@ -217,11 +206,11 @@ export class DriverTestRunner { await this.ensureMode(test.target.mode); } - const spokenOutput = await this._collectSpeech(AFTER_KEYS_DELAY, () => + const spokenOutput = await this._collectSpeech(timesOption.afterKeys, () => this.sendKeys(atKeysFromCommand(validCommand)) ); - await this._collectSpeech(AFTER_NAVIGATION_DELAY, async () => { + await this._collectSpeech(timesOption.afterNav, async () => { await this.log(AgentMessage.OPEN_PAGE, { url: 'about:blank' }); await this.browserDriver.navigate('about:blank'); }); diff --git a/src/host/cli-run-plan.js b/src/host/cli-run-plan.js index ee1b311..3e962c7 100644 --- a/src/host/cli-run-plan.js +++ b/src/host/cli-run-plan.js @@ -15,6 +15,7 @@ import { hostMain } from './main.js'; import { HostMessage, createHostLogger } from './messages.js'; import { plansFrom } from './plan-from.js'; import { HostServer } from './server.js'; +import { timesOptionDescription } from '../shared/times-option.js'; export const command = 'run-plan [plan-files..]'; @@ -156,6 +157,7 @@ export const builder = (args = yargs) => return { [name]: value }; }, }, + times: timesOptionDescription, }) .showHidden('show-hidden') .middleware(verboseMiddleware) diff --git a/src/shared/times-option.js b/src/shared/times-option.js new file mode 100644 index 0000000..7a4333a --- /dev/null +++ b/src/shared/times-option.js @@ -0,0 +1,42 @@ +// modeSwitch - After switching modes, delay this long to receive speech. +// afterNav - Delay this long after a navigation to receive and clear speech. +// afterKeys - After pressing a key, delay this long to receive and record speech. +// testSetup - Delay this long after pressing the test setup to receive and clear speech. +// docReady - Wait this long for document to be ready (safari only). + +export const timesOption = { + afterNav: 1000, + afterKeys: 5000, + testSetup: 1000, + modeSwitch: 750, + docReady: 2000, +}; + +const timesDefaults = { ...timesOption }; + +export function timesArgs(opts = timesOption) { + return Object.entries(opts) + .map(([key, value]) => `${key}:${value}`) + .join(','); +} + +export const timesOptionDescription = { + // hidden by default because it's a really long one + hidden: true, + default: timesArgs(timesDefaults), + describe: 'Configure timeout and delays', + coerce(arg) { + if (!arg) return; + const parts = arg.split(/,/g); + for (const part of parts) { + const match = part.match(/^([^:]+):(\d+)$/); + if (!match) { + throw new Error(`Error parsing times "${part}"`); + } + if (!timesOption[match[1]]) { + throw new Error(`Unknown times param "${match[1]}"`); + } + timesOption[match[1]] = parseInt(match[2], 10); + } + }, +}; From cdeffd1cc2980474283106ff7dd39b9faf4fe9ca Mon Sep 17 00:00:00 2001 From: "Mx. Corey Frang" Date: Mon, 15 Jul 2024 15:19:57 -0400 Subject: [PATCH 2/8] types draft --- src/shared/times-option.js | 21 ++++++++++++++++----- src/shared/types.js | 9 +++++++++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/shared/times-option.js b/src/shared/times-option.js index 7a4333a..3c9d82c 100644 --- a/src/shared/times-option.js +++ b/src/shared/times-option.js @@ -1,9 +1,12 @@ -// modeSwitch - After switching modes, delay this long to receive speech. -// afterNav - Delay this long after a navigation to receive and clear speech. -// afterKeys - After pressing a key, delay this long to receive and record speech. -// testSetup - Delay this long after pressing the test setup to receive and clear speech. -// docReady - Wait this long for document to be ready (safari only). +/// +/** + * @module shared + */ + +/** + * @type AriaATCIShared.timesOption + */ export const timesOption = { afterNav: 1000, afterKeys: 5000, @@ -14,12 +17,20 @@ export const timesOption = { const timesDefaults = { ...timesOption }; +/** + * Convert the times dictionary to a string to pass to args printout. + * @param {AriaATCIShared.timesOption} opts + * @returns String + */ export function timesArgs(opts = timesOption) { return Object.entries(opts) .map(([key, value]) => `${key}:${value}`) .join(','); } +/** + * the yargs setup for the times option + */ export const timesOptionDescription = { // hidden by default because it's a really long one hidden: true, diff --git a/src/shared/types.js b/src/shared/types.js index b0a1d0d..efcc49b 100644 --- a/src/shared/types.js +++ b/src/shared/types.js @@ -47,3 +47,12 @@ * @param {AriaATCIShared.JobBinding<*>} binding * @returns {Promise} */ + +/** + * @typedef AriaATCIShared.timesOption + * @property {Number} afterNav Timeout used after navigation to collect and discard speech. + * @property {Number} afterKeys Timeout used to wait for speech to finish after pressing keys. + * @property {Number} testSetup Timeout used after pressing test setup button to collect and discard speech. + * @property {Number} modeSwitch Timeout used after switching modes to check resulting speech (NVDA). + * @property {Number} docReady Timeout used waiting for document ready (Safari). + */ From 85864e6ba5bd13c9604f5f1c6c592be1c9bd1f66 Mon Sep 17 00:00:00 2001 From: "Mx. Corey Frang" Date: Wed, 17 Jul 2024 10:56:48 -0400 Subject: [PATCH 3/8] individual options for each time --- src/agent/cli.js | 6 +-- src/host/cli-run-plan.js | 4 +- src/shared/times-option.js | 75 +++++++++++++++++++++++--------------- 3 files changed, 51 insertions(+), 34 deletions(-) diff --git a/src/agent/cli.js b/src/agent/cli.js index c476cca..8ac106a 100644 --- a/src/agent/cli.js +++ b/src/agent/cli.js @@ -12,7 +12,7 @@ import { iterateEmitter } from '../shared/iterate-emitter.js'; import { createRunner } from './create-test-runner.js'; import { agentMain } from './main.js'; import { AgentMessage, createAgentLogger } from './messages.js'; -import { timesArgs, timesOptionDescription } from '../shared/times-option.js'; +import { timesArgs, timesOptionsConfig } from '../shared/times-option.js'; /** @param {yargs} args */ export function buildAgentCliOptions(args = yargs) { @@ -77,7 +77,7 @@ export function buildAgentCliOptions(args = yargs) { choices: ['request', 'skip'], hidden: true, }, - times: timesOptionDescription, + ...timesOptionsConfig, }) .showHidden('show-hidden'); } @@ -88,7 +88,7 @@ export function buildAgentCliOptions(args = yargs) { */ export function agentCliArgsFromOptionsMap(options) { const args = []; - args.push('--times', timesArgs()); + args.push(...timesArgs()); for (const key of Object.keys(options)) { const value = options[key]; switch (key) { diff --git a/src/host/cli-run-plan.js b/src/host/cli-run-plan.js index 3e962c7..ad043d8 100644 --- a/src/host/cli-run-plan.js +++ b/src/host/cli-run-plan.js @@ -15,7 +15,7 @@ import { hostMain } from './main.js'; import { HostMessage, createHostLogger } from './messages.js'; import { plansFrom } from './plan-from.js'; import { HostServer } from './server.js'; -import { timesOptionDescription } from '../shared/times-option.js'; +import { timesOptionsConfig } from '../shared/times-option.js'; export const command = 'run-plan [plan-files..]'; @@ -157,7 +157,7 @@ export const builder = (args = yargs) => return { [name]: value }; }, }, - times: timesOptionDescription, + ...timesOptionsConfig, }) .showHidden('show-hidden') .middleware(verboseMiddleware) diff --git a/src/shared/times-option.js b/src/shared/times-option.js index 3c9d82c..1e6c74f 100644 --- a/src/shared/times-option.js +++ b/src/shared/times-option.js @@ -17,37 +17,54 @@ export const timesOption = { const timesDefaults = { ...timesOption }; +function makeSnakeCasedOption(optionName, description) { + const snakeCased = optionName.replace(/[A-Z]/g, cap => '-' + cap.toLowerCase()); + const optionText = `time-${snakeCased}`; + return optionText; +} + +function addOptionConfig(optionName, describe) { + timesOptionsConfig[makeSnakeCasedOption(optionName)] = { + hidden: true, + default: timesOption[optionName], + describe, + coerce(arg) { + const isNumber = typeof arg === 'number'; + if (!isNumber && !arg.match(/^\d+$/)) { + throw new Error('option value not a number'); + } + const time = isNumber ? arg : parseInt(arg, 10); + if (time <= 0) { + throw new Error('time must be positive and non-zero'); + } + timesOption[optionName] = time; + }, + }; +} + /** - * Convert the times dictionary to a string to pass to args printout. - * @param {AriaATCIShared.timesOption} opts - * @returns String + * the yargs configuration for the time options */ -export function timesArgs(opts = timesOption) { - return Object.entries(opts) - .map(([key, value]) => `${key}:${value}`) - .join(','); -} +export const timesOptionsConfig = {}; +addOptionConfig('afterNav', 'Timeout used after navigation to collect and discard speech.'); +addOptionConfig('afterKeys', 'Timeout used to wait for speech to finish after pressing keys.'); +addOptionConfig( + 'testSetup', + 'Timeout used after pressing test setup button to collect and discard speech.' +); +addOptionConfig( + 'modeSwitch', + 'Timeout used after switching modes to check resulting speech (NVDA).' +); +addOptionConfig('docReady', 'Timeout used waiting for document ready (Safari).'); /** - * the yargs setup for the times option + * Convert the times dictionary to an array of strings to pass back to args. + * @param {AriaATCIShared.timesOption} opts + * @returns [String] */ -export const timesOptionDescription = { - // hidden by default because it's a really long one - hidden: true, - default: timesArgs(timesDefaults), - describe: 'Configure timeout and delays', - coerce(arg) { - if (!arg) return; - const parts = arg.split(/,/g); - for (const part of parts) { - const match = part.match(/^([^:]+):(\d+)$/); - if (!match) { - throw new Error(`Error parsing times "${part}"`); - } - if (!timesOption[match[1]]) { - throw new Error(`Unknown times param "${match[1]}"`); - } - timesOption[match[1]] = parseInt(match[2], 10); - } - }, -}; +export function timesArgs(opts = timesOption) { + return Object.entries(opts).flatMap(([key, value]) => + value === timesDefaults[key] ? [] : ['--' + makeSnakeCasedOption(key), String(value)] + ); +} From 01d05fbc1def2ed420a66610007eb7bfd3d8e756 Mon Sep 17 00:00:00 2001 From: "Mx. Corey Frang" Date: Wed, 17 Jul 2024 11:36:18 -0400 Subject: [PATCH 4/8] more restrictive types for the helper functions --- src/shared/times-option.js | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/src/shared/times-option.js b/src/shared/times-option.js index 1e6c74f..82ee7d3 100644 --- a/src/shared/times-option.js +++ b/src/shared/times-option.js @@ -15,14 +15,27 @@ export const timesOption = { docReady: 2000, }; +/** + * @type AriaATCIShared.timesOption + */ const timesDefaults = { ...timesOption }; -function makeSnakeCasedOption(optionName, description) { +/** + * Convert from 'afterNav' to 'time-after-nav'. + * @param {keyof AriaATCIShared.timesOption} optionName + * @returns String + */ +function makeSnakeCasedOption(optionName) { const snakeCased = optionName.replace(/[A-Z]/g, cap => '-' + cap.toLowerCase()); const optionText = `time-${snakeCased}`; return optionText; } +/** + * Create a yargs description for the specified timesOption. + * @param {keyof AriaATCIShared.timesOption} optionName Key from timesOption + * @param {String} describe Description to be used in --show-help + */ function addOptionConfig(optionName, describe) { timesOptionsConfig[makeSnakeCasedOption(optionName)] = { hidden: true, @@ -61,10 +74,18 @@ addOptionConfig('docReady', 'Timeout used waiting for document ready (Safari).') /** * Convert the times dictionary to an array of strings to pass back to args. * @param {AriaATCIShared.timesOption} opts - * @returns [String] + * @returns [string] */ export function timesArgs(opts = timesOption) { - return Object.entries(opts).flatMap(([key, value]) => - value === timesDefaults[key] ? [] : ['--' + makeSnakeCasedOption(key), String(value)] - ); + const args = []; + for (const key of Object.keys(timesOption)) { + const value = timesOption[key]; + // no need to pass on "default" value + if (value === timesDefaults[key]) continue; + // casting in jsdoc syntax is complicated - the extra () around key are + // required to make the type annotation work. + args.push(makeSnakeCasedOption(/** @type {keyof AriaATCIShared.timesOption} */ (key))); + args.push(String(value)); + } + return args; } From c644f56b7e48361cbce3ada14bc040162e50b449 Mon Sep 17 00:00:00 2001 From: "Mx. Corey Frang" Date: Wed, 17 Jul 2024 11:37:04 -0400 Subject: [PATCH 5/8] prefer lowercase string and number type (seems to be tsc standard) --- src/shared/times-option.js | 4 ++-- src/shared/types.js | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/shared/times-option.js b/src/shared/times-option.js index 82ee7d3..df80676 100644 --- a/src/shared/times-option.js +++ b/src/shared/times-option.js @@ -23,7 +23,7 @@ const timesDefaults = { ...timesOption }; /** * Convert from 'afterNav' to 'time-after-nav'. * @param {keyof AriaATCIShared.timesOption} optionName - * @returns String + * @returns string */ function makeSnakeCasedOption(optionName) { const snakeCased = optionName.replace(/[A-Z]/g, cap => '-' + cap.toLowerCase()); @@ -34,7 +34,7 @@ function makeSnakeCasedOption(optionName) { /** * Create a yargs description for the specified timesOption. * @param {keyof AriaATCIShared.timesOption} optionName Key from timesOption - * @param {String} describe Description to be used in --show-help + * @param {string} describe Description to be used in --show-help */ function addOptionConfig(optionName, describe) { timesOptionsConfig[makeSnakeCasedOption(optionName)] = { diff --git a/src/shared/types.js b/src/shared/types.js index efcc49b..8df72e0 100644 --- a/src/shared/types.js +++ b/src/shared/types.js @@ -50,9 +50,9 @@ /** * @typedef AriaATCIShared.timesOption - * @property {Number} afterNav Timeout used after navigation to collect and discard speech. - * @property {Number} afterKeys Timeout used to wait for speech to finish after pressing keys. - * @property {Number} testSetup Timeout used after pressing test setup button to collect and discard speech. - * @property {Number} modeSwitch Timeout used after switching modes to check resulting speech (NVDA). - * @property {Number} docReady Timeout used waiting for document ready (Safari). + * @property {number} afterNav Timeout used after navigation to collect and discard speech. + * @property {number} afterKeys Timeout used to wait for speech to finish after pressing keys. + * @property {number} testSetup Timeout used after pressing test setup button to collect and discard speech. + * @property {number} modeSwitch Timeout used after switching modes to check resulting speech (NVDA). + * @property {number} docReady Timeout used waiting for document ready (Safari). */ From 0d7edebe46938b06943ef2deda2f9ea22be2a35b Mon Sep 17 00:00:00 2001 From: "Mx. Corey Frang" Date: Mon, 22 Jul 2024 12:16:06 -0400 Subject: [PATCH 6/8] remove makeSnakeCasedKey function --- src/shared/times-option.js | 46 ++++++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/src/shared/times-option.js b/src/shared/times-option.js index df80676..620ebce 100644 --- a/src/shared/times-option.js +++ b/src/shared/times-option.js @@ -20,24 +20,15 @@ export const timesOption = { */ const timesDefaults = { ...timesOption }; -/** - * Convert from 'afterNav' to 'time-after-nav'. - * @param {keyof AriaATCIShared.timesOption} optionName - * @returns string - */ -function makeSnakeCasedOption(optionName) { - const snakeCased = optionName.replace(/[A-Z]/g, cap => '-' + cap.toLowerCase()); - const optionText = `time-${snakeCased}`; - return optionText; -} - /** * Create a yargs description for the specified timesOption. * @param {keyof AriaATCIShared.timesOption} optionName Key from timesOption + * @param {string} argName The text used for the argument (without leading --) * @param {string} describe Description to be used in --show-help */ -function addOptionConfig(optionName, describe) { - timesOptionsConfig[makeSnakeCasedOption(optionName)] = { +function addOptionConfig(optionName, argName, describe) { + timesOptionsArgNameMap.set(optionName, argName); + timesOptionsConfig[argName] = { hidden: true, default: timesOption[optionName], describe, @@ -51,25 +42,45 @@ function addOptionConfig(optionName, describe) { throw new Error('time must be positive and non-zero'); } timesOption[optionName] = time; + return time; }, }; } +/** + * @type Map + */ +const timesOptionsArgNameMap = new Map(); + /** * the yargs configuration for the time options */ export const timesOptionsConfig = {}; -addOptionConfig('afterNav', 'Timeout used after navigation to collect and discard speech.'); -addOptionConfig('afterKeys', 'Timeout used to wait for speech to finish after pressing keys.'); +addOptionConfig( + 'afterNav', + 'time-after-nav', + 'Timeout used after navigation to collect and discard speech.' +); +addOptionConfig( + 'afterKeys', + 'time-after-keys', + 'Timeout used to wait for speech to finish after pressing keys.' +); addOptionConfig( 'testSetup', + 'time-test-setup', 'Timeout used after pressing test setup button to collect and discard speech.' ); addOptionConfig( 'modeSwitch', + 'time-mode-switch', 'Timeout used after switching modes to check resulting speech (NVDA).' ); -addOptionConfig('docReady', 'Timeout used waiting for document ready (Safari).'); +addOptionConfig( + 'docReady', + 'time-mode-switch', + 'Timeout used waiting for document ready (Safari).' +); /** * Convert the times dictionary to an array of strings to pass back to args. @@ -84,7 +95,8 @@ export function timesArgs(opts = timesOption) { if (value === timesDefaults[key]) continue; // casting in jsdoc syntax is complicated - the extra () around key are // required to make the type annotation work. - args.push(makeSnakeCasedOption(/** @type {keyof AriaATCIShared.timesOption} */ (key))); + const argName = timesOptionsArgNameMap.get(key); + args.push('--' + argName); args.push(String(value)); } return args; From ea8d35153d6eae8355fe3adb674dfbc98301cfba Mon Sep 17 00:00:00 2001 From: "Mx. Corey Frang" Date: Mon, 22 Jul 2024 13:18:30 -0400 Subject: [PATCH 7/8] pass timesOption config to modules instead of stateful module --- .../create-safari-apple-script-driver.js | 6 ++- src/agent/browser-driver/create.js | 5 ++- src/agent/cli.js | 9 +++- src/agent/create-test-runner.js | 5 ++- src/agent/driver-test-runner.js | 18 +++++--- src/agent/types.js | 1 + src/host/agent.js | 1 + src/host/cli-run-plan.js | 5 ++- src/shared/times-option.js | 44 +++++++++++-------- 9 files changed, 60 insertions(+), 34 deletions(-) diff --git a/src/agent/browser-driver/create-safari-apple-script-driver.js b/src/agent/browser-driver/create-safari-apple-script-driver.js index ac83e12..6c82c2b 100644 --- a/src/agent/browser-driver/create-safari-apple-script-driver.js +++ b/src/agent/browser-driver/create-safari-apple-script-driver.js @@ -1,5 +1,4 @@ import { execFile } from 'child_process'; -import { timesOption } from '../../shared/times-option.js'; /** * @param {string} source @@ -41,7 +40,10 @@ const evalJavaScript = source => { end tell`); }; -export default async () => { +/** + * @param {AriaATCIShared.timesOption} timesOption + */ +export default async timesOption => { await execScript(`tell application "Safari" if documents = {} then make new document activate diff --git a/src/agent/browser-driver/create.js b/src/agent/browser-driver/create.js index dec933d..0192a55 100644 --- a/src/agent/browser-driver/create.js +++ b/src/agent/browser-driver/create.js @@ -6,13 +6,14 @@ import createSafariAppleScriptDriver from './create-safari-apple-script-driver.j * @param {{toString: function(): string}} options.url * @param {AriaATCIAgent.Browser} [options.browser] * @param {Promise} options.abortSignal + * @param {AriaATCIShared.timesOption} options.timesOption * * @returns {Promise} */ -export async function createBrowserDriver({ url, browser = 'firefox', abortSignal }) { +export async function createBrowserDriver({ url, browser = 'firefox', abortSignal, timesOption }) { const driver = browser === 'safari' - ? await createSafariAppleScriptDriver() + ? await createSafariAppleScriptDriver(timesOption) : await createWebDriver(browser, url.toString()); abortSignal.then(() => driver.quit()); return driver; diff --git a/src/agent/cli.js b/src/agent/cli.js index 8ac106a..66f116e 100644 --- a/src/agent/cli.js +++ b/src/agent/cli.js @@ -12,7 +12,7 @@ import { iterateEmitter } from '../shared/iterate-emitter.js'; import { createRunner } from './create-test-runner.js'; import { agentMain } from './main.js'; import { AgentMessage, createAgentLogger } from './messages.js'; -import { timesArgs, timesOptionsConfig } from '../shared/times-option.js'; +import { getTimesOption, timesArgs, timesOptionsConfig } from '../shared/times-option.js'; /** @param {yargs} args */ export function buildAgentCliOptions(args = yargs) { @@ -88,7 +88,6 @@ export function buildAgentCliOptions(args = yargs) { */ export function agentCliArgsFromOptionsMap(options) { const args = []; - args.push(...timesArgs()); for (const key of Object.keys(options)) { const value = options[key]; switch (key) { @@ -131,6 +130,9 @@ export function agentCliArgsFromOptionsMap(options) { case 'mockOpenPage': args.push(`--mock-open-page=${value}`); break; + case 'timesOption': + args.push(...timesArgs(value)); + break; default: throw new Error(`unknown agent cli argument ${key}`); } @@ -152,6 +154,7 @@ export function pickAgentCliOptions({ atDriverUrl, mock, mockOpenPage, + timesOption, }) { return { ...(debug === undefined ? {} : { debug }), @@ -163,6 +166,7 @@ export function pickAgentCliOptions({ ...(atDriverUrl === undefined ? {} : { atDriverUrl }), ...(mock === undefined ? {} : { mock }), ...(mockOpenPage === undefined ? {} : { mockOpenPage }), + timesOption, }; } @@ -248,6 +252,7 @@ async function agentRunnerMiddleware(argv) { webDriverBrowser: argv.webDriverBrowser, atDriverUrl: argv.atDriverUrl, abortSignal: argv.abortSignal, + timesOption: getTimesOption(argv), }); } diff --git a/src/agent/create-test-runner.js b/src/agent/create-test-runner.js index 203a647..4c89984 100644 --- a/src/agent/create-test-runner.js +++ b/src/agent/create-test-runner.js @@ -19,6 +19,7 @@ import { AgentMessage } from './messages.js'; * @param {AriaATCIAgent.Log} options.log * @param {AriaATCIAgent.MockOptions} [options.mock] * @param {AriaATCIAgent.Browser} [options.webDriverBrowser] + * @param {AriaATCIShared.timesOption} options.timesOption * @param {{toString: function(): string}} options.webDriverUrl * @returns {Promise} */ @@ -30,11 +31,13 @@ export async function createRunner(options) { return new MockTestRunner({ mock: options.mock, ...options }); } await new Promise(resolve => setTimeout(resolve, 1000)); + const { timesOption } = options; const [browserDriver, atDriver] = await Promise.all([ createBrowserDriver({ url: options.webDriverUrl, browser: options.webDriverBrowser, abortSignal: options.abortSignal, + timesOption, }).catch(cause => { throw new Error('Error initializing browser driver', { cause }); }), @@ -46,5 +49,5 @@ export async function createRunner(options) { throw new Error('Error connecting to at-driver', { cause }); }), ]); - return new DriverTestRunner({ ...options, browserDriver, atDriver }); + return new DriverTestRunner({ ...options, browserDriver, atDriver, timesOption }); } diff --git a/src/agent/driver-test-runner.js b/src/agent/driver-test-runner.js index c9fd4c3..2489f34 100644 --- a/src/agent/driver-test-runner.js +++ b/src/agent/driver-test-runner.js @@ -3,7 +3,6 @@ /// import { startJob } from '../shared/job.js'; -import { timesOption } from '../shared/times-option.js'; import { ATDriver, ATKey, webDriverCodePoints } from './at-driver.js'; import { AgentMessage } from './messages.js'; @@ -19,13 +18,15 @@ export class DriverTestRunner { * @param {AriaATCIAgent.Log} options.log * @param {BrowserDriver} options.browserDriver * @param {ATDriver} options.atDriver + * @param {AriaATCIShared.timesOption} options.timesOption */ - constructor({ baseUrl, log, browserDriver, atDriver }) { + constructor({ baseUrl, log, browserDriver, atDriver, timesOption }) { this.baseUrl = baseUrl; this.log = log; this.browserDriver = browserDriver; this.atDriver = atDriver; this.collectedCapabilities = this.getCapabilities(); + this.timesOption = timesOption; } async getCapabilities() { @@ -46,7 +47,10 @@ export class DriverTestRunner { await this.browserDriver.documentReady(); try { - await this.browserDriver.clickWhenPresent('.button-run-test-setup', timesOption.testSetup); + await this.browserDriver.clickWhenPresent( + '.button-run-test-setup', + this.timesOption.testSetup + ); } catch ({}) { await this.log(AgentMessage.NO_RUN_TEST_SETUP, { referencePage }); } @@ -68,7 +72,7 @@ export class DriverTestRunner { let unknownCollected = ''; // there are 2 modes, so we will try pressing mode switch up to twice for (let triesRemain = 2; triesRemain > 0; triesRemain--) { - const speechResponse = await this._collectSpeech(timesOption.modeSwitch, () => + const speechResponse = await this._collectSpeech(this.timesOption.modeSwitch, () => this.sendKeys(sequence) ); while (speechResponse.length) { @@ -191,7 +195,7 @@ export class DriverTestRunner { const { value: validCommand, errors } = validateKeysFromCommand(command); if (validCommand) { - await this._collectSpeech(timesOption.afterNav, () => + await this._collectSpeech(this.timesOption.afterNav, () => this.openPage({ url: this._appendBaseUrl(test.target.referencePage), referencePage: test.target.referencePage, @@ -206,11 +210,11 @@ export class DriverTestRunner { await this.ensureMode(test.target.mode); } - const spokenOutput = await this._collectSpeech(timesOption.afterKeys, () => + const spokenOutput = await this._collectSpeech(this.timesOption.afterKeys, () => this.sendKeys(atKeysFromCommand(validCommand)) ); - await this._collectSpeech(timesOption.afterNav, async () => { + await this._collectSpeech(this.timesOption.afterNav, async () => { await this.log(AgentMessage.OPEN_PAGE, { url: 'about:blank' }); await this.browserDriver.navigate('about:blank'); }); diff --git a/src/agent/types.js b/src/agent/types.js index db10769..d9c36ef 100644 --- a/src/agent/types.js +++ b/src/agent/types.js @@ -57,6 +57,7 @@ * @property {AriaATCIShared.BaseURL} [webDriverUrl] * @property {AriaATCIAgent.Browser} [webDriverBrowser] * @property {AriaATCIShared.BaseURL} [atDriverUrl] + * @property {AriaATCIShared.timesOption} [timesOption] */ /** diff --git a/src/host/agent.js b/src/host/agent.js index 603cbb0..82ec12d 100644 --- a/src/host/agent.js +++ b/src/host/agent.js @@ -319,6 +319,7 @@ class AgentDeveloperProtocol extends AgentProtocol { atDriverUrl: options.atDriverUrl, webDriverBrowser: options.webDriverBrowser, webDriverUrl: options.webDriverUrl, + timesOption: options.timesOption, }), log, tests: iterateEmitter(this._testEmitter, 'message', 'stop'), diff --git a/src/host/cli-run-plan.js b/src/host/cli-run-plan.js index ad043d8..9e26a1a 100644 --- a/src/host/cli-run-plan.js +++ b/src/host/cli-run-plan.js @@ -15,7 +15,7 @@ import { hostMain } from './main.js'; import { HostMessage, createHostLogger } from './messages.js'; import { plansFrom } from './plan-from.js'; import { HostServer } from './server.js'; -import { timesOptionsConfig } from '../shared/times-option.js'; +import { getTimesOption, timesOptionsConfig } from '../shared/times-option.js'; export const command = 'run-plan [plan-files..]'; @@ -274,6 +274,8 @@ function mainAgentMiddleware(argv) { agentMockOpenPage, } = argv; + const timesOption = getTimesOption(argv); + argv.agent = new Agent({ log, protocol, @@ -286,6 +288,7 @@ function mainAgentMiddleware(argv) { atDriverUrl: agentAtDriverUrl, mock: agentMock, mockOpenPage: agentMockOpenPage, + timesOption: timesOption, }), }); } diff --git a/src/shared/times-option.js b/src/shared/times-option.js index 620ebce..9f18827 100644 --- a/src/shared/times-option.js +++ b/src/shared/times-option.js @@ -7,7 +7,7 @@ /** * @type AriaATCIShared.timesOption */ -export const timesOption = { +const timesDefaults = { afterNav: 1000, afterKeys: 5000, testSetup: 1000, @@ -15,11 +15,6 @@ export const timesOption = { docReady: 2000, }; -/** - * @type AriaATCIShared.timesOption - */ -const timesDefaults = { ...timesOption }; - /** * Create a yargs description for the specified timesOption. * @param {keyof AriaATCIShared.timesOption} optionName Key from timesOption @@ -30,7 +25,7 @@ function addOptionConfig(optionName, argName, describe) { timesOptionsArgNameMap.set(optionName, argName); timesOptionsConfig[argName] = { hidden: true, - default: timesOption[optionName], + default: timesDefaults[optionName], describe, coerce(arg) { const isNumber = typeof arg === 'number'; @@ -41,14 +36,13 @@ function addOptionConfig(optionName, argName, describe) { if (time <= 0) { throw new Error('time must be positive and non-zero'); } - timesOption[optionName] = time; return time; }, }; } /** - * @type Map + * @type Map */ const timesOptionsArgNameMap = new Map(); @@ -76,28 +70,40 @@ addOptionConfig( 'time-mode-switch', 'Timeout used after switching modes to check resulting speech (NVDA).' ); -addOptionConfig( - 'docReady', - 'time-mode-switch', - 'Timeout used waiting for document ready (Safari).' -); +addOptionConfig('docReady', 'time-doc-ready', 'Timeout used waiting for document ready (Safari).'); /** * Convert the times dictionary to an array of strings to pass back to args. * @param {AriaATCIShared.timesOption} opts * @returns [string] */ -export function timesArgs(opts = timesOption) { +export function timesArgs(opts) { const args = []; - for (const key of Object.keys(timesOption)) { - const value = timesOption[key]; + for (const key of Object.keys(opts)) { + const value = opts[key]; // no need to pass on "default" value - if (value === timesDefaults[key]) continue; + if (value == timesDefaults[key]) continue; // casting in jsdoc syntax is complicated - the extra () around key are // required to make the type annotation work. - const argName = timesOptionsArgNameMap.get(key); + const argName = timesOptionsArgNameMap.get(/** @type keyof AriaATCIShared.timesOption */ (key)); args.push('--' + argName); args.push(String(value)); } return args; } + +/** + * Convert the arguments parse result into a timesOption object. + * @param {any} args The parsed arguments + * @returns {AriaATCIShared.timesOption} + */ +export function getTimesOption(args) { + const result = { ...timesDefaults }; + for (const key in result) { + const mapped = timesOptionsArgNameMap.get(/** @type keyof AriaATCIShared.timesOption */ (key)); + if (mapped) { + if (args[mapped]) result[key] = args[mapped]; + } + } + return result; +} From 639688dda0fbef5e01320aaf1761c35dc9c91541 Mon Sep 17 00:00:00 2001 From: "Mx. Corey Frang" Date: Tue, 23 Jul 2024 13:22:22 -0400 Subject: [PATCH 8/8] fix jsdoc --- src/shared/times-option.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/times-option.js b/src/shared/times-option.js index 9f18827..e3c52fc 100644 --- a/src/shared/times-option.js +++ b/src/shared/times-option.js @@ -75,7 +75,7 @@ addOptionConfig('docReady', 'time-doc-ready', 'Timeout used waiting for document /** * Convert the times dictionary to an array of strings to pass back to args. * @param {AriaATCIShared.timesOption} opts - * @returns [string] + * @returns {string[]} */ export function timesArgs(opts) { const args = [];