Dynamic Order History Validation with POM + Test Structure

Master structured testing with test.describe, test.step, and dynamic order validation

🕒 Teaching Flow

1. Story / Intro (5 min)

👉 "Yesterday we placed an order & got an OrderID. But as testers, we must confirm it actually appears in Order History. Today, we'll dynamically search the OrderID in history using loops & conditions, while writing structured tests with describe, test, and test.step."
Validation Analogy:
Like a detective checking if a case file exists in the filing cabinet. We place an order (create a case), get a case number (OrderID), then search through the filing system (Order History) to confirm it's properly recorded.

Validation Flow Overview

🔄 Complete Order Validation Journey

Place order and capture OrderID
Navigate to Order History page
Search for OrderID in history table
Validate order details and status
Verify order appears in correct position
Test negative scenarios (fake OrderID)

🎬 Test Structure Benefits

  • test.describe: Groups related tests for better organization
  • test: Defines individual test scenarios
  • test.step: Breaks tests into readable, debuggable steps
  • Better Reporting: Clear trace reports with step-by-step execution
  • Maintainability: Easier to understand and modify tests

POM: OrderHistoryPage.js (15 min)

// pages/OrderHistoryPage.js
export class OrderHistoryPage {
  constructor(page) {
    this.page = page;
    this.orderHistoryLink = page.getByRole("link", { name: "Order history" });
    this.ordersTable = page.locator("table.order-history tbody tr");
    this.orderReference = page.locator('.order-reference');
    this.orderDate = page.locator('.order-date');
    this.orderTotal = page.locator('.order-total');
    this.orderStatus = page.locator('.order-status');
    this.noOrdersMessage = page.getByText("You have not placed any orders yet");
  }

  async goto() {
    await this.orderHistoryLink.click();
    await this.page.waitForLoadState('networkidle');
    await this.page.waitForSelector('table.order-history tbody tr', { timeout: 10000 });
  }

  async findOrder(orderId) {
    await this.ordersTable.first().waitFor({ timeout: 5000 });
    const rows = await this.ordersTable.allTextContents();
    return rows.some(row => row.includes(orderId));
  }

  async findOrderWithLoop(orderId) {
    await this.ordersTable.first().waitFor({ timeout: 5000 });
    const rows = this.ordersTable;
    const rowCount = await rows.count();
    
    for (let i = 0; i < rowCount; i++) {
      const rowOrderId = await rows.nth(i).locator("th").textContent();
      if (orderId.includes(rowOrderId)) {
        await rows.nth(i).locator("button").first().click();
        break;
      }
    }
    
    const orderIdDetails = await this.page.locator(".col-text").textContent();
    const isOrderFound = orderId.includes(orderIdDetails);
    expect(isOrderFound).toBeTruthy();
    return isOrderFound;
  }

  async getAllOrders() {
    await this.ordersTable.first().waitFor({ timeout: 5000 });
    const orders = await this.ordersTable.all();
    return await Promise.all(orders.map(async (row) => ({
      orderId: await row.locator('.order-reference').textContent(),
      date: await row.locator('.order-date').textContent(),
      total: await row.locator('.order-total').textContent(),
      status: await row.locator('.order-status').textContent()
    })));
  }

  async getOrderStatus(orderId) {
    const orderRow = this.page.locator('tr').filter({ hasText: orderId });
    await orderRow.waitFor({ timeout: 5000 });
    const isVisible = await orderRow.isVisible();
    if (isVisible) {
      return await orderRow.locator('.order-status').textContent();
    }
    return null;
  }

  async getOrderDetails(orderId) {
    const orderRow = this.page.locator('tr').filter({ hasText: orderId });
    await orderRow.waitFor({ timeout: 5000 });
    const isVisible = await orderRow.isVisible();
    if (isVisible) {
      return {
        orderId: await orderRow.locator('.order-reference').textContent(),
        date: await orderRow.locator('.order-date').textContent(),
        total: await orderRow.locator('.order-total').textContent(),
        status: await orderRow.locator('.order-status').textContent()
      };
    }
    return null;
  }

