Skip to content

Commit

Permalink
update saucelabs provider
Browse files Browse the repository at this point in the history
  • Loading branch information
edoardocavazza committed Oct 18, 2023
1 parent 502d3d5 commit c3a1e00
Show file tree
Hide file tree
Showing 12 changed files with 558 additions and 1,243 deletions.
14 changes: 11 additions & 3 deletions .github/workflows/lint_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ jobs:
file: coverage/clover.xml

test-saucelabs:
name: Test SauceLabs
name: Test Saucelabs
runs-on: ubuntu-latest
concurrency: saucelabs
strategy:
Expand All @@ -119,7 +119,15 @@ jobs:
- 'chrome-latest'
- 'chrome-latest-1'
- 'chrome-latest-2'
- 'chrome-69'
- 'chrome-80'
- 'firefox-latest'
- 'firefox-latest-1'
- 'firefox-latest-2'
- 'firefox-90'
# - 'safari-latest'
# - 'safari-latest-1'
- 'safari-14'
- 'edge-latest'
steps:
- name: Checkout the repository
uses: actions/checkout@v3
Expand All @@ -135,7 +143,7 @@ jobs:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: true

- name: Run tests
run: yarn test:saucelabs --browser.name sauce:${{ matrix.browser }} --coverage
run: yarn test:saucelabs --browser.name remote:${{ matrix.browser }} --coverage
env:
SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }}
SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }}
2 changes: 2 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ jobs:

- name: Install project dependencies
run: yarn install
env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: true

- name: Create Release Pull Request or Publish to npm
id: changesets
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"test": "yarn test:typings && yarn test:browser && yarn test:node",
"test:typings": "tsc -p test/typings --noEmit",
"test:browser": "vitest --config vitest.browser.ts",
"test:browserstack": "vitest --config vitest.browserstack.ts",
"test:saucelabs": "vitest --config vitest.saucelabs.ts",
"test:node": "vitest --config vitest.node.ts",
"lint": "prettier --check . && eslint src test",
Expand All @@ -66,13 +67,12 @@
"eslint-plugin-jsdoc": "^39.3.2",
"htm": "^3.0.3",
"ip": "^1.1.8",
"mocha": "^8.4.0",
"playwright": "^1.38.1",
"prettier": "^3.0.3",
"publint": "^0.2.3",
"rimraf": "^5.0.2",
"rxjs": "^7.8.1",
"saucelabs": "7.2.2",
"saucelabs": "^7.4.0",
"typescript": "^5.0.0",
"vitest": "^0.34.6",
"webdriverio": "^8.17.0"
Expand Down
8 changes: 7 additions & 1 deletion src/CustomElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,14 @@ export type CustomElement<T extends HTMLElement = HTMLElement> = T & {
* @param attributeName The name of the updated attribute.
* @param oldValue The previous value of the attribute.
* @param newValue The new value for the attribute (null if removed).
* @param namespace Attribute namespace.
*/
attributeChangedCallback(attrName: string, oldValue: null | string, newValue: null | string): void;
attributeChangedCallback(
attrName: string,
oldValue: null | string,
newValue: null | string,
namespace?: null | string
): void;
};

/**
Expand Down
7 changes: 6 additions & 1 deletion src/polyfill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,12 @@ function shimConstructor(constructor: CustomElementConstructor & { __shim?: bool
return;
}

element.attributeChangedCallback(attributeName, mutation.oldValue, element.getAttribute(attributeName));
element.attributeChangedCallback(
attributeName,
mutation.oldValue,
element.getAttribute(attributeName),
mutation.attributeNamespace
);
});
});
observer.observe(element, {
Expand Down
4 changes: 2 additions & 2 deletions test/providers/vitest-saucelabs-provider.d.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { SauceLabsOptions } from 'saucelabs';
import type { SauceConnectOptions, SauceLabsOptions } from 'saucelabs';
import type { RemoteOptions } from 'webdriverio';

interface SaucelabsConfig {
options?: Partial<SauceLabsOptions>;
connect?: Partial<SauceLabsOptions>;
connect?: Partial<SauceConnectOptions>;
capabilities?: Record<string, RemoteOptions['capabilities']>;
}

Expand Down
134 changes: 104 additions & 30 deletions test/providers/vitest-saucelabs-provider.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,64 @@
import type { Awaitable } from '@vitest/utils';
import ip from 'ip';
import Saucelabs, { type SauceConnectInstance, type SauceLabsOptions } from 'saucelabs';
import Saucelabs, { type SauceConnectInstance, type SauceConnectOptions, type SauceLabsOptions } from 'saucelabs';
import type { Task } from 'vitest';
import { remote, type Browser, type RemoteOptions } from 'webdriverio';

/**
* Check if a test of the suite has failed.
* @param suite List of tasks.
* @returns Whetever the suite failed or not.
* @see https://github.com/vitest-dev/vitest/blob/main/packages/runner/src/utils/tasks.ts
*/
function hasFailed(suite: Task[]): boolean {
return suite.some((s) => !s.result || s.result.state === 'fail' || (s.type === 'suite' && hasFailed(s.tasks)));
}

