-
Notifications
You must be signed in to change notification settings - Fork 0
Testing
Three test engines live in this repo and only one of them gates a pull request. Vitest runs on every PR; Playwright runs nightly against a real stack and is split into a project that can turn the build red and one that cannot; Cypress is present, largely unwired, and honest about it. The hub's Developer Setup has the cross-repo view of which suite needs what. This page is what you need to write and debug tests here.
npm test is ng test, which runs the @angular/build:unit-test builder over src/**/*.spec.ts in a jsdom environment. Specs are co-located with the code they cover. The conventions are unremarkable — services and pipes get unit tests, components get them when they carry real logic, trivial presentational components do not, and HTTP is mocked with provideHttpClientTesting.
The one thing that will cost you an hour is that there are two Vitest configs and the runner only reads one of them.
| File | Read by | Not read by |
|---|---|---|
vitest-base.config.ts |
ng test (the Angular unit-test builder) |
— |
vitest.config.ts |
a direct vitest invocation |
ng test |
src/test.setup.ts |
vitest.config.ts's setupFiles
|
ng test |
The project standard is ng test, so vitest-base.config.ts is the canonical config. Three consequences:
-
setupFilesinvitest-base.config.tswould not take effect either. The Angular builder ignores user setup files unless they are listed underarchitect.test.options.setupFilesinangular.json, and it injects its owninit-testbed.js(which callsinitTestEnvironment) andvitest-mock-patch.jsbefore any spec runs. -
Global module mocks must go through
resolve.alias, notvi.mock.vi.mockin a setup file collides with the injected mock patch, which breaks Vitest's stack-trace-based importer detection. The working pattern is invitest-base.config.ts:@microsoft/signalris aliased tosrc/testing/signalr.mock.tsso nothing in a unit test attempts a real negotiate. Per-specvi.mockstill works and overrides the alias for that file. -
vitest.config.tscarries a Vite plugin that inlinestemplateUrland blanksstyleUrlat transform time, because jsdom cannot fetch local file URLs for Angular's JIT compiler. Underng testthe builder handles component resources itself, so that plugin is part of the fallback path only.
Worth flagging rather than trusting: src/test.setup.ts contains a window.matchMedia stub and a getTestBed().initTestEnvironment(..., { teardown: { destroyAfterEach: false } }) workaround for an NG0205: Injector has already been destroyed teardown race. Both files' own comments say the builder ignores that setup file, so neither the stub nor the teardown flag applies to a standard ng test run. If you hit that NG0205 race, the fix is not "it's already handled".
src/testing/ holds the doubles that are meant to be shared: the SignalR mock above, and signal-input-harness.ts for driving signal-based component inputs.
e2e/playwright.config.ts has no webServer block. Nothing is started for you. Bring up the UI at http://localhost:4200 and the API at http://localhost:5000 yourself — the containerised dev loop, or compose-e2e.yml from forge-deploy, which is what CI uses — then npm run e2e.
Sign-in does not go through the login screen. e2e/helpers/auth.helper.ts posts to the API, then writes the token and user into localStorage under the keys AuthService reads on init. It is fast and it is also the reason a change to how the SPA persists a session breaks every spec at once. Seed credentials come from SEED_USER_PASSWORD; base URLs are overridable through environment variables. Chromium only, one worker, no retries — set PLAYWRIGHT_CHANNEL=chrome on a host where Playwright ships no bundled browser build.
e2e/lib/ is the part worth reading before writing a spec: small libraries for data tables, dialogs, forms, navigation, detail panels, snackbars and entity links, built around a data-testid convention that the application templates carry throughout. Reach for page.locator('[data-testid=…]') through those helpers rather than CSS or text selectors.
| Project | Contains | Effect on the build |
|---|---|---|
functional |
everything else — behaviour assertions | The gate. Red here is red. |
docgen |
generate-ui-*, *-audit, *screenshot*, *-verify, and a handful of named specs |
Non-gating. Runs as its own continue-on-error job. |
The split is not a quality judgement. The docgen specs exist to produce artifacts — documentation, screenshot sets, visual audits — and full-page captures of complex screens are memory-hungry enough to flake on a small runner while passing on a well-resourced box. Letting them redden the nightly meant the nightly was permanently red and therefore ignored.
.github/workflows/nightly.yml shards the functional project across parallel runners, each with its own freshly seeded stack, and merges the per-shard blob reports into one HTML report at the end. Playwright shards at file granularity, so a describe.serial block stays intact on one shard and within-file fixtures are safe. The hazard is the one to remember when writing specs: a spec that depends on data created by a different spec file will fail unpredictably under sharding. Every spec must create what it needs.
One inconsistency in that setup: playwright.config.ts sets a globalTimeout and its comment explains the value as sitting below the nightly job's ceiling so the suite stops itself gracefully with a readable report instead of being killed mid-test. The sharded job's timeout-minutes is now lower than that globalTimeout, so for the functional shards the graceful inner cap can no longer fire first. The comment describes an arrangement that the workflow has since moved away from.
e2e/tests/smoke/contract-drift.spec.ts runs no browser. It parses forge-api's controller sources from a sibling ../forge-api checkout, extracting every [Route] and method-level HTTP attribute (correctly pairing each method with the nearest preceding class route, because one .cs file can hold several controllers), then extracts every URL the SPA's services call and compares the two sets.
- A frontend URL with no matching backend route fails. That is a call that will 404 in production.
- A backend route with no frontend caller warns. That is usually fine.
This is the answer to the fact that there is no generated client — see The Surfaces § The seam with forge-api, and forge-api for the routing conventions the parser assumes.
And it carries the lesson worth taking from this whole page. Both this spec and api-smoke.spec.ts read that sibling path, actions/checkout will not place a repo outside the workspace, and so for a long stretch after the nightly workflow was written both specs threw "Controllers directory not found" on every run — the two most valuable gates in the suite were dead in CI while appearing to run. The fix was to check forge-api out inside the workspace and symlink it to the expected sibling path. A check that silently degrades when an input is missing is worse than one that fails: lint:i18n's cross-repo scan has the same conditional shape and the same exposure (i18n).
Run the three smoke specs together with npm run test:smoke (test:api-smoke, test:contract-drift, test:critical-flows).
Not gates — tools, each with its own config:
| Command | What it is |
|---|---|
npm run scenario |
An interactive, headed scenario tree (01-foundation then a branch), run sequentially with long timeouts because the steps pause for a human. Also the way to populate a stack with realistic data. |
npm run simulate |
The week-simulation framework under e2e/simulation/. |
npm run screenshots |
Screenshot capture with its own config. |
npm run stress |
A tsx orchestrator under e2e/stress/, plus screenshot auditing and review tooling. |
npm run e2e:gen-docs / gen-landmarks
|
The doc-generation specs, invoked directly by grep. |
npm run training:verify |
Screenshots each training module's page and has a vision model compare it against the module's written content, producing a discrepancy report (--fix rewrites the seed data and re-verifies). |
cypress/e2e/ holds a per-feature spec suite, and cypress/support/commands.ts registers a login command — which takes an email, not a role, whatever CLAUDE.md says. Nothing in CI runs any of it. The only wired script is npm run test:a11y, which runs the axe accessibility spec; npm run cy:run runs the whole suite if you want it. Treat the Cypress suite as a historical artifact that still executes rather than as coverage you can rely on, and add new end-to-end coverage in Playwright.
| Suite | When | Blocks a merge |
|---|---|---|
npm run lint, lint:i18n, lint:standards, build, test
|
Every PR and push | Yes |
| CodeQL | Every PR, push to main, weekly | Findings surface in the Security tab |
Playwright functional
|
Nightly, on demand, on release/*
|
No — but a red nightly is a real failure |
Playwright docgen
|
Nightly, own job | No, by design |
| Cypress | Manually | No |
forge-ui · Apache 2.0 · built by Armory Works — this wiki covers the frontend; product-level truth lives on the Forge wiki.
Peer repos