@@ -22,6 +22,9 @@ interface StartSessionArgs {
22
22
23
23
/**
24
24
* Starts an App Live session after filtering, fuzzy matching, and launching.
25
+ * @param args - The arguments for starting the session.
26
+ * @returns The launch URL for the session.
27
+ * @throws Will throw an error if no devices are found or if the app URL is invalid.
25
28
*/
26
29
export async function startSession ( args : StartSessionArgs ) : Promise < string > {
27
30
const { appPath, desiredPlatform, desiredPhone } = args ;
@@ -32,52 +35,126 @@ export async function startSession(args: StartSessionArgs): Promise<string> {
32
35
group . devices . map ( ( dev : any ) => ( { ...dev , os : group . os } ) ) ,
33
36
) ;
34
37
35
- // Exact filter by platform and version
38
+ desiredPlatformVersion = resolvePlatformVersion (
39
+ allDevices ,
40
+ desiredPlatform ,
41
+ desiredPlatformVersion ,
42
+ ) ;
43
+
44
+ const filteredDevices = filterDevicesByPlatformAndVersion (
45
+ allDevices ,
46
+ desiredPlatform ,
47
+ desiredPlatformVersion ,
48
+ ) ;
49
+
50
+ const matches = await fuzzySearchDevices ( filteredDevices , desiredPhone ) ;
51
+
52
+ const selectedDevice = validateAndSelectDevice (
53
+ matches ,
54
+ desiredPhone ,
55
+ desiredPlatform ,
56
+ desiredPlatformVersion ,
57
+ ) ;
58
+
59
+ const { app_url } = await uploadApp ( appPath ) ;
60
+
61
+ validateAppUrl ( app_url ) ;
62
+
63
+ const launchUrl = constructLaunchUrl (
64
+ app_url ,
65
+ selectedDevice ,
66
+ desiredPlatform ,
67
+ desiredPlatformVersion ,
68
+ ) ;
69
+
70
+ openBrowser ( launchUrl ) ;
71
+
72
+ return launchUrl ;
73
+ }
74
+
75
+ /**
76
+ * Resolves the platform version based on the desired platform and version.
77
+ * @param allDevices - The list of all devices.
78
+ * @param desiredPlatform - The desired platform (android or ios).
79
+ * @param desiredPlatformVersion - The desired platform version.
80
+ * @returns The resolved platform version.
81
+ * @throws Will throw an error if the platform version is not valid.
82
+ */
83
+ function resolvePlatformVersion (
84
+ allDevices : DeviceEntry [ ] ,
85
+ desiredPlatform : string ,
86
+ desiredPlatformVersion : string ,
87
+ ) : string {
36
88
if (
37
- desiredPlatformVersion == "latest" ||
38
- desiredPlatformVersion == "oldest"
89
+ desiredPlatformVersion === "latest" ||
90
+ desiredPlatformVersion === "oldest"
39
91
) {
40
92
const filtered = allDevices . filter ( ( d ) => d . os === desiredPlatform ) ;
41
93
filtered . sort ( ( a , b ) => {
42
94
const versionA = parseFloat ( a . os_version ) ;
43
95
const versionB = parseFloat ( b . os_version ) ;
44
96
return desiredPlatformVersion === "latest"
45
- ? versionB - versionA // descending for "latest"
46
- : versionA - versionB ; // ascending for specific version
97
+ ? versionB - versionA
98
+ : versionA - versionB ;
47
99
} ) ;
48
100
49
- const requiredVersion = filtered [ 0 ] . os_version ;
50
-
51
- desiredPlatformVersion = requiredVersion ;
101
+ return filtered [ 0 ] . os_version ;
52
102
}
53
- const filtered = allDevices . filter ( ( d ) => {
103
+ return desiredPlatformVersion ;
104
+ }
105
+
106
+ /**
107
+ * Filters devices based on the desired platform and version.
108
+ * @param allDevices - The list of all devices.
109
+ * @param desiredPlatform - The desired platform (android or ios).
110
+ * @param desiredPlatformVersion - The desired platform version.
111
+ * @returns The filtered list of devices.
112
+ * @throws Will throw an error if the platform version is not valid.
113
+ */
114
+ function filterDevicesByPlatformAndVersion (
115
+ allDevices : DeviceEntry [ ] ,
116
+ desiredPlatform : string ,
117
+ desiredPlatformVersion : string ,
118
+ ) : DeviceEntry [ ] {
119
+ return allDevices . filter ( ( d ) => {
54
120
if ( d . os !== desiredPlatform ) return false ;
55
121
56
- // Attempt to compare as floats
57
122
try {
58
123
const versionA = parseFloat ( d . os_version ) ;
59
124
const versionB = parseFloat ( desiredPlatformVersion ) ;
60
125
return versionA === versionB ;
61
126
} catch {
62
- // Fallback to exact string match if parsing fails
63
127
return d . os_version === desiredPlatformVersion ;
64
128
}
65
129
} ) ;
130
+ }
66
131
67
- // Fuzzy match
68
- const matches = await fuzzySearchDevices ( filtered , desiredPhone ) ;
69
-
132
+ /**
133
+ * Validates the selected device and handles multiple matches.
134
+ * @param matches - The list of device matches.
135
+ * @param desiredPhone - The desired phone name.
136
+ * @param desiredPlatform - The desired platform (android or ios).
137
+ * @param desiredPlatformVersion - The desired platform version.
138
+ * @returns The selected device entry.
139
+ */
140
+ function validateAndSelectDevice (
141
+ matches : DeviceEntry [ ] ,
142
+ desiredPhone : string ,
143
+ desiredPlatform : string ,
144
+ desiredPlatformVersion : string ,
145
+ ) : DeviceEntry {
70
146
if ( matches . length === 0 ) {
71
147
throw new Error (
72
148
`No devices found matching "${ desiredPhone } " for ${ desiredPlatform } ${ desiredPlatformVersion } ` ,
73
149
) ;
74
150
}
151
+
75
152
const exactMatch = matches . find (
76
153
( d ) => d . display_name . toLowerCase ( ) === desiredPhone . toLowerCase ( ) ,
77
154
) ;
78
155
79
156
if ( exactMatch ) {
80
- matches . splice ( 0 , matches . length , exactMatch ) ; // Replace matches with the exact match
157
+ return exactMatch ;
81
158
} else if ( matches . length >= 1 ) {
82
159
const names = matches . map ( ( d ) => d . display_name ) . join ( ", " ) ;
83
160
const error_message =
@@ -87,57 +164,79 @@ export async function startSession(args: StartSessionArgs): Promise<string> {
87
164
throw new Error ( `${ error_message } ` ) ;
88
165
}
89
166
90
- const { app_url } = await uploadApp ( appPath ) ;
167
+ return matches [ 0 ] ;
168
+ }
91
169
92
- if ( ! app_url . match ( "bs://" ) ) {
170
+ /**
171
+ * Validates the app URL.
172
+ * @param appUrl - The app URL to validate.
173
+ * @throws Will throw an error if the app URL is not valid.
174
+ */
175
+ function validateAppUrl ( appUrl : string ) : void {
176
+ if ( ! appUrl . match ( "bs://" ) ) {
93
177
throw new Error ( "The app path is not a valid BrowserStack app URL." ) ;
94
178
}
179
+ }
95
180
96
- const device = matches [ 0 ] ;
181
+ /**
182
+ * Constructs the launch URL for the App Live session.
183
+ * @param appUrl - The app URL.
184
+ * @param device - The selected device entry.
185
+ * @param desiredPlatform - The desired platform (android or ios).
186
+ * @param desiredPlatformVersion - The desired platform version.
187
+ * @returns The constructed launch URL.
188
+ */
189
+ function constructLaunchUrl (
190
+ appUrl : string ,
191
+ device : DeviceEntry ,
192
+ desiredPlatform : string ,
193
+ desiredPlatformVersion : string ,
194
+ ) : string {
97
195
const deviceParam = sanitizeUrlParam (
98
196
device . display_name . replace ( / \s + / g, "+" ) ,
99
197
) ;
100
198
101
199
const params = new URLSearchParams ( {
102
200
os : desiredPlatform ,
103
201
os_version : desiredPlatformVersion ,
104
- app_hashed_id : app_url . split ( "bs://" ) . pop ( ) || "" ,
202
+ app_hashed_id : appUrl . split ( "bs://" ) . pop ( ) || "" ,
105
203
scale_to_fit : "true" ,
106
204
speed : "1" ,
107
205
start : "true" ,
108
206
} ) ;
109
- const launchUrl = `https://app-live.browserstack.com/dashboard#${ params . toString ( ) } &device=${ deviceParam } ` ;
110
207
208
+ return `https://app-live.browserstack.com/dashboard#${ params . toString ( ) } &device=${ deviceParam } ` ;
209
+ }
210
+
211
+ /**
212
+ * Opens the launch URL in the default browser.
213
+ * @param launchUrl - The URL to open.
214
+ * @throws Will throw an error if the browser fails to open.
215
+ */
216
+ function openBrowser ( launchUrl : string ) : void {
111
217
try {
112
- // Use platform-specific commands with proper escaping
113
218
const command =
114
219
process . platform === "darwin"
115
220
? [ "open" , launchUrl ]
116
221
: process . platform === "win32"
117
222
? [ "cmd" , "/c" , "start" , launchUrl ]
118
223
: [ "xdg-open" , launchUrl ] ;
119
224
120
- // nosemgrep:javascript.lang.security.detect-child-process.detect-child-process
121
225
const child = childProcess . spawn ( command [ 0 ] , command . slice ( 1 ) , {
122
226
stdio : "ignore" ,
123
227
detached : true ,
124
228
} ) ;
125
229
126
- // Handle process errors
127
230
child . on ( "error" , ( error ) => {
128
231
logger . error (
129
232
`Failed to open browser automatically: ${ error } . Please open this URL manually: ${ launchUrl } ` ,
130
233
) ;
131
234
} ) ;
132
235
133
- // Unref the child process to allow the parent to exit
134
236
child . unref ( ) ;
135
-
136
- return launchUrl ;
137
237
} catch ( error ) {
138
238
logger . error (
139
239
`Failed to open browser automatically: ${ error } . Please open this URL manually: ${ launchUrl } ` ,
140
240
) ;
141
- return launchUrl ;
142
241
}
143
242
}
0 commit comments