mirror of
https://github.com/stablyai/orca.git
synced 2026-10-01 08:01:56 +00:00
test(mobile): add iOS files preview parity
This commit is contained in:
@@ -2458,4 +2458,7 @@ copy.
|
||||
| 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 | Complete | The deterministic iOS fixture now opens the real `Casks/orca.rb` from the unchanged Files UI into the unchanged Preview UI. Files passes at 0.030% changed pixels / 0.128 mean channel difference and Preview at 0.061% / 0.274 against 3% / 4 budgets. Equivalent hosted route values retain one logical preview load, and RNW uses the existing native iOS font fallback. |
|
||||
| 2026-07-28 | Complete | Both the cached-app and fresh Xcode build/install complete journeys pass Files/Preview parity plus Tasks, Session, Agent History portrait/landscape, Desktop restart/E2EE recovery, native-touch resume, Source Control, third diff tab, standalone Review, and private-origin isolation. |
|
||||
| 2026-07-28 | Complete | RNW package `9e5e807523e8b917fef68f221cc1fd2e1a16dbe07d7077e717238eed17003b52` verifies with 49 assets, 9,330,604 raw bytes, and 2,697,919 gzip bytes. The mobile suite passes 553 files / 3,297 tests with 2 expected skips; all typechecks and repository quality gates pass. This slice's root suite hit an unrelated timeout that passed in isolation, so the prior complete root-suite checkpoint remains authoritative. |
|
||||
| 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. |
|
||||
|
||||
@@ -486,6 +486,14 @@ 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 iOS fixture now also navigates the unchanged Files tree into the real
|
||||
`Casks/orca.rb` Preview in native and hosted modes. Files measures 0.030%
|
||||
changed pixels / 0.128 mean channel difference and Preview measures 0.061% /
|
||||
0.274, within the 3% / 4 budgets. Both the cached-app and fresh Xcode
|
||||
build/install complete journeys pass. Hosted Preview source identity is stable
|
||||
across equivalent route rerenders, while web typography follows the existing
|
||||
native iOS fallback rather than introducing a replacement presentation.
|
||||
|
||||
The interrupted-transcript versus hook-status mismatch and a real structured
|
||||
prompt response pass Host 37 Simulator replay. Current package `4b7df7d4…`
|
||||
also carries the network-denied local Mermaid engine and its WebKit-compatible
|
||||
|
||||
@@ -1851,6 +1851,15 @@ failure, or fatal process error. The final post-rebase run uses independently
|
||||
verified build
|
||||
`f852d8525d2b0e20d79262d74ce3ef74bfa73c3e55b95176bfb1b467beafae61`.
|
||||
|
||||
The deterministic iOS fixture now covers the full Files-to-Preview transition
|
||||
with the real `Casks/orca.rb` content. Native and hosted use the same
|
||||
`MobileFilePreviewScreen`; equivalent route values retain one logical preview
|
||||
load, and RNW follows the native iOS font fallback. Files and Preview pass
|
||||
strict screenshot parity in both cached-app and fresh Xcode build/install
|
||||
complete journeys. The independently verified package containing this work is
|
||||
`9e5e807523e8b917fef68f221cc1fd2e1a16dbe07d7077e717238eed17003b52`:
|
||||
49 assets, 9,330,604 raw bytes, and 2,697,919 gzip bytes.
|
||||
|
||||
The host-only router now imports the existing
|
||||
`mobile/app/h/[hostId]/session/[worktreeId].tsx` route directly; a
|
||||
source-identity test prevents that wrapper from growing copied presentation.
|
||||
|
||||
@@ -57,14 +57,27 @@ 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 same exact-app iOS gate now covers the unchanged Files route and a real
|
||||
Preview navigation through `Casks/orca.rb`. Files passes at 0.030% changed
|
||||
pixels and 0.128 mean channel difference; Preview passes at 0.061% and 0.274.
|
||||
Both cached-app and fresh Xcode build/install journeys pass the complete route,
|
||||
recovery, review, and isolation matrix. Equivalent hosted route values no
|
||||
longer restart Preview loads, and RNW preserves the native iOS font fallback.
|
||||
|
||||
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, 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.
|
||||
`9e5e807523e8b917fef68f221cc1fd2e1a16dbe07d7077e717238eed17003b52`:
|
||||
49 assets, 9,330,604 raw bytes, and 2,697,919 gzip bytes. The current mobile
|
||||
suite passes 553 files / 3,297 tests with 2 expected skips; all project
|
||||
typechecks and repository-wide quality gates pass. A fresh root-suite attempt
|
||||
hit an unrelated 30-second timeout in
|
||||
`project-view-wrapper-source-context-boundary.test.ts` and was interrupted
|
||||
under concurrent test load; that exact file passes alone, so the prior complete
|
||||
root-suite checkpoint above remains authoritative.
|
||||
|
||||
## 1. Finish Hosted Feature Parity
|
||||
|
||||
|
||||
@@ -130,8 +130,7 @@ export async function captureHostedCoreRouteParity({
|
||||
screenshotName: 'hosted-session-portrait.png',
|
||||
title: expectedWorkspace
|
||||
})
|
||||
const backPoint = await readHostedWebViewControlPoint(sessionDocument, 'Back to worktrees')
|
||||
await tapHostedIosPoint(emulator, backPoint)
|
||||
await tapHostedSessionBack(emulator, sessionDocument, timeoutMs)
|
||||
activeWorkspaceDocument = await waitForVisibleHostedWebView({
|
||||
discoveryUrl,
|
||||
expectedText: 'Orca Desktop',
|
||||
@@ -214,6 +213,19 @@ function tasksBackPoint(titlePoint) {
|
||||
return { x: Math.max(0.04, titlePoint.x - 0.12), y: titlePoint.y }
|
||||
}
|
||||
|
||||
async function tapHostedSessionBack(emulator, document, timeoutMs) {
|
||||
try {
|
||||
await tapHostedIosAccessibilityControl(
|
||||
emulator,
|
||||
'Back to worktrees',
|
||||
Math.min(timeoutMs, 5_000)
|
||||
)
|
||||
} catch {
|
||||
const backPoint = await readHostedWebViewControlPoint(document, 'Back to worktrees')
|
||||
await tapHostedIosPoint(emulator, backPoint)
|
||||
}
|
||||
}
|
||||
|
||||
async function captureSimulatorScreenshot(deviceUdid, outputPath) {
|
||||
await execFileAsync('xcrun', ['simctl', 'io', deviceUdid, 'screenshot', outputPath])
|
||||
}
|
||||
|
||||
@@ -0,0 +1,308 @@
|
||||
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
|
||||
} from './hosted-ios-emulator-accessibility.mjs'
|
||||
import { assertHostedIosScreenshotParity } from './hosted-ios-screenshot-parity.mjs'
|
||||
import {
|
||||
activateHostedWebViewControl,
|
||||
readHostedWebViewState,
|
||||
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 FILES_STABLE_LABEL = 'Open folder Casks'
|
||||
const PREVIEW_FILE_LABEL = 'Preview file orca.rb'
|
||||
const PREVIEW_STABLE_LABEL = 'File preview'
|
||||
const PREVIEW_STABLE_TEXT = 'cask "orca" do'
|
||||
|
||||
export async function captureNativeFilesPreviewBaselines({
|
||||
deviceUdid,
|
||||
emulator,
|
||||
expectedWorkspace,
|
||||
runtimeDirectory,
|
||||
timeoutMs
|
||||
}) {
|
||||
await dismissEmulatorDeveloperMenuIfPresent(emulator)
|
||||
await tapHostedIosAccessibilityControlByLabelPrefix(emulator, expectedWorkspace, timeoutMs)
|
||||
await waitForHostedIosAccessibilityControlByLabelPrefix(emulator, 'Mobile Emulator', timeoutMs)
|
||||
await tapHostedIosAccessibilityControl(emulator, 'Open file explorer', timeoutMs)
|
||||
await waitForHostedIosAccessibilityControl(emulator, FILES_STABLE_LABEL, timeoutMs)
|
||||
const files = await captureNativeRoute({
|
||||
deviceUdid,
|
||||
emulator,
|
||||
runtimeDirectory,
|
||||
screenshotName: 'native-files-portrait.png',
|
||||
title: 'Files',
|
||||
timeoutMs
|
||||
})
|
||||
await tapHostedIosAccessibilityControl(emulator, FILES_STABLE_LABEL, timeoutMs)
|
||||
await tapHostedIosAccessibilityControl(emulator, PREVIEW_FILE_LABEL, timeoutMs)
|
||||
await waitForHostedIosAccessibilityControl(emulator, PREVIEW_STABLE_LABEL, timeoutMs)
|
||||
const preview = await captureNativeRoute({
|
||||
deviceUdid,
|
||||
emulator,
|
||||
runtimeDirectory,
|
||||
screenshotName: 'native-file-preview-portrait.png',
|
||||
title: 'orca.rb',
|
||||
timeoutMs
|
||||
})
|
||||
await returnNativeToWorkspaces({ emulator, expectedWorkspace, timeoutMs })
|
||||
return { files, preview }
|
||||
}
|
||||
|
||||
export async function captureHostedFilesPreviewParity({
|
||||
deviceUdid,
|
||||
discoveryUrl,
|
||||
emulator,
|
||||
expectedWorkspace,
|
||||
nativeBaselines,
|
||||
runtimeDirectory,
|
||||
timeoutMs,
|
||||
workspaceDocument
|
||||
}) {
|
||||
await activateHostedWorkspaceRow(
|
||||
workspaceDocument,
|
||||
expectedWorkspace,
|
||||
activateHostedWebViewControl,
|
||||
timeoutMs,
|
||||
() =>
|
||||
waitForVisibleHostedWebView({
|
||||
discoveryUrl,
|
||||
expectedText: 'Orca Desktop',
|
||||
timeoutMs
|
||||
})
|
||||
)
|
||||
const sessionDocument = await waitForVisibleHostedWebView({
|
||||
discoveryUrl,
|
||||
expectedText: 'Mobile Emulator',
|
||||
expectedHrefIncludes: '/session/',
|
||||
timeoutMs
|
||||
})
|
||||
const filesDocument = await tapForHostedTransition({
|
||||
document: sessionDocument,
|
||||
emulator,
|
||||
label: 'Open file explorer',
|
||||
timeoutMs,
|
||||
resolve: (transitionTimeoutMs) =>
|
||||
waitForVisibleHostedWebView({
|
||||
discoveryUrl,
|
||||
expectedText: 'Files',
|
||||
expectedHrefIncludes: '/files/',
|
||||
timeoutMs: transitionTimeoutMs
|
||||
})
|
||||
})
|
||||
await waitForHostedLabel(filesDocument, FILES_STABLE_LABEL, timeoutMs)
|
||||
const files = await captureHostedRoute({
|
||||
deviceUdid,
|
||||
document: filesDocument,
|
||||
nativeBaseline: nativeBaselines.files,
|
||||
runtimeDirectory,
|
||||
screenshotName: 'hosted-files-portrait.png',
|
||||
title: 'Files',
|
||||
timeoutMs
|
||||
})
|
||||
await tapForHostedTransition({
|
||||
document: filesDocument,
|
||||
emulator,
|
||||
label: FILES_STABLE_LABEL,
|
||||
timeoutMs,
|
||||
resolve: (transitionTimeoutMs) =>
|
||||
waitForHostedLabel(filesDocument, PREVIEW_FILE_LABEL, transitionTimeoutMs)
|
||||
})
|
||||
await tapForHostedTransition({
|
||||
document: filesDocument,
|
||||
emulator,
|
||||
label: PREVIEW_FILE_LABEL,
|
||||
timeoutMs,
|
||||
resolve: (transitionTimeoutMs) =>
|
||||
waitForVisibleHostedWebView({
|
||||
discoveryUrl,
|
||||
expectedText: 'orca.rb',
|
||||
expectedHrefIncludes: '/files/preview/',
|
||||
timeoutMs: transitionTimeoutMs
|
||||
})
|
||||
})
|
||||
const previewDocument = await waitForVisibleHostedWebView({
|
||||
discoveryUrl,
|
||||
expectedText: PREVIEW_STABLE_TEXT,
|
||||
expectedHrefIncludes: '/files/preview/',
|
||||
timeoutMs
|
||||
})
|
||||
const preview = await captureHostedRoute({
|
||||
deviceUdid,
|
||||
document: previewDocument,
|
||||
nativeBaseline: nativeBaselines.preview,
|
||||
runtimeDirectory,
|
||||
screenshotName: 'hosted-file-preview-portrait.png',
|
||||
title: 'orca.rb',
|
||||
timeoutMs
|
||||
})
|
||||
const returnedWorkspaceDocument = await returnHostedToWorkspaces({
|
||||
discoveryUrl,
|
||||
emulator,
|
||||
expectedWorkspace,
|
||||
filesDocument,
|
||||
previewDocument,
|
||||
timeoutMs
|
||||
})
|
||||
return {
|
||||
evidence: {
|
||||
files: filesPreviewParityEvidence(nativeBaselines.files, files),
|
||||
preview: filesPreviewParityEvidence(nativeBaselines.preview, preview)
|
||||
},
|
||||
workspaceDocument: returnedWorkspaceDocument
|
||||
}
|
||||
}
|
||||
|
||||
export function filesPreviewParityEvidence(nativeCapture, hostedCapture) {
|
||||
return {
|
||||
nativeScreenshot: path.basename(nativeCapture.screenshot),
|
||||
hostedScreenshot: path.basename(hostedCapture.screenshot),
|
||||
nativeScreenTitlePoint: nativeCapture.screenTitlePoint,
|
||||
hostedScreenTitlePoint: hostedCapture.screenTitlePoint,
|
||||
screenshotParity: hostedCapture.screenshotParity
|
||||
}
|
||||
}
|
||||
|
||||
async function captureNativeRoute(options) {
|
||||
const screenTitlePoint = await waitForHostedIosAccessibilityControl(
|
||||
options.emulator,
|
||||
options.title,
|
||||
options.timeoutMs
|
||||
)
|
||||
await delay(500)
|
||||
const screenshot = path.join(options.runtimeDirectory, options.screenshotName)
|
||||
await captureSimulatorScreenshot(options.deviceUdid, screenshot)
|
||||
return { screenTitlePoint, screenshot }
|
||||
}
|
||||
|
||||
async function captureHostedRoute(options) {
|
||||
const screenTitlePoint = await readHostedWebViewTextPoint(options.document, options.title)
|
||||
const screenshot = path.join(options.runtimeDirectory, options.screenshotName)
|
||||
const deadline = Date.now() + options.timeoutMs
|
||||
let lastError = new Error(`${options.title} did not reach screenshot parity`)
|
||||
while (Date.now() < deadline) {
|
||||
await delay(500)
|
||||
await captureSimulatorScreenshot(options.deviceUdid, screenshot)
|
||||
try {
|
||||
const screenshotParity = await assertHostedIosScreenshotParity({
|
||||
hostedLandmark: screenTitlePoint,
|
||||
hostedScreenshot: screenshot,
|
||||
nativeLandmark: options.nativeBaseline.screenTitlePoint,
|
||||
nativeScreenshot: options.nativeBaseline.screenshot
|
||||
})
|
||||
return { screenTitlePoint, screenshot, screenshotParity }
|
||||
} catch (error) {
|
||||
lastError = error
|
||||
}
|
||||
}
|
||||
throw lastError
|
||||
}
|
||||
|
||||
async function returnNativeToWorkspaces({ emulator, expectedWorkspace, timeoutMs }) {
|
||||
await tapHostedIosAccessibilityControl(emulator, 'Back to files', timeoutMs)
|
||||
await waitForHostedIosAccessibilityControl(emulator, FILES_STABLE_LABEL, timeoutMs)
|
||||
await tapHostedIosAccessibilityControl(emulator, 'Back to session', timeoutMs)
|
||||
await waitForHostedIosAccessibilityControlByLabelPrefix(emulator, 'Mobile Emulator', timeoutMs)
|
||||
await tapHostedIosAccessibilityControl(emulator, 'Back to worktrees', timeoutMs)
|
||||
await waitForHostedIosAccessibilityControlByLabelPrefix(emulator, expectedWorkspace, timeoutMs)
|
||||
}
|
||||
|
||||
async function returnHostedToWorkspaces(options) {
|
||||
const filesDocument = await tapForHostedTransition({
|
||||
document: options.previewDocument,
|
||||
emulator: options.emulator,
|
||||
label: 'Back to files',
|
||||
timeoutMs: options.timeoutMs,
|
||||
resolve: (transitionTimeoutMs) =>
|
||||
waitForVisibleHostedWebView({
|
||||
discoveryUrl: options.discoveryUrl,
|
||||
expectedText: 'Files',
|
||||
expectedHrefIncludes: '/files/',
|
||||
timeoutMs: transitionTimeoutMs
|
||||
})
|
||||
})
|
||||
const sessionDocument = await tapForHostedTransition({
|
||||
document: filesDocument,
|
||||
emulator: options.emulator,
|
||||
label: 'Back to session',
|
||||
timeoutMs: options.timeoutMs,
|
||||
resolve: (transitionTimeoutMs) =>
|
||||
waitForVisibleHostedWebView({
|
||||
discoveryUrl: options.discoveryUrl,
|
||||
expectedText: 'Mobile Emulator',
|
||||
expectedHrefIncludes: '/session/',
|
||||
timeoutMs: transitionTimeoutMs
|
||||
})
|
||||
})
|
||||
return tapForHostedTransition({
|
||||
document: sessionDocument,
|
||||
emulator: options.emulator,
|
||||
label: 'Back to worktrees',
|
||||
timeoutMs: options.timeoutMs,
|
||||
resolve: (transitionTimeoutMs) =>
|
||||
waitForVisibleHostedWebView({
|
||||
discoveryUrl: options.discoveryUrl,
|
||||
expectedText: 'Orca Desktop',
|
||||
timeoutMs: transitionTimeoutMs
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function tapForHostedTransition({ document, emulator, label, resolve, timeoutMs }) {
|
||||
const deadline = Date.now() + timeoutMs
|
||||
let lastError = new Error(`${label} did not transition`)
|
||||
for (let attempt = 0; attempt < 3 && Date.now() < deadline; attempt += 1) {
|
||||
try {
|
||||
await tapHostedControl(emulator, document, label, timeoutMs, attempt)
|
||||
return await resolve(Math.min(3_000, Math.max(1_000, deadline - Date.now())))
|
||||
} catch (error) {
|
||||
lastError = error
|
||||
}
|
||||
}
|
||||
throw lastError
|
||||
}
|
||||
|
||||
async function tapHostedControl(emulator, document, label, timeoutMs, attempt) {
|
||||
if (attempt === 0) {
|
||||
try {
|
||||
await tapHostedIosAccessibilityControl(emulator, label, Math.min(timeoutMs, 5_000))
|
||||
return
|
||||
} catch {
|
||||
// WebKit can omit a descendant while refreshing its accessibility tree.
|
||||
}
|
||||
}
|
||||
const point = await readHostedWebViewControlPoint(document, label)
|
||||
await tapHostedIosPoint(emulator, point)
|
||||
}
|
||||
|
||||
async function waitForHostedLabel(document, label, timeoutMs) {
|
||||
const deadline = Date.now() + timeoutMs
|
||||
let state
|
||||
while (Date.now() < deadline) {
|
||||
state = await readHostedWebViewState(document)
|
||||
if (state.labels.includes(label)) {
|
||||
return state
|
||||
}
|
||||
await delay(250)
|
||||
}
|
||||
throw new Error(`${label} was not present. Last labels: ${(state?.labels ?? []).join(', ')}`)
|
||||
}
|
||||
|
||||
async function captureSimulatorScreenshot(deviceUdid, outputPath) {
|
||||
await execFileAsync('xcrun', ['simctl', 'io', deviceUdid, 'screenshot', outputPath])
|
||||
}
|
||||
|
||||
function delay(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
import process from 'node:process'
|
||||
|
||||
const usage =
|
||||
'Usage: node scripts/run-hosted-webview-simulator-e2e.mjs [--device <name|udid>] [--timeout-ms <ms>] [--security-only] [--native-settings-only] [--source-control-only] [--skip-native-build]'
|
||||
'Usage: node scripts/run-hosted-webview-simulator-e2e.mjs [--device <name|udid>] [--timeout-ms <ms>] [--security-only] [--files-preview-only] [--native-settings-only] [--source-control-only] [--skip-native-build]'
|
||||
|
||||
export function parseHostedWebViewSimulatorE2eOptions(args) {
|
||||
const parsed = {
|
||||
device: 'iPhone 17 Pro',
|
||||
filesPreviewOnly: false,
|
||||
nativeSettingsOnly: false,
|
||||
securityOnly: false,
|
||||
skipNativeBuild: false,
|
||||
@@ -21,6 +22,8 @@ export function parseHostedWebViewSimulatorE2eOptions(args) {
|
||||
parsed.timeoutMs = Number(args[++index])
|
||||
} else if (args[index] === '--security-only') {
|
||||
parsed.securityOnly = true
|
||||
} else if (args[index] === '--files-preview-only') {
|
||||
parsed.filesPreviewOnly = true
|
||||
} else if (args[index] === '--native-settings-only') {
|
||||
parsed.nativeSettingsOnly = true
|
||||
} else if (args[index] === '--source-control-only') {
|
||||
@@ -38,8 +41,12 @@ export function parseHostedWebViewSimulatorE2eOptions(args) {
|
||||
throw new Error('--timeout-ms must be an integer of at least 10000')
|
||||
}
|
||||
if (
|
||||
[parsed.securityOnly, parsed.nativeSettingsOnly, parsed.sourceControlOnly].filter(Boolean)
|
||||
.length > 1
|
||||
[
|
||||
parsed.securityOnly,
|
||||
parsed.filesPreviewOnly,
|
||||
parsed.nativeSettingsOnly,
|
||||
parsed.sourceControlOnly
|
||||
].filter(Boolean).length > 1
|
||||
) {
|
||||
throw new Error('Focused journey options are mutually exclusive')
|
||||
}
|
||||
|
||||
@@ -21,6 +21,10 @@ import {
|
||||
captureHostedCoreRouteParity,
|
||||
captureNativeCoreRouteBaselines
|
||||
} from './hosted-ios-core-route-parity.mjs'
|
||||
import {
|
||||
captureHostedFilesPreviewParity,
|
||||
captureNativeFilesPreviewBaselines
|
||||
} from './hosted-ios-files-preview-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'
|
||||
@@ -100,7 +104,10 @@ async function main() {
|
||||
completeHostedIosNativeOnboarding(emulator, expectedWorkspace, options.timeoutMs)
|
||||
)
|
||||
const nativeCoreRoutes =
|
||||
options.securityOnly || options.nativeSettingsOnly || options.sourceControlOnly
|
||||
options.securityOnly ||
|
||||
options.filesPreviewOnly ||
|
||||
options.nativeSettingsOnly ||
|
||||
options.sourceControlOnly
|
||||
? null
|
||||
: await evidenceStep('native Tasks and Session baselines', () =>
|
||||
captureNativeCoreRouteBaselines({
|
||||
@@ -111,8 +118,23 @@ async function main() {
|
||||
timeoutMs: options.timeoutMs
|
||||
})
|
||||
)
|
||||
const nativeAgentHistory =
|
||||
const nativeFilesPreview =
|
||||
options.securityOnly || options.nativeSettingsOnly || options.sourceControlOnly
|
||||
? null
|
||||
: await evidenceStep('native Files and Preview baselines', () =>
|
||||
captureNativeFilesPreviewBaselines({
|
||||
deviceUdid,
|
||||
emulator,
|
||||
expectedWorkspace,
|
||||
runtimeDirectory,
|
||||
timeoutMs: options.timeoutMs
|
||||
})
|
||||
)
|
||||
const nativeAgentHistory =
|
||||
options.securityOnly ||
|
||||
options.filesPreviewOnly ||
|
||||
options.nativeSettingsOnly ||
|
||||
options.sourceControlOnly
|
||||
? null
|
||||
: await evidenceStep('native Agent History baseline', () =>
|
||||
captureNativeAgentHistoryBaseline({
|
||||
@@ -134,7 +156,10 @@ async function main() {
|
||||
timeoutMs: options.timeoutMs
|
||||
})
|
||||
const hostedCoreRoutes =
|
||||
options.securityOnly || options.nativeSettingsOnly || options.sourceControlOnly
|
||||
options.securityOnly ||
|
||||
options.filesPreviewOnly ||
|
||||
options.nativeSettingsOnly ||
|
||||
options.sourceControlOnly
|
||||
? null
|
||||
: await evidenceStep('hosted Tasks and Session parity', () =>
|
||||
captureHostedCoreRouteParity({
|
||||
@@ -149,15 +174,31 @@ async function main() {
|
||||
})
|
||||
)
|
||||
const activeWorkspaceDocument = hostedCoreRoutes?.workspaceDocument ?? workspaceDocument
|
||||
const hostedFilesPreview =
|
||||
options.securityOnly || options.nativeSettingsOnly || options.sourceControlOnly
|
||||
? null
|
||||
: await evidenceStep('hosted Files and Preview parity', () =>
|
||||
captureHostedFilesPreviewParity({
|
||||
deviceUdid,
|
||||
discoveryUrl: `http://127.0.0.1:${inspectorPort}`,
|
||||
emulator,
|
||||
expectedWorkspace,
|
||||
nativeBaselines: nativeFilesPreview,
|
||||
runtimeDirectory,
|
||||
timeoutMs: options.timeoutMs,
|
||||
workspaceDocument: activeWorkspaceDocument
|
||||
})
|
||||
)
|
||||
const parityWorkspaceDocument = hostedFilesPreview?.workspaceDocument ?? activeWorkspaceDocument
|
||||
const historyEvidence =
|
||||
options.securityOnly || options.sourceControlOnly
|
||||
options.securityOnly || options.filesPreviewOnly || options.sourceControlOnly
|
||||
? null
|
||||
: options.nativeSettingsOnly
|
||||
? await evidenceStep('native Terminal Settings journey', () =>
|
||||
verifyNativeSettingsJourney({
|
||||
discoveryUrl: `http://127.0.0.1:${inspectorPort}`,
|
||||
emulator,
|
||||
workspaceDocument: activeWorkspaceDocument,
|
||||
workspaceDocument: parityWorkspaceDocument,
|
||||
expectedWorkspace,
|
||||
timeoutMs: options.timeoutMs
|
||||
})
|
||||
@@ -169,13 +210,13 @@ async function main() {
|
||||
emulator,
|
||||
nativeAgentHistory,
|
||||
runtimeDirectory,
|
||||
workspaceDocument: activeWorkspaceDocument,
|
||||
workspaceDocument: parityWorkspaceDocument,
|
||||
expectedWorkspace,
|
||||
timeoutMs: options.timeoutMs
|
||||
})
|
||||
)
|
||||
const sourceControlReview =
|
||||
options.securityOnly || options.nativeSettingsOnly
|
||||
options.securityOnly || options.filesPreviewOnly || options.nativeSettingsOnly
|
||||
? null
|
||||
: await evidenceStep('Source Control and Review journey', async () => {
|
||||
if (options.sourceControlOnly) {
|
||||
@@ -207,14 +248,15 @@ async function main() {
|
||||
timeoutMs: options.timeoutMs
|
||||
})
|
||||
})
|
||||
const securityDocument = options.securityOnly
|
||||
? workspaceDocument
|
||||
: await waitForVisibleHostedWebView({
|
||||
discoveryUrl: `http://127.0.0.1:${inspectorPort}`,
|
||||
expectedText: options.nativeSettingsOnly ? 'Mobile Emulator' : 'reviewed',
|
||||
expectedHrefIncludes: options.nativeSettingsOnly ? '/session/' : '/review/',
|
||||
timeoutMs: options.timeoutMs
|
||||
})
|
||||
const securityDocument =
|
||||
options.securityOnly || options.filesPreviewOnly
|
||||
? parityWorkspaceDocument
|
||||
: await waitForVisibleHostedWebView({
|
||||
discoveryUrl: `http://127.0.0.1:${inspectorPort}`,
|
||||
expectedText: options.nativeSettingsOnly ? 'Mobile Emulator' : 'reviewed',
|
||||
expectedHrefIncludes: options.nativeSettingsOnly ? '/session/' : '/review/',
|
||||
timeoutMs: options.timeoutMs
|
||||
})
|
||||
const networkIsolation = await evidenceStep('network isolation probe', () =>
|
||||
verifyHostedWebViewNetworkIsolation({
|
||||
document: securityDocument,
|
||||
@@ -248,6 +290,7 @@ async function main() {
|
||||
nativeOnboarding,
|
||||
agentHistory: historyEvidence,
|
||||
coreRouteParity: hostedCoreRoutes?.evidence ?? null,
|
||||
filesPreviewParity: hostedFilesPreview?.evidence ?? null,
|
||||
sourceControlReview
|
||||
},
|
||||
null,
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createElement } from 'react'
|
||||
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { HostFilePreviewOperations } from './host-file-preview-operations'
|
||||
import type { MobileFilePreviewRouteState } from './mobile-file-preview-route'
|
||||
import { MobileFilePreviewScreen } from './MobileFilePreviewScreen'
|
||||
import { sourceKeyForPreview } from './mobile-file-preview-source'
|
||||
import {
|
||||
hasUnsavedMobileTerminalArtifactDraft,
|
||||
@@ -6,7 +11,71 @@ import {
|
||||
shouldKeepDirtyDraftOnPreviewLoadResult
|
||||
} from './mobile-file-preview-editability'
|
||||
|
||||
vi.mock('react-native', () => ({
|
||||
Alert: { alert: vi.fn() },
|
||||
BackHandler: { addEventListener: () => ({ remove: vi.fn() }) },
|
||||
Pressable: 'Pressable',
|
||||
Text: 'Text',
|
||||
View: 'View',
|
||||
useWindowDimensions: () => ({ width: 390, height: 844 })
|
||||
}))
|
||||
|
||||
vi.mock('react-native-safe-area-context', () => ({
|
||||
SafeAreaView: 'SafeAreaView'
|
||||
}))
|
||||
|
||||
vi.mock('expo-router', () => ({
|
||||
useRouter: () => ({ back: vi.fn() })
|
||||
}))
|
||||
|
||||
vi.mock('lucide-react-native', () => ({
|
||||
ChevronLeft: 'ChevronLeft',
|
||||
Save: 'Save'
|
||||
}))
|
||||
|
||||
vi.mock('../transport/client-context', () => ({
|
||||
useForceReconnect: () => vi.fn(),
|
||||
useHostClient: () => ({ client: null, state: 'disconnected' })
|
||||
}))
|
||||
|
||||
vi.mock('../theme/mobile-theme', () => ({
|
||||
colors: { textPrimary: '#fff', textSecondary: '#999' },
|
||||
spacing: { md: 16 }
|
||||
}))
|
||||
|
||||
vi.mock('./mobile-file-preview-styles', () => ({
|
||||
filePreviewStyles: {}
|
||||
}))
|
||||
|
||||
vi.mock('./MobileFilePreviewBody', () => ({
|
||||
MobileFilePreviewBody: 'MobileFilePreviewBody'
|
||||
}))
|
||||
|
||||
vi.mock('./default-host-file-preview-operations', () => ({
|
||||
defaultHostFilePreviewOperations: vi.fn()
|
||||
}))
|
||||
|
||||
function suppressReactTestRendererDeprecationWarning(): () => void {
|
||||
const originalConsoleError = console.error
|
||||
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation((...args) => {
|
||||
const firstArg = args[0]
|
||||
if (typeof firstArg === 'string' && firstArg.includes('react-test-renderer is deprecated')) {
|
||||
return
|
||||
}
|
||||
originalConsoleError(...args)
|
||||
})
|
||||
return () => consoleErrorSpy.mockRestore()
|
||||
}
|
||||
|
||||
describe('MobileFilePreviewScreen', () => {
|
||||
let renderer: ReactTestRenderer | null = null
|
||||
|
||||
afterEach(() => {
|
||||
renderer?.unmount()
|
||||
renderer = null
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('treats empty terminal artifact text previews as editable', () => {
|
||||
expect(isEditableMobileTerminalArtifactPreview({ status: 'empty', kind: 'text' })).toBe(true)
|
||||
expect(
|
||||
@@ -81,4 +150,55 @@ describe('MobileFilePreviewScreen', () => {
|
||||
})
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('does not reload when an equivalent route object is rendered', async () => {
|
||||
const route: MobileFilePreviewRouteState = {
|
||||
ok: true,
|
||||
params: {
|
||||
hostId: 'host-1',
|
||||
worktreeId: 'worktree-1',
|
||||
relativePath: 'Casks/orca.rb',
|
||||
source: 'worktree'
|
||||
}
|
||||
}
|
||||
const operations: HostFilePreviewOperations = {
|
||||
load: vi.fn(async () => ({
|
||||
status: 'ready',
|
||||
kind: 'text',
|
||||
content: 'cask "orca" do',
|
||||
truncated: false,
|
||||
byteLength: 14
|
||||
})),
|
||||
saveTerminalArtifact: vi.fn(),
|
||||
reconnect: vi.fn(),
|
||||
openExternalUrl: vi.fn()
|
||||
}
|
||||
const restoreConsoleError = suppressReactTestRendererDeprecationWarning()
|
||||
try {
|
||||
await act(async () => {
|
||||
renderer = create(
|
||||
createElement(MobileFilePreviewScreen, {
|
||||
route,
|
||||
operations,
|
||||
connectionState: 'connected',
|
||||
nativeHostBinding: false
|
||||
})
|
||||
)
|
||||
})
|
||||
await act(async () => {
|
||||
renderer!.update(
|
||||
createElement(MobileFilePreviewScreen, {
|
||||
route: { ok: true, params: { ...route.params } },
|
||||
operations,
|
||||
connectionState: 'connected',
|
||||
nativeHostBinding: false
|
||||
})
|
||||
)
|
||||
})
|
||||
} finally {
|
||||
restoreConsoleError()
|
||||
}
|
||||
|
||||
expect(operations.load).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -17,7 +17,11 @@ import {
|
||||
displayNameFromPreviewPath,
|
||||
type MobileFilePreviewRouteState
|
||||
} from './mobile-file-preview-route'
|
||||
import { previewSourceFromRoute, sourceKeyForPreview } from './mobile-file-preview-source'
|
||||
import {
|
||||
previewSourceFromRoute,
|
||||
sourceKeyForPreview,
|
||||
sourceRevisionForPreview
|
||||
} from './mobile-file-preview-source'
|
||||
import { normalizeMobileFilePreviewLineColumn } from './mobile-file-preview-line-column'
|
||||
import {
|
||||
hasUnsavedMobileTerminalArtifactDraft,
|
||||
@@ -43,17 +47,16 @@ export function MobileFilePreviewScreen({
|
||||
}: Props) {
|
||||
const router = useRouter()
|
||||
const previewParams = route.ok ? route.params : null
|
||||
const nativeHost = useHostClient(nativeHostBinding ? previewParams?.hostId : undefined)
|
||||
const previewHostId = previewParams?.hostId
|
||||
const nativeHost = useHostClient(nativeHostBinding ? previewHostId : undefined)
|
||||
const forceReconnect = useForceReconnect()
|
||||
const operations = useMemo(
|
||||
() =>
|
||||
operationsProp ??
|
||||
(nativeHost.client && previewParams
|
||||
? defaultHostFilePreviewOperations(nativeHost.client, () =>
|
||||
forceReconnect(previewParams.hostId)
|
||||
)
|
||||
(nativeHost.client && previewHostId
|
||||
? defaultHostFilePreviewOperations(nativeHost.client, () => forceReconnect(previewHostId))
|
||||
: null),
|
||||
[forceReconnect, nativeHost.client, operationsProp, previewParams]
|
||||
[forceReconnect, nativeHost.client, operationsProp, previewHostId]
|
||||
)
|
||||
const connState = connectionState ?? nativeHost.state
|
||||
const handleOpenExternalUrl = useCallback(
|
||||
@@ -73,10 +76,15 @@ export function MobileFilePreviewScreen({
|
||||
const savedContentRef = useRef(savedContent)
|
||||
const draftSourceKeyRef = useRef<string | null>(null)
|
||||
const { width, height } = useWindowDimensions()
|
||||
const routePreviewSource = useMemo(
|
||||
() => (previewParams ? previewSourceFromRoute(previewParams) : null),
|
||||
[previewParams]
|
||||
)
|
||||
const routePreviewSourceCandidate = previewParams ? previewSourceFromRoute(previewParams) : null
|
||||
const routePreviewSourceRevision = sourceRevisionForPreview(routePreviewSourceCandidate)
|
||||
const routePreviewSourceRef = useRef(routePreviewSourceCandidate)
|
||||
const routePreviewSourceRevisionRef = useRef(routePreviewSourceRevision)
|
||||
if (routePreviewSourceRevisionRef.current !== routePreviewSourceRevision) {
|
||||
routePreviewSourceRef.current = routePreviewSourceCandidate
|
||||
routePreviewSourceRevisionRef.current = routePreviewSourceRevision
|
||||
}
|
||||
const routePreviewSource = routePreviewSourceRef.current
|
||||
const [previewSource, setPreviewSource] = useState<MobileFilePreviewSource | null>(
|
||||
routePreviewSource
|
||||
)
|
||||
@@ -85,6 +93,8 @@ export function MobileFilePreviewScreen({
|
||||
() => sourceKeyForPreview(routePreviewSource),
|
||||
[routePreviewSource]
|
||||
)
|
||||
const hasPreviewParams = previewParams !== null
|
||||
const routeErrorMessage = route.ok ? null : route.message
|
||||
const previewSourceKeyRef = useRef(previewSourceKey)
|
||||
const lineColumn = useMemo(
|
||||
() =>
|
||||
@@ -113,8 +123,8 @@ export function MobileFilePreviewScreen({
|
||||
|
||||
const loadPreview = useCallback(async () => {
|
||||
const loadSourceKey = previewSourceKey
|
||||
if (!previewParams || !previewSource || loadSourceKey !== routePreviewSourceKey) {
|
||||
setPreview(previewError(route.ok ? 'Unable to load preview' : route.message))
|
||||
if (!hasPreviewParams || !previewSource || loadSourceKey !== routePreviewSourceKey) {
|
||||
setPreview(previewError(routeErrorMessage ?? 'Unable to load preview'))
|
||||
return
|
||||
}
|
||||
const preserveDirtyDraft =
|
||||
@@ -167,11 +177,11 @@ export function MobileFilePreviewScreen({
|
||||
}
|
||||
}, [
|
||||
connState,
|
||||
hasPreviewParams,
|
||||
operations,
|
||||
previewParams,
|
||||
previewSource,
|
||||
previewSourceKey,
|
||||
route,
|
||||
routeErrorMessage,
|
||||
routePreviewSourceKey
|
||||
])
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { sourceKeyForPreview } from './mobile-file-preview-source'
|
||||
import { sourceKeyForPreview, sourceRevisionForPreview } from './mobile-file-preview-source'
|
||||
|
||||
describe('mobile-file-preview-source', () => {
|
||||
it('uses structured preview keys so colons in paths cannot collide', () => {
|
||||
@@ -25,4 +25,17 @@ describe('mobile-file-preview-source', () => {
|
||||
sourceKeyForPreview({ source: 'worktree', worktreeId: 'wt:1', relativePath: 'a:b.ts' })
|
||||
).toBe(JSON.stringify(['worktree', 'wt:1', 'a:b.ts']))
|
||||
})
|
||||
|
||||
it('tracks refreshed terminal capabilities separately from file identity', () => {
|
||||
const original = {
|
||||
source: 'terminalArtifact' as const,
|
||||
worktreeId: 'wt:1',
|
||||
absolutePath: '/tmp/result.json',
|
||||
grantId: 'grant-1'
|
||||
}
|
||||
const refreshed = { ...original, grantId: 'grant-2' }
|
||||
|
||||
expect(sourceKeyForPreview(original)).toBe(sourceKeyForPreview(refreshed))
|
||||
expect(sourceRevisionForPreview(original)).not.toBe(sourceRevisionForPreview(refreshed))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -37,3 +37,20 @@ export function sourceKeyForPreview(source: MobileFilePreviewSource | null): str
|
||||
])
|
||||
: JSON.stringify(['worktree', source.worktreeId, source.relativePath])
|
||||
}
|
||||
|
||||
export function sourceRevisionForPreview(source: MobileFilePreviewSource | null): string | null {
|
||||
if (!source) {
|
||||
return null
|
||||
}
|
||||
return source.source === 'terminalArtifact'
|
||||
? JSON.stringify([
|
||||
'terminal',
|
||||
source.worktreeId,
|
||||
source.absolutePath,
|
||||
source.grantId,
|
||||
source.terminalHandle ?? '',
|
||||
source.pathText ?? '',
|
||||
source.cwd ?? ''
|
||||
])
|
||||
: JSON.stringify(['worktree', source.worktreeId, source.relativePath])
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { StyleSheet } from 'react-native'
|
||||
import { Platform, StyleSheet } from 'react-native'
|
||||
import { colors, radii, spacing, typography } from '../theme/mobile-theme'
|
||||
|
||||
export const filePreviewStyles = StyleSheet.create({
|
||||
@@ -94,7 +94,10 @@ export const filePreviewStyles = StyleSheet.create({
|
||||
},
|
||||
textPreview: {
|
||||
color: colors.textPrimary,
|
||||
fontFamily: typography.monoFamily,
|
||||
...Platform.select({
|
||||
default: { fontFamily: typography.monoFamily },
|
||||
web: {}
|
||||
}),
|
||||
fontSize: 13,
|
||||
lineHeight: 19
|
||||
},
|
||||
@@ -161,7 +164,10 @@ export const filePreviewStyles = StyleSheet.create({
|
||||
editInput: {
|
||||
flex: 1,
|
||||
color: colors.textPrimary,
|
||||
fontFamily: typography.monoFamily,
|
||||
...Platform.select({
|
||||
default: { fontFamily: typography.monoFamily },
|
||||
web: {}
|
||||
}),
|
||||
fontSize: 13,
|
||||
lineHeight: 19,
|
||||
padding: 0
|
||||
|
||||
@@ -143,7 +143,39 @@ describe('hosted iOS core-route parity', () => {
|
||||
expect.any(Function)
|
||||
)
|
||||
expect(mocks.readTextPoint).toHaveBeenCalledWith(expect.anything(), 'Filter')
|
||||
expect(mocks.readControlPoint).toHaveBeenCalledWith(expect.anything(), 'Back to worktrees')
|
||||
expect(mocks.tapControl).toHaveBeenCalledWith(
|
||||
{ deviceUdid: 'simulator' },
|
||||
'Back to worktrees',
|
||||
5_000
|
||||
)
|
||||
expect(mocks.compareScreenshots).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('uses a measured point when the hosted session back control is missing from AX', async () => {
|
||||
mocks.tapControl.mockRejectedValueOnce(new Error('missing accessibility descendant'))
|
||||
|
||||
await captureHostedCoreRouteParity({
|
||||
deviceUdid: 'simulator',
|
||||
discoveryUrl: 'http://127.0.0.1:9222',
|
||||
emulator: { deviceUdid: 'simulator' },
|
||||
expectedWorkspace: 'mobile-rearch',
|
||||
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'
|
||||
}
|
||||
},
|
||||
runtimeDirectory: '/tmp/parity',
|
||||
timeoutMs: 30_000,
|
||||
workspaceDocument: { href: 'orca-mobile-web://build/h/host' }
|
||||
})
|
||||
|
||||
expect(mocks.readControlPoint).toHaveBeenCalledWith(expect.anything(), 'Back to worktrees')
|
||||
expect(mocks.tapPoint).toHaveBeenCalledWith({ deviceUdid: 'simulator' }, { x: 0.8, y: 0.1 })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
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(),
|
||||
readState: vi.fn(),
|
||||
readTextPoint: vi.fn(),
|
||||
tapControl: vi.fn(),
|
||||
tapControlByPrefix: vi.fn(),
|
||||
tapPoint: vi.fn(),
|
||||
waitForControl: vi.fn(),
|
||||
waitForControlByPrefix: 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
|
||||
}))
|
||||
vi.mock('../../scripts/hosted-ios-screenshot-parity.mjs', () => ({
|
||||
assertHostedIosScreenshotParity: mocks.compareScreenshots
|
||||
}))
|
||||
vi.mock('../../scripts/hosted-webview-cdp-session.mjs', () => ({
|
||||
activateHostedWebViewControl: mocks.activateControl,
|
||||
readHostedWebViewState: mocks.readState,
|
||||
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 {
|
||||
captureHostedFilesPreviewParity,
|
||||
captureNativeFilesPreviewBaselines
|
||||
} from '../../scripts/hosted-ios-files-preview-parity.mjs'
|
||||
|
||||
describe('hosted iOS Files and Preview 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.readState.mockResolvedValue({
|
||||
href: 'orca-mobile-web://build/h/host/files/worktree',
|
||||
bodyText: 'Files Casks orca.rb',
|
||||
labels: ['Open folder Casks', 'Preview file orca.rb', 'File preview'],
|
||||
placeholders: []
|
||||
})
|
||||
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.waitForDocument.mockImplementation(({ expectedHrefIncludes }) => ({
|
||||
href: `orca-mobile-web://build${expectedHrefIncludes ?? '/h/host'}`,
|
||||
targetId: expectedHrefIncludes ?? 'workspace'
|
||||
}))
|
||||
})
|
||||
|
||||
it('captures native Files and Preview and returns to worktrees', async () => {
|
||||
const baselines = await captureNativeFilesPreviewBaselines({
|
||||
deviceUdid: 'simulator',
|
||||
emulator: { deviceUdid: 'simulator' },
|
||||
expectedWorkspace: 'mobile-rearch',
|
||||
runtimeDirectory: '/tmp/parity',
|
||||
timeoutMs: 30_000
|
||||
})
|
||||
|
||||
expect(mocks.tapControl).toHaveBeenCalledWith(
|
||||
{ deviceUdid: 'simulator' },
|
||||
'Open file explorer',
|
||||
30_000
|
||||
)
|
||||
expect(mocks.tapControl).toHaveBeenCalledWith(
|
||||
{ deviceUdid: 'simulator' },
|
||||
'Preview file orca.rb',
|
||||
30_000
|
||||
)
|
||||
expect(mocks.tapControl).toHaveBeenCalledWith(
|
||||
{ deviceUdid: 'simulator' },
|
||||
'Back to worktrees',
|
||||
30_000
|
||||
)
|
||||
expect(baselines).toEqual({
|
||||
files: {
|
||||
screenTitlePoint: { x: 0.2, y: 0.1 },
|
||||
screenshot: '/tmp/parity/native-files-portrait.png'
|
||||
},
|
||||
preview: {
|
||||
screenTitlePoint: { x: 0.2, y: 0.1 },
|
||||
screenshot: '/tmp/parity/native-file-preview-portrait.png'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('compares hosted Files and Preview and returns to the workspace route', async () => {
|
||||
const result = await captureHostedFilesPreviewParity({
|
||||
deviceUdid: 'simulator',
|
||||
discoveryUrl: 'http://127.0.0.1:9222',
|
||||
emulator: { deviceUdid: 'simulator' },
|
||||
expectedWorkspace: 'mobile-rearch',
|
||||
nativeBaselines: {
|
||||
files: {
|
||||
screenTitlePoint: { x: 0.2, y: 0.1 },
|
||||
screenshot: '/tmp/parity/native-files-portrait.png'
|
||||
},
|
||||
preview: {
|
||||
screenTitlePoint: { x: 0.2, y: 0.1 },
|
||||
screenshot: '/tmp/parity/native-file-preview-portrait.png'
|
||||
}
|
||||
},
|
||||
runtimeDirectory: '/tmp/parity',
|
||||
timeoutMs: 30_000,
|
||||
workspaceDocument: { href: 'orca-mobile-web://build/h/host' }
|
||||
})
|
||||
|
||||
expect(result.evidence.files.screenshotParity).toEqual({ changedPixelRatio: 0.01 })
|
||||
expect(result.evidence.preview.screenshotParity).toEqual({ changedPixelRatio: 0.01 })
|
||||
expect(result.workspaceDocument.href).toContain('/h/host')
|
||||
expect(mocks.activateWorkspace).toHaveBeenCalledOnce()
|
||||
expect(mocks.tapControl).toHaveBeenCalledWith(
|
||||
{ deviceUdid: 'simulator' },
|
||||
'Open file explorer',
|
||||
5_000
|
||||
)
|
||||
expect(mocks.tapControl).toHaveBeenCalledWith(
|
||||
{ deviceUdid: 'simulator' },
|
||||
'Preview file orca.rb',
|
||||
5_000
|
||||
)
|
||||
expect(mocks.compareScreenshots).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('uses a measured WebView point when a control is missing from native accessibility', async () => {
|
||||
mocks.tapControl.mockRejectedValueOnce(new Error('missing accessibility descendant'))
|
||||
|
||||
await captureHostedFilesPreviewParity({
|
||||
deviceUdid: 'simulator',
|
||||
discoveryUrl: 'http://127.0.0.1:9222',
|
||||
emulator: { deviceUdid: 'simulator' },
|
||||
expectedWorkspace: 'mobile-rearch',
|
||||
nativeBaselines: {
|
||||
files: {
|
||||
screenTitlePoint: { x: 0.2, y: 0.1 },
|
||||
screenshot: '/tmp/parity/native-files-portrait.png'
|
||||
},
|
||||
preview: {
|
||||
screenTitlePoint: { x: 0.2, y: 0.1 },
|
||||
screenshot: '/tmp/parity/native-file-preview-portrait.png'
|
||||
}
|
||||
},
|
||||
runtimeDirectory: '/tmp/parity',
|
||||
timeoutMs: 30_000,
|
||||
workspaceDocument: { href: 'orca-mobile-web://build/h/host' }
|
||||
})
|
||||
|
||||
expect(mocks.readControlPoint).toHaveBeenCalledWith(expect.anything(), 'Open file explorer')
|
||||
expect(mocks.tapPoint).toHaveBeenCalledWith({ deviceUdid: 'simulator' }, { x: 0.8, y: 0.1 })
|
||||
})
|
||||
})
|
||||
@@ -5,6 +5,7 @@ describe('hosted WebView simulator E2E options', () => {
|
||||
it('retains bounded defaults', () => {
|
||||
expect(parseHostedWebViewSimulatorE2eOptions([])).toEqual({
|
||||
device: 'iPhone 17 Pro',
|
||||
filesPreviewOnly: false,
|
||||
nativeSettingsOnly: false,
|
||||
securityOnly: false,
|
||||
skipNativeBuild: false,
|
||||
@@ -34,7 +35,7 @@ describe('hosted WebView simulator E2E options', () => {
|
||||
|
||||
it('rejects mutually exclusive focused journeys', () => {
|
||||
expect(() =>
|
||||
parseHostedWebViewSimulatorE2eOptions(['--security-only', '--source-control-only'])
|
||||
parseHostedWebViewSimulatorE2eOptions(['--files-preview-only', '--source-control-only'])
|
||||
).toThrow('mutually exclusive')
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user