Skip to content

feat: implement browser-only test mode (Phase 2)#264

Merged
goosewobbler merged 50 commits into
mainfrom
feat/electron-browser-mode
May 11, 2026
Merged

feat: implement browser-only test mode (Phase 2)#264
goosewobbler merged 50 commits into
mainfrom
feat/electron-browser-mode

Conversation

@goosewobbler
Copy link
Copy Markdown
Contributor

Mirror Tauri's browser mode for Electron: adds mode='browser' +
devServerUrl to ElectronServiceOptions/GlobalOptions, skips binary
detection and CDP bridge in launcher, injects ipcRenderer IPC stub
into the browser page, and exposes browser.electron.mock(channel) for
test-side IPC channel mocking.

Also extracts WDIO_MOCK_SETUP_SCRIPT from TauriAdapter into a shared
injection.ts constant so both adapters compose from the same browser-
side mock factory, and fully implements ElectronAdapter (previously all
stubs) with all FrameworkAdapter methods.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com

goosewobbler and others added 2 commits May 6, 2026 12:41
Mirror Tauri's browser mode for Electron: adds mode='browser' +
devServerUrl to ElectronServiceOptions/GlobalOptions, skips binary
detection and CDP bridge in launcher, injects ipcRenderer IPC stub
into the browser page, and exposes browser.electron.mock(channel) for
test-side IPC channel mocking.

Also extracts WDIO_MOCK_SETUP_SCRIPT from TauriAdapter into a shared
injection.ts constant so both adapters compose from the same browser-
side mock factory, and fully implements ElectronAdapter (previously all
stubs) with all FrameworkAdapter methods.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rity

- Refactored test cases in `index.spec.ts` to group related tests under descriptive `describe` blocks, improving organization and readability.
- Updated test descriptions to follow a consistent "should" format, clarifying expected behaviors for each test case.
- Ensured comprehensive coverage of the `serializeHandler`, `buildRegistrationScript`, `buildSetImplementationScript`, `buildWithImplementationScript`, and `parseCallData` methods.
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 6, 2026

Release Preview — no release

No bump label detected.
Reason: No release labels found (need bump:* or release:stable)
Note: Add bump:patch, bump:minor, or bump:major to trigger a release.


Updated automatically by ReleaseKit

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 6, 2026

Greptile Summary

This PR implements browser-only test mode for the Electron service (Phase 2), mirroring the existing Tauri browser mode: it adds mode='browser' + devServerUrl config options, skips binary detection and the CDP bridge, injects an ipcRenderer stub into the page, and exposes browser.electron.mock(channel) for IPC mocking. It also extracts the shared browser-side mock factory (WDIO_MOCK_SETUP_SCRIPT) from TauriAdapter into injection.ts, fully implements ElectronAdapter (all methods were previously stubs), and introduces a per-instance MockUpdateScheduler to serialize batched mock-update calls.

  • Browser mode plumbing: launcher.ts validates all Electron capabilities share the same mode, strips the Electron binary from goog:chromeOptions, sets browserName: 'chrome', then exits early. service.ts navigates to devServerUrl, injects the IPC stub, patches browser.url() for re-injection on navigation, and exposes the electron.* API surface.
  • Shared mock factory: WDIO_MOCK_SETUP_SCRIPT is extracted from TauriAdapter with several bug fixes — mockResolvedValue/mockRejectedValue now record the resolved/rejected value directly (not the Promise object), mockClear uses in-place array mutation (.length = 0) so external references to mock.calls stay valid, and error objects are serialised/reconstructed across the WebDriver boundary.
  • Tauri additions: emitEvent is added to TauriServiceAPI for both browser and native modes, and patchBrowserUrl now rethrows on injection failure instead of silently swallowing it.

Confidence Score: 4/5

The browser-mode feature is substantial and well-structured; the most impactful issues from the previous review cycle are addressed. The remaining open concern (installCommandOverrides firing in native mode) is acknowledged and deferred to #268.

