E2E Test Automation with WebdriverIO

WebdriverIO is one of the most powerful tools for automating web application testing. It controls a real browser, interacts with your app the same way a user would, and tells you exactly what passed and what didn't. In this article, I'll walk you through how I built a WebdriverIO automation suite from the ground up — the structure, the patterns, and the real test code behind it.

the implementation

Project Structure

Here's how the project is organized:

javascript
1automation-teststore-webdriverio/
2 config/
3 production.e2e.conf.js ← separate config for production environment
4 test/
5 helper/
6 generator.js ← random test data generator
7 pageobjects/
8 page.js ← base page (shared logic)
9 homepage.js ← Homepage page object
10 navbar.js ← Navbar page object
11 login.page.js ← Login page object
12 registerPage.js ← Register page object
13 successRegister.js ← Success page object
14 specialspage.js ← Specials page object
15 subnav.js ← Sub navigation page object
16 specs/
17 navbar.spec.js ← Navbar tests
18 loginPage.spec.js ← Login tests
19 registerPage.spec.js ← Register tests
20 wdio.conf.js ← Main configuration file
21 package.json
21 lines

The separation is intentional. Page objects hold the selectors and actions. Spec files hold the test scenarios. They never mix.

Reusable Navbar Component

The Configuration File

Everything starts with wdio.conf.js. This file tells WebdriverIO how to run tests — which browser to use, which files to run, how long to wait, and where to send the report.

javascript
1const browserName = process.env.BROWSER || "firefox";
2
3export const config = {
4 runner: "local",
5 framework: "mocha",
6 maxInstances: 10,
7 waitforTimeout: 10000,
8
9 capabilities: [{
10 browserName: browserName,
11 acceptInsecureCerts: true,
12 }],
13
14 reporters: [
15 "spec",
16 ["allure", { outputDir: "allure-results" }],
17 ],
18};
18 lines

A few things worth noting here:

Browser is configurable via environment variable. You don't hardcode the browser. Instead, you pass it at runtime:

BROWSER=chrome npx wdio run ./wdio.conf.js

No config change needed to switch browsers.

Test suites are grouped. Instead of running all tests every time, you can run just the suite you need:

javascript
1suites: {
2 login: ["test/specs/loginPage.spec.js"],
3 register: ["test/specs/registerPage.spec.js"],
4 e2e: [
5 "test/specs/navbar.spec.js",
6 "test/specs/loginPage.spec.js",
7 "test/specs/registerPage.spec.js",
8 ],
9},
9 lines

Run them with:

javascript
1npm run login # runs only login tests
2npm run register # runs only register tests
3npm run e2e # runs the full suite
3 lines

Allure is wired in. Every test run automatically generates a detailed report in allure-results/ that you can open in a browser — showing pass/fail per test, execution time, and error screenshots.

The Page Object Pattern

The most important design decision in this project is the Page Object Model (POM). The idea is simple: every page in the app gets its own class. That class holds all the element selectors and all the actions you can take on that page.

01.

Base Page

javascript
1export default class Page {
2 open(path) {
3 return browser.url(`https://automationteststore.com/${path}`)
4 }
5}
5 lines

Every page object extends this base class. So opening any page is just one line.

02.

Login Page Object

javascript
1get loginName() {
2 return $("#loginFrm_loginname");
3 }
4
5 get password() {
6 return $("#loginFrm_password");
7 }
8
9 get loginBtn() {
10 return $('button[title="Login"]');
11 }
12
13 get alertErrorMsg() {
14 return $(".alert-error");
15 }
16
17 async login(loginName, password) {
18 await this.loginName.waitForDisplayed();
19 await this.loginName.setValue(loginName);
20 await this.password.setValue(password);
21 await this.loginBtn.click();
22 }
23}
24
25export default new LoginPage();
25 lines

Notice the login() method — it wraps the entire login flow into one reusable action. In any test that needs a logged-in user, you just call:

await loginPage.login("Cahya123", "Cahya123");

One line. No duplicated steps.

03.

Register Page Object

The Register page object is larger because the registration form has many fields — first name, last name, email, phone, address, city, region, zip code, country, login name, password, confirm password, and privacy policy checkbox. Each field has its own getter:

javascript
1get firstName() { return $("#AccountFrm_firstname"); }
2get email() { return $("#AccountFrm_email"); }
3get loginName() { return $("#AccountFrm_loginname"); }
4get password() { return $("#AccountFrm_password"); }
5get privacyPolicyCheckbox() { return $("#AccountFrm_agree"); }
5 lines

And each validation message has its own getter too:

javascript
1get minCharFirstnameMsg() {
2 return $('//span[contains(text(), "First Name must be between 1 and 32 characters!")]');
3}
4
5get duplicateLoginNameMsg() {
6 return $('//span[contains(text(), "This login name is not available.")]');
7}
7 lines

