Skip to content
Practical lab

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.

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.

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

assessment.html — the form (as served)
<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>
assessment.js — validation (as served)
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.';
});
npx playwright test — first run (this morning)
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?

  1. 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.
  2. The browser blocks it: the input is required.
  3. The server rejects it, and the page shows the server's error.
  4. 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?

  1. 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.
  2. Nothing: /contact has a form too.
  3. 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?

  1. 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.
  2. The tests are flaky; they should be rerun.
  3. 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?

  1. 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.
  2. That the form works.
  3. 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

npx playwright test — after the fix
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 of 4 of the Conversion Functionality Lab: the form markup and the validation script as served, with the question of what happens when the email field is empty.
    1. Step 1: read the markup and the script together.
  2. The wrong option 'the browser blocks it' is checked; the red feedback points at the email input's missing required attribute and the form's novalidate.
    2. A wrong answer, explained line by line.
  3. The correct option — the page shows Thanks and posts anyway — is checked, with the green explanation and the Next step button.
    3. The matching answer: the false success state.
  4. The end of the lab: the passing Playwright run after the fix, with the note that the endpoint was mocked, the score, Save to My Project and the links onward.
    4. Green is exactly as wide as the assertions.