Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
e40ab35
[NO-ISSUE] test(webkit): add browser-mode test foundation
robsongajunior Jul 7, 2026
7f7243e
[NO-ISSUE] test(webkit): add Button suite and finalize browser-mode c…
robsongajunior Jul 7, 2026
a9e6230
[NO-ISSUE] test(webkit): add functional suites for 24 monolithic comp…
HerbertJulio Jul 1, 2026
11d7586
[NO-ISSUE] test(webkit): add functional suites for 24 input/field/sim…
HerbertJulio Jul 1, 2026
fb2f36e
[NO-ISSUE] test(webkit): add functional suites for composition, overl…
HerbertJulio Jul 2, 2026
fa12b98
[NO-ISSUE] docs(webkit): add testing rule (browser-mode functional su…
HerbertJulio Jul 2, 2026
05e0759
[NO-ISSUE] test(webkit): adapt copy-button test to IconButton span wr…
HerbertJulio Jul 2, 2026
ee25ca2
fix: deleting orfan test, empty-results-block.test.ts
robsongajunior Jul 2, 2026
006a71b
fix: infra/packing, vitetest v4, hook+scaffold, ci+docs higiene, conf…
robsongajunior Jul 7, 2026
8ddfd38
chore: pnpm.lock
robsongajunior Jul 7, 2026
dc1d184
fix: .test.ts lint, eslint configuration, sync tests real scnario
robsongajunior Jul 3, 2026
bd32efa
feat: adding regression test configuration
robsongajunior Jul 7, 2026
9f17f8f
chore: "visual-test-freeze" string usage
robsongajunior Jul 7, 2026
591c60f
chore: remove @aziontech/webkit.dev alias storybook
robsongajunior Jul 7, 2026
9c39d53
chore: sync calendar.test.ts update
robsongajunior Jul 7, 2026
7023f3f
chore: update pnpm webkit:format
robsongajunior Jul 7, 2026
cdeda81
chore: rollback apps to packages workspace
robsongajunior Jul 7, 2026
ce0b99c
chore: update -lock file with apps
robsongajunior Jul 7, 2026
74c55d0
fix: sync prettier@3.9.4 cross package
robsongajunior Jul 7, 2026
4f079ff
fix: stylelint, ts check
robsongajunior Jul 7, 2026
074d10c
chore: delete __image_snapshots__/darwin
robsongajunior Jul 7, 2026
7b9e2eb
chore: complete tests
robsongajunior Jul 8, 2026
68b0daa
Merge branch 'dev' into test/webkit
robsongajunior Jul 8, 2026
5e4a8c2
chore: update pipeline filename
robsongajunior Jul 8, 2026
e235902
chore: update playwrite dependencie version and add to storybook app
robsongajunior Jul 8, 2026
7162a79
chore: storybook baseline upload (download from gh artifact)
robsongajunior Jul 8, 2026
9682053
chore: adding storybook:test:visual:update:baseline scripts package.json
robsongajunior Jul 9, 2026
25e014f
Merge branch 'dev' into test/webkit
robsongajunior Jul 9, 2026
ee947d1
Merge branch 'dev' into test/webkit
robsongajunior Jul 9, 2026
1394d51
Merge branch 'dev' into merge/webkit
robsongajunior Jul 10, 2026
d0afadb
chore: governance adding tests requirements
robsongajunior Jul 10, 2026
6fd882d
refactor: tests with new event pattern
robsongajunior Jul 10, 2026
f0cf448
chore: adjust jest folder test, ignoring templates
robsongajunior Jul 10, 2026
aa04408
chore: regenerating visual baseline
robsongajunior Jul 10, 2026
98d6837
refactor: exclusive storybook regenerate baseline
robsongajunior Jul 10, 2026
7eb1104
chore: regenerated baseline
robsongajunior Jul 10, 2026
872b77c
fix: 'Property access on null or undefined'
robsongajunior Jul 10, 2026
d643968
Potential fix for pull request finding 'Property access on null or un…
robsongajunior Jul 10, 2026
8b56a3b
Potential fix for pull request finding 'Property access on null or un…
robsongajunior Jul 10, 2026
9803ee7
Potential fix for pull request finding 'Useless conditional'
robsongajunior Jul 10, 2026
11c9d8a
Potential fix for pull request finding 'Property access on null or un…
robsongajunior Jul 10, 2026
7e5dd97
Potential fix for pull request finding 'Property access on null or un…
robsongajunior Jul 10, 2026
db18682
Potential fix for pull request finding 'Property access on null or un…
robsongajunior Jul 10, 2026
8326779
fix: breadcrumb test
robsongajunior Jul 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
4 changes: 4 additions & 0 deletions .claude/hooks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ PostToolUse hook on `Write|Edit|MultiEdit` of the same `.vue` paths. Re-reads th

Bypassed for legacy components (same whitelist). Shared parser library: [`_lib/spec.mjs`](./_lib/spec.mjs).

### [`enforce-test-exists.mjs`](./enforce-test-exists.mjs)

PostToolUse hook on `Write` of a **root** component `.vue` (`packages/webkit/src/components/<category>/<name>/<name>.vue` — file name equals its folder). Warns (exit 2, surfaced to the agent) when the co-located `<name>.test.ts` is missing, so every component ships the browser-mode functional suite mandated by [`../rules/testing.md`](../rules/testing.md). PostToolUse (not Pre) so it never deadlocks `/component-create`, which writes the `.vue` before a test can exist. Composition sub-components are skipped (tested through their root). Bypassed for legacy components (same whitelist).

## Adding a new hook

1. Create `<name>.mjs` in this directory.
Expand Down
76 changes: 76 additions & 0 deletions .claude/hooks/enforce-test-exists.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#!/usr/bin/env node
// PostToolUse hook: after a webkit-layer ROOT component .vue is written, warns
// when the co-located <name>.test.ts is missing. Every component ships a
// browser-mode functional suite next to its .vue (.claude/rules/testing.md).
//
// Why PostToolUse (not Pre): /component-create writes the .vue before any test
// can exist, so a Pre block would deadlock the pipeline. Post surfaces the
// reminder without undoing the write — same wiring as validate-spec-compliance.
//
// Only the ROOT .vue is checked (basename === folder name). Composition
// sub-components are tested through their root (testing.md), and resolveSpec…
// already returns null for sub-components nested in their own folder.
//
// Bypassed for components on the legacy whitelist
// (.claude/hooks/_lib/legacy-components.json).

import { existsSync } from 'node:fs'
import { basename, dirname, relative, resolve } from 'node:path'
import { isLegacyComponent, resolveSpecForComponentPath } from './_lib/spec.mjs'

const ROOT = process.cwd()

function readStdin() {
return new Promise((res) => {
let data = ''
process.stdin.on('data', (chunk) => (data += chunk))
process.stdin.on('end', () => res(data))
})
}

async function main() {
const raw = await readStdin()
let input
try {
input = JSON.parse(raw)
} catch {
process.exit(0)
}

if (input.tool_name !== 'Write') process.exit(0)
const filePath = input.tool_input?.file_path
if (!filePath) process.exit(0)

const abs = resolve(filePath)
const info = resolveSpecForComponentPath(abs, ROOT)
if (!info) process.exit(0)

// Root component only: <category>/<name>/<name>.vue. Composition sub-components
// (<name>-part.vue) are tested through their root, so skip them.
if (basename(abs, '.vue') !== info.name) process.exit(0)

// Legacy components bypass.
if (isLegacyComponent(info.category, info.name, ROOT)) process.exit(0)

const testPath = resolve(dirname(abs), `${info.name}.test.ts`)
if (existsSync(testPath)) process.exit(0)

process.stderr.write(
`enforce-test-exists: ${info.category}/${info.name} has no co-located ${info.name}.test.ts\n\n` +
`Every webkit component ships a browser-mode functional suite next to its .vue\n` +
`(.claude/rules/testing.md). Missing:\n` +
` ${relative(ROOT, testPath)}\n\n` +
`Reuse the story via composeStories, assert the functional surface (render,\n` +
`props -> data-*, events, disabled/loading suppression, v-model, ARIA, and\n` +
`a11y via expectNoA11yViolations), and run it in Vitest browser mode (never\n` +
`jsdom). If this is a legacy component, add\n` +
` { "category": "${info.category}", "name": "${info.name}" }\n` +
`to .claude/hooks/_lib/legacy-components.json.\n`
)
process.exit(2)
}

main().catch((err) => {
process.stderr.write(`enforce-test-exists hook error: ${err?.message ?? err}\n`)
process.exit(0) // fail open
})
77 changes: 77 additions & 0 deletions .claude/rules/testing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Rule: testing — every component ships a functional browser-mode suite

Every webkit component ships a co-located `*.test.ts` that proves it **works**, exercised in a **real browser**. The spec-compliance hooks prove a `.vue` *declares* what its `.specs/<name>.md` promises; they do not prove a prop is read, an event is emitted, a focus ring is reachable, or an overlay closes on `Escape`. This rule closes that gap with the smallest per-component cost, and fixes the **floor** (not the ceiling) every component must clear.

## The rule

> Every component under `packages/webkit/src/components/<category>/<name>/` ships one `<name>.test.ts` next to the `.vue`. It runs in **Vitest browser mode** (Playwright Chromium — never jsdom), reuses the component's Storybook story as the fixture via `composeStories`, asserts the functional surface below, and runs `axe-core` against the rendered tree. Composition sub-components are tested **through their root**; only the root gets a `.test.ts` (unless the spec promises behavior the root test cannot reach).

## Why browser mode, never jsdom

jsdom returns no-ops for `focus`, `document.activeElement`, layout/`getBoundingClientRect`, and does not surface `<Teleport>`d content — so a test that "passes" there is a false positive for exactly the behaviors that break in production (keyboard, focus trap, overlays, positioning, contrast). We run in real Chromium so those are real.

- **No mocks for layout / positioning / focus / `<Teleport>`.** If a test "needs" one of those mocks, the test is wrong. Real browser makes them real.
- Teleported overlay content escapes the render container — query it from `document.body`, not the `render()` result.

## The stack (already wired — do not reinvent)

- `packages/webkit/vitest.config.ts` — `@vitejs/plugin-vue`, `browser: { provider: 'playwright', instances: [{ browser: 'chromium' }], headless: true }`, `define: { 'process.env.NODE_ENV': ... }` (so `@testing-library/vue`'s `fireEvent` runs in the browser), `resolve.alias['@aziontech/webkit'] = '@aziontech/webkit.dev'` (self-reference so story imports resolve), `retry: process.env.CI ? 2 : 0`.
- `packages/webkit/src/test/setup.ts` — imports `@aziontech/theme/globals.css` (styled DOM ⇒ axe contrast is real) + `cleanup()`.
- `packages/webkit/src/test/axe.ts` — `expectNoA11yViolations(container)`.
- `.github/workflows/test.yml` — sharded (×4) + retry, runs only when webkit/storybook changes.
- Publish-safety: `packages/webkit/package.json#files` negates `*.test.ts` and `src/test/**` (verified with `pnpm --filter webkit pack:dry`). Test files never ship to npm.

## Conventions

- **Location:** `packages/webkit/src/components/<category>/<name>/<name>.test.ts` (sibling of the `.vue` / `index.ts`).
- **Imports:** `describe`/`it`/`expect` from `vitest` (explicit — no `globals`); `render`/`fireEvent`/`screen` from `@testing-library/vue`; `userEvent` from `@storybook/test` for realistic keyboard/pointer; `composeStories` from `@storybook/vue3`; `expectNoA11yViolations` from the relative `test/axe`; the component via relative `./...`.
- **Story import is a RELATIVE path**, never a `@stories` alias — `validate-references.mjs` cannot resolve a vite alias and will block the write. If a component has no story, test it directly with `render(Component, { props })`.
- **Local runs** (when node_modules is symlinked into a worktree) set `PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN=false CI=` so pnpm's deps check doesn't abort against symlinks.

## What every `<name>.test.ts` must cover

| # | Surface | Assertion |
|---|---|---|
| 1 | Render | mounts without throwing; the `data-testid` fallback is present; consumer `data-testid` override wins |
| 2 | Props / variants | each variant prop (`kind`, `size`, …) maps to its `data-*` / attribute / rendered state |
| 3 | Events | every event in the spec's Events table fires with the right payload on the real user action |
| 4 | Suppression | when `disabled` / `loading` / `readonly`, the action is **not** emitted |
| 5 | v-model | drive the input, assert `update:modelValue` (and `update:open` / `update:*`) with the exact value |
| 6 | ARIA | `role`, `aria-expanded`, `aria-busy`, `aria-disabled`, `aria-selected`… as the template declares |
| 7 | a11y | `expectNoA11yViolations(container)` on the default render + any variant whose semantics differ |
| 8 | Composition | a context-aware sub-component reflects/drives the root's `provide`/`inject` state with no manual wiring |
| 9 | Overlay | open/close (trigger + second click), `Escape` closes and returns focus, panel Teleports to `body`, scroll-lock while open |
| 10 | Recursive | nested instances ≥2 levels deep render and propagate context (active item, open submenu, orientation) |

A tiny `it.each` smoke over enum variants ("mounts without throwing") is a **floor**, never the substance.

## The functional bar — no false positives, no filler

- Assert **only what you read** in the source. Never invent props/events/testids/aria/sub-components.
- **Forbidden:** assertions on Tailwind/class strings, pixel positions, animation timing, or internal component state.
- **If a test only passes when the implementation is written one specific way, delete it.** It traps refactors and adds no signal.
- If a test reveals a real component defect you cannot satisfy without changing the `.vue`, **`it.skip` it with a one-line reason** — never fake a pass or weaken an assertion into meaninglessness. Record the gap in the PR.

## Composition, overlay, recursive — how to reach them

- **Composition:** import the compound root (default export of `index.ts`) and its sub-components; render a realistic composed tree; assert dot-notation resolves (`Root.Sub`), `provide`/`inject` delivers state, events fire, `v-model` round-trips. Data-driven roots (`data` + `columns`) render via props and assert rows/cells render through the sub-components.
- **Overlay** (`data-state="open|closed"` + `<Teleport>` + trigger): use `userEvent`; query the panel from `document.body`; assert `role`/`aria-*`, `Escape` + focus restoration, backdrop/close-button dismissal, scroll-lock.
- **Recursive** (`navigation-menu`, `breadcrumb`): build a ≥2-level tree; assert nested render + context propagation; exercise keyboard where supported.

## Hard prohibitions

- No jsdom; no mocking layout/positioning/focus/`<Teleport>`.
- No `@stories` alias in a test import — use a relative path (the reference hook blocks the alias).
- No class-string / pixel / animation-timing / internal-state assertions.
- No test file outside the co-located `<name>.test.ts` convention; sub-components do not get their own test unless the root cannot reach the behavior.
- Do not edit a `.vue` to make a test pass — fix the test, or `it.skip` + document.

## Enforcement

- `.github/workflows/test.yml` runs `pnpm webkit:test` (sharded browser mode) on every PR/push touching webkit.
- `validate-references.mjs` blocks a test whose imports don't resolve (including a mistaken `@stories` alias).
- `pnpm --filter webkit pack:dry` must list **no** `*.test.ts` — the `files` negation keeps tests out of the published package.

## Why this rule exists

A prop can be declared and never read; an event typed and never emitted; a focus ring rendered and never reached by `Tab`; an overlay that never closes on `Escape`. Lint, types and the Storybook build catch none of these. A real-browser functional suite, reusing the story as its fixture, catches all of them at one file per component — and refuses to pass on the jsdom no-ops that would otherwise give false confidence.
4 changes: 4 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@
{
"type": "command",
"command": "node .claude/hooks/validate-spec-compliance.mjs"
},
{
"type": "command",
"command": "node .claude/hooks/enforce-test-exists.mjs"
}
]
}
Expand Down
Loading
Loading