Google binds its session cookies to the device (Device Bound Session Credentials) and signs out sessions whose cookies look stolen, which can sign the owner out of their real Chrome. The default is now storebound's own profile per store; storebound starts Chrome itself (--user-data-dir, --remote-debugging-port=0, no --enable-automation, navigator.webdriver false) and attaches over CDP. --use-chrome-session google|apple is an explicit opt-in that warns. /signin/rejected is said plainly.
141 lines
9.1 KiB
TypeScript
141 lines
9.1 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, persistentExecutable, 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 window starts STOREBOUND_CHROME first, then Google Chrome, then full Chromium; the copy only Google Chrome', () => {
|
|
const i = { storeboundChrome: '/x/Chrome for Testing', chrome: CHROME, chromium: '/c' };
|
|
expect(persistentExecutable('visible', i).path).toBe('/x/Chrome for Testing');
|
|
expect(persistentExecutable('visible', { ...i, storeboundChrome: null }).path).toBe(CHROME);
|
|
expect(persistentExecutable('visible', { storeboundChrome: null, chrome: null, chromium: '/c' }).path).toBe('/c');
|
|
expect(persistentExecutable('chrome', i).path).toBe(CHROME);
|
|
expect(() => persistentExecutable('chrome', { ...i, chrome: null })).toThrow('Install Google Chrome.');
|
|
expect(() => persistentExecutable('visible', { storeboundChrome: null, chrome: null, chromium: null }, { STOREBOUND_CHROME: '/gone' })).toThrow('STOREBOUND_CHROME /gone: no such file');
|
|
});
|
|
test('openConsoleBrowser with nothing installed: the resolver error, nothing launched', async () => {
|
|
const f = fakeCache('none', []);
|
|
const e = await openConsoleBrowser(tmp, { store: 'apple', fresh: true, profileDir: join(tmp, 'sb-profile'), say: () => {}, launchDeps: { pw: f.pw, installed: { storeboundChrome: null, chrome: null, chromium: null } } }).catch(x => x as Error);
|
|
expect((e as Error).message).toContain('no usable browser');
|
|
expect(f.calls).toEqual([]);
|
|
});
|
|
});
|