diff --git a/e2e/tests/deep-coverage.spec.ts b/e2e/tests/deep-coverage.spec.ts new file mode 100644 index 00000000..3a84e5c7 --- /dev/null +++ b/e2e/tests/deep-coverage.spec.ts @@ -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, 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); + }); +});