๐Ÿ—๏ธ POM Basics + Exports + Login Page Setup

"Building Maintainable Test Automation with Page Object Model"

Duration: 1 Hour | Theme: Design Patterns & Code Organization

๐Ÿ•’ Teaching Flow

๐ŸŽญ Story / Intro (5 min)

๐Ÿ‘‰ "Imagine you're testing PrestaShop. If we put all steps (login, search, checkout) in one script โ†’ it's messy, unscalable. Instead, we create **Page Objects** โ†’ each page gets its own reusable class. This is like having mini-testers inside your code, each responsible for one page."

1. ๏ฟฝ๏ฟฝ๏ธ POM Basics (20 min)

๐Ÿ’ก What is POM?

Page Object Model is a design pattern โ†’ separates locators & actions for each page into one file.

Improves readability, reusability, maintainability.

// Without POM (messy code) import { test, expect } from '@playwright/test'; test('Login without POM', async ({ page }) => { await page.goto('https://learn-playwright.great-site.net/'); await page.locator('#email').fill('qa@example.com'); await page.locator('#passwd').fill('password123'); await page.locator('#SubmitLogin').click(); await expect(page.locator('.account')).toContainText('QA Tester'); });
// With POM (clean code) // pages/LoginPage.js export class LoginPage { constructor(page) { this.page = page; this.emailField = page.locator('#email'); this.passwordField = page.locator('#passwd'); this.loginBtn = page.locator('#SubmitLogin'); } async goto() { await this.page.goto('https://learn-playwright.great-site.net/'); } async login(email, password) { await this.emailField.fill(email); await this.passwordField.fill(password); await this.loginBtn.click(); } }
// tests/login.spec.js import { test, expect } from '@playwright/test'; import { LoginPage } from '../pages/LoginPage'; test('Login with POM', async ({ page }) => { const loginPage = new LoginPage(page); await loginPage.goto(); await loginPage.login('qa@example.com', 'password123'); await expect(page.locator('.account')).toContainText('QA Tester'); });
๐ŸŽฏ Takeaway:

Without POM โ†’ hard to maintain.

With POM โ†’ clean, reusable, structured.

๐Ÿ’ก POM Benefits

  • Maintainability: Update locators in one place
  • Reusability: Use same page class across multiple tests
  • Readability: Tests become self-documenting
  • Scalability: Easy to add new pages and functionality
  • Team Collaboration: Different team members can work on different pages

2. ๐Ÿ“ฆ Basics of Module Exports (15 min)

๐Ÿ’ก Why exports?

To reuse classes/functions across multiple files.

