mirror of
https://github.com/electron/electron.git
synced 2026-10-28 11:44:46 +08:00
* build: stop re-running failed specs individually at the end of a test job script/spec-runner.js had an --enableRerun mode that, after a failing run, parsed the JUnit report and re-ran each failed test on its own in a fresh Electron. CI passed --enableRerun=3 on the macOS, Windows and Linux test legs, so a test that only passes when it has the machine to itself turned the job green while still being recorded as a failure. In practice the mode only ever made one solo attempt per test (a failed rerun exited the runner immediately), and later failures were never rerun once an earlier one failed again. Remove the mode: the JUnit parsing, the solo rerun loop, the per-launch --log-file renaming and the --files filtering that only reruns used. A failing run now exits 1 straight away. Drop --enableRerun=3 from the test workflow and the @xmldom/xmldom dev dependency that only the JUnit parser used. vitest's in-suite retry (retry: 3 on CI) is unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LPMvJBQiyP6b7QjatACmnS * test: give the MSIX suite's setup PowerShell commands more time The autoUpdater MSIX suite installs its signing certificate and registers Electron as a sparse package in a before hook, each through PowerShell with a 30s timeout. While other spec files run alongside it, these two commands regularly take longer than that on windows-arm64 (the hook failed after roughly 60-90s), but they complete when the suite runs on its own, which is why the job only went green through the end-of-job solo rerun. vitest does not retry a failed hook, so with the rerun gone that would fail the job. This is a timeout increase on purpose: the operations themselves are just slow on a loaded host. Give those two setup commands 120s each and the hook 300s; every other PowerShell call keeps the 30s limit. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LPMvJBQiyP6b7QjatACmnS * test: make sure the browser sees user activation before createWritable() "File System API, denies permission when trying to create a writable file handle" failed every in-suite attempt on Linux and macOS with "SecurityError: User activation is required to request permissions", and the permission request handler was never called, so the browser-side FileSystemAccessPermissionContext saw no transient activation. It passed when re-run on its own. The test grants activation and calls createWritable() in the same executeJavaScript(code, true). The renderer reports the activation on the frame's channel-associated LocalFrameHost interface, but createWritable() reaches the browser on the file handle's own Mojo pipe, so the permission check can run before the activation update has been processed. Activate the page in a separate executeJavaScript() call first: its reply comes back on the channel-associated ElectronFrame interface, after the activation update. Do the same in the matching "allows permission" test, which makes the same request. This is the most likely explanation from the code and the in-suite traces; the failure doesn't reproduce on an idle Linux machine. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LPMvJBQiyP6b7QjatACmnS * test: poll for the restarted Network Service instead of sleeping 500ms The Network Service crash recovery specs crash the service and then sleep for a fixed 500ms before asserting that the next net.fetch() succeeds. On busy Windows runners the NetworkContext (or, for the utility process, the URLLoaderFactory update) isn't back yet after 500ms, and the fetch fails with net::ERR_FAILED. The test then only passed when re-run on its own. Poll a probe server with net.fetch() until a request succeeds, then make the asserted request. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LPMvJBQiyP6b7QjatACmnS * test: acknowledge utility process results before the child exits The api-net-spec.js fixture runs itUtility test bodies in a utility process and does postMessage(result) immediately followed by process.exit(0). The result travels over the parent port's message pipe and the exit is reported by the process host, so the exit can reach the parent first (or the message can be lost entirely when the process goes away before the port is flushed). On Windows this failed a scattering of net specs with "utility process exited (0) before posting a result", which then passed when re-run on their own. Make the fixture wait for an acknowledgement from the parent before it exits, on the failure paths too, and have waitForUtilityResult() send that acknowledgement once it has the result. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LPMvJBQiyP6b7QjatACmnS * test: navigate to a local page in the sandbox "exit" event spec "exposes "exit" event to preload script" registers a process 'exit' handler that sends an IPC, then navigates the hidden window to http://www.google.com. That is a cross-site navigation, so the 'exit' handler runs while the old document is being unloaded in a renderer that is about to go away, and on busy macOS runners the IPC sometimes never arrived (the test timed out after 30s and passed when re-run on its own). It also made the test depend on reaching the internet. Navigate to spec/fixtures/pages/blank.html instead. The unload then stays in the same renderer and the IPC is sent before the new page commits. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LPMvJBQiyP6b7QjatACmnS * test: reach the heap limit quickly in the heap snapshot utility spec "supports generating snapshots via v8.setHeapSnapshotNearHeapLimit" forks oom-grow.js with a 50MB old space limit and waits for it to run out of memory. The fixture grew the heap by one 1000-character string per setImmediate, so it took tens of thousands of event loop turns (about 8s on an idle Linux machine) to get near the limit, and on busy macos-x64 runners that went past the 60s timeout. It passed when re-run on its own. Allocate an array of 128K numbers per turn instead. The test now takes about 1s locally (1.2-1.7s with every core busy) and still produces the snapshot it checks for. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LPMvJBQiyP6b7QjatACmnS * test: wait for the guest to be destroyed in the USB/HID chooser specs The navigator.usb and navigator.hid "does not crash when the requesting webContents is destroyed from the select-*-device handler" specs call guest.destroy() in the handler and then check isDestroyed() after a single setTimeout() tick. destroy() only posts the teardown to the UI thread, so on a busy runner the check can run first and fail with "expected false to be true" (seen on macos-x64). Poll isDestroyed() until it is true instead. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LPMvJBQiyP6b7QjatACmnS * test: run the document.visibilityState/hidden specs serially "visibilityState changes when window is hidden" failed all four in-suite attempts on a macos-x64 main push with "expected 'hidden' to equal 'visible'" on the page's first report, before the test hides the window, and passed when re-run on its own. These specs run in the parallel phase, where other spec files open windows at the same time. On macOS a window that another one covers is occluded, and Chromium then reports its page as hidden. Tag the describe serial so its windows are the only ones on screen. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LPMvJBQiyP6b7QjatACmnS * test: run the protocol.handle throughput spec serially "protocol module handle is fast" compares the time to fetch a 128MB body directly and through a protocol.handle() interceptor and requires the median ratio to stay under 3. It ran in the parallel phase, where other spec files' Electron processes compete for the CPU and can slow one side of a pair more than the other; on the Chromium roll PR and on release branches it failed with "median ratio >= 3" and passed when re-run on its own. Tag it serial so the measurement has the machine to itself. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LPMvJBQiyP6b7QjatACmnS * test: wait longer for the preload code cache probe in its before hook The "preload code cache" suite's before hook loads a probe preload and waits up to 5s for its cache file to appear, to learn the process lock half of the file name. The browser writes that file from a BEST_EFFORT thread pool task, which a busy machine can hold back for longer than that; on macos-arm64 the hook failed with "timed out waiting for probe cache file to be written" and passed when re-run on its own. vitest does not retry hooks, so give this wait 30s (it returns as soon as the file is there) and the hook 60s. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LPMvJBQiyP6b7QjatACmnS * test: run the import/require parity fixture from a test, not a hook The "import / require parity" suite ran its fixture app, which starts a main, a utility and a renderer process, in a before hook with the 20s fixture timeout. On busy windows-x64 runners it sometimes did not finish in time ("Timed out after 20000ms waiting for ... import-require-parity to exit") and the suite only passed when re-run on its own. vitest does not retry hooks, so with that rerun gone a single slow launch would fail the job. Run the fixture from the first test instead and share the result with the other two. A failed or timed-out run is forgotten, so vitest's retry of that test starts a fresh one, like every other fixture launch in this file. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LPMvJBQiyP6b7QjatACmnS * test: skip already-destroyed webContents in cleanupWebContents() cleanupWebContents() takes a snapshot of webContents.getAllWebContents() and destroys each entry in turn. Destroying one can take others in the snapshot down with it (its guests or devtools, for example), and calling destroy() on one of those throws "Object has been destroyed". That failed the "chrome extensions" suite's after hook on linux-x64, and hooks are not retried, so the suite only passed when re-run on its own. Skip entries that are already destroyed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LPMvJBQiyP6b7QjatACmnS * test: focus the webview before requesting fullscreen in the ESC spec Since Chromium 157 refuses requestFullscreen() from a view that doesn't have focus (SuppressFullscreenFromUnfocusedView, the same change #54883 handled for the simpleFullScreen spec), "pressing ESC should emit the leave-html-full-screen event" has needed in-suite retries on macOS. The test used a hidden window, whose page can never be focused. Use a shown window, take app activation on macOS as #54883 does, focus the window and then the <webview> element, and wait until the guest page reports document.hasFocus() before calling requestFullscreen(). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LPMvJBQiyP6b7QjatACmnS * test: share the Network Service recovery probe between the crash specs Both crash-recovery specs now wait through waitForNetworkServiceRecovery(), which races each probe against a timeout and, for the utility process variant, the child's exit, so a stalled request or a dead child fails fast with its cause. The network-restart-test.js child is now registered with deferKillUtilityProcess so that a failed wait doesn't leak it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LPMvJBQiyP6b7QjatACmnS * test: refocus the window on each poll in the webview ESC spec Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LPMvJBQiyP6b7QjatACmnS * test: give every preload code cache write wait the same longer timeout The per-test waits on the cache write still used waitFor's 5s default, while the beforeAll probe waiting on the same thread pool write had 30s. They now share cacheWriteTimeout, and the suite's test timeout leaves room for it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LPMvJBQiyP6b7QjatACmnS * test: give the MSIX afterAll unregistration the setup timeout too Remove-AppxPackage runs in a hook under the same load as setup, and hooks are not retried. Both hook timeouts are now derived from powershellSetupTimeout so that they cover the commands they run. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LPMvJBQiyP6b7QjatACmnS * test: navigate the sandbox exit-event page cross-site to a local server The page now navigates to the suite's server on 127.0.0.1, so 'exit' is again covered on a cross-site, process-swapping navigation, as it was with google.com, without depending on the internet. The spec checks that the navigation did swap processes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LPMvJBQiyP6b7QjatACmnS * test: run createWritable() through webFrameMain.executeJavaScript() webFrameMain.executeJavaScript() updates the browser process's user activation state before it runs the script, so the permission request no longer races the activation update. This replaces the extra activation round trip. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LPMvJBQiyP6b7QjatACmnS * build: reject a leftover --enableRerun in the spec runner It is no longer a known option, so it would otherwise be forwarded to the Electron workers as a switch that silently does nothing. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LPMvJBQiyP6b7QjatACmnS * test: break comments added in this PR at sentence and clause boundaries Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LPMvJBQiyP6b7QjatACmnS * test: keep the individual-test rerun for now (moved to a follow-up) Per review, this PR now only fixes the specs that passed thanks to the end-of-job solo rerun, so the fixes can be backported. Restore the spec runner's --enableRerun mode, its @xmldom/xmldom dependency, --enableRerun=3 in the test workflow and the vitest.config.ts comment to main's versions. Removing the rerun moves to a follow-up once main has been clean for a few days. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LPMvJBQiyP6b7QjatACmnS * test: go back to a same-site navigation in the sandbox exit-event spec A cross-site navigation unloads the old page through RenderFrameHostImpl::Unload, which gives the renderer only kUnloadTimeout (500ms) before the process is torn down. That is the race this spec originally lost on busy runners. Navigate to the local blank.html again, so the unload stays in the same renderer, and drop the process-swap check and the IPC handshake that only the cross-site variant needed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LPMvJBQiyP6b7QjatACmnS * test: retry a Network Service recovery probe that times out A probe that got no response within 5s threw, and waitUntil fails on a thrown error, so one slow probe right after the crash failed the test although the next one would have succeeded. A timed-out probe now counts as a failed attempt; only the utility process exiting still fails right away. Since a probe can now be abandoned while its fetch is still in flight, the utility process fixture echoes an id sent with each fetch, and the spec waits for the reply with that id, so a late reply isn't taken as the reply to a later probe or to the final fetch. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LPMvJBQiyP6b7QjatACmnS * test: handle a rejected 'exit' wait before the first recovery probe once(child, 'exit') rejects if the utility process emits 'error', and nothing chained off it until the first recovery probe. An 'error' before then was an unhandled rejection, which fails the whole run rather than this test. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LPMvJBQiyP6b7QjatACmnS * test: log errors in the utility net fixture while it waits for the ack The fixture now stays up for a round trip after posting its result. In that window, leftover async work from the test body (a socket 'error' with no listener, a late unhandled rejection) could kill it with a non-zero code and fail a test that passed. While waiting for the ack, log such errors instead. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LPMvJBQiyP6b7QjatACmnS
384 lines
14 KiB
TypeScript
384 lines
14 KiB
TypeScript
import { BrowserWindow } from 'electron';
|
|
|
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
|
|
import * as fs from 'node:fs';
|
|
import { createRequire } from 'node:module';
|
|
import * as os from 'node:os';
|
|
import * as path from 'node:path';
|
|
import { pathToFileURL } from 'node:url';
|
|
|
|
import { spawnAndWait } from './lib/spec-helpers.ts';
|
|
|
|
const require = createRequire(import.meta.url);
|
|
|
|
const fixtureTimeout = 20000;
|
|
const fixtureKillTimeout = 5000;
|
|
|
|
const runFixture = async (appPath: string, args: string[] = []) => {
|
|
const spawnArgs = [appPath, ...args];
|
|
if (process.platform === 'darwin') spawnArgs.push('--use-mock-keychain');
|
|
return await spawnAndWait(process.execPath, spawnArgs, {
|
|
timeout: fixtureTimeout,
|
|
killTimeout: fixtureKillTimeout,
|
|
stripOutput: true
|
|
});
|
|
};
|
|
|
|
const fixturePath = path.resolve(import.meta.dirname, 'fixtures', 'esm');
|
|
|
|
describe('esm', () => {
|
|
describe('main process', () => {
|
|
it('should load an esm entrypoint', async () => {
|
|
const result = await runFixture(path.resolve(fixturePath, 'entrypoint.mjs'));
|
|
expect(result.code).toBe(0);
|
|
expect(result.stdout).toBe('ESM Launch, ready: false');
|
|
});
|
|
|
|
it('should load an esm entrypoint based on type=module', async () => {
|
|
const result = await runFixture(path.resolve(fixturePath, 'package'));
|
|
expect(result.code).toBe(0);
|
|
expect(result.stdout).toBe('ESM Package Launch, ready: false');
|
|
});
|
|
|
|
it('should wait for a top-level await before declaring the app ready', async () => {
|
|
const result = await runFixture(path.resolve(fixturePath, 'top-level-await.mjs'));
|
|
expect(result.code).toBe(0);
|
|
expect(result.stdout).toBe('Top level await, ready: false');
|
|
});
|
|
|
|
it('should allow usage of pre-app-ready apis in top-level await', async () => {
|
|
const result = await runFixture(path.resolve(fixturePath, 'pre-app-ready-apis.mjs'));
|
|
expect(result.code).toBe(0);
|
|
});
|
|
|
|
it('should allow use of dynamic import', async () => {
|
|
const result = await runFixture(path.resolve(fixturePath, 'dynamic.mjs'));
|
|
expect(result.code).toBe(0);
|
|
expect(result.stdout).toBe('Exit with app, ready: false');
|
|
});
|
|
|
|
it("import 'electron/lol' should throw", async () => {
|
|
const result = await runFixture(path.resolve(fixturePath, 'electron-modules', 'import-lol.mjs'));
|
|
expect(result.code).toBe(1);
|
|
expect(result.stderr).toMatch(/Error \[ERR_MODULE_NOT_FOUND\]/);
|
|
});
|
|
|
|
it("import 'electron/main' should not throw", async () => {
|
|
const result = await runFixture(path.resolve(fixturePath, 'electron-modules', 'import-main.mjs'));
|
|
expect(result.code).toBe(0);
|
|
});
|
|
|
|
it("import 'electron/renderer' should not throw", async () => {
|
|
const result = await runFixture(path.resolve(fixturePath, 'electron-modules', 'import-renderer.mjs'));
|
|
expect(result.code).toBe(0);
|
|
});
|
|
|
|
it("import 'electron/common' should not throw", async () => {
|
|
const result = await runFixture(path.resolve(fixturePath, 'electron-modules', 'import-common.mjs'));
|
|
expect(result.code).toBe(0);
|
|
});
|
|
|
|
it("import 'electron/utility' should not throw", async () => {
|
|
const result = await runFixture(path.resolve(fixturePath, 'electron-modules', 'import-utility.mjs'));
|
|
expect(result.code).toBe(0);
|
|
});
|
|
});
|
|
|
|
// The `electron` module that `import` sees is produced by wrapping the one
|
|
// `require()` returns, so the two should expose exactly the same names bound
|
|
// to exactly the same objects in every process type that can load ESM.
|
|
describe('import / require parity', () => {
|
|
// The fixture starts a main, a utility and a renderer process,
|
|
// so it is run once and shared by the tests below.
|
|
// It runs from the first test rather than a beforeAll hook,
|
|
// so that a run that fails or times out on a busy machine is retried
|
|
// like any other test (hooks are not);
|
|
// a failed run is forgotten so that the retry starts a fresh one.
|
|
let results: Promise<Record<string, string[]>> | null = null;
|
|
const getResults = () => {
|
|
results ??= (async () => {
|
|
const result = await runFixture(path.resolve(fixturePath, 'import-require-parity'));
|
|
expect([0, 1], `fixture did not run to completion:\n${result.stderr}`).toContain(result.code);
|
|
return JSON.parse(result.stdout.split('\n').pop()!);
|
|
})().catch((error) => {
|
|
results = null;
|
|
throw error;
|
|
});
|
|
return results;
|
|
};
|
|
|
|
for (const processType of ['main', 'utility', 'renderer']) {
|
|
it(`exposes the same electron module to import and require() in the ${processType} process`, async () => {
|
|
const parity = await getResults();
|
|
expect(parity).toHaveProperty([processType]);
|
|
expect(parity[processType]).toEqual([]);
|
|
});
|
|
}
|
|
});
|
|
|
|
describe('renderer process', () => {
|
|
let w: BrowserWindow | null = null;
|
|
const tempDirs: string[] = [];
|
|
|
|
afterEach(async () => {
|
|
if (w) w.close();
|
|
w = null;
|
|
while (tempDirs.length) {
|
|
await fs.promises.rm(tempDirs.pop()!, { force: true, recursive: true });
|
|
}
|
|
});
|
|
|
|
async function loadWindowWithPreload(preload: string, webPreferences: Electron.WebPreferences) {
|
|
const tmpDir = await fs.promises.mkdtemp(path.resolve(os.tmpdir(), 'e-spec-preload-'));
|
|
tempDirs.push(tmpDir);
|
|
const preloadPath = path.resolve(tmpDir, 'preload.mjs');
|
|
await fs.promises.writeFile(preloadPath, preload);
|
|
|
|
w = new BrowserWindow({
|
|
show: false,
|
|
webPreferences: {
|
|
...webPreferences,
|
|
preload: preloadPath
|
|
}
|
|
});
|
|
|
|
let error: Error | null = null;
|
|
w.webContents.on('preload-error', (_, __, err) => {
|
|
error = err;
|
|
});
|
|
|
|
await w.loadFile(path.resolve(fixturePath, 'empty.html'));
|
|
|
|
return [w.webContents, error] as [Electron.WebContents, Error | null];
|
|
}
|
|
|
|
describe('nodeIntegration', () => {
|
|
let badFilePath = '';
|
|
|
|
beforeEach(async () => {
|
|
badFilePath = path.resolve(path.resolve(os.tmpdir(), 'bad-file.badjs'));
|
|
await fs.promises.writeFile(badFilePath, 'const foo = "bar";');
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await fs.promises.unlink(badFilePath);
|
|
});
|
|
|
|
it('should support an esm entrypoint', async () => {
|
|
const [webContents] = await loadWindowWithPreload(
|
|
'import { resolve } from "path"; window.resolvePath = resolve;',
|
|
{
|
|
nodeIntegration: true,
|
|
sandbox: false,
|
|
contextIsolation: false
|
|
}
|
|
);
|
|
|
|
const exposedType = await webContents.executeJavaScript('typeof window.resolvePath');
|
|
expect(exposedType).toBe('function');
|
|
});
|
|
|
|
it('should delay load until the ESM import chain is complete', async () => {
|
|
const [webContents] = await loadWindowWithPreload(
|
|
`import { resolve } from "path";
|
|
await new Promise(r => setTimeout(r, 500));
|
|
window.resolvePath = resolve;`,
|
|
{
|
|
nodeIntegration: true,
|
|
sandbox: false,
|
|
contextIsolation: false
|
|
}
|
|
);
|
|
|
|
const exposedType = await webContents.executeJavaScript('typeof window.resolvePath');
|
|
expect(exposedType).toBe('function');
|
|
});
|
|
|
|
it('should support a top-level await fetch blocking the page load', async () => {
|
|
const [webContents] = await loadWindowWithPreload(
|
|
`
|
|
const r = await fetch("package/package.json");
|
|
window.packageJson = await r.json();`,
|
|
{
|
|
nodeIntegration: true,
|
|
sandbox: false,
|
|
contextIsolation: false
|
|
}
|
|
);
|
|
|
|
const packageJson = await webContents.executeJavaScript('window.packageJson');
|
|
expect(packageJson).toEqual(require('./fixtures/esm/package/package.json'));
|
|
});
|
|
|
|
const hostsUrl = pathToFileURL(
|
|
process.platform === 'win32' ? 'C:\\Windows\\System32\\drivers\\etc\\hosts' : '/etc/hosts'
|
|
);
|
|
|
|
describe('without context isolation', () => {
|
|
it('should use Blinks dynamic loader in the main world', async () => {
|
|
const [webContents] = await loadWindowWithPreload('', {
|
|
nodeIntegration: true,
|
|
sandbox: false,
|
|
contextIsolation: false
|
|
});
|
|
|
|
let error: Error | null = null;
|
|
try {
|
|
await webContents.executeJavaScript(`import(${JSON.stringify(hostsUrl)})`);
|
|
} catch (err) {
|
|
error = err as Error;
|
|
}
|
|
|
|
expect(error).not.toBe(null);
|
|
// This is a blink specific error message
|
|
expect(error?.message).toContain('Failed to fetch dynamically imported module');
|
|
});
|
|
|
|
it('should use Node.js ESM dynamic loader in the preload', async () => {
|
|
const [, preloadError] = await loadWindowWithPreload(
|
|
`await import(${JSON.stringify(pathToFileURL(badFilePath))})`,
|
|
{
|
|
nodeIntegration: true,
|
|
sandbox: false,
|
|
contextIsolation: false
|
|
}
|
|
);
|
|
|
|
expect(preloadError).not.toBe(null);
|
|
// This is a node.js specific error message
|
|
expect(preloadError!.toString()).toContain('Unknown file extension');
|
|
});
|
|
|
|
it('should use import.meta callback handling from Node.js for Node.js modules', async () => {
|
|
const result = await runFixture(path.resolve(fixturePath, 'import-meta'));
|
|
expect(result.code).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('with context isolation', () => {
|
|
it('should use Node.js ESM dynamic loader in the isolated context', async () => {
|
|
const [, preloadError] = await loadWindowWithPreload(
|
|
`await import(${JSON.stringify(pathToFileURL(badFilePath))})`,
|
|
{
|
|
nodeIntegration: true,
|
|
sandbox: false,
|
|
contextIsolation: true
|
|
}
|
|
);
|
|
|
|
expect(preloadError).not.toBe(null);
|
|
// This is a node.js specific error message
|
|
expect(preloadError!.toString()).toContain('Unknown file extension');
|
|
});
|
|
|
|
it('should use Blinks dynamic loader in the main world', async () => {
|
|
const [webContents] = await loadWindowWithPreload('', {
|
|
nodeIntegration: true,
|
|
sandbox: false,
|
|
contextIsolation: true
|
|
});
|
|
|
|
let error: Error | null = null;
|
|
try {
|
|
await webContents.executeJavaScript(`import(${JSON.stringify(hostsUrl)})`);
|
|
} catch (err) {
|
|
error = err as Error;
|
|
}
|
|
|
|
expect(error).not.toBe(null);
|
|
// This is a blink specific error message
|
|
expect(error?.message).toContain('Failed to fetch dynamically imported module');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('without nodeIntegration and contextIsolation', () => {
|
|
it('exposes Node globals to an ESM preload script', async () => {
|
|
const [webContents] = await loadWindowWithPreload(
|
|
`globalThis.preloadNodeGlobals = {
|
|
processType: typeof process,
|
|
bufferType: typeof Buffer,
|
|
setImmediateType: typeof setImmediate,
|
|
globalType: typeof global
|
|
};`,
|
|
{
|
|
nodeIntegration: false,
|
|
sandbox: false,
|
|
contextIsolation: false
|
|
}
|
|
);
|
|
|
|
const globals = await webContents.executeJavaScript('globalThis.preloadNodeGlobals');
|
|
expect(globals).toEqual({
|
|
processType: 'object',
|
|
bufferType: 'function',
|
|
setImmediateType: 'function',
|
|
globalType: 'object'
|
|
});
|
|
});
|
|
|
|
it('removes Node globals from the renderer main world after the preload runs', async () => {
|
|
const [webContents] = await loadWindowWithPreload('globalThis.preloadFinished = true;', {
|
|
nodeIntegration: false,
|
|
sandbox: false,
|
|
contextIsolation: false
|
|
});
|
|
|
|
const pageGlobals = await webContents.executeJavaScript(`({
|
|
preloadFinished: globalThis.preloadFinished,
|
|
processType: typeof process,
|
|
bufferType: typeof Buffer,
|
|
setImmediateType: typeof setImmediate,
|
|
globalType: typeof global
|
|
})`);
|
|
expect(pageGlobals).toEqual({
|
|
preloadFinished: true,
|
|
processType: 'undefined',
|
|
bufferType: 'undefined',
|
|
setImmediateType: 'undefined',
|
|
globalType: 'undefined'
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('electron modules', () => {
|
|
it("import 'electron/lol' should throw", async () => {
|
|
const [, error] = await loadWindowWithPreload('import { ipcRenderer } from "electron/lol";', {
|
|
sandbox: false
|
|
});
|
|
expect(error).not.toBe(null);
|
|
expect(error?.message).toMatch(/Cannot find package 'electron'/);
|
|
});
|
|
|
|
it("import 'electron/main' should not throw", async () => {
|
|
const [, error] = await loadWindowWithPreload('import { ipcRenderer } from "electron/main";', {
|
|
sandbox: false
|
|
});
|
|
expect(error).toBe(null);
|
|
});
|
|
|
|
it("import 'electron/renderer' should not throw", async () => {
|
|
const [, error] = await loadWindowWithPreload('import { ipcRenderer } from "electron/renderer";', {
|
|
sandbox: false
|
|
});
|
|
expect(error).toBe(null);
|
|
});
|
|
|
|
it("import 'electron/common' should not throw", async () => {
|
|
const [, error] = await loadWindowWithPreload('import { ipcRenderer } from "electron/common";', {
|
|
sandbox: false
|
|
});
|
|
expect(error).toBe(null);
|
|
});
|
|
|
|
it("import 'electron/utility' should not throw", async () => {
|
|
const [, error] = await loadWindowWithPreload('import { ipcRenderer } from "electron/utility";', {
|
|
sandbox: false
|
|
});
|
|
expect(error).toBe(null);
|
|
});
|
|
});
|
|
});
|
|
});
|