Skip to content

fix(native-mobile-core): honour capability-level launcher options#440

Merged
goosewobbler merged 2 commits into
mainfrom
fix/launcher-capability-options
Jun 19, 2026
Merged

fix(native-mobile-core): honour capability-level launcher options#440
goosewobbler merged 2 commits into
mainfrom
fix/launcher-capability-options

Conversation

@goosewobbler

Copy link
Copy Markdown
Contributor

Stacked on #426 (feat/406-rn-metro-lifecycle) — review/merge that first; this PR's diff is the launcher-options delta.

The footgun

The mobile launchers read run-level options (autoInstallDriver, doctor, and RN's manageMetro/metroProjectRoot/prebundle/metroPort) from this.options — the service-registration options (['react-native', {…}]). If a user instead configures the service on the capability (wdio:reactNativeServiceOptions — the idiomatic WDIO pattern, and what the e2e dogfooding conf did), those options are silently ignored.

This is exactly what broke #426's RN Android E2E: manageMetro was set only on the capability, so managed Metro never started → the app couldn't load its bundle → "Hermes inspector is not connected" on every execute/mock spec. Worse, doctor: { strict: true } — the check meant to catch "Metro not reachable" — was disabled by the same gap, so nothing flagged it. (The conf itself is fixed on #426 by passing options at the service level; this PR fixes the underlying footgun so capability-only config also works.)

Fix

MobileBaseLauncher.resolveLauncherOptions(capabilities) returns the global options merged with the per-capability wdio:<framework>ServiceOptions (capability wins, matching the precedence mergeServiceOptions already uses for mutateCapability). Run-level decisions now read it:

  • base (onPrepare): autoInstallDriver, doctor → so Flutter inherits the fix automatically (its doctor was also silently off when configured on the capability).
  • RN (onPrepare): manageMetro/metroProjectRoot/prebundle/platform; (onWorkerStart): metroPort/platform for the pre-launch adb reverse.

The per-capability mutateCapability merge is unchanged (it correctly uses each cap's own options).

Tests

  • native-mobile-core +2: autoInstallDriver and doctor: { strict: true } set on the capability drive the launcher.
  • react-native-service +1: manageMetro on the capability starts managed Metro.
  • All green: native-mobile-core 110, react-native-service 141; typecheck + biome clean.

Note: when #426 squash-merges to main, this branch's base should retarget to main (the stacked-PR merge-target trap).

🤖 Generated with Claude Code

@greptile-apps

greptile-apps Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a silent footgun where run-level launcher options (autoInstallDriver, doctor, manageMetro, metroPort, etc.) were read exclusively from the service-registration options (this.options), ignoring any values set on the per-capability wdio:<framework>ServiceOptions — the idiomatic WDIO pattern used by the e2e dogfooding config.

  • Adds MobileBaseLauncher.resolveLauncherOptions(capabilities) which folds all per-capability service options on top of this.options (capability wins, matching the existing mergeServiceOptions precedence), and updates onPrepare in both the base and RN subclass to read from the merged result.
  • Captures #resolvedOptions on the RN launcher before super.onPrepare so doctorChecks probes the correct metroHost/metroPort even when those values come from the capability.
  • Adds tests in both packages confirming that autoInstallDriver, doctor: { strict: true }, manageMetro, and metroPort all work when configured only at the capability level.

Confidence Score: 5/5

Safe to merge — all changed paths have direct test coverage and the mutation-safety of mergeServiceOptions is confirmed by reading the implementation.

The fix is surgical: resolveLauncherOptions folds per-capability options on top of service-level options using the same spread-based mergeServiceOptions already used throughout the codebase. The #resolvedOptions capture before super.onPrepare correctly threads the merged Metro host/port into doctorChecks, closing the last gap called out in a prior review round.

No files require special attention.

Important Files Changed

Filename Overview
packages/native-mobile-core/src/launcher.ts Adds resolveLauncherOptions() which folds all per-capability service options into this.options (capability wins via object spread); onPrepare now reads effectiveOptions for autoInstallDriver and doctor instead of this.options directly.
packages/react-native-service/src/launcher.ts Adds #resolvedOptions instance field captured in onPrepare before super.onPrepare so doctorChecks uses the capability-merged metroHost/metroPort. All Metro startup options and adb reverse port also read from resolveLauncherOptions.
packages/native-mobile-core/test/launcher.spec.ts Adds two regression tests: autoInstallDriver and doctor: { strict: true } set only on the capability now trigger the expected code paths.
packages/react-native-service/test/launcher.spec.ts Adds mock for reactNativeDoctorChecks and three new tests covering manageMetro on capability, and capability-level metroPort reaching the doctor check.
e2e/wdio.react-native.conf.ts Comment updated to reflect the new resolveLauncherOptions behaviour.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant WDIO as WDIO Runner
    participant RNL as ReactNativeLauncher onPrepare
    participant Base as MobileBaseLauncher onPrepare
    participant DC as doctorChecks

    WDIO->>RNL: onPrepare(config, capabilities)
    RNL->>RNL: resolveLauncherOptions(capabilities)
    RNL->>RNL: "#resolvedOptions = merged options"
    alt options.manageMetro
        RNL->>RNL: MetroProcess.start(port, projectRoot)
    end
    RNL->>Base: super.onPrepare(config, capabilities)
    Base->>Base: "effectiveOptions = resolveLauncherOptions(capabilities)"
    Base->>Base: effectiveOptions.autoInstallDriver
    Base->>Base: resolveDoctor(effectiveOptions.doctor)
    Base->>DC: doctorChecks(config, platforms)
    DC->>DC: "#resolvedOptions ?? this.options"
    DC-->>Base: checks[]
    Base-->>RNL: done
    RNL-->>WDIO: done

    WDIO->>RNL: onWorkerStart(cid, workerCaps)
    RNL->>Base: super.onWorkerStart(cid, workerCaps)
    Base-->>RNL: done
    RNL->>RNL: resolveLauncherOptions(workerCaps)
    alt "platform === android"
        RNL->>RNL: adbReverse(port, udid)
    end
    RNL-->>WDIO: done
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant WDIO as WDIO Runner
    participant RNL as ReactNativeLauncher onPrepare
    participant Base as MobileBaseLauncher onPrepare
    participant DC as doctorChecks

    WDIO->>RNL: onPrepare(config, capabilities)
    RNL->>RNL: resolveLauncherOptions(capabilities)
    RNL->>RNL: "#resolvedOptions = merged options"
    alt options.manageMetro
        RNL->>RNL: MetroProcess.start(port, projectRoot)
    end
    RNL->>Base: super.onPrepare(config, capabilities)
    Base->>Base: "effectiveOptions = resolveLauncherOptions(capabilities)"
    Base->>Base: effectiveOptions.autoInstallDriver
    Base->>Base: resolveDoctor(effectiveOptions.doctor)
    Base->>DC: doctorChecks(config, platforms)
    DC->>DC: "#resolvedOptions ?? this.options"
    DC-->>Base: checks[]
    Base-->>RNL: done
    RNL-->>WDIO: done

    WDIO->>RNL: onWorkerStart(cid, workerCaps)
    RNL->>Base: super.onWorkerStart(cid, workerCaps)
    Base-->>RNL: done
    RNL->>RNL: resolveLauncherOptions(workerCaps)
    alt "platform === android"
        RNL->>RNL: adbReverse(port, udid)
    end
    RNL-->>WDIO: done
Loading

Reviews (3): Last reviewed commit: "docs(e2e): update RN service-options com..." | Re-trigger Greptile

goosewobbler added a commit that referenced this pull request Jun 19, 2026
Run-level launcher decisions read this.options (the service-registration options)
only, so options set on the capability (wdio:<framework>ServiceOptions — the
idiomatic WDIO pattern) were silently ignored: manageMetro never started Metro,
and doctor/autoInstallDriver no-opped.

Add MobileBaseLauncher.resolveLauncherOptions(capabilities) — global options
merged with the per-capability options (capability wins) — read for
autoInstallDriver + doctor (base) and manageMetro/metroProjectRoot/prebundle/
metroPort (RN onPrepare + onWorkerStart). Flutter inherits the base fix.

Also thread the resolved metroHost/metroPort into the RN Metro doctor check:
doctorChecks runs inside super.onPrepare, so onPrepare stashes the merged options
on the instance (#resolvedOptions) for it to read — otherwise a capability-only
metroPort would make the doctor probe the default 8081 (Greptile #440).

Tests: native-mobile-core +2 (autoInstallDriver + doctor from the capability),
react-native-service +2 (manageMetro from the capability; doctor probes the
capability-level metroPort).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@goosewobbler goosewobbler force-pushed the fix/launcher-capability-options branch from 797f038 to 927c3f0 Compare June 19, 2026 14:45
Base automatically changed from feat/406-rn-metro-lifecycle to main June 19, 2026 17:00
goosewobbler and others added 2 commits June 19, 2026 18:03
Run-level launcher decisions read this.options (the service-registration options)
only, so options set on the capability (wdio:<framework>ServiceOptions — the
idiomatic WDIO pattern) were silently ignored: manageMetro never started Metro,
and doctor/autoInstallDriver no-opped.

Add MobileBaseLauncher.resolveLauncherOptions(capabilities) — global options
merged with the per-capability options (capability wins) — read for
autoInstallDriver + doctor (base) and manageMetro/metroProjectRoot/prebundle/
metroPort (RN onPrepare + onWorkerStart). Flutter inherits the base fix.

Also thread the resolved metroHost/metroPort into the RN Metro doctor check:
doctorChecks runs inside super.onPrepare, so onPrepare stashes the merged options
on the instance (#resolvedOptions) for it to read — otherwise a capability-only
metroPort would make the doctor probe the default 8081 (Greptile #440).

Tests: native-mobile-core +2 (autoInstallDriver + doctor from the capability),
react-native-service +2 (manageMetro from the capability; doctor probes the
capability-level metroPort).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ehaviour

With resolveLauncherOptions the launcher reads run-level options from the service
registration AND the capability, so the prior 'NOT from the capability' note (and
the now-historical footgun post-mortem) no longer hold.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@goosewobbler goosewobbler force-pushed the fix/launcher-capability-options branch from 927c3f0 to ccf0d4c Compare June 19, 2026 17:04
@github-actions

Copy link
Copy Markdown
Contributor

Standing release PR: #400 · 12 packages queued · open 63h 58m · ✅ ready to merge

Release Preview — 12 packages

Note: Labels on this PR are advisory in standing-pr mode. Bumps come from conventional commits in the standing PR; override by editing labels on the standing PR itself. Add release:immediate to bypass the standing PR and release this PR directly.

These changes will be added to the release PR (#400) when merged:

Changelog

Project-wide changes

Changed

  • update RN service-options comment for the merged-options behaviour (e2e)

Fixed

  • honour capability-level launcher options (native-mobile-core)
@wdio/dioxus-service N/A → 1.0.1

Changed

  • Update version to 1.0.1
@wdio/electron-cdp-bridge wdio-electron-cdp-bridge@v10.0.0 → 10.0.1

Changed

  • Update version to 10.0.1
@wdio/electron-service wdio-electron-service@v10.0.0 → 10.0.1

Changed

  • Update version to 10.0.1
@wdio/flutter-service v1.0.0-next.0 → 1.0.1

Changed

  • Update version to 1.0.1
@wdio/native-cdp-bridge v0.1.0-next.0 → 0.1.1

Changed

  • Update version to 0.1.1
@wdio/native-mobile-core v1.0.0 → 1.0.1

Fixed

  • honour capability-level launcher options (native-mobile-core)
@wdio/native-spy wdio-native-spy@v1.1.0 → 1.1.1

Changed

  • Update version to 1.1.1
@wdio/native-types wdio-native-types@v2.3.1 → 2.3.2

Changed

  • Update version to 2.3.2
@wdio/native-utils wdio-native-utils@v2.4.0 → 2.4.1

Changed

  • Update version to 2.4.1
@wdio/react-native-service v1.0.0-next.0 → 1.0.1

Fixed

  • honour capability-level launcher options (native-mobile-core)
@wdio/tauri-service wdio-tauri-service@v1.1.0 → 1.1.1

Changed

  • Update version to 1.1.1
dioxus-package-test-app v0.1.0 → 0.1.1

Changed

  • Update version to 0.1.1

After merge — predicted release

No version escalation — this PR's changes will be included in the queued release without affecting the projected versions.

Package Standing PR This PR After merge
@wdio/dioxus-service 1.1.0 1.0.1 1.1.0
@wdio/electron-cdp-bridge 10.1.0 10.0.1 10.1.0
@wdio/electron-service 10.1.0 10.0.1 10.1.0
@wdio/flutter-service 1.1.0 1.0.1 1.1.0
@wdio/native-cdp-bridge 0.2.0 0.1.1 0.2.0
@wdio/native-mobile-core 1.1.0 1.0.1 1.1.0
@wdio/native-spy 1.2.0 1.1.1 1.2.0
@wdio/native-types 2.4.0 2.3.2 2.4.0
@wdio/native-utils 2.5.0 2.4.1 2.5.0
@wdio/react-native-service 1.1.0 1.0.1 1.1.0
@wdio/tauri-service 1.2.0 1.1.1 1.2.0
dioxus-package-test-app 0.2.0 0.1.1 0.2.0

Updated automatically by ReleaseKit

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