Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement minimum required plugin functions #3

Merged
merged 6 commits into from
Mar 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 0 additions & 16 deletions .github/SUPPORT.md

This file was deleted.

4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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(browserId: string, 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(browserId, 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';
}
}
147 changes: 121 additions & 26 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,140 @@
export default {
import { SauceDriver } from './driver.js';
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
* 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 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.
// 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.
* Perform any cleanups necessary here.
*
* 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 sauce"` 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 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
*/
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"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: Removed this on a whim. Seems to work so far.

"allowJs": true
},
"include": ["./src/**/*"]
}