From 2c1b0d409b260d4478562a626d7792cf88eac2aa Mon Sep 17 00:00:00 2001 From: Viktor Vaczi Date: Fri, 12 Jun 2026 04:58:52 +0200 Subject: [PATCH] feat(wasm-dom): appearance-panel e2e spec + wheel propagation; visual notes New tests/kicad/appearance.spec.ts: tab cycle through Layers/Objects/ Nets and back (with blank-rows regression assertion), wheel scrolling in Layers (movement + exact scroll-back restoration) and Objects, with screenshots per state. All 3 pass against the DOM pcbnew. Co-Authored-By: Claude Fable 5 --- docs/features/wx-dom-port/visual-notes.md | 22 +++ tests/kicad/appearance.spec.ts | 182 ++++++++++++++++++++++ wxwidgets | 2 +- 3 files changed, 205 insertions(+), 1 deletion(-) create mode 100644 tests/kicad/appearance.spec.ts diff --git a/docs/features/wx-dom-port/visual-notes.md b/docs/features/wx-dom-port/visual-notes.md index f105b0f..f8a3dd4 100644 --- a/docs/features/wx-dom-port/visual-notes.md +++ b/docs/features/wx-dom-port/visual-notes.md @@ -162,3 +162,25 @@ slider/statline/statbox wired: - **Known cosmetic gap:** DOM menubar/toolbar styling differs from univ pixels by design (native buttons, system font); the kicad reference regions therefore use per-port reference images. + +## Appearance-panel workstreams (2026-06-12) + +User-reported pcbnew issues, all fixed and e2e-pinned +(tests/kicad/appearance.spec.ts, screenshots appearance-*.png): + 20. DOM elements never clipped to ancestor viewports → layer rows + rendered past the pane. UpdateDomGeometry now pushes + clip-path:inset() from the accumulated ancestor client rects. + 21. wxWindowWasm::ScrollWindow never moved children (univ does) → + scrolled panes could not scroll at all. + 22. Mouse events over DOM-backed children never reached the wx pipeline + (listeners only on #canvas) → no hover/wheel/right-click over + labels/sliders. Document-level forwarding into wx_dom_mouse. + 23. Wheel events died at the deepest window: wx mouse events don't + self-propagate and native ports rely on platform routing. + HandleMouseWheelEvent walks up to the TLW. + 24. Generic (Motif-era) notebook replaced by a DOM-native wxNotebook: + real side-by-side tab buttons (ellipsis, no label overflow), + reliable switching, native look; registry 'tab' contract kept. + 25. Universal tooltip layer (#wx-tooltip div, 600 ms, hover hit-test + driven) — island widgets (color swatches, visibility toggles) get + KiCad's SetToolTip texts; title attrs replaced by aria-label. diff --git a/tests/kicad/appearance.spec.ts b/tests/kicad/appearance.spec.ts new file mode 100644 index 0000000..99467ee --- /dev/null +++ b/tests/kicad/appearance.spec.ts @@ -0,0 +1,182 @@ +// Appearance panel (right side of pcbnew): Layers/Objects/Nets notebook. +// Regression coverage for the DOM-port fixes: native tab switching (all +// three tabs and back), wheel scrolling inside the scrolled pages with +// viewport clipping, and rows surviving tab round-trips. Screenshots are +// written to test-results/appearance-*.png for visual review. + +import { test, expect, Page } from '@playwright/test'; +import { clickByLabel } from '../e2e/utils/element-tracker'; + +declare global { + interface Window { + wxElementRegistry: any; + wxDomPort?: boolean; + wxDomControls?: Map; + } +} + +async function completeWizard(page: Page): Promise { + await expect(page.locator('#canvas')).toBeVisible({ timeout: 90000 }); + await page.waitForFunction(() => !!window.wxElementRegistry, null, { timeout: 90000 }); + await page.waitForTimeout(2000); + + for (let i = 1; i <= 10; i++) { + let clicked = await clickByLabel(page, 'Next >'); + + if (!clicked) { + await clickByLabel(page, 'Finish'); + break; + } + + await page.waitForTimeout(500); + } + + // let the main frame settle + await page.waitForTimeout(4000); +} + +type Tab = { label: string; subType: string; centerX: number; centerY: number }; + +// The appearance notebook's tabs: rendered 'tab' elements with real +// coordinates (other, hidden notebooks register at 0,0). +async function appearanceTabs(page: Page): Promise { + return page.evaluate(() => { + const out: any[] = []; + window.wxElementRegistry.renderedElements.forEach((e: any) => { + if (e.elementType === 'tab' && e.centerX > 0 && + ['Layers', 'Objects', 'Nets'].includes(e.label)) { + out.push({ label: e.label, subType: e.subType, + centerX: e.centerX, centerY: e.centerY }); + } + }); + return out; + }); +} + +async function selectTab(page: Page, label: string): Promise { + const tabs = await appearanceTabs(page); + const tab = tabs.find(t => t.label === label); + expect(tab, `appearance tab ${label}`).toBeTruthy(); + await page.mouse.click(tab!.centerX, tab!.centerY); + + await expect.poll(async () => { + const after = await appearanceTabs(page); + return after.find(t => t.label === label)?.subType; + }, { timeout: 5000, intervals: [200] }).toBe('selected'); + + await page.waitForTimeout(400); +} + +// DOM port only: viewport rects of row labels inside the appearance pane +// (spans are real elements there; the canvas port draws them as pixels). +async function rowLabelTops(page: Page, labels: string[]): Promise> { + return page.evaluate((wanted: string[]) => { + const out: Record = {}; + for (const w of wanted) out[w] = null; + if (!window.wxDomControls) return out; + for (const [, el] of window.wxDomControls) { + if (el.tagName === 'SPAN' && wanted.includes(el.textContent || '')) { + out[el.textContent as string] = Math.round(el.getBoundingClientRect().top); + } + } + return out; + }, labels); +} + +test.describe('Appearance panel (Layers/Objects/Nets)', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/kicad/pcbnew.html'); + }); + + test('tabs switch through all three pages and back', async ({ page }) => { + await completeWizard(page); + + const tabs = await appearanceTabs(page); + expect(tabs.map(t => t.label).sort()).toEqual(['Layers', 'Nets', 'Objects']); + expect(tabs.find(t => t.label === 'Layers')?.subType).toBe('selected'); + + await page.screenshot({ path: 'test-results/appearance-00-layers.png' }); + + await selectTab(page, 'Objects'); + await page.screenshot({ path: 'test-results/appearance-01-objects.png' }); + + await selectTab(page, 'Nets'); + await page.screenshot({ path: 'test-results/appearance-02-nets.png' }); + + // and back to the start + await selectTab(page, 'Layers'); + await page.screenshot({ path: 'test-results/appearance-03-layers-again.png' }); + + // DOM port: layer rows must survive the tab round-trip (regression: + // pages came back blank after switching away and back) + if (await page.evaluate(() => !!window.wxDomPort)) { + const tops = await rowLabelTops(page, ['F.Cu', 'B.Cu']); + expect(tops['F.Cu'], 'F.Cu row visible after tab round-trip').not.toBeNull(); + expect(tops['B.Cu'], 'B.Cu row visible after tab round-trip').not.toBeNull(); + } + }); + + test('layer list scrolls with the wheel and clips at the pane', async ({ page }) => { + await completeWizard(page); + + const tabs = await appearanceTabs(page); + const layersTab = tabs.find(t => t.label === 'Layers'); + expect(layersTab).toBeTruthy(); + + const isDom = await page.evaluate(() => !!window.wxDomPort); + + // hover INSIDE the layer list (just below the tab strip) + const hoverX = layersTab!.centerX; + const hoverY = layersTab!.centerY + 120; + await page.mouse.move(hoverX, hoverY); + + const before = isDom ? await rowLabelTops(page, ['B.Cu', 'F.Mask']) : null; + + await page.mouse.wheel(0, 240); + await page.waitForTimeout(800); + await page.screenshot({ path: 'test-results/appearance-10-layers-scrolled.png' }); + + if (isDom && before) { + const after = await rowLabelTops(page, ['B.Cu', 'F.Mask']); + expect(after['B.Cu'], 'B.Cu moved up after wheel scroll') + .toBeLessThan(before['B.Cu']!); + expect(after['F.Mask'], 'F.Mask moved up after wheel scroll') + .toBeLessThan(before['F.Mask']!); + } + + // scroll back up restores the start of the list + await page.mouse.wheel(0, -480); + await page.waitForTimeout(800); + await page.screenshot({ path: 'test-results/appearance-11-layers-scrolled-back.png' }); + + if (isDom && before) { + const restored = await rowLabelTops(page, ['B.Cu']); + expect(restored['B.Cu'], 'B.Cu back at its original position') + .toBe(before['B.Cu']); + } + }); + + test('objects page scrolls with the wheel', async ({ page }) => { + await completeWizard(page); + + await selectTab(page, 'Objects'); + + const tabs = await appearanceTabs(page); + const objectsTab = tabs.find(t => t.label === 'Objects')!; + const isDom = await page.evaluate(() => !!window.wxDomPort); + + await page.mouse.move(objectsTab.centerX, objectsTab.centerY + 120); + + const before = isDom ? await rowLabelTops(page, ['Ratsnest']) : null; + + await page.mouse.wheel(0, 240); + await page.waitForTimeout(800); + await page.screenshot({ path: 'test-results/appearance-20-objects-scrolled.png' }); + + if (isDom && before && before['Ratsnest'] !== null) { + const after = await rowLabelTops(page, ['Ratsnest']); + expect(after['Ratsnest'], 'Ratsnest row moved after wheel scroll') + .toBeLessThan(before['Ratsnest']!); + } + }); +}); diff --git a/wxwidgets b/wxwidgets index 1bc2c1a..21f2da0 160000 --- a/wxwidgets +++ b/wxwidgets @@ -1 +1 @@ -Subproject commit 1bc2c1a2d7f19e3282e76d1bdb2876a5c732e191 +Subproject commit 21f2da07f81cf7a0f00726fc1d10f7c76b6a45d2