  async getOrderCount() {
    await this.ordersTable.first().waitFor({ timeout: 5000 });
    return await this.ordersTable.count();
  }

  async hasOrders() {
    const count = await this.getOrderCount();
    return count > 0;
  }

  async getLatestOrder() {
    const orders = await this.getAllOrders();
    return orders.length > 0 ? orders[0] : null;
  }

  async searchOrderByDate(dateRange) {
    const orders = await this.getAllOrders();
    return orders.filter(order => {
      const orderDate = new Date(order.date);
      return orderDate >= dateRange.start && orderDate <= dateRange.end;
    });
  }
}
Note: allTextContents() fetches all rows. We check if any row includes the orderId. This approach is resilient to UI changes and works with dynamic content.

Using test.describe, test, and test.step (30 min)

// tests/e2e-order-history.spec.js
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage.js';
import { HomePage } from '../pages/HomePage.js';
import { ProductPage } from '../pages/ProductPage.js';
import { CartPage } from '../pages/CartPage.js';
import { CheckoutPage } from '../pages/CheckoutPage.js';
import { OrderHistoryPage } from '../pages/OrderHistoryPage.js';
import fs from 'fs';

test.describe('E2E Order Placement & Validation', () => {
  
  test.beforeEach(async ({ page }) => {
    // Each test starts with a fresh page
    await page.goto('https://learn-playwright.great-site.net/');
  });

  test('Place order and validate in Order History', async ({ page }) => {
    // Initialize POMs
    const loginPage = new LoginPage(page);
    const homePage = new HomePage(page);
    const productPage = new ProductPage(page);
    const cartPage = new CartPage(page);
    const checkoutPage = new CheckoutPage(page);
    const orderHistoryPage = new OrderHistoryPage(page);

    let orderId;

    await test.step('Login and go to homepage', async () => {
      await loginPage.goto();
      await page.waitForLoadState('networkidle');
      await expect(page).toHaveURL(/.*learn-playwright\.great-site\.net/);
      const isLoggedIn = await loginPage.isLoggedIn();
      expect(isLoggedIn).toBeTruthy();
    });

    await test.step('Search and add product to cart', async () => {
      await homePage.searchProduct("Dress");
      await page.waitForLoadState('networkidle');
      await expect(page).toHaveURL(/.*search/);
      await productPage.addToCart("Printed Dress");
      await page.waitForSelector('.cart-products-count', { timeout: 10000 });
      const cartCount = await page.locator('.cart-products-count').textContent();
      expect(cartCount).toBeTruthy();
    });

    await test.step('Proceed to checkout & place order', async () => {
      await cartPage.proceedToCheckout();
      await page.waitForLoadState('networkidle');
      await expect(page).toHaveURL(/.*checkout/);
      await checkoutPage.placeOrder();
      await page.waitForLoadState('networkidle');
      orderId = await checkoutPage.getOrderId();
      expect(orderId).toBeTruthy();
      console.log("✅ Order placed successfully! Order ID:", orderId);
      await expect(checkoutPage.orderConfirmation).toBeVisible();
      const orderConfirmed = await checkoutPage.isOrderConfirmed();
      expect(orderConfirmed).toBeTruthy();
    });

    await test.step('Validate order in Order History', async () => {
      await orderHistoryPage.goto();
      await page.waitForLoadState('networkidle');
      const found = await orderHistoryPage.findOrder(orderId);
      expect(found).toBeTruthy();
      
      // Additional validation
      const orderDetails = await orderHistoryPage.getOrderDetails(orderId);
      expect(orderDetails).toBeTruthy();
      expect(orderDetails.orderId).toContain(orderId);
      expect(orderDetails.status).toBeTruthy();
      expect(orderDetails.total).toBeTruthy();
    });

    await test.step('Save order data for future reference', async () => {
      const orderData = {
        orderId: orderId,
        timestamp: new Date().toISOString(),
        product: "Printed Dress",
        status: "confirmed"
      };

      // Read existing orders or create new array
      let orders = [];
      try {
        const existingData = fs.readFileSync('orders.json', 'utf8');
        orders = JSON.parse(existingData);
      } catch (error) {
        // File doesn't exist, start with empty array
      }

      orders.push(orderData);
      fs.writeFileSync('orders.json', JSON.stringify(orders, null, 2));
    });
  });

  test('Validate multiple orders in history', async ({ page }) => {
    const homePage = new HomePage(page);
    const productPage = new ProductPage(page);
    const cartPage = new CartPage(page);
    const checkoutPage = new CheckoutPage(page);
    const orderHistoryPage = new OrderHistoryPage(page);

    const orderIds = [];

    await test.step('Place first order', async () => {
      await homePage.searchProduct("Dress");
      await page.waitForLoadState('networkidle');
      await productPage.addToCart("Printed Dress");
      await page.waitForSelector('.cart-products-count', { timeout: 10000 });
      await cartPage.proceedToCheckout();
      await page.waitForLoadState('networkidle');
      await checkoutPage.placeOrder();
      await page.waitForLoadState('networkidle');
      const firstOrderId = await checkoutPage.getOrderId();
      expect(firstOrderId).toBeTruthy();
      orderIds.push(firstOrderId);
    });

    await test.step('Place second order', async () => {
      await homePage.goto();
      await page.waitForLoadState('networkidle');
      await homePage.searchProduct("Summer");
      await page.waitForLoadState('networkidle');
      await productPage.addToCart("Printed Summer Dress");
      await page.waitForSelector('.cart-products-count', { timeout: 10000 });
      await cartPage.proceedToCheckout();
      await page.waitForLoadState('networkidle');
      await checkoutPage.placeOrder();
      await page.waitForLoadState('networkidle');
      const secondOrderId = await checkoutPage.getOrderId();
      expect(secondOrderId).toBeTruthy();
      orderIds.push(secondOrderId);
    });

    await test.step('Validate both orders in history', async () => {
      await orderHistoryPage.goto();
      await page.waitForLoadState('networkidle');
      
      for (const orderId of orderIds) {
        const found = await orderHistoryPage.findOrder(orderId);
        expect(found).toBeTruthy();
      }

      // Verify total order count
      const orderCount = await orderHistoryPage.getOrderCount();
      expect(orderCount).toBeGreaterThanOrEqual(2);
      
      // Verify orders are different
      expect(orderIds[0]).not.toBe(orderIds[1]);
    });
  });

  test('Negative test - Validate fake order ID not found', async ({ page }) => {
    const orderHistoryPage = new OrderHistoryPage(page);

    await test.step('Navigate to order history', async () => {
      await orderHistoryPage.goto();
      await page.waitForLoadState('networkidle');
    });

    await test.step('Search for fake order ID', async () => {
      const fakeOrderId = "FAKE12345";
      const found = await orderHistoryPage.findOrder(fakeOrderId);
      expect(found).toBeFalsy();
      
      // Additional negative validation
      const orderDetails = await orderHistoryPage.getOrderDetails(fakeOrderId);
      expect(orderDetails).toBeNull();
    });
  });

  test('Validate order status and details', async ({ page }) => {
    const homePage = new HomePage(page);
    const productPage = new ProductPage(page);
    const cartPage = new CartPage(page);
    const checkoutPage = new CheckoutPage(page);
    const orderHistoryPage = new OrderHistoryPage(page);

    let orderId;

    await test.step('Place order', async () => {
      await homePage.searchProduct("Dress");
      await page.waitForLoadState('networkidle');
      await productPage.addToCart("Printed Dress");
      await page.waitForSelector('.cart-products-count', { timeout: 10000 });
      await cartPage.proceedToCheckout();
      await page.waitForLoadState('networkidle');
      await checkoutPage.placeOrder();
      await page.waitForLoadState('networkidle');
      orderId = await checkoutPage.getOrderId();
      expect(orderId).toBeTruthy();
    });

    await test.step('Validate order status', async () => {
      await orderHistoryPage.goto();
      await page.waitForLoadState('networkidle');
      const orderStatus = await orderHistoryPage.getOrderStatus(orderId);
      expect(orderStatus).toBeTruthy();
      expect(orderStatus.toLowerCase()).toContain('processing');
    });

    await test.step('Validate order details', async () => {
      const orderDetails = await orderHistoryPage.getOrderDetails(orderId);
      expect(orderDetails).toBeTruthy();
      expect(orderDetails.orderId).toContain(orderId);
      expect(orderDetails.status).toBeTruthy();
      expect(orderDetails.total).toBeTruthy();
      expect(orderDetails.date).toBeTruthy();
    });
  });
});

