fix(wasm): radio groups no longer merge into one
Bump wxwidgets to the %p->%zu radio-group name fix and add a standalone wxWidgets harness plus an e2e regression that asserts three radio groups in one window select independently. Build artifacts are gitignored; only radiogroups_test.cpp is tracked. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
39cb04b127
commit
13cac06778
6 changed files with 235 additions and 2 deletions
|
|
@ -146,6 +146,7 @@ all: minimal_test.html \
|
|||
$(S)/dialog/dialog_test.html \
|
||||
$(S)/timer/timer_test.html \
|
||||
$(S)/tree/tree_test.html \
|
||||
$(S)/radiogroups/radiogroups_test.html \
|
||||
$(S)/dataview/dataview_test.html \
|
||||
$(S)/htmlwin/htmlwin_test.html \
|
||||
$(S)/stc/stc_test.html \
|
||||
|
|
@ -344,6 +345,13 @@ $(S)/tree/tree_test.o: $(S)/tree/tree_test.cpp
|
|||
$(S)/tree/tree_test.html: $(S)/tree/tree_test.o $(WX_CORE_LIB) $(JS_FILES)
|
||||
$(CXX) $< $(LDFLAGS_NOGL) --pre-js $(JS) --shell-file $(HTML) -o $@
|
||||
|
||||
# Radio button groups test (no GL) - multiple wxRB_GROUP groups in one window
|
||||
$(S)/radiogroups/radiogroups_test.o: $(S)/radiogroups/radiogroups_test.cpp
|
||||
$(CXX) -c $(CXXFLAGS) $< -o $@
|
||||
|
||||
$(S)/radiogroups/radiogroups_test.html: $(S)/radiogroups/radiogroups_test.o $(WX_CORE_LIB) $(JS_FILES)
|
||||
$(CXX) $< $(LDFLAGS_NOGL) --pre-js $(JS) --shell-file $(HTML) -o $@
|
||||
|
||||
# DataView test (no GL)
|
||||
$(S)/dataview/dataview_test.o: $(S)/dataview/dataview_test.cpp
|
||||
$(CXX) -c $(CXXFLAGS) $< -o $@
|
||||
|
|
@ -641,6 +649,7 @@ grid: $(S)/grid/grid_test.html
|
|||
dialog: $(S)/dialog/dialog_test.html
|
||||
timer: $(S)/timer/timer_test.html
|
||||
tree: $(S)/tree/tree_test.html
|
||||
radiogroups: $(S)/radiogroups/radiogroups_test.html
|
||||
dataview: $(S)/dataview/dataview_test.html
|
||||
htmlwin: $(S)/htmlwin/htmlwin_test.html
|
||||
stc: $(S)/stc/stc_test.html
|
||||
|
|
@ -695,7 +704,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
|
||||
.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
|
||||
|
||||
# === Coroutine pthread variant — reproduces the KiCad Asyncify-fiber x pthreads crash ===
|
||||
# Same modal-free harness as `coroutine`, but compiled/linked with pthreads to match
|
||||
|
|
|
|||
143
tests/apps/standalone/radiogroups/radiogroups_test.cpp
Normal file
143
tests/apps/standalone/radiogroups/radiogroups_test.cpp
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
// wxRadioButton Group Test - exercises MULTIPLE radio-button groups in one window
|
||||
//
|
||||
// KiCad's Preferences "Mouse and Touchpad" panel lays out three independent
|
||||
// scroll-gesture radio groups (Zoom / Pan up-down / Pan left-right), each as a
|
||||
// row of [--, Ctrl, Shift, Alt] radios with the first button flagged wxRB_GROUP.
|
||||
//
|
||||
// In the WASM DOM port the per-group HTML "name" must be unique per group so the
|
||||
// browser keeps the groups independent. This harness reproduces that layout: it
|
||||
// pre-selects a DIFFERENT column in each of the three groups, so a correctly
|
||||
// grouped build shows exactly one selection PER ROW (three total). If the groups
|
||||
// collapse into one (the %p-empty-name bug), only one radio across all three
|
||||
// rows can stay checked.
|
||||
|
||||
#include "wx/wxprec.h"
|
||||
|
||||
#ifndef WX_PRECOMP
|
||||
#include "wx/wx.h"
|
||||
#endif
|
||||
|
||||
#include "wx/statline.h"
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
#include <emscripten/emscripten.h>
|
||||
#endif
|
||||
|
||||
class RadioGroupsFrame : public wxFrame
|
||||
{
|
||||
public:
|
||||
RadioGroupsFrame()
|
||||
: wxFrame(nullptr, wxID_ANY, "wxRadioButton Group Test",
|
||||
wxDefaultPosition, wxSize(640, 420))
|
||||
{
|
||||
wxPanel* panel = new wxPanel(this);
|
||||
wxBoxSizer* mainSizer = new wxBoxSizer(wxVERTICAL);
|
||||
|
||||
wxStaticText* desc = new wxStaticText(panel, wxID_ANY,
|
||||
"Three INDEPENDENT radio groups (mirrors KiCad's scroll-gesture panel).\n"
|
||||
"Each row pre-selects a different column, so a correct build shows one\n"
|
||||
"selection per row (three total). If the groups merge, only one survives.");
|
||||
mainSizer->Add(desc, 0, wxALL, 8);
|
||||
|
||||
// 5-column grid: row label + [--, Ctrl, Shift, Alt]
|
||||
wxFlexGridSizer* grid = new wxFlexGridSizer(5, 6, 12);
|
||||
|
||||
grid->Add(new wxStaticText(panel, wxID_ANY, ""));
|
||||
grid->Add(new wxStaticText(panel, wxID_ANY, "--"), 0, wxALIGN_CENTER);
|
||||
grid->Add(new wxStaticText(panel, wxID_ANY, "Ctrl"), 0, wxALIGN_CENTER);
|
||||
grid->Add(new wxStaticText(panel, wxID_ANY, "Shift"), 0, wxALIGN_CENTER);
|
||||
grid->Add(new wxStaticText(panel, wxID_ANY, "Alt"), 0, wxALIGN_CENTER);
|
||||
|
||||
// Row order and wxRB_GROUP flags MUST match the real panel: each row's
|
||||
// first radio starts a new group; the rest join it.
|
||||
addGroupRow(panel, grid, "Group A (Zoom)", m_groupA);
|
||||
addGroupRow(panel, grid, "Group B (Pan up/down)", m_groupB);
|
||||
addGroupRow(panel, grid, "Group C (Pan left/right)", m_groupC);
|
||||
|
||||
mainSizer->Add(grid, 0, wxALL, 12);
|
||||
|
||||
wxButton* check = new wxButton(panel, wxID_ANY, "Check selections");
|
||||
check->Bind(wxEVT_BUTTON, &RadioGroupsFrame::OnCheck, this);
|
||||
mainSizer->Add(check, 0, wxLEFT | wxBOTTOM, 12);
|
||||
|
||||
mainSizer->Add(new wxStaticText(panel, wxID_ANY, "Log"), 0, wxLEFT, 12);
|
||||
m_log = new wxTextCtrl(panel, wxID_ANY, "", wxDefaultPosition, wxSize(-1, 110),
|
||||
wxTE_MULTILINE | wxTE_READONLY);
|
||||
mainSizer->Add(m_log, 1, wxEXPAND | wxALL, 12);
|
||||
|
||||
panel->SetSizer(mainSizer);
|
||||
|
||||
CreateStatusBar();
|
||||
SetStatusText("radio groups test app started");
|
||||
Log("radio groups test app started");
|
||||
|
||||
// Pre-select a DIFFERENT column per group (distinct, like the real
|
||||
// defaults: Zoom=--, Pan up/down=Shift, Pan left/right=Ctrl). Deferred
|
||||
// until after the frame is shown, mirroring how real panels apply
|
||||
// settings in TransferDataToWindow(); SetValue() from the ctor runs
|
||||
// before the DOM radio nodes are realized and would not paint.
|
||||
CallAfter([this]
|
||||
{
|
||||
m_groupA[COL_NONE]->SetValue(true);
|
||||
m_groupB[COL_SHIFT]->SetValue(true);
|
||||
m_groupC[COL_CTRL]->SetValue(true);
|
||||
Log("Initial selection: A=--, B=Shift, C=Ctrl (one per group)");
|
||||
});
|
||||
}
|
||||
|
||||
private:
|
||||
enum Col { COL_NONE = 0, COL_CTRL, COL_SHIFT, COL_ALT, COL_COUNT };
|
||||
|
||||
void addGroupRow(wxWindow* parent, wxFlexGridSizer* grid, const wxString& label,
|
||||
wxRadioButton* (&out)[COL_COUNT])
|
||||
{
|
||||
grid->Add(new wxStaticText(parent, wxID_ANY, label), 0, wxALIGN_CENTER_VERTICAL);
|
||||
|
||||
for (int c = 0; c < COL_COUNT; ++c)
|
||||
{
|
||||
// First radio of the row starts a fresh group.
|
||||
long style = (c == 0) ? wxRB_GROUP : 0;
|
||||
out[c] = new wxRadioButton(parent, wxID_ANY, "", wxDefaultPosition,
|
||||
wxDefaultSize, style);
|
||||
grid->Add(out[c], 0, wxALIGN_CENTER);
|
||||
}
|
||||
}
|
||||
|
||||
void OnCheck(wxCommandEvent&)
|
||||
{
|
||||
Log(wxString::Format("Group A: %s", selectedCol(m_groupA)));
|
||||
Log(wxString::Format("Group B: %s", selectedCol(m_groupB)));
|
||||
Log(wxString::Format("Group C: %s", selectedCol(m_groupC)));
|
||||
}
|
||||
|
||||
wxString selectedCol(wxRadioButton* (&group)[COL_COUNT])
|
||||
{
|
||||
static const char* names[COL_COUNT] = { "--", "Ctrl", "Shift", "Alt" };
|
||||
for (int c = 0; c < COL_COUNT; ++c)
|
||||
{
|
||||
if (group[c]->GetValue())
|
||||
return names[c];
|
||||
}
|
||||
return "(none!)";
|
||||
}
|
||||
|
||||
void Log(const wxString& msg) { m_log->AppendText(msg + "\n"); }
|
||||
|
||||
wxRadioButton* m_groupA[COL_COUNT];
|
||||
wxRadioButton* m_groupB[COL_COUNT];
|
||||
wxRadioButton* m_groupC[COL_COUNT];
|
||||
wxTextCtrl* m_log;
|
||||
};
|
||||
|
||||
class RadioGroupsApp : public wxApp
|
||||
{
|
||||
public:
|
||||
virtual bool OnInit() override
|
||||
{
|
||||
RadioGroupsFrame* frame = new RadioGroupsFrame();
|
||||
frame->Show();
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
wxIMPLEMENT_APP(RadioGroupsApp);
|
||||
BIN
tests/baseline-screenshots/radiogroups-01-loaded.png
Normal file
BIN
tests/baseline-screenshots/radiogroups-01-loaded.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
BIN
tests/baseline-screenshots/radiogroups-02-selections.png
Normal file
BIN
tests/baseline-screenshots/radiogroups-02-selections.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
81
tests/e2e/radiogroups.spec.ts
Normal file
81
tests/e2e/radiogroups.spec.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
// wxRadioButton group exclusivity tests.
|
||||
//
|
||||
// Regression coverage for the WASM DOM-port bug where every wxRadioButton got
|
||||
// the same HTML `name` ("wxrb-", because `%p` rendered empty in wxString::Format),
|
||||
// collapsing all radio groups in a window into one. The harness lays out three
|
||||
// independent groups, each pre-selecting a different column, so a correct build
|
||||
// keeps three distinct group `name`s with exactly one checked radio per group.
|
||||
import { test, expect, tryLoadApp } from './utils/fixtures';
|
||||
|
||||
const URL = '/standalone/radiogroups/radiogroups_test.html';
|
||||
|
||||
// Collect, per HTML radio group `name`, how many radios it has and how many are checked.
|
||||
async function readGroups(page: import('@playwright/test').Page) {
|
||||
return page.evaluate(() => {
|
||||
const radios = Array.from(
|
||||
document.querySelectorAll<HTMLInputElement>('input[type=radio]')
|
||||
).filter(r => r.offsetParent !== null); // visible only
|
||||
|
||||
const byName: Record<string, { total: number; checked: number }> = {};
|
||||
for (const r of radios) {
|
||||
(byName[r.name] ??= { total: 0, checked: 0 }).total++;
|
||||
if (r.checked) byName[r.name].checked++;
|
||||
}
|
||||
return {
|
||||
radioCount: radios.length,
|
||||
distinctNames: Array.from(new Set(radios.map(r => r.name))),
|
||||
byName,
|
||||
totalChecked: radios.filter(r => r.checked).length,
|
||||
details: radios.map((r, i) => ({ i, name: r.name, checked: r.checked })),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
test.describe('wxRadioButton groups', () => {
|
||||
test('radio groups app loads without errors', async ({ page, testLogger }) => {
|
||||
await page.goto(URL);
|
||||
const loaded = await tryLoadApp(page);
|
||||
await page.screenshot({ path: 'test-results/radiogroups-01-loaded.png', fullPage: true });
|
||||
expect(loaded, 'radio groups app should load').toBe(true);
|
||||
expect(testLogger.errors.filter(e => !e.includes('favicon'))).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('selecting in one group does not disturb the others', async ({ page }) => {
|
||||
await page.goto(URL);
|
||||
expect(await tryLoadApp(page), 'app should load').toBe(true);
|
||||
|
||||
// 3 groups x 4 columns = 12 radios.
|
||||
await page.waitForFunction(
|
||||
() => document.querySelectorAll('input[type=radio]').length >= 12,
|
||||
undefined,
|
||||
{ timeout: 10_000 }
|
||||
);
|
||||
|
||||
// Structural proof of the fix: three separate HTML exclusivity groups, not
|
||||
// one merged group. (Pre-fix every radio shares name "wxrb-" => length 1.)
|
||||
const before = await readGroups(page);
|
||||
expect(before.radioCount, 'should render 3 groups x 4 columns').toBe(12);
|
||||
expect(before.distinctNames,
|
||||
`expected 3 group names, got ${JSON.stringify(before.distinctNames)}`).toHaveLength(3);
|
||||
|
||||
// Behavioural proof: click one radio in each group, on a different column.
|
||||
// Groups are contiguous in DOM order: A=0..3, B=4..7, C=8..11.
|
||||
const radios = page.locator('input[type=radio]');
|
||||
await radios.nth(1).click(); // Group A, Ctrl
|
||||
await radios.nth(6).click(); // Group B, Shift
|
||||
await radios.nth(11).click(); // Group C, Alt
|
||||
await page.waitForTimeout(100);
|
||||
|
||||
const after = await readGroups(page);
|
||||
await page.screenshot({ path: 'test-results/radiogroups-02-selections.png', fullPage: true });
|
||||
|
||||
// Each group keeps exactly its own selection. Pre-fix, the three clicks land
|
||||
// in one merged group so only the last survives => totalChecked === 1.
|
||||
expect(after.totalChecked, 'one selection per group => 3 total').toBe(3);
|
||||
for (const [name, counts] of Object.entries(after.byName)) {
|
||||
expect(counts.checked, `group ${name} should keep exactly one checked`).toBe(1);
|
||||
}
|
||||
// Spot-check the exact radios we clicked are the ones checked.
|
||||
expect(after.details.filter(d => d.checked).map(d => d.i)).toEqual([1, 6, 11]);
|
||||
});
|
||||
});
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit 5d40228ac4c446c0f57c2e8d2d2bea8db827d45d
|
||||
Subproject commit cca8eed9f965cd771c0cdfb31af692a27ff80eb4
|
||||
Loading…
Reference in a new issue