From 1bdc8bb1c327bf7630bc1e9f5b3c541bea45c7d2 Mon Sep 17 00:00:00 2001 From: artem-babich Date: Tue, 15 Nov 2022 09:58:33 +0400 Subject: [PATCH 01/15] init custom actions --- custom-config.js | 18 ++++++++ src/api/test-controller/custom-actions.ts | 51 +++++++++++++++++++++ src/api/test-controller/index.js | 40 +++++++++++++++- src/api/wrap-custom-action.ts | 56 +++++++++++++++++++++++ src/configuration/option-names.ts | 1 + src/configuration/types.ts | 4 +- src/test-run/commands/actions.d.ts | 6 +++ src/test-run/commands/actions.js | 15 ++++++ src/test-run/commands/type.js | 1 + src/test-run/index.ts | 9 ++++ 10 files changed, 198 insertions(+), 3 deletions(-) create mode 100644 custom-config.js create mode 100644 src/api/test-controller/custom-actions.ts create mode 100644 src/api/wrap-custom-action.ts diff --git a/custom-config.js b/custom-config.js new file mode 100644 index 00000000000..a89c83d6d5b --- /dev/null +++ b/custom-config.js @@ -0,0 +1,18 @@ +module.exports = { + customActions: { + async makeCoffee (selector) { + await this + .click(selector) + .custom.makeTea('https://example.org', 'a'); + }, + async makeTea (url, selector) { + await this + .navigateTo(url) + .click(selector); + }, + async getSelectorValue () { + return 'a'; + }, + }, +}; + diff --git a/src/api/test-controller/custom-actions.ts b/src/api/test-controller/custom-actions.ts new file mode 100644 index 00000000000..a46ee07309c --- /dev/null +++ b/src/api/test-controller/custom-actions.ts @@ -0,0 +1,51 @@ +import TestRun from '../../test-run'; +import { getCallsiteForMethod } from '../../errors/get-callsite'; +import { RunCustomActionCommand } from '../../test-run/commands/actions'; +import { delegateAPI } from '../../utils/delegated-api'; + +function delegatedAPI (methodName: string, accessor = ''): string { + return `_${methodName}$${accessor}`; +} + +export default class CustomActions { + private readonly _testRun: TestRun; + + constructor (testRun: TestRun) { + this._testRun = testRun; + this._registerCustomActions(); + } + + _registerCustomActions (): void { + // @ts-ignore + const customActions = this._testRun?.opts?.customActions || {}; + + Object.entries(customActions).forEach(([ name, fn ]) => { + // @ts-ignore + CustomActions.prototype[delegatedAPI(name)] = (...args) => { + const callsite = getCallsiteForMethod(name); + + // @ts-ignore + return this._testRun.controller._enqueueCommand(RunCustomActionCommand, { fn, args }, null, callsite); + }; + }); + + this._extendTestControllerAPIList(customActions); + } + + // @ts-ignore + _extendTestControllerAPIList (actions): void { + const customActionsList = Object.entries(actions).map(([name]) => { + return { + srcProp: delegatedAPI(name), + apiProp: name, + accessor: '', + }; + }); + + // @ts-ignore + CustomActions.API_LIST = customActionsList; + + // @ts-ignore + delegateAPI(CustomActions.prototype, CustomActions.API_LIST, { useCurrentCtxAsHandler: true }); + } +} diff --git a/src/api/test-controller/index.js b/src/api/test-controller/index.js index 6ed644d95de..f7d8888ceb4 100644 --- a/src/api/test-controller/index.js +++ b/src/api/test-controller/index.js @@ -57,6 +57,7 @@ import { SkipJsErrorsCommand, AddRequestHooksCommand, RemoveRequestHooksCommand, + RunCustomActionCommand, } from '../../test-run/commands/actions'; import { @@ -83,6 +84,7 @@ import ReExecutablePromise from '../../utils/re-executable-promise'; import sendRequest from '../../test-run/request/send'; import { RequestRuntimeError } from '../../errors/runtime'; import { RUNTIME_ERRORS } from '../../errors/types'; +import CustomActions from './custom-actions'; const originalThen = Promise.resolve().then; @@ -99,10 +101,39 @@ export default class TestController { this.testRun = testRun; this.executionChain = Promise.resolve(); this.warningLog = testRun.warningLog; + this.customActions = new CustomActions(testRun); this._addTestControllerToExecutionChain(); } + _registerCustomActions () { + const customActions = this.testRun?.opts?.customActions || {}; + + Object.entries(customActions).forEach(([ name, fn ]) => { + TestController.prototype[delegatedAPI(name)] = (...args) => { + const callsite = getCallsiteForMethod(name); + + return this._enqueueCommand(RunCustomActionCommand, { fn, args }, null, callsite); + }; + }); + + this._extendTestControllerAPIList(customActions); + } + + _extendTestControllerAPIList (actions) { + const customActionsList = Object.entries(actions).map(([name]) => { + return { + srcProp: delegatedAPI(name), + apiProp: name, + accessor: '', + }; + }); + + TestController.API_LIST = [...TestController.API_LIST, ...customActionsList ]; + + delegateAPI(TestController.prototype, TestController.API_LIST, { useCurrentCtxAsHandler: true }); + } + _addTestControllerToExecutionChain () { this.executionChain._testController = this; } @@ -164,8 +195,9 @@ export default class TestController { return this.executionChain; } - _enqueueCommand (CmdCtor, cmdArgs, validateCommandFn) { - const callsite = getCallsiteForMethod(CmdCtor.methodName); + _enqueueCommand (CmdCtor, cmdArgs, validateCommandFn, callsite) { + callsite = callsite || getCallsiteForMethod(CmdCtor.methodName); + const command = this._createCommand(CmdCtor, cmdArgs, callsite); if (typeof validateCommandFn === 'function') @@ -222,6 +254,10 @@ export default class TestController { return this.testRun.browser; } + _custom$getter () { + return this.customActions || new CustomActions(this.testRun); + } + [delegatedAPI(DispatchEventCommand.methodName)] (selector, eventName, options = {}) { return this._enqueueCommand(DispatchEventCommand, { selector, eventName, options, relatedTarget: options.relatedTarget }); } diff --git a/src/api/wrap-custom-action.ts b/src/api/wrap-custom-action.ts new file mode 100644 index 00000000000..e27cc4180d5 --- /dev/null +++ b/src/api/wrap-custom-action.ts @@ -0,0 +1,56 @@ +import TestController from './test-controller'; +import testRunTracker from './test-run-tracker'; +import TestRun from '../test-run'; +import TestCafeErrorList from '../errors/error-list'; +import { MissingAwaitError } from '../errors/test-run'; +import addRenderedWarning from '../notifications/add-rendered-warning'; +import WARNING_MESSAGES from '../notifications/warning-message'; + +export default function wrapCustomAction (fn: Function): Function { + return async (testRun: TestRun, functionArgs: any) => { + let result = null; + const errList = new TestCafeErrorList(); + const markeredfn = (controller: TestController, args: any):Function => fn.call(controller, ...args); + + function addWarnings (callsiteSet: Set>, message: string): void { + callsiteSet.forEach(callsite => { + addRenderedWarning(testRun.warningLog, message, callsite); + callsiteSet.delete(callsite); + }); + } + + function addErrors (callsiteSet: Set>, ErrorClass: any): void { + callsiteSet.forEach(callsite => { + errList.addError(new ErrorClass(callsite)); + callsiteSet.delete(callsite); + }); + } + + testRun.controller = new TestController(testRun); + + testRun.observedCallsites.clear(); + + testRunTracker.ensureEnabled(); + + try { + result = await markeredfn(testRun.controller, functionArgs); + } + catch (err) { + errList.addError(err); + } + + if (!errList.hasUncaughtErrorsInTestCode) { + for (const { callsite, actionId } of testRun.observedCallsites.awaitedSnapshotWarnings.values()) + addRenderedWarning(testRun.warningLog, { message: WARNING_MESSAGES.excessiveAwaitInAssertion, actionId }, callsite); + + + addWarnings(testRun.observedCallsites.unawaitedSnapshotCallsites, WARNING_MESSAGES.missingAwaitOnSnapshotProperty); + addErrors(testRun.observedCallsites.callsitesWithoutAwait, MissingAwaitError); + } + + if (errList.hasErrors) + throw errList; + + return result; + }; +} diff --git a/src/configuration/option-names.ts b/src/configuration/option-names.ts index 4ff06d79590..e5e6f7ca9d2 100644 --- a/src/configuration/option-names.ts +++ b/src/configuration/option-names.ts @@ -56,6 +56,7 @@ enum OptionNames { dashboard = 'dashboard', baseUrl = 'baseUrl', disableCrossDomain = 'disableCrossDomain', + customActions = 'customActions', } export default OptionNames; diff --git a/src/configuration/types.ts b/src/configuration/types.ts index 9f230ba91e7..b3713b046d8 100644 --- a/src/configuration/types.ts +++ b/src/configuration/types.ts @@ -31,5 +31,7 @@ interface GlobalHooks { request?: typeof RequestHook[] | typeof RequestHook; } +type CustomActions = { [name: string]: () => void } + // eslint-disable-next-line @typescript-eslint/no-unused-vars -type OptionValue = undefined | null | string | boolean | number | string[] | Function | { [key: string]: any } | ScreenshotOptionValue | QuarantineOptionValue | CompilerOptions | GlobalHooks | SkipJsErrorsOptionValue; +type OptionValue = undefined | null | string | boolean | number | string[] | Function | { [key: string]: any } | ScreenshotOptionValue | QuarantineOptionValue | CompilerOptions | GlobalHooks | SkipJsErrorsOptionValue | CustomActions; diff --git a/src/test-run/commands/actions.d.ts b/src/test-run/commands/actions.d.ts index 814ae58daf1..5e00ab8be31 100644 --- a/src/test-run/commands/actions.d.ts +++ b/src/test-run/commands/actions.d.ts @@ -278,3 +278,9 @@ export class RemoveRequestHooksCommand extends ActionCommandBase { public hooks: RequestHook[]; } +export class RunCustomActionCommand extends ActionCommandBase { + public constructor (obj: object, testRun: TestRun, validateProperties: boolean); + public fn: Function; + public args: any; +} + diff --git a/src/test-run/commands/actions.js b/src/test-run/commands/actions.js index eb07f31011e..905ba29fe2c 100644 --- a/src/test-run/commands/actions.js +++ b/src/test-run/commands/actions.js @@ -769,6 +769,21 @@ export class SkipJsErrorsCommand extends ActionCommandBase { } } +export class RunCustomActionCommand extends ActionCommandBase { + static methodName = camelCase(TYPE.runCustomAction); + + constructor (obj, testRun, validateProperties) { + super(obj, testRun, TYPE.runCustomAction, validateProperties); + } + + getAssignableProperties () { + return [ + { name: 'fn', type: functionArgument, required: true }, + { name: 'args', required: false }, + ]; + } +} + export class AddRequestHooksCommand extends ActionCommandBase { static methodName = camelCase(TYPE.addRequestHooks); diff --git a/src/test-run/commands/type.js b/src/test-run/commands/type.js index 9d58636159b..8a63204aef2 100644 --- a/src/test-run/commands/type.js +++ b/src/test-run/commands/type.js @@ -71,4 +71,5 @@ export default { skipJsErrors: 'skip-js-errors', addRequestHooks: 'add-request-hooks', removeRequestHooks: 'remove-request-hooks', + runCustomAction: 'run-custom-action', }; diff --git a/src/test-run/index.ts b/src/test-run/index.ts index 1bbb88d5752..82df4e1a206 100644 --- a/src/test-run/index.ts +++ b/src/test-run/index.ts @@ -129,6 +129,7 @@ import { CookieOptions } from './commands/options'; import { prepareSkipJsErrorsOptions } from '../api/skip-js-errors'; import { CookieProviderFactory } from './cookies/factory'; import { CookieProvider } from './cookies/base'; +import wrapCustomAction from '../api/wrap-custom-action'; import { ProxylessRoleProvider, @@ -1277,6 +1278,14 @@ export default class TestRun extends AsyncEventEmitter { return await fn(); } + if (command.type === COMMAND_TYPE.runCustomAction) { + debugger; + const { fn, args } = command as any; + const wrappedFn = wrapCustomAction(fn); + + return await wrappedFn(this, args); + } + if (command.type === COMMAND_TYPE.assertion) return this._executeAssertion(command as AssertionCommand, callsite as CallsiteRecord); From bc0b1d615aeec32d803679af7822b64769b7ba29 Mon Sep 17 00:00:00 2001 From: artem-babich Date: Thu, 17 Nov 2022 10:48:54 +0400 Subject: [PATCH 02/15] refactor 1 --- src/api/test-controller/custom-actions.ts | 44 +++++++++++------------ src/api/test-controller/delegated-api.ts | 3 ++ src/api/test-controller/index.d.ts | 2 +- src/api/test-controller/index.js | 38 ++------------------ 4 files changed, 27 insertions(+), 60 deletions(-) create mode 100644 src/api/test-controller/delegated-api.ts diff --git a/src/api/test-controller/custom-actions.ts b/src/api/test-controller/custom-actions.ts index a46ee07309c..27d366b8726 100644 --- a/src/api/test-controller/custom-actions.ts +++ b/src/api/test-controller/custom-actions.ts @@ -1,39 +1,39 @@ -import TestRun from '../../test-run'; import { getCallsiteForMethod } from '../../errors/get-callsite'; import { RunCustomActionCommand } from '../../test-run/commands/actions'; import { delegateAPI } from '../../utils/delegated-api'; - -function delegatedAPI (methodName: string, accessor = ''): string { - return `_${methodName}$${accessor}`; -} +import { Dictionary } from '../../configuration/interfaces'; +import TestController from './index'; +import delegatedAPI from './delegated-api'; export default class CustomActions { - private readonly _testRun: TestRun; + private _testController: TestController; + private readonly _customActions: Dictionary; + + constructor (testController: TestController, customActions: Dictionary) { + this._testController = testController; + this._customActions = customActions || {}; - constructor (testRun: TestRun) { - this._testRun = testRun; this._registerCustomActions(); } _registerCustomActions (): void { - // @ts-ignore - const customActions = this._testRun?.opts?.customActions || {}; - - Object.entries(customActions).forEach(([ name, fn ]) => { + Object.entries(this._customActions).forEach(([ name, fn ]) => { // @ts-ignore - CustomActions.prototype[delegatedAPI(name)] = (...args) => { - const callsite = getCallsiteForMethod(name); + this[delegatedAPI(name)] = (...args) => { + const callsite = getCallsiteForMethod(name) || void 0; - // @ts-ignore - return this._testRun.controller._enqueueCommand(RunCustomActionCommand, { fn, args }, null, callsite); + return this._testController._enqueueCommand(RunCustomActionCommand, { fn, args }, this._validateCommand, callsite); }; }); - this._extendTestControllerAPIList(customActions); + this._delegateAPI(this._customActions); } - // @ts-ignore - _extendTestControllerAPIList (actions): void { + _validateCommand (): boolean { + return true; + } + + _delegateAPI (actions: Dictionary): void { const customActionsList = Object.entries(actions).map(([name]) => { return { srcProp: delegatedAPI(name), @@ -42,10 +42,6 @@ export default class CustomActions { }; }); - // @ts-ignore - CustomActions.API_LIST = customActionsList; - - // @ts-ignore - delegateAPI(CustomActions.prototype, CustomActions.API_LIST, { useCurrentCtxAsHandler: true }); + delegateAPI(this, customActionsList, { useCurrentCtxAsHandler: true }); } } diff --git a/src/api/test-controller/delegated-api.ts b/src/api/test-controller/delegated-api.ts new file mode 100644 index 00000000000..d537891d26e --- /dev/null +++ b/src/api/test-controller/delegated-api.ts @@ -0,0 +1,3 @@ +export default function delegatedAPI (methodName: string, accessor = ''): string { + return `_${ methodName }$${ accessor }`; +} diff --git a/src/api/test-controller/index.d.ts b/src/api/test-controller/index.d.ts index fb3067f8118..f50e1891948 100644 --- a/src/api/test-controller/index.d.ts +++ b/src/api/test-controller/index.d.ts @@ -9,7 +9,7 @@ export default class TestController { public constructor (testRun: TestRun | TestRunProxy); public testRun: TestRun; public warningLog: WarningLog; - public _enqueueCommand (CmdCtor: unknown, cmdArgs: object, validateCommand: Function): () => Promise; + public _enqueueCommand (CmdCtor: unknown, cmdArgs: object, validateCommand: Function, callsite?: CallsiteRecord): () => Promise; public checkForExcessiveAwaits (checkedCallsite: CallsiteRecord, { actionId }: CommandBase): void; public static enableDebugForNonDebugCommands (): void; public static disableDebugForNonDebugCommands (): void; diff --git a/src/api/test-controller/index.js b/src/api/test-controller/index.js index f7d8888ceb4..da19f6985fd 100644 --- a/src/api/test-controller/index.js +++ b/src/api/test-controller/index.js @@ -57,7 +57,6 @@ import { SkipJsErrorsCommand, AddRequestHooksCommand, RemoveRequestHooksCommand, - RunCustomActionCommand, } from '../../test-run/commands/actions'; import { @@ -85,15 +84,12 @@ import sendRequest from '../../test-run/request/send'; import { RequestRuntimeError } from '../../errors/runtime'; import { RUNTIME_ERRORS } from '../../errors/types'; import CustomActions from './custom-actions'; +import delegatedAPI from './delegated-api'; const originalThen = Promise.resolve().then; let inDebug = false; -function delegatedAPI (methodName, accessor = '') { - return `_${methodName}$${accessor}`; -} - export default class TestController { constructor (testRun) { this._executionContext = null; @@ -101,39 +97,11 @@ export default class TestController { this.testRun = testRun; this.executionChain = Promise.resolve(); this.warningLog = testRun.warningLog; - this.customActions = new CustomActions(testRun); + this.customActions = new CustomActions(this, testRun?.opts?.customActions); this._addTestControllerToExecutionChain(); } - _registerCustomActions () { - const customActions = this.testRun?.opts?.customActions || {}; - - Object.entries(customActions).forEach(([ name, fn ]) => { - TestController.prototype[delegatedAPI(name)] = (...args) => { - const callsite = getCallsiteForMethod(name); - - return this._enqueueCommand(RunCustomActionCommand, { fn, args }, null, callsite); - }; - }); - - this._extendTestControllerAPIList(customActions); - } - - _extendTestControllerAPIList (actions) { - const customActionsList = Object.entries(actions).map(([name]) => { - return { - srcProp: delegatedAPI(name), - apiProp: name, - accessor: '', - }; - }); - - TestController.API_LIST = [...TestController.API_LIST, ...customActionsList ]; - - delegateAPI(TestController.prototype, TestController.API_LIST, { useCurrentCtxAsHandler: true }); - } - _addTestControllerToExecutionChain () { this.executionChain._testController = this; } @@ -255,7 +223,7 @@ export default class TestController { } _custom$getter () { - return this.customActions || new CustomActions(this.testRun); + return this.customActions || new CustomActions(this, this.testRun.opts.customActions); } [delegatedAPI(DispatchEventCommand.methodName)] (selector, eventName, options = {}) { From 219b51f3ec1c3689c81794db3c360f9604675d94 Mon Sep 17 00:00:00 2001 From: artem-babich Date: Wed, 23 Nov 2022 09:20:36 +0400 Subject: [PATCH 03/15] refactor 2 --- src/api/test-controller/add-errors.ts | 17 ++++++ src/api/test-run-tracker.ts | 19 +++--- src/api/wrap-custom-action.ts | 26 ++------ src/api/wrap-test-function.ts | 19 +----- src/errors/runtime/templates.js | 2 + src/errors/types.js | 2 + src/runner/index.js | 16 +++++ src/test-run/index.ts | 4 +- .../fixtures/custom-actions/actions.js | 25 ++++++++ .../fixtures/custom-actions/pages/index.html | 22 +++++++ .../fixtures/custom-actions/test.js | 61 +++++++++++++++++++ .../custom-actions/testcafe-fixtures/index.js | 29 +++++++++ test/functional/setup.js | 2 + 13 files changed, 197 insertions(+), 47 deletions(-) create mode 100644 src/api/test-controller/add-errors.ts create mode 100644 test/functional/fixtures/custom-actions/actions.js create mode 100644 test/functional/fixtures/custom-actions/pages/index.html create mode 100644 test/functional/fixtures/custom-actions/test.js create mode 100644 test/functional/fixtures/custom-actions/testcafe-fixtures/index.js diff --git a/src/api/test-controller/add-errors.ts b/src/api/test-controller/add-errors.ts new file mode 100644 index 00000000000..c66b127560b --- /dev/null +++ b/src/api/test-controller/add-errors.ts @@ -0,0 +1,17 @@ +import addRenderedWarning from '../../notifications/add-rendered-warning'; +import TestRun from '../../test-run'; +import TestCafeErrorList from '../../errors/error-list'; + +export function addWarnings (callsiteSet: Set>, message: string, testRun: TestRun): void { + callsiteSet.forEach(callsite => { + addRenderedWarning(testRun.warningLog, message, callsite); + callsiteSet.delete(callsite); + }); +} + +export function addErrors (callsiteSet: Set>, ErrorClass: any, errList: TestCafeErrorList): void { + callsiteSet.forEach(callsite => { + errList.addError(new ErrorClass(callsite)); + callsiteSet.delete(callsite); + }); +} diff --git a/src/api/test-run-tracker.ts b/src/api/test-run-tracker.ts index 10219f8a615..b28143256ec 100644 --- a/src/api/test-run-tracker.ts +++ b/src/api/test-run-tracker.ts @@ -66,21 +66,22 @@ class TestRunTracker extends EventEmitter { } } - public addTrackingMarkerToFunction (testRunId: string, fn: Function): Function { + public addTrackingMarkerToFunction (testRunId: string, fn: Function, context?: any): Function { const markerFactoryBody = ` - return function ${this.getMarkedFnName(testRunId)} () { + return function ${ this.getMarkedFnName(testRunId) } () { + context = context || this; switch (arguments.length) { - case 0: return fn.call(this); - case 1: return fn.call(this, arguments[0]); - case 2: return fn.call(this, arguments[0], arguments[1]); - case 3: return fn.call(this, arguments[0], arguments[1], arguments[2]); - case 4: return fn.call(this, arguments[0], arguments[1], arguments[2], arguments[3]); - default: return fn.apply(this, arguments); + case 0: return fn.call(context); + case 1: return fn.call(context, arguments[0]); + case 2: return fn.call(context, arguments[0], arguments[1]); + case 3: return fn.call(context, arguments[0], arguments[1], arguments[2]); + case 4: return fn.call(context, arguments[0], arguments[1], arguments[2], arguments[3]); + default: return fn.apply(context, arguments); } }; `; - return new Function('fn', markerFactoryBody)(fn); + return new Function('fn', 'context', markerFactoryBody)(fn, context); } public getContextTestRunId (): string | null { diff --git a/src/api/wrap-custom-action.ts b/src/api/wrap-custom-action.ts index e27cc4180d5..78f78610fc5 100644 --- a/src/api/wrap-custom-action.ts +++ b/src/api/wrap-custom-action.ts @@ -5,35 +5,22 @@ import TestCafeErrorList from '../errors/error-list'; import { MissingAwaitError } from '../errors/test-run'; import addRenderedWarning from '../notifications/add-rendered-warning'; import WARNING_MESSAGES from '../notifications/warning-message'; +import { addErrors, addWarnings } from './test-controller/add-errors'; export default function wrapCustomAction (fn: Function): Function { return async (testRun: TestRun, functionArgs: any) => { let result = null; const errList = new TestCafeErrorList(); - const markeredfn = (controller: TestController, args: any):Function => fn.call(controller, ...args); - - function addWarnings (callsiteSet: Set>, message: string): void { - callsiteSet.forEach(callsite => { - addRenderedWarning(testRun.warningLog, message, callsite); - callsiteSet.delete(callsite); - }); - } - - function addErrors (callsiteSet: Set>, ErrorClass: any): void { - callsiteSet.forEach(callsite => { - errList.addError(new ErrorClass(callsite)); - callsiteSet.delete(callsite); - }); - } testRun.controller = new TestController(testRun); - testRun.observedCallsites.clear(); + const markeredfn = testRunTracker.addTrackingMarkerToFunction(testRun.id, fn, testRun.controller); + testRun.observedCallsites.clear(); testRunTracker.ensureEnabled(); try { - result = await markeredfn(testRun.controller, functionArgs); + result = await markeredfn(...functionArgs); } catch (err) { errList.addError(err); @@ -43,9 +30,8 @@ export default function wrapCustomAction (fn: Function): Function { for (const { callsite, actionId } of testRun.observedCallsites.awaitedSnapshotWarnings.values()) addRenderedWarning(testRun.warningLog, { message: WARNING_MESSAGES.excessiveAwaitInAssertion, actionId }, callsite); - - addWarnings(testRun.observedCallsites.unawaitedSnapshotCallsites, WARNING_MESSAGES.missingAwaitOnSnapshotProperty); - addErrors(testRun.observedCallsites.callsitesWithoutAwait, MissingAwaitError); + addWarnings(testRun.observedCallsites.unawaitedSnapshotCallsites, WARNING_MESSAGES.missingAwaitOnSnapshotProperty, testRun); + addErrors(testRun.observedCallsites.callsitesWithoutAwait, MissingAwaitError, errList); } if (errList.hasErrors) diff --git a/src/api/wrap-test-function.ts b/src/api/wrap-test-function.ts index d234dab0d85..3c955b460e6 100644 --- a/src/api/wrap-test-function.ts +++ b/src/api/wrap-test-function.ts @@ -5,6 +5,7 @@ import TestCafeErrorList from '../errors/error-list'; import { MissingAwaitError } from '../errors/test-run'; import addRenderedWarning from '../notifications/add-rendered-warning'; import WARNING_MESSAGES from '../notifications/warning-message'; +import { addErrors, addWarnings } from './test-controller/add-errors'; export default function wrapTestFunction (fn: Function): Function { return async (testRun: TestRun) => { @@ -12,20 +13,6 @@ export default function wrapTestFunction (fn: Function): Function { const errList = new TestCafeErrorList(); const markeredfn = testRunTracker.addTrackingMarkerToFunction(testRun.id, fn); - function addWarnings (callsiteSet: Set>, message: string): void { - callsiteSet.forEach(callsite => { - addRenderedWarning(testRun.warningLog, message, callsite); - callsiteSet.delete(callsite); - }); - } - - function addErrors (callsiteSet: Set>, ErrorClass: any): void { - callsiteSet.forEach(callsite => { - errList.addError(new ErrorClass(callsite)); - callsiteSet.delete(callsite); - }); - } - testRun.controller = new TestController(testRun); testRun.observedCallsites.clear(); @@ -43,8 +30,8 @@ export default function wrapTestFunction (fn: Function): Function { for (const { callsite, actionId } of testRun.observedCallsites.awaitedSnapshotWarnings.values()) addRenderedWarning(testRun.warningLog, { message: WARNING_MESSAGES.excessiveAwaitInAssertion, actionId }, callsite); - addWarnings(testRun.observedCallsites.unawaitedSnapshotCallsites, WARNING_MESSAGES.missingAwaitOnSnapshotProperty); - addErrors(testRun.observedCallsites.callsitesWithoutAwait, MissingAwaitError); + addWarnings(testRun.observedCallsites.unawaitedSnapshotCallsites, WARNING_MESSAGES.missingAwaitOnSnapshotProperty, testRun); + addErrors(testRun.observedCallsites.callsitesWithoutAwait, MissingAwaitError, errList); } if (errList.hasErrors) diff --git a/src/errors/runtime/templates.js b/src/errors/runtime/templates.js index 4a1cdc8a58c..fb662043511 100644 --- a/src/errors/runtime/templates.js +++ b/src/errors/runtime/templates.js @@ -139,4 +139,6 @@ export default { [RUNTIME_ERRORS.invalidSkipJsErrorsOptionsObjectProperty]: `The "{optionName}" option does not exist. Use the following options to configure skipJsErrors: ${getConcatenatedValuesString(Object.keys(SKIP_JS_ERRORS_OPTIONS_OBJECT_OPTION_NAMES))}.`, [RUNTIME_ERRORS.invalidSkipJsErrorsCallbackWithOptionsProperty]: `The "{optionName}" option does not exist. Use the following options to configure skipJsErrors callback: ${getConcatenatedValuesString(Object.keys(SKIP_JS_ERRORS_CALLBACK_WITH_OPTIONS_OPTION_NAMES))}.`, [RUNTIME_ERRORS.invalidCommandInJsonCompiler]: `TestCafe terminated the test run. The "{path}" file contains an unknown Chrome User Flow action "{action}". Remove the action to continue. Refer to the following article for the definitive list of supported Chrome User Flow actions: https://testcafe.io/documentation/403998/guides/experimental-capabilities/chrome-replay-support#supported-replay-actions`, + [RUNTIME_ERRORS.invalidCustomActionsOptionType]: `The value of the customActions option does not belong to type Object. Refer to the following article for custom action setup instructions: CUSTOM_ACTIONS_LINK`, + [RUNTIME_ERRORS.invalidCustomActionType]: `The {actionName} custom action does not contain an asynchronous function. Actual data type: {type}. Refer to the following article for custom action setup instructions: CUSTOM_ACTIONS_LINK`, }; diff --git a/src/errors/types.js b/src/errors/types.js index 56b68526e61..915ad37e0c6 100644 --- a/src/errors/types.js +++ b/src/errors/types.js @@ -182,4 +182,6 @@ export const RUNTIME_ERRORS = { invalidSkipJsErrorsOptionsObjectProperty: 'E1076', invalidSkipJsErrorsCallbackWithOptionsProperty: 'E1077', invalidCommandInJsonCompiler: 'E1078', + invalidCustomActionsOptionType: 'E1079', + invalidCustomActionType: 'E1080', }; diff --git a/src/runner/index.js b/src/runner/index.js index 3493eb31d76..c4c5655d65a 100644 --- a/src/runner/index.js +++ b/src/runner/index.js @@ -317,6 +317,21 @@ export default class Runner extends EventEmitter { validateSkipJsErrorsOptionValue(skipJsErrorsOptions, GeneralError); } + _validateCustomActionsOption () { + const customActions = this.configuration.getOption(OPTION_NAMES.customActions); + + if (!customActions) + return; + + if (typeof customActions !== 'object') + throw new GeneralError(RUNTIME_ERRORS.invalidCustomActionsOptionType); + + for (const name in customActions) { + if (typeof customActions[name] !== 'function') + throw new GeneralError(RUNTIME_ERRORS.invalidCustomActionType, name, typeof customActions[name]); + } + } + async _validateBrowsers () { const browsers = this.configuration.getOption(OPTION_NAMES.browsers); @@ -485,6 +500,7 @@ export default class Runner extends EventEmitter { this._validateQuarantineOptions(); this._validateConcurrencyOption(); this._validateSkipJsErrorsOption(); + this._validateCustomActionsOption(); await this._validateBrowsers(); } diff --git a/src/test-run/index.ts b/src/test-run/index.ts index 82df4e1a206..d27e995371d 100644 --- a/src/test-run/index.ts +++ b/src/test-run/index.ts @@ -77,6 +77,7 @@ import { DeleteCookiesCommand, AddRequestHooksCommand, RemoveRequestHooksCommand, + RunCustomActionCommand, } from './commands/actions'; import { RUNTIME_ERRORS, TEST_RUN_ERRORS } from '../errors/types'; @@ -1279,8 +1280,7 @@ export default class TestRun extends AsyncEventEmitter { } if (command.type === COMMAND_TYPE.runCustomAction) { - debugger; - const { fn, args } = command as any; + const { fn, args } = command as RunCustomActionCommand; const wrappedFn = wrapCustomAction(fn); return await wrappedFn(this, args); diff --git a/test/functional/fixtures/custom-actions/actions.js b/test/functional/fixtures/custom-actions/actions.js new file mode 100644 index 00000000000..1d683158e8e --- /dev/null +++ b/test/functional/fixtures/custom-actions/actions.js @@ -0,0 +1,25 @@ +const { Selector } = require('../../../../lib/api/exportable-lib'); + +async function clickBySelector (selector) { + await this.click(selector); +} + +async function getSpanTextBySelector (selector) { + return await Selector(selector).innerText; +} + +async function typeTextAndClickButton (inputSelector, buttonSelector, inputText) { + await this.typeText(inputSelector, inputText).click(buttonSelector); +} + +async function typeToInputAndCheckResult (inputSelector, buttonSelector, resultSelector, inputText) { + await this.custom.typeTextAndClickButton(inputSelector, buttonSelector, inputText) + .expect(await this.custom.getSpanTextBySelector(resultSelector)).eql(inputText); +} + +module.exports = { + getSpanTextBySelector, + clickBySelector, + typeTextAndClickButton, + typeToInputAndCheckResult, +}; diff --git a/test/functional/fixtures/custom-actions/pages/index.html b/test/functional/fixtures/custom-actions/pages/index.html new file mode 100644 index 00000000000..96b43b922f8 --- /dev/null +++ b/test/functional/fixtures/custom-actions/pages/index.html @@ -0,0 +1,22 @@ + + + + + Page error + + + + + + + + + + diff --git a/test/functional/fixtures/custom-actions/test.js b/test/functional/fixtures/custom-actions/test.js new file mode 100644 index 00000000000..d25e288c902 --- /dev/null +++ b/test/functional/fixtures/custom-actions/test.js @@ -0,0 +1,61 @@ +const { + clickBySelector, + getSpanTextBySelector, + typeTextAndClickButton, + typeToInputAndCheckResult, +} = require('./actions'); + +const { expect } = require('chai'); + +describe('[API] Custom Actions', function () { + it('Should run custom click action', function () { + return runTests('./testcafe-fixtures/index.js', 'Should run custom click action', { customActions: { clickBySelector } }); + }); + + it('Should return value from custom action', function () { + return runTests('./testcafe-fixtures/index.js', 'Should return value from custom action', { + customActions: { + clickBySelector, + getSpanTextBySelector, + }, + }); + }); + + it('Should chain multiple actions inside custom action', function () { + return runTests('./testcafe-fixtures/index.js', 'Should chain multiple actions', { + customActions: { + typeTextAndClickButton, + }, + }); + }); + + it('Should run custom action inside another custom action', function () { + return runTests('./testcafe-fixtures/index.js', 'Should run custom action inside another custom action', { + customActions: { + typeToInputAndCheckResult, + typeTextAndClickButton, + getSpanTextBySelector, + }, + }); + }); + + it('Should throw an exception inside custom action', function () { + return runTests('./testcafe-fixtures/index.js', 'Should throw an exception inside custom action', { + customActions: { clickBySelector }, + shouldFail: true, + }) + .catch(errs => { + expect(errs[0]).contains('The specified selector does not match any element in the DOM tree.'); + }); + }); + + it('Should throw an exception due to undefined action', function () { + return runTests('./testcafe-fixtures/index.js', 'Should throw an exception inside custom action', { + shouldFail: true, + }) + .catch(errs => { + expect(errs[0]).contains('TypeError: t.custom.clickBySelector is not a function'); + }); + }); +}); + diff --git a/test/functional/fixtures/custom-actions/testcafe-fixtures/index.js b/test/functional/fixtures/custom-actions/testcafe-fixtures/index.js new file mode 100644 index 00000000000..614cf21c242 --- /dev/null +++ b/test/functional/fixtures/custom-actions/testcafe-fixtures/index.js @@ -0,0 +1,29 @@ +import { Selector } from 'testcafe'; + +fixture`Custom Actions` + .page`http://localhost:3000/fixtures/custom-actions/pages/index.html`; + +test('Should run custom click action', async t => { + await t.custom.clickBySelector('#button1'); +}); + +test('Should return value from custom action', async t => { + const before = await t.custom.getSpanTextBySelector('#result1'); + const after = await t.custom.clickBySelector('#button1') + .custom.getSpanTextBySelector('#result1'); + + await t.expect(before).eql('').expect(after).eql('OK'); +}); + +test('Should chain multiple actions', async t => { + await t.custom.typeTextAndClickButton('#input1', '#button2', 'Some text') + .expect(Selector('#result2').innerText).eql('Some text'); +}); + +test('Should run custom action inside another custom action', async t => { + await t.custom.typeToInputAndCheckResult('#input1', '#button2', '#result2', 'Some text'); +}); + +test('Should throw an exception inside custom action', async t => { + await t.custom.clickBySelector('blablabla'); +}); diff --git a/test/functional/setup.js b/test/functional/setup.js index 5b037084254..2bc4ba393c7 100644 --- a/test/functional/setup.js +++ b/test/functional/setup.js @@ -239,6 +239,7 @@ before(function () { testExecutionTimeout, runExecutionTimeout, baseUrl, + customActions, } = opts; const actualBrowsers = browsersInfo.filter(browserInfo => { @@ -309,6 +310,7 @@ before(function () { testExecutionTimeout, runExecutionTimeout, baseUrl, + customActions, }) .then(failedCount => { if (customReporters) From 197f43b6db32db4f64e3149af5e5e45bfaefc925 Mon Sep 17 00:00:00 2001 From: artem-babich Date: Wed, 23 Nov 2022 18:42:27 +0400 Subject: [PATCH 04/15] add server tests --- test/server/configuration-test.js | 15 ++++++++++ test/server/data/custom-actions/config.js | 11 +++++++ test/server/runner-test.js | 35 +++++++++++++++++++++++ 3 files changed, 61 insertions(+) create mode 100644 test/server/data/custom-actions/config.js diff --git a/test/server/configuration-test.js b/test/server/configuration-test.js index 8cd371e37af..71d46667d67 100644 --- a/test/server/configuration-test.js +++ b/test/server/configuration-test.js @@ -552,6 +552,21 @@ describe('TestCafeConfiguration', function () { expect(configuration.getOption('hooks')).to.equal(clone.getOption('hooks')); }); }); + + describe('[API] CustomActions', () => { + it('Should get custom actions from the JS Configuration file property', async () => { + const customConfigFilePath = './test/server/data/custom-actions/config.js'; + + const configuration = new TestCafeConfiguration(customConfigFilePath); + + await configuration.init(); + + const customActions = configuration.getOption('customActions'); + + expect(customActions.makeSomething).to.be.a('function'); + expect(customActions.doSomething).to.be.a('function'); + }); + }); }); describe('Default values', function () { diff --git a/test/server/data/custom-actions/config.js b/test/server/data/custom-actions/config.js new file mode 100644 index 00000000000..d29f9a21165 --- /dev/null +++ b/test/server/data/custom-actions/config.js @@ -0,0 +1,11 @@ +module.exports = { + customActions: { + async makeSomething () { + await this.click(); + }, + async doSomething () { + await this.custom.makeSomething(); + }, + fake: 'some fake data' + } +} diff --git a/test/server/runner-test.js b/test/server/runner-test.js index c626c734ac4..d7f31e16bde 100644 --- a/test/server/runner-test.js +++ b/test/server/runner-test.js @@ -1336,6 +1336,41 @@ describe('Runner', () => { 'The testRun.globalAfter hook (string) is not of expected type (function).'); } }); + + it('Should raise an error if customActions property belongs to invalid type', async function () { + try { + await runner + .browsers(connection) + .src('test/server/data/test-suites/basic/testfile2.js') + .run({ + customActions: 'string', + }); + + throw new Error('Promise rejection expected'); + } + catch (err) { + expect(err.message).eql('The value of the customActions option does not belong to type Object. Refer to the following article for custom action setup instructions: CUSTOM_ACTIONS_LINK'); + } + }); + + it('Should raise an error if some of custom actions belongs to invalid type', async function () { + try { + await runner + .browsers(connection) + .src('test/server/data/test-suites/basic/testfile2.js') + .run({ + customActions: { + makeCoffee () { }, + makeTea: 'string', + }, + }); + + throw new Error('Promise rejection expected'); + } + catch (err) { + expect(err.message).eql('The makeTea custom action does not contain an asynchronous function. Actual data type: string. Refer to the following article for custom action setup instructions: CUSTOM_ACTIONS_LINK'); + } + }); }); describe('.clientScripts', () => { From 38e326c4e13a74108d58b75dd38cef4de3d89a5d Mon Sep 17 00:00:00 2001 From: artem-babich Date: Thu, 24 Nov 2022 09:53:41 +0400 Subject: [PATCH 05/15] skip debug tests --- test/functional/fixtures/custom-actions/test.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/functional/fixtures/custom-actions/test.js b/test/functional/fixtures/custom-actions/test.js index d25e288c902..b219b23116f 100644 --- a/test/functional/fixtures/custom-actions/test.js +++ b/test/functional/fixtures/custom-actions/test.js @@ -6,8 +6,9 @@ const { } = require('./actions'); const { expect } = require('chai'); +const config = require('../../config'); -describe('[API] Custom Actions', function () { +(config.experimentalDebug ? describe.skip : describe)('[API] Custom Actions', function () { it('Should run custom click action', function () { return runTests('./testcafe-fixtures/index.js', 'Should run custom click action', { customActions: { clickBySelector } }); }); From 1b9c9a88a78d5fc66131de1089672a236e661cfe Mon Sep 17 00:00:00 2001 From: artem-babich Date: Thu, 24 Nov 2022 10:13:56 +0400 Subject: [PATCH 06/15] remove custom-config.js --- custom-config.js | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 custom-config.js diff --git a/custom-config.js b/custom-config.js deleted file mode 100644 index a89c83d6d5b..00000000000 --- a/custom-config.js +++ /dev/null @@ -1,18 +0,0 @@ -module.exports = { - customActions: { - async makeCoffee (selector) { - await this - .click(selector) - .custom.makeTea('https://example.org', 'a'); - }, - async makeTea (url, selector) { - await this - .navigateTo(url) - .click(selector); - }, - async getSelectorValue () { - return 'a'; - }, - }, -}; - From f5b01675677c1ce2d897ac06bce121b7a6c3be35 Mon Sep 17 00:00:00 2001 From: artem-babich Date: Thu, 8 Dec 2022 14:34:13 +0400 Subject: [PATCH 07/15] refactor, add action name to command, add actionResult to reportTestActionDone --- .../{add-errors.ts => add-message.ts} | 0 src/api/test-controller/custom-actions.ts | 2 +- src/api/wrap-custom-action.ts | 40 ++-------- src/api/wrap-test-function.ts | 37 ++++++--- src/errors/runtime/templates.js | 2 +- src/reporter/command/command-formatter.ts | 10 ++- src/test-run/commands/actions.d.ts | 2 + src/test-run/commands/actions.js | 1 + .../fixtures/custom-actions/actions.js | 5 ++ .../fixtures/custom-actions/test.js | 76 ++++++++++++++++++- .../custom-actions/testcafe-fixtures/index.js | 6 ++ 11 files changed, 129 insertions(+), 52 deletions(-) rename src/api/test-controller/{add-errors.ts => add-message.ts} (100%) diff --git a/src/api/test-controller/add-errors.ts b/src/api/test-controller/add-message.ts similarity index 100% rename from src/api/test-controller/add-errors.ts rename to src/api/test-controller/add-message.ts diff --git a/src/api/test-controller/custom-actions.ts b/src/api/test-controller/custom-actions.ts index 27d366b8726..9425422a644 100644 --- a/src/api/test-controller/custom-actions.ts +++ b/src/api/test-controller/custom-actions.ts @@ -22,7 +22,7 @@ export default class CustomActions { this[delegatedAPI(name)] = (...args) => { const callsite = getCallsiteForMethod(name) || void 0; - return this._testController._enqueueCommand(RunCustomActionCommand, { fn, args }, this._validateCommand, callsite); + return this._testController._enqueueCommand(RunCustomActionCommand, { fn, args, name }, this._validateCommand, callsite); }; }); diff --git a/src/api/wrap-custom-action.ts b/src/api/wrap-custom-action.ts index 78f78610fc5..6933cc1e78e 100644 --- a/src/api/wrap-custom-action.ts +++ b/src/api/wrap-custom-action.ts @@ -1,42 +1,12 @@ -import TestController from './test-controller'; import testRunTracker from './test-run-tracker'; -import TestRun from '../test-run'; -import TestCafeErrorList from '../errors/error-list'; -import { MissingAwaitError } from '../errors/test-run'; -import addRenderedWarning from '../notifications/add-rendered-warning'; -import WARNING_MESSAGES from '../notifications/warning-message'; -import { addErrors, addWarnings } from './test-controller/add-errors'; +import wrapTestFunction, { WrapTestFunctionExecutorArguments } from './wrap-test-function'; export default function wrapCustomAction (fn: Function): Function { - return async (testRun: TestRun, functionArgs: any) => { - let result = null; - const errList = new TestCafeErrorList(); - - testRun.controller = new TestController(testRun); - + const executor = async function ({ testRun, functionArgs }: WrapTestFunctionExecutorArguments): Promise { const markeredfn = testRunTracker.addTrackingMarkerToFunction(testRun.id, fn, testRun.controller); - testRun.observedCallsites.clear(); - testRunTracker.ensureEnabled(); - - try { - result = await markeredfn(...functionArgs); - } - catch (err) { - errList.addError(err); - } - - if (!errList.hasUncaughtErrorsInTestCode) { - for (const { callsite, actionId } of testRun.observedCallsites.awaitedSnapshotWarnings.values()) - addRenderedWarning(testRun.warningLog, { message: WARNING_MESSAGES.excessiveAwaitInAssertion, actionId }, callsite); - - addWarnings(testRun.observedCallsites.unawaitedSnapshotCallsites, WARNING_MESSAGES.missingAwaitOnSnapshotProperty, testRun); - addErrors(testRun.observedCallsites.callsitesWithoutAwait, MissingAwaitError, errList); - } - - if (errList.hasErrors) - throw errList; - - return result; + return await markeredfn(...functionArgs); }; + + return wrapTestFunction(fn, executor); } diff --git a/src/api/wrap-test-function.ts b/src/api/wrap-test-function.ts index 3c955b460e6..25cc7b5c294 100644 --- a/src/api/wrap-test-function.ts +++ b/src/api/wrap-test-function.ts @@ -1,34 +1,48 @@ -import TestController from './test-controller'; import testRunTracker from './test-run-tracker'; import TestRun from '../test-run'; +import TestController from './test-controller'; import TestCafeErrorList from '../errors/error-list'; import { MissingAwaitError } from '../errors/test-run'; import addRenderedWarning from '../notifications/add-rendered-warning'; import WARNING_MESSAGES from '../notifications/warning-message'; -import { addErrors, addWarnings } from './test-controller/add-errors'; +import { addErrors, addWarnings } from './test-controller/add-message'; + +export interface WrapTestFunctionExecutorArguments { + testRun: TestRun; + functionArgs: any[]; + fn: Function; +} -export default function wrapTestFunction (fn: Function): Function { - return async (testRun: TestRun) => { - let result = null; - const errList = new TestCafeErrorList(); - const markeredfn = testRunTracker.addTrackingMarkerToFunction(testRun.id, fn); +const defaultExecutor = async function ({ testRun, fn }: WrapTestFunctionExecutorArguments): Promise { + const markeredfn = testRunTracker.addTrackingMarkerToFunction(testRun.id, fn); + + return await markeredfn(testRun.controller); +}; + +export default function wrapTestFunction (fn: Function, executor: Function = defaultExecutor): Function { + return async (testRun: TestRun, functionArgs: any) => { + let result = null; + const errList = new TestCafeErrorList(); testRun.controller = new TestController(testRun); testRun.observedCallsites.clear(); - testRunTracker.ensureEnabled(); try { - result = await markeredfn(testRun.controller); + result = await executor({ fn, functionArgs, testRun }); } catch (err) { errList.addError(err); } if (!errList.hasUncaughtErrorsInTestCode) { - for (const { callsite, actionId } of testRun.observedCallsites.awaitedSnapshotWarnings.values()) - addRenderedWarning(testRun.warningLog, { message: WARNING_MESSAGES.excessiveAwaitInAssertion, actionId }, callsite); + for (const { callsite, actionId } of testRun.observedCallsites.awaitedSnapshotWarnings.values()) { + addRenderedWarning(testRun.warningLog, { + message: WARNING_MESSAGES.excessiveAwaitInAssertion, + actionId, + }, callsite); + } addWarnings(testRun.observedCallsites.unawaitedSnapshotCallsites, WARNING_MESSAGES.missingAwaitOnSnapshotProperty, testRun); addErrors(testRun.observedCallsites.callsitesWithoutAwait, MissingAwaitError, errList); @@ -40,3 +54,4 @@ export default function wrapTestFunction (fn: Function): Function { return result; }; } + diff --git a/src/errors/runtime/templates.js b/src/errors/runtime/templates.js index fb662043511..daf311bb358 100644 --- a/src/errors/runtime/templates.js +++ b/src/errors/runtime/templates.js @@ -140,5 +140,5 @@ export default { [RUNTIME_ERRORS.invalidSkipJsErrorsCallbackWithOptionsProperty]: `The "{optionName}" option does not exist. Use the following options to configure skipJsErrors callback: ${getConcatenatedValuesString(Object.keys(SKIP_JS_ERRORS_CALLBACK_WITH_OPTIONS_OPTION_NAMES))}.`, [RUNTIME_ERRORS.invalidCommandInJsonCompiler]: `TestCafe terminated the test run. The "{path}" file contains an unknown Chrome User Flow action "{action}". Remove the action to continue. Refer to the following article for the definitive list of supported Chrome User Flow actions: https://testcafe.io/documentation/403998/guides/experimental-capabilities/chrome-replay-support#supported-replay-actions`, [RUNTIME_ERRORS.invalidCustomActionsOptionType]: `The value of the customActions option does not belong to type Object. Refer to the following article for custom action setup instructions: CUSTOM_ACTIONS_LINK`, - [RUNTIME_ERRORS.invalidCustomActionType]: `The {actionName} custom action does not contain an asynchronous function. Actual data type: {type}. Refer to the following article for custom action setup instructions: CUSTOM_ACTIONS_LINK`, + [RUNTIME_ERRORS.invalidCustomActionType]: `TestCafe cannot parse the custom action, because the action definition is invalid. Format the definition in accordance with the custom actions guide: CUSTOM_ACTIONS_LINK`, }; diff --git a/src/reporter/command/command-formatter.ts b/src/reporter/command/command-formatter.ts index f81160131d0..3f214e843a5 100644 --- a/src/reporter/command/command-formatter.ts +++ b/src/reporter/command/command-formatter.ts @@ -2,7 +2,7 @@ import { isEmpty } from 'lodash'; import { ExecuteSelectorCommand, ExecuteClientFunctionCommand } from '../../test-run/commands/observation'; import { NavigateToCommand, - PressKeyCommand, + PressKeyCommand, RunCustomActionCommand, SetNativeDialogHandlerCommand, TypeTextCommand, UseRoleCommand, @@ -54,6 +54,9 @@ export class CommandFormatter { else this._assignProperties(this._command, formattedCommand); + if (this._command instanceof RunCustomActionCommand) + this._assignCustomActionResult(formattedCommand); + this._maskConfidentialInfo(formattedCommand); return formattedCommand; @@ -125,6 +128,11 @@ export class CommandFormatter { return command.url; } + private _assignCustomActionResult (formatedCommand: FormattedCommand) :void { + if (this._result !== void 0) + formatedCommand.actionResult = this._result; + } + private _assignProperties (command: CommandBase, formattedCommand: FormattedCommand): void { if (!this._command.getReportedProperties) return; diff --git a/src/test-run/commands/actions.d.ts b/src/test-run/commands/actions.d.ts index 5e00ab8be31..e7d23c9fcde 100644 --- a/src/test-run/commands/actions.d.ts +++ b/src/test-run/commands/actions.d.ts @@ -281,6 +281,8 @@ export class RemoveRequestHooksCommand extends ActionCommandBase { export class RunCustomActionCommand extends ActionCommandBase { public constructor (obj: object, testRun: TestRun, validateProperties: boolean); public fn: Function; + public name: string; public args: any; + public actionResult: any; } diff --git a/src/test-run/commands/actions.js b/src/test-run/commands/actions.js index 905ba29fe2c..d8483ab963b 100644 --- a/src/test-run/commands/actions.js +++ b/src/test-run/commands/actions.js @@ -779,6 +779,7 @@ export class RunCustomActionCommand extends ActionCommandBase { getAssignableProperties () { return [ { name: 'fn', type: functionArgument, required: true }, + { name: 'name', type: stringArgument, required: true }, { name: 'args', required: false }, ]; } diff --git a/test/functional/fixtures/custom-actions/actions.js b/test/functional/fixtures/custom-actions/actions.js index 1d683158e8e..8f80fb6dafa 100644 --- a/test/functional/fixtures/custom-actions/actions.js +++ b/test/functional/fixtures/custom-actions/actions.js @@ -17,9 +17,14 @@ async function typeToInputAndCheckResult (inputSelector, buttonSelector, resultS .expect(await this.custom.getSpanTextBySelector(resultSelector)).eql(inputText); } +function getTextValue () { + return 'some text'; +} + module.exports = { getSpanTextBySelector, clickBySelector, typeTextAndClickButton, typeToInputAndCheckResult, + getTextValue, }; diff --git a/test/functional/fixtures/custom-actions/test.js b/test/functional/fixtures/custom-actions/test.js index b219b23116f..0833aec6e9e 100644 --- a/test/functional/fixtures/custom-actions/test.js +++ b/test/functional/fixtures/custom-actions/test.js @@ -2,11 +2,12 @@ const { clickBySelector, getSpanTextBySelector, typeTextAndClickButton, - typeToInputAndCheckResult, + typeToInputAndCheckResult, getTextValue, } = require('./actions'); -const { expect } = require('chai'); -const config = require('../../config'); +const { expect } = require('chai'); +const config = require('../../config'); +const { createReporter } = require('../../utils/reporter'); (config.experimentalDebug ? describe.skip : describe)('[API] Custom Actions', function () { it('Should run custom click action', function () { @@ -40,6 +41,14 @@ const config = require('../../config'); }); }); + it('Should run non-async custom action', function () { + return runTests('./testcafe-fixtures/index.js', 'Should run non-async custom action', { + customActions: { + getTextValue, + }, + }); + }); + it('Should throw an exception inside custom action', function () { return runTests('./testcafe-fixtures/index.js', 'Should throw an exception inside custom action', { customActions: { clickBySelector }, @@ -58,5 +67,66 @@ const config = require('../../config'); expect(errs[0]).contains('TypeError: t.custom.clickBySelector is not a function'); }); }); + + it('Should report all actions in correct order', function () { + function ReporterRecord (phase, actionName, command) { + this.phase = phase; + this.actionName = actionName; + if (command.type !== 'run-custom-action') + return this; + + delete command.actionId; + delete command.fn; + delete command.args; + + this.command = command; + } + + const result = []; + const expectedResult = [ + { phase: 'start', actionName: 'runCustomAction', command: { type: 'run-custom-action', name: 'typeToInputAndCheckResult' } }, + { phase: 'start', actionName: 'runCustomAction', command: { type: 'run-custom-action', name: 'typeTextAndClickButton' } }, + { phase: 'start', actionName: 'typeText' }, + { phase: 'end', actionName: 'typeText' }, + { phase: 'start', actionName: 'click' }, + { phase: 'end', actionName: 'click' }, + { phase: 'end', actionName: 'runCustomAction', command: { type: 'run-custom-action', name: 'typeTextAndClickButton' } }, + { phase: 'start', actionName: 'runCustomAction', command: { type: 'run-custom-action', name: 'getSpanTextBySelector' } }, + { phase: 'start', actionName: 'execute-selector' }, + { phase: 'end', actionName: 'execute-selector' }, + { + phase: 'end', + actionName: 'runCustomAction', + command: { + type: 'run-custom-action', + name: 'getSpanTextBySelector', + actionResult: 'Some text', + }, + }, + { phase: 'start', actionName: 'eql' }, + { phase: 'end', actionName: 'eql' }, + { phase: 'end', actionName: 'runCustomAction', command: { type: 'run-custom-action', name: 'typeToInputAndCheckResult' } }, + ]; + + const reporter = createReporter({ + reportTestActionStart: (name, { command }) => { + result.push(new ReporterRecord('start', name, command)); + }, + reportTestActionDone: (name, { command }) => { + result.push(new ReporterRecord('end', name, command)); + }, + }); + + return runTests('./testcafe-fixtures/index.js', 'Should run custom action inside another custom action', { + customActions: { + typeToInputAndCheckResult, + typeTextAndClickButton, + getSpanTextBySelector, + }, + reporter, + }).then(() => { + expect(result).to.deep.equal(expectedResult); + }); + }); }); diff --git a/test/functional/fixtures/custom-actions/testcafe-fixtures/index.js b/test/functional/fixtures/custom-actions/testcafe-fixtures/index.js index 614cf21c242..0642bb565df 100644 --- a/test/functional/fixtures/custom-actions/testcafe-fixtures/index.js +++ b/test/functional/fixtures/custom-actions/testcafe-fixtures/index.js @@ -24,6 +24,12 @@ test('Should run custom action inside another custom action', async t => { await t.custom.typeToInputAndCheckResult('#input1', '#button2', '#result2', 'Some text'); }); +test('Should run non-async custom action', async t => { + const result = await t.custom.getTextValue(); + + await t.expect(result).eql('some text'); +}); + test('Should throw an exception inside custom action', async t => { await t.custom.clickBySelector('blablabla'); }); From 06beeec923bbff5f1df86dfe30091ceee5029a9f Mon Sep 17 00:00:00 2001 From: artem-babich Date: Thu, 8 Dec 2022 14:54:34 +0400 Subject: [PATCH 08/15] fix error message, fix server test --- src/errors/runtime/templates.js | 2 +- test/server/runner-test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/errors/runtime/templates.js b/src/errors/runtime/templates.js index daf311bb358..a955b555093 100644 --- a/src/errors/runtime/templates.js +++ b/src/errors/runtime/templates.js @@ -140,5 +140,5 @@ export default { [RUNTIME_ERRORS.invalidSkipJsErrorsCallbackWithOptionsProperty]: `The "{optionName}" option does not exist. Use the following options to configure skipJsErrors callback: ${getConcatenatedValuesString(Object.keys(SKIP_JS_ERRORS_CALLBACK_WITH_OPTIONS_OPTION_NAMES))}.`, [RUNTIME_ERRORS.invalidCommandInJsonCompiler]: `TestCafe terminated the test run. The "{path}" file contains an unknown Chrome User Flow action "{action}". Remove the action to continue. Refer to the following article for the definitive list of supported Chrome User Flow actions: https://testcafe.io/documentation/403998/guides/experimental-capabilities/chrome-replay-support#supported-replay-actions`, [RUNTIME_ERRORS.invalidCustomActionsOptionType]: `The value of the customActions option does not belong to type Object. Refer to the following article for custom action setup instructions: CUSTOM_ACTIONS_LINK`, - [RUNTIME_ERRORS.invalidCustomActionType]: `TestCafe cannot parse the custom action, because the action definition is invalid. Format the definition in accordance with the custom actions guide: CUSTOM_ACTIONS_LINK`, + [RUNTIME_ERRORS.invalidCustomActionType]: `TestCafe cannot parse the "{actionName}" action, because the action definition is invalid. Format the definition in accordance with the custom actions guide: CUSTOM_ACTIONS_LINK`, }; diff --git a/test/server/runner-test.js b/test/server/runner-test.js index d7f31e16bde..800db9c1979 100644 --- a/test/server/runner-test.js +++ b/test/server/runner-test.js @@ -1368,7 +1368,7 @@ describe('Runner', () => { throw new Error('Promise rejection expected'); } catch (err) { - expect(err.message).eql('The makeTea custom action does not contain an asynchronous function. Actual data type: string. Refer to the following article for custom action setup instructions: CUSTOM_ACTIONS_LINK'); + expect(err.message).eql('TestCafe cannot parse the "makeTea" action, because the action definition is invalid. Format the definition in accordance with the custom actions guide: CUSTOM_ACTIONS_LINK'); } }); }); From 905b5a56281cde3fe9ef35262aef58923d8f2de0 Mon Sep 17 00:00:00 2001 From: artem-babich Date: Thu, 8 Dec 2022 17:04:09 +0400 Subject: [PATCH 09/15] fix functional test for mobile, refactor types --- src/test-run/commands/actions.d.ts | 1 - .../fixtures/custom-actions/test.js | 21 ++++++++++++------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/test-run/commands/actions.d.ts b/src/test-run/commands/actions.d.ts index e7d23c9fcde..513f3a2fbcc 100644 --- a/src/test-run/commands/actions.d.ts +++ b/src/test-run/commands/actions.d.ts @@ -283,6 +283,5 @@ export class RunCustomActionCommand extends ActionCommandBase { public fn: Function; public name: string; public args: any; - public actionResult: any; } diff --git a/test/functional/fixtures/custom-actions/test.js b/test/functional/fixtures/custom-actions/test.js index 0833aec6e9e..641077cf830 100644 --- a/test/functional/fixtures/custom-actions/test.js +++ b/test/functional/fixtures/custom-actions/test.js @@ -82,7 +82,7 @@ const { createReporter } = require('../../utils/reporter'); this.command = command; } - const result = []; + const result = {}; const expectedResult = [ { phase: 'start', actionName: 'runCustomAction', command: { type: 'run-custom-action', name: 'typeToInputAndCheckResult' } }, { phase: 'start', actionName: 'runCustomAction', command: { type: 'run-custom-action', name: 'typeTextAndClickButton' } }, @@ -108,13 +108,16 @@ const { createReporter } = require('../../utils/reporter'); { phase: 'end', actionName: 'runCustomAction', command: { type: 'run-custom-action', name: 'typeToInputAndCheckResult' } }, ]; + function addTestRecord ( phase, testRunId, name, command) { + if (!result[testRunId]) + result[testRunId] = []; + + result[testRunId].push(new ReporterRecord(phase, name, command)); + } + const reporter = createReporter({ - reportTestActionStart: (name, { command }) => { - result.push(new ReporterRecord('start', name, command)); - }, - reportTestActionDone: (name, { command }) => { - result.push(new ReporterRecord('end', name, command)); - }, + reportTestActionStart: (name, { command, testRunId }) => addTestRecord('start', testRunId, name, command), + reportTestActionDone: (name, { command, testRunId }) => addTestRecord('end', testRunId, name, command), }); return runTests('./testcafe-fixtures/index.js', 'Should run custom action inside another custom action', { @@ -125,7 +128,9 @@ const { createReporter } = require('../../utils/reporter'); }, reporter, }).then(() => { - expect(result).to.deep.equal(expectedResult); + Object.values(result).map(res => { + expect(res).to.deep.equal(expectedResult); + }); }); }); }); From 31be429bd4eb6e39c0495c88c26e56cbb7c91064 Mon Sep 17 00:00:00 2001 From: artem-babich Date: Fri, 9 Dec 2022 11:39:17 +0400 Subject: [PATCH 10/15] refactor: rename public enqueueCommand --- src/api/test-controller/assertion.ts | 2 +- src/api/test-controller/custom-actions.ts | 2 +- src/api/test-controller/index.d.ts | 2 +- src/api/test-controller/index.js | 88 +++++++++++------------ 4 files changed, 47 insertions(+), 47 deletions(-) diff --git a/src/api/test-controller/assertion.ts b/src/api/test-controller/assertion.ts index 39b1c869387..4ea676b42d5 100644 --- a/src/api/test-controller/assertion.ts +++ b/src/api/test-controller/assertion.ts @@ -60,7 +60,7 @@ export default class Assertion { message = void 0; } - return this._testController._enqueueCommand(command, { + return this._testController.enqueueCommand(command, { assertionType: command.methodName, actual: this._actual, expected: assertionArgs.expected, diff --git a/src/api/test-controller/custom-actions.ts b/src/api/test-controller/custom-actions.ts index 9425422a644..2defcf10236 100644 --- a/src/api/test-controller/custom-actions.ts +++ b/src/api/test-controller/custom-actions.ts @@ -22,7 +22,7 @@ export default class CustomActions { this[delegatedAPI(name)] = (...args) => { const callsite = getCallsiteForMethod(name) || void 0; - return this._testController._enqueueCommand(RunCustomActionCommand, { fn, args, name }, this._validateCommand, callsite); + return this._testController.enqueueCommand(RunCustomActionCommand, { fn, args, name }, this._validateCommand, callsite); }; }); diff --git a/src/api/test-controller/index.d.ts b/src/api/test-controller/index.d.ts index f50e1891948..886014efed0 100644 --- a/src/api/test-controller/index.d.ts +++ b/src/api/test-controller/index.d.ts @@ -9,7 +9,7 @@ export default class TestController { public constructor (testRun: TestRun | TestRunProxy); public testRun: TestRun; public warningLog: WarningLog; - public _enqueueCommand (CmdCtor: unknown, cmdArgs: object, validateCommand: Function, callsite?: CallsiteRecord): () => Promise; + public enqueueCommand (CmdCtor: unknown, cmdArgs: object, validateCommand: Function, callsite?: CallsiteRecord): () => Promise; public checkForExcessiveAwaits (checkedCallsite: CallsiteRecord, { actionId }: CommandBase): void; public static enableDebugForNonDebugCommands (): void; public static disableDebugForNonDebugCommands (): void; diff --git a/src/api/test-controller/index.js b/src/api/test-controller/index.js index da19f6985fd..05ace4ab26d 100644 --- a/src/api/test-controller/index.js +++ b/src/api/test-controller/index.js @@ -163,7 +163,7 @@ export default class TestController { return this.executionChain; } - _enqueueCommand (CmdCtor, cmdArgs, validateCommandFn, callsite) { + enqueueCommand (CmdCtor, cmdArgs, validateCommandFn, callsite) { callsite = callsite || getCallsiteForMethod(CmdCtor.methodName); const command = this._createCommand(CmdCtor, cmdArgs, callsite); @@ -227,7 +227,7 @@ export default class TestController { } [delegatedAPI(DispatchEventCommand.methodName)] (selector, eventName, options = {}) { - return this._enqueueCommand(DispatchEventCommand, { selector, eventName, options, relatedTarget: options.relatedTarget }); + return this.enqueueCommand(DispatchEventCommand, { selector, eventName, options, relatedTarget: options.relatedTarget }); } _prepareCookieArguments (args, isSetCommand = false) { @@ -250,17 +250,17 @@ export default class TestController { } [delegatedAPI(GetCookiesCommand.methodName)] (...args) { - return this._enqueueCommand(GetCookiesCommand, this._prepareCookieArguments(args)); + return this.enqueueCommand(GetCookiesCommand, this._prepareCookieArguments(args)); } [delegatedAPI(SetCookiesCommand.methodName)] (...args) { const { urls, cookies } = this._prepareCookieArguments(args, true); - return this._enqueueCommand(SetCookiesCommand, { cookies, url: urls[0] }); + return this.enqueueCommand(SetCookiesCommand, { cookies, url: urls[0] }); } [delegatedAPI(DeleteCookiesCommand.methodName)] (...args) { - return this._enqueueCommand(DeleteCookiesCommand, this._prepareCookieArguments(args)); + return this.enqueueCommand(DeleteCookiesCommand, this._prepareCookieArguments(args)); } _prepareRequestArguments (bindOptions, ...args) { @@ -323,27 +323,27 @@ export default class TestController { } [delegatedAPI(ClickCommand.methodName)] (selector, options) { - return this._enqueueCommand(ClickCommand, { selector, options }); + return this.enqueueCommand(ClickCommand, { selector, options }); } [delegatedAPI(RightClickCommand.methodName)] (selector, options) { - return this._enqueueCommand(RightClickCommand, { selector, options }); + return this.enqueueCommand(RightClickCommand, { selector, options }); } [delegatedAPI(DoubleClickCommand.methodName)] (selector, options) { - return this._enqueueCommand(DoubleClickCommand, { selector, options }); + return this.enqueueCommand(DoubleClickCommand, { selector, options }); } [delegatedAPI(HoverCommand.methodName)] (selector, options) { - return this._enqueueCommand(HoverCommand, { selector, options }); + return this.enqueueCommand(HoverCommand, { selector, options }); } [delegatedAPI(DragCommand.methodName)] (selector, dragOffsetX, dragOffsetY, options) { - return this._enqueueCommand(DragCommand, { selector, dragOffsetX, dragOffsetY, options }); + return this.enqueueCommand(DragCommand, { selector, dragOffsetX, dragOffsetY, options }); } [delegatedAPI(DragToElementCommand.methodName)] (selector, destinationSelector, options) { - return this._enqueueCommand(DragToElementCommand, { selector, destinationSelector, options }); + return this.enqueueCommand(DragToElementCommand, { selector, destinationSelector, options }); } _getSelectorForScroll (args) { @@ -384,7 +384,7 @@ export default class TestController { if (typeof args[0] === 'number') [ x, y, options ] = args; - return this._enqueueCommand(ScrollCommand, { selector, x, y, position, options }); + return this.enqueueCommand(ScrollCommand, { selector, x, y, position, options }); } [delegatedAPI(ScrollByCommand.methodName)] (...args) { @@ -392,23 +392,23 @@ export default class TestController { const [byX, byY, options] = args; - return this._enqueueCommand(ScrollByCommand, { selector, byX, byY, options }); + return this.enqueueCommand(ScrollByCommand, { selector, byX, byY, options }); } [delegatedAPI(ScrollIntoViewCommand.methodName)] (selector, options) { - return this._enqueueCommand(ScrollIntoViewCommand, { selector, options }); + return this.enqueueCommand(ScrollIntoViewCommand, { selector, options }); } [delegatedAPI(TypeTextCommand.methodName)] (selector, text, options) { - return this._enqueueCommand(TypeTextCommand, { selector, text, options }); + return this.enqueueCommand(TypeTextCommand, { selector, text, options }); } [delegatedAPI(SelectTextCommand.methodName)] (selector, startPos, endPos, options) { - return this._enqueueCommand(SelectTextCommand, { selector, startPos, endPos, options }); + return this.enqueueCommand(SelectTextCommand, { selector, startPos, endPos, options }); } [delegatedAPI(SelectTextAreaContentCommand.methodName)] (selector, startLine, startPos, endLine, endPos, options) { - return this._enqueueCommand(SelectTextAreaContentCommand, { + return this.enqueueCommand(SelectTextAreaContentCommand, { selector, startLine, startPos, @@ -419,7 +419,7 @@ export default class TestController { } [delegatedAPI(SelectEditableContentCommand.methodName)] (startSelector, endSelector, options) { - return this._enqueueCommand(SelectEditableContentCommand, { + return this.enqueueCommand(SelectEditableContentCommand, { startSelector, endSelector, options, @@ -427,30 +427,30 @@ export default class TestController { } [delegatedAPI(PressKeyCommand.methodName)] (keys, options) { - return this._enqueueCommand(PressKeyCommand, { keys, options }); + return this.enqueueCommand(PressKeyCommand, { keys, options }); } [delegatedAPI(WaitCommand.methodName)] (timeout) { - return this._enqueueCommand(WaitCommand, { timeout }); + return this.enqueueCommand(WaitCommand, { timeout }); } [delegatedAPI(NavigateToCommand.methodName)] (url) { - return this._enqueueCommand(NavigateToCommand, { url }); + return this.enqueueCommand(NavigateToCommand, { url }); } [delegatedAPI(SetFilesToUploadCommand.methodName)] (selector, filePath) { - return this._enqueueCommand(SetFilesToUploadCommand, { selector, filePath }); + return this.enqueueCommand(SetFilesToUploadCommand, { selector, filePath }); } [delegatedAPI(ClearUploadCommand.methodName)] (selector) { - return this._enqueueCommand(ClearUploadCommand, { selector }); + return this.enqueueCommand(ClearUploadCommand, { selector }); } [delegatedAPI(TakeScreenshotCommand.methodName)] (options) { if (options && typeof options !== 'object') options = { path: options }; - return this._enqueueCommand(TakeScreenshotCommand, options); + return this.enqueueCommand(TakeScreenshotCommand, options); } [delegatedAPI(TakeElementScreenshotCommand.methodName)] (selector, ...args) { @@ -465,33 +465,33 @@ export default class TestController { else commandArgs.path = args[0]; - return this._enqueueCommand(TakeElementScreenshotCommand, commandArgs); + return this.enqueueCommand(TakeElementScreenshotCommand, commandArgs); } [delegatedAPI(ResizeWindowCommand.methodName)] (width, height) { - return this._enqueueCommand(ResizeWindowCommand, { width, height }); + return this.enqueueCommand(ResizeWindowCommand, { width, height }); } [delegatedAPI(ResizeWindowToFitDeviceCommand.methodName)] (device, options) { - return this._enqueueCommand(ResizeWindowToFitDeviceCommand, { device, options }); + return this.enqueueCommand(ResizeWindowToFitDeviceCommand, { device, options }); } [delegatedAPI(MaximizeWindowCommand.methodName)] () { - return this._enqueueCommand(MaximizeWindowCommand); + return this.enqueueCommand(MaximizeWindowCommand); } [delegatedAPI(SwitchToIframeCommand.methodName)] (selector) { - return this._enqueueCommand(SwitchToIframeCommand, { selector }); + return this.enqueueCommand(SwitchToIframeCommand, { selector }); } [delegatedAPI(SwitchToMainWindowCommand.methodName)] () { - return this._enqueueCommand(SwitchToMainWindowCommand); + return this.enqueueCommand(SwitchToMainWindowCommand); } [delegatedAPI(OpenWindowCommand.methodName)] (url) { this._validateMultipleWindowCommand(OpenWindowCommand.methodName); - return this._enqueueCommand(OpenWindowCommand, { url }); + return this.enqueueCommand(OpenWindowCommand, { url }); } [delegatedAPI(CloseWindowCommand.methodName)] (window) { @@ -499,13 +499,13 @@ export default class TestController { this._validateMultipleWindowCommand(CloseWindowCommand.methodName); - return this._enqueueCommand(CloseWindowCommand, { windowId }); + return this.enqueueCommand(CloseWindowCommand, { windowId }); } [delegatedAPI(GetCurrentWindowCommand.methodName)] () { this._validateMultipleWindowCommand(GetCurrentWindowCommand.methodName); - return this._enqueueCommand(GetCurrentWindowCommand); + return this.enqueueCommand(GetCurrentWindowCommand); } [delegatedAPI(SwitchToWindowCommand.methodName)] (windowSelector) { @@ -525,19 +525,19 @@ export default class TestController { args = { windowId: windowSelector?.id }; } - return this._enqueueCommand(command, args); + return this.enqueueCommand(command, args); } [delegatedAPI(SwitchToParentWindowCommand.methodName)] () { this._validateMultipleWindowCommand(SwitchToParentWindowCommand.methodName); - return this._enqueueCommand(SwitchToParentWindowCommand); + return this.enqueueCommand(SwitchToParentWindowCommand); } [delegatedAPI(SwitchToPreviousWindowCommand.methodName)] () { this._validateMultipleWindowCommand(SwitchToPreviousWindowCommand.methodName); - return this._enqueueCommand(SwitchToPreviousWindowCommand); + return this.enqueueCommand(SwitchToPreviousWindowCommand); } _eval$ (fn, options) { @@ -551,7 +551,7 @@ export default class TestController { } [delegatedAPI(SetNativeDialogHandlerCommand.methodName)] (fn, options) { - return this._enqueueCommand(SetNativeDialogHandlerCommand, { + return this.enqueueCommand(SetNativeDialogHandlerCommand, { dialogHandler: { fn, options }, }); } @@ -596,37 +596,37 @@ export default class TestController { // NOTE: do not need to enqueue the Debug command if we are in debugging mode. // The Debug command will be executed by CDP. // Also, we are forced to add empty function to the execution chain to preserve it. - return this.isCompilerServiceMode() ? this._enqueueTask(DebugCommand.methodName, noop) : this._enqueueCommand(DebugCommand); + return this.isCompilerServiceMode() ? this._enqueueTask(DebugCommand.methodName, noop) : this.enqueueCommand(DebugCommand); } [delegatedAPI(SetTestSpeedCommand.methodName)] (speed) { - return this._enqueueCommand(SetTestSpeedCommand, { speed }); + return this.enqueueCommand(SetTestSpeedCommand, { speed }); } [delegatedAPI(SetPageLoadTimeoutCommand.methodName)] (duration) { - return this._enqueueCommand(SetPageLoadTimeoutCommand, { duration }, (testController, command) => { + return this.enqueueCommand(SetPageLoadTimeoutCommand, { duration }, (testController, command) => { addWarning(testController.warningLog, { message: getDeprecationMessage(DEPRECATED.setPageLoadTimeout), actionId: command.actionId }); }); } [delegatedAPI(UseRoleCommand.methodName)] (role) { - return this._enqueueCommand(UseRoleCommand, { role }); + return this.enqueueCommand(UseRoleCommand, { role }); } [delegatedAPI(SkipJsErrorsCommand.methodName)] (options) { - return this._enqueueCommand(SkipJsErrorsCommand, { options }); + return this.enqueueCommand(SkipJsErrorsCommand, { options }); } [delegatedAPI(AddRequestHooksCommand.methodName)] (...hooks) { hooks = flattenDeep(hooks); - return this._enqueueCommand(AddRequestHooksCommand, { hooks }); + return this.enqueueCommand(AddRequestHooksCommand, { hooks }); } [delegatedAPI(RemoveRequestHooksCommand.methodName)] (...hooks) { hooks = flattenDeep(hooks); - return this._enqueueCommand(RemoveRequestHooksCommand, { hooks }); + return this.enqueueCommand(RemoveRequestHooksCommand, { hooks }); } static enableDebugForNonDebugCommands () { From 2a7483873e12c1b151bc08d36c9e02d756ce1323 Mon Sep 17 00:00:00 2001 From: artem-babich Date: Sat, 10 Dec 2022 13:50:46 +0400 Subject: [PATCH 11/15] fix doc link, add actionResult for all runs, refactor --- src/errors/runtime/templates.js | 5 +++-- src/reporter/command/command-formatter.ts | 10 +++++----- test/functional/fixtures/custom-actions/test.js | 10 +++++----- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/errors/runtime/templates.js b/src/errors/runtime/templates.js index a955b555093..d7d3cbe3671 100644 --- a/src/errors/runtime/templates.js +++ b/src/errors/runtime/templates.js @@ -12,6 +12,7 @@ const DOCUMENTATION_LINKS = { TEST_SOURCE_PARAMETER: 'https://testcafe.io/documentation/402639/reference/command-line-interface#file-pathglob-pattern', FILTER_SETTINGS: 'https://testcafe.io/documentation/402638/reference/configuration-file#filter', HEADLESS_MODE: 'https://testcafe.io/documentation/402828/guides/concepts/browsers#test-in-headless-mode', + CUSTOM_ACTIONS: 'https://testcafe.io/documentation/404150/guides/advanced-guides/custom-test-actions', }; export default { @@ -139,6 +140,6 @@ export default { [RUNTIME_ERRORS.invalidSkipJsErrorsOptionsObjectProperty]: `The "{optionName}" option does not exist. Use the following options to configure skipJsErrors: ${getConcatenatedValuesString(Object.keys(SKIP_JS_ERRORS_OPTIONS_OBJECT_OPTION_NAMES))}.`, [RUNTIME_ERRORS.invalidSkipJsErrorsCallbackWithOptionsProperty]: `The "{optionName}" option does not exist. Use the following options to configure skipJsErrors callback: ${getConcatenatedValuesString(Object.keys(SKIP_JS_ERRORS_CALLBACK_WITH_OPTIONS_OPTION_NAMES))}.`, [RUNTIME_ERRORS.invalidCommandInJsonCompiler]: `TestCafe terminated the test run. The "{path}" file contains an unknown Chrome User Flow action "{action}". Remove the action to continue. Refer to the following article for the definitive list of supported Chrome User Flow actions: https://testcafe.io/documentation/403998/guides/experimental-capabilities/chrome-replay-support#supported-replay-actions`, - [RUNTIME_ERRORS.invalidCustomActionsOptionType]: `The value of the customActions option does not belong to type Object. Refer to the following article for custom action setup instructions: CUSTOM_ACTIONS_LINK`, - [RUNTIME_ERRORS.invalidCustomActionType]: `TestCafe cannot parse the "{actionName}" action, because the action definition is invalid. Format the definition in accordance with the custom actions guide: CUSTOM_ACTIONS_LINK`, + [RUNTIME_ERRORS.invalidCustomActionsOptionType]: `The value of the customActions option does not belong to type Object. Refer to the following article for custom action setup instructions: ${ DOCUMENTATION_LINKS.CUSTOM_ACTIONS }`, + [RUNTIME_ERRORS.invalidCustomActionType]: `TestCafe cannot parse the "{actionName}" action, because the action definition is invalid. Format the definition in accordance with the custom actions guide: ${ DOCUMENTATION_LINKS.CUSTOM_ACTIONS }`, }; diff --git a/src/reporter/command/command-formatter.ts b/src/reporter/command/command-formatter.ts index 3f214e843a5..aca22b94d2c 100644 --- a/src/reporter/command/command-formatter.ts +++ b/src/reporter/command/command-formatter.ts @@ -2,7 +2,8 @@ import { isEmpty } from 'lodash'; import { ExecuteSelectorCommand, ExecuteClientFunctionCommand } from '../../test-run/commands/observation'; import { NavigateToCommand, - PressKeyCommand, RunCustomActionCommand, + PressKeyCommand, + RunCustomActionCommand, SetNativeDialogHandlerCommand, TypeTextCommand, UseRoleCommand, @@ -55,7 +56,7 @@ export class CommandFormatter { this._assignProperties(this._command, formattedCommand); if (this._command instanceof RunCustomActionCommand) - this._assignCustomActionResult(formattedCommand); + formattedCommand.actionResult = this._prepareCustomActionResult(); this._maskConfidentialInfo(formattedCommand); @@ -128,9 +129,8 @@ export class CommandFormatter { return command.url; } - private _assignCustomActionResult (formatedCommand: FormattedCommand) :void { - if (this._result !== void 0) - formatedCommand.actionResult = this._result; + private _prepareCustomActionResult (): unknown { + return this._result; } private _assignProperties (command: CommandBase, formattedCommand: FormattedCommand): void { diff --git a/test/functional/fixtures/custom-actions/test.js b/test/functional/fixtures/custom-actions/test.js index 641077cf830..50a3097e003 100644 --- a/test/functional/fixtures/custom-actions/test.js +++ b/test/functional/fixtures/custom-actions/test.js @@ -84,14 +84,14 @@ const { createReporter } = require('../../utils/reporter'); const result = {}; const expectedResult = [ - { phase: 'start', actionName: 'runCustomAction', command: { type: 'run-custom-action', name: 'typeToInputAndCheckResult' } }, - { phase: 'start', actionName: 'runCustomAction', command: { type: 'run-custom-action', name: 'typeTextAndClickButton' } }, + { phase: 'start', actionName: 'runCustomAction', command: { type: 'run-custom-action', name: 'typeToInputAndCheckResult', actionResult: void 0 } }, + { phase: 'start', actionName: 'runCustomAction', command: { type: 'run-custom-action', name: 'typeTextAndClickButton', actionResult: void 0 } }, { phase: 'start', actionName: 'typeText' }, { phase: 'end', actionName: 'typeText' }, { phase: 'start', actionName: 'click' }, { phase: 'end', actionName: 'click' }, - { phase: 'end', actionName: 'runCustomAction', command: { type: 'run-custom-action', name: 'typeTextAndClickButton' } }, - { phase: 'start', actionName: 'runCustomAction', command: { type: 'run-custom-action', name: 'getSpanTextBySelector' } }, + { phase: 'end', actionName: 'runCustomAction', command: { type: 'run-custom-action', name: 'typeTextAndClickButton', actionResult: void 0 } }, + { phase: 'start', actionName: 'runCustomAction', command: { type: 'run-custom-action', name: 'getSpanTextBySelector', actionResult: void 0 } }, { phase: 'start', actionName: 'execute-selector' }, { phase: 'end', actionName: 'execute-selector' }, { @@ -105,7 +105,7 @@ const { createReporter } = require('../../utils/reporter'); }, { phase: 'start', actionName: 'eql' }, { phase: 'end', actionName: 'eql' }, - { phase: 'end', actionName: 'runCustomAction', command: { type: 'run-custom-action', name: 'typeToInputAndCheckResult' } }, + { phase: 'end', actionName: 'runCustomAction', command: { type: 'run-custom-action', name: 'typeToInputAndCheckResult', actionResult: void 0 } }, ]; function addTestRecord ( phase, testRunId, name, command) { From 1f02cf4c2f201ed3eae1099cac4deb8a7119a6f5 Mon Sep 17 00:00:00 2001 From: artem-babich Date: Sat, 10 Dec 2022 14:03:19 +0400 Subject: [PATCH 12/15] fix server test --- test/server/runner-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/server/runner-test.js b/test/server/runner-test.js index 800db9c1979..0b2bc9f3f95 100644 --- a/test/server/runner-test.js +++ b/test/server/runner-test.js @@ -1368,7 +1368,7 @@ describe('Runner', () => { throw new Error('Promise rejection expected'); } catch (err) { - expect(err.message).eql('TestCafe cannot parse the "makeTea" action, because the action definition is invalid. Format the definition in accordance with the custom actions guide: CUSTOM_ACTIONS_LINK'); + expect(err.message).eql('TestCafe cannot parse the "makeTea" action, because the action definition is invalid. Format the definition in accordance with the custom actions guide: https://testcafe.io/documentation/404150/guides/advanced-guides/custom-test-actions'); } }); }); From 40ef16b51a789d293b8b0d28b059e96b684caedb Mon Sep 17 00:00:00 2001 From: artem-babich Date: Sat, 10 Dec 2022 15:49:07 +0400 Subject: [PATCH 13/15] fix server test #2 --- test/server/runner-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/server/runner-test.js b/test/server/runner-test.js index 0b2bc9f3f95..f3fa74de08f 100644 --- a/test/server/runner-test.js +++ b/test/server/runner-test.js @@ -1349,7 +1349,7 @@ describe('Runner', () => { throw new Error('Promise rejection expected'); } catch (err) { - expect(err.message).eql('The value of the customActions option does not belong to type Object. Refer to the following article for custom action setup instructions: CUSTOM_ACTIONS_LINK'); + expect(err.message).eql('The value of the customActions option does not belong to type Object. Refer to the following article for custom action setup instructions: https://testcafe.io/documentation/404150/guides/advanced-guides/custom-test-actions'); } }); From 3a6709df7b01310a6d80c4e304da487795ed70ab Mon Sep 17 00:00:00 2001 From: artem-babich Date: Mon, 12 Dec 2022 11:15:03 +0400 Subject: [PATCH 14/15] refactor: remove unnecessary method --- src/reporter/command/command-formatter.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/reporter/command/command-formatter.ts b/src/reporter/command/command-formatter.ts index aca22b94d2c..b75f00b6076 100644 --- a/src/reporter/command/command-formatter.ts +++ b/src/reporter/command/command-formatter.ts @@ -56,7 +56,7 @@ export class CommandFormatter { this._assignProperties(this._command, formattedCommand); if (this._command instanceof RunCustomActionCommand) - formattedCommand.actionResult = this._prepareCustomActionResult(); + formattedCommand.actionResult = this._result; this._maskConfidentialInfo(formattedCommand); @@ -129,10 +129,6 @@ export class CommandFormatter { return command.url; } - private _prepareCustomActionResult (): unknown { - return this._result; - } - private _assignProperties (command: CommandBase, formattedCommand: FormattedCommand): void { if (!this._command.getReportedProperties) return; From 78294e66de02ec2e9697cd7f1c429b6d608e9f3e Mon Sep 17 00:00:00 2001 From: artem-babich Date: Mon, 12 Dec 2022 13:52:29 +0400 Subject: [PATCH 15/15] refactor: rename custom prop to customActions --- src/api/test-controller/index.js | 6 +++--- .../fixtures/custom-actions/actions.js | 4 ++-- test/functional/fixtures/custom-actions/test.js | 2 +- .../custom-actions/testcafe-fixtures/index.js | 16 ++++++++-------- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/api/test-controller/index.js b/src/api/test-controller/index.js index 05ace4ab26d..feab4a7bdc0 100644 --- a/src/api/test-controller/index.js +++ b/src/api/test-controller/index.js @@ -97,7 +97,7 @@ export default class TestController { this.testRun = testRun; this.executionChain = Promise.resolve(); this.warningLog = testRun.warningLog; - this.customActions = new CustomActions(this, testRun?.opts?.customActions); + this._customActions = new CustomActions(this, testRun?.opts?.customActions); this._addTestControllerToExecutionChain(); } @@ -222,8 +222,8 @@ export default class TestController { return this.testRun.browser; } - _custom$getter () { - return this.customActions || new CustomActions(this, this.testRun.opts.customActions); + _customActions$getter () { + return this._customActions || new CustomActions(this, this.testRun.opts.customActions); } [delegatedAPI(DispatchEventCommand.methodName)] (selector, eventName, options = {}) { diff --git a/test/functional/fixtures/custom-actions/actions.js b/test/functional/fixtures/custom-actions/actions.js index 8f80fb6dafa..372fea10cf7 100644 --- a/test/functional/fixtures/custom-actions/actions.js +++ b/test/functional/fixtures/custom-actions/actions.js @@ -13,8 +13,8 @@ async function typeTextAndClickButton (inputSelector, buttonSelector, inputText) } async function typeToInputAndCheckResult (inputSelector, buttonSelector, resultSelector, inputText) { - await this.custom.typeTextAndClickButton(inputSelector, buttonSelector, inputText) - .expect(await this.custom.getSpanTextBySelector(resultSelector)).eql(inputText); + await this.customActions.typeTextAndClickButton(inputSelector, buttonSelector, inputText) + .expect(await this.customActions.getSpanTextBySelector(resultSelector)).eql(inputText); } function getTextValue () { diff --git a/test/functional/fixtures/custom-actions/test.js b/test/functional/fixtures/custom-actions/test.js index 50a3097e003..c3be930533d 100644 --- a/test/functional/fixtures/custom-actions/test.js +++ b/test/functional/fixtures/custom-actions/test.js @@ -64,7 +64,7 @@ const { createReporter } = require('../../utils/reporter'); shouldFail: true, }) .catch(errs => { - expect(errs[0]).contains('TypeError: t.custom.clickBySelector is not a function'); + expect(errs[0]).contains('TypeError: t.customActions.clickBySelector is not a function'); }); }); diff --git a/test/functional/fixtures/custom-actions/testcafe-fixtures/index.js b/test/functional/fixtures/custom-actions/testcafe-fixtures/index.js index 0642bb565df..d34158409ab 100644 --- a/test/functional/fixtures/custom-actions/testcafe-fixtures/index.js +++ b/test/functional/fixtures/custom-actions/testcafe-fixtures/index.js @@ -4,32 +4,32 @@ fixture`Custom Actions` .page`http://localhost:3000/fixtures/custom-actions/pages/index.html`; test('Should run custom click action', async t => { - await t.custom.clickBySelector('#button1'); + await t.customActions.clickBySelector('#button1'); }); test('Should return value from custom action', async t => { - const before = await t.custom.getSpanTextBySelector('#result1'); - const after = await t.custom.clickBySelector('#button1') - .custom.getSpanTextBySelector('#result1'); + const before = await t.customActions.getSpanTextBySelector('#result1'); + const after = await t.customActions.clickBySelector('#button1') + .customActions.getSpanTextBySelector('#result1'); await t.expect(before).eql('').expect(after).eql('OK'); }); test('Should chain multiple actions', async t => { - await t.custom.typeTextAndClickButton('#input1', '#button2', 'Some text') + await t.customActions.typeTextAndClickButton('#input1', '#button2', 'Some text') .expect(Selector('#result2').innerText).eql('Some text'); }); test('Should run custom action inside another custom action', async t => { - await t.custom.typeToInputAndCheckResult('#input1', '#button2', '#result2', 'Some text'); + await t.customActions.typeToInputAndCheckResult('#input1', '#button2', '#result2', 'Some text'); }); test('Should run non-async custom action', async t => { - const result = await t.custom.getTextValue(); + const result = await t.customActions.getTextValue(); await t.expect(result).eql('some text'); }); test('Should throw an exception inside custom action', async t => { - await t.custom.clickBySelector('blablabla'); + await t.customActions.clickBySelector('blablabla'); });