🕒 Teaching Flow
1. Story Intro (5 min)
👉 "Hardcoding data makes automation brittle. Real-world test frameworks read from data files, reuse utilities, and write back test results like Order IDs. Today we'll use JSON for input, utilities for output, and add support for alerts & calendars."
Data-Driven Analogy:
Like a restaurant that reads from a menu (JSON data), uses a kitchen (utilities), and writes orders on receipts (test results). Each customer (test) gets a different meal (data) but follows the same cooking process (test flow).
Like a restaurant that reads from a menu (JSON data), uses a kitchen (utilities), and writes orders on receipts (test results). Each customer (test) gets a different meal (data) but follows the same cooking process (test flow).
Data-Driven Testing with JSON (25 min)
Step 1 – Create JSON Test Data
📂 test-data/test-data.json
[
{
"username": "qa@example.com",
"password": "password123",
"productName": "Printed Dress",
"expectedPrice": "$26.00",
"quantity": 1
},
{
"username": "qa2@example.com",
"password": "password456",
"productName": "Blouse",
"expectedPrice": "$27.00",
"quantity": 2
},
{
"username": "admin@example.com",
"password": "admin123",
"productName": "Printed Summer Dress",
"expectedPrice": "$28.98",
"quantity": 1
}
]
Step 2 – Utility for Storing Order Data
📂 utils/DataManager.js
import fs from "fs";
import path from "path";
export class DataManager {
static saveOrder(product, orderId, additionalData = {}) {
const filePath = `./test-data/${product.replace(/\s+/g, '-').toLowerCase()}-order.json`;
const data = {
product,
orderId,
createdAt: new Date().toISOString(),
...additionalData
};
// Ensure directory exists
const dir = path.dirname(filePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
console.log(`✅ Order saved for ${product}: ${orderId}`);
return data;
}
static getOrder(product) {
const filePath = `./test-data/${product.replace(/\s+/g, '-').toLowerCase()}-order.json`;
if (!fs.existsSync(filePath)) {
throw new Error(`No order file found for product: ${product}`);
}
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
}
static saveMultipleOrders(orders) {
const filePath = './test-data/all-orders.json';
const existingOrders = this.getAllOrders();
const updatedOrders = [...existingOrders, ...orders];
fs.writeFileSync(filePath, JSON.stringify(updatedOrders, null, 2));
console.log(`✅ Saved ${orders.length} orders to all-orders.json`);
}
static getAllOrders() {
const filePath = './test-data/all-orders.json';
if (!fs.existsSync(filePath)) {
return [];
}
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
}
static findLatestOrder() {
const orders = this.getAllOrders();
if (orders.length === 0) {
return null;
}
return orders.sort((a, b) =>
new Date(b.createdAt) - new Date(a.createdAt)
)[0];
}
static clearOrders() {
const filePath = './test-data/all-orders.json';
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
console.log('🗑️ Cleared all orders');
}
}
}
Step 3 – Parameterized E2E Test
📂 tests/e2e-place-order.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 { DataManager } from "../utils/DataManager.js";
// Load test data
const testData = JSON.parse(JSON.stringify(require("../test-data/test-data.json")));
test.describe('Data-Driven Order Placement', () => {
test.beforeAll(async () => {
// Clear previous orders before starting
DataManager.clearOrders();
});
for (const record of testData) {
test(`E2E Place Order for ${record.productName}`, async ({ page }) => {
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);
await test.step(`Login with user: ${record.username}`, async () => {
await loginPage.goto();
await page.waitForLoadState('networkidle');
await loginPage.login(record.username, record.password);
const isLoggedIn = await loginPage.isLoggedIn();
expect(isLoggedIn).toBeTruthy();
});
await test.step(`Search and add product: ${record.productName}`, async () => {
await homePage.searchProduct(record.productName);
await page.waitForLoadState('networkidle');
await productPage.addToCart(record.productName);
await page.waitForSelector('.cart-products-count', { timeout: 10000 });
// Verify product was added to cart
const cartCount = await page.locator('.cart-products-count').textContent();
expect(cartCount).toBeTruthy();
});
await test.step("Checkout and place order", async () => {
await cartPage.proceedToCheckout();
await page.waitForLoadState('networkidle');
await checkoutPage.placeOrder();
await page.waitForLoadState('networkidle');
const orderId = await checkoutPage.getOrderId();
expect(orderId).toBeTruthy();
console.log(`✅ Order placed for ${record.productName}: ${orderId}`);
// Save to JSON using utility
const orderData = DataManager.saveOrder(record.productName, orderId, {
username: record.username,
expectedPrice: record.expectedPrice,
quantity: record.quantity
});
// Also save to all orders
DataManager.saveMultipleOrders([orderData]);
});
});
}
});
Step 4 – Validation Test Using Stored JSON
📂 tests/e2e-validate-order.spec.js
import { test, expect } from "@playwright/test";
import { OrderHistoryPage } from "../pages/OrderHistoryPage.js";
import { DataManager } from "../utils/DataManager.js";
test.describe('Data-Driven Order Validation', () => {
test("Validate orders from saved JSON", async ({ page }) => {
const orderHistoryPage = new OrderHistoryPage(page);
const allOrders = DataManager.getAllOrders();
expect(allOrders.length).toBeGreaterThan(0);
console.log(`📝 Validating ${allOrders.length} orders from JSON`);
await test.step("Navigate to order history", async () => {
await orderHistoryPage.goto();
await page.waitForLoadState('networkidle');
});
await test.step("Validate each order", async () => {
for (const orderDetails of allOrders) {
console.log(`🔍 Validating Order: ${orderDetails.product} - ${orderDetails.orderId}`);
const found = await orderHistoryPage.findOrder(orderDetails.orderId);
expect(found).toBeTruthy();
// Additional validation
const orderData = await orderHistoryPage.getOrderDetails(orderDetails.orderId);
expect(orderData).toBeTruthy();
expect(orderData.orderId).toContain(orderDetails.orderId);
}
});
await test.step("Validate latest order", async () => {
const latestOrder = DataManager.findLatestOrder();
expect(latestOrder).toBeTruthy();
const found = await orderHistoryPage.findOrder(latestOrder.orderId);
expect(found).toBeTruthy();
console.log(`✅ Latest order validated: ${latestOrder.product} - ${latestOrder.orderId}`);
});
});
test("Validate specific product order", async ({ page }) => {
const orderHistoryPage = new OrderHistoryPage(page);
// Read specific order from JSON
const orderDetails = DataManager.getOrder("Printed Dress");
console.log("📝 Validating Specific Order:", orderDetails);
await orderHistoryPage.goto();
await page.waitForLoadState('networkidle');
const found = await orderHistoryPage.findOrder(orderDetails.orderId);
expect(found).toBeTruthy();
const orderData = await orderHistoryPage.getOrderDetails(orderDetails.orderId);
expect(orderData).toBeTruthy();
expect(orderData.orderId).toContain(orderDetails.orderId);
});
});
Handling Alerts (15 min)
Alert Types in Web Applications:
- JavaScript Alerts: Simple popup messages
- Confirm Dialogs: Yes/No confirmation popups
- Prompt Dialogs: Input field popups
- Browser Alerts: Page unload confirmations
Basic Alert Handling
// Basic alert handling
test("Handle JS Alert", async ({ page }) => {
// Listen for dialog events
page.on("dialog", async (dialog) => {
console.log("Dialog message:", dialog.message());
console.log("Dialog type:", dialog.type());
await dialog.accept(); // Click OK
});
await page.goto("https://the-internet.herokuapp.com/javascript_alerts");
await page.getByText("Click for JS Alert").click();
// Verify alert was handled
await expect(page.locator("#result")).toContainText("You successfully clicked an alert");
});
Confirm Dialog Handling
// Handle confirm dialogs
test("Handle Confirm Dialog - Accept", async ({ page }) => {
page.on("dialog", async (dialog) => {
console.log("Confirm message:", dialog.message());
await dialog.accept(); // Click OK
});
await page.goto("https://the-internet.herokuapp.com/javascript_alerts");
await page.getByText("Click for JS Confirm").click();
await expect(page.locator("#result")).toContainText("You clicked: Ok");
});
test("Handle Confirm Dialog - Dismiss", async ({ page }) => {
page.on("dialog", async (dialog) => {
console.log("Confirm message:", dialog.message());
await dialog.dismiss(); // Click Cancel
});
await page.goto("https://the-internet.herokuapp.com/javascript_alerts");
await page.getByText("Click for JS Confirm").click();
await expect(page.locator("#result")).toContainText("You clicked: Cancel");
});
Prompt Dialog Handling
// Handle prompt dialogs
test("Handle Prompt Dialog", async ({ page }) => {
const testMessage = "Hello from Playwright!";
page.on("dialog", async (dialog) => {
console.log("Prompt message:", dialog.message());
await dialog.accept(testMessage); // Enter text and click OK
});
await page.goto("https://the-internet.herokuapp.com/javascript_alerts");
await page.getByText("Click for JS Prompt").click();
await expect(page.locator("#result")).toContainText(`You entered: ${testMessage}`);
});
Advanced Alert Handling with Utilities
📂 utils/AlertHandler.js
export class AlertHandler {
static async handleAlert(page, action = 'accept', message = null) {
return new Promise((resolve) => {
page.on("dialog", async (dialog) => {
console.log(`Alert detected: ${dialog.message()}`);
switch (action.toLowerCase()) {
case 'accept':
await dialog.accept();
break;
case 'dismiss':
await dialog.dismiss();
break;
case 'prompt':
await dialog.accept(message);
break;
}
resolve(dialog.message());
});
});
}
static async waitForAlert(page, timeout = 5000) {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
reject(new Error('Alert did not appear within timeout'));
}, timeout);
page.on("dialog", async (dialog) => {
clearTimeout(timeoutId);
resolve(dialog);
});
});
}
}
E2E Test with Alert Handling
// E2E test with alert handling
test("E2E Order with Alert Confirmation", async ({ page }) => {
const homePage = new HomePage(page);
const productPage = new ProductPage(page);
const cartPage = new CartPage(page);
const checkoutPage = new CheckoutPage(page);
// Set up alert handler
const alertPromise = AlertHandler.handleAlert(page, 'accept');
await test.step("Add product to cart", async () => {
await homePage.goto();
await homePage.searchProduct("Printed Dress");
await productPage.addToCart("Printed Dress");
});
await test.step("Proceed to checkout with alert", async () => {
await cartPage.proceedToCheckout();
// Wait for and handle alert
const alertMessage = await alertPromise;
expect(alertMessage).toContain("confirm");
await checkoutPage.placeOrder();
const orderId = await checkoutPage.getOrderId();
expect(orderId).toBeTruthy();
});
});
Calendar Handling (20 min)
Calendar Selection Strategies:
- Date Input Fields: Direct value setting
- Calendar Widgets: Click-based selection
- Dropdown Calendars: Month/Year selection
- Dynamic Dates: Relative date calculations
Basic Calendar Selection
// Basic calendar selection
test("Select specific date in calendar", async ({ page }) => {
await page.goto("https://demoqa.com/date-picker");
// Method 1: Direct input
await page.fill("#datePickerMonthYearInput", "12/25/2024");
// Verify the date was set
await expect(page.locator("#datePickerMonthYearInput"))
.toHaveValue("December 25, 2024");
});
Dynamic Date Selection
// Dynamic date selection (today + 5 days)
test("Select future date in calendar", async ({ page }) => {
await page.goto("https://demoqa.com/date-picker");
await page.click("#datePickerMonthYearInput");
const date = new Date();
date.setDate(date.getDate() + 5);
const year = date.getFullYear();
const month = date.toLocaleString("default", { month: "long" });
const day = date.getDate();
// Select year
await page.locator(".react-datepicker__year-select").selectOption(`${year}`);
// Select month
await page.locator(".react-datepicker__month-select").selectOption(month);
// Select day
await page.click(`.react-datepicker__day--0${day}`);
// Verify selection
await expect(page.locator("#datePickerMonthYearInput"))
.toHaveValue(`${month} ${day}, ${year}`);
});
Advanced Calendar Utilities
📂 utils/CalendarHelper.js
export class CalendarHelper {
static getFutureDate(daysFromNow) {
const date = new Date();
date.setDate(date.getDate() + daysFromNow);
return date;
}
static getPastDate(daysAgo) {
const date = new Date();
date.setDate(date.getDate() - daysAgo);
return date;
}
static getLastDayOfMonth() {
const date = new Date();
return new Date(date.getFullYear(), date.getMonth() + 1, 0);
}
static formatDateForInput(date) {
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
});
}
static formatDateForDisplay(date) {
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
}
static async selectDateFromCalendar(page, date, calendarSelector = "#datePickerMonthYearInput") {
await page.click(calendarSelector);
const year = date.getFullYear();
const month = date.toLocaleString("default", { month: "long" });
const day = date.getDate();
// Select year
await page.locator(".react-datepicker__year-select").selectOption(`${year}`);
// Select month
await page.locator(".react-datepicker__month-select").selectOption(month);
// Select day
await page.click(`.react-datepicker__day--0${day}`);
return `${month} ${day}, ${year}`;
}
static async selectDateRange(page, startDate, endDate) {
// Select start date
await this.selectDateFromCalendar(page, startDate, "#startDate");
// Select end date
await this.selectDateFromCalendar(page, endDate, "#endDate");
}
}
E2E Test with Calendar Selection
// E2E test with calendar selection
test("E2E Order with Delivery Date Selection", async ({ page }) => {
const homePage = new HomePage(page);
const productPage = new ProductPage(page);
const cartPage = new CartPage(page);
const checkoutPage = new CheckoutPage(page);
await test.step("Add product to cart", async () => {
await homePage.goto();
await homePage.searchProduct("Printed Dress");
await productPage.addToCart("Printed Dress");
});
await test.step("Proceed to checkout", async () => {
await cartPage.proceedToCheckout();
});
await test.step("Select delivery date", async () => {
// Select delivery date (7 days from now)
const deliveryDate = CalendarHelper.getFutureDate(7);
const formattedDate = await CalendarHelper.selectDateFromCalendar(page, deliveryDate);
console.log(`📅 Selected delivery date: ${formattedDate}`);
// Verify date was selected
await expect(page.locator("#deliveryDate")).toHaveValue(formattedDate);
});
await test.step("Complete order", async () => {
await checkoutPage.placeOrder();
const orderId = await checkoutPage.getOrderId();
expect(orderId).toBeTruthy();
// Save order with delivery date
DataManager.saveOrder("Printed Dress", orderId, {
deliveryDate: CalendarHelper.formatDateForDisplay(deliveryDate)
});
});
});
Complex Calendar Scenarios
// Complex calendar scenarios
test("Select last day of current month", async ({ page }) => {
await page.goto("https://demoqa.com/date-picker");
const lastDayOfMonth = CalendarHelper.getLastDayOfMonth();
const formattedDate = await CalendarHelper.selectDateFromCalendar(page, lastDayOfMonth);
console.log(`📅 Last day of month: ${formattedDate}`);
await expect(page.locator("#datePickerMonthYearInput"))
.toHaveValue(formattedDate);
});
test("Select date range for reports", async ({ page }) => {
await page.goto("https://demoqa.com/date-picker");
const startDate = CalendarHelper.getPastDate(30); // 30 days ago
const endDate = CalendarHelper.getFutureDate(7); // 7 days from now
await CalendarHelper.selectDateRange(page, startDate, endDate);
console.log(`📅 Date range: ${CalendarHelper.formatDateForDisplay(startDate)} to ${CalendarHelper.formatDateForDisplay(endDate)}`);
});
🧑💻 Interactive Questions & Answers
1. Why use JSON.parse(JSON.stringify(require())) instead of just require()?
Answer:
- Deep cloning: Creates a complete copy of the data, preventing mutations
- Test isolation: Each test gets fresh data, avoiding test interference
- Data integrity: Original JSON file remains unchanged during test execution
- Parallel execution: Multiple tests can run simultaneously without data conflicts
- Example:
// Without cloning - dangerous const data = require("../test-data/test-data.json"); data[0].username = "modified"; // Affects other tests! // With cloning - safe const data = JSON.parse(JSON.stringify(require("../test-data/test-data.json"))); data[0].username = "modified"; // Only affects this test
2. How does naming a test dynamically as ${record.productName} help in CI reports?
Answer:
- Clear identification: Each test has a unique, descriptive name
- Easy debugging: Failed tests are immediately identifiable by product name
- Better reporting: CI tools can group and filter tests by name patterns
- Test organization: Reports show exactly which data set caused failures
- Example output:
✅ E2E Place Order for Printed Dress ❌ E2E Place Order for Blouse ✅ E2E Place Order for Printed Summer Dress
3. What are benefits of having a DataManager utility vs writing inside spec?
Answer:
- Reusability: Same utility can be used across multiple test files
- Maintainability: Changes to data handling logic only need to be made in one place
- Testability: Utilities can be unit tested independently
- Consistency: Standardized data handling across all tests
- Error handling: Centralized error handling for file operations
- Example:
// Without utility - repetitive and error-prone fs.writeFileSync(`./test-data/${product}-order.json`, JSON.stringify(data)); // With utility - clean and reusable DataManager.saveOrder(product, orderId);
4. How would you extend DataManager to save multiple orders in a single file?
Answer:
// Extended DataManager methods
static saveMultipleOrders(orders) {
const filePath = './test-data/all-orders.json';
const existingOrders = this.getAllOrders();
const updatedOrders = [...existingOrders, ...orders];
fs.writeFileSync(filePath, JSON.stringify(updatedOrders, null, 2));
console.log(`✅ Saved ${orders.length} orders to all-orders.json`);
}
static getAllOrders() {
const filePath = './test-data/all-orders.json';
if (!fs.existsSync(filePath)) {
return [];
}
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
}
static findOrdersByProduct(product) {
const allOrders = this.getAllOrders();
return allOrders.filter(order => order.product === product);
}
static findLatestOrder() {
const orders = this.getAllOrders();
if (orders.length === 0) return null;
return orders.sort((a, b) =>
new Date(b.createdAt) - new Date(a.createdAt)
)[0];
}
5. In calendar handling, when would you use .selectOption() vs .click()?
Answer:
- selectOption(): Use for dropdown elements (year, month selectors)
- click(): Use for clickable elements (day buttons, calendar cells)
- Performance: selectOption() is faster for dropdown selections
- Reliability: selectOption() is more reliable for form elements
- Example:
// Use selectOption for dropdowns await page.locator(".react-datepicker__year-select").selectOption("2024"); await page.locator(".react-datepicker__month-select").selectOption("December"); // Use click for day selection await page.click(".react-datepicker__day--025"); await page.click(".react-datepicker__day--031");
6. How do you handle different alert types in a single test?
Answer:
- Sequential handling: Handle alerts one by one as they appear
- Type detection: Check alert type and handle accordingly
- Promise-based approach: Use promises to handle multiple alerts
- Example:
test("Handle multiple alerts", async ({ page }) => { const alerts = []; page.on("dialog", async (dialog) => { alerts.push(dialog.type()); if (dialog.type() === 'alert') { await dialog.accept(); } else if (dialog.type() === 'confirm') { await dialog.accept(); } else if (dialog.type() === 'prompt') { await dialog.accept("Test input"); } }); // Trigger multiple alerts await page.click("#alert-button"); await page.click("#confirm-button"); await page.click("#prompt-button"); expect(alerts).toEqual(['alert', 'confirm', 'prompt']); });
7. How do you handle calendar widgets that don't use standard HTML inputs?
Answer:
- Custom selectors: Use data attributes or specific CSS classes
- Keyboard navigation: Use arrow keys to navigate calendar
- API approach: Use JavaScript to set date values directly
- Example:
// Custom calendar widget await page.click('[data-testid="calendar-trigger"]'); await page.click('[data-testid="calendar-day-25"]'); // Keyboard navigation await page.click('[data-testid="calendar-trigger"]'); await page.keyboard.press('ArrowRight'); // Navigate to next day await page.keyboard.press('Enter'); // Select current day // JavaScript approach await page.evaluate((date) => { document.querySelector('#hidden-date-input').value = date; }, '2024-12-25');
8. How do you validate that alerts were handled correctly?
Answer:
- Result verification: Check page content after alert handling
- State validation: Verify application state changed as expected
- Message capture: Store and validate alert messages
- Example:
test("Validate alert handling", async ({ page }) => { let alertMessage = ''; page.on("dialog", async (dialog) => { alertMessage = dialog.message(); await dialog.accept(); }); await page.click("#delete-button"); // Verify alert was handled expect(alertMessage).toContain("Are you sure"); await expect(page.locator("#success-message")).toBeVisible(); await expect(page.locator("#item-list")).toHaveCount(0); });
🎯 Day 5 Homework Tasks
🟢 Beginner
Task 1
Add a new product in test-data.json → verify test runs automatically.
Task 2
Write alert test that dismisses popup instead of accepting.
🟡 Intermediate
Task 3
Extend DataManager → save orders into a single orders.json array.
Task 4
Validate two orders in Order History by looping JSON.
🔴 Advanced
Task 5
Enhance DataManager → add findLatestOrder() method (returns most recent order).
Task 6
Calendar Task: Write utility that picks current month's last date dynamically.
Best Practices & Tips
Data-Driven Testing Best Practices:
- Use meaningful test data that represents real-world scenarios
- Keep test data files organized and well-documented
- Implement proper error handling for missing or invalid data
- Use data validation to ensure test data integrity
- Clean up test data between test runs to avoid interference
Common Pitfalls:
- Don't hardcode test data in test files - use external data sources
- Avoid modifying shared test data during test execution
- Don't ignore alert handling - it can cause tests to hang
- Be careful with calendar date calculations - consider timezone issues
- Always validate that alerts were handled correctly
Alert & Calendar Handling Tips:
- Set up alert handlers before triggering actions that cause alerts
- Use proper wait strategies for calendar widgets to load
- Implement retry logic for flaky calendar interactions
- Test both positive and negative alert scenarios
- Use utilities to centralize common calendar operations
✅ Outcomes
- Understand data-driven testing with JSON
- Parameterize tests with loops & dynamic names
- Store & retrieve order details with DataManager utility
- Handle alerts (accept/dismiss) effectively
- Handle calendar selection dynamically with assertions
- Implement comprehensive test data management
- Create reusable utilities for common operations
- Apply best practices for maintainable test automation