Email codes and links

Connect a test mailbox and use its codes and sign-in links without reading their secret values.

If sign-in sends a code or link by email, connect a test mailbox to Retest. Your test can enter the code or open the link without receiving its secret value.

Connect a mailbox Link to Connect a mailbox

Supply a function that reads your mailbox and returns messages newest first. This example reads a JSON outbox endpoint; replace the request with one for your test mail service.

retest.config.ts
import type { InboxReader } from '@rehearsal-labs/retest'import { chrome, defineConfig } from '@rehearsal-labs/retest'const baseUrl = 'http://127.0.0.1:4173'const readMailbox: InboxReader = async ({ signal }) => {  const response = await fetch(new URL('/outbox', baseUrl), { signal })  if (!response.ok) throw new Error('Could not read the test mailbox')  return response.json()}const config = defineConfig({  apps: { web: chrome({ baseUrl }) },  inboxes: { owner: readMailbox },  locks: ['inbox'],})export default configdeclare module '@rehearsal-labs/retest' {  interface Register {    config: typeof config  }}

The reader returns an array of InboxMessage objects, which Retest validates on each read. Your adapter must extract the codes and links; Retest does not parse an email's HTML to find them.

FieldWhat to return
subject, from, textStrings, with the message body as plain text.
codesAn array of one-time codes as strings.
linksAn array of { url, text } objects with each full address and its visible text.
receivedAtThe arrival time as a Date, an ISO time string or milliseconds since 1970.

Pass the abort signal to your mail request, as the example does. Retest aborts it when the read times out or the test stops, so the request can stop too.

Enter a code Link to Enter a code

In this email sign-in example, the app asks for an address and sends a code. Request it through the form, wait for the code field, then read the message with latest().

tests/email-sign-in.retest.ts
import { expect, inbox, test } from '@rehearsal-labs/retest'test('verifies an email code', { locks: ['inbox'] }, async ({ page }) => {  await page.goto('/')  await page.getByLabel('Email').fill('owner@tasks.test')  await page.getByRole('button', { name: 'Send code' }).click()  await expect(page.getByLabel('Code')).toBeVisible()  const message = await inbox('owner').latest({ subject: 'sign-in code' })  await page.getByLabel('Code').fill(message.code)  await page.getByRole('button', { name: 'Verify' }).click()  await expect(page.getByTestId('result')).toHaveText('Verified')})

latest() waits for the newest matching message received since the test started. The subject string matches a part of the subject, without case sensitivity; you can also pass a RegExp.

message.code names the first code in that message. Retest types it as a secret and records {{code}}, so check the app's verification result rather than comparing the code itself.

If the message offers a sign-in link, replace the code-entry steps with navigation using its handle. With no argument, link() chooses the first token-bearing link, or the first link if none carries a token.

TypeScript
await page.goto(message.link())await expect(page.getByRole('heading')).toHaveText('Signed in by link')

When an email has several links, use message.link(2) for its second link or message.link({ path: '/verify' }) for one at or below that path. message.links exposes each link's text, origin, redacted path and token flag, without its query or fragment.

A code may be typed, and a link may open, only on the test apps' base URL origins. Add another destination to the inbox's origins when your sign-in service uses a different site.

retest.config.ts
defineConfig({  // Keep your apps and locks here.  inboxes: {    owner: {      messages: readMailbox,      origins: ['https://login.tasks.example'],    },  },})

Keep the readMailbox function from the first example and replace its inboxes setting with this one. An unlisted destination fails with not_actionable before Retest types the code or opens the link.

Fresh mail and failures Link to Fresh mail and failures

Each read advances this test's position in the inbox, so it never returns the same message twice or returns an older message afterwards. Await each read directly; latest() is not allowed inside expect.poll().

Give every test sharing this mailbox the inbox lock from the example. It prevents those tests from running together within one run, so they cannot take each other's mail. Separate concurrent runs still need separate mailboxes or coordination outside Retest.

If no matching fresh message arrives within the assertion budget, the read fails with timeout and reports the message counts. Check the subject and receivedAt first; the mailbox clock must agree with the machine running Retest.

For a slower mail service, give latest() an explicit timeout, such as 15_000 milliseconds. The wait can last no longer than the test's remaining time, so adjust the test budget too if needed.

A reader that throws or returns an invalid message fails with setup_failed, as does a message with a code shorter than four characters. A message without a code fails with usage when you try to fill message.code.

Redaction and platforms Link to Redaction and platforms

Retest redacts codes and token-bearing links from message text, recorded addresses, logs and reports. Inbox reads appear in the report timeline, so you can see how long a test waited without seeing the secret values.

Pixels are not redacted

A code or link that your app displays can appear in screenshots or recordings. Recordings and evidence explains the capture rules and their limits.

Inbox flows are supported on Chrome and Chromium, and have been checked on Chrome. Firefox and WebKit receive the same text and address, but their inbox flows have not been checked. Electron accepts a code only on the inbox's explicit origins and has no goto() for links. Native apps cannot fill inbox codes.

Next steps Link to Next steps