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
Source: Node.js: Command-line API
The server was started with these commands for every run in this section. The variables it reads are:
| Variable | Default | Effect |
|---|---|---|
|
|
Port the server listens on. |
|
a throwaway value for local runs |
Password of the |
|
a throwaway value for local runs |
Password of the |
|
|
Delay added to |
|
on |
Set to |
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. |
|
public |
Catalogue, six books per page. Query parameters |
|
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. |
|
public |
Sign-in form. A signed-in user is redirected to |
|
reader or admin |
"Hello" heading, the role and the user’s own reviews. Anonymous visitors are redirected to |
|
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 |
|---|---|
|
JSON |
|
One book plus |
|
Reviews, optionally for one book. |
|
Requires a session (401 otherwise). Body |
|
|
|
Body |
|
Signs out. |
|
Restores the 24 seeded books and 3 seeded reviews and clears every session. |
|
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, notblue-button-2). -
Containers and items are separate ids:
book-listholdsbook-carditems;review-listholdsreviewitems. -
State is an attribute, not a class: a review row has
data-state="pending"while the request is in flight anddata-state="saved"afterwards. Tests assert on the attribute. Book cards carrydata-book-id. -
Prefer roles and labels first. Inputs also have labels ("Username", "Password", "Rating", "Your review") and buttons have names, so tests can use
getByRoleandgetByLabel; 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 |
|---|---|
|
Header, main navigation and logo link. |
|
Navigation entries that change with the session. |
|
Button that switches light and dark themes and stores the choice in |
|
Search form, the "N books found" live region and the no-results message. |
|
Catalogue list and its items. |
|
Pager. |
|
Detail page. |
|
Review list. |
|
"Add review" form and its validation message. |
|
Sign-in form. |
|
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")
Source: Selenium: Getting started
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
Source: Playwright: Writing tests, Cypress: Writing and organizing tests, Maven: Standard directory layout, pytest: Good integration practices
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 withcy.env()and public values withCypress.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.0andselenium/standalone-chrome:4.51.0.