@noego/wood 0.4.6 → 0.4.7

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 (241) hide show
  1. package/bin/wood.js +6 -1
  2. package/dist/client/app_env.cjs +39 -0
  3. package/dist/client/app_env.cjs.map +1 -0
  4. package/dist/client/app_env.d.cts +7 -0
  5. package/dist/client/app_env.d.ts +7 -0
  6. package/dist/client/app_env.js +14 -0
  7. package/dist/client/app_env.js.map +1 -0
  8. package/dist/client/index.cjs +33 -9
  9. package/dist/client/index.cjs.map +1 -1
  10. package/dist/client/index.d.cts +5 -1
  11. package/dist/client/index.d.ts +5 -1
  12. package/dist/client/index.js +32 -9
  13. package/dist/client/index.js.map +1 -1
  14. package/dist/codegen/channel_map_generator.cjs +102 -0
  15. package/dist/codegen/channel_map_generator.cjs.map +1 -0
  16. package/dist/codegen/channel_map_generator.d.cts +6 -0
  17. package/dist/codegen/channel_map_generator.d.ts +6 -0
  18. package/dist/codegen/channel_map_generator.js +78 -0
  19. package/dist/codegen/channel_map_generator.js.map +1 -0
  20. package/dist/codegen/controller_registration_generator.cjs +6 -1
  21. package/dist/codegen/controller_registration_generator.cjs.map +1 -1
  22. package/dist/codegen/controller_registration_generator.js +6 -1
  23. package/dist/codegen/controller_registration_generator.js.map +1 -1
  24. package/dist/codegen/generated_file_notice.cjs +40 -0
  25. package/dist/codegen/generated_file_notice.cjs.map +1 -0
  26. package/dist/codegen/generated_file_notice.d.cts +3 -0
  27. package/dist/codegen/generated_file_notice.d.ts +3 -0
  28. package/dist/codegen/generated_file_notice.js +16 -0
  29. package/dist/codegen/generated_file_notice.js.map +1 -0
  30. package/dist/codegen/index.cjs +3 -0
  31. package/dist/codegen/index.cjs.map +1 -1
  32. package/dist/codegen/index.d.cts +1 -0
  33. package/dist/codegen/index.d.ts +1 -0
  34. package/dist/codegen/index.js +2 -0
  35. package/dist/codegen/index.js.map +1 -1
  36. package/dist/codegen/preload_generator.cjs +97 -1
  37. package/dist/codegen/preload_generator.cjs.map +1 -1
  38. package/dist/codegen/preload_generator.js +97 -1
  39. package/dist/codegen/preload_generator.js.map +1 -1
  40. package/dist/codegen/renderer_app_generator.cjs +6 -1
  41. package/dist/codegen/renderer_app_generator.cjs.map +1 -1
  42. package/dist/codegen/renderer_app_generator.js +6 -1
  43. package/dist/codegen/renderer_app_generator.js.map +1 -1
  44. package/dist/codegen/renderer_controller_registration_generator.cjs +6 -1
  45. package/dist/codegen/renderer_controller_registration_generator.cjs.map +1 -1
  46. package/dist/codegen/renderer_controller_registration_generator.js +6 -1
  47. package/dist/codegen/renderer_controller_registration_generator.js.map +1 -1
  48. package/dist/codegen/renderer_view_manifest_generator.cjs +6 -1
  49. package/dist/codegen/renderer_view_manifest_generator.cjs.map +1 -1
  50. package/dist/codegen/renderer_view_manifest_generator.js +6 -1
  51. package/dist/codegen/renderer_view_manifest_generator.js.map +1 -1
  52. package/dist/codegen/runtime_generator.cjs +6 -1
  53. package/dist/codegen/runtime_generator.cjs.map +1 -1
  54. package/dist/codegen/runtime_generator.js +6 -1
  55. package/dist/codegen/runtime_generator.js.map +1 -1
  56. package/dist/codegen/test_mock_generator.cjs +5 -1
  57. package/dist/codegen/test_mock_generator.cjs.map +1 -1
  58. package/dist/codegen/test_mock_generator.js +5 -1
  59. package/dist/codegen/test_mock_generator.js.map +1 -1
  60. package/dist/codegen/type_generator.cjs +6 -1
  61. package/dist/codegen/type_generator.cjs.map +1 -1
  62. package/dist/codegen/type_generator.js +6 -1
  63. package/dist/codegen/type_generator.js.map +1 -1
  64. package/dist/index.cjs +19 -6
  65. package/dist/index.cjs.map +1 -1
  66. package/dist/index.d.cts +7 -1
  67. package/dist/index.d.ts +7 -1
  68. package/dist/index.js +13 -1
  69. package/dist/index.js.map +1 -1
  70. package/dist/main/index.cjs +133 -55
  71. package/dist/main/index.cjs.map +1 -1
  72. package/dist/main/index.d.cts +30 -2
  73. package/dist/main/index.d.ts +30 -2
  74. package/dist/main/index.js +134 -55
  75. package/dist/main/index.js.map +1 -1
  76. package/dist/main/renderer_trace_drain_coordinator.cjs +127 -0
  77. package/dist/main/renderer_trace_drain_coordinator.cjs.map +1 -0
  78. package/dist/main/renderer_trace_drain_coordinator.d.cts +46 -0
  79. package/dist/main/renderer_trace_drain_coordinator.d.ts +46 -0
  80. package/dist/main/renderer_trace_drain_coordinator.js +101 -0
  81. package/dist/main/renderer_trace_drain_coordinator.js.map +1 -0
  82. package/dist/router/wood_router.cjs +9 -1
  83. package/dist/router/wood_router.cjs.map +1 -1
  84. package/dist/router/wood_router.d.cts +1 -1
  85. package/dist/router/wood_router.d.ts +1 -1
  86. package/dist/router/wood_router.js +9 -1
  87. package/dist/router/wood_router.js.map +1 -1
  88. package/dist/runtime/app_env.cjs +52 -0
  89. package/dist/runtime/app_env.cjs.map +1 -0
  90. package/dist/runtime/app_env.d.cts +10 -0
  91. package/dist/runtime/app_env.d.ts +10 -0
  92. package/dist/runtime/app_env.js +18 -0
  93. package/dist/runtime/app_env.js.map +1 -0
  94. package/dist/runtime/index.cjs +9 -2
  95. package/dist/runtime/index.cjs.map +1 -1
  96. package/dist/runtime/index.d.cts +5 -1
  97. package/dist/runtime/index.d.ts +5 -1
  98. package/dist/runtime/index.js +9 -1
  99. package/dist/runtime/index.js.map +1 -1
  100. package/dist/runtime/renderer_log_dispatcher.cjs.map +1 -1
  101. package/dist/runtime/renderer_log_dispatcher.d.cts +1 -1
  102. package/dist/runtime/renderer_log_dispatcher.d.ts +1 -1
  103. package/dist/runtime/renderer_log_dispatcher.js.map +1 -1
  104. package/dist/runtime/renderer_trace_batch_receiver_registry.cjs +50 -0
  105. package/dist/runtime/renderer_trace_batch_receiver_registry.cjs.map +1 -0
  106. package/dist/runtime/renderer_trace_batch_receiver_registry.d.cts +10 -0
  107. package/dist/runtime/renderer_trace_batch_receiver_registry.d.ts +10 -0
  108. package/dist/runtime/renderer_trace_batch_receiver_registry.js +24 -0
  109. package/dist/runtime/renderer_trace_batch_receiver_registry.js.map +1 -0
  110. package/dist/runtime/runtime.cjs +75 -57
  111. package/dist/runtime/runtime.cjs.map +1 -1
  112. package/dist/runtime/runtime.d.cts +8 -1
  113. package/dist/runtime/runtime.d.ts +8 -1
  114. package/dist/runtime/runtime.js +80 -58
  115. package/dist/runtime/runtime.js.map +1 -1
  116. package/dist/testing/browser.cjs +540 -11
  117. package/dist/testing/browser.cjs.map +1 -1
  118. package/dist/testing/browser.d.cts +219 -3
  119. package/dist/testing/browser.d.ts +219 -3
  120. package/dist/testing/browser.js +540 -11
  121. package/dist/testing/browser.js.map +1 -1
  122. package/dist/testing/create_controller_harness.d.cts +1 -1
  123. package/dist/testing/create_controller_harness.d.ts +1 -1
  124. package/dist/testing/index.cjs +13 -0
  125. package/dist/testing/index.cjs.map +1 -1
  126. package/dist/testing/index.d.cts +7 -2
  127. package/dist/testing/index.d.ts +7 -2
  128. package/dist/testing/index.js +14 -0
  129. package/dist/testing/index.js.map +1 -1
  130. package/dist/trace-testing/index.d.cts +1 -1
  131. package/dist/trace-testing/index.d.ts +1 -1
  132. package/dist/trace-testing/trace_recorder.d.cts +1 -1
  133. package/dist/trace-testing/trace_recorder.d.ts +1 -1
  134. package/dist/trace-transport/index.cjs +32 -0
  135. package/dist/trace-transport/index.cjs.map +1 -0
  136. package/dist/trace-transport/index.d.cts +6 -0
  137. package/dist/trace-transport/index.d.ts +6 -0
  138. package/dist/trace-transport/index.js +7 -0
  139. package/dist/trace-transport/index.js.map +1 -0
  140. package/dist/trace_hub-Cob54qRx.d.cts +56 -0
  141. package/dist/trace_hub-Cob54qRx.d.ts +56 -0
  142. package/dist/tracing/index.cjs +14 -0
  143. package/dist/tracing/index.cjs.map +1 -1
  144. package/dist/tracing/index.d.cts +7 -1
  145. package/dist/tracing/index.d.ts +7 -1
  146. package/dist/tracing/index.js +12 -0
  147. package/dist/tracing/index.js.map +1 -1
  148. package/dist/tracing/renderer_trace_batch_host.cjs +80 -0
  149. package/dist/tracing/renderer_trace_batch_host.cjs.map +1 -0
  150. package/dist/tracing/renderer_trace_batch_host.d.cts +16 -0
  151. package/dist/tracing/renderer_trace_batch_host.d.ts +16 -0
  152. package/dist/tracing/renderer_trace_batch_host.js +56 -0
  153. package/dist/tracing/renderer_trace_batch_host.js.map +1 -0
  154. package/dist/tracing/renderer_trace_batch_transport.cjs +313 -0
  155. package/dist/tracing/renderer_trace_batch_transport.cjs.map +1 -0
  156. package/dist/tracing/renderer_trace_batch_transport.d.cts +53 -0
  157. package/dist/tracing/renderer_trace_batch_transport.d.ts +53 -0
  158. package/dist/tracing/renderer_trace_batch_transport.js +292 -0
  159. package/dist/tracing/renderer_trace_batch_transport.js.map +1 -0
  160. package/dist/tracing/trace_commit_writer.cjs +159 -0
  161. package/dist/tracing/trace_commit_writer.cjs.map +1 -0
  162. package/dist/tracing/trace_commit_writer.d.cts +97 -0
  163. package/dist/tracing/trace_commit_writer.d.ts +97 -0
  164. package/dist/tracing/trace_commit_writer.js +135 -0
  165. package/dist/tracing/trace_commit_writer.js.map +1 -0
  166. package/dist/tracing/trace_hub.cjs +58 -3
  167. package/dist/tracing/trace_hub.cjs.map +1 -1
  168. package/dist/tracing/trace_hub.d.cts +2 -46
  169. package/dist/tracing/trace_hub.d.ts +2 -46
  170. package/dist/tracing/trace_hub.js +56 -3
  171. package/dist/tracing/trace_hub.js.map +1 -1
  172. package/dist/tracing/trace_provider.cjs +42 -12
  173. package/dist/tracing/trace_provider.cjs.map +1 -1
  174. package/dist/tracing/trace_provider.d.cts +15 -1
  175. package/dist/tracing/trace_provider.d.ts +15 -1
  176. package/dist/tracing/trace_provider.js +43 -12
  177. package/dist/tracing/trace_provider.js.map +1 -1
  178. package/dist/tracing/types/wood_trace_batch.types.cjs +17 -0
  179. package/dist/tracing/types/wood_trace_batch.types.cjs.map +1 -0
  180. package/dist/tracing/types/wood_trace_batch.types.d.cts +124 -0
  181. package/dist/tracing/types/wood_trace_batch.types.d.ts +124 -0
  182. package/dist/tracing/types/wood_trace_batch.types.js +1 -0
  183. package/dist/tracing/types/wood_trace_batch.types.js.map +1 -0
  184. package/dist/tracing/types/wood_trace_sink.types.cjs +17 -0
  185. package/dist/tracing/types/wood_trace_sink.types.cjs.map +1 -0
  186. package/dist/tracing/types/wood_trace_sink.types.d.cts +2 -0
  187. package/dist/tracing/types/wood_trace_sink.types.d.ts +2 -0
  188. package/dist/tracing/types/wood_trace_sink.types.js +1 -0
  189. package/dist/tracing/types/wood_trace_sink.types.js.map +1 -0
  190. package/dist/tracing/wood_trace_batch_contract.cjs +229 -0
  191. package/dist/tracing/wood_trace_batch_contract.cjs.map +1 -0
  192. package/dist/tracing/wood_trace_batch_contract.d.cts +23 -0
  193. package/dist/tracing/wood_trace_batch_contract.d.ts +23 -0
  194. package/dist/tracing/wood_trace_batch_contract.js +205 -0
  195. package/dist/tracing/wood_trace_batch_contract.js.map +1 -0
  196. package/dist/transport/build_transport_bridge.cjs +62 -0
  197. package/dist/transport/build_transport_bridge.cjs.map +1 -0
  198. package/dist/transport/build_transport_bridge.d.cts +29 -0
  199. package/dist/transport/build_transport_bridge.d.ts +29 -0
  200. package/dist/transport/build_transport_bridge.js +38 -0
  201. package/dist/transport/build_transport_bridge.js.map +1 -0
  202. package/dist/transport/in_process_router_transport.cjs +41 -0
  203. package/dist/transport/in_process_router_transport.cjs.map +1 -0
  204. package/dist/transport/in_process_router_transport.d.cts +31 -0
  205. package/dist/transport/in_process_router_transport.d.ts +31 -0
  206. package/dist/transport/in_process_router_transport.js +17 -0
  207. package/dist/transport/in_process_router_transport.js.map +1 -0
  208. package/dist/transport/index.cjs +42 -0
  209. package/dist/transport/index.cjs.map +1 -0
  210. package/dist/transport/index.d.cts +8 -0
  211. package/dist/transport/index.d.ts +8 -0
  212. package/dist/transport/index.js +19 -0
  213. package/dist/transport/index.js.map +1 -0
  214. package/dist/transport/loopback_remote_transport.cjs +270 -0
  215. package/dist/transport/loopback_remote_transport.cjs.map +1 -0
  216. package/dist/transport/loopback_remote_transport.d.cts +49 -0
  217. package/dist/transport/loopback_remote_transport.d.ts +49 -0
  218. package/dist/transport/loopback_remote_transport.js +237 -0
  219. package/dist/transport/loopback_remote_transport.js.map +1 -0
  220. package/dist/transport/loopback_serialization.cjs +116 -0
  221. package/dist/transport/loopback_serialization.cjs.map +1 -0
  222. package/dist/transport/loopback_serialization.d.cts +28 -0
  223. package/dist/transport/loopback_serialization.d.ts +28 -0
  224. package/dist/transport/loopback_serialization.js +91 -0
  225. package/dist/transport/loopback_serialization.js.map +1 -0
  226. package/dist/transport/transport_contract_suite.cjs +110 -0
  227. package/dist/transport/transport_contract_suite.cjs.map +1 -0
  228. package/dist/transport/transport_contract_suite.d.cts +23 -0
  229. package/dist/transport/transport_contract_suite.d.ts +23 -0
  230. package/dist/transport/transport_contract_suite.js +85 -0
  231. package/dist/transport/transport_contract_suite.js.map +1 -0
  232. package/dist/transport/wood_rpc_transport.cjs +17 -0
  233. package/dist/transport/wood_rpc_transport.cjs.map +1 -0
  234. package/dist/transport/wood_rpc_transport.d.cts +44 -0
  235. package/dist/transport/wood_rpc_transport.d.ts +44 -0
  236. package/dist/transport/wood_rpc_transport.js +1 -0
  237. package/dist/transport/wood_rpc_transport.js.map +1 -0
  238. package/docs/browser-testing.md +139 -1
  239. package/package.json +22 -3
  240. package/src/components/NavigationShell.browser.test.ts +236 -0
  241. package/src/components/NavigationShell.svelte +162 -49
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The single seam every test-mode RPC path is built on.
3
+ *
4
+ * Production uses the Electron preload bridge (`ipcRenderer.invoke` +
5
+ * `ipcRenderer.on('__event:...')`). Tests use one of the in-process or loopback
6
+ * implementations. The generated bridge client is built over whichever transport
7
+ * is supplied, so application code never knows which one is active.
8
+ *
9
+ * IMPORTANT (fast-tier isolation): this module and `InProcessRouterTransport`
10
+ * must never import `electron` or `playwright`. Keep this file dependency-free.
11
+ */
12
+ interface WoodRpcTransport {
13
+ /**
14
+ * Dispatch a request/response call to a channel. `payload` is optional so
15
+ * inputless operations can be called as `invoke(channel)`.
16
+ */
17
+ invoke(channel: string, payload?: unknown): Promise<unknown>;
18
+ /**
19
+ * Subscribe to a main-process push channel (the `__event:`-prefixed events
20
+ * emitted by `WindowEventService`). Returns an unsubscribe function.
21
+ */
22
+ subscribe(channel: string, listener: (payload: unknown) => void): () => void;
23
+ }
24
+ /** One channel's input/output contract. The generated channel map is a record of these. */
25
+ interface WoodChannelContract {
26
+ input: unknown;
27
+ output: unknown;
28
+ }
29
+ /** Shape a generated channel map conforms to: channel name -> { input, output }. */
30
+ type WoodChannelMapLike = Record<string, WoodChannelContract>;
31
+ /** Extract the input type for a channel in a generated map. */
32
+ type WoodChannelInput<M extends WoodChannelMapLike, C extends keyof M> = M[C]['input'];
33
+ /** Extract the output type for a channel in a generated map. */
34
+ type WoodChannelOutput<M extends WoodChannelMapLike, C extends keyof M> = M[C]['output'];
35
+ /**
36
+ * A `call()` typed from a generated channel map. Harness `api.call` is this,
37
+ * parameterized on the app's generated `WoodChannelMap`:
38
+ *
39
+ * const call: TypedCall<WoodChannelMap> = ...;
40
+ * await call('messages.send', { threadId, text }); // fully typed
41
+ */
42
+ type TypedCall<M extends WoodChannelMapLike> = <C extends keyof M & string>(channel: C, ...payload: M[C]['input'] extends void ? [] : [M[C]['input']]) => Promise<M[C]['output']>;
43
+
44
+ export type { TypedCall, WoodChannelContract, WoodChannelInput, WoodChannelMapLike, WoodChannelOutput, WoodRpcTransport };
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The single seam every test-mode RPC path is built on.
3
+ *
4
+ * Production uses the Electron preload bridge (`ipcRenderer.invoke` +
5
+ * `ipcRenderer.on('__event:...')`). Tests use one of the in-process or loopback
6
+ * implementations. The generated bridge client is built over whichever transport
7
+ * is supplied, so application code never knows which one is active.
8
+ *
9
+ * IMPORTANT (fast-tier isolation): this module and `InProcessRouterTransport`
10
+ * must never import `electron` or `playwright`. Keep this file dependency-free.
11
+ */
12
+ interface WoodRpcTransport {
13
+ /**
14
+ * Dispatch a request/response call to a channel. `payload` is optional so
15
+ * inputless operations can be called as `invoke(channel)`.
16
+ */
17
+ invoke(channel: string, payload?: unknown): Promise<unknown>;
18
+ /**
19
+ * Subscribe to a main-process push channel (the `__event:`-prefixed events
20
+ * emitted by `WindowEventService`). Returns an unsubscribe function.
21
+ */
22
+ subscribe(channel: string, listener: (payload: unknown) => void): () => void;
23
+ }
24
+ /** One channel's input/output contract. The generated channel map is a record of these. */
25
+ interface WoodChannelContract {
26
+ input: unknown;
27
+ output: unknown;
28
+ }
29
+ /** Shape a generated channel map conforms to: channel name -> { input, output }. */
30
+ type WoodChannelMapLike = Record<string, WoodChannelContract>;
31
+ /** Extract the input type for a channel in a generated map. */
32
+ type WoodChannelInput<M extends WoodChannelMapLike, C extends keyof M> = M[C]['input'];
33
+ /** Extract the output type for a channel in a generated map. */
34
+ type WoodChannelOutput<M extends WoodChannelMapLike, C extends keyof M> = M[C]['output'];
35
+ /**
36
+ * A `call()` typed from a generated channel map. Harness `api.call` is this,
37
+ * parameterized on the app's generated `WoodChannelMap`:
38
+ *
39
+ * const call: TypedCall<WoodChannelMap> = ...;
40
+ * await call('messages.send', { threadId, text }); // fully typed
41
+ */
42
+ type TypedCall<M extends WoodChannelMapLike> = <C extends keyof M & string>(channel: C, ...payload: M[C]['input'] extends void ? [] : [M[C]['input']]) => Promise<M[C]['output']>;
43
+
44
+ export type { TypedCall, WoodChannelContract, WoodChannelInput, WoodChannelMapLike, WoodChannelOutput, WoodRpcTransport };
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=wood_rpc_transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -257,11 +257,149 @@ await mounted.expect.hidden('.menu');
257
257
  await mounted.expect.insideViewport('.composer', { margin: 8 });
