In this example we are going to explore how to create custom assertion in playwright by extending expect. Creating custom assertions in Playwright is useful when built-in ones don’t cover your specific case. Since Playwright uses Jest-style expect from @playwright/test, you can extend it using custom functions, or integrate with tools like expect.extend() for full custom matchers.
Playwright is a powerful tool for end-to-end testing, and it provides built-in assertions to validate your test expectations. These assertions are used to check if elements are visible, contain specific text, are enabled, and more.
Here's a quick overview and example of how assertions work in Playwright.
import { test, expect } from '@playwright/test'; test('basic assertion example', async ({ page }) => { await page.goto('https://example.com'); // Assertion: Page title contains 'Example' await expect(page).toHaveTitle(/Example/); // Assertion: H1 contains text 'Example Domain' const heading = page.locator('h1'); await expect(heading).toHaveText('Example Domain'); });
Here is simple example to create custom assertion by extending expect in playwright.
Extend expect with expect.extend() (Custom Matcher)
Playwright allows using expect.extend() like Jest to create richer matchers:
Example -1
helpers/custom-matchers.ts
// helpers/custom-matchers.ts import { expect } from '@playwright/test'; expect.extend({ async toBeRed(received) { const color = await received.evaluate((el: HTMLElement) => { return getComputedStyle(el).color; }); const pass = color === 'rgb(255, 0, 0)'; return { message: () => `expected element to ${pass ? 'not ' : ''}be red, but got ${color}`, pass, }; } });
import { test } from '@playwright/test'; import './helpers/custom-matchers'; // 👈 import the extension
test('custom matcher example', async ({ page }) => { await page.goto('https://example.com'); const redText = page.locator('#error-text'); await expect(redText).toBeRed(); });
Example -2 (Custom Matcher toHaveItemsCount())
In this example where cart is a locator for a shopping cart list, and we want to assert that it contains exactly 3 items.
helpers/custom-matchers.ts
// helpers/custom-matchers.ts import { expect } from '@playwright/test'; import type { Locator } from '@playwright/test'; expect.extend({ async toHaveItemsCount(received: Locator, expectedCount: number) { const count = await received.count(); const pass = count === expectedCount; return { message: () => `Expected cart to have ${expectedCount} item(s), but found ${count}`, pass, }; }, });
example2.spec.js
// tests/cart.test.ts import { test, expect } from '@playwright/test'; import '../helpers/custom-matchers'; // 👈 import the extension test('cart has correct number of items', async ({ page }) => { await page.goto('https://example.com/cart'); const cartItems = page.locator('.cart-item'); await expect(cartItems).toHaveItemsCount(3); });
Let see the more example for custom assertion that helps you to create more robust validation checks.
let's begin with implementing some custom expects via fixtures. We'll start with toBeValidDate(), The logic on this custom expect is straight forward, take the received data from the expect, and validate that when using Date.parse(received) parses (and doesn't return NaN, which is a falsey value). From there we pass back the details needed when overwriting an expect.
lib/fixtures/toBeValidDate.ts
// lib/fixtures/toBeValidDate.ts import { expect as baseExpect } from "@playwright/test"; export { test } from "@playwright/test"; export const expect = baseExpect.extend({ toBeValidDate(received: any) { const pass = Date.parse(received) && typeof received === "string" ? true : false; if (pass) { return { message: () => "passed", pass: true, }; } else { return { message: () => `toBeValidDate() assertion failed.\nYou expected '${received}' to be a valid date.\n`, pass: false, }; } }, });
Take note I am exporting both test and expect in this fixture in order that I have access to test when utilizing this fixture in my test. This is a decision I made, that you don't have to make in your tests. This does allow me to only have 1 test/expect import, rather than importing test from @playwright/test.
tests/auth/login.post.spec.ts
// If I didn't export test import { expect } from "lib/fixtures/fixtures"; (more on fixtures below) import { test } from "@playwright/test"; // Since I did export test I can do this import { test, expect } from "lib/fixtures/fixtures"; test.describe("auth/login POST requests", async () => { ... test("POST with no body", async ({ request }) => { const response = await request.post(`auth/login`, {}); expect(response.status()).toBe(400); const body = await response.json(); expect(body.timestamp).toBeValidDate(); expect(body.status).toBe(400); expect(body.error).toBe("Bad Request"); expect(body.path).toBe(`/auth/login`); }); });
Similarly, we can create assertions on a large GET items request with multiple arrays that are returned, we can create more generic assertions for multiple values.
lib/fixtures/toBeOneOfValues.ts
import { expect as baseExpect } from "@playwright/test"; export { test } from "@playwright/test"; export const expect = baseExpect.extend({ toBeOneOfValues(received: any, array: any[]) { const pass = array.includes(received); if (pass) { return { message: () => "passed", pass: true, }; } else { return { message: () => `toBeOneOfValues() assertion failed.\nYou expected [${array}] to include '${received}'\n`, pass: false, }; } }, });
The below custom expects makes asserting that the response is the correct type super easy!
lib/fixtures/typesExpects.ts
import { expect as baseExpect } from "@playwright/test"; export { test } from "@playwright/test"; export const expect = baseExpect.extend({ toBeOneOfTypes(received: any, array: string[]) { const pass = array.includes(typeof received) || (array.includes(null) && received == null); if (pass) { return { message: () => "passed", pass: true, }; } else { return { message: () => `toBeOneOfTypes() assertion failed.\nYou expected '${ received == null ? "null" : typeof received }' type to be one of [${array}] types\n${ array.includes(null) ? `WARNING: [${array}] array contains 'null' type which is not printed in the error\n` : null }`, pass: false, }; } }, toBeNumber(received: any) { const pass = typeof received == "number"; if (pass) { return { message: () => "passed", pass: true, }; } else { return { message: () => `toBeNumber() assertion failed.\nYou expected '${received}' to be a number but it's a ${typeof received}\n`, pass: false, }; } }, toBeString(received: any) { const pass = typeof received == "string"; if (pass) { return { message: () => "passed", pass: true, }; } else { return { message: () => `toBeString() assertion failed.\nYou expected '${received}' to be a string but it's a ${typeof received}\n`, pass: false, }; } }, toBeBoolean(received: any) { const pass = typeof received == "boolean"; if (pass) { return { message: () => "passed", pass: true, }; } else { return { message: () => `toBeBoolean() assertion failed.\nYou expected '${received}' to be a boolean but it's a ${typeof received}\n`, pass: false, }; } }, toBeObject(received: any) { const pass = typeof received == "object"; if (pass) { return { message: () => "passed", pass: true, }; } else { return { message: () => `toBeObject() assertion failed.\nYou expected '${received}' to be an object but it's a ${typeof received}\n`, pass: false, }; } }, });
In the below spec you can see all the different custom expects used in one test.
tests/test.spec.ts
// tests/test.spec.ts import { test, expect } from "from "lib/fixtures/fixtures"; // Import the custom matchers definition test.describe("Custom Assertions", async () => { test("with fixtures", async ({ request }) => { const response = await request.post(`auth/login`, {}); expect(response.status()).toBe(400); const body = await response.json(); expect(body.timestamp).toBeValidDate(); const dateStr = "2021-01-01"; expect(dateStr).toBeValidDate(); const number = 123; expect(number).toBeNumber(); const boolean = true; expect(boolean).toBeBoolean(); const string = "string"; expect(string).toBeString(); expect(body.status).toBeOneOfValues([400, 401, 403]); expect(body.status).toBeOneOfTypes(["number", "null"]); }); });
MergeExpects Fixture
If you were paying attention in the above example you probably noticed that I only had 1 import import { test, expect } from "@fixtures/fixtures"; for all the different fixtures we've added. With the 1.39 release, the playwright team introduced an easy way to merge expect.extend and test.extend allowing you to make your imports less verbose and super clean!For our example I created a fixtures.ts file with the below content. I am importing in mergeExpects() which is a new addition with the latest release, along with all the other expect.extend fixtures. I am then creating and exporting a new expect variable setting it equal to the response of mergeExpects(fixture1, fixture2, fixture3, etc). This will create a single fixture that can be imported into all my tests that use these custom assertions.
lib/fixtures/fixtures.ts
// lib/fixtures/fixtures.ts import { mergeExpects } from "@playwright/test"; import { expect as toBeOneOfValuesExpect } from "lib/fixtures/toBeOneOfValues"; import { expect as toBeValidDate } from "lib/fixtures/toBeValidDate"; import { expect as typesExpects } from "lib/fixtures/typesExpects"; export { test } from "@playwright/test"; export const expect = mergeExpects(toBeOneOfValuesExpect, toBeValidDate, typesExpects);
tsconfig.json
// tsconfig.json { "compilerOptions": { "baseUrl": ".", "esModuleInterop": true, "paths": { "@datafactory/*": ["lib/datafactory/*"], "@helpers/*": ["lib/helpers/*"], "@fixtures/*": ["lib/fixtures/*"] } } }
Importing should look like this with the above change
// new import { test, expect } from "@fixtures/fixtures"; // old import { test, expect } from "lib/fixtures/fixtures";
This is all about creating a simple custom assertion playwright.
No comments:
Post a Comment