+ Wireless Headphones
+Premium sound quality
+diff --git a/README.md b/README.md index 08da410..b726865 100644 --- a/README.md +++ b/README.md @@ -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  diff --git a/project-setup-and-storage-state/tests/session.setup.ts b/project-setup-and-storage-state/tests/session.setup.ts index 7d912d9..1955676 100644 --- a/project-setup-and-storage-state/tests/session.setup.ts +++ b/project-setup-and-storage-state/tests/session.setup.ts @@ -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 }) diff --git a/request-mocking/.gitignore b/request-mocking/.gitignore new file mode 100644 index 0000000..ae383f1 --- /dev/null +++ b/request-mocking/.gitignore @@ -0,0 +1,6 @@ +# Playwright +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/request-mocking/package-lock.json b/request-mocking/package-lock.json new file mode 100644 index 0000000..de37b8f --- /dev/null +++ b/request-mocking/package-lock.json @@ -0,0 +1,97 @@ +{ + "name": "request-mocking", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "request-mocking", + "version": "1.0.0", + "license": "MIT", + "devDependencies": { + "@playwright/test": "^1.54.1", + "@types/node": "^24.1.0" + } + }, + "node_modules/@playwright/test": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", + "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "24.12.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz", + "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/request-mocking/package.json b/request-mocking/package.json new file mode 100644 index 0000000..ef0f5cf --- /dev/null +++ b/request-mocking/package.json @@ -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" + } +} diff --git a/request-mocking/playwright.config.ts b/request-mocking/playwright.config.ts new file mode 100644 index 0000000..bc9244d --- /dev/null +++ b/request-mocking/playwright.config.ts @@ -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"] }, + }, + ], +}) diff --git a/request-mocking/tests/abort.spec.ts b/request-mocking/tests/abort.spec.ts new file mode 100644 index 0000000..e7be145 --- /dev/null +++ b/request-mocking/tests/abort.spec.ts @@ -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(` + +
Order confirmed!
+ + `) + + 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(` +
+ Premium sound quality
+
+ Tactile typing experience
+Something went wrong. Please try again.
+ + `) + + 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(` + + + + + `) + + await page.getByRole("button", { name: "Load dashboard" }).click() + + await expect(page.locator("#username")).toHaveText("Grace Hopper") + await expect(page.locator("#theme")).toHaveText("dark") +}) diff --git a/request-mocking/tests/intercept.spec.ts b/request-mocking/tests/intercept.spec.ts new file mode 100644 index 0000000..ec6231a --- /dev/null +++ b/request-mocking/tests/intercept.spec.ts @@ -0,0 +1,138 @@ +import { expect, test } from "@playwright/test" + +/** + * page.waitForRequest() and page.waitForResponse() let you capture and assert + * on requests as they happen. A route handler also gives you access to the + * full request object — url, method, headers, and body — before fulfilling. + */ + +test("inspect request details inside a route handler", async ({ page }) => { + let capturedMethod = "" + let capturedHeader = "" + + await page.route("**/api/profile", async (route) => { + capturedMethod = route.request().method() + capturedHeader = route.request().headers()["x-api-version"] ?? "" + + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ name: "Margaret Hamilton" }), + }) + }) + + await page.setContent(` + + + + `) + + await page.getByRole("button", { name: "Load profile" }).click() + + await expect(page.locator("#name")).toHaveText("Margaret Hamilton") + expect(capturedMethod).toBe("GET") + expect(capturedHeader).toBe("2") +}) + +test("use waitForRequest to assert what the page sends to the server", async ({ page }) => { + await page.route("**/api/search**", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ results: ["Playwright", "Puppeteer", "Cypress"] }), + }) + }) + + await page.setContent(` + + +