Login, signup, email verification, 2FA, the device flow, OAuth consent, invitation acceptance and password reset
Testing an eight-screen auth funnel that has no test ids
What makes it hard
Eight screens stand between a stranger and a session, and not one of them carries a data-testid. Every control is reachable only by its visible copy, and that copy comes out of a translation catalogue.
That has a consequence people usually miss. I click the "Sign in" button is not a locale-neutral assertion — it is an English assertion, silently, on a product that ships six catalogues. The suite is pinned to one of them and nobody wrote that down.
The state machines are worse than the fields:
- the device-authorisation card has four states — code entry, consent, authorised, error — and the only thing that distinguishes them is prose
- the invitation page has one error screen per redirect code, all of them rendered as a sentence
- OAuth consent lists a variable number of scopes, and a scenario needs to assert which ones
How to cover it
Start from the locator order and be honest about where this surface lands in it: role and accessible name, then label, then test id, then placeholder, then text, then CSS. With no test ids, role plus name genuinely is the best available handle — the problem is that the name is translated, not that the strategy is wrong.
So do two things at once.
- Pin the locale in the environment file (
use.locale) so the implicit assertion becomes an explicit one. A suite that only passes in English should say so inenvs/<env>.yaml, not in a reviewer's memory. - Put the test id you have asked for into the heal context now, before it exists. It costs nothing while it is missing, and the day it lands it becomes the recovery path without a feature-file edit.
readonly submit = this.heal.locator(
this.page.getByRole('button', { name: 'Sign in' }),
{ role: 'button', name: 'Sign in', testId: 'login-submit', description: 'login submit' },
);The healer only probes alternatives when the primary fails, so this is not a fallback you pay for on the happy path.
What to ask the application team for, in priority order: one test id per control; a data attribute carrying the state on any card that has more than one (data-device-state="input|consent|authorized|error"), so a scenario asserts a state machine instead of matching a sentence; and an error code on each refusal screen rather than only a message.
Two things SDODS will not do for you here, plainly:
- there is no email assertion of any kind, so the verification link and the invitation link are unreachable from Gherkin. Seed the accepted state through the API and let the UI scenario start after the link, or stop the scenario honestly at "the code was sent".
- there is no multi-tab step, so an OAuth popup cannot be driven from a shared step.
BasePagedoes exposewaitForPopup(), so a project step in a page object can drive it — the shared library will not.
Scenario sketch
@ui @smoke @user:unverified
Scenario: An unverified account is held at the verification screen
Given I use a leased user with role "unverified"
When I navigate to the "login" page
And I fill the "Email" field with "{{email}}"
And I fill the "Password" field with "{{password}}"
And I click the "Sign in" button
Then the page URL should contain "/verify"
And I should see the text "Enter the code we sent"
@ui @regression
Scenario: The device flow refuses a code that has expired
Given I navigate to the "device" page
When I fill the "Code" field with "{{expiredDeviceCode}}"
And I click the "Continue" button
Then the element with test id "device-card" should contain "error"The second scenario is written against a test id and a state attribute that do not exist yet. That is deliberate: it is the request, in the form the application team can act on, and it stays @fixme-free by being the one scenario you keep red on purpose until the attribute lands.
Further reading: locator strategy and page objects.
Other use cases
- A 403 that a shared cache is free to replayAuthorisation refusals on an API served through a CDN
- Plan caps, spending limits, and a gate that never firedBilling, usage and spending-limit routes behind plan entitlements
- A workflow canvas SDODS cannot dragA node-graph editor: 193 block types, edges, handles, sub-block inputs and subflow containers
- Ten scenarios said "Forbidden" and meant "you are out of workflows"A shared fixture workspace on a free plan with a three-workflow cap, holding twenty-three