PRACTICAL GUIDE / playwright tags and annotations
Operational Test Metadata with Playwright Tags, Annotations, and TestInfo
Use Playwright tags, annotations, TestInfo, and attachments to route suites, record runtime context, preserve artifacts, and improve reporter output.
In this guide13 sections
- Define a Metadata Contract Before Adding Labels
- Use Tags for Selection
- Make Grep Expressions Reviewable
- Use Annotations for Explanatory Context
- Add Runtime Annotations Only When Context Emerges
- Use TestInfo as the Artifact Boundary
- Capture Failure Evidence with an Automatic Fixture
- Use Built-In Status Semantics Correctly
- Design Reporter Consumption Deliberately
- Diagnose Metadata Failures
- Balance Rich Context Against Noise
- Operational Checklist
- Conclusion: Treat Metadata as a Shared Interface
What you will learn
- Define a Metadata Contract Before Adding Labels
- Use Tags for Selection
- Make Grep Expressions Reviewable
- Use Annotations for Explanatory Context
Test metadata is operational infrastructure. It decides which Playwright tests run in a pull request, who receives a failed build, which requirement a result supports, and what evidence survives in the report. Tags, annotations, and attachments only help when their meanings are stable enough for people and automation to trust.
A useful metadata contract separates selection from explanation. Tags provide a small filter vocabulary. Annotations preserve typed context. TestInfo contributes runtime identity and safe artifact paths. Reporters consume those values; they should not have to reverse-engineer meaning from decorative title text.
Define a Metadata Contract Before Adding Labels
Start with the questions your pipeline must answer. Typical dimensions are product area, test tier, risk class, and owner. Keep each vocabulary short: @checkout, @smoke, @p0, and @team-payments are understandable if their definitions live in one contributor document and CI uses the same tokens.
Do not tag implementation details that can be read from the project, file path, or browser name. Avoid synonyms such as @critical, @priority-zero, and @p0 for one concept. Every extra spelling weakens filters and dashboards.
The official Playwright annotations guide requires tags to begin with @ and supports tags and annotations in test or group details. That declarative form is easier for discovery and reporting than mutating metadata late without need.
Animated field map
Operational Metadata Through Playwright
Ownership and risk metadata select execution, gain runtime context, collect evidence, and reach the final reporter.
01 / ownership metadata
Ownership Metadata
Declare product area, risk, owner, and requirement using governed values.
02 / tag filtering
Tag Filtering
Select an intentional test subset with reviewed grep expressions.
03 / runtime annotation
Runtime Annotation
Record context that becomes known only during fixture or test execution.
04 / artifact attachment
Artifact Attachment
Preserve bounded, sanitized diagnostics under a unique test output path.
05 / reporter output
Reporter Output
Expose routing context and evidence to humans and CI integrations.
Use Tags for Selection
Declare tags in the details object so they remain separate from the human title. A test can have several orthogonal tags, but each should answer a real filtering question. Group-level tags are useful when every child test belongs to the same area.
import { expect, test } from "@playwright/test";
test.describe("refunds", { tag: ["@payments", "@refunds"] }, () => {
test("an operator can approve a pending refund", {
tag: ["@smoke", "@p0"],
}, async ({ page }) => {
await page.goto("/refunds/RF-1042");
await page.getByRole("button", { name: "Approve refund" }).click();
await expect(page.getByRole("status")).toHaveText("Refund approved");
});
});The child receives useful suite metadata without repeating it in every title. Keep the business title readable when tags are rendered beside it in an HTML report.
Make Grep Expressions Reviewable
--grep matches the test's searchable title and tags. Simple single-tag filters are easy to reason about. For OR and AND combinations, quote the regular expression in shell commands and keep the CI expression visible in configuration rather than constructing it dynamically from undocumented variables.
npx playwright test --grep "@smoke"
npx playwright test --grep "@smoke|@p0"
npx playwright test --grep "(?=.*@payments)(?=.*@smoke)"
npx playwright test --grep-invert "@manual-only"Run --list with production filters during CI configuration changes and inspect the selected tests. A filter that silently selects zero tests is a pipeline defect. A broad token embedded in ordinary title text can also match unexpectedly, which is another reason to keep tag names distinctive.
Use Annotations for Explanatory Context
Annotations have a type and a description. They are appropriate for an issue URL, requirement id, owner, or a reason that should appear in reporter output but should not select execution. Use a small type taxonomy and structured descriptions that can be parsed if an integration needs them.
test("declined payment leaves the order unpaid", {
tag: ["@payments", "@regression"],
annotation: [
{ type: "owner", description: "team-payments" },
{ type: "requirement", description: "PAY-DECLINE-03" },
{ type: "risk", description: "revenue-protection" },
],
}, async ({ page }) => {
await page.goto("/checkout");
// Enter a deterministic declined-payment scenario and assert the order state.
});Do not put a changing build URL or raw request payload into a declaration. Static annotations should remain valid every time the test is discovered.
Add Runtime Annotations Only When Context Emerges
Some values are unknown until a fixture leases an account or the server assigns a cohort. Add those through test.info().annotations once the value exists. Keep descriptions bounded and safe for report publication.
import { test as base } from "@playwright/test";
type TenantFixtures = {
tenant: { id: string; class: "standard" | "enterprise" };
};
export const test = base.extend<TenantFixtures>({
tenant: async ({ request }, use) => {
const tenant = await leaseTenant(request);
test.info().annotations.push({
type: "tenant-class",
description: tenant.class,
});
await use(tenant);
await releaseTenant(request, tenant.id);
},
});The class is useful for triage; the tenant's secret token is not. If a reporter must aggregate runtime annotations, confirm that it reads the final test result rather than only discovery-time metadata.
Use TestInfo as the Artifact Boundary
TestInfo identifies the running test, project, retry, output directory, expected status, and actual status. Prefer those fields to reconstructing identity from filenames or environment variables. testInfo.outputPath(...) gives each test a non-conflicting location even under parallel execution.
testInfo.attach accepts a body or a path, not both. Await the call because Playwright copies path-based attachments into reporter-accessible storage. Choose a meaningful content type so reports can render or download the evidence correctly.
import fs from "node:fs/promises";
import { test } from "@playwright/test";
test("export includes the requested date range", async ({ page }, testInfo) => {
await page.goto("/reports");
await page.getByLabel("From").fill("2026-07-01");
await page.getByLabel("To").fill("2026-07-10");
const context = {
project: testInfo.project.name,
retry: testInfo.retry,
dateRange: { from: "2026-07-01", to: "2026-07-10" },
};
const contextFile = testInfo.outputPath("report-context.json");
await fs.writeFile(contextFile, JSON.stringify(context, null, 2), "utf8");
await testInfo.attach("report-context", {
path: contextFile,
contentType: "application/json",
});
});The temporary file is local to the test result. Avoid writing artifacts to a shared fixed path where workers can overwrite one another.
Capture Failure Evidence with an Automatic Fixture
An automatic fixture can attach diagnostics only when actual and expected status differ. This keeps successful reports lean while standardizing failure context. Capture small summaries first; traces, screenshots, and videos already have dedicated Playwright policies.
import { test as base } from "@playwright/test";
type DiagnosticFixtures = { failureContext: void };
export const test = base.extend<DiagnosticFixtures>({
failureContext: [async ({ page }, use, testInfo) => {
await use();
if (testInfo.status !== testInfo.expectedStatus) {
const summary = {
url: page.url(),
title: await page.title().catch(() => "unavailable"),
project: testInfo.project.name,
retry: testInfo.retry,
};
await testInfo.attach("failure-context", {
body: JSON.stringify(summary, null, 2),
contentType: "application/json",
});
}
}, { auto: true }],
});If pages can contain personal data in URLs or titles, sanitize before attaching. Diagnostic convenience does not override the report's retention and access policy.
Use Built-In Status Semantics Correctly
skip, fixme, and slow communicate execution behavior, not product taxonomy. Use conditional forms with a reason when a browser or environment genuinely cannot support the scenario. Do not transform a long-lived product defect into a silent skip without an issue annotation and owner.
Retries also belong in configuration or test policy, not a @retry tag that no runner understands. A quarantine tag can select a separate job, but the report must still preserve expected status, issue, and expiry information through real configuration and annotations.
Design Reporter Consumption Deliberately
Built-in reporters display titles, projects, annotations, attachments, steps, and status in different formats. A custom reporter should treat metadata as input, not infer ownership from directory spelling when a declared owner exists. Keep compatibility in mind if several reporters run together, such as terminal output plus HTML and a CI integration.
Version the metadata taxonomy like any shared interface. Before renaming @checkout to @purchase, update CI grep expressions, dashboards, contributor guidance, and reporter mappings in one change. Otherwise the suite and operational tooling disagree about which tests exist.
Diagnose Metadata Failures
An unexpectedly empty CI job usually means a grep expression or renamed tag no longer matches. Duplicate owner annotations indicate inheritance and test-level overrides need a policy. Missing attachments often come from not awaiting attach, deleting a file too early, or writing outside a test-owned output path.
Huge reports point to unconditional attachments or repeated payload dumps. Leaked credentials point to missing sanitization at the attachment boundary. If runtime annotations never reach a dashboard, confirm the reporter reads completed results. If engineers cannot tell whether a token is a tier, area, or status, the taxonomy has outgrown informal naming.
Balance Rich Context Against Noise
More metadata can improve routing while making every test declaration hard to scan. Inherit stable area and owner values at a describe level, and declare case-specific risk or requirements at the test. Runtime values should be added only when they improve reproducibility or aggregation.
Attachments cost storage, transfer time, and review attention. Capture them on failure or for tests whose output is itself under examination. Prefer a concise JSON summary over a full database export, and rely on trace retention policy for detailed browser history.
Define retention and visibility with the same care as capture. A harmless local attachment may enter a long-lived CI artifact or external dashboard, so metadata producers need to know where reporter output is published and who can read it.
Operational Checklist
- Define a short vocabulary for area, tier, risk, and owner.
- Prefix every tag with
@and eliminate synonyms. - Keep tags for selection and annotations for explanation.
- Validate CI grep expressions with the discovered test list.
- Add static metadata declaratively at test or describe scope.
- Add runtime annotations only after sanitizing their descriptions.
- Write temporary files through
testInfo.outputPath. - Await every attachment and set the correct content type.
- Attach bounded evidence on failure instead of dumping all state.
- Update reporters and CI filters whenever the taxonomy changes.
Conclusion: Treat Metadata as a Shared Interface
Choose the filters the pipeline actually needs, codify them as a small tag vocabulary, and move explanatory details into typed annotations. Use TestInfo for runtime identity and isolated artifact paths, then publish only sanitized evidence that improves triage. Once CI, reports, and contributors share the same definitions, metadata becomes dependable routing information instead of decoration.
// LIVE COURSE / THE TESTING ACADEMY
Playwright Automation Mastery
Go beyond Selenium. Master Playwright with JS/TS in 90 days.
From the instructor behind this guide.
Playwright jobs are growing 8x faster than Selenium. 90 days / 75+ live hrs / Tue-Thu-Sat 7 AM IST.
PRIMARY REFERENCES
Verify the details at the source
QABattle guides are practical explanations. Product behavior, standards, and APIs can change, so use these primary references for the canonical details.
- 01Playwright documentation
Microsoft
Canonical API, locator, fixture, browser, and test-runner behavior.
- 02Playwright best practices
Microsoft
Official guidance for resilient tests, isolation, and user-facing locators.
- 03
FAQ / QUICK ANSWERS
Questions testers ask
What is the difference between a Playwright tag and annotation?
A tag is an `@`-prefixed token suited to filtering and grouping tests. An annotation carries a type and optional description, making it better for ownership, issue links, requirements, or runtime context shown by reporters.
How do I run Playwright tests by tag?
Add tags in the test or describe details object, then use `--grep` or `--grep-invert` with an intentional regular expression. Test the filter in CI because multiple tags and title text participate in matching.
Can annotations be added while a test is running?
Yes. Push a type and description into `test.info().annotations` when the value is only known at runtime, such as a selected tenant class or feature-flag cohort.
How should files be attached with TestInfo?
Call `testInfo.attach` with either a body or a file path and await it. Use `testInfo.outputPath` for collision-free temporary files when tests run in parallel, and remove secrets before attaching data.
Should retry and quarantine status be represented only by tags?
No. Tags can select an operational group, but skip, fixme, slow, retries, and issue tracking each have specific semantics. Preserve the reason in an annotation or configuration instead of encoding the whole lifecycle in a tag name.
RELATED GUIDES
Continue the learning route
GUIDE 01
How to Run Tests in Parallel with Playwright
Learn how to run tests in parallel with Playwright: workers, sharding, isolation, CI config, flakiness fixes, and a practical checklist for faster suites.
GUIDE 02
Classify Flaky, Expected, and Failed Tests with Playwright Retries
Use Playwright retries, annotations, worker behavior, and result evidence to distinguish flaky tests, expected failures, and real regressions in CI.
GUIDE 03
Parameterize Playwright Tests Without Breaking Hooks or Reporting
Parameterize Playwright tests with typed case tables, correct hook boundaries, unique titles, project options, and reports that preserve per-case diagnosis.
GUIDE 04
Read Playwright Trace Viewer Like a Failure Timeline
Analyze failed Playwright tests through action logs, DOM snapshots, requests, console evidence, and attachments instead of guessing from screenshots.