CI proved cross-file sharing of a virtual-module mock body cannot work in
@vitest/browser-playwright 4.1.6: the static-import spread fails the hoist
("no top level variables"), and the await-vi.hoisted-import form fails to
parse ("Unexpected identifier 'vi'"). vi.hoisted has the same hoist
constraint as vi.mock, so there is no way to thread an external module's
body into the factory here.
Reverts Phase 1: restores the 4 $app/forms/$app/navigation specs to their
inline factories, inlines NotificationBell.spec's forms stub, deletes the
src/__mocks__/$app/* modules and the $mocks alias (vite, vitest-coverage,
kit). The no-factory-ban meta-test stays (no-factory vi.mock is still
banned). ADR-012 amended to record the infeasibility. Everything else
($app/state migration, confirm context-inject, notification refactor, the
pin, the meta-test) is unaffected. Part of #560.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
88 lines
3.4 KiB
TypeScript
88 lines
3.4 KiB
TypeScript
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([]);
|
|
});
|
|
});
|