Compare commits

...

135 Commits

Author SHA1 Message Date
Samuel Attard
8bc4bc55f1 Revert "ci: split macos-x64 tests into 3 shards (#50968)"
This reverts commit a57dbb55cc.
2026-04-13 01:00:44 -07:00
Samuel Attard
9ef4ca2a7b test: fix remaining linux-x64 unhandled rejections
- extensions.spec.ts: the two 'does not take precedence over Electron
  webRequest' tests use an async IIFE inside new Promise(). onBeforeRequest/
  onBeforeSendHeaders fires on the navigation itself, so the outer promise
  resolves (and the load is cancelled/aborted by teardown) before the IIFE's
  awaited loadURL settles; the IIFE then rejects unhandled. .catch() the IIFE.
- api-service-worker-main.spec.ts: loadWorkerScript() wraps wc.loadURL and all
  24 call sites are fire-and-forget; wrap the helper's return in
  dangerouslyIgnoreWebContentsLoadResult. Also .catch() the once(ipcMain,'ping',
  {signal}).then() chain so abortController.abort() in finally doesn't surface
  an AbortError.
- serial/chromium-focus-and-clipboard.spec.ts: executeJavaScript returned the
  WindowProxy from window.open(), which can't be structured-cloned over IPC;
  append '; null' so the result is serializable.
2026-04-13 01:00:15 -07:00
Samuel Attard
2a6960541e fix: don't call process.exit from uncaughtException after vitest loads
vitest's fork worker patches process.exit to throw. Our worker-entry
uncaughtException handler called process.exit(1), which then threw inside
the handler, so Node exited with code 7 ('exception handler threw') and the
original error was hidden. Scope the handler to bootstrap only and remove it
once workers/forks.js is loaded.

Also fix the crash diagnostic: WorkerRequest carries files at
message.context.files (FileSpecification[]), not message.files.
2026-04-13 01:00:15 -07:00
Samuel Attard
e90456a9d6 build: distribute spec/serial/* across shards alongside parallel files
split-tests now buckets spec/*.spec.ts and spec/serial/*.spec.ts as two
independent sets (each sorted by test count, round-robined) so every shard
gets a proportional slice of both. run.js already routes spec/serial/* to
the --no-file-parallelism phase, so within a shard serial files still run
one-at-a-time. This also fixes the serial phase never running on CI — it
was skipped because split-tests only emitted parallel positionals.
2026-04-13 01:00:15 -07:00
Samuel Attard
8c92be4037 test: fix window leaks in api-subframe, api-view, api-web-frame, api-web-utils
- api-subframe: 'should not crash when changing subframe src' never closed w
- api-view: 'can be added as a child of another View' declared a local const w
  shadowing the describe-level one the afterEach closes
- api-web-frame / api-web-utils: defer(() => w.close()) and afterAll win.close()
  don't await 'closed', so windows were still in getAllWindows() when the
  global leak check fired; use closeWindow() which awaits
2026-04-13 01:00:14 -07:00
Samuel Attard
76900132a9 build: log which files a worker was running when it exits unexpectedly
ElectronPoolWorker now tracks the last 'files' payload sent to the child and
prints it (with pid, code, signal) if the child exits non-zero outside stop().
2026-04-13 01:00:14 -07:00
Samuel Attard
3100799343 refactor: move no-windows-leaked assertion to a global afterAll
closeWindow's assertNotWindows path tried to assert 'this was the only
window' one tick after closing, which only worked if the next-tick fired
after the outer afterEach(closeAllWindows) — a race that mostly held under
mocha and doesn't under vitest.

Replace it with assertNoWindowsLeaked() registered as a setupFiles-level
afterAll — it runs once per file after every test-file afterAll, so suites
that share a window across tests (useRemoteContext, the api-context-bridge
reuse, etc.) aren't flagged on every test. Verified ordering with probe
files: a describe-level afterAll(closeWindow) runs before the global check,
and a file that never cleans up is flagged.

closeWindow is now a plain awaited close; the assertNotWindows option and
its 7 call-site opt-outs are removed. useRemoteContext's afterAll now awaits
'closed' so the shared window is actually gone before the leak check fires.
2026-04-13 01:00:14 -07:00
Samuel Attard
d9c2df194f build: use 2 test shards for linux-x64-asan
ASAN instrumented binaries are slower; keep asan at 2 shards while regular
Linux runs a single shard.
2026-04-13 01:00:13 -07:00
Samuel Attard
9a6e0a0251 test: consume {type:'ready'} and send shutdown in api-app setProxy utility test
api-net's shared-utility-process commit changed api-net-spec.js to post a
ready handshake and wait for an explicit shutdown instead of exiting after
each message. This test forks the same fixture and was still assuming the
old one-shot protocol, so it read 'ready' as the result (data.ok undefined).
2026-04-13 00:59:57 -07:00
Samuel Attard
06f54c3ef1 build: reduce Linux test shards from 3 to 1
vitest's file-level parallelism makes per-shard wall-clock short enough that
the fixed per-job setup cost dominates; one 8-core job beats three.
2026-04-13 00:59:56 -07:00
Samuel Attard
2e175b96a0 fix: guard useRemoteContext afterAll close() with isDestroyed()
If the remote-context window was already torn down (closeAllWindows, crash,
or a prior test's cleanup), w.close() throws 'Object has been destroyed'.
2026-04-13 00:59:30 -07:00
Samuel Attard
b337bdd789 test: fix vacuous expect(async fn).to.not.throw() in BrowserView destroy test
The assertion passed an async function to .to.not.throw(), which only checks
sync throws, so it was always green and the unawaited body leaked the
'data:text/html,hello there' loadURL rejection as unhandled. Invoke the IIFE
and await expect(p).to.not.be.rejected() instead.
2026-04-13 00:59:30 -07:00
Samuel Attard
3e0125da61 build: run macos-arm64 tests on macos-15-xlarge; maxWorkers 3 for arm64
macos-15 (4 vCPU / 7 GB) was starving — several workers missed the 90s
startup window. xlarge gives the same headroom x64 gets.
2026-04-13 00:59:30 -07:00
Samuel Attard
e363fd2136 test: move TAB-focus and darwin notification-event tests to spec/serial/
The 'focus handling' describe in chromium.spec.ts (w.focus() + sendInputEvent
TAB cycling) and the three darwin 'emits show and close events' Notification
tests both depend on OS-singleton state (window focus, Notification Center).
2026-04-13 00:59:30 -07:00
Samuel Attard
aed406000e lint: exclude spec/fixtures/** from spec-only rules
Fixture JS files are standalone Electron apps; they can't import spec-helpers
so no-unawaited-load (and the other spec rules) are noise there.
2026-04-13 00:59:29 -07:00
Samuel Attard
28fb9f0965 chore: wrap unawaited loadURL/loadFile in dangerouslyIgnoreWebContentsLoadResult 2026-04-13 00:59:29 -07:00
Samuel Attard
8c887520fd build: cap vitest maxWorkers on macOS CI (arm64: 2, x64: 6)
macos-arm64 tests run on the 4-vCPU / 7 GB macos-15 runner; three concurrent
Electron main processes (cpus-1 default) plus their GPU/network-service
children exhausted it and several workers missed the 90s start window.
macos-x64 uses macos-15-large (12-vCPU / 30 GB); cap at 6 for headroom.
2026-04-13 00:59:29 -07:00
Samuel Attard
5f7a2f99e1 fix: await closeWindow's assert-no-windows check; add no-unawaited-load lint rule
closeWindow's assertNotWindows path scheduled a detached setTimeout that
asserted BaseWindow.getAllWindows().length === 0; by the time the timer
fired the next test's beforeEach had created windows, so the assertion
rejected unhandled. Now it's an inline awaited check so the failure is
attributed to the right test.

no-unawaited-load flags loadURL()/loadFile() calls whose promise isn't
awaited, returned, assigned, .then/.catch'd, passed as an argument, or
explicitly voided. 393 warnings across spec/ — set to 'warn' for now.
2026-04-13 00:59:28 -07:00
Samuel Attard
032cb1d26c chore: faster darwin update tests 2026-04-13 00:59:28 -07:00
Samuel Attard
ce203ae0fc test: isolate crash-case child Electrons with per-test --user-data-dir; fix express/psList CJS imports
crash.spec spawns child Electrons that defaulted to the shared userData
profile; under parallel workers that contended with other test-spawned
Electrons (autoupdater etc.). Each child now gets a mkdtemp --user-data-dir
that afterEach removes (even on timeout, since the kill+rmSync runs in the
hook).

api-autoupdater-darwin: 'import * as express' under vite SSR gives a
namespace object, so express() fails with '__vite_ssr_import_N__ is not a
function'. Use 'import = require()' for express and psList which are
called directly as functions.
2026-04-13 00:59:28 -07:00
Samuel Attard
56d213bc96 test: use query (encoded) + userData dir for unload/close fixture output path
search: passes the path raw and os.tmpdir() may not be the right tmp inside
the CI container; query: {out} URL-encodes properly and userData is the
per-worker mkdtemp we control.
2026-04-13 00:59:28 -07:00
Samuel Attard
b6494c70b1 test: move BrowserView setBackgroundColor screen-capture tests to spec/serial
These capture the primary display and assert center-pixel colour, which
breaks when a parallel worker has a window visible.
2026-04-13 00:59:27 -07:00
Samuel Attard
a23a4b8f5c test: skip blur-during-destroy test in api-web-contents-view
The 'does not crash when closed via window.close()' test (added in #47933)
listens for 'blur' during WebContents destruction; on macOS that Emit runs
a microtask checkpoint after weak_factory_ is already torn down, so any
re-entrant Destroy() -> GetWeakPtr() DCHECKs at base/memory/weak_ptr.h:298.
Skipped with a TODO until the native lifecycle is fixed.

Also rewrites the 'accepts existing webContents object' count assertion to
compare id sets so still-closing contents from prior tests don't skew it.
2026-04-13 00:59:27 -07:00
Samuel Attard
dd93cd1fa9 test: fix linux shard failures (in-app-purchase, browser-view, web-contents-view, modules/esm)
- api-in-app-purchase: wrap in ifdescribe(darwin) instead of early-return,
  which left an empty suite that vitest treats as an error
- api-browser-view: custom afterEach nulls w before defer() callbacks run;
  call runCleanupFunctions() first so defer(() => w.removeBrowserView(...))
  sees a live window
- api-web-contents-view: afterEach used contents.close() without awaiting
  destruction, so the next test's getAllWebContents() count included
  still-closing leftovers; use cleanupWebContents() which awaits 'destroyed'
- modules/esm: run the ESM-vs-CJS key comparison in a child Electron (outside
  vite's alias) via a .mjs fixture, sort both lists, and filter Node's
  CJS-interop 'default'/'module.exports' keys before comparing
2026-04-13 00:59:27 -07:00
Samuel Attard
dc8cd08dc7 perf: reuse a single BrowserWindow per sandbox mode in api-context-bridge
makeBindingWindow now lazily creates one window per describe and swaps
preloads via session.registerPreloadScript/unregisterPreloadScript instead
of constructing a fresh BrowserWindow per test. ~21s -> ~9s for 144 tests.
2026-04-13 00:59:26 -07:00
Samuel Attard
6bbc801fc2 lint: add /** @remote no-locals */ mode and tag callWithBindings
no-locals is stricter than plain @remote: closures may not reference
ANY identifier declared in the file (imports from anywhere, or file-scope
const/let/var/function/class). JS/DOM globals are fine. Applied to
callWithBindings in api-context-bridge.spec.ts, whose closures are
executeJavaScript'd in a page world with no __rt shim and no require.
2026-04-13 00:59:26 -07:00
Samuel Attard
4a506e7e25 fix: route contextBridge through remote-tools for makeBindingWindow closures
makeBindingWindow stringifies its bindingCreator closure into a preload
script; under vite the captured 'contextBridge' import becomes
__vite_ssr_import_N__.contextBridge. Tag makeBindingWindow /** @remote */,
run rewriteForRemoteEval on the closure, and declare 'const __rt = renderer_1'
in the preload so __rt.contextBridge resolves.

remote-tools.ts re-exports contextBridge/ipcRenderer/webFrame from
electron/renderer — undefined at runtime in the main process, but typed
correctly for closures that only ever execute in a preload.

callWithBindings closures only reference their 'root' param, so no change
needed there. 144/144 pass.
2026-04-13 00:59:26 -07:00
Samuel Attard
59b3e45d0c perf: share a single utility process across itUtility tests in api-net
Lazy-fork once, await a 'ready' handshake, then post each test body to the
same long-lived child. The fixture no longer exits after each message; it
posts {ok:false} on failure and keeps running, and exits cleanly on a
{type:'shutdown'} message sent from afterAll.
2026-04-13 00:59:26 -07:00
Samuel Attard
2276abbd96 feat: support /** @remote */ tagging in the remote-tools-imports lint rule
Functions declared with a /** @remote */ JSDoc (or const/for-of bindings
so tagged) are treated like itremote/remotely: any closure passed to them
is checked for imports that don't come from ./lib/remote-tools.

api-net.spec.ts's itUtility stringifies its closure and posts it to a
utility-process fixture, so it's the same __vite_ssr_import_ problem.
- itUtility now runs rewriteForRemoteEval(fn) before posting
- the fixture declares a local __rt spreading net-helpers + electron/main
- the for-of loop that aliases itUtility as 'test' is tagged @remote
- remote-tools.ts re-exports net, session, http, defer, and the net-helpers
- api-net.spec.ts imports those via remote-tools (closure bodies unchanged)

api-net: 252 passed / 2 todo.
2026-04-13 00:59:25 -07:00
Samuel Attard
5b10d20d38 chore: update yarn.lock after dropping sinon from spec deps 2026-04-13 00:59:25 -07:00
Samuel Attard
88ec0f4bef test: await two previously-unawaited waitUntil calls
Both passed without actually waiting for the condition; now that waitUntil
takes ctx.signal they at least stopped on abort, but the assertions were
still never gating the test.
2026-04-13 00:59:25 -07:00
Samuel Attard
cb60689173 test: replace sinon with vi.mock in version-bump and drop sinon dep
Same ESM-vs-CJS stub issue as release-notes: sinon.stub on
require('node:child_process') doesn't intercept the ESM import that
version-bumper.ts uses under vite. vi.mock('node:child_process', {spy:true})
does. sinon is no longer used anywhere in spec/.
2026-04-13 00:59:24 -07:00
Samuel Attard
34c05cd1a5 refactor: thread ctx.signal through waitUntil so polling stops on test abort 2026-04-13 00:59:24 -07:00
Samuel Attard
4929a267e6 test: replace sinon stub with vi.mock for spawnSync in release-notes
Under vite, notes.ts imports spawnSync via ESM; sinon.stub on the CJS
require() object doesn't intercept that binding. vi.mock('node:child_process',
{spy: true}) replaces the module in vite's graph so vi.mocked(cp.spawnSync)
.mockImplementation() applies to the import notes.ts sees.
2026-04-13 00:59:24 -07:00
Samuel Attard
7d31ad9297 test: write unload/close fixture output to per-test tmp paths
close.html and unload.html previously wrote to __dirname + '/close'|'/unload'
inside spec/fixtures/api/ itself. Pass the output path via ?out= query param
and use a pid+timestamp tmp file so parallel workers and retries never race.
2026-04-13 00:59:24 -07:00
Samuel Attard
8cebedc7b6 test: fix nested it() calls that should have been describe()/ifdescribe()
16 inner it() calls in api-browser-window.spec.ts were wrapped in an outer
it() or ifit() where the author meant describe()/ifdescribe(). Under mocha
these inner tests were registered as siblings at collection time; under
vitest they throw. Also removes two unused chai imports in api-safe-storage
left over from moving chai.use() to setup.ts.
2026-04-13 00:59:23 -07:00
Samuel Attard
69c9ff51eb lint: add no-nested-tests rule for it()/ifit() nested in test bodies
Mocha silently tolerated it() inside another it()/ifit() body; vitest does
not. The rule tracks test-call nesting depth and flags any inner definition.
2026-04-13 00:59:23 -07:00
Samuel Attard
64f3c1c199 chore: route itremote/remotely closure imports through remote-tools
Swaps expect/setTimeout/BrowserWindow imports in the 5 files flagged by
remote-tools-imports/no-foreign-imports-in-remote-closure so their
stringified closures resolve via the __rt shim. Import-line changes only.
2026-04-13 00:59:23 -07:00
Samuel Attard
0d321ccae0 lint: add rule banning foreign imports in itremote/remotely closures
remote-tools-imports/no-foreign-imports-in-remote-closure flags any
identifier inside an itremote()/remotely() closure that is bound to an
import from somewhere other than spec/lib/remote-tools. Those bindings
get rewritten to __vite_ssr_import_N__ by vite's SSR transform and fail
when the closure is stringified and eval'd in a remote context.

Skips type-only imports and type-annotation positions. Currently flags
50 sites across 5 files (node, webview, chromium, fuses, api-native-image).
2026-04-13 00:59:22 -07:00
Samuel Attard
36e5f6a2a5 feat: add spec/lib/remote-tools for imports used in itremote/remotely closures
vite's SSR transform rewrites import bindings to __vite_ssr_import_N__,
which breaks closures that are stringified and eval'd in a remote context.

remote-tools.ts re-exports the common modules (path, fs, url, expect, ...)
so spec files can 'import { path, expect } from ./lib/remote-tools' and
closure bodies stay unchanged. runRemote/remotely regex-replace every
__vite_ssr_import_N__ with __rt (a renderer-side object whose keys mirror
the remote-tools export names), so __rt.path.join(...) etc. resolve with
no property-name ambiguity.

asar.spec.ts: 170 failing itremote tests -> 0, import-line changes only.
2026-04-13 00:59:22 -07:00
Samuel Attard
65f2dee205 build: use 8-core Linux test runners for parallel vitest workers 2026-04-13 00:59:22 -07:00
Samuel Attard
c2304737f1 fix: correct defer() ordering and make runCleanupFunctions drain-safe
vitest's onTestFinished runs *after* afterEach (contrary to the earlier
assumption), so defer() callbacks saw windows already destroyed by
afterEach(closeAllWindows). And runCleanupFunctions didn't clear its
queue on throw, so one bad defer cascaded into every subsequent test.

- runCleanupFunctions now splices the queue before iterating and wraps
  each cleanup in try/catch, aggregating errors.
- closeAllWindows runs runCleanupFunctions() first, so the 190+ existing
  afterEach(closeAllWindows) call sites get mocha's ordering (defers run,
  then windows close) without modification. setup.ts's onTestFinished
  remains as a fallback for tests that defer() without closeAllWindows.
- cleanupWebContents guards against already-destroyed contents.
2026-04-13 00:59:21 -07:00
Samuel Attard
8e51023155 test: await closeWindow(c) in 'should attach child window to parent'
The unawaited async call let the child window linger into the next test's
afterEach assertNotWindows check.
2026-04-13 00:59:21 -07:00
Samuel Attard
7cdd1314d2 fix: drop unused assertNotWindows param from closeAllWindows
vitest passes a context object as the first arg to hook callbacks, so
afterEach(closeAllWindows) effectively called closeAllWindows(ctx) - a
truthy value that triggered the unused assert-no-windows-remain path,
producing unhandled rejections. afterAll(closeAllWindows) failed outright
with a FixtureParseError because vitest parses the param list.

No caller ever passed the argument explicitly, so the param is removed.
2026-04-13 00:59:21 -07:00
Samuel Attard
984f5c5023 test: isolate OS-singleton tests into spec/serial/ for post-parallel execution 2026-04-13 00:59:21 -07:00
Samuel Attard
abadff1643 test: deflake 'creates unique session id for each target'
Await each CDP step in sequence and use Runtime.evaluate on the attached
session after Debugger.enable is acknowledged, so Debugger.scriptParsed is
guaranteed to fire regardless of page-load timing. 0/10 flakes (was ~3/5).
2026-04-13 00:59:20 -07:00
Samuel Attard
6beb0f3de3 fix: use inline require() in stringified utility-process closure
The closure at spec/api-app.spec.ts:2282 is stringified and eval'd inside
a utility-process fixture. Under vite's SSR transform the captured import
bindings become __vite_ssr_import_N__ refs that don't exist in the fixture.
Inline require() calls survive toString() intact.
2026-04-13 00:59:20 -07:00
Samuel Attard
2e826cdaa4 fix: align runner app name, userData path, and decouple net-helpers from vitest
- spec/_vitest_runner/package.json: name/productName match spec/package.json
  so app.name assertions hold
- worker-entry: userData is <mkdtemp>/<app.name>/ so getPath('userData')
  still includes the app name
- net-helpers: import defer from defer-helpers and own listen() directly,
  so the utility-process fixture (which ts-node-requires net-helpers) no
  longer pulls in spec-helpers -> vitest
2026-04-13 00:59:20 -07:00
Samuel Attard
dc02d7907b fix: use regex alias for electron/* and split defer helpers from spec-helpers
vite's resolve.alias in object form matched the bare 'electron' specifier
but not 'electron/main' etc. The array-with-regex form matches all four.

Also extract defer/runCleanupFunctions into spec/lib/defer-helpers.ts so
setup.ts doesn't transitively import 'electron/main' (setupFiles go
through a slightly different SSR load path than test files).
2026-04-13 00:59:19 -07:00
Samuel Attard
3bb67099de chore: remove remaining mocha references
- refresh-page fixture served mocha.js as a 'large JS file' over a custom
  protocol; point it at chai.js instead since mocha is no longer a dep
- drop descriptive 'mocha' mentions from comments
2026-04-13 00:59:19 -07:00
Samuel Attard
6fea5e8691 build: switch spec runner from mocha to vitest 2026-04-13 00:59:19 -07:00
Samuel Attard
7a5dc9e725 chore: remove ELECTRON_FORCE_TEST_SUITE_EXIT mechanism
This was a win-arm64 workaround for the mocha runner: process.exit() could
hang in the single Electron process, so the suite SIGTERMed itself after
printing the failure count to stdout for script/spec-runner.js to scrape.

Under vitest, the process that decides and reports the exit code is the
plain-Node vitest CLI; Electron instances are pool workers that the pool
tears down. The workaround no longer applies.
2026-04-13 00:59:19 -07:00
Samuel Attard
2067a6b975 chore: port CI-only behaviour and defer() ordering from spec/index.js
- allowOnly: !CI matches mocha's forbidOnly
- retry: 3 on CI matches mocha's CI retries
- runCleanupFunctions via ctx.onTestFinished so defer()-ed cleanups run
  before test-file afterEach hooks, matching the per-suite hook ordering
  in spec/index.js
2026-04-13 00:59:18 -07:00
Samuel Attard
3dbd48441e chore: replace ifit/ifdescribe addOnly shim with vitest runIf 2026-04-13 00:59:18 -07:00
Samuel Attard
6eee70a432 chore: migrate webview-spec.ts to vitest 2026-04-13 00:59:18 -07:00
Samuel Attard
07cdd3543b chore: migrate visibility-state-spec.ts to vitest 2026-04-13 00:59:17 -07:00
Samuel Attard
e7d867952c chore: migrate version-bump-spec.ts to vitest 2026-04-13 00:59:17 -07:00
Samuel Attard
e4474b232e chore: migrate types-spec.ts to vitest 2026-04-13 00:59:17 -07:00
Samuel Attard
d39cbc204b chore: migrate spellchecker-spec.ts to vitest 2026-04-13 00:59:17 -07:00
Samuel Attard
b1f3befe21 chore: migrate security-warnings-spec.ts to vitest 2026-04-13 00:59:16 -07:00
Samuel Attard
6d99359799 chore: migrate release-notes-spec.ts to vitest 2026-04-13 00:59:16 -07:00
Samuel Attard
b5c9d0bdff chore: migrate process-binding-spec.ts to vitest 2026-04-13 00:59:16 -07:00
Samuel Attard
46464ace53 chore: migrate parse-features-string-spec.ts to vitest 2026-04-13 00:59:15 -07:00
Samuel Attard
46ce6bdc94 chore: migrate node-spec.ts to vitest 2026-04-13 00:59:15 -07:00
Samuel Attard
295deb50e0 chore: migrate modules-spec.ts to vitest 2026-04-13 00:59:15 -07:00
Samuel Attard
2e8b3fb5bf chore: migrate mas-spec.ts to vitest 2026-04-13 00:59:14 -07:00
Samuel Attard
4304cd88b6 chore: migrate logging-spec.ts to vitest 2026-04-13 00:59:14 -07:00
Samuel Attard
b93c6dd11a chore: migrate guest-window-manager-spec.ts to vitest 2026-04-13 00:59:14 -07:00
Samuel Attard
6c9447f2e6 chore: migrate fuses-spec.ts to vitest 2026-04-13 00:59:14 -07:00
Samuel Attard
1127cd3b6c chore: migrate extensions-spec.ts to vitest 2026-04-13 00:59:13 -07:00
Samuel Attard
0608bc93a0 chore: migrate esm-spec.ts to vitest 2026-04-13 00:59:13 -07:00
Samuel Attard
0a1e6119a6 chore: migrate drag-region-spec.ts to vitest 2026-04-13 00:59:13 -07:00
Samuel Attard
d69cee4a05 chore: migrate deprecate-spec.ts to vitest 2026-04-13 00:59:12 -07:00
Samuel Attard
9c6df31551 chore: migrate crash-spec.ts to vitest 2026-04-13 00:59:12 -07:00
Samuel Attard
178456f69e chore: migrate cpp-heap-spec.ts to vitest 2026-04-13 00:59:12 -07:00
Samuel Attard
8eac3fc601 chore: migrate chromium-spec.ts to vitest 2026-04-13 00:59:12 -07:00
Samuel Attard
971d4369eb chore: migrate autofill-spec.ts to vitest 2026-04-13 00:59:11 -07:00
Samuel Attard
8dc0354881 chore: migrate asar-spec.ts to vitest 2026-04-13 00:59:11 -07:00
Samuel Attard
4d6349eeca chore: migrate asar-integrity-spec.ts to vitest 2026-04-13 00:59:11 -07:00
Samuel Attard
24fc081b2f chore: migrate api-web-utils-spec.ts to vitest 2026-04-13 00:59:10 -07:00
Samuel Attard
1e0e9a41f3 chore: migrate api-web-request-spec.ts to vitest 2026-04-13 00:59:10 -07:00
Samuel Attard
d2a5c72b5f chore: migrate api-web-frame-spec.ts to vitest 2026-04-13 00:59:10 -07:00
Samuel Attard
2e7aaacbbe chore: migrate api-web-frame-main-spec.ts to vitest 2026-04-13 00:59:09 -07:00
Samuel Attard
0419a43b84 chore: migrate api-web-contents-view-spec.ts to vitest 2026-04-13 00:59:09 -07:00
Samuel Attard
0faef330c1 chore: migrate api-web-contents-spec.ts to vitest 2026-04-13 00:59:09 -07:00
Samuel Attard
25a4d53dcc chore: migrate api-view-spec.ts to vitest 2026-04-13 00:59:09 -07:00
Samuel Attard
644bc84146 chore: migrate api-utility-process-spec.ts to vitest 2026-04-13 00:59:08 -07:00
Samuel Attard
040fa8cb6a chore: migrate api-tray-spec.ts to vitest 2026-04-13 00:59:08 -07:00
Samuel Attard
6fb8c72b96 chore: migrate api-touch-bar-spec.ts to vitest 2026-04-13 00:59:08 -07:00
Samuel Attard
8881184167 chore: migrate api-system-preferences-spec.ts to vitest 2026-04-13 00:59:07 -07:00
Samuel Attard
c34e07fae5 chore: migrate api-subframe-spec.ts to vitest 2026-04-13 00:59:07 -07:00
Samuel Attard
8cdb30f0f0 chore: migrate api-shell-spec.ts to vitest 2026-04-13 00:59:07 -07:00
Samuel Attard
b58390fe1f chore: migrate api-shared-texture-spec.ts to vitest 2026-04-13 00:59:07 -07:00
Samuel Attard
86589f707f chore: migrate api-session-spec.ts to vitest 2026-04-13 00:59:06 -07:00
Samuel Attard
62a6b8118e chore: migrate api-service-workers-spec.ts to vitest 2026-04-13 00:59:06 -07:00
Samuel Attard
7e9d88bd74 chore: migrate api-service-worker-main-spec.ts to vitest 2026-04-13 00:59:06 -07:00
Samuel Attard
562330457e chore: migrate api-screen-spec.ts to vitest 2026-04-13 00:59:06 -07:00
Samuel Attard
913304c5bb chore: migrate api-safe-storage-spec.ts to vitest 2026-04-13 00:59:05 -07:00
Samuel Attard
85643d44e5 chore: migrate api-protocol-spec.ts to vitest 2026-04-13 00:59:05 -07:00
Samuel Attard
442cc4e005 chore: migrate api-process-spec.ts to vitest 2026-04-13 00:59:05 -07:00
Samuel Attard
6c8102697a chore: migrate api-power-save-blocker-spec.ts to vitest 2026-04-13 00:59:04 -07:00
Samuel Attard
b9b6285b5d chore: migrate api-power-monitor-spec.ts to vitest 2026-04-13 00:59:04 -07:00
Samuel Attard
1d2e635ab2 chore: migrate api-notification-spec.ts to vitest 2026-04-13 00:59:04 -07:00
Samuel Attard
b474e10633 chore: migrate api-notification-dbus-spec.ts to vitest 2026-04-13 00:59:04 -07:00
Samuel Attard
3a030f81c2 chore: migrate api-net-spec.ts to vitest 2026-04-13 00:59:03 -07:00
Samuel Attard
f8d0d2d599 chore: migrate api-net-session-spec.ts to vitest 2026-04-13 00:59:03 -07:00
Samuel Attard
220171a697 chore: migrate api-net-log-spec.ts to vitest 2026-04-13 00:59:03 -07:00
Samuel Attard
ad5dd349f4 chore: migrate api-net-custom-protocols-spec.ts to vitest 2026-04-13 00:59:03 -07:00
Samuel Attard
8799634083 chore: migrate api-native-theme-spec.ts to vitest 2026-04-13 00:59:02 -07:00
Samuel Attard
29ffaae7a1 chore: migrate api-native-image-spec.ts to vitest 2026-04-13 00:59:02 -07:00
Samuel Attard
1932404af2 chore: migrate api-menu-spec.ts to vitest 2026-04-13 00:59:02 -07:00
Samuel Attard
2c23564a7a chore: migrate api-menu-item-spec.ts to vitest 2026-04-13 00:59:02 -07:00
Samuel Attard
ad1b1d4b0b chore: migrate api-media-handler-spec.ts to vitest 2026-04-13 00:59:01 -07:00
Samuel Attard
e6c6e38b24 chore: migrate api-ipc-spec.ts to vitest 2026-04-13 00:59:01 -07:00
Samuel Attard
d8345f4054 chore: migrate api-ipc-renderer-spec.ts to vitest 2026-04-13 00:59:01 -07:00
Samuel Attard
f6e164dadf chore: migrate api-ipc-main-spec.ts to vitest 2026-04-13 00:59:00 -07:00
Samuel Attard
68883ece6f chore: migrate api-in-app-purchase-spec.ts to vitest 2026-04-13 00:59:00 -07:00
Samuel Attard
c9625bea13 chore: migrate api-image-view-spec.ts to vitest 2026-04-13 00:59:00 -07:00
Samuel Attard
c28e50d10f chore: migrate api-global-shortcut-spec.ts to vitest 2026-04-13 00:59:00 -07:00
Samuel Attard
f945aa5e39 chore: migrate api-dialog-spec.ts to vitest 2026-04-13 00:58:59 -07:00
Samuel Attard
046631a8fe chore: migrate api-desktop-capturer-spec.ts to vitest 2026-04-13 00:58:59 -07:00
Samuel Attard
f124d7f261 chore: migrate api-debugger-spec.ts to vitest 2026-04-13 00:58:59 -07:00
Samuel Attard
5d6ebd6e4a chore: migrate api-crash-reporter-spec.ts to vitest 2026-04-13 00:58:59 -07:00
Samuel Attard
827ad50c53 chore: migrate api-corner-smoothing-spec.ts to vitest 2026-04-13 00:58:58 -07:00
Samuel Attard
c46433d1cd chore: migrate api-context-bridge-spec.ts to vitest 2026-04-13 00:58:58 -07:00
Samuel Attard
515adf3399 chore: migrate api-content-tracing-spec.ts to vitest 2026-04-13 00:58:58 -07:00
Samuel Attard
8d7d778ab7 chore: migrate api-clipboard-spec.ts to vitest 2026-04-13 00:58:57 -07:00
Samuel Attard
93d9fe675d chore: migrate api-browser-window-spec.ts to vitest 2026-04-13 00:58:57 -07:00
Samuel Attard
d64680ff0d chore: migrate api-browser-view-spec.ts to vitest 2026-04-13 00:58:57 -07:00
Samuel Attard
689d8ddc11 chore: migrate api-autoupdater-msix-spec.ts to vitest 2026-04-13 00:58:56 -07:00
Samuel Attard
22b79ba2f9 chore: migrate api-autoupdater-darwin-spec.ts to vitest 2026-04-13 00:58:56 -07:00
Samuel Attard
02541f8606 chore: migrate api-auto-updater-spec.ts to vitest 2026-04-13 00:58:56 -07:00
Samuel Attard
034ac4fc04 chore: migrate api-app-spec.ts to vitest 2026-04-13 00:58:56 -07:00
Samuel Attard
d8fe4fd091 chore: prepare spec helpers and vitest setup for migration 2026-04-13 00:58:55 -07:00
Samuel Attard
7a446b0f84 build: add vitest runner infrastructure for Electron main-process tests
Adds spec/_vitest_runner/, a custom vitest pool that spawns each worker
as a full Electron main process (not a node-mode fork) so tests can use
real Electron APIs. Each worker gets a pool-owned mkdtemp userData dir
that is cleaned up on worker stop. File-level parallelism is enabled;
intra-file concurrency stays off to match current mocha behaviour.

Run with: yarn test:vitest
2026-04-13 00:58:55 -07:00
126 changed files with 7336 additions and 6182 deletions

View File

@@ -81,7 +81,7 @@ When working on the `roller/chromium/main` branch for Chromium upgrades, use `e
- JS/TS files: kebab-case (`file-name.ts`)
- C++ files: snake_case with `electron_api_` prefix (`electron_api_safe_storage.cc`)
- Test files: `api-{module-name}-spec.ts` in `spec/`
- Test files: `api-{module-name}.spec.ts` in `spec/`
- Source file lists are maintained in `filenames.gni` (with platform-specific sections)
### JavaScript/TypeScript

View File

@@ -269,7 +269,7 @@ jobs:
needs: checkout-macos
with:
build-runs-on: macos-15-xlarge
test-runs-on: macos-15
test-runs-on: macos-15-xlarge
target-platform: macos
target-arch: arm64
is-release: false
@@ -290,7 +290,7 @@ jobs:
with:
build-runs-on: electron-arc-centralus-linux-amd64-32core
clang-tidy-runs-on: electron-arc-centralus-linux-amd64-8core
test-runs-on: electron-arc-centralus-linux-amd64-4core
test-runs-on: electron-arc-centralus-linux-amd64-8core
build-container: '{"image":"ghcr.io/electron/build:${{ needs.checkout-linux.outputs.build-image-sha }}","options":"--user root","volumes":["/mnt/cross-instance-cache:/mnt/cross-instance-cache"]}'
clang-tidy-container: '{"image":"ghcr.io/electron/build:${{ needs.checkout-linux.outputs.build-image-sha }}","options":"--user root","volumes":["/mnt/cross-instance-cache:/mnt/cross-instance-cache"]}'
test-container: '{"image":"ghcr.io/electron/build:${{ needs.checkout-linux.outputs.build-image-sha }}","options":"--user root --privileged --init"}'
@@ -312,7 +312,7 @@ jobs:
if: ${{ needs.setup.outputs.src == 'true' }}
with:
build-runs-on: electron-arc-centralus-linux-amd64-32core
test-runs-on: electron-arc-centralus-linux-amd64-4core
test-runs-on: electron-arc-centralus-linux-amd64-8core
build-container: '{"image":"ghcr.io/electron/build:${{ needs.checkout-linux.outputs.build-image-sha }}","options":"--user root","volumes":["/mnt/cross-instance-cache:/mnt/cross-instance-cache"]}'
test-container: '{"image":"ghcr.io/electron/build:${{ needs.checkout-linux.outputs.build-image-sha }}","options":"--user root --privileged --init"}'
target-platform: linux

View File

@@ -57,8 +57,6 @@ jobs:
- name: Run Electron Tests in QEMU 64k Container
shell: bash
env:
MOCHA_REPORTER: mocha-multi-reporters
MOCHA_MULTI_REPORTERS: mocha-junit-reporter, tap
ELECTRON_DISABLE_SECURITY_WARNINGS: 1
DISPLAY: ':99.0'
run: |

View File

@@ -66,7 +66,7 @@ jobs:
fail-fast: false
matrix:
build-type: ${{ inputs.target-platform == 'macos' && fromJSON('["darwin","mas"]') || (inputs.target-platform == 'win' && fromJSON('["win"]') || fromJSON('["linux"]')) }}
shard: ${{ case(inputs.display-server == 'wayland', fromJSON('[1]'), inputs.target-platform == 'linux', fromJSON('[1, 2, 3]'), inputs.target-platform == 'macos' && inputs.target-arch == 'x64', fromJSON('[1, 2, 3]'), fromJSON('[1, 2]')) }}
shard: ${{ case(inputs.display-server == 'wayland', fromJSON('[1]'), inputs.is-asan == true, fromJSON('[1, 2]'), inputs.target-platform == 'linux', fromJSON('[1]'), fromJSON('[1, 2]')) }}
env:
BUILD_TYPE: ${{ matrix.build-type }}
TARGET_ARCH: ${{ inputs.target-arch }}
@@ -218,16 +218,15 @@ jobs:
shell: bash
timeout-minutes: 60
env:
MOCHA_REPORTER: mocha-multi-reporters
MOCHA_MULTI_REPORTERS: mocha-junit-reporter, tap
ELECTRON_DISABLE_SECURITY_WARNINGS: 1
DISPLAY: ':99.0'
NPM_CONFIG_MSVS_VERSION: '2022'
run: |
cd src/electron
export ELECTRON_TEST_RESULTS_DIR=`pwd`/junit
export ELECTRON_EXTRA_ARGS="--trace-uncaught --enable-logging"
JUNIT_ARGS="--reporter=default --reporter=junit --outputFile.junit=junit/test-results-main.xml"
# Get which tests are on this shard
tests_files=$(node script/split-tests ${{ matrix.shard }} ${{ case(inputs.display-server == 'wayland', 1, inputs.target-platform == 'linux', 3, inputs.target-platform == 'macos' && inputs.target-arch == 'x64', 3, 2) }})
tests_files=$(node script/split-tests ${{ matrix.shard }} ${{ case(inputs.display-server == 'wayland', 1, inputs.is-asan == true, 2, inputs.target-platform == 'linux', 1, 2) }})
if [ "${{ inputs.display-server }}" = "wayland" ]; then
allowlist_file=script/wayland-test-allowlist.txt
filtered_tests=""
@@ -251,11 +250,8 @@ jobs:
if [ "${{ inputs.target-arch }}" = "x86" ]; then
export npm_config_arch="ia32"
fi
if [ "${{ inputs.target-arch }}" = "arm64" ]; then
export ELECTRON_FORCE_TEST_SUITE_EXIT="true"
fi
fi
node script/yarn.js test --runners=main --enableRerun=3 --trace-uncaught --enable-logging --files $tests_files
node script/yarn.js test $JUNIT_ARGS $tests_files
else
chown :builduser .. && chmod g+w ..
chown -R :builduser . && chmod -R g+w .
@@ -269,21 +265,19 @@ jobs:
export NSS_DISABLE_ARENA_FREE_LIST=1
export NSS_DISABLE_UNLOAD=1
export LLVM_SYMBOLIZER_PATH=$PWD/third_party/llvm-build/Release+Asserts/bin/llvm-symbolizer
export MOCHA_TIMEOUT=180000
echo "Piping output to ASAN_SYMBOLIZE ($ASAN_SYMBOLIZE)"
cd electron
runuser -u builduser -- xvfb-run script/actions/run-tests.sh script/yarn.js test --runners=main --trace-uncaught --enable-logging --files $tests_files | $ASAN_SYMBOLIZE
runuser -u builduser -- xvfb-run script/actions/run-tests.sh script/yarn.js test --testTimeout=180000 $JUNIT_ARGS $tests_files | $ASAN_SYMBOLIZE
else
if [ "${{ inputs.target-arch }}" = "arm" ]; then
runuser -u builduser -- xvfb-run script/actions/run-tests.sh script/yarn.js test --skipYarnInstall --runners=main --enableRerun=3 --trace-uncaught --enable-logging --files $tests_files
else
runuser -u builduser -- xvfb-run script/actions/run-tests.sh script/yarn.js test --skipYarnInstall $JUNIT_ARGS $tests_files
else
if [ "${{ inputs.display-server }}" = "wayland" ]; then
runuser -u builduser -- script/actions/run-tests-wayland.sh script/yarn.js test --runners=main --enableRerun=3 --trace-uncaught --enable-logging --files $tests_files
runuser -u builduser -- script/actions/run-tests-wayland.sh script/yarn.js test $JUNIT_ARGS $tests_files
else
runuser -u builduser -- xvfb-run script/actions/run-tests.sh script/yarn.js test --runners=main --enableRerun=3 --trace-uncaught --enable-logging --files $tests_files
runuser -u builduser -- xvfb-run script/actions/run-tests.sh script/yarn.js test $JUNIT_ARGS $tests_files
fi
fi
fi
fi
- name: Take screenshot on timeout or cancellation

View File

@@ -11,6 +11,18 @@
{
"name": "no-only-tests",
"specifier": "./script/lint-plugins/no-only-tests.mjs"
},
{
"name": "remote-tools-imports",
"specifier": "./script/lint-plugins/remote-tools-imports.mjs"
},
{
"name": "no-nested-tests",
"specifier": "./script/lint-plugins/no-nested-tests.mjs"
},
{
"name": "no-unawaited-load",
"specifier": "./script/lint-plugins/no-unawaited-load.mjs"
}
],
"categories": {
@@ -315,7 +327,18 @@
{
"files": ["spec/**/*.ts", "spec/**/*.js", "spec/**/*.mjs"],
"rules": {
"no-only-tests/no-only-tests": "error"
"no-only-tests/no-only-tests": "error",
"remote-tools-imports/no-foreign-imports-in-remote-closure": "error",
"no-nested-tests/no-nested-tests": "error",
"no-unawaited-load/no-unawaited-load": "warn"
}
},
{
"files": ["spec/fixtures/**"],
"rules": {
"no-unawaited-load/no-unawaited-load": "off",
"remote-tools-imports/no-foreign-imports-in-remote-closure": "off",
"no-nested-tests/no-nested-tests": "off"
}
},
{

View File

@@ -8,7 +8,6 @@
"@datadog/datadog-ci": "^5.9.1",
"@electron/asar": "^4.0.1",
"@electron/docs-parser": "^2.0.0",
"@electron/fiddle-core": "^1.3.4",
"@electron/github-app-auth": "^3.2.0",
"@electron/lint-roller": "^3.2.0",
"@electron/typescript-definitions": "^9.1.5",
@@ -21,12 +20,10 @@
"@types/semver": "^7.5.8",
"@types/stream-json": "^1.7.8",
"@types/temp": "^0.9.4",
"@xmldom/xmldom": "^0.8.12",
"buffer": "^6.0.3",
"chalk": "^4.1.0",
"check-for-leaks": "^1.2.1",
"events": "^3.2.0",
"folder-hash": "^4.1.2",
"got": "^11.8.5",
"husky": "^9.1.7",
"lint-staged": "^16.1.0",
@@ -46,6 +43,7 @@
"ts-node": "6.2.0",
"typescript": "^5.8.3",
"url": "^0.11.4",
"vitest": "^4.1.2",
"webpack": "^5.104.1",
"webpack-cli": "^6.0.1",
"wrapper-webpack-plugin": "^2.2.0",
@@ -86,7 +84,7 @@
"prepare": "husky",
"repl": "node ./script/start.js --interactive",
"start": "node ./script/start.js",
"test": "node ./script/spec-runner.js",
"test": "node ./spec/_vitest_runner/run.js",
"tsc": "tsc",
"webpack": "webpack"
},

View File

@@ -29,4 +29,5 @@ for _ in {1..100}; do
sleep 0.1
done
node "$@" --ozone-platform=wayland
export ELECTRON_EXTRA_ARGS="${ELECTRON_EXTRA_ARGS:-} --ozone-platform=wayland"
node "$@"

View File

@@ -0,0 +1,69 @@
// Flags it()/ifit()/itremote() calls that appear inside the body of another
// it()/ifit()/itremote() call. Mocha tolerated this; vitest does not.
const TEST_CALLEES = new Set(['it', 'itremote', 'test', 'specify']);
function isTestCall(node) {
if (node.type !== 'CallExpression') return null;
let callee = node.callee;
// unwrap it.only / it.skip / it.runIf(cond)(...)
if (callee.type === 'MemberExpression' && callee.object.type === 'Identifier') {
callee = callee.object;
}
// ifit(cond)(name, fn) — callee is CallExpression whose callee is Identifier 'ifit'
if (callee.type === 'CallExpression') {
const inner = callee.callee;
if (inner.type === 'Identifier' && (inner.name === 'ifit' || inner.name === 'it')) {
return { name: inner.name, fn: node.arguments.find((a) => isFunctionLike(a)) };
}
if (
inner.type === 'MemberExpression' &&
inner.object.type === 'Identifier' &&
(inner.object.name === 'it' || inner.object.name === 'test')
) {
return { name: inner.object.name, fn: node.arguments.find((a) => isFunctionLike(a)) };
}
return null;
}
if (callee.type === 'Identifier' && TEST_CALLEES.has(callee.name)) {
return { name: callee.name, fn: node.arguments.find((a) => isFunctionLike(a)) };
}
return null;
}
function isFunctionLike(node) {
return (
node &&
(node.type === 'FunctionExpression' ||
node.type === 'ArrowFunctionExpression' ||
(node.type === 'CallExpression' && node.callee.type === 'Identifier' && node.callee.name === 'withDone'))
);
}
export default {
meta: { name: 'no-nested-tests' },
rules: {
'no-nested-tests': {
meta: { type: 'problem' },
create(context) {
let depth = 0;
return {
CallExpression(node) {
const test = isTestCall(node);
if (!test) return;
if (depth > 0) {
context.report({
node: node.callee,
message: `'${test.name}()' is nested inside another test body. vitest does not allow nested test definitions; hoist this to the enclosing describe().`
});
}
depth++;
},
'CallExpression:exit'(node) {
if (isTestCall(node)) depth--;
}
};
}
}
}
};

View File

@@ -0,0 +1,79 @@
// Flags loadURL()/loadFile() calls whose returned promise is neither awaited,
// returned, .then()/.catch()'d, nor assigned. These reject as unhandled when
// the load is aborted (e.g. the test moves on or the window closes).
const LOAD_METHODS = new Set(['loadURL', 'loadFile']);
function isHandled(node, parent) {
if (!parent) return false;
switch (parent.type) {
case 'AwaitExpression':
return true;
case 'ReturnStatement':
return true;
case 'ArrowFunctionExpression':
// Implicit return: (…) => w.loadURL(…)
return parent.body === node;
case 'VariableDeclarator':
// const p = w.loadURL(…) — assume the promise is handled later.
return parent.init === node;
case 'AssignmentExpression':
return parent.right === node;
case 'MemberExpression':
// w.loadURL(…).catch(…) / .then(…) — the MemberExpression is the object
// of a surrounding CallExpression; treat any property access as handled
// to avoid false positives on .then/.catch/.finally chains.
return parent.object === node;
case 'CallExpression':
// Passed as an argument, e.g. expect(w.loadURL(...)).to.eventually…,
// Promise.all([w.loadURL(...)]), once(w, 'x').then(w.loadURL(...))
return parent.arguments?.includes(node) || parent.callee === node;
case 'ArrayExpression':
// [w.loadURL(...)] — typically Promise.all input.
return true;
case 'ConditionalExpression':
case 'LogicalExpression':
// cond ? w.loadURL(a) : w.loadURL(b) — handled if the whole expr is.
return true;
case 'UnaryExpression':
// void w.loadURL(...) — explicit discard.
return parent.operator === 'void';
}
return false;
}
export default {
meta: { name: 'no-unawaited-load' },
rules: {
'no-unawaited-load': {
meta: { type: 'suggestion' },
create(context) {
const sourceCode = context.sourceCode ?? context.getSourceCode?.();
const ancestorsOf = (n) => sourceCode?.getAncestors?.(n) ?? context.getAncestors?.() ?? [];
return {
CallExpression(node) {
const callee = node.callee;
if (
callee.type !== 'MemberExpression' ||
callee.property.type !== 'Identifier' ||
!LOAD_METHODS.has(callee.property.name)
) {
return;
}
const ancestors = ancestorsOf(node);
const parent = ancestors[ancestors.length - 1] ?? node.parent;
if (isHandled(node, parent)) return;
context.report({
node: callee.property,
message:
`'${callee.property.name}()' returns a promise that is not awaited, ` +
`returned, assigned, or .catch()'d. If the load may be aborted, add ` +
`'.catch(() => {})' or 'void' to suppress the unhandled rejection.`
});
}
};
}
}
}
};

View File

@@ -0,0 +1,226 @@
// Flags imports (other than from spec/lib/remote-tools) that are referenced
// inside itremote()/remotely() closures. vite's SSR transform rewrites import
// bindings to __vite_ssr_import_N__, which breaks when the closure is
// stringified and eval'd in a renderer/child process. Importing from
// remote-tools instead lets runRemote()'s __rt shim resolve them.
const REMOTE_TOOLS_RE = /[./]lib\/remote-tools(\.ts)?$/;
const REMOTE_TAG_RE = /@remote\b([^\n*]*)/;
function getRemoteTag(node, sourceCode) {
const comments = sourceCode.getCommentsBefore?.(node) ?? node.leadingComments ?? [];
for (const c of comments) {
const m = REMOTE_TAG_RE.exec(c.value);
if (m) return { noLocals: /\bno-locals\b/.test(m[1]) };
}
return null;
}
function isRemoteCall(callee, taggedCallees) {
// itremote(name, fn), itremote.only(name, fn), itremote.skip(name, fn)
if (callee.type === 'Identifier' && callee.name === 'itremote') return { fnArg: 1 };
if (
callee.type === 'MemberExpression' &&
callee.object.type === 'Identifier' &&
callee.object.name === 'itremote' &&
callee.property.type === 'Identifier' &&
(callee.property.name === 'only' || callee.property.name === 'skip')
) {
return { fnArg: 1 };
}
// <anything>.remotely(fn, ...args)
if (
callee.type === 'MemberExpression' &&
callee.property.type === 'Identifier' &&
callee.property.name === 'remotely'
) {
return { fnArg: 0 };
}
// Calls to identifiers tagged /** @remote */ (function decls, const bindings,
// or for-of loop vars). fn arg is assumed to be the first function-expression
// argument, resolved at the call site.
if (callee.type === 'Identifier' && taggedCallees.has(callee.name)) {
return { fnArg: 'firstFunction', noLocals: taggedCallees.get(callee.name).noLocals };
}
return null;
}
function collectDeclared(node, declared) {
if (!node) return;
if (node.type === 'Identifier') {
declared.add(node.name);
} else if (node.type === 'ObjectPattern') {
for (const p of node.properties) collectDeclared(p.value ?? p.argument, declared);
} else if (node.type === 'ArrayPattern') {
for (const e of node.elements) collectDeclared(e, declared);
} else if (node.type === 'RestElement') {
collectDeclared(node.argument, declared);
} else if (node.type === 'AssignmentPattern') {
collectDeclared(node.left, declared);
}
}
function walkClosure(node, onIdentifierRef, declared) {
if (!node || typeof node !== 'object') return;
switch (node.type) {
case 'Identifier':
if (!declared.has(node.name)) onIdentifierRef(node);
return;
case 'MemberExpression':
walkClosure(node.object, onIdentifierRef, declared);
if (node.computed) walkClosure(node.property, onIdentifierRef, declared);
return;
case 'Property':
if (node.computed) walkClosure(node.key, onIdentifierRef, declared);
walkClosure(node.value, onIdentifierRef, declared);
return;
case 'VariableDeclarator':
collectDeclared(node.id, declared);
walkClosure(node.init, onIdentifierRef, declared);
return;
case 'FunctionDeclaration':
case 'FunctionExpression':
case 'ArrowFunctionExpression': {
const inner = new Set(declared);
if (node.id) inner.add(node.id.name);
for (const p of node.params) collectDeclared(p, inner);
walkClosure(node.body, onIdentifierRef, inner);
return;
}
case 'CatchClause': {
const inner = new Set(declared);
collectDeclared(node.param, inner);
walkClosure(node.body, onIdentifierRef, inner);
return;
}
}
for (const key of Object.keys(node)) {
if (
key === 'type' ||
key === 'loc' ||
key === 'range' ||
key === 'start' ||
key === 'end' ||
key === 'parent' ||
key === 'typeAnnotation' ||
key === 'typeParameters' ||
key === 'returnType'
) {
continue;
}
const child = node[key];
if (Array.isArray(child)) {
for (const c of child) walkClosure(c, onIdentifierRef, declared);
} else if (child && typeof child === 'object' && typeof child.type === 'string') {
walkClosure(child, onIdentifierRef, declared);
}
}
}
export default {
meta: { name: 'remote-tools-imports' },
rules: {
'no-foreign-imports-in-remote-closure': {
meta: { type: 'problem' },
create(context) {
// Map of import-binding name -> { source, node } for everything NOT
// from remote-tools.
const foreignImports = new Map();
// Every identifier declared in this file (imports from anywhere,
// plus const/let/var/function/class at any scope). Used by no-locals.
const fileLocals = new Set();
// Names of functions/bindings tagged /** @remote */ in this file,
// mapped to {noLocals:boolean}.
const taggedCallees = new Map();
const sourceCode = context.sourceCode ?? context.getSourceCode?.();
const addLocal = (idNode) => {
if (idNode?.type === 'Identifier') fileLocals.add(idNode.name);
};
const collectTagged = (idNode, commentTarget) => {
if (idNode?.type !== 'Identifier') return;
const tag = getRemoteTag(commentTarget, sourceCode);
if (tag) taggedCallees.set(idNode.name, tag);
};
return {
ImportDeclaration(node) {
const source = node.source.value;
for (const spec of node.specifiers) addLocal(spec.local);
if (REMOTE_TOOLS_RE.test(source)) return;
if (node.importKind === 'type') return;
for (const spec of node.specifiers) {
if (spec.importKind === 'type') continue;
foreignImports.set(spec.local.name, { source, node: spec.local });
}
},
FunctionDeclaration(node) {
addLocal(node.id);
collectTagged(node.id, node);
},
ClassDeclaration(node) {
addLocal(node.id);
},
VariableDeclaration(node) {
for (const d of node.declarations) collectDeclared(d.id, fileLocals);
if (!getRemoteTag(node, sourceCode)) return;
for (const d of node.declarations) collectTagged(d.id, node);
},
ForOfStatement(node) {
if (!getRemoteTag(node, sourceCode)) return;
const decl = node.left;
if (decl.type === 'VariableDeclaration') {
for (const d of decl.declarations) collectTagged(d.id, node);
}
},
CallExpression(node) {
const match = isRemoteCall(node.callee, taggedCallees);
if (!match) return;
const fnArg =
match.fnArg === 'firstFunction'
? node.arguments.find(
(a) => a && (a.type === 'FunctionExpression' || a.type === 'ArrowFunctionExpression')
)
: node.arguments[match.fnArg];
if (!fnArg || (fnArg.type !== 'FunctionExpression' && fnArg.type !== 'ArrowFunctionExpression')) {
return;
}
const reported = new Set();
walkClosure(
fnArg,
(id) => {
if (reported.has(id.name)) return;
if (match.noLocals) {
if (!fileLocals.has(id.name)) return;
reported.add(id.name);
context.report({
node: id,
message:
`'${id.name}' is declared in this file but referenced inside a ` +
`/** @remote no-locals */ closure. These closures are stringified and ` +
`evaluated with no access to the enclosing scope; use only parameters ` +
`and JS/DOM globals.`
});
return;
}
const hit = foreignImports.get(id.name);
if (hit) {
reported.add(id.name);
context.report({
node: id,
message:
`'${id.name}' is imported from '${hit.source}' but used inside a ` +
`stringified remote closure. Import it from './lib/remote-tools' ` +
`instead (add it there if missing).`
});
}
},
new Set()
);
}
};
}
}
}
};

View File

@@ -1,472 +0,0 @@
#!/usr/bin/env node
const { ElectronVersions, Installer } = require('@electron/fiddle-core');
const { DOMParser } = require('@xmldom/xmldom');
const chalk = require('chalk');
const { hashElement } = require('folder-hash');
const minimist = require('minimist');
const childProcess = require('node:child_process');
const crypto = require('node:crypto');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const unknownFlags = [];
const pass = chalk.green('✓');
const fail = chalk.red('✗');
const FAILURE_STATUS_KEY = 'Electron_Spec_Runner_Failures';
const args = minimist(process.argv, {
boolean: ['skipYarnInstall'],
string: ['runners', 'target', 'electronVersion'],
number: ['enableRerun'],
unknown: (arg) => unknownFlags.push(arg)
});
const unknownArgs = [];
for (const flag of unknownFlags) {
unknownArgs.push(flag);
const onlyFlag = flag.replace(/^-+/, '');
if (args[onlyFlag]) {
unknownArgs.push(args[onlyFlag]);
}
}
const utils = require('./lib/utils');
const { YARN_SCRIPT_PATH } = require('./yarn');
const BASE = path.resolve(__dirname, '../..');
const runners = new Map([['main', { description: 'Main process specs', run: runMainProcessElectronTests }]]);
const specHashPath = path.resolve(__dirname, '../spec/.hash');
if (args.electronVersion) {
if (args.runners && args.runners !== 'main') {
console.log(`${fail} only 'main' runner can be used with --electronVersion`);
process.exit(1);
}
args.runners = 'main';
}
let runnersToRun = null;
if (args.runners !== undefined) {
runnersToRun = args.runners.split(',').filter((value) => value);
if (!runnersToRun.every((r) => [...runners.keys()].includes(r))) {
console.log(`${fail} ${runnersToRun} must be a subset of [${[...runners.keys()].join(' | ')}]`);
process.exit(1);
}
console.log('Only running:', runnersToRun);
} else {
console.log(`Triggering runners: ${[...runners.keys()].join(', ')}`);
}
async function main() {
if (args.electronVersion) {
const versions = await ElectronVersions.create();
if (args.electronVersion === 'latest') {
args.electronVersion = versions.latest.version;
} else if (args.electronVersion.startsWith('latest@')) {
const majorVersion = parseInt(args.electronVersion.slice('latest@'.length));
const ver = versions.inMajor(majorVersion).slice(-1)[0];
if (ver) {
args.electronVersion = ver.version;
} else {
console.log(`${fail} '${majorVersion}' is not a recognized Electron major version`);
process.exit(1);
}
} else if (!versions.isVersion(args.electronVersion)) {
console.log(`${fail} '${args.electronVersion}' is not a recognized Electron version`);
process.exit(1);
}
const versionString = `v${args.electronVersion}`;
console.log(`Running against Electron ${chalk.green(versionString)}`);
}
const [lastSpecHash, lastSpecInstallHash] = loadLastSpecHash();
const [currentSpecHash, currentSpecInstallHash] = await getSpecHash();
const somethingChanged = currentSpecHash !== lastSpecHash || lastSpecInstallHash !== currentSpecInstallHash;
if (somethingChanged && !args.skipYarnInstall) {
await installSpecModules(path.resolve(__dirname, '..', 'spec'));
await getSpecHash().then(saveSpecHash);
}
if (!fs.existsSync(path.resolve(__dirname, '../electron.d.ts'))) {
console.log('Generating electron.d.ts as it is missing');
generateTypeDefinitions();
}
await runElectronTests();
}
function generateTypeDefinitions() {
const { status } = childProcess.spawnSync('npm', ['run', 'create-typescript-definitions'], {
cwd: path.resolve(__dirname, '..'),
stdio: 'inherit',
shell: true
});
if (status !== 0) {
throw new Error(`Electron typescript definition generation failed with exit code: ${status}.`);
}
}
function loadLastSpecHash() {
return fs.existsSync(specHashPath) ? fs.readFileSync(specHashPath, 'utf8').split('\n') : [null, null];
}
function saveSpecHash([newSpecHash, newSpecInstallHash]) {
fs.writeFileSync(specHashPath, `${newSpecHash}\n${newSpecInstallHash}`);
}
async function runElectronTests() {
const errors = [];
const testResultsDir = process.env.ELECTRON_TEST_RESULTS_DIR;
for (const [runnerId, { description, run }] of runners) {
if (runnersToRun && !runnersToRun.includes(runnerId)) {
console.info('\nSkipping:', description);
continue;
}
try {
console.info('\nRunning:', description);
if (testResultsDir) {
process.env.MOCHA_FILE = path.join(testResultsDir, `test-results-${runnerId}.xml`);
}
await run();
} catch (err) {
errors.push([runnerId, err]);
}
}
if (errors.length !== 0) {
for (const err of errors) {
console.error('\n\nRunner Failed:', err[0]);
console.error(err[1]);
}
console.log(`${fail} Electron test runners have failed`);
process.exit(1);
}
}
async function asyncSpawn(exe, runnerArgs) {
return new Promise((resolve, reject) => {
let forceExitResult = 0;
const child = childProcess.spawn(exe, runnerArgs, {
cwd: path.resolve(__dirname, '../..')
});
if (process.env.ELECTRON_TEST_PID_DUMP_PATH && child.pid) {
fs.writeFileSync(process.env.ELECTRON_TEST_PID_DUMP_PATH, child.pid.toString());
}
child.stdout.pipe(process.stdout);
child.stderr.pipe(process.stderr);
if (process.env.ELECTRON_FORCE_TEST_SUITE_EXIT) {
child.stdout.on('data', (data) => {
const failureRE = RegExp(`${FAILURE_STATUS_KEY}: (\\d.*)`);
const failures = data.toString().match(failureRE);
if (failures) {
forceExitResult = parseInt(failures[1], 10);
}
});
}
child.on('error', (error) => reject(error));
child.on('close', (status, signal) => {
let returnStatus = 0;
if (process.env.ELECTRON_FORCE_TEST_SUITE_EXIT) {
returnStatus = forceExitResult;
} else {
returnStatus = status;
}
resolve({ status: returnStatus, signal });
});
});
}
function parseJUnitXML(specDir) {
if (!fs.existsSync(process.env.MOCHA_FILE)) {
console.error('JUnit XML file not found:', process.env.MOCHA_FILE);
return [];
}
const xmlContent = fs.readFileSync(process.env.MOCHA_FILE, 'utf8');
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xmlContent, 'text/xml');
const failedTests = [];
// find failed tests by looking for all testsuite nodes with failure > 0
const testSuites = xmlDoc.getElementsByTagName('testsuite');
for (let i = 0; i < testSuites.length; i++) {
const testSuite = testSuites[i];
const failures = testSuite.getAttribute('failures');
if (failures > 0) {
const testcases = testSuite.getElementsByTagName('testcase');
for (let i = 0; i < testcases.length; i++) {
const testcase = testcases[i];
const failures = testcase.getElementsByTagName('failure');
const errors = testcase.getElementsByTagName('error');
if (failures.length > 0 || errors.length > 0) {
const testName = testcase.getAttribute('name');
const filePath = testSuite.getAttribute('file');
const fileName = filePath ? path.relative(specDir, filePath) : 'unknown file';
const failureInfo = {
name: testName,
file: fileName,
filePath
};
if (failures.length > 0) {
failureInfo.failure = failures[0].textContent || failures[0].nodeValue || 'No failure message';
}
if (errors.length > 0) {
failureInfo.error = errors[0].textContent || errors[0].nodeValue || 'No error message';
}
failedTests.push(failureInfo);
}
}
}
}
return failedTests;
}
async function rerunFailedTest(specDir, testName, testInfo) {
console.log('\n========================================');
console.log(`Rerunning failed test: ${testInfo.name} (${testInfo.file})`);
console.log('========================================');
let grepPattern = testInfo.name;
// Escape special regex characters in test name
grepPattern = grepPattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const args = [];
if (testInfo.filePath) {
args.push('--files', testInfo.filePath);
}
args.push('-g', grepPattern);
const success = await runTestUsingElectron(specDir, testName, false, args);
if (success) {
console.log(`✅ Test passed: ${testInfo.name}`);
return true;
} else {
console.log(`❌ Test failed again: ${testInfo.name}`);
return false;
}
}
async function rerunFailedTests(specDir, testName) {
console.log('\n📋 Parsing JUnit XML for failed tests...');
const failedTests = parseJUnitXML(specDir);
if (failedTests.length === 0) {
console.log('No failed tests could be found.');
process.exit(1);
return;
}
// Save off the original junit xml file
if (fs.existsSync(process.env.MOCHA_FILE)) {
fs.copyFileSync(process.env.MOCHA_FILE, `${process.env.MOCHA_FILE}.save`);
}
console.log(`\n📊 Found ${failedTests.length} failed test(s):`);
failedTests.forEach((test, index) => {
console.log(` ${index + 1}. ${test.name} (${test.file})`);
});
// Step 3: Rerun each failed test individually
console.log('\n🔄 Rerunning failed tests individually...\n');
const results = {
total: failedTests.length,
passed: 0,
failed: 0
};
let index = 0;
for (const testInfo of failedTests) {
let runCount = 0;
let success = false;
let retryTest = false;
while (!success && runCount < args.enableRerun) {
success = await rerunFailedTest(specDir, testName, testInfo);
if (success) {
results.passed++;
} else {
if (runCount === args.enableRerun - 1) {
results.failed++;
} else {
retryTest = true;
console.log(`Retrying test (${runCount + 1}/${args.enableRerun})...`);
}
}
// Add a small delay between tests
if (retryTest || index < failedTests.length - 1) {
console.log('\nWaiting 2 seconds before next test...');
await new Promise((resolve) => setTimeout(resolve, 2000));
}
runCount++;
}
index++;
}
// Step 4: Summary
console.log('\n📈 Summary:');
console.log(`Total failed tests: ${results.total}`);
console.log(`Passed on rerun: ${results.passed}`);
console.log(`Still failing: ${results.failed}`);
// Restore the original junit xml file
if (fs.existsSync(`${process.env.MOCHA_FILE}.save`)) {
fs.renameSync(`${process.env.MOCHA_FILE}.save`, process.env.MOCHA_FILE);
}
if (results.failed === 0) {
console.log('🎉 All previously failed tests now pass!');
} else {
console.log(`⚠️ ${results.failed} test(s) are still failing`);
process.exit(1);
}
}
async function runTestUsingElectron(specDir, testName, shouldRerun, additionalArgs = []) {
let exe;
if (args.electronVersion) {
const installer = new Installer();
exe = await installer.install(args.electronVersion);
} else {
exe = path.resolve(BASE, utils.getElectronExec());
}
let argsToPass = unknownArgs.slice(2);
if (additionalArgs.includes('--files')) {
argsToPass = argsToPass.filter(
(arg) => arg.toString().indexOf('--files') === -1 && arg.toString().indexOf('spec/') === -1
);
}
const runnerArgs = [`electron/${specDir}`, ...argsToPass, ...additionalArgs];
if (process.platform === 'linux') {
runnerArgs.unshift(path.resolve(__dirname, 'dbus_mock.py'), exe);
exe = 'python3';
}
console.log(`Running: ${exe} ${runnerArgs.join(' ')}`);
const { status, signal } = await asyncSpawn(exe, runnerArgs);
if (status !== 0) {
if (status) {
const textStatus = process.platform === 'win32' ? `0x${status.toString(16)}` : status.toString();
console.log(`${fail} Electron tests failed with code ${textStatus}.`);
} else {
console.log(`${fail} Electron tests failed with kill signal ${signal}.`);
}
if (shouldRerun) {
await rerunFailedTests(specDir, testName);
} else {
process.exit(1);
}
}
console.log(`${pass} Electron ${testName} process tests passed.`);
return true;
}
async function runMainProcessElectronTests() {
let shouldRerun = false;
if (args.enableRerun && args.enableRerun > 0) {
shouldRerun = true;
}
await runTestUsingElectron('spec', 'main', shouldRerun);
}
async function installSpecModules(dir) {
const env = {
npm_config_msvs_version: '2022',
...process.env,
CXXFLAGS: process.env.CXXFLAGS,
npm_config_yes: 'true'
};
if (args.electronVersion) {
env.npm_config_target = args.electronVersion;
env.npm_config_disturl = 'https://electronjs.org/headers';
env.npm_config_runtime = 'electron';
env.npm_config_devdir = path.join(os.homedir(), '.electron-gyp');
env.npm_config_build_from_source = 'true';
const { status } = childProcess.spawnSync('npm', ['run', 'node-gyp-install', '--ensure'], {
env,
cwd: dir,
stdio: 'inherit',
shell: true
});
if (status !== 0) {
console.log(`${fail} Failed to "npm run node-gyp-install" install in '${dir}'`);
process.exit(1);
}
} else {
env.npm_config_nodedir = path.resolve(BASE, `out/${utils.getOutDir({ shouldLog: true })}/gen/node_headers`);
}
if (fs.existsSync(path.resolve(dir, 'node_modules'))) {
await fs.promises.rm(path.resolve(dir, 'node_modules'), { force: true, recursive: true });
}
const yarnArgs = [YARN_SCRIPT_PATH, 'install', '--immutable'];
const { status } = childProcess.spawnSync(process.execPath, yarnArgs, {
env,
cwd: dir,
stdio: 'inherit',
shell: process.platform === 'win32'
});
if (status !== 0 && !process.env.IGNORE_YARN_INSTALL_ERROR) {
console.log(`${fail} Failed to yarn install in '${dir}'`);
process.exit(1);
}
if (process.platform === 'linux') {
const { status: rebuildStatus } = childProcess.spawnSync('npm', ['rebuild', 'abstract-socket'], {
env,
cwd: dir,
stdio: 'inherit',
shell: process.platform === 'win32'
});
if (rebuildStatus !== 0) {
console.log(`${fail} Failed to rebuild abstract-socket native module`);
process.exit(1);
}
}
}
function getSpecHash() {
return Promise.all([
(async () => {
const hasher = crypto.createHash('SHA256');
hasher.update(fs.readFileSync(path.resolve(__dirname, '../yarn.lock')));
hasher.update(fs.readFileSync(path.resolve(__dirname, '../spec/package.json')));
hasher.update(fs.readFileSync(path.resolve(__dirname, '../script/spec-runner.js')));
return hasher.digest('hex');
})(),
(async () => {
const specNodeModulesPath = path.resolve(__dirname, '../spec/node_modules');
if (!fs.existsSync(specNodeModulesPath)) {
return null;
}
const { hash } = await hashElement(specNodeModulesPath, {
folders: {
exclude: ['.bin']
}
});
return hash;
})()
]);
}
main().catch((error) => {
console.error('An error occurred inside the spec runner:', error);
process.exit(1);
});

View File

@@ -6,29 +6,29 @@ const path = require('node:path');
const currentShard = parseInt(process.argv[2], 10);
const shardCount = parseInt(process.argv[3], 10);
const specFiles = glob.sync('spec/*-spec.ts');
const buckets = [];
for (let i = 0; i < shardCount; i++) {
buckets.push([]);
}
const testsInSpecFile = Object.create(null);
for (const specFile of specFiles) {
const testContent = fs.readFileSync(specFile, 'utf8');
testsInSpecFile[specFile] = testContent.split('it(').length;
function testCountIn(file) {
return fs.readFileSync(file, 'utf8').split('it(').length;
}
specFiles.sort((a, b) => {
return testsInSpecFile[b] - testsInSpecFile[a];
});
let shard = 0;
for (const specFile of specFiles) {
buckets[shard].push(path.normalize(specFile));
shard++;
if (shard === shardCount) shard = 0;
function distribute(files) {
files.sort((a, b) => testCountIn(b) - testCountIn(a));
let shard = 0;
for (const file of files) {
buckets[shard].push(path.normalize(file));
shard++;
if (shard === shardCount) shard = 0;
}
}
// Parallel and serial sets are distributed independently so each shard gets a
// proportional slice of both; run.js routes spec/serial/* to the
// --no-file-parallelism phase.
distribute(glob.sync('spec/*.spec.ts'));
distribute(glob.sync('spec/serial/*.spec.ts'));
console.log(buckets[currentShard - 1].join(' '));

View File

@@ -1,5 +1,5 @@
spec/parse-features-string-spec.ts
spec/types-spec.ts
spec/version-bump-spec.ts
spec/api-app-spec.ts
spec/api-browser-window-spec.ts
spec/parse-features-string.spec.ts
spec/types.spec.ts
spec/version-bump.spec.ts
spec/api-app.spec.ts
spec/api-browser-window.spec.ts

View File

@@ -0,0 +1,118 @@
import { ForksPoolWorker, type PoolOptions, type WorkerRequest } from 'vitest/node';
import { spawn, type ChildProcess } from 'node:child_process';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { getAbsoluteElectronExec } from '../../script/lib/utils';
/**
* A vitest PoolWorker that spawns each worker as a full Electron main
* process (not a node-mode fork) so tests can exercise real Electron APIs.
*
* We extend the built-in ForksPoolWorker so we inherit all IPC / teardown
* plumbing and only override how the child process is created.
*/
class ElectronPoolWorker extends ForksPoolWorker {
override name = 'electron';
private readonly distPath: string;
constructor(options: PoolOptions) {
super(options);
this.distPath = options.distPath;
}
private userDataDir: string | undefined;
override async start() {
if ((this as any)._fork) return;
const electronExec = process.env.ELECTRON_TESTS_EXECUTABLE || getAbsoluteElectronExec();
if (!fs.existsSync(electronExec)) {
throw new Error(
`Electron binary not found at '${electronExec}'. ` +
`Build Electron first (e build) or set ELECTRON_TESTS_EXECUTABLE.`
);
}
// Allocate a guaranteed-unique userData dir for this worker. The pool owns
// its lifecycle so it can be removed once the worker exits.
this.userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'electron-vitest-'));
const env = {
...(this as any).env,
// Make sure the child is a real Electron main process, not node-mode.
ELECTRON_RUN_AS_NODE: undefined,
ELECTRON_DISABLE_SECURITY_WARNINGS: 'true',
// Tell worker-entry.js where vitest's fork worker lives.
VITEST_DIST_PATH: this.distPath,
ELECTRON_VITEST_USER_DATA_DIR: this.userDataDir
};
const extraArgs = process.env.ELECTRON_EXTRA_ARGS ? process.env.ELECTRON_EXTRA_ARGS.split(' ').filter(Boolean) : [];
// spawn() + an 'ipc' stdio slot gives the child process.send/on('message'),
// which is what vitest's fork worker protocol relies on.
const child: ChildProcess = spawn(electronExec, [path.resolve(__dirname), ...extraArgs], {
env,
execArgv: (this as any).execArgv,
stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
serialization: 'advanced'
} as any);
(this as any)._fork = child;
const stdout = (this as any).stdout;
const stderr = (this as any).stderr;
if (child.stdout) {
stdout.setMaxListeners(1 + stdout.getMaxListeners());
child.stdout.pipe(stdout);
}
if (child.stderr) {
stderr.setMaxListeners(1 + stderr.getMaxListeners());
child.stderr.pipe(stderr);
}
child.once('exit', (code, signal) => {
if (!this.stopping && (code !== 0 || signal)) {
console.error(
`[electron-pool] worker pid=${child.pid} exited unexpectedly (code=${code} signal=${signal}) ` +
`while running: ${this.lastFiles.join(', ') || '<no files assigned yet>'}`
);
}
});
}
private lastFiles: string[] = [];
private stopping = false;
override send(message: WorkerRequest) {
if (message.type === 'run' || message.type === 'collect') {
this.lastFiles = message.context.files.map((f) =>
typeof f === 'string'
? f
: ((f as { filepath?: string; file?: string }).filepath ?? (f as { file?: string }).file ?? String(f))
);
}
super.send(message);
}
override async stop() {
this.stopping = true;
try {
await super.stop();
} finally {
if (this.userDataDir) {
fs.rmSync(this.userDataDir, { recursive: true, force: true });
this.userDataDir = undefined;
}
}
}
}
export default {
name: 'electron',
createPoolWorker: (options: PoolOptions) => new ElectronPoolWorker(options)
};

View File

@@ -0,0 +1,4 @@
// Vitest externalises bare specifiers to native `import()`, but Electron only
// hooks CJS `require('electron')`. Alias 'electron' here so the runner ends up
// in CJS-land and gets the real module.
module.exports = require('electron');

View File

@@ -0,0 +1,6 @@
{
"name": "electron-test-main",
"productName": "Electron Test Main",
"version": "0.1.0",
"main": "worker-entry.js"
}

155
spec/_vitest_runner/run.js Normal file
View File

@@ -0,0 +1,155 @@
#!/usr/bin/env node
const childProcess = require('node:child_process');
const crypto = require('node:crypto');
const fs = require('node:fs');
const path = require('node:path');
const { getAbsoluteElectronExec, getOutDir } = require('../../script/lib/utils');
const { YARN_SCRIPT_PATH } = require('../../script/yarn');
const ROOT = path.resolve(__dirname, '..', '..');
const SPEC_DIR = path.resolve(ROOT, 'spec');
const SPEC_HASH_PATH = path.resolve(SPEC_DIR, '.hash');
const VITEST_BIN = path.join(ROOT, 'node_modules', '.bin', 'vitest');
const rawArgs = process.argv.slice(2);
const skipYarnInstall = rawArgs.includes('--skipYarnInstall');
const vitestArgs = rawArgs.filter((a) => a !== '--skipYarnInstall');
function getSpecHash() {
const hasher = crypto.createHash('SHA256');
hasher.update(fs.readFileSync(path.resolve(ROOT, 'yarn.lock')));
hasher.update(fs.readFileSync(path.resolve(SPEC_DIR, 'package.json')));
hasher.update(fs.readFileSync(__filename));
return hasher.digest('hex');
}
function installSpecModules() {
const env = {
npm_config_msvs_version: '2022',
...process.env,
npm_config_nodedir: path.resolve(ROOT, '..', `out/${getOutDir({ shouldLog: true })}/gen/node_headers`),
npm_config_yes: 'true'
};
const nodeModules = path.resolve(SPEC_DIR, 'node_modules');
if (fs.existsSync(nodeModules)) {
fs.rmSync(nodeModules, { force: true, recursive: true });
}
const { status } = childProcess.spawnSync(process.execPath, [YARN_SCRIPT_PATH, 'install', '--immutable'], {
env,
cwd: SPEC_DIR,
stdio: 'inherit',
shell: process.platform === 'win32'
});
if (status !== 0 && !process.env.IGNORE_YARN_INSTALL_ERROR) {
console.error(`Failed to yarn install in '${SPEC_DIR}'`);
process.exit(1);
}
if (process.platform === 'linux') {
const { status: rebuildStatus } = childProcess.spawnSync('npm', ['rebuild', 'abstract-socket'], {
env,
cwd: SPEC_DIR,
stdio: 'inherit'
});
if (rebuildStatus !== 0) {
console.error('Failed to rebuild abstract-socket native module');
process.exit(1);
}
}
}
function generateTypeDefinitions() {
const { status } = childProcess.spawnSync('npm', ['run', 'create-typescript-definitions'], {
cwd: ROOT,
stdio: 'inherit',
shell: true
});
if (status !== 0) {
throw new Error(`Electron typescript definition generation failed with exit code: ${status}.`);
}
}
if (!skipYarnInstall) {
const currentHash = getSpecHash();
const lastHash = fs.existsSync(SPEC_HASH_PATH) ? fs.readFileSync(SPEC_HASH_PATH, 'utf8') : null;
if (currentHash !== lastHash || !fs.existsSync(path.resolve(SPEC_DIR, 'node_modules'))) {
installSpecModules();
fs.writeFileSync(SPEC_HASH_PATH, getSpecHash());
}
}
if (!fs.existsSync(path.resolve(ROOT, 'electron.d.ts'))) {
console.log('Generating electron.d.ts as it is missing');
generateTypeDefinitions();
}
const exe = process.env.ELECTRON_TESTS_EXECUTABLE || getAbsoluteElectronExec();
console.log(`Electron binary: ${exe}`);
const env = {
...process.env,
ELECTRON_TESTS_EXECUTABLE: exe
};
const configPath = path.join(__dirname, 'vitest.config.ts');
function runVitest(extraArgs) {
let command = VITEST_BIN;
let args = ['run', '--config', configPath, ...extraArgs];
// On Linux, run vitest under a mocked D-Bus so every spawned Electron worker
// inherits the session/system bus addresses.
if (process.platform === 'linux') {
args = [path.resolve(ROOT, 'script', 'dbus_mock.py'), command, ...args];
command = 'python3';
}
const result = childProcess.spawnSync(command, args, { cwd: ROOT, stdio: 'inherit', env });
return result.status ?? 1;
}
const SERIAL_DIR = path.join('spec', 'serial');
function isSerialPath(a) {
const rel = path.relative(ROOT, path.resolve(ROOT, a));
return rel === SERIAL_DIR || rel.startsWith(SERIAL_DIR + path.sep);
}
function isTestPath(a) {
return !a.startsWith('-') && (a.includes('/') || a.includes(path.sep) || /\.spec\.[cm]?[tj]s$/.test(a));
}
function serialArgs(args) {
const out = [];
let hasPositional = false;
for (let i = 0; i < args.length; i++) {
const a = args[i];
if (a.startsWith('--outputFile.junit=')) {
out.push(a.replace(/\.xml$/, '-serial.xml'));
} else if (a === '--outputFile.junit') {
out.push(a, args[++i].replace(/\.xml$/, '-serial.xml'));
} else if (!isTestPath(a)) {
out.push(a);
} else {
hasPositional = true;
if (isSerialPath(a)) out.push(a);
}
}
return { args: out, hasPositional };
}
const { args: sArgs, hasPositional } = serialArgs(vitestArgs);
const positionalSerial = hasPositional && sArgs.some((a) => isTestPath(a));
const positionalParallel = hasPositional && vitestArgs.some((a) => isTestPath(a) && !isSerialPath(a));
let parallelStatus = 0;
if (!hasPositional || positionalParallel) {
parallelStatus = runVitest(['--exclude', 'spec/serial/**', ...vitestArgs]);
}
let serialStatus = 0;
if (!hasPositional || positionalSerial) {
console.log('\nRunning spec/serial/** without file parallelism...');
serialStatus = runVitest(['--no-file-parallelism', ...sArgs, ...(hasPositional ? [] : ['spec/serial/**/*.spec.ts'])]);
}
process.exit(parallelStatus || serialStatus);

View File

@@ -0,0 +1,44 @@
import * as chai from 'chai';
import { afterAll, beforeEach } from 'vitest';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { runCleanupFunctions } from '../lib/defer-helpers';
import { assertNoWindowsLeaked } from '../lib/window-helpers';
import chaiAsPromised = require('chai-as-promised');
import dirtyChai = require('dirty-chai');
chai.use(chaiAsPromised);
chai.use(dirtyChai);
// Show full object diff.
// https://github.com/chaijs/chai/issues/469
chai.config.truncateThreshold = 0;
// Skip any tests listed in disabled-tests.json.
const disabledTests = new Set(JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'disabled-tests.json'), 'utf8')));
beforeEach((ctx) => {
// Fallback drain for defer()ed cleanups. Most tests use afterEach(closeAllWindows)
// which already runs these first; this catches tests that defer() without it.
// Note: onTestFinished runs *after* afterEach, so callbacks reaching here must
// not assume windows still exist.
ctx.onTestFinished(runCleanupFunctions);
const parts: string[] = [ctx.task.name];
let suite = ctx.task.suite;
while (suite) {
if (suite.name) parts.unshift(suite.name);
suite = suite.suite;
}
if (disabledTests.has(parts.join(' '))) {
ctx.skip();
}
});
// Runs once per file, after all test-file-level afterAll hooks (setupFiles
// hooks are outermost). Using afterAll rather than afterEach so suites that
// intentionally share a window across tests (useRemoteContext, etc.) are not
// flagged on every test.
afterAll(assertNoWindowsLeaked);

View File

@@ -0,0 +1,42 @@
import { defineConfig } from 'vitest/config';
import * as path from 'node:path';
import electronPool from './electron-pool';
const electronShim = path.resolve(__dirname, 'electron-shim.cjs');
// Each worker is a full Electron main process (GPU process, network service,
// etc.), so the usual cpus-1 default can starve the smaller hosted runners.
function ciMaxWorkers(): number | undefined {
if (!process.env.CI || process.platform !== 'darwin') return undefined;
return process.arch === 'arm64' ? 3 : 6;
}
export default defineConfig({
resolve: {
alias: [{ find: /^electron(\/(main|common|renderer))?$/, replacement: electronShim }]
},
test: {
include: ['spec/**/*.spec.ts'],
exclude: ['spec/fixtures/**', 'spec/node_modules/**'],
setupFiles: ['./spec/_vitest_runner/setup.ts'],
// Custom pool: each worker is a real Electron main process.
pool: electronPool as any,
// Run test *files* in parallel across workers...
fileParallelism: true,
isolate: true,
// ...but keep tests *within* a file sequential.
sequence: { concurrent: false },
allowOnly: !process.env.CI,
retry: process.env.CI ? 3 : 0,
maxWorkers: ciMaxWorkers(),
testTimeout: 30_000,
hookTimeout: 30_000,
server: {
deps: {
external: [/electron-shim\.cjs$/]
}
}
}
});

View File

@@ -0,0 +1,98 @@
// This file is the Electron main-process entry for each vitest pool worker.
// It is launched via `spawn(electron, [<spec-v2 dir>])` with an IPC channel,
// so `process.send` is available and vitest's fork-worker protocol works.
const { app, protocol } = require('electron');
const fs = require('node:fs');
const path = require('node:path');
const v8 = require('node:v8');
// Catch-and-exit only while bootstrapping. Once vitest's fork worker is
// loaded it patches process.exit (to throw) and installs its own handlers,
// so calling process.exit from here would re-throw inside an uncaughtException
// handler and exit the process with Node's code 7, hiding the real error.
function bootstrapUncaughtHandler(err) {
console.error('Unhandled exception in vitest worker bootstrap:', err);
process.exit(1);
}
process.on('uncaughtException', bootstrapUncaughtHandler);
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = 'true';
if (process.env.ELECTRON_TEST_DISABLE_HARDWARE_ACCELERATION) {
app.disableHardwareAcceleration();
}
// The pool allocates (mkdtemp) and cleans up the parent directory; the worker
// adds app.name so tests that assert getPath('userData') contains the app name
// still hold.
const userDataBase = process.env.ELECTRON_VITEST_USER_DATA_DIR;
if (!userDataBase) {
throw new Error('ELECTRON_VITEST_USER_DATA_DIR was not provided by the pool');
}
const userDataDir = path.join(userDataBase, app.name);
fs.mkdirSync(userDataDir, { recursive: true });
app.setPath('userData', userDataDir);
v8.setFlagsFromString('--expose_gc');
app.commandLine.appendSwitch('js-flags', '--expose_gc');
app.on('window-all-closed', () => null);
// Use fake device for Media Stream to replace actual camera and microphone.
app.commandLine.appendSwitch('use-fake-device-for-media-stream');
app.commandLine.appendSwitch(
'host-resolver-rules',
[
'MAP localhost2 127.0.0.1',
'MAP ipv4.localhost2 10.0.0.1',
'MAP ipv6.localhost2 [::1]',
'MAP notfound.localhost2 ~NOTFOUND'
].join(', ')
);
// Enable features required by tests.
app.commandLine.appendSwitch(
'enable-features',
[
// spec/api-web-frame-main.spec.ts
'DocumentPolicyIncludeJSCallStacksInCrashReports',
// spec/spellchecker.spec.ts
'UnrestrictSpellingAndGrammarForTesting'
].join(',')
);
global.standardScheme = 'app';
global.zoomScheme = 'zoom';
global.serviceWorkerScheme = 'sw';
protocol.registerSchemesAsPrivileged([
{ scheme: global.standardScheme, privileges: { standard: true, secure: true, stream: false } },
{ scheme: global.zoomScheme, privileges: { standard: true, secure: true } },
{ scheme: global.serviceWorkerScheme, privileges: { allowServiceWorkers: true, standard: true, secure: true } },
{ scheme: 'http-like', privileges: { standard: true, secure: true, corsEnabled: true, supportFetchAPI: true } },
{ scheme: 'cors-blob', privileges: { corsEnabled: true, supportFetchAPI: true } },
{ scheme: 'cors', privileges: { corsEnabled: true, supportFetchAPI: true } },
{ scheme: 'no-cors', privileges: { supportFetchAPI: true } },
{ scheme: 'no-fetch', privileges: { corsEnabled: true } },
{ scheme: 'stream', privileges: { standard: true, stream: true } },
{ scheme: 'foo', privileges: { standard: true } },
{ scheme: 'bar', privileges: { standard: true } }
]);
app
.whenReady()
.then(async () => {
const distPath = process.env.VITEST_DIST_PATH;
if (!distPath) {
throw new Error('VITEST_DIST_PATH was not provided by the pool');
}
// Importing this registers the process.on('message') handler that speaks
// vitest's pool protocol and actually runs the test files.
await import(path.join(distPath, 'workers/forks.js'));
// vitest's worker patches process.exit and owns error handling from here.
process.removeListener('uncaughtException', bootstrapUncaughtHandler);
})
.catch((err) => {
console.error('Failed to bootstrap vitest worker:', err);
process.exit(1);
});

View File

@@ -2,6 +2,7 @@ import { app, BrowserWindow, Menu, session, net as electronNet, WebContents, uti
import { assert, expect } from 'chai';
import * as semver from 'semver';
import { afterAll, afterEach, beforeAll, beforeEach, describe, it } from 'vitest';
import * as cp from 'node:child_process';
import { once } from 'node:events';
@@ -15,7 +16,15 @@ import { setTimeout } from 'node:timers/promises';
import { promisify } from 'node:util';
import { collectStreamBody, getResponse } from './lib/net-helpers';
import { ifdescribe, ifit, isWayland, listen, waitUntil } from './lib/spec-helpers';
import {
ifdescribe,
ifit,
isWayland,
listen,
waitUntil,
withDone,
dangerouslyIgnoreWebContentsLoadResult
} from './lib/spec-helpers';
import { closeWindow, closeAllWindows } from './lib/window-helpers';
const fixturesPath = path.resolve(__dirname, 'fixtures');
@@ -41,7 +50,7 @@ describe('app module', () => {
let secureUrl: string;
const certPath = path.join(fixturesPath, 'certificates');
before(async () => {
beforeAll(async () => {
const options = {
key: fs.readFileSync(path.join(certPath, 'server.key')),
cert: fs.readFileSync(path.join(certPath, 'server.pem')),
@@ -66,9 +75,11 @@ describe('app module', () => {
secureUrl = (await listen(server)).url;
});
after((done) => {
server.close(() => done());
});
afterAll(
withDone((done) => {
server.close(() => done());
})
);
describe('app.getVersion()', () => {
it('returns the version field of package.json', () => {
@@ -244,8 +255,7 @@ describe('app module', () => {
expectedAdditionalData: unknown;
}
it('prevents the second launch of app', async function () {
this.timeout(120000);
it('prevents the second launch of app', { timeout: 120000 }, async () => {
const appPath = path.join(fixturesPath, 'api', 'singleton-data');
const first = cp.spawn(process.execPath, [appPath]);
await once(first.stdout, 'data');
@@ -425,53 +435,60 @@ describe('app module', () => {
const socketPath =
process.platform === 'win32' ? '\\\\.\\pipe\\electron-app-relaunch' : '/tmp/electron-app-relaunch';
beforeEach((done) => {
fs.unlink(socketPath, () => {
server = net.createServer();
server.listen(socketPath);
done();
});
});
afterEach((done) => {
server!.close(() => {
if (process.platform === 'win32') {
beforeEach(
withDone((done) => {
fs.unlink(socketPath, () => {
server = net.createServer();
server.listen(socketPath);
done();
} else {
fs.unlink(socketPath, () => done());
}
});
});
});
})
);
it('relaunches the app', function (done) {
this.timeout(120000);
let state = 'none';
server!.once('error', (error) => done(error));
server!.on('connection', (client) => {
client.once('data', (data) => {
if (String(data) === '--first' && state === 'none') {
state = 'first-launch';
} else if (String(data) === '--second' && state === 'first-launch') {
state = 'second-launch';
} else if (String(data) === '--third' && state === 'second-launch') {
afterEach(
withDone((done) => {
server!.close(() => {
if (process.platform === 'win32') {
done();
} else {
done(`Unexpected state: "${state}", data: "${data}"`);
fs.unlink(socketPath, () => done());
}
});
});
})
);
const appPath = path.join(fixturesPath, 'api', 'relaunch');
const child = cp.spawn(process.execPath, [appPath, '--first']);
child.stdout.on('data', (c) => console.log(c.toString()));
child.stderr.on('data', (c) => console.log(c.toString()));
child.on('exit', (code, signal) => {
if (code !== 0) {
console.log(`Process exited with code "${code}" signal "${signal}"`);
}
});
});
it(
'relaunches the app',
{ timeout: 120000 },
() =>
new Promise<void>((resolve, reject) => {
let state = 'none';
server!.once('error', (error) => reject(error));
server!.on('connection', (client) => {
client.once('data', (data) => {
if (String(data) === '--first' && state === 'none') {
state = 'first-launch';
} else if (String(data) === '--second' && state === 'first-launch') {
state = 'second-launch';
} else if (String(data) === '--third' && state === 'second-launch') {
resolve();
} else {
reject(new Error(`Unexpected state: "${state}", data: "${data}"`));
}
});
});
const appPath = path.join(fixturesPath, 'api', 'relaunch');
const child = cp.spawn(process.execPath, [appPath, '--first']);
child.stdout.on('data', (c) => console.log(c.toString()));
child.stderr.on('data', (c) => console.log(c.toString()));
child.on('exit', (code, signal) => {
if (code !== 0) {
console.log(`Process exited with code "${code}" signal "${signal}"`);
}
});
})
);
});
ifdescribe(process.platform === 'darwin')('app.setUserActivity(type, userInfo)', () => {
@@ -485,35 +502,35 @@ describe('app module', () => {
afterEach(closeAllWindows);
it('is emitted when visiting a server with a self-signed cert', async () => {
const w = new BrowserWindow({ show: false });
w.loadURL(secureUrl);
dangerouslyIgnoreWebContentsLoadResult(w.loadURL(secureUrl));
await once(app, 'certificate-error');
});
describe('when denied', () => {
before(() => {
beforeAll(() => {
app.on('certificate-error', (event, webContents, url, error, certificate, callback) => {
callback(false);
});
});
after(() => {
afterAll(() => {
app.removeAllListeners('certificate-error');
});
it('causes did-fail-load', async () => {
const w = new BrowserWindow({ show: false });
w.loadURL(secureUrl);
dangerouslyIgnoreWebContentsLoadResult(w.loadURL(secureUrl));
await once(w.webContents, 'did-fail-load');
});
});
});
// xdescribe('app.importCertificate', () => {
// describe.skip('app.importCertificate', () => {
// let w = null
// before(function () {
// beforeAll((ctx) => {
// if (process.platform !== 'linux') {
// this.skip()
// ctx.skip()
// }
// })
@@ -629,7 +646,7 @@ describe('app module', () => {
const expectedBadgeCount = 42;
after(() => {
afterAll(() => {
app.badgeCount = 0;
});
@@ -1354,7 +1371,7 @@ describe('app module', () => {
ifdescribe(process.platform !== 'linux')('select-client-certificate event', () => {
let w: BrowserWindow;
before(function () {
beforeAll(function () {
session.fromPartition('empty-certificate').setCertificateVerifyProc((req, cb) => {
cb(0);
});
@@ -1376,7 +1393,7 @@ describe('app module', () => {
})
);
after(() => session.fromPartition('empty-certificate').setCertificateVerifyProc(null));
afterAll(() => session.fromPartition('empty-certificate').setCertificateVerifyProc(null));
it('can respond with empty certificate list', async () => {
app.once('select-client-certificate', function (event, webContents, url, list, callback) {
@@ -1402,7 +1419,7 @@ describe('app module', () => {
let Winreg: any;
let classesKey: any;
before(function () {
beforeAll(function () {
Winreg = require('winreg');
classesKey = new Winreg({
@@ -1411,20 +1428,22 @@ describe('app module', () => {
});
});
after(function (done) {
if (process.platform !== 'win32') {
done();
} else {
const protocolKey = new Winreg({
hive: Winreg.HKCU,
key: `\\Software\\Classes\\${protocol}`
});
afterAll(
withDone((done) => {
if (process.platform !== 'win32') {
done();
} else {
const protocolKey = new Winreg({
hive: Winreg.HKCU,
key: `\\Software\\Classes\\${protocol}`
});
// The last test leaves the registry dirty,
// delete the protocol key for those of us who test at home
protocolKey.destroy(() => done());
}
});
// The last test leaves the registry dirty,
// delete the protocol key for those of us who test at home
protocolKey.destroy(() => done());
}
})
);
beforeEach(() => {
app.removeAsDefaultProtocolClient(protocol);
@@ -1766,82 +1785,92 @@ describe('app module', () => {
const socketPath =
process.platform === 'win32' ? '\\\\.\\pipe\\electron-mixed-sandbox' : '/tmp/electron-mixed-sandbox';
beforeEach(function (done) {
fs.unlink(socketPath, () => {
server = net.createServer();
server.listen(socketPath);
done();
});
});
afterEach((done) => {
if (appProcess != null) appProcess.kill();
if (server) {
server.close(() => {
if (process.platform === 'win32') {
done();
} else {
fs.unlink(socketPath, () => done());
}
beforeEach(
withDone((done) => {
fs.unlink(socketPath, () => {
server = net.createServer();
server.listen(socketPath);
done();
});
} else {
done();
}
});
})
);
afterEach(
withDone((done) => {
if (appProcess != null) appProcess.kill();
if (server) {
server.close(() => {
if (process.platform === 'win32') {
done();
} else {
fs.unlink(socketPath, () => done());
}
});
} else {
done();
}
})
);
describe('when app.enableSandbox() is called', () => {
it('adds --enable-sandbox to all renderer processes', (done) => {
const appPath = path.join(fixturesPath, 'api', 'mixed-sandbox-app');
appProcess = cp.spawn(process.execPath, [appPath, '--app-enable-sandbox'], { stdio: 'inherit' });
it(
'adds --enable-sandbox to all renderer processes',
withDone((done) => {
const appPath = path.join(fixturesPath, 'api', 'mixed-sandbox-app');
appProcess = cp.spawn(process.execPath, [appPath, '--app-enable-sandbox'], { stdio: 'inherit' });
server.once('error', (error) => {
done(error);
});
server.on('connection', (client) => {
client.once('data', (data) => {
const argv = JSON.parse(data.toString());
expect(argv.sandbox).to.include('--enable-sandbox');
expect(argv.sandbox).to.not.include('--no-sandbox');
expect(argv.noSandbox).to.include('--enable-sandbox');
expect(argv.noSandbox).to.not.include('--no-sandbox');
expect(argv.noSandboxDevtools).to.equal(true);
expect(argv.sandboxDevtools).to.equal(true);
done();
server.once('error', (error) => {
done(error);
});
});
});
server.on('connection', (client) => {
client.once('data', (data) => {
const argv = JSON.parse(data.toString());
expect(argv.sandbox).to.include('--enable-sandbox');
expect(argv.sandbox).to.not.include('--no-sandbox');
expect(argv.noSandbox).to.include('--enable-sandbox');
expect(argv.noSandbox).to.not.include('--no-sandbox');
expect(argv.noSandboxDevtools).to.equal(true);
expect(argv.sandboxDevtools).to.equal(true);
done();
});
});
})
);
});
describe('when the app is launched with --enable-sandbox', () => {
it('adds --enable-sandbox to all renderer processes', (done) => {
const appPath = path.join(fixturesPath, 'api', 'mixed-sandbox-app');
appProcess = cp.spawn(process.execPath, [appPath, '--enable-sandbox'], { stdio: 'inherit' });
it(
'adds --enable-sandbox to all renderer processes',
withDone((done) => {
const appPath = path.join(fixturesPath, 'api', 'mixed-sandbox-app');
appProcess = cp.spawn(process.execPath, [appPath, '--enable-sandbox'], { stdio: 'inherit' });
server.once('error', (error) => {
done(error);
});
server.on('connection', (client) => {
client.once('data', (data) => {
const argv = JSON.parse(data.toString());
expect(argv.sandbox).to.include('--enable-sandbox');
expect(argv.sandbox).to.not.include('--no-sandbox');
expect(argv.noSandbox).to.include('--enable-sandbox');
expect(argv.noSandbox).to.not.include('--no-sandbox');
expect(argv.noSandboxDevtools).to.equal(true);
expect(argv.sandboxDevtools).to.equal(true);
done();
server.once('error', (error) => {
done(error);
});
});
});
server.on('connection', (client) => {
client.once('data', (data) => {
const argv = JSON.parse(data.toString());
expect(argv.sandbox).to.include('--enable-sandbox');
expect(argv.sandbox).to.not.include('--no-sandbox');
expect(argv.noSandbox).to.include('--enable-sandbox');
expect(argv.noSandbox).to.not.include('--no-sandbox');
expect(argv.noSandboxDevtools).to.equal(true);
expect(argv.sandboxDevtools).to.equal(true);
done();
});
});
})
);
});
}
);
@@ -1858,7 +1887,7 @@ describe('app module', () => {
describe('app.isActive', () => {
afterEach(closeAllWindows);
it('returns true when the app becomes active', async () => {
it('returns true when the app becomes active', async (ctx) => {
expect(app.isActive()).to.equal(false);
const w = new BrowserWindow({
@@ -1869,7 +1898,7 @@ describe('app module', () => {
w.show();
await expect(waitUntil(() => app.isActive())).to.eventually.be.fulfilled();
await expect(waitUntil(() => app.isActive(), ctx.signal)).to.eventually.be.fulfilled();
w.close();
app.hide();
@@ -1879,19 +1908,19 @@ describe('app module', () => {
ifdescribe(process.platform === 'darwin')('app hide and show API', () => {
describe('app.isHidden', () => {
it('returns true when the app is hidden', async () => {
it('returns true when the app is hidden', async (ctx) => {
app.hide();
await expect(waitUntil(() => app.isHidden())).to.eventually.be.fulfilled();
await expect(waitUntil(() => app.isHidden(), ctx.signal)).to.eventually.be.fulfilled();
});
it('returns false when the app is shown', async () => {
it('returns false when the app is shown', async (ctx) => {
app.show();
await expect(waitUntil(() => !app.isHidden())).to.eventually.be.fulfilled();
await expect(waitUntil(() => !app.isHidden(), ctx.signal)).to.eventually.be.fulfilled();
});
});
});
ifdescribe(process.platform === 'darwin')('dock APIs', () => {
after(async () => {
afterAll(async () => {
await app.dock?.show();
});
@@ -1944,7 +1973,7 @@ describe('app module', () => {
});
describe('dock.setBadge', () => {
after(() => {
afterAll(() => {
app.dock?.setBadge('');
});
@@ -2083,7 +2112,7 @@ describe('app module', () => {
});
describe('configureHostResolver', () => {
after(() => {
afterAll(() => {
// Returns to the default configuration.
app.configureHostResolver({});
});
@@ -2258,8 +2287,15 @@ describe('app module', () => {
it('impacts proxy for requests made from utility process', async () => {
const utilityFixturePath = path.resolve(__dirname, 'fixtures', 'api', 'utility-process', 'api-net-spec.js');
// This closure is stringified and evaluated inside the utility process
// fixture, so it must not capture any imports from this file (vite's SSR
// transform rewrites those to __vite_ssr_import_N__ bindings that don't
// exist in the fixture). Use inline require() instead.
const fn = async () => {
const urlRequest = electronNet.request('http://example.com/');
const { net } = require('electron');
const { expect } = require('chai');
const { getResponse, collectStreamBody } = require('../../../lib/net-helpers');
const urlRequest = net.request('http://example.com/');
const response = await getResponse(urlRequest);
expect(response.statusCode).to.equal(200);
const message = await collectStreamBody(response);
@@ -2277,11 +2313,15 @@ describe('app module', () => {
const child = utilityProcess.fork(utilityFixturePath, [], {
execArgv: ['--expose-gc']
});
const [ready] = await once(child, 'message');
expect(ready?.type).to.equal('ready');
child.postMessage({ fn: `(${fn})()` });
const [data] = await once(child, 'message');
expect(data.ok).to.be.true(data.message);
// Cleanup.
const [code] = await once(child, 'exit');
const exited = once(child, 'exit');
child.postMessage({ type: 'shutdown' });
const [code] = await exited;
expect(code).to.equal(0);
});
@@ -2350,7 +2390,7 @@ describe('default behavior', () => {
describe('user agent fallback', () => {
let initialValue: string;
before(() => {
beforeAll(() => {
initialValue = app.userAgentFallback!;
});
@@ -2376,7 +2416,7 @@ describe('default behavior', () => {
let server: http.Server;
let serverUrl: string;
before(async () => {
beforeAll(async () => {
server = http.createServer((request, response) => {
if (request.headers.authorization) {
return response.end('ok');
@@ -2387,13 +2427,13 @@ describe('default behavior', () => {
serverUrl = (await listen(server)).url;
});
after(() => {
afterAll(() => {
server.close();
});
it('should emit a login event on app when a WebContents hits a 401', async () => {
const w = new BrowserWindow({ show: false });
w.loadURL(serverUrl);
dangerouslyIgnoreWebContentsLoadResult(w.loadURL(serverUrl));
const [, webContents] = (await once(app, 'login')) as [any, WebContents];
expect(webContents).to.equal(w.webContents);
});

View File

@@ -1,6 +1,7 @@
import { autoUpdater } from 'electron/main';
import { expect } from 'chai';
import { describe, it } from 'vitest';
import { once } from 'node:events';

View File

@@ -1,9 +1,11 @@
import { autoUpdater, systemPreferences } from 'electron';
import { expect } from 'chai';
import * as express from 'express';
import * as psList from 'ps-list';
import * as uuid from 'uuid';
import { afterAll, afterEach, beforeEach, describe, it } from 'vitest';
import express = require('express');
import psList = require('ps-list');
import * as cp from 'node:child_process';
import * as fs from 'node:fs';
@@ -21,18 +23,16 @@ import {
unsignApp
} from './lib/codesign-helpers';
import { withTempDirectory } from './lib/fs-helpers';
import { ifdescribe, ifit } from './lib/spec-helpers';
import { ifdescribe, ifit, withDone } from './lib/spec-helpers';
// We can only test the auto updater on darwin non-component builds
ifdescribe(shouldRunCodesignTests)('autoUpdater behavior', function () {
this.timeout(120000);
ifdescribe(shouldRunCodesignTests)('autoUpdater behavior', { timeout: 120000 }, () => {
let identity = '';
beforeEach(function () {
beforeEach((ctx) => {
const result = getCodesignIdentity();
if (result === null) {
this.skip();
ctx.skip();
} else {
identity = result;
}
@@ -154,7 +154,7 @@ ifdescribe(shouldRunCodesignTests)('autoUpdater behavior', function () {
return cachedZips[key];
};
after(() => {
afterAll(() => {
for (const version of Object.keys(cachedZips)) {
cp.spawnSync('rm', ['-r', path.dirname(cachedZips[version])]);
}
@@ -226,18 +226,20 @@ ifdescribe(shouldRunCodesignTests)('autoUpdater behavior', function () {
let httpServer: http.Server = null as any;
let requests: express.Request[] = [];
beforeEach((done) => {
requests = [];
server = express();
server.use((req, res, next) => {
requests.push(req);
next();
});
httpServer = server.listen(0, '127.0.0.1', () => {
port = (httpServer.address() as AddressInfo).port;
done();
});
});
beforeEach(
withDone((done) => {
requests = [];
server = express();
server.use((req, res, next) => {
requests.push(req);
next();
});
httpServer = server.listen(0, '127.0.0.1', () => {
port = (httpServer.address() as AddressInfo).port;
done();
});
})
);
afterEach(async () => {
if (httpServer) {

View File

@@ -1,5 +1,6 @@
import { expect } from 'chai';
import * as express from 'express';
import { afterAll, afterEach, beforeAll, beforeEach, describe, it } from 'vitest';
import * as http from 'node:http';
import { AddressInfo } from 'node:net';
@@ -17,7 +18,7 @@ import {
uninstallMsixPackage,
unregisterExecutableWithIdentity
} from './lib/msix-helpers';
import { ifdescribe } from './lib/spec-helpers';
import { ifdescribe, withDone } from './lib/spec-helpers';
const ELECTRON_MSIX_ALIAS = 'ElectronMSIX.exe';
const MAIN_JS_PATH = getMainJsFixturePath();
@@ -25,17 +26,15 @@ const MSIX_V1 = getMsixFixturePath('v1');
const MSIX_V2 = getMsixFixturePath('v2');
// We can only test the MSIX updater on Windows
ifdescribe(shouldRunMsixTests)('autoUpdater MSIX behavior', function () {
this.timeout(120000);
before(async function () {
ifdescribe(shouldRunMsixTests)('autoUpdater MSIX behavior', { timeout: 120000 }, () => {
beforeAll(async function () {
await installMsixCertificate();
const electronExec = getElectronExecutable();
await registerExecutableWithIdentity(electronExec);
});
after(async function () {
afterAll(async function () {
await unregisterExecutableWithIdentity();
});
@@ -75,18 +74,20 @@ ifdescribe(shouldRunMsixTests)('autoUpdater MSIX behavior', function () {
let httpServer: http.Server = null as any;
let requests: express.Request[] = [];
beforeEach((done) => {
requests = [];
server = express();
server.use((req, res, next) => {
requests.push(req);
next();
});
httpServer = server.listen(0, '127.0.0.1', () => {
port = (httpServer.address() as AddressInfo).port;
done();
});
});
beforeEach(
withDone((done) => {
requests = [];
server = express();
server.use((req, res, next) => {
requests.push(req);
next();
});
httpServer = server.listen(0, '127.0.0.1', () => {
port = (httpServer.address() as AddressInfo).port;
done();
});
})
);
afterEach(async () => {
if (httpServer) {

View File

@@ -1,12 +1,18 @@
import { BrowserView, BrowserWindow, screen, session, webContents } from 'electron/main';
import { BrowserView, BrowserWindow, session, webContents } from 'electron/main';
import { expect } from 'chai';
import { afterEach, beforeEach, describe, it } from 'vitest';
import { once } from 'node:events';
import * as path from 'node:path';
import { ScreenCapture, hasCapturableScreen } from './lib/screen-helpers';
import { defer, ifit, startRemoteControlApp } from './lib/spec-helpers';
import {
defer,
runCleanupFunctions,
startRemoteControlApp,
withDone,
dangerouslyIgnoreWebContentsLoadResult
} from './lib/spec-helpers';
import { closeWindow } from './lib/window-helpers';
describe('BrowserView module', () => {
@@ -32,6 +38,7 @@ describe('BrowserView module', () => {
});
afterEach(async () => {
await runCleanupFunctions();
if (w && !w.isDestroyed()) {
const p = once(w.webContents, 'destroyed');
await closeWindow(w);
@@ -82,43 +89,8 @@ describe('BrowserView module', () => {
}).not.to.throw();
});
ifit(hasCapturableScreen())('sets the background color to transparent if none is set', async () => {
const display = screen.getPrimaryDisplay();
const WINDOW_BACKGROUND_COLOR = '#55ccbb';
w.show();
w.setBounds(display.bounds);
w.setBackgroundColor(WINDOW_BACKGROUND_COLOR);
await w.loadURL('data:text/html,<html></html>');
view = new BrowserView();
view.setBounds(display.bounds);
w.setBrowserView(view);
await view.webContents.loadURL('data:text/html,hello there');
const screenCapture = new ScreenCapture(display);
await screenCapture.expectColorAtCenterMatches(WINDOW_BACKGROUND_COLOR);
});
ifit(hasCapturableScreen())('successfully applies the background color', async () => {
const WINDOW_BACKGROUND_COLOR = '#55ccbb';
const VIEW_BACKGROUND_COLOR = '#ff00ff';
const display = screen.getPrimaryDisplay();
w.show();
w.setBounds(display.bounds);
w.setBackgroundColor(WINDOW_BACKGROUND_COLOR);
await w.loadURL('data:text/html,<html></html>');
view = new BrowserView();
view.setBounds(display.bounds);
w.setBrowserView(view);
w.setBackgroundColor(VIEW_BACKGROUND_COLOR);
await view.webContents.loadURL('data:text/html,hello there');
const screenCapture = new ScreenCapture(display);
await screenCapture.expectColorAtCenterMatches(VIEW_BACKGROUND_COLOR);
});
// Screen-capture tests for setBackgroundColor live in
// spec/serial/browser-view-background-color.spec.ts.
});
describe('BrowserView.setAutoResize()', () => {
@@ -409,13 +381,15 @@ describe('BrowserView module', () => {
w.addBrowserView(view);
});
it('does not crash if the webContents is destroyed after a URL is loaded', () => {
it('does not crash if the webContents is destroyed after a URL is loaded', async () => {
view = new BrowserView();
expect(async () => {
view.setBounds({ x: 0, y: 0, width: 400, height: 300 });
await view.webContents.loadURL('data:text/html,hello there');
view.webContents.destroy();
}).to.not.throw();
await expect(
(async () => {
view.setBounds({ x: 0, y: 0, width: 400, height: 300 });
await view.webContents.loadURL('data:text/html,hello there');
view.webContents.destroy();
})()
).to.not.be.rejected();
});
it('can handle BrowserView reparenting', async () => {
@@ -424,7 +398,7 @@ describe('BrowserView module', () => {
expect(view.ownerWindow).to.be.null('ownerWindow');
w.addBrowserView(view);
view.webContents.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(view.webContents.loadURL('about:blank'));
await once(view.webContents, 'did-finish-load');
expect(view.ownerWindow).to.equal(w);
@@ -436,7 +410,7 @@ describe('BrowserView module', () => {
w.close();
view.webContents.loadURL(`file://${fixtures}/pages/blank.html`);
dangerouslyIgnoreWebContentsLoadResult(view.webContents.loadURL(`file://${fixtures}/pages/blank.html`));
await once(view.webContents, 'did-finish-load');
// Clean up - the afterEach hook assumes the webContents on w is still alive.
@@ -454,7 +428,7 @@ describe('BrowserView module', () => {
w2.addBrowserView(view);
expect(view.ownerWindow).to.equal(w2);
w2.webContents.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(w2.webContents.loadURL('about:blank'));
await once(w2.webContents, 'did-finish-load');
w2.close();
});
@@ -467,7 +441,7 @@ describe('BrowserView module', () => {
w.addBrowserView(view);
expect(view.ownerWindow).to.equal(w);
w.webContents.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(w.webContents.loadURL('about:blank'));
await once(w.webContents, 'did-finish-load');
});
@@ -711,7 +685,7 @@ describe('BrowserView module', () => {
await rc.remotely(() => {
const { app, BrowserView, BrowserWindow } = require('electron');
const bv = new BrowserView();
bv.webContents.loadURL('about:blank');
bv.webContents.loadURL('about:blank').catch(() => {});
const bw = new BrowserWindow({ show: false });
bw.addBrowserView(bv);
setTimeout(() => {
@@ -742,17 +716,22 @@ describe('BrowserView module', () => {
});
describe('window.open()', () => {
it('works in BrowserView', (done) => {
view = new BrowserView();
w.setBrowserView(view);
view.webContents.setWindowOpenHandler(({ url, frameName }) => {
expect(url).to.equal('http://host/');
expect(frameName).to.equal('host');
done();
return { action: 'deny' };
});
view.webContents.loadFile(path.join(fixtures, 'pages', 'window-open.html'));
});
it(
'works in BrowserView',
withDone((done) => {
view = new BrowserView();
w.setBrowserView(view);
view.webContents.setWindowOpenHandler(({ url, frameName }) => {
expect(url).to.equal('http://host/');
expect(frameName).to.equal('host');
done();
return { action: 'deny' };
});
dangerouslyIgnoreWebContentsLoadResult(
view.webContents.loadFile(path.join(fixtures, 'pages', 'window-open.html'))
);
})
);
});
describe('BrowserView.capturePage(rect)', () => {
@@ -778,7 +757,7 @@ describe('BrowserView module', () => {
expect(image.isEmpty()).to.equal(true);
});
xit('resolves after the window is hidden and capturer count is non-zero', async () => {
it.skip('resolves after the window is hidden and capturer count is non-zero', async () => {
view = new BrowserView({
webPreferences: {
backgroundThrottling: false

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
import { app, contentTracing, TraceConfig, TraceCategoriesAndOptions } from 'electron/main';
import { expect } from 'chai';
import { beforeEach, describe, it } from 'vitest';
import * as fs from 'node:fs';
import * as path from 'node:path';
@@ -31,14 +32,8 @@ ifdescribe(!['arm', 'arm64'].includes(process.arch) || process.platform !== 'lin
}
});
describe('startRecording', function () {
if (process.platform === 'win32' && process.arch === 'arm64') {
// WOA needs more time
this.timeout(10e3);
} else {
this.timeout(5e3);
}
// WOA needs more time
describe('startRecording', { timeout: process.platform === 'win32' && process.arch === 'arm64' ? 10e3 : 5e3 }, () => {
const getFileSizeInKiloBytes = (filePath: string) => {
const stats = fs.statSync(filePath);
const fileSizeInBytes = stats.size;
@@ -95,50 +90,48 @@ ifdescribe(!['arm', 'arm64'].includes(process.arch) || process.platform !== 'lin
});
});
ifdescribe(process.platform !== 'linux')('stopRecording', function () {
if (process.platform === 'win32' && process.arch === 'arm64') {
// WOA needs more time
this.timeout(10e3);
} else {
this.timeout(5e3);
// WOA needs more time
ifdescribe(process.platform !== 'linux')(
'stopRecording',
{ timeout: process.platform === 'win32' && process.arch === 'arm64' ? 10e3 : 5e3 },
() => {
// FIXME(samuelmaddock): this test regularly flakes
it.skip('does not crash on empty string', async () => {
const options = {
categoryFilter: '*',
traceOptions: 'record-until-full,enable-sampling'
};
await contentTracing.startRecording(options);
const path = await contentTracing.stopRecording('');
expect(path).to.be.a('string').that.is.not.empty('result path');
expect(fs.statSync(path).isFile()).to.be.true('output exists');
});
it('calls its callback with a result file path', async () => {
const resultFilePath = await record(/* options */ {}, outputFilePath);
expect(resultFilePath).to.be.a('string').and.be.equal(outputFilePath);
});
it('creates a temporary file when an empty string is passed', async function () {
const resultFilePath = await record(/* options */ {}, /* outputFilePath */ '');
expect(resultFilePath).to.be.a('string').that.is.not.empty('result path');
});
it('creates a temporary file when no path is passed', async function () {
const resultFilePath = await record(/* options */ {}, /* outputFilePath */ undefined);
expect(resultFilePath).to.be.a('string').that.is.not.empty('result path');
});
it('rejects if no trace is happening', async () => {
await expect(contentTracing.stopRecording()).to.be.rejectedWith(
'Failed to stop tracing - no trace in progress'
);
});
}
);
// FIXME(samuelmaddock): this test regularly flakes
it.skip('does not crash on empty string', async () => {
const options = {
categoryFilter: '*',
traceOptions: 'record-until-full,enable-sampling'
};
await contentTracing.startRecording(options);
const path = await contentTracing.stopRecording('');
expect(path).to.be.a('string').that.is.not.empty('result path');
expect(fs.statSync(path).isFile()).to.be.true('output exists');
});
it('calls its callback with a result file path', async () => {
const resultFilePath = await record(/* options */ {}, outputFilePath);
expect(resultFilePath).to.be.a('string').and.be.equal(outputFilePath);
});
it('creates a temporary file when an empty string is passed', async function () {
const resultFilePath = await record(/* options */ {}, /* outputFilePath */ '');
expect(resultFilePath).to.be.a('string').that.is.not.empty('result path');
});
it('creates a temporary file when no path is passed', async function () {
const resultFilePath = await record(/* options */ {}, /* outputFilePath */ undefined);
expect(resultFilePath).to.be.a('string').that.is.not.empty('result path');
});
it('rejects if no trace is happening', async () => {
await expect(contentTracing.stopRecording()).to.be.rejectedWith('Failed to stop tracing - no trace in progress');
});
});
describe('getTraceBufferUsage', function () {
this.timeout(10e3);
describe('getTraceBufferUsage', { timeout: 10e3 }, () => {
it('does not crash and returns valid usage data', async () => {
await app.whenReady();
await contentTracing.startRecording({
@@ -167,8 +160,7 @@ ifdescribe(!['arm', 'arm64'].includes(process.arch) || process.platform !== 'lin
});
describe('captured events', () => {
it('include V8 samples from the main process', async function () {
this.timeout(60000);
it('include V8 samples from the main process', { timeout: 60000 }, async () => {
await contentTracing.startRecording({
categoryFilter: 'disabled-by-default-v8.cpu_profiler',
traceOptions: 'record-until-full'

View File

@@ -1,7 +1,7 @@
import { BrowserWindow, ipcMain } from 'electron/main';
import { contextBridge } from 'electron/renderer';
import { expect } from 'chai';
import { afterAll, afterEach, beforeAll, describe, it } from 'vitest';
import * as cp from 'node:child_process';
import { once } from 'node:events';
@@ -10,7 +10,8 @@ import * as http from 'node:http';
import * as os from 'node:os';
import * as path from 'node:path';
import { listen } from './lib/spec-helpers';
import { contextBridge, rewriteForRemoteEval } from './lib/remote-tools';
import { listen, dangerouslyIgnoreWebContentsLoadResult } from './lib/spec-helpers';
import { closeWindow } from './lib/window-helpers';
const fixturesPath = path.resolve(__dirname, 'fixtures', 'api', 'context-bridge');
@@ -21,7 +22,7 @@ describe('contextBridge', () => {
let server: http.Server;
let serverUrl: string;
before(async () => {
beforeAll(async () => {
server = http.createServer((req, res) => {
res.setHeader('Content-Type', 'text/html');
res.end('');
@@ -29,13 +30,13 @@ describe('contextBridge', () => {
serverUrl = (await listen(server)).url;
});
after(async () => {
afterAll(async () => {
if (server) await new Promise((resolve) => server.close(resolve));
server = null as any;
await closeWindow(w);
});
afterEach(async () => {
await closeWindow(w);
if (dir) await fs.promises.rm(dir, { force: true, recursive: true });
});
@@ -47,9 +48,12 @@ describe('contextBridge', () => {
preload: path.resolve(fixturesPath, 'can-bind-preload.js')
}
});
w.loadFile(path.resolve(fixturesPath, 'empty.html'));
dangerouslyIgnoreWebContentsLoadResult(w.loadFile(path.resolve(fixturesPath, 'empty.html')));
const [, bound] = await once(ipcMain, 'context-bridge-bound');
expect(bound).to.equal(false);
await closeWindow(w);
w = null as unknown as BrowserWindow;
});
it('should be accessible when contextIsolation is enabled', async () => {
@@ -60,15 +64,34 @@ describe('contextBridge', () => {
preload: path.resolve(fixturesPath, 'can-bind-preload.js')
}
});
w.loadFile(path.resolve(fixturesPath, 'empty.html'));
dangerouslyIgnoreWebContentsLoadResult(w.loadFile(path.resolve(fixturesPath, 'empty.html')));
const [, bound] = await once(ipcMain, 'context-bridge-bound');
expect(bound).to.equal(true);
await closeWindow(w);
w = null as unknown as BrowserWindow;
});
const generateTests = (useSandbox: boolean) => {
describe(`with sandbox=${useSandbox}`, () => {
let registeredPreloads: string[] = [];
afterEach(() => {
for (const registeredPreload of registeredPreloads) {
w.webContents.session.unregisterPreloadScript(registeredPreload);
}
registeredPreloads = [];
});
afterAll(async () => {
await closeWindow(w);
w = null as unknown as BrowserWindow;
});
/** @remote */
const makeBindingWindow = async (bindingCreator: Function, worldId: number = 0) => {
const bindingSrc = rewriteForRemoteEval(bindingCreator);
const preloadContentForMainWorld = `const renderer_1 = require('electron');
const __rt = renderer_1;
${
useSandbox
? ''
@@ -78,9 +101,10 @@ describe('contextBridge', () => {
run: () => gc()
});`
}
(${bindingCreator.toString()})();`;
(${bindingSrc})();`;
const preloadContentForIsolatedWorld = `const renderer_1 = require('electron');
const __rt = renderer_1;
${
useSandbox
? ''
@@ -93,7 +117,7 @@ describe('contextBridge', () => {
run: () => gc()
});`
}
(${bindingCreator.toString()})();`;
(${bindingSrc})();`;
const tmpDir = await fs.promises.mkdtemp(path.resolve(os.tmpdir(), 'electron-spec-preload-'));
dir = tmpDir;
@@ -101,19 +125,27 @@ describe('contextBridge', () => {
path.resolve(tmpDir, 'preload.js'),
worldId === 0 ? preloadContentForMainWorld : preloadContentForIsolatedWorld
);
w = new BrowserWindow({
show: false,
webPreferences: {
contextIsolation: true,
nodeIntegration: true,
sandbox: useSandbox,
preload: path.resolve(tmpDir, 'preload.js'),
additionalArguments: ['--unsafely-expose-electron-internals-for-testing']
}
});
await w.loadURL(serverUrl);
w =
w ||
new BrowserWindow({
show: false,
webPreferences: {
contextIsolation: true,
nodeIntegration: true,
sandbox: useSandbox,
additionalArguments: ['--unsafely-expose-electron-internals-for-testing']
}
});
registeredPreloads.push(
w.webContents.session.registerPreloadScript({
filePath: path.resolve(tmpDir, 'preload.js'),
type: 'frame'
})
);
await w.loadURL(serverUrl).catch(() => {});
};
/** @remote no-locals */
const callWithBindings = (fn: Function, worldId: number = 0) =>
worldId === 0
? w.webContents.executeJavaScript(`(${fn.toString()})(window)`)

View File

@@ -2,6 +2,7 @@ import { NativeImage, nativeImage } from 'electron/common';
import { BrowserWindow } from 'electron/main';
import { AssertionError, expect } from 'chai';
import { afterEach, describe, it } from 'vitest';
import path = require('node:path');

View File

@@ -3,6 +3,7 @@ import { app } from 'electron/main';
import * as Busboy from 'busboy';
import { expect } from 'chai';
import * as uuid from 'uuid';
import { describe, it } from 'vitest';
import * as childProcess from 'node:child_process';
import { EventEmitter } from 'node:events';
@@ -11,7 +12,15 @@ import * as http from 'node:http';
import * as path from 'node:path';
import { setTimeout } from 'node:timers/promises';
import { ifdescribe, ifit, defer, startRemoteControlApp, repeatedly, listen } from './lib/spec-helpers';
import {
ifdescribe,
ifit,
defer,
startRemoteControlApp,
repeatedly,
listen,
dangerouslyIgnoreWebContentsLoadResult
} from './lib/spec-helpers';
const isWindowsOnArm = process.platform === 'win32' && process.arch === 'arm64';
const isLinuxOnArm = process.platform === 'linux' && process.arch.includes('arm');
@@ -263,10 +272,9 @@ ifdescribe(!isLinuxOnArm && !process.mas && !process.env.DISABLE_CRASH_REPORTER_
// Regression: base::circular_deque relocates elements on growth,
// corrupting crashpad::Annotation's self-referential pointers and
// causing missing crash keys or a hung handler. See crash_keys.cc.
it('does not corrupt the crashpad annotation list after deque reallocation', async function () {
it('does not corrupt the crashpad annotation list after deque reallocation', { timeout: 45000 }, async () => {
// Tight timeout so a hanging handler fails fast instead of waiting
// for the mocha default of 120s.
this.timeout(45000);
// for the suite default.
const { port, waitForCrash } = await startServer();
runCrashApp('renderer-dynamic-keys', port);
const crash = await Promise.race([
@@ -314,7 +322,7 @@ ifdescribe(!isLinuxOnArm && !process.mas && !process.env.DISABLE_CRASH_REPORTER_
show: false,
webPreferences: { nodeIntegration: true, contextIsolation: false }
});
bw.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(bw.loadURL('about:blank'));
bw.webContents.executeJavaScript(
"process._linkedBinding('electron_common_v8_util').triggerFatalErrorForTesting()"
);
@@ -330,20 +338,22 @@ ifdescribe(!isLinuxOnArm && !process.mas && !process.env.DISABLE_CRASH_REPORTER_
});
describe('OOM crash keys', () => {
it('reports OOM stack trace and heap statistics when renderer runs out of memory', async function () {
this.timeout(120000);
const { port, waitForCrash } = await startServer();
runCrashApp('renderer-oom', port, ['--js-flags=--max-old-space-size=128']);
const crash = await waitForCrash();
expect(crash.process_type).to.equal('renderer');
expect(crash['electron.v8-oom.stack']).to.be.a('string');
expect(crash['electron.v8-oom.stack']).to.include('oomTrigger');
expect(crash['electron.v8-oom.heap.used']).to.be.a('string');
expect(crash['electron.v8-oom.heap.limit']).to.be.a('string');
});
it(
'reports OOM stack trace and heap statistics when renderer runs out of memory',
{ timeout: 120000 },
async () => {
const { port, waitForCrash } = await startServer();
runCrashApp('renderer-oom', port, ['--js-flags=--max-old-space-size=128']);
const crash = await waitForCrash();
expect(crash.process_type).to.equal('renderer');
expect(crash['electron.v8-oom.stack']).to.be.a('string');
expect(crash['electron.v8-oom.stack']).to.include('oomTrigger');
expect(crash['electron.v8-oom.heap.used']).to.be.a('string');
expect(crash['electron.v8-oom.heap.limit']).to.be.a('string');
}
);
it('captures the calling function on JSON.stringify OOM', async function () {
this.timeout(120000);
it('captures the calling function on JSON.stringify OOM', { timeout: 120000 }, async () => {
const { port, waitForCrash } = await startServer();
runCrashApp('renderer-oom-json', port, ['--js-flags=--max-old-space-size=128']);
const crash = await waitForCrash();
@@ -352,8 +362,7 @@ ifdescribe(!isLinuxOnArm && !process.mas && !process.env.DISABLE_CRASH_REPORTER_
expect(crash['electron.v8-oom.stack']).to.include('serializeData');
});
it('captures OOM crash keys inside a web worker', async function () {
this.timeout(120000);
it('captures OOM crash keys inside a web worker', { timeout: 120000 }, async () => {
const { port, waitForCrash } = await startServer();
runCrashApp('renderer-oom-worker', port, ['--js-flags=--max-old-space-size=128']);
const crash = await waitForCrash();
@@ -521,7 +530,7 @@ ifdescribe(!isLinuxOnArm && !process.mas && !process.env.DISABLE_CRASH_REPORTER_
show: false,
webPreferences: { nodeIntegration: true, contextIsolation: false }
});
bw.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(bw.loadURL('about:blank'));
bw.webContents.executeJavaScript('process.crash()');
});
await waitForCrash();
@@ -578,7 +587,7 @@ ifdescribe(!isLinuxOnArm && !process.mas && !process.env.DISABLE_CRASH_REPORTER_
show: false,
webPreferences: { nodeIntegration: true, contextIsolation: false }
});
bw.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(bw.loadURL('about:blank'));
await bw.webContents.executeJavaScript(
"require('electron').crashReporter.addExtraParameter('hello', 'world')"
);
@@ -630,7 +639,7 @@ ifdescribe(!isLinuxOnArm && !process.mas && !process.env.DISABLE_CRASH_REPORTER_
show: false,
webPreferences: { nodeIntegration: true, contextIsolation: false }
});
bw.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(bw.loadURL('about:blank'));
bw.webContents.executeJavaScript('process.crash()');
});
} else if (processType === 'sandboxed-renderer') {
@@ -641,7 +650,7 @@ ifdescribe(!isLinuxOnArm && !process.mas && !process.env.DISABLE_CRASH_REPORTER_
show: false,
webPreferences: { sandbox: true, preload, contextIsolation: false }
});
bw.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(bw.loadURL('about:blank'));
}, preloadPath);
} else if (processType === 'node') {
const crashScriptPath = path.join(__dirname, 'fixtures', 'apps', 'crash', 'node-crash.js');

View File

@@ -1,13 +1,14 @@
import { BrowserWindow } from 'electron/main';
import { expect } from 'chai';
import { afterEach, beforeEach, describe, it } from 'vitest';
import { once } from 'node:events';
import * as http from 'node:http';
import * as path from 'node:path';
import { emittedUntil } from './lib/events-helpers';
import { listen } from './lib/spec-helpers';
import { listen, dangerouslyIgnoreWebContentsLoadResult } from './lib/spec-helpers';
import { closeAllWindows } from './lib/window-helpers';
describe('debugger module', () => {
@@ -54,7 +55,7 @@ describe('debugger module', () => {
});
it("doesn't disconnect an active devtools session", async () => {
w.webContents.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(w.webContents.loadURL('about:blank'));
const detach = once(w.webContents.debugger, 'detach');
w.webContents.debugger.attach();
w.webContents.openDevTools();
@@ -78,7 +79,7 @@ describe('debugger module', () => {
});
it('returns response', async () => {
w.webContents.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(w.webContents.loadURL('about:blank'));
w.webContents.debugger.attach();
const params = { expression: '4+2' };
@@ -91,7 +92,7 @@ describe('debugger module', () => {
});
it('returns response when devtools is opened', async () => {
w.webContents.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(w.webContents.loadURL('about:blank'));
w.webContents.debugger.attach();
const opened = once(w.webContents, 'devtools-opened');
@@ -112,7 +113,7 @@ describe('debugger module', () => {
process.platform !== 'win32'
? `file://${path.join(fixtures, 'pages', 'a.html')}`
: `file:///${path.join(fixtures, 'pages', 'a.html').replaceAll('\\', '/')}`;
w.webContents.loadURL(url);
dangerouslyIgnoreWebContentsLoadResult(w.webContents.loadURL(url));
w.webContents.debugger.attach();
const message = emittedUntil(
w.webContents.debugger,
@@ -128,7 +129,7 @@ describe('debugger module', () => {
});
it('returns error message when command fails', async () => {
w.webContents.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(w.webContents.loadURL('about:blank'));
w.webContents.debugger.attach();
const promise = w.webContents.debugger.sendCommand('Test');
@@ -144,7 +145,7 @@ describe('debugger module', () => {
});
const { url } = await listen(server);
w.loadURL(url);
dangerouslyIgnoreWebContentsLoadResult(w.loadURL(url));
// If we do this synchronously, it's fast enough to attach and enable
// network capture before the load. If we do it before the loadURL, for
// some reason network capture doesn't get enabled soon enough and we get
@@ -187,7 +188,7 @@ describe('debugger module', () => {
const { url } = await listen(server);
w.webContents.debugger.sendCommand('Network.enable');
w.loadURL(url);
dangerouslyIgnoreWebContentsLoadResult(w.loadURL(url));
await loadingFinished;
});
@@ -225,7 +226,7 @@ describe('debugger module', () => {
});
it('uses empty sessionId by default', async () => {
w.webContents.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(w.webContents.loadURL('about:blank'));
w.webContents.debugger.attach();
const onMessage = once(w.webContents.debugger, 'message');
await w.webContents.debugger.sendCommand('Target.setDiscoverTargets', { discover: true });
@@ -236,33 +237,38 @@ describe('debugger module', () => {
w.webContents.debugger.detach();
});
it('creates unique session id for each target', (done) => {
w.webContents.loadFile(path.join(__dirname, 'fixtures', 'sub-frames', 'debug-frames.html'));
it('creates unique session id for each target', async () => {
await w.webContents.loadFile(path.join(__dirname, 'fixtures', 'sub-frames', 'debug-frames.html'));
w.webContents.debugger.attach();
let debuggerSessionId: string;
w.webContents.debugger.on('message', (_event, ...args) => {
const [method, params, sessionId] = args;
if (method === 'Target.targetCreated') {
w.webContents.debugger
.sendCommand('Target.attachToTarget', { targetId: params.targetInfo.targetId, flatten: true })
.then((result) => {
debuggerSessionId = result.sessionId;
w.webContents.debugger.sendCommand('Debugger.enable', {}, result.sessionId);
// Ensure debugger finds a script to pause to possibly reduce flaky
// tests.
w.webContents.mainFrame.executeJavaScript('void 0;');
});
}
if (method === 'Debugger.scriptParsed') {
if (sessionId === debuggerSessionId) {
w.webContents.debugger.detach();
done();
const targetCreated = new Promise<{ targetId: string }>((resolve) => {
w.webContents.debugger.on('message', (_event, method, params) => {
if (method === 'Target.targetCreated') {
resolve({ targetId: params.targetInfo.targetId });
}
}
});
});
w.webContents.debugger.sendCommand('Target.setDiscoverTargets', { discover: true });
await w.webContents.debugger.sendCommand('Target.setDiscoverTargets', { discover: true });
const { targetId } = await targetCreated;
const { sessionId } = await w.webContents.debugger.sendCommand('Target.attachToTarget', {
targetId,
flatten: true
});
expect(sessionId).to.be.a('string').and.not.be.empty();
const scriptParsed = new Promise<string>((resolve) => {
w.webContents.debugger.on('message', (_event, method, _params, messageSessionId) => {
if (method === 'Debugger.scriptParsed') resolve(messageSessionId);
});
});
await w.webContents.debugger.sendCommand('Debugger.enable', {}, sessionId);
// Evaluate via CDP on the attached session so a script is parsed after
// Debugger.enable has been acknowledged, regardless of page load timing.
await w.webContents.debugger.sendCommand('Runtime.evaluate', { expression: 'void 0;' }, sessionId);
expect(await scriptParsed).to.equal(sessionId);
w.webContents.debugger.detach();
});
});
});

View File

@@ -1,11 +1,11 @@
import { screen, desktopCapturer, BrowserWindow } from 'electron/main';
import { expect } from 'chai';
import { describe, it } from 'vitest';
import { once } from 'node:events';
import { setTimeout } from 'node:timers/promises';
import { ifdescribe, ifit } from './lib/spec-helpers';
import { ifit } from './lib/spec-helpers';
import { closeAllWindows } from './lib/window-helpers';
function getSourceTypes(): ('window' | 'screen')[] {
@@ -173,143 +173,7 @@ describe('desktopCapturer', () => {
});
// Linux doesn't return any window sources.
ifit(process.platform !== 'linux')('moveAbove should move the window at the requested place', async function () {
// DesktopCapturer.getSources() is guaranteed to return in the correct
// z-order from foreground to background.
const MAX_WIN = 4;
const wList: BrowserWindow[] = [];
const destroyWindows = () => {
for (const w of wList) {
w.destroy();
}
};
try {
for (let i = 0; i < MAX_WIN; i++) {
const w = new BrowserWindow({ show: false, width: 100, height: 100 });
wList.push(w);
}
expect(wList.length).to.equal(MAX_WIN);
// Show and focus all the windows.
for (const w of wList) {
const wShown = once(w, 'show');
const wFocused = once(w, 'focus');
w.show();
w.focus();
await wShown;
await wFocused;
}
// At this point our windows should be showing from bottom to top.
// DesktopCapturer.getSources() returns sources sorted from foreground to
// background, i.e. top to bottom.
let sources = await desktopCapturer.getSources({
types: ['window'],
thumbnailSize: { width: 0, height: 0 }
});
expect(sources).to.be.an('array').that.is.not.empty();
expect(sources.length).to.gte(MAX_WIN);
// Only keep our windows, they must be in the MAX_WIN first windows.
sources.splice(MAX_WIN, sources.length - MAX_WIN);
expect(sources.length).to.equal(MAX_WIN);
expect(sources.length).to.equal(wList.length);
// Check that the sources and wList are sorted in the reverse order.
// If they're not, skip remaining checks because either focus or
// window placement are not reliable in the running test environment.
const wListReversed = wList.slice().reverse();
const proceed = sources.every((source, index) => source.id === wListReversed[index].getMediaSourceId());
if (!proceed) return;
// Move windows so wList is sorted from foreground to background.
for (const [i, w] of wList.entries()) {
if (i < wList.length - 1) {
const next = wList[wList.length - 1];
w.focus();
w.moveAbove(next.getMediaSourceId());
// Ensure the window has time to move.
await setTimeout(2000);
}
}
sources = await desktopCapturer.getSources({
types: ['window'],
thumbnailSize: { width: 0, height: 0 }
});
sources.splice(MAX_WIN, sources.length);
expect(sources.length).to.equal(MAX_WIN);
expect(sources.length).to.equal(wList.length);
// Check that the sources and wList are sorted in the same order.
for (const [index, source] of sources.entries()) {
const wID = wList[index].getMediaSourceId();
expect(source.id).to.equal(wID);
}
} finally {
destroyWindows();
}
});
// Linux doesn't return any window sources.
ifdescribe(process.platform !== 'linux')('fetchWindowIcons', function () {
// Tests are sequentially dependent
this.bail(true);
let w: BrowserWindow;
let testSource: Electron.DesktopCapturerSource | undefined;
let appIcon: Electron.NativeImage | undefined;
before(async () => {
w = new BrowserWindow({
width: 200,
height: 200,
show: true,
title: 'desktop-capturer-test-window'
});
await w.loadURL('about:blank');
const sources = await desktopCapturer.getSources({
types: ['window'],
fetchWindowIcons: true
});
testSource = sources.find((s) => s.name === 'desktop-capturer-test-window');
appIcon = testSource?.appIcon;
});
after(() => {
if (w) w.destroy();
});
it('should find the test window in the list of captured sources', () => {
expect(testSource, `The ${w.getTitle()} window was not found by desktopCapturer`).to.exist();
});
it('should return a non-null appIcon for the captured window', () => {
expect(appIcon, 'appIcon property is null or undefined').to.exist();
});
it('should return an appIcon that is not an empty image', () => {
expect(appIcon?.isEmpty()).to.be.false();
});
it('should return an appIcon that encodes to a valid PNG data URL', () => {
const url = appIcon?.toDataURL();
expect(url).to.be.a('string');
// This is header 'data:image/png;base64,' length;
expect(url?.length).to.be.greaterThan(22);
expect(url?.startsWith('data:image/png;base64,')).to.be.true();
});
it('should return an appIcon with dimensions greater than 0x0 pixels', () => {
const { width, height } = appIcon?.getSize() || { width: 0, height: 0 };
expect(width).to.be.greaterThan(0);
expect(height).to.be.greaterThan(0);
});
});
// Tests are sequentially dependent; later tests will fail if an earlier one does.
});

View File

@@ -2,6 +2,7 @@ import { nativeImage } from 'electron/common';
import { BaseWindow, BrowserWindow, ImageView } from 'electron/main';
import { expect } from 'chai';
import { afterEach, describe, it } from 'vitest';
import * as path from 'node:path';

View File

@@ -1,14 +1,11 @@
import { inAppPurchase } from 'electron/main';
import { expect } from 'chai';
import { it } from 'vitest';
import { ifdescribe } from './lib/spec-helpers';
describe('inAppPurchase module', function () {
if (process.platform !== 'darwin') return;
this.timeout(3 * 60 * 1000);
ifdescribe(process.platform === 'darwin')('inAppPurchase module', { timeout: 3 * 60 * 1000 }, () => {
it('canMakePayments() returns a boolean', () => {
const canMakePayments = inAppPurchase.canMakePayments();
expect(canMakePayments).to.be.a('boolean');

View File

@@ -1,12 +1,13 @@
import { ipcMain, BrowserWindow } from 'electron/main';
import { expect } from 'chai';
import { afterEach, beforeEach, describe, it } from 'vitest';
import * as cp from 'node:child_process';
import { once } from 'node:events';
import * as path from 'node:path';
import { defer } from './lib/spec-helpers';
import { defer, withDone, dangerouslyIgnoreWebContentsLoadResult } from './lib/spec-helpers';
import { closeAllWindows } from './lib/window-helpers';
describe('ipc main module', () => {
@@ -19,38 +20,44 @@ describe('ipc main module', () => {
ipcMain.removeAllListeners('send-sync-message');
});
it('does not crash when reply is not sent and browser is destroyed', (done) => {
const w = new BrowserWindow({
show: false,
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
});
ipcMain.once('send-sync-message', (event) => {
event.returnValue = null;
done();
});
w.loadFile(path.join(fixtures, 'api', 'send-sync-message.html'));
});
it(
'does not crash when reply is not sent and browser is destroyed',
withDone((done) => {
const w = new BrowserWindow({
show: false,
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
});
ipcMain.once('send-sync-message', (event) => {
event.returnValue = null;
done();
});
dangerouslyIgnoreWebContentsLoadResult(w.loadFile(path.join(fixtures, 'api', 'send-sync-message.html')));
})
);
it('does not crash when reply is sent by multiple listeners', (done) => {
const w = new BrowserWindow({
show: false,
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
});
ipcMain.on('send-sync-message', (event) => {
event.returnValue = null;
});
ipcMain.on('send-sync-message', (event) => {
event.returnValue = null;
done();
});
w.loadFile(path.join(fixtures, 'api', 'send-sync-message.html'));
});
it(
'does not crash when reply is sent by multiple listeners',
withDone((done) => {
const w = new BrowserWindow({
show: false,
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
});
ipcMain.on('send-sync-message', (event) => {
event.returnValue = null;
});
ipcMain.on('send-sync-message', (event) => {
event.returnValue = null;
done();
});
dangerouslyIgnoreWebContentsLoadResult(w.loadFile(path.join(fixtures, 'api', 'send-sync-message.html')));
})
);
});
describe('ipcMain.on', () => {
@@ -85,7 +92,7 @@ describe('ipc main module', () => {
contextIsolation: false
}
});
w.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
const v = await w.webContents.executeJavaScript(`new Promise((resolve, reject) => {
const { ipcRenderer } = require('electron')
ipcRenderer.send('test-echo', 'hello')

View File

@@ -1,6 +1,7 @@
import { ipcMain, BrowserWindow } from 'electron/main';
import { expect } from 'chai';
import { afterAll, beforeAll, describe, it } from 'vitest';
import { once } from 'node:events';
@@ -8,7 +9,7 @@ import { closeWindow } from './lib/window-helpers';
describe('ipcRenderer module', () => {
let w: BrowserWindow;
before(async () => {
beforeAll(async () => {
w = new BrowserWindow({
show: false,
webPreferences: {
@@ -20,7 +21,7 @@ describe('ipcRenderer module', () => {
await w.loadURL('about:blank');
w.webContents.on('console-message', (_event, ...args) => console.error(...args));
});
after(async () => {
afterAll(async () => {
await closeWindow(w);
w = null as unknown as BrowserWindow;
});

View File

@@ -1,12 +1,13 @@
import { BrowserWindow, ipcMain, IpcMainInvokeEvent, MessageChannelMain, WebContents } from 'electron/main';
import { expect } from 'chai';
import { afterAll, afterEach, beforeAll, describe, it } from 'vitest';
import { EventEmitter, once } from 'node:events';
import * as http from 'node:http';
import * as path from 'node:path';
import { defer, listen } from './lib/spec-helpers';
import { defer, listen, dangerouslyIgnoreWebContentsLoadResult } from './lib/spec-helpers';
import { closeAllWindows } from './lib/window-helpers';
const v8Util = process._linkedBinding('electron_common_v8_util');
@@ -16,11 +17,11 @@ describe('ipc module', () => {
describe('invoke', () => {
let w: BrowserWindow;
before(async () => {
beforeAll(async () => {
w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
await w.loadURL('about:blank');
});
after(async () => {
afterAll(async () => {
w.destroy();
});
@@ -147,11 +148,11 @@ describe('ipc module', () => {
describe('ordering', () => {
let w: BrowserWindow;
before(async () => {
beforeAll(async () => {
w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
await w.loadURL('about:blank');
});
after(async () => {
afterAll(async () => {
w.destroy();
});
@@ -246,7 +247,7 @@ describe('ipc module', () => {
it('can send a port to the main process', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
w.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
const p = once(ipcMain, 'port');
await w.webContents.executeJavaScript(
`(${function () {
@@ -265,7 +266,7 @@ describe('ipc module', () => {
it('can sent a message without a transfer', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
w.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
const p = once(ipcMain, 'port');
await w.webContents.executeJavaScript(
`(${function () {
@@ -280,7 +281,7 @@ describe('ipc module', () => {
it('throws when the transferable is invalid', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
w.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
const p = once(ipcMain, 'port');
await w.webContents.executeJavaScript(
`(${function () {
@@ -298,7 +299,7 @@ describe('ipc module', () => {
it('can communicate between main and renderer', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
w.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
const p = once(ipcMain, 'port');
await w.webContents.executeJavaScript(
`(${function () {
@@ -321,7 +322,7 @@ describe('ipc module', () => {
it('can receive a port from a renderer over a MessagePort connection', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
w.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
function fn() {
const channel1 = new MessageChannel();
const channel2 = new MessageChannel();
@@ -349,8 +350,8 @@ describe('ipc module', () => {
it('can forward a port from one renderer to another renderer', async () => {
const w1 = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
const w2 = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
w1.loadURL('about:blank');
w2.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(w1.loadURL('about:blank'));
dangerouslyIgnoreWebContentsLoadResult(w2.loadURL('about:blank'));
w1.webContents.executeJavaScript(
`(${function () {
const channel = new MessageChannel();
@@ -384,7 +385,7 @@ describe('ipc module', () => {
show: false,
webPreferences: { nodeIntegration: true, contextIsolation: false }
});
w.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
await w.webContents.executeJavaScript(
`(${function () {
const { ipcRenderer } = require('electron');
@@ -408,7 +409,7 @@ describe('ipc module', () => {
show: false,
webPreferences: { nodeIntegration: true, contextIsolation: false }
});
w.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
await w.webContents.executeJavaScript(
`(${async function () {
const { port2 } = new MessageChannel();
@@ -427,7 +428,7 @@ describe('ipc module', () => {
show: false,
webPreferences: { nodeIntegration: true, contextIsolation: false }
});
w.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
ipcMain.once('do-a-gc', () => v8Util.requestGarbageCollectionForTesting());
await w.webContents.executeJavaScript(
`(${async function () {
@@ -570,7 +571,7 @@ describe('ipc module', () => {
show: false,
webPreferences: { nodeIntegration: true, contextIsolation: false }
});
w.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
await w.webContents.executeJavaScript(
`(${function () {
const { ipcRenderer } = require('electron');
@@ -593,7 +594,7 @@ describe('ipc module', () => {
show: false,
webPreferences: { nodeIntegration: true, contextIsolation: false }
});
w.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
await w.webContents.executeJavaScript(
`(${function () {
const { ipcRenderer } = require('electron');
@@ -695,7 +696,7 @@ describe('ipc module', () => {
show: false,
webPreferences: { nodeIntegration: true, contextIsolation: false }
});
w.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
await w.webContents.executeJavaScript(
`(${function () {
const { ipcRenderer } = require('electron');
@@ -797,7 +798,7 @@ describe('ipc module', () => {
it('receives ipc messages sent from the WebContents', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
w.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
w.webContents.executeJavaScript("require('electron').ipcRenderer.send('test', 42)");
const [, num] = await once(w.webContents.ipc, 'test');
expect(num).to.equal(42);
@@ -805,7 +806,7 @@ describe('ipc module', () => {
it('receives sync-ipc messages sent from the WebContents', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
w.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
w.webContents.ipc.on('test', (event, arg) => {
event.returnValue = arg * 2;
});
@@ -815,7 +816,7 @@ describe('ipc module', () => {
it('receives postMessage messages sent from the WebContents, w/ MessagePorts', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
w.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
w.webContents.executeJavaScript(
"require('electron').ipcRenderer.postMessage('test', null, [(new MessageChannel).port1])"
);
@@ -825,7 +826,7 @@ describe('ipc module', () => {
it('handles invoke messages sent from the WebContents', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
w.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
w.webContents.ipc.handle('test', (_event, arg) => arg * 2);
const result = await w.webContents.executeJavaScript("require('electron').ipcRenderer.invoke('test', 42)");
expect(result).to.equal(42 * 2);
@@ -833,7 +834,7 @@ describe('ipc module', () => {
it('cascades to ipcMain', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
w.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
let gotFromIpcMain = false;
const ipcMainReceived = new Promise<void>((resolve) =>
ipcMain.on('test', () => {
@@ -856,7 +857,7 @@ describe('ipc module', () => {
it('overrides ipcMain handlers', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
w.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
w.webContents.ipc.handle('test', (_event, arg) => arg * 2);
ipcMain.handle('test', () => {
throw new Error('should not be called');
@@ -868,7 +869,7 @@ describe('ipc module', () => {
it('falls back to ipcMain handlers', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
w.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
ipcMain.handle('test', (_event, arg) => {
return arg * 2;
});
@@ -905,7 +906,7 @@ describe('ipc module', () => {
afterEach(closeAllWindows);
it('responds to ipc messages in the main frame', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
w.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
w.webContents.executeJavaScript("require('electron').ipcRenderer.send('test', 42)");
const [, arg] = await once(w.webContents.mainFrame.ipc, 'test');
expect(arg).to.equal(42);
@@ -913,7 +914,7 @@ describe('ipc module', () => {
it('responds to sync ipc messages in the main frame', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
w.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
w.webContents.mainFrame.ipc.on('test', (event, arg) => {
event.returnValue = arg * 2;
});
@@ -923,7 +924,7 @@ describe('ipc module', () => {
it('receives postMessage messages sent from the WebContents, w/ MessagePorts', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
w.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
w.webContents.executeJavaScript(
"require('electron').ipcRenderer.postMessage('test', null, [(new MessageChannel).port1])"
);
@@ -933,7 +934,7 @@ describe('ipc module', () => {
it('handles invoke messages sent from the WebContents', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
w.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
w.webContents.mainFrame.ipc.handle('test', (_event, arg) => arg * 2);
const result = await w.webContents.executeJavaScript("require('electron').ipcRenderer.invoke('test', 42)");
expect(result).to.equal(42 * 2);
@@ -941,7 +942,7 @@ describe('ipc module', () => {
it('cascades to WebContents and ipcMain', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
w.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
let gotFromIpcMain = false;
let gotFromWebContents = false;
const ipcMainReceived = new Promise<void>((resolve) =>
@@ -972,7 +973,7 @@ describe('ipc module', () => {
it('overrides ipcMain handlers', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
w.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
w.webContents.mainFrame.ipc.handle('test', (_event, arg) => arg * 2);
ipcMain.handle('test', () => {
throw new Error('should not be called');
@@ -984,7 +985,7 @@ describe('ipc module', () => {
it('overrides WebContents handlers', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
w.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
w.webContents.ipc.handle('test', () => {
throw new Error('should not be called');
});
@@ -999,7 +1000,7 @@ describe('ipc module', () => {
it('falls back to WebContents handlers', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
w.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
w.webContents.ipc.handle('test', (_event, arg) => {
return arg * 2;
});
@@ -1048,7 +1049,7 @@ describe('ipc module', () => {
"window.onunload = () => require('electron').ipcRenderer.send('unload'); void 0"
);
const onUnloadIpc = once(w.webContents.mainFrame.ipc, 'unload');
w.loadURL(`http://127.0.0.1:${port}`); // cross-origin navigation
dangerouslyIgnoreWebContentsLoadResult(w.loadURL(`http://127.0.0.1:${port}`)); // cross-origin navigation
const [{ senderFrame }] = await onUnloadIpc;
expect(senderFrame.detached).to.be.true();
});

View File

@@ -1,6 +1,7 @@
import { BrowserWindow, session, desktopCapturer } from 'electron/main';
import { expect } from 'chai';
import { afterAll, afterEach, beforeAll, describe, it } from 'vitest';
import * as http from 'node:http';
@@ -13,20 +14,20 @@ describe('setDisplayMediaRequestHandler', () => {
// requires a secure context.
let server: http.Server;
let serverUrl: string;
before(async () => {
beforeAll(async () => {
server = http.createServer((req, res) => {
res.setHeader('Content-Type', 'text/html');
res.end('');
});
serverUrl = (await listen(server)).url;
});
after(() => {
afterAll(() => {
server.close();
});
ifit(process.platform !== 'darwin')('works when calling getDisplayMedia', async function () {
ifit(process.platform !== 'darwin')('works when calling getDisplayMedia', async (ctx) => {
if ((await desktopCapturer.getSources({ types: ['screen'] })).length === 0) {
return this.skip();
return ctx.skip();
}
const ses = session.fromPartition('' + Math.random());
let requestHandlerCalled = false;

View File

@@ -1,9 +1,10 @@
import { BrowserWindow, app, Menu, MenuItem, MenuItemConstructorOptions } from 'electron/main';
import { expect } from 'chai';
import { afterEach, beforeEach, describe, it } from 'vitest';
import { roleList, execute } from '../lib/browser/api/menu-item-roles';
import { ifit, ifdescribe } from './lib/spec-helpers';
import { ifit, ifdescribe, withDone } from './lib/spec-helpers';
import { closeAllWindows } from './lib/window-helpers';
function keys<Key extends string, Value>(record: Record<Key, Value>) {
@@ -108,23 +109,26 @@ describe('MenuItems', () => {
});
describe('MenuItem.click', () => {
it('should be called with the item object passed', (done) => {
const menu = Menu.buildFromTemplate([
{
label: 'text',
click: (item) => {
try {
expect(item.constructor.name).to.equal('MenuItem');
expect(item.label).to.equal('text');
done();
} catch (e) {
done(e);
it(
'should be called with the item object passed',
withDone((done) => {
const menu = Menu.buildFromTemplate([
{
label: 'text',
click: (item) => {
try {
expect(item.constructor.name).to.equal('MenuItem');
expect(item.label).to.equal('text');
done();
} catch (e) {
done(e);
}
}
}
}
]);
menu._executeCommand({}, menu.items[0].commandId);
});
]);
menu._executeCommand({}, menu.items[0].commandId);
})
);
});
describe('MenuItem with checked/radio property', () => {

View File

@@ -1,6 +1,7 @@
import { BrowserWindow, Menu, MenuItem } from 'electron/main';
import { assert, expect } from 'chai';
import { afterEach, beforeEach, describe, it } from 'vitest';
import * as cp from 'node:child_process';
import { once } from 'node:events';
@@ -9,7 +10,7 @@ import { setTimeout } from 'node:timers/promises';
import { sortMenuItems } from '../lib/browser/api/menu-utils';
import { singleModifierCombinations } from './lib/accelerator-helpers';
import { ifit } from './lib/spec-helpers';
import { ifit, withDone } from './lib/spec-helpers';
import { closeWindow } from './lib/window-helpers';
const fixturesPath = path.resolve(__dirname, 'fixtures');
@@ -848,23 +849,29 @@ describe('Menu module', function () {
expect(y).to.equal(101);
});
it('works with a given BrowserWindow, options and callback', (done) => {
const { x, y } = menu.popup({
window: w,
x: 100,
y: 101,
callback: () => done()
}) as unknown as { x: number; y: number };
it(
'works with a given BrowserWindow, options and callback',
withDone((done) => {
const { x, y } = menu.popup({
window: w,
x: 100,
y: 101,
callback: () => done()
}) as unknown as { x: number; y: number };
expect(x).to.equal(100);
expect(y).to.equal(101);
menu.closePopup();
});
expect(x).to.equal(100);
expect(y).to.equal(101);
menu.closePopup();
})
);
it('works with a given BrowserWindow, no options, and a callback', (done) => {
menu.popup({ window: w, callback: () => done() });
menu.closePopup();
});
it(
'works with a given BrowserWindow, no options, and a callback',
withDone((done) => {
menu.popup({ window: w, callback: () => done() });
menu.closePopup();
})
);
it('prevents menu from getting garbage-collected when popuping', async () => {
const menu = Menu.buildFromTemplate([{ role: 'paste' }]);
@@ -893,44 +900,47 @@ describe('Menu module', function () {
// https://github.com/electron/electron/issues/35724
// Maximizing window is enough to trigger the bug
// FIXME(dsanders11): Test always passes on CI, even pre-fix
ifit(process.platform === 'linux' && !process.env.CI)('does not trigger issue #35724', (done) => {
const showAndCloseMenu = async () => {
await setTimeout(1000);
menu.popup({ window: w, x: 50, y: 50 });
await setTimeout(500);
const closed = once(menu, 'menu-will-close');
menu.closePopup();
await closed;
};
ifit(process.platform === 'linux' && !process.env.CI)(
'does not trigger issue #35724',
withDone((done) => {
const showAndCloseMenu = async () => {
await setTimeout(1000);
menu.popup({ window: w, x: 50, y: 50 });
await setTimeout(500);
const closed = once(menu, 'menu-will-close');
menu.closePopup();
await closed;
};
const failOnEvent = () => {
done(new Error('Menu closed prematurely'));
};
const failOnEvent = () => {
done(new Error('Menu closed prematurely'));
};
assert(!w.isVisible());
w.on('show', async () => {
assert(!w.isMaximized());
// Show the menu once, then maximize window
await showAndCloseMenu();
// NOTE - 'maximize' event never fires on CI for Linux
const maximized = once(w, 'maximize');
w.maximize();
await maximized;
assert(!w.isVisible());
w.on('show', async () => {
assert(!w.isMaximized());
// Show the menu once, then maximize window
await showAndCloseMenu();
// NOTE - 'maximize' event never fires on CI for Linux
const maximized = once(w, 'maximize');
w.maximize();
await maximized;
// Bug only seems to trigger programmatically after showing the menu once more
await showAndCloseMenu();
// Bug only seems to trigger programmatically after showing the menu once more
await showAndCloseMenu();
// Now ensure the menu stays open until we close it
await setTimeout(500);
menu.once('menu-will-close', failOnEvent);
menu.popup({ window: w, x: 50, y: 50 });
await setTimeout(1500);
menu.off('menu-will-close', failOnEvent);
menu.once('menu-will-close', () => done());
menu.closePopup();
});
w.show();
});
// Now ensure the menu stays open until we close it
await setTimeout(500);
menu.once('menu-will-close', failOnEvent);
menu.popup({ window: w, x: 50, y: 50 });
await setTimeout(1500);
menu.off('menu-will-close', failOnEvent);
menu.once('menu-will-close', () => done());
menu.closePopup();
});
w.show();
})
);
const chunkSize = 10;
let chunkCount = 0;

View File

@@ -1,9 +1,10 @@
import { nativeImage } from 'electron/common';
import { expect } from 'chai';
import { describe, it } from 'vitest';
import * as path from 'node:path';
import { expect } from './lib/remote-tools';
import { ifdescribe, ifit, itremote, useRemoteContext } from './lib/spec-helpers';
import { expectDeprecationMessages } from './lib/warning-helpers';

View File

@@ -1,6 +1,7 @@
import { nativeTheme, BrowserWindow, ipcMain } from 'electron/main';
import { expect } from 'chai';
import { afterEach, describe, it } from 'vitest';
import { once } from 'node:events';
import * as path from 'node:path';

View File

@@ -1,6 +1,7 @@
import { net, protocol } from 'electron/main';
import { expect } from 'chai';
import { describe, it } from 'vitest';
import * as path from 'node:path';
import * as url from 'node:url';

View File

@@ -1,6 +1,7 @@
import { session, net } from 'electron/main';
import { expect } from 'chai';
import { afterAll, afterEach, beforeAll, beforeEach, describe, it } from 'vitest';
import * as ChildProcess from 'node:child_process';
import { once } from 'node:events';
@@ -10,7 +11,7 @@ import { Socket } from 'node:net';
import * as os from 'node:os';
import * as path from 'node:path';
import { ifit, listen } from './lib/spec-helpers';
import { ifit, listen, withDone } from './lib/spec-helpers';
const appPath = path.join(__dirname, 'fixtures', 'api', 'net-log');
const dumpFile = path.join(os.tmpdir(), 'net_log.json');
@@ -23,7 +24,7 @@ describe('netLog module', () => {
let serverUrl: string;
const connections: Set<Socket> = new Set();
before(async () => {
beforeAll(async () => {
server = http.createServer();
server.on('connection', (connection) => {
connections.add(connection);
@@ -37,15 +38,17 @@ describe('netLog module', () => {
serverUrl = (await listen(server)).url;
});
after((done) => {
for (const connection of connections) {
connection.destroy();
}
server.close(() => {
server = null as any;
done();
});
});
afterAll(
withDone((done) => {
for (const connection of connections) {
connection.destroy();
}
server.close(() => {
server = null as any;
done();
});
})
);
beforeEach(() => {
expect(testNetLog().currentlyLogging).to.be.false('currently logging');

View File

@@ -1,6 +1,7 @@
import { net, session, BrowserWindow, ClientRequestConstructorOptions } from 'electron/main';
import { expect } from 'chai';
import { afterEach, beforeEach, describe, it } from 'vitest';
import * as dns from 'node:dns';
@@ -14,14 +15,12 @@ describe('net module (session)', () => {
beforeEach(() => {
respondNTimes.routeFailure = false;
});
afterEach(async function () {
afterEach(async (ctx) => {
await session.defaultSession.clearCache();
if (respondNTimes.routeFailure && this.test) {
if (!this.test.isFailed()) {
throw new Error(
'Failing this test due an unhandled error in the respondOnce route handler, check the logs above for the actual error'
);
}
if (respondNTimes.routeFailure && ctx.task.result?.state !== 'fail') {
throw new Error(
'Failing this test due an unhandled error in the respondOnce route handler, check the logs above for the actual error'
);
}
});

View File

@@ -1,50 +1,75 @@
import { net, session, ClientRequest, ClientRequestConstructorOptions, utilityProcess } from 'electron/main';
import { ClientRequest, ClientRequestConstructorOptions, utilityProcess } from 'electron/main';
import { expect } from 'chai';
import { afterAll, afterEach, beforeAll, beforeEach, describe, it, type TestFunction } from 'vitest';
import { once } from 'node:events';
import * as fs from 'node:fs';
import * as http from 'node:http';
import * as http2 from 'node:http2';
import * as path from 'node:path';
import { setTimeout } from 'node:timers/promises';
import {
collectStreamBody,
collectStreamBodyBuffer,
defer,
expect,
getResponse,
http,
kOneKiloByte,
kOneMegaByte,
net,
once,
randomBuffer,
randomString,
respondNTimes,
respondOnce
} from './lib/net-helpers';
import { listen, defer, ifdescribe, isTestingBindingAvailable } from './lib/spec-helpers';
respondOnce,
rewriteForRemoteEval,
session,
setTimeout
} from './lib/remote-tools';
import { listen, ifdescribe, isTestingBindingAvailable } from './lib/spec-helpers';
const utilityFixturePath = path.resolve(__dirname, 'fixtures', 'api', 'utility-process', 'api-net-spec.js');
const fixturesPath = path.resolve(__dirname, 'fixtures');
let sharedUtilityChild: Electron.UtilityProcess | null = null;
async function getUtilityChild() {
if (sharedUtilityChild) return sharedUtilityChild;
const child = utilityProcess.fork(utilityFixturePath, [], { execArgv: ['--expose-gc'] });
const [ready] = await once(child, 'message');
expect(ready?.type).to.equal('ready');
child.once('exit', (code) => {
if (sharedUtilityChild === child && code !== 0) {
console.error(`api-net utility process exited unexpectedly with code ${code}`);
}
if (sharedUtilityChild === child) sharedUtilityChild = null;
});
sharedUtilityChild = child;
return child;
}
async function closeUtilityChild() {
const child = sharedUtilityChild;
if (!child) return;
sharedUtilityChild = null;
const exited = once(child, 'exit');
child.postMessage({ type: 'shutdown' });
await exited;
}
/** @remote */
async function itUtility(name: string, fn?: Function, args?: { [key: string]: any }) {
it(`${name} in utility process`, async () => {
const child = utilityProcess.fork(utilityFixturePath, [], {
execArgv: ['--expose-gc']
});
if (fn) {
child.postMessage({ fn: `(${fn})()`, args });
} else {
child.postMessage({ fn: '(() => {})()', args });
}
const [data] = await once(child, 'message');
const child = await getUtilityChild();
const body = fn ? `(${rewriteForRemoteEval(fn)})()` : '(() => {})()';
const result = once(child, 'message');
child.postMessage({ fn: body, args });
const [data] = await result;
expect(data.ok).to.be.true(data.message);
// Cleanup.
const [code] = await once(child, 'exit');
expect(code).to.equal(0);
});
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async function itIgnoringArgs(name: string, fn?: Mocha.Func | Mocha.AsyncFunc, args?: { [key: string]: any }) {
async function itIgnoringArgs(name: string, fn?: TestFunction, args?: { [key: string]: any }) {
it(name, fn);
}
@@ -52,13 +77,11 @@ describe('net module', () => {
beforeEach(() => {
respondNTimes.routeFailure = false;
});
afterEach(async function () {
if (respondNTimes.routeFailure && this.test) {
if (!this.test.isFailed()) {
throw new Error(
'Failing this test due an unhandled error in the respondOnce route handler, check the logs above for the actual error'
);
}
afterEach(async (ctx) => {
if (respondNTimes.routeFailure && ctx.task.result?.state !== 'fail') {
throw new Error(
'Failing this test due an unhandled error in the respondOnce route handler, check the logs above for the actual error'
);
}
});
@@ -87,14 +110,16 @@ describe('net module', () => {
}
);
before(async () => {
beforeAll(async () => {
http2URL = (await listen(h2server)).url + '/';
});
after(() => {
afterAll(async () => {
h2server.close();
await closeUtilityChild();
});
/** @remote — `test` may be itUtility, which stringifies its closure */
for (const test of [itIgnoringArgs, itUtility]) {
describe('HTTP basics', () => {
test('should be able to issue a basic GET request', async () => {

View File

@@ -2,7 +2,7 @@
// with the session bus. This requires python-dbusmock to be installed and
// running at $DBUS_SESSION_BUS_ADDRESS.
//
// script/spec-runner.js spawns dbusmock, which sets DBUS_SESSION_BUS_ADDRESS.
// spec/_vitest_runner/run.js spawns dbusmock, which sets DBUS_SESSION_BUS_ADDRESS.
//
// See https://pypi.python.org/pypi/python-dbusmock to read about dbusmock.
@@ -11,12 +11,13 @@ import { app } from 'electron/main';
import { expect } from 'chai';
import * as dbus from 'dbus-native';
import { afterAll, beforeAll, describe, it } from 'vitest';
import { once } from 'node:events';
import * as path from 'node:path';
import { promisify } from 'node:util';
import { ifdescribe } from './lib/spec-helpers';
import { ifdescribe, withDone } from './lib/spec-helpers';
const fixturesPath = path.join(__dirname, 'fixtures');
@@ -33,7 +34,7 @@ ifdescribe(!skip)('Notification module (dbus)', () => {
const appName = 'api-notification-dbus-spec';
const serviceName = 'org.freedesktop.Notifications';
before(async () => {
beforeAll(async () => {
// init app
app.name = appName;
app.setDesktopName(`${appName}.desktop`);
@@ -63,7 +64,7 @@ ifdescribe(!skip)('Notification module (dbus)', () => {
);
});
after(async () => {
afterAll(async () => {
// cleanup dbus
if (reset) await reset();
// cleanup app
@@ -106,21 +107,23 @@ ifdescribe(!skip)('Notification module (dbus)', () => {
};
}
before((done) => {
mock.on('MethodCalled', onMethodCalled(done));
// lazy load Notification after we listen to MethodCalled mock signal
Notification = require('electron').Notification;
const n = new Notification({
title: 'title',
subtitle: 'subtitle',
body: 'body',
icon: nativeImage.createFromPath(path.join(fixturesPath, 'assets', 'notification_icon.png')),
replyPlaceholder: 'replyPlaceholder',
sound: 'sound',
closeButtonText: 'closeButtonText'
});
n.show();
});
beforeAll(
withDone((done) => {
mock.on('MethodCalled', onMethodCalled(done));
// lazy load Notification after we listen to MethodCalled mock signal
Notification = require('electron').Notification;
const n = new Notification({
title: 'title',
subtitle: 'subtitle',
body: 'body',
icon: nativeImage.createFromPath(path.join(fixturesPath, 'assets', 'notification_icon.png')),
replyPlaceholder: 'replyPlaceholder',
sound: 'sound',
closeButtonText: 'closeButtonText'
});
n.show();
})
);
it(`should call ${serviceName} to show notifications`, async () => {
const calls = await getCalls();

View File

@@ -1,6 +1,7 @@
import { Notification } from 'electron/main';
import { expect } from 'chai';
import { describe, it } from 'vitest';
import { once } from 'node:events';
@@ -282,63 +283,6 @@ describe('Notification module', () => {
expect(n.toastXml).to.equal('<xml/>');
});
ifit(process.platform === 'darwin')('emits show and close events', async () => {
const n = new Notification({
title: 'test notification',
body: 'test body',
silent: true
});
{
const e = once(n, 'show');
n.show();
await e;
}
{
const e = once(n, 'close');
n.close();
await e;
}
});
ifit(process.platform === 'darwin')('emits show and close events with custom id', async () => {
const n = new Notification({
id: 'test-custom-id',
title: 'test notification',
body: 'test body',
silent: true
});
{
const e = once(n, 'show');
n.show();
await e;
}
{
const e = once(n, 'close');
n.close();
await e;
}
});
ifit(process.platform === 'darwin')('emits show and close events with custom id and groupId', async () => {
const n = new Notification({
id: 'E017VKL2N8H|C07RBMNS9EK|1772656675.039',
groupId: 'E017VKL2N8H|C07RBMNS9EK',
title: 'test notification',
body: 'test body',
silent: true
});
{
const e = once(n, 'show');
n.show();
await e;
}
{
const e = once(n, 'close');
n.close();
await e;
}
});
ifit(process.platform === 'win32')('can show notification with custom id and groupId', () => {
const n = new Notification({
id: 'test-custom-id',

View File

@@ -1,19 +1,20 @@
// For these tests we use a fake DBus daemon to verify powerMonitor module
// interaction with the system bus. This requires python-dbusmock installed and
// running (with the DBUS_SYSTEM_BUS_ADDRESS environment variable set).
// script/spec-runner.js will take care of spawning the fake DBus daemon and setting
// spec/_vitest_runner/run.js will take care of spawning the fake DBus daemon and setting
// DBUS_SYSTEM_BUS_ADDRESS when python-dbusmock is installed.
//
// See https://pypi.python.org/pypi/python-dbusmock for more information about
// python-dbusmock.
import { expect } from 'chai';
import * as dbus from 'dbus-native';
import { afterAll, beforeAll, describe, it } from 'vitest';
import { once } from 'node:events';
import { setTimeout } from 'node:timers/promises';
import { promisify } from 'node:util';
import { ifdescribe, startRemoteControlApp } from './lib/spec-helpers';
import { ifdescribe, startRemoteControlApp, withDone } from './lib/spec-helpers';
describe('powerMonitor', () => {
let logindMock: any, dbusMockPowerMonitor: any, getCalls: any, emitSignal: any, reset: any;
@@ -21,7 +22,7 @@ describe('powerMonitor', () => {
ifdescribe(process.platform === 'linux' && process.env.DBUS_SYSTEM_BUS_ADDRESS != null)(
'when powerMonitor module is loaded with dbus mock',
() => {
before(async () => {
beforeAll(async () => {
const systemBus = dbus.systemBus();
const loginService = systemBus.getService('org.freedesktop.login1');
const getInterface = promisify(loginService.getInterface.bind(loginService));
@@ -31,7 +32,7 @@ describe('powerMonitor', () => {
reset = promisify(logindMock.Reset.bind(logindMock));
});
after(async () => {
afterAll(async () => {
await reset();
});
@@ -43,11 +44,13 @@ describe('powerMonitor', () => {
return cb;
}
before((done) => {
logindMock.on('MethodCalled', onceMethodCalled(done));
// lazy load powerMonitor after we listen to MethodCalled mock signal
dbusMockPowerMonitor = require('electron').powerMonitor;
});
beforeAll(
withDone((done) => {
logindMock.on('MethodCalled', onceMethodCalled(done));
// lazy load powerMonitor after we listen to MethodCalled mock signal
dbusMockPowerMonitor = require('electron').powerMonitor;
})
);
it('should call Inhibit to delay suspend once a listener is added', async () => {
// No calls to dbus until a listener is added
@@ -113,7 +116,7 @@ describe('powerMonitor', () => {
});
describe('when a listener is added to shutdown event', () => {
before(async () => {
beforeAll(async () => {
const calls = await getCalls();
expect(calls).to.be.an('array').that.has.lengthOf(2);
dbusMockPowerMonitor.once('shutdown', () => {});
@@ -134,12 +137,15 @@ describe('powerMonitor', () => {
});
describe('when PrepareForShutdown(true) signal is sent by logind', () => {
it('should emit "shutdown" event', (done) => {
dbusMockPowerMonitor.once('shutdown', () => {
done();
});
emitSignal('org.freedesktop.login1.Manager', 'PrepareForShutdown', 'b', [['b', true]]);
});
it(
'should emit "shutdown" event',
withDone((done) => {
dbusMockPowerMonitor.once('shutdown', () => {
done();
});
emitSignal('org.freedesktop.login1.Manager', 'PrepareForShutdown', 'b', [['b', true]]);
})
);
});
});
}
@@ -154,7 +160,7 @@ describe('powerMonitor', () => {
describe('when powerMonitor module is loaded', () => {
let powerMonitor: typeof Electron.powerMonitor;
before(() => {
beforeAll(() => {
powerMonitor = require('electron').powerMonitor;
});
describe('powerMonitor.getSystemIdleState', () => {

View File

@@ -1,6 +1,7 @@
import { powerSaveBlocker } from 'electron/main';
import { expect } from 'chai';
import { describe, it } from 'vitest';
describe('powerSaveBlocker module', () => {
it('can be started and stopped', () => {

View File

@@ -2,6 +2,7 @@ import { BrowserWindow } from 'electron';
import { app } from 'electron/main';
import { expect } from 'chai';
import { afterAll, beforeAll, describe, it } from 'vitest';
import * as fs from 'node:fs';
import * as path from 'node:path';
@@ -108,11 +109,11 @@ describe('process module', () => {
describe('renderer process', () => {
let w: BrowserWindow;
before(async () => {
beforeAll(async () => {
w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
await w.loadURL('about:blank');
});
after(closeAllWindows);
afterAll(closeAllWindows);
generateSpecs((fn, ...args) => {
const jsonArgs = args.map((value) => JSON.stringify(value)).join(',');

View File

@@ -2,6 +2,7 @@ import { protocol, webContents, WebContents, session, BrowserWindow, ipcMain, ne
import { expect } from 'chai';
import { v4 } from 'uuid';
import { afterAll, afterEach, beforeAll, beforeEach, describe, it } from 'vitest';
import * as ChildProcess from 'node:child_process';
import { EventEmitter, once } from 'node:events';
@@ -16,7 +17,7 @@ import { setTimeout } from 'node:timers/promises';
import * as url from 'node:url';
import { collectStreamBody, getResponse } from './lib/net-helpers';
import { listen, defer, ifit } from './lib/spec-helpers';
import { listen, defer, ifit, withDone, dangerouslyIgnoreWebContentsLoadResult } from './lib/spec-helpers';
import { WebmGenerator } from './lib/video-helpers';
import { closeAllWindows, closeWindow } from './lib/window-helpers';
@@ -83,10 +84,10 @@ function deferPromise(): Promise<any> & { resolve: Function; reject: Function }
describe('protocol module', () => {
let contents: WebContents;
// NB. sandbox: true is used because it makes navigations much (~8x) faster.
before(() => {
beforeAll(() => {
contents = (webContents as typeof ElectronInternal.WebContents).create({ sandbox: true });
});
after(() => contents.destroy());
afterAll(() => contents.destroy());
async function ajax(url: string, options = {}) {
// Note that we need to do navigation every time after a protocol is
@@ -289,7 +290,9 @@ describe('protocol module', () => {
});
const loaded = once(ipcMain, 'loaded-iframe-custom-protocol');
w.loadFile(path.join(__dirname, 'fixtures', 'pages', 'iframe-protocol.html'));
dangerouslyIgnoreWebContentsLoadResult(
w.loadFile(path.join(__dirname, 'fixtures', 'pages', 'iframe-protocol.html'))
);
await loaded;
});
@@ -356,7 +359,7 @@ describe('protocol module', () => {
res.end(text);
}
});
after(() => server.close());
defer(() => server.close());
const { port } = await listen(server);
const url = `${protocolName}://fake-host`;
const redirectURL = `http://127.0.0.1:${port}/serverRedirect`;
@@ -366,17 +369,20 @@ describe('protocol module', () => {
expect(r.data).to.equal(text);
});
it('can access request headers', (done) => {
protocol.registerHttpProtocol(protocolName, (request) => {
try {
expect(request).to.have.property('headers');
done();
} catch (e) {
done(e);
}
});
ajax(protocolName + '://fake-host').catch(() => {});
});
it(
'can access request headers',
withDone((done) => {
protocol.registerHttpProtocol(protocolName, (request) => {
try {
expect(request).to.have.property('headers');
done();
} catch (e) {
done(e);
}
});
ajax(protocolName + '://fake-host').catch(() => {});
})
);
});
}
@@ -644,7 +650,7 @@ describe('protocol module', () => {
// FIXME(zcbenz): This test was passing because the test itself was wrong,
// I don't know whether it ever passed before and we should take a look at
// it in future.
xit('can send POST request', async () => {
it.skip('can send POST request', async () => {
const server = http.createServer((req, res) => {
let body = '';
req.on('data', (chunk) => {
@@ -655,7 +661,7 @@ describe('protocol module', () => {
});
server.close();
});
after(() => server.close());
defer(() => server.close());
const { url } = await listen(server);
interceptHttpProtocol('http', (request, callback) => {
const data: Electron.ProtocolResponse = {
@@ -679,7 +685,7 @@ describe('protocol module', () => {
expect(details.url).to.equal('http://fake-host/');
callback({ cancel: true });
});
after(() => customSession.webRequest.onBeforeRequest(null));
defer(() => customSession.webRequest.onBeforeRequest(null));
interceptHttpProtocol('http', (request, callback) => {
callback({
@@ -690,17 +696,20 @@ describe('protocol module', () => {
await expect(ajax('http://fake-host')).to.be.eventually.rejectedWith(Error);
});
it('can access request headers', (done) => {
protocol.interceptHttpProtocol('http', (request) => {
try {
expect(request).to.have.property('headers');
done();
} catch (e) {
done(e);
}
});
ajax('http://fake-host').catch(() => {});
});
it(
'can access request headers',
withDone((done) => {
protocol.interceptHttpProtocol('http', (request) => {
try {
expect(request).to.have.property('headers');
done();
} catch (e) {
done(e);
}
});
ajax('http://fake-host').catch(() => {});
})
);
});
describe('protocol.interceptStreamProtocol', () => {
@@ -812,7 +821,7 @@ describe('protocol module', () => {
});
}
});
after(() => protocol.unregisterProtocol(serviceWorkerScheme));
afterAll(() => protocol.unregisterProtocol(serviceWorkerScheme));
it('should fail when registering invalid service worker', async () => {
await contents.loadURL(`${serviceWorkerScheme}://${v4()}.com`);
@@ -889,21 +898,27 @@ describe('protocol module', () => {
await requestReceived;
});
it('can access files through the FileSystem API', (done) => {
const filePath = path.join(fixturesPath, 'pages', 'filesystem.html');
protocol.registerFileProtocol(standardScheme, (request, callback) => callback({ path: filePath }));
w.loadURL(origin);
ipcMain.once('file-system-error', (event, err) => done(err));
ipcMain.once('file-system-write-end', () => done());
});
it(
'can access files through the FileSystem API',
withDone((done) => {
const filePath = path.join(fixturesPath, 'pages', 'filesystem.html');
protocol.registerFileProtocol(standardScheme, (request, callback) => callback({ path: filePath }));
dangerouslyIgnoreWebContentsLoadResult(w.loadURL(origin));
ipcMain.once('file-system-error', (event, err) => done(err));
ipcMain.once('file-system-write-end', () => done());
})
);
it('registers secure, when {secure: true}', (done) => {
const filePath = path.join(fixturesPath, 'pages', 'cache-storage.html');
ipcMain.once('success', () => done());
ipcMain.once('failure', (event, err) => done(err));
protocol.registerFileProtocol(standardScheme, (request, callback) => callback({ path: filePath }));
w.loadURL(origin);
});
it(
'registers secure, when {secure: true}',
withDone((done) => {
const filePath = path.join(fixturesPath, 'pages', 'cache-storage.html');
ipcMain.once('success', () => done());
ipcMain.once('failure', (event, err) => done(err));
protocol.registerFileProtocol(standardScheme, (request, callback) => callback({ path: filePath }));
dangerouslyIgnoreWebContentsLoadResult(w.loadURL(origin));
})
);
});
describe('protocol.registerSchemesAsPrivileged cors-fetch', function () {
@@ -996,7 +1011,7 @@ describe('protocol module', () => {
const consoleMessages: string[] = [];
newContents.on('console-message', (e) => consoleMessages.push(e.message));
try {
newContents.loadURL(standardScheme + '://fake-host');
dangerouslyIgnoreWebContentsLoadResult(newContents.loadURL(standardScheme + '://fake-host'));
const [, response] = await once(ipcMain, 'response');
expect(response).to.deep.equal(expected);
expect(consoleMessages.join('\n')).to.match(expectedConsole);
@@ -1016,7 +1031,7 @@ describe('protocol module', () => {
const videoPath = path.join(fixturesPath, 'video.webm');
let w: BrowserWindow;
before(async () => {
beforeAll(async () => {
// generate test video
const imageBase64 = await fs.promises.readFile(videoSourceImagePath, 'base64');
const imageDataUrl = `data:image/webp;base64,${imageBase64}`;
@@ -1031,11 +1046,11 @@ describe('protocol module', () => {
});
});
after(async () => {
afterAll(async () => {
await fs.promises.unlink(videoPath);
});
beforeEach(async function () {
beforeEach(async (ctx) => {
w = new BrowserWindow({ show: false });
await w.loadURL('about:blank');
if (
@@ -1043,7 +1058,7 @@ describe('protocol module', () => {
"document.createElement('video').canPlayType('video/webm; codecs=\"vp8.0\"')"
))
) {
this.skip();
ctx.skip();
}
});
@@ -1103,7 +1118,7 @@ describe('protocol module', () => {
});
try {
newContents.loadURL(testingScheme + '://fake-host');
dangerouslyIgnoreWebContentsLoadResult(newContents.loadURL(testingScheme + '://fake-host'));
const [, response] = await once(ipcMain, 'result');
expect(response).to.deep.equal(expected);
} finally {
@@ -1183,28 +1198,31 @@ describe('protocol module', () => {
expect(body).to.equal('hello https://foo/');
});
it('receives requests to the existing file scheme', (done) => {
const filePath = path.join(__dirname, 'fixtures', 'pages', 'a.html');
it(
'receives requests to the existing file scheme',
withDone((done) => {
const filePath = path.join(__dirname, 'fixtures', 'pages', 'a.html');
protocol.handle('file', (req) => {
let file;
if (process.platform === 'win32') {
file = `file:///${filePath.replaceAll('\\', '/')}`;
} else {
file = `file://${filePath}`;
}
protocol.handle('file', (req) => {
let file;
if (process.platform === 'win32') {
file = `file:///${filePath.replaceAll('\\', '/')}`;
} else {
file = `file://${filePath}`;
}
if (req.url === file) done();
return new Response(req.url);
});
if (req.url === file) done();
return new Response(req.url);
});
defer(() => {
protocol.unhandle('file');
});
defer(() => {
protocol.unhandle('file');
});
const w = new BrowserWindow();
w.loadFile(filePath);
});
const w = new BrowserWindow();
dangerouslyIgnoreWebContentsLoadResult(w.loadFile(filePath));
})
);
it('receives requests to an existing scheme when navigating', async () => {
protocol.handle('https', (req) => new Response('hello ' + req.url));
@@ -1517,7 +1535,7 @@ describe('protocol module', () => {
protocol.unhandle('http-like');
});
const w = new BrowserWindow({ show: false });
w.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
const expectedHashChunks = await w.webContents.executeJavaScript(`
const dataStream = () =>
new ReadableStream({

View File

@@ -1,8 +1,7 @@
import { safeStorage } from 'electron/main';
import * as chai from 'chai';
import { expect } from 'chai';
import * as chaiAsPromised from 'chai-as-promised';
import { afterAll, beforeAll, describe, it } from 'vitest';
import * as cp from 'node:child_process';
import { once } from 'node:events';
@@ -11,16 +10,14 @@ import * as path from 'node:path';
import { ifdescribe } from './lib/spec-helpers';
chai.use(chaiAsPromised);
describe('safeStorage module', () => {
before(() => {
beforeAll(() => {
if (process.platform === 'linux') {
safeStorage.setUsePlainTextEncryption(true);
}
});
after(async () => {
afterAll(async () => {
const pathToEncryptedString = path.resolve(__dirname, 'fixtures', 'api', 'safe-storage', 'encrypted.txt');
if (fs.existsSync(pathToEncryptedString)) {
await fs.promises.rm(pathToEncryptedString, { force: true, recursive: true });

View File

@@ -1,6 +1,7 @@
import { Display, screen, desktopCapturer } from 'electron/main';
import { expect } from 'chai';
import { afterEach, beforeEach, describe, it } from 'vitest';
describe('screen module', () => {
describe('methods reassignment', () => {

View File

@@ -1,13 +1,14 @@
import { ipcMain, session, webContents as webContentsModule, WebContents } from 'electron/main';
import { expect } from 'chai';
import { afterEach, beforeEach, describe, it } from 'vitest';
import { once, on } from 'node:events';
import * as fs from 'node:fs';
import * as http from 'node:http';
import * as path from 'node:path';
import { listen, waitUntil } from './lib/spec-helpers';
import { listen, waitUntil, dangerouslyIgnoreWebContentsLoadResult } from './lib/spec-helpers';
// Toggle to add extra debug output
const DEBUG = !process.env.CI;
@@ -75,7 +76,9 @@ describe('ServiceWorkerMain module', () => {
async function loadWorkerScript(scriptUrl?: string) {
const scriptParams = scriptUrl ? `?scriptUrl=${scriptUrl}` : '';
return wc.loadURL(`${baseUrl}/index.html${scriptParams}`);
// Call sites never await this (they await waitForServiceWorker instead),
// so a load aborted by teardown would otherwise reject unhandled.
return dangerouslyIgnoreWebContentsLoadResult(wc.loadURL(`${baseUrl}/index.html${scriptParams}`));
}
async function unregisterAllServiceWorkers() {
@@ -143,7 +146,7 @@ describe('ServiceWorkerMain module', () => {
});
it('does not crash on script error', async () => {
wc.loadURL(`${baseUrl}/index.html?scriptUrl=sw-script-error.js`);
dangerouslyIgnoreWebContentsLoadResult(wc.loadURL(`${baseUrl}/index.html?scriptUrl=sw-script-error.js`));
let serviceWorker;
const actualStatuses = [];
for await (const [{ versionId, runningStatus }] of on(serviceWorkers, 'running-status-changed')) {
@@ -159,12 +162,12 @@ describe('ServiceWorkerMain module', () => {
expect(serviceWorker).to.not.be.undefined();
});
it('does not find unregistered service worker', async () => {
it('does not find unregistered service worker', async (ctx) => {
loadWorkerScript();
const runningServiceWorker = await waitForServiceWorker('running');
const { versionId } = runningServiceWorker;
unregisterAllServiceWorkers();
await waitUntil(() => runningServiceWorker.isDestroyed());
await waitUntil(() => runningServiceWorker.isDestroyed(), ctx.signal);
const serviceWorker = serviceWorkers.getWorkerFromVersionID(versionId);
expect(serviceWorker).to.be.undefined();
});
@@ -177,20 +180,20 @@ describe('ServiceWorkerMain module', () => {
expect(serviceWorker.isDestroyed()).to.be.false();
});
it('is destroyed after being unregistered', async () => {
it('is destroyed after being unregistered', async (ctx) => {
loadWorkerScript();
const serviceWorker = await waitForServiceWorker();
expect(serviceWorker.isDestroyed()).to.be.false();
await unregisterAllServiceWorkers();
await waitUntil(() => serviceWorker.isDestroyed());
await waitUntil(() => serviceWorker.isDestroyed(), ctx.signal);
});
});
describe('"running-status-changed" event', () => {
it('handles when content::ServiceWorkerVersion has been destroyed', async () => {
it('handles when content::ServiceWorkerVersion has been destroyed', async (ctx) => {
loadWorkerScript('sw-unregister-self.js');
const serviceWorker = await waitForServiceWorker('running');
await waitUntil(() => serviceWorker.isDestroyed());
await waitUntil(() => serviceWorker.isDestroyed(), ctx.signal);
});
});
@@ -279,20 +282,20 @@ describe('ServiceWorkerMain module', () => {
expect(serviceWorker._countExternalRequests()).to.equal(0);
});
it('throws when starting task after destroyed', async () => {
it('throws when starting task after destroyed', async (ctx) => {
loadWorkerScript();
const serviceWorker = await waitForServiceWorker();
await unregisterAllServiceWorkers();
await waitUntil(() => serviceWorker.isDestroyed());
await waitUntil(() => serviceWorker.isDestroyed(), ctx.signal);
expect(() => serviceWorker.startTask()).to.throw();
});
it('throws when ending task after destroyed', async () => {
it('throws when ending task after destroyed', async (ctx) => {
loadWorkerScript();
const serviceWorker = await waitForServiceWorker();
const task = serviceWorker.startTask();
await unregisterAllServiceWorkers();
await waitUntil(() => serviceWorker.isDestroyed());
await waitUntil(() => serviceWorker.isDestroyed(), ctx.signal);
expect(() => task.end()).to.throw();
});
});
@@ -300,7 +303,7 @@ describe('ServiceWorkerMain module', () => {
describe("'versionId' property", () => {
it('matches the expected value', async () => {
const runningStatusChanged = once(serviceWorkers, 'running-status-changed');
wc.loadURL(`${baseUrl}/index.html`);
dangerouslyIgnoreWebContentsLoadResult(wc.loadURL(`${baseUrl}/index.html`));
const [{ versionId }] = await runningStatusChanged;
const serviceWorker = serviceWorkers.getWorkerFromVersionID(versionId);
expect(serviceWorker).to.not.be.undefined();
@@ -360,9 +363,11 @@ describe('ServiceWorkerMain module', () => {
const abortController = new AbortController();
try {
let pingReceived = false;
once(ipcMain, 'ping', { signal: abortController.signal }).then(() => {
pingReceived = true;
});
once(ipcMain, 'ping', { signal: abortController.signal })
.then(() => {
pingReceived = true;
})
.catch(() => {});
runTest(serviceWorker, { name: 'testSend', args: ['ping'] });
await once(ses, '-ipc-message');
await new Promise<void>(queueMicrotask);

View File

@@ -2,13 +2,14 @@ import { session, webContents, WebContents } from 'electron/main';
import { expect } from 'chai';
import { v4 } from 'uuid';
import { afterEach, beforeAll, beforeEach, describe, it } from 'vitest';
import { on, once } from 'node:events';
import * as fs from 'node:fs';
import * as http from 'node:http';
import * as path from 'node:path';
import { listen } from './lib/spec-helpers';
import { listen, dangerouslyIgnoreWebContentsLoadResult } from './lib/spec-helpers';
const partition = 'service-workers-spec';
@@ -18,7 +19,7 @@ describe('session.serviceWorkers', () => {
let baseUrl: string;
let w: WebContents;
before(async () => {
beforeAll(async () => {
ses = session.fromPartition(partition);
await ses.clearStorageData();
});
@@ -54,7 +55,7 @@ describe('session.serviceWorkers', () => {
});
it('should report one as running once you load a page with a service worker', async () => {
w.loadURL(`${baseUrl}/index.html`);
dangerouslyIgnoreWebContentsLoadResult(w.loadURL(`${baseUrl}/index.html`));
await once(ses.serviceWorkers, 'console-message');
const workers = ses.serviceWorkers.getAllRunning();
const ids = Object.keys(workers) as any[] as number[];
@@ -64,7 +65,7 @@ describe('session.serviceWorkers', () => {
describe('getFromVersionID()', () => {
it('should report the correct script url and scope', async () => {
w.loadURL(`${baseUrl}/index.html`);
dangerouslyIgnoreWebContentsLoadResult(w.loadURL(`${baseUrl}/index.html`));
const eventInfo = await once(ses.serviceWorkers, 'console-message');
const details: Electron.MessageDetails = eventInfo[1];
const worker = ses.serviceWorkers.getFromVersionID(details.versionId);
@@ -77,7 +78,7 @@ describe('session.serviceWorkers', () => {
describe('console-message event', () => {
it('should correctly keep the source, message and level', async () => {
const messages: Record<string, Electron.MessageDetails> = {};
w.loadURL(`${baseUrl}/index.html?scriptUrl=sw-logs.js`);
dangerouslyIgnoreWebContentsLoadResult(w.loadURL(`${baseUrl}/index.html?scriptUrl=sw-logs.js`));
for await (const [, details] of on(ses.serviceWorkers, 'console-message')) {
messages[details.message] = details;
expect(details).to.have.property('source', 'console-api');

View File

@@ -3,6 +3,7 @@ import { app, session, BrowserWindow, net, ipcMain, Session, webFrameMain, WebFr
import * as auth from 'basic-auth';
import { expect } from 'chai';
import * as send from 'send';
import { afterAll, afterEach, beforeAll, beforeEach, describe, it } from 'vitest';
import * as ChildProcess from 'node:child_process';
import { once } from 'node:events';
@@ -12,7 +13,7 @@ import * as https from 'node:https';
import * as path from 'node:path';
import { setTimeout } from 'node:timers/promises';
import { defer, ifit, listen, waitUntil } from './lib/spec-helpers';
import { defer, ifit, listen, waitUntil, withDone, dangerouslyIgnoreWebContentsLoadResult } from './lib/spec-helpers';
import { closeAllWindows } from './lib/window-helpers';
describe('session module', () => {
@@ -324,8 +325,7 @@ describe('session module', () => {
});
});
it('should survive an app restart for persistent partition', async function () {
this.timeout(60000);
it('should survive an app restart for persistent partition', { timeout: 60000 }, async () => {
const appPath = path.join(fixtures, 'api', 'cookie-app');
const runAppWithPhase = (phase: string) => {
@@ -686,7 +686,7 @@ describe('session module', () => {
resolve({ itemUrl: item.getURL(), itemFilename: item.getFilename(), item });
});
});
w.loadURL(url);
dangerouslyIgnoreWebContentsLoadResult(w.loadURL(url));
const { item, itemUrl, itemFilename } = await downloadPrevented;
expect(itemUrl).to.equal(url + '/');
expect(itemFilename).to.equal('mockFile.txt');
@@ -735,7 +735,7 @@ describe('session module', () => {
});
customSession = session.fromPartition(partitionName);
await customSession.protocol.registerStringProtocol(protocolName, handler);
w.loadURL(`${protocolName}://fake-host`);
dangerouslyIgnoreWebContentsLoadResult(w.loadURL(`${protocolName}://fake-host`));
await once(ipcMain, 'hello');
});
});
@@ -936,17 +936,19 @@ describe('session module', () => {
const scheme = 'cors-blob';
const protocol = session.defaultSession.protocol;
const url = `${scheme}://host`;
after(async () => {
afterAll(async () => {
await protocol.unregisterProtocol(scheme);
});
afterEach(closeAllWindows);
it('returns blob data for uuid', (done) => {
const postData = JSON.stringify({
type: 'blob',
value: 'hello'
});
const content = `<html>
it(
'returns blob data for uuid',
withDone((done) => {
const postData = JSON.stringify({
type: 'blob',
value: 'hello'
});
const content = `<html>
<script>
let fd = new FormData();
fd.append('file', new Blob(['${postData}'], {type:'application/json'}));
@@ -954,29 +956,30 @@ describe('session module', () => {
</script>
</html>`;
protocol.registerStringProtocol(scheme, (request, callback) => {
try {
if (request.method === 'GET') {
callback({ data: content, mimeType: 'text/html' });
} else if (request.method === 'POST') {
const uuid = request.uploadData![1].blobUUID;
expect(uuid).to.be.a('string');
session.defaultSession.getBlobData(uuid!).then((result) => {
try {
expect(result.toString()).to.equal(postData);
done();
} catch (e) {
done(e);
}
});
protocol.registerStringProtocol(scheme, (request, callback) => {
try {
if (request.method === 'GET') {
callback({ data: content, mimeType: 'text/html' });
} else if (request.method === 'POST') {
const uuid = request.uploadData![1].blobUUID;
expect(uuid).to.be.a('string');
session.defaultSession.getBlobData(uuid!).then((result) => {
try {
expect(result.toString()).to.equal(postData);
done();
} catch (e) {
done(e);
}
});
}
} catch (e) {
done(e);
}
} catch (e) {
done(e);
}
});
const w = new BrowserWindow({ show: false });
w.loadURL(url);
});
});
const w = new BrowserWindow({ show: false });
dangerouslyIgnoreWebContentsLoadResult(w.loadURL(url));
})
);
});
describe('ses.getBlobData() (gc)', () => {
@@ -984,7 +987,7 @@ describe('session module', () => {
const protocol = session.defaultSession.protocol;
const v8Util = process._linkedBinding('electron_common_v8_util');
const waitForBlobDataRejection = (uuid: string) =>
const waitForBlobDataRejection = (uuid: string, signal: AbortSignal) =>
waitUntil(async () => {
const attempt = session.defaultSession
.getBlobData(uuid)
@@ -993,14 +996,14 @@ describe('session module', () => {
const deadline = setTimeout(1000).then(() => false);
const rejected = await Promise.race([attempt, deadline]);
return rejected;
});
}, signal);
const waitForGarbageCollection = (weak: WeakRef<object>) =>
const waitForGarbageCollection = (weak: WeakRef<object>, signal: AbortSignal) =>
waitUntil(() => {
v8Util.requestGarbageCollectionForTesting();
v8Util.runUntilIdle();
return weak.deref() === undefined;
});
}, signal);
const makeContent = (url: string, postData: string) => `<html>
<script>
@@ -1057,7 +1060,7 @@ describe('session module', () => {
}
});
it('rejects after wrapper is collected', async () => {
it('rejects after wrapper is collected', async (ctx) => {
const url = `${scheme}://gc-released-${Date.now()}`;
const postData = 'payload';
const content = makeContent(url, postData);
@@ -1075,8 +1078,8 @@ describe('session module', () => {
const weak = new WeakRef(heldDataPipe as object);
heldDataPipe = null;
await waitForGarbageCollection(weak);
await waitForBlobDataRejection(uuid);
await waitForGarbageCollection(weak, ctx.signal);
await waitForBlobDataRejection(uuid, ctx.signal);
} finally {
await protocol.unregisterProtocol(scheme);
}
@@ -1087,13 +1090,15 @@ describe('session module', () => {
const scheme = 'cors-blob';
const protocol = session.defaultSession.protocol;
const url = `${scheme}://host`;
after(async () => {
afterAll(async () => {
await protocol.unregisterProtocol(scheme);
});
afterEach(closeAllWindows);
it('returns blob data for uuid', (done) => {
const content = `<html>
it(
'returns blob data for uuid',
withDone((done) => {
const content = `<html>
<script>
let fd = new FormData();
fd.append("data", new Blob(new Array(65_537).fill('a')));
@@ -1101,30 +1106,31 @@ describe('session module', () => {
</script>
</html>`;
protocol.registerStringProtocol(scheme, (request, callback) => {
try {
if (request.method === 'GET') {
callback({ data: content, mimeType: 'text/html' });
} else if (request.method === 'POST') {
const uuid = request.uploadData![1].blobUUID;
expect(uuid).to.be.a('string');
session.defaultSession.getBlobData(uuid!).then((result) => {
try {
const data = new Array(65_537).fill('a');
expect(result.toString()).to.equal(data.join(''));
done();
} catch (e) {
done(e);
}
});
protocol.registerStringProtocol(scheme, (request, callback) => {
try {
if (request.method === 'GET') {
callback({ data: content, mimeType: 'text/html' });
} else if (request.method === 'POST') {
const uuid = request.uploadData![1].blobUUID;
expect(uuid).to.be.a('string');
session.defaultSession.getBlobData(uuid!).then((result) => {
try {
const data = new Array(65_537).fill('a');
expect(result.toString()).to.equal(data.join(''));
done();
} catch (e) {
done(e);
}
});
}
} catch (e) {
done(e);
}
} catch (e) {
done(e);
}
});
const w = new BrowserWindow({ show: false });
w.loadURL(url);
});
});
const w = new BrowserWindow({ show: false });
dangerouslyIgnoreWebContentsLoadResult(w.loadURL(url));
})
);
});
describe('ses.setCertificateVerifyProc(callback)', () => {
@@ -1150,9 +1156,11 @@ describe('session module', () => {
serverUrl = (await listen(server)).url;
});
afterEach((done) => {
server.close(done);
});
afterEach(
withDone((done) => {
server.close(done);
})
);
afterEach(closeAllWindows);
it('accepts the request when the callback is called with 0', async () => {
@@ -1310,7 +1318,7 @@ describe('session module', () => {
let port: number;
let downloadServer: http.Server;
before(async () => {
beforeAll(async () => {
downloadServer = http.createServer((req, res) => {
res.writeHead(200, {
'Content-Length': mockPDF.length,
@@ -1322,7 +1330,7 @@ describe('session module', () => {
port = (await listen(downloadServer)).port;
});
after(async () => {
afterAll(async () => {
await new Promise((resolve) => downloadServer.close(resolve));
});
@@ -1854,14 +1862,14 @@ describe('session module', () => {
// requires a secure context.
let server: http.Server;
let serverUrl: string;
before(async () => {
beforeAll(async () => {
server = http.createServer((req, res) => {
res.setHeader('Content-Type', 'text/html');
res.end('');
});
serverUrl = (await listen(server)).url;
});
after(() => {
afterAll(() => {
server.close();
});

View File

@@ -1,11 +1,12 @@
import { BaseWindow } from 'electron';
import { expect } from 'chai';
import { afterEach, describe, it } from 'vitest';
import { randomUUID } from 'node:crypto';
import * as path from 'node:path';
import { ifdescribe } from './lib/spec-helpers';
import { ifdescribe, dangerouslyIgnoreWebContentsLoadResult } from './lib/spec-helpers';
import { closeWindow } from './lib/window-helpers';
const fixtures = path.resolve(__dirname, 'fixtures');
@@ -31,8 +32,7 @@ ifdescribe(!skip)('sharedTexture module', () => {
}
});
it('successfully imported and rendered with subtle api', async function () {
this.timeout(debugSpec ? 100000 : 10000);
it('successfully imported and rendered with subtle api', { timeout: debugSpec ? 100000 : 10000 }, async () => {
type CapturedTextureHolder = {
importedSubtle: Electron.SharedTextureImportedSubtle;
texture: Electron.OffscreenSharedTexture;
@@ -142,8 +142,8 @@ ifdescribe(!skip)('sharedTexture module', () => {
resolve();
});
win.loadFile(htmlPath);
osr.loadFile(osrPath);
dangerouslyIgnoreWebContentsLoadResult(win.loadFile(htmlPath));
dangerouslyIgnoreWebContentsLoadResult(osr.loadFile(osrPath));
};
app.whenReady().then(() => {
@@ -256,8 +256,8 @@ ifdescribe(!skip)('sharedTexture module', () => {
resolve();
});
win.loadFile(htmlPath);
osr.loadFile(osrPath);
dangerouslyIgnoreWebContentsLoadResult(win.loadFile(htmlPath));
dangerouslyIgnoreWebContentsLoadResult(osr.loadFile(osrPath));
};
app.whenReady().then(() => {
@@ -266,12 +266,20 @@ ifdescribe(!skip)('sharedTexture module', () => {
});
};
it('successfully imported and rendered with managed api, without iframe', async () => {
return runSharedTextureManagedTest(false);
}).timeout(debugSpec ? 100000 : 10000);
it(
'successfully imported and rendered with managed api, without iframe',
{ timeout: debugSpec ? 100000 : 10000 },
async () => {
return runSharedTextureManagedTest(false);
}
);
it('successfully imported and rendered with managed api, with iframe', async () => {
return runSharedTextureManagedTest(true);
}).timeout(debugSpec ? 100000 : 10000);
it(
'successfully imported and rendered with managed api, with iframe',
{ timeout: debugSpec ? 100000 : 10000 },
async () => {
return runSharedTextureManagedTest(true);
}
);
});
});

View File

@@ -2,6 +2,7 @@ import { shell } from 'electron/common';
import { BrowserWindow, app } from 'electron/main';
import { expect } from 'chai';
import { afterAll, afterEach, beforeEach, describe, it } from 'vitest';
import { execSync } from 'node:child_process';
import { once } from 'node:events';
@@ -10,7 +11,7 @@ import * as http from 'node:http';
import * as os from 'node:os';
import * as path from 'node:path';
import { ifdescribe, ifit, listen } from './lib/spec-helpers';
import { ifdescribe, ifit, listen, dangerouslyIgnoreWebContentsLoadResult } from './lib/spec-helpers';
import { closeAllWindows } from './lib/window-helpers';
describe('shell module', () => {
@@ -18,14 +19,13 @@ describe('shell module', () => {
let envVars: Record<string, string | undefined> = {};
let server: http.Server;
after(function () {
this.timeout(60000);
afterAll(() => {
if (process.env.CI && process.platform === 'win32') {
// Edge may cause issues with visibility tests, so make sure it is closed after testing.
const killEdge = 'Get-Process | Where Name -Like "msedge" | Stop-Process';
execSync(killEdge, { shell: 'powershell.exe' });
}
});
}, 60000);
beforeEach(function () {
envVars = {
@@ -96,21 +96,6 @@ describe('shell module', () => {
requestReceived
]);
});
ifit(process.platform === 'darwin')(
'removes focus from the electron window after opening an external link',
async () => {
const url = 'http://127.0.0.1';
const w = new BrowserWindow({ show: true });
await once(w, 'focus');
expect(w.isFocused()).to.be.true();
await Promise.all<void>([shell.openExternal(url), once(w, 'blur') as Promise<any>]);
expect(w.isFocused()).to.be.false();
}
);
});
describe('shell.trashItem()', () => {
@@ -131,7 +116,7 @@ describe('shell module', () => {
ifit(!(process.platform === 'win32' && process.arch === 'ia32'))('works in the renderer process', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
w.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
await expect(
w.webContents.executeJavaScript("require('electron').shell.trashItem('does-not-exist')")
).to.be.rejectedWith(/does-not-exist|Failed to move item|Failed to create FileOperation/);

View File

@@ -1,13 +1,14 @@
import { app, BrowserWindow, ipcMain } from 'electron/main';
import { expect } from 'chai';
import { afterAll, afterEach, beforeAll, beforeEach, describe, it } from 'vitest';
import { once } from 'node:events';
import * as http from 'node:http';
import * as path from 'node:path';
import { emittedNTimes } from './lib/events-helpers';
import { ifdescribe, listen } from './lib/spec-helpers';
import { defer, ifdescribe, listen, dangerouslyIgnoreWebContentsLoadResult } from './lib/spec-helpers';
import { closeWindow } from './lib/window-helpers';
describe('renderer nodeIntegrationInSubFrames', () => {
@@ -33,7 +34,9 @@ describe('renderer nodeIntegrationInSubFrames', () => {
it('should load preload scripts in top level iframes', async () => {
const detailsPromise = emittedNTimes(ipcMain, 'preload-ran', 2);
w.loadFile(path.resolve(__dirname, `fixtures/sub-frames/frame-container${fixtureSuffix}.html`));
dangerouslyIgnoreWebContentsLoadResult(
w.loadFile(path.resolve(__dirname, `fixtures/sub-frames/frame-container${fixtureSuffix}.html`))
);
const [event1, event2] = await detailsPromise;
expect(event1[0].senderFrame.frameToken).to.not.equal(event2[0].senderFrame.frameToken);
expect(event1[0].senderFrame.frameToken).to.equal(event1[2]);
@@ -42,7 +45,9 @@ describe('renderer nodeIntegrationInSubFrames', () => {
it('should load preload scripts in nested iframes', async () => {
const detailsPromise = emittedNTimes(ipcMain, 'preload-ran', 3);
w.loadFile(path.resolve(__dirname, `fixtures/sub-frames/frame-with-frame-container${fixtureSuffix}.html`));
dangerouslyIgnoreWebContentsLoadResult(
w.loadFile(path.resolve(__dirname, `fixtures/sub-frames/frame-with-frame-container${fixtureSuffix}.html`))
);
const [event1, event2, event3] = await detailsPromise;
expect(event1[0].senderFrame.frameToken).to.not.equal(event2[0].senderFrame.frameToken);
expect(event1[0].senderFrame.frameToken).to.not.equal(event3[0].senderFrame.frameToken);
@@ -54,7 +59,9 @@ describe('renderer nodeIntegrationInSubFrames', () => {
it('should correctly reply to the main frame with using event.reply', async () => {
const detailsPromise = emittedNTimes(ipcMain, 'preload-ran', 2);
w.loadFile(path.resolve(__dirname, `fixtures/sub-frames/frame-container${fixtureSuffix}.html`));
dangerouslyIgnoreWebContentsLoadResult(
w.loadFile(path.resolve(__dirname, `fixtures/sub-frames/frame-container${fixtureSuffix}.html`))
);
const [event1] = await detailsPromise;
const pongPromise = once(ipcMain, 'preload-pong');
event1[0].reply('preload-ping');
@@ -64,7 +71,9 @@ describe('renderer nodeIntegrationInSubFrames', () => {
it('should correctly reply to the main frame with using event.senderFrame.send', async () => {
const detailsPromise = emittedNTimes(ipcMain, 'preload-ran', 2);
w.loadFile(path.resolve(__dirname, `fixtures/sub-frames/frame-container${fixtureSuffix}.html`));
dangerouslyIgnoreWebContentsLoadResult(
w.loadFile(path.resolve(__dirname, `fixtures/sub-frames/frame-container${fixtureSuffix}.html`))
);
const [event1] = await detailsPromise;
const pongPromise = once(ipcMain, 'preload-pong');
event1[0].senderFrame.send('preload-ping');
@@ -74,7 +83,9 @@ describe('renderer nodeIntegrationInSubFrames', () => {
it('should correctly reply to the sub-frames with using event.reply', async () => {
const detailsPromise = emittedNTimes(ipcMain, 'preload-ran', 2);
w.loadFile(path.resolve(__dirname, `fixtures/sub-frames/frame-container${fixtureSuffix}.html`));
dangerouslyIgnoreWebContentsLoadResult(
w.loadFile(path.resolve(__dirname, `fixtures/sub-frames/frame-container${fixtureSuffix}.html`))
);
const [, event2] = await detailsPromise;
const pongPromise = once(ipcMain, 'preload-pong');
event2[0].reply('preload-ping');
@@ -84,7 +95,9 @@ describe('renderer nodeIntegrationInSubFrames', () => {
it('should correctly reply to the sub-frames with using event.senderFrame.send', async () => {
const detailsPromise = emittedNTimes(ipcMain, 'preload-ran', 2);
w.loadFile(path.resolve(__dirname, `fixtures/sub-frames/frame-container${fixtureSuffix}.html`));
dangerouslyIgnoreWebContentsLoadResult(
w.loadFile(path.resolve(__dirname, `fixtures/sub-frames/frame-container${fixtureSuffix}.html`))
);
const [, event2] = await detailsPromise;
const pongPromise = once(ipcMain, 'preload-pong');
event2[0].senderFrame.send('preload-ping');
@@ -94,7 +107,9 @@ describe('renderer nodeIntegrationInSubFrames', () => {
it('should correctly reply to the nested sub-frames with using event.reply', async () => {
const detailsPromise = emittedNTimes(ipcMain, 'preload-ran', 3);
w.loadFile(path.resolve(__dirname, `fixtures/sub-frames/frame-with-frame-container${fixtureSuffix}.html`));
dangerouslyIgnoreWebContentsLoadResult(
w.loadFile(path.resolve(__dirname, `fixtures/sub-frames/frame-with-frame-container${fixtureSuffix}.html`))
);
const [, , event3] = await detailsPromise;
const pongPromise = once(ipcMain, 'preload-pong');
event3[0].reply('preload-ping');
@@ -104,7 +119,9 @@ describe('renderer nodeIntegrationInSubFrames', () => {
it('should correctly reply to the nested sub-frames with using event.senderFrame.send', async () => {
const detailsPromise = emittedNTimes(ipcMain, 'preload-ran', 3);
w.loadFile(path.resolve(__dirname, `fixtures/sub-frames/frame-with-frame-container${fixtureSuffix}.html`));
dangerouslyIgnoreWebContentsLoadResult(
w.loadFile(path.resolve(__dirname, `fixtures/sub-frames/frame-with-frame-container${fixtureSuffix}.html`))
);
const [, , event3] = await detailsPromise;
const pongPromise = once(ipcMain, 'preload-pong');
event3[0].senderFrame.send('preload-ping');
@@ -114,7 +131,9 @@ describe('renderer nodeIntegrationInSubFrames', () => {
it('should not expose globals in main world', async () => {
const detailsPromise = emittedNTimes(ipcMain, 'preload-ran', 2);
w.loadFile(path.resolve(__dirname, `fixtures/sub-frames/frame-container${fixtureSuffix}.html`));
dangerouslyIgnoreWebContentsLoadResult(
w.loadFile(path.resolve(__dirname, `fixtures/sub-frames/frame-container${fixtureSuffix}.html`))
);
const details = await detailsPromise;
const senders = details.map((event) => event[0].sender);
const isolatedGlobals = await Promise.all(
@@ -216,6 +235,7 @@ describe('renderer nodeIntegrationInSubFrames', () => {
describe('subframe with non-standard schemes', () => {
it('should not crash when changing subframe src to about:blank and back', async () => {
const w = new BrowserWindow({ show: false, width: 400, height: 400 });
defer(() => closeWindow(w));
const fwfPath = path.resolve(__dirname, 'fixtures/sub-frames/frame-with-frame.html');
await w.loadFile(fwfPath);
@@ -253,7 +273,7 @@ ifdescribe(process.platform !== 'linux')('cross-site frame sandboxing', () => {
let crossSiteUrl: string;
let serverUrl: string;
before(async function () {
beforeAll(async function () {
server = http.createServer((req, res) => {
res.end(`<iframe name="frame" src="${crossSiteUrl}" />`);
});
@@ -261,7 +281,7 @@ ifdescribe(process.platform !== 'linux')('cross-site frame sandboxing', () => {
crossSiteUrl = serverUrl.replace('127.0.0.1', 'localhost');
});
after(() => {
afterAll(() => {
server.close();
server = null as unknown as http.Server;
});

View File

@@ -1,6 +1,7 @@
import { systemPreferences } from 'electron/main';
import { expect } from 'chai';
import { describe, it } from 'vitest';
import { ifdescribe, ifit } from './lib/spec-helpers';

View File

@@ -1,9 +1,11 @@
import { BaseWindow, BrowserWindow, TouchBar } from 'electron/main';
import { expect } from 'chai';
import { afterEach, beforeEach, describe, it } from 'vitest';
import * as path from 'node:path';
import { withDone } from './lib/spec-helpers';
import { closeWindow } from './lib/window-helpers';
const {
@@ -117,29 +119,35 @@ describe('TouchBar module', () => {
touchBar.escapeItem = null;
});
it('calls the callback on the items when a window interaction event fires', (done) => {
const button = new TouchBarButton({
label: 'bar',
click: () => {
done();
}
});
const touchBar = new TouchBar({ items: [button] });
window.setTouchBar(touchBar);
window.emit('-touch-bar-interaction', {}, (button as any).id);
});
it(
'calls the callback on the items when a window interaction event fires',
withDone((done) => {
const button = new TouchBarButton({
label: 'bar',
click: () => {
done();
}
});
const touchBar = new TouchBar({ items: [button] });
window.setTouchBar(touchBar);
window.emit('-touch-bar-interaction', {}, (button as any).id);
})
);
it('calls the callback on the escape item when a window interaction event fires', (done) => {
const button = new TouchBarButton({
label: 'bar',
click: () => {
done();
}
});
const touchBar = new TouchBar({ escapeItem: button });
window.setTouchBar(touchBar);
window.emit('-touch-bar-interaction', {}, (button as any).id);
});
it(
'calls the callback on the escape item when a window interaction event fires',
withDone((done) => {
const button = new TouchBarButton({
label: 'bar',
click: () => {
done();
}
});
const touchBar = new TouchBar({ escapeItem: button });
window.setTouchBar(touchBar);
window.emit('-touch-bar-interaction', {}, (button as any).id);
})
);
});
}
});

View File

@@ -2,6 +2,7 @@ import { nativeImage } from 'electron/common';
import { Menu, Tray } from 'electron/main';
import { expect } from 'chai';
import { afterEach, beforeEach, describe, it } from 'vitest';
import * as path from 'node:path';
import { setTimeout } from 'node:timers/promises';

View File

@@ -2,6 +2,7 @@ import { systemPreferences } from 'electron';
import { BrowserWindow, MessageChannelMain, utilityProcess, app } from 'electron/main';
import { expect } from 'chai';
import { describe, it } from 'vitest';
import * as childProcess from 'node:child_process';
import { once } from 'node:events';
@@ -12,7 +13,7 @@ import { setImmediate } from 'node:timers/promises';
import { pathToFileURL } from 'node:url';
import { respondOnce, randomString, kOneKiloByte } from './lib/net-helpers';
import { ifit, startRemoteControlApp } from './lib/spec-helpers';
import { ifit, startRemoteControlApp, withDone } from './lib/spec-helpers';
import { closeWindow } from './lib/window-helpers';
const fixturesPath = path.resolve(__dirname, 'fixtures', 'api', 'utility-process');
@@ -60,21 +61,27 @@ describe('utilityProcess module', () => {
await once(child, 'spawn');
});
it("emits 'exit' when child process exits gracefully", (done) => {
const child = utilityProcess.fork(path.join(fixturesPath, 'empty.js'));
child.on('exit', (code) => {
expect(code).to.equal(0);
done();
});
});
it(
"emits 'exit' when child process exits gracefully",
withDone((done) => {
const child = utilityProcess.fork(path.join(fixturesPath, 'empty.js'));
child.on('exit', (code) => {
expect(code).to.equal(0);
done();
});
})
);
it("emits 'exit' when the child process file does not exist", (done) => {
const child = utilityProcess.fork('nonexistent');
child.on('exit', (code) => {
expect(code).to.equal(1);
done();
});
});
it(
"emits 'exit' when the child process file does not exist",
withDone((done) => {
const child = utilityProcess.fork('nonexistent');
child.on('exit', (code) => {
expect(code).to.equal(1);
done();
});
})
);
ifit(!isWindows32Bit)('emits the correct error code when child process exits nonzero', async () => {
const child = utilityProcess.fork(path.join(fixturesPath, 'empty.js'));
@@ -457,86 +464,95 @@ describe('utilityProcess module', () => {
});
describe('behavior', () => {
it('supports starting the v8 inspector with --inspect-brk', (done) => {
const child = utilityProcess.fork(path.join(fixturesPath, 'log.js'), [], {
stdio: 'pipe',
execArgv: ['--inspect-brk']
});
let output = '';
const cleanup = () => {
child.stderr!.removeListener('data', listener);
child.stdout!.removeListener('data', listener);
child.once('exit', () => {
done();
it(
'supports starting the v8 inspector with --inspect-brk',
withDone((done) => {
const child = utilityProcess.fork(path.join(fixturesPath, 'log.js'), [], {
stdio: 'pipe',
execArgv: ['--inspect-brk']
});
child.kill();
};
const listener = (data: Buffer) => {
output += data;
if (/Debugger listening on ws:/m.test(output)) {
let output = '';
const cleanup = () => {
child.stderr!.removeListener('data', listener);
child.stdout!.removeListener('data', listener);
child.once('exit', () => {
done();
});
child.kill();
};
const listener = (data: Buffer) => {
output += data;
if (/Debugger listening on ws:/m.test(output)) {
cleanup();
}
};
child.stderr!.on('data', listener);
child.stdout!.on('data', listener);
})
);
it(
'supports starting the v8 inspector with --inspect and a provided port',
withDone((done) => {
const child = utilityProcess.fork(path.join(fixturesPath, 'log.js'), [], {
stdio: 'pipe',
execArgv: ['--inspect=17364']
});
let output = '';
const cleanup = () => {
child.stderr!.removeListener('data', listener);
child.stdout!.removeListener('data', listener);
child.once('exit', () => {
done();
});
child.kill();
};
const listener = (data: Buffer) => {
output += data;
if (/Debugger listening on ws:/m.test(output)) {
expect(output.trim()).to.contain(':17364', 'should be listening on port 17364');
cleanup();
}
};
child.stderr!.on('data', listener);
child.stdout!.on('data', listener);
})
);
it(
'supports changing dns verbatim with --dns-result-order',
withDone((done) => {
const child = utilityProcess.fork(path.join(fixturesPath, 'dns-result-order.js'), [], {
stdio: 'pipe',
execArgv: ['--dns-result-order=ipv4first']
});
let output = '';
const cleanup = () => {
child.stderr!.removeListener('data', listener);
child.stdout!.removeListener('data', listener);
child.once('exit', () => {
done();
});
child.kill();
};
const listener = (data: Buffer) => {
output += data;
expect(output.trim()).to.contain('ipv4first', 'default verbatim should be ipv4first');
cleanup();
}
};
};
child.stderr!.on('data', listener);
child.stdout!.on('data', listener);
});
it('supports starting the v8 inspector with --inspect and a provided port', (done) => {
const child = utilityProcess.fork(path.join(fixturesPath, 'log.js'), [], {
stdio: 'pipe',
execArgv: ['--inspect=17364']
});
let output = '';
const cleanup = () => {
child.stderr!.removeListener('data', listener);
child.stdout!.removeListener('data', listener);
child.once('exit', () => {
done();
});
child.kill();
};
const listener = (data: Buffer) => {
output += data;
if (/Debugger listening on ws:/m.test(output)) {
expect(output.trim()).to.contain(':17364', 'should be listening on port 17364');
cleanup();
}
};
child.stderr!.on('data', listener);
child.stdout!.on('data', listener);
});
it('supports changing dns verbatim with --dns-result-order', (done) => {
const child = utilityProcess.fork(path.join(fixturesPath, 'dns-result-order.js'), [], {
stdio: 'pipe',
execArgv: ['--dns-result-order=ipv4first']
});
let output = '';
const cleanup = () => {
child.stderr!.removeListener('data', listener);
child.stdout!.removeListener('data', listener);
child.once('exit', () => {
done();
});
child.kill();
};
const listener = (data: Buffer) => {
output += data;
expect(output.trim()).to.contain('ipv4first', 'default verbatim should be ipv4first');
cleanup();
};
child.stderr!.on('data', listener);
child.stdout!.on('data', listener);
});
child.stderr!.on('data', listener);
child.stdout!.on('data', listener);
})
);
ifit(process.platform !== 'win32')('supports redirecting stdout to parent process', async () => {
const result = 'Output from utility process';

View File

@@ -1,7 +1,9 @@
import { BaseWindow, View } from 'electron/main';
import { expect } from 'chai';
import { afterEach, describe, it } from 'vitest';
import { withDone } from './lib/spec-helpers';
import { closeWindow } from './lib/window-helpers';
describe('View', () => {
@@ -39,7 +41,7 @@ describe('View', () => {
});
it('can be added as a child of another View', async () => {
const w = new BaseWindow();
w = new BaseWindow();
const v1 = new View();
const v2 = new View();
@@ -136,20 +138,23 @@ describe('View', () => {
expect(child.getBounds()).to.deep.equal({ x: 10, y: 15, width: 25, height: 30 });
});
it('can set bounds with animation', (done) => {
const v = new View();
v.setBounds(
{ x: 0, y: 0, width: 100, height: 100 },
{
animate: {
duration: 300
it(
'can set bounds with animation',
withDone((done) => {
const v = new View();
v.setBounds(
{ x: 0, y: 0, width: 100, height: 100 },
{
animate: {
duration: 300
}
}
}
);
setTimeout(() => {
expect(v.getBounds()).to.deep.equal({ x: 0, y: 0, width: 100, height: 100 });
done();
}, 350);
});
);
setTimeout(() => {
expect(v.getBounds()).to.deep.equal({ x: 0, y: 0, width: 100, height: 100 });
done();
}, 350);
})
);
});
});

View File

@@ -1,12 +1,13 @@
import { BaseWindow, BrowserWindow, View, WebContentsView, webContents, screen } from 'electron/main';
import { expect } from 'chai';
import { afterEach, beforeEach, describe, it } from 'vitest';
import { once } from 'node:events';
import { setTimeout as setTimeoutAsync } from 'node:timers/promises';
import { HexColors, ScreenCapture, hasCapturableScreen, nextFrameTime } from './lib/screen-helpers';
import { defer, ifdescribe, waitUntil } from './lib/spec-helpers';
import { defer, ifdescribe, waitUntil, withDone, dangerouslyIgnoreWebContentsLoadResult } from './lib/spec-helpers';
import { closeAllWindows } from './lib/window-helpers';
describe('WebContentsView', () => {
@@ -27,7 +28,7 @@ describe('WebContentsView', () => {
});
it('accepts existing webContents object', async () => {
const currentWebContentsCount = webContents.getAllWebContents().length;
const before = new Set(webContents.getAllWebContents().map((c) => c.id));
const wc = (webContents as typeof ElectronInternal.WebContents).create({ sandbox: true });
defer(() => wc.destroy());
@@ -38,10 +39,9 @@ describe('WebContentsView', () => {
});
expect(webContentsView.webContents).to.eq(wc);
expect(webContents.getAllWebContents().length).to.equal(
currentWebContentsCount + 1,
'expected only single webcontents to be created'
);
const created = webContents.getAllWebContents().filter((c) => !before.has(c.id));
expect(created).to.have.lengthOf(1, 'expected only single webcontents to be created');
expect(created[0].id).to.equal(wc.id);
});
it('should throw error when created with already attached webContents to BrowserWindow', () => {
@@ -67,7 +67,7 @@ describe('WebContentsView', () => {
const webContentsView = new WebContentsView();
const wc = webContentsView.webContents;
wc.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(wc.loadURL('about:blank'));
wc.destroy();
const destroyed = once(wc, 'destroyed');
@@ -82,7 +82,7 @@ describe('WebContentsView', () => {
const webContentsView = new WebContentsView();
defer(() => webContentsView.webContents.destroy());
webContentsView.webContents.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(webContentsView.webContents.loadURL('about:blank'));
expect(
() =>
@@ -163,23 +163,28 @@ describe('WebContentsView', () => {
return arr;
}
it("doesn't crash when GCed during allocation", (done) => {
// eslint-disable-next-line no-new
new WebContentsView();
setTimeout(() => {
// NB. the crash we're testing for is the lack of a current `v8::Context`
// when emitting an event in WebContents's destructor. V8 is inconsistent
// about whether or not there's a current context during garbage
// collection, and it seems that `v8Util.requestGarbageCollectionForTesting`
// causes a GC in which there _is_ a current context, so the crash isn't
// triggered. Thus, we force a GC by other means: namely, by allocating a
// bunch of stuff.
triggerGCByAllocation();
done();
});
});
it(
"doesn't crash when GCed during allocation",
withDone((done) => {
// eslint-disable-next-line no-new
new WebContentsView();
setTimeout(() => {
// NB. the crash we're testing for is the lack of a current `v8::Context`
// when emitting an event in WebContents's destructor. V8 is inconsistent
// about whether or not there's a current context during garbage
// collection, and it seems that `v8Util.requestGarbageCollectionForTesting`
// causes a GC in which there _is_ a current context, so the crash isn't
// triggered. Thus, we force a GC by other means: namely, by allocating a
// bunch of stuff.
triggerGCByAllocation();
done();
});
})
);
it('does not crash when closed via window.close()', async () => {
// TODO(#50982): re-enable once the native blur-during-destruction DCHECK is
// resolved. This test's blur handler is the re-entry vector.
it.skip('does not crash when closed via window.close()', async () => {
const bw = new BrowserWindow();
const wcv = new WebContentsView();
const wc = wcv.webContents;
@@ -194,7 +199,7 @@ describe('WebContentsView', () => {
});
});
wc.loadURL('data:text/html,<script>window.close()</script>');
dangerouslyIgnoreWebContentsLoadResult(wc.loadURL('data:text/html,<script>window.close()</script>'));
const open = await dto;
expect(open).to.be.false();
@@ -265,12 +270,14 @@ describe('WebContentsView', () => {
expect(await v.webContents.executeJavaScript('initialVisibility')).to.equal('visible');
});
it('becomes hidden when parent window is hidden', async () => {
it('becomes hidden when parent window is hidden', async (ctx) => {
const w = new BaseWindow();
const v = new WebContentsView();
w.setContentView(v);
await v.webContents.loadURL('about:blank');
await expect(waitUntil(async () => await haveVisibilityState(v, 'visible'))).to.eventually.be.fulfilled();
await expect(
waitUntil(async () => await haveVisibilityState(v, 'visible'), ctx.signal)
).to.eventually.be.fulfilled();
const p = v.webContents.executeJavaScript(
'new Promise(resolve => document.addEventListener("visibilitychange", resolve))'
);
@@ -301,12 +308,14 @@ describe('WebContentsView', () => {
expect(await v.webContents.executeJavaScript('document.visibilityState')).to.equal('visible');
});
it('does not change when view is moved between two visible windows', async () => {
it('does not change when view is moved between two visible windows', async (ctx) => {
const w = new BaseWindow();
const v = new WebContentsView();
w.setContentView(v);
await v.webContents.loadURL('about:blank');
await expect(waitUntil(async () => await haveVisibilityState(v, 'visible'))).to.eventually.be.fulfilled();
await expect(
waitUntil(async () => await haveVisibilityState(v, 'visible'), ctx.signal)
).to.eventually.be.fulfilled();
const p = v.webContents.executeJavaScript(
'new Promise(resolve => document.addEventListener("visibilitychange", () => resolve(document.visibilityState)))'
@@ -330,7 +339,7 @@ describe('WebContentsView', () => {
expect(visibilityState).to.equal('visible');
});
it('tracks visibility for multiple child WebContentsViews', async () => {
it('tracks visibility for multiple child WebContentsViews', async (ctx) => {
const w = new BaseWindow({ show: false });
const cv = new View();
w.setContentView(cv);
@@ -345,21 +354,33 @@ describe('WebContentsView', () => {
await v1.webContents.loadURL('about:blank');
await v2.webContents.loadURL('about:blank');
await expect(waitUntil(async () => await haveVisibilityState(v1, 'hidden'))).to.eventually.be.fulfilled();
await expect(waitUntil(async () => await haveVisibilityState(v2, 'hidden'))).to.eventually.be.fulfilled();
await expect(
waitUntil(async () => await haveVisibilityState(v1, 'hidden'), ctx.signal)
).to.eventually.be.fulfilled();
await expect(
waitUntil(async () => await haveVisibilityState(v2, 'hidden'), ctx.signal)
).to.eventually.be.fulfilled();
w.show();
await expect(waitUntil(async () => await haveVisibilityState(v1, 'visible'))).to.eventually.be.fulfilled();
await expect(waitUntil(async () => await haveVisibilityState(v2, 'visible'))).to.eventually.be.fulfilled();
await expect(
waitUntil(async () => await haveVisibilityState(v1, 'visible'), ctx.signal)
).to.eventually.be.fulfilled();
await expect(
waitUntil(async () => await haveVisibilityState(v2, 'visible'), ctx.signal)
).to.eventually.be.fulfilled();
w.hide();
await expect(waitUntil(async () => await haveVisibilityState(v1, 'hidden'))).to.eventually.be.fulfilled();
await expect(waitUntil(async () => await haveVisibilityState(v2, 'hidden'))).to.eventually.be.fulfilled();
await expect(
waitUntil(async () => await haveVisibilityState(v1, 'hidden'), ctx.signal)
).to.eventually.be.fulfilled();
await expect(
waitUntil(async () => await haveVisibilityState(v2, 'hidden'), ctx.signal)
).to.eventually.be.fulfilled();
});
it('tracks visibility independently when a child WebContentsView is hidden via setVisible', async () => {
it('tracks visibility independently when a child WebContentsView is hidden via setVisible', async (ctx) => {
const w = new BaseWindow();
const cv = new View();
w.setContentView(cv);
@@ -374,21 +395,29 @@ describe('WebContentsView', () => {
await v1.webContents.loadURL('about:blank');
await v2.webContents.loadURL('about:blank');
await expect(waitUntil(async () => await haveVisibilityState(v1, 'visible'))).to.eventually.be.fulfilled();
await expect(waitUntil(async () => await haveVisibilityState(v2, 'visible'))).to.eventually.be.fulfilled();
await expect(
waitUntil(async () => await haveVisibilityState(v1, 'visible'), ctx.signal)
).to.eventually.be.fulfilled();
await expect(
waitUntil(async () => await haveVisibilityState(v2, 'visible'), ctx.signal)
).to.eventually.be.fulfilled();
v1.setVisible(false);
await expect(waitUntil(async () => await haveVisibilityState(v1, 'hidden'))).to.eventually.be.fulfilled();
await expect(
waitUntil(async () => await haveVisibilityState(v1, 'hidden'), ctx.signal)
).to.eventually.be.fulfilled();
// v2 should remain visible while v1 is hidden
expect(await v2.webContents.executeJavaScript('document.visibilityState')).to.equal('visible');
v1.setVisible(true);
await expect(waitUntil(async () => await haveVisibilityState(v1, 'visible'))).to.eventually.be.fulfilled();
await expect(
waitUntil(async () => await haveVisibilityState(v1, 'visible'), ctx.signal)
).to.eventually.be.fulfilled();
});
it('fires a single visibilitychange event per show/hide transition', async () => {
it('fires a single visibilitychange event per show/hide transition', async (ctx) => {
const w = new BaseWindow({ show: false });
const v = new WebContentsView();
w.setContentView(v);
@@ -402,13 +431,17 @@ describe('WebContentsView', () => {
`);
w.show();
await expect(waitUntil(async () => await haveVisibilityState(v, 'visible'))).to.eventually.be.fulfilled();
await expect(
waitUntil(async () => await haveVisibilityState(v, 'visible'), ctx.signal)
).to.eventually.be.fulfilled();
// Give any delayed/queued occlusion updates time to fire.
await setTimeoutAsync(1500);
w.hide();
await expect(waitUntil(async () => await haveVisibilityState(v, 'hidden'))).to.eventually.be.fulfilled();
await expect(
waitUntil(async () => await haveVisibilityState(v, 'hidden'), ctx.signal)
).to.eventually.be.fulfilled();
await setTimeoutAsync(1500);
@@ -445,7 +478,7 @@ describe('WebContentsView', () => {
v.setBorderRadius(100);
const readyForCapture = once(v.webContents, 'ready-to-show');
v.webContents.loadURL(backgroundUrl);
dangerouslyIgnoreWebContentsLoadResult(v.webContents.loadURL(backgroundUrl));
const inset = 10;
// Adjust for macOS menu bar height which seems to be about 24px
@@ -497,7 +530,7 @@ describe('WebContentsView', () => {
w.setContentView(v);
const readyForCapture = once(v.webContents, 'ready-to-show');
v.webContents.loadURL(backgroundUrl);
dangerouslyIgnoreWebContentsLoadResult(v.webContents.loadURL(backgroundUrl));
await readyForCapture;
const corner = corners[0];
@@ -512,38 +545,4 @@ describe('WebContentsView', () => {
v.setBorderRadius(100);
});
});
describe('focusOnNavigation webPreference', () => {
it('focuses the webContents on navigation by default', async () => {
const w = new BrowserWindow();
await once(w, 'focus');
const v = new WebContentsView();
w.setContentView(v);
await v.webContents.loadURL('about:blank');
const devToolsFocused = once(v.webContents, 'devtools-focused');
v.webContents.openDevTools({ mode: 'right' });
await devToolsFocused;
expect(v.webContents.isFocused()).to.be.false();
await v.webContents.loadURL('data:text/html,<body>test</body>');
expect(v.webContents.isFocused()).to.be.true();
});
it('does not focus the webContents on navigation when focusOnNavigation is false', async () => {
const w = new BrowserWindow();
await once(w, 'focus');
const v = new WebContentsView({
webPreferences: {
focusOnNavigation: false
}
});
w.setContentView(v);
await v.webContents.loadURL('about:blank');
const devToolsFocused = once(v.webContents, 'devtools-focused');
v.webContents.openDevTools({ mode: 'right' });
await devToolsFocused;
expect(v.webContents.isFocused()).to.be.false();
await v.webContents.loadURL('data:text/html,<body>test</body>');
expect(v.webContents.isFocused()).to.be.false();
});
});
});

View File

@@ -1,7 +1,7 @@
import { clipboard } from 'electron/common';
import { BrowserWindow, WebFrameMain, webFrameMain, ipcMain, app, WebContents } from 'electron/main';
import { expect } from 'chai';
import { afterAll, afterEach, beforeAll, beforeEach, describe, it } from 'vitest';
import { once } from 'node:events';
import * as http from 'node:http';
@@ -10,7 +10,7 @@ import { setTimeout } from 'node:timers/promises';
import * as url from 'node:url';
import { emittedNTimes } from './lib/events-helpers';
import { defer, ifit, listen, waitUntil } from './lib/spec-helpers';
import { defer, ifit, listen, waitUntil, dangerouslyIgnoreWebContentsLoadResult } from './lib/spec-helpers';
import { closeAllWindows } from './lib/window-helpers';
describe('webFrameMain module', () => {
@@ -108,12 +108,12 @@ describe('webFrameMain module', () => {
let serverA: Server;
let serverB: Server;
before(async () => {
beforeAll(async () => {
serverA = await createServer();
serverB = await createServer();
});
after(() => {
afterAll(() => {
serverA.server.close();
serverB.server.close();
});
@@ -206,14 +206,14 @@ describe('webFrameMain module', () => {
describe('WebFrame.visibilityState', () => {
// DISABLED-FIXME(MarshallOfSound): Fix flaky test
it('should match window state', async () => {
it('should match window state', async (ctx) => {
const w = new BrowserWindow({ show: true });
await w.loadURL('about:blank');
const webFrame = w.webContents.mainFrame;
expect(webFrame.visibilityState).to.equal('visible');
w.hide();
await expect(waitUntil(() => webFrame.visibilityState === 'hidden')).to.eventually.be.fulfilled();
await expect(waitUntil(() => webFrame.visibilityState === 'hidden', ctx.signal)).to.eventually.be.fulfilled();
});
});
@@ -296,10 +296,10 @@ describe('webFrameMain module', () => {
let server: Awaited<ReturnType<typeof createServer>>;
let w: BrowserWindow;
before(async () => {
beforeAll(async () => {
server = await createServer();
});
after(() => {
afterAll(() => {
server.server.close();
});
beforeEach(async () => {
@@ -355,7 +355,7 @@ describe('webFrameMain module', () => {
console.log('mainFrame.url', mainFrame.url);
});
it('returns null upon accessing senderFrame after cross-origin navigation', async () => {
it('returns null upon accessing senderFrame after cross-origin navigation', async (ctx) => {
w = new BrowserWindow({
show: false,
webPreferences: {
@@ -368,9 +368,9 @@ describe('webFrameMain module', () => {
await w.webContents.loadURL(server.crossOriginUrl);
// senderFrame now points to a disposed RenderFrameHost. It should
// be null when attempting to access the lazily evaluated property.
waitUntil(() => {
await waitUntil(() => {
return event.senderFrame === null;
});
}, ctx.signal);
});
it('is detached when unload handler sends IPC', async () => {
@@ -477,7 +477,7 @@ describe('webFrameMain module', () => {
// frame-with-frame-container.html, frame-with-frame.html, frame.html
const didFrameFinishLoad = emittedNTimes(w.webContents, 'did-frame-finish-load', 3);
w.loadFile(path.join(subframesPath, 'frame-with-frame-container.html'));
dangerouslyIgnoreWebContentsLoadResult(w.loadFile(path.join(subframesPath, 'frame-with-frame-container.html')));
for (const [, isMainFrame, frameProcessId, frameRoutingId] of await didFrameFinishLoad) {
const frame = webFrameMain.fromId(frameProcessId, frameRoutingId);
@@ -504,14 +504,14 @@ describe('webFrameMain module', () => {
describe('webFrameMain.collectJavaScriptCallStack', () => {
let server: Server;
before(async () => {
beforeAll(async () => {
server = await createServer({
headers: {
'Document-Policy': 'include-js-call-stacks-in-crash-reports'
}
});
});
after(() => {
afterAll(() => {
server.server.close();
});
@@ -525,102 +525,11 @@ describe('webFrameMain module', () => {
});
});
describe('webFrameMain.copyVideoFrameAt', () => {
const insertVideoInFrame = async (frame: WebFrameMain) => {
const videoFilePath = url.pathToFileURL(path.join(fixtures, 'cat-spin.mp4')).href;
await frame.executeJavaScript(`
const video = document.createElement('video');
video.src = '${videoFilePath}';
video.muted = true;
video.loop = true;
video.play();
document.body.appendChild(video);
`);
};
const getFramePosition = async (frame: WebFrameMain) => {
const point = (await frame.executeJavaScript(
`(${() => {
const iframe = document.querySelector('iframe');
if (!iframe) return;
const rect = iframe.getBoundingClientRect();
return { x: Math.floor(rect.x), y: Math.floor(rect.y) };
}})()`
)) as Electron.Point;
expect(point).to.be.an('object');
return point;
};
const copyVideoFrameInFrame = async (frame: WebFrameMain) => {
const point = (await frame.executeJavaScript(
`(${() => {
const video = document.querySelector('video');
if (!video) return;
const rect = video.getBoundingClientRect();
return {
x: Math.floor(rect.x + rect.width / 2),
y: Math.floor(rect.y + rect.height / 2)
};
}})()`
)) as Electron.Point;
expect(point).to.be.an('object');
// Translate coordinate to be relative of main frame
if (frame.parent) {
const framePosition = await getFramePosition(frame.parent);
point.x += framePosition.x;
point.y += framePosition.y;
}
expect(clipboard.readImage().isEmpty()).to.be.true();
// wait for video to load
await frame.executeJavaScript(
`(${() => {
const video = document.querySelector('video');
if (!video) return;
return new Promise((resolve) => {
if (video.readyState >= 4) resolve(null);
else video.addEventListener('canplaythrough', resolve, { once: true });
});
}})()`
);
frame.copyVideoFrameAt(point.x, point.y);
await waitUntil(() => clipboard.availableFormats().includes('image/png'));
expect(clipboard.readImage().isEmpty()).to.be.false();
};
beforeEach(() => {
clipboard.clear();
});
// TODO: Re-enable on Windows CI once Chromium fixes the intermittent
// backwards-time DCHECK hit while copying video frames:
// DCHECK failed: !delta.is_negative().
ifit(!(process.platform === 'win32' && process.env.CI))('copies video frame in main frame', async () => {
const w = new BrowserWindow({ show: false });
await w.webContents.loadFile(path.join(fixtures, 'blank.html'));
await insertVideoInFrame(w.webContents.mainFrame);
await copyVideoFrameInFrame(w.webContents.mainFrame);
await waitUntil(() => clipboard.availableFormats().includes('image/png'));
});
ifit(!(process.platform === 'win32' && process.env.CI))('copies video frame in subframe', async () => {
const w = new BrowserWindow({ show: false });
await w.webContents.loadFile(path.join(subframesPath, 'frame-with-frame.html'));
const subframe = w.webContents.mainFrame.frames[0];
expect(subframe).to.exist();
await insertVideoInFrame(subframe);
await copyVideoFrameInFrame(subframe);
await waitUntil(() => clipboard.availableFormats().includes('image/png'));
});
});
describe('"frame-created" event', () => {
it('emits when the main frame is created', async () => {
const w = new BrowserWindow({ show: false });
const promise = once(w.webContents, 'frame-created') as Promise<[any, Electron.FrameCreatedDetails]>;
w.webContents.loadFile(path.join(subframesPath, 'frame.html'));
dangerouslyIgnoreWebContentsLoadResult(w.webContents.loadFile(path.join(subframesPath, 'frame.html')));
const [, details] = await promise;
expect(details.frame).to.equal(w.webContents.mainFrame);
});
@@ -630,7 +539,7 @@ describe('webFrameMain module', () => {
const promise = emittedNTimes(w.webContents, 'frame-created', 2) as Promise<
[any, Electron.FrameCreatedDetails][]
>;
w.webContents.loadFile(path.join(subframesPath, 'frame-container.html'));
dangerouslyIgnoreWebContentsLoadResult(w.webContents.loadFile(path.join(subframesPath, 'frame-container.html')));
const [[, mainDetails], [, nestedDetails]] = await promise;
expect(mainDetails.frame).to.equal(w.webContents.mainFrame);
expect(nestedDetails.frame).to.equal(w.webContents.mainFrame.frames[0]);
@@ -661,7 +570,7 @@ describe('webFrameMain module', () => {
it('emits for top-level frame', async () => {
const w = new BrowserWindow({ show: false });
const promise = once(w.webContents.mainFrame, 'dom-ready');
w.webContents.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(w.webContents.loadURL('about:blank'));
await promise;
});
@@ -676,7 +585,7 @@ describe('webFrameMain module', () => {
});
});
});
w.webContents.loadFile(path.join(subframesPath, 'frame-with-frame.html'));
dangerouslyIgnoreWebContentsLoadResult(w.webContents.loadFile(path.join(subframesPath, 'frame-with-frame.html')));
await promise;
});
});

View File

@@ -1,11 +1,13 @@
import { BrowserWindow, ipcMain, WebContents } from 'electron/main';
import { expect } from 'chai';
import { afterAll, beforeAll, describe, it } from 'vitest';
import { once } from 'node:events';
import * as path from 'node:path';
import { defer } from './lib/spec-helpers';
import { defer, dangerouslyIgnoreWebContentsLoadResult } from './lib/spec-helpers';
import { closeWindow } from './lib/window-helpers';
describe('webFrame module', () => {
const fixtures = path.resolve(__dirname, 'fixtures');
@@ -19,9 +21,9 @@ describe('webFrame module', () => {
preload: path.join(fixtures, 'pages', 'world-safe-preload.js')
}
});
defer(() => w.close());
defer(() => closeWindow(w));
const isSafe = once(ipcMain, 'executejs-safe');
w.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
const [, wasSafe] = await isSafe;
expect(wasSafe).to.equal(true);
});
@@ -35,9 +37,9 @@ describe('webFrame module', () => {
preload: path.join(fixtures, 'pages', 'world-safe-preload-error.js')
}
});
defer(() => w.close());
defer(() => closeWindow(w));
const execError = once(ipcMain, 'executejs-safe');
w.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
const [, error] = await execError;
expect(error).to.not.equal(null, 'Error should not be null');
expect(error).to.have.property('message', 'Uncaught Error: An object could not be cloned.');
@@ -51,7 +53,7 @@ describe('webFrame module', () => {
contextIsolation: false
}
});
defer(() => w.close());
defer(() => closeWindow(w));
await w.loadFile(path.join(fixtures, 'pages', 'webframe-spell-check.html'));
w.focus();
await w.webContents.executeJavaScript('document.querySelector("input").focus()', true);
@@ -77,7 +79,7 @@ describe('webFrame module', () => {
describe('api', () => {
let w: WebContents;
let win: BrowserWindow;
before(async () => {
beforeAll(async () => {
win = new BrowserWindow({ show: false, webPreferences: { contextIsolation: false, nodeIntegration: true } });
await win.loadURL('data:text/html,<iframe name="test"></iframe>');
w = win.webContents;
@@ -89,8 +91,8 @@ describe('webFrame module', () => {
`);
});
after(() => {
win.close();
afterAll(async () => {
await closeWindow(win);
win = null as unknown as BrowserWindow;
});

View File

@@ -1,6 +1,7 @@
import { ipcMain, net, protocol, session, WebContents, webContents } from 'electron/main';
import { expect } from 'chai';
import { afterAll, afterEach, beforeAll, describe, it } from 'vitest';
import * as WebSocket from 'ws';
import { once } from 'node:events';
@@ -13,7 +14,7 @@ import * as qs from 'node:querystring';
import { ReadableStream } from 'node:stream/web';
import * as url from 'node:url';
import { listen, defer, startRemoteControlApp } from './lib/spec-helpers';
import { listen, defer, startRemoteControlApp, dangerouslyIgnoreWebContentsLoadResult } from './lib/spec-helpers';
const fixturesPath = path.resolve(__dirname, 'fixtures');
@@ -60,14 +61,14 @@ describe('webRequest module', () => {
}
);
before(async () => {
beforeAll(async () => {
protocol.registerStringProtocol('cors', (req, cb) => cb(''));
defaultURL = (await listen(server)).url + '/';
http2URL = (await listen(h2server)).url + '/';
console.log(http2URL);
});
after(() => {
afterAll(() => {
server.close();
h2server.close();
protocol.unregisterProtocol('cors');
@@ -75,13 +76,13 @@ describe('webRequest module', () => {
let contents: WebContents;
// NB. sandbox: true is used because it makes navigations much (~8x) faster.
before(async () => {
beforeAll(async () => {
contents = (webContents as typeof ElectronInternal.WebContents).create({ sandbox: true });
// const w = new BrowserWindow({webPreferences: {sandbox: true}})
// contents = w.webContents
await contents.loadFile(path.join(fixturesPath, 'pages', 'fetch.html'));
});
after(() => contents.destroy());
afterAll(() => contents.destroy());
async function ajax(url: string, options = {}) {
return contents.executeJavaScript(`ajax("${url}", ${JSON.stringify(options)})`);
@@ -895,7 +896,9 @@ describe('webRequest module', () => {
ses.webRequest.onCompleted(null);
});
contents.loadFile(path.join(fixturesPath, 'api', 'webrequest.html'), { query: { port: `${port}` } });
dangerouslyIgnoreWebContentsLoadResult(
contents.loadFile(path.join(fixturesPath, 'api', 'webrequest.html'), { query: { port: `${port}` } })
);
await once(ipcMain, 'websocket-success');
expect(receivedHeaders['/websocket'].Upgrade[0]).to.equal('websocket');

View File

@@ -1,10 +1,12 @@
import { BrowserWindow } from 'electron/main';
import { expect } from 'chai';
import { describe, it } from 'vitest';
import * as path from 'node:path';
import { defer } from './lib/spec-helpers';
import { closeWindow } from './lib/window-helpers';
// import { once } from 'node:events';
@@ -21,7 +23,7 @@ describe('webUtils module', () => {
sandbox: false
}
});
defer(() => w.close());
defer(() => closeWindow(w));
await w.loadFile(path.resolve(fixtures, 'pages', 'file-input.html'));
const pathFromWebUtils = await w.webContents.executeJavaScript(
'require("electron").webUtils.getPathForFile(new Blob([1, 2, 3]))'
@@ -38,7 +40,7 @@ describe('webUtils module', () => {
sandbox: false
}
});
defer(() => w.close());
defer(() => closeWindow(w));
await w.loadFile(path.resolve(fixtures, 'pages', 'file-input.html'));
const { debugger: debug } = w.webContents;
debug.attach();

View File

@@ -3,6 +3,7 @@ import { flipFuses, FuseV1Config, FuseV1Options, FuseVersion } from '@electron/f
import { resedit } from '@electron/packager/dist/resedit';
import { expect } from 'chai';
import { afterEach, beforeEach, describe, it } from 'vitest';
import * as cp from 'node:child_process';
import * as nodeCrypto from 'node:crypto';
@@ -60,9 +61,7 @@ const expectToHaveCrashed = (res: SpawnResult) => {
}
};
describe('fuses', function () {
this.timeout(120000);
describe('fuses', { timeout: 120000 }, () => {
let tmpDir: string;
let appPath: string;

View File

@@ -1,14 +1,21 @@
import { BrowserWindow, ipcMain } from 'electron/main';
import { expect } from 'chai';
import { afterEach, beforeEach, describe, it } from 'vitest';
import { once } from 'node:events';
import * as importedFs from 'node:fs';
import * as path from 'node:path';
import * as url from 'node:url';
import { Worker } from 'node:worker_threads';
import { getRemoteContext, ifdescribe, ifit, itremote, useRemoteContext } from './lib/spec-helpers';
import { expect, fs, path, url } from './lib/remote-tools';
import {
defer,
getRemoteContext,
ifdescribe,
ifit,
itremote,
useRemoteContext,
withDone,
dangerouslyIgnoreWebContentsLoadResult
} from './lib/spec-helpers';
import { closeAllWindows } from './lib/window-helpers';
describe('asar package', () => {
@@ -18,8 +25,8 @@ describe('asar package', () => {
afterEach(closeAllWindows);
describe('asar protocol', () => {
it('sets __dirname correctly', async function () {
after(function () {
it('sets __dirname correctly', async () => {
defer(() => {
ipcMain.removeAllListeners('dirname');
});
@@ -34,13 +41,13 @@ describe('asar package', () => {
});
const p = path.resolve(asarDir, 'web.asar', 'index.html');
const dirnameEvent = once(ipcMain, 'dirname');
w.loadFile(p);
dangerouslyIgnoreWebContentsLoadResult(w.loadFile(p));
const [, dirname] = await dirnameEvent;
expect(dirname).to.equal(path.dirname(p));
});
it('loads script tag in html', async function () {
after(function () {
it('loads script tag in html', async () => {
defer(() => {
ipcMain.removeAllListeners('ping');
});
@@ -55,15 +62,13 @@ describe('asar package', () => {
});
const p = path.resolve(asarDir, 'script.asar', 'index.html');
const ping = once(ipcMain, 'ping');
w.loadFile(p);
dangerouslyIgnoreWebContentsLoadResult(w.loadFile(p));
const [, message] = await ping;
expect(message).to.equal('pong');
});
it('loads video tag in html', async function () {
this.timeout(60000);
after(function () {
it('loads video tag in html', { timeout: 60000 }, async () => {
defer(() => {
ipcMain.removeAllListeners('asar-video');
});
@@ -77,7 +82,7 @@ describe('asar package', () => {
}
});
const p = path.resolve(asarDir, 'video.asar', 'index.html');
w.loadFile(p);
dangerouslyIgnoreWebContentsLoadResult(w.loadFile(p));
const [, message, error] = await once(ipcMain, 'asar-video');
if (message === 'ended') {
expect(error).to.be.null();
@@ -117,18 +122,21 @@ describe('asar package', () => {
describe('worker threads', function () {
// DISABLED-FIXME(#38192): only disabled for ASan.
ifit(!process.env.IS_ASAN)('should start worker thread from asar file', function (callback) {
const p = path.join(asarDir, 'worker_threads.asar', 'worker.js');
const w = new Worker(p);
ifit(!process.env.IS_ASAN)(
'should start worker thread from asar file',
withDone((done) => {
const p = path.join(asarDir, 'worker_threads.asar', 'worker.js');
const w = new Worker(p);
w.on('error', (err) => callback(err));
w.on('message', (message) => {
expect(message).to.equal('ping');
w.terminate();
w.on('error', (err) => done(err));
w.on('message', (message) => {
expect(message).to.equal('ping');
w.terminate();
callback(null);
});
});
done();
});
})
);
});
});
@@ -145,7 +153,6 @@ function promisify(_f: Function): any {
describe('asar package', function () {
const fixtures = path.join(__dirname, 'fixtures');
const asarDir = path.join(fixtures, 'test.asar');
const fs = require('node:fs') as typeof importedFs; // dummy, to fool typescript
useRemoteContext({
url: url.pathToFileURL(path.join(fixtures, 'pages', 'blank.html')),
@@ -1171,31 +1178,37 @@ describe('asar package', function () {
expect(err.code).to.equal('ENOENT');
});
it('handles null for options', function (done) {
const p = path.join(asarDir, 'a.asar', 'dir1');
fs.readdir(p, null, function (err, dirs) {
try {
expect(err).to.be.null();
expect(dirs).to.deep.equal(['file1', 'file2', 'file3', 'link1', 'link2']);
done();
} catch (e) {
done(e);
}
});
});
it(
'handles null for options',
withDone((done) => {
const p = path.join(asarDir, 'a.asar', 'dir1');
fs.readdir(p, null, function (err, dirs) {
try {
expect(err).to.be.null();
expect(dirs).to.deep.equal(['file1', 'file2', 'file3', 'link1', 'link2']);
done();
} catch (e) {
done(e);
}
});
})
);
it('handles undefined for options', function (done) {
const p = path.join(asarDir, 'a.asar', 'dir1');
fs.readdir(p, undefined, function (err, dirs) {
try {
expect(err).to.be.null();
expect(dirs).to.deep.equal(['file1', 'file2', 'file3', 'link1', 'link2']);
done();
} catch (e) {
done(e);
}
});
});
it(
'handles undefined for options',
withDone((done) => {
const p = path.join(asarDir, 'a.asar', 'dir1');
fs.readdir(p, undefined, function (err, dirs) {
try {
expect(err).to.be.null();
expect(dirs).to.deep.equal(['file1', 'file2', 'file3', 'link1', 'link2']);
done();
} catch (e) {
done(e);
}
});
})
);
});
describe('fs.promises.readdir', function () {
@@ -1707,7 +1720,7 @@ describe('asar package', function () {
/*
describe('process.env.ELECTRON_NO_ASAR', function () {
itremote('disables asar support in forked processes', function (done) {
itremote('disables asar support in forked processes', withDone((done) => {
const forked = ChildProcess.fork(path.join(__dirname, 'fixtures', 'module', 'no-asar.js'), [], {
env: {
ELECTRON_NO_ASAR: true
@@ -1722,9 +1735,9 @@ describe('asar package', function () {
done(e);
}
});
});
}));
itremote('disables asar support in spawned processes', function (done) {
itremote('disables asar support in spawned processes', withDone((done) => {
const spawned = ChildProcess.spawn(process.execPath, [path.join(__dirname, 'fixtures', 'module', 'no-asar.js')], {
env: {
ELECTRON_NO_ASAR: true,
@@ -1746,7 +1759,7 @@ describe('asar package', function () {
done(e);
}
});
});
}));
});
*/
});

View File

@@ -1,6 +1,7 @@
import { BrowserWindow } from 'electron';
import { expect } from 'chai';
import { afterEach, describe, it } from 'vitest';
import * as path from 'node:path';
import { setTimeout } from 'node:timers/promises';

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,5 @@
import { expect } from 'chai';
import { describe, it } from 'vitest';
import { once } from 'node:events';
import * as path from 'node:path';

View File

@@ -1,7 +1,9 @@
import { expect } from 'chai';
import { afterEach, describe } from 'vitest';
import * as cp from 'node:child_process';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { ifit, waitUntil } from './lib/spec-helpers';
@@ -9,10 +11,16 @@ import { ifit, waitUntil } from './lib/spec-helpers';
const fixturePath = path.resolve(__dirname, 'fixtures', 'crash-cases');
let children: cp.ChildProcessWithoutNullStreams[] = [];
const userDataDirs: string[] = [];
const runFixtureAndEnsureCleanExit = async (args: string[], customEnv: NodeJS.ProcessEnv) => {
let out = '';
const child = cp.spawn(process.execPath, args, {
// Give each fixture child its own profile so parallel workers (and the
// multiple crash cases within this worker) don't contend on the default
// Chromium userData singleton lock.
const userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'electron-crash-case-'));
userDataDirs.push(userDataDir);
const child = cp.spawn(process.execPath, [...args, `--user-data-dir=${userDataDir}`], {
env: {
...process.env,
...customEnv
@@ -62,12 +70,15 @@ const shouldRunCase = (crashCase: string) => {
};
describe('crash cases', () => {
afterEach(async () => {
afterEach(async (ctx) => {
for (const child of children) {
child.kill();
}
await waitUntil(() => children.length === 0);
await waitUntil(() => children.length === 0, ctx.signal);
children.length = 0;
for (const dir of userDataDirs.splice(0, userDataDirs.length)) {
fs.rmSync(dir, { recursive: true, force: true });
}
});
const cases = fs.readdirSync(fixturePath);

View File

@@ -1,4 +1,5 @@
import { expect } from 'chai';
import { afterEach, beforeEach, describe, it } from 'vitest';
import * as deprecate from '../lib/common/deprecate';

View File

@@ -1,6 +1,7 @@
import { BrowserWindow } from 'electron';
import { expect } from 'chai';
import { afterEach, beforeEach, describe, it } from 'vitest';
import * as cp from 'node:child_process';
import * as fs from 'node:fs';

View File

@@ -12,6 +12,7 @@ import {
} from 'electron/main';
import { expect } from 'chai';
import { afterAll, afterEach, beforeAll, beforeEach, describe, it } from 'vitest';
import * as WebSocket from 'ws';
import { spawn } from 'node:child_process';
@@ -21,7 +22,13 @@ import * as http from 'node:http';
import * as path from 'node:path';
import { emittedNTimes, emittedUntil } from './lib/events-helpers';
import { ifit, listen, startRemoteControlApp, waitUntil } from './lib/spec-helpers';
import {
ifit,
listen,
startRemoteControlApp,
waitUntil,
dangerouslyIgnoreWebContentsLoadResult
} from './lib/spec-helpers';
import { expectWarningMessages } from './lib/warning-helpers';
import { closeAllWindows, closeWindow, cleanupWebContents } from './lib/window-helpers';
@@ -37,7 +44,7 @@ describe('chrome extensions', () => {
let url: string;
let port: number;
let wss: WebSocket.Server;
before(async () => {
beforeAll(async () => {
server = http.createServer((req, res) => {
if (req.url === '/cors') {
res.setHeader('Access-Control-Allow-Origin', 'http://example.com');
@@ -56,7 +63,7 @@ describe('chrome extensions', () => {
({ port, url } = await listen(server));
});
after(async () => {
afterAll(async () => {
server.close();
wss.close();
await cleanupWebContents();
@@ -417,10 +424,10 @@ describe('chrome extensions', () => {
return false;
}
it('can cancel http requests', async () => {
it('can cancel http requests', async (ctx) => {
await w.loadURL(url);
await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'chrome-webRequest'));
await expect(waitUntil(haveRejectedFetch)).to.eventually.be.fulfilled();
await expect(waitUntil(haveRejectedFetch, ctx.signal)).to.eventually.be.fulfilled();
});
it('does not cancel http requests when no extension loaded', async () => {
@@ -431,6 +438,9 @@ describe('chrome extensions', () => {
it('does not take precedence over Electron webRequest - http', async () => {
return new Promise<void>((resolve) => {
// onBeforeRequest fires (and cancels) on the navigation itself, so the
// outer promise resolves before loadURL settles; the IIFE then rejects
// with ERR_FAILED.
(async () => {
customSession.webRequest.onBeforeRequest((details, callback) => {
resolve();
@@ -440,7 +450,7 @@ describe('chrome extensions', () => {
await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'chrome-webRequest'));
fetch(w.webContents, url);
})();
})().catch(() => {});
});
});
@@ -452,7 +462,7 @@ describe('chrome extensions', () => {
});
await w.loadFile(path.join(fixtures, 'api', 'webrequest.html'), { query: { port: `${port}` } });
await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'chrome-webRequest-wss'));
})();
})().catch(() => {});
});
});
@@ -471,7 +481,7 @@ describe('chrome extensions', () => {
describe('chrome.tabs', () => {
let customSession: Session;
before(async () => {
beforeAll(async () => {
customSession = session.fromPartition(`persist:${uuid.v4()}`);
await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'chrome-api'));
});
@@ -551,7 +561,7 @@ describe('chrome extensions', () => {
webPreferences: { session: customSession, nodeIntegration: true, contextIsolation: false }
});
try {
w.loadURL(url);
dangerouslyIgnoreWebContentsLoadResult(w.loadURL(url));
const [, resp] = await once(ipcMain, 'bg-page-message-response');
expect(resp.message).to.deep.equal({ some: 'message' });
expect(resp.sender.id).to.be.a('string');
@@ -719,12 +729,12 @@ describe('chrome extensions', () => {
const result = await w.webContents.executeJavaScript('document.documentElement.style.backgroundColor');
expect(result).to.equal('red');
});
w.loadURL(url);
dangerouslyIgnoreWebContentsLoadResult(w.loadURL(url));
});
it('should run content script at document_idle', async () => {
await addExtension('content-script-document-idle');
w.loadURL(url);
dangerouslyIgnoreWebContentsLoadResult(w.loadURL(url));
const result = await w.webContents.executeJavaScript('document.body.style.backgroundColor');
expect(result).to.equal('red');
});
@@ -735,7 +745,7 @@ describe('chrome extensions', () => {
const result = await w.webContents.executeJavaScript('document.documentElement.style.backgroundColor');
expect(result).to.equal('red');
});
w.loadURL(url);
dangerouslyIgnoreWebContentsLoadResult(w.loadURL(url));
});
});
@@ -750,7 +760,7 @@ describe('chrome extensions', () => {
let server: http.Server;
let port: number;
before(async () => {
beforeAll(async () => {
server = http.createServer(async (_, res) => {
try {
const content = await fs.readFile(contentPath, 'utf-8');
@@ -767,7 +777,7 @@ describe('chrome extensions', () => {
session.defaultSession.extensions.loadExtension(contentScript);
});
after(() => {
afterAll(() => {
session.defaultSession.extensions.removeExtension('content-script-test');
server.close();
});
@@ -792,7 +802,7 @@ describe('chrome extensions', () => {
it('applies matching rules in subframes', async () => {
const detailsPromise = emittedNTimes(w.webContents, 'did-frame-finish-load', 2);
w.loadURL(`http://127.0.0.1:${port}`);
dangerouslyIgnoreWebContentsLoadResult(w.loadURL(`http://127.0.0.1:${port}`));
const frameEvents = await detailsPromise;
await Promise.all(
frameEvents.map(async (frameEvent) => {
@@ -991,7 +1001,7 @@ describe('chrome extensions', () => {
let customSession: Session;
let w = null as unknown as BrowserWindow;
before(async () => {
beforeAll(async () => {
customSession = session.fromPartition(`persist:${uuid.v4()}`);
await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'chrome-i18n', 'v3'));
});
@@ -1083,7 +1093,7 @@ describe('chrome extensions', () => {
let customSession: Session;
let w = null as unknown as BrowserWindow;
before(async () => {
beforeAll(async () => {
customSession = session.fromPartition(`persist:${uuid.v4()}`);
await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'chrome-action-fail'));
});
@@ -1140,7 +1150,7 @@ describe('chrome extensions', () => {
let customSession: Session;
let w = null as unknown as BrowserWindow;
before(async () => {
beforeAll(async () => {
customSession = session.fromPartition(`persist:${uuid.v4()}`);
await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'chrome-tabs', 'api-async'));
});
@@ -1423,7 +1433,7 @@ describe('chrome extensions', () => {
let customSession: Session;
let w = null as unknown as BrowserWindow;
before(async () => {
beforeAll(async () => {
customSession = session.fromPartition(`persist:${uuid.v4()}`);
await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'chrome-scripting'));
});
@@ -1507,7 +1517,7 @@ describe('chrome extensions', () => {
let driver: BrowserWindow;
let victim: BrowserWindow;
before(async () => {
beforeAll(async () => {
extSession = session.fromPartition(`persist:${uuid.v4()}`);
otherSession = session.fromPartition(`persist:${uuid.v4()}`);
await extSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'tabs-cross-session'));

View File

@@ -2,7 +2,8 @@
<body>
<script type="text/javascript" charset="utf-8">
window.addEventListener('unload', function (e) {
require('node:fs').writeFileSync(__dirname + '/close', 'close');
const out = new URLSearchParams(location.search).get('out');
require('node:fs').writeFileSync(out, 'close');
}, false);
window.onload = () => window.close();
</script>

View File

@@ -2,7 +2,8 @@
<body>
<script type="text/javascript" charset="utf-8">
window.addEventListener('unload', function (e) {
require('node:fs').writeFileSync(__dirname + '/unload', 'unload');
const out = new URLSearchParams(location.search).get('out');
require('node:fs').writeFileSync(out, 'unload');
}, false);
</script>
</body>

View File

@@ -18,12 +18,31 @@ v8.setFlagsFromString('--expose_gc');
chai_1.use(require('chai-as-promised'));
chai_1.use(require('dirty-chai'));
// Mirror of spec/lib/remote-tools.ts for closures rewritten by
// rewriteForRemoteEval() — each __vite_ssr_import_N__ becomes __rt.
const __rt = {
...net_helpers_1,
...main_1,
expect: chai_1.expect,
once: node_events_1.once,
setTimeout: promises_1.setTimeout,
defer: require('../../../lib/defer-helpers').defer,
path: require('node:path'),
fs: require('node:fs'),
url,
http
};
function fail(message) {
process.parentPort.postMessage({ ok: false, message });
}
process.parentPort.on('message', async (e) => {
// Equivalent of beforeEach in spec/api-net-spec.ts
if (e.data?.type === 'shutdown') {
process.exit(0);
}
// Equivalent of beforeEach in spec/api-net.spec.ts
net_helpers_1.respondNTimes.routeFailure = false;
try {
@@ -37,18 +56,19 @@ process.parentPort.on('message', async (e) => {
await eval(e.data.fn);
} catch (err) {
fail(`${err}`);
process.exit(1);
return;
}
// Equivalent of afterEach in spec/api-net-spec.ts
// Equivalent of afterEach in spec/api-net.spec.ts
if (net_helpers_1.respondNTimes.routeFailure) {
fail(
'Failing this test due an unhandled error in the respondOnce route handler, check the logs above for the actual error'
);
process.exit(1);
return;
}
// Test passed
process.parentPort.postMessage({ ok: true });
process.exit(0);
});
process.parentPort.postMessage({ type: 'ready' });

View File

@@ -1,9 +1,9 @@
<html>
<body>
<!-- Use mocha which has a large enough js file -->
<script src="mocha.js"></script>
<!-- Use chai which has a large enough js file -->
<script src="chai.js"></script>
<script>
mocha.setup('bdd');
void chai;
</script>
</body>
</html>

View File

@@ -25,8 +25,8 @@ app.once('ready', async () => {
protocol.handle('atom', (request) => {
let { pathname } = new URL(request.url);
if (pathname === '/mocha.js') {
pathname = path.resolve(__dirname, '../../../node_modules/mocha/mocha.js');
if (pathname === '/chai.js') {
pathname = path.resolve(__dirname, '../../../node_modules/chai/chai.js');
} else {
pathname = path.join(__dirname, pathname);
}

View File

@@ -0,0 +1,14 @@
import * as esmElectron from 'electron';
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const cjsElectron = require('electron');
process.stdout.write(
JSON.stringify({
esm: Object.keys(esmElectron).sort(),
cjs: Object.keys(cjsElectron).sort()
})
);
process.exit(0);

View File

@@ -1,11 +1,11 @@
import { BrowserWindow } from 'electron';
import { expect } from 'chai';
import { describe, it } from 'vitest';
import { spawn, spawnSync } from 'node:child_process';
import { once } from 'node:events';
import path = require('node:path');
import { BrowserWindow } from './lib/remote-tools';
import { startRemoteControlApp } from './lib/spec-helpers';
describe('fuses', () => {

View File

@@ -1,12 +0,0 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
export async function getFiles(
dir: string,
test: (file: string) => boolean = (_: string) => true // eslint-disable-line @typescript-eslint/no-unused-vars
): Promise<string[]> {
return fs.promises
.readdir(dir)
.then((files) => files.map((file) => path.join(dir, file)))
.then((files) => files.filter((file) => test(file)));
}

View File

@@ -1,13 +1,14 @@
import { BrowserWindow, screen } from 'electron';
import { expect, assert } from 'chai';
import { afterAll, afterEach, beforeAll, beforeEach, describe, it } from 'vitest';
import { once } from 'node:events';
import * as http from 'node:http';
import * as nodePath from 'node:path';
import { HexColors, ScreenCapture, hasCapturableScreen } from './lib/screen-helpers';
import { ifit, listen } from './lib/spec-helpers';
import { ifit, listen, withDone, dangerouslyIgnoreWebContentsLoadResult } from './lib/spec-helpers';
import { closeAllWindows } from './lib/window-helpers';
describe('webContents.setWindowOpenHandler', () => {
@@ -20,34 +21,37 @@ describe('webContents.setWindowOpenHandler', () => {
afterEach(closeAllWindows);
it('does not fire window creation events if the handler callback throws an error', (done) => {
const error = new Error('oh no');
const listeners = process.listeners('uncaughtException');
process.removeAllListeners('uncaughtException');
process.on('uncaughtException', (thrown) => {
try {
expect(thrown).to.equal(error);
done();
} catch (e) {
done(e);
} finally {
process.removeAllListeners('uncaughtException');
for (const listener of listeners) {
process.on('uncaughtException', listener);
it(
'does not fire window creation events if the handler callback throws an error',
withDone((done) => {
const error = new Error('oh no');
const listeners = process.listeners('uncaughtException');
process.removeAllListeners('uncaughtException');
process.on('uncaughtException', (thrown) => {
try {
expect(thrown).to.equal(error);
done();
} catch (e) {
done(e);
} finally {
process.removeAllListeners('uncaughtException');
for (const listener of listeners) {
process.on('uncaughtException', listener);
}
}
}
});
});
browserWindow.webContents.on('did-create-window', () => {
assert.fail('did-create-window should not be called with an overridden window.open');
});
browserWindow.webContents.on('did-create-window', () => {
assert.fail('did-create-window should not be called with an overridden window.open');
});
browserWindow.webContents.executeJavaScript("window.open('about:blank', '', 'show=no') && true");
browserWindow.webContents.executeJavaScript("window.open('about:blank', '', 'show=no') && true");
browserWindow.webContents.setWindowOpenHandler(() => {
throw error;
});
});
browserWindow.webContents.setWindowOpenHandler(() => {
throw error;
});
})
);
it('does not fire window creation events if the handler callback returns a bad result', async () => {
const bad = new Promise((resolve) => {
@@ -171,13 +175,15 @@ describe('webContents.setWindowOpenHandler', () => {
return { action: 'deny' };
});
browserWindow.webContents.loadURL(
`data:text/html,${encodeURIComponent(`
dangerouslyIgnoreWebContentsLoadResult(
browserWindow.webContents.loadURL(
`data:text/html,${encodeURIComponent(`
<form action="http://example.com" target="_blank" method="POST" id="form">
<input name="key" value="value"></input>
</form>
<script>form.submit()</script>
`)}`
)
);
});
const { url, frameName, features, disposition, referrer, postBody } = details;
@@ -391,7 +397,7 @@ describe('webContents.setWindowOpenHandler', () => {
let server: http.Server;
let url: string;
before(async () => {
beforeAll(async () => {
server = http.createServer((request, response) => {
switch (request.url) {
case '/index':
@@ -414,7 +420,7 @@ describe('webContents.setWindowOpenHandler', () => {
url = (await listen(server)).url;
});
after(() => {
afterAll(() => {
server.close();
});

View File

@@ -1,210 +0,0 @@
const { app, protocol } = require('electron');
const fs = require('node:fs');
const path = require('node:path');
const v8 = require('node:v8');
const FAILURE_STATUS_KEY = 'Electron_Spec_Runner_Failures';
// We want to terminate on errors, not throw up a dialog
process.on('uncaughtException', (err) => {
console.error('Unhandled exception in main spec runner:', err);
process.exit(1);
});
// Tell ts-node which tsconfig to use
process.env.TS_NODE_PROJECT = path.resolve(__dirname, '../tsconfig.spec.json');
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = 'true';
// Some Linux machines have broken hardware acceleration support.
if (process.env.ELECTRON_TEST_DISABLE_HARDWARE_ACCELERATION) {
app.disableHardwareAcceleration();
}
v8.setFlagsFromString('--expose_gc');
app.commandLine.appendSwitch('js-flags', '--expose_gc');
// Prevent the spec runner quitting when the first window closes
app.on('window-all-closed', () => null);
// Use fake device for Media Stream to replace actual camera and microphone.
app.commandLine.appendSwitch('use-fake-device-for-media-stream');
app.commandLine.appendSwitch(
'host-resolver-rules',
[
'MAP localhost2 127.0.0.1',
'MAP ipv4.localhost2 10.0.0.1',
'MAP ipv6.localhost2 [::1]',
'MAP notfound.localhost2 ~NOTFOUND'
].join(', ')
);
// Enable features required by tests.
app.commandLine.appendSwitch(
'enable-features',
[
// spec/api-web-frame-main-spec.ts
'DocumentPolicyIncludeJSCallStacksInCrashReports',
// spec/spellchecker-spec.ts - allows spellcheck without user gesture
// https://chromium-review.googlesource.com/c/chromium/src/+/7452579
'UnrestrictSpellingAndGrammarForTesting'
].join(',')
);
global.standardScheme = 'app';
global.zoomScheme = 'zoom';
global.serviceWorkerScheme = 'sw';
protocol.registerSchemesAsPrivileged([
{ scheme: global.standardScheme, privileges: { standard: true, secure: true, stream: false } },
{ scheme: global.zoomScheme, privileges: { standard: true, secure: true } },
{ scheme: global.serviceWorkerScheme, privileges: { allowServiceWorkers: true, standard: true, secure: true } },
{ scheme: 'http-like', privileges: { standard: true, secure: true, corsEnabled: true, supportFetchAPI: true } },
{ scheme: 'cors-blob', privileges: { corsEnabled: true, supportFetchAPI: true } },
{ scheme: 'cors', privileges: { corsEnabled: true, supportFetchAPI: true } },
{ scheme: 'no-cors', privileges: { supportFetchAPI: true } },
{ scheme: 'no-fetch', privileges: { corsEnabled: true } },
{ scheme: 'stream', privileges: { standard: true, stream: true } },
{ scheme: 'foo', privileges: { standard: true } },
{ scheme: 'bar', privileges: { standard: true } }
]);
app
.whenReady()
.then(async () => {
require('ts-node/register');
const argv = require('yargs')
.boolean('ci')
.array('files')
.string('g')
.alias('g', 'grep')
.boolean('i')
.alias('i', 'invert').argv;
const Mocha = require('mocha');
const mochaOptions = {
forbidOnly: process.env.CI
};
if (process.env.CI) {
mochaOptions.retries = 3;
}
if (process.env.MOCHA_REPORTER) {
mochaOptions.reporter = process.env.MOCHA_REPORTER;
}
if (process.env.MOCHA_MULTI_REPORTERS) {
mochaOptions.reporterOptions = {
reporterEnabled: process.env.MOCHA_MULTI_REPORTERS
};
}
// The MOCHA_GREP and MOCHA_INVERT are used in some vendor builds for sharding
// tests.
if (process.env.MOCHA_GREP) {
mochaOptions.grep = process.env.MOCHA_GREP;
}
if (process.env.MOCHA_INVERT) {
mochaOptions.invert = process.env.MOCHA_INVERT === 'true';
}
const mocha = new Mocha(mochaOptions);
// Add a root hook on mocha to skip any tests that are disabled
const disabledTests = new Set(JSON.parse(fs.readFileSync(path.join(__dirname, 'disabled-tests.json'), 'utf8')));
mocha.suite.beforeEach(function () {
// TODO(clavin): add support for disabling *suites* by title, not just tests
if (disabledTests.has(this.currentTest?.fullTitle())) {
this.skip();
}
});
// The cleanup method is registered this way rather than through an
// `afterEach` at the top level so that it can run before other `afterEach`
// methods.
//
// The order of events is:
// 1. test completes,
// 2. `defer()`-ed methods run, in reverse order,
// 3. regular `afterEach` hooks run.
const { runCleanupFunctions } = require('./lib/spec-helpers');
mocha.suite.on('suite', function attach(suite) {
suite.afterEach('cleanup', runCleanupFunctions);
suite.on('suite', attach);
});
if (!process.env.MOCHA_REPORTER) {
mocha.ui('bdd').reporter('tap');
}
const mochaTimeout = process.env.MOCHA_TIMEOUT || 30000;
mocha.timeout(mochaTimeout);
if (argv.grep) mocha.grep(argv.grep);
if (argv.invert) mocha.invert();
const baseElectronDir = path.resolve(__dirname, '..');
const validTestPaths =
argv.files &&
argv.files.map((file) => (path.isAbsolute(file) ? path.relative(baseElectronDir, file) : path.normalize(file)));
const filter = (file) => {
if (!/-spec\.[tj]s$/.test(file)) {
return false;
}
// This allows you to run specific modules only:
// npm run test -match=menu
const moduleMatch = process.env.npm_config_match ? new RegExp(process.env.npm_config_match, 'g') : null;
if (moduleMatch && !moduleMatch.test(file)) {
return false;
}
if (validTestPaths && !validTestPaths.includes(path.relative(baseElectronDir, file))) {
return false;
}
return true;
};
const { getFiles } = require('./get-files');
const testFiles = await getFiles(__dirname, filter);
for (const file of testFiles.sort()) {
mocha.addFile(file);
}
if (validTestPaths && validTestPaths.length > 0 && testFiles.length === 0) {
console.error('Test files were provided, but they did not match any searched files');
console.error('provided file paths (relative to electron/):', validTestPaths);
process.exit(1);
}
const cb = () => {
// Ensure the callback is called after runner is defined
process.nextTick(() => {
if (process.env.ELECTRON_FORCE_TEST_SUITE_EXIT === 'true') {
console.log(`${FAILURE_STATUS_KEY}: ${runner.failures}`);
process.kill(process.pid);
} else {
process.exit(runner.failures);
}
});
};
// Set up chai in the correct order
const chai = require('chai');
chai.use(require('chai-as-promised'));
chai.use(require('dirty-chai'));
// Show full object diff
// https://github.com/chaijs/chai/issues/469
chai.config.truncateThreshold = 0;
const runner = mocha.run(cb);
const RETRY_EVENT = Mocha?.Runner?.constants?.EVENT_TEST_RETRY || 'retry';
runner.on(RETRY_EVENT, (test, err) => {
console.log(`Failure in test: "${test.fullTitle()}"`);
if (err?.stack) console.log(err.stack.split('\n').slice(0, 3).join('\n'));
console.log(`Retrying test (${test.currentRetry() + 1}/${test.retries()})...`);
});
})
.catch((err) => {
console.error('An error occurred while running the spec runner');
console.error(err);
process.exit(1);
});

View File

@@ -26,7 +26,7 @@ export function getCodesignIdentity() {
export async function copyMacOSFixtureApp(newDir: string, fixture: string | null = 'initial') {
const appBundlePath = path.resolve(process.execPath, '../../..');
const newPath = path.resolve(newDir, 'Electron.app');
cp.spawnSync('cp', ['-R', appBundlePath, path.dirname(newPath)]);
cp.spawnSync('cp', ['-cR', appBundlePath, path.dirname(newPath)]);
if (fixture) {
const appDir = path.resolve(newPath, 'Contents/Resources/app');
await fs.promises.mkdir(appDir, { recursive: true });

25
spec/lib/defer-helpers.ts Normal file
View File

@@ -0,0 +1,25 @@
type CleanupFunction = (() => void) | (() => Promise<void>);
const cleanupFunctions: CleanupFunction[] = [];
export async function runCleanupFunctions() {
// Drain before running so a throwing cleanup can't leave stale entries to
// be re-run on the next test.
const pending = cleanupFunctions.splice(0, cleanupFunctions.length);
const errors: unknown[] = [];
for (const cleanup of pending) {
try {
const r = cleanup();
if (r instanceof Promise) {
await r;
}
} catch (err) {
errors.push(err);
}
}
if (errors.length === 1) throw errors[0];
if (errors.length > 1) throw new AggregateError(errors, `${errors.length} defer() cleanups failed`);
}
export function defer(f: CleanupFunction) {
cleanupFunctions.unshift(f);
}

View File

@@ -2,9 +2,20 @@ import { expect } from 'chai';
import * as dns from 'node:dns';
import * as http from 'node:http';
import { Socket } from 'node:net';
import * as http2 from 'node:http2';
import * as https from 'node:https';
import { AddressInfo, Socket } from 'node:net';
import * as url from 'node:url';
import { defer, listen } from './spec-helpers';
import { defer } from './defer-helpers';
export async function listen(server: http.Server | https.Server | http2.Http2SecureServer) {
const hostname = '127.0.0.1';
await new Promise<void>((resolve) => server.listen(0, hostname, () => resolve()));
const { port } = server.address() as AddressInfo;
const protocol = server instanceof http.Server ? 'http' : 'https';
return { port, hostname, url: url.format({ protocol, hostname, port }) };
}
// See https://github.com/nodejs/node/issues/40702.
dns.setDefaultResultOrder('ipv4first');

62
spec/lib/remote-tools.ts Normal file
View File

@@ -0,0 +1,62 @@
// Anything an itremote()/remotely() closure references via `import` must come
// from here. vite's SSR transform rewrites the spec file's import of this
// module to `__vite_ssr_import_N__`, so `path.join(...)` in a closure becomes
// `__vite_ssr_import_N__.path.join(...)`. runRemote()/remotely() replace every
// `__vite_ssr_import_\d+__` with `__rt`, a renderer-side object whose keys
// mirror these export names exactly — so `__rt.path.join(...)` resolves
// correctly with no property-name guessing.
export * as path from 'node:path';
export * as fs from 'node:fs';
export * as url from 'node:url';
export * as util from 'node:util';
export * as os from 'node:os';
export * as cp from 'node:child_process';
export * as http from 'node:http';
export { once } from 'node:events';
export { setTimeout } from 'node:timers/promises';
export { expect } from 'chai';
export { BrowserWindow, nativeImage, net, session, webContents } from 'electron/main';
// Renderer-only; undefined when this module is loaded in the main process,
// but typed correctly for closures stringified into preload scripts.
export { contextBridge, ipcRenderer, webFrame } from 'electron/renderer';
export { defer } from './defer-helpers';
export {
collectStreamBody,
collectStreamBodyBuffer,
getResponse,
kOneKiloByte,
kOneMegaByte,
randomBuffer,
randomString,
respondNTimes,
respondOnce
} from './net-helpers';
// Renderer-side mirror of the exports above. Keep the keys in sync.
// Context-specific targets (e.g. the api-net utility-process fixture) can
// declare their own __rt with additional keys for helpers that aren't
// require()-able from a renderer.
export const REMOTE_TOOLS_SHIM = `{
path: require('node:path'),
fs: require('node:fs'),
url: require('node:url'),
util: require('node:util'),
os: require('node:os'),
cp: require('node:child_process'),
http: require('node:http'),
once: require('node:events').once,
setTimeout: require('node:timers/promises').setTimeout,
expect: require('chai').expect,
BrowserWindow: require('electron').BrowserWindow,
nativeImage: require('electron').nativeImage,
net: require('electron').net,
session: require('electron').session,
webContents: require('electron').webContents,
}`;
const SSR_IMPORT_RE = /__vite_ssr_import_\d+__/g;
export function rewriteForRemoteEval(fn: Function): string {
return String(fn).replace(SSR_IMPORT_RE, '__rt');
}

View File

@@ -1,29 +1,55 @@
import { BrowserWindow } from 'electron/main';
import { AssertionError } from 'chai';
import { SuiteFunction, TestFunction } from 'mocha';
import { afterAll, beforeAll, describe, it } from 'vitest';
import * as childProcess from 'node:child_process';
import { once } from 'node:events';
import * as http from 'node:http';
import * as http2 from 'node:http2';
import * as https from 'node:https';
import * as net from 'node:net';
import * as path from 'node:path';
import { setTimeout } from 'node:timers/promises';
import * as url from 'node:url';
import * as v8 from 'node:v8';
const addOnly = <T>(fn: Function): T => {
const wrapped = (...args: any[]) => {
return fn(...args);
};
(wrapped as any).only = wrapped;
(wrapped as any).skip = wrapped;
return wrapped as any;
};
import { defer } from './defer-helpers';
import { REMOTE_TOOLS_SHIM, rewriteForRemoteEval } from './remote-tools';
export const ifit = (condition: boolean) => (condition ? it : addOnly<TestFunction>(it.skip));
export const ifdescribe = (condition: boolean) => (condition ? describe : addOnly<SuiteFunction>(describe.skip));
export { defer, runCleanupFunctions } from './defer-helpers';
export { listen } from './net-helpers';
/**
* Swallow a loadURL()/loadFile() rejection so it does not surface as an
* unhandled rejection when the load is expected to be aborted (e.g. the test
* awaits an event and then closes the window). Returns the same promise with
* the rejection suppressed.
*
* Each call site is technical debt: follow-up work should replace these with
* an explicit await or a targeted .catch() once the test's actual contract
* is understood.
*/
export function dangerouslyIgnoreWebContentsLoadResult<T>(p: Promise<T>): Promise<T | void> {
return p.catch(() => {});
}
export const ifit = (condition: boolean) => it.runIf(condition);
export const ifdescribe = (condition: boolean) => describe.runIf(condition);
type DoneCallback = (err?: unknown) => void;
/**
* Adapts a callback-style test (receiving a `done` function) into a
* vitest-compatible test that returns a Promise. `done()` resolves,
* `done(err)` rejects.
*/
export function withDone(fn: (done: DoneCallback) => void): () => Promise<void> {
return () =>
new Promise<void>((resolve, reject) => {
const done: DoneCallback = (err) => {
if (err != null) reject(err instanceof Error ? err : new Error(String(err)));
else resolve();
};
fn(done);
});
}
export const isWayland =
process.platform === 'linux' &&
@@ -31,22 +57,6 @@ export const isWayland =
!!process.env.WAYLAND_DISPLAY ||
process.argv.includes('--ozone-platform=wayland'));
type CleanupFunction = (() => void) | (() => Promise<void>);
const cleanupFunctions: CleanupFunction[] = [];
export async function runCleanupFunctions() {
for (const cleanup of cleanupFunctions) {
const r = cleanup();
if (r instanceof Promise) {
await r;
}
}
cleanupFunctions.length = 0;
}
export function defer(f: CleanupFunction) {
cleanupFunctions.unshift(f);
}
class RemoteControlApp {
process: childProcess.ChildProcess;
port: number;
@@ -85,7 +95,9 @@ class RemoteControlApp {
};
remotely = (script: Function, ...args: any[]): Promise<any> => {
return this.remoteEval(`(${script})(...${JSON.stringify(args)})`);
return this.remoteEval(
`(() => { const __rt = ${REMOTE_TOOLS_SHIM}; return (${rewriteForRemoteEval(script)})(...${JSON.stringify(args)}); })()`
);
};
}
@@ -109,11 +121,17 @@ export async function startRemoteControlApp(extraArgs: string[] = [], options?:
return new RemoteControlApp(appProcess, port);
}
export function waitUntil(callback: () => boolean | Promise<boolean>, opts: { rate?: number; timeout?: number } = {}) {
export function waitUntil(
callback: () => boolean | Promise<boolean>,
signal: AbortSignal,
opts: { rate?: number; timeout?: number } = {}
) {
const { rate = 10, timeout = 10000 } = opts;
return (async () => {
signal.throwIfAborted();
const ac = new AbortController();
const signal = ac.signal;
const combined = AbortSignal.any([signal, ac.signal]);
let checkCompleted = false;
let timedOut = false;
@@ -130,19 +148,25 @@ export function waitUntil(callback: () => boolean | Promise<boolean>, opts: { ra
return result;
};
setTimeout(timeout, { signal }).then(() => {
timedOut = true;
checkCompleted = true;
});
setTimeout(timeout, undefined, { signal: combined })
.then(() => {
timedOut = true;
checkCompleted = true;
})
.catch(() => {});
while (checkCompleted === false) {
if (signal.aborted) {
ac.abort();
throw signal.reason ?? new Error('waitUntil aborted');
}
const checkSatisfied = await check();
if (checkSatisfied === true) {
ac.abort();
checkCompleted = true;
return;
} else {
await setTimeout(rate);
await setTimeout(rate, undefined, { signal: combined }).catch(() => {});
}
}
@@ -189,16 +213,21 @@ export async function getRemoteContext() {
}
export function useRemoteContext(opts?: any) {
before(async () => {
beforeAll(async () => {
remoteContext.unshift(await makeRemoteContext(opts));
});
after(() => {
afterAll(async () => {
const w = remoteContext.shift();
w!.close();
if (w && !w.isDestroyed()) {
const closed = once(w, 'closed');
w.close();
await closed;
}
});
}
async function runRemote(type: 'skip' | 'none' | 'only', name: string, fn: Function, args?: any[]) {
const src = rewriteForRemoteEval(fn);
const wrapped = async () => {
const w = await getRemoteContext();
const { ok, message } = await w.webContents.executeJavaScript(`(async () => {
@@ -207,7 +236,8 @@ async function runRemote(type: 'skip' | 'none' | 'only', name: string, fn: Funct
const promises_1 = require('node:timers/promises')
chai_1.use(require('chai-as-promised'))
chai_1.use(require('dirty-chai'))
await (${fn})(...${JSON.stringify(args ?? [])})
const __rt = ${REMOTE_TOOLS_SHIM};
await (${src})(...${JSON.stringify(args ?? [])})
return {ok: true};
} catch (e) {
return {ok: false, message: e.message}
@@ -243,14 +273,6 @@ export const itremote = Object.assign(
}
);
export async function listen(server: http.Server | https.Server | http2.Http2SecureServer) {
const hostname = '127.0.0.1';
await new Promise<void>((resolve) => server.listen(0, hostname, () => resolve()));
const { port } = server.address() as net.AddressInfo;
const protocol = server instanceof http.Server ? 'http' : 'https';
return { port, hostname, url: url.format({ protocol, hostname, port }) };
}
export function isTestingBindingAvailable() {
try {
process._linkedBinding('electron_common_testing');

View File

@@ -4,6 +4,8 @@ import { expect } from 'chai';
import { once } from 'node:events';
import { runCleanupFunctions } from './defer-helpers';
async function ensureWindowIsClosed(window: BaseWindow | null) {
if (window && !window.isDestroyed()) {
if (window instanceof BrowserWindow && window.webContents && !window.webContents.isDestroyed()) {
@@ -24,34 +26,32 @@ async function ensureWindowIsClosed(window: BaseWindow | null) {
}
}
export const closeWindow = async (
window: BaseWindow | null = null,
{ assertNotWindows } = { assertNotWindows: true }
) => {
export const closeWindow = async (window: BaseWindow | null = null) => {
await ensureWindowIsClosed(window);
if (assertNotWindows) {
let windows = BaseWindow.getAllWindows();
if (windows.length > 0) {
setTimeout(async () => {
// Wait until next tick to assert that all windows have been closed.
windows = BaseWindow.getAllWindows();
try {
expect(windows).to.have.lengthOf(0);
} finally {
for (const win of windows) {
await ensureWindowIsClosed(win);
}
}
});
}
}
};
export async function closeAllWindows(assertNotWindows = false) {
export async function assertNoWindowsLeaked() {
const windows = BaseWindow.getAllWindows();
try {
expect(windows).to.have.lengthOf(
0,
`${windows.length} window(s) leaked across test boundary (ids: ${windows.map((w) => w.id).join(', ')})`
);
} finally {
for (const win of windows) {
await ensureWindowIsClosed(win);
}
}
}
export async function closeAllWindows() {
// Under vitest, setupFiles-level hooks run after test-file afterEach hooks,
// so defer()ed cleanups would see already-destroyed windows. Running them
// here (the innermost afterEach in practice) preserves the mocha ordering.
await runCleanupFunctions();
let windowsClosed = 0;
for (const w of BaseWindow.getAllWindows()) {
await closeWindow(w, { assertNotWindows });
await closeWindow(w);
windowsClosed++;
}
return windowsClosed;
@@ -61,6 +61,7 @@ export async function cleanupWebContents() {
let webContentsDestroyed = 0;
const existingWCS = webContents.getAllWebContents();
for (const contents of existingWCS) {
if (contents.isDestroyed()) continue;
const isDestroyed = once(contents, 'destroyed');
contents.destroy();
await isDestroyed;

View File

@@ -2,6 +2,7 @@ import { app } from 'electron';
import { expect } from 'chai';
import * as uuid from 'uuid';
import { it } from 'vitest';
import { once } from 'node:events';
import * as fs from 'node:fs/promises';
@@ -154,7 +155,7 @@ ifdescribe(isTestingBindingAvailable())('logging', () => {
additionalArguments: ['--unsafely-expose-electron-internals-for-testing']
}
});
w.loadURL('about:blank');
w.loadURL('about:blank').catch(() => {});
w.webContents.once('did-finish-load', () => {
setTimeout(() => {
app.quit();

View File

@@ -1,3 +1,5 @@
import { describe, it } from 'vitest';
import * as cp from 'node:child_process';
import * as path from 'node:path';
@@ -108,9 +110,7 @@ ifdescribe(process.platform === 'darwin' && process.mas)('Mac App Store build',
return foundPrivateAPIs;
};
it('should not use private macOS APIs in main process', function () {
this.timeout(60000);
it('should not use private macOS APIs in main process', { timeout: 60000 }, () => {
const binaries = getElectronBinaries();
const foundPrivateAPIs = checkBinaryForPrivateAPIs(binaries.mainProcess, 'Electron main process');
@@ -139,9 +139,7 @@ ifdescribe(process.platform === 'darwin' && process.mas)('Mac App Store build',
}
});
it('should not use private macOS APIs in Electron Framework', function () {
this.timeout(60000);
it('should not use private macOS APIs in Electron Framework', { timeout: 60000 }, () => {
// Check the Electron Framework binary (mentioned in issue #49616)
const binaries = getElectronBinaries();
const foundAPIs = checkBinaryForPrivateAPIs(binaries.framework, 'Electron Framework');
@@ -155,9 +153,7 @@ ifdescribe(process.platform === 'darwin' && process.mas)('Mac App Store build',
}
});
it('should not use private macOS APIs in helper processes', function () {
this.timeout(60000);
it('should not use private macOS APIs in helper processes', { timeout: 60000 }, () => {
const binaries = getElectronBinaries();
const allFoundAPIs: Record<string, string[]> = {};
@@ -180,9 +176,7 @@ ifdescribe(process.platform === 'darwin' && process.mas)('Mac App Store build',
}
});
it('should not reference private Objective-C classes', function () {
this.timeout(60000);
it('should not reference private Objective-C classes', { timeout: 60000 }, () => {
// Check for private Objective-C classes (appear as _OBJC_CLASS_$_ClassName)
const privateClasses = ['NSAccessibilityRemoteUIElement', 'CAContext'];

View File

@@ -1,13 +1,14 @@
import { BrowserWindow, utilityProcess } from 'electron/main';
import { expect } from 'chai';
import { afterEach, describe, it } from 'vitest';
import * as childProcess from 'node:child_process';
import { once } from 'node:events';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { ifdescribe, ifit } from './lib/spec-helpers';
import { ifdescribe, ifit, withDone, dangerouslyIgnoreWebContentsLoadResult } from './lib/spec-helpers';
import { closeAllWindows } from './lib/window-helpers';
const Module = require('node:module') as NodeJS.ModuleInternal;
@@ -25,7 +26,7 @@ describe('modules support', () => {
show: false,
webPreferences: { nodeIntegration: true, contextIsolation: false }
});
w.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
await expect(w.webContents.executeJavaScript("{ require('@electron-ci/echo'); null }")).to.be.fulfilled();
});
@@ -56,7 +57,7 @@ describe('modules support', () => {
show: false,
webPreferences: { nodeIntegration: true, contextIsolation: false }
});
w.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
await expect(w.webContents.executeJavaScript("{ require('@electron-ci/uv-dlopen'); null }")).to.be.fulfilled();
});
@@ -69,13 +70,16 @@ describe('modules support', () => {
describe('q', () => {
describe('Q.when', () => {
it('emits the fulfil callback', (done) => {
const Q = require('q');
Q(true).then((val: boolean) => {
expect(val).to.be.true();
done();
});
});
it(
'emits the fulfil callback',
withDone((done) => {
const Q = require('q');
Q(true).then((val: boolean) => {
expect(val).to.be.true();
done();
});
})
);
});
});
@@ -99,7 +103,7 @@ describe('modules support', () => {
show: false,
webPreferences: { nodeIntegration: true, contextIsolation: false }
});
w.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
await expect(w.webContents.executeJavaScript("{ require('electron/lol'); null }")).to.eventually.be.rejected();
});
@@ -127,7 +131,7 @@ describe('modules support', () => {
show: false,
webPreferences: { nodeIntegration: true, contextIsolation: false }
});
w.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
await expect(w.webContents.executeJavaScript("{ require('electron'); null }")).to.be.fulfilled();
});
@@ -142,7 +146,7 @@ describe('modules support', () => {
show: false,
webPreferences: { nodeIntegration: true, contextIsolation: false }
});
w.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
await expect(w.webContents.executeJavaScript("{ require('electron/main'); null }")).to.be.fulfilled();
});
@@ -163,7 +167,7 @@ describe('modules support', () => {
show: false,
webPreferences: { nodeIntegration: true, contextIsolation: false }
});
w.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
await expect(w.webContents.executeJavaScript("{ require('electron/renderer'); null }")).to.be.fulfilled();
});
@@ -184,7 +188,7 @@ describe('modules support', () => {
show: false,
webPreferences: { nodeIntegration: true, contextIsolation: false }
});
w.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
await expect(w.webContents.executeJavaScript("{ require('electron/common'); null }")).to.be.fulfilled();
});
@@ -205,7 +209,7 @@ describe('modules support', () => {
show: false,
webPreferences: { nodeIntegration: true, contextIsolation: false }
});
w.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
await expect(w.webContents.executeJavaScript("{ require('electron/utility'); null }")).to.be.fulfilled();
});
@@ -304,7 +308,7 @@ describe('modules support', () => {
show: false,
webPreferences: { nodeIntegration: true, contextIsolation: false }
});
w.loadURL('about:blank');
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
const result = await w.webContents.executeJavaScript('typeof require("q").when');
expect(result).to.equal('function');
});
@@ -312,14 +316,32 @@ describe('modules support', () => {
});
describe('esm', () => {
// These run in a child Electron process because the test runner aliases
// 'electron' through a CJS shim, which changes what `import('electron')`
// returns.
const runFixture = async () => {
const child = childProcess.spawn(process.execPath, [path.join(fixtures, 'module', 'electron-esm-vs-cjs.mjs')], {
stdio: ['ignore', 'pipe', 'inherit']
});
let out = '';
child.stdout.on('data', (d) => {
out += d;
});
const [code] = await once(child, 'exit');
expect(code).to.equal(0);
return JSON.parse(out) as { esm: string[]; cjs: string[] };
};
it('can load the built-in "electron" module via ESM import', async () => {
await expect(import('electron')).to.eventually.be.ok();
const { esm } = await runFixture();
expect(esm.length).to.be.greaterThan(0);
});
it('the built-in "electron" module loaded via ESM import has the same exports as the CJS module', async () => {
const esmElectron = await import('electron');
const cjsElectron = require('electron');
expect(Object.keys(esmElectron)).to.deep.equal(Object.keys(cjsElectron));
const { esm, cjs } = await runFixture();
// Node's CJS→ESM interop adds these wrapper keys; they're not API exports.
const interopKeys = new Set(['default', 'module.exports']);
expect(esm.filter((k) => !interopKeys.has(k))).to.deep.equal(cjs);
});
});
});

View File

@@ -1,6 +1,6 @@
import { webContents } from 'electron/main';
import { expect } from 'chai';
import { afterAll, afterEach, beforeEach, describe, it } from 'vitest';
import * as childProcess from 'node:child_process';
import { once } from 'node:events';
@@ -17,7 +17,8 @@ import {
spawn
} from './lib/codesign-helpers';
import { withTempDirectory } from './lib/fs-helpers';
import { getRemoteContext, ifdescribe, ifit, itremote, useRemoteContext } from './lib/spec-helpers';
import { expect } from './lib/remote-tools';
import { getRemoteContext, ifdescribe, ifit, itremote, useRemoteContext, withDone } from './lib/spec-helpers';
const mainFixturesPath = path.resolve(__dirname, 'fixtures');
@@ -277,31 +278,40 @@ describe('node feature', () => {
describe('contexts', () => {
describe('setTimeout called under Chromium event loop in browser process', () => {
it('Can be scheduled in time', (done) => {
setTimeout(done, 0);
});
it(
'Can be scheduled in time',
withDone((done) => {
setTimeout(done, 0);
})
);
it('Can be promisified', (done) => {
util.promisify(setTimeout)(0).then(done);
});
it(
'Can be promisified',
withDone((done) => {
util.promisify(setTimeout)(0).then(done);
})
);
});
describe('setInterval called under Chromium event loop in browser process', () => {
it('can be scheduled in time', (done) => {
let interval: any = null;
let clearing = false;
const clear = () => {
if (interval === null || clearing) return;
it(
'can be scheduled in time',
withDone((done) => {
let interval: any = null;
let clearing = false;
const clear = () => {
if (interval === null || clearing) return;
// interval might trigger while clearing (remote is slow sometimes)
clearing = true;
clearInterval(interval);
clearing = false;
interval = null;
done();
};
interval = setInterval(clear, 10);
});
// interval might trigger while clearing (remote is slow sometimes)
clearing = true;
clearInterval(interval);
clearing = false;
interval = null;
done();
};
interval = setInterval(clear, 10);
})
);
});
const suspendListeners = (emitter: EventEmitter, eventName: string, callback: (...args: any[]) => void) => {
@@ -712,72 +722,78 @@ describe('node feature', () => {
let child: childProcess.ChildProcessWithoutNullStreams;
let exitPromise: Promise<any[]>;
it('Fails for options disallowed by Node.js itself', (done) => {
after(async () => {
const [code, signal] = await exitPromise;
expect(signal).to.equal(null);
it(
'Fails for options disallowed by Node.js itself',
withDone((done) => {
afterAll(async () => {
const [code, signal] = await exitPromise;
expect(signal).to.equal(null);
// Exit code 9 indicates cli flag parsing failure
expect(code).to.equal(9);
child.kill();
});
// Exit code 9 indicates cli flag parsing failure
expect(code).to.equal(9);
child.kill();
});
const env = { ...process.env, NODE_OPTIONS: '--v8-options' };
child = childProcess.spawn(process.execPath, { env });
exitPromise = once(child, 'exit');
const env = { ...process.env, NODE_OPTIONS: '--v8-options' };
child = childProcess.spawn(process.execPath, { env });
exitPromise = once(child, 'exit');
let output = '';
let success = false;
const cleanup = () => {
child.stderr.removeListener('data', listener);
child.stdout.removeListener('data', listener);
};
let output = '';
let success = false;
const cleanup = () => {
child.stderr.removeListener('data', listener);
child.stdout.removeListener('data', listener);
};
const listener = (data: Buffer) => {
output += data;
if (/electron: --v8-options is not allowed in NODE_OPTIONS/m.test(output)) {
success = true;
cleanup();
done();
}
};
const listener = (data: Buffer) => {
output += data;
if (/electron: --v8-options is not allowed in NODE_OPTIONS/m.test(output)) {
success = true;
cleanup();
done();
}
};
child.stderr.on('data', listener);
child.stdout.on('data', listener);
child.on('exit', () => {
if (!success) {
cleanup();
done(new Error(`Unexpected output: ${output.toString()}`));
}
});
});
child.stderr.on('data', listener);
child.stdout.on('data', listener);
child.on('exit', () => {
if (!success) {
cleanup();
done(new Error(`Unexpected output: ${output.toString()}`));
}
});
})
);
it('Disallows crypto-related options', (done) => {
after(() => {
child.kill();
});
it(
'Disallows crypto-related options',
withDone((done) => {
afterAll(() => {
child.kill();
});
const appPath = path.join(fixtures, 'module', 'noop.js');
const env = { ...process.env, NODE_OPTIONS: '--use-openssl-ca' };
child = childProcess.spawn(process.execPath, ['--enable-logging', appPath], { env });
const appPath = path.join(fixtures, 'module', 'noop.js');
const env = { ...process.env, NODE_OPTIONS: '--use-openssl-ca' };
child = childProcess.spawn(process.execPath, ['--enable-logging', appPath], { env });
let output = '';
const cleanup = () => {
child.stderr.removeListener('data', listener);
child.stdout.removeListener('data', listener);
};
let output = '';
const cleanup = () => {
child.stderr.removeListener('data', listener);
child.stdout.removeListener('data', listener);
};
const listener = (data: Buffer) => {
output += data;
if (/The NODE_OPTION --use-openssl-ca is not supported in Electron/m.test(output)) {
cleanup();
done();
}
};
const listener = (data: Buffer) => {
output += data;
if (/The NODE_OPTION --use-openssl-ca is not supported in Electron/m.test(output)) {
cleanup();
done();
}
};
child.stderr.on('data', listener);
child.stdout.on('data', listener);
});
child.stderr.on('data', listener);
child.stdout.on('data', listener);
})
);
it('does allow --require in non-packaged apps', async () => {
const appPath = path.join(fixtures, 'module', 'noop.js');
@@ -829,10 +845,10 @@ describe('node feature', () => {
ifdescribe(shouldRunCodesignTests)('NODE_OPTIONS in signed app', function () {
let identity = '';
beforeEach(function () {
beforeEach((ctx) => {
const result = getCodesignIdentity();
if (result === null) {
this.skip();
ctx.skip();
} else {
identity = result;
}
@@ -855,14 +871,14 @@ describe('node feature', () => {
});
});
it('is disabled when invoked by alien binary in app bundle in ELECTRON_RUN_AS_NODE mode', async function () {
it('is disabled when invoked by alien binary in app bundle in ELECTRON_RUN_AS_NODE mode', async (ctx) => {
await withTempDirectory(async (dir) => {
const appPath = await copyMacOSFixtureApp(dir);
await signApp(appPath, identity);
// Find system node and copy it to app bundle.
const nodePath = process.env.PATH?.split(path.delimiter).find((dir) => fs.existsSync(path.join(dir, 'node')));
if (!nodePath) {
this.skip();
ctx.skip();
return;
}
const alienBinary = path.join(appPath, 'Contents/MacOS/node');
@@ -891,36 +907,39 @@ describe('node feature', () => {
let child: childProcess.ChildProcessWithoutNullStreams;
let exitPromise: Promise<any[]>;
it('Prohibits crypto-related flags in ELECTRON_RUN_AS_NODE mode', (done) => {
after(async () => {
const [code, signal] = await exitPromise;
expect(signal).to.equal(null);
expect(code).to.equal(9);
child.kill();
});
it(
'Prohibits crypto-related flags in ELECTRON_RUN_AS_NODE mode',
withDone((done) => {
afterAll(async () => {
const [code, signal] = await exitPromise;
expect(signal).to.equal(null);
expect(code).to.equal(9);
child.kill();
});
child = childProcess.spawn(process.execPath, ['--force-fips'], {
env: { ELECTRON_RUN_AS_NODE: 'true' }
});
exitPromise = once(child, 'exit');
child = childProcess.spawn(process.execPath, ['--force-fips'], {
env: { ELECTRON_RUN_AS_NODE: 'true' }
});
exitPromise = once(child, 'exit');
let output = '';
const cleanup = () => {
child.stderr.removeListener('data', listener);
child.stdout.removeListener('data', listener);
};
let output = '';
const cleanup = () => {
child.stderr.removeListener('data', listener);
child.stdout.removeListener('data', listener);
};
const listener = (data: Buffer) => {
output += data;
if (/.*The Node.js cli flag --force-fips is not supported in Electron/m.test(output)) {
cleanup();
done();
}
};
const listener = (data: Buffer) => {
output += data;
if (/.*The Node.js cli flag --force-fips is not supported in Electron/m.test(output)) {
cleanup();
done();
}
};
child.stderr.on('data', listener);
child.stdout.on('data', listener);
});
child.stderr.on('data', listener);
child.stdout.on('data', listener);
})
);
});
describe('process.stdout', () => {
@@ -956,28 +975,35 @@ describe('node feature', () => {
exitPromise = null as any;
});
it('Supports starting the v8 inspector with --inspect/--inspect-brk', (done) => {
child = childProcess.spawn(process.execPath, ['--inspect-brk', path.join(fixtures, 'module', 'run-as-node.js')], {
env: { ELECTRON_RUN_AS_NODE: 'true' }
});
it(
'Supports starting the v8 inspector with --inspect/--inspect-brk',
withDone((done) => {
child = childProcess.spawn(
process.execPath,
['--inspect-brk', path.join(fixtures, 'module', 'run-as-node.js')],
{
env: { ELECTRON_RUN_AS_NODE: 'true' }
}
);
let output = '';
const cleanup = () => {
child.stderr.removeListener('data', listener);
child.stdout.removeListener('data', listener);
};
let output = '';
const cleanup = () => {
child.stderr.removeListener('data', listener);
child.stdout.removeListener('data', listener);
};
const listener = (data: Buffer) => {
output += data;
if (/Debugger listening on ws:/m.test(output)) {
cleanup();
done();
}
};
const listener = (data: Buffer) => {
output += data;
if (/Debugger listening on ws:/m.test(output)) {
cleanup();
done();
}
};
child.stderr.on('data', listener);
child.stdout.on('data', listener);
});
child.stderr.on('data', listener);
child.stdout.on('data', listener);
})
);
it('Supports starting the v8 inspector with --inspect and a provided port', async () => {
child = childProcess.spawn(
@@ -1026,53 +1052,56 @@ describe('node feature', () => {
});
// IPC Electron child process not supported on Windows.
ifit(process.platform !== 'win32')('does not crash when quitting with the inspector connected', function (done) {
child = childProcess.spawn(process.execPath, [path.join(fixtures, 'module', 'delay-exit'), '--inspect=0'], {
stdio: ['ipc']
}) as childProcess.ChildProcessWithoutNullStreams;
exitPromise = once(child, 'exit');
ifit(process.platform !== 'win32')(
'does not crash when quitting with the inspector connected',
withDone((done) => {
child = childProcess.spawn(process.execPath, [path.join(fixtures, 'module', 'delay-exit'), '--inspect=0'], {
stdio: ['ipc']
}) as childProcess.ChildProcessWithoutNullStreams;
exitPromise = once(child, 'exit');
const cleanup = () => {
child.stderr.removeListener('data', listener);
child.stdout.removeListener('data', listener);
};
const cleanup = () => {
child.stderr.removeListener('data', listener);
child.stdout.removeListener('data', listener);
};
let output = '';
const success = false;
function listener(data: Buffer) {
output += data;
console.log(data.toString()); // NOTE: temporary debug logging to try to catch flake.
const match = /^Debugger listening on (ws:\/\/.+:\d+\/.+)\n/m.exec(output.trim());
if (match) {
cleanup();
// NOTE: temporary debug logging to try to catch flake.
child.stderr.on('data', (m) => console.log(m.toString()));
child.stdout.on('data', (m) => console.log(m.toString()));
const w = (webContents as typeof ElectronInternal.WebContents).create();
w.loadURL('about:blank')
.then(() =>
w.executeJavaScript(`new Promise(resolve => {
let output = '';
const success = false;
function listener(data: Buffer) {
output += data;
console.log(data.toString()); // NOTE: temporary debug logging to try to catch flake.
const match = /^Debugger listening on (ws:\/\/.+:\d+\/.+)\n/m.exec(output.trim());
if (match) {
cleanup();
// NOTE: temporary debug logging to try to catch flake.
child.stderr.on('data', (m) => console.log(m.toString()));
child.stdout.on('data', (m) => console.log(m.toString()));
const w = (webContents as typeof ElectronInternal.WebContents).create();
w.loadURL('about:blank')
.then(() =>
w.executeJavaScript(`new Promise(resolve => {
const connection = new WebSocket(${JSON.stringify(match[1])})
connection.onopen = () => {
connection.onclose = () => resolve()
connection.close()
}
})`)
)
.then(() => {
w.destroy();
child.send('plz-quit');
done();
});
)
.then(() => {
w.destroy();
child.send('plz-quit');
done();
});
}
}
}
child.stderr.on('data', listener);
child.stdout.on('data', listener);
child.on('exit', () => {
if (!success) cleanup();
});
});
child.stderr.on('data', listener);
child.stdout.on('data', listener);
child.on('exit', () => {
if (!success) cleanup();
});
})
);
it('Supports js binding', async () => {
child = childProcess.spawn(
@@ -1123,26 +1152,29 @@ describe('node feature', () => {
child.kill();
});
it('performs microtask checkpoint correctly', (done) => {
let timer: NodeJS.Timeout;
const listener = () => {
done(new Error('catch block is delayed to next tick'));
};
it(
'performs microtask checkpoint correctly',
withDone((done) => {
let timer: NodeJS.Timeout;
const listener = () => {
done(new Error('catch block is delayed to next tick'));
};
const f3 = async () => {
return new Promise((resolve, reject) => {
timer = setTimeout(listener);
reject(new Error('oops'));
});
};
const f3 = async () => {
return new Promise((resolve, reject) => {
timer = setTimeout(listener);
reject(new Error('oops'));
});
};
setTimeout(() => {
f3().catch(() => {
clearTimeout(timer);
done();
setTimeout(() => {
f3().catch(() => {
clearTimeout(timer);
done();
});
});
});
});
})
);
describe('type stripping', () => {
it('strips TypeScript types automatically in the main process', async () => {

Some files were not shown because too many files have changed in this diff Show More