Secrets and sign-in

Type passwords and codes without the test holding them, and sign in once for many tests.

A test types a password the way a person does, into the field on the page. But the test file never holds the password.

Name a secret Link to Name a secret

The config says where each secret comes from. The test names it with secret().

retest.config.ts
secrets: { password: env('TASK_APP_PASSWORD') },
tests/sign-in.retest.ts
await page.getByLabel('Password').fill(secret('password'))
  • The test's process sends the secret's name. Retest's own process reads the value and types it.
  • The variable an env source reads is removed from the test's environment.
  • An env source is read once, when the run starts. A value that is missing, empty or shorter than four characters stops the run with exit code 2.
  • However it is printed, the secret reads {{password}}.

Secrets from a function Link to Secrets from a function

A source can be a function, for values that change, such as a one-time code. Retest calls it each time a fill uses the secret.

retest.config.ts
secrets: {  password: () => vault.read('password'),  code: () => inbox.latestCode(),},
  • The function is called with { signal }. Retest aborts the signal once it stops waiting, so pass it on, as to fetch.
  • If the function throws or gives no text, that fill fails setup_failed and names the secret.
  • The function is called as the fill begins. Wait for the page that asks for the code first, as in await expect(page.getByLabel('Code')).toBeVisible().

Where a secret may be typed Link to Where a secret may be typed

A secret is bound to the origins of the base URLs of the test's apps. secretOrigins adds more.

retest.config.ts
secretOrigins: { password: ['https://auth.example.com'] },

On a page of any other origin, the fill fails not_actionable at once and nothing is typed. Retest checks the origin before it reads the value, and again just before it types.

What is hidden Link to What is hidden

  • Retest writes {{name}} in place of every value. That covers events, results, logs, app server output, browser logs, the terminal and every address it records.
  • Page text the test reads is hidden before it reaches the test, so a check against it compares {{password}}.
  • A command whose locator holds a whole secret value is refused, and never sent to the page.

Screenshots are not redacted. A secret the page shows appears in its screenshot. Reports and evidence explains what Retest holds back.

Sign in once Link to Sign in once

test.setup(state, fn) signs in the way a person would. When it passes, Retest saves the browser's cookies and the localStorage of the origins it visited, under the state's name.

tests/sign-in.retest.ts
import { expect, secret, test } from '@rehearsal-labs/retest'test.setup('signed-in', async ({ page }) => {  await page.goto('/login')  await page.getByLabel('User name').fill('alice')  await page.getByLabel('Password').fill(secret('password'))  await page.getByRole('button', { name: 'Sign in' }).click()  await expect(page.getByTestId('account')).toHaveText('Signed in as alice')})test('shows the account', { state: 'signed-in' }, async ({ page }) => {  await page.goto('/account')  await expect(page.getByText('Signed in as alice')).toBeVisible()})
  • A setup runs before the first test that needs its state, once for each target those tests use.
  • A test with state starts with the state restored. A test without it starts signed out.
  • A setup that fails makes every test that needs it not_run, with the setup's failure as the reason.
  • When you run some files only, Retest finds the setups they need in the other files and runs those too.
  • The saved state holds session cookies, so Retest deletes it when the run ends. sessionStorage is not saved.

Several people Link to Several people

Each app a test names gets its own browser context, with its own cookies and storage. Two apps on one site are two people, each signed in with their own setup.

TypeScript
test.setup('owner-account', { apps: ['owner'] }, async ({ owner }) => { /* sign in as the owner */ })test.setup('member-account', { apps: ['member'] }, async ({ member }) => { /* sign in as the member */ })test('the member reads the record the owner made', {  apps: ['owner', 'member'],  state: { owner: 'owner-account', member: 'member-account' },}, async ({ owner, member }) => {  const reference = 'record-' + randomUUID()  // the owner makes the record under reference; the member opens it by reference})

Find a shared record by a value the test made, such as reference above. A title another run also used could match the wrong record.