Files
ccs/tests/unit/utils/browser/chrome-reuse.test.ts
T

270 lines
9.4 KiB
TypeScript

import { afterEach, describe, expect, it } from 'bun:test';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import {
resolveConfiguredBrowserProfileDir,
resolveBrowserRuntimeEnv,
resolveDefaultChromeUserDataDir,
} from '../../../../src/utils/browser/chrome-reuse';
describe('chrome reuse resolver', () => {
const originalHome = process.env.HOME;
const originalLocalAppData = process.env.LOCALAPPDATA;
const originalUserProfile = process.env.USERPROFILE;
let tempDirs: string[] = [];
let servers: Array<{ stop: () => void }> = [];
function createTempDir(prefix: string): string {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
tempDirs.push(dir);
return dir;
}
function writeDevToolsActivePort(profileDir: string, content: string): void {
fs.mkdirSync(profileDir, { recursive: true });
fs.writeFileSync(path.join(profileDir, 'DevToolsActivePort'), content, 'utf8');
}
async function startDevToolsServer(versionPayload: Record<string, unknown>) {
const server = Bun.serve({
port: 0,
fetch(request: Request) {
if (new URL(request.url).pathname === '/json/version') {
return Response.json(versionPayload);
}
return new Response('not found', { status: 404 });
},
});
servers.push(server);
return server;
}
async function startFailingDevToolsServer(status: number, body = 'error') {
const server = Bun.serve({
port: 0,
fetch(request: Request) {
if (new URL(request.url).pathname === '/json/version') {
return new Response(body, { status });
}
return new Response('not found', { status: 404 });
},
});
servers.push(server);
return server;
}
async function startMalformedJsonDevToolsServer() {
const server = Bun.serve({
port: 0,
fetch(request: Request) {
if (new URL(request.url).pathname === '/json/version') {
return new Response('{invalid-json', {
status: 200,
headers: { 'content-type': 'application/json' },
});
}
return new Response('not found', { status: 404 });
},
});
servers.push(server);
return server;
}
async function reserveClosedPort(): Promise<number> {
const server = Bun.serve({
port: 0,
fetch() {
return new Response('ok');
},
});
const port = server.port;
server.stop(true);
return port;
}
afterEach(() => {
for (const server of servers) {
server.stop(true);
}
servers = [];
for (const dir of tempDirs) {
fs.rmSync(dir, { recursive: true, force: true });
}
tempDirs = [];
if (originalLocalAppData === undefined) {
delete process.env.LOCALAPPDATA;
} else {
process.env.LOCALAPPDATA = originalLocalAppData;
}
if (originalHome === undefined) {
delete process.env.HOME;
} else {
process.env.HOME = originalHome;
}
if (originalUserProfile === undefined) {
delete process.env.USERPROFILE;
} else {
process.env.USERPROFILE = originalUserProfile;
}
});
it('uses explicit profile-dir before the default path and resolves the websocket target', async () => {
const explicitProfileDir = createTempDir('ccs-chrome-explicit-');
const defaultProfileDir = createTempDir('ccs-chrome-default-');
const server = await startDevToolsServer({
Browser: 'Chrome/136.0.0.0',
webSocketDebuggerUrl: 'ws://127.0.0.1/devtools/browser/target-1',
});
writeDevToolsActivePort(explicitProfileDir, `${server.port}\n/devtools/browser/from-explicit`);
const runtimeEnv = await resolveBrowserRuntimeEnv({
profileDir: explicitProfileDir,
});
expect(runtimeEnv).toEqual({
CCS_BROWSER_USER_DATA_DIR: explicitProfileDir,
CCS_BROWSER_DEVTOOLS_HOST: '127.0.0.1',
CCS_BROWSER_DEVTOOLS_PORT: String(server.port),
CCS_BROWSER_DEVTOOLS_HTTP_URL: `http://127.0.0.1:${server.port}`,
CCS_BROWSER_DEVTOOLS_WS_URL: 'ws://127.0.0.1/devtools/browser/target-1',
});
expect(fs.existsSync(path.join(defaultProfileDir, 'DevToolsActivePort'))).toBe(false);
});
it('only enables browser reuse for an explicitly configured profile directory', () => {
const isolatedHome = createTempDir('ccs-chrome-config-home-');
const defaultProfileDir = path.join(
isolatedHome,
'Library',
'Application Support',
'Google',
'Chrome'
);
writeDevToolsActivePort(defaultProfileDir, '9222\n/devtools/browser/stale-default');
process.env.HOME = isolatedHome;
process.env.USERPROFILE = isolatedHome;
expect(resolveConfiguredBrowserProfileDir()).toBeUndefined();
expect(resolveConfiguredBrowserProfileDir(defaultProfileDir)).toBe(defaultProfileDir);
});
it('uses an explicit devtools port override when metadata is missing', async () => {
const profileDir = createTempDir('ccs-chrome-explicit-port-');
const server = await startDevToolsServer({
Browser: 'Chrome/136.0.0.0',
webSocketDebuggerUrl: 'ws://127.0.0.1/devtools/browser/explicit-port',
});
const runtimeEnv = await resolveBrowserRuntimeEnv({
profileDir,
devtoolsPort: String(server.port),
});
expect(runtimeEnv.CCS_BROWSER_DEVTOOLS_PORT).toBe(String(server.port));
expect(runtimeEnv.CCS_BROWSER_DEVTOOLS_WS_URL).toBe(
'ws://127.0.0.1/devtools/browser/explicit-port'
);
});
it('throws a clear error when DevToolsActivePort metadata is missing', async () => {
const profileDir = createTempDir('ccs-chrome-missing-metadata-');
await expect(resolveBrowserRuntimeEnv({ profileDir })).rejects.toThrow(
`Chrome reuse metadata not found: ${path.join(profileDir, 'DevToolsActivePort')}`
);
});
it('throws a clear error when DevToolsActivePort metadata is invalid', async () => {
const profileDir = createTempDir('ccs-chrome-invalid-metadata-');
writeDevToolsActivePort(profileDir, 'not-a-port\n/devtools/browser/target');
await expect(resolveBrowserRuntimeEnv({ profileDir })).rejects.toThrow(
`Chrome reuse metadata is invalid: ${path.join(profileDir, 'DevToolsActivePort')}`
);
});
it('throws before launch fallback when the DevTools endpoint is stale or unreachable', async () => {
const profileDir = createTempDir('ccs-chrome-unreachable-');
const port = await reserveClosedPort();
writeDevToolsActivePort(profileDir, `${port}\n/devtools/browser/stale`);
await expect(resolveBrowserRuntimeEnv({ profileDir })).rejects.toThrow(
`Chrome DevTools endpoint is unreachable: http://127.0.0.1:${port}`
);
});
it('throws a clear error when the DevTools endpoint is reachable without a websocket target', async () => {
const profileDir = createTempDir('ccs-chrome-missing-ws-');
const server = await startDevToolsServer({ Browser: 'Chrome/136.0.0.0' });
writeDevToolsActivePort(profileDir, `${server.port}\n/devtools/browser/no-ws`);
await expect(resolveBrowserRuntimeEnv({ profileDir })).rejects.toThrow(
`Chrome DevTools endpoint did not provide a websocket target: http://127.0.0.1:${server.port}/json/version`
);
});
it('throws a clear error when the DevTools endpoint returns a non-200 response', async () => {
const profileDir = createTempDir('ccs-chrome-bad-status-');
const server = await startFailingDevToolsServer(500);
writeDevToolsActivePort(profileDir, `${server.port}\n/devtools/browser/bad-status`);
await expect(resolveBrowserRuntimeEnv({ profileDir })).rejects.toThrow(
`Chrome DevTools endpoint is unreachable: http://127.0.0.1:${server.port}`
);
});
it('throws a clear error when the DevTools endpoint returns malformed JSON', async () => {
const profileDir = createTempDir('ccs-chrome-bad-json-');
const server = await startMalformedJsonDevToolsServer();
writeDevToolsActivePort(profileDir, `${server.port}\n/devtools/browser/bad-json`);
await expect(resolveBrowserRuntimeEnv({ profileDir })).rejects.toThrow(
`Chrome DevTools endpoint is unreachable: http://127.0.0.1:${server.port}`
);
});
it('resolves platform default Chrome user-data-dir paths', () => {
const isolatedHome = createTempDir('ccs-chrome-home-');
const env = {
HOME: isolatedHome,
USERPROFILE: isolatedHome,
LOCALAPPDATA: 'C:/Users/test/AppData/Local',
};
expect(resolveDefaultChromeUserDataDir('darwin', env)).toBe(
path.join(isolatedHome, 'Library', 'Application Support', 'Google', 'Chrome')
);
expect(resolveDefaultChromeUserDataDir('linux', env)).toBe(
path.join(isolatedHome, '.config', 'google-chrome')
);
expect(resolveDefaultChromeUserDataDir('win32', env)).toBe(
path.normalize('C:/Users/test/AppData/Local/Google/Chrome/User Data')
);
});
it('throws on win32 when LOCALAPPDATA is missing', () => {
delete process.env.LOCALAPPDATA;
expect(() => resolveDefaultChromeUserDataDir('win32')).toThrow(
'LOCALAPPDATA is required to resolve the default Chrome user-data-dir on Windows'
);
});
it('throws a clear error when the resolved profile directory does not exist', async () => {
const missingProfileDir = path.join(
createTempDir('ccs-chrome-missing-dir-'),
'missing-profile'
);
await expect(resolveBrowserRuntimeEnv({ profileDir: missingProfileDir })).rejects.toThrow(
`Chrome profile directory is invalid: ${missingProfileDir}`
);
});
});