import electronBinaryPath from 'electron'; import { _electron as electron, expect, test as base, type ElectronApplication, type Page } from '@playwright/test'; import { mkdir, mkdtemp, rm } from 'node:fs/promises'; import { createServer } from 'node:net'; import { tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; type ElectronFixtures = { electronApp: ElectronApplication; page: Page; homeDir: string; userDataDir: string; launchElectronApp: () => Promise; }; const repoRoot = resolve(process.cwd()); const electronEntry = join(repoRoot, 'dist-electron/main/index.js'); async function allocatePort(): Promise { return await new Promise((resolvePort, reject) => { const server = createServer(); server.once('error', reject); server.listen(0, '127.0.0.1', () => { const address = server.address(); if (!address || typeof address === 'string') { server.close(() => reject(new Error('Failed to allocate an ephemeral port'))); return; } const { port } = address; server.close((error) => { if (error) { reject(error); return; } resolvePort(port); }); }); }); } async function launchClawXElectron(homeDir: string, userDataDir: string): Promise { const hostApiPort = await allocatePort(); const electronEnv = process.platform === 'linux' ? { ELECTRON_DISABLE_SANDBOX: '1' } : {}; return await electron.launch({ executablePath: electronBinaryPath, args: [electronEntry], env: { ...process.env, ...electronEnv, HOME: homeDir, USERPROFILE: homeDir, APPDATA: join(homeDir, 'AppData', 'Roaming'), LOCALAPPDATA: join(homeDir, 'AppData', 'Local'), XDG_CONFIG_HOME: join(homeDir, '.config'), CLAWX_E2E: '1', CLAWX_USER_DATA_DIR: userDataDir, CLAWX_PORT_CLAWX_HOST_API: String(hostApiPort), }, timeout: 90_000, }); } export const test = base.extend({ homeDir: async ({ browserName: _browserName }, provideHomeDir) => { const homeDir = await mkdtemp(join(tmpdir(), 'clawx-e2e-home-')); await mkdir(join(homeDir, '.config'), { recursive: true }); await mkdir(join(homeDir, 'AppData', 'Local'), { recursive: true }); await mkdir(join(homeDir, 'AppData', 'Roaming'), { recursive: true }); try { await provideHomeDir(homeDir); } finally { await rm(homeDir, { recursive: true, force: true }); } }, userDataDir: async ({ browserName: _browserName }, provideUserDataDir) => { const userDataDir = await mkdtemp(join(tmpdir(), 'clawx-e2e-user-data-')); try { await provideUserDataDir(userDataDir); } finally { await rm(userDataDir, { recursive: true, force: true }); } }, launchElectronApp: async ({ homeDir, userDataDir }, provideLauncher) => { await provideLauncher(async () => await launchClawXElectron(homeDir, userDataDir)); }, electronApp: async ({ launchElectronApp }, provideElectronApp) => { const app = await launchElectronApp(); let appClosed = false; app.once('close', () => { appClosed = true; }); try { await provideElectronApp(app); } finally { if (!appClosed) { await app.close().catch(() => {}); } } }, page: async ({ electronApp }, providePage) => { const page = await electronApp.firstWindow(); await page.waitForLoadState('domcontentloaded'); await providePage(page); }, }); export async function completeSetup(page: Page): Promise { await expect(page.getByTestId('setup-page')).toBeVisible(); await page.getByTestId('setup-skip-button').click(); await expect(page.getByTestId('main-layout')).toBeVisible(); } export { expect };