Repository: saucelabs/node-saucelabs
Pull request #82 (2020-05-18 23:51:07)

Allow specifying a Sauce Connect version
It would be nice if someone could easily switch multiple Sauce Connect versions by passing in a parameter. It could also allow Sauce employees and their customers to test new beta versions.

enriquegh: Change looks good but seems like starting SC is not working.
Tried:

```
./bin/sl sc
```
as well as a package:
```node

const SauceLabs = require('saucelabs').default;

(async () => {
    const myAccount = new SauceLabs();
    // using constructor options
    // const myAccount = new SauceLabs({ user: "YOUR-USER", key: "YOUR-ACCESS-KEY"});

    // get job details of last run job
    const job = await myAccount.listJobs(
        process.env.SAUCE_USERNAME,
        { limit: 1, full: true }
    );

    console.log(job);
    const sc = await myAccount.startSauceConnect({
        sePort: 1234
    })
    await sc.close()
})()
```
enriquegh: LGTM

Patch:
diff --git a/README.md b/README.md
index e44ed2e..38e82c4 100644
--- a/README.md
+++ b/README.md
@@ -98,8 +98,10 @@ $ sl downloadJobAsset 690c5877710c422d8be4c622b40c747f video.mp4 --filepath ./vi
 or start Sauce Connect Proxy in EU datacenter:
 
 ```sh
+# start Sauce Connect tunnel for eu-central-1 region
 $ sl sc --region eu --tunnel-identifier "my-tunnel"
-
+# run a specific Sauce Connect version
+$ sl sc --scVersion 4.5.4
 # see all available Sauce Connect parameters via:
 $ sl sc --help
 ```
diff --git a/package.json b/package.json
index 02c8478..c83496c 100644
--- a/package.json
+++ b/package.json
@@ -64,7 +64,7 @@
     "codecov": "^3.7.0",
     "eslint": "^6.8.0",
     "eslint-plugin-import": "^2.20.2",
-    "jest": "^26.0.1",
+    "jest": "^25.5.4",
     "np": "^6.2.3",
     "npm-run-all": "^4.1.5",
     "prettier": "^2.0.5",
diff --git a/src/constants.js b/src/constants.js
index 48d064c..1b56ceb 100644
--- a/src/constants.js
+++ b/src/constants.js
@@ -1,15 +1,15 @@
 import { camelCase } from 'change-case'
 import { version } from '../package.json'
 
-export const SAUCE_CONNECT_VERSION = '4.5.4'
+export const DEFAULT_SAUCE_CONNECT_VERSION = '4.5.4'
 export const SAUCE_CONNECT_BASE = 'https://saucelabs.com/downloads'
 export const SAUCE_CONNECT_DISTS = [
-    [`${SAUCE_CONNECT_BASE}/sc-${SAUCE_CONNECT_VERSION}-osx.zip`, 'darwin'],
-    [`${SAUCE_CONNECT_BASE}/sc-${SAUCE_CONNECT_VERSION}-win32.zip`, 'win32'],
-    [`${SAUCE_CONNECT_BASE}/sc-${SAUCE_CONNECT_VERSION}-linux.tar.gz`, 'linux', 'x64'],
-    [`${SAUCE_CONNECT_BASE}/sc-${SAUCE_CONNECT_VERSION}_linux32.tar.gz`, 'linux']
+    [`${SAUCE_CONNECT_BASE}/sc-%s-osx.zip`, 'darwin'],
+    [`${SAUCE_CONNECT_BASE}/sc-%s-win32.zip`, 'win32'],
+    [`${SAUCE_CONNECT_BASE}/sc-%s-linux.tar.gz`, 'linux', 'x64'],
+    [`${SAUCE_CONNECT_BASE}/sc-%s_linux32.tar.gz`, 'linux']
 ]
