@noego/wood 0.6.2 → 0.7.1

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.
Files changed (242) hide show
  1. package/README.md +7 -0
  2. package/dist/browser-test-setup.cjs +29 -0
  3. package/dist/browser-test-setup.cjs.map +1 -0
  4. package/dist/browser-test-setup.d.cts +8 -0
  5. package/dist/browser-test-setup.d.ts +8 -0
  6. package/dist/browser-test-setup.js +9 -0
  7. package/dist/browser-test-setup.js.map +1 -0
  8. package/dist/client/global_polyfill.d.cts +1 -0
  9. package/dist/client/global_polyfill.d.ts +1 -0
  10. package/dist/client/index.cjs +7 -0
  11. package/dist/client/index.cjs.map +1 -1
  12. package/dist/client/index.d.cts +7 -4
  13. package/dist/client/index.d.ts +7 -4
  14. package/dist/client/index.js +6 -1
  15. package/dist/client/index.js.map +1 -1
  16. package/dist/codegen/renderer_app_generator.cjs +97 -24
  17. package/dist/codegen/renderer_app_generator.cjs.map +1 -1
  18. package/dist/codegen/renderer_app_generator.js +97 -24
  19. package/dist/codegen/renderer_app_generator.js.map +1 -1
  20. package/dist/codegen/type_generator.cjs +9 -0
  21. package/dist/codegen/type_generator.cjs.map +1 -1
  22. package/dist/codegen/type_generator.js +9 -0
  23. package/dist/codegen/type_generator.js.map +1 -1
  24. package/dist/config/config_parser.cjs +10 -0
  25. package/dist/config/config_parser.cjs.map +1 -1
  26. package/dist/config/config_parser.js +10 -0
  27. package/dist/config/config_parser.js.map +1 -1
  28. package/dist/fetch.cjs +38 -0
  29. package/dist/fetch.cjs.map +1 -0
  30. package/dist/fetch.d.cts +7 -0
  31. package/dist/fetch.d.ts +7 -0
  32. package/dist/fetch.js +12 -0
  33. package/dist/fetch.js.map +1 -0
  34. package/dist/frontend/frontend_page.cjs +3 -3
  35. package/dist/frontend/frontend_page.cjs.map +1 -1
  36. package/dist/frontend/frontend_page.d.cts +1 -1
  37. package/dist/frontend/frontend_page.d.ts +1 -1
  38. package/dist/frontend/frontend_page.js +3 -3
  39. package/dist/frontend/frontend_page.js.map +1 -1
  40. package/dist/frontend/frontend_surface.cjs +31 -17
  41. package/dist/frontend/frontend_surface.cjs.map +1 -1
  42. package/dist/frontend/frontend_surface.d.cts +12 -2
  43. package/dist/frontend/frontend_surface.d.ts +12 -2
  44. package/dist/frontend/frontend_surface.js +37 -17
  45. package/dist/frontend/frontend_surface.js.map +1 -1
  46. package/dist/frontend/index.d.cts +2 -0
  47. package/dist/frontend/index.d.ts +2 -0
  48. package/dist/index.cjs +5 -0
  49. package/dist/index.cjs.map +1 -1
  50. package/dist/index.d.cts +25 -0
  51. package/dist/index.d.ts +25 -0
  52. package/dist/index.js +3 -0
  53. package/dist/index.js.map +1 -1
  54. package/dist/loader/index.cjs +19 -2
  55. package/dist/loader/index.cjs.map +1 -1
  56. package/dist/loader/index.d.cts +1 -0
  57. package/dist/loader/index.d.ts +1 -0
  58. package/dist/loader/index.js +19 -1
  59. package/dist/loader/index.js.map +1 -1
  60. package/dist/loader/loader_orchestration.cjs +82 -0
  61. package/dist/loader/loader_orchestration.cjs.map +1 -0
  62. package/dist/loader/loader_orchestration.d.cts +66 -0
  63. package/dist/loader/loader_orchestration.d.ts +66 -0
  64. package/dist/loader/loader_orchestration.js +51 -0
  65. package/dist/loader/loader_orchestration.js.map +1 -0
  66. package/dist/loader/loader_runner.cjs +46 -41
  67. package/dist/loader/loader_runner.cjs.map +1 -1
  68. package/dist/loader/loader_runner.d.cts +12 -6
  69. package/dist/loader/loader_runner.d.ts +12 -6
  70. package/dist/loader/loader_runner.js +50 -41
  71. package/dist/loader/loader_runner.js.map +1 -1
  72. package/dist/native_browser.types-BY2daUOS.d.ts +189 -0
  73. package/dist/native_browser.types-vWXtE4rM.d.cts +189 -0
  74. package/dist/navigation/index.cjs +15 -2
  75. package/dist/navigation/index.cjs.map +1 -1
  76. package/dist/navigation/index.d.cts +2 -0
  77. package/dist/navigation/index.d.ts +2 -0
  78. package/dist/navigation/index.js +8 -1
  79. package/dist/navigation/index.js.map +1 -1
  80. package/dist/runtime/get_container.cjs +2 -0
  81. package/dist/runtime/get_container.cjs.map +1 -1
  82. package/dist/runtime/get_container.js +2 -0
  83. package/dist/runtime/get_container.js.map +1 -1
  84. package/dist/runtime/index.cjs +5 -0
  85. package/dist/runtime/index.cjs.map +1 -1
  86. package/dist/runtime/index.d.cts +2 -0
  87. package/dist/runtime/index.d.ts +2 -0
  88. package/dist/runtime/index.js +3 -0
  89. package/dist/runtime/index.js.map +1 -1
  90. package/dist/runtime/runtime_capabilities.cjs +35 -0
  91. package/dist/runtime/runtime_capabilities.cjs.map +1 -0
  92. package/dist/runtime/runtime_capabilities.d.cts +6 -0
  93. package/dist/runtime/runtime_capabilities.d.ts +6 -0
  94. package/dist/runtime/runtime_capabilities.js +11 -0
  95. package/dist/runtime/runtime_capabilities.js.map +1 -0
  96. package/dist/runtime/runtime_module.cjs +2 -0
  97. package/dist/runtime/runtime_module.cjs.map +1 -1
  98. package/dist/runtime/runtime_module.js +2 -0
  99. package/dist/runtime/runtime_module.js.map +1 -1
  100. package/dist/test-wood.cjs +150 -0
  101. package/dist/test-wood.cjs.map +1 -0
  102. package/dist/test-wood.d.cts +62 -0
  103. package/dist/test-wood.d.ts +62 -0
  104. package/dist/test-wood.js +115 -0
  105. package/dist/test-wood.js.map +1 -0
  106. package/dist/testing/browser.cjs +92 -163
  107. package/dist/testing/browser.cjs.map +1 -1
  108. package/dist/testing/browser.d.cts +14 -13
  109. package/dist/testing/browser.d.ts +14 -13
  110. package/dist/testing/browser.js +88 -162
  111. package/dist/testing/browser.js.map +1 -1
  112. package/dist/testing/create_controller_harness.d.cts +2 -1
  113. package/dist/testing/create_controller_harness.d.ts +2 -1
  114. package/dist/testing/create_router_bridge.cjs +19 -15
  115. package/dist/testing/create_router_bridge.cjs.map +1 -1
  116. package/dist/testing/create_router_bridge.d.cts +8 -1
  117. package/dist/testing/create_router_bridge.d.ts +8 -1
  118. package/dist/testing/create_router_bridge.js +18 -15
  119. package/dist/testing/create_router_bridge.js.map +1 -1
  120. package/dist/testing/index.cjs +12 -4
  121. package/dist/testing/index.cjs.map +1 -1
  122. package/dist/testing/index.d.cts +11 -5
  123. package/dist/testing/index.d.ts +11 -5
  124. package/dist/testing/index.js +9 -3
  125. package/dist/testing/index.js.map +1 -1
  126. package/dist/testing/native_browser/browser_pool.cjs +31 -0
  127. package/dist/testing/native_browser/browser_pool.cjs.map +1 -0
  128. package/dist/testing/native_browser/browser_pool.d.cts +1 -0
  129. package/dist/testing/native_browser/browser_pool.d.ts +1 -0
  130. package/dist/testing/native_browser/browser_pool.js +6 -0
  131. package/dist/testing/native_browser/browser_pool.js.map +1 -0
  132. package/dist/testing/native_browser/happy_dom_tab.cjs +107 -0
  133. package/dist/testing/native_browser/happy_dom_tab.cjs.map +1 -0
  134. package/dist/testing/native_browser/happy_dom_tab.d.cts +35 -0
  135. package/dist/testing/native_browser/happy_dom_tab.d.ts +35 -0
  136. package/dist/testing/native_browser/happy_dom_tab.js +73 -0
  137. package/dist/testing/native_browser/happy_dom_tab.js.map +1 -0
  138. package/dist/testing/native_browser/happy_dom_transfer.cjs +29 -0
  139. package/dist/testing/native_browser/happy_dom_transfer.cjs.map +1 -0
  140. package/dist/testing/native_browser/happy_dom_transfer.d.cts +1 -0
  141. package/dist/testing/native_browser/happy_dom_transfer.d.ts +1 -0
  142. package/dist/testing/native_browser/happy_dom_transfer.js +5 -0
  143. package/dist/testing/native_browser/happy_dom_transfer.js.map +1 -0
  144. package/dist/testing/native_browser/host_transport.cjs +89 -0
  145. package/dist/testing/native_browser/host_transport.cjs.map +1 -0
  146. package/dist/testing/native_browser/host_transport.d.cts +44 -0
  147. package/dist/testing/native_browser/host_transport.d.ts +44 -0
  148. package/dist/testing/native_browser/host_transport.js +65 -0
  149. package/dist/testing/native_browser/host_transport.js.map +1 -0
  150. package/dist/testing/native_browser/native_browser.types.cjs +17 -0
  151. package/dist/testing/native_browser/native_browser.types.cjs.map +1 -0
  152. package/dist/testing/native_browser/native_browser.types.d.cts +31 -0
  153. package/dist/testing/native_browser/native_browser.types.d.ts +31 -0
  154. package/dist/testing/native_browser/native_browser.types.js +1 -0
  155. package/dist/testing/native_browser/native_browser.types.js.map +1 -0
  156. package/dist/testing/native_browser/native_browser_environment.cjs +143 -0
  157. package/dist/testing/native_browser/native_browser_environment.cjs.map +1 -0
  158. package/dist/testing/native_browser/native_browser_environment.d.cts +36 -0
  159. package/dist/testing/native_browser/native_browser_environment.d.ts +36 -0
  160. package/dist/testing/native_browser/native_browser_environment.js +109 -0
  161. package/dist/testing/native_browser/native_browser_environment.js.map +1 -0
  162. package/dist/testing/native_browser/native_browser_lifecycle.cjs +31 -0
  163. package/dist/testing/native_browser/native_browser_lifecycle.cjs.map +1 -0
  164. package/dist/testing/native_browser/native_browser_lifecycle.d.cts +1 -0
  165. package/dist/testing/native_browser/native_browser_lifecycle.d.ts +1 -0
  166. package/dist/testing/native_browser/native_browser_lifecycle.js +6 -0
  167. package/dist/testing/native_browser/native_browser_lifecycle.js.map +1 -0
  168. package/dist/testing/native_browser/playwright_tab.cjs +124 -0
  169. package/dist/testing/native_browser/playwright_tab.cjs.map +1 -0
  170. package/dist/testing/native_browser/playwright_tab.d.cts +35 -0
  171. package/dist/testing/native_browser/playwright_tab.d.ts +35 -0
  172. package/dist/testing/native_browser/playwright_tab.js +100 -0
  173. package/dist/testing/native_browser/playwright_tab.js.map +1 -0
  174. package/dist/testing/native_browser/renderer_compilation.cjs +149 -0
  175. package/dist/testing/native_browser/renderer_compilation.cjs.map +1 -0
  176. package/dist/testing/native_browser/renderer_compilation.d.cts +35 -0
  177. package/dist/testing/native_browser/renderer_compilation.d.ts +35 -0
  178. package/dist/testing/native_browser/renderer_compilation.js +115 -0
  179. package/dist/testing/native_browser/renderer_compilation.js.map +1 -0
  180. package/dist/testing/native_browser/renderer_entry.cjs +148 -0
  181. package/dist/testing/native_browser/renderer_entry.cjs.map +1 -0
  182. package/dist/testing/native_browser/renderer_entry.d.cts +13 -0
  183. package/dist/testing/native_browser/renderer_entry.d.ts +13 -0
  184. package/dist/testing/native_browser/renderer_entry.js +114 -0
  185. package/dist/testing/native_browser/renderer_entry.js.map +1 -0
  186. package/dist/testing/registration_only_root.cjs +61 -5
  187. package/dist/testing/registration_only_root.cjs.map +1 -1
  188. package/dist/testing/registration_only_root.d.cts +25 -4
  189. package/dist/testing/registration_only_root.d.ts +25 -4
  190. package/dist/testing/registration_only_root.js +60 -5
  191. package/dist/testing/registration_only_root.js.map +1 -1
  192. package/dist/testing/runner_importer.cjs +75 -0
  193. package/dist/testing/runner_importer.cjs.map +1 -0
  194. package/dist/testing/runner_importer.d.cts +8 -0
  195. package/dist/testing/runner_importer.d.ts +8 -0
  196. package/dist/testing/runner_importer.js +41 -0
  197. package/dist/testing/runner_importer.js.map +1 -0
  198. package/dist/testing/test_wood_config.cjs +407 -273
  199. package/dist/testing/test_wood_config.cjs.map +1 -1
  200. package/dist/testing/test_wood_config.d.cts +18 -123
  201. package/dist/testing/test_wood_config.d.ts +18 -123
  202. package/dist/testing/test_wood_config.js +416 -271
  203. package/dist/testing/test_wood_config.js.map +1 -1
  204. package/dist/testing/wood_config_source.cjs +12 -0
  205. package/dist/testing/wood_config_source.cjs.map +1 -1
  206. package/dist/testing/wood_config_source.d.cts +10 -0
  207. package/dist/testing/wood_config_source.d.ts +10 -0
  208. package/dist/testing/wood_config_source.js +12 -0
  209. package/dist/testing/wood_config_source.js.map +1 -1
  210. package/dist/testing/wood_env_renderer.cjs +31 -4
  211. package/dist/testing/wood_env_renderer.cjs.map +1 -1
  212. package/dist/testing/wood_env_renderer.d.cts +14 -1
  213. package/dist/testing/wood_env_renderer.d.ts +14 -1
  214. package/dist/testing/wood_env_renderer.js +31 -4
  215. package/dist/testing/wood_env_renderer.js.map +1 -1
  216. package/dist/testing/wood_lazy_runtime.cjs +109 -0
  217. package/dist/testing/wood_lazy_runtime.cjs.map +1 -0
  218. package/dist/testing/wood_lazy_runtime.d.cts +36 -0
  219. package/dist/testing/wood_lazy_runtime.d.ts +36 -0
  220. package/dist/testing/wood_lazy_runtime.js +84 -0
  221. package/dist/testing/wood_lazy_runtime.js.map +1 -0
  222. package/dist/testing/wood_test_selection.cjs +34 -55
  223. package/dist/testing/wood_test_selection.cjs.map +1 -1
  224. package/dist/testing/wood_test_selection.d.cts +18 -24
  225. package/dist/testing/wood_test_selection.d.ts +18 -24
  226. package/dist/testing/wood_test_selection.js +30 -54
  227. package/dist/testing/wood_test_selection.js.map +1 -1
  228. package/dist/testing-api.cjs +17 -0
  229. package/dist/testing-api.cjs.map +1 -0
  230. package/dist/testing-api.d.cts +18 -0
  231. package/dist/testing-api.d.ts +18 -0
  232. package/dist/testing-api.js +1 -0
  233. package/dist/testing-api.js.map +1 -0
  234. package/dist/types/config.cjs.map +1 -1
  235. package/dist/types/config.d.cts +4 -0
  236. package/dist/types/config.d.ts +4 -0
  237. package/docs/native-browser-testing.md +164 -0
  238. package/package.json +31 -5
  239. package/src/components/NavigationShell.browser.test.ts +10 -10
  240. package/src/components/NavigationShell.cleanup.browser.test.ts +57 -0
  241. package/src/components/NavigationShell.scopes.browser.test.ts +5 -5
  242. package/src/components/NavigationShell.svelte +50 -24
