Writing disabled-state tests for a cascading form — and what I found when one failed
I wrote the test. It passed. And it lied to me.
The form
Booking form: specialty → doctor → day → time. A classic cascading select. Each step unlocks the next.
The mental model writes itself: pick a specialty, the doctor dropdown enables. Pick a doctor, the day selector enables. Pick a day — and only then — the time slot selector enables.
I was testing the last step. Time select should be disabled until the user picks a day.
await page.selectOption('[data-testid="booking-specialty"]',
'Cardiology');
await page.selectOption('[data-testid="booking-doctor"]', { index: 0 });
await page.waitForSelector('[data-testid="booking-slot-day"]');
await expect(page.locator('[data-testid="booking-slot-time"]')
).toBeDisabled();
The test failed.
received: enabled
What I expected to find
Timing. A missing waitFor. The element hadn't fully loaded yet.
Added waitForLoadState. Added an explicit waitFor for the time element. Still: enabled.
Not a timing issue.
What was actually happening
Opened the page source. Two lines in the slot handler:
slotDayEl.value = dayKeys[0];
fillTimeOptionsForDay(dayKeys[0]);
When slots loaded, the page automatically selected the first available day — without any user input. Then it immediately filled the time options for that day.
The state "time select disabled before user picks a day" didn't exist in this product. By the time slots appeared, a day was already chosen.
This was an intentional UX decision — reduce clicks, pre-select the most sensible default.
The lie
I had previously written a different version of this test:
await expect(page.locator('[data-testid="booking-slot-time"]')
).toBeEnabled();
That test passed. I marked the behavior as verified.
What did it actually verify? That the time select was enabled after slots loaded.
What it didn't verify: whether the user had done anything to make it enabled.
The test confirmed the element's state. It said nothing about how the system got there.
If I had only written the toBeEnabled() version — if I'd never tried to test the disabled state — the CI would have stayed green, and I would have shipped a complete misunderstanding of how the form worked.
The failure taught me more than the passing test did.
The fix
The actual behavior: time select is disabled at page load, before any doctor is chosen. It enables when slots load — because the page auto-selects the first available day.
The correct test captures what actually happens:
// page load — no doctor selected yet, time is disabled
await expect(timeSelect).toBeDisabled();
// doctor selected, slots load, first day auto-selected — time enables
await page.selectOption(doctorSelect, { index: 0 });
await page.waitForSelector(slotDaySelect);
await expect(timeSelect).toBeEnabled();
This test documents what the system does — not what I assumed it would do.
Why it matters
A passing test doesn't tell you the system works the way you think it works.
It tells you the assertion succeeded given the actual system state — whatever produced that state.
The toBeEnabled() test confirmed a state. Not a behavior. The difference only became visible when I tried to test the opposite and the test failed.
Some of the most useful information about a system comes from tests that fail in unexpected ways.
The hidden assumption "I assumed a cascading form means each step requires explicit user selection. The product had a different model — reduce clicks by pre-selecting sensible defaults."
Part of the "Hidden Assumptions in Test Automation" series.
Full project (API + UI + E2E + CI + AI endpoint): GitHub
Top comments (0)