The PR adds a large new execution path with complex multiremote and per-instance lifecycle management. The previous review cycle surfaced many issues — mode detection limited to the first cap, devServerUrl validation per-cap, mockResolvedValue serializing a Promise, mockClear replacing arrays, root-browser URL patching, stale mock re-registration, and injection-error swallowing — and this iteration demonstrates careful, targeted fixes for each one. The installCommandOverrides-in-native-mode regression is the only remaining known defect on the changed path, and the developer has explicitly deferred it to a follow-up PR.

packages/electron-service/src/service.ts contains the most complex logic (MockUpdateScheduler, multiremote URL patching, mock store key scheme, concurrent registration gate) and warrants careful review. packages/native-spy/src/interceptor/injection.ts is the shared browser-side mock factory and any bug there affects both Electron and Tauri browser modes.

Important Files Changed

Filename Overview
packages/electron-service/src/service.ts Core browser-mode orchestration: initBrowserMode, patchBrowserUrl, getElectronBrowserModeAPI, MockUpdateScheduler, per-instance store keys. Several previously-flagged issues are resolved; residual complexity around multiremote mock ownership remains.
packages/electron-service/src/mock.ts createElectronBrowserModeMock: all async mock methods, implState tracking for __replayBrowserImpl, full update() sync via IPC interceptor scripts.
packages/electron-service/src/launcher.ts Validates uniform mode across all capabilities, validates devServerUrl per-cap, strips Electron binary from chromeOptions, skips CDP/binary setup in browser mode. Previously-flagged first-cap-only issues are fixed.
packages/native-spy/src/interceptor/injection.ts Extracts WDIO_MOCK_SETUP_SCRIPT from TauriAdapter with bug fixes: __wdioType sentinels for resolved/rejected values, in-place array mutation in mockClear, stable _mockSnapshot reference.
packages/native-spy/src/interceptor/electron.ts Full ElectronAdapter implementation: buildBrowserIpcInjectionScript injects ipcRenderer stub with invoke/send/sendSync + no-op on/once/removeListener/removeAllListeners, all other FrameworkAdapter methods implemented.
packages/native-spy/src/interceptor/tauri.ts Migrated to shared WDIO_MOCK_SETUP_SCRIPT; adds event subsystem (wdio_tauri_listeners, wdio_emit_tauri_event, wdio_handle_plugin_event) to support browser-mode emitEvent. Error serialization added to buildCallDataReadScript.
packages/tauri-service/src/service.ts Adds emitEvent to TauriServiceAPI with both browser-mode (via wdio_emit_tauri_event) and native-mode paths; patchBrowserUrl now rethrows on injection failure.
packages/native-spy/src/interceptor/syncProtocol.ts parseCallData now recursively reconstructs Error objects from __wdioError sentinel, matching the new error serialization in buildCallDataReadScript.
packages/native-spy/src/mock.ts mockClear changed from array replacement to in-place mutation (.length = 0) to keep external array references valid after vitest-side clear operations.
packages/electron-service/src/mockStore.ts Adds setMockWithKey (bypasses getMockName key derivation) and deleteMockByRef (reference-equality lookup) for browser-mode instance-scoped keys.
packages/native-types/src/electron.ts Adds mode and devServerUrl fields to ElectronServiceOptions and ElectronServiceGlobalOptions.
packages/native-types/src/tauri.ts Adds TauriEventTarget union type mirroring @tauri-apps/api/event; adds emitEvent to TauriServiceAPI.

Sequence Diagram

