Skip to content

Commit

Permalink
support running mocha in parallel mode
Browse files Browse the repository at this point in the history
This library doesn't work as is when mocha is configured
to run parallel jobs. The biggest change is how hooks work;
there are now separate processes coordinated by mocha that have
their own life-cycle. The library can't even be imported
correctly in parallel mode, since it attempts to use `before`
and `after`, and there is now a context in which those are
not defined.

This commit makes it possible to import the library in contexts
where `before` and `after` are not yet defined, and to access the
needed methods for starting up and cleaning up without immediately
invoking them. The user can then install the hooks in the way
that parallel operation requires, see:
  https://mochajs.org/#defining-a-root-hook-plugin

In parallel mode, the before/after hooks for the library will
run at the beginning and end of each test file. This imposes
a cost, since the browser has a non-trivial startup time.
If the user is willing to clean up (or ignore) browser processes,
they can set a flag to skip browser shutdowns at the end of each
test file.

Perhaps someday mocha will have hooks for the lifecycle of worker
processes, and then a cleaner solution will be possible.

Some debugging functionality of mocha-webdriver doesn't make
sense in a multi-process scenario - I haven't touched this.
Debugging will be best done without parallelism, this tweak
is targetted at use in CI workflows.
  • Loading branch information
paulfitz committed May 26, 2023
1 parent 38a732f commit 58045f0
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 7 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ to enable, for `driver.fetchLogs()` and for `enableDebugCapture()`. Defaults to
value.
- `MOCHA_WEBDRIVER_IGNORE_CHROME_VERSION`: Disable chromedriver's check that it supports the installed version of Chrome. Normally the installed chromedriver (controlled by the version in `yarn.lock`) must [match Chrome's version](https://chromedriver.chromium.org/downloads/version-selection). When tests are run by different developers and test environments, that can cause difficulties. On the other hand, incompatible behavior is rare, so this option offers a practical workaround.
- `MOCHA_WEBDRIVER_NO_CONTROL_BANNER`: suppress the "Chrome is being controlled by automated test software" banner. This banner may cause Chrome (as of version 79) to ignore clicks immediately after loading a page.
- `MOCHA_WEBDRIVER_SKIP_CLEANUP`: stops the library from cleaning up the driver, or doing any of the special termination behavior. When running tests in parallel, mocha will call set up and clean up at the test file level, so there may be a speed-up possible by skipping clean up and reusing the driver within individual test worker processes. But you'll need to clean up browser processes yourself.

If running mocha with `--parallel` set, the library won't be automatically
initialized and cleaned up; you'll need to set [root hooks](https://mochajs.org/#defining-a-root-hook-plugin) to the
values given by `getMochaHooks()`.

## Useful methods

Expand Down
71 changes: 64 additions & 7 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,29 @@ export {LogType, logTypes} from './logs';

/**
* Use `import {driver} from 'webdriver-mocha'. Note that it's already enhanced with extra methods
* by "webdriver-plus" module.
* by "webdriver-plus" module. The driver object is a proxy, since
* depending when exactly hooks are called it may not exist yet when
* the library is imported.
*/
export let driver: WebDriver;
export const driver: WebDriver = new Proxy({} as any, {
get(_, prop) {
if (!driver) {
throw new Error('WebDriver accessed before initialization');
}
return (_driver as any)[prop];
}
});
let _driver: WebDriver|undefined;

/**
* Replace the driver. Can be useful for testing purposes.
* Returns the driver being replaced.
*/
export function setDriver(newDriver?: WebDriver): WebDriver|undefined {
const oldDriver = _driver;
_driver = newDriver;
return oldDriver;
}

/**
* To modify webdriver options, call this before mocha's before() hook. Your callback will be
Expand Down Expand Up @@ -184,7 +204,10 @@ export async function createDriver(options: {extraArgs?: string[]} = {}): Promis
}

// Start up the webdriver and serve files that its browser will see.
before(async function() {
export async function beforeMochaWebdriverTests(this: Mocha.Context) {
// If this has already been called, there's nothing to do.
if (this._driver) { return; }

this.timeout(20000); // Set a longer default timeout.

// Add stack trace enhancement (no-op if MOCHA_WEBDRIVER_STACKTRACES isn't set).
Expand All @@ -193,8 +216,8 @@ before(async function() {
// Prepend node_modules/.bin to PATH, for chromedriver/geckodriver to be found.
process.env.PATH = npmRunPath({cwd: __dirname});

driver = await createDriver();
});
setDriver(await createDriver());
}

// Helper to return whether the given suite had any failures.
function suiteFailed(ctx: Mocha.Context): boolean {
Expand All @@ -205,7 +228,9 @@ function suiteFailed(ctx: Mocha.Context): boolean {
}

// Quit the webdriver and stop serving files, unless we failed and --no-exit is given.
after(async function() {
export async function afterMochaWebdriverTests(this: Mocha.Context) {
if (!this._driver) { return; }

this.timeout(6000);
const testParent = this.test!.parent!;
if (suiteFailed(this) && noexit) {
Expand All @@ -219,7 +244,39 @@ after(async function() {
} else {
await cleanup(this);
}
});
this._driver = undefined;
}

// Do not attempt to set the hooks if `before` is not defined, or if
// a MOCHA_WORKER_ID is set (revealing that we are in a parallel job
// managed by mocha). Both these cases arise when using mocha's parallel
// jobs support. When running tests in parallel, hooks need to be set
// using the exports.mochaHooks mechanism.
if (typeof before !== 'undefined' && process.env.MOCHA_WORKER_ID === undefined) {
before(beforeMochaWebdriverTests);
if (!process.env.MOCHA_WEBDRIVER_SKIP_CLEANUP) {
after(afterMochaWebdriverTests);
}
}

// Get mocha hooks to expose as exports.mochaHooks, a newer style of
// setting up hooks. Necessary when running tests in parallel.
// In parallel mode, before/afterAll hooks trigger at the file level, see:
// https://mochajs.org/#available-root-hooks
// That means we are starting and stopping the web driver more than is
// strictly necessary. There aren't any hooks for when individual workers
// start or shut down unfortunately. If you're willing to delete any
// browser instances yourself, or they don't matter (e.g. in CI), you
// can set MOCHA_WEBDRIVER_SKIP_CLEANUP=1 to skip that, so the browser
// instance gets reused for different test files run by a given worker.
export function getMochaHooks() {
return {
beforeAll: beforeMochaWebdriverTests,
...(process.env.MOCHA_WEBDRIVER_SKIP_CLEANUP ? {
afterAll: afterMochaWebdriverTests
} : undefined),
};
}

async function cleanup(context: IMochaContext) {
// Start all cleanup in parallel, so that hangup of driver.quit does not block other cleanup.
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"lint": "tslint -p .",
"prepack": "npm run build && npm run test && npm run lint",
"test": "MOCHA_WEBDRIVER_HEADLESS=1 mocha test",
"test-parallel": "MOCHA_WEBDRIVER_HEADLESS=1 mocha test --parallel --jobs=4",
"test-debug": "mocha test -b --no-exit"
},
"files": [
Expand Down
5 changes: 5 additions & 0 deletions test/init-mocha-webdriver.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,8 @@ if (process.env.MOCHA_WEBDRIVER_IGNORE_CHROME_VERSION === undefined) {
if (!process.env.SELENIUM_BROWSER) {
process.env.SELENIUM_BROWSER = "chrome";
}

if (process.env.MOCHA_WORKER_ID !== undefined) {
const {getMochaHooks} = require('../lib');
exports.mochaHooks = getMochaHooks();
}

0 comments on commit 58045f0

Please sign in to comment.