TypeScript · @playwright/test
CRM with Playwright
A leads table with filters, select-all and bulk assign, a new-lead modal with duplicate checks, an activity log, convert-to-deal, and a Kanban pipeline you can drag cards across.
App under test
📈 SalesPulse
- URL
- /apps/crm
- sales@crm.dev
- Password
- Crm@123
- Stages
- Qualified → Won/Lost
Scenarios to automate
Work top to bottom — each one adds a new technique.
- 1
Table filters
BeginnerCombine lead-search, lead-status-filter and lead-source-filter; assert lead-count and rows.
- 2
Select all & bulk assign
Intermediateselect-all-leads ticks every visible row; bulk-owner + apply-bulk updates owner-<id> cells.
- 3
New lead validation
Intermediatesave-lead with an existing email shows “already exists”; bad phone/value show error-*.
- 4
Activity log
IntermediateSwitch activity-note/call/email, save, and assert the newest activity-item text.
- 5
Convert to deal
Advancedconvert-lead → deal-value + deal-close → lead-status becomes Converted and converted-banner appears.
- 6
Kanban via menu
IntermediateChange move-deal-<company> and verify the card is inside stage-<id> and stage-total-* updates.
- 7
Kanban drag & drop
AdvancedDrag a deal-card onto another stage-* column (HTML5 DnD — dispatch events in Selenium/Cypress).
- 8
Dashboard KPIs
BeginnerWinning a deal changes kpi-won and kpi-win-rate on the dashboard.
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 ×3 · XPath ×4 · getByTestId ×13
test('adds a lead and moves the deal through the pipeline', async ({ page }) => {
await page.goto('/apps/crm');
await page.getByLabel('Work email').fill('sales@crm.dev');
await page.locator('#crm-password').fill('Crm@123');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page).toHaveURL(/\/apps\/crm\/dashboard/);
await page.locator('xpath=//a[normalize-space()="Leads"]').click();
await page.getByTestId('new-lead').click();
await page.getByLabel('Contact name').fill('Grace Lee');
await page.locator('#lead-company').fill('Globex Corp');
await page.getByTestId('lead-email').fill('grace@globex.com');
await page.getByLabel('Phone').fill('5550142233');
await page.locator('#lead-source').selectOption('linkedin');
await page.getByTestId('lead-value').fill('45000');
await page.getByRole('button', { name: 'Save lead' }).click();
await expect(page.getByTestId('leads-toast')).toContainText('created');
await expect(page.getByTestId('leads-table')).toContainText('Globex Corp');
// Newest lead is listed first
await page.getByTestId('open-lead').nth(0).click();
await expect(page.getByTestId('lead-title')).toContainText('Grace Lee');
await page.locator('xpath=//button[normalize-space()="Log call"]').click();
await page.getByTestId('activity-text').fill('Intro call — interested in the annual plan');
await page.getByRole('button', { name: 'Save call' }).click();
await expect(page.getByTestId('activity-timeline')).toContainText('annual plan');
await page.locator('xpath=//button[normalize-space()="Convert to deal"]').click();
await page.getByLabel('Deal value (USD)').fill('45000');
await page.getByRole('combobox', { name: 'Expected close' }).selectOption('30');
await page.locator('xpath=//button[normalize-space()="Create deal"]').click();
await expect(page.getByTestId('lead-status')).toContainText('Converted');
await page.getByTestId('view-pipeline').click();
await expect(page.getByTestId('stage-qualified')).toContainText('Globex Corp');
await page.getByRole('combobox', { name: 'Move Globex Corp' }).selectOption('proposal');
await expect(page.getByTestId('stage-proposal')).toContainText('Globex Corp');
await expect(page.getByTestId('stage-total-proposal')).toContainText('$212,000');
});