sequenceDiagram
    participant Test as Test Runner
    participant Launcher as ElectronLaunchService
    participant Service as ElectronWorkerService
    participant Browser as Chrome (browser mode)
    participant Page as App Page

    Test->>Launcher: onPrepare(caps)
    Launcher->>Launcher: "validate all caps same mode='browser'"
    Launcher->>Launcher: validate devServerUrl per-cap
    Launcher->>Launcher: "set browserName='chrome', strip binary"
    Launcher-->>Test: return (skip CDP/binary setup)

    Test->>Service: beforeSession(caps, instance)
    Service->>Service: initBrowserMode(browser)
    Service->>Browser: browser.url(devServerUrl)
    Browser->>Page: navigate
    Service->>Browser: browser.execute(injectionScript)
    Note over Browser,Page: window.__wdio_spy__, window.__wdio_mocks__,<br/>window.electron.ipcRenderer stub injected
    Service->>Browser: patchBrowserUrl() — wraps url() for re-injection
    Service->>Browser: installCommandOverrides() — click/setValue triggers mock sync
    Service->>Browser: "browser.electron = getElectronBrowserModeAPI()"

    Test->>Browser: browser.electron.mock('channel')
    Browser->>Page: execute(buildRegistrationScript)
    Note over Page: window.__wdio_mocks__['channel'] = spy.fn()
    Browser-->>Test: ElectronFunctionMock

    Test->>Browser: browser.url('new-page')
    Browser->>Page: navigate (wipes window)
    Browser->>Page: execute(injectionScript) [re-inject]
    Note over Browser: liveness check in mock() detects !isLive
    Test->>Browser: browser.electron.mock('channel') [re-register]
    Browser->>Page: execute(buildRegistrationScript)
    Browser->>Page: __replayBrowserImpl() [restore impl]
    Browser->>Browser: existing.mockClear()

    Test->>Browser: browser.$('btn').click()
    Browser->>Browser: MockUpdateScheduler.schedule()
    Browser->>Page: execute(buildCallDataReadScript)
    Page-->>Browser: "{ calls, results, invocationCallOrder }"
    Browser->>Browser: mock.update() — sync to Node.js side
Loading

Reviews (39): Last reviewed commit: "docs(browser-mode): clarify multiremote ..." | Re-trigger Greptile

Comment thread packages/electron-service/src/launcher.ts Outdated
goosewobbler and others added 2 commits May 6, 2026 13:05
…tion

Two bugs in getElectronBrowserModeAPI.mock():

1. The bare catch{} swallowed any exception from mockReset(), silently
   creating a fresh mock instead of surfacing WebDriver or script errors.
   Now only catches the 'No mock registered for' error from getMock().

2. After browser.url() navigation window.__wdio_mocks__ is wiped. When
   mock(channel) finds an existing store entry and calls mockReset(), the
   inner script silently no-ops because the channel key no longer exists
   in the browser. Re-run buildRegistrationScript before mockReset() to
   restore the browser-side entry, then reset clears call history on both
   sides as expected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…lities

Previously only the first Electron capability's mode was inspected,
causing incorrect native-vs-browser setup when a multiremote config
mixed modes. Now all Electron capabilities are checked; a SevereServiceError
is thrown immediately if they disagree, preventing silent misconfiguration.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread packages/native-spy/src/interceptor/electron.ts
goosewobbler and others added 2 commits May 6, 2026 13:32
…ners

Apps commonly call ipcRenderer.on() during module init to register push-
event handlers. Without stubs these calls throw 'TypeError: not a function'
and crash the page before any test runs. Add no-op stubs for the four
listener methods so apps load cleanly; push-event IPC is not interceptable
in browser mode but the app can still start.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
In browser mode there is no Electron binary and windowHandle is never
set, so ensureActiveWindowFocus would call switchToWindow with an
undefined handle on every DOM command, throwing "no such window" for
every click/setValue. Guard beforeCommand with an early return when
mode === 'browser'; installCommandOverrides (mock syncing) is unaffected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread packages/electron-service/src/launcher.ts Outdated
goosewobbler and others added 8 commits May 6, 2026 14:31
…mode

