Playwright Test Structure
π
2026-06-23 | π Playwright
Playwright Test Structure and Hooks | Rookie Tutorial\n\nThis chapter introduces how Playwright tests are organized, including test grouping, execution control, and the usage of lifecycle hooks.\n\n* * *\n\n## test.describe() Test Grouping\n\n`test.describe()` is used to organize related tests together into logical groups.\n\n## Example\n\n// File path: tests/describe-demo.spec.ts\n\nimport{ test, expect } from '@playwright/test';\n\n// First level grouping\n\n test.describe('TUTORIAL Homepage Tests',()=>{\n\ntest('Page title is correct', async ({ page })=>{\n\n await page.goto('');\n\n await expect(page).toHaveTitle(/TUTORIAL/);\n\n});\n\ntest('Navigation bar exists', async ({ page })=>{\n\n await page.goto('');\n\n// Assert navigation element exists\n\n await expect(page.locator('nav')).toBeVisible();\n\n});\n\n});\n\n// Multiple describe blocks can be defined\n\n test.describe('Search Function Tests',()=>{\n\ntest('Search box is visible', async ({ page })=>{\n\n await page.goto('');\n\n await expect(\n\n page.getByPlaceholder('Search')\n\n).toBeVisible();\n\n});\n\n});\n\nTest reports will display results grouped by `describe`, making it easy to review.\n\n* * *\n\n## Nested describe\n\n`test.describe()` can be nested to form a tree structure:\n\n## Example\n\ntest.describe('User Module',()=>{\n\ntest.describe('Login Function',()=>{\n\n test('Login with correct credentials', async ({ page })=>{/* ... */});\n\n test('Login fails with wrong password', async ({ page })=>{/* ... */});\n\n});\n\ntest.describe('Registration Function',()=>{\n\n test('Register with valid information', async ({ page })=>{/* ... */});\n\n test('Registration fails with existing email', async ({ page })=>{/* ... */});\n\n});\n\n});\n\n* * *\n\n## test.skip() Skip Tests\n\n`test.skip()` is used to skip a test; skipped tests will not be executed.\n\n## Example\n\n// Skip directly (commonly used to temporarily disable failing feature tests)\n\n test.skip('Feature test not yet completed', async ({ page })=>{\n\n// This test will not execute\n\n});\n\n// Conditional skip (determined at runtime)\n\n test('Skip under specific conditions', async ({ page, browserName })=>{\n\n// Skip in Firefox\n\n test.skip(browserName ==='firefox','Firefox temporarily does not support this feature');\n\n// Test logic only executes in non-Firefox browsers\n\n await page.goto('');\n\n});\n\n* * *\n\n## test.fail() Mark Expected Failure\n\n`test.fail()` is used to mark a test that is known to fail.\n\n## Example\n\n// Mark as expected failure (will not be reported as test failure)\n\n test.fail('Known Bug: Search function not yet implemented', async ({ page })=>{\n\n await page.goto('');\n\n// This assertion is expected to fail\n\n await expect(page.locator('.search-results')).toBeVisible();\n\n});\n\n// If the test unexpectedly passes, it will be reported as a failure\n\n// This reminds you "Bug is fixed, can remove test.fail mark"\n\n> The purpose of `test.fail()` is reverse marking: test failure = expected, test pass = abnormal (may need to remove mark).\n\n* * *\n\n## test.only() Run Only Current Test\n\n`test.only()` is used during development and debugging to run only a specific test, ignoring other tests in the file.\n\n## Example\n\n// During development and debugging, only run the current test\n\n test.only('I am debugging this test', async ({ page })=>{\n\n// Only this test will run\n\n await page.goto('');\n\n});\n\ntest('This test will be ignored', async ({ page })=>{\n\n// Due to test.only() above, this will not run\n\n});\n\n> `test.only()` is a debugging tool and should not be committed to the repository. If you set `forbidOnly: true` in `playwright.config.ts`, CI will detect `only` and fail the build.\n\n* * *\n\n## test.fixme() Mark for Fix\n\n`test.fixme()` is similar to `test.skip()`, but semantically means "this test needs to be fixed".\n\n## Example\n\n// Mark as needing fix (will not run)\n\n test.fixme('Login flow needs update due to API changes', async ({ page })=>{\n\n// This test will temporarily not execute\n\n});\n\n* * *\n\n## test.slow() Mark Slow Tests\n\n`test.slow()` increases the test timeout by 3 times.\n\n## Example\n\n// Mark as slow test, timeout tripled\n\n test.slow('Large data volume test', async ({ page })=>{\n\n// Default timeout for this test will be 90 seconds (30 seconds Γ 3)\n\n// Suitable for scenarios requiring large data loading or complex calculations\n\n});\n\n* * *\n\n## Test Hooks\n\nHook functions let you perform setup and cleanup at different stages of testing.\n\n### test.beforeEach() and test.afterEach()\n\n`beforeEach` executes before each test, `afterEach` executes after each test.\n\n## Example\n\n// File path: tests/hooks-demo.spec.ts\n\nimport{ test, expect } from '@playwright/test';\n\ntest.beforeEach(async ({ page })=>{\n\n// Navigate to the same page before each test\n\n await page.goto('');\n\n});\n\ntest.afterEach(async ({ page })=>{\n\n// Perform cleanup after each test (e.g., clear created data)\n\n// Playwright automatically cleans up browser context,\n\n// cleanup here mainly refers to server-side state\n\n console.log('Test completed, starting cleanup...');\n\n});\n\ntest('First test', async ({ page })=>{\n\n// page is already on TUTORIAL homepage\n\n await expect(page).toHaveTitle(/TUTORIAL/);\n\n});\n\ntest('Second test', async ({ page })=>{\n\n// page is also already on TUTORIAL homepage\n\n// This is a brand new context, previous test's Cookie/Storage will not persist\n\n await expect(page.locator('nav')).toBeVisible();\n\n});\n\n### test.beforeAll() and test.afterAll()\n\n`beforeAll` executes once before all tests in the current scope, `afterAll` executes once after all tests.\n\n## Example\n\ntest.describe('User Management',()=>{\n\n test.beforeAll(async ({ browser })=>{\n\n// Create test user before all tests (executed only once)\n\n// Note: beforeAll cannot use page fixture\n\n});\n\ntest.afterAll(async ({ browser })=>{\n\n// Delete test user after all tests (executed only once)\n\n});\n\ntest('View user list', async ({ page })=>{/* ... */});\n\n test('Edit user information', async ({ page })=>{/* ... */});\n\n});\n\n> `beforeAll` and `afterAll` cannot access the `page` fixture because page is independent for each test. If you need to operate on pages, use `beforeEach`.\n\n* * *\n\n## Hook Scope and Execution Order\n\nThe scope of hooks depends on their location:\n\n## Example\n\n// File-level hooks β apply to entire file\n\ntest.beforeEach(async ()=>{\n\n console.log('File-level beforeEach');\n\n});\n\ntest.describe('Group A',()=>{\n\n// Group-level hooks β only apply to Group A\n\n test.beforeEach(async ()=>{\n\n console.log('Group A beforeEach');\n\n});\n\ntest('Test A1', async ()=>{/* ... */});\n\n test('Test A2', async ()=>{/* ... */});\n\n});\n\ntest.describe('Group B',()=>{\n\n// Group B has no own hooks, only file-level hooks apply\n\n test('Test B1', async ()=>{/* ... */});\n\n});\n\nExecution order: outer hooks execute first, inner hooks execute after.\n\nFor A1 and A2: outputs `"File-level beforeEach"` first, then `"Group A beforeEach"`.\n\nFor B1: only outputs `"File-level beforeEach"`.