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().
secrets: { password: env('TASK_APP_PASSWORD') },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
envsource reads is removed from the test's environment. - An
envsource 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.
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 tofetch. - If the function throws or gives no text, that fill fails
setup_failedand 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.
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.
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
statestarts 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.
sessionStorageis 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.
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.