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