@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.
- package/bin/wood.js +6 -1
- package/dist/client/app_env.cjs +39 -0
- package/dist/client/app_env.cjs.map +1 -0
- package/dist/client/app_env.d.cts +7 -0
- package/dist/client/app_env.d.ts +7 -0
- package/dist/client/app_env.js +14 -0
- package/dist/client/app_env.js.map +1 -0
- package/dist/client/index.cjs +33 -9
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.d.cts +5 -1
- package/dist/client/index.d.ts +5 -1
- package/dist/client/index.js +32 -9
- package/dist/client/index.js.map +1 -1
- package/dist/codegen/channel_map_generator.cjs +102 -0
- package/dist/codegen/channel_map_generator.cjs.map +1 -0
- package/dist/codegen/channel_map_generator.d.cts +6 -0
- package/dist/codegen/channel_map_generator.d.ts +6 -0
- package/dist/codegen/channel_map_generator.js +78 -0
- package/dist/codegen/channel_map_generator.js.map +1 -0
- package/dist/codegen/controller_registration_generator.cjs +6 -1
- package/dist/codegen/controller_registration_generator.cjs.map +1 -1
- package/dist/codegen/controller_registration_generator.js +6 -1
- package/dist/codegen/controller_registration_generator.js.map +1 -1
- package/dist/codegen/generated_file_notice.cjs +40 -0
- package/dist/codegen/generated_file_notice.cjs.map +1 -0
- package/dist/codegen/generated_file_notice.d.cts +3 -0
- package/dist/codegen/generated_file_notice.d.ts +3 -0
- package/dist/codegen/generated_file_notice.js +16 -0
- package/dist/codegen/generated_file_notice.js.map +1 -0
- package/dist/codegen/index.cjs +3 -0
- package/dist/codegen/index.cjs.map +1 -1
- package/dist/codegen/index.d.cts +1 -0
- package/dist/codegen/index.d.ts +1 -0
- package/dist/codegen/index.js +2 -0
- package/dist/codegen/index.js.map +1 -1
- package/dist/codegen/preload_generator.cjs +97 -1
- package/dist/codegen/preload_generator.cjs.map +1 -1
- package/dist/codegen/preload_generator.js +97 -1
- package/dist/codegen/preload_generator.js.map +1 -1
- package/dist/codegen/renderer_app_generator.cjs +6 -1
- package/dist/codegen/renderer_app_generator.cjs.map +1 -1
- package/dist/codegen/renderer_app_generator.js +6 -1
- package/dist/codegen/renderer_app_generator.js.map +1 -1
- package/dist/codegen/renderer_controller_registration_generator.cjs +6 -1
- package/dist/codegen/renderer_controller_registration_generator.cjs.map +1 -1
- package/dist/codegen/renderer_controller_registration_generator.js +6 -1
- package/dist/codegen/renderer_controller_registration_generator.js.map +1 -1
- package/dist/codegen/renderer_view_manifest_generator.cjs +6 -1
- package/dist/codegen/renderer_view_manifest_generator.cjs.map +1 -1
- package/dist/codegen/renderer_view_manifest_generator.js +6 -1
- package/dist/codegen/renderer_view_manifest_generator.js.map +1 -1
- package/dist/codegen/runtime_generator.cjs +6 -1
- package/dist/codegen/runtime_generator.cjs.map +1 -1
- package/dist/codegen/runtime_generator.js +6 -1
- package/dist/codegen/runtime_generator.js.map +1 -1
- package/dist/codegen/test_mock_generator.cjs +5 -1
- package/dist/codegen/test_mock_generator.cjs.map +1 -1
- package/dist/codegen/test_mock_generator.js +5 -1
- package/dist/codegen/test_mock_generator.js.map +1 -1
- package/dist/codegen/type_generator.cjs +6 -1
- package/dist/codegen/type_generator.cjs.map +1 -1
- package/dist/codegen/type_generator.js +6 -1
- package/dist/codegen/type_generator.js.map +1 -1
- package/dist/index.cjs +19 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +13 -1
- package/dist/index.js.map +1 -1
- package/dist/main/index.cjs +133 -55
- package/dist/main/index.cjs.map +1 -1
- package/dist/main/index.d.cts +30 -2
- package/dist/main/index.d.ts +30 -2
- package/dist/main/index.js +134 -55
- package/dist/main/index.js.map +1 -1
- package/dist/main/renderer_trace_drain_coordinator.cjs +127 -0
- package/dist/main/renderer_trace_drain_coordinator.cjs.map +1 -0
- package/dist/main/renderer_trace_drain_coordinator.d.cts +46 -0
- package/dist/main/renderer_trace_drain_coordinator.d.ts +46 -0
- package/dist/main/renderer_trace_drain_coordinator.js +101 -0
- package/dist/main/renderer_trace_drain_coordinator.js.map +1 -0
- package/dist/router/wood_router.cjs +9 -1
- package/dist/router/wood_router.cjs.map +1 -1
- package/dist/router/wood_router.d.cts +1 -1
- package/dist/router/wood_router.d.ts +1 -1
- package/dist/router/wood_router.js +9 -1
- package/dist/router/wood_router.js.map +1 -1
- package/dist/runtime/app_env.cjs +52 -0
- package/dist/runtime/app_env.cjs.map +1 -0
- package/dist/runtime/app_env.d.cts +10 -0
- package/dist/runtime/app_env.d.ts +10 -0
- package/dist/runtime/app_env.js +18 -0
- package/dist/runtime/app_env.js.map +1 -0
- package/dist/runtime/index.cjs +9 -2
- package/dist/runtime/index.cjs.map +1 -1
- package/dist/runtime/index.d.cts +5 -1
- package/dist/runtime/index.d.ts +5 -1
- package/dist/runtime/index.js +9 -1
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/renderer_log_dispatcher.cjs.map +1 -1
- package/dist/runtime/renderer_log_dispatcher.d.cts +1 -1
- package/dist/runtime/renderer_log_dispatcher.d.ts +1 -1
- package/dist/runtime/renderer_log_dispatcher.js.map +1 -1
- package/dist/runtime/renderer_trace_batch_receiver_registry.cjs +50 -0
- package/dist/runtime/renderer_trace_batch_receiver_registry.cjs.map +1 -0
- package/dist/runtime/renderer_trace_batch_receiver_registry.d.cts +10 -0
- package/dist/runtime/renderer_trace_batch_receiver_registry.d.ts +10 -0
- package/dist/runtime/renderer_trace_batch_receiver_registry.js +24 -0
- package/dist/runtime/renderer_trace_batch_receiver_registry.js.map +1 -0
- package/dist/runtime/runtime.cjs +75 -57
- package/dist/runtime/runtime.cjs.map +1 -1
- package/dist/runtime/runtime.d.cts +8 -1
- package/dist/runtime/runtime.d.ts +8 -1
- package/dist/runtime/runtime.js +80 -58
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/testing/browser.cjs +540 -11
- package/dist/testing/browser.cjs.map +1 -1
- package/dist/testing/browser.d.cts +219 -3
- package/dist/testing/browser.d.ts +219 -3
- package/dist/testing/browser.js +540 -11
- package/dist/testing/browser.js.map +1 -1
- package/dist/testing/create_controller_harness.d.cts +1 -1
- package/dist/testing/create_controller_harness.d.ts +1 -1
- package/dist/testing/index.cjs +13 -0
- package/dist/testing/index.cjs.map +1 -1
- package/dist/testing/index.d.cts +7 -2
- package/dist/testing/index.d.ts +7 -2
- package/dist/testing/index.js +14 -0
- package/dist/testing/index.js.map +1 -1
- package/dist/trace-testing/index.d.cts +1 -1
- package/dist/trace-testing/index.d.ts +1 -1
- package/dist/trace-testing/trace_recorder.d.cts +1 -1
- package/dist/trace-testing/trace_recorder.d.ts +1 -1
- package/dist/trace-transport/index.cjs +32 -0
- package/dist/trace-transport/index.cjs.map +1 -0
- package/dist/trace-transport/index.d.cts +6 -0
- package/dist/trace-transport/index.d.ts +6 -0
- package/dist/trace-transport/index.js +7 -0
- package/dist/trace-transport/index.js.map +1 -0
- package/dist/trace_hub-Cob54qRx.d.cts +56 -0
- package/dist/trace_hub-Cob54qRx.d.ts +56 -0
- package/dist/tracing/index.cjs +14 -0
- package/dist/tracing/index.cjs.map +1 -1
- package/dist/tracing/index.d.cts +7 -1
- package/dist/tracing/index.d.ts +7 -1
- package/dist/tracing/index.js +12 -0
- package/dist/tracing/index.js.map +1 -1
- package/dist/tracing/renderer_trace_batch_host.cjs +80 -0
- package/dist/tracing/renderer_trace_batch_host.cjs.map +1 -0
- package/dist/tracing/renderer_trace_batch_host.d.cts +16 -0
- package/dist/tracing/renderer_trace_batch_host.d.ts +16 -0
- package/dist/tracing/renderer_trace_batch_host.js +56 -0
- package/dist/tracing/renderer_trace_batch_host.js.map +1 -0
- package/dist/tracing/renderer_trace_batch_transport.cjs +313 -0
- package/dist/tracing/renderer_trace_batch_transport.cjs.map +1 -0
- package/dist/tracing/renderer_trace_batch_transport.d.cts +53 -0
- package/dist/tracing/renderer_trace_batch_transport.d.ts +53 -0
- package/dist/tracing/renderer_trace_batch_transport.js +292 -0
- package/dist/tracing/renderer_trace_batch_transport.js.map +1 -0
- package/dist/tracing/trace_commit_writer.cjs +159 -0
- package/dist/tracing/trace_commit_writer.cjs.map +1 -0
- package/dist/tracing/trace_commit_writer.d.cts +97 -0
- package/dist/tracing/trace_commit_writer.d.ts +97 -0
- package/dist/tracing/trace_commit_writer.js +135 -0
- package/dist/tracing/trace_commit_writer.js.map +1 -0
- package/dist/tracing/trace_hub.cjs +58 -3
- package/dist/tracing/trace_hub.cjs.map +1 -1
- package/dist/tracing/trace_hub.d.cts +2 -46
- package/dist/tracing/trace_hub.d.ts +2 -46
- package/dist/tracing/trace_hub.js +56 -3
- package/dist/tracing/trace_hub.js.map +1 -1
- package/dist/tracing/trace_provider.cjs +42 -12
- package/dist/tracing/trace_provider.cjs.map +1 -1
- package/dist/tracing/trace_provider.d.cts +15 -1
- package/dist/tracing/trace_provider.d.ts +15 -1
- package/dist/tracing/trace_provider.js +43 -12
- package/dist/tracing/trace_provider.js.map +1 -1
- package/dist/tracing/types/wood_trace_batch.types.cjs +17 -0
- package/dist/tracing/types/wood_trace_batch.types.cjs.map +1 -0
- package/dist/tracing/types/wood_trace_batch.types.d.cts +124 -0
- package/dist/tracing/types/wood_trace_batch.types.d.ts +124 -0
- package/dist/tracing/types/wood_trace_batch.types.js +1 -0
- package/dist/tracing/types/wood_trace_batch.types.js.map +1 -0
- package/dist/tracing/types/wood_trace_sink.types.cjs +17 -0
- package/dist/tracing/types/wood_trace_sink.types.cjs.map +1 -0
- package/dist/tracing/types/wood_trace_sink.types.d.cts +2 -0
- package/dist/tracing/types/wood_trace_sink.types.d.ts +2 -0
- package/dist/tracing/types/wood_trace_sink.types.js +1 -0
- package/dist/tracing/types/wood_trace_sink.types.js.map +1 -0
- package/dist/tracing/wood_trace_batch_contract.cjs +229 -0
- package/dist/tracing/wood_trace_batch_contract.cjs.map +1 -0
- package/dist/tracing/wood_trace_batch_contract.d.cts +23 -0
- package/dist/tracing/wood_trace_batch_contract.d.ts +23 -0
- package/dist/tracing/wood_trace_batch_contract.js +205 -0
- package/dist/tracing/wood_trace_batch_contract.js.map +1 -0
- package/dist/transport/build_transport_bridge.cjs +62 -0
- package/dist/transport/build_transport_bridge.cjs.map +1 -0
- package/dist/transport/build_transport_bridge.d.cts +29 -0
- package/dist/transport/build_transport_bridge.d.ts +29 -0
- package/dist/transport/build_transport_bridge.js +38 -0
- package/dist/transport/build_transport_bridge.js.map +1 -0
- package/dist/transport/in_process_router_transport.cjs +41 -0
- package/dist/transport/in_process_router_transport.cjs.map +1 -0
- package/dist/transport/in_process_router_transport.d.cts +31 -0
- package/dist/transport/in_process_router_transport.d.ts +31 -0
- package/dist/transport/in_process_router_transport.js +17 -0
- package/dist/transport/in_process_router_transport.js.map +1 -0
- package/dist/transport/index.cjs +42 -0
- package/dist/transport/index.cjs.map +1 -0
- package/dist/transport/index.d.cts +8 -0
- package/dist/transport/index.d.ts +8 -0
- package/dist/transport/index.js +19 -0
- package/dist/transport/index.js.map +1 -0
- package/dist/transport/loopback_remote_transport.cjs +270 -0
- package/dist/transport/loopback_remote_transport.cjs.map +1 -0
- package/dist/transport/loopback_remote_transport.d.cts +49 -0
- package/dist/transport/loopback_remote_transport.d.ts +49 -0
- package/dist/transport/loopback_remote_transport.js +237 -0
- package/dist/transport/loopback_remote_transport.js.map +1 -0
- package/dist/transport/loopback_serialization.cjs +116 -0
- package/dist/transport/loopback_serialization.cjs.map +1 -0
- package/dist/transport/loopback_serialization.d.cts +28 -0
- package/dist/transport/loopback_serialization.d.ts +28 -0
- package/dist/transport/loopback_serialization.js +91 -0
- package/dist/transport/loopback_serialization.js.map +1 -0
- package/dist/transport/transport_contract_suite.cjs +110 -0
- package/dist/transport/transport_contract_suite.cjs.map +1 -0
- package/dist/transport/transport_contract_suite.d.cts +23 -0
- package/dist/transport/transport_contract_suite.d.ts +23 -0
- package/dist/transport/transport_contract_suite.js +85 -0
- package/dist/transport/transport_contract_suite.js.map +1 -0
- package/dist/transport/wood_rpc_transport.cjs +17 -0
- package/dist/transport/wood_rpc_transport.cjs.map +1 -0
- package/dist/transport/wood_rpc_transport.d.cts +44 -0
- package/dist/transport/wood_rpc_transport.d.ts +44 -0
- package/dist/transport/wood_rpc_transport.js +1 -0
- package/dist/transport/wood_rpc_transport.js.map +1 -0
- package/docs/browser-testing.md +139 -1
- package/package.json +22 -3
- package/src/components/NavigationShell.browser.test.ts +236 -0
- 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":[]}
|
package/docs/browser-testing.md
CHANGED
|
@@ -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.
|
|
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.
|
|
121
|
+
"@noego/logger": "^0.0.4",
|
|
104
122
|
"@noego/stitch": "^1.0.3",
|
|
105
|
-
"@noego/
|
|
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
|
+
});
|