From b9579e2a2eb090151af72c96551213d44e68094b Mon Sep 17 00:00:00 2001 From: CRT Date: Tue, 13 Oct 2026 02:55:12 +0800 Subject: [PATCH] shots / console: resolve playwright only from an installed node_modules copy; in a project without playwright Bun auto-installed the newest one into its global cache, whose browsers were never downloaded (capture-web on Linux CI: 'headless shell: not installed'; macOS hid it with Google Chrome) --- cli/shots/browser.ts | 7 +++++++ tests/browser-resolve.test.ts | 20 +++++++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/cli/shots/browser.ts b/cli/shots/browser.ts index 6ce5358..00026ce 100644 --- a/cli/shots/browser.ts +++ b/cli/shots/browser.ts @@ -29,6 +29,10 @@ async function chromium(root: string): Promise { for (const base of [root, join(import.meta.dir, '../..')]) { try { const p = Bun.resolveSync('playwright', base); + // only an installed copy (node_modules): outside a project Bun auto-installs the newest playwright into its global + // cache (~/.bun/install/cache), whose browser build was never downloaded ("headless shell: not installed" on + // Linux CI, where no Google Chrome hides it); storebound's own copy is next + if (!isInstalledPackage(p)) continue; return (await import(p)).chromium; } catch { /* next */ } } @@ -37,6 +41,9 @@ async function chromium(root: string): Promise { throw new Error('playwright not found: in your project run `bun add -d playwright` (and `bunx playwright install chromium` if Google Chrome is not installed)'); } +/** A resolved module path inside a node_modules folder (not one of Bun's auto-installs from its global cache). */ +export const isInstalledPackage = (resolved: string): boolean => /[\\/]node_modules[\\/]/.test(resolved); + /** Playwright's chromium (same resolution as `launch`), for connectOverCDP. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export const chromiumFor = (root: string): Promise => chromium(root); diff --git a/tests/browser-resolve.test.ts b/tests/browser-resolve.test.ts index 62575f4..223f901 100644 --- a/tests/browser-resolve.test.ts +++ b/tests/browser-resolve.test.ts @@ -8,7 +8,7 @@ import { existsSync, mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync import { hostname, tmpdir } from 'node:os'; import { join } from 'node:path'; import { openConsoleBrowser } from '../cli/console/session'; -import { installedBrowsers, launch, launchCandidates, launchPersistent, persistentExecutable, profileHolder } from '../cli/shots/browser'; +import { chromiumFor, installedBrowsers, isInstalledPackage, launch, launchCandidates, launchPersistent, persistentExecutable, profileHolder } from '../cli/shots/browser'; /* eslint-disable @typescript-eslint/no-explicit-any */ @@ -139,3 +139,21 @@ describe('console fill', () => { expect(f.calls).toEqual([]); }); }); + +describe('playwright resolution', () => { + test('a project without playwright uses storebound\'s installed copy, never a Bun auto-install of the newest one', async () => { + // regression (Linux CI): `shots capture` in a temp project resolved playwright through Bun's auto-install (the + // newest version, in ~/.bun/install/cache) whose browser build was not downloaded: "headless shell: not installed" + expect(isInstalledPackage('/p/node_modules/playwright/index.mjs')).toBe(true); + expect(isInstalledPackage('C:\\p\\node_modules\\playwright\\index.mjs')).toBe(true); + expect(isInstalledPackage('/root/.bun/install/cache/playwright@1.64.0@@@1/index.mjs')).toBe(false); + const bare = mkdtempSync(join(tmpdir(), 'sb-no-playwright-')); + try { + // in a child whose cwd has no node_modules, like a real run in such a project (auto-install is on there) + const mod = JSON.stringify(join(import.meta.dir, '..', 'cli', 'shots', 'browser.ts')); + const r = Bun.spawnSync(['bun', '-e', `const { chromiumFor } = await import(${mod}); console.log((await chromiumFor(process.cwd())).executablePath());`], { cwd: bare }); + const own = await chromiumFor(join(import.meta.dir, '..')); + expect(r.stdout.toString().trim()).toBe(own.executablePath()); + } finally { rmSync(bare, { recursive: true, force: true }); } + }); +});