New Concept: test.describe, test, and test.step (15 min)

Test Structure Components:
  • test.describe - Groups related tests together. Useful for E2E flow, smoke tests, regression suites.
  • test - Defines one test scenario.
  • test.step - Breaks a test into smaller named steps → improves trace reports and readability.
Movie Analogy:
  • describe = Movie (collection of scenes)
  • test = One scene
  • step = Dialogue in the scene

test.step Benefits:

// Without test.step - hard to debug
test('Order placement', async ({ page }) => {
  await loginPage.goto();
  await homePage.searchProduct("Dress");
  await productPage.addToCart("Printed Dress");
  await cartPage.proceedToCheckout();
  await checkoutPage.placeOrder();
  // If this fails, which step failed?
});

// With test.step - clear debugging
test('Order placement', async ({ page }) => {
  await test.step('Login and navigate', async () => {
    await loginPage.goto();
  });
  
  await test.step('Search and add product', async () => {
    await homePage.searchProduct("Dress");
    await productPage.addToCart("Printed Dress");
  });
  
  await test.step('Complete checkout', async () => {
    await cartPage.proceedToCheckout();
    await checkoutPage.placeOrder();
  });
});

test.describe Organization:

// Group by feature
test.describe('Order Management', () => {
  test('Place order', async () => {});
  test('View order history', async () => {});
  test('Cancel order', async () => {});
});

