mirror of
https://github.com/payloadcms/payload.git
synced 2026-09-14 20:07:19 +08:00
eslint rule
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
const SCOPED_METHODS = new Set([
|
||||
'afterAll',
|
||||
'afterEach',
|
||||
'aroundAll',
|
||||
'aroundEach',
|
||||
'beforeAll',
|
||||
'beforeEach',
|
||||
'describe',
|
||||
'suite',
|
||||
])
|
||||
|
||||
/**
|
||||
* Returns the identifier at the start of a member or call chain.
|
||||
*
|
||||
* Examples:
|
||||
* - `test.describe` -> `test`
|
||||
* - `test.describe.each` -> `test`
|
||||
* - `test.options({ db: 'mongo' }).describe` -> `test`
|
||||
*/
|
||||
function getRootIdentifier(node) {
|
||||
if (!node) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (node.type === 'Identifier') {
|
||||
return node
|
||||
}
|
||||
|
||||
if (node.type === 'CallExpression') {
|
||||
return getRootIdentifier(node.callee)
|
||||
}
|
||||
|
||||
if (node.type === 'ChainExpression') {
|
||||
return getRootIdentifier(node.expression)
|
||||
}
|
||||
|
||||
if (node.type === 'MemberExpression') {
|
||||
return getRootIdentifier(node.object)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function getStaticPropertyName(node) {
|
||||
if (!node.computed && node.property.type === 'Identifier') {
|
||||
return node.property.name
|
||||
}
|
||||
|
||||
if (node.computed && node.property.type === 'Literal') {
|
||||
return typeof node.property.value === 'string' ? node.property.value : null
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
export const rule = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description:
|
||||
'Require standalone Vitest suite and hook functions so editor test discovery uses the correct test name',
|
||||
category: 'Possible Errors',
|
||||
recommended: false,
|
||||
},
|
||||
messages: {
|
||||
useStandalone:
|
||||
'Use the standalone `{{method}}(...)` API instead of `{{testIdentifier}}.{{method}}(...)`. Scoped suite and hook methods can make the VS Code Vitest extension discover the wrong test name.',
|
||||
},
|
||||
schema: [],
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
MemberExpression(node) {
|
||||
const method = getStaticPropertyName(node)
|
||||
|
||||
if (!method || !SCOPED_METHODS.has(method)) {
|
||||
return
|
||||
}
|
||||
|
||||
const rootIdentifier = getRootIdentifier(node.object)
|
||||
|
||||
if (!rootIdentifier || !['it', 'test'].includes(rootIdentifier.name)) {
|
||||
return
|
||||
}
|
||||
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'useStandalone',
|
||||
data: {
|
||||
method,
|
||||
testIdentifier: rootIdentifier.name,
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default rule
|
||||
@@ -0,0 +1,76 @@
|
||||
import { RuleTester } from 'eslint'
|
||||
import { afterAll, describe, it } from 'vitest'
|
||||
|
||||
import rule from './no-vitest-scoped-methods.js'
|
||||
|
||||
// Wire ESLint's RuleTester into Vitest so each case becomes a real test. These
|
||||
// static hooks exist at runtime but are not declared in `@types/eslint`.
|
||||
const ruleTesterHooks = RuleTester as unknown as {
|
||||
afterAll: typeof afterAll
|
||||
describe: typeof describe
|
||||
it: typeof it
|
||||
itOnly: typeof it.only
|
||||
}
|
||||
ruleTesterHooks.afterAll = afterAll
|
||||
ruleTesterHooks.describe = describe
|
||||
ruleTesterHooks.it = it
|
||||
ruleTesterHooks.itOnly = it.only
|
||||
|
||||
const ruleTester = new RuleTester({
|
||||
languageOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: 'module',
|
||||
},
|
||||
})
|
||||
|
||||
ruleTester.run('no-vitest-scoped-methods', rule, {
|
||||
valid: [
|
||||
"describe('posts', () => {})",
|
||||
'beforeEach(() => {})',
|
||||
"test('creates a post', () => {})",
|
||||
"test.skip('creates a post', () => {})",
|
||||
"test.each([])('creates a post', () => {})",
|
||||
"test.runIf(true)('creates a post', () => {})",
|
||||
"playwrightTest.describe('posts', () => {})",
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: "test.describe('posts', () => {})",
|
||||
errors: [
|
||||
{ data: { method: 'describe', testIdentifier: 'test' }, messageId: 'useStandalone' },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'test.beforeEach(() => {})',
|
||||
errors: [
|
||||
{ data: { method: 'beforeEach', testIdentifier: 'test' }, messageId: 'useStandalone' },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'it.afterAll(() => {})',
|
||||
errors: [{ data: { method: 'afterAll', testIdentifier: 'it' }, messageId: 'useStandalone' }],
|
||||
},
|
||||
{
|
||||
code: "test.suite({ config: './config.ts' })('posts', () => {})",
|
||||
errors: [{ data: { method: 'suite', testIdentifier: 'test' }, messageId: 'useStandalone' }],
|
||||
},
|
||||
{
|
||||
code: "test.describe.each([])('posts', () => {})",
|
||||
errors: [
|
||||
{ data: { method: 'describe', testIdentifier: 'test' }, messageId: 'useStandalone' },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "test.options({ db: 'mongo' }).describe('posts', () => {})",
|
||||
errors: [
|
||||
{ data: { method: 'describe', testIdentifier: 'test' }, messageId: 'useStandalone' },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "test['beforeEach'](() => {})",
|
||||
errors: [
|
||||
{ data: { method: 'beforeEach', testIdentifier: 'test' }, messageId: 'useStandalone' },
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
@@ -1,6 +1,7 @@
|
||||
import noJsxImportStatements from './customRules/no-jsx-import-statements.js'
|
||||
import noNonRetryableAssertions from './customRules/no-non-retryable-assertions.js'
|
||||
import noRelativeMonorepoImports from './customRules/no-relative-monorepo-imports.js'
|
||||
import noVitestScopedMethods from './customRules/no-vitest-scoped-methods.js'
|
||||
import noImportsFromExportsDir from './customRules/no-imports-from-exports-dir.js'
|
||||
import noFlakyAssertions from './customRules/no-flaky-assertions.js'
|
||||
import noImportsFromSelf from './customRules/no-imports-from-self.js'
|
||||
@@ -15,6 +16,7 @@ const index = {
|
||||
rules: {
|
||||
'no-jsx-import-statements': noJsxImportStatements,
|
||||
'no-relative-monorepo-imports': noRelativeMonorepoImports,
|
||||
'no-vitest-scoped-methods': noVitestScopedMethods,
|
||||
'no-imports-from-exports-dir': noImportsFromExportsDir,
|
||||
'no-imports-from-self': noImportsFromSelf,
|
||||
'no-conflicting-lexical-markdown-imports': noConflictingLexicalMarkdownImports,
|
||||
|
||||
@@ -47,6 +47,7 @@ export const testEslintConfig = [
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'payload/no-vitest-scoped-methods': 'error',
|
||||
'vitest/no-standalone-expect': [
|
||||
'error',
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user