`import electron from "electron"` bound the module with a leading `default` export segment, so `electron.ipcMain.handle(...)`, `new electron.BrowserWindow(...)` and destructuring from that binding matched no Electron API. Electron's CommonJS exports define no `default` member, so the segment can only denote the module object; drop it when classifying Electron bindings. The binding classifier already recorded optional-call roots, but the IPC, context-bridge, utility-process and sender-validation visitors accepted only plain calls. `ipcMain?.handle(...)`, `contextBridge?.exposeInMainWorld(...)` and the common `event.senderFrame?.url.startsWith(...)` check were dropped without a limitation. Refs #1656 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
77 lines
2.5 KiB
TypeScript
77 lines
2.5 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
|
|
|
|
describe("Electron API identity", () => {
|
|
it("recovers APIs through an ES module default import of Electron", () => {
|
|
const analysis = analyzeJavaScriptStaticSource(`
|
|
import electron from "electron";
|
|
const { ipcMain } = electron;
|
|
new electron.BrowserWindow({ webPreferences: { preload: "./preload.js" } });
|
|
electron.contextBridge.exposeInMainWorld("api", { ping: true });
|
|
electron.ipcRenderer.invoke("default:ping");
|
|
ipcMain.handle("destructured:ping", () => "pong");
|
|
electron.default.ipcMain.handle("nested-default", () => "local");
|
|
`);
|
|
|
|
expect(analysis.electron.browser_windows).toMatchObject([
|
|
{ preload_path: "./preload.js" },
|
|
]);
|
|
expect(analysis.electron.context_bridge_apis).toMatchObject([
|
|
{ api_key: "api", members: ["ping"] },
|
|
]);
|
|
expect(
|
|
analysis.electron.ipc.map(({ side, operation, channel }) => ({
|
|
side,
|
|
operation,
|
|
channel,
|
|
})),
|
|
).toEqual([
|
|
{ side: "renderer", operation: "invoke", channel: "default:ping" },
|
|
{ side: "main", operation: "handle", channel: "destructured:ping" },
|
|
]);
|
|
});
|
|
|
|
it("recovers APIs and sender checks written with optional chaining", () => {
|
|
const analysis = analyzeJavaScriptStaticSource(`
|
|
const { contextBridge, ipcMain, utilityProcess } = require("electron");
|
|
contextBridge?.exposeInMainWorld("api", {});
|
|
utilityProcess?.fork("./worker.js");
|
|
ipcMain?.handle("optional:ping", (event) => {
|
|
if (!event.senderFrame?.url.startsWith("file://")) return null;
|
|
return event.sender?.getURL() === "file:///index.html";
|
|
});
|
|
`);
|
|
|
|
expect(analysis.electron.context_bridge_apis).toMatchObject([
|
|
{ api_key: "api" },
|
|
]);
|
|
expect(analysis.electron.utility_processes).toMatchObject([
|
|
{ module_path: "./worker.js" },
|
|
]);
|
|
expect(analysis.electron.ipc).toMatchObject([
|
|
{ side: "main", operation: "handle", channel: "optional:ping" },
|
|
]);
|
|
expect(
|
|
analysis.electron.sender_validations.map(
|
|
({ subject, mechanism, expected }) => ({
|
|
subject,
|
|
mechanism,
|
|
expected: expected.value,
|
|
}),
|
|
),
|
|
).toEqual([
|
|
{
|
|
subject: "sender-url",
|
|
mechanism: "call:startsWith",
|
|
expected: "file://",
|
|
},
|
|
{
|
|
subject: "sender-url",
|
|
mechanism: "comparison:===",
|
|
expected: "file:///index.html",
|
|
},
|
|
]);
|
|
});
|
|
});
|