import { ESLint } from 'eslint' import { describe, expect, it } from 'vitest' const eslint = new ESLint({ cwd: process.cwd(), cache: false, }) const fixingEslint = new ESLint({ cwd: process.cwd(), cache: false, fix: true, }) const lint = async (filePath: string, code: string) => { const [result] = await eslint.lintText(code, { filePath }) return { errors: result.messages.filter(({ severity }) => severity === 2), output: result.output, } } const lintWithFix = async (filePath: string, code: string) => { const [result] = await fixingEslint.lintText(code, { filePath }) return result.output } const expectValid = async (filePath: string, code: string) => { const { errors } = await lint(filePath, code) expect(errors).toEqual([]) } const expectInvalid = async (filePath: string, code: string, ruleId: string) => { const { errors } = await lint(filePath, code) expect(errors.some((error) => error.ruleId === ruleId)).toBe(true) } describe('ESLint conventions', () => { it('enforces PascalCase component and camelCase utility/test filenames', async () => { await expectValid('src/components/FutureCard.tsx', 'export const FutureCard = () =>
') await expectInvalid( 'src/components/future-card.tsx', 'export const FutureCard = () =>
', 'check-file/filename-naming-convention', ) await expectValid('src/utilities/futureValue.ts', 'export const futureValue = 1') await expectValid('tests/int/futureValue.int.spec.ts', 'export const futureValue = 1') await expectInvalid( 'tests/int/future-value.int.spec.ts', 'export const futureValue = 1', 'check-file/filename-naming-convention', ) }, 15_000) it('requires uppercase component arrows to use const', async () => { await expectInvalid( 'src/components/FutureCard.tsx', 'export let FutureCard = () =>
', 'local/component-arrow-conventions', ) }) it('requires implicit JSX returns only for pure component arrows', async () => { const code = 'export const FutureCard = () => { return
}' const result = await lint('src/components/FutureCard.tsx', code) const output = await lintWithFix('src/components/FutureCard.tsx', code) expect( result.errors.some((error) => error.ruleId === 'local/component-arrow-conventions'), ).toBe(true) expect(output).toContain('export const FutureCard = () => (
)') await expectValid( 'src/components/FutureList.tsx', 'export const FutureList = () => { const items = [1]; return items.map((item) => { return
{item}
}) }', ) }) it('reports pure JSX component returns without deleting block comments', async () => { const code = 'export const FutureCard = () => { /* keep this rationale */ return
}' const result = await lint('src/components/FutureCard.tsx', code) const output = (await lintWithFix('src/components/FutureCard.tsx', code)) ?? code expect( result.errors.some((error) => error.ruleId === 'local/component-arrow-conventions'), ).toBe(true) expect(output).toContain('keep this rationale') }) it('allows callback block returns without applying arrow-body-style globally', async () => { await expectValid( 'src/components/FutureList.tsx', 'export const FutureList = () => [1].map((item) => { return
{item}
})', ) }) it('rejects every ordinary component default export form', async () => { const examples = [ 'export default function FutureCard() { return
}', 'const futureCard = () =>
; export default futureCard', 'export default memo(() =>
)', 'export default (function FutureCard() { return
})', 'export default function () { return
}', 'export default (() =>
)', ] for (const code of examples) { await expectInvalid( 'src/components/FutureCard.tsx', code, 'local/no-component-default-export', ) } }) it('accepts only an inline named function default in Next framework files', async () => { await expectValid( 'src/app/future/page.tsx', 'export default function FuturePage() { return
}', ) const invalidExamples = [ 'const futurePage = () =>
; export default futurePage', 'const FuturePage = () =>
; export default FuturePage', 'export default memo(() =>
)', 'export default (function FuturePage() { return
})', 'export default (() =>
)', 'export default function () { return
}', ] for (const code of invalidExamples) { await expectInvalid('src/app/future/page.tsx', code, 'local/framework-default-export') } }) it('requires Next framework files to have a default export', async () => { await expectInvalid( 'src/app/future/page.tsx', 'export function FuturePage() { return
}', 'local/framework-default-export', ) }) it('limits Payload admin exemptions to the exact existing catch-all files', async () => { await expectValid( 'src/app/(payload)/admin/[[...segments]]/page.tsx', 'const Page = () =>
; export default Page', ) await expectValid( 'src/app/(payload)/admin/[[...segments]]/not-found.tsx', 'const NotFound = () =>
; export default NotFound', ) await expectInvalid( 'src/app/(payload)/admin/future/page.tsx', 'export function FutureAdminPage() { return
}', 'local/framework-default-export', ) }) it('exempts non-component config object defaults', async () => { await expectValid( 'src/utilities/futureConfig.ts', 'const futureConfig = { enabled: true }; export default futureConfig', ) }) it('rejects non-async exports from top-level use server modules', async () => { await expectInvalid( 'src/app/(campaign)/campanha/actions/futureAction.ts', `'use server'\n\nexport const safeMessages = ['x'] as const\n`, 'local/use-server-async-exports', ) await expectInvalid( 'src/app/(campaign)/campanha/actions/futureAction.ts', `'use server'\n\nexport function syncAction() { return 1 }\n`, 'local/use-server-async-exports', ) await expectInvalid( 'src/app/(campaign)/campanha/actions/futureAction.ts', `'use server'\n\nexport { helper } from './futureHelper'\n`, 'local/use-server-async-exports', ) }) it('allows async function and type-only exports from use server modules', async () => { await expectValid( 'src/app/(campaign)/campanha/actions/futureAction.ts', `'use server'\nexport type Result = { ok: true }\nexport const save = async () => ({ ok: true as const })\nexport async function load() { return 1 }\n`, ) await expectValid( 'src/app/(campaign)/campanha/(app)/municipios/municipalityStaffEditMessages.ts', `export const municipalityStaffEditSafeMessages = ['x'] as const\n`, ) }) it('bans hardcoded ISO fixture dates in e2e specs but not state timestamps', async () => { await expectInvalid( 'tests/e2e/futureFixture.e2e.spec.ts', `const fixture = { startAt: '2026-09-01T13:00:00.000Z' }`, 'no-restricted-syntax', ) await expectInvalid( 'tests/e2e/futureFixture.e2e.spec.ts', `const fixture = { endDate: '2026-09-01T13:00:00.000Z' }`, 'no-restricted-syntax', ) // `disabledAt`/`lastSyncedAt` are the state under test, not a time window; // a computed `startAt` is exactly the sanctioned derivation. await expectValid( 'tests/e2e/futureFixture.e2e.spec.ts', `const fixture = { disabledAt: '2026-08-11T00:00:00.000Z' }\nconst startAt = 'dynamic'\nconst activity = { startAt }`, ) }) })