@@ -15,6 +15,43 @@ import type { DeviceInfo } from '../../../utils/device.ts';
1515import { AppError } from '../../../utils/errors.ts' ;
1616import { findBounds , parseUiHierarchy } from '../ui-hierarchy.ts' ;
1717
18+ async function withMockedAdb (
19+ tempPrefix : string ,
20+ script : string ,
21+ run : ( ctx : { argsLogPath : string ; device : DeviceInfo } ) => Promise < void > ,
22+ ) : Promise < void > {
23+ const tmpDir = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , tempPrefix ) ) ;
24+ const adbPath = path . join ( tmpDir , 'adb' ) ;
25+ const argsLogPath = path . join ( tmpDir , 'args.log' ) ;
26+ await fs . writeFile ( adbPath , script , 'utf8' ) ;
27+ await fs . chmod ( adbPath , 0o755 ) ;
28+
29+ const previousPath = process . env . PATH ;
30+ const previousArgsFile = process . env . AGENT_DEVICE_TEST_ARGS_FILE ;
31+ process . env . PATH = `${ tmpDir } ${ path . delimiter } ${ previousPath ?? '' } ` ;
32+ process . env . AGENT_DEVICE_TEST_ARGS_FILE = argsLogPath ;
33+
34+ const device : DeviceInfo = {
35+ platform : 'android' ,
36+ id : 'emulator-5554' ,
37+ name : 'Pixel' ,
38+ kind : 'emulator' ,
39+ booted : true ,
40+ } ;
41+
42+ try {
43+ await run ( { argsLogPath, device } ) ;
44+ } finally {
45+ process . env . PATH = previousPath ;
46+ if ( previousArgsFile === undefined ) {
47+ delete process . env . AGENT_DEVICE_TEST_ARGS_FILE ;
48+ } else {
49+ process . env . AGENT_DEVICE_TEST_ARGS_FILE = previousArgsFile ;
50+ }
51+ await fs . rm ( tmpDir , { recursive : true , force : true } ) ;
52+ }
53+ }
54+
1855test ( 'parseUiHierarchy reads double-quoted Android node attributes' , ( ) => {
1956 const xml =
2057 '<hierarchy><node class="android.widget.TextView" text="Hello" content-desc="Greeting" resource-id="com.demo:id/title" bounds="[10,20][110,60]" clickable="true" enabled="true"/></hierarchy>' ;
@@ -281,126 +318,45 @@ test('swipeAndroid invokes adb input swipe with duration', async () => {
281318} ) ;
282319
283320test ( 'setAndroidSetting permission grant camera uses pm grant' , async ( ) => {
284- const tmpDir = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , 'agent-device-android-permission-camera-' ) ) ;
285- const adbPath = path . join ( tmpDir , 'adb' ) ;
286- const argsLogPath = path . join ( tmpDir , 'args.log' ) ;
287- await fs . writeFile (
288- adbPath ,
321+ await withMockedAdb (
322+ 'agent-device-android-permission-camera-' ,
289323 '#!/bin/sh\nprintf "__CMD__\\n" >> "$AGENT_DEVICE_TEST_ARGS_FILE"\nprintf "%s\\n" "$@" >> "$AGENT_DEVICE_TEST_ARGS_FILE"\nexit 0\n' ,
290- 'utf8' ,
324+ async ( { argsLogPath, device } ) => {
325+ await setAndroidSetting ( device , 'permission' , 'grant' , 'com.example.app' , {
326+ permissionTarget : 'camera' ,
327+ } ) ;
328+ const logged = await fs . readFile ( argsLogPath , 'utf8' ) ;
329+ assert . match ( logged , / s h e l l \n p m \n g r a n t \n c o m \. e x a m p l e \. a p p \n a n d r o i d \. p e r m i s s i o n \. C A M E R A / ) ;
330+ } ,
291331 ) ;
292- await fs . chmod ( adbPath , 0o755 ) ;
293-
294- const previousPath = process . env . PATH ;
295- const previousArgsFile = process . env . AGENT_DEVICE_TEST_ARGS_FILE ;
296- process . env . PATH = `${ tmpDir } ${ path . delimiter } ${ previousPath ?? '' } ` ;
297- process . env . AGENT_DEVICE_TEST_ARGS_FILE = argsLogPath ;
298-
299- const device : DeviceInfo = {
300- platform : 'android' ,
301- id : 'emulator-5554' ,
302- name : 'Pixel' ,
303- kind : 'emulator' ,
304- booted : true ,
305- } ;
306-
307- try {
308- await setAndroidSetting ( device , 'permission' , 'grant' , 'com.example.app' , {
309- permissionTarget : 'camera' ,
310- } ) ;
311- const logged = await fs . readFile ( argsLogPath , 'utf8' ) ;
312- assert . match ( logged , / s h e l l \n p m \n g r a n t \n c o m \. e x a m p l e \. a p p \n a n d r o i d \. p e r m i s s i o n \. C A M E R A / ) ;
313- } finally {
314- process . env . PATH = previousPath ;
315- if ( previousArgsFile === undefined ) {
316- delete process . env . AGENT_DEVICE_TEST_ARGS_FILE ;
317- } else {
318- process . env . AGENT_DEVICE_TEST_ARGS_FILE = previousArgsFile ;
319- }
320- await fs . rm ( tmpDir , { recursive : true , force : true } ) ;
321- }
322332} ) ;
323333
324334test ( 'setAndroidSetting permission deny notifications uses appops' , async ( ) => {
325- const tmpDir = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , 'agent-device-android-permission-notifications-' ) ) ;
326- const adbPath = path . join ( tmpDir , 'adb' ) ;
327- const argsLogPath = path . join ( tmpDir , 'args.log' ) ;
328- await fs . writeFile (
329- adbPath ,
335+ await withMockedAdb (
336+ 'agent-device-android-permission-notifications-' ,
330337 '#!/bin/sh\nprintf "__CMD__\\n" >> "$AGENT_DEVICE_TEST_ARGS_FILE"\nprintf "%s\\n" "$@" >> "$AGENT_DEVICE_TEST_ARGS_FILE"\nexit 0\n' ,
331- 'utf8' ,
338+ async ( { argsLogPath, device } ) => {
339+ await setAndroidSetting ( device , 'permission' , 'deny' , 'com.example.app' , {
340+ permissionTarget : 'notifications' ,
341+ } ) ;
342+ const logged = await fs . readFile ( argsLogPath , 'utf8' ) ;
343+ assert . match ( logged , / s h e l l \n a p p o p s \n s e t \n c o m \. e x a m p l e \. a p p \n P O S T _ N O T I F I C A T I O N \n d e n y / ) ;
344+ } ,
332345 ) ;
333- await fs . chmod ( adbPath , 0o755 ) ;
334-
335- const previousPath = process . env . PATH ;
336- const previousArgsFile = process . env . AGENT_DEVICE_TEST_ARGS_FILE ;
337- process . env . PATH = `${ tmpDir } ${ path . delimiter } ${ previousPath ?? '' } ` ;
338- process . env . AGENT_DEVICE_TEST_ARGS_FILE = argsLogPath ;
339-
340- const device : DeviceInfo = {
341- platform : 'android' ,
342- id : 'emulator-5554' ,
343- name : 'Pixel' ,
344- kind : 'emulator' ,
345- booted : true ,
346- } ;
347-
348- try {
349- await setAndroidSetting ( device , 'permission' , 'deny' , 'com.example.app' , {
350- permissionTarget : 'notifications' ,
351- } ) ;
352- const logged = await fs . readFile ( argsLogPath , 'utf8' ) ;
353- assert . match ( logged , / s h e l l \n a p p o p s \n s e t \n c o m \. e x a m p l e \. a p p \n P O S T _ N O T I F I C A T I O N \n d e n y / ) ;
354- } finally {
355- process . env . PATH = previousPath ;
356- if ( previousArgsFile === undefined ) {
357- delete process . env . AGENT_DEVICE_TEST_ARGS_FILE ;
358- } else {
359- process . env . AGENT_DEVICE_TEST_ARGS_FILE = previousArgsFile ;
360- }
361- await fs . rm ( tmpDir , { recursive : true , force : true } ) ;
362- }
363346} ) ;
364347
365348test ( 'setAndroidSetting permission reset camera maps to pm revoke' , async ( ) => {
366- const tmpDir = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , 'agent-device-android-permission-reset-' ) ) ;
367- const adbPath = path . join ( tmpDir , 'adb' ) ;
368- const argsLogPath = path . join ( tmpDir , 'args.log' ) ;
369- await fs . writeFile (
370- adbPath ,
349+ await withMockedAdb (
350+ 'agent-device-android-permission-reset-' ,
371351 '#!/bin/sh\nprintf "__CMD__\\n" >> "$AGENT_DEVICE_TEST_ARGS_FILE"\nprintf "%s\\n" "$@" >> "$AGENT_DEVICE_TEST_ARGS_FILE"\nexit 0\n' ,
372- 'utf8' ,
352+ async ( { argsLogPath, device } ) => {
353+ await setAndroidSetting ( device , 'permission' , 'reset' , 'com.example.app' , {
354+ permissionTarget : 'camera' ,
355+ } ) ;
356+ const logged = await fs . readFile ( argsLogPath , 'utf8' ) ;
357+ assert . match ( logged , / s h e l l \n p m \n r e v o k e \n c o m \. e x a m p l e \. a p p \n a n d r o i d \. p e r m i s s i o n \. C A M E R A / ) ;
358+ } ,
373359 ) ;
374- await fs . chmod ( adbPath , 0o755 ) ;
375-
376- const previousPath = process . env . PATH ;
377- const previousArgsFile = process . env . AGENT_DEVICE_TEST_ARGS_FILE ;
378- process . env . PATH = `${ tmpDir } ${ path . delimiter } ${ previousPath ?? '' } ` ;
379- process . env . AGENT_DEVICE_TEST_ARGS_FILE = argsLogPath ;
380-
381- const device : DeviceInfo = {
382- platform : 'android' ,
383- id : 'emulator-5554' ,
384- name : 'Pixel' ,
385- kind : 'emulator' ,
386- booted : true ,
387- } ;
388-
389- try {
390- await setAndroidSetting ( device , 'permission' , 'reset' , 'com.example.app' , {
391- permissionTarget : 'camera' ,
392- } ) ;
393- const logged = await fs . readFile ( argsLogPath , 'utf8' ) ;
394- assert . match ( logged , / s h e l l \n p m \n r e v o k e \n c o m \. e x a m p l e \. a p p \n a n d r o i d \. p e r m i s s i o n \. C A M E R A / ) ;
395- } finally {
396- process . env . PATH = previousPath ;
397- if ( previousArgsFile === undefined ) {
398- delete process . env . AGENT_DEVICE_TEST_ARGS_FILE ;
399- } else {
400- process . env . AGENT_DEVICE_TEST_ARGS_FILE = previousArgsFile ;
401- }
402- await fs . rm ( tmpDir , { recursive : true , force : true } ) ;
403- }
404360} ) ;
405361
406362test ( 'setAndroidSetting permission rejects mode argument' , async ( ) => {
@@ -420,18 +376,15 @@ test('setAndroidSetting permission rejects mode argument', async () => {
420376 ( error : unknown ) => {
421377 assert . equal ( error instanceof AppError , true ) ;
422378 assert . equal ( ( error as AppError ) . code , 'INVALID_ARGS' ) ;
423- assert . match ( ( error as AppError ) . message , / m o d e i s o n l y s u p p o r t e d f o r i O S p h o t o s / i) ;
379+ assert . match ( ( error as AppError ) . message , / m o d e i s o n l y s u p p o r t e d f o r p h o t o s / i) ;
424380 return true ;
425381 } ,
426382 ) ;
427383} ) ;
428384
429385test ( 'setAndroidSetting permission grant photos falls back to legacy permission on older SDK' , async ( ) => {
430- const tmpDir = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , 'agent-device-android-permission-photos-fallback-' ) ) ;
431- const adbPath = path . join ( tmpDir , 'adb' ) ;
432- const argsLogPath = path . join ( tmpDir , 'args.log' ) ;
433- await fs . writeFile (
434- adbPath ,
386+ await withMockedAdb (
387+ 'agent-device-android-permission-photos-fallback-' ,
435388 [
436389 '#!/bin/sh' ,
437390 'printf "__CMD__\\n" >> "$AGENT_DEVICE_TEST_ARGS_FILE"' ,
@@ -451,37 +404,13 @@ test('setAndroidSetting permission grant photos falls back to legacy permission
451404 'exit 1' ,
452405 '' ,
453406 ] . join ( '\n' ) ,
454- 'utf8' ,
407+ async ( { argsLogPath, device } ) => {
408+ await setAndroidSetting ( device , 'permission' , 'grant' , 'com.example.app' , {
409+ permissionTarget : 'photos' ,
410+ } ) ;
411+ const logged = await fs . readFile ( argsLogPath , 'utf8' ) ;
412+ assert . match ( logged , / s h e l l \n g e t p r o p \n r o \. b u i l d \. v e r s i o n \. s d k / ) ;
413+ assert . match ( logged , / s h e l l \n p m \n g r a n t \n c o m \. e x a m p l e \. a p p \n a n d r o i d \. p e r m i s s i o n \. R E A D _ E X T E R N A L _ S T O R A G E / ) ;
414+ } ,
455415 ) ;
456- await fs . chmod ( adbPath , 0o755 ) ;
457-
458- const previousPath = process . env . PATH ;
459- const previousArgsFile = process . env . AGENT_DEVICE_TEST_ARGS_FILE ;
460- process . env . PATH = `${ tmpDir } ${ path . delimiter } ${ previousPath ?? '' } ` ;
461- process . env . AGENT_DEVICE_TEST_ARGS_FILE = argsLogPath ;
462-
463- const device : DeviceInfo = {
464- platform : 'android' ,
465- id : 'emulator-5554' ,
466- name : 'Pixel' ,
467- kind : 'emulator' ,
468- booted : true ,
469- } ;
470-
471- try {
472- await setAndroidSetting ( device , 'permission' , 'grant' , 'com.example.app' , {
473- permissionTarget : 'photos' ,
474- } ) ;
475- const logged = await fs . readFile ( argsLogPath , 'utf8' ) ;
476- assert . match ( logged , / s h e l l \n g e t p r o p \n r o \. b u i l d \. v e r s i o n \. s d k / ) ;
477- assert . match ( logged , / s h e l l \n p m \n g r a n t \n c o m \. e x a m p l e \. a p p \n a n d r o i d \. p e r m i s s i o n \. R E A D _ E X T E R N A L _ S T O R A G E / ) ;
478- } finally {
479- process . env . PATH = previousPath ;
480- if ( previousArgsFile === undefined ) {
481- delete process . env . AGENT_DEVICE_TEST_ARGS_FILE ;
482- } else {
483- process . env . AGENT_DEVICE_TEST_ARGS_FILE = previousArgsFile ;
484- }
485- await fs . rm ( tmpDir , { recursive : true , force : true } ) ;
486- }
487416} ) ;
0 commit comments