fix(wasm): 3D-viewer toolbar clicks hijacked by hidden main-frame DOM controls — input barrier + canvas anchoring
wxwidgets bump: main-window DOM controls join the input barrier when a secondary window overlaps them; secondary windows/GL canvases anchored to the .window.toplevel border→outline. Tests (TDD, red on the old wx.js): standalone secondary-frame app (main-frame wxChoice under a secondary wxFrame's AUI toolbar) + e2e/secondary-frame-input spec (fall-through hit-testing, click delivery, barrier follows drags), and kicad/3d-viewer-toolbar-hijack spec (viewer at top-left over pcbnew's combos — the user-reported repro). modal.spec border assertion updated to the outline ring. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
4b92175c4e
commit
d7cb570f07
6 changed files with 380 additions and 5 deletions
|
|
@ -175,6 +175,7 @@ all: minimal_test.html \
|
||||||
$(S)/layout/layout_test.html \
|
$(S)/layout/layout_test.html \
|
||||||
$(S)/aui/aui_test.html \
|
$(S)/aui/aui_test.html \
|
||||||
$(S)/toolbar/toolbar_test.html \
|
$(S)/toolbar/toolbar_test.html \
|
||||||
|
$(S)/secondary-frame/secondary-frame_test.html \
|
||||||
$(S)/grid/grid_test.html \
|
$(S)/grid/grid_test.html \
|
||||||
$(S)/dialog/dialog_test.html \
|
$(S)/dialog/dialog_test.html \
|
||||||
$(S)/timer/timer_test.html \
|
$(S)/timer/timer_test.html \
|
||||||
|
|
@ -362,6 +363,15 @@ $(S)/toolbar/toolbar_test.o: $(S)/toolbar/toolbar_test.cpp
|
||||||
$(S)/toolbar/toolbar_test.html: $(S)/toolbar/toolbar_test.o $(WX_CORE_LIB) $(JS_FILES)
|
$(S)/toolbar/toolbar_test.html: $(S)/toolbar/toolbar_test.o $(WX_CORE_LIB) $(JS_FILES)
|
||||||
$(CXX) $< $(LDFLAGS_NOGL) --pre-js $(JS) --shell-file $(HTML) -o $@
|
$(CXX) $< $(LDFLAGS_NOGL) --pre-js $(JS) --shell-file $(HTML) -o $@
|
||||||
|
|
||||||
|
# Secondary-frame input test (no GL)
|
||||||
|
$(S)/secondary-frame/secondary-frame_test.o: $(S)/secondary-frame/secondary-frame_test.cpp
|
||||||
|
$(CXX) -c $(CXXFLAGS) $< -o $@
|
||||||
|
|
||||||
|
$(S)/secondary-frame/secondary-frame_test.html: $(S)/secondary-frame/secondary-frame_test.o $(WX_CORE_LIB) $(JS_FILES)
|
||||||
|
$(CXX) $< $(LDFLAGS_NOGL) --pre-js $(JS) --shell-file $(HTML) -o $@
|
||||||
|
|
||||||
|
secondary-frame: $(S)/secondary-frame/secondary-frame_test.html
|
||||||
|
|
||||||
# Grid test (no GL)
|
# Grid test (no GL)
|
||||||
$(S)/grid/grid_test.o: $(S)/grid/grid_test.cpp
|
$(S)/grid/grid_test.o: $(S)/grid/grid_test.cpp
|
||||||
$(CXX) -c $(CXXFLAGS) $< -o $@
|
$(CXX) -c $(CXXFLAGS) $< -o $@
|
||||||
|
|
|
||||||
135
tests/apps/standalone/secondary-frame/secondary-frame_test.cpp
Normal file
135
tests/apps/standalone/secondary-frame/secondary-frame_test.cpp
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
// Secondary-frame input test - a secondary wxFrame (like KiCad's 3D viewer)
|
||||||
|
// overlapping the main frame's native DOM controls.
|
||||||
|
//
|
||||||
|
// The wasm port renders wxChoice as a real DOM <select> with
|
||||||
|
// pointer-events:auto, while a secondary frame's window div is
|
||||||
|
// pointer-events:none (clicks fall through to #canvas for the C++ hit-test).
|
||||||
|
// Without an input barrier for the main window's controls, a click on the
|
||||||
|
// secondary frame's toolbar lands on the hidden <select> underneath and pops
|
||||||
|
// its native dropdown (pcbjam: 3D viewer toolbar vs pcbnew's track-width
|
||||||
|
// selector).
|
||||||
|
|
||||||
|
#include "wx/wxprec.h"
|
||||||
|
|
||||||
|
#ifndef WX_PRECOMP
|
||||||
|
#include "wx/wx.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "wx/artprov.h"
|
||||||
|
#include "wx/aui/auibar.h"
|
||||||
|
|
||||||
|
#ifdef __EMSCRIPTEN__
|
||||||
|
#include <emscripten/emscripten.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
class SecondaryFrameTestApp : public wxApp
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual bool OnInit() override;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum {
|
||||||
|
ID_CHOICE_TRACK = wxID_HIGHEST + 1,
|
||||||
|
ID_TOOL_ORTHO,
|
||||||
|
ID_TOOL_LAYERS
|
||||||
|
};
|
||||||
|
|
||||||
|
class SecondaryFrame : public wxFrame
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit SecondaryFrame(wxWindow* parent)
|
||||||
|
: wxFrame(parent, wxID_ANY, "Secondary Frame",
|
||||||
|
wxPoint(0, 10), wxSize(420, 220))
|
||||||
|
{
|
||||||
|
wxAuiToolBar* tb = new wxAuiToolBar(this, wxID_ANY, wxDefaultPosition,
|
||||||
|
wxDefaultSize, wxAUI_TB_HORZ_LAYOUT | wxAUI_TB_HORIZONTAL);
|
||||||
|
tb->AddTool(ID_TOOL_ORTHO, "Ortho",
|
||||||
|
wxArtProvider::GetBitmap(wxART_TICK_MARK, wxART_TOOLBAR),
|
||||||
|
"Use orthographic projection", wxITEM_CHECK);
|
||||||
|
tb->AddTool(ID_TOOL_LAYERS, "Layers",
|
||||||
|
wxArtProvider::GetBitmap(wxART_LIST_VIEW, wxART_TOOLBAR),
|
||||||
|
"Show layers manager", wxITEM_CHECK);
|
||||||
|
tb->Realize();
|
||||||
|
|
||||||
|
wxBoxSizer* sizer = new wxBoxSizer(wxVERTICAL);
|
||||||
|
sizer->Add(tb, 0, wxEXPAND);
|
||||||
|
sizer->AddStretchSpacer(1);
|
||||||
|
SetSizer(sizer);
|
||||||
|
|
||||||
|
Bind(wxEVT_TOOL, &SecondaryFrame::OnTool, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void OnTool(wxCommandEvent& evt)
|
||||||
|
{
|
||||||
|
const char* name = evt.GetId() == ID_TOOL_ORTHO ? "Ortho" : "Layers";
|
||||||
|
#ifdef __EMSCRIPTEN__
|
||||||
|
EM_ASM({
|
||||||
|
console.log('[SECFRAME] tool ' + UTF8ToString($0) + ' toggled ' + ($1 ? 'on' : 'off'));
|
||||||
|
}, name, evt.IsChecked() ? 1 : 0);
|
||||||
|
#else
|
||||||
|
wxUnusedVar(name);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class MainTestFrame : public wxFrame
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
MainTestFrame()
|
||||||
|
: wxFrame(nullptr, wxID_ANY, "Secondary Frame Input WASM Test",
|
||||||
|
wxDefaultPosition, wxSize(900, 600))
|
||||||
|
{
|
||||||
|
// Native wxChoice (a DOM <select> in the wasm port) near the top-left,
|
||||||
|
// where the secondary frame will overlap it — mirrors pcbnew's
|
||||||
|
// track-width selector under the 3D viewer's toolbar. On a panel: a
|
||||||
|
// wxFrame auto-sizes a lone child to fill its client area, which
|
||||||
|
// would stretch a bare choice fullscreen.
|
||||||
|
wxPanel* panel = new wxPanel(this);
|
||||||
|
wxArrayString widths;
|
||||||
|
widths.Add("Track: use netclass width");
|
||||||
|
widths.Add("0.2 mm");
|
||||||
|
widths.Add("0.25 mm");
|
||||||
|
widths.Add("0.4 mm");
|
||||||
|
m_choice = new wxChoice(panel, ID_CHOICE_TRACK, wxPoint(10, 40),
|
||||||
|
wxSize(180, 24), widths);
|
||||||
|
m_choice->SetSelection(0);
|
||||||
|
|
||||||
|
Bind(wxEVT_CHOICE, &MainTestFrame::OnChoice, this, ID_CHOICE_TRACK);
|
||||||
|
|
||||||
|
SecondaryFrame* sec = new SecondaryFrame(this);
|
||||||
|
sec->Show(true);
|
||||||
|
|
||||||
|
#ifdef __EMSCRIPTEN__
|
||||||
|
EM_ASM({
|
||||||
|
console.log('[SECFRAME] secondary-frame test app started');
|
||||||
|
});
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void OnChoice(wxCommandEvent& evt)
|
||||||
|
{
|
||||||
|
#ifdef __EMSCRIPTEN__
|
||||||
|
EM_ASM({
|
||||||
|
console.log('[SECFRAME] choice selected ' + $0);
|
||||||
|
}, evt.GetSelection());
|
||||||
|
#else
|
||||||
|
wxUnusedVar(evt);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
wxChoice* m_choice;
|
||||||
|
};
|
||||||
|
|
||||||
|
wxIMPLEMENT_APP(SecondaryFrameTestApp);
|
||||||
|
|
||||||
|
bool SecondaryFrameTestApp::OnInit()
|
||||||
|
{
|
||||||
|
if (!wxApp::OnInit())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
MainTestFrame* frame = new MainTestFrame();
|
||||||
|
frame->Show(true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
@ -106,8 +106,8 @@ test.describe('Modal dialog border + drag (pcbjam #22)', () => {
|
||||||
if (!el) return null;
|
if (!el) return null;
|
||||||
const cs = getComputedStyle(el);
|
const cs = getComputedStyle(el);
|
||||||
return {
|
return {
|
||||||
borderTopStyle: cs.borderTopStyle,
|
outlineStyle: cs.outlineStyle,
|
||||||
borderTopWidth: cs.borderTopWidth,
|
outlineWidth: cs.outlineWidth,
|
||||||
boxShadow: cs.boxShadow,
|
boxShadow: cs.boxShadow,
|
||||||
};
|
};
|
||||||
}, MODAL_SEL);
|
}, MODAL_SEL);
|
||||||
|
|
@ -115,10 +115,14 @@ test.describe('Modal dialog border + drag (pcbjam #22)', () => {
|
||||||
expect(style, 'modal element should exist').not.toBeNull();
|
expect(style, 'modal element should exist').not.toBeNull();
|
||||||
testLogger.consoleLogs.push(`[MODAL_BORDER] ${JSON.stringify(style)}`);
|
testLogger.consoleLogs.push(`[MODAL_BORDER] ${JSON.stringify(style)}`);
|
||||||
|
|
||||||
const hasBorder = style!.borderTopStyle !== 'none' && parseFloat(style!.borderTopWidth) > 0;
|
// The 1px ring is an outline, not a border: a border shifts the padding
|
||||||
|
// box (where .window-canvas and wx-dom controls anchor) 1px off the wx
|
||||||
|
// model rect, while an outline draws outside the box with no layout
|
||||||
|
// effect.
|
||||||
|
const hasRing = style!.outlineStyle !== 'none' && parseFloat(style!.outlineWidth) > 0;
|
||||||
const hasShadow = style!.boxShadow !== 'none' && style!.boxShadow !== '';
|
const hasShadow = style!.boxShadow !== 'none' && style!.boxShadow !== '';
|
||||||
|
|
||||||
expect(hasBorder, `expected a visible border, got ${JSON.stringify(style)}`).toBe(true);
|
expect(hasRing, `expected a visible outline ring, got ${JSON.stringify(style)}`).toBe(true);
|
||||||
expect(hasShadow, `expected a box-shadow, got ${JSON.stringify(style)}`).toBe(true);
|
expect(hasShadow, `expected a box-shadow, got ${JSON.stringify(style)}`).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
119
tests/e2e/secondary-frame-input.spec.ts
Normal file
119
tests/e2e/secondary-frame-input.spec.ts
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
// Secondary-frame input barrier — clicks on a secondary frame must reach that
|
||||||
|
// frame, not the main frame's native DOM controls underneath it.
|
||||||
|
//
|
||||||
|
// The wasm port renders wxChoice as a real DOM <select> (pointer-events:auto)
|
||||||
|
// while a secondary frame's window div is pointer-events:none, so clicks over
|
||||||
|
// the frame are meant to fall through to #canvas for the C++ hit-test. Without
|
||||||
|
// a barrier for the main window's controls, the fall-through is intercepted by
|
||||||
|
// the hidden <select> and the browser pops its native dropdown instead of
|
||||||
|
// activating the frame's toolbar button (pcbjam: the 3D viewer's toolbar over
|
||||||
|
// pcbnew's track-width selector).
|
||||||
|
//
|
||||||
|
// Registry coords are wx-screen coords, i.e. #canvas-relative CSS px; the wx
|
||||||
|
// template host does not put the canvas at the page origin, so every page
|
||||||
|
// interaction adds the canvas's page offset.
|
||||||
|
import type { Page } from '@playwright/test';
|
||||||
|
import { test, expect, waitForWxApp } from './utils/fixtures';
|
||||||
|
import { findByTooltip, waitUntil } from './utils/element-tracker';
|
||||||
|
|
||||||
|
const APP = '/standalone/secondary-frame/secondary-frame_test.html';
|
||||||
|
|
||||||
|
// The app: main frame with a wxChoice at wx-screen (10,40)–(190,64); secondary
|
||||||
|
// frame at (0,10) 420x220 with an AUI toolbar of two check tools, covering the
|
||||||
|
// choice.
|
||||||
|
|
||||||
|
function canvasOffset(page: Page) {
|
||||||
|
return page.evaluate(() => {
|
||||||
|
const r = document.getElementById('canvas')!.getBoundingClientRect();
|
||||||
|
return { x: r.left, y: r.top };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function hitAtPage(page: Page, x: number, y: number) {
|
||||||
|
return page.evaluate(([px, py]) => {
|
||||||
|
const el = document.elementFromPoint(px, py);
|
||||||
|
return el ? { tag: el.tagName, cls: String(el.className) } : null;
|
||||||
|
}, [x, y]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function bootApp(page: Page) {
|
||||||
|
await page.goto(APP);
|
||||||
|
await waitForWxApp(page);
|
||||||
|
await waitUntil(page, () => {
|
||||||
|
const r = window.wxElementRegistry;
|
||||||
|
return !!r?.findAllRendered
|
||||||
|
&& r.findAllRendered({ elementType: 'tool' })
|
||||||
|
.some((t) => t.tooltip?.includes('orthographic'));
|
||||||
|
}, 'secondary frame AUI toolbar rendered');
|
||||||
|
}
|
||||||
|
|
||||||
|
test.describe('secondary frame over main-frame DOM controls', () => {
|
||||||
|
|
||||||
|
test('toolbar clicks reach the secondary frame, covered select is inert', async ({ page, testLogger }) => {
|
||||||
|
await bootApp(page);
|
||||||
|
const off = await canvasOffset(page);
|
||||||
|
|
||||||
|
const ortho = await findByTooltip(page, 'orthographic', { elementType: 'tool' });
|
||||||
|
expect(ortho, 'Ortho check tool must be in the registry').not.toBeNull();
|
||||||
|
const toolPage = { x: off.x + ortho!.centerX, y: off.y + ortho!.centerY };
|
||||||
|
|
||||||
|
// The tool sits inside the secondary frame, over the main frame's
|
||||||
|
// <select>. DOM hit-testing at its centre must NOT surface the select.
|
||||||
|
const hit = await hitAtPage(page, toolPage.x, toolPage.y);
|
||||||
|
expect(hit, 'something must be hit-testable at the tool centre').not.toBeNull();
|
||||||
|
expect(hit!.tag, `hit at tool centre page(${toolPage.x},${toolPage.y}) must fall through to #canvas, not the main frame's <select>`)
|
||||||
|
.toBe('CANVAS');
|
||||||
|
|
||||||
|
// Clicking the tool must toggle IT — not pop the hidden select.
|
||||||
|
await page.mouse.click(toolPage.x, toolPage.y);
|
||||||
|
await expect.poll(
|
||||||
|
() => testLogger.consoleLogs.some((l) => l.includes('[SECFRAME] tool Ortho toggled on')),
|
||||||
|
{ message: 'the Ortho check tool should receive the click', timeout: 5000 },
|
||||||
|
).toBe(true);
|
||||||
|
|
||||||
|
// The covered select carries the barrier class.
|
||||||
|
const selectState = await page.evaluate(() => {
|
||||||
|
const sel = document.querySelector('#main-window select.wx-dom-control');
|
||||||
|
return sel ? { inert: sel.classList.contains('wx-inert') } : null;
|
||||||
|
});
|
||||||
|
expect(selectState, 'the wxChoice must be rendered as a DOM select').not.toBeNull();
|
||||||
|
expect(selectState!.inert, 'the covered select must be input-inert').toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('barrier follows the frame: dragging it away frees the select, back re-blocks it', async ({ page }) => {
|
||||||
|
await bootApp(page);
|
||||||
|
const off = await canvasOffset(page);
|
||||||
|
|
||||||
|
const titlebarGrab = () => page.evaluate(() => {
|
||||||
|
const el = document.querySelector('#window-container .window.toplevel .window-titlebar')!;
|
||||||
|
const r = el.getBoundingClientRect();
|
||||||
|
return { x: r.left + 60, y: r.top + r.height / 2 };
|
||||||
|
});
|
||||||
|
|
||||||
|
// Drag the secondary frame far right so it no longer covers the select.
|
||||||
|
const bar = await titlebarGrab();
|
||||||
|
await page.mouse.move(bar.x, bar.y);
|
||||||
|
await page.mouse.down();
|
||||||
|
await page.mouse.move(bar.x + 460, bar.y + 60, { steps: 6 });
|
||||||
|
await page.mouse.up();
|
||||||
|
|
||||||
|
await expect.poll(async () => page.evaluate(() => {
|
||||||
|
const sel = document.querySelector('#main-window select.wx-dom-control')!;
|
||||||
|
return sel.classList.contains('wx-inert');
|
||||||
|
}), { message: 'uncovered select must become interactive again', timeout: 5000 }).toBe(false);
|
||||||
|
const freeHit = await hitAtPage(page, off.x + 100, off.y + 52);
|
||||||
|
expect(freeHit!.tag, 'hit-test over the uncovered select must reach it').toBe('SELECT');
|
||||||
|
|
||||||
|
// Drag back over the select — the barrier must re-engage.
|
||||||
|
const bar2 = await titlebarGrab();
|
||||||
|
await page.mouse.move(bar2.x, bar2.y);
|
||||||
|
await page.mouse.down();
|
||||||
|
await page.mouse.move(bar2.x - 460, bar2.y - 60, { steps: 6 });
|
||||||
|
await page.mouse.up();
|
||||||
|
|
||||||
|
await expect.poll(async () => page.evaluate(() => {
|
||||||
|
const sel = document.querySelector('#main-window select.wx-dom-control')!;
|
||||||
|
return sel.classList.contains('wx-inert');
|
||||||
|
}), { message: 're-covered select must be input-inert again', timeout: 5000 }).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
107
tests/kicad/3d-viewer-toolbar-hijack.spec.ts
Normal file
107
tests/kicad/3d-viewer-toolbar-hijack.spec.ts
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
// 3D viewer toolbar vs pcbnew's hidden DOM controls.
|
||||||
|
//
|
||||||
|
// The 3D viewer is a secondary frame: its window div is pointer-events:none so
|
||||||
|
// clicks fall through to #canvas for the C++ hit-test. pcbnew's toolbar combos
|
||||||
|
// (track width / via / grid / zoom) are real DOM <select>s with
|
||||||
|
// pointer-events:auto inside #main-window. Where the viewer's toolbar row
|
||||||
|
// overlaps one of those selects, the fall-through used to be intercepted by
|
||||||
|
// the hidden select — the browser popped "Track: use netclass width / Edit
|
||||||
|
// Pre-defined Sizes..." instead of pressing the viewer's button. The input
|
||||||
|
// barrier (recomputeModalBarrier in wx.js) must make the covered main-frame
|
||||||
|
// controls inert while the viewer overlaps them.
|
||||||
|
import { test, expect } from './fixtures';
|
||||||
|
import { waitUntil, findByTooltip } from '../e2e/utils/element-tracker';
|
||||||
|
import { waitForPcbnew } from './utils/pcbnew-ready';
|
||||||
|
import { loadBoard, countGlCanvases, openThreeDViewer } from './utils/threed-viewer';
|
||||||
|
|
||||||
|
test.describe('3D viewer toolbar over pcbnew DOM controls', () => {
|
||||||
|
test.setTimeout(240000);
|
||||||
|
|
||||||
|
test('viewer toolbar clicks are not hijacked by hidden pcbnew selects', async ({ page, testLogger }) => {
|
||||||
|
await page.goto('/kicad/pcbnew.html');
|
||||||
|
await waitForPcbnew(page);
|
||||||
|
await loadBoard(page, testLogger);
|
||||||
|
|
||||||
|
const glBefore = await countGlCanvases(page);
|
||||||
|
await openThreeDViewer(page, glBefore);
|
||||||
|
|
||||||
|
// Drag the viewer to the top-left (the user-reported layout): its
|
||||||
|
// toolbar row then overlaps pcbnew's track-width/via/grid/zoom selects.
|
||||||
|
const bar = await page.evaluate(() => {
|
||||||
|
const win = [...document.querySelectorAll<HTMLElement>('#window-container .window.toplevel')]
|
||||||
|
.find((w) => getComputedStyle(w).display !== 'none' && w.getBoundingClientRect().width > 100)!;
|
||||||
|
const r = win.querySelector('.window-titlebar')!.getBoundingClientRect();
|
||||||
|
const wr = win.getBoundingClientRect();
|
||||||
|
return { x: r.left + 60, y: r.top + r.height / 2, winLeft: wr.left, winTop: wr.top };
|
||||||
|
});
|
||||||
|
await page.mouse.move(bar.x, bar.y);
|
||||||
|
await page.mouse.down();
|
||||||
|
await page.mouse.move(bar.x - bar.winLeft, bar.y - bar.winTop + 2, { steps: 8 });
|
||||||
|
await page.mouse.up();
|
||||||
|
|
||||||
|
// The viewer's own AUI toolbar is registry-tracked; wait for it.
|
||||||
|
await waitUntil(page, () => {
|
||||||
|
const r = window.wxElementRegistry;
|
||||||
|
return !!r?.findAllRendered
|
||||||
|
&& r.findAllRendered({ elementType: 'tool' })
|
||||||
|
.some((t) => t.tooltip?.includes('orthographic'));
|
||||||
|
}, '3D viewer toolbar rendered');
|
||||||
|
|
||||||
|
// Every centre of the VIEWER's own toolbar (the AUI toolbar that owns
|
||||||
|
// toggleOrtho, selected via parentId — the registry holds every
|
||||||
|
// frame's tools) must DOM-hit-test to #canvas: the buttons are
|
||||||
|
// canvas-painted, so anything else there (a pcbnew <select>) would
|
||||||
|
// swallow the click.
|
||||||
|
const hits = await page.evaluate(() => {
|
||||||
|
const reg = window.wxElementRegistry!;
|
||||||
|
const all = reg.findAllRendered({ elementType: 'tool' });
|
||||||
|
const ortho = all.find((t) => t.tooltip?.includes('orthographic'))!;
|
||||||
|
return all
|
||||||
|
.filter((t) => t.parentId === ortho.parentId)
|
||||||
|
.map((t) => {
|
||||||
|
const el = document.elementFromPoint(t.centerX, t.centerY);
|
||||||
|
return {
|
||||||
|
tip: (t.tooltip ?? '').split('\n')[0],
|
||||||
|
x: t.centerX, y: t.centerY,
|
||||||
|
hit: el ? el.tagName : 'none',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(hits.length, 'viewer toolbar tools must be tracked').toBeGreaterThan(5);
|
||||||
|
const hijacked = hits.filter((h) => h.hit !== 'CANVAS');
|
||||||
|
expect(hijacked, 'no viewer toolbar tool may be shadowed by a live main-frame DOM control')
|
||||||
|
.toEqual([]);
|
||||||
|
|
||||||
|
// The covered track-width select must carry the input barrier...
|
||||||
|
const trackSelect = () => page.evaluate(() => {
|
||||||
|
const sel = [...document.querySelectorAll<HTMLSelectElement>('#main-window select.wx-dom-control')]
|
||||||
|
.find((s) => [...s.options].some((o) => o.text.includes('use netclass width')));
|
||||||
|
return sel ? { inert: sel.classList.contains('wx-inert') } : null;
|
||||||
|
});
|
||||||
|
const covered = await trackSelect();
|
||||||
|
expect(covered, 'pcbnew track-width select must exist').not.toBeNull();
|
||||||
|
expect(covered!.inert, 'track-width select under the viewer must be input-inert').toBe(true);
|
||||||
|
|
||||||
|
// ...and the click must reach the viewer's button: toggling ortho
|
||||||
|
// appends " [checked]" to its registry label.
|
||||||
|
const ortho = await findByTooltip(page, 'orthographic', { elementType: 'tool' });
|
||||||
|
expect(ortho, 'toggleOrtho must be in the registry').not.toBeNull();
|
||||||
|
expect((ortho!.label ?? '').includes('[checked]')).toBe(false);
|
||||||
|
await page.mouse.click(ortho!.centerX, ortho!.centerY);
|
||||||
|
await expect.poll(async () => {
|
||||||
|
const t = await findByTooltip(page, 'orthographic', { elementType: 'tool' });
|
||||||
|
return (t?.label ?? '').includes('[checked]');
|
||||||
|
}, { message: 'clicking toggleOrtho must toggle IT, not a hidden select', timeout: 10000 })
|
||||||
|
.toBe(true);
|
||||||
|
|
||||||
|
// Closing the viewer must release the barrier.
|
||||||
|
await page.evaluate(() => {
|
||||||
|
const win = [...document.querySelectorAll<HTMLElement>('#window-container .window.toplevel')]
|
||||||
|
.find((w) => getComputedStyle(w).display !== 'none' && w.getBoundingClientRect().width > 100)!;
|
||||||
|
(win.querySelector('.window-titlebar-close') as HTMLElement).click();
|
||||||
|
});
|
||||||
|
await expect.poll(async () => (await trackSelect())!.inert,
|
||||||
|
{ message: 'track-width select must be interactive after the viewer closes', timeout: 10000 })
|
||||||
|
.toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit c1f14775ba33ca2043840d4fcfbac1b2ea5beae1
|
Subproject commit 4752f1bfbebfd25e7a2bf376a0ffd28bbd5ac876
|
||||||
Loading…
Reference in a new issue