mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-15 22:21:20 +00:00
270 lines
9.4 KiB
TypeScript
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}`
|
|
);
|
|
});
|
|
});
|