`console fill --fresh-profile` failed with "Executable doesn't exist ... chromium-1187" when only Playwright's headless shell was downloaded, although Google Chrome was installed (also with STOREBOUND_CHROME set). The Chrome launch did run, but failed because an earlier storebound run still held the profile, and the fallback rethrew the FIRST error. Every launch (renders, captures, console profile and copy modes, the desktop sidecar) now goes through one resolver that checks the exact executable before trying it: - headless: STOREBOUND_CHROME, headless shell, full Chromium, Chrome - visible / persistent: STOREBOUND_CHROME, Google Chrome, full Chromium (channel "chromium"); never the headless shell - copy mode: Google Chrome only A missing browser is skipped; an installed one that fails reports its own error; nothing usable names what was looked for plus the doctor / playwright install hint. A profile whose SingletonLock names a live process is reported before launching.
143 lines
8.6 KiB
TypeScript
143 lines
8.6 KiB
TypeScript
// 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-<rev> in the
|
|
// cache, `console fill --fresh-profile` reported "Executable doesn't exist ... chromium-<rev>" 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;
|
|
}
|
|
});
|
|
});
|