From cd1ef033a0f14139a01a95cfdc2ea9971d106788 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Tue, 21 Apr 2026 07:57:15 -0400 Subject: [PATCH 01/33] feat(platforms): add run-level runner lifecycle hooks --- PLAN.md | 207 ++++++++++++++++++++ packages/jest/src/__tests__/harness.test.ts | 193 ++++++++++++++---- packages/jest/src/harness.ts | 45 +++-- packages/platforms/src/types.ts | 4 +- 4 files changed, 388 insertions(+), 61 deletions(-) create mode 100644 PLAN.md diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 0000000..dcf501b --- /dev/null +++ b/PLAN.md @@ -0,0 +1,207 @@ +# iOS XCTest Agent MVP Plan + +## Goal + +Implement an iOS XCTest-based agent that can run against both simulators and physical devices, and use it in the MVP to auto-accept permission prompts on a best-effort basis. + +This should be a generic XCTest integration for Harness, not a permission-specific helper, so it can be reused for other iOS system-level automation later. + +## MVP Scope + +- iOS only +- Support both simulator and physical device targets +- Start the XCTest agent once per Harness run +- Stop the XCTest agent during Harness teardown +- Best-effort auto-accept of permission prompts +- Unknown prompts are ignored silently +- No public testing API changes +- No deny/override behavior +- No Android implementation in this phase +- No `simctl privacy` optimization in this phase + +## Architecture Direction + +- Add a generic run-level lifecycle hook so platform runners can prepare and dispose auxiliary tooling needed for the run. +- Implement the iOS side using a generic `XCTest agent` concept owned by `platform-ios`. +- Package the XCTest agent as a small Xcode project generated with `xcodegen`. +- Use the same XCTest agent concept for both iOS simulators and physical devices. +- Keep permission prompt handling as the first XCTest agent capability, not the only one. + +## Phase 1: Lifecycle Integration + +Status: Completed + +Objective: create the Harness and platform lifecycle seam needed to run auxiliary tooling once per run. + +Deliverables: + +- Run-level prepare/dispose hooks available on platform runners +- Harness wired to invoke those hooks once per run +- Coverage for success, error, and teardown paths + +Notes: + +- This phase should remain generic and not mention XCTest directly in shared abstractions. +- The outcome should be reusable by any future platform-owned run helper. + +Parallelization: + +- Can be done independently from XCTest project creation +- Must land before full end-to-end iOS wiring is completed + +## Phase 2: XCTest Agent Project + +Objective: create the reusable iOS XCTest agent project and prove it can be generated reproducibly. + +Deliverables: + +- New internal `xctest-agent` project inside `packages/platform-ios` +- Project generated from `xcodegen` spec rather than manually maintained project internals +- Minimal shared project structure suitable for both simulator and physical-device builds +- Documented build assumptions and cache inputs + +Notes: + +- This phase focuses on project packaging and generation, not Harness integration. +- The top-level naming should stay generic so additional XCTest-driven capabilities can be added later. + +Parallelization: + +- Can proceed in parallel with Phase 1 +- Can also proceed in parallel with the host-side iOS orchestration design work in Phase 3 + +## Phase 3: iOS XCTest Agent Orchestration + +Objective: add host-side orchestration in `platform-ios` to build, cache, start, and stop the XCTest agent. + +Deliverables: + +- Internal `platform-ios` orchestration for the XCTest agent +- Support for simulator destinations +- Support for physical-device destinations +- Artifact reuse strategy for simulator and device builds +- Clear separation between agent lifecycle management and agent behaviors + +Notes: + +- Simulator and physical device should share the same orchestration model, even if build artifacts differ. +- The orchestration should treat the agent as a long-lived run-level helper, not something restarted per test file. + +Parallelization: + +- Depends on enough output from Phase 2 to know what project is being built and launched +- Can be developed in parallel with Phase 4 if the behavior contract is kept narrow + +## Phase 4: Permission Prompt Capability + +Objective: implement the first XCTest agent capability: best-effort auto-accept of permission prompts. + +Deliverables: + +- Permission prompt interruption handling inside the XCTest agent +- Best-effort positive-action tapping behavior +- Silent ignore behavior for unrecognized prompts +- Capability scoped so it can later live beside other XCTest agent behaviors + +Notes: + +- This phase should not introduce any public Harness API. +- The implementation should be framed as one capability of the generic agent. + +Parallelization: + +- Can proceed in parallel with most of Phase 3 once the lifecycle between host and agent is understood +- Final validation depends on Phase 3 integration + +## Phase 5: End-to-End iOS Wiring + +Objective: connect the generic lifecycle, iOS orchestration, and permission capability into the actual Harness run flow. + +Deliverables: + +- iOS simulator runs start the XCTest agent before first app launch +- iOS physical-device runs start the XCTest agent before first app launch +- Both stop the agent during teardown +- Existing app launch and restart behavior remains unchanged +- No per-file permission synchronization is introduced + +Notes: + +- The agent should be started lazily before the first app launch, not eagerly at Harness creation time. +- This phase is where the MVP becomes functionally available. + +Parallelization: + +- Depends on Phases 1 through 4 +- Should be kept small by reusing the outputs of earlier phases rather than adding new concepts + +## Phase 6: Validation And Hardening + +Objective: verify the MVP works on real targets and stabilize the integration. + +Deliverables: + +- Automated coverage for host-side lifecycle and orchestration behavior +- Manual validation on at least one iOS simulator +- Manual validation on at least one physical iOS device +- Basic operational documentation for future contributors + +Validation focus: + +- First-run build experience +- Reuse of cached artifacts on later runs +- Permission prompt auto-accept for at least one real prompt source such as camera +- No obvious teardown leaks or stuck background processes + +Parallelization: + +- Automated coverage can be built alongside Phase 5 +- Manual validation happens after end-to-end wiring is in place + +## Suggested Parallel Workstreams + +### Stream A: Shared Lifecycle + +- Phase 1 + +### Stream B: XCTest Agent Project + +- Phase 2 + +### Stream C: iOS Agent Runtime Orchestration + +- Phase 3 + +### Stream D: Permission Capability + +- Phase 4 + +### Stream E: Final Wiring And Validation + +- Phase 5 +- Phase 6 + +## Dependency Summary + +- Phase 1 is required before final integration +- Phase 2 is required before full orchestration can be finalized +- Phase 3 depends on Phase 2 +- Phase 4 can begin before Phase 3 is finished, but depends on the agent project shape from Phase 2 +- Phase 5 depends on Phases 1 through 4 +- Phase 6 depends on Phase 5 + +## Explicit Non-Goals For This Plan + +- Public permission configuration API +- Per-test or per-file permission overrides +- Deny behavior +- Android permission automation +- Simulator fast-path optimization through `simctl privacy` +- Strict unsupported-permission detection or reporting + +## Follow-Up After MVP + +- Add Android best-effort pregrant support via `adb` +- Add `simctl privacy` fast path for the iOS simulator where supported +- Add more XCTest agent capabilities beyond permission prompts +- Revisit public API design once internal behavior is proven in practice diff --git a/packages/jest/src/__tests__/harness.test.ts b/packages/jest/src/__tests__/harness.test.ts index c58a3ed..40c8f66 100644 --- a/packages/jest/src/__tests__/harness.test.ts +++ b/packages/jest/src/__tests__/harness.test.ts @@ -125,7 +125,7 @@ const createReporter = (): Reporter => { }; const createMetroInstance = ( - overrides: Partial = {} + overrides: Partial = {}, ): MetroInstance => ({ events: createReporter(), httpServer: {} as never, @@ -163,11 +163,13 @@ const createAppMonitor = (): { }; const createPlatformRunner = ( - overrides: Partial = {} + overrides: Partial = {}, ): HarnessPlatformRunner => ({ + prepareRun: vi.fn(async () => undefined), startApp: vi.fn(async () => undefined), restartApp: vi.fn(async () => undefined), stopApp: vi.fn(async () => undefined), + disposeRun: vi.fn(async () => undefined), dispose: vi.fn(async () => undefined), isAppRunning: vi.fn(async () => true), createAppMonitor: () => createAppMonitor().appMonitor, @@ -175,7 +177,7 @@ const createPlatformRunner = ( }); const createHarnessConfig = ( - overrides: Partial = {} + overrides: Partial = {}, ): HarnessConfig => ({ appRegistryComponentName: 'App', @@ -196,7 +198,7 @@ const createHarnessConfig = ( unstable__skipAlreadyIncludedModules: false, webSocketPort: 3001, ...overrides, - } as HarnessConfig); + }) as HarnessConfig; beforeEach(() => { vi.clearAllMocks(); @@ -230,7 +232,7 @@ describe('waitForAppReady', () => { emitReady(); await readyPromise; options.onAttemptReset?.(); - } + }, ); await waitForAppReady({ @@ -255,7 +257,7 @@ describe('waitForAppReady', () => { startAttempt: expect.any(Function), waitForReady: expect.any(Function), waitForCrash: expect.any(Function), - }) + }), ); expect(crashSupervisor.isReady()).toBe(true); @@ -276,7 +278,7 @@ describe('waitForAppReady', () => { mocks.waitForMetroBackedAppReady.mockImplementationOnce( async (options: WaitForMetroBackedAppReadyOptions) => { await options.startAttempt(); - } + }, ); await waitForAppReady({ @@ -324,7 +326,7 @@ describe('getHarness', () => { setTimeout(() => { reject(new DOMException('The operation was aborted', 'AbortError')); }, 20); - }) + }), ); const platform: HarnessPlatform = { @@ -333,7 +335,7 @@ describe('getHarness', () => { name: 'ios', platformId: 'ios', runner: `data:text/javascript,${encodeURIComponent( - 'export default (...args) => globalThis.__HARNESS_PLATFORM_RUNNER__(...args);' + 'export default (...args) => globalThis.__HARNESS_PLATFORM_RUNNER__(...args);', )}`, }; @@ -343,8 +345,8 @@ describe('getHarness', () => { platformReadyTimeout: 10, }), platform, - '/tmp/project' - ) + '/tmp/project', + ), ).rejects.toBeInstanceOf(PlatformReadyTimeoutError); }); @@ -372,14 +374,14 @@ describe('getHarness', () => { name: 'ios', platformId: 'ios', runner: `data:text/javascript,${encodeURIComponent( - 'export default (...args) => globalThis.__HARNESS_PLATFORM_RUNNER__(...args);' + 'export default (...args) => globalThis.__HARNESS_PLATFORM_RUNNER__(...args);', )}`, }; const harness = await getHarness( createHarnessConfig(), platform, - '/tmp/project' + '/tmp/project', ); expect(runner).toHaveBeenCalledWith( @@ -389,10 +391,64 @@ describe('getHarness', () => { }), expect.objectContaining({ signal: expect.any(AbortSignal), - }) + }), + ); + + await harness.dispose(); + }); + + it('prepares the platform runner once during harness creation and disposes it during teardown', async () => { + const { serverBridge } = createBridgeServer(); + const appMonitor = createAppMonitor(); + const callOrder: string[] = []; + const platformInstance = createPlatformRunner({ + prepareRun: vi.fn(async () => { + callOrder.push('prepareRun'); + }), + createAppMonitor: () => appMonitor.appMonitor, + disposeRun: vi.fn(async () => { + callOrder.push('disposeRun'); + }), + dispose: vi.fn(async () => { + callOrder.push('dispose'); + }), + }); + const metroInstance = createMetroInstance(); + + mocks.getBridgeServer.mockResolvedValue(serverBridge); + mocks.getMetroInstance.mockResolvedValue(metroInstance); + + ( + globalThis as typeof globalThis & { + __HARNESS_PLATFORM_RUNNER__?: (...args: unknown[]) => Promise; + } + ).__HARNESS_PLATFORM_RUNNER__ = vi.fn(async () => platformInstance); + + const platform: HarnessPlatform = { + config: {}, + getResourceLockKey: () => 'ios:test-platform-run-lifecycle', + name: 'ios', + platformId: 'ios', + runner: `data:text/javascript,${encodeURIComponent( + 'export default (...args) => globalThis.__HARNESS_PLATFORM_RUNNER__(...args);', + )}`, + }; + + const harness = await getHarness( + createHarnessConfig(), + platform, + '/tmp/project', ); + expect(platformInstance.prepareRun).toHaveBeenCalledTimes(1); + expect(appMonitor.appMonitor.start).toHaveBeenCalledTimes(1); + expect(callOrder).toEqual(['prepareRun']); + await harness.dispose(); + + expect(platformInstance.disposeRun).toHaveBeenCalledTimes(1); + expect(platformInstance.dispose).toHaveBeenCalledTimes(1); + expect(callOrder).toEqual(['prepareRun', 'disposeRun', 'dispose']); }); it('resolves and exposes a fallback Metro port before platform init', async () => { @@ -422,14 +478,14 @@ describe('getHarness', () => { name: 'android', platformId: 'android', runner: `data:text/javascript,${encodeURIComponent( - 'export default (...args) => globalThis.__HARNESS_PLATFORM_RUNNER__(...args);' + 'export default (...args) => globalThis.__HARNESS_PLATFORM_RUNNER__(...args);', )}`, }; const harness = await getHarness( createHarnessConfig(), platform, - '/tmp/project' + '/tmp/project', ); expect(harness.config.metroPort).toBe(8082); @@ -439,7 +495,7 @@ describe('getHarness', () => { metroPort: 8082, }), }), - expect.any(AbortSignal) + expect.any(AbortSignal), ); expect(runner).toHaveBeenCalledWith( platform.config, @@ -448,7 +504,7 @@ describe('getHarness', () => { }), expect.objectContaining({ signal: expect.any(AbortSignal), - }) + }), ); expect(mocks.logMetroPortFallback).toHaveBeenCalledWith(8081, 8082); @@ -467,7 +523,7 @@ describe('getHarness', () => { }; await expect( - getHarness(createHarnessConfig(), platform, '/tmp/project') + getHarness(createHarnessConfig(), platform, '/tmp/project'), ).rejects.toBeInstanceOf(MetroPortRangeExhaustedError); expect(mocks.getBridgeServer).not.toHaveBeenCalled(); @@ -496,19 +552,76 @@ describe('getHarness', () => { name: 'legacy-ios', platformId: 'ios', runner: `data:text/javascript,${encodeURIComponent( - 'export default (...args) => globalThis.__HARNESS_PLATFORM_RUNNER__(...args);' + 'export default (...args) => globalThis.__HARNESS_PLATFORM_RUNNER__(...args);', )}`, }; const harness = await getHarness( createHarnessConfig(), platform, - '/tmp/project' + '/tmp/project', ); await harness.dispose(); }); + it('disposes a prepared platform runner when harness creation fails after prepareRun', async () => { + const { serverBridge } = createBridgeServer(); + const appMonitor = createAppMonitor(); + const disposeRun = vi.fn(async () => undefined); + const dispose = vi.fn(async () => undefined); + const platformInstance = createPlatformRunner({ + createAppMonitor: () => appMonitor.appMonitor, + disposeRun, + dispose, + }); + const metroInstance = createMetroInstance(); + const plugin = definePlugin<{}, HarnessConfig, HarnessPlatform>({ + name: 'failing-before-run-plugin', + createState: () => ({}), + hooks: { + harness: { + beforeRun: () => { + throw new Error('before-run failed'); + }, + }, + }, + }); + + mocks.getBridgeServer.mockResolvedValue(serverBridge); + mocks.getMetroInstance.mockResolvedValue(metroInstance); + + ( + globalThis as typeof globalThis & { + __HARNESS_PLATFORM_RUNNER__?: (...args: unknown[]) => Promise; + } + ).__HARNESS_PLATFORM_RUNNER__ = vi.fn(async () => platformInstance); + + const platform: HarnessPlatform = { + config: {}, + getResourceLockKey: () => 'ios:test-platform-run-lifecycle-error', + name: 'ios', + platformId: 'ios', + runner: `data:text/javascript,${encodeURIComponent( + 'export default (...args) => globalThis.__HARNESS_PLATFORM_RUNNER__(...args);', + )}`, + }; + + await expect( + getHarness( + createHarnessConfig({ + plugins: [plugin], + }), + platform, + '/tmp/project', + ), + ).rejects.toThrow('before-run failed'); + + expect(platformInstance.prepareRun).toHaveBeenCalledTimes(1); + expect(disposeRun).toHaveBeenCalledTimes(1); + expect(dispose).toHaveBeenCalledTimes(1); + }); + it('routes ensureAppReady through the shared Metro startup helper', async () => { const { serverBridge, emitReady } = createBridgeServer(); const appMonitor = createAppMonitor(); @@ -527,7 +640,7 @@ describe('getHarness', () => { const readyPromise = options.waitForReady(new AbortController().signal); emitReady(); await readyPromise; - } + }, ); ( @@ -547,7 +660,7 @@ describe('getHarness', () => { name: 'ios', platformId: 'ios', runner: `data:text/javascript,${encodeURIComponent( - 'export default (...args) => globalThis.__HARNESS_PLATFORM_RUNNER__(...args);' + 'export default (...args) => globalThis.__HARNESS_PLATFORM_RUNNER__(...args);', )}`, getResourceLockKey: () => 'ios:simulator:iPhone 17 Pro:26.2', }; @@ -557,7 +670,7 @@ describe('getHarness', () => { bridgeTimeout: 1, }), platform, - '/tmp/project' + '/tmp/project', ); await harness.ensureAppReady('/tmp/example.harness.ts'); @@ -593,7 +706,7 @@ describe('getHarness', () => { const readyPromise = options.waitForReady(new AbortController().signal); emitReady(); await readyPromise; - } + }, ); ( @@ -613,7 +726,7 @@ describe('getHarness', () => { name: 'ios', platformId: 'ios', runner: `data:text/javascript,${encodeURIComponent( - 'export default (...args) => globalThis.__HARNESS_PLATFORM_RUNNER__(...args);' + 'export default (...args) => globalThis.__HARNESS_PLATFORM_RUNNER__(...args);', )}`, getResourceLockKey: () => 'ios:simulator:iPhone 17 Pro:26.2', }; @@ -621,13 +734,13 @@ describe('getHarness', () => { const harness = await getHarness( createHarnessConfig(), platform, - '/tmp/project' + '/tmp/project', ); expect(mocks.getBridgeServer).toHaveBeenCalledWith( expect.objectContaining({ noServer: true, - }) + }), ); expect(mocks.getMetroInstance).toHaveBeenCalledWith( expect.objectContaining({ @@ -635,7 +748,7 @@ describe('getHarness', () => { [HARNESS_BRIDGE_PATH]: serverBridge.ws, }, }), - expect.any(AbortSignal) + expect.any(AbortSignal), ); await harness.restart('/tmp/restart.harness.ts'); @@ -691,7 +804,7 @@ describe('plugins', () => { ctx.appLaunchOptions == null ? 'no-launch-options' : 'launch-options' - }` + }`, ); }, beforeRun: (ctx) => { @@ -700,24 +813,24 @@ describe('plugins', () => { ctx.appLaunchOptions == null ? 'no-launch-options' : 'launch-options' - }` + }`, ); }, afterRun: (ctx) => { observedHooks.push( - `afterRun:${ctx.state.creationCount}:${ctx.reason}` + `afterRun:${ctx.state.creationCount}:${ctx.reason}`, ); }, beforeDispose: (ctx) => { observedHooks.push( - `beforeDispose:${ctx.state.creationCount}:${ctx.reason}` + `beforeDispose:${ctx.state.creationCount}:${ctx.reason}`, ); }, }, runtime: { ready: (ctx) => { observedHooks.push( - `runtime.ready:${ctx.runId}:${ctx.device.platform}` + `runtime.ready:${ctx.runId}:${ctx.device.platform}`, ); }, disconnected: (ctx) => { @@ -743,7 +856,7 @@ describe('plugins', () => { name: 'ios', platformId: 'ios', runner: `data:text/javascript,${encodeURIComponent( - 'export default (...args) => globalThis.__HARNESS_PLATFORM_RUNNER__(...args);' + 'export default (...args) => globalThis.__HARNESS_PLATFORM_RUNNER__(...args);', )}`, getResourceLockKey: () => 'ios:simulator:iPhone 17 Pro:26.2', }; @@ -753,7 +866,7 @@ describe('plugins', () => { plugins: [plugin], }), platform, - '/tmp/project' + '/tmp/project', ); harness.setRunState({ @@ -828,7 +941,7 @@ describe('plugins', () => { name: 'ios', platformId: 'ios', runner: `data:text/javascript,${encodeURIComponent( - 'export default (...args) => globalThis.__HARNESS_PLATFORM_RUNNER__(...args);' + 'export default (...args) => globalThis.__HARNESS_PLATFORM_RUNNER__(...args);', )}`, getResourceLockKey: () => resourceKey, }; @@ -836,13 +949,13 @@ describe('plugins', () => { const firstHarness = await getHarness( createHarnessConfig(), platform, - '/tmp/project' + '/tmp/project', ); const secondHarnessPromise = getHarness( createHarnessConfig(), platform, - '/tmp/project' + '/tmp/project', ); await new Promise((resolve) => setTimeout(resolve, 1100)); @@ -864,7 +977,7 @@ describe('plugins', () => { describe('StartupStallError', () => { it('includes the configured timeout and attempt count', () => { expect(new StartupStallError(1_500, 4).message).toBe( - 'The app did not request its Metro bundle after 4 launch attempts within 1500ms. Last Metro status: unknown.' + 'The app did not request its Metro bundle after 4 launch attempts within 1500ms. Last Metro status: unknown.', ); }); }); diff --git a/packages/jest/src/harness.ts b/packages/jest/src/harness.ts index c065a49..92bafda 100644 --- a/packages/jest/src/harness.ts +++ b/packages/jest/src/harness.ts @@ -78,7 +78,7 @@ export type Harness = { config: HarnessConfig; runTests: ( path: string, - options: HarnessRunTestsOptions + options: HarnessRunTestsOptions, ) => Promise; ensureAppReady: (testFilePath: string) => Promise; restart: (testFilePath?: string) => Promise; @@ -92,7 +92,7 @@ export type Harness = { export const maybeLogMetroCacheReuse = ( config: HarnessConfig, platform: HarnessPlatform, - projectRoot: string + projectRoot: string, ): void => { if (config.unstable__enableMetroCache && isMetroCacheReusable(projectRoot)) { logMetroCacheReused(platform); @@ -116,7 +116,7 @@ const waitForAbort = (signal: AbortSignal): Promise => { () => { reject(signal.reason ?? createAbortError()); }, - { once: true } + { once: true }, ); }); }; @@ -239,7 +239,7 @@ const getHarnessInternal = async ( config: HarnessConfig, platform: HarnessPlatform, projectRoot: string, - signal: AbortSignal + signal: AbortSignal, ): Promise => { const context: HarnessContext = { platform, @@ -247,7 +247,7 @@ const getHarnessInternal = async ( harnessLogger.debug( 'creating Harness internals for runner=%s platform=%s', platform.name, - platform.platformId + platform.platformId, ); const resourceLockKey = await (platform.getResourceLockKey?.() ?? getDefaultResourceLockKey(platform)); @@ -261,7 +261,7 @@ const getHarnessInternal = async ( harnessLogger.debug( 'waiting in queue for runner=%s key=%s', platform.name, - resourceLockKey + resourceLockKey, ); }, onStillWaiting: (elapsedMs) => { @@ -275,7 +275,7 @@ const getHarnessInternal = async ( 'still waiting in queue for runner=%s key=%s elapsedMs=%d', platform.name, resourceLockKey, - elapsedMs + elapsedMs, ); }, }); @@ -285,7 +285,7 @@ const getHarnessInternal = async ( harnessLogger.debug( 'resource lock acquired for runner=%s key=%s', platform.name, - resourceLockKey + resourceLockKey, ); try { const { @@ -358,7 +358,7 @@ const getHarnessInternal = async ( object, HarnessConfig, HarnessPlatform - > + >, >( name: TName, payload: Omit< @@ -373,7 +373,7 @@ const getHarnessInternal = async ( | 'timestamp' | 'abortSignal' | 'meta' - > + >, ) => { trackHook(pluginManager.callHook(name, payload)); }; @@ -384,11 +384,11 @@ const getHarnessInternal = async ( context, }); harnessLogger.debug( - 'starting Metro, platform runner, and bridge initialization' + 'starting Metro, platform runner, and bridge initialization', ); harnessLogger.debug( 'bridge server initialized on Metro websocket path %s', - HARNESS_BRIDGE_PATH + HARNESS_BRIDGE_PATH, ); const [metroInstance, platformInstance] = await (async () => { try { @@ -402,7 +402,7 @@ const getHarnessInternal = async ( serverBridge.ws as unknown as MetroWebSocketEndpoint, }, }, - signal + signal, ).then((instance) => { harnessLogger.debug('Metro initialized'); return instance; @@ -415,7 +415,7 @@ const getHarnessInternal = async ( .then((module) => module.default(platform.config, runtimeConfig, { signal, - } satisfies HarnessPlatformInitOptions) + } satisfies HarnessPlatformInitOptions), ) .then((instance) => { harnessLogger.debug('platform runner initialized'); @@ -650,6 +650,8 @@ const getHarnessInternal = async ( harnessLogger.debug('client log forwarding enabled'); } + let didPrepareRun = false; + const dispose = async (reason: 'normal' | 'abort' | 'error' = 'normal') => { harnessLogger.debug('disposing Harness (reason=%s)', reason); let hookError: unknown; @@ -689,6 +691,7 @@ const getHarnessInternal = async ( await Promise.all([ crashSupervisor.dispose(), serverBridge.dispose(), + didPrepareRun ? platformInstance.disposeRun?.() : undefined, platformInstance.dispose(), metroInstance.dispose(), metroPortLease?.release(), @@ -721,6 +724,8 @@ const getHarnessInternal = async ( appLaunchOptions, }); await flushPendingHooks(); + await platformInstance.prepareRun?.(); + didPrepareRun = true; await appMonitor.start(); harnessLogger.debug('app monitor started'); await pluginManager.callHook('harness:before-run', { @@ -737,7 +742,7 @@ const getHarnessInternal = async ( await dispose( error instanceof DOMException && error.name === 'AbortError' ? 'abort' - : 'error' + : 'error', ); throw error; } @@ -758,7 +763,7 @@ const getHarnessInternal = async ( crashSupervisor.reset(); harnessLogger.debug( - 'app not ready, waiting for launch and runtime readiness' + 'app not ready, waiting for launch and runtime readiness', ); await waitForAppReady({ metroInstance, @@ -783,7 +788,7 @@ const getHarnessInternal = async ( harnessLogger.debug( 'restarting app (testFile=%s mode=%s)', testFilePath ?? 'n/a', - testFilePath ? 'stop-and-ensure-ready' : 'direct-restart' + testFilePath ? 'stop-and-ensure-ready' : 'direct-restart', ); if (testFilePath) { @@ -848,17 +853,17 @@ const getHarnessInternal = async ( export const getHarness = async ( config: HarnessConfig, platform: HarnessPlatform, - projectRoot: string + projectRoot: string, ): Promise => { harnessLogger.debug( 'creating Harness with platform ready timeout %dms', - config.platformReadyTimeout + config.platformReadyTimeout, ); return await getHarnessInternal( config, platform, projectRoot, - new AbortController().signal + new AbortController().signal, ); }; diff --git a/packages/platforms/src/types.ts b/packages/platforms/src/types.ts index 406e958..63dc12d 100644 --- a/packages/platforms/src/types.ts +++ b/packages/platforms/src/types.ts @@ -99,14 +99,16 @@ export type AppLaunchOptions = | VegaAppLaunchOptions; export type HarnessPlatformRunner = { + prepareRun?: () => Promise; startApp: (options?: AppLaunchOptions) => Promise; restartApp: (options?: AppLaunchOptions) => Promise; stopApp: () => Promise; + disposeRun?: () => Promise; dispose: () => Promise; isAppRunning: () => Promise; createAppMonitor: (options?: CreateAppMonitorOptions) => AppMonitor; getCrashDetails?: ( - options: CrashDetailsLookupOptions + options: CrashDetailsLookupOptions, ) => Promise; }; From e9a68bd29436bf937f8830e8e4393c97489bda0b Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Tue, 21 Apr 2026 08:00:32 -0400 Subject: [PATCH 02/33] feat(platform-ios): scaffold the xctest agent project --- PLAN.md | 2 + packages/platform-ios/package.json | 3 ++ packages/platform-ios/xctest-agent/.gitignore | 2 + .../HarnessXCTestAgentHost/AgentHostApp.swift | 6 +++ .../HarnessXCTestAgentTests.swift | 11 +++++ packages/platform-ios/xctest-agent/README.md | 37 ++++++++++++++++ .../platform-ios/xctest-agent/project.yml | 44 +++++++++++++++++++ 7 files changed, 105 insertions(+) create mode 100644 packages/platform-ios/xctest-agent/.gitignore create mode 100644 packages/platform-ios/xctest-agent/HarnessXCTestAgentHost/AgentHostApp.swift create mode 100644 packages/platform-ios/xctest-agent/HarnessXCTestAgentTests/HarnessXCTestAgentTests.swift create mode 100644 packages/platform-ios/xctest-agent/README.md create mode 100644 packages/platform-ios/xctest-agent/project.yml diff --git a/PLAN.md b/PLAN.md index dcf501b..532d442 100644 --- a/PLAN.md +++ b/PLAN.md @@ -51,6 +51,8 @@ Parallelization: ## Phase 2: XCTest Agent Project +Status: Completed + Objective: create the reusable iOS XCTest agent project and prove it can be generated reproducibly. Deliverables: diff --git a/packages/platform-ios/package.json b/packages/platform-ios/package.json index 24cd179..9455042 100644 --- a/packages/platform-ios/package.json +++ b/packages/platform-ios/package.json @@ -3,6 +3,9 @@ "description": "Apple platform for React Native Harness", "version": "1.1.0", "type": "module", + "scripts": { + "xctest-agent:generate": "xcodegen generate --spec ./xctest-agent/project.yml --project ./xctest-agent" + }, "main": "./dist/index.js", "module": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/platform-ios/xctest-agent/.gitignore b/packages/platform-ios/xctest-agent/.gitignore new file mode 100644 index 0000000..8bfc69d --- /dev/null +++ b/packages/platform-ios/xctest-agent/.gitignore @@ -0,0 +1,2 @@ +HarnessXCTestAgent.xcodeproj/ +build/ diff --git a/packages/platform-ios/xctest-agent/HarnessXCTestAgentHost/AgentHostApp.swift b/packages/platform-ios/xctest-agent/HarnessXCTestAgentHost/AgentHostApp.swift new file mode 100644 index 0000000..8852d12 --- /dev/null +++ b/packages/platform-ios/xctest-agent/HarnessXCTestAgentHost/AgentHostApp.swift @@ -0,0 +1,6 @@ +import UIKit + +@main +final class AgentHostApp: UIResponder, UIApplicationDelegate { + var window: UIWindow? +} diff --git a/packages/platform-ios/xctest-agent/HarnessXCTestAgentTests/HarnessXCTestAgentTests.swift b/packages/platform-ios/xctest-agent/HarnessXCTestAgentTests/HarnessXCTestAgentTests.swift new file mode 100644 index 0000000..361c931 --- /dev/null +++ b/packages/platform-ios/xctest-agent/HarnessXCTestAgentTests/HarnessXCTestAgentTests.swift @@ -0,0 +1,11 @@ +import XCTest + +final class HarnessXCTestAgentTests: XCTestCase { + override func setUpWithError() throws { + continueAfterFailure = false + } + + func testAgentProjectBootstraps() { + XCTAssertTrue(true) + } +} diff --git a/packages/platform-ios/xctest-agent/README.md b/packages/platform-ios/xctest-agent/README.md new file mode 100644 index 0000000..f37726a --- /dev/null +++ b/packages/platform-ios/xctest-agent/README.md @@ -0,0 +1,37 @@ +# Harness XCTest Agent + +Internal XcodeGen-backed project used by `@react-native-harness/platform-apple` to build a reusable XCTest agent. + +## Generate The Project + +From the repo root: + +```bash +pnpm --filter @react-native-harness/platform-apple run xctest-agent:generate +``` + +The generated project is intentionally not committed. The source of truth is `xctest-agent/project.yml` plus the files referenced from it. + +## Project Shape + +- `HarnessXCTestAgentHost`: minimal iOS host app target used to package and run the agent on simulator and physical-device destinations +- `HarnessXCTestAgentTests`: UI-testing bundle where agent capabilities live +- `HarnessXCTestAgent` scheme: stable scheme name for future host-side orchestration + +## Build Assumptions + +- `xcodegen` is available on the host machine +- Xcode and the iOS platform SDKs are installed +- Simulator builds can use the generated project without additional signing configuration +- Physical-device builds require signing inputs, such as `DEVELOPMENT_TEAM`, to be provided by the caller at build time +- The project stays generic so additional XCTest-driven behaviors can be added without renaming targets or schemes + +## Cache Inputs + +When build artifact caching is added, these files should be treated as the primary cache inputs for project generation and XCTest agent builds: + +- `packages/platform-ios/xctest-agent/project.yml` +- `packages/platform-ios/xctest-agent/HarnessXCTestAgentHost/AgentHostApp.swift` +- `packages/platform-ios/xctest-agent/HarnessXCTestAgentTests/HarnessXCTestAgentTests.swift` + +The selected Xcode version and any injected signing settings should also be part of higher-level cache keys because they affect the produced artifacts. diff --git a/packages/platform-ios/xctest-agent/project.yml b/packages/platform-ios/xctest-agent/project.yml new file mode 100644 index 0000000..9462dc1 --- /dev/null +++ b/packages/platform-ios/xctest-agent/project.yml @@ -0,0 +1,44 @@ +name: HarnessXCTestAgent +options: + createIntermediateGroups: true +settings: + base: + SWIFT_VERSION: 5.0 + IPHONEOS_DEPLOYMENT_TARGET: 16.0 + MARKETING_VERSION: 1.0 + CURRENT_PROJECT_VERSION: 1 + CODE_SIGN_STYLE: Automatic +targets: + HarnessXCTestAgentHost: + type: application + platform: iOS + sources: + - path: HarnessXCTestAgentHost + settings: + base: + GENERATE_INFOPLIST_FILE: YES + PRODUCT_BUNDLE_IDENTIFIER: dev.reactnativeharness.xctest-agent.host + TARGETED_DEVICE_FAMILY: 1,2 + SUPPORTED_PLATFORMS: iphoneos iphonesimulator + HarnessXCTestAgentTests: + type: bundle.ui-testing + platform: iOS + testTargetName: HarnessXCTestAgentHost + sources: + - path: HarnessXCTestAgentTests + settings: + base: + GENERATE_INFOPLIST_FILE: YES + PRODUCT_BUNDLE_IDENTIFIER: dev.reactnativeharness.xctest-agent.tests + TARGETED_DEVICE_FAMILY: 1,2 + SUPPORTED_PLATFORMS: iphoneos iphonesimulator +schemes: + HarnessXCTestAgent: + build: + targets: + HarnessXCTestAgentHost: all + HarnessXCTestAgentTests: all + test: + gatherCoverageData: false + targets: + - name: HarnessXCTestAgentTests From 426a3fb4233b5d6e51159bec4b82b142a431dbdc Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Tue, 21 Apr 2026 08:17:56 -0400 Subject: [PATCH 03/33] feat(platform-ios): orchestrate the xctest agent lifecycle --- PLAN.md | 2 + .../__tests__/instance-xctest-agent.test.ts | 127 +++++++ .../src/__tests__/xctest-agent.test.ts | 214 ++++++++++++ packages/platform-ios/src/instance.ts | 59 +++- packages/platform-ios/src/xctest-agent.ts | 312 ++++++++++++++++++ .../HarnessXCTestAgentTests.swift | 13 +- 6 files changed, 711 insertions(+), 16 deletions(-) create mode 100644 packages/platform-ios/src/__tests__/instance-xctest-agent.test.ts create mode 100644 packages/platform-ios/src/__tests__/xctest-agent.test.ts create mode 100644 packages/platform-ios/src/xctest-agent.ts diff --git a/PLAN.md b/PLAN.md index 532d442..74d2cc2 100644 --- a/PLAN.md +++ b/PLAN.md @@ -74,6 +74,8 @@ Parallelization: ## Phase 3: iOS XCTest Agent Orchestration +Status: Completed + Objective: add host-side orchestration in `platform-ios` to build, cache, start, and stop the XCTest agent. Deliverables: diff --git a/packages/platform-ios/src/__tests__/instance-xctest-agent.test.ts b/packages/platform-ios/src/__tests__/instance-xctest-agent.test.ts new file mode 100644 index 0000000..06ed328 --- /dev/null +++ b/packages/platform-ios/src/__tests__/instance-xctest-agent.test.ts @@ -0,0 +1,127 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { + DEFAULT_METRO_PORT, + type Config as HarnessConfig, +} from '@react-native-harness/config'; +import * as simctl from '../xcrun/simctl.js'; +import * as devicectl from '../xcrun/devicectl.js'; + +const mocks = vi.hoisted(() => ({ + dispose: vi.fn(async () => undefined), + ensureStarted: vi.fn(async () => undefined), + prepare: vi.fn(async () => undefined), + createXCTestAgentController: vi.fn(), +})); + +vi.mock('../xctest-agent.js', () => ({ + createXCTestAgentController: mocks.createXCTestAgentController, +})); + +import { + getApplePhysicalDevicePlatformInstance, + getAppleSimulatorPlatformInstance, +} from '../instance.js'; + +const harnessConfig = { + metroPort: DEFAULT_METRO_PORT, +} as HarnessConfig; + +describe('iOS XCTest agent runner integration', () => { + beforeEach(() => { + vi.clearAllMocks(); + mocks.createXCTestAgentController.mockReturnValue({ + prepare: mocks.prepare, + ensureStarted: mocks.ensureStarted, + stop: vi.fn(async () => undefined), + dispose: mocks.dispose, + }); + }); + + it('prepares and lazily starts the simulator XCTest agent', async () => { + vi.spyOn(simctl, 'getSimulatorId').mockResolvedValue('sim-udid'); + vi.spyOn(simctl, 'isAppInstalled').mockResolvedValue(true); + vi.spyOn(simctl, 'getSimulatorStatus').mockResolvedValue('Booted'); + vi.spyOn(simctl, 'applyHarnessJsLocationOverride').mockResolvedValue( + undefined, + ); + vi.spyOn(simctl, 'startApp').mockResolvedValue(undefined); + vi.spyOn(simctl, 'stopApp').mockResolvedValue(undefined); + vi.spyOn(simctl, 'clearHarnessJsLocationOverride').mockResolvedValue( + undefined, + ); + + const instance = await getAppleSimulatorPlatformInstance( + { + name: 'ios', + device: { + type: 'simulator', + name: 'iPhone 16 Pro', + systemVersion: '18.0', + }, + bundleId: 'com.harnessplayground', + }, + harnessConfig, + { + signal: new AbortController().signal, + }, + ); + + await instance.prepareRun?.(); + await instance.startApp(); + await instance.disposeRun?.(); + + expect(mocks.createXCTestAgentController).toHaveBeenCalledWith({ + target: { + kind: 'simulator', + id: 'sim-udid', + }, + }); + expect(mocks.prepare).toHaveBeenCalledTimes(1); + expect(mocks.ensureStarted).toHaveBeenCalledTimes(1); + expect(mocks.dispose).toHaveBeenCalledTimes(1); + }); + + it('prepares and lazily starts the physical-device XCTest agent', async () => { + vi.spyOn(devicectl, 'getDevice').mockResolvedValue({ + identifier: 'device-udid', + deviceProperties: { + name: 'My iPhone', + osVersionNumber: '18.0', + }, + hardwareProperties: { + marketingName: 'iPhone', + productType: 'iPhone17,1', + udid: 'device-udid', + }, + }); + vi.spyOn(devicectl, 'isAppInstalled').mockResolvedValue(true); + vi.spyOn(devicectl, 'startApp').mockResolvedValue(undefined); + vi.spyOn(devicectl, 'stopApp').mockResolvedValue(undefined); + + const instance = await getApplePhysicalDevicePlatformInstance( + { + name: 'ios-device', + device: { + type: 'physical', + name: 'My iPhone', + }, + bundleId: 'com.harnessplayground', + }, + harnessConfig, + ); + + await instance.prepareRun?.(); + await instance.restartApp(); + await instance.disposeRun?.(); + + expect(mocks.createXCTestAgentController).toHaveBeenCalledWith({ + target: { + kind: 'device', + id: 'device-udid', + }, + }); + expect(mocks.prepare).toHaveBeenCalledTimes(1); + expect(mocks.ensureStarted).toHaveBeenCalledTimes(1); + expect(mocks.dispose).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/platform-ios/src/__tests__/xctest-agent.test.ts b/packages/platform-ios/src/__tests__/xctest-agent.test.ts new file mode 100644 index 0000000..810eeb1 --- /dev/null +++ b/packages/platform-ios/src/__tests__/xctest-agent.test.ts @@ -0,0 +1,214 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import fs from 'node:fs'; +import path from 'node:path'; +import { createHash } from 'node:crypto'; +import { fileURLToPath } from 'node:url'; + +const mocks = vi.hoisted(() => ({ + kill: vi.fn(), + spawn: vi.fn(), +})); + +vi.mock('@react-native-harness/tools', async () => { + const actual = await vi.importActual< + typeof import('@react-native-harness/tools') + >('@react-native-harness/tools'); + + return { + ...actual, + spawn: mocks.spawn, + }; +}); + +import { createXCTestAgentController } from '../xctest-agent.js'; + +const projectRoot = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + '..', + '..', + 'xctest-agent', +); +const buildRoot = path.join(projectRoot, 'build'); + +const createLongRunningSubprocess = () => { + let stopped = false; + + const iterable = { + nodeChildProcess: Promise.resolve({ + kill: vi.fn(() => { + stopped = true; + mocks.kill(); + }), + }), + async *[Symbol.asyncIterator]() { + while (!stopped) { + await new Promise((resolve) => setTimeout(resolve, 0)); + } + }, + }; + + return iterable; +}; + +describe('xctest-agent orchestration', () => { + beforeEach(() => { + vi.clearAllMocks(); + rmBuildRoot(); + mocks.spawn.mockImplementation((file: string, args?: string[]) => { + if (file === 'xcodebuild' && args?.[0] === 'test-without-building') { + return createLongRunningSubprocess(); + } + + return createLongRunningSubprocess(); + }); + }); + + afterEach(() => { + rmBuildRoot(); + }); + + it('builds the simulator agent artifacts and writes a cache manifest', async () => { + const controller = createXCTestAgentController({ + target: { + kind: 'simulator', + id: 'sim-123', + }, + }); + + await controller.prepare(); + + expect(mocks.spawn).toHaveBeenNthCalledWith( + 1, + 'xcodegen', + expect.arrayContaining([ + 'generate', + '--spec', + expect.stringContaining('project.yml'), + ]), + ); + expect(mocks.spawn).toHaveBeenNthCalledWith( + 2, + 'xcodebuild', + expect.arrayContaining([ + 'build-for-testing', + '-destination', + 'platform=iOS Simulator,id=sim-123', + ]), + ); + expect( + fs.existsSync(path.join(buildRoot, 'simulator', 'build-manifest.json')), + ).toBe(true); + }); + + it('reuses cached build artifacts for repeated prepares on the same destination kind', async () => { + fs.mkdirSync(path.join(buildRoot, 'device', 'Build', 'Products'), { + recursive: true, + }); + fs.writeFileSync( + path.join(buildRoot, 'device', 'build-manifest.json'), + JSON.stringify({ + buildInputsHash: getCurrentInputsHash(), + destinationKind: 'device', + }), + ); + + const controller = createXCTestAgentController({ + target: { + kind: 'device', + id: 'device-123', + }, + }); + + await controller.prepare(); + + expect(mocks.spawn).toHaveBeenCalledTimes(1); + expect(mocks.spawn).toHaveBeenCalledWith( + 'xcodegen', + expect.arrayContaining(['generate']), + ); + }); + + it('starts the agent lazily and stops the long-lived test process on dispose', async () => { + const controller = createXCTestAgentController({ + target: { + kind: 'simulator', + id: 'sim-999', + }, + capabilities: [ + { + getLaunchEnvironment: () => ({ + HARNESS_XCTEST_AGENT_MODE: 'test', + }), + }, + ], + }); + + await controller.ensureStarted(); + await controller.ensureStarted(); + + expect(mocks.spawn).toHaveBeenCalledTimes(3); + expect(mocks.spawn).toHaveBeenLastCalledWith( + 'xcodebuild', + expect.arrayContaining([ + 'test-without-building', + '-destination', + 'platform=iOS Simulator,id=sim-999', + ]), + expect.objectContaining({ + env: expect.objectContaining({ + HARNESS_XCTEST_AGENT_MODE: 'test', + }), + }), + ); + + await controller.dispose(); + + expect(mocks.kill).toHaveBeenCalledTimes(1); + }); +}); + +const rmBuildRoot = () => { + fs.rmSync(buildRoot, { + force: true, + recursive: true, + }); +}; + +const getCurrentInputsHash = (): string => { + const hash = createHash('sha256'); + + for (const filePath of getInputFiles(projectRoot)) { + hash.update(path.relative(projectRoot, filePath)); + hash.update('\0'); + hash.update(fs.readFileSync(filePath)); + hash.update('\0'); + } + + return hash.digest('hex'); +}; + +const getInputFiles = (root: string): string[] => { + const entries = fs.readdirSync(root, { withFileTypes: true }); + const files: string[] = []; + + for (const entry of entries) { + if ( + entry.name === 'build' || + entry.name.endsWith('.xcodeproj') || + entry.name === '.gitignore' + ) { + continue; + } + + const entryPath = path.join(root, entry.name); + + if (entry.isDirectory()) { + files.push(...getInputFiles(entryPath)); + continue; + } + + files.push(entryPath); + } + + return files.sort(); +}; diff --git a/packages/platform-ios/src/instance.ts b/packages/platform-ios/src/instance.ts index 554ebb1..5785dc1 100644 --- a/packages/platform-ios/src/instance.ts +++ b/packages/platform-ios/src/instance.ts @@ -25,6 +25,7 @@ import { import { HarnessAppPathError } from './errors.js'; import { logger } from '@react-native-harness/tools'; import fs from 'node:fs'; +import { createXCTestAgentController } from './xctest-agent.js'; const iosInstanceLogger = logger.child('ios-instance'); @@ -53,14 +54,14 @@ const createNoopAppMonitor = (): AppMonitor => ({ export const getAppleSimulatorPlatformInstance = async ( config: ApplePlatformConfig, harnessConfig: HarnessConfig, - init: HarnessPlatformInitOptions + init: HarnessPlatformInitOptions, ): Promise => { assertAppleDeviceSimulator(config.device); const detectNativeCrashes = harnessConfig.detectNativeCrashes ?? true; const udid = await simctl.getSimulatorId( config.device.name, - config.device.systemVersion + config.device.systemVersion, ); if (!udid) { @@ -73,7 +74,7 @@ export const getAppleSimulatorPlatformInstance = async ( iosInstanceLogger.debug( 'resolved iOS simulator %s with status %s', udid, - simulatorStatus + simulatorStatus, ); if ( @@ -84,7 +85,7 @@ export const getAppleSimulatorPlatformInstance = async ( iosInstanceLogger.debug( 'booting iOS simulator %s from status %s', udid, - simulatorStatus + simulatorStatus, ); await simctl.bootSimulator(udid); startedByHarness = true; @@ -95,14 +96,14 @@ export const getAppleSimulatorPlatformInstance = async ( } else if (simctl.isBootingSimulatorStatus(simulatorStatus)) { logger.info( 'Waiting for iOS simulator %s to finish booting...', - config.device.name + config.device.name, ); } if (!simctl.isBootedSimulatorStatus(simulatorStatus)) { iosInstanceLogger.debug( 'waiting for iOS simulator %s to finish booting', - udid + udid, ); await simctl.waitForBoot(udid, init.signal); } @@ -117,30 +118,45 @@ export const getAppleSimulatorPlatformInstance = async ( await simctl.applyHarnessJsLocationOverride( udid, config.bundleId, - `localhost:${harnessConfig.metroPort}` + `localhost:${harnessConfig.metroPort}`, ); + const xctestAgent = createXCTestAgentController({ + target: { + kind: 'simulator', + id: udid, + }, + }); + return { + prepareRun: async () => { + await xctestAgent.prepare(); + }, startApp: async (options) => { + await xctestAgent.ensureStarted(); await simctl.startApp( udid, config.bundleId, (options as typeof config.appLaunchOptions | undefined) ?? - config.appLaunchOptions + config.appLaunchOptions, ); }, restartApp: async (options) => { + await xctestAgent.ensureStarted(); await simctl.stopApp(udid, config.bundleId); await simctl.startApp( udid, config.bundleId, (options as typeof config.appLaunchOptions | undefined) ?? - config.appLaunchOptions + config.appLaunchOptions, ); }, stopApp: async () => { await simctl.stopApp(udid, config.bundleId); }, + disposeRun: async () => { + await xctestAgent.dispose(); + }, dispose: async () => { await simctl.stopApp(udid, config.bundleId); await simctl.clearHarnessJsLocationOverride(udid, config.bundleId); @@ -169,14 +185,14 @@ export const getAppleSimulatorPlatformInstance = async ( export const getApplePhysicalDevicePlatformInstance = async ( config: ApplePlatformConfig, - harnessConfig: HarnessConfig + harnessConfig: HarnessConfig, ): Promise => { assertAppleDevicePhysical(config.device); const detectNativeCrashes = harnessConfig.detectNativeCrashes ?? true; if (harnessConfig.metroPort !== DEFAULT_METRO_PORT) { throw new Error( - `Custom Metro port ${harnessConfig.metroPort} is not supported on physical iOS devices. Physical devices always connect to port ${DEFAULT_METRO_PORT}.` + `Custom Metro port ${harnessConfig.metroPort} is not supported on physical iOS devices. Physical devices always connect to port ${DEFAULT_METRO_PORT}.`, ); } @@ -193,31 +209,46 @@ export const getApplePhysicalDevicePlatformInstance = async ( if (!isAvailable) { throw new AppNotInstalledError( config.bundleId, - getDeviceName(config.device) + getDeviceName(config.device), ); } + const xctestAgent = createXCTestAgentController({ + target: { + kind: 'device', + id: deviceId, + }, + }); + return { + prepareRun: async () => { + await xctestAgent.prepare(); + }, startApp: async (options) => { + await xctestAgent.ensureStarted(); await devicectl.startApp( deviceId, config.bundleId, (options as typeof config.appLaunchOptions | undefined) ?? - config.appLaunchOptions + config.appLaunchOptions, ); }, restartApp: async (options) => { + await xctestAgent.ensureStarted(); await devicectl.stopApp(deviceId, config.bundleId); await devicectl.startApp( deviceId, config.bundleId, (options as typeof config.appLaunchOptions | undefined) ?? - config.appLaunchOptions + config.appLaunchOptions, ); }, stopApp: async () => { await devicectl.stopApp(deviceId, config.bundleId); }, + disposeRun: async () => { + await xctestAgent.dispose(); + }, dispose: async () => { await devicectl.stopApp(deviceId, config.bundleId); }, diff --git a/packages/platform-ios/src/xctest-agent.ts b/packages/platform-ios/src/xctest-agent.ts new file mode 100644 index 0000000..a7b3be6 --- /dev/null +++ b/packages/platform-ios/src/xctest-agent.ts @@ -0,0 +1,312 @@ +import { logger, spawn, type Subprocess } from '@react-native-harness/tools'; +import fs from 'node:fs'; +import { createHash } from 'node:crypto'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const xctestAgentLogger = logger.child('ios-xctest-agent'); + +const XCTEST_AGENT_PROJECT_NAME = 'HarnessXCTestAgent'; +const XCTEST_AGENT_SCHEME_NAME = 'HarnessXCTestAgent'; + +type XCTestAgentTarget = + | { + kind: 'simulator'; + id: string; + } + | { + kind: 'device'; + id: string; + }; + +export type XCTestAgentCapability = { + getLaunchEnvironment?: () => Record; +}; + +type XCTestAgentBuildManifest = { + buildInputsHash: string; + destinationKind: XCTestAgentTarget['kind']; +}; + +export type XCTestAgentController = { + prepare: () => Promise; + ensureStarted: () => Promise; + stop: () => Promise; + dispose: () => Promise; +}; + +const getXCTestAgentProjectRoot = (): string => { + return fileURLToPath(new URL('../xctest-agent', import.meta.url)); +}; + +const getXCTestAgentProjectFilePath = (): string => { + return path.join( + getXCTestAgentProjectRoot(), + `${XCTEST_AGENT_PROJECT_NAME}.xcodeproj`, + ); +}; + +const getXCTestAgentSpecPath = (): string => { + return path.join(getXCTestAgentProjectRoot(), 'project.yml'); +}; + +const getXCTestAgentBuildRoot = (): string => { + return path.join(getXCTestAgentProjectRoot(), 'build'); +}; + +const getXCTestAgentDerivedDataPath = (target: XCTestAgentTarget): string => { + return path.join(getXCTestAgentBuildRoot(), target.kind); +}; + +const getXCTestAgentBuildManifestPath = (target: XCTestAgentTarget): string => { + return path.join( + getXCTestAgentDerivedDataPath(target), + 'build-manifest.json', + ); +}; + +const getXCTestAgentDestination = (target: XCTestAgentTarget): string => { + return target.kind === 'simulator' + ? `platform=iOS Simulator,id=${target.id}` + : `platform=iOS,id=${target.id}`; +}; + +const getXCTestAgentBuildProductsPath = (target: XCTestAgentTarget): string => { + return path.join(getXCTestAgentDerivedDataPath(target), 'Build', 'Products'); +}; + +const readBuildManifest = ( + target: XCTestAgentTarget, +): XCTestAgentBuildManifest | null => { + const manifestPath = getXCTestAgentBuildManifestPath(target); + + if (!fs.existsSync(manifestPath)) { + return null; + } + + return JSON.parse( + fs.readFileSync(manifestPath, 'utf8'), + ) as XCTestAgentBuildManifest; +}; + +const writeBuildManifest = ( + target: XCTestAgentTarget, + manifest: XCTestAgentBuildManifest, +) => { + fs.mkdirSync(getXCTestAgentDerivedDataPath(target), { recursive: true }); + fs.writeFileSync( + getXCTestAgentBuildManifestPath(target), + JSON.stringify(manifest, null, 2), + ); +}; + +const getProjectInputFilePaths = (root: string): string[] => { + const entries = fs.readdirSync(root, { withFileTypes: true }); + const files: string[] = []; + + for (const entry of entries) { + if ( + entry.name === 'build' || + entry.name.endsWith('.xcodeproj') || + entry.name === '.gitignore' + ) { + continue; + } + + const entryPath = path.join(root, entry.name); + + if (entry.isDirectory()) { + files.push(...getProjectInputFilePaths(entryPath)); + continue; + } + + files.push(entryPath); + } + + return files.sort(); +}; + +const getProjectInputsHash = (): string => { + const projectRoot = getXCTestAgentProjectRoot(); + const hash = createHash('sha256'); + + for (const filePath of getProjectInputFilePaths(projectRoot)) { + hash.update(path.relative(projectRoot, filePath)); + hash.update('\0'); + hash.update(fs.readFileSync(filePath)); + hash.update('\0'); + } + + return hash.digest('hex'); +}; + +const shouldReuseBuildArtifacts = ( + target: XCTestAgentTarget, + buildInputsHash: string, +): boolean => { + const manifest = readBuildManifest(target); + + if (!manifest) { + return false; + } + + if ( + manifest.buildInputsHash !== buildInputsHash || + manifest.destinationKind !== target.kind + ) { + return false; + } + + return fs.existsSync(getXCTestAgentBuildProductsPath(target)); +}; + +const createProcessStopper = async (process: Subprocess | null) => { + if (!process) { + return; + } + + try { + (await process.nodeChildProcess).kill(); + } catch { + // Ignore agent shutdown failures during teardown. + } +}; + +export const createXCTestAgentController = (options: { + target: XCTestAgentTarget; + capabilities?: XCTestAgentCapability[]; +}): XCTestAgentController => { + const { target } = options; + const capabilities = options.capabilities ?? []; + let prepared = false; + let agentProcess: Subprocess | null = null; + let processTask: Promise | null = null; + + const getLaunchEnvironment = (): Record => { + return Object.assign( + {}, + ...capabilities.map( + (capability) => capability.getLaunchEnvironment?.() ?? {}, + ), + ); + }; + + const prepare = async () => { + if (prepared) { + return; + } + + const projectRoot = getXCTestAgentProjectRoot(); + const buildInputsHash = getProjectInputsHash(); + + xctestAgentLogger.debug( + 'generating XCTest agent project for %s', + target.kind, + ); + await spawn('xcodegen', [ + 'generate', + '--spec', + getXCTestAgentSpecPath(), + '--project', + projectRoot, + ]); + + if (shouldReuseBuildArtifacts(target, buildInputsHash)) { + prepared = true; + xctestAgentLogger.debug( + 'reusing cached XCTest agent build for %s', + target.kind, + ); + return; + } + + fs.mkdirSync(getXCTestAgentBuildRoot(), { recursive: true }); + + xctestAgentLogger.debug('building XCTest agent for %s', target.kind); + await spawn('xcodebuild', [ + 'build-for-testing', + '-project', + getXCTestAgentProjectFilePath(), + '-scheme', + XCTEST_AGENT_SCHEME_NAME, + '-destination', + getXCTestAgentDestination(target), + '-derivedDataPath', + getXCTestAgentDerivedDataPath(target), + ]); + + writeBuildManifest(target, { + buildInputsHash, + destinationKind: target.kind, + }); + prepared = true; + }; + + const ensureStarted = async () => { + await prepare(); + + if (agentProcess) { + return; + } + + xctestAgentLogger.debug('starting XCTest agent for %s', target.kind); + agentProcess = spawn( + 'xcodebuild', + [ + 'test-without-building', + '-project', + getXCTestAgentProjectFilePath(), + '-scheme', + XCTEST_AGENT_SCHEME_NAME, + '-destination', + getXCTestAgentDestination(target), + '-derivedDataPath', + getXCTestAgentDerivedDataPath(target), + ], + { + cwd: getXCTestAgentProjectRoot(), + env: { + ...process.env, + ...getLaunchEnvironment(), + }, + stdout: 'pipe', + stderr: 'pipe', + }, + ); + + const currentProcess = agentProcess; + + processTask = (async () => { + try { + for await (const line of currentProcess) { + xctestAgentLogger.debug('[agent:%s] %s', target.kind, line); + } + } catch (error) { + xctestAgentLogger.debug('XCTest agent process stopped', error); + } finally { + if (agentProcess === currentProcess) { + agentProcess = null; + processTask = null; + } + } + })(); + }; + + const stop = async () => { + const currentProcess = agentProcess; + agentProcess = null; + + await createProcessStopper(currentProcess); + await processTask; + processTask = null; + }; + + return { + prepare, + ensureStarted, + stop, + dispose: async () => { + await stop(); + }, + }; +}; diff --git a/packages/platform-ios/xctest-agent/HarnessXCTestAgentTests/HarnessXCTestAgentTests.swift b/packages/platform-ios/xctest-agent/HarnessXCTestAgentTests/HarnessXCTestAgentTests.swift index 361c931..89e8279 100644 --- a/packages/platform-ios/xctest-agent/HarnessXCTestAgentTests/HarnessXCTestAgentTests.swift +++ b/packages/platform-ios/xctest-agent/HarnessXCTestAgentTests/HarnessXCTestAgentTests.swift @@ -1,11 +1,20 @@ import XCTest final class HarnessXCTestAgentTests: XCTestCase { + private enum Constants { + static let defaultSessionDuration: TimeInterval = 60 * 60 + } + override func setUpWithError() throws { continueAfterFailure = false } - func testAgentProjectBootstraps() { - XCTAssertTrue(true) + func testAgentSession() { + let app = XCUIApplication() + app.launch() + + RunLoop.current.run( + until: Date().addingTimeInterval(Constants.defaultSessionDuration) + ) } } From 2bbaaf1d26b113ae0872c847766ee46461c094f9 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Tue, 21 Apr 2026 10:01:30 -0400 Subject: [PATCH 04/33] feat(platform-ios): add permission prompt agent capability --- PLAN.md | 2 + .../__tests__/instance-xctest-agent.test.ts | 10 +++ .../xctest-agent-capabilities.test.ts | 12 ++++ packages/platform-ios/src/instance.ts | 3 + .../src/xctest-agent-capabilities.ts | 13 ++++ .../AgentCapability.swift | 11 +++ .../HarnessXCTestAgentTests.swift | 24 ++++++- .../PermissionPromptCapability.swift | 67 +++++++++++++++++++ packages/platform-ios/xctest-agent/README.md | 7 ++ 9 files changed, 146 insertions(+), 3 deletions(-) create mode 100644 packages/platform-ios/src/__tests__/xctest-agent-capabilities.test.ts create mode 100644 packages/platform-ios/src/xctest-agent-capabilities.ts create mode 100644 packages/platform-ios/xctest-agent/HarnessXCTestAgentTests/AgentCapability.swift create mode 100644 packages/platform-ios/xctest-agent/HarnessXCTestAgentTests/PermissionPromptCapability.swift diff --git a/PLAN.md b/PLAN.md index 74d2cc2..8020475 100644 --- a/PLAN.md +++ b/PLAN.md @@ -98,6 +98,8 @@ Parallelization: ## Phase 4: Permission Prompt Capability +Status: Completed + Objective: implement the first XCTest agent capability: best-effort auto-accept of permission prompts. Deliverables: diff --git a/packages/platform-ios/src/__tests__/instance-xctest-agent.test.ts b/packages/platform-ios/src/__tests__/instance-xctest-agent.test.ts index 06ed328..80ebf3b 100644 --- a/packages/platform-ios/src/__tests__/instance-xctest-agent.test.ts +++ b/packages/platform-ios/src/__tests__/instance-xctest-agent.test.ts @@ -71,6 +71,11 @@ describe('iOS XCTest agent runner integration', () => { await instance.disposeRun?.(); expect(mocks.createXCTestAgentController).toHaveBeenCalledWith({ + capabilities: [ + expect.objectContaining({ + getLaunchEnvironment: expect.any(Function), + }), + ], target: { kind: 'simulator', id: 'sim-udid', @@ -115,6 +120,11 @@ describe('iOS XCTest agent runner integration', () => { await instance.disposeRun?.(); expect(mocks.createXCTestAgentController).toHaveBeenCalledWith({ + capabilities: [ + expect.objectContaining({ + getLaunchEnvironment: expect.any(Function), + }), + ], target: { kind: 'device', id: 'device-udid', diff --git a/packages/platform-ios/src/__tests__/xctest-agent-capabilities.test.ts b/packages/platform-ios/src/__tests__/xctest-agent-capabilities.test.ts new file mode 100644 index 0000000..39e2874 --- /dev/null +++ b/packages/platform-ios/src/__tests__/xctest-agent-capabilities.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, it } from 'vitest'; +import { createPermissionPromptAutoAcceptCapability } from '../xctest-agent-capabilities.js'; + +describe('xctest agent capabilities', () => { + it('enables best-effort permission prompt auto-accept through launch environment', () => { + const capability = createPermissionPromptAutoAcceptCapability(); + + expect(capability.getLaunchEnvironment?.()).toEqual({ + HARNESS_XCTEST_AGENT_AUTO_ACCEPT_PERMISSIONS: '1', + }); + }); +}); diff --git a/packages/platform-ios/src/instance.ts b/packages/platform-ios/src/instance.ts index 5785dc1..c645478 100644 --- a/packages/platform-ios/src/instance.ts +++ b/packages/platform-ios/src/instance.ts @@ -26,6 +26,7 @@ import { HarnessAppPathError } from './errors.js'; import { logger } from '@react-native-harness/tools'; import fs from 'node:fs'; import { createXCTestAgentController } from './xctest-agent.js'; +import { createPermissionPromptAutoAcceptCapability } from './xctest-agent-capabilities.js'; const iosInstanceLogger = logger.child('ios-instance'); @@ -126,6 +127,7 @@ export const getAppleSimulatorPlatformInstance = async ( kind: 'simulator', id: udid, }, + capabilities: [createPermissionPromptAutoAcceptCapability()], }); return { @@ -218,6 +220,7 @@ export const getApplePhysicalDevicePlatformInstance = async ( kind: 'device', id: deviceId, }, + capabilities: [createPermissionPromptAutoAcceptCapability()], }); return { diff --git a/packages/platform-ios/src/xctest-agent-capabilities.ts b/packages/platform-ios/src/xctest-agent-capabilities.ts new file mode 100644 index 0000000..320d26e --- /dev/null +++ b/packages/platform-ios/src/xctest-agent-capabilities.ts @@ -0,0 +1,13 @@ +import type { XCTestAgentCapability } from './xctest-agent.js'; + +const ENABLE_PERMISSION_PROMPT_AUTO_ACCEPT = + 'HARNESS_XCTEST_AGENT_AUTO_ACCEPT_PERMISSIONS'; + +export const createPermissionPromptAutoAcceptCapability = + (): XCTestAgentCapability => { + return { + getLaunchEnvironment: () => ({ + [ENABLE_PERMISSION_PROMPT_AUTO_ACCEPT]: '1', + }), + }; + }; diff --git a/packages/platform-ios/xctest-agent/HarnessXCTestAgentTests/AgentCapability.swift b/packages/platform-ios/xctest-agent/HarnessXCTestAgentTests/AgentCapability.swift new file mode 100644 index 0000000..443595b --- /dev/null +++ b/packages/platform-ios/xctest-agent/HarnessXCTestAgentTests/AgentCapability.swift @@ -0,0 +1,11 @@ +import XCTest + +protocol AgentCapability { + func setUp() throws + func tick() throws +} + +extension AgentCapability { + func setUp() throws {} + func tick() throws {} +} diff --git a/packages/platform-ios/xctest-agent/HarnessXCTestAgentTests/HarnessXCTestAgentTests.swift b/packages/platform-ios/xctest-agent/HarnessXCTestAgentTests/HarnessXCTestAgentTests.swift index 89e8279..96050df 100644 --- a/packages/platform-ios/xctest-agent/HarnessXCTestAgentTests/HarnessXCTestAgentTests.swift +++ b/packages/platform-ios/xctest-agent/HarnessXCTestAgentTests/HarnessXCTestAgentTests.swift @@ -3,18 +3,36 @@ import XCTest final class HarnessXCTestAgentTests: XCTestCase { private enum Constants { static let defaultSessionDuration: TimeInterval = 60 * 60 + static let tickInterval: TimeInterval = 1 } + private var capabilities: [AgentCapability] = [] + override func setUpWithError() throws { continueAfterFailure = false + capabilities = [ + PermissionPromptCapability.fromEnvironment() + ].compactMap { $0 } + + for capability in capabilities { + try capability.setUp() + } } func testAgentSession() { let app = XCUIApplication() app.launch() - RunLoop.current.run( - until: Date().addingTimeInterval(Constants.defaultSessionDuration) - ) + let sessionDeadline = Date().addingTimeInterval(Constants.defaultSessionDuration) + + while Date() < sessionDeadline { + for capability in capabilities { + try? capability.tick() + } + + RunLoop.current.run( + until: Date().addingTimeInterval(Constants.tickInterval) + ) + } } } diff --git a/packages/platform-ios/xctest-agent/HarnessXCTestAgentTests/PermissionPromptCapability.swift b/packages/platform-ios/xctest-agent/HarnessXCTestAgentTests/PermissionPromptCapability.swift new file mode 100644 index 0000000..8b8420a --- /dev/null +++ b/packages/platform-ios/xctest-agent/HarnessXCTestAgentTests/PermissionPromptCapability.swift @@ -0,0 +1,67 @@ +import XCTest + +final class PermissionPromptCapability: AgentCapability { + private enum Environment { + static let autoAcceptPermissions = "HARNESS_XCTEST_AGENT_AUTO_ACCEPT_PERMISSIONS" + } + + private enum Constants { + static let knownPositiveButtonLabels = [ + "Allow", + "OK", + "Continue", + "Next", + "While Using App", + "While Using the App", + "Always Allow", + "Allow Once", + "Join", + "Pair", + "Allow Full Access" + ] + } + + private let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard") + private let hostApplication = XCUIApplication() + + static func fromEnvironment() -> PermissionPromptCapability? { + guard ProcessInfo.processInfo.environment[Environment.autoAcceptPermissions] == "1" else { + return nil + } + + return PermissionPromptCapability() + } + + func setUp() throws { + addUIInterruptionMonitor(withDescription: "Harness permission prompt handler") { [weak self] alert in + guard let self else { + return false + } + + return self.tapPositiveAction(in: alert) + } + } + + func tick() throws { + _ = tapPositiveAction(in: springboard.alerts.firstMatch) + _ = tapPositiveAction(in: springboard.sheets.firstMatch) + hostApplication.activate() + } + + private func tapPositiveAction(in element: XCUIElement) -> Bool { + guard element.exists else { + return false + } + + for label in Constants.knownPositiveButtonLabels { + let button = element.buttons[label] + + if button.exists { + button.tap() + return true + } + } + + return false + } +} diff --git a/packages/platform-ios/xctest-agent/README.md b/packages/platform-ios/xctest-agent/README.md index f37726a..3787ca7 100644 --- a/packages/platform-ios/xctest-agent/README.md +++ b/packages/platform-ios/xctest-agent/README.md @@ -18,6 +18,11 @@ The generated project is intentionally not committed. The source of truth is `xc - `HarnessXCTestAgentTests`: UI-testing bundle where agent capabilities live - `HarnessXCTestAgent` scheme: stable scheme name for future host-side orchestration +## Current Capability + +- Best-effort permission prompt auto-accept for recognized positive actions +- Unknown prompts are ignored silently so the generic agent can coexist with future capabilities + ## Build Assumptions - `xcodegen` is available on the host machine @@ -33,5 +38,7 @@ When build artifact caching is added, these files should be treated as the prima - `packages/platform-ios/xctest-agent/project.yml` - `packages/platform-ios/xctest-agent/HarnessXCTestAgentHost/AgentHostApp.swift` - `packages/platform-ios/xctest-agent/HarnessXCTestAgentTests/HarnessXCTestAgentTests.swift` +- `packages/platform-ios/xctest-agent/HarnessXCTestAgentTests/AgentCapability.swift` +- `packages/platform-ios/xctest-agent/HarnessXCTestAgentTests/PermissionPromptCapability.swift` The selected Xcode version and any injected signing settings should also be part of higher-level cache keys because they affect the produced artifacts. From 791fb3789b20c8356335e0e039829a73cbbf29e8 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Tue, 21 Apr 2026 10:30:07 -0400 Subject: [PATCH 05/33] feat(jest): lazily prepare run helpers before app launch --- PLAN.md | 2 + packages/jest/src/__tests__/harness.test.ts | 144 ++++++++++++++++---- packages/jest/src/harness.ts | 17 ++- 3 files changed, 136 insertions(+), 27 deletions(-) diff --git a/PLAN.md b/PLAN.md index 8020475..42b9055 100644 --- a/PLAN.md +++ b/PLAN.md @@ -121,6 +121,8 @@ Parallelization: ## Phase 5: End-to-End iOS Wiring +Status: Completed + Objective: connect the generic lifecycle, iOS orchestration, and permission capability into the actual Harness run flow. Deliverables: diff --git a/packages/jest/src/__tests__/harness.test.ts b/packages/jest/src/__tests__/harness.test.ts index 40c8f66..6545335 100644 --- a/packages/jest/src/__tests__/harness.test.ts +++ b/packages/jest/src/__tests__/harness.test.ts @@ -397,14 +397,18 @@ describe('getHarness', () => { await harness.dispose(); }); - it('prepares the platform runner once during harness creation and disposes it during teardown', async () => { - const { serverBridge } = createBridgeServer(); + it('prepares the platform runner lazily before first app launch and disposes it during teardown', async () => { + const { serverBridge, emitReady } = createBridgeServer(); const appMonitor = createAppMonitor(); const callOrder: string[] = []; + const restartApp = vi.fn(async () => { + callOrder.push('restartApp'); + }); const platformInstance = createPlatformRunner({ prepareRun: vi.fn(async () => { callOrder.push('prepareRun'); }), + restartApp, createAppMonitor: () => appMonitor.appMonitor, disposeRun: vi.fn(async () => { callOrder.push('disposeRun'); @@ -417,6 +421,14 @@ describe('getHarness', () => { mocks.getBridgeServer.mockResolvedValue(serverBridge); mocks.getMetroInstance.mockResolvedValue(metroInstance); + mocks.waitForMetroBackedAppReady.mockImplementationOnce( + async (options: WaitForMetroBackedAppReadyOptions) => { + await options.startAttempt(); + const readyPromise = options.waitForReady(new AbortController().signal); + emitReady(); + await readyPromise; + }, + ); ( globalThis as typeof globalThis & { @@ -440,15 +452,26 @@ describe('getHarness', () => { '/tmp/project', ); - expect(platformInstance.prepareRun).toHaveBeenCalledTimes(1); + expect(platformInstance.prepareRun).not.toHaveBeenCalled(); expect(appMonitor.appMonitor.start).toHaveBeenCalledTimes(1); - expect(callOrder).toEqual(['prepareRun']); + expect(callOrder).toEqual([]); + + await harness.ensureAppReady('/tmp/example.harness.ts'); + + expect(platformInstance.prepareRun).toHaveBeenCalledTimes(1); + expect(restartApp).toHaveBeenCalledTimes(1); + expect(callOrder).toEqual(['prepareRun', 'restartApp']); await harness.dispose(); expect(platformInstance.disposeRun).toHaveBeenCalledTimes(1); expect(platformInstance.dispose).toHaveBeenCalledTimes(1); - expect(callOrder).toEqual(['prepareRun', 'disposeRun', 'dispose']); + expect(callOrder).toEqual([ + 'prepareRun', + 'restartApp', + 'disposeRun', + 'dispose', + ]); }); it('resolves and exposes a fallback Metro port before platform init', async () => { @@ -565,7 +588,7 @@ describe('getHarness', () => { await harness.dispose(); }); - it('disposes a prepared platform runner when harness creation fails after prepareRun', async () => { + it('disposes a prepared platform runner when first app launch fails after prepareRun', async () => { const { serverBridge } = createBridgeServer(); const appMonitor = createAppMonitor(); const disposeRun = vi.fn(async () => undefined); @@ -576,17 +599,15 @@ describe('getHarness', () => { dispose, }); const metroInstance = createMetroInstance(); - const plugin = definePlugin<{}, HarnessConfig, HarnessPlatform>({ - name: 'failing-before-run-plugin', - createState: () => ({}), - hooks: { - harness: { - beforeRun: () => { - throw new Error('before-run failed'); - }, - }, + const restartApp = vi + .spyOn(platformInstance, 'restartApp') + .mockRejectedValueOnce(new Error('restart failed')); + + mocks.waitForMetroBackedAppReady.mockImplementationOnce( + async (options: WaitForMetroBackedAppReadyOptions) => { + await options.startAttempt(); }, - }); + ); mocks.getBridgeServer.mockResolvedValue(serverBridge); mocks.getMetroInstance.mockResolvedValue(metroInstance); @@ -607,21 +628,96 @@ describe('getHarness', () => { )}`, }; + const harness = await getHarness( + createHarnessConfig(), + platform, + '/tmp/project', + ); + await expect( - getHarness( - createHarnessConfig({ - plugins: [plugin], - }), - platform, - '/tmp/project', - ), - ).rejects.toThrow('before-run failed'); + harness.ensureAppReady('/tmp/example.harness.ts'), + ).rejects.toThrow('restart failed'); expect(platformInstance.prepareRun).toHaveBeenCalledTimes(1); + expect(restartApp).toHaveBeenCalledTimes(1); + + await harness.dispose(); + expect(disposeRun).toHaveBeenCalledTimes(1); expect(dispose).toHaveBeenCalledTimes(1); }); + it('prepares the platform runner only once across multiple app launches in the same run', async () => { + const { serverBridge, emitReady } = createBridgeServer(); + const appMonitor = createAppMonitor(); + const prepareRun = vi.fn(async () => undefined); + const restartApp = vi.fn(async () => undefined); + const stopApp = vi.fn(async () => undefined); + const platformInstance = createPlatformRunner({ + prepareRun, + restartApp, + stopApp, + isAppRunning: vi.fn(async () => false), + createAppMonitor: () => appMonitor.appMonitor, + }); + const metroInstance = createMetroInstance(); + + mocks.getBridgeServer.mockResolvedValue(serverBridge); + mocks.getMetroInstance.mockResolvedValue(metroInstance); + mocks.waitForMetroBackedAppReady + .mockImplementationOnce( + async (options: WaitForMetroBackedAppReadyOptions) => { + await options.startAttempt(); + const readyPromise = options.waitForReady( + new AbortController().signal, + ); + emitReady(); + await readyPromise; + }, + ) + .mockImplementationOnce( + async (options: WaitForMetroBackedAppReadyOptions) => { + await options.startAttempt(); + const readyPromise = options.waitForReady( + new AbortController().signal, + ); + emitReady(); + await readyPromise; + }, + ); + + ( + globalThis as typeof globalThis & { + __HARNESS_PLATFORM_RUNNER__?: (...args: unknown[]) => Promise; + } + ).__HARNESS_PLATFORM_RUNNER__ = vi.fn(async () => platformInstance); + + const platform: HarnessPlatform = { + config: {}, + getResourceLockKey: () => 'ios:test-platform-run-lifecycle-once', + name: 'ios', + platformId: 'ios', + runner: `data:text/javascript,${encodeURIComponent( + 'export default (...args) => globalThis.__HARNESS_PLATFORM_RUNNER__(...args);', + )}`, + }; + + const harness = await getHarness( + createHarnessConfig(), + platform, + '/tmp/project', + ); + + await harness.ensureAppReady('/tmp/first.harness.ts'); + await harness.restart('/tmp/second.harness.ts'); + + expect(prepareRun).toHaveBeenCalledTimes(1); + expect(restartApp).toHaveBeenCalledTimes(2); + expect(stopApp).toHaveBeenCalledTimes(1); + + await harness.dispose(); + }); + it('routes ensureAppReady through the shared Metro startup helper', async () => { const { serverBridge, emitReady } = createBridgeServer(); const appMonitor = createAppMonitor(); diff --git a/packages/jest/src/harness.ts b/packages/jest/src/harness.ts index 92bafda..cef2ddc 100644 --- a/packages/jest/src/harness.ts +++ b/packages/jest/src/harness.ts @@ -322,6 +322,7 @@ const getHarnessInternal = async ( let activeTestFilePath: string | undefined; const pendingHookPromises = new Set>(); let pendingHookError: unknown; + let didPrepareRun = false; const getCurrentRunId = () => currentRun?.runId; const toRelativeTestFilePath = (testFilePath?: string) => @@ -650,7 +651,14 @@ const getHarnessInternal = async ( harnessLogger.debug('client log forwarding enabled'); } - let didPrepareRun = false; + const ensurePlatformRunPrepared = async () => { + if (didPrepareRun) { + return; + } + + await platformInstance.prepareRun?.(); + didPrepareRun = true; + }; const dispose = async (reason: 'normal' | 'abort' | 'error' = 'normal') => { harnessLogger.debug('disposing Harness (reason=%s)', reason); @@ -724,8 +732,6 @@ const getHarnessInternal = async ( appLaunchOptions, }); await flushPendingHooks(); - await platformInstance.prepareRun?.(); - didPrepareRun = true; await appMonitor.start(); harnessLogger.debug('app monitor started'); await pluginManager.callHook('harness:before-run', { @@ -776,6 +782,10 @@ const getHarnessInternal = async ( testFilePath, crashSupervisor, appLaunchOptions, + launchApp: async () => { + await ensurePlatformRunPrepared(); + await platformInstance.restartApp(appLaunchOptions); + }, }); await flushPendingHooks(); harnessLogger.debug('app is ready for %s', testFilePath); @@ -796,6 +806,7 @@ const getHarnessInternal = async ( await platformInstance.stopApp(); } else { harnessLogger.debug('requesting direct app restart'); + await ensurePlatformRunPrepared(); await platformInstance.restartApp(appLaunchOptions); } From 5fb5d309313c397e1de757c2528b8677d619a04a Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Tue, 21 Apr 2026 10:33:26 -0400 Subject: [PATCH 06/33] test(platform-ios): harden xctest agent validation --- .../src/__tests__/xctest-agent.test.ts | 42 +++++++++++++++++++ packages/platform-ios/xctest-agent/README.md | 30 +++++++++++++ 2 files changed, 72 insertions(+) diff --git a/packages/platform-ios/src/__tests__/xctest-agent.test.ts b/packages/platform-ios/src/__tests__/xctest-agent.test.ts index 810eeb1..719422c 100644 --- a/packages/platform-ios/src/__tests__/xctest-agent.test.ts +++ b/packages/platform-ios/src/__tests__/xctest-agent.test.ts @@ -165,6 +165,48 @@ describe('xctest-agent orchestration', () => { expect(mocks.kill).toHaveBeenCalledTimes(1); }); + + it('rebuilds when the cached build manifest no longer matches project inputs', async () => { + fs.mkdirSync(path.join(buildRoot, 'simulator', 'Build', 'Products'), { + recursive: true, + }); + fs.writeFileSync( + path.join(buildRoot, 'simulator', 'build-manifest.json'), + JSON.stringify({ + buildInputsHash: 'stale-manifest-hash', + destinationKind: 'simulator', + }), + ); + + const controller = createXCTestAgentController({ + target: { + kind: 'simulator', + id: 'sim-123', + }, + }); + + await controller.prepare(); + + expect(mocks.spawn).toHaveBeenCalledTimes(2); + expect(mocks.spawn).toHaveBeenNthCalledWith( + 2, + 'xcodebuild', + expect.arrayContaining(['build-for-testing']), + ); + }); + + it('skips killing the agent process when dispose is called before startup', async () => { + const controller = createXCTestAgentController({ + target: { + kind: 'device', + id: 'device-123', + }, + }); + + await controller.dispose(); + + expect(mocks.kill).not.toHaveBeenCalled(); + }); }); const rmBuildRoot = () => { diff --git a/packages/platform-ios/xctest-agent/README.md b/packages/platform-ios/xctest-agent/README.md index 3787ca7..36e4903 100644 --- a/packages/platform-ios/xctest-agent/README.md +++ b/packages/platform-ios/xctest-agent/README.md @@ -23,6 +23,36 @@ The generated project is intentionally not committed. The source of truth is `xc - Best-effort permission prompt auto-accept for recognized positive actions - Unknown prompts are ignored silently so the generic agent can coexist with future capabilities +## Operations + +Validate the generated scheme and targets: + +```bash +xcodebuild -project "packages/platform-ios/xctest-agent/HarnessXCTestAgent.xcodeproj" -list +``` + +Run the host-side validation and hardening tests from `packages/platform-ios`: + +```bash +pnpm vitest run src/__tests__/xctest-agent-capabilities.test.ts src/__tests__/xctest-agent.test.ts src/__tests__/instance-xctest-agent.test.ts src/__tests__/instance.test.ts +``` + +Build and cache behavior: + +- The project is regenerated from `xctest-agent/project.yml` during prepare +- Build artifacts are cached under `packages/platform-ios/xctest-agent/build/` +- Simulator and physical-device builds use separate derived-data roots +- Cache reuse depends on the XcodeGen inputs hash matching the stored build manifest + +Manual validation checklist: + +1. Build an iOS app artifact and set `HARNESS_APP_PATH` when simulator installation is needed. +2. Run Harness against an iOS simulator and confirm the first run triggers XCTest agent generation and build. +3. Run Harness a second time on the same target and confirm cached artifacts are reused. +4. Trigger a real permission prompt, such as camera access, and confirm the positive action is tapped automatically. +5. Confirm Harness teardown does not leave a stuck `xcodebuild` XCTest agent process behind. +6. Repeat the same flow on a connected physical iOS device with the required signing inputs available. + ## Build Assumptions - `xcodegen` is available on the host machine From e2a73f8a4141c6465f8560c37375f7bf4548a281 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Fri, 24 Apr 2026 10:36:49 -0400 Subject: [PATCH 07/33] feat(platform-ios): integrate xctest permission agent --- .../project.pbxproj | 4 +- .../ios/HarnessPlayground/Info.plist | 2 + apps/playground/ios/Podfile.lock | 394 +++-- apps/playground/package.json | 19 +- .../src/__tests__/ui/permissions.harness.tsx | 73 + packages/platform-ios/package.json | 4 +- .../__tests__/instance-xctest-agent.test.ts | 16 +- .../src/__tests__/instance.test.ts | 71 +- .../xctest-agent-capabilities.test.ts | 16 + .../src/__tests__/xctest-agent-client.test.ts | 95 ++ .../src/__tests__/xctest-agent.test.ts | 171 ++- .../platform-ios/src/appium-ios-device.d.ts | 7 + packages/platform-ios/src/instance.ts | 24 +- .../src/xctest-agent-capabilities.ts | 7 + .../platform-ios/src/xctest-agent-client.ts | 97 ++ .../src/xctest-agent-transport-device.ts | 150 ++ .../src/xctest-agent-transport-simulator.ts | 103 ++ .../src/xctest-agent-transport.ts | 18 + packages/platform-ios/src/xctest-agent.ts | 214 ++- packages/platform-ios/xctest-agent/.gitignore | 2 - .../project.pbxproj | 455 ++++++ .../contents.xcworkspacedata | 7 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 35 + .../Assets.xcassets/Contents.json | 6 + .../Logo.imageset/Contents.json | 21 + .../Assets.xcassets/Logo.imageset/logo.jpg | Bin 0 -> 107504 bytes .../PoweredBy.imageset/Contents.json | 21 + .../PoweredBy.imageset/powered-by.png | Bin 0 -> 6006 bytes .../HarnessXCTestAgent/ContentView.swift | 34 + .../HarnessXCTestAgentApp.swift | 17 + .../HarnessXCTestAgentHost/AgentHostApp.swift | 6 - .../HarnessXCTestAgentTests.swift | 38 - .../PermissionPromptCapability.swift | 67 - .../AgentCapability.swift | 0 .../HarnessXCTestAgentUITests.swift | 333 +++++ .../PermissionPromptCapability.swift | 105 ++ packages/platform-ios/xctest-agent/README.md | 74 - .../xctest-agent/manual-run/xcodebuild.log | 43 + .../platform-ios/xctest-agent/project.yml | 44 - pnpm-lock.yaml | 1315 ++++++++++++++++- 41 files changed, 3608 insertions(+), 511 deletions(-) create mode 100644 apps/playground/src/__tests__/ui/permissions.harness.tsx create mode 100644 packages/platform-ios/src/__tests__/xctest-agent-client.test.ts create mode 100644 packages/platform-ios/src/appium-ios-device.d.ts create mode 100644 packages/platform-ios/src/xctest-agent-client.ts create mode 100644 packages/platform-ios/src/xctest-agent-transport-device.ts create mode 100644 packages/platform-ios/src/xctest-agent-transport-simulator.ts create mode 100644 packages/platform-ios/src/xctest-agent-transport.ts delete mode 100644 packages/platform-ios/xctest-agent/.gitignore create mode 100644 packages/platform-ios/xctest-agent/HarnessXCTestAgent.xcodeproj/project.pbxproj create mode 100644 packages/platform-ios/xctest-agent/HarnessXCTestAgent.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 packages/platform-ios/xctest-agent/HarnessXCTestAgent/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 packages/platform-ios/xctest-agent/HarnessXCTestAgent/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 packages/platform-ios/xctest-agent/HarnessXCTestAgent/Assets.xcassets/Contents.json create mode 100644 packages/platform-ios/xctest-agent/HarnessXCTestAgent/Assets.xcassets/Logo.imageset/Contents.json create mode 100644 packages/platform-ios/xctest-agent/HarnessXCTestAgent/Assets.xcassets/Logo.imageset/logo.jpg create mode 100644 packages/platform-ios/xctest-agent/HarnessXCTestAgent/Assets.xcassets/PoweredBy.imageset/Contents.json create mode 100644 packages/platform-ios/xctest-agent/HarnessXCTestAgent/Assets.xcassets/PoweredBy.imageset/powered-by.png create mode 100644 packages/platform-ios/xctest-agent/HarnessXCTestAgent/ContentView.swift create mode 100644 packages/platform-ios/xctest-agent/HarnessXCTestAgent/HarnessXCTestAgentApp.swift delete mode 100644 packages/platform-ios/xctest-agent/HarnessXCTestAgentHost/AgentHostApp.swift delete mode 100644 packages/platform-ios/xctest-agent/HarnessXCTestAgentTests/HarnessXCTestAgentTests.swift delete mode 100644 packages/platform-ios/xctest-agent/HarnessXCTestAgentTests/PermissionPromptCapability.swift rename packages/platform-ios/xctest-agent/{HarnessXCTestAgentTests => HarnessXCTestAgentUITests}/AgentCapability.swift (100%) create mode 100644 packages/platform-ios/xctest-agent/HarnessXCTestAgentUITests/HarnessXCTestAgentUITests.swift create mode 100644 packages/platform-ios/xctest-agent/HarnessXCTestAgentUITests/PermissionPromptCapability.swift delete mode 100644 packages/platform-ios/xctest-agent/README.md create mode 100644 packages/platform-ios/xctest-agent/manual-run/xcodebuild.log delete mode 100644 packages/platform-ios/xctest-agent/project.yml diff --git a/apps/playground/ios/HarnessPlayground.xcodeproj/project.pbxproj b/apps/playground/ios/HarnessPlayground.xcodeproj/project.pbxproj index 76a53d2..f31363f 100644 --- a/apps/playground/ios/HarnessPlayground.xcodeproj/project.pbxproj +++ b/apps/playground/ios/HarnessPlayground.xcodeproj/project.pbxproj @@ -376,7 +376,7 @@ "-DFOLLY_CFG_NO_COROUTINES=1", "-DFOLLY_HAVE_CLOCK_GETTIME=1", ); - REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; + REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../node_modules/react-native"; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"; USE_HERMES = true; @@ -444,7 +444,7 @@ "-DFOLLY_CFG_NO_COROUTINES=1", "-DFOLLY_HAVE_CLOCK_GETTIME=1", ); - REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; + REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../node_modules/react-native"; SDKROOT = iphoneos; USE_HERMES = true; VALIDATE_PRODUCT = YES; diff --git a/apps/playground/ios/HarnessPlayground/Info.plist b/apps/playground/ios/HarnessPlayground/Info.plist index 40a49bc..0044b85 100644 --- a/apps/playground/ios/HarnessPlayground/Info.plist +++ b/apps/playground/ios/HarnessPlayground/Info.plist @@ -33,6 +33,8 @@ NSAllowsLocalNetworking + NSCameraUsageDescription + Harness Playground uses the camera to validate permission handling. NSLocationWhenInUseUsageDescription RCTNewArchEnabled diff --git a/apps/playground/ios/Podfile.lock b/apps/playground/ios/Podfile.lock index 1d738d3..1a535f2 100644 --- a/apps/playground/ios/Podfile.lock +++ b/apps/playground/ios/Podfile.lock @@ -5,7 +5,7 @@ PODS: - FBLazyVector (0.82.1) - fmt (11.0.2) - glog (0.3.5) - - HarnessUI (1.0.0): + - HarnessUI (1.1.0): - boost - DoubleConversion - fast_float @@ -36,6 +36,65 @@ PODS: - hermes-engine (0.82.1): - hermes-engine/Pre-built (= 0.82.1) - hermes-engine/Pre-built (0.82.1) + - NitroImage (0.13.1): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - NitroModules + - RCT-Folly + - RCT-Folly/Fabric + - RCTRequired + - RCTTypeSafety + - React-callinvoker + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - SocketRocket + - Yoga + - NitroModules (0.35.4): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric + - RCTRequired + - RCTTypeSafety + - React-callinvoker + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - SocketRocket + - Yoga - RCT-Folly (2024.11.18.00): - boost - DoubleConversion @@ -2350,85 +2409,119 @@ PODS: - React-utils (= 0.82.1) - SocketRocket - SocketRocket (0.7.1) + - VisionCamera (5.0.4): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - NitroImage + - NitroModules + - RCT-Folly + - RCT-Folly/Fabric + - RCTRequired + - RCTTypeSafety + - React-callinvoker + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - SocketRocket + - Yoga - Yoga (0.0.0) DEPENDENCIES: - - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) - - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) - - fast_float (from `../node_modules/react-native/third-party-podspecs/fast_float.podspec`) - - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) - - fmt (from `../node_modules/react-native/third-party-podspecs/fmt.podspec`) - - glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`) + - boost (from `../../../node_modules/react-native/third-party-podspecs/boost.podspec`) + - DoubleConversion (from `../../../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) + - fast_float (from `../../../node_modules/react-native/third-party-podspecs/fast_float.podspec`) + - FBLazyVector (from `../../../node_modules/react-native/Libraries/FBLazyVector`) + - fmt (from `../../../node_modules/react-native/third-party-podspecs/fmt.podspec`) + - glog (from `../../../node_modules/react-native/third-party-podspecs/glog.podspec`) - "HarnessUI (from `../node_modules/@react-native-harness/ui`)" - - hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`) - - RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) - - RCTDeprecation (from `../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`) - - RCTRequired (from `../node_modules/react-native/Libraries/Required`) - - RCTTypeSafety (from `../node_modules/react-native/Libraries/TypeSafety`) - - React (from `../node_modules/react-native/`) - - React-callinvoker (from `../node_modules/react-native/ReactCommon/callinvoker`) - - React-Core (from `../node_modules/react-native/`) - - React-Core/RCTWebSocket (from `../node_modules/react-native/`) - - React-CoreModules (from `../node_modules/react-native/React/CoreModules`) - - React-cxxreact (from `../node_modules/react-native/ReactCommon/cxxreact`) - - React-debug (from `../node_modules/react-native/ReactCommon/react/debug`) - - React-defaultsnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/defaults`) - - React-domnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/dom`) - - React-Fabric (from `../node_modules/react-native/ReactCommon`) - - React-FabricComponents (from `../node_modules/react-native/ReactCommon`) - - React-FabricImage (from `../node_modules/react-native/ReactCommon`) - - React-featureflags (from `../node_modules/react-native/ReactCommon/react/featureflags`) - - React-featureflagsnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/featureflags`) - - React-graphics (from `../node_modules/react-native/ReactCommon/react/renderer/graphics`) - - React-hermes (from `../node_modules/react-native/ReactCommon/hermes`) - - React-idlecallbacksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks`) - - React-ImageManager (from `../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios`) - - React-jserrorhandler (from `../node_modules/react-native/ReactCommon/jserrorhandler`) - - React-jsi (from `../node_modules/react-native/ReactCommon/jsi`) - - React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`) - - React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector-modern`) - - React-jsinspectorcdp (from `../node_modules/react-native/ReactCommon/jsinspector-modern/cdp`) - - React-jsinspectornetwork (from `../node_modules/react-native/ReactCommon/jsinspector-modern/network`) - - React-jsinspectortracing (from `../node_modules/react-native/ReactCommon/jsinspector-modern/tracing`) - - React-jsitooling (from `../node_modules/react-native/ReactCommon/jsitooling`) - - React-jsitracing (from `../node_modules/react-native/ReactCommon/hermes/executor/`) - - React-logger (from `../node_modules/react-native/ReactCommon/logger`) - - React-Mapbuffer (from `../node_modules/react-native/ReactCommon`) - - React-microtasksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`) - - React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`) - - React-oscompat (from `../node_modules/react-native/ReactCommon/oscompat`) - - React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`) - - React-performancecdpmetrics (from `../node_modules/react-native/ReactCommon/react/performance/cdpmetrics`) - - React-performancetimeline (from `../node_modules/react-native/ReactCommon/react/performance/timeline`) - - React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`) - - React-RCTAnimation (from `../node_modules/react-native/Libraries/NativeAnimation`) - - React-RCTAppDelegate (from `../node_modules/react-native/Libraries/AppDelegate`) - - React-RCTBlob (from `../node_modules/react-native/Libraries/Blob`) - - React-RCTFabric (from `../node_modules/react-native/React`) - - React-RCTFBReactNativeSpec (from `../node_modules/react-native/React`) - - React-RCTImage (from `../node_modules/react-native/Libraries/Image`) - - React-RCTLinking (from `../node_modules/react-native/Libraries/LinkingIOS`) - - React-RCTNetwork (from `../node_modules/react-native/Libraries/Network`) - - React-RCTRuntime (from `../node_modules/react-native/React/Runtime`) - - React-RCTSettings (from `../node_modules/react-native/Libraries/Settings`) - - React-RCTText (from `../node_modules/react-native/Libraries/Text`) - - React-RCTVibration (from `../node_modules/react-native/Libraries/Vibration`) - - React-rendererconsistency (from `../node_modules/react-native/ReactCommon/react/renderer/consistency`) - - React-renderercss (from `../node_modules/react-native/ReactCommon/react/renderer/css`) - - React-rendererdebug (from `../node_modules/react-native/ReactCommon/react/renderer/debug`) - - React-RuntimeApple (from `../node_modules/react-native/ReactCommon/react/runtime/platform/ios`) - - React-RuntimeCore (from `../node_modules/react-native/ReactCommon/react/runtime`) - - React-runtimeexecutor (from `../node_modules/react-native/ReactCommon/runtimeexecutor`) - - React-RuntimeHermes (from `../node_modules/react-native/ReactCommon/react/runtime`) - - React-runtimescheduler (from `../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler`) - - React-timing (from `../node_modules/react-native/ReactCommon/react/timing`) - - React-utils (from `../node_modules/react-native/ReactCommon/react/utils`) - - React-webperformancenativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/webperformance`) + - hermes-engine (from `../../../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`) + - NitroImage (from `../../../node_modules/react-native-nitro-image`) + - NitroModules (from `../../../node_modules/react-native-nitro-modules`) + - RCT-Folly (from `../../../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) + - RCTDeprecation (from `../../../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`) + - RCTRequired (from `../../../node_modules/react-native/Libraries/Required`) + - RCTTypeSafety (from `../../../node_modules/react-native/Libraries/TypeSafety`) + - React (from `../../../node_modules/react-native/`) + - React-callinvoker (from `../../../node_modules/react-native/ReactCommon/callinvoker`) + - React-Core (from `../../../node_modules/react-native/`) + - React-Core/RCTWebSocket (from `../../../node_modules/react-native/`) + - React-CoreModules (from `../../../node_modules/react-native/React/CoreModules`) + - React-cxxreact (from `../../../node_modules/react-native/ReactCommon/cxxreact`) + - React-debug (from `../../../node_modules/react-native/ReactCommon/react/debug`) + - React-defaultsnativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/defaults`) + - React-domnativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/dom`) + - React-Fabric (from `../../../node_modules/react-native/ReactCommon`) + - React-FabricComponents (from `../../../node_modules/react-native/ReactCommon`) + - React-FabricImage (from `../../../node_modules/react-native/ReactCommon`) + - React-featureflags (from `../../../node_modules/react-native/ReactCommon/react/featureflags`) + - React-featureflagsnativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/featureflags`) + - React-graphics (from `../../../node_modules/react-native/ReactCommon/react/renderer/graphics`) + - React-hermes (from `../../../node_modules/react-native/ReactCommon/hermes`) + - React-idlecallbacksnativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks`) + - React-ImageManager (from `../../../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios`) + - React-jserrorhandler (from `../../../node_modules/react-native/ReactCommon/jserrorhandler`) + - React-jsi (from `../../../node_modules/react-native/ReactCommon/jsi`) + - React-jsiexecutor (from `../../../node_modules/react-native/ReactCommon/jsiexecutor`) + - React-jsinspector (from `../../../node_modules/react-native/ReactCommon/jsinspector-modern`) + - React-jsinspectorcdp (from `../../../node_modules/react-native/ReactCommon/jsinspector-modern/cdp`) + - React-jsinspectornetwork (from `../../../node_modules/react-native/ReactCommon/jsinspector-modern/network`) + - React-jsinspectortracing (from `../../../node_modules/react-native/ReactCommon/jsinspector-modern/tracing`) + - React-jsitooling (from `../../../node_modules/react-native/ReactCommon/jsitooling`) + - React-jsitracing (from `../../../node_modules/react-native/ReactCommon/hermes/executor/`) + - React-logger (from `../../../node_modules/react-native/ReactCommon/logger`) + - React-Mapbuffer (from `../../../node_modules/react-native/ReactCommon`) + - React-microtasksnativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`) + - React-NativeModulesApple (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`) + - React-oscompat (from `../../../node_modules/react-native/ReactCommon/oscompat`) + - React-perflogger (from `../../../node_modules/react-native/ReactCommon/reactperflogger`) + - React-performancecdpmetrics (from `../../../node_modules/react-native/ReactCommon/react/performance/cdpmetrics`) + - React-performancetimeline (from `../../../node_modules/react-native/ReactCommon/react/performance/timeline`) + - React-RCTActionSheet (from `../../../node_modules/react-native/Libraries/ActionSheetIOS`) + - React-RCTAnimation (from `../../../node_modules/react-native/Libraries/NativeAnimation`) + - React-RCTAppDelegate (from `../../../node_modules/react-native/Libraries/AppDelegate`) + - React-RCTBlob (from `../../../node_modules/react-native/Libraries/Blob`) + - React-RCTFabric (from `../../../node_modules/react-native/React`) + - React-RCTFBReactNativeSpec (from `../../../node_modules/react-native/React`) + - React-RCTImage (from `../../../node_modules/react-native/Libraries/Image`) + - React-RCTLinking (from `../../../node_modules/react-native/Libraries/LinkingIOS`) + - React-RCTNetwork (from `../../../node_modules/react-native/Libraries/Network`) + - React-RCTRuntime (from `../../../node_modules/react-native/React/Runtime`) + - React-RCTSettings (from `../../../node_modules/react-native/Libraries/Settings`) + - React-RCTText (from `../../../node_modules/react-native/Libraries/Text`) + - React-RCTVibration (from `../../../node_modules/react-native/Libraries/Vibration`) + - React-rendererconsistency (from `../../../node_modules/react-native/ReactCommon/react/renderer/consistency`) + - React-renderercss (from `../../../node_modules/react-native/ReactCommon/react/renderer/css`) + - React-rendererdebug (from `../../../node_modules/react-native/ReactCommon/react/renderer/debug`) + - React-RuntimeApple (from `../../../node_modules/react-native/ReactCommon/react/runtime/platform/ios`) + - React-RuntimeCore (from `../../../node_modules/react-native/ReactCommon/react/runtime`) + - React-runtimeexecutor (from `../../../node_modules/react-native/ReactCommon/runtimeexecutor`) + - React-RuntimeHermes (from `../../../node_modules/react-native/ReactCommon/react/runtime`) + - React-runtimescheduler (from `../../../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler`) + - React-timing (from `../../../node_modules/react-native/ReactCommon/react/timing`) + - React-utils (from `../../../node_modules/react-native/ReactCommon/react/utils`) + - React-webperformancenativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/webperformance`) - ReactAppDependencyProvider (from `build/generated/ios`) - ReactCodegen (from `build/generated/ios`) - - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) + - ReactCommon/turbomodule/core (from `../../../node_modules/react-native/ReactCommon`) - SocketRocket (~> 0.7.1) - - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) + - VisionCamera (from `../../../node_modules/react-native-vision-camera`) + - Yoga (from `../../../node_modules/react-native/ReactCommon/yoga`) SPEC REPOS: trunk: @@ -2436,154 +2529,160 @@ SPEC REPOS: EXTERNAL SOURCES: boost: - :podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec" + :podspec: "../../../node_modules/react-native/third-party-podspecs/boost.podspec" DoubleConversion: - :podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" + :podspec: "../../../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" fast_float: - :podspec: "../node_modules/react-native/third-party-podspecs/fast_float.podspec" + :podspec: "../../../node_modules/react-native/third-party-podspecs/fast_float.podspec" FBLazyVector: - :path: "../node_modules/react-native/Libraries/FBLazyVector" + :path: "../../../node_modules/react-native/Libraries/FBLazyVector" fmt: - :podspec: "../node_modules/react-native/third-party-podspecs/fmt.podspec" + :podspec: "../../../node_modules/react-native/third-party-podspecs/fmt.podspec" glog: - :podspec: "../node_modules/react-native/third-party-podspecs/glog.podspec" + :podspec: "../../../node_modules/react-native/third-party-podspecs/glog.podspec" HarnessUI: :path: "../node_modules/@react-native-harness/ui" hermes-engine: - :podspec: "../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec" + :podspec: "../../../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec" :tag: hermes-2025-09-01-RNv0.82.0-265ef62ff3eb7289d17e366664ac0da82303e101 + NitroImage: + :path: "../../../node_modules/react-native-nitro-image" + NitroModules: + :path: "../../../node_modules/react-native-nitro-modules" RCT-Folly: - :podspec: "../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec" + :podspec: "../../../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec" RCTDeprecation: - :path: "../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation" + :path: "../../../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation" RCTRequired: - :path: "../node_modules/react-native/Libraries/Required" + :path: "../../../node_modules/react-native/Libraries/Required" RCTTypeSafety: - :path: "../node_modules/react-native/Libraries/TypeSafety" + :path: "../../../node_modules/react-native/Libraries/TypeSafety" React: - :path: "../node_modules/react-native/" + :path: "../../../node_modules/react-native/" React-callinvoker: - :path: "../node_modules/react-native/ReactCommon/callinvoker" + :path: "../../../node_modules/react-native/ReactCommon/callinvoker" React-Core: - :path: "../node_modules/react-native/" + :path: "../../../node_modules/react-native/" React-CoreModules: - :path: "../node_modules/react-native/React/CoreModules" + :path: "../../../node_modules/react-native/React/CoreModules" React-cxxreact: - :path: "../node_modules/react-native/ReactCommon/cxxreact" + :path: "../../../node_modules/react-native/ReactCommon/cxxreact" React-debug: - :path: "../node_modules/react-native/ReactCommon/react/debug" + :path: "../../../node_modules/react-native/ReactCommon/react/debug" React-defaultsnativemodule: - :path: "../node_modules/react-native/ReactCommon/react/nativemodule/defaults" + :path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/defaults" React-domnativemodule: - :path: "../node_modules/react-native/ReactCommon/react/nativemodule/dom" + :path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/dom" React-Fabric: - :path: "../node_modules/react-native/ReactCommon" + :path: "../../../node_modules/react-native/ReactCommon" React-FabricComponents: - :path: "../node_modules/react-native/ReactCommon" + :path: "../../../node_modules/react-native/ReactCommon" React-FabricImage: - :path: "../node_modules/react-native/ReactCommon" + :path: "../../../node_modules/react-native/ReactCommon" React-featureflags: - :path: "../node_modules/react-native/ReactCommon/react/featureflags" + :path: "../../../node_modules/react-native/ReactCommon/react/featureflags" React-featureflagsnativemodule: - :path: "../node_modules/react-native/ReactCommon/react/nativemodule/featureflags" + :path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/featureflags" React-graphics: - :path: "../node_modules/react-native/ReactCommon/react/renderer/graphics" + :path: "../../../node_modules/react-native/ReactCommon/react/renderer/graphics" React-hermes: - :path: "../node_modules/react-native/ReactCommon/hermes" + :path: "../../../node_modules/react-native/ReactCommon/hermes" React-idlecallbacksnativemodule: - :path: "../node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks" + :path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks" React-ImageManager: - :path: "../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios" + :path: "../../../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios" React-jserrorhandler: - :path: "../node_modules/react-native/ReactCommon/jserrorhandler" + :path: "../../../node_modules/react-native/ReactCommon/jserrorhandler" React-jsi: - :path: "../node_modules/react-native/ReactCommon/jsi" + :path: "../../../node_modules/react-native/ReactCommon/jsi" React-jsiexecutor: - :path: "../node_modules/react-native/ReactCommon/jsiexecutor" + :path: "../../../node_modules/react-native/ReactCommon/jsiexecutor" React-jsinspector: - :path: "../node_modules/react-native/ReactCommon/jsinspector-modern" + :path: "../../../node_modules/react-native/ReactCommon/jsinspector-modern" React-jsinspectorcdp: - :path: "../node_modules/react-native/ReactCommon/jsinspector-modern/cdp" + :path: "../../../node_modules/react-native/ReactCommon/jsinspector-modern/cdp" React-jsinspectornetwork: - :path: "../node_modules/react-native/ReactCommon/jsinspector-modern/network" + :path: "../../../node_modules/react-native/ReactCommon/jsinspector-modern/network" React-jsinspectortracing: - :path: "../node_modules/react-native/ReactCommon/jsinspector-modern/tracing" + :path: "../../../node_modules/react-native/ReactCommon/jsinspector-modern/tracing" React-jsitooling: - :path: "../node_modules/react-native/ReactCommon/jsitooling" + :path: "../../../node_modules/react-native/ReactCommon/jsitooling" React-jsitracing: - :path: "../node_modules/react-native/ReactCommon/hermes/executor/" + :path: "../../../node_modules/react-native/ReactCommon/hermes/executor/" React-logger: - :path: "../node_modules/react-native/ReactCommon/logger" + :path: "../../../node_modules/react-native/ReactCommon/logger" React-Mapbuffer: - :path: "../node_modules/react-native/ReactCommon" + :path: "../../../node_modules/react-native/ReactCommon" React-microtasksnativemodule: - :path: "../node_modules/react-native/ReactCommon/react/nativemodule/microtasks" + :path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/microtasks" React-NativeModulesApple: - :path: "../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios" + :path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios" React-oscompat: - :path: "../node_modules/react-native/ReactCommon/oscompat" + :path: "../../../node_modules/react-native/ReactCommon/oscompat" React-perflogger: - :path: "../node_modules/react-native/ReactCommon/reactperflogger" + :path: "../../../node_modules/react-native/ReactCommon/reactperflogger" React-performancecdpmetrics: - :path: "../node_modules/react-native/ReactCommon/react/performance/cdpmetrics" + :path: "../../../node_modules/react-native/ReactCommon/react/performance/cdpmetrics" React-performancetimeline: - :path: "../node_modules/react-native/ReactCommon/react/performance/timeline" + :path: "../../../node_modules/react-native/ReactCommon/react/performance/timeline" React-RCTActionSheet: - :path: "../node_modules/react-native/Libraries/ActionSheetIOS" + :path: "../../../node_modules/react-native/Libraries/ActionSheetIOS" React-RCTAnimation: - :path: "../node_modules/react-native/Libraries/NativeAnimation" + :path: "../../../node_modules/react-native/Libraries/NativeAnimation" React-RCTAppDelegate: - :path: "../node_modules/react-native/Libraries/AppDelegate" + :path: "../../../node_modules/react-native/Libraries/AppDelegate" React-RCTBlob: - :path: "../node_modules/react-native/Libraries/Blob" + :path: "../../../node_modules/react-native/Libraries/Blob" React-RCTFabric: - :path: "../node_modules/react-native/React" + :path: "../../../node_modules/react-native/React" React-RCTFBReactNativeSpec: - :path: "../node_modules/react-native/React" + :path: "../../../node_modules/react-native/React" React-RCTImage: - :path: "../node_modules/react-native/Libraries/Image" + :path: "../../../node_modules/react-native/Libraries/Image" React-RCTLinking: - :path: "../node_modules/react-native/Libraries/LinkingIOS" + :path: "../../../node_modules/react-native/Libraries/LinkingIOS" React-RCTNetwork: - :path: "../node_modules/react-native/Libraries/Network" + :path: "../../../node_modules/react-native/Libraries/Network" React-RCTRuntime: - :path: "../node_modules/react-native/React/Runtime" + :path: "../../../node_modules/react-native/React/Runtime" React-RCTSettings: - :path: "../node_modules/react-native/Libraries/Settings" + :path: "../../../node_modules/react-native/Libraries/Settings" React-RCTText: - :path: "../node_modules/react-native/Libraries/Text" + :path: "../../../node_modules/react-native/Libraries/Text" React-RCTVibration: - :path: "../node_modules/react-native/Libraries/Vibration" + :path: "../../../node_modules/react-native/Libraries/Vibration" React-rendererconsistency: - :path: "../node_modules/react-native/ReactCommon/react/renderer/consistency" + :path: "../../../node_modules/react-native/ReactCommon/react/renderer/consistency" React-renderercss: - :path: "../node_modules/react-native/ReactCommon/react/renderer/css" + :path: "../../../node_modules/react-native/ReactCommon/react/renderer/css" React-rendererdebug: - :path: "../node_modules/react-native/ReactCommon/react/renderer/debug" + :path: "../../../node_modules/react-native/ReactCommon/react/renderer/debug" React-RuntimeApple: - :path: "../node_modules/react-native/ReactCommon/react/runtime/platform/ios" + :path: "../../../node_modules/react-native/ReactCommon/react/runtime/platform/ios" React-RuntimeCore: - :path: "../node_modules/react-native/ReactCommon/react/runtime" + :path: "../../../node_modules/react-native/ReactCommon/react/runtime" React-runtimeexecutor: - :path: "../node_modules/react-native/ReactCommon/runtimeexecutor" + :path: "../../../node_modules/react-native/ReactCommon/runtimeexecutor" React-RuntimeHermes: - :path: "../node_modules/react-native/ReactCommon/react/runtime" + :path: "../../../node_modules/react-native/ReactCommon/react/runtime" React-runtimescheduler: - :path: "../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler" + :path: "../../../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler" React-timing: - :path: "../node_modules/react-native/ReactCommon/react/timing" + :path: "../../../node_modules/react-native/ReactCommon/react/timing" React-utils: - :path: "../node_modules/react-native/ReactCommon/react/utils" + :path: "../../../node_modules/react-native/ReactCommon/react/utils" React-webperformancenativemodule: - :path: "../node_modules/react-native/ReactCommon/react/nativemodule/webperformance" + :path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/webperformance" ReactAppDependencyProvider: :path: build/generated/ios ReactCodegen: :path: build/generated/ios ReactCommon: - :path: "../node_modules/react-native/ReactCommon" + :path: "../../../node_modules/react-native/ReactCommon" + VisionCamera: + :path: "../../../node_modules/react-native-vision-camera" Yoga: - :path: "../node_modules/react-native/ReactCommon/yoga" + :path: "../../../node_modules/react-native/ReactCommon/yoga" SPEC CHECKSUMS: boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90 @@ -2592,8 +2691,10 @@ SPEC CHECKSUMS: FBLazyVector: 0aa6183b9afe3c31fc65b5d1eeef1f3c19b63bfa fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd glog: 5683914934d5b6e4240e497e0f4a3b42d1854183 - HarnessUI: 23b272c7d3a0a3628479d1287c1d4bd59b562636 + HarnessUI: 01740b858c62c55d42995d4ca459ead036b96c9a hermes-engine: 273e30e7fb618279934b0b95ffab60ecedb7acf5 + NitroImage: dfec7a8d5e6ba8228ed780bc70041e762cbbbd0b + NitroModules: b24827b7772f5a030aef074547a2393a6e03579e RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669 RCTDeprecation: f17e2ebc07876ca9ab8eb6e4b0a4e4647497ae3a RCTRequired: e2c574c1b45231f7efb0834936bd609d75072b63 @@ -2657,9 +2758,10 @@ SPEC CHECKSUMS: React-utils: abf37b162f560cd0e3e5d037af30bb796512246d React-webperformancenativemodule: 50a57c713a90d27ae3ab947a6c9c8859bcb49709 ReactAppDependencyProvider: a45ef34bb22dc1c9b2ac1f74167d9a28af961176 - ReactCodegen: 878add6c7d8ff8cea87697c44d29c03b79b6f2d9 + ReactCodegen: 65ae48ae967a383859da021028e6e8dd7b2d97d1 ReactCommon: 804dc80944fa90b86800b43c871742ec005ca424 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 + VisionCamera: 889238ad98665463fcc2fa44385614979263cfc7 Yoga: 689c8e04277f3ad631e60fe2a08e41d411daf8eb PODFILE CHECKSUM: 0a1696308b49d81f7b7a744c9ae31d90de903a3e diff --git a/apps/playground/package.json b/apps/playground/package.json index bc466f1..0daa879 100644 --- a/apps/playground/package.json +++ b/apps/playground/package.json @@ -9,24 +9,27 @@ "react": "19.2.3", "react-dom": "19.2.3", "react-native": "0.82.1", + "react-native-nitro-image": "^0.13.1", + "react-native-nitro-modules": "^0.35.4", + "react-native-vision-camera": "^5.0.4", "react-native-web": "^0.21.2" }, "devDependencies": { - "react-native-harness": "workspace:*", - "@react-native-harness/runtime": "workspace:*", - "@react-native-harness/ui": "workspace:*", "@react-native-community/cli": "20.0.0", "@react-native-community/cli-platform-android": "20.0.0", "@react-native-community/cli-platform-ios": "20.0.0", + "@react-native-harness/jest": "workspace:*", + "@react-native-harness/platform-android": "workspace:*", + "@react-native-harness/platform-apple": "workspace:*", + "@react-native-harness/platform-vega": "workspace:*", + "@react-native-harness/platform-web": "workspace:*", + "@react-native-harness/runtime": "workspace:*", + "@react-native-harness/ui": "workspace:*", "@react-native/babel-preset": "0.82.1", "@react-native/eslint-config": "0.82.1", "@react-native/metro-config": "0.82.1", "@react-native/typescript-config": "0.82.1", - "@react-native-harness/jest": "workspace:*", "jest": "^30.2.0", - "@react-native-harness/platform-android": "workspace:*", - "@react-native-harness/platform-apple": "workspace:*", - "@react-native-harness/platform-vega": "workspace:*", - "@react-native-harness/platform-web": "workspace:*" + "react-native-harness": "workspace:*" } } diff --git a/apps/playground/src/__tests__/ui/permissions.harness.tsx b/apps/playground/src/__tests__/ui/permissions.harness.tsx new file mode 100644 index 0000000..c7894a5 --- /dev/null +++ b/apps/playground/src/__tests__/ui/permissions.harness.tsx @@ -0,0 +1,73 @@ +import React, { useState } from 'react'; +import { + describe, + expect, + render, + test, + waitUntil, +} from 'react-native-harness'; +import { screen, userEvent } from '@react-native-harness/ui'; +import { Platform, Pressable, Text, View } from 'react-native'; + +describe('Permissions', () => { + test('should allow iOS camera permissions through the system prompt', async () => { + if (Platform.OS !== 'ios') { + return; + } + + const { VisionCamera } = + require('react-native-vision-camera') as typeof import('react-native-vision-camera'); + const initialStatus = VisionCamera.cameraPermissionStatus; + let latestStatus = initialStatus; + + const CameraPermissionTrigger = () => { + const [status, setStatus] = useState(initialStatus); + + const handlePress = async () => { + const wasGranted = await VisionCamera.requestCameraPermission(); + const nextStatus = wasGranted ? 'authorized' : 'denied'; + latestStatus = nextStatus; + setStatus(nextStatus); + }; + + return ( + + {status} + + Request camera permission + + + ); + }; + + await render(); + + expect(initialStatus).not.toBe('denied'); + + const requestButton = await screen.findByTestId( + 'request-camera-permission', + ); + await userEvent.press(requestButton); + + await waitUntil(() => latestStatus === 'authorized', { timeout: 30000 }); + + expect(latestStatus).toBe('authorized'); + expect(await screen.findByTestId('camera-permission-status')).toBeDefined(); + }); +}); diff --git a/packages/platform-ios/package.json b/packages/platform-ios/package.json index 9455042..0085b65 100644 --- a/packages/platform-ios/package.json +++ b/packages/platform-ios/package.json @@ -3,9 +3,6 @@ "description": "Apple platform for React Native Harness", "version": "1.1.0", "type": "module", - "scripts": { - "xctest-agent:generate": "xcodegen generate --spec ./xctest-agent/project.yml --project ./xctest-agent" - }, "main": "./dist/index.js", "module": "./dist/index.js", "types": "./dist/index.d.ts", @@ -22,6 +19,7 @@ "@react-native-harness/config": "workspace:*", "@react-native-harness/platforms": "workspace:*", "@react-native-harness/tools": "workspace:*", + "appium-ios-device": "^3.1.11", "zod": "^3.25.67", "tslib": "^2.3.0" }, diff --git a/packages/platform-ios/src/__tests__/instance-xctest-agent.test.ts b/packages/platform-ios/src/__tests__/instance-xctest-agent.test.ts index 80ebf3b..098c488 100644 --- a/packages/platform-ios/src/__tests__/instance-xctest-agent.test.ts +++ b/packages/platform-ios/src/__tests__/instance-xctest-agent.test.ts @@ -37,7 +37,7 @@ describe('iOS XCTest agent runner integration', () => { }); }); - it('prepares and lazily starts the simulator XCTest agent', async () => { + it('starts the simulator XCTest agent during platform initialization', async () => { vi.spyOn(simctl, 'getSimulatorId').mockResolvedValue('sim-udid'); vi.spyOn(simctl, 'isAppInstalled').mockResolvedValue(true); vi.spyOn(simctl, 'getSimulatorStatus').mockResolvedValue('Booted'); @@ -66,11 +66,11 @@ describe('iOS XCTest agent runner integration', () => { }, ); - await instance.prepareRun?.(); await instance.startApp(); - await instance.disposeRun?.(); + await instance.dispose(); expect(mocks.createXCTestAgentController).toHaveBeenCalledWith({ + appBundleId: 'com.harnessplayground', capabilities: [ expect.objectContaining({ getLaunchEnvironment: expect.any(Function), @@ -81,12 +81,12 @@ describe('iOS XCTest agent runner integration', () => { id: 'sim-udid', }, }); - expect(mocks.prepare).toHaveBeenCalledTimes(1); + expect(mocks.prepare).not.toHaveBeenCalled(); expect(mocks.ensureStarted).toHaveBeenCalledTimes(1); expect(mocks.dispose).toHaveBeenCalledTimes(1); }); - it('prepares and lazily starts the physical-device XCTest agent', async () => { + it('starts the physical-device XCTest agent during platform initialization', async () => { vi.spyOn(devicectl, 'getDevice').mockResolvedValue({ identifier: 'device-udid', deviceProperties: { @@ -115,11 +115,11 @@ describe('iOS XCTest agent runner integration', () => { harnessConfig, ); - await instance.prepareRun?.(); await instance.restartApp(); - await instance.disposeRun?.(); + await instance.dispose(); expect(mocks.createXCTestAgentController).toHaveBeenCalledWith({ + appBundleId: 'com.harnessplayground', capabilities: [ expect.objectContaining({ getLaunchEnvironment: expect.any(Function), @@ -130,7 +130,7 @@ describe('iOS XCTest agent runner integration', () => { id: 'device-udid', }, }); - expect(mocks.prepare).toHaveBeenCalledTimes(1); + expect(mocks.prepare).not.toHaveBeenCalled(); expect(mocks.ensureStarted).toHaveBeenCalledTimes(1); expect(mocks.dispose).toHaveBeenCalledTimes(1); }); diff --git a/packages/platform-ios/src/__tests__/instance.test.ts b/packages/platform-ios/src/__tests__/instance.test.ts index 8867f6c..bc77895 100644 --- a/packages/platform-ios/src/__tests__/instance.test.ts +++ b/packages/platform-ios/src/__tests__/instance.test.ts @@ -14,6 +14,17 @@ import { mkdtempSync, mkdirSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; +const xctestAgentMocks = vi.hoisted(() => ({ + createXCTestAgentController: vi.fn(), + dispose: vi.fn(async () => undefined), + ensureStarted: vi.fn(async () => undefined), + prepare: vi.fn(async () => undefined), +})); + +vi.mock('../xctest-agent.js', () => ({ + createXCTestAgentController: xctestAgentMocks.createXCTestAgentController, +})); + const harnessConfig = { metroPort: DEFAULT_METRO_PORT, } as HarnessConfig; @@ -30,6 +41,12 @@ describe('iOS platform instance dependency validation', () => { beforeEach(() => { vi.restoreAllMocks(); vi.unstubAllEnvs(); + xctestAgentMocks.createXCTestAgentController.mockReturnValue({ + prepare: xctestAgentMocks.prepare, + ensureStarted: xctestAgentMocks.ensureStarted, + stop: vi.fn(async () => undefined), + dispose: xctestAgentMocks.dispose, + }); }); it('does not require extra dependencies before creating a simulator instance', async () => { @@ -37,7 +54,7 @@ describe('iOS platform instance dependency validation', () => { vi.spyOn(simctl, 'isAppInstalled').mockResolvedValue(true); vi.spyOn(simctl, 'getSimulatorStatus').mockResolvedValue('Booted'); vi.spyOn(simctl, 'applyHarnessJsLocationOverride').mockResolvedValue( - undefined + undefined, ); const config = { @@ -51,7 +68,7 @@ describe('iOS platform instance dependency validation', () => { }; await expect( - getAppleSimulatorPlatformInstance(config, harnessConfig, init) + getAppleSimulatorPlatformInstance(config, harnessConfig, init), ).resolves.toBeDefined(); }); @@ -77,7 +94,7 @@ describe('iOS platform instance dependency validation', () => { }; await expect( - getApplePhysicalDevicePlatformInstance(config, harnessConfig) + getApplePhysicalDevicePlatformInstance(config, harnessConfig), ).resolves.toBeDefined(); expect(getDevice).toHaveBeenCalledWith('My iPhone'); }); @@ -106,8 +123,8 @@ describe('iOS platform instance dependency validation', () => { await expect( getApplePhysicalDevicePlatformInstance( config, - harnessConfigWithoutNativeCrashDetection - ) + harnessConfigWithoutNativeCrashDetection, + ), ).resolves.toBeDefined(); }); @@ -116,7 +133,7 @@ describe('iOS platform instance dependency validation', () => { vi.spyOn(simctl, 'isAppInstalled').mockResolvedValue(true); vi.spyOn(simctl, 'getSimulatorStatus').mockResolvedValue('Booted'); vi.spyOn(simctl, 'applyHarnessJsLocationOverride').mockResolvedValue( - undefined + undefined, ); const instance = await getAppleSimulatorPlatformInstance( @@ -130,7 +147,7 @@ describe('iOS platform instance dependency validation', () => { bundleId: 'com.harnessplayground', }, harnessConfigWithoutNativeCrashDetection, - init + init, ); const listener = vi.fn(); @@ -169,13 +186,13 @@ describe('iOS platform instance dependency validation', () => { bundleId: 'com.harnessplayground', }, harnessConfig, - init + init, ); expect(applyOverride).toHaveBeenCalledWith( 'sim-udid', 'com.harnessplayground', - 'localhost:8081' + 'localhost:8081', ); await instance.dispose(); @@ -183,7 +200,7 @@ describe('iOS platform instance dependency validation', () => { expect(stopApp).toHaveBeenCalledWith('sim-udid', 'com.harnessplayground'); expect(clearOverride).toHaveBeenCalledWith( 'sim-udid', - 'com.harnessplayground' + 'com.harnessplayground', ); expect(shutdownSimulator).not.toHaveBeenCalled(); }); @@ -199,11 +216,11 @@ describe('iOS platform instance dependency validation', () => { .mockResolvedValue(undefined); vi.spyOn(simctl, 'isAppInstalled').mockResolvedValue(true); vi.spyOn(simctl, 'applyHarnessJsLocationOverride').mockResolvedValue( - undefined + undefined, ); vi.spyOn(simctl, 'stopApp').mockResolvedValue(undefined); vi.spyOn(simctl, 'clearHarnessJsLocationOverride').mockResolvedValue( - undefined + undefined, ); const shutdownSimulator = vi .spyOn(simctl, 'shutdownSimulator') @@ -220,7 +237,7 @@ describe('iOS platform instance dependency validation', () => { bundleId: 'com.harnessplayground', }, harnessConfig, - init + init, ); expect(bootSimulator).toHaveBeenCalledWith('sim-udid'); @@ -242,11 +259,11 @@ describe('iOS platform instance dependency validation', () => { .mockResolvedValue(undefined); vi.spyOn(simctl, 'isAppInstalled').mockResolvedValue(true); vi.spyOn(simctl, 'applyHarnessJsLocationOverride').mockResolvedValue( - undefined + undefined, ); vi.spyOn(simctl, 'stopApp').mockResolvedValue(undefined); vi.spyOn(simctl, 'clearHarnessJsLocationOverride').mockResolvedValue( - undefined + undefined, ); const shutdownSimulator = vi .spyOn(simctl, 'shutdownSimulator') @@ -263,7 +280,7 @@ describe('iOS platform instance dependency validation', () => { bundleId: 'com.harnessplayground', }, harnessConfig, - init + init, ); expect(bootSimulator).not.toHaveBeenCalled(); @@ -285,11 +302,11 @@ describe('iOS platform instance dependency validation', () => { .mockResolvedValue(undefined); vi.spyOn(simctl, 'isAppInstalled').mockResolvedValue(true); vi.spyOn(simctl, 'applyHarnessJsLocationOverride').mockResolvedValue( - undefined + undefined, ); vi.spyOn(simctl, 'stopApp').mockResolvedValue(undefined); vi.spyOn(simctl, 'clearHarnessJsLocationOverride').mockResolvedValue( - undefined + undefined, ); const shutdownSimulator = vi .spyOn(simctl, 'shutdownSimulator') @@ -306,7 +323,7 @@ describe('iOS platform instance dependency validation', () => { bundleId: 'com.harnessplayground', }, harnessConfig, - init + init, ); expect(bootSimulator).toHaveBeenCalledWith('sim-udid'); @@ -329,7 +346,7 @@ describe('iOS platform instance dependency validation', () => { .spyOn(simctl, 'installApp') .mockResolvedValue(undefined); vi.spyOn(simctl, 'applyHarnessJsLocationOverride').mockResolvedValue( - undefined + undefined, ); try { @@ -345,8 +362,8 @@ describe('iOS platform instance dependency validation', () => { bundleId: 'com.harnessplayground', }, harnessConfig, - init - ) + init, + ), ).resolves.toBeDefined(); expect(installApp).toHaveBeenCalledWith('sim-udid', bundlePath); @@ -372,15 +389,15 @@ describe('iOS platform instance dependency validation', () => { bundleId: 'com.harnessplayground', }, harnessConfig, - init - ) + init, + ), ).rejects.toBeInstanceOf(HarnessAppPathError); }); it('throws a HarnessAppPathError when HARNESS_APP_PATH points to a missing app', async () => { vi.stubEnv( 'HARNESS_APP_PATH', - join(tmpdir(), 'rn-harness-ios-missing-app', 'Missing.app') + join(tmpdir(), 'rn-harness-ios-missing-app', 'Missing.app'), ); vi.spyOn(simctl, 'getSimulatorId').mockResolvedValue('sim-udid'); vi.spyOn(simctl, 'getSimulatorStatus').mockResolvedValue('Booted'); @@ -398,8 +415,8 @@ describe('iOS platform instance dependency validation', () => { bundleId: 'com.harnessplayground', }, harnessConfig, - init - ) + init, + ), ).rejects.toBeInstanceOf(HarnessAppPathError); }); }); diff --git a/packages/platform-ios/src/__tests__/xctest-agent-capabilities.test.ts b/packages/platform-ios/src/__tests__/xctest-agent-capabilities.test.ts index 39e2874..0aceb42 100644 --- a/packages/platform-ios/src/__tests__/xctest-agent-capabilities.test.ts +++ b/packages/platform-ios/src/__tests__/xctest-agent-capabilities.test.ts @@ -9,4 +9,20 @@ describe('xctest agent capabilities', () => { HARNESS_XCTEST_AGENT_AUTO_ACCEPT_PERMISSIONS: '1', }); }); + + it('enables permission auto-accept in the runtime configuration', () => { + const capability = createPermissionPromptAutoAcceptCapability(); + + expect( + capability.updateConfiguration?.({ + permissions: { + autoAcceptPermissions: false, + }, + }), + ).toEqual({ + permissions: { + autoAcceptPermissions: true, + }, + }); + }); }); diff --git a/packages/platform-ios/src/__tests__/xctest-agent-client.test.ts b/packages/platform-ios/src/__tests__/xctest-agent-client.test.ts new file mode 100644 index 0000000..6defc1a --- /dev/null +++ b/packages/platform-ios/src/__tests__/xctest-agent-client.test.ts @@ -0,0 +1,95 @@ +import { describe, expect, it, vi } from 'vitest'; +import { createXCTestAgentClient } from '../xctest-agent-client.js'; +import type { XCTestAgentTransport } from '../xctest-agent-transport.js'; + +describe('xctest-agent client', () => { + it('sends typed permission commands over the internal transport', async () => { + const request = vi + .fn>() + .mockResolvedValueOnce({ + body: JSON.stringify({ + permissions: { + autoAcceptPermissions: false, + }, + status: 'ok', + }), + headers: {}, + statusCode: 200, + }) + .mockResolvedValueOnce({ + body: JSON.stringify({ + permissions: { + autoAcceptPermissions: true, + }, + }), + headers: {}, + statusCode: 200, + }) + .mockResolvedValueOnce({ + body: JSON.stringify({ + permissions: { + autoAcceptPermissions: true, + }, + }), + headers: {}, + statusCode: 200, + }); + const dispose = vi.fn(async () => undefined); + const client = createXCTestAgentClient({ + dispose, + request, + }); + + await expect(client.health()).resolves.toEqual({ + permissions: { + autoAcceptPermissions: false, + }, + status: 'ok', + }); + await expect( + client.configurePermissions({ + autoAcceptPermissions: true, + }), + ).resolves.toEqual({ + autoAcceptPermissions: true, + }); + await expect(client.getPermissionsConfig()).resolves.toEqual({ + autoAcceptPermissions: true, + }); + + expect(request).toHaveBeenNthCalledWith(1, { + method: 'GET', + path: '/health', + body: undefined, + }); + expect(request).toHaveBeenNthCalledWith(2, { + method: 'POST', + path: '/permissions/configure', + body: JSON.stringify({ + autoAcceptPermissions: true, + }), + }); + expect(request).toHaveBeenNthCalledWith(3, { + method: 'GET', + path: '/permissions', + body: undefined, + }); + await client.dispose(); + expect(dispose).toHaveBeenCalledTimes(1); + }); + + it('throws on non-success responses', async () => { + const client = createXCTestAgentClient({ + dispose: vi.fn(async () => undefined), + request: vi.fn(async () => ({ + body: '{"error":"bad request"}', + headers: {}, + statusCode: 400, + })), + }); + + await expect(client.health()).rejects.toThrow( + 'XCTest agent GET /health failed with status 400', + ); + }); +}); diff --git a/packages/platform-ios/src/__tests__/xctest-agent.test.ts b/packages/platform-ios/src/__tests__/xctest-agent.test.ts index 719422c..8ee9740 100644 --- a/packages/platform-ios/src/__tests__/xctest-agent.test.ts +++ b/packages/platform-ios/src/__tests__/xctest-agent.test.ts @@ -1,10 +1,21 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import fs from 'node:fs'; +import os from 'node:os'; import path from 'node:path'; import { createHash } from 'node:crypto'; import { fileURLToPath } from 'node:url'; const mocks = vi.hoisted(() => ({ + activeAgentStops: [] as Array<() => void>, + configurePermissions: vi.fn(async () => ({ autoAcceptPermissions: true })), + disposeClient: vi.fn(async () => undefined), + disposeTransport: vi.fn(async () => undefined), + health: vi.fn(async () => ({ + permissions: { + autoAcceptPermissions: false, + }, + status: 'ok', + })), kill: vi.fn(), spawn: vi.fn(), })); @@ -20,7 +31,32 @@ vi.mock('@react-native-harness/tools', async () => { }; }); +vi.mock('../xctest-agent-client.js', () => ({ + createXCTestAgentClient: vi.fn(() => ({ + configurePermissions: mocks.configurePermissions, + dispose: mocks.disposeClient, + getPermissionsConfig: vi.fn(), + health: mocks.health, + })), +})); + +vi.mock('../xctest-agent-transport-simulator.js', () => ({ + createSimulatorXCTestAgentTransport: vi.fn(() => ({ + dispose: mocks.disposeTransport, + request: vi.fn(), + })), +})); + +vi.mock('../xctest-agent-transport-device.js', () => ({ + createDeviceXCTestAgentTransport: vi.fn(() => ({ + dispose: mocks.disposeTransport, + request: vi.fn(), + })), +})); + import { createXCTestAgentController } from '../xctest-agent.js'; +import { createDeviceXCTestAgentTransport } from '../xctest-agent-transport-device.js'; +import { createSimulatorXCTestAgentTransport } from '../xctest-agent-transport-simulator.js'; const projectRoot = path.resolve( path.dirname(fileURLToPath(import.meta.url)), @@ -28,15 +64,21 @@ const projectRoot = path.resolve( '..', 'xctest-agent', ); -const buildRoot = path.join(projectRoot, 'build'); +let buildRoot = ''; +let tempProjectRoot = ''; +const originalCwd = process.cwd(); const createLongRunningSubprocess = () => { let stopped = false; + const stop = () => { + stopped = true; + }; + const iterable = { nodeChildProcess: Promise.resolve({ kill: vi.fn(() => { - stopped = true; + stop(); mocks.kill(); }), }), @@ -47,24 +89,38 @@ const createLongRunningSubprocess = () => { }, }; - return iterable; + return { + stop, + subprocess: iterable, + }; }; describe('xctest-agent orchestration', () => { beforeEach(() => { vi.clearAllMocks(); + tempProjectRoot = fs.mkdtempSync( + path.join(os.tmpdir(), 'rn-harness-xctest-agent-'), + ); + process.chdir(tempProjectRoot); + buildRoot = path.join(tempProjectRoot, '.harness', 'xctest-agent'); rmBuildRoot(); + mocks.activeAgentStops.length = 0; mocks.spawn.mockImplementation((file: string, args?: string[]) => { if (file === 'xcodebuild' && args?.[0] === 'test-without-building') { - return createLongRunningSubprocess(); + const process = createLongRunningSubprocess(); + mocks.activeAgentStops.push(process.stop); + return process.subprocess; } - return createLongRunningSubprocess(); + return createLongRunningSubprocess().subprocess; }); }); afterEach(() => { rmBuildRoot(); + process.chdir(originalCwd); + fs.rmSync(tempProjectRoot, { recursive: true, force: true }); + tempProjectRoot = ''; }); it('builds the simulator agent artifacts and writes a cache manifest', async () => { @@ -79,15 +135,6 @@ describe('xctest-agent orchestration', () => { expect(mocks.spawn).toHaveBeenNthCalledWith( 1, - 'xcodegen', - expect.arrayContaining([ - 'generate', - '--spec', - expect.stringContaining('project.yml'), - ]), - ); - expect(mocks.spawn).toHaveBeenNthCalledWith( - 2, 'xcodebuild', expect.arrayContaining([ 'build-for-testing', @@ -121,15 +168,12 @@ describe('xctest-agent orchestration', () => { await controller.prepare(); - expect(mocks.spawn).toHaveBeenCalledTimes(1); - expect(mocks.spawn).toHaveBeenCalledWith( - 'xcodegen', - expect.arrayContaining(['generate']), - ); + expect(mocks.spawn).not.toHaveBeenCalled(); }); - it('starts the agent lazily and stops the long-lived test process on dispose', async () => { + it('starts the agent lazily, waits for readiness, and configures permissions', async () => { const controller = createXCTestAgentController({ + port: 49152, target: { kind: 'simulator', id: 'sim-999', @@ -139,6 +183,13 @@ describe('xctest-agent orchestration', () => { getLaunchEnvironment: () => ({ HARNESS_XCTEST_AGENT_MODE: 'test', }), + updateConfiguration: (configuration) => ({ + ...configuration, + permissions: { + ...configuration.permissions, + autoAcceptPermissions: true, + }, + }), }, ], }); @@ -146,7 +197,7 @@ describe('xctest-agent orchestration', () => { await controller.ensureStarted(); await controller.ensureStarted(); - expect(mocks.spawn).toHaveBeenCalledTimes(3); + expect(mocks.spawn).toHaveBeenCalledTimes(2); expect(mocks.spawn).toHaveBeenLastCalledWith( 'xcodebuild', expect.arrayContaining([ @@ -157,12 +208,54 @@ describe('xctest-agent orchestration', () => { expect.objectContaining({ env: expect.objectContaining({ HARNESS_XCTEST_AGENT_MODE: 'test', + HARNESS_XCTEST_AGENT_PORT: '49152', }), }), ); + expect(createSimulatorXCTestAgentTransport).toHaveBeenCalledWith({ + port: 49152, + }); + expect(mocks.health).toHaveBeenCalledTimes(1); + expect(mocks.configurePermissions).toHaveBeenCalledWith({ + autoAcceptPermissions: true, + }); await controller.dispose(); + expect(mocks.kill).toHaveBeenCalledTimes(1); + expect(mocks.disposeClient).toHaveBeenCalledTimes(1); + }); + + it('selects the device transport for physical devices', async () => { + const controller = createXCTestAgentController({ + port: 49153, + target: { + kind: 'device', + id: 'device-555', + }, + }); + + await controller.ensureStarted(); + + expect(createDeviceXCTestAgentTransport).toHaveBeenCalledWith({ + deviceId: 'device-555', + port: 49153, + }); + }); + + it('kills the agent process during disposal', async () => { + const controller = createXCTestAgentController({ + port: 49154, + shutdownTimeoutMs: 1, + target: { + kind: 'simulator', + id: 'sim-timeout', + }, + }); + + await controller.ensureStarted(); + await controller.dispose(); + expect(mocks.kill).toHaveBeenCalledTimes(1); }); @@ -187,14 +280,40 @@ describe('xctest-agent orchestration', () => { await controller.prepare(); - expect(mocks.spawn).toHaveBeenCalledTimes(2); + expect(mocks.spawn).toHaveBeenCalledTimes(1); expect(mocks.spawn).toHaveBeenNthCalledWith( - 2, + 1, 'xcodebuild', expect.arrayContaining(['build-for-testing']), ); }); + it('fails fast when the checked-in xcode project is missing', async () => { + const projectPath = path.join(projectRoot, 'HarnessXCTestAgent.xcodeproj'); + const hiddenProjectPath = path.join( + projectRoot, + 'HarnessXCTestAgent.xcodeproj.test-hidden', + ); + + fs.renameSync(projectPath, hiddenProjectPath); + + try { + const controller = createXCTestAgentController({ + target: { + kind: 'simulator', + id: 'sim-404', + }, + }); + + await expect(controller.prepare()).rejects.toThrow( + 'Missing checked-in XCTest agent project', + ); + expect(mocks.spawn).not.toHaveBeenCalled(); + } finally { + fs.renameSync(hiddenProjectPath, projectPath); + } + }); + it('skips killing the agent process when dispose is called before startup', async () => { const controller = createXCTestAgentController({ target: { @@ -234,11 +353,7 @@ const getInputFiles = (root: string): string[] => { const files: string[] = []; for (const entry of entries) { - if ( - entry.name === 'build' || - entry.name.endsWith('.xcodeproj') || - entry.name === '.gitignore' - ) { + if (entry.name === 'build' || entry.name === '.gitignore') { continue; } diff --git a/packages/platform-ios/src/appium-ios-device.d.ts b/packages/platform-ios/src/appium-ios-device.d.ts new file mode 100644 index 0000000..11c1583 --- /dev/null +++ b/packages/platform-ios/src/appium-ios-device.d.ts @@ -0,0 +1,7 @@ +declare module 'appium-ios-device' { + import type net from 'node:net'; + + export const utilities: { + connectPort: (udid: string, port: number) => Promise; + }; +} diff --git a/packages/platform-ios/src/instance.ts b/packages/platform-ios/src/instance.ts index c645478..fb237f3 100644 --- a/packages/platform-ios/src/instance.ts +++ b/packages/platform-ios/src/instance.ts @@ -123,6 +123,7 @@ export const getAppleSimulatorPlatformInstance = async ( ); const xctestAgent = createXCTestAgentController({ + appBundleId: config.bundleId, target: { kind: 'simulator', id: udid, @@ -130,12 +131,10 @@ export const getAppleSimulatorPlatformInstance = async ( capabilities: [createPermissionPromptAutoAcceptCapability()], }); + await xctestAgent.ensureStarted(); + return { - prepareRun: async () => { - await xctestAgent.prepare(); - }, startApp: async (options) => { - await xctestAgent.ensureStarted(); await simctl.startApp( udid, config.bundleId, @@ -144,7 +143,6 @@ export const getAppleSimulatorPlatformInstance = async ( ); }, restartApp: async (options) => { - await xctestAgent.ensureStarted(); await simctl.stopApp(udid, config.bundleId); await simctl.startApp( udid, @@ -156,10 +154,8 @@ export const getAppleSimulatorPlatformInstance = async ( stopApp: async () => { await simctl.stopApp(udid, config.bundleId); }, - disposeRun: async () => { - await xctestAgent.dispose(); - }, dispose: async () => { + await xctestAgent.dispose(); await simctl.stopApp(udid, config.bundleId); await simctl.clearHarnessJsLocationOverride(udid, config.bundleId); @@ -216,6 +212,7 @@ export const getApplePhysicalDevicePlatformInstance = async ( } const xctestAgent = createXCTestAgentController({ + appBundleId: config.bundleId, target: { kind: 'device', id: deviceId, @@ -223,12 +220,10 @@ export const getApplePhysicalDevicePlatformInstance = async ( capabilities: [createPermissionPromptAutoAcceptCapability()], }); + await xctestAgent.ensureStarted(); + return { - prepareRun: async () => { - await xctestAgent.prepare(); - }, startApp: async (options) => { - await xctestAgent.ensureStarted(); await devicectl.startApp( deviceId, config.bundleId, @@ -237,7 +232,6 @@ export const getApplePhysicalDevicePlatformInstance = async ( ); }, restartApp: async (options) => { - await xctestAgent.ensureStarted(); await devicectl.stopApp(deviceId, config.bundleId); await devicectl.startApp( deviceId, @@ -249,10 +243,8 @@ export const getApplePhysicalDevicePlatformInstance = async ( stopApp: async () => { await devicectl.stopApp(deviceId, config.bundleId); }, - disposeRun: async () => { - await xctestAgent.dispose(); - }, dispose: async () => { + await xctestAgent.dispose(); await devicectl.stopApp(deviceId, config.bundleId); }, isAppRunning: async () => { diff --git a/packages/platform-ios/src/xctest-agent-capabilities.ts b/packages/platform-ios/src/xctest-agent-capabilities.ts index 320d26e..38e270c 100644 --- a/packages/platform-ios/src/xctest-agent-capabilities.ts +++ b/packages/platform-ios/src/xctest-agent-capabilities.ts @@ -9,5 +9,12 @@ export const createPermissionPromptAutoAcceptCapability = getLaunchEnvironment: () => ({ [ENABLE_PERMISSION_PROMPT_AUTO_ACCEPT]: '1', }), + updateConfiguration: (configuration) => ({ + ...configuration, + permissions: { + ...configuration.permissions, + autoAcceptPermissions: true, + }, + }), }; }; diff --git a/packages/platform-ios/src/xctest-agent-client.ts b/packages/platform-ios/src/xctest-agent-client.ts new file mode 100644 index 0000000..93f28b5 --- /dev/null +++ b/packages/platform-ios/src/xctest-agent-client.ts @@ -0,0 +1,97 @@ +import type { + XCTestAgentTransport, + XCTestAgentTransportResponse, +} from './xctest-agent-transport.js'; + +export type XCTestAgentPermissionsConfiguration = { + autoAcceptPermissions: boolean; +}; + +type XCTestAgentHealthResponse = { + permissions: XCTestAgentPermissionsConfiguration; + status: 'ok'; +}; + +type XCTestAgentPermissionsResponse = { + permissions: XCTestAgentPermissionsConfiguration; +}; + +export type XCTestAgentClient = { + configurePermissions: ( + permissions: XCTestAgentPermissionsConfiguration, + ) => Promise; + dispose: () => Promise; + getPermissionsConfig: () => Promise; + health: () => Promise; +}; + +export const createXCTestAgentClient = ( + transport: XCTestAgentTransport, +): XCTestAgentClient => { + const requestJson = async (options: { + body?: unknown; + method: 'GET' | 'POST'; + path: string; + }): Promise => { + const response = await transport.request({ + method: options.method, + path: options.path, + body: + options.body === undefined ? undefined : JSON.stringify(options.body), + }); + + return parseJsonResponse(response, `${options.method} ${options.path}`); + }; + + return { + health: () => { + return requestJson({ + method: 'GET', + path: '/health', + }); + }, + configurePermissions: async (permissions) => { + const response = await requestJson({ + method: 'POST', + path: '/permissions/configure', + body: permissions, + }); + + return response.permissions; + }, + getPermissionsConfig: async () => { + const response = await requestJson({ + method: 'GET', + path: '/permissions', + }); + + return response.permissions; + }, + dispose: async () => { + await transport.dispose(); + }, + }; +}; + +const parseJsonResponse = ( + response: XCTestAgentTransportResponse, + operation: string, +): T => { + if (response.statusCode < 200 || response.statusCode >= 300) { + throw new Error( + `XCTest agent ${operation} failed with status ${response.statusCode}: ${response.body}`, + ); + } + + try { + return JSON.parse(response.body) as T; + } catch (error) { + throw new Error( + `XCTest agent ${operation} returned invalid JSON: ${getErrorMessage(error)}`, + ); + } +}; + +const getErrorMessage = (error: unknown): string => { + return error instanceof Error ? error.message : String(error); +}; diff --git a/packages/platform-ios/src/xctest-agent-transport-device.ts b/packages/platform-ios/src/xctest-agent-transport-device.ts new file mode 100644 index 0000000..92d588c --- /dev/null +++ b/packages/platform-ios/src/xctest-agent-transport-device.ts @@ -0,0 +1,150 @@ +import { utilities } from 'appium-ios-device'; +import type net from 'node:net'; +import type { + XCTestAgentTransport, + XCTestAgentTransportRequest, + XCTestAgentTransportResponse, +} from './xctest-agent-transport.js'; + +export const createDeviceXCTestAgentTransport = (options: { + deviceId: string; + port: number; + timeoutMs?: number; +}): XCTestAgentTransport => { + const timeoutMs = options.timeoutMs ?? 5000; + + return { + request: async ( + request: XCTestAgentTransportRequest, + ): Promise => { + const socket = (await utilities.connectPort( + options.deviceId, + options.port, + )) as net.Socket; + + return await performSocketRequest(socket, request, timeoutMs); + }, + dispose: async () => undefined, + }; +}; + +const performSocketRequest = async ( + socket: net.Socket, + request: XCTestAgentTransportRequest, + timeoutMs: number, +): Promise => { + return await new Promise((resolve, reject) => { + let settled = false; + const chunks: Buffer[] = []; + + const finish = (callback: () => void) => { + if (settled) { + return; + } + + settled = true; + socket.removeAllListeners(); + socket.destroy(); + callback(); + }; + + socket.setTimeout(timeoutMs, () => { + finish(() => { + reject( + new Error( + `Timed out waiting for XCTest agent response after ${timeoutMs}ms`, + ), + ); + }); + }); + + socket.on('data', (chunk: Buffer) => { + chunks.push(chunk); + }); + + socket.on('end', () => { + finish(() => { + try { + resolve(parseHttpResponse(Buffer.concat(chunks).toString('utf8'))); + } catch (error) { + reject(error); + } + }); + }); + + socket.on('error', (error) => { + finish(() => { + reject(error); + }); + }); + + socket.write(serializeHttpRequest(request)); + socket.end(); + }); +}; + +const serializeHttpRequest = (request: XCTestAgentTransportRequest): string => { + const body = request.body ?? ''; + const bodyLength = Buffer.byteLength(body, 'utf8'); + const headers = [ + `Host: localhost`, + 'Connection: close', + 'Accept: application/json', + ]; + + if (request.body !== undefined) { + headers.push('Content-Type: application/json'); + headers.push(`Content-Length: ${bodyLength}`); + } + + return [ + `${request.method} ${request.path} HTTP/1.1`, + ...headers, + '', + body, + ].join('\r\n'); +}; + +const parseHttpResponse = ( + responseText: string, +): XCTestAgentTransportResponse => { + const separatorIndex = responseText.indexOf('\r\n\r\n'); + + if (separatorIndex === -1) { + throw new Error(`Invalid XCTest agent HTTP response: ${responseText}`); + } + + const rawHeaders = responseText.slice(0, separatorIndex).split('\r\n'); + const statusLine = rawHeaders.shift(); + + if (!statusLine) { + throw new Error('Missing XCTest agent HTTP status line'); + } + + const [, rawStatusCode] = statusLine.split(' '); + const statusCode = Number(rawStatusCode); + + if (!Number.isFinite(statusCode)) { + throw new Error(`Invalid XCTest agent HTTP status code: ${statusLine}`); + } + + const headers: Record = {}; + + for (const header of rawHeaders) { + const separator = header.indexOf(':'); + + if (separator === -1) { + continue; + } + + const name = header.slice(0, separator).trim().toLowerCase(); + const value = header.slice(separator + 1).trim(); + headers[name] = value; + } + + return { + statusCode, + headers, + body: responseText.slice(separatorIndex + 4), + }; +}; diff --git a/packages/platform-ios/src/xctest-agent-transport-simulator.ts b/packages/platform-ios/src/xctest-agent-transport-simulator.ts new file mode 100644 index 0000000..28be674 --- /dev/null +++ b/packages/platform-ios/src/xctest-agent-transport-simulator.ts @@ -0,0 +1,103 @@ +import http from 'node:http'; +import type { + XCTestAgentTransport, + XCTestAgentTransportRequest, + XCTestAgentTransportResponse, +} from './xctest-agent-transport.js'; + +export const createSimulatorXCTestAgentTransport = (options: { + host?: string; + port: number; +}): XCTestAgentTransport => { + const host = options.host ?? '127.0.0.1'; + const agent = new http.Agent({ keepAlive: false }); + + return { + request: async ( + request: XCTestAgentTransportRequest, + ): Promise => { + return await performHttpRequest({ + agent, + body: request.body, + host, + method: request.method, + path: request.path, + port: options.port, + }); + }, + dispose: async () => { + agent.destroy(); + }, + }; +}; + +const performHttpRequest = async (options: { + agent: http.Agent; + body?: string; + host: string; + method: 'GET' | 'POST'; + path: string; + port: number; +}): Promise => { + return await new Promise((resolve, reject) => { + const request = http.request( + { + agent: options.agent, + host: options.host, + method: options.method, + path: options.path, + port: options.port, + headers: { + ...(options.body === undefined + ? {} + : { + 'content-type': 'application/json', + 'content-length': Buffer.byteLength(options.body, 'utf8'), + }), + connection: 'close', + }, + }, + (response) => { + const chunks: Buffer[] = []; + + response.on('data', (chunk: Buffer | string) => { + chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk); + }); + + response.on('end', () => { + resolve({ + statusCode: response.statusCode ?? 0, + body: Buffer.concat(chunks).toString('utf8'), + headers: getResponseHeaders(response.headers), + }); + }); + + response.on('error', reject); + }, + ); + + request.on('error', reject); + + if (options.body !== undefined) { + request.write(options.body); + } + + request.end(); + }); +}; + +const getResponseHeaders = ( + headers: http.IncomingHttpHeaders, +): Record => { + const values: Record = {}; + + for (const [key, value] of Object.entries(headers)) { + if (value === undefined) { + continue; + } + + values[key] = Array.isArray(value) ? value.join(', ') : value; + } + + return values; +}; diff --git a/packages/platform-ios/src/xctest-agent-transport.ts b/packages/platform-ios/src/xctest-agent-transport.ts new file mode 100644 index 0000000..a0780b0 --- /dev/null +++ b/packages/platform-ios/src/xctest-agent-transport.ts @@ -0,0 +1,18 @@ +export type XCTestAgentTransportRequest = { + method: 'GET' | 'POST'; + path: string; + body?: string; +}; + +export type XCTestAgentTransportResponse = { + body: string; + headers: Record; + statusCode: number; +}; + +export type XCTestAgentTransport = { + dispose: () => Promise; + request: ( + request: XCTestAgentTransportRequest, + ) => Promise; +}; diff --git a/packages/platform-ios/src/xctest-agent.ts b/packages/platform-ios/src/xctest-agent.ts index a7b3be6..bdcc0f0 100644 --- a/packages/platform-ios/src/xctest-agent.ts +++ b/packages/platform-ios/src/xctest-agent.ts @@ -3,11 +3,27 @@ import fs from 'node:fs'; import { createHash } from 'node:crypto'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; +import { + createXCTestAgentClient, + type XCTestAgentPermissionsConfiguration, +} from './xctest-agent-client.js'; +import type { XCTestAgentTransport } from './xctest-agent-transport.js'; +import { createDeviceXCTestAgentTransport } from './xctest-agent-transport-device.js'; +import { createSimulatorXCTestAgentTransport } from './xctest-agent-transport-simulator.js'; const xctestAgentLogger = logger.child('ios-xctest-agent'); const XCTEST_AGENT_PROJECT_NAME = 'HarnessXCTestAgent'; const XCTEST_AGENT_SCHEME_NAME = 'HarnessXCTestAgent'; +const XCTEST_AGENT_DEFAULT_PORT = 49_200; +const XCTEST_AGENT_PORT_ENV = 'HARNESS_XCTEST_AGENT_PORT'; +const XCTEST_AGENT_TARGET_BUNDLE_ID_ENV = + 'HARNESS_XCTEST_AGENT_TARGET_BUNDLE_ID'; +const XCTEST_AGENT_STARTUP_TIMEOUT_MS = 30_000; +const XCTEST_AGENT_SHUTDOWN_TIMEOUT_MS = 5_000; +const XCTEST_AGENT_STARTUP_POLL_INTERVAL_MS = 250; +const HARNESS_DIRNAME = '.harness'; +const XCTEST_AGENT_BUILD_DIRNAME = 'xctest-agent'; type XCTestAgentTarget = | { @@ -21,6 +37,13 @@ type XCTestAgentTarget = export type XCTestAgentCapability = { getLaunchEnvironment?: () => Record; + updateConfiguration?: ( + configuration: XCTestAgentRuntimeConfiguration, + ) => XCTestAgentRuntimeConfiguration; +}; + +export type XCTestAgentRuntimeConfiguration = { + permissions: XCTestAgentPermissionsConfiguration; }; type XCTestAgentBuildManifest = { @@ -46,12 +69,20 @@ const getXCTestAgentProjectFilePath = (): string => { ); }; -const getXCTestAgentSpecPath = (): string => { - return path.join(getXCTestAgentProjectRoot(), 'project.yml'); +const assertXCTestAgentProjectExists = () => { + const projectFilePath = getXCTestAgentProjectFilePath(); + + if (fs.existsSync(projectFilePath)) { + return; + } + + throw new Error( + `Missing checked-in XCTest agent project at ${projectFilePath}. Include the checked-in project in the package artifact.`, + ); }; const getXCTestAgentBuildRoot = (): string => { - return path.join(getXCTestAgentProjectRoot(), 'build'); + return path.join(process.cwd(), HARNESS_DIRNAME, XCTEST_AGENT_BUILD_DIRNAME); }; const getXCTestAgentDerivedDataPath = (target: XCTestAgentTarget): string => { @@ -105,11 +136,7 @@ const getProjectInputFilePaths = (root: string): string[] => { const files: string[] = []; for (const entry of entries) { - if ( - entry.name === 'build' || - entry.name.endsWith('.xcodeproj') || - entry.name === '.gitignore' - ) { + if (entry.name === 'build' || entry.name === '.gitignore') { continue; } @@ -172,47 +199,143 @@ const createProcessStopper = async (process: Subprocess | null) => { } }; +const getDefaultRuntimeConfiguration = (): XCTestAgentRuntimeConfiguration => { + return { + permissions: { + autoAcceptPermissions: false, + }, + }; +}; + +const getRuntimeConfiguration = ( + capabilities: XCTestAgentCapability[], +): XCTestAgentRuntimeConfiguration => { + return capabilities.reduce((configuration, capability) => { + return capability.updateConfiguration?.(configuration) ?? configuration; + }, getDefaultRuntimeConfiguration()); +}; + +const delay = async (ms: number) => { + await new Promise((resolve) => setTimeout(resolve, ms)); +}; + +const waitForAgentReady = async (options: { + client: ReturnType; + startupTimeoutMs: number; +}) => { + const deadline = Date.now() + options.startupTimeoutMs; + let lastError: unknown = null; + + while (Date.now() < deadline) { + try { + await options.client.health(); + return; + } catch (error) { + lastError = error; + await delay(XCTEST_AGENT_STARTUP_POLL_INTERVAL_MS); + } + } + + throw new Error( + `Timed out waiting for XCTest agent readiness: ${getErrorMessage(lastError)}`, + ); +}; + +const waitForShutdown = async (options: { + processTask: Promise | null; + shutdownTimeoutMs: number; +}): Promise => { + if (!options.processTask) { + return; + } + + await Promise.race([ + options.processTask, + delay(options.shutdownTimeoutMs), + ]); +}; + +const getErrorMessage = (error: unknown): string => { + if (!error) { + return 'unknown error'; + } + + return error instanceof Error ? error.message : String(error); +}; + export const createXCTestAgentController = (options: { + appBundleId?: string; target: XCTestAgentTarget; capabilities?: XCTestAgentCapability[]; + port?: number; + shutdownTimeoutMs?: number; + startupTimeoutMs?: number; }): XCTestAgentController => { const { target } = options; const capabilities = options.capabilities ?? []; + const startupTimeoutMs = + options.startupTimeoutMs ?? XCTEST_AGENT_STARTUP_TIMEOUT_MS; + const shutdownTimeoutMs = + options.shutdownTimeoutMs ?? XCTEST_AGENT_SHUTDOWN_TIMEOUT_MS; let prepared = false; let agentProcess: Subprocess | null = null; + let agentClient: ReturnType | null = null; let processTask: Promise | null = null; const getLaunchEnvironment = (): Record => { return Object.assign( {}, + options.appBundleId + ? { + [XCTEST_AGENT_TARGET_BUNDLE_ID_ENV]: options.appBundleId, + } + : {}, ...capabilities.map( (capability) => capability.getLaunchEnvironment?.() ?? {}, ), ); }; + const getPort = async (): Promise => { + return options.port ?? XCTEST_AGENT_DEFAULT_PORT; + }; + + const createTransport = async (): Promise => { + const port = await getPort(); + + if (target.kind === 'simulator') { + return createSimulatorXCTestAgentTransport({ port }); + } + + return createDeviceXCTestAgentTransport({ + deviceId: target.id, + port, + }); + }; + const prepare = async () => { if (prepared) { return; } - const projectRoot = getXCTestAgentProjectRoot(); const buildInputsHash = getProjectInputsHash(); xctestAgentLogger.debug( - 'generating XCTest agent project for %s', + 'verifying checked-in XCTest agent project for %s', target.kind, ); - await spawn('xcodegen', [ - 'generate', - '--spec', - getXCTestAgentSpecPath(), - '--project', - projectRoot, - ]); + xctestAgentLogger.info( + 'Using checked-in XCTest agent project for %s target', + target.kind, + ); + assertXCTestAgentProjectExists(); if (shouldReuseBuildArtifacts(target, buildInputsHash)) { prepared = true; + xctestAgentLogger.info( + 'Reusing cached XCTest agent build for %s target', + target.kind, + ); xctestAgentLogger.debug( 'reusing cached XCTest agent build for %s', target.kind, @@ -223,6 +346,7 @@ export const createXCTestAgentController = (options: { fs.mkdirSync(getXCTestAgentBuildRoot(), { recursive: true }); xctestAgentLogger.debug('building XCTest agent for %s', target.kind); + xctestAgentLogger.info('Building XCTest agent for %s target', target.kind); await spawn('xcodebuild', [ 'build-for-testing', '-project', @@ -239,17 +363,25 @@ export const createXCTestAgentController = (options: { buildInputsHash, destinationKind: target.kind, }); + xctestAgentLogger.info('Built XCTest agent for %s target', target.kind); prepared = true; }; const ensureStarted = async () => { await prepare(); - if (agentProcess) { + if (agentProcess && agentClient) { return; } + const port = await getPort(); + const runtimeConfiguration = getRuntimeConfiguration(capabilities); + xctestAgentLogger.debug('starting XCTest agent for %s', target.kind); + xctestAgentLogger.info( + 'Starting XCTest agent session for %s target', + target.kind, + ); agentProcess = spawn( 'xcodebuild', [ @@ -260,6 +392,10 @@ export const createXCTestAgentController = (options: { XCTEST_AGENT_SCHEME_NAME, '-destination', getXCTestAgentDestination(target), + '-parallel-testing-enabled', + 'NO', + '-maximum-parallel-testing-workers', + '1', '-derivedDataPath', getXCTestAgentDerivedDataPath(target), ], @@ -267,6 +403,7 @@ export const createXCTestAgentController = (options: { cwd: getXCTestAgentProjectRoot(), env: { ...process.env, + [XCTEST_AGENT_PORT_ENV]: String(port), ...getLaunchEnvironment(), }, stdout: 'pipe', @@ -275,30 +412,65 @@ export const createXCTestAgentController = (options: { ); const currentProcess = agentProcess; + const transport = await createTransport(); + const client = createXCTestAgentClient(transport); + agentClient = client; processTask = (async () => { try { for await (const line of currentProcess) { - xctestAgentLogger.debug('[agent:%s] %s', target.kind, line); + xctestAgentLogger.info('[agent:%s] %s', target.kind, line); } } catch (error) { xctestAgentLogger.debug('XCTest agent process stopped', error); } finally { if (agentProcess === currentProcess) { agentProcess = null; + agentClient = null; processTask = null; } } })(); + + try { + await waitForAgentReady({ + client, + startupTimeoutMs, + }); + await client.configurePermissions(runtimeConfiguration.permissions); + } catch (error) { + xctestAgentLogger.warn( + 'XCTest agent startup failed for %s: %s', + target.kind, + getErrorMessage(error), + ); + await transport.dispose(); + agentClient = null; + await createProcessStopper(currentProcess); + await processTask; + throw error; + } }; const stop = async () => { const currentProcess = agentProcess; + const currentClient = agentClient; + const currentProcessTask = processTask; agentProcess = null; + agentClient = null; + processTask = null; + + xctestAgentLogger.info( + 'Stopping XCTest agent session for %s target', + target.kind, + ); await createProcessStopper(currentProcess); - await processTask; - processTask = null; + await waitForShutdown({ + processTask: currentProcessTask, + shutdownTimeoutMs, + }); + await currentClient?.dispose(); }; return { diff --git a/packages/platform-ios/xctest-agent/.gitignore b/packages/platform-ios/xctest-agent/.gitignore deleted file mode 100644 index 8bfc69d..0000000 --- a/packages/platform-ios/xctest-agent/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -HarnessXCTestAgent.xcodeproj/ -build/ diff --git a/packages/platform-ios/xctest-agent/HarnessXCTestAgent.xcodeproj/project.pbxproj b/packages/platform-ios/xctest-agent/HarnessXCTestAgent.xcodeproj/project.pbxproj new file mode 100644 index 0000000..85eec7d --- /dev/null +++ b/packages/platform-ios/xctest-agent/HarnessXCTestAgent.xcodeproj/project.pbxproj @@ -0,0 +1,455 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXContainerItemProxy section */ + 79A245FD2F99756A0071600E /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 79A245DD2F9975690071600E /* Project object */; + proxyType = 1; + remoteGlobalIDString = 79A245E42F9975690071600E; + remoteInfo = HarnessXCTestAgent; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 79A245E52F9975690071600E /* HarnessXCTestAgent.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = HarnessXCTestAgent.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 79A245FC2F99756A0071600E /* HarnessXCTestAgentUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = HarnessXCTestAgentUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 79A245E72F9975690071600E /* HarnessXCTestAgent */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = HarnessXCTestAgent; + sourceTree = ""; + }; + 79A245FF2F99756A0071600E /* HarnessXCTestAgentUITests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = HarnessXCTestAgentUITests; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 79A245E22F9975690071600E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 79A245F92F99756A0071600E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 79A245DC2F9975690071600E = { + isa = PBXGroup; + children = ( + 79A245E72F9975690071600E /* HarnessXCTestAgent */, + 79A245FF2F99756A0071600E /* HarnessXCTestAgentUITests */, + 79A245E62F9975690071600E /* Products */, + ); + sourceTree = ""; + }; + 79A245E62F9975690071600E /* Products */ = { + isa = PBXGroup; + children = ( + 79A245E52F9975690071600E /* HarnessXCTestAgent.app */, + 79A245FC2F99756A0071600E /* HarnessXCTestAgentUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 79A245E42F9975690071600E /* HarnessXCTestAgent */ = { + isa = PBXNativeTarget; + buildConfigurationList = 79A246062F99756A0071600E /* Build configuration list for PBXNativeTarget "HarnessXCTestAgent" */; + buildPhases = ( + 79A245E12F9975690071600E /* Sources */, + 79A245E22F9975690071600E /* Frameworks */, + 79A245E32F9975690071600E /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 79A245E72F9975690071600E /* HarnessXCTestAgent */, + ); + name = HarnessXCTestAgent; + packageProductDependencies = ( + ); + productName = HarnessXCTestAgent; + productReference = 79A245E52F9975690071600E /* HarnessXCTestAgent.app */; + productType = "com.apple.product-type.application"; + }; + 79A245FB2F99756A0071600E /* HarnessXCTestAgentUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 79A2460C2F99756A0071600E /* Build configuration list for PBXNativeTarget "HarnessXCTestAgentUITests" */; + buildPhases = ( + 79A245F82F99756A0071600E /* Sources */, + 79A245F92F99756A0071600E /* Frameworks */, + 79A245FA2F99756A0071600E /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 79A245FE2F99756A0071600E /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 79A245FF2F99756A0071600E /* HarnessXCTestAgentUITests */, + ); + name = HarnessXCTestAgentUITests; + packageProductDependencies = ( + ); + productName = HarnessXCTestAgentUITests; + productReference = 79A245FC2F99756A0071600E /* HarnessXCTestAgentUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 79A245DD2F9975690071600E /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 2620; + LastUpgradeCheck = 2620; + TargetAttributes = { + 79A245E42F9975690071600E = { + CreatedOnToolsVersion = 26.2; + }; + 79A245FB2F99756A0071600E = { + CreatedOnToolsVersion = 26.2; + TestTargetID = 79A245E42F9975690071600E; + }; + }; + }; + buildConfigurationList = 79A245E02F9975690071600E /* Build configuration list for PBXProject "HarnessXCTestAgent" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 79A245DC2F9975690071600E; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = 79A245E62F9975690071600E /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 79A245E42F9975690071600E /* HarnessXCTestAgent */, + 79A245FB2F99756A0071600E /* HarnessXCTestAgentUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 79A245E32F9975690071600E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 79A245FA2F99756A0071600E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 79A245E12F9975690071600E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 79A245F82F99756A0071600E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 79A245FE2F99756A0071600E /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 79A245E42F9975690071600E /* HarnessXCTestAgent */; + targetProxy = 79A245FD2F99756A0071600E /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 79A246042F99756A0071600E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.2; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 79A246052F99756A0071600E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.2; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 79A246072F99756A0071600E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.callstackincubator.HarnessXCTestAgent; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 79A246082F99756A0071600E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.callstackincubator.HarnessXCTestAgent; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 79A2460D2F99756A0071600E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.callstackincubator.HarnessXCTestAgentUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = HarnessXCTestAgent; + }; + name = Debug; + }; + 79A2460E2F99756A0071600E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.callstackincubator.HarnessXCTestAgentUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = HarnessXCTestAgent; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 79A245E02F9975690071600E /* Build configuration list for PBXProject "HarnessXCTestAgent" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 79A246042F99756A0071600E /* Debug */, + 79A246052F99756A0071600E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 79A246062F99756A0071600E /* Build configuration list for PBXNativeTarget "HarnessXCTestAgent" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 79A246072F99756A0071600E /* Debug */, + 79A246082F99756A0071600E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 79A2460C2F99756A0071600E /* Build configuration list for PBXNativeTarget "HarnessXCTestAgentUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 79A2460D2F99756A0071600E /* Debug */, + 79A2460E2F99756A0071600E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 79A245DD2F9975690071600E /* Project object */; +} diff --git a/packages/platform-ios/xctest-agent/HarnessXCTestAgent.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/platform-ios/xctest-agent/HarnessXCTestAgent.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/packages/platform-ios/xctest-agent/HarnessXCTestAgent.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/platform-ios/xctest-agent/HarnessXCTestAgent/Assets.xcassets/AccentColor.colorset/Contents.json b/packages/platform-ios/xctest-agent/HarnessXCTestAgent/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/packages/platform-ios/xctest-agent/HarnessXCTestAgent/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/platform-ios/xctest-agent/HarnessXCTestAgent/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/platform-ios/xctest-agent/HarnessXCTestAgent/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..2305880 --- /dev/null +++ b/packages/platform-ios/xctest-agent/HarnessXCTestAgent/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/platform-ios/xctest-agent/HarnessXCTestAgent/Assets.xcassets/Contents.json b/packages/platform-ios/xctest-agent/HarnessXCTestAgent/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/packages/platform-ios/xctest-agent/HarnessXCTestAgent/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/platform-ios/xctest-agent/HarnessXCTestAgent/Assets.xcassets/Logo.imageset/Contents.json b/packages/platform-ios/xctest-agent/HarnessXCTestAgent/Assets.xcassets/Logo.imageset/Contents.json new file mode 100644 index 0000000..ccc5869 --- /dev/null +++ b/packages/platform-ios/xctest-agent/HarnessXCTestAgent/Assets.xcassets/Logo.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "logo.jpg", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/platform-ios/xctest-agent/HarnessXCTestAgent/Assets.xcassets/Logo.imageset/logo.jpg b/packages/platform-ios/xctest-agent/HarnessXCTestAgent/Assets.xcassets/Logo.imageset/logo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..fbdfc3f1daca6b9571602dec984b5a3f9e361fb7 GIT binary patch literal 107504 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 z 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

5LRMdWdYtg6B4Y5s+UYf+sj7mzF<0 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 b9STyse&*r%ZeP21;k<7I@KF6b^?(1_G%aEc literal 0 HcmV?d00001 diff --git a/packages/platform-ios/xctest-agent/HarnessXCTestAgent/Assets.xcassets/PoweredBy.imageset/Contents.json b/packages/platform-ios/xctest-agent/HarnessXCTestAgent/Assets.xcassets/PoweredBy.imageset/Contents.json new file mode 100644 index 0000000..35b43e9 --- /dev/null +++ b/packages/platform-ios/xctest-agent/HarnessXCTestAgent/Assets.xcassets/PoweredBy.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "powered-by.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/platform-ios/xctest-agent/HarnessXCTestAgent/Assets.xcassets/PoweredBy.imageset/powered-by.png b/packages/platform-ios/xctest-agent/HarnessXCTestAgent/Assets.xcassets/PoweredBy.imageset/powered-by.png new file mode 100644 index 0000000000000000000000000000000000000000..5377d689d084bbc6c97d40f4b0a9965dc57b92ad GIT binary patch literal 6006 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 PermissionPromptCapability? { - guard ProcessInfo.processInfo.environment[Environment.autoAcceptPermissions] == "1" else { - return nil - } - - return PermissionPromptCapability() - } - - func setUp() throws { - addUIInterruptionMonitor(withDescription: "Harness permission prompt handler") { [weak self] alert in - guard let self else { - return false - } - - return self.tapPositiveAction(in: alert) - } - } - - func tick() throws { - _ = tapPositiveAction(in: springboard.alerts.firstMatch) - _ = tapPositiveAction(in: springboard.sheets.firstMatch) - hostApplication.activate() - } - - private func tapPositiveAction(in element: XCUIElement) -> Bool { - guard element.exists else { - return false - } - - for label in Constants.knownPositiveButtonLabels { - let button = element.buttons[label] - - if button.exists { - button.tap() - return true - } - } - - return false - } -} diff --git a/packages/platform-ios/xctest-agent/HarnessXCTestAgentTests/AgentCapability.swift b/packages/platform-ios/xctest-agent/HarnessXCTestAgentUITests/AgentCapability.swift similarity index 100% rename from packages/platform-ios/xctest-agent/HarnessXCTestAgentTests/AgentCapability.swift rename to packages/platform-ios/xctest-agent/HarnessXCTestAgentUITests/AgentCapability.swift diff --git a/packages/platform-ios/xctest-agent/HarnessXCTestAgentUITests/HarnessXCTestAgentUITests.swift b/packages/platform-ios/xctest-agent/HarnessXCTestAgentUITests/HarnessXCTestAgentUITests.swift new file mode 100644 index 0000000..ea45440 --- /dev/null +++ b/packages/platform-ios/xctest-agent/HarnessXCTestAgentUITests/HarnessXCTestAgentUITests.swift @@ -0,0 +1,333 @@ +import XCTest +import Network + +final class HarnessXCTestAgentState { + private let lock = NSLock() + private var _permissions: PermissionPromptConfiguration + private var _isShutdownRequested = false + + init(permissions: PermissionPromptConfiguration) { + _permissions = permissions + } + + var permissions: PermissionPromptConfiguration { + lock.lock() + defer { lock.unlock() } + return _permissions + } + + var isShutdownRequested: Bool { + lock.lock() + defer { lock.unlock() } + return _isShutdownRequested + } + + func updatePermissions(_ permissions: PermissionPromptConfiguration) { + lock.lock() + _permissions = permissions + lock.unlock() + } + + func requestShutdown() { + lock.lock() + _isShutdownRequested = true + lock.unlock() + } +} + +private struct XCTestAgentHealthResponse: Codable { + let permissions: PermissionPromptConfiguration + let status: String +} + +private struct XCTestAgentPermissionsResponse: Codable { + let permissions: PermissionPromptConfiguration +} + +private struct XCTestAgentShutdownResponse: Codable { + let accepted: Bool +} + +private struct XCTestAgentRequest { + let body: Data + let method: String + let path: String +} + +private struct XCTestAgentResponse { + let body: Data + let statusCode: Int +} + +private final class XCTestAgentHTTPServer { + private let encoder = JSONEncoder() + private let handler: (XCTestAgentRequest) -> XCTestAgentResponse + private let listener: NWListener + private let queue = DispatchQueue(label: "dev.reactnativeharness.xctest-agent.http") + + init(port: UInt16, handler: @escaping (XCTestAgentRequest) -> XCTestAgentResponse) throws { + guard let listenerPort = NWEndpoint.Port(rawValue: port) else { + throw NSError(domain: "HarnessXCTestAgent", code: 1, userInfo: [ + NSLocalizedDescriptionKey: "Invalid XCTest agent port \(port)" + ]) + } + + self.listener = try NWListener(using: .tcp, on: listenerPort) + self.handler = handler + } + + func start(log: @escaping (String) -> Void) { + listener.newConnectionHandler = { [weak self] connection in + self?.handle(connection: connection, log: log) + } + listener.stateUpdateHandler = { state in + log("HTTP listener state: \(String(describing: state))") + } + listener.start(queue: queue) + } + + func stop() { + listener.cancel() + } + + private func handle(connection: NWConnection, log: @escaping (String) -> Void) { + connection.start(queue: queue) + receive(on: connection, buffer: Data(), log: log) + } + + private func receive(on connection: NWConnection, buffer: Data, log: @escaping (String) -> Void) { + connection.receive(minimumIncompleteLength: 1, maximumLength: 64 * 1024) { + [weak self] data, _, isComplete, error in + guard let self else { + connection.cancel() + return + } + + if let error { + log("HTTP receive failed: \(error.localizedDescription)") + connection.cancel() + return + } + + var nextBuffer = buffer + if let data { + nextBuffer.append(data) + } + + if let request = self.parseRequest(from: nextBuffer) { + let response = self.handler(request) + self.send(response: response, on: connection, log: log) + return + } + + if isComplete { + connection.cancel() + return + } + + self.receive(on: connection, buffer: nextBuffer, log: log) + } + } + + private func parseRequest(from data: Data) -> XCTestAgentRequest? { + guard let headerRange = data.range(of: Data("\r\n\r\n".utf8)) else { + return nil + } + + let headerData = data[..= 2 else { + return nil + } + + let contentLength = headerLines.dropFirst().reduce(0) { partialResult, line in + let parts = line.split(separator: ":", maxSplits: 1).map(String.init) + guard parts.count == 2 else { + return partialResult + } + + return parts[0].trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == "content-length" + ? (Int(parts[1].trimmingCharacters(in: .whitespacesAndNewlines)) ?? 0) + : partialResult + } + + let bodyStart = headerRange.upperBound + let bodyEnd = data.index(bodyStart, offsetBy: contentLength, limitedBy: data.endIndex) + + guard let bodyEnd else { + return nil + } + + return XCTestAgentRequest( + body: data[bodyStart.. Void) { + let statusText = response.statusCode == 200 ? "OK" : "Error" + let header = "HTTP/1.1 \(response.statusCode) \(statusText)\r\nContent-Type: application/json\r\nConnection: close\r\nContent-Length: \(response.body.count)\r\n\r\n" + let payload = Data(header.utf8) + response.body + + connection.send(content: payload, contentContext: .defaultMessage, isComplete: true, completion: .contentProcessed { error in + if let error { + log("HTTP send failed: \(error.localizedDescription)") + } + + connection.cancel() + }) + } + + func encode(_ value: T) -> Data { + return (try? encoder.encode(value)) ?? Data("{}".utf8) + } +} + +final class HarnessXCTestAgentUITests: XCTestCase { + private enum Environment { + static let targetBundleIdentifier = "HARNESS_XCTEST_AGENT_TARGET_BUNDLE_ID" + } + + private enum Constants { + static let defaultSessionDuration: TimeInterval = 60 * 60 + static let tickInterval: TimeInterval = 1 + } + + private let state = HarnessXCTestAgentState( + permissions: PermissionPromptConfiguration.fromEnvironment() + ) + private lazy var targetApplication = makeTargetApplication() + private let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard") + private var capabilities: [AgentCapability] = [] + private var httpServer: XCTestAgentHTTPServer? + + private func log(_ message: String) { + NSLog("[HarnessXCTestAgent] %@", message) + } + + private func makeTargetApplication() -> XCUIApplication { + if let bundleIdentifier = ProcessInfo.processInfo.environment[Environment.targetBundleIdentifier], !bundleIdentifier.isEmpty { + return XCUIApplication(bundleIdentifier: bundleIdentifier) + } + + return XCUIApplication() + } + + private func jsonResponse(_ value: T) -> XCTestAgentResponse { + guard let httpServer else { + return XCTestAgentResponse(body: Data("{}".utf8), statusCode: 500) + } + + return XCTestAgentResponse(body: httpServer.encode(value), statusCode: 200) + } + + private func handleRequest(_ request: XCTestAgentRequest) -> XCTestAgentResponse { + switch (request.method, request.path) { + case ("GET", "/health"): + return jsonResponse( + XCTestAgentHealthResponse( + permissions: state.permissions, + status: "ok" + ) + ) + case ("POST", "/permissions/configure"): + guard let configuration = try? JSONDecoder().decode( + PermissionPromptConfiguration.self, + from: request.body + ) else { + return XCTestAgentResponse(body: Data("{\"error\":\"invalid configuration\"}".utf8), statusCode: 400) + } + + state.updatePermissions(configuration) + return jsonResponse(XCTestAgentPermissionsResponse(permissions: state.permissions)) + case ("GET", "/permissions"): + return jsonResponse(XCTestAgentPermissionsResponse(permissions: state.permissions)) + case ("POST", "/shutdown"): + state.requestShutdown() + return jsonResponse(XCTestAgentShutdownResponse(accepted: true)) + default: + return XCTestAgentResponse(body: Data("{\"error\":\"not found\"}".utf8), statusCode: 404) + } + } + + private func startHTTPServer() throws { + let port = UInt16(ProcessInfo.processInfo.environment["HARNESS_XCTEST_AGENT_PORT"] ?? "49200") ?? 49200 + httpServer = try XCTestAgentHTTPServer(port: port) { [weak self] request in + guard let self else { + return XCTestAgentResponse(body: Data("{}".utf8), statusCode: 500) + } + + return handleRequest(request) + } + httpServer?.start(log: log) + log("HTTP server started on port \(port)") + } + + override func setUpWithError() throws { + continueAfterFailure = false + capabilities = [ + PermissionPromptCapability( + state: state, + application: targetApplication, + springboard: springboard + ) + ] + + log("setUpWithError started") + log("enabled capabilities: \(capabilities.map { String(describing: type(of: $0)) }.joined(separator: ", "))") + + targetApplication.launch() + + for capability in capabilities { + if let permissionPromptCapability = capability as? PermissionPromptCapability { + addUIInterruptionMonitor(withDescription: "Harness permission prompt handler") { alert in + permissionPromptCapability.logInterruption(alert.label) + return permissionPromptCapability.handleInterruption(alert) + } + } + } + + for capability in capabilities { + try capability.setUp() + } + + try startHTTPServer() + + log("setUpWithError completed") + } + + override func tearDown() { + httpServer?.stop() + httpServer = nil + super.tearDown() + } + + @MainActor + func testAgentSession() { + log("testAgentSession started") + + let sessionDeadline = Date().addingTimeInterval(Constants.defaultSessionDuration) + + while Date() < sessionDeadline && !state.isShutdownRequested { + for capability in capabilities { + try? capability.tick() + } + + RunLoop.current.run( + until: Date().addingTimeInterval(Constants.tickInterval) + ) + } + + log("testAgentSession completed") + } +} diff --git a/packages/platform-ios/xctest-agent/HarnessXCTestAgentUITests/PermissionPromptCapability.swift b/packages/platform-ios/xctest-agent/HarnessXCTestAgentUITests/PermissionPromptCapability.swift new file mode 100644 index 0000000..d29e891 --- /dev/null +++ b/packages/platform-ios/xctest-agent/HarnessXCTestAgentUITests/PermissionPromptCapability.swift @@ -0,0 +1,105 @@ +import XCTest + +private enum PermissionPromptEnvironment { + static let autoAcceptPermissions = "HARNESS_XCTEST_AGENT_AUTO_ACCEPT_PERMISSIONS" +} + +struct PermissionPromptConfiguration: Codable { + var autoAcceptPermissions: Bool + + static func fromEnvironment() -> PermissionPromptConfiguration { + return PermissionPromptConfiguration( + autoAcceptPermissions: ProcessInfo.processInfo.environment[PermissionPromptEnvironment.autoAcceptPermissions] == "1" + ) + } +} + +final class PermissionPromptCapability: AgentCapability { + private enum Constants { + static let knownPositiveButtonLabels = [ + "Allow", + "OK", + "Continue", + "Next", + "While Using App", + "While Using the App", + "Always Allow", + "Allow Once", + "Join", + "Pair", + "Allow Full Access" + ] + } + + private let application: XCUIApplication + private let springboard: XCUIApplication + private let state: HarnessXCTestAgentState + + private func log(_ message: String) { + NSLog("[HarnessXCTestAgent][PermissionPromptCapability] %@", message) + } + + init( + state: HarnessXCTestAgentState, + application: XCUIApplication, + springboard: XCUIApplication + ) { + self.state = state + self.application = application + self.springboard = springboard + } + + func setUp() throws { + if state.permissions.autoAcceptPermissions { + log("permission prompt capability enabled") + } + } + + func logInterruption(_ alertLabel: String) { + log("interruption monitor received alert: \(alertLabel)") + } + + func handleInterruption(_ alert: XCUIElement) -> Bool { + guard state.permissions.autoAcceptPermissions else { + return false + } + + return tapPositiveAction(in: alert) + } + + func tick() throws { + guard state.permissions.autoAcceptPermissions else { + return + } + + let handledAppAlert = tapPositiveAction(in: application.alerts.firstMatch) + let handledAppSheet = tapPositiveAction(in: application.sheets.firstMatch) + let handledSpringboardAlert = tapPositiveAction(in: springboard.alerts.firstMatch) + let handledSpringboardSheet = tapPositiveAction(in: springboard.sheets.firstMatch) + + if handledAppAlert || handledAppSheet || handledSpringboardAlert || handledSpringboardSheet { + log("handled permission prompt during tick") + } + } + + private func tapPositiveAction(in element: XCUIElement) -> Bool { + guard element.exists else { + return false + } + + for label in Constants.knownPositiveButtonLabels { + let button = element.buttons[label] + + if button.exists { + log("tapping button: \(label)") + button.tap() + return true + } + } + + let visibleButtons = element.buttons.allElementsBoundByIndex.map(\.label) + log("prompt found but no matching positive action. buttons=\(visibleButtons)") + + return false + } +} diff --git a/packages/platform-ios/xctest-agent/README.md b/packages/platform-ios/xctest-agent/README.md deleted file mode 100644 index 36e4903..0000000 --- a/packages/platform-ios/xctest-agent/README.md +++ /dev/null @@ -1,74 +0,0 @@ -# Harness XCTest Agent - -Internal XcodeGen-backed project used by `@react-native-harness/platform-apple` to build a reusable XCTest agent. - -## Generate The Project - -From the repo root: - -```bash -pnpm --filter @react-native-harness/platform-apple run xctest-agent:generate -``` - -The generated project is intentionally not committed. The source of truth is `xctest-agent/project.yml` plus the files referenced from it. - -## Project Shape - -- `HarnessXCTestAgentHost`: minimal iOS host app target used to package and run the agent on simulator and physical-device destinations -- `HarnessXCTestAgentTests`: UI-testing bundle where agent capabilities live -- `HarnessXCTestAgent` scheme: stable scheme name for future host-side orchestration - -## Current Capability - -- Best-effort permission prompt auto-accept for recognized positive actions -- Unknown prompts are ignored silently so the generic agent can coexist with future capabilities - -## Operations - -Validate the generated scheme and targets: - -```bash -xcodebuild -project "packages/platform-ios/xctest-agent/HarnessXCTestAgent.xcodeproj" -list -``` - -Run the host-side validation and hardening tests from `packages/platform-ios`: - -```bash -pnpm vitest run src/__tests__/xctest-agent-capabilities.test.ts src/__tests__/xctest-agent.test.ts src/__tests__/instance-xctest-agent.test.ts src/__tests__/instance.test.ts -``` - -Build and cache behavior: - -- The project is regenerated from `xctest-agent/project.yml` during prepare -- Build artifacts are cached under `packages/platform-ios/xctest-agent/build/` -- Simulator and physical-device builds use separate derived-data roots -- Cache reuse depends on the XcodeGen inputs hash matching the stored build manifest - -Manual validation checklist: - -1. Build an iOS app artifact and set `HARNESS_APP_PATH` when simulator installation is needed. -2. Run Harness against an iOS simulator and confirm the first run triggers XCTest agent generation and build. -3. Run Harness a second time on the same target and confirm cached artifacts are reused. -4. Trigger a real permission prompt, such as camera access, and confirm the positive action is tapped automatically. -5. Confirm Harness teardown does not leave a stuck `xcodebuild` XCTest agent process behind. -6. Repeat the same flow on a connected physical iOS device with the required signing inputs available. - -## Build Assumptions - -- `xcodegen` is available on the host machine -- Xcode and the iOS platform SDKs are installed -- Simulator builds can use the generated project without additional signing configuration -- Physical-device builds require signing inputs, such as `DEVELOPMENT_TEAM`, to be provided by the caller at build time -- The project stays generic so additional XCTest-driven behaviors can be added without renaming targets or schemes - -## Cache Inputs - -When build artifact caching is added, these files should be treated as the primary cache inputs for project generation and XCTest agent builds: - -- `packages/platform-ios/xctest-agent/project.yml` -- `packages/platform-ios/xctest-agent/HarnessXCTestAgentHost/AgentHostApp.swift` -- `packages/platform-ios/xctest-agent/HarnessXCTestAgentTests/HarnessXCTestAgentTests.swift` -- `packages/platform-ios/xctest-agent/HarnessXCTestAgentTests/AgentCapability.swift` -- `packages/platform-ios/xctest-agent/HarnessXCTestAgentTests/PermissionPromptCapability.swift` - -The selected Xcode version and any injected signing settings should also be part of higher-level cache keys because they affect the produced artifacts. diff --git a/packages/platform-ios/xctest-agent/manual-run/xcodebuild.log b/packages/platform-ios/xctest-agent/manual-run/xcodebuild.log new file mode 100644 index 0000000..ff334a4 --- /dev/null +++ b/packages/platform-ios/xctest-agent/manual-run/xcodebuild.log @@ -0,0 +1,43 @@ +Command line invocation: + /Applications/Xcode.app/Contents/Developer/usr/bin/xcodebuild test-without-building -project HarnessXCTestAgent.xcodeproj -scheme HarnessXCTestAgent -destination "platform=iOS Simulator,id=CE6BE68F-A9BD-4A3A-8F57-37FEA8AC3FB8" -parallel-testing-enabled NO -maximum-parallel-testing-workers 1 -derivedDataPath build/simulator + +2026-04-22 18:02:23.825 xcodebuild[72153:11965075] [MT] IDERunDestination: Supported platforms for the buildables in the current scheme is empty. +2026-04-22 18:02:42.554208-0400 HarnessXCTestAgentUITests-Runner[72652:11967943] [Default] Running tests... + t = nans Interface orientation changed to Portrait +Test Suite 'All tests' started at 2026-04-22 18:02:42.755. +Test Suite 'HarnessXCTestAgentUITests.xctest' started at 2026-04-22 18:02:42.755. +Test Suite 'HarnessXCTestAgentUITests' started at 2026-04-22 18:02:42.755. +Test Case '-[HarnessXCTestAgentUITests.HarnessXCTestAgentUITests testAgentSession]' started. + t = 0.00s Start Test at 2026-04-22 18:02:42.756 + t = 0.13s Set Up +2026-04-22 18:02:42.891067-0400 HarnessXCTestAgentUITests-Runner[72652:11967943] [HarnessXCTestAgent] setUpWithError started +2026-04-22 18:02:42.896411-0400 HarnessXCTestAgentUITests-Runner[72652:11967943] [HarnessXCTestAgent] enabled capabilities: PermissionPromptCapability + t = 0.14s Open com.callstackincubator.HarnessXCTestAgent + t = 0.14s Launch com.callstackincubator.HarnessXCTestAgent + t = 0.95s Setting up automation session + t = 1.59s Wait for com.callstackincubator.HarnessXCTestAgent to idle +2026-04-22 18:02:45.495856-0400 HarnessXCTestAgentUITests-Runner[72652:11967943] [HarnessXCTestAgent] HTTP server started on port 49200 +2026-04-22 18:02:45.495989-0400 HarnessXCTestAgentUITests-Runner[72652:11967943] [HarnessXCTestAgent] setUpWithError completed +2026-04-22 18:02:45.496398-0400 HarnessXCTestAgentUITests-Runner[72652:11968142] [] nw_listener_socket_inbox_create_socket setsockopt SO_NECP_LISTENUUID failed [2: No such file or directory] +2026-04-22 18:02:45.496553-0400 HarnessXCTestAgentUITests-Runner[72652:11967943] [HarnessXCTestAgent] testAgentSession started +2026-04-22 18:02:45.502439-0400 HarnessXCTestAgentUITests-Runner[72652:11968142] [HarnessXCTestAgent] HTTP listener state: ready +2026-04-22 18:03:44.727750-0400 HarnessXCTestAgentUITests-Runner[72652:11967943] [HarnessXCTestAgent] testAgentSession completed + t = 61.97s Tear Down +2026-04-22 18:03:44.730653-0400 HarnessXCTestAgentUITests-Runner[72652:11968236] [HarnessXCTestAgent] HTTP listener state: cancelled +Test Case '-[HarnessXCTestAgentUITests.HarnessXCTestAgentUITests testAgentSession]' passed (62.334 seconds). +Test Suite 'HarnessXCTestAgentUITests' passed at 2026-04-22 18:03:45.091. + Executed 1 test, with 0 failures (0 unexpected) in 62.334 (62.335) seconds +Test Suite 'HarnessXCTestAgentUITests.xctest' passed at 2026-04-22 18:03:45.092. + Executed 1 test, with 0 failures (0 unexpected) in 62.334 (62.336) seconds +Test Suite 'All tests' passed at 2026-04-22 18:03:45.092. + Executed 1 test, with 0 failures (0 unexpected) in 62.334 (62.337) seconds +2026-04-22 18:03:45.424 xcodebuild[72153:11965075] [MT] IDETestOperationsObserverDebug: 81.245 elapsed -- Testing started completed. +2026-04-22 18:03:45.424 xcodebuild[72153:11965075] [MT] IDETestOperationsObserverDebug: 0.000 sec, +0.000 sec -- start +2026-04-22 18:03:45.424 xcodebuild[72153:11965075] [MT] IDETestOperationsObserverDebug: 81.245 sec, +81.245 sec -- end + +Test session results, code coverage, and logs: + /Users/szymon.chmal/Projects/react-native-harness/packages/platform-ios/xctest-agent/build/simulator/Logs/Test/Test-HarnessXCTestAgent-2026.04.22_18-02-23--0400.xcresult + +** TEST EXECUTE SUCCEEDED ** + +Testing started diff --git a/packages/platform-ios/xctest-agent/project.yml b/packages/platform-ios/xctest-agent/project.yml deleted file mode 100644 index 9462dc1..0000000 --- a/packages/platform-ios/xctest-agent/project.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: HarnessXCTestAgent -options: - createIntermediateGroups: true -settings: - base: - SWIFT_VERSION: 5.0 - IPHONEOS_DEPLOYMENT_TARGET: 16.0 - MARKETING_VERSION: 1.0 - CURRENT_PROJECT_VERSION: 1 - CODE_SIGN_STYLE: Automatic -targets: - HarnessXCTestAgentHost: - type: application - platform: iOS - sources: - - path: HarnessXCTestAgentHost - settings: - base: - GENERATE_INFOPLIST_FILE: YES - PRODUCT_BUNDLE_IDENTIFIER: dev.reactnativeharness.xctest-agent.host - TARGETED_DEVICE_FAMILY: 1,2 - SUPPORTED_PLATFORMS: iphoneos iphonesimulator - HarnessXCTestAgentTests: - type: bundle.ui-testing - platform: iOS - testTargetName: HarnessXCTestAgentHost - sources: - - path: HarnessXCTestAgentTests - settings: - base: - GENERATE_INFOPLIST_FILE: YES - PRODUCT_BUNDLE_IDENTIFIER: dev.reactnativeharness.xctest-agent.tests - TARGETED_DEVICE_FAMILY: 1,2 - SUPPORTED_PLATFORMS: iphoneos iphonesimulator -schemes: - HarnessXCTestAgent: - build: - targets: - HarnessXCTestAgentHost: all - HarnessXCTestAgentTests: all - test: - gatherCoverageData: false - targets: - - name: HarnessXCTestAgentTests diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4528a58..863697d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -137,6 +137,15 @@ importers: react-native: specifier: 0.82.1 version: 0.82.1(@babel/core@7.27.4)(@react-native-community/cli@20.0.0(typescript@5.9.3))(@react-native/metro-config@0.82.1)(@types/react@19.1.13)(react@19.2.3) + react-native-nitro-image: + specifier: ^0.13.1 + version: 0.13.1(react-native-nitro-modules@0.35.4(react-native@0.82.1(@babel/core@7.27.4)(@react-native-community/cli@20.0.0(typescript@5.9.3))(@react-native/metro-config@0.82.1)(@types/react@19.1.13)(react@19.2.3))(react@19.2.3))(react-native@0.82.1(@babel/core@7.27.4)(@react-native-community/cli@20.0.0(typescript@5.9.3))(@react-native/metro-config@0.82.1)(@types/react@19.1.13)(react@19.2.3))(react@19.2.3) + react-native-nitro-modules: + specifier: ^0.35.4 + version: 0.35.4(react-native@0.82.1(@babel/core@7.27.4)(@react-native-community/cli@20.0.0(typescript@5.9.3))(@react-native/metro-config@0.82.1)(@types/react@19.1.13)(react@19.2.3))(react@19.2.3) + react-native-vision-camera: + specifier: ^5.0.4 + version: 5.0.4(react-native-nitro-image@0.13.1(react-native-nitro-modules@0.35.4(react-native@0.82.1(@babel/core@7.27.4)(@react-native-community/cli@20.0.0(typescript@5.9.3))(@react-native/metro-config@0.82.1)(@types/react@19.1.13)(react@19.2.3))(react@19.2.3))(react-native@0.82.1(@babel/core@7.27.4)(@react-native-community/cli@20.0.0(typescript@5.9.3))(@react-native/metro-config@0.82.1)(@types/react@19.1.13)(react@19.2.3))(react@19.2.3))(react-native-nitro-modules@0.35.4(react-native@0.82.1(@babel/core@7.27.4)(@react-native-community/cli@20.0.0(typescript@5.9.3))(@react-native/metro-config@0.82.1)(@types/react@19.1.13)(react@19.2.3))(react@19.2.3))(react-native@0.82.1(@babel/core@7.27.4)(@react-native-community/cli@20.0.0(typescript@5.9.3))(@react-native/metro-config@0.82.1)(@types/react@19.1.13)(react@19.2.3))(react@19.2.3) react-native-web: specifier: ^0.21.2 version: 0.21.2(encoding@0.1.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -430,6 +439,9 @@ importers: '@react-native-harness/tools': specifier: workspace:* version: link:../tools + appium-ios-device: + specifier: ^3.1.11 + version: 3.1.11 tslib: specifier: ^2.3.0 version: 2.8.1 @@ -603,6 +615,26 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} + '@appium/logger@2.0.6': + resolution: {integrity: sha512-9e8n9CtINBwi1ASEU5OyswmR2F7OnbrGfmf9yTy9i+rx4GR9RJlEp0/arsxvuyWCep67tOmM4FiRyXxxHjOK5Q==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: '>=10'} + + '@appium/schema@1.1.0': + resolution: {integrity: sha512-m0vTLU7mhC9RR294Nz84g+FhEQ0iZKq6p3rfz1+qfEqCXRXUvDbllSOu2tCVpBKMIoEFZAmkwjuwXobJpCnilQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: '>=10'} + + '@appium/support@7.1.0': + resolution: {integrity: sha512-kY4Qv4TzLCYmZnN2eNptEa8RiRzpbimIQ6tKuDaqLC2Y3q5Al4NumL/xRQAvfXJq/hNezq2Jh8NwciEW8zX/0g==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: '>=10'} + + '@appium/tsconfig@1.1.2': + resolution: {integrity: sha512-lHKBm7hXCROc1Ha/cBxS4o3iQkeY96Pz7qM9Uh9vFDkdpTGBk56V1lmc3iGcgBYKBlaRT/LZmTsqClvHoiXhvw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: '>=10'} + + '@appium/types@1.3.0': + resolution: {integrity: sha512-Gv4ev/5K5N7TvAHqem2DmB50zipC951QlmCDpuxDNHQl2dtCr20vJgnN8if7upqLcBX/6yNp3udR+f1n99zgcQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: '>=10'} + '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} @@ -1312,9 +1344,16 @@ packages: '@clack/prompts@1.0.0-alpha.9': resolution: {integrity: sha512-sKs0UjiHFWvry4SiRfBi5Qnj0C/6AYx8aKkFPZQSuUZXgAram25ZDmhQmP7vj1aFyLpfHWtLQjWvOvcat0TOLg==} + '@colors/colors@1.6.0': + resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} + engines: {node: '>=0.1.90'} + '@emnapi/core@1.5.0': resolution: {integrity: sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==} + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + '@emnapi/runtime@1.5.0': resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==} @@ -1695,6 +1734,143 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -2684,6 +2860,9 @@ packages: '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + '@sec-ant/readable-stream@0.4.1': + resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + '@selderee/plugin-htmlparser2@0.11.0': resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} @@ -2918,6 +3097,9 @@ packages: resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} engines: {node: '>=10.13.0'} + '@tsconfig/node20@20.1.9': + resolution: {integrity: sha512-IjlTv1RsvnPtUcjTqtVsZExKVq+KQx4g5pCP5tI7rAs6Xesl2qFwSz/tPDBC4JajkL/MlezBu3gPUwqRHl+RIg==} + '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} @@ -3011,6 +3193,9 @@ packages: '@types/node@20.19.25': resolution: {integrity: sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==} + '@types/normalize-package-data@2.4.4': + resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + '@types/parse-json@4.0.2': resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} @@ -3403,6 +3588,10 @@ packages: '@webassemblyjs/wast-printer@1.14.1': resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} + '@xmldom/xmldom@0.8.13': + resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==} + engines: {node: '>=10.0.0'} + '@xtuc/ieee754@1.2.0': resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} @@ -3540,6 +3729,18 @@ packages: appdirsjs@1.2.7: resolution: {integrity: sha512-Quji6+8kLBC3NnBeo14nPDq0+2jUs5s3/xEye+udFHumHhRk4M7aAMXp/PBJqkKYGuuyR9M/6Dq7d2AViiGmhw==} + appium-ios-device@3.1.11: + resolution: {integrity: sha512-ccW8jAfZTtKc6mvFbbHCkVbB8/OxOdBolAB/sAHmwGl0jDrCrzMWOINkx1EdZx6QIrNPAw11Op1HibIRU66RWA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: '>=10'} + + archiver-utils@5.0.2: + resolution: {integrity: sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==} + engines: {node: '>= 14'} + + archiver@7.0.1: + resolution: {integrity: sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==} + engines: {node: '>= 14'} + argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} @@ -3620,6 +3821,10 @@ packages: async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + asyncbox@6.1.0: + resolution: {integrity: sha512-KZwKNVnDdDe0ubN+fFMuHhSljZNHnbjdJABImoqFzQP61oIg6sMlhXIqOIu3WRd7YwW89q+eVj2Ty/Ax5dbh2Q==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: '>=10'} + asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} @@ -3645,10 +3850,21 @@ packages: axios@1.13.2: resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==} + axios@1.15.0: + resolution: {integrity: sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==} + axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} + b4a@1.8.0: + resolution: {integrity: sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==} + peerDependencies: + react-native-b4a: '*' + peerDependenciesMeta: + react-native-b4a: + optional: true + babel-jest@29.7.0: resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -3742,9 +3958,57 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + bare-events@2.8.2: + resolution: {integrity: sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==} + peerDependencies: + bare-abort-controller: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true + + bare-fs@4.7.1: + resolution: {integrity: sha512-WDRsyVN52eAx/lBamKD6uyw8H4228h/x0sGGGegOamM2cd7Pag88GfMQalobXI+HaEUxpCkbKQUDOQqt9wawRw==} + engines: {bare: '>=1.16.0'} + peerDependencies: + bare-buffer: '*' + peerDependenciesMeta: + bare-buffer: + optional: true + + bare-os@3.9.0: + resolution: {integrity: sha512-JTjuZyNIDpw+GytMO4a6TK1VXdVKKJr6DRxEHasyuYyShV2deuiHJK/ahGZlebc+SG0/wJCB9XK8gprBGDFi/Q==} + engines: {bare: '>=1.14.0'} + + bare-path@3.0.0: + resolution: {integrity: sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==} + + bare-stream@2.13.0: + resolution: {integrity: sha512-3zAJRZMDFGjdn+RVnNpF9kuELw+0Fl3lpndM4NcEOhb9zwtSo/deETfuIwMSE5BXanA0FrN1qVjffGwAg2Y7EA==} + peerDependencies: + bare-abort-controller: '*' + bare-buffer: '*' + bare-events: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true + bare-buffer: + optional: true + bare-events: + optional: true + + bare-url@2.4.2: + resolution: {integrity: sha512-/9a2j4ac6ckpmAHvod/ob7x439OAHst/drc2Clnq+reRYd/ovddwcF4LfoxHyNk5AuGBnPg+HqFjmE/Zpq6v0A==} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + base64-stream@1.0.0: + resolution: {integrity: sha512-BQQZftaO48FcE1Kof9CmXMFaAdqkcNorgc8CxesZv9nMbbTF1EFyQe89UOuh//QMmdtfUDXyO8rgUalemL5ODA==} + baseline-browser-mapping@2.8.29: resolution: {integrity: sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA==} hasBin: true @@ -3753,6 +4017,10 @@ packages: resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==} engines: {node: '>= 0.8'} + big-integer@1.6.52: + resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} + engines: {node: '>=0.6'} + big.js@5.2.2: resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==} @@ -3766,6 +4034,9 @@ packages: bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + bluebird@3.7.2: + resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} + body-parser@1.20.3: resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} @@ -3776,12 +4047,23 @@ packages: boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + bplist-creator@0.1.1: + resolution: {integrity: sha512-Ese7052fdWrxp/vqSJkydgx/1MdBnNOCV2XVfbmdGWD2H6EYza+Q4pyYSuVSnCUD22hfI/BFI4jHaC3NLXLlJQ==} + + bplist-parser@0.3.2: + resolution: {integrity: sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==} + engines: {node: '>= 5.10.0'} + brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} brace-expansion@2.0.2: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + brace-expansion@5.0.5: + resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} + engines: {node: 18 || 20 || >=22} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -3804,12 +4086,22 @@ packages: engines: {node: '>= 0.4.0'} hasBin: true + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + + buffer-crc32@1.0.0: + resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} + engines: {node: '>=8.0.0'} + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + bundle-require@5.1.0: resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -4029,6 +4321,10 @@ packages: commondir@1.0.1: resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + compress-commons@6.0.2: + resolution: {integrity: sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==} + engines: {node: '>= 14'} + compressible@2.0.18: resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} engines: {node: '>= 0.6'} @@ -4060,6 +4356,9 @@ packages: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} + console-control-strings@1.1.0: + resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} + content-disposition@0.5.4: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} @@ -4095,6 +4394,9 @@ packages: core-js@3.47.0: resolution: {integrity: sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==} + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + corser@2.0.1: resolution: {integrity: sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==} engines: {node: '>= 0.4.0'} @@ -4121,6 +4423,15 @@ packages: typescript: optional: true + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + + crc32-stream@6.0.0: + resolution: {integrity: sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==} + engines: {node: '>= 14'} + cron-parser@4.9.0: resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} engines: {node: '>=12.0.0'} @@ -4322,6 +4633,10 @@ packages: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + detect-newline@3.1.0: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} @@ -4385,6 +4700,9 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + duplexer@0.1.2: + resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} + eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -4751,6 +5069,9 @@ packages: eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + events-universal@1.0.1: + resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} + events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} @@ -4792,6 +5113,9 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} @@ -4882,6 +5206,10 @@ packages: resolution: {integrity: sha512-WgZ+nKbELDa6N3i/9nrHeNznm+lY3z4YfhDDWgW+5P0pdmMj26bxaxU11ookgY3NyP9GC7HvZ9etp0jRFqGEeQ==} engines: {node: '>=8'} + find-up-simple@1.0.1: + resolution: {integrity: sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==} + engines: {node: '>=18'} + find-up@3.0.0: resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} engines: {node: '>=6'} @@ -4923,6 +5251,15 @@ packages: debug: optional: true + follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + for-each@0.3.5: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} @@ -4981,6 +5318,10 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + ftp-response-parser@1.0.1: + resolution: {integrity: sha512-++Ahlo2hs/IC7UVQzjcSAfeUpCwTTzs4uvG5XfGnsinIFkWUYF4xWwPd5qZuK8MJrmUIxFMuHcfqaosCDjvIWw==} + engines: {node: '>=0.8.0'} + function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} @@ -5018,6 +5359,10 @@ packages: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} + get-stream@9.0.1: + resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} + engines: {node: '>=18'} + get-symbol-description@1.1.0: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} @@ -5046,6 +5391,10 @@ packages: deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} + glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me @@ -5188,6 +5537,10 @@ packages: hookable@6.1.0: resolution: {integrity: sha512-ZoKZSJgu8voGK2geJS+6YtYjvIzu9AOM/KZXsBxr83uhLL++e9pEv/dlgwgy3dvHg06kTz6JOh1hk3C8Ceiymw==} + hosted-git-info@9.0.2: + resolution: {integrity: sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg==} + engines: {node: ^20.17.0 || >=22.9.0} + html-encoding-sniffer@3.0.0: resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} engines: {node: '>=12'} @@ -5310,6 +5663,10 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + index-to-position@1.2.0: + resolution: {integrity: sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==} + engines: {node: '>=18'} + inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. @@ -5486,6 +5843,10 @@ packages: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} + is-stream@4.0.1: + resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} + engines: {node: '>=18'} + is-string@1.1.1: resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} engines: {node: '>= 0.4'} @@ -5502,6 +5863,10 @@ packages: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + is-weakmap@2.0.2: resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} engines: {node: '>= 0.4'} @@ -5526,12 +5891,22 @@ packages: resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} engines: {node: '>=8'} + isarray@0.0.1: + resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + isexe@4.0.0: + resolution: {integrity: sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==} + engines: {node: '>=20'} + isomorphic-ws@5.0.0: resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} peerDependencies: @@ -5792,6 +6167,10 @@ packages: engines: {node: '>=6'} hasBin: true + jsftp@2.1.3: + resolution: {integrity: sha512-r79EVB8jaNAZbq8hvanL8e8JGu2ZNr2bXdHC4ZdQhRImpSPpnWwm5DYVzQ5QxJmtGtKhNNuvqGgbNaFl604fEQ==} + engines: {node: '>=6'} + json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} @@ -5804,6 +6183,9 @@ packages: json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -5845,6 +6227,10 @@ packages: resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} engines: {node: '>=0.10.0'} + klaw@4.1.0: + resolution: {integrity: sha512-1zGZ9MF9H22UnkpVeuaGKOjfA2t6WrfdrJmGjy16ykcjnKQDmHVX+KI477rpbGevz/5FD4MC3xf1oxylBgcaQw==} + engines: {node: '>=14.14.0'} + kleur@3.0.3: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} @@ -5866,6 +6252,10 @@ packages: launch-editor@2.12.0: resolution: {integrity: sha512-giOHXoOtifjdHqUamwKq6c49GzBdLjvxrd2D+Q4V6uOHopJv7p9VJxikDsQ/CBXZbEITgUqSVHXLTG3VhPP1Dg==} + lazystream@1.0.1: + resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} + engines: {node: '>= 0.6.3'} + leac@0.6.0: resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==} @@ -5923,6 +6313,9 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lockfile@1.0.4: + resolution: {integrity: sha512-cvbTwETRfsFh4nHsL1eGWapU1XFi5Ot9E85sWAwia7Y7EgB7vfqcZhTKZ+l7hCGxSPoushMv5GKhT5PdLv03WA==} + lodash-es@4.17.23: resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==} @@ -5950,10 +6343,17 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + log-symbols@4.1.0: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} + log-symbols@7.0.1: + resolution: {integrity: sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==} + engines: {node: '>=18'} + log4js@6.9.1: resolution: {integrity: sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==} engines: {node: '>=8.0'} @@ -5981,6 +6381,14 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@11.3.3: + resolution: {integrity: sha512-JvNw9Y81y33E+BEYPr0U7omo+U9AySnsMsEiXgwT6yqd31VQWTLNQqmT4ou5eqPFUrTfIDFta2wKhB1hyohtAQ==} + engines: {node: 20 || >=22} + + lru-cache@11.3.5: + resolution: {integrity: sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==} + engines: {node: 20 || >=22} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -6313,6 +6721,10 @@ packages: resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==} hasBin: true + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -6343,6 +6755,10 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + mkdirp@1.0.4: resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} engines: {node: '>=10'} @@ -6381,6 +6797,10 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + ncp@2.0.0: + resolution: {integrity: sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==} + hasBin: true + negotiator@0.6.3: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} @@ -6432,6 +6852,10 @@ packages: resolution: {integrity: sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==} engines: {node: '>=0.12.0'} + normalize-package-data@8.0.0: + resolution: {integrity: sha512-RWk+PI433eESQ7ounYxIp67CYuVsS1uYSonX3kA6ps/3LWfjVQa/ptEg6Y3T6uAMq1mWpX9PQ+qx+QaHpsc7gQ==} + engines: {node: ^20.17.0 || >=22.9.0} + normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} @@ -6581,6 +7005,10 @@ packages: resolution: {integrity: sha512-i8PyM2JnsNChVSYWLr2BAjNoLi0BAYC+wecOnZnVV+YSNJkzP7cWmvI34dk0WArWfH9KwBHNoZI3P3MppImlIA==} engines: {node: '>=20'} + p-limit@7.3.0: + resolution: {integrity: sha512-7cIXg/Z0M5WZRblrsOla88S4wAK+zOQQWeBYfV3qJuJXMr+LnbYjaadrFaS0JILfEDPVqHyKnZ1Z/1d6J9VVUw==} + engines: {node: '>=20'} + p-locate@3.0.0: resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==} engines: {node: '>=6'} @@ -6605,6 +7033,10 @@ packages: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} + package-directory@8.2.0: + resolution: {integrity: sha512-qJSu5Mo6tHmRxCy2KCYYKYgcfBdUpy9dwReaZD/xwf608AUk/MoRtIOWzgDtUeGeC7n/55yC3MI1Q+MbSoektw==} + engines: {node: '>=18'} + package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} @@ -6619,6 +7051,14 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} + parse-json@8.3.0: + resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==} + engines: {node: '>=18'} + + parse-listing@1.1.3: + resolution: {integrity: sha512-a1p1i+9Qyc8pJNwdrSvW1g5TPxRH0sywVi6OzVvYHRo6xwF9bDWBxtH0KkxeOOvhUE8vAMtiSfsYQFOuK901eA==} + engines: {node: '>=0.6.21'} + parse-passwd@1.0.0: resolution: {integrity: sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==} engines: {node: '>=0.10.0'} @@ -6656,6 +7096,10 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + path-to-regexp@0.1.12: resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} @@ -6673,6 +7117,9 @@ packages: peberminta@0.9.0: resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==} + pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -6721,6 +7168,14 @@ packages: engines: {node: '>=18'} hasBin: true + plist@3.1.0: + resolution: {integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==} + engines: {node: '>=10.4.0'} + + pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + pngjs@7.0.0: resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==} engines: {node: '>=14.19.0'} @@ -6985,6 +7440,13 @@ packages: resolution: {integrity: sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + promise.series@0.2.0: resolution: {integrity: sha512-VWQJyU2bcDTgZw8kpfBpB/ejZASlCrzwz5f2hjb/zlujOEB4oeiAhHygAWq8ubsX2GVkD4kCU5V2dwOTaCY5EQ==} engines: {node: '>=0.12'} @@ -7015,6 +7477,10 @@ packages: proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} + psl@1.15.0: resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} @@ -7073,11 +7539,32 @@ packages: react-lazy-with-preload@2.2.1: resolution: {integrity: sha512-ONSb8gizLE5jFpdHAclZ6EAAKuFX2JydnFXPPPjoUImZlLjGtKzyBS8SJgJq7CpLgsGKh9QCZdugJyEEOVC16Q==} + react-native-nitro-image@0.13.1: + resolution: {integrity: sha512-o2t1DNmNV57XfCtP6zX3LY7S3oJg1FrwlttuQM9nxoAPNl2bDBaKp+J3NtVFP+vriDPRTRChJP2xqGPbTGzzxw==} + peerDependencies: + react: '*' + react-native: '*' + react-native-nitro-modules: '*' + + react-native-nitro-modules@0.35.4: + resolution: {integrity: sha512-4qZa+1kgR/sPRNZv+UShxyArEPpovWxw76Dfd/DtCVtkQ92wOOxGIzdYvndprabd+t+r8zNYgYEPYE74gzkuVQ==} + peerDependencies: + react: '*' + react-native: '*' + react-native-url-polyfill@3.0.0: resolution: {integrity: sha512-aA5CiuUCUb/lbrliVCJ6lZ17/RpNJzvTO/C7gC/YmDQhTUoRD5q5HlJfwLWcxz4VgAhHwXKzhxH+wUN24tAdqg==} peerDependencies: react-native: '*' + react-native-vision-camera@5.0.4: + resolution: {integrity: sha512-RkKPiI3nC0eqrmJM8PP6VWEZpsMaKY8TzZM53Vq8OKfmiO+MAipd7EVmGH0cTos7o6AJjFgh0QUQz9oIDN1awg==} + peerDependencies: + react: '*' + react-native: '*' + react-native-nitro-image: '*' + react-native-nitro-modules: '*' + react-native-web@0.21.2: resolution: {integrity: sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg==} peerDependencies: @@ -7134,10 +7621,27 @@ packages: resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==} engines: {node: '>=0.10.0'} + read-pkg@10.1.0: + resolution: {integrity: sha512-I8g2lArQiP78ll51UeMZojewtYgIRCKCWqZEgOO8c/uefTI+XDXvCSXu3+YNUaTNvZzobrL5+SqHjBrByRRTdg==} + engines: {node: '>=20'} + + readable-stream@1.1.14: + resolution: {integrity: sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==} + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + readable-stream@3.6.2: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} + readable-stream@4.7.0: + resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + readdir-glob@1.1.3: + resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} + readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} @@ -7353,6 +7857,9 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + sanitize-filename@1.6.4: + resolution: {integrity: sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==} + saxes@6.0.0: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} @@ -7402,6 +7909,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + send@0.19.0: resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} engines: {node: '>= 0.8.0'} @@ -7441,6 +7953,10 @@ packages: setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -7531,6 +8047,18 @@ packages: space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + spdx-correct@3.2.0: + resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} + + spdx-exceptions@2.5.0: + resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} + + spdx-expression-parse@3.0.1: + resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + + spdx-license-ids@3.0.23: + resolution: {integrity: sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==} + sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} @@ -7570,10 +8098,20 @@ packages: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} + stream-buffers@2.2.0: + resolution: {integrity: sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==} + engines: {node: '>= 0.10.0'} + + stream-combiner@0.2.2: + resolution: {integrity: sha512-6yHMqgLYDzQDcAkL+tjJDC5nSNuNIx0vZtRZeiPh7Saef7VHX9H5Ijn9l2VIol2zaNYlYEX6KyuT/237A58qEQ==} + streamroller@3.1.5: resolution: {integrity: sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==} engines: {node: '>=8.0'} + streamx@2.25.0: + resolution: {integrity: sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==} + string-hash@1.1.3: resolution: {integrity: sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==} @@ -7615,6 +8153,12 @@ packages: resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} engines: {node: '>= 0.4'} + string_decoder@0.10.31: + resolution: {integrity: sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} @@ -7682,6 +8226,10 @@ packages: engines: {node: '>=16 || 14 >=14.17'} hasBin: true + supports-color@10.2.2: + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} + engines: {node: '>=18'} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -7714,6 +8262,10 @@ packages: resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} engines: {node: ^14.18.0 || >=16.0.0} + tagged-tag@1.0.0: + resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} + engines: {node: '>=20'} + tapable@2.3.0: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} @@ -7722,6 +8274,16 @@ packages: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} + tar-stream@3.1.8: + resolution: {integrity: sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==} + + teen_process@4.1.0: + resolution: {integrity: sha512-AN8y3MYPExB3r2mkkX9r0wEF4xPfhKOj6YvcfeIqQai+GVhTIhjjdkPvwI5CFT4z8UQ5aZWldzbJ+jNejYAdGw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: '>=10'} + + teex@1.0.1: + resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==} + terser-webpack-plugin@5.3.14: resolution: {integrity: sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==} engines: {node: '>= 10.13.0'} @@ -7751,6 +8313,9 @@ packages: resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} engines: {node: '>=18'} + text-decoder@1.2.7: + resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==} + thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} engines: {node: '>=0.8'} @@ -7761,6 +8326,9 @@ packages: throat@5.0.0: resolution: {integrity: sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==} + through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -7830,6 +8398,9 @@ packages: trough@2.2.0: resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + truncate-utf8-bytes@1.0.2: + resolution: {integrity: sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==} + ts-api-utils@2.1.0: resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} engines: {node: '>=18.12'} @@ -7888,6 +8459,14 @@ packages: resolution: {integrity: sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==} engines: {node: '>=8'} + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + + type-fest@5.5.0: + resolution: {integrity: sha512-PlBfpQwiUvGViBNX84Yxwjsdhd1TUlXr6zjX7eoirtCPIr08NAmxwa+fcYBTeRQxHo9YC9wwF3m9i700sHma8g==} + engines: {node: '>=20'} + type-is@1.6.18: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} @@ -7957,6 +8536,10 @@ packages: resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} engines: {node: '>=4'} + unicorn-magic@0.4.0: + resolution: {integrity: sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==} + engines: {node: '>=20'} + unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} @@ -7997,6 +8580,10 @@ packages: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} + unorm@1.6.0: + resolution: {integrity: sha512-b2/KCUlYZUeA7JFUuRJZPUtr4gZvBh7tavtv4fvk4+KV9pfGiR6CQAQAWl49ZpR3ts2dk4FYkP7EIgDJoiOLDA==} + engines: {node: '>= 0.4.0'} + unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} @@ -8034,6 +8621,9 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + utf8-byte-length@1.0.5: + resolution: {integrity: sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==} + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -8041,10 +8631,17 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} + uuid@13.0.0: + resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==} + hasBin: true + v8-to-istanbul@9.3.0: resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} engines: {node: '>=10.12.0'} + validate-npm-package-license@3.0.4: + resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -8227,6 +8824,11 @@ packages: engines: {node: '>= 8'} hasBin: true + which@6.0.1: + resolution: {integrity: sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==} + engines: {node: ^20.17.0 || >=22.9.0} + hasBin: true + why-is-node-running@2.3.0: resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} engines: {node: '>=8'} @@ -8310,6 +8912,10 @@ packages: resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} engines: {node: '>=12'} + xmlbuilder@15.1.1: + resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} + engines: {node: '>=8.0'} + xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} @@ -8348,6 +8954,10 @@ packages: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} + yauzl@3.3.0: + resolution: {integrity: sha512-PtGEvEP30p7sbIBJKUBjUnqgTVOyMURc4dLo9iNyAJnNIEz9pm88cCXF21w94Kg3k6RXkeZh5DHOGS0qEONvNQ==} + engines: {node: '>=12'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -8356,6 +8966,14 @@ packages: resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==} engines: {node: '>=12.20'} + yoctocolors@2.1.2: + resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} + engines: {node: '>=18'} + + zip-stream@6.0.1: + resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} + engines: {node: '>= 14'} + zod@3.25.67: resolution: {integrity: sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==} @@ -8387,6 +9005,72 @@ snapshots: '@jridgewell/gen-mapping': 0.3.8 '@jridgewell/trace-mapping': 0.3.25 + '@appium/logger@2.0.6': + dependencies: + console-control-strings: 1.1.0 + lodash: 4.18.1 + lru-cache: 11.3.3 + set-blocking: 2.0.0 + + '@appium/schema@1.1.0': + dependencies: + json-schema: 0.4.0 + + '@appium/support@7.1.0': + dependencies: + '@appium/logger': 2.0.6 + '@appium/tsconfig': 1.1.2 + '@appium/types': 1.3.0 + '@colors/colors': 1.6.0 + archiver: 7.0.1 + asyncbox: 6.1.0 + axios: 1.15.0 + base64-stream: 1.0.0 + bluebird: 3.7.2 + bplist-creator: 0.1.1 + bplist-parser: 0.3.2 + form-data: 4.0.5 + get-stream: 9.0.1 + glob: 13.0.6 + jsftp: 2.1.3(supports-color@10.2.2) + klaw: 4.1.0 + lockfile: 1.0.4 + lodash: 4.18.1 + log-symbols: 7.0.1 + ncp: 2.0.0 + package-directory: 8.2.0 + plist: 3.1.0 + pluralize: 8.0.0 + read-pkg: 10.1.0 + resolve-from: 5.0.0 + sanitize-filename: 1.6.4 + semver: 7.7.4 + shell-quote: 1.8.3 + supports-color: 10.2.2 + teen_process: 4.1.0 + type-fest: 5.5.0 + uuid: 13.0.0 + which: 6.0.1 + yauzl: 3.3.0 + optionalDependencies: + sharp: 0.34.5 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - debug + - react-native-b4a + + '@appium/tsconfig@1.1.2': + dependencies: + '@tsconfig/node20': 20.1.9 + + '@appium/types@1.3.0': + dependencies: + '@appium/logger': 2.0.6 + '@appium/schema': 1.1.0 + '@appium/tsconfig': 1.1.2 + type-fest: 5.5.0 + '@babel/code-frame@7.27.1': dependencies: '@babel/helper-validator-identifier': 7.27.1 @@ -9284,11 +9968,18 @@ snapshots: picocolors: 1.1.1 sisteransi: 1.0.5 + '@colors/colors@1.6.0': {} + '@emnapi/core@1.5.0': dependencies: '@emnapi/wasi-threads': 1.1.0 tslib: 2.8.1 + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + '@emnapi/runtime@1.5.0': dependencies: tslib: 2.8.1 @@ -9494,33 +10185,130 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.29.0': {} + '@eslint/js@9.29.0': {} + + '@eslint/object-schema@2.1.6': {} + + '@eslint/plugin-kit@0.3.2': + dependencies: + '@eslint/core': 0.15.0 + levn: 0.4.1 + + '@hapi/hoek@9.3.0': {} + + '@hapi/topo@5.1.0': + dependencies: + '@hapi/hoek': 9.3.0 + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.6': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.3.1 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.3.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@img/colour@1.1.0': + optional: true + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true - '@eslint/object-schema@2.1.6': {} + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true - '@eslint/plugin-kit@0.3.2': - dependencies: - '@eslint/core': 0.15.0 - levn: 0.4.1 + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true - '@hapi/hoek@9.3.0': {} + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true - '@hapi/topo@5.1.0': - dependencies: - '@hapi/hoek': 9.3.0 + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true - '@humanfs/core@0.19.1': {} + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true - '@humanfs/node@0.16.6': + '@img/sharp-wasm32@0.34.5': dependencies: - '@humanfs/core': 0.19.1 - '@humanwhocodes/retry': 0.3.1 + '@emnapi/runtime': 1.10.0 + optional: true - '@humanwhocodes/module-importer@1.0.1': {} + '@img/sharp-win32-arm64@0.34.5': + optional: true - '@humanwhocodes/retry@0.3.1': {} + '@img/sharp-win32-ia32@0.34.5': + optional: true - '@humanwhocodes/retry@0.4.3': {} + '@img/sharp-win32-x64@0.34.5': + optional: true '@isaacs/cliui@8.0.2': dependencies: @@ -10832,7 +11620,9 @@ snapshots: metro-config: 0.83.3 metro-runtime: 0.83.3 transitivePeerDependencies: + - bufferutil - supports-color + - utf-8-validate '@react-native/normalize-colors@0.74.89': {} @@ -11236,6 +12026,8 @@ snapshots: '@rtsao/scc@1.1.0': {} + '@sec-ant/readable-stream@0.4.1': {} + '@selderee/plugin-htmlparser2@0.11.0': dependencies: domhandler: 5.0.3 @@ -11487,6 +12279,8 @@ snapshots: '@trysound/sax@0.2.0': {} + '@tsconfig/node20@20.1.9': {} + '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 @@ -11601,6 +12395,8 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/normalize-package-data@2.4.4': {} + '@types/parse-json@4.0.2': {} '@types/pixelmatch@5.2.6': @@ -12096,6 +12892,8 @@ snapshots: '@webassemblyjs/ast': 1.14.1 '@xtuc/long': 4.2.2 + '@xmldom/xmldom@0.8.13': {} + '@xtuc/ieee754@1.2.0': {} '@xtuc/long@4.2.2': {} @@ -12212,6 +13010,46 @@ snapshots: appdirsjs@1.2.7: {} + appium-ios-device@3.1.11: + dependencies: + '@appium/support': 7.1.0 + asyncbox: 6.1.0 + axios: 1.13.2 + bluebird: 3.7.2 + bplist-creator: 0.1.1 + bplist-parser: 0.3.2 + lodash: 4.17.21 + semver: 7.7.2 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - debug + - react-native-b4a + + archiver-utils@5.0.2: + dependencies: + glob: 10.5.0 + graceful-fs: 4.2.11 + is-stream: 2.0.1 + lazystream: 1.0.1 + lodash: 4.18.1 + normalize-path: 3.0.0 + readable-stream: 4.7.0 + + archiver@7.0.1: + dependencies: + archiver-utils: 5.0.2 + async: 3.2.6 + buffer-crc32: 1.0.0 + readable-stream: 4.7.0 + readdir-glob: 1.1.3 + tar-stream: 3.1.8 + zip-stream: 6.0.1 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + argparse@1.0.10: dependencies: sprintf-js: 1.0.3 @@ -12313,6 +13151,10 @@ snapshots: async@3.2.6: {} + asyncbox@6.1.0: + dependencies: + p-limit: 7.3.0 + asynckit@0.4.0: {} at-least-node@1.0.0: {} @@ -12341,8 +13183,18 @@ snapshots: transitivePeerDependencies: - debug + axios@1.15.0: + dependencies: + follow-redirects: 1.16.0 + form-data: 4.0.5 + proxy-from-env: 2.1.0 + transitivePeerDependencies: + - debug + axobject-query@4.1.0: {} + b4a@1.8.0: {} + babel-jest@29.7.0(@babel/core@7.27.4): dependencies: '@babel/core': 7.27.4 @@ -12499,14 +13351,52 @@ snapshots: balanced-match@1.0.2: {} + balanced-match@4.0.4: {} + + bare-events@2.8.2: {} + + bare-fs@4.7.1: + dependencies: + bare-events: 2.8.2 + bare-path: 3.0.0 + bare-stream: 2.13.0(bare-events@2.8.2) + bare-url: 2.4.2 + fast-fifo: 1.3.2 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + bare-os@3.9.0: {} + + bare-path@3.0.0: + dependencies: + bare-os: 3.9.0 + + bare-stream@2.13.0(bare-events@2.8.2): + dependencies: + streamx: 2.25.0 + teex: 1.0.1 + optionalDependencies: + bare-events: 2.8.2 + transitivePeerDependencies: + - react-native-b4a + + bare-url@2.4.2: + dependencies: + bare-path: 3.0.0 + base64-js@1.5.1: {} + base64-stream@1.0.0: {} + baseline-browser-mapping@2.8.29: {} basic-auth@2.0.1: dependencies: safe-buffer: 5.1.2 + big-integer@1.6.52: {} + big.js@5.2.2: {} binary-extensions@2.3.0: {} @@ -12519,6 +13409,8 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 + bluebird@3.7.2: {} + body-parser@1.20.3: dependencies: bytes: 3.1.2 @@ -12540,6 +13432,14 @@ snapshots: boolbase@1.0.0: {} + bplist-creator@0.1.1: + dependencies: + stream-buffers: 2.2.0 + + bplist-parser@0.3.2: + dependencies: + big-integer: 1.6.52 + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 @@ -12549,6 +13449,10 @@ snapshots: dependencies: balanced-match: 1.0.2 + brace-expansion@5.0.5: + dependencies: + balanced-match: 4.0.4 + braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -12574,6 +13478,10 @@ snapshots: btoa@1.2.1: {} + buffer-crc32@0.2.13: {} + + buffer-crc32@1.0.0: {} + buffer-from@1.1.2: {} buffer@5.7.1: @@ -12581,6 +13489,11 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + bundle-require@5.1.0(esbuild@0.27.2): dependencies: esbuild: 0.27.2 @@ -12779,6 +13692,14 @@ snapshots: commondir@1.0.1: {} + compress-commons@6.0.2: + dependencies: + crc-32: 1.2.2 + crc32-stream: 6.0.0 + is-stream: 2.0.1 + normalize-path: 3.0.0 + readable-stream: 4.7.0 + compressible@2.0.18: dependencies: mime-db: 1.54.0 @@ -12818,6 +13739,8 @@ snapshots: consola@3.4.2: {} + console-control-strings@1.1.0: {} + content-disposition@0.5.4: dependencies: safe-buffer: 5.2.1 @@ -12847,6 +13770,8 @@ snapshots: core-js@3.47.0: {} + core-util-is@1.0.3: {} + corser@2.0.1: {} cosmiconfig@7.1.0: @@ -12875,6 +13800,13 @@ snapshots: optionalDependencies: typescript: 5.9.3 + crc-32@1.2.2: {} + + crc32-stream@6.0.0: + dependencies: + crc-32: 1.2.2 + readable-stream: 4.7.0 + cron-parser@4.9.0: dependencies: luxon: 3.6.1 @@ -13026,9 +13958,11 @@ snapshots: dependencies: ms: 2.0.0 - debug@3.2.7: + debug@3.2.7(supports-color@10.2.2): dependencies: ms: 2.1.3 + optionalDependencies: + supports-color: 10.2.2 debug@4.4.1: dependencies: @@ -13084,6 +14018,9 @@ snapshots: destroy@1.2.0: {} + detect-libc@2.1.2: + optional: true + detect-newline@3.1.0: {} detect-port@1.6.1: @@ -13160,6 +14097,8 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + duplexer@0.1.2: {} + eastasianwidth@0.2.0: {} ee-first@1.1.1: {} @@ -13420,7 +14359,7 @@ snapshots: eslint-import-resolver-node@0.3.9: dependencies: - debug: 3.2.7 + debug: 3.2.7(supports-color@10.2.2) is-core-module: 2.16.1 resolve: 1.22.10 transitivePeerDependencies: @@ -13428,7 +14367,7 @@ snapshots: eslint-module-utils@2.12.0(@typescript-eslint/parser@8.47.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.29.0(jiti@2.4.2)): dependencies: - debug: 3.2.7 + debug: 3.2.7(supports-color@10.2.2) optionalDependencies: '@typescript-eslint/parser': 8.47.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.9.3) eslint: 9.29.0(jiti@2.4.2) @@ -13456,7 +14395,7 @@ snapshots: array.prototype.findlastindex: 1.2.6 array.prototype.flat: 1.3.3 array.prototype.flatmap: 1.3.3 - debug: 3.2.7 + debug: 3.2.7(supports-color@10.2.2) doctrine: 2.1.0 eslint: 9.29.0(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 @@ -13741,6 +14680,12 @@ snapshots: eventemitter3@4.0.7: {} + events-universal@1.0.1: + dependencies: + bare-events: 2.8.2 + transitivePeerDependencies: + - bare-abort-controller + events@3.3.0: {} execa@5.1.1: @@ -13818,6 +14763,8 @@ snapshots: fast-deep-equal@3.1.3: {} + fast-fifo@1.3.2: {} + fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -13930,6 +14877,8 @@ snapshots: dependencies: find-file-up: 2.0.1 + find-up-simple@1.0.1: {} + find-up@3.0.0: dependencies: locate-path: 3.0.0 @@ -13967,6 +14916,8 @@ snapshots: optionalDependencies: debug: 4.4.1 + follow-redirects@1.16.0: {} + for-each@0.3.5: dependencies: is-callable: 1.2.7 @@ -14031,6 +14982,10 @@ snapshots: fsevents@2.3.3: optional: true + ftp-response-parser@1.0.1: + dependencies: + readable-stream: 1.1.14 + function-bind@1.1.2: {} function.prototype.name@1.1.8: @@ -14074,6 +15029,11 @@ snapshots: get-stream@6.0.1: {} + get-stream@9.0.1: + dependencies: + '@sec-ant/readable-stream': 0.4.1 + is-stream: 4.0.1 + get-symbol-description@1.1.0: dependencies: call-bound: 1.0.4 @@ -14110,6 +15070,12 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 1.11.1 + glob@13.0.6: + dependencies: + minimatch: 10.2.5 + minipass: 7.1.3 + path-scurry: 2.0.2 + glob@7.2.3: dependencies: fs.realpath: 1.0.0 @@ -14344,6 +15310,10 @@ snapshots: hookable@6.1.0: {} + hosted-git-info@9.0.2: + dependencies: + lru-cache: 11.3.5 + html-encoding-sniffer@3.0.0: dependencies: whatwg-encoding: 2.0.0 @@ -14503,6 +15473,8 @@ snapshots: imurmurhash@0.1.4: {} + index-to-position@1.2.0: {} + inflight@1.0.6: dependencies: once: 1.4.0 @@ -14658,6 +15630,8 @@ snapshots: is-stream@2.0.1: {} + is-stream@4.0.1: {} + is-string@1.1.1: dependencies: call-bound: 1.0.4 @@ -14675,6 +15649,8 @@ snapshots: is-unicode-supported@0.1.0: {} + is-unicode-supported@2.1.0: {} + is-weakmap@2.0.2: {} is-weakref@1.1.1: @@ -14694,10 +15670,16 @@ snapshots: dependencies: is-docker: 2.2.1 + isarray@0.0.1: {} + + isarray@1.0.0: {} + isarray@2.0.5: {} isexe@2.0.0: {} + isexe@4.0.0: {} + isomorphic-ws@5.0.0(ws@8.18.0): dependencies: ws: 8.18.0 @@ -15249,6 +16231,17 @@ snapshots: jsesc@3.1.0: {} + jsftp@2.1.3(supports-color@10.2.2): + dependencies: + debug: 3.2.7(supports-color@10.2.2) + ftp-response-parser: 1.0.1 + once: 1.4.0 + parse-listing: 1.1.3 + stream-combiner: 0.2.2 + unorm: 1.6.0 + transitivePeerDependencies: + - supports-color + json-buffer@3.0.1: {} json-parse-even-better-errors@2.3.1: {} @@ -15257,6 +16250,8 @@ snapshots: json-schema-traverse@1.0.0: {} + json-schema@0.4.0: {} + json-stable-stringify-without-jsonify@1.0.1: {} json5@1.0.2: @@ -15301,6 +16296,8 @@ snapshots: kind-of@6.0.3: {} + klaw@4.1.0: {} + kleur@3.0.3: {} koa-compose@4.1.0: {} @@ -15337,6 +16334,10 @@ snapshots: picocolors: 1.1.1 shell-quote: 1.8.3 + lazystream@1.0.1: + dependencies: + readable-stream: 2.3.8 + leac@0.6.0: {} leven@3.1.0: {} @@ -15386,6 +16387,10 @@ snapshots: dependencies: p-locate: 5.0.0 + lockfile@1.0.4: + dependencies: + signal-exit: 3.0.7 + lodash-es@4.17.23: {} lodash.camelcase@4.3.0: {} @@ -15404,11 +16409,18 @@ snapshots: lodash@4.17.21: {} + lodash@4.18.1: {} + log-symbols@4.1.0: dependencies: chalk: 4.1.2 is-unicode-supported: 0.1.0 + log-symbols@7.0.1: + dependencies: + is-unicode-supported: 2.1.0 + yoctocolors: 2.1.2 + log4js@6.9.1: dependencies: date-format: 4.0.14 @@ -15441,6 +16453,10 @@ snapshots: lru-cache@10.4.3: {} + lru-cache@11.3.3: {} + + lru-cache@11.3.5: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -16130,6 +17146,10 @@ snapshots: mini-svg-data-uri@1.4.4: {} + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.5 + minimatch@3.1.2: dependencies: brace-expansion: 1.1.12 @@ -16156,6 +17176,8 @@ snapshots: minipass@7.1.2: {} + minipass@7.1.3: {} + mkdirp@1.0.4: {} mlly@1.8.0: @@ -16185,6 +17207,8 @@ snapshots: natural-compare@1.4.0: {} + ncp@2.0.0: {} + negotiator@0.6.3: {} negotiator@0.6.4: {} @@ -16222,6 +17246,12 @@ snapshots: node-stream-zip@1.15.0: {} + normalize-package-data@8.0.0: + dependencies: + hosted-git-info: 9.0.2 + semver: 7.7.4 + validate-npm-package-license: 3.0.4 + normalize-path@3.0.0: {} normalize-range@0.1.2: {} @@ -16436,6 +17466,10 @@ snapshots: dependencies: yocto-queue: 1.2.1 + p-limit@7.3.0: + dependencies: + yocto-queue: 1.2.1 + p-locate@3.0.0: dependencies: p-limit: 2.3.0 @@ -16459,6 +17493,10 @@ snapshots: p-try@2.2.0: {} + package-directory@8.2.0: + dependencies: + find-up-simple: 1.0.1 + package-json-from-dist@1.0.1: {} parent-module@1.0.1: @@ -16482,6 +17520,14 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + parse-json@8.3.0: + dependencies: + '@babel/code-frame': 7.27.1 + index-to-position: 1.2.0 + type-fest: 4.41.0 + + parse-listing@1.1.3: {} + parse-passwd@1.0.0: {} parse5@7.3.0: @@ -16510,6 +17556,11 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 + path-scurry@2.0.2: + dependencies: + lru-cache: 11.3.5 + minipass: 7.1.3 + path-to-regexp@0.1.12: {} path-type@4.0.0: {} @@ -16520,6 +17571,8 @@ snapshots: peberminta@0.9.0: {} + pend@1.2.0: {} + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -16558,6 +17611,14 @@ snapshots: optionalDependencies: fsevents: 2.3.2 + plist@3.1.0: + dependencies: + '@xmldom/xmldom': 0.8.13 + base64-js: 1.5.1 + xmlbuilder: 15.1.1 + + pluralize@8.0.0: {} + pngjs@7.0.0: {} portfinder@1.0.37: @@ -16799,6 +17860,10 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.3.1 + process-nextick-args@2.0.1: {} + + process@0.11.10: {} + promise.series@0.2.0: {} promise@7.3.1: @@ -16831,6 +17896,8 @@ snapshots: proxy-from-env@1.1.0: {} + proxy-from-env@2.1.0: {} + psl@1.15.0: dependencies: punycode: 2.3.1 @@ -16889,11 +17956,29 @@ snapshots: react-lazy-with-preload@2.2.1: {} + react-native-nitro-image@0.13.1(react-native-nitro-modules@0.35.4(react-native@0.82.1(@babel/core@7.27.4)(@react-native-community/cli@20.0.0(typescript@5.9.3))(@react-native/metro-config@0.82.1)(@types/react@19.1.13)(react@19.2.3))(react@19.2.3))(react-native@0.82.1(@babel/core@7.27.4)(@react-native-community/cli@20.0.0(typescript@5.9.3))(@react-native/metro-config@0.82.1)(@types/react@19.1.13)(react@19.2.3))(react@19.2.3): + dependencies: + react: 19.2.3 + react-native: 0.82.1(@babel/core@7.27.4)(@react-native-community/cli@20.0.0(typescript@5.9.3))(@react-native/metro-config@0.82.1)(@types/react@19.1.13)(react@19.2.3) + react-native-nitro-modules: 0.35.4(react-native@0.82.1(@babel/core@7.27.4)(@react-native-community/cli@20.0.0(typescript@5.9.3))(@react-native/metro-config@0.82.1)(@types/react@19.1.13)(react@19.2.3))(react@19.2.3) + + react-native-nitro-modules@0.35.4(react-native@0.82.1(@babel/core@7.27.4)(@react-native-community/cli@20.0.0(typescript@5.9.3))(@react-native/metro-config@0.82.1)(@types/react@19.1.13)(react@19.2.3))(react@19.2.3): + dependencies: + react: 19.2.3 + react-native: 0.82.1(@babel/core@7.27.4)(@react-native-community/cli@20.0.0(typescript@5.9.3))(@react-native/metro-config@0.82.1)(@types/react@19.1.13)(react@19.2.3) + react-native-url-polyfill@3.0.0(react-native@0.82.1(@babel/core@7.27.4)(@react-native-community/cli@20.0.0(typescript@5.9.3))(@react-native/metro-config@0.82.1)(@types/react@19.1.13)(react@19.2.3)): dependencies: react-native: 0.82.1(@babel/core@7.27.4)(@react-native-community/cli@20.0.0(typescript@5.9.3))(@react-native/metro-config@0.82.1)(@types/react@19.1.13)(react@19.2.3) whatwg-url-without-unicode: 8.0.0-3 + react-native-vision-camera@5.0.4(react-native-nitro-image@0.13.1(react-native-nitro-modules@0.35.4(react-native@0.82.1(@babel/core@7.27.4)(@react-native-community/cli@20.0.0(typescript@5.9.3))(@react-native/metro-config@0.82.1)(@types/react@19.1.13)(react@19.2.3))(react@19.2.3))(react-native@0.82.1(@babel/core@7.27.4)(@react-native-community/cli@20.0.0(typescript@5.9.3))(@react-native/metro-config@0.82.1)(@types/react@19.1.13)(react@19.2.3))(react@19.2.3))(react-native-nitro-modules@0.35.4(react-native@0.82.1(@babel/core@7.27.4)(@react-native-community/cli@20.0.0(typescript@5.9.3))(@react-native/metro-config@0.82.1)(@types/react@19.1.13)(react@19.2.3))(react@19.2.3))(react-native@0.82.1(@babel/core@7.27.4)(@react-native-community/cli@20.0.0(typescript@5.9.3))(@react-native/metro-config@0.82.1)(@types/react@19.1.13)(react@19.2.3))(react@19.2.3): + dependencies: + react: 19.2.3 + react-native: 0.82.1(@babel/core@7.27.4)(@react-native-community/cli@20.0.0(typescript@5.9.3))(@react-native/metro-config@0.82.1)(@types/react@19.1.13)(react@19.2.3) + react-native-nitro-image: 0.13.1(react-native-nitro-modules@0.35.4(react-native@0.82.1(@babel/core@7.27.4)(@react-native-community/cli@20.0.0(typescript@5.9.3))(@react-native/metro-config@0.82.1)(@types/react@19.1.13)(react@19.2.3))(react@19.2.3))(react-native@0.82.1(@babel/core@7.27.4)(@react-native-community/cli@20.0.0(typescript@5.9.3))(@react-native/metro-config@0.82.1)(@types/react@19.1.13)(react@19.2.3))(react@19.2.3) + react-native-nitro-modules: 0.35.4(react-native@0.82.1(@babel/core@7.27.4)(@react-native-community/cli@20.0.0(typescript@5.9.3))(@react-native/metro-config@0.82.1)(@types/react@19.1.13)(react@19.2.3))(react@19.2.3) + react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: '@babel/runtime': 7.27.6 @@ -16984,12 +18069,49 @@ snapshots: react@19.2.3: {} + read-pkg@10.1.0: + dependencies: + '@types/normalize-package-data': 2.4.4 + normalize-package-data: 8.0.0 + parse-json: 8.3.0 + type-fest: 5.5.0 + unicorn-magic: 0.4.0 + + readable-stream@1.1.14: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 0.0.1 + string_decoder: 0.10.31 + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + readable-stream@3.6.2: dependencies: inherits: 2.0.4 string_decoder: 1.3.0 util-deprecate: 1.0.2 + readable-stream@4.7.0: + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + + readdir-glob@1.1.3: + dependencies: + minimatch: 5.1.6 + readdirp@3.6.0: dependencies: picomatch: 2.3.1 @@ -17304,6 +18426,10 @@ snapshots: safer-buffer@2.1.2: {} + sanitize-filename@1.6.4: + dependencies: + truncate-utf8-bytes: 1.0.2 + saxes@6.0.0: dependencies: xmlchars: 2.2.0 @@ -17353,6 +18479,8 @@ snapshots: semver@7.7.2: {} + semver@7.7.4: {} + send@0.19.0: dependencies: debug: 2.6.9 @@ -17416,6 +18544,38 @@ snapshots: setprototypeof@1.2.0: {} + sharp@0.34.5: + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.7.4 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + optional: true + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -17517,6 +18677,20 @@ snapshots: space-separated-tokens@2.0.2: {} + spdx-correct@3.2.0: + dependencies: + spdx-expression-parse: 3.0.1 + spdx-license-ids: 3.0.23 + + spdx-exceptions@2.5.0: {} + + spdx-expression-parse@3.0.1: + dependencies: + spdx-exceptions: 2.5.0 + spdx-license-ids: 3.0.23 + + spdx-license-ids@3.0.23: {} + sprintf-js@1.0.3: {} ssim.js@3.5.0: {} @@ -17546,6 +18720,13 @@ snapshots: es-errors: 1.3.0 internal-slot: 1.1.0 + stream-buffers@2.2.0: {} + + stream-combiner@0.2.2: + dependencies: + duplexer: 0.1.2 + through: 2.3.8 + streamroller@3.1.5: dependencies: date-format: 4.0.14 @@ -17554,6 +18735,15 @@ snapshots: transitivePeerDependencies: - supports-color + streamx@2.25.0: + dependencies: + events-universal: 1.0.1 + fast-fifo: 1.3.2 + text-decoder: 1.2.7 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + string-hash@1.1.3: {} string-length@4.0.2: @@ -17625,6 +18815,12 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + string_decoder@0.10.31: {} + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + string_decoder@1.3.0: dependencies: safe-buffer: 5.2.1 @@ -17690,6 +18886,8 @@ snapshots: pirates: 4.0.7 ts-interface-checker: 0.1.13 + supports-color@10.2.2: {} + supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -17728,6 +18926,8 @@ snapshots: dependencies: '@pkgr/core': 0.2.9 + tagged-tag@1.0.0: {} + tapable@2.3.0: {} tar-stream@2.2.0: @@ -17738,6 +18938,29 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 + tar-stream@3.1.8: + dependencies: + b4a: 1.8.0 + bare-fs: 4.7.1 + fast-fifo: 1.3.2 + streamx: 2.25.0 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + + teen_process@4.1.0: + dependencies: + lodash: 4.18.1 + shell-quote: 1.8.3 + + teex@1.0.1: + dependencies: + streamx: 2.25.0 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + terser-webpack-plugin@5.3.14(@swc/core@1.5.29(@swc/helpers@0.5.17))(webpack@5.102.1(@swc/core@1.5.29(@swc/helpers@0.5.17))): dependencies: '@jridgewell/trace-mapping': 0.3.25 @@ -17768,6 +18991,12 @@ snapshots: glob: 10.5.0 minimatch: 9.0.5 + text-decoder@1.2.7: + dependencies: + b4a: 1.8.0 + transitivePeerDependencies: + - react-native-b4a + thenify-all@1.6.0: dependencies: thenify: 3.3.1 @@ -17778,6 +19007,8 @@ snapshots: throat@5.0.0: {} + through@2.3.8: {} + tinybench@2.9.0: {} tinyexec@0.3.2: {} @@ -17828,6 +19059,10 @@ snapshots: trough@2.2.0: {} + truncate-utf8-bytes@1.0.2: + dependencies: + utf8-byte-length: 1.0.5 + ts-api-utils@2.1.0(typescript@5.9.3): dependencies: typescript: 5.9.3 @@ -17890,6 +19125,12 @@ snapshots: type-fest@0.7.1: {} + type-fest@4.41.0: {} + + type-fest@5.5.0: + dependencies: + tagged-tag: 1.0.0 + type-is@1.6.18: dependencies: media-typer: 0.3.0 @@ -17975,6 +19216,8 @@ snapshots: unicode-property-aliases-ecmascript@2.1.0: {} + unicorn-magic@0.4.0: {} + unified@11.0.5: dependencies: '@types/unist': 3.0.3 @@ -18026,6 +19269,8 @@ snapshots: universalify@2.0.1: {} + unorm@1.6.0: {} + unpipe@1.0.0: {} unrs-resolver@1.11.1: @@ -18081,16 +19326,25 @@ snapshots: dependencies: react: 19.2.3 + utf8-byte-length@1.0.5: {} + util-deprecate@1.0.2: {} utils-merge@1.0.1: {} + uuid@13.0.0: {} + v8-to-istanbul@9.3.0: dependencies: '@jridgewell/trace-mapping': 0.3.25 '@types/istanbul-lib-coverage': 2.0.6 convert-source-map: 2.0.0 + validate-npm-package-license@3.0.4: + dependencies: + spdx-correct: 3.2.0 + spdx-expression-parse: 3.0.1 + vary@1.1.2: {} vfile-location@5.0.3: @@ -18324,6 +19578,10 @@ snapshots: dependencies: isexe: 2.0.0 + which@6.0.1: + dependencies: + isexe: 4.0.0 + why-is-node-running@2.3.0: dependencies: siginfo: 2.0.0 @@ -18373,6 +19631,8 @@ snapshots: xml-name-validator@4.0.0: {} + xmlbuilder@15.1.1: {} + xmlchars@2.2.0: {} y18n@4.0.3: {} @@ -18416,10 +19676,23 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 + yauzl@3.3.0: + dependencies: + buffer-crc32: 0.2.13 + pend: 1.2.0 + yocto-queue@0.1.0: {} yocto-queue@1.2.1: {} + yoctocolors@2.1.2: {} + + zip-stream@6.0.1: + dependencies: + archiver-utils: 5.0.2 + compress-commons: 6.0.2 + readable-stream: 4.7.0 + zod@3.25.67: {} zustand@5.0.5(@types/react@19.1.13)(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3)): From cd4040f6d636be14e7e8e2a16db99ad85015c471 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Tue, 28 Apr 2026 11:35:49 +0200 Subject: [PATCH 08/33] refactor(platform-ios): remove unused shutdown endpoint and prepareRun/disposeRun hooks --- packages/jest/src/harness.ts | 17 -- .../src/__tests__/xctest-agent.test.ts | 65 +++++- packages/platform-ios/src/instance.ts | 6 + packages/platform-ios/src/xctest-agent.ts | 202 +++++++++++++++--- .../AppIcon.appiconset/Contents.json | 1 + .../HarnessXCTestAgentUITests.swift | 21 +- packages/platforms/src/types.ts | 2 - 7 files changed, 240 insertions(+), 74 deletions(-) diff --git a/packages/jest/src/harness.ts b/packages/jest/src/harness.ts index cef2ddc..cb918bb 100644 --- a/packages/jest/src/harness.ts +++ b/packages/jest/src/harness.ts @@ -322,8 +322,6 @@ const getHarnessInternal = async ( let activeTestFilePath: string | undefined; const pendingHookPromises = new Set>(); let pendingHookError: unknown; - let didPrepareRun = false; - const getCurrentRunId = () => currentRun?.runId; const toRelativeTestFilePath = (testFilePath?: string) => testFilePath == null @@ -651,15 +649,6 @@ const getHarnessInternal = async ( harnessLogger.debug('client log forwarding enabled'); } - const ensurePlatformRunPrepared = async () => { - if (didPrepareRun) { - return; - } - - await platformInstance.prepareRun?.(); - didPrepareRun = true; - }; - const dispose = async (reason: 'normal' | 'abort' | 'error' = 'normal') => { harnessLogger.debug('disposing Harness (reason=%s)', reason); let hookError: unknown; @@ -699,7 +688,6 @@ const getHarnessInternal = async ( await Promise.all([ crashSupervisor.dispose(), serverBridge.dispose(), - didPrepareRun ? platformInstance.disposeRun?.() : undefined, platformInstance.dispose(), metroInstance.dispose(), metroPortLease?.release(), @@ -782,10 +770,6 @@ const getHarnessInternal = async ( testFilePath, crashSupervisor, appLaunchOptions, - launchApp: async () => { - await ensurePlatformRunPrepared(); - await platformInstance.restartApp(appLaunchOptions); - }, }); await flushPendingHooks(); harnessLogger.debug('app is ready for %s', testFilePath); @@ -806,7 +790,6 @@ const getHarnessInternal = async ( await platformInstance.stopApp(); } else { harnessLogger.debug('requesting direct app restart'); - await ensurePlatformRunPrepared(); await platformInstance.restartApp(appLaunchOptions); } diff --git a/packages/platform-ios/src/__tests__/xctest-agent.test.ts b/packages/platform-ios/src/__tests__/xctest-agent.test.ts index 8ee9740..55e75a6 100644 --- a/packages/platform-ios/src/__tests__/xctest-agent.test.ts +++ b/packages/platform-ios/src/__tests__/xctest-agent.test.ts @@ -68,20 +68,43 @@ let buildRoot = ''; let tempProjectRoot = ''; const originalCwd = process.cwd(); -const createLongRunningSubprocess = () => { +const createLongRunningSubprocess = (options?: { + ignoreSignal?: NodeJS.Signals; +}) => { let stopped = false; + const listeners = new Set<() => void>(); const stop = () => { stopped = true; + for (const listener of listeners) { + listener(); + } }; - const iterable = { - nodeChildProcess: Promise.resolve({ - kill: vi.fn(() => { - stop(); - mocks.kill(); - }), + const childProcess = { + exitCode: null, + kill: vi.fn((signal?: NodeJS.Signals) => { + mocks.kill(signal); + + if (signal === options?.ignoreSignal) { + return; + } + + stop(); + }), + off: vi.fn((_event: string, listener: () => void) => { + listeners.delete(listener); + return childProcess; + }), + once: vi.fn((_event: string, listener: () => void) => { + listeners.add(listener); + return childProcess; }), + signalCode: null, + }; + + const iterable = { + nodeChildProcess: Promise.resolve(childProcess), async *[Symbol.asyncIterator]() { while (!stopped) { await new Promise((resolve) => setTimeout(resolve, 0)); @@ -257,6 +280,34 @@ describe('xctest-agent orchestration', () => { await controller.dispose(); expect(mocks.kill).toHaveBeenCalledTimes(1); + expect(mocks.kill).toHaveBeenCalledWith('SIGTERM'); + }); + + it('force kills the agent process when graceful shutdown times out', async () => { + mocks.spawn.mockImplementation((file: string, args?: string[]) => { + if (file === 'xcodebuild' && args?.[0] === 'test-without-building') { + return createLongRunningSubprocess({ ignoreSignal: 'SIGTERM' }) + .subprocess; + } + + return createLongRunningSubprocess().subprocess; + }); + + const controller = createXCTestAgentController({ + port: 49155, + shutdownTimeoutMs: 1, + target: { + kind: 'simulator', + id: 'sim-timeout', + }, + }); + + await controller.ensureStarted(); + await controller.dispose(); + + expect(mocks.kill).toHaveBeenCalledTimes(2); + expect(mocks.kill).toHaveBeenNthCalledWith(1, 'SIGTERM'); + expect(mocks.kill).toHaveBeenNthCalledWith(2, 'SIGKILL'); }); it('rebuilds when the cached build manifest no longer matches project inputs', async () => { diff --git a/packages/platform-ios/src/instance.ts b/packages/platform-ios/src/instance.ts index fb237f3..95fc96c 100644 --- a/packages/platform-ios/src/instance.ts +++ b/packages/platform-ios/src/instance.ts @@ -156,12 +156,16 @@ export const getAppleSimulatorPlatformInstance = async ( }, dispose: async () => { await xctestAgent.dispose(); + console.log('[ios dispose] XCTest agent disposed'); await simctl.stopApp(udid, config.bundleId); + console.log('[ios dispose] app stopped'); await simctl.clearHarnessJsLocationOverride(udid, config.bundleId); + console.log('[ios dispose] JS location override cleared'); if (startedByHarness) { logger.info('Shutting down iOS simulator %s...', config.device.name); await simctl.shutdownSimulator(udid); + console.log('[ios dispose] simulator shut down'); } }, isAppRunning: async () => { @@ -245,7 +249,9 @@ export const getApplePhysicalDevicePlatformInstance = async ( }, dispose: async () => { await xctestAgent.dispose(); + console.log('[ios-device dispose] XCTest agent disposed'); await devicectl.stopApp(deviceId, config.bundleId); + console.log('[ios-device dispose] app stopped'); }, isAppRunning: async () => { return await devicectl.isAppRunning(deviceId, config.bundleId); diff --git a/packages/platform-ios/src/xctest-agent.ts b/packages/platform-ios/src/xctest-agent.ts index bdcc0f0..8db351c 100644 --- a/packages/platform-ios/src/xctest-agent.ts +++ b/packages/platform-ios/src/xctest-agent.ts @@ -187,18 +187,6 @@ const shouldReuseBuildArtifacts = ( return fs.existsSync(getXCTestAgentBuildProductsPath(target)); }; -const createProcessStopper = async (process: Subprocess | null) => { - if (!process) { - return; - } - - try { - (await process.nodeChildProcess).kill(); - } catch { - // Ignore agent shutdown failures during teardown. - } -}; - const getDefaultRuntimeConfiguration = (): XCTestAgentRuntimeConfiguration => { return { permissions: { @@ -244,15 +232,156 @@ const waitForAgentReady = async (options: { const waitForShutdown = async (options: { processTask: Promise | null; shutdownTimeoutMs: number; -}): Promise => { +}): Promise => { if (!options.processTask) { - return; + return true; } - await Promise.race([ - options.processTask, - delay(options.shutdownTimeoutMs), + const timedOut = Symbol('timedOut'); + const result = await Promise.race([ + options.processTask.then(() => undefined), + delay(options.shutdownTimeoutMs).then(() => timedOut), ]); + + return result !== timedOut; +}; + +const waitForChildProcessExit = async (subprocess: Subprocess) => { + const childProcess = await subprocess.nodeChildProcess; + + if (childProcess.exitCode !== null || childProcess.signalCode !== null) { + return; + } + + await new Promise((resolve) => { + const cleanup = () => { + childProcess.off('close', finish); + childProcess.off('error', finish); + childProcess.off('exit', finish); + }; + + const finish = () => { + cleanup(); + resolve(); + }; + + childProcess.once('close', finish); + childProcess.once('error', finish); + childProcess.once('exit', finish); + }); +}; + +const stopProcess = async (options: { + process: Subprocess | null; + processTask: Promise | null; + shutdownTimeoutMs: number; + targetKind: XCTestAgentTarget['kind']; +}) => { + if (!options.process) { + console.log('[xctest dispose] no agent process to stop'); + return; + } + + let childProcess: Awaited; + + try { + childProcess = await options.process.nodeChildProcess; + console.log('[xctest dispose] resolved child process'); + } catch { + console.log('[xctest dispose] failed to resolve child process'); + return; + } + + childProcess.kill('SIGTERM'); + console.log('[xctest dispose] sent SIGTERM'); + + if ( + await waitForShutdown({ + processTask: options.processTask, + shutdownTimeoutMs: options.shutdownTimeoutMs, + }) + ) { + console.log('[xctest dispose] process stopped after SIGTERM'); + return; + } + + console.log('[xctest dispose] SIGTERM wait timed out'); + + xctestAgentLogger.warn( + 'XCTest agent session for %s target did not stop after %dms; forcing shutdown', + options.targetKind, + options.shutdownTimeoutMs, + ); + childProcess.kill('SIGKILL'); + console.log('[xctest dispose] sent SIGKILL'); + + if ( + await waitForShutdown({ + processTask: options.processTask, + shutdownTimeoutMs: options.shutdownTimeoutMs, + }) + ) { + console.log('[xctest dispose] process stopped after SIGKILL'); + return; + } + + console.log('[xctest dispose] SIGKILL wait timed out'); +}; + +const logActiveHandles = () => { + const getActiveHandles = (process as unknown as { + _getActiveHandles?: () => unknown[]; + })._getActiveHandles; + + if (!getActiveHandles) { + return; + } + + console.log( + '[xctest dispose] active handles', + getActiveHandles().map((handle) => { + const constructorName = handle?.constructor?.name; + + if (constructorName !== 'ChildProcess') { + return constructorName; + } + + const childProcess = handle as { + exitCode?: number | null; + killed?: boolean; + pid?: number; + signalCode?: NodeJS.Signals | null; + spawnargs?: string[]; + spawnfile?: string; + }; + + return { + type: constructorName, + exitCode: childProcess.exitCode, + killed: childProcess.killed, + pid: childProcess.pid, + signalCode: childProcess.signalCode, + spawnargs: childProcess.spawnargs, + spawnfile: childProcess.spawnfile, + }; + }), + ); +}; + +const stopProcessAndLogHandles = async (options: { + process: Subprocess | null; + processTask: Promise | null; + shutdownTimeoutMs: number; + targetKind: XCTestAgentTarget['kind']; +}) => { + await stopProcess({ + process: options.process, + processTask: options.processTask, + shutdownTimeoutMs: options.shutdownTimeoutMs, + targetKind: options.targetKind, + }); + + logActiveHandles(); }; const getErrorMessage = (error: unknown): string => { @@ -416,7 +545,22 @@ export const createXCTestAgentController = (options: { const client = createXCTestAgentClient(transport); agentClient = client; - processTask = (async () => { + void Promise.resolve(currentProcess).catch((error) => { + xctestAgentLogger.debug('XCTest agent process exited', error); + }); + + processTask = waitForChildProcessExit(currentProcess).finally(() => { + console.log('[xctest process] child process completed'); + + if (agentProcess === currentProcess) { + console.log('[xctest process] clearing active agent process reference'); + agentProcess = null; + agentClient = null; + processTask = null; + } + }); + + void (async () => { try { for await (const line of currentProcess) { xctestAgentLogger.info('[agent:%s] %s', target.kind, line); @@ -424,11 +568,7 @@ export const createXCTestAgentController = (options: { } catch (error) { xctestAgentLogger.debug('XCTest agent process stopped', error); } finally { - if (agentProcess === currentProcess) { - agentProcess = null; - agentClient = null; - processTask = null; - } + console.log('[xctest process] output stream completed'); } })(); @@ -446,8 +586,12 @@ export const createXCTestAgentController = (options: { ); await transport.dispose(); agentClient = null; - await createProcessStopper(currentProcess); - await processTask; + await stopProcessAndLogHandles({ + process: currentProcess, + processTask, + shutdownTimeoutMs, + targetKind: target.kind, + }); throw error; } }; @@ -465,12 +609,14 @@ export const createXCTestAgentController = (options: { target.kind, ); - await createProcessStopper(currentProcess); - await waitForShutdown({ + await currentClient?.dispose(); + console.log('[xctest dispose] client disposed'); + await stopProcessAndLogHandles({ + process: currentProcess, processTask: currentProcessTask, shutdownTimeoutMs, + targetKind: target.kind, }); - await currentClient?.dispose(); }; return { diff --git a/packages/platform-ios/xctest-agent/HarnessXCTestAgent/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/platform-ios/xctest-agent/HarnessXCTestAgent/Assets.xcassets/AppIcon.appiconset/Contents.json index 2305880..c027ad0 100644 --- a/packages/platform-ios/xctest-agent/HarnessXCTestAgent/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/packages/platform-ios/xctest-agent/HarnessXCTestAgent/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,6 +1,7 @@ { "images" : [ { + "filename" : "logo.jpg", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" diff --git a/packages/platform-ios/xctest-agent/HarnessXCTestAgentUITests/HarnessXCTestAgentUITests.swift b/packages/platform-ios/xctest-agent/HarnessXCTestAgentUITests/HarnessXCTestAgentUITests.swift index ea45440..b40d5d1 100644 --- a/packages/platform-ios/xctest-agent/HarnessXCTestAgentUITests/HarnessXCTestAgentUITests.swift +++ b/packages/platform-ios/xctest-agent/HarnessXCTestAgentUITests/HarnessXCTestAgentUITests.swift @@ -4,7 +4,6 @@ import Network final class HarnessXCTestAgentState { private let lock = NSLock() private var _permissions: PermissionPromptConfiguration - private var _isShutdownRequested = false init(permissions: PermissionPromptConfiguration) { _permissions = permissions @@ -16,23 +15,11 @@ final class HarnessXCTestAgentState { return _permissions } - var isShutdownRequested: Bool { - lock.lock() - defer { lock.unlock() } - return _isShutdownRequested - } - func updatePermissions(_ permissions: PermissionPromptConfiguration) { lock.lock() _permissions = permissions lock.unlock() } - - func requestShutdown() { - lock.lock() - _isShutdownRequested = true - lock.unlock() - } } private struct XCTestAgentHealthResponse: Codable { @@ -44,9 +31,6 @@ private struct XCTestAgentPermissionsResponse: Codable { let permissions: PermissionPromptConfiguration } -private struct XCTestAgentShutdownResponse: Codable { - let accepted: Bool -} private struct XCTestAgentRequest { let body: Data @@ -252,9 +236,6 @@ final class HarnessXCTestAgentUITests: XCTestCase { return jsonResponse(XCTestAgentPermissionsResponse(permissions: state.permissions)) case ("GET", "/permissions"): return jsonResponse(XCTestAgentPermissionsResponse(permissions: state.permissions)) - case ("POST", "/shutdown"): - state.requestShutdown() - return jsonResponse(XCTestAgentShutdownResponse(accepted: true)) default: return XCTestAgentResponse(body: Data("{\"error\":\"not found\"}".utf8), statusCode: 404) } @@ -318,7 +299,7 @@ final class HarnessXCTestAgentUITests: XCTestCase { let sessionDeadline = Date().addingTimeInterval(Constants.defaultSessionDuration) - while Date() < sessionDeadline && !state.isShutdownRequested { + while Date() < sessionDeadline { for capability in capabilities { try? capability.tick() } diff --git a/packages/platforms/src/types.ts b/packages/platforms/src/types.ts index 63dc12d..d11394d 100644 --- a/packages/platforms/src/types.ts +++ b/packages/platforms/src/types.ts @@ -99,11 +99,9 @@ export type AppLaunchOptions = | VegaAppLaunchOptions; export type HarnessPlatformRunner = { - prepareRun?: () => Promise; startApp: (options?: AppLaunchOptions) => Promise; restartApp: (options?: AppLaunchOptions) => Promise; stopApp: () => Promise; - disposeRun?: () => Promise; dispose: () => Promise; isAppRunning: () => Promise; createAppMonitor: (options?: CreateAppMonitorOptions) => AppMonitor; From fc5a014fb54fd8f4e5cd214930f5b963fa60652e Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Tue, 28 Apr 2026 11:50:46 +0200 Subject: [PATCH 09/33] feat(platform-ios): correct dispose of the resources --- packages/platform-ios/src/instance.ts | 30 ++++++++++++++++++++------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/packages/platform-ios/src/instance.ts b/packages/platform-ios/src/instance.ts index 95fc96c..1dd99b0 100644 --- a/packages/platform-ios/src/instance.ts +++ b/packages/platform-ios/src/instance.ts @@ -131,7 +131,19 @@ export const getAppleSimulatorPlatformInstance = async ( capabilities: [createPermissionPromptAutoAcceptCapability()], }); - await xctestAgent.ensureStarted(); + let agentStarted = false; + try { + await xctestAgent.ensureStarted(); + agentStarted = true; + } finally { + if (!agentStarted) { + await xctestAgent.dispose(); + await simctl.clearHarnessJsLocationOverride(udid, config.bundleId); + if (startedByHarness) { + await simctl.shutdownSimulator(udid); + } + } + } return { startApp: async (options) => { @@ -156,16 +168,12 @@ export const getAppleSimulatorPlatformInstance = async ( }, dispose: async () => { await xctestAgent.dispose(); - console.log('[ios dispose] XCTest agent disposed'); await simctl.stopApp(udid, config.bundleId); - console.log('[ios dispose] app stopped'); await simctl.clearHarnessJsLocationOverride(udid, config.bundleId); - console.log('[ios dispose] JS location override cleared'); if (startedByHarness) { logger.info('Shutting down iOS simulator %s...', config.device.name); await simctl.shutdownSimulator(udid); - console.log('[ios dispose] simulator shut down'); } }, isAppRunning: async () => { @@ -224,7 +232,15 @@ export const getApplePhysicalDevicePlatformInstance = async ( capabilities: [createPermissionPromptAutoAcceptCapability()], }); - await xctestAgent.ensureStarted(); + let agentStarted = false; + try { + await xctestAgent.ensureStarted(); + agentStarted = true; + } finally { + if (!agentStarted) { + await xctestAgent.dispose(); + } + } return { startApp: async (options) => { @@ -249,9 +265,7 @@ export const getApplePhysicalDevicePlatformInstance = async ( }, dispose: async () => { await xctestAgent.dispose(); - console.log('[ios-device dispose] XCTest agent disposed'); await devicectl.stopApp(deviceId, config.bundleId); - console.log('[ios-device dispose] app stopped'); }, isAppRunning: async () => { return await devicectl.isAppRunning(deviceId, config.bundleId); From 1ce2d8101c4b20249cbf9446b056ec69e49626fa Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Tue, 28 Apr 2026 12:19:51 +0200 Subject: [PATCH 10/33] feat(platform-ios): use loop-based alert detection --- .../AppIcon.appiconset/logo.jpg | Bin 0 -> 107504 bytes .../HarnessXCTestAgentUITests.swift | 12 +- .../PermissionPromptCapability.swift | 105 ------------------ .../PermissionPromptWatchdog.swift | 67 +++++++++++ 4 files changed, 68 insertions(+), 116 deletions(-) create mode 100644 packages/platform-ios/xctest-agent/HarnessXCTestAgent/Assets.xcassets/AppIcon.appiconset/logo.jpg delete mode 100644 packages/platform-ios/xctest-agent/HarnessXCTestAgentUITests/PermissionPromptCapability.swift create mode 100644 packages/platform-ios/xctest-agent/HarnessXCTestAgentUITests/PermissionPromptWatchdog.swift diff --git a/packages/platform-ios/xctest-agent/HarnessXCTestAgent/Assets.xcassets/AppIcon.appiconset/logo.jpg b/packages/platform-ios/xctest-agent/HarnessXCTestAgent/Assets.xcassets/AppIcon.appiconset/logo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..fbdfc3f1daca6b9571602dec984b5a3f9e361fb7 GIT binary patch literal 107504 zcmbq(cQ~8T`*-Y7o3!?xv5DHP6+1>G)U4JFrDm;aQKKleVuYAUNUWBi6s2fUGh$bZ zs;#Q1R@dwM{r#@@djEg#^E`h%*Llvl&gYzaoO7SgzlDFR01iu23sV3U6#zhWQ2_sz z0m%TG{|fDe(EUdk80hI47%nj~U6f0lmsl=cJe=%Y>}(gW0Fav-C?LYm$1fr+E-tO2 zsB-NZoa+C#gMY69oQ$*pS^y1|IDndyiiVTw-ylE>aPbYGruolc|5vD}X&9Mk=^5yl zss6A0KXtTp^bCyumH;gOX~#jsaZ$C#|Dy8}hM&;Z<_)jEt7|Sf3O8}l2=A0PnUxA} zrGtG5tHZ(3)9+lbUar+St=B1j7Qc&&q>fX`9{0h3H|N&FTn$J-0qJJzQ!``fHU~sk zu7!kRcvA?HKhijWoDl3tVd<1_$4E32ArU<3*v5{)B0t*m zJ88$n$TUa8X3!MO{+@sW3k4(HwHq>TtCut}HoByzZd^J|xqdu3s1%6s3uqJ#8@JcE zEMq~Pub|~(r_j(VtN~T!`ITl=zMzPw5jo?XyP@FtwHtd{_M_$Ag^RBJ4 zX>DMZ-+#R#&fb1*8{gvXL@0NR#x3X-P zv6#7|u!7JiVRMwu4UF3AviKYP!^VJ6i4L!~AVM|)WUN(8JDX6AzhfrE!K4VbG@Zz0 z-{Li$4)a>*)b)qZL1F`tcLvW~t^6!}U^n4YFH2?a9fOw8<)}q|XJtZ0BicJb(Gdo4 zS8Q{!Lf|}X+ey&KuD;7}w>uOTKSa$y)2v)Z5Ah3EigK3*V!uzDd~btBbQ#Bk@WHK$ z@)mwUe$S|VEKGTOyu@}YU=#Cl+cXu&QLlNaNtQSSzXf;$yIm4Yb|!LdKr_S^!xasw z%%|1TsJcoUxkL;=1k9Cy_ZZpjJqHVcz(!<0mbm9TT`hayZaq5}xs>Rtdfg^nxxntw z6zacOxIYWkDVm%0X@A^(7Kw^$#kuJO~xM!S=jobB3r?y+%`*V?Qd+SCR}VcNXP3zy=$tmmx+xCL%uBS|yUj_Pl8_$$kXJ9x_xsmz*?HZO3z|lAzdz#mEZBnyq$k&0>=glIcezgDC#>(S7=E8qU`FD@m_w__N%Q&IK)SqIHE0k&xzr9xa&YY)EcwBGqD{BdXJo@)^t z9*_P9vTe;W8>5)~0`7&36L377-Xy|s$ zx!jx8)^sN{^7qcky3V@sBm83JZc{p^$=xGTE}Z=ighgFP*LKrFJz|7f+CI@nE-`u+FYKO~25Hxoz*0 zxz^>GHt)zT$Jje?k5e6q$bc+UjTsldIU>JCk!};X*a3h4^U(O!aun@ zixzPwz~LkV@!=gZ$m(>C@P$O+*lSPJsdP_)tw3!KTDmBU*47afK{{rT7c-4m(zZ!Z z4|IH+ls03Nafp1k7xZE;VD@arG6nL5IKJC{(@IvnacgNC5`e>N7h;xaJ-locGEpVl zbbfz>ei-^A?KnNt6|7VHX$w;|LI6zTk3>oRqRbiM$FVw>d|DQXla4v0$~bBr+M(Jj z+pW&1n$kmym^l=1x1o(pc{*!pS?90xJ(f( zpN8}dTLYti0FC?7qEvY^w299+reP#TEEGari4jk4I2Gm1Kw}@vMvA^=nWa4=!u&}Y z(_sIp;2GOSw5?Iq8BTr)BoYU`4yk3Iwq&g_>d`4|HbjvqymkVNO&u19xo3@YWhZml zcSG&Y=VRtwZNO7FchdWtP}^M82~5Nlc{7Nu@BoR4s5d$Dc^|MXe_*B)(fQsDx`iXO zS&o4C1>3}Lr&kXCJdphdxO!hcW+BDM=E;g`XI?9wsVd)_c#AayD`a42-Ul4da4hiN zO}yNuv}CkGkC4&lmyMT0em53uS)`C0K9KzaPA1N8)b+kbo$oS}O@pVl8Y8b<_@726 zWxp5?VrS3vc+@PIoZ`OMaI%L7k%{t+5l)tSin;*=p{H&dvsF$(aEG0U+>-FwJs#ovWs?aSGC8R#_v#eJYhU?#wWrw z-Tmr#>o?K1g72em10j-R*Ou9EI@1x#uxaP5Z55HtililGw9%v~q0^vL9L1#qb*7=x zmV1*to{#`hgQr~P4B5uk)e(UL?_rI}#KdduLZYP_yiaXVOz!gg^ndQbn&ybgvk2~` zF2Pp&_=2@B6!La3$|gOc!y+hL=dMvLIYo$Py3MYKpOO`ebcu&RPiHmU39z%^VDMG+ zK}Wbd<6V4-nIUX8X=!biXeqAMsE7wc)&eK{h1WRvT2_)8b%WkSR0p&(R_0GP( zZiuU`p2$11%slixo24EdW<6`6GP`Z|9_UdtY2sY4U`NrND-JCe-w|;4_GY`BkfNqLP3c~Zbu(0|_dH5- zduq!Dg+F%+kB`)`+6j8vC?{Z#Q6e;*cgn}Yv^?*~!#MqsV^a9&?BMWgQJNJ$D^~ZE ztcr|iB^Tb01j{xw@W6x!8tKfU35f>yBC5`^m;PPT0VR%m}M!S+n9T`X)HEV9G$ zd>`k&*L|{w1RX}GE#q8opGCZAS_;B|Ng0bvICmk*N0&}Bk-Ja6pi5VbE1>f_?)y7RbEZB13J+G{Nfhs* z<@bL(he8DxmeL9QFLNrBHgU-AxG=i0MmXQO-``#j z5K-8od2>I5{2KEIJv{YwmVXM%T_PLbVA#y&?W^J2AmZH_Hz4}r2na_wWW_Duk?*gI z{Wu=J``N>Q{j$jb`G?^G`aEu1_w~We`n$UiK5}jTJqw`eZJ30*bm}OC&+X>#RMy_v zkq>~fQ4TB$hyjct)$@85(9p?=x7RlxCXp!_1uk$u@2sm3Myx$g&So(yP?4IzOJ{Lj zs}rYmQW1UU;%uGtV`o_SnvB(i$HsPTZ1*;ho3cR2=;5cAus9{-Tsf;KWOogmnPiWW zrqE|$!IQi3>mOaG>knlHc48qFA*UiQTUvXjpAsX0&cJy?(;e)!dK!FrNssQ64H=H_W|y&-w$w(TBV#yhF6+Nn~@Q-HTsq&uhC!;5!Ec(9y`??+ z1e&+sw0c9g(r)T1#V?Ql{8ptA1Zf08>S2!QnT@=6sJH7&SH=jl_iAU5nLSNTN4BVF zOn2hXY)?Q%mODr6^_S;2Pj1@W%O-}qgDR`ut;|}@;+vmIx80cx>U~sH_2T@d!VkJy z%@eAP(B`~~cY2<}(b7VCzvYYWoBmxnNu7L7UU(URj%!*!Luim2rFL5aqi&Qgjv%B< z3Z`Gm-k&k?rEQ8iVY2!?`88NpJlRcUUi%h|%@k&!TC$xKq-He4B9xWWXkqZth3h-cs;SQEMVp3Ed{bHTdYXL1!7BT^ zV*tCr$WdT5yT#dEv%iTSh<=OpP=bRy(7CjIv%@j(+{N;H6ea?n_ua*6Cyrz5qsL0} zKY+?NzH{va{Fmp0c$?K*`HX4H4HcJUIHm*V3|DMPxGvj1+yxlsxn4f>-23S@`1gjz@(YpD zK0x#b%chhbnGgCB=M2?2VHDivOO`;hc_UD!$62`=t?87DJ_t1LkwuZe(=FWez@%JQ z>;)W~3pm~Cn>R1u#IwpP_MUk+tkk0qD=#+@BenrMrbEI}fzX8{FOv#OYy&=GpfE!8 zO*|dw;v9TmyVE!)18#y;DA-0DWEiZ1?Y-?KAbg7^ms7_7avlf8*;7>l_MSeUc#+>U z3fgqR$Vt~^&Lxv}IbaX(Z0631be|cY+|faN7v5Ou0iV}KtBXu-HU0w_eUv@Xx$d47 zGPB%V{QSGELUhHL-iO76-)Aa-qwWg+F2R$J_f8&uJcBn+*AZgfAN&IZ3v1709-938 zxjlRco~aSi9sPP5Yfx@_G==Sv26`^L6r< z@>Cc47e)@cJ3D&V1CnpVX}m(&D?)<&?gIt^- zDJM%Hi>ajhlq`XcadeOjydlZ>4!8I5qh!6`9%Ban0G-wShYh4v0r@aFP$iCL4|8{3 z*Z23~$5p`a|Kuh0*sn8hTakO_XRbr0j~UXsUvD22u#bfUSTxbwUekX*ksnr`!G1E` zCI5o{{y6{J48Sf3VAJ}QX7e!oVcO62Kl3613g8)k(POQ?Lq4hUglx5jPsB7!k9Z=|6&@BW7v=U;mR57z(OEgtQ&ql&z; ztZc$`yleW2$(6?`_kD$H&yrOoF96`{)EC{_r}V}9x5Fofraw_1wI=!R0Y|T)?na%9 z9i5x+%Mar-{v~>*l{Z^Fb2?l!hYw#an z;asOB+x4%^L64wc^TVHql#8U(Y!z_cF@2WY(zDd9)WnDD+r`Vv+q-DUgDiX;q{DX< zMYcZXfqmKRL#ycvP!j2msny%JiX{tY3UF6#^q&^i#9L9v+R|cBqSVM?|=SD;x->rze%ma>R?gjW3n2SG9sTn^1g zhqzJ)pT9^D*H{(n>2I>jEo+GsZ338nFqHjR84*rk^Q0liY99Vdz8)U$Ic~cjhZJ9l zGn`AAs3+7bOq}l--84M^ShRTf=z7Zd_2uiBm;^m%0Km+(OlxwV4#$)#YRw4r%f8M5}i^)xGFy0oH_bU1RkWwxv} zbUU1K3)B zsq8(ndAaZsgfzB^)z#X1W$|Y&#GgS05V^AOG^_TwQ*V9k`pk>XJ)EH9F1huju>4>_ zSSTRcZFcQCeqL28W=uZLo95g71e)WAR>$>K4-wx#!v0JK59)IMjnjSlLu*4tgdIS2 zbhAxJWb*LSy@zw`hky8x@{5hUk*ZgmoIk>dj^P6>*IvH7_cE7}6R?-|{L71oy%CA$+F-e!=FSJjf5+3Jw>~|X zEC2xbj`H(2F-E`5dc$X9S-*cdV*OD2Q)4VV^&!LAqet}qE9%8pUQwF|xp4=N_ z_}rPmu=U{Srjzl9ih~dJzy2PdOrn7w-;z#sm(JFIY%^p4_#&N_CYyh?O#$O?ZhU-o z80-{?tPdK2)U>~(+c#_aGWf7O#^BmD0Nb+{H^SeeC8~sH9u&7^6b0V#O2SEl>r5Bn$wAZTW7Ne)rMz9sdL9bP1k{1f7a}`0hO)k6gOxc&&e8JAxwHNKR7>4_(W)Cm@tbN_QQ{H3vC#$Nb(ZJm| zau$Z}xWtWo3w1R?R;Q%q^Ya)97q<*^VNt(jYkn$E#!&iZNVf<5pO7FXJS`69w_N0K z1uHf%6@E_?aJ-iz{E^a3-v)r~#Y>^R%;0XW2h|MLW>z|*BY5O#_IOvA+TZk(9o>@q zf}7WzPkOnBr8Y+LjprW7bV$AuXcCr+Yx_PS-*HCznKZ@RW#l!#lUH`y=cOH2;Ks|&sX+Qh9un3D ze%xD7@M2t8BhX^g?cHKa*o%DiBi>h~KW5l`<*4gRRbQad{m>Bu5=6l=hd~U+A)N+W zJC3_>>+eWk^;Q9Una@A;=yZLsi4Q1rv7&d_vzJ1YoRCWJy_36{HT$nxze+@XdC+*Q z&wcVrRcQ|WAcNutc8ah&bEsXm<45vbX2!novviF00oqU9IB@XENN=yTSP7P`#JptO zcWBP;{|BJzqxrlF*!>4c`H!)mJVcQG;@@OMVz$`=@@_a^f?97sn~X9klCzQbE<4s8 zN9d6xS+0Y8{sHvAffX8|=83!6SC||EAy?NV5u0Bu{8MvO?vqQ1H`=bKy$Cwf)9sY* z_5^&YkdU$6@=}`A3f#M|b$*DsJ2d!`*}^sR(Ay7UEcWEYx$gcDG}6s}sa9j5c$*Z< z9qQPk91#Y?Q}WKZ$G=QHZUc*z)}YD9AR>(XwDL!EZ1~xP$Jestjjg8}4;T^{c&T-o z*Csz)WBnsQ*>&n&1h&a_a|s`mO!z((dd4%CYdyY**1qhHIN?|9o=&%BB&~ zxZJnw8;U2K&elcSvyqFP%k*oh48$CtD%M?8y4qv&vo~w@DYBf7DggixjM^|0`o0ru ze@b%e5hN!MR8=jBov?Xq`@f_RWh?hDZf)`T3l_WWsyqSjAjA}eN9%i|dt zR9cO?s10_JPT=peRF|~Ei%a(HzXx+~Tirx|xBLTCor9RGK(Hz{w>5&gbcRjgT;MpS zU_RhI5D9FulDOqlm|y7^@?&x`vtlHK?6%aX@S2|H1)v_9vmeb_L{;KklE?E~<1u)7*u8pla`*RHNZLrDTO9>2U zKx(WAwLbdS7W`=K@7qScX2dtii46Hz^^d}#-EE&Qy*^(u6=8*rT}+VA!{O;O68h`A~)?~frHhXFz+lpZ zB)z0;Mw7yI@0Rr_I?6Q+M&n|56~QsPehO<$K90rnfp&rgoA-#LUke1KhCY!dL|+$-sNfpeh;TBI2^E z1+0zjSB-fi$j_r;XA8az-`K66bOZYQqetBYlYa;T+PO^o@H zdnCAm$W#f>7c<@={rZNF+M)J|*43@= ztO)llyj4c}rB{L0UzoAR&Z<8r= zxsZUd>6cfD+*b@8-0ik^IbE^jDm(Cor~hYt2kQeS1VU}+MK`eTx#b!Z05!b&TspVy zoeKf=M(i!Z^1hF83}e{)fiXEzXAxZ;$?DIX#LVs71|sNI_a4*v0= z!){e+exayt>+PCd?m$>kx%nV(1w0uWaC`1G^&+nB#ir-EGr#HQ<096G22;r{uv7*V zt+xVyk73OZ1j2zXVx@p6obfcF&Ds&(nGxpeP}Bn<=E?O9#aY!!W#*O0TgnQ~-9|!a za?IP@CbeyxbY^+SA!AbV?-Vsg@Ja<(QW5a2T%v~ToGG>4hgc)qYhg3=5KHb_Ly$Vy z#Aib+KC3N+Mh+LqQ?Dx5RA3Xz5K}j5tacRMzMmnyU#}ymF2=w}vr;%V5*+E?-)Taw zlpEJKfd(Xr=hjx`A^+?)wPhv+GQAH0@0-| zHaOjBZYCp^XjJTCRAo}HQrFZbT}94wC!&R6(r%y+d| z)n^Q^wO?x0$*=xg@8S}`ZN&tHBiTj$t?f3^=9kB7O1c|jwtX4JKxO=%$5%HBYPn|0 znziebh6-ReQ%*+v2}$Jr^*?I0y{`?GJESR0y*P2SRyU_$JgthqeU{4mGi zhqyhkJ~d~sus3Bcz-7WB^ER?i;&#DjV|OOX7Ycm%^By1mBXRm@5ijQ^VNWEhz~Nfe5c6@a22%?X*)3ywnA=TK?5Elw zV5@-J%24mKRihNL+#*FSP+Ydp|Cv!gX_Zh6YE?24T;OU*ZGhUd1=Ly}%G~~=4Exrb zyla^u2h_5XmoAxBh?T^E`B+|tGjxK-=`mJ_v11>Ph||!i``9Iq43eP~f!oo~d8g}r zDNQkb3s4aqC>POg+>12kFI3gTrJ55e!zaK)`9>Dj8|PCAH(Xl zE}`+b4fG8Z*eBFs8q5oQr$i&$7HzV$h@_{9*|Dtm(upw`%X{%Um4p`ZAN`|j-Bh>> zNe4*#gk>jHXC&|DH;Pw1DP*a(@O-+BRjJt)g%>P@tAbK8_IS6D6^otR_Nk;-Mw{R|VSP|N=W3Q!>kW?d2$_SEWs-&-jA zL`mp4DcMc|Fk`)=hppjqiEYK&6{k2v3vhG4#^J1mE76j zn^vIX*BLQ+Y_0f+YHae%j`d2|)h$!pGHhNe&H1sJ6{4gDnG}v=u2HEAZ7^Ai<95iC z%WM41uG3OLdA9u-=sOyI!!P6~$EQii*F!XIUuLct8kNbCcy77yWdAR=R86HrDIg~E{CCuT(w|tfb}NN2XCAuG0;T* zO?ao>)pY>|O*NaG!lZyS*%`G=$y*x8mH>$N5)i}B`k;9V#K0549E=Eee`3`Gy(uT* zBA*`t&l0Z|vS>wDJAjRD2v{=6dJb7kgWA!BJWw@oI`xt`w(vu)!~GeDP*#4fJ@lnw zf+GaULe*`Y0&ca;$+XtRQpK0xQ`-r6XbebvifYbqqIzO4|X!#a&Wa@WdcZ z%>^WimU>K`ib{vFOwv+T~Z~v@sa3_&qh6O z3BI5$ayr`ZfgQHlU7h z8(ztun$d@H!#<;(7e`lsu|<3_l%MNS6|zHB818W$sp8b?RW;CM1ki4MOYS-B7D^hM zHwA)mQ3})3>_bsx;UHE@&Rh~8OMNP}E~HJ=5ej8;^l?l5c6}~Z7QH>*QPBR~!d-ID zM0VRmei|4L}vlC{z z2pm34P%we5if3~&g+2*PhL~v)66rjXItO!UY^c8tW7ltZ!?fB{$VQl2Sk1NGLl^}gkob0G9Q zkZinR|)URGK*fq%6%aH-Z}XpP>#ZqP4LFF1tVe@?KWVQ=dLX5i~E*$Wz^WzM&Ofl z*|Y=RK`E>{Kv0LH+&rK^Uk-1pn%AFSViYD}{&>#LYRYbE%N$73B8htCB?VjHha{M8 z!5$g$U2vi`I4*e40BR~4T52k4>P!D2K~r-AxWs7a=xG@k#YI)Q)eM1GI4<)_NXw{b zT-7v^Qs=oKL^EDAqM`#-*M)A{fb`$@UHvR6L9DSKEVSq-{ths{+?`2rcnUFN_Wzq3 zh+)Od(;mO2`Ne>pDdXf6w6|&5Mb#d@LDNDF3q;jDWEnpREf%aNCABm?W#I9+c`oH) z_m$1(XqGC}eH6HwQ_aZaVLI`kg`e21)x~;f&%RgpRq85SWO^pU~QFcp*182O2Im~^X-RNJ$#>sV;C)I<^IpqXR(D93KzV*e_sVnpeV-*f6mEnzGnLFQCi;Qav&7Zz=>il+ zWSBxcEW}5{-!eH6?qBa1&6oxDFP4PWTiYm%tL?HgP^>#1tIcI2g$s%^c3PN-KVGWn zNwVjZ8JQ+d?k7%4S)KQjXeKEP(dS9}0tL4XsH?aIql*eS$ASy-c0&W6IyTJWdQ5wp z3leu>m#r9U0I37#e+cZd#Fsm^v65@7)`uRiKjuW-Np-f}ZBKmUd@6MW2HU8q5_6RU z*c#3~Fc1fEy04xdgqc^om1PNu!B4H7wUeeJY9xPr)}J*wW=s$bI;O)`Qat7s9fRJ0 zb2p4p3T*i?9f=J`KA(9o2?n)q>+Wf7zEEkA@zm<*vpVp=h=bM)cgCh%?s&$v z^yJ^M=|>FOFm(w6gvv#w%l28n!zBe(-Qp6LFzdyl0Qx$;4C+2_L=D>rbyJXME`ry4?gy2bfAjQx$?TTFIaPU{zn9sm?9iLD71myZ~4qk;v zcII`qV-|S^#DM_IWz~^$iZy<-U;7Gd`Qoy0^{odJO>^t$ZPQcOo*N&#M z>x{AKLqxkK2ZKv}M~mZMB}PN&e_*Q;LGV?Dx9)m=nf7~*o`({k?GM0iRb6zZr;-ey zB!8%BH>Ij8t$;U9@HG_Th@+w1b@)TSg)-N&qkR%n@)S@|_h`911o*X5nMracIXyY= zX8k*wN6RTNrmDbC40Tb!+G24B$^4PIRhcL0mqOvA$powP!RjE~(R9DE&|F5s8@hGs zRhXG-H20GqqaufoLm8W4b{ZnUHPxL=P-6t2J7QfnsVR>#>v{_u&T%|H$r;$iTc+9U zV{eog9x4g`6A(J)&@{&U!HjD{8kEBfI+}lK_IMf3UrFO5+49cI|Eo^e{mW|hpIV0q zt9#26(G1qeS(CI;mSaI;PKjJ!Y^`8kS^_--6{I*PoH>`_UEh+K_n%wTdyX6@BieHe z3P6Le_ud-$wwwg&TOfEHzsq;r{&bVs;5y|TNv`E?4sb!)_x~wyuVv6)n0C6c&g4_( zvabpieUj#wiHMVS#3!xkxvy`mE0;q-v_~H7mVI0Mj4QPT(F_6;iWynuECbKN?d?`U zZ94>uwSc@=1|IKvE12R9gWqrzMFSQGNMwnmog_hKZS1qv(GXJ4TR2SabuHfbRj^32g09ul_jgj9jw5$%8dzw%)nuK@joanlS10_5 zK}hX@wPU*fxX2?MPE!wG(P?GZ$X7;R@Y$UDzBU1lM>Ur-^Z*8l4#i1^icguNt1Qxd zR=r#YgrT<^aENO>`+3T8%*YsFe}luy&X;2s89%2F%r$8c9v5fz1kWlqa-vBYTelm> ze)QU<*C@0^dX;gX!K{2tb#p-S4Og0a55AbnFFg%moxPCg=rxMe1Q+{lXe0SW6hHk2 zLv;v40dKD;9mKf7L3TgQ;$)ST?F!>ZX`WNI)IzAWRI_fUPH$cvQZZrxCl$6wRVPYr znH_LQ&|~x+OyQW~W~S?B<^LKm^D!Y?;+DDx)M6Btvnvx5lMihhfYkyutXYL1-h$jO zAH6fU9&D3QHc6Rfy~HMc_3vo6UcOY-VuezrKYgCw-;~(ODpcdUrD%S!jBu(*XO8!c zFG&n?zErf;6Y2(+26vz4mPFFs8MuDui8g99l!w7rzHVDh&%QjfFOp}c$=t5&Dhei1 zxk0lcWj~k+6I2Yw;r>^&ny73W~WA$0YY-ZAO@ zHZj`x)b#Gq)!RzVetH0XOG`H1u_DmX8tP4!#P28ldb0`9Q{OzbXQNq?iSWDM9p7C{ zYJ&M>ZQyQO8w5nebW-U(nOEhDcU_xx=J(uHsAzPI5D|1R`|U+LCCd=F9J0$T`;^|V zV$Yt992OUn_GD>$+63_A@yuhi^IM`!;S(k>^ zO)y!zehC!%RwH)33PBC)EBaur+)4v|)7&9!(|~PqZVt?!Q#hJ3#5GjyzM6>UU<8RL zD7=t^0Y^eLBdEhd-ME{%wHRZNY!gp9U-t_qHnJqa`|i^2H>F+ePNXg|f|$obB!83X9AUnM@NzzU3a2a{dsngJu2X;_51) zkBEV*XKqy9X7&xqYDLt-^AlQ#FEQZ0J5+;!bq`5FZ<3zZ%H559>d5(TNYR0)gz(%q2Y&PiFT2aGM$m zs)c_Go5*(9$rIE}v4`K~qZg=>c4FY-mJ;?S+y*e=6F{C-APnwkC0hT9v+wJ|MXw(I zl0_ZS?7Z8`XXT>9sF_KTeW%2#YR*Y5ZP|ocLBn95*O#3HIPXjZCyHHi}sv z(v)&DHR`y}b9QYlM=SIVZe-n0g`dX}di?9whHGMfvQ)_Rs2Lx;36;BhSx(b!VFW0Z z%HXntSYS65?%MSrF7F%m@?%Nu8c+@J_ zemlFNeqIpmv)ZDX@PlJj5PZw#hB1B&eJE$Q(N&A+VN#Md(6hN00Lyhx%%|=aHk{$_ zg!DCV9_gz;y1ixhnGnuizkYj#>aJ?dY3O#iw2cL@k_N)%Fj`DM@ZfK&0idY%nu=%D3`Lh<$;?jSzo6aN{~4iSjj- z$AA5pk3Jje);v9HCo`HNE1H~G7As*S`&h2hwMp=~uj3QG46Hwo_hqegh@SfW<01M; zv+O$dtG6NUAJ{_vro+%cSLyaB4sFp(a+S>o_hc8vGZ-{XrA{zAJK16~hg@59mn|JL zr&)J`H2H=08fjIkno7 z(9C2B6t#e80Q#H~&vRsX0Lkn+6Nf%V`9NB}O<8l1=StIB`c-Q;7#T^ZOh0A^pdEZ_ ziQ)17=UIK3n-1i{tw&G2?W33PGeCW=`;-r7NxmyX!2Mbt)jVkqj&5REwtS&kWgyXY zy)k>8YEVi~&Zx$;MXX=FFY=w^F#wD6h1yiEXVfIiMf_Vp)Q;|Xm{w|`&H#m=ax|Hk&z zsh_ZTueZc^Wi1Od)9xKptnU&_%^O|&>bVz!-@-~{WB^iuaGDu!ml8c;$qs;)U89V? zj*VTce(#zcDYs4gn( zWp<%lxYH>;N{g!|!(z};w`GY?D1CjoAvx0kIyIiYlR>1bw&Y5F)Jw&bXqS63@fI-V zd{v0e3Ndat3gzGJ(qmiaBZWI8O{y4&{ynvu79OSb-MRl%PJ3{|%!Wo?*E5YKT7*zS z>04A)DHrOb8YCDg=Oj)^A=i1?E|)=SiH|jKx)zeG>QVx0kS3lzWrB)+ydmlfyI1;o zdXGanjmOnSM$7WDo*2;{m(Q}nV)bbfY_e{zhduQ@Q>#2|mXqt5@i)Z3(>+63eel{5 zePo~$;vqW6AoN*-tGL#YRVsvYXuWvdZl7B%pdj(FA@j6sg4`cWlBbvj&mgI8KYU0u z;nctoca*?lH5$|{NwQXyr$~X4TxZq+C8M& zoHqbuuU_pE*gA#AFuE0Is_E2Jer1z3sun8ZXJ9q9iKa1GDx)*WK`b$_Xes@mNx#(_ zB|1@~zlo{^n5Q^-J|bMhhj;jZSQ@6Gg&w~L0~u3d9*xr#7Eh$C=H88H2B+N9t9mY# z@=V`3H<&*^BP`W+0p#1s^x{%a$jMZp9klXmW;a{6rNWNhh@6Lp#%mWIk6v2`Io+0A z_`XqD0W8o%9Qwkve>U11r~kxxRt$gQhDe% zsj+Avw0eAFy#ZD|=RL}wWvi~$X-!bx>0%n|15RHVKBdlP8NQ~|9cDMkA7Ny%_NdOZ ztQOGjLrdk9mVK`^j|}31!*ic^KJGARHnh=-&3MefRK{glnFllF9{xMlWI&P)HC)*Y zltu-a4kwp=W!@~2%`4S#e)Og&F;nVgz}3`c@z~P4(wF^nQ*8i+cNHQoZRN7?^bK;tboF%f!2|LO)-YyP5MKkeCFhF>3sc7can~yuFM9W0~xIdI{9T|g3Fk~}OS+5aKy3PF6 zy|V9=vBcnY{hvFG%o)d*|ENSW`FK3U+OJ`es*c84c|y+>biR!n(p53X>@HI)tVvZT zU_mWmBdd0aLoXk`v87uKK;L^a!EE(N<3SNed5kadg(%NAIH2z~iPjkIT445cw^4i` zOue6$PR31`J~J#|7y4R~g&@s!vaYi7cVT~dMp&_O*!>6+b+10-7TdE+f#SV6j-YQH zRJ&CWK_?~(TVFWysxs%2Z0Fo%-}~6a0Uctf@zb;`>FbKo)xkz*DeXG#b;qw$kJ|kD zyI{=`k7%7z?o=hStEMX47KGT!p>F#HL3CRJ?bbwv*A#=eUKP@LaP^}{TC7^dtOY$Hzv!j(-zd{6=!EG zWl-iV&9QKsW;@PjWaBqyGR|V|x>O9Qw)?*0q>e zo|ISI^elB=Kv`0PuywuWZ<&R8tjO8XCZe>fb<+kitbMzsN@XUol;r=z(p82v{lDGO zC0&Yihkz2&jP4jMhzwL(rKQ;@DPg44q&xl5A~G5QX%G>V!RQ=a+j#cBuIJsZz1lbK z`#$GBl zt`H})$;ZCx9zJCYrNGvr9)%VV zl3*U<59?Za1fxAPCGQd$sga*8nIxtF{&=$r7t)vz6hs3)3qS3pz4&Vh;B=w!SE}2D zb~AJh?QaT?14Mc99vZCM{m@A~Nz-ak8$hixKKo_y{U*j&ytHm=huiO_^Xv35Y0RIJ zDLZ}sKY+wSGF#5JQdee;t{_RxPbEw~YV>&oVdbT-YAaql$^yH_+39@xSaLb-#&EI9 zPJQ{H0!p6R%sdSyUQJO>Gbvh1!TZ;!Dmp)+tPRWZ?xmvHp3?=JFVP>mxYgw7M{78$ z`(uNdiDxVYnNK)6x@3&^o&9KZsB$29+vs>ky3_JsTUPob#W7R?gLeqnn^V%kB?C|$ z-_p5uA7k;}kZZCx4N;qMEtz z_cp_H9M=8zW)ouFWx)d=Eor8VDnJgpuW00cKPMacbka^OC2m}?oI{w7E-qR@#es2< z-^y~6jJ$s^X(1ar8$W#CGYTrCj5pI9#$>={^gnbd)2DhS^6!YUdvfji7e6to?kkH8 zS)f487(4JZ?)6OF57;bLuR=2!TGHsjW3p%({KyK-%{6sETQ<6*0Dwd!G~ttvF~xyG ziKrp?xBMm*JX^~EaS#%pQWfICm(nlty>-T*cdv^qO3g~}KGCq+oajKB^{ZHGIhluV zgNp|%WrIb@t4x{|FaCXQ=q+bGBciy!gIiTmV)qb4SpDnOWgkRhz&haGtw=Qfj)$OWGtsv8;gQb=~6!{4Viu>1vg{*Mb436kohq1 zbqlig^Ds#m<*6n0mf0FL;mLxAgP9OXrDp1O5ujPny`RFjmMEi@ZvfgG-HYV%$?QZIX%&@$fHm0`o<5pIJqCCYYWTCa%A zmtxSW8p&HKz0n4emz|rtUz(r2sMg>p6w~ZE7F56=q&spnd7Ecw+MG|3hU`O2{BD&L zC0u#_1+(_1bO%cu%L>FUBz)Y3o(dqH5(%2hr3D_LxlH4oc7*tP7R36dODIY=JH&b% z?g^y*tYK@1z5Z2j#L8|LTCG2>$e~EQz+wBi=x8bpO`K>z7a&%3U)8ymoHBjSrHaM< z2X~UG(HnP?-?MKt6;BxQ?2vQcYgT?YkDF8!13nr~a$w*YyiV$$aofXLs(?reNVfz- zBQ3i@&jT5HR|e*p=$sM?PQU<@%%*kTx0G#RB4v`)KDCf|rot&oC_mB7Hsg54CpV}5 z{@=f%Gdu$SN%Lii+hkF;s=v`%D=7i(lCeU~qerlrY5wc)-kX_hMOx8(dgg;3V>d&E z*RlP_A9bAQOy^L<{^p$iI6{CN*_A18OQhMWZ*tNhcv{t)6#?v#YsCu|8K1A70d~so zr591mKWA!TaiZb4tW_^zR-PA{`v+oIOrIhlxBAdKG$pHa1bopO&&0gCPOEAlE9CmQ zS6n~~fNXM<)^%Hmh7YAp1T>H^0vj2FR0^pT3?h+fzOFx_iwX$*iJ))}=w~ASdi>@} zPcd1fD1(nJ|C&?yCkTEuQycjxpvAsegL3W1n*qC@UYS&>QO0PbX7x_OyLWuitej7wzV=7%Ev82zD)aIc*YSp`zB=2jCTFmGX)f>gl*C0vJ!e)OC~Dmz%WIEVv=b9>yg-=RU@oO{F2%?gv>h%EcAPE5VsjuqT_c zw#7Fmsq_Gc*RRwk$%86K3Ej#|=@Ndg*2SFMBYRE|CsHu?=Qwf4Q|7|WQtSgEqfXuW zx9DFl4Xj5u9i?P{rMs3spA;*(iCrzB1i7_DOp=J7_2@tyf-;|~&Ff?p3W+)JIq54b z%l;}^o!iO64k-#%m?*q~3^p9u)aodU)6Aa4FW**fMRK~IyJ!)SbWah|x^E!W3 z*OrshzJh3TcPGbo-pB*5zJnF zAuczuCrw{Ve>1J;B-m`NhDnG=kW8zc=dHnzwtjZwCLaOe+W++0f!^Q_4`v4*-56?38=yu$I` zK?Md5g5HE`WfasF%uLYr214(Xc%qouZXNB{!TCMzyDz6!m&}a|L}}nyq$M(Vp;MWT zut1`1FfCdm<^pBdTOiF-^!8E3hr**Eih-cit2whxar>3{KxJCJDayQOPF>j z9kH$J`(#`EqCC|5Uhnz`R4xJ3knFKDf}#1RlE7GUZNL7>C`W)`R@Cc{pX(WK7O0#^ z)tK+OX|z=4?Wh|}%~`HZTEwhkOS$jg=UNo@ZfJ}Y0R5Ayw1*Teo2`k&Ur)2hsI_@i z5qq)_@|Z-W;+Cafa~dl={F=xr=t+}` z0z(!DkRsg2`xBHGjv;7!V*x$ThlLoxlq}VY>>T0dQSQotO(J;M4Z3%Cw{+ffPj<$8$UIKsHCK)XW4)Ra zN(F3-CvoJhcO2lCm?_zfO_j{O3wilqc}ba4#d^h2B6#t75&9%3<5s|o^~3K)`!c#- zpUM$GH6Z{fs&2Zn>M4=NTh|Jkmv2pkqFro+o!QSt>b3PL=z=2$IT=%mSlz5?9mq{D z$Jw+rYic^miAxhj2nOrD)9D4|DMmpp01J)10&ZMh)7NTYr|0s@ti*9<*@6Tg24Xj( z0Kp)iDWr%n|Hpp;$MG4t{R+|>X8-Jcx1X?;r>HlQwlgO8ub7La=@@}!51wV6@DLh_+W#gG=>$)XqL1jv%84heKi(RnoVr%@R$*bg=@mJ#;v z56}T=H0S@O&aLH;4`?v77=l_~9gZ2wYwZ3zUPQi0J{3uThf>^BO=|?^Q4E6K?0fIG zJk3p&=cZvJO~@Bj{uSNJdmljdux~c-tDr9dvU3&?q-~zUcFdN|7-nh{Js;o#d_&`` ztdxYxuult8?s@a5pY2KgT9q4(_q}3f#ihZCU>*CEs&f_nf5WCGg^Pq8b<%2HRsxoi zvxBefIR$xN((;F*GIZQOMwW4^eMG6yfId_GBh;Y!is`kFflP<0aIgQ)CPq*(%3sN z7Zn%@r$epT#|}I}GWL0YRS;x25+U&qDPGk{VzyGBcc5s!nN4~k8zS;*a_MVPyb`P{V4QR+SV@6^|#%%nH33g)7qFU7Nab zrdCs#cILu;w)KCH;uz-uBx$R0tPd3pF2dk-#j%#+K(N%qc)0JNn^c`+v5PVF<5XGU zdEti-EN595JM3K2#!XC;TJqCp<(Yr4)$|WA>PDAFC!fT9Abr+~Ojc9CEfdi-kMQY_ z`mJG^-6INn%2d|}^HyFoEpPvCI} zsLt)?Fp1cnubs~R&s04u4;oBL&A9YEfBE~uZg})@{^1IObMZ>^l&cPAh!$FYcGK8{ z2tNCIj)>usy3xO$L?MBS!O9GnU;r>MzaFf=Y_D(*IKQ_u*q{e_DCzV&0rqw|H z+t6d$EUeI9(*ZB=whFEH#bo^jte%-48>=RkLmnmiiQ?nPPFd`IT@v}?UVkfxqS__P zqO9Q3zPLDi=m;6q(0*L89()!Y#{Rm&$$K|!ZsgAjRYp&V%yL%T+O%T9U04We64l-wM!OBjxQ?(UxcIn)I1y(%(YQ*? zJE1hD`?AR_Uy?n^Iuf}$<*qJ3Tucmn1?qjgvg7lWEIJar#}i=NGg~s(uH!je|RY%>mF>P!r0gNL0lntaN?+o%Ik{4PlX=c`P<*k)X%MB2i2OD+0kewcZDi$^d9^8 zhL{JrPubSl+Pts^Lkf||vn=5Ptsv_r?!!o@j*fb=eQJ@_gr8WmeRQ5iJampj&8Ol` z6eV@P<7?>Y;^U8+R7DChu@)>pGxi-;c5fTIuLDoJEdi_O47eO+prN=AH(Obg#L)5^H4V)~ofTuX>LWZr75_rq^5- zbC7k&QUv739iR;pF&y5Cx{y9-3lChBY{O+whuv;tX^XCFI%o)Mm!gn;Fn^h3LI*c# zEYkw8?618)c<@qnZlQ5|m=`ryxPkXAehlG~xXSLj4Xiy--KR2)r;_wMXZoU;t47A^ zKCMoi$r>#pd^$SP7@YuTU%Qr7XY@C!;w1Ohji1RK6d0j5dz&D@_#@KkbD9WBOIW01 zd*f9l6Rm)V_1PPB+c!foZb=x17NG?5S#6A+S%}J%(qL%z3%42 zmF2?5bJs)QBv$d}4$uivyaanx=nv47lNE@F3^2=z$e~$60PoHQ;(z!uA;~}%<6Fnb^Qy@zm;o7 z_^YnnG`5GY-og(4wJ+WQ#(<58h8xqm8%W~~cvaUO1G~_>x&!0kV>PeE0}j%C)2Bc-8MiXk&=#;5 zu@ojij^glbyl6KQ0qDTbL&Ao3A>&QnW9Bk^+(CCrIJg5md@{C)usEh|=fE14rNus= zTpw}&L*-s&pIuK}^CMa_L*__U+gevWb4qKo4fQ3TyO!5;AOT2w2e>u7x{ZhQt8PLr zh@%~30LqcS?*OJkCbUpaisomO9mnru2U;yR$NKE_$KI`C?5|@nl?MyQ7{twk!jAh! zd0#)?0sLw$m4$He+k{;fS#}!t@~TK@D0=0he3;W%#}Y=Ta93I169qU`8Z^V@UJljf z^f2821ku{b=(A4NQVbZRKmNS*T&JrR3Sg5uf4-`qqJAPXCa;VUWW<|thfhoQB)UKN za|sn5&#$T)V82H8Pz>#bF`Qee{c`B>rnkw{h9%QTm4vi$63sLzzGh~c*9Fo}VrFrw zC?SmWA%5=tWd35$A<#RMEpU%t-F=hc+ydJ#s!XcPFiSznOq!be7D&}M_s0%HPI;cw=>!C6gx9cy)|St zk0#H!i{A45WIHFel+T!>v4eYr>@p-qt(Y|Uv1WXPy>px*kM#|;zmWe1j`!ujFL-#vC4hd9(*N06R&b}&jYz%QAM^`4oKG&K9tLgp?DaHwb{0d{iE612&%}u^4 z-jsx11!FO2O|Y1M&C>vuAbE+CFTl=+Gf{R6|NTr|vLm2a|5?gGJ=@A&6#KyEMRk#f z?PIDf$G7sE3D#`ut-ASPh8pO(`A8RsRz69A*HSiZ2GDs?g>M5_dF#Kr=^luDi$a@9tViJWcpCPIZk7J^8g3 z2ow(HS+*r2{7Jyab0DVpBe7P4dbX}>nRJJUjPt9Gp2%ch02k^yCGKaJXpNW_jNbvT zv)yY@WX$RB7s`#5H;J$7ng__w!~lX)ZDdmIO+Hq)u!Sp}UtTAgF^9@(@lfN4fJMtD ztR;W2n(04s5)nEyjh{QJ3s5FEWO*KG0!a>jV2@EgzO=ZyMc@`MA9HmOroS%vR3cij ziuxQoSo!oVyOl<^q=}ASPa{E4qCRZ(X&?@mb7uO}fsibPc>5pY95zKHr)rDXgb7Y$ z-$S0IK4C|kNa)REVbAKC#Vgq9~xNz@i;i2Q2f0%Q8 z4NG)`|M$oSCW9hKWdVi%+H~JLSfA@1fDOVmp>;Hq^{3bntv}p62?$AI;o&Y3Z5_3U z(^8QH&#UAgWKVp*h)6oFJp%qZ0WzuX{|89~2fmuQSsC`~;#}g~Lr-Sj3Q4Hw)nomt zf?2Z1z;RRfY)@L$A-*`qZgaerMb562jn~&KHs*6`$e(2G)RXJ@M?Aw;tO5|$*8-biegRP3@dJ0{0 zpcm)FCPZy()>&1AsYL9iQ@>~#q$4dq83eGg6d!A4;cUth{hPupi{$+Vq;>s^2#`OF zTk2zMbZL#q+J1buaEWC^XWRku@YfGMC^s1;~o)>Xj zK;m;}vX*E&+(TWQ6NJl$_R4kn$7F{9Z`3L~Ycl+*awvRqj?E;T4pORHZ{;O?;;N*QU5MPQ%LSTPYNOj@6^g1JE~T(r8Ku2o-pC>c z7&fHjT~*R>Sa})CljqxPxgAIbXN91QVpR>Jt%gC)|E&qzwZqt_wE2J(TGx%om`$3s zt*l-X9-faSzXg(+-h$oB z-Np+2Y%=pJ{7{AM`~MooClft>{VT>~WL|7LB5oHuhPS=5NBjsU>j{s$TBzpCi~wP= zFz`&MjF=)#4hfUIN1`9P6psVBe&XkC18FRI9>pX4oKDyCNZQcP{%rPoCc%iAG#S~U zwU<|ApW0eP^o&~gaMuUqJIra8-B4@nBGUz_r!NNY<~9(P&VLbBh!y_Rs*xyo{?Gc0 zC0^5|5sp?n2L+<@znwx**FT$mzm@x$nXx?(J`z3dJ+b)>8(ImvO*)4#UqQZvDJ=od zRqgDMw{(SVXSH zB3-FMNhNrya(k6LVdgEt{&2c1H{wT50J;JR=)2S?e1`F_8o+GpA1eUJY}uSkjX&>HT9)1odYU^e;zj zVZN>10XQxu6GDY>?T9KHg0(( zBb2r2HgH&_`3k#o+jzj`8KH9Ol^r@ffyM5Oa}x3-!ASiqayC?=svwlzEwCUQFb}96 zk^Pk{Q>LLPX*!nEaM?nap~luC-Yr@WG|JU3OOMT?U$O}b)@lgWQpf6~2?u}wTKltF zg*#}iAUF5EhZtcZjAGiqGFWOeF@ZCHFunLqmJ)eM^wzhs#1PkTIu%LXg&UZ~vEjjR zBiajk2OulVEx1%gU0OL9MVtTS2-!s^htrzQonyg6lC3ycPXsllrb6ftyl&Ie{daim z4j_$lKU~HmTl_tEk88kkA;5v(DKET68#`5JA#9lSOWi^eOwO(Qsq1z(VjG8;zC@_b zUh?dnwgr*m*bvuAnD7DMYN2n|ZR{er^$zg%``)4Hh$;~p;(kRt41rDJ{0_Lhx>YV; z9m2kcPh6A#hBj2YGB|`h7yS5JurtM7a`m!ncpCc?2h2nOZeTeWp?R3a4%Xs6otpN^ zh6G-%^zY1aOQ)v2V5di+awKiC8s$bCDOF_00c({^D5@ILQnR(p%`BHm&3#!ww0mbu#R02X-mf0B)V-IOk19a)@> z6B{gHe&`i&N8#|YCXV$|WU>(Bhblmwz(Q{YE+Bt!a~Q}K2DZ-m_Za4KyK&k47q=7o z^Whk}=<6-7`J2SDBiT{&SkJKVFV~W_$EJC=t{qsp&QLFO2h3b`GeQ*eY6FMpz;R)R z&h|07wpLdH%pdrx{-glEyr(YGE*m(ZwG4s1(;GYzGDm#1{D%JB zGCeV0IE#w^@fPhDJux65r7+~g+Y4KS6p1C<_Bz|xyOSk|w2#d1zFgX&87P2|aO1l! za9p$S6C_L(wxm*%C~B>3_L`e$Zm)4Wf(~PdGX3!CqJog#X)yN0H?LWHt^ec5->9tkoudBrR_jlDC^W-bB7-oBl%- z>P2TJjO$=$QLwvDI#erhO;0rPHEFU}M)eh@+w_#n@hWg8d<`>r9#K1uubqaQ#=x7u zE`|Tj|7Ju|&^m;ehjh220-J}Mu%$Sl_~oQ7s_@}8Vgy&YQ7C>|@|y4+O#tW5Zgn%^ zN#XK`qe6|VwOpsPSGTZAuXb$3Ex|5Ua``dQ)r;XuU?v!XD_ywhteh{>&-hQ|w{Cu< zsR*aJAd-gWB+R>qv0(B14)FW{`1}HPg4!vax>^}k$=CquVi2;OlVg;j_t8D}-Nn6x z{_ibIX^+~f{CMeNavD(eZ%;}ZP(a-F1o2H@sUuCKS3 z9oTA&!NrQ^4mJr@Fnx?D!-4ZI?*K^{+GL0gd#3!(xNMJk4_|Uk1#L_y^Y4W=?uvl4 zaew)jw#7(ut$xo$nXFv4uYCMx+fgBoHGgKa)+8+L_ETn@cBuvh><9!cOGiZNiYI&V zh~)kbKK|v}{UUG&r++-80ybE1GjS72pf!fMLU6-FXgn=H#;o9>7N({0lX#H2P_8aL z$?v$K1a6MUGAI^mimNw`%ciKqfAv^kYk2nU!Zv1M1(>~|`}_Ab+0;#3?T-9=4)p_o zZ=b2Sn6O(Oiz%zC8ncQ>?baG%@u>2G))^P#cWTeK;gT4~YW^wvK{p(<`9ov`^lw_Y&MKHcK9l}O&i&z_MJ?^Bj73UoO@67AKW|MWe+v5BmtZ^G_T8dE_-p1uG zt0o<;41Nd4haHg2*J#sQc`ntM`rQVfH=iS@aKP;D;oaMnKgA#%lxnWuASjdXg@A*H z5Ag7O+@=WlVG{QekI};J0E-dr8xbhZ^H7<0ftqFYp;2Kr}HbV4K;qaNyTFz$ew-W3QS^ z+Od_+f1MFl*uPs75$l-RL)~B8-X~)k5Z_vn+9FR00rof3Vz7hW)1;sHD1#rfiGrxx zvZ_nX3C3Gv{gL<0L5sDEBJ>WXpCZ}l+Td!%n`=;jUJNudjzuT^;V7i6EixbQH*`>X zH-a~O@TM>?jPVNR9;%D)?bvA|Wo5Yh!F2x4ixAeUg`+J*co_~Tb3tpkzQTO%HT)ft z6!y{gy(6xdM2~GS@Z90kTjOapB%C9iN+nA<&7 z9`v_!6|loJMF%q8`@LC#feTD#6_p-rVaYhkoy@HusU%<3rRwT>MBv^{+4s=XT%x*VZ&*iFe?=inq9 zML5p=QW65-Hl%-^^1dENx?;ews%CyJ3Qy{1i+NUO#b6(Z7@J8$wDRrOV`SoDT*yacG+mf&d-^+rdrFbbSEB04m@Cd z(0WRxH#OgfzdZ#JOkP+&91wmgI+NcrBM;Z!B*w{wtwk_j%1xh_E}TJN3s|z5(CjHJ z4g zv?|uTrIX|81LC_r*U#1nd-D5r=h~FElre9L?6vRF(+Dzb0S=QUM)Zn)#=;(Qig|f! zbC^9@5XwY)j+g+rl^OLX2#*J(*;pxx{){QyHpB|p2@}c|o#{-lO^exlwzg?Dgt5K0 zH1b5}PUhpuuQxvG{Sn;S66^T}eL8dQHGPhF5V4H~PU5IB-@SCFZ(zEIfoqe!-Tqdet z%G|@H|IK~i@g-gshz##5zHa$8P7_{(ryz)T5jJRifro;BVxzBa{=q8GIbj>Di(EFV zxQm8in>o?>K}P4NAY{#D=2HX(*UA;nh3AxKDoI}Tp2ZDlJ?hZg`<-(?GcsB7wPl!J zaOdg|lUh6ETE%g&ov)Q_Z>shes9%kEjZ!roWu=cv-_-|ayxHc9MO0PufPLQ!mu@*xG5))mGZj3$_re_kR0vefola>}FmX!}#``Oi z3UQA&xrR^s!JkD+8dtv+`T$|wx%(^Lg07pF+@?-UK-TVaAn7GScj2tNavBE|xteSb zYyOQp!v1$D*#;KjB8>d9Cn$MuqN+Qndb010=M~QN;VGR~n2fn}pFXR6bWa(v(xeR3 z{vB&iN@qh%if`??y?|?CmtF43kEzmnWlJj~JJKNfbo;6~v8K+*N$QpSyiloCoJyGJ zy9Y0-)Hd=s^Tqt_WzZqjMp;6Se|^!{@uI~UZiZ*ECeMQc&rh}&4PflQO zaNyAHIedRVO29<_?na;rb{+n^@uBf7YEV9`O1d({{_h3Gs=ll;IdxDw8FTR~bBN1|Es)Y+@s+F^MfSF53>aE zsJWp3^kyZW`A=eLS}v1-vC!|rZ{X(_!y5&Yhj@YJ2tRPby>w^rRel27PsRXOtb#gL ztz>4pwf6;abk_*fb%EEuf`P)VA;Mr<8C>V6h9+PgvGwEG;6baD|>;=rcUd6CIC7QF&aLA1^qVDU7|Dhy!2PyN-5VKB&>v z7UziVL5}-9eDmNi-62YHukXu3K<}u}ChBvx>~iX+Bem$1VIsA$xESGF1~RTq;3a8F zf3NUC>K92f9fxt~LlB`)EYvzwavxZzKj=JV6KI?32<| zeL_Gp-CQBXn=k@aA)0^%vqQ8yvZ}GRe};r8MxH^^=+)0O`<~UWiryzd>NEi!11ME-Be-sNn%{(oBS z<NyBABa zcfQWYR?hX5*Cr!#t~7~V19mfd2T(DI$mh|KZ3F|C80GGk^FL3Dq8)PF7CC|&m9)3L~6>1Q6hQ7+Y2DrbQZbqqv?7URgwEeg_~ z1F@u~8s~bL!&9tj!#;m`9V{jG@;xiU^}BD{Lw#(D?FYVw`cuKzZcj{YXc*z4fX7?= zhVJ?U?*q;Dx@}X(Lx$gY0uW%OuXK4Wlv-o68*e~M(-q~&G4Q9?L`VX{=o}f zJ}XH*Oj3i=SFVl6 zlI?iyOnOfcxAtDf%^(#7(h5@Zp?{mG&QFnQcGzKN?E6A9>Prn#vUmyAVyS%4W|7LD z(*ZVhfhuO^EJ{8@hE{I?64(1onLbU&FBW7f#WeH($v-$D*uaIEV>FOhe^E} z{%dM{#MM7{h6O&KxgEZW*q4_`(Hxdwdg6A($l%v6hX9@PDwl%IgDxgARgz_DZeH6^8^)Bng^VO zg20aZX?m_uG&xlc`-AFHtK&1UXLsOX_agXRL@@geS1ZIPynp`o$yM&5>>-!_8N&1o zXp9lx<4VU1@m##ObzgscwO5)(h4gz4YMFdDpnvfnzx=Zdk00@GpL3fex3%bIapZq%d#VcS9mtAd+KTxN_k~LT}uS>3N@U z9kl9nQX2~;GDc1bvU*#^yqcvI;GNWHZZD82aho(?*Sr^(PODs8MVd9342Y4<)2SE; z3Z|5osZ78o3{$h_!q)K7uoe&jnh5m>&0DYD@NauK#GcCRo<4vc;_^nv$%wk3#p3an z@aGdh{*ag%FkAV5F7|fwcy%p3(yGRs2}~@kSQZB?3V`SRD-Uvre`rDfE7q+nbgD|$ z>PMo5{C}erEQpsz20<5AeGh(FB*_kC$_60swx0%Sla%=DG zBlh+v7HVW(cI+z)J(6l?vE$zvOmgQ-x#s{4!0xz=+P5UzGR{*n&ZhNji`t1lh}_NB zH?$J)+_`z_`{|cGQ$g%W`&&*=_Q=1w&piHW2MlH^N{7$53-t~eX966xkg0Xbm7g8r z&p}%ibG4$ieQeKYtf`{o#lAnDe`=<#o$B+BrAjv6wk^SfG4CYH%FX5FBjUXQ->%QE ziRYi>SGflv&C)oOnj0P^@TcdjF=VS{u@eQ#SG(|-Wa)A)c1*Vld#mps3SZ|7MhoQ~ zMHUgJ1X)?VRhIJa5sQXP<>+SYeD7yGXRT!8p%?sav^1|}Rwl0_w%f&T-~XUKu7H`! znF$nQ`?W#X#&T<$)&Cz4lAapdJQ)`spI9nVCenBEWQGJcwM%|FM-;nYEI$^;BTot| z7#TEh1NIbebVeD-OOeATA3qe7;PKZcF6sht z2`9^ikIRoRrp+>DLvx-QY>-HS7_*c`JbeKK??PnzrdC}sC zg+)L9;Q*~&yD8La-jc-(ZGL|o(u!*>$s<@=HMf0E*Uak4nZMgqJN$JBKKD^R{#wn! zm?+5mJRrBB^bcfe$Lcoj=Ih?n@or>_h=*7V%Gl)dtH`2NyVF}_RqI4f?+fRd3`H8LXQajjzjwt>~-^e)*jC=ao!W zmHaxki0_il&XcqwO{1l&v>EH=e<37&!<(qsYr3!J*m(#@Ee`Ehi1l%hWPk9738g9r7sk5! zP&{HX&n+xY=q!a69f>8JwZ~e8a%hoqTY7uaIa+5C%ZQMsl99T&6F^&sAiuHV`&5e8 zTOVBPi_1KA>)G9jHz*|orbu5IBp(c_vzc2)%M`rlWY$=3sGyv!|IdVPZKF94EQmMP zPEfds7g4}vwsOlgJv!v&)Fa1UBV_o(EIzXs2<3N)(XpPPj13zpg9AoLqJ#gFwW@lH zn`sl1UrGt6ZTEw=CUpgk-&_Rd!>vz=6A!?|z23v6w;Q6M6sO*vE_`@jvKalu1$3F*}gyBx%VE%*x8I zCH`mDrM- zm0;24gP25{h?(29u)=20xqb18wPL2U~ z2lA0>lNC1QnI~`XqMY3|#J;v{oTz9)7UiJro58h@qnE}6XAAyn={-yi;f~Xp#%3nYFql-#|-o935v5(-dQL7Z*XK=G&+I2IQ2TbV`H$-N&FQ z{~>>i=bvNeK*~!SSiQU)^R1rN#f~=cgH722{Gd_CxO)D?=6G8UiHxFM0A!l`H3l+@G8bL zbxyic911JRl#jk%jvV04W^_|g#UJ17oHKE(IfGi&8!v1e+eRjv`_DAMct@e8iX)FhCultL@ynI>hESYKS`kt7Xoq8P3s zCd~R5X-Z}6V;vLEd5gFLMypB1N4y7yre>GJ4YDe)4Y65kWMM8eo~8cGpU5``KW!WxQ+Z{wQhR(qqI`3nC!-ZbXx zdx7Ze>YfIpfb1U%XYHuyje31p2kG)zpr~&*1FnUw_0tf3i7eU&~CYcXBHa(7hPb zv-#k^_laA@gr6c_xWWgX(lbhQ3l%gQ#B*`1dzo5^Tc}K0nYV6|orvmvGvpR|?bhY! zZfLa!Z_b9oRwkNW#@$(gESQ6YDPX??J=bKyvYA)lFsbP8 zG?PfNO?w7YpQE(2XRZP1*UN8Jj}Ot1jFUWq;!v3&VTCkC*K$0qSjRl3ad zZrYCSum9u*N%aJ5vLw9BuXGkD4vs&Qh_V}8o9eS*)Md!zpi3D)Np}r+<+okQ{NGJWHKb7X$+-C%N1ZNoWH)UwldQeuq<*%TdupaH( zNr(E#-T}ZQD3z-kqOeh|kDN-@@E+m+1D8N(za>QHYZOU?zdHACP8Uj`zzax2#sF)z zRt0FqI3^kZ9G=)g$$B8XL2T%orT)ToGgyO^O^{PX`8dk*Gt}a!0nVONDIFI=>3mq& zmISyfY3eK0M$kZjkR-PkjvRaz%fe(7cqHF|kb@yX4!!ss-DDI3YF>2&CqWVMqGhqQ znBCp1e_ZmFP4P_Dh3mvB3I+w87NJxgB7h@P!}P2F03Go(B;Bhzw7b+q0WppS8T+j3 z;8{+)!pDZ{x52sRl8gPT&l{(W*0_P%Uj~d(B1kyAM!2lNz@1(xL|pT&FrX_yYiVdu z7%(b?&^!+&^#zO(+bi`;*2kmImG6n*I_M6-ngJ%IpG6IkV#D@-&Qt6iYQ5Y#Q8 zGHC(XIy`ZoBFL?yM{WjzvQqGNl=TK-4xMGz&b-770ieFVttt+j1ep-*JP~IF)$9`L z_aX-Q-Y`unvhgWg7l@>!fS|2oPL%hY0pz30FeZ)}Bjp@f{S_P%YS%y$AKQd} z%F|h@%#l^V3IOFdseYnNQa&?y6;vu@vg8&B>pH1!*rKUNoEN|{?#wT71`wc%6H8op zM--3|W#w9!AgV``a-I~eiVB!KHIX+1mKca9eiY!+GIT)V2Pwpjj1DJB))5<<%3}$| zDQVsqae!-g`{2BvGKk@qBPj9l$)S}qqA!OCX6M zA}=xAm1TLRr|gSNa7i7dS>XsK^5Yq;=Zjp!dF2H}=qLGF6j;?x0<4;a@s#4>*O_9V zi(KoRWGW}hsG_ETyzfapa+L%f-UUj%AKBu)lzCL6O{6gB8{3`qRTVi{^0npBEXwhi zAQj7a7~=3I_#hBf{{W0AXfY?jJ3wj5p-K^=T)s4id(m=L;K(8qj~&VID@uU04$DgN z)R$w0J2}D-b>~#&PmNSG=x6ObzW|RFBxk9wHV~sW&Mj^%_Q3Ty*%)?VsVVYPm=pO^-5)zJxfwBW>bK0*PTQPkS4>=2&(5pr}PQgg%`qlY&3DZ#;Z1-EWFj_C?Td1 zwoq)ci02euuPMW-@l+2ACuu=e-q0p8aF+%~lU7byU5&&Jc|?6RX_GQYQI$lcbY`Ig z(;1fC<{{V+BZ@HK(5r;_4-iwYcnU-Xn$@o$fC(i7VWJ#s5Jc*hxmfWra9*^5t6062 z*TB&W;j%;!;aLw;o=`+D0ChsGB@p0^0?O=$o*{EV<=_=34Fa9NE49k~QyIZ@!lb2P zt>4z346|R@fL(w9r)8&|oGI;bCkx-gIFAE4{@EZIEbM|F1AFi;XcF-1$VYCs)UFt@+XzMd zc(~lx6a)5DIYWsovbVYwC2g%3tCoSx^8WySan^IGm~KNj_9;XIRzsQwIUOa(x090j zWN6WPiecYDM9^sAK(a~Wv3_!~0c0tlfb6dD6-8O7P!H7N?5>M4trF7%CY?zmYKD-} zNOKBTn9mcV;B9nH6V)1_$`k|k<>e@}5;wf>PYgh*Y$5PDvQ?(92T*#eF6rX9omUgn z{f4ui4k+<-4xuB@9gs%{3gBY?-0jI1kM{QGkZ|PHM+K${D-yOwuyswqSCr_W?9nIc zs=Se@mFNEekuqNa)m~bv)GUoF-Tu%lyjkR<#8ykUG${gZkb5nEKGXgb5fS!-TfDx&6@Nvh7^mAYM=J5J|3oN34aoj7={LG#3?n{7ytgi@~n-4*9n%pN{!SBusj!I~Iz7n|h}Kd&uO zhxGXB(7h48(5Q8@!oiP#d>Pl1wxi0e;uSWdwWtWwexh)aR33iEJDfI>6N(Wygw-uA z4#0^1w2c``e!QX;(trfSTH-6^9qKVMD46p5Dmc@&0@W=bC1m*F7FEWe(K>J*s=;C^ zh0t)JaR(($TsbW1<)JFNoL>gK6nflOh)*u6NzL)IhyWX`8X`zRoFoa0x}Z>ar1MTC zM;au11+p$-U3qum85DRDSdiCtsYQDu>RLa%xb=6tT;AtdSd|pb8PIUUbb|(7P@K&@ zK-=p}NFXASYAaOG1(3~H^0rsDEYA!mP3*LuR+$^atH7zV!oRGqsm$=^j`LwK?a&x$ zgbG36@t!kJES01|dd(_!1GfyOp4QUBD*%7clYAOxF3ScA?~ZrZd-|2YnsHR2J5ugs z>5%Ty2LKX9Yr>Vr%|~=-)@jp2i@e?7OUS&j7Ij3$0I4iCysmPXIKSfME5T(vsA^nG zwyE5D7?+#CTM!jd0~J`_s+5R+rxI+;qd#;Uji=q_STj`EdHQ8{Mu$DM^$P&f@M2x%x!6^=z> zW@UmYRJ>?7y7omEiilXD4fsve;Q1FY5#v;f%?Ugscp%UDbscMFbTN1WriLwJKoB5< zTnR#xjtY~^QN{A4n6%vr&CR1{`IP$wB&#LgE2yZ`LZM6AyUb;ve@{#AC zsO-Ej2#~B<5S!w_iQC4m^Q~wDjd2!kV~8BYGYDZk27bxDqW=Khhf->eAUat(ul9FR zI`E3&k^r}*_-zB3DAd%CLBAbgR0LH^VX}}dlZFR?!4Tl+83f?S)peBKptHO`ZVX$a z#y@#4h_(978Z$+3+E@4MIdTmuztE35@>39at1gKw@d5}qF6p6V?l{XvadJeOYr*;x zoWvTnWofe+)PiWPyn_G)F#!+NMdnM*)qK?ifk2?8aT(!D#&)Gu212-b;t{}AEo!8+ z>qmr`z)mTuf#FT018^&;*H^^(UV@Mg%?8@Kp*+0O@Y%;PF&sx!N<^I=4SH^yPTJC< z6tbu5(Bo69kPi(iu|fkX?#%LHt`s?U#40-x%#)B#>Z4N%W``aIwkH5uu|NFw^ZiW)BSac- ziR9GB1UQILG9W%>q&8j=LW{x9Ozg?By$pngQ3Hw!`6~dtoQ@yqLq?u%S5~=5mQ3(s z#BA+Di!fv1ua$jnYsByf)+@FNuBkaPzny)h*hTdG&j8K8m!C=p*wX(32$ z4!Oe)BC70dDm)Cft3~T|80y|6cy^lz$Bikve`SfqdfFTq2goZ{g@Co=nqh|*1bX*a zF%_5*=bNg)bzv5b;x$vN{zt3RDFjY!#SGKc+Z9@+6u;c4AXy6C5d=9j3dqK9@gyy(Lo74n>-4#yDG|M5ppuQQNH?$Dk z^86}wt7(-6zgsI%ph6tRYXe0$uZ69Y6<9-a6lsCecqSp&C#jyH`u(Osg+nN zqI!ap%N#1ID9$iNNW5WG!b*-b7YVwOmXq2@#D7)u&)wsc1mjo%qmeUsc`#c7LTJKK z(?Y1K;+WG%rL8RH`10}u12{+-LK)12mncvdtqpKQiX{(%mbY! zfJM@cIbKlZ1{-PC>V0@vR789dv zcv{u)pr}-#Or(cm;3yU9obA@A4r|#4y3UxHQlg%9&Z+y3 zCv@fG;^)i06Go2xqg4c#8<+iP6+&NqiI_(zV$;OC8Q^ zs*5cK_gm_8z;~PbDnzq9RAn-5o(LU!a|z&tFue11x@yY_MG{Ok&l{4;p_+jzOT+gB zIG`o+F^ma9Tn=G8S5bIca|Ia=&g%0_!v`c$7gBg{RvP1PN3!TWL1)N4SI&yKv#lDT zb0qbx(x#?Q10{$gnCj{QMJT~jAkHeCno4n7;e=DxXq=tkFBM9|h%-PjDPc>2KpZqY z(!Iz0JLYG6AkT>=u#iBR9beb7vX;jtfTeh$Age*ZQb2*DiPimPbGjOz#w zAsGITnOfQcn@#A1k_3o7_`$>j7l1r0iZti?r&EXTLqx+O&?v!u=}Ih&qJUZCs$w$A z%$!cNJ!J93kl*rJW8!%EIyRz)DUUkqCKC7K3Wiky%uZ!wx5dz@CqeJvQs}= z<_R!7sI-T>MkRG22|P}~kW{H|^g^Y#w;@-kggg@o@5no$Iw&$c6ye!2lSYo9pqdxw zma<77SWk#{EN{PGF)TMIvQj4grL^QY{PxRHhE4VB4@&wqiAaD>Uy6QH$b8 zP>7V`v;DQ)oFn}_*AjuN6;;NZrmBhYLDt4z8!&~MgZ&zyFbFS`^)m`FAQcqZ;x7yV zE$|36{WmLGS3Du-g1mZ6VU8|>9_`G-L`>>DB@Pvy|U7jv-Dt zV1uEd2R7EsZhQ~VjGm!aF25zQY4VNf1rWjH7ux2TIKCIF}x)j2v#cG2Hvmxou37LoJQI&o4=sV0)dC$Y>j*xbJXu;mkX*pe5HqZ3()*O3M z%_h<`?-C9!jITTRrlX&tHRKJjSI;QnOwE3abn|v^MN)O$mB*ea%d2rJ%p@zZsFRy& zuXY~DE0`1vnaE>^Re2Z`$;qAvgk!D!WPqRyPW4>sDo zYz+k*trZj@`HyN23IUERtci;#Qz9UVAW94}ihM$&&K=n5}@dJpH(O|Tb zQ*Tl2iNG1zqE4sv#YJE(>h3#&vMFt5YH)RA0g$#=RPb>0nViK+NF&i zR7Z65?W@rmZT+A64vR5QC^ZH|2N4WZiX;`0a7?W^w1MOOA%)%?(^nTt#i_|}wM#(? zlR#fdf(@eI-8uR7`rS~G89br`NmoCG#ynhp+y;)b9Asv-+-n;F@arOE#Q zQ(C&qI0InT7Uwkt{X!lJiP!Z+iIZ(huptU@K1t9%R=wdB3cT z^UqVG2*_DL>Yi0c`zCEne50@#hlDXpvQ?9EAY!}#aG7|WIWL84i=#3cb5v61-7P9L z7(5072m6we7>ZS73$s#m!#8=hD!l5d38Mh;ZGdzh+QX5eoMx~Bc9Q^7S_uKw&NdXO z#;%;$zyAQvXUAGv7V$Zxv22QMM?n$lW?|4*F%F;{QlM{yJskp9h;vjMO~V=ykw=3{ z;D507Jw;l@-AT|L(c(=P2Y&l3AKoG^xSl3;j+@Twv& zUM*UK#!|H(Cl!_b)H?1`;g2(6XyUNK=w$&c&T<7mqaVqA&iPaT)~IthW`dE~Yp7XJ4aEVYhn}BK4=47WtD70mTa=VX`Jqwe|<{kqq@G^$T4m=RXIP8JS7GIW9 z*)NMLW!gpR>Y$jSmQ`4`5cg(?rVuNfbK6Q-w=56&v?}L zc>bnYlB{5k23IW&)KeLos~{Y1xYEQj`Io4he8gNu*lsTzV=xZL{B;W|S0=(84y6q+ zF8M|PH0_&(v@S_Qihy&CGQ#7C+bfRYHyUZs!CPE1>rIR+ILrz1I)*UrV1*d%ie!ou z)>xdhN08SY2bNaO{{VjzThv{hvb^^$>@^xH39diVPjp@)0U9qB&Vxh~6lN&eLm)Xx zyN$KvitnOuLA==({{T)PP&s3-a^vKc)m;2vaFnr3Q!esDkY*43M#;rd6}DaeBe?QE zsLS$4%aR8jAJj$ z-Q}n}KWdG&Sw;ZV_7M5R5$8`Z{( zZ+;Ik(?gPBrnBNYZ0d| z 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

5LRMdWdYtg6B4Y5s+UYf+sj7mzF<0 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 b9STyse&*r%ZeP21;k<7I@KF6b^?(1_G%aEc literal 0 HcmV?d00001 diff --git a/packages/platform-ios/xctest-agent/HarnessXCTestAgentUITests/HarnessXCTestAgentUITests.swift b/packages/platform-ios/xctest-agent/HarnessXCTestAgentUITests/HarnessXCTestAgentUITests.swift index b40d5d1..d4c507c 100644 --- a/packages/platform-ios/xctest-agent/HarnessXCTestAgentUITests/HarnessXCTestAgentUITests.swift +++ b/packages/platform-ios/xctest-agent/HarnessXCTestAgentUITests/HarnessXCTestAgentUITests.swift @@ -257,9 +257,8 @@ final class HarnessXCTestAgentUITests: XCTestCase { override func setUpWithError() throws { continueAfterFailure = false capabilities = [ - PermissionPromptCapability( + PermissionPromptWatchdog( state: state, - application: targetApplication, springboard: springboard ) ] @@ -269,15 +268,6 @@ final class HarnessXCTestAgentUITests: XCTestCase { targetApplication.launch() - for capability in capabilities { - if let permissionPromptCapability = capability as? PermissionPromptCapability { - addUIInterruptionMonitor(withDescription: "Harness permission prompt handler") { alert in - permissionPromptCapability.logInterruption(alert.label) - return permissionPromptCapability.handleInterruption(alert) - } - } - } - for capability in capabilities { try capability.setUp() } diff --git a/packages/platform-ios/xctest-agent/HarnessXCTestAgentUITests/PermissionPromptCapability.swift b/packages/platform-ios/xctest-agent/HarnessXCTestAgentUITests/PermissionPromptCapability.swift deleted file mode 100644 index d29e891..0000000 --- a/packages/platform-ios/xctest-agent/HarnessXCTestAgentUITests/PermissionPromptCapability.swift +++ /dev/null @@ -1,105 +0,0 @@ -import XCTest - -private enum PermissionPromptEnvironment { - static let autoAcceptPermissions = "HARNESS_XCTEST_AGENT_AUTO_ACCEPT_PERMISSIONS" -} - -struct PermissionPromptConfiguration: Codable { - var autoAcceptPermissions: Bool - - static func fromEnvironment() -> PermissionPromptConfiguration { - return PermissionPromptConfiguration( - autoAcceptPermissions: ProcessInfo.processInfo.environment[PermissionPromptEnvironment.autoAcceptPermissions] == "1" - ) - } -} - -final class PermissionPromptCapability: AgentCapability { - private enum Constants { - static let knownPositiveButtonLabels = [ - "Allow", - "OK", - "Continue", - "Next", - "While Using App", - "While Using the App", - "Always Allow", - "Allow Once", - "Join", - "Pair", - "Allow Full Access" - ] - } - - private let application: XCUIApplication - private let springboard: XCUIApplication - private let state: HarnessXCTestAgentState - - private func log(_ message: String) { - NSLog("[HarnessXCTestAgent][PermissionPromptCapability] %@", message) - } - - init( - state: HarnessXCTestAgentState, - application: XCUIApplication, - springboard: XCUIApplication - ) { - self.state = state - self.application = application - self.springboard = springboard - } - - func setUp() throws { - if state.permissions.autoAcceptPermissions { - log("permission prompt capability enabled") - } - } - - func logInterruption(_ alertLabel: String) { - log("interruption monitor received alert: \(alertLabel)") - } - - func handleInterruption(_ alert: XCUIElement) -> Bool { - guard state.permissions.autoAcceptPermissions else { - return false - } - - return tapPositiveAction(in: alert) - } - - func tick() throws { - guard state.permissions.autoAcceptPermissions else { - return - } - - let handledAppAlert = tapPositiveAction(in: application.alerts.firstMatch) - let handledAppSheet = tapPositiveAction(in: application.sheets.firstMatch) - let handledSpringboardAlert = tapPositiveAction(in: springboard.alerts.firstMatch) - let handledSpringboardSheet = tapPositiveAction(in: springboard.sheets.firstMatch) - - if handledAppAlert || handledAppSheet || handledSpringboardAlert || handledSpringboardSheet { - log("handled permission prompt during tick") - } - } - - private func tapPositiveAction(in element: XCUIElement) -> Bool { - guard element.exists else { - return false - } - - for label in Constants.knownPositiveButtonLabels { - let button = element.buttons[label] - - if button.exists { - log("tapping button: \(label)") - button.tap() - return true - } - } - - let visibleButtons = element.buttons.allElementsBoundByIndex.map(\.label) - log("prompt found but no matching positive action. buttons=\(visibleButtons)") - - return false - } -} diff --git a/packages/platform-ios/xctest-agent/HarnessXCTestAgentUITests/PermissionPromptWatchdog.swift b/packages/platform-ios/xctest-agent/HarnessXCTestAgentUITests/PermissionPromptWatchdog.swift new file mode 100644 index 0000000..7b0631f --- /dev/null +++ b/packages/platform-ios/xctest-agent/HarnessXCTestAgentUITests/PermissionPromptWatchdog.swift @@ -0,0 +1,67 @@ +import XCTest + +private enum PermissionPromptEnvironment { + static let autoAcceptPermissions = "HARNESS_XCTEST_AGENT_AUTO_ACCEPT_PERMISSIONS" +} + +struct PermissionPromptConfiguration: Codable { + var autoAcceptPermissions: Bool + + static func fromEnvironment() -> PermissionPromptConfiguration { + return PermissionPromptConfiguration( + autoAcceptPermissions: ProcessInfo.processInfo.environment[PermissionPromptEnvironment.autoAcceptPermissions] == "1" + ) + } +} + +final class PermissionPromptWatchdog: AgentCapability { + private enum Constants { + static let knownPositiveButtonLabels = [ + "Allow", + "OK", + "Continue", + "Next", + "While Using App", + "While Using the App", + "Always Allow", + "Allow Once", + "Join", + "Pair", + "Allow Full Access" + ] + } + + private let springboard: XCUIApplication + private let state: HarnessXCTestAgentState + + private func log(_ message: String) { + NSLog("[HarnessXCTestAgent][PermissionPromptWatchdog] %@", message) + } + + init(state: HarnessXCTestAgentState, springboard: XCUIApplication) { + self.state = state + self.springboard = springboard + } + + func setUp() throws { + if state.permissions.autoAcceptPermissions { + log("permission prompt watchdog enabled") + } + } + + func tick() throws { + guard state.permissions.autoAcceptPermissions else { + return + } + + for label in Constants.knownPositiveButtonLabels { + let button = springboard.buttons[label].firstMatch + + if button.exists && button.isHittable { + log("tapping button: \(label)") + button.tap() + return + } + } + } +} From 8d3f7af167fa11201724e56890ef7914583e503c Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Tue, 28 Apr 2026 12:20:52 +0200 Subject: [PATCH 11/33] chore(platform-ios): remove unneeded file --- .../xctest-agent/manual-run/xcodebuild.log | 43 ------------------- 1 file changed, 43 deletions(-) delete mode 100644 packages/platform-ios/xctest-agent/manual-run/xcodebuild.log diff --git a/packages/platform-ios/xctest-agent/manual-run/xcodebuild.log b/packages/platform-ios/xctest-agent/manual-run/xcodebuild.log deleted file mode 100644 index ff334a4..0000000 --- a/packages/platform-ios/xctest-agent/manual-run/xcodebuild.log +++ /dev/null @@ -1,43 +0,0 @@ -Command line invocation: - /Applications/Xcode.app/Contents/Developer/usr/bin/xcodebuild test-without-building -project HarnessXCTestAgent.xcodeproj -scheme HarnessXCTestAgent -destination "platform=iOS Simulator,id=CE6BE68F-A9BD-4A3A-8F57-37FEA8AC3FB8" -parallel-testing-enabled NO -maximum-parallel-testing-workers 1 -derivedDataPath build/simulator - -2026-04-22 18:02:23.825 xcodebuild[72153:11965075] [MT] IDERunDestination: Supported platforms for the buildables in the current scheme is empty. -2026-04-22 18:02:42.554208-0400 HarnessXCTestAgentUITests-Runner[72652:11967943] [Default] Running tests... - t = nans Interface orientation changed to Portrait -Test Suite 'All tests' started at 2026-04-22 18:02:42.755. -Test Suite 'HarnessXCTestAgentUITests.xctest' started at 2026-04-22 18:02:42.755. -Test Suite 'HarnessXCTestAgentUITests' started at 2026-04-22 18:02:42.755. -Test Case '-[HarnessXCTestAgentUITests.HarnessXCTestAgentUITests testAgentSession]' started. - t = 0.00s Start Test at 2026-04-22 18:02:42.756 - t = 0.13s Set Up -2026-04-22 18:02:42.891067-0400 HarnessXCTestAgentUITests-Runner[72652:11967943] [HarnessXCTestAgent] setUpWithError started -2026-04-22 18:02:42.896411-0400 HarnessXCTestAgentUITests-Runner[72652:11967943] [HarnessXCTestAgent] enabled capabilities: PermissionPromptCapability - t = 0.14s Open com.callstackincubator.HarnessXCTestAgent - t = 0.14s Launch com.callstackincubator.HarnessXCTestAgent - t = 0.95s Setting up automation session - t = 1.59s Wait for com.callstackincubator.HarnessXCTestAgent to idle -2026-04-22 18:02:45.495856-0400 HarnessXCTestAgentUITests-Runner[72652:11967943] [HarnessXCTestAgent] HTTP server started on port 49200 -2026-04-22 18:02:45.495989-0400 HarnessXCTestAgentUITests-Runner[72652:11967943] [HarnessXCTestAgent] setUpWithError completed -2026-04-22 18:02:45.496398-0400 HarnessXCTestAgentUITests-Runner[72652:11968142] [] nw_listener_socket_inbox_create_socket setsockopt SO_NECP_LISTENUUID failed [2: No such file or directory] -2026-04-22 18:02:45.496553-0400 HarnessXCTestAgentUITests-Runner[72652:11967943] [HarnessXCTestAgent] testAgentSession started -2026-04-22 18:02:45.502439-0400 HarnessXCTestAgentUITests-Runner[72652:11968142] [HarnessXCTestAgent] HTTP listener state: ready -2026-04-22 18:03:44.727750-0400 HarnessXCTestAgentUITests-Runner[72652:11967943] [HarnessXCTestAgent] testAgentSession completed - t = 61.97s Tear Down -2026-04-22 18:03:44.730653-0400 HarnessXCTestAgentUITests-Runner[72652:11968236] [HarnessXCTestAgent] HTTP listener state: cancelled -Test Case '-[HarnessXCTestAgentUITests.HarnessXCTestAgentUITests testAgentSession]' passed (62.334 seconds). -Test Suite 'HarnessXCTestAgentUITests' passed at 2026-04-22 18:03:45.091. - Executed 1 test, with 0 failures (0 unexpected) in 62.334 (62.335) seconds -Test Suite 'HarnessXCTestAgentUITests.xctest' passed at 2026-04-22 18:03:45.092. - Executed 1 test, with 0 failures (0 unexpected) in 62.334 (62.336) seconds -Test Suite 'All tests' passed at 2026-04-22 18:03:45.092. - Executed 1 test, with 0 failures (0 unexpected) in 62.334 (62.337) seconds -2026-04-22 18:03:45.424 xcodebuild[72153:11965075] [MT] IDETestOperationsObserverDebug: 81.245 elapsed -- Testing started completed. -2026-04-22 18:03:45.424 xcodebuild[72153:11965075] [MT] IDETestOperationsObserverDebug: 0.000 sec, +0.000 sec -- start -2026-04-22 18:03:45.424 xcodebuild[72153:11965075] [MT] IDETestOperationsObserverDebug: 81.245 sec, +81.245 sec -- end - -Test session results, code coverage, and logs: - /Users/szymon.chmal/Projects/react-native-harness/packages/platform-ios/xctest-agent/build/simulator/Logs/Test/Test-HarnessXCTestAgent-2026.04.22_18-02-23--0400.xcresult - -** TEST EXECUTE SUCCEEDED ** - -Testing started From e14bce107486f5ae10eded40b8e85b0c35ef61ed Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Tue, 28 Apr 2026 12:26:33 +0200 Subject: [PATCH 12/33] chore(platform-ios): remove devlogs plumbing --- packages/platform-ios/src/xctest-agent.ts | 87 +---------------------- 1 file changed, 3 insertions(+), 84 deletions(-) diff --git a/packages/platform-ios/src/xctest-agent.ts b/packages/platform-ios/src/xctest-agent.ts index 8db351c..ace0ee3 100644 --- a/packages/platform-ios/src/xctest-agent.ts +++ b/packages/platform-ios/src/xctest-agent.ts @@ -278,7 +278,6 @@ const stopProcess = async (options: { targetKind: XCTestAgentTarget['kind']; }) => { if (!options.process) { - console.log('[xctest dispose] no agent process to stop'); return; } @@ -286,14 +285,11 @@ const stopProcess = async (options: { try { childProcess = await options.process.nodeChildProcess; - console.log('[xctest dispose] resolved child process'); } catch { - console.log('[xctest dispose] failed to resolve child process'); return; } childProcess.kill('SIGTERM'); - console.log('[xctest dispose] sent SIGTERM'); if ( await waitForShutdown({ @@ -301,87 +297,20 @@ const stopProcess = async (options: { shutdownTimeoutMs: options.shutdownTimeoutMs, }) ) { - console.log('[xctest dispose] process stopped after SIGTERM'); return; } - console.log('[xctest dispose] SIGTERM wait timed out'); - xctestAgentLogger.warn( 'XCTest agent session for %s target did not stop after %dms; forcing shutdown', options.targetKind, options.shutdownTimeoutMs, ); childProcess.kill('SIGKILL'); - console.log('[xctest dispose] sent SIGKILL'); - - if ( - await waitForShutdown({ - processTask: options.processTask, - shutdownTimeoutMs: options.shutdownTimeoutMs, - }) - ) { - console.log('[xctest dispose] process stopped after SIGKILL'); - return; - } - - console.log('[xctest dispose] SIGKILL wait timed out'); -}; - -const logActiveHandles = () => { - const getActiveHandles = (process as unknown as { - _getActiveHandles?: () => unknown[]; - })._getActiveHandles; - - if (!getActiveHandles) { - return; - } - console.log( - '[xctest dispose] active handles', - getActiveHandles().map((handle) => { - const constructorName = handle?.constructor?.name; - - if (constructorName !== 'ChildProcess') { - return constructorName; - } - - const childProcess = handle as { - exitCode?: number | null; - killed?: boolean; - pid?: number; - signalCode?: NodeJS.Signals | null; - spawnargs?: string[]; - spawnfile?: string; - }; - - return { - type: constructorName, - exitCode: childProcess.exitCode, - killed: childProcess.killed, - pid: childProcess.pid, - signalCode: childProcess.signalCode, - spawnargs: childProcess.spawnargs, - spawnfile: childProcess.spawnfile, - }; - }), - ); -}; - -const stopProcessAndLogHandles = async (options: { - process: Subprocess | null; - processTask: Promise | null; - shutdownTimeoutMs: number; - targetKind: XCTestAgentTarget['kind']; -}) => { - await stopProcess({ - process: options.process, + await waitForShutdown({ processTask: options.processTask, shutdownTimeoutMs: options.shutdownTimeoutMs, - targetKind: options.targetKind, }); - - logActiveHandles(); }; const getErrorMessage = (error: unknown): string => { @@ -545,15 +474,8 @@ export const createXCTestAgentController = (options: { const client = createXCTestAgentClient(transport); agentClient = client; - void Promise.resolve(currentProcess).catch((error) => { - xctestAgentLogger.debug('XCTest agent process exited', error); - }); - processTask = waitForChildProcessExit(currentProcess).finally(() => { - console.log('[xctest process] child process completed'); - if (agentProcess === currentProcess) { - console.log('[xctest process] clearing active agent process reference'); agentProcess = null; agentClient = null; processTask = null; @@ -567,8 +489,6 @@ export const createXCTestAgentController = (options: { } } catch (error) { xctestAgentLogger.debug('XCTest agent process stopped', error); - } finally { - console.log('[xctest process] output stream completed'); } })(); @@ -586,7 +506,7 @@ export const createXCTestAgentController = (options: { ); await transport.dispose(); agentClient = null; - await stopProcessAndLogHandles({ + await stopProcess({ process: currentProcess, processTask, shutdownTimeoutMs, @@ -610,8 +530,7 @@ export const createXCTestAgentController = (options: { ); await currentClient?.dispose(); - console.log('[xctest dispose] client disposed'); - await stopProcessAndLogHandles({ + await stopProcess({ process: currentProcess, processTask: currentProcessTask, shutdownTimeoutMs, From f04607cf96943c6958d442bf935df28cb3ccd5ef Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Tue, 28 Apr 2026 14:56:02 +0200 Subject: [PATCH 13/33] feat(platform-ios): use direct communication --- actions/shared/index.cjs | 3 + .../project.pbxproj | 10 + apps/playground/rn-harness.config.mjs | 7 +- packages/jest/src/__tests__/harness.test.ts | 262 +--- packages/jest/src/harness.ts | 9 +- packages/platform-ios/package.json | 1 - .../src/__tests__/launch-options.test.ts | 22 + .../src/__tests__/xctest-agent.test.ts | 3 + .../platform-ios/src/appium-ios-device.d.ts | 7 - packages/platform-ios/src/config.ts | 8 + packages/platform-ios/src/factory.ts | 7 +- packages/platform-ios/src/instance.ts | 43 +- packages/platform-ios/src/xcrun/devicectl.ts | 80 + .../src/xctest-agent-transport-device.ts | 196 ++- packages/platform-ios/src/xctest-agent.ts | 154 +- .../HarnessXCTestAgentUITests.swift | 24 +- packages/tools/src/index.ts | 1 + packages/tools/src/net.ts | 13 + packages/tools/src/spawn.ts | 11 +- pnpm-lock.yaml | 1293 +---------------- 20 files changed, 482 insertions(+), 1672 deletions(-) delete mode 100644 packages/platform-ios/src/appium-ios-device.d.ts create mode 100644 packages/tools/src/net.ts diff --git a/actions/shared/index.cjs b/actions/shared/index.cjs index d81a627..0575912 100644 --- a/actions/shared/index.cjs +++ b/actions/shared/index.cjs @@ -4194,6 +4194,9 @@ var coerce = { }; var NEVER = INVALID; +// ../tools/dist/net.js +var import_node_net = __toESM(require("net"), 1); + // ../tools/dist/logger.js var import_node_util = __toESM(require("util"), 1); var import_picocolors = __toESM(require_picocolors(), 1); diff --git a/apps/playground/ios/HarnessPlayground.xcodeproj/project.pbxproj b/apps/playground/ios/HarnessPlayground.xcodeproj/project.pbxproj index f31363f..79788dd 100644 --- a/apps/playground/ios/HarnessPlayground.xcodeproj/project.pbxproj +++ b/apps/playground/ios/HarnessPlayground.xcodeproj/project.pbxproj @@ -191,10 +191,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-HarnessPlayground/Pods-HarnessPlayground-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-HarnessPlayground/Pods-HarnessPlayground-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-HarnessPlayground/Pods-HarnessPlayground-frameworks.sh\"\n"; @@ -230,10 +234,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-HarnessPlayground/Pods-HarnessPlayground-resources-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-HarnessPlayground/Pods-HarnessPlayground-resources-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-HarnessPlayground/Pods-HarnessPlayground-resources.sh\"\n"; @@ -260,6 +268,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = BAJL5U28HC; ENABLE_BITCODE = NO; INFOPLIST_FILE = HarnessPlayground/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.1; @@ -288,6 +297,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = BAJL5U28HC; INFOPLIST_FILE = HarnessPlayground/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.1; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/apps/playground/rn-harness.config.mjs b/apps/playground/rn-harness.config.mjs index 5561f44..a21fe1e 100644 --- a/apps/playground/rn-harness.config.mjs +++ b/apps/playground/rn-harness.config.mjs @@ -23,6 +23,7 @@ export default { entryPoint: './index.js', appRegistryComponentName: 'HarnessPlayground', plugins: [harnessLoggingPlugin()], +detectNativeCrashes: false, runners: [ androidPlatform({ @@ -72,8 +73,10 @@ export default { }), applePlatform({ name: 'iphone-16-pro', - device: applePhysicalDevice('iPhone (Szymon) (2)'), - bundleId: 'react-native-harness', + device: applePhysicalDevice('iPhone (Szymon) (2)', { + codeSign: { teamId: 'BAJL5U28HC' }, + }), + bundleId: 'com.harnessplayground', }), applePlatform({ name: 'ios', diff --git a/packages/jest/src/__tests__/harness.test.ts b/packages/jest/src/__tests__/harness.test.ts index 6545335..f828b09 100644 --- a/packages/jest/src/__tests__/harness.test.ts +++ b/packages/jest/src/__tests__/harness.test.ts @@ -165,11 +165,9 @@ const createAppMonitor = (): { const createPlatformRunner = ( overrides: Partial = {}, ): HarnessPlatformRunner => ({ - prepareRun: vi.fn(async () => undefined), startApp: vi.fn(async () => undefined), restartApp: vi.fn(async () => undefined), stopApp: vi.fn(async () => undefined), - disposeRun: vi.fn(async () => undefined), dispose: vi.fn(async () => undefined), isAppRunning: vi.fn(async () => true), createAppMonitor: () => createAppMonitor().appMonitor, @@ -397,83 +395,6 @@ describe('getHarness', () => { await harness.dispose(); }); - it('prepares the platform runner lazily before first app launch and disposes it during teardown', async () => { - const { serverBridge, emitReady } = createBridgeServer(); - const appMonitor = createAppMonitor(); - const callOrder: string[] = []; - const restartApp = vi.fn(async () => { - callOrder.push('restartApp'); - }); - const platformInstance = createPlatformRunner({ - prepareRun: vi.fn(async () => { - callOrder.push('prepareRun'); - }), - restartApp, - createAppMonitor: () => appMonitor.appMonitor, - disposeRun: vi.fn(async () => { - callOrder.push('disposeRun'); - }), - dispose: vi.fn(async () => { - callOrder.push('dispose'); - }), - }); - const metroInstance = createMetroInstance(); - - mocks.getBridgeServer.mockResolvedValue(serverBridge); - mocks.getMetroInstance.mockResolvedValue(metroInstance); - mocks.waitForMetroBackedAppReady.mockImplementationOnce( - async (options: WaitForMetroBackedAppReadyOptions) => { - await options.startAttempt(); - const readyPromise = options.waitForReady(new AbortController().signal); - emitReady(); - await readyPromise; - }, - ); - - ( - globalThis as typeof globalThis & { - __HARNESS_PLATFORM_RUNNER__?: (...args: unknown[]) => Promise; - } - ).__HARNESS_PLATFORM_RUNNER__ = vi.fn(async () => platformInstance); - - const platform: HarnessPlatform = { - config: {}, - getResourceLockKey: () => 'ios:test-platform-run-lifecycle', - name: 'ios', - platformId: 'ios', - runner: `data:text/javascript,${encodeURIComponent( - 'export default (...args) => globalThis.__HARNESS_PLATFORM_RUNNER__(...args);', - )}`, - }; - - const harness = await getHarness( - createHarnessConfig(), - platform, - '/tmp/project', - ); - - expect(platformInstance.prepareRun).not.toHaveBeenCalled(); - expect(appMonitor.appMonitor.start).toHaveBeenCalledTimes(1); - expect(callOrder).toEqual([]); - - await harness.ensureAppReady('/tmp/example.harness.ts'); - - expect(platformInstance.prepareRun).toHaveBeenCalledTimes(1); - expect(restartApp).toHaveBeenCalledTimes(1); - expect(callOrder).toEqual(['prepareRun', 'restartApp']); - - await harness.dispose(); - - expect(platformInstance.disposeRun).toHaveBeenCalledTimes(1); - expect(platformInstance.dispose).toHaveBeenCalledTimes(1); - expect(callOrder).toEqual([ - 'prepareRun', - 'restartApp', - 'disposeRun', - 'dispose', - ]); - }); - it('resolves and exposes a fallback Metro port before platform init', async () => { const { serverBridge } = createBridgeServer(); const appMonitor = createAppMonitor(); @@ -588,136 +509,6 @@ describe('getHarness', () => { await harness.dispose(); }); - it('disposes a prepared platform runner when first app launch fails after prepareRun', async () => { - const { serverBridge } = createBridgeServer(); - const appMonitor = createAppMonitor(); - const disposeRun = vi.fn(async () => undefined); - const dispose = vi.fn(async () => undefined); - const platformInstance = createPlatformRunner({ - createAppMonitor: () => appMonitor.appMonitor, - disposeRun, - dispose, - }); - const metroInstance = createMetroInstance(); - const restartApp = vi - .spyOn(platformInstance, 'restartApp') - .mockRejectedValueOnce(new Error('restart failed')); - - mocks.waitForMetroBackedAppReady.mockImplementationOnce( - async (options: WaitForMetroBackedAppReadyOptions) => { - await options.startAttempt(); - }, - ); - - mocks.getBridgeServer.mockResolvedValue(serverBridge); - mocks.getMetroInstance.mockResolvedValue(metroInstance); - - ( - globalThis as typeof globalThis & { - __HARNESS_PLATFORM_RUNNER__?: (...args: unknown[]) => Promise; - } - ).__HARNESS_PLATFORM_RUNNER__ = vi.fn(async () => platformInstance); - - const platform: HarnessPlatform = { - config: {}, - getResourceLockKey: () => 'ios:test-platform-run-lifecycle-error', - name: 'ios', - platformId: 'ios', - runner: `data:text/javascript,${encodeURIComponent( - 'export default (...args) => globalThis.__HARNESS_PLATFORM_RUNNER__(...args);', - )}`, - }; - - const harness = await getHarness( - createHarnessConfig(), - platform, - '/tmp/project', - ); - - await expect( - harness.ensureAppReady('/tmp/example.harness.ts'), - ).rejects.toThrow('restart failed'); - - expect(platformInstance.prepareRun).toHaveBeenCalledTimes(1); - expect(restartApp).toHaveBeenCalledTimes(1); - - await harness.dispose(); - - expect(disposeRun).toHaveBeenCalledTimes(1); - expect(dispose).toHaveBeenCalledTimes(1); - }); - - it('prepares the platform runner only once across multiple app launches in the same run', async () => { - const { serverBridge, emitReady } = createBridgeServer(); - const appMonitor = createAppMonitor(); - const prepareRun = vi.fn(async () => undefined); - const restartApp = vi.fn(async () => undefined); - const stopApp = vi.fn(async () => undefined); - const platformInstance = createPlatformRunner({ - prepareRun, - restartApp, - stopApp, - isAppRunning: vi.fn(async () => false), - createAppMonitor: () => appMonitor.appMonitor, - }); - const metroInstance = createMetroInstance(); - - mocks.getBridgeServer.mockResolvedValue(serverBridge); - mocks.getMetroInstance.mockResolvedValue(metroInstance); - mocks.waitForMetroBackedAppReady - .mockImplementationOnce( - async (options: WaitForMetroBackedAppReadyOptions) => { - await options.startAttempt(); - const readyPromise = options.waitForReady( - new AbortController().signal, - ); - emitReady(); - await readyPromise; - }, - ) - .mockImplementationOnce( - async (options: WaitForMetroBackedAppReadyOptions) => { - await options.startAttempt(); - const readyPromise = options.waitForReady( - new AbortController().signal, - ); - emitReady(); - await readyPromise; - }, - ); - - ( - globalThis as typeof globalThis & { - __HARNESS_PLATFORM_RUNNER__?: (...args: unknown[]) => Promise; - } - ).__HARNESS_PLATFORM_RUNNER__ = vi.fn(async () => platformInstance); - - const platform: HarnessPlatform = { - config: {}, - getResourceLockKey: () => 'ios:test-platform-run-lifecycle-once', - name: 'ios', - platformId: 'ios', - runner: `data:text/javascript,${encodeURIComponent( - 'export default (...args) => globalThis.__HARNESS_PLATFORM_RUNNER__(...args);', - )}`, - }; - - const harness = await getHarness( - createHarnessConfig(), - platform, - '/tmp/project', - ); - - await harness.ensureAppReady('/tmp/first.harness.ts'); - await harness.restart('/tmp/second.harness.ts'); - - expect(prepareRun).toHaveBeenCalledTimes(1); - expect(restartApp).toHaveBeenCalledTimes(2); - expect(stopApp).toHaveBeenCalledTimes(1); - - await harness.dispose(); - }); - it('routes ensureAppReady through the shared Metro startup helper', async () => { const { serverBridge, emitReady } = createBridgeServer(); const appMonitor = createAppMonitor(); @@ -1000,6 +791,59 @@ describe('plugins', () => { ]); }); + it('disposes lifecycle hooks and platform resources only once', async () => { + const { serverBridge } = createBridgeServer(); + const platformInstance = createPlatformRunner(); + const observedHooks: string[] = []; + + mocks.getBridgeServer.mockResolvedValue(serverBridge); + mocks.getMetroInstance.mockResolvedValue(createMetroInstance()); + + ( + globalThis as typeof globalThis & { + __HARNESS_PLATFORM_RUNNER__?: (...args: unknown[]) => Promise; + } + ).__HARNESS_PLATFORM_RUNNER__ = vi.fn(async () => platformInstance); + + const plugin = definePlugin({ + name: 'dispose-plugin', + hooks: { + harness: { + afterRun: () => { + observedHooks.push('afterRun'); + }, + beforeDispose: () => { + observedHooks.push('beforeDispose'); + }, + }, + }, + }); + + const platform: HarnessPlatform = { + config: {}, + name: 'ios', + platformId: 'ios', + runner: `data:text/javascript,${encodeURIComponent( + 'export default (...args) => globalThis.__HARNESS_PLATFORM_RUNNER__(...args);', + )}`, + getResourceLockKey: () => 'ios:simulator:iPhone 17 Pro:26.2', + }; + + const harness = await getHarness( + createHarnessConfig({ + plugins: [plugin], + }), + platform, + '/tmp/project', + ); + + await harness.dispose(); + await harness.dispose(); + + expect(observedHooks).toEqual(['afterRun', 'beforeDispose']); + expect(platformInstance.dispose).toHaveBeenCalledTimes(1); + }); + it('waits in queue before starting Metro and releases the lock on dispose', async () => { const resourceKey = 'ios:simulator:iPhone 17 Pro:26.2'; const firstPlatformRunner = createPlatformRunner(); diff --git a/packages/jest/src/harness.ts b/packages/jest/src/harness.ts index cb918bb..91a9539 100644 --- a/packages/jest/src/harness.ts +++ b/packages/jest/src/harness.ts @@ -649,7 +649,10 @@ const getHarnessInternal = async ( harnessLogger.debug('client log forwarding enabled'); } - const dispose = async (reason: 'normal' | 'abort' | 'error' = 'normal') => { + let disposePromise: Promise | null = null; + const disposeOnce = async ( + reason: 'normal' | 'abort' | 'error' = 'normal' + ) => { harnessLogger.debug('disposing Harness (reason=%s)', reason); let hookError: unknown; @@ -708,6 +711,10 @@ const getHarnessInternal = async ( throw cleanupError; } }; + const dispose = (reason: 'normal' | 'abort' | 'error' = 'normal') => { + disposePromise ??= disposeOnce(reason); + return disposePromise; + }; if (signal.aborted) { await dispose('abort'); diff --git a/packages/platform-ios/package.json b/packages/platform-ios/package.json index 0085b65..24cd179 100644 --- a/packages/platform-ios/package.json +++ b/packages/platform-ios/package.json @@ -19,7 +19,6 @@ "@react-native-harness/config": "workspace:*", "@react-native-harness/platforms": "workspace:*", "@react-native-harness/tools": "workspace:*", - "appium-ios-device": "^3.1.11", "zod": "^3.25.67", "tslib": "^2.3.0" }, diff --git a/packages/platform-ios/src/__tests__/launch-options.test.ts b/packages/platform-ios/src/__tests__/launch-options.test.ts index 0800bdd..1e4e058 100644 --- a/packages/platform-ios/src/__tests__/launch-options.test.ts +++ b/packages/platform-ios/src/__tests__/launch-options.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest'; import { + getDeviceConnectionHost, getDeviceCtlLaunchArgs, } from '../xcrun/devicectl.js'; import { getSimctlChildEnvironment } from '../xcrun/simctl.js'; @@ -40,4 +41,25 @@ describe('Apple app launch options', () => { '--retry=1', ]); }); + + it('uses the CoreDevice tunnel IP as the direct device connection host', () => { + expect( + getDeviceConnectionHost({ + identifier: 'device-id', + connectionProperties: { + tunnelIPAddress: 'fd12:3456:789a::1', + potentialHostnames: ['my-iphone.local'], + }, + deviceProperties: { + name: 'My iPhone', + osVersionNumber: '18.0', + }, + hardwareProperties: { + marketingName: 'iPhone', + productType: 'iPhone17,1', + udid: '00008140-001600222422201C', + }, + }) + ).toBe('fd12:3456:789a::1'); + }); }); diff --git a/packages/platform-ios/src/__tests__/xctest-agent.test.ts b/packages/platform-ios/src/__tests__/xctest-agent.test.ts index 55e75a6..2cb6e48 100644 --- a/packages/platform-ios/src/__tests__/xctest-agent.test.ts +++ b/packages/platform-ios/src/__tests__/xctest-agent.test.ts @@ -186,6 +186,7 @@ describe('xctest-agent orchestration', () => { target: { kind: 'device', id: 'device-123', + codeSign: { teamId: 'TESTTEAM01' }, }, }); @@ -255,6 +256,7 @@ describe('xctest-agent orchestration', () => { target: { kind: 'device', id: 'device-555', + codeSign: { teamId: 'TESTTEAM01' }, }, }); @@ -370,6 +372,7 @@ describe('xctest-agent orchestration', () => { target: { kind: 'device', id: 'device-123', + codeSign: { teamId: 'TESTTEAM01' }, }, }); diff --git a/packages/platform-ios/src/appium-ios-device.d.ts b/packages/platform-ios/src/appium-ios-device.d.ts deleted file mode 100644 index 11c1583..0000000 --- a/packages/platform-ios/src/appium-ios-device.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -declare module 'appium-ios-device' { - import type net from 'node:net'; - - export const utilities: { - connectPort: (udid: string, port: number) => Promise; - }; -} diff --git a/packages/platform-ios/src/config.ts b/packages/platform-ios/src/config.ts index cce8125..56490f8 100644 --- a/packages/platform-ios/src/config.ts +++ b/packages/platform-ios/src/config.ts @@ -11,9 +11,16 @@ export const AppleSimulatorSchema = z.object({ systemVersion: z.string().min(1, 'System version is required'), }); +export const ApplePhysicalDeviceCodeSignSchema = z.object({ + teamId: z.string().min(1, 'Team ID is required'), + signingIdentity: z.string().optional(), + provisioningProfile: z.string().optional(), +}); + export const ApplePhysicalDeviceSchema = z.object({ type: z.literal('physical'), name: z.string().min(1, 'Name is required'), + codeSign: ApplePhysicalDeviceCodeSignSchema.optional(), }); export const AppleDeviceSchema = z.discriminatedUnion('type', [ @@ -29,6 +36,7 @@ export const ApplePlatformConfigSchema = z.object({ }); export type AppleSimulator = z.infer; +export type ApplePhysicalDeviceCodeSign = z.infer; export type ApplePhysicalDevice = z.infer; export type AppleDevice = z.infer; export type ApplePlatformConfig = z.infer; diff --git a/packages/platform-ios/src/factory.ts b/packages/platform-ios/src/factory.ts index a4dfc23..a73af97 100644 --- a/packages/platform-ios/src/factory.ts +++ b/packages/platform-ios/src/factory.ts @@ -2,6 +2,7 @@ import { HarnessPlatform } from '@react-native-harness/platforms'; import type { AppleSimulator, ApplePhysicalDevice, + ApplePhysicalDeviceCodeSign, ApplePlatformConfig, } from './config.js'; @@ -14,9 +15,13 @@ export const appleSimulator = ( systemVersion, }); -export const applePhysicalDevice = (name: string): ApplePhysicalDevice => ({ +export const applePhysicalDevice = ( + name: string, + options?: { codeSign?: ApplePhysicalDeviceCodeSign }, +): ApplePhysicalDevice => ({ type: 'physical', name, + codeSign: options?.codeSign, }); export const applePlatform = ( diff --git a/packages/platform-ios/src/instance.ts b/packages/platform-ios/src/instance.ts index 1dd99b0..a1044fb 100644 --- a/packages/platform-ios/src/instance.ts +++ b/packages/platform-ios/src/instance.ts @@ -223,23 +223,32 @@ export const getApplePhysicalDevicePlatformInstance = async ( ); } - const xctestAgent = createXCTestAgentController({ - appBundleId: config.bundleId, - target: { - kind: 'device', - id: deviceId, - }, - capabilities: [createPermissionPromptAutoAcceptCapability()], - }); - - let agentStarted = false; - try { - await xctestAgent.ensureStarted(); - agentStarted = true; - } finally { - if (!agentStarted) { - await xctestAgent.dispose(); + const xctestAgent = config.device.codeSign + ? createXCTestAgentController({ + appBundleId: config.bundleId, + target: { + kind: 'device', + id: device.hardwareProperties.udid, + codeSign: config.device.codeSign, + }, + capabilities: [createPermissionPromptAutoAcceptCapability()], + }) + : null; + + if (xctestAgent) { + let agentStarted = false; + try { + await xctestAgent.ensureStarted(); + agentStarted = true; + } finally { + if (!agentStarted) { + await xctestAgent.dispose(); + } } + } else { + iosInstanceLogger.info( + 'Skipping XCTest agent for physical device (no codeSign config provided)', + ); } return { @@ -264,7 +273,7 @@ export const getApplePhysicalDevicePlatformInstance = async ( await devicectl.stopApp(deviceId, config.bundleId); }, dispose: async () => { - await xctestAgent.dispose(); + await xctestAgent?.dispose(); await devicectl.stopApp(deviceId, config.bundleId); }, isAppRunning: async () => { diff --git a/packages/platform-ios/src/xcrun/devicectl.ts b/packages/platform-ios/src/xcrun/devicectl.ts index 85d958e..2f6422a 100644 --- a/packages/platform-ios/src/xcrun/devicectl.ts +++ b/packages/platform-ios/src/xcrun/devicectl.ts @@ -35,7 +35,11 @@ export const devicectl = async ( export type AppleDeviceInfo = { identifier: string; + connectionProperties?: AppleDeviceConnectionProperties; deviceProperties: { + dnsName?: string; + hostname?: string; + hostName?: string; name: string; osVersionNumber: string; }; @@ -44,6 +48,23 @@ export type AppleDeviceInfo = { productType: string; udid: string; }; + networkProperties?: AppleDeviceNetworkProperties; +}; + +type AppleDeviceConnectionProperties = { + dnsName?: string; + hostname?: string; + hostName?: string; + potentialHostnames?: string[]; + tunnelIPAddress?: string; + tunnelIPHostname?: string; +}; + +type AppleDeviceNetworkProperties = { + dnsName?: string; + hostname?: string; + hostName?: string; + ipAddress?: string; }; export const listDevices = async (): Promise => { @@ -53,6 +74,65 @@ export const listDevices = async (): Promise => { return result.devices; }; +type AppleDeviceDetailsResult = + | AppleDeviceInfo + | { + device: AppleDeviceInfo; + }; + +export const getDeviceDetails = async ( + identifier: string +): Promise => { + const result = await devicectl('device', [ + 'info', + 'details', + '--device', + identifier, + ]); + + return 'device' in result ? result.device : result; +}; + +export const getDeviceConnectionHost = ( + device: AppleDeviceInfo +): string | null => { + const connection = device.connectionProperties; + const network = device.networkProperties; + + const candidates = [ + connection?.tunnelIPAddress, + connection?.tunnelIPHostname, + connection?.dnsName, + connection?.hostName, + connection?.hostname, + network?.ipAddress, + network?.dnsName, + network?.hostName, + network?.hostname, + device.deviceProperties.dnsName, + device.deviceProperties.hostName, + device.deviceProperties.hostname, + ...(connection?.potentialHostnames ?? []), + ].filter((host): host is string => Boolean(host)); + + return candidates[0] ?? null; +}; + +export const getDeviceHostname = async ( + identifier: string +): Promise => { + const details = await getDeviceDetails(identifier); + const hostname = getDeviceConnectionHost(details); + + if (!hostname) { + throw new Error( + `Could not determine iOS device hostname for ${identifier}. Run "xcrun devicectl device info details --device ${identifier} --json-output " and verify that CoreDevice reports connectionProperties.tunnelIPAddress or a DNS hostname.` + ); + } + + return hostname; +}; + export type AppleAppInfo = { bundleIdentifier: string; name: string; diff --git a/packages/platform-ios/src/xctest-agent-transport-device.ts b/packages/platform-ios/src/xctest-agent-transport-device.ts index 92d588c..4248e1e 100644 --- a/packages/platform-ios/src/xctest-agent-transport-device.ts +++ b/packages/platform-ios/src/xctest-agent-transport-device.ts @@ -1,5 +1,5 @@ -import { utilities } from 'appium-ios-device'; -import type net from 'node:net'; +import http from 'node:http'; +import * as devicectl from './xcrun/devicectl.js'; import type { XCTestAgentTransport, XCTestAgentTransportRequest, @@ -12,139 +12,105 @@ export const createDeviceXCTestAgentTransport = (options: { timeoutMs?: number; }): XCTestAgentTransport => { const timeoutMs = options.timeoutMs ?? 5000; + const agent = new http.Agent({ keepAlive: false }); + const host = devicectl.getDeviceHostname(options.deviceId); return { request: async ( - request: XCTestAgentTransportRequest, + request: XCTestAgentTransportRequest ): Promise => { - const socket = (await utilities.connectPort( - options.deviceId, - options.port, - )) as net.Socket; - - return await performSocketRequest(socket, request, timeoutMs); + return await performHttpRequest({ + agent, + body: request.body, + host: await host, + method: request.method, + path: request.path, + port: options.port, + timeoutMs, + }); + }, + dispose: async () => { + agent.destroy(); }, - dispose: async () => undefined, }; }; -const performSocketRequest = async ( - socket: net.Socket, - request: XCTestAgentTransportRequest, - timeoutMs: number, -): Promise => { +const performHttpRequest = async (options: { + agent: http.Agent; + body?: string; + host: string; + method: 'GET' | 'POST'; + path: string; + port: number; + timeoutMs: number; +}): Promise => { return await new Promise((resolve, reject) => { - let settled = false; - const chunks: Buffer[] = []; - - const finish = (callback: () => void) => { - if (settled) { - return; + const request = http.request( + { + agent: options.agent, + host: options.host, + method: options.method, + path: options.path, + port: options.port, + timeout: options.timeoutMs, + headers: { + ...(options.body === undefined + ? {} + : { + 'content-type': 'application/json', + 'content-length': Buffer.byteLength(options.body, 'utf8'), + }), + connection: 'close', + }, + }, + (response) => { + const chunks: Buffer[] = []; + + response.on('data', (chunk: Buffer | string) => { + chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk); + }); + + response.on('end', () => { + resolve({ + statusCode: response.statusCode ?? 0, + body: Buffer.concat(chunks).toString('utf8'), + headers: getResponseHeaders(response.headers), + }); + }); + + response.on('error', reject); } - - settled = true; - socket.removeAllListeners(); - socket.destroy(); - callback(); - }; - - socket.setTimeout(timeoutMs, () => { - finish(() => { - reject( - new Error( - `Timed out waiting for XCTest agent response after ${timeoutMs}ms`, - ), - ); - }); + ); + + request.on('timeout', () => { + request.destroy( + new Error( + `Timed out waiting for XCTest agent response after ${options.timeoutMs}ms` + ) + ); }); + request.on('error', reject); - socket.on('data', (chunk: Buffer) => { - chunks.push(chunk); - }); - - socket.on('end', () => { - finish(() => { - try { - resolve(parseHttpResponse(Buffer.concat(chunks).toString('utf8'))); - } catch (error) { - reject(error); - } - }); - }); - - socket.on('error', (error) => { - finish(() => { - reject(error); - }); - }); + if (options.body !== undefined) { + request.write(options.body); + } - socket.write(serializeHttpRequest(request)); - socket.end(); + request.end(); }); }; -const serializeHttpRequest = (request: XCTestAgentTransportRequest): string => { - const body = request.body ?? ''; - const bodyLength = Buffer.byteLength(body, 'utf8'); - const headers = [ - `Host: localhost`, - 'Connection: close', - 'Accept: application/json', - ]; +const getResponseHeaders = ( + headers: http.IncomingHttpHeaders +): Record => { + const values: Record = {}; - if (request.body !== undefined) { - headers.push('Content-Type: application/json'); - headers.push(`Content-Length: ${bodyLength}`); - } - - return [ - `${request.method} ${request.path} HTTP/1.1`, - ...headers, - '', - body, - ].join('\r\n'); -}; - -const parseHttpResponse = ( - responseText: string, -): XCTestAgentTransportResponse => { - const separatorIndex = responseText.indexOf('\r\n\r\n'); - - if (separatorIndex === -1) { - throw new Error(`Invalid XCTest agent HTTP response: ${responseText}`); - } - - const rawHeaders = responseText.slice(0, separatorIndex).split('\r\n'); - const statusLine = rawHeaders.shift(); - - if (!statusLine) { - throw new Error('Missing XCTest agent HTTP status line'); - } - - const [, rawStatusCode] = statusLine.split(' '); - const statusCode = Number(rawStatusCode); - - if (!Number.isFinite(statusCode)) { - throw new Error(`Invalid XCTest agent HTTP status code: ${statusLine}`); - } - - const headers: Record = {}; - - for (const header of rawHeaders) { - const separator = header.indexOf(':'); - - if (separator === -1) { + for (const [key, value] of Object.entries(headers)) { + if (value === undefined) { continue; } - const name = header.slice(0, separator).trim().toLowerCase(); - const value = header.slice(separator + 1).trim(); - headers[name] = value; + values[key] = Array.isArray(value) ? value.join(', ') : value; } - return { - statusCode, - headers, - body: responseText.slice(separatorIndex + 4), - }; + return values; }; diff --git a/packages/platform-ios/src/xctest-agent.ts b/packages/platform-ios/src/xctest-agent.ts index ace0ee3..89aa21c 100644 --- a/packages/platform-ios/src/xctest-agent.ts +++ b/packages/platform-ios/src/xctest-agent.ts @@ -1,4 +1,9 @@ -import { logger, spawn, type Subprocess } from '@react-native-harness/tools'; +import { + getAvailablePort, + logger, + spawn, + type Subprocess, +} from '@react-native-harness/tools'; import fs from 'node:fs'; import { createHash } from 'node:crypto'; import path from 'node:path'; @@ -7,6 +12,7 @@ import { createXCTestAgentClient, type XCTestAgentPermissionsConfiguration, } from './xctest-agent-client.js'; +import type { ApplePhysicalDeviceCodeSign } from './config.js'; import type { XCTestAgentTransport } from './xctest-agent-transport.js'; import { createDeviceXCTestAgentTransport } from './xctest-agent-transport-device.js'; import { createSimulatorXCTestAgentTransport } from './xctest-agent-transport-simulator.js'; @@ -15,7 +21,6 @@ const xctestAgentLogger = logger.child('ios-xctest-agent'); const XCTEST_AGENT_PROJECT_NAME = 'HarnessXCTestAgent'; const XCTEST_AGENT_SCHEME_NAME = 'HarnessXCTestAgent'; -const XCTEST_AGENT_DEFAULT_PORT = 49_200; const XCTEST_AGENT_PORT_ENV = 'HARNESS_XCTEST_AGENT_PORT'; const XCTEST_AGENT_TARGET_BUNDLE_ID_ENV = 'HARNESS_XCTEST_AGENT_TARGET_BUNDLE_ID'; @@ -33,12 +38,13 @@ type XCTestAgentTarget = | { kind: 'device'; id: string; + codeSign: ApplePhysicalDeviceCodeSign; }; export type XCTestAgentCapability = { getLaunchEnvironment?: () => Record; updateConfiguration?: ( - configuration: XCTestAgentRuntimeConfiguration, + configuration: XCTestAgentRuntimeConfiguration ) => XCTestAgentRuntimeConfiguration; }; @@ -49,6 +55,7 @@ export type XCTestAgentRuntimeConfiguration = { type XCTestAgentBuildManifest = { buildInputsHash: string; destinationKind: XCTestAgentTarget['kind']; + codeSign?: ApplePhysicalDeviceCodeSign; }; export type XCTestAgentController = { @@ -65,7 +72,7 @@ const getXCTestAgentProjectRoot = (): string => { const getXCTestAgentProjectFilePath = (): string => { return path.join( getXCTestAgentProjectRoot(), - `${XCTEST_AGENT_PROJECT_NAME}.xcodeproj`, + `${XCTEST_AGENT_PROJECT_NAME}.xcodeproj` ); }; @@ -77,7 +84,7 @@ const assertXCTestAgentProjectExists = () => { } throw new Error( - `Missing checked-in XCTest agent project at ${projectFilePath}. Include the checked-in project in the package artifact.`, + `Missing checked-in XCTest agent project at ${projectFilePath}. Include the checked-in project in the package artifact.` ); }; @@ -92,22 +99,54 @@ const getXCTestAgentDerivedDataPath = (target: XCTestAgentTarget): string => { const getXCTestAgentBuildManifestPath = (target: XCTestAgentTarget): string => { return path.join( getXCTestAgentDerivedDataPath(target), - 'build-manifest.json', + 'build-manifest.json' ); }; -const getXCTestAgentDestination = (target: XCTestAgentTarget): string => { +const getXCTestAgentBuildDestination = (target: XCTestAgentTarget): string => { + return target.kind === 'simulator' + ? `platform=iOS Simulator,id=${target.id}` + : `generic/platform=iOS`; +}; + +const getXCTestAgentRunDestination = (target: XCTestAgentTarget): string => { return target.kind === 'simulator' ? `platform=iOS Simulator,id=${target.id}` : `platform=iOS,id=${target.id}`; }; +const getXCTestAgentBuildSigningArgs = ( + target: XCTestAgentTarget +): string[] => { + if (target.kind === 'simulator') { + return [ + 'CODE_SIGNING_ALLOWED=NO', + 'CODE_SIGNING_REQUIRED=NO', + 'CODE_SIGN_IDENTITY=', + 'DEVELOPMENT_TEAM=', + ]; + } + + const { teamId, signingIdentity, provisioningProfile } = target.codeSign; + const args = [ + 'CODE_SIGN_STYLE=Automatic', + `DEVELOPMENT_TEAM=${teamId}`, + `CODE_SIGN_IDENTITY=${signingIdentity ?? 'Apple Development'}`, + ]; + + if (provisioningProfile) { + args.push(`PROVISIONING_PROFILE_SPECIFIER=${provisioningProfile}`); + } + + return args; +}; + const getXCTestAgentBuildProductsPath = (target: XCTestAgentTarget): string => { return path.join(getXCTestAgentDerivedDataPath(target), 'Build', 'Products'); }; const readBuildManifest = ( - target: XCTestAgentTarget, + target: XCTestAgentTarget ): XCTestAgentBuildManifest | null => { const manifestPath = getXCTestAgentBuildManifestPath(target); @@ -116,18 +155,18 @@ const readBuildManifest = ( } return JSON.parse( - fs.readFileSync(manifestPath, 'utf8'), + fs.readFileSync(manifestPath, 'utf8') ) as XCTestAgentBuildManifest; }; const writeBuildManifest = ( target: XCTestAgentTarget, - manifest: XCTestAgentBuildManifest, + manifest: XCTestAgentBuildManifest ) => { fs.mkdirSync(getXCTestAgentDerivedDataPath(target), { recursive: true }); fs.writeFileSync( getXCTestAgentBuildManifestPath(target), - JSON.stringify(manifest, null, 2), + JSON.stringify(manifest, null, 2) ); }; @@ -169,7 +208,7 @@ const getProjectInputsHash = (): string => { const shouldReuseBuildArtifacts = ( target: XCTestAgentTarget, - buildInputsHash: string, + buildInputsHash: string ): boolean => { const manifest = readBuildManifest(target); @@ -184,6 +223,17 @@ const shouldReuseBuildArtifacts = ( return false; } + if (target.kind === 'device') { + if ( + manifest.codeSign?.teamId !== target.codeSign.teamId || + manifest.codeSign?.signingIdentity !== target.codeSign.signingIdentity || + manifest.codeSign?.provisioningProfile !== + target.codeSign.provisioningProfile + ) { + return false; + } + } + return fs.existsSync(getXCTestAgentBuildProductsPath(target)); }; @@ -196,7 +246,7 @@ const getDefaultRuntimeConfiguration = (): XCTestAgentRuntimeConfiguration => { }; const getRuntimeConfiguration = ( - capabilities: XCTestAgentCapability[], + capabilities: XCTestAgentCapability[] ): XCTestAgentRuntimeConfiguration => { return capabilities.reduce((configuration, capability) => { return capability.updateConfiguration?.(configuration) ?? configuration; @@ -225,7 +275,9 @@ const waitForAgentReady = async (options: { } throw new Error( - `Timed out waiting for XCTest agent readiness: ${getErrorMessage(lastError)}`, + `Timed out waiting for XCTest agent readiness: ${getErrorMessage( + lastError + )}` ); }; @@ -257,7 +309,6 @@ const waitForChildProcessExit = async (subprocess: Subprocess) => { const cleanup = () => { childProcess.off('close', finish); childProcess.off('error', finish); - childProcess.off('exit', finish); }; const finish = () => { @@ -267,7 +318,6 @@ const waitForChildProcessExit = async (subprocess: Subprocess) => { childProcess.once('close', finish); childProcess.once('error', finish); - childProcess.once('exit', finish); }); }; @@ -303,7 +353,7 @@ const stopProcess = async (options: { xctestAgentLogger.warn( 'XCTest agent session for %s target did not stop after %dms; forcing shutdown', options.targetKind, - options.shutdownTimeoutMs, + options.shutdownTimeoutMs ); childProcess.kill('SIGKILL'); @@ -313,6 +363,11 @@ const stopProcess = async (options: { }); }; +const toTestRunnerEnv = (env: Record): Record => + Object.fromEntries( + Object.entries(env).map(([key, value]) => [`TEST_RUNNER_${key}`, value]) + ); + const getErrorMessage = (error: unknown): string => { if (!error) { return 'unknown error'; @@ -349,18 +404,12 @@ export const createXCTestAgentController = (options: { } : {}, ...capabilities.map( - (capability) => capability.getLaunchEnvironment?.() ?? {}, - ), + (capability) => capability.getLaunchEnvironment?.() ?? {} + ) ); }; - const getPort = async (): Promise => { - return options.port ?? XCTEST_AGENT_DEFAULT_PORT; - }; - - const createTransport = async (): Promise => { - const port = await getPort(); - + const createTransport = (port: number): XCTestAgentTransport => { if (target.kind === 'simulator') { return createSimulatorXCTestAgentTransport({ port }); } @@ -380,11 +429,11 @@ export const createXCTestAgentController = (options: { xctestAgentLogger.debug( 'verifying checked-in XCTest agent project for %s', - target.kind, + target.kind ); xctestAgentLogger.info( 'Using checked-in XCTest agent project for %s target', - target.kind, + target.kind ); assertXCTestAgentProjectExists(); @@ -392,11 +441,11 @@ export const createXCTestAgentController = (options: { prepared = true; xctestAgentLogger.info( 'Reusing cached XCTest agent build for %s target', - target.kind, + target.kind ); xctestAgentLogger.debug( 'reusing cached XCTest agent build for %s', - target.kind, + target.kind ); return; } @@ -412,14 +461,17 @@ export const createXCTestAgentController = (options: { '-scheme', XCTEST_AGENT_SCHEME_NAME, '-destination', - getXCTestAgentDestination(target), + getXCTestAgentBuildDestination(target), '-derivedDataPath', getXCTestAgentDerivedDataPath(target), + ...(target.kind === 'device' ? ['-allowProvisioningUpdates'] : []), + ...getXCTestAgentBuildSigningArgs(target), ]); writeBuildManifest(target, { buildInputsHash, destinationKind: target.kind, + codeSign: target.kind === 'device' ? target.codeSign : undefined, }); xctestAgentLogger.info('Built XCTest agent for %s target', target.kind); prepared = true; @@ -432,14 +484,15 @@ export const createXCTestAgentController = (options: { return; } - const port = await getPort(); + const port = options.port ?? (await getAvailablePort()); const runtimeConfiguration = getRuntimeConfiguration(capabilities); xctestAgentLogger.debug('starting XCTest agent for %s', target.kind); xctestAgentLogger.info( 'Starting XCTest agent session for %s target', - target.kind, + target.kind ); + xctestAgentLogger.debug('Using XCTest agent port %d', port); agentProcess = spawn( 'xcodebuild', [ @@ -449,7 +502,7 @@ export const createXCTestAgentController = (options: { '-scheme', XCTEST_AGENT_SCHEME_NAME, '-destination', - getXCTestAgentDestination(target), + getXCTestAgentRunDestination(target), '-parallel-testing-enabled', 'NO', '-maximum-parallel-testing-workers', @@ -461,16 +514,23 @@ export const createXCTestAgentController = (options: { cwd: getXCTestAgentProjectRoot(), env: { ...process.env, - [XCTEST_AGENT_PORT_ENV]: String(port), - ...getLaunchEnvironment(), + ...toTestRunnerEnv({ + [XCTEST_AGENT_PORT_ENV]: String(port), + ...getLaunchEnvironment(), + }), }, - stdout: 'pipe', - stderr: 'pipe', - }, + stdout: 'ignore', + stderr: 'ignore', + } ); const currentProcess = agentProcess; - const transport = await createTransport(); + if (typeof currentProcess.catch === 'function') { + void currentProcess.catch((error) => { + xctestAgentLogger.debug('XCTest agent process stopped', error); + }); + } + const transport = createTransport(port); const client = createXCTestAgentClient(transport); agentClient = client; @@ -482,16 +542,6 @@ export const createXCTestAgentController = (options: { } }); - void (async () => { - try { - for await (const line of currentProcess) { - xctestAgentLogger.info('[agent:%s] %s', target.kind, line); - } - } catch (error) { - xctestAgentLogger.debug('XCTest agent process stopped', error); - } - })(); - try { await waitForAgentReady({ client, @@ -502,7 +552,7 @@ export const createXCTestAgentController = (options: { xctestAgentLogger.warn( 'XCTest agent startup failed for %s: %s', target.kind, - getErrorMessage(error), + getErrorMessage(error) ); await transport.dispose(); agentClient = null; @@ -526,7 +576,7 @@ export const createXCTestAgentController = (options: { xctestAgentLogger.info( 'Stopping XCTest agent session for %s target', - target.kind, + target.kind ); await currentClient?.dispose(); diff --git a/packages/platform-ios/xctest-agent/HarnessXCTestAgentUITests/HarnessXCTestAgentUITests.swift b/packages/platform-ios/xctest-agent/HarnessXCTestAgentUITests/HarnessXCTestAgentUITests.swift index d4c507c..31abf4b 100644 --- a/packages/platform-ios/xctest-agent/HarnessXCTestAgentUITests/HarnessXCTestAgentUITests.swift +++ b/packages/platform-ios/xctest-agent/HarnessXCTestAgentUITests/HarnessXCTestAgentUITests.swift @@ -190,7 +190,7 @@ final class HarnessXCTestAgentUITests: XCTestCase { private let state = HarnessXCTestAgentState( permissions: PermissionPromptConfiguration.fromEnvironment() ) - private lazy var targetApplication = makeTargetApplication() + private var lastTargetApplicationState: XCUIApplication.State? private let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard") private var capabilities: [AgentCapability] = [] private var httpServer: XCTestAgentHTTPServer? @@ -199,12 +199,26 @@ final class HarnessXCTestAgentUITests: XCTestCase { NSLog("[HarnessXCTestAgent] %@", message) } - private func makeTargetApplication() -> XCUIApplication { + private func makeTargetApplication() -> XCUIApplication? { if let bundleIdentifier = ProcessInfo.processInfo.environment[Environment.targetBundleIdentifier], !bundleIdentifier.isEmpty { return XCUIApplication(bundleIdentifier: bundleIdentifier) } - return XCUIApplication() + return nil + } + + private func observeTargetApplication() { + guard let targetApplication = makeTargetApplication() else { + return + } + + let currentState = targetApplication.state + if currentState == lastTargetApplicationState { + return + } + + lastTargetApplicationState = currentState + log("target application state changed: \(String(describing: currentState))") } private func jsonResponse(_ value: T) -> XCTestAgentResponse { @@ -266,8 +280,6 @@ final class HarnessXCTestAgentUITests: XCTestCase { log("setUpWithError started") log("enabled capabilities: \(capabilities.map { String(describing: type(of: $0)) }.joined(separator: ", "))") - targetApplication.launch() - for capability in capabilities { try capability.setUp() } @@ -290,6 +302,8 @@ final class HarnessXCTestAgentUITests: XCTestCase { let sessionDeadline = Date().addingTimeInterval(Constants.defaultSessionDuration) while Date() < sessionDeadline { + observeTargetApplication() + for capability in capabilities { try? capability.tick() } diff --git a/packages/tools/src/index.ts b/packages/tools/src/index.ts index e5561b3..48b56eb 100644 --- a/packages/tools/src/index.ts +++ b/packages/tools/src/index.ts @@ -1,4 +1,5 @@ export * from './abort.js'; +export * from './net.js'; export * from './color.js'; export * from './logger.js'; export * from './prompts.js'; diff --git a/packages/tools/src/net.ts b/packages/tools/src/net.ts new file mode 100644 index 0000000..d9e96b1 --- /dev/null +++ b/packages/tools/src/net.ts @@ -0,0 +1,13 @@ +import net from 'node:net'; + +export const getAvailablePort = (): Promise => + new Promise((resolve, reject) => { + const server = net.createServer(); + + server.listen(0, '127.0.0.1', () => { + const { port } = server.address() as net.AddressInfo; + server.close((err) => (err ? reject(err) : resolve(port))); + }); + + server.on('error', reject); + }); diff --git a/packages/tools/src/spawn.ts b/packages/tools/src/spawn.ts index 348274d..65b0bbe 100644 --- a/packages/tools/src/spawn.ts +++ b/packages/tools/src/spawn.ts @@ -10,10 +10,9 @@ export const spawn = ( args?: readonly string[], options?: SpawnOptions ): Subprocess => { - const defaultStream = 'pipe'; const defaultOptions: Options = { - stdin: defaultStream, - stdout: defaultStream, + stdin: 'ignore', + stdout: 'pipe', // Always 'pipe' stderr to handle errors properly down the line stderr: 'pipe', }; @@ -25,7 +24,11 @@ export const spawn = ( return childProcess; }; -export const spawnAndForget = async (file: string, args?: readonly string[], options?: SpawnOptions): Promise => { +export const spawnAndForget = async ( + file: string, + args?: readonly string[], + options?: SpawnOptions +): Promise => { try { await spawn(file, args, options); } catch { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 863697d..b3621a9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -439,9 +439,6 @@ importers: '@react-native-harness/tools': specifier: workspace:* version: link:../tools - appium-ios-device: - specifier: ^3.1.11 - version: 3.1.11 tslib: specifier: ^2.3.0 version: 2.8.1 @@ -615,26 +612,6 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} - '@appium/logger@2.0.6': - resolution: {integrity: sha512-9e8n9CtINBwi1ASEU5OyswmR2F7OnbrGfmf9yTy9i+rx4GR9RJlEp0/arsxvuyWCep67tOmM4FiRyXxxHjOK5Q==} - engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: '>=10'} - - '@appium/schema@1.1.0': - resolution: {integrity: sha512-m0vTLU7mhC9RR294Nz84g+FhEQ0iZKq6p3rfz1+qfEqCXRXUvDbllSOu2tCVpBKMIoEFZAmkwjuwXobJpCnilQ==} - engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: '>=10'} - - '@appium/support@7.1.0': - resolution: {integrity: sha512-kY4Qv4TzLCYmZnN2eNptEa8RiRzpbimIQ6tKuDaqLC2Y3q5Al4NumL/xRQAvfXJq/hNezq2Jh8NwciEW8zX/0g==} - engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: '>=10'} - - '@appium/tsconfig@1.1.2': - resolution: {integrity: sha512-lHKBm7hXCROc1Ha/cBxS4o3iQkeY96Pz7qM9Uh9vFDkdpTGBk56V1lmc3iGcgBYKBlaRT/LZmTsqClvHoiXhvw==} - engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: '>=10'} - - '@appium/types@1.3.0': - resolution: {integrity: sha512-Gv4ev/5K5N7TvAHqem2DmB50zipC951QlmCDpuxDNHQl2dtCr20vJgnN8if7upqLcBX/6yNp3udR+f1n99zgcQ==} - engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: '>=10'} - '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} @@ -1344,16 +1321,9 @@ packages: '@clack/prompts@1.0.0-alpha.9': resolution: {integrity: sha512-sKs0UjiHFWvry4SiRfBi5Qnj0C/6AYx8aKkFPZQSuUZXgAram25ZDmhQmP7vj1aFyLpfHWtLQjWvOvcat0TOLg==} - '@colors/colors@1.6.0': - resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} - engines: {node: '>=0.1.90'} - '@emnapi/core@1.5.0': resolution: {integrity: sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==} - '@emnapi/runtime@1.10.0': - resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} - '@emnapi/runtime@1.5.0': resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==} @@ -1734,143 +1704,6 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} - '@img/colour@1.1.0': - resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} - engines: {node: '>=18'} - - '@img/sharp-darwin-arm64@0.34.5': - resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [darwin] - - '@img/sharp-darwin-x64@0.34.5': - resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [darwin] - - '@img/sharp-libvips-darwin-arm64@1.2.4': - resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} - cpu: [arm64] - os: [darwin] - - '@img/sharp-libvips-darwin-x64@1.2.4': - resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} - cpu: [x64] - os: [darwin] - - '@img/sharp-libvips-linux-arm64@1.2.4': - resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} - cpu: [arm64] - os: [linux] - - '@img/sharp-libvips-linux-arm@1.2.4': - resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} - cpu: [arm] - os: [linux] - - '@img/sharp-libvips-linux-ppc64@1.2.4': - resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} - cpu: [ppc64] - os: [linux] - - '@img/sharp-libvips-linux-riscv64@1.2.4': - resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} - cpu: [riscv64] - os: [linux] - - '@img/sharp-libvips-linux-s390x@1.2.4': - resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} - cpu: [s390x] - os: [linux] - - '@img/sharp-libvips-linux-x64@1.2.4': - resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} - cpu: [x64] - os: [linux] - - '@img/sharp-libvips-linuxmusl-arm64@1.2.4': - resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} - cpu: [arm64] - os: [linux] - - '@img/sharp-libvips-linuxmusl-x64@1.2.4': - resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} - cpu: [x64] - os: [linux] - - '@img/sharp-linux-arm64@0.34.5': - resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [linux] - - '@img/sharp-linux-arm@0.34.5': - resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm] - os: [linux] - - '@img/sharp-linux-ppc64@0.34.5': - resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [ppc64] - os: [linux] - - '@img/sharp-linux-riscv64@0.34.5': - resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [riscv64] - os: [linux] - - '@img/sharp-linux-s390x@0.34.5': - resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [s390x] - os: [linux] - - '@img/sharp-linux-x64@0.34.5': - resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [linux] - - '@img/sharp-linuxmusl-arm64@0.34.5': - resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [linux] - - '@img/sharp-linuxmusl-x64@0.34.5': - resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [linux] - - '@img/sharp-wasm32@0.34.5': - resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [wasm32] - - '@img/sharp-win32-arm64@0.34.5': - resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [win32] - - '@img/sharp-win32-ia32@0.34.5': - resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [ia32] - os: [win32] - - '@img/sharp-win32-x64@0.34.5': - resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [win32] - '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -2860,9 +2693,6 @@ packages: '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} - '@sec-ant/readable-stream@0.4.1': - resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} - '@selderee/plugin-htmlparser2@0.11.0': resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} @@ -3097,9 +2927,6 @@ packages: resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} engines: {node: '>=10.13.0'} - '@tsconfig/node20@20.1.9': - resolution: {integrity: sha512-IjlTv1RsvnPtUcjTqtVsZExKVq+KQx4g5pCP5tI7rAs6Xesl2qFwSz/tPDBC4JajkL/MlezBu3gPUwqRHl+RIg==} - '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} @@ -3193,9 +3020,6 @@ packages: '@types/node@20.19.25': resolution: {integrity: sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==} - '@types/normalize-package-data@2.4.4': - resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} - '@types/parse-json@4.0.2': resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} @@ -3588,10 +3412,6 @@ packages: '@webassemblyjs/wast-printer@1.14.1': resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} - '@xmldom/xmldom@0.8.13': - resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==} - engines: {node: '>=10.0.0'} - '@xtuc/ieee754@1.2.0': resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} @@ -3729,18 +3549,6 @@ packages: appdirsjs@1.2.7: resolution: {integrity: sha512-Quji6+8kLBC3NnBeo14nPDq0+2jUs5s3/xEye+udFHumHhRk4M7aAMXp/PBJqkKYGuuyR9M/6Dq7d2AViiGmhw==} - appium-ios-device@3.1.11: - resolution: {integrity: sha512-ccW8jAfZTtKc6mvFbbHCkVbB8/OxOdBolAB/sAHmwGl0jDrCrzMWOINkx1EdZx6QIrNPAw11Op1HibIRU66RWA==} - engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: '>=10'} - - archiver-utils@5.0.2: - resolution: {integrity: sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==} - engines: {node: '>= 14'} - - archiver@7.0.1: - resolution: {integrity: sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==} - engines: {node: '>= 14'} - argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} @@ -3821,10 +3629,6 @@ packages: async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} - asyncbox@6.1.0: - resolution: {integrity: sha512-KZwKNVnDdDe0ubN+fFMuHhSljZNHnbjdJABImoqFzQP61oIg6sMlhXIqOIu3WRd7YwW89q+eVj2Ty/Ax5dbh2Q==} - engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: '>=10'} - asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} @@ -3850,21 +3654,10 @@ packages: axios@1.13.2: resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==} - axios@1.15.0: - resolution: {integrity: sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==} - axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} - b4a@1.8.0: - resolution: {integrity: sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==} - peerDependencies: - react-native-b4a: '*' - peerDependenciesMeta: - react-native-b4a: - optional: true - babel-jest@29.7.0: resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -3958,57 +3751,9 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - balanced-match@4.0.4: - resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} - engines: {node: 18 || 20 || >=22} - - bare-events@2.8.2: - resolution: {integrity: sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==} - peerDependencies: - bare-abort-controller: '*' - peerDependenciesMeta: - bare-abort-controller: - optional: true - - bare-fs@4.7.1: - resolution: {integrity: sha512-WDRsyVN52eAx/lBamKD6uyw8H4228h/x0sGGGegOamM2cd7Pag88GfMQalobXI+HaEUxpCkbKQUDOQqt9wawRw==} - engines: {bare: '>=1.16.0'} - peerDependencies: - bare-buffer: '*' - peerDependenciesMeta: - bare-buffer: - optional: true - - bare-os@3.9.0: - resolution: {integrity: sha512-JTjuZyNIDpw+GytMO4a6TK1VXdVKKJr6DRxEHasyuYyShV2deuiHJK/ahGZlebc+SG0/wJCB9XK8gprBGDFi/Q==} - engines: {bare: '>=1.14.0'} - - bare-path@3.0.0: - resolution: {integrity: sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==} - - bare-stream@2.13.0: - resolution: {integrity: sha512-3zAJRZMDFGjdn+RVnNpF9kuELw+0Fl3lpndM4NcEOhb9zwtSo/deETfuIwMSE5BXanA0FrN1qVjffGwAg2Y7EA==} - peerDependencies: - bare-abort-controller: '*' - bare-buffer: '*' - bare-events: '*' - peerDependenciesMeta: - bare-abort-controller: - optional: true - bare-buffer: - optional: true - bare-events: - optional: true - - bare-url@2.4.2: - resolution: {integrity: sha512-/9a2j4ac6ckpmAHvod/ob7x439OAHst/drc2Clnq+reRYd/ovddwcF4LfoxHyNk5AuGBnPg+HqFjmE/Zpq6v0A==} - base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - base64-stream@1.0.0: - resolution: {integrity: sha512-BQQZftaO48FcE1Kof9CmXMFaAdqkcNorgc8CxesZv9nMbbTF1EFyQe89UOuh//QMmdtfUDXyO8rgUalemL5ODA==} - baseline-browser-mapping@2.8.29: resolution: {integrity: sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA==} hasBin: true @@ -4017,10 +3762,6 @@ packages: resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==} engines: {node: '>= 0.8'} - big-integer@1.6.52: - resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} - engines: {node: '>=0.6'} - big.js@5.2.2: resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==} @@ -4034,9 +3775,6 @@ packages: bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} - bluebird@3.7.2: - resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} - body-parser@1.20.3: resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} @@ -4047,23 +3785,12 @@ packages: boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - bplist-creator@0.1.1: - resolution: {integrity: sha512-Ese7052fdWrxp/vqSJkydgx/1MdBnNOCV2XVfbmdGWD2H6EYza+Q4pyYSuVSnCUD22hfI/BFI4jHaC3NLXLlJQ==} - - bplist-parser@0.3.2: - resolution: {integrity: sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==} - engines: {node: '>= 5.10.0'} - brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} brace-expansion@2.0.2: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} - brace-expansion@5.0.5: - resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} - engines: {node: 18 || 20 || >=22} - braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -4086,22 +3813,12 @@ packages: engines: {node: '>= 0.4.0'} hasBin: true - buffer-crc32@0.2.13: - resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} - - buffer-crc32@1.0.0: - resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} - engines: {node: '>=8.0.0'} - buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} - buffer@6.0.3: - resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} - bundle-require@5.1.0: resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -4321,10 +4038,6 @@ packages: commondir@1.0.1: resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} - compress-commons@6.0.2: - resolution: {integrity: sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==} - engines: {node: '>= 14'} - compressible@2.0.18: resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} engines: {node: '>= 0.6'} @@ -4356,9 +4069,6 @@ packages: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} - console-control-strings@1.1.0: - resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} - content-disposition@0.5.4: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} @@ -4394,9 +4104,6 @@ packages: core-js@3.47.0: resolution: {integrity: sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==} - core-util-is@1.0.3: - resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} - corser@2.0.1: resolution: {integrity: sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==} engines: {node: '>= 0.4.0'} @@ -4423,15 +4130,6 @@ packages: typescript: optional: true - crc-32@1.2.2: - resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} - engines: {node: '>=0.8'} - hasBin: true - - crc32-stream@6.0.0: - resolution: {integrity: sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==} - engines: {node: '>= 14'} - cron-parser@4.9.0: resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} engines: {node: '>=12.0.0'} @@ -4633,10 +4331,6 @@ packages: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - detect-libc@2.1.2: - resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} - engines: {node: '>=8'} - detect-newline@3.1.0: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} @@ -4700,9 +4394,6 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} - duplexer@0.1.2: - resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} - eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -5069,9 +4760,6 @@ packages: eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} - events-universal@1.0.1: - resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} - events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} @@ -5113,9 +4801,6 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - fast-fifo@1.3.2: - resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} - fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} @@ -5206,10 +4891,6 @@ packages: resolution: {integrity: sha512-WgZ+nKbELDa6N3i/9nrHeNznm+lY3z4YfhDDWgW+5P0pdmMj26bxaxU11ookgY3NyP9GC7HvZ9etp0jRFqGEeQ==} engines: {node: '>=8'} - find-up-simple@1.0.1: - resolution: {integrity: sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==} - engines: {node: '>=18'} - find-up@3.0.0: resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} engines: {node: '>=6'} @@ -5251,15 +4932,6 @@ packages: debug: optional: true - follow-redirects@1.16.0: - resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} - engines: {node: '>=4.0'} - peerDependencies: - debug: '*' - peerDependenciesMeta: - debug: - optional: true - for-each@0.3.5: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} @@ -5318,10 +4990,6 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] - ftp-response-parser@1.0.1: - resolution: {integrity: sha512-++Ahlo2hs/IC7UVQzjcSAfeUpCwTTzs4uvG5XfGnsinIFkWUYF4xWwPd5qZuK8MJrmUIxFMuHcfqaosCDjvIWw==} - engines: {node: '>=0.8.0'} - function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} @@ -5359,10 +5027,6 @@ packages: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} - get-stream@9.0.1: - resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} - engines: {node: '>=18'} - get-symbol-description@1.1.0: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} @@ -5391,10 +5055,6 @@ packages: deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true - glob@13.0.6: - resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} - engines: {node: 18 || 20 || >=22} - glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me @@ -5537,10 +5197,6 @@ packages: hookable@6.1.0: resolution: {integrity: sha512-ZoKZSJgu8voGK2geJS+6YtYjvIzu9AOM/KZXsBxr83uhLL++e9pEv/dlgwgy3dvHg06kTz6JOh1hk3C8Ceiymw==} - hosted-git-info@9.0.2: - resolution: {integrity: sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg==} - engines: {node: ^20.17.0 || >=22.9.0} - html-encoding-sniffer@3.0.0: resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} engines: {node: '>=12'} @@ -5663,10 +5319,6 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} - index-to-position@1.2.0: - resolution: {integrity: sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==} - engines: {node: '>=18'} - inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. @@ -5843,10 +5495,6 @@ packages: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} - is-stream@4.0.1: - resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} - engines: {node: '>=18'} - is-string@1.1.1: resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} engines: {node: '>= 0.4'} @@ -5863,10 +5511,6 @@ packages: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} - is-unicode-supported@2.1.0: - resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} - engines: {node: '>=18'} - is-weakmap@2.0.2: resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} engines: {node: '>= 0.4'} @@ -5891,22 +5535,12 @@ packages: resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} engines: {node: '>=8'} - isarray@0.0.1: - resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==} - - isarray@1.0.0: - resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} - isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - isexe@4.0.0: - resolution: {integrity: sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==} - engines: {node: '>=20'} - isomorphic-ws@5.0.0: resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} peerDependencies: @@ -6167,10 +5801,6 @@ packages: engines: {node: '>=6'} hasBin: true - jsftp@2.1.3: - resolution: {integrity: sha512-r79EVB8jaNAZbq8hvanL8e8JGu2ZNr2bXdHC4ZdQhRImpSPpnWwm5DYVzQ5QxJmtGtKhNNuvqGgbNaFl604fEQ==} - engines: {node: '>=6'} - json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} @@ -6183,9 +5813,6 @@ packages: json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} - json-schema@0.4.0: - resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} - json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -6227,10 +5854,6 @@ packages: resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} engines: {node: '>=0.10.0'} - klaw@4.1.0: - resolution: {integrity: sha512-1zGZ9MF9H22UnkpVeuaGKOjfA2t6WrfdrJmGjy16ykcjnKQDmHVX+KI477rpbGevz/5FD4MC3xf1oxylBgcaQw==} - engines: {node: '>=14.14.0'} - kleur@3.0.3: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} @@ -6252,10 +5875,6 @@ packages: launch-editor@2.12.0: resolution: {integrity: sha512-giOHXoOtifjdHqUamwKq6c49GzBdLjvxrd2D+Q4V6uOHopJv7p9VJxikDsQ/CBXZbEITgUqSVHXLTG3VhPP1Dg==} - lazystream@1.0.1: - resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} - engines: {node: '>= 0.6.3'} - leac@0.6.0: resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==} @@ -6313,9 +5932,6 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} - lockfile@1.0.4: - resolution: {integrity: sha512-cvbTwETRfsFh4nHsL1eGWapU1XFi5Ot9E85sWAwia7Y7EgB7vfqcZhTKZ+l7hCGxSPoushMv5GKhT5PdLv03WA==} - lodash-es@4.17.23: resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==} @@ -6343,17 +5959,10 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - lodash@4.18.1: - resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} - log-symbols@4.1.0: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} - log-symbols@7.0.1: - resolution: {integrity: sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==} - engines: {node: '>=18'} - log4js@6.9.1: resolution: {integrity: sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==} engines: {node: '>=8.0'} @@ -6381,14 +5990,6 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - lru-cache@11.3.3: - resolution: {integrity: sha512-JvNw9Y81y33E+BEYPr0U7omo+U9AySnsMsEiXgwT6yqd31VQWTLNQqmT4ou5eqPFUrTfIDFta2wKhB1hyohtAQ==} - engines: {node: 20 || >=22} - - lru-cache@11.3.5: - resolution: {integrity: sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==} - engines: {node: 20 || >=22} - lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -6721,10 +6322,6 @@ packages: resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==} hasBin: true - minimatch@10.2.5: - resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} - engines: {node: 18 || 20 || >=22} - minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -6755,10 +6352,6 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} - minipass@7.1.3: - resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} - engines: {node: '>=16 || 14 >=14.17'} - mkdirp@1.0.4: resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} engines: {node: '>=10'} @@ -6797,10 +6390,6 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - ncp@2.0.0: - resolution: {integrity: sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==} - hasBin: true - negotiator@0.6.3: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} @@ -6852,10 +6441,6 @@ packages: resolution: {integrity: sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==} engines: {node: '>=0.12.0'} - normalize-package-data@8.0.0: - resolution: {integrity: sha512-RWk+PI433eESQ7ounYxIp67CYuVsS1uYSonX3kA6ps/3LWfjVQa/ptEg6Y3T6uAMq1mWpX9PQ+qx+QaHpsc7gQ==} - engines: {node: ^20.17.0 || >=22.9.0} - normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} @@ -7005,10 +6590,6 @@ packages: resolution: {integrity: sha512-i8PyM2JnsNChVSYWLr2BAjNoLi0BAYC+wecOnZnVV+YSNJkzP7cWmvI34dk0WArWfH9KwBHNoZI3P3MppImlIA==} engines: {node: '>=20'} - p-limit@7.3.0: - resolution: {integrity: sha512-7cIXg/Z0M5WZRblrsOla88S4wAK+zOQQWeBYfV3qJuJXMr+LnbYjaadrFaS0JILfEDPVqHyKnZ1Z/1d6J9VVUw==} - engines: {node: '>=20'} - p-locate@3.0.0: resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==} engines: {node: '>=6'} @@ -7033,10 +6614,6 @@ packages: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} - package-directory@8.2.0: - resolution: {integrity: sha512-qJSu5Mo6tHmRxCy2KCYYKYgcfBdUpy9dwReaZD/xwf608AUk/MoRtIOWzgDtUeGeC7n/55yC3MI1Q+MbSoektw==} - engines: {node: '>=18'} - package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} @@ -7051,14 +6628,6 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} - parse-json@8.3.0: - resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==} - engines: {node: '>=18'} - - parse-listing@1.1.3: - resolution: {integrity: sha512-a1p1i+9Qyc8pJNwdrSvW1g5TPxRH0sywVi6OzVvYHRo6xwF9bDWBxtH0KkxeOOvhUE8vAMtiSfsYQFOuK901eA==} - engines: {node: '>=0.6.21'} - parse-passwd@1.0.0: resolution: {integrity: sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==} engines: {node: '>=0.10.0'} @@ -7096,10 +6665,6 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} - path-scurry@2.0.2: - resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} - engines: {node: 18 || 20 || >=22} - path-to-regexp@0.1.12: resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} @@ -7117,9 +6682,6 @@ packages: peberminta@0.9.0: resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==} - pend@1.2.0: - resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} - picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -7168,14 +6730,6 @@ packages: engines: {node: '>=18'} hasBin: true - plist@3.1.0: - resolution: {integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==} - engines: {node: '>=10.4.0'} - - pluralize@8.0.0: - resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} - engines: {node: '>=4'} - pngjs@7.0.0: resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==} engines: {node: '>=14.19.0'} @@ -7440,13 +6994,6 @@ packages: resolution: {integrity: sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - process-nextick-args@2.0.1: - resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} - - process@0.11.10: - resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} - engines: {node: '>= 0.6.0'} - promise.series@0.2.0: resolution: {integrity: sha512-VWQJyU2bcDTgZw8kpfBpB/ejZASlCrzwz5f2hjb/zlujOEB4oeiAhHygAWq8ubsX2GVkD4kCU5V2dwOTaCY5EQ==} engines: {node: '>=0.12'} @@ -7477,10 +7024,6 @@ packages: proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} - proxy-from-env@2.1.0: - resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} - engines: {node: '>=10'} - psl@1.15.0: resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} @@ -7621,27 +7164,10 @@ packages: resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==} engines: {node: '>=0.10.0'} - read-pkg@10.1.0: - resolution: {integrity: sha512-I8g2lArQiP78ll51UeMZojewtYgIRCKCWqZEgOO8c/uefTI+XDXvCSXu3+YNUaTNvZzobrL5+SqHjBrByRRTdg==} - engines: {node: '>=20'} - - readable-stream@1.1.14: - resolution: {integrity: sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==} - - readable-stream@2.3.8: - resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} - readable-stream@3.6.2: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} - readable-stream@4.7.0: - resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - readdir-glob@1.1.3: - resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} - readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} @@ -7857,9 +7383,6 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - sanitize-filename@1.6.4: - resolution: {integrity: sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==} - saxes@6.0.0: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} @@ -7909,11 +7432,6 @@ packages: engines: {node: '>=10'} hasBin: true - semver@7.7.4: - resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} - engines: {node: '>=10'} - hasBin: true - send@0.19.0: resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} engines: {node: '>= 0.8.0'} @@ -7953,10 +7471,6 @@ packages: setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - sharp@0.34.5: - resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -8047,18 +7561,6 @@ packages: space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} - spdx-correct@3.2.0: - resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} - - spdx-exceptions@2.5.0: - resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} - - spdx-expression-parse@3.0.1: - resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} - - spdx-license-ids@3.0.23: - resolution: {integrity: sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==} - sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} @@ -8098,20 +7600,10 @@ packages: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} - stream-buffers@2.2.0: - resolution: {integrity: sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==} - engines: {node: '>= 0.10.0'} - - stream-combiner@0.2.2: - resolution: {integrity: sha512-6yHMqgLYDzQDcAkL+tjJDC5nSNuNIx0vZtRZeiPh7Saef7VHX9H5Ijn9l2VIol2zaNYlYEX6KyuT/237A58qEQ==} - streamroller@3.1.5: resolution: {integrity: sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==} engines: {node: '>=8.0'} - streamx@2.25.0: - resolution: {integrity: sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==} - string-hash@1.1.3: resolution: {integrity: sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==} @@ -8153,12 +7645,6 @@ packages: resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} engines: {node: '>= 0.4'} - string_decoder@0.10.31: - resolution: {integrity: sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==} - - string_decoder@1.1.1: - resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} - string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} @@ -8226,10 +7712,6 @@ packages: engines: {node: '>=16 || 14 >=14.17'} hasBin: true - supports-color@10.2.2: - resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} - engines: {node: '>=18'} - supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -8262,10 +7744,6 @@ packages: resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} engines: {node: ^14.18.0 || >=16.0.0} - tagged-tag@1.0.0: - resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} - engines: {node: '>=20'} - tapable@2.3.0: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} @@ -8274,16 +7752,6 @@ packages: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} - tar-stream@3.1.8: - resolution: {integrity: sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==} - - teen_process@4.1.0: - resolution: {integrity: sha512-AN8y3MYPExB3r2mkkX9r0wEF4xPfhKOj6YvcfeIqQai+GVhTIhjjdkPvwI5CFT4z8UQ5aZWldzbJ+jNejYAdGw==} - engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: '>=10'} - - teex@1.0.1: - resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==} - terser-webpack-plugin@5.3.14: resolution: {integrity: sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==} engines: {node: '>= 10.13.0'} @@ -8313,9 +7781,6 @@ packages: resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} engines: {node: '>=18'} - text-decoder@1.2.7: - resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==} - thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} engines: {node: '>=0.8'} @@ -8326,9 +7791,6 @@ packages: throat@5.0.0: resolution: {integrity: sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==} - through@2.3.8: - resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} - tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -8398,9 +7860,6 @@ packages: trough@2.2.0: resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} - truncate-utf8-bytes@1.0.2: - resolution: {integrity: sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==} - ts-api-utils@2.1.0: resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} engines: {node: '>=18.12'} @@ -8459,14 +7918,6 @@ packages: resolution: {integrity: sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==} engines: {node: '>=8'} - type-fest@4.41.0: - resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} - engines: {node: '>=16'} - - type-fest@5.5.0: - resolution: {integrity: sha512-PlBfpQwiUvGViBNX84Yxwjsdhd1TUlXr6zjX7eoirtCPIr08NAmxwa+fcYBTeRQxHo9YC9wwF3m9i700sHma8g==} - engines: {node: '>=20'} - type-is@1.6.18: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} @@ -8536,10 +7987,6 @@ packages: resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} engines: {node: '>=4'} - unicorn-magic@0.4.0: - resolution: {integrity: sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==} - engines: {node: '>=20'} - unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} @@ -8580,10 +8027,6 @@ packages: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} - unorm@1.6.0: - resolution: {integrity: sha512-b2/KCUlYZUeA7JFUuRJZPUtr4gZvBh7tavtv4fvk4+KV9pfGiR6CQAQAWl49ZpR3ts2dk4FYkP7EIgDJoiOLDA==} - engines: {node: '>= 0.4.0'} - unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} @@ -8621,9 +8064,6 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - utf8-byte-length@1.0.5: - resolution: {integrity: sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==} - util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -8631,17 +8071,10 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} - uuid@13.0.0: - resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==} - hasBin: true - v8-to-istanbul@9.3.0: resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} engines: {node: '>=10.12.0'} - validate-npm-package-license@3.0.4: - resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} - vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -8824,11 +8257,6 @@ packages: engines: {node: '>= 8'} hasBin: true - which@6.0.1: - resolution: {integrity: sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==} - engines: {node: ^20.17.0 || >=22.9.0} - hasBin: true - why-is-node-running@2.3.0: resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} engines: {node: '>=8'} @@ -8912,10 +8340,6 @@ packages: resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} engines: {node: '>=12'} - xmlbuilder@15.1.1: - resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} - engines: {node: '>=8.0'} - xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} @@ -8954,10 +8378,6 @@ packages: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} - yauzl@3.3.0: - resolution: {integrity: sha512-PtGEvEP30p7sbIBJKUBjUnqgTVOyMURc4dLo9iNyAJnNIEz9pm88cCXF21w94Kg3k6RXkeZh5DHOGS0qEONvNQ==} - engines: {node: '>=12'} - yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -8966,14 +8386,6 @@ packages: resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==} engines: {node: '>=12.20'} - yoctocolors@2.1.2: - resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} - engines: {node: '>=18'} - - zip-stream@6.0.1: - resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} - engines: {node: '>= 14'} - zod@3.25.67: resolution: {integrity: sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==} @@ -9005,72 +8417,6 @@ snapshots: '@jridgewell/gen-mapping': 0.3.8 '@jridgewell/trace-mapping': 0.3.25 - '@appium/logger@2.0.6': - dependencies: - console-control-strings: 1.1.0 - lodash: 4.18.1 - lru-cache: 11.3.3 - set-blocking: 2.0.0 - - '@appium/schema@1.1.0': - dependencies: - json-schema: 0.4.0 - - '@appium/support@7.1.0': - dependencies: - '@appium/logger': 2.0.6 - '@appium/tsconfig': 1.1.2 - '@appium/types': 1.3.0 - '@colors/colors': 1.6.0 - archiver: 7.0.1 - asyncbox: 6.1.0 - axios: 1.15.0 - base64-stream: 1.0.0 - bluebird: 3.7.2 - bplist-creator: 0.1.1 - bplist-parser: 0.3.2 - form-data: 4.0.5 - get-stream: 9.0.1 - glob: 13.0.6 - jsftp: 2.1.3(supports-color@10.2.2) - klaw: 4.1.0 - lockfile: 1.0.4 - lodash: 4.18.1 - log-symbols: 7.0.1 - ncp: 2.0.0 - package-directory: 8.2.0 - plist: 3.1.0 - pluralize: 8.0.0 - read-pkg: 10.1.0 - resolve-from: 5.0.0 - sanitize-filename: 1.6.4 - semver: 7.7.4 - shell-quote: 1.8.3 - supports-color: 10.2.2 - teen_process: 4.1.0 - type-fest: 5.5.0 - uuid: 13.0.0 - which: 6.0.1 - yauzl: 3.3.0 - optionalDependencies: - sharp: 0.34.5 - transitivePeerDependencies: - - bare-abort-controller - - bare-buffer - - debug - - react-native-b4a - - '@appium/tsconfig@1.1.2': - dependencies: - '@tsconfig/node20': 20.1.9 - - '@appium/types@1.3.0': - dependencies: - '@appium/logger': 2.0.6 - '@appium/schema': 1.1.0 - '@appium/tsconfig': 1.1.2 - type-fest: 5.5.0 - '@babel/code-frame@7.27.1': dependencies: '@babel/helper-validator-identifier': 7.27.1 @@ -9968,18 +9314,11 @@ snapshots: picocolors: 1.1.1 sisteransi: 1.0.5 - '@colors/colors@1.6.0': {} - '@emnapi/core@1.5.0': dependencies: '@emnapi/wasi-threads': 1.1.0 tslib: 2.8.1 - '@emnapi/runtime@1.10.0': - dependencies: - tslib: 2.8.1 - optional: true - '@emnapi/runtime@1.5.0': dependencies: tslib: 2.8.1 @@ -10171,144 +9510,47 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/eslintrc@3.3.1': - dependencies: - ajv: 6.12.6 - debug: 4.4.1 - espree: 10.4.0 - globals: 14.0.0 - ignore: 5.3.2 - import-fresh: 3.3.1 - js-yaml: 4.1.0 - minimatch: 3.1.2 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - - '@eslint/js@9.29.0': {} - - '@eslint/object-schema@2.1.6': {} - - '@eslint/plugin-kit@0.3.2': - dependencies: - '@eslint/core': 0.15.0 - levn: 0.4.1 - - '@hapi/hoek@9.3.0': {} - - '@hapi/topo@5.1.0': - dependencies: - '@hapi/hoek': 9.3.0 - - '@humanfs/core@0.19.1': {} - - '@humanfs/node@0.16.6': - dependencies: - '@humanfs/core': 0.19.1 - '@humanwhocodes/retry': 0.3.1 - - '@humanwhocodes/module-importer@1.0.1': {} - - '@humanwhocodes/retry@0.3.1': {} - - '@humanwhocodes/retry@0.4.3': {} - - '@img/colour@1.1.0': - optional: true - - '@img/sharp-darwin-arm64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.2.4 - optional: true - - '@img/sharp-darwin-x64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.2.4 - optional: true - - '@img/sharp-libvips-darwin-arm64@1.2.4': - optional: true - - '@img/sharp-libvips-darwin-x64@1.2.4': - optional: true - - '@img/sharp-libvips-linux-arm64@1.2.4': - optional: true - - '@img/sharp-libvips-linux-arm@1.2.4': - optional: true - - '@img/sharp-libvips-linux-ppc64@1.2.4': - optional: true - - '@img/sharp-libvips-linux-riscv64@1.2.4': - optional: true - - '@img/sharp-libvips-linux-s390x@1.2.4': - optional: true - - '@img/sharp-libvips-linux-x64@1.2.4': - optional: true - - '@img/sharp-libvips-linuxmusl-arm64@1.2.4': - optional: true - - '@img/sharp-libvips-linuxmusl-x64@1.2.4': - optional: true - - '@img/sharp-linux-arm64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.2.4 - optional: true - - '@img/sharp-linux-arm@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.2.4 - optional: true - - '@img/sharp-linux-ppc64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-ppc64': 1.2.4 - optional: true + '@eslint/eslintrc@3.3.1': + dependencies: + ajv: 6.12.6 + debug: 4.4.1 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color - '@img/sharp-linux-riscv64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-riscv64': 1.2.4 - optional: true + '@eslint/js@9.29.0': {} - '@img/sharp-linux-s390x@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.2.4 - optional: true + '@eslint/object-schema@2.1.6': {} - '@img/sharp-linux-x64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.2.4 - optional: true + '@eslint/plugin-kit@0.3.2': + dependencies: + '@eslint/core': 0.15.0 + levn: 0.4.1 - '@img/sharp-linuxmusl-arm64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 - optional: true + '@hapi/hoek@9.3.0': {} - '@img/sharp-linuxmusl-x64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.2.4 - optional: true + '@hapi/topo@5.1.0': + dependencies: + '@hapi/hoek': 9.3.0 + + '@humanfs/core@0.19.1': {} - '@img/sharp-wasm32@0.34.5': + '@humanfs/node@0.16.6': dependencies: - '@emnapi/runtime': 1.10.0 - optional: true + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.3.1 - '@img/sharp-win32-arm64@0.34.5': - optional: true + '@humanwhocodes/module-importer@1.0.1': {} - '@img/sharp-win32-ia32@0.34.5': - optional: true + '@humanwhocodes/retry@0.3.1': {} - '@img/sharp-win32-x64@0.34.5': - optional: true + '@humanwhocodes/retry@0.4.3': {} '@isaacs/cliui@8.0.2': dependencies: @@ -12026,8 +11268,6 @@ snapshots: '@rtsao/scc@1.1.0': {} - '@sec-ant/readable-stream@0.4.1': {} - '@selderee/plugin-htmlparser2@0.11.0': dependencies: domhandler: 5.0.3 @@ -12279,8 +11519,6 @@ snapshots: '@trysound/sax@0.2.0': {} - '@tsconfig/node20@20.1.9': {} - '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 @@ -12395,8 +11633,6 @@ snapshots: dependencies: undici-types: 6.21.0 - '@types/normalize-package-data@2.4.4': {} - '@types/parse-json@4.0.2': {} '@types/pixelmatch@5.2.6': @@ -12892,8 +12128,6 @@ snapshots: '@webassemblyjs/ast': 1.14.1 '@xtuc/long': 4.2.2 - '@xmldom/xmldom@0.8.13': {} - '@xtuc/ieee754@1.2.0': {} '@xtuc/long@4.2.2': {} @@ -13010,46 +12244,6 @@ snapshots: appdirsjs@1.2.7: {} - appium-ios-device@3.1.11: - dependencies: - '@appium/support': 7.1.0 - asyncbox: 6.1.0 - axios: 1.13.2 - bluebird: 3.7.2 - bplist-creator: 0.1.1 - bplist-parser: 0.3.2 - lodash: 4.17.21 - semver: 7.7.2 - transitivePeerDependencies: - - bare-abort-controller - - bare-buffer - - debug - - react-native-b4a - - archiver-utils@5.0.2: - dependencies: - glob: 10.5.0 - graceful-fs: 4.2.11 - is-stream: 2.0.1 - lazystream: 1.0.1 - lodash: 4.18.1 - normalize-path: 3.0.0 - readable-stream: 4.7.0 - - archiver@7.0.1: - dependencies: - archiver-utils: 5.0.2 - async: 3.2.6 - buffer-crc32: 1.0.0 - readable-stream: 4.7.0 - readdir-glob: 1.1.3 - tar-stream: 3.1.8 - zip-stream: 6.0.1 - transitivePeerDependencies: - - bare-abort-controller - - bare-buffer - - react-native-b4a - argparse@1.0.10: dependencies: sprintf-js: 1.0.3 @@ -13151,10 +12345,6 @@ snapshots: async@3.2.6: {} - asyncbox@6.1.0: - dependencies: - p-limit: 7.3.0 - asynckit@0.4.0: {} at-least-node@1.0.0: {} @@ -13183,18 +12373,8 @@ snapshots: transitivePeerDependencies: - debug - axios@1.15.0: - dependencies: - follow-redirects: 1.16.0 - form-data: 4.0.5 - proxy-from-env: 2.1.0 - transitivePeerDependencies: - - debug - axobject-query@4.1.0: {} - b4a@1.8.0: {} - babel-jest@29.7.0(@babel/core@7.27.4): dependencies: '@babel/core': 7.27.4 @@ -13351,52 +12531,14 @@ snapshots: balanced-match@1.0.2: {} - balanced-match@4.0.4: {} - - bare-events@2.8.2: {} - - bare-fs@4.7.1: - dependencies: - bare-events: 2.8.2 - bare-path: 3.0.0 - bare-stream: 2.13.0(bare-events@2.8.2) - bare-url: 2.4.2 - fast-fifo: 1.3.2 - transitivePeerDependencies: - - bare-abort-controller - - react-native-b4a - - bare-os@3.9.0: {} - - bare-path@3.0.0: - dependencies: - bare-os: 3.9.0 - - bare-stream@2.13.0(bare-events@2.8.2): - dependencies: - streamx: 2.25.0 - teex: 1.0.1 - optionalDependencies: - bare-events: 2.8.2 - transitivePeerDependencies: - - react-native-b4a - - bare-url@2.4.2: - dependencies: - bare-path: 3.0.0 - base64-js@1.5.1: {} - base64-stream@1.0.0: {} - baseline-browser-mapping@2.8.29: {} basic-auth@2.0.1: dependencies: safe-buffer: 5.1.2 - big-integer@1.6.52: {} - big.js@5.2.2: {} binary-extensions@2.3.0: {} @@ -13409,8 +12551,6 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 - bluebird@3.7.2: {} - body-parser@1.20.3: dependencies: bytes: 3.1.2 @@ -13432,14 +12572,6 @@ snapshots: boolbase@1.0.0: {} - bplist-creator@0.1.1: - dependencies: - stream-buffers: 2.2.0 - - bplist-parser@0.3.2: - dependencies: - big-integer: 1.6.52 - brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 @@ -13449,10 +12581,6 @@ snapshots: dependencies: balanced-match: 1.0.2 - brace-expansion@5.0.5: - dependencies: - balanced-match: 4.0.4 - braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -13478,10 +12606,6 @@ snapshots: btoa@1.2.1: {} - buffer-crc32@0.2.13: {} - - buffer-crc32@1.0.0: {} - buffer-from@1.1.2: {} buffer@5.7.1: @@ -13489,11 +12613,6 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 - buffer@6.0.3: - dependencies: - base64-js: 1.5.1 - ieee754: 1.2.1 - bundle-require@5.1.0(esbuild@0.27.2): dependencies: esbuild: 0.27.2 @@ -13692,14 +12811,6 @@ snapshots: commondir@1.0.1: {} - compress-commons@6.0.2: - dependencies: - crc-32: 1.2.2 - crc32-stream: 6.0.0 - is-stream: 2.0.1 - normalize-path: 3.0.0 - readable-stream: 4.7.0 - compressible@2.0.18: dependencies: mime-db: 1.54.0 @@ -13739,8 +12850,6 @@ snapshots: consola@3.4.2: {} - console-control-strings@1.1.0: {} - content-disposition@0.5.4: dependencies: safe-buffer: 5.2.1 @@ -13770,8 +12879,6 @@ snapshots: core-js@3.47.0: {} - core-util-is@1.0.3: {} - corser@2.0.1: {} cosmiconfig@7.1.0: @@ -13800,13 +12907,6 @@ snapshots: optionalDependencies: typescript: 5.9.3 - crc-32@1.2.2: {} - - crc32-stream@6.0.0: - dependencies: - crc-32: 1.2.2 - readable-stream: 4.7.0 - cron-parser@4.9.0: dependencies: luxon: 3.6.1 @@ -13958,11 +13058,9 @@ snapshots: dependencies: ms: 2.0.0 - debug@3.2.7(supports-color@10.2.2): + debug@3.2.7: dependencies: ms: 2.1.3 - optionalDependencies: - supports-color: 10.2.2 debug@4.4.1: dependencies: @@ -14018,9 +13116,6 @@ snapshots: destroy@1.2.0: {} - detect-libc@2.1.2: - optional: true - detect-newline@3.1.0: {} detect-port@1.6.1: @@ -14097,8 +13192,6 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 - duplexer@0.1.2: {} - eastasianwidth@0.2.0: {} ee-first@1.1.1: {} @@ -14359,7 +13452,7 @@ snapshots: eslint-import-resolver-node@0.3.9: dependencies: - debug: 3.2.7(supports-color@10.2.2) + debug: 3.2.7 is-core-module: 2.16.1 resolve: 1.22.10 transitivePeerDependencies: @@ -14367,7 +13460,7 @@ snapshots: eslint-module-utils@2.12.0(@typescript-eslint/parser@8.47.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.29.0(jiti@2.4.2)): dependencies: - debug: 3.2.7(supports-color@10.2.2) + debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.47.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.9.3) eslint: 9.29.0(jiti@2.4.2) @@ -14395,7 +13488,7 @@ snapshots: array.prototype.findlastindex: 1.2.6 array.prototype.flat: 1.3.3 array.prototype.flatmap: 1.3.3 - debug: 3.2.7(supports-color@10.2.2) + debug: 3.2.7 doctrine: 2.1.0 eslint: 9.29.0(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 @@ -14680,12 +13773,6 @@ snapshots: eventemitter3@4.0.7: {} - events-universal@1.0.1: - dependencies: - bare-events: 2.8.2 - transitivePeerDependencies: - - bare-abort-controller - events@3.3.0: {} execa@5.1.1: @@ -14763,8 +13850,6 @@ snapshots: fast-deep-equal@3.1.3: {} - fast-fifo@1.3.2: {} - fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -14877,8 +13962,6 @@ snapshots: dependencies: find-file-up: 2.0.1 - find-up-simple@1.0.1: {} - find-up@3.0.0: dependencies: locate-path: 3.0.0 @@ -14916,8 +13999,6 @@ snapshots: optionalDependencies: debug: 4.4.1 - follow-redirects@1.16.0: {} - for-each@0.3.5: dependencies: is-callable: 1.2.7 @@ -14982,10 +14063,6 @@ snapshots: fsevents@2.3.3: optional: true - ftp-response-parser@1.0.1: - dependencies: - readable-stream: 1.1.14 - function-bind@1.1.2: {} function.prototype.name@1.1.8: @@ -15029,11 +14106,6 @@ snapshots: get-stream@6.0.1: {} - get-stream@9.0.1: - dependencies: - '@sec-ant/readable-stream': 0.4.1 - is-stream: 4.0.1 - get-symbol-description@1.1.0: dependencies: call-bound: 1.0.4 @@ -15070,12 +14142,6 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 1.11.1 - glob@13.0.6: - dependencies: - minimatch: 10.2.5 - minipass: 7.1.3 - path-scurry: 2.0.2 - glob@7.2.3: dependencies: fs.realpath: 1.0.0 @@ -15310,10 +14376,6 @@ snapshots: hookable@6.1.0: {} - hosted-git-info@9.0.2: - dependencies: - lru-cache: 11.3.5 - html-encoding-sniffer@3.0.0: dependencies: whatwg-encoding: 2.0.0 @@ -15473,8 +14535,6 @@ snapshots: imurmurhash@0.1.4: {} - index-to-position@1.2.0: {} - inflight@1.0.6: dependencies: once: 1.4.0 @@ -15630,8 +14690,6 @@ snapshots: is-stream@2.0.1: {} - is-stream@4.0.1: {} - is-string@1.1.1: dependencies: call-bound: 1.0.4 @@ -15649,8 +14707,6 @@ snapshots: is-unicode-supported@0.1.0: {} - is-unicode-supported@2.1.0: {} - is-weakmap@2.0.2: {} is-weakref@1.1.1: @@ -15670,16 +14726,10 @@ snapshots: dependencies: is-docker: 2.2.1 - isarray@0.0.1: {} - - isarray@1.0.0: {} - isarray@2.0.5: {} isexe@2.0.0: {} - isexe@4.0.0: {} - isomorphic-ws@5.0.0(ws@8.18.0): dependencies: ws: 8.18.0 @@ -16231,17 +15281,6 @@ snapshots: jsesc@3.1.0: {} - jsftp@2.1.3(supports-color@10.2.2): - dependencies: - debug: 3.2.7(supports-color@10.2.2) - ftp-response-parser: 1.0.1 - once: 1.4.0 - parse-listing: 1.1.3 - stream-combiner: 0.2.2 - unorm: 1.6.0 - transitivePeerDependencies: - - supports-color - json-buffer@3.0.1: {} json-parse-even-better-errors@2.3.1: {} @@ -16250,8 +15289,6 @@ snapshots: json-schema-traverse@1.0.0: {} - json-schema@0.4.0: {} - json-stable-stringify-without-jsonify@1.0.1: {} json5@1.0.2: @@ -16296,8 +15333,6 @@ snapshots: kind-of@6.0.3: {} - klaw@4.1.0: {} - kleur@3.0.3: {} koa-compose@4.1.0: {} @@ -16334,10 +15369,6 @@ snapshots: picocolors: 1.1.1 shell-quote: 1.8.3 - lazystream@1.0.1: - dependencies: - readable-stream: 2.3.8 - leac@0.6.0: {} leven@3.1.0: {} @@ -16387,10 +15418,6 @@ snapshots: dependencies: p-locate: 5.0.0 - lockfile@1.0.4: - dependencies: - signal-exit: 3.0.7 - lodash-es@4.17.23: {} lodash.camelcase@4.3.0: {} @@ -16409,18 +15436,11 @@ snapshots: lodash@4.17.21: {} - lodash@4.18.1: {} - log-symbols@4.1.0: dependencies: chalk: 4.1.2 is-unicode-supported: 0.1.0 - log-symbols@7.0.1: - dependencies: - is-unicode-supported: 2.1.0 - yoctocolors: 2.1.2 - log4js@6.9.1: dependencies: date-format: 4.0.14 @@ -16453,10 +15473,6 @@ snapshots: lru-cache@10.4.3: {} - lru-cache@11.3.3: {} - - lru-cache@11.3.5: {} - lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -17146,10 +16162,6 @@ snapshots: mini-svg-data-uri@1.4.4: {} - minimatch@10.2.5: - dependencies: - brace-expansion: 5.0.5 - minimatch@3.1.2: dependencies: brace-expansion: 1.1.12 @@ -17176,8 +16188,6 @@ snapshots: minipass@7.1.2: {} - minipass@7.1.3: {} - mkdirp@1.0.4: {} mlly@1.8.0: @@ -17207,8 +16217,6 @@ snapshots: natural-compare@1.4.0: {} - ncp@2.0.0: {} - negotiator@0.6.3: {} negotiator@0.6.4: {} @@ -17246,12 +16254,6 @@ snapshots: node-stream-zip@1.15.0: {} - normalize-package-data@8.0.0: - dependencies: - hosted-git-info: 9.0.2 - semver: 7.7.4 - validate-npm-package-license: 3.0.4 - normalize-path@3.0.0: {} normalize-range@0.1.2: {} @@ -17466,10 +16468,6 @@ snapshots: dependencies: yocto-queue: 1.2.1 - p-limit@7.3.0: - dependencies: - yocto-queue: 1.2.1 - p-locate@3.0.0: dependencies: p-limit: 2.3.0 @@ -17493,10 +16491,6 @@ snapshots: p-try@2.2.0: {} - package-directory@8.2.0: - dependencies: - find-up-simple: 1.0.1 - package-json-from-dist@1.0.1: {} parent-module@1.0.1: @@ -17520,14 +16514,6 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 - parse-json@8.3.0: - dependencies: - '@babel/code-frame': 7.27.1 - index-to-position: 1.2.0 - type-fest: 4.41.0 - - parse-listing@1.1.3: {} - parse-passwd@1.0.0: {} parse5@7.3.0: @@ -17556,11 +16542,6 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 - path-scurry@2.0.2: - dependencies: - lru-cache: 11.3.5 - minipass: 7.1.3 - path-to-regexp@0.1.12: {} path-type@4.0.0: {} @@ -17571,8 +16552,6 @@ snapshots: peberminta@0.9.0: {} - pend@1.2.0: {} - picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -17611,14 +16590,6 @@ snapshots: optionalDependencies: fsevents: 2.3.2 - plist@3.1.0: - dependencies: - '@xmldom/xmldom': 0.8.13 - base64-js: 1.5.1 - xmlbuilder: 15.1.1 - - pluralize@8.0.0: {} - pngjs@7.0.0: {} portfinder@1.0.37: @@ -17860,10 +16831,6 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.3.1 - process-nextick-args@2.0.1: {} - - process@0.11.10: {} - promise.series@0.2.0: {} promise@7.3.1: @@ -17896,8 +16863,6 @@ snapshots: proxy-from-env@1.1.0: {} - proxy-from-env@2.1.0: {} - psl@1.15.0: dependencies: punycode: 2.3.1 @@ -18069,49 +17034,12 @@ snapshots: react@19.2.3: {} - read-pkg@10.1.0: - dependencies: - '@types/normalize-package-data': 2.4.4 - normalize-package-data: 8.0.0 - parse-json: 8.3.0 - type-fest: 5.5.0 - unicorn-magic: 0.4.0 - - readable-stream@1.1.14: - dependencies: - core-util-is: 1.0.3 - inherits: 2.0.4 - isarray: 0.0.1 - string_decoder: 0.10.31 - - readable-stream@2.3.8: - dependencies: - core-util-is: 1.0.3 - inherits: 2.0.4 - isarray: 1.0.0 - process-nextick-args: 2.0.1 - safe-buffer: 5.1.2 - string_decoder: 1.1.1 - util-deprecate: 1.0.2 - readable-stream@3.6.2: dependencies: inherits: 2.0.4 string_decoder: 1.3.0 util-deprecate: 1.0.2 - readable-stream@4.7.0: - dependencies: - abort-controller: 3.0.0 - buffer: 6.0.3 - events: 3.3.0 - process: 0.11.10 - string_decoder: 1.3.0 - - readdir-glob@1.1.3: - dependencies: - minimatch: 5.1.6 - readdirp@3.6.0: dependencies: picomatch: 2.3.1 @@ -18426,10 +17354,6 @@ snapshots: safer-buffer@2.1.2: {} - sanitize-filename@1.6.4: - dependencies: - truncate-utf8-bytes: 1.0.2 - saxes@6.0.0: dependencies: xmlchars: 2.2.0 @@ -18479,8 +17403,6 @@ snapshots: semver@7.7.2: {} - semver@7.7.4: {} - send@0.19.0: dependencies: debug: 2.6.9 @@ -18544,38 +17466,6 @@ snapshots: setprototypeof@1.2.0: {} - sharp@0.34.5: - dependencies: - '@img/colour': 1.1.0 - detect-libc: 2.1.2 - semver: 7.7.4 - optionalDependencies: - '@img/sharp-darwin-arm64': 0.34.5 - '@img/sharp-darwin-x64': 0.34.5 - '@img/sharp-libvips-darwin-arm64': 1.2.4 - '@img/sharp-libvips-darwin-x64': 1.2.4 - '@img/sharp-libvips-linux-arm': 1.2.4 - '@img/sharp-libvips-linux-arm64': 1.2.4 - '@img/sharp-libvips-linux-ppc64': 1.2.4 - '@img/sharp-libvips-linux-riscv64': 1.2.4 - '@img/sharp-libvips-linux-s390x': 1.2.4 - '@img/sharp-libvips-linux-x64': 1.2.4 - '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 - '@img/sharp-libvips-linuxmusl-x64': 1.2.4 - '@img/sharp-linux-arm': 0.34.5 - '@img/sharp-linux-arm64': 0.34.5 - '@img/sharp-linux-ppc64': 0.34.5 - '@img/sharp-linux-riscv64': 0.34.5 - '@img/sharp-linux-s390x': 0.34.5 - '@img/sharp-linux-x64': 0.34.5 - '@img/sharp-linuxmusl-arm64': 0.34.5 - '@img/sharp-linuxmusl-x64': 0.34.5 - '@img/sharp-wasm32': 0.34.5 - '@img/sharp-win32-arm64': 0.34.5 - '@img/sharp-win32-ia32': 0.34.5 - '@img/sharp-win32-x64': 0.34.5 - optional: true - shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -18677,20 +17567,6 @@ snapshots: space-separated-tokens@2.0.2: {} - spdx-correct@3.2.0: - dependencies: - spdx-expression-parse: 3.0.1 - spdx-license-ids: 3.0.23 - - spdx-exceptions@2.5.0: {} - - spdx-expression-parse@3.0.1: - dependencies: - spdx-exceptions: 2.5.0 - spdx-license-ids: 3.0.23 - - spdx-license-ids@3.0.23: {} - sprintf-js@1.0.3: {} ssim.js@3.5.0: {} @@ -18720,13 +17596,6 @@ snapshots: es-errors: 1.3.0 internal-slot: 1.1.0 - stream-buffers@2.2.0: {} - - stream-combiner@0.2.2: - dependencies: - duplexer: 0.1.2 - through: 2.3.8 - streamroller@3.1.5: dependencies: date-format: 4.0.14 @@ -18735,15 +17604,6 @@ snapshots: transitivePeerDependencies: - supports-color - streamx@2.25.0: - dependencies: - events-universal: 1.0.1 - fast-fifo: 1.3.2 - text-decoder: 1.2.7 - transitivePeerDependencies: - - bare-abort-controller - - react-native-b4a - string-hash@1.1.3: {} string-length@4.0.2: @@ -18815,12 +17675,6 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 - string_decoder@0.10.31: {} - - string_decoder@1.1.1: - dependencies: - safe-buffer: 5.1.2 - string_decoder@1.3.0: dependencies: safe-buffer: 5.2.1 @@ -18886,8 +17740,6 @@ snapshots: pirates: 4.0.7 ts-interface-checker: 0.1.13 - supports-color@10.2.2: {} - supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -18926,8 +17778,6 @@ snapshots: dependencies: '@pkgr/core': 0.2.9 - tagged-tag@1.0.0: {} - tapable@2.3.0: {} tar-stream@2.2.0: @@ -18938,29 +17788,6 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 - tar-stream@3.1.8: - dependencies: - b4a: 1.8.0 - bare-fs: 4.7.1 - fast-fifo: 1.3.2 - streamx: 2.25.0 - transitivePeerDependencies: - - bare-abort-controller - - bare-buffer - - react-native-b4a - - teen_process@4.1.0: - dependencies: - lodash: 4.18.1 - shell-quote: 1.8.3 - - teex@1.0.1: - dependencies: - streamx: 2.25.0 - transitivePeerDependencies: - - bare-abort-controller - - react-native-b4a - terser-webpack-plugin@5.3.14(@swc/core@1.5.29(@swc/helpers@0.5.17))(webpack@5.102.1(@swc/core@1.5.29(@swc/helpers@0.5.17))): dependencies: '@jridgewell/trace-mapping': 0.3.25 @@ -18991,12 +17818,6 @@ snapshots: glob: 10.5.0 minimatch: 9.0.5 - text-decoder@1.2.7: - dependencies: - b4a: 1.8.0 - transitivePeerDependencies: - - react-native-b4a - thenify-all@1.6.0: dependencies: thenify: 3.3.1 @@ -19007,8 +17828,6 @@ snapshots: throat@5.0.0: {} - through@2.3.8: {} - tinybench@2.9.0: {} tinyexec@0.3.2: {} @@ -19059,10 +17878,6 @@ snapshots: trough@2.2.0: {} - truncate-utf8-bytes@1.0.2: - dependencies: - utf8-byte-length: 1.0.5 - ts-api-utils@2.1.0(typescript@5.9.3): dependencies: typescript: 5.9.3 @@ -19125,12 +17940,6 @@ snapshots: type-fest@0.7.1: {} - type-fest@4.41.0: {} - - type-fest@5.5.0: - dependencies: - tagged-tag: 1.0.0 - type-is@1.6.18: dependencies: media-typer: 0.3.0 @@ -19216,8 +18025,6 @@ snapshots: unicode-property-aliases-ecmascript@2.1.0: {} - unicorn-magic@0.4.0: {} - unified@11.0.5: dependencies: '@types/unist': 3.0.3 @@ -19269,8 +18076,6 @@ snapshots: universalify@2.0.1: {} - unorm@1.6.0: {} - unpipe@1.0.0: {} unrs-resolver@1.11.1: @@ -19326,25 +18131,16 @@ snapshots: dependencies: react: 19.2.3 - utf8-byte-length@1.0.5: {} - util-deprecate@1.0.2: {} utils-merge@1.0.1: {} - uuid@13.0.0: {} - v8-to-istanbul@9.3.0: dependencies: '@jridgewell/trace-mapping': 0.3.25 '@types/istanbul-lib-coverage': 2.0.6 convert-source-map: 2.0.0 - validate-npm-package-license@3.0.4: - dependencies: - spdx-correct: 3.2.0 - spdx-expression-parse: 3.0.1 - vary@1.1.2: {} vfile-location@5.0.3: @@ -19578,10 +18374,6 @@ snapshots: dependencies: isexe: 2.0.0 - which@6.0.1: - dependencies: - isexe: 4.0.0 - why-is-node-running@2.3.0: dependencies: siginfo: 2.0.0 @@ -19631,8 +18423,6 @@ snapshots: xml-name-validator@4.0.0: {} - xmlbuilder@15.1.1: {} - xmlchars@2.2.0: {} y18n@4.0.3: {} @@ -19676,23 +18466,10 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 - yauzl@3.3.0: - dependencies: - buffer-crc32: 0.2.13 - pend: 1.2.0 - yocto-queue@0.1.0: {} yocto-queue@1.2.1: {} - yoctocolors@2.1.2: {} - - zip-stream@6.0.1: - dependencies: - archiver-utils: 5.0.2 - compress-commons: 6.0.2 - readable-stream: 4.7.0 - zod@3.25.67: {} zustand@5.0.5(@types/react@19.1.13)(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3)): From 3380be60d11513c8deb568528502d7700e8a1f91 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Tue, 28 Apr 2026 15:10:32 +0200 Subject: [PATCH 14/33] fix: avoid eager iOS device crash diagnostics Stop collecting device crash artifacts when the app monitor starts so XCTest setup does not trigger devicectl diagnose before app launch. --- packages/jest/src/__tests__/harness.test.ts | 74 +++++++++++++++++++ packages/jest/src/crash-supervisor.ts | 5 ++ packages/jest/src/harness.ts | 10 +++ .../src/__tests__/app-monitor.test.ts | 24 ++++++ packages/platform-ios/src/app-monitor.ts | 21 +----- 5 files changed, 117 insertions(+), 17 deletions(-) diff --git a/packages/jest/src/__tests__/harness.test.ts b/packages/jest/src/__tests__/harness.test.ts index f828b09..8d84a0b 100644 --- a/packages/jest/src/__tests__/harness.test.ts +++ b/packages/jest/src/__tests__/harness.test.ts @@ -572,6 +572,80 @@ describe('getHarness', () => { await harness.dispose(); }); + it('does not forward pre-launch crash monitor events to plugins', async () => { + const { serverBridge } = createBridgeServer(); + const appMonitor = createAppMonitor(); + const platformInstance = createPlatformRunner({ + createAppMonitor: () => appMonitor.appMonitor, + }); + const metroInstance = createMetroInstance(); + const appEvents: string[] = []; + + mocks.getBridgeServer.mockResolvedValue(serverBridge); + mocks.getMetroInstance.mockResolvedValue(metroInstance); + + ( + globalThis as typeof globalThis & { + __HARNESS_PLATFORM_RUNNER__?: (...args: unknown[]) => Promise; + } + ).__HARNESS_PLATFORM_RUNNER__ = vi.fn(async () => platformInstance); + + const platform: HarnessPlatform = { + config: {}, + name: 'ios', + platformId: 'ios', + runner: `data:text/javascript,${encodeURIComponent( + 'export default (...args) => globalThis.__HARNESS_PLATFORM_RUNNER__(...args);', + )}`, + getResourceLockKey: () => 'ios:simulator:iPhone 17 Pro:26.2', + }; + + const harness = await getHarness( + createHarnessConfig({ + plugins: [ + definePlugin({ + name: 'capture-app-events', + hooks: { + app: { + possibleCrash: (ctx) => { + appEvents.push(`possibleCrash:${ctx.testFile ?? 'n/a'}`); + }, + exited: (ctx) => { + appEvents.push(`exited:${ctx.testFile ?? 'n/a'}`); + }, + }, + }, + }), + ], + }), + platform, + '/tmp/project', + ); + + appMonitor.emit({ + type: 'possible_crash', + source: 'logs', + isConfirmed: true, + crashDetails: { + source: 'logs', + summary: 'stale pre-launch crash signal', + }, + }); + appMonitor.emit({ + type: 'app_exited', + source: 'polling', + isConfirmed: true, + crashDetails: { + source: 'polling', + summary: 'stale pre-launch exit signal', + }, + }); + + await harness.dispose(); + + expect(appEvents).toEqual([]); + }); + it('routes restart(testFilePath) through the shared Metro startup helper', async () => { const { serverBridge, emitReady } = createBridgeServer(); const appMonitor = createAppMonitor(); diff --git a/packages/jest/src/crash-supervisor.ts b/packages/jest/src/crash-supervisor.ts index 6ba1fae..94b56f3 100644 --- a/packages/jest/src/crash-supervisor.ts +++ b/packages/jest/src/crash-supervisor.ts @@ -26,6 +26,7 @@ export type CrashSupervisor = { beginLaunch: (testFilePath: string) => void; markReady: () => void; beginTestRun: (testFilePath: string) => void; + isArmed: () => boolean; stop: () => Promise; start: () => Promise; waitForCrash: (testFilePath: string) => Promise; @@ -255,6 +256,9 @@ export const createCrashSupervisor = ({ state = 'running'; }; + const isArmed = () => + state === 'launching' || state === 'ready' || state === 'running'; + const stop = async () => { monitoring = false; await appMonitor.stop(); @@ -310,6 +314,7 @@ export const createCrashSupervisor = ({ beginLaunch, markReady, beginTestRun, + isArmed, stop, start, waitForCrash, diff --git a/packages/jest/src/harness.ts b/packages/jest/src/harness.ts index 91a9539..fa54fb9 100644 --- a/packages/jest/src/harness.ts +++ b/packages/jest/src/harness.ts @@ -600,6 +600,8 @@ const getHarnessInternal = async ( return; } + const shouldReportCrashEvent = crashSupervisor.isArmed(); + if (event.type === 'app_started') { scheduleHook('app:started', { runId, @@ -612,6 +614,10 @@ const getHarnessInternal = async ( } if (event.type === 'app_exited') { + if (!shouldReportCrashEvent) { + return; + } + scheduleHook('app:exited', { runId, testFile: activeTestFilePath, @@ -625,6 +631,10 @@ const getHarnessInternal = async ( } if (event.type === 'possible_crash') { + if (!shouldReportCrashEvent) { + return; + } + scheduleHook('app:possible-crash', { runId, testFile: activeTestFilePath, diff --git a/packages/platform-ios/src/__tests__/app-monitor.test.ts b/packages/platform-ios/src/__tests__/app-monitor.test.ts index a7336c7..31956d4 100644 --- a/packages/platform-ios/src/__tests__/app-monitor.test.ts +++ b/packages/platform-ios/src/__tests__/app-monitor.test.ts @@ -290,6 +290,30 @@ describe('createIosDeviceAppMonitor', () => { expect(events.some((event) => event.type === 'app_exited')).toBe(true); }); + it('does not collect device crash artifacts during monitor startup', async () => { + vi.spyOn(devicectl, 'getAppInfo').mockResolvedValue({ + bundleIdentifier: 'com.harnessplayground', + name: 'HarnessPlayground', + version: '1.0', + url: '/private/var/HarnessPlayground.app', + }); + vi.spyOn(devicectl, 'getProcesses').mockResolvedValue([]); + const collectCrashArtifactsSpy = vi.spyOn( + diagnostics, + 'collectCrashArtifacts', + ); + + const monitor = createIosDeviceAppMonitor({ + deviceId: 'device-udid', + bundleId: 'com.harnessplayground', + }); + + await monitor.start(); + await monitor.stop(); + + expect(collectCrashArtifactsSpy).not.toHaveBeenCalled(); + }); + it('enriches device crashes with Apple-native pulled crash reports', async () => { vi.spyOn(devicectl, 'getAppInfo').mockResolvedValue({ bundleIdentifier: 'com.harnessplayground', diff --git a/packages/platform-ios/src/app-monitor.ts b/packages/platform-ios/src/app-monitor.ts index 6c29259..6bab0f7 100644 --- a/packages/platform-ios/src/app-monitor.ts +++ b/packages/platform-ios/src/app-monitor.ts @@ -16,7 +16,6 @@ import { import * as devicectl from './xcrun/devicectl.js'; import * as simctl from './xcrun/simctl.js'; import { - collectCrashArtifacts, waitForCrashArtifact, } from './crash-diagnostics.js'; @@ -184,15 +183,15 @@ const createAppMonitorBase = () => { : []; const matchingByProcess = options.processName ? recentCrashArtifacts.filter( - (artifact) => artifact.processName === options.processName - ) + (artifact) => artifact.processName === options.processName + ) : []; const candidates = matchingByPid.length > 0 ? matchingByPid : matchingByProcess.length > 0 - ? matchingByProcess - : recentCrashArtifacts; + ? matchingByProcess + : recentCrashArtifacts; const preferredCandidates = candidates.filter( (artifact) => artifact.artifactType === 'ios-crash-report' ); @@ -581,18 +580,6 @@ export const createIosDeviceAppMonitor = ({ } })(); - const initialArtifacts = await collectCrashArtifacts({ - targetId: deviceId, - targetType: 'device', - bundleId, - processNames, - crashArtifactWriter, - minOccurredAt: monitorStartedAt, - }); - - for (const artifact of initialArtifacts) { - base.recordCrashArtifact(artifact); - } }; const stopLogMonitor = async () => { From 65c2dfba67ca8342ac47760bef53faa936e8354a Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Tue, 28 Apr 2026 15:15:17 +0200 Subject: [PATCH 15/33] feat: gate permission automation behind config Add a shared permissions flag that defaults to false and only starts the iOS XCTest permission helper when explicitly enabled. --- apps/playground/rn-harness.config.mjs | 3 +- packages/config/src/types.ts | 7 +++ .../__tests__/instance-xctest-agent.test.ts | 40 ++++++++++-- .../src/__tests__/instance.test.ts | 62 ++++++++++++++++++- packages/platform-ios/src/instance.ts | 30 +++++---- .../docs/getting-started/configuration.mdx | 15 +++++ 6 files changed, 138 insertions(+), 19 deletions(-) diff --git a/apps/playground/rn-harness.config.mjs b/apps/playground/rn-harness.config.mjs index a21fe1e..559f19f 100644 --- a/apps/playground/rn-harness.config.mjs +++ b/apps/playground/rn-harness.config.mjs @@ -23,7 +23,6 @@ export default { entryPoint: './index.js', appRegistryComponentName: 'HarnessPlayground', plugins: [harnessLoggingPlugin()], -detectNativeCrashes: false, runners: [ androidPlatform({ @@ -121,6 +120,8 @@ detectNativeCrashes: false, platformReadyTimeout: 300000, bridgeTimeout: 120000, + permissions: true, + detectNativeCrashes: true, resetEnvironmentBetweenTestFiles: true, unstable__enableMetroCache: true, unstable__skipAlreadyIncludedModules: false, diff --git a/packages/config/src/types.ts b/packages/config/src/types.ts index 0c83098..056c9f6 100644 --- a/packages/config/src/types.ts +++ b/packages/config/src/types.ts @@ -69,6 +69,13 @@ export const ConfigSchema = z resetEnvironmentBetweenTestFiles: z.boolean().optional().default(true), unstable__skipAlreadyIncludedModules: z.boolean().optional().default(false), unstable__enableMetroCache: z.boolean().optional().default(false), + permissions: z + .boolean() + .optional() + .default(false) + .describe( + 'Enable platform-specific permission prompt automation. When false, Harness does not start permission-handling helpers such as the iOS XCTest agent.' + ), detectNativeCrashes: z.boolean().optional().default(true), crashDetectionInterval: z diff --git a/packages/platform-ios/src/__tests__/instance-xctest-agent.test.ts b/packages/platform-ios/src/__tests__/instance-xctest-agent.test.ts index 098c488..eac3fc8 100644 --- a/packages/platform-ios/src/__tests__/instance-xctest-agent.test.ts +++ b/packages/platform-ios/src/__tests__/instance-xctest-agent.test.ts @@ -25,6 +25,10 @@ import { const harnessConfig = { metroPort: DEFAULT_METRO_PORT, } as HarnessConfig; +const harnessConfigWithPermissionsEnabled = { + metroPort: DEFAULT_METRO_PORT, + permissions: true, +} as HarnessConfig; describe('iOS XCTest agent runner integration', () => { beforeEach(() => { @@ -37,7 +41,7 @@ describe('iOS XCTest agent runner integration', () => { }); }); - it('starts the simulator XCTest agent during platform initialization', async () => { + it('starts the simulator XCTest agent during platform initialization when permissions are enabled', async () => { vi.spyOn(simctl, 'getSimulatorId').mockResolvedValue('sim-udid'); vi.spyOn(simctl, 'isAppInstalled').mockResolvedValue(true); vi.spyOn(simctl, 'getSimulatorStatus').mockResolvedValue('Booted'); @@ -60,7 +64,7 @@ describe('iOS XCTest agent runner integration', () => { }, bundleId: 'com.harnessplayground', }, - harnessConfig, + harnessConfigWithPermissionsEnabled, { signal: new AbortController().signal, }, @@ -86,7 +90,7 @@ describe('iOS XCTest agent runner integration', () => { expect(mocks.dispose).toHaveBeenCalledTimes(1); }); - it('starts the physical-device XCTest agent during platform initialization', async () => { + it('starts the physical-device XCTest agent during platform initialization when permissions are enabled', async () => { vi.spyOn(devicectl, 'getDevice').mockResolvedValue({ identifier: 'device-udid', deviceProperties: { @@ -112,7 +116,7 @@ describe('iOS XCTest agent runner integration', () => { }, bundleId: 'com.harnessplayground', }, - harnessConfig, + harnessConfigWithPermissionsEnabled, ); await instance.restartApp(); @@ -134,4 +138,32 @@ describe('iOS XCTest agent runner integration', () => { expect(mocks.ensureStarted).toHaveBeenCalledTimes(1); expect(mocks.dispose).toHaveBeenCalledTimes(1); }); + + it('does not start the simulator XCTest agent when permissions are disabled', async () => { + vi.spyOn(simctl, 'getSimulatorId').mockResolvedValue('sim-udid'); + vi.spyOn(simctl, 'isAppInstalled').mockResolvedValue(true); + vi.spyOn(simctl, 'getSimulatorStatus').mockResolvedValue('Booted'); + vi.spyOn(simctl, 'applyHarnessJsLocationOverride').mockResolvedValue( + undefined, + ); + + await getAppleSimulatorPlatformInstance( + { + name: 'ios', + device: { + type: 'simulator', + name: 'iPhone 16 Pro', + systemVersion: '18.0', + }, + bundleId: 'com.harnessplayground', + }, + harnessConfig, + { + signal: new AbortController().signal, + }, + ); + + expect(mocks.createXCTestAgentController).not.toHaveBeenCalled(); + expect(mocks.ensureStarted).not.toHaveBeenCalled(); + }); }); diff --git a/packages/platform-ios/src/__tests__/instance.test.ts b/packages/platform-ios/src/__tests__/instance.test.ts index bc77895..7dd0864 100644 --- a/packages/platform-ios/src/__tests__/instance.test.ts +++ b/packages/platform-ios/src/__tests__/instance.test.ts @@ -28,6 +28,10 @@ vi.mock('../xctest-agent.js', () => ({ const harnessConfig = { metroPort: DEFAULT_METRO_PORT, } as HarnessConfig; +const harnessConfigWithPermissionsEnabled = { + metroPort: DEFAULT_METRO_PORT, + permissions: true, +} as HarnessConfig; const init = { signal: new AbortController().signal, }; @@ -72,6 +76,31 @@ describe('iOS platform instance dependency validation', () => { ).resolves.toBeDefined(); }); + it('does not start the simulator XCTest agent when permissions are disabled', async () => { + vi.spyOn(simctl, 'getSimulatorId').mockResolvedValue('sim-udid'); + vi.spyOn(simctl, 'isAppInstalled').mockResolvedValue(true); + vi.spyOn(simctl, 'getSimulatorStatus').mockResolvedValue('Booted'); + vi.spyOn(simctl, 'applyHarnessJsLocationOverride').mockResolvedValue( + undefined, + ); + + await getAppleSimulatorPlatformInstance( + { + name: 'ios', + device: { + type: 'simulator', + name: 'iPhone 16 Pro', + systemVersion: '18.0', + }, + bundleId: 'com.harnessplayground', + }, + harnessConfig, + init, + ); + + expect(xctestAgentMocks.createXCTestAgentController).not.toHaveBeenCalled(); + }); + it('discovers the physical device directly through devicectl', async () => { const getDevice = vi.spyOn(devicectl, 'getDevice').mockResolvedValue({ identifier: 'physical-device-id', @@ -99,6 +128,37 @@ describe('iOS platform instance dependency validation', () => { expect(getDevice).toHaveBeenCalledWith('My iPhone'); }); + it('does not start the physical-device XCTest agent when permissions are disabled', async () => { + vi.spyOn(devicectl, 'getDevice').mockResolvedValue({ + identifier: 'physical-device-id', + deviceProperties: { + name: 'My iPhone', + osVersionNumber: '18.0', + }, + hardwareProperties: { + marketingName: 'iPhone', + productType: 'iPhone17,1', + udid: '00008140-001600222422201C', + }, + }); + vi.spyOn(devicectl, 'isAppInstalled').mockResolvedValue(true); + + await getApplePhysicalDevicePlatformInstance( + { + name: 'ios-device', + device: { + type: 'physical', + name: 'My iPhone', + codeSign: { teamId: 'TESTTEAM01' }, + }, + bundleId: 'com.harnessplayground', + }, + harnessConfig, + ); + + expect(xctestAgentMocks.createXCTestAgentController).not.toHaveBeenCalled(); + }); + it('skips physical crash monitoring setup when native crash detection is disabled', async () => { vi.spyOn(devicectl, 'getDevice').mockResolvedValue({ identifier: 'physical-device-id', @@ -185,7 +245,7 @@ describe('iOS platform instance dependency validation', () => { }, bundleId: 'com.harnessplayground', }, - harnessConfig, + harnessConfigWithPermissionsEnabled, init, ); diff --git a/packages/platform-ios/src/instance.ts b/packages/platform-ios/src/instance.ts index a1044fb..55b53a4 100644 --- a/packages/platform-ios/src/instance.ts +++ b/packages/platform-ios/src/instance.ts @@ -59,6 +59,7 @@ export const getAppleSimulatorPlatformInstance = async ( ): Promise => { assertAppleDeviceSimulator(config.device); const detectNativeCrashes = harnessConfig.detectNativeCrashes ?? true; + const permissionsEnabled = harnessConfig.permissions ?? false; const udid = await simctl.getSimulatorId( config.device.name, @@ -122,22 +123,24 @@ export const getAppleSimulatorPlatformInstance = async ( `localhost:${harnessConfig.metroPort}`, ); - const xctestAgent = createXCTestAgentController({ - appBundleId: config.bundleId, - target: { - kind: 'simulator', - id: udid, - }, - capabilities: [createPermissionPromptAutoAcceptCapability()], - }); + const xctestAgent = permissionsEnabled + ? createXCTestAgentController({ + appBundleId: config.bundleId, + target: { + kind: 'simulator', + id: udid, + }, + capabilities: [createPermissionPromptAutoAcceptCapability()], + }) + : null; let agentStarted = false; try { - await xctestAgent.ensureStarted(); + await xctestAgent?.ensureStarted(); agentStarted = true; } finally { if (!agentStarted) { - await xctestAgent.dispose(); + await xctestAgent?.dispose(); await simctl.clearHarnessJsLocationOverride(udid, config.bundleId); if (startedByHarness) { await simctl.shutdownSimulator(udid); @@ -167,7 +170,7 @@ export const getAppleSimulatorPlatformInstance = async ( await simctl.stopApp(udid, config.bundleId); }, dispose: async () => { - await xctestAgent.dispose(); + await xctestAgent?.dispose(); await simctl.stopApp(udid, config.bundleId); await simctl.clearHarnessJsLocationOverride(udid, config.bundleId); @@ -199,6 +202,7 @@ export const getApplePhysicalDevicePlatformInstance = async ( ): Promise => { assertAppleDevicePhysical(config.device); const detectNativeCrashes = harnessConfig.detectNativeCrashes ?? true; + const permissionsEnabled = harnessConfig.permissions ?? false; if (harnessConfig.metroPort !== DEFAULT_METRO_PORT) { throw new Error( @@ -223,7 +227,7 @@ export const getApplePhysicalDevicePlatformInstance = async ( ); } - const xctestAgent = config.device.codeSign + const xctestAgent = permissionsEnabled && config.device.codeSign ? createXCTestAgentController({ appBundleId: config.bundleId, target: { @@ -245,7 +249,7 @@ export const getApplePhysicalDevicePlatformInstance = async ( await xctestAgent.dispose(); } } - } else { + } else if (permissionsEnabled) { iosInstanceLogger.info( 'Skipping XCTest agent for physical device (no codeSign config provided)', ); diff --git a/website/src/docs/getting-started/configuration.mdx b/website/src/docs/getting-started/configuration.mdx index 6bcc75b..e638cd8 100644 --- a/website/src/docs/getting-started/configuration.mdx +++ b/website/src/docs/getting-started/configuration.mdx @@ -100,6 +100,7 @@ For Expo projects, the `entryPoint` should be set to the path specified in the ` | `bridgeTimeout` | Bridge timeout in milliseconds (default: `60000`). | | `bundleStartTimeout` | Bundle start timeout in milliseconds (default: `60000`). | | `maxAppRestarts` | Maximum number of automatic app relaunch attempts while Harness is waiting for startup (default: `2`). | +| `permissions` | Enable platform-specific permission prompt automation (default: `false`). On iOS, this controls whether Harness starts the XCTest-based permission helper. | | `resetEnvironmentBetweenTestFiles` | Reset environment between test files (default: `true`). | | `detectNativeCrashes` | Detect native app crashes during startup and test execution (default: `true`). | | `crashDetectionInterval` | Interval in milliseconds to check for native crashes (default: `500`). | @@ -176,6 +177,20 @@ react-native-harness --harnessRunner ios ## Platform Ready Timeout +## Permissions + +Use the `permissions` flag to opt into Harness-managed permission prompt handling. + +```javascript +{ + permissions: true, +} +``` + +**Default:** `false` + +When `permissions` is `false`, Harness does not start platform-specific permission automation helpers. On iOS that means no XCTest agent session is started for permission auto-accept. Physical iOS devices still require `device.codeSign` in the runner config when you set `permissions: true`. + The platform ready timeout controls how long React Native Harness waits for the selected device, simulator, or emulator to become usable. This includes device discovery, simulator or emulator boot, and platform runtime setup before the app is launched. ```javascript From f658867014606f847f5810ec55f888dfafecaddc Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Tue, 28 Apr 2026 15:22:51 +0200 Subject: [PATCH 16/33] docs: add permissions guide Document the current permissions flag behavior and clarify that iOS permission automation exists today while Android-specific permission automation is not yet implemented. --- website/src/docs/guides/_meta.json | 5 ++ website/src/docs/guides/permissions.mdx | 76 +++++++++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 website/src/docs/guides/permissions.mdx diff --git a/website/src/docs/guides/_meta.json b/website/src/docs/guides/_meta.json index dbcc4f6..eedc60f 100644 --- a/website/src/docs/guides/_meta.json +++ b/website/src/docs/guides/_meta.json @@ -1,4 +1,9 @@ [ + { + "type": "file", + "name": "permissions", + "label": "Permissions" + }, { "type": "file", "name": "ui-testing", diff --git a/website/src/docs/guides/permissions.mdx b/website/src/docs/guides/permissions.mdx new file mode 100644 index 0000000..e9cd126 --- /dev/null +++ b/website/src/docs/guides/permissions.mdx @@ -0,0 +1,76 @@ +import { PackageManagerTabs } from '@theme'; + +# Permissions + +Harness exposes a single top-level `permissions` flag: + +```javascript +{ + permissions: true, +} +``` + +This setting is currently **all-or-nothing**. +You can enable permission automation for the whole run or disable it completely. +Harness does not yet support selecting individual permission types or per-platform overrides. + +## Current Behavior + +When `permissions` is `false`: + +- Harness does not start permission automation helpers. +- On iOS, this means the XCTest-based permission helper is not started. + +When `permissions` is `true`: + +- On iOS simulators, Harness starts the XCTest agent and enables best-effort permission prompt auto-accept. +- On iOS physical devices, Harness also starts the XCTest agent when the runner is configured with `device.codeSign`. +- On Android, the shared config flag exists, but there is currently no dedicated Android permission automation implementation in the codebase. + +## What iOS Actually Automates + +The current iOS implementation does **not** pre-approve every permission up front. +Instead, it runs a best-effort watchdog that taps known positive system-prompt buttons such as: + +- `Allow` +- `OK` +- `Continue` +- `While Using the App` +- `Always Allow` +- `Allow Once` + +Because this is button-based automation, it should be treated as a practical helper for common prompts rather than a guarantee that every permission dialog will be handled. + +## Performance Impact + +Enabling `permissions` can make Harness heavier on iOS because it starts an additional XCTest agent session alongside the normal run. + +This can have a negative impact on: + +- startup time +- device preparation time +- overall run performance + +The impact is most important to keep in mind for iOS physical devices, where the XCTest agent requires code signing and extra setup, but the same helper is also started on iOS simulators when permissions are enabled. + +## Physical iOS Devices + +To use permission automation on a physical iOS device, the runner must provide `device.codeSign`. + +```javascript +applePlatform({ + name: 'iphone', + device: applePhysicalDevice('My iPhone', { + codeSign: { teamId: 'TEAMID1234' }, + }), + bundleId: 'com.example.myapp', +}) +``` + +If `permissions: true` is set without `device.codeSign`, Harness skips the XCTest agent for that physical device. + +## Recommendation + +Leave `permissions` disabled unless your tests actually need system permission prompts to be handled automatically. + +That keeps the default Harness startup path lighter and avoids paying the extra iOS XCTest overhead on runs that do not need it. From 2fcfa0cd6ecb60f11727c1180d097ada0a916dcb Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Tue, 28 Apr 2026 17:03:57 +0200 Subject: [PATCH 17/33] feat(platform-android): implement permission automation via adb Add permission granting functionality for Android that uses adb to automatically grant a comprehensive set of common dangerous permissions when the permissions config flag is enabled. Works on both emulators and physical devices. - Create android/permissions.ts with default permission list - Add adb.grantPermissions() function to grant permissions via 'pm grant' - Integrate permission granting in emulator and physical device instance setup - Add comprehensive tests for permission granting scenarios - Update permissions documentation to describe Android implementation --- .../src/__tests__/adb.test.ts | 42 +++++ .../src/__tests__/instance.test.ts | 144 ++++++++++++++++++ packages/platform-android/src/adb.ts | 24 +++ packages/platform-android/src/instance.ts | 13 ++ packages/platform-android/src/permissions.ts | 27 ++++ website/src/docs/guides/permissions.mdx | 28 +++- 6 files changed, 275 insertions(+), 3 deletions(-) create mode 100644 packages/platform-android/src/permissions.ts diff --git a/packages/platform-android/src/__tests__/adb.test.ts b/packages/platform-android/src/__tests__/adb.test.ts index 032a4b1..87b8450 100644 --- a/packages/platform-android/src/__tests__/adb.test.ts +++ b/packages/platform-android/src/__tests__/adb.test.ts @@ -595,4 +595,46 @@ describe('getStartAppArgs', () => { await expect(waitPromise).resolves.toBeUndefined(); expect(spawnSpy).toHaveBeenCalledTimes(2); }); + + it('grants permissions to an app', async () => { + const { grantPermissions } = await import('../adb.js'); + const spawnSpy = vi.spyOn(tools, 'spawn'); + spawnSpy.mockResolvedValue({ + stdout: '', + } as Awaited>); + + await grantPermissions('emulator-5554', 'com.example.app', [ + 'android.permission.CAMERA', + 'android.permission.RECORD_AUDIO', + ]); + + expect(spawnSpy).toHaveBeenCalledTimes(2); + expect(spawnSpy).toHaveBeenNthCalledWith(1, expect.any(String), [ + '-s', + 'emulator-5554', + 'shell', + 'pm', + 'grant', + 'com.example.app', + 'android.permission.CAMERA', + ]); + expect(spawnSpy).toHaveBeenNthCalledWith(2, expect.any(String), [ + '-s', + 'emulator-5554', + 'shell', + 'pm', + 'grant', + 'com.example.app', + 'android.permission.RECORD_AUDIO', + ]); + }); + + it('handles empty permission list when granting permissions', async () => { + const { grantPermissions } = await import('../adb.js'); + const spawnSpy = vi.spyOn(tools, 'spawn'); + + await grantPermissions('emulator-5554', 'com.example.app', []); + + expect(spawnSpy).not.toHaveBeenCalled(); + }); }); diff --git a/packages/platform-android/src/__tests__/instance.test.ts b/packages/platform-android/src/__tests__/instance.test.ts index 505fd4e..7c82dcd 100644 --- a/packages/platform-android/src/__tests__/instance.test.ts +++ b/packages/platform-android/src/__tests__/instance.test.ts @@ -607,4 +607,148 @@ describe('Android platform instance', () => { expect(appMonitor.addListener(listener)).toBeUndefined(); expect(appMonitor.removeListener(listener)).toBeUndefined(); }); + + it('grants permissions when permissions are enabled for emulator', async () => { + vi.spyOn( + await import('../environment.js'), + 'ensureAndroidEmulatorEnvironment', + ).mockResolvedValue('/tmp/android-sdk'); + vi.spyOn(adb, 'getDeviceIds').mockResolvedValue(['emulator-5554']); + vi.spyOn(adb, 'getEmulatorName').mockResolvedValue('Pixel_8_API_35'); + vi.spyOn(adb, 'waitForBoot').mockResolvedValue('emulator-5554'); + vi.spyOn(adb, 'isAppInstalled').mockResolvedValue(true); + vi.spyOn(adb, 'reversePort').mockResolvedValue(undefined); + vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined); + vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234); + vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue( + undefined, + ); + const grantPermissions = vi + .spyOn(adb, 'grantPermissions') + .mockResolvedValue(undefined); + + const harnessConfigWithPermissions = { + ...harnessConfig, + permissions: true, + } as HarnessConfig; + + await getAndroidEmulatorPlatformInstance( + { + name: 'android', + device: { + type: 'emulator', + name: 'Pixel_8_API_35', + avd: { + apiLevel: 35, + profile: 'pixel_8', + diskSize: '1G', + heapSize: '1G', + }, + }, + bundleId: 'com.harnessplayground', + activityName: '.MainActivity', + }, + harnessConfigWithPermissions, + init, + ); + + expect(grantPermissions).toHaveBeenCalledWith( + 'emulator-5554', + 'com.harnessplayground', + expect.arrayContaining([ + 'android.permission.CAMERA', + 'android.permission.RECORD_AUDIO', + 'android.permission.ACCESS_FINE_LOCATION', + ]), + ); + }); + + it('does not grant permissions when permissions are disabled for emulator', async () => { + vi.spyOn( + await import('../environment.js'), + 'ensureAndroidEmulatorEnvironment', + ).mockResolvedValue('/tmp/android-sdk'); + vi.spyOn(adb, 'getDeviceIds').mockResolvedValue(['emulator-5554']); + vi.spyOn(adb, 'getEmulatorName').mockResolvedValue('Pixel_8_API_35'); + vi.spyOn(adb, 'waitForBoot').mockResolvedValue('emulator-5554'); + vi.spyOn(adb, 'isAppInstalled').mockResolvedValue(true); + vi.spyOn(adb, 'reversePort').mockResolvedValue(undefined); + vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined); + vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234); + vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue( + undefined, + ); + const grantPermissions = vi + .spyOn(adb, 'grantPermissions') + .mockResolvedValue(undefined); + + await getAndroidEmulatorPlatformInstance( + { + name: 'android', + device: { + type: 'emulator', + name: 'Pixel_8_API_35', + avd: { + apiLevel: 35, + profile: 'pixel_8', + diskSize: '1G', + heapSize: '1G', + }, + }, + bundleId: 'com.harnessplayground', + activityName: '.MainActivity', + }, + harnessConfig, + init, + ); + + expect(grantPermissions).not.toHaveBeenCalled(); + }); + + it('grants permissions when permissions are enabled for physical device', async () => { + vi.spyOn(adb, 'getDeviceIds').mockResolvedValue(['012345']); + vi.spyOn(adb, 'getDeviceInfo').mockResolvedValue({ + manufacturer: 'motorola', + model: 'moto g72', + }); + vi.spyOn(adb, 'isAppInstalled').mockResolvedValue(true); + vi.spyOn(adb, 'reversePort').mockResolvedValue(undefined); + vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined); + vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234); + vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue( + undefined, + ); + const grantPermissions = vi + .spyOn(adb, 'grantPermissions') + .mockResolvedValue(undefined); + + const harnessConfigWithPermissions = { + ...harnessConfig, + permissions: true, + } as HarnessConfig; + + await getAndroidPhysicalDevicePlatformInstance( + { + name: 'android-device', + device: { + type: 'physical', + manufacturer: 'motorola', + model: 'moto g72', + }, + bundleId: 'com.harnessplayground', + activityName: '.MainActivity', + }, + harnessConfigWithPermissions, + ); + + expect(grantPermissions).toHaveBeenCalledWith( + '012345', + 'com.harnessplayground', + expect.arrayContaining([ + 'android.permission.CAMERA', + 'android.permission.RECORD_AUDIO', + 'android.permission.ACCESS_FINE_LOCATION', + ]), + ); + }); }); diff --git a/packages/platform-android/src/adb.ts b/packages/platform-android/src/adb.ts index 1058107..9b12739 100644 --- a/packages/platform-android/src/adb.ts +++ b/packages/platform-android/src/adb.ts @@ -730,3 +730,27 @@ export const getConnectedDevices = async (): Promise => { return devices; }; + +export const grantPermissions = async ( + adbId: string, + bundleId: string, + permissions: string[], +): Promise => { + if (permissions.length === 0) { + return; + } + + const grantCommands = permissions.map((permission) => [ + '-s', + adbId, + 'shell', + 'pm', + 'grant', + bundleId, + permission, + ]); + + await Promise.all( + grantCommands.map((args) => spawn(getAdbBinaryPath(), args as string[])), + ); +}; diff --git a/packages/platform-android/src/instance.ts b/packages/platform-android/src/instance.ts index 5cf97ab..2f77ae9 100644 --- a/packages/platform-android/src/instance.ts +++ b/packages/platform-android/src/instance.ts @@ -34,6 +34,7 @@ import { import { isInteractive } from '@react-native-harness/tools'; import fs from 'node:fs'; import type { AppMonitor } from '@react-native-harness/platforms'; +import { getDefaultAndroidPermissions } from './permissions.js'; const androidInstanceLogger = logger.child('android-instance'); @@ -180,6 +181,7 @@ export const getAndroidEmulatorPlatformInstance = async ( ): Promise => { assertAndroidDeviceEmulator(config.device); const detectNativeCrashes = harnessConfig.detectNativeCrashes ?? true; + const permissionsEnabled = harnessConfig.permissions ?? false; const emulatorConfig = config.device; const emulatorName = emulatorConfig.name; const avdConfig = emulatorConfig.avd; @@ -258,6 +260,11 @@ export const getAndroidEmulatorPlatformInstance = async ( const appUid = await configureAndroidRuntime(adbId, config, harnessConfig); + if (permissionsEnabled) { + const defaultPermissions = getDefaultAndroidPermissions(); + await adb.grantPermissions(adbId, config.bundleId, defaultPermissions); + } + return { startApp: async (options) => { await adb.startApp( @@ -315,6 +322,7 @@ export const getAndroidPhysicalDevicePlatformInstance = async ( ): Promise => { assertAndroidDevicePhysical(config.device); const detectNativeCrashes = harnessConfig.detectNativeCrashes ?? true; + const permissionsEnabled = harnessConfig.permissions ?? false; const adbId = await getAdbId(config.device); @@ -333,6 +341,11 @@ export const getAndroidPhysicalDevicePlatformInstance = async ( const appUid = await configureAndroidRuntime(adbId, config, harnessConfig); + if (permissionsEnabled) { + const defaultPermissions = getDefaultAndroidPermissions(); + await adb.grantPermissions(adbId, config.bundleId, defaultPermissions); + } + return { startApp: async (options) => { await adb.startApp( diff --git a/packages/platform-android/src/permissions.ts b/packages/platform-android/src/permissions.ts new file mode 100644 index 0000000..07bba47 --- /dev/null +++ b/packages/platform-android/src/permissions.ts @@ -0,0 +1,27 @@ +const DEFAULT_PERMISSIONS = [ + 'android.permission.ACCESS_FINE_LOCATION', + 'android.permission.ACCESS_COARSE_LOCATION', + 'android.permission.CAMERA', + 'android.permission.RECORD_AUDIO', + 'android.permission.READ_CALENDAR', + 'android.permission.WRITE_CALENDAR', + 'android.permission.READ_CONTACTS', + 'android.permission.WRITE_CONTACTS', + 'android.permission.READ_CALL_LOG', + 'android.permission.WRITE_CALL_LOG', + 'android.permission.READ_EXTERNAL_STORAGE', + 'android.permission.WRITE_EXTERNAL_STORAGE', + 'android.permission.READ_PHONE_STATE', + 'android.permission.CALL_PHONE', + 'android.permission.READ_SMS', + 'android.permission.SEND_SMS', + 'android.permission.RECEIVE_SMS', + 'android.permission.READ_CELL_BROADCASTS', + 'android.permission.BODY_SENSORS', + 'android.permission.INTERNET', + 'android.permission.ACCESS_NETWORK_STATE', +]; + +export const getDefaultAndroidPermissions = (): string[] => { + return DEFAULT_PERMISSIONS; +}; diff --git a/website/src/docs/guides/permissions.mdx b/website/src/docs/guides/permissions.mdx index e9cd126..3214a90 100644 --- a/website/src/docs/guides/permissions.mdx +++ b/website/src/docs/guides/permissions.mdx @@ -25,11 +25,13 @@ When `permissions` is `true`: - On iOS simulators, Harness starts the XCTest agent and enables best-effort permission prompt auto-accept. - On iOS physical devices, Harness also starts the XCTest agent when the runner is configured with `device.codeSign`. -- On Android, the shared config flag exists, but there is currently no dedicated Android permission automation implementation in the codebase. +- On Android emulators and physical devices, Harness uses `adb shell pm grant` to automatically grant a set of common permissions. -## What iOS Actually Automates +## Platform-Specific Implementation Details -The current iOS implementation does **not** pre-approve every permission up front. +### iOS + +The iOS implementation does **not** pre-approve every permission up front. Instead, it runs a best-effort watchdog that taps known positive system-prompt buttons such as: - `Allow` @@ -41,6 +43,26 @@ Instead, it runs a best-effort watchdog that taps known positive system-prompt b Because this is button-based automation, it should be treated as a practical helper for common prompts rather than a guarantee that every permission dialog will be handled. +### Android + +On Android, Harness pre-grants a comprehensive set of common dangerous permissions using `adb shell pm grant`. This approach: + +- Grants permissions **proactively** before the app runs, avoiding permission dialogs during testing +- Works on both emulators and physical devices +- Includes location, camera, microphone, contacts, calendar, storage, SMS, and other common test permissions +- Uses the `pm grant` command which requires API 23+ or emulated environment support + +The default set of granted permissions includes: +- Location (fine and coarse) +- Camera and microphone +- Contacts and calendar (read/write) +- Call log (read/write) +- Storage (read/write) +- Phone state and calling +- SMS (read/send/receive) +- Body sensors +- Network state and internet + ## Performance Impact Enabling `permissions` can make Harness heavier on iOS because it starts an additional XCTest agent session alongside the normal run. From 2e30e860f4c93600bb27350d893348f2eb76ca6b Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Tue, 28 Apr 2026 17:52:29 +0200 Subject: [PATCH 18/33] fix: harden device crash and permission handling --- actions/shared/index.cjs | 1 + packages/jest/src/__tests__/harness.test.ts | 74 -------------- packages/jest/src/crash-supervisor.ts | 5 - packages/jest/src/harness.ts | 10 -- packages/platform-android/src/adb-errors.ts | 47 +++++++++ packages/platform-android/src/adb.ts | 19 +++- .../src/__tests__/app-monitor.test.ts | 24 ----- .../src/__tests__/crash-diagnostics.test.ts | 7 +- packages/platform-ios/src/app-monitor.ts | 13 +++ .../platform-ios/src/crash-diagnostics.ts | 96 +++++++++++++++---- .../src/xcrun/devicectl-errors.ts | 38 ++++++++ packages/platform-ios/src/xcrun/devicectl.ts | 45 +++++---- .../src/xctest-agent-transport-device.ts | 11 ++- 13 files changed, 223 insertions(+), 167 deletions(-) create mode 100644 packages/platform-android/src/adb-errors.ts create mode 100644 packages/platform-ios/src/xcrun/devicectl-errors.ts diff --git a/actions/shared/index.cjs b/actions/shared/index.cjs index 0575912..aa7d0b7 100644 --- a/actions/shared/index.cjs +++ b/actions/shared/index.cjs @@ -4415,6 +4415,7 @@ var ConfigSchema = external_exports.object({ resetEnvironmentBetweenTestFiles: external_exports.boolean().optional().default(true), unstable__skipAlreadyIncludedModules: external_exports.boolean().optional().default(false), unstable__enableMetroCache: external_exports.boolean().optional().default(false), + permissions: external_exports.boolean().optional().default(false).describe("Enable platform-specific permission prompt automation. When false, Harness does not start permission-handling helpers such as the iOS XCTest agent."), detectNativeCrashes: external_exports.boolean().optional().default(true), crashDetectionInterval: external_exports.number().min(100, "Crash detection interval must be at least 100ms").default(500), disableViewFlattening: external_exports.boolean().optional().default(false).describe("Disable view flattening in React Native. This will set collapsable={true} for all View components to ensure they are not flattened by the native layout engine."), diff --git a/packages/jest/src/__tests__/harness.test.ts b/packages/jest/src/__tests__/harness.test.ts index 8d84a0b..f828b09 100644 --- a/packages/jest/src/__tests__/harness.test.ts +++ b/packages/jest/src/__tests__/harness.test.ts @@ -572,80 +572,6 @@ describe('getHarness', () => { await harness.dispose(); }); - it('does not forward pre-launch crash monitor events to plugins', async () => { - const { serverBridge } = createBridgeServer(); - const appMonitor = createAppMonitor(); - const platformInstance = createPlatformRunner({ - createAppMonitor: () => appMonitor.appMonitor, - }); - const metroInstance = createMetroInstance(); - const appEvents: string[] = []; - - mocks.getBridgeServer.mockResolvedValue(serverBridge); - mocks.getMetroInstance.mockResolvedValue(metroInstance); - - ( - globalThis as typeof globalThis & { - __HARNESS_PLATFORM_RUNNER__?: (...args: unknown[]) => Promise; - } - ).__HARNESS_PLATFORM_RUNNER__ = vi.fn(async () => platformInstance); - - const platform: HarnessPlatform = { - config: {}, - name: 'ios', - platformId: 'ios', - runner: `data:text/javascript,${encodeURIComponent( - 'export default (...args) => globalThis.__HARNESS_PLATFORM_RUNNER__(...args);', - )}`, - getResourceLockKey: () => 'ios:simulator:iPhone 17 Pro:26.2', - }; - - const harness = await getHarness( - createHarnessConfig({ - plugins: [ - definePlugin({ - name: 'capture-app-events', - hooks: { - app: { - possibleCrash: (ctx) => { - appEvents.push(`possibleCrash:${ctx.testFile ?? 'n/a'}`); - }, - exited: (ctx) => { - appEvents.push(`exited:${ctx.testFile ?? 'n/a'}`); - }, - }, - }, - }), - ], - }), - platform, - '/tmp/project', - ); - - appMonitor.emit({ - type: 'possible_crash', - source: 'logs', - isConfirmed: true, - crashDetails: { - source: 'logs', - summary: 'stale pre-launch crash signal', - }, - }); - appMonitor.emit({ - type: 'app_exited', - source: 'polling', - isConfirmed: true, - crashDetails: { - source: 'polling', - summary: 'stale pre-launch exit signal', - }, - }); - - await harness.dispose(); - - expect(appEvents).toEqual([]); - }); - it('routes restart(testFilePath) through the shared Metro startup helper', async () => { const { serverBridge, emitReady } = createBridgeServer(); const appMonitor = createAppMonitor(); diff --git a/packages/jest/src/crash-supervisor.ts b/packages/jest/src/crash-supervisor.ts index 94b56f3..6ba1fae 100644 --- a/packages/jest/src/crash-supervisor.ts +++ b/packages/jest/src/crash-supervisor.ts @@ -26,7 +26,6 @@ export type CrashSupervisor = { beginLaunch: (testFilePath: string) => void; markReady: () => void; beginTestRun: (testFilePath: string) => void; - isArmed: () => boolean; stop: () => Promise; start: () => Promise; waitForCrash: (testFilePath: string) => Promise; @@ -256,9 +255,6 @@ export const createCrashSupervisor = ({ state = 'running'; }; - const isArmed = () => - state === 'launching' || state === 'ready' || state === 'running'; - const stop = async () => { monitoring = false; await appMonitor.stop(); @@ -314,7 +310,6 @@ export const createCrashSupervisor = ({ beginLaunch, markReady, beginTestRun, - isArmed, stop, start, waitForCrash, diff --git a/packages/jest/src/harness.ts b/packages/jest/src/harness.ts index fa54fb9..91a9539 100644 --- a/packages/jest/src/harness.ts +++ b/packages/jest/src/harness.ts @@ -600,8 +600,6 @@ const getHarnessInternal = async ( return; } - const shouldReportCrashEvent = crashSupervisor.isArmed(); - if (event.type === 'app_started') { scheduleHook('app:started', { runId, @@ -614,10 +612,6 @@ const getHarnessInternal = async ( } if (event.type === 'app_exited') { - if (!shouldReportCrashEvent) { - return; - } - scheduleHook('app:exited', { runId, testFile: activeTestFilePath, @@ -631,10 +625,6 @@ const getHarnessInternal = async ( } if (event.type === 'possible_crash') { - if (!shouldReportCrashEvent) { - return; - } - scheduleHook('app:possible-crash', { runId, testFile: activeTestFilePath, diff --git a/packages/platform-android/src/adb-errors.ts b/packages/platform-android/src/adb-errors.ts new file mode 100644 index 0000000..8075e9a --- /dev/null +++ b/packages/platform-android/src/adb-errors.ts @@ -0,0 +1,47 @@ +export class AdbError extends Error { + constructor(message: string) { + super(message); + this.name = 'AdbError'; + } +} + +export class AdbDeviceNotFoundError extends AdbError { + constructor(adbId: string) { + super( + `Android device "${adbId}" not found or not connected. ` + + `Run "adb devices" to see available devices.` + ); + this.name = 'AdbDeviceNotFoundError'; + } +} + +export class AdbAppNotInstalledError extends AdbError { + constructor(bundleId: string, adbId: string) { + super( + `App "${bundleId}" is not installed on device "${adbId}". ` + + `Install the app before running tests.` + ); + this.name = 'AdbAppNotInstalledError'; + } +} + +export class AdbPermissionGrantError extends AdbError { + constructor(bundleId: string, permissions: string[], adbId: string) { + const permissionList = permissions.join(', '); + super( + `Failed to grant permissions [${permissionList}] to "${bundleId}" on device "${adbId}". ` + + `Verify the app is installed and the device supports these permissions.` + ); + this.name = 'AdbPermissionGrantError'; + } +} + +export class AdbBinaryNotFoundError extends AdbError { + constructor() { + super( + `adb binary not found or not accessible. ` + + `Ensure Android SDK is properly installed and ANDROID_HOME is set.` + ); + this.name = 'AdbBinaryNotFoundError'; + } +} diff --git a/packages/platform-android/src/adb.ts b/packages/platform-android/src/adb.ts index 9b12739..44d5c96 100644 --- a/packages/platform-android/src/adb.ts +++ b/packages/platform-android/src/adb.ts @@ -23,6 +23,10 @@ import { getEmulatorStartupArgs, type EmulatorBootMode, } from './emulator-startup.js'; +import { + AdbAppNotInstalledError, + AdbPermissionGrantError, +} from './adb-errors.js'; const wait = async (ms: number): Promise => { await new Promise((resolve) => { @@ -740,6 +744,11 @@ export const grantPermissions = async ( return; } + const isInstalled = await isAppInstalled(adbId, bundleId); + if (!isInstalled) { + throw new AdbAppNotInstalledError(bundleId, adbId); + } + const grantCommands = permissions.map((permission) => [ '-s', adbId, @@ -750,7 +759,11 @@ export const grantPermissions = async ( permission, ]); - await Promise.all( - grantCommands.map((args) => spawn(getAdbBinaryPath(), args as string[])), - ); + try { + await Promise.all( + grantCommands.map((args) => spawn(getAdbBinaryPath(), args as string[])), + ); + } catch (error) { + throw new AdbPermissionGrantError(bundleId, permissions, adbId); + } }; diff --git a/packages/platform-ios/src/__tests__/app-monitor.test.ts b/packages/platform-ios/src/__tests__/app-monitor.test.ts index 31956d4..a7336c7 100644 --- a/packages/platform-ios/src/__tests__/app-monitor.test.ts +++ b/packages/platform-ios/src/__tests__/app-monitor.test.ts @@ -290,30 +290,6 @@ describe('createIosDeviceAppMonitor', () => { expect(events.some((event) => event.type === 'app_exited')).toBe(true); }); - it('does not collect device crash artifacts during monitor startup', async () => { - vi.spyOn(devicectl, 'getAppInfo').mockResolvedValue({ - bundleIdentifier: 'com.harnessplayground', - name: 'HarnessPlayground', - version: '1.0', - url: '/private/var/HarnessPlayground.app', - }); - vi.spyOn(devicectl, 'getProcesses').mockResolvedValue([]); - const collectCrashArtifactsSpy = vi.spyOn( - diagnostics, - 'collectCrashArtifacts', - ); - - const monitor = createIosDeviceAppMonitor({ - deviceId: 'device-udid', - bundleId: 'com.harnessplayground', - }); - - await monitor.start(); - await monitor.stop(); - - expect(collectCrashArtifactsSpy).not.toHaveBeenCalled(); - }); - it('enriches device crashes with Apple-native pulled crash reports', async () => { vi.spyOn(devicectl, 'getAppInfo').mockResolvedValue({ bundleIdentifier: 'com.harnessplayground', diff --git a/packages/platform-ios/src/__tests__/crash-diagnostics.test.ts b/packages/platform-ios/src/__tests__/crash-diagnostics.test.ts index 21ba567..6aa0cf7 100644 --- a/packages/platform-ios/src/__tests__/crash-diagnostics.test.ts +++ b/packages/platform-ios/src/__tests__/crash-diagnostics.test.ts @@ -65,7 +65,7 @@ describe('collectCrashArtifacts', () => { }); }); - it('collects device crash artifacts from systemCrashLogs before falling back to diagnose', async () => { + it('collects device crash artifacts from systemCrashLogs', async () => { const outputRoot = fs.mkdtempSync( join(tmpdir(), 'rn-harness-devicectl-crash-logs-'), ); @@ -89,10 +89,6 @@ describe('collectCrashArtifacts', () => { fs.copyFileSync(crashPath, options.destination); }, ); - const diagnoseSpy = vi - .spyOn(devicectl, 'diagnose') - .mockResolvedValue(undefined); - const artifacts = await collectCrashArtifacts({ targetId: 'device-udid', targetType: 'device', @@ -108,7 +104,6 @@ describe('collectCrashArtifacts', () => { bundleId: 'com.harnessplayground', signal: 'SIGABRT', }); - expect(diagnoseSpy).not.toHaveBeenCalled(); }); it('persists matched crash artifacts with the provided writer', async () => { diff --git a/packages/platform-ios/src/app-monitor.ts b/packages/platform-ios/src/app-monitor.ts index 6bab0f7..3d8cc9b 100644 --- a/packages/platform-ios/src/app-monitor.ts +++ b/packages/platform-ios/src/app-monitor.ts @@ -16,6 +16,7 @@ import { import * as devicectl from './xcrun/devicectl.js'; import * as simctl from './xcrun/simctl.js'; import { + collectCrashArtifacts, waitForCrashArtifact, } from './crash-diagnostics.js'; @@ -580,6 +581,18 @@ export const createIosDeviceAppMonitor = ({ } })(); + const initialArtifacts = await collectCrashArtifacts({ + targetId: deviceId, + targetType: 'device', + bundleId, + processNames, + crashArtifactWriter, + minOccurredAt: monitorStartedAt, + }); + + for (const artifact of initialArtifacts) { + base.recordCrashArtifact(artifact); + } }; const stopLogMonitor = async () => { diff --git a/packages/platform-ios/src/crash-diagnostics.ts b/packages/platform-ios/src/crash-diagnostics.ts index 1c8ab1b..acc8698 100644 --- a/packages/platform-ios/src/crash-diagnostics.ts +++ b/packages/platform-ios/src/crash-diagnostics.ts @@ -5,7 +5,7 @@ import type { } from '@react-native-harness/platforms'; import { logger } from '@react-native-harness/tools'; import fs from 'node:fs'; -import { tmpdir } from 'node:os'; +import { homedir, tmpdir } from 'node:os'; import { join } from 'node:path'; import { randomUUID } from 'node:crypto'; import { iosCrashParser } from './crash-parser.js'; @@ -237,6 +237,72 @@ const collectSimulatorCrashArtifacts = async ({ } }; +const collectCrashArtifactsFromDiagnosticReports = ( + options: CollectCrashArtifactsOptions, +): DiagnosedCrashArtifact[] => { + const diagnosticReportsDir = join( + homedir(), + 'Library', + 'Logs', + 'DiagnosticReports', + ); + + if (!fs.existsSync(diagnosticReportsDir)) { + return []; + } + + const matchingEntries = fs + .readdirSync(diagnosticReportsDir) + .filter((entry) => entry.endsWith('.ips')) + .filter((entry) => + options.processNames.some((name) => entry.startsWith(`${name}-`)), + ); + + const artifacts: DiagnosedCrashArtifact[] = []; + + for (const entry of matchingEntries) { + const path = join(diagnosticReportsDir, entry); + const contents = fs.readFileSync(path, 'utf8'); + const parsed = iosCrashParser.parse({ path, contents }); + + if (!parsed) { + continue; + } + + if ( + options.minOccurredAt !== undefined && + parsed.occurredAt < options.minOccurredAt + ) { + continue; + } + + const artifactPath = options.crashArtifactWriter + ? options.crashArtifactWriter.persistArtifact({ + artifactKind: 'ios-crash-report', + source: { kind: 'file', path }, + }) + : path; + + const artifact: DiagnosedCrashArtifact = { + ...parsed, + artifactType: 'ios-crash-report', + artifactPath, + occurredAt: parsed.occurredAt, + }; + + artifact.score = scoreCrashArtifact({ artifact, options }); + artifacts.push(artifact); + } + + return artifacts.sort((left, right) => { + if ((right.score ?? 0) !== (left.score ?? 0)) { + return (right.score ?? 0) - (left.score ?? 0); + } + + return right.occurredAt - left.occurredAt; + }); +}; + const collectPhysicalCrashArtifacts = async ({ targetId, processNames, @@ -286,28 +352,18 @@ const collectPhysicalCrashArtifacts = async ({ return copiedArtifacts; } } - - const outputDir = createTempDirectory('rn-harness-devicectl-diagnose'); - - try { - await devicectl.diagnose(targetId, outputDir); - return parseCrashArtifacts({ - rootDir: outputDir, - options: { - targetId, - targetType: 'device', - processNames, - bundleId, - crashArtifactWriter, - minOccurredAt, - }, - }); - } finally { - fs.rmSync(outputDir, { recursive: true, force: true }); - } } finally { fs.rmSync(crashLogsDir, { recursive: true, force: true }); } + + return collectCrashArtifactsFromDiagnosticReports({ + targetId, + targetType: 'device', + processNames, + bundleId, + crashArtifactWriter, + minOccurredAt, + }); }; export const collectCrashArtifacts = async ( diff --git a/packages/platform-ios/src/xcrun/devicectl-errors.ts b/packages/platform-ios/src/xcrun/devicectl-errors.ts new file mode 100644 index 0000000..8d97534 --- /dev/null +++ b/packages/platform-ios/src/xcrun/devicectl-errors.ts @@ -0,0 +1,38 @@ +export class DevicectlError extends Error { + constructor(message: string) { + super(message); + this.name = 'DevicectlError'; + } +} + +export class DeviceNotFoundError extends DevicectlError { + constructor(deviceId: string) { + super( + `iOS device "${deviceId}" not found. ` + + `Run "xcrun devicectl list devices" to see available devices.` + ); + this.name = 'DeviceNotFoundError'; + } +} + +export class DeviceHostnameLookupError extends DevicectlError { + constructor(deviceId: string, details?: string) { + const detailsMessage = details ? ` (${details})` : ''; + super( + `Failed to determine network hostname for iOS device "${deviceId}"${detailsMessage}. ` + + `Verify the device is connected and can communicate over the network. ` + + `Run "xcrun devicectl device info details --device ${deviceId}" to diagnose.` + ); + this.name = 'DeviceHostnameLookupError'; + } +} + +export class DeviceAppNotFoundError extends DevicectlError { + constructor(bundleId: string, deviceId: string) { + super( + `App "${bundleId}" not found on iOS device "${deviceId}". ` + + `Install the app before running tests.` + ); + this.name = 'DeviceAppNotFoundError'; + } +} diff --git a/packages/platform-ios/src/xcrun/devicectl.ts b/packages/platform-ios/src/xcrun/devicectl.ts index 2f6422a..6269a23 100644 --- a/packages/platform-ios/src/xcrun/devicectl.ts +++ b/packages/platform-ios/src/xcrun/devicectl.ts @@ -4,6 +4,10 @@ import fs from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { randomUUID } from 'node:crypto'; +import { + DeviceHostnameLookupError, + DeviceNotFoundError, +} from './devicectl-errors.js'; export const devicectl = async ( command: string, @@ -121,16 +125,25 @@ export const getDeviceConnectionHost = ( export const getDeviceHostname = async ( identifier: string ): Promise => { - const details = await getDeviceDetails(identifier); - const hostname = getDeviceConnectionHost(details); - - if (!hostname) { - throw new Error( - `Could not determine iOS device hostname for ${identifier}. Run "xcrun devicectl device info details --device ${identifier} --json-output " and verify that CoreDevice reports connectionProperties.tunnelIPAddress or a DNS hostname.` - ); + try { + const details = await getDeviceDetails(identifier); + const hostname = getDeviceConnectionHost(details); + + if (!hostname) { + throw new DeviceHostnameLookupError( + identifier, + 'CoreDevice did not report a network address' + ); + } + + return hostname; + } catch (error) { + if (error instanceof DeviceHostnameLookupError) { + throw error; + } + + throw new DeviceNotFoundError(identifier); } - - return hostname; }; export type AppleAppInfo = { @@ -289,20 +302,6 @@ export const copyFileFrom = async ( ]); }; -export const diagnose = async ( - identifier: string, - outputDir: string -): Promise => { - await devicectl('diagnose', [ - '--devices', - identifier, - '--no-archive', - '--archive-destination', - outputDir, - '--keep-temp-dir', - ]); -}; - export const stopApp = async ( identifier: string, bundleId: string diff --git a/packages/platform-ios/src/xctest-agent-transport-device.ts b/packages/platform-ios/src/xctest-agent-transport-device.ts index 4248e1e..218def1 100644 --- a/packages/platform-ios/src/xctest-agent-transport-device.ts +++ b/packages/platform-ios/src/xctest-agent-transport-device.ts @@ -13,7 +13,14 @@ export const createDeviceXCTestAgentTransport = (options: { }): XCTestAgentTransport => { const timeoutMs = options.timeoutMs ?? 5000; const agent = new http.Agent({ keepAlive: false }); - const host = devicectl.getDeviceHostname(options.deviceId); + let hostPromise: Promise | null = null; + + const getHost = (): Promise => { + if (!hostPromise) { + hostPromise = devicectl.getDeviceHostname(options.deviceId); + } + return hostPromise; + }; return { request: async ( @@ -22,7 +29,7 @@ export const createDeviceXCTestAgentTransport = (options: { return await performHttpRequest({ agent, body: request.body, - host: await host, + host: await getHost(), method: request.method, path: request.path, port: options.port, From 5becc034101d3c6cd47d26a9e8147673757ff28c Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Wed, 29 Apr 2026 09:11:51 +0200 Subject: [PATCH 19/33] fix: use grouped permission listing for adb grants --- .../src/__tests__/adb.test.ts | 76 ++++++++++++---- packages/platform-android/src/adb.ts | 90 +++++++++++++++++-- 2 files changed, 145 insertions(+), 21 deletions(-) diff --git a/packages/platform-android/src/__tests__/adb.test.ts b/packages/platform-android/src/__tests__/adb.test.ts index 87b8450..df03c04 100644 --- a/packages/platform-android/src/__tests__/adb.test.ts +++ b/packages/platform-android/src/__tests__/adb.test.ts @@ -596,20 +596,54 @@ describe('getStartAppArgs', () => { expect(spawnSpy).toHaveBeenCalledTimes(2); }); - it('grants permissions to an app', async () => { + it('grants requested dangerous permissions to an app', async () => { const { grantPermissions } = await import('../adb.js'); - const spawnSpy = vi.spyOn(tools, 'spawn'); - spawnSpy.mockResolvedValue({ - stdout: '', - } as Awaited>); + const spawnSpy = vi + .spyOn(tools, 'spawn') + .mockResolvedValueOnce({ + stdout: 'package:com.example.app\n', + } as Awaited>) + .mockResolvedValueOnce({ + stdout: `requested permissions:\n android.permission.CAMERA\n android.permission.INTERNET\n android.permission.ACCESS_FINE_LOCATION\ninstall permissions:\n`, + } as Awaited>) + .mockResolvedValueOnce({ + stdout: `Dangerous Permissions:\n permission:android.permission.CAMERA\n permission:android.permission.ACCESS_FINE_LOCATION\n`, + } as Awaited>) + .mockResolvedValue({ + stdout: '', + } as Awaited>); - await grantPermissions('emulator-5554', 'com.example.app', [ - 'android.permission.CAMERA', - 'android.permission.RECORD_AUDIO', - ]); + await grantPermissions('emulator-5554', 'com.example.app'); - expect(spawnSpy).toHaveBeenCalledTimes(2); + expect(spawnSpy).toHaveBeenCalledTimes(5); expect(spawnSpy).toHaveBeenNthCalledWith(1, expect.any(String), [ + '-s', + 'emulator-5554', + 'shell', + 'pm', + 'list', + 'packages', + 'com.example.app', + ]); + expect(spawnSpy).toHaveBeenNthCalledWith(2, expect.any(String), [ + '-s', + 'emulator-5554', + 'shell', + 'dumpsys', + 'package', + 'com.example.app', + ]); + expect(spawnSpy).toHaveBeenNthCalledWith(3, expect.any(String), [ + '-s', + 'emulator-5554', + 'shell', + 'pm', + 'list', + 'permissions', + '-g', + '-d', + ]); + expect(spawnSpy).toHaveBeenNthCalledWith(4, expect.any(String), [ '-s', 'emulator-5554', 'shell', @@ -618,23 +652,33 @@ describe('getStartAppArgs', () => { 'com.example.app', 'android.permission.CAMERA', ]); - expect(spawnSpy).toHaveBeenNthCalledWith(2, expect.any(String), [ + expect(spawnSpy).toHaveBeenNthCalledWith(5, expect.any(String), [ '-s', 'emulator-5554', 'shell', 'pm', 'grant', 'com.example.app', - 'android.permission.RECORD_AUDIO', + 'android.permission.ACCESS_FINE_LOCATION', ]); }); - it('handles empty permission list when granting permissions', async () => { + it('does nothing when the app has no grantable dangerous permissions', async () => { const { grantPermissions } = await import('../adb.js'); - const spawnSpy = vi.spyOn(tools, 'spawn'); + const spawnSpy = vi + .spyOn(tools, 'spawn') + .mockResolvedValueOnce({ + stdout: 'package:com.example.app\n', + } as Awaited>) + .mockResolvedValueOnce({ + stdout: `requested permissions:\n android.permission.INTERNET\ninstall permissions:\n`, + } as Awaited>) + .mockResolvedValueOnce({ + stdout: `Dangerous Permissions:\n permission:android.permission.CAMERA\n`, + } as Awaited>); - await grantPermissions('emulator-5554', 'com.example.app', []); + await grantPermissions('emulator-5554', 'com.example.app'); - expect(spawnSpy).not.toHaveBeenCalled(); + expect(spawnSpy).toHaveBeenCalledTimes(3); }); }); diff --git a/packages/platform-android/src/adb.ts b/packages/platform-android/src/adb.ts index 44d5c96..cc04d54 100644 --- a/packages/platform-android/src/adb.ts +++ b/packages/platform-android/src/adb.ts @@ -378,6 +378,79 @@ export const getDeviceInfo = async ( return { manufacturer, model }; }; +const getRequestedPermissions = async ( + adbId: string, + bundleId: string, +): Promise => { + const { stdout } = await spawn(getAdbBinaryPath(), [ + '-s', + adbId, + 'shell', + 'dumpsys', + 'package', + bundleId, + ]); + + const requestedPermissions = new Set(); + const lines = stdout.split('\n'); + let inRequestedPermissionsSection = false; + + for (const line of lines) { + const trimmedLine = line.trim(); + + if (trimmedLine === 'requested permissions:') { + inRequestedPermissionsSection = true; + continue; + } + + if (!inRequestedPermissionsSection) { + continue; + } + + if (trimmedLine === '') { + continue; + } + + if (trimmedLine.endsWith(':')) { + break; + } + + if (/^[a-zA-Z0-9_.]+$/.test(trimmedLine)) { + requestedPermissions.add(trimmedLine); + continue; + } + + break; + } + + return [...requestedPermissions]; +}; + +const getDangerousPermissions = async (adbId: string): Promise> => { + const { stdout } = await spawn(getAdbBinaryPath(), [ + '-s', + adbId, + 'shell', + 'pm', + 'list', + 'permissions', + '-g', + '-d', + ]); + + const dangerousPermissions = new Set(); + + for (const match of stdout.matchAll(/permission:([a-zA-Z0-9_.]+)/g)) { + const permission = match[1]?.trim(); + + if (permission) { + dangerousPermissions.add(permission); + } + } + + return dangerousPermissions; +}; + export const isBootCompleted = async (adbId: string): Promise => { try { const bootCompleted = await getShellProperty(adbId, 'sys.boot_completed'); @@ -738,17 +811,24 @@ export const getConnectedDevices = async (): Promise => { export const grantPermissions = async ( adbId: string, bundleId: string, - permissions: string[], ): Promise => { - if (permissions.length === 0) { - return; - } - const isInstalled = await isAppInstalled(adbId, bundleId); if (!isInstalled) { throw new AdbAppNotInstalledError(bundleId, adbId); } + const [requestedPermissions, dangerousPermissions] = await Promise.all([ + getRequestedPermissions(adbId, bundleId), + getDangerousPermissions(adbId), + ]); + const permissions = requestedPermissions.filter((permission) => + dangerousPermissions.has(permission), + ); + + if (permissions.length === 0) { + return; + } + const grantCommands = permissions.map((permission) => [ '-s', adbId, From 4e732ae454b9bb3deca6480e3017fbc7fe70f4ac Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Wed, 29 Apr 2026 09:12:48 +0200 Subject: [PATCH 20/33] fix: satisfy xctest agent test lint rule --- .../src/__tests__/xctest-agent.test.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/platform-ios/src/__tests__/xctest-agent.test.ts b/packages/platform-ios/src/__tests__/xctest-agent.test.ts index 2cb6e48..9b9cc0d 100644 --- a/packages/platform-ios/src/__tests__/xctest-agent.test.ts +++ b/packages/platform-ios/src/__tests__/xctest-agent.test.ts @@ -105,10 +105,16 @@ const createLongRunningSubprocess = (options?: { const iterable = { nodeChildProcess: Promise.resolve(childProcess), - async *[Symbol.asyncIterator]() { - while (!stopped) { - await new Promise((resolve) => setTimeout(resolve, 0)); - } + [Symbol.asyncIterator]() { + return { + next: async () => { + while (!stopped) { + await new Promise((resolve) => setTimeout(resolve, 0)); + } + + return { done: true, value: undefined }; + }, + }; }, }; From 24a77609e39b186b5b5842bc8bdee90c7584601c Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Wed, 29 Apr 2026 09:13:15 +0200 Subject: [PATCH 21/33] fix: clear playground iOS development team --- .../ios/HarnessPlayground.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/playground/ios/HarnessPlayground.xcodeproj/project.pbxproj b/apps/playground/ios/HarnessPlayground.xcodeproj/project.pbxproj index 79788dd..39a5b1c 100644 --- a/apps/playground/ios/HarnessPlayground.xcodeproj/project.pbxproj +++ b/apps/playground/ios/HarnessPlayground.xcodeproj/project.pbxproj @@ -268,7 +268,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = BAJL5U28HC; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; INFOPLIST_FILE = HarnessPlayground/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.1; @@ -297,7 +297,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = BAJL5U28HC; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = HarnessPlayground/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.1; LD_RUNPATH_SEARCH_PATHS = ( From bd7e0fc243168b062d72d474f42bd2a82fb6bab4 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Wed, 29 Apr 2026 09:13:52 +0200 Subject: [PATCH 22/33] feat(platform-android): grant declared app permissions --- .../android/app/src/main/AndroidManifest.xml | 1 + .../src/__tests__/ui/permissions.harness.tsx | 11 +++----- .../src/__tests__/instance.test.ts | 10 ------- packages/platform-android/src/adb-id.ts | 4 +++ packages/platform-android/src/instance.ts | 7 ++--- packages/platform-android/src/permissions.ts | 27 ------------------- 6 files changed, 10 insertions(+), 50 deletions(-) delete mode 100644 packages/platform-android/src/permissions.ts diff --git a/apps/playground/android/app/src/main/AndroidManifest.xml b/apps/playground/android/app/src/main/AndroidManifest.xml index fb78f39..a971b7c 100644 --- a/apps/playground/android/app/src/main/AndroidManifest.xml +++ b/apps/playground/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,7 @@ + { - test('should allow iOS camera permissions through the system prompt', async () => { - if (Platform.OS !== 'ios') { - return; - } - - const { VisionCamera } = - require('react-native-vision-camera') as typeof import('react-native-vision-camera'); + test('should allow camera permissions when requested', async () => { const initialStatus = VisionCamera.cameraPermissionStatus; let latestStatus = initialStatus; diff --git a/packages/platform-android/src/__tests__/instance.test.ts b/packages/platform-android/src/__tests__/instance.test.ts index 7c82dcd..f6ef952 100644 --- a/packages/platform-android/src/__tests__/instance.test.ts +++ b/packages/platform-android/src/__tests__/instance.test.ts @@ -655,11 +655,6 @@ describe('Android platform instance', () => { expect(grantPermissions).toHaveBeenCalledWith( 'emulator-5554', 'com.harnessplayground', - expect.arrayContaining([ - 'android.permission.CAMERA', - 'android.permission.RECORD_AUDIO', - 'android.permission.ACCESS_FINE_LOCATION', - ]), ); }); @@ -744,11 +739,6 @@ describe('Android platform instance', () => { expect(grantPermissions).toHaveBeenCalledWith( '012345', 'com.harnessplayground', - expect.arrayContaining([ - 'android.permission.CAMERA', - 'android.permission.RECORD_AUDIO', - 'android.permission.ACCESS_FINE_LOCATION', - ]), ); }); }); diff --git a/packages/platform-android/src/adb-id.ts b/packages/platform-android/src/adb-id.ts index a24b938..2d714d0 100644 --- a/packages/platform-android/src/adb-id.ts +++ b/packages/platform-android/src/adb-id.ts @@ -16,6 +16,10 @@ export const getAdbId = async ( for (const adbId of adbIds) { if (isAndroidDeviceEmulator(device)) { + if (!isAdbIdEmulator(adbId)) { + continue; + } + const emulatorName = await adb.getEmulatorName(adbId); if (emulatorName === device.name) { diff --git a/packages/platform-android/src/instance.ts b/packages/platform-android/src/instance.ts index 2f77ae9..977d84a 100644 --- a/packages/platform-android/src/instance.ts +++ b/packages/platform-android/src/instance.ts @@ -34,7 +34,6 @@ import { import { isInteractive } from '@react-native-harness/tools'; import fs from 'node:fs'; import type { AppMonitor } from '@react-native-harness/platforms'; -import { getDefaultAndroidPermissions } from './permissions.js'; const androidInstanceLogger = logger.child('android-instance'); @@ -261,8 +260,7 @@ export const getAndroidEmulatorPlatformInstance = async ( const appUid = await configureAndroidRuntime(adbId, config, harnessConfig); if (permissionsEnabled) { - const defaultPermissions = getDefaultAndroidPermissions(); - await adb.grantPermissions(adbId, config.bundleId, defaultPermissions); + await adb.grantPermissions(adbId, config.bundleId); } return { @@ -342,8 +340,7 @@ export const getAndroidPhysicalDevicePlatformInstance = async ( const appUid = await configureAndroidRuntime(adbId, config, harnessConfig); if (permissionsEnabled) { - const defaultPermissions = getDefaultAndroidPermissions(); - await adb.grantPermissions(adbId, config.bundleId, defaultPermissions); + await adb.grantPermissions(adbId, config.bundleId); } return { diff --git a/packages/platform-android/src/permissions.ts b/packages/platform-android/src/permissions.ts deleted file mode 100644 index 07bba47..0000000 --- a/packages/platform-android/src/permissions.ts +++ /dev/null @@ -1,27 +0,0 @@ -const DEFAULT_PERMISSIONS = [ - 'android.permission.ACCESS_FINE_LOCATION', - 'android.permission.ACCESS_COARSE_LOCATION', - 'android.permission.CAMERA', - 'android.permission.RECORD_AUDIO', - 'android.permission.READ_CALENDAR', - 'android.permission.WRITE_CALENDAR', - 'android.permission.READ_CONTACTS', - 'android.permission.WRITE_CONTACTS', - 'android.permission.READ_CALL_LOG', - 'android.permission.WRITE_CALL_LOG', - 'android.permission.READ_EXTERNAL_STORAGE', - 'android.permission.WRITE_EXTERNAL_STORAGE', - 'android.permission.READ_PHONE_STATE', - 'android.permission.CALL_PHONE', - 'android.permission.READ_SMS', - 'android.permission.SEND_SMS', - 'android.permission.RECEIVE_SMS', - 'android.permission.READ_CELL_BROADCASTS', - 'android.permission.BODY_SENSORS', - 'android.permission.INTERNET', - 'android.permission.ACCESS_NETWORK_STATE', -]; - -export const getDefaultAndroidPermissions = (): string[] => { - return DEFAULT_PERMISSIONS; -}; From 8380f1db7dbf2beaad653a140e7ab95940a01c1e Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Wed, 29 Apr 2026 09:25:17 +0200 Subject: [PATCH 23/33] fix: skip camera permission test on web --- apps/playground/src/__tests__/ui/permissions.harness.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/playground/src/__tests__/ui/permissions.harness.tsx b/apps/playground/src/__tests__/ui/permissions.harness.tsx index 4fcf77b..4388399 100644 --- a/apps/playground/src/__tests__/ui/permissions.harness.tsx +++ b/apps/playground/src/__tests__/ui/permissions.harness.tsx @@ -7,11 +7,15 @@ import { waitUntil, } from 'react-native-harness'; import { screen, userEvent } from '@react-native-harness/ui'; -import { Pressable, Text, View } from 'react-native'; +import { Platform, Pressable, Text, View } from 'react-native'; import { VisionCamera} from 'react-native-vision-camera'; describe('Permissions', () => { test('should allow camera permissions when requested', async () => { + if (Platform.OS === 'web') { + return; + } + const initialStatus = VisionCamera.cameraPermissionStatus; let latestStatus = initialStatus; From 7caa577c4743a24af900ee1465e095c327b1e6b9 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Wed, 29 Apr 2026 09:54:20 +0200 Subject: [PATCH 24/33] chore(platform-android): add debug logs for permission grants --- packages/platform-android/src/adb.ts | 37 +++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/packages/platform-android/src/adb.ts b/packages/platform-android/src/adb.ts index cc04d54..6ec5bd9 100644 --- a/packages/platform-android/src/adb.ts +++ b/packages/platform-android/src/adb.ts @@ -1,5 +1,5 @@ import { type AndroidAppLaunchOptions } from '@react-native-harness/platforms'; -import { spawn, SubprocessError } from '@react-native-harness/tools'; +import { logger, spawn, SubprocessError } from '@react-native-harness/tools'; import { spawn as nodeSpawn } from 'node:child_process'; import type { ChildProcessByStdio } from 'node:child_process'; import { access, rm } from 'node:fs/promises'; @@ -63,6 +63,7 @@ const waitWithSignal = async ( const EMULATOR_STARTUP_OBSERVATION_TIMEOUT_MS = 5000; const EMULATOR_OUTPUT_BUFFER_LIMIT = 16 * 1024; +const androidAdbLogger = logger.child('android-adb'); export const emulatorProcess = { startDetachedProcess: ( @@ -812,6 +813,11 @@ export const grantPermissions = async ( adbId: string, bundleId: string, ): Promise => { + androidAdbLogger.debug('grantPermissions:start %o', { + adbId, + bundleId, + }); + const isInstalled = await isAppInstalled(adbId, bundleId); if (!isInstalled) { throw new AdbAppNotInstalledError(bundleId, adbId); @@ -825,7 +831,18 @@ export const grantPermissions = async ( dangerousPermissions.has(permission), ); + androidAdbLogger.debug('grantPermissions:resolved %o', { + adbId, + bundleId, + requestedPermissions, + permissions, + }); + if (permissions.length === 0) { + androidAdbLogger.debug('grantPermissions:skip %o', { + adbId, + bundleId, + }); return; } @@ -840,10 +857,28 @@ export const grantPermissions = async ( ]); try { + androidAdbLogger.debug('grantPermissions:commands %o', { + adbId, + bundleId, + grantCommands, + }); + await Promise.all( grantCommands.map((args) => spawn(getAdbBinaryPath(), args as string[])), ); + + androidAdbLogger.debug('grantPermissions:success %o', { + adbId, + bundleId, + permissions, + }); } catch (error) { + androidAdbLogger.debug('grantPermissions:error %o', { + adbId, + bundleId, + permissions, + error, + }); throw new AdbPermissionGrantError(bundleId, permissions, adbId); } }; From 2ad3539990a769478fe83ec8818f83667836e81e Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Wed, 29 Apr 2026 09:55:03 +0200 Subject: [PATCH 25/33] chore(playground): update xcodeproj --- .../ios/HarnessPlayground.xcodeproj/project.pbxproj | 8 -------- 1 file changed, 8 deletions(-) diff --git a/apps/playground/ios/HarnessPlayground.xcodeproj/project.pbxproj b/apps/playground/ios/HarnessPlayground.xcodeproj/project.pbxproj index 39a5b1c..331591b 100644 --- a/apps/playground/ios/HarnessPlayground.xcodeproj/project.pbxproj +++ b/apps/playground/ios/HarnessPlayground.xcodeproj/project.pbxproj @@ -191,14 +191,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-HarnessPlayground/Pods-HarnessPlayground-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-HarnessPlayground/Pods-HarnessPlayground-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-HarnessPlayground/Pods-HarnessPlayground-frameworks.sh\"\n"; @@ -234,14 +230,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-HarnessPlayground/Pods-HarnessPlayground-resources-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-HarnessPlayground/Pods-HarnessPlayground-resources-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-HarnessPlayground/Pods-HarnessPlayground-resources.sh\"\n"; From 4d393e845998ebca17ad9d159705cf692140f8bb Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Wed, 29 Apr 2026 10:25:59 +0200 Subject: [PATCH 26/33] ci: use xcode 26 for e2e jobs --- .github/workflows/e2e-tests.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index c1dafb4..3f14cd0 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -130,6 +130,11 @@ jobs: node-version: '24.10.0' cache: 'pnpm' + - name: Setup Xcode 26 + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: '26.0' + - name: Install Watchman run: brew install watchman @@ -370,6 +375,11 @@ jobs: node-version: '24.10.0' cache: 'pnpm' + - name: Setup Xcode 26 + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: '26.0' + - name: Install Watchman run: brew install watchman From e2b2c87e1a9ab4a39cd052fde591c950e7e1ec1c Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Wed, 29 Apr 2026 10:27:15 +0200 Subject: [PATCH 27/33] fix(platform-android): keep emulator camera enabled --- .../platform-android/src/__tests__/emulator-startup.test.ts | 3 +++ packages/platform-android/src/emulator-startup.ts | 2 -- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/platform-android/src/__tests__/emulator-startup.test.ts b/packages/platform-android/src/__tests__/emulator-startup.test.ts index 790ccca..a2a9bcb 100644 --- a/packages/platform-android/src/__tests__/emulator-startup.test.ts +++ b/packages/platform-android/src/__tests__/emulator-startup.test.ts @@ -10,6 +10,9 @@ describe('emulator startup modes', () => { '-no-snapshot-save', ]) ); + expect(getEmulatorStartupArgs('Pixel_8_API_35', 'default-boot')).not.toEqual( + expect.arrayContaining(['-camera-back', 'none']) + ); }); it('builds clean snapshot generation args', () => { diff --git a/packages/platform-android/src/emulator-startup.ts b/packages/platform-android/src/emulator-startup.ts index d18e5e0..2611084 100644 --- a/packages/platform-android/src/emulator-startup.ts +++ b/packages/platform-android/src/emulator-startup.ts @@ -9,8 +9,6 @@ const COMMON_EMULATOR_ARGS = [ 'swiftshader_indirect', '-noaudio', '-no-boot-anim', - '-camera-back', - 'none', ] as const; export const getEmulatorStartupArgs = ( From a38828eb882e83dc822880e36559228506bd6b05 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Thu, 30 Apr 2026 07:41:40 +0200 Subject: [PATCH 28/33] fix(platform-android): reinstall apps on cached emulators --- .../src/__tests__/adb.test.ts | 16 ++++++ .../src/__tests__/instance.test.ts | 54 +++++++++++++++++++ packages/platform-android/src/adb.ts | 7 +++ packages/platform-android/src/instance.ts | 22 +++++++- 4 files changed, 97 insertions(+), 2 deletions(-) diff --git a/packages/platform-android/src/__tests__/adb.test.ts b/packages/platform-android/src/__tests__/adb.test.ts index df03c04..ffc2319 100644 --- a/packages/platform-android/src/__tests__/adb.test.ts +++ b/packages/platform-android/src/__tests__/adb.test.ts @@ -12,6 +12,7 @@ import { hasAvd, installApp, startEmulator, + uninstallApp, waitForBoot, waitForEmulatorDisconnect, } from '../adb.js'; @@ -146,6 +147,21 @@ describe('getStartAppArgs', () => { ]); }); + it('uninstalls the app via adb', async () => { + const spawnSpy = vi + .spyOn(tools, 'spawn') + .mockResolvedValueOnce({} as Awaited>); + + await uninstallApp('emulator-5554', 'com.example.app'); + + expect(spawnSpy).toHaveBeenCalledWith(expect.stringMatching(/adb$/), [ + '-s', + 'emulator-5554', + 'uninstall', + 'com.example.app', + ]); + }); + it('creates an AVD and appends config overrides', async () => { vi.spyOn(avdConfig, 'readAvdConfig').mockResolvedValue({}); const spawnSpy = vi diff --git a/packages/platform-android/src/__tests__/instance.test.ts b/packages/platform-android/src/__tests__/instance.test.ts index f6ef952..6c719b8 100644 --- a/packages/platform-android/src/__tests__/instance.test.ts +++ b/packages/platform-android/src/__tests__/instance.test.ts @@ -429,6 +429,60 @@ describe('Android platform instance', () => { fs.rmSync(appPath, { force: true }); }); + it('reinstalls the app from HARNESS_APP_PATH when already installed', async () => { + const appPath = path.join(os.tmpdir(), 'HarnessPlayground-installed.apk'); + fs.writeFileSync(appPath, 'apk'); + vi.stubEnv('HARNESS_APP_PATH', appPath); + vi.spyOn( + await import('../environment.js'), + 'ensureAndroidEmulatorEnvironment', + ).mockResolvedValue('/tmp/android-sdk'); + vi.spyOn(adb, 'getDeviceIds').mockResolvedValue(['emulator-5554']); + vi.spyOn(adb, 'getEmulatorName').mockResolvedValue('Pixel_8_API_35'); + vi.spyOn(adb, 'waitForBoot').mockResolvedValue('emulator-5554'); + vi.spyOn(adb, 'isAppInstalled').mockResolvedValue(true); + const uninstallApp = vi + .spyOn(adb, 'uninstallApp') + .mockResolvedValue(undefined); + const installApp = vi.spyOn(adb, 'installApp').mockResolvedValue(undefined); + vi.spyOn(adb, 'reversePort').mockResolvedValue(undefined); + vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined); + vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234); + vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue( + undefined, + ); + + await expect( + getAndroidEmulatorPlatformInstance( + { + name: 'android', + device: { + type: 'emulator', + name: 'Pixel_8_API_35', + avd: { + apiLevel: 35, + profile: 'pixel_8', + diskSize: '1G', + heapSize: '1G', + }, + }, + bundleId: 'com.harnessplayground', + activityName: '.MainActivity', + }, + harnessConfig, + init, + ), + ).resolves.toBeDefined(); + + expect(uninstallApp).toHaveBeenCalledWith( + 'emulator-5554', + 'com.harnessplayground', + ); + expect(installApp).toHaveBeenCalledWith('emulator-5554', appPath); + + fs.rmSync(appPath, { force: true }); + }); + it('throws a HarnessAppPathError when HARNESS_APP_PATH is missing', async () => { vi.spyOn(adb, 'getDeviceIds').mockResolvedValue(['emulator-5554']); vi.spyOn(adb, 'getEmulatorName').mockResolvedValue('Pixel_8_API_35'); diff --git a/packages/platform-android/src/adb.ts b/packages/platform-android/src/adb.ts index 6ec5bd9..6ee5349 100644 --- a/packages/platform-android/src/adb.ts +++ b/packages/platform-android/src/adb.ts @@ -476,6 +476,13 @@ export const installApp = async ( await spawn(getAdbBinaryPath(), ['-s', adbId, 'install', '-r', appPath]); }; +export const uninstallApp = async ( + adbId: string, + bundleId: string, +): Promise => { + await spawn(getAdbBinaryPath(), ['-s', adbId, 'uninstall', bundleId]); +}; + export const hasAvd = async (name: string): Promise => { const avds = await getAvds(); return avds.includes(name); diff --git a/packages/platform-android/src/instance.ts b/packages/platform-android/src/instance.ts index 977d84a..d28eb17 100644 --- a/packages/platform-android/src/instance.ts +++ b/packages/platform-android/src/instance.ts @@ -59,6 +59,20 @@ const getHarnessAppPath = (): string => { return appPath; }; +const getOptionalHarnessAppPath = (): string | undefined => { + const appPath = process.env.HARNESS_APP_PATH; + + if (!appPath) { + return undefined; + } + + if (!fs.existsSync(appPath)) { + throw new HarnessAppPathError('invalid', appPath); + } + + return appPath; +}; + const configureAndroidRuntime = async ( adbId: string, config: AndroidPlatformConfig, @@ -251,10 +265,14 @@ export const getAndroidEmulatorPlatformInstance = async ( ); const isInstalled = await adb.isAppInstalled(adbId, config.bundleId); + const appPath = getOptionalHarnessAppPath(); - if (!isInstalled) { - const appPath = getHarnessAppPath(); + if (isInstalled && appPath) { + await adb.uninstallApp(adbId, config.bundleId); await adb.installApp(adbId, appPath); + } else if (!isInstalled) { + const installPath = appPath ?? getHarnessAppPath(); + await adb.installApp(adbId, installPath); } const appUid = await configureAndroidRuntime(adbId, config, harnessConfig); From 4f92d79f62f33dd8c8ac00fac10b00a31de06a3d Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Thu, 30 Apr 2026 07:53:12 +0200 Subject: [PATCH 29/33] fix(platform-ios): persist xctest startup logs --- .github/workflows/e2e-tests.yml | 40 +++++++ .../src/__tests__/xctest-agent.test.ts | 23 +++- packages/platform-ios/src/xctest-agent.ts | 104 ++++++++++++++---- .../src/__tests__/harness-artifacts.test.ts | 37 +++++++ packages/tools/src/harness-artifacts.ts | 49 +++++++++ packages/tools/src/index.ts | 1 + 6 files changed, 232 insertions(+), 22 deletions(-) create mode 100644 packages/tools/src/__tests__/harness-artifacts.test.ts create mode 100644 packages/tools/src/harness-artifacts.ts diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 3f14cd0..a9aa0af 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -104,6 +104,14 @@ jobs: echo "HARNESS_RUNNER=$HARNESS_RUNNER" echo "HARNESS_EXIT_CODE=$HARNESS_EXIT_CODE" + - name: Upload Harness logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: harness-logs-e2e-android + path: apps/playground/.harness/logs + if-no-files-found: ignore + e2e-ios: name: E2E iOS runs-on: macos-latest @@ -197,6 +205,14 @@ jobs: echo "HARNESS_RUNNER=$HARNESS_RUNNER" echo "HARNESS_EXIT_CODE=$HARNESS_EXIT_CODE" + - name: Upload Harness logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: harness-logs-e2e-ios + path: apps/playground/.harness/logs + if-no-files-found: ignore + e2e-web: name: E2E Web runs-on: ubuntu-22.04 @@ -243,6 +259,14 @@ jobs: echo "HARNESS_RUNNER=$HARNESS_RUNNER" echo "HARNESS_EXIT_CODE=$HARNESS_EXIT_CODE" + - name: Upload Harness logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: harness-logs-e2e-web + path: apps/playground/.harness/logs + if-no-files-found: ignore + crash-validate-android: name: Crash Validation Android runs-on: ubuntu-22.04 @@ -349,6 +373,14 @@ jobs: exit 1 fi + - name: Upload Harness logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: harness-logs-crash-validate-android + path: apps/playground/.harness/logs + if-no-files-found: ignore + crash-validate-ios: name: Crash Validation iOS runs-on: macos-latest @@ -465,3 +497,11 @@ jobs: echo "ERROR: No crash report artifacts found in $CRASH_DIR" exit 1 fi + + - name: Upload Harness logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: harness-logs-crash-validate-ios + path: apps/playground/.harness/logs + if-no-files-found: ignore diff --git a/packages/platform-ios/src/__tests__/xctest-agent.test.ts b/packages/platform-ios/src/__tests__/xctest-agent.test.ts index 9b9cc0d..8d82358 100644 --- a/packages/platform-ios/src/__tests__/xctest-agent.test.ts +++ b/packages/platform-ios/src/__tests__/xctest-agent.test.ts @@ -3,6 +3,7 @@ import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { createHash } from 'node:crypto'; +import { PassThrough } from 'node:stream'; import { fileURLToPath } from 'node:url'; const mocks = vi.hoisted(() => ({ @@ -101,6 +102,8 @@ const createLongRunningSubprocess = (options?: { return childProcess; }), signalCode: null, + stderr: new PassThrough(), + stdout: new PassThrough(), }; const iterable = { @@ -184,6 +187,9 @@ describe('xctest-agent orchestration', () => { path.join(buildRoot, 'device', 'build-manifest.json'), JSON.stringify({ buildInputsHash: getCurrentInputsHash(), + codeSign: { + teamId: 'TESTTEAM01', + }, destinationKind: 'device', }), ); @@ -237,8 +243,8 @@ describe('xctest-agent orchestration', () => { ]), expect.objectContaining({ env: expect.objectContaining({ - HARNESS_XCTEST_AGENT_MODE: 'test', - HARNESS_XCTEST_AGENT_PORT: '49152', + TEST_RUNNER_HARNESS_XCTEST_AGENT_MODE: 'test', + TEST_RUNNER_HARNESS_XCTEST_AGENT_PORT: '49152', }), }), ); @@ -249,6 +255,19 @@ describe('xctest-agent orchestration', () => { expect(mocks.configurePermissions).toHaveBeenCalledWith({ autoAcceptPermissions: true, }); + const logDirectories = fs.readdirSync(path.join(tempProjectRoot, '.harness', 'logs')); + expect(logDirectories).toHaveLength(1); + const xcodebuildLogPath = path.join( + tempProjectRoot, + '.harness', + 'logs', + logDirectories[0]!, + 'xcodebuild.log' + ); + expect(fs.existsSync(xcodebuildLogPath)).toBe(true); + expect(fs.readFileSync(xcodebuildLogPath, 'utf8')).toContain( + 'command=xcodebuild test-without-building' + ); await controller.dispose(); diff --git a/packages/platform-ios/src/xctest-agent.ts b/packages/platform-ios/src/xctest-agent.ts index 89aa21c..4b769ae 100644 --- a/packages/platform-ios/src/xctest-agent.ts +++ b/packages/platform-ios/src/xctest-agent.ts @@ -1,4 +1,5 @@ import { + createHarnessArtifactDirectory, getAvailablePort, logger, spawn, @@ -6,6 +7,8 @@ import { } from '@react-native-harness/tools'; import fs from 'node:fs'; import { createHash } from 'node:crypto'; +import { PassThrough, pipeline } from 'node:stream'; +import { promisify } from 'node:util'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { @@ -24,11 +27,12 @@ const XCTEST_AGENT_SCHEME_NAME = 'HarnessXCTestAgent'; const XCTEST_AGENT_PORT_ENV = 'HARNESS_XCTEST_AGENT_PORT'; const XCTEST_AGENT_TARGET_BUNDLE_ID_ENV = 'HARNESS_XCTEST_AGENT_TARGET_BUNDLE_ID'; -const XCTEST_AGENT_STARTUP_TIMEOUT_MS = 30_000; +const XCTEST_AGENT_STARTUP_TIMEOUT_MS = 60_000; const XCTEST_AGENT_SHUTDOWN_TIMEOUT_MS = 5_000; const XCTEST_AGENT_STARTUP_POLL_INTERVAL_MS = 250; const HARNESS_DIRNAME = '.harness'; const XCTEST_AGENT_BUILD_DIRNAME = 'xctest-agent'; +const pipelineAsync = promisify(pipeline); type XCTestAgentTarget = | { @@ -376,6 +380,53 @@ const getErrorMessage = (error: unknown): string => { return error instanceof Error ? error.message : String(error); }; +const attachProcessOutputLog = async (options: { + command: string; + logFilePath: string; + process: Subprocess; +}) => { + fs.mkdirSync(path.dirname(options.logFilePath), { recursive: true }); + fs.writeFileSync( + options.logFilePath, + [ + `timestamp=${new Date().toISOString()}`, + `command=${options.command}`, + '', + ].join('\n'), + 'utf8' + ); + const output = fs.createWriteStream(options.logFilePath, { flags: 'a' }); + + const childProcess = await options.process.nodeChildProcess; + const mergedOutput = new PassThrough(); + const forwardStream = async ( + stream: NodeJS.ReadableStream | null | undefined, + label: 'stdout' | 'stderr' + ) => { + if (!stream) { + return; + } + + for await (const chunk of stream) { + mergedOutput.write(`[${label}] `); + mergedOutput.write(chunk); + if (Buffer.isBuffer(chunk) ? !chunk.includes(0x0a) : !String(chunk).endsWith('\n')) { + mergedOutput.write('\n'); + } + } + }; + + const pipeTask = pipelineAsync(mergedOutput, output); + const forwardTask = Promise.all([ + forwardStream(childProcess.stdout, 'stdout'), + forwardStream(childProcess.stderr, 'stderr'), + ]).finally(() => { + mergedOutput.end(); + }); + + void Promise.allSettled([pipeTask, forwardTask]); +}; + export const createXCTestAgentController = (options: { appBundleId?: string; target: XCTestAgentTarget; @@ -390,6 +441,13 @@ export const createXCTestAgentController = (options: { options.startupTimeoutMs ?? XCTEST_AGENT_STARTUP_TIMEOUT_MS; const shutdownTimeoutMs = options.shutdownTimeoutMs ?? XCTEST_AGENT_SHUTDOWN_TIMEOUT_MS; + const logArtifacts = createHarnessArtifactDirectory({ + artifactType: 'logs', + bundleId: options.appBundleId, + platformId: 'ios', + runnerName: `xctest-agent-${target.kind}`, + }); + const xcodebuildLogPath = path.join(logArtifacts.directoryPath, 'xcodebuild.log'); let prepared = false; let agentProcess: Subprocess | null = null; let agentClient: ReturnType | null = null; @@ -493,23 +551,24 @@ export const createXCTestAgentController = (options: { target.kind ); xctestAgentLogger.debug('Using XCTest agent port %d', port); + const xcodebuildArgs = [ + 'test-without-building', + '-project', + getXCTestAgentProjectFilePath(), + '-scheme', + XCTEST_AGENT_SCHEME_NAME, + '-destination', + getXCTestAgentRunDestination(target), + '-parallel-testing-enabled', + 'NO', + '-maximum-parallel-testing-workers', + '1', + '-derivedDataPath', + getXCTestAgentDerivedDataPath(target), + ]; agentProcess = spawn( 'xcodebuild', - [ - 'test-without-building', - '-project', - getXCTestAgentProjectFilePath(), - '-scheme', - XCTEST_AGENT_SCHEME_NAME, - '-destination', - getXCTestAgentRunDestination(target), - '-parallel-testing-enabled', - 'NO', - '-maximum-parallel-testing-workers', - '1', - '-derivedDataPath', - getXCTestAgentDerivedDataPath(target), - ], + xcodebuildArgs, { cwd: getXCTestAgentProjectRoot(), env: { @@ -519,10 +578,14 @@ export const createXCTestAgentController = (options: { ...getLaunchEnvironment(), }), }, - stdout: 'ignore', - stderr: 'ignore', } ); + void attachProcessOutputLog({ + command: ['xcodebuild', ...xcodebuildArgs].join(' '), + logFilePath: xcodebuildLogPath, + process: agentProcess, + }); + xctestAgentLogger.info('Saving XCTest agent xcodebuild logs to %s', xcodebuildLogPath); const currentProcess = agentProcess; if (typeof currentProcess.catch === 'function') { @@ -550,9 +613,10 @@ export const createXCTestAgentController = (options: { await client.configurePermissions(runtimeConfiguration.permissions); } catch (error) { xctestAgentLogger.warn( - 'XCTest agent startup failed for %s: %s', + 'XCTest agent startup failed for %s: %s (logs: %s)', target.kind, - getErrorMessage(error) + getErrorMessage(error), + xcodebuildLogPath ); await transport.dispose(); agentClient = null; diff --git a/packages/tools/src/__tests__/harness-artifacts.test.ts b/packages/tools/src/__tests__/harness-artifacts.test.ts new file mode 100644 index 0000000..a75af89 --- /dev/null +++ b/packages/tools/src/__tests__/harness-artifacts.test.ts @@ -0,0 +1,37 @@ +import { afterEach, describe, expect, it } from 'vitest'; +import fs from 'node:fs'; +import path from 'node:path'; +import { tmpdir } from 'node:os'; +import { createHarnessArtifactDirectory } from '../harness-artifacts.js'; + +describe('createHarnessArtifactDirectory', () => { + const rootDir = fs.mkdtempSync( + path.join(tmpdir(), 'rn-harness-artifact-directories-') + ); + + afterEach(() => { + fs.rmSync(rootDir, { recursive: true, force: true }); + fs.mkdirSync(rootDir, { recursive: true }); + }); + + it('creates a reusable run directory inside the requested artifact type', () => { + const artifacts = createHarnessArtifactDirectory({ + artifactType: 'logs', + bundleId: 'com.harnessplayground.dev', + platformId: 'ios', + rootDir, + runTimestamp: '2026-04-29T10-45-31-645Z', + runnerName: 'xctest-agent simulator', + }); + + expect(artifacts.rootDir).toBe(path.join(rootDir, 'logs')); + expect(artifacts.directoryPath).toBe( + path.join( + rootDir, + 'logs', + '2026-04-29T10-45-31-645Z--ios--xctest-agent-simulator--com.harnessplayground.dev' + ) + ); + expect(fs.existsSync(artifacts.directoryPath)).toBe(true); + }); +}); diff --git a/packages/tools/src/harness-artifacts.ts b/packages/tools/src/harness-artifacts.ts new file mode 100644 index 0000000..6a9fbc9 --- /dev/null +++ b/packages/tools/src/harness-artifacts.ts @@ -0,0 +1,49 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +const getDefaultHarnessRoot = () => path.join(process.cwd(), '.harness'); + +const sanitizePathSegment = (value: string) => + value + .replace(/[^a-zA-Z0-9._-]+/g, '-') + .replace(/-+/g, '-') + .replace(/^-|-$/g, '') || 'artifact'; + +const formatRunTimestamp = (value: Date) => + value.toISOString().replace(/[:.]/g, '-'); + +export const createHarnessArtifactDirectory = ({ + artifactType, + bundleId, + platformId, + rootDir = getDefaultHarnessRoot(), + runTimestamp = formatRunTimestamp(new Date()), + runnerName, +}: { + artifactType: string; + bundleId?: string; + platformId: string; + rootDir?: string; + runTimestamp?: string; + runnerName: string; +}) => { + const artifactRoot = path.join(rootDir, sanitizePathSegment(artifactType)); + const runDirName = [ + runTimestamp, + platformId, + runnerName, + bundleId, + ] + .filter(Boolean) + .map((value) => sanitizePathSegment(value)) + .join('--'); + const directoryPath = path.join(artifactRoot, runDirName); + + fs.mkdirSync(directoryPath, { recursive: true }); + + return { + directoryPath, + rootDir: artifactRoot, + runTimestamp, + }; +}; diff --git a/packages/tools/src/index.ts b/packages/tools/src/index.ts index 48b56eb..a8fc950 100644 --- a/packages/tools/src/index.ts +++ b/packages/tools/src/index.ts @@ -9,5 +9,6 @@ export * from './error.js'; export * from './events.js'; export * from './packages.js'; export * from './crash-artifacts.js'; +export * from './harness-artifacts.js'; export * from './regex.js'; export * from './isInteractive.js'; From 415de1de9ed726aefde35c7cb57b8672656b77d9 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Thu, 30 Apr 2026 07:55:45 +0200 Subject: [PATCH 30/33] fix(tools): narrow harness artifact path segments --- packages/tools/src/harness-artifacts.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/tools/src/harness-artifacts.ts b/packages/tools/src/harness-artifacts.ts index 6a9fbc9..cebeffb 100644 --- a/packages/tools/src/harness-artifacts.ts +++ b/packages/tools/src/harness-artifacts.ts @@ -12,6 +12,9 @@ const sanitizePathSegment = (value: string) => const formatRunTimestamp = (value: Date) => value.toISOString().replace(/[:.]/g, '-'); +const isDefined = (value: string | undefined): value is string => + value !== undefined; + export const createHarnessArtifactDirectory = ({ artifactType, bundleId, @@ -34,7 +37,7 @@ export const createHarnessArtifactDirectory = ({ runnerName, bundleId, ] - .filter(Boolean) + .filter(isDefined) .map((value) => sanitizePathSegment(value)) .join('--'); const directoryPath = path.join(artifactRoot, runDirName); From dbccf771be925fdfa9511b540afb3030f61c9afa Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Thu, 30 Apr 2026 08:26:20 +0200 Subject: [PATCH 31/33] chore: bump timeout --- packages/platform-ios/src/xctest-agent.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/platform-ios/src/xctest-agent.ts b/packages/platform-ios/src/xctest-agent.ts index 4b769ae..e455bd6 100644 --- a/packages/platform-ios/src/xctest-agent.ts +++ b/packages/platform-ios/src/xctest-agent.ts @@ -27,7 +27,7 @@ const XCTEST_AGENT_SCHEME_NAME = 'HarnessXCTestAgent'; const XCTEST_AGENT_PORT_ENV = 'HARNESS_XCTEST_AGENT_PORT'; const XCTEST_AGENT_TARGET_BUNDLE_ID_ENV = 'HARNESS_XCTEST_AGENT_TARGET_BUNDLE_ID'; -const XCTEST_AGENT_STARTUP_TIMEOUT_MS = 60_000; +const XCTEST_AGENT_STARTUP_TIMEOUT_MS = 120_000; const XCTEST_AGENT_SHUTDOWN_TIMEOUT_MS = 5_000; const XCTEST_AGENT_STARTUP_POLL_INTERVAL_MS = 250; const HARNESS_DIRNAME = '.harness'; @@ -36,14 +36,14 @@ const pipelineAsync = promisify(pipeline); type XCTestAgentTarget = | { - kind: 'simulator'; - id: string; - } + kind: 'simulator'; + id: string; + } | { - kind: 'device'; - id: string; - codeSign: ApplePhysicalDeviceCodeSign; - }; + kind: 'device'; + id: string; + codeSign: ApplePhysicalDeviceCodeSign; + }; export type XCTestAgentCapability = { getLaunchEnvironment?: () => Record; @@ -232,7 +232,7 @@ const shouldReuseBuildArtifacts = ( manifest.codeSign?.teamId !== target.codeSign.teamId || manifest.codeSign?.signingIdentity !== target.codeSign.signingIdentity || manifest.codeSign?.provisioningProfile !== - target.codeSign.provisioningProfile + target.codeSign.provisioningProfile ) { return false; } @@ -458,8 +458,8 @@ export const createXCTestAgentController = (options: { {}, options.appBundleId ? { - [XCTEST_AGENT_TARGET_BUNDLE_ID_ENV]: options.appBundleId, - } + [XCTEST_AGENT_TARGET_BUNDLE_ID_ENV]: options.appBundleId, + } : {}, ...capabilities.map( (capability) => capability.getLaunchEnvironment?.() ?? {} From 2d0ef7de366268b3b2e0f1f7f753edb00e5288c4 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Thu, 30 Apr 2026 15:11:40 +0200 Subject: [PATCH 32/33] fix(platform-ios): lower xctest agent deployment target --- .../xctest-agent/HarnessXCTestAgent.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/platform-ios/xctest-agent/HarnessXCTestAgent.xcodeproj/project.pbxproj b/packages/platform-ios/xctest-agent/HarnessXCTestAgent.xcodeproj/project.pbxproj index 85eec7d..eb9e095 100644 --- a/packages/platform-ios/xctest-agent/HarnessXCTestAgent.xcodeproj/project.pbxproj +++ b/packages/platform-ios/xctest-agent/HarnessXCTestAgent.xcodeproj/project.pbxproj @@ -252,7 +252,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 26.2; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; @@ -309,7 +309,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 26.2; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; From d7d99c6a6ee01c83fa7397e5c8819b3016e6f485 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Thu, 30 Apr 2026 17:07:27 +0200 Subject: [PATCH 33/33] docs: fix permissions guide --- .../version-plan-1777573200000.md | 5 ++++ website/src/docs/guides/permissions.mdx | 26 ++++++++----------- 2 files changed, 16 insertions(+), 15 deletions(-) create mode 100644 .nx/version-plans/version-plan-1777573200000.md diff --git a/.nx/version-plans/version-plan-1777573200000.md b/.nx/version-plans/version-plan-1777573200000.md new file mode 100644 index 0000000..266ea89 --- /dev/null +++ b/.nx/version-plans/version-plan-1777573200000.md @@ -0,0 +1,5 @@ +--- +__default__: patch +--- + +Add the `permissions` config flag for cross-platform permission automation, using the iOS XCTest agent for prompt auto-accept and Android `adb pm grant` for requested dangerous permissions. diff --git a/website/src/docs/guides/permissions.mdx b/website/src/docs/guides/permissions.mdx index 3214a90..e27c8fe 100644 --- a/website/src/docs/guides/permissions.mdx +++ b/website/src/docs/guides/permissions.mdx @@ -25,7 +25,7 @@ When `permissions` is `true`: - On iOS simulators, Harness starts the XCTest agent and enables best-effort permission prompt auto-accept. - On iOS physical devices, Harness also starts the XCTest agent when the runner is configured with `device.codeSign`. -- On Android emulators and physical devices, Harness uses `adb shell pm grant` to automatically grant a set of common permissions. +- On Android emulators and physical devices, Harness uses `adb shell pm grant` to automatically grant the app's requested dangerous permissions. ## Platform-Specific Implementation Details @@ -45,23 +45,19 @@ Because this is button-based automation, it should be treated as a practical hel ### Android -On Android, Harness pre-grants a comprehensive set of common dangerous permissions using `adb shell pm grant`. This approach: +On Android, Harness inspects the installed app and grants only the permissions that meet both of these conditions: + +- The app declares the permission in its manifest +- The permission is currently classified by the device as a dangerous permission + +This approach: - Grants permissions **proactively** before the app runs, avoiding permission dialogs during testing - Works on both emulators and physical devices -- Includes location, camera, microphone, contacts, calendar, storage, SMS, and other common test permissions -- Uses the `pm grant` command which requires API 23+ or emulated environment support - -The default set of granted permissions includes: -- Location (fine and coarse) -- Camera and microphone -- Contacts and calendar (read/write) -- Call log (read/write) -- Storage (read/write) -- Phone state and calling -- SMS (read/send/receive) -- Body sensors -- Network state and internet +- Avoids trying to grant normal permissions such as `INTERNET` that do not require a runtime grant +- Uses `adb shell pm grant` for each matching permission + +In practice, apps commonly end up having permissions such as camera, microphone, location, contacts, calendar, storage, SMS, or sensors granted automatically, but the exact set depends on what the app requests. ## Performance Impact