Keeps code DRY (Don't Repeat Yourself).

// utils/helpers.js export function generateRandomEmail() { return `user_${Date.now()}@test.com`; } export function generateRandomPassword() { return `pass_${Math.random().toString(36).substring(2, 8)}`; } export const TEST_DATA = { validEmail: 'qa@example.com', validPassword: 'password123' };
// tests/register.spec.js import { test } from '@playwright/test'; import { generateRandomEmail, generateRandomPassword, TEST_DATA } from '../utils/helpers'; test('Register user', async ({ page }) => { const email = generateRandomEmail(); const password = generateRandomPassword(); await page.goto('https://learn-playwright.great-site.net/'); await page.fill('#email_create', email); await page.fill('#passwd', password); // Use predefined test data console.log('Using test data:', TEST_DATA); });
๐ŸŽฏ Takeaway:

Exports = packaging functions/classes โ†’ reuse everywhere.

Think of it like "common tools" that every test can borrow.

๐Ÿ’ก Export Types

  • Named Exports: `export function name()` - import with `{ name }`
  • Default Exports: `export default class` - import without braces
  • Multiple Exports: Export several items from one file
  • Constants: Export configuration and test data

3. ๐Ÿงช Showing Login Page (Demo + Discussion) (20 min)

๐ŸŽฏ Login Page Implementation

Use LoginPage class built above.

Demonstrate login success + failure case.

// Enhanced LoginPage with error handling export class LoginPage { constructor(page) { this.page = page; this.emailField = page.locator('#email'); this.passwordField = page.locator('#passwd'); this.loginBtn = page.locator('#SubmitLogin'); this.errorMessage = page.locator('.alert-danger'); this.successMessage = page.locator('.account'); } async goto() { await this.page.goto('https://learn-playwright.great-site.net/'); } async login(email, password) { await this.emailField.fill(email); await this.passwordField.fill(password); await this.loginBtn.click(); } async loginInvalid(email, password) { await this.login(email, password); await expect(this.errorMessage).toBeVisible(); } async assertLoginSuccess() { await expect(this.successMessage).toBeVisible(); } }
// Test cases using enhanced LoginPage import { test, expect } from '@playwright/test'; import { LoginPage } from '../pages/LoginPage'; test('Login success case', async ({ page }) => { const loginPage = new LoginPage(page); await loginPage.goto(); await loginPage.login('qa@example.com', 'password123'); await loginPage.assertLoginSuccess(); }); test('Login failure case', async ({ page }) => { const loginPage = new LoginPage(page); await loginPage.goto(); await loginPage.loginInvalid('qa@example.com', 'wrongpassword'); // Error message assertion is handled inside loginInvalid method });
๐ŸŽฏ Real-time QA Angle:

Manual testers already test login flows โ†’ now they'll see how it looks in automation, cleanly via POM.

๐Ÿ’ก Login Flow Benefits

  • Success Case: Login with valid credentials โ†’ dashboard visible
  • Failure Case: Login with wrong password โ†’ assert error message
  • Reusability: Same LoginPage class used across multiple test scenarios
  • Maintainability: If login form changes, update only LoginPage class

๐Ÿง‘โ€๐Ÿ’ป Interactive Questions

๐Ÿ’ก Conceptual Questions

  1. Why is POM better than writing locators directly inside test scripts?
  2. How do `export` and `import` help in POM structure?
  3. If the login button locator changes โ†’ how many files do you need to edit in:
    • Without POM?
    • With POM?

๐Ÿ’ก Code Snippet Questions

๐Ÿ‘‰ What will this output?

// utils.js export function add(a, b) { return a + b; } // test.js import { add } from './utils.js'; console.log(add(2, '3'));

Expected: "23" because JS doesn't enforce number types, unlike TypeScript.

๐ŸŽฏ Answers:
  1. POM Benefits: Better maintainability, reusability, readability, and scalability
  2. Export/Import: Enables code reuse across files, keeps code DRY, organizes functionality
  3. File Updates: Without POM: update every test file; With POM: update only the page class

๐ŸŽฏ Learning Outcomes

๐Ÿ“‘ Practice Tasks

๐ŸŸข Beginner

  1. Create a HomePage class with goto() method.
  2. Add reusable method to click "Sign In" link.

๐ŸŸก Intermediate

  1. Extend LoginPage with loginInvalid() method โ†’ assert error message.
  2. Create a helpers.js with generateRandomPassword() โ†’ import in test.

๐Ÿ”ด Advanced

  1. Create ProductPage class with method addToCart(productName).
  2. Write test that logs in using LoginPage โ†’ adds product using ProductPage.

๐ŸŽฏ Additional Interview Questions & Answers

๐Ÿ’ก Q1: What are the main differences between POM and Page Factory patterns?

๐ŸŽฏ Answer:

Key differences:

  • POM: Simple class-based approach, each page gets its own class
  • Page Factory: Uses factory pattern to create page objects, more complex but flexible
  • Implementation: POM is easier to implement and understand
  • Scalability: Page Factory handles complex page hierarchies better
  • Learning Curve: POM is better for beginners, Page Factory for advanced users

๐Ÿ’ก Q2: How do you handle dynamic elements in POM?

๐ŸŽฏ Answer:

Dynamic element strategies:

  • Dynamic Locators: Use functions that generate locators based on parameters
  • Wait Strategies: Implement explicit waits for dynamic content
  • Conditional Logic: Check element state before interacting
  • Retry Mechanisms: Implement retry logic for flaky elements
// Example: Dynamic element handling async getProductByName(productName) { return this.page.locator(`[data-product-name="${productName}"]`); } async waitForProductToLoad(productName) { await this.page.waitForSelector(`[data-product-name="${productName}"]`); }

๐Ÿ’ก Q3: What's the difference between `export default` and named exports?

๐ŸŽฏ Answer:

Export types comparison:

Named Export Default Export
`export class LoginPage` `export default class LoginPage`
`import { LoginPage } from './LoginPage'` `import LoginPage from './LoginPage'`
Multiple exports per file Only one default export per file

๐Ÿ’ก Q4: How do you handle test data management in POM?

๐ŸŽฏ Answer:

Test data strategies:

  • Data Files: Store test data in JSON, CSV, or Excel files
  • Helper Functions: Generate dynamic test data (emails, passwords)
  • Environment Variables: Use different data for different environments
  • Data Builders: Create objects with default values that can be overridden
  • Fixtures: Use Playwright fixtures for reusable test data
// Example: Test data management export class TestDataBuilder { static buildUser(overrides = {}) { return { email: 'qa@example.com', password: 'password123', firstName: 'QA', lastName: 'Tester', ...overrides }; } } // Usage in test const user = TestDataBuilder.buildUser({ email: 'custom@example.com' });

๐Ÿ’ก Q5: What are the best practices for organizing POM files?

๐ŸŽฏ Answer:

File organization best practices:

๐Ÿ“ project/ โ”œโ”€โ”€ ๐Ÿ“ pages/ โ”‚ โ”œโ”€โ”€ ๐Ÿ“„ LoginPage.js โ”‚ โ”œโ”€โ”€ ๐Ÿ“„ HomePage.js โ”‚ โ”œโ”€โ”€ ๐Ÿ“„ ProductPage.js โ”‚ โ””โ”€โ”€ ๐Ÿ“„ CheckoutPage.js โ”œโ”€โ”€ ๐Ÿ“ utils/ โ”‚ โ”œโ”€โ”€ ๐Ÿ“„ helpers.js โ”‚ โ”œโ”€โ”€ ๐Ÿ“„ testData.js โ”‚ โ””โ”€โ”€ ๐Ÿ“„ constants.js โ”œโ”€โ”€ ๐Ÿ“ tests/ โ”‚ โ”œโ”€โ”€ ๐Ÿ“„ login.spec.js โ”‚ โ”œโ”€โ”€ ๐Ÿ“„ product.spec.js โ”‚ โ””โ”€โ”€ ๐Ÿ“„ checkout.spec.js โ””โ”€โ”€ ๐Ÿ“„ playwright.config.js
  • Consistent Naming: Use descriptive names ending with 'Page'
  • Single Responsibility: Each page class handles one page
  • Common Base Class: Create BasePage for shared functionality
  • Utility Separation: Keep helper functions separate from page classes

๐Ÿ’ก Q6: How do you handle page navigation and URL assertions in POM?

๐ŸŽฏ Answer:

Navigation and URL handling:

  • URL Constants: Store URLs in constants file
  • Navigation Methods: Implement goto() methods in each page class
  • URL Assertions: Verify correct page after navigation
  • Relative Navigation: Use relative paths when possible
// Example: URL management export const URLs = { HOME: 'https://learn-playwright.great-site.net/', LOGIN: 'https://learn-playwright.great-site.net/login', PRODUCTS: 'https://learn-playwright.great-site.net/products' }; export class BasePage { constructor(page) { this.page = page; } async goto(url) { await this.page.goto(url); } async assertCurrentUrl(expectedUrl) { await expect(this.page).toHaveURL(expectedUrl); } }

๐Ÿ’ก Q7: What's the difference between POM and Appium's Page Object Model?

๐ŸŽฏ Answer:

Key differences:

  • Platform: POM in Playwright is for web automation, Appium for mobile
  • Locators: Playwright uses web selectors, Appium uses mobile-specific locators
  • Actions: Playwright has web-specific actions, Appium has mobile gestures
  • Implementation: Core concept is the same, but implementation details differ
  • Tools: Playwright has built-in POM support, Appium requires additional setup

๐Ÿ’ก Q8: How do you implement inheritance in POM?

๐ŸŽฏ Answer:

Inheritance strategies:

  • Base Page Class: Create common functionality in BasePage
  • Common Methods: Implement shared methods like waitForPageLoad, takeScreenshot
  • Constructor Chaining: Use super() to call parent constructor
  • Method Overriding: Override methods in child classes when needed
// Example: Inheritance in POM export class BasePage { constructor(page) { this.page = page; } async waitForPageLoad() { await this.page.waitForLoadState('networkidle'); } async takeScreenshot(name) { await this.page.screenshot({ path: `screenshots/${name}.png` }); } } export class LoginPage extends BasePage { constructor(page) { super(page); this.emailField = page.locator('#email'); this.passwordField = page.locator('#passwd'); } async login(email, password) { await this.emailField.fill(email); await this.passwordField.fill(password); await this.page.locator('#SubmitLogin').click(); await this.waitForPageLoad(); // Inherited method } }