Nightwatch.js Page Objects: Organizing Your E2E Tests
Page objects are the single most impactful pattern you can apply to a growing Nightwatch test suite.
Page objects are the single most impactful pattern you can apply to a growing Nightwatch test suite. Without them, selector strings and interaction sequences get duplicated across dozens of test files. When the UI changes, you're doing a global find-and-replace instead of editing one file.
What a Page Object Is
A page object represents one page (or significant component) of your app. It encapsulates:
- Elements — CSS/XPath selectors, defined once
- Commands — sequences of actions specific to that page (login, fill form, submit)
- Sections — sub-regions of the page with their own element scopes
Your test files consume page objects instead of raw selectors.
Basic Page Object Structure
Nightwatch page objects are defined using module.exports with elements, commands, and optionally sections.
// tests/e2e/page-objects/LoginPage.js
const loginCommands = {
login(email, password) {
return this
.waitForElementVisible('@emailInput')
.clearValue('@emailInput')
.setValue('@emailInput', email)
.clearValue('@passwordInput')
.setValue('@passwordInput', password)
.click('@submitButton');
},
assertErrorMessage(text) {
return this
.assert.visible('@errorMessage')
.assert.textContains('@errorMessage', text);
}
};
module.exports = {
url: 'https://example.com/login',
commands: [loginCommands],
elements: {
emailInput: {
selector: 'input[name="email"]'
},
passwordInput: {
selector: 'input[name="password"]'
},
submitButton: {
selector: 'button[type="submit"]'
},
errorMessage: {
selector: '.alert-error'
},
forgotPasswordLink: {
selector: 'a[href="/forgot-password"]'
}
}
};Elements prefixed with @ in commands refer to the elements object. Nightwatch resolves them automatically.
Registering Page Objects in Config
Tell Nightwatch where to find your page objects:
// nightwatch.conf.js
module.exports = {
src_folders: ['tests/e2e/specs'],
page_objects_path: ['tests/e2e/page-objects'],
// ...
};Multiple paths are supported — useful if you organize by feature area.
Using Page Objects in Tests
// tests/e2e/specs/login.test.js
describe('Login', function () {
let loginPage;
before(function (browser) {
loginPage = browser.page.LoginPage();
});
it('logs in with valid credentials', function (browser) {
loginPage
.navigate()
.login('user@example.com', 'password123');
browser.assert.urlContains('/dashboard');
});
it('shows error for invalid credentials', function (browser) {
loginPage
.navigate()
.login('user@example.com', 'wrongpassword')
.assertErrorMessage('Invalid email or password');
});
it('forgot password link is present', function (browser) {
loginPage
.navigate()
.assert.visible('@forgotPasswordLink');
});
});browser.page.LoginPage() — Nightwatch maps the filename to the accessor. LoginPage.js becomes browser.page.LoginPage().
The navigate() method is built into Nightwatch page objects — it navigates to the url defined in the page object.
Sections
Sections let you scope a subset of a page with its own element selectors. Useful for headers, navbars, modals, or any repeated component.
// tests/e2e/page-objects/DashboardPage.js
module.exports = {
url: 'https://example.com/dashboard',
elements: {
pageTitle: { selector: 'h1.dashboard-title' }
},
sections: {
header: {
selector: 'header.site-header',
elements: {
logo: { selector: '.logo' },
userMenu: { selector: '.user-menu' },
logoutButton: { selector: '[data-action="logout"]' }
},
commands: [{
logout() {
return this
.click('@userMenu')
.waitForElementVisible('@logoutButton')
.click('@logoutButton');
}
}]
},
sidebar: {
selector: 'nav.sidebar',
elements: {
projectsLink: { selector: 'a[href="/projects"]' },
settingsLink: { selector: 'a[href="/settings"]' }
}
}
}
};Access sections in tests:
it('logs out via header', function (browser) {
const dashboard = browser.page.DashboardPage();
const header = dashboard.section.header;
dashboard.navigate();
header.logout();
browser.assert.urlContains('/login');
});XPath Selectors
If CSS is not sufficient, use XPath:
elements: {
submitButton: {
selector: '//button[contains(text(), "Submit Order")]',
locateStrategy: 'xpath'
}
}Inheriting from a Base Page
For shared behavior across pages, create a base page object and require it:
// tests/e2e/page-objects/BasePage.js
const baseCommands = {
waitForPageLoad() {
return this.waitForElementVisible('body', 3000);
},
assertNoErrors() {
return this.assert.not.elementPresent('.error-boundary');
}
};
module.exports = { commands: [baseCommands] };// tests/e2e/page-objects/CheckoutPage.js
const base = require('./BasePage');
const checkoutCommands = {
fillShippingAddress(address) {
return this
.setValue('@streetInput', address.street)
.setValue('@cityInput', address.city)
.setValue('@zipInput', address.zip);
}
};
module.exports = {
url: 'https://example.com/checkout',
commands: [...base.commands, checkoutCommands],
elements: {
streetInput: { selector: '#shipping-street' },
cityInput: { selector: '#shipping-city' },
zipInput: { selector: '#shipping-zip' },
continueButton: { selector: '.btn-continue' }
}
};Folder Structure
For projects with 20+ pages, organize by feature area:
tests/e2e/
page-objects/
auth/
LoginPage.js
RegisterPage.js
dashboard/
DashboardPage.js
ProjectListPage.js
checkout/
CartPage.js
CheckoutPage.js
components/
Header.js
Modal.js
specs/
auth/
login.test.js
checkout/
checkout-happy-path.test.jsRegister the root path and Nightwatch will find nested files:
page_objects_path: ['tests/e2e/page-objects']Access nested page objects with browser.page.auth.LoginPage() or browser.page.checkout.CartPage().
Common Mistakes
Putting too much logic in page objects. Page objects should know how to interact with the page, not how tests should flow. Business logic belongs in the test, not the page object.
Not returning this from commands. Every command in a page object should return this to enable chaining. Break the chain and callers cannot chain further calls.
Duplicating selectors across page objects. If a selector appears in more than one page object, it belongs in a shared component page object.
Once your page objects are solid, adding new tests is fast. A new login test is three lines: navigate, login, assert. The implementation is already there.