From 301cfc5c9e39a3333c7402061f9b88fa8e827686 Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 2 Jun 2026 19:30:57 +0200 Subject: [PATCH] test(meta): ban no-factory vi.mock of virtual modules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- frontend/src/__meta__/no-factory-ban.test.ts | 85 ++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 frontend/src/__meta__/no-factory-ban.test.ts diff --git a/frontend/src/__meta__/no-factory-ban.test.ts b/frontend/src/__meta__/no-factory-ban.test.ts new file mode 100644 index 00000000..10efcd64 --- /dev/null +++ b/frontend/src/__meta__/no-factory-ban.test.ts @@ -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([]); + }); +});