A UX test of client-side validation: open Sign up, click Submit with no name, and expect “Name is required”. Playwright and Cypress express the same user path.
SUT: js-ui · id: ux.form-validation
Cached CI results from 10/3/2026, 10:14:14 AM (ci · d7784ca)
Playwright launches a real browser (Chromium here), gives you a page object, and uses locators plus expect() to assert what the user sees. Traces and auto-waiting make flaky clicks easier to debug than raw WebDriver.
webServer starts samples/js-ui. Each test opens a page, interacts through roles and CSS, and asserts URL or visible text. The SUT is HTML/CSS/JS in the browser — not an imported function.
Testing architecture
UX / E2E tests drive a real browser against samples/js-ui. The test is a user path: click a link or submit a form, then assert what appears on screen or in the URL. Failures point at markup, CSS, or client JS — not at a unit-tested helper alone.
The browser loads signup.html. Submit is clicked with an empty Name field. The page’s own JavaScript (not the HTML5 tooltip) reveals #name-error. The test asserts visibility and exact copy — that is the UX contract.
SUT: js-ui · samples/js-ui/public/signup.html · run npm test in examples/ux/form-validation/playwright
Code under test · samples/js-ui/public/signup.html
Cypress runs tests in (or beside) the browser with a chainable API: cy.visit, cy.click, cy.should. The open-source runner is enough for these demos; we do not use Cypress Cloud.
start-server-and-test boots js-ui, then cypress run visits the page, clicks, and asserts. Commands retry until the assertion passes or times out — that retry is Cypress’s default stability model.
Testing architecture
UX / E2E tests drive a real browser against samples/js-ui. The test is a user path: click a link or submit a form, then assert what appears on screen or in the URL. Failures point at markup, CSS, or client JS — not at a unit-tested helper alone.
The browser loads signup.html. Submit is clicked with an empty Name field. The page’s own JavaScript (not the HTML5 tooltip) reveals #name-error. The test asserts visibility and exact copy — that is the UX contract.
SUT: js-ui · samples/js-ui/public/app.js · run npm test in examples/ux/form-validation/cypress
Code under test · samples/js-ui/public/app.js
// Increment the on-page counter (used by the navigate smoke path as a live control)
const increment = document.getElementById("increment");
const countOut = document.getElementById("count");
if (increment && countOut) {
increment.addEventListener("click", () => {
countOut.textContent = String(Number(countOut.textContent ?? "0") + 1);
});
}
// Client-side required-name check — the form-validation UX scenario asserts this message
const form = document.getElementById("signup-form");
const nameInput = document.getElementById("name");
const nameError = document.getElementById("name-error");
const success = document.getElementById("success");
const thanksName = document.getElementById("thanks-name");
if (form && nameInput && nameError && success && thanksName) {
form.addEventListener("submit", (event) => {
event.preventDefault();
const empty = nameInput.value.trim() === "";
nameError.hidden = !empty;
success.hidden = empty;
if (!empty) {
// textContent, never innerHTML — markup in the name must not run
thanksName.textContent = nameInput.value.trim();
}
});
}
Test · examples/ux/form-validation/cypress/signup.cy.js · MIT
· run in examples/ux/form-validation/cypress: npm test
// Cypress browser test — same empty-submit contract as Playwright
describe("signup form", () => {
it("empty submit shows Name is required", () => {
cy.visit("/signup.html");
// Click Submit without typing a name
cy.contains("button", "Submit").click();
cy.get("#name-error").should("be.visible").and("have.text", "Name is required");
});
});