Story E2E (Playwright) — a viewer, a member and an admin walk the same four flows and are refused in exactly the right places
The story's end-to-end receipt: the verification_recipe on the story, automated. It answers the question the unit tests cannot — did anything a member or admin does today stop working? — which for a story that revokes capability matters more than proving the revocations landed.
The walk
Three actors on the same seeded project, driving the real UI:
| Actor | Must still be able to | Must now be refused |
|---|---|---|
| admin | every flow below, unchanged | nothing |
| member | run a planning job, edit and rank work items, read every report, save a filter | run an importer, delete or archive a work item |
| viewer | browse the board, the backlog, the sprint list and every report | start or complete a sprint, move an item into a sprint, run a planning job, accept a triage item, author a saved filter |
Plus one actor the UI cannot represent and the test must: a workspace member with NO project membership on an open project, who may still browse and edit but may not run the planner.
How to write it so it does not lie
- Assert the refusal where the user meets it, not on a network response the page swallows. This story adds no UI treatment for a refusal — that is the permission-gated UI story's — so the honest assertion for a refused write is the state did not change: the sprint is still not started, the item is still in the backlog, the row is still there. Assert on the authoritative post-condition, never on a toast this story did not build.
- The "still works" half needs the same care. Every positive row above is a regression check on shipped behaviour, and it is the half that catches an over-tightened gate. Give it as many assertions as the refusals get.
- Wait on authoritative signals per the repo's E2E discipline, not on timeouts, and pin the workspace and active-project cookies rather than relying on whatever the seed left active.
Boundary
No acceptance video. The story ships no user-observable surface of its own — no page, panel or control — and the only visible difference is a refusal the shipped client already renders however it renders it. It is a NON-UI story by the acceptance-video rule's own test and accepts on its tests, exactly as the administrative split did. If a reviewer wants a clip, that is a signal the boundary moved and should be raised rather than absorbed here.
Acceptance criteria
- One Playwright spec covers all three roles plus the no-membership actor, seeded through the existing E2E fixtures rather than through the UI.
- Every refusal is asserted as an unchanged post-condition, and every "still works" is asserted as a changed one.
- The spec fails if a gate is removed: prove it once by reverting one gate locally, watching the spec go red, and restoring it — say so in the PR body.
- No assertion depends on a toast, banner or disabled state, since none is this story's deliverable.
- The spec is registered in the same shard configuration as the other permission E2Es so it runs in CI, and its runtime is reported in the PR body.
pnpm test:e2e --grep <this spec>is green locally against a production build.
Context refs
- The administrative split's E2E — the sibling spec, its fixtures and its role-switching helpers.
motir-core/CLAUDE.md§ E2E — the authoritative-wait discipline.- The story — the
verification_recipethis spec automates. - The permission-gated UI story — the owner of everything this spec deliberately does not assert.