agent-device 0.18.3 → 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/android-multitouch-helper/dist/{agent-device-android-multitouch-helper-0.18.3.apk → agent-device-android-multitouch-helper-0.19.1.apk} +0 -0
- package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.19.1.apk.sha256 +1 -0
- package/android-multitouch-helper/dist/{agent-device-android-multitouch-helper-0.18.3.manifest.json → agent-device-android-multitouch-helper-0.19.1.manifest.json} +4 -4
- package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.18.3.apk → agent-device-android-snapshot-helper-0.19.1.apk} +0 -0
- package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.19.1.apk.sha256 +1 -0
- package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.18.3.manifest.json → agent-device-android-snapshot-helper-0.19.1.manifest.json} +6 -6
- package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTapPointPolicy.swift +28 -0
- package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+AXSnapshotFallback.swift +33 -127
- package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +630 -177
- package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandJournal.swift +155 -0
- package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+FlatSnapshotFiltering.swift +67 -0
- package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +173 -122
- package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Navigation.swift +172 -0
- package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScrollGesture.swift +64 -0
- package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SequenceExecution.swift +76 -182
- package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift +92 -51
- package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotCapturePlan.swift +213 -137
- package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedGesturePolicy.swift +142 -0
- package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntry.swift +208 -46
- package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Transport.swift +82 -11
- package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests.swift +74 -0
- package/dist/src/agent-browser-tool.js +1 -0
- package/dist/src/android-adb.d.ts +63 -234
- package/dist/src/android-adb.js +1 -1
- package/dist/src/android-input-ownership.js +6 -0
- package/dist/src/android.js +1 -5
- package/dist/src/app-lifecycle.js +7 -0
- package/dist/src/apple-runner-options.js +1 -0
- package/dist/src/apps.js +5 -11
- package/dist/src/artifacts.d.ts +4 -3
- package/dist/src/artifacts.js +1 -1
- package/dist/src/audio-probe.js +1 -1
- package/dist/src/batch.d.ts +2 -177
- package/dist/src/batch.js +1 -1
- package/dist/src/cli-help.js +178 -60
- package/dist/src/cli-output.js +1 -1
- package/dist/src/cli-runner.js +2 -0
- package/dist/src/cli.js +12 -41
- package/dist/src/client-metro-companion.js +1 -0
- package/dist/src/client.js +3 -0
- package/dist/src/command-metadata.js +1 -0
- package/dist/src/command-schema.js +22 -0
- package/dist/src/command-surface.js +1 -1
- package/dist/src/contracts.d.ts +2 -213
- package/dist/src/contracts.js +1 -1
- package/dist/src/debug-symbols.js +5 -3
- package/dist/src/device-input-state.js +2 -0
- package/dist/src/devices.js +1 -1
- package/dist/src/devices2.js +1 -0
- package/dist/src/devices3.js +1 -0
- package/dist/src/find.js +1 -1
- package/dist/src/finders.d.ts +2 -81
- package/dist/src/finders.js +1 -1
- package/dist/src/generic.js +1 -1
- package/dist/src/handler-utils.js +1 -1
- package/dist/src/helper.js +2 -0
- package/dist/src/host-process.js +2 -0
- package/dist/src/index.d.ts +1444 -1786
- package/dist/src/index.js +1 -1
- package/dist/src/input-actions.js +3 -1
- package/dist/src/input-actions2.js +1 -0
- package/dist/src/install-source.d.ts +13 -14
- package/dist/src/install-source.js +1 -1
- package/dist/src/install-source2.js +1 -0
- package/dist/src/interaction-snapshot.js +1 -0
- package/dist/src/interaction.js +1 -1
- package/dist/src/interactor.js +2 -1
- package/dist/src/internal/bin.d.ts +1 -1
- package/dist/src/internal/bin.js +2 -5
- package/dist/src/internal/companion-tunnel.d.ts +1 -1
- package/dist/src/internal/companion-tunnel.js +1 -1
- package/dist/src/internal/daemon.d.ts +1 -1
- package/dist/src/internal/daemon.js +18 -1
- package/dist/src/internal/png-worker.d.ts +29 -26
- package/dist/src/internal/png-worker.js +1 -1
- package/dist/src/internal/update-check-entry.d.ts +1 -1
- package/dist/src/internal/update-check-entry.js +1 -1
- package/dist/src/io.d.ts +2 -85
- package/dist/src/io.js +1 -1
- package/dist/src/lease.js +1 -1
- package/dist/src/linux.js +1 -1
- package/dist/src/manifest.js +1 -0
- package/dist/src/metro.d.ts +69 -113
- package/dist/src/metro.js +1 -1
- package/dist/src/metro2.js +1 -0
- package/dist/src/notifications.js +1 -1
- package/dist/src/output-path.js +1 -0
- package/dist/src/path-resolution.js +1 -0
- package/dist/src/png-worker-contract.js +1 -0
- package/dist/src/points.js +4 -0
- package/dist/src/project-runtime.js +1 -0
- package/dist/src/provider.js +1 -1
- package/dist/src/providers.js +1 -0
- package/dist/src/react-native.js +1 -1
- package/dist/src/record-trace-recording.js +2 -5
- package/dist/src/record-trace.js +1 -1
- package/dist/src/recording-provider.js +1 -1
- package/dist/src/registry.js +38 -0
- package/dist/src/remote-config.d.ts +2 -83
- package/dist/src/reporting.js +9 -0
- package/dist/src/rolldown-runtime.js +1 -0
- package/dist/src/runner-client.js +1 -0
- package/dist/src/runner-provider.js +1 -0
- package/dist/src/runner-sequence.js +1 -1
- package/dist/src/runtime-transport.js +1 -0
- package/dist/src/runtime.js +3 -0
- package/dist/src/scoped-provider.js +1 -0
- package/dist/src/sdk-android-adb.d.ts +144 -0
- package/dist/src/sdk-android-adb.js +4 -0
- package/dist/src/sdk-app-inventory.d.ts +4 -0
- package/dist/src/sdk-app-inventory.js +1 -0
- package/dist/src/sdk-batch-runner.d.ts +39 -0
- package/dist/src/sdk-batch-runner.js +2 -0
- package/dist/src/sdk-batch.js +2 -0
- package/dist/src/sdk-contracts.d.ts +386 -0
- package/dist/src/sdk-contracts.js +2 -0
- package/dist/src/sdk-finders.d.ts +60 -0
- package/dist/src/sdk-finders.js +1 -0
- package/dist/src/sdk-io.d.ts +78 -0
- package/dist/src/sdk-io.js +1 -0
- package/dist/src/sdk-remote-config.d.ts +139 -0
- package/dist/src/sdk-remote-config.js +1 -0
- package/dist/src/sdk-selectors.js +1 -0
- package/dist/src/selector-runtime.js +1 -1
- package/dist/src/selectors.d.ts +38 -137
- package/dist/src/selectors.js +1 -1
- package/dist/src/selectors2.js +1 -0
- package/dist/src/server.js +3 -2
- package/dist/src/session.js +10 -11
- package/dist/src/setting-state.js +1 -0
- package/dist/src/simctl.js +1 -0
- package/dist/src/simulator.js +1 -2
- package/dist/src/snapshot-diagnostics.js +3 -0
- package/dist/src/snapshot-helper.js +5 -0
- package/dist/src/snapshot-quality.js +1 -0
- package/dist/src/snapshot.js +1 -2
- package/dist/src/snapshot2.js +2 -0
- package/dist/src/{5560.js → timeouts.js} +1 -1
- package/dist/src/tool-provider.js +3 -0
- package/dist/src/tool-provider2.js +1 -0
- package/dist/src/ttl-memo.js +1 -0
- package/dist/src/tv-remote.js +14 -0
- package/dist/src/update-check.js +1 -0
- package/dist/src/url.js +1 -0
- package/dist/src/version.js +1 -0
- package/dist/src/web.js +1 -1
- package/dist/src/xml.js +2 -0
- package/package.json +13 -19
- package/server.json +2 -2
- package/skills/agent-device/SKILL.md +8 -7
- package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.18.3.apk.sha256 +0 -1
- package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.18.3.apk.sha256 +0 -1
- package/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandJournal.swift +0 -443
- package/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+FlatSnapshotFiltering.swift +0 -169
- package/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScrollGesture.swift +0 -220
- package/dist/src/1404.js +0 -1
- package/dist/src/1986.js +0 -1
- package/dist/src/2748.js +0 -1
- package/dist/src/2948.js +0 -30
- package/dist/src/3006.js +0 -1
- package/dist/src/314.js +0 -1
- package/dist/src/3340.js +0 -8
- package/dist/src/4418.js +0 -1
- package/dist/src/4428.js +0 -3
- package/dist/src/485.js +0 -1
- package/dist/src/4917.js +0 -1
- package/dist/src/495.js +0 -5
- package/dist/src/5448.js +0 -3
- package/dist/src/5484.js +0 -1
- package/dist/src/5643.js +0 -1
- package/dist/src/587.js +0 -1
- package/dist/src/5913.js +0 -4
- package/dist/src/6849.js +0 -1
- package/dist/src/6918.js +0 -1
- package/dist/src/6967.js +0 -1
- package/dist/src/760.js +0 -1
- package/dist/src/7871.js +0 -1
- package/dist/src/8184.js +0 -1
- package/dist/src/8225.js +0 -1
- package/dist/src/8391.js +0 -1
- package/dist/src/8404.js +0 -2
- package/dist/src/8414.js +0 -2
- package/dist/src/8875.js +0 -4
- package/dist/src/893.js +0 -1
- package/dist/src/8936.js +0 -1
- package/dist/src/9414.js +0 -1
- package/dist/src/9581.js +0 -22
- package/dist/src/9612.js +0 -1
- package/dist/src/9624.js +0 -1
- package/dist/src/9722.js +0 -39
- package/dist/src/devices~1.js +0 -1
- package/dist/src/devices~2.js +0 -1
- package/dist/src/input-actions~1.js +0 -1
- package/dist/src/rslib-runtime.js +0 -1
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunner/AgentDeviceRunnerApp.m +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/project.pbxproj +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/xcshareddata/xcschemes/AgentDeviceRunner.xcscheme +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/AgentDeviceRunnerUITests-Bridging-Header.h +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/AgentDeviceRunnerUITests.entitlements +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAXSnapshotBridge.h +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAXSnapshotBridge.m +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerObjCExceptionCatcher.h +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerObjCExceptionCatcher.m +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.h +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.m +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Alert.swift +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Environment.swift +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Exceptions.swift +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Lifecycle.swift +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScreenRecorder.swift +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SystemModal.swift +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TvRemote.swift +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests.xctestplan +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/RecordingScripts/recording-overlay.swift +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/RecordingScripts/recording-resize.swift +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/RecordingScripts/recording-trim.swift +0 -0
|
@@ -99,7 +99,7 @@ extension RunnerTests {
|
|
|
99
99
|
.table
|
|
100
100
|
]
|
|
101
101
|
|
|
102
|
-
|
|
102
|
+
static let flatInteractiveFallbackBudget: TimeInterval = 1.0
|
|
103
103
|
|
|
104
104
|
func snapshotFast(app: XCUIApplication, options: SnapshotOptions) throws -> DataPayload {
|
|
105
105
|
if let blocking = blockingSystemAlertSnapshot() {
|
|
@@ -302,7 +302,11 @@ extension RunnerTests {
|
|
|
302
302
|
return DataPayload(nodes: nodes, truncated: false)
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
-
func snapshotFlatInteractive(
|
|
305
|
+
func snapshotFlatInteractive(
|
|
306
|
+
app: XCUIApplication,
|
|
307
|
+
options: SnapshotOptions,
|
|
308
|
+
planDeadline: Date = .distantFuture
|
|
309
|
+
) -> DataPayload {
|
|
306
310
|
var nodes: [SnapshotNode] = [
|
|
307
311
|
interactiveRootNode(rect: .zero)
|
|
308
312
|
]
|
|
@@ -310,9 +314,12 @@ extension RunnerTests {
|
|
|
310
314
|
return DataPayload(nodes: nodes, truncated: false)
|
|
311
315
|
}
|
|
312
316
|
|
|
313
|
-
|
|
317
|
+
// Bounded by both its own sweep budget and the umbrella capture-plan deadline, so a
|
|
318
|
+
// chained recovery tier can never push the plan past the main-thread watchdog (#1105).
|
|
319
|
+
let sweepDeadline = options.interactiveOnly
|
|
314
320
|
? Date().addingTimeInterval(Self.flatInteractiveFallbackBudget)
|
|
315
321
|
: Date.distantFuture
|
|
322
|
+
let deadline = min(sweepDeadline, planDeadline)
|
|
316
323
|
let viewport = safeSnapshotViewport(app: app)
|
|
317
324
|
var seen = Set<String>()
|
|
318
325
|
var candidates: [SnapshotNode] = []
|
|
@@ -380,6 +387,7 @@ extension RunnerTests {
|
|
|
380
387
|
|
|
381
388
|
func snapshotAccessibilityUnavailable(failure: SnapshotCaptureFailure) -> DataPayload {
|
|
382
389
|
NSLog("AGENT_DEVICE_RUNNER_SNAPSHOT_AX_UNAVAILABLE=%@", failure.message)
|
|
390
|
+
runnerAccessibilityHealth = .unavailable
|
|
383
391
|
invalidateCachedTarget(reason: Self.axSnapshotUnavailableReason)
|
|
384
392
|
// This is a planned terminal result, so it carries the structured verdict like every other
|
|
385
393
|
// planned snapshot — downstream sparse handling keys off the verdict, not node shapes.
|
|
@@ -426,50 +434,6 @@ extension RunnerTests {
|
|
|
426
434
|
)
|
|
427
435
|
}
|
|
428
436
|
|
|
429
|
-
#if AGENT_DEVICE_RUNNER_UNIT_TESTS
|
|
430
|
-
func testSnapshotAccessibilityUnavailableMarksSparseSnapshotRunnerFatal() {
|
|
431
|
-
currentApp = app
|
|
432
|
-
currentBundleId = "com.example.app"
|
|
433
|
-
|
|
434
|
-
let payload = snapshotAccessibilityUnavailable(
|
|
435
|
-
failure: SnapshotCaptureFailure(
|
|
436
|
-
code: Self.axSnapshotErrorCode,
|
|
437
|
-
message: Self.axSnapshotFailureMessage,
|
|
438
|
-
hint: Self.axSnapshotHint
|
|
439
|
-
)
|
|
440
|
-
)
|
|
441
|
-
|
|
442
|
-
XCTAssertEqual(payload.message, "\(Self.axSnapshotFailureMessage) Hint: \(Self.axSnapshotHint)")
|
|
443
|
-
XCTAssertEqual(payload.nodes?.count, 1)
|
|
444
|
-
XCTAssertEqual(payload.nodes?.first?.type, "Application")
|
|
445
|
-
XCTAssertEqual(payload.truncated, true)
|
|
446
|
-
XCTAssertEqual(payload.runnerFatal, true)
|
|
447
|
-
XCTAssertEqual(payload.runnerFatalReason, Self.axSnapshotUnavailableReason)
|
|
448
|
-
XCTAssertNil(currentApp)
|
|
449
|
-
XCTAssertNil(currentBundleId)
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
func testRecoveredSnapshotMessagePreservesHint() {
|
|
453
|
-
let message = recoveredSnapshotMessage(
|
|
454
|
-
SnapshotCaptureFailure(
|
|
455
|
-
code: Self.axSnapshotErrorCode,
|
|
456
|
-
message: Self.axSnapshotFailureMessage,
|
|
457
|
-
hint: Self.axSnapshotHint
|
|
458
|
-
)
|
|
459
|
-
)
|
|
460
|
-
|
|
461
|
-
XCTAssertTrue(message.contains(Self.axSnapshotFailureMessage))
|
|
462
|
-
XCTAssertTrue(message.contains(Self.axSnapshotHint))
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
func testRawSnapshotTooLargeFailureIsStructured() {
|
|
466
|
-
let failure = rawSnapshotTooLargeFailure(nodeCount: Self.rawSnapshotMaxNodes + 1)
|
|
467
|
-
|
|
468
|
-
XCTAssertEqual(failure.code, Self.rawSnapshotTooLargeCode)
|
|
469
|
-
XCTAssertTrue(failure.message.contains("\(Self.rawSnapshotMaxNodes) nodes"))
|
|
470
|
-
XCTAssertEqual(failure.hint, Self.rawSnapshotTooLargeHint)
|
|
471
|
-
}
|
|
472
|
-
#endif
|
|
473
437
|
|
|
474
438
|
private func interactiveRootNode(rect: CGRect) -> SnapshotNode {
|
|
475
439
|
SnapshotNode(
|
|
@@ -562,12 +526,24 @@ extension RunnerTests {
|
|
|
562
526
|
|
|
563
527
|
func makeSnapshotTraversalContext(
|
|
564
528
|
app: XCUIApplication,
|
|
565
|
-
options: SnapshotOptions
|
|
529
|
+
options: SnapshotOptions,
|
|
530
|
+
captureDeadline: Date = .distantFuture,
|
|
531
|
+
treeCaptureSliceBudgetOverride: TimeInterval? = nil
|
|
566
532
|
) throws -> SnapshotTraversalContext? {
|
|
567
|
-
let viewport =
|
|
568
|
-
|
|
533
|
+
let (viewport, queryRoot) = try runMainThreadWork(
|
|
534
|
+
command: nil,
|
|
535
|
+
timeout: min(1.0, max(0.1, captureDeadline.timeIntervalSinceNow)),
|
|
536
|
+
timeoutError: snapshotMainThreadTimeoutError("preparing tree snapshot")
|
|
537
|
+
) {
|
|
538
|
+
(
|
|
539
|
+
self.safeSnapshotViewport(app: app),
|
|
540
|
+
options.scope.flatMap { self.findScopeElement(app: app, scope: $0) } ?? app
|
|
541
|
+
)
|
|
542
|
+
}
|
|
569
543
|
|
|
570
|
-
|
|
544
|
+
let treeSliceBudget = treeCaptureSliceBudgetOverride ?? treeCaptureSliceBudget
|
|
545
|
+
let slice = min(treeSliceBudget, max(0.5, captureDeadline.timeIntervalSinceNow))
|
|
546
|
+
guard let rootSnapshot = try captureSnapshotRootBounded(queryRoot, sliceSeconds: slice) else {
|
|
571
547
|
return nil
|
|
572
548
|
}
|
|
573
549
|
|
|
@@ -582,6 +558,71 @@ extension RunnerTests {
|
|
|
582
558
|
)
|
|
583
559
|
}
|
|
584
560
|
|
|
561
|
+
static let xCTestSnapshotTimeoutCode = "IOS_TREE_CAPTURE_TIMEOUT"
|
|
562
|
+
|
|
563
|
+
func hasAbandonedTreeCapture() -> Bool {
|
|
564
|
+
treeCaptureLock.lock()
|
|
565
|
+
defer { treeCaptureLock.unlock() }
|
|
566
|
+
return abandonedTreeCaptureCount > 0
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/// Runs the blocking tree-snapshot XPC on the main thread bounded by `sliceSeconds`. On
|
|
570
|
+
/// timeout the XPC keeps running on main (it cannot be cancelled); the capture is marked
|
|
571
|
+
/// abandoned so plans avoid XCTest-backed tiers until it drains, the tree backend is penalized
|
|
572
|
+
/// for this bundle, and the plan moves to the platform's independent recovery tier when one
|
|
573
|
+
/// exists (#1105/#1122).
|
|
574
|
+
private func captureSnapshotRootBounded(
|
|
575
|
+
_ element: XCUIElement,
|
|
576
|
+
sliceSeconds: TimeInterval
|
|
577
|
+
) throws -> XCUIElementSnapshot? {
|
|
578
|
+
if Thread.isMainThread {
|
|
579
|
+
return try captureSnapshotRoot(element)
|
|
580
|
+
}
|
|
581
|
+
return try runMainThreadWork(
|
|
582
|
+
command: nil,
|
|
583
|
+
timeout: sliceSeconds,
|
|
584
|
+
timeoutError: treeCaptureTimeoutError(sliceSeconds: sliceSeconds),
|
|
585
|
+
onAbandoned: {
|
|
586
|
+
self.treeCaptureLock.lock()
|
|
587
|
+
self.abandonedTreeCaptureCount += 1
|
|
588
|
+
self.treeCaptureLock.unlock()
|
|
589
|
+
NSLog("AGENT_DEVICE_RUNNER_TREE_CAPTURE_SLICE_TIMEOUT slice=%.1f", sliceSeconds)
|
|
590
|
+
self.penalizeSnapshotXCTestChannel(
|
|
591
|
+
bundleId: self.currentBundleId,
|
|
592
|
+
reason: "tree_capture_slice_timeout"
|
|
593
|
+
)
|
|
594
|
+
},
|
|
595
|
+
onDrained: {
|
|
596
|
+
self.treeCaptureLock.lock()
|
|
597
|
+
self.abandonedTreeCaptureCount -= 1
|
|
598
|
+
self.treeCaptureLock.unlock()
|
|
599
|
+
NSLog("AGENT_DEVICE_RUNNER_TREE_CAPTURE_DRAINED")
|
|
600
|
+
}
|
|
601
|
+
) {
|
|
602
|
+
try self.captureSnapshotRoot(element)
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
private func treeCaptureTimeoutError(sliceSeconds: TimeInterval) -> () -> Error {
|
|
607
|
+
{
|
|
608
|
+
SnapshotCaptureFailure(
|
|
609
|
+
code: Self.xCTestSnapshotTimeoutCode,
|
|
610
|
+
message: "the XCTest tree capture exceeded its \(Int(sliceSeconds))s time slice",
|
|
611
|
+
hint: "The capture plan will avoid or tightly bound XCTest-backed snapshot tiers on this screen."
|
|
612
|
+
)
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
func snapshotMainThreadTimeoutError(_ operation: String) -> () -> Error {
|
|
617
|
+
{
|
|
618
|
+
SnapshotCaptureFailure(
|
|
619
|
+
code: Self.xCTestSnapshotTimeoutCode,
|
|
620
|
+
message: "timed out while \(operation) on the XCTest main thread",
|
|
621
|
+
hint: "The capture plan will skip XCTest-backed snapshot tiers while the previous main-thread work drains."
|
|
622
|
+
)
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
585
626
|
private func captureSnapshotRoot(_ element: XCUIElement) throws -> XCUIElementSnapshot? {
|
|
586
627
|
var rootSnapshot: XCUIElementSnapshot?
|
|
587
628
|
var swiftErrorMessage: String?
|
|
@@ -28,6 +28,40 @@ enum SnapshotBackendKind: String, CaseIterable {
|
|
|
28
28
|
case recursiveTree = "tree"
|
|
29
29
|
case querySweep = "queries"
|
|
30
30
|
case privateAX = "private-ax"
|
|
31
|
+
|
|
32
|
+
var usesXCTestAccessibilityChannel: Bool {
|
|
33
|
+
switch self {
|
|
34
|
+
case .recursiveTree, .querySweep:
|
|
35
|
+
return true
|
|
36
|
+
case .privateAX:
|
|
37
|
+
return false
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
var isAvailableOnCurrentPlatform: Bool {
|
|
42
|
+
switch self {
|
|
43
|
+
case .recursiveTree, .querySweep:
|
|
44
|
+
return true
|
|
45
|
+
case .privateAX:
|
|
46
|
+
#if os(iOS) && targetEnvironment(simulator)
|
|
47
|
+
return true
|
|
48
|
+
#else
|
|
49
|
+
return false
|
|
50
|
+
#endif
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
enum SnapshotXCTestChannelPlanState: Equatable {
|
|
56
|
+
case normal
|
|
57
|
+
case deferredToIndependentBackend
|
|
58
|
+
case boundedXCTestProbe
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
struct EffectiveSnapshotCapturePlan {
|
|
62
|
+
let plan: [SnapshotBackendKind]
|
|
63
|
+
let xCTestChannelState: SnapshotXCTestChannelPlanState
|
|
64
|
+
let treeCaptureSliceBudgetOverride: TimeInterval?
|
|
31
65
|
}
|
|
32
66
|
|
|
33
67
|
/// What the plan runner does when every backend failed or stayed sparse.
|
|
@@ -52,6 +86,7 @@ extension RunnerTests {
|
|
|
52
86
|
/// but chained recovery tiers must never stack past the 30s main-thread watchdog: when the
|
|
53
87
|
/// budget is spent, remaining tiers are skipped and the best payload so far is returned.
|
|
54
88
|
static let snapshotPlanBudget: TimeInterval = 20
|
|
89
|
+
static let penalizedXCTestProbeTreeSliceBudget: TimeInterval = 1
|
|
55
90
|
static let collapsedLeafMinimumSegments = 10
|
|
56
91
|
|
|
57
92
|
static func payloadNodeCount(_ payload: DataPayload?) -> Int {
|
|
@@ -63,6 +98,70 @@ extension RunnerTests {
|
|
|
63
98
|
static let regularVisiblePlan: [SnapshotBackendKind] = [.recursiveTree, .querySweep, .privateAX]
|
|
64
99
|
static let rawDiagnosticPlan: [SnapshotBackendKind] = [.recursiveTree, .privateAX]
|
|
65
100
|
|
|
101
|
+
// MARK: XCTest accessibility channel penalty (cross-attempt memory, #1105/#1156)
|
|
102
|
+
//
|
|
103
|
+
// On some deep/dynamic screens the XCTest bulk snapshot no longer fails fast with
|
|
104
|
+
// kAXErrorIllegalArgument (the #758 signature) — it grinds for many seconds first. One slow
|
|
105
|
+
// grind is tolerable; re-grinding on every subsequent capture of the same screen buries the
|
|
106
|
+
// main thread past the execution watchdog. After a slow, timed-out, or abandoned XCTest-backed
|
|
107
|
+
// capture, later plans for the same bundle use non-XCTest recovery tiers until the penalty expires.
|
|
108
|
+
|
|
109
|
+
func penalizeSnapshotXCTestChannel(bundleId: String?, reason: String) {
|
|
110
|
+
snapshotXCTestChannelPenaltyLock.lock()
|
|
111
|
+
snapshotXCTestChannelPenaltyBundleId = bundleId
|
|
112
|
+
snapshotXCTestChannelPenaltyUntil = Date().addingTimeInterval(snapshotXCTestChannelPenaltyDuration)
|
|
113
|
+
snapshotXCTestChannelPenaltyLock.unlock()
|
|
114
|
+
NSLog(
|
|
115
|
+
"AGENT_DEVICE_RUNNER_SNAPSHOT_XCTEST_CHANNEL_PENALIZED bundle=%@ reason=%@",
|
|
116
|
+
bundleId ?? "",
|
|
117
|
+
reason
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
func isSnapshotXCTestChannelPenalized(bundleId: String?) -> Bool {
|
|
122
|
+
snapshotXCTestChannelPenaltyLock.lock()
|
|
123
|
+
defer { snapshotXCTestChannelPenaltyLock.unlock() }
|
|
124
|
+
guard Date() < snapshotXCTestChannelPenaltyUntil else { return false }
|
|
125
|
+
// A penalty recorded without a bundle id applies to whatever target is current.
|
|
126
|
+
guard let penalized = snapshotXCTestChannelPenaltyBundleId else { return true }
|
|
127
|
+
return penalized == bundleId
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/// Pure plan-reorder rule: a penalized XCTest accessibility channel uses independent backends
|
|
131
|
+
/// when the platform has one, otherwise it keeps XCTest work on a short probe. The raw
|
|
132
|
+
/// diagnostic plan keeps tree-first errors, and unknown plans are left untouched.
|
|
133
|
+
static func effectiveSnapshotCapturePlan(
|
|
134
|
+
_ plan: [SnapshotBackendKind],
|
|
135
|
+
xCTestChannelPenalized: Bool,
|
|
136
|
+
availableBackends: Set<SnapshotBackendKind> = Set(SnapshotBackendKind.allCases)
|
|
137
|
+
) -> EffectiveSnapshotCapturePlan {
|
|
138
|
+
guard xCTestChannelPenalized, plan == Self.regularVisiblePlan else {
|
|
139
|
+
return EffectiveSnapshotCapturePlan(
|
|
140
|
+
plan: plan,
|
|
141
|
+
xCTestChannelState: .normal,
|
|
142
|
+
treeCaptureSliceBudgetOverride: nil
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
let availablePlan = plan.filter { availableBackends.contains($0) }
|
|
146
|
+
let recoveryPlan = availablePlan.filter { !$0.usesXCTestAccessibilityChannel }
|
|
147
|
+
if !recoveryPlan.isEmpty {
|
|
148
|
+
return EffectiveSnapshotCapturePlan(
|
|
149
|
+
plan: recoveryPlan,
|
|
150
|
+
xCTestChannelState: .deferredToIndependentBackend,
|
|
151
|
+
treeCaptureSliceBudgetOverride: nil
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
return EffectiveSnapshotCapturePlan(
|
|
155
|
+
plan: availablePlan.filter(\.usesXCTestAccessibilityChannel),
|
|
156
|
+
xCTestChannelState: .boundedXCTestProbe,
|
|
157
|
+
treeCaptureSliceBudgetOverride: Self.penalizedXCTestProbeTreeSliceBudget
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
func shouldSkipSnapshotBackendForAbandonedTreeCapture(_ kind: SnapshotBackendKind) -> Bool {
|
|
162
|
+
kind.usesXCTestAccessibilityChannel && hasAbandonedTreeCapture()
|
|
163
|
+
}
|
|
164
|
+
|
|
66
165
|
// MARK: Plan runner
|
|
67
166
|
|
|
68
167
|
func runSnapshotCapturePlan(
|
|
@@ -76,21 +175,76 @@ extension RunnerTests {
|
|
|
76
175
|
var axFailure: SnapshotCaptureFailure?
|
|
77
176
|
let deadline = Date().addingTimeInterval(Self.snapshotPlanBudget)
|
|
78
177
|
|
|
79
|
-
|
|
80
|
-
|
|
178
|
+
// Reorder is iOS-only because hostile screens can make XCTest tree/query work grind while
|
|
179
|
+
// the app remains visually responsive. Simulators can avoid that channel through private AX;
|
|
180
|
+
// physical devices have no independent semantic backend yet, so they use a bounded probe.
|
|
181
|
+
var xCTestChannelPenalized = false
|
|
182
|
+
#if os(iOS)
|
|
183
|
+
xCTestChannelPenalized = isSnapshotXCTestChannelPenalized(bundleId: currentBundleId)
|
|
184
|
+
#endif
|
|
185
|
+
let effective = Self.effectiveSnapshotCapturePlan(
|
|
186
|
+
plan,
|
|
187
|
+
xCTestChannelPenalized: xCTestChannelPenalized,
|
|
188
|
+
availableBackends: Set(SnapshotBackendKind.allCases.filter(\.isAvailableOnCurrentPlatform))
|
|
189
|
+
)
|
|
190
|
+
let effectivePlan = effective.plan
|
|
191
|
+
switch effective.xCTestChannelState {
|
|
192
|
+
case .normal:
|
|
193
|
+
break
|
|
194
|
+
case .deferredToIndependentBackend:
|
|
195
|
+
firstFailure = (
|
|
196
|
+
"XCTest-backed snapshot tiers were deferred after recent slow accessibility work on this screen",
|
|
197
|
+
"budget"
|
|
198
|
+
)
|
|
199
|
+
NSLog("AGENT_DEVICE_RUNNER_SNAPSHOT_XCTEST_CHANNEL_DEFERRED bundle=%@", currentBundleId ?? "")
|
|
200
|
+
case .boundedXCTestProbe:
|
|
201
|
+
firstFailure = (
|
|
202
|
+
"XCTest-backed snapshot tiers are running with a short recovery probe after recent slow accessibility work on this screen",
|
|
203
|
+
"budget"
|
|
204
|
+
)
|
|
205
|
+
NSLog("AGENT_DEVICE_RUNNER_SNAPSHOT_XCTEST_CHANNEL_PROBE_BOUNDED bundle=%@", currentBundleId ?? "")
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
for kind in effectivePlan {
|
|
209
|
+
if kind != effectivePlan.first && Date() >= deadline {
|
|
81
210
|
NSLog("AGENT_DEVICE_RUNNER_SNAPSHOT_PLAN_BUDGET_EXHAUSTED skipped=%@", kind.rawValue)
|
|
82
211
|
if firstFailure == nil {
|
|
83
212
|
firstFailure = ("the capture plan ran out of its time budget", "budget")
|
|
84
213
|
}
|
|
85
214
|
break
|
|
86
215
|
}
|
|
216
|
+
// While an abandoned tree capture is still grinding inside testmanagerd, XCTest-backed
|
|
217
|
+
// tiers would block behind it; only independent backends stay responsive (#1105).
|
|
218
|
+
if shouldSkipSnapshotBackendForAbandonedTreeCapture(kind) {
|
|
219
|
+
NSLog("AGENT_DEVICE_RUNNER_SNAPSHOT_TIER_SKIPPED_XCTEST_OCCUPIED tier=%@", kind.rawValue)
|
|
220
|
+
if firstFailure == nil {
|
|
221
|
+
firstFailure = (
|
|
222
|
+
"the XCTest capture channel is occupied by an abandoned tree capture",
|
|
223
|
+
"budget"
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
continue
|
|
227
|
+
}
|
|
87
228
|
let capture: SnapshotBackendCapture
|
|
229
|
+
let backendStartedAt = Date()
|
|
88
230
|
do {
|
|
89
|
-
guard
|
|
231
|
+
guard
|
|
232
|
+
let result = try captureWithBackend(
|
|
233
|
+
kind,
|
|
234
|
+
app: app,
|
|
235
|
+
options: options,
|
|
236
|
+
deadline: deadline,
|
|
237
|
+
treeCaptureSliceBudgetOverride: effective.treeCaptureSliceBudgetOverride
|
|
238
|
+
)
|
|
239
|
+
else {
|
|
240
|
+
recordSlowXCTestSnapshotBackendIfNeeded(kind, startedAt: backendStartedAt)
|
|
90
241
|
continue
|
|
91
242
|
}
|
|
92
243
|
capture = result
|
|
244
|
+
recordSlowXCTestSnapshotBackendIfNeeded(kind, startedAt: backendStartedAt)
|
|
93
245
|
} catch let failure as SnapshotCaptureFailure {
|
|
246
|
+
recordXCTestSnapshotBackendFailureIfNeeded(kind, failure: failure)
|
|
247
|
+
recordSlowXCTestSnapshotBackendIfNeeded(kind, startedAt: backendStartedAt)
|
|
94
248
|
if Self.isAxSnapshotFailure(failure) { axFailure = failure }
|
|
95
249
|
if firstFailure == nil {
|
|
96
250
|
firstFailure = (failure.message, Self.isAxSnapshotFailure(failure) ? "ax-rejected" : "capture-failed")
|
|
@@ -111,7 +265,7 @@ extension RunnerTests {
|
|
|
111
265
|
continue
|
|
112
266
|
}
|
|
113
267
|
|
|
114
|
-
let recovered = kind !=
|
|
268
|
+
let recovered = kind != effectivePlan.first || effective.xCTestChannelState != .normal
|
|
115
269
|
if recovered {
|
|
116
270
|
NSLog(
|
|
117
271
|
"AGENT_DEVICE_RUNNER_SNAPSHOT_RECOVERED backend=%@ reason=%@",
|
|
@@ -150,34 +304,76 @@ extension RunnerTests {
|
|
|
150
304
|
best.map { stampedSnapshotPayload($0.capture, backend: $0.kind, state: "sparse", reason: firstFailure) }
|
|
151
305
|
?? stampedSnapshotPayload(
|
|
152
306
|
SnapshotBackendCapture(payload: sparseTruncatedSnapshotPayload(), effectiveDepth: nil),
|
|
153
|
-
backend: plan.last ?? .recursiveTree,
|
|
307
|
+
backend: effectivePlan.last ?? plan.last ?? .recursiveTree,
|
|
154
308
|
state: "sparse",
|
|
155
309
|
reason: firstFailure
|
|
156
310
|
)
|
|
157
311
|
return fallbackPayload
|
|
158
312
|
}
|
|
159
313
|
|
|
314
|
+
/// Marks XCTest-backed snapshot tiers as penalized when one attempt ground past the slow-capture
|
|
315
|
+
/// threshold — even a successful one: the next capture of this screen must not re-grind.
|
|
316
|
+
private func recordSlowXCTestSnapshotBackendIfNeeded(_ kind: SnapshotBackendKind, startedAt: Date) {
|
|
317
|
+
guard kind.usesXCTestAccessibilityChannel else { return }
|
|
318
|
+
let elapsed = Date().timeIntervalSince(startedAt)
|
|
319
|
+
guard elapsed > snapshotXCTestSlowCaptureThreshold else { return }
|
|
320
|
+
penalizeSnapshotXCTestChannel(
|
|
321
|
+
bundleId: currentBundleId,
|
|
322
|
+
reason: "slow_\(kind.rawValue)_capture_\(Int(elapsed * 1000))ms"
|
|
323
|
+
)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private func recordXCTestSnapshotBackendFailureIfNeeded(
|
|
327
|
+
_ kind: SnapshotBackendKind,
|
|
328
|
+
failure: SnapshotCaptureFailure
|
|
329
|
+
) {
|
|
330
|
+
guard kind.usesXCTestAccessibilityChannel, failure.code == Self.xCTestSnapshotTimeoutCode else { return }
|
|
331
|
+
penalizeSnapshotXCTestChannel(
|
|
332
|
+
bundleId: currentBundleId,
|
|
333
|
+
reason: "\(kind.rawValue)_backend_timeout"
|
|
334
|
+
)
|
|
335
|
+
}
|
|
336
|
+
|
|
160
337
|
private func captureWithBackend(
|
|
161
338
|
_ kind: SnapshotBackendKind,
|
|
162
339
|
app: XCUIApplication,
|
|
163
|
-
options: SnapshotOptions
|
|
340
|
+
options: SnapshotOptions,
|
|
341
|
+
deadline: Date,
|
|
342
|
+
treeCaptureSliceBudgetOverride: TimeInterval?
|
|
164
343
|
) throws -> SnapshotBackendCapture? {
|
|
165
344
|
switch kind {
|
|
166
345
|
case .recursiveTree:
|
|
167
|
-
guard
|
|
346
|
+
guard
|
|
347
|
+
let context = try makeSnapshotTraversalContext(
|
|
348
|
+
app: app,
|
|
349
|
+
options: options,
|
|
350
|
+
captureDeadline: deadline,
|
|
351
|
+
treeCaptureSliceBudgetOverride: treeCaptureSliceBudgetOverride
|
|
352
|
+
)
|
|
353
|
+
else {
|
|
168
354
|
return nil
|
|
169
355
|
}
|
|
170
|
-
let payload =
|
|
171
|
-
|
|
172
|
-
:
|
|
356
|
+
let payload = try runMainThreadWork(
|
|
357
|
+
command: nil,
|
|
358
|
+
timeout: min(treeCaptureSliceBudget, max(0.5, deadline.timeIntervalSinceNow)),
|
|
359
|
+
timeoutError: snapshotMainThreadTimeoutError("processing tree snapshot")
|
|
360
|
+
) {
|
|
361
|
+
options.raw
|
|
362
|
+
? try self.rawTreeSnapshotPayload(context: context, options: options)
|
|
363
|
+
: self.recursiveTreeSnapshotPayload(context: context, options: options)
|
|
364
|
+
}
|
|
173
365
|
return SnapshotBackendCapture(payload: payload, effectiveDepth: nil)
|
|
174
366
|
case .querySweep:
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
367
|
+
let payload = try runMainThreadWork(
|
|
368
|
+
command: nil,
|
|
369
|
+
timeout: min(Self.flatInteractiveFallbackBudget, max(0.1, deadline.timeIntervalSinceNow)),
|
|
370
|
+
timeoutError: snapshotMainThreadTimeoutError("running query-sweep snapshot")
|
|
371
|
+
) {
|
|
372
|
+
self.snapshotFlatInteractive(app: app, options: options, planDeadline: deadline)
|
|
373
|
+
}
|
|
374
|
+
return SnapshotBackendCapture(payload: payload, effectiveDepth: nil)
|
|
179
375
|
case .privateAX:
|
|
180
|
-
return privateAXSnapshotCapture(app: app, options: options)
|
|
376
|
+
return privateAXSnapshotCapture(app: app, options: options, deadline: deadline)
|
|
181
377
|
}
|
|
182
378
|
}
|
|
183
379
|
|
|
@@ -255,6 +451,7 @@ extension RunnerTests {
|
|
|
255
451
|
state: String,
|
|
256
452
|
reason: (reason: String, code: String)?
|
|
257
453
|
) -> DataPayload {
|
|
454
|
+
runnerAccessibilityHealth = reason?.code == "ax-rejected" ? .unavailable : .healthy
|
|
258
455
|
let payload = capture.payload
|
|
259
456
|
let quality = SnapshotQuality(
|
|
260
457
|
state: state,
|
|
@@ -283,7 +480,7 @@ extension RunnerTests {
|
|
|
283
480
|
? " The primary capture ran out of its time budget (busy app or simulator); the recovered tree is authoritative for this screen."
|
|
284
481
|
: " This usually means the app publishes an unhealthy accessibility tree — fixing the app's accessibility is the real cure. Treat screenshot as visual truth when this warning appears."
|
|
285
482
|
parts.append(
|
|
286
|
-
"
|
|
483
|
+
"Detected an overly complex or slow accessibility tree. Fell back to the \(quality.backend) snapshot backend"
|
|
287
484
|
+ (quality.reason.map { " after: \($0)." } ?? ".")
|
|
288
485
|
+ meaning
|
|
289
486
|
)
|
|
@@ -304,124 +501,3 @@ extension RunnerTests {
|
|
|
304
501
|
}
|
|
305
502
|
}
|
|
306
503
|
|
|
307
|
-
#if AGENT_DEVICE_RUNNER_UNIT_TESTS
|
|
308
|
-
// MARK: - In-bundle unit tests
|
|
309
|
-
|
|
310
|
-
extension RunnerTests {
|
|
311
|
-
private func planTestNode(
|
|
312
|
-
index: Int,
|
|
313
|
-
type: String,
|
|
314
|
-
label: String? = nil,
|
|
315
|
-
identifier: String? = nil,
|
|
316
|
-
hittable: Bool = false,
|
|
317
|
-
parentIndex: Int? = nil
|
|
318
|
-
) -> SnapshotNode {
|
|
319
|
-
SnapshotNode(
|
|
320
|
-
index: index,
|
|
321
|
-
type: type,
|
|
322
|
-
label: label,
|
|
323
|
-
identifier: identifier,
|
|
324
|
-
value: nil,
|
|
325
|
-
rect: snapshotRect(from: .zero),
|
|
326
|
-
enabled: true,
|
|
327
|
-
focused: nil,
|
|
328
|
-
selected: nil,
|
|
329
|
-
hittable: hittable,
|
|
330
|
-
depth: parentIndex == nil ? 0 : 1,
|
|
331
|
-
parentIndex: parentIndex,
|
|
332
|
-
hiddenContentAbove: nil,
|
|
333
|
-
hiddenContentBelow: nil
|
|
334
|
-
)
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
func testSparsePayloadReasonMatrix() {
|
|
338
|
-
let root = planTestNode(index: 0, type: "Application", label: "Example App", hittable: true)
|
|
339
|
-
let window = planTestNode(index: 1, type: "Window", parentIndex: 0)
|
|
340
|
-
let button = planTestNode(index: 1, type: "Button", label: "Ok", hittable: true, parentIndex: 0)
|
|
341
|
-
|
|
342
|
-
// Labeled, hittable root over a bare window is still sparse.
|
|
343
|
-
XCTAssertNotNil(Self.sparsePayloadReason(DataPayload(nodes: [root, window], truncated: false)))
|
|
344
|
-
// Deadline-truncated near-empty sweep needs recovery even with one real control.
|
|
345
|
-
XCTAssertNotNil(Self.sparsePayloadReason(DataPayload(nodes: [root, button], truncated: true)))
|
|
346
|
-
// The same tiny tree from a completed sweep is a legitimately minimal screen.
|
|
347
|
-
XCTAssertNil(Self.sparsePayloadReason(DataPayload(nodes: [root, button], truncated: false)))
|
|
348
|
-
// Empty payloads are degraded.
|
|
349
|
-
XCTAssertNotNil(Self.sparsePayloadReason(DataPayload(nodes: [], truncated: false)))
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
func testCollapsedLeafIndexesFlagsMergedContainersOnly() {
|
|
353
|
-
let root = planTestNode(index: 0, type: "Application", label: "App")
|
|
354
|
-
let merged = planTestNode(
|
|
355
|
-
index: 1,
|
|
356
|
-
type: "Other",
|
|
357
|
-
label: (0...30).map { "Row \($0), Tap" }.joined(separator: ", "),
|
|
358
|
-
parentIndex: 0
|
|
359
|
-
)
|
|
360
|
-
let prose = planTestNode(
|
|
361
|
-
index: 2,
|
|
362
|
-
type: "StaticText",
|
|
363
|
-
label: (0...30).map { "clause \($0)" }.joined(separator: ", "),
|
|
364
|
-
parentIndex: 0
|
|
365
|
-
)
|
|
366
|
-
XCTAssertEqual(Self.collapsedLeafIndexes([root, merged, prose]), [1])
|
|
367
|
-
XCTAssertNil(Self.collapsedLeafIndexes([root, prose]))
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
func testLegacyQualityMessageStatesFallbackMeaning() {
|
|
371
|
-
let recovered = SnapshotQuality(
|
|
372
|
-
state: "recovered",
|
|
373
|
-
backend: "queries",
|
|
374
|
-
reason: "snapshot returned only structural application/window nodes",
|
|
375
|
-
reasonCode: "sparse-tree",
|
|
376
|
-
effectiveDepth: nil,
|
|
377
|
-
collapsedLeafIndexes: nil
|
|
378
|
-
)
|
|
379
|
-
let message = Self.legacyQualityMessage(recovered)
|
|
380
|
-
XCTAssertTrue(message?.contains("queries accessibility backend") == true)
|
|
381
|
-
XCTAssertTrue(message?.contains("fixing the app's accessibility") == true)
|
|
382
|
-
XCTAssertTrue(message?.contains("screenshot as visual truth") == true)
|
|
383
|
-
XCTAssertNil(
|
|
384
|
-
Self.legacyQualityMessage(
|
|
385
|
-
SnapshotQuality(
|
|
386
|
-
state: "healthy", backend: "tree", reason: nil, reasonCode: nil, effectiveDepth: nil,
|
|
387
|
-
collapsedLeafIndexes: nil)
|
|
388
|
-
)
|
|
389
|
-
)
|
|
390
|
-
}
|
|
391
|
-
func testTerminalFailsClosedOnInteractiveAxFailureRegardlessOfSparseBest() {
|
|
392
|
-
// Interactive AX failure must invalidate + fail closed; a later tier's sparse synthetic-root
|
|
393
|
-
// "best" must never downgrade this to a returned-sparse payload (regression: best == nil guard).
|
|
394
|
-
XCTAssertEqual(
|
|
395
|
-
Self.resolveSnapshotPlanTerminal(terminal: .sparseWithFatalOnAXFailure, interactiveOnly: true),
|
|
396
|
-
.failClosed
|
|
397
|
-
)
|
|
398
|
-
XCTAssertEqual(
|
|
399
|
-
Self.resolveSnapshotPlanTerminal(terminal: .sparseWithFatalOnAXFailure, interactiveOnly: false),
|
|
400
|
-
.sparseBest
|
|
401
|
-
)
|
|
402
|
-
XCTAssertEqual(
|
|
403
|
-
Self.resolveSnapshotPlanTerminal(terminal: .throwOnAXFailure, interactiveOnly: true),
|
|
404
|
-
.throwAxFailure
|
|
405
|
-
)
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
func testSnapshotAccessibilityUnavailableCarriesSparseVerdict() {
|
|
409
|
-
currentApp = app
|
|
410
|
-
currentBundleId = "com.example.app"
|
|
411
|
-
defer {
|
|
412
|
-
currentApp = nil
|
|
413
|
-
currentBundleId = nil
|
|
414
|
-
}
|
|
415
|
-
let payload = snapshotAccessibilityUnavailable(
|
|
416
|
-
failure: SnapshotCaptureFailure(
|
|
417
|
-
code: "IOS_AX_SNAPSHOT_FAILED",
|
|
418
|
-
message: "kAXErrorIllegalArgument",
|
|
419
|
-
hint: "use screenshot"
|
|
420
|
-
)
|
|
421
|
-
)
|
|
422
|
-
XCTAssertEqual(payload.runnerFatal, true)
|
|
423
|
-
XCTAssertEqual(payload.snapshotQuality?.state, "sparse")
|
|
424
|
-
XCTAssertEqual(payload.snapshotQuality?.reasonCode, "ax-rejected")
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
#endif
|