Skip to content

Commit b00c3e3

Browse files
feat: Allow additional assert function names with expect-expect (#155)
1 parent aca762e commit b00c3e3

File tree

6 files changed

+185
-2
lines changed

6 files changed

+185
-2
lines changed

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,24 @@ This plugin bundles two configurations to work with both `@playwright/test` or
4747
}
4848
```
4949

50+
## Global Settings
51+
52+
The plugin reads global settings from your ESLint configuration's shared data under the `playwright` key. It supports the following settings:
53+
54+
- `additionalAssertFunctionNames`: an array of function names to treat as assertion functions for the case of rules like `expect-expect`, which enforces the presence of at least one assertion per test case. This allows such rules to recognise custom assertion functions as valid assertions. The global setting applies to all modules. The [`expect-expect` rule accepts an option by the same name](./rules/expect-expect.md#additionalassertfunctionnames) to enable per-module configuration (.e.g, for module-specific custom assert functions).
55+
56+
You can configure these settings like so:
57+
58+
```json
59+
{
60+
"settings": {
61+
"playwright": {
62+
"additionalAssertFunctionNames": ["assertCustomCondition"]
63+
}
64+
}
65+
}
66+
```
67+
5068
## List of Supported Rules
5169

5270
✔: Enabled in the recommended configuration.\

docs/rules/expect-expect.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,42 @@ test('should work with callbacks/async', async () => {
2727
});
2828
});
2929
```
30+
31+
## Options
32+
33+
```json
34+
{
35+
"playwright/expect-expect": [
36+
"error",
37+
{
38+
"additionalAssertFunctionNames": ["assertCustomCondition"]
39+
}
40+
]
41+
}
42+
```
43+
44+
### `additionalAssertFunctionNames`
45+
46+
An array of function names to treat as assertion functions. Only standalone functions are supported. Configure globally acceptable assert function names using [the global setting](../global-settings.md). You can also customize assert function names per-file. For example:
47+
48+
```ts
49+
/* eslint playwright/expect-expect: ["error", { "additionalAssertFunctionNames": ["assertScrolledToBottom"] }] */
50+
51+
function assertScrolledToBottom(page) {
52+
// ...
53+
}
54+
55+
describe('scrolling', () => {
56+
test('button click', async ({ page }) => {
57+
// ...
58+
await assertScrolledToBottom(page)
59+
})
60+
61+
test('another way to scroll', async ({ page }) => {
62+
// ...
63+
await assertScrolledToBottom(page)
64+
})
65+
})
66+
```
67+
68+
The rule option and the global setting are merged. On a file level, both are considered.

src/rules/expect-expect.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,24 @@
11
import { Rule } from 'eslint';
22
import * as ESTree from 'estree';
3-
import { isExpectCall, isTest } from '../utils/ast';
3+
import { isExpectCall, isIdentifier, isTest } from '../utils/ast';
4+
import { getAdditionalAssertFunctionNames } from '../utils/misc';
5+
6+
function isAssertionCall(
7+
node: ESTree.CallExpression,
8+
additionalAssertFunctionNames: string[]
9+
) {
10+
return (
11+
isExpectCall(node) ||
12+
additionalAssertFunctionNames.find((name) =>
13+
isIdentifier(node.callee, name)
14+
)
15+
);
16+
}
417