@@ -0,0 +1,115 @@
1
+ import { fileURLToPath } from "node:url";
2
+ const logs = /* @__PURE__ */ new WeakMap();
3
+ function derive(builder, method, args) {
4
+ const state = logs.get(builder);
5
+ const copied = [...args];
6
+ const optionsIndex = method === "method" ? 3 : 2;
7
+ if (!["select", "use"].includes(method) && copied[optionsIndex] !== void 0) {
8
+ copied[optionsIndex] = Object.freeze({ ...copied[optionsIndex] });
9
+ }
10
+ if (method === "load" && typeof copied[0] !== "string") copied[0] = Object.freeze({ ...copied[0] });
11
+ const child = Object.create(WoodTestBuilder.prototype);
12
+ logs.set(child, {
13
+ config: state.config,
14
+ commands: Object.freeze([...state.commands, Object.freeze({ method, args: Object.freeze(copied) })])
15
+ });
16
+ return Object.freeze(child);
17
+ }
18
+ function plain(value) {
19
+ return !!value && typeof value === "object" && !Array.isArray(value) && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
20
+ }
21
+ function copyOwner(owner, input) {
22
+ if (!plain(input) || Reflect.ownKeys(input).some((key) => key !== "module" && key !== "path") || Reflect.ownKeys(input).length > 1) {
23
+ throw new TypeError(`select({ ${owner} }) takes module OR path, or {}`);
24
+ }
25
+ const value = { ...input };
26
+ if (typeof value.module === "string") value.module = Object.freeze([value.module]);
27
+ else if (Array.isArray(value.module)) value.module = Object.freeze([...value.module]);
28
+ if (Array.isArray(value.path)) value.path = Object.freeze([...value.path]);
29
+ return Object.freeze(value);
30
+ }
31
+ function copySelection(selection) {
32
+ if (!plain(selection) || Reflect.ownKeys(selection).some((key) => key !== "main" && key !== "renderer")) {
33
+ throw new TypeError("select requires { main } and/or { renderer }");
34
+ }
35
+ const out = {};
36
+ if ("main" in selection) out.main = copyOwner("main", selection.main);
37
+ if ("renderer" in selection) out.renderer = copyOwner("renderer", selection.renderer);
38
+ if (!("main" in out) && !("renderer" in out)) throw new TypeError("select requires { main } and/or { renderer }");
39
+ return Object.freeze(out);
40
+ }
41
+ function identity(value, what) {
42
+ if (typeof value !== "string" || !value.trim()) throw new TypeError(`${what} requires a nonempty identity`);
43
+ }
44
+ class WoodTestBuilder {
45
+ constructor() {
46
+ }
47
+ fetch(configure) {
48
+ if (typeof configure !== "function") throw new TypeError("fetch() requires a route configuration callback");
49
+ return derive(this, "fetch", [configure]);
50
+ }
51
+ browser(host = "playwright") {
52
+ if (host !== "playwright" && host !== "happy_dom") throw new TypeError("browser() accepts playwright or happy_dom");
53
+ return derive(this, "browser", [host]);
54
+ }
55
+ select(selection) {
56
+ return derive(this, "select", [copySelection(selection)]);
57
+ }
58
+ use(stub) {
59
+ if (!stub || typeof stub !== "object") throw new TypeError("use() expects a testStub() description");
60
+ return derive(this, "use", [stub]);
61
+ }
62
+ class(target, replacement, options) {
63
+ return derive(this, "class", options === void 0 ? [target, replacement] : [target, replacement, options]);
64
+ }
65
+ function(target, replacement, options) {
66
+ return derive(this, "function", options === void 0 ? [target, replacement] : [target, replacement, options]);
67
+ }
68
+ method(target, method, replacement, options) {
69
+ identity(method, "method()");
70
+ return derive(this, "method", options === void 0 ? [target, method, replacement] : [target, method, replacement, options]);
71
+ }
72
+ value(target, replacement, options) {
73
+ return derive(this, "value", options === void 0 ? [target, replacement] : [target, replacement, options]);
74
+ }
75
+ middleware(identity_, replacement, options) {
76
+ identity(identity_, "middleware()");
77
+ return derive(this, "middleware", options === void 0 ? [identity_, replacement] : [identity_, replacement, options]);
78
+ }
79
+ load(identity_, replacement, options) {
80
+ return derive(this, "load", options === void 0 ? [identity_, replacement] : [identity_, replacement, options]);
81
+ }
82
+ /**
83
+ * Load the implementation and replay the recorded calls in order. Under
84
+ * Vitest the build binds to the project-local runner's module registry so
85
+ * production modules keep the identity the test file imported.
86
+ */
87
+ async build() {
88
+ const state = logs.get(this);
89
+ const implementation = await import("./testing/index.js");
90
+ let builder = implementation.testWood(state.config);
91
+ if (process.env.VITEST) builder = builder.importer(await implementation.vitestModuleImporter(state.config));
92
+ for (const command of state.commands) {
93
+ if (command.method === "use") {
94
+ const { stubEntries } = await import("@noego/testing");
95
+ stubEntries(command.args[0]);
96
+ }
97
+ builder = builder[command.method].apply(builder, [...command.args]);
98
+ }
99
+ const environment = await builder.build();
100
+ const { registerTestResource } = await import("@noego/testing");
101
+ return await registerTestResource(environment, "testWood");
102
+ }
103
+ }
104
+ function testWood(configPath) {
105
+ const config = configPath instanceof URL ? fileURLToPath(configPath) : typeof configPath === "string" && configPath.startsWith("file:") ? fileURLToPath(configPath) : configPath;
106
+ if (typeof config !== "string" || !config.trim()) throw new TypeError("testWood requires an original wood.config.yml path or file URL");
107
+ const builder = Object.create(WoodTestBuilder.prototype);
108
+ logs.set(builder, { config, commands: Object.freeze([]) });
109
+ return Object.freeze(builder);
110
+ }
111
+ export {
112
+ WoodTestBuilder,
113
+ testWood
114
+ };
115
+ //# sourceMappingURL=test-wood.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/test-wood.ts"],"sourcesContent":["import type { ConfigureFetch } from '@noego/testing/fetch';\n/**\n * Production-safe Wood testing facade, exported from the `@noego/wood` root.\n *\n * Importing `@noego/wood` or declaring a test loads no testing, DOM,\n * Playwright or Electron modules: every call is recorded on an immutable\n * builder and replayed onto the real `@noego/wood/testing` implementation\n * only inside `build()`.\n *\n * The canonical surface is exactly:\n *\n * select({ main?, renderer? }) what exists (module | path | {} per owner)\n * use(testStub()) compose a shared replacement description\n * class / function / method / value exact declared tokens\n * middleware(identity, …) the executable behind a source middleware id\n * load(identity, …) the page/layout companion data loader\n * browser([happy_dom]) native Page (default) or happy-dom Window\n * build() no selection = every declared owner, whole\n *\n * Explicit browser() selects a native renderer host. Without it, renderer\n * selection stays in the fast in-process Node controller tier.\n */\nimport { fileURLToPath } from 'node:url';\nimport type {\n ExecutableReplacement,\n LoaderIdentity,\n MethodDescriptor,\n OwnerSelection,\n ClassToken, ReplacementClass,\n ReplacementFunction,\n ReplacementOptions,\n TestStub,\n} from '@noego/testing';\nimport type { TestWoodBuilder } from './testing/index.js';\nimport type { WoodBrowserHost, WoodTestEnvironmentFor } from './testing/native_browser/native_browser.types.js';\n\nexport interface WoodTestSelection {\n readonly main?: OwnerSelection;\n readonly renderer?: OwnerSelection;\n}\n\ntype Method = 'select' | 'use' | 'class' | 'function' | 'method' | 'value' | 'middleware' | 'load' | 'browser' | 'fetch';\ntype Command = Readonly<{ method: Method; args: readonly unknown[] }>;\nconst logs = new WeakMap<object, { config: string; commands: readonly Command[] }>();\n\nfunction derive<H extends WoodBrowserHost | null>(builder: WoodTestBuilder<H>, method: Method, args: readonly unknown[]): WoodTestBuilder<H> {\n const state = logs.get(builder)!;\n const copied = [...args];\n const optionsIndex = method === 'method' ? 3 : 2;\n if (!['select', 'use'].includes(method) && copied[optionsIndex] !== undefined) {\n copied[optionsIndex] = Object.freeze({ ...(copied[optionsIndex] as ReplacementOptions) });\n }\n if (method === 'load' && typeof copied[0] !== 'string') copied[0] = Object.freeze({ ...(copied[0] as object) });\n const child = Object.create(WoodTestBuilder.prototype) as WoodTestBuilder<H>;\n logs.set(child, {\n config: state.config,\n commands: Object.freeze([...state.commands, Object.freeze({ method, args: Object.freeze(copied) })]),\n });\n return Object.freeze(child);\n}\n\nfunction plain(value: unknown): value is Record<string, unknown> {\n return !!value && typeof value === 'object' && !Array.isArray(value)\n && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);\n}\nfunction copyOwner(owner: string, input: unknown): OwnerSelection {\n if (!plain(input)\n || Reflect.ownKeys(input).some(key => key !== 'module' && key !== 'path')\n || Reflect.ownKeys(input).length > 1) {\n throw new TypeError(`select({ ${owner} }) takes module OR path, or {}`);\n }\n const value = { ...(input as Record<string, unknown>) };\n if (typeof value.module === 'string') value.module = Object.freeze([value.module]);\n else if (Array.isArray(value.module)) value.module = Object.freeze([...value.module]);\n if (Array.isArray(value.path)) value.path = Object.freeze([...value.path]);\n return Object.freeze(value) as OwnerSelection;\n}\n\n/** Structural copy only; owner contents are validated by the shared contract at build. */\nfunction copySelection(selection: WoodTestSelection): WoodTestSelection {\n if (!plain(selection)\n || Reflect.ownKeys(selection).some(key => key !== 'main' && key !== 'renderer')) {\n throw new TypeError('select requires { main } and/or { renderer }');\n }\n const out: { main?: OwnerSelection; renderer?: OwnerSelection } = {};\n if ('main' in selection) out.main = copyOwner('main', selection.main);\n if ('renderer' in selection) out.renderer = copyOwner('renderer', selection.renderer);\n if (!('main' in out) && !('renderer' in out)) throw new TypeError('select requires { main } and/or { renderer }');\n return Object.freeze(out);\n}\n\nfunction identity(value: unknown, what: string): void {\n if (typeof value !== 'string' || !value.trim()) throw new TypeError(`${what} requires a nonempty identity`);\n}\n\n/** The root-export builder: a recorder over the `@noego/wood/testing` builder's canonical surface. */\nexport class WoodTestBuilder<H extends WoodBrowserHost | null = null> {\n private constructor() {}\n fetch(configure: ConfigureFetch): WoodTestBuilder<H> {\n if (typeof configure !== 'function') throw new TypeError('fetch() requires a route configuration callback');\n return derive(this, 'fetch', [configure]);\n }\n /** No argument = Playwright/Chromium; happy_dom = a native happy-dom Window. */\n browser(): WoodTestBuilder<'playwright'>;\n browser(host: 'playwright'): WoodTestBuilder<'playwright'>;\n browser(host: 'happy_dom'): WoodTestBuilder<'happy_dom'>;\n browser(host: WoodBrowserHost = 'playwright'): WoodTestBuilder<WoodBrowserHost> {\n if (host !== 'playwright' && host !== 'happy_dom') throw new TypeError('browser() accepts playwright or happy_dom');\n return derive(this, 'browser', [host]) as WoodTestBuilder<WoodBrowserHost>;\n }\n select(selection: WoodTestSelection): WoodTestBuilder<H> { return derive(this, 'select', [copySelection(selection)]); }\n use(stub: TestStub): WoodTestBuilder<H> {\n if (!stub || typeof stub !== 'object') throw new TypeError('use() expects a testStub() description');\n return derive(this, 'use', [stub]);\n }\n class<T>(target: ClassToken<T>, replacement: ReplacementClass<T>, options?: ReplacementOptions): WoodTestBuilder<H> {\n return derive(this, 'class', options === undefined ? [target, replacement] : [target, replacement, options]);\n }\n function(target: unknown, replacement: ReplacementFunction, options?: ReplacementOptions): WoodTestBuilder<H> {\n return derive(this, 'function', options === undefined ? [target, replacement] : [target, replacement, options]);\n }\n method(target: unknown, method: string, replacement: MethodDescriptor, options?: ReplacementOptions): WoodTestBuilder<H> {\n identity(method, 'method()');\n return derive(this, 'method', options === undefined ? [target, method, replacement] : [target, method, replacement, options]);\n }\n value(target: unknown, replacement: unknown, options?: ReplacementOptions): WoodTestBuilder<H> {\n return derive(this, 'value', options === undefined ? [target, replacement] : [target, replacement, options]);\n }\n middleware(identity_: string, replacement: ExecutableReplacement, options?: ReplacementOptions): WoodTestBuilder<H> {\n identity(identity_, 'middleware()');\n return derive(this, 'middleware', options === undefined ? [identity_, replacement] : [identity_, replacement, options]);\n }\n load(identity_: LoaderIdentity, replacement: ExecutableReplacement, options?: ReplacementOptions): WoodTestBuilder<H> {\n return derive(this, 'load', options === undefined ? [identity_, replacement] : [identity_, replacement, options]);\n }\n /**\n * Load the implementation and replay the recorded calls in order. Under\n * Vitest the build binds to the project-local runner's module registry so\n * production modules keep the identity the test file imported.\n */\n async build(): Promise<WoodTestEnvironmentFor<H>> {\n const state = logs.get(this)!;\n const implementation = await import('./testing/index.js');\n let builder: TestWoodBuilder = implementation.testWood(state.config);\n if (process.env.VITEST) builder = builder.importer(await implementation.vitestModuleImporter(state.config));\n for (const command of state.commands) {\n if (command.method === 'use') {\n const { stubEntries } = await import('@noego/testing');\n stubEntries(command.args[0] as TestStub); // reject legacy IoC presets on the canonical root facade\n }\n builder = (builder[command.method] as (...args: unknown[]) => TestWoodBuilder).apply(builder, [...command.args]);\n }\n const environment = await builder.build();\n const { registerTestResource } = await import('@noego/testing');\n return await registerTestResource(environment, 'testWood') as WoodTestEnvironmentFor<H>;\n }\n}\n\nexport function testWood(configPath: string | URL): WoodTestBuilder {\n const config = configPath instanceof URL ? fileURLToPath(configPath)\n : typeof configPath === 'string' && configPath.startsWith('file:') ? fileURLToPath(configPath) : configPath;\n if (typeof config !== 'string' || !config.trim()) throw new TypeError('testWood requires an original wood.config.yml path or file URL');\n const builder = Object.create(WoodTestBuilder.prototype) as WoodTestBuilder;\n logs.set(builder, { config, commands: Object.freeze([]) });\n return Object.freeze(builder);\n}\n"],"mappings":"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","names":[]}
@@ -28,8 +28,11 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
28
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
29
  var browser_exports = {};
