Skip to content

Commit

Permalink
Add custom options to expect-expect (#235)
Browse files Browse the repository at this point in the history
* Exclude .idea files

* Update expect-expect to support multiple expressions

* Create docs for expect-expect with options

* Remove yarn.lock from PR

---------

Co-authored-by: Sam Mayer <samuel.a.mayer5.ctr@army.mil>
  • Loading branch information
samayer12 and Sam Mayer authored Aug 31, 2023
1 parent 17f5853 commit 30c5819
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 91 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
node_modules/
dist/
eslint-remote-tester-results/
fixtures/node_modules
fixtures/node_modules
.idea/
19 changes: 17 additions & 2 deletions docs/rules/expect-expect.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,22 @@ Examples of **correct** code for this rule:

```js
test('myLogic', () => {
const actual = myLogic()
expect(actual).toBe(true)
const actual = myLogic()
expect(actual).toBe(true)
})
```

## Options

> Default: `expect`
Array of custom expression strings that are converted into a regular expression.

```json
{
"custom-expressions": [
"expectValue",
"mySecondExpression"
]
}
```
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"eslint-doc-generator": "^1.4.3",
"eslint-plugin-eslint-plugin": "^5.1.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-vitest": "^0.2.8",
"eslint-remote-tester": "^3.0.0",
"eslint-remote-tester-repositories": "^1.0.1",
"jiti": "^1.19.3",
Expand All @@ -78,4 +79,4 @@
"dependencies": {
"@typescript-eslint/utils": "^6.4.0"
}
}
}
58 changes: 38 additions & 20 deletions src/rules/expect-expect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { getTestCallExpressionsFromDeclaredVariables, isTypeOfVitestFnCall } fro

export const RULE_NAME = 'expect-expect'
export type MESSAGE_ID = 'expectedExpect';
type Options = [{'custom-expressions': string[]}]

/**
* Checks if node names returned by getNodeName matches any of the given star patterns
Expand All @@ -12,43 +13,57 @@ export type MESSAGE_ID = 'expectedExpect';
* request.**.expect
* request.**.expect*
*/
function buildRegularExpression(pattern: string) {
return new RegExp(
`^${pattern
.split('.')
.map(x => {
if (x === '**') return '[a-z\\d\\.]*'
return x.replace(/\*/gu, '[a-z\\d]*')
})
.join('\\.')}(\\.|$)`,
'ui')
}