518
export default {
619
create(context) {
720
const unchecked: ESTree.CallExpression[] = [];
21+
const additionalAssertFunctionNames = getAdditionalAssertFunctionNames(context)
822

923
function checkExpressions(nodes: ESTree.Node[]) {
1024
for (const node of nodes) {
@@ -22,7 +36,7 @@ export default {
2236
CallExpression(node) {
2337
if (isTest(node, ['fixme', 'only', 'skip'])) {
2438
unchecked.push(node);
25-
} else if (isExpectCall(node)) {
39+
} else if (isAssertionCall(node, additionalAssertFunctionNames)) {
2640
checkExpressions(context.getAncestors());
2741
}
2842
},
@@ -43,6 +57,18 @@ export default {
4357
messages: {
4458
noAssertions: 'Test has no assertions',
4559
},
60+
schema: [
61+
{
62+
additionalProperties: false,
63+
properties: {
64+
additionalAssertFunctionNames: {
65+
items: [{ type: 'string' }],
66+
type: 'array',
67+
},
68+
},
69+
type: 'object',
70+
},
71+
],
4672
type: 'problem',
4773
},
4874
} as Rule.RuleModule;

src/utils/misc.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,19 @@
1+
import { Rule } from 'eslint';
2+
3+
import { Settings } from './types';
4+
15
export const getAmountData = (amount: number) => ({
26
amount: amount.toString(),
37
s: amount === 1 ? '' : 's',
48
});
9+
10+
interface AssertFunctionNamesOptions {
11+
additionalAssertFunctionNames?: string[];
12+
};
13+
14+
export function getAdditionalAssertFunctionNames(context: Rule.RuleContext): string[] {
15+
const globalSettings = (context.settings as Settings).playwright?.additionalAssertFunctionNames ?? [] as string[]
16+
const ruleSettings = (context.options[0] as AssertFunctionNamesOptions | undefined)?.additionalAssertFunctionNames ?? [] as string[]
17+
18+
return [...globalSettings, ...ruleSettings]
19+
}

src/utils/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,9 @@ export type TypedNodeWithParent<T extends ESTree.Node['type']> = Extract<
1212
export type KnownCallExpression = ESTree.CallExpression & {
1313
callee: ESTree.MemberExpression;
1414
};
15+
16+
export interface Settings {
17+
playwright?: {
18+
additionalAssertFunctionNames?: readonly string[]
19+
}
20+
}

test/spec/expect-expect.spec.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,49 @@ runRuleTester('expect-expect', rule, {
1212
code: 'test.skip("should fail", () => {});',
1313
errors: [{ messageId: 'noAssertions' }],
1414
},
15+
{
16+
code: dedent`
17+
test('should fail', async ({ page }) => {
18+
await assertCustomCondition(page)
19+
})
20+
`,
21+
errors: [{ messageId: 'noAssertions' }],
22+
},
23+
{
24+
name: 'Global settings no false positives',
25+
code: dedent`
26+
test('should fail', async ({ page }) => {
27+
await assertCustomCondition(page)
28+
})
29+
`,
30+
errors: [{ messageId: 'noAssertions' }],
31+
settings: {
32+
playwright: { additionalAssertFunctionNames: ['wayComplexCustomCondition'] },
33+
},
34+
},
35+
{
36+
name: 'Rule settings no false positives',
37+
code: dedent`
38+
test('should fail', async ({ page }) => {
39+
await assertCustomCondition(page)
40+
})
41+
`,
42+
errors: [{ messageId: 'noAssertions' }],
43+
options: [{ additionalAssertFunctionNames: ['wayComplexCustomCondition'] }],
44+
},
45+
{
46+
name: 'Global settings no false positives',
47+
code: dedent`
48+
test('should fail', async ({ page }) => {
49+
await assertCustomCondition(page)
50+
})
51+
`,
52+
errors: [{ messageId: 'noAssertions' }],
53+
settings: {
54+
playwright: { additionalAssertFunctionNames: ['wayComplexGlobalCustomCondition'] },
55+
},
56+
options: [{ additionalAssertFunctionNames: ['wayComplexRuleCustomCondition'] }],
57+
},
1558
],
1659
valid: [
1760
'foo();',
@@ -36,5 +79,41 @@ runRuleTester('expect-expect', rule, {
3679
});
3780
`,
3881
},
82+
{
83+
name: 'Global settings only',
84+
code: dedent`
85+
test('should fail', async ({ page }) => {
86+
await assertCustomCondition(page)
87+
})
88+
`,
89+
settings: {
90+
playwright: { additionalAssertFunctionNames: ['assertCustomCondition'] }
91+
}
92+
},
93+
{
94+
name: 'Rule settings only',
95+
code: dedent`
96+
test('should fail', async ({ page }) => {
97+
await assertCustomCondition(page)
98+
})
99+
`,
100+
options: [{ additionalAssertFunctionNames: ['assertCustomCondition'] }],
101+
},
102+
{
103+
name: 'Global and rule settings combine rather than override',
104+
code: dedent`
105+
test('should fail', async ({ page }) => {
106+
await assertCustomCondition(page)
107+
})
108+
109+
test('complex failure', async ({ page }) => {
110+
await wayComplexCustomCondition(page)
111+
})
112+
`,
113+
settings: {
114+
playwright: { additionalAssertFunctionNames: ['assertCustomCondition'] },
115+
},
116+
options: [{ additionalAssertFunctionNames: ['wayComplexCustomCondition'] }],
117+
},
39118
],
40119
});

0 commit comments

Comments
 (0)