30
30
  __export(browser_exports, {
31
- browser: () => browser,
31
+ browserAssertions: () => browserAssertions,
32
32
  createBrowserBridgeFixture: () => createBrowserBridgeFixture,
33
+ createBrowserFixture: () => createBrowserFixture,
34
+ createWoodBrowserResolver: () => createWoodBrowserResolver,
35
+ loadSveltePlugin: () => loadSveltePlugin,
33
36
  startSharedBrowserViteServer: () => startSharedBrowserViteServer
34
37
  });
35
38
  module.exports = __toCommonJS(browser_exports);
@@ -40,6 +43,7 @@ var import_node_path = __toESM(require("node:path"), 1);
40
43
  var import_node_url = require("node:url");
41
44
  var import_tracing = require("../tracing/index.js");
42
45
  var import_trace_testing = require("../trace-testing/index.js");
46
+ var import_renderer_app_generator = require("../codegen/renderer_app_generator.cjs");
43
47
  var import_geometry = require("./geometry.js");
44
48
  const DEFAULT_VIEWPORT = {
45
49
  width: 1280,
@@ -97,6 +101,16 @@ async function waitForBoundedClose(promise, timeoutMs = DEFAULT_RESOURCE_CLOSE_T
97
101
  clearTimeout(timeout);
98
102
  }
99
103
  }
