@wcstack/testing 1.33.0 → 2.0.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 +1 -1
- package/README.md +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.esm.js +7 -3
- package/dist/index.esm.js.map +1 -1
- package/package.json +3 -3
package/README.ja.md
CHANGED
|
@@ -50,7 +50,7 @@ npm install -D @wcstack/testing @wcstack/state @wcstack/server vitest happy-dom
|
|
|
50
50
|
|---|---|
|
|
51
51
|
| `root` | `document` か ShadowRoot — ここに query する |
|
|
52
52
|
| `container` | `document.body` か shadow host |
|
|
53
|
-
| `state(
|
|
53
|
+
| `state()` | ルートの `<wcs-state>` のアクセサ — v2 は 1 root 1 ツリーで名前は無い。ボリューム(`mount=`)はそのツリーの一部(パス接頭辞で読む)。(`mount()` は DOM のマウント・`mount=` は state のマウント)。無ければ throw |
|
|
54
54
|
| `state().read(fn)` | readonly プロキシに対して `fn` を走らせ結果を返す |
|
|
55
55
|
| `state().write(fn)` | writable プロキシに対して `fn`(同期 / 非同期)を走らせる — ハンドラと同じ。後に `await settle()` |
|
|
56
56
|
| `state().element` | 要素そのもの |
|
package/README.md
CHANGED
|
@@ -50,7 +50,7 @@ The returned `MountedApp`:
|
|
|
50
50
|
|---|---|
|
|
51
51
|
| `root` | `document` or the ShadowRoot — query it |
|
|
52
52
|
| `container` | `document.body` or the shadow host |
|
|
53
|
-
| `state(
|
|
53
|
+
| `state()` | Accessor for the root `<wcs-state>` — v2 has one state tree per root, so there is nothing to name. Volumes (`mount=`) are part of that tree: read them by path prefix. (`mount()` mounts DOM; `mount=` mounts state.) Throws if absent |
|
|
54
54
|
| `state().read(fn)` | Run `fn` against a readonly proxy and return its result |
|
|
55
55
|
| `state().write(fn)` | Run `fn` (sync or async) against a writable proxy — exactly what a handler does. Follow with `await settle()` |
|
|
56
56
|
| `state().element` | The element itself |
|
package/dist/index.d.ts
CHANGED
|
@@ -42,8 +42,11 @@ interface MountedApp {
|
|
|
42
42
|
readonly root: Document | ShadowRoot;
|
|
43
43
|
/** The node whose children are the mounted HTML: `document.body` or the shadow host. */
|
|
44
44
|
readonly container: Element;
|
|
45
|
-
/**
|
|
46
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Accessor for the root `<wcs-state>` (v2: one state tree per root — volumes
|
|
47
|
+
* (`mount=`) and `bind-component` elements are not it). Throws if absent.
|
|
48
|
+
*/
|
|
49
|
+
state(): StateHandle;
|
|
47
50
|
/** Remove the mounted HTML. */
|
|
48
51
|
unmount(): void;
|
|
49
52
|
}
|
package/dist/index.esm.js
CHANGED
|
@@ -99,11 +99,15 @@ async function mount(html, options = {}) {
|
|
|
99
99
|
return {
|
|
100
100
|
root,
|
|
101
101
|
container,
|
|
102
|
-
state(
|
|
102
|
+
state(...legacyArgs) {
|
|
103
|
+
if (legacyArgs.length > 0) {
|
|
104
|
+
throw new Error(`@wcstack/testing: state(name) was removed in v2 — there is one state tree per root. ` +
|
|
105
|
+
`Mount the named state onto the tree (<wcs-state mount="...">) and read it by its path prefix via state().`);
|
|
106
|
+
}
|
|
103
107
|
const element = [...root.querySelectorAll(stateTagName)]
|
|
104
|
-
.find((el) =>
|
|
108
|
+
.find((el) => !el.hasAttribute("mount") && !el.hasAttribute("bind-component"));
|
|
105
109
|
if (element === undefined) {
|
|
106
|
-
throw new Error(`@wcstack/testing: no <${stateTagName}
|
|
110
|
+
throw new Error(`@wcstack/testing: no <${stateTagName}> under the mounted root`);
|
|
107
111
|
}
|
|
108
112
|
return {
|
|
109
113
|
element,
|
package/dist/index.esm.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.esm.js","sources":["../../src/mount.ts","../../src/settle.ts","../../src/fire.ts","../../src/installDom.ts"],"sourcesContent":["/**\n * mount() — the README recipe (\"Testing Your Page\", @wcstack/state) as one call:\n * register the elements, insert the HTML, wait for every element and binding\n * under it, and hand back typed accessors for the page's `<wcs-state>`s.\n *\n * The wait is `@wcstack/server`'s `waitForReady` — the same stabilization loop\n * `renderToString` performs, so `<wcs-router>`'s first route, a `$connectedCallback`\n * that inserts more elements, and `<wcs-state>`'s binding construction are all\n * covered by the one call (docs/app-testing-and-typescript-impl-plan.md D11).\n */\n\nimport { waitForReady } from \"@wcstack/server\";\n\nexport type BootstrapFunction = () => void | Promise<void>;\n\nexport interface MountOptions {\n /**\n * Where to insert the HTML. `\"document\"` (default) replaces `document.body`'s\n * content; `\"shadow\"` puts it inside an open ShadowRoot on a fresh host element\n * appended to `document.body` (bindings then scope to that root).\n */\n readonly root?: \"document\" | \"shadow\";\n /**\n * Element registrations to run before inserting the HTML. Default:\n * `[bootstrapState]` imported lazily from `@wcstack/state`. Add the packages\n * the page uses — `bootstrapRouter`, `bootstrapFetch`, … — as functions or\n * async loaders (`async () => (await import(\"@wcstack/router\")).bootstrapRouter()`);\n * every wcstack bootstrap is idempotent, so calling them per test is safe.\n */\n readonly bootstrap?: readonly BootstrapFunction[];\n /** Tag name of the state element when `bootstrapState({ tagNames })` renamed it (default `wcs-state`). */\n readonly stateTagName?: string;\n /** Passed to `waitForReady` (default 10 stabilization rounds). */\n readonly maxIterations?: number;\n}\n\nexport interface StateHandle {\n /** The `<wcs-state>` element itself. */\n readonly element: HTMLElement;\n /** Read through a readonly proxy — the value `fn` returns is passed back. */\n read<T>(fn: (state: any) => T): T;\n /** Write through a writable proxy, exactly as a handler does. Follow with `await settle()`. */\n write(fn: (state: any) => void | Promise<void>): Promise<void>;\n}\n\nexport interface MountedApp {\n /** The root the HTML lives under: `document` or the ShadowRoot. Query it. */\n readonly root: Document | ShadowRoot;\n /** The node whose children are the mounted HTML: `document.body` or the shadow host. */\n readonly container: Element;\n /** Accessor for the `<wcs-state>` named `name` (default `\"default\"`, i.e. no `name` attribute). Throws if absent. */\n state(name?: string): StateHandle;\n /** Remove the mounted HTML. */\n unmount(): void;\n}\n\ninterface StateElementLike extends HTMLElement {\n createState(mutability: \"readonly\" | \"writable\", callback: (state: any) => void): void;\n createStateAsync(mutability: \"readonly\" | \"writable\", callback: (state: any) => Promise<void>): Promise<void>;\n}\n\nlet inlineScriptLoaderPatched = false;\n\n/**\n * Route `<wcs-state>` inline `<script type=\"module\">` through the `data:` URL\n * loader. Node cannot import `blob:` URLs, so the browser path (`URL.createObjectURL`)\n * would leave an inline-script state pending forever; the loader falls back to a\n * `data:` URL when `createObjectURL` is absent — the same switch `@wcstack/server`\n * flips for SSR. Applied once per process, only when a browser-style\n * `createObjectURL` is present (i.e. under vitest's happy-dom environment).\n */\nfunction patchInlineScriptLoader(): void {\n if (inlineScriptLoaderPatched) return;\n inlineScriptLoaderPatched = true;\n if (typeof URL.createObjectURL === \"function\") {\n (URL as unknown as { createObjectURL: unknown }).createObjectURL = undefined;\n }\n}\n\nconst TEXT_SETTER_PATCHED = Symbol.for(\"wcstack.testing.textSetterPatched\");\n\n/**\n * happy-dom's `textContent` setter treats a numeric `0` as empty (and `innerText`\n * throws on any non-string), whereas browsers stringify: `el.textContent = 0`\n * renders \"0\". A `textContent: count` binding assigns the raw number, so a count\n * that reaches zero would vanish only under happy-dom. Wrap the setters on every\n * prototype that owns one (Node, Element, HTMLElement) to coerce the way the\n * DOM spec does (`null` → \"\", anything else → `String(value)`). Idempotent per\n * prototype, so a fresh window from `installDom()` gets patched too.\n */\nfunction patchTextSetters(): void {\n const targets: Array<[unknown, string]> = [\n [globalThis.Node, \"textContent\"],\n [globalThis.Element, \"textContent\"],\n [globalThis.HTMLElement, \"textContent\"],\n [globalThis.HTMLElement, \"innerText\"],\n ];\n for (const [ctor, property] of targets) {\n const proto = (ctor as { prototype?: object } | undefined)?.prototype as (Record<symbol, unknown> & object) | undefined;\n if (proto === undefined) continue;\n const marker = `${String(TEXT_SETTER_PATCHED)}:${property}`;\n // own-property check: Element.prototype inherits Node.prototype's marker, and an\n // inherited marker must not make the Element setter (the one happy-dom uses) skip.\n if (Object.prototype.hasOwnProperty.call(proto, marker)) continue;\n const descriptor = Object.getOwnPropertyDescriptor(proto, property);\n if (descriptor?.set === undefined) continue;\n const originalSet = descriptor.set;\n Object.defineProperty(proto, property, {\n ...descriptor,\n set(this: unknown, value: unknown) {\n originalSet.call(this, value === null || value === undefined ? \"\" : String(value));\n },\n });\n Object.defineProperty(proto, marker, { value: true, enumerable: false, configurable: true });\n }\n}\n\nasync function defaultBootstraps(): Promise<readonly BootstrapFunction[]> {\n // Lazy: the element classes bind their base class at module evaluation, which\n // must happen after the DOM globals exist (installDom() in bare Node).\n const { bootstrapState } = await import(\"@wcstack/state\");\n return [bootstrapState];\n}\n\nexport async function mount(html: string, options: MountOptions = {}): Promise<MountedApp> {\n patchInlineScriptLoader();\n patchTextSetters();\n\n for (const bootstrap of options.bootstrap ?? (await defaultBootstraps())) {\n await bootstrap();\n }\n\n const stateTagName = options.stateTagName ?? \"wcs-state\";\n let root: Document | ShadowRoot;\n let container: Element;\n if (options.root === \"shadow\") {\n const host = document.createElement(\"div\");\n host.setAttribute(\"data-wcs-testing-host\", \"\");\n const shadow = host.attachShadow({ mode: \"open\" });\n shadow.innerHTML = html;\n document.body.appendChild(host);\n root = shadow;\n container = host;\n } else {\n document.body.innerHTML = html;\n root = document;\n container = document.body;\n }\n\n await waitForReady(root, { maxIterations: options.maxIterations });\n\n return {\n root,\n container,\n state(name = \"default\"): StateHandle {\n const element = [...root.querySelectorAll<StateElementLike>(stateTagName)]\n .find((el) => (el.getAttribute(\"name\") ?? \"default\") === name);\n if (element === undefined) {\n throw new Error(`@wcstack/testing: no <${stateTagName}${name === \"default\" ? \"\" : ` name=\"${name}\"`}> under the mounted root`);\n }\n return {\n element,\n read(fn) {\n let out!: ReturnType<typeof fn>;\n element.createState(\"readonly\", (state) => {\n out = fn(state);\n });\n return out;\n },\n async write(fn) {\n await element.createStateAsync(\"writable\", async (state) => {\n await fn(state);\n });\n },\n };\n },\n unmount() {\n if (root === document) {\n document.body.innerHTML = \"\";\n } else {\n container.remove();\n }\n },\n };\n}\n","/**\n * Let a state write reach the DOM.\n *\n * `@wcstack/state` applies updates on the microtask queue; two microtask turns\n * cover the write → apply chain, and one macrotask (`setTimeout(0)`) covers\n * anything a binding defers (e.g. a `customElements.whenDefined` re-apply).\n * This is exactly the wait the README recipe uses, fixed in one place.\n */\nexport async function settle(): Promise<void> {\n await Promise.resolve();\n await Promise.resolve();\n await new Promise<void>((resolve) => setTimeout(resolve, 0));\n}\n","/**\n * Dispatch a DOM event the way a user action would: bubbling by default, a\n * `CustomEvent` when `detail` is given, a plain `Event` otherwise.\n *\n * Returns what `dispatchEvent` returns (`false` when a handler called\n * `preventDefault()`). Follow with `await settle()` before asserting the DOM.\n */\nexport function fire(target: EventTarget, type: string, init: EventInit & { detail?: unknown } = {}): boolean {\n const { detail, ...eventInit } = init;\n const event = detail !== undefined\n ? new CustomEvent(type, { bubbles: true, ...eventInit, detail })\n : new Event(type, { bubbles: true, ...eventInit });\n return target.dispatchEvent(event);\n}\n","/**\n * Bare Node (no vitest `environment: 'happy-dom'`): create a happy-dom `Window`\n * and install its globals, using the same `installGlobals` `@wcstack/server` runs\n * for SSR. Returns an async restore function that also closes the window.\n *\n * Import order matters: `@wcstack/state`'s element classes pick their base class\n * when the module is evaluated, so `mount()` imports it lazily — after this\n * function has installed `HTMLElement`. Do the same for any other wcstack\n * package you bootstrap (`async () => (await import(\"@wcstack/router\")).bootstrapRouter()`).\n *\n * `happy-dom` is an optional peer: it is only needed here. A `window` can also be\n * passed in (`installDom({ window })`) to skip the import entirely.\n */\n\nimport { installGlobals } from \"@wcstack/server\";\n\nexport interface InstallDomOptions {\n /** `window.location` for the page (default `http://localhost/`). */\n readonly url?: string;\n /** A ready-made happy-dom `Window`; when given, `happy-dom` is not imported. */\n readonly window?: unknown;\n}\n\ninterface HappyDomWindowLike {\n readonly happyDOM: { close(): Promise<void> };\n}\n\nexport async function installDom(options: InstallDomOptions = {}): Promise<() => Promise<void>> {\n const window = options.window ?? (await createWindow(options.url ?? \"http://localhost/\"));\n const restore = installGlobals(window as Parameters<typeof installGlobals>[0]);\n return async () => {\n restore();\n await (window as HappyDomWindowLike).happyDOM.close();\n };\n}\n\nasync function createWindow(url: string): Promise<unknown> {\n let mod: { Window: new (options: { url: string }) => unknown };\n try {\n mod = (await import(\"happy-dom\")) as typeof mod;\n } catch {\n throw new Error(\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\",\n );\n }\n return new mod.Window({ url });\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.esm.js","sources":["../../src/mount.ts","../../src/settle.ts","../../src/fire.ts","../../src/installDom.ts"],"sourcesContent":["/**\n * mount() — the README recipe (\"Testing Your Page\", @wcstack/state) as one call:\n * register the elements, insert the HTML, wait for every element and binding\n * under it, and hand back typed accessors for the page's `<wcs-state>`s.\n *\n * The wait is `@wcstack/server`'s `waitForReady` — the same stabilization loop\n * `renderToString` performs, so `<wcs-router>`'s first route, a `$connectedCallback`\n * that inserts more elements, and `<wcs-state>`'s binding construction are all\n * covered by the one call (docs/app-testing-and-typescript-impl-plan.md D11).\n */\n\nimport { waitForReady } from \"@wcstack/server\";\n\nexport type BootstrapFunction = () => void | Promise<void>;\n\nexport interface MountOptions {\n /**\n * Where to insert the HTML. `\"document\"` (default) replaces `document.body`'s\n * content; `\"shadow\"` puts it inside an open ShadowRoot on a fresh host element\n * appended to `document.body` (bindings then scope to that root).\n */\n readonly root?: \"document\" | \"shadow\";\n /**\n * Element registrations to run before inserting the HTML. Default:\n * `[bootstrapState]` imported lazily from `@wcstack/state`. Add the packages\n * the page uses — `bootstrapRouter`, `bootstrapFetch`, … — as functions or\n * async loaders (`async () => (await import(\"@wcstack/router\")).bootstrapRouter()`);\n * every wcstack bootstrap is idempotent, so calling them per test is safe.\n */\n readonly bootstrap?: readonly BootstrapFunction[];\n /** Tag name of the state element when `bootstrapState({ tagNames })` renamed it (default `wcs-state`). */\n readonly stateTagName?: string;\n /** Passed to `waitForReady` (default 10 stabilization rounds). */\n readonly maxIterations?: number;\n}\n\nexport interface StateHandle {\n /** The `<wcs-state>` element itself. */\n readonly element: HTMLElement;\n /** Read through a readonly proxy — the value `fn` returns is passed back. */\n read<T>(fn: (state: any) => T): T;\n /** Write through a writable proxy, exactly as a handler does. Follow with `await settle()`. */\n write(fn: (state: any) => void | Promise<void>): Promise<void>;\n}\n\nexport interface MountedApp {\n /** The root the HTML lives under: `document` or the ShadowRoot. Query it. */\n readonly root: Document | ShadowRoot;\n /** The node whose children are the mounted HTML: `document.body` or the shadow host. */\n readonly container: Element;\n /**\n * Accessor for the root `<wcs-state>` (v2: one state tree per root — volumes\n * (`mount=`) and `bind-component` elements are not it). Throws if absent.\n */\n state(): StateHandle;\n /** Remove the mounted HTML. */\n unmount(): void;\n}\n\ninterface StateElementLike extends HTMLElement {\n createState(mutability: \"readonly\" | \"writable\", callback: (state: any) => void): void;\n createStateAsync(mutability: \"readonly\" | \"writable\", callback: (state: any) => Promise<void>): Promise<void>;\n}\n\nlet inlineScriptLoaderPatched = false;\n\n/**\n * Route `<wcs-state>` inline `<script type=\"module\">` through the `data:` URL\n * loader. Node cannot import `blob:` URLs, so the browser path (`URL.createObjectURL`)\n * would leave an inline-script state pending forever; the loader falls back to a\n * `data:` URL when `createObjectURL` is absent — the same switch `@wcstack/server`\n * flips for SSR. Applied once per process, only when a browser-style\n * `createObjectURL` is present (i.e. under vitest's happy-dom environment).\n */\nfunction patchInlineScriptLoader(): void {\n if (inlineScriptLoaderPatched) return;\n inlineScriptLoaderPatched = true;\n if (typeof URL.createObjectURL === \"function\") {\n (URL as unknown as { createObjectURL: unknown }).createObjectURL = undefined;\n }\n}\n\nconst TEXT_SETTER_PATCHED = Symbol.for(\"wcstack.testing.textSetterPatched\");\n\n/**\n * happy-dom's `textContent` setter treats a numeric `0` as empty (and `innerText`\n * throws on any non-string), whereas browsers stringify: `el.textContent = 0`\n * renders \"0\". A `textContent: count` binding assigns the raw number, so a count\n * that reaches zero would vanish only under happy-dom. Wrap the setters on every\n * prototype that owns one (Node, Element, HTMLElement) to coerce the way the\n * DOM spec does (`null` → \"\", anything else → `String(value)`). Idempotent per\n * prototype, so a fresh window from `installDom()` gets patched too.\n */\nfunction patchTextSetters(): void {\n const targets: Array<[unknown, string]> = [\n [globalThis.Node, \"textContent\"],\n [globalThis.Element, \"textContent\"],\n [globalThis.HTMLElement, \"textContent\"],\n [globalThis.HTMLElement, \"innerText\"],\n ];\n for (const [ctor, property] of targets) {\n const proto = (ctor as { prototype?: object } | undefined)?.prototype as (Record<symbol, unknown> & object) | undefined;\n if (proto === undefined) continue;\n const marker = `${String(TEXT_SETTER_PATCHED)}:${property}`;\n // own-property check: Element.prototype inherits Node.prototype's marker, and an\n // inherited marker must not make the Element setter (the one happy-dom uses) skip.\n if (Object.prototype.hasOwnProperty.call(proto, marker)) continue;\n const descriptor = Object.getOwnPropertyDescriptor(proto, property);\n if (descriptor?.set === undefined) continue;\n const originalSet = descriptor.set;\n Object.defineProperty(proto, property, {\n ...descriptor,\n set(this: unknown, value: unknown) {\n originalSet.call(this, value === null || value === undefined ? \"\" : String(value));\n },\n });\n Object.defineProperty(proto, marker, { value: true, enumerable: false, configurable: true });\n }\n}\n\nasync function defaultBootstraps(): Promise<readonly BootstrapFunction[]> {\n // Lazy: the element classes bind their base class at module evaluation, which\n // must happen after the DOM globals exist (installDom() in bare Node).\n const { bootstrapState } = await import(\"@wcstack/state\");\n return [bootstrapState];\n}\n\nexport async function mount(html: string, options: MountOptions = {}): Promise<MountedApp> {\n patchInlineScriptLoader();\n patchTextSetters();\n\n for (const bootstrap of options.bootstrap ?? (await defaultBootstraps())) {\n await bootstrap();\n }\n\n const stateTagName = options.stateTagName ?? \"wcs-state\";\n let root: Document | ShadowRoot;\n let container: Element;\n if (options.root === \"shadow\") {\n const host = document.createElement(\"div\");\n host.setAttribute(\"data-wcs-testing-host\", \"\");\n const shadow = host.attachShadow({ mode: \"open\" });\n shadow.innerHTML = html;\n document.body.appendChild(host);\n root = shadow;\n container = host;\n } else {\n document.body.innerHTML = html;\n root = document;\n container = document.body;\n }\n\n await waitForReady(root, { maxIterations: options.maxIterations });\n\n return {\n root,\n container,\n state(...legacyArgs: never[]): StateHandle {\n if (legacyArgs.length > 0) {\n throw new Error(\n `@wcstack/testing: state(name) was removed in v2 — there is one state tree per root. ` +\n `Mount the named state onto the tree (<wcs-state mount=\"...\">) and read it by its path prefix via state().`,\n );\n }\n const element = [...root.querySelectorAll<StateElementLike>(stateTagName)]\n .find((el) => !el.hasAttribute(\"mount\") && !el.hasAttribute(\"bind-component\"));\n if (element === undefined) {\n throw new Error(`@wcstack/testing: no <${stateTagName}> under the mounted root`);\n }\n return {\n element,\n read(fn) {\n let out!: ReturnType<typeof fn>;\n element.createState(\"readonly\", (state) => {\n out = fn(state);\n });\n return out;\n },\n async write(fn) {\n await element.createStateAsync(\"writable\", async (state) => {\n await fn(state);\n });\n },\n };\n },\n unmount() {\n if (root === document) {\n document.body.innerHTML = \"\";\n } else {\n container.remove();\n }\n },\n };\n}\n","/**\n * Let a state write reach the DOM.\n *\n * `@wcstack/state` applies updates on the microtask queue; two microtask turns\n * cover the write → apply chain, and one macrotask (`setTimeout(0)`) covers\n * anything a binding defers (e.g. a `customElements.whenDefined` re-apply).\n * This is exactly the wait the README recipe uses, fixed in one place.\n */\nexport async function settle(): Promise<void> {\n await Promise.resolve();\n await Promise.resolve();\n await new Promise<void>((resolve) => setTimeout(resolve, 0));\n}\n","/**\n * Dispatch a DOM event the way a user action would: bubbling by default, a\n * `CustomEvent` when `detail` is given, a plain `Event` otherwise.\n *\n * Returns what `dispatchEvent` returns (`false` when a handler called\n * `preventDefault()`). Follow with `await settle()` before asserting the DOM.\n */\nexport function fire(target: EventTarget, type: string, init: EventInit & { detail?: unknown } = {}): boolean {\n const { detail, ...eventInit } = init;\n const event = detail !== undefined\n ? new CustomEvent(type, { bubbles: true, ...eventInit, detail })\n : new Event(type, { bubbles: true, ...eventInit });\n return target.dispatchEvent(event);\n}\n","/**\n * Bare Node (no vitest `environment: 'happy-dom'`): create a happy-dom `Window`\n * and install its globals, using the same `installGlobals` `@wcstack/server` runs\n * for SSR. Returns an async restore function that also closes the window.\n *\n * Import order matters: `@wcstack/state`'s element classes pick their base class\n * when the module is evaluated, so `mount()` imports it lazily — after this\n * function has installed `HTMLElement`. Do the same for any other wcstack\n * package you bootstrap (`async () => (await import(\"@wcstack/router\")).bootstrapRouter()`).\n *\n * `happy-dom` is an optional peer: it is only needed here. A `window` can also be\n * passed in (`installDom({ window })`) to skip the import entirely.\n */\n\nimport { installGlobals } from \"@wcstack/server\";\n\nexport interface InstallDomOptions {\n /** `window.location` for the page (default `http://localhost/`). */\n readonly url?: string;\n /** A ready-made happy-dom `Window`; when given, `happy-dom` is not imported. */\n readonly window?: unknown;\n}\n\ninterface HappyDomWindowLike {\n readonly happyDOM: { close(): Promise<void> };\n}\n\nexport async function installDom(options: InstallDomOptions = {}): Promise<() => Promise<void>> {\n const window = options.window ?? (await createWindow(options.url ?? \"http://localhost/\"));\n const restore = installGlobals(window as Parameters<typeof installGlobals>[0]);\n return async () => {\n restore();\n await (window as HappyDomWindowLike).happyDOM.close();\n };\n}\n\nasync function createWindow(url: string): Promise<unknown> {\n let mod: { Window: new (options: { url: string }) => unknown };\n try {\n mod = (await import(\"happy-dom\")) as typeof mod;\n } catch {\n throw new Error(\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\",\n );\n }\n return new mod.Window({ url });\n}\n"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wcstack/testing",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
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
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.esm.js",
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
},
|
|
44
44
|
"license": "MIT",
|
|
45
45
|
"peerDependencies": {
|
|
46
|
-
"@wcstack/server": "^
|
|
47
|
-
"@wcstack/state": "^
|
|
46
|
+
"@wcstack/server": "^2.0.0",
|
|
47
|
+
"@wcstack/state": "^2.0.0",
|
|
48
48
|
"happy-dom": ">=20"
|
|
49
49
|
},
|
|
50
50
|
"peerDependenciesMeta": {
|