The previous check only validated the first Electron capability's
devServerUrl. In multiremote configs each cap can carry its own URL,
so the others were never checked and an invalid URL would reach
browser.url() with a confusing WebDriver error instead of the
descriptive SevereServiceError. Now every cap is validated
independently, falling back to globalOptions.devServerUrl as before.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… validation

The previous fix added a validation loop over electronCapsList (filtered
by direct cap[CUSTOM_CAPABILITY_NAME] presence) while cap mutation used
capsList.flatMap(getElectronCapabilities) — which also unwraps W3C
alwaysMatch-wrapped caps. Those nested caps bypassed URL validation
entirely and were mutated without a devServerUrl check.

Extract all Electron caps once via getElectronCapabilities (consistent
with the native-mode path) and use the same set for mode detection, URL
validation, and cap mutation. This also eliminates the duplicate flatMap
call in native mode.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…k collision

Two issues in browser-mode service:

1. initBrowserMode threw plain Error for missing devServerUrl; replaced
   with SevereServiceError so the runner stops cleanly (consistent with
   the launcher-side validation).

2. In multiremote, getElectronBrowserModeAPI is called once per instance
   but the mock() path uses a shared mockStore. If instance A registers
   'channel' first and instance B's mock() call finds that entry, the
   old code re-registered on B's page then called mockReset() on A's
   mock — running inner scripts against A's browser. Fixed with a
   WeakMap<ElectronFunctionMock, Browser> that records which browser
   instance owns each browser-mode mock. mock() only reuses a store
   entry when the owner matches the calling browser; otherwise it creates
   a fresh mock for the current instance.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…stances

Updated the ElectronWorkerService to only apply the patchBrowserUrl method when the browser is not in multiremote mode. This change prevents unnecessary URL patching in multiremote configurations, ensuring cleaner handling of browser instances.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… overrides

Refactored the createElectronBrowserModeMock function to simplify the handling of mock data by resetting the original mock's calls, results, and invocation order in a single step. Additionally, updated the installCommandOverrides method to accept an optional targetBrowser parameter, allowing for more flexible command overriding across different browser instances. This enhances the overall efficiency and clarity of the ElectronWorkerService implementation.
Updated the README to include new features of Browser Mode, such as the ability to test the renderer in Chrome against a Vite dev server. Added detailed documentation on Browser Mode in the new browser-mode.md file, including setup instructions, IPC mocking, and usage examples. Enhanced the API reference to clarify limitations and provide guidance on using browser-specific methods. This improves overall clarity and usability for developers transitioning to Browser Mode.
…imitations

Introduced a comprehensive guide for Browser Mode in a new browser-mode.md file, detailing setup, IPC mocking, and usage examples. Updated the README to highlight the new feature of testing Tauri frontends in Chrome against a Vite dev server. Enhanced the API reference to specify limitations of certain commands in Browser Mode, ensuring clarity for developers. This improves the overall documentation and usability for users transitioning to Browser Mode.
Updated the Electron mock store to include a new method for deleting mocks by reference, improving the management of mock instances. Refactored the createElectronBrowserModeMock function to utilize a unique key for storing mocks, ensuring better isolation and retrieval. Enhanced tests to cover the new functionality, ensuring robust handling of mock lifecycle events. This refactor streamlines mock operations and improves overall code clarity.
Comment thread packages/electron-service/src/mock.ts
…Url handling

Enhanced the initBrowserMode method to streamline the initialization process for both single and multiremote browser instances. Added checks to ensure devServerUrl is validated for each instance, improving error handling with SevereServiceError. This refactor clarifies the flow of browser mode setup and ensures consistent behavior across different configurations.
…ementation` details

Added comprehensive information about the `mock.withImplementation()` function in browser mode, including its serialization behavior and limitations. Provided usage examples to guide developers on implementing temporary mocks during UI actions. This enhancement improves clarity and usability for users working with browser mode in Electron.
…hImplementation` details

Added detailed explanations for the `mock.withImplementation()` function in browser mode, highlighting its serialization process and limitations. Included usage examples to assist developers in implementing temporary mocks during UI actions, enhancing the clarity and usability of the documentation for browser mode in Tauri.
…owser mode

Updated the createElectronBrowserModeMock function to improve the mock restoration process, ensuring that the IPC channel remains registered for consistent test behavior. Adjusted the mockRestore method to clear history while preserving the mock's name and channel registration. Additionally, modified the ElectronWorkerService to throw errors when IPC script injection fails after navigation, enhancing error handling. Expanded tests to cover these changes, ensuring robust functionality and clarity in mock management.
Eliminated the unused import of mockStore from mock.ts to clean up the code and improve clarity. This change contributes to better maintainability of the module.
Comment thread packages/electron-service/src/service.ts
…ron.mock() in browser mode

Implemented error handling in the ElectronWorkerService to prevent the use of browser.electron.mock() on the root multiremote browser in browser mode. Updated tests to verify that appropriate error messages are thrown, guiding users to call mock() on specific instances instead. This enhancement improves the robustness of the service and clarifies usage for developers.
Comment thread packages/electron-service/src/service.ts
Comment thread packages/electron-service/src/service.ts Outdated
…n browser mode

Implemented the installation of command overrides in the ElectronWorkerService for browser instances, enhancing the service's functionality. Additionally, ensured that existing mocks are cleared before executing browser commands, improving the reliability of mock behavior during tests. This update contributes to better management of browser interactions and mock lifecycle.
Comment thread packages/electron-service/src/service.ts
goosewobbler and others added 2 commits May 10, 2026 13:46
Expose browser.tauri.emitEvent(event, payload?, target?) on the
worker-side API. Routes through the in-page registry in browser mode
and through Tauri's real event.emit/emitTo via the plugin bridge in
native mode, so the same call works in both modes.

Adds the new TauriEventTarget union for typed target filtering and
documents the feature in api-reference.md plus a new Events section
in browser-mode.md covering emitting from tests, targeted emits,
asserting on frontend emit() calls, and once() semantics.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Browser mode cannot intercept IPC for preloads that use
nodeIntegration: true without contextBridge — the synthetic
window.electron.ipcRenderer the injection creates won't match a
custom API shape. Add a Limitations row and a Troubleshooting entry
pointing to contextBridge migration.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@goosewobbler goosewobbler changed the title feat(electron): implement browser-only test mode (Phase 2) feat: implement browser-only test mode (Phase 2) May 10, 2026
Comment thread packages/native-spy/src/interceptor/injection.ts
Arch's nodejs package tracks bleeding-edge releases (currently 26.1.0).
Node 26 ships with a tighter undici that rejects something WDIO's
webdriver client sends on POST /session — failing every Tauri Arch
package test with UND_ERR_INVALID_ARG before any service code runs.

Install Node 20 LTS from the official tarball instead. Matches the
Node version Ubuntu/Debian pin via setup_20.x and makes Arch builds
repeatable across rolling-repo updates.

Tracking WDIO + Node 26 compatibility upstream as a follow-up.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
goosewobbler and others added 3 commits May 10, 2026 16:51
…ction files

Streamlined the mock clearing process in both the mock.ts and injection.ts files by replacing array reassignments with length resets. This change enhances performance and maintains the intended functionality during mock resets, ensuring a more efficient clearing mechanism without altering existing behavior.
Introduced an error replacer function in the Electron and Tauri adapters to serialize Error objects, ensuring that error details are preserved during mock data processing. Updated the parseCallData function to reconstruct Errors from serialized data, improving the accuracy of call and result tracking in mock implementations. Enhanced integration tests to validate the round-trip of Error objects in both calls and results.
…send/sendSync

Tauri patchBrowserUrl now rethrows after warning, matching the Electron
adapter so failed re-injection surfaces immediately instead of producing
misleading 'unmocked Tauri command' errors downstream. Electron send and
sendSync route to __wdio_mocks__ when a mock is registered, throwing only
when no mock exists.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread packages/tauri-service/src/service.ts
…te missing inner mocks

