Skip to content

The Test That Counts Requests, Not Pixels

Adityo Guni Waluyo

A Playwright test that intercepts network traffic to prove a form blocks submission client-side, plus a date-filter edge case that almost slipped through.

TL;DR

The test checks that submitting without a service pillar shows an inline error and sends no network request, verified via Playwright route interception. It fills all other fields and handles dev-only login plus a required phone setup wizard beforehand. It also adds coverage for a date filter edge case, ensuring today-to-today includes today using an exclusive bound the next day.

Count Requests, Not Pixels

The test was done rendering the form. Every required field filled neatly, except one. And the assertion I cared about was not the error text on screen. It was a counter: how many requests actually left the browser. If that number is not zero, the test fails, even if the page shows a beautiful inline error.

The acceptance criteria said the service registration form must have a pilar layanan (service pillar) selected, and submitting without it must be blocked on the client. Blocked, meaning: inline error shows up and the registration request never gets sent. Not "blocked" in the sense that the server politely rejects it afterwards. When the contract is about the network, checking the DOM is not enough, because a form can look validated while still firing the request behind your back.

So the test intercepts the network. Playwright bundles a test runner, assertions, isolation, and parallelization into one package [1], and the part this test leans on is page.route: catch everything headed to the registrations endpoint, record the URL, then let it through. The recorded array becomes the witness. Submit with the pillar missing, assert the error is visible, then assert the witness heard nothing.

Fill Everything Except One Field

For that assertion to mean anything, every other field has to be valid first. Bidang, contact, address, a description long enough to pass validation. Only the pillar is left empty on purpose. One missing field, so the gate is the only thing being tested.

Two details in this test were not obvious before writing it. First, the session comes from a dev-login endpoint that only exists with APP_ENV=development; anywhere else the test skips itself instead of failing. The token is injected into localStorage before the page loads. Second, a fresh dev profile has no phone number yet, so the app's data-completion wizard shows up first and the test has to fill the wizard before it even reaches the registration form. What reads as one step in the index is actually three.

A Date Nit That Almost Slipped Through

The same commit carries a small revision marked as a review nit: the date-filter test got a new edge case. A row created today must still show up when the filter's from and to are both set to today. With a pure less-than upper bound against the date, a row stamped with today's date drops out of the result, while any user picking "today to today" obviously expects today's data.

The upper bound was never written as a naive less-than-or-equal. If the column is a datetime, comparing against a bare date reads as midnight, so rows from that day's afternoon and evening would vanish. The pattern in the query is an exclusive bound one day past the end: < DATE_ADD(?, INTERVAL 1 DAY), which makes the whole to day inclusive. DATE_ADD() adds time values, intervals, to a date value [3], and MySQL comparison operators keep returning true, false, or NULL as usual [2], so the boundary is testable behaviour, not magic. The commit's job is locking it in a test so nobody flips it back quietly.

I have met this boundary before while debugging lying counters with date filters: exclusive upper bound targeting the next day. Range traders always get caught at the edges, and now this edge is pinned down by a test.

Tests About Absence

Something else changed around the test. The layanan row in the unit-testing index got updated, from "login gate" to "submit without pillar blocked client-side (inline error, request not sent)". The index is what CI reruns, so the written contract and the executed tests moved together.

Writing a test that verifies the absence of something, a request that must not happen, felt like overkill to me once. Routing traffic, counting requests, just to prove a form does not send what it looks like it does not send. Then I spent one too many evenings on bugs shaped like "the form looked valid, but the data arrived anyway". The gap sits between what appears blocked and what is proven not sent. This test closes that gap, and it never needed server access to make the point.

Sources

Related articles