Agent docs

2 · Deriving E2E cases from a surface doc

The method.

The method. Given a surface doc written to 01-documenting-a-surface.md, the test suite is a transcription rather than an invention.

Audience: an agent (or person) writing E2E tests from a surface doc.

Reference implementation: walmart-mvp/docs/testing/e2e-derivation.md, which carries this walk plus that app's own environment traps and suite layout.

§A The walk#

Setup comes from section 2, "How you get here". Each entry row is a navigation path. Prefer the real entry over a direct URL when the surface has one, because the direct URL skips whatever the real entry sets up. Where a row says a surface is reachable only externally, that is a case in itself: assert the redirect, not the page.

Gating cases come from section 3, "Who can see it". Every named gate is at least two cases, satisfied and unsatisfied. Where the doc says actions are ABSENT rather than disabled, assert absence. Those differ, and asserting the wrong one produces a test that passes against a broken build.

Positive cases come from section 5, one row each. The columns are already in test shape:

ColumnTest element
Preconditionarrange
Triggeract
Observable successassert
Observable failurethe negative case, same row

Every row yields at least two cases, because both outcomes are named. A row whose failure column reads "n/a" yields one, and the "n/a" is the doc saying so rather than an omission.

Negative and state cases come from section 6. Six rows, six cases, and the two usually missed are empty and permission-denied. Section 6 distinguishes loading-with-no-cache from loading-with-cache, and the cached row often renders nothing at all because the surface repaints instantly. A test that waits for a skeleton on a warm cache waits forever.

Data-shape cases come from section 7. The cache key tells you what to reset between specs. The revisit column is its own case: a surface marked "repaints from cache instantly" is asserted to show rows WITHOUT a loading state on return, and one marked "blanks on every open" is asserted to blank.

Section 8 tells you what not to file. A failing assertion matching a known-gaps line is a documented gap, not a discovery. Lines marked RULED carry a tracking pointer so the doc line is findable by grep when the fix lands; that policy lives at 01-documenting-a-surface.md §A5.

§B Preconditions, and the traps that pass for the wrong reason#

1. Reset the tenant between specs. Cache keys are org-namespaced (00-data-loading.md §A2), so a fixture that does not reset the active org lets a spec inherit the previous spec's cached rows. The spec then passes without exercising a fetch, which is the worst failure available: green, fast, and meaningless.

2. Every control the doc names must have an accessible name. A doc can describe an action perfectly and still not be transcribable: an icon-only button with no label cannot be reached by role, so the case has to fall back to a structural selector or cannot be written at all. Treat a missing accessible name as a product defect the derivation exposed, not as a docs problem. The kit derives one for icon-only buttons (explicit label, then tooltip, then icon key), committed 2026-08-09 and shipping with the next release, so the common case is covered by default. An explicit label is still better where the icon alone is ambiguous, and the derived name is what tells you which case you are in: a button announcing "Trash" names the glyph rather than the action, which is the signal to add a real label at that call site.

3. On a gated surface, assert the refusal SENTENCE. Never a phrase that also appears in the page chrome. A spec-viewer gate test asserting /Versable staff|staff only/i passed whether the gate held or not, because "Versable staff" is also the breadcrumb; the staff-side test passed for the same wrong reason. Assert the exact refusal copy the doc's section 3 records. This is the worst failure the method can produce, because it is green.

4. Know which controls are not what they look like. A design-system control often is not its native element. The kit Select is an aria-labelled button opening a role=listbox of role=option rows, so locator("select") matches nothing. That reads as "this control has no options" rather than as a bad locator, which invites a test.skip() that hides the case permanently. Assert on the real roles and on a known option count. Before writing a selector for any kit control, check what it actually renders.

§C The case nobody writes from imagination#

From walmart's Jobs surface, one section-5 row yields three cases, not two:

  1. Publish succeeds: a success toast naming what published.
  2. Publish throws: an ERROR toast.
  3. Publish finds nothing eligible: a WARNING toast, "Nothing was ready to publish. Check Error management for identity issues."

Case 3 is the whole argument for the method. The request succeeded and the answer was zero, so a suite asserting only "error on failure" is green while the behavior is untested. It reached the doc because the grid had a cell that had to be filled, and it reached the suite because the doc named its observable.

@versable-git/ui · reference, canon, and method, read in place