QA How-To
How to Skip and group tests in Playwright (2026)
Learn playwright how to skip and group tests with describe, it.skip, conditional skips, tags, hooks behavior, CI policy, and anti-patterns for 2026.
22 min read | 2,542 words
TL;DR
Group Playwright tests with nested describe/context. Skip with it.skip or describe.skip (plus a reason). Use test.only only locally. For cross-file groups, use folders and tags. Conditional skips belong behind documented Playwright.env flags.
Key Takeaways
- Group related examples with nested describe or context and shared hooks at the right level.
- Skip with it.skip or describe.skip and include a ticket or reason in the title.
- Treat test.only as temporary local focus, never as a committed smoke strategy.
- Use this.skip() or describe versus describe.skip for environment-conditional execution.
- Scale cross-file grouping with folders and tags filtered in CI.
- Track skips as visible quality debt with owners instead of commenting out tests.
- Keep hook placement intentional so skips do not create surprise setup cost or side effects.
The short answer to playwright how to skip and group tests is to use Mocha's suite API: nest describe blocks to group, use context as an alias for readable grouping, and use test.skip / test.describe.skip (or xit / xdescribe) to exclude examples intentionally. Use test.only / test.test.describe.only only for temporary local focus. For cross-file grouping in CI, add tags and grep-style filters rather than commenting out code.
Skipping and grouping are how professional suites stay honest under partial outages, incomplete features, and large domain boundaries. This guide covers structure, skip vs pending, exclusive tests, conditional skips, hooks interaction, tagging strategies, anti-patterns, and interview answers with practical Playwright examples for 2026.
TL;DR
| Intent | API / technique | Commit to main? |
|---|---|---|
| Group related examples | Nested describe / context |
Yes |
| Skip one test | test.skip or xit |
Yes, with ticket |
| Skip a suite | test.describe.skip or xdescribe |
Yes, with ticket |
| Run only one locally | test.only / test.test.describe.only |
No |
| Pending placeholder | test('title') without body (where supported) |
Rarely |
| Cross-file group | Tags + --grep filter |
Yes |
| Env-based skip | Conditional describe / runtime skip helpers |
Yes, documented |
Group for readability and shared hooks. Skip with a reason. Never confuse skip (exclude) with only (focus).
1. Playwright How to Skip and Group Tests: Mental Model
Playwright inherits Mocha's BDD interface for organizing tests. A spec file loads one or more top-level suites. A suite (describe / context) groups examples and can nest. An example (it / specify) is the runnable unit reporters count.
Grouping answers: "What feature or risk area is this?" Skipping answers: "Should this example run in this pipeline right now?" Focusing answers: "Temporarily run only this while I debug." Those three verbs must stay distinct in code review.
Bad suites look like flat lists of fifty it blocks with copy-pasted setup. Good suites look like a table of contents: auth, validation, permissions, error paths. Bad skipping looks like large commented regions. Good skipping looks like test.skip('...', () => { ... }) with a ticket link in the title or a comment above.
When people search playwright how to skip and group tests, they often paste test.skip into a file and stop. This guide goes further: hooks, CI policy, tags, and how grouping interacts with isolation commands from how to run a single test in Playwright.
2. Group With describe, context, and Nested Suites
// tests/billing/invoices.spec.ts
test.describe('Billing invoices', () => {
test.beforeEach(async ({ page }) => {
page.loginAs('finance@example.com')
await page.goto('/billing/invoices')
})
context('list view', () => {
test('shows invoices for the current workspace', () => {
page.locator('[data-testid=invoice-row]').should('have.length.greaterThan', 0)
})
test('filters by status', () => {
page.locator('[data-testid=status-filter]').select('Open')
page.locator('[data-testid=invoice-row]').each(($row) => {
page.wrap($row).should('contain', 'Open')
})
})
})
context('detail view', () => {
test('opens an invoice PDF download affordance', () => {
page.locator('[data-testid=invoice-row]').first().click()
page.locator('[data-testid=download-pdf]')).toBeVisible()
})
})
})
context is an alias of describe. Use whichever reads better. Many teams use describe for the feature and context for state variations (logged out, admin, empty state).
Nesting benefits:
- Shared
beforeEachat the right level reduces duplication. - Reports and open-mode UI become navigable.
test.describe.skipcan park an entire branch of coverage intentionally.- Ownership is clearer in code review ("you changed the permissions context").
Do not nest endlessly. Three levels is usually enough. Deeper trees often mean the file should split.
3. Skip Examples With test.skip, xit, and Pending Titles
test.skip('exports CSV for 10k rows (BUG-1842)', () => {
// still keep the body so the intent is not lost
page.locator('[data-testid=export-csv]').click()
page.readFile('test-results/invoices.csv').should('exist')
})
Equivalent form:
xit('exports CSV for 10k rows (BUG-1842)', () => {
// ...
})
Title hygiene for skips:
- Include a ticket id when the skip is defect-driven.
- Include a date or milestone when the skip is feature-flag driven.
- Prefer keeping the test body, not deleting it, so re-enablement is easy.
Pending tests (declared without implementation) signal unfinished work. Prefer explicit test.skip with a reason over mysterious empty placeholders that confuse newcomers.
Skipping is a contract with your future self. A skip without a reason becomes permanent dead weight.
4. Skip Suites With test.describe.skip and xdescribe
test.describe.skip('Legacy PayPal checkout (remove after 2026-09)', () => {
test('completes payment with PayPal sandbox', () => {
// ...
})
})
Suite skips are ideal when an entire integration is disabled in an environment, or when a partner API is in planned maintenance and every child example would fail for the same external reason. Prefer suite skip over twenty individual skips for the same cause.
Caveat: hooks inside a skipped suite do not run, which is usually what you want. Hooks outside the skipped suite still run for other suites in the file. Structure files so a skipped island does not force expensive global setup for no reason, or accept that root before hooks still execute when other suites remain active.
5. Exclusive Focus: test.only and test.test.describe.only Are Not Skips
Focus is the inverse operation:
test.only('calculates tax for CA addresses', () => {
// ...
})
Focused tests skip everything else by exclusion from the run set. That is useful locally and dangerous in main. Policy:
- Lint against exclusive tests in CI.
- Code review rejects
.onlyon protected branches. - Teach juniors the difference on day one.
A common interview trap is treating .only as a permanent way to "group smoke tests." That is wrong. Smoke grouping belongs in tags or dedicated smoke specs, not exclusive markers.
6. Compare Skip, Only, and Grouping Techniques
| Technique | Effect on suite | Good for | Bad for |
|---|---|---|---|
Nested describe |
Organizes, shares hooks | Features, states | Tiny files with one test |
test.skip |
Excludes one example | Known bugs, WIP | Hiding flakes forever |
test.describe.skip |
Excludes a branch | Dead features, env gaps | Silent long-term disablement |
test.only |
Runs focus set only | Local debug | CI, shared branches |
| Tags + grep | Filters across files | Smoke, squad ownership | Ad hoc one-off names |
| Commented-out tests | None (dead code) | Never | Everything |
If your strategy is mostly commented blocks, you do not have a skip strategy. You have archaeology.
7. Conditional Skips Based on Environment and Feature Flags
Sometimes a test should run only in staging, or only when a flag is on:
const canRunPayments = process.env['PAYMENTS_E2E') === true
test.describe('Card payments', () => {
before(function () {
if (!canRunPayments) {
this.skip()
}
})
test('charges a visa test card', () => {
await page.goto('/checkout')
// ...
})
})
this.skip() inside a hook or test (function form, not arrow, when relying on Mocha this) dynamically skips at runtime. Arrow functions do not bind Mocha's this, so use function () {} when calling this.skip().
Alternative structure:
const describePayments = canRunPayments ? describe : test.describe.skip
describePayments('Card payments', () => {
test('charges a visa test card', () => {
// ...
})
})
Document the env var in README. Conditional skips that nobody can turn on are permanent skips in disguise.
Feature flag example:
test('shows the new invoice timeline', function () {
if (!process.env['FF_INVOICE_TIMELINE')) {
this.skip()
}
await page.goto('/billing/invoices/inv_1')
page.locator('[data-testid=invoice-timeline]')).toBeVisible()
})
Prefer enabling flags in test environments deterministically over skipping large areas forever.
8. Hooks, Order, and What Still Runs When You Skip
Understanding hooks prevents surprise cost and surprise failures:
test.describe('Workspace settings', () => {
before(() => {
// runs once if any test in this suite runs
page.task('seed:workspace', { id: 'ws_demo' })
})
test.beforeEach(async ({ page }) => {
page.session('owner', () => page.loginAs('owner@example.com'))
await page.goto('/settings')
})
test('updates workspace name', () => { /* ... */ })
test.skip('deletes workspace (blocked on BACKEND-77)', () => {
// body kept for later
})
})
The skipped example does not run its body. beforeEach still runs for the active examples. If every example is skipped, Mocha generally does not execute the suite the same way as an active suite; do not depend on skipped suites for seeding side effects.
Root support file hooks in tests/setup.ts still load. Skipping tests does not unload global listeners. Keep global hooks lightweight.
For auth-heavy suites, page.session grouping by role inside nested contexts keeps setup coherent without re-login cost on every example.
9. Group Across Files With Folders, Naming, and Tags
File system grouping:
tests/
auth/
login.spec.ts
logout.spec.ts
billing/
invoices.spec.ts
taxes.spec.ts
smoke/
critical-path.spec.ts
Config can include patterns per job:
// illustrative: different CI jobs pass different globs
// smoke job:
// npx playwright test "tests/smoke/**/*.spec.ts"
Tag grouping inside tests (with a grep-style plugin):
test('logs in with email and password', { tags: ['@smoke', '@auth'] }, () => {
// ...
})
npx playwright test --grep @smoke
Tags excel when a smoke path cuts across folders. Folders excel when ownership maps to domains. Use both.
Related: headed investigation of a group often uses --spec on a folder. See how to run tests in headed mode in Playwright.
10. Playwright How to Skip and Group Tests in CI Policy
A healthy CI policy looks like this:
- Default branch runs the full non-skipped suite (plus intentional env filters).
- Skips require tickets and appear in a weekly triage list.
.onlyfails lint on PRs.- Smoke job uses tags or a smoke folder, not exclusive tests.
- Quarantine moves chronically flaky tests to skip with owner and expiry, not silent retries forever.
Example PR check idea: fail if the diff introduces test.skip without a BUG- or TICKET- pattern in the title. Perfect automation is optional; review culture is not.
Track skip count as a metric. A rising skip curve means quality debt, not velocity.
11. Anti-Patterns That Rot a Suite
- Commenting out tests instead of
test.skip. - Skipping flakes permanently without root cause ownership.
- Using
.onlyas smoke selection. - Mega-describes with unrelated examples and one giant
beforeEach. - Duplicate groups across files with divergent setup helpers.
- Empty skipped suites left for years after a feature dies (delete instead).
- Conditional skips with secret env vars nobody sets locally.
- Grouping only by ticket number instead of user-facing behavior.
Replace comments with skips, skips with fixes, and fixes with smaller groups that match risk.
12. Refactor Playbook: From Flat File to Structured Suite
Starting point:
test.describe('settings', () => {
test('test1', () => {})
test('test2', () => {})
// ... 40 more
})
Target structure:
- Split by user capability: profile, security, notifications.
- Move shared login to
beforeEachorpage.session. - Create
context('validation')vscontext('happy path'). - Extract true end-to-end journeys that span apps into a smoke folder.
- Skip only the examples blocked externally, with tickets.
Refactor in vertical slices. Do not rewrite the entire folder in one PR if the suite is critical; structure is a series of small moves with green CI between them.
When failures concentrate in one context, isolation commands help:
npx playwright test "tests/settings/security.spec.ts"
Pair with title grep if a single context is still large.
13. Reporting, Pending Counts, and Stakeholder Communication
Reporters show pending or skipped counts. Stakeholders sometimes panic when pending rises after a release freeze. Communicate proactively:
- "Twelve skips are partner-sandbox tests disabled during Acme API migration (EPIC-55)."
- "Two skips are quarantined flakes with owners and expiry dates."
Never hide skips by deleting assertions that protected revenue paths. Transparency beats vanity green.
Open mode displays skipped tests differently from failed tests. Teach the team to read the runner summary: failed means fix now; skipped means manage the backlog; passed means still not a substitute for exploratory testing.
14. Worked Example: Permissions Matrix Grouping
test.describe('Project permissions', () => {
const roles = [
{ name: 'owner', canDelete: true },
{ name: 'editor', canDelete: false },
{ name: 'viewer', canDelete: false },
] as const
roles.forEach((role) => {
context(`as ${role.name}`, () => {
test.beforeEach(async ({ page }) => {
page.loginAs(`${role.name}@example.com`)
await page.goto('/projects/prj_1')
})
test('views project overview', () => {
page.locator('h1').should('contain', 'Apollo')
})
test(`${role.canDelete ? 'allows' : 'blocks'} project deletion`, () => {
if (role.canDelete) {
page.locator('[data-testid=delete-project]').should('be.enabled')
} else {
page.locator('[data-testid=delete-project]').should('not.exist')
}
})
})
})
})
This groups by role without three nearly identical files. If viewer deletion UI is temporarily wrong in staging only, you can skip a single generated-style example carefully, but dynamic skips inside loops need clear titles so the pending list stays readable.
Dynamic generation is powerful; keep titles stable for flake tracking and screenshots.
Skip and Group Governance Checklist
Confirm every skip has a reason and owner, no .only on main, groups match product language, hooks sit at the correct nesting level, smoke is tag or folder based, conditional skips are documented, pending counts are reviewed weekly, and dead skipped features are deleted. Confirm new engineers can find the right file from the folder tree without tribal knowledge. Confirm interview candidates from your team can explain skip versus only without mixing them. This governance keeps Mocha structure from decaying into a junk drawer.
Add one more operational habit: when you skip a test because of an upstream outage, create a calendar reminder or ticket expiry. Skips without expiry become folklore. When the outage ends, re-enable deliberately and watch the first pipeline closely. If the test fails for a new reason, fix it then, do not re-skip blindly. Grouping and skipping are stewardship tools. Used well, they tell a true story about what your suite covers today. Used poorly, they invent a story that shipping is safer than it is. Prefer truth.
Also align naming with product analytics events when possible. If the product calls a surface "Workspace preferences," do not name the suite "Settings misc." Shared language helps engineers, PMs, and support map failures to features quickly. That is the hidden productivity win of good grouping: faster triage meetings, not prettier trees for their own sake. Skip titles should be equally searchable in your issue tracker so a BUG id jumps from Playwright pending lists to Jira without guesswork during incident response and release readiness reviews weekly.
15. Migration Notes From Other Runners and Team Training Scripts
Engineers arriving from Jest, Pytest, or TestNG often reach for familiar mental models. Jest's test.describe.skip maps cleanly. Pytest's markers are closer to Playwright tags than to Mocha nested suites. TestNG groups resemble tag filters. During onboarding, draw that map explicitly so people do not invent a second organization system beside the one already in the repo.
A 30-minute training outline you can reuse:
- Open a well-structured spec and walk the describe tree in
playwright test --ui. - Demonstrate
test.skipand show the pending count. - Demonstrate
test.only, show the danger, then remove it. - Run a
@smokegrep job in CLI. - Open the skip triage board and assign one quarantine ticket.
Record that session once. New hires watch it before they write their first PR. This lowers review friction more than any style guide paragraph alone.
If you migrate from Cucumber-style feature files, do not force Gherkin syntax into Mocha unless the team truly practices BDD collaboration with non-engineers. Grouping by describe names that read like behaviors often gives most of the communication benefit with less tooling. Choose ceremony levels deliberately.
Finally, when two squads share one Playwright project, agree who owns which top-level folder and which tags. Ownership vacuums create orphan skips. A CODEOWNERS file pointing at tests/billing/ does more for skip hygiene than another wiki page about "please don't skip tests." Pair ownership with the weekly pending-count review and the process stays alive.
Interview Questions and Answers
Q: How do you skip a test in Playwright?
I use test.skip or xit and keep the body with a ticket reference in the title. For whole areas I use test.describe.skip.
Q: How do you group tests?
I nest describe or context blocks by feature and state, share hooks at the right level, and use folders plus tags for cross-file groups.
Q: What is the difference between skip and only?
Skip excludes specific tests from running while the rest execute. Only focuses a subset and excludes everything else. Only is for local debugging, not permanent suite design.
Q: How do you skip tests conditionally?
I check Playwright.env and call this.skip() inside a function form hook or test, or I select describe versus test.describe.skip when defining the suite.
Q: How should smoke tests be organized?
With a smoke folder and/or @smoke tags filtered in CI. Not with committed .only markers.
Q: What is dangerous about commenting out tests?
They disappear from pending metrics, rot without context, and are hard to re-enable safely. Explicit skips remain visible.
Q: How do hooks behave with skipped tests?
Skipped examples do not run their bodies. Active siblings still get suite-level hooks. I do not rely on fully skipped suites to perform required seeding.
Common Mistakes
- Committing
test.onlyas a fake grouping strategy. - Skipping flakes forever without owners.
- Commenting out large blocks instead of
test.skip. - Putting unrelated tests in one
describewith a bloatedbeforeEach. - Using arrow functions with
this.skip()and wondering why it fails. - Conditional skips tied to undocumented env vars.
- Deleting skipped coverage for revenue paths to make reports look clean.
- Nesting describes six levels deep instead of splitting files.
- Mixing ticket-only names that mean nothing to new engineers.
- Forgetting to re-enable skips after the blocking ticket closes.
Conclusion
For playwright how to skip and group tests, structure suites with nested describe/context, exclude intentionally with test.skip/test.describe.skip plus reasons, and reserve .only for ephemeral local focus. Scale grouping with folders and tags, and manage skips as visible debt rather than hidden comments.
Open your largest flat spec this week, split it into two contexts with tighter hooks, convert any commented tests into ticketed skips, and lint against exclusive tests. That modest cleanup improves readability, CI honesty, and onboarding immediately.
Interview Questions and Answers
How do you organize a large Playwright suite?
I group by product domain in folders, nest describe/context by user state or capability, keep hooks local to the smallest sensible suite, and use tags for smoke and squad filters. I avoid mega-files with unrelated examples.
When do you skip versus delete a test?
I skip when coverage should return soon or is blocked externally. I delete when the feature is gone or the test no longer maps to a user risk. Skips need reasons; deletions need agreement that the risk is gone.
How do you prevent test.only from breaking CI coverage?
ESLint rules, CI greps, and review checklists block exclusive tests on protected branches. I also educate the team that smoke selection uses tags or folders, not only.
Explain conditional skipping for multi-environment pipelines.
I gate suites with Playwright.env flags documented in README, using this.skip() or describe.skip selection. Local defaults should make the common path easy, and CI sets flags explicitly per job.
How do tags differ from describe blocks?
describe structures a file and hook hierarchy. Tags classify examples across files for selection. A test can live in a billing describe and still carry @smoke for the release job.
What metrics would you track around skips?
Skip count over time, age of skips, percent with ticket ids, and reopen rate after re-enablement. Rising unmanaged skips are a quality signal leadership should see.
How do hooks interact with skipped tests?
Skipped tests do not execute their bodies. Sibling active tests still run suite hooks. I never depend on a fully skipped suite to seed data required elsewhere.
Frequently Asked Questions
How do I skip a test in Playwright?
Use it.skip('title', () => { ... }) or xit(...). Prefer keeping the body and a ticket id in the title so the skip is visible and reversible.
How do I skip a whole suite?
Use describe.skip or xdescribe around the group. This is better than skipping every child individually when they share the same blocker.
What is the difference between it.skip and test.only?
it.skip excludes that test while others run. test.only focuses one or more tests and skips the non-focused rest. only is for local debugging.
How do I group tests in Playwright?
Nest describe or context blocks by feature and state, share beforeEach hooks appropriately, and organize files into domain folders. Add tags for cross-cutting smoke sets.
Can I skip a test based on an environment variable?
Yes. Check Playwright.env and call this.skip() inside a function-form test or hook, or choose describe versus describe.skip when defining the suite.
Why does this.skip fail in an arrow function?
Arrow functions do not bind Mocha's this. Use a classic function () {} callback when you need this.skip().
How should we handle flaky tests?
Quarantine with it.skip plus owner and ticket, fix the root cause, and re-enable. Do not leave permanent skips or unlimited retries as the only strategy.
Related Guides
- How to Skip and group tests in Cypress (2026)
- How to Skip and group tests in Selenium (2026)
- How to Run tests in headed mode in Playwright (2026)
- How to Debug a failing test in VS Code in Playwright (2026)
- How to Run tests in headed mode in Cypress (2026)
- How to Run tests in headed mode in Selenium (2026)