Skip to content

FOUNDATIONS · LESSON 5 OF 24

Forms, selections and validation

Exercise inputs through the same controls a learner uses.

  • fill
  • selectOption
  • check
  • validation

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

fill replaces text in an editable input. selectOption chooses a native select value, and check sets a checkbox to its checked state. These express intent more clearly than simulated keystrokes when the requirement is simply to enter a value.

Test both accepted and rejected input. A form that accepts a valid title but silently accepts a blank one is incomplete. The practice form uses its own validation so the error is a stable part of the interface.

Assert the visible outcome after submission: the row, chosen priority and status message. Checking only that the click succeeded says nothing about whether the data was accepted.

The app deliberately stores browser-local tasks. Notification is only a label in this lab; no email or external message is sent. A test should not imply a real integration exists because the UI says notification.

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('submit a high-priority task', async ({ page }) => {
  await page.goto('/');
  await page.getByLabel('Task title', { exact: true }).fill('Practice forms');
  await page.getByLabel('Priority', { exact: true }).selectOption('high');
  await page.getByLabel('Notify me').check();
  await page.getByRole('button', { name: 'Add task', exact: true }).click();
  await expect(page.getByTestId('task-row')).toContainText('high priority');
  await expect(page.locator('#task-status')).toHaveText('Task added with notification');
});
test('reject a blank title', async ({ page }) => {
  await page.goto('/');
  await page.getByLabel('Task title', { exact: true }).fill('   ');
  await page.getByRole('button', { name: 'Add task', exact: true }).click();
  await expect(page.getByRole('alert')).toContainText('Enter a title');
  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-forms-inputs-and-validation",
  "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. Selecting by value avoids depending on the option’s position.
  2. check is idempotent: it aims for a checked state.
  3. The negative test verifies both the error and absence of a saved task.

Verify your result

  • Two tests pass independently.
  • Blank input adds no row.

4. Practice independently

Verify that the title input is cleared after a successful submission.

Show the explanation

Add await expect(page.getByLabel("Task title", { exact: true })).toHaveValue("") after the successful submission.

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 ↗