wx wasm: grid cell editors typeable (wxwidgets → d32535fefb) + gating e2e
tests/kicad/grid-editors-typing.spec.ts: Symbol Properties fields grid — types into the canvas-drawn STC Value editor and the wxComboCtrl-based Footprint editor, commits via OK, saves and reads both values back. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MKyjp6N4BeYb1qZVJemPFN
This commit is contained in:
parent
9575498c9e
commit
c80622a860
2 changed files with 333 additions and 1 deletions
332
tests/kicad/grid-editors-typing.spec.ts
Normal file
332
tests/kicad/grid-editors-typing.spec.ts
Normal file
|
|
@ -0,0 +1,332 @@
|
|||
/**
|
||||
* Grid cell editors in a wasm dialog must be typeable — the Symbol Properties
|
||||
* fields grid as the specimen (fields_grid_table.cpp mixes all three editor
|
||||
* kinds):
|
||||
*
|
||||
* - Value → GRID_CELL_STC_EDITOR: canvas-drawn wxStyledTextCtrl. Text
|
||||
* only arrives as wxEVT_CHAR, which this port derives from the
|
||||
* browser 'keypress'. The canvas keydown callback is a plain
|
||||
* (non-promising) entry, so its wx job is deferred and the
|
||||
* callback used to answer preventDefault=true by default —
|
||||
* cancelling keypress and every typed character (app.cpp
|
||||
* KeyCallback / evtloop.cpp wxWasmRunOnDispatchContext).
|
||||
* - Footprint → GRID_CELL_FPID_EDITOR on wxComboCtrl: its inner <input>
|
||||
* stayed at the 10px creation size because
|
||||
* wxComboCtrlBase::PositionTextCtrl bails when
|
||||
* GetHandle() is NULL, which the wasm port hard-coded.
|
||||
*
|
||||
* Both are asserted end-to-end: type, commit, OK, save, read the file back.
|
||||
*/
|
||||
|
||||
import type { Page } from "@playwright/test";
|
||||
import { test, expect } from "./fixtures";
|
||||
|
||||
const SCH = `(kicad_sch
|
||||
\t(version 20231120)
|
||||
\t(generator "eeschema")
|
||||
\t(uuid "cccc0000-0000-0000-0000-000000000011")
|
||||
\t(paper "A4")
|
||||
\t(lib_symbols
|
||||
\t\t(symbol "Device:R"
|
||||
\t\t\t(pin_numbers (hide yes))
|
||||
\t\t\t(pin_names (offset 0))
|
||||
\t\t\t(exclude_from_sim no) (in_bom yes) (on_board yes)
|
||||
\t\t\t(property "Reference" "R" (at 2.032 0 90) (effects (font (size 1.27 1.27))))
|
||||
\t\t\t(property "Value" "R" (at 0 0 90) (effects (font (size 1.27 1.27))))
|
||||
\t\t\t(symbol "R_0_1"
|
||||
\t\t\t\t(rectangle (start -1.016 -2.54) (end 1.016 2.54)
|
||||
\t\t\t\t\t(stroke (width 0.254) (type default)) (fill (type none)))
|
||||
\t\t\t)
|
||||
\t\t\t(symbol "R_1_1"
|
||||
\t\t\t\t(pin passive line (at 0 3.81 270) (length 1.27)
|
||||
\t\t\t\t\t(name "~" (effects (font (size 1.27 1.27))))
|
||||
\t\t\t\t\t(number "1" (effects (font (size 1.27 1.27)))))
|
||||
\t\t\t\t(pin passive line (at 0 -3.81 90) (length 1.27)
|
||||
\t\t\t\t\t(name "~" (effects (font (size 1.27 1.27))))
|
||||
\t\t\t\t\t(number "2" (effects (font (size 1.27 1.27)))))
|
||||
\t\t\t)
|
||||
\t\t)
|
||||
\t)
|
||||
\t(symbol
|
||||
\t\t(lib_id "Device:R")
|
||||
\t\t(at 100 100 0)
|
||||
\t\t(unit 1)
|
||||
\t\t(exclude_from_sim no) (in_bom yes) (on_board yes) (dnp no)
|
||||
\t\t(uuid "dddd0000-0000-0000-0000-000000000012")
|
||||
\t\t(property "Reference" "R1" (at 102 98 0) (effects (font (size 1.27 1.27)) (justify left)))
|
||||
\t\t(property "Value" "10k" (at 102 101 0) (effects (font (size 1.27 1.27)) (justify left)))
|
||||
\t\t(instances (project "grid-editors" (path "/cccc0000-0000-0000-0000-000000000011" (reference "R1") (unit 1))))
|
||||
\t)
|
||||
)`;
|
||||
|
||||
const SYMBOL_UUID = "dddd0000-0000-0000-0000-000000000012";
|
||||
const DIR = "/home/kicad/documents";
|
||||
const FILE = `${DIR}/grid-editors.kicad_sch`;
|
||||
|
||||
type Mod = {
|
||||
kicadOpenFile(p: string): unknown;
|
||||
kicadOpenFileBusy(): boolean;
|
||||
kicadCollabGetPos(id: string): string;
|
||||
kicadCollabGetViewport(): string;
|
||||
kicadSaveSchematic(p: string): unknown;
|
||||
};
|
||||
type FS = {
|
||||
mkdirTree(p: string): void;
|
||||
writeFile(p: string, d: string): void;
|
||||
readFile(p: string, o: { encoding: "utf8" }): string;
|
||||
};
|
||||
|
||||
async function bootAndOpen(page: Page): Promise<void> {
|
||||
await page.goto("/kicad/eeschema.html");
|
||||
await expect(page.locator("#canvas")).toBeVisible({ timeout: 120000 });
|
||||
await page.waitForFunction(
|
||||
() => {
|
||||
const m = (window as unknown as { Module?: Partial<Mod> }).Module;
|
||||
return (
|
||||
typeof m?.kicadOpenFile === "function" &&
|
||||
typeof m?.kicadCollabGetPos === "function" &&
|
||||
typeof m?.kicadSaveSchematic === "function"
|
||||
);
|
||||
},
|
||||
null,
|
||||
{ timeout: 120000 },
|
||||
);
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
!!window.wxElementRegistry &&
|
||||
window.wxElementRegistry
|
||||
.findAll({ visible: true })
|
||||
.some((e) => /Frame$/.test(e.typeName) || (e.name || "").endsWith("Frame")),
|
||||
null,
|
||||
{ timeout: 120000 },
|
||||
);
|
||||
await page.evaluate(
|
||||
({ sch, dir, file }) => {
|
||||
const w = window as unknown as { FS: FS; Module: Mod };
|
||||
try {
|
||||
w.FS.mkdirTree(dir);
|
||||
} catch {
|
||||
/* exists */
|
||||
}
|
||||
w.FS.writeFile(file, sch);
|
||||
w.Module.kicadOpenFile(file);
|
||||
},
|
||||
{ sch: SCH, dir: DIR, file: FILE },
|
||||
);
|
||||
await expect
|
||||
.poll(() => page.evaluate(() => (window.Module as unknown as Mod).kicadOpenFileBusy()), {
|
||||
timeout: 120000,
|
||||
intervals: [250],
|
||||
})
|
||||
.toBe(false);
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
page.evaluate(
|
||||
(id) => (window.Module as unknown as Mod).kicadCollabGetPos(id),
|
||||
SYMBOL_UUID,
|
||||
),
|
||||
{ timeout: 30000, intervals: [250] },
|
||||
)
|
||||
.toMatch(/^-?[\d.]+,-?[\d.]+$/);
|
||||
}
|
||||
|
||||
/** Screen-space centre of the fixture symbol (viewport math as
|
||||
* quasimodal-strand.spec.ts). */
|
||||
async function symbolScreenPos(page: Page): Promise<{ x: number; y: number }> {
|
||||
const glId = await page.evaluate(() => {
|
||||
const visible = Array.from(document.querySelectorAll('[id^="glcanvas-"]'))
|
||||
.map((c) => c as HTMLCanvasElement)
|
||||
.find(
|
||||
(c) =>
|
||||
window.getComputedStyle(c).display !== "none" &&
|
||||
c.getBoundingClientRect().width > 0,
|
||||
);
|
||||
return visible?.id ?? null;
|
||||
});
|
||||
expect(glId, "a visible GAL canvas (glcanvas-*)").toBeTruthy();
|
||||
const box = await page.locator(`#${glId}`).boundingBox();
|
||||
expect(box, "canvas bounding box").not.toBeNull();
|
||||
const { vp, pos } = await page.evaluate((id) => {
|
||||
const m = window.Module as unknown as Mod;
|
||||
return {
|
||||
vp: JSON.parse(m.kicadCollabGetViewport()) as {
|
||||
cx: number;
|
||||
cy: number;
|
||||
scale: number;
|
||||
w: number;
|
||||
h: number;
|
||||
},
|
||||
pos: m.kicadCollabGetPos(id),
|
||||
};
|
||||
}, SYMBOL_UUID);
|
||||
const [wx, wy] = pos.split(",").map(Number);
|
||||
return {
|
||||
x: box!.x + (wx - vp.cx) * vp.scale + vp.w / 2,
|
||||
y: box!.y + (wy - vp.cy) * vp.scale + vp.h / 2,
|
||||
};
|
||||
}
|
||||
|
||||
const dialogCount = (page: Page) =>
|
||||
page.evaluate(
|
||||
() =>
|
||||
window.wxElementRegistry
|
||||
.findAll({ visible: true })
|
||||
.filter((e) => /Dialog/i.test(e.typeName) && e.typeName !== "wxFileDialog").length,
|
||||
);
|
||||
|
||||
type Box = { x: number; y: number; w: number; h: number };
|
||||
|
||||
/** Visible wx-dom text inputs (the combo/text grid editors and the
|
||||
* "Library link" field), with their viewport boxes. */
|
||||
const visibleTextInputs = (page: Page) =>
|
||||
page.evaluate(() =>
|
||||
Array.from(document.querySelectorAll("input.wx-dom-control[type=text]"))
|
||||
.map((i) => i as HTMLInputElement)
|
||||
.filter((i) => i.style.display !== "none")
|
||||
.map((i) => {
|
||||
const r = i.getBoundingClientRect();
|
||||
return {
|
||||
value: i.value,
|
||||
focused: document.activeElement === i,
|
||||
box: { x: r.x, y: r.y, w: r.width, h: r.height } as Box,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
/** The fields grid opens with the Reference cell's text editor already up
|
||||
* (row 0, "Value" column) — the one geometry anchor the page exposes for
|
||||
* the grid's cell layout: same column for every row, and the editor box is
|
||||
* the cell box (wxGridCellTextEditor::SetSize expands it by 2px each side,
|
||||
* the cell pitch adds the 1px grid line: pitch = editor height + 1). */
|
||||
async function valueColumnAnchor(page: Page): Promise<{ cx: number; row0cy: number; pitch: number }> {
|
||||
let anchor: Box | undefined;
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const inputs = await visibleTextInputs(page);
|
||||
anchor = inputs.find((i) => i.value === "R1")?.box;
|
||||
return anchor ? anchor.w : 0;
|
||||
},
|
||||
{ timeout: 20000, intervals: [250] },
|
||||
)
|
||||
.toBeGreaterThan(100);
|
||||
return {
|
||||
cx: anchor!.x + anchor!.w / 2,
|
||||
row0cy: anchor!.y + anchor!.h / 2,
|
||||
pitch: anchor!.h + 1,
|
||||
};
|
||||
}
|
||||
|
||||
async function clickOk(page: Page): Promise<void> {
|
||||
const rect = await page.evaluate(() => {
|
||||
const btn = Array.from(document.querySelectorAll("#window-container button")).find(
|
||||
(b) => (b.textContent ?? "").trim() === "OK",
|
||||
);
|
||||
if (!btn) return null;
|
||||
const r = (btn as HTMLElement).getBoundingClientRect();
|
||||
return { x: r.x + r.width / 2, y: r.y + r.height / 2, w: r.width };
|
||||
});
|
||||
expect(rect, "the dialog has an OK button").not.toBeNull();
|
||||
expect(rect!.w, "OK button has extent").toBeGreaterThan(0);
|
||||
await page.mouse.click(rect!.x, rect!.y);
|
||||
await expect.poll(() => dialogCount(page), { timeout: 20000, intervals: [250] }).toBe(0);
|
||||
}
|
||||
|
||||
async function savedProperty(page: Page, name: string): Promise<string | null> {
|
||||
const text = await page.evaluate((file) => {
|
||||
const w = window as unknown as { FS: FS; Module: Mod };
|
||||
w.Module.kicadSaveSchematic(file);
|
||||
return w.FS.readFile(file, { encoding: "utf8" });
|
||||
}, FILE);
|
||||
// The symbol instance's property (the lib_symbols template carries the same
|
||||
// names — take the LAST occurrence, the instance is serialized after
|
||||
// lib_symbols).
|
||||
const re = new RegExp(`\\(property "${name}" "([^"]*)"`, "g");
|
||||
let last: string | null = null;
|
||||
for (const m of text.matchAll(re)) last = m[1];
|
||||
return last;
|
||||
}
|
||||
|
||||
test.describe("grid cell editors are typeable (Symbol Properties)", () => {
|
||||
test("STC editor (Value) receives typed characters; combo editor (Footprint) is full-width", async ({
|
||||
page,
|
||||
}) => {
|
||||
test.setTimeout(240000);
|
||||
await bootAndOpen(page);
|
||||
|
||||
const { x, y } = await symbolScreenPos(page);
|
||||
await page.mouse.dblclick(x, y);
|
||||
await expect.poll(() => dialogCount(page), { timeout: 20000, intervals: [250] }).toBeGreaterThan(0);
|
||||
|
||||
const { cx, row0cy, pitch } = await valueColumnAnchor(page);
|
||||
const valueRowY = row0cy + pitch; // row 1: Value
|
||||
const footprintRowY = row0cy + 2 * pitch; // row 2: Footprint
|
||||
|
||||
// --- Value: canvas-drawn wxStyledTextCtrl editor -----------------------
|
||||
await page.mouse.dblclick(cx, valueRowY);
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
page.evaluate(() =>
|
||||
window.wxElementRegistry
|
||||
.findAll({ visible: true })
|
||||
.filter((e) => e.typeName === "wxStyledTextCtrl").length,
|
||||
),
|
||||
{ timeout: 20000, intervals: [250] },
|
||||
)
|
||||
.toBeGreaterThan(0);
|
||||
// BeginEdit selects the whole cell text, but the double-click's trailing
|
||||
// mouse-up lands in the freshly shown editor and collapses that selection
|
||||
// to a caret — select explicitly (SCINTILLA_TRICKS handles Ctrl+A).
|
||||
await page.keyboard.press("Control+a");
|
||||
await page.keyboard.type("47k");
|
||||
await page.keyboard.press("Enter"); // GRID_CELL_STC_EDITOR single-line accept
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
page.evaluate(() =>
|
||||
window.wxElementRegistry
|
||||
.findAll({ visible: true })
|
||||
.filter((e) => e.typeName === "wxStyledTextCtrl").length,
|
||||
),
|
||||
{ timeout: 20000, intervals: [250] },
|
||||
)
|
||||
.toBe(0);
|
||||
|
||||
// --- Footprint: wxComboCtrl-based editor --------------------------------
|
||||
await page.mouse.dblclick(cx, footprintRowY);
|
||||
let combo: { value: string; focused: boolean; box: Box } | undefined;
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const inputs = await visibleTextInputs(page);
|
||||
combo = inputs.find((i) => Math.abs(i.box.y + i.box.h / 2 - footprintRowY) < pitch / 2);
|
||||
return combo ? 1 : 0;
|
||||
},
|
||||
{ timeout: 20000, intervals: [250] },
|
||||
)
|
||||
.toBe(1);
|
||||
// The bug: the inner text ctrl stayed at its 10px creation width.
|
||||
expect(combo!.box.w, "combo editor text ctrl spans the cell").toBeGreaterThan(100);
|
||||
expect(combo!.focused, "combo editor text ctrl took focus").toBe(true);
|
||||
// Per-key delay: Firefox reorders characters typed at full speed into a
|
||||
// wx-dom <input> whose wx side echoes its value back (combo text ctrl) —
|
||||
// a separate wx-dom echo/ordering issue, not what this spec gates.
|
||||
await page.keyboard.type("Resistor_SMD:R_0805", { delay: 40 });
|
||||
await expect
|
||||
.poll(async () => (await visibleTextInputs(page)).find((i) => i.focused)?.value ?? "", {
|
||||
timeout: 20000,
|
||||
intervals: [250],
|
||||
})
|
||||
.toBe("Resistor_SMD:R_0805");
|
||||
|
||||
// OK commits the still-open cell editor (CommitPendingChanges).
|
||||
await clickOk(page);
|
||||
|
||||
expect(await savedProperty(page, "Value"), "Value typed through the STC editor").toBe("47k");
|
||||
expect(await savedProperty(page, "Footprint"), "Footprint typed through the combo editor").toBe(
|
||||
"Resistor_SMD:R_0805",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit fa46f1822c24e36ba02b73c35b0b0520df9dab9e
|
||||
Subproject commit d32535fefb0b3cc5858ac1f55d57739e9ace3c7e
|
||||
Loading…
Reference in a new issue