@nativele/ts-dom 0.5.1 → 0.5.2
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/CHANGELOG.md +33 -0
- package/README.md +6 -1
- package/README.zh-TW.md +6 -1
- package/dist/cli.mjs +1 -1
- package/dist/index.mjs +3 -3
- package/dist/{lint-DfUAh7t3.mjs → lint-Bwytj7L8.mjs} +1 -1
- package/dist/oxlint-plugin.mjs +1 -1
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/skills/README.md +7 -4
- package/skills/nativele-integration/SKILL.md +92 -22
- package/skills/nativele-state-management/SKILL.md +319 -0
- package/skills/nativele-tailwind/SKILL.md +220 -0
- package/skills/nativele-tanstack-query/SKILL.md +302 -0
- package/skills/nativele-testing/SKILL.md +364 -0
- package/skills/nativele-zod/SKILL.md +345 -0
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nativele-testing
|
|
3
|
+
description: "Test Nativele components in an app: component tests with Vitest and happy-dom (or jsdom), real-browser tests with Vitest browser mode in headless Chrome, and page tests with Playwright. Covers generating the templates before tests, creating elements, props, events, lists, data-if, stores, slots, test isolation, and what a DOM shim cannot show. Use when a project depends on @nativele/ts-dom and on vitest, @playwright/test or playwright."
|
|
4
|
+
metadata:
|
|
5
|
+
tier: official
|
|
6
|
+
nativele: ">=0.5.2 <0.6.0"
|
|
7
|
+
verified: "2026-11-05"
|
|
8
|
+
tools: "vitest@5.1.0 happy-dom@21.0.4 jsdom@30.1.3 @vitest/browser-playwright@5.1.0 playwright@1.64.1 @playwright/test@1.64.1 vite@8.3.6 typescript@7.0.2"
|
|
9
|
+
author: "Nativele maintainers"
|
|
10
|
+
detects: "vitest @playwright/test playwright"
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Testing Nativele components
|
|
14
|
+
|
|
15
|
+
## When to use
|
|
16
|
+
|
|
17
|
+
- An app that already uses Nativele (`@nativele/ts-dom`) and needs tests for its components: rendering, props, events, lists, `data-if`, stores and slots.
|
|
18
|
+
- Signals: `@nativele/ts-dom` together with `vitest`, `@playwright/test` or `playwright` in `package.json`, or a request to add tests to a project with `.html` templates and `define()` calls.
|
|
19
|
+
- The starting point is a Vite app with Nativele set up as in the `nativele-vite` Skill (`npm create vite@latest -- --template vanilla-ts`, the `tsDom()` plugin in `vite.config.ts`, components in `src/nv/`). This Skill adds only what testing Nativele needs; it does not teach Vitest or Playwright.
|
|
20
|
+
- Not for writing the components (`npx ts-dom skills add nativele-core`), and not for testing the library itself.
|
|
21
|
+
|
|
22
|
+
The recommended path is Vitest: component tests in happy-dom, and the few behaviors a DOM shim gets wrong in Vitest browser mode on headless Chrome. The alternative is Playwright Test for tests of whole built pages.
|
|
23
|
+
|
|
24
|
+
## Versions
|
|
25
|
+
|
|
26
|
+
| Tool | Verified version |
|
|
27
|
+
|---|---|
|
|
28
|
+
| @nativele/ts-dom | 0.5.2 |
|
|
29
|
+
| vitest | 5.1.0 |
|
|
30
|
+
| happy-dom | 21.0.4 |
|
|
31
|
+
| jsdom | 30.1.3 (the same component tests pass; not the recommended environment) |
|
|
32
|
+
| @vitest/browser-playwright | 5.1.0 |
|
|
33
|
+
| playwright | 1.64.1 (used only to drive the installed Chrome; no browser is downloaded) |
|
|
34
|
+
| @playwright/test | 1.64.1 |
|
|
35
|
+
| vite | 8.3.6 |
|
|
36
|
+
| typescript | 7.0.2 |
|
|
37
|
+
|
|
38
|
+
Verified on 2026-11-05 with Node 24 and Google Chrome 148, starting from `create-vite` 9.3.4 with the `vanilla-ts` template, with the example project in the Nativele repository (`skills/nativele-testing/example`), which CI runs again when the example or the library changes and once a month. Vitest 5 changed its configuration (`test.api`, `test.projects`, providers as separate packages); with Vitest 4 or older, or another major version of any tool above, treat every step as unverified.
|
|
39
|
+
|
|
40
|
+
## Set-up
|
|
41
|
+
|
|
42
|
+
1. Install the test tools:
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
npm i -D vitest@5.1.0 happy-dom@21.0.4
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
2. Put the test configuration in the same `vite.config.ts` that has `tsDom()`, so Vitest loads the plugin. The plugin writes the generated `x.html.ts` files and `ts-dom-tags.ts` when Vitest starts, so `vitest run` works on a clean checkout with nothing generated yet. A separate `vitest.config.ts` replaces `vite.config.ts` for Vitest; if the project has one, add `tsDom({ dirs: ["src/nv"] })` to its `plugins` too (see Troubleshooting).
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
// vite.config.ts
|
|
52
|
+
import { defineConfig } from "vitest/config";
|
|
53
|
+
import { tsDom } from "@nativele/ts-dom/vite";
|
|
54
|
+
|
|
55
|
+
export default defineConfig({
|
|
56
|
+
plugins: [tsDom({ dirs: ["src/nv"] })],
|
|
57
|
+
test: {
|
|
58
|
+
projects: [
|
|
59
|
+
{ extends: true, test: { name: "unit", include: ["test/**/*.test.ts"], exclude: ["test/**/*.browser.test.ts"], environment: "happy-dom" } },
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
3. Keep the tests in `test/` and add it to `include` in `tsconfig.json` (`"include": ["src", "test"]`). Vitest does not type-check; `ts-dom check src/nv` runs the project's `tsc`, which then checks the tests too, and `ts-dom-tags.ts` types `document.createElement("counter-card")` as the component class, with its props.
|
|
66
|
+
4. Add the scripts:
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
npm pkg set scripts.test="vitest run --project unit"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Keep `ts-dom check src/nv` in the build script (as `nativele-vite` sets it up) or run it before or after the tests: it is what reports type errors in the tests and the templates.
|
|
73
|
+
|
|
74
|
+
5. For the real-browser tests, install the Vitest provider and Playwright, and add a second project. It drives the Chrome already installed on the machine (`CHROME_BIN`, or `/usr/bin/google-chrome`, which GitHub's `ubuntu-latest` runners have), so nothing is downloaded and `npx playwright install` is not needed:
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
npm i -D @vitest/browser-playwright@5.1.0 playwright@1.64.1
|
|
78
|
+
npm pkg set scripts.test:browser="vitest run --project browser"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
// vite.config.ts: the browser project, next to "unit" in test.projects
|
|
83
|
+
import { playwright } from "@vitest/browser-playwright";
|
|
84
|
+
|
|
85
|
+
const chrome = process.env.CHROME_BIN ?? "/usr/bin/google-chrome";
|
|
86
|
+
|
|
87
|
+
{
|
|
88
|
+
extends: true,
|
|
89
|
+
test: {
|
|
90
|
+
name: "browser",
|
|
91
|
+
include: ["test/**/*.browser.test.ts"],
|
|
92
|
+
api: { port: 63432, strictPort: true },
|
|
93
|
+
browser: {
|
|
94
|
+
enabled: true,
|
|
95
|
+
headless: true,
|
|
96
|
+
provider: playwright({ launchOptions: { executablePath: chrome, args: ["--no-sandbox", "--no-zygote", "--disable-gpu", "--disable-dev-shm-usage"] } }),
|
|
97
|
+
instances: [{ browser: "chromium" }],
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`headless: true` keeps the run the same locally and in CI. The launch arguments suit containers, WSL and CI runners. Name only the tests that need a real browser `*.browser.test.ts` (limitation 1); everything else stays in happy-dom, which starts in a fraction of the time.
|
|
104
|
+
|
|
105
|
+
6. Alternative, for tests of whole pages (the built app, every component on it, navigation): Playwright Test against `vite preview`, with the same Chrome:
|
|
106
|
+
|
|
107
|
+
```sh
|
|
108
|
+
npm i -D @playwright/test@1.64.1
|
|
109
|
+
npm pkg set scripts.test:e2e="playwright test"
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
// playwright.config.ts
|
|
114
|
+
import { defineConfig } from "@playwright/test";
|
|
115
|
+
|
|
116
|
+
const chrome = process.env.CHROME_BIN ?? "/usr/bin/google-chrome";
|
|
117
|
+
|
|
118
|
+
export default defineConfig({
|
|
119
|
+
testDir: "e2e",
|
|
120
|
+
use: {
|
|
121
|
+
baseURL: "http://localhost:4432",
|
|
122
|
+
launchOptions: { executablePath: chrome, args: ["--no-sandbox", "--no-zygote", "--disable-gpu", "--disable-dev-shm-usage"] },
|
|
123
|
+
},
|
|
124
|
+
webServer: { command: "npm run build && npx vite preview --port 4432 --strictPort", url: "http://localhost:4432" },
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Add `e2e` to `include` in `tsconfig.json` so `ts-dom check` type-checks the specs, and `test-results` and `playwright-report` to `.gitignore`.
|
|
129
|
+
|
|
130
|
+
## Example
|
|
131
|
+
|
|
132
|
+
The component under test (from the example project, `src/nv/counter-card.*`):
|
|
133
|
+
|
|
134
|
+
```html
|
|
135
|
+
<!-- src/nv/counter-card.html -->
|
|
136
|
+
<style scoped>
|
|
137
|
+
b { color: rgb(200, 0, 0); }
|
|
138
|
+
</style>
|
|
139
|
+
<p>{{ label }}: <b>{{ count }}</b></p>
|
|
140
|
+
<button type="button" data-on-click="increment">+1</button>
|
|
141
|
+
<p data-if="many">That is a lot.</p>
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
// src/nv/counter-card.ts
|
|
146
|
+
import { Component, define, event } from "@nativele/ts-dom";
|
|
147
|
+
import tpl from "./counter-card.html.js";
|
|
148
|
+
|
|
149
|
+
export class CounterCard extends Component(tpl, {
|
|
150
|
+
props: { label: "Count" },
|
|
151
|
+
state: { count: 0 },
|
|
152
|
+
derive: { many: (s) => s.count >= 3 },
|
|
153
|
+
emits: { "count-change": event<{ count: number }>() },
|
|
154
|
+
}) {
|
|
155
|
+
increment(): void {
|
|
156
|
+
this.set({ count: this.state.count + 1 });
|
|
157
|
+
this.emit("count-change", { count: this.state.count });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
define("counter-card", CounterCard);
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Its tests, in happy-dom:
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
// test/counter-card.test.ts
|
|
167
|
+
import { afterEach, expect, test } from "vitest";
|
|
168
|
+
import type { EmitEvent } from "@nativele/ts-dom";
|
|
169
|
+
import "../src/nv/counter-card.ts";
|
|
170
|
+
import type { CounterCard } from "../src/nv/counter-card.ts";
|
|
171
|
+
|
|
172
|
+
afterEach(() => document.body.replaceChildren());
|
|
173
|
+
|
|
174
|
+
function mount(label?: string): CounterCard {
|
|
175
|
+
const el = document.createElement("counter-card");
|
|
176
|
+
if (label !== undefined) el.label = label;
|
|
177
|
+
document.body.append(el);
|
|
178
|
+
return el;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
test("renders when it is appended, with props set as properties before", () => {
|
|
182
|
+
const el = document.createElement("counter-card");
|
|
183
|
+
el.label = "Clicks";
|
|
184
|
+
expect(el.childNodes.length).toBe(0);
|
|
185
|
+
document.body.append(el);
|
|
186
|
+
expect(el.querySelector("p")?.textContent).toBe("Clicks: 0");
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("a prop changed later, as a property or an attribute, renders at once", () => {
|
|
190
|
+
const el = mount();
|
|
191
|
+
el.label = "Taps";
|
|
192
|
+
expect(el.querySelector("p")?.textContent).toBe("Taps: 0");
|
|
193
|
+
el.setAttribute("label", "Hits");
|
|
194
|
+
expect(el.querySelector("p")?.textContent).toBe("Hits: 0");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("emit: listen on the element, read detail; the event does not bubble", () => {
|
|
198
|
+
const el = mount();
|
|
199
|
+
const details: { count: number }[] = [];
|
|
200
|
+
let heardByDocument = 0;
|
|
201
|
+
el.addEventListener("count-change", (e) => details.push((e as EmitEvent<CounterCard, "count-change">).detail));
|
|
202
|
+
document.addEventListener("count-change", () => heardByDocument++);
|
|
203
|
+
el.querySelector("button")!.click();
|
|
204
|
+
el.querySelector("button")!.click();
|
|
205
|
+
expect(details).toEqual([{ count: 1 }, { count: 2 }]);
|
|
206
|
+
expect(heardByDocument).toBe(0);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("data-if: the block is absent until its value is truthy", () => {
|
|
210
|
+
const el = mount();
|
|
211
|
+
const lots = () => [...el.querySelectorAll("p")].some((p) => p.textContent === "That is a lot.");
|
|
212
|
+
for (let i = 0; i < 2; i++) el.querySelector("button")!.click();
|
|
213
|
+
expect(lots()).toBe(false);
|
|
214
|
+
el.querySelector("button")!.click();
|
|
215
|
+
expect(lots()).toBe(true);
|
|
216
|
+
});
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
The steps every test follows:
|
|
220
|
+
|
|
221
|
+
1. Import the component module for its side effect (`import "../src/nv/counter-card.ts"`): that runs `define()`. A type-only import registers nothing (limitation 2).
|
|
222
|
+
2. Create the element with `document.createElement(tag)`; set object, array and other props as properties (`el.todos = [...]`), before or after appending. String, number and boolean props can also be attributes.
|
|
223
|
+
3. Append it to `document.body`. A Light DOM component has no children until it is connected; a component renders when it is appended.
|
|
224
|
+
4. Assert at once. Rendering is synchronous: after `append`, a prop assignment, `setAttribute`, `el.click()` or `store.set()`, the DOM is already updated, and `emit` listeners have already run. For a form, set the input's `value`, then call `click()` on its submit button or `form.requestSubmit()`: both run the template's `data-on-submit` handler, in happy-dom too. The only waits are for slot changes made after connection (one microtask, below).
|
|
225
|
+
5. Empty the document in `afterEach` (`document.body.replaceChildren()`): removing the elements runs `disconnected()` and ends their `bind`, `on` and `onCleanup` registrations.
|
|
226
|
+
|
|
227
|
+
Other parts of the API, as in the example project's tests:
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
// lists (test/todo-list.test.ts): items as components; an item's event reaches the parent with hit.key
|
|
231
|
+
const el = document.createElement("todo-list");
|
|
232
|
+
el.todos = todos;
|
|
233
|
+
document.body.append(el);
|
|
234
|
+
expect([...el.querySelectorAll("todo-row")].map((r) => r.textContent?.trim())).toEqual(["Write tests", "Run them"]);
|
|
235
|
+
expect(el.lists.todos.items).toEqual(todos);
|
|
236
|
+
el.querySelectorAll("todo-row")[1]!.querySelector("input")!.click();
|
|
237
|
+
expect(el.state.done).toEqual([2]);
|
|
238
|
+
|
|
239
|
+
// stores and bind (test/user-badge.test.ts): reset the shared store before each test
|
|
240
|
+
beforeEach(() => session.set({ user: "guest" }));
|
|
241
|
+
session.set({ user: "Ada" });
|
|
242
|
+
expect(el.textContent?.trim()).toBe("Signed in as Ada");
|
|
243
|
+
|
|
244
|
+
// Light DOM slots (test/panels.test.ts): children present at connection are placed at once;
|
|
245
|
+
// a child added later is placed by a MutationObserver, after one microtask
|
|
246
|
+
el.append(title);
|
|
247
|
+
await Promise.resolve();
|
|
248
|
+
expect(el.assignedNodes("title")).toEqual([title]);
|
|
249
|
+
|
|
250
|
+
// Shadow DOM slots: the template is in el.shadowRoot; the native slotchange event also fires after one microtask
|
|
251
|
+
el.shadowRoot!.querySelector("slot[name=title]")!.addEventListener("slotchange", onChange);
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
A real-browser test, for what happy-dom cannot show (limitation 1):
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
// test/move.browser.test.ts
|
|
258
|
+
import { afterEach, expect, test, vi } from "vitest";
|
|
259
|
+
import "../src/nv/todo-list.ts";
|
|
260
|
+
import { TodoRow, type Todo } from "../src/nv/todo-row.ts";
|
|
261
|
+
|
|
262
|
+
afterEach(() => {
|
|
263
|
+
document.body.replaceChildren();
|
|
264
|
+
vi.restoreAllMocks();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const todos: Todo[] = [
|
|
268
|
+
{ id: 1, title: "Write tests", done: false },
|
|
269
|
+
{ id: 2, title: "Run them", done: true },
|
|
270
|
+
];
|
|
271
|
+
|
|
272
|
+
test("Chrome moves a list item with moveBefore: it stays connected and keeps focus", () => {
|
|
273
|
+
expect("moveBefore" in Element.prototype).toBe(true);
|
|
274
|
+
const el = document.createElement("todo-list");
|
|
275
|
+
el.todos = todos;
|
|
276
|
+
document.body.append(el);
|
|
277
|
+
const disconnected = vi.spyOn(TodoRow.prototype, "disconnected");
|
|
278
|
+
const connected = vi.spyOn(TodoRow.prototype, "connected");
|
|
279
|
+
const input = el.querySelector("todo-row input") as HTMLInputElement;
|
|
280
|
+
input.focus();
|
|
281
|
+
el.todos = [todos[1]!, todos[0]!];
|
|
282
|
+
expect(el.querySelectorAll("todo-row input")[1]).toBe(input);
|
|
283
|
+
expect(disconnected).not.toHaveBeenCalled();
|
|
284
|
+
expect(connected).not.toHaveBeenCalled();
|
|
285
|
+
expect(document.activeElement).toBe(input);
|
|
286
|
+
});
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Under happy-dom this test fails: there is no `moveBefore`, both callbacks run and the input loses focus. The example's `test/todo-list.test.ts` asserts the happy-dom behavior, so a later happy-dom release that adds `moveBefore` shows up as a failing test. The Vitest API in browser mode is the same: the example's other happy-dom tests also pass unchanged when run in the browser project, so moving a test there is a matter of renaming it.
|
|
290
|
+
|
|
291
|
+
A page test with Playwright (the alternative):
|
|
292
|
+
|
|
293
|
+
```ts
|
|
294
|
+
// e2e/app.spec.ts
|
|
295
|
+
import { expect, test } from "@playwright/test";
|
|
296
|
+
|
|
297
|
+
test("the built page renders the components and they respond", async ({ page }) => {
|
|
298
|
+
const errors: string[] = [];
|
|
299
|
+
page.on("pageerror", (e) => errors.push(e.message));
|
|
300
|
+
await page.goto("/");
|
|
301
|
+
const counter = page.locator("counter-card");
|
|
302
|
+
await expect(counter.locator("p").first()).toHaveText("Clicks: 0");
|
|
303
|
+
await counter.getByRole("button", { name: "+1" }).click();
|
|
304
|
+
await expect(counter.locator("b")).toHaveText("1");
|
|
305
|
+
await expect(page.locator("todo-list todo-row")).toHaveText(["Write tests", "Run them in Chrome"]);
|
|
306
|
+
await expect(page.locator("light-panel h2")).toHaveText("Light");
|
|
307
|
+
await expect(page.locator("shadow-panel h2")).toHaveText("Untitled");
|
|
308
|
+
await expect(page.getByRole("heading", { name: "Shadow" })).toBeVisible();
|
|
309
|
+
expect(errors).toEqual([]);
|
|
310
|
+
});
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Playwright locators reach into open shadow roots, so `page.locator("shadow-panel h2")` finds the heading inside a `shadow: true` component; its text is the slot fallback, not what is shown (limitation 6).
|
|
314
|
+
|
|
315
|
+
## Known limitations
|
|
316
|
+
|
|
317
|
+
1. **What happy-dom does not have.** happy-dom 21 (and jsdom 30) have no `Element.prototype.moveBefore`, so a list item that moves on reorder is taken out and put back: `disconnected()` and `connected()` run, `bind` subscriptions end and start again, and a focused input in it loses focus. Chrome 148 has `moveBefore`, and the same move runs neither callback and keeps focus. Test moves and focus in a `*.browser.test.ts`. Everything else the example covers behaves the same in happy-dom, jsdom and Chrome: custom elements and attribute changes, `MutationObserver` (Light DOM slots), Shadow DOM, native slots and `slotchange`, events, lists, `data-if` and stores. Computed styles are not reliable in a DOM shim (jsdom ignores the `@scope` rule that `<style scoped>` produces; happy-dom applies it); assert styles, layout and real input in the browser.
|
|
318
|
+
2. **Import the module, not only the type.** `import type { CounterCard }` (or `import { type CounterCard }`) is erased, so `define()` never runs: `document.createElement("counter-card")` returns a plain element that never renders, and `tsc` still types it as `CounterCard` through `ts-dom-tags.ts`. The symptom is `expected undefined to be ...` or `Cannot read properties of null (reading 'click')`. Add `import "../src/nv/counter-card.ts"`. Child components used in a template are imported by its generated module, so importing the parent is enough.
|
|
319
|
+
3. **One registry per test file.** Vitest runs each test file in a new environment by default (`isolate: true`), in happy-dom and in browser mode: each file starts with an empty document and an empty `customElements`. Inside a file, the document and the registry are shared by all its tests, so empty the document in `afterEach`. With `isolate: false` and the files run one after another in the same worker (`--no-isolate --no-file-parallelism`), they share both: the document keeps the previous file's elements, and a direct `customElements.define()` of a name already defined throws.
|
|
320
|
+
4. **A tag cannot be redefined.** `define()` with a name that is already defined does nothing and keeps the first class; `customElements.define()` throws an error whose message says the name is already used or registered (in happy-dom, jsdom and Chrome). `vi.resetModules()` followed by a new `import()` creates a new class and new module-level stores, but the tag keeps running the first class, bound to the first store. Reset shared stores with `store.set(...)` in `beforeEach` instead, and give a component defined only for one test file a name no other module uses.
|
|
321
|
+
5. **Do not add a `data-ref` only for a test.** `ts-dom check` reports a ref that the component class never reads as `unused-ref`, and access from a test does not count. Query the rendered DOM in the test (`el.querySelector("button")`, `el.shadowRoot.querySelector(...)`, `el.assignedNodes(name)`). The same applies to the oxlint rule: turn `ts-dom/unused-markup` off for test folders, as the setup guide says.
|
|
322
|
+
6. **Text of a slotted heading in a Shadow DOM component.** `textContent` (and Playwright's `toHaveText` on the element inside the shadow root) reads the slot's fallback content, not the slotted nodes: in the example, `shadow-panel h2` has the text `Untitled` while it shows `Shadow`. Read the slot with `el.assignedNodes(name)`, or in Playwright use `getByRole("heading", { name: "Shadow" })`, which follows what is shown. A Light DOM component has the slotted nodes in place, so its text is what is shown.
|
|
323
|
+
7. **Slot changes after connection are asynchronous.** Children added to a Light DOM component after it is connected are placed by a `MutationObserver`, and `slotchange(name)` runs then; for a Shadow DOM component, the native `slotchange` event fires at the same point. `await Promise.resolve()` once before asserting is enough in happy-dom, jsdom and Chrome. Children present when the element is appended are placed synchronously.
|
|
324
|
+
8. **Typing custom events.** `addEventListener` types an unknown event name as `Event`, so `e.detail` is a `tsc` error (`TS2339`); cast with `EmitEvent<CounterCard, "count-change">`.
|
|
325
|
+
9. Not verified: Vitest watch mode after a template change, Vitest 4 or older, browsers other than Chrome, Playwright's downloaded browsers (`npx playwright install`) instead of the installed Chrome, `@vitest/browser-webdriverio`, and testing components inside a UI framework (see that framework's Skill).
|
|
326
|
+
|
|
327
|
+
## Verify
|
|
328
|
+
|
|
329
|
+
Run in the project:
|
|
330
|
+
|
|
331
|
+
```sh
|
|
332
|
+
npx vitest run --project unit # expect: every test file passes, on a clean checkout too
|
|
333
|
+
npx ts-dom check src/nv # expect: ts-dom check: no issues (this type-checks the tests)
|
|
334
|
+
npx ts-dom doctor src/nv # expect: ts-dom doctor: the set-up looks right (info lines are suggestions)
|
|
335
|
+
npx vitest run --project browser # expect: the *.browser.test.ts files pass in headless Chrome
|
|
336
|
+
npx playwright test # if you use the alternative: builds, serves on port 4432, passes
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
The example project's `verify` script runs exactly these, plus the same unit tests under jsdom as a third Vitest project (`"environment": "jsdom"`), which shows that jsdom works for the same tests. Its tests prove each limitation above: the module import, per-file isolation, `define()` twice, `vi.resetModules()`, slot timing in both modes, and the move that happy-dom and Chrome handle differently.
|
|
340
|
+
|
|
341
|
+
## Troubleshooting
|
|
342
|
+
|
|
343
|
+
| Symptom | Cause | Fix |
|
|
344
|
+
|---|---|---|
|
|
345
|
+
| `Error: Failed to resolve import "./counter-card.html.js" from "src/nv/counter-card.ts". Does the file exist?` | Vitest runs without the `tsDom()` plugin (a `vitest.config.ts` without it replaces `vite.config.ts`), and nothing generated the files | add `tsDom({ dirs: ["src/nv"] })` to the config Vitest loads, or run `ts-dom gen src/nv` before the tests |
|
|
346
|
+
| `expected undefined to be '...'`, `Cannot read properties of null (reading 'click')` on a freshly appended element | the component module was imported only as a type, so the tag is not defined | `import "../src/nv/x.ts"` (limitation 2) |
|
|
347
|
+
| `TS2339: Property 'detail' does not exist on type 'Event'` from `ts-dom check` | the listener's event is a plain `Event` | cast with `EmitEvent<Component, "event-name">` |
|
|
348
|
+
| `TS2339: Property 'label' does not exist on type 'HTMLElement'` on `document.createElement("counter-card")` | `tsc` ran without `ts-dom-tags.ts` (nothing generated it), or the component class is not exported | run `ts-dom check src/nv` instead of plain `tsc`; export the class |
|
|
349
|
+
| `src/nv/x.html:1:1 error [unused-ref] data-ref="..." is never used by the component class` | a ref added for a test | query the DOM in the test and remove the ref (limitation 5) |
|
|
350
|
+
| A test passes alone and fails with others in the same file, or sees elements from another test | the document is shared within a file | `afterEach(() => document.body.replaceChildren())`; reset module-level stores in `beforeEach` |
|
|
351
|
+
| `Error: browserType.launch: Failed to launch chromium because executable doesn't exist at /usr/bin/google-chrome` | no Chrome at that path | set `CHROME_BIN` to the Chrome or Chromium binary |
|
|
352
|
+
| `Error: browserType.launch: Executable doesn't exist at ~/.cache/ms-playwright/...` and `Looks like Playwright was just installed or updated` | `executablePath` is missing from `launchOptions`, so Playwright looks for its own downloaded browser | set `executablePath` as in step 5 |
|
|
353
|
+
| DEPRECATED: `test.browser.api` was deprecated in Vitest 5. Use `test.api` instead. | the port is set where Vitest 4 had it | move `api` from `test.browser` to `test` of the browser project |
|
|
354
|
+
| A test that moves list items passes in happy-dom and fails in Chrome (or the reverse) | happy-dom has no `moveBefore` | assert moves in a `*.browser.test.ts` (limitation 1) |
|
|
355
|
+
|
|
356
|
+
## See also
|
|
357
|
+
|
|
358
|
+
- Rendering is synchronous, `set` and `batch`: https://github.com/Pixmi/nativele/blob/main/docs/components.md#data-and-view-state-optional
|
|
359
|
+
- Props as properties and attributes, `emit`, events that do not bubble: https://github.com/Pixmi/nativele/blob/main/docs/components.md#data-flow-props-down-events-up
|
|
360
|
+
- Light DOM and Shadow DOM slots, `assignedNodes`, `slotchange`: https://github.com/Pixmi/nativele/blob/main/docs/components.md#light-default-and-shadow
|
|
361
|
+
- Stores and `bind`: https://github.com/Pixmi/nativele/blob/main/docs/components.md#state-store-when-not-using-state
|
|
362
|
+
- Lists, and what moving an item does with and without `moveBefore`: https://github.com/Pixmi/nativele/blob/main/docs/lists.md#moving-items-connecting-and-subscriptions
|
|
363
|
+
- The Vite plugin, `ts-dom check`, and lint in test folders: https://github.com/Pixmi/nativele/blob/main/docs/tooling.md
|
|
364
|
+
- Setting up the Vite app first: `npx ts-dom skills nativele-vite`
|