playwright-skip-aftereach-plugin
v1.0.0
Published
Playwright plugin that skips afterEach hooks when a test is dynamically skipped
Downloads
102
Maintainers
Readme
playwright-skip-aftereach-plugin
Playwright plugin that automatically skips afterEach hooks when a test is dynamically skipped via test.skip().
The problem
When test.skip() is called inside a test body, Playwright marks the test as skipped but still runs every registered afterEach hook. This is intentional by design (playwright#19984), but it forces you to duplicate the skip condition in each hook:
test.afterEach(async ({ page }, testInfo) => {
if (testInfo.project.name !== 'chromium') return; // duplicated condition 😞
await doSomethingChromiumSpecific(page);
});
test('chromium-only feature', async ({ page }, testInfo) => {
test.skip(testInfo.project.name !== 'chromium', 'chromium only');
// ...
});This plugin removes that duplication entirely.
Installation
npm install -D playwright-skip-aftereach-pluginUsage
Transparent install (recommended)
Add a single import at the top of playwright.config.ts. No changes needed anywhere else.
// playwright.config.ts
import 'playwright-skip-aftereach-plugin/install';
import { defineConfig } from '@playwright/test';
export default defineConfig({ ... });From that point on, every afterEach hook across all spec files is automatically skipped whenever the test was dynamically skipped — including hooks that use Playwright fixtures.
// my-feature.spec.ts — unchanged, imports only from @playwright/test
import { test, expect } from '@playwright/test';
test.afterEach(async ({ page }) => {
// This won't run if the test was skipped
await doSomethingChromiumSpecific(page);
});
test('chromium-only feature', async ({ page }, testInfo) => {
test.skip(testInfo.project.name !== 'chromium', 'chromium only');
// ...
});Explicit wrapper (ifNotSkipped)
For cases where you want to opt in per-hook rather than globally, use the ifNotSkipped helper. The name reads as a sentence at the call site:
import { test } from '@playwright/test';
import { ifNotSkipped } from 'playwright-skip-aftereach-plugin';
test.afterEach(ifNotSkipped(async () => {
await cleanup();
}));
test.afterEach(ifNotSkipped(async (testInfo) => {
console.log('ran after:', testInfo.title);
}));Note: the callback passed to
ifNotSkippedreceivestestInfobut not Playwright fixtures. To access fixtures, use the transparent install approach instead.
Use cases
Multi-browser setup with browser-specific tests
The most common scenario. Your playwright.config.ts runs tests on Chromium, Firefox, and WebKit. A particular test only makes sense on Chromium, so it skips itself on the others. Without this plugin, any afterEach hook that does Chromium-specific cleanup will still run (and likely fail or produce noise) on Firefox and WebKit.
Without the plugin — condition duplicated in the hook:
test.afterEach(async ({ page }, testInfo) => {
if (testInfo.project.name !== 'chromium') return; // must repeat the condition
await page.evaluate(() => window.__resetChromiumFeature());
});
test('uses Chromium-specific API', async ({ page }, testInfo) => {
test.skip(testInfo.project.name !== 'chromium', 'chromium only');
await page.evaluate(() => window.__setupChromiumFeature());
// ...
});With the plugin — declare the condition once, in the test:
// playwright.config.ts
import 'playwright-skip-aftereach-plugin/install';
// my-feature.spec.ts
test.afterEach(async ({ page }) => {
await page.evaluate(() => window.__resetChromiumFeature()); // just the logic
});
test('uses Chromium-specific API', async ({ page }, testInfo) => {
test.skip(testInfo.project.name !== 'chromium', 'chromium only');
await page.evaluate(() => window.__setupChromiumFeature());
// ...
});Feature-flag or environment gating
test.afterEach(async ({ page }) => {
await page.evaluate(() => window.__teardownBetaFeature());
});
test('beta feature', async ({ page }) => {
test.skip(process.env.BETA_ENABLED !== 'true', 'beta not enabled');
// ...
});Conditional setup/teardown pairs
When a test skips itself after detecting a precondition isn't met (e.g. a required service is unavailable), the teardown should be skipped too — no setup happened, so there's nothing to tear down.
test.afterEach(async ({ request }) => {
await request.delete('/api/test-session');
});
test('requires external service', async ({ request }) => {
const available = await request.get('/health').then(r => r.ok());
test.skip(!available, 'service unavailable');
await request.post('/api/test-session');
// ...
});How it works
Playwright loads playwright.config.ts in each worker process before requiring spec files. @playwright/test exports a singleton test object held in Node's module cache. The install module patches test.afterEach once on that singleton, so every subsequent afterEach registration — across all spec files in that worker — goes through the wrapper.
Playwright injects fixtures into afterEach callbacks by parsing the function's first parameter with fn.toString(). The wrapper preserves the original function's string representation via a toString override, so fixture injection continues to work transparently.
The patch is idempotent: importing the install module multiple times has no effect.
API
| Export | From | Description |
|---|---|---|
| 'playwright-skip-aftereach-plugin/install' | side-effect import | Patches test.afterEach globally for the current worker process |
| ifNotSkipped(fn) | 'playwright-skip-aftereach-plugin' | HOF that wraps a callback and skips it when the test was skipped |
License
MIT
