Skip to content

RELIABLE SUITES · LESSON 6 OF 24

Lists, filtering and task CRUD

Verify a complete lifecycle without relying on element positions.

  • filter
  • toHaveCount
  • scoped actions
  • CRUD

Your runnable checkpoint

Extract into a new practice folder. Each download contains tests, configuration, the complete local practice app and an offline lesson. Use Node.js 22 or 24. Each checkpoint is independent.

Install dependencies and browser binaries using the commands below. The test runner starts the practice server on port 4174. Tests target localhost, never the production EduPoly site.

1. Understand the concepts

Repeated rows are common in real applications. Locate the collection, narrow to the intended record and perform the action inside that record. Index-based actions become fragile when sorting or filtering changes.

A lifecycle test creates its own data, changes it, checks a filtered view and removes it. It should not depend on a task created by an earlier test. Capture or choose a unique identifier when working with a shared backend.

Use count assertions to verify collection outcomes. A disappearing row after filtering does not necessarily mean deletion; restore the relevant view when checking persistence or removal.

This scenario keeps one task so its expected counts are simple. Later lessons show how to isolate backend data when many tests run at once.

2. Build and run

These are complete test files and configuration. The download also includes app/ and server.mjs; preserve their paths. Stop any manually started practice server before running the suite.

Terminal
npm install
npx playwright install chromium
npm run check
npm test

tests/lesson.spec.ts

tests/lesson.spec.ts
import { test, expect } from '@playwright/test';
test('create, complete, filter and delete', async ({ page }) => {
  await page.goto('/');
  await page.getByLabel('Task title', { exact: true }).fill('Lifecycle task');
  await page.getByRole('button', { name: 'Add task', exact: true }).click();
  const row = page.getByTestId('task-row').filter({ hasText: 'Lifecycle task' });
  await row.getByRole('button', { name: 'Complete', exact: true }).click();
  await expect(row).toHaveAttribute('data-done', 'true');
  await page.getByLabel('Filter tasks').selectOption('active');
  await expect(page.getByTestId('task-row')).toHaveCount(0);
  await page.getByLabel('Filter tasks').selectOption('done');
  await expect(row).toHaveCount(1);
  await row.getByRole('button', { name: 'Delete', exact: true }).click();
  await expect(page.getByTestId('task-row')).toHaveCount(0);
});

playwright.config.ts

playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 1 : 0,
  workers: process.env.CI ? 2 : undefined,
  timeout: 30000,
  expect: { timeout: 5000 },
  reporter: [['list'], ['html', { open: 'never' }]],
  use: { baseURL: 'http://127.0.0.1:4174', trace: 'retain-on-failure', screenshot: 'only-on-failure' },
  projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
  webServer: { command: 'node server.mjs', url: 'http://127.0.0.1:4174', reuseExistingServer: false, timeout: 15000 }
});

package.json

package.json
{
  "name": "edupoly-playwright-lists-filtering-and-crud",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "test": "playwright test",
    "test:headed": "playwright test --headed",
    "test:ui": "playwright test --ui",
    "check": "tsc --noEmit",
    "report": "playwright show-report",
    "serve": "node server.mjs"
  },
  "devDependencies": {
    "@playwright/test": "1.56.1",
    "typescript": "5.9.3",
    "@types/node": "22.20.5"
  }
}

tsconfig.json

tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "CommonJS",
    "moduleResolution": "Node",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "types": [
      "node"
    ],
    "noEmit": true
  },
  "include": [
    "tests/**/*.ts",
    "pages/**/*.ts",
    "playwright.config.ts"
  ]
}

.gitignore

.gitignore
node_modules/
playwright-report/
test-results/
playwright/.auth/
.env

3. Trace the code and check the result

  1. The row locator is reused safely after rerenders.
  2. data-done is an explicit state attribute in the lab.
  3. Filtering and deletion have separate assertions.

Verify your result

  • The task appears under Completed and not Active.
  • Deleting it removes the remaining row.

4. Practice independently

Create another active task and verify completing the first does not affect it.

Show the explanation

Use two named row locators and assert data-done is false on the second after completing the first.

When something goes wrong

If a browser executable is missing, run npx playwright install chromium in the project folder. If port 4174 is occupied, stop your manual practice server. Read assertion call logs for mismatched locators or expectations. Use npm run report to inspect failures; do not add fixed sleeps or broad resets to hide them.

Read the related official Playwright documentation ↗