@@ -161,9 +161,17 @@ test('parseUiHierarchy decodes XML entities in Android node attributes', () => {
161161 assert . equal ( result . nodes [ 0 ] ! . label , 'Line 1\nLine 2\t&<>"\'' ) ;
162162} ) ;
163163
164+ test ( 'parseUiHierarchy reads Android bounds with negative coordinates' , ( ) => {
165+ const xml =
166+ '<hierarchy><node class="android.widget.TextView" text="Clipped" bounds="[0,935][-67,994]"/></hierarchy>' ;
167+
168+ const result = parseUiHierarchy ( xml , 800 , { raw : true } ) ;
169+ assert . deepEqual ( result . nodes [ 0 ] ! . rect , { x : 0 , y : 935 , width : 0 , height : 59 } ) ;
170+ } ) ;
171+
164172test ( 'androidUiNodes exposes decoded Android hierarchy metadata' , ( ) => {
165173 const xml =
166- '<hierarchy><node package="com.example.app" class="android.widget.EditText" text="Fish & Chips" content-desc="Search field" resource-id="com.example.app:id/search" bounds="[10,20][110,70]" clickable="false" enabled="true" visible-to-user="true" focusable="true" focused="true" password="true" window-index="0" window-type="1" window-layer="3" window-active="true" window-focused="false" window-bounds="[0,0][390,844]"/></hierarchy>' ;
174+ '<hierarchy><node package="com.example.app" class="android.widget.EditText" text="Fish & Chips" content-desc="Search field" resource-id="com.example.app:id/search" bounds="[10,20][110,70]" clickable="false" enabled="true" visible-to-user="true" drawing-order="4" focusable="true" focused="true" password="true" window-index="0" window-type="1" window-layer="3" window-active="true" window-focused="false" window-bounds="[0,0][390,844]"/></hierarchy>' ;
167175
168176 assert . deepEqual ( Array . from ( androidUiNodes ( xml ) ) , [
169177 {
@@ -177,6 +185,7 @@ test('androidUiNodes exposes decoded Android hierarchy metadata', () => {
177185 clickable : false ,
178186 enabled : true ,
179187 visibleToUser : true ,
188+ drawingOrder : 4 ,
180189 focusable : true ,
181190 focused : true ,
182191 password : true ,
@@ -272,7 +281,7 @@ test('parseUiHierarchy excludes Android nodes that are not visible to the user',
272281 ) ;
273282} ) ;
274283
275- test ( 'parseUiHierarchy preserves Android visible-to-user metadata in raw snapshots' , ( ) => {
284+ test ( 'parseUiHierarchy prunes Android nodes that are not visible to the user in raw snapshots' , ( ) => {
276285 const xml = `<hierarchy>
277286 <node class="android.widget.FrameLayout" bounds="[0,0][390,844]" enabled="true" visible-to-user="true">
278287 <node class="android.widget.Button" text="Hidden drawer action" bounds="[10,80][200,120]" clickable="true" enabled="true" visible-to-user="false"/>
@@ -281,8 +290,160 @@ test('parseUiHierarchy preserves Android visible-to-user metadata in raw snapsho
281290
282291 const result = parseUiHierarchy ( xml , 800 , { raw : true } ) ;
283292 assert . equal ( result . nodes [ 0 ] ! . visibleToUser , true ) ;
284- assert . equal ( result . nodes [ 1 ] ! . label , 'Hidden drawer action' ) ;
285- assert . equal ( result . nodes [ 1 ] ! . visibleToUser , false ) ;
293+ assert . equal (
294+ result . nodes . some ( ( node ) => node . label === 'Hidden drawer action' ) ,
295+ false ,
296+ ) ;
297+ } ) ;
298+
299+ test ( 'parseUiHierarchy prunes descendants of Android nodes that are not visible to the user' , ( ) => {
300+ const xml = `<hierarchy>
301+ <node class="android.widget.FrameLayout" bounds="[0,0][390,844]" enabled="true" visible-to-user="true">
302+ <node class="android.view.ViewGroup" bounds="[0,0][390,844]" enabled="true" visible-to-user="false">
303+ <node class="android.widget.Button" text="Hidden drawer action" bounds="[10,80][200,120]" clickable="true" enabled="true" visible-to-user="true"/>
304+ </node>
305+ </node>
306+ </hierarchy>` ;
307+
308+ const result = parseUiHierarchy ( xml , 800 , { raw : true } ) ;
309+ assert . equal (
310+ result . nodes . some ( ( node ) => node . label === 'Hidden drawer action' ) ,
311+ false ,
312+ ) ;
313+ } ) ;
314+
315+ test ( 'parseUiHierarchy prunes lower drawing-order subtrees covered by a foreground sibling' , ( ) => {
316+ const xml = `<hierarchy>
317+ <node class="android.widget.FrameLayout" bounds="[0,0][390,844]" visible-to-user="true" drawing-order="0">
318+ <node class="android.view.ViewGroup" bounds="[0,0][390,844]" visible-to-user="true" drawing-order="2">
319+ <node class="android.widget.Button" text="Foreground action" bounds="[24,420][366,480]" clickable="true" enabled="true" visible-to-user="true" drawing-order="1"/>
320+ </node>
321+ <node class="android.view.ViewGroup" bounds="[0,0][390,844]" visible-to-user="true" drawing-order="1">
322+ <node class="android.widget.ScrollView" bounds="[0,120][300,844]" scrollable="true" clickable="true" enabled="true" visible-to-user="true" drawing-order="1">
323+ <node class="android.widget.Button" text="Hidden drawer action" bounds="[0,220][280,280]" clickable="true" enabled="true" visible-to-user="true" drawing-order="1"/>
324+ </node>
325+ </node>
326+ </node>
327+ </hierarchy>` ;
328+
329+ const result = parseUiHierarchy ( xml , 800 , { raw : true } ) ;
330+ assert . equal (
331+ result . nodes . some ( ( node ) => node . label === 'Foreground action' ) ,
332+ true ,
333+ ) ;
334+ assert . equal (
335+ result . nodes . some ( ( node ) => node . label === 'Hidden drawer action' ) ,
336+ false ,
337+ ) ;
338+ } ) ;
339+
340+ test ( 'parseUiHierarchy keeps visible side-by-side drawer and content subtrees' , ( ) => {
341+ const xml = `<hierarchy>
342+ <node class="android.widget.FrameLayout" bounds="[0,0][390,844]" visible-to-user="true" drawing-order="0">
343+ <node class="android.view.ViewGroup" bounds="[0,0][120,844]" visible-to-user="true" drawing-order="2">
344+ <node class="android.widget.Button" text="Visible drawer action" bounds="[0,220][110,280]" clickable="true" enabled="true" visible-to-user="true" drawing-order="1"/>
345+ </node>
346+ <node class="android.view.ViewGroup" bounds="[120,0][390,844]" visible-to-user="true" drawing-order="1">
347+ <node class="android.widget.Button" text="Visible content action" bounds="[150,420][366,480]" clickable="true" enabled="true" visible-to-user="true" drawing-order="1"/>
348+ </node>
349+ </node>
350+ </hierarchy>` ;
351+
352+ const result = parseUiHierarchy ( xml , 800 , { raw : true } ) ;
353+ assert . equal (
354+ result . nodes . some ( ( node ) => node . label === 'Visible drawer action' ) ,
355+ true ,
356+ ) ;
357+ assert . equal (
358+ result . nodes . some ( ( node ) => node . label === 'Visible content action' ) ,
359+ true ,
360+ ) ;
361+ } ) ;
362+
363+ test ( 'parseUiHierarchy keeps lower siblings when drawing-order metadata is unavailable' , ( ) => {
364+ const xml = `<hierarchy>
365+ <node class="android.widget.FrameLayout" bounds="[0,0][390,844]" visible-to-user="true">
366+ <node class="android.view.ViewGroup" bounds="[0,0][390,844]" visible-to-user="true">
367+ <node class="android.widget.Button" text="Foreground action" bounds="[24,420][366,480]" clickable="true" enabled="true" visible-to-user="true"/>
368+ </node>
369+ <node class="android.view.ViewGroup" bounds="[0,0][390,844]" visible-to-user="true">
370+ <node class="android.widget.Button" text="Legacy drawer action" bounds="[0,220][280,280]" clickable="true" enabled="true" visible-to-user="true"/>
371+ </node>
372+ </node>
373+ </hierarchy>` ;
374+
375+ const result = parseUiHierarchy ( xml , 800 , { raw : true } ) ;
376+ assert . equal (
377+ result . nodes . some ( ( node ) => node . label === 'Foreground action' ) ,
378+ true ,
379+ ) ;
380+ assert . equal (
381+ result . nodes . some ( ( node ) => node . label === 'Legacy drawer action' ) ,
382+ true ,
383+ ) ;
384+ } ) ;
385+
386+ test ( 'parseUiHierarchy keeps overlapping siblings when drawing-order ties' , ( ) => {
387+ const xml = `<hierarchy>
388+ <node class="android.widget.FrameLayout" bounds="[0,0][390,844]" visible-to-user="true" drawing-order="0">
389+ <node class="android.view.ViewGroup" bounds="[0,0][390,844]" visible-to-user="true" drawing-order="1">
390+ <node class="android.widget.Button" text="First tied action" bounds="[24,420][366,480]" clickable="true" enabled="true" visible-to-user="true" drawing-order="1"/>
391+ </node>
392+ <node class="android.view.ViewGroup" bounds="[0,0][390,844]" visible-to-user="true" drawing-order="1">
393+ <node class="android.widget.Button" text="Second tied action" bounds="[0,220][280,280]" clickable="true" enabled="true" visible-to-user="true" drawing-order="1"/>
394+ </node>
395+ </node>
396+ </hierarchy>` ;
397+
398+ const result = parseUiHierarchy ( xml , 800 , { raw : true } ) ;
399+ assert . equal (
400+ result . nodes . some ( ( node ) => node . label === 'First tied action' ) ,
401+ true ,
402+ ) ;
403+ assert . equal (
404+ result . nodes . some ( ( node ) => node . label === 'Second tied action' ) ,
405+ true ,
406+ ) ;
407+ } ) ;
408+
409+ test ( 'parseUiHierarchy keeps lower siblings below the covered-area threshold' , ( ) => {
410+ const xml = `<hierarchy>
411+ <node class="android.widget.FrameLayout" bounds="[0,0][390,844]" visible-to-user="true" drawing-order="0">
412+ <node class="android.view.ViewGroup" bounds="[0,0][390,717]" visible-to-user="true" drawing-order="2">
413+ <node class="android.widget.Button" text="Partial overlay action" bounds="[24,420][366,480]" clickable="true" enabled="true" visible-to-user="true" drawing-order="1"/>
414+ </node>
415+ <node class="android.view.ViewGroup" bounds="[0,0][390,844]" visible-to-user="true" drawing-order="1">
416+ <node class="android.widget.Button" text="Mostly visible action" bounds="[0,760][280,820]" clickable="true" enabled="true" visible-to-user="true" drawing-order="1"/>
417+ </node>
418+ </node>
419+ </hierarchy>` ;
420+
421+ const result = parseUiHierarchy ( xml , 800 , { raw : true } ) ;
422+ assert . equal (
423+ result . nodes . some ( ( node ) => node . label === 'Partial overlay action' ) ,
424+ true ,
425+ ) ;
426+ assert . equal (
427+ result . nodes . some ( ( node ) => node . label === 'Mostly visible action' ) ,
428+ true ,
429+ ) ;
430+ } ) ;
431+
432+ test ( 'parseUiHierarchy keeps lower siblings covered only by non-agent-visible overlays' , ( ) => {
433+ const xml = `<hierarchy>
434+ <node class="android.widget.FrameLayout" bounds="[0,0][390,844]" visible-to-user="true" drawing-order="0">
435+ <node class="android.view.ViewGroup" bounds="[0,0][390,844]" visible-to-user="true" drawing-order="2"/>
436+ <node class="android.view.ViewGroup" bounds="[0,0][390,844]" visible-to-user="true" drawing-order="1">
437+ <node class="android.widget.Button" text="Still visible action" bounds="[0,220][280,280]" clickable="true" enabled="true" visible-to-user="true" drawing-order="1"/>
438+ </node>
439+ </node>
440+ </hierarchy>` ;
441+
442+ const result = parseUiHierarchy ( xml , 800 , { raw : true } ) ;
443+ assert . equal (
444+ result . nodes . some ( ( node ) => node . label === 'Still visible action' ) ,
445+ true ,
446+ ) ;
286447} ) ;
287448
288449test ( 'parseUiHierarchy ignores attribute-name prefix spoofing' , ( ) => {
0 commit comments