/**
* A Saucelabs provider for vitest.
*/
export default class SaucelabsProvider {
name = 'saucelabs';

/**
* Vitest does not exposes WorkspaceProject
* @see https://github.com/vitest-dev/vitest/blob/main/packages/vitest/src/node/workspace.ts
*/
protected ctx: any;

Check warning on line 26 in test/providers/vitest-saucelabs-provider.ts

View workflow job for this annotation

GitHub Actions / lint-test / Lint

Unexpected any. Specify a different type
protected testName: string;
protected saucelabs: Saucelabs;
protected sauceOptions: Partial<SauceLabsOptions>;
protected connectOptions: Partial<SauceConnectOptions>;
protected capabilities: RemoteOptions['capabilities'];
protected tunnelIdentifier: string;

private _browserPromise: Promise<Browser> | null = null;
private _tunnelPromise: Promise<SauceConnectInstance> | null = null;
private _heartbeat: ReturnType<typeof setInterval>;

getSupportedBrowsers() {
return Object.assign([], {
includes: (value: string) => value.startsWith('sauce:'),
includes: (value: string) => value.startsWith('remote:'),
});
}

initialize(ctx, { browser }) {
const saucelabsConfig = ctx.browser.config.saucelabs || {};
initialize({ ctx, config, browser }, { browser: browserName }) {
this.ctx = ctx;
this.testName = config.name;

this.testName = ctx.config.name;
const saucelabsConfig = browser.config.saucelabs || {};
const sauceOptions = (this.sauceOptions = {
user: process.env.SAUCE_USERNAME as string,
key: process.env.SAUCE_ACCESS_KEY as string,
...saucelabsConfig.options,
});
this.capabilities = saucelabsConfig.capabilities[browser.replace('sauce:', '')];
this.connectOptions = {
tunnelIdentifier: `vitest-${Date.now()}`,
noSslBumpDomains: 'all',
...saucelabsConfig.connect,
};
this.capabilities = saucelabsConfig.capabilities[browserName.replace('remote:', '')];
if (!this.capabilities) {
throw new Error(`Missing capabilities for browser name ${browser}`);
throw new Error(`Missing capabilities for browser name ${browserName}`);
}

this.tunnelIdentifier = `vitest-${Date.now()}`;
this.saucelabs = new Saucelabs(sauceOptions);
}

Expand All @@ -47,10 +67,19 @@ export default class SaucelabsProvider {
return this._tunnelPromise;
}

return (this._tunnelPromise = this.saucelabs.startSauceConnect({
tunnelIdentifier: this.tunnelIdentifier,
noSslBumpDomains: 'all',
}));
return (this._tunnelPromise = this.saucelabs.startSauceConnect(this.connectOptions));
}

protected setupHeartbeat(browser: Browser) {
this._heartbeat = setInterval(async () => {
try {
await browser.getTitle();
} catch (e) {
if (this._heartbeat != null) {
clearInterval(this._heartbeat);
}
}
}, 60 * 1000);
}

async openBrowser() {
Expand All @@ -60,19 +89,34 @@ export default class SaucelabsProvider {

return (this._browserPromise = Promise.resolve().then(async () => {
await this.startTunnel();
return remote({

const capabilities =
'version' in this.capabilities
? {
...this.capabilities,
name: this.testName,
build: this.testName,
tunnelIdentifier: this.connectOptions.tunnelIdentifier,
}
: {
...this.capabilities,
'sauce:options': {
name: this.testName,
build: this.testName,
tunnelIdentifier: this.connectOptions.tunnelIdentifier,
},
};

const browser = await remote({
logLevel: 'error',
capabilities: {
...this.capabilities,
'sauce:options': {
name: this.testName,
build: this.testName,
tunnelIdentifier: this.tunnelIdentifier,
},
},
capabilities,
user: this.sauceOptions.user,
key: this.sauceOptions.key,
});

this.setupHeartbeat(browser);

return browser;
}));
}

Expand All @@ -84,16 +128,26 @@ export default class SaucelabsProvider {
}

await browser.navigateTo(url);
const title = await browser.getTitle();
if (title !== 'Vitest Browser Runner') {
throw new Error('Failed to open url');
}
}

// TODO
// @see https://github.com/vitest-dev/vitest/blob/eac7776521bcf4e335771b1ab4f823f40ad9c4ff/packages/vitest/src/node/browser/webdriver.ts#L74
// eslint-disable-next-line @typescript-eslint/no-unused-vars
catchError(_cb: (error: Error) => Awaitable<void>) {
/**
* TODO
* @returns A callback.
* @see https://github.com/vitest-dev/vitest/blob/eac7776521bcf4e335771b1ab4f823f40ad9c4ff/packages/vitest/src/node/browser/webdriver.ts#L74
*/
catchError() {
return () => {};
}

async close() {
if (this._heartbeat != null) {
clearInterval(this._heartbeat);
}

try {
if (this._tunnelPromise) {
const tunnel = await this._tunnelPromise;
Expand All @@ -102,16 +156,36 @@ export default class SaucelabsProvider {
} catch {
//
}

try {
if (this._browserPromise) {
const browser = await this._browserPromise;
await browser.deleteSession();

const { user, key } = this.sauceOptions;
if (user && key) {
const files = this.ctx.state.getFiles();
const passed = !!files.length && !hasFailed(Array.from(files));
await fetch(`https://saucelabs.com/rest/v1/${user}/jobs/${browser.sessionId}`, {
method: 'PUT',
headers: {
'Authorization': `Basic ${Buffer.from(`${user}:${key}`).toString('base64')}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
passed,
}),
});
}
}
} catch {
//
}
// TODO
// @see https://github.com/vitest-dev/vitest/blob/eac7776521bcf4e335771b1ab4f823f40ad9c4ff/packages/vitest/src/node/browser/webdriver.ts#L83

/**
* TODO
* @see https://github.com/vitest-dev/vitest/blob/eac7776521bcf4e335771b1ab4f823f40ad9c4ff/packages/vitest/src/node/browser/webdriver.ts#L83
*/
process.exit();
}
}
Loading

0 comments on commit c3a1e00

Please sign in to comment.