From 1210c9207cb7f66ef807ec44ffb41ad4c840e077 Mon Sep 17 00:00:00 2001 From: Etienne Lescot Date: Sun, 26 Jul 2026 19:17:28 +0200 Subject: [PATCH 1/2] fix(inspector): stop the collapse button swallowing every pane's Help click FloatingInspector floats its "Collapse inspector" button over the reused pane header (position: absolute, right: 12, 26px wide). That lands squarely on whatever the header puts at its right edge and eats its clicks. The Help button was the casualty: rendered on every facet, visible, and unreachable by mouse. CaptionsPane's status icon sits under it too. Fixed where all three headers share a definition rather than nudging the one button: .paneHead now reserves the floated control its own lane. Every pane wearing that class is rendered inside FloatingInspector, so there is no other caller to regress. Caught by the e2e rewrite in the following commit. --- src/components/ai-edition/NewEditorShell.module.css | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/components/ai-edition/NewEditorShell.module.css b/src/components/ai-edition/NewEditorShell.module.css index e396623b2..8ce46a1aa 100644 --- a/src/components/ai-edition/NewEditorShell.module.css +++ b/src/components/ai-edition/NewEditorShell.module.css @@ -846,6 +846,12 @@ align-items: center; gap: var(--sp-2); padding: 12px var(--sp-4); + /* Every pane wearing this header is rendered inside FloatingInspector, which + floats its "Collapse inspector" button (right: 12, 26px wide) over the + header's right edge — landing squarely on whatever sits there and + swallowing its clicks. The Help button was the casualty: visible on every + facet, and unclickable. Reserve the floated button its own lane instead. */ + padding-right: 46px; border-bottom: 1px solid var(--border-soft); min-height: 44px; } From a0387759e34979857dc0c43090e2812861e59b68 Mon Sep 17 00:00:00 2001 From: Etienne Lescot Date: Sun, 26 Jul 2026 19:18:17 +0200 Subject: [PATCH 2/2] test(e2e): retarget the editor specs at the v4 shell MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The three editor specs were written against the pre-v4 shell and could not pass: they waited on a `timeline-pane` testid that died with TimelinePane.tsx, read data-clip-count / data-zoom-multiplier / data-px-per-sec attributes the v4 timeline never had, and seeded the browser shim through the localStorage keys it stopped reading when it moved to `browser-shim-projects-v2`. Their fixtures were schemaVersion 3, so they exercised the v3→v4→v5 migration path rather than the shape the app actually stores. roadmap-coverage.spec.ts → v4-shell.spec.ts, renamed off the retired roadmap task ids, three behaviours kept and two dropped: - asset file size, now read off MediaStage's cards; the sized and unsized branches share one fixture instead of two page loads - the facet help popover, still live in RightPanes' Pane but reached from the inspector rail rather than a right-panel tab. Its "Settings for X" fallback assertion is gone with the fallback itself — every pane has real help text - ctrl+wheel zoom, which no longer has a px-per-sec ceiling; v4 zooms a nav window bounded at a 2% minimum span, so that is what the test measures Dropped: the ruler hover-guide, which does not exist in v4 (the only ruler overlay left is the pill-drag snap guide), and the chat "applied:" case, which asserted that the browser shim's canned reply carries no tool calls — the mock, not the app, and the positive half is unreachable without a real provider. seed.spec.ts → editor-smoke.spec.ts. Its timeline half was unsalvageable but its preview half still holds, and it seeds nothing, which makes it the only cover for the cold-boot branch (Preview → EditorEmptyState) and for a crash during mount against an empty shim. Now also listens for pageerror, not just console.error — an uncaught mount exception is the failure a boot smoke test exists to catch. diagnostic.spec.ts deleted. It dumped the DOM and a11y tree to find selectors in the old shell and ended in expect(true).toBe(true); the selectors it existed to discover are written down in v4-shell.spec.ts now. Both specs read E2E_BASE_URL, so tests/e2e no longer hardcodes a port. E2E_BASE_URL=http://localhost:5199 npx playwright test tests/e2e/v4-shell.spec.ts tests/e2e/editor-smoke.spec.ts → 4 passed --- tests/e2e/diagnostic.spec.ts | 209 ---------------- tests/e2e/editor-smoke.spec.ts | 39 +++ tests/e2e/roadmap-coverage.spec.ts | 367 ----------------------------- tests/e2e/seed.spec.ts | 34 --- tests/e2e/v4-shell.spec.ts | 151 ++++++++++++ 5 files changed, 190 insertions(+), 610 deletions(-) delete mode 100644 tests/e2e/diagnostic.spec.ts create mode 100644 tests/e2e/editor-smoke.spec.ts delete mode 100644 tests/e2e/roadmap-coverage.spec.ts delete mode 100644 tests/e2e/seed.spec.ts create mode 100644 tests/e2e/v4-shell.spec.ts diff --git a/tests/e2e/diagnostic.spec.ts b/tests/e2e/diagnostic.spec.ts deleted file mode 100644 index 9a879e4a5..000000000 --- a/tests/e2e/diagnostic.spec.ts +++ /dev/null @@ -1,209 +0,0 @@ -// Diagnostic: open the editor with a seeded project and dump the -// accessibility tree. Used to figure out the right selectors for -// chat input, help button, and timeline ruler — selectors I can't -// guess from the source alone. -import { expect, test } from "@playwright/test"; - -const EDITOR_URL = "http://localhost:5173/?windowType=editor"; - -const DOC = { - schemaVersion: 3, - project: { - id: "proj_diag", - title: "Diagnostic", - createdAt: "2026-07-01T00:00:00.000Z", - updatedAt: "2026-07-01T00:00:00.000Z", - }, - assets: [ - { - id: "asset_diag", - kind: "video", - label: "diagnostic.mp4", - originalPath: "C:\\nonexistent\\diagnostic.mp4", - durationSec: 600, - sizeBytes: 12_345_678, - video: { codec: "h264", width: 1920, height: 1080, fps: 30 }, - }, - ], - transcripts: [], - timeline: { - clips: [ - { - id: "clip_diag", - assetId: "asset_diag", - sourceStartSec: 0, - sourceEndSec: 600, - timelineStartSec: 0, - timelineEndSec: 600, - wordRefs: [], - origin: "system", - reason: "", - }, - ], - gaps: [], - trimRanges: [], - muteRanges: [], - speedRanges: [], - captionRanges: [], - }, - annotations: [], - zoomRanges: [ - { - id: "z1", - startMs: 5_000, - endMs: 15_000, - depth: 2, - focus: { cx: 0.5, cy: 0.5 }, - }, - { - id: "z2", - startMs: 30_000, - endMs: 45_000, - depth: 3, - focus: { cx: 0.5, cy: 0.5 }, - }, - ], - legacyEditor: null, - agent: { pendingQuestions: [], suggestions: [], lastAppliedOperations: [] }, - preview: { strategy: "seek", revision: 0 }, - export: { preset: "final-balanced", lastJobId: null }, - history: { revisions: [] }, -}; - -test("diagnostic: dump editor DOM + accessibility tree", async ({ page }) => { - await page.addInitScript((serialized) => { - const d = JSON.parse(serialized); - localStorage.setItem("browser-shim-document", serialized); - localStorage.setItem( - "browser-shim-projects", - JSON.stringify([ - { - id: d.project.id, - title: d.project.title, - updatedAt: d.project.updatedAt, - assetCount: d.assets.length, - }, - ]), - ); - }, JSON.stringify(DOC)); - await page.goto(EDITOR_URL, { waitUntil: "domcontentloaded" }); - // Hook all console output from the renderer. - page.on("console", (msg) => { - console.log(`[renderer:${msg.type()}] ${msg.text()}`); - }); - page.on("pageerror", (err) => { - console.log(`[renderer:pageerror] ${err.message}`); - }); - await page.getByTestId("timeline-pane").waitFor({ state: "visible" }); - // ponytail: wait for the mount useEffect to actually load the project - // (data-clip-count > 0), so the dump reflects a populated editor. - try { - await page.waitForFunction( - () => { - const el = document.querySelector('[data-testid="timeline-pane"]'); - const n = el?.getAttribute("data-clip-count"); - return n !== null && Number(n) > 0; - }, - undefined, - { timeout: 5_000 }, - ); - } catch { - console.log("[diag] clip-count never went > 0 — editor stayed in empty state"); - } - // Print the localStorage keys we ended up with, to verify the seed. - const storageKeys = await page.evaluate(() => { - return Object.keys(localStorage).map((k) => ({ - key: k, - preview: (localStorage.getItem(k) ?? "").slice(0, 80), - })); - }); - console.log(`[diag] localStorage keys: ${JSON.stringify(storageKeys)}`); - - // Probe the shim directly. We need to reach the project store's listProjects. - const shimResult = await page.evaluate(async () => { - try { - // The shim is installed on window.electronAPI; the project store - // goes through nativeBridgeClient which is also window-attached - // via the shim's module. Try both routes. - const win = window as unknown as { - electronAPI?: { getPlatform?: () => unknown }; - nativeBridgeClient?: { - aiEdition?: { - listProjects?: () => Promise; - get?: (id: string) => Promise; - }; - }; - }; - const out: Record = {}; - out.electronAPI = Object.keys(win.electronAPI ?? {}); - out.nativeBridgeClient = Object.keys(win.nativeBridgeClient ?? {}); - out.aiEdition = Object.keys(win.nativeBridgeClient?.aiEdition ?? {}); - try { - out.listProjectsResult = await win.nativeBridgeClient?.aiEdition?.listProjects?.(); - } catch (e) { - out.listProjectsError = String(e); - } - return out; - } catch (err) { - return { error: String(err) }; - } - }); - console.log(`[diag] shim probe: ${JSON.stringify(shimResult)}`); - await page.screenshot({ path: "test-results/diagnostic.png", fullPage: true }); - - // Buttons in the right pane header. - const helpButtons = await page.getByRole("button", { name: /help/i }).count(); - console.log(`[diag] help buttons found via getByRole('button', {name: /help/i}): ${helpButtons}`); - - const allButtons = await page.locator("button").count(); - console.log(`[diag] total