@wcstack/testing 1.32.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/README.ja.md +101 -0
- package/README.md +101 -0
- package/dist/index.d.ts +95 -0
- package/dist/index.esm.js +197 -0
- package/dist/index.esm.js.map +1 -0
- package/package.json +76 -0
package/README.ja.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# @wcstack/testing
|
|
2
|
+
|
|
3
|
+
> 🤖 **AI coding agents**: This README is a package-level reference, not the primary entry point for building a wcstack application. If you have not already done so, first read the repository [README](https://github.com/wcstack/wcstack#readme) and [AGENTS.md](https://github.com/wcstack/wcstack/blob/main/AGENTS.md), then use the [wcstack-app skill](https://github.com/wcstack/wcstack-skill).
|
|
4
|
+
|
|
5
|
+
**もし、ページのテストが import 1 つで済んだら?**
|
|
6
|
+
|
|
7
|
+
`<wcs-state>` のページは素の DOM なので、happy-dom で既にヘッドレスにテストできます — レシピは [state README](../state/README.ja.md#ページをテストする) にあり、テスト専用 API は要りません。`@wcstack/testing` はそのレシピをパッケージにしたもの: HTML をマウントし、全要素と全バインドを待ち、state を読み書きし、settle して assert する。便利であって必須ではなく、素のレシピはこのパッケージ無しでも動き続けます。
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { mount, settle, fire } from "@wcstack/testing";
|
|
11
|
+
|
|
12
|
+
const app = await mount(`
|
|
13
|
+
<wcs-state json='{"count": 1}'></wcs-state>
|
|
14
|
+
<p data-wcs="textContent: count"></p>
|
|
15
|
+
<button data-wcs="onclick: up">+1</button>
|
|
16
|
+
`);
|
|
17
|
+
expect(app.root.querySelector("p")!.textContent).toBe("1");
|
|
18
|
+
|
|
19
|
+
await app.state().write((s) => { s.count = 42; }); // ハンドラがやっていること
|
|
20
|
+
await settle();
|
|
21
|
+
expect(app.root.querySelector("p")!.textContent).toBe("42");
|
|
22
|
+
|
|
23
|
+
fire(app.root.querySelector("button")!, "click"); // ユーザーがやること
|
|
24
|
+
await settle();
|
|
25
|
+
app.unmount();
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install -D @wcstack/testing @wcstack/state @wcstack/server vitest happy-dom
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`@wcstack/state` と `@wcstack/server` は peer です: `mount()` は state で要素を登録し、server の `waitForReady` で待ちます — `renderToString` がシリアライズ前に使う安定化ループそのものなので、`<wcs-router>` の初回ルート、要素を追加する `$connectedCallback`、`<wcs-state>` のバインディング構築が 1 呼び出しで揃います。vitest なら `environment: 'happy-dom'` を設定するだけ。素の Node では `installDom()` を使います。
|
|
33
|
+
|
|
34
|
+
## API
|
|
35
|
+
|
|
36
|
+
### `mount(html, options?) → Promise<MountedApp>`
|
|
37
|
+
|
|
38
|
+
要素を登録し、`html` を挿入し、root 配下の全てが ready になったら resolve します。
|
|
39
|
+
|
|
40
|
+
| オプション | 説明 |
|
|
41
|
+
|---|---|
|
|
42
|
+
| `root` | `"document"`(既定)— HTML は `document.body` の中身になる。`"shadow"` — `document.body` に追加した host の open ShadowRoot に入り、バインドはその root に閉じる |
|
|
43
|
+
| `bootstrap` | 先に走らせる登録関数。既定 `[bootstrapState]`(`@wcstack/state` から遅延 import)。ページが使うもの — `bootstrapRouter`・`bootstrapFetch` … — を関数か非同期ローダー `async () => (await import("@wcstack/router")).bootstrapRouter()` で足す。wcstack の bootstrap は全て冪等 |
|
|
44
|
+
| `stateTagName` | `bootstrapState({ tagNames })` で state タグを改名したときの名前(既定 `wcs-state`) |
|
|
45
|
+
| `maxIterations` | 待機中に挿入された要素を拾う安定化ループの回数(既定 10) |
|
|
46
|
+
|
|
47
|
+
返る `MountedApp`:
|
|
48
|
+
|
|
49
|
+
| メンバー | 説明 |
|
|
50
|
+
|---|---|
|
|
51
|
+
| `root` | `document` か ShadowRoot — ここに query する |
|
|
52
|
+
| `container` | `document.body` か shadow host |
|
|
53
|
+
| `state(name?)` | その `name` 属性を持つ `<wcs-state>` のアクセサ(既定 `"default"` = 属性なし)。無ければ throw |
|
|
54
|
+
| `state().read(fn)` | readonly プロキシに対して `fn` を走らせ結果を返す |
|
|
55
|
+
| `state().write(fn)` | writable プロキシに対して `fn`(同期 / 非同期)を走らせる — ハンドラと同じ。後に `await settle()` |
|
|
56
|
+
| `state().element` | 要素そのもの |
|
|
57
|
+
| `unmount()` | マウントした HTML を取り除く |
|
|
58
|
+
|
|
59
|
+
`mount()` は happy-dom の 2 つの角も均して、ページがブラウザと同じに振る舞うようにします:
|
|
60
|
+
|
|
61
|
+
- ブラウザ風の `URL.createObjectURL` があればプロセスにつき 1 回それを無効化する — Node は `blob:` URL を import できず、これが無いとインライン `<script type="module">` の state が永久に読み込み中になる(ローダーは SSR と同じく `data:` URL 経路に倒れる)。
|
|
62
|
+
- happy-dom の `textContent` / `innerText` setter を DOM 仕様どおりに文字列化するよう包む — happy-dom は数値 `0` を空文字にし(`innerText` は数値で throw する)、`textContent: count` のバインドが 0 で消えるのは happy-dom 下だけになるため。
|
|
63
|
+
|
|
64
|
+
### `settle() → Promise<void>`
|
|
65
|
+
|
|
66
|
+
マイクロタスク 2 段とマクロタスク 1 段 — 書き込みが DOM に届くのに十分な待ち。`write()`・`fire()`・任意の DOM イベントの後に使います。
|
|
67
|
+
|
|
68
|
+
### `fire(target, type, init?) → boolean`
|
|
69
|
+
|
|
70
|
+
`Event`(`init.detail` があれば `CustomEvent`)を既定で bubbling させて dispatch します。ハンドラが `preventDefault()` を呼ぶと `false`。
|
|
71
|
+
|
|
72
|
+
### `installDom(options?) → Promise<() => Promise<void>>`
|
|
73
|
+
|
|
74
|
+
DOM 環境の無いランナー向け: happy-dom の `Window`(`options.url`・既定 `http://localhost/`)を作るか `options.window` を受け取り、server の `installGlobals` でグローバルを差し替えます。返る関数がグローバルを戻して window を close します。`@wcstack/state`(他の wcstack パッケージも)はこれを呼んだ**後に** import してください — 要素クラスはモジュール評価時に基底クラスを決めます。`happy-dom` は optional peer で、ここでだけ必要です。
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
import { installDom, mount, settle } from "@wcstack/testing";
|
|
78
|
+
|
|
79
|
+
const restore = await installDom();
|
|
80
|
+
try {
|
|
81
|
+
const app = await mount(html); // @wcstack/state は DOM ができた後に遅延 import される
|
|
82
|
+
// ...
|
|
83
|
+
} finally {
|
|
84
|
+
await restore();
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## 死角
|
|
89
|
+
|
|
90
|
+
happy-dom が再現できない 2 点。実ブラウザの e2e(Playwright)を 1 本残してください:
|
|
91
|
+
|
|
92
|
+
- `customElements.define` は既存ノードを**差し替えて**アップグレードするので、「遅れて define された同一ノードに値が届く」はヘッドレスでは検証できない。
|
|
93
|
+
- イベントのタイミングが実ブラウザと違う。
|
|
94
|
+
|
|
95
|
+
## 実例
|
|
96
|
+
|
|
97
|
+
[`examples/state-testing-todo/`](../../examples/state-testing-todo/) — todo ページと、それを `mount` / `fire` / `settle` で動かす vitest スイート。
|
|
98
|
+
|
|
99
|
+
## License
|
|
100
|
+
|
|
101
|
+
MIT
|
package/README.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# @wcstack/testing
|
|
2
|
+
|
|
3
|
+
> 🤖 **AI coding agents**: This README is a package-level reference, not the primary entry point for building a wcstack application. If you have not already done so, first read the repository [README](https://github.com/wcstack/wcstack#readme) and [AGENTS.md](https://github.com/wcstack/wcstack/blob/main/AGENTS.md), then use the [wcstack-app skill](https://github.com/wcstack/wcstack-skill).
|
|
4
|
+
|
|
5
|
+
**What if testing a page were one import?**
|
|
6
|
+
|
|
7
|
+
A `<wcs-state>` page is plain DOM, so it already tests headlessly with happy-dom — the recipe is in the [state README](../state/README.md#testing-your-page) and needs no test-only API. `@wcstack/testing` is that recipe packaged: mount the HTML, wait for every element and binding, read and write the state, settle, assert. It is a convenience, not a requirement; the bare recipe keeps working without it.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { mount, settle, fire } from "@wcstack/testing";
|
|
11
|
+
|
|
12
|
+
const app = await mount(`
|
|
13
|
+
<wcs-state json='{"count": 1}'></wcs-state>
|
|
14
|
+
<p data-wcs="textContent: count"></p>
|
|
15
|
+
<button data-wcs="onclick: up">+1</button>
|
|
16
|
+
`);
|
|
17
|
+
expect(app.root.querySelector("p")!.textContent).toBe("1");
|
|
18
|
+
|
|
19
|
+
await app.state().write((s) => { s.count = 42; }); // what a handler does
|
|
20
|
+
await settle();
|
|
21
|
+
expect(app.root.querySelector("p")!.textContent).toBe("42");
|
|
22
|
+
|
|
23
|
+
fire(app.root.querySelector("button")!, "click"); // what a user does
|
|
24
|
+
await settle();
|
|
25
|
+
app.unmount();
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install -D @wcstack/testing @wcstack/state @wcstack/server vitest happy-dom
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`@wcstack/state` and `@wcstack/server` are peers: `mount()` registers the elements from state and waits with server's `waitForReady` — the same stabilization loop `renderToString` uses before serializing, so `<wcs-router>`'s first route, a `$connectedCallback` that inserts more elements, and `<wcs-state>`'s binding construction are all covered by one call. Under vitest, set `environment: 'happy-dom'` and nothing else is needed; in bare Node, see `installDom()`.
|
|
33
|
+
|
|
34
|
+
## API
|
|
35
|
+
|
|
36
|
+
### `mount(html, options?) → Promise<MountedApp>`
|
|
37
|
+
|
|
38
|
+
Registers the elements, inserts `html`, and resolves once everything under the root is ready.
|
|
39
|
+
|
|
40
|
+
| Option | Description |
|
|
41
|
+
|---|---|
|
|
42
|
+
| `root` | `"document"` (default) — the HTML becomes `document.body`'s content. `"shadow"` — it goes into an open ShadowRoot on a fresh host appended to `document.body`, so bindings scope to that root |
|
|
43
|
+
| `bootstrap` | Registration functions to run first. Default `[bootstrapState]` (imported lazily from `@wcstack/state`). Add what the page uses — `bootstrapRouter`, `bootstrapFetch`, … — as functions or async loaders: `async () => (await import("@wcstack/router")).bootstrapRouter()`. Every wcstack bootstrap is idempotent |
|
|
44
|
+
| `stateTagName` | The state tag when `bootstrapState({ tagNames })` renamed it (default `wcs-state`) |
|
|
45
|
+
| `maxIterations` | Stabilization rounds for elements inserted while waiting (default 10) |
|
|
46
|
+
|
|
47
|
+
The returned `MountedApp`:
|
|
48
|
+
|
|
49
|
+
| Member | Description |
|
|
50
|
+
|---|---|
|
|
51
|
+
| `root` | `document` or the ShadowRoot — query it |
|
|
52
|
+
| `container` | `document.body` or the shadow host |
|
|
53
|
+
| `state(name?)` | Accessor for the `<wcs-state>` with that `name` attribute (default `"default"` = none). Throws if absent |
|
|
54
|
+
| `state().read(fn)` | Run `fn` against a readonly proxy and return its result |
|
|
55
|
+
| `state().write(fn)` | Run `fn` (sync or async) against a writable proxy — exactly what a handler does. Follow with `await settle()` |
|
|
56
|
+
| `state().element` | The element itself |
|
|
57
|
+
| `unmount()` | Remove the mounted HTML |
|
|
58
|
+
|
|
59
|
+
`mount()` also smooths two happy-dom edges so the page behaves as in a browser:
|
|
60
|
+
|
|
61
|
+
- it disables `URL.createObjectURL` once per process when a browser-style one is present — Node cannot import `blob:` URLs, and without this an inline `<script type="module">` state never finishes loading (the loader then takes its `data:` URL path, as SSR does);
|
|
62
|
+
- it wraps happy-dom's `textContent` / `innerText` setters to stringify like the DOM spec — happy-dom turns a numeric `0` into an empty string (and `innerText` throws on numbers), so a `textContent: count` binding would blank out at zero only under happy-dom.
|
|
63
|
+
|
|
64
|
+
### `settle() → Promise<void>`
|
|
65
|
+
|
|
66
|
+
Two microtask turns and one macrotask — enough for a write to reach the DOM. Use it after `write()`, `fire()`, or any DOM event.
|
|
67
|
+
|
|
68
|
+
### `fire(target, type, init?) → boolean`
|
|
69
|
+
|
|
70
|
+
Dispatches an `Event` (or a `CustomEvent` when `init.detail` is given), bubbling by default. Returns `false` when a handler called `preventDefault()`.
|
|
71
|
+
|
|
72
|
+
### `installDom(options?) → Promise<() => Promise<void>>`
|
|
73
|
+
|
|
74
|
+
For runners without a DOM environment: creates a happy-dom `Window` (`options.url`, default `http://localhost/`) — or takes `options.window` — and installs its globals with server's `installGlobals`. The returned function restores the globals and closes the window. Import `@wcstack/state` (and any other wcstack package) **after** calling it: element classes bind their base class at module evaluation. `happy-dom` is an optional peer, needed only here.
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
import { installDom, mount, settle } from "@wcstack/testing";
|
|
78
|
+
|
|
79
|
+
const restore = await installDom();
|
|
80
|
+
try {
|
|
81
|
+
const app = await mount(html); // @wcstack/state is imported lazily, after the DOM exists
|
|
82
|
+
// ...
|
|
83
|
+
} finally {
|
|
84
|
+
await restore();
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Blind spots
|
|
89
|
+
|
|
90
|
+
Two things happy-dom cannot reproduce; keep one browser e2e (Playwright) for them:
|
|
91
|
+
|
|
92
|
+
- `customElements.define` upgrades existing nodes by **replacing** them, so "a value reaches the same node after a late define" cannot be asserted headlessly.
|
|
93
|
+
- Event timing differs from real browsers.
|
|
94
|
+
|
|
95
|
+
## Example
|
|
96
|
+
|
|
97
|
+
[`examples/state-testing-todo/`](../../examples/state-testing-todo/) — a todo page and its vitest suite driving it through `mount` / `fire` / `settle`.
|
|
98
|
+
|
|
99
|
+
## License
|
|
100
|
+
|
|
101
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mount() — the README recipe ("Testing Your Page", @wcstack/state) as one call:
|
|
3
|
+
* register the elements, insert the HTML, wait for every element and binding
|
|
4
|
+
* under it, and hand back typed accessors for the page's `<wcs-state>`s.
|
|
5
|
+
*
|
|
6
|
+
* The wait is `@wcstack/server`'s `waitForReady` — the same stabilization loop
|
|
7
|
+
* `renderToString` performs, so `<wcs-router>`'s first route, a `$connectedCallback`
|
|
8
|
+
* that inserts more elements, and `<wcs-state>`'s binding construction are all
|
|
9
|
+
* covered by the one call (docs/app-testing-and-typescript-impl-plan.md D11).
|
|
10
|
+
*/
|
|
11
|
+
type BootstrapFunction = () => void | Promise<void>;
|
|
12
|
+
interface MountOptions {
|
|
13
|
+
/**
|
|
14
|
+
* Where to insert the HTML. `"document"` (default) replaces `document.body`'s
|
|
15
|
+
* content; `"shadow"` puts it inside an open ShadowRoot on a fresh host element
|
|
16
|
+
* appended to `document.body` (bindings then scope to that root).
|
|
17
|
+
*/
|
|
18
|
+
readonly root?: "document" | "shadow";
|
|
19
|
+
/**
|
|
20
|
+
* Element registrations to run before inserting the HTML. Default:
|
|
21
|
+
* `[bootstrapState]` imported lazily from `@wcstack/state`. Add the packages
|
|
22
|
+
* the page uses — `bootstrapRouter`, `bootstrapFetch`, … — as functions or
|
|
23
|
+
* async loaders (`async () => (await import("@wcstack/router")).bootstrapRouter()`);
|
|
24
|
+
* every wcstack bootstrap is idempotent, so calling them per test is safe.
|
|
25
|
+
*/
|
|
26
|
+
readonly bootstrap?: readonly BootstrapFunction[];
|
|
27
|
+
/** Tag name of the state element when `bootstrapState({ tagNames })` renamed it (default `wcs-state`). */
|
|
28
|
+
readonly stateTagName?: string;
|
|
29
|
+
/** Passed to `waitForReady` (default 10 stabilization rounds). */
|
|
30
|
+
readonly maxIterations?: number;
|
|
31
|
+
}
|
|
32
|
+
interface StateHandle {
|
|
33
|
+
/** The `<wcs-state>` element itself. */
|
|
34
|
+
readonly element: HTMLElement;
|
|
35
|
+
/** Read through a readonly proxy — the value `fn` returns is passed back. */
|
|
36
|
+
read<T>(fn: (state: any) => T): T;
|
|
37
|
+
/** Write through a writable proxy, exactly as a handler does. Follow with `await settle()`. */
|
|
38
|
+
write(fn: (state: any) => void | Promise<void>): Promise<void>;
|
|
39
|
+
}
|
|
40
|
+
interface MountedApp {
|
|
41
|
+
/** The root the HTML lives under: `document` or the ShadowRoot. Query it. */
|
|
42
|
+
readonly root: Document | ShadowRoot;
|
|
43
|
+
/** The node whose children are the mounted HTML: `document.body` or the shadow host. */
|
|
44
|
+
readonly container: Element;
|
|
45
|
+
/** Accessor for the `<wcs-state>` named `name` (default `"default"`, i.e. no `name` attribute). Throws if absent. */
|
|
46
|
+
state(name?: string): StateHandle;
|
|
47
|
+
/** Remove the mounted HTML. */
|
|
48
|
+
unmount(): void;
|
|
49
|
+
}
|
|
50
|
+
declare function mount(html: string, options?: MountOptions): Promise<MountedApp>;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Let a state write reach the DOM.
|
|
54
|
+
*
|
|
55
|
+
* `@wcstack/state` applies updates on the microtask queue; two microtask turns
|
|
56
|
+
* cover the write → apply chain, and one macrotask (`setTimeout(0)`) covers
|
|
57
|
+
* anything a binding defers (e.g. a `customElements.whenDefined` re-apply).
|
|
58
|
+
* This is exactly the wait the README recipe uses, fixed in one place.
|
|
59
|
+
*/
|
|
60
|
+
declare function settle(): Promise<void>;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Dispatch a DOM event the way a user action would: bubbling by default, a
|
|
64
|
+
* `CustomEvent` when `detail` is given, a plain `Event` otherwise.
|
|
65
|
+
*
|
|
66
|
+
* Returns what `dispatchEvent` returns (`false` when a handler called
|
|
67
|
+
* `preventDefault()`). Follow with `await settle()` before asserting the DOM.
|
|
68
|
+
*/
|
|
69
|
+
declare function fire(target: EventTarget, type: string, init?: EventInit & {
|
|
70
|
+
detail?: unknown;
|
|
71
|
+
}): boolean;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Bare Node (no vitest `environment: 'happy-dom'`): create a happy-dom `Window`
|
|
75
|
+
* and install its globals, using the same `installGlobals` `@wcstack/server` runs
|
|
76
|
+
* for SSR. Returns an async restore function that also closes the window.
|
|
77
|
+
*
|
|
78
|
+
* Import order matters: `@wcstack/state`'s element classes pick their base class
|
|
79
|
+
* when the module is evaluated, so `mount()` imports it lazily — after this
|
|
80
|
+
* function has installed `HTMLElement`. Do the same for any other wcstack
|
|
81
|
+
* package you bootstrap (`async () => (await import("@wcstack/router")).bootstrapRouter()`).
|
|
82
|
+
*
|
|
83
|
+
* `happy-dom` is an optional peer: it is only needed here. A `window` can also be
|
|
84
|
+
* passed in (`installDom({ window })`) to skip the import entirely.
|
|
85
|
+
*/
|
|
86
|
+
interface InstallDomOptions {
|
|
87
|
+
/** `window.location` for the page (default `http://localhost/`). */
|
|
88
|
+
readonly url?: string;
|
|
89
|
+
/** A ready-made happy-dom `Window`; when given, `happy-dom` is not imported. */
|
|
90
|
+
readonly window?: unknown;
|
|
91
|
+
}
|
|
92
|
+
declare function installDom(options?: InstallDomOptions): Promise<() => Promise<void>>;
|
|
93
|
+
|
|
94
|
+
export { fire, installDom, mount, settle };
|
|
95
|
+
export type { BootstrapFunction, InstallDomOptions, MountOptions, MountedApp, StateHandle };
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { waitForReady, installGlobals } from '@wcstack/server';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* mount() — the README recipe ("Testing Your Page", @wcstack/state) as one call:
|
|
5
|
+
* register the elements, insert the HTML, wait for every element and binding
|
|
6
|
+
* under it, and hand back typed accessors for the page's `<wcs-state>`s.
|
|
7
|
+
*
|
|
8
|
+
* The wait is `@wcstack/server`'s `waitForReady` — the same stabilization loop
|
|
9
|
+
* `renderToString` performs, so `<wcs-router>`'s first route, a `$connectedCallback`
|
|
10
|
+
* that inserts more elements, and `<wcs-state>`'s binding construction are all
|
|
11
|
+
* covered by the one call (docs/app-testing-and-typescript-impl-plan.md D11).
|
|
12
|
+
*/
|
|
13
|
+
let inlineScriptLoaderPatched = false;
|
|
14
|
+
/**
|
|
15
|
+
* Route `<wcs-state>` inline `<script type="module">` through the `data:` URL
|
|
16
|
+
* loader. Node cannot import `blob:` URLs, so the browser path (`URL.createObjectURL`)
|
|
17
|
+
* would leave an inline-script state pending forever; the loader falls back to a
|
|
18
|
+
* `data:` URL when `createObjectURL` is absent — the same switch `@wcstack/server`
|
|
19
|
+
* flips for SSR. Applied once per process, only when a browser-style
|
|
20
|
+
* `createObjectURL` is present (i.e. under vitest's happy-dom environment).
|
|
21
|
+
*/
|
|
22
|
+
function patchInlineScriptLoader() {
|
|
23
|
+
if (inlineScriptLoaderPatched)
|
|
24
|
+
return;
|
|
25
|
+
inlineScriptLoaderPatched = true;
|
|
26
|
+
if (typeof URL.createObjectURL === "function") {
|
|
27
|
+
URL.createObjectURL = undefined;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const TEXT_SETTER_PATCHED = Symbol.for("wcstack.testing.textSetterPatched");
|
|
31
|
+
/**
|
|
32
|
+
* happy-dom's `textContent` setter treats a numeric `0` as empty (and `innerText`
|
|
33
|
+
* throws on any non-string), whereas browsers stringify: `el.textContent = 0`
|
|
34
|
+
* renders "0". A `textContent: count` binding assigns the raw number, so a count
|
|
35
|
+
* that reaches zero would vanish only under happy-dom. Wrap the setters on every
|
|
36
|
+
* prototype that owns one (Node, Element, HTMLElement) to coerce the way the
|
|
37
|
+
* DOM spec does (`null` → "", anything else → `String(value)`). Idempotent per
|
|
38
|
+
* prototype, so a fresh window from `installDom()` gets patched too.
|
|
39
|
+
*/
|
|
40
|
+
function patchTextSetters() {
|
|
41
|
+
const targets = [
|
|
42
|
+
[globalThis.Node, "textContent"],
|
|
43
|
+
[globalThis.Element, "textContent"],
|
|
44
|
+
[globalThis.HTMLElement, "textContent"],
|
|
45
|
+
[globalThis.HTMLElement, "innerText"],
|
|
46
|
+
];
|
|
47
|
+
for (const [ctor, property] of targets) {
|
|
48
|
+
const proto = ctor?.prototype;
|
|
49
|
+
if (proto === undefined)
|
|
50
|
+
continue;
|
|
51
|
+
const marker = `${String(TEXT_SETTER_PATCHED)}:${property}`;
|
|
52
|
+
// own-property check: Element.prototype inherits Node.prototype's marker, and an
|
|
53
|
+
// inherited marker must not make the Element setter (the one happy-dom uses) skip.
|
|
54
|
+
if (Object.prototype.hasOwnProperty.call(proto, marker))
|
|
55
|
+
continue;
|
|
56
|
+
const descriptor = Object.getOwnPropertyDescriptor(proto, property);
|
|
57
|
+
if (descriptor?.set === undefined)
|
|
58
|
+
continue;
|
|
59
|
+
const originalSet = descriptor.set;
|
|
60
|
+
Object.defineProperty(proto, property, {
|
|
61
|
+
...descriptor,
|
|
62
|
+
set(value) {
|
|
63
|
+
originalSet.call(this, value === null || value === undefined ? "" : String(value));
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
Object.defineProperty(proto, marker, { value: true, enumerable: false, configurable: true });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async function defaultBootstraps() {
|
|
70
|
+
// Lazy: the element classes bind their base class at module evaluation, which
|
|
71
|
+
// must happen after the DOM globals exist (installDom() in bare Node).
|
|
72
|
+
const { bootstrapState } = await import('@wcstack/state');
|
|
73
|
+
return [bootstrapState];
|
|
74
|
+
}
|
|
75
|
+
async function mount(html, options = {}) {
|
|
76
|
+
patchInlineScriptLoader();
|
|
77
|
+
patchTextSetters();
|
|
78
|
+
for (const bootstrap of options.bootstrap ?? (await defaultBootstraps())) {
|
|
79
|
+
await bootstrap();
|
|
80
|
+
}
|
|
81
|
+
const stateTagName = options.stateTagName ?? "wcs-state";
|
|
82
|
+
let root;
|
|
83
|
+
let container;
|
|
84
|
+
if (options.root === "shadow") {
|
|
85
|
+
const host = document.createElement("div");
|
|
86
|
+
host.setAttribute("data-wcs-testing-host", "");
|
|
87
|
+
const shadow = host.attachShadow({ mode: "open" });
|
|
88
|
+
shadow.innerHTML = html;
|
|
89
|
+
document.body.appendChild(host);
|
|
90
|
+
root = shadow;
|
|
91
|
+
container = host;
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
document.body.innerHTML = html;
|
|
95
|
+
root = document;
|
|
96
|
+
container = document.body;
|
|
97
|
+
}
|
|
98
|
+
await waitForReady(root, { maxIterations: options.maxIterations });
|
|
99
|
+
return {
|
|
100
|
+
root,
|
|
101
|
+
container,
|
|
102
|
+
state(name = "default") {
|
|
103
|
+
const element = [...root.querySelectorAll(stateTagName)]
|
|
104
|
+
.find((el) => (el.getAttribute("name") ?? "default") === name);
|
|
105
|
+
if (element === undefined) {
|
|
106
|
+
throw new Error(`@wcstack/testing: no <${stateTagName}${name === "default" ? "" : ` name="${name}"`}> under the mounted root`);
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
element,
|
|
110
|
+
read(fn) {
|
|
111
|
+
let out;
|
|
112
|
+
element.createState("readonly", (state) => {
|
|
113
|
+
out = fn(state);
|
|
114
|
+
});
|
|
115
|
+
return out;
|
|
116
|
+
},
|
|
117
|
+
async write(fn) {
|
|
118
|
+
await element.createStateAsync("writable", async (state) => {
|
|
119
|
+
await fn(state);
|
|
120
|
+
});
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
},
|
|
124
|
+
unmount() {
|
|
125
|
+
if (root === document) {
|
|
126
|
+
document.body.innerHTML = "";
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
container.remove();
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Let a state write reach the DOM.
|
|
137
|
+
*
|
|
138
|
+
* `@wcstack/state` applies updates on the microtask queue; two microtask turns
|
|
139
|
+
* cover the write → apply chain, and one macrotask (`setTimeout(0)`) covers
|
|
140
|
+
* anything a binding defers (e.g. a `customElements.whenDefined` re-apply).
|
|
141
|
+
* This is exactly the wait the README recipe uses, fixed in one place.
|
|
142
|
+
*/
|
|
143
|
+
async function settle() {
|
|
144
|
+
await Promise.resolve();
|
|
145
|
+
await Promise.resolve();
|
|
146
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Dispatch a DOM event the way a user action would: bubbling by default, a
|
|
151
|
+
* `CustomEvent` when `detail` is given, a plain `Event` otherwise.
|
|
152
|
+
*
|
|
153
|
+
* Returns what `dispatchEvent` returns (`false` when a handler called
|
|
154
|
+
* `preventDefault()`). Follow with `await settle()` before asserting the DOM.
|
|
155
|
+
*/
|
|
156
|
+
function fire(target, type, init = {}) {
|
|
157
|
+
const { detail, ...eventInit } = init;
|
|
158
|
+
const event = detail !== undefined
|
|
159
|
+
? new CustomEvent(type, { bubbles: true, ...eventInit, detail })
|
|
160
|
+
: new Event(type, { bubbles: true, ...eventInit });
|
|
161
|
+
return target.dispatchEvent(event);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Bare Node (no vitest `environment: 'happy-dom'`): create a happy-dom `Window`
|
|
166
|
+
* and install its globals, using the same `installGlobals` `@wcstack/server` runs
|
|
167
|
+
* for SSR. Returns an async restore function that also closes the window.
|
|
168
|
+
*
|
|
169
|
+
* Import order matters: `@wcstack/state`'s element classes pick their base class
|
|
170
|
+
* when the module is evaluated, so `mount()` imports it lazily — after this
|
|
171
|
+
* function has installed `HTMLElement`. Do the same for any other wcstack
|
|
172
|
+
* package you bootstrap (`async () => (await import("@wcstack/router")).bootstrapRouter()`).
|
|
173
|
+
*
|
|
174
|
+
* `happy-dom` is an optional peer: it is only needed here. A `window` can also be
|
|
175
|
+
* passed in (`installDom({ window })`) to skip the import entirely.
|
|
176
|
+
*/
|
|
177
|
+
async function installDom(options = {}) {
|
|
178
|
+
const window = options.window ?? (await createWindow(options.url ?? "http://localhost/"));
|
|
179
|
+
const restore = installGlobals(window);
|
|
180
|
+
return async () => {
|
|
181
|
+
restore();
|
|
182
|
+
await window.happyDOM.close();
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
async function createWindow(url) {
|
|
186
|
+
let mod;
|
|
187
|
+
try {
|
|
188
|
+
mod = (await import('happy-dom'));
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
throw new Error("@wcstack/testing: installDom() needs happy-dom (npm i -D happy-dom), or pass { window } — under vitest with environment: 'happy-dom' it is not needed at all");
|
|
192
|
+
}
|
|
193
|
+
return new mod.Window({ url });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export { fire, installDom, mount, settle };
|
|
197
|
+
//# sourceMappingURL=index.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.esm.js","sources":["../../src/mount.ts","../../src/settle.ts","../../src/fire.ts","../../src/installDom.ts"],"sourcesContent":["/**\r\n * mount() — the README recipe (\"Testing Your Page\", @wcstack/state) as one call:\r\n * register the elements, insert the HTML, wait for every element and binding\r\n * under it, and hand back typed accessors for the page's `<wcs-state>`s.\r\n *\r\n * The wait is `@wcstack/server`'s `waitForReady` — the same stabilization loop\r\n * `renderToString` performs, so `<wcs-router>`'s first route, a `$connectedCallback`\r\n * that inserts more elements, and `<wcs-state>`'s binding construction are all\r\n * covered by the one call (docs/app-testing-and-typescript-impl-plan.md D11).\r\n */\r\n\r\nimport { waitForReady } from \"@wcstack/server\";\r\n\r\nexport type BootstrapFunction = () => void | Promise<void>;\r\n\r\nexport interface MountOptions {\r\n /**\r\n * Where to insert the HTML. `\"document\"` (default) replaces `document.body`'s\r\n * content; `\"shadow\"` puts it inside an open ShadowRoot on a fresh host element\r\n * appended to `document.body` (bindings then scope to that root).\r\n */\r\n readonly root?: \"document\" | \"shadow\";\r\n /**\r\n * Element registrations to run before inserting the HTML. Default:\r\n * `[bootstrapState]` imported lazily from `@wcstack/state`. Add the packages\r\n * the page uses — `bootstrapRouter`, `bootstrapFetch`, … — as functions or\r\n * async loaders (`async () => (await import(\"@wcstack/router\")).bootstrapRouter()`);\r\n * every wcstack bootstrap is idempotent, so calling them per test is safe.\r\n */\r\n readonly bootstrap?: readonly BootstrapFunction[];\r\n /** Tag name of the state element when `bootstrapState({ tagNames })` renamed it (default `wcs-state`). */\r\n readonly stateTagName?: string;\r\n /** Passed to `waitForReady` (default 10 stabilization rounds). */\r\n readonly maxIterations?: number;\r\n}\r\n\r\nexport interface StateHandle {\r\n /** The `<wcs-state>` element itself. */\r\n readonly element: HTMLElement;\r\n /** Read through a readonly proxy — the value `fn` returns is passed back. */\r\n read<T>(fn: (state: any) => T): T;\r\n /** Write through a writable proxy, exactly as a handler does. Follow with `await settle()`. */\r\n write(fn: (state: any) => void | Promise<void>): Promise<void>;\r\n}\r\n\r\nexport interface MountedApp {\r\n /** The root the HTML lives under: `document` or the ShadowRoot. Query it. */\r\n readonly root: Document | ShadowRoot;\r\n /** The node whose children are the mounted HTML: `document.body` or the shadow host. */\r\n readonly container: Element;\r\n /** Accessor for the `<wcs-state>` named `name` (default `\"default\"`, i.e. no `name` attribute). Throws if absent. */\r\n state(name?: string): StateHandle;\r\n /** Remove the mounted HTML. */\r\n unmount(): void;\r\n}\r\n\r\ninterface StateElementLike extends HTMLElement {\r\n createState(mutability: \"readonly\" | \"writable\", callback: (state: any) => void): void;\r\n createStateAsync(mutability: \"readonly\" | \"writable\", callback: (state: any) => Promise<void>): Promise<void>;\r\n}\r\n\r\nlet inlineScriptLoaderPatched = false;\r\n\r\n/**\r\n * Route `<wcs-state>` inline `<script type=\"module\">` through the `data:` URL\r\n * loader. Node cannot import `blob:` URLs, so the browser path (`URL.createObjectURL`)\r\n * would leave an inline-script state pending forever; the loader falls back to a\r\n * `data:` URL when `createObjectURL` is absent — the same switch `@wcstack/server`\r\n * flips for SSR. Applied once per process, only when a browser-style\r\n * `createObjectURL` is present (i.e. under vitest's happy-dom environment).\r\n */\r\nfunction patchInlineScriptLoader(): void {\r\n if (inlineScriptLoaderPatched) return;\r\n inlineScriptLoaderPatched = true;\r\n if (typeof URL.createObjectURL === \"function\") {\r\n (URL as unknown as { createObjectURL: unknown }).createObjectURL = undefined;\r\n }\r\n}\r\n\r\nconst TEXT_SETTER_PATCHED = Symbol.for(\"wcstack.testing.textSetterPatched\");\r\n\r\n/**\r\n * happy-dom's `textContent` setter treats a numeric `0` as empty (and `innerText`\r\n * throws on any non-string), whereas browsers stringify: `el.textContent = 0`\r\n * renders \"0\". A `textContent: count` binding assigns the raw number, so a count\r\n * that reaches zero would vanish only under happy-dom. Wrap the setters on every\r\n * prototype that owns one (Node, Element, HTMLElement) to coerce the way the\r\n * DOM spec does (`null` → \"\", anything else → `String(value)`). Idempotent per\r\n * prototype, so a fresh window from `installDom()` gets patched too.\r\n */\r\nfunction patchTextSetters(): void {\r\n const targets: Array<[unknown, string]> = [\r\n [globalThis.Node, \"textContent\"],\r\n [globalThis.Element, \"textContent\"],\r\n [globalThis.HTMLElement, \"textContent\"],\r\n [globalThis.HTMLElement, \"innerText\"],\r\n ];\r\n for (const [ctor, property] of targets) {\r\n const proto = (ctor as { prototype?: object } | undefined)?.prototype as (Record<symbol, unknown> & object) | undefined;\r\n if (proto === undefined) continue;\r\n const marker = `${String(TEXT_SETTER_PATCHED)}:${property}`;\r\n // own-property check: Element.prototype inherits Node.prototype's marker, and an\r\n // inherited marker must not make the Element setter (the one happy-dom uses) skip.\r\n if (Object.prototype.hasOwnProperty.call(proto, marker)) continue;\r\n const descriptor = Object.getOwnPropertyDescriptor(proto, property);\r\n if (descriptor?.set === undefined) continue;\r\n const originalSet = descriptor.set;\r\n Object.defineProperty(proto, property, {\r\n ...descriptor,\r\n set(this: unknown, value: unknown) {\r\n originalSet.call(this, value === null || value === undefined ? \"\" : String(value));\r\n },\r\n });\r\n Object.defineProperty(proto, marker, { value: true, enumerable: false, configurable: true });\r\n }\r\n}\r\n\r\nasync function defaultBootstraps(): Promise<readonly BootstrapFunction[]> {\r\n // Lazy: the element classes bind their base class at module evaluation, which\r\n // must happen after the DOM globals exist (installDom() in bare Node).\r\n const { bootstrapState } = await import(\"@wcstack/state\");\r\n return [bootstrapState];\r\n}\r\n\r\nexport async function mount(html: string, options: MountOptions = {}): Promise<MountedApp> {\r\n patchInlineScriptLoader();\r\n patchTextSetters();\r\n\r\n for (const bootstrap of options.bootstrap ?? (await defaultBootstraps())) {\r\n await bootstrap();\r\n }\r\n\r\n const stateTagName = options.stateTagName ?? \"wcs-state\";\r\n let root: Document | ShadowRoot;\r\n let container: Element;\r\n if (options.root === \"shadow\") {\r\n const host = document.createElement(\"div\");\r\n host.setAttribute(\"data-wcs-testing-host\", \"\");\r\n const shadow = host.attachShadow({ mode: \"open\" });\r\n shadow.innerHTML = html;\r\n document.body.appendChild(host);\r\n root = shadow;\r\n container = host;\r\n } else {\r\n document.body.innerHTML = html;\r\n root = document;\r\n container = document.body;\r\n }\r\n\r\n await waitForReady(root, { maxIterations: options.maxIterations });\r\n\r\n return {\r\n root,\r\n container,\r\n state(name = \"default\"): StateHandle {\r\n const element = [...root.querySelectorAll<StateElementLike>(stateTagName)]\r\n .find((el) => (el.getAttribute(\"name\") ?? \"default\") === name);\r\n if (element === undefined) {\r\n throw new Error(`@wcstack/testing: no <${stateTagName}${name === \"default\" ? \"\" : ` name=\"${name}\"`}> under the mounted root`);\r\n }\r\n return {\r\n element,\r\n read(fn) {\r\n let out!: ReturnType<typeof fn>;\r\n element.createState(\"readonly\", (state) => {\r\n out = fn(state);\r\n });\r\n return out;\r\n },\r\n async write(fn) {\r\n await element.createStateAsync(\"writable\", async (state) => {\r\n await fn(state);\r\n });\r\n },\r\n };\r\n },\r\n unmount() {\r\n if (root === document) {\r\n document.body.innerHTML = \"\";\r\n } else {\r\n container.remove();\r\n }\r\n },\r\n };\r\n}\r\n","/**\r\n * Let a state write reach the DOM.\r\n *\r\n * `@wcstack/state` applies updates on the microtask queue; two microtask turns\r\n * cover the write → apply chain, and one macrotask (`setTimeout(0)`) covers\r\n * anything a binding defers (e.g. a `customElements.whenDefined` re-apply).\r\n * This is exactly the wait the README recipe uses, fixed in one place.\r\n */\r\nexport async function settle(): Promise<void> {\r\n await Promise.resolve();\r\n await Promise.resolve();\r\n await new Promise<void>((resolve) => setTimeout(resolve, 0));\r\n}\r\n","/**\r\n * Dispatch a DOM event the way a user action would: bubbling by default, a\r\n * `CustomEvent` when `detail` is given, a plain `Event` otherwise.\r\n *\r\n * Returns what `dispatchEvent` returns (`false` when a handler called\r\n * `preventDefault()`). Follow with `await settle()` before asserting the DOM.\r\n */\r\nexport function fire(target: EventTarget, type: string, init: EventInit & { detail?: unknown } = {}): boolean {\r\n const { detail, ...eventInit } = init;\r\n const event = detail !== undefined\r\n ? new CustomEvent(type, { bubbles: true, ...eventInit, detail })\r\n : new Event(type, { bubbles: true, ...eventInit });\r\n return target.dispatchEvent(event);\r\n}\r\n","/**\r\n * Bare Node (no vitest `environment: 'happy-dom'`): create a happy-dom `Window`\r\n * and install its globals, using the same `installGlobals` `@wcstack/server` runs\r\n * for SSR. Returns an async restore function that also closes the window.\r\n *\r\n * Import order matters: `@wcstack/state`'s element classes pick their base class\r\n * when the module is evaluated, so `mount()` imports it lazily — after this\r\n * function has installed `HTMLElement`. Do the same for any other wcstack\r\n * package you bootstrap (`async () => (await import(\"@wcstack/router\")).bootstrapRouter()`).\r\n *\r\n * `happy-dom` is an optional peer: it is only needed here. A `window` can also be\r\n * passed in (`installDom({ window })`) to skip the import entirely.\r\n */\r\n\r\nimport { installGlobals } from \"@wcstack/server\";\r\n\r\nexport interface InstallDomOptions {\r\n /** `window.location` for the page (default `http://localhost/`). */\r\n readonly url?: string;\r\n /** A ready-made happy-dom `Window`; when given, `happy-dom` is not imported. */\r\n readonly window?: unknown;\r\n}\r\n\r\ninterface HappyDomWindowLike {\r\n readonly happyDOM: { close(): Promise<void> };\r\n}\r\n\r\nexport async function installDom(options: InstallDomOptions = {}): Promise<() => Promise<void>> {\r\n const window = options.window ?? (await createWindow(options.url ?? \"http://localhost/\"));\r\n const restore = installGlobals(window as Parameters<typeof installGlobals>[0]);\r\n return async () => {\r\n restore();\r\n await (window as HappyDomWindowLike).happyDOM.close();\r\n };\r\n}\r\n\r\nasync function createWindow(url: string): Promise<unknown> {\r\n let mod: { Window: new (options: { url: string }) => unknown };\r\n try {\r\n mod = (await import(\"happy-dom\")) as typeof mod;\r\n } catch {\r\n throw new Error(\r\n \"@wcstack/testing: installDom() needs happy-dom (npm i -D happy-dom), or pass { window } — under vitest with environment: 'happy-dom' it is not needed at all\",\r\n );\r\n }\r\n return new mod.Window({ url });\r\n}\r\n"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wcstack/testing",
|
|
3
|
+
"version": "1.32.0",
|
|
4
|
+
"description": "Headless test helpers for wcstack pages: mount HTML under happy-dom, await bindings, read/write state, settle updates - the README recipe as one import.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.esm.js",
|
|
7
|
+
"module": "./dist/index.esm.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.esm.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"clean": "rimraf dist .tsc-out",
|
|
20
|
+
"build": "node scripts/build-deps.mjs && rimraf dist .tsc-out && tsc && rollup -c",
|
|
21
|
+
"test": "vitest run",
|
|
22
|
+
"test:watch": "vitest",
|
|
23
|
+
"test:coverage": "vitest run --coverage",
|
|
24
|
+
"lint": "eslint src"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"web-components",
|
|
28
|
+
"wcstack",
|
|
29
|
+
"testing",
|
|
30
|
+
"vitest",
|
|
31
|
+
"happy-dom",
|
|
32
|
+
"data-wcs"
|
|
33
|
+
],
|
|
34
|
+
"author": "mogera551",
|
|
35
|
+
"homepage": "https://wcstack.github.io",
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "https://github.com/wcstack/wcstack.git",
|
|
39
|
+
"directory": "packages/testing"
|
|
40
|
+
},
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/wcstack/wcstack/issues"
|
|
43
|
+
},
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"@wcstack/server": "^1.32.0",
|
|
47
|
+
"@wcstack/state": "^1.32.0",
|
|
48
|
+
"happy-dom": ">=20"
|
|
49
|
+
},
|
|
50
|
+
"peerDependenciesMeta": {
|
|
51
|
+
"happy-dom": {
|
|
52
|
+
"optional": true
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@eslint/js": "^9.39.1",
|
|
57
|
+
"@rollup/plugin-typescript": "^11.1.6",
|
|
58
|
+
"@types/node": "^22.10.0",
|
|
59
|
+
"@typescript-eslint/eslint-plugin": "^8.33.1",
|
|
60
|
+
"@typescript-eslint/parser": "^8.33.1",
|
|
61
|
+
"@vitest/coverage-v8": "^4.0.15",
|
|
62
|
+
"@wcstack/router": "file:../router",
|
|
63
|
+
"@wcstack/server": "file:../server",
|
|
64
|
+
"@wcstack/state": "file:../state",
|
|
65
|
+
"eslint": "^9.39.1",
|
|
66
|
+
"globals": "^16.2.0",
|
|
67
|
+
"happy-dom": "^20.0.11",
|
|
68
|
+
"rimraf": "^6.0.1",
|
|
69
|
+
"rollup": "^4.22.4",
|
|
70
|
+
"rollup-plugin-dts": "^6.1.1",
|
|
71
|
+
"tslib": "^2.8.1",
|
|
72
|
+
"typescript": "^5.9.3",
|
|
73
|
+
"typescript-eslint": "^8.49.0",
|
|
74
|
+
"vitest": "^4.0.15"
|
|
75
|
+
}
|
|
76
|
+
}
|