test(meta): ban no-factory vi.mock of virtual modules
A vi.mock('$app/navigation') with no factory does not auto-resolve to a
__mocks__ file for SvelteKit virtual modules — it substitutes some
exports and leaves others (replaceState) bound to the live router, which
is exactly the PR #657 failure. This Node-mode source scan, mirroring
no-async-mock-factories and no-duplicate-mock-ids, fails at every vitest
invocation if any *.svelte.{spec,test}.ts reintroduces the pattern, and
forecloses ADR-012's rejected Option C. Part of #560.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
85
frontend/src/__meta__/no-factory-ban.test.ts
Normal file
85
frontend/src/__meta__/no-factory-ban.test.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
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 dedup pattern keeps the factory and shares its body:
|
||||
// import * as formsMock from '$mocks/$app/forms';
|
||||
// vi.mock('$app/forms', () => ({ ...formsMock }));
|
||||
//
|
||||
// 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 shared-body spread factory', () => {
|
||||
const fixture = `import * as formsMock from '$mocks/$app/forms';
|
||||
vi.mock('$app/forms', () => ({ ...formsMock }));`;
|
||||
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([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user