Preserve stable snapshots and stabilize Electron e2e (#734)

This commit is contained in:
Lingxuan Zuo
2026-04-01 20:35:01 +08:00
committed by GitHub
Unverified
parent 34bbb039d3
commit 5a3da41562
21 changed files with 758 additions and 78 deletions

View File

@@ -5,12 +5,16 @@ import { createServer } from 'node:net';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
type LaunchElectronOptions = {
skipSetup?: boolean;
};
type ElectronFixtures = {
electronApp: ElectronApplication;
page: Page;
homeDir: string;
userDataDir: string;
launchElectronApp: () => Promise<ElectronApplication>;
launchElectronApp: (options?: LaunchElectronOptions) => Promise<ElectronApplication>;
};
const repoRoot = resolve(process.cwd());
@@ -38,7 +42,77 @@ async function allocatePort(): Promise<number> {
});
}
async function launchClawXElectron(homeDir: string, userDataDir: string): Promise<ElectronApplication> {
async function getStableWindow(app: ElectronApplication): Promise<Page> {
const deadline = Date.now() + 30_000;
let page = await app.firstWindow();
while (Date.now() < deadline) {
const openWindows = app.windows().filter((candidate) => !candidate.isClosed());
const currentWindow = openWindows.at(-1) ?? page;
if (currentWindow && !currentWindow.isClosed()) {
try {
await currentWindow.waitForLoadState('domcontentloaded', { timeout: 2_000 });
return currentWindow;
} catch (error) {
if (!String(error).includes('has been closed')) {
throw error;
}
}
}
try {
page = await app.waitForEvent('window', { timeout: 2_000 });
} catch {
// Keep polling until a stable window is available or the deadline expires.
}
}
throw new Error('No stable Electron window became available');
}
async function closeElectronApp(app: ElectronApplication, timeoutMs = 5_000): Promise<void> {
let closed = false;
await Promise.race([
(async () => {
const [closeResult] = await Promise.allSettled([
app.waitForEvent('close', { timeout: timeoutMs }),
app.evaluate(({ app: electronApp }) => {
electronApp.quit();
}),
]);
if (closeResult.status === 'fulfilled') {
closed = true;
}
})(),
new Promise((resolve) => setTimeout(resolve, timeoutMs)),
]);
if (closed) {
return;
}
try {
await app.close();
return;
} catch {
// Fall through to process kill if Playwright cannot close the app cleanly.
}
try {
app.process().kill('SIGKILL');
} catch {
// Ignore process kill failures during e2e teardown.
}
}
async function launchClawXElectron(
homeDir: string,
userDataDir: string,
options: LaunchElectronOptions = {},
): Promise<ElectronApplication> {
const hostApiPort = await allocatePort();
const electronEnv = process.platform === 'linux'
? { ELECTRON_DISABLE_SANDBOX: '1' }
@@ -56,6 +130,7 @@ async function launchClawXElectron(homeDir: string, userDataDir: string): Promis
XDG_CONFIG_HOME: join(homeDir, '.config'),
CLAWX_E2E: '1',
CLAWX_USER_DATA_DIR: userDataDir,
...(options.skipSetup ? { CLAWX_E2E_SKIP_SETUP: '1' } : {}),
CLAWX_PORT_CLAWX_HOST_API: String(hostApiPort),
},
timeout: 90_000,
@@ -85,7 +160,7 @@ export const test = base.extend<ElectronFixtures>({
},
launchElectronApp: async ({ homeDir, userDataDir }, provideLauncher) => {
await provideLauncher(async () => await launchClawXElectron(homeDir, userDataDir));
await provideLauncher(async (options?: LaunchElectronOptions) => await launchClawXElectron(homeDir, userDataDir, options));
},
electronApp: async ({ launchElectronApp }, provideElectronApp) => {
@@ -99,14 +174,13 @@ export const test = base.extend<ElectronFixtures>({
await provideElectronApp(app);
} finally {
if (!appClosed) {
await app.close().catch(() => {});
await closeElectronApp(app);
}
}
},
page: async ({ electronApp }, providePage) => {
const page = await electronApp.firstWindow();
await page.waitForLoadState('domcontentloaded');
const page = await getStableWindow(electronApp);
await providePage(page);
},
});
@@ -117,4 +191,6 @@ export async function completeSetup(page: Page): Promise<void> {
await expect(page.getByTestId('main-layout')).toBeVisible();
}
export { closeElectronApp };
export { getStableWindow };
export { expect };