Skip to content

Commit

Permalink
Introduce CustomActions feature (closes #1535) (#7393)
Browse files Browse the repository at this point in the history
## 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
  • Loading branch information
Artem-Babich authored Dec 14, 2022
1 parent 5534e34 commit f3dc5d2
Show file tree
Hide file tree
Showing 27 changed files with 521 additions and 87 deletions.
17 changes: 17 additions & 0 deletions src/api/test-controller/add-message.ts
Original file line number Diff line number Diff line change
@@ -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<Record<string, any>>, message: string, testRun: TestRun): void {
callsiteSet.forEach(callsite => {
addRenderedWarning(testRun.warningLog, message, callsite);
callsiteSet.delete(callsite);
});
}

export function addErrors (callsiteSet: Set<Record<string, any>>, ErrorClass: any, errList: TestCafeErrorList): void {
callsiteSet.forEach(callsite => {
errList.addError(new ErrorClass(callsite));
callsiteSet.delete(callsite);
});
}
2 changes: 1 addition & 1 deletion src/api/test-controller/assertion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
47 changes: 47 additions & 0 deletions src/api/test-controller/custom-actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { getCallsiteForMethod } from '../../errors/get-callsite';
import { RunCustomActionCommand } from '../../test-run/commands/actions';
import { delegateAPI } from '../../utils/delegated-api';
import { Dictionary } from '../../configuration/interfaces';
import TestController from './index';
import delegatedAPI from './delegated-api';

export default class CustomActions {
private _testController: TestController;
private readonly _customActions: Dictionary<Function>;

constructor (testController: TestController, customActions: Dictionary<Function>) {
this._testController = testController;
this._customActions = customActions || {};

this._registerCustomActions();
}

_registerCustomActions (): void {
Object.entries(this._customActions).forEach(([ name, fn ]) => {
// @ts-ignore
this[delegatedAPI(name)] = (...args) => {
const callsite = getCallsiteForMethod(name) || void 0;

return this._testController.enqueueCommand(RunCustomActionCommand, { fn, args, name }, this._validateCommand, callsite);
};
});

this._delegateAPI(this._customActions);
}

_validateCommand (): boolean {
return true;
}

_delegateAPI (actions: Dictionary<Function>): void {
const customActionsList = Object.entries(actions).map(([name]) => {
return {
srcProp: delegatedAPI(name),
apiProp: name,
accessor: '',
};
});

delegateAPI(this, customActionsList, { useCurrentCtxAsHandler: true });
}
}
3 changes: 3 additions & 0 deletions src/api/test-controller/delegated-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function delegatedAPI (methodName: string, accessor = ''): string {
return `_${ methodName }$${ accessor }`;
}
2 changes: 1 addition & 1 deletion src/api/test-controller/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<unknown>;
public enqueueCommand (CmdCtor: unknown, cmdArgs: object, validateCommand: Function, callsite?: CallsiteRecord): () => Promise<unknown>;
public checkForExcessiveAwaits (checkedCallsite: CallsiteRecord, { actionId }: CommandBase): void;
public static enableDebugForNonDebugCommands (): void;
public static disableDebugForNonDebugCommands (): void;
Expand Down
Loading

0 comments on commit f3dc5d2

Please sign in to comment.