// Group by user type
test.describe('Admin Tests', () => {
  test('Manage orders', async () => {});
  test('Update order status', async () => {});
});

// Group by test type
test.describe('Smoke Tests', () => {
  test('Login flow', async () => {});
  test('Basic navigation', async () => {});
});

Wait Strategies & Expect Statements (10 min)

Essential Wait Strategies:
  • waitForLoadState('networkidle'): Wait for all network requests to complete
  • waitForSelector(): Wait for specific elements to appear
  • locator().waitFor(): Wait for locator to be ready
  • locator().isVisible(): Check element visibility with proper waits

Common Wait Patterns:

// Wait for network to be idle
await page.waitForLoadState('networkidle');

// Wait for specific element with timeout
await page.waitForSelector('.order-history tbody tr', { timeout: 10000 });

// Wait for locator to be ready
await this.ordersTable.first().waitFor({ timeout: 5000 });

// Check visibility with proper wait
const isVisible = await page.locator('h1:has-text("Order History")').isVisible();
expect(isVisible).toBeTruthy();

// Wait for function to return true
await page.waitForFunction(() => {
  return document.querySelector('.order-history tbody tr') !== null;
});

Expect Statement Patterns:

// Basic assertions
expect(orderId).toBeTruthy();
expect(found).toBeFalsy();
expect(orderCount).toBeGreaterThanOrEqual(2);

// String assertions
expect(orderDetails.orderId).toContain(orderId);
expect(orderStatus.toLowerCase()).toContain('processing');

// URL assertions
await expect(page).toHaveURL(/.*checkout/);
await expect(page).toHaveURL(/.*learn-playwright\.great-site\.net/);

// Element visibility
await expect(checkoutPage.orderConfirmation).toBeVisible();
await expect(orderHistoryPage.orderConfirmation).toBeHidden();

// Array assertions
expect(orderIds[0]).not.toBe(orderIds[1]);
expect(orders.length).toBeGreaterThan(0);

🧑‍💻 Interactive Questions & Answers

1. Why is test.step helpful when debugging failed tests?

