// The one browser resolver (cli/shots/browser.ts): which executable each launch kind uses, checked against a fake // Playwright cache layout, never launching a real browser. Regression: with only chromium_headless_shell- in the // cache, `console fill --fresh-profile` reported "Executable doesn't exist ... chromium-" although Google Chrome // was installed (the Chrome error was hidden behind the first one; that Chrome failed because an earlier run still // held the profile). import { afterAll, describe, expect, test } from 'bun:test'; import { existsSync, mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'; import { hostname, tmpdir } from 'node:os'; import { join } from 'node:path'; import { openConsoleBrowser } from '../cli/console/session'; import { installedBrowsers, launch, launchCandidates, launchPersistent, profileHolder } from '../cli/shots/browser'; /* eslint-disable @typescript-eslint/no-explicit-any */ const tmp = mkdtempSync(join(tmpdir(), 'sb-browser-resolve-')); afterAll(() => rmSync(tmp, { recursive: true, force: true })); const CHROME = '/opt/google/chrome/chrome'; // channel "chrome" on linux /** A Playwright cache with the given builds (dirs) and a fake chromium whose launches are recorded. */ function fakeCache(name: string, builds: string[], opts: { chrome?: boolean; fail?: (o: any) => Error | null } = {}) { const cache = join(tmp, name); for (const b of builds) { const exe = b.startsWith('chromium-') ? join(cache, b, 'chrome-linux', 'chrome') : join(cache, b, 'chrome-linux', 'headless_shell'); mkdirSync(join(exe, '..'), { recursive: true }); writeFileSync(exe, ''); } const fullPath = join(cache, 'chromium-1187', 'chrome-linux', 'chrome'); const shellPresent = builds.some(b => b.startsWith('chromium_headless_shell-')); const calls: any[] = []; const start = (o: any) => { calls.push(o); const f = opts.fail?.(o); if (f) throw f; // what Playwright does: no channel + headless -> the headless shell; channel chromium / no channel headed -> full build const exe = o.executablePath ?? (o.channel === 'chrome' ? (opts.chrome ? CHROME : null) : (!o.channel && o.headless !== false) ? (shellPresent ? 'shell' : null) : existsSync(fullPath) ? fullPath : null); if (!exe) throw new Error(`browserType.launch: Executable doesn't exist at ${fullPath}`); return { exe, pages: () => [], newPage: async () => ({ close: async () => {} }), on: () => {}, close: async () => {} }; }; const pw = { executablePath: () => fullPath, launch: async (o: any) => start(o), launchPersistentContext: async (_dir: string, o: any) => start(o) }; const exists = (p: string) => p === CHROME ? !!opts.chrome : existsSync(p); const installed = installedBrowsers(pw, {}, exists, 'linux'); return { pw, calls, installed }; } describe('launch order', () => { test('visible / persistent: Google Chrome, then full Chromium, never the headless shell', () => { expect(launchCandidates('visible', { storeboundChrome: null, chrome: CHROME, chromium: '/c' }).map(c => c.opts)).toEqual([{ channel: 'chrome' }, { channel: 'chromium' }]); expect(launchCandidates('visible', { storeboundChrome: null, chrome: null, chromium: null })).toEqual([]); }); test('headless: headless shell, full Chromium, Google Chrome', () => { expect(launchCandidates('headless', { storeboundChrome: null, chrome: CHROME, chromium: '/c' }).map(c => c.opts)).toEqual([{}, { channel: 'chromium' }, { channel: 'chrome' }]); }); test('STOREBOUND_CHROME first for every kind but the Chrome-only copy mode', () => { const i = { storeboundChrome: '/x/chrome', chrome: CHROME, chromium: null }; expect(launchCandidates('headless', i)[0]!.opts).toEqual({ executablePath: '/x/chrome' }); expect(launchCandidates('visible', i)[0]!.opts).toEqual({ executablePath: '/x/chrome' }); expect(launchCandidates('chrome', i).map(c => c.opts)).toEqual([{ channel: 'chrome' }]); }); }); describe('fake Playwright cache', () => { test('headless shell only + Google Chrome: console (persistent) uses Chrome, renders use the shell', async () => { const f = fakeCache('shell-only', ['chromium_headless_shell-1187'], { chrome: true }); expect(f.installed.chromium).toBeNull(); const ctx = await launchPersistent(tmp, join(tmp, 'p1'), { headless: false }, { pw: f.pw, installed: f.installed }); expect(ctx.exe).toBe(CHROME); expect(f.calls).toEqual([{ headless: false, channel: 'chrome' }]); // a test asking for headless still gets a real Chrome, not the shell const h = await launchPersistent(tmp, join(tmp, 'p1'), { headless: true }, { pw: f.pw, installed: f.installed }); expect(h.exe).toBe(CHROME); const r = await launch(tmp, { headless: true }, { pw: f.pw, installed: f.installed }); expect(r.exe).toBe('shell'); }); test('full Chromium only: console uses it via channel "chromium"', async () => { const f = fakeCache('full-only', ['chromium-1187']); const ctx = await launchPersistent(tmp, join(tmp, 'p2'), { headless: true }, { pw: f.pw, installed: f.installed }); expect(ctx.exe).toBe(f.installed.chromium!); expect(f.calls[0].channel).toBe('chromium'); }); test('headless render without the shell falls back to full Chromium', async () => { const f = fakeCache('render-full', ['chromium-1187'], { chrome: true }); const r = await launch(tmp, { headless: true }, { pw: f.pw, installed: f.installed }); expect(r.exe).toBe(f.installed.chromium!); }); test('nothing usable: a clear error naming what was looked for and the fix', async () => { const f = fakeCache('empty', ['chromium_headless_shell-1187']); const e = await launchPersistent(tmp, join(tmp, 'p3'), {}, { pw: f.pw, installed: f.installed }).catch(x => x as Error); expect((e as Error).message).toContain('no usable browser'); expect((e as Error).message).toContain('Google Chrome'); expect((e as Error).message).toContain('never the headless shell'); expect((e as Error).message).toContain('bunx playwright install chromium'); expect((e as Error).message).toContain('storebound doctor'); expect(f.calls).toEqual([]); }); test('an installed browser that fails reports its own error, not an earlier "Executable doesn\'t exist"', async () => { const f = fakeCache('chrome-fails', ['chromium_headless_shell-1187'], { chrome: true, fail: o => o.channel === 'chrome' ? new Error('Target page, context or browser has been closed') : null }); const e = await launchPersistent(tmp, join(tmp, 'p4'), {}, { pw: f.pw, installed: f.installed }).catch(x => x as Error); expect((e as Error).message).toMatch(/^Google Chrome .* did not start: Target page/); expect((e as Error).message).not.toContain("Executable doesn't exist"); }); }); describe('profile held by another browser', () => { test('a SingletonLock naming a live process is reported before any launch', async () => { const dir = join(tmp, 'locked'); mkdirSync(dir); symlinkSync(`${hostname()}-${process.pid}`, join(dir, 'SingletonLock')); expect(profileHolder(dir)).toBe(process.pid); const f = fakeCache('locked-cache', [], { chrome: true }); const e = await launchPersistent(tmp, dir, {}, { pw: f.pw, installed: f.installed }).catch(x => x as Error); expect((e as Error).message).toContain(`is still open in another browser (process ${process.pid})`); expect(f.calls).toEqual([]); }); test('a stale lock (dead process, other host) is ignored', () => { const dir = join(tmp, 'stale'); mkdirSync(dir); symlinkSync(`${hostname()}-999999`, join(dir, 'SingletonLock')); expect(profileHolder(dir, () => false)).toBeNull(); expect(profileHolder(dir, () => true, 'another-host')).toBeNull(); expect(profileHolder(join(tmp, 'no-such-dir'))).toBeNull(); }); }); describe('console fill', () => { test('the persistent launch gets executablePath = STOREBOUND_CHROME when it is set', async () => { const exe = join(tmp, 'Chrome for Testing'); writeFileSync(exe, ''); const before = process.env.STOREBOUND_CHROME; process.env.STOREBOUND_CHROME = exe; try { const f = fakeCache('sb-chrome', ['chromium_headless_shell-1187'], { chrome: true }); const b = await openConsoleBrowser(tmp, { store: 'apple', fresh: true, profileDir: join(tmp, 'sb-profile'), say: () => {}, launchDeps: { pw: f.pw } }); expect(b.mode).toBe('profile'); expect(f.calls).toHaveLength(1); expect(f.calls[0].executablePath).toBe(exe); expect(f.calls[0].channel).toBeUndefined(); expect(f.calls[0].args).toContain('--restore-last-session'); } finally { if (before === undefined) delete process.env.STOREBOUND_CHROME; else process.env.STOREBOUND_CHROME = before; } }); });