🕒 Teaching Flow
1. Story Intro (5 min)
Think of API interception like a restaurant where you can replace the kitchen (backend) with a fake kitchen that serves exactly what you want. The waiter (UI) still works normally, but now you control what food (data) gets served. This is perfect for testing when the real kitchen is broken or not ready yet.
Website & API Recap (10 min)
🏨 Available API Endpoints:
- GET
/api/room→ List all available rooms - GET
/api/room/{id}→ Get specific room details - POST
/api/booking→ Create new room booking
📋 Room API Response Example:
{
"rooms": [
{
"roomid": 1,
"roomName": "101",
"type": "Single",
"accessible": true,
"image": "/images/room1.jpg",
"description": "Aenean porttitor mauris sit amet lacinia molestie...",
"features": ["TV", "WiFi", "Safe"],
"roomPrice": 100
}
]
}
- Real-world application with actual APIs
- Perfect for learning API + UI integration
- Stable environment for consistent testing
- Multiple endpoints to practice different scenarios
Normal Flow (Without Intercept)
📂 tests/normal-flow.spec.js
import { test, expect } from "@playwright/test";
test("Get rooms from API and display - Normal Flow", async ({ page }) => {
// Step 1: Navigate to the website
await page.goto("https://automationintesting.online/");
// Step 2: Wait for the page to fully load
await page.waitForLoadState("networkidle");
// Step 3: Wait for rooms to load (they load via API call)
await page.waitForSelector(".room");
// Step 4: Get all room information
const rooms = await page.locator(".room").allTextContents();
// Step 5: Log and verify rooms
console.log("Rooms found:", rooms);
expect(rooms.length).toBeGreaterThan(0);
// Step 6: Verify specific room details
const roomText = rooms.join(" ");
expect(roomText).toContain("101"); // Room number from API
expect(roomText).toContain("Single"); // Room type
});
- Browser requests real room API data from
/api/room - API returns actual room information (roomid, roomName, type, price, etc.)
- UI displays the real room data on the website
- Test validates what's actually on the server
Intercepting with `page.route()` (Fake Response)
📂 tests/intercept-fake-response.spec.js
import { test, expect } from "@playwright/test";
test("Intercept rooms API with fake response", async ({ page }) => {
// Step 1: Create fake room data
const fakeResponse = {
rooms: [
{
roomid: 1,
roomName: "101",
type: "Single",
accessible: true,
image: "/images/room1.jpg",
description: "This is a fake room for testing",
features: ["TV", "WiFi", "Safe"],
roomPrice: 100
},
{
roomid: 2,
roomName: "102",
type: "Double",
accessible: true,
image: "/images/room2.jpg",
description: "Another fake room for testing",
features: ["TV", "Radio", "Safe"],
roomPrice: 150
}
]
};
// Step 2: Set up route interception
// Why "**/room"? The ** means "match any path that contains /room"
// This will intercept both /api/room and /api/room/1
await page.route("**/room", async route => {
console.log("🚫 Intercepted API call:", route.request().url());
console.log("🔍 Request method:", route.request().method());
// Step 3: Get original response (optional - for learning)
const originalResponse = await page.request.fetch(route.request());
console.log("📡 Original response status:", originalResponse.status());
// Step 4: Replace with fake response
await route.fulfill({
status: 200,
body: JSON.stringify(fakeResponse),
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*"
}
});
});
// Step 5: Navigate to website
await page.goto("https://automationintesting.online/");
// Step 6: Wait for rooms to load
await page.waitForSelector(".room");
// Step 7: Verify fake data is displayed
const roomText = await page.locator(".room").first().textContent();
console.log("📋 Room shown on UI:", roomText);
// Step 8: Assert fake data appears
expect(roomText).toContain("101");
expect(roomText).toContain("Single");
expect(roomText).toContain("This is a fake room for testing");
});
- Browser → asks for
GET /api/room - Playwright → intercepts the request using
**/roompattern - QA → injects fake room response data
- Browser → renders fake room data on UI
- Test → validates fake room data appears correctly
- ** = matches any path before "/room"
- Intercepts:
/api/room,/api/room/1,/api/room/2 - Flexible: One pattern catches multiple endpoints
- Specific: Only intercepts room-related API calls
- Unstable APIs: When backend is down or slow
- Development Phase: When APIs aren't ready yet
- Consistent Testing: Same data every time
- Edge Cases: Test specific scenarios easily
Using API + UI Together (Create Booking)
📂 tests/hybrid-api-ui.spec.js
import { test, expect } from "@playwright/test";
test("Create room booking via API, validate in UI - Hybrid Test", async ({ request, page }) => {
// Step 1: Create room booking via API
console.log("🚀 Step 1: Creating room booking via API...");
const apiResponse = await request.post("https://automationintesting.online/api/booking", {
data: {
roomid: 2,
firstname: "QA",
lastname: "Student",
depositpaid: false,
bookingdates: {
checkin: "2025-09-23",
checkout: "2025-09-27"
},
email: "qa.student@test.com",
phone: "98765 45678"
},
});
// Step 2: Verify API response
expect(apiResponse.ok()).toBeTruthy();
expect(apiResponse.status()).toBe(201);
const responseBody = await apiResponse.json();
console.log("✅ Booking created:", responseBody);
// Step 3: Navigate to UI
console.log("🌐 Step 2: Navigating to UI...");
await page.goto("https://automationintesting.online/");
await page.waitForLoadState("networkidle");
// Step 4: Wait for rooms to load
await page.waitForSelector(".room");
// Step 5: Validate room information in UI
console.log("🔍 Step 3: Validating room data in UI...");
const roomText = await page.locator(".room").allTextContents();
const allRoomsText = roomText.join(" ");
console.log("📋 All rooms text:", allRoomsText);
// Step 6: Assert room details in UI
expect(allRoomsText).toContain("102"); // Room number from API
expect(allRoomsText).toContain("Double"); // Room type
expect(allRoomsText).toContain("150"); // Room price
console.log("✅ Hybrid test completed successfully!");
});
📋 Expected Booking API Response:
{
"bookingid": 123,
"roomid": 2,
"firstname": "QA",
"lastname": "Student",
"depositpaid": false,
"bookingdates": {
"checkin": "2025-09-23",
"checkout": "2025-09-27"
},
"email": "qa.student@test.com",
"phone": "98765 45678"
}
- Create booking via API (fast and reliable)
- Check UI displays room data correctly
- Stable + Fast - best of both worlds
- Real-world scenario - how users actually interact
Advanced Route Interception Patterns
Multiple Route Interception
📂 tests/multiple-routes.spec.js
import { test, expect } from "@playwright/test";
test("Intercept multiple room API endpoints", async ({ page }) => {
// Step 1: Intercept GET /api/room (all rooms)
await page.route("**/api/room", async route => {
if (route.request().method() === 'GET' && !route.request().url().includes('/api/room/')) {
const fakeRooms = {
rooms: [
{
roomid: 1,
roomName: "101",
type: "Single",
roomPrice: 100,
features: ["TV", "WiFi"]
},
{
roomid: 2,
roomName: "102",
type: "Double",
roomPrice: 150,
features: ["TV", "Radio"]
}
]
};
await route.fulfill({
status: 200,
body: JSON.stringify(fakeRooms),
headers: { "Content-Type": "application/json" }
});
} else {
// Let other methods (POST) pass through
await route.continue();
}
});
// Step 2: Intercept specific room by ID
await page.route("**/api/room/1", async route => {
const fakeRoom = {
roomid: 1,
roomName: "101",
type: "Single",
accessible: true,
image: "/images/room1.jpg",
description: "This is a fake single room for testing",
features: ["TV", "WiFi", "Safe"],
roomPrice: 100
};
await route.fulfill({
status: 200,
body: JSON.stringify(fakeRoom),
headers: { "Content-Type": "application/json" }
});
});
await page.goto("https://automationintesting.online/");
await page.waitForSelector(".room");
const roomText = await page.locator(".room").textContent();
expect(roomText).toContain("101");
expect(roomText).toContain("Single");
});
Conditional Route Interception
📂 tests/conditional-routes.spec.js
import { test, expect } from "@playwright/test";
test("Conditional route interception based on request", async ({ page }) => {
await page.route("**/room**", async route => {
const url = route.request().url();
const method = route.request().method();
console.log(`🔍 Intercepted: ${method} ${url}`);
// Different responses based on request type
if (method === 'GET' && url.includes('/api/room/')) {
// Single room request (e.g., /api/room/1)
const singleRoom = {
roomid: 1,
roomName: "101",
type: "Single",
accessible: true,
image: "/images/room1.jpg",
description: "This is a single room for testing",
features: ["TV", "WiFi", "Safe"],
roomPrice: 100
};
await route.fulfill({
status: 200,
body: JSON.stringify(singleRoom),
headers: { "Content-Type": "application/json" }
});
} else if (method === 'GET') {
// All rooms request (e.g., /api/room)
const allRooms = {
rooms: [
{
roomid: 1,
roomName: "101",
type: "Single",
roomPrice: 100,
features: ["TV", "WiFi"]
},
{
roomid: 2,
roomName: "102",
type: "Double",
roomPrice: 150,
features: ["TV", "Radio"]
}
]
};
await route.fulfill({
status: 200,
body: JSON.stringify(allRooms),
headers: { "Content-Type": "application/json" }
});
} else {
// Let other methods pass through
await route.continue();
}
});
await page.goto("https://automationintesting.online/");
await page.waitForSelector(".room");
const roomText = await page.locator(".room").textContent();
expect(roomText).toContain("101");
expect(roomText).toContain("102");
});
Fail + Debug with Trace Viewer (10 min)
Configure Trace Recording
📂 playwright.config.js
import { defineConfig } from "@playwright/test";
export default defineConfig({
use: {
// Debugging configuration
trace: "retain-on-failure", // Keep trace files for failed tests
screenshot: "only-on-failure", // Take screenshots only when tests fail
video: "retain-on-failure", // Record videos only for failed tests
// Additional debugging options
headless: false, // Run in headed mode to see what's happening
slowMo: 1000, // Slow down operations by 1 second
},
// Test configuration
retries: 2, // Retry failed tests twice
timeout: 30000, // 30 second timeout per test
// Reporter configuration
reporter: [
['html'], // HTML report with screenshots and videos
['json', { outputFile: 'test-results.json' }], // JSON report
],
});
Create Failing Test for Debugging
📂 tests/debug-failing-test.spec.js
import { test, expect } from "@playwright/test";
test("Failing test for debugging with trace", async ({ page }) => {
// Step 1: Set up route interception
await page.route("**/booking", async route => {
// Intentionally return invalid data to cause failure
const invalidResponse = {
bookings: [
{
firstname: "Debug",
lastname: "Test",
roomid: "invalid", // This might cause UI issues
checkin: "invalid-date", // Invalid date format
checkout: "invalid-date"
}
]
};
await route.fulfill({
status: 200,
body: JSON.stringify(invalidResponse),
headers: { "Content-Type": "application/json" }
});
});
// Step 2: Navigate to website
await page.goto("https://automationintesting.online/");
await page.waitForLoadState("networkidle");
// Step 3: Wait for bookings (this might fail)
await page.waitForSelector(".bookings", { timeout: 5000 });
// Step 4: This assertion will likely fail
const bookingText = await page.locator(".bookings").textContent();
expect(bookingText).toContain("ValidBooking"); // This will fail
// Step 5: Take screenshot for debugging
await page.screenshot({ path: 'debug-screenshot.png' });
});
Run and Debug
# Run the failing test npx playwright test tests/debug-failing-test.spec.js --trace on # Open trace viewer to debug npx playwright show-trace test-results/chromium/debug-failing-test.spec.js-trace.zip # Open HTML report npx playwright show-report
- Network Tab: See intercepted requests and responses
- Console Tab: Check for JavaScript errors
- Timeline: See exactly when the test failed
- DOM Snapshot: View page state at failure point
Visual Test for Intercepted Data (10 min)
📂 tests/visual-intercepted-data.spec.js
import { test, expect } from "@playwright/test";
test("Visual Snapshot of fake booking data", async ({ page }) => {
// Step 1: Create fake booking response
const fakeResponse = {
bookings: [
{
firstname: "Visual",
lastname: "Check",
roomid: 2,
checkin: "2025-09-20",
checkout: "2025-09-22",
totalprice: 200,
depositpaid: true,
additionalneeds: "WiFi + Breakfast"
},
{
firstname: "Test",
lastname: "User",
roomid: 3,
checkin: "2025-09-25",
checkout: "2025-09-27",
totalprice: 300,
depositpaid: false,
additionalneeds: "Parking"
}
]
};
// Step 2: Intercept API call
await page.route("**/booking", route =>
route.fulfill({
status: 200,
body: JSON.stringify(fakeResponse),
headers: { "Content-Type": "application/json" }
})
);
// Step 3: Navigate and wait for data
await page.goto("https://automationintesting.online/");
await page.waitForSelector(".bookings");
// Step 4: Wait for animations to complete
await page.waitForTimeout(1000);
// Step 5: Take visual snapshot
await expect(page).toHaveScreenshot("fake-booking-visual.png");
// Step 6: Also take snapshot of just the bookings section
const bookingsSection = page.locator(".bookings");
await expect(bookingsSection).toHaveScreenshot("bookings-section-only.png");
});
test("Visual test with different fake data", async ({ page }) => {
// Different fake data for comparison
const differentFakeResponse = {
bookings: [
{
firstname: "Different",
lastname: "Data",
roomid: 1,
checkin: "2025-10-01",
checkout: "2025-10-05"
}
]
};
await page.route("**/booking", route =>
route.fulfill({
status: 200,
body: JSON.stringify(differentFakeResponse),
headers: { "Content-Type": "application/json" }
})
);
await page.goto("https://automationintesting.online/");
await page.waitForSelector(".bookings");
await page.waitForTimeout(1000);
// This will create a different baseline
await expect(page).toHaveScreenshot("different-fake-booking.png");
});
- Consistent Screenshots: Same fake data = same visual output
- Predictable Results: No random data affecting visual comparisons
- Edge Case Testing: Test specific UI states easily
- Regression Prevention: Catch visual changes in UI components
🧑💻 Interactive Questions & Answers
1. What's the difference between `route.fulfill()` and `route.continue()`?
Answer:
- route.fulfill(): Replaces the request with your own response
- route.continue(): Lets the original request proceed normally
- Use fulfill() when: You want to mock/fake the response
- Use continue() when: You want to let the real API handle the request
- Example:
// Mock the response await route.fulfill({ status: 200, body: JSON.stringify({ fake: "data" }) }); // Let real API handle it await route.continue();
2. Why call `page.request.fetch(route.request())` before fulfilling?
Answer:
- Get Original Response: See what the real API would return
- Debugging: Compare real vs fake responses
- Partial Mocking: Modify only specific fields from real response
- Learning: Understand the real API structure
- Example:
await page.route("**/booking", async route => { // Get real response first const realResponse = await page.request.fetch(route.request()); const realData = await realResponse.json(); // Modify only specific fields realData.bookings[0].firstname = "Modified"; // Return modified response await route.fulfill({ body: JSON.stringify(realData) }); });
3. How does mocking API help when backend is unstable?
Answer:
- Consistent Testing: Same data every time, no random failures
- Faster Execution: No waiting for slow or down APIs
- Isolated Testing: Test UI logic without backend dependencies
- Development Phase: Test UI before backend is ready
- Edge Cases: Test specific scenarios easily
- Example:
// When backend is down, mock successful response await page.route("**/booking", route => { route.fulfill({ status: 200, body: JSON.stringify({ bookings: [] }) }); }); // Test continues normally even if backend is down
4. Can we mock partial response fields instead of full response?
Answer:
- Yes, absolutely! You can modify only specific fields
- Get Real Response: Fetch original data first
- Modify Fields: Change only what you need to test
- Return Modified: Send back the modified response
- Example:
await page.route("**/booking", async route => { // Get real response const realResponse = await page.request.fetch(route.request()); const realData = await realResponse.json(); // Modify only specific fields if (realData.bookings && realData.bookings.length > 0) { realData.bookings[0].firstname = "Modified Name"; realData.bookings[0].totalprice = 999; } // Return modified response await route.fulfill({ body: JSON.stringify(realData) }); });
5. What happens if your fake response doesn't match UI schema?
Answer:
- UI Errors: JavaScript errors if UI expects specific fields
- Display Issues: Missing or broken UI elements
- Test Failures: Assertions might fail unexpectedly
- Debugging Needed: Use trace viewer to see what went wrong
- Best Practice: Match the real API structure exactly
- Example:
// Bad: Missing required fields const badResponse = { firstname: "John" }; // Missing lastname, roomid, etc. // Good: Complete response structure const goodResponse = { bookings: [{ firstname: "John", lastname: "Doe", roomid: 1, checkin: "2025-09-20", checkout: "2025-09-25", totalprice: 150, depositpaid: true, additionalneeds: "WiFi" }] };
6. How do you handle authentication in route interception?
Answer:
- Check Headers: Verify authentication tokens in request headers
- Mock Auth Responses: Return fake auth data when needed
- Bypass Auth: Skip authentication for testing purposes
- Example:
await page.route("**/booking", async route => { const headers = route.request().headers(); const authToken = headers['authorization']; if (!authToken) { // Return unauthorized response await route.fulfill({ status: 401, body: JSON.stringify({ error: "Unauthorized" }) }); } else { // Return fake booking data await route.fulfill({ status: 200, body: JSON.stringify({ bookings: [] }) }); } });
7. How do you test error scenarios with route interception?
Answer:
- Mock Error Responses: Return 404, 500, or other error status codes
- Test Error Handling: Verify UI handles errors gracefully
- Network Failures: Simulate network timeouts or failures
- Example:
// Test 404 error await page.route("**/booking", route => { route.fulfill({ status: 404, body: JSON.stringify({ error: "Not found" }) }); }); // Test 500 error await page.route("**/booking", route => { route.fulfill({ status: 500, body: JSON.stringify({ error: "Internal server error" }) }); }); // Test network timeout await page.route("**/booking", route => { route.abort('failed'); // Simulate network failure });
8. How do you organize route interception in large test suites?
Answer:
- Utility Functions: Create reusable route setup functions
- Test Fixtures: Use Playwright fixtures for common setups
- Configuration Files: Store mock data in separate files
- Example:
// utils/route-helpers.js export async function setupBookingRoutes(page, mockData) { await page.route("**/booking", route => { route.fulfill({ status: 200, body: JSON.stringify(mockData) }); }); } // In test file import { setupBookingRoutes } from '../utils/route-helpers.js'; test("My test", async ({ page }) => { const mockData = { bookings: [] }; await setupBookingRoutes(page, mockData); // ... rest of test });
🎯 Day 10 Homework Tasks
🟢 Beginner
Intercept GET /api/room → show single fake room with your custom description and verify it appears in UI.
Assert UI displays Playwright Test Room by intercepting the room API call and providing fake room data.
🟡 Intermediate
Create a room booking via POST /api/booking → validate room data appears in UI (complete hybrid test with proper error handling).
Fail test on purpose by providing invalid room mock data → debug with Trace Viewer and identify the issue.
🔴 Advanced
Write test that intercepts GET /api/room and injects 5 fake rooms dynamically from an array with different room types and prices.
Add visual snapshot test for the new fake rooms list and create a utility function for reusable route interception.
Best Practices & Tips
- Always match the real API response structure exactly
- Use meaningful fake data that represents real scenarios
- Test both success and error scenarios with mocked responses
- Clean up route interceptions after each test
- Use descriptive names for mock data and responses
- Don't forget to handle CORS headers in mocked responses
- Avoid hardcoded mock data - use variables for reusability
- Don't mock everything - let some real API calls pass through
- Be careful with async/await in route handlers
- Don't ignore error handling in route interception
- Use API calls for data setup and validation
- Use UI testing for user interaction flows
- Combine both approaches for comprehensive coverage
- Test data consistency between API and UI
- Use route interception to test edge cases and error scenarios
✅ Outcomes
- Understand Playwright routes & API interception concepts
- Fake API responses to test UI rendering with controlled data
- Run hybrid API + UI validation tests
- Debug failures with Trace Viewer + screenshots + videos
- Add Visual Testing for intercepted responses
- Handle authentication and error scenarios in route interception
- Organize and maintain route interception in large test suites
- Apply best practices for reliable and maintainable test automation