Answer:
  • Clear failure location: You know exactly which step failed, not just which test.
  • Better trace reports: Playwright generates detailed step-by-step reports.
  • Easier debugging: Focus on the specific step that failed rather than the entire test.
  • Parallel execution: Steps can be run in parallel when possible.
  • Retry logic: Can retry individual steps instead of entire tests.
  • Example: If "Proceed to checkout" step fails, you know the issue is in cart navigation, not login or product selection.

2. If orderId changes every run, how do you still validate it?

Answer:
  • Capture and reuse: Store the orderId from order placement and use it for validation.
  • Dynamic validation: Use the captured orderId to search in order history.
  • Pattern matching: Use regex patterns to match order ID format instead of exact strings.
  • Latest order validation: Check if the order appears in the most recent orders.
  • Example:
    // Capture orderId during placement
    const orderId = await checkoutPage.getOrderId();
    
    // Use captured orderId for validation
    const found = await orderHistoryPage.findOrder(orderId);
    expect(found).toBeTruthy();

3. What's the difference between validating via UI (Order History) vs API (coming soon)?

Answer:
Aspect UI Validation (Order History) API Validation
Speed Slower (renders page, loads DOM) Faster (direct data access)
Reliability Can be flaky (UI changes, timing) More stable (direct data)
User Experience Tests actual user journey Tests backend functionality
Data Accuracy Tests what user sees Tests actual data state
Maintenance Higher (UI changes frequently) Lower (API contracts stable)

4. Can we skip login completely using session storage (Day 2)?

Answer:
  • Yes, absolutely! Session storage eliminates the need for login in every test.
  • Configuration setup: Use storageState: 'userState.json' in playwright.config.js
  • Automatic application: Session is applied to every test automatically
  • Performance benefit: Saves 2-5 seconds per test by skipping login
  • Example:
    // playwright.config.js
    export default defineConfig({
      use: {
        storageState: 'userState.json', // Auto-applies session
      },
    });
    
    // In test - no login needed!
    test('Order placement', async ({ page }) => {
      await page.goto('https://learn-playwright.great-site.net/');
      // Already logged in via session storage
      await homePage.searchProduct("Dress");
    });

5. How do you handle dynamic order IDs that change format?

Answer:
  • Pattern matching: Use regex to match order ID patterns instead of exact strings.
  • Partial matching: Search for partial order ID or common prefixes.
  • Latest order approach: Always check the most recent order instead of searching by ID.
  • Timestamp validation: Use order creation time to identify the correct order.
  • Example:
    // Pattern matching approach
    const orderIdPattern = /^ORD-\d{8}-\w{4}$/;
    const found = await orderHistoryPage.findOrderByPattern(orderIdPattern);
    
    // Latest order approach
    const latestOrder = await orderHistoryPage.getLatestOrder();
    expect(latestOrder).toBeTruthy();

6. What's the best way to organize test.describe blocks?

Answer:
  • By feature: Group tests by functionality (Order Management, User Authentication)
  • By user role: Separate admin, user, and guest tests
  • By test type: Smoke, regression, integration tests
  • By priority: Critical path, nice-to-have features
  • By environment: Different describe blocks for different environments
  • Example:
    test.describe('Order Management', () => {
      test.describe('Order Placement', () => {
        test('Single product order', async () => {});
        test('Multiple product order', async () => {});
      });
      
      test.describe('Order History', () => {
        test('View order details', async () => {});
        test('Search orders', async () => {});
      });
    });

7. How do you handle flaky order history tests?

Answer:
  • Wait strategies: Use proper waits for dynamic content to load
  • Retry mechanisms: Implement retry logic for critical validations
  • Data cleanup: Clean up test data between test runs
  • Stable selectors: Use data attributes instead of CSS classes
  • Parallel execution: Run tests in isolation to avoid interference
  • Example:
    // Wait for order to appear in history
    await page.waitForFunction(() => {
      return document.querySelector('.order-history tbody tr');
    });
    
    // Retry mechanism
    const maxRetries = 3;
    for (let i = 0; i < maxRetries; i++) {
      const found = await orderHistoryPage.findOrder(orderId);
      if (found) break;
      await page.waitForTimeout(1000);
    }

8. What's the difference between beforeEach and beforeAll in test organization?

