A sample test automation project demonstrating:
- UI validations with WebdriverIO (browser automation)
- API validations with PactumJS (REST API testing)
Both frameworks run independently against the public demo site the-internet.herokuapp.com, so the suite works out of the box with no backend setup.
| Layer | Framework | Target |
|---|---|---|
| UI | WebdriverIO | https://the-internet.herokuapp.com/login |
| API | PactumJS | https://the-internet.herokuapp.com (HTTP layer) |
.
├── package.json # scripts + dependencies
├── wdio.conf.js # WebdriverIO config (UI runner)
├── config/
│ └── constants.js # shared BASE_URL, timeouts, credentials
├── constants/
│ └── waits/
│ └── constant_waits.js # named wait durations
├── data/
│ └── contact.data.js # test-data builders (externalized fixtures)
├── locators/ # selectors only ({ selector } objects, no behaviour)
│ ├── login/login.locator.js
│ ├── secure/secure.locator.js
│ ├── admin/admin.locator.js
│ └── contact/contact.locator.js
├── utils/
│ ├── wdio.js # wdioHelper: wraps all WebdriverIO interactions
│ ├── booking-api.js # PactumJS client (room integration test)
│ └── contact-api.js # PactumJS client (contact integration test)
└── test/
├── pageobjects/ # Page Object Model: behaviour, talks to wdioHelper
│ ├── page.js # base page
│ ├── login.page.js
│ ├── secure.page.js
│ ├── admin.page.js
│ └── contact.page.js
├── specs/ # UI / integration specs (run by WebdriverIO)
│ ├── login.e2e.js
│ ├── room.integration.e2e.js # API + UI integration (rooms)
│ └── contact.integration.e2e.js # API + UI integration (contacts)
└── api/ # API-only specs (run by Mocha + PactumJS)
├── status-codes.spec.js
└── auth-and-redirects.spec.js
The two layers are kept separate on purpose:
- WebdriverIO has its own test runner (
wdio) and only picks uptest/specs/**. - PactumJS is a plain library, so its specs run under Mocha and live in
test/api/**.
The project is CommonJS and the UI side follows a layered convention:
- locators/ — selectors only, as
{ selector }objects under a flatelementsmap per page file, grouped by section comments (elements.USERNAME). - utils/wdio.js —
wdioHelper, a thin wrapper over WebdriverIO (setValue,clickElement,getText,waitForElementVisible,isDisplayed,createDynamicLocator). Page objects never call$()orbrowserdirectly. This is a local stand-in for the internalbstackautomation-helpers/wdiopackage. - pageobjects/ — behaviour-only classes exported as singletons
(
module.exports.LoginPage = new LoginPage()), driving the helper with locators.
- Node.js (v18.20+ recommended) and npm
- Google Chrome installed (WebdriverIO auto-manages the matching driver)
npm installShared settings live in config/constants.js — the base URL, default API timeout, and demo credentials. Both the WebdriverIO config and the PactumJS specs import from there, so there is a single source of truth.
Point the suite at a different environment without touching code via the
BASE_URL environment variable:
BASE_URL=https://staging.example.com npm test# API tests only (PactumJS via Mocha)
npm run test:api
# UI tests only (WebdriverIO) — runs headless Chrome by default
npm run test:ui
# Everything (API then UI)
npm testTo watch the browser during UI runs, comment out the --headless=new
line in the goog:chromeOptions.args array in wdio.conf.js.
- Successful login with valid credentials
- Rejection of an invalid username
- Rejection of an invalid password
Built with the Page Object Model — selectors and actions live in
test/pageobjects/, keeping specs readable and maintainable. Each test runs in
a fresh browser session (browser.reloadSession() in beforeEach) so the
tests stay independent and reliable against the free demo site.
status-codes.spec.js— landing page,/status_codes/200|404|500, and unknown-path 404 handlingauth-and-redirects.spec.js— HTTP Basic Auth (401 vs 200), redirect handling without auto-follow, and form login viaPOST /authenticate
These showcase common PactumJS features: expectStatus, expectHeader /
expectHeaderContains, expectBodyContains, withAuth, withForm, and
withFollowRedirects.
A single end-to-end flow against restful-booker-platform (an app with an admin API and an admin UI over the same data):
- API generates test data —
POST /api/auth/loginfor a token, thenPOST /api/roomto create a uniquely named room. A follow-upGET /api/roomfinds the new room and captures its id. (API chaining: token → create → list → id.) - UI validates it — log into the admin panel and assert the room appears in the room list.
- API cleans up —
DELETE /api/room/{id}and confirm the room is gone. Cleanup runs in anafterhook, so it happens even if the UI step fails.
The API calls are encapsulated in utils/booking-api.js (PactumJS), and the UI uses the admin.page.js page object. PactumJS runs happily inside the WebdriverIO/Mocha spec, which is what lets a single test span both layers.
The same pattern against the Contact List App:
- API generates test data —
POST /usersregisters a throwaway user (returns a token), thenPOST /contactscreates a contact and returns its id. (API chaining: register → token → add contact → id.) - UI validates it — log in with the new user and assert the contact appears in the contact list.
- API cleans up —
DELETE /contacts/{id}thenDELETE /users/me.
API calls live in utils/contact-api.js; the UI uses contact.page.js. Each run uses a unique email, so the test is self-contained and repeatable.