Selenium/Explanation/Page object pattern

This page gives more information about page object pattern mentioned at Selenium/Explanation/Stack page.

This tutorial will assume that you are running tests from your machine, targeting MediaWiki-Docker. For more examples see Selenium/Reference/Example Code.

Examples will:

  • create an user via the API
  • log in as the newly created user using the web interface
  • check that the user is logged in

Purpose

edit

There are two main purposes of the page object pattern.

  • Move parts of the application that change a lot (user interface) to a central location. When the user interface changes, the test suite only needs to be updated in one place.
  • Make tests easier to read and understand by moving complexity to page objects.

Let's describe the two purposes with code samples from MediaWiki Core (as of August 2024).

Example

edit

With page object pattern

edit

The code is split into two files. user.js contains the test. createaccount.page.js contains the page object.

Please notice how readable user.js is. You can immediately see that the point of User should be able to log in is to log in as a user with credentials username and password.

tests/selenium/specs/user.js

'use strict';

const assert = require( 'assert' );
const UserLoginPage = require( 'wdio-mediawiki/LoginPage' );
const Api = require( 'wdio-mediawiki/Api' );
const Util = require( 'wdio-mediawiki/Util' );

describe( 'User', () => {
  let password, username, bot;

  before( async () => {
    bot = await Api.bot();
  } );

  beforeEach( async () => {
    await browser.deleteAllCookies();
    username = Util.getTestString( 'User-' );
    password = Util.getTestString();
  } );

  it( 'should be able to log in', async () => {
    // create
    await Api.createAccount( bot, username, password );

    // log in
    await UserLoginPage.login( username, password );

    // check
    const actualUsername = await browser.execute( () => mw.config.get( 'wgUserName' ) );
    assert.strictEqual( await actualUsername, username );
  } );
} );

Please notice that LoginPage.js has two major sections. In the first section are elements on the page (username(), password()...). The second section contains actions (open() and login()).

tests/selenium/wdio-mediawiki/LoginPage.js

'use strict';

const Page = require( './Page' );

class LoginPage extends Page {
  get username() {
    return $( '#wpName1' );
  }

  get password() {
    return $( '#wpPassword1' );
  }

  get loginButton() {
    return $( '#wpLoginAttempt' );
  }

  open() {
    super.openTitle( 'Special:UserLogin' );
  }

  async login( username, password ) {
    await this.open();
    await this.username.setValue( username );
    await this.password.setValue( password );
    await this.loginButton.click();
  }

}

module.exports = new LoginPage();

Without page object pattern

edit

Let's compare that to a solution without page object pattern.

tests/selenium/docs/Page_object_pattern/specs/login.js

'use strict';

const Api = require( 'wdio-mediawiki/Api' );
const Util = require( 'wdio-mediawiki/Util' );

// baseUrl is required for our continuous integration.
// If you don't have MW_SERVER and MW_SCRIPT_PATH environment variables set
// you can probably hardcode it to something like this:
// const baseUrl = 'http://localhost:8080/wiki/';
const baseUrl = `${ process.env.MW_SERVER }${ process.env.MW_SCRIPT_PATH }/index.php?title=`;

describe( 'User', () => {
  let password, username, bot;

  before( async () => {
    bot = await Api.bot();
  } );

  beforeEach( async () => {
    username = Util.getTestString( 'User-' );
    password = Util.getTestString();
  } );

  it( 'should be able to log in without page object', async () => {
    // create
    await Api.createAccount( bot, username, password );

    // log in
    await browser.url( `${ baseUrl }Special:UserLogin` );
    await $( '#wpName1' ).setValue( username );
    await $( '#wpPassword1' ).setValue( password );
    await $( '#wpLoginAttempt' ).click();

    // check
    const actualUsername = await browser.execute( () => mw.config.get( 'wgUserName' ) );
    expect( actualUsername ).toBe( username );
  } );
} );

Conclusion

edit

With page object pattern, the framework is more complex, there are more files (two instead of one) and there is more overall code, but the tests are more readable and more maintainable.

Without page object pattern, the framework is simpler, there are less files (one instead of two) and there is less overall code, but the tests are less readable and less maintainable.

To keep this page short, I've selected a simple example. If the test suite is logging the user in a lot, without page object pattern, code for logging in would have to be copy/pasted, leading to duplication and harder maintenance.

With the page object pattern, logging in is just a call to UserLoginPage.login() function. If any of the elements for logging in change, only LoginPage.js needs to be updated. If the logic of logging in changes (for example, login button is now a div or a span) only UserLoginPage.login() needs to be updated.

More information

edit