Skip to content

Commit cb0e6e3

Browse files
committed
test: add unit tests for ensurePlaywright auto-install logic
Tests cover: consumer resolution (fast path), auto-install when missing, skip-install when cached, and Chromium browser installation. Also adds a note to CLAUDE.md that features/fixes should include unit tests. https://claude.ai/code/session_01XDxRzi588C73zsj1xPwmEb
1 parent d638d3e commit cb0e6e3

File tree

2 files changed

+144
-0
lines changed

2 files changed

+144
-0
lines changed

CLAUDE.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ pnpm test # all packages
4747
pnpm test --watch # watch mode
4848
```
4949

50+
Features and bug fixes should include a unit test. Keep tests focused and avoid over-mocking —
51+
test the behaviour that matters, not implementation details.
52+
5053
Integration tests require a running app. See `tests/integration/`.
5154

5255
## External-First Design Principle
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import { join } from 'node:path';
3+
4+
// Mock node:child_process
5+
vi.mock('node:child_process', () => ({
6+
execFileSync: vi.fn(),
7+
}));
8+
9+
// Mock node:fs — keep real join/path but control existsSync and readdirSync
10+
vi.mock('node:fs', async () => {
11+
const actual = await vi.importActual<typeof import('node:fs')>('node:fs');
12+
return {
13+
...actual,
14+
existsSync: vi.fn(),
15+
readdirSync: vi.fn(),
16+
};
17+
});
18+
19+
// Mock node:module — control createRequire
20+
vi.mock('node:module', () => ({
21+
createRequire: vi.fn(),
22+
}));
23+
24+
// Mock node:os
25+
vi.mock('node:os', () => ({
26+
homedir: () => '/mock-home',
27+
tmpdir: () => '/mock-tmp',
28+
}));
29+
30+
import { existsSync, readdirSync } from 'node:fs';
31+
import { execFileSync } from 'node:child_process';
32+
import { createRequire } from 'node:module';
33+
import { ensurePlaywright } from '../../packages/core/src/recorder/ensure-playwright.js';
34+
35+
const mockExistsSync = vi.mocked(existsSync);
36+
const mockReaddirSync = vi.mocked(readdirSync);
37+
const mockExecFileSync = vi.mocked(execFileSync);
38+
const mockCreateRequire = vi.mocked(createRequire);
39+
40+
const fakePw = { chromium: { launch: vi.fn() } };
41+
42+
beforeEach(() => {
43+
vi.resetAllMocks();
44+
vi.spyOn(console, 'info').mockImplementation(() => {});
45+
});
46+
47+
describe('ensurePlaywright', () => {
48+
it('returns playwright from consumer node_modules when available', async () => {
49+
const fakeRequire = vi.fn().mockReturnValue(fakePw);
50+
mockCreateRequire.mockReturnValue(fakeRequire as any);
51+
52+
const result = await ensurePlaywright();
53+
54+
expect(result).toBe(fakePw);
55+
expect(mockCreateRequire).toHaveBeenCalledTimes(1);
56+
expect(String(mockCreateRequire.mock.calls[0][0])).toContain('package.json');
57+
// Should NOT run npm install
58+
expect(mockExecFileSync).not.toHaveBeenCalled();
59+
});
60+
61+
it('auto-installs when consumer does not have playwright', async () => {
62+
const consumerRequire = vi.fn().mockImplementation(() => {
63+
throw new Error('MODULE_NOT_FOUND');
64+
});
65+
const cacheRequire = vi.fn().mockReturnValue(fakePw);
66+
67+
mockCreateRequire
68+
.mockReturnValueOnce(consumerRequire as any)
69+
.mockReturnValueOnce(cacheRequire as any);
70+
71+
// Playwright package.json not yet in cache; Chromium is already present
72+
mockExistsSync.mockImplementation((p) => {
73+
const path = String(p);
74+
if (path.includes('@playwright')) return false;
75+
if (path.includes('ms-playwright')) return true;
76+
return false;
77+
});
78+
mockReaddirSync.mockReturnValue(['chromium-1234'] as any);
79+
80+
const result = await ensurePlaywright();
81+
82+
expect(result).toBe(fakePw);
83+
expect(mockExecFileSync).toHaveBeenCalledWith(
84+
'npm',
85+
expect.arrayContaining(['install', '@playwright/test']),
86+
expect.objectContaining({ stdio: 'inherit' }),
87+
);
88+
});
89+
90+
it('skips npm install when playwright is already cached', async () => {
91+
const consumerRequire = vi.fn().mockImplementation(() => {
92+
throw new Error('MODULE_NOT_FOUND');
93+
});
94+
const cacheRequire = vi.fn().mockReturnValue(fakePw);
95+
96+
mockCreateRequire
97+
.mockReturnValueOnce(consumerRequire as any)
98+
.mockReturnValueOnce(cacheRequire as any);
99+
100+
// Everything already exists
101+
mockExistsSync.mockReturnValue(true);
102+
mockReaddirSync.mockReturnValue(['chromium-1234'] as any);
103+
104+
const result = await ensurePlaywright();
105+
106+
expect(result).toBe(fakePw);
107+
// No npm install, no playwright install
108+
expect(mockExecFileSync).not.toHaveBeenCalled();
109+
});
110+
111+
it('installs chromium when browser cache is missing', async () => {
112+
const consumerRequire = vi.fn().mockImplementation(() => {
113+
throw new Error('MODULE_NOT_FOUND');
114+
});
115+
const cacheRequire = vi.fn().mockReturnValue(fakePw);
116+
117+
mockCreateRequire
118+
.mockReturnValueOnce(consumerRequire as any)
119+
.mockReturnValueOnce(cacheRequire as any);
120+
121+
const cacheDir = join('/mock-home', '.cache', 'git-glimpse', 'playwright');
122+
123+
mockExistsSync.mockImplementation((p) => {
124+
const path = String(p);
125+
// Package is installed but chromium cache doesn't exist
126+
if (path.includes('@playwright')) return true;
127+
if (path.includes('ms-playwright')) return false;
128+
return false;
129+
});
130+
131+
const result = await ensurePlaywright();
132+
133+
expect(result).toBe(fakePw);
134+
// Should have called playwright install chromium
135+
expect(mockExecFileSync).toHaveBeenCalledWith(
136+
join(cacheDir, 'node_modules', '.bin', 'playwright'),
137+
['install', 'chromium', '--with-deps'],
138+
expect.objectContaining({ stdio: 'inherit' }),
139+
);
140+
});
141+
});

0 commit comments

Comments
 (0)