@noego/wood 0.6.2 → 0.7.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.
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 +387 -272
  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 +396 -270
  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 +28 -3
  211. package/dist/testing/wood_env_renderer.cjs.map +1 -1
  212. package/dist/testing/wood_env_renderer.d.cts +12 -1
  213. package/dist/testing/wood_env_renderer.d.ts +12 -1
  214. package/dist/testing/wood_env_renderer.js +28 -3
  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
@@ -1,9 +1,25 @@
1
+ import { FETCH_RUNTIME } from "@noego/runtime/fetch";
2
+ import { createTestFetch, defineFetchRoutes } from "@noego/testing/fetch";
1
3
  import { disposeControllerLifecycle, guardControllerLifecycleOnce } from "../controller/controller_lifecycle.js";
2
4
  import path from "node:path";
3
- import { ExecutionContext, flattenModule } from "@noego/ioc";
4
- import { testIoc, ENV_REGISTRY } from "@noego/testing";
5
+ import { LoadAs } from "@noego/ioc";
6
+ import { ExecutionContext } from "@noego/ioc";
7
+ import {
8
+ applyTestBindings,
9
+ createTestExecutables,
10
+ describeTestBindings,
11
+ ENV_REGISTRY,
12
+ resolveTestOverlays,
13
+ testIoc,
14
+ TestStub,
15
+ testStub,
16
+ stubEntries
17
+ } from "@noego/testing";
5
18
  import { installWoodRuntime, WoodRouter } from "../runtime/runtime_module.js";
19
+ import { assertDataLoader } from "../loader/loader_orchestration.js";
20
+ import { MiddlewareResolver } from "../middleware/middleware_resolver.js";
6
21
  import { Navigation } from "../navigation/index.js";
22
+ import { createOperationApi, RouterBridgeEvents } from "./create_router_bridge.js";
7
23
  import { TestWoodError, WOOD_FRONTEND_BRIDGE } from "./test_wood.js";
