@@ -76,6 +76,7 @@ import { AppError } from '../../../utils/errors.ts';
7676import { runCmd } from '../../../utils/exec.ts' ;
7777import { retryWithPolicy } from '../../../utils/retry.ts' ;
7878import { parseIosDeviceAppsPayload , parseIosDeviceProcessesPayload } from '../devicectl.ts' ;
79+ import { withAppleToolProvider } from '../tool-provider.ts' ;
7980
8081const IOS_TEST_DEVICE : DeviceInfo = {
8182 platform : 'ios' ,
@@ -203,6 +204,20 @@ async function withMockedXcrun(
203204 }
204205}
205206
207+ async function withCapturedXcrunArgs ( run : ( calls : string [ ] [ ] ) => Promise < void > ) : Promise < void > {
208+ const calls : string [ ] [ ] = [ ] ;
209+ await withAppleToolProvider (
210+ async ( cmd , args ) => {
211+ assert . equal ( cmd , 'xcrun' ) ;
212+ calls . push ( args ) ;
213+ return { exitCode : 0 , stdout : '' , stderr : '' } ;
214+ } ,
215+ async ( ) => {
216+ await run ( calls ) ;
217+ } ,
218+ ) ;
219+ }
220+
206221function injectDefaultPrivacyHelp ( script : string ) : string {
207222 if ( script . includes ( 'AGENT_DEVICE_CUSTOM_PRIVACY_HELP' ) ) return script ;
208223 const helpBlock = `if [ "$1" = "simctl" ] && [ "$2" = "privacy" ] && [ "$3" = "help" ]; then
@@ -815,9 +830,9 @@ test('openIosApp web URL on iOS device without app falls back to Safari', async
815830 'launch' ,
816831 '--device' ,
817832 'ios-device-1' ,
818- 'com.apple.mobilesafari' ,
819833 '--payload-url' ,
820834 'https://example.com/path' ,
835+ 'com.apple.mobilesafari' ,
821836 ] ) ;
822837 } finally {
823838 process . env . PATH = previousPath ;
@@ -864,9 +879,9 @@ test('openIosApp custom scheme on iOS device uses active app context', async ()
864879 'launch' ,
865880 '--device' ,
866881 'ios-device-1' ,
867- 'com.example.app' ,
868882 '--payload-url' ,
869883 'myapp://item/42' ,
884+ 'com.example.app' ,
870885 ] ) ;
871886 } finally {
872887 process . env . PATH = previousPath ;
@@ -925,6 +940,142 @@ test('openIosApp captures iOS simulator launch console output when requested', a
925940 }
926941} ) ;
927942
943+ test ( 'openIosApp emits a clean simctl launch when launchArgs is an empty array' , async ( ) => {
944+ await withCapturedXcrunArgs ( async ( calls ) => {
945+ mockEnsureBootedSimulator . mockResolvedValue ( ) ;
946+ await openIosApp ( IOS_TEST_SIMULATOR , 'MyApp' , {
947+ appBundleId : 'com.example.app' ,
948+ launchArgs : [ ] ,
949+ } ) ;
950+ assert . deepEqual ( calls , [ [ 'simctl' , 'launch' , 'sim-1' , 'com.example.app' ] ] ) ;
951+ } ) ;
952+ } ) ;
953+
954+ test ( 'openIosApp forwards dash-prefixed launchArgs after the bundle id on iOS simulator' , async ( ) => {
955+ await withCapturedXcrunArgs ( async ( calls ) => {
956+ mockEnsureBootedSimulator . mockResolvedValue ( ) ;
957+ await openIosApp ( IOS_TEST_SIMULATOR , 'MyApp' , {
958+ appBundleId : 'com.example.app' ,
959+ launchArgs : [ '-FeatureFlag' , 'YES' ] ,
960+ } ) ;
961+ assert . deepEqual ( calls , [
962+ [ 'simctl' , 'launch' , 'sim-1' , 'com.example.app' , '-FeatureFlag' , 'YES' ] ,
963+ ] ) ;
964+ } ) ;
965+ } ) ;
966+
967+ test ( 'openIosApp inserts the devicectl delimiter before the bundle id for iOS device launchArgs' , async ( ) => {
968+ await withCapturedXcrunArgs ( async ( calls ) => {
969+ await openIosApp ( IOS_TEST_DEVICE , 'MyApp' , {
970+ appBundleId : 'com.example.app' ,
971+ launchArgs : [ '-FeatureFlag' , 'YES' ] ,
972+ } ) ;
973+ assert . deepEqual ( calls , [
974+ [
975+ 'devicectl' ,
976+ 'device' ,
977+ 'process' ,
978+ 'launch' ,
979+ '--device' ,
980+ 'ios-device-1' ,
981+ '--' ,
982+ 'com.example.app' ,
983+ '-FeatureFlag' ,
984+ 'YES' ,
985+ ] ,
986+ ] ) ;
987+ } ) ;
988+ } ) ;
989+
990+ test ( 'openIosApp inserts the devicectl delimiter after --payload-url for iOS device deep links' , async ( ) => {
991+ await withCapturedXcrunArgs ( async ( calls ) => {
992+ await openIosApp ( IOS_TEST_DEVICE , 'myapp://item/42' , {
993+ appBundleId : 'com.example.app' ,
994+ launchArgs : [ '-Tracking' , 'NO' ] ,
995+ } ) ;
996+ assert . deepEqual ( calls , [
997+ [
998+ 'devicectl' ,
999+ 'device' ,
1000+ 'process' ,
1001+ 'launch' ,
1002+ '--device' ,
1003+ 'ios-device-1' ,
1004+ '--payload-url' ,
1005+ 'myapp://item/42' ,
1006+ '--' ,
1007+ 'com.example.app' ,
1008+ '-Tracking' ,
1009+ 'NO' ,
1010+ ] ,
1011+ ] ) ;
1012+ } ) ;
1013+ } ) ;
1014+
1015+ test ( 'openIosApp rejects launchArgs combined with URL deep link on iOS simulator' , async ( ) => {
1016+ mockEnsureBootedSimulator . mockResolvedValue ( ) ;
1017+ await assert . rejects (
1018+ ( ) =>
1019+ openIosApp ( IOS_TEST_SIMULATOR , 'myapp://item/42' , {
1020+ launchArgs : [ '-FeatureFlag' , 'YES' ] ,
1021+ } ) ,
1022+ ( error : unknown ) => {
1023+ assert . ok ( error instanceof AppError ) ;
1024+ assert . equal ( error . code , 'INVALID_ARGS' ) ;
1025+ assert . match ( String ( error . message ) , / s i m c t l o p e n u r l / ) ;
1026+ return true ;
1027+ } ,
1028+ ) ;
1029+ await assert . rejects (
1030+ ( ) =>
1031+ openIosApp ( IOS_TEST_SIMULATOR , 'MyApp' , {
1032+ appBundleId : 'com.example.app' ,
1033+ url : 'https://example.com/path' ,
1034+ launchArgs : [ '-FeatureFlag' , 'YES' ] ,
1035+ } ) ,
1036+ ( error : unknown ) => {
1037+ assert . ok ( error instanceof AppError ) ;
1038+ assert . equal ( error . code , 'INVALID_ARGS' ) ;
1039+ return true ;
1040+ } ,
1041+ ) ;
1042+ } ) ;
1043+
1044+ test ( 'openIosApp treats empty launchArgs as absent for iOS simulator URL opens' , async ( ) => {
1045+ await withCapturedXcrunArgs ( async ( calls ) => {
1046+ mockEnsureBootedSimulator . mockResolvedValue ( ) ;
1047+ await openIosApp ( IOS_TEST_SIMULATOR , 'myapp://item/42' , { launchArgs : [ ] } ) ;
1048+ await openIosApp ( IOS_TEST_SIMULATOR , 'MyApp' , {
1049+ appBundleId : 'com.example.app' ,
1050+ url : 'https://example.com/path' ,
1051+ launchArgs : [ ] ,
1052+ } ) ;
1053+ assert . deepEqual ( calls , [
1054+ [ 'simctl' , 'openurl' , 'sim-1' , 'myapp://item/42' ] ,
1055+ [ 'simctl' , 'openurl' , 'sim-1' , 'https://example.com/path' ] ,
1056+ ] ) ;
1057+ } ) ;
1058+ } ) ;
1059+
1060+ test ( 'openIosApp rejects non-empty launchArgs on macOS' , async ( ) => {
1061+ await assert . rejects (
1062+ ( ) =>
1063+ openIosApp ( MACOS_TEST_DEVICE , 'TextEdit' , {
1064+ launchArgs : [ '-FeatureFlag' , 'YES' ] ,
1065+ } ) ,
1066+ ( error : unknown ) => {
1067+ assert . ok ( error instanceof AppError ) ;
1068+ assert . equal ( error . code , 'UNSUPPORTED_OPERATION' ) ;
1069+ assert . match ( String ( error . message ) , / m a c O S / ) ;
1070+ return true ;
1071+ } ,
1072+ ) ;
1073+ } ) ;
1074+
1075+ test ( 'openIosApp treats empty launchArgs as absent on macOS' , async ( ) => {
1076+ await openIosApp ( MACOS_TEST_DEVICE , 'TextEdit' , { launchArgs : [ ] } ) ;
1077+ } ) ;
1078+
9281079test ( 'readIosClipboardText rejects physical devices' , async ( ) => {
9291080 await assert . rejects (
9301081 ( ) => readIosClipboardText ( IOS_TEST_DEVICE ) ,
@@ -1393,9 +1544,9 @@ test('openIosApp with app and URL on iOS device launches app bundle with payload
13931544 'launch' ,
13941545 '--device' ,
13951546 'ios-device-1' ,
1396- 'com.example.app' ,
13971547 '--payload-url' ,
13981548 'myapp://screen/to' ,
1549+ 'com.example.app' ,
13991550 ] ) ;
14001551 } finally {
14011552 process . env . PATH = previousPath ;
0 commit comments