e2e-testora
e2e-testora
end-to-end, verified

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.

The big idea

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.

Tests as data, not scattered files

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.

Real browsers, real endpoints

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.

One control room

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.

The mental model

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.

Functional RequirementWhat the product must do

A capability of the app and the environment it lives in.

Authentication · Registration · Listing-site scraping · Video generation
SuiteA coherent slice of that capability

Groups fixtures that belong together under one requirement.

“Login Flow” · “Register Flow” · “Supported listing sites”
FixtureOne target, one setup

A page or endpoint to exercise, with a baseUrl and shared input.

“Login using email/password” → /en/login
Test CaseA single behaviour to prove

The 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.

Anatomy

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.

the catalog, as data
// 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
};
Writing cases

Three flavours of test case

A case’s scriptType decides how much testora does for you versus how much control you take.

single
Fill fields, submit, assert.

The 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.

multi
The same case, many inputs.

One 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.

scripted
Raw TestCafe. Full control.

When 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.

a scripted body — you get `t` (TestCafe) and `run` (the current params)
// 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);
Environments

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.

Frontendhttp://localhost:3233
APIhttp://localhost:3234/api/v1
SecretWEBAPP_ADMIN_PASSWORD (env)
API overrideWEBAPP_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:3233

Secrets 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.

End to end

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.

  1. 1
    Define 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.

    Open Requirements
  2. 2
    Add a suite

    Group related fixtures. “Login Flow” lives under Authentication and will hold every login scenario.

    Open Suites
  3. 3
    Create a fixture

    Point at the page: “Login using email/password” with baseUrl /en/login. testora resolves it to the dev frontend automatically.

    Open Fixtures
  4. 4
    Write 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.

    Open Test Cases
  5. 5
    Run & 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.

    Open Run Tests
  6. 6
    Read the results

    Every case (and every run within it) lands in Results with status, duration and the exact error text the console showed.

    Open Results
Running

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.

Fixture

Just this page/endpoint and its cases. The tightest loop while iterating.

Suite

Every fixture in the suite, in order — e.g. all of “Supported listing sites”.

Requirement

Every fixture across every suite under the capability. The full regression net.

Live console

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.

Cancel anytime

A run executes server-side. Hit Cancel and the abort signal propagates straight into TestCafe; between fixtures, remaining work is skipped cleanly.

Multi-run cases

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.

Survives reloads

The active run id is remembered, so a full page refresh re-attaches to the same run and replays its log and final result.

Results

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.

CaseRunStatusWhy it matters
Valid login succeedspassedA real session token landed in localStorage.
Login fails with invalid passwordrun 1 / 2passedRejected, no session, stayed on /login.
Login is rate-limited after failurespassedAPI returned HTTP 429 after a rapid burst.
User can generate a video from a URLfailedWizard 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.

Field notes

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.

Beat the hydration race

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.

API net + browser smoke

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.)

Keep success cases re-runnable

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.

Live within rate limits

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.

Make it yours

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.

1
Pick a capability and create a Functional Requirement with its environment root (your dev URL).
2
Add a Suite for each coherent group of scenarios.
3
Create Fixtures for the pages or endpoints, using relative baseUrls so you can flip environments later.
4
Add stable selectors in your app (data-testid) — they make scripted cases dramatically less brittle.
5
Write cases: start declarative (single/multi), drop to scripted when the flow gets real.
6
Assert observable state (storage, network status, DOM) over brittle redirects or localized strings.
7
Run the fixture, then promote to suite/requirement runs once it’s green.
Reference

One-screen cheat sheet

vocabulary
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)
scriptType
single    declarative: fill → submit → assert
multi     single, repeated over runs[]
scripted  raw TestCafe body: t + run
baseUrl resolution
"/path"          → <FR root> + /path
"https://x/path" → absolute override
""               → <FR root> itself
assert what you can see
localStorage token  → logged in
HTTP 429            → rate limited
stays on /login     → rejected
res.status / body   → API behaviour
That’s the whole loop.

Model the behaviour, run it against the real app, read the verdict. Start with the seeded example requirements, or wire up your own.