8
24
  import {
9
25
  defaultImporter,
@@ -11,9 +27,10 @@ import {
11
27
  loadWoodConfigSource
12
28
  } from "./wood_config_source.js";
13
29
  import {
30
+ DECLARED_OWNERS,
14
31
  resolveSelection,
15
- selectCatalog,
16
- selectOperations
32
+ selectOperations,
33
+ selectRendererCatalog
17
34
  } from "./wood_test_selection.js";
18
35
  import {
19
36
  buildRendererTier
@@ -24,6 +41,7 @@ import {
24
41
  RegistrationOnlyRoot,
25
42
  TestWoodCleanupError
26
43
  } from "./registration_only_root.js";
44
+ import { LazyRuntimeStage, lazyMember } from "./wood_lazy_runtime.js";
27
45
  const CONTAINER_REGISTRY_SYMBOL = /* @__PURE__ */ Symbol.for("@noego/wood.container.registry");
28
46
  function globalRegistrySize() {
29
47
  const registry = globalThis[CONTAINER_REGISTRY_SYMBOL];
@@ -34,126 +52,6 @@ function tierMissing(tier, member) {
34
52
  `the ${member} is not available: this environment was built without { ${tier} } \u2014 add it to select()`
35
53
  );
36
54
  }
37
- function recordComposer(compose) {
38
- if (typeof compose !== "function") throw new TestWoodError("owner composition expects a callback");
39
- const calls = [];
40
- const root = {
41
- use: (preset) => {
42
- calls.push({ method: "use", args: [preset], flat: false });
43
- return root;
44
- },
45
- classes: (config) => {
46
- calls.push({ method: "classes", args: [config], flat: false });
47
- return root;
48
- },
49
- functions: (config) => {
50
- calls.push({ method: "functions", args: [config], flat: false });
51
- return root;
52
- },
53
- values: (config) => {
54
- calls.push({ method: "values", args: [config], flat: false });
55
- return root;
56
- },
57
- methods: (config) => {
58
- calls.push({ method: "methods", args: [config], flat: false });
59
- return root;
60
- }
61
- };
62
- compose(root);
63
- return calls;
64
- }
65
- function applyCalls(builder, calls) {
66
- let next = builder;
67
- for (const call of [...calls].sort((a, b) => a.order - b.order)) {
68
- next = next[call.method](...call.args);
69
- }
70
- return next;
71
- }
72
- function ownerTokens(builder) {
73
- const tokens = /* @__PURE__ */ new Set();
74
- const visit = (token) => {
75
- if (tokens.has(token)) return;
76
- tokens.add(token);
77
- if (typeof token !== "function") return;
78
- const injected = Reflect.getMetadata?.(/* @__PURE__ */ Symbol.for("ioc:inject:tokens"), token);
79
- const design = Reflect.getMetadata?.("design:paramtypes", token) ?? [];
80
- const indices = /* @__PURE__ */ new Set([...design.keys(), ...injected?.keys() ?? []]);
81
- for (const index of indices) visit(injected?.has(index) ? injected.get(index) : design[index]);
82
- };
83
- for (const write of builder.writes) {
84
- if (write.op === "use") {
85
- for (const registration of flattenModule(write.module)) {
86
- visit(registration.token);
87
- if (registration.kind === "class") visit(registration.implementation);
88
- for (const dependency of registration.dependencies) visit(dependency);
89
- }
90
- } else if (write.op !== "methods") {
91
- visit(write.key);
92
- if (write.op === "classes") visit(write.implementation);
93
- }
94
- }
95
- return tokens;
96
- }
97
- function tokenLabel(token) {
98
- if (typeof token === "function" && token.name) return token.name;
99
- return String(token);
100
- }
101
- function callEntries(call) {
102
- applyCalls(testIoc(), [{ ...call, order: 0 }]);
103
- const config = call.args[0];
104
- if (Array.isArray(config) || config instanceof Map) return [...config];
105
- return Reflect.ownKeys(config).map((key) => [key, config[key]]);
106
- }
107
- function resolveOwnerToken(key, exact, main, renderer, method) {
108
- let token = key;
109
- if (!exact && typeof key === "string") {
110
- const matches = [.../* @__PURE__ */ new Set([...main, ...renderer])].filter(
111
- (candidate) => candidate === key || typeof candidate === "function" && candidate.name === key
112
- );
113
- if (matches.length > 1) {
114
- throw new TestWoodError(`Ambiguous ${method} replacement name "${key}"; use an exact token`);
115
- }
116
- token = matches[0] ?? key;
117
- }
118
- const ownsMain = main.has(token);
119
- const ownsRenderer = renderer.has(token);
120
- if (!ownsMain && !ownsRenderer) {
121
- throw new TestWoodError(
122
- `Unknown ${method} replacement ${tokenLabel(token)} in the selected roots; register it with .main() or .renderer()`
123
- );
124
- }
125
- if (ownsMain && ownsRenderer) {
126
- throw new TestWoodError(
127
- `Ambiguous ${method} replacement ${tokenLabel(token)} belongs to main and renderer; target .main() or .renderer() explicitly`
128
- );
129
- }
130
- return { token, owner: ownsMain ? "main" : "renderer" };
131
- }
132
- function routeCalls(calls, main, renderer, target) {
133
- for (const call of calls) {
134
- if (!call.flat || call.method !== "classes") continue;
135
- const exact = Array.isArray(call.args[0]) || call.args[0] instanceof Map;
136
- for (const [key, implementation] of callEntries(call)) {
137
- const routed = resolveOwnerToken(key, exact || typeof key !== "string", main, renderer, call.method);
138
- (routed.owner === "main" ? main : renderer).add(implementation);
139
- }
140
- }
141
- const result = [];
142
- for (const call of calls) {
143
- if (!call.flat) {
144
- if (target === "main") result.push(call);
145
- continue;
146
- }
147
- const exact = Array.isArray(call.args[0]) || call.args[0] instanceof Map;
148
- for (const [key, value] of callEntries(call)) {
149
- const routed = resolveOwnerToken(key, exact || typeof key !== "string", main, renderer, call.method);
150
- if (routed.owner === target) {
151
- result.push({ ...call, flat: false, args: [[[routed.token, value]]] });
152
- }
153
- }
154
- }
155
- return result;
156
- }
157
55
  async function loadController(source, importer, reference, owner) {
158
56
  const file = owner === "main" ? source.mainControllerFile(reference) : source.rendererControllerFile(reference);
159
57
  const controller = await importDefaultExport(
@@ -218,6 +116,7 @@ function assertNoGlobalContainerLeak(before, source) {
218
116
  }
219
117
  function createComposeContext(source, state, selection, operations, live) {
220
118
  return {
119
+ events: new RouterBridgeEvents(),
221
120
  configPath: source.configPath,
222
121
  configDir: source.configDir,
223
122
  config: source.config,
@@ -225,10 +124,10 @@ function createComposeContext(source, state, selection, operations, live) {
225
124
  operations,
226
125
  importer: state.importer,
227
126
  call: async (operation, input) => {
228
- if (!live.router || !live.env) {
127
+ if (!live.env || !live.router && !live.runtime) {
229
128
  throw new TestWoodError("compose context call() is available from start() onward, not during compose");
230
129
  }
231
- const router = live.router;
130
+ const router = live.router ?? (await live.runtime.main.acquire()).router;
232
131
  return await ExecutionContext.run(
233
132
  live.env.root,
234
133
  () => router.dispatch(operation, input)
@@ -236,34 +135,49 @@ function createComposeContext(source, state, selection, operations, live) {
236
135
  }
237
136
  };
238
137
  }
239
- function createAssemblyDisposer(live, mainRegistrations, rendererRegistrations, controllerScopes) {
138
+ function createAssemblyDisposer(live, mainRegistrations, rendererRegistrations, controllerScopes, network) {
240
139
  let closing = null;
241
- return () => closing ?? (closing = cleanupAll("testWood cleanup failed", [
242
- ["renderer", async () => {
243
- if (live.renderer) await live.renderer.dispose();
244
- }],
245
- ...[...controllerScopes].reverse().map(
246
- (scope, index) => [`controller scope ${controllerScopes.length - index}`, () => scope.dispose()]
247
- ),
248
- ["compose drain", async () => {
249
- await live.composed?.drain?.();
250
- }],
251
- ["main root", async () => {
252
- if (live.env) await disposeRoot(live.env);
253
- else await mainRegistrations.actualRoot.dispose();
254
- }],
255
- ["unbuilt renderer root", async () => {
256
- if (!live.renderer && rendererRegistrations) await rendererRegistrations.actualRoot.dispose();
257
- }],
258
- ["compose external dispose", async () => {
259
- await live.composed?.dispose?.();
260
- }]
261
- ]));
140
+ return () => {
141
+ live.disposed = true;
142
+ return closing ?? (closing = cleanupAll("testWood cleanup failed", [
143
+ // An initialization racing this dispose finishes first (it refuses to
144
+ // complete once `disposed` is set) so every resource it created late is
145
+ // released below. A rollback-triggered dispose skips this: its own
146
+ // stage has already finished and waiting would be a self-await.
147
+ ["runtime settle", async () => {
148
+ if (live.rolledBack || !live.runtime) return;
149
+ await live.runtime.main.settled();
150
+ await live.runtime.renderer?.settled();
151
+ }],
152
+ ["renderer", async () => {
153
+ if (live.renderer) await live.renderer.dispose();
154
+ }],
155
+ ...[...controllerScopes].reverse().map(
156
+ (scope, index) => [`controller scope ${controllerScopes.length - index}`, () => scope.dispose()]
157
+ ),
158
+ ["compose drain", async () => {
159
+ const root = live.env?.root ?? mainRegistrations.actualRoot;
160
+ if (!root.isDisposed()) await ExecutionContext.run(root, () => live.composed?.drain?.());
161
+ }],
162
+ ["main root", async () => {
163
+ if (live.env) await disposeRoot(live.env);
164
+ else await mainRegistrations.actualRoot.dispose();
165
+ }],
166
+ ["unbuilt renderer root", async () => {
167
+ if (!live.renderer && rendererRegistrations) await rendererRegistrations.actualRoot.dispose();
168
+ }],
169
+ ["compose external dispose", async () => {
170
+ await live.composed?.dispose?.();
171
+ }],
172
+ ["external fetch", () => network.dispose()]
173
+ ]));
174
+ };
262
175
  }
263
176
  async function loadAssemblyControllers(inputs) {
264
177
  const main = await loadMainControllers(inputs.source, inputs.operations, inputs.state.importer);
265
- const renderer = inputs.catalog ? await loadRendererControllers(inputs.source, inputs.catalog.all(), inputs.state.importer) : /* @__PURE__ */ new Map();
266
- return { main, renderer };
178
+ const renderer = inputs.catalog && !inputs.state.browserHost ? await loadRendererControllers(inputs.source, inputs.catalog.all(), inputs.state.importer) : /* @__PURE__ */ new Map();
179
+ const loaders = inputs.catalog && !inputs.state.browserHost ? await loadRendererLoaders(inputs.source, inputs.catalog.all(), inputs.state.importer) : /* @__PURE__ */ new Map();
180
+ return { main, renderer, loaders };
267
181
  }
268
182
  function installAssemblyRuntime(inputs, mainControllers) {
269
183
  installWoodRuntime(inputs.mainRegistrations.root, {
@@ -275,14 +189,66 @@ function installAssemblyRuntime(inputs, mainControllers) {
275
189
  configuredRouter.registerControllerLoader(identity, () => Promise.resolve({ default: controller }));
276
190
  }
277
191
  configuredRouter.loadOperations(inputs.operations);
192
+ registerMiddlewareSeams(inputs, configuredRouter);
278
193
  }
279
194
  });
280
195
  }
196
+ function registerMiddlewareSeams(inputs, configuredRouter) {
197
+ const executables = inputs.live.executables;
198
+ if (!executables || !executables.middleware.size) return;
199
+ const sourceResolver = new MiddlewareResolver(inputs.source.mainMiddlewareDir);
200
+ for (const spec of executables.middleware) {
201
+ configuredRouter.registerMiddleware(spec, async (context) => {
202
+ const original = await sourceResolver.resolve(spec);
203
+ return executables.main.replace("middleware", spec, original)(context);
204
+ });
205
+ }
206
+ }
207
+ function selectedMiddleware(operations) {
208
+ const identities = /* @__PURE__ */ new Set();
209
+ for (const operation of operations.values()) for (const spec of operation.middleware) identities.add(spec);
210
+ return [...identities];
211
+ }
212
+ async function loadRendererLoaders(source, views, importer) {
213
+ const loaders = /* @__PURE__ */ new Map();
214
+ for (const view of views) {
215
+ for (const viewPath of [...view.layouts, view.viewPath]) {
216
+ const reference = source.rendererLoaderReference(viewPath);
217
+ if (!reference || loaders.has(reference.identity)) continue;
218
+ const mod = await importer(reference.file);
219
+ if (mod.default === void 0) {
220
+ throw new TestWoodError(`data loader ${reference.identity} (${reference.file}) has no default export`);
221
+ }
222
+ try {
223
+ loaders.set(reference.identity, assertDataLoader(mod.default, `data loader ${reference.identity}`));
224
+ } catch (error) {
225
+ throw new TestWoodError(error instanceof Error ? error.message : String(error));
226
+ }
227
+ }
228
+ }
229
+ return loaders;
230
+ }
231
+ function rendererLoaderReferences(loaders) {
232
+ return [...loaders.keys()].map((identity) => Object.freeze({ path: identity, export: "default" }));
233
+ }
281
234
  async function configureRendererRegistrations(inputs) {
282
235
  const registrations = inputs.rendererRegistrations;
283
236
  if (!registrations) return;
284
- registrations.declare(Navigation);
285
- registrations.declare(WOOD_FRONTEND_BRIDGE);
237
+ registrations.root.registerFunction(FETCH_RUNTIME, () => inputs.network, { owned: false, loadAs: LoadAs.Singleton });
238
+ registrations.declareBinding(Navigation, "singleton");
239
+ registrations.declareBinding(WOOD_FRONTEND_BRIDGE, "singleton");
240
+ const composePath = inputs.source.hookModule(inputs.source.config.renderer.compose);
241
+ if (composePath) {
242
+ const composeRenderer = await importDefaultExport(
243
+ inputs.state.importer,
244
+ composePath,
245
+ "renderer.compose"
246
+ );
247
+ if (typeof composeRenderer !== "function") {
248
+ throw new TestWoodError(`renderer.compose default export must be a function (${composePath})`);
249
+ }
250
+ await ExecutionContext.run(registrations.root, () => composeRenderer(registrations.root));
251
+ }
286
252
  await ExecutionContext.run(
287
253
  registrations.root,
288
254
  () => inputs.live.composed?.configureRendererRoot?.(registrations.root)
@@ -299,62 +265,149 @@ function registerControllersBeforeOverlays(inputs, mainControllers, rendererCont
299
265
  inputs.rendererRegistrations?.declare(controller);
300
266
  }
301
267
  }
302
- function buildAssemblyIoc(inputs) {
303
- const explicitMain = inputs.state.mainCalls.filter((call) => !call.flat);
304
- const mainOwners = /* @__PURE__ */ new Set([
305
- ...inputs.mainRegistrations.owners,
306
- ...ownerTokens(applyCalls(testIoc(), explicitMain))
307
- ]);
308
- const rendererOwners = /* @__PURE__ */ new Set([
309
- ...inputs.rendererRegistrations?.owners ?? [],
310
- ...ownerTokens(applyCalls(testIoc(), inputs.state.rendererCalls))
311
- ]);
312
- const routedMain = routeCalls(inputs.state.mainCalls, mainOwners, rendererOwners, "main");
313
- const routedRenderer = routeCalls(inputs.state.mainCalls, mainOwners, rendererOwners, "renderer");
268
+ function buildAssemblyIoc(inputs, controllers) {
269
+ const catalogs = [{
270
+ owner: "main",
271
+ bindings: describeTestBindings(testIoc().use(inputs.mainRegistrations.module()), [...controllers.main.values()]),
272
+ middleware: selectedMiddleware(inputs.operations),
273
+ loaders: []
274
+ }];
275
+ if (inputs.rendererRegistrations && inputs.catalog) {
276
+ catalogs.push({
277
+ owner: "renderer",
278
+ bindings: describeTestBindings(testIoc().use(inputs.rendererRegistrations.module()), [...controllers.renderer.values()]),
279
+ middleware: [],
280
+ loaders: rendererLoaderReferences(controllers.loaders)
281
+ });
282
+ }
283
+ const overlays = resolveTestOverlays(inputs.state.overlays, catalogs);
284
+ const mainPlan = applyTestBindings(testIoc(), overlays, "main");
285
+ const rendererPlan = applyTestBindings(testIoc(), overlays, "renderer");
314
286
  return {
315
- main: applyCalls(testIoc(), routedMain),
316
- renderer: applyCalls(testIoc(), [...inputs.state.rendererCalls, ...routedRenderer])
287
+ main: mainPlan.builder,
288
+ mainBorrowed: mainPlan.buildOptions.borrowedTokens ?? [],
289
+ renderer: rendererPlan.builder,
290
+ rendererBorrowed: rendererPlan.buildOptions.borrowedTokens ?? [],
291
+ overlays,
292
+ executables: {
293
+ main: createTestExecutables(overlays, "main"),
294
+ renderer: createTestExecutables(overlays, "renderer"),
295
+ middleware: new Set(overlays.filter((overlay) => overlay.owner === "main" && overlay.entry.kind === "middleware").map((overlay) => overlay.entry.target))
296
+ }
297
+ };
298
+ }
299
+ function createRuntimeRollback(inputs) {
300
+ return (error) => {
301
+ if (inputs.live.rolledBack || inputs.live.disposed) return Promise.reject(error);
302
+ inputs.live.rolledBack = true;
303
+ return rollbackEnvironment(error, inputs.dispose, "testWood runtime initialization failed and rollback failed");
317
304
  };
318
305
  }
306
+ function assertRuntimeMayStart(inputs, stage) {
307
+ if (inputs.live.disposed) {
308
+ throw new TestWoodError(`this testWood environment was disposed before its ${stage} runtime was used`);
309
+ }
310
+ }
311
+ function assertRuntimeMayContinue(inputs, stage) {
312
+ if (inputs.live.disposed) {
313
+ throw new TestWoodError(`this testWood environment was disposed while its ${stage} runtime was initializing`);
314
+ }
315
+ }
316
+ async function initializeMainRuntime(inputs) {
317
+ assertRuntimeMayStart(inputs, "main");
318
+ const root = inputs.live.env.root;
319
+ const router = await Promise.resolve(
320
+ ExecutionContext.run(root, () => root.get(WoodRouter))
321
+ );
322
+ inputs.live.router = router;
323
+ assertRuntimeMayContinue(inputs, "main");
324
+ await ExecutionContext.run(root, () => inputs.live.composed?.start?.());
325
+ assertRuntimeMayContinue(inputs, "main");
326
+ assertNoGlobalContainerLeak(inputs.registryBefore, inputs.source);
327
+ return { router };
328
+ }
329
+ async function initializeRendererRuntime(inputs, main, renderer) {
330
+ const { router } = await main.acquire();
331
+ assertRuntimeMayStart(inputs, "renderer");
332
+ const composed = inputs.live.composed;
333
+ const configurePageScope = composed?.configurePageScope?.bind(composed);
334
+ const hooks = {
335
+ events: renderer.events,
336
+ ...configurePageScope ? { configurePageScope } : {}
337
+ };
338
+ const tier = await buildRendererTier({
339
+ source: inputs.source,
340
+ router,
341
+ catalog: inputs.catalog,
342
+ operations: inputs.operations,
343
+ importer: inputs.state.importer,
344
+ hooks,
345
+ dom: null,
346
+ ioc: renderer.ioc,
347
+ borrowedTokens: renderer.borrowedTokens,
348
+ registrations: inputs.rendererRegistrations,
349
+ controllerClasses: renderer.controllerClasses,
350
+ loaders: renderer.loaders,
351
+ loaderModules: renderer.loaderModules
352
+ });
353
+ inputs.live.renderer = tier;
354
+ assertRuntimeMayContinue(inputs, "renderer");
355
+ return tier;
356
+ }
357
+ function createAssemblyRuntime(inputs, renderer) {
358
+ const rollback = createRuntimeRollback(inputs);
359
+ const main = new LazyRuntimeStage(() => initializeMainRuntime(inputs), rollback);
360
+ const rendererStage = renderer ? new LazyRuntimeStage(() => initializeRendererRuntime(inputs, main, renderer), rollback) : null;
361
+ return { main, renderer: rendererStage };
362
+ }
319
363
  async function materializeEnvironment(inputs) {
364
+ inputs.mainRegistrations.root.registerFunction(FETCH_RUNTIME, () => inputs.network, { owned: false, loadAs: LoadAs.Singleton });
320
365
  const controllers = await loadAssemblyControllers(inputs);
321
366
  inputs.live.composed = await runCompose(inputs.mainRegistrations.root, inputs.source, inputs.context);
367
+ assertNoGlobalContainerLeak(inputs.registryBefore, inputs.source);
322
368
  installAssemblyRuntime(inputs, controllers.main);
323
369
  await configureRendererRegistrations(inputs);
324
370
  registerControllersBeforeOverlays(inputs, controllers.main, controllers.renderer);
325
- const ioc = buildAssemblyIoc(inputs);
326
- inputs.live.env = await buildRecordedEnvironment(ioc.main, inputs.mainRegistrations, "main");
371
+ const ioc = buildAssemblyIoc(inputs, controllers);
372
+ inputs.live.executables = ioc.executables;
373
+ inputs.live.env = await buildRecordedEnvironment(ioc.main, inputs.mainRegistrations, "main", ioc.mainBorrowed);
327
374
  const root = inputs.live.env.root;
328
- inputs.live.router = await Promise.resolve(
329
- ExecutionContext.run(root, () => root.get(WoodRouter))
330
- );
331
- await ExecutionContext.run(root, () => inputs.live.composed?.start?.());
332
- assertNoGlobalContainerLeak(inputs.registryBefore, inputs.source);
333
- if (inputs.catalog && inputs.rendererRegistrations) {
334
- inputs.live.renderer = await buildRendererTier({
335
- source: inputs.source,
336
- router: inputs.live.router,
337
- catalog: inputs.catalog,
338
- operations: inputs.operations,
339
- importer: inputs.state.importer,
340
- hooks: inputs.live.composed,
341
- dom: inputs.selection.dom,
342
- ioc: ioc.renderer,
343
- registrations: inputs.rendererRegistrations,
344
- controllerClasses: controllers.renderer
345
- });
346
- }
347
- return environmentSurface({
375
+ const renderer = inputs.catalog && inputs.rendererRegistrations ? {
376
+ ioc: ioc.renderer,
377
+ borrowedTokens: ioc.rendererBorrowed,
378
+ controllerClasses: controllers.renderer,
379
+ events: inputs.live.composed?.events ?? inputs.context.events,
380
+ loaders: ioc.executables.renderer,
381
+ loaderModules: controllers.loaders
382
+ } : null;
383
+ const runtime = createAssemblyRuntime(inputs, renderer);
384
+ inputs.live.runtime = runtime;
385
+ const environment = environmentSurface({
348
386
  root,
387
+ network: inputs.network,
349
388
  env: inputs.live.env,
350
- router: inputs.live.router,
351
389
  operations: inputs.operations,
352
- renderer: inputs.live.renderer,
390
+ runtime,
391
+ allOperations: inputs.source.operations,
392
+ events: renderer?.events ?? inputs.live.composed?.events ?? inputs.context.events,
393
+ dom: false,
353
394
  controllerScopes: inputs.controllerScopes,
395
+ executables: ioc.executables,
354
396
  dispose: inputs.dispose
355
397
  });
398
+ if (inputs.state.browserHost) {
399
+ const { createNativeBrowserEnvironment } = await import("./native_browser/native_browser_environment.js");
400
+ return createNativeBrowserEnvironment({
401
+ source: inputs.source,
402
+ views: inputs.catalog.all(),
403
+ environment,
404
+ network: inputs.network,
405
+ events: () => inputs.live.composed?.events ?? inputs.context.events
406
+ }, inputs.state.browserHost);
407
+ }
408
+ return environment;
356
409
  }
357
- async function rollbackEnvironment(error, dispose) {
410
+ async function rollbackEnvironment(error, dispose, message = "testWood build failed and rollback failed") {
358
411
  try {
359
412
  await dispose();
360
413
  } catch (cleanupError) {
@@ -362,35 +415,48 @@ async function rollbackEnvironment(error, dispose) {
362
415
  ...error instanceof TestWoodCleanupError ? error.failures : [],
363
416
  ...cleanupError instanceof TestWoodCleanupError ? cleanupError.failures : [{ owner: "build rollback", error: cleanupError }]
364
417
  ];
365
- throw new TestWoodCleanupError("testWood build failed and rollback failed", failures, error);
418
+ throw new TestWoodCleanupError(message, failures, error);
366
419
  }
367
420
  throw error;
368
421
  }
369
- async function assembleEnvironment(state, selection) {
422
+ async function assembleEnvironment(state, selected) {
370
423
  const source = loadWoodConfigSource(state.configPath);
424
+ let selection = selected ?? resolveSelection(
425
+ Object.fromEntries(DECLARED_OWNERS.filter((owner) => source.config[owner]).map((owner) => [owner, {}]))
426
+ );
371
427
  const operations = selectOperations(source.operations, selection.main);
372
- const catalog = selection.pages ? selectCatalog(source.views(), selection.pages) : null;
373
- if (!catalog && state.rendererCalls.length) {
374
- throw new TestWoodError(".renderer() composition requires a selected pages tier");
428
+ const catalog = selectRendererCatalog(source.views, selection);
429
+ if (state.browserHost && !catalog) {
430
+ throw new TestWoodError("browser() requires a selected renderer owner; add renderer to select()");
375
431
  }
432
+ if (state.browserHost && stubEntries(state.overlays).some((entry) => entry.kind === "load" || entry.owner === "renderer")) {
433
+ throw new TestWoodError("Native browser renderer replacements are not supported yet: renderer code executes in its own realm. Keep main-process seams, or use the existing in-process renderer test environment for renderer .class/.method/.value/.load seams.");
434
+ }
435
+ const network = createTestFetch(state.fetchConfigurations.flatMap(defineFetchRoutes));
376
436
  const registryBefore = globalRegistrySize();
377
- const mainRegistrations = new RegistrationOnlyRoot("main.testing.compose");
378
- const rendererRegistrations = catalog ? new RegistrationOnlyRoot("configureRendererRoot") : null;
437
+ const mainRegistrations = new RegistrationOnlyRoot("main.testing.compose", source.configPath);
438
+ const rendererRegistrations = catalog && !state.browserHost ? new RegistrationOnlyRoot("configureRendererRoot", source.configPath) : null;
379
439
  const controllerScopes = [];
380
440
  const live = {
381
441
  composed: null,
382
442
  env: null,
383
443
  renderer: null,
384
- router: null
444
+ router: null,
445
+ executables: null,
446
+ runtime: null,
447
+ disposed: false,
448
+ rolledBack: false
385
449
  };
386
450
  const context = createComposeContext(source, state, selection, operations, live);
387
451
  const dispose = createAssemblyDisposer(
388
452
  live,
389
453
  mainRegistrations,
390
454
  rendererRegistrations,
391
- controllerScopes
455
+ controllerScopes,
456
+ network
392
457
  );
393
458
  const inputs = {
459
+ network,
394
460
  state,
395
461
  selection,
396
462
  source,
@@ -410,24 +476,52 @@ async function assembleEnvironment(state, selection) {
410
476
  return rollbackEnvironment(error, dispose);
411
477
  }
412
478
  }
479
+ const OPEN_FIRST = "await open(page) (or another renderer call) first";
413
480
  function environmentSurface(parts) {
414
- const { root, env, router, operations, renderer, controllerScopes, dispose } = parts;
415
- const dom = () => renderer?.dom ?? tierMissing("dom", "dom driver");
416
- const pages = () => renderer ?? tierMissing("pages", "renderer");
481
+ const { root, env, operations, allOperations, runtime, events, dom: domSelected, controllerScopes, executables, dispose } = parts;
482
+ const pages = () => runtime.renderer ?? tierMissing("pages", "renderer");
483
+ const dom = async () => {
484
+ if (!domSelected) tierMissing("dom", "dom driver");
485
+ return (await pages().acquire()).dom;
486
+ };
487
+ const router = lazyMember(
488
+ "router",
489
+ runtime.main.map((main) => main.router),
490
+ ["dispatch"],
491
+ "await call()/get()/controller() first"
492
+ );
493
+ const frontend = runtime.renderer ? lazyMember(
494
+ "frontend",
495
+ runtime.renderer.map((tier) => tier.frontend),
496
+ ["open", "act", "settle", "dispose"],
497
+ OPEN_FIRST
498
+ ) : null;
499
+ const rendererRoot = runtime.renderer ? lazyMember(
500
+ "rendererRoot",
501
+ runtime.renderer.map((tier) => tier.rendererRoot),
502
+ ["get", "dispose"],
503
+ OPEN_FIRST
504
+ ) : null;
505
+ const dispatchOperation = async (operation, input) => {
506
+ if (!operations.has(operation)) {
507
+ throw new TestWoodError(
508
+ `operation "${operation}" is not in the selected aperture; available operations:
509
+ ${[...operations.keys()].join("\n ")}`
510
+ );
511
+ }
512
+ const { router: live } = await runtime.main.acquire();
513
+ return await ExecutionContext.run(root, () => live.dispatch(operation, input));
514
+ };
515
+ const api = createOperationApi(allOperations.keys(), dispatchOperation);
417
516
  return {
418
517
  root,
419
518
  router,
420
519
  operations,
421
- call: async (operation, input) => {
422
- if (!operations.has(operation)) {
423
- throw new TestWoodError(
424
- `operation "${operation}" is not in the selected aperture; available operations:
425
- ${[...operations.keys()].join("\n ")}`
426
- );
427
- }
428
- return await ExecutionContext.run(root, () => router.dispatch(operation, input));
429
- },
520
+ api,
521
+ rawApi: api,
522
+ call: dispatchOperation,
430
523
  controller: async (controller) => {
524
+ await runtime.main.acquire();
431
525
  const scope = root.extend();
432
526
  const ownsController = !root.isRegistered(controller);
433
527
  let instance;
@@ -442,41 +536,54 @@ function environmentSurface(parts) {
442
536
  guardControllerLifecycleOnce(instance);
443
537
  return instance;
444
538
  },
445
- get: async (token) => await root.get(token),
539
+ get: async (token) => {
540
+ await runtime.main.acquire();
541
+ return await ExecutionContext.run(root, () => root.get(token));
542
+ },
446
543
  get frontend() {
447
- return pages().frontend;
544
+ return frontend ?? tierMissing("pages", "renderer");
448
545
  },
449
546
  get events() {
450
- return pages().events;
547
+ return events ?? tierMissing("pages", "renderer");
451
548
  },
452
549
  get rendererRoot() {
453
- return pages().rendererRoot;
550
+ return rendererRoot ?? tierMissing("pages", "renderer");
551
+ },
552
+ open: async (page, params, query) => (await pages().acquire()).open(page, params, query),
553
+ navigate: async (page, params, query) => (await pages().acquire()).navigate(page, params, query),
554
+ current: () => pages().current?.current() ?? null,
555
+ settle: async () => {
556
+ await parts.network.settle();
557
+ if (runtime.main.requested) await (await runtime.main.acquire()).router.drain();
558
+ if (runtime.renderer?.requested) await (await runtime.renderer.acquire()).settle();
454
559
  },
455
- open: async (page, params, query) => pages().open(page, params, query),
456
- navigate: async (page, params, query) => pages().navigate(page, params, query),
457
- current: () => pages().current(),
458
- settle: async () => pages().settle(),
459
560
  get target() {
460
- return dom().target;
561
+ if (!domSelected) tierMissing("dom", "dom driver");
562
+ const tier = pages().current;
563
+ if (!tier) throw new TestWoodError("no page is mounted; open(page) first");
564
+ return tier.dom.target;
461
565
  },
462
- click: async (selector) => dom().click(selector),
463
- type: async (selector, text) => dom().type(selector, text),
464
- submit: async (selector) => dom().submit(selector),
465
- text: async (selector) => dom().text(selector),
466
- exists: async (selector) => dom().exists(selector),
566
+ click: async (selector) => (await dom()).click(selector),
567
+ type: async (selector, text) => (await dom()).type(selector, text),
568
+ submit: async (selector) => (await dom()).submit(selector),
569
+ text: async (selector) => (await dom()).text(selector),
570
+ exists: async (selector) => (await dom()).exists(selector),
467
571
  // Method expectations only: `settle()` is the completion contract for
468
572
  // owned renderer work, and a test may legitimately verify while a
469
573
  // deliberately deferred stream is still in flight.
470
574
  verify: async () => {
471
575
  const failures = [];
472
576
  await env.verify().catch((error) => failures.push({ owner: "main", error }));
577
+ await executables.main.verify().catch((error) => failures.push({ owner: "main middleware", error }));
578
+ const renderer = runtime.renderer?.current ?? null;
473
579
  if (renderer) {
474
580
  await renderer.verify().catch((error) => failures.push({ owner: "renderer", error }));
475
581
  }
582
+ await executables.renderer.verify().catch((error) => failures.push({ owner: "renderer loaders", error }));
476
583
  if (failures.length === 1) throw failures[0].error;
477
584
  if (failures.length) {
478
585
  throw new TestWoodError(
479
- "testWood verification failed in both roots:\n " + failures.map(({ owner, error }) => `${owner}: ${error instanceof Error ? error.message : String(error)}`).join("\n ")
586
+ "testWood verification failed in several owners:\n " + failures.map(({ owner, error }) => `${owner}: ${error instanceof Error ? error.message : String(error)}`).join("\n ")
480
587
  );
481
588
  }
482
589
  },
@@ -495,60 +602,79 @@ class TestWoodBuilder {
495
602
  return new TestWoodBuilder({
496
603
  configPath: path.resolve(configPath),
497
604
  selection: null,
498
- mainCalls: [],
499
- rendererCalls: [],
500
- importer: defaultImporter
605
+ browserHost: null,
606
+ fetchConfigurations: [],
607
+ importer: defaultImporter,
608
+ overlays: testStub()
501
609
  });
502
610
  }
503
- /** Declare which tiers exist: `main`, `pages`, `dom`. Replaces any earlier selection. */
611
+ fetch(configure) {
612
+ if (typeof configure !== "function") throw new TypeError("fetch() requires a route configuration callback");
613
+ return this.derive({ fetchConfigurations: Object.freeze([...this.state.fetchConfigurations, configure]) });
614
+ }
615
+ browser(host = "playwright") {
616
+ if (host !== "playwright" && host !== "happy_dom") throw new TestWoodError("browser() accepts playwright or happy_dom");
617
+ return new TestWoodBuilder({ ...this.state, browserHost: host });
618
+ }
619
+ /** Declare selected tiers, snapshotting the source selector at call time. */
504
620
  select(selection) {
505
- resolveSelection(selection);
506
- return this.derive({ selection });
621
+ const resolved = resolveSelection(selection);
622
+ return this.derive({ selection: Object.freeze({
623
+ ...resolved.main !== null ? { main: resolved.main } : {},
624
+ ...resolved.renderer !== null ? { renderer: resolved.renderer } : {}
625
+ }) });
507
626
  }
508
627
  /** Route hook/controller imports through a caller-owned importer (escape hatch). */
509
628
  importer(importer) {
510
629
  if (typeof importer !== "function") throw new TestWoodError("importer() expects a function");
511
630
  return this.derive({ importer });
512
631
  }
513
- /** Explicitly compose or replace dependencies on the main-process root. */
514
- main(compose) {
515
- return this.append("main", recordComposer(compose));
516
- }
517
- /** Explicitly compose or replace dependencies on the renderer root. */
518
- renderer(compose) {
519
- return this.append("renderer", recordComposer(compose));
520
- }
521
- /** Reusable presets retain the historical main-root default. */
632
+ /** Compose a reusable canonical testStub() replacement description. */
522
633
  use(preset) {
523
- return this.append("main", [{ method: "use", args: [preset], flat: false }]);
524
- }
525
- /** Exact-token replacement routed to the sole declared selected owner. */
526
- classes(config) {
527
- return this.append("main", [{ method: "classes", args: [config], flat: true }]);
528
- }
529
- /** Exact-token replacement routed to the sole declared selected owner. */
530
- functions(config) {
531
- return this.append("main", [{ method: "functions", args: [config], flat: true }]);
532
- }
533
- /** Exact-token replacement routed to the sole declared selected owner. */
534
- values(config) {
535
- return this.append("main", [{ method: "values", args: [config], flat: true }]);
536
- }
537
- /** Exact-token method seams routed to the sole declared selected owner. */
538
- methods(config) {
539
- return this.append("main", [{ method: "methods", args: [config], flat: true }]);
540
- }
634
+ if (!(preset instanceof TestStub)) throw new TestWoodError("use() requires testStub()");
635
+ return this.derive({ overlays: this.state.overlays.use(preset) });
636
+ }
637
+ /* -- canonical singular replacements (shared @noego/testing engine) ------ */
638
+ /** Replace the class bound to an exact declared token; the declared lifetime is kept. */
639
+ class(target, replacement, options) {
640
+ return this.derive({ overlays: this.state.overlays.class(target, replacement, options) });
641
+ }
642
+ /** Replace a declared factory/provider (dependencies and lifetime kept) or a callable value. */
643
+ function(target, replacement, options) {
644
+ return this.derive({ overlays: this.state.overlays.function(target, replacement, options) });
645
+ }
646
+ /** A test.* method seam on the FINAL binding of an exact declared token. */
647
+ method(target, method, replacement, options) {
648
+ return this.derive({ overlays: this.state.overlays.method(target, method, replacement, options) });
649
+ }
650
+ /** Provide a caller-owned value for an exact declared token (never disposed by the app). */
651
+ value(target, replacement, options) {
652
+ return this.derive({ overlays: this.state.overlays.value(target, replacement, options) });
653
+ }
654
+ /**
655
+ * Replace the executable satisfying a production middleware identity
656
+ * (`file.path:selector` as written in the operations source). Membership,
657
+ * order and inheritance always come from the source.
658
+ */
659
+ middleware(identity, replacement, options) {
660
+ return this.derive({ overlays: this.state.overlays.middleware(identity, replacement, options) });
661
+ }
662
+ /**
663
+ * Replace the DATA LOADER behind a selected page/layout: the companion
664
+ * `.load` source of a view (`pages/chat.svelte` → `pages/chat.load.ts`).
665
+ * The identity is the config-relative source path (`ui/pages/chat.load.ts`),
666
+ * export `default`; the executable receives the loader `RequestData`
667
+ * (`url` = page identity, `params`, `query`) and yields the data the
668
+ * controller's `initialize({ params, query, data })` receives. A source
669
+ * with no companion loader is not a declared identity (build error).
670
+ * Controllers are replaced with `.class`, never here.
671
+ */
672
+ load(identity, replacement, options) {
673
+ return this.derive({ overlays: this.state.overlays.load(identity, replacement, options) });
674
+ }
675
+ /** Build; without `select()`, every owner the config declares is selected whole. */
541
676
  async build() {
542
- if (!this.state.selection) {
543
- throw new TestWoodError("select() a tier before build(): { main }, { pages }, or both");
544
- }
545
- return assembleEnvironment(this.state, resolveSelection(this.state.selection));
546
- }
547
- append(owner, calls) {
548
- const start = this.state.mainCalls.length + this.state.rendererCalls.length;
549
- const ordered = calls.map((call, index) => ({ ...call, order: start + index }));
550
- applyCalls(testIoc(), ordered);
551
- return owner === "main" ? this.derive({ mainCalls: [...this.state.mainCalls, ...ordered] }) : this.derive({ rendererCalls: [...this.state.rendererCalls, ...ordered] });
677
+ return await assembleEnvironment(this.state, this.state.selection ? resolveSelection(this.state.selection) : null);
552
678
  }
553
679
  derive(patch) {
554
680
  return new TestWoodBuilder({ ...this.state, ...patch });