Test Annotations & Organization
Table of Contents
- Skip Annotations
- Fixme & Fail Annotations
- Slow Tests
- Test Steps
- Custom Annotations
- Conditional Annotations
Skip Annotations
Basic Skip
// Skip unconditionally
test.skip("feature not implemented", async ({ page }) => {
// This test won't run
});
// Skip with reason
test("payment flow", async ({ page }) => {
test.skip(true, "Payment gateway in maintenance");
// Test body won't execute
});
Conditional Skip
test("webkit-specific feature", async ({ page, browserName }) => {
test.skip(browserName !== "webkit", "This feature only works in WebKit");
await page.goto("/webkit-feature");
});
test("production only", async ({ page }) => {
test.skip(process.env.ENV !== "production", "Only runs against production");
await page.goto("/prod-feature");
});
Skip by Platform
test("windows-specific", async ({ page }) => {
test.skip(process.platform !== "win32", "Windows only");
});
test("not on CI", async ({ page }) => {
test.skip(!!process.env.CI, "Skipped in CI environment");
});
Skip Describe Block
test.describe("Admin features", () => {
test.skip(
({ browserName }) => browserName === "firefox",
"Firefox admin bug",
);
test("admin dashboard", async ({ page }) => {
// Skipped in Firefox
});
test("admin settings", async ({ page }) => {
// Skipped in Firefox
});
});
Fixme & Fail Annotations
Fixme - Known Issues
// Mark test as needing fix (skips the test)
test.fixme("broken after refactor", async ({ page }) => {
// Test won't run but is tracked
});
// Conditional fixme
test("flaky on CI", async ({ page }) => {
test.fixme(!!process.env.CI, "Investigate CI flakiness - ticket #123");
await page.goto("/flaky-feature");
});
Fail - Expected Failures
// Test is expected to fail (runs but expects failure)
test("known bug", async ({ page }) => {
test.fail();
await page.goto("/buggy-page");
// If this passes, the test fails (bug was fixed!)
await expect(page.getByText("Working")).toBeVisible();
});
// Conditional fail
test("fails on webkit", async ({ page, browserName }) => {
test.fail(browserName === "webkit", "WebKit rendering bug #456");
await page.goto("/render-test");
await expect(page.getByTestId("element")).toHaveCSS("width", "100px");
});
Difference Between Skip, Fixme, Fail
| Annotation | Runs? | Use Case |
|---|
test.skip() | No | Feature not applicable |
test.fixme() | No | Known bug, needs investigation |
test.fail() | Yes | Expected to fail, tracking a bug |
Slow Tests
Mark Slow Tests
// Triple the default timeout
test("large data import", async ({ page }) => {
test.slow();
await page.goto("/import");
await page.setInputFiles("#file", "large-file.csv");
await page.getByRole("button", { name: "Import" }).click();
await expect(page.getByText("Import complete")).toBeVisible();
});
// Conditional slow
test("video processing", async ({ page, browserName }) => {
test.slow(browserName === "webkit", "WebKit video processing is slow");
await page.goto("/video-editor");
});
Custom Timeout
test("very long operation", async ({ page }) => {
// Set specific timeout (in milliseconds)
test.setTimeout(120000); // 2 minutes
await page.goto("/long-operation");
});
// Timeout for describe block
test.describe("Integration tests", () => {
test.describe.configure({ timeout: 60000 });
test("test 1", async ({ page }) => {
// Has 60 second timeout
});
});
Test Steps
Basic Steps
test("checkout flow", async ({ page }) => {
await test.step("Add item to cart", async () => {
await page.goto("/products");
await page.getByRole("button", { name: "Add to Cart" }).click();
});
await test.step("Go to checkout", async () => {
await page.getByRole("link", { name: "Cart" }).click();
await page.getByRole("button", { name: "Checkout" }).click();
});
await test.step("Fill shipping info", async () => {
await page.getByLabel("Address").fill("123 Test St");
await page.getByLabel("City").fill("Test City");
});
await test.step("Complete payment", async () => {
await page.getByLabel("Card").fill("4242424242424242");
await page.getByRole("button", { name: "Pay" }).click();
});
await expect(page.getByText("Order confirmed")).toBeVisible();
});
Nested Steps
test("user registration", async ({ page }) => {
await test.step("Fill registration form", async () => {
await page.goto("/register");
await test.step("Personal info", async () => {
await page.getByLabel("Name").fill("John Doe");
await page.getByLabel("Email").fill("[email protected]");
});
await test.step("Security", async () => {
await page.getByLabel("Password").fill("SecurePass123");
await page.getByLabel("Confirm Password").fill("SecurePass123");
});
});
await test.step("Submit and verify", async () => {
await page.getByRole("button", { name: "Register" }).click();
await expect(page.getByText("Welcome")).toBeVisible();
});
});
Steps with Return Values
test("verify order", async ({ page }) => {
const orderId = await test.step("Create order", async () => {
await page.goto("/checkout");
await page.getByRole("button", { name: "Place Order" }).click();
// Return value from step
return await page.getByTestId("order-id").textContent();
});
await test.step("Verify order details", async () => {
await page.goto(`/orders/${orderId}`);
await expect(page.getByText(`Order #${orderId}`)).toBeVisible();
});
});
Step in Page Object
// pages/checkout.page.ts
export class CheckoutPage {
async fillShippingInfo(address: string, city: string) {
await test.step("Fill shipping information", async () => {
await this.page.getByLabel("Address").fill(address);
await this.page.getByLabel("City").fill(city);
});
}
async completePayment(cardNumber: string) {
await test.step("Complete payment", async () => {
await this.page.getByLabel("Card").fill(cardNumber);
await this.page.getByRole("button", { name: "Pay" }).click();
});
}
}
Custom Annotations
Add Annotations
test("important feature", async ({ page }, testInfo) => {
// Add custom annotation
testInfo.annotations.push({
type: "priority",
description: "high",
});
testInfo.annotations.push({
type: "ticket",
description: "JIRA-123",
});
await page.goto("/feature");
});
Annotation Fixture
// fixtures/annotations.fixture.ts
import { test as base, TestInfo } from "@playwright/test";
type AnnotationFixtures = {
annotate: {
ticket: (id: string) => void;
priority: (level: "low" | "medium" | "high") => void;
owner: (name: string) => void;
};
};
export const test = base.extend<AnnotationFixtures>({
annotate: async ({}, use, testInfo) => {
await use({
ticket: (id) => {
testInfo.annotations.push({ type: "ticket", description: id });
},
priority: (level) => {
testInfo.annotations.push({ type: "priority", description: level });
},
owner: (name) => {
testInfo.annotations.push({ type: "owner", description: name });
},
});
},
});
// Usage
test("critical feature", async ({ page, annotate }) => {
annotate.ticket("JIRA-456");
annotate.priority("high");
annotate.owner("Alice");
await page.goto("/critical");
});
Read Annotations in Reporter
// reporters/annotation-reporter.ts
import { Reporter, TestCase, TestResult } from "@playwright/test/reporter";
class AnnotationReporter implements Reporter {
onTestEnd(test: TestCase, result: TestResult) {
const ticket = test.annotations.find((a) => a.type === "ticket");
const priority = test.annotations.find((a) => a.type === "priority");
if (ticket) {
console.log(`Test linked to: ${ticket.description}`);
}
if (priority?.description === "high" && result.status === "failed") {
console.log(`HIGH PRIORITY FAILURE: ${test.title}`);
}
}
}
export default AnnotationReporter;
Conditional Annotations
Annotation Helper
// helpers/test-annotations.ts
import { test } from "@playwright/test";
export function skipInCI(reason = "Skipped in CI") {
test.skip(!!process.env.CI, reason);
}
export function skipInBrowser(browser: string, reason: string) {
test.beforeEach(({ browserName }) => {
test.skip(browserName === browser, reason);
});
}
export function onlyInEnv(env: string) {
test.skip(process.env.ENV !== env, `Only runs in ${env}`);
}
// tests/feature.spec.ts
import { skipInCI, onlyInEnv } from "../helpers/test-annotations";
test("local only feature", async ({ page }) => {
skipInCI("Uses local resources");
await page.goto("/local-feature");
});
test("production check", async ({ page }) => {
onlyInEnv("production");
await page.goto("/prod-only");
});
Describe-Level Conditions
test.describe("Mobile features", () => {
test.beforeEach(({ isMobile }) => {
test.skip(!isMobile, "Mobile only tests");
});
test("touch gestures", async ({ page }) => {
// Only runs on mobile
});
});
test.describe("Desktop features", () => {
test.beforeEach(({ isMobile }) => {
test.skip(isMobile, "Desktop only tests");
});
test("hover interactions", async ({ page }) => {
// Only runs on desktop
});
});
Anti-Patterns to Avoid
| Anti-Pattern | Problem | Solution |
|---|
| Skipping without reason | Hard to track why | Always provide description |
| Too many skipped tests | Test debt accumulates | Review and clean up regularly |
| Using skip instead of fixme | Loses intent | Use fixme for bugs, skip for N/A |
| Not using steps | Hard to debug failures | Group logical actions in steps |
Related References