Files
Codeman/test/check-browser-test-excludes.test.ts
Codeman maintainer dfd3df8289 fix(build): merge-time fixes for #500
- pre-push hook: skip with a notice when npm is not on PATH (GUI git
  clients and IDEs often run hooks with a minimal PATH), instead of
  blocking every push on "npm: not found"; real-push test with a
  stripped PATH
- test/git-hooks.test.ts: pin GIT_CONFIG_NOSYSTEM=1 and
  GIT_CONFIG_GLOBAL=/dev/null around the resolveGitHooksDir tests, so
  an exported global or a system core.hooksPath no longer fails them
- watch tsconfig.json, .prettierignore and .editorconfig too:
  typecheck and format:check read them
- check:browser-excludes: fail loudly when the vitest list output and
  the walked test/**/*.test.ts tree share no path (format drift would
  otherwise pass vacuously)
- Reword the PRE_PUSH_MARKER comment: bumping its version would make every
  installed v1 hook read as foreign and never refresh again.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 16:28:56 +02:00

122 lines
4.9 KiB
TypeScript

/**
* @fileoverview scripts/check-browser-test-excludes.mjs: the detection side (which test
* files need a real browser) and the leak computation. The exclusion side is vitest's own
* `vitest list`, which `npm run check:browser-excludes` exercises for real in CI.
*/
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import {
findBrowserTests,
findLeaks,
findTestFiles,
importsBrowserDriver,
listingMatchesTree,
parseVitestFileList,
} from '../scripts/check-browser-test-excludes.mjs';
import { BROWSER_TEST_GLOBS } from '../config/test-suites';
const repoRoot = resolve(import.meta.dirname, '..');
// Fixture sources are assembled from the module name at runtime, so THIS file never contains
// a literal driver import and is not itself flagged by the checker it tests.
const fromImport = (mod: string) => `import { chromium, type Browser } from '${mod}';\n`;
describe('importsBrowserDriver', () => {
it.each([
fromImport('playwright'),
fromImport('playwright-core').replace(/'/g, '"'),
fromImport('@playwright/test'),
fromImport('puppeteer'),
`import type { Page } from '${'playwright'}';`,
`const { chromium } = require('${'playwright'}');`,
`const pw = await import('${'playwright'}');`,
])('flags %s', (src) => {
expect(importsBrowserDriver(src)).toBe(true);
});
it.each([
"import { describe } from 'vitest';",
"// needs ms-playwright's cache dir\nconst dir = '.cache/ms-playwright';",
fromImport('./playwright-helpers'),
fromImport('playwright-extra-thing'),
])('ignores %s', (src) => {
expect(importsBrowserDriver(src)).toBe(false);
});
});
describe('findBrowserTests (fixture tree)', () => {
let root: string;
beforeAll(() => {
root = mkdtempSync(join(tmpdir(), 'codeman-browser-excludes-'));
const put = (rel: string, src: string) => {
mkdirSync(join(root, rel, '..'), { recursive: true });
writeFileSync(join(root, rel), src);
};
put('test/unit.test.ts', "import { it } from 'vitest';\n");
put('test/legacy-name.test.ts', fromImport('playwright'));
put('test/new.browser.test.ts', fromImport('playwright'));
put('test/nested/deep.test.ts', fromImport('puppeteer'));
put('test/helpers/browser.ts', fromImport('playwright')); // not a test file
});
afterAll(() => rmSync(root, { recursive: true, force: true }));
it('finds driver imports by content, recursively, as sorted repo-relative paths', () => {
expect(findBrowserTests(root)).toEqual([
'test/legacy-name.test.ts',
'test/nested/deep.test.ts',
'test/new.browser.test.ts',
]);
});
it('lists every test file, browser-driven or not, in the same form', () => {
expect(findTestFiles(root)).toEqual([
'test/legacy-name.test.ts',
'test/nested/deep.test.ts',
'test/new.browser.test.ts',
'test/unit.test.ts',
]);
});
});
describe('parseVitestFileList + findLeaks', () => {
it('keeps only test paths and normalizes a leading ./', () => {
const out = '\n./test/a.test.ts\ntest/b.test.ts\nsome banner line\n test/c.test.ts \n';
expect([...parseVitestFileList(out)].sort()).toEqual(['test/a.test.ts', 'test/b.test.ts', 'test/c.test.ts']);
});
it('reports exactly the browser tests the CI set still collects', () => {
const ci = new Set(['test/unit.test.ts', 'test/legacy-name.test.ts']);
expect(findLeaks(['test/legacy-name.test.ts', 'test/new.browser.test.ts'], ci)).toEqual([
'test/legacy-name.test.ts',
]);
expect(findLeaks(['test/new.browser.test.ts'], ci)).toEqual([]);
});
it('flags a non-empty listing whose paths never match the tree instead of passing vacuously', () => {
const tree = ['test/legacy-name.test.ts', 'test/unit.test.ts'];
// e.g. a vitest upgrade that starts printing absolute paths: nothing leaks, but only
// because nothing matches, so the checker must refuse rather than report success.
const drifted = parseVitestFileList('/repo/test/legacy-name.test.ts\n/repo/test/unit.test.ts\n');
expect(drifted.size).toBe(2);
expect(findLeaks(['test/legacy-name.test.ts'], drifted)).toEqual([]);
expect(listingMatchesTree(drifted, tree)).toBe(false);
const healthy = parseVitestFileList('test/legacy-name.test.ts\ntest/unit.test.ts\n');
expect(listingMatchesTree(healthy, tree)).toBe(true);
});
});
describe('against this repository', () => {
it('detects every file already listed in BROWSER_TEST_GLOBS', () => {
// If detection stopped recognising a known browser test, the checker would go blind to
// exactly the class of file it exists for.
const detected = new Set(findBrowserTests(repoRoot));
const literals = BROWSER_TEST_GLOBS.filter((g) => !/[*?[{]/.test(g));
expect(literals.length).toBeGreaterThan(0);
for (const file of literals) expect(detected, file).toContain(file);
});
});