11import { test , expect } from 'vitest' ;
2- import { classifyAndroidHelperContentRecovery } from '../snapshot-content-recovery.ts' ;
2+ import {
3+ classifyAndroidHelperContent ,
4+ type AndroidHelperContentRecoveryDecision ,
5+ } from '../snapshot-content-recovery.ts' ;
6+
7+ // Legacy assertions below check the unusable decision (or its absence); outcome-specific tests
8+ // call classifyAndroidHelperContent directly.
9+ function unusableDecisionFor (
10+ ...args : Parameters < typeof classifyAndroidHelperContent >
11+ ) : AndroidHelperContentRecoveryDecision | undefined {
12+ const result = classifyAndroidHelperContent ( ...args ) ;
13+ return result . outcome === 'unusable' ? result . decision : undefined ;
14+ }
315
416test ( 'keeps known IME blocking windows instead of falling back to covered app content' , ( ) => {
5- const decision = classifyAndroidHelperContentRecovery (
17+ const decision = unusableDecisionFor (
618 helperXml ( [
719 node ( {
820 windowType : 1 ,
@@ -40,7 +52,7 @@ test('keeps known IME blocking windows instead of falling back to covered app co
4052} ) ;
4153
4254test ( 'rejects helper output with only one meaningful IME node' , ( ) => {
43- const decision = classifyAndroidHelperContentRecovery (
55+ const decision = unusableDecisionFor (
4456 helperXml ( [
4557 node ( {
4658 windowType : 1 ,
@@ -68,7 +80,7 @@ test('rejects helper output with only one meaningful IME node', () => {
6880} ) ;
6981
7082test ( 'rejects helper output with no foreground app or IME content' , ( ) => {
71- const decision = classifyAndroidHelperContentRecovery (
83+ const decision = unusableDecisionFor (
7284 helperXml ( [
7385 node ( {
7486 windowType : 1 ,
@@ -96,7 +108,7 @@ test('rejects helper output with no foreground app or IME content', () => {
96108} ) ;
97109
98110test ( 'keeps a meaningful foreground application surface owned by another package' , ( ) => {
99- const decision = classifyAndroidHelperContentRecovery (
111+ const decision = unusableDecisionFor (
100112 helperXml ( [
101113 node ( {
102114 windowType : 1 ,
@@ -129,7 +141,7 @@ test('keeps a meaningful foreground application surface owned by another package
129141} ) ;
130142
131143test ( 'rejects a status-bar-only capture without window metadata' , ( ) => {
132- const decision = classifyAndroidHelperContentRecovery (
144+ const decision = unusableDecisionFor (
133145 helperXml ( [
134146 node ( {
135147 text : '7:52' ,
@@ -160,7 +172,7 @@ test('rejects a status-bar-only capture without window metadata', () => {
160172} ) ;
161173
162174test ( 'rejects framework-owned content without window metadata' , ( ) => {
163- const decision = classifyAndroidHelperContentRecovery (
175+ const decision = unusableDecisionFor (
164176 helperXml ( [
165177 node ( {
166178 text : 'System dialog' ,
@@ -184,7 +196,7 @@ test('rejects framework-owned content without window metadata', () => {
184196} ) ;
185197
186198test ( 'keeps a recognized system dialog without window metadata' , ( ) => {
187- const decision = classifyAndroidHelperContentRecovery (
199+ const decision = unusableDecisionFor (
188200 helperXml ( [
189201 node ( {
190202 text : "Demo isn't responding" ,
@@ -225,7 +237,7 @@ test('keeps a recognized system dialog without window metadata', () => {
225237} ) ;
226238
227239test ( 'rejects system chrome inherited under an empty application window' , ( ) => {
228- const decision = classifyAndroidHelperContentRecovery (
240+ const decision = unusableDecisionFor (
229241 helperXml ( [
230242 node ( {
231243 windowType : 1 ,
@@ -258,6 +270,114 @@ test('rejects system chrome inherited under an empty application window', () =>
258270 expect ( decision ?. diagnostics . helperNonSystemMeaningfulNodeCount ) . toBe ( 0 ) ;
259271} ) ;
260272
273+ test ( 'returns a meaningful active system surface (notification shade) faithfully' , ( ) => {
274+ const result = classifyAndroidHelperContent (
275+ helperXml ( [
276+ node ( {
277+ windowType : 3 ,
278+ windowActive : true ,
279+ packageName : 'com.android.systemui' ,
280+ className : 'android.widget.FrameLayout' ,
281+ } ) ,
282+ node ( {
283+ text : 'Wed, Jul 16' ,
284+ packageName : 'com.android.systemui' ,
285+ className : 'android.widget.TextView' ,
286+ } ) ,
287+ node ( {
288+ text : 'Internet' ,
289+ resourceId : 'com.android.systemui:id/qs_tile_internet' ,
290+ packageName : 'com.android.systemui' ,
291+ className : 'android.widget.Switch' ,
292+ } ) ,
293+ node ( {
294+ text : 'Manage' ,
295+ resourceId : 'com.android.systemui:id/manage_settings' ,
296+ packageName : 'com.android.systemui' ,
297+ className : 'android.widget.Button' ,
298+ } ) ,
299+ ] ) ,
300+ {
301+ backend : 'android-helper' ,
302+ nodeCount : 4 ,
303+ rootPresent : true ,
304+ windowCount : 1 ,
305+ captureMode : 'interactive-windows' ,
306+ } ,
307+ { foregroundAppPackage : 'com.android.settings' } ,
308+ ) ;
309+
310+ expect ( result . outcome ) . toBe ( 'system-surface-only' ) ;
311+ } ) ;
312+
313+ test ( 'rejects a sparse active system surface below the meaningful-content floor' , ( ) => {
314+ const result = classifyAndroidHelperContent (
315+ helperXml ( [
316+ node ( {
317+ windowType : 3 ,
318+ windowActive : true ,
319+ packageName : 'com.android.systemui' ,
320+ className : 'android.widget.FrameLayout' ,
321+ } ) ,
322+ node ( {
323+ text : '7:52' ,
324+ resourceId : 'com.android.systemui:id/clock' ,
325+ packageName : 'com.android.systemui' ,
326+ className : 'android.widget.TextView' ,
327+ } ) ,
328+ ] ) ,
329+ {
330+ backend : 'android-helper' ,
331+ nodeCount : 2 ,
332+ rootPresent : true ,
333+ windowCount : 1 ,
334+ captureMode : 'interactive-windows' ,
335+ } ,
336+ { foregroundAppPackage : 'com.android.settings' } ,
337+ ) ;
338+
339+ expect ( result . outcome ) . toBe ( 'unusable' ) ;
340+ } ) ;
341+
342+ test ( 'rejects a content-rich system window that is neither active nor focused' , ( ) => {
343+ const result = classifyAndroidHelperContent (
344+ helperXml ( [
345+ node ( {
346+ windowType : 3 ,
347+ packageName : 'com.android.systemui' ,
348+ className : 'android.widget.FrameLayout' ,
349+ } ) ,
350+ node ( {
351+ text : 'Wed, Jul 16' ,
352+ packageName : 'com.android.systemui' ,
353+ className : 'android.widget.TextView' ,
354+ } ) ,
355+ node ( {
356+ text : 'Internet' ,
357+ resourceId : 'com.android.systemui:id/qs_tile_internet' ,
358+ packageName : 'com.android.systemui' ,
359+ className : 'android.widget.Switch' ,
360+ } ) ,
361+ node ( {
362+ text : 'Manage' ,
363+ resourceId : 'com.android.systemui:id/manage_settings' ,
364+ packageName : 'com.android.systemui' ,
365+ className : 'android.widget.Button' ,
366+ } ) ,
367+ ] ) ,
368+ {
369+ backend : 'android-helper' ,
370+ nodeCount : 4 ,
371+ rootPresent : true ,
372+ windowCount : 1 ,
373+ captureMode : 'interactive-windows' ,
374+ } ,
375+ { foregroundAppPackage : 'com.android.settings' } ,
376+ ) ;
377+
378+ expect ( result . outcome ) . toBe ( 'unusable' ) ;
379+ } ) ;
380+
261381function helperXml ( nodes : string [ ] ) : string {
262382 return `<?xml version='1.0' encoding='UTF-8' standalone='yes' ?><hierarchy>${ nodes . join ( '' ) } </hierarchy>` ;
263383}
@@ -268,6 +388,10 @@ function node(options: {
268388 packageName : string ;
269389 className : string ;
270390 windowType ?: number ;
391+ windowActive ?: boolean ;
271392} ) : string {
272- return `<node index="0"${ options . windowType === undefined ? '' : ` window-type="${ options . windowType } "` } text="${ options . text ?? '' } " resource-id="${ options . resourceId ?? '' } " class="${ options . className } " package="${ options . packageName } " visible-to-user="true" enabled="true" bounds="[0,0][100,100]" />` ;
393+ const windowAttrs =
394+ ( options . windowType === undefined ? '' : ` window-type="${ options . windowType } "` ) +
395+ ( options . windowActive === undefined ? '' : ` window-active="${ options . windowActive } "` ) ;
396+ return `<node index="0"${ windowAttrs } text="${ options . text ?? '' } " resource-id="${ options . resourceId ?? '' } " class="${ options . className } " package="${ options . packageName } " visible-to-user="true" enabled="true" bounds="[0,0][100,100]" />` ;
273397}
0 commit comments