TypeScript · @playwright/test
Project management with Playwright
A sprint Kanban you can drag across (or use the move menu), a create-issue modal with label chips, issue pages with sub-task progress and comments, an SVG burndown chart, and a backlog with bulk move to sprint.
App under test
🗂️ TaskBoard
- URL
- /apps/projects
- Sprint
- Sprint 14 · day 6 of 10
- Issue keys
- PA-1 … (next PA-9)
- Login
- not required
Scenarios to automate
Work top to bottom — each one adds a new technique.
- 1
Assignee avatars filter
Beginnerfilter-assignee-* toggles (aria-pressed) and only-mine narrow the board; assert board-count.
- 2
Create-issue validation
Intermediatesave-task with blanks shows task-errors and error-*; a short title is rejected.
- 3
Chips with Enter
IntermediateType in label-input and press Enter (or add-label); remove with remove-label-*.
- 4
Drag across columns
AdvancedDrag a task-card onto col-review (HTML5 DnD); or use move-task-<key>. col-count-* updates.
- 5
Sub-task progress
BeginnerAdd sub-tasks, tick subtask-0 and assert subtask-progress “1/2”.
- 6
SVG chart data
Advancedburndown-point circles carry data-day/data-points; the last point equals burndown-remaining.
- 7
Bulk move
IntermediateTick select-<key> in the backlog and move-to-sprint; the issues appear on the board.
Sample Playwright suite
A working starting point that finds elements every way Playwright can — by role, label, text, id, name, CSS, XPath and data-testid — so you practice them all. Pick your language, switch to data-testid only to compare, or try your own in the Locator Playground.
import { test, expect } from '@playwright/test';
// playwright.config.ts → use: { baseURL: 'https://www.practiceqaautomation.com' }
// Locators in this suite: getByRole ×5 · getByLabel ×3 · getByPlaceholder ×3 · CSS ×1 · XPath ×4 · getByTestId ×13
test('creates an issue and finishes it in the sprint', async ({ page }) => {
await page.goto('/apps/projects');
await page.getByTestId('create-task').click();
await page.getByLabel('Summary').fill('Add login rate limiting');
await page.getByRole('combobox', { name: 'Type' }).selectOption('story');
await page.locator('#task-priority').selectOption('high');
await page.getByRole('combobox', { name: 'Assignee' }).selectOption('amir');
await page.getByLabel('Story points').selectOption('5');
await page.getByPlaceholder('Type and press Enter', { exact: true }).fill('security');
await page.locator('xpath=//button[normalize-space()="Add"]').click();
await expect(page.getByTestId('label-chip')).toContainText('security');
await page.locator('xpath=//button[normalize-space()="Create"]').click();
await expect(page.getByTestId('board-toast')).toContainText('PA-9 created');
await expect(page.getByTestId('col-todo')).toContainText('Add login rate limiting');
await page.getByRole('link', { name: 'Add login rate limiting' }).click();
await page.getByPlaceholder('Add a sub-task', { exact: true }).fill('Write tests');
await page.locator('xpath=//button[normalize-space()="Add"]').click();
await page.getByPlaceholder('Add a sub-task', { exact: true }).fill('Implement limiter');
await page.locator('xpath=//button[normalize-space()="Add"]').click();
await page.getByLabel('Write tests').check();
await expect(page.getByTestId('subtask-progress')).toContainText('1/2');
await page.getByTestId('back-to-board').click();
await page.getByTestId('move-task-PA-9').selectOption('done');
await expect(page.getByTestId('col-done')).toContainText('Add login rate limiting');
await page.getByRole('link', { name: 'Sprint report' }).click();
await expect(page.getByTestId('burndown-total')).toContainText('26 pts');
await expect(page.getByTestId('burndown-remaining')).toContainText('18 pts');
await page.getByRole('link', { name: 'Backlog' }).click();
await page.getByTestId('select-PA-6').check();
await page.getByTestId('move-to-sprint').click();
await expect(page.getByTestId('backlog-count')).toContainText('2 issues');
});