Native-mode emitEvent now branches on target presence in Node before
executeScript serializes arguments — this avoids the WebDriver JSON
coercion of undefined to null that would route emitEvent(name, payload)
through emitTo(null, ...) instead of emit(...).

Native-mode mockClear and mockReset in mockFactory now optional-chain
through the api/prototype lookup and method invocation, so beforeTest
hooks survive the case where a previous test restored the mock.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread packages/electron-service/src/service.ts
goosewobbler and others added 2 commits May 10, 2026 17:58
…extracted mock methods

The element-command overrides (click/doubleClick/setValue/clearValue)
trigger updateAllMocks() after each interaction, dispatching one CDP
round-trip per registered mock. They were unintentionally installed in
native mode where mocks already sync via the existing CDP path, adding
latency proportional to mock count. Restrict installation to browser
mode where the renderer-side __wdio_mocks__ state actually needs the
post-interaction sync.

Bind outerMockClear, outerMockReset, outerMockImplementation, and
outerMockImplementationOnce when extracting them in
createElectronBrowserModeMock, matching the native createMock factory.
Calling these as detached functions previously left this undefined and
risked breaking vitest internals that depend on the mock instance.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous P1 fix scoped installCommandOverrides to browser mode based on
the assumption that the function was new in this PR and added a CDP round-trip
per DOM interaction. Verification against main shows the function predates this
PR and is required in native mode — the e2e test "should trigger mock updates
when DOM interactions occur" depends on it.

The .bind(outerMock) fix in mock.ts is preserved as a separate, valid bug fix.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread packages/electron-service/docs/browser-mode.md
The "without resetting call history or implementation" claim contradicts
implemented behaviour:

- Electron: when navigation wipes window.__wdio_mocks__, mock() re-registers
  and replays the implementation but clears call history via mockClear().
  Calls accumulated pre-navigation are truncated to zero — opposite of what
  the doc promised.

- Tauri: mock() unconditionally calls mockReset() on the existing mock,
  clearing both history and implementation on every re-call. The
  beforeAll-only-set-once example pattern doesn't work; setup must be in
  beforeEach. After navigation, mock() alone won't re-register the
  browser-side spy — mockRestore() then mock() is required.

Also updates the Navigation example and Troubleshooting entry in the Tauri
guide to show the correct mockRestore-then-mock recovery flow.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread packages/electron-service/src/service.ts
goosewobbler and others added 2 commits May 10, 2026 23:01
…mode

Element command overrides (click, doubleClick, setValue, clearValue) call
updateAllMocks() after every DOM interaction. In native mode this is a
redundant CDP round-trip per command — native mocks already sync via
executeCdp's per-call update path and via the wrapperMock's update() method.
Browser mode genuinely needs the override because there is no CDP-driven
auto-sync.

Note: the native-mode E2E test "should trigger mock updates when DOM
interactions occur" (e2e/test/electron/mocking.spec.ts) was designed to
exercise this override path and will need a follow-up — either an explicit
await mock.update() in the assertion, or removal if the override is no
longer part of the supported native-mode contract.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous attempt to scope installCommandOverrides() to browser mode
broke a large number of E2E suites that rely on element commands (click,
doubleClick, setValue, clearValue) auto-syncing native-mode mock state
after DOM interactions. Restore the override on the native-mode before()
path and reinstate the two unit tests that exercised it.

The new browser-mode-only test "should install element command overrides
in browser mode" is left in place — it remains valid coverage because the
override is now installed via both the native-mode path and the
initBrowserMode() path.

The Greptile P1 about latency proportional to mock count remains open and
should be addressed in a follow-up — most likely by batching
updateAllMocks() reads into a single CDP round-trip rather than one call
per registered mock.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
goosewobbler and others added 8 commits May 11, 2026 00:34
…llow-up

