
Why Page Object Model Matters for Appium
After maintaining test automation frameworks for years, I have learned one fundamental truth: without proper architecture, your test suite becomes unmaintainable. The Page Object Model (POM) is not just a design pattern—it is the foundation of professional mobile test automation.
Core Principles of POM
1. Separation of Concerns
- Page classes: Encapsulate element locators and page operations
- Test classes: Focus purely on test logic and assertions
- Utilities: Handle common operations like waits and gestures
2. Single Responsibility
Each page object should represent one logical screen or component:
- LoginPage handles login functionality
- HomePage handles navigation and main features
- SearchPage handles search operations
Base Page Class Implementation
// base.page.js
class BasePage {
constructor(driver) {
this.driver = driver;
this.timeout = 30000;
}
async waitForElement(locator) {
const element = await this.driver.$(locator);
await element.waitForDisplayed({ timeout: this.timeout });
return element;
}
async click(locator) {
const element = await this.waitForElement(locator);
await element.click();
}
async type(locator, text) {
const element = await this.waitForElement(locator);
await element.setValue(text);
}
async getText(locator) {
const element = await this.waitForElement(locator);
return await element.getText();
}
async isDisplayed(locator) {
try {
const element = await this.driver.$(locator);
return await element.isDisplayed();
} catch (error) {
return false;
}
}
async swipeUp() {
const { width, height } = await this.driver.getWindowSize();
await this.driver.execute('mobile: swipeGesture', {
left: width / 2,
top: height * 0.8,
width: 0,
height: height * 0.4,
direction: 'up',
percent: 0.75
});
}
}
Screen-Specific Page Object
// login.page.js
import BasePage from './base.page';
class LoginPage extends BasePage {
// Locators
get usernameField() { return '~username_input'; }
get passwordField() { return '~password_input'; }
get loginButton() { return '~login_button'; }
get errorMessage() { return '~error_message'; }
get forgotPasswordLink() { return '~forgot_password'; }
// Actions
async enterUsername(username) {
await this.type(this.usernameField, username);
}
async enterPassword(password) {
await this.type(this.passwordField, password);
}
async tapLogin() {
await this.click(this.loginButton);
}
async login(username, password) {
await this.enterUsername(username);
await this.enterPassword(password);
await this.tapLogin();
}
// Verifications
async getErrorText() {
return await this.getText(this.errorMessage);
}
async isErrorDisplayed() {
return await this.isDisplayed(this.errorMessage);
}
}
export default new LoginPage();
Test Class Using Page Objects
// login.test.js
import { expect } from 'chai';
import LoginPage from '../pages/login.page';
import HomePage from '../pages/home.page';
describe('Login Functionality', () => {
beforeEach(async () => {
await driver.launchApp();
});
it('should login with valid credentials', async () => {
await LoginPage.login('testuser', 'validpassword');
expect(await HomePage.isWelcomeDisplayed()).to.be.true;
});
it('should show error for invalid credentials', async () => {
await LoginPage.login('invalid', 'wrong');
expect(await LoginPage.isErrorDisplayed()).to.be.true;
expect(await LoginPage.getErrorText()).to.include('Invalid');
});
it('should navigate to forgot password', async () => {
await LoginPage.click(LoginPage.forgotPasswordLink);
expect(await ForgotPasswordPage.isPageDisplayed()).to.be.true;
});
});
Component-Based Page Objects
// components/navigation-bar.component.js
class NavigationBar {
constructor(driver) {
this.driver = driver;
}
get homeTab() { return '~tab_home'; }
get searchTab() { return '~tab_search'; }
get profileTab() { return '~tab_profile'; }
async navigateToHome() {
const element = await this.driver.$(this.homeTab);
await element.click();
}
async navigateToSearch() {
const element = await this.driver.$(this.searchTab);
await element.click();
}
async navigateToProfile() {
const element = await this.driver.$(this.profileTab);
await element.click();
}
}
// home.page.js
import BasePage from './base.page';
import NavigationBar from './components/navigation-bar.component';
class HomePage extends BasePage {
constructor(driver) {
super(driver);
this.navBar = new NavigationBar(driver);
}
}
Best Practices Summary
- Use descriptive locators with accessibility IDs when possible
- Keep page objects focused on single screens or components
- Create reusable components for shared UI elements
- Return page objects from navigation methods for fluent APIs
- Use getter functions for lazy loading of elements
- Implement proper wait strategies in base class
Tags:TechnologyTutorialGuide
X
Written by XQA Team
Our team of experts delivers insights on technology, business, and design. We are dedicated to helping you build better products and scale your business.
•