TypeScript · @playwright/test
HR with Playwright
Clock in with a live timer, apply for leave with weekend-aware day counts and half days, approve as the HR manager, watch the balance drop, browse a paginated directory and download payslips.
App under test
👥 PeopleHub
- URL
- /apps/hr
- Employee
- emp / Hr@123
- HR manager
- hrmanager / Hr@123
- Casual leave
- 8 days
Scenarios to automate
Work top to bottom — each one adds a new technique.
- 1
Role-based pages
BeginnerThe employee sees access-denied on Approvals; the manager can't open Apply leave.
- 2
Clock in/out
Beginnerclock-in starts worked-time ticking; clock-out records last-session.
- 3
Disabled weekends
IntermediateSaturday/Sunday chips in leave-from/leave-to are disabled; leave-to before leave-from is disabled too.
- 4
Working-day maths
IntermediateA Fri → Mon range counts 2 days in leave-days; half-day (same day only) counts 0.5.
- 5
Validation rules
IntermediateExceeding the balance, overlapping an existing request or a short reason each show error-*.
- 6
Approval workflow
AdvancedEmployee submits → manager approves → leave-status Approved and balance-* drops by leave-days.
- 7
Reject with comment
Advancedreject-leave requires a reason; the employee sees it under their request.
- 8
Directory paging
BeginnerFilter by department, page with page-next, open an employee-modal and read employee-email.
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 ×6 · getByLabel ×4 · CSS ×4 · XPath ×3 · getByTestId ×8
test('applies for leave and gets it approved', async ({ page }) => {
await page.goto('/apps/hr');
await page.getByLabel('Username').fill('emp');
await page.locator('#hr-password').fill('Hr@123');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByTestId('balance-casual')).toContainText('8');
await page.locator('xpath=//button[normalize-space()="Clock in"]').click();
await expect(page.getByTestId('attendance-status')).toContainText('Clocked in');
await page.getByTestId('apply-leave').click();
await page.getByRole('combobox', { name: 'Leave type' }).selectOption('casual');
// First working day for both ends → a single-day, half-day leave
await page.locator('[data-testid="leave-from"]:not([disabled])').first().click();
await page.locator('[data-testid="leave-to"]:not([disabled])').first().click();
await page.getByLabel('Half day (single-day leave only)').check();
await expect(page.getByTestId('leave-days')).toContainText('0.5');
await page.locator('#leave-reason').fill('Doctor\'s appointment in the morning');
await page.locator('xpath=//button[normalize-space()="Submit request"]').click();
await expect(page.getByTestId('leave-submitted')).toBeVisible();
await expect(page.getByTestId('leave-status')).toContainText('Pending');
await page.getByRole('button', { name: 'Log out' }).click();
// HR manager approves
await page.getByLabel('Username').fill('hrmanager');
await page.locator('#hr-password').fill('Hr@123');
await page.getByRole('button', { name: 'Sign in' }).click();
await page.locator('xpath=//a[normalize-space()="Approvals"]').click();
await page.getByTestId('approve-leave').nth(0).click();
await expect(page.getByTestId('approval-toast')).toContainText('approved');
await page.getByRole('button', { name: 'Log out' }).click();
// The employee's balance has dropped
await page.getByLabel('Username').fill('emp');
await page.locator('#hr-password').fill('Hr@123');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByTestId('balance-casual')).toContainText('7.5');
});