-export const SAUCE_VERSION_NOTE = `node-saucelabs v${version}\nSauce Connect v${SAUCE_CONNECT_VERSION}`
+export const SAUCE_VERSION_NOTE = `node-saucelabs v${version}\nSauce Connect v${DEFAULT_SAUCE_CONNECT_VERSION}`
 
 const protocols = [
     require('../apis/sauce.json'),
@@ -121,6 +121,16 @@ const CLI_PARAM_KEYS = CLI_PARAMS.map((param) => param.name)
 const CLI_PARAM_ALIASES = CLI_PARAMS.map((param) => param.alias).filter(Boolean)
 
 export const SAUCE_CONNECT_CLI_PARAMS = [{
+    /**
+     * custom parameter
+     */
+    name: 'sc-version',
+    description: 'Specify the Sauce Connect version you want to use.',
+    default: DEFAULT_SAUCE_CONNECT_VERSION
+}, {
+    /**
+     * Sauce Connect parameter
+     */
     alias: 'a',
     name: 'auth',
     description: 'Perform basic authentication when an URL on <host:port> asks for a username and password.'
diff --git a/src/index.js b/src/index.js
index 1a92204..5d8c98e 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,4 +1,5 @@
 import fs from 'fs'
+import util from 'util'
 import path from 'path'
 import { spawn } from 'child_process'
 
@@ -12,9 +13,9 @@ import {
 } from './utils'
 import {
     PROTOCOL_MAP, DEFAULT_OPTIONS, SYMBOL_INSPECT, SYMBOL_TOSTRING,
-    SYMBOL_ITERATOR, TO_STRING_TAG, SAUCE_CONNECT_VERSION,
-    SAUCE_CONNECT_DISTS, SC_PARAMS_TO_STRIP, SC_READY_MESSAGE,
-    SC_CLOSE_MESSAGE, SC_CLOSE_TIMEOUT
+    SYMBOL_ITERATOR, TO_STRING_TAG, SAUCE_CONNECT_DISTS,
+    SC_PARAMS_TO_STRIP, SC_READY_MESSAGE, SC_CLOSE_MESSAGE,
+    SC_CLOSE_TIMEOUT, DEFAULT_SAUCE_CONNECT_VERSION
 } from './constants'
 
 export default class SauceLabs {
@@ -200,13 +201,14 @@ export default class SauceLabs {
             }
         }
 
+        const sauceConnectVersion = argv.scVersion || DEFAULT_SAUCE_CONNECT_VERSION
         const { host, basePath } = PROTOCOL_MAP.get('listJobs')
         const restUrl = getSauceEndpoint(host + basePath, this._options.region, this._options.headless) + '/v1'
         const args = Object.entries(argv)
             /**
-             * filter out yargs and yargs params
+             * filter out yargs, yargs params and custom parameters
              */
-            .filter(([k]) => !['_', '$0', ...SC_PARAMS_TO_STRIP].includes(k))
+            .filter(([k]) => !['_', '$0', 'sc-version', ...SC_PARAMS_TO_STRIP].includes(k))
             /**
              * remove duplicate params by yargs
              */
@@ -217,15 +219,15 @@ export default class SauceLabs {
         args.push(`--api-key=${this._accessKey}`)
         args.push(`--rest-url=${restUrl}`)
 
-        const bin = SAUCE_CONNECT_DISTS.reduce((bin, dist) => {
-            bin.src(...dist)
+        const bin = SAUCE_CONNECT_DISTS.reduce((bin, [downloadUrl, ...args]) => {
+            bin.src(util.format(downloadUrl, sauceConnectVersion), ...args)
             return bin
         }, new BinWrapper())
 
         bin
-            .dest(path.join(__dirname, 'bin', 'bin'))
-            .use(process.platform.startsWith('win') ? 'sc.exe' : 'sc')
-            .version(`v${SAUCE_CONNECT_VERSION}`)
+            .dest(path.join(__dirname, `.sc-v${sauceConnectVersion}`))
+            .use('/bin/' + (process.platform.startsWith('win') ? 'sc.exe' : 'sc'))
+            .version(`v${sauceConnectVersion}`)
 
         await bin.run(['--version'])
         const cp = spawn(bin.path(), args)
@@ -245,7 +247,14 @@ export default class SauceLabs {
 
             cp.stderr.on('data', (data) => reject(new Error(data.toString())))
             cp.stdout.on('data', (data) => {
-                if (data.toString().includes(SC_READY_MESSAGE)) {
+                const output = data.toString()
+                /**
+                 * print to stdout if called via CLI
+                 */
+                if (fromCLI) {
+                    process.stdout.write(output)
+                }
+                if (output.includes(SC_READY_MESSAGE)) {
                     return resolve(returnObj)
                 }
             })


Test patch:
diff --git a/tests/__mocks__/bin-wrapper.js b/tests/__mocks__/bin-wrapper.js
index 33c055f..a166d7c 100644
--- a/tests/__mocks__/bin-wrapper.js
+++ b/tests/__mocks__/bin-wrapper.js
@@ -1,3 +1,4 @@
+const instances = []
 class BinWrapperMock {
     constructor () {
         this.run = jest.fn().mockReturnValue(Promise.resolve()),
@@ -6,7 +7,9 @@ class BinWrapperMock {
         this.dest = jest.fn().mockReturnValue(this)
         this.use = jest.fn().mockReturnValue(this)
         this.version = jest.fn().mockReturnValue(this)
+        instances.push(this)
     }
 }
 
 export default BinWrapperMock
+export { instances }
diff --git a/tests/index.test.js b/tests/index.test.js
index 003d170..ab3ab76 100644
--- a/tests/index.test.js
+++ b/tests/index.test.js
@@ -3,6 +3,7 @@ import got from 'got'
 import { spawn } from 'child_process'
 
 import SauceLabs from '../src'
+import { instances } from 'bin-wrapper'
 
 jest.mock('fs')
 const fs = require('fs')
@@ -251,9 +252,17 @@ describe('startSauceConnect', () => {
     it('should start sauce connect with proper parsed args', async () => {
         const api = new SauceLabs({ user: 'foo', key: 'bar', headless: true })
         setTimeout(() => stdoutEmitter.emit('data', 'Sauce Connect is up, you may start your tests'), 50)
-        await api.startSauceConnect({ tunnelIdentifier: 'my-tunnel', 'proxy-tunnel': 'abc' })
+        await api.startSauceConnect({
+            scVersion: '1.2.3',
+            tunnelIdentifier: 'my-tunnel',
+            'proxy-tunnel': 'abc'
+        })
         expect(spawn).toBeCalledTimes(1)
         expect(spawn.mock.calls).toMatchSnapshot()
+
+        expect(instances).toHaveLength(1)
+        expect(instances[0].dest.mock.calls[0][0].endsWith('.sc-v1.2.3'))
+            .toBe(true)
     })
 
     it('should close sauce connect', async () => {
diff --git a/tests/typings/test.ts b/tests/typings/test.ts
index 66fb667..8be6c9f 100644
--- a/tests/typings/test.ts
+++ b/tests/typings/test.ts
@@ -12,7 +12,10 @@ async function foobar () {
     const job = await api.getJobV1_1('foobar')
     console.log(job.selenium_version)
 
-    const sc = await api.startSauceConnect({ tunnelIdentifier: '1234' })
+    const sc = await api.startSauceConnect({
+        scVersion: '4.5.4',
+        tunnelIdentifier: '1234'
+    })
     sc.cp.pid
     await sc.close()