pcbjam/tests/e2e/uipolish.spec.ts
Gergő Törcsvári 931935c70a
findings group O: chooser Enter + infobar close fixes (wx → 15e5315244) with repro specs and uipolish guards
- tests/kicad/footprint-chooser-confirm.spec.ts (O-2): typed filter + Enter
  confirms the chooser and a footprint is placed.
- tests/kicad/infobar-dismiss.spec.ts (O-3): real click on the older-version
  infobar close glyph dismisses it; GAL rect shift logged.
- uipolish app/spec: rounded-neg-radius (O-1 guard), enable-propagation (O-3),
  dom-nav-keys Enter/ArrowDown → CHAR_HOOK with TEXT_ENTER once (O-2).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3cyLvFWbfNdCNW7x7C2A
2026-08-28 11:31:32 +02:00

135 lines
5.7 KiB
TypeScript

// UI Polish Test - Regression guards for the wasm-ui-polish fixes
// (wxwidgets wasm-port commit 4a02a39c07).
//
// The C++ app (tests/apps/standalone/uipolish/uipolish_test.cpp) runs
// self-asserting checks at boot and logs one "[UIPOLISH_TEST] <check>: PASS/
// FAIL" line each plus a "done checks=N pass=N" summary:
// clip-clear / clip-empty / clip-box DC clip box reaches the canvas
// (pre-fix every clip was empty and the JS layer disabled clipping —
// the collapsed wire-properties-panel bug)
// blit-origin Blit honors the source DC device origin (wxBufferedDC)
// mask-alpha ConvertToImage carries wxMask as alpha (infobar close btn)
// scaled-dims ConvertToImage returns physical size for scaled bitmaps
// checkbox-floor wxCheckBox best-height floor (selection-filter density)
// statbmp-best wxStaticBitmap best size is the bundle's LOGICAL size
// rounded-neg-radius negative/oversize DrawRoundedRectangle radius paints,
// never throws (findings O-1 — the badge that killed the wx tick)
//
// statbmp-best only discriminates at devicePixelRatio >= 1.5 (pre-fix the
// FromPhys path inflated 16 -> 32 there), so a second pass runs the app at
// deviceScaleFactor: 2 and additionally asserts the <img> ships the 32px
// asset at 16 CSS px — the crisp-layer-eyes contract.
//
// Determinism: no waitForTimeout; readiness via waitForWxApp plus polling for
// the app's own "done" summary line.
import { test, expect, waitForWxApp } from './utils/fixtures';
import { stableShot } from './utils/element-tracker';
import { Page } from '@playwright/test';
const CHECKS = [
'clip-clear',
'clip-empty',
'clip-box',
'blit-origin',
'mask-alpha',
'scaled-dims',
'checkbox-floor',
'statbmp-best',
'rounded-neg-radius',
'enable-propagation',
];
async function waitForDone(page: Page, logs: string[]) {
await expect.poll(
() => logs.some((l) => l.includes('[UIPOLISH_TEST] done checks=')),
{ message: 'app should log its check summary' },
).toBe(true);
}
function collectUipolishLogs(page: Page): string[] {
const logs: string[] = [];
page.on('console', (msg) => {
const text = msg.text();
if (text.includes('[UIPOLISH_TEST]')) logs.push(text);
});
return logs;
}
function assertChecks(logs: string[]) {
for (const check of CHECKS) {
const pass = logs.some((l) => l.includes(`[UIPOLISH_TEST] ${check}: PASS`));
const fail = logs.find((l) => l.includes(`[UIPOLISH_TEST] ${check}: FAIL`));
expect(fail, `${check} must not FAIL (${fail ?? ''})`).toBeUndefined();
expect(pass, `${check} must PASS`).toBe(true);
}
}
test.describe('UI Polish regression guards', () => {
test('all DC/bestsize checks pass at default DPR', async ({ page }) => {
const logs = collectUipolishLogs(page);
await page.goto('/standalone/uipolish/uipolish_test.html');
await waitForWxApp(page);
await waitForDone(page, logs);
await stableShot(page, 'uipolish-01-default-dpr.png', { fullPage: true });
assertChecks(logs);
// dom-nav-keys (findings O-2): keydown Enter / ArrowDown on the DOM
// <input> must surface as wxEVT_CHAR_HOOK (13 = WXK_RETURN, 317 =
// WXK_DOWN); the Skip()ing hook lets wxEVT_TEXT_ENTER fire exactly once.
const domIdLine = logs.find((l) => l.includes('navkeys domId='));
expect(domIdLine, 'navkeys text ctrl announced its DOM id').toBeDefined();
const domId = Number(domIdLine!.split('domId=')[1]);
const fired = await page.evaluate((id) => {
const el = document.querySelector(`[data-wx-dom-id="${id}"]`) as HTMLElement | null;
const input = (el?.tagName === 'INPUT' ? el : el?.querySelector('input')) as HTMLInputElement | null;
if (!input) return false;
input.focus();
for (const key of ['Enter', 'ArrowDown']) {
input.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true, cancelable: true }));
}
return true;
}, domId);
expect(fired, 'navkeys <input> found').toBe(true);
await expect.poll(() => logs.some((l) => l.includes('charhook key=13')),
{ message: 'Enter reached wxEVT_CHAR_HOOK' }).toBe(true);
await expect.poll(() => logs.some((l) => l.includes('charhook key=317')),
{ message: 'ArrowDown reached wxEVT_CHAR_HOOK' }).toBe(true);
await expect.poll(() => logs.filter((l) => l.includes('[UIPOLISH_TEST] textenter')).length,
{ message: 'TEXT_ENTER fired once' }).toBe(1);
});
});
test.describe('UI Polish regression guards @2x', () => {
// statbmp-best is only a regression guard on a hi-DPI display: pre-fix the
// base DoGetBestSize inflated the layout box to physical pixels (32) once
// GetDPIScaleFactor() >= 1.5.
test.use({ deviceScaleFactor: 2 });
test('statbmp lays out at logical size and ships the 2x asset', async ({ page }) => {
const logs = collectUipolishLogs(page);
await page.goto('/standalone/uipolish/uipolish_test.html');
await waitForWxApp(page);
await waitForDone(page, logs);
await stableShot(page, 'uipolish-02-hidpi.png', { fullPage: true });
assertChecks(logs);
// The frame's wxStaticBitmap is the only <img> control in the app: it must
// ship the hi-res 32px asset but occupy the LOGICAL 16 CSS px.
const img = await page.evaluate(() => {
const el = Array.from(document.querySelectorAll('img.wx-dom-control'))
.map((e) => e as HTMLImageElement)
.find((e) => e.getBoundingClientRect().width > 0);
if (!el) return null;
const r = el.getBoundingClientRect();
return { naturalWidth: el.naturalWidth, cssWidth: Math.round(r.width) };
});
expect(img, 'the statbmp <img> should exist').not.toBeNull();
expect(img!.naturalWidth, 'should ship the 2x (32px) asset').toBe(32);
expect(img!.cssWidth, 'should occupy the logical 16 CSS px').toBe(16);
});
});