@@ -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,114 @@ 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 lower siblings covered only by non-agent-visible overlays' , ( ) => {
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="2"/>
390+ <node class="android.view.ViewGroup" bounds="[0,0][390,844]" visible-to-user="true" drawing-order="1">
391+ <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"/>
392+ </node>
393+ </node>
394+ </hierarchy>` ;
395+
396+ const result = parseUiHierarchy ( xml , 800 , { raw : true } ) ;
397+ assert . equal (
398+ result . nodes . some ( ( node ) => node . label === 'Still visible action' ) ,
399+ true ,
400+ ) ;
286401} ) ;
287402
288403test ( 'parseUiHierarchy ignores attribute-name prefix spoofing' , ( ) => {
0 commit comments