Tips for writing Page Objects and Component Objects

1. Implementing class properties should only return strings

Any logic related to the browser happens inside the spec file. This keeps our spec files readable and the Page / Component Objects simple and predictable.

All implementing classes should only return strings: DashboardPage.menuButton would return '[data-test="menu-button"] (see point 3 below). In the spec file this selector can be used to create a WebdriverIO Element: const menuBtn = await browser.$(DashboardPage.menuButton);.

2. No new methods in implementing classes

Just like the previous point, all logic should happen in the spec file. This means that no methods should be written in implementing classes. Even though it might seem tempting to do (for example to prevent duplication), we value readability of the spec file and predictability of Page / Component Objects more.

3. Use data-attributes as selectors

In a large app refactors are common. It can happen that the structure of your HTML changes and you want the selectors in your Page Objects to be as resilient as possible to changes.

Let’s consider an example where you have a wrapper element: 

. For SEO reasons might want to use a semantic 

// wdio.conf.js
exports.config = {
  specs: ['spec/**/*.spec.ts'],
  framework: 'jasmine',
  jasmineNodeOpts: {
    defaultTimeoutInterval: 60000,
    grep: '#happy-flow',
    invertGrep: false,
  // ... other config
// ./spec/grep-example.spec.ts
describe('various tests', () => {
  it('I will be executed #happy-flow', () => {});
  it('I will be filtered', () => {});

However, even when there is no match on the grep, creates a browser instance. This does not scale with hundreds of spec files. Starting up a new window takes time and if you need to execute preparation tasks (logging in, loading base URL, etc.) it adds up. Additionally, some WebDrivers are slow and, in our case, testing on real devices is even slower.

We needed to optimize. The biggest bottleneck was testing on real devices. We opted for an approach where we would only run #happy-flow tests on mobile. All combinations look like this:

We knew that only a small subset of all spec files would match the #happy-flow tests. A possible approach could be to split up our tests in multiple files (e.g. *.spec.ts*.happy-flow.spec.ts*.mobile.spec.ts), but that would cause bad developer experience and also duplicate expensive actions such as preparation steps, navigating to URL, clean-up, etc.).

We decided to create our own grep script, which reads all possible spec files in memory, executes a regex based on our hashtags and returns an array of filenames that have a spec file that matches our requirements.

// ./wdio.conf.js
const argv = require('yargs').argv;

const { 
   getRegularExpression } = require('./getSpecFiles');

const cmdOpts = {
  'happy-flow': argv.happyFlow,

const SPECS = getFilteredSpecFiles(['spec/**/*.spec.ts'], cmdOpts);

exports.config = {
  specs: argv.spec || SPECS,
  framework: 'jasmine',
  jasmineNodeOpts: {
    defaultTimeoutInterval: 60000,
    grep: getRegularExpression(cmdOpts),
    invertGrep: false,
    isVerbose: false,
// getSpecFiles.js
const glob = require('glob');
const { readFileSync } = require('fs-extra');

function getFilteredSpecFiles(specs, opts = {}) {
  const { glob, grep } = getRegularExpression(opts);
  const fileNames = getGlobMatches(specs);

  const includedSpecFiles = [];

  for (const path of fileNames) {
    const file = readFileSync(path, 'utf-8');

    if (file.match(glob)) {

  return includedSpecFiles;

function getGlobMatches(specs) {
  return specs.reduce((accumulator, spec) => {
    return [...accumulator, ...glob.sync(spec)];
  }, []);

function getRegularExpression({ happyFlow, target }) {
  if (!happyFlow && target === 'desktop') {
    // ❌ #happy-flow and ❌ #mobile
    return {
      glob: /^.*(describe|it)\((?!.*(#happy-flow|#mobile)).*/gm,
      grep: /^(?!.*(#happy-flow|#mobile)).*/,
  } else if (happyFlow && target === 'desktop') {
    // ✅ #happy-flow, ❌ #mobile
    return {
      glob: /^.*(describe|it)\((?!.*(#mobile)).*(#happy-flow).*/gm,
      grep: /^(?!.*(#mobile)).*(#happy-flow).*/,
  } else if (!happyFlow && target === 'mobile') {
    // ❌ #happy-flow, ❌ #desktop, ✅ #mobile
    return {
      glob: /^.*(describe|it)\((?!.*(#happy-flow|#desktop)).*(#mobile).*/gm,
      grep: /^(?!.*(#happy-flow|#desktop)).*(#mobile).*/
  } else if (happyFlow && target === 'mobile') {
    // ✅ #happy-flow, ❌ #desktop, ✅ #mobile
    return {
      glob: /^.*(describe|it)\((?!.*(#desktop)).*(#happy-flow).*/gm,
      grep: /^(?!.*(#desktop)).*(#happy-flow).*/
  } else {
    throw new Error('unforeseen case');

exports.getFilteredSpecFiles = getFilteredSpecFiles;
exports.getRegularExpression = getRegularExpression;

This optimization allowed us to reduce the amount of created browser instances from over two hundred to a couple dozen, while at the same time keeping developer experience great (they can write all their tests in a single spec file).

If you have any comments, potential improvements or completely disagree with this approach, leave a note in the comments, I’d love to hear your thoughts!

Thanks for reading!

Bonus: favorite WDIO plugins ❤

  • Selenium standalone service
    This service sets up a selenium server for you to use without any hassle. It will also provide you with browser drivers.
  • Axe-core
    Best way to test for accessibility issues. Technically not a plug-in, but easily set up using this example
  • wdio-image-comparison-service
    Easy to use, yet powerful way to automatically take screenshots of pages or components and compare them to a baseline
  • Performance
    For performance we use an in-house package called Gonzales.

About the author

Patrick Sevat
Senior Frontend Developer

Patrick is crazy about JS/TS and has the small ambition to transform Rabobank from a bank with an IT department to a tech company with a banking license.

Related articles

Tackling asynchronous integration testing in Senses

  • 16 September 2021
  • 3 min.
By Patrick Sevat

Optimize your git clone / fetch strategy for CI pipelines

  • 15 September 2021
  • 1 min
By Patrick Sevat