This means in the test, you never write raw selectors. You always write human-readable property names.

The Test Data Generator

One of the trickiest problems in registration tests is uniqueness. If every test uses the same login name, the second test will always fail with "duplicate login name." The solution is a random data generator:

javascript
1import { animals, uniqueNamesGenerator } from "unique-names-generator";
2
3function generateUniqueName() {
4 let randomName = uniqueNamesGenerator({
5 dictionaries: [animals],
6 length: 1,
7 });
8 while (randomName.length < 10) {
9 randomName += uniqueNamesGenerator({ dictionaries: [animals], length: 1 });
10 }
11 return randomName.substring(0, 10);
12}
13
14function generatePassword() {
15 return uniqueNamesGenerator({ dictionaries: [animals], length: 1 });
16}
17
18function generatePhonenumber() {
19 return parseInt(Math.random().toFixed(6).replace("0.", ""));
20}
20 lines

Every time the registration test runs, it generates a unique name, a unique email, a unique phone number, and a unique login name. Tests never collide with each other — or with existing data in the system.

The Test Specs

01.

Navbar Tests

javascript
1beforeEach(async () => {
2 await browser.maximizeWindow();
3 await homepage.open();
4 });
5
6 afterEach(async () => {
7 await browser.deleteCookies();
8 await browser.refresh();
9 });
10
11 it("should display the logo", async () => {
12 await navbar.logo.waitForDisplayed();
13 expect(navbar.logo).toBeDisplayed();
14 });
15
16 it("navigate to homepage on clicking logo button", async () => {
17 await navbar.logo.click();
18 await expect(browser).toHaveUrl("https://automationteststore.com/");
19 });
20
21 it("dropdown is appear on hovering over account button", async () => {
22 await navbar.accountBtn.moveTo();
23 await expect(navbar.accountDropdownMenu).toBeDisplayed();
24 });
25});
25 lines

beforeEach runs before every test — maximizes the window and opens the homepage fresh. afterEach clears cookies and refreshes — so every test starts clean, with no leftover session data.

02.

Login Tests

javascript
1it("Should be able to show message when login with invalid credential", async () => {
2 await loginPage.loginName.setValue("1231231");
3 await loginPage.password.setValue("123123");
4 await loginPage.loginBtn.click();
5 await expect(loginPage.alertErrorMsg).toBeDisplayed();
6 });
7
8 it("Should be able to login", async () => {
9 await loginPage.login("Cahya123", "Cahya123");
10 });
11});
11 lines

Two scenarios — invalid credentials show an error, valid credentials succeed. Clean, readable, and maintainable.

03.

Register Tests — Validation Coverage

The register spec is the most comprehensive. It tests every single validation rule the form enforces:

TestWhat it verifies
First name emptyError message appears
First name > 32 charactersError message appears
Last name emptyError message appears
Email emptyError message appears
Address < 3 charactersError message appears
Address > 128 charactersError message appears
City < 3 charactersError message appears
City > 128 charactersError message appears
Region not selectedError message appears
ZIP code emptyError message appears
Login name emptyError message appears
Login name < 5 charactersError message appears
Login name > 64 charactersError message appears
Duplicate login name"Not available" message appears
Password < 4 charactersError message appears
Password > 20 charactersError message appears
Password maskedInput type is password
Password ≠ confirm passwordAlert error appears
Privacy policy not checkedAlert error appears
All valid dataRedirects to success page

The Allure Report

After every run, results are saved to allure-results/. You can generate and open the visual report with:

javascript
1npx allure generate allure-results --clean
2npx allure open
2 lines

The report shows every test by name, pass/fail status, execution duration, and — on failure — the exact error and the line where it happened.

What This Suite Covers

AreaCoverage
Navbar — element visibility
Navbar — all navigation links
Navbar — hover dropdown
Login — valid credentials
Login — invalid credentials
Register — all field validations
Register — duplicate login name
Register — password security
Register — full happy path
Session cleanup between tests
Random test data — no collisions
Multi-browser support
Allure reporting

A WebdriverIO suite built with the Page Object Model is clean, maintainable, and readable by anyone on the team. Selectors live in page objects. Test logic lives in spec files. Random data generators keep tests independent. And Allure reports make results easy to share with clients and stakeholders. When a bug appears, you know exactly which test caught it, what the expected behavior was, and what the app actually did instead.

the testing stack

Every tool chosen with purpose — from feature to assertion.

WebdriverIO

WebdriverIO

for browser automation and element interaction

Jenkins

Jenkins

runs the load tests automatically as part of the CI/CD pipeline

Allure Report

Allure Report

to visualize test results in a clean, shareable report

Available for work

|

I'd love to hear about it — let's see if we're a good fit.

cahyaputraugira95@gmail.com