@@ -11,12 +11,14 @@ import {
1111 createIosSnapshotRequest ,
1212 deriveIosCaptureHint ,
1313} from '@agent-device/capture-kit/ios-snapshot-planning' ;
14+ import { toIosSnapshotEngineErrorDetails } from './types.ts' ;
1415import {
1516 compactIosInteractiveSnapshot ,
1617 createIosSnapshotEngine ,
1718 IosSnapshotEngineError ,
1819 presentIosSnapshot ,
1920 publishIosSnapshot ,
21+ resolveIosViewportEvidenceFromRoots ,
2022} from './index.ts' ;
2123import type { Rect , RawSnapshotNode } from '@agent-device/kernel/snapshot' ;
2224
@@ -189,6 +191,65 @@ test('regular presentation fails typed when the viewport is missing or the graph
189191 ) ;
190192} ) ;
191193
194+ test ( 'engine error details preserve typed public context' , ( ) => {
195+ const error = new IosSnapshotEngineError ( 'invalid-viewport' , 'invalid viewport' , {
196+ field : 'viewport' ,
197+ index : 4 ,
198+ parentIndex : 2 ,
199+ frame : { x : 0 , y : 0 , width : 10 , height : 10 } ,
200+ clip : { x : 1 , y : 2 , width : 3 , height : 4 } ,
201+ projection : 'regular' ,
202+ } ) ;
203+
204+ assert . deepEqual ( toIosSnapshotEngineErrorDetails ( error ) , {
205+ reason : 'invalid-viewport' ,
206+ field : 'viewport' ,
207+ index : 4 ,
208+ parentIndex : 2 ,
209+ frame : { x : 0 , y : 0 , width : 10 , height : 10 } ,
210+ clip : { x : 1 , y : 2 , width : 3 , height : 4 } ,
211+ projection : 'regular' ,
212+ } ) ;
213+ } ) ;
214+
215+ test ( 'viewport evidence prefers reported application and window roots' , ( ) => {
216+ assert . deepEqual (
217+ resolveIosViewportEvidenceFromRoots ( [
218+ { type : 'XCUIElementTypeApplication' , rectStatus : 'invalid' } ,
219+ {
220+ type : 'XCUIElementTypeWindow' ,
221+ rect : { x : 0 , y : 0 , width : 390 , height : 844 } ,
222+ rectStatus : 'reported' ,
223+ } ,
224+ ] ) ,
225+ { kind : 'reported' , rect : { x : 0 , y : 0 , width : 390 , height : 844 } } ,
226+ ) ;
227+ } ) ;
228+
229+ test ( 'viewport evidence can fall back to the largest top-level root' , ( ) => {
230+ assert . deepEqual (
231+ resolveIosViewportEvidenceFromRoots (
232+ [
233+ { type : 'Other' , rect : { x : 0 , y : 0 , width : 100 , height : 100 } } ,
234+ { type : 'Other' , rect : { x : 0 , y : 0 , width : 200 , height : 300 } } ,
235+ ] ,
236+ { fallbackToLargestRoot : true } ,
237+ ) ,
238+ { kind : 'reported' , rect : { x : 0 , y : 0 , width : 200 , height : 300 } } ,
239+ ) ;
240+ } ) ;
241+
242+ test ( 'viewport evidence preserves explicit missing geometry reasons' , ( ) => {
243+ assert . deepEqual (
244+ resolveIosViewportEvidenceFromRoots ( [ { type : 'Application' , rectStatus : 'invalid' } ] ) ,
245+ { kind : 'missing' , reason : 'invalid' } ,
246+ ) ;
247+ assert . deepEqual (
248+ resolveIosViewportEvidenceFromRoots ( [ { type : 'Application' , rectStatus : 'not-provided' } ] ) ,
249+ { kind : 'missing' , reason : 'not-provided' } ,
250+ ) ;
251+ } ) ;
252+
192253test ( 'presented runner payloads and optional quality payloads cross the host invariant' , ( ) => {
193254 const request = createIosSnapshotRequest ( ) ;
194255 const presentedCapture = publishIosSnapshot ( acquiredInput ( request , nestedNodes ( ) ) , request ) ;
@@ -234,10 +295,12 @@ test('unavailable hittability never becomes regular actionability', () => {
234295 residue : [ { kind : 'unavailable-fact' as const , fact : 'hittability' as const } ] ,
235296 } satisfies IosSnapshotAcquisition ;
236297 const acquired = publishIosSnapshot ( { stage : 'acquired' , acquisition : unavailable } , request ) ;
237- assert . equal (
238- acquired . payload . nodes . find ( ( node ) => node . label === 'Partially visible' ) ?. hittable ,
239- false ,
298+ const partiallyVisible = acquired . payload . nodes . find (
299+ ( node ) => node . label === 'Partially visible' ,
240300 ) ;
301+ assert . ok ( partiallyVisible ) ;
302+ assert . equal ( partiallyVisible . hittable , undefined ) ;
303+ assert . equal ( 'hittable' in partiallyVisible , false ) ;
241304
242305 const available = publishIosSnapshot ( acquiredInput ( request , nestedNodes ( ) ) , request ) ;
243306 const presented : IosSnapshotInput = {
0 commit comments