Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions tests/apps/Makefile.wasm
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ CXXFLAGS += $(EH_FLAGS)
# - js_writeTextToClipboard, js_readTextFromClipboard, js_clipboardHasText, js_clearClipboard: for clipboard
# - js_enumerateFonts: for font enumeration via Local Font Access API
BASE_LDFLAGS = $(EH_FLAGS) -sALLOW_MEMORY_GROWTH -sERROR_ON_UNDEFINED_SYMBOLS=0 \
-s "EXPORTED_RUNTIME_METHODS=['HEAPU8','HEAP8','HEAP32','ccall']" \
-s "EXPORTED_RUNTIME_METHODS=['HEAPU8','HEAP8','HEAP32','ccall','stringToNewUTF8']" \
-sASYNCIFY=1 \
-sASYNCIFY_STACK_SIZE=65536 \
-sASYNCIFY_IMPORTS=['startModal','js_writeTextToClipboard','js_readTextFromClipboard','js_clipboardHasText','js_clearClipboard','js_enumerateFonts']
Expand Down Expand Up @@ -213,7 +213,8 @@ all: minimal_test.html \
$(S)/stattext-ellipsize/stattext-ellipsize_test.html \
$(S)/tooltip-lifetime/tooltip-lifetime_test.html \
$(S)/tooltip-toolbar/tooltip-toolbar_test.html \
$(S)/warp-pointer/warp-pointer_test.html
$(S)/warp-pointer/warp-pointer_test.html \
$(S)/selevent-clientdata/selevent-clientdata_test.html

# Main test app
minimal_test.o: minimal_test.cpp
Expand Down Expand Up @@ -270,6 +271,19 @@ $(S)/warp-pointer/warp-pointer_test.html: $(S)/warp-pointer/warp-pointer_test.o

warp-pointer: $(S)/warp-pointer/warp-pointer_test.html

# --- wxWidgets DOM-port vs native parity reproductions (parity-audit) ---------

# Selection command events must carry per-item client data (choice/listbox/
# combobox) — else KiCad's Track & Via Properties dialog null-derefs.
$(S)/selevent-clientdata/selevent-clientdata_test.o: $(S)/selevent-clientdata/selevent-clientdata_test.cpp
$(CXX) -c $(CXXFLAGS) $< -o $@

$(S)/selevent-clientdata/selevent-clientdata_test.html: $(S)/selevent-clientdata/selevent-clientdata_test.o $(WX_CORE_LIB) $(JS_FILES)
$(CXX) $< $(LDFLAGS_NOGL) --pre-js $(JS) --shell-file $(HTML) -o $@

selevent-clientdata: $(S)/selevent-clientdata/selevent-clientdata_test.html
.PHONY: selevent-clientdata

# Menu test (no GL)
$(S)/menu/menu_test.o: $(S)/menu/menu_test.cpp
$(CXX) -c $(CXXFLAGS) $< -o $@
Expand Down
137 changes: 137 additions & 0 deletions tests/apps/standalone/selevent-clientdata/selevent-clientdata_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// Selection command events must carry per-item client data (DOM port).
//
// Bug (src/wasm/{choice,listbox,combobox}.cpp, OnDomEvent):
//
// wxCommandEvent event(wxEVT_CHOICE, GetId());
// event.SetInt(m_selection);
// event.SetString(GetString(m_selection));
// event.SetEventObject(this);
// HandleWindowEvent(event); // <-- no client data EVER attached
//
// The hand-rolled wxEVT_CHOICE / wxEVT_LISTBOX / wxEVT_COMBOBOX events never call
// InitCommandEventWithItems(), so event.GetClientData()/GetClientObject() always
// return NULL even when the picked item was appended WITH client data. Native
// ports route through wxControlWithItemsBase::SendSelectionChangedEvent(), which
// copies the selected item's client object/data into the event
// (src/common/ctrlsub.cpp).
//
// This is a real crash in KiCad: pcbnew's Track & Via Properties dialog does
// static_cast<VIA_DIMENSION*>(aEvent.GetClientData())->m_Diameter
// with no null guard (dialog_track_via_properties.cpp onViaSelect) — a normal
// "pick a predefined via size" action then traps the WASM module.
//
// The repro appends items WITH typed client data (wxStringClientData), then the
// spec fires a real DOM 'change' on each control's element. The bound handler
// reads the event's client object and self-reports:
//
// RED (bug present): GetClientObject() == NULL -> [REPRO] <ctrl>: FAIL
// GREEN (fixed): GetClientObject() == the picked item's data -> PASS

#include "wx/wxprec.h"
#ifndef WX_PRECOMP
#include "wx/wx.h"
#endif

#include "wx/combobox.h"

#ifdef __EMSCRIPTEN__
#include <emscripten/emscripten.h>
#endif

static void Repro(const wxString& line)
{
#ifdef __EMSCRIPTEN__
EM_ASM({ console.log('[REPRO] ' + UTF8ToString($0)); },
(const char *)line.utf8_str());
#else
wxPrintf("[REPRO] %s\n", line);
#endif
}

// Expected client-data payload for item index n: "DATA_A", "DATA_B", ...
static wxString ExpectedData(int n)
{
return wxString::Format("DATA_%c", (char)('A' + n));
}

