Describe how your app should behave. Run it for real. Watch it pass.
testora is a database-backed control room for end-to-end tests. You model what your product must do as structured data — requirements, suites, fixtures, cases — and testora turns it into live TestCafe browser runs, streams the console as it goes, and stores every result. It works for any web app; throughout this guide we drive the real the app under test app and its local dev environment as the worked example.
A catalog of behaviour you can actually run
testora sits between “a wiki of test cases” and “a folder of automation scripts” — and gives you the best of both.
Every requirement, suite, fixture and case lives in PostgreSQL. The catalog is queryable, diffable and shareable — not buried in a folder of spec files nobody reads.
Under the hood it's TestCafe. Cases either drive a real Chrome session (clicks, typing, assertions) or hit your API directly with t.request — whichever proves the behaviour fastest.
Pick what to run, watch the live console stream, cancel mid-flight, and read color-coded results — all from the same place, on any machine that can reach the app under test.
Four nested layers, top to bottom
Everything in testora hangs off this hierarchy. Internalise it once and the whole app makes sense. A run can target any level — the layers below it all execute.
A capability of the app and the environment it lives in.
Authentication · Registration · Listing-site scraping · Video generationGroups fixtures that belong together under one requirement.
“Login Flow” · “Register Flow” · “Supported listing sites”A page or endpoint to exercise, with a baseUrl and shared input.
“Login using email/password” → /en/loginThe assertion. Can repeat across many parameterised runs.
“Login fails with invalid password” (×2 runs)A Run executes a fixture and produces a Result per case (per run). Point it at a single fixture, a whole suite, or an entire requirement and testora walks every fixture beneath it in turn.
What a slice of the catalog looks like
Here is an example login capability expressed as testora data. Notice how the fixture’s relative baseUrl is resolved against the requirement’s environment root.
// Functional Requirement — the capability + its environment
const authenticationFR = {
id: "auth",
title: "Authentication",
baseUrl: "http://localhost:3233", // app dev frontend
};
// Suite — a coherent group under that requirement
const loginFlowSuite = {
suiteId: "login-flow", frId: "auth", title: "Login Flow",
};
// Fixture — one target page + shared setup
const loginWithEmailFixture = {
fixtureId: "login-with-email",
suiteId: "login-flow",
title: "Login using email/password",
baseUrl: "/en/login", // inherits → http://localhost:3233/en/login
commonInput: {},
};
// Test Case — the behaviour, repeated across parameterised runs
const invalidPassword = {
caseId: "invalid-password",
fixtureId: "login-with-email",
title: "Login fails with invalid password",
scriptType: "scripted",
runs: [
{ email: ACCOUNT_EMAIL, password: "wrong", expectAuth: false },
{ email: ACCOUNT_EMAIL, password: "incorrect", expectAuth: false },
],
script: LOGIN_ATTEMPT_SCRIPT, // a TestCafe body, see below
};Three flavours of test case
A case’s scriptType decides how much testora does for you versus how much control you take.
singleThe declarative path. Describe inputs and an expected outcome; testora’s generator builds the TestCafe spec for you. Best for plain forms with one straightforward result.
multiOne assertion, a runs[] array of parameter sets. testora executes it once per entry and reports each as “(run 1…N)”. Perfect for validation matrices and data-driven coverage.
scriptedWhen the flow doesn’t fit a template, drop down to a real TestCafe body with the `t` controller and the current `run`. This is how every the app under test case is written — multi-step flows, API calls, custom waits.
// LOGIN_ATTEMPT_SCRIPT — runs once per entry in the case's runs[]
const email = Selector('[data-testid="login-email"]');
const submit = Selector('[data-testid="login-submit"]');
// Hydration gate: the submit button is disabled until React hydrates.
await t.expect(submit.hasAttribute('disabled')).notOk({ timeout: 60000 });
await t.typeText(email, run.email, { replace: true });
await t.typeText(Selector('[data-testid="login-password"]'), run.password, { replace: true });
await t.click(submit);
// Assert observable behaviour, not a brittle redirect or toast string:
const authed = await t.eval(() => !!localStorage.getItem('auth_token'));
await t.expect(authed).eql(run.expectAuth, run.scenario);One switch moves every test between local and live
The requirement owns the environment root. Fixtures point at it with a relative path — so flipping a requirement from your dev box to production re-points all of its fixtures at once.
Dev environment
What the seeded examples target out of the box.
http://localhost:3233http://localhost:3234/api/v1WEBAPP_ADMIN_PASSWORD (env)WEBAPP_API_URL (env)How baseUrl resolves
Fixture baseUrl, three behaviours.
"/en/login"relative → joined onto the requirement root→ http://localhost:3233/en/login"https://app.example.com/en/login"absolute → full override→ https://app.example.com/en/login""empty → the requirement root itself→ http://localhost:3233Secrets never live in the catalog. Scripts read them from the environment at run time (e.g. process.env.WEBAPP_ADMIN_PASSWORD), and runs pass a sentinel like "__VALID__" to request “the real password” without ever storing it.
Build a capability’s coverage in six moves
The same path works for any app. Here it is for the app under test’s login — from empty catalog to green checks.
- 1Open RequirementsDefine the requirement
Create an FR for the capability and set its environment root. For the app under test login that’s Authentication @ http://localhost:3233.
- 2Open SuitesAdd a suite
Group related fixtures. “Login Flow” lives under Authentication and will hold every login scenario.
- 3Open FixturesCreate a fixture
Point at the page: “Login using email/password” with baseUrl /en/login. testora resolves it to the dev frontend automatically.
- 4Open Test CasesWrite the cases
One per behaviour: valid login, wrong password, bad email format, missing fields, brute-force, rate-limit. Use runs[] to cover several inputs in a single case.
- 5Open Run TestsRun & watch
Pick the fixture (or the whole Login Flow suite) on Run Tests and watch TestCafe stream a real Chrome session in the live console.
- 6Open ResultsRead the results
Every case (and every run within it) lands in Results with status, duration and the exact error text the console showed.
Choose your blast radius
On the Run Tests page, pick a scope and a target. Higher scopes simply execute all the fixtures nested beneath them, one after another, into one streamed run.
Just this page/endpoint and its cases. The tightest loop while iterating.
Every fixture in the suite, in order — e.g. all of “Supported listing sites”.
Every fixture across every suite under the capability. The full regression net.
Color-coded TestCafe output streams as it happens — green for passes, red for failures, yellow for warnings. It keeps streaming even if you navigate to another page.
A run executes server-side. Hit Cancel and the abort signal propagates straight into TestCafe; between fixtures, remaining work is skipped cleanly.
A case with N entries in runs[] shows up as “…(run 1)…(run N)”, each independently pass/fail, so one case can prove a whole matrix.
The active run id is remembered, so a full page refresh re-attaches to the same run and replays its log and final result.
Every run, attributable all the way up
Results are joined back through case → fixture → suite → requirement, so you can filter at any level and the stored error text matches exactly what the live console showed.
| Case | Run | Status | Why it matters |
|---|---|---|---|
| Valid login succeeds | — | passed | A real session token landed in localStorage. |
| Login fails with invalid password | run 1 / 2 | passed | Rejected, no session, stayed on /login. |
| Login is rate-limited after failures | — | passed | API returned HTTP 429 after a rapid burst. |
| User can generate a video from a URL | — | failed | Wizard never reached awaiting_approval in time. |
Because failures store the formatted TestCafe error, the Results page is enough to triage most breakages without re-running anything.
Patterns that survive a real app
These are the exact tactics the example suites use to stay green against a live, race-prone dev server. Steal them for your own app.
Trap — SSR renders the input before React wires its onChange — a single typeText gets wiped by the first re-render.
Fix — Gate on a hydration signal (the submit/Start button enabling), then type and re-type until the value sticks.
Trap — Driving the full browser wizard for every variation is slow and flaky.
Fix — Cover the matrix fast through t.request against the API, then keep one thin browser case to prove the real flow end-to-end. (See the app under test’s scraper-routing vs scraper-live.)
Trap — A passing sign-up creates a user — run it twice and the unique-email constraint trips.
Fix — Generate a fresh plus-addressed email per run (asafarim+e2e<unique>@gmail.com) so the suite is idempotent.
Trap — /auth/login is throttled to 10/60s; logging in per run trips the throttler.
Fix — Authenticate once per fixture, cache the JWT on globalThis with a TTL under expiry, and retry only on an unexpected 429.
Adapting testora to any app
Nothing here is app-specific. Swap the URLs and selectors and the same workflow models a checkout, a dashboard, an onboarding wizard — anything with a browser or an API.
One-screen cheat sheet
Requirement what + where (env root)
Suite a coherent group of fixtures
Fixture one page/endpoint + setup
Case one behaviour to prove
run one parameterised execution
Result outcome of a case (per run)single declarative: fill → submit → assert
multi single, repeated over runs[]
scripted raw TestCafe body: t + run"/path" → <FR root> + /path
"https://x/path" → absolute override
"" → <FR root> itselflocalStorage token → logged in
HTTP 429 → rate limited
stays on /login → rejected
res.status / body → API behaviourModel the behaviour, run it against the real app, read the verdict. Start with the seeded example requirements, or wire up your own.