From 3feb8bf332afe5261efb99fb2f290312c87d3269 Mon Sep 17 00:00:00 2001 From: OrcaWin <293788423+OrcaWin@users.noreply.github.com> Date: Tue, 28 Jul 2026 02:58:13 -0700 Subject: [PATCH] test(mobile): extend Android hosted route parity --- ...hybrid-webview-implementation-checklist.md | 15 +- ...-mobile-hybrid-webview-parity-inventory.md | 23 +- ...bile-hybrid-webview-single-pr-migration.md | 8 + ...27-mobile-hybrid-webview-remaining-work.md | 12 +- .../hosted-android-agent-history-journey.mjs | 203 ++++++++++++++++++ ...sted-ios-source-control-review-journey.mjs | 43 +++- ...sted-android-source-control-review-e2e.mjs | 15 +- ...sted-android-agent-history-journey.test.ts | 98 +++++++++ ...-ios-source-control-review-journey.test.ts | 5 + 9 files changed, 392 insertions(+), 30 deletions(-) create mode 100644 mobile/scripts/hosted-android-agent-history-journey.mjs create mode 100644 mobile/src/mobile-web/hosted-android-agent-history-journey.test.ts diff --git a/docs/reference/plans/2026-07-22-mobile-hybrid-webview-implementation-checklist.md b/docs/reference/plans/2026-07-22-mobile-hybrid-webview-implementation-checklist.md index 8a1cbc4eee4..7b50caafa4e 100644 --- a/docs/reference/plans/2026-07-22-mobile-hybrid-webview-implementation-checklist.md +++ b/docs/reference/plans/2026-07-22-mobile-hybrid-webview-implementation-checklist.md @@ -27,8 +27,8 @@ now pass; iOS Simulator and Android emulator manual previous-generation recovery, cache clear/redownload, and corrupt-active cold fallback now pass through the native recovery surface; the exact iOS Simulator and Android API - 36 Source Control and Review journeys and the full post-rebase validation - matrix now pass + 36 Agent History, Source Control, and Review journeys and the full + post-rebase validation matrix now pass - **Last updated:** July 28, 2026 - **Design:** [`2026-07-22-mobile-hybrid-webview-single-pr-migration.md`](./2026-07-22-mobile-hybrid-webview-single-pr-migration.md) @@ -857,7 +857,12 @@ recovery, or physical-device gates. threshold (observed 2.2677%), a mean-channel-difference budget of 4 (observed 2.5663), and a vertical header-landmark budget of 0.005 (observed 0.00075). The status bar alone is masked. The full live - accessibility and remaining screen/device/state matrix remain open. + accessibility and remaining screen/device/state matrix remain open. The + Pixel 9 Pro API 36 emulator now also passes the same existing route's three + scopes, lazy preview, search/no-match/clear flow, synthetic Resume rejection, + and native-touch Resume into a second Session tab. That fresh paired-app run + continues through Source Control, changed-file handoff, standalone Review, + both isolation probes, and the Android bridge log audit. - [x] Implement approvals and elicitation/question flows. The existing prompt cards call a named response operation that freshly verifies the exact workspace, tab, terminal, agent, provider session, and transcript before @@ -2436,5 +2441,7 @@ copy. | 2026-07-28 | Complete | The exact Pixel 9 Pro API 36 arm64 Debug APK was rebuilt, installed, paired to a disposable Desktop runtime, entered the unchanged workspace and Session UI, opened Source Control, opened a changed file as a second Session diff tab, and verified standalone Review. Deliberate-red network/navigation isolation ended with zero sentinel observations. | | 2026-07-28 | Complete | The Android route harness now clears and audits fresh logcat. The final journey contains no Expo bridge rejection, Kotlin conversion/cast failure, or fatal process error. Focused coverage passes 6 files / 41 tests, Android native module tests pass, mobile and RNW typechecks/lints/formatting/max-lines pass, and the final rebased RNW verifier passes build `f852d8525d2b0e20d79262d74ce3ef74bfa73c3e55b95176bfb1b467beafae61`. | | 2026-07-28 | Complete | Rebased the single migration commit onto `origin/main` at `1fa9ffb5e`; the sole root-package conflict retained upstream's current code-quality script shape and the migration's dedicated mobile-web lint. The branch remains one commit ahead and zero behind. | -| 2026-07-28 | Complete | Final post-rebase validation passes 550 mobile files / 3,285 tests with 2 expected skips and 3,769 root files / 39,207 tests with 62 expected skips. Root/mobile/RNW typechecks, lint, formatting, reliability gates, localization, max-lines, the verified 49-asset RNW package, and the rebuilt Android journey all pass. | +| 2026-07-28 | Complete | The Pixel 9 Pro API 36 route gate now seeds a disposable Agent History fixture and passes Workspace/Project/All scopes, lazy preview, search/no-match/clear, synthetic privileged-resume rejection, and native-touch resume into a second Session tab. The same app session then opens Source Control, a third changed-file Session tab, standalone Review, both isolation probes, and a clean bridge log audit. | +| 2026-07-28 | Complete | Rebased both migration commits without conflict onto current `origin/main` at `0404f27b3`. The branch is two commits ahead and zero behind. | +| 2026-07-28 | Complete | Final validation passes 551 mobile files / 3,286 tests with 2 expected skips and 3,770 root files / 39,212 tests with 62 expected skips. Root/mobile/RNW typechecks, lint, formatting, reliability gates, localization, max-lines, the verified 49-asset RNW package, and the rebuilt Android journey all pass. | | 2026-07-28 | Next | Complete the remaining parity inventory and cutover cleanup, then execute the physical-device, topology, security, performance, packaged-release, and App Store gates. | diff --git a/docs/reference/plans/2026-07-22-mobile-hybrid-webview-parity-inventory.md b/docs/reference/plans/2026-07-22-mobile-hybrid-webview-parity-inventory.md index bc0e95bfdbd..7faf78e1b06 100644 --- a/docs/reference/plans/2026-07-22-mobile-hybrid-webview-parity-inventory.md +++ b/docs/reference/plans/2026-07-22-mobile-hybrid-webview-parity-inventory.md @@ -3,7 +3,7 @@ - **Status:** In progress; route, terminal, native, persisted-state, UX-state, accessibility/input, notification, native-chat, and UI-preservation boundaries frozen -- **Last updated:** July 27, 2026 +- **Last updated:** July 28, 2026 - **Design:** [`2026-07-22-mobile-hybrid-webview-single-pr-migration.md`](./2026-07-22-mobile-hybrid-webview-single-pr-migration.md) - **Checklist:** @@ -66,11 +66,11 @@ but is a component, not a route. It migrates with the session UI. | `mobile/app/h/[hostId]/accounts.tsx` | Host agent-account usage and selection | Mobile web app | Complete: the same screen uses typed native/web host-account adapters | | `mobile/app/h/[hostId]/tasks.tsx` | Host task providers, task details, mutations, workspace creation | Mobile web app | Complete on iOS Simulator: same route/presentation with strict native/web operations | | `mobile/app/h/[hostId]/session/[worktreeId].tsx` | Sessions, tabs, terminal, browser, native chat, files, attachments, dictation | Mobile web app | Reuse current session presentation; native-chat/agent-state slice is adapter-complete and awaiting live parity evidence | -| `mobile/app/h/[hostId]/agent-history/[worktreeId].tsx` | Agent session history and resume | Mobile web app | Same panel/list presentation now mounted with bounded opaque snapshot/preview/resume operations; live hosted parity remains | +| `mobile/app/h/[hostId]/agent-history/[worktreeId].tsx` | Agent session history and resume | Mobile web app | Complete on iOS and Android emulators: same panel/list presentation, bounded opaque snapshot/preview/resume operations, scopes, search, and trusted-resume mediation | | `mobile/app/h/[hostId]/files/[worktreeId].tsx` | File explorer | Mobile web app | Complete at adapter level: the same `MobileFileExplorerPanel` is mounted by a thin hosted route with bounded opaque directory reads and native-shell reconnect | | `mobile/app/h/[hostId]/files/preview/[worktreeId].tsx` | File, Markdown, image, editable, HTML, and Mermaid previews | Mobile web app | Reuse current preview UI; HTML is sanitized in a hash-bound inert frame, while native/hosted Mermaid use the locally bundled hash-authorized engine and unchanged fallback presentation | -| `mobile/app/h/[hostId]/source-control/[worktreeId].tsx` | Source-control hub | Mobile web app | Complete on iOS Simulator: same `MobileSourceControlPanel`, including Session-origin changed-file handoff; Android route evidence remains | -| `mobile/app/h/[hostId]/review/[worktreeId].tsx` | Diff review and comments | Mobile web app | Complete on iOS Simulator: same review presentation and virtualization; standalone controls verified independently | +| `mobile/app/h/[hostId]/source-control/[worktreeId].tsx` | Source-control hub | Mobile web app | Complete on iOS and Android emulators: same `MobileSourceControlPanel`, including Session-origin changed-file handoff | +| `mobile/app/h/[hostId]/review/[worktreeId].tsx` | Diff review and comments | Mobile web app | Complete on iOS and Android emulators: same review presentation and virtualization; standalone controls verified independently | | `mobile/app/h/[hostId]/history/[worktreeId].tsx` | Legacy source-control history redirect | Mobile web app | Complete: provider-neutral compatibility redirect preserves the history segment during rollout | | `mobile/app/h/[hostId]/pr/[worktreeId].tsx` | Legacy pull-request redirect | Mobile web app | Complete: provider-neutral compatibility redirect preserves the pull-request segment during rollout | | `mobile/app/hybrid-prototype.tsx` | Experimental single-document Option B prototype | Remove | Replace with production host route after all gates pass | @@ -472,13 +472,14 @@ IDs, and unrelated provider state never enter the page result. ## Next Inventory Action -The dedicated hosted Files, Preview, Source Control, and Review routes now -mount the unchanged presentations through native/web adapters. The iPhone 17 -Pro Simulator verifies Session-origin changed-file handoff, standalone Review, -and both isolation probes. Provider-neutral History and PR compatibility -redirects are present. Run the same Source Control/Review route evidence on -Android after restoring the Android SDK/AVD, then close the remaining -native-versus-hosted interaction matrix. +The dedicated hosted Files, Preview, Source Control, Review, and Agent History +routes mount the unchanged presentations through native/web adapters. The +iPhone 17 Pro Simulator verifies Session-origin changed-file handoff, +standalone Review, and both isolation probes. The Pixel 9 Pro API 36 emulator +now verifies those paths in the same fresh app session as Agent History scopes, +preview, filtering, synthetic-resume rejection, and native-touch resume. +Provider-neutral History and PR compatibility redirects are present. Close the +remaining native-versus-hosted interaction matrix. The interrupted-transcript versus hook-status mismatch and a real structured prompt response pass Host 37 Simulator replay. Current package `4b7df7d4…` diff --git a/docs/reference/plans/2026-07-22-mobile-hybrid-webview-single-pr-migration.md b/docs/reference/plans/2026-07-22-mobile-hybrid-webview-single-pr-migration.md index 417455c1e73..d746e6e2e28 100644 --- a/docs/reference/plans/2026-07-22-mobile-hybrid-webview-single-pr-migration.md +++ b/docs/reference/plans/2026-07-22-mobile-hybrid-webview-single-pr-migration.md @@ -2092,6 +2092,14 @@ accessibility finding remains open. This is not completion of the phone/tablet, shell-state, route, screen-reader, or physical-device matrix. +The Pixel 9 Pro API 36 emulator now exercises that same hosted Agent History +presentation in a freshly installed paired app. Workspace/Project/All scopes, +lazy preview, search/no-match/clear, synthetic privileged-resume rejection, and +native-touch resume into a second Session tab pass. The same authenticated +WebView then opens Source Control, a changed file as a third Session tab, and +standalone Review before passing the network/navigation isolation probes and +fresh Android bridge-log audit. + Native route ownership now has exact-app lifecycle evidence too. A gesture-gated request from the unchanged hosted Session records only a request-scoped Terminal Settings intent, posts the broker success response, and diff --git a/docs/reference/plans/2026-07-27-mobile-hybrid-webview-remaining-work.md b/docs/reference/plans/2026-07-27-mobile-hybrid-webview-remaining-work.md index 51db928d95f..4f6345db25e 100644 --- a/docs/reference/plans/2026-07-27-mobile-hybrid-webview-remaining-work.md +++ b/docs/reference/plans/2026-07-27-mobile-hybrid-webview-remaining-work.md @@ -37,9 +37,15 @@ verifies standalone Review. The deliberate-red network/navigation isolation probe records zero escaped traffic, and the embedded log audit records no Expo bridge rejection, Kotlin conversion/cast error, or fatal process error. -The migration is rebased onto `origin/main` at `1fa9ffb5e`. Post-rebase -validation passes 550 mobile files / 3,285 tests with 2 expected skips and -3,769 root files / 39,207 tests with 62 expected skips. All project typechecks, +The Android route gate now also seeds a disposable Agent History fixture and +verifies the unchanged screen's Workspace/Project/All scopes, lazy preview, +search/no-match/clear flow, rejection of synthetic privileged activation, and +native-touch resume into a second Session tab before continuing through Source +Control and Review. + +The migration is rebased onto `origin/main` at `0404f27b3`. Post-rebase +validation passes 551 mobile files / 3,286 tests with 2 expected skips and +3,770 root files / 39,212 tests with 62 expected skips. All project typechecks, root/mobile/mobile-web lint, reliability gates, and the max-lines ratchet pass. The independently verified React Native Web package is `f852d8525d2b0e20d79262d74ce3ef74bfa73c3e55b95176bfb1b467beafae61`: diff --git a/mobile/scripts/hosted-android-agent-history-journey.mjs b/mobile/scripts/hosted-android-agent-history-journey.mjs new file mode 100644 index 00000000000..fdabd4abe50 --- /dev/null +++ b/mobile/scripts/hosted-android-agent-history-journey.mjs @@ -0,0 +1,203 @@ +import { + activateHostedWebViewControl, + readHostedWebViewState, + readHostedWebViewTextPoint, + setHostedWebViewInput, + waitForVisibleHostedWebView +} from './hosted-webview-cdp-session.mjs' +import { + EMULATOR_AGENT_HISTORY_PREVIEW_MARKER, + EMULATOR_AGENT_HISTORY_TITLE +} from './emulator-agent-history-fixture.mjs' +import { tapHostedAndroidPoint } from './hosted-android-emulator-accessibility.mjs' +import { readHostedWebViewControlPoint } from './hosted-webview-control-point.mjs' + +const SEARCH_PLACEHOLDER = 'Search sessions, repo:, path:' + +export async function verifyHostedAndroidAgentHistoryJourney({ + discoveryUrl, + emulator, + sessionDocument, + timeoutMs +}) { + if (!sessionDocument.href.includes('/session/')) { + throw new Error('Agent History journey requires an active Session route.') + } + const actionsDocument = await evidenceStep('open Session actions', () => + retryAndroidRouteActivation({ + activate: () => tapAndroidLabel(emulator, sessionDocument, 'More session actions'), + wait: () => + waitForVisibleHostedWebView({ + discoveryUrl, + expectedText: 'Agent History', + expectedHrefIncludes: '/session/', + timeoutMs: Math.min(timeoutMs, 3_000) + }) + }) + ) + await evidenceStep('open Agent History', () => + retryAndroidRouteActivation({ + activate: () => + tapAndroidText(emulator, actionsDocument, 'Agent History', Math.min(timeoutMs, 5_000)), + wait: () => + waitForVisibleHostedWebView({ + discoveryUrl, + expectedText: 'Agent Session History', + expectedHrefIncludes: '/agent-history/', + timeoutMs: Math.min(timeoutMs, 3_000) + }) + }) + ) + let historyDocument = await waitForVisibleHostedWebView({ + discoveryUrl, + expectedText: EMULATOR_AGENT_HISTORY_TITLE, + expectedHrefIncludes: '/agent-history/', + timeoutMs + }) + const state = await evidenceStep('read Agent History state', () => + readHostedWebViewState(historyDocument) + ) + assertAgentHistoryState(state) + await evidenceStep('expand Agent History row', () => + activateHostedWebViewControl(historyDocument, { + kind: 'text', + value: EMULATOR_AGENT_HISTORY_TITLE + }) + ) + historyDocument = await waitForVisibleHostedWebView({ + discoveryUrl, + expectedText: EMULATOR_AGENT_HISTORY_PREVIEW_MARKER, + timeoutMs + }) + await evidenceStep('filter Agent History', () => + setHostedWebViewInput(historyDocument, { + placeholder: SEARCH_PLACEHOLDER, + value: 'NO_MATCHING_AGENT_HISTORY_SESSION' + }) + ) + historyDocument = await waitForVisibleHostedWebView({ + discoveryUrl, + expectedText: 'No sessions match your search.', + timeoutMs + }) + await evidenceStep('clear Agent History filter', () => + setHostedWebViewInput(historyDocument, { + placeholder: SEARCH_PLACEHOLDER, + value: '' + }) + ) + historyDocument = await waitForVisibleHostedWebView({ + discoveryUrl, + expectedText: EMULATOR_AGENT_HISTORY_TITLE, + timeoutMs + }) + for (const scope of ['Project', 'All']) { + await evidenceStep(`select ${scope} scope`, () => + activateHostedWebViewControl(historyDocument, { + kind: 'text', + value: scope + }) + ) + historyDocument = await waitForVisibleHostedWebView({ + discoveryUrl, + expectedText: EMULATOR_AGENT_HISTORY_TITLE, + timeoutMs + }) + } + await evidenceStep('reject untrusted synthetic resume', () => + activateHostedWebViewControl(historyDocument, { + kind: 'label', + value: 'Resume agent session' + }) + ) + historyDocument = await waitForVisibleHostedWebView({ + discoveryUrl, + expectedText: 'Failed to resume session.', + timeoutMs + }) + const nativeTouchPoint = await evidenceStep('resume Agent History from native touch', () => + tapAndroidLabel(emulator, historyDocument, 'Resume agent session') + ) + const resumedSessionDocument = await waitForVisibleHostedWebView({ + discoveryUrl, + expectedText: '2 tabs', + expectedHrefIncludes: '/session/', + timeoutMs + }) + return { + route: state.href, + scopes: ['Workspace', 'Project', 'All'], + search: 'present', + row: EMULATOR_AGENT_HISTORY_TITLE, + preview: EMULATOR_AGENT_HISTORY_PREVIEW_MARKER, + resume: { + synthetic: 'gesture rejected', + native: 'queued', + nativeTouchPoint, + resumedRoute: resumedSessionDocument.href + }, + headerControls: ['Back', 'Refresh agent sessions'], + returnedSessionDocument: resumedSessionDocument + } +} + +async function tapAndroidLabel(emulator, document, label) { + const point = await readHostedWebViewControlPoint(document, label) + return tapHostedAndroidPoint(emulator, point) +} + +async function tapAndroidText(emulator, document, text, timeoutMs) { + const deadline = Date.now() + timeoutMs + let lastError = new Error(`Hosted Android text was not visible: ${text}`) + while (Date.now() < deadline) { + try { + const point = await readHostedWebViewTextPoint(document, text) + return await tapHostedAndroidPoint(emulator, point) + } catch (error) { + lastError = error + await delay(100) + } + } + throw lastError +} + +async function retryAndroidRouteActivation({ activate, wait }) { + let lastError = new Error('Hosted Android route activation failed') + for (let attempt = 0; attempt < 3; attempt += 1) { + try { + await activate() + return await wait() + } catch (error) { + lastError = error + } + } + throw lastError +} + +function assertAgentHistoryState(state) { + for (const expected of ['Workspace', 'Project', 'All']) { + if (!state.bodyText.includes(expected)) { + throw new Error(`Agent History is missing its ${expected} scope.`) + } + } + if (!state.labels.includes('Back') || !state.labels.includes('Refresh agent sessions')) { + throw new Error('Agent History is missing its existing header controls.') + } + if (!state.placeholders.includes(SEARCH_PLACEHOLDER)) { + throw new Error('Agent History is missing its existing search input.') + } +} + +async function evidenceStep(label, run) { + try { + return await run() + } catch (error) { + throw new Error(`${label} failed: ${error instanceof Error ? error.message : String(error)}`, { + cause: error + }) + } +} + +function delay(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)) +} diff --git a/mobile/scripts/hosted-ios-source-control-review-journey.mjs b/mobile/scripts/hosted-ios-source-control-review-journey.mjs index 3a0513a4995..c1334e27db1 100644 --- a/mobile/scripts/hosted-ios-source-control-review-journey.mjs +++ b/mobile/scripts/hosted-ios-source-control-review-journey.mjs @@ -11,20 +11,16 @@ export async function verifyHostedSourceControlReviewJourney({ emulator, sessionDocument, timeoutMs, + expectedSessionDiffText = '2 tabs', tapPoint = tapHostedIosPoint }) { - const sourceControlPoint = await journeyStep('measure Open source control', () => - readHostedWebViewControlPoint(sessionDocument, 'Open source control') - ) - await journeyStep('tap Open source control', () => - tapPoint(emulator, sourceControlPoint, 'Open source control') - ) const sourceControl = await journeyStep('wait for Source Control route', () => - waitForVisibleHostedWebView({ + openSourceControlRoute({ discoveryUrl, - expectedText: 'Source Control', - expectedHrefIncludes: '/source-control/', - timeoutMs + emulator, + sessionDocument, + timeoutMs, + tapPoint }) ) const sourceState = await journeyStep('read populated Source Control state', () => @@ -51,7 +47,7 @@ export async function verifyHostedSourceControlReviewJourney({ const sessionDiff = await journeyStep('wait for Session diff route', () => waitForVisibleHostedWebView({ discoveryUrl, - expectedText: '2 tabs', + expectedText: expectedSessionDiffText, expectedHrefIncludes: '/session/', requireInteractiveControls: false, timeoutMs @@ -84,6 +80,31 @@ export async function verifyHostedSourceControlReviewJourney({ } } +async function openSourceControlRoute({ + discoveryUrl, + emulator, + sessionDocument, + timeoutMs, + tapPoint +}) { + let lastError = new Error('Source Control route did not open') + for (let attempt = 0; attempt < 3; attempt += 1) { + try { + const point = await readHostedWebViewControlPoint(sessionDocument, 'Open source control') + await tapPoint(emulator, point, 'Open source control') + return await waitForVisibleHostedWebView({ + discoveryUrl, + expectedText: 'Source Control', + expectedHrefIncludes: '/source-control/', + timeoutMs: Math.min(timeoutMs, 3_000) + }) + } catch (error) { + lastError = error + } + } + throw lastError +} + async function waitForChangedFileState(document, timeoutMs) { const deadline = Date.now() + timeoutMs let state diff --git a/mobile/scripts/run-hosted-android-source-control-review-e2e.mjs b/mobile/scripts/run-hosted-android-source-control-review-e2e.mjs index ed938dd46ad..6550ae0a714 100644 --- a/mobile/scripts/run-hosted-android-source-control-review-e2e.mjs +++ b/mobile/scripts/run-hosted-android-source-control-review-e2e.mjs @@ -5,6 +5,7 @@ import path from 'node:path' import process from 'node:process' import { promisify } from 'node:util' import { resolveEmulatorOrcaCli } from './emulator-orca-cli-selection.mjs' +import { verifyHostedAndroidAgentHistoryJourney } from './hosted-android-agent-history-journey.mjs' import { tapHostedAndroidAccessibilityControl, tapHostedAndroidPoint, @@ -83,6 +84,7 @@ async function main() { logSuccess: () => {} }) ) + runtime.env.ORCA_E2E_MOBILE_AGENT_HISTORY_FIXTURE = '1' await stage('test workspace registration', () => registerWorktreeForPairingRuntime(runtime, worktree, { orca: runOrca, @@ -175,11 +177,21 @@ async function main() { timeoutMs: options.timeoutMs }) ) + const agentHistoryResult = await stage('Agent History journey', () => + verifyHostedAndroidAgentHistoryJourney({ + discoveryUrl, + emulator, + sessionDocument, + timeoutMs: options.timeoutMs + }) + ) + const { returnedSessionDocument, ...agentHistory } = agentHistoryResult const sourceControlReview = await stage('Source Control and Review journey', () => verifyHostedSourceControlReviewJourney({ discoveryUrl, emulator, - sessionDocument, + sessionDocument: returnedSessionDocument, + expectedSessionDiffText: '3 tabs', timeoutMs: options.timeoutMs, tapPoint: tapHostedAndroidJourneyControl }) @@ -217,6 +229,7 @@ async function main() { device: await runAndroidAdb(adb, ['shell', 'getprop', 'ro.product.model']), pid: inspector.pid, workspace: expectedWorkspace, + agentHistory, sourceControlReview, networkIsolation, navigationIsolation, diff --git a/mobile/src/mobile-web/hosted-android-agent-history-journey.test.ts b/mobile/src/mobile-web/hosted-android-agent-history-journey.test.ts new file mode 100644 index 00000000000..5a946bf0e39 --- /dev/null +++ b/mobile/src/mobile-web/hosted-android-agent-history-journey.test.ts @@ -0,0 +1,98 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const mocks = vi.hoisted(() => ({ + activateControl: vi.fn(), + readControlPoint: vi.fn(), + readState: vi.fn(), + readTextPoint: vi.fn(), + setInput: vi.fn(), + tapPoint: vi.fn(), + waitForDocument: vi.fn() +})) + +vi.mock('../../scripts/hosted-webview-cdp-session.mjs', () => ({ + activateHostedWebViewControl: mocks.activateControl, + readHostedWebViewState: mocks.readState, + readHostedWebViewTextPoint: mocks.readTextPoint, + setHostedWebViewInput: mocks.setInput, + waitForVisibleHostedWebView: mocks.waitForDocument +})) + +vi.mock('../../scripts/hosted-android-emulator-accessibility.mjs', () => ({ + tapHostedAndroidPoint: mocks.tapPoint +})) + +vi.mock('../../scripts/hosted-webview-control-point.mjs', () => ({ + readHostedWebViewControlPoint: mocks.readControlPoint +})) + +import { verifyHostedAndroidAgentHistoryJourney } from '../../scripts/hosted-android-agent-history-journey.mjs' + +describe('hosted Android Agent History journey', () => { + beforeEach(() => { + vi.clearAllMocks() + mocks.activateControl.mockResolvedValue(undefined) + mocks.readControlPoint.mockResolvedValue({ x: 0.4, y: 0.3 }) + mocks.readTextPoint.mockResolvedValue({ x: 0.5, y: 0.4 }) + mocks.setInput.mockResolvedValue(undefined) + mocks.tapPoint.mockImplementation((_emulator, point) => point) + mocks.readState.mockResolvedValue({ + href: 'orca-mobile-web://build/h/host/agent-history/workspace', + bodyText: 'Workspace Project All', + labels: ['Back', 'Refresh agent sessions', 'Resume agent session'], + placeholders: ['Search sessions, repo:, path:'] + }) + mocks.waitForDocument.mockImplementation(({ expectedHrefIncludes, expectedText }) => ({ + href: `orca-mobile-web://build/h/host${ + expectedHrefIncludes ?? + (expectedText === '2 tabs' || expectedText === 'Agent History' + ? '/session/' + : '/agent-history/') + }workspace` + })) + }) + + it('covers the existing scopes, preview, search, trusted resume, and Back flow', async () => { + const sessionDocument = { + href: 'orca-mobile-web://build/h/host/session/workspace' + } + + const result = await verifyHostedAndroidAgentHistoryJourney({ + discoveryUrl: 'http://127.0.0.1:9222', + emulator: { adb: '/sdk/adb' }, + sessionDocument, + timeoutMs: 30_000 + }) + + const { returnedSessionDocument, ...evidence } = result + expect(evidence).toEqual({ + route: 'orca-mobile-web://build/h/host/agent-history/workspace', + scopes: ['Workspace', 'Project', 'All'], + search: 'present', + row: 'Hybrid Agent History Fixture', + preview: 'E2E_AGENT_HISTORY_PREVIEW_MARKER', + resume: { + synthetic: 'gesture rejected', + native: 'queued', + nativeTouchPoint: { x: 0.4, y: 0.3 }, + resumedRoute: 'orca-mobile-web://build/h/host/session/workspace' + }, + headerControls: ['Back', 'Refresh agent sessions'] + }) + expect(returnedSessionDocument.href).toContain('/session/') + expect(mocks.setInput).toHaveBeenNthCalledWith(1, expect.anything(), { + placeholder: 'Search sessions, repo:, path:', + value: 'NO_MATCHING_AGENT_HISTORY_SESSION' + }) + expect(mocks.setInput).toHaveBeenNthCalledWith(2, expect.anything(), { + placeholder: 'Search sessions, repo:, path:', + value: '' + }) + expect(mocks.readControlPoint.mock.calls.map((call) => call[1])).toEqual([ + 'More session actions', + 'Resume agent session' + ]) + expect(mocks.readTextPoint.mock.calls.map((call) => call[1])).toEqual(['Agent History']) + expect(mocks.tapPoint).toHaveBeenCalledTimes(3) + }) +}) diff --git a/mobile/src/mobile-web/hosted-ios-source-control-review-journey.test.ts b/mobile/src/mobile-web/hosted-ios-source-control-review-journey.test.ts index a72267615b8..94713947bfc 100644 --- a/mobile/src/mobile-web/hosted-ios-source-control-review-journey.test.ts +++ b/mobile/src/mobile-web/hosted-ios-source-control-review-journey.test.ts @@ -112,6 +112,7 @@ describe('hosted iOS Source Control and Review journey', () => { sessionDocument: { href: 'orca-mobile-web://build/h/host/session/workspace' }, + expectedSessionDiffText: '3 tabs', timeoutMs: 30_000, tapPoint }) @@ -121,5 +122,9 @@ describe('hosted iOS Source Control and Review journey', () => { 'Open source control', 'Open changed file mobile/app/index.tsx' ]) + expect(mocks.waitForDocument).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ expectedText: '3 tabs' }) + ) }) })