From 0b1a945d692397e1abd7ad900c159dbdeb470f69 Mon Sep 17 00:00:00 2001 From: 2SinceJuly Date: Mon, 9 Nov 2026 19:53:11 +0800 Subject: [PATCH] fix(electron): model view constructors as window-like surfaces (#9571) * fix(electron): model view constructors as window-like surfaces Electron's static pass recognized a window only through the literal `BrowserWindow` constructor, so `BaseWindow`, `BrowserView` and `WebContentsView` produced no node and their literal `webPreferences` were invisible. IPC registrations reached through such a view's own `webContents.ipc` were dropped too, because the receiver resolver had the same single-constructor gate -- VS Code alone registers seven channels this way, and a shipped sample of four applications put 29 percent of window construction sites on an unmodeled constructor. Read webPreferences through one constructor set shared by the construction inspector and the IPC receiver resolver. The finding only carries the new `window_constructor` field when the site is not `BrowserWindow`, so existing projections and their evidence stay byte-identical. A non-BrowserWindow owner is not admitted for `loadFile`/`loadURL` on itself: only BrowserWindow exposes those directly. * fix(electron): keep the view constructor label lookup nullable Observation labels are nullable in the application graph schema, so the byLabel lookup failed tsc with TS18047 and took the Static checks job down with it. --------- Co-authored-by: 2SinceJuly <202121119+2SinceJuly@users.noreply.github.com> Co-authored-by: morluto <76467478+morluto@users.noreply.github.com> --- .../javascript-artifact-reconstruction.md | 6 + .../ElectronBoundaryGraphWindows.ts | 7 +- .../electronStaticAnalysisBrowser.ts | 68 ++++++++--- .../electronStaticAnalysisScopedIpc.test.ts | 106 +++++++++++++++++ .../javascript/electronStaticAnalysisTypes.ts | 21 +++- .../javascript/electronStaticRenderer.ts | 38 ++++-- .../electronWindowConstructorSurfaces.ts | 41 +++++++ .../javascriptStaticAnalysisState.ts | 6 +- .../JavaScriptAnalysisWorkerSchemas.ts | 3 + .../electronApplicationAnalysis.test.ts | 108 ++++++++++++++++++ 10 files changed, 377 insertions(+), 27 deletions(-) create mode 100644 src/domain/javascript/electronWindowConstructorSurfaces.ts diff --git a/docs/guides/javascript/javascript-artifact-reconstruction.md b/docs/guides/javascript/javascript-artifact-reconstruction.md index f568cc4153..68a773a18f 100644 --- a/docs/guides/javascript/javascript-artifact-reconstruction.md +++ b/docs/guides/javascript/javascript-artifact-reconstruction.md @@ -328,6 +328,12 @@ failure does not authorize expanding relationships that parent admission decline ## What is reconstructed +Electron `BrowserView` and `WebContentsView` expose `webContents` and accept +`webPreferences`, including declared preloads. `BaseWindow` has neither surface +of its own. Preload associations retain the parsed constructor's mechanism rather +than labeling every view as a BrowserWindow. These are static associations, not +proof that a renderer or preload executed. + The projector reuses the content-addressed artifact inventory and safe artifact readers. It retains canonical artifact-relative paths, exact byte counts, SHA-256 digests, inventory IDs, ASAR container identity, and `.asar.unpacked` diff --git a/src/application/javascript/ElectronBoundaryGraphWindows.ts b/src/application/javascript/ElectronBoundaryGraphWindows.ts index b0bda84c59..2e29fde073 100644 --- a/src/application/javascript/ElectronBoundaryGraphWindows.ts +++ b/src/application/javascript/ElectronBoundaryGraphWindows.ts @@ -89,8 +89,11 @@ const addBrowserWindow = ( ), observations: [ { - label: `BrowserWindow@${javascriptSourceRangeKey(value.location)}`, + label: `${value.window_constructor ?? "BrowserWindow"}@${javascriptSourceRangeKey(value.location)}`, properties: { + ...(value.window_constructor === undefined + ? {} + : { window_constructor: value.window_constructor }), source_path: file.path, options_status: value.options_status, web_preferences_status: value.web_preferences_status, @@ -190,7 +193,7 @@ const addWindowPreload = ( kind: "electron-preload", anchor: input.file, resolution, - mechanism: "BrowserWindow:webPreferences.preload", + mechanism: `${input.value.window_constructor ?? "BrowserWindow"}:webPreferences.preload`, range: input.value.location, coverage: input.coverage, }); diff --git a/src/domain/javascript/electronStaticAnalysisBrowser.ts b/src/domain/javascript/electronStaticAnalysisBrowser.ts index 568f6d15f5..6f8e6613a8 100644 --- a/src/domain/javascript/electronStaticAnalysisBrowser.ts +++ b/src/domain/javascript/electronStaticAnalysisBrowser.ts @@ -8,6 +8,7 @@ import type { ElectronStaticValue, ElectronUtilityProcessFinding, ElectronWebPreference, + ElectronWindowConstructor, } from "./electronStaticAnalysisTypes.js"; import { contextBridgeExposureArguments } from "./javascriptContextBridgeExposure.js"; import { @@ -38,6 +39,26 @@ import { hasElectronOptionObjectCandidate, resolveElectronOptionObject, } from "./electronStaticOptionObjects.js"; +import { + ELECTRON_WINDOW_CONSTRUCTOR_SURFACES, + electronWindowConstructor, + type ElectronWindowConstructorSurface, +} from "./electronWindowConstructorSurfaces.js"; + +const constructorSurfaces = ( + constructor: ElectronWindowConstructor, +): ElectronWindowConstructorSurface => + ELECTRON_WINDOW_CONSTRUCTOR_SURFACES[constructor]; + +/** The modeled window constructor a `new` callee names, if any. */ +const electronWindowConstructorOf = ( + callee: t.Node, + context: JavaScriptFindingContext, +): ElectronWindowConstructor | undefined => { + const path = electronCalleePath(callee, context); + const name = path?.length === 1 ? path[0] : undefined; + return name === undefined ? undefined : electronWindowConstructor(name); +}; /** Inspect BrowserWindow, contextBridge, and utility-process syntax. */ export const inspectElectronBrowserNode = ( @@ -45,11 +66,15 @@ export const inspectElectronBrowserNode = ( context: JavaScriptFindingContext, ): void => { if (t.isNewExpression(node)) { - const path = electronCalleePath(node.callee, context); - if (path?.length === 1 && path[0] === "BrowserWindow") { - if (needsConstantOptions(node)) - context.accumulator.pendingBrowserWindows.push(node); - else inspectBrowserWindow(node, context); + const constructor = electronWindowConstructorOf(node.callee, context); + if (constructor !== undefined) { + const preferences = constructorSurfaces(constructor).preferences; + if (preferences && needsConstantOptions(node)) + context.accumulator.pendingBrowserWindows.push({ + node, + constructor, + }); + else inspectBrowserWindow(node, context, constructor); } } if (!t.isCallExpression(node) && !t.isOptionalCallExpression(node)) return; @@ -76,7 +101,7 @@ export function* finishElectronBrowserWindowsSteps( const pending = context.accumulator.pendingBrowserWindows; if (pending.length === 0) return; if ( - pending.some((node) => { + pending.some(({ node }) => { const argument = argumentNode(node.arguments[0]); const options = argument && unwrapJavaScriptExpression(argument).node; if (t.isIdentifier(options)) @@ -89,8 +114,8 @@ export function* finishElectronBrowserWindowsSteps( }) ) yield* index.indexMemberMutationFactsSteps(); - for (const node of pending) { - inspectBrowserWindow(node, context, index); + for (const { node, constructor } of pending) { + inspectBrowserWindow(node, context, constructor, index); yield; } // Inline findings arrive during traversal; deferred findings join that same @@ -104,17 +129,25 @@ export function* finishElectronBrowserWindowsSteps( const inspectBrowserWindow = ( node: t.NewExpression, context: JavaScriptFindingContext, + constructorName: ElectronWindowConstructor, index?: JavaScriptBindingIndex, ): void => { + // A constructor that accepts no webPreferences has none to read: the literal + // option it carries is inert, so reporting it as a preference surface would + // invent a preload the runtime never loads. + const readsPreferences = constructorSurfaces(constructorName).preferences; const argument = argumentNode(node.arguments[0]); const options = argument === undefined ? undefined : unwrapJavaScriptExpression(argument).node; - const resolvedOptions = - (index && - resolveElectronOptionObject(options, node, index, ["webPreferences"])) ?? - options; + const resolvedOptions = readsPreferences + ? ((index && + resolveElectronOptionObject(options, node, index, [ + "webPreferences", + ])) ?? + options) + : options; const lookup = staticObjectProperty(resolvedOptions, "webPreferences"); const declaredPreferences = lookup.status === "explicit" @@ -134,13 +167,16 @@ const inspectBrowserWindow = ( ]; const collected = collectWindowOptions( context.source, - lookup.status, - resolvedPreferences, + readsPreferences ? lookup.status : "missing", + readsPreferences ? resolvedPreferences : undefined, context.pathIntrinsics, ); context.accumulator.unknownFindings += collected.unknown; const finding: ElectronBrowserWindowFinding = { ...electronCandidateAuthority(node.callee, context), + ...(constructorName === "BrowserWindow" + ? {} + : { window_constructor: constructorName }), options_status: resolvedOptions === undefined ? "missing" @@ -169,14 +205,14 @@ const inspectBrowserWindow = ( role: "preload", path: finding.preload_path, resolution_context: finding.preload_resolution_context, - mechanism: "BrowserWindow:webPreferences.preload", + mechanism: `${constructorName}:webPreferences.preload`, module_key: null, location: range(node), }, }); addLocatedFinding(context, { collection: context.accumulator.browserWindows, - key: "electron-window\0BrowserWindow", + key: `electron-window\0${constructorName}`, node, value: finding, }); diff --git a/src/domain/javascript/electronStaticAnalysisScopedIpc.test.ts b/src/domain/javascript/electronStaticAnalysisScopedIpc.test.ts index f9b9935162..956a82f901 100644 --- a/src/domain/javascript/electronStaticAnalysisScopedIpc.test.ts +++ b/src/domain/javascript/electronStaticAnalysisScopedIpc.test.ts @@ -142,3 +142,109 @@ it.each(["webContents.ipc", "webContents.mainFrame.ipc"])( ).toEqual([]); }, ); + +it.each([ + ["BrowserView", "webContents.ipc", "web-contents"], + ["WebContentsView", "webContents.mainFrame.ipc", "main-frame"], +] as const)( + "retains a proven %s %s handler through the view's own contents", + (constructor, surface, kind) => { + const analysis = analyzeJavaScriptStaticSource(` + const { ${constructor} } = require("electron"); + const owner = new ${constructor}({}); + owner.${surface}.handle("view-channel", () => {}); + `); + expect(analysis.parse_error_count).toBe(0); + expect(analysis.electron.browser_windows).toMatchObject([ + { window_constructor: constructor, options_status: "object-literal" }, + ]); + expect(analysis.electron.ipc).toMatchObject([ + { + side: "main", + operation: "handle", + channel: "view-channel", + receiver: { kind }, + }, + ]); + }, +); + +it.each(["webContents.ipc", "webContents.mainFrame.ipc", "loadURL"])( + "omits a BaseWindow %s receiver that the runtime does not own", + (surface) => { + const analysis = analyzeJavaScriptStaticSource(` + const { BaseWindow } = require("electron"); + const base = new BaseWindow({ width: 800 }); + base.${surface}.handle("inert", () => {}); + `); + expect(analysis.parse_error_count).toBe(0); + expect(analysis.electron.ipc).toEqual([]); + expect(analysis.electron.browser_windows).toMatchObject([ + { + window_constructor: "BaseWindow", + options_status: "object-literal", + web_preferences_status: "missing", + preload_path: null, + }, + ]); + }, +); + +it("keeps a lookalike construction unproven for a view constructor", () => { + const analysis = analyzeJavaScriptStaticSource(` + const { WebContentsView } = require("electron"); + function shadow(WebContentsView) { + const view = new WebContentsView({ webPreferences: { sandbox: true } }); + view.webContents.ipc.handle("shadowed", () => {}); + } + `); + expect(analysis.parse_error_count).toBe(0); + expect(analysis.electron.browser_windows).toEqual([]); + expect(analysis.electron.ipc).toEqual([]); +}); + +it.each(["BrowserView", "WebContentsView"])( + "reads literal webPreferences on a %s construction", + (constructor) => { + expect( + analyzeJavaScriptStaticSource(` + const { ${constructor} } = require("electron"); + const owner = new ${constructor}({ webPreferences: { preload: "./p.js", sandbox: true } }); + `).electron.browser_windows, + ).toMatchObject([ + { + window_constructor: constructor, + web_preferences_status: "object-literal", + web_preferences: [ + { name: "preload", value: { status: "literal", value: "./p.js" } }, + { name: "sandbox", value: { status: "literal", value: true } }, + ], + preload_path: "./p.js", + preload_resolution_context: "module-specifier", + }, + ]); + }, +); + +it.each(["BaseWindow"])( + "does not report a webPreferences surface a %s never accepts", + (constructor) => { + const analysis = analyzeJavaScriptStaticSource(` + const { ${constructor} } = require("electron"); + const owner = new ${constructor}({ webPreferences: { preload: "./p.js", sandbox: true } }); + `); + expect(analysis.parse_error_count).toBe(0); + expect(analysis.electron.browser_windows).toMatchObject([ + { + window_constructor: constructor, + options_status: "object-literal", + web_preferences_status: "missing", + web_preferences: [], + preload_path: null, + }, + ]); + expect( + analysis.role_paths.filter(({ role }) => role === "preload"), + ).toEqual([]); + }, +); diff --git a/src/domain/javascript/electronStaticAnalysisTypes.ts b/src/domain/javascript/electronStaticAnalysisTypes.ts index a974ebf8e6..4f8977c46c 100644 --- a/src/domain/javascript/electronStaticAnalysisTypes.ts +++ b/src/domain/javascript/electronStaticAnalysisTypes.ts @@ -33,8 +33,27 @@ export type ElectronBrowserWindowPreload = readonly preload_resolution_context: null; }; -/** Statically visible BrowserWindow construction and preference surface. */ +/** + * Constructors that create a window-like Electron surface. Each one is read + * through the capability table in `electronWindowConstructorSurfaces.ts`, + * because they do not all expose the same ones. + */ +export type ElectronWindowConstructor = + | "BrowserWindow" + | "BaseWindow" + | "BrowserView" + | "WebContentsView"; + +/** Statically visible window construction and preference surface. */ export type ElectronBrowserWindowFinding = ElectronBrowserWindowPreload & { + /** + * Constructor that produced this surface. Absent means `BrowserWindow`; the + * others are named so the construction site is never read as a window API. + */ + readonly window_constructor?: Exclude< + ElectronWindowConstructor, + "BrowserWindow" + >; readonly options_status: "object-literal" | "dynamic" | "missing"; readonly web_preferences_status: "object-literal" | "dynamic" | "missing"; readonly web_preferences: readonly ElectronWebPreference[]; diff --git a/src/domain/javascript/electronStaticRenderer.ts b/src/domain/javascript/electronStaticRenderer.ts index 9a207ec895..4b68c08e21 100644 --- a/src/domain/javascript/electronStaticRenderer.ts +++ b/src/domain/javascript/electronStaticRenderer.ts @@ -15,6 +15,11 @@ import { } from "./javascriptSemanticState.js"; import { completeSemanticSteps } from "./javascriptSemanticTraversal.js"; import type { JavaScriptFindingContext } from "./javascriptStaticAnalysisState.js"; +import { + ELECTRON_WINDOW_CONSTRUCTOR_SURFACES, + electronWindowConstructor, + type ElectronWindowConstructorSurface, +} from "./electronWindowConstructorSurfaces.js"; /** A renderer load belongs to an intact, source-proven BrowserWindow instance. */ export const electronRendererLoadAuthority = ( @@ -90,17 +95,28 @@ export const resolveElectronBrowserWindowReceiver = ( receiver = unwrapJavaScriptExpression(initializer.node).node; } if (!t.isNewExpression(receiver)) return undefined; - const constructor = electronCalleePath(receiver.callee, context); + const constructorPath = electronCalleePath(receiver.callee, context); + const constructorName = + constructorPath?.length === 1 ? constructorPath[0] : undefined; + const constructor = + constructorName === undefined + ? undefined + : electronWindowConstructor(constructorName); + // Receivers are admitted only for constructors whose capability entry proves + // the addressed surface, so a window-like construction never gains IPC or + // load facts the runtime would not produce. if ( - constructor?.length !== 1 || - constructor[0] !== "BrowserWindow" || - !supportedReceiverPath(path) + constructor === undefined || + !supportedReceiverPath( + path, + ELECTRON_WINDOW_CONSTRUCTOR_SURFACES[constructor], + ) ) return undefined; if ( context.electronExportIntact?.([ "electron", - "BrowserWindow", + constructor, "prototype", ...path, ]) !== true @@ -119,11 +135,19 @@ export const resolveElectronBrowserWindowReceiver = ( }; }; -const supportedReceiverPath = (path: readonly string[]): boolean => { +const supportedReceiverPath = ( + path: readonly string[], + surfaces: ElectronWindowConstructorSurface, +): boolean => { + // A constructor that owns its webContents accepts preferences, preload, + // renderer loads and scoped IPC through it; one that does not owns only the + // native frame, so none of those surfaces exist on the instance. + if (!surfaces.webContents) return false; const operation = path.at(-1); if (operation === "loadFile" || operation === "loadURL") return ( - path.length === 1 || (path.length === 2 && path[0] === "webContents") + (surfaces.selfLoad && path.length === 1) || + (path.length === 2 && path[0] === "webContents") ); return ( path[0] === "webContents" && diff --git a/src/domain/javascript/electronWindowConstructorSurfaces.ts b/src/domain/javascript/electronWindowConstructorSurfaces.ts new file mode 100644 index 0000000000..882fd4eaaf --- /dev/null +++ b/src/domain/javascript/electronWindowConstructorSurfaces.ts @@ -0,0 +1,41 @@ +import type { ElectronWindowConstructor } from "./electronStaticAnalysisTypes.js"; + +/** + * Electron modules that build a window-like `webContents` surface and the + * surfaces each one exposes. One table so the construction inspector and the + * scoped-IPC receiver resolver cannot disagree about which constructors carry + * a preference, preload or `webContents` surface. + */ +export interface ElectronWindowConstructorSurface { + /** The constructor accepts `webPreferences` in its options object. */ + readonly preferences: boolean; + /** The instance owns `webContents`, and so `webContents.ipc`, directly. */ + readonly webContents: boolean; + /** The instance exposes `loadFile`/`loadURL` on itself, not via contents. */ + readonly selfLoad: boolean; +} + +export const ELECTRON_WINDOW_CONSTRUCTOR_SURFACES: Readonly< + Record +> = { + // A BrowserWindow is created with its own contents and preferences. + BrowserWindow: { preferences: true, webContents: true, selfLoad: true }, + // BaseWindow owns native frame geometry; its contents live in separately + // attached WebContentsViews, so it accepts no webPreferences and exposes no + // webContents of its own. + BaseWindow: { preferences: false, webContents: false, selfLoad: false }, + // A BrowserView is constructed with webPreferences and owns its webContents. + BrowserView: { preferences: true, webContents: true, selfLoad: false }, + // A WebContentsView accepts preferences or an explicitly adopted webContents. + WebContentsView: { preferences: true, webContents: true, selfLoad: false }, +}; + +const CONSTRUCTOR_NAMES: ReadonlySet = new Set( + Object.keys(ELECTRON_WINDOW_CONSTRUCTOR_SURFACES), +); + +/** Narrow an Electron export path segment to a modeled window constructor. */ +export const electronWindowConstructor = ( + name: string, +): ElectronWindowConstructor | undefined => + CONSTRUCTOR_NAMES.has(name) ? (name as ElectronWindowConstructor) : undefined; diff --git a/src/domain/javascript/javascriptStaticAnalysisState.ts b/src/domain/javascript/javascriptStaticAnalysisState.ts index 962a94043c..35fb8a684b 100644 --- a/src/domain/javascript/javascriptStaticAnalysisState.ts +++ b/src/domain/javascript/javascriptStaticAnalysisState.ts @@ -23,6 +23,7 @@ import type { ElectronSenderValidationFinding, ElectronProtocolFinding, ElectronUtilityProcessFinding, + ElectronWindowConstructor, } from "./electronStaticAnalysisTypes.js"; import { JavaScriptModuleRangeIndex } from "./javascriptStaticAnalysisFindings.js"; @@ -80,7 +81,10 @@ export interface JavaScriptAnalysisAccumulator { readonly sourceMaps: JavaScriptStaticAnalysis["source_map_urls"][number][]; readonly registrations: JavaScriptBundlerRegistration[]; readonly browserWindows: LocatedJavaScriptFinding[]; - readonly pendingBrowserWindows: t.NewExpression[]; + readonly pendingBrowserWindows: { + readonly node: t.NewExpression; + readonly constructor: ElectronWindowConstructor; + }[]; readonly contextBridgeApis: LocatedJavaScriptFinding[]; readonly ipc: LocatedJavaScriptFinding[]; readonly senderValidations: LocatedJavaScriptFinding[]; diff --git a/src/javascript/analysis/JavaScriptAnalysisWorkerSchemas.ts b/src/javascript/analysis/JavaScriptAnalysisWorkerSchemas.ts index c5235a56ba..413b8abf16 100644 --- a/src/javascript/analysis/JavaScriptAnalysisWorkerSchemas.ts +++ b/src/javascript/analysis/JavaScriptAnalysisWorkerSchemas.ts @@ -153,6 +153,9 @@ const ipc = z }); const windowShape = { ...electronFindingContext, + window_constructor: z + .enum(["BaseWindow", "BrowserView", "WebContentsView"]) + .exactOptional(), options_status: z.enum(["object-literal", "dynamic", "missing"]), web_preferences_status: z.enum(["object-literal", "dynamic", "missing"]), web_preferences: z.array( diff --git a/tests/boundary/filesystem/javascript-applications/electronApplicationAnalysis.test.ts b/tests/boundary/filesystem/javascript-applications/electronApplicationAnalysis.test.ts index dea99967f5..dad21e441c 100644 --- a/tests/boundary/filesystem/javascript-applications/electronApplicationAnalysis.test.ts +++ b/tests/boundary/filesystem/javascript-applications/electronApplicationAnalysis.test.ts @@ -164,6 +164,77 @@ describe("static Electron application analysis", () => { }); }); +describe("view constructor surfaces", () => { + it("reconstructs view constructors without inventing window surfaces", async () => { + const root = await viewConstructorFixtureDirectory(); + + const result = await reconstructJavaScriptArtifact({ input_path: root }); + const graph = parseJavaScriptApplicationGraph(result.graph); + + const windows = graph.nodes.filter(({ kind }) => kind === "browser-window"); + expect(windows).toHaveLength(4); + const byLabel = (prefix: string) => + windows + .flatMap(({ observations }) => observations) + .find(({ label }) => label?.startsWith(`${prefix}@`))?.properties ?? {}; + const browserWindow = byLabel("BrowserWindow"); + const baseWindow = byLabel("BaseWindow"); + // A control: BrowserWindow keeps its construction label and surface. + expect(browserWindow).toMatchObject({ + options_status: "object-literal", + web_preferences_status: "object-literal", + preload_path: "./browser-preload.js", + }); + expect(browserWindow).not.toHaveProperty("window_constructor"); + expect(windows.map(({ observations }) => observations[0]?.label)).toEqual( + expect.arrayContaining([ + expect.stringMatching(/^BrowserWindow@/u), + expect.stringMatching(/^BaseWindow@/u), + ]), + ); + // BaseWindow accepts no webPreferences and exposes no webContents, so its + // inert option must not become a preload or an IPC surface. + expect(baseWindow).toMatchObject({ + window_constructor: "BaseWindow", + options_status: "object-literal", + web_preferences_status: "missing", + preload_path: null, + }); + const preloadRoles = graph.nodes.filter( + ({ kind }) => kind === "electron-preload", + ); + expect(preloadRoles).toHaveLength(3); + expect(preloadRoles[0]?.observations[0]?.properties).toMatchObject({ + declared_path: "./browser-preload.js", + }); + for (const [constructor, path] of [ + ["BrowserView", "./view-preload.js"], + ["WebContentsView", "./contents-preload.js"], + ] as const) { + expect(byLabel(constructor)).toMatchObject({ + window_constructor: constructor, + web_preferences_status: "object-literal", + preload_path: path, + }); + expect( + preloadRoles + .flatMap((node) => node.observations) + .some( + ({ properties }) => + properties.mechanism === `${constructor}:webPreferences.preload`, + ), + ).toBe(true); + } + expect(result.electron_summary.ipc).toMatchObject({ + main_handlers: 3, + }); + const channels = graph.nodes.filter(({ kind }) => kind === "ipc-channel"); + expect(channels.flatMap(({ observations }) => observations)).toHaveLength( + 3, + ); + }); +}); + describe("native addon association", () => { it.each([ { native: false, associations: [] }, @@ -602,6 +673,43 @@ const fixtureDirectory = async (): Promise => { return root; }; +const viewConstructorFixtureDirectory = async (): Promise => { + const root = await createTestTempDirectory("rea-electron-view-constructors-"); + await Promise.all([ + writeFile( + join(root, "package.json"), + JSON.stringify({ + name: "rea-electron-view-constructors", + main: "main.cjs", + }), + ), + writeFile(join(root, "browser-preload.js"), "module.exports = {};"), + writeFile(join(root, "view-preload.js"), "module.exports = {};"), + writeFile(join(root, "contents-preload.js"), "module.exports = {};"), + writeFile( + join(root, "main.cjs"), + String.raw` +const { BrowserWindow, BaseWindow, BrowserView, WebContentsView } = require("electron"); +const window = new BrowserWindow({ + webPreferences: { preload: "./browser-preload.js" }, +}); +window.webContents.ipc.handle("window-channel", () => "window"); +const base = new BaseWindow({ + width: 800, + webPreferences: { preload: "./view-preload.js" }, +}); +base.webContents.ipc.handle("base-channel", () => "base"); +const view = new BrowserView({ webPreferences: { preload: "./view-preload.js" } }); +view.webContents.ipc.handle("view-channel", () => "view"); +const options = { webPreferences: { preload: "./contents-preload.js" } }; +const contents = new WebContentsView(options); +contents.webContents.mainFrame.ipc.handle("contents-channel", () => "contents"); +`, + ), + ]); + return root; +}; + const requireAliasFixtureDirectory = async (): Promise => { const root = await createTestTempDirectory("rea-electron-require-aliases-"); await Promise.all([