Skip to content

Commit f3dc5d2

Browse files
authored
Introduce CustomActions feature (closes #1535) (#7393)
## Purpose In some cases, you need to create custom actions that contain multiple default actions or other custom actions. ## Approach Provide the ability to describe and register custom actions in a JS configuration file. Create a **TestController.customActions** property containing these actions. If the user function returns a value, the result of calling the user action will be that value. If the function does not return a value, the result will be a TestController object, which you can chain further. ## API ### Define Custom Actions ```js // JS Configuration file module.exports = { customActions: { async makeSomething (selector) { await this.click(selector); }, async getSelectorValue (selector) { return await Selector(selector).innerText; }, } } ``` ### Use Custom Actions ```js test('Check span value', async t => { const spanValue = await t.customActions.getSelectorValue('#result'); await t.expect(spanValue).eql('OK'); }); test('Click the button and check span value', async t => { const spanValue = await t.customActions.makeSomething() .customActions.getSelectorValue('#result'); await t.expect(spanValue).eql('OK'); }); ``` ### Reporter Changes (For Dashboard team): - runCustomAction is a command that fires **reportTestActionStart** **before** any inner action starts and fires **reportTestActionDone** **after** the latest inner action fineshed. - you can identify each custom action run using **command.actionId** property. - in the case of concurrency mode you can also use testRunId. - you can also access the result of the runCustomAction command(the value returned by the custom action) in the **reportTestActionDone** function using **command.actionResult** property. An example of reporter: ```js const customActionsStack = {} const shouldReportInnerActions = false; function isCustomAction (name /*or type */) { return name ==='runCustomAction'; // or return type === 'run-custom-action'; } { reportTestActionStart: (actionName, { command, testRunId }) => { if(isCustomAction(actionName /* or command.type */ ) ) { const { actionId, name } = command; customActionsStack[testRunId].push({ actionId, name }) ; } }, reportTestActionDone: (name, { command, testRunId }) => { if( isCustomAction(actionName /* or command.type */ ) ) { customActionsStack[testRunId].pop(); const { actionResult, actionId, name } = command; // Do something with actionResult return; } if (!shouldReportInnerActions && customActionsStack[testRunId].length) { // Do not report action } else { // Report action } } } ``` ## References Closes #1535 ## Pre-Merge TODO - [x] Write tests for your proposed changes - [x] Make sure that existing tests do not fail - [x] Make sure that the documentation link is correct in the Error template
1 parent 5534e34 commit f3dc5d2

File tree

27 files changed

+521
-87
lines changed

27 files changed

+521
-87
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import addRenderedWarning from '../../notifications/add-rendered-warning';
2+
import TestRun from '../../test-run';
3+
import TestCafeErrorList from '../../errors/error-list';
4+
5+
export function addWarnings (callsiteSet: Set<Record<string, any>>, message: string, testRun: TestRun): void {
6+
callsiteSet.forEach(callsite => {
7+
addRenderedWarning(testRun.warningLog, message, callsite);
8+
callsiteSet.delete(callsite);
9+
});
10+
}
11+
12+
export function addErrors (callsiteSet: Set<Record<string, any>>, ErrorClass: any, errList: TestCafeErrorList): void {
13+
callsiteSet.forEach(callsite => {
14+
errList.addError(new ErrorClass(callsite));
15+
callsiteSet.delete(callsite);
16+
});
17+
}

src/api/test-controller/assertion.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export default class Assertion {
6060
message = void 0;
6161
}
6262

63-
return this._testController._enqueueCommand(command, {
63+
return this._testController.enqueueCommand(command, {
6464
assertionType: command.methodName,
6565
actual: this._actual,
6666
expected: assertionArgs.expected,
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { getCallsiteForMethod } from '../../errors/get-callsite';
2+
import { RunCustomActionCommand } from '../../test-run/commands/actions';
3+
import { delegateAPI } from '../../utils/delegated-api';
4+
import { Dictionary } from '../../configuration/interfaces';
5+
import TestController from './index';
6+
import delegatedAPI from './delegated-api';
7+
8+
export default class CustomActions {
9+
private _testController: TestController;
10+
private readonly _customActions: Dictionary<Function>;
11+
12+
constructor (testController: TestController, customActions: Dictionary<Function>) {
13+
this._testController = testController;
14+
this._customActions = customActions || {};
15+
16+
this._registerCustomActions();
17+
}
18+
19+
_registerCustomActions (): void {
20+
Object.entries(this._customActions).forEach(([ name, fn ]) => {
21+
// @ts-ignore
22+
this[delegatedAPI(name)] = (...args) => {
23+
const callsite = getCallsiteForMethod(name) || void 0;
24+
25+
return this._testController.enqueueCommand(RunCustomActionCommand, { fn, args, name }, this._validateCommand, callsite);
26+
};
27+
});
28+
29+
this._delegateAPI(this._customActions);
30+
}
31+
32+
_validateCommand (): boolean {
33+
return true;
34+
}
35+
36+
_delegateAPI (actions: Dictionary<Function>): void {
37+
const customActionsList = Object.entries(actions).map(([name]) => {
38+
return {
39+
srcProp: delegatedAPI(name),
40+
apiProp: name,
41+
accessor: '',
42+
};
43+
});
44+
45+
delegateAPI(this, customActionsList, { useCurrentCtxAsHandler: true });
46+
}
47+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function delegatedAPI (methodName: string, accessor = ''): string {
2+
return `_${ methodName }$${ accessor }`;
3+
}

src/api/test-controller/index.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export default class TestController {
99
public constructor (testRun: TestRun | TestRunProxy);
1010
public testRun: TestRun;
1111
public warningLog: WarningLog;
12-
public _enqueueCommand (CmdCtor: unknown, cmdArgs: object, validateCommand: Function): () => Promise<unknown>;
12+
public enqueueCommand (CmdCtor: unknown, cmdArgs: object, validateCommand: Function, callsite?: CallsiteRecord): () => Promise<unknown>;
1313
public checkForExcessiveAwaits (checkedCallsite: CallsiteRecord, { actionId }: CommandBase): void;
1414
public static enableDebugForNonDebugCommands (): void;
1515
public static disableDebugForNonDebugCommands (): void;

0 commit comments

Comments
 (0)