104
+ async function closeRequired(promise, owner) {
105
+ let timeout;
106
+ try {
107
+ await Promise.race([promise, new Promise((_, reject) => {
108
+ timeout = setTimeout(() => reject(new Error(`${owner} cleanup timed out`)), DEFAULT_RESOURCE_CLOSE_TIMEOUT_MS);
109
+ })]);
110
+ } finally {
111
+ if (timeout) clearTimeout(timeout);
112
+ }
113
+ }
100
114
  function channelToManifestEntry(channel, handler) {
101
115
  const pathSegments = channel.split(".").filter(Boolean);
102
116
  const action = pathSegments.length > 0 ? pathSegments[pathSegments.length - 1] : channel;
@@ -256,6 +270,11 @@ function createWoodBrowserResolver(rootDir) {
256
270
  name: "wood-browser-resolver",
257
271
  enforce: "pre",
258
272
  resolveId(source) {
273
+ if (source.startsWith("@noego/wood/")) {
274
+ const resolved = resolvePackageModule(rootDir, source);
275
+ const esm = resolved?.endsWith(".cjs") ? resolved.replace(/\.cjs$/, ".js") : resolved;
276
+ return esm && import_node_fs.default.existsSync(esm) ? esm : null;
277
+ }
259
278
  if (source === "svelte") {
260
279
  return svelteClientEntry;
261
280
  }
@@ -339,14 +358,14 @@ function workerRuntimeKey(input) {
339
358
  ].join("::");
340
359
  }
341
360
  async function createBrowserWorkerRuntime(input) {
342
- let browser2 = null;
361
+ let browser = null;
343
362
  let cacheRoot = null;
344
363
  let ownedServer = null;
345
364
  try {
346
365
  const playwrightBrowser = await loadPlaywright(input.browserName, input.rootDir);
347
366
  if (input.wsEndpoint) {
348
367
  try {
349
- browser2 = await playwrightBrowser.connect(input.wsEndpoint);
368
+ browser = await playwrightBrowser.connect(input.wsEndpoint);
350
369
  } catch (error) {
351
370
  if (!browserConnectWarnings.has(input.wsEndpoint)) {
352
371
  browserConnectWarnings.add(input.wsEndpoint);
@@ -357,8 +376,8 @@ async function createBrowserWorkerRuntime(input) {
357
376
  }
358
377
  }
359
378
  }
360
- if (!browser2) {
361
- browser2 = await playwrightBrowser.launch({ headless: input.headless });
379
+ if (!browser) {
380
+ browser = await playwrightBrowser.launch({ headless: input.headless });
362
381
  }
363
382
  let vite;
364
383
  if (input.viteUrl) {
@@ -378,7 +397,7 @@ async function createBrowserWorkerRuntime(input) {
378
397
  };
379
398
  }
380
399
  return {
381
- browser: browser2,
400
+ browser,
382
401
  vite,
383
402
  rootDir: input.rootDir,
384
403
  key: input.key,
@@ -388,7 +407,7 @@ async function createBrowserWorkerRuntime(input) {
388
407
  };
389
408
  } catch (error) {
390
409
  await Promise.all([
391
- browser2?.close().catch(() => void 0),
410
+ browser?.close().catch(() => void 0),
392
411
  ownedServer?.close().catch(() => void 0)
393
412
  ]);
394
413
  if (cacheRoot) {
@@ -499,7 +518,19 @@ function assertFileExists(filePath, label) {
499
518
  function writeEntry(tempDir, name, source) {
500
519
  import_node_fs.default.mkdirSync(tempDir, { recursive: true });
501
520
  const entryPath = import_node_path.default.join(tempDir, `${name}.ts`);
502
- import_node_fs.default.writeFileSync(entryPath, source);
521
+ import_node_fs.default.writeFileSync(entryPath, `import { unmount as __woodHarnessUnmount } from 'svelte';
522
+ ` + source + `
523
+ let __woodHarnessClosing;
524
+ window.__WOOD_BROWSER_UNMOUNT__ = () => __woodHarnessClosing ??= Promise.resolve().then(async () => {
525
+ const app = window.__WOOD_BROWSER_APP__;
526
+ if (!app) return;
527
+ try { await app.disposeNavigation?.(); }
528
+ finally {
529
+ await __woodHarnessUnmount(app);
530
+ window.__WOOD_BROWSER_APP__ = undefined;
531
+ }
532
+ });
533
+ `);
503
534
  return entryPath;
504
535
  }
505
536
  function writeHtml(tempDir, name, entryPath, cssFiles) {
@@ -532,103 +563,6 @@ function findView(views, routeKey) {
532
563
  }
533
564
  return found;
534
565
  }
535
- function resolveComponentPath(rootDir, componentDir, filePath) {
536
- if (import_node_path.default.isAbsolute(filePath)) return filePath;
537
- const base = componentDir ? resolveFromRoot(rootDir, componentDir) : rootDir;
538
- const componentDirName = componentDir ? import_node_path.default.basename(componentDir) : "";
539
- if (componentDirName && filePath.replace(/\\/g, "/").startsWith(`${componentDirName}/`)) {
540
- return import_node_path.default.resolve(base, filePath.slice(componentDirName.length + 1));
541
- }
542
- return import_node_path.default.resolve(base, filePath);
543
- }
544
- function buildRouteEntrySource(input) {
545
- const { view, rootDir, componentDir, routeKey, route } = input;
546
- const viewPath = resolveComponentPath(rootDir, componentDir, view.viewPath);
547
- const layoutPaths = view.layouts.map((layout) => resolveComponentPath(rootDir, componentDir, layout));
548
- const controllerPath = view.controller ? resolveComponentPath(rootDir, componentDir, view.controller) : null;
549
- const layoutControllerPaths = view.layoutControllers?.map(
550
- (controller) => controller ? resolveComponentPath(rootDir, componentDir, controller) : null
551
- ) ?? [];
552
- const imports = [
553
- `import { mount } from 'svelte';`,
554
- `import NavigationShell from '@noego/wood/navigation-shell';`,
555
- `import { getContainer } from '@noego/wood/client';`,
556
- `import { getNavigation } from '@noego/wood/navigation';`,
557
- `import ViewComponent from '${viteImport(viewPath)}';`,
558
- ...layoutPaths.map((layoutPath, index) => `import Layout${index} from '${viteImport(layoutPath)}';`),
559
- ...controllerPath ? [`import PageController from '${viteImport(controllerPath)}';`] : [],
560
- ...layoutControllerPaths.map(
561
- (controllerPath2, index) => controllerPath2 ? `import LayoutController${index} from '${viteImport(controllerPath2)}';` : ""
562
- ).filter(Boolean)
563
- ];
564
- const layoutControllerClasses = layoutControllerPaths.map(
565
- (controllerPath2, index) => controllerPath2 ? `LayoutController${index}` : "undefined"
566
- );
567
- return `${imports.join("\n")}
568
-
569
- const entry = {
570
- key: ${JSON.stringify(routeKey)},
571
- windowName: ${JSON.stringify(view.windowName)},
572
- pageName: ${JSON.stringify(view.pageName)},
573
- viewPath: ${JSON.stringify(view.viewPath)},
574
- layoutPaths: ${JSON.stringify(view.layouts)},
575
- layoutControllers: ${JSON.stringify(view.layoutControllers ?? [])},
576
- controller: ${JSON.stringify(view.controller)},
577
- layoutControllerClasses: [${layoutControllerClasses.join(", ")}],
578
- controllerClass: ${controllerPath ? "PageController" : "undefined"},
579
- view: ViewComponent,
580
- layouts: [${layoutPaths.map((_, index) => `Layout${index}`).join(", ")}],
581
- };
582
-
583
- function createScopedContainer() {
584
- const root = getContainer();
585
- const scope = root.extend();
586
- return scope;
587
- }
588
-
589
- async function resolveController(ControllerClass) {
590
- if (!ControllerClass) return undefined;
591
- const scope = createScopedContainer();
592
- scope.registerClass?.(ControllerClass);
593
- return await scope.get(ControllerClass);
594
- }
595
-
596
- async function createViewController(nextEntry) {
597
- return await resolveController(nextEntry.controllerClass);
598
- }
599
-
600
- async function createLayoutControllers(nextEntry) {
601
- const controllers = [];
602
- for (const ControllerClass of nextEntry.layoutControllerClasses) {
603
- controllers.push(await resolveController(ControllerClass));
604
- }
605
- return controllers;
606
- }
607
-
608
- function getViewManifestEntry(nextKey) {
609
- return nextKey === entry.key ? entry : null;
610
- }
611
-
612
- const navigation = getNavigation();
613
- navigation.replace(entry.key, ${JSON.stringify(route.params)}, ${JSON.stringify(route.query)});
614
-
615
- const controller = await createViewController(entry);
616
- const layoutControllers = await createLayoutControllers(entry);
617
-
618
- window.__WOOD_BROWSER_APP__ = mount(NavigationShell, {
619
- target: document.getElementById('app'),
620
- props: {
621
- manifest: { [entry.key]: entry },
622
- initialEntry: entry,
623
- initialController: controller,
624
- initialLayoutControllers: layoutControllers,
625
- createViewControllerFn: createViewController,
626
- createLayoutControllersFn: createLayoutControllers,
627
- getViewManifestEntryFn: getViewManifestEntry,
628
- },
629
- });
630
- `;
631
- }
632
566
  function buildGeneratedAppEntrySource(input) {
633
567
  const hasRouteContext = Object.keys(input.route.params).length > 0 || Object.keys(input.route.query).length > 0;
634
568
  const rendererTraceEnable = input.enableRendererTrace ? `try {
@@ -638,14 +572,14 @@ function buildGeneratedAppEntrySource(input) {
638
572
  traceConfiguration.configureEnabled(true);
639
573
  } catch (error) { /* best-effort renderer-trace enable */ }` : "";
640
574
  const navigationApply = hasRouteContext ? `getNavigation().replace(${JSON.stringify(input.routeKey)}, ${JSON.stringify(input.route.params)}, ${JSON.stringify(input.route.query)});` : "";
641
- return `import { app } from '${viteImport(input.generatedApp)}';
575
+ return `import * as generated from '${viteImport(input.generatedApp)}';
642
576
  import { getNavigation } from '${input.navigationModule}';
643
577
  import { getContainer, TraceProviderConfiguration } from '${input.clientModule}';
644
578
  window.__WOOD_BROWSER_DEFAULT_ROUTE__ = ${JSON.stringify(input.routeKey)};
645
- window.__WOOD_BROWSER_APP__ = await app(document.getElementById('app'));
579
+ window.__WOOD_BROWSER_APP__ = await generated.app(document.getElementById('app'), ${JSON.stringify(input.route)});
646
580
  window.__WOOD_BROWSER_NAV__ = getNavigation();
647
581
  ${rendererTraceEnable}
648
- ${navigationApply}
582
+ if (!generated.supportsInitialRoute) { ${navigationApply} }
649
583
  `;
650
584
  }
651
585
  function buildComponentEntrySource(input) {
@@ -1048,7 +982,7 @@ async function captureCoverage(page, selector) {
1048
982
  await cdp.detach().catch(() => void 0);
1049
983
  }
1050
984
  }
1051
- function createExpect(page, outputDir) {
985
+ function browserAssertions(page, outputDir = import_node_path.default.resolve(".wood-browser-output")) {
1052
986
  async function waitForVisible(selector) {
1053
987
  const locator = page.locator(selector).first();
1054
988
  try {
@@ -1369,19 +1303,7 @@ function createSurface(input) {
1369
1303
  page: input.page,
1370
1304
  bridge: input.bridge,
1371
1305
  navigation: createMountedNavigation(input.page),
1372
- expect: createExpect(input.page, input.outputDir),
1373
- locator(selector) {
1374
- return input.page.locator(selector);
1375
- },
1376
- async click(selector) {
1377
- await input.page.locator(selector).click();
1378
- },
1379
- async fill(selector, value) {
1380
- await input.page.locator(selector).fill(value);
1381
- },
1382
- async evaluate(fn) {
1383
- return await input.page.evaluate(fn);
1384
- },
1306
+ assertions: browserAssertions(input.page, input.outputDir),
1385
1307
  destroy: input.destroy
1386
1308
  };
1387
1309
  }
@@ -1418,17 +1340,17 @@ async function openMountPage(input) {
1418
1340
  bridge: mountedBridge,
1419
1341
  outputDir: import_node_path.default.join(input.runtime.rootDir, "test-results", "wood-browser"),
1420
1342
  destroy: async () => {
1421
- await page.evaluate(() => {
1422
- const app = window.__WOOD_BROWSER_APP__;
1423
- if (app && typeof app === "object" && typeof app.$destroy === "function") {
1424
- app.$destroy();
1425
- }
1426
- }).catch(() => void 0);
1343
+ if (!page.isClosed()) await page.evaluate(() => window.__WOOD_BROWSER_UNMOUNT__?.());
1427
1344
  }
1428
1345
  });
1429
1346
  }
1430
- async function createHarness(config = {}) {
1347
+ async function createBrowserFixture(config = {}) {
1431
1348
  const rootDir = normalizeRoot(config.rootDir);
1349
+ const css = (config.css ?? []).map((cssPath) => {
1350
+ const resolved = resolveFromRoot(rootDir, cssPath);
1351
+ assertFileExists(resolved, "CSS file");
1352
+ return resolved;
1353
+ });
1432
1354
  const browserName = config.browser?.browserName ?? "chromium";
1433
1355
  const headless = config.browser?.headless ?? true;
1434
1356
  const workerRuntime = await acquireBrowserWorkerRuntime({
@@ -1437,7 +1359,7 @@ async function createHarness(config = {}) {
1437
1359
  headless
1438
1360
  });
1439
1361
  let released = false;
1440
- let destroyed = false;
1362
+ let destroying;
1441
1363
  let tempDir = null;
1442
1364
  let context = null;
1443
1365
  let page = null;
@@ -1476,11 +1398,6 @@ async function createHarness(config = {}) {
1476
1398
  rootDir,
1477
1399
  transportCleanups: []
1478
1400
  };
1479
- const css = (config.css ?? []).map((cssPath) => {
1480
- const resolved = resolveFromRoot(rootDir, cssPath);
1481
- assertFileExists(resolved, "CSS file");
1482
- return resolved;
1483
- });
1484
1401
  return {
1485
1402
  page,
1486
1403
  ...traceRecorder ? { traceRecorder } : {},
@@ -1495,12 +1412,20 @@ async function createHarness(config = {}) {
1495
1412
  const viewsPath = resolveFromRoot(rootDir, config.viewsConfig);
1496
1413
  const views = await loadViews(viewsPath);
1497
1414
  const view = findView(views, routeKey);
1498
- entrySource = buildRouteEntrySource({
1415
+ const generatedApp = import_node_path.default.join(runtime.tempDir, `route-app-${routeKey.replace(/[^a-zA-Z0-9_-]/g, "-")}.ts`);
1416
+ import_node_fs.default.writeFileSync(generatedApp, (0, import_renderer_app_generator.generateRendererApp)([view], {
1417
+ componentDir: config.componentDir ? resolveFromRoot(rootDir, config.componentDir) : rootDir,
1418
+ outDir: runtime.tempDir,
1419
+ entryViewKey: routeKey
1420
+ }));
1421
+ entrySource = buildGeneratedAppEntrySource({
1422
+ generatedApp,
1499
1423
  routeKey,
1500
- view,
1501
- rootDir,
1502
- componentDir: config.componentDir,
1503
- route
1424
+ route,
1425
+ navigationModule: resolveEsmModule(rootDir, "@noego/wood/navigation"),
1426
+ clientModule: resolveEsmModule(rootDir, "@noego/wood/client"),
1427
+ enableRendererTrace: config.trace?.enabled === true,
1428
+ traceMinLevel: config.trace?.minLevel ?? "debug"
1504
1429
  });
1505
1430
  } else if (config.generatedApp) {
1506
1431
  const generatedApp = resolveFromRoot(rootDir, config.generatedApp);
@@ -1515,7 +1440,7 @@ async function createHarness(config = {}) {
1515
1440
  traceMinLevel: config.trace?.minLevel ?? "debug"
1516
1441
  });
1517
1442
  } else {
1518
- throw new Error("mountRoute requires either viewsConfig or generatedApp in browser.createHarness().");
1443
+ throw new Error("mountRoute requires either viewsConfig or generatedApp in createBrowserFixture().");
1519
1444
  }
1520
1445
  const mounted = await openMountPage({
1521
1446
  runtime,
@@ -1613,34 +1538,38 @@ async function createHarness(config = {}) {
1613
1538
  async flushRendererTraces() {
1614
1539
  await page.evaluate(() => window.__woodBrowserTraceFlush?.()).catch(() => void 0);
1615
1540
  },
1616
- async destroy() {
1617
- if (destroyed) {
1618
- return;
1619
- }
1620
- destroyed = true;
1621
- for (const cleanup of runtime.transportCleanups.splice(0)) {
1622
- try {
1623
- cleanup();
1624
- } catch {
1625
- }
1626
- }
1627
- await Promise.all([
1628
- waitForBoundedClose(page.close()),
1629
- waitForBoundedClose(context.close())
1630
- ]);
1631
- traceRecorder?.disable();
1632
- import_node_fs.default.rmSync(tempDir, { recursive: true, force: true });
1633
- releaseRuntimeOnce();
1541
+ destroy() {
1542
+ return destroying ?? (destroying = Promise.resolve().then(async () => {
1543
+ const errors = [];
1544
+ const attempt = async (action) => {
1545
+ try {
1546
+ await action();
1547
+ } catch (error) {
1548
+ errors.push(error);
1549
+ }
1550
+ };
1551
+ if (!page.isClosed()) await attempt(() => closeRequired(
1552
+ page.evaluate(() => window.__WOOD_BROWSER_UNMOUNT__?.()),
1553
+ "renderer"
1554
+ ));
1555
+ for (const cleanup of runtime.transportCleanups.splice(0)) await attempt(cleanup);
1556
+ await attempt(() => closeRequired(page.close(), "browser page"));
1557
+ await attempt(() => closeRequired(context.close(), "browser context"));
1558
+ await attempt(() => traceRecorder?.disable());
1559
+ await attempt(() => import_node_fs.default.rmSync(tempDir, { recursive: true, force: true }));
1560
+ releaseRuntimeOnce();
1561
+ if (errors.length) throw Object.assign(new Error("Wood browser cleanup failed: " + errors.map((error) => error instanceof Error ? error.message : String(error)).join("; ")), { errors });
1562
+ }));
1634
1563
  }
1635
1564
  };
1636
1565
  }
1637
- const browser = {
1638
- createHarness
1639
- };
1640
1566
  // Annotate the CommonJS export names for ESM import in node:
1641
1567
  0 && (module.exports = {
1642
- browser,
1568
+ browserAssertions,
1643
1569
  createBrowserBridgeFixture,
1570
+ createBrowserFixture,
1571
+ createWoodBrowserResolver,
1572
+ loadSveltePlugin,
1644
1573
  startSharedBrowserViteServer
1645
1574
  });
1646
1575
  //# sourceMappingURL=browser.cjs.map