Answer:
  • beforeEach: Runs before each individual test - good for test isolation
  • beforeAll: Runs once before all tests in the describe block - good for expensive setup
  • Use beforeEach for: Page navigation, fresh state setup, test data preparation
  • Use beforeAll for: Database seeding, API setup, global configuration
  • Example:
    test.describe('Order Tests', () => {
      test.beforeAll(async () => {
        // Run once - setup test database
        await setupTestDatabase();
      });
      
      test.beforeEach(async ({ page }) => {
        // Run before each test - fresh page
        await page.goto('https://learn-playwright.great-site.net/');
      });
    });

9. When should you use waitForLoadState('networkidle') vs waitForSelector()?

Answer:
  • waitForLoadState('networkidle'): Use when you need to wait for all network requests to complete, especially after form submissions or navigation
  • waitForSelector(): Use when you need to wait for specific elements to appear, more precise than networkidle
  • Combined approach: Often use both - networkidle first, then waitForSelector for specific elements
  • Performance consideration: networkidle can be slower but more reliable for dynamic content
  • Example:
    // After form submission
    await page.click('#submit');
    await page.waitForLoadState('networkidle');
    await page.waitForSelector('.success-message');
    
    // For specific element appearance
    await page.waitForSelector('.order-history tbody tr', { timeout: 10000 });

10. How do you handle flaky tests with proper wait strategies?

Answer:
  • Use explicit waits: Always wait for elements before interacting with them
  • Combine wait strategies: Use networkidle + waitForSelector for maximum reliability
  • Set appropriate timeouts: Don't use default timeouts for critical elements
  • Wait for state changes: Wait for specific conditions rather than fixed delays
  • Retry mechanisms: Implement retry logic for critical validations
  • Example:
    // Robust wait strategy
    await page.waitForLoadState('networkidle');
    await page.waitForSelector('.order-history tbody tr', { timeout: 10000 });
    await this.ordersTable.first().waitFor({ timeout: 5000 });
    
    // Retry mechanism
    const maxRetries = 3;
    for (let i = 0; i < maxRetries; i++) {
      const found = await orderHistoryPage.findOrder(orderId);
      if (found) break;
      await page.waitForTimeout(1000);
    }

🎯 Day 4 Homework Tasks

🟢 Beginner

Task 1

Extend OrderHistoryPage → add method getAllOrders() that returns all Order IDs.

Task 2

Print all orders in console after validation.

🟡 Intermediate

Task 3

Write a negative test: Validate a fake orderId = "ABC12345" is not found in history.

Task 4

Add one more product order in the same test → confirm multiple orders appear.

🔴 Advanced

Task 5

Extend POM with method getOrderStatus(orderId) → return order status.

Task 6

Validate that latest order status = "Processing".

Task 7

Refactor test to use beforeEach to load session state automatically.

Best Practices & Tips

Test Structure Best Practices:
  • Use descriptive test names that explain the scenario
  • Group related tests with test.describe for better organization
  • Break complex tests into logical steps with test.step
  • Keep tests independent - each should run in isolation
  • Use beforeEach for test setup and afterEach for cleanup
Common Pitfalls:
  • Don't rely on test execution order - tests should be independent
  • Avoid hard-coded waits - use proper wait strategies
  • Don't test implementation details - test user behavior
  • Keep test data clean to avoid test interference
  • Handle dynamic content properly with appropriate waits
Order Validation Strategies:
  • Use pattern matching for dynamic order IDs
  • Implement retry logic for flaky validations
  • Test both positive and negative scenarios
  • Validate order details, not just presence
  • Use session storage to avoid repeated logins

✅ Outcomes

  • Dynamically validate OrderIDs in Order History using loops and conditions
  • Use test.describe, test, and test.step for structured tests
  • Apply POM + loop logic in real test cases
  • Implement comprehensive order validation strategies
  • Handle dynamic content and flaky test scenarios
  • Organize tests effectively with proper grouping and structure
  • Apply best practices for maintainable test automation
  • Prepare for Day 5 (Alerts & Frames) where you start tackling edge cases