diff --git a/src/__tests__/runtime-snapshot.test.ts b/src/__tests__/runtime-snapshot.test.ts index 82f0e1492..b3e53b9b2 100644 --- a/src/__tests__/runtime-snapshot.test.ts +++ b/src/__tests__/runtime-snapshot.test.ts @@ -190,7 +190,7 @@ test('runtime snapshot does not suggest full-screen React Native warning parents assert.doesNotMatch(result.warnings?.[0] ?? '', /@e1/); }); -test('runtime snapshot prefers TextView Minimize over Dismiss on Android React Native stack overlays', async () => { +test('runtime snapshot recognizes Android React Native stack overlays with Dismiss and Minimize controls', async () => { const result = await createSnapshotOnlyDevice({ nodes: [ { ref: 'e1', index: 0, depth: 0, type: 'android.widget.TextView', label: 'useOnyx.ts:80:43' }, @@ -204,7 +204,7 @@ test('runtime snapshot prefers TextView Minimize over Dismiss on Android React N assertReactNativeOverlayWarning(result.warnings); }); -test('runtime snapshot does not suggest Dismiss for Android RedBox stacks without Minimize', async () => { +test('runtime snapshot recognizes Android RedBox stacks without Minimize', async () => { const result = await createSnapshotOnlyDevice({ nodes: [ { ref: 'e1', index: 0, depth: 0, type: 'android.widget.TextView', label: 'useOnyx.ts:80:43' }, @@ -215,7 +215,6 @@ test('runtime snapshot does not suggest Dismiss for Android RedBox stacks withou }).capture.snapshot({ session: 'default', interactiveOnly: true }); assertReactNativeOverlayWarning(result.warnings); - assert.doesNotMatch(result.warnings?.[0] ?? '', /Dismiss before continuing|press @e2/); }); test('runtime snapshot warns when iOS hierarchy looks like a React Native overlay', async () => { diff --git a/src/commands/react-native/overlay.ts b/src/commands/react-native/overlay.ts index 901ac1f1d..d1b89571c 100644 --- a/src/commands/react-native/overlay.ts +++ b/src/commands/react-native/overlay.ts @@ -21,7 +21,7 @@ export type ReactNativeOverlayState = { }; export type ReactNativeOverlayDismissTarget = { - action: 'close' | 'dismiss' | 'minimize' | 'close-collapsed-banner'; + action: 'close' | 'dismiss' | 'close-collapsed-banner'; point: Point; rect?: Rect; ref?: string; @@ -149,18 +149,6 @@ function collectReactNativeOverlayFacts(nodes: SnapshotNode[]): ReactNativeOverl function resolveSafeDismissAction( facts: ReactNativeOverlayFacts, ): ReactNativeOverlayDismissTarget | null { - if (facts.redBox) { - const minimize = firstControlNodeWithRect(facts.minimizeNodes); - if (minimize) return targetFromNode(minimize, 'minimize'); - const dismiss = firstControlNodeWithRect(facts.dismissNodes); - return dismiss - ? { - ...targetFromNode(dismiss, actionFromDismissNode(dismiss)), - warning: 'RedBox Minimize control was not exposed; used Dismiss fallback', - } - : null; - } - const dismiss = firstControlNodeWithRect(facts.dismissNodes); if (dismiss) return targetFromNode(dismiss, actionFromDismissNode(dismiss)); diff --git a/src/compat/maestro/__tests__/runtime-assertions.test.ts b/src/compat/maestro/__tests__/runtime-assertions.test.ts index 01a4356fc..8bb593c6e 100644 --- a/src/compat/maestro/__tests__/runtime-assertions.test.ts +++ b/src/compat/maestro/__tests__/runtime-assertions.test.ts @@ -93,7 +93,7 @@ test('invokeMaestroAssertVisible retries transient snapshot failures until a lat } }); -test('invokeMaestroAssertVisible dismisses React Native overlays before retrying native iOS wait', async () => { +test('invokeMaestroAssertVisible does not dismiss React Native overlays during native iOS wait', async () => { const calls: Array<[string, string[] | undefined]> = []; let waits = 0; const response = await invokeMaestroAssertVisible({ @@ -118,18 +118,13 @@ test('invokeMaestroAssertVisible dismisses React Native overlays before retrying } return { ok: true, data: { matches: 1 } }; } - if (req.command === 'react-native') { - return { ok: true, data: { dismissed: true } }; - } return { ok: false, error: { code: 'UNEXPECTED_COMMAND', message: req.command } }; }, }); - assert.equal(response.ok, true); + assert.equal(response.ok, false); assert.deepEqual(calls, [ ['wait', ['Ready', '60000']], - ['react-native', ['dismiss-overlay']], - ['wait', ['Ready', '60000']], ]); }); @@ -185,7 +180,7 @@ test('invokeMaestroAssertVisible treats an elapsed ellipsis loading gate as alre } }); -test('invokeMaestroAssertVisible dismisses React Native overlays during snapshot assertions', async () => { +test('invokeMaestroAssertVisible reports React Native overlays during snapshot assertions', async () => { const calls: Array<[string, string[] | undefined]> = []; let snapshots = 0; const response = await invokeMaestroAssertVisible({ @@ -222,19 +217,15 @@ test('invokeMaestroAssertVisible dismisses React Native overlays during snapshot ), }; } - if (req.command === 'react-native') { - return { ok: true, data: { dismissed: true } }; - } return { ok: false, error: { code: 'UNEXPECTED_COMMAND', message: req.command } }; }, }); - assert.equal(response.ok, true); - assert.deepEqual(calls, [ - ['snapshot', []], - ['react-native', ['dismiss-overlay']], - ['snapshot', []], - ]); + assert.equal(response.ok, false); + if (!response.ok) { + assert.match(response.error.message, /React Native overlay is covering app content/); + } + assert.deepEqual(calls, [['snapshot', []]]); }); test('invokeMaestroAssertVisible fails fast when a RedBox has no dismiss target', async () => { @@ -278,7 +269,6 @@ test('invokeMaestroAssertVisible fails fast when a RedBox has no dismiss target' } assert.deepEqual(calls, [ ['snapshot', []], - ['react-native', ['dismiss-overlay']], ]); }); diff --git a/src/compat/maestro/__tests__/runtime-interactions.test.ts b/src/compat/maestro/__tests__/runtime-interactions.test.ts index 2ab4a02a1..acf5c5377 100644 --- a/src/compat/maestro/__tests__/runtime-interactions.test.ts +++ b/src/compat/maestro/__tests__/runtime-interactions.test.ts @@ -40,24 +40,15 @@ test('invokeMaestroTapOn clicks normal Close/Dismiss buttons when no React Nativ expect(commands).toEqual(['snapshot', 'click']); }); -test('invokeMaestroTapOn uses React Native overlay dismissal for overlay controls', async () => { - const { response, commands } = await runTapOn( +test('invokeMaestroTapOn clicks explicit React Native overlay controls directly', async () => { + const { response, commands, clicks } = await runTapOn( 'label="Dismiss" || text="Dismiss" || id="Dismiss"', () => overlayDismissButtonSnapshot(), ); expect(response.ok).toBe(true); - expect(commands).toEqual(['snapshot', 'react-native']); -}); - -test('invokeMaestroTapOn dismisses React Native overlays blocking app content and retries', async () => { - const { response, commands, clicks } = await runTapOn('id="article"', (snapshotIndex) => - snapshotIndex === 1 ? overlayBlockingArticleSnapshot() : articleButtonSnapshot(), - ); - - expect(response.ok).toBe(true); - expect(commands).toEqual(['snapshot', 'react-native', 'snapshot', 'click']); - expect(clicks).toEqual([['201', '149']]); + expect(commands).toEqual(['snapshot', 'click']); + expect(clicks).toEqual([['355', '30']]); }); test('invokeMaestroSwipeScreen maps horizontal directional swipes to native gesture presets', async () => { @@ -232,9 +223,6 @@ async function runTapOn( snapshots += 1; return { ok: true, data: readSnapshot(snapshots) }; } - if (req.command === 'react-native') { - return { ok: true, data: { dismissed: true } }; - } if (req.command === 'click') { clicks.push(req.positionals ?? []); return { ok: true, data: {} }; @@ -288,62 +276,6 @@ function buttonSnapshot(label: string): SnapshotState { }; } -function articleButtonSnapshot(): SnapshotState { - return { - createdAt: Date.now(), - nodes: [ - appNode(), - windowNode(), - { - index: 2, - ref: 'e3', - type: 'Button', - identifier: 'article', - label: 'Article', - depth: 4, - parentIndex: 1, - rect: { x: 142, y: 128.66666412353516, width: 118, height: 40 }, - }, - ], - }; -} - -function overlayBlockingArticleSnapshot(): SnapshotState { - return { - createdAt: Date.now(), - nodes: [ - ...articleButtonSnapshot().nodes, - { - index: 10, - ref: 'e10', - type: 'StaticText', - label: 'Runtime Error', - depth: 2, - parentIndex: 1, - rect: { x: 0, y: 0, width: 402, height: 40 }, - }, - { - index: 11, - ref: 'e11', - type: 'Button', - label: 'Minimize', - depth: 2, - parentIndex: 1, - rect: { x: 320, y: 12, width: 70, height: 36 }, - }, - { - index: 12, - ref: 'e12', - type: 'StaticText', - label: 'Call Stack', - depth: 2, - parentIndex: 1, - rect: { x: 0, y: 52, width: 402, height: 40 }, - }, - ], - }; -} - function overlayDismissButtonSnapshot(): SnapshotState { return { createdAt: Date.now(), diff --git a/src/compat/maestro/runtime-assertions.ts b/src/compat/maestro/runtime-assertions.ts index 25c795e77..b96b49b8f 100644 --- a/src/compat/maestro/runtime-assertions.ts +++ b/src/compat/maestro/runtime-assertions.ts @@ -5,7 +5,6 @@ import type { SnapshotState } from '../../utils/snapshot.ts'; import { sleep } from '../../utils/timeouts.ts'; import { captureMaestroRawSnapshot, - dismissReactNativeOverlayIfPresent, errorResponse, rememberMaestroVisibleContext, readSnapshotState, @@ -70,11 +69,7 @@ async function invokeNativeMaestroVisibleWait( nativeWaitQuery: string, ): Promise { const nativeStartedAt = Date.now(); - let nativeResponse = await runNativeVisibleWait(params, args, nativeWaitQuery); - if (!nativeResponse.ok && shouldRetryNativeWaitAfterOverlayDismiss(nativeResponse)) { - const overlayResponse = await dismissReactNativeOverlayIfPresent(params); - if (overlayResponse) nativeResponse = await runNativeVisibleWait(params, args, nativeWaitQuery); - } + const nativeResponse = await runNativeVisibleWait(params, args, nativeWaitQuery); if (!nativeResponse.ok) return nativeResponse; return visibleAssertionResponse( { @@ -120,20 +115,12 @@ async function invokeSnapshotMaestroAssertVisible( const deadlineMs = args.timeoutMs + MAESTRO_ASSERTION_POLICY.assertVisibleGraceMs; let lastResponse: DaemonResponse | undefined; let capturedAfterDeadline = false; - let dismissedOverlay = false; while (true) { const captureStartedAt = Date.now(); const sample = await readMaestroVisibilitySample(params, args.selector, 'assertVisible'); if (sample.visible) return visibleAssertionResponse(sample.response, args.selector, startedAt); lastResponse = sample.response; - const failedSample = await handleFailedVisibleSample(params, args, sample, { - dismissedOverlay, - startedAt, - }); - if (failedSample.kind === 'retry-after-overlay-dismiss') { - dismissedOverlay = true; - continue; - } + const failedSample = handleFailedVisibleSample(params.baseReq, args, sample, startedAt); if (failedSample.kind === 'return') return failedSample.response; const deadline = readVisibleAssertionDeadlineAction({ @@ -159,30 +146,21 @@ async function invokeSnapshotMaestroAssertVisible( ); } -async function handleFailedVisibleSample( - params: { - baseReq: ReplayBaseRequest; - invoke: MaestroRuntimeInvoke; - }, +function handleFailedVisibleSample( + baseReq: ReplayBaseRequest, args: MaestroVisibilityAssertionArgs, sample: Exclude, - state: { dismissedOverlay: boolean; startedAt: number }, -): Promise< + startedAt: number, +): | { kind: 'continue' } - | { kind: 'retry-after-overlay-dismiss' } - | { kind: 'return'; response: DaemonResponse } -> { - const overlayRetry = await maybeDismissOverlayAfterSnapshotFailure( - params, - sample.response, - state.dismissedOverlay, - ); - if (overlayRetry === 'dismissed') return { kind: 'retry-after-overlay-dismiss' }; - if (overlayRetry === 'blocked') return { kind: 'return', response: sample.response }; - if (shouldPassAlreadyPastLoading(params.baseReq, args.selector, sample.snapshot)) { + | { kind: 'return'; response: DaemonResponse } { + if (isReactNativeOverlayBlockingAssertion(sample.response)) { + return { kind: 'return', response: sample.response }; + } + if (shouldPassAlreadyPastLoading(baseReq, args.selector, sample.snapshot)) { return { kind: 'return', - response: alreadyPastLoadingResponse(args.selector, args.timeoutMs, state.startedAt), + response: alreadyPastLoadingResponse(args.selector, args.timeoutMs, startedAt), }; } return { kind: 'continue' }; @@ -218,31 +196,7 @@ function readVisibleAssertionDeadlineAction(params: { : 'finish'; } -async function maybeDismissOverlayAfterSnapshotFailure( - params: { - baseReq: ReplayBaseRequest; - invoke: MaestroRuntimeInvoke; - }, - response: DaemonResponse, - dismissedOverlay: boolean, -): Promise<'dismissed' | 'blocked' | 'none'> { - if (dismissedOverlay || !shouldRetrySnapshotAssertionAfterOverlayDismiss(response)) { - return 'none'; - } - const overlayResponse = await dismissReactNativeOverlayIfPresent(params); - return overlayResponse ? 'dismissed' : 'blocked'; -} - -function shouldRetryNativeWaitAfterOverlayDismiss(response: DaemonResponse): boolean { - return ( - !response.ok && - response.error.code === 'COMMAND_FAILED' && - (response.error.message.includes('Current surface:') || - response.error.message.includes('React Native overlay')) - ); -} - -function shouldRetrySnapshotAssertionAfterOverlayDismiss(response: DaemonResponse): boolean { +function isReactNativeOverlayBlockingAssertion(response: DaemonResponse): boolean { return ( !response.ok && response.error.code === 'COMMAND_FAILED' && diff --git a/src/compat/maestro/runtime-interactions.ts b/src/compat/maestro/runtime-interactions.ts index da5f9b07e..a4018b138 100644 --- a/src/compat/maestro/runtime-interactions.ts +++ b/src/compat/maestro/runtime-interactions.ts @@ -1,9 +1,5 @@ import { getSnapshotReferenceFrame } from '../../daemon/touch-reference-frame.ts'; import type { DaemonRequest, DaemonResponse } from '../../daemon/types.ts'; -import { - detectReactNativeOverlay, - readReactNativeOverlayActionNodes, -} from '../../commands/react-native/overlay.ts'; import { buildSwipeGesturePlan, clampGesturePoint, @@ -18,7 +14,6 @@ import { pointForMaestroTapOnTarget, swipeCoordinatesFromTarget } from './runtim import { captureMaestroRawSnapshot, clearMaestroVisibleContext, - dismissReactNativeOverlayIfPresent, errorResponse, readCachedMaestroReferenceFrame, readMaestroVisibleContext, @@ -437,42 +432,11 @@ async function invokeMaestroSnapshotTapOn( selector: string, options: MaestroTapOnOptions, ): Promise<{ response: DaemonResponse; targetResolved: boolean }> { - const target = await resolveMaestroTapTargetWithOverlayRetry(params, selector, options); - if (!target.ok) return { response: target.response, targetResolved: false }; - const overlayResponse = await maybeDismissReactNativeOverlayTapTarget( - params, - selector, - target.target, - ); - if (overlayResponse) { - if (overlayResponse.ok) clearMaestroVisibleContext(params.scope); - return { - response: overlayResponse, - targetResolved: true, - }; - } - return await clickMaestroSnapshotTarget(params, selector, target.target); -} - -async function resolveMaestroTapTargetWithOverlayRetry( - params: MaestroTapOnParams, - selector: string, - options: MaestroTapOnOptions, -): Promise< - { ok: true; target: ResolvedMaestroSnapshotTarget } | { ok: false; response: DaemonResponse } -> { const target = await resolveMaestroSnapshotTarget(params, selector, options, 'tapOn', { promoteTapTarget: true, }); - if (target.ok || !isReactNativeOverlayBlockedResponse(target.response)) return target; - - const overlayResponse = await dismissReactNativeOverlayIfPresent(params); - if (overlayResponse?.ok) clearMaestroVisibleContext(params.scope); - if (!overlayResponse) return target; - - return await resolveMaestroSnapshotTarget(params, selector, options, 'tapOn', { - promoteTapTarget: true, - }); + if (!target.ok) return { response: target.response, targetResolved: false }; + return await clickMaestroSnapshotTarget(params, selector, target.target); } async function clickMaestroSnapshotTarget( @@ -517,32 +481,6 @@ async function clickMaestroSnapshotTarget( }; } -async function maybeDismissReactNativeOverlayTapTarget( - params: MaestroTapOnParams, - selector: string, - target: ResolvedMaestroSnapshotTarget, -): Promise { - const query = extractMaestroVisibleTextQuery(selector)?.trim().toLowerCase(); - if (query !== 'dismiss' && query !== 'minimize' && query !== 'close') return null; - if (!isReactNativeOverlayControlTarget(target)) return null; - return await dismissReactNativeOverlayIfPresent(params); -} - -function isReactNativeOverlayControlTarget(target: ResolvedMaestroSnapshotTarget): boolean { - const overlay = detectReactNativeOverlay(target.snapshot.nodes); - if (!overlay.detected) return false; - return readReactNativeOverlayActionNodes(overlay).some( - (node) => node.index === target.node.index, - ); -} - -function isReactNativeOverlayBlockedResponse(response: DaemonResponse): boolean { - return ( - !response.ok && - response.error.code === 'ELEMENT_NOT_FOUND' && - response.error.message.includes('React Native overlay is covering app content') - ); -} async function invokeMaestroFuzzyTapOn( params: MaestroTapOnParams, diff --git a/src/compat/maestro/runtime-support.ts b/src/compat/maestro/runtime-support.ts index 56df6f35a..f58f3f1ab 100644 --- a/src/compat/maestro/runtime-support.ts +++ b/src/compat/maestro/runtime-support.ts @@ -86,19 +86,6 @@ export function clearMaestroVisibleContext(scope: ReplayVarScope | undefined): v if (scope) maestroVisibleContextCache.delete(scope); } -export async function dismissReactNativeOverlayIfPresent(params: { - baseReq: ReplayBaseRequest; - invoke: MaestroRuntimeInvoke; -}): Promise { - const response = await params.invoke({ - ...params.baseReq, - command: 'react-native', - positionals: ['dismiss-overlay'], - }); - if (response.ok && response.data?.dismissed === true) return response; - return null; -} - function rememberMaestroReferenceFrame(scope: ReplayVarScope, data: unknown): void { const snapshot = readSnapshotState(data); const frame = getSnapshotReferenceFrame(snapshot); diff --git a/src/daemon/handlers/__tests__/react-native.test.ts b/src/daemon/handlers/__tests__/react-native.test.ts index 9ed677501..7128ac09e 100644 --- a/src/daemon/handlers/__tests__/react-native.test.ts +++ b/src/daemon/handlers/__tests__/react-native.test.ts @@ -270,11 +270,11 @@ test('react-native dismiss-overlay rejects unsafe collapsed warning coordinate f }); }); -test('react-native dismiss-overlay minimizes RedBox error overlays instead of dismissing them', async () => { +test('react-native dismiss-overlay dismisses RedBox error overlays instead of minimizing them', async () => { const sessionName = 'rn-redbox-session'; const sessionStore = makeSessionStore(); sessionStore.set(sessionName, makeSession(sessionName)); - mockDispatchCommand.mockResolvedValue({ x: 265, y: 752 }); + mockDispatchCommand.mockResolvedValue({ x: 95, y: 752 }); mockCaptureSnapshot .mockResolvedValueOnce({ snapshot: { @@ -303,14 +303,7 @@ test('react-native dismiss-overlay minimizes RedBox error overlays instead of di }) .mockResolvedValueOnce({ snapshot: { - nodes: [ - { - index: 0, - ref: 'e20', - label: '!, Runtime Error: NativeModule is null', - rect: { x: 10, y: 786, width: 382, height: 67 }, - }, - ], + nodes: [], createdAt: Date.now(), }, }); @@ -333,29 +326,29 @@ test('react-native dismiss-overlay minimizes RedBox error overlays instead of di expect(mockDispatchCommand).toHaveBeenCalledWith( expect.objectContaining({ platform: 'ios' }), 'press', - ['265', '752'], + ['95', '752'], undefined, expect.any(Object), ); expect(response?.ok && response.data).toMatchObject({ action: 'dismiss-overlay', - overlayAction: 'minimize', - ref: 'e3', - minimized: true, + overlayAction: 'dismiss', + ref: 'e2', + dismissed: true, verified: true, verificationRequired: false, - message: 'React Native RedBox minimize action sent and verified minimized', - x: 265, + message: 'React Native overlay dismiss action sent and verified gone', + x: 95, y: 752, }); - expect(response?.ok && response.data?.dismissed).toBeUndefined(); + expect(response?.ok && response.data?.minimized).toBeUndefined(); }); -test('react-native dismiss-overlay reports unverified minimize when RedBox controls remain', async () => { +test('react-native dismiss-overlay reports unverified dismiss when RedBox controls remain', async () => { const sessionName = 'rn-redbox-still-full-session'; const sessionStore = makeSessionStore(); sessionStore.set(sessionName, makeSession(sessionName)); - mockDispatchCommand.mockResolvedValue({ x: 265, y: 752 }); + mockDispatchCommand.mockResolvedValue({ x: 95, y: 752 }); const fullRedBoxSnapshot = { snapshot: { nodes: [ @@ -402,18 +395,17 @@ test('react-native dismiss-overlay reports unverified minimize when RedBox contr expect(response?.ok).toBe(true); expect(response?.ok && response.data).toMatchObject({ action: 'dismiss-overlay', - overlayAction: 'minimize', - minimized: false, + overlayAction: 'dismiss', + dismissed: true, verified: false, verificationRequired: true, - verificationWarning: expect.stringContaining('RedBox controls are still detected'), + verificationWarning: expect.stringContaining('React Native overlay is still detected'), nextCommand: 'agent-device screenshot --overlay-refs', - message: - 'React Native RedBox minimize action sent, but full RedBox controls are still detected', + message: 'React Native overlay dismiss action sent, but verification still detects an overlay', }); }); -test('react-native dismiss-overlay falls back to Dismiss when RedBox Minimize is absent', async () => { +test('react-native dismiss-overlay uses Dismiss when RedBox Minimize is absent', async () => { const sessionName = 'rn-redbox-dismiss-session'; const sessionStore = makeSessionStore(); sessionStore.set(sessionName, makeSession(sessionName)); @@ -464,8 +456,8 @@ test('react-native dismiss-overlay falls back to Dismiss when RedBox Minimize is action: 'dismiss-overlay', overlayAction: 'dismiss', ref: 'e2', - warning: 'RedBox Minimize control was not exposed; used Dismiss fallback', }); + expect(response?.ok && response.data?.warning).toBeUndefined(); }); test('react-native dismiss-overlay accepts RedBox control labels with keyboard shortcut suffixes', async () => { @@ -519,8 +511,8 @@ test('react-native dismiss-overlay accepts RedBox control labels with keyboard s action: 'dismiss-overlay', overlayAction: 'dismiss', ref: 'e2', - warning: 'RedBox Minimize control was not exposed; used Dismiss fallback', }); + expect(response?.ok && response.data?.warning).toBeUndefined(); }); test('react-native dismiss-overlay prefers concrete RedBox buttons over labeled wrappers', async () => { diff --git a/src/daemon/handlers/react-native.ts b/src/daemon/handlers/react-native.ts index eaeb21d0a..26e300d36 100644 --- a/src/daemon/handlers/react-native.ts +++ b/src/daemon/handlers/react-native.ts @@ -105,7 +105,7 @@ async function dismissReactNativeOverlayTarget( params.contextFromFlags(req.flags, session.appBundleId, session.trace?.outPath), )) ?? {}; const actionFinishedAt = Date.now(); - const verification = await verifyReactNativeOverlayDismissal(params, session, target.action); + const verification = await verifyReactNativeOverlayDismissal(params, session); const responseData = stripUndefined({ ...readSnapshotNodesReferenceFrame(snapshot.nodes), ...data, @@ -116,13 +116,12 @@ async function dismissReactNativeOverlayTarget( ref: target.ref, label: target.label, warning: target.warning, - dismissed: target.action === 'minimize' ? undefined : true, - minimized: target.action === 'minimize' ? verification.verified : undefined, + dismissed: true, verified: verification.verified, verificationRequired: !verification.verified, verificationWarning: verification.verificationWarning, nextCommand: verification.nextCommand, - ...successText(formatDismissMessage(target, verification)), + ...successText(formatDismissMessage(verification)), }); return finalizeTouchInteraction({ session, @@ -140,7 +139,6 @@ async function dismissReactNativeOverlayTarget( async function verifyReactNativeOverlayDismissal( params: InteractionHandlerParams, session: SessionState, - action: ReactNativeOverlayDismissTarget['action'], ): Promise<{ verified: boolean; verificationWarning?: string; @@ -155,9 +153,6 @@ async function verifyReactNativeOverlayDismissal( { interactiveOnly: true }, ); const overlay = analyzeReactNativeOverlay(verificationSnapshot.nodes); - if (action === 'minimize') { - return verifyReactNativeRedBoxMinimized(overlay); - } if (!overlay.detected) { return { verified: true, @@ -171,31 +166,7 @@ async function verifyReactNativeOverlayDismissal( }; } -function verifyReactNativeRedBoxMinimized(overlay: ReturnType): { - verified: boolean; - verificationWarning?: string; - nextCommand?: string; -} { - if (overlay.minimizeNodes.length === 0 && overlay.dismissNodes.length === 0) { - return { verified: true }; - } - return { - verified: false, - verificationWarning: - 'React Native RedBox controls are still detected after minimize. Use screenshot --overlay-refs for visual evidence and report the overlay instead of pressing the warning body.', - nextCommand: 'agent-device screenshot --overlay-refs', - }; -} - -function formatDismissMessage( - target: ReactNativeOverlayDismissTarget, - verification: { verified: boolean }, -): string { - if (target.action === 'minimize') { - return verification.verified - ? 'React Native RedBox minimize action sent and verified minimized' - : 'React Native RedBox minimize action sent, but full RedBox controls are still detected'; - } +function formatDismissMessage(verification: { verified: boolean }): string { if (verification.verified) { return 'React Native overlay dismiss action sent and verified gone'; } diff --git a/test/skillgym/suites/agent-device-smoke-suite.ts b/test/skillgym/suites/agent-device-smoke-suite.ts index 7bb0bf540..169e6e2db 100644 --- a/test/skillgym/suites/agent-device-smoke-suite.ts +++ b/test/skillgym/suites/agent-device-smoke-suite.ts @@ -1002,7 +1002,7 @@ const SKILL_GUIDANCE_CASES: Case[] = [ contract: [ 'App name: Agent Device Tester', 'Current screen after opening will trigger console.warn', - 'Fresh interactive snapshot should show the minimized React Native warning overlay', + 'Fresh interactive snapshot should show the React Native warning overlay', 'Fresh interactive snapshot shows @e4 label="×" for the small close icon', 'The warning overlay can obscure UI and intercept taps', 'Target selector after dismissing overlay: id="submit-order"', @@ -1076,7 +1076,7 @@ const SKILL_GUIDANCE_CASES: Case[] = [ ], }), makeCase({ - id: 'rn-redbox-stack-minimize-before-continuing', + id: 'rn-redbox-stack-dismiss-before-continuing', contract: [ 'App name: Agent Device Tester', 'Platform: Android', @@ -1084,15 +1084,15 @@ const SKILL_GUIDANCE_CASES: Case[] = [ 'Stack includes useOnyx.ts:80:43 and LHNOptionsList.tsx:77', 'Overlay controls include Dismiss and Minimize', 'The RedBox may be caused by an infinite render loop', - 'Target selector after minimizing overlay: id="submit-order"', + 'Target selector after dismissing overlay: id="submit-order"', ], - task: 'Plan commands to recognize the RedBox stack trace, run the React Native overlay command so it prefers Minimize over Dismiss, re-snapshot, then continue to id="submit-order" while reporting the RedBox later.', + task: 'Plan commands to recognize the RedBox stack trace, run the React Native overlay command so it dismisses the overlay, re-snapshot, then continue to id="submit-order" while reporting the RedBox later.', outputs: [ /snapshot -i[\s\S]*react-native\s+dismiss-overlay[\s\S]*snapshot -i[\s\S]*submit-order/i, ], forbiddenOutputs: [ RAW_COORDINATE_TARGET, - /(?:press|click)\b[^\n]*Dismiss/i, + /(?:press|click)\b[^\n]*(?:Dismiss|Minimize)/i, /alert accept/i, /failed nav|navigation failed/i, ],