47 fixed rows (the same every load). Sort by clicking headers, filter, paginate, edit a salary inline or delete rows. Use Reset data to restore.
1. Sort by salary
Intermediate
Show all rows, sort by salary from highest to lowest, and prove the order is correct using the real cell values.
Expected: after two clicks the salary header has aria-sort="descending", and the 47 salaries read from the table are in descending order.
Show me how
import { test, expect } from '@playwright/test';
test('sorts salaries from highest to lowest', async ({ page }) => {
await page.goto('/tables.html');
// Show every row on one page so we can read them all at once.
await page.getByTestId('page-size').selectOption('50');
await page.getByTestId('sort-salary').click(); // ascending
await page.getByTestId('sort-salary').click(); // descending
await expect(page.locator('th[data-key="salary"]')).toHaveAttribute('aria-sort', 'descending');
// allTextContents() returns every matching cell's text in DOM order.
const salaries = (await page.locator('td[data-col="salary"]').allTextContents())
.map((text) => Number(text.replace(/,/g, '')));
expect(salaries).toHaveLength(47);
// Compare with a copy we sort ourselves: never trust the app to check the app.
expect(salaries).toEqual([...salaries].sort((a, b) => b - a));
});
2. Filter and check the total
Intermediate
Choose the Engineering department and verify the footer total adds up.
Expected: the counter shows 10 of 47 rows, every visible row is in Engineering, and the Total salary equals the sum of the visible salaries.
Show me how
import { test, expect } from '@playwright/test';
// Salaries are displayed as "54,000": strip the separators before doing maths.
const toNumber = (text: string) => Number(text.replace(/,/g, ''));
test('filters by department and totals the salaries', async ({ page }) => {
await page.goto('/tables.html');
// Show all rows on one page, so the visible cells are the whole filtered set.
await page.getByTestId('page-size').selectOption('50');
await page.getByTestId('dept-filter').selectOption('Engineering');
// toHaveText() waits for the table to re-render after the filter change.
await expect(page.getByTestId('row-count')).toHaveText('10 of 47 rows');
// A Set of all departments must contain exactly one value.
const departments = await page.locator('td[data-col="department"]').allTextContents();
expect(new Set(departments)).toEqual(new Set(['Engineering']));
// Recalculate the total ourselves and compare it with the footer.
const salaries = (await page.locator('td[data-col="salary"]').allTextContents()).map(toNumber);
const total = salaries.reduce((sum, s) => sum + s, 0);
expect(toNumber(await page.getByTestId('salary-total').innerText())).toBe(total);
});
3. Walk every page
Intermediate
With the default 10 rows per page, click Next until you can't, collecting every employee ID on the way.
Expected: you finish on Page 5 of 5 with the Next button disabled, having collected 47 IDs and no duplicates.
Show me how
import { test, expect } from '@playwright/test';
test('collects every id across all pages', async ({ page }) => {
await page.goto('/tables.html');
const next = page.getByTestId('next-page');
const ids: string[] = [];
while (true) {
ids.push(...await page.locator('td[data-col="id"]').allTextContents());
// isDisabled() reads the state right now (no waiting),
// which is what we want for a loop condition.
if (await next.isDisabled()) break;
await next.click();
}
await expect(page.getByTestId('page-info')).toHaveText('Page 5 of 5');
expect(ids).toHaveLength(47);
expect(new Set(ids).size).toBe(47);
});
4. Edit a salary, then delete a row
Intermediate
Change employee 1's salary to 99,999. Then delete employee 3, which asks for confirmation in a browser dialog.
Expected: row 1 shows 99,999; the dialog asks to delete the employee by name; after accepting, row 3 is gone and the counter says 46 of 46 rows.
Show me how
import { test, expect } from '@playwright/test';
test('edits a salary and deletes a row', async ({ page }) => {
await page.goto('/tables.html');
await page.getByTestId('edit-1').click();
// The input only exists in edit mode; Playwright waits for it to appear.
await page.getByTestId('salary-input-1').fill('99999');
await page.getByTestId('save-1').click();
await expect(page.getByTestId('row-1').locator('td[data-col="salary"]')).toHaveText('99,999');
// Dialogs must be handled BEFORE the click that opens them.
// Without a handler, Playwright dismisses dialogs (= Cancel).
page.once('dialog', async (dialog) => {
expect(dialog.type()).toBe('confirm');
expect(dialog.message()).toMatch(/^Delete .+\?$/);
await dialog.accept();
});
await page.getByTestId('delete-3').click();
await expect(page.getByTestId('row-3')).toHaveCount(0);
await expect(page.getByTestId('row-count')).toHaveText('46 of 46 rows');
});