agent-device 0.19.0 → 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.19.0.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.19.0.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.19.0.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.19.0.manifest.json → agent-device-android-snapshot-helper-0.19.1.manifest.json} +6 -6
- package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+AXSnapshotFallback.swift +24 -7
- package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +506 -119
- package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +153 -48
- package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Navigation.swift +172 -0
- package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SequenceExecution.swift +76 -5
- package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift +60 -52
- package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotCapturePlan.swift +159 -56
- package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedGesturePolicy.swift +142 -0
- package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests.swift +16 -14
- package/dist/src/android-adb.d.ts +1 -2
- package/dist/src/android-adb.js +1 -1
- package/dist/src/android-input-ownership.js +6 -0
- package/dist/src/android.js +1 -1
- package/dist/src/app-lifecycle.js +3 -3
- package/dist/src/apple-runner-options.js +1 -1
- package/dist/src/apps.js +5 -5
- package/dist/src/cli-help.js +131 -34
- package/dist/src/cli-runner.js +1 -1
- package/dist/src/cli.js +11 -11
- package/dist/src/client.js +2 -2
- package/dist/src/command-metadata.js +1 -1
- package/dist/src/command-schema.js +3 -3
- package/dist/src/device-input-state.js +2 -2
- package/dist/src/devices2.js +1 -1
- package/dist/src/find.js +1 -1
- package/dist/src/generic.js +1 -1
- package/dist/src/index.d.ts +199 -20
- package/dist/src/input-actions.js +3 -3
- package/dist/src/interaction-snapshot.js +1 -1
- package/dist/src/interaction.js +1 -1
- package/dist/src/interactor.js +2 -2
- package/dist/src/internal/bin.js +1 -1
- package/dist/src/internal/daemon.js +17 -15
- package/dist/src/lease.js +1 -1
- package/dist/src/linux.js +1 -1
- package/dist/src/metro.d.ts +0 -1
- package/dist/src/notifications.js +1 -1
- package/dist/src/points.js +1 -1
- package/dist/src/project-runtime.js +1 -1
- package/dist/src/providers.js +1 -1
- package/dist/src/react-native.js +1 -1
- package/dist/src/record-trace-recording.js +2 -2
- package/dist/src/record-trace.js +1 -1
- package/dist/src/registry.js +24 -23
- package/dist/src/reporting.js +3 -3
- package/dist/src/runner-client.js +1 -1
- package/dist/src/runtime.js +2 -2
- package/dist/src/sdk-android-adb.d.ts +2 -2
- package/dist/src/sdk-batch-runner.d.ts +0 -1
- package/dist/src/sdk-batch.js +2 -2
- package/dist/src/sdk-finders.d.ts +1 -2
- package/dist/src/sdk-finders.js +1 -1
- package/dist/src/sdk-io.d.ts +0 -1
- package/dist/src/sdk-remote-config.d.ts +1 -2
- package/dist/src/sdk-remote-config.js +1 -1
- package/dist/src/sdk-selectors.js +1 -1
- package/dist/src/selector-runtime.js +1 -1
- package/dist/src/selectors.d.ts +0 -1
- package/dist/src/selectors.js +1 -1
- package/dist/src/selectors2.js +1 -1
- package/dist/src/server.js +3 -3
- package/dist/src/session.js +10 -10
- package/dist/src/simctl.js +1 -1
- package/dist/src/simulator.js +1 -1
- package/dist/src/snapshot-diagnostics.js +2 -2
- package/dist/src/snapshot-helper.js +4 -4
- package/dist/src/snapshot-quality.js +1 -1
- package/dist/src/snapshot2.js +2 -2
- package/dist/src/tv-remote.js +14 -0
- package/dist/src/web.js +1 -1
- package/package.json +5 -4
- package/server.json +2 -2
- package/skills/agent-device/SKILL.md +8 -7
- package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.19.0.apk.sha256 +0 -1
- package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.19.0.apk.sha256 +0 -1
- package/dist/src/device-rotation.js +0 -14
- package/dist/src/logcat.js +0 -6
|
@@ -188,6 +188,15 @@ extension RunnerTests {
|
|
|
188
188
|
var abandoned = false
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
+
struct ActiveCommandContext {
|
|
192
|
+
let app: XCUIApplication
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
enum ActiveCommandPreparation {
|
|
196
|
+
case response(Response)
|
|
197
|
+
case context(ActiveCommandContext)
|
|
198
|
+
}
|
|
199
|
+
|
|
191
200
|
enum MainThreadBusyState {
|
|
192
201
|
case idle
|
|
193
202
|
case busy(abandonedForSeconds: TimeInterval)
|
|
@@ -259,12 +268,35 @@ extension RunnerTests {
|
|
|
259
268
|
if Thread.isMainThread {
|
|
260
269
|
return try executeOnMainSafely(command: command)
|
|
261
270
|
}
|
|
262
|
-
|
|
271
|
+
if command.command == .snapshot {
|
|
272
|
+
return try executeSnapshotDispatched(command: command)
|
|
273
|
+
}
|
|
274
|
+
return try runMainThreadWork(
|
|
275
|
+
command: command,
|
|
276
|
+
timeout: mainThreadExecutionTimeout,
|
|
277
|
+
timeoutError: mainThreadExecutionTimeoutError
|
|
278
|
+
) {
|
|
279
|
+
try self.executeOnMainSafely(command: command)
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
func runMainThreadWork<T>(
|
|
284
|
+
command: Command?,
|
|
285
|
+
timeout: TimeInterval,
|
|
286
|
+
timeoutError: @escaping () -> Error,
|
|
287
|
+
onAbandoned: (() -> Void)? = nil,
|
|
288
|
+
onDrained: (() -> Void)? = nil,
|
|
289
|
+
_ work: @escaping () throws -> T
|
|
290
|
+
) throws -> T {
|
|
291
|
+
if Thread.isMainThread {
|
|
292
|
+
return try work()
|
|
293
|
+
}
|
|
294
|
+
var result: Result<T, Error>?
|
|
263
295
|
let semaphore = DispatchSemaphore(value: 0)
|
|
264
296
|
let workState = MainThreadWorkState()
|
|
265
297
|
DispatchQueue.main.async {
|
|
266
298
|
do {
|
|
267
|
-
result = .success(try
|
|
299
|
+
result = .success(try work())
|
|
268
300
|
} catch {
|
|
269
301
|
result = .failure(error)
|
|
270
302
|
}
|
|
@@ -275,15 +307,16 @@ extension RunnerTests {
|
|
|
275
307
|
self.abandonedMainThreadWorkSince = nil
|
|
276
308
|
NSLog("AGENT_DEVICE_RUNNER_ABANDONED_WORK_DRAINED")
|
|
277
309
|
}
|
|
310
|
+
self.mainThreadWorkLock.unlock()
|
|
311
|
+
onDrained?()
|
|
278
312
|
} else {
|
|
279
313
|
workState.finished = true
|
|
314
|
+
self.mainThreadWorkLock.unlock()
|
|
280
315
|
}
|
|
281
|
-
self.mainThreadWorkLock.unlock()
|
|
282
316
|
semaphore.signal()
|
|
283
317
|
}
|
|
284
|
-
let waitResult = semaphore.wait(timeout: .now() +
|
|
318
|
+
let waitResult = semaphore.wait(timeout: .now() + timeout)
|
|
285
319
|
if waitResult == .timedOut {
|
|
286
|
-
// The main queue work may still be running; we stop waiting and report timeout.
|
|
287
320
|
mainThreadWorkLock.lock()
|
|
288
321
|
let stillRunning = !workState.finished
|
|
289
322
|
if stillRunning {
|
|
@@ -292,24 +325,14 @@ extension RunnerTests {
|
|
|
292
325
|
if abandonedMainThreadWorkSince == nil {
|
|
293
326
|
abandonedMainThreadWorkSince = Date()
|
|
294
327
|
}
|
|
328
|
+
onAbandoned?()
|
|
295
329
|
}
|
|
296
330
|
mainThreadWorkLock.unlock()
|
|
297
|
-
|
|
298
|
-
// The next capture on this screen must not re-grind the tree backend.
|
|
299
|
-
penalizeSnapshotTreeBackend(
|
|
300
|
-
bundleId: command.appBundleId,
|
|
301
|
-
reason: "main_thread_watchdog"
|
|
302
|
-
)
|
|
303
|
-
}
|
|
304
|
-
throw NSError(
|
|
305
|
-
domain: RunnerErrorDomain.general,
|
|
306
|
-
code: RunnerErrorCode.mainThreadExecutionTimedOut,
|
|
307
|
-
userInfo: [NSLocalizedDescriptionKey: "main thread execution timed out"]
|
|
308
|
-
)
|
|
331
|
+
throw timeoutError()
|
|
309
332
|
}
|
|
310
333
|
switch result {
|
|
311
|
-
case .success(let
|
|
312
|
-
return
|
|
334
|
+
case .success(let value):
|
|
335
|
+
return value
|
|
313
336
|
case .failure(let error):
|
|
314
337
|
throw error
|
|
315
338
|
case .none:
|
|
@@ -321,6 +344,14 @@ extension RunnerTests {
|
|
|
321
344
|
}
|
|
322
345
|
}
|
|
323
346
|
|
|
347
|
+
private func mainThreadExecutionTimeoutError() -> Error {
|
|
348
|
+
NSError(
|
|
349
|
+
domain: RunnerErrorDomain.general,
|
|
350
|
+
code: RunnerErrorCode.mainThreadExecutionTimedOut,
|
|
351
|
+
userInfo: [NSLocalizedDescriptionKey: "main thread execution timed out"]
|
|
352
|
+
)
|
|
353
|
+
}
|
|
354
|
+
|
|
324
355
|
// MARK: - Command Handling
|
|
325
356
|
|
|
326
357
|
private func executeOnMainSafely(command: Command) throws -> Response {
|
|
@@ -384,68 +415,171 @@ extension RunnerTests {
|
|
|
384
415
|
}
|
|
385
416
|
}
|
|
386
417
|
|
|
387
|
-
private func
|
|
388
|
-
var
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
418
|
+
private func executeSnapshotDispatched(command: Command) throws -> Response {
|
|
419
|
+
var hasRetried = false
|
|
420
|
+
while true {
|
|
421
|
+
let failureCountBefore = try runMainThreadWork(
|
|
422
|
+
command: command,
|
|
423
|
+
timeout: mainThreadExecutionTimeout,
|
|
424
|
+
timeoutError: mainThreadExecutionTimeoutError
|
|
425
|
+
) {
|
|
426
|
+
self.currentXCTestFailureCount()
|
|
427
|
+
}
|
|
428
|
+
let response = try executeSnapshotDispatchedOnce(command: command)
|
|
429
|
+
let recordedFailureResponse = try runMainThreadWork(
|
|
430
|
+
command: command,
|
|
431
|
+
timeout: mainThreadExecutionTimeout,
|
|
432
|
+
timeoutError: mainThreadExecutionTimeoutError
|
|
433
|
+
) {
|
|
434
|
+
self.didRecordXCTestFailure(since: failureCountBefore)
|
|
435
|
+
? self.xctestRecordedFailureResponse(command: command, response: response)
|
|
436
|
+
: nil
|
|
437
|
+
}
|
|
438
|
+
if let recordedFailureResponse {
|
|
439
|
+
try runMainThreadWork(
|
|
440
|
+
command: command,
|
|
441
|
+
timeout: mainThreadExecutionTimeout,
|
|
442
|
+
timeoutError: mainThreadExecutionTimeoutError
|
|
443
|
+
) {
|
|
444
|
+
self.invalidateCachedTarget(reason: "xctest_recorded_failure")
|
|
398
445
|
}
|
|
399
|
-
|
|
400
|
-
// Do not reuse stale bundle targets when the caller does not explicitly request one.
|
|
401
|
-
currentApp = nil
|
|
402
|
-
currentBundleId = nil
|
|
446
|
+
return recordedFailureResponse
|
|
403
447
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
448
|
+
if !hasRetried, shouldRetryCommand(command), shouldRetryResponse(response) {
|
|
449
|
+
NSLog(
|
|
450
|
+
"AGENT_DEVICE_RUNNER_RETRY command=%@ reason=response_unavailable",
|
|
451
|
+
command.command.rawValue
|
|
452
|
+
)
|
|
453
|
+
hasRetried = true
|
|
454
|
+
try runMainThreadWork(
|
|
455
|
+
command: command,
|
|
456
|
+
timeout: mainThreadExecutionTimeout,
|
|
457
|
+
timeoutError: mainThreadExecutionTimeoutError
|
|
458
|
+
) {
|
|
459
|
+
self.invalidateCachedTarget(reason: "response_unavailable")
|
|
460
|
+
self.sleepFor(self.retryCooldown)
|
|
461
|
+
}
|
|
462
|
+
continue
|
|
411
463
|
}
|
|
464
|
+
return response
|
|
465
|
+
}
|
|
466
|
+
}
|
|
412
467
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
468
|
+
private func executeSnapshotDispatchedOnce(command: Command) throws -> Response {
|
|
469
|
+
let preparation = try runMainThreadWork(
|
|
470
|
+
command: command,
|
|
471
|
+
timeout: mainThreadExecutionTimeout,
|
|
472
|
+
timeoutError: mainThreadExecutionTimeoutError
|
|
473
|
+
) {
|
|
474
|
+
try self.prepareActiveCommandContextSafely(command: command)
|
|
475
|
+
}
|
|
476
|
+
switch preparation {
|
|
477
|
+
case .response(let response):
|
|
478
|
+
return response
|
|
479
|
+
case .context(let context):
|
|
480
|
+
return try executeSnapshotPrepared(command: command, activeApp: context.app)
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
private func executeSnapshotPrepared(command: Command, activeApp: XCUIApplication) throws -> Response {
|
|
485
|
+
let options = SnapshotOptions(
|
|
486
|
+
interactiveOnly: command.interactiveOnly ?? false,
|
|
487
|
+
depth: command.depth,
|
|
488
|
+
scope: command.scope,
|
|
489
|
+
raw: command.raw ?? false
|
|
490
|
+
)
|
|
491
|
+
do {
|
|
492
|
+
let payload: DataPayload
|
|
493
|
+
if options.raw {
|
|
494
|
+
payload = try snapshotRaw(app: activeApp, options: options)
|
|
495
|
+
} else {
|
|
496
|
+
payload = try snapshotFast(app: activeApp, options: options)
|
|
497
|
+
}
|
|
498
|
+
setNeedsPostSnapshotInteractionDelay()
|
|
499
|
+
return Response(ok: true, data: payload)
|
|
500
|
+
} catch let failure as SnapshotCaptureFailure {
|
|
501
|
+
invalidateCachedTargetAfterSnapshotFailure()
|
|
502
|
+
return Response(
|
|
503
|
+
ok: false,
|
|
504
|
+
error: ErrorPayload(
|
|
505
|
+
code: failure.code,
|
|
506
|
+
message: failure.message,
|
|
507
|
+
hint: failure.hint
|
|
508
|
+
)
|
|
417
509
|
)
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
private func setNeedsPostSnapshotInteractionDelay() {
|
|
514
|
+
if Thread.isMainThread {
|
|
515
|
+
needsPostSnapshotInteractionDelay = true
|
|
516
|
+
return
|
|
517
|
+
}
|
|
518
|
+
guard !hasAbandonedTreeCapture() else {
|
|
519
|
+
NSLog("AGENT_DEVICE_RUNNER_POST_SNAPSHOT_DELAY_MARK_SKIPPED_XCTEST_OCCUPIED")
|
|
520
|
+
return
|
|
521
|
+
}
|
|
522
|
+
do {
|
|
523
|
+
try runMainThreadWork(
|
|
524
|
+
command: nil,
|
|
525
|
+
timeout: 1,
|
|
526
|
+
timeoutError: mainThreadExecutionTimeoutError
|
|
527
|
+
) {
|
|
528
|
+
self.needsPostSnapshotInteractionDelay = true
|
|
427
529
|
}
|
|
530
|
+
} catch {
|
|
531
|
+
NSLog("AGENT_DEVICE_RUNNER_POST_SNAPSHOT_DELAY_MARK_FAILED=%@", String(describing: error))
|
|
532
|
+
}
|
|
533
|
+
}
|
|
428
534
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
)
|
|
441
|
-
if !skipInteractionExistenceWait && !activeApp.waitForExistence(timeout: 2) {
|
|
442
|
-
if let bundleId = requestedBundleId {
|
|
443
|
-
return Response(ok: false, error: ErrorPayload(message: "app '\(bundleId)' is not available"))
|
|
444
|
-
}
|
|
445
|
-
return Response(ok: false, error: ErrorPayload(message: "runner app is not available"))
|
|
446
|
-
}
|
|
447
|
-
applyInteractionStabilizationIfNeeded()
|
|
535
|
+
private func invalidateCachedTargetAfterSnapshotFailure() {
|
|
536
|
+
if Thread.isMainThread {
|
|
537
|
+
invalidateCachedTarget(reason: "ax_snapshot_failure")
|
|
538
|
+
return
|
|
539
|
+
}
|
|
540
|
+
do {
|
|
541
|
+
try runMainThreadWork(
|
|
542
|
+
command: nil,
|
|
543
|
+
timeout: 1,
|
|
544
|
+
timeoutError: mainThreadExecutionTimeoutError
|
|
545
|
+
) {
|
|
546
|
+
self.invalidateCachedTarget(reason: "ax_snapshot_failure")
|
|
448
547
|
}
|
|
548
|
+
} catch {
|
|
549
|
+
NSLog("AGENT_DEVICE_RUNNER_SNAPSHOT_INVALIDATION_FAILED=%@", String(describing: error))
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
private func prepareActiveCommandContextSafely(command: Command) throws -> ActiveCommandPreparation {
|
|
554
|
+
var preparation: ActiveCommandPreparation?
|
|
555
|
+
let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
|
|
556
|
+
preparation = self.prepareActiveCommandContext(command: command)
|
|
557
|
+
})
|
|
558
|
+
if let exceptionMessage {
|
|
559
|
+
throw NSError(
|
|
560
|
+
domain: RunnerErrorDomain.exception,
|
|
561
|
+
code: RunnerErrorCode.objcException,
|
|
562
|
+
userInfo: [NSLocalizedDescriptionKey: exceptionMessage]
|
|
563
|
+
)
|
|
564
|
+
}
|
|
565
|
+
guard let preparation else {
|
|
566
|
+
throw NSError(
|
|
567
|
+
domain: RunnerErrorDomain.general,
|
|
568
|
+
code: RunnerErrorCode.commandReturnedNoResponse,
|
|
569
|
+
userInfo: [NSLocalizedDescriptionKey: "snapshot preflight returned no response"]
|
|
570
|
+
)
|
|
571
|
+
}
|
|
572
|
+
return preparation
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
private func executeOnMain(command: Command) throws -> Response {
|
|
576
|
+
let preparation = prepareActiveCommandContext(command: command)
|
|
577
|
+
let activeApp: XCUIApplication
|
|
578
|
+
switch preparation {
|
|
579
|
+
case .response(let response):
|
|
580
|
+
return response
|
|
581
|
+
case .context(let context):
|
|
582
|
+
activeApp = context.app
|
|
449
583
|
}
|
|
450
584
|
|
|
451
585
|
switch command.command {
|
|
@@ -515,6 +649,89 @@ extension RunnerTests {
|
|
|
515
649
|
}
|
|
516
650
|
case .uptime:
|
|
517
651
|
return executeUptime()
|
|
652
|
+
default:
|
|
653
|
+
break
|
|
654
|
+
}
|
|
655
|
+
return try executeOnMainPrepared(command: command, activeApp: activeApp)
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
private func prepareActiveCommandContext(command: Command) -> ActiveCommandPreparation {
|
|
659
|
+
var activeApp = currentApp ?? app
|
|
660
|
+
if shouldSkipAppActivationPreflight(command) {
|
|
661
|
+
activeApp = resolveAppWithoutActivation(command: command)
|
|
662
|
+
} else if !isRunnerLifecycleCommand(command.command) {
|
|
663
|
+
let normalizedBundleId = command.appBundleId?
|
|
664
|
+
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
665
|
+
let requestedBundleId = (normalizedBundleId?.isEmpty == true) ? nil : normalizedBundleId
|
|
666
|
+
if let bundleId = requestedBundleId {
|
|
667
|
+
if currentBundleId != bundleId || currentApp == nil {
|
|
668
|
+
_ = activateTarget(bundleId: bundleId, reason: "bundle_changed")
|
|
669
|
+
}
|
|
670
|
+
} else {
|
|
671
|
+
// Do not reuse stale bundle targets when the caller does not explicitly request one.
|
|
672
|
+
currentApp = nil
|
|
673
|
+
currentBundleId = nil
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
activeApp = currentApp ?? app
|
|
677
|
+
if let bundleId = requestedBundleId, targetNeedsActivation(activeApp) {
|
|
678
|
+
activeApp = activateTarget(bundleId: bundleId, reason: "stale_target")
|
|
679
|
+
} else if requestedBundleId == nil, targetNeedsActivation(activeApp) {
|
|
680
|
+
ensureRunnerHostAppActive(reason: "missing_app_bundle")
|
|
681
|
+
activeApp = app
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
let skipExistenceWait = canUseFastForegroundAppGuard(
|
|
685
|
+
activeApp: activeApp,
|
|
686
|
+
requestedBundleId: requestedBundleId,
|
|
687
|
+
command: command.command
|
|
688
|
+
)
|
|
689
|
+
if !skipExistenceWait && !activeApp.waitForExistence(timeout: appExistenceTimeout) {
|
|
690
|
+
if let bundleId = requestedBundleId {
|
|
691
|
+
activeApp = activateTarget(bundleId: bundleId, reason: "missing_after_wait")
|
|
692
|
+
guard activeApp.waitForExistence(timeout: appExistenceTimeout) else {
|
|
693
|
+
return .response(Response(ok: false, error: ErrorPayload(message: "app '\(bundleId)' is not available")))
|
|
694
|
+
}
|
|
695
|
+
} else {
|
|
696
|
+
return .response(Response(ok: false, error: ErrorPayload(message: "runner app is not available")))
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
if isInteractionCommand(command.command) {
|
|
701
|
+
if let bundleId = requestedBundleId, activeApp.state != .runningForeground {
|
|
702
|
+
activeApp = activateTarget(bundleId: bundleId, reason: "interaction_foreground_guard")
|
|
703
|
+
} else if requestedBundleId == nil, activeApp.state != .runningForeground {
|
|
704
|
+
ensureRunnerHostAppActive(reason: "interaction_missing_app_bundle")
|
|
705
|
+
activeApp = app
|
|
706
|
+
}
|
|
707
|
+
let skipInteractionExistenceWait = canUseFastForegroundAppGuard(
|
|
708
|
+
activeApp: activeApp,
|
|
709
|
+
requestedBundleId: requestedBundleId,
|
|
710
|
+
command: command.command
|
|
711
|
+
)
|
|
712
|
+
if !skipInteractionExistenceWait && !activeApp.waitForExistence(timeout: 2) {
|
|
713
|
+
if let bundleId = requestedBundleId {
|
|
714
|
+
return .response(Response(ok: false, error: ErrorPayload(message: "app '\(bundleId)' is not available")))
|
|
715
|
+
}
|
|
716
|
+
return .response(Response(ok: false, error: ErrorPayload(message: "runner app is not available")))
|
|
717
|
+
}
|
|
718
|
+
applyInteractionStabilizationIfNeeded()
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
return .context(ActiveCommandContext(app: activeApp))
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
private func executeOnMainPrepared(command: Command, activeApp: XCUIApplication) throws -> Response {
|
|
725
|
+
var activeApp = activeApp
|
|
726
|
+
switch command.command {
|
|
727
|
+
case .status, .shutdown, .recordStart, .recordStop, .uptime:
|
|
728
|
+
return Response(
|
|
729
|
+
ok: false,
|
|
730
|
+
error: ErrorPayload(
|
|
731
|
+
code: "UNSUPPORTED_OPERATION",
|
|
732
|
+
message: "\(command.command.rawValue) cannot be executed through the prepared command path"
|
|
733
|
+
)
|
|
734
|
+
)
|
|
518
735
|
case .tap:
|
|
519
736
|
if let selectorKey = command.selectorKey, let selectorValue = command.selectorValue {
|
|
520
737
|
let match = findElement(
|
|
@@ -576,12 +793,16 @@ extension RunnerTests {
|
|
|
576
793
|
if let x = command.x, let y = command.y {
|
|
577
794
|
var fallback: GestureFallback?
|
|
578
795
|
if command.synthesized == true {
|
|
796
|
+
let policyKind = SynthesizedGesturePolicyKind.coordinateTap
|
|
797
|
+
let context = synthesizedCoordinateContext(policy: synthesizedGesturePolicy(policyKind))
|
|
579
798
|
let (timing, outcome) = performGesture(activeApp, idleTimeout: false) {
|
|
580
|
-
synthesizedTapAt(app: activeApp, x: x, y: y)
|
|
799
|
+
synthesizedTapAt(app: activeApp, x: x, y: y, context: context)
|
|
581
800
|
}
|
|
582
801
|
if case .performed = outcome {
|
|
802
|
+
logSynthesizedGesturePolicyDecision(kind: policyKind, context: context, fallbackAttempted: false)
|
|
583
803
|
return gestureResponse(message: "tapped", timing: timing)
|
|
584
804
|
}
|
|
805
|
+
logSynthesizedGesturePolicyDecision(kind: policyKind, context: context, fallbackAttempted: true)
|
|
585
806
|
fallback = gestureFallback(strategy: "xctest-coordinate-tap", from: outcome)
|
|
586
807
|
}
|
|
587
808
|
let touchFrame = resolvedTouchVisualizationFrame(app: activeApp, x: x, y: y)
|
|
@@ -645,12 +866,13 @@ extension RunnerTests {
|
|
|
645
866
|
y2: y2,
|
|
646
867
|
durationMs: command.durationMs,
|
|
647
868
|
synthesized: command.synthesized == true,
|
|
648
|
-
message: "dragged"
|
|
869
|
+
message: "dragged",
|
|
870
|
+
synthesizedPolicyKind: .synthesizedDrag
|
|
649
871
|
)
|
|
650
872
|
case .scroll:
|
|
651
|
-
// Fused frame-resolve + drag scroll for non-tvOS.
|
|
652
|
-
//
|
|
653
|
-
//
|
|
873
|
+
// Fused frame-resolve + drag scroll for non-tvOS. On iOS this intentionally stays on the
|
|
874
|
+
// AX-free synthesized coordinate lane so scroll keeps working when XCTest cannot serialize
|
|
875
|
+
// the accessibility tree.
|
|
654
876
|
guard let direction = command.direction,
|
|
655
877
|
direction == "up" || direction == "down" || direction == "left" || direction == "right"
|
|
656
878
|
else {
|
|
@@ -662,7 +884,14 @@ extension RunnerTests {
|
|
|
662
884
|
)
|
|
663
885
|
)
|
|
664
886
|
}
|
|
665
|
-
let
|
|
887
|
+
let scrollPolicyKind = SynthesizedGesturePolicyKind.scroll
|
|
888
|
+
guard let scrollContext = synthesizedCoordinateContext(policy: synthesizedGesturePolicy(scrollPolicyKind)) else {
|
|
889
|
+
return Response(
|
|
890
|
+
ok: false,
|
|
891
|
+
error: ErrorPayload(message: "scroll could not resolve a usable interaction frame")
|
|
892
|
+
)
|
|
893
|
+
}
|
|
894
|
+
let frame = scrollReferenceFrame(app: activeApp, context: scrollContext)
|
|
666
895
|
guard frame.width > 0, frame.height > 0 else {
|
|
667
896
|
return Response(
|
|
668
897
|
ok: false,
|
|
@@ -702,8 +931,10 @@ extension RunnerTests {
|
|
|
702
931
|
x2: frame.minX + plan.x2,
|
|
703
932
|
y2: frame.minY + plan.y2,
|
|
704
933
|
durationMs: command.durationMs,
|
|
705
|
-
synthesized:
|
|
706
|
-
message: "scrolled"
|
|
934
|
+
synthesized: shouldUseSynthesizedScrollPath(),
|
|
935
|
+
message: "scrolled",
|
|
936
|
+
synthesizedContext: scrollContext.withReferenceFrame(frame),
|
|
937
|
+
synthesizedPolicyKind: scrollPolicyKind
|
|
707
938
|
)
|
|
708
939
|
case .desktopScroll:
|
|
709
940
|
guard let direction = command.direction,
|
|
@@ -837,33 +1068,7 @@ extension RunnerTests {
|
|
|
837
1068
|
}
|
|
838
1069
|
return Response(ok: true, data: DataPayload(text: text))
|
|
839
1070
|
case .snapshot:
|
|
840
|
-
|
|
841
|
-
interactiveOnly: command.interactiveOnly ?? false,
|
|
842
|
-
depth: command.depth,
|
|
843
|
-
scope: command.scope,
|
|
844
|
-
raw: command.raw ?? false
|
|
845
|
-
)
|
|
846
|
-
do {
|
|
847
|
-
let payload: DataPayload
|
|
848
|
-
if options.raw {
|
|
849
|
-
payload = try snapshotRaw(app: activeApp, options: options)
|
|
850
|
-
} else {
|
|
851
|
-
payload = try snapshotFast(app: activeApp, options: options)
|
|
852
|
-
}
|
|
853
|
-
needsPostSnapshotInteractionDelay = true
|
|
854
|
-
return Response(ok: true, data: payload)
|
|
855
|
-
} catch let failure as SnapshotCaptureFailure {
|
|
856
|
-
invalidateCachedTarget(reason: "ax_snapshot_failure")
|
|
857
|
-
// Other thrown errors fall through to executeOnMainSafely's generic error response.
|
|
858
|
-
return Response(
|
|
859
|
-
ok: false,
|
|
860
|
-
error: ErrorPayload(
|
|
861
|
-
code: failure.code,
|
|
862
|
-
message: failure.message,
|
|
863
|
-
hint: failure.hint
|
|
864
|
-
)
|
|
865
|
-
)
|
|
866
|
-
}
|
|
1071
|
+
return try executeSnapshotPrepared(command: command, activeApp: activeApp)
|
|
867
1072
|
case .screenshot:
|
|
868
1073
|
let screenshot: XCUIScreenshot
|
|
869
1074
|
#if os(macOS)
|
|
@@ -1055,10 +1260,9 @@ extension RunnerTests {
|
|
|
1055
1260
|
}
|
|
1056
1261
|
}
|
|
1057
1262
|
|
|
1058
|
-
/// Shared drag execution for `.drag` and the fused `.scroll`.
|
|
1059
|
-
///
|
|
1060
|
-
///
|
|
1061
|
-
/// gestureResponse(.drag). `.scroll` uses the synthesized path only when a duration is requested.
|
|
1263
|
+
/// Shared drag execution for `.drag` and the fused `.scroll`. The iOS synthesized lane keeps
|
|
1264
|
+
/// each command's fallback policy explicit: scroll requires private synthesis, while explicit
|
|
1265
|
+
/// synthesized drag can still use the coordinate fallback unless AX is known unavailable.
|
|
1062
1266
|
private func executeDragGesture(
|
|
1063
1267
|
activeApp: XCUIApplication,
|
|
1064
1268
|
x: Double,
|
|
@@ -1067,8 +1271,30 @@ extension RunnerTests {
|
|
|
1067
1271
|
y2: Double,
|
|
1068
1272
|
durationMs: Double?,
|
|
1069
1273
|
synthesized: Bool,
|
|
1070
|
-
message: String
|
|
1274
|
+
message: String,
|
|
1275
|
+
synthesizedContext: SynthesizedCoordinateContext? = nil,
|
|
1276
|
+
synthesizedPolicyKind: SynthesizedGesturePolicyKind
|
|
1071
1277
|
) -> Response {
|
|
1278
|
+
let commandName = dragCommandName(message: message)
|
|
1279
|
+
guard x.isFinite, y.isFinite, x2.isFinite, y2.isFinite else {
|
|
1280
|
+
return Response(
|
|
1281
|
+
ok: false,
|
|
1282
|
+
error: ErrorPayload(code: "INVALID_ARGS", message: "\(commandName) requires finite coordinates")
|
|
1283
|
+
)
|
|
1284
|
+
}
|
|
1285
|
+
if synthesized, let synthesizedResponse = executeSynthesizedDragGesture(
|
|
1286
|
+
activeApp: activeApp,
|
|
1287
|
+
x: x,
|
|
1288
|
+
y: y,
|
|
1289
|
+
x2: x2,
|
|
1290
|
+
y2: y2,
|
|
1291
|
+
durationMs: durationMs,
|
|
1292
|
+
message: message,
|
|
1293
|
+
context: synthesizedContext,
|
|
1294
|
+
policyKind: synthesizedPolicyKind
|
|
1295
|
+
) {
|
|
1296
|
+
return synthesizedResponse
|
|
1297
|
+
}
|
|
1072
1298
|
let dragPoints = keyboardAvoidingDragPoints(app: activeApp, x: x, y: y, x2: x2, y2: y2)
|
|
1073
1299
|
let dragFrame = resolvedDragVisualizationFrame(
|
|
1074
1300
|
app: activeApp,
|
|
@@ -1080,6 +1306,7 @@ extension RunnerTests {
|
|
|
1080
1306
|
var fallback: GestureFallback?
|
|
1081
1307
|
if synthesized {
|
|
1082
1308
|
let durationMs = min(max(durationMs ?? 250, 16), 10000)
|
|
1309
|
+
let context = synthesizedCoordinateContext(policy: synthesizedGesturePolicy(synthesizedPolicyKind))
|
|
1083
1310
|
let (timing, outcome) = performGesture(activeApp, idleTimeout: false) {
|
|
1084
1311
|
synthesizedDragAt(
|
|
1085
1312
|
app: activeApp,
|
|
@@ -1087,7 +1314,8 @@ extension RunnerTests {
|
|
|
1087
1314
|
y: dragPoints.y,
|
|
1088
1315
|
x2: dragPoints.x2,
|
|
1089
1316
|
y2: dragPoints.y2,
|
|
1090
|
-
durationMs: durationMs
|
|
1317
|
+
durationMs: durationMs,
|
|
1318
|
+
context: context
|
|
1091
1319
|
)
|
|
1092
1320
|
}
|
|
1093
1321
|
if case .performed = outcome {
|
|
@@ -1119,6 +1347,154 @@ extension RunnerTests {
|
|
|
1119
1347
|
)
|
|
1120
1348
|
}
|
|
1121
1349
|
|
|
1350
|
+
private func executeSynthesizedDragGesture(
|
|
1351
|
+
activeApp: XCUIApplication,
|
|
1352
|
+
x: Double,
|
|
1353
|
+
y: Double,
|
|
1354
|
+
x2: Double,
|
|
1355
|
+
y2: Double,
|
|
1356
|
+
durationMs: Double?,
|
|
1357
|
+
message: String,
|
|
1358
|
+
context: SynthesizedCoordinateContext?,
|
|
1359
|
+
policyKind: SynthesizedGesturePolicyKind
|
|
1360
|
+
) -> Response? {
|
|
1361
|
+
#if os(iOS)
|
|
1362
|
+
let policy = synthesizedGesturePolicy(policyKind)
|
|
1363
|
+
let context = context ?? synthesizedCoordinateContext(policy: policy)
|
|
1364
|
+
guard let plan = axFreeSynthesizedDragPlan(
|
|
1365
|
+
app: activeApp,
|
|
1366
|
+
x: x,
|
|
1367
|
+
y: y,
|
|
1368
|
+
x2: x2,
|
|
1369
|
+
y2: y2,
|
|
1370
|
+
context: context
|
|
1371
|
+
)
|
|
1372
|
+
else {
|
|
1373
|
+
if context?.allowsXCTestCoordinateFallback == true {
|
|
1374
|
+
logSynthesizedGesturePolicyDecision(kind: policyKind, context: context, fallbackAttempted: true)
|
|
1375
|
+
return executeCoordinateDragFallback(
|
|
1376
|
+
activeApp: activeApp,
|
|
1377
|
+
x: x,
|
|
1378
|
+
y: y,
|
|
1379
|
+
x2: x2,
|
|
1380
|
+
y2: y2,
|
|
1381
|
+
durationMs: durationMs,
|
|
1382
|
+
message: message,
|
|
1383
|
+
fallback: nil
|
|
1384
|
+
)
|
|
1385
|
+
}
|
|
1386
|
+
logSynthesizedGesturePolicyDecision(kind: policyKind, context: context, fallbackAttempted: false)
|
|
1387
|
+
return Response(
|
|
1388
|
+
ok: false,
|
|
1389
|
+
error: ErrorPayload(
|
|
1390
|
+
code: "INVALID_ARGS",
|
|
1391
|
+
message: "\(dragCommandName(message: message)) could not resolve a finite synthesized coordinate frame"
|
|
1392
|
+
)
|
|
1393
|
+
)
|
|
1394
|
+
}
|
|
1395
|
+
let durationMs = min(max(durationMs ?? 250, 16), 10000)
|
|
1396
|
+
let dragFrame = axFreeDragVisualizationFrame(
|
|
1397
|
+
x: plan.points.x,
|
|
1398
|
+
y: plan.points.y,
|
|
1399
|
+
x2: plan.points.x2,
|
|
1400
|
+
y2: plan.points.y2,
|
|
1401
|
+
referenceFrame: plan.referenceFrame
|
|
1402
|
+
)
|
|
1403
|
+
let (timing, outcome) = performGesture(activeApp, idleTimeout: false) {
|
|
1404
|
+
synthesizedDragAt(
|
|
1405
|
+
app: activeApp,
|
|
1406
|
+
x: plan.points.x,
|
|
1407
|
+
y: plan.points.y,
|
|
1408
|
+
x2: plan.points.x2,
|
|
1409
|
+
y2: plan.points.y2,
|
|
1410
|
+
durationMs: durationMs,
|
|
1411
|
+
context: plan.context
|
|
1412
|
+
)
|
|
1413
|
+
}
|
|
1414
|
+
if case .performed = outcome {
|
|
1415
|
+
logSynthesizedGesturePolicyDecision(kind: policyKind, context: plan.context, fallbackAttempted: false)
|
|
1416
|
+
return gestureResponse(message: message, timing: timing, frame: .drag(dragFrame))
|
|
1417
|
+
}
|
|
1418
|
+
if plan.context.allowsXCTestCoordinateFallback {
|
|
1419
|
+
logSynthesizedGesturePolicyDecision(kind: policyKind, context: plan.context, fallbackAttempted: true)
|
|
1420
|
+
return executeCoordinateDragFallback(
|
|
1421
|
+
activeApp: activeApp,
|
|
1422
|
+
x: plan.points.x,
|
|
1423
|
+
y: plan.points.y,
|
|
1424
|
+
x2: plan.points.x2,
|
|
1425
|
+
y2: plan.points.y2,
|
|
1426
|
+
durationMs: durationMs,
|
|
1427
|
+
message: message,
|
|
1428
|
+
fallback: gestureFallback(strategy: "xctest-coordinate-drag", from: outcome)
|
|
1429
|
+
)
|
|
1430
|
+
}
|
|
1431
|
+
logSynthesizedGesturePolicyDecision(kind: policyKind, context: plan.context, fallbackAttempted: false)
|
|
1432
|
+
return unsupportedResponse(for: outcome)
|
|
1433
|
+
#else
|
|
1434
|
+
return nil
|
|
1435
|
+
#endif
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
private func executeCoordinateDragFallback(
|
|
1439
|
+
activeApp: XCUIApplication,
|
|
1440
|
+
x: Double,
|
|
1441
|
+
y: Double,
|
|
1442
|
+
x2: Double,
|
|
1443
|
+
y2: Double,
|
|
1444
|
+
durationMs: Double?,
|
|
1445
|
+
message: String,
|
|
1446
|
+
fallback: GestureFallback?
|
|
1447
|
+
) -> Response {
|
|
1448
|
+
let dragPoints = keyboardAvoidingDragPoints(app: activeApp, x: x, y: y, x2: x2, y2: y2)
|
|
1449
|
+
let dragFrame = resolvedDragVisualizationFrame(
|
|
1450
|
+
app: activeApp,
|
|
1451
|
+
x: dragPoints.x,
|
|
1452
|
+
y: dragPoints.y,
|
|
1453
|
+
x2: dragPoints.x2,
|
|
1454
|
+
y2: dragPoints.y2
|
|
1455
|
+
)
|
|
1456
|
+
let holdDuration = synthesizedSwipeFallbackHoldDuration(durationMs: durationMs ?? 250)
|
|
1457
|
+
let (timing, outcome) = performGesture(activeApp) {
|
|
1458
|
+
dragAt(
|
|
1459
|
+
app: activeApp,
|
|
1460
|
+
x: dragPoints.x,
|
|
1461
|
+
y: dragPoints.y,
|
|
1462
|
+
x2: dragPoints.x2,
|
|
1463
|
+
y2: dragPoints.y2,
|
|
1464
|
+
holdDuration: holdDuration
|
|
1465
|
+
)
|
|
1466
|
+
}
|
|
1467
|
+
if let response = unsupportedResponse(for: outcome) {
|
|
1468
|
+
return response
|
|
1469
|
+
}
|
|
1470
|
+
return gestureResponse(
|
|
1471
|
+
message: message,
|
|
1472
|
+
timing: timing,
|
|
1473
|
+
frame: .drag(dragFrame),
|
|
1474
|
+
fallback: fallback
|
|
1475
|
+
)
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
private func scrollReferenceFrame(app: XCUIApplication, context: SynthesizedCoordinateContext) -> CGRect {
|
|
1479
|
+
#if os(iOS)
|
|
1480
|
+
return synthesizedFrameAvoidingKeyboardWhenAllowed(app: app, context: context)
|
|
1481
|
+
#else
|
|
1482
|
+
return resolvedTouchReferenceFrame(app: app, appFrame: app.frame)
|
|
1483
|
+
#endif
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
private func shouldUseSynthesizedScrollPath() -> Bool {
|
|
1487
|
+
#if os(iOS)
|
|
1488
|
+
return true
|
|
1489
|
+
#else
|
|
1490
|
+
return false
|
|
1491
|
+
#endif
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
private func dragCommandName(message: String) -> String {
|
|
1495
|
+
return message == "scrolled" ? "scroll" : "drag"
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1122
1498
|
private func currentXCTestFailureCount() -> Int {
|
|
1123
1499
|
return testRun?.failureCount ?? 0
|
|
1124
1500
|
}
|
|
@@ -1151,20 +1527,31 @@ extension RunnerTests {
|
|
|
1151
1527
|
|
|
1152
1528
|
private func shouldSkipAppActivationPreflight(_ command: Command) -> Bool {
|
|
1153
1529
|
#if os(iOS)
|
|
1154
|
-
// Coordinate-only synthesized taps can run after an AX-fatal screen because they do not
|
|
1155
|
-
// app activation, window lookup, keyboard lookup, or element resolution. Selector/text
|
|
1156
|
-
// intentionally stay on the normal AX path because they need an element query.
|
|
1530
|
+
// Coordinate-only synthesized taps can run after an AX-fatal screen because they do not
|
|
1531
|
+
// need app activation, window lookup, keyboard lookup, or element resolution. Selector/text
|
|
1532
|
+
// interactions intentionally stay on the normal AX path because they need an element query.
|
|
1533
|
+
// Scroll/drag/sequence keep the normal foreground guard and stabilization path.
|
|
1534
|
+
guard command.text == nil, command.selectorKey == nil else { return false }
|
|
1535
|
+
guard hasCachedTargetForActivationSkip(command: command) else { return false }
|
|
1157
1536
|
return command.command == .tap
|
|
1158
1537
|
&& command.synthesized == true
|
|
1159
1538
|
&& command.x != nil
|
|
1160
1539
|
&& command.y != nil
|
|
1161
|
-
&& command.text == nil
|
|
1162
|
-
&& command.selectorKey == nil
|
|
1163
1540
|
#else
|
|
1164
1541
|
return false
|
|
1165
1542
|
#endif
|
|
1166
1543
|
}
|
|
1167
1544
|
|
|
1545
|
+
private func hasCachedTargetForActivationSkip(command: Command) -> Bool {
|
|
1546
|
+
guard currentApp != nil else { return false }
|
|
1547
|
+
guard let bundleId = command.appBundleId?.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
1548
|
+
!bundleId.isEmpty
|
|
1549
|
+
else {
|
|
1550
|
+
return true
|
|
1551
|
+
}
|
|
1552
|
+
return currentBundleId == bundleId
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1168
1555
|
private func resolveAppWithoutActivation(command: Command) -> XCUIApplication {
|
|
1169
1556
|
guard let bundleId = command.appBundleId?
|
|
1170
1557
|
.trimmingCharacters(in: .whitespacesAndNewlines),
|