PracticeQAAutomation logoPracticeQAAutomation
Pw

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. 1

    Combined filters

    Beginner

    job-search, job-location, job-experience, job-salary and remote-only narrow job-count.

  2. 2

    Optional URL validation

    Intermediate

    applicant-linkedin can be blank, but a non-linkedin.com/in URL shows error-applicant-linkedin.

  3. 3

    Upload rules

    Intermediate

    resume-file rejects .txt/.jpg and >2 MB; the next step is blocked until a valid file is chosen.

  4. 4

    Screening answers

    Beginner

    q-experience, q-authorized-yes/no and q-notice are all required; review-* shows them.

  5. 5

    Two-role workflow

    Advanced

    Apply as candidate → recruiter shortlists and schedules → application-status shows “Interview scheduled”.

  6. 6

    Conditional actions

    Intermediate

    Only Applied rows show shortlist-*, only Shortlisted rows show schedule-*; Rejected rows show neither.

  7. 7

    Modal validation

    Intermediate

    confirm-interview without day/time/mode shows schedule-error; weekends are disabled.

  8. 8

    Save jobs

    Beginner

    save-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.

Language
hirehub.spec.ts
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();
});

Practicing another tool?