1
+ // File: src/tools/live-utils/start-session.ts
1
2
import { sanitizeUrlParam } from "../../lib/utils" ;
2
3
import logger from "../../logger" ;
3
4
import childProcess from "child_process" ;
5
+ import { getLiveData } from "./device-cache" ;
6
+ import { resolveVersion } from "./version-resolver" ;
7
+ import { customFuzzySearch } from "../../lib/fuzzy" ;
4
8
5
- interface StartSessionArgs {
6
- browser : string ;
9
+ export interface DesktopArgs {
10
+ platformType : "desktop" ;
11
+ url : string ;
7
12
os : string ;
8
13
osVersion : string ;
9
- url : string ;
14
+ browser : string ;
10
15
browserVersion : string ;
11
16
isLocal : boolean ;
12
17
}
18
+ export interface MobileArgs {
19
+ platformType : "mobile" ;
20
+ url : string ;
21
+ os : string ;
22
+ osVersion : string ;
23
+ device : string ;
24
+ browser : string ;
25
+ isLocal : boolean ;
26
+ }
13
27
28
+ /**
29
+ * Entrypoint: detects platformType & delegates.
30
+ */
14
31
export async function startBrowserSession (
15
- args : StartSessionArgs ,
32
+ args : DesktopArgs | MobileArgs ,
16
33
) : Promise < string > {
17
- // Sanitize all input parameters
18
- const sanitizedArgs = {
19
- browser : sanitizeUrlParam ( args . browser ) ,
20
- os : sanitizeUrlParam ( args . os ) ,
21
- osVersion : sanitizeUrlParam ( args . osVersion ) ,
34
+ if ( args . platformType === "desktop" ) {
35
+ const entry = await filterDesktop ( args ) ;
36
+ const url = buildDesktopUrl ( args , entry ) ;
37
+ openBrowser ( url ) ;
38
+ return url ;
39
+ } else {
40
+ const entry = await filterMobile ( args ) ;
41
+ const url = buildMobileUrl ( args , entry ) ;
42
+ openBrowser ( url ) ;
43
+ return url ;
44
+ }
45
+ }
46
+
47
+ // ——— Desktop ———
48
+
49
+ interface DesktopEntry {
50
+ os : string ;
51
+ os_version : string ;
52
+ browser : string ;
53
+ browser_version : string ;
54
+ }
55
+
56
+ async function filterDesktop ( args : DesktopArgs ) : Promise < DesktopEntry > {
57
+ const data = await getLiveData ( ) ;
58
+ const all : DesktopEntry [ ] = data . desktop . flatMap ( ( plat : any ) =>
59
+ plat . browsers . map ( ( b : any ) => ( {
60
+ os : plat . os ,
61
+ os_version : plat . os_version ,
62
+ browser : b . browser ,
63
+ browser_version : b . browser_version ,
64
+ } ) ) ,
65
+ ) ;
66
+
67
+ let entries = all . filter ( ( e ) => e . os === args . os ) ;
68
+ if ( ! entries . length ) throw new Error ( `No OS entries for "${ args . os } ".` ) ;
69
+
70
+ entries = entries . filter ( ( e ) => e . browser === args . browser ) ;
71
+ if ( ! entries . length )
72
+ throw new Error ( `No browser "${ args . browser } " on ${ args . os } .` ) ;
73
+
74
+ const uniqueVers = Array . from ( new Set ( entries . map ( ( e ) => e . os_version ) ) ) ;
75
+
76
+ let chosenOS : string ;
77
+ if ( args . os === "OS X" ) {
78
+ // macOS named versions: fuzzy match or pick first/last in JSON order
79
+ if ( args . osVersion === "latest" ) {
80
+ chosenOS = uniqueVers [ uniqueVers . length - 1 ] ;
81
+ } else if ( args . osVersion === "oldest" ) {
82
+ chosenOS = uniqueVers [ 0 ] ;
83
+ } else {
84
+ // try fuzzy
85
+ const fuzzy = customFuzzySearch (
86
+ uniqueVers . map ( ( v ) => ( { os_version : v } ) ) ,
87
+ [ "os_version" ] ,
88
+ args . osVersion ,
89
+ 1 ,
90
+ ) ;
91
+ chosenOS = fuzzy . length ? fuzzy [ 0 ] . os_version : args . osVersion ;
92
+ }
93
+ // fallback if still not valid
94
+ if ( ! uniqueVers . includes ( chosenOS ) ) {
95
+ chosenOS = uniqueVers [ 0 ] ;
96
+ }
97
+ } else {
98
+ // numeric/semantic resolve for Windows
99
+ chosenOS = resolveVersion ( args . osVersion , uniqueVers ) ;
100
+ }
101
+ entries = entries . filter ( ( e ) => e . os_version === chosenOS ) ;
102
+ // resolve browser version
103
+ const browVers = entries . map ( ( e ) => e . browser_version ) ;
104
+ const chosenBrow = resolveVersion ( args . browserVersion , browVers ) ;
105
+ const final = entries . find ( ( e ) => e . browser_version === chosenBrow ) ;
106
+ if ( ! final )
107
+ throw new Error ( `No entry for browser version "${ args . browserVersion } ".` ) ;
108
+
109
+ return final ;
110
+ }
111
+
112
+ function buildDesktopUrl ( args : DesktopArgs , e : DesktopEntry ) : string {
113
+ const params = new URLSearchParams ( {
114
+ os : sanitizeUrlParam ( e . os ) ,
115
+ os_version : sanitizeUrlParam ( e . os_version ) ,
116
+ browser : sanitizeUrlParam ( e . browser ) ,
117
+ browser_version : sanitizeUrlParam ( e . browser_version ) ,
22
118
url : sanitizeUrlParam ( args . url ) ,
23
- browserVersion : sanitizeUrlParam ( args . browserVersion ) ,
24
- isLocal : args . isLocal ,
119
+ scale_to_fit : "true" ,
120
+ resolution : "responsive-mode" ,
121
+ speed : "1" ,
122
+ local : args . isLocal ? "true" : "false" ,
123
+ start : "true" ,
124
+ } ) ;
125
+ return `https://live.browserstack.com/dashboard#${ params . toString ( ) } ` ;
126
+ }
127
+
128
+ // ——— Mobile ———
129
+
130
+ interface MobileEntry {
131
+ os : string ;
132
+ os_version : string ;
133
+ display_name : string ;
134
+ }
135
+
136
+ async function filterMobile ( args : MobileArgs ) : Promise < MobileEntry > {
137
+ const data = await getLiveData ( ) ;
138
+ const all : MobileEntry [ ] = data . mobile . flatMap ( ( grp : any ) =>
139
+ grp . devices . map ( ( d : any ) => ( {
140
+ os : grp . os ,
141
+ os_version : d . os_version ,
142
+ display_name : d . display_name ,
143
+ } ) ) ,
144
+ ) ;
145
+
146
+ let candidates = all . filter ( ( d ) => d . os === args . os ) ;
147
+ if ( ! candidates . length )
148
+ throw new Error ( `No mobile OS entries for "${ args . os } ".` ) ;
149
+
150
+ // resolve OS version
151
+ const vers = candidates . map ( ( d ) => d . os_version ) ;
152
+ const chosen = resolveVersion ( args . osVersion , vers ) ;
153
+ candidates = candidates . filter ( ( d ) => d . os_version === chosen ) ;
154
+
155
+ // fuzzy‐match device name
156
+ const matches = customFuzzySearch (
157
+ candidates ,
158
+ [ "display_name" ] ,
159
+ args . device ,
160
+ 5 ,
161
+ ) ;
162
+ if ( ! matches . length )
163
+ throw new Error (
164
+ `No devices matching "${ args . device } " on ${ args . os } ${ chosen } .` ,
165
+ ) ;
166
+
167
+ const exact = matches . find (
168
+ ( m ) => m . display_name . toLowerCase ( ) === args . device . toLowerCase ( ) ,
169
+ ) ;
170
+ if ( ! exact ) {
171
+ const names = matches . map ( ( m ) => m . display_name ) . join ( ", " ) ;
172
+ throw new Error ( `Did you mean: ${ names } ?` ) ;
173
+ }
174
+ return exact ;
175
+ }
176
+
177
+ function buildMobileUrl ( args : MobileArgs , d : MobileEntry ) : string {
178
+ const os_map = {
179
+ android : "Android" ,
180
+ ios : "iOS" ,
181
+ winphone : "Winphone" ,
25
182
} ;
183
+ const os = os_map [ d . os as keyof typeof os_map ] || d . os ;
26
184
27
- // Construct URL with encoded parameters
28
185
const params = new URLSearchParams ( {
29
- os : sanitizedArgs . os ,
30
- os_version : sanitizedArgs . osVersion ,
31
- browser : sanitizedArgs . browser ,
32
- browser_version : sanitizedArgs . browserVersion ,
186
+ os : sanitizeUrlParam ( os ) ,
187
+ os_version : sanitizeUrlParam ( d . os_version ) ,
188
+ device : d . display_name ,
189
+ device_browser : sanitizeUrlParam ( args . browser ) ,
190
+ url : sanitizeUrlParam ( args . url ) ,
33
191
scale_to_fit : "true" ,
34
- url : sanitizedArgs . url ,
35
- resolution : "responsive-mode" ,
36
192
speed : "1" ,
37
- local : sanitizedArgs . isLocal ? "true" : "false" ,
38
193
start : "true" ,
39
194
} ) ;
195
+ return `https://live.browserstack.com/dashboard#${ params . toString ( ) } ` ;
196
+ }
40
197
41
- const launchUrl = `https://live.browserstack.com/dashboard# ${ params . toString ( ) } ` ;
198
+ // ——— Open a browser window ———
42
199
200
+ function openBrowser ( launchUrl : string ) : void {
43
201
try {
44
- // Use platform-specific commands with proper escaping
45
202
const command =
46
203
process . platform === "darwin"
47
204
? [ "open" , launchUrl ]
@@ -54,22 +211,11 @@ export async function startBrowserSession(
54
211
stdio : "ignore" ,
55
212
detached : true ,
56
213
} ) ;
57
-
58
- // Handle process errors
59
- child . on ( "error" , ( error ) => {
60
- logger . error (
61
- `Failed to open browser automatically: ${ error } . Please open this URL manually: ${ launchUrl } ` ,
62
- ) ;
63
- } ) ;
64
-
65
- // Unref the child process to allow the parent to exit
66
- child . unref ( ) ;
67
-
68
- return launchUrl ;
69
- } catch ( error ) {
70
- logger . error (
71
- `Failed to open browser automatically: ${ error } . Please open this URL manually: ${ launchUrl } ` ,
214
+ child . on ( "error" , ( err ) =>
215
+ logger . error ( `Failed to open browser: ${ err } . URL: ${ launchUrl } ` ) ,
72
216
) ;
73
- return launchUrl ;
217
+ child . unref ( ) ;
218
+ } catch ( err ) {
219
+ logger . error ( `Failed to launch browser: ${ err } . URL: ${ launchUrl } ` ) ;
74
220
}
75
221
}
0 commit comments