mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 16:02:50 +00:00
test(mobile): add iOS core route parity
This commit is contained in:
@@ -11,9 +11,9 @@
|
||||
the production hosted broker/client contract, and reconnect pass a real
|
||||
Docker transport journey; hosted workspace, session, and native-chat reads
|
||||
and production package delivery also cross the real mobile Relay client and
|
||||
E2EE stack through a protocol-compatible local cell; the first automated
|
||||
native-versus-hosted populated-screen gate now passes Agent History on the
|
||||
iPhone 17 Pro Simulator; the hosted Session now hands Terminal Settings to the
|
||||
E2EE stack through a protocol-compatible local cell; automated
|
||||
native-versus-hosted populated-screen gates now pass Tasks, Session, and
|
||||
Agent History on the iPhone 17 Pro Simulator; the hosted Session now hands Terminal Settings to the
|
||||
existing native route and returns to the identical hosted session; native-chat
|
||||
deadlines and gesture-gated image attachments preserve the unchanged composer
|
||||
through opaque shell-owned references; the Android API 36 emulator now stages
|
||||
@@ -107,10 +107,10 @@ to the existing RPC behavior. Named page schemas and broker grants now cover
|
||||
repository/settings/trust/provider/SSH/agent/hook/capability reads with opaque
|
||||
repository and SSH authority; source search, base resolution, create
|
||||
operations, native revalidation, and the complete web adapter now pass focused
|
||||
round-trip tests. Populated Agent History now passes deterministic native-versus-
|
||||
hosted screenshot and interaction parity in portrait and landscape; the
|
||||
remaining routes, shell states, tablet/device classes, and accessibility
|
||||
matrix remain open.
|
||||
round-trip tests. Populated Tasks and Session now pass deterministic
|
||||
native-versus-hosted portrait screenshot and interaction parity, and Agent
|
||||
History passes in portrait and landscape; the remaining routes, shell states,
|
||||
tablet/device classes, and accessibility matrix remain open.
|
||||
The unchanged session route now receives snapshot, subscription, blank-terminal
|
||||
creation, activation, and close behavior through `HostSessionTabOperations`;
|
||||
native retains the existing RPC mapping while the hosted route uses only the
|
||||
@@ -407,6 +407,14 @@ Source Control and Review journey. Session-origin changed files open a second
|
||||
Session diff tab, standalone Review retains its existing controls, and the same
|
||||
run passes the private-origin network and navigation isolation probes.
|
||||
|
||||
The same simulator now captures native and hosted Tasks and Session from one
|
||||
disposable Desktop fixture. Tasks passes at 0.022% changed pixels, 0.084 mean
|
||||
channel difference, and 0.000016 vertical-title delta. Session passes at
|
||||
0.800%, 1.693, and 0.000366 respectively, within the 3%, 4, and 0.005 budgets.
|
||||
The complete cached-app run also passes Agent History portrait/landscape,
|
||||
Desktop restart/recovery, synthetic-resume rejection, native-touch resume, a
|
||||
third Session diff tab, standalone Review, and both isolation probes.
|
||||
|
||||
The exact Pixel 9 Pro API 36 arm64 Debug APK now passes the same journey after a
|
||||
fresh native build and install. Android accessibility opens the unchanged
|
||||
Source Control hub and changed-file action, Session retains the second diff tab,
|
||||
@@ -416,8 +424,9 @@ escaped traffic and a clean native bridge log audit.
|
||||
|
||||
**Next action:** Complete the remaining parity inventory and cutover cleanup
|
||||
before expanding the external validation matrix.
|
||||
Extend the deterministic native-versus-hosted screenshot fixture beyond the
|
||||
passing populated Agent History portrait case. The authoritative shared RNW
|
||||
Extend the deterministic native-versus-hosted screenshot fixture across the
|
||||
remaining populated routes, shell states, tablet/device classes, and
|
||||
accessibility matrix. The authoritative shared RNW
|
||||
route is now governed by a separate reviewed ceiling of 10 MiB total, 3 MiB
|
||||
gzip, 9.5 MiB of scripts, 256 KiB of styles, and 64 assets. The earlier 8 MiB /
|
||||
2 MiB / 7.5 MiB ceilings held before the existing Mermaid presentation was
|
||||
@@ -2444,4 +2453,9 @@ copy.
|
||||
| 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 | Complete | A cached exact-app iPhone 17 Pro / iOS 26.5 Simulator run captured native and hosted Tasks and Session from the same fixture. Tasks passed at 0.022% changed pixels / 0.084 mean channel difference / 0.000016 vertical-title delta; Session passed at 0.800% / 1.693 / 0.000366 against 3% / 4 / 0.005 budgets. |
|
||||
| 2026-07-28 | Complete | The same iOS run passed Agent History portrait/landscape parity, forced Desktop restart and E2EE recovery, synthetic-resume rejection, native-touch resume, Source Control, a third changed-file Session tab, standalone Review, and private-origin network/navigation isolation. |
|
||||
| 2026-07-28 | Finding | The existing non-embedded Tasks toolbar icon has no native accessibility label. The parity driver uses the accessible Filter row to locate the unchanged icon; full VoiceOver review remains open. Source Control native taps now prefer platform accessibility and retry a measured point after a silent WebKit miss, and the full journey asserts the correct third diff tab. |
|
||||
| 2026-07-28 | Complete | A fresh iOS native build/install passed the complete exact-app journey with the final parity and route-settling driver: Tasks, Session, Agent History portrait/landscape, Desktop restart/recovery, native-touch resume, third diff tab, standalone Review, and both private-origin isolation probes. |
|
||||
| 2026-07-28 | Complete | Post-slice validation passes 552 mobile files / 3,291 tests with 2 expected skips and 3,770 root files / 39,212 tests with 62 expected skips. All three typechecks, root/mobile/RNW lint, reliability, localization, max-lines, changed-file and full-mobile formatting, and diff hygiene pass; fetched `origin/main` remains `0404f27b3`. |
|
||||
| 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. |
|
||||
|
||||
@@ -478,7 +478,12 @@ 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
|
||||
The iPhone fixture also captures native and hosted Tasks and Session from the
|
||||
same disposable runtime: Tasks passes at 0.022% changed pixels / 0.084 mean
|
||||
channel difference / 0.000016 vertical-title delta, and Session passes at
|
||||
0.800% / 1.693 / 0.000366 within the 3% / 4 / 0.005 budgets. Provider-neutral
|
||||
History and PR compatibility redirects are present. The non-embedded Tasks
|
||||
toolbar icon's missing native accessibility label remains open. Close the
|
||||
remaining native-versus-hosted interaction matrix.
|
||||
|
||||
The interrupted-transcript versus hook-status mismatch and a real structured
|
||||
|
||||
@@ -2092,6 +2092,15 @@ accessibility finding remains open. This is not completion of the phone/tablet,
|
||||
shell-state, route,
|
||||
screen-reader, or physical-device matrix.
|
||||
|
||||
The same deterministic fixture now captures the unchanged native and hosted
|
||||
Tasks and Session presentations from one disposable Desktop runtime. Tasks
|
||||
passes at 0.022% changed pixels, 0.084 mean channel difference, and 0.000016
|
||||
vertical-title delta. Session passes at 0.800%, 1.693, and 0.000366
|
||||
respectively, within the 3%, 4, and 0.005 budgets. The existing non-embedded
|
||||
Tasks toolbar icon has no native accessibility label, so the fixture locates
|
||||
its row from the accessible Filter control and preserves the current UI. That
|
||||
missing label remains part of the full accessibility gate.
|
||||
|
||||
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
|
||||
@@ -2322,8 +2331,9 @@ feature UI.
|
||||
overwrite those native records.
|
||||
- Add deterministic native-versus-web screenshot and interaction fixtures for
|
||||
phone, tablet, portrait, landscape, loading, offline, error, and populated
|
||||
states. Populated Agent History portrait now passes the first calibrated
|
||||
pixel and vertical safe-area fixture; the remainder of the matrix stays open.
|
||||
states. Populated Tasks and Session portrait plus Agent History portrait and
|
||||
landscape now pass calibrated pixel and vertical safe-area fixtures; the
|
||||
remainder of the matrix stays open.
|
||||
- After every rebase, reconcile upstream mobile presentation changes through
|
||||
the shared source and rerun the parity fixtures; do not preserve an older
|
||||
forked snapshot for the web runtime.
|
||||
@@ -2385,6 +2395,10 @@ independently renders standalone Review with its existing controls. The same
|
||||
run passes private-origin network and navigation isolation. The exact Pixel 9
|
||||
Pro API 36 arm64 Debug APK passes the matching route journey and isolation
|
||||
corpus after a fresh build/install, with a clean native bridge log audit.
|
||||
After Agent History resumes a second Session tab, the full iOS journey now
|
||||
asserts that the changed file creates the third tab. Native iOS route taps
|
||||
prefer the accessibility control and retry the measured WebView point after a
|
||||
silent WebKit miss.
|
||||
|
||||
### 10. Adapt remaining host features and native capabilities
|
||||
|
||||
|
||||
@@ -43,10 +43,25 @@ 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
|
||||
The iPhone 17 Pro / iOS 26.5 Simulator now captures the unchanged native and
|
||||
hosted Tasks and Session screens from one disposable Desktop runtime. Tasks
|
||||
passes at 0.022% changed pixels, 0.084 mean channel difference, and 0.000016
|
||||
vertical-title delta. Session passes at 0.800%, 1.693, and 0.000366
|
||||
respectively, within the 3%, 4, and 0.005 budgets. The same fresh exact-app run
|
||||
passes Agent History portrait/landscape parity, Desktop restart and recovery,
|
||||
native-touch resume, a third Session diff tab, standalone Review, and both
|
||||
isolation probes.
|
||||
|
||||
The existing non-embedded Tasks toolbar icon has no native accessibility label.
|
||||
The fixture locates its unchanged row from the accessible Filter control and
|
||||
uses the existing icon position. This is recorded as an accessibility finding,
|
||||
not treated as a reason to change product UI inside the migration.
|
||||
|
||||
The migration is rebased onto `origin/main` at `0404f27b3`. Current post-rebase
|
||||
validation passes 552 mobile files / 3,291 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.
|
||||
root/mobile/mobile-web lint, reliability gates, changed-file and full-mobile
|
||||
formatting, localization, and the max-lines ratchet pass.
|
||||
The independently verified React Native Web package is
|
||||
`f852d8525d2b0e20d79262d74ce3ef74bfa73c3e55b95176bfb1b467beafae61`:
|
||||
49 assets, 9,330,210 raw bytes, and 2,697,838 gzip bytes.
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
import { execFile } from 'node:child_process'
|
||||
import path from 'node:path'
|
||||
import { promisify } from 'node:util'
|
||||
import { dismissEmulatorDeveloperMenuIfPresent } from './emulator-developer-menu-dismissal.mjs'
|
||||
import {
|
||||
tapHostedIosAccessibilityControl,
|
||||
tapHostedIosAccessibilityControlByLabelPrefix,
|
||||
tapHostedIosPoint,
|
||||
waitForHostedIosAccessibilityControl,
|
||||
waitForHostedIosAccessibilityControlByLabelPrefix,
|
||||
waitForHostedIosAccessibilityControlMatch
|
||||
} from './hosted-ios-emulator-accessibility.mjs'
|
||||
import { assertHostedIosScreenshotParity } from './hosted-ios-screenshot-parity.mjs'
|
||||
import {
|
||||
activateHostedWebViewControl,
|
||||
readHostedWebViewTextPoint,
|
||||
waitForVisibleHostedWebView
|
||||
} from './hosted-webview-cdp-session.mjs'
|
||||
import { readHostedWebViewControlPoint } from './hosted-webview-control-point.mjs'
|
||||
import { activateHostedWorkspaceRow } from './hosted-webview-workspace-activation.mjs'
|
||||
|
||||
const execFileAsync = promisify(execFile)
|
||||
const TASKS_TOOLBAR_X = 0.87
|
||||
const TASK_STABLE_TEXTS = [
|
||||
'Connect your Linear account',
|
||||
'No Linear tasks',
|
||||
'No GitHub tasks',
|
||||
'No GitLab tasks',
|
||||
'No matching tasks',
|
||||
'Choose a GitHub project',
|
||||
'Update Orca desktop'
|
||||
]
|
||||
|
||||
export async function captureNativeCoreRouteBaselines({
|
||||
deviceUdid,
|
||||
emulator,
|
||||
expectedWorkspace,
|
||||
runtimeDirectory,
|
||||
timeoutMs
|
||||
}) {
|
||||
await dismissEmulatorDeveloperMenuIfPresent(emulator)
|
||||
const filterPoint = await waitForHostedIosAccessibilityControl(emulator, 'Filter', timeoutMs)
|
||||
// The existing non-embedded Tasks icon has no native accessibility label.
|
||||
await tapHostedIosPoint(emulator, { x: TASKS_TOOLBAR_X, y: filterPoint.y })
|
||||
const taskStableState = await waitForHostedIosAccessibilityControlMatch(
|
||||
emulator,
|
||||
TASK_STABLE_TEXTS,
|
||||
timeoutMs
|
||||
)
|
||||
const tasks = await captureNativeRoute({
|
||||
deviceUdid,
|
||||
emulator,
|
||||
runtimeDirectory,
|
||||
screenshotName: 'native-tasks-portrait.png',
|
||||
title: 'Tasks',
|
||||
timeoutMs
|
||||
})
|
||||
tasks.stableText = taskStableState.label
|
||||
await tapHostedIosPoint(emulator, tasksBackPoint(tasks.screenTitlePoint))
|
||||
await waitForHostedIosAccessibilityControlByLabelPrefix(emulator, expectedWorkspace, timeoutMs)
|
||||
await tapHostedIosAccessibilityControlByLabelPrefix(emulator, expectedWorkspace, timeoutMs)
|
||||
await waitForHostedIosAccessibilityControlByLabelPrefix(emulator, 'Mobile Emulator', timeoutMs)
|
||||
const session = await captureNativeRoute({
|
||||
deviceUdid,
|
||||
emulator,
|
||||
runtimeDirectory,
|
||||
screenshotName: 'native-session-portrait.png',
|
||||
title: expectedWorkspace,
|
||||
timeoutMs
|
||||
})
|
||||
await tapHostedIosAccessibilityControl(emulator, 'Back to worktrees', timeoutMs)
|
||||
await waitForHostedIosAccessibilityControlByLabelPrefix(emulator, expectedWorkspace, timeoutMs)
|
||||
return { session, tasks }
|
||||
}
|
||||
|
||||
export async function captureHostedCoreRouteParity({
|
||||
deviceUdid,
|
||||
discoveryUrl,
|
||||
emulator,
|
||||
expectedWorkspace,
|
||||
nativeBaselines,
|
||||
runtimeDirectory,
|
||||
timeoutMs,
|
||||
workspaceDocument
|
||||
}) {
|
||||
const tasksDocument = await openHostedTasks({
|
||||
discoveryUrl,
|
||||
emulator,
|
||||
stableText: nativeBaselines.tasks.stableText,
|
||||
timeoutMs,
|
||||
workspaceDocument
|
||||
})
|
||||
const tasks = await captureHostedRoute({
|
||||
deviceUdid,
|
||||
document: tasksDocument,
|
||||
nativeBaseline: nativeBaselines.tasks,
|
||||
runtimeDirectory,
|
||||
screenshotName: 'hosted-tasks-portrait.png',
|
||||
title: 'Tasks'
|
||||
})
|
||||
await tapHostedIosPoint(emulator, tasksBackPoint(tasks.screenTitlePoint))
|
||||
let activeWorkspaceDocument = await waitForVisibleHostedWebView({
|
||||
discoveryUrl,
|
||||
expectedText: 'Orca Desktop',
|
||||
timeoutMs
|
||||
})
|
||||
await activateHostedWorkspaceRow(
|
||||
activeWorkspaceDocument,
|
||||
expectedWorkspace,
|
||||
activateHostedWebViewControl,
|
||||
timeoutMs,
|
||||
() =>
|
||||
waitForVisibleHostedWebView({
|
||||
discoveryUrl,
|
||||
expectedText: 'Orca Desktop',
|
||||
timeoutMs
|
||||
})
|
||||
)
|
||||
const sessionDocument = await waitForVisibleHostedWebView({
|
||||
discoveryUrl,
|
||||
expectedText: 'Mobile Emulator',
|
||||
expectedHrefIncludes: '/session/',
|
||||
timeoutMs
|
||||
})
|
||||
const session = await captureHostedRoute({
|
||||
deviceUdid,
|
||||
document: sessionDocument,
|
||||
nativeBaseline: nativeBaselines.session,
|
||||
runtimeDirectory,
|
||||
screenshotName: 'hosted-session-portrait.png',
|
||||
title: expectedWorkspace
|
||||
})
|
||||
const backPoint = await readHostedWebViewControlPoint(sessionDocument, 'Back to worktrees')
|
||||
await tapHostedIosPoint(emulator, backPoint)
|
||||
activeWorkspaceDocument = await waitForVisibleHostedWebView({
|
||||
discoveryUrl,
|
||||
expectedText: 'Orca Desktop',
|
||||
timeoutMs
|
||||
})
|
||||
return {
|
||||
evidence: {
|
||||
session: coreRouteParityEvidence(nativeBaselines.session, session),
|
||||
tasks: coreRouteParityEvidence(nativeBaselines.tasks, tasks)
|
||||
},
|
||||
workspaceDocument: activeWorkspaceDocument
|
||||
}
|
||||
}
|
||||
|
||||
export function coreRouteParityEvidence(nativeCapture, hostedCapture) {
|
||||
return {
|
||||
nativeScreenshot: path.basename(nativeCapture.screenshot),
|
||||
hostedScreenshot: path.basename(hostedCapture.screenshot),
|
||||
nativeScreenTitlePoint: nativeCapture.screenTitlePoint,
|
||||
hostedScreenTitlePoint: hostedCapture.screenTitlePoint,
|
||||
screenshotParity: hostedCapture.screenshotParity
|
||||
}
|
||||
}
|
||||
|
||||
async function openHostedTasks({
|
||||
discoveryUrl,
|
||||
emulator,
|
||||
stableText,
|
||||
timeoutMs,
|
||||
workspaceDocument
|
||||
}) {
|
||||
const filterPoint = await readHostedWebViewTextPoint(workspaceDocument, 'Filter')
|
||||
await tapHostedIosPoint(emulator, { x: TASKS_TOOLBAR_X, y: filterPoint.y })
|
||||
return waitForVisibleHostedWebView({
|
||||
discoveryUrl,
|
||||
expectedText: stableText,
|
||||
expectedHrefIncludes: '/tasks',
|
||||
timeoutMs
|
||||
})
|
||||
}
|
||||
|
||||
async function captureNativeRoute({
|
||||
deviceUdid,
|
||||
emulator,
|
||||
runtimeDirectory,
|
||||
screenshotName,
|
||||
title,
|
||||
timeoutMs
|
||||
}) {
|
||||
const screenTitlePoint = await waitForHostedIosAccessibilityControl(emulator, title, timeoutMs)
|
||||
await delay(500)
|
||||
const screenshot = path.join(runtimeDirectory, screenshotName)
|
||||
await captureSimulatorScreenshot(deviceUdid, screenshot)
|
||||
return { screenTitlePoint, screenshot }
|
||||
}
|
||||
|
||||
async function captureHostedRoute({
|
||||
deviceUdid,
|
||||
document,
|
||||
nativeBaseline,
|
||||
runtimeDirectory,
|
||||
screenshotName,
|
||||
title
|
||||
}) {
|
||||
const screenTitlePoint = await readHostedWebViewTextPoint(document, title)
|
||||
await delay(500)
|
||||
const screenshot = path.join(runtimeDirectory, screenshotName)
|
||||
await captureSimulatorScreenshot(deviceUdid, screenshot)
|
||||
const capture = { screenTitlePoint, screenshot }
|
||||
capture.screenshotParity = await assertHostedIosScreenshotParity({
|
||||
hostedLandmark: capture.screenTitlePoint,
|
||||
hostedScreenshot: capture.screenshot,
|
||||
nativeLandmark: nativeBaseline.screenTitlePoint,
|
||||
nativeScreenshot: nativeBaseline.screenshot
|
||||
})
|
||||
return capture
|
||||
}
|
||||
|
||||
function tasksBackPoint(titlePoint) {
|
||||
return { x: Math.max(0.04, titlePoint.x - 0.12), y: titlePoint.y }
|
||||
}
|
||||
|
||||
async function captureSimulatorScreenshot(deviceUdid, outputPath) {
|
||||
await execFileAsync('xcrun', ['simctl', 'io', deviceUdid, 'screenshot', outputPath])
|
||||
}
|
||||
|
||||
function delay(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
@@ -3,7 +3,10 @@ import {
|
||||
waitForVisibleHostedWebView
|
||||
} from './hosted-webview-cdp-session.mjs'
|
||||
import { readHostedWebViewControlPoint } from './hosted-webview-control-point.mjs'
|
||||
import { tapHostedIosPoint } from './hosted-ios-emulator-accessibility.mjs'
|
||||
import {
|
||||
tapHostedIosAccessibilityControl,
|
||||
tapHostedIosPoint
|
||||
} from './hosted-ios-emulator-accessibility.mjs'
|
||||
import { navigateHostedWebViewRoute } from './hosted-webview-route-navigation.mjs'
|
||||
|
||||
export async function verifyHostedSourceControlReviewJourney({
|
||||
@@ -12,7 +15,7 @@ export async function verifyHostedSourceControlReviewJourney({
|
||||
sessionDocument,
|
||||
timeoutMs,
|
||||
expectedSessionDiffText = '2 tabs',
|
||||
tapPoint = tapHostedIosPoint
|
||||
tapPoint = tapHostedJourneyPoint
|
||||
}) {
|
||||
const sourceControl = await journeyStep('wait for Source Control route', () =>
|
||||
openSourceControlRoute({
|
||||
@@ -38,18 +41,14 @@ export async function verifyHostedSourceControlReviewJourney({
|
||||
if (!changedFileLabel) {
|
||||
throw new Error('Source Control has no changed file available for Review.')
|
||||
}
|
||||
const changedFilePoint = await journeyStep(`measure ${changedFileLabel}`, () =>
|
||||
readHostedWebViewControlPoint(sourceControl, changedFileLabel)
|
||||
)
|
||||
await journeyStep(`tap ${changedFileLabel}`, () =>
|
||||
tapPoint(emulator, changedFilePoint, changedFileLabel)
|
||||
)
|
||||
const sessionDiff = await journeyStep('wait for Session diff route', () =>
|
||||
waitForVisibleHostedWebView({
|
||||
openSessionDiffRoute({
|
||||
discoveryUrl,
|
||||
emulator,
|
||||
expectedText: expectedSessionDiffText,
|
||||
expectedHrefIncludes: '/session/',
|
||||
requireInteractiveControls: false,
|
||||
label: changedFileLabel,
|
||||
sourceControl,
|
||||
tapPoint,
|
||||
timeoutMs
|
||||
})
|
||||
)
|
||||
@@ -80,6 +79,17 @@ export async function verifyHostedSourceControlReviewJourney({
|
||||
}
|
||||
}
|
||||
|
||||
async function tapHostedJourneyPoint(emulator, point, label, attempt = 0) {
|
||||
if (label && attempt === 0) {
|
||||
try {
|
||||
return await tapHostedIosAccessibilityControl(emulator, label, 5_000)
|
||||
} catch {
|
||||
// WebKit can omit a descendant while refreshing its accessibility tree.
|
||||
}
|
||||
}
|
||||
return tapHostedIosPoint(emulator, point)
|
||||
}
|
||||
|
||||
async function openSourceControlRoute({
|
||||
discoveryUrl,
|
||||
emulator,
|
||||
@@ -91,7 +101,7 @@ async function openSourceControlRoute({
|
||||
for (let attempt = 0; attempt < 3; attempt += 1) {
|
||||
try {
|
||||
const point = await readHostedWebViewControlPoint(sessionDocument, 'Open source control')
|
||||
await tapPoint(emulator, point, 'Open source control')
|
||||
await tapPoint(emulator, point, 'Open source control', attempt)
|
||||
return await waitForVisibleHostedWebView({
|
||||
discoveryUrl,
|
||||
expectedText: 'Source Control',
|
||||
@@ -105,6 +115,47 @@ async function openSourceControlRoute({
|
||||
throw lastError
|
||||
}
|
||||
|
||||
async function openSessionDiffRoute({
|
||||
discoveryUrl,
|
||||
emulator,
|
||||
expectedText,
|
||||
label,
|
||||
sourceControl,
|
||||
tapPoint,
|
||||
timeoutMs
|
||||
}) {
|
||||
let lastError = new Error('Session diff route did not open')
|
||||
for (let attempt = 0; attempt < 3; attempt += 1) {
|
||||
try {
|
||||
const point = await readHostedWebViewControlPoint(sourceControl, label)
|
||||
await tapPoint(emulator, point, label, attempt)
|
||||
} catch (error) {
|
||||
lastError = error
|
||||
continue
|
||||
}
|
||||
try {
|
||||
return await waitForSessionDiff(discoveryUrl, expectedText, Math.min(timeoutMs, 3_000))
|
||||
} catch (error) {
|
||||
lastError = error
|
||||
}
|
||||
const transitioned = await waitForSessionDiff(discoveryUrl, '', 1_000).catch(() => null)
|
||||
if (transitioned) {
|
||||
return waitForSessionDiff(discoveryUrl, expectedText, timeoutMs)
|
||||
}
|
||||
}
|
||||
throw lastError
|
||||
}
|
||||
|
||||
function waitForSessionDiff(discoveryUrl, expectedText, timeoutMs) {
|
||||
return waitForVisibleHostedWebView({
|
||||
discoveryUrl,
|
||||
expectedText,
|
||||
expectedHrefIncludes: '/session/',
|
||||
requireInteractiveControls: false,
|
||||
timeoutMs
|
||||
})
|
||||
}
|
||||
|
||||
async function waitForChangedFileState(document, timeoutMs) {
|
||||
const deadline = Date.now() + timeoutMs
|
||||
let state
|
||||
|
||||
@@ -17,6 +17,10 @@ import {
|
||||
waitForVisibleHostedWebView
|
||||
} from './hosted-webview-cdp-session.mjs'
|
||||
import { captureNativeAgentHistoryBaseline } from './hosted-ios-agent-history-parity.mjs'
|
||||
import {
|
||||
captureHostedCoreRouteParity,
|
||||
captureNativeCoreRouteBaselines
|
||||
} from './hosted-ios-core-route-parity.mjs'
|
||||
import { verifyHostedAgentHistoryJourney } from './hosted-ios-agent-history-journey.mjs'
|
||||
import { openHostedIosHybridRoute } from './hosted-ios-hybrid-route-handoff.mjs'
|
||||
import { verifyHostedNativeTerminalSettingsHandoff } from './hosted-ios-native-settings-handoff.mjs'
|
||||
@@ -95,6 +99,18 @@ async function main() {
|
||||
const nativeOnboarding = await evidenceStep('native onboarding', () =>
|
||||
completeHostedIosNativeOnboarding(emulator, expectedWorkspace, options.timeoutMs)
|
||||
)
|
||||
const nativeCoreRoutes =
|
||||
options.securityOnly || options.nativeSettingsOnly || options.sourceControlOnly
|
||||
? null
|
||||
: await evidenceStep('native Tasks and Session baselines', () =>
|
||||
captureNativeCoreRouteBaselines({
|
||||
deviceUdid,
|
||||
emulator,
|
||||
expectedWorkspace,
|
||||
runtimeDirectory,
|
||||
timeoutMs: options.timeoutMs
|
||||
})
|
||||
)
|
||||
const nativeAgentHistory =
|
||||
options.securityOnly || options.nativeSettingsOnly || options.sourceControlOnly
|
||||
? null
|
||||
@@ -117,6 +133,22 @@ async function main() {
|
||||
expectedText: 'Orca Desktop',
|
||||
timeoutMs: options.timeoutMs
|
||||
})
|
||||
const hostedCoreRoutes =
|
||||
options.securityOnly || options.nativeSettingsOnly || options.sourceControlOnly
|
||||
? null
|
||||
: await evidenceStep('hosted Tasks and Session parity', () =>
|
||||
captureHostedCoreRouteParity({
|
||||
deviceUdid,
|
||||
discoveryUrl: `http://127.0.0.1:${inspectorPort}`,
|
||||
emulator,
|
||||
expectedWorkspace,
|
||||
nativeBaselines: nativeCoreRoutes,
|
||||
runtimeDirectory,
|
||||
timeoutMs: options.timeoutMs,
|
||||
workspaceDocument
|
||||
})
|
||||
)
|
||||
const activeWorkspaceDocument = hostedCoreRoutes?.workspaceDocument ?? workspaceDocument
|
||||
const historyEvidence =
|
||||
options.securityOnly || options.sourceControlOnly
|
||||
? null
|
||||
@@ -125,7 +157,7 @@ async function main() {
|
||||
verifyNativeSettingsJourney({
|
||||
discoveryUrl: `http://127.0.0.1:${inspectorPort}`,
|
||||
emulator,
|
||||
workspaceDocument,
|
||||
workspaceDocument: activeWorkspaceDocument,
|
||||
expectedWorkspace,
|
||||
timeoutMs: options.timeoutMs
|
||||
})
|
||||
@@ -137,7 +169,7 @@ async function main() {
|
||||
emulator,
|
||||
nativeAgentHistory,
|
||||
runtimeDirectory,
|
||||
workspaceDocument,
|
||||
workspaceDocument: activeWorkspaceDocument,
|
||||
expectedWorkspace,
|
||||
timeoutMs: options.timeoutMs
|
||||
})
|
||||
@@ -170,6 +202,7 @@ async function main() {
|
||||
return verifyHostedSourceControlReviewJourney({
|
||||
discoveryUrl: `http://127.0.0.1:${inspectorPort}`,
|
||||
emulator,
|
||||
expectedSessionDiffText: options.sourceControlOnly ? '2 tabs' : '3 tabs',
|
||||
sessionDocument,
|
||||
timeoutMs: options.timeoutMs
|
||||
})
|
||||
@@ -214,6 +247,7 @@ async function main() {
|
||||
navigationIsolation,
|
||||
nativeOnboarding,
|
||||
agentHistory: historyEvidence,
|
||||
coreRouteParity: hostedCoreRoutes?.evidence ?? null,
|
||||
sourceControlReview
|
||||
},
|
||||
null,
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
activateControl: vi.fn(),
|
||||
activateWorkspace: vi.fn(),
|
||||
captureScreenshot: vi.fn(),
|
||||
compareScreenshots: vi.fn(),
|
||||
dismissDeveloperMenu: vi.fn(),
|
||||
readControlPoint: vi.fn(),
|
||||
readTextPoint: vi.fn(),
|
||||
tapControl: vi.fn(),
|
||||
tapControlByPrefix: vi.fn(),
|
||||
tapPoint: vi.fn(),
|
||||
waitForControl: vi.fn(),
|
||||
waitForControlByPrefix: vi.fn(),
|
||||
waitForControlMatch: vi.fn(),
|
||||
waitForDocument: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('node:child_process', () => ({
|
||||
execFile: mocks.captureScreenshot
|
||||
}))
|
||||
vi.mock('../../scripts/emulator-developer-menu-dismissal.mjs', () => ({
|
||||
dismissEmulatorDeveloperMenuIfPresent: mocks.dismissDeveloperMenu
|
||||
}))
|
||||
vi.mock('../../scripts/hosted-ios-emulator-accessibility.mjs', () => ({
|
||||
tapHostedIosAccessibilityControl: mocks.tapControl,
|
||||
tapHostedIosAccessibilityControlByLabelPrefix: mocks.tapControlByPrefix,
|
||||
tapHostedIosPoint: mocks.tapPoint,
|
||||
waitForHostedIosAccessibilityControl: mocks.waitForControl,
|
||||
waitForHostedIosAccessibilityControlByLabelPrefix: mocks.waitForControlByPrefix,
|
||||
waitForHostedIosAccessibilityControlMatch: mocks.waitForControlMatch
|
||||
}))
|
||||
vi.mock('../../scripts/hosted-ios-screenshot-parity.mjs', () => ({
|
||||
assertHostedIosScreenshotParity: mocks.compareScreenshots
|
||||
}))
|
||||
vi.mock('../../scripts/hosted-webview-cdp-session.mjs', () => ({
|
||||
activateHostedWebViewControl: mocks.activateControl,
|
||||
readHostedWebViewTextPoint: mocks.readTextPoint,
|
||||
waitForVisibleHostedWebView: mocks.waitForDocument
|
||||
}))
|
||||
vi.mock('../../scripts/hosted-webview-control-point.mjs', () => ({
|
||||
readHostedWebViewControlPoint: mocks.readControlPoint
|
||||
}))
|
||||
vi.mock('../../scripts/hosted-webview-workspace-activation.mjs', () => ({
|
||||
activateHostedWorkspaceRow: mocks.activateWorkspace
|
||||
}))
|
||||
|
||||
import {
|
||||
captureHostedCoreRouteParity,
|
||||
captureNativeCoreRouteBaselines
|
||||
} from '../../scripts/hosted-ios-core-route-parity.mjs'
|
||||
|
||||
describe('hosted iOS core-route parity', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mocks.captureScreenshot.mockImplementation((_command, _args, callback) =>
|
||||
callback(null, '', '')
|
||||
)
|
||||
mocks.compareScreenshots.mockResolvedValue({ changedPixelRatio: 0.01 })
|
||||
mocks.readControlPoint.mockResolvedValue({ x: 0.8, y: 0.1 })
|
||||
mocks.readTextPoint.mockResolvedValue({ x: 0.2, y: 0.1 })
|
||||
mocks.tapControl.mockResolvedValue(undefined)
|
||||
mocks.tapControlByPrefix.mockResolvedValue(undefined)
|
||||
mocks.tapPoint.mockResolvedValue(undefined)
|
||||
mocks.waitForControl.mockResolvedValue({ x: 0.2, y: 0.1 })
|
||||
mocks.waitForControlByPrefix.mockResolvedValue({ x: 0.2, y: 0.1 })
|
||||
mocks.waitForControlMatch.mockResolvedValue({
|
||||
label: 'Connect your Linear account',
|
||||
x: 0.5,
|
||||
y: 0.5
|
||||
})
|
||||
mocks.waitForDocument.mockImplementation(({ expectedHrefIncludes }) => ({
|
||||
href: `orca-mobile-web://build${expectedHrefIncludes ?? '/h/host'}`
|
||||
}))
|
||||
})
|
||||
|
||||
it('captures native Tasks and Session from the same presentation source', async () => {
|
||||
const baselines = await captureNativeCoreRouteBaselines({
|
||||
deviceUdid: 'simulator',
|
||||
emulator: { deviceUdid: 'simulator' },
|
||||
expectedWorkspace: 'mobile-rearch',
|
||||
runtimeDirectory: '/tmp/parity',
|
||||
timeoutMs: 30_000
|
||||
})
|
||||
|
||||
expect(baselines.tasks.stableText).toBe('Connect your Linear account')
|
||||
expect(mocks.tapPoint).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
{ deviceUdid: 'simulator' },
|
||||
{ x: 0.87, y: 0.1 }
|
||||
)
|
||||
expect(mocks.tapPoint).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
{ deviceUdid: 'simulator' },
|
||||
expect.objectContaining({ y: 0.1 })
|
||||
)
|
||||
expect(mocks.tapPoint.mock.calls[1][1].x).toBeCloseTo(0.08)
|
||||
expect(mocks.tapControl).toHaveBeenCalledWith(
|
||||
{ deviceUdid: 'simulator' },
|
||||
'Back to worktrees',
|
||||
30_000
|
||||
)
|
||||
expect(baselines.session.screenshot).toBe('/tmp/parity/native-session-portrait.png')
|
||||
})
|
||||
|
||||
it('compares hosted Tasks and Session and returns to the workspace route', async () => {
|
||||
const nativeBaselines = {
|
||||
tasks: {
|
||||
screenTitlePoint: { x: 0.2, y: 0.1 },
|
||||
screenshot: '/tmp/parity/native-tasks-portrait.png',
|
||||
stableText: 'Connect your Linear account'
|
||||
},
|
||||
session: {
|
||||
screenTitlePoint: { x: 0.2, y: 0.1 },
|
||||
screenshot: '/tmp/parity/native-session-portrait.png'
|
||||
}
|
||||
}
|
||||
|
||||
const result = await captureHostedCoreRouteParity({
|
||||
deviceUdid: 'simulator',
|
||||
discoveryUrl: 'http://127.0.0.1:9222',
|
||||
emulator: { deviceUdid: 'simulator' },
|
||||
expectedWorkspace: 'mobile-rearch',
|
||||
nativeBaselines,
|
||||
runtimeDirectory: '/tmp/parity',
|
||||
timeoutMs: 30_000,
|
||||
workspaceDocument: { href: 'orca-mobile-web://build/h/host' }
|
||||
})
|
||||
|
||||
expect(result.evidence.tasks.screenshotParity).toEqual({
|
||||
changedPixelRatio: 0.01
|
||||
})
|
||||
expect(result.evidence.session.screenshotParity).toEqual({
|
||||
changedPixelRatio: 0.01
|
||||
})
|
||||
expect(result.workspaceDocument.href).toContain('/h/host')
|
||||
expect(mocks.activateWorkspace).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
'mobile-rearch',
|
||||
mocks.activateControl,
|
||||
30_000,
|
||||
expect.any(Function)
|
||||
)
|
||||
expect(mocks.readTextPoint).toHaveBeenCalledWith(expect.anything(), 'Filter')
|
||||
expect(mocks.readControlPoint).toHaveBeenCalledWith(expect.anything(), 'Back to worktrees')
|
||||
expect(mocks.compareScreenshots).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
@@ -4,6 +4,7 @@ const mocks = vi.hoisted(() => ({
|
||||
navigateRoute: vi.fn(),
|
||||
readControlPoint: vi.fn(),
|
||||
readState: vi.fn(),
|
||||
tapAccessibilityControl: vi.fn(),
|
||||
tapPoint: vi.fn(),
|
||||
waitForDocument: vi.fn()
|
||||
}))
|
||||
@@ -18,6 +19,7 @@ vi.mock('../../scripts/hosted-webview-control-point.mjs', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('../../scripts/hosted-ios-emulator-accessibility.mjs', () => ({
|
||||
tapHostedIosAccessibilityControl: mocks.tapAccessibilityControl,
|
||||
tapHostedIosPoint: mocks.tapPoint
|
||||
}))
|
||||
|
||||
@@ -34,6 +36,7 @@ describe('hosted iOS Source Control and Review journey', () => {
|
||||
.mockResolvedValueOnce({ x: 0.4, y: 0.2 })
|
||||
.mockResolvedValueOnce({ x: 0.5, y: 0.4 })
|
||||
mocks.navigateRoute.mockResolvedValue(undefined)
|
||||
mocks.tapAccessibilityControl.mockResolvedValue(undefined)
|
||||
mocks.tapPoint.mockResolvedValue(undefined)
|
||||
mocks.waitForDocument
|
||||
.mockResolvedValueOnce({
|
||||
@@ -85,18 +88,19 @@ describe('hosted iOS Source Control and Review journey', () => {
|
||||
sessionDocument,
|
||||
'Open source control'
|
||||
)
|
||||
expect(mocks.tapPoint).toHaveBeenNthCalledWith(
|
||||
expect(mocks.tapAccessibilityControl).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
emulator,
|
||||
{ x: 0.4, y: 0.2 },
|
||||
'Open source control'
|
||||
'Open source control',
|
||||
5_000
|
||||
)
|
||||
expect(mocks.tapPoint).toHaveBeenNthCalledWith(
|
||||
expect(mocks.tapAccessibilityControl).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
emulator,
|
||||
{ x: 0.5, y: 0.4 },
|
||||
'Open changed file mobile/app/index.tsx'
|
||||
'Open changed file mobile/app/index.tsx',
|
||||
5_000
|
||||
)
|
||||
expect(mocks.tapPoint).not.toHaveBeenCalled()
|
||||
expect(mocks.navigateRoute).toHaveBeenCalledWith(
|
||||
{ href: 'orca-mobile-web://build/h/host/session/workspace?name=repo' },
|
||||
'/h/host/review/workspace?scope=all&name=repo'
|
||||
@@ -127,4 +131,99 @@ describe('hosted iOS Source Control and Review journey', () => {
|
||||
expect.objectContaining({ expectedText: '3 tabs' })
|
||||
)
|
||||
})
|
||||
|
||||
it('falls back to measured points when WebKit omits accessibility descendants', async () => {
|
||||
mocks.tapAccessibilityControl.mockRejectedValue(new Error('missing descendant'))
|
||||
|
||||
await verifyHostedSourceControlReviewJourney({
|
||||
discoveryUrl: 'http://127.0.0.1:9222',
|
||||
emulator: { deviceUdid: 'simulator' },
|
||||
sessionDocument: {
|
||||
href: 'orca-mobile-web://build/h/host/session/workspace'
|
||||
},
|
||||
timeoutMs: 30_000
|
||||
})
|
||||
|
||||
expect(mocks.tapPoint).toHaveBeenNthCalledWith(1, expect.anything(), {
|
||||
x: 0.4,
|
||||
y: 0.2
|
||||
})
|
||||
expect(mocks.tapPoint).toHaveBeenNthCalledWith(2, expect.anything(), {
|
||||
x: 0.5,
|
||||
y: 0.4
|
||||
})
|
||||
})
|
||||
|
||||
it('falls back to a measured point when an accessibility tap silently misses', async () => {
|
||||
mocks.readControlPoint
|
||||
.mockReset()
|
||||
.mockResolvedValueOnce({ x: 0.4, y: 0.2 })
|
||||
.mockResolvedValueOnce({ x: 0.5, y: 0.4 })
|
||||
.mockResolvedValueOnce({ x: 0.5, y: 0.4 })
|
||||
mocks.waitForDocument
|
||||
.mockReset()
|
||||
.mockResolvedValueOnce({
|
||||
href: 'orca-mobile-web://build/h/host/source-control/workspace?name=repo&origin=session'
|
||||
})
|
||||
.mockRejectedValueOnce(new Error('route unchanged'))
|
||||
.mockRejectedValueOnce(new Error('route unchanged'))
|
||||
.mockResolvedValueOnce({
|
||||
href: 'orca-mobile-web://build/h/host/session/workspace?name=repo'
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
href: 'orca-mobile-web://build/h/host/review/workspace'
|
||||
})
|
||||
|
||||
await verifyHostedSourceControlReviewJourney({
|
||||
discoveryUrl: 'http://127.0.0.1:9222',
|
||||
emulator: { deviceUdid: 'simulator' },
|
||||
sessionDocument: {
|
||||
href: 'orca-mobile-web://build/h/host/session/workspace'
|
||||
},
|
||||
timeoutMs: 30_000
|
||||
})
|
||||
|
||||
expect(mocks.tapAccessibilityControl).toHaveBeenCalledTimes(2)
|
||||
expect(mocks.tapPoint).toHaveBeenCalledWith(expect.anything(), {
|
||||
x: 0.5,
|
||||
y: 0.4
|
||||
})
|
||||
})
|
||||
|
||||
it('waits for tab state after the route changes', async () => {
|
||||
const sessionDocument = {
|
||||
href: 'orca-mobile-web://build/h/host/session/workspace?name=repo'
|
||||
}
|
||||
mocks.waitForDocument
|
||||
.mockReset()
|
||||
.mockResolvedValueOnce({
|
||||
href: 'orca-mobile-web://build/h/host/source-control/workspace?name=repo&origin=session'
|
||||
})
|
||||
.mockRejectedValueOnce(new Error('tab count pending'))
|
||||
.mockResolvedValueOnce(sessionDocument)
|
||||
.mockResolvedValueOnce(sessionDocument)
|
||||
.mockResolvedValueOnce({
|
||||
href: 'orca-mobile-web://build/h/host/review/workspace'
|
||||
})
|
||||
|
||||
await verifyHostedSourceControlReviewJourney({
|
||||
discoveryUrl: 'http://127.0.0.1:9222',
|
||||
emulator: { deviceUdid: 'simulator' },
|
||||
sessionDocument: {
|
||||
href: 'orca-mobile-web://build/h/host/session/workspace'
|
||||
},
|
||||
timeoutMs: 30_000
|
||||
})
|
||||
|
||||
expect(mocks.tapAccessibilityControl).toHaveBeenCalledTimes(2)
|
||||
expect(mocks.tapPoint).not.toHaveBeenCalled()
|
||||
expect(mocks.waitForDocument).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
expect.objectContaining({ expectedText: '' })
|
||||
)
|
||||
expect(mocks.waitForDocument).toHaveBeenNthCalledWith(
|
||||
4,
|
||||
expect.objectContaining({ expectedText: '2 tabs' })
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user