wasm: validate kicad hi-dpi scaling
This commit is contained in:
parent
6fa6c9207f
commit
a49ed49d5d
12 changed files with 490 additions and 30 deletions
|
|
@ -146,7 +146,8 @@ all: minimal_test.html \
|
|||
$(S)/maximize/maximize_test.html \
|
||||
$(S)/earlysize/earlysize_test.html \
|
||||
$(S)/threadpool/threadpool_test.html \
|
||||
$(S)/logerror/logerror_test.html
|
||||
$(S)/logerror/logerror_test.html \
|
||||
$(S)/retinascale/retinascale_test.html
|
||||
|
||||
# Main test app (uses GL)
|
||||
minimal_test.o: minimal_test.cpp
|
||||
|
|
@ -493,8 +494,17 @@ earlysize: $(S)/earlysize/earlysize_test.html
|
|||
threadpool: $(S)/threadpool/threadpool_test.html
|
||||
logerror: $(S)/logerror/logerror_test.html
|
||||
|
||||
# Retina/HiDPI scaling test
|
||||
$(S)/retinascale/retinascale_test.o: $(S)/retinascale/retinascale_test.cpp
|
||||
$(CXX) -c $(CXXFLAGS) $< -o $@
|
||||
|
||||
$(S)/retinascale/retinascale_test.html: $(S)/retinascale/retinascale_test.o $(WX_CORE_LIB)
|
||||
$(CXX) $< $(LDFLAGS_NOGL) --pre-js $(JS) --shell-file $(HTML) -o $@
|
||||
|
||||
retinascale: $(S)/retinascale/retinascale_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 maximize earlysize threadpool logerror
|
||||
.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 earlysize threadpool logerror retinascale
|
||||
|
|
|
|||
|
|
@ -79,11 +79,10 @@
|
|||
var canvas = document.createElement('canvas');
|
||||
canvas.id = 'canvas';
|
||||
canvas.style.display = 'none';
|
||||
// Set both CSS display size and internal pixel resolution
|
||||
// wx.js owns the backing-store size via setWindowRect(); keep the HTML
|
||||
// shell responsible only for the CSS size.
|
||||
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(); };
|
||||
|
|
|
|||
109
tests/apps/standalone/retinascale/retinascale_test.cpp
Normal file
109
tests/apps/standalone/retinascale/retinascale_test.cpp
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
// Retina/HiDPI Scaling Test
|
||||
// Verifies that bitmaps render at correct logical pixel sizes regardless of
|
||||
// display scale factor. Icons should appear the same size on 1x and 2x displays.
|
||||
|
||||
#include "wx/wx.h"
|
||||
#include "wx/dcmemory.h"
|
||||
|
||||
// Create a colored icon bitmap at the given size with scaleFactor=1.0
|
||||
// (matching how KiCad creates toolbar icons via KiScaledBitmap)
|
||||
wxBitmap CreateTestIcon(const wxColour& color, int size)
|
||||
{
|
||||
wxBitmap bmp(size, size);
|
||||
wxMemoryDC dc(bmp);
|
||||
|
||||
dc.SetBackground(wxBrush(wxColour(240, 240, 240)));
|
||||
dc.Clear();
|
||||
|
||||
dc.SetPen(wxPen(*wxBLACK, 1));
|
||||
dc.SetBrush(wxBrush(color));
|
||||
dc.DrawRectangle(1, 1, size - 2, size - 2);
|
||||
|
||||
// Draw a diagonal cross to make scaling visible
|
||||
dc.SetPen(wxPen(color, 2));
|
||||
dc.DrawLine(2, 2, size - 2, size - 2);
|
||||
dc.DrawLine(size - 2, 2, 2, size - 2);
|
||||
|
||||
return bmp;
|
||||
}
|
||||
|
||||
class RetinaScaleFrame : public wxFrame
|
||||
{
|
||||
public:
|
||||
RetinaScaleFrame() : wxFrame(nullptr, wxID_ANY, "Retina Scale Test",
|
||||
wxDefaultPosition, wxSize(800, 600))
|
||||
{
|
||||
// Log scale factor info
|
||||
double contentSF = GetContentScaleFactor();
|
||||
double dpiSF = GetDPIScaleFactor();
|
||||
wxLogMessage("ContentScaleFactor: %.1f, DPIScaleFactor: %.1f", contentSF, dpiSF);
|
||||
|
||||
wxPanel* panel = new wxPanel(this);
|
||||
wxBoxSizer* mainSizer = new wxBoxSizer(wxVERTICAL);
|
||||
|
||||
// Create a toolbar with icons at different sizes
|
||||
wxToolBar* toolbar = CreateToolBar();
|
||||
toolbar->SetName("toolbar");
|
||||
|
||||
wxBitmap icon16 = CreateTestIcon(*wxRED, 16);
|
||||
wxBitmap icon24 = CreateTestIcon(*wxBLUE, 24);
|
||||
wxBitmap icon32 = CreateTestIcon(*wxGREEN, 32);
|
||||
|
||||
toolbar->AddTool(1001, "Icon16", icon16, "16px icon");
|
||||
toolbar->AddTool(1002, "Icon24", icon24, "24px icon");
|
||||
toolbar->AddTool(1003, "Icon32", icon32, "32px icon");
|
||||
toolbar->Realize();
|
||||
|
||||
// Info label
|
||||
wxString info = wxString::Format(
|
||||
"Scale: content=%.1f dpi=%.1f\n"
|
||||
"Icons should be 16, 24, 32 logical pixels wide respectively.\n"
|
||||
"Below: bitmap buttons with the same icons for comparison.",
|
||||
contentSF, dpiSF);
|
||||
|
||||
wxStaticText* label = new wxStaticText(panel, wxID_ANY, info);
|
||||
label->SetName("info");
|
||||
mainSizer->Add(label, 0, wxALL, 10);
|
||||
|
||||
// Bitmap buttons row for comparison
|
||||
wxBoxSizer* btnSizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
|
||||
wxBitmapButton* btn16 = new wxBitmapButton(panel, wxID_ANY, icon16);
|
||||
btn16->SetName("btn16");
|
||||
btnSizer->Add(btn16, 0, wxALL, 5);
|
||||
|
||||
wxBitmapButton* btn24 = new wxBitmapButton(panel, wxID_ANY, icon24);
|
||||
btn24->SetName("btn24");
|
||||
btnSizer->Add(btn24, 0, wxALL, 5);
|
||||
|
||||
wxBitmapButton* btn32 = new wxBitmapButton(panel, wxID_ANY, icon32);
|
||||
btn32->SetName("btn32");
|
||||
btnSizer->Add(btn32, 0, wxALL, 5);
|
||||
|
||||
mainSizer->Add(btnSizer, 0, wxLEFT, 5);
|
||||
|
||||
// Reference line: a static text showing expected sizes
|
||||
wxStaticText* refLabel = new wxStaticText(panel, wxID_ANY,
|
||||
"If scaling is correct, the red icon above should be ~16px, "
|
||||
"blue ~24px, green ~32px (logical pixels).");
|
||||
refLabel->SetName("reference");
|
||||
mainSizer->Add(refLabel, 0, wxALL, 10);
|
||||
|
||||
panel->SetSizer(mainSizer);
|
||||
|
||||
wxLogMessage("RetinaScaleFrame created successfully");
|
||||
}
|
||||
};
|
||||
|
||||
class RetinaScaleApp : public wxApp
|
||||
{
|
||||
public:
|
||||
bool OnInit() override
|
||||
{
|
||||
RetinaScaleFrame* frame = new RetinaScaleFrame();
|
||||
frame->Show(true);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
wxIMPLEMENT_APP(RetinaScaleApp);
|
||||
16
tests/e2e/retinascale.spec.ts
Normal file
16
tests/e2e/retinascale.spec.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
// Retina/HiDPI Scaling Test - Verifies bitmaps render at correct logical pixel sizes
|
||||
import { test, expect, tryLoadApp } from './utils/fixtures';
|
||||
|
||||
test.describe('Retina Scale Tests', () => {
|
||||
|
||||
test('Bitmap icons render at correct logical size', async ({ page, testLogger }) => {
|
||||
await page.goto('/standalone/retinascale/retinascale_test.html');
|
||||
const loaded = await tryLoadApp(page);
|
||||
|
||||
await page.screenshot({ path: 'test-results/retinascale-01-loaded.png', fullPage: true });
|
||||
|
||||
expect(loaded, 'Retina scale test app should load').toBe(true);
|
||||
expect(testLogger.errors.filter(e => !e.includes('favicon'))).toHaveLength(0);
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -1,3 +1,6 @@
|
|||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import type { Page } from '@playwright/test';
|
||||
import { test, expect } from './fixtures';
|
||||
import { clickByLabel } from '../e2e/utils/element-tracker';
|
||||
|
||||
|
|
@ -5,44 +8,352 @@ import { clickByLabel } from '../e2e/utils/element-tracker';
|
|||
* PCBnew WASM E2E Tests
|
||||
*/
|
||||
|
||||
const PCBNEW_REFERENCE = path.resolve(__dirname, '../wizard-04-finish-headless.png');
|
||||
const REFERENCE_REGIONS = [
|
||||
{ name: 'header', x: 0, y: 0, width: 1280, height: 90, maxDiffRatio: 0.12, maxMeanChannelDiff: 12 },
|
||||
] as const;
|
||||
|
||||
type CanvasMetrics = {
|
||||
dpr: number;
|
||||
mainCanvas: null | {
|
||||
width: number;
|
||||
height: number;
|
||||
rectWidth: number;
|
||||
rectHeight: number;
|
||||
};
|
||||
glCanvas: null | {
|
||||
id: string;
|
||||
width: number;
|
||||
height: number;
|
||||
rectWidth: number;
|
||||
rectHeight: number;
|
||||
viewport: number[] | null;
|
||||
};
|
||||
};
|
||||
|
||||
type RegistryMetrics = {
|
||||
elementStats: null | {
|
||||
total: number;
|
||||
byType: Record<string, number>;
|
||||
};
|
||||
renderedStats: null | {
|
||||
total: number;
|
||||
byType: Record<string, number>;
|
||||
};
|
||||
toolbars: Array<{
|
||||
id: string;
|
||||
typeName: string;
|
||||
screenX: number;
|
||||
screenY: number;
|
||||
width: number;
|
||||
height: number;
|
||||
label: string;
|
||||
name: string;
|
||||
}>;
|
||||
auiParts: Array<{
|
||||
id: string;
|
||||
subType: string;
|
||||
label: string;
|
||||
screenX: number;
|
||||
screenY: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}>;
|
||||
};
|
||||
|
||||
type ReferenceComparison = {
|
||||
name: string;
|
||||
actualWidth: number;
|
||||
actualHeight: number;
|
||||
referenceWidth: number;
|
||||
referenceHeight: number;
|
||||
diffPixels: number;
|
||||
diffRatio: number;
|
||||
meanChannelDiff: number;
|
||||
};
|
||||
|
||||
type ReferenceRegion = typeof REFERENCE_REGIONS[number];
|
||||
|
||||
async function compareToReference(
|
||||
page: Page,
|
||||
actualPng: Buffer,
|
||||
referencePath: string,
|
||||
region: ReferenceRegion
|
||||
): Promise<ReferenceComparison> {
|
||||
const referencePng = fs.readFileSync(referencePath);
|
||||
|
||||
return page.evaluate(async ({ actualBase64, referenceBase64, crop }) => {
|
||||
const loadImage = async (base64: string): Promise<HTMLImageElement> => {
|
||||
const image = new Image();
|
||||
image.src = `data:image/png;base64,${base64}`;
|
||||
await image.decode();
|
||||
return image;
|
||||
};
|
||||
|
||||
const [actual, reference] = await Promise.all([
|
||||
loadImage(actualBase64),
|
||||
loadImage(referenceBase64),
|
||||
]);
|
||||
|
||||
if (actual.width !== reference.width || actual.height !== reference.height) {
|
||||
return {
|
||||
name: crop.name,
|
||||
actualWidth: actual.width,
|
||||
actualHeight: actual.height,
|
||||
referenceWidth: reference.width,
|
||||
referenceHeight: reference.height,
|
||||
diffPixels: Number.POSITIVE_INFINITY,
|
||||
diffRatio: Number.POSITIVE_INFINITY,
|
||||
meanChannelDiff: Number.POSITIVE_INFINITY,
|
||||
};
|
||||
}
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = crop.width;
|
||||
canvas.height = crop.height;
|
||||
|
||||
const context = canvas.getContext('2d', { willReadFrequently: true });
|
||||
|
||||
if (!context) {
|
||||
throw new Error('2D canvas context unavailable for screenshot comparison');
|
||||
}
|
||||
|
||||
context.drawImage(actual, crop.x, crop.y, crop.width, crop.height, 0, 0, crop.width, crop.height);
|
||||
const actualData = context.getImageData(0, 0, canvas.width, canvas.height).data;
|
||||
|
||||
context.clearRect(0, 0, canvas.width, canvas.height);
|
||||
context.drawImage(reference, crop.x, crop.y, crop.width, crop.height, 0, 0, crop.width, crop.height);
|
||||
const referenceData = context.getImageData(0, 0, canvas.width, canvas.height).data;
|
||||
|
||||
let diffPixels = 0;
|
||||
let totalChannelDiff = 0;
|
||||
|
||||
for (let i = 0; i < actualData.length; i += 4) {
|
||||
const dr = Math.abs(actualData[i] - referenceData[i]);
|
||||
const dg = Math.abs(actualData[i + 1] - referenceData[i + 1]);
|
||||
const db = Math.abs(actualData[i + 2] - referenceData[i + 2]);
|
||||
const da = Math.abs(actualData[i + 3] - referenceData[i + 3]);
|
||||
const maxDiff = Math.max(dr, dg, db, da);
|
||||
|
||||
totalChannelDiff += dr + dg + db + da;
|
||||
|
||||
if (maxDiff > 16) {
|
||||
diffPixels += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: crop.name,
|
||||
actualWidth: actual.width,
|
||||
actualHeight: actual.height,
|
||||
referenceWidth: reference.width,
|
||||
referenceHeight: reference.height,
|
||||
diffPixels,
|
||||
diffRatio: diffPixels / (canvas.width * canvas.height),
|
||||
meanChannelDiff: totalChannelDiff / actualData.length,
|
||||
};
|
||||
}, {
|
||||
actualBase64: actualPng.toString('base64'),
|
||||
referenceBase64: referencePng.toString('base64'),
|
||||
crop: region,
|
||||
});
|
||||
}
|
||||
|
||||
async function getCanvasMetrics(page: Page): Promise<CanvasMetrics> {
|
||||
return page.evaluate(() => {
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const mainCanvas = document.querySelector('#canvas') as HTMLCanvasElement | null;
|
||||
const glCanvas =
|
||||
Array.from(document.querySelectorAll('[id^="glcanvas-"]'))
|
||||
.map((canvas) => canvas as HTMLCanvasElement)
|
||||
.find((canvas) => {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const style = window.getComputedStyle(canvas);
|
||||
return style.display !== 'none' && rect.width > 0 && rect.height > 0;
|
||||
}) ??
|
||||
document.querySelector('[id^="glcanvas-"]') as HTMLCanvasElement | null;
|
||||
|
||||
const mainRect = mainCanvas?.getBoundingClientRect();
|
||||
const glRect = glCanvas?.getBoundingClientRect();
|
||||
const gl =
|
||||
glCanvas?.getContext('webgl2') ||
|
||||
glCanvas?.getContext('webgl');
|
||||
const viewport = gl ? Array.from(gl.getParameter(gl.VIEWPORT) as Int32Array | number[]) : null;
|
||||
|
||||
return {
|
||||
dpr,
|
||||
mainCanvas: mainCanvas && mainRect ? {
|
||||
width: mainCanvas.width,
|
||||
height: mainCanvas.height,
|
||||
rectWidth: mainRect.width,
|
||||
rectHeight: mainRect.height,
|
||||
} : null,
|
||||
glCanvas: glCanvas && glRect ? {
|
||||
id: glCanvas.id,
|
||||
width: glCanvas.width,
|
||||
height: glCanvas.height,
|
||||
rectWidth: glRect.width,
|
||||
rectHeight: glRect.height,
|
||||
viewport,
|
||||
} : null,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function getRegistryMetrics(page: Page): Promise<RegistryMetrics> {
|
||||
return page.evaluate(() => {
|
||||
const registry = window.wxElementRegistry;
|
||||
|
||||
if (!registry) {
|
||||
return {
|
||||
elementStats: null,
|
||||
renderedStats: null,
|
||||
toolbars: [],
|
||||
auiParts: [],
|
||||
};
|
||||
}
|
||||
|
||||
const allElements = registry.findAll({ visible: true });
|
||||
const toolbars = allElements
|
||||
.filter((element) => /ToolBar/.test(element.typeName))
|
||||
.map((element) => ({
|
||||
id: element.id,
|
||||
typeName: element.typeName,
|
||||
screenX: element.screenX,
|
||||
screenY: element.screenY,
|
||||
width: element.width,
|
||||
height: element.height,
|
||||
label: element.label,
|
||||
name: element.name,
|
||||
}));
|
||||
|
||||
const auiParts = registry.findAllRendered
|
||||
? registry.findAllRendered({ elementType: 'auipart' })
|
||||
.map((part) => ({
|
||||
id: part.id,
|
||||
subType: part.subType,
|
||||
label: part.label,
|
||||
screenX: part.screenX,
|
||||
screenY: part.screenY,
|
||||
width: part.width,
|
||||
height: part.height,
|
||||
}))
|
||||
: [];
|
||||
|
||||
return {
|
||||
elementStats: registry.getStats(),
|
||||
renderedStats: registry.getRenderedStats ? registry.getRenderedStats() : null,
|
||||
toolbars,
|
||||
auiParts,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
test.describe('PCBnew WASM', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/kicad/pcbnew.html');
|
||||
});
|
||||
|
||||
test('click through setup wizard to load PCBnew', async ({ page, testLogger }) => {
|
||||
// Wait for main canvas to be visible (KiCad takes time to initialize)
|
||||
test('click through setup wizard to load PCBnew', async ({ page }) => {
|
||||
await expect(page.locator('#canvas')).toBeVisible({ timeout: 90000 });
|
||||
await page.waitForFunction(() => !!window.wxElementRegistry, null, { timeout: 90000 });
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Screenshot initial wizard state
|
||||
await page.screenshot({ path: 'test-results/wizard-00-initial.png' });
|
||||
await page.screenshot({ path: 'test-results/wizard-00-initial.png', scale: 'device' });
|
||||
|
||||
// Click through wizard - try Next >, then Finish if not found
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
let clicked = await clickByLabel(page, 'Next >');
|
||||
if (clicked) {
|
||||
// Continue to next page
|
||||
} else {
|
||||
// Try Finish button
|
||||
|
||||
if (!clicked) {
|
||||
clicked = await clickByLabel(page, 'Finish');
|
||||
|
||||
if (clicked) {
|
||||
await page.waitForTimeout(500);
|
||||
await page.screenshot({ path: `test-results/wizard-${String(i).padStart(2, '0')}-finish.png` });
|
||||
break;
|
||||
} else {
|
||||
break;
|
||||
await page.screenshot({
|
||||
path: `test-results/wizard-${String(i).padStart(2, '0')}-finish.png`,
|
||||
scale: 'device'
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
await page.waitForTimeout(500);
|
||||
await page.screenshot({ path: `test-results/wizard-${String(i).padStart(2, '0')}.png` });
|
||||
await page.screenshot({
|
||||
path: `test-results/wizard-${String(i).padStart(2, '0')}.png`,
|
||||
scale: 'device'
|
||||
});
|
||||
}
|
||||
|
||||
// Wait for PCBnew to fully load
|
||||
await page.waitForTimeout(2000);
|
||||
await page.screenshot({ path: 'test-results/pcbnew-loaded.png' });
|
||||
const metrics = await getCanvasMetrics(page);
|
||||
const registryMetrics = await getRegistryMetrics(page);
|
||||
|
||||
expect(metrics.dpr).toBeGreaterThan(1);
|
||||
expect(metrics.mainCanvas).not.toBeNull();
|
||||
expect(metrics.glCanvas).not.toBeNull();
|
||||
expect(registryMetrics.toolbars.length).toBeGreaterThanOrEqual(4);
|
||||
|
||||
if (!metrics.mainCanvas || !metrics.glCanvas) {
|
||||
throw new Error('KiCad canvases not initialized');
|
||||
}
|
||||
|
||||
expect(Math.round(metrics.mainCanvas.rectWidth * metrics.dpr)).toBe(metrics.mainCanvas.width);
|
||||
expect(Math.round(metrics.mainCanvas.rectHeight * metrics.dpr)).toBe(metrics.mainCanvas.height);
|
||||
expect(metrics.glCanvas.rectWidth).toBeGreaterThan(800);
|
||||
expect(metrics.glCanvas.rectHeight).toBeGreaterThan(500);
|
||||
expect(Math.round(metrics.glCanvas.rectWidth * metrics.dpr)).toBe(metrics.glCanvas.width);
|
||||
expect(Math.round(metrics.glCanvas.rectHeight * metrics.dpr)).toBe(metrics.glCanvas.height);
|
||||
|
||||
const viewport = metrics.glCanvas.viewport;
|
||||
expect(viewport).not.toBeNull();
|
||||
|
||||
if (!viewport) {
|
||||
throw new Error('WebGL viewport unavailable');
|
||||
}
|
||||
|
||||
expect(viewport[2]).toBe(metrics.glCanvas.width);
|
||||
expect(viewport[3]).toBe(metrics.glCanvas.height);
|
||||
|
||||
const verticalToolbars = registryMetrics.toolbars.filter((toolbar) => toolbar.height > 100);
|
||||
expect(verticalToolbars).toHaveLength(2);
|
||||
|
||||
for (const toolbar of verticalToolbars) {
|
||||
expect(toolbar.width).toBeLessThanOrEqual(40);
|
||||
}
|
||||
|
||||
const appearancePane = registryMetrics.auiParts.find((part) =>
|
||||
part.subType === 'content' && part.label === 'Appearance'
|
||||
);
|
||||
expect(appearancePane).toBeTruthy();
|
||||
|
||||
if (!appearancePane) {
|
||||
throw new Error('Appearance pane metrics unavailable');
|
||||
}
|
||||
|
||||
expect(appearancePane.width).toBeGreaterThanOrEqual(200);
|
||||
expect(appearancePane.width).toBeLessThanOrEqual(240);
|
||||
|
||||
await page.evaluate(() => {
|
||||
document.documentElement.style.cursor = 'none';
|
||||
document.body.style.cursor = 'none';
|
||||
});
|
||||
|
||||
const cssScreenshot = await page.screenshot({
|
||||
path: 'test-results/pcbnew-loaded-css.png',
|
||||
scale: 'css'
|
||||
});
|
||||
for (const region of REFERENCE_REGIONS) {
|
||||
const reference = await compareToReference(page, cssScreenshot, PCBNEW_REFERENCE, region);
|
||||
|
||||
expect(reference.actualWidth).toBe(reference.referenceWidth);
|
||||
expect(reference.actualHeight).toBe(reference.referenceHeight);
|
||||
expect(reference.diffRatio, `${reference.name} diff ratio`).toBeLessThan(region.maxDiffRatio);
|
||||
expect(reference.meanChannelDiff, `${reference.name} mean channel diff`).toBeLessThan(region.maxMeanChannelDiff);
|
||||
}
|
||||
|
||||
await page.screenshot({ path: 'test-results/pcbnew-loaded.png', scale: 'device' });
|
||||
|
||||
// Verify PCBnew loaded
|
||||
const canvasCount = await page.locator('canvas').count();
|
||||
expect(canvasCount).toBeGreaterThan(0);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
"build-wasm": "cd apps && make -f Makefile.wasm",
|
||||
"serve": "npx serve apps -p 8080 -c ../serve.json",
|
||||
"setup:kicad": "./scripts/setup-kicad-wasm.sh",
|
||||
"test:kicad": "npm run setup:kicad && playwright test --config=playwright-kicad.config.ts --project=firefox",
|
||||
"test:kicad": "npm run setup:kicad && playwright test --config=playwright-kicad.config.ts --project=firefox --headed",
|
||||
"test:kicad:headed": "npm run setup:kicad && playwright test --config=playwright-kicad.config.ts --project=chromium --headed"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -43,7 +43,16 @@ fi
|
|||
|
||||
# wxWidgets WASM JavaScript glue code (defines JS functions called from WASM)
|
||||
echo "Copying wxWidgets WASM glue code..."
|
||||
cp "$PROJECT_ROOT/wxwidgets/build/wasm/wx.js" "$KICAD_TEST/"
|
||||
if [ -f "$OUTPUT_DIR/wx.js" ]; then
|
||||
cp "$OUTPUT_DIR/wx.js" "$KICAD_TEST/"
|
||||
else
|
||||
if docker compose -f "$PROJECT_ROOT/docker/docker-compose.yml" cp \
|
||||
kicad-wasm-builder:/workspace/build-wasm/wxwidgets/build/wasm/wx.js "$KICAD_TEST/" 2>/dev/null; then
|
||||
:
|
||||
else
|
||||
cp "$PROJECT_ROOT/wxwidgets/build/wasm/wx.js" "$KICAD_TEST/"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "KiCad WASM files copied to $KICAD_TEST"
|
||||
ls -lh "$KICAD_TEST"
|
||||
|
|
|
|||
BIN
tests/wizard-04-finish-headless.png
Normal file
BIN
tests/wizard-04-finish-headless.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 120 KiB |
Loading…
Reference in a new issue