You know the APIs. Now let us put them together for real scenarios you will face in every project. Loading states, pagination, empty results, rate limiting, file downloads. These are the patterns that separate a good test suite from a great one.
import { test, expect } from '@playwright/test';
test('loading spinner appears and disappears', async ({ page }) => {
// Delay the response so we can see the spinner
await page.route('**/api/products', async (route) => {
await new Promise((resolve) => setTimeout(resolve, 2000));
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([{ id: 1, name: 'Product A' }]),
});
});
await page.goto('/shopping');
// Spinner should be visible immediately
await expect(page.getByTestId('loading-spinner')).toBeVisible();
// After API responds, spinner disappears and data shows
await expect(page.getByTestId('loading-spinner')).toBeHidden({ timeout: 5000 });
await expect(page.getByText('Product A')).toBeVisible();
});test('paginated product list', async ({ page }) => {
// Page 1
await page.route('**/api/products?page=1*', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
products: Array.from({ length: 10 }, (_, i) => ({
id: i + 1,
name: `Product ${i + 1}`,
})),
totalPages: 3,
currentPage: 1,
}),
});
});
// Page 2
await page.route('**/api/products?page=2*', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
products: Array.from({ length: 10 }, (_, i) => ({
id: i + 11,
name: `Product ${i + 11}`,
})),
totalPages: 3,
currentPage: 2,
}),
});
});
await page.goto('/shopping?page=1');
// Verify page 1 data
await expect(page.getByText('Product 1')).toBeVisible();
await expect(page.getByText('Product 10')).toBeVisible();
// Navigate to page 2
await page.getByRole('button', { name: 'Next' }).click();
// Verify page 2 data
await expect(page.getByText('Product 11')).toBeVisible();
});test('empty search results', async ({ page }) => {
await page.route('**/api/products*', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ products: [], total: 0 }),
});
});
await page.goto('/shopping');
await page.getByPlaceholder('Search').fill('xyznonexistent');
await page.keyboard.press('Enter');
// Empty state UI should appear
await expect(page.getByText(/no products found|no results/i)).toBeVisible();
await expect(page.getByRole('button', { name: /clear filter/i })).toBeVisible();
});
test('empty cart', async ({ page }) => {
await page.route('**/api/cart', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ items: [], total: 0 }),
});
});
await page.goto('/shopping/cart');
await expect(page.getByText(/cart is empty/i)).toBeVisible();
await expect(page.getByRole('link', { name: /continue shopping/i })).toBeVisible();
});test('show rate limit message on 429', async ({ page }) => {
await page.route('**/api/products*', async (route) => {
await route.fulfill({
status: 429,
contentType: 'application/json',
headers: {
'Retry-After': '60',
},
body: JSON.stringify({
error: 'Too many requests',
retryAfter: 60,
}),
});
});
await page.goto('/shopping');
await expect(page.getByText(/too many requests|slow down/i)).toBeVisible();
});test('block images and analytics for faster tests', async ({ page }) => {
// Block images
await page.route('**/*.{png,jpg,jpeg,gif,svg,webp}', (route) =>
route.abort()
);
// Block analytics and tracking
await page.route('**/*analytics*', (route) => route.abort());
await page.route('**/*tracking*', (route) => route.abort());
await page.route('**/google-analytics.com/**', (route) => route.abort());
// Block fonts (if not testing typography)
await page.route('**/*.{woff,woff2,ttf}', (route) => route.abort());
await page.goto('/shopping');
// Page loads much faster without these resources
});test('mock a PDF download', async ({ page }) => {
await page.route('**/api/invoice/download', async (route) => {
await route.fulfill({
status: 200,
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': 'attachment; filename="invoice-001.pdf"',
},
body: Buffer.from('fake-pdf-content'),
});
});
const [download] = await Promise.all([
page.waitForEvent('download'),
page.getByRole('button', { name: 'Download Invoice' }).click(),
]);
expect(download.suggestedFilename()).toBe('invoice-001.pdf');
});Create a test utility file (e.g., tests/helpers/mock-api.ts) with reusable functions like mockProducts(), mockEmptyCart(), mockServerError(). Your team will thank you.
Key Point: These patterns cover 90% of real-world mocking needs: loading states, pagination, empty states, rate limiting, blocking resources, and file downloads.