function matchesAssertFunctionName(
nodeName: string,
patterns: readonly string[]
): boolean {
return patterns.some(p =>
new RegExp(
`^${p
.split('.')
.map(x => {
if (x === '**') return '[a-z\\d\\.]*'
return x.replace(/\*/gu, '[a-z\\d]*')
})
.join('\\.')}(\\.|$)`,
'ui'
).test(nodeName)
return patterns.some(pattern =>
buildRegularExpression(pattern).test(nodeName)
)
}

export default createEslintRule<[], MESSAGE_ID>({
export default createEslintRule<Options, MESSAGE_ID>({
name: RULE_NAME,
meta: {
type: 'suggestion',
docs: {
description: 'Enforce having expectation in test body',
recommended: 'strict'
},
schema: [],
schema: [
{
type: 'object',
properties: {
'custom-expressions': {
type: 'array'
}
},
additionalProperties: false
}
],
messages: {
expectedExpect: 'Use \'expect\' in test body'
}
},
defaultOptions: [],
defaultOptions: [{ 'custom-expressions': ['expect'] }],
create: (context) => {
const unchecked: TSESTree.CallExpression[] = []
const validExpressions = context.options.map(option => option['custom-expressions']).flat()

function checkCallExpressionUsed(nodes: TSESTree.Node[]) {
function checkCallExpressionUsed(nodes: TSESTree.Node[], unchecked: TSESTree.CallExpression[]) {
for (const node of nodes) {
const index = node.type === AST_NODE_TYPES.CallExpression
? unchecked.indexOf(node)
Expand All @@ -57,7 +72,7 @@ export default createEslintRule<[], MESSAGE_ID>({
if (node.type === AST_NODE_TYPES.FunctionDeclaration) {
const declaredVariables = context.getDeclaredVariables(node)
const textCallExpression = getTestCallExpressionsFromDeclaredVariables(declaredVariables, context)
checkCallExpressionUsed(textCallExpression)
checkCallExpressionUsed(textCallExpression, unchecked)
}
if (index !== -1) {
unchecked.splice(index, 1)
Expand All @@ -72,10 +87,13 @@ export default createEslintRule<[], MESSAGE_ID>({

if (isTypeOfVitestFnCall(node, context, ['test'])) {
if (node.callee.type === AST_NODE_TYPES.MemberExpression &&
isSupportedAccessor(node.callee.property, 'todo')) return
isSupportedAccessor(node.callee.property, 'todo')) {
return
}

unchecked.push(node)
} else if (matchesAssertFunctionName(name, ['expect'])) {
checkCallExpressionUsed(context.getAncestors())
} else if (matchesAssertFunctionName(name, validExpressions)) {
checkCallExpressionUsed(context, unchecked)
}
},
'Program:exit'() {
Expand Down
155 changes: 88 additions & 67 deletions tests/expect-expect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,71 +3,92 @@ import rule, { RULE_NAME } from '../src/rules/expect-expect'
import { ruleTester } from './ruleTester'

describe(RULE_NAME, () => {
it(RULE_NAME, () => {
ruleTester.run(RULE_NAME, rule, {
valid: [
`test("shows error", () => {
expect(true).toBe(false);
});`,
`it("foo", function () {
expect(true).toBe(false);
})`,
`it('foo', () => {
expect(true).toBe(false);
});
function myTest() { if ('bar') {} }`,
`function myTest(param) {}
describe('my test', () => {
it('should do something', () => {
myTest("num");
expect(1).toEqual(1);
});
});`,
`const myFunc = () => {};
it("works", () => expect(myFunc()).toBe(undefined));`,
`describe('title', () => {
it('test is not ok', () => {
[1, 2, 3, 4, 5, 6].forEach((n) => {
expect(n).toBe(1);
});
});
});`,
`desctibe('title', () => {
test('some test', () => {
expect(obj1).not.toEqual(obj2);
})
})`,
'it("should pass", () => expect(true).toBeDefined())',
`const myFunc = () => {};
it("works", () => expect(myFunc()).toBe(undefined));`,
`const myFunc = () => {};
it("works", () => expect(myFunc()).toBe(undefined));`
],
invalid: [
{
code: 'test("shows error", () => {});',
errors: [{ messageId: 'expectedExpect' }]
},
{
code: `it("foo", function () {
if (1 === 2) {}
})`,
errors: [{ messageId: 'expectedExpect' }]
},
{
code: `import { it } from 'vitest';
describe('Button with increment', () => {
it('should show name props', () => {
console.log('test with missing expect');
});
});`,
errors: [{ messageId: 'expectedExpect' }]
},
{
code: 'it("should also fail",() => expectSaga(mySaga).returns());',
errors: [{ messageId: 'expectedExpect' }]
}
]
})
})
it(`${RULE_NAME} with custom expressions`, () => {
ruleTester.run(RULE_NAME, rule, {
valid: [{
code: 'test("shows success", () => {expectValue(true).toBe(false);});',
options: [{ 'custom-expressions': ['expectValue'] }]
},
{
code: 'test("shows success", () => {' +
'mySecondExpression(true).toBe(true);});',
options: [
{ 'custom-expressions': ['expectValue', 'mySecondExpression'] }
]
}],
invalid: [
{
code: 'test("shows error", () => {});',
errors: [{ messageId: 'expectedExpect' }]
}
]
})
})
it(`${RULE_NAME} without custom expressions`, () => {
ruleTester.run(RULE_NAME, rule, {
valid: [
`test("shows error", () => {
expect(true).toBe(false);
});`,
`it("foo", function () {
expect(true).toBe(false);
})`,
`it('foo', () => {
expect(true).toBe(false);
});
function myTest() { if ('bar') {} }`,
`function myTest(param) {}
describe('my test', () => {
it('should do something', () => {
myTest("num");
expect(1).toEqual(1);
});
});`,
`const myFunc = () => {};
it("works", () => expect(myFunc()).toBe(undefined));`,
`describe('title', () => {
it('test is not ok', () => {
[1, 2, 3, 4, 5, 6].forEach((n) => {
expect(n).toBe(1);
});
});
});`,
`describe('title', () => {
test('some test', () => {
expect(obj1).not.toEqual(obj2);
})
})`,
'it("should pass", () => expect(true).toBeDefined())',
`const myFunc = () => {};
it("works", () => expect(myFunc()).toBe(undefined));`,
`const myFunc = () => {};
it("works", () => expect(myFunc()).toBe(undefined));`
],
invalid: [
{
code: 'test("shows error", () => {});',
errors: [{ messageId: 'expectedExpect' }]
},
{
code: `it("foo", function () {
if (1 === 2) {}
})`,
errors: [{ messageId: 'expectedExpect' }]
},
{
code: `import { it } from 'vitest';
describe('Button with increment', () => {
it('should show name props', () => {
console.log('test with missing expect');
});
});`,
errors: [{ messageId: 'expectedExpect' }]
},
{
code: 'it("should also fail",() => expectSaga(mySaga).returns());',
errors: [{ messageId: 'expectedExpect' }]
}
]
})
})
})

0 comments on commit 30c5819

Please sign in to comment.