// Read the selection event's client OBJECT and report PASS/FAIL for `name`.
static void CheckEvent(const wxString& name, wxCommandEvent& evt)
{
const int sel = evt.GetInt();
wxClientData *obj = evt.GetClientObject();
if (!obj)
{
Repro(name + ": FAIL (GetClientObject()==null, sel=" +
wxString::Format("%d", sel) + ")");
return;
}

wxStringClientData *sd = static_cast<wxStringClientData *>(obj);
const wxString got = sd->GetData();
const wxString want = ExpectedData(sel);
if (got == want)
Repro(name + ": PASS (" + got + ")");
else
Repro(name + ": FAIL (got '" + got + "' want '" + want + "')");
}

class ReproFrame : public wxFrame
{
public:
ReproFrame();

private:
void OnChoice(wxCommandEvent &e) { CheckEvent("choice_clientdata", e); }
void OnListBox(wxCommandEvent &e) { CheckEvent("listbox_clientdata", e); }
void OnComboBox(wxCommandEvent &e) { CheckEvent("combobox_clientdata", e); }
};

static void Fill(wxControlWithItems *ctrl)
{
ctrl->Append("Alpha", new wxStringClientData("DATA_A"));
ctrl->Append("Beta", new wxStringClientData("DATA_B"));
ctrl->Append("Gamma", new wxStringClientData("DATA_C"));
}

ReproFrame::ReproFrame()
: wxFrame(nullptr, wxID_ANY, "selection client-data repro",
wxDefaultPosition, wxSize(360, 320))
{
wxBoxSizer *sizer = new wxBoxSizer(wxVERTICAL);

wxChoice *choice = new wxChoice(this, wxID_ANY);
Fill(choice);
choice->Bind(wxEVT_CHOICE, &ReproFrame::OnChoice, this);
sizer->Add(choice, 0, wxALL | wxEXPAND, 8);

wxListBox *listbox = new wxListBox(this, wxID_ANY);
Fill(listbox);
listbox->Bind(wxEVT_LISTBOX, &ReproFrame::OnListBox, this);
sizer->Add(listbox, 1, wxALL | wxEXPAND, 8);

// editable combobox -> <input list=...> in the DOM port
wxComboBox *combo = new wxComboBox(this, wxID_ANY, "");
Fill(combo);
combo->Bind(wxEVT_COMBOBOX, &ReproFrame::OnComboBox, this);
sizer->Add(combo, 0, wxALL | wxEXPAND, 8);

SetSizer(sizer);

#ifdef __EMSCRIPTEN__
CallAfter([] { EM_ASM({ console.log('[REPRO] selevent ready'); }); });
#endif
}

class ReproApp : public wxApp
{
public:
bool OnInit() override
{
if (!wxApp::OnInit())
return false;
(new ReproFrame())->Show(true);
return true;
}
};

wxIMPLEMENT_APP(ReproApp);
67 changes: 67 additions & 0 deletions tests/e2e/parity-audit.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { test, expect, tryLoadApp } from './utils/fixtures';

// Red-green reproductions for wxWidgets DOM-port vs native (wxGTK) parity gaps
// found in the parity audit. Each standalone app
// (tests/apps/standalone/<name>/) drives the exact buggy path and self-reports a
// "[REPRO] <name>: PASS/FAIL" line (or exposes live DOM state); the test is RED
// while the bug is present and GREEN after the wasm-layer fix.

// Find the "[REPRO] <name>: ..." line emitted by a repro app.
function reproLine(logs: string[], name: string): string | undefined {
return logs.find((l) => l.includes(`[REPRO] ${name}:`));
}

async function waitReady(testLogger: { consoleLogs: string[] }, marker: string) {
await expect
.poll(() => testLogger.consoleLogs.some((l) => l.includes(marker)), {
timeout: 30000,
message: `repro app should emit "${marker}"`,
})
.toBe(true);
}

test.describe('wxWidgets DOM-port parity reproductions', () => {
// C-1 (Critical): wxEVT_CHOICE/LISTBOX/COMBOBOX are hand-rolled and never call
// InitCommandEventWithItems(), so event.GetClientObject() is always NULL even
// when the item was appended WITH client data. KiCad's Track & Via Properties
// dialog static_cast<VIA_DIMENSION*>(aEvent.GetClientData())->... then traps
// the WASM module on a normal selection. The app appends typed client data and
// the handler checks the event carries it.
test('selection events carry per-item client data (choice/listbox/combobox)', async ({
page,
testLogger,
}) => {
await page.goto('/standalone/selevent-clientdata/selevent-clientdata_test.html');
expect(await tryLoadApp(page, 30000), 'repro app should load').toBe(true);
await waitReady(testLogger, '[REPRO] selevent ready');

// wxChoice -> <select> (single): pick "Beta" (index 1, client data DATA_B).
await page.locator('select:not([multiple])').first().selectOption({ index: 1 });
await expect
.poll(() => reproLine(testLogger.consoleLogs, 'choice_clientdata') ?? '', {
timeout: 10000,
message: 'wxChoice selection event must carry the item client object',
})
.toContain('PASS');

// wxListBox -> <select multiple>: pick index 1.
await page.locator('select[multiple]').first().selectOption({ index: 1 });
await expect
.poll(() => reproLine(testLogger.consoleLogs, 'listbox_clientdata') ?? '', {
timeout: 10000,
message: 'wxListBox selection event must carry the item client object',
})
.toContain('PASS');

// wxComboBox -> <input list=...>: commit "Beta" (index 1).
const combo = page.locator('input[list]').first();
await combo.fill('Beta');
await combo.dispatchEvent('change');
await expect
.poll(() => reproLine(testLogger.consoleLogs, 'combobox_clientdata') ?? '', {
timeout: 10000,
message: 'wxComboBox selection event must carry the item client object',
})
.toContain('PASS');
});
});
Loading
Loading