Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

This repository holds the demo code for Checkly tutorials and videos.

## Intercept and Mock HTTP Requests with page.route()

### 🧑‍💻 [Code](/request-mocking)

## How to add Type Checking and Linting to your Playwright Project

![types-checking-social](https://github.com/user-attachments/assets/b86dbc82-65a9-4a2e-b7ce-3a20177136ab)
Expand Down
7 changes: 5 additions & 2 deletions project-setup-and-storage-state/tests/session.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ const AUTH_FILE = ".auth/user.json"

setup("authenticate", async ({ page }) => {
await page.goto("https://app.checklyhq.com")
if (!process.env.USER || !process.env.PW) {
throw new Error("Environment variables USER and PW must be set before running setup.")
}
await page
.getByPlaceholder("yours@example.com")
.fill(process.env.USER as string)
await page.getByPlaceholder("your password").fill(process.env.PW as string)
.fill(process.env.USER)
await page.getByPlaceholder("your password").fill(process.env.PW)
await page.getByLabel("Log In").click()
await expect(page.getByLabel("Home")).toBeVisible()
await page.context().storageState({ path: AUTH_FILE })
Expand Down
6 changes: 6 additions & 0 deletions request-mocking/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Playwright
node_modules/
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
97 changes: 97 additions & 0 deletions request-mocking/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions request-mocking/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "request-mocking",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {},
"keywords": [],
"author": "",
"license": "MIT",
"type": "commonjs",
"devDependencies": {
"@playwright/test": "^1.54.1",
"@types/node": "^24.1.0"
}
}
31 changes: 31 additions & 0 deletions request-mocking/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { defineConfig, devices } from "@playwright/test"

/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: "./tests",
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
},

/* Configure projects for major browsers */
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
],
})
94 changes: 94 additions & 0 deletions request-mocking/tests/abort.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { expect, test } from "@playwright/test"

/**
* route.abort() drops a request entirely — the browser receives a network
* error for that request. Use it to block unwanted traffic or test fallbacks.
*/

test("block analytics calls so tests don't pollute real data", async ({ page }) => {
const blockedRequests: string[] = []

await page.route("**/analytics/**", async (route) => {
blockedRequests.push(route.request().url())
await route.abort()
})

await page.setContent(`
<button id="purchase">Buy now</button>
<p id="confirmation" hidden>Order confirmed!</p>
<script>
document.getElementById('purchase').onclick = async () => {
fetch('http://analytics.test/analytics/events', {
method: 'POST',
body: JSON.stringify({ event: 'purchase_clicked' })
}).catch(() => {})

document.getElementById('confirmation').hidden = false
}
</script>
`)

await page.getByRole("button", { name: "Buy now" }).click()

await expect(page.locator("#confirmation")).toBeVisible()
expect(blockedRequests.length).toBe(1)
expect(blockedRequests[0]).toContain("/analytics/events")
})

test("block image requests to focus on page text structure", async ({ page }) => {
let imageRequestCount = 0

await page.route("**/*.{png,jpg,jpeg,gif,webp,svg}", async (route) => {
imageRequestCount++
await route.abort()
})

await page.setContent(`
<h1>Product Catalog</h1>
<article>
<img src="http://cdn.test/images/product-1.jpg" alt="Wireless headphones" />
<h2>Wireless Headphones</h2>
<p>Premium sound quality</p>
</article>
<article>
<img src="http://cdn.test/images/product-2.png" alt="Mechanical keyboard" />
<h2>Mechanical Keyboard</h2>
<p>Tactile typing experience</p>
</article>
`)

await expect(page.getByRole("heading", { name: "Product Catalog" })).toBeVisible()
await expect(page.getByRole("heading", { name: "Wireless Headphones" })).toBeVisible()
await expect(page.getByRole("heading", { name: "Mechanical Keyboard" })).toBeVisible()

expect(imageRequestCount).toBe(2)
})

test("test app behavior when a required service is unavailable", async ({ page }) => {
await page.route("**/api/feature-flags", async (route) => {
await route.abort()
})

await page.setContent(`
<div id="app">Loading...</div>
<script>
async function init() {
let flags = { newDashboard: false }

try {
const res = await fetch('http://mock.test/api/feature-flags')
flags = await res.json()
} catch {
// Feature flag service is down — fall back to safe defaults
}

const app = document.getElementById('app')
app.textContent = flags.newDashboard ? 'New Dashboard (beta)' : 'Classic Dashboard'
}

init()
</script>
`)

await expect(page.locator("#app")).toHaveText("Classic Dashboard")
})
118 changes: 118 additions & 0 deletions request-mocking/tests/fulfill.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { expect, test } from "@playwright/test"

/**
* route.fulfill() returns a completely synthetic response — Playwright never
* contacts the real server. Use it to control exactly what the page receives.
*
* Note: fetch() calls in page.setContent() pages require absolute URLs since
* there is no HTTP origin to resolve relative paths against.
*/

test("mock a JSON API response", async ({ page }) => {
await page.route("**/api/user", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ name: "Ada Lovelace", role: "admin" }),
})
})

await page.setContent(`
<button id="load">Load user</button>
<p id="name"></p>
<p id="role"></p>
<script>
document.getElementById('load').onclick = async () => {
const res = await fetch('http://mock.test/api/user')
const data = await res.json()
document.getElementById('name').textContent = data.name
document.getElementById('role').textContent = data.role
}
</script>
`)

await page.getByRole("button", { name: "Load user" }).click()

await expect(page.locator("#name")).toHaveText("Ada Lovelace")
await expect(page.locator("#role")).toHaveText("admin")
})

test("simulate a server error to test error handling UI", async ({ page }) => {
await page.route("**/api/products", async (route) => {
await route.fulfill({
status: 500,
contentType: "application/json",
body: JSON.stringify({ error: "Internal Server Error" }),
})
})

await page.setContent(`
<button id="load">Load products</button>
<ul id="list"></ul>
<p id="error" hidden>Something went wrong. Please try again.</p>
<script>
document.getElementById('load').onclick = async () => {
const res = await fetch('http://mock.test/api/products')
if (!res.ok) {
document.getElementById('error').hidden = false
return
}
const products = await res.json()
const list = document.getElementById('list')
products.forEach(p => {
const li = document.createElement('li')
li.textContent = p.name
list.appendChild(li)
})
}
</script>
`)

await page.getByRole("button", { name: "Load products" }).click()

await expect(page.locator("#error")).toBeVisible()
await expect(page.locator("#list li")).toHaveCount(0)
})

test("use a glob pattern to mock multiple API endpoints at once", async ({ page }) => {
await page.route("**/api/**", async (route) => {
const url = route.request().url()

if (url.includes("/api/user")) {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ name: "Grace Hopper" }),
})
} else if (url.includes("/api/settings")) {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ theme: "dark" }),
})
} else {
await route.fulfill({ status: 404 })
}
})

await page.setContent(`
<button id="load">Load dashboard</button>
<p id="username"></p>
<p id="theme"></p>
<script>
document.getElementById('load').onclick = async () => {
const [user, settings] = await Promise.all([
fetch('http://mock.test/api/user').then(r => r.json()),
fetch('http://mock.test/api/settings').then(r => r.json()),
])
document.getElementById('username').textContent = user.name
document.getElementById('theme').textContent = settings.theme
}
</script>
`)

await page.getByRole("button", { name: "Load dashboard" }).click()

await expect(page.locator("#username")).toHaveText("Grace Hopper")
await expect(page.locator("#theme")).toHaveText("dark")
})
Loading