From 051dec3597c296cef238db78c0ff278ebd91f8c2 Mon Sep 17 00:00:00 2001 From: artem-babich Date: Thu, 8 Dec 2022 14:34:13 +0400 Subject: [PATCH] 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'); });