import { describe, it, expect } from 'vitest'; import { readdirSync, readFileSync } from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; // Belt-and-braces detector for the no-factory vi.mock anti-pattern named in // ADR-012 (the PR #657 failure class). A `vi.mock('$app/navigation')` with no // factory does NOT auto-resolve to an adjacent __mocks__ file the way Jest's // __mocks__/ does: for SvelteKit virtual modules, vitest substitutes some // exports (plain function refs like goto) but leaves others bound to the live // implementation (replaceState, which delegates through a getter). The result // is a partial mock that crashes when an unsubstituted export is hit. // // The sanctioned form keeps an INLINE sync factory: // vi.mock('$app/forms', () => ({ enhance(node, submit) { ... } })); // (Sharing the body via a module imported into the factory is infeasible in // browser mode — vitest hoists vi.mock above the import; see ADR-012.) // // ESLint and the CI grep guard catch the pattern earlier; this in-suite test // catches it at every vitest invocation — the layer hardest to disable. It // also forecloses ADR-012's rejected Option C (config-level auto-resolve). // // We scan source text rather than parsing AST: fast, no parser dependency, // good enough for the named anti-pattern. The pattern matches a `vi.mock` // call whose only argument is a string literal (no factory after a comma). const NO_FACTORY_VI_MOCK = /vi\.mock\(\s*['"][^'"]+['"]\s*\)/; export function hasNoFactoryViMock(source: string): boolean { return NO_FACTORY_VI_MOCK.test(source); } const __dirname = path.dirname(fileURLToPath(import.meta.url)); const SRC_ROOT = path.resolve(__dirname, '..'); function findBrowserSpecs(): string[] { const entries = readdirSync(SRC_ROOT, { recursive: true, withFileTypes: true }); return entries .filter( (e) => e.isFile() && (e.name.endsWith('.svelte.test.ts') || e.name.endsWith('.svelte.spec.ts')) ) .map((e) => path.join(e.parentPath ?? (e as { path: string }).path, e.name)); } describe('scan: hasNoFactoryViMock', () => { it('flags a vi.mock with a string id and no factory', () => { expect(hasNoFactoryViMock(`vi.mock('$app/navigation');`)).toBe(true); }); it('flags a no-factory vi.mock written across multiple lines', () => { const fixture = `vi.mock( '$app/forms' );`; expect(hasNoFactoryViMock(fixture)).toBe(true); }); it('does not flag a vi.mock with an inline factory', () => { expect(hasNoFactoryViMock(`vi.mock('$app/forms', () => ({ enhance: () => () => {} }));`)).toBe( false ); }); it('does not flag a vi.mock with a multi-line inline factory', () => { const fixture = `vi.mock('$app/forms', () => ({ enhance: (node, submit) => ({ destroy() {} }) }));`; expect(hasNoFactoryViMock(fixture)).toBe(false); }); it('does not flag a vi.mock with a named factory reference', () => { expect(hasNoFactoryViMock(`vi.mock('$app/state', factory);`)).toBe(false); }); it('does not flag source with no vi.mock at all', () => { expect(hasNoFactoryViMock(`const x = vi.fn();`)).toBe(false); }); }); describe('browser specs: no no-factory vi.mock of a virtual module', () => { it('every src/**/*.svelte.{test,spec}.ts file keeps its factory', () => { const specFiles = findBrowserSpecs(); expect(specFiles.length).toBeGreaterThan(0); const offenders = specFiles.filter((file) => hasNoFactoryViMock(readFileSync(file, 'utf-8'))); expect(offenders).toEqual([]); }); });