258
258
  await mounted.expect.notClipped('.popover');
259
259
  await mounted.expect.notOverlapping('.toast', '.composer');
260
- await mounted.expect.zIndexAbove('.modal', '.backdrop');
261
260
  await mounted.expect.stableAfterAnimation('.drawer');
262
261
  await mounted.expect.screenshot('settings-drawer');
263
262
  ```
264
263
 
264
+ ### Geometry assertion kit
265
+
266
+ Honest alignment, occlusion, and front/behind primitives. They read real boxes
267
+ (`getBoundingClientRect`), query Chromium's real paint tree (CDP
268
+ `DOMSnapshot.captureSnapshot`), and hit-test real painted pixels
269
+ (`document.elementFromPoint`) rather than trusting computed `z-index`.
270
+
271
+ **Prefer the whole-element mechanisms.** They answer "which layer is on top" and
272
+ "is any foreign layer painted over this element" for the entire element, with no
273
+ point to pick. The point-sampling forms are the special case for point-specific
274
+ questions ("does a click at THIS spot land here").
275
+
276
+ The whole-element visibility/stacking hierarchy is:
277
+
278
+ - `inFrontOf` — paint-order front/behind between two named elements.
279
+ - `notCovered` — paint-order coverage: is ANY foreign element painted over the
280
+ target's rect. This is the visibility check (replaces the removed
281
+ `fullyVisible`).
282
+ - `unoccluded` — click-target point check honouring `pointer-events`.
283
+
284
+ ```ts
285
+ // Batch rect reader (one snapshot per selector, in order).
286
+ const [head, value] = await mounted.expect.rects(['.col-head', '.col-value']);
287
+
288
+ // Alignment: shared edge / shared center axis within a tolerance (default 1px).
289
+ await mounted.expect.sharedEdge('.col-head', '.col-value', 'right', { tolerance: 1 });
290
+ await mounted.expect.sharedAxis('.col-head', '.col-value', 'centerX', { tolerance: 1 });
291
+
292
+ // Spacing: edge-to-edge gap between two elements along an axis. Order is derived
293
+ // from the rects (leftmost/topmost is the earlier box), not the argument order.
294
+ // At least one of equals/min/max is required; all are within `tolerance` (1px).
295
+ await mounted.expect.gapBetween('.chip-a', '.chip-b', 'horizontal', { equals: 8 });
296
+ await mounted.expect.gapBetween('.row-a', '.row-b', 'vertical', { min: 4, max: 12 });
297
+
298
+ // Spacing: 3+ elements evenly spaced along an axis (sorted by position, adjacent
299
+ // gaps compared). Passes when max gap - min gap <= tolerance.
300
+ await mounted.expect.evenlySpaced(['.tick-1', '.tick-2', '.tick-3'], 'horizontal', { tolerance: 1 });
301
+
302
+ // Front/behind (DEFAULT: whole-element paint order via CDP DOMSnapshot).
303
+ // Passes when `.modal` has a strictly greater global paint-order index than
304
+ // `.backdrop` — painted later == on top.
305
+ await mounted.expect.inFrontOf('.modal', '.backdrop');
306
+
307
+ // Whole-element visibility (DEFAULT): no foreign layer paints over the target.
308
+ // Paint-order coverage via CDP DOMSnapshot — inspectable, not a black box.
309
+ await mounted.expect.notCovered('.primary-button');
310
+
311
+ // --- Point-specific special cases ---
312
+
313
+ // "Does a click at this spot land on the target?" (honours pointer-events).
314
+ await mounted.expect.unoccluded('.primary-button');
315
+
316
+ // Front/behind AT ONE PIXEL: hit-test an explicit point (or the intersection
317
+ // center when omitted). Use only when the question is point-specific.
318
+ await mounted.expect.inFrontOf('.tooltip', '.row', { at: { x: 120, y: 48 } });
319
+
320
+ // Low-level hit test: describes the top painted element at a point.
321
+ const hit = await mounted.expect.hitTest('.primary-button', 'center');
322
+ // hit.isTarget / hit.isDescendantOfTarget / hit.description
323
+ ```
324
+
325
+ `gapBetween` and `evenlySpaced` (spacing): `gapBetween(a, b, axis, opts)` measures
326
+ the edge-to-edge distance between two elements along the axis. The order is read
327
+ from the rects, not the argument order — the box with the smaller leading edge
328
+ (left for `'horizontal'`, top for `'vertical'`) is the earlier box and the gap is
329
+ `later.leading - earlier.trailing`. At least one of `equals` / `min` / `max` is
330
+ required. A negative gap means the border boxes OVERLAP along the axis; that fails
331
+ with a clear overlap message UNLESS the constraint explicitly allows negatives (a
332
+ negative `equals` or `min`), so you can assert an intentional overlap.
333
+ `evenlySpaced(selectors, axis, opts)` needs 3+ elements, sorts them by position,
334
+ computes adjacent gaps, and passes when `max gap - min gap <= tolerance`; its
335
+ failure message lists every gap and names the widest and narrowest (offending)
336
+ pairs.
337
+
338
+ > **Border-box caveat.** Both spacing matchers read border boxes
339
+ > (`getBoundingClientRect`), so the gap is the distance between the elements'
340
+ > outer borders and EXCLUDES any padding inside them. A test that cares about
341
+ > visual whitespace between inner content should target the inner content
342
+ > elements, not their padded wrappers.
343
+
344
+ `inFrontOf` (default) and `zIndexAbove` (deprecated): `zIndexAbove` compares
345
+ numeric computed `z-index` only and therefore lies whenever stacking contexts,
346
+ DOM paint order (auto/equal z-index), or transform/opacity contexts decide the
347
+ real order. `inFrontOf` reads the browser's actual global paint order, so it
348
+ agrees with the painted pixel. Higher paint-order index == painted later == on
349
+ top (verified against the stacking-context trap, where a `z-index:9999`
350
+ descendant of a `z-index:1` parent still paints BEHIND a root-context sibling at
351
+ `z-index:2`). Multiple paint fragments collapse to the element's max index. Paint
352
+ order is Chromium-only (needs a CDP session); pass `{ at }` to use the
353
+ point hit-test form on other browsers. Non-overlapping elements still get a valid
354
+ verdict — the failure message notes there is no shared pixel where front/behind is
355
+ visually observable.
356
+
357
+ `notCovered` vs `unoccluded`:
358
+
359
+ - `notCovered` is the **painted-coverage / visibility** check. From the real
360
+ paint tree (CDP `DOMSnapshot.captureSnapshot` with paint order + computed
361
+ styles + DOM rects) it finds every element whose rect overlaps the target,
362
+ excludes the target's own ancestors/descendants, and excludes candidates that
363
+ paint nothing visible in the overlap per an explicit, documented
364
+ transparent-wrapper heuristic (`opacity:0`/`visibility:hidden`, or no
365
+ background-color alpha AND `background-image:none` AND all border widths `0px`
366
+ AND no own text/replaced content — `box-shadow`/`outline` are NOT counted as
367
+ coverage). It then fails if any remaining foreign element has a higher paint
368
+ order than the target, naming that element, its rect, and both paint indices.
369
+ - `unoccluded` is the **click-target** check. It hit-tests one pixel and honours
370
+ `pointer-events` — a `pointer-events: none` overlay painted on top does NOT
371
+ fail it, because it does not intercept the click. It is not a general
372
+ visibility check.
373
+
374
+ Because `notCovered` tracks PAINTED coverage, a visually-opaque
375
+ `pointer-events: none` overlay DOES fail `notCovered` while `unoccluded` passes —
376
+ the honest, complementary difference between "covered by a painted layer" and
377
+ "reachable by a click". The transparent-wrapper heuristic is the key asymmetry:
378
+ a bare positioning wrapper spanning the target passes `notCovered` (it paints
379
+ nothing), while an opaque overlay fails.
380
+
381
+ > **Why not IntersectionObserver v2?** `notCovered` replaced an earlier
382
+ > `fullyVisible` matcher built on IOv2 (`{ trackVisibility: true, delay: 100 }`,
383
+ > read via `entry.isVisible`). IOv2's `isVisible` is a black box: Chromium
384
+ > reports `false` not only for distorting ancestor effects (`opacity < 1`,
385
+ > `filter`, `transform`, `mix-blend-mode`) but also for its OCCLUSION pass — ANY
386
+ > element painted later than the target within its rect disqualifies visibility,
387
+ > **even a fully transparent one**, and IOv2 never checks whether the covering
388
+ > pixels are actually opaque. This was pinned empirically against the demo app's
389
+ > global stylesheet: the culprit is a single `#app::after { content:''; inset:0;
390
+ > z-index:9999 }` frame-overlay pseudo-element. Reduced to just
391
+ > `content:''; position:absolute; inset:0` (no background, no shadow, no
392
+ > z-index) it still forced `isVisible:false` for every element beneath it;
393
+ > removing `content` (so no pseudo box is generated) restored `true`. A matcher
394
+ > whose "not visible" verdict can mean either real occlusion or an invisible
395
+ > overlay is untrustworthy — so it was replaced by `notCovered`, whose coverage
396
+ > rule is fully inspectable and exempts transparent layers by construction.
397
+
398
+ > `elementFromPoint` (used by `unoccluded` and `inFrontOf { at }`) honours
399
+ > `pointer-events`. An element with `pointer-events: none` painted on top is
400
+ > transparent to those point checks — usually the intended contract for a
401
+ > click-target assertion.
402
+
265
403
  Screenshots are written to `test-results/wood-browser`.
266
404
 
267
405
  ## Lifecycle
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noego/wood",
3
- "version": "0.4.6",
3
+ "version": "0.4.7",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -49,6 +49,24 @@
49
49
  "import": "./dist/trace-testing/index.js",
50
50
  "default": "./dist/trace-testing/index.js"
51
51
  },
52
+ "./transport": {
53
+ "types": "./dist/transport/index.d.ts",
54
+ "require": "./dist/transport/index.cjs",
55
+ "import": "./dist/transport/index.js",
56
+ "default": "./dist/transport/index.js"
57
+ },
58
+ "./trace-transport": {
59
+ "types": "./dist/trace-transport/index.d.ts",
60
+ "require": "./dist/trace-transport/index.cjs",
61
+ "import": "./dist/trace-transport/index.js",
62
+ "default": "./dist/trace-transport/index.js"
63
+ },
64
+ "./runtime": {
65
+ "types": "./dist/runtime/index.d.ts",
66
+ "require": "./dist/runtime/index.cjs",
67
+ "import": "./dist/runtime/index.js",
68
+ "default": "./dist/runtime/index.js"
69
+ },
52
70
  "./client": {
53
71
  "types": "./dist/client/index.d.ts",
54
72
  "require": "./dist/client/index.cjs",
@@ -100,9 +118,10 @@
100
118
  },
101
119
  "dependencies": {
102
120
  "@noego/ioc": "^0.3.1",
103
- "@noego/logger": "^0.0.2",
121
+ "@noego/logger": "^0.0.4",
104
122
  "@noego/stitch": "^1.0.3",
105
- "@noego/trace": "^0.0.2",
123
+ "@noego/telemetry": "^0.0.1",
124
+ "@noego/trace": "^0.0.6",
106
125
  "ajv": "^8.x",
107
126
  "ajv-formats": "^3.x",
108
127
  "chokidar": "^4.x",
@@ -0,0 +1,236 @@
1
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { afterEach, describe, expect, test } from 'vitest';
5
+ import { browser } from '../testing/browser';
6
+
7
+ type ReproSnapshot = {
8
+ pageCreateCount: number;
9
+ layoutCreateCount: number;
10
+ disposeCounts: Record<string, number>;
11
+ rendered: string;
12
+ pageErrors: string[];
13
+ };
14
+
15
+ describe('NavigationShell failed transition recovery', () => {
16
+ const harnesses: Array<{ destroy(): Promise<void> }> = [];
17
+ const tempRoots: string[] = [];
18
+
19
+ afterEach(async () => {
20
+ while (harnesses.length > 0) {
21
+ await harnesses.pop()?.destroy();
22
+ }
23
+ while (tempRoots.length > 0) {
24
+ rmSync(tempRoots.pop()!, { recursive: true, force: true });
25
+ }
26
+ });
27
+
28
+ test('does not orphan a controller when a sibling layout creation rejects', async () => {
29
+ const woodRoot = path.resolve(__dirname, '../..');
30
+ const rootDir = mkdtempSync(path.join(os.tmpdir(), 'wood-navigation-shell-repro-'));
31
+ tempRoots.push(rootDir);
32
+ mkdirSync(path.join(rootDir, 'ui'), { recursive: true });
33
+
34
+ writeFileSync(
35
+ path.join(rootDir, 'ui', 'ReproView.svelte'),
36
+ `<script lang="ts">
37
+ let { data = {}, page = {} } = $props<{
38
+ data?: { id?: string };
39
+ page?: { params?: { threadId?: string } };
40
+ }>();
41
+ </script>
42
+
43
+ <main data-repro-view>
44
+ thread={page.params?.threadId ?? 'missing'};controller={data.id ?? 'missing'}
45
+ </main>`,
46
+ );
47
+
48
+ writeFileSync(
49
+ path.join(rootDir, 'ui', 'ReproLayout.svelte'),
50
+ `<script lang="ts">
51
+ import type { Snippet } from 'svelte';
52
+ let { children }: { children: Snippet } = $props();
53
+ </script>
54
+
55
+ <section data-repro-layout>{@render children()}</section>`,
56
+ );
57
+
58
+ writeFileSync(
59
+ path.join(rootDir, 'ui', 'NavigationShellRepro.svelte'),
60
+ `<script lang="ts">
61
+ import '/@fs/${woodRoot}/src/client/global_polyfill.ts';
62
+ import NavigationShell from '/@fs/${woodRoot}/src/components/NavigationShell.svelte';
63
+ import { getNavigation } from '/@fs/${woodRoot}/src/navigation/index.ts';
64
+ import ReproLayout from './ReproLayout.svelte';
65
+ import ReproView from './ReproView.svelte';
66
+
67
+ type ReproState = {
68
+ pageCreateCount: number;
69
+ layoutCreateCount: number;
70
+ disposeCounts: Record<string, number>;
71
+ };
72
+
73
+ const state: ReproState = {
74
+ pageCreateCount: 0,
75
+ layoutCreateCount: 0,
76
+ disposeCounts: {},
77
+ };
78
+ (window as unknown as { __WOOD_NAVIGATION_REPRO__: ReproState })
79
+ .__WOOD_NAVIGATION_REPRO__ = state;
80
+
81
+ function createController(id: string) {
82
+ state.disposeCounts[id] = 0;
83
+ return {
84
+ data: { id },
85
+ dispose() {
86
+ state.disposeCounts[id] += 1;
87
+ },
88
+ };
89
+ }
90
+
91
+ const entry = {
92
+ key: 'main.chat_dom',
93
+ windowName: 'main',
94
+ pageName: 'chat_dom',
95
+ layoutPaths: ['ui/ReproLayout.svelte'],
96
+ layoutControllers: ['repro-layout-controller'],
97
+ layoutPreserve: [false],
98
+ view: ReproView,
99
+ layouts: [ReproLayout],
100
+ };
101
+ const manifest = { [entry.key]: entry };
102
+ const initialController = createController('initial-page');
103
+ const initialLayoutControllers = [createController('initial-layout')];
104
+
105
+ async function createViewControllerFn() {
106
+ state.pageCreateCount += 1;
107
+ return createController(\`page-\${state.pageCreateCount}\`);
108
+ }
109
+
110
+ async function createLayoutControllersFn() {
111
+ state.layoutCreateCount += 1;
112
+ if (state.layoutCreateCount === 1) {
113
+ await Promise.resolve();
114
+ throw new Error('controlled layout creation failure');
115
+ }
116
+ return [createController(\`layout-\${state.layoutCreateCount}\`)];
117
+ }
118
+
119
+ function getViewManifestEntryFn(viewKey: string) {
120
+ return manifest[viewKey as keyof typeof manifest] ?? null;
121
+ }
122
+
123
+ const navigation = getNavigation();
124
+ navigation.replace(entry.key, { threadId: '1' }, {});
125
+ (window as unknown as {
126
+ __WOOD_NAVIGATION_REPRO_GO__: (threadId: string) => void;
127
+ }).__WOOD_NAVIGATION_REPRO_GO__ = (threadId: string) => {
128
+ navigation.go(entry.key, { threadId }, {});
129
+ };
130
+ </script>
131
+
132
+ <NavigationShell
133
+ {manifest}
134
+ initialEntry={entry}
135
+ {initialController}
136
+ {initialLayoutControllers}
137
+ {createViewControllerFn}
138
+ {createLayoutControllersFn}
139
+ {getViewManifestEntryFn}
140
+ />`,
141
+ );
142
+
143
+ const pageErrors: string[] = [];
144
+ const consoleErrors: string[] = [];
145
+ const harness = await browser.createHarness({
146
+ rootDir: woodRoot,
147
+ browser: { headless: true },
148
+ page: {
149
+ beforeMount({ page }) {
150
+ page.on('pageerror', (error: Error) => pageErrors.push(error.message));
151
+ page.on('console', (message: { type(): string; text(): string }) => {
152
+ if (message.type() === 'error') consoleErrors.push(message.text());
153
+ });
154
+ },
155
+ },
156
+ });
157
+ harnesses.push(harness);
158
+ const mounted = await harness.mountComponent(
159
+ path.join(rootDir, 'ui', 'NavigationShellRepro.svelte'),
160
+ );
161
+ await mounted.locator('[data-repro-view]').waitFor({ state: 'visible', timeout: 5_000 }).catch(
162
+ async (error: unknown) => {
163
+ throw new Error(
164
+ `${error instanceof Error ? error.message : String(error)}\n` +
165
+ `Page errors: ${pageErrors.join(' | ')}\n` +
166
+ `Console errors: ${consoleErrors.join(' | ')}\n` +
167
+ `HTML: ${await mounted.page.content()}`,
168
+ );
169
+ },
170
+ );
171
+
172
+ await expect.poll(async () => {
173
+ return (await mounted.locator('[data-repro-view]').textContent())?.replace(/\s+/g, ' ').trim();
174
+ }).toBe('thread=1;controller=initial-page');
175
+
176
+ await mounted.evaluate(() => {
177
+ (window as unknown as {
178
+ __WOOD_NAVIGATION_REPRO_GO__: (threadId: string) => void;
179
+ }).__WOOD_NAVIGATION_REPRO_GO__('2');
180
+ });
181
+ await expect.poll(async () => {
182
+ return await mounted.evaluate(() => {
183
+ return (window as unknown as {
184
+ __WOOD_NAVIGATION_REPRO__: { layoutCreateCount: number };
185
+ }).__WOOD_NAVIGATION_REPRO__.layoutCreateCount;
186
+ });
187
+ }).toBe(1);
188
+ await expect.poll(() => pageErrors.length + consoleErrors.length).toBeGreaterThan(0);
189
+
190
+ await mounted.evaluate(() => {
191
+ (window as unknown as {
192
+ __WOOD_NAVIGATION_REPRO_GO__: (threadId: string) => void;
193
+ }).__WOOD_NAVIGATION_REPRO_GO__('2');
194
+ });
195
+ await expect.poll(async () => {
196
+ return await mounted.evaluate(() => {
197
+ return (window as unknown as {
198
+ __WOOD_NAVIGATION_REPRO__: { pageCreateCount: number };
199
+ }).__WOOD_NAVIGATION_REPRO__.pageCreateCount;
200
+ });
201
+ }).toBe(2);
202
+ await expect.poll(async () => {
203
+ return (await mounted.locator('[data-repro-view]').textContent())?.replace(/\s+/g, ' ').trim();
204
+ }).toBe('thread=2;controller=page-2');
205
+
206
+ const snapshot = await mounted.evaluate<ReproSnapshot>(() => {
207
+ const state = (window as unknown as {
208
+ __WOOD_NAVIGATION_REPRO__: Omit<ReproSnapshot, 'rendered' | 'pageErrors'>;
209
+ }).__WOOD_NAVIGATION_REPRO__;
210
+ return {
211
+ ...state,
212
+ disposeCounts: { ...state.disposeCounts },
213
+ rendered: document.querySelector('[data-repro-view]')?.textContent?.replace(/\s+/g, ' ').trim() ?? '',
214
+ pageErrors: [],
215
+ };
216
+ });
217
+ snapshot.pageErrors = [...pageErrors];
218
+
219
+ expect(
220
+ snapshot,
221
+ 'A failed transition must preserve committed controllers and dispose any staged controller before a retry.',
222
+ ).toEqual({
223
+ pageCreateCount: 2,
224
+ layoutCreateCount: 2,
225
+ disposeCounts: {
226
+ 'initial-page': 1,
227
+ 'initial-layout': 1,
228
+ 'page-1': 1,
229
+ 'page-2': 0,
230
+ 'layout-2': 0,
231
+ },
232
+ rendered: 'thread=2;controller=page-2',
233
+ pageErrors: [],
234
+ });
235
+ }, 30_000);
236
+ });