TypeScript · @playwright/test
Recruitment with Playwright
Candidates filter jobs and apply through a 4-step form (contact, résumé upload, screening questions, review); recruiters sign in to shortlist, reject with a reason, view résumés and schedule interviews that update the candidate's status.
App under test
💼 HireHub
- URL
- /apps/jobs
- Recruiter
- recruiter@hirehub.dev
- Password
- Hire@123
- Résumé
- PDF/DOCX ≤ 2 MB
Scenarios to automate
Work top to bottom — each one adds a new technique.
- 1
Combined filters
Beginnerjob-search, job-location, job-experience, job-salary and remote-only narrow job-count.
- 2
Optional URL validation
Intermediateapplicant-linkedin can be blank, but a non-linkedin.com/in URL shows error-applicant-linkedin.
- 3
Upload rules
Intermediateresume-file rejects .txt/.jpg and >2 MB; the next step is blocked until a valid file is chosen.
- 4
Screening answers
Beginnerq-experience, q-authorized-yes/no and q-notice are all required; review-* shows them.
- 5
Two-role workflow
AdvancedApply as candidate → recruiter shortlists and schedules → application-status shows “Interview scheduled”.
- 6
Conditional actions
IntermediateOnly Applied rows show shortlist-*, only Shortlisted rows show schedule-*; Rejected rows show neither.
- 7
Modal validation
Intermediateconfirm-interview without day/time/mode shows schedule-error; weekends are disabled.
- 8
Save jobs
Beginnersave-job-<id> toggles aria-pressed (bookmark icon).
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 ×4 · CSS ×4 · XPath ×4 · getByTestId ×15
test('applies for a job and gets an interview', async ({ page }) => {
await page.goto('/apps/jobs');
await page.locator('#job-search').fill('QA');
await page.getByRole('switch', { name: 'Remote only' }).click();
await expect(page.getByTestId('job-count')).toContainText('2 jobs');
await page.getByTestId('view-job-j1').click();
await page.getByTestId('easy-apply').click();
await page.getByLabel('Full name').fill('Amir Khan');
await page.getByLabel('Email').fill('amir@example.com');
await page.locator('#applicant-phone').fill('9876543210');
await page.getByTestId('apply-next').click();
await page.locator('#resume-file').setInputFiles('fixtures/resume.pdf');
await page.getByTestId('apply-next').click();
await page.getByLabel('Years of testing experience').fill('6');
await page.locator('xpath=//button[normalize-space()="Yes"]').click();
await page.getByRole('combobox', { name: 'Notice period' }).selectOption('30');
await page.getByTestId('apply-next').click();
await expect(page.getByTestId('review-resume')).toContainText('resume.pdf');
await page.locator('xpath=//button[normalize-space()="Submit application"]').click();
await expect(page.getByTestId('application-id')).toContainText('APP-501');
// Recruiter side
await page.getByRole('link', { name: 'Recruiter' }).click();
await page.getByLabel('Email').fill('recruiter@hirehub.dev');
await page.locator('#recruiter-password').fill('Hire@123');
await page.locator('xpath=//button[normalize-space()="Sign in"]').click();
await page.getByTestId('job-applicants-j1').click();
await page.getByTestId('shortlist-APP-501').click();
await page.getByTestId('schedule-APP-501').click();
await page.locator('[data-testid="interview-day"]:not([disabled])').first().click();
await page.getByRole('combobox', { name: 'Time' }).selectOption('11:30');
await page.getByTestId('interview-video').click();
await page.locator('xpath=//button[normalize-space()="Send invite"]').click();
await expect(page.getByTestId('status-APP-501')).toContainText('Interview scheduled');
await page.getByRole('link', { name: 'My applications' }).click();
await expect(page.getByTestId('application-status')).toContainText('Interview scheduled');
await expect(page.getByTestId('interview-details')).toBeVisible();
});