pcbjam/tests/kicad/eeschema-url-regex.spec.ts

135 lines
5.4 KiB
TypeScript
Raw Normal View History

import { test, expect } from './fixtures';
/**
* Eeschema URL-detection wxRegEx regression (WASM strconv UTF-8 fix).
*
* Loading a schematic that renders text/symbol fields calls IsURL() /
* LinkifyHTML() (kicad/common/string_utils.cpp), whose static wxRegEx pattern
* ends in a negated class containing U+00B6 (PILCROW). In the emscripten build
* wxString stores UTF-8 (wxUSE_UNICODE_UTF8) and `wxS(...)` is a NARROW literal;
* clang emits U+00B6 as UTF-8 bytes 0xC2 0xB6. The implicit char*->wxString
* conversion used wxConvLibc, which under musl's default "C" locale (MB_CUR_MAX
* == 1) does NOT decode UTF-8 it maps each non-ASCII byte into the
* surrogate-escape range (0xDF80 + byte), so 0xC2 0xB6 became U+DFC2 U+DFB6.
* utf8_str() then re-emitted those as WTF-8 and PCRE2 rejected the pattern with
* "Invalid regular expression ...: code points 0xd800-0xdfff are not defined",
* popping a modal error dialog over the schematic.
*
* Fix (wxwidgets/src/common/strconv.cpp): on __EMSCRIPTEN__ bind wxConvLibc to a
* UTF-8 converter instead of the libc/locale one, so narrow literals (and all
* web I/O, which is UTF-8) decode correctly regardless of the current locale.
*
* This test opens a text_box-bearing schematic (with a real URL) through the
* programmatic Module.kicadOpenFile() hook and asserts the regex error never
* fires: no "Invalid regular expression" in the console, no error dialog.
*/
const URL_SCH = `(kicad_sch
\t(version 20250114)
\t(generator "eeschema")
\t(generator_version "9.0")
\t(uuid "44444444-4444-4444-4444-444444444444")
\t(paper "A4")
\t(lib_symbols)
\t(text_box "Docs: https://kicad.org/help ¶ end"
\t\t(exclude_from_sim no)
\t\t(at 50.8 50.8 0)
\t\t(size 80 20)
\t\t(margins 0.9525 0.9525 0.9525 0.9525)
\t\t(stroke (width 0) (type default))
\t\t(fill (type none))
\t\t(effects (font (size 1.27 1.27)) (justify left top))
\t\t(uuid "55555555-0000-0000-0000-000000000001")
\t)
\t(sheet_instances
\t\t(path "/"
\t\t\t(page "1")
\t\t)
\t)
)
`;
type EmscriptenFS = {
mkdirTree(path: string): void;
writeFile(path: string, data: string): void;
};
type KicadModule = { kicadOpenFile(path: string): unknown };
const REGEX_ERR = /invalid regular expression|0xd800|UTF-8 error|code points/i;
test.describe('Eeschema URL-detection regex', () => {
test('renders a text_box with a URL without the wxRegEx UTF-8 modal', async ({ page }) => {
const consoleLines: string[] = [];
page.on('console', (m) => consoleLines.push(`[${m.type()}] ${m.text()}`));
await page.goto('/kicad/eeschema.html');
await expect(page.locator('#canvas')).toBeVisible({ timeout: 90000 });
await page.waitForFunction(() => !!window.wxElementRegistry, null, { timeout: 90000 });
await page.waitForFunction(
() =>
typeof (window as unknown as { Module?: KicadModule }).Module?.kicadOpenFile ===
'function',
null,
{ timeout: 90000 }
);
await page.waitForFunction(
() =>
!!window.wxElementRegistry &&
window.wxElementRegistry
.findAll({ visible: true })
.some((e) => /Frame$/.test(e.typeName) || (e.name || '').endsWith('Frame')),
null,
{ timeout: 90000 }
);
expect(await page.title()).toMatch(/untitled/i);
const openedPath = await page.evaluate((content) => {
const w = window as unknown as { FS: EmscriptenFS; Module: KicadModule };
const dir = '/home/kicad/documents';
try {
w.FS.mkdirTree(dir);
} catch {
/* already exists */
}
const path = `${dir}/url.kicad_sch`;
w.FS.writeFile(path, content);
w.Module.kicadOpenFile(path);
return path;
}, URL_SCH);
expect(openedPath).toContain('url.kicad_sch');
await expect
.poll(async () => page.title(), { timeout: 30000, intervals: [500] })
.toMatch(/url/i);
// Let the canvas paint the text_box (this is when IsURL()/LinkifyHTML()
// compile the static wxRegEx and would have thrown the modal).
await page.waitForTimeout(1500);
await page.screenshot({
path: 'test-results/eeschema-url-regex.png',
feat(tests): screenshot regression + Discord review, perf-tracked New tooling in tests/tools/screenshots/ (TypeScript via tsx): - compare.ts: one pixelmatch engine (AA-excluded), connected-component "where to look" boxes, old|new+boxes|heatmap triptych, per-engine floors. - promote.ts: churn-free updater — overwrite a baseline only when decoded pixels differ beyond the floor, copying CI bytes verbatim (no re-encode churn); pulls a CI run via `gh run download` or a local --from dir. - post-discord.ts: always-on CI-on-main report (SHA + e2e status + the track-only runtime-perf table), then screenshot triptychs, batched + size-capped + flood-collapsed + 429-aware. - perf-report.ts: perf table with Δ vs the previous main run (via gh). - changelog.ts: no-build git-history baseline differ (Discord trigger B). - noise.ts / gen-manifest.ts: calibration + manifest generation. CI wiring: - wasm-build.yml: post-test step runs the gate + report on the already- produced test-results (no extra build); report-only (continue-on-error), posts only on push to main, inert without DISCORD_WEBHOOK_URL. - ci-ubicloud.yml: secrets: inherit (pass the webhook through). - screenshot-changelog.yml: ~30s no-build changelog on baseline changes. screenshot-manifest.json: canonical 354-name set + best-effort engine tags (313 chromium-swiftshader / 41 firefox-llvmpipe). Normalize scale:'device'->'css' across 18 spec files (no-op at CI DSF=1) so committed baselines are uniformly css-scaled. Design: CI's Linux render is the single source of truth; no pinned container (accept rare env drift -> re-promote); dev commits via promote. Replaces the byte-cmp compare-screenshots.sh + file-size-proxy update-baseline-screenshots.sh (kept for now until the first re-baseline). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 18:01:15 +02:00
scale: 'css',
});
// The wxRegEx compile failure surfaces two ways: a wxLogError logged to
// the console, and (its default GUI target) a modal error dialog. Assert
// neither happened. A pre-fix build renders this same text_box and so
// hits IsURL() -> the failing static regex -> both signals fire.
const regexConsole = consoleLines.filter((l) => REGEX_ERR.test(l));
expect(regexConsole, `regex error in console:\n${regexConsole.join('\n')}`).toHaveLength(0);
const errDialogs = await page.evaluate((src) => {
const reg = window.wxElementRegistry;
if (!reg) return [];
return reg
.findAll({})
.filter((e: any) =>
new RegExp(src, 'i').test(`${e.name || ''} ${e.label || ''} ${e.text || ''}`)
)
.map((e: any) => ({ typeName: e.typeName, name: e.name, text: e.text }));
}, REGEX_ERR.source);
expect(errDialogs, `error dialog present:\n${JSON.stringify(errDialogs)}`).toHaveLength(0);
});
});