Testing
Unit Tests
Unit tests run on Vitest and live beside the source they cover, in src/**/__tests__/*.test.ts. The Nuxt package is the exception, keeping its tests in packages/nuxt/tests/unit.
pnpm test # every package
pnpm --filter @thunderid/react test # one package
Packages that render components (browser, react, react-router, tanstack-router) also expose test:browser, which runs the same specs in a real browser environment.
End-to-end Testing
The end-to-end suite lives in tests/e2e and runs on Playwright. It drives the sample apps in a real browser against a real ThunderID server, covering what unit tests cannot: the redirect handoff to Gate, cookie and session behaviour across a reload, and each framework integration wiring the core SDK up correctly.
Six sample apps are exercised, each on its own port:
| Sample | Port |
|---|---|
| browser | 5173 |
| react | 5174 |
| vue | 5175 |
| express | 3000 |
| nextjs | 3001 |
| nuxt | 3002 |
The node quickstart has no UI and is spawned directly by its own spec.
The quickstarts live in the samples repository and depend on the published packages. tests/e2e/scripts/prepare-samples.ts copies them into tests/e2e/.samples and installs each against tarballs of your local package builds, so the suite tests your changes. It clones main by default; set THUNDERID_SAMPLES_REF to use another branch or tag, or THUNDERID_SAMPLES_DIR to use a local checkout of the samples repository.
Configure and Run
You need curl, jq, unzip, pnpm, lsof, and git on your PATH. One script does everything:
cd tests/e2e
./run-e2e.sh
Arguments pass through to Playwright, so run a single spec with:
./run-e2e.sh tests/react-quickstart/sign-in-out.spec.ts
The script owns the whole lifecycle. It downloads and starts a server, mints an admin token, imports the test applications, builds the packages, prepares the quickstarts, writes each sample's .env, starts all six apps, runs the tests, then stops the apps.
run-e2e.sh aborts if anything already answers on https://localhost:8090. Stop any server you started by hand before running it.
Set THUNDERID_VERSION to pin a specific server release instead of taking the latest:
THUNDERID_VERSION=1.0.1 ./run-e2e.sh
Configuration comes from defaults.env, copied to .env on the first run. Values in .env win, and defaults.env fills the gaps:
| Variable | Default |
|---|---|
SERVER_URL | https://localhost:8090 |
ADMIN_USERNAME / ADMIN_PASSWORD | admin / admin |
TEST_USER_USERNAME | e2e-test-user |
TEST_USER_PASSWORD | E2ePassword@123 |
BROWSER_APP_URL | http://localhost:5173 |
REACT_APP_URL | http://localhost:5174 |
VUE_APP_URL | http://localhost:5175 |
EXPRESS_APP_URL | http://localhost:3000 |
NEXTJS_APP_URL | http://localhost:3001 |
NUXT_APP_URL | http://localhost:3002 |
global-setup.ts fails the run and lists anything missing, so a partial .env surfaces immediately rather than midway through a spec.
Once a run has finished, pnpm report opens the HTML report and pnpm ui opens Playwright's interactive mode against an already-running server.
Writing Tests
Applications are declared, not created by hand. thunderid-config/sample-apps.yaml holds one entry per sample with a fixed application ID, imported through /import with upsert enabled. Add a sample by adding an entry there rather than clicking through the Console, so every contributor and CI provision the same thing.
The test user is different: global-setup.ts creates it through the API before the run and global-teardown.ts deletes it afterward. Teardown fails loudly if the delete does not succeed, so a leaked user surfaces as a failure rather than silently accumulating.
Tests use the page object model. Shared page classes live in pages/, and Playwright fixtures in fixtures/sample-apps/ expose one page object per sample. A spec asks for the fixture it needs and never constructs a page itself:
test('TC001: signs in with valid credentials', async ({reactQuickstartPage}) => {
await reactQuickstartPage.goto(appUrl);
await reactQuickstartPage.verifyHomePageLoaded();
await reactQuickstartPage.clickSignInButton();
await reactQuickstartPage.verifyLoginPageLoaded();
await reactQuickstartPage.login(username, password);
await reactQuickstartPage.verifyLoggedIn();
});
Specs are grouped one directory per sample under tests/, and each test name carries a TCnnn: identifier.
Put assertions about the login screen in the shared gate-login.page.ts, since every sample redirects to the same Gate DOM. The React, Vue, Next.js, and Nuxt samples share thunderid-web-sample.page.ts because they render the same markup; only the browser and Express samples need their own page objects.
Things to Know
- The server uses a self-signed certificate. Playwright sets
ignoreHTTPSErrors, and direct API calls go through an undici agent configured to accept it. A new HTTP call made outside those helpers fails certificate validation. - There is no Playwright
webServerblock. One backend and six sample apps are shared across the run, so the script starts them rather than Playwright. - Do not run a sample's own
prepare-dev.cjs. It rewritesworkspace:*dependencies tolatest, which takes the sample off your local build. - Cleanup frees ports 8090, 5173-5175, and 3000-3002, and removes the downloaded server. A run interrupted with Ctrl+C may leave a port bound; rerun the script and it clears them.
Continuous Integration
pr-builder.yml runs on every pull request: an audit, dependency review, a build-lint-test job, and the end-to-end suite through the run-e2e-suite composite action. e2e-nightly.yml runs the same suite on a schedule at 18:30 UTC and uploads its report as an artifact.