test: 💍 add tab switch fix test
This commit is contained in:
parent
58fb1c5db7
commit
f4af42514a
4 changed files with 238 additions and 2 deletions
|
|
@ -128,6 +128,7 @@ all: minimal_test.html \
|
|||
$(S)/menu/menu_test.html \
|
||||
$(S)/contextmenu/contextmenu_test.html \
|
||||
$(S)/scrollbar/scrollbar_test.html \
|
||||
$(S)/notebook/notebook_test.html \
|
||||
$(S)/clipboard/clipboard_test.html \
|
||||
$(S)/filedialog/filedialog_test.html \
|
||||
$(S)/layout/layout_test.html \
|
||||
|
|
@ -236,6 +237,12 @@ $(S)/scrollbar/scrollbar_test.o: $(S)/scrollbar/scrollbar_test.cpp
|
|||
$(S)/scrollbar/scrollbar_test.html: $(S)/scrollbar/scrollbar_test.o $(WX_CORE_LIB) $(JS_FILES)
|
||||
$(CXX) $< $(LDFLAGS_NOGL) --pre-js $(JS) --shell-file $(HTML) -o $@
|
||||
|
||||
$(S)/notebook/notebook_test.o: $(S)/notebook/notebook_test.cpp
|
||||
$(CXX) -c $(CXXFLAGS) $< -o $@
|
||||
|
||||
$(S)/notebook/notebook_test.html: $(S)/notebook/notebook_test.o $(WX_CORE_LIB) $(JS_FILES)
|
||||
$(CXX) $< $(LDFLAGS_NOGL) --pre-js $(JS) --shell-file $(HTML) -o $@
|
||||
|
||||
# Clipboard test (no GL)
|
||||
$(S)/clipboard/clipboard_test.o: $(S)/clipboard/clipboard_test.cpp
|
||||
$(CXX) -c $(CXXFLAGS) $< -o $@
|
||||
|
|
@ -572,6 +579,7 @@ $(S)/asyncify-races/races_test_nosleepfix.html: $(S)/asyncify-races/races_test.o
|
|||
menu: $(S)/menu/menu_test.html
|
||||
contextmenu: $(S)/contextmenu/contextmenu_test.html
|
||||
scrollbar: $(S)/scrollbar/scrollbar_test.html
|
||||
notebook: $(S)/notebook/notebook_test.html
|
||||
clipboard: $(S)/clipboard/clipboard_test.html
|
||||
filedialog: $(S)/filedialog/filedialog_test.html
|
||||
layout: $(S)/layout/layout_test.html
|
||||
|
|
@ -635,7 +643,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
|
||||
.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
|
||||
|
||||
# === Coroutine pthread variant — reproduces the KiCad Asyncify-fiber x pthreads crash ===
|
||||
# Same modal-free harness as `coroutine`, but compiled/linked with pthreads to match
|
||||
|
|
|
|||
147
tests/apps/standalone/notebook/notebook_test.cpp
Normal file
147
tests/apps/standalone/notebook/notebook_test.cpp
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
// wxNotebook page-relayout Test - regression coverage for the WASM DOM-port
|
||||
// fix in wxwidgets/src/wasm/notebook.cpp (wxNotebook::WasmRelayoutSelectedPage).
|
||||
//
|
||||
// Bug (pcbjam#8): KiCad's APPEARANCE_CONTROLS::OnNotebookPageChanged calls
|
||||
// Fit() on the just-selected page from a wxEVT_NOTEBOOK_PAGE_CHANGED handler.
|
||||
// That collapses a wxScrolledWindow child's viewport to zero height; because
|
||||
// DoSetSize() only re-runs layout when the size actually changes, resizing the
|
||||
// page back was a no-op and the scrolled rows stayed clip-pathed away ("pages
|
||||
// came back blank" after switching tabs away and back).
|
||||
//
|
||||
// This app reproduces the same shape in pure wxWidgets: a wxNotebook whose
|
||||
// "Scrolled" page wraps a wxScrolledWindow full of labelled rows, plus a
|
||||
// PAGE_CHANGED handler that calls page->Fit() exactly like KiCad. With the fix,
|
||||
// OnDomEvent re-asserts the page geometry after SetSelection() so the rows stay
|
||||
// painted; without it, they collapse. The rows are wxStaticText so the DOM port
|
||||
// renders them as <span> elements the e2e spec can hit-test.
|
||||
|
||||
#include "wx/wxprec.h"
|
||||
|
||||
#ifndef WX_PRECOMP
|
||||
#include "wx/wx.h"
|
||||
#endif
|
||||
|
||||
#include "wx/notebook.h"
|
||||
#include "wx/scrolwin.h"
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
#include <emscripten/emscripten.h>
|
||||
#endif
|
||||
|
||||
static const int kRowCount = 30;
|
||||
|
||||
class NotebookApp : public wxApp
|
||||
{
|
||||
public:
|
||||
virtual bool OnInit() override;
|
||||
};
|
||||
|
||||
class NotebookFrame : public wxFrame
|
||||
{
|
||||
public:
|
||||
NotebookFrame();
|
||||
|
||||
private:
|
||||
wxNotebook* m_notebook = nullptr;
|
||||
|
||||
wxWindow* CreatePlainPage();
|
||||
wxWindow* CreateScrolledPage();
|
||||
|
||||
void Log(const wxString& msg);
|
||||
void OnPageChanged(wxNotebookEvent& evt);
|
||||
};
|
||||
|
||||
bool NotebookApp::OnInit()
|
||||
{
|
||||
if ( !wxApp::OnInit() )
|
||||
return false;
|
||||
|
||||
(new NotebookFrame())->Show(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
wxIMPLEMENT_APP(NotebookApp);
|
||||
|
||||
NotebookFrame::NotebookFrame()
|
||||
: wxFrame(nullptr, wxID_ANY, "wxNotebook Page Relayout Test",
|
||||
wxDefaultPosition, wxSize(420, 480))
|
||||
{
|
||||
wxBoxSizer* sizer = new wxBoxSizer(wxVERTICAL);
|
||||
|
||||
m_notebook = new wxNotebook(this, wxID_ANY);
|
||||
m_notebook->AddPage(CreatePlainPage(), "Plain", true);
|
||||
m_notebook->AddPage(CreateScrolledPage(), "Scrolled", false);
|
||||
|
||||
sizer->Add(m_notebook, 1, wxEXPAND | wxALL, 6);
|
||||
SetSizer(sizer);
|
||||
|
||||
// Reproduce KiCad's APPEARANCE_CONTROLS::OnNotebookPageChanged: call Fit()
|
||||
// on the selected page from the PAGE_CHANGED handler. This is what collapses
|
||||
// the scrolled child; the DOM-port fix repairs it after this handler runs.
|
||||
m_notebook->Bind(wxEVT_NOTEBOOK_PAGE_CHANGED, &NotebookFrame::OnPageChanged, this);
|
||||
|
||||
Log("notebook test app started");
|
||||
}
|
||||
|
||||
wxWindow* NotebookFrame::CreatePlainPage()
|
||||
{
|
||||
wxPanel* panel = new wxPanel(m_notebook);
|
||||
wxBoxSizer* sizer = new wxBoxSizer(wxVERTICAL);
|
||||
sizer->Add(new wxStaticText(panel, wxID_ANY, "Plain page"), 0, wxALL, 10);
|
||||
sizer->Add(new wxStaticText(panel, wxID_ANY,
|
||||
"Switch to the Scrolled page and back."),
|
||||
0, wxLEFT | wxBOTTOM, 10);
|
||||
panel->SetSizer(sizer);
|
||||
return panel;
|
||||
}
|
||||
|
||||
wxWindow* NotebookFrame::CreateScrolledPage()
|
||||
{
|
||||
// panel -> wxScrolledWindow -> many rows. Mirrors KiCad's layer panel:
|
||||
// the page is a wxPanel that hosts a scrolled list; Fit() on the panel is
|
||||
// what drives the viewport to zero.
|
||||
wxPanel* panel = new wxPanel(m_notebook);
|
||||
|
||||
wxScrolledWindow* scrolled =
|
||||
new wxScrolledWindow(panel, wxID_ANY, wxDefaultPosition, wxDefaultSize,
|
||||
wxVSCROLL | wxBORDER_SIMPLE);
|
||||
scrolled->SetScrollRate(0, 10);
|
||||
|
||||
wxBoxSizer* rows = new wxBoxSizer(wxVERTICAL);
|
||||
for ( int i = 0; i < kRowCount; ++i )
|
||||
rows->Add(new wxStaticText(scrolled, wxID_ANY,
|
||||
wxString::Format("Row %d", i)),
|
||||
0, wxALL, 4);
|
||||
scrolled->SetSizer(rows);
|
||||
scrolled->FitInside();
|
||||
|
||||
wxBoxSizer* pageSizer = new wxBoxSizer(wxVERTICAL);
|
||||
pageSizer->Add(scrolled, 1, wxEXPAND | wxALL, 4);
|
||||
panel->SetSizer(pageSizer);
|
||||
return panel;
|
||||
}
|
||||
|
||||
void NotebookFrame::Log(const wxString& msg)
|
||||
{
|
||||
#ifdef __EMSCRIPTEN__
|
||||
EM_ASM({
|
||||
console.log('[NOTEBOOK_EVENT] ' + UTF8ToString($0));
|
||||
}, (const char*)msg.utf8_str());
|
||||
#else
|
||||
wxLogMessage("[NOTEBOOK_EVENT] %s", msg);
|
||||
#endif
|
||||
}
|
||||
|
||||
void NotebookFrame::OnPageChanged(wxNotebookEvent& evt)
|
||||
{
|
||||
const int sel = evt.GetSelection();
|
||||
if ( sel != wxNOT_FOUND )
|
||||
{
|
||||
if ( wxWindow* const page = m_notebook->GetPage(sel) )
|
||||
page->Fit();
|
||||
|
||||
Log(wxString::Format("switched to page %d (%s)",
|
||||
sel, m_notebook->GetPageText(sel)));
|
||||
}
|
||||
evt.Skip();
|
||||
}
|
||||
81
tests/e2e/notebook.spec.ts
Normal file
81
tests/e2e/notebook.spec.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
// wxNotebook page-relayout e2e — regression coverage for the DOM-port fix in
|
||||
// wxwidgets/src/wasm/notebook.cpp (wxNotebook::WasmRelayoutSelectedPage).
|
||||
//
|
||||
// The "Scrolled" page wraps a wxScrolledWindow full of "Row N" labels, and the
|
||||
// app's wxEVT_NOTEBOOK_PAGE_CHANGED handler calls page->Fit() exactly like
|
||||
// KiCad's APPEARANCE_CONTROLS. Without the fix that collapses the scrolled
|
||||
// viewport and clip-paths the rows away after a tab round-trip; with it,
|
||||
// OnDomEvent re-asserts the geometry so the rows stay painted.
|
||||
import { test, expect, tryLoadApp } from './utils/fixtures';
|
||||
import { clickTab } from './utils/element-tracker';
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
wxDomControls?: Map<number, HTMLElement>;
|
||||
}
|
||||
}
|
||||
|
||||
// Whether each row label is genuinely PAINTED (not merely present in the DOM).
|
||||
// A bare getBoundingClientRect is not enough: when the scrolled window collapses
|
||||
// on a tab round-trip the rows keep their layout box but are removed by an
|
||||
// ancestor's clip-path. clip-path also affects hit-testing, so we sample points
|
||||
// down each row's box and treat it as visible only if the row (or its content)
|
||||
// is actually returned by elementsFromPoint. This is what makes the collapse
|
||||
// regression fail the assertion. (Mirrors kicad/appearance.spec.ts:rowsVisible.)
|
||||
async function rowsVisible(page: Page, labels: string[]): Promise<Record<string, boolean>> {
|
||||
return page.evaluate((wanted: string[]) => {
|
||||
const out: Record<string, boolean> = {};
|
||||
for (const w of wanted) out[w] = false;
|
||||
if (!window.wxDomControls) return out;
|
||||
const visible = (el: HTMLElement): boolean => {
|
||||
const cs = getComputedStyle(el);
|
||||
if (cs.display === 'none' || cs.visibility === 'hidden') return false;
|
||||
const r = el.getBoundingClientRect();
|
||||
if (r.width < 1 || r.height < 1) return false;
|
||||
const cx = r.left + r.width / 2;
|
||||
const ys = [r.top + 1, r.top + r.height / 2, r.bottom - 1];
|
||||
return ys.some((y) => {
|
||||
const hits = document.elementsFromPoint(cx, y);
|
||||
return hits.some((h) => h === el || el.contains(h));
|
||||
});
|
||||
};
|
||||
for (const [, el] of window.wxDomControls) {
|
||||
const txt = el.textContent || '';
|
||||
if (el.tagName === 'SPAN' && wanted.includes(txt) && visible(el)) out[txt] = true;
|
||||
}
|
||||
return out;
|
||||
}, labels);
|
||||
}
|
||||
|
||||
test.describe('DOM-port wxNotebook page relayout', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/standalone/notebook/notebook_test.html');
|
||||
expect(await tryLoadApp(page, 30000), 'notebook app should load').toBe(true);
|
||||
await page.waitForTimeout(500);
|
||||
});
|
||||
|
||||
test('scrolled page rows survive a tab round-trip', async ({ page, testLogger }) => {
|
||||
// First visit to the scrolled page: rows must be painted.
|
||||
expect(await clickTab(page, 'Scrolled'), 'switch to Scrolled').toBe(true);
|
||||
await page.waitForTimeout(300);
|
||||
await page.screenshot({ path: 'test-results/notebook-01-scrolled.png', fullPage: true });
|
||||
|
||||
let vis = await rowsVisible(page, ['Row 0', 'Row 1']);
|
||||
expect(vis['Row 0'], 'Row 0 visible on first visit').toBe(true);
|
||||
|
||||
// Round-trip: away to Plain, then back to Scrolled. The PAGE_CHANGED Fit()
|
||||
// collapses the scrolled child; WasmRelayoutSelectedPage must restore it.
|
||||
expect(await clickTab(page, 'Plain'), 'switch to Plain').toBe(true);
|
||||
await page.waitForTimeout(300);
|
||||
expect(await clickTab(page, 'Scrolled'), 'switch back to Scrolled').toBe(true);
|
||||
await page.waitForTimeout(300);
|
||||
await page.screenshot({ path: 'test-results/notebook-02-scrolled-again.png', fullPage: true });
|
||||
|
||||
vis = await rowsVisible(page, ['Row 0', 'Row 1']);
|
||||
expect(vis['Row 0'], 'Row 0 visible after tab round-trip').toBe(true);
|
||||
expect(vis['Row 1'], 'Row 1 visible after tab round-trip').toBe(true);
|
||||
|
||||
expect(testLogger.errors.filter((e) => !e.includes('favicon'))).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit 776957cd6fc2685a76e0de39c463be441070cea1
|
||||
Subproject commit 16339f805b213f7040033d7f410d249926e6df66
|
||||
Loading…
Reference in a new issue