@marianmeres/stuic 3.114.0 → 3.115.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/H/README.md +1 -1
- package/dist/components/H/index.css +4 -1
- package/docs/component-testing/00-overview-and-roadmap.md +119 -0
- package/docs/component-testing/01-framework-setup.md +164 -0
- package/docs/component-testing/02-test-conventions.md +148 -0
- package/docs/component-testing/03-component-coverage-roadmap.md +92 -0
- package/docs/component-testing/04-hard-cases-and-e2e.md +97 -0
- package/docs/component-testing/05-ci.md +88 -0
- package/docs/component-testing/PROGRESS.md +63 -0
- package/docs/component-testing/README.md +38 -0
- package/docs/testing.md +27 -8
- package/package.json +8 -3
|
@@ -63,7 +63,7 @@ Render a semantic `h2` but style it like an `h4`:
|
|
|
63
63
|
| `--stuic-h-line-height` | `1.2` | Line height |
|
|
64
64
|
| `--stuic-h-letter-spacing` | `normal` | Letter spacing |
|
|
65
65
|
| `--stuic-h-color` | `inherit` | Text color |
|
|
66
|
-
| `--stuic-h-margin` | `0`
|
|
66
|
+
| `--stuic-h-margin` | `0 0 0.5em` | Margin (bottom-only) |
|
|
67
67
|
| `--stuic-h1-font-size` | `clamp(1.875rem, 1.6rem + 1vw, 2.5rem)` | H1 font size (fluid) |
|
|
68
68
|
| `--stuic-h2-font-size` | `clamp(1.5rem, 1.3rem + 0.9vw, 2.125rem)` | H2 font size (fluid) |
|
|
69
69
|
| `--stuic-h3-font-size` | `clamp(1.25rem, 1.1rem + 0.65vw, 1.625rem)` | H3 font size (fluid) |
|
|
@@ -10,7 +10,10 @@
|
|
|
10
10
|
--stuic-h-line-height: 1.2;
|
|
11
11
|
--stuic-h-letter-spacing: normal;
|
|
12
12
|
--stuic-h-color: inherit;
|
|
13
|
-
|
|
13
|
+
/* Bottom-only by default: binds the heading to the content below it, while
|
|
14
|
+
space above comes from the previous element's collapsing bottom margin.
|
|
15
|
+
Avoids the first-child / flex-grid issues a baked-in margin-top would cause. */
|
|
16
|
+
--stuic-h-margin: 0 0 0.5em;
|
|
14
17
|
|
|
15
18
|
/* Per-level font sizes — fluid scaling, original values at ~1024px viewport */
|
|
16
19
|
--stuic-h1-font-size: clamp(1.875rem, 1.6rem + 1vw, 2.5rem);
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
GENERATED ANALYSIS — @marianmeres/stuic real-browser component testing
|
|
3
|
+
Produced 2026-06-08 by multi-agent research → adversarial verify → synthesize.
|
|
4
|
+
Claims verified against the codebase at commit cc9958b and the live Vitest 4 /
|
|
5
|
+
vitest-browser-svelte docs. Planning artifact; no code was changed.
|
|
6
|
+
-->
|
|
7
|
+
|
|
8
|
+
# @marianmeres/stuic — Component Testing: Overview & Roadmap
|
|
9
|
+
|
|
10
|
+
> **Verdict:** the proposed stack — Vitest Browser Mode + `vitest-browser-svelte` + Playwright/Chromium
|
|
11
|
+
> — is the right default for *this* library, where the value being shipped is precisely the DOM/layout/
|
|
12
|
+
> focus/positioning behavior that the current node/server-build test setup **cannot exercise at all**.
|
|
13
|
+
> The claim is *mostly* correct rather than gospel: it's not an officially-mandated singular standard
|
|
14
|
+
> (svelte.dev still nominally leads with jsdom + @testing-library), and one term was dated — modern
|
|
15
|
+
> spelling is `vitest-browser-svelte` + **`@vitest/browser-playwright`** + `playwright` on **Vitest 4**.
|
|
16
|
+
>
|
|
17
|
+
> **The one thing that matters most:** a **vitest 3 → 4 major upgrade is a hard prerequisite**
|
|
18
|
+
> (`vitest-browser-svelte@^2` peer-requires `vitest ^4`; Browser Mode is only stable in v4). Do it as
|
|
19
|
+
> a discrete, reversible first commit and confirm the 9 existing suites stay green before anything else.
|
|
20
|
+
>
|
|
21
|
+
> **The second thing:** route tests by filename into a Vitest `projects` split — `*.test.ts` → fast
|
|
22
|
+
> **node** (the existing 9 suites, untouched), `*.svelte.test.ts` → real **browser**. Get that glob
|
|
23
|
+
> right and nothing regresses; get it wrong and either utils crawl in a browser or components fail in
|
|
24
|
+
> node with the very server-build error this effort exists to escape.
|
|
25
|
+
>
|
|
26
|
+
> Read order: this file → [`01-framework-setup`](./01-framework-setup.md) for the exact config →
|
|
27
|
+
> [`02-test-conventions`](./02-test-conventions.md) for how to write a test → then work
|
|
28
|
+
> [`PROGRESS.md`](./PROGRESS.md) top-down. [`03`](./03-component-coverage-roadmap.md) ranks all 74
|
|
29
|
+
> components; [`04`](./04-hard-cases-and-e2e.md) handles the hard 30; [`05`](./05-ci.md) is CI.
|
|
30
|
+
|
|
31
|
+
## A note on `docs/testing.md` (this is a partial reversal — by design)
|
|
32
|
+
|
|
33
|
+
[`docs/testing.md`](../testing.md) records a deliberate decision **not** to test component rendering
|
|
34
|
+
("50+ components × prop combos = slow suite, tiny yield; rendering is gated by svelte-check + publint +
|
|
35
|
+
build"). That reasoning holds for *"does it render"* and we keep it. What changes: browser mode lets
|
|
36
|
+
us test *"does it **behave**"* — events, two-way binding, aria/disabled/active state, focus traps,
|
|
37
|
+
viewport-clamped positioning (cf. the `9d8c974` annotation regression) — which the build does **not**
|
|
38
|
+
cover and which was **previously impossible**. Updating `docs/testing.md` to add this layer is an
|
|
39
|
+
explicit sprint task so the docs don't contradict each other.
|
|
40
|
+
|
|
41
|
+
## Top recommendations across all dimensions (ranked)
|
|
42
|
+
|
|
43
|
+
| Rank | Recommendation | Dimension | Value | Effort | Risk | Why now |
|
|
44
|
+
|------|----------------|-----------|-------|--------|------|---------|
|
|
45
|
+
| 1 | Upgrade vitest 3→4, verify 9 suites green | [01](./01-framework-setup.md) | high | S | med | Gating prerequisite; nothing installs without it |
|
|
46
|
+
| 2 | Add `projects` split (node `server` + browser `client`) + Chromium | [01](./01-framework-setup.md) | high | S | med | The harness; routes by `*.svelte.test.ts` filename |
|
|
47
|
+
| 3 | Separator smoke test — prove client build + `$effect` actually run | [01](./01-framework-setup.md) | high | S | med | Disproves/confirms the documented server-build blocker |
|
|
48
|
+
| 4 | Reconcile `docs/testing.md` (behavior ✅, rendering still ❌) | [02](./02-test-conventions.md) | med | S | low | Keep docs internally consistent before scaling |
|
|
49
|
+
| 5 | Button — flagship; sets every assertion pattern | [03](./03-component-coverage-roadmap.md) | high | S | low | Most-used primitive; template for the rest |
|
|
50
|
+
| 6 | Pill, Switch — events + binding patterns | [03](./03-component-coverage-roadmap.md) | high | S | low | Cover dismiss/toggle/bind once, reuse everywhere |
|
|
51
|
+
| 7 | Spinner, Skeleton, DismissibleMessage, Avatar, Progress | [03](./03-component-coverage-roadmap.md) | high | S | low | Deterministic, high-traffic; quick wins |
|
|
52
|
+
| 8 | **One hard proof** — anchor-position viewport clamp (or focus trap) | [04](./04-hard-cases-and-e2e.md) | high | M | med | Guards a real recent regression; proves browser mode's worth |
|
|
53
|
+
| 9 | Minimal GitHub Actions workflow | [05](./05-ci.md) | high | S | low | Stops broken tests reaching npm; once a few tests pass |
|
|
54
|
+
| 10 | Tier-2 form fields (`FieldInput` first, then the family) | [03](./03-component-coverage-roadmap.md) | med | M | low | Largest component group; one pattern unlocks many |
|
|
55
|
+
| 11 | Portals/focus-traps in browser mode (Modal/Drawer/Backdrop) | [04](./04-hard-cases-and-e2e.md) | med | M | med | High-value a11y contracts; after patterns settle |
|
|
56
|
+
| 12 | Standalone Playwright E2E layer (drag, Milkdown, checkout flows) | [04](./04-hard-cases-and-e2e.md) | med | L | med | Separate later initiative; explicitly out of sprint 1 |
|
|
57
|
+
|
|
58
|
+
> **Deliberately deferred as low-yield:** visual-regression / `toMatchScreenshot`, multi-browser
|
|
59
|
+
> (Firefox/WebKit) matrix, and exhaustive prop-matrix coverage. Revisit only if motivated by a real bug.
|
|
60
|
+
|
|
61
|
+
## Recommended first sprint (do these first)
|
|
62
|
+
|
|
63
|
+
Branch: `feat/component-testing`. One commit per task.
|
|
64
|
+
|
|
65
|
+
1. **Vitest 4 upgrade (#1)** — `pnpm add -D vitest@^4`, run `pnpm test`, confirm 9 suites green. Why
|
|
66
|
+
first: everything else peer-depends on it; isolating it makes the one risky bump reversible.
|
|
67
|
+
2. **Browser harness (#2, #3)** — add browser deps, the `projects` config, `playwright install
|
|
68
|
+
chromium`, fix the test scripts, and land the Separator smoke test. Unblocks all component tests
|
|
69
|
+
and proves the server-build blocker is gone. Detail in [01](./01-framework-setup.md).
|
|
70
|
+
3. **Reconcile `docs/testing.md` (#4)** — small doc edit so the philosophy matches reality.
|
|
71
|
+
4. **Button (#5)** — establishes the assertion vocabulary ([02](./02-test-conventions.md)) every later
|
|
72
|
+
test reuses. Highest-leverage single component.
|
|
73
|
+
5. **Pill → Switch → Spinner → Skeleton → DismissibleMessage → Avatar → Progress (#6, #7)** — the easy
|
|
74
|
+
tier, one commit each; fast, deterministic, high-traffic coverage.
|
|
75
|
+
6. **One hard proof (#8)** — anchor-position viewport clamp (recommended) or focus trap; the payoff
|
|
76
|
+
moment that justifies the whole setup. Detail in [04](./04-hard-cases-and-e2e.md).
|
|
77
|
+
7. **CI (#9)** — the ~30-line workflow, now that there's a real suite to run. Detail in [05](./05-ci.md).
|
|
78
|
+
|
|
79
|
+
## Cross-cutting themes
|
|
80
|
+
|
|
81
|
+
- **Filename routing is load-bearing.** The `*.svelte.test.ts` vs `*.test.ts` convention is what keeps
|
|
82
|
+
node fast and browser correct. It appears in every dimension.
|
|
83
|
+
- **Test behavior, not rendering.** The build already proves components render; tests exist for the
|
|
84
|
+
contracts it can't see (events, binding, aria, geometry). This both reconciles `docs/testing.md` and
|
|
85
|
+
picks the high-yield targets.
|
|
86
|
+
- **Extract-then-unit-test stays valid.** Pure logic in `_internal/*.ts` (drop math, cron parsing,
|
|
87
|
+
clamp math) should keep getting fast node tests; browser mode is additive, not a replacement.
|
|
88
|
+
- **One commit per component** keeps the effort resumable and reviewable, exactly matching the
|
|
89
|
+
PROGRESS.md convention.
|
|
90
|
+
|
|
91
|
+
## Dependency / sequencing notes
|
|
92
|
+
|
|
93
|
+
```mermaid
|
|
94
|
+
flowchart TD
|
|
95
|
+
A["1. vitest 3→4 upgrade"] --> B["2. projects config + Chromium + smoke test"]
|
|
96
|
+
B --> C["3. reconcile docs/testing.md"]
|
|
97
|
+
B --> D["4. Button (flagship patterns)"]
|
|
98
|
+
D --> E["5. easy tier: Pill, Switch, Spinner, Skeleton, ..."]
|
|
99
|
+
E --> F["8. one hard proof (anchor clamp / focus trap)"]
|
|
100
|
+
E --> G["9. CI workflow"]
|
|
101
|
+
F --> H["later: Tier 2 fields, portals, standalone Playwright E2E"]
|
|
102
|
+
G --> H
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Completeness check
|
|
106
|
+
|
|
107
|
+
- **Snippet-heavy components** (Button needs `children`): the `createRawSnippet` pattern is settled in
|
|
108
|
+
[02](./02-test-conventions.md); first real exercise is Button — watch for friction and codify a shared
|
|
109
|
+
helper home then.
|
|
110
|
+
- **SvelteKit-plugin-in-browser-mode** is the top unknown; the Separator smoke test (task 2) is the
|
|
111
|
+
designated early canary, with a documented fallback (plain `svelte()` plugin for the client project).
|
|
112
|
+
- **`packageManager` field is absent** — surfaces in CI ([05](./05-ci.md)); resolve when writing the
|
|
113
|
+
workflow.
|
|
114
|
+
- Not yet scoped: a dedicated `playwright.config.ts` for the standalone E2E layer — intentionally
|
|
115
|
+
deferred to its own future initiative ([04](./04-hard-cases-and-e2e.md)).
|
|
116
|
+
|
|
117
|
+
Source documents: [`01-framework-setup`](./01-framework-setup.md), [`02-test-conventions`](./02-test-conventions.md),
|
|
118
|
+
[`03-component-coverage-roadmap`](./03-component-coverage-roadmap.md), [`04-hard-cases-and-e2e`](./04-hard-cases-and-e2e.md),
|
|
119
|
+
[`05-ci`](./05-ci.md).
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
GENERATED ANALYSIS — @marianmeres/stuic real-browser component testing
|
|
3
|
+
Produced 2026-06-08 by multi-agent research → adversarial verify → synthesize.
|
|
4
|
+
Claims verified against the codebase at commit cc9958b and the live Vitest 4 docs.
|
|
5
|
+
Planning artifact; no code was changed.
|
|
6
|
+
-->
|
|
7
|
+
|
|
8
|
+
# Framework Setup
|
|
9
|
+
|
|
10
|
+
> This is the infrastructure dimension: the one-time work that turns "no DOM, no `$effect`"
|
|
11
|
+
> into a working browser test harness. The single most important takeaway: **a Vitest
|
|
12
|
+
> `projects` split routed by filename** (`*.test.ts` → fast node, `*.svelte.test.ts` → real
|
|
13
|
+
> browser) keeps the 9 existing suites untouched while adding component tests on top. The one
|
|
14
|
+
> hard prerequisite is a **vitest 3 → 4 major upgrade** — `vitest-browser-svelte@^2` peer-requires
|
|
15
|
+
> `vitest ^4`, and Browser Mode only became *stable* in Vitest 4.
|
|
16
|
+
|
|
17
|
+
## Current state (verified at `cc9958b`)
|
|
18
|
+
|
|
19
|
+
- **Package manager:** pnpm. **Svelte:** 5.56.2. **SvelteKit:** 2.63.0 (adapter-auto). **Vitest:** 3.2.6.
|
|
20
|
+
- **Test config:** none dedicated. Vitest is configured only implicitly through
|
|
21
|
+
[`vite.config.ts`](../../vite.config.ts) (`plugins: [tailwindcss(), sveltekit()]`), and the
|
|
22
|
+
script is `"test": "vitest --dir src/"`.
|
|
23
|
+
- **What runs today:** 9 suites / ~59 tests, all **node environment, pure logic** — validation
|
|
24
|
+
helpers, stack classes (`NotificationsStack`, `AlertConfirmPromptStack`), `tr`, `replace-map`,
|
|
25
|
+
`storage-abstraction`, `checkout-utils`, `max-height`, `phone-validation`. Zero component tests.
|
|
26
|
+
- **Why components can't be tested today:** the `@sveltejs/kit/vite` plugin makes Vitest resolve
|
|
27
|
+
Svelte's **server (SSR) build** in node, so components render as strings and `$effect` / actions /
|
|
28
|
+
lifecycle never run (documented in project memory; an `$effect.root` + `flushSync` probe left the
|
|
29
|
+
effect body unrun even with `resolve.conditions: ['browser']`). Real-browser mode fixes this by
|
|
30
|
+
resolving the **client build** and running in an actual Chromium page.
|
|
31
|
+
- **No CI** (`.github/` absent). No browser-testing deps installed.
|
|
32
|
+
|
|
33
|
+
## Target state
|
|
34
|
+
|
|
35
|
+
A two-project Vitest config. Tests are routed purely by filename:
|
|
36
|
+
|
|
37
|
+
| Project | Environment | Matches | Runs |
|
|
38
|
+
|---------|-------------|---------|------|
|
|
39
|
+
| `server` | `node` | `src/**/*.test.ts` (excl. `*.svelte.test.ts`) | the existing 9 pure-logic suites — unchanged, fast |
|
|
40
|
+
| `client` | real browser (Chromium via Playwright) | `src/**/*.svelte.test.ts` | new component tests with DOM, `$effect`, layout, focus |
|
|
41
|
+
|
|
42
|
+
## Step 1 — Upgrade Vitest 3 → 4 (own commit, gating)
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pnpm add -D vitest@^4
|
|
46
|
+
pnpm test # confirm the 9 existing suites still pass before touching anything else
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Vitest 4 breaking changes that touch this repo (budget for them):
|
|
50
|
+
|
|
51
|
+
1. **Browser providers are separate packages now** — there is no built-in `@vitest/browser` provider; you install `@vitest/browser-playwright` (Step 2).
|
|
52
|
+
2. **Browser context import moved** — `@vitest/browser/context` → `vitest/browser` (only matters if/when we import `page`, `userEvent`, etc. directly; not needed for the basics).
|
|
53
|
+
3. `test.workspace` is deprecated in favour of `test.projects` (we use `projects` from the start).
|
|
54
|
+
|
|
55
|
+
> **Risk:** an incidental break in the 9 node suites during the bump. Mitigation: this is its own
|
|
56
|
+
> commit; if `pnpm test` regresses, fix or revert before any browser work. Verify against
|
|
57
|
+
> `@sveltejs/vite-plugin-svelte@6.2.4` and `@tailwindcss/vite` — both are Vite-7-era and compatible
|
|
58
|
+
> with Vitest 4, but confirm the dev server (`pnpm dev`) and `pnpm build` still work after the bump.
|
|
59
|
+
|
|
60
|
+
## Step 2 — Add browser deps + Chromium
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pnpm add -D @vitest/browser-playwright vitest-browser-svelte playwright
|
|
64
|
+
pnpm exec playwright install chromium # without this, browser tests fail: "No browsers found"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
We do **not** install `@vitest/browser` directly (pulled in transitively by the provider), and we
|
|
68
|
+
do **not** need `@testing-library/svelte` or `@testing-library/jest-dom` — `vitest-browser-svelte`'s
|
|
69
|
+
`render()` + `expect.element` replace both.
|
|
70
|
+
|
|
71
|
+
## Step 3 — The `projects` config
|
|
72
|
+
|
|
73
|
+
Add a `test` block to [`vite.config.ts`](../../vite.config.ts) (keep the file; just extend it).
|
|
74
|
+
Verified against the live Vitest 4 docs (`provider: playwright()` is an imported **function**, not
|
|
75
|
+
the old `'playwright'` string; `instances` is required):
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
/// <reference types="node" />
|
|
79
|
+
import { sveltekit } from "@sveltejs/kit/vite";
|
|
80
|
+
import { defineConfig } from "vitest/config";
|
|
81
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
82
|
+
import { playwright } from "@vitest/browser-playwright";
|
|
83
|
+
import dotenv from "dotenv";
|
|
84
|
+
|
|
85
|
+
dotenv.config();
|
|
86
|
+
|
|
87
|
+
export default defineConfig({
|
|
88
|
+
plugins: [tailwindcss(), sveltekit()],
|
|
89
|
+
server: {
|
|
90
|
+
port: parseInt(process.env.DEV_PORT || "8886"),
|
|
91
|
+
host: true,
|
|
92
|
+
},
|
|
93
|
+
test: {
|
|
94
|
+
projects: [
|
|
95
|
+
{
|
|
96
|
+
extends: true, // inherit root plugins (tailwind + sveltekit)
|
|
97
|
+
test: {
|
|
98
|
+
name: "server",
|
|
99
|
+
environment: "node",
|
|
100
|
+
include: ["src/**/*.test.ts"],
|
|
101
|
+
exclude: ["src/**/*.svelte.test.ts"],
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
extends: true,
|
|
106
|
+
test: {
|
|
107
|
+
name: "client",
|
|
108
|
+
include: ["src/**/*.svelte.test.ts"],
|
|
109
|
+
setupFiles: ["vitest-browser-svelte"],
|
|
110
|
+
testTimeout: 2000,
|
|
111
|
+
browser: {
|
|
112
|
+
enabled: true,
|
|
113
|
+
headless: true,
|
|
114
|
+
provider: playwright(),
|
|
115
|
+
instances: [{ browser: "chromium" }],
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Why this shape:
|
|
125
|
+
|
|
126
|
+
- **`extends: true`** makes each project inherit the root `plugins` array, so both the `tailwindcss()`
|
|
127
|
+
(component CSS / variant classes resolve — several STUIC components assert on variant/size class
|
|
128
|
+
output) and `sveltekit()` plugins apply. In the `client` project Vitest builds for the browser, so
|
|
129
|
+
the **client** Svelte build is used and `$effect`/actions run — the exact thing the node setup can't do.
|
|
130
|
+
- **The filename glob is the routing mechanism** and the single most important config decision. Note
|
|
131
|
+
`src/**/*.test.ts` *also* matches `foo.svelte.test.ts` (it ends in `.test.ts`), so the server
|
|
132
|
+
project's `exclude` is **required**, not optional.
|
|
133
|
+
- **`setupFiles: ['vitest-browser-svelte']`** is the entire setup — it registers auto-cleanup and
|
|
134
|
+
helpers. Browser mode eliminates the 60–250-line jsdom API-mock setup files older guides used.
|
|
135
|
+
- **`headless: true`** for CI; drop it (or `--browser.headless=false`) locally to watch tests run.
|
|
136
|
+
|
|
137
|
+
## Step 4 — Update the test scripts
|
|
138
|
+
|
|
139
|
+
`--dir src/` is now redundant (the `include` globs scope each project). Suggested:
|
|
140
|
+
|
|
141
|
+
```jsonc
|
|
142
|
+
"test": "vitest run", // one-shot, used by CI
|
|
143
|
+
"test:watch": "vitest", // watch mode for local dev
|
|
144
|
+
"test:ui": "vitest --ui" // optional: the Vitest UI, handy for browser tests
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
`pnpm test` then runs **both** projects (node + browser) in one command.
|
|
148
|
+
|
|
149
|
+
## Step 5 — Smoke test (prove the harness before writing real tests)
|
|
150
|
+
|
|
151
|
+
Create one trivial browser test — e.g. `src/lib/components/Separator/Separator.svelte.test.ts`
|
|
152
|
+
that renders `<Separator />` and asserts it's in the document. If it goes green, the client build +
|
|
153
|
+
Chromium + tailwind + sveltekit-plugin chain all work. **This is where the project-memory
|
|
154
|
+
"server-build" risk is actually disproven or confirmed** — treat a passing smoke test as the gate to
|
|
155
|
+
the rest of the plan.
|
|
156
|
+
|
|
157
|
+
## Open questions / decisions needed
|
|
158
|
+
|
|
159
|
+
- **Config location** — keep it in `vite.config.ts` (recommended, minimal change; chosen here) vs a
|
|
160
|
+
dedicated `vitest.config.ts`. Purely organizational; nothing functional depends on it.
|
|
161
|
+
- **`@sveltejs/kit/vite` vs plain `@sveltejs/vite-plugin-svelte` in the `client` project** — the
|
|
162
|
+
SvelteKit plugin should be fine in browser mode (the `sv` CLI's own vitest add-on uses it), but if
|
|
163
|
+
the smoke test surfaces SSR/`$lib` alias issues, the fallback is to give the `client` project a
|
|
164
|
+
plain `svelte()` plugin instead of inheriting `sveltekit()`. Decide only if the smoke test fails.
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
GENERATED ANALYSIS — @marianmeres/stuic real-browser component testing
|
|
3
|
+
Produced 2026-06-08 by multi-agent research → adversarial verify → synthesize.
|
|
4
|
+
Claims verified against the codebase at commit cc9958b and the live
|
|
5
|
+
vitest-browser-svelte docs. Planning artifact; no code was changed.
|
|
6
|
+
-->
|
|
7
|
+
|
|
8
|
+
# Test Conventions
|
|
9
|
+
|
|
10
|
+
> How to write a STUIC browser component test, and — just as important — **what is worth
|
|
11
|
+
> asserting**. The headline: test *behavior the build can't see* (events fire, bindings update,
|
|
12
|
+
> aria/disabled/active states, computed layout), not "does it render" (already gated by
|
|
13
|
+
> `svelte-check` + `publint` + the build). Use `render()` → locators → `expect.element`. In Svelte 5
|
|
14
|
+
> events are props, so you assert them with spies; snippet children come from `createRawSnippet`.
|
|
15
|
+
|
|
16
|
+
## Reconciling with `docs/testing.md`
|
|
17
|
+
|
|
18
|
+
[`docs/testing.md`](../testing.md) currently states the library **deliberately does not** test full
|
|
19
|
+
component rendering ("50+ components × prop combinations = slow suite with tiny yield... Rendering is
|
|
20
|
+
already gated by svelte-check + publint + the build") and treats interactive/visual behavior as
|
|
21
|
+
out of scope.
|
|
22
|
+
|
|
23
|
+
That reasoning was **correct for what it described and is not actually reversed here** — it just
|
|
24
|
+
predates a capability we didn't have:
|
|
25
|
+
|
|
26
|
+
- "Does it render / compile / export" → still low-yield, still covered by `svelte-check` + `publint` +
|
|
27
|
+
build. We will **not** write tests for that.
|
|
28
|
+
- "Does it *behave*" — click handlers, two-way `bind:`, `aria-*`/`disabled`/`active` state, focus
|
|
29
|
+
traps, viewport-clamped anchor positioning (cf. the recent `9d8c974` annotation-clamp fix) — was
|
|
30
|
+
**previously impossible** (node/server build, no DOM, no `$effect`). Browser mode makes it possible,
|
|
31
|
+
and *this* is the high-yield target.
|
|
32
|
+
|
|
33
|
+
**Task in the roadmap:** update `docs/testing.md` to add this browser-test layer so the docs aren't
|
|
34
|
+
self-contradictory — promote "interactive behavior" from ❌ to ✅-when-it's-a-real-contract, and point
|
|
35
|
+
to this directory. (See [`PROGRESS.md`](./PROGRESS.md), sprint task.)
|
|
36
|
+
|
|
37
|
+
## File naming & location
|
|
38
|
+
|
|
39
|
+
- One test per component, **co-located** next to the `.svelte` file (matches the existing co-located
|
|
40
|
+
style, e.g. `Input/phone-validation.test.ts`).
|
|
41
|
+
- Name it `ComponentName.svelte.test.ts` — the `.svelte.test.ts` suffix is what routes it into the
|
|
42
|
+
browser `client` project (see [01](./01-framework-setup.md)). A plain `*.test.ts` next to a
|
|
43
|
+
component stays in the fast node project (correct for extracted pure logic like `_internal/*.ts`).
|
|
44
|
+
|
|
45
|
+
## The canonical test
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
// src/lib/components/Pill/Pill.svelte.test.ts
|
|
49
|
+
import { render } from "vitest-browser-svelte";
|
|
50
|
+
import { expect, test, vi } from "vitest";
|
|
51
|
+
import Pill from "./Pill.svelte";
|
|
52
|
+
|
|
53
|
+
test("renders label and applies intent", async () => {
|
|
54
|
+
const screen = render(Pill, { label: "Beta", intent: "primary" });
|
|
55
|
+
const pill = screen.getByText("Beta");
|
|
56
|
+
await expect.element(pill).toBeVisible();
|
|
57
|
+
await expect.element(pill).toHaveAttribute("data-intent", "primary");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("dismiss button fires ondismiss", async () => {
|
|
61
|
+
const ondismiss = vi.fn();
|
|
62
|
+
const screen = render(Pill, { label: "Beta", ondismiss });
|
|
63
|
+
await screen.getByRole("button", { name: /dismiss/i }).click();
|
|
64
|
+
expect(ondismiss).toHaveBeenCalledOnce();
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Key facts (verified against the `vitest-browser-svelte` README):
|
|
69
|
+
|
|
70
|
+
- `render(Component, props)` returns a **screen** object with Playwright-style locator methods
|
|
71
|
+
(`getByRole`, `getByText`, `getByLabelText`, …). `render` is imported from `"vitest-browser-svelte"`.
|
|
72
|
+
- **`expect.element(locator)` auto-retries** — it polls the DOM until the assertion passes or times
|
|
73
|
+
out (`testTimeout`, set to 2000ms in our config). Prefer it over reading values synchronously; it
|
|
74
|
+
removes the need for `act`/manual `tick()` for in-component reactivity.
|
|
75
|
+
- `.click()`, `.fill()`, etc. are awaited interactions on a locator.
|
|
76
|
+
- **No `@testing-library`, no `act`.** `vitest-browser-svelte` intentionally omits `act`.
|
|
77
|
+
|
|
78
|
+
## Events are props (Svelte 5)
|
|
79
|
+
|
|
80
|
+
There are no `component.$on` listeners in Svelte 5 — events are callback props (`onclick`,
|
|
81
|
+
`onchange`, and STUIC's own `ondismiss`, `onLogout`, etc.). Assert them by passing a spy:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
const onclick = vi.fn();
|
|
85
|
+
render(Button, { onclick, children: text("Save") });
|
|
86
|
+
await screen.getByRole("button").click();
|
|
87
|
+
expect(onclick).toHaveBeenCalledOnce();
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Snippet props (`children`, `header`, etc.)
|
|
91
|
+
|
|
92
|
+
Many STUIC components take snippet props (`children`, header/footer snippets). A `.svelte.test.ts`
|
|
93
|
+
file can't contain markup, so build snippets with `createRawSnippet`:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
import { createRawSnippet } from "svelte";
|
|
97
|
+
|
|
98
|
+
// tiny helper for static text children — keep it in a shared test util
|
|
99
|
+
const text = (s: string) => createRawSnippet(() => ({ render: () => `<span>${s}</span>` }));
|
|
100
|
+
|
|
101
|
+
render(Button, { children: text("Click me") });
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
For complex snippet scenarios (a snippet that itself renders a STUIC component, takes args, etc.),
|
|
105
|
+
the escape hatch is a **fixture component**: a small `ComponentName.fixture.svelte` next to the test
|
|
106
|
+
that composes the real component with markup, then `render(Fixture, props)`. Prefer `createRawSnippet`
|
|
107
|
+
for the common case; reach for a fixture only when markup composition is the thing under test.
|
|
108
|
+
|
|
109
|
+
## Two-way binding (`bind:value`, `bind:checked`)
|
|
110
|
+
|
|
111
|
+
To assert that user interaction updates bound state, render a fixture that binds the prop to local
|
|
112
|
+
state and exposes it, **or** assert the observable DOM proxy (e.g. the hidden `<input>`'s `checked`
|
|
113
|
+
for `Switch`, the `<input>`'s `value` for `FieldInput`). Default to asserting the DOM — it's what a
|
|
114
|
+
consumer observes — and use a fixture only when you must read the bound JS value directly.
|
|
115
|
+
|
|
116
|
+
## What to assert (the high-yield checklist)
|
|
117
|
+
|
|
118
|
+
For each component, prefer these over snapshot dumps:
|
|
119
|
+
|
|
120
|
+
1. **Prop → DOM contract** — `intent`/`variant`/`size` → `data-*` attribute or class; `href` → renders
|
|
121
|
+
`<a>` vs `<button>`; `disabled` → the `disabled` attribute / blocked clicks.
|
|
122
|
+
2. **Events** — the right callback fires once, with the right argument, and `disabled` suppresses it.
|
|
123
|
+
3. **Binding** — interaction updates the value/checked state.
|
|
124
|
+
4. **A11y** — `role`, `aria-*`, label association, focus order where it's a contract.
|
|
125
|
+
5. **Computed layout** (browser-only superpower) — width from a `progress` value, viewport clamping
|
|
126
|
+
of anchor-positioned elements, focus actually trapped. These are exactly what jsdom returns zeros
|
|
127
|
+
for, and what regressed in recent commits.
|
|
128
|
+
|
|
129
|
+
Avoid: asserting exact class strings (brittle — assert the `data-*` contract or a single meaningful
|
|
130
|
+
class), and snapshotting full HTML (self-closing-tag / class-order differences make them noisy).
|
|
131
|
+
|
|
132
|
+
## Cleanup gotcha
|
|
133
|
+
|
|
134
|
+
`vitest-browser-svelte` cleans up **before** each test, not after — so the last rendered component
|
|
135
|
+
stays on screen for debugging. Import from `vitest-browser-svelte/pure` to opt out of auto-cleanup
|
|
136
|
+
when a test needs to manage it manually. Rarely needed.
|
|
137
|
+
|
|
138
|
+
## Timing / flakiness
|
|
139
|
+
|
|
140
|
+
Rely on `expect.element` retries and awaited locator actions — **never fixed `sleep`s**. In-component
|
|
141
|
+
reactivity resolves through the retry loop; only assertions on *external* universal state living in a
|
|
142
|
+
`*.svelte.ts` module may need `flushSync()` from `svelte`.
|
|
143
|
+
|
|
144
|
+
## Open questions / decisions needed
|
|
145
|
+
|
|
146
|
+
- **Shared test utilities** — agree on one home for the `text()` snippet helper and any future
|
|
147
|
+
fixtures (suggest `src/lib/test-utils/` or `src/test-helpers.ts`), so it's not redefined per file.
|
|
148
|
+
Decide when the second snippet-needing component lands.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
GENERATED ANALYSIS — @marianmeres/stuic real-browser component testing
|
|
3
|
+
Produced 2026-06-08 by multi-agent research → adversarial verify → synthesize.
|
|
4
|
+
Component inventory verified against src/lib/components at commit cc9958b.
|
|
5
|
+
Planning artifact; no code was changed.
|
|
6
|
+
-->
|
|
7
|
+
|
|
8
|
+
# Component Coverage Roadmap
|
|
9
|
+
|
|
10
|
+
> The library has **74 components** (≈105 `.svelte` files incl. sub-components). Roughly **26 are
|
|
11
|
+
> "easy"** (deterministic prop→DOM, no portals/traps), **23 "medium"** (actions, focus jumps, layout
|
|
12
|
+
> reads), **30 "hard/E2E-only"** (portals, focus traps, anchor positioning, drag, Milkdown). The plan:
|
|
13
|
+
> **one commit per component**, starting with the easy primitives that are used everywhere, then a
|
|
14
|
+
> single "hard proof" to validate that browser mode earns its keep (see [04](./04-hard-cases-and-e2e.md)).
|
|
15
|
+
|
|
16
|
+
## Summary of recommendations
|
|
17
|
+
|
|
18
|
+
| # | Recommendation | Value | Effort | Risk |
|
|
19
|
+
|---|----------------|-------|--------|------|
|
|
20
|
+
| 1 | Cover the **easy tier** primitives first (Button, Pill, Switch, …), one commit each | high | S | low |
|
|
21
|
+
| 2 | Establish reusable assertion patterns on Button (the flagship), reuse everywhere | high | S | low |
|
|
22
|
+
| 3 | Do **one hard proof** early (focus trap or anchor positioning) to de-risk the approach | high | M | med |
|
|
23
|
+
| 4 | Cover the **medium tier** (form fields with actions) once patterns are settled | med | M | low |
|
|
24
|
+
| 5 | Defer the **hard 30** to a later sprint / standalone Playwright E2E layer | med | L | med |
|
|
25
|
+
|
|
26
|
+
## Tier 1 — Easy, highest-value (the warm-up; one commit each)
|
|
27
|
+
|
|
28
|
+
Deterministic prop→DOM/aria/event surface; no portals, traps, or external geometry. Paths under
|
|
29
|
+
`src/lib/components/`.
|
|
30
|
+
|
|
31
|
+
| Order | Component | Path | What to assert |
|
|
32
|
+
|-------|-----------|------|----------------|
|
|
33
|
+
| 1 | **Button** | `Button/Button.svelte` | `intent`/`variant`/`size` → `data-*`; `disabled` attr + click suppressed; `href` → `<a>`; spinner shows/hides; `roleSwitch` toggle; `onclick` fires once. The flagship — sets every pattern. |
|
|
34
|
+
| 2 | **Pill** | `Pill/Pill.svelte` | intent/variant → `data-*`; `active` state; dismissible click → `ondismiss` (+ `stopPropagation`); `href` → link; disabled blocks click. |
|
|
35
|
+
| 3 | **Switch** | `Switch/Switch.svelte` | `checked` reflected in hidden input; click toggles; `disabled`; intent/size classes; on/off snippets. First real "reactivity in a browser" proof. |
|
|
36
|
+
| 4 | **Spinner** | `Spinner/Spinner.svelte` | size → dims; `count` → number of bars; thickness/direction/duration → style/CSS var. Pure CSS, no motion lib. |
|
|
37
|
+
| 5 | **Skeleton** | `Skeleton/Skeleton.svelte` | variant (text/circle/rect) markup; `lines` count; rounded class; animation off under `prefers-reduced-motion`; width/height inline style. |
|
|
38
|
+
| 6 | **DismissibleMessage** | `DismissibleMessage/DismissibleMessage.svelte` | message renders; `intent` → `data-intent`; dismiss button hidden when `onDismiss=false`; `ondismiss` fires; new message resets dismissed state. |
|
|
39
|
+
| 7 | **Avatar** | `Avatar/Avatar.svelte` | `src` → `<img>` + alt; initials extracted from name; icon fallback; error→fallback switch; `autoColor` deterministic; size class. |
|
|
40
|
+
| 8 | **Progress** | `Progress/Progress.svelte` | `type=bar` vs `circle` markup; value → bar **width** / circle stroke (genuine layout read — jsdom can't). |
|
|
41
|
+
| 9 | **Separator** | `Separator/Separator.svelte` | orientation → classes/role. Near-zero risk — good harness smoke test (see [01](./01-framework-setup.md) Step 5). |
|
|
42
|
+
| 10 | **H** | `H/H.svelte` | `level` → correct `h1`–`h6` tag; size override class. |
|
|
43
|
+
| 11 | **KbdShortcut** | `KbdShortcut/KbdShortcut.svelte` | key labels rendered; modifier order; cross-platform output. |
|
|
44
|
+
| 12 | **ButtonGroupRadio** | `ButtonGroupRadio/ButtonGroupRadio.svelte` | single selection; click changes selection; value binding. |
|
|
45
|
+
| 13 | **ListItemButton** | `ListItemButton/ListItemButton.svelte` | active class; disabled blocks click; `onclick`; icon/avatar slot. |
|
|
46
|
+
| 14 | **Card** | `Card/Card.svelte` | variant → `data-*`; header/footer/children snippets render. |
|
|
47
|
+
| 15 | **TabbedMenu** | `TabbedMenu/TabbedMenu.svelte` | tab click updates bound `activeTab`; active content renders; indicator moves. |
|
|
48
|
+
| 16 | **IconSwap** | `IconSwap/IconSwap.svelte` | `active` swaps icon; animation class applied. |
|
|
49
|
+
| 17 | **Collapsible** | `Collapsible/Collapsible.svelte` | `needsCollapse` true when `scrollHeight > clientHeight` (browser-only); expand removes line-clamp; toggle visible only when needed. |
|
|
50
|
+
|
|
51
|
+
> The first sprint targets **#1–#9 plus one hard proof**; #10–#17 are ranked backlog (still Tier 1,
|
|
52
|
+
> just diminishing marginal value). Adjust freely as patterns settle.
|
|
53
|
+
|
|
54
|
+
## Tier 2 — Medium value, moderate complexity (next sprint)
|
|
55
|
+
|
|
56
|
+
Use actions (`trim`/`typeahead`/`validate`/`autogrow`/`tooltip`) and/or real focus/geometry, but stay
|
|
57
|
+
deterministic. Form fields are the bulk and share one harness pattern once `FieldInput` is solved.
|
|
58
|
+
|
|
59
|
+
`FieldInput`, `FieldTextarea`, `FieldCheckbox`, `FieldSelect`, `FieldRadios`, `FieldSwitch`,
|
|
60
|
+
`FieldKeyValues`, `FieldObject`, `FieldPhoneNumber`, `FieldLikeButton` · `OtpInput` (focus jumps,
|
|
61
|
+
paste) · `Nav` (expand/collapse) · `ImageCycler` · `AppShell`/`AppShellSimple` · `PricingTable` ·
|
|
62
|
+
`ThemePreview` · `AssetsPreview` · `SlidingPanels` · `Notifications` (timers; portal — borderline) ·
|
|
63
|
+
`TypeaheadInput` · `ColorScheme` (localStorage).
|
|
64
|
+
|
|
65
|
+
Pattern note: for the `validate` action, inject a stub validator (`(k) => k` style, as existing util
|
|
66
|
+
tests do) rather than exercising live validation — keeps these fast and deterministic.
|
|
67
|
+
|
|
68
|
+
## Tier 3 — Hard / E2E-only (later; see [04](./04-hard-cases-and-e2e.md))
|
|
69
|
+
|
|
70
|
+
Portals & focus traps (`Modal`, `ModalDialog`, `Backdrop`, `Drawer`, `AlertConfirmPrompt`), anchor
|
|
71
|
+
positioning (`DropdownMenu`, `CommandMenu`, `UserAvatarMenu`), drag (`Tree`, `FieldOptions`,
|
|
72
|
+
`FieldFile`, `FieldAssets`), scroll/observer (`Carousel`, `DataTable`, `Book`), heavy 3rd-party
|
|
73
|
+
(`MarkdownEditor`/Milkdown, `CronInput`), and the composite flows (`Checkout/*`, `LoginForm`,
|
|
74
|
+
`RegisterForm`, `EmailVerifyForm`, `LoginOrRegisterForm`, `FieldInputLocalized`).
|
|
75
|
+
|
|
76
|
+
Several of these *can* be done in browser mode (focus traps, anchor positioning, observers all work
|
|
77
|
+
in a real Chromium); only drag-heavy and Milkdown components genuinely want standalone Playwright
|
|
78
|
+
E2E. [04](./04-hard-cases-and-e2e.md) draws that line.
|
|
79
|
+
|
|
80
|
+
## Cadence
|
|
81
|
+
|
|
82
|
+
- **One component per commit**, message like `test(Button): browser-mode coverage`.
|
|
83
|
+
- Each commit: write `ComponentName.svelte.test.ts`, run `pnpm test` (both projects green), tick the
|
|
84
|
+
row in [`PROGRESS.md`](./PROGRESS.md), commit.
|
|
85
|
+
- Don't chase coverage %. Stop at the behavior contracts in [02](./02-test-conventions.md)'s checklist.
|
|
86
|
+
|
|
87
|
+
## Open questions / decisions needed
|
|
88
|
+
|
|
89
|
+
- **Depth per component** — assert the headline contracts (recommended) vs exhaustive prop-matrix.
|
|
90
|
+
Default: headline contracts; expand only where a real regression happened.
|
|
91
|
+
- **Where to stop Tier 1 before pausing for the hard proof** — plan assumes after #9; confirm or
|
|
92
|
+
extend.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
GENERATED ANALYSIS — @marianmeres/stuic real-browser component testing
|
|
3
|
+
Produced 2026-06-08 by multi-agent research → adversarial verify → synthesize.
|
|
4
|
+
Claims verified against src/lib at commit cc9958b. Planning artifact; no code was changed.
|
|
5
|
+
-->
|
|
6
|
+
|
|
7
|
+
# Hard Cases & E2E Strategy
|
|
8
|
+
|
|
9
|
+
> ~30 of the 74 components are "hard". But "hard" splits two ways: **most are hard for *jsdom* yet
|
|
10
|
+
> perfectly testable in Vitest browser mode** (focus traps, anchor positioning, ResizeObserver — all
|
|
11
|
+
> work in a real Chromium); only **drag-heavy and Milkdown-class** components genuinely need a
|
|
12
|
+
> separate Playwright **E2E** layer against a running app. The single most valuable near-term action
|
|
13
|
+
> is **one "hard proof"** test now — pick the thing that recently regressed — to prove browser mode
|
|
14
|
+
> earns its setup cost before we invest in the easy tier en masse.
|
|
15
|
+
|
|
16
|
+
## The two kinds of "hard"
|
|
17
|
+
|
|
18
|
+
| Kind | Examples | Where it's tested |
|
|
19
|
+
|------|----------|-------------------|
|
|
20
|
+
| **Hard for jsdom, fine in browser mode** | focus traps, anchor/viewport positioning, scroll/observer tracking, transitions, computed layout | **Vitest browser mode** (this plan) — a real Chromium page gives real `getBoundingClientRect`, focus, layout |
|
|
21
|
+
| **Hard even in browser mode** | drag-and-drop reorder, file drop, full WYSIWYG editors, multi-step composite flows | **Standalone Playwright E2E** against `pnpm dev`/`preview` (deferred to a later sprint) |
|
|
22
|
+
|
|
23
|
+
This matters because the naive read ("30 components are E2E-only") is wrong — it would defer exactly
|
|
24
|
+
the high-value behaviors (focus, positioning) that motivated this whole effort. Most of the 30 belong
|
|
25
|
+
in browser mode; only a handful are true E2E.
|
|
26
|
+
|
|
27
|
+
## The "hard proof" — do this once, early
|
|
28
|
+
|
|
29
|
+
Per the agreed scope (easy warm-up → one hard proof), after the first easy components land, write
|
|
30
|
+
**one** test that is impossible in jsdom and protects a real, recent regression. Two strong
|
|
31
|
+
candidates, both verified to exist:
|
|
32
|
+
|
|
33
|
+
### Candidate A (recommended) — anchor-position viewport clamping
|
|
34
|
+
|
|
35
|
+
- **Code:** [`src/lib/utils/anchor-position.ts`](../../src/lib/utils/anchor-position.ts), consumed by
|
|
36
|
+
`DropdownMenu/DropdownMenu.svelte` (and others).
|
|
37
|
+
- **Why:** this is precisely what regressed in `9d8c974` *"clamp anchor-positioned annotations to
|
|
38
|
+
viewport on all paths"* and `8c52afe`. A test here has immediate, proven value and prevents
|
|
39
|
+
recurrence. It's also the textbook "jsdom returns all-zeros from `getBoundingClientRect`" case — so
|
|
40
|
+
it can only exist in browser mode.
|
|
41
|
+
- **Shape:** render a host + an anchored element positioned near a viewport edge in a real page;
|
|
42
|
+
assert the element's rect stays clamped inside `window.innerWidth/innerHeight`.
|
|
43
|
+
- **Bonus:** the *pure* clamp math in `anchor-position.ts` can additionally get a fast **node** test
|
|
44
|
+
(`anchor-position.test.ts`) — cheap regression net independent of the browser.
|
|
45
|
+
|
|
46
|
+
### Candidate B (alternative) — focus trap
|
|
47
|
+
|
|
48
|
+
- **Code:** [`src/lib/actions/focus-trap.ts`](../../src/lib/actions/focus-trap.ts), used by
|
|
49
|
+
`ModalDialog`, `Backdrop`, `Drawer`.
|
|
50
|
+
- **Why:** Tab/Shift-Tab cycling and `returnFocus`-on-teardown are core a11y contracts that node
|
|
51
|
+
cannot exercise at all.
|
|
52
|
+
- **Shape:** render a container with the action + 3 focusables; assert Tab from the last wraps to the
|
|
53
|
+
first, Shift-Tab from the first wraps to the last, and focus returns to the opener on destroy.
|
|
54
|
+
|
|
55
|
+
> **Open question (resolve at this task):** A or B as the single hard proof. Recommendation: **A** —
|
|
56
|
+
> it guards a regression that already happened twice. Doing both is fine but only one is required to
|
|
57
|
+
> de-risk the approach.
|
|
58
|
+
|
|
59
|
+
## Portals & focus traps (browser-mode, later sprint)
|
|
60
|
+
|
|
61
|
+
`Modal`, `ModalDialog`, `Backdrop`, `Drawer`, `AlertConfirmPrompt` — all use
|
|
62
|
+
[`focus-trap.ts`](../../src/lib/actions/focus-trap.ts), scroll-lock, and an Escape-key stack. These
|
|
63
|
+
are testable in browser mode (open → focus trapped → Escape closes → backdrop click closes →
|
|
64
|
+
`returnFocus`). The stack/queue *logic* is already unit-tested (`AlertConfirmPromptStack`,
|
|
65
|
+
`NotificationsStack`); browser tests add the DOM-interaction layer. Higher effort, real value —
|
|
66
|
+
schedule after Tier 1/2.
|
|
67
|
+
|
|
68
|
+
## Anchor positioning (browser-mode, later sprint)
|
|
69
|
+
|
|
70
|
+
`DropdownMenu`, `CommandMenu`, `UserAvatarMenu` — positioning + keyboard nav + click-outside. Test
|
|
71
|
+
positioning and keyboard nav in browser mode; the search/filter logic should be extracted to
|
|
72
|
+
`_internal/*.ts` and unit-tested in node where practical.
|
|
73
|
+
|
|
74
|
+
## True E2E candidates (standalone Playwright, deferred)
|
|
75
|
+
|
|
76
|
+
These want a running app, not isolated component mounts:
|
|
77
|
+
|
|
78
|
+
- **Drag & file drop:** `Tree` (reorder + auto-expand + async `onMove`), `FieldOptions`, `FieldFile`,
|
|
79
|
+
`FieldAssets`. Extract and node-test the pure logic (`calcDropPosition`, `isDescendantOf` per
|
|
80
|
+
`docs/testing.md`); E2E the gesture.
|
|
81
|
+
- **Heavy editors:** `MarkdownEditor` (Milkdown + CodeMirror) — E2E only; mock/skip at component
|
|
82
|
+
level. `CronInput` — node-test the cron logic, E2E the builder UI.
|
|
83
|
+
- **Composite flows:** `Checkout/*`, `LoginForm`/`RegisterForm`/`EmailVerifyForm`/
|
|
84
|
+
`LoginOrRegisterForm`, `FieldInputLocalized` — multi-step, cross-component; E2E the flow, browser-
|
|
85
|
+
test the leaf fields individually.
|
|
86
|
+
|
|
87
|
+
A standalone Playwright E2E project (its own `playwright.config.ts` against `pnpm preview`) is a
|
|
88
|
+
**separate later initiative** — explicitly out of scope for the first sprint. Visual-regression
|
|
89
|
+
(Vitest 4 ships `toMatchScreenshot`) is likewise deferred.
|
|
90
|
+
|
|
91
|
+
## Open questions / decisions needed
|
|
92
|
+
|
|
93
|
+
- **Hard proof A vs B** (above) — recommend A.
|
|
94
|
+
- **When to start the standalone Playwright E2E layer** — not in sprint 1; revisit after Tier 2.
|
|
95
|
+
- **Logic-extraction appetite** — several hard components get much cheaper if pure logic moves to
|
|
96
|
+
`_internal/*.ts` (already endorsed by `docs/testing.md`). Decide per-component whether to extract
|
|
97
|
+
before testing.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
GENERATED ANALYSIS — @marianmeres/stuic real-browser component testing
|
|
3
|
+
Produced 2026-06-08 by multi-agent research → adversarial verify → synthesize.
|
|
4
|
+
Claims verified against the repo at commit cc9958b. Planning artifact; no code was changed.
|
|
5
|
+
-->
|
|
6
|
+
|
|
7
|
+
# CI — GitHub Actions
|
|
8
|
+
|
|
9
|
+
> The repo is on GitHub (`github.com/marianmeres/stuic`) and publishes to npm, so CI pays off: it
|
|
10
|
+
> runs the tests automatically on every push/PR in a clean machine, catching regressions **before**
|
|
11
|
+
> a broken release reaches npm. The plan: **one ~30-line workflow** that installs Chromium and runs
|
|
12
|
+
> `pnpm test`. It's fully decoupled from the tests themselves (those run identically via `pnpm test`
|
|
13
|
+
> locally) and is scheduled **late in the first sprint**, once a few component tests pass locally, so
|
|
14
|
+
> the first CI run verifies something real.
|
|
15
|
+
|
|
16
|
+
## What CI is, briefly
|
|
17
|
+
|
|
18
|
+
A robot that runs your checks on every push. **GitHub Actions** is GitHub's built-in version: you
|
|
19
|
+
commit a YAML file at `.github/workflows/test.yml` describing *when* to run (push / pull_request) and
|
|
20
|
+
*what steps* to run on a fresh Linux VM. GitHub then shows a green ✓ / red ✗ on each commit and PR.
|
|
21
|
+
Free for this repo's usage.
|
|
22
|
+
|
|
23
|
+
## The one wrinkle vs a normal Node test job
|
|
24
|
+
|
|
25
|
+
Browser tests need a real Chromium binary on the CI machine, so the workflow adds one step —
|
|
26
|
+
`pnpm exec playwright install --with-deps chromium` — before `pnpm test`. (`--with-deps` also
|
|
27
|
+
installs the OS libraries Chromium needs on the Ubuntu runner.) Everything else is a standard
|
|
28
|
+
pnpm + Node setup.
|
|
29
|
+
|
|
30
|
+
## The workflow (to create as the sprint's CI task)
|
|
31
|
+
|
|
32
|
+
```yaml
|
|
33
|
+
# .github/workflows/test.yml
|
|
34
|
+
name: test
|
|
35
|
+
|
|
36
|
+
on:
|
|
37
|
+
push:
|
|
38
|
+
branches: [master]
|
|
39
|
+
pull_request:
|
|
40
|
+
|
|
41
|
+
jobs:
|
|
42
|
+
test:
|
|
43
|
+
runs-on: ubuntu-latest
|
|
44
|
+
steps:
|
|
45
|
+
- uses: actions/checkout@v4
|
|
46
|
+
|
|
47
|
+
- uses: pnpm/action-setup@v4
|
|
48
|
+
|
|
49
|
+
- uses: actions/setup-node@v4
|
|
50
|
+
with:
|
|
51
|
+
node-version: 22
|
|
52
|
+
cache: pnpm
|
|
53
|
+
|
|
54
|
+
- run: pnpm install --frozen-lockfile
|
|
55
|
+
|
|
56
|
+
- name: Install Chromium for browser tests
|
|
57
|
+
run: pnpm exec playwright install --with-deps chromium
|
|
58
|
+
|
|
59
|
+
- name: Run tests (node + browser projects)
|
|
60
|
+
run: pnpm test # = "vitest run", runs both projects headless
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Notes:
|
|
64
|
+
|
|
65
|
+
- `pnpm test` must be the one-shot `vitest run` (see [01](./01-framework-setup.md) Step 4), not watch
|
|
66
|
+
mode, or CI hangs.
|
|
67
|
+
- `browser.headless: true` is already set in the config, so no display server is needed.
|
|
68
|
+
- Pin `node-version` to whatever you develop on (22 shown). `pnpm/action-setup@v4` reads the pnpm
|
|
69
|
+
version from `package.json`'s `packageManager` field if present — the repo currently has none, so
|
|
70
|
+
either add one (e.g. `"packageManager": "pnpm@9.x"`) or pin `version:` in the action. **Decide this
|
|
71
|
+
when creating the workflow.**
|
|
72
|
+
|
|
73
|
+
## Alternative considered (not chosen)
|
|
74
|
+
|
|
75
|
+
Running the job inside the official Playwright Docker image
|
|
76
|
+
(`container: mcr.microsoft.com/playwright:v1.xx-jammy`) ships browsers pre-installed and is ~30%
|
|
77
|
+
faster per published benchmarks. For a library this size the absolute difference is seconds; the
|
|
78
|
+
plain workflow above is more readable, so it's the chosen default. The Docker route stays available
|
|
79
|
+
if CI time ever matters.
|
|
80
|
+
|
|
81
|
+
## Open questions / decisions needed
|
|
82
|
+
|
|
83
|
+
- **`packageManager` field vs pinned pnpm version in the action** — pick one so `pnpm/action-setup`
|
|
84
|
+
is deterministic.
|
|
85
|
+
- **Node version** to pin (suggest 22).
|
|
86
|
+
- **Add `pnpm check` / `pnpm lint` to the same workflow?** — cheap to fold in (the repo already has
|
|
87
|
+
both scripts) and would make CI a full quality gate, but it widens scope beyond testing. Recommend
|
|
88
|
+
yes, as a second job, but only after the test job is green.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Implementation Progress — Component Testing
|
|
2
|
+
|
|
3
|
+
Living tracker for acting on [`00-overview-and-roadmap.md`](./00-overview-and-roadmap.md).
|
|
4
|
+
A fresh conversation should read this file first, then the relevant `NN-*.md` section.
|
|
5
|
+
|
|
6
|
+
**Status legend:** ⬜ not started · 🚧 in progress · ⏸️ blocked/awaiting decision · ✅ done · ⏭️ deferred
|
|
7
|
+
|
|
8
|
+
> Convention: branch `feat/component-testing`; **one commit per task**. Each task resolves its source
|
|
9
|
+
> doc's "Open questions" first (record in the Decisions log), then implement → `pnpm test` green →
|
|
10
|
+
> tick here → commit.
|
|
11
|
+
|
|
12
|
+
## First sprint (harness + easy tier + one hard proof + CI)
|
|
13
|
+
|
|
14
|
+
Branch: `feat/component-testing`
|
|
15
|
+
|
|
16
|
+
| # | Task | Source | Status | Commit |
|
|
17
|
+
|---|------|--------|--------|--------|
|
|
18
|
+
| 1 | Upgrade vitest 3→4; confirm 9 existing node suites still green | [01](./01-framework-setup.md) Step 1 | ✅ | `71e47e2` |
|
|
19
|
+
| 2 | Browser harness: add deps, `projects` config split, `playwright install chromium`, fix test scripts, Separator smoke test | [01](./01-framework-setup.md) Steps 2–5 | ✅ | `980b323` |
|
|
20
|
+
| 3 | Reconcile [`docs/testing.md`](../testing.md) — add the browser-behavior layer | [02](./02-test-conventions.md) | ✅ | _next_ |
|
|
21
|
+
| 4 | **Button** — flagship; establish assertion patterns | [03](./03-component-coverage-roadmap.md) #1 | ⬜ | — |
|
|
22
|
+
| 5 | **Pill** — intent/active/dismissible event | [03](./03-component-coverage-roadmap.md) #2 | ⬜ | — |
|
|
23
|
+
| 6 | **Switch** — checked binding, toggle, disabled | [03](./03-component-coverage-roadmap.md) #3 | ⬜ | — |
|
|
24
|
+
| 7 | **Spinner** — size/count/direction | [03](./03-component-coverage-roadmap.md) #4 | ⬜ | — |
|
|
25
|
+
| 8 | **Skeleton** — variants, reduced-motion | [03](./03-component-coverage-roadmap.md) #5 | ⬜ | — |
|
|
26
|
+
| 9 | **DismissibleMessage** — intent, dismiss, auto-reset | [03](./03-component-coverage-roadmap.md) #6 | ⬜ | — |
|
|
27
|
+
| 10 | **Avatar** — initials/img/icon fallback, autoColor | [03](./03-component-coverage-roadmap.md) #7 | ⬜ | — |
|
|
28
|
+
| 11 | **Progress** — value→width/stroke (real layout) | [03](./03-component-coverage-roadmap.md) #8 | ⬜ | — |
|
|
29
|
+
| 12 | **Hard proof** — anchor-position viewport clamp (rec.) *or* focus trap | [04](./04-hard-cases-and-e2e.md) | ⏸️ | — |
|
|
30
|
+
| 13 | CI — minimal GitHub Actions `test.yml` | [05](./05-ci.md) | ⬜ | — |
|
|
31
|
+
|
|
32
|
+
## Backlog (ranked, post-sprint)
|
|
33
|
+
|
|
34
|
+
| Rank | Task | Source | Status |
|
|
35
|
+
|------|------|--------|--------|
|
|
36
|
+
| 14 | Rest of Tier 1 (Separator already smoke-tested · H, KbdShortcut, ButtonGroupRadio, ListItemButton, Card, TabbedMenu, IconSwap, Collapsible) | [03](./03-component-coverage-roadmap.md) #10–17 | ⬜ |
|
|
37
|
+
| 15 | Tier 2 — `FieldInput` first, then the Field* family + OtpInput, Nav, etc. | [03](./03-component-coverage-roadmap.md) | ⬜ |
|
|
38
|
+
| 16 | Portals/focus-traps in browser mode (Modal, ModalDialog, Backdrop, Drawer, AlertConfirmPrompt) | [04](./04-hard-cases-and-e2e.md) | ⬜ |
|
|
39
|
+
| 17 | Anchor-positioned menus (DropdownMenu, CommandMenu, UserAvatarMenu) + extract search logic to `_internal` | [04](./04-hard-cases-and-e2e.md) | ⬜ |
|
|
40
|
+
| 18 | Standalone Playwright E2E layer (drag: Tree/FieldOptions/FieldFile; Milkdown; Checkout/auth flows) | [04](./04-hard-cases-and-e2e.md) | ⏭️ |
|
|
41
|
+
| 19 | Add `pnpm check` + `pnpm lint` as a second CI job | [05](./05-ci.md) | ⬜ |
|
|
42
|
+
| 20 | (Maybe) visual-regression via `toMatchScreenshot`; multi-browser matrix | [00](./00-overview-and-roadmap.md) | ⏭️ |
|
|
43
|
+
|
|
44
|
+
## Decisions log
|
|
45
|
+
|
|
46
|
+
- **2026-06-08** — Adopt **Vitest 4 Browser Mode + `vitest-browser-svelte` + `@vitest/browser-playwright` (Chromium)** — verified the right default for a component library whose value is DOM/layout/focus behavior the current node/server-build setup can't test.
|
|
47
|
+
- **2026-06-08** — **Take the vitest 3→4 major upgrade now** (gating prerequisite) — `vitest-browser-svelte@^2` peer-requires `vitest ^4`; the vitest-3-compatible `0.1.0` is a dead-end.
|
|
48
|
+
- **2026-06-08** — **Scope:** easy warm-up (Button/Pill/Switch/…) then **one** hard proof — not full coverage up front.
|
|
49
|
+
- **2026-06-08** — **Browsers: Chromium only** — leanest CI; add Firefox/WebKit only if engine-specific behavior demands it.
|
|
50
|
+
- **2026-06-08** — **CI: yes, minimal GitHub Actions**, added late in the sprint once a few component tests pass locally.
|
|
51
|
+
- **2026-06-08** — **Config lives in `vite.config.ts`** (extend with a `test` block) rather than a separate `vitest.config.ts` — minimal change.
|
|
52
|
+
- **2026-06-08** — Task 1 done: **vitest 4.1.8** installed (vite already `^7.3.5`, compatible); all 9 node suites / 59 tests pass unchanged. `--dir src/` still supported in v4.
|
|
53
|
+
- **2026-06-08** — Task 2 done: harness works. Deps: `@vitest/browser-playwright 4.1.8`, `playwright 1.60.0`, `vitest-browser-svelte 2.1.1`. `projects` split added to `vite.config.ts`; scripts now `test` = `vitest run` (+ `test:watch`, `test:ui`). Separator smoke test (3 assertions) passes in Chromium → **10 files / 62 tests green**, `pnpm check` clean (0 errors). **The documented SvelteKit-plugin/server-build blocker is resolved** — browser mode resolves the client build, `toHaveClass("stuic-separator")` confirms tailwind + the client runtime run. No `client`-project plugin fallback needed.
|
|
54
|
+
- **⏸️ Open (task 12):** which single hard proof — anchor-position clamp (recommended, guards the `9d8c974` regression) vs focus trap.
|
|
55
|
+
- **⏸️ Open (task 13):** `packageManager` field vs pinned pnpm version in the CI action; Node version to pin.
|
|
56
|
+
|
|
57
|
+
## How to resume (for a fresh conversation)
|
|
58
|
+
|
|
59
|
+
1. Read this file + [`00-overview-and-roadmap.md`](./00-overview-and-roadmap.md).
|
|
60
|
+
2. Pick the next ⬜ task; open its source doc section for the verified detail.
|
|
61
|
+
3. Resolve that task's "Open questions" with the owner; record in the Decisions log.
|
|
62
|
+
4. On `feat/component-testing`: implement → `pnpm test` (both projects green) → update this file →
|
|
63
|
+
commit when the owner asks (one commit per task).
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
GENERATED ANALYSIS — @marianmeres/stuic real-browser component testing
|
|
3
|
+
Produced 2026-06-08 by multi-agent research → adversarial verify → synthesize.
|
|
4
|
+
Claims verified against the codebase at commit cc9958b and the live Vitest 4 /
|
|
5
|
+
vitest-browser-svelte docs. Planning artifact; no code was changed.
|
|
6
|
+
-->
|
|
7
|
+
|
|
8
|
+
# Component Testing — @marianmeres/stuic
|
|
9
|
+
|
|
10
|
+
This directory holds the plan for introducing **real-browser component tests** to STUIC
|
|
11
|
+
(Vitest 4 Browser Mode + `vitest-browser-svelte` + Playwright/Chromium). It was produced
|
|
12
|
+
2026-06-08 from a research pass over the codebase and the current Svelte/Vitest ecosystem.
|
|
13
|
+
It is a **planning artifact — no code has been changed**; every claim is verified against the
|
|
14
|
+
repo at commit `cc9958b` or against the live docs cited in each section.
|
|
15
|
+
|
|
16
|
+
**Start here:** [`00-overview-and-roadmap.md`](./00-overview-and-roadmap.md). Then track and
|
|
17
|
+
resume execution from [`PROGRESS.md`](./PROGRESS.md).
|
|
18
|
+
|
|
19
|
+
## Documents
|
|
20
|
+
|
|
21
|
+
| # | Doc | Scope | Headline |
|
|
22
|
+
|---|-----|-------|----------|
|
|
23
|
+
| 00 | [overview-and-roadmap](./00-overview-and-roadmap.md) | synthesis + roadmap | The stack is the right default; vitest 3→4 upgrade is the gating prerequisite. |
|
|
24
|
+
| 01 | [framework-setup](./01-framework-setup.md) | infra | Upgrade vitest 4, add a `projects` split (node `server` + browser `client`), route by filename. |
|
|
25
|
+
| 02 | [test-conventions](./02-test-conventions.md) | how-to | `render()` + locators + `expect.element`; events are props (spies); snippets via `createRawSnippet`. |
|
|
26
|
+
| 03 | [component-coverage-roadmap](./03-component-coverage-roadmap.md) | what to cover | 74 components tiered; warm up on Button/Pill/Switch, one commit per component. |
|
|
27
|
+
| 04 | [hard-cases-and-e2e](./04-hard-cases-and-e2e.md) | the hard 30 | Portals/focus-traps/anchor-positioning: one "hard proof" now; standalone Playwright E2E deferred. |
|
|
28
|
+
| 05 | [ci](./05-ci.md) | automation | One ~30-line GitHub Actions workflow; install Chromium, run `pnpm test`. |
|
|
29
|
+
|
|
30
|
+
## How it was produced
|
|
31
|
+
|
|
32
|
+
Five parallel research agents (test-infra audit, full component inventory, multistep-format
|
|
33
|
+
extraction, two independent web-research angles on the stack) → synthesis → live-docs
|
|
34
|
+
verification of the exact Vitest 4 config syntax → this plan.
|
|
35
|
+
|
|
36
|
+
> Nothing here is decided beyond the four clarifying answers recorded in
|
|
37
|
+
> [`PROGRESS.md`](./PROGRESS.md) → Decisions log. Each doc's "Open questions / decisions needed"
|
|
38
|
+
> lists what still needs a call.
|
package/docs/testing.md
CHANGED
|
@@ -11,7 +11,12 @@ This is a component library. Most of its correctness guarantees come from:
|
|
|
11
11
|
3. **The build** — every component compiles, every export resolves.
|
|
12
12
|
4. **Manual/visual review** — styling, animation, keyboard interaction, a11y cues.
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
Tests are for what those tools can't see. There are **two layers**, split by filename:
|
|
15
|
+
|
|
16
|
+
- **`*.test.ts` — node, fast.** Pure deterministic logic where a regression silently corrupts data.
|
|
17
|
+
- **`*.svelte.test.ts` — real browser (Chromium).** Component _behavior_ the build can't see: events firing, two-way `bind:`, `aria`/`disabled`/`active` state, focus traps, computed layout/positioning.
|
|
18
|
+
|
|
19
|
+
We still explicitly don't try to test everything. The browser layer targets **behavior contracts**, not "does it render" — see [`component-testing/`](./component-testing/) for the strategy, roadmap, and how-to.
|
|
15
20
|
|
|
16
21
|
## What we test
|
|
17
22
|
|
|
@@ -20,6 +25,7 @@ Unit tests are for what those tools can't see: **pure deterministic logic where
|
|
|
20
25
|
- **Validation helpers** — `validateEmail`, `validateAddress`, `validateCustomerForm`, `validateLoginForm`, `validatePhoneNumber`, `addressesEqual`.
|
|
21
26
|
- **State-machine classes** — `NotificationsStack`, `AlertConfirmPromptStack`, `SwitchState`, `InputHistory`. Tri-state transitions, dedupe, ordering, cleanup semantics.
|
|
22
27
|
- **Pure utilities** — `replace-map`, `tr`, `storage-abstraction`, and anything else with non-trivial input/output logic.
|
|
28
|
+
- **Component behavior** (browser mode, `*.svelte.test.ts`) — prop→DOM/`aria` contracts, events firing, `bind:` updates, focus traps, viewport-clamped positioning. One component at a time, asserting contracts a consumer relies on — not every prop permutation.
|
|
23
29
|
|
|
24
30
|
### ⚠️ Maybe, if motivated by a regression
|
|
25
31
|
|
|
@@ -27,18 +33,23 @@ Unit tests are for what those tools can't see: **pure deterministic logic where
|
|
|
27
33
|
|
|
28
34
|
### ❌ We don't test
|
|
29
35
|
|
|
30
|
-
- **
|
|
31
|
-
- **Visual regression**.
|
|
32
|
-
- **
|
|
36
|
+
- **Exhaustive prop-matrix / "does it render" tests.** 50+ components × every prop combination = slow suite with tiny yield. Rendering is already gated by `svelte-check` + `publint` + the build; the browser layer asserts _behavior contracts_, not coverage of every permutation.
|
|
37
|
+
- **Visual regression**. A separate concern (Vitest's `toMatchScreenshot` / screenshot diffing) — deferred, not part of `pnpm test` today.
|
|
38
|
+
- **Heavy gestures & 3rd-party editors** (drag-drop reorder, file drop, Milkdown/CodeMirror) — deferred to a future standalone Playwright **E2E** layer, not the in-repo browser project. Extract and node-test their pure logic where practical.
|
|
33
39
|
- **Coverage % targets**. They're the wrong goal for a component library.
|
|
34
40
|
|
|
35
41
|
## Running tests
|
|
36
42
|
|
|
37
43
|
```bash
|
|
38
|
-
pnpm
|
|
44
|
+
pnpm test # both projects (node + browser), one-shot
|
|
45
|
+
pnpm test:watch # watch mode
|
|
46
|
+
pnpm test:ui # Vitest UI (handy for the browser project)
|
|
39
47
|
```
|
|
40
48
|
|
|
41
|
-
Vitest
|
|
49
|
+
Vitest runs **two projects**, routed by filename: a node project for `*.test.ts` and a real-browser
|
|
50
|
+
(Chromium, via Playwright) project for `*.svelte.test.ts`. Tests live next to the code they test:
|
|
51
|
+
`foo.ts` → `foo.test.ts`; `Foo.svelte` → `Foo.svelte.test.ts`. The browser binary is installed once
|
|
52
|
+
with `pnpm exec playwright install chromium`.
|
|
42
53
|
|
|
43
54
|
## Writing a test
|
|
44
55
|
|
|
@@ -65,8 +76,16 @@ import type { TranslateFn } from "$lib/types.js";
|
|
|
65
76
|
const t: TranslateFn = (k) => k;
|
|
66
77
|
```
|
|
67
78
|
|
|
79
|
+
For **component** tests (`*.svelte.test.ts`), the patterns differ — `render()` from
|
|
80
|
+
`vitest-browser-svelte`, locators, and the retry-able `expect.element`; events are props (assert with
|
|
81
|
+
spies); snippet children come from `createRawSnippet`. See
|
|
82
|
+
[`component-testing/02-test-conventions.md`](./component-testing/02-test-conventions.md) for the full
|
|
83
|
+
how-to and the "what to assert" checklist.
|
|
84
|
+
|
|
68
85
|
## When in doubt
|
|
69
86
|
|
|
70
|
-
- **Logic in a `.ts` file with clear input/output?** Write a test.
|
|
87
|
+
- **Logic in a `.ts` file with clear input/output?** Write a `*.test.ts`.
|
|
88
|
+
- **A component behavior that's a real contract** (event fires, value binds, focus traps, position
|
|
89
|
+
clamps)? Write a `*.svelte.test.ts`.
|
|
71
90
|
- **A regression just bit you in production?** Write a test for that specific case before fixing.
|
|
72
|
-
- **
|
|
91
|
+
- **"Does it render with these 12 props?"** Probably don't bother — the build already covers that.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marianmeres/stuic",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.115.0",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "vite dev",
|
|
6
6
|
"build": "vite build && pnpm run prepack",
|
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|
14
14
|
"format": "prettier --write .",
|
|
15
15
|
"lint": "eslint . && prettier --check .",
|
|
16
|
-
"test": "vitest
|
|
16
|
+
"test": "vitest run",
|
|
17
|
+
"test:watch": "vitest",
|
|
18
|
+
"test:ui": "vitest --ui",
|
|
17
19
|
"svelte-check": "svelte-check",
|
|
18
20
|
"svelte-package": "svelte-package",
|
|
19
21
|
"rp": "pnpm run build && ./release.sh patch",
|
|
@@ -145,9 +147,11 @@
|
|
|
145
147
|
"@tailwindcss/typography": "^0.5.19",
|
|
146
148
|
"@tailwindcss/vite": "^4.3.0",
|
|
147
149
|
"@types/node": "^25.9.2",
|
|
150
|
+
"@vitest/browser-playwright": "^4.1.8",
|
|
148
151
|
"dotenv": "^16.6.1",
|
|
149
152
|
"eslint": "^9.39.4",
|
|
150
153
|
"globals": "^16.5.0",
|
|
154
|
+
"playwright": "^1.60.0",
|
|
151
155
|
"prettier": "^3.8.3",
|
|
152
156
|
"prettier-plugin-svelte": "^3.5.2",
|
|
153
157
|
"publint": "^0.3.21",
|
|
@@ -158,7 +162,8 @@
|
|
|
158
162
|
"typescript": "^5.9.3",
|
|
159
163
|
"typescript-eslint": "^8.60.1",
|
|
160
164
|
"vite": "^7.3.5",
|
|
161
|
-
"vitest": "^
|
|
165
|
+
"vitest": "^4.1.8",
|
|
166
|
+
"vitest-browser-svelte": "^2.1.1"
|
|
162
167
|
},
|
|
163
168
|
"dependencies": {
|
|
164
169
|
"@marianmeres/clog": "^3.21.0",
|