mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-31 20:29:56 +02:00
[eric] test: add a deep-coverage e2e spec that drives every reachable user surface on the packaged app and asserts no renderer crashes per step
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
import { test, expect, ElectronApplication, Page } from '@playwright/test';
|
||||
import { launchApp, waitForMainWindow } from '../helpers/launch';
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
// Deep interactive coverage: drives every reachable user-facing surface on the
|
||||
// packaged app and asserts no renderer crashes per step. Runs on every CI push
|
||||
// against the matrix (windows-latest + macos-13 + macos-14). Replaces the "I
|
||||
// physically click everything" manual gap with a hermetic automated one that
|
||||
// has no foreground-lock contention because CI runners have no competing app.
|
||||
|
||||
function backendLogPath(): string {
|
||||
if (process.platform === 'win32') return path.join(process.env.APPDATA || '', 'OpenSwarm', 'data', 'backend.log');
|
||||
if (process.platform === 'darwin') return path.join(os.homedir(), 'Library', 'Application Support', 'OpenSwarm', 'data', 'backend.log');
|
||||
return path.join(process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share'), 'OpenSwarm', 'data', 'backend.log');
|
||||
}
|
||||
|
||||
function crashCount(): number {
|
||||
try { return (fs.readFileSync(backendLogPath(), 'utf8').match(/renderer process gone/g) || []).length; }
|
||||
catch { return 0; }
|
||||
}
|
||||
|
||||
test.describe.configure({ mode: 'serial' });
|
||||
test.describe('deep interactive coverage', () => {
|
||||
let app: ElectronApplication;
|
||||
let page: Page;
|
||||
let baseline = 0;
|
||||
|
||||
const noNewCrashes = (label: string) => {
|
||||
const now = crashCount();
|
||||
expect(now, `renderer crashed during: ${label}`).toBe(baseline);
|
||||
};
|
||||
|
||||
const safeClick = async (locator: ReturnType<Page['getByText']>, label: string) => {
|
||||
if ((await locator.count()) === 0) { test.info().annotations.push({ type: 'skip', description: `${label}: target not visible` }); return false; }
|
||||
await locator.first().click({ timeout: 5000 }).catch((e) => { test.info().annotations.push({ type: 'click-fail', description: `${label}: ${e.message}` }); });
|
||||
return true;
|
||||
};
|
||||
|
||||
test.beforeAll(async () => {
|
||||
app = await launchApp();
|
||||
page = await waitForMainWindow(app);
|
||||
baseline = crashCount();
|
||||
});
|
||||
|
||||
test.afterAll(async () => { await app?.close().catch(() => {}); });
|
||||
|
||||
test('home renders without crashing', async ({}, info) => {
|
||||
await page.screenshot({ path: info.outputPath('home.png') });
|
||||
noNewCrashes('home render');
|
||||
});
|
||||
|
||||
test('onboarding panel opens on Continue', async ({}, info) => {
|
||||
await safeClick(page.getByText(/^Continue/), 'Continue');
|
||||
await page.waitForTimeout(2000);
|
||||
await page.screenshot({ path: info.outputPath('onboarding-step1.png') });
|
||||
noNewCrashes('onboarding step 1 mount');
|
||||
});
|
||||
|
||||
test('roadmap (See all todos) renders all 8 steps', async ({}, info) => {
|
||||
await safeClick(page.getByText('See all todos'), 'See all todos');
|
||||
await page.waitForTimeout(1500);
|
||||
await page.screenshot({ path: info.outputPath('onboarding-roadmap.png') });
|
||||
noNewCrashes('onboarding roadmap mount');
|
||||
await page.keyboard.press('Escape').catch(() => {});
|
||||
await page.waitForTimeout(500);
|
||||
});
|
||||
|
||||
test('Settings opens and every tab renders', async ({}, info) => {
|
||||
await safeClick(page.getByText('Settings', { exact: true }), 'Settings nav');
|
||||
await page.waitForTimeout(1500);
|
||||
for (const tab of ['General', 'Models', 'Usage', 'Commands']) {
|
||||
const t = page.getByRole('tab', { name: tab }).first();
|
||||
if (await t.count()) {
|
||||
await t.click({ timeout: 3000 }).catch(() => {});
|
||||
await page.waitForTimeout(900);
|
||||
await page.screenshot({ path: info.outputPath(`settings-${tab.toLowerCase()}.png`) });
|
||||
noNewCrashes(`Settings ${tab} tab`);
|
||||
}
|
||||
}
|
||||
await page.getByText('Close', { exact: true }).first().click({ timeout: 2000 }).catch(() => page.keyboard.press('Escape'));
|
||||
await page.waitForTimeout(700);
|
||||
});
|
||||
|
||||
test('Settings toggles flip + revert (effect verified)', async ({}, info) => {
|
||||
await safeClick(page.getByText('Settings', { exact: true }), 'Settings nav for toggles');
|
||||
await page.waitForTimeout(1500);
|
||||
const toggles = page.locator('input[type="checkbox"], [role="switch"]');
|
||||
const n = Math.min(await toggles.count(), 5);
|
||||
for (let i = 0; i < n; i++) {
|
||||
const t = toggles.nth(i);
|
||||
const before = await t.isChecked().catch(() => null);
|
||||
await t.click({ timeout: 2000 }).catch(() => {});
|
||||
await page.waitForTimeout(400);
|
||||
const after = await t.isChecked().catch(() => null);
|
||||
if (before !== null && after !== null) expect(after, `toggle #${i} did not flip`).not.toBe(before);
|
||||
await t.click({ timeout: 2000 }).catch(() => {}); // revert
|
||||
await page.waitForTimeout(300);
|
||||
noNewCrashes(`toggle ${i} flip+revert`);
|
||||
}
|
||||
await page.screenshot({ path: info.outputPath('settings-toggles.png') });
|
||||
await page.getByText('Close', { exact: true }).first().click({ timeout: 2000 }).catch(() => page.keyboard.press('Escape'));
|
||||
await page.waitForTimeout(700);
|
||||
});
|
||||
|
||||
test('Customization: Skills / Actions / Modes render', async ({}, info) => {
|
||||
for (const screen of ['Skills', 'Actions', 'Modes']) {
|
||||
await safeClick(page.getByText(screen, { exact: true }), screen);
|
||||
await page.waitForTimeout(1500);
|
||||
await page.screenshot({ path: info.outputPath(`${screen.toLowerCase()}.png`) });
|
||||
noNewCrashes(screen);
|
||||
}
|
||||
});
|
||||
|
||||
test('Modes editor (RichPromptEditor) opens without TSF crash', async ({}, info) => {
|
||||
await safeClick(page.getByText('Modes', { exact: true }), 'Modes');
|
||||
await page.waitForTimeout(1200);
|
||||
const editIcons = page.locator('[aria-label="Edit"], [aria-label*="edit mode" i]');
|
||||
if (await editIcons.count()) {
|
||||
await editIcons.first().click({ timeout: 3000 }).catch(() => {});
|
||||
await page.waitForTimeout(2000);
|
||||
await page.screenshot({ path: info.outputPath('mode-editor.png') });
|
||||
noNewCrashes('RichPromptEditor mount');
|
||||
await page.keyboard.press('Escape').catch(() => {});
|
||||
await page.waitForTimeout(600);
|
||||
}
|
||||
});
|
||||
|
||||
test('Dashboard canvas opens', async ({}, info) => {
|
||||
await safeClick(page.getByText('Getting Started', { exact: true }), 'Getting Started dashboard');
|
||||
await page.waitForTimeout(2000);
|
||||
await page.screenshot({ path: info.outputPath('dashboard-canvas.png') });
|
||||
noNewCrashes('dashboard canvas open');
|
||||
});
|
||||
|
||||
test('New Agent compose box opens (EditorSurface contentEditable mount)', async ({}, info) => {
|
||||
await safeClick(page.getByRole('button', { name: 'New Agent' }) as any, 'New Agent');
|
||||
await page.waitForTimeout(2500);
|
||||
await page.screenshot({ path: info.outputPath('new-agent-compose.png') });
|
||||
noNewCrashes('New Agent compose mount');
|
||||
await page.keyboard.press('Escape').catch(() => {});
|
||||
await page.waitForTimeout(500);
|
||||
});
|
||||
|
||||
test('Browser card mounts (webview path)', async ({}, info) => {
|
||||
await safeClick(page.getByRole('button', { name: 'Browser' }) as any, 'Browser');
|
||||
await page.waitForTimeout(3000);
|
||||
await page.screenshot({ path: info.outputPath('browser-card.png') });
|
||||
noNewCrashes('Browser card mount (webview)');
|
||||
});
|
||||
|
||||
test('History panel opens', async ({}, info) => {
|
||||
await safeClick(page.getByRole('button', { name: 'History' }) as any, 'History');
|
||||
await page.waitForTimeout(1500);
|
||||
await page.screenshot({ path: info.outputPath('history.png') });
|
||||
noNewCrashes('History panel mount');
|
||||
await page.keyboard.press('Escape').catch(() => {});
|
||||
await page.waitForTimeout(500);
|
||||
});
|
||||
|
||||
test('Add note mounts (sticky)', async ({}, info) => {
|
||||
await safeClick(page.getByRole('button', { name: 'Add note' }) as any, 'Add note');
|
||||
await page.waitForTimeout(1500);
|
||||
await page.screenshot({ path: info.outputPath('note.png') });
|
||||
noNewCrashes('Add note mount');
|
||||
});
|
||||
|
||||
test('Add App picker opens', async ({}, info) => {
|
||||
await safeClick(page.getByRole('button', { name: 'Add App' }) as any, 'Add App');
|
||||
await page.waitForTimeout(1500);
|
||||
await page.screenshot({ path: info.outputPath('add-app-picker.png') });
|
||||
noNewCrashes('Add App picker mount');
|
||||
await page.keyboard.press('Escape').catch(() => {});
|
||||
await page.waitForTimeout(500);
|
||||
});
|
||||
|
||||
test('zero new renderer-gone-lines across the entire walkthrough', () => {
|
||||
expect(crashCount(), 'one or more surfaces crashed the renderer; check earlier test annotations').toBe(baseline);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user