Fifteen minutes, four decisions. A landing page the owner says "works — I filled it in myself". You get the form markup, its validation script, and a browser test somebody wrote and never ran. Find the state the owner never tested, the button that breaks its own promise, and what a green run does and does not prove.
Conversion Functionality Lab
A landing page that says Thanks without an email address and a button that breaks its own promise. Find both, read the failing check, then the passing one.
The interactive version could not be loaded. The complete lab, including every answer, is on this page below.
Declared sample data
The page, the script and the test output are fixtures written for this lab. The landing page is included in the fixture bundle so you can open it locally and run the test yourself; it posts to nothing.
Evidence
<a class="cta" href="/contact">Book a free 15-minute assessment</a> <form id="book" action="/api/book" method="post" novalidate> <label for="name">Your name</label> <input id="name" name="name" required> <label for="email">Email — we confirm your slot here</label> <input id="email" name="email" type="text"> <label for="slot">Preferred time</label> <select id="slot" name="slot"><option>Morning</option><option>Afternoon</option></select> <button type="submit">Request my assessment</button> <p id="msg" role="status"></p> </form>
form.addEventListener('submit', function (ev) {
ev.preventDefault();
if (!name.value.trim()) { msg.textContent = 'Please enter your name.'; return; }
// email is checked server-side
fetch('/api/book', { method: 'POST', body: new FormData(form) });
msg.textContent = 'Thanks! We will email you to confirm.';
});Running 3 tests using 1 worker
✓ 1 assessment.spec.ts:6 › required name shows an error (412ms)
✘ 2 assessment.spec.ts:14 › empty email is rejected before a success message (1.1s)
✘ 3 assessment.spec.ts:27 › primary CTA lands on the booking form (688ms)
2) assessment.spec.ts:14 › empty email is rejected before a success message
Error: expect(locator).toHaveText(expected)
Locator: getByRole('status')
Expected pattern: /email/i
Received string: "Thanks! We will email you to confirm."
3) assessment.spec.ts:27 › primary CTA lands on the booking form
Error: expect(page).toHaveURL(expected)
Expected: /\/book$/
Received: "http://127.0.0.1:4173/contact"
2 failed, 1 passed (3.4s)Steps
Step 1 — Which validation state is broken?
Read the markup and the script together. Ask: what happens when the email field is empty?
Evidence: assessment.html — the form (as served), assessment.js — validation (as served)
What does the page do with an empty email?
- It shows 'Thanks!' and posts the form. The email input has no required attribute and type="text", the form is novalidate, and the script checks only the name — so a visitor who skips the one field the clinic needs is told it worked.
- The browser blocks it: the input is required.
- The server rejects it, and the page shows the server's error.
- Nothing is broken; the owner filled it in and it worked.
Answer and explanation
It shows 'Thanks!' and posts the form. The email input has no required attribute and type="text", the form is novalidate, and the script checks only the name — so a visitor who skips the one field the clinic needs is told it worked.
Three things combine: no required on the email field, novalidate on the form (so the browser would not check even type="email"), and a script that returns early only for a blank name. The success message is written unconditionally after fetch() is called — not after it succeeds. Anyone who fills the form in fully never sees this; that is why 'I tried it myself' proves nothing about the states you did not try.
Step 2 — Where does the button go?
The label promises a booking. Read the href.
Evidence: assessment.html — the form (as served)
What is wrong with the primary call to action?
- It links to /contact — a generic contact page — while its label promises the booking form. The visitor who clicks the promise lands on something else and has to find the form again.
- Nothing: /contact has a form too.
- The label is too long.
Answer and explanation
It links to /contact — a generic contact page — while its label promises the booking form. The visitor who clicks the promise lands on something else and has to find the form again.
Message match is not only ad-to-page; it is label-to-destination. A button that says 'Book' and lands on 'Contact us' breaks the promise at the moment of highest intent. The third test in the run asserts exactly this and fails with the received URL.
Step 3 — Read the failing run
Someone wrote the test last month and never ran it. Read the two failures.
Evidence: npx playwright test — first run (this morning)
What do the two failures prove?
- Test 2 proves the empty-email state shows the success message (received 'Thanks!', expected a message about email). Test 3 proves the CTA lands on /contact. Both faults are reproduced by a check, not an opinion.
- The tests are flaky; they should be rerun.
- That bookings are not being emailed to reception.
Answer and explanation
Test 2 proves the empty-email state shows the success message (received 'Thanks!', expected a message about email). Test 3 proves the CTA lands on /contact. Both faults are reproduced by a check, not an opinion.
The value of a test that fails is that it names the received value. 'Received: Thanks! We will email you to confirm.' is the whole diagnosis of test 2 in one line. Note the test does not prove anything about email delivery — the fixture posts to nothing and the suite mocks nothing about the inbox.
Step 4 — After the fix: what does green prove?
The email field gained required and type="email", the script checks it, the success message now waits for a 2xx response, and the CTA points at #book. Read the second run.
Evidence: npx playwright test — after the fix
What does the passing run establish?
- That the two states the tests cover now behave as specified — the empty email is rejected before any success message, and the CTA lands on the form. Nothing about delivery, and nothing about states the suite does not cover.
- That the form works.
- That bookings will go up.
Answer and explanation
That the two states the tests cover now behave as specified — the empty email is rejected before any success message, and the CTA lands on the form. Nothing about delivery, and nothing about states the suite does not cover.
Green is exactly as wide as the assertions. This suite covers three states; a fourth (a malformed email such as 'bob@') would need a fourth test. And the mocked route in the passing run means the page's request was intercepted — the real endpoint was never called, by design, because this ran on a laptop, not production.
The corrected version
Running 3 tests using 1 worker
✓ 1 assessment.spec.ts:6 › required name shows an error (398ms)
✓ 2 assessment.spec.ts:14 › empty email is rejected before a success message (455ms)
(route /api/book mocked: fulfilled 200 {"ok":true} — no real request left the page)
✓ 3 assessment.spec.ts:27 › primary CTA lands on the booking form (301ms)
3 passed (2.1s)Two faults a person testing the happy path never sees, both reproduced by a check that prints what it received. The Playwright guide builds this suite from scratch, including the mocked submission, and the contact-form Weekly Fix covers the part no browser test can: whether the message arrived.
Walkthrough
Four screenshots of this lab, taken from the published page: the start, a deliberately wrong answer, the matching answer, and the corrected version. Every screen is described in the image text.

1. Step 1: read the markup and the script together. 
2. A wrong answer, explained line by line. 
3. The matching answer: the false success state. 
4. Green is exactly as wide as the assertions.
Take it to your own site
- How to audit a landing page with Claude Code — the guide this lab is drawn from
- Generate a functional-validation checklist for your own page
- Sample landing page conversion audit
- Claude Code Conversion & Revenue Optimization Toolkit — The functional checks in the toolkit are the runnable version of the four questions in this lab, for every landing page you own.