Implementation:Webdriverio Webdriverio BrowserStack Launcher Tunnel
Metadata
| Field | Value |
|---|---|
| Page ID | BrowserStack_Launcher_Tunnel |
| Wiki | Webdriverio_Webdriverio |
| Type | Implementation (API Doc) |
| Domains | Testing, Cloud, Networking |
| Knowledge Sources | Repo (https://github.com/webdriverio/webdriverio), Doc (https://www.browserstack.com/docs/local-testing) |
| Related Principles | Principle: Local_Tunnel_Connectivity |
Overview
Concrete tool for managing local tunnels within the WDIO service lifecycle for cloud testing providers. This document covers the BrowserStack Local tunnel (primary), Sauce Connect tunnel, and TestingBot tunnel implementations. Each service manages the tunnel binary lifecycle: starting in onPrepare (before any tests run), automatically injecting tunnel identifiers into capabilities, and stopping in onComplete (after all tests finish).
Description
The tunnel launcher services follow a consistent pattern across all three providers:
- Constructor -- Receives service options and WDIO configuration.
onPrepare-- Checks if tunneling is enabled, configures tunnel options, injects tunnel identifiers into capabilities, starts the tunnel binary, and measures boot time.onComplete-- Stops the tunnel binary and cleans up.
The BrowserStack implementation is the most feature-rich, with support for local testing, test reporting, Percy visual testing, accessibility automation, and test orchestration. The tunnel management is one component of the larger BrowserstackLauncherService.
Source
| File | Lines | Description |
|---|---|---|
packages/wdio-browserstack-service/src/launcher.ts |
L70-577 | BrowserstackLauncherService class with tunnel management in onPrepare
|
packages/wdio-browserstack-service/src/launcher.ts |
L529-577 | BrowserStack Local tunnel start logic |
packages/wdio-browserstack-service/src/launcher.ts |
L580-670+ | onComplete hook with tunnel stop logic
|
packages/wdio-sauce-service/src/launcher.ts |
L20-143 | SauceLauncher with Sauce Connect tunnel management
|
packages/wdio-testingbot-service/src/launcher.ts |
L12-78 | TestingBotLauncher with TestingBot Tunnel management
|
packages/wdio-browserstack-service/src/types.ts |
L62-212 | BrowserstackConfig interface (tunnel options)
|
packages/wdio-sauce-service/src/types.ts |
L4-51 | SauceServiceConfig interface (tunnel options)
|
packages/wdio-testingbot-service/src/types.ts |
L49-60 | TestingbotOptions interface (tunnel options)
|
BrowserStack Local Tunnel
Configuration
services: [['browserstack', {
browserstackLocal: boolean, // Enable/disable the tunnel (default: false)
forcedStop: boolean, // Kill tunnel without waiting for callback (default: false)
opts: Partial<BSOptions> // BrowserStack Local binary options
}]]
I/O Contract -- Service Configuration:
| Parameter | Type | Default | Description |
|---|---|---|---|
browserstackLocal |
boolean |
false |
Enable local tunnel |
forcedStop |
boolean |
false |
Force-kill tunnel binary on completion |
opts |
Partial<BSOptions> |
{} |
Binary options (localIdentifier, verbose, proxy, etc.) |
opts.localIdentifier |
string |
auto-generated | Unique identifier for this tunnel instance |
Tunnel Start (onPrepare)
// packages/wdio-browserstack-service/src/launcher.ts (L529-577)
// Simplified tunnel start logic:
if (!this._options.browserstackLocal) {
return BStackLogger.info('browserstackLocal is not enabled - skipping...')
}
const opts = {
key: this._config.key,
...this._options.opts
}
this.browserstackLocal = new BrowserstackLocalLauncher.Local()
// Inject 'local' flag into all capabilities
this._updateCaps(capabilities, 'local')
if (opts.localIdentifier) {
this._updateCaps(capabilities, 'localIdentifier', opts.localIdentifier)
}
// Start with 60-second timeout
performance.mark('tbTunnelStart')
return Promise.race([
promisify(this.browserstackLocal.start.bind(this.browserstackLocal))(opts),
new Promise((resolve, reject) => {
timer = setTimeout(function () {
reject('Browserstack Local failed to start within 60 seconds!')
}, 60000)
})
])
I/O Contract -- onPrepare:
| Input | Type | Description |
|---|---|---|
config |
Options.Testrunner |
WDIO configuration (includes key for authentication)
|
capabilities |
Capabilities.TestrunnerCapabilities |
Capabilities array (mutated to add tunnel flags) |
| Output | Type | Description |
| Returns | Promise<void> |
Resolves when tunnel is ready, rejects on timeout (60s) or error |
| Side effects | Capability mutation | Adds local: true and localIdentifier to all capabilities
|
Tunnel Stop (onComplete)
// packages/wdio-browserstack-service/src/launcher.ts (L653-670+)
if (!this.browserstackLocal || !this.browserstackLocal.isRunning()) {
return
}
const pid = this.browserstackLocal.pid
this.browserstackLocal.stop((err: Error) => {
// cleanup callback
})
Sauce Connect Tunnel
Configuration
services: [['sauce', {
sauceConnect: boolean, // Enable/disable Sauce Connect (default: false)
sauceConnectOpts: SauceConnectOptions // Sauce Connect options
}]]
Tunnel Start (onPrepare)
// packages/wdio-sauce-service/src/launcher.ts (L35-101)
async onPrepare (config, capabilities) {
if (!this._options.sauceConnect) {
return
}
const sauceConnectTunnelName = (
this._options.sauceConnectOpts?.tunnelName ||
`SC-tunnel-${Math.random().toString().slice(2)}`
)
const sauceConnectOpts: SauceConnectOptions = {
tunnelName: sauceConnectTunnelName,
...this._options.sauceConnectOpts,
metadata: metadata
}
// Inject tunnel name into all capabilities
const prepareCapability = makeCapabilityFactory(sauceConnectTunnelName)
for (const capability of capabilities) {
prepareCapability(capability)
}
log.info('Starting Sauce Connect Tunnel')
performance.mark('sauceConnectStart')
this._sauceConnectProcess = await this.startTunnel(sauceConnectOpts)
performance.mark('sauceConnectEnd')
}
I/O Contract -- SauceServiceConfig tunnel options:
| Parameter | Type | Default | Description |
|---|---|---|---|
sauceConnect |
boolean |
false |
Enable Sauce Connect tunnel |
sauceConnectOpts |
SauceConnectOptions |
{} |
Sauce Connect binary options |
sauceConnectOpts.tunnelName |
string |
auto-generated | Unique tunnel identifier |
Tunnel Stop (onComplete)
// packages/wdio-sauce-service/src/launcher.ts (L136-142)
onComplete () {
if (!this._sauceConnectProcess) {
return
}
return this._sauceConnectProcess.close()
}
TestingBot Tunnel
Configuration
services: [['testingbot', {
tbTunnel: boolean, // Enable/disable TestingBot Tunnel (default: false)
tbTunnelOpts: TunnelLauncherOptions // Tunnel binary options
}]]
Tunnel Start (onPrepare)
// packages/wdio-testingbot-service/src/launcher.ts (L20-65)
async onPrepare (config, capabilities) {
if (!this.options.tbTunnel || !config.user || !config.key) {
return
}
const tbTunnelIdentifier = (
this.options.tbTunnelOpts?.tunnelIdentifier ||
`TB-tunnel-${Math.random().toString().slice(2)}`
)
this.tbTunnelOpts = Object.assign({
apiKey: config.user,
apiSecret: config.key,
'tunnel-identifier': tbTunnelIdentifier,
}, this.options.tbTunnelOpts)
// Inject tunnel identifier into all capabilities
for (const capability of capabilitiesEntries) {
const c = (caps as Capabilities.W3CCapabilities).alwaysMatch || caps
if (!c['tb:options']) {
c['tb:options'] = {}
}
c['tb:options']['tunnel-identifier'] = tbTunnelIdentifier
}
performance.mark('tbTunnelStart')
this.tunnel = await promisify(testingbotTunnel)(this.tbTunnelOpts)
performance.mark('tbTunnelEnd')
}
I/O Contract -- TestingbotOptions tunnel options:
| Parameter | Type | Default | Description |
|---|---|---|---|
tbTunnel |
boolean |
false |
Enable TestingBot Tunnel |
tbTunnelOpts |
TunnelLauncherOptions |
{} |
Tunnel binary options |
tbTunnelOpts.tunnelIdentifier |
string |
auto-generated | Unique tunnel identifier |
tbTunnelOpts.apiKey |
string |
from config.user |
TestingBot API key |
tbTunnelOpts.apiSecret |
string |
from config.key |
TestingBot API secret |
Tunnel Stop (onComplete)
// packages/wdio-testingbot-service/src/launcher.ts (L71-77)
onComplete () {
if (!this.tunnel) {
return
}
return new Promise(resolve => this.tunnel!.close(resolve))
}
Cross-Provider Comparison
| Feature | BrowserStack | Sauce Labs | TestingBot |
|---|---|---|---|
| Enable flag | browserstackLocal: true |
sauceConnect: true |
tbTunnel: true
|
| Options key | opts |
sauceConnectOpts |
tbTunnelOpts
|
| Tunnel ID property | opts.localIdentifier |
sauceConnectOpts.tunnelName |
tbTunnelOpts.tunnelIdentifier
|
| Capability namespace | bstack:options.localIdentifier |
sauce:options.tunnelName |
tb:options['tunnel-identifier']
|
| Auto-ID generation | No (requires manual ID) | Yes (SC-tunnel-*) |
Yes (TB-tunnel-*)
|
| Start timeout | 60 seconds | No explicit timeout | No explicit timeout |
| Retry on failure | No | Yes (3 retries for ENOENT) | No |
| Boot time measurement | Yes (PerformanceObserver) | Yes (PerformanceObserver) | Yes (PerformanceObserver) |
Full Example: BrowserStack Local Configuration
// wdio.conf.ts
export const config: WebdriverIO.Config = {
user: process.env.BROWSERSTACK_USERNAME,
key: process.env.BROWSERSTACK_ACCESS_KEY,
services: [['browserstack', {
browserstackLocal: true,
opts: {
localIdentifier: 'ci-tunnel-' + process.env.BUILD_NUMBER,
verbose: true
},
forcedStop: false,
setSessionName: true,
setSessionStatus: true
}]],
capabilities: [{
browserName: 'chrome',
browserVersion: 'latest',
'bstack:options': {
os: 'Windows',
osVersion: '11',
local: true,
localIdentifier: 'ci-tunnel-' + process.env.BUILD_NUMBER,
buildName: 'Local Dev Build',
video: true
}
}],
baseUrl: 'http://localhost:3000',
specs: ['./test/specs/**/*.spec.ts'],
framework: 'mocha'
}