From d9264549bfae5597995b53cd6fe5de4ef4f7b46c Mon Sep 17 00:00:00 2001 From: Istvan Matejcsok <119620946+matejcsok-ee@users.noreply.github.com> Date: Mon, 6 Jul 2026 09:16:50 +0200 Subject: [PATCH] =?UTF-8?q?test:=20=F0=9F=92=8D=20regression=20test=20for?= =?UTF-8?q?=20the=20notebook=20layer-list=20collapse?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/apps/Makefile.wasm | 11 +- .../collapse-relayout_test.cpp | 197 ++++++++++++++++++ tests/e2e/collapse-relayout.spec.ts | 86 ++++++++ wxwidgets | 2 +- 4 files changed, 294 insertions(+), 2 deletions(-) create mode 100644 tests/apps/standalone/collapse-relayout/collapse-relayout_test.cpp create mode 100644 tests/e2e/collapse-relayout.spec.ts diff --git a/tests/apps/Makefile.wasm b/tests/apps/Makefile.wasm index 313c97d..324d2e4 100644 --- a/tests/apps/Makefile.wasm +++ b/tests/apps/Makefile.wasm @@ -169,6 +169,7 @@ all: minimal_test.html \ $(S)/propgrid/propgrid_test.html \ $(S)/pickers/pickers_test.html \ $(S)/collapsible/collapsible_test.html \ + $(S)/collapse-relayout/collapse-relayout_test.html \ $(S)/listctrl/listctrl_test.html \ $(S)/infobar/infobar_test.html \ $(S)/dataviewvirtual/dataviewvirtual_test.html \ @@ -422,6 +423,13 @@ $(S)/collapsible/collapsible_test.o: $(S)/collapsible/collapsible_test.cpp $(S)/collapsible/collapsible_test.html: $(S)/collapsible/collapsible_test.o $(WX_CORE_LIB) $(JS_FILES) $(CXX) $< $(LDFLAGS_NOGL) --pre-js $(JS) --shell-file $(HTML) -o $@ +# collapse-relayout: notebook page + proportion-1 scrolled list + collapsible pane (no GL) +$(S)/collapse-relayout/collapse-relayout_test.o: $(S)/collapse-relayout/collapse-relayout_test.cpp + $(CXX) -c $(CXXFLAGS) $< -o $@ + +$(S)/collapse-relayout/collapse-relayout_test.html: $(S)/collapse-relayout/collapse-relayout_test.o $(WX_CORE_LIB) $(JS_FILES) + $(CXX) $< $(LDFLAGS_NOGL) --pre-js $(JS) --shell-file $(HTML) -o $@ + # ListCtrl virtual mode test (no GL) $(S)/listctrl/listctrl_test.o: $(S)/listctrl/listctrl_test.cpp $(CXX) -c $(CXXFLAGS) $< -o $@ @@ -767,6 +775,7 @@ dnd: $(S)/dnd/dnd_test.html propgrid: $(S)/propgrid/propgrid_test.html pickers: $(S)/pickers/pickers_test.html collapsible: $(S)/collapsible/collapsible_test.html +collapse-relayout: $(S)/collapse-relayout/collapse-relayout_test.html listctrl: $(S)/listctrl/listctrl_test.html infobar: $(S)/infobar/infobar_test.html dataviewvirtual: $(S)/dataviewvirtual/dataviewvirtual_test.html @@ -813,7 +822,7 @@ clean: rm -f $(S)/*/*_test*.html $(S)/*/*_test*.js $(S)/*/*_test*.wasm rm -f $(S)/*/*_repro*.html $(S)/*/*_repro*.js $(S)/*/*_repro*.wasm -.PHONY: all clean menu contextmenu scrollbar clipboard filedialog layout aui toolbar grid dialog timer tree dataview htmlwin stc print dnd propgrid pickers collapsible listctrl infobar dataviewvirtual auinotebook wizard gridedit calendar gridrenderers printpreview bitmapbuttons specialized validators ownerdrawn popup xml wasmedge fontenum textdecor bitmask regions maximize earlysize selectheight threadpool logerror retinascale coroutine coroutine-nested asyncify-races notebook radiogroups +.PHONY: all clean menu contextmenu scrollbar clipboard filedialog layout aui toolbar grid dialog timer tree dataview htmlwin stc print dnd propgrid pickers collapsible listctrl infobar dataviewvirtual auinotebook wizard gridedit calendar gridrenderers printpreview bitmapbuttons specialized validators ownerdrawn popup xml wasmedge fontenum textdecor bitmask regions maximize earlysize selectheight threadpool logerror retinascale coroutine coroutine-nested asyncify-races notebook radiogroups collapse-relayout # === Coroutine pthread variant — reproduces the KiCad Asyncify-fiber x pthreads crash === # Same modal-free harness as `coroutine`, but compiled/linked with pthreads to match diff --git a/tests/apps/standalone/collapse-relayout/collapse-relayout_test.cpp b/tests/apps/standalone/collapse-relayout/collapse-relayout_test.cpp new file mode 100644 index 0000000..7c1e638 --- /dev/null +++ b/tests/apps/standalone/collapse-relayout/collapse-relayout_test.cpp @@ -0,0 +1,197 @@ +// collapse-relayout test — reproduces the pcbnew "Layer Display Options" bug. +// +// In pcbnew's Appearance sidebar, the Layers notebook page holds a proportion-1 +// wxScrolledCanvas layer list above a collapsible "Layer Display Options" pane. +// On toggle, KiCad runs `Freeze(); page->Fit(); outerSizer->Layout(); Thaw();`. +// In the WASM DOM port that permanently collapses the layer list: page->Fit() +// shrinks the notebook page to content-min (a scrolled window reports a tiny +// best height when vertical scrolling is enabled, see wxScrolledT_Helper:: +// FilterBestSize), and the following outer Layout() re-asserts the *notebook* +// at its UNCHANGED size — which the DOM port's DoSetSize() treats as a no-op +// that never fires wxEVT_SIZE, so wxNotebook::DoSize() never restores the page. +// The list stays at height 0 (its rows clip-pathed away) until reload. +// +// This app mirrors that exact structure with stock widgets so the failure can +// be reproduced and regression-tested without KiCad. + +#include "wx/wxprec.h" + +#ifndef WX_PRECOMP + #include "wx/wx.h" +#endif + +#include "wx/collpane.h" +#include "wx/notebook.h" + +#ifdef __EMSCRIPTEN__ +#include +#endif + +namespace +{ +const char* const kLayerNames[] = { + "top_cu", "bottom_cu", "F.Adhesive", "B.Adhesive", + "F.Paste", "B.Paste", "F.Silkscreen", "B.Silkscreen", + "F.Mask", "B.Mask", "User.Drawings", "User.Comments", + "User.Eco1", "User.Eco2", "Edge.Cuts", +}; + +const wxColour kSwatchColours[] = { + wxColour(200, 52, 52), wxColour( 77, 127, 196), wxColour(107, 33, 125), + wxColour( 20, 20, 120), wxColour(140, 130, 130), wxColour( 40, 160, 160), + wxColour(224, 224, 122), wxColour(224, 160, 160), wxColour( 96, 40, 112), + wxColour( 32, 112, 112), wxColour(200, 200, 210), wxColour( 90, 140, 200), + wxColour(160, 224, 224), wxColour(200, 190, 60), wxColour(210, 210, 210), +}; +} + +class CollapseRelayoutApp : public wxApp +{ +public: + virtual bool OnInit() override; +}; + +class CollapseRelayoutFrame : public wxFrame +{ +public: + CollapseRelayoutFrame(); + +private: + wxNotebook* m_notebook = nullptr; + wxPanel* m_page = nullptr; // the "Layers" notebook page + wxScrolledWindow* m_scrolled = nullptr; // the layer list ("layerlist") + wxCollapsiblePane* m_pane = nullptr; // "Layer Display Options" + + void BuildLayerRows(); + void BuildDisplayOptions(); + void OnPaneChanged(wxCollapsiblePaneEvent& evt); + + wxDECLARE_EVENT_TABLE(); +}; + +enum { ID_DISPLAY_PANE = wxID_HIGHEST + 1 }; + +wxBEGIN_EVENT_TABLE(CollapseRelayoutFrame, wxFrame) + EVT_COLLAPSIBLEPANE_CHANGED(ID_DISPLAY_PANE, CollapseRelayoutFrame::OnPaneChanged) +wxEND_EVENT_TABLE() + +wxIMPLEMENT_APP(CollapseRelayoutApp); + +bool CollapseRelayoutApp::OnInit() +{ + if (!wxApp::OnInit()) + return false; + + (new CollapseRelayoutFrame())->Show(true); + return true; +} + +CollapseRelayoutFrame::CollapseRelayoutFrame() + : wxFrame(nullptr, wxID_ANY, "collapse-relayout WASM Test", + wxDefaultPosition, wxSize(420, 820)) +{ + // Frame -> notebook (prop 1) -> page -> [ scrolled (prop 1), pane (prop 0) ] + wxBoxSizer* frameSizer = new wxBoxSizer(wxVERTICAL); + + m_notebook = new wxNotebook(this, wxID_ANY); + + m_page = new wxPanel(m_notebook, wxID_ANY); + wxBoxSizer* pageSizer = new wxBoxSizer(wxVERTICAL); + + // The layer list: proportion 1, vertical scrolling. Named so the e2e test + // can read its geometry from the element registry. + m_scrolled = new wxScrolledWindow(m_page, wxID_ANY, wxDefaultPosition, + wxDefaultSize, wxVSCROLL); + m_scrolled->SetName("layerlist"); + m_scrolled->SetScrollRate(0, 5); // vertical scroll -> best height caps to ~0 + BuildLayerRows(); + pageSizer->Add(m_scrolled, 1, wxEXPAND); + + // "Layer Display Options": proportion 0, starts collapsed (like KiCad). + m_pane = new wxCollapsiblePane(m_page, ID_DISPLAY_PANE, "Layer Display Options"); + BuildDisplayOptions(); + pageSizer->Add(m_pane, 0, wxEXPAND | wxALL, 5); + + m_page->SetSizer(pageSizer); + m_notebook->AddPage(m_page, "Layers", true); + + frameSizer->Add(m_notebook, 1, wxEXPAND); + SetSizer(frameSizer); + + // A plain wxScrolledWindow doesn't appear in the JS element registry (its + // Create() runs during base construction, so it's skipped as a base type), + // so the test can't read its geometry from the registry. Report the height + // directly from C++ instead: once after the initial layout settles... + CallAfter([this]() { +#ifdef __EMSCRIPTEN__ + EM_ASM({ console.log('[COLLAPSE_RELAYOUT] initial layerlist height = ' + $0); }, + m_scrolled->GetSize().GetHeight()); +#endif + }); + +#ifdef __EMSCRIPTEN__ + EM_ASM({ console.log('[COLLAPSE_RELAYOUT] app started'); }); +#endif +} + +void CollapseRelayoutFrame::BuildLayerRows() +{ + wxBoxSizer* listSizer = new wxBoxSizer(wxVERTICAL); + + for (size_t i = 0; i < WXSIZEOF(kLayerNames); ++i) + { + wxPanel* row = new wxPanel(m_scrolled, wxID_ANY); + wxBoxSizer* rowSizer = new wxBoxSizer(wxHORIZONTAL); + + wxPanel* swatch = new wxPanel(row, wxID_ANY, wxDefaultPosition, wxSize(24, 16)); + swatch->SetBackgroundColour(kSwatchColours[i]); + rowSizer->Add(swatch, 0, wxALIGN_CENTER_VERTICAL | wxALL, 4); + + rowSizer->Add(new wxStaticText(row, wxID_ANY, kLayerNames[i]), + 1, wxALIGN_CENTER_VERTICAL | wxALL, 4); + + row->SetSizer(rowSizer); + listSizer->Add(row, 0, wxEXPAND); + } + + m_scrolled->SetSizer(listSizer); + m_scrolled->FitInside(); // grow the virtual area to hold all rows +} + +void CollapseRelayoutFrame::BuildDisplayOptions() +{ + wxWindow* paneWin = m_pane->GetPane(); + wxBoxSizer* s = new wxBoxSizer(wxVERTICAL); + + s->Add(new wxStaticText(paneWin, wxID_ANY, "Inactive layers (H):"), 0, wxALL, 4); + + wxBoxSizer* modes = new wxBoxSizer(wxHORIZONTAL); + modes->Add(new wxRadioButton(paneWin, wxID_ANY, "Normal", + wxDefaultPosition, wxDefaultSize, wxRB_GROUP), + 0, wxALL, 4); + modes->Add(new wxRadioButton(paneWin, wxID_ANY, "Dim"), 0, wxALL, 4); + modes->Add(new wxRadioButton(paneWin, wxID_ANY, "Hide"), 0, wxALL, 4); + s->Add(modes, 0, wxEXPAND); + + s->Add(new wxCheckBox(paneWin, wxID_ANY, "Flip board view"), 0, wxALL, 4); + + paneWin->SetSizer(s); + s->Fit(paneWin); +} + +// Replicates KiCad's WX_COLLAPSIBLE_PANE_CHANGED handler in +// pcbnew/widgets/appearance_controls.cpp (the Freeze / page->Fit / outer +// Layout / Thaw idiom) that triggers the bug. +void CollapseRelayoutFrame::OnPaneChanged(wxCollapsiblePaneEvent& WXUNUSED(evt)) +{ + Freeze(); + m_page->Fit(); // shrink the notebook page to content-min + GetSizer()->Layout(); // re-assert the notebook at its UNCHANGED size + Thaw(); + +#ifdef __EMSCRIPTEN__ + // With the bug the list collapses to ~0; after the fix it stays tall. + EM_ASM({ console.log('[COLLAPSE_RELAYOUT] layerlist height after toggle = ' + $0); }, + m_scrolled->GetSize().GetHeight()); +#endif +} diff --git a/tests/e2e/collapse-relayout.spec.ts b/tests/e2e/collapse-relayout.spec.ts new file mode 100644 index 0000000..cdcb005 --- /dev/null +++ b/tests/e2e/collapse-relayout.spec.ts @@ -0,0 +1,86 @@ +// collapse-relayout — regression test for the pcbnew "Layer Display Options" bug. +// +// The Layers notebook page holds a proportion-1 scrolled layer list above a +// collapsible "Layer Display Options" pane. Toggling the pane runs KiCad's +// `Freeze(); page->Fit(); outerSizer->Layout(); Thaw();` idiom, which in the +// WASM DOM port permanently collapses the layer list (it stays at height 0, +// rows clip-pathed away, until reload). See wxNotebook::WasmRelayoutSelectedPage +// / wxWindowWasm::DoSetSize in the wxwidgets wasm port. +// +// A plain wxScrolledWindow is not in the JS element registry (its Create() runs +// during base construction and is skipped as a base type), so the app reports +// the layer-list height directly from C++ via console.log; the test asserts on +// those. It is RED before the wasm-layer fix and GREEN after. +import { test, expect, tryLoadApp } from './utils/fixtures'; +import { clickByLabel, findByType, waitForRegistry } from './utils/element-tracker'; + +const APP = '/standalone/collapse-relayout/collapse-relayout_test.html'; +const HEADER_LABEL = 'Layer Display Options'; + +// Parse "[COLLAPSE_RELAYOUT] = N" from the console logs (-1 if absent). +function reportedHeight(logs: string[], tag: string): number { + const line = logs.find(l => l.includes(tag)); + const m = line?.match(/=\s*(\d+)/); + return m ? parseInt(m[1], 10) : -1; +} + +// Expand the collapsible pane: click its header (wxGenericCollapsibleHeaderCtrl +// registers with the pane's label). Fall back to clicking the top of the pane. +async function expandPane(page: import('@playwright/test').Page): Promise { + if (await clickByLabel(page, HEADER_LABEL)) return true; + const panes = await findByType(page, 'wxGenericCollapsiblePane'); + if (panes.length) { + await page.mouse.click(panes[0].screenX + 15, panes[0].screenY + 10); + return true; + } + return false; +} + +test.describe('collapse-relayout (Layer Display Options bug)', () => { + + test('app loads with a populated layer list', async ({ page, testLogger }) => { + await page.goto(APP); + const loaded = await tryLoadApp(page); + expect(loaded, 'App should load').toBe(true); + await waitForRegistry(page); + await page.waitForTimeout(300); + + await page.screenshot({ path: 'test-results/collapse-relayout-01-loaded.png', fullPage: true }); + + expect(testLogger.consoleLogs.some(l => l.includes('[COLLAPSE_RELAYOUT] app started')), + 'app-started log should be present').toBe(true); + + const initial = reportedHeight(testLogger.consoleLogs, 'initial layerlist height'); + console.log('initial layerlist height =', initial); + expect(initial, 'layer list should start with a real height').toBeGreaterThan(100); + + expect(testLogger.errors.filter(e => !e.includes('favicon'))).toHaveLength(0); + }); + + test('layer list survives expanding "Layer Display Options"', async ({ page, testLogger }) => { + await page.goto(APP); + const loaded = await tryLoadApp(page); + expect(loaded, 'App should load').toBe(true); + await waitForRegistry(page); + await page.waitForTimeout(300); + + const initial = reportedHeight(testLogger.consoleLogs, 'initial layerlist height'); + expect(initial, 'layer list should start with a real height').toBeGreaterThan(100); + + const clicked = await expandPane(page); + expect(clicked, `"${HEADER_LABEL}" pane header should be found and clicked`).toBe(true); + await page.waitForTimeout(400); + + await page.screenshot({ path: 'test-results/collapse-relayout-02-expanded.png', fullPage: true }); + + // The handler only logs on a real toggle — its presence proves the click + // reached the pane, and N is the list height measured right after relayout. + const afterToggle = reportedHeight(testLogger.consoleLogs, 'layerlist height after toggle'); + console.log('layerlist height after toggle =', afterToggle); + expect(afterToggle, 'pane toggle handler should have run').toBeGreaterThan(-1); + + // Core assertion: the list must NOT have collapsed to ~0. + expect(afterToggle, 'layer list height after expanding the pane').toBeGreaterThan(50); + }); + +}); diff --git a/wxwidgets b/wxwidgets index b3f0a09..4cad1e4 160000 --- a/wxwidgets +++ b/wxwidgets @@ -1 +1 @@ -Subproject commit b3f0a09d43ebcbe87cdf36b16a156551d4171519 +Subproject commit 4cad1e4876b9749fd8b9e550a3b504c430bbe1a3