Skip to content

feat(ui): UI project with every quality gate, on a translated placeholder page (#358) - #374

Merged
bitbiter-dev merged 8 commits into
masterfrom
feat/358-ui-project
Oct 30, 2026
Merged

bitbiter-dev merged 8 commits into
masterfrom
feat/358-ui-project

Conversation

@bitbiter-dev

@bitbiter-dev bitbiter-dev commented Oct 30, 2026 •

Copy link
Copy Markdown
Owner

Closes #358. Part of #344. Decisions made during implementation: comment.

Summary

src/Anichron.UI/                 pnpm project, outside the .NET solution
├── src/app/                     main.tsx, App.tsx, router.tsx (code-based route tree), base.css
├── src/i18n/                    createI18n(), locales/en.json, keys typed from it
├── src/features/placeholder/    PlaceholderPage.tsx + its seam-2 test
└── src/test/                    renderApp(path), MSW server, axeResults(), fake timers
scripts/ui-mutation-changed.sh   scoped StrykerJS run (+ .test.sh)
scripts/ui-mutation-guard.sh     mirrors mutation-guard.sh (+ .test.sh)
docs/adr/0014-…                  why .tsx is out of mutation scope, router.tsx back in
 CI gate
-  needs: [checks, integration, mutation, docker]
+  needs: [checks, integration, mutation, ui, docker]
+ UI: typecheck, lint, format, depcruise, vitest, script tests, ui-mutation-guard,
+     scoped Stryker (PR) / full (master), build, size-limit
 CodeQL:      csharp  +  javascript-typescript
 Dependabot:  +npm (/src/Anichron.UI), lint tooling grouped apart
 pre-commit:  +prettier --check, eslint, tsc --noEmit, depcruise   (only when src/Anichron.UI/** is staged)
 pre-push:    +scripts/ui-pre-push.sh   (skips unless the branch changes src/Anichron.UI; frozen install, then vitest run)
  • Toolchain: TypeScript 6.0 (strict, noUncheckedIndexedAccess, noImplicitOverride). TypeScript 7 is out because typescript-eslint requires <6.1. Also Vite 8 with the React Compiler through Babel (the Rust compiler is marked experimental), TanStack Router, react-i18next, Vitest 5 in jsdom with MSW 3 (onUnhandledFrame: 'error'), and vitest-axe.
  • ESLint 9, not 10: eslint-plugin-jsx-a11y declares peers only up to ESLint 9, and strictPeerDependencies is on, so the install fails on a mismatch.
  • Browser targets: browserslist in package.json, iOS first, last two versions. Vite's build.target is derived from it.
  • Node 26.13.0 in .nvmrc and engines, with pnpm 12.16.1 in packageManager; CI installs both through pnpm/setup@v3. The check:
    • Node 26 became Active LTS on 2026-10-28, and 24 went into maintenance on 2026-10-20.
    • Vite tests Node 26 in its CI matrix, and Playwright's system requirements list 22, 24 and 26.
    • StrykerJS declares engines >=22, but its own CI runs only 22 and 24, so I checked it by running it: the full UI run on Node 26 reports 100% (11 killed), and an earlier run failed at 91.67% on a real survivor.
  • Rule 1 ("only api/ calls fetch") is in ESLint: dependency-cruiser cannot see globals. See the Decision comment.
  • Literal strings: react/jsx-no-literals covers JSX text, and no-restricted-syntax covers user-visible attributes (aria-label, alt, title, placeholder, …). A t('key') call passes; a literal fails.
  • Shared-TMPDIR hazard: does not apply to StrykerJS.
    • Its sandboxes live in the project's own .stryker-tmp.
    • The Vitest runner hands the active mutant to the tests in process (provide/inject), and its setup file is written inside the sandbox.
    • Recorded in docs/mutation-testing.md, "The UI".
  • Base stylesheet: a minimal base.css (system font, color-scheme) gives the CSS budget a first caller.

Evidence

Seam 2. renderApp('/') renders the whole app through its real route tree. It finds the Anichron heading and the body text, and axe reports no violations.

axe yields through setTimeout, so it hung under fake timers (the test timed out at 5 s). It now passes: axeResults drives axe on the fake clock.

StrykerJS proved both ways:

  • Before: initAsync: false survived; the run reported 91.67% and exited 1. The mutant was equivalent, since i18next initialises synchronously with inline catalogs (measured), so the option was removed rather than suppressed.
  • After: 100.00%, 11 of 11 killed, including mutants killed by a test inside describe (stryker-js#6265).

Each dependency-cruiser rule failing on a deliberate violation (the files were removed afterwards):

features/X → features/Y   error features-are-isolated: src/features/other/Other.tsx → src/features/placeholder/PlaceholderPage.tsx
shared → app              error shared-and-api-stay-below-features: src/shared/a.ts → src/app/App.tsx
api → feature             error only-app-imports-features: src/api/b.ts → src/features/placeholder/PlaceholderPage.tsx
i18n → feature            error only-app-imports-features: src/i18n/c.tsx → src/features/placeholder/PlaceholderPage.tsx
cycle                     error no-circular: src/shared/x.ts → src/shared/y.ts → src/shared/x.ts
prod → test code          error production-imports-no-test-code: src/shared/d.ts → src/test/server.ts
.ts → .tsx                error logic-imports-no-views: src/shared/e.ts → src/shared/View.tsx

Network rule (ESLint): src/shared/net.ts fails and the same file under src/api/ passes:

2:9   Unexpected use of 'fetch'. Only src/api/ talks to the network …          no-restricted-globals
3:9   'window.fetch' is restricted from being used. …                         no-restricted-properties
4:14  Unexpected use of 'EventSource'. …                                       no-restricted-globals

Lint and budget:

  • A .tsx component with five branches fails complexity 4.
  • size-limit: 109.32 kB of 150 kB JS and 117 B of 15 kB CSS. Lowering the JS limit to 100 kB exits 1.

Hooks, checked in this worktree with core.hooksPath pointed at it:

  • With a UI file staged, the four UI tasks run: Prettier 0.5 s, ESLint 3.3 s, tsc 1.3 s, depcruise 0.5 s. depcruise is under the ~2 s bound.
  • With only a script staged, each reports "Skipped, no staged matched files".

Script tests:

  • ui-mutation-changed.test.sh, 13 cases. They cover scoped vs whole scope, deleted files, a types-only change (exit 0, "nothing to mutate"), and a failing run still failing.
  • ui-mutation-guard.test.sh, 17 cases.
  • ui-pre-push.test.sh, 5 cases: skip on a backend-only push, install then test on a UI change, failing tests fail the push.
  • With no Node on PATH, pnpm test downloaded Node 26.13.0 and passed.
  • mutation-guard.sh still passes.

Merge Danger

Door: two-way. No schema, contract or data change. The UI isn't served yet (#359).

Blast Radius: CI-wide.

  • The CI gate now waits on the UI job.
  • The CodeQL job becomes a two-language matrix. Branch protection's CodeQL check comes from code scanning results, not the job name, so it is unaffected.
  • Pre-push gains scripts/ui-pre-push.sh. When the branch changes nothing under src/Anichron.UI, it skips, so backend-only pushes are unaffected. Otherwise it runs a frozen install, then vitest run. Node needs no PATH setup: devEngines.runtime pins 26.13.0 in the lockfile, and pnpm downloads it. The version is stated in .nvmrc, engines and devEngines, so bump all three together.
  • The initial-JS glob dist/assets/index-*.js counts only the entry chunk. Once Rollup splits a shared chunk that the entry preloads, it undercounts, so revisit it with the first route chunk.

🤖 Generated with Claude Code

bitbiter-dev and others added 7 commits October 30, 2026 15:23
React + strict TypeScript on Vite with the React Compiler, TanStack Router,
react-i18next, ESLint, Prettier, dependency-cruiser, Vitest with MSW and
vitest-axe, and a size-limit budget.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…358)

ADR 0014 records why views are out of mutation scope and why the route
tree is back in. ui-mutation-changed.sh scopes a run to changed UI files,
ui-mutation-guard.sh keeps the setup honest, and the hooks run the UI's
format, lint, typecheck and dependency rules when UI files are staged and
its tests before a push.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#358)

Documents the UI's commands, layout rules and mutation rules in CLAUDE.md,
and in docs/mutation-testing.md why StrykerJS needs no private TMPDIR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A deleted UI file now makes the scoped mutation run cover the whole
scope, literal strings in user-visible attributes fail lint, the unused
user-event package is gone, and rationale comments are trimmed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-advanced-security

Copy link
Copy Markdown

You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool.

What Enabling Code Scanning Means:

  • The 'Security' tab will display more code scanning analysis results (e.g., for the default branch).
  • Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results.
  • You will be able to see the analysis results for the pull request's branch on this overview once the scans have completed and the checks have passed.

For more information about GitHub Code Scanning, check out the documentation.

…changed (#358)

devEngines.runtime pins Node 26.13.0 in the lockfile, so the hooks need
no Node on PATH. scripts/ui-pre-push.sh skips a push that changes nothing
under src/Anichron.UI and runs a frozen install before the tests, so a
fresh worktree works.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bitbiter-dev
bitbiter-dev merged commit a917146 into master Oct 30, 2026
19 checks passed
@bitbiter-dev
bitbiter-dev deleted the feat/358-ui-project branch October 30, 2026 03:38
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.

UI project with every quality gate, on a translated placeholder page

2 participants