- Use page objects
- Avoid protocol methods and prefer commands
- Avoid timeouts
- Avoid caching elements
- Avoid arbitrary pause() calls
- Use mocks
- Use waitFor, waitForVisible etc (TODO)
- Other gotchas
- Shadow DOM
The page-object pattern is a useful abstraction to separate the ugly implementation details from the assertions your tests need to make. Page objects are a lot like a service layer. They're responsible for querying the DOM for elements, filling out forms, waiting for things, etc. This gives your test specs a clean api to work with, and can stay focused on making assertions. Page objects are also useful in that they can be reused in multiple tests.
Protocol commands bypass nifty features of webdriverio (TODO: like what?). They're also painful to look at. If you must use them, do it in a page object. example:
bad:
it('clicks the element', () => {
const el = browser.$('button');
browser.elementIdClick(el.value.ELEMENT);
expect(browser.elementIdText(el.value.ELEMENT).value).toBe('clicked');
});
better:
it('clicks the element', () => {
pageObject.button.click();
expect(pageObject.button.getText()).toBe('clicked');
});
Unless something typically takes LONGER than the default timeout (currently, 30s), avoid using timeouts. It won't make your tests any faster, but will allow them to fail falsely more often. If you must use a timeout, try to do it in a page-object instead of your .spec files.
Caching elements leads to stale element references. It also dirties up your test code with utility methods. Instead, use getters in page objects so when you interact with an element, you're always interacting with an "as up-to-date as possible" element.
bad:
function getButton() {
return browser.$('button');
}
//.. later
it('button is enabled', () => {
expect(button.isEnabled()).toBe(true);
})
better:
// pageObject.js
get button() {
return browser.$('button');
}
// test.spec.js
it('button is enabled', () => {
expect(pageObject.button.isEnabled()).toBe(true);
});
Avoid calling browser.pause(randomNumber)
in your test code. Pause is useful in that it's very easy to use, but it's also confusing when used in .spec files. Instead, wrap it in a page-object method like so:
// pageObject.js
waitForDialog() {
browser.pause(DIALOG_ANIMATION_TIME);
}
// test.spec.js
it('should close the dialog', () => {
page.someButton.click();
page.waitForDialog();
// assert something
})
By using it this way, the pause timeout value is localized in your page object and the waitForDialog
method name gives hints as to what is going on.
Mocks are incredibly important. Mocking API responses gives you full control over timing. Without this control, you give up a lot of stability in your tests and are at the mercy of network latency. Testing against live API's is important but that should be reserved for "live" or "user" tests.
Jasmine (currently) will swallow errors in beforeAll and afterAll and afterEach. Wrap contents of those methods in try catch.
bad:
beforeAll(() => {
pageObject.open();
});
better:
beforeAll(() => {
try {
pageObject.open();
} catch(e) {
console.error('error in beforeAll', e);
throw e;
}
});
The Safari webdriver doesn't work with element.isDisplayed()
. This is being worked on. (TODO: is there a work-around?)
It seems that running tests remotely takes about twice as long as running them locally. This is not including startup times. When deciding on timeout values, figure out the average local timeout, and double it.
Known issues:
Firefox: mozilla/geckodriver#1414 attempting to call setValue('foo')
on an input inside shadow DOM fails with an error. The workaround is to set the value manually via a browser.execute(fn..)
call.