The Toggle Is the Test Condition
A discount field whose validation changes with a sibling selector, a discount that must be absent before it appears, and a rule enforced only in the UI — three controls where the state change is the test.
You tested the field. You entered a valid value, you entered an invalid one, the error appeared where it should.
You did not test the control.
Those are different jobs, and the difference is where a specific class of defect lives — the class where every individual value behaves correctly and the sequence of values does not.
A Field That Changes Its Own Rules
A discount can be entered two ways. Percentage, or absolute amount. A selector next to the field decides which.
The validation follows the selector. As a percentage, the value is capped at 100. As an absolute amount, it is capped at the price of the product. Two different ceilings, two different failure messages, one input box.
And the backend does not care about either representation. Percentage discounts are converted to absolute before anything reaches the billing system. What the agent sees on screen and what the integration stores are two descriptions of the same discount, and only one of them is durable.
There are two test conditions here that no amount of valid-and-invalid value testing will reach.
The first is the switch itself. Type a value, then change the type. Does the validation re-evaluate against the new ceiling, or does the previous type's verdict survive the change? A number that was legal as a percentage can be illegal as an amount on a cheap product — but the error state, the disabled button, the green tick, all of them were computed under the old rule, and none of them are obliged to recompute themselves when the rule changes underneath.
The second is the representation. Assert the converted absolute amount in the payload, not the percentage on screen. Those are the same number only if the conversion is right, which is precisely the thing under test.
The Mechanism
A control that owns its own validation logic is a small state machine, and you have been treating it as an input box.
State machines are not tested by their states. They are tested by their transitions. Every field on screen whose behaviour is governed by a sibling control — a type selector, a radio group, a checkbox, a picklist that filters another picklist — has transitions, and the transitions are where stale state lives.
The value is one dimension. The path to that value is another. Most test cases only have the first.
Absence Is Half the Test
The same blindness shows up on visibility.
In one cart, a set of discounts is conditional. A parent product's discount and four time-limited free-period discounts do not exist as options until that parent product is added to the cart. Add the product, and they appear.
The typical test: add the product, confirm the discount is available, apply it, confirm the net comes out at zero. Green.
That test verifies that a discount can be applied. It says nothing whatsoever about the condition, because the condition is what happens when the product is not there — and that half was never executed.
Conditional visibility is one test with two steps, and neither step is a test on its own:
- Confirm the element is absent first. Open the cart without the trigger product and record that the discount is not offered, not visible, not selectable. This is the step people skip, because a screen with nothing on it does not feel like evidence.
- Then add the trigger and confirm it appears. Only now has the conditional logic been exercised in both directions, and only now can the case fail if someone removes the condition and makes the discount permanently available.
Run the second step alone and you have a click-through with a green tick on it. It will keep passing after the condition is deleted entirely.
The Third Question: Where Is the Rule Enforced?
Two add-on products in the same family are mutually exclusive. Pick one, the other becomes unavailable.
That constraint lives in the UI. There is no back-end validation and no API guard behind it. And the scope decision for that piece of work explicitly ruled out testing the API bypass — a reasonable decision, made deliberately.
Which means a technically invalid order, carrying both products, can be created by anything that does not go through the cart.
The test that proves the UI blocks the combination is a good test. It is also a test with a boundary, and the boundary belongs in writing. Not in a bug report, not in a conversation — in the test plan, as a sentence:
"Mutual exclusivity validated at UI level; API-level enforcement out of scope."
That sentence is worth more than ten bullet points about happy paths. It converts an unknown into a documented, owned risk. When an integration later creates that order and somebody asks how it got through, the answer already exists and has a date on it.
Why This Escapes
Because all three cases pass. Every one of them.
The percentage discount validates. The absolute discount validates. The conditional discount appears when the product is added. The mutually exclusive pair is blocked in the cart. Any reviewer reading the test cases sees correct expectations and correct results, and there is nothing in the document to point at.
What is missing is not an assertion. It is a dimension. Nobody wrote the case where the type changes, because the test case template has a field for input and a field for expected result and no field for how you got there. Nobody wrote the absence step, because the artefact of that step is an empty screen. Nobody wrote down the enforcement layer, because the layer is not visible in the requirement — only in the code.
So the questions have to be asked during analysis, before the template starts shaping the thinking:
Does anything on this screen change the rules for anything else on this screen? Is anything here conditional, and have I tested the condition or only the consequence? And at which layer is each rule actually enforced?
Three questions. They take a minute and they change what the suite is capable of catching.
Which control in your current suite is tested only in its final state — and never on the way there?