Skip to content

Commit

Permalink
feat: implement minimum required plugin functions
Browse files Browse the repository at this point in the history
  • Loading branch information
alexplischke committed Mar 14, 2024
1 parent 1c48897 commit d946391
Show file tree
Hide file tree
Showing 8 changed files with 204 additions and 67 deletions.
16 changes: 0 additions & 16 deletions .github/SUPPORT.md

This file was deleted.

6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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:
Expand All @@ -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();
```

Expand Down
39 changes: 20 additions & 19 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
50 changes: 50 additions & 0 deletions src/driver.ts
Original file line number Diff line number Diff line change
@@ -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<string, Client>();

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);
}
}
19 changes: 19 additions & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
@@ -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';
}
}
135 changes: 109 additions & 26 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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<void> {
// 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<void> {
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<void> {},

/**
* 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<string[]> {
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<boolean> {
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.',
);
},
};
3 changes: 1 addition & 2 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
"extends": "@tsconfig/node20/tsconfig.json",
"compilerOptions": {
"outDir": "./lib",
"allowJs": true,
"target": "es5"
"allowJs": true
},
"include": ["./src/**/*"]
}

0 comments on commit d946391

Please sign in to comment.