Variables & secrets
For the complete documentation index, see llms.txtEnvironments, tokens and credentials live in variables — not hardcoded in scenarios.
Two files
unotest/.env— ordinary variables.unotest/.secrets— secrets, git-ignored.
Reference them by bare UPPER_SNAKE identifiers in scenarios:
goto(APP_BASE_URL + "/login");fill(getByLabel("Email"), TEST_USER_EMAIL);fill(getByLabel("Password"), TEST_PASSWORD);Secrets are masked
Values registered as secrets are redacted in logs and failure artifacts (shown
as ‹secret:NAME›), so they never leak into a trace or a screenshot bundle.
That covers everything a run says out loud as well as everything it writes:
the run journal, what the runner prints to its terminal, the same lines as
the viewer’s System pane streams them, and the copy kept beside the run in
stdout.log / stderr.log.
Masking is by value, wherever the value appears — with one explicit
exception: the run’s own identifiers in runtime.json (the run id, the
scenario path, the test function, the file and function names of the call
stack) are never rewritten, so LOGIN=admin does not turn
guard-admin.js into guard-‹secret:LOGIN›.js.
What masking cannot cover
Masking knows the values you gave it. A secret the application shows for the first time — a token it has just minted, a one-time code, a recovery key — is not one of them, and an assertion that reads the block containing it puts it in the journal, the screenshot and the failure bundle, because an assertion’s job is to report what it saw:
// Leaks the token into every artifact of a failed run.assertText(page.locator('.token-panel'), 'copy it now');Assert on the label, the heading or the presence of the element instead, and never on the text of the block that displays the value.
The judge’s connection (UNOTEST_JUDGE_MODE, _URL, _TOKEN, …) belongs
in the same files — the token in .secrets; see
LLM judge.
Across environments
Because the test references variables, the same scenario runs against dev, staging or prod. Named environments are overlay files layered over the base pair — an overlay holds only the keys that differ (later file wins; ambient shell env beats every file):
# unotest/.env — base: shared values + local defaultsAPP_BASE_URL=http://localhost:3000TEST_USER_EMAIL=demo@example.com
# unotest/.env.staging — overlay: only what differs on stagingAPP_BASE_URL=https://staging.app.devSecrets overlay the same way: unotest/.secrets.staging over
unotest/.secrets. An environment exists as soon as either overlay does —
.env.<name> or .secrets.<name>; the names example, sample and
template are never offered as environments. Each environment keeps its own
run history under unotest/.runs.<name>/; a target adds its own axis in
front (unotest/.runs-mobile.staging/).
unotest/.env.example is the template init writes once. It is your file:
a later init never rewrites it, but reports the keys the current template
declares that your file does not mention (skipped — the current template also declares: …) — add them by hand, or re-run with --force to take the
template.
Select the environment per run:
npx @unotest/web e2e auth/login --env staging# or: UNOTEST_ENV=staging npx @unotest/web e2e auth/loginNo --env / UNOTEST_ENV means base files only.
Run history follows the same axis: a run started with --env staging
writes its artifacts under unotest/.runs.staging/, while base runs stay
in unotest/.runs/. Each environment keeps its own history, so results
from different environments never mix (keep the wildcard
unotest/.runs*/ line in .gitignore — that’s what init writes).
The viewer’s Variables panel lets you edit values,
reveal/hide secrets, and toggle boolean flags without leaving the window.
It also has an env switcher at the top of the panel — the same
.env.<name>-overlay switcher the status bar shows as env overlay: pick
base or any overlay and the panel shows
the merged layers exactly as a run in that environment resolves them —
overlay-owned values carry an env badge, edits go to the file that
defines the key, and new variables are always created in the base file.
Switching is server-side: the Overview and Runs views re-scope to that
environment’s history, and runs started from the viewer spawn with its
UNOTEST_ENV. Template files like .env.example are never offered as
environments. On a box, the project and environment the viewer serves are a
different thing: they are named and switched in the tree header, not here.
On a box the panel also lists what the box injects into every run of the
environment — its target, the operator’s variables and the names of its
secrets — each with a box badge. Those rows are read-only here: a write
that names one is refused (HTTP 409, <NAME> is set by the box for this environment — change it on the box), because no file defines it and the
box’s value is applied after the files.