Module-level mockUpdatePending/mockUpdatePromise coalesced concurrent
clicks into a single batch, silently dropping later clicks' update data
once the in-flight batch had captured its snapshot. State was also
shared across all service instances and the promise reference leaked
after each batch.

Replace with per-browser MockUpdateScheduler (WeakMap-keyed) using a
running/queued promise pattern: a click arriving mid-flight enqueues at
most one follow-up batch that runs after the current one settles, so
post-click data is always captured. Per-browser keying isolates
multiremote instances; native-mode mocks (key without \x00) remain
shared.

Adds integration test suite under test/integration/ with controllable
fakes (modeled after tauri-service) covering five scenarios:
concurrent batches, coalescing, failure recovery, cross-instance
independence, empty-store no-op. Two unit tests added to service.spec.ts
for the scheduler queue and recovery paths.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two concurrent browser.electron.mock(channel) calls after navigation
could both observe !isBrowserSideLive, both run the registration
script, both replay impl state, and both mockClear() — losing any
calls made between the two replays.

Gate the re-registration through a per-(browser, channel) in-flight
promise stored in a WeakMap. Concurrent callers await the same
promise so the liveness check, registration script, impl replay, and
mockClear all happen exactly once. The slot is cleared in .finally()
so a later call retries fresh after a failure.

Adds four integration scenarios covering the race, channel
independence, the live-browser fast path, and post-failure retry.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three scenarios that exercise the scheduler, registration gate, and
patchBrowserUrl together — the wiring between them is otherwise only
covered piecewise:

- Full navigation cycle: mock, click (scheduler updates), navigate
  (patchBrowserUrl re-injects), mock again (gate re-registers), click
  (scheduler updates again).
- Four concurrent mock() callers racing to recover after navigation
  all share a single mockClear and impl replay.
- No unhandled rejection emitted when in-flight scheduler work
  completes after the mock store is dropped.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
sendSync returned whatever the mock returned, so mockResolvedValue on a
sync channel silently yielded a Promise where callers expected a raw
value. Detect the thenable and throw with a message pointing at
mockReturnValue / synchronous mockImplementation.

window.__wdio_spy__ was reassigned on every injection, so any
same-page re-injection (or a manual re-run of the injection script)
replaced the factory identity. Match the existing __wdio_mocks__ guard
and create the factory only when absent.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The launcher was deleting the entire goog:chromeOptions when transforming
an Electron capability into browser mode, silently dropping any
user-supplied args, extensions, or prefs. Only the `binary` field is
Electron-specific; preserve everything else.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The getter built a fresh literal `{ calls, results, invocationCallOrder }`
on every access, so consumers that cached `m.mock` and compared it later
would see different identities even though the inner arrays were the
same. Allocate the snapshot once at mock-creation time and return that
same reference from the getter.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ates per-instance

Two related correctness fixes on the browser-mode update path:

The MockUpdateScheduler cleared #running in its .finally before the
queued chain's #runOnce had a chance to run. A third schedule() arriving
in that gap saw #running=null and spawned a parallel batch alongside the
queued one. Add an atomic promotion step that hands #queued into
#running before the slot is cleared.

In multiremote, the element-command override captured the root browser,
so the scheduler filtered mocks by the root's per-browser key suffix and
matched nothing — per-instance mocks (registered under each instance's
suffix) were never updated and tests saw stale call data. Use
`this.browser` (the per-instance owning session that fires the override)
so the scheduler routes to the right per-instance mock bucket.

Tests cover the race window directly and a simulated multiremote
override invocation.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Document that per-instance devServerUrl is used at startup only, that
root url() navigates all instances to the same href, and that mock() on
the root multiremote browser is unsupported. Closes the gap surfaced
during the multiremote review.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@goosewobbler goosewobbler merged commit a84c4eb into main May 11, 2026
147 of 149 checks passed
@goosewobbler goosewobbler deleted the feat/electron-browser-mode branch May 11, 2026 09:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant