1import { describe, expect, it } from "vite-plus/test";
2
3import { PREVIEW_WEBVIEW_PREFERENCES } from "./WebviewPreferences.ts";
4
5/**
6 * Mirrors Electron's webview attribute parser closely enough to catch the
7 * regressions we've already hit:
8 *
9 * - whitespace inside the comma-separated list silently drops keys (so
10 * `" sandbox=true"` becomes an unknown key and Electron falls back to
11 * defaults — re-opening the Node-leak window we closed),
12 * - non-`true`/`false` values (`"yes"`, `"no"`, etc.) are kept as truthy
13 * strings and assigned to a boolean preference, which silently flips
14 * `contextIsolation=no` to ENABLED (then react-grab can't see the React
15 * DevTools hook and componentName resolution always returns null).
16 *
17 * The actual Electron parser does roughly:
18 *
19 * for (const pair of webpreferences.split(',')) {
20 * const [key, value] = pair.split('=');
21 * prefs[key] = value; // value left as a string
22 * }
23 *
24 * then later coerces booleans via `Boolean(value)`. Replicating that here
25 * keeps the test independent of Electron internals while still failing if
26 * we accidentally ship `"contextIsolation=no"` again.
27 */
28function parseWebPreferences(input: string): Record<string, unknown> {
29 const out: Record<string, unknown> = {};
30 for (const pair of input.split(",")) {
31 if (pair !== pair.trim()) {
32 // Electron's parser doesn't trim; surface the bug as undefined-key.
33 out[pair] = pair.split("=")[1];
34 continue;
35 }
36 const [key, value] = pair.split("=");
37 if (!key) continue;
38 out[key] = value;
39 }
40 return out;
41}
42
43describe("PREVIEW_WEBVIEW_PREFERENCES", () => {
44 const parsed = parseWebPreferences(PREVIEW_WEBVIEW_PREFERENCES);
45
46 it("contains exactly the three security-critical keys", () => {
47 expect(Object.keys(parsed).toSorted()).toEqual(
48 ["contextIsolation", "nodeIntegration", "sandbox"].toSorted(),
49 );
50 });
51
52 it("uses canonical JS-boolean string literals (not yes/no, on/off, 1/0)", () => {
53 // `value="no"` is a TRUTHY string when assigned to webPreferences.X — so
54 // `contextIsolation="no"` would silently leave isolation ENABLED. Lock
55 // the values to `"true"` / `"false"` so the parser does the right thing.
56 for (const value of Object.values(parsed)) {
57 expect(value).toMatch(/^(true|false)$/);
58 }
59 });
60
61 it("disables context isolation (so react-grab can see the page's React DevTools hook)", () => {
62 expect(parsed["contextIsolation"]).toBe("false");
63 });
64
65 it("keeps the renderer sandbox enabled (so the page cannot reach Node APIs)", () => {
66 expect(parsed["sandbox"]).toBe("true");
67 });
68
69 it("disables nodeIntegration (defense in depth — page never gets Node)", () => {
70 expect(parsed["nodeIntegration"]).toBe("false");
71 });
72
73 it("contains no whitespace (Electron's parser does not trim)", () => {
74 // Electron splits on `,` without trimming, so any whitespace would turn
75 // a key into an unknown one and silently drop the security flag.
76 expect(PREVIEW_WEBVIEW_PREFERENCES).not.toMatch(/\s/);
77 });
78});