Skip to content
axxbetadocs
nextv0.2.26
GitHub
These are the docs of main, not released yet. See the docs of v0.2.26, the latest release.

Sign in to web apps

View .mdOpen in Claude

Every scenario has a browser context of its own, a fresh browser profile, so each one signs in: nothing a scenario signs in to is shared with another, which is what lets them run side by side (How the web-core pack works). There are two ways.

Signing in is pages, fields and buttons like any others, however many pages it takes: the browser follows your app to its identity provider and back, as it does for people. A Background does it for all the scenarios of a feature:

Background:
Given the parcels web app with the following properties:
| url | http://localhost:8400/portal |
And the "/account" page is opened
And the "Sign in" link is clicked
And the "Log in with Email" button is clicked
And the "Email Address" field is filled with "orders@fjord-outdoor.example"
And the "Password" field is filled with "${env:FJORD_OUTDOOR_PASSWORD}"
And the "Login" button is clicked
And the "Grant Access" button is clicked
And the page shows "Signed in as Fjord Outdoor"

This is the sign-in your shops go through, so it is checked in every scenario, and against a provider in your Compose file it takes about a second.

When a scenario is about what a signed-in shop does rather than about signing in, start its browser signed in, with what your app keeps its session in. Each is a property of the web app, and each goes to the app’s own address only:

Property
cookie.<name> a cookie, such as a session cookie: cookie.session
header.<name> a header on every request to the app, such as a token: header.Authorization with Bearer ${env:SHOP_TOKEN}
local storage.<key> an item of the page’s local storage, where single-page apps often keep their tokens
session storage.<key> an item of each tab’s session storage, where some sign-in libraries keep theirs
username, password HTTP authentication, for apps (or test environments) behind a login prompt
Scenario: A returning shop sees its parcels
Given the shop web app with the following properties:
| url | http://localhost:8400/portal |
| local storage.token | ${env:SHOP_TOKEN} |
When the "/parcels" page of the shop web app is opened
Then the page shows "Signed in as Fjord Outdoor"

Where the token comes from is up to your test environment: a long-lived test token in the environment, one your identity provider issues to a test client, or a session your service creates for a seeded shop. Scenarios still share nothing: every scenario’s browser starts with its own copy.

A step of your own can wrap either way in your own words, like Given the shop is signed in as fjord-outdoor (Write custom steps).

Keep passwords and tokens in the environment, as ${env:…}. Whatever a step takes from the environment is a secret: failure messages, the failure report and saved traces show it as ********, down to the form data the browser sends and the headers of its requests. Screenshots and videos show the page as it is: a password field shows only dots, so type secrets into password fields.

For your own tests, run Dex, a small OpenID Connect provider, in the Compose file with shops of your choosing. Your app signs in with it as it does with your real provider:

compose.yaml
services:
dex:
image: ghcr.io/dexidp/dex:v2.45.1
command: ['dex', 'serve', '/etc/dex/config.yaml']
volumes: ['./dex.yaml:/etc/dex/config.yaml:ro']
ports: ['5556:5556']
parcels:
build: ../app
ports: ['8400:8400']
extra_hosts: ['dex.localhost:host-gateway'] # Dex, where the browsers find it
dex.yaml
issuer: http://dex.localhost:5556/dex
storage:
type: memory
web:
http: 0.0.0.0:5556
staticClients:
- id: parcels-portal
name: Parcels portal
secret: portal-client-secret
redirectURIs: ['http://localhost:8400/portal/callback']
enablePasswordDB: true
staticPasswords:
- email: orders@fjord-outdoor.example
# htpasswd -nbBC 10 "" "$FJORD_OUTDOOR_PASSWORD" | tr -d ':\n'
hash: '<the bcrypt hash of the password>'
username: Fjord Outdoor
userID: fjord-outdoor
connectors:
- type: mockCallback
id: mock
name: Example
  • The issuer is one address for both the browser and your app. The browsers, on your machine, take any *.localhost name for your machine, where Dex’s port is published; extra_hosts sends your app’s container there too. Give your app that issuer, the client’s id and secret, and its callback. An app that runs on your machine needs no extra_hosts.
  • Dex asks which way to sign in (Log in with Email or Log in with Example), then for the email address and password, then to grant access. oauth2: {skipApprovalScreen: true} leaves out the last page.
  • Log in with Example signs in a fixed test user, Kilgore Trout, without a password: enough for scenarios about what a signed-in shop sees rather than about signing in.
  • Dex keeps no session of its own: after signing out of your app, signing in asks for the password again.