Skip to content

test(dashboard): add Vitest infrastructure and first test suites#243

Open
walidozich wants to merge 4 commits into
oblien:mainfrom
walidozich:chore/dashboard-test-infrastructure
Open

test(dashboard): add Vitest infrastructure and first test suites#243
walidozich wants to merge 4 commits into
oblien:mainfrom
walidozich:chore/dashboard-test-infrastructure

Conversation

@walidozich

Copy link
Copy Markdown

Summary

Adds Vitest test infrastructure to apps/dashboard and the first suites that use it, taking the workspace from 25 tests to 116 and making React component tests possible for the first time.

Motivation

apps/dashboard is the largest workspace in the repo (474 source files) and had 4 test files. That gap was not neglect: writing a component test was impossible without config changes. Three separate blockers:

  1. No vitest.config.ts existed for the workspace.
  2. tsconfig.json sets "jsx": "preserve", which Vitest's esbuild transform honors, so any .tsx test file was a syntax error before it ran.
  3. The @/* alias did not resolve at runtime. Vitest does not read tsconfig.paths. The existing confirm-server-access.test.ts imports @/lib/api, but only via import type, which esbuild erases, so it passed by accident. The first test doing a real value import through @/ would have failed to resolve.

With the infrastructure in place, this PR also covers four pure-logic modules that had no regression protection, including the project status derivation that decides what every project card displays.

Related issue

Refs #216. Per CONTRIBUTING.md, tests do not need prior issue agreement, so this is not gated on one.

Changes

apps/dashboard

  • Add vitest.config.ts. Wires the @ alias for runtime resolution, forces jsx: "automatic" to override the app's preserve setting, and splits the suite into two Vitest projects: .test.ts on Node, .test.tsx on jsdom.
  • Add vitest.setup.ts. Registers @testing-library/jest-dom matchers and an explicit cleanup() in afterEach (needed because globals is disabled, so RTL's automatic cleanup never registers).
  • Add src/utils/deploymentPhaseDetector.test.ts (39 tests) covering the deploy phase state machine.
  • Add src/utils/project-status.test.ts (29 tests) pinning every branch of the getProjectStatus precedence chain.
  • Add src/utils/subdomain.test.ts (12 tests).
  • Add src/lib/dotenv.test.ts (11 tests).
  • Add 5 devDependencies: jsdom, @testing-library/react, @testing-library/dom, @testing-library/jest-dom, @testing-library/user-event.

CONTRIBUTING.md

  • Add a Testing section documenting the layout, the two-project split and why it exists, how to write a component test, and the expectation that a test is proven able to fail. Includes a Mermaid diagram. Pure addition, no existing lines modified.

Why the environment is split rather than flat jsdom

Running the whole suite under jsdom breaks the existing src/i18n/i18n-parity.test.ts. jsdom patches the global URL so import.meta.url resolves against the document location instead of a file: base, and scripts/check-i18n.mjs then throws TypeError: The URL must be of scheme file. Splitting by extension keeps pure-logic tests on Node, which matches their pre-existing behavior exactly and avoids paying jsdom startup for suites that never touch the DOM. Environment setup time for the existing suite went from 7.75s under flat jsdom to 0ms. environmentMatchGlobs was removed in Vitest 4, so test.projects is the supported mechanism.

Verification

Full suite, typecheck, and a scoped Prettier check:

$ bun run --cwd apps/dashboard test

 ✓  unit  src/lib/dotenv.test.ts (11 tests)
 ✓  unit  src/app/(dashboard)/projects/[id]/components/env-diff.test.ts (12 tests)
 ✓  unit  src/utils/subdomain.test.ts (12 tests)
 ✓  unit  src/components/permissions/confirm-server-access.test.ts (6 tests)
 ✓  unit  src/utils/project-status.test.ts (29 tests)
 ✓  unit  src/utils/deploymentPhaseDetector.test.ts (39 tests)
 ✓  unit  src/i18n/i18n-parity.test.ts (2 tests)
 ✓  unit  src/lib/api/urls.test.ts (5 tests)

 Test Files  8 passed (8)
      Tests  116 passed (116)

The 25 pre-existing tests pass unchanged, which was the hard requirement for adding the config.

The full workspace suite is green, confirming the new config does not disturb any other workspace:

$ bun run test

@repo/api:test:  Test Files  99 passed (99)
@repo/api:test:       Tests  996 passed | 3 skipped (999)

 Tasks:    7 successful, 7 total

Proving the tests can fail. Every suite was mutation-checked. As one example, inverting the precedence so activeDeploymentId is checked before awaitingDecision in project-status.ts:

$ sed -i 's/return "attention";/return "live";/' apps/dashboard/src/utils/project-status.ts
$ bun run --cwd apps/dashboard test

 × awaitingDecision beats activeDeploymentId, never rendering green live
 × routingUnsynced beats activeDeploymentId too

 Test Files  1 failed | 7 passed (8)
      Tests  2 failed | 114 passed (116)

Exactly the two guarding tests fail, with no collateral cascade. Source restored afterwards with git checkout --, confirmed clean via git diff.

Component testing works. Verified with a temporary .tsx test asserting all four load-bearing behaviors together (JSX transform, jsdom render, a toBeInTheDocument() matcher proving the setup file ran, a real value import through @/, and cleanup between tests). Removed before commit; the first committed component test will land with the follow-up covering lib/sseMessageProcessors.ts.

Notes for reviewers

Four pre-existing bugs surfaced in deploymentPhaseDetector.ts while writing its tests. They are not fixed here, because behavior changes need an agreed issue first. The tests characterize current behavior rather than asserting desired behavior, and I am happy to open an issue:

  1. The ready pattern /done/i is unanchored, so text like "we condone this" is classified as ready.
  2. An unrecognized ---PHASE: xyz--- marker returns progress: 0 unconditionally, discarding real progress elsewhere in the line.
  3. parseLogEntry produces malformed output like "-3:-40" when timestamp precedes startTime, because padStart does not handle a negative sign.
  4. aggregatePhaseInfo reads only logs.slice(-5) with a strict > against an initial maxProgress of 0, so a completed 100% log outside that window is invisible to the aggregate.

Prettier reports two pre-existing findings in CONTRIBUTING.md (lines 11-12 and 35) that predate this branch. I deliberately left them alone rather than bundling unrelated reformatting; my added lines are Prettier-clean.

Checklist

  • One change per PR, one bug, or one agreed feature, with nothing unrelated bundled in
  • The diff is scoped, no reformatting or lint fixes on lines I wasn't otherwise changing
  • A test fails without this change and passes with it (or I explained above why there isn't one)
  • bun run test, bun run --cwd <workspace> lint, and bun format all pass locally
  • I understand every line of this diff and can explain it in review

Copilot AI review requested due to automatic review settings July 26, 2026 17:40

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds Vitest-based testing infrastructure to apps/dashboard, enabling .tsx component tests (via a jsdom project) while keeping existing logic tests on Node, and introduces initial unit test suites for several previously-untested modules. Also extends CONTRIBUTING.md with guidance on the new dashboard test setup and workflow.

Changes:

  • Add apps/dashboard/vitest.config.ts with @/* runtime aliasing, JSX transform override, and a split Node/jsdom project setup.
  • Add apps/dashboard/vitest.setup.ts to register jest-dom matchers and ensure React Testing Library cleanup runs with globals: false.
  • Add new unit test suites for dotenv parsing, subdomain normalization, project status derivation, and deployment phase detection; update dashboard devDependencies accordingly.

Reviewed changes

Copilot reviewed 8 out of 9 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
CONTRIBUTING.md Documents test layout and the dashboard’s split Vitest project approach.
bun.lock Adds the new dashboard devDependencies (including jsdom/testing-library) and resulting lock updates.
apps/dashboard/package.json Adds Vitest DOM/component testing devDependencies.
apps/dashboard/vitest.config.ts Introduces Vitest config (alias + JSX override) and splits tests into Node vs jsdom projects by extension.
apps/dashboard/vitest.setup.ts Adds jest-dom integration and explicit RTL cleanup after each test.
apps/dashboard/src/lib/dotenv.test.ts Adds unit coverage for dotenv parsing behavior.
apps/dashboard/src/utils/deploymentPhaseDetector.test.ts Adds extensive unit coverage for phase detection and log parsing/aggregation behavior.
apps/dashboard/src/utils/project-status.test.ts Adds unit coverage for status precedence + label/meta utilities.
apps/dashboard/src/utils/subdomain.test.ts Adds unit coverage for subdomain normalization behaviors.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread CONTRIBUTING.md Outdated
Comment on lines +169 to +170
Tests are colocated with the code they cover: `foo.test.ts` sits next to `foo.ts`. Every
workspace uses [Vitest](https://vitest.dev/).
expect(normalizeSubdomain("café")).toBe("caf");
});

it("strips leading and trailing hyphens, unlike normalizeSubdomandInput", () => {
Comment on lines +141 to +144
// Exhaustiveness guard: every ProjectStatus value must have non-empty
// styling, so adding a new status without wiring up its badge/dot fails
// this test loudly instead of silently rendering blank CSS classes.
const allStatuses: ProjectStatus[] = [
@walidozich

Copy link
Copy Markdown
Author

Thanks, all three were correct. Addressed in 07c7dc2.

1. CONTRIBUTING.md wording was inaccurate. Verified both counts: non-colocated tests do exist (packages/adapters/test/, apps/cli/test/), and apps/email/server/test/from-header.test.ts does import from bun:test. Reworded to say most workspaces use Vitest and most colocate, called out the test/ directory convention and the Bun runner by name, and scoped the rest of the section explicitly to apps/dashboard.

2. Typo fixed. normalizeSubdomandInput to normalizeSubdomainInput.

3. The "exhaustiveness guard" comment overclaimed. You were right that a manual array silently drifts. Rather than soften the wording, I made the guard real by keying it as a Record<ProjectStatus, true>:

const STATUS_COVERAGE: Record<ProjectStatus, true> = { live: true, /* ... */ draft: true };
const allStatuses = Object.keys(STATUS_COVERAGE) as ProjectStatus[];

Verified it actually fails now by temporarily adding "archived" to the ProjectStatus union:

apps/dashboard/src/utils/project-status.test.ts(150,9): error TS2741:
  Property 'archived' is missing in type '{ live: true; ... draft: true; }'
  but required in type 'Record<ProjectStatus, true>'.

So adding a status without covering it now breaks tsc --noEmit. Union reverted afterwards; suite is back to 8 files / 116 tests passing.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 9 changed files in this pull request and generated no new comments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants