From abbd68a184fe31d564eceff5c022da1b6d80c54d Mon Sep 17 00:00:00 2001 From: masaaki-hori Date: Mon, 5 Oct 2026 10:52:15 +0900 Subject: [PATCH 1/2] fix(ios): forward dartVmServicePort launch flags to XCUITest injectDartVmServicePortFlags only mutated the driver's processed caps, but XCUITest is started from a separate copy of the raw W3C capabilities, so --vm-service-port / --disable-service-auth-codes never reached the app and the dartVmServicePort fallback connected to a closed port. Mirror the resulting processArguments into each forwarded alwaysMatch and drop the key from firstMatch to avoid a duplicate-key validation error. Co-Authored-By: Claude Opus 5.5 --- driver/lib/sessions/ios.ts | 23 ++++++++-- driver/test/ios-dart-vm-service-port.test.mjs | 42 +++++++++++++++++++ 2 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 driver/test/ios-dart-vm-service-port.test.mjs diff --git a/driver/lib/sessions/ios.ts b/driver/lib/sessions/ios.ts index feedb04c..20eb961f 100644 --- a/driver/lib/sessions/ios.ts +++ b/driver/lib/sessions/ios.ts @@ -26,7 +26,7 @@ export async function startIOSSession( ...args: any[] ): Promise<[XCUITestDriver, IsolateSocket | null]> { this.log.info(`Starting an IOS proxy session`); - injectDartVmServicePortFlags(caps); + injectDartVmServicePortFlags(caps, args); const iosdriver = new XCUITestDriver({} as XCUITestDriverOpts); if (!caps.observatoryWsUri) { iosdriver.eventEmitter.once('syslogStarted', (syslog) => { @@ -249,10 +249,17 @@ async function ensureDeviceLogCaptureStarted( * Other entries (including a user-supplied `--observatory-port=*`) are left * untouched. * + * `caps` is the driver's own processed copy, while XCUITest is started from the + * raw W3C capabilities in `w3cCapsList`, so the resulting `processArguments` are + * mirrored into each of their `alwaysMatch` (and dropped from `firstMatch` to + * avoid a duplicate-key validation error). Otherwise the flags never reach the + * app under test. + * * If `dartVmServicePort` is not set, the caps are left untouched. - * @param caps The W3C capabilities passed to the driver session. + * @param caps The processed capabilities of the driver session. + * @param w3cCapsList The raw W3C capabilities forwarded to XCUITest. */ -function injectDartVmServicePortFlags(caps: Record): void { +export function injectDartVmServicePortFlags(caps: Record, w3cCapsList: any[] = []): void { const port = caps.dartVmServicePort; if (typeof port !== 'number') { return; @@ -265,6 +272,16 @@ function injectDartVmServicePortFlags(caps: Record): void { filtered.push(DISABLE_SERVICE_AUTH_CODES_FLAG); } caps.processArguments.args = filtered; + + for (const w3cCaps of w3cCapsList) { + if (!w3cCaps?.alwaysMatch || typeof w3cCaps.alwaysMatch !== 'object') { + continue; + } + w3cCaps.alwaysMatch['appium:processArguments'] = caps.processArguments; + for (const firstMatch of Array.isArray(w3cCaps.firstMatch) ? w3cCaps.firstMatch : []) { + delete firstMatch?.['appium:processArguments']; + } + } } async function requireFreePort(this: FlutterDriver, port: number) { diff --git a/driver/test/ios-dart-vm-service-port.test.mjs b/driver/test/ios-dart-vm-service-port.test.mjs new file mode 100644 index 00000000..e1c69f55 --- /dev/null +++ b/driver/test/ios-dart-vm-service-port.test.mjs @@ -0,0 +1,42 @@ +import assert from 'node:assert/strict'; +import {it} from 'node:test'; + +import {injectDartVmServicePortFlags} from '../build/lib/sessions/ios.js'; + +const w3cCaps = (alwaysMatch = {}, firstMatch = [{}]) => ({alwaysMatch, firstMatch}); + +it('mirrors the injected VM service flags into the W3C caps forwarded to XCUITest', () => { + const caps = {dartVmServicePort: 9123, processArguments: {args: ['--foo'], env: {A: '1'}}}; + const forwarded = w3cCaps({'appium:dartVmServicePort': 9123}, [ + {'appium:processArguments': {args: ['--foo'], env: {A: '1'}}}, + ]); + + injectDartVmServicePortFlags(caps, [forwarded, undefined, {}]); + + const expected = {args: ['--foo', '--vm-service-port=9123', '--disable-service-auth-codes'], env: {A: '1'}}; + assert.deepEqual(caps.processArguments, expected); + assert.deepEqual(forwarded.alwaysMatch['appium:processArguments'], expected); + assert.deepEqual(forwarded.firstMatch, [{}]); +}); + +it('replaces a user-supplied --vm-service-port with the capability value', () => { + const caps = {dartVmServicePort: 9123, processArguments: {args: ['--vm-service-port=1']}}; + const forwarded = w3cCaps(); + + injectDartVmServicePortFlags(caps, [forwarded]); + + assert.deepEqual(forwarded.alwaysMatch['appium:processArguments'].args, [ + '--vm-service-port=9123', + '--disable-service-auth-codes', + ]); +}); + +it('leaves both caps untouched when dartVmServicePort is not set', () => { + const caps = {}; + const forwarded = w3cCaps({'appium:app': 'x'}); + + injectDartVmServicePortFlags(caps, [forwarded]); + + assert.deepEqual(caps, {}); + assert.deepEqual(forwarded, w3cCaps({'appium:app': 'x'})); +}); From 41313b00e8f23b6327bc91919ddccffa2ccd32d4 Mon Sep 17 00:00:00 2001 From: masaaki-hori Date: Mon, 5 Oct 2026 19:05:29 +0900 Subject: [PATCH 2/2] test(ios): cover dartVmServicePort forwarding through startIOSSession Keep injectDartVmServicePortFlags private and assert on the W3C capabilities that startIOSSession hands to XCUITestDriver.createSession. Co-Authored-By: Claude Opus 5.5 --- driver/lib/sessions/ios.ts | 2 +- driver/test/ios-dart-vm-service-port.test.mjs | 48 +++++++++++++------ 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/driver/lib/sessions/ios.ts b/driver/lib/sessions/ios.ts index 20eb961f..3f638393 100644 --- a/driver/lib/sessions/ios.ts +++ b/driver/lib/sessions/ios.ts @@ -259,7 +259,7 @@ async function ensureDeviceLogCaptureStarted( * @param caps The processed capabilities of the driver session. * @param w3cCapsList The raw W3C capabilities forwarded to XCUITest. */ -export function injectDartVmServicePortFlags(caps: Record, w3cCapsList: any[] = []): void { +function injectDartVmServicePortFlags(caps: Record, w3cCapsList: any[] = []): void { const port = caps.dartVmServicePort; if (typeof port !== 'number') { return; diff --git a/driver/test/ios-dart-vm-service-port.test.mjs b/driver/test/ios-dart-vm-service-port.test.mjs index e1c69f55..95a6ff7c 100644 --- a/driver/test/ios-dart-vm-service-port.test.mjs +++ b/driver/test/ios-dart-vm-service-port.test.mjs @@ -1,42 +1,60 @@ import assert from 'node:assert/strict'; -import {it} from 'node:test'; +import {afterEach, beforeEach, it, mock} from 'node:test'; -import {injectDartVmServicePortFlags} from '../build/lib/sessions/ios.js'; +import {XCUITestDriver} from 'appium-xcuitest-driver'; +import {startIOSSession} from '../build/lib/sessions/ios.js'; + +const noopLog = {info() {}, debug() {}, warn() {}, error() {}}; const w3cCaps = (alwaysMatch = {}, firstMatch = [{}]) => ({alwaysMatch, firstMatch}); -it('mirrors the injected VM service flags into the W3C caps forwarded to XCUITest', () => { +let forwardedArgs; + +beforeEach(() => { + forwardedArgs = undefined; + mock.method(XCUITestDriver.prototype, 'createSession', async (...args) => { + forwardedArgs = args; + }); +}); + +afterEach(() => mock.restoreAll()); + +// Without `app`/`bundleId`, startIOSSession returns right after creating the XCUITest session, +// so these tests only exercise what gets forwarded to XCUITest. +const start = (caps, ...args) => startIOSSession.call({log: noopLog}, caps, ...args); + +it('forwards the injected VM service flags to XCUITest', async () => { const caps = {dartVmServicePort: 9123, processArguments: {args: ['--foo'], env: {A: '1'}}}; const forwarded = w3cCaps({'appium:dartVmServicePort': 9123}, [ {'appium:processArguments': {args: ['--foo'], env: {A: '1'}}}, ]); - injectDartVmServicePortFlags(caps, [forwarded, undefined, {}]); + await start(caps, forwarded, undefined, {}); - const expected = {args: ['--foo', '--vm-service-port=9123', '--disable-service-auth-codes'], env: {A: '1'}}; - assert.deepEqual(caps.processArguments, expected); - assert.deepEqual(forwarded.alwaysMatch['appium:processArguments'], expected); + assert.equal(forwardedArgs[0], forwarded); + assert.deepEqual(forwarded.alwaysMatch['appium:processArguments'], { + args: ['--foo', '--vm-service-port=9123', '--disable-service-auth-codes'], + env: {A: '1'}, + }); assert.deepEqual(forwarded.firstMatch, [{}]); }); -it('replaces a user-supplied --vm-service-port with the capability value', () => { +it('replaces a user-supplied --vm-service-port with the capability value', async () => { const caps = {dartVmServicePort: 9123, processArguments: {args: ['--vm-service-port=1']}}; const forwarded = w3cCaps(); - injectDartVmServicePortFlags(caps, [forwarded]); + await start(caps, forwarded); - assert.deepEqual(forwarded.alwaysMatch['appium:processArguments'].args, [ + assert.deepEqual(forwardedArgs[0].alwaysMatch['appium:processArguments'].args, [ '--vm-service-port=9123', '--disable-service-auth-codes', ]); }); -it('leaves both caps untouched when dartVmServicePort is not set', () => { - const caps = {}; +it('forwards the W3C caps unchanged when dartVmServicePort is not set', async () => { const forwarded = w3cCaps({'appium:app': 'x'}); - injectDartVmServicePortFlags(caps, [forwarded]); + await start({}, forwarded); - assert.deepEqual(caps, {}); - assert.deepEqual(forwarded, w3cCaps({'appium:app': 'x'})); + assert.deepEqual(forwardedArgs[0], w3cCaps({'appium:app': 'x'})); });