Example Application & Test Layout

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

Reliable E2E examples need an application that behaves the same every time. Every example in this section runs against Bookshelf, a small original web application written for the purpose: a server-rendered catalogue of 24 fictional books with search and pagination, a detail page with an SVG cover, reader and admin accounts, an "add review" form with an optimistic user interface, light and dark themes, a responsive layout and a pair of test-only endpoints that reset and seed the data. It uses only Node.js, keeps all data in memory and has no dependencies.

Bookshelf is a throwaway application that is not published with this documentation. This page therefore documents the contract the examples rely on, so you can run them against your own application by changing the base URL and the selectors, or rebuild an equivalent in an afternoon. Everything here was checked on 25 October 2026 with Node.js 24; the tools are Playwright 1.64, Cypress 16.2 and Selenium 4.51 (see the section home page for the full baseline).

Starting the application

cd bookshelf
npm start                                   # http://localhost:3000
PORT=4000 npm start                         # another port
BOOKSHELF_REVIEW_DELAY_MS=100 npm start     # faster review saves for tests

The server was started with these commands for every run in this section. The variables it reads are:

Variable Default Effect

PORT

3000

Port the server listens on.

BOOKSHELF_READER_PASSWORD

a throwaway value for local runs

Password of the reader user. Tests read it from the same variable instead of hard-coding it.

BOOKSHELF_ADMIN_PASSWORD

a throwaway value for local runs

Password of the admin user.

BOOKSHELF_REVIEW_DELAY_MS

400

Delay added to POST /api/reviews so the optimistic "pending" state is observable. Lower it in test runs.

BOOKSHELF_TEST_API

on

