Write Mocha style tests using selenium-webdriver, with many conveniences.
The mocha-webdriver
package simplifies creating browser tests in JS or TypeScript using
selenium-webdriver.
npm install --save-dev mocha-webdriver
Writing a browser test:
// fooTest.ts
import {assert, driver} from 'mocha-webdriver';
import * as path from 'path';
describe('fooTest', function() {
before(async function() {
this.timeout(20000);
await driver.get(`file://${path.resolve(__dirname)}/fooTestPage.html`);
});
it('should say Hello World', async function() {
assert.equal(await driver.find('.hello').getText(), 'Hello World!');
});
});
Running the test:
mocha test/fooTest.ts
# To debug a failing test
mocha test/fooTest.ts -b --no-exit
(If mocha is locally installed, run ./node_modules/.bin/mocha
or $(npm bin)/mocha
).
You may select the browser to start SELENIUM_BROWSER=chrome|firefox
environment variable (see selenium
docs
for other variables). Some additional environment variables are also supported:
-
MOCHA_WEBDRIVER_HEADLESS
: start browser in headless mode if set to a non-empty value -
MOCHA_WEBDRIVER_ARGS
: pass the given args to the browser (e.g.--disable-gpu --foo=bar
) -
MOCHA_WEBDRIVER_WINSIZE
: start browser with the given window size, given asWIDTHxHEIGHT
(e.g.900x600
) -
MOCHA_WEBDRIVER_MAX_CALLS
: limit the number of parallel selenium calls to this number, e.g.5
. You can use this to work around an issue in selenium-standalone, causing "Connection reset" errors. -
MOCHA_WEBDRIVER_LOGDIR
: in conjunction with enableDebugCapture, a directory into which to save logs and screenshots automatically after any failed test case. -
MOCHA_WEBDRIVER_LOGTYPES
: comma-separated list of which log types to enable, fordriver.fetchLogs()
and forenableDebugCapture()
. Defaults tobrowser,driver
. (Note: Supported by Chrome, but not Firefox, as of June 2019.) -
MOCHA_WEBDRIVER_STACKTRACES
: Enhance stack traces with async frames, if set to a non-empty 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 inyarn.lock
) must match Chrome's version. 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 to the
values given by getMochaHooks()
.
mocha-webdriver
provides several enhancement to the webdriver interface:
Shorthand to find an element, or a child of an element, by css selector, e.g. .class-name
.
Shorthand to find all elements matching the given css selector. Also available on a WebElement.
If the mapper
argument is given, it is a function applied to all the found elements, and
findAll() returns the results of this function. E.g. findAll('a', (el) => el.getAttribute('href'))
.
Shorthand to wait for an element matching the given selector to be present. Also available on a WebElement.
Find elements matching the given css selector, then return the first one whose innerText matches the given pattern. Accepts both regular expression pattern or plain text pattern. Also available on a WebElement. E.g.
driver.findContent("button", /Accept/);
Note that for performance reasons, it only queries the browser once and searches using Javascript in browser.
Shorthand to wait for an element containing specific innerText matching the given pattern. Accepts both regular expression pattern or plain text pattern. Also available on a WebElement.
Find the closest ancestor of this element that matches the css selector.
Chainable variants of elem.click(), elem.sendKeys(), etc. E.g.
await driver.find('input.my-input').doClear().doSendKeys('hello');
is equivalent to
const elem = await driver.find('input.my-input');
await elem.clear();
await elem.sendKeys('hello');
Shorthand for elem.getAttribute('value')
.
Returns a human-friendly description of this element, for example "button#btn.my-class[some-uuid]"
.
This is particularly useful in the REPL, described below.
Similar to the underlying webdriver's getRect()
, but returns an object that includes properties
{left, right, top, bottom, height, width}
, and whose property rect
contains the original
object from webdriver (with {x, y, height, width}
).
Moves the mouse to the given location in pixels relative to this element. This is a chainable
method. E.g. await driver.find('#btn').mouseMove({x: 100}).doClick()
.
Returns whether this element is the current activeElement. Note that matches(":focus")
may also
be used to the same effect.
Returns whether this element is present in the DOM of the current page.
Returns the 0-based index of this element among its sibling elements.
Returns whether this element matches the given CSS selector. For instance, check if an element has a
class, use matches(".red")
. You can use any selector, e.g. matches(".foo:active > li")
.
Performs "mouseDown" or "mouseUp" action with the given button, Button.LEFT by default.
Moves the mouse by the given offset relative to its current position.
Send keys to the window.
Takes a screenshot, and saves it to MW_SCREENSHOT_DIR/screenshot-{N}.png
if the
MW_SCREENSHOT_DIR
environment variable is set.
-
relPath
may specify a different destination filename, relative toMW_SCREENSHOT_DIR
. -
relPath
may include{N}
token, to replace with "1", "2", etc to find an available name. (WhilerelPath
may includes subdirectories, the{N}
token may only be used in the filename part.) -
dir
may specify a different destination directory. If empty, the screenshot will be skipped.
If called in a mocha test suite (i.e. inside describe()
), adds an afterEach
hook to save logs
and a screenshot after any failed test, only if MOCHA_WEBDRIVER_LOGDIR
variable is set. The
files are named:
MOCHA_WEBDRIVER_LOGDIR/{name}-{logtype}-{N}.log
-
MOCHA_WEBDRIVER_LOGDIR/{name}-screenshot-{N}.png
, wherename
is the basename of the test file, andN
is a numeric suffix.
This is helpful for debugging failing tests. Screenshots are particularly helpful in headless mode. See also #Logging.
Sometimes you need to set options, such as browser preferences, on webdriver creation.
Some can be changed via environment variables such as MOCHA_WEBDRIVER_ARGS
(see above). For
others, you can use setOptionsModifyFunc()
.
Call this before mocha's before()
hook, and modify chromeOpts
and/or firefoxOpts
to
your needs. For example:
setOptionsModifyFunc(({chromeOpts, firefoxOpts}) => {
chromeOpts.setUserPreferences({
download: { default_directory: '/tmp' }
});
firefoxOpts.setPreference('browser.download.dir', '/tmp');
});
For available methods, see
Chrome Options
and Firefox Options.
For available preferences, see Chrome Prefs and about:config
for Firefox.
As with any webdriver tests, your test is just telling a browser what to do. It's up to you to have a page to go to and interact with. Often such a page requires loading some client-side code which is the actual logic to be tested.
If you'd like to build and serve such code on the fly, you have to say how to start and stop such a server, and mocha-webdriver provides a helper to use it, as in the following example.
// fooTest.ts
import {useServer} from 'mocha-webdriver';
import {server} from './myServer';
describe('fooTest', function() {
useServer(server);
enableDebugCapture();
before(async function() {
this.timeout(20000);
await driver.get(`${server.getHost()}/fooTest/`);
});
...
});
//======================================================================
// myServer.ts
import {IMochaContext, IMochaServer} from 'mocha-webdriver';
import * as path from 'path';
import * as serve from 'webpack-serve';
export class MyWebpackServer implements IMochaServer {
private _server: any;
public async start(ctx: IMochaContext) {
ctx.timeout(10000); // Optionally, adjust the timeout.
const config = require(path.resolve(__dirname, 'webpack.config.js'));
this._server = await serve({}, {config});
}
public async stop() {
this._server.app.stop();
}
public getHost(): string {
const {app, options} = this._server;
const {port} = app.server.address();
return `${options.protocol}://${options.host}:${port}`;
}
}
export const server = new MyWebpackServer();
When you are working on a test, you can be more productive by keeping mocha running. Start it with
mocha test/fooTest.ts -b -E
If a test fails (which you can ensure if needed by adding assert(false)
somewhere in the test),
mocha will keep the browser running, and present a node REPL prompt. You may run commands in the
REPL manually, e.g. examine the page with driver.find('.foo')
or interact with it with
driver.find('.foo').click()
.
If you change test code, you can re-run the failing test suite directly from this prompt:
>>> rerun()
This is much faster than starting up the test from scratch.
If you modify some module during debugging in REPL, use the resetModule(moduleName)
helper to
have that module's code reloaded (the failed test's module itself always gets reloaded by
rerun()
). For example:
>>> resetModule('./testUtils');
If you need to use some module in the REPL, you may simply require it and use it. If you commonly
need something, you can add extra context to the REPL using addToRepl(name, value)
helper in any
test suite or at top level. For example:
const fs = require('fs-extra');
describe("foo", () => {
// Make "fs" available in the REPL if any of the "foo" tests fail (and "--no-exit" is used)
addToRepl("fs", fs);
});
If debugging in headless mode, you can use screenshots to see what's happening on the virtual
screen. In REPL, screenshot()
function is available to assist with that:
>>> screenshot() // saves screenshot to "./screenshot-1.png", "./screenshot-2.png", etc.
>>> screenshot("snap.png") // saves screenshot to "./snap.png"
When running automated tests, if any test fails, you want to get enough information to debug it.
To enable the collection of such info, call within your mocha test suite (i.e. inside a call to describe()
):
enableDebugCapture();
In your automated running environment, set also the MOCHA_WEBDRIVER_LOGDIR
environment variable
to the directory to which to store debug info when a test fails.
For each failed test case, you'll get a screenshot of the browser from the moment after the test failed, and the contents of the browser console log and of webdriver log, collected during the run of that test case. The logs only work on Chrome, but will contain each call to the browser, and its return value.
Node does not do a great job with stack traces from async/await code. If your test fails, you will typically get a stack trace like this:
NoSuchElementError: no such element: Unable to locate element: {"method":"css selector","selector":".nonexistent"}
(Session info: headless chrome=74.0.3729.169)
(Driver info: chromedriver=2.43.600229 (3fae4d0cda5334b4f533bede5a4787f7b832d052),platform=Mac OS X 10.13.6 x86_64)
at Object.checkLegacyResponse (/Users/dmitry/devel/mocha-webdriver/node_modules/selenium-webdriver/lib/error.js:585:15)
at parseHttpResponse (/Users/dmitry/devel/mocha-webdriver/node_modules/selenium-webdriver/lib/http.js:533:13)
at Executor.execute (/Users/dmitry/devel/mocha-webdriver/node_modules/selenium-webdriver/lib/http.js:468:26)
at processTicksAndRejections (internal/process/task_queues.js:89:5)
at thenableWebDriverProxy.execute (/Users/dmitry/devel/mocha-webdriver/node_modules/selenium-webdriver/lib/webdriver.js:696:17) {
name: 'NoSuchElementError',
remoteStacktrace: ''
}
Note that it contains no information about which line of your test triggered it. The sample above is from Node 12.2.0, which has better support for async stack traces. Disappointingly, it doesn't help here. In Node 10, this situation occurs in more cases.
At the cost of some overhead (perfectly acceptable in tests), we can do much better.
Set MOCHA_WEBDRIVER_STACKTRACES=1
environment variable, and stack traces start looking like
this:
NoSuchElementError: no such element: Unable to locate element: {"method":"css selector","selector":".nonexistent"}
(Session info: headless chrome=74.0.3729.169)
(Driver info: chromedriver=2.43.600229 (3fae4d0cda5334b4f533bede5a4787f7b832d052),platform=Mac OS X 10.13.6 x86_64)
at Object.checkLegacyResponse (/Users/dmitry/devel/mocha-webdriver/node_modules/selenium-webdriver/lib/error.js:585:15)
at parseHttpResponse (/Users/dmitry/devel/mocha-webdriver/node_modules/selenium-webdriver/lib/http.js:533:13)
at Executor.execute (/Users/dmitry/devel/mocha-webdriver/node_modules/selenium-webdriver/lib/http.js:468:26)
at processTicksAndRejections (internal/process/task_queues.js:89:5)
at thenableWebDriverProxy.execute (/Users/dmitry/devel/mocha-webdriver/node_modules/selenium-webdriver/lib/webdriver.js:696:17)
at [enhanced] thenableWebDriverProxy.findElement (/Users/dmitry/devel/mocha-webdriver/node_modules/selenium-webdriver/lib/webdriver.js:911:17)
at [enhanced] thenableWebDriverProxy.find (/Users/dmitry/devel/mocha-webdriver/lib/webdriver-plus.ts:192:17)
at [enhanced] Object.helperFunc2 (/Users/dmitry/devel/mocha-webdriver/test/helpers.ts:18:18)
at [enhanced] Context.<anonymous> (/Users/dmitry/devel/mocha-webdriver/test/test-stackTraces.ts:75:7)
at [enhanced] Context.<anonymous> (/Users/dmitry/devel/mocha-webdriver/test/test-stackTraces.ts:75:13) {
name: 'NoSuchElementError',
remoteStacktrace: ''
}
Note the lines with the [enhanced]
marker, which contain information about lines in your actual
test files.
When your test code uses helper functions, stack traces may only report the location within the
helper, and not the location of the helper's caller. To get both, you need to wrap the helper with
stackWrapFunc()
. See also test/helpers.ts
for an example of how to wrap an entire module of
helpers.