Skip to content

← All use cases

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
A form field is the easy half. Ask which states a screen has before you ask which controls it has — states are what a rename actually breaks.

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 in envs/<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.
ts
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. BasePage does expose waitForPopup(), so a project step in a page object can drive it — the shared library will not.

Scenario sketch

gherkin
@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