11import { describe , it , expect , beforeEach , vi } from 'vitest' ;
2+
3+ const { mockTunnel } = vi . hoisted ( ( ) => ( {
4+ mockTunnel : { start : vi . fn ( ) , stop : vi . fn ( ) } ,
5+ } ) ) ;
6+
7+ vi . mock ( 'browserstack-local' , ( ) => ( {
8+ Local : class {
9+ start = mockTunnel . start ;
10+ stop = mockTunnel . stop ;
11+ } ,
12+ } ) ) ;
13+
214import { BrowserStackProvider } from '../../src/providers/cloud/browserstack.provider' ;
315
416describe ( 'BrowserStackProvider' , ( ) => {
@@ -9,14 +21,24 @@ describe('BrowserStackProvider', () => {
921 } ) ;
1022
1123 describe ( 'getConnectionConfig' , ( ) => {
12- it ( 'returns BrowserStack hub connection details ' , ( ) => {
13- const config = provider . getConnectionConfig ( { } ) ;
24+ it ( 'returns hub.browserstack.com for browser platform ' , ( ) => {
25+ const config = provider . getConnectionConfig ( { platform : 'browser' } ) ;
1426 expect ( config . hostname ) . toBe ( 'hub.browserstack.com' ) ;
1527 expect ( config . protocol ) . toBe ( 'https' ) ;
1628 expect ( config . port ) . toBe ( 443 ) ;
1729 expect ( config . path ) . toBe ( '/wd/hub' ) ;
1830 } ) ;
1931
32+ it ( 'returns hub-cloud.browserstack.com for android platform' , ( ) => {
33+ const config = provider . getConnectionConfig ( { platform : 'android' } ) ;
34+ expect ( config . hostname ) . toBe ( 'hub-cloud.browserstack.com' ) ;
35+ } ) ;
36+
37+ it ( 'returns hub-cloud.browserstack.com for ios platform' , ( ) => {
38+ const config = provider . getConnectionConfig ( { platform : 'ios' } ) ;
39+ expect ( config . hostname ) . toBe ( 'hub-cloud.browserstack.com' ) ;
40+ } ) ;
41+
2042 it ( 'reads credentials from environment variables' , ( ) => {
2143 vi . stubEnv ( 'BROWSERSTACK_USERNAME' , 'myuser' ) ;
2244 vi . stubEnv ( 'BROWSERSTACK_ACCESS_KEY' , 'mykey' ) ;
@@ -164,6 +186,17 @@ describe('BrowserStackProvider', () => {
164186 const bstack = caps [ 'bstack:options' ] as Record < string , unknown > ;
165187 expect ( bstack . local ) . toBe ( true ) ;
166188 } ) ;
189+
190+ it ( 'sets local: true in bstack:options when browserstackLocal is "external"' , ( ) => {
191+ const caps = provider . buildCapabilities ( {
192+ platform : 'android' ,
193+ deviceName : 'Pixel 7' ,
194+ app : 'bs://abc' ,
195+ browserstackLocal : 'external' ,
196+ } ) ;
197+ const bstack = caps [ 'bstack:options' ] as Record < string , unknown > ;
198+ expect ( bstack . local ) . toBe ( true ) ;
199+ } ) ;
167200 } ) ;
168201
169202 describe ( 'getSessionType' , ( ) => {
@@ -185,4 +218,69 @@ describe('BrowserStackProvider', () => {
185218 expect ( provider . shouldAutoDetach ( { } ) ) . toBe ( false ) ;
186219 } ) ;
187220 } ) ;
221+
222+ describe ( 'startTunnel' , ( ) => {
223+ beforeEach ( ( ) => {
224+ vi . stubEnv ( 'BROWSERSTACK_ACCESS_KEY' , 'testkey' ) ;
225+ mockTunnel . start . mockReset ( ) ;
226+ mockTunnel . stop . mockReset ( ) ;
227+ } ) ;
228+
229+ it ( 'returns the tunnel instance on successful start' , async ( ) => {
230+ mockTunnel . start . mockImplementation ( ( opts : unknown , cb : ( err : unknown ) => void ) => cb ( null ) ) ;
231+
232+ const handle = await provider . startTunnel ( { } ) ;
233+ expect ( handle ) . toBeDefined ( ) ;
234+ } ) ;
235+
236+ it ( 'passes logFile pointing to os.tmpdir() to avoid polluting cwd' , async ( ) => {
237+ let capturedOpts : unknown ;
238+ mockTunnel . start . mockImplementation ( ( opts : unknown , cb : ( err : unknown ) => void ) => {
239+ capturedOpts = opts ;
240+ cb ( null ) ;
241+ } ) ;
242+
243+ await provider . startTunnel ( { } ) ;
244+ const logFile = ( capturedOpts as Record < string , unknown > ) . logFile as string ;
245+ expect ( logFile ) . toBeDefined ( ) ;
246+ expect ( logFile ) . toContain ( 'browserstack-local' ) ;
247+ } ) ;
248+
249+ it ( 'passes forceLocal: true to tunnel start' , async ( ) => {
250+ let capturedOpts : unknown ;
251+ mockTunnel . start . mockImplementation ( ( opts : unknown , cb : ( err : unknown ) => void ) => {
252+ capturedOpts = opts ;
253+ cb ( null ) ;
254+ } ) ;
255+
256+ await provider . startTunnel ( { } ) ;
257+ expect ( ( capturedOpts as Record < string , unknown > ) . forceLocal ) . toBe ( true ) ;
258+ } ) ;
259+
260+ it ( 'returns null when tunnel is already running (plain object error with message)' , async ( ) => {
261+ mockTunnel . start . mockImplementation ( ( opts : unknown , cb : ( err : unknown ) => void ) =>
262+ cb ( { message : 'another browserstack local client is running' } ) ,
263+ ) ;
264+
265+ const handle = await provider . startTunnel ( { } ) ;
266+ expect ( handle ) . toBeNull ( ) ;
267+ } ) ;
268+
269+ it ( 'returns null when server is already listening (plain object error with message)' , async ( ) => {
270+ mockTunnel . start . mockImplementation ( ( opts : unknown , cb : ( err : unknown ) => void ) =>
271+ cb ( { message : 'server is listening on port 45691' } ) ,
272+ ) ;
273+
274+ const handle = await provider . startTunnel ( { } ) ;
275+ expect ( handle ) . toBeNull ( ) ;
276+ } ) ;
277+
278+ it ( 'rethrows unrecognised errors' , async ( ) => {
279+ mockTunnel . start . mockImplementation ( ( opts : unknown , cb : ( err : unknown ) => void ) =>
280+ cb ( { message : 'some other fatal error' } ) ,
281+ ) ;
282+
283+ await expect ( provider . startTunnel ( { } ) ) . rejects . toEqual ( { message : 'some other fatal error' } ) ;
284+ } ) ;
285+ } ) ;
188286} ) ;
0 commit comments