Set to off to disable /api/test/*, as a production-like server would.

Passwords come from the environment; the examples in this section read BOOKSHELF_READER_PASSWORD and never put a password in a test file. Test projects take the base URL from BASE_URL, defaulting to http://localhost:3000.

Routes and roles

Route Access What it shows

/

public

Home page with a search form and three featured books.

/books

public

Catalogue, six books per page. Query parameters q (search title, author or genre) and page.

/books/:id

public

Book detail: cover, description, average rating and the reviews. Signed-in users also get the "add review" form; others get a "sign in" link. An unknown id returns a 404 page.

/signin

public

Sign-in form. A signed-in user is redirected to /account. The next query parameter chooses where to go afterwards.

/account

reader or admin

"Hello" heading, the role and the user’s own reviews. Anonymous visitors are redirected to /signin?next=/account.

/admin

admin

Statistics, a "Reset data" button and every review. A reader gets a 403 page; anonymous visitors are redirected to sign in.

Two users exist: reader (role reader) and admin (role admin). The session is a cookie named bookshelf_session, marked HttpOnly and SameSite=Lax.

The REST API

Endpoint Behaviour

GET /api/books?q=&page=

JSON { items, total, page, pageSize, pages }.

GET /api/books/:id

One book plus averageRating; 404 when unknown.

GET /api/reviews?bookId=

Reviews, optionally for one book.

POST /api/reviews

Requires a session (401 otherwise). Body { bookId, rating, text }. Validation: rating 1 to 5 and text of at least 10 characters, otherwise 422 with { errors: { ... } }. A text containing the word FAIL returns 500, which makes the optimistic row roll back so rollbacks can be tested. Returns 201 with the saved review.

GET /api/session

{ authenticated, name, role }.

POST /api/session

Body { username, password }; sets the cookie, 401 for bad credentials.

DELETE /api/session

Signs out.

POST /api/test/reset

Restores the 24 seeded books and 3 seeded reviews and clears every session.

POST /api/test/seed

Replaces the books and/or reviews with the given arrays; missing book fields get defaults.

The test endpoints let a test arrange state through HTTP instead of clicking through the user interface, which is faster and less flaky (the principle is explained in Test Design & Page Objects). These calls were run against a live server:

curl -s -X POST localhost:3000/api/test/reset
# {"books":24,"reviews":3}

curl -s -X POST localhost:3000/api/test/seed -H 'content-type: application/json' \
  -d '{"books":[{"title":"Only Book","author":"Test Author"}],"reviews":[]}'
# {"books":1,"reviews":0}

curl -s localhost:3000/api/books
# {"items":[{"id":1,"year":2020,"price":12.5,"genre":"Fiction", ... "title":"Only Book","author":"Test Author"}],"total":1,"page":1,"pageSize":6,"pages":1}

curl -s -X POST localhost:3000/api/reviews -b cookies.txt -H 'content-type: application/json' \
  -d '{"bookId":1,"rating":9,"text":"x"}'
# {"errors":{"rating":"Choose a rating from 1 to 5","text":"Write at least 10 characters"}}

Source: curl: manual

The last call assumes a cookies.txt jar written by a prior curl -c cookies.txt -X POST localhost:3000/api/session ... call with the reader’s credentials.

data-testid conventions

Elements a test needs carry a data-testid attribute whose name describes the thing, not the markup: book-card, book-title, search-input, review-submit. The conventions:

  • Lower-case, hyphen-separated nouns naming the element’s role in the page, not its position or style (page-next, not blue-button-2).

  • Containers and items are separate ids: book-list holds book-card items; review-list holds review items.

  • State is an attribute, not a class: a review row has data-state="pending" while the request is in flight and data-state="saved" afterwards. Tests assert on the attribute. Book cards carry data-book-id.

  • Prefer roles and labels first. Inputs also have labels ("Username", "Password", "Rating", "Your review") and buttons have names, so tests can use getByRole and getByLabel; the test ids are the fallback for things with no accessible name, as the locator guidance in the Playwright and Cypress best-practice pages recommends.

The most used ids:

Test id Element

site-header, main-nav, brand

Header, main navigation and logo link.

nav-signin, nav-account, nav-admin, signout

Navigation entries that change with the session.

theme-toggle

Button that switches light and dark themes and stores the choice in localStorage (data-theme on the html element).

search-input, search-submit, result-count, empty-state

Search form, the "N books found" live region and the no-results message.

book-list, book-card, book-title, book-author

Catalogue list and its items.

pagination, page-prev, page-next, page-indicator

Pager.

book-detail, book-cover, book-price, book-description, average-rating

Detail page.

review-list, review, review-author, review-rating, review-text, no-reviews

Review list.

review-form, review-rating-input, review-text-input, review-submit, review-error

"Add review" form and its validation message.

signin-form, username, password, signin-submit, signin-error

Sign-in form.

account-heading, account-role, my-reviews, admin-heading, admin-stats, admin-reset, all-reviews, forbidden

Account and admin pages.

Base URLs and configuration per tool

Every tool receives the application address from configuration, never from the test code, so the same tests run against a local server, a container or a staging host.

// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  use: {
    baseURL: process.env.BASE_URL ?? 'http://localhost:3000',
    trace: 'on-first-retry',
    testIdAttribute: 'data-testid',
  },
  // Start Bookshelf unless BASE_URL points at a server that is already running.
  webServer: process.env.BASE_URL
    ? undefined
    : {
        command: 'node server.js',
        cwd: '../../bookshelf',
        url: 'http://localhost:3000',
        reuseExistingServer: true,
        env: { BOOKSHELF_REVIEW_DELAY_MS: '100' },
      },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
    { name: 'webkit', use: { ...devices['Desktop Safari'] } },
  ],
});
// cypress.config.ts
import { defineConfig } from 'cypress';

export default defineConfig({
  e2e: { baseUrl: 'http://localhost:3000', video: false, setupNodeEvents() {} },
  // Read in tests with cy.env(['READER_PASSWORD']); override with CYPRESS_READER_PASSWORD.
  env: { READER_PASSWORD: 'reader-local-only' },
});

The Cypress example places a throwaway local value in the configuration file so the tests run unchanged; a real project keeps real credentials out of the file and supplies them through a CYPRESS_ environment variable or a git-ignored cypress.env.json, which is the practice Cypress’s own best-practice page recommends for secrets read with cy.env().

For Selenium, the base URL is a constant or an environment variable in the test code. The Java and Python versions used throughout the section:

// src/test/java/demo/ReviewPageObjectTest.java (excerpt)
static final String BASE_URL = System.getenv().getOrDefault("BASE_URL", "http://localhost:3000");
static final String READER_PASSWORD =
    System.getenv().getOrDefault("BOOKSHELF_READER_PASSWORD", "reader-local-only");
# tests/conftest.py (excerpt)
BASE_URL = os.environ.get("BASE_URL", "http://localhost:3000")
READER_PASSWORD = os.environ.get("BOOKSHELF_READER_PASSWORD", "reader-local-only")

When tests run inside a container and Bookshelf runs on the host, use http://host.docker.internal:3000 as the base URL on Docker Desktop. The Playwright container run in this section used docker run --rm --init --ipc=host -e BASE_URL=http://host.docker.internal:3000 -v $PWD:/work -w /work mcr.microsoft.com/playwright:v1.64.1-noble npx playwright test; the configuration above skips webServer whenever BASE_URL is set.

Project layout per tool

playwright/
  package.json
  playwright.config.ts
  tests/
    fixtures.ts                  # extends test with page objects and a reset
    pages/
      book-details-page.ts
      sign-in-page.ts
    smoke.spec.ts
    review-page-object.spec.ts
cypress/
  package.json
  cypress.config.ts
  tsconfig.json
  cypress/
    e2e/
      smoke.cy.ts
      review-page-object.cy.ts
    support/
      e2e.ts                     # loaded before every spec
      commands.ts                # custom commands such as cy.signIn()
      pages/
        book-details-page.ts
selenium-java/
  pom.xml                        # selenium-java 4.51, JUnit Jupiter 5.14, surefire
  src/test/java/demo/
    pages/
      BookDetailsPage.java
      SignInPage.java
    SmokeTest.java
    ReviewPageObjectTest.java
selenium-python/
  .venv/                         # python >= 3.10 for Selenium 4.51
  tests/
    conftest.py                  # fixtures: driver, base_url, reset_data
    pages/
      __init__.py
      book_details_page.py
      sign_in_page.py
    test_smoke.py
    test_review_page_object.py

Playwright test files end in .spec.ts and are found in testDir; Cypress specs end in .cy.ts under cypress/e2e; Maven’s Surefire plugin runs classes under src/test/java whose names match *Test; pytest collects test_*.py files. The Playwright, Cypress, Selenium Java and Selenium Python projects listed here were all run against Bookshelf for this page (the page-object specs from Test Design & Page Objects and the smoke specs).

What changed recently

  • Playwright 1.64 requires Node.js 22, 24 or 26.

  • Cypress 16 needs Node.js 22, 24 or 26 (Node 20 and 25 were dropped) and removed Cypress.env(): tests read secrets with cy.env() and public values with Cypress.expose().

  • Selenium 4.51 for Python needs Python 3.10 or later; the Node.js binding requires Node.js 22 or later.

  • Docker images for each tool are versioned with the tool: mcr.microsoft.com/playwright:v1.64.1-noble, cypress/included:16.2.0 and selenium/standalone-chrome:4.51.0.