From d946391739e24eef2b34be7d31b03ea89a7a809d Mon Sep 17 00:00:00 2001 From: Alex Plischke Date: Thu, 14 Mar 2024 15:52:12 -0700 Subject: [PATCH 1/6] feat: implement minimum required plugin functions --- .github/SUPPORT.md | 16 ------ README.md | 6 +- package-lock.json | 39 ++++++------- package.json | 3 +- src/driver.ts | 50 +++++++++++++++++ src/errors.ts | 19 +++++++ src/index.ts | 135 ++++++++++++++++++++++++++++++++++++--------- tsconfig.json | 3 +- 8 files changed, 204 insertions(+), 67 deletions(-) delete mode 100644 .github/SUPPORT.md create mode 100644 src/driver.ts create mode 100644 src/errors.ts diff --git a/.github/SUPPORT.md b/.github/SUPPORT.md deleted file mode 100644 index 6da7770..0000000 --- a/.github/SUPPORT.md +++ /dev/null @@ -1,16 +0,0 @@ -# Getting help with _projectname_ - -Thanks for using _projectname_. Before filing an issue, there are a few places -to explore and pieces to put together to make the process as smooth as possible. - -First, let's figure out where to ask: - - * If it's a question on a Sauce Labs specific platform feature, send an email to [support@saucelabs.com](support@saucelabs.com). - * If it's a question on using this specific project, file an issue. - * If it's a bug: you're in the right place, please also file an issue. - -Before opening a new issue, be sure to [search issues and pull requests](https://github.com/saucelabs/_projectname_/issues) to make sure the bug hasn't been reported and/or already fixed in the development version. By default, the search will be pre-populated with `is:issue is:open`. You can [edit the qualifiers](https://help.github.com/articles/searching-issues-and-pull-requests/) (e.g. `is:pr`, `is:closed`) as needed. For example, you'd simply remove `is:open` to search _all_ issues in the repo, open or closed. - -The maintainer team is committed to respond to your request within a timeframe of _projectSLATime_ hrs. - -Thanks for your help! \ No newline at end of file diff --git a/README.md b/README.md index 5b45e2d..cc65d02 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # testcafe-browser-provider-sauce -This is the official Sauce Labs browser provider plugin for [TestCafe](http://devexpress.github.io/testcafe). +This is the official SauceDriver Labs browser provider plugin for [TestCafe](http://devexpress.github.io/testcafe). ## Install @@ -19,7 +19,7 @@ testcafe -b sauce When you run tests from the command line, use the alias when specifying browsers: ``` -testcafe sauce:browser1 'path/to/test/file.js' +testcafe sauce:chrome 'path/to/test/file.js' ``` When you use API, pass the alias to the `browsers()` method: @@ -28,7 +28,7 @@ When you use API, pass the alias to the `browsers()` method: testCafe .createRunner() .src('path/to/test/file.js') - .browsers('sauce:browser1') + .browsers('sauce:chrome') .run(); ``` diff --git a/package-lock.json b/package-lock.json index 6a1a10b..3865638 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,8 @@ "version": "0.0.1", "license": "MIT", "dependencies": { - "saucelabs-connector": "2.0.0" + "saucelabs-connector": "2.0.0", + "webdriver": "7.33.0" }, "devDependencies": { "@eslint/js": "8.57.0", @@ -2623,9 +2624,9 @@ } }, "node_modules/@wdio/config/node_modules/@types/node": { - "version": "18.19.17", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.17.tgz", - "integrity": "sha512-SzyGKgwPzuWp2SHhlpXKzCX0pIOfcI4V2eF37nNBJOhwlegQ83omtVQ1XxZpDE06V/d6AQvfQdPfnw0tRC//Ng==", + "version": "18.19.24", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.24.tgz", + "integrity": "sha512-eghAz3gnbQbvnHqB+mgB2ZR3aH6RhdEmHGS48BnV75KceQPHqabkxKI0BbUSsqhqy2Ddhc2xD/VAR9ySZd57Lw==", "dependencies": { "undici-types": "~5.26.4" } @@ -2737,9 +2738,9 @@ } }, "node_modules/@wdio/utils/node_modules/@types/node": { - "version": "18.19.17", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.17.tgz", - "integrity": "sha512-SzyGKgwPzuWp2SHhlpXKzCX0pIOfcI4V2eF37nNBJOhwlegQ83omtVQ1XxZpDE06V/d6AQvfQdPfnw0tRC//Ng==", + "version": "18.19.24", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.24.tgz", + "integrity": "sha512-eghAz3gnbQbvnHqB+mgB2ZR3aH6RhdEmHGS48BnV75KceQPHqabkxKI0BbUSsqhqy2Ddhc2xD/VAR9ySZd57Lw==", "dependencies": { "undici-types": "~5.26.4" } @@ -7721,9 +7722,9 @@ } }, "node_modules/webdriver/node_modules/@types/node": { - "version": "18.19.17", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.17.tgz", - "integrity": "sha512-SzyGKgwPzuWp2SHhlpXKzCX0pIOfcI4V2eF37nNBJOhwlegQ83omtVQ1XxZpDE06V/d6AQvfQdPfnw0tRC//Ng==", + "version": "18.19.24", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.24.tgz", + "integrity": "sha512-eghAz3gnbQbvnHqB+mgB2ZR3aH6RhdEmHGS48BnV75KceQPHqabkxKI0BbUSsqhqy2Ddhc2xD/VAR9ySZd57Lw==", "dependencies": { "undici-types": "~5.26.4" } @@ -9788,9 +9789,9 @@ }, "dependencies": { "@types/node": { - "version": "18.19.17", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.17.tgz", - "integrity": "sha512-SzyGKgwPzuWp2SHhlpXKzCX0pIOfcI4V2eF37nNBJOhwlegQ83omtVQ1XxZpDE06V/d6AQvfQdPfnw0tRC//Ng==", + "version": "18.19.24", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.24.tgz", + "integrity": "sha512-eghAz3gnbQbvnHqB+mgB2ZR3aH6RhdEmHGS48BnV75KceQPHqabkxKI0BbUSsqhqy2Ddhc2xD/VAR9ySZd57Lw==", "requires": { "undici-types": "~5.26.4" } @@ -9868,9 +9869,9 @@ }, "dependencies": { "@types/node": { - "version": "18.19.17", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.17.tgz", - "integrity": "sha512-SzyGKgwPzuWp2SHhlpXKzCX0pIOfcI4V2eF37nNBJOhwlegQ83omtVQ1XxZpDE06V/d6AQvfQdPfnw0tRC//Ng==", + "version": "18.19.24", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.24.tgz", + "integrity": "sha512-eghAz3gnbQbvnHqB+mgB2ZR3aH6RhdEmHGS48BnV75KceQPHqabkxKI0BbUSsqhqy2Ddhc2xD/VAR9ySZd57Lw==", "requires": { "undici-types": "~5.26.4" } @@ -13572,9 +13573,9 @@ }, "dependencies": { "@types/node": { - "version": "18.19.17", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.17.tgz", - "integrity": "sha512-SzyGKgwPzuWp2SHhlpXKzCX0pIOfcI4V2eF37nNBJOhwlegQ83omtVQ1XxZpDE06V/d6AQvfQdPfnw0tRC//Ng==", + "version": "18.19.24", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.24.tgz", + "integrity": "sha512-eghAz3gnbQbvnHqB+mgB2ZR3aH6RhdEmHGS48BnV75KceQPHqabkxKI0BbUSsqhqy2Ddhc2xD/VAR9ySZd57Lw==", "requires": { "undici-types": "~5.26.4" } diff --git a/package.json b/package.json index 4ff6bb4..86f0c1a 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,8 @@ "typescript-eslint": "7.2.0" }, "dependencies": { - "saucelabs-connector": "2.0.0" + "saucelabs-connector": "2.0.0", + "webdriver": "7.33.0" }, "peerDependencies": { "testcafe": "^2.0.0" diff --git a/src/driver.ts b/src/driver.ts new file mode 100644 index 0000000..2bf05bd --- /dev/null +++ b/src/driver.ts @@ -0,0 +1,50 @@ +import WebDriver, { Client } from 'webdriver'; + +export class SauceDriver { + private readonly username: string; + private readonly accessKey: string; + private readonly tunnelName: string; + private sessions = new Map(); + + constructor(username: string, accessKey: string, tunnelName: string) { + this.username = username; + this.accessKey = accessKey; + this.tunnelName = tunnelName; + } + + async openBrowser(url: string, browserName: string) { + const webDriver = await WebDriver.newSession({ + protocol: 'https', + hostname: `ondemand.saucelabs.com`, // TODO multi region support + port: 443, + user: this.username, + key: this.accessKey, + capabilities: { + name: 'testcafe sauce provider job', // TODO make this configurable + browserName: browserName, + buildName: 'TCPRVDR', // TODO make this configurable + tunnelIdentifier: this.tunnelName, + idleTimeout: 3600, // 1 hour + }, + logLevel: 'error', + connectionRetryTimeout: 9 * 60 * 1000, // 9 minutes + connectionRetryCount: 3, + path: '/wd/hub', + }); + this.sessions.set(browserName, webDriver); + + // TODO do we need a keep-alive? + + await webDriver.navigateTo(url); + + return { + jobUrl: `https://app.saucelabs.com/tests/${webDriver.sessionId}`, + webDriver: webDriver, + }; + } + + async closeBrowser(browserId: string) { + await this.sessions.get(browserId)?.deleteSession(); + this.sessions.delete(browserId); + } +} diff --git a/src/errors.ts b/src/errors.ts new file mode 100644 index 0000000..b39edd3 --- /dev/null +++ b/src/errors.ts @@ -0,0 +1,19 @@ +export class AuthError extends Error { + constructor() { + super( + 'Authentication failed. Please assign the correct username and access key ' + + 'to the SAUCE_USERNAME and SAUCE_ACCESS_KEY environment variables.', + ); + this.name = 'AuthError'; + } +} + +export class TunnelNameError extends Error { + constructor() { + super( + 'The SAUCE_TUNNEL_NAME environment variable is not set. Please start a ' + + 'tunnel first and set the SAUCE_TUNNEL_NAME environment variable.', + ); + this.name = 'TunnelNameError'; + } +} diff --git a/src/index.ts b/src/index.ts index 25e1de6..b1c67e1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,45 +1,128 @@ -export default { +import { SauceDriver } from './driver.js'; +import { AuthError, TunnelNameError } from './errors'; + +let sauceDriver: SauceDriver; + +module.exports = { + /** + * Indicates whether the browser provider supports multiple browsers. + * + * https://github.com/DevExpress/testcafe/blob/4a30f1c3b8769ca68c9b7912911f1dd8aa91d62c/src/browser/provider/index.ts#L65 + */ isMultiBrowser: true, - // Required - must be implemented - // Browser control - async openBrowser(/* id, pageUrl, browserName */) { - throw new Error('Not implemented!'); - }, + /** + * Called by TestCafe to initialize the browser provider. + * + * https://github.com/DevExpress/testcafe/blob/4a30f1c3b8769ca68c9b7912911f1dd8aa91d62c/src/browser/provider/plugin-host.js#L81 + */ + async init(): Promise { + const username = process.env.SAUCE_USERNAME; + const accessKey = process.env.SAUCE_ACCESS_KEY; + const tunnelName = process.env.SAUCE_TUNNEL_NAME; + + if (!username || !accessKey) { + throw new AuthError(); + } + if (!tunnelName) { + throw new TunnelNameError(); + } - async closeBrowser(/* id */) { - throw new Error('Not implemented!'); + sauceDriver = new SauceDriver(username, accessKey, tunnelName); }, - // Optional - implement methods you need, remove other methods - // Initialization - async init() { - return; + /** + * Called by TestCafe to open a browser. + * + * https://github.com/DevExpress/testcafe/blob/4a30f1c3b8769ca68c9b7912911f1dd8aa91d62c/src/browser/provider/plugin-host.js#L72 + * + * @param browserId + * @param url + * @param browserName + */ + async openBrowser( + browserId: string, + url: string, + browserName: string, + ): Promise { + // TODO check available concurrency and wait if necessary + + // TODO check tunnel status and wait if necessary + // See https://docs.saucelabs.com/secure-connections/sauce-connect-5/operation/readiness-checks/. + + console.log('Starting browser on SauceDriver Labs...'); + const { jobUrl } = await sauceDriver.openBrowser(url, browserName); + console.log('Browser started.'); + + // Pass the job URL to TestCafe, which it will append to the test report. + // Output: + // Running tests in: + // - Chrome 122.0.0.0 / Windows 10 (https://app.saucelabs.com/tests/8545f0fb12a24da290af1f6b87dcc530) + this.setUserAgentMetaInfo(browserId, jobUrl); }, - async dispose() { - return; + /** + * Called by TestCafe to close a browser. + * + * https://github.com/DevExpress/testcafe/blob/4a30f1c3b8769ca68c9b7912911f1dd8aa91d62c/src/browser/provider/plugin-host.js#L76 + * + * @param browserId + */ + async closeBrowser(browserId: string): Promise { + await sauceDriver.closeBrowser(browserId); }, - // Browser names handling - async getBrowserList() { - throw new Error('Not implemented!'); + /** + * Called by TestCafe at the end of the test run. + * Performs any cleanups necessary. + * + * https://github.com/DevExpress/testcafe/blob/4a30f1c3b8769ca68c9b7912911f1dd8aa91d62c/src/browser/provider/plugin-host.js#L85 + */ + async dispose(): Promise {}, + + /** + * Called by TestCafe to get the list of available browsers. + * + * E.g. `"testcafe -b sauceDriver"` will call this method to print the available + * browsers. + * + * https://github.com/DevExpress/testcafe/blob/4a30f1c3b8769ca68c9b7912911f1dd8aa91d62c/src/browser/provider/plugin-host.js#L91 + */ + async getBrowserList(): Promise { + return ['chrome']; }, - async isValidBrowserName(/* browserName */) { - return true; + /** + * Called by TestCafe to verify if the user specified browser is valid. + * + * E.g. `"testcafe -b sauceDriver:chrome:latest"` will call this method to verify. + * + * https://github.com/DevExpress/testcafe/blob/4a30f1c3b8769ca68c9b7912911f1dd8aa91d62c/src/browser/provider/plugin-host.js#L95 + * @param browserName + */ + async isValidBrowserName(browserName: string): Promise { + return (await this.getBrowserList()).includes(browserName); }, - // Extra methods + /** + * Called by TestCafe to resize the browser window. + * + * https://github.com/DevExpress/testcafe/blob/master/src/browser/provider/plugin-host.js#L126 + */ async resizeWindow(/* id, width, height, currentWidth, currentHeight */) { - // this.reportWarning( - // 'The window resize functionality is not supported by the "saucelabs-official" browser provider.', - // ); + this.reportWarning( + 'The window resize functionality is not supported by the Sauce Labs browser provider plugin.', + ); }, + /** + * Called by TestCafe to take a screenshot. + * + * https://github.com/DevExpress/testcafe/blob/4a30f1c3b8769ca68c9b7912911f1dd8aa91d62c/src/browser/provider/plugin-host.js#L134 + */ async takeScreenshot(/* id, screenshotPath, pageWidth, pageHeight */) { - // this.reportWarning( - // 'The screenshot functionality is not supported by the "saucelabs-official" browser provider.', - // ); + this.reportWarning( + 'The screenshot functionality is not supported by the Sauce Labs browser provider plugin.', + ); }, }; diff --git a/tsconfig.json b/tsconfig.json index d015002..872f70a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,8 +2,7 @@ "extends": "@tsconfig/node20/tsconfig.json", "compilerOptions": { "outDir": "./lib", - "allowJs": true, - "target": "es5" + "allowJs": true }, "include": ["./src/**/*"] } From 12c84225af91f9b095b617ac0b3fe1b9be24de14 Mon Sep 17 00:00:00 2001 From: Alex Plischke Date: Thu, 14 Mar 2024 15:55:52 -0700 Subject: [PATCH 2/6] grep shenanigans --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cc65d02..d62a1da 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # testcafe-browser-provider-sauce -This is the official SauceDriver Labs browser provider plugin for [TestCafe](http://devexpress.github.io/testcafe). +This is the official Sauce Labs browser provider plugin for [TestCafe](http://devexpress.github.io/testcafe). ## Install From 588d073ccae15f1a4720d81ed52cb0f325c33869 Mon Sep 17 00:00:00 2001 From: Alex Plischke Date: Thu, 14 Mar 2024 17:17:21 -0700 Subject: [PATCH 3/6] better wording --- src/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index b1c67e1..98cd6e7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,7 +5,8 @@ let sauceDriver: SauceDriver; module.exports = { /** - * Indicates whether the browser provider supports multiple browsers. + * Inspected by TestCafe to check whether the browser provider supports + * multiple browsers. * * https://github.com/DevExpress/testcafe/blob/4a30f1c3b8769ca68c9b7912911f1dd8aa91d62c/src/browser/provider/index.ts#L65 */ From 4a6d1253e404861ef2488e19aeee9b7a8ea95075 Mon Sep 17 00:00:00 2001 From: Alex Plischke Date: Thu, 14 Mar 2024 17:20:34 -0700 Subject: [PATCH 4/6] add cautionary note --- src/index.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/index.ts b/src/index.ts index 98cd6e7..404d1fb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,13 @@ import { AuthError, TunnelNameError } from './errors'; let sauceDriver: SauceDriver; +/** + * The Sauce Labs browser provider plugin for TestCafe. + * + * CAUTION: Do not export the `default` keyword, as TestCafe will not be able to + * load the plugin. Neither does it support the `exports =` syntax without + * `module`. + */ module.exports = { /** * Inspected by TestCafe to check whether the browser provider supports From 02018b4e8bfca77985c64bc2da48e2ee55288366 Mon Sep 17 00:00:00 2001 From: Alex Plischke Date: Fri, 15 Mar 2024 10:14:49 -0700 Subject: [PATCH 5/6] fix: not closing browser at the end of the run --- src/driver.ts | 4 ++-- src/index.ts | 12 ++++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/driver.ts b/src/driver.ts index 2bf05bd..fb32caf 100644 --- a/src/driver.ts +++ b/src/driver.ts @@ -12,7 +12,7 @@ export class SauceDriver { this.tunnelName = tunnelName; } - async openBrowser(url: string, browserName: string) { + async openBrowser(browserId: string, url: string, browserName: string) { const webDriver = await WebDriver.newSession({ protocol: 'https', hostname: `ondemand.saucelabs.com`, // TODO multi region support @@ -31,7 +31,7 @@ export class SauceDriver { connectionRetryCount: 3, path: '/wd/hub', }); - this.sessions.set(browserName, webDriver); + this.sessions.set(browserId, webDriver); // TODO do we need a keep-alive? diff --git a/src/index.ts b/src/index.ts index 404d1fb..18e20d6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -58,8 +58,12 @@ module.exports = { // TODO check tunnel status and wait if necessary // See https://docs.saucelabs.com/secure-connections/sauce-connect-5/operation/readiness-checks/. - console.log('Starting browser on SauceDriver Labs...'); - const { jobUrl } = await sauceDriver.openBrowser(url, browserName); + console.log('Starting browser on Sauce Labs...'); + const { jobUrl } = await sauceDriver.openBrowser( + browserId, + url, + browserName, + ); console.log('Browser started.'); // Pass the job URL to TestCafe, which it will append to the test report. @@ -91,7 +95,7 @@ module.exports = { /** * Called by TestCafe to get the list of available browsers. * - * E.g. `"testcafe -b sauceDriver"` will call this method to print the available + * E.g. `"testcafe -b sauce"` will call this method to print the available * browsers. * * https://github.com/DevExpress/testcafe/blob/4a30f1c3b8769ca68c9b7912911f1dd8aa91d62c/src/browser/provider/plugin-host.js#L91 @@ -103,7 +107,7 @@ module.exports = { /** * Called by TestCafe to verify if the user specified browser is valid. * - * E.g. `"testcafe -b sauceDriver:chrome:latest"` will call this method to verify. + * E.g. `"testcafe -b sauce:chrome@latest"` will call this method to verify. * * https://github.com/DevExpress/testcafe/blob/4a30f1c3b8769ca68c9b7912911f1dd8aa91d62c/src/browser/provider/plugin-host.js#L95 * @param browserName From 9c7f594e93964dad139595b42de8c953e870ded9 Mon Sep 17 00:00:00 2001 From: Alex Plischke Date: Fri, 15 Mar 2024 10:18:55 -0700 Subject: [PATCH 6/6] docs --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 18e20d6..6fe63dc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -86,7 +86,7 @@ module.exports = { /** * Called by TestCafe at the end of the test run. - * Performs any cleanups necessary. + * Perform any cleanups necessary here. * * https://github.com/DevExpress/testcafe/blob/4a30f1c3b8769ca68c9b7912911f1dd8aa91d62c/src/browser/provider/plugin-host.js#L85 */