Add Vitest Electron BrowserProvider foundation#9276
Conversation
# Conflicts: # common/config/rush/pnpm-lock.yaml
There was a problem hiding this comment.
Pull request overview
Introduces a new internal @itwin/vitest-certa-bridge package to provide an Electron-backed Vitest BrowserProvider (with a preload-based backend-callback bridge), and migrates @itwin/core-electron renderer integration testing from Certa/Mocha-style wiring to Vitest browser mode.
Changes:
- Added
@itwin/vitest-certa-bridge, including an Electron BrowserProvider, provider-session main entry, and callback registry + smoke tests. - Migrated
core/electronrenderer integration coverage to Vitest browser mode; backend tests remain “spawned Electron main” but orchestrated by Vitest with JUnit output. - Removed obsolete
core/electronMocha/Certa/Webpack frontend test wiring and updated repo configs (Rush project list, browser-approved packages, lockfile, CODEOWNERS, change files).
Reviewed changes
Copilot reviewed 32 out of 33 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| tools/vitest-certa-bridge/tsconfig.test-fixtures.json | Adds TS build config for compiling provider test fixtures. |
| tools/vitest-certa-bridge/tsconfig.json | Base TS config for the new bridge package (CJS output). |
| tools/vitest-certa-bridge/tsconfig.eslint.json | ESLint project TS config for the new bridge package. |
| tools/vitest-certa-bridge/src/test/vitest.electron-provider.config.mts | Vitest config to run the provider smoke test in Electron browser mode. |
| tools/vitest-certa-bridge/src/test/vitest.config.mts | Default Vitest config for bridge package unit tests. |
| tools/vitest-certa-bridge/src/test/fixtures/electron-provider-user-preload.ts | Fixture preload used to validate preload composition. |
| tools/vitest-certa-bridge/src/test/fixtures/electron-provider-backend-init.ts | Fixture backend init that registers callbacks for the smoke test. |
| tools/vitest-certa-bridge/src/test/electron-provider-smoke.test.ts | Smoke test verifying real Vitest APIs in Electron renderer + backend callback bridge. |
| tools/vitest-certa-bridge/src/test/bridge.test.ts | Unit tests for the callback registry/dispatch behavior. |
| tools/vitest-certa-bridge/src/index.ts | Public package-root exports for callback registration APIs. |
| tools/vitest-certa-bridge/src/electron/types.ts | Defines provider options surface for Electron BrowserProvider. |
| tools/vitest-certa-bridge/src/electron/provider.ts | Implements the custom Vitest BrowserProvider that spawns Electron sessions. |
| tools/vitest-certa-bridge/src/electron/provider-session.ts | Electron main-process entry that creates the BrowserWindow and preload bridge. |
| tools/vitest-certa-bridge/src/callbackRegistry.ts | Implements global callback registry + dispatch/token validation. |
| tools/vitest-certa-bridge/README.md | Documents provider usage, callback bridge, and security model. |
| tools/vitest-certa-bridge/package.json | New package metadata, build/test scripts, and export map. |
| tools/vitest-certa-bridge/eslint.config.js | ESLint configuration for the new bridge package. |
| rush.json | Registers @itwin/vitest-certa-bridge as a Rush project. |
| core/electron/vitest.frontend.config.mts | Adds Vitest browser-mode config using the new Electron provider for renderer tests. |
| core/electron/vitest.config.mts | Adds Vitest config for backend integration orchestration + JUnit output. |
| core/electron/src/test/frontend/utils/webpack.config.js | Removes legacy Webpack bundling for Certa-based renderer tests. |
| core/electron/src/test/frontend/utils/certa.json | Removes legacy Certa config for renderer integration tests. |
| core/electron/src/test/frontend/utils/backend.ts | Updates renderer-backend init for ElectronHost startup under the new flow. |
| core/electron/src/test/frontend/ElectronApp.test.ts | Migrates renderer integration tests from Chai/Mocha to Vitest APIs. |
| core/electron/src/test/backend/RunElectronBackendTests.ts | Uses Vitest as the orchestrator and improves spawned Electron process timeout/cleanup. |
| core/electron/src/test/backend/ElectronHost.test.ts | Adjusts ElectronHost tests for updated startup options and more stable maximize/unmaximize assertions. |
| core/electron/src/test/backend/.mocharc.json | Removes Mocha config (backend integration now run via Vitest config). |
| core/electron/package.json | Switches integration test scripts to Vitest, adds ESM build, and adds export map/deep exports. |
| common/config/rush/pnpm-lock.yaml | Locks new dependencies (@vitest/browser, vitest, bridge package) and removes Certa/Mocha deps from core/electron. |
| common/config/rush/browser-approved-packages.json | Approves the new bridge package and broadens allowed categories for some testing/tooling deps. |
| common/changes/@itwin/vitest-certa-bridge/nam-vcb-electron-provider_2026-05-07-15-48.json | Change entry for introducing the bridge package. |
| common/changes/@itwin/core-electron/nam-vcb-01-electron-redesign_2026-05-07-19-48.json | Change entry for migrating Electron renderer integration coverage to Vitest. |
| .github/CODEOWNERS | Adds code ownership for the new bridge package paths. |
Files not reviewed (1)
- common/config/rush/pnpm-lock.yaml: Language not supported
| /** Resolve the Electron provider main-process entry point from this package's compiled CJS output. */ | ||
| function getProviderSessionEntryPath(): string { | ||
| // __dirname is either lib/cjs/electron or lib/esm/electron. Electron main runs under CJS, | ||
| // so always target the CJS provider-session entry. | ||
| const pkgRoot = path.resolve(__dirname, "../../.."); | ||
| return path.join(pkgRoot, "lib", "cjs", "electron", "provider-session.js"); | ||
| } |
There was a problem hiding this comment.
🤖 Nambot (powered by OpenAI GPT-5) — Good catch. I changed the provider entry to avoid __dirname so the ESM build can be imported directly by Vitest.
The provider now uses createRequire(...) plus package export resolution to locate the compiled CJS provider-session.js, which keeps the Electron main-process entry on CJS without relying on CJS-only globals in the ESM provider module. I also validated the ESM import directly with:
node --input-type=module -e "const mod = await import('./lib/esm/electron/provider.js'); console.log(typeof mod.default)"|
|
||
| const cacheDir = fs.mkdtempSync(path.join(os.tmpdir(), `vitest-electron-${sessionId}-`)); | ||
| const electronBin = require("electron/index.js"); | ||
| const sessionEntry = getProviderSessionEntryPath(); | ||
| const electronArgs = [...(this._options.electronArgs ?? []), sessionEntry]; |
There was a problem hiding this comment.
🤖 Nambot (powered by OpenAI GPT-5) — Fixed. The provider no longer uses the ambient CommonJS require global to resolve Electron.
It now creates a local require via Node's createRequire(...) and calls moduleRequire("electron/index.js"), which works from the ESM-loaded provider while still returning Electron's executable path.
| electronProcess.on("exit", (code, signal) => { | ||
| this._sessions.delete(sessionId); | ||
| cleanupCacheDir(cacheDir); | ||
| if (!settled && code !== 0 && code !== null) |
There was a problem hiding this comment.
🤖 Nambot (powered by OpenAI GPT-5) — Fixed. The startup wait now rejects on any pre-ready Electron process exit, including code === 0 and signal exits.
That means openPage() no longer waits until the timeout if Electron exits before printing the provider ready marker.
| // Vitest browser mode executes test modules inside a tester iframe. Load the preload | ||
| // in subframes too so renderer tests can reach the callback bridge from that iframe. | ||
| nodeIntegrationInSubFrames: true, |
There was a problem hiding this comment.
🤖 Nambot (powered by OpenAI GPT-5) — I kept this setting, but tightened the comment/docs because the tradeoff is intentional for Vitest browser mode.
Vitest executes the test module inside a same-origin tester iframe. Without loading the preload into that subframe, core/electron's renderer test loses window.itwinjs and falls back to require("electron"), which fails with require is not defined under nodeIntegration: false.
The BrowserWindow still has nodeIntegration: false and contextIsolation: true; this option is being used so the preload/contextBridge surface is present in Vitest's tester iframe, not so renderer tests can depend on Node globals. I updated the inline comment and README to make that explicit.
| const cacheDir = process.env.ELECTRON_CACHE_DIR; | ||
| await ElectronHost.startup(cacheDir ? { | ||
| iModelHost: { | ||
| cacheDir, | ||
| profileName: `renderer-${process.pid}`, | ||
| }, | ||
| } : undefined); |
There was a problem hiding this comment.
🤖 Nambot (powered by OpenAI GPT-5) — Fixed. The core/electron backend init now reads VITEST_ELECTRON_CACHE_DIR first, with ELECTRON_CACHE_DIR as a compatibility fallback.
I also have the provider populate both env vars so package-local init modules can use either name while we settle the provider contract.
| "exports": { | ||
| "./ElectronFrontend": { | ||
| "types": "./lib/cjs/ElectronFrontend.d.ts", | ||
| "import": "./lib/esm/ElectronFrontend.js", | ||
| "require": "./lib/cjs/ElectronFrontend.js" | ||
| }, | ||
| "./ElectronBackend": { | ||
| "types": "./lib/cjs/ElectronBackend.d.ts", | ||
| "import": "./lib/esm/ElectronBackend.js", | ||
| "require": "./lib/cjs/ElectronBackend.js" | ||
| }, |
There was a problem hiding this comment.
🤖 Nambot (powered by OpenAI GPT-5) — I don't think we should add a root export here.
@itwin/core-electron did not previously define main, module, or a runtime package-root entrypoint, and src/core-electron.ts is explicitly marked docs-only with "Do not import it in real code!" because it mixes frontend and backend exports. Adding "." now would make that invalid docs barrel look like a supported runtime API.
The intended runtime contract in this PR is the narrower pair of subpaths: @itwin/core-electron/ElectronFrontend and @itwin/core-electron/ElectronBackend, plus temporary deep-CJS compatibility exports for existing internal consumers.
| /test-apps/imodel-from-reality-model @iTwin/itwinjs-core-display | ||
|
|
||
| /tools @aruniverse @wgoehrig | ||
| /tools/certa @aruniverse @wgoehrig |
There was a problem hiding this comment.
@aruniverse, should we still be adding Bill as code owner in core ?
There was a problem hiding this comment.
🤖 Nambot (powered by OpenAI GPT-5) — I added Bill here to preserve the existing /tools ownership, not because the new package specifically needs an extra owner.
/tools is already owned by @aruniverse @wgoehrig, and this more specific /tools/vitest-certa-bridge rule overrides that broader rule. If we only listed @hl662, we'd accidentally drop the existing tools owners for this path. Happy to narrow it if you/Arun think this package should have a different owner set.
Why
This is the first PR in the Vitest/Certa bridge stack. The goal is to establish the Electron renderer foundation before migrating larger full-stack packages.
The key requirement is that Electron renderer tests run inside a real Vitest browser runtime — not a custom Mocha-style renderer harness, not a fake
vitestshim, and not anodeIntegration-dependent test window.This PR proves that path with
core/electronas the first consumer.What
Adds a new internal package:
@itwin/vitest-certa-bridge@itwin/vitest-certa-bridge/electron-provideras a custom Vitest BrowserProviderBrowserWindow, and navigates it to Vitest's browser-mode session URLnodeIntegration: falseandcontextIsolation: truebackendInitModuleandpreloadModuleMigrates
core/electronfrontend integration coverage to Vitest browser modeElectronApp.test.tsnow runs through the Electron BrowserProviderElectronHosttests remain on the existing spawned Electron-main model, with Vitest as the outer orchestrator/reporterKeeps the bridge package intentionally small
Cleans up obsolete
core/electrontest wiringcore/electronScope notes
This PR is intentionally the foundation layer only.
It proves:
core/electroncan consume the provider for renderer integration coverageIt does not attempt to migrate
full-stack-tests/coreyet. That migration belongs in a follow-up stack PR, where the package-specific Chrome + Electron test orchestration can be designed separately.Validation
Targeted validation run locally:
cd core/electron rushx build rushx lint rushx test:integrationAlso verified:
Repo-wide lint was also run before opening the draft PR:
It is not fully green due to an existing unrelated error outside this PR's touched packages:
In that repo-wide lint run,
@itwin/vitest-certa-bridgeand@itwin/core-electronboth completed successfully.Expected/known non-blocking noise:
rush testwas previously run during this work and surfaced unrelated baseline failures outside this PR's touched packages. The branch-relevant deep@itwin/core-electronexport-map breakage found during that run was fixed by adding backward-compatible deep CJS exports.Follow-ups
full-stack-tests/coresubset in the next stack PR.full-stack-tests/coreruntime split before adding more bridge API:@itwin/vitest-certa-bridgeonly if the full-stack migration proves a reusable transport is needed.Nambot 🤖 (powered by OpenAI GPT-5)