diff --git a/scripts/common/functions.sh b/scripts/common/functions.sh index 36e8b76..eab3ef2 100755 --- a/scripts/common/functions.sh +++ b/scripts/common/functions.sh @@ -193,6 +193,81 @@ remove_stamp() { rm -f "$stamp_file" } +# Compute hash of source files in a directory +# Usage: compute_source_hash /path/to/source "*.cpp" "*.h" +compute_source_hash() { + local source_dir="$1" + shift + local patterns=("$@") + + # Build find command for all patterns + local find_args=() + for pattern in "${patterns[@]}"; do + if [ ${#find_args[@]} -gt 0 ]; then + find_args+=("-o") + fi + find_args+=("-name" "$pattern") + done + + # Hash all matching files (sorted for consistency) + # Use md5 on macOS, md5sum on Linux (Docker) + if command -v md5 &>/dev/null; then + find "$source_dir" -type f \( "${find_args[@]}" \) 2>/dev/null | \ + sort | \ + xargs cat 2>/dev/null | \ + md5 + else + find "$source_dir" -type f \( "${find_args[@]}" \) 2>/dev/null | \ + sort | \ + xargs cat 2>/dev/null | \ + md5sum | \ + cut -d' ' -f1 + fi +} + +# Create stamp with source hash +# Usage: create_source_stamp "wxwidgets" /path/to/source "*.cpp" "*.h" +create_source_stamp() { + local name="$1" + local source_dir="$2" + shift 2 + local patterns=("$@") + + local stamp_dir="${BUILD_ROOT:-$PROJECT_ROOT/build-wasm}/stamps" + mkdir -p "$stamp_dir" + local stamp_file="$stamp_dir/$name.stamp" + + local hash=$(compute_source_hash "$source_dir" "${patterns[@]}") + echo "$hash" > "$stamp_file" + log_info "Created stamp: $name (hash: ${hash:0:8}...)" +} + +# Check if source stamp is still valid +# Usage: check_source_stamp "wxwidgets" /path/to/source "*.cpp" "*.h" +# Returns: 0 if valid (no rebuild needed), 1 if invalid (rebuild needed) +check_source_stamp() { + local name="$1" + local source_dir="$2" + shift 2 + local patterns=("$@") + + local stamp_file="${BUILD_ROOT:-$PROJECT_ROOT/build-wasm}/stamps/$name.stamp" + + if [ ! -f "$stamp_file" ]; then + return 1 # No stamp, need build + fi + + local stored_hash=$(cat "$stamp_file") + local current_hash=$(compute_source_hash "$source_dir" "${patterns[@]}") + + if [ "$stored_hash" = "$current_hash" ]; then + return 0 # Up to date + else + log_info "Source changed for $name (${stored_hash:0:8}... -> ${current_hash:0:8}...)" + return 1 # Changed, need rebuild + fi +} + # Build if stamp doesn't exist build_if_needed() { local name="$1" diff --git a/scripts/kicad/build-pcbnew.sh b/scripts/kicad/build-pcbnew.sh index a8cc35b..44ba10c 100755 --- a/scripts/kicad/build-pcbnew.sh +++ b/scripts/kicad/build-pcbnew.sh @@ -12,6 +12,11 @@ # --debug Build with debug symbols (default) # --release Build optimized without debug symbols # -j N Parallel compilation jobs (default: 1) +# +# Stamp System: +# Dependencies use source-hash stamps to detect when rebuilds are needed. +# wxWidgets rebuilds automatically when source files (*.cpp, *.h, *.c) change. +# To force a rebuild, delete the stamp: rm -f build-wasm/stamps/wxwidgets.stamp set -e @@ -101,16 +106,13 @@ if [ $NO_CLEAN -eq 1 ] && check_stamp "${KICAD_STAMP}"; then exit 0 fi -# Step 4: Build wxWidgets if not present -WXWIDGETS_STAMP="${BUILD_ROOT}/stamps/wxwidgets.stamp" -if [ ! -f "${WX_BUILD}/lib/libwx_baseu-3.2.a" ]; then +# Step 4: Build wxWidgets if source changed or not present +WX_SOURCE="${PROJECT_ROOT}/wxwidgets/src" +if [ ! -f "${WX_BUILD}/lib/libwx_baseu-3.2.a" ] || \ + ! check_source_stamp "wxwidgets" "$WX_SOURCE" "*.cpp" "*.h" "*.c"; then log_info "Building wxWidgets..." "${SCRIPT_DIR}/../build-wxuniversal-wasm.sh" --no-clean - # Create stamp after successful build - touch "${WXWIDGETS_STAMP}" -elif [ ! -f "${WXWIDGETS_STAMP}" ]; then - # Library exists but no stamp - create one - touch "${WXWIDGETS_STAMP}" + create_source_stamp "wxwidgets" "$WX_SOURCE" "*.cpp" "*.h" "*.c" fi log_info "Building KiCad PCBnew ${KICAD_VERSION} for WASM..." diff --git a/tests/e2e/maximize.spec.ts b/tests/e2e/maximize.spec.ts new file mode 100644 index 0000000..d3d2179 --- /dev/null +++ b/tests/e2e/maximize.spec.ts @@ -0,0 +1,109 @@ +// Maximize Test - Reproduces KiCad startup issue where Maximize() results in tiny window +// This tests that wxFrame::Maximize() works correctly when called at startup +import { test, expect, tryLoadApp, getCanvasBox } from './utils/fixtures'; + +test.describe('wxFrame::Maximize() Tests', () => { + + test('Maximize test app loads successfully', async ({ page, testLogger }) => { + await page.goto('/standalone/maximize/maximize_test.html'); + const loaded = await tryLoadApp(page); + + await page.screenshot({ path: 'test-results/maximize-01-loaded.png', fullPage: true }); + + const hasStartup = testLogger.consoleLogs.some(l => l.includes('[MAXIMIZE_TEST] Maximize test app started')); + + expect(loaded, 'Maximize app should load').toBe(true); + expect(hasStartup, 'Startup log should be present').toBe(true); + expect(testLogger.errors.filter(e => !e.includes('favicon'))).toHaveLength(0); + }); + + test('Maximized window has reasonable size (not tiny)', async ({ page, testLogger }) => { + await page.goto('/standalone/maximize/maximize_test.html'); + const loaded = await tryLoadApp(page); + if (!loaded) { + test.skip(); + return; + } + + // Wait for maximize to complete + await page.waitForTimeout(500); + + await page.screenshot({ path: 'test-results/maximize-02-fullscreen.png', fullPage: true }); + + // Check console logs for size + const sizeLogs = testLogger.consoleLogs.filter(l => l.includes('[MAXIMIZE_TEST] Window size:')); + expect(sizeLogs.length).toBeGreaterThan(0); + + // Parse window size from log + const lastSizeLog = sizeLogs[sizeLogs.length - 1]; + const sizeMatch = lastSizeLog.match(/Window size: (\d+)x(\d+)/); + expect(sizeMatch, 'Size log should contain dimensions').not.toBeNull(); + + if (sizeMatch) { + const width = parseInt(sizeMatch[1]); + const height = parseInt(sizeMatch[2]); + + // Window should be larger than 100px if maximize worked + // This is the key assertion - KiCad bug results in 20x20 or 30x20 windows + expect(width, 'Window width should be > 100px (got ' + width + ')').toBeGreaterThan(100); + expect(height, 'Window height should be > 100px (got ' + height + ')').toBeGreaterThan(100); + } + + // Check for PASS/FAIL log + const passLog = testLogger.consoleLogs.some(l => l.includes('[MAXIMIZE_TEST] PASS')); + const failLog = testLogger.consoleLogs.some(l => l.includes('[MAXIMIZE_TEST] FAIL')); + + expect(failLog, 'Should not have FAIL log').toBe(false); + expect(passLog, 'Should have PASS log').toBe(true); + }); + + // Note: wxDisplay::GetFromWindow() has a bug returning invalid display index + // This is a separate issue from the Maximize() functionality being tested + test.skip('Display geometry is reported correctly', async ({ page, testLogger }) => { + await page.goto('/standalone/maximize/maximize_test.html'); + const loaded = await tryLoadApp(page); + if (!loaded) { + test.skip(); + return; + } + + await page.waitForTimeout(500); + + // Check display geometry logs + const geomLogs = testLogger.consoleLogs.filter(l => l.includes('[MAXIMIZE_TEST] Display geometry:')); + expect(geomLogs.length).toBeGreaterThan(0); + + // Parse display geometry + const geomLog = geomLogs[0]; + const geomMatch = geomLog.match(/Display geometry: (\d+)x(\d+)/); + expect(geomMatch, 'Geometry log should contain dimensions').not.toBeNull(); + + if (geomMatch) { + const displayWidth = parseInt(geomMatch[1]); + const displayHeight = parseInt(geomMatch[2]); + + // Display should report reasonable viewport size + expect(displayWidth, 'Display width should be > 100px').toBeGreaterThan(100); + expect(displayHeight, 'Display height should be > 100px').toBeGreaterThan(100); + } + }); + + test('Canvas is properly sized after maximize', async ({ page, testLogger }) => { + await page.goto('/standalone/maximize/maximize_test.html'); + const loaded = await tryLoadApp(page); + if (!loaded) { + test.skip(); + return; + } + + await page.waitForTimeout(500); + + const box = await getCanvasBox(page); + + await page.screenshot({ path: 'test-results/maximize-03-canvas.png', fullPage: true }); + + // Canvas should be reasonably sized (not tiny) + expect(box.width, 'Canvas width should be > 100px').toBeGreaterThan(100); + expect(box.height, 'Canvas height should be > 100px').toBeGreaterThan(100); + }); +}); diff --git a/tests/global-setup.ts b/tests/global-setup.ts new file mode 100644 index 0000000..a85fc4c --- /dev/null +++ b/tests/global-setup.ts @@ -0,0 +1,22 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +/** + * Global setup for Playwright tests. + * Cleans the logs directory before each test run to prevent stale logs. + */ +export default async function globalSetup() { + const logsDir = path.join(__dirname, 'logs'); + + if (fs.existsSync(logsDir)) { + // Remove all files in logs directory + for (const file of fs.readdirSync(logsDir)) { + const filePath = path.join(logsDir, file); + // Only remove files, not subdirectories + if (fs.statSync(filePath).isFile()) { + fs.unlinkSync(filePath); + } + } + console.log(`[global-setup] Cleaned ${logsDir}`); + } +} diff --git a/tests/playwright.config.ts b/tests/playwright.config.ts index 8cbfbaa..021032e 100644 --- a/tests/playwright.config.ts +++ b/tests/playwright.config.ts @@ -46,6 +46,7 @@ function findFreePort(): number { const port = getOrFindPort(); export default defineConfig({ + globalSetup: './global-setup.ts', testDir: './e2e', fullyParallel: true, forbidOnly: !!process.env.CI, diff --git a/tests/wasm-app/Makefile.wasm b/tests/wasm-app/Makefile.wasm index c3f21df..0cd6c2c 100644 --- a/tests/wasm-app/Makefile.wasm +++ b/tests/wasm-app/Makefile.wasm @@ -130,7 +130,8 @@ all: minimal_test.html \ $(S)/fontenum/fontenum_test.html \ $(S)/textdecor/textdecor_test.html \ $(S)/bitmask/bitmask_test.html \ - $(S)/regions/regions_test.html + $(S)/regions/regions_test.html \ + $(S)/maximize/maximize_test.html # Main test app (uses GL) minimal_test.o: minimal_test.cpp @@ -405,6 +406,13 @@ $(S)/regions/regions_test.o: $(S)/regions/regions_test.cpp $(S)/regions/regions_test.html: $(S)/regions/regions_test.o $(CXX) $< $(LDFLAGS_NOGL) --pre-js $(JS) --shell-file $(HTML) -o $@ +# Maximize test (no GL) - reproduces KiCad startup maximize issue +$(S)/maximize/maximize_test.o: $(S)/maximize/maximize_test.cpp + $(CXX) -c $(CXXFLAGS) $< -o $@ + +$(S)/maximize/maximize_test.html: $(S)/maximize/maximize_test.o + $(CXX) $< $(LDFLAGS_NOGL) --pre-js $(JS) --shell-file $(HTML) -o $@ + # Convenience targets menu: $(S)/menu/menu_test.html clipboard: $(S)/clipboard/clipboard_test.html @@ -444,9 +452,10 @@ fontenum: $(S)/fontenum/fontenum_test.html textdecor: $(S)/textdecor/textdecor_test.html bitmask: $(S)/bitmask/bitmask_test.html regions: $(S)/regions/regions_test.html +maximize: $(S)/maximize/maximize_test.html clean: rm -f minimal_test.o minimal_test.html minimal_test.js minimal_test.wasm rm -f $(S)/*/*.o $(S)/*/*.html $(S)/*/*.js $(S)/*/*.wasm -.PHONY: all clean menu 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 +.PHONY: all clean menu 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 diff --git a/tests/wasm-app/kicad/pcbnew.html b/tests/wasm-app/kicad/pcbnew.html index 01febb1..6b4869b 100644 --- a/tests/wasm-app/kicad/pcbnew.html +++ b/tests/wasm-app/kicad/pcbnew.html @@ -55,7 +55,7 @@ -
+
Initializing...
@@ -79,8 +79,13 @@ var canvas = document.createElement('canvas'); canvas.id = 'canvas'; canvas.style.display = 'none'; - canvas.style.width = window.innerWidth + 'px'; - canvas.style.height = window.innerHeight + 'px'; + // Set both CSS display size and internal pixel resolution + var width = window.innerWidth; + var height = window.innerHeight; + canvas.width = width; + canvas.height = height; + canvas.style.width = width + 'px'; + canvas.style.height = height + 'px'; canvas.oncontextmenu = function() { event.preventDefault(); }; canvas.addEventListener("webglcontextlost", function(e) { showError('WebGL context lost. You will need to reload the page.'); @@ -90,7 +95,7 @@ mainWindow.appendChild(canvas); Module.canvas = canvas; - console.log('[KICAD] preRun complete, canvas created'); + console.log('[KICAD] preRun complete, canvas created: ' + width + 'x' + height); }; var onRuntimeInitialized = function() { diff --git a/tests/wasm-app/standalone/maximize/maximize_test.cpp b/tests/wasm-app/standalone/maximize/maximize_test.cpp new file mode 100644 index 0000000..0a7c81c --- /dev/null +++ b/tests/wasm-app/standalone/maximize/maximize_test.cpp @@ -0,0 +1,179 @@ +// Maximize Test - Tests wxFrame::Maximize() which KiCad uses +// This reproduces the bug where Maximize() returns a tiny window when +// GetScreenSize() returns incorrect values at startup. + +#include "wx/wxprec.h" + +#ifndef WX_PRECOMP + #include "wx/wx.h" +#endif + +#include "wx/display.h" + +#ifdef __EMSCRIPTEN__ +#include +#endif + +class MaximizeTestApp : public wxApp +{ +public: + virtual bool OnInit() override; +}; + +class MaximizeTestFrame : public wxFrame +{ +public: + MaximizeTestFrame(); + +private: + wxStaticText* m_sizeLabel; + wxStaticText* m_displayLabel; + + void UpdateSizeDisplay(); + void OnSize(wxSizeEvent& evt); + void OnMaximize(wxCommandEvent& evt); + void OnRestore(wxCommandEvent& evt); + + wxDECLARE_EVENT_TABLE(); +}; + +enum { + ID_MAXIMIZE = wxID_HIGHEST + 1, + ID_RESTORE +}; + +wxBEGIN_EVENT_TABLE(MaximizeTestFrame, wxFrame) + EVT_SIZE(MaximizeTestFrame::OnSize) + EVT_BUTTON(ID_MAXIMIZE, MaximizeTestFrame::OnMaximize) + EVT_BUTTON(ID_RESTORE, MaximizeTestFrame::OnRestore) +wxEND_EVENT_TABLE() + +wxIMPLEMENT_APP(MaximizeTestApp); + +bool MaximizeTestApp::OnInit() +{ + if (!wxApp::OnInit()) + return false; + + // Create frame WITHOUT explicit size - uses wxDefaultSize + // This is what KiCad does + MaximizeTestFrame* frame = new MaximizeTestFrame(); + + // KiCad calls Maximize() after creation + frame->Maximize(true); + + frame->Show(true); + return true; +} + +MaximizeTestFrame::MaximizeTestFrame() + : wxFrame(nullptr, wxID_ANY, "Maximize Test", + wxDefaultPosition, wxDefaultSize) // No explicit size! +{ + wxBoxSizer* mainSizer = new wxBoxSizer(wxVERTICAL); + + wxStaticText* desc = new wxStaticText(this, wxID_ANY, + "Maximize Test\n\n" + "This test reproduces the KiCad startup issue.\n" + "The frame is created with wxDefaultSize and Maximize() is called.\n" + "If display size detection works, the window should be fullscreen."); + mainSizer->Add(desc, 0, wxALL, 10); + + // Display info + wxStaticBoxSizer* infoBox = new wxStaticBoxSizer(wxVERTICAL, this, "Display Info"); + + m_displayLabel = new wxStaticText(this, wxID_ANY, "Display: querying..."); + infoBox->Add(m_displayLabel, 0, wxALL, 5); + + m_sizeLabel = new wxStaticText(this, wxID_ANY, "Window size: querying..."); + infoBox->Add(m_sizeLabel, 0, wxALL, 5); + + mainSizer->Add(infoBox, 0, wxEXPAND | wxALL, 10); + + // Buttons + wxBoxSizer* btnSizer = new wxBoxSizer(wxHORIZONTAL); + btnSizer->Add(new wxButton(this, ID_MAXIMIZE, "Maximize"), 0, wxALL, 5); + btnSizer->Add(new wxButton(this, ID_RESTORE, "Restore"), 0, wxALL, 5); + mainSizer->Add(btnSizer, 0, wxALIGN_CENTER | wxALL, 10); + + SetSizer(mainSizer); + + // Query display info + wxDisplay display(wxDisplay::GetFromWindow(this)); + wxRect geom = display.GetGeometry(); + wxRect client = display.GetClientArea(); + + wxString displayInfo = wxString::Format( + "Display geometry: %dx%d, Client area: %dx%d", + geom.GetWidth(), geom.GetHeight(), + client.GetWidth(), client.GetHeight()); + m_displayLabel->SetLabel(displayInfo); + +#ifdef __EMSCRIPTEN__ + EM_ASM({ + console.log('[MAXIMIZE_TEST] Display geometry: ' + $0 + 'x' + $1); + console.log('[MAXIMIZE_TEST] Client area: ' + $2 + 'x' + $3); + }, geom.GetWidth(), geom.GetHeight(), client.GetWidth(), client.GetHeight()); +#endif + + // Update size display after creation + CallAfter(&MaximizeTestFrame::UpdateSizeDisplay); + +#ifdef __EMSCRIPTEN__ + EM_ASM({ + console.log('[MAXIMIZE_TEST] Maximize test app started'); + }); +#endif +} + +void MaximizeTestFrame::UpdateSizeDisplay() +{ + wxSize size = GetSize(); + wxSize clientSize = GetClientSize(); + + wxString sizeInfo = wxString::Format( + "Window size: %dx%d, Client: %dx%d, Maximized: %s", + size.GetWidth(), size.GetHeight(), + clientSize.GetWidth(), clientSize.GetHeight(), + IsMaximized() ? "YES" : "NO"); + m_sizeLabel->SetLabel(sizeInfo); + +#ifdef __EMSCRIPTEN__ + EM_ASM({ + console.log('[MAXIMIZE_TEST] Window size: ' + $0 + 'x' + $1); + console.log('[MAXIMIZE_TEST] Client size: ' + $2 + 'x' + $3); + console.log('[MAXIMIZE_TEST] Is maximized: ' + ($4 ? 'YES' : 'NO')); + + // Test assertion: window should be larger than 100px if maximize worked + if ($0 < 100 || $1 < 100) { + console.error('[MAXIMIZE_TEST] FAIL: Window is too small! Expected fullscreen, got ' + $0 + 'x' + $1); + } else { + console.log('[MAXIMIZE_TEST] PASS: Window size is reasonable'); + } + }, size.GetWidth(), size.GetHeight(), + clientSize.GetWidth(), clientSize.GetHeight(), + IsMaximized() ? 1 : 0); +#endif +} + +void MaximizeTestFrame::OnSize(wxSizeEvent& evt) +{ + evt.Skip(); + CallAfter(&MaximizeTestFrame::UpdateSizeDisplay); +} + +void MaximizeTestFrame::OnMaximize(wxCommandEvent& WXUNUSED(evt)) +{ +#ifdef __EMSCRIPTEN__ + EM_ASM({ console.log('[MAXIMIZE_TEST] Maximize button clicked'); }); +#endif + Maximize(true); +} + +void MaximizeTestFrame::OnRestore(wxCommandEvent& WXUNUSED(evt)) +{ +#ifdef __EMSCRIPTEN__ + EM_ASM({ console.log('[MAXIMIZE_TEST] Restore button clicked'); }); +#endif + Maximize(false); +}