Skip to content

Commit 5f93e58

Browse files
committed
refactor(ios): make failure scope a required constructor arg; count a turned surface once
Addresses review on #2664. - SnapshotSourceError takes scope as a required argument (no 'generation' default), positioned after the required code. The two factories name their side: snapshotSourceError -> 'generation', snapshotSourceCaptureError -> 'capture'. New errors.test.ts pins both. - unplacedSurfaceHostCount folds a turned surface's window and the surface directly under it into one host, so the disclosed count is a surface count even when both report the turned box. Package test covers the both-turned pair.
1 parent f107d74 commit 5f93e58

4 files changed

Lines changed: 52 additions & 9 deletions

File tree

‎apple/snapshot-presentation/Sources/AgentDeviceSnapshotPresentation/SnapshotCoordinateSpace.swift‎

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -214,17 +214,29 @@ public enum SnapshotGeometrySpace: Equatable {
214214
/// two spaces instead of letting rects under those hosts read as addresses (#2612).
215215
public static func unplacedSurfaceHostCount(in nodes: [RawAXNode], viewport: CGRect) -> Int {
216216
guard isPlottable(viewport) else { return 0 }
217-
var count = 0
218-
for node in nodes {
217+
// `parentIndex` is a position into this array: every producer appends nodes in traversal order
218+
// with `index == nodes.count` and sets a child's `parentIndex` to the parent's index, so
219+
// `nodes[parentIndex]` is that parent. A turned host is one surface even when both its window
220+
// and the surface directly under it report the turned box, so a turned node whose parent is
221+
// itself turned is folded into that parent rather than counted a second time.
222+
func turnedFrame(_ node: RawAXNode) -> Bool {
219223
let parentIsWindow = node.parentIndex.map { index in
220224
nodes.indices.contains(index) && isWindowType(nodes[index].type)
221225
} ?? false
222226
guard isSurfaceHost(isWindow: isWindowType(node.type), parentIsWindow: parentIsWindow)
223-
else { continue }
224-
let frame = CGRect(
225-
x: node.rect.x, y: node.rect.y, width: node.rect.width, height: node.rect.height
227+
else { return false }
228+
return isQuarterTurned(
229+
CGRect(x: node.rect.x, y: node.rect.y, width: node.rect.width, height: node.rect.height),
230+
relativeTo: viewport
226231
)
227-
if isQuarterTurned(frame, relativeTo: viewport) { count += 1 }
232+
}
233+
var count = 0
234+
for node in nodes {
235+
guard turnedFrame(node) else { continue }
236+
let parentTurned = node.parentIndex.map { index in
237+
nodes.indices.contains(index) && turnedFrame(nodes[index])
238+
} ?? false
239+
if !parentTurned { count += 1 }
228240
}
229241
return count
230242
}

‎apple/snapshot-presentation/Tests/AgentDeviceSnapshotPresentationTests/CoordinateSpaceTests.swift‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,16 @@ final class CoordinateSpaceTests: XCTestCase {
355355
]
356356
XCTAssertEqual(SnapshotGeometrySpace.unplacedSurfaceHostCount(in: deep, viewport: viewport), 0)
357357
XCTAssertEqual(SnapshotGeometrySpace.unplacedSurfaceHostCount(in: unplaced, viewport: .infinite), 0)
358+
359+
// One turned surface is one host even when the window AND the surface under it both report the
360+
// turned box: the child folds into the turned window rather than counting a second time.
361+
let bothTurned = [
362+
node(0, type: "Application", rect: SnapshotRect(x: 0, y: 0, width: 874, height: 402), parent: nil),
363+
node(1, type: "Window", rect: SnapshotRect(x: 0, y: 0, width: 402, height: 874), parent: 0),
364+
node(2, type: "Other", rect: SnapshotRect(x: 0, y: 0, width: 402, height: 874), parent: 1),
365+
node(3, type: "Key", rect: SnapshotRect(x: 154, y: 77, width: 45, height: 72), parent: 2),
366+
]
367+
XCTAssertEqual(SnapshotGeometrySpace.unplacedSurfaceHostCount(in: bothTurned, viewport: viewport), 1)
358368
}
359369

360370
/// Golden parity table (#2612): every case in contracts/fixtures/window-coordinate-space.json
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import assert from 'node:assert/strict';
2+
import { test } from 'vitest';
3+
import { snapshotSourceCaptureError, snapshotSourceError } from './errors.ts';
4+
5+
// The scope decides whether the route retires the app generation from the bridge, so it is a
6+
// required constructor argument (no default): a failure cannot be built without naming its side of
7+
// the rule. These two factories are the only constructors, and each names its scope.
8+
test('snapshotSourceError is evidence about the app generation', () => {
9+
const error = snapshotSourceError('transport-failure', 'bridge-disconnected');
10+
assert.equal(error.failureScope, 'generation');
11+
});
12+
13+
test('snapshotSourceCaptureError is evidence about this capture only', () => {
14+
const preparing = snapshotSourceCaptureError('preparing', 'bridge-preparation-pending');
15+
assert.equal(preparing.failureScope, 'capture');
16+
const rotated = snapshotSourceCaptureError('unsupported', 'window-coordinate-space-unresolved', {
17+
windows: 1,
18+
});
19+
assert.equal(rotated.failureScope, 'capture');
20+
assert.equal(rotated.details?.windows, 1);
21+
});

‎packages/platform-apple/src/snapshot-source/errors.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@ export class SnapshotSourceError extends AppError {
2020
constructor(
2121
kind: SnapshotSourceFailureKind,
2222
code: string,
23+
scope: SnapshotSourceFailureScope,
2324
message = `iOS Simulator snapshot source ${kind}: ${code}`,
2425
details: Readonly<Record<string, unknown>> = {},
2526
cause?: unknown,
26-
scope: SnapshotSourceFailureScope = 'generation',
2727
) {
2828
super(
2929
APP_ERROR_CODE_BY_KIND[kind],
@@ -50,7 +50,7 @@ export function snapshotSourceError(
5050
details: Readonly<Record<string, unknown>> = {},
5151
cause?: unknown,
5252
): SnapshotSourceError {
53-
return new SnapshotSourceError(kind, code, undefined, details, cause);
53+
return new SnapshotSourceError(kind, code, 'generation', undefined, details, cause);
5454
}
5555

5656
/**
@@ -63,7 +63,7 @@ export function snapshotSourceCaptureError(
6363
code: string,
6464
details: Readonly<Record<string, unknown>> = {},
6565
): SnapshotSourceError {
66-
return new SnapshotSourceError(kind, code, undefined, details, undefined, 'capture');
66+
return new SnapshotSourceError(kind, code, 'capture', undefined, details);
6767
}
6868

6969
export function asSnapshotSourceError(error: unknown): SnapshotSourceError {

0 commit comments

Comments
 (0)