@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
package/dist/testing/browser.js
CHANGED
|
@@ -45,7 +45,9 @@ function mergeBridge(base, override) {
|
|
|
45
45
|
...base?.events ?? {},
|
|
46
46
|
...override?.events ?? {}
|
|
47
47
|
},
|
|
48
|
-
record: override?.record ?? base?.record
|
|
48
|
+
record: override?.record ?? base?.record,
|
|
49
|
+
...override?.transport ?? base?.transport ? { transport: override?.transport ?? base?.transport } : {},
|
|
50
|
+
...override?.rpcManifest ?? base?.rpcManifest ? { rpcManifest: override?.rpcManifest ?? base?.rpcManifest } : {}
|
|
49
51
|
};
|
|
50
52
|
}
|
|
51
53
|
async function waitForBoundedClose(promise, timeoutMs = DEFAULT_RESOURCE_CLOSE_TIMEOUT_MS) {
|
|
@@ -86,7 +88,9 @@ function createBridgeState(input) {
|
|
|
86
88
|
initialEvents,
|
|
87
89
|
calls: [],
|
|
88
90
|
emittedEvents: [],
|
|
89
|
-
record: input?.record ?? true
|
|
91
|
+
record: input?.record ?? true,
|
|
92
|
+
...input?.transport ? { transport: input.transport } : {},
|
|
93
|
+
...input?.rpcManifest ? { rpcManifest: input.rpcManifest } : {}
|
|
90
94
|
};
|
|
91
95
|
}
|
|
92
96
|
function createBrowserBridgeFixture(input) {
|
|
@@ -94,20 +98,29 @@ function createBrowserBridgeFixture(input) {
|
|
|
94
98
|
}
|
|
95
99
|
function createBrowserBridgeFixtureWithTrace(input, traceConfig) {
|
|
96
100
|
const state = createBridgeState(input);
|
|
97
|
-
const manifest = Array.from(state.operations.entries()).map(([channel, handler]) => channelToManifestEntry(channel, handler));
|
|
101
|
+
const manifest = state.rpcManifest ?? Array.from(state.operations.entries()).map(([channel, handler]) => channelToManifestEntry(channel, handler));
|
|
98
102
|
return {
|
|
99
103
|
state,
|
|
100
104
|
manifest,
|
|
101
|
-
bridgeInit: buildBridgeInitScript(manifest, traceConfig)
|
|
105
|
+
bridgeInit: buildBridgeInitScript(manifest, traceConfig, Boolean(state.transport))
|
|
102
106
|
};
|
|
103
107
|
}
|
|
104
|
-
function buildBridgeInitScript(manifest, traceConfig) {
|
|
108
|
+
function buildBridgeInitScript(manifest, traceConfig, transportMode) {
|
|
105
109
|
const traceEnabled = traceConfig?.enabled === true;
|
|
106
110
|
const traceMinLevel = traceConfig?.minLevel ?? "debug";
|
|
107
111
|
return `
|
|
108
112
|
(() => {
|
|
109
113
|
const manifest = ${JSON.stringify(manifest)};
|
|
114
|
+
const transportMode = ${JSON.stringify(transportMode)};
|
|
110
115
|
const listeners = new Map();
|
|
116
|
+
window.__woodBrowserTracePending = new Set();
|
|
117
|
+
window.__woodBrowserTraceFlush = async () => {
|
|
118
|
+
// Drain in-flight __trace exposeBinding round-trips so the test-process
|
|
119
|
+
// renderer-trace ack loop can barrier on a definite quiescent point.
|
|
120
|
+
while (window.__woodBrowserTracePending.size > 0) {
|
|
121
|
+
await Promise.allSettled(Array.from(window.__woodBrowserTracePending));
|
|
122
|
+
}
|
|
123
|
+
};
|
|
111
124
|
const bridge = {
|
|
112
125
|
__rpcManifest: manifest,
|
|
113
126
|
__woodConfig: async () => ({
|
|
@@ -115,7 +128,14 @@ function buildBridgeInitScript(manifest, traceConfig) {
|
|
|
115
128
|
rendererTraceMinLevel: ${JSON.stringify(traceMinLevel)}
|
|
116
129
|
}),
|
|
117
130
|
__log: () => {},
|
|
118
|
-
__trace: (payload) =>
|
|
131
|
+
__trace: (payload) => {
|
|
132
|
+
const result = window.__woodBrowserTrace?.(payload);
|
|
133
|
+
if (result && typeof result.then === 'function') {
|
|
134
|
+
window.__woodBrowserTracePending.add(result);
|
|
135
|
+
result.finally(() => window.__woodBrowserTracePending.delete(result));
|
|
136
|
+
}
|
|
137
|
+
return result;
|
|
138
|
+
},
|
|
119
139
|
__window: {
|
|
120
140
|
current: async () => ({ ok: true, windowId: 'wood-browser-harness', defaultRoute: window.__WOOD_BROWSER_DEFAULT_ROUTE__ }),
|
|
121
141
|
open: async () => ({}),
|
|
@@ -125,8 +145,13 @@ function buildBridgeInitScript(manifest, traceConfig) {
|
|
|
125
145
|
maximize: async () => ({}),
|
|
126
146
|
},
|
|
127
147
|
on(channel, callback) {
|
|
128
|
-
|
|
148
|
+
const first = !listeners.has(channel);
|
|
149
|
+
if (first) listeners.set(channel, new Set());
|
|
129
150
|
listeners.get(channel).add(callback);
|
|
151
|
+
// In real-backend mode, relay the first subscription per channel to the
|
|
152
|
+
// test process so it can subscribe on the WoodRpcTransport and forward
|
|
153
|
+
// main-process push events back into __woodBrowserEmit.
|
|
154
|
+
if (transportMode && first) window.__woodBrowserSubscribe?.(channel);
|
|
130
155
|
return () => listeners.get(channel)?.delete(callback);
|
|
131
156
|
},
|
|
132
157
|
};
|
|
@@ -177,6 +202,11 @@ function resolvePackageModule(rootDir, source) {
|
|
|
177
202
|
}
|
|
178
203
|
}
|
|
179
204
|
}
|
|
205
|
+
function resolveEsmModule(rootDir, source) {
|
|
206
|
+
const resolved = resolvePackageModule(rootDir, source);
|
|
207
|
+
const esm = resolved?.endsWith(".cjs") ? resolved.replace(/\.cjs$/, ".js") : resolved;
|
|
208
|
+
return esm && fs.existsSync(esm) ? viteImport(esm) : source;
|
|
209
|
+
}
|
|
180
210
|
function resolvePackageDir(rootDir, packageName) {
|
|
181
211
|
const packageJson = resolvePackageModule(rootDir, `${packageName}/package.json`);
|
|
182
212
|
return packageJson ? path.dirname(packageJson) : null;
|
|
@@ -487,9 +517,22 @@ window.__WOOD_BROWSER_APP__ = mount(NavigationShell, {
|
|
|
487
517
|
`;
|
|
488
518
|
}
|
|
489
519
|
function buildGeneratedAppEntrySource(input) {
|
|
520
|
+
const hasRouteContext = Object.keys(input.route.params).length > 0 || Object.keys(input.route.query).length > 0;
|
|
521
|
+
const rendererTraceEnable = input.enableRendererTrace ? `try {
|
|
522
|
+
const traceConfiguration = await getContainer().instance(TraceProviderConfiguration);
|
|
523
|
+
traceConfiguration.configureRuntime('renderer');
|
|
524
|
+
traceConfiguration.configureMinLevel(${JSON.stringify(input.traceMinLevel)});
|
|
525
|
+
traceConfiguration.configureEnabled(true);
|
|
526
|
+
} catch (error) { /* best-effort renderer-trace enable */ }` : "";
|
|
527
|
+
const navigationApply = hasRouteContext ? `getNavigation().replace(${JSON.stringify(input.routeKey)}, ${JSON.stringify(input.route.params)}, ${JSON.stringify(input.route.query)});` : "";
|
|
490
528
|
return `import { app } from '${viteImport(input.generatedApp)}';
|
|
529
|
+
import { getNavigation } from '${input.navigationModule}';
|
|
530
|
+
import { getContainer, TraceProviderConfiguration } from '${input.clientModule}';
|
|
491
531
|
window.__WOOD_BROWSER_DEFAULT_ROUTE__ = ${JSON.stringify(input.routeKey)};
|
|
492
532
|
window.__WOOD_BROWSER_APP__ = await app(document.getElementById('app'));
|
|
533
|
+
window.__WOOD_BROWSER_NAV__ = getNavigation();
|
|
534
|
+
${rendererTraceEnable}
|
|
535
|
+
${navigationApply}
|
|
493
536
|
`;
|
|
494
537
|
}
|
|
495
538
|
function buildComponentEntrySource(input) {
|
|
@@ -530,7 +573,7 @@ window.__WOOD_BROWSER_APP__ = mount(WoodRecursiveRender, {
|
|
|
530
573
|
});
|
|
531
574
|
`;
|
|
532
575
|
}
|
|
533
|
-
async function installBridge(page, bridgeState, manifest, bridgeInit) {
|
|
576
|
+
async function installBridge(page, bridgeState, manifest, bridgeInit, transportCleanups) {
|
|
534
577
|
await page.exposeBinding("__woodBrowserInvoke", async (_source, channel, input) => {
|
|
535
578
|
const call = {
|
|
536
579
|
channel,
|
|
@@ -540,17 +583,39 @@ async function installBridge(page, bridgeState, manifest, bridgeInit) {
|
|
|
540
583
|
if (bridgeState.record) {
|
|
541
584
|
bridgeState.calls.push(call);
|
|
542
585
|
}
|
|
586
|
+
if (bridgeState.transport && !bridgeState.operations.has(channel)) {
|
|
587
|
+
return await bridgeState.transport.invoke(channel, input);
|
|
588
|
+
}
|
|
543
589
|
const handler = bridgeState.operations.get(channel);
|
|
544
590
|
if (typeof handler === "function") {
|
|
545
591
|
return await handler(input, { channel, calls: bridgeState.calls });
|
|
546
592
|
}
|
|
547
593
|
return handler;
|
|
548
594
|
});
|
|
595
|
+
await installTransportEventForwarding(page, bridgeState, transportCleanups);
|
|
549
596
|
await page.addInitScript(bridgeInit);
|
|
550
597
|
await page.addInitScript((entries) => {
|
|
551
598
|
window.__WOOD_BROWSER_BRIDGE_MANIFEST__ = entries;
|
|
552
599
|
}, manifest);
|
|
553
600
|
}
|
|
601
|
+
async function installTransportEventForwarding(page, bridgeState, transportCleanups) {
|
|
602
|
+
const transport = bridgeState.transport;
|
|
603
|
+
if (!transport) return;
|
|
604
|
+
const subscribed = /* @__PURE__ */ new Set();
|
|
605
|
+
await page.exposeBinding("__woodBrowserSubscribe", (_source, channel) => {
|
|
606
|
+
if (subscribed.has(channel)) return;
|
|
607
|
+
subscribed.add(channel);
|
|
608
|
+
const unsubscribe = transport.subscribe(channel, (payload) => {
|
|
609
|
+
void page.evaluate(
|
|
610
|
+
({ eventChannel, eventPayload }) => {
|
|
611
|
+
window.__woodBrowserEmit?.(eventChannel, eventPayload);
|
|
612
|
+
},
|
|
613
|
+
{ eventChannel: channel, eventPayload: payload }
|
|
614
|
+
).catch(() => void 0);
|
|
615
|
+
});
|
|
616
|
+
transportCleanups.push(unsubscribe);
|
|
617
|
+
});
|
|
618
|
+
}
|
|
554
619
|
async function installTraceBinding(page, traceRecorder, context) {
|
|
555
620
|
if (!traceRecorder) {
|
|
556
621
|
return;
|
|
@@ -634,6 +699,259 @@ async function elementRect(page, selector) {
|
|
|
634
699
|
};
|
|
635
700
|
}).catch(() => null);
|
|
636
701
|
}
|
|
702
|
+
async function firstRect(page, selector) {
|
|
703
|
+
return await page.locator(selector).first().evaluate((element) => {
|
|
704
|
+
const rect = element.getBoundingClientRect();
|
|
705
|
+
return {
|
|
706
|
+
top: rect.top,
|
|
707
|
+
left: rect.left,
|
|
708
|
+
right: rect.right,
|
|
709
|
+
bottom: rect.bottom,
|
|
710
|
+
width: rect.width,
|
|
711
|
+
height: rect.height
|
|
712
|
+
};
|
|
713
|
+
}).catch(() => null);
|
|
714
|
+
}
|
|
715
|
+
function describeHitElement(hit) {
|
|
716
|
+
if (!hit.tag) return "<no element at point>";
|
|
717
|
+
const id = hit.id ? `#${hit.id}` : "";
|
|
718
|
+
const classes = hit.classes.length > 0 ? `.${hit.classes.join(".")}` : "";
|
|
719
|
+
return `<${hit.tag.toLowerCase()}${id}${classes}>`;
|
|
720
|
+
}
|
|
721
|
+
async function capturePaintOrders(page, selectors) {
|
|
722
|
+
const context = page.context?.();
|
|
723
|
+
if (!context || typeof context.newCDPSession !== "function") {
|
|
724
|
+
throw new Error(
|
|
725
|
+
"inFrontOf paint-order comparison requires a Chromium CDP session (page.context().newCDPSession). It is Chromium-only; pass { at } to use the point hit-test form on other browsers."
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
const cdp = await context.newCDPSession(page);
|
|
729
|
+
try {
|
|
730
|
+
await cdp.send("DOM.enable");
|
|
731
|
+
const { root } = await cdp.send("DOM.getDocument", { depth: -1 });
|
|
732
|
+
const backendIds = /* @__PURE__ */ new Map();
|
|
733
|
+
for (const selector of selectors) {
|
|
734
|
+
let backendNodeId = null;
|
|
735
|
+
try {
|
|
736
|
+
const { nodeId } = await cdp.send("DOM.querySelector", { nodeId: root.nodeId, selector });
|
|
737
|
+
if (nodeId) {
|
|
738
|
+
const described = await cdp.send("DOM.describeNode", { nodeId });
|
|
739
|
+
backendNodeId = described.node?.backendNodeId ?? null;
|
|
740
|
+
}
|
|
741
|
+
} catch {
|
|
742
|
+
backendNodeId = null;
|
|
743
|
+
}
|
|
744
|
+
backendIds.set(selector, backendNodeId);
|
|
745
|
+
}
|
|
746
|
+
const snapshot = await cdp.send("DOMSnapshot.captureSnapshot", {
|
|
747
|
+
computedStyles: [],
|
|
748
|
+
includePaintOrder: true,
|
|
749
|
+
includeDOMRects: false
|
|
750
|
+
});
|
|
751
|
+
const paintByBackend = collectMaxPaintOrders(snapshot);
|
|
752
|
+
const result = /* @__PURE__ */ new Map();
|
|
753
|
+
for (const selector of selectors) {
|
|
754
|
+
const backendId = backendIds.get(selector) ?? null;
|
|
755
|
+
const paint = backendId == null ? void 0 : paintByBackend.get(backendId);
|
|
756
|
+
result.set(selector, { paintOrder: paint ?? null, found: paint !== void 0 });
|
|
757
|
+
}
|
|
758
|
+
return result;
|
|
759
|
+
} finally {
|
|
760
|
+
await cdp.detach().catch(() => void 0);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
function collectMaxPaintOrders(snapshot) {
|
|
764
|
+
const paintByBackend = /* @__PURE__ */ new Map();
|
|
765
|
+
for (const doc of snapshot.documents ?? []) {
|
|
766
|
+
const backendOfNode = doc.nodes?.backendNodeId ?? [];
|
|
767
|
+
const nodeIndex = doc.layout?.nodeIndex ?? [];
|
|
768
|
+
const paintOrders = doc.layout?.paintOrders ?? [];
|
|
769
|
+
for (let i = 0; i < nodeIndex.length; i += 1) {
|
|
770
|
+
const paint = paintOrders[i];
|
|
771
|
+
const backendId = backendOfNode[nodeIndex[i]];
|
|
772
|
+
if (paint === void 0 || backendId === void 0) continue;
|
|
773
|
+
const prev = paintByBackend.get(backendId);
|
|
774
|
+
if (prev === void 0 || paint > prev) paintByBackend.set(backendId, paint);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
return paintByBackend;
|
|
778
|
+
}
|
|
779
|
+
function axisEdges(rect, axis) {
|
|
780
|
+
return axis === "horizontal" ? { leading: rect.left, trailing: rect.right } : { leading: rect.top, trailing: rect.bottom };
|
|
781
|
+
}
|
|
782
|
+
function axisGap(first, second, axis) {
|
|
783
|
+
const a = axisEdges(first, axis);
|
|
784
|
+
const b = axisEdges(second, axis);
|
|
785
|
+
const [earlier, later] = a.leading <= b.leading ? [a, b] : [b, a];
|
|
786
|
+
return later.leading - earlier.trailing;
|
|
787
|
+
}
|
|
788
|
+
function describeGapConstraint(constraint) {
|
|
789
|
+
const parts = [];
|
|
790
|
+
if (constraint.equals !== void 0) parts.push(`equals ${constraint.equals}`);
|
|
791
|
+
if (constraint.min !== void 0) parts.push(`min ${constraint.min}`);
|
|
792
|
+
if (constraint.max !== void 0) parts.push(`max ${constraint.max}`);
|
|
793
|
+
return parts.join(", ");
|
|
794
|
+
}
|
|
795
|
+
function gapConstraintViolation(gap, constraint, tolerance) {
|
|
796
|
+
if (constraint.equals !== void 0 && Math.abs(gap - constraint.equals) > tolerance) {
|
|
797
|
+
return `expected gap to equal ${constraint.equals} (\xB1${tolerance})`;
|
|
798
|
+
}
|
|
799
|
+
if (constraint.min !== void 0 && gap < constraint.min - tolerance) {
|
|
800
|
+
return `expected gap >= ${constraint.min}`;
|
|
801
|
+
}
|
|
802
|
+
if (constraint.max !== void 0 && gap > constraint.max + tolerance) {
|
|
803
|
+
return `expected gap <= ${constraint.max}`;
|
|
804
|
+
}
|
|
805
|
+
return null;
|
|
806
|
+
}
|
|
807
|
+
function rectsOverlap(a, b) {
|
|
808
|
+
return Boolean(
|
|
809
|
+
a && b && a.left < b.right && a.right > b.left && a.top < b.bottom && a.bottom > b.top
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
const COVERAGE_STYLE_KEYS = [
|
|
813
|
+
"opacity",
|
|
814
|
+
"visibility",
|
|
815
|
+
"background-color",
|
|
816
|
+
"background-image",
|
|
817
|
+
"border-top-width",
|
|
818
|
+
"border-right-width",
|
|
819
|
+
"border-bottom-width",
|
|
820
|
+
"border-left-width"
|
|
821
|
+
];
|
|
822
|
+
function colorAlphaIsZero(color) {
|
|
823
|
+
if (color === "transparent") return true;
|
|
824
|
+
const match = color.match(/^rgba?\(([^)]*)\)/);
|
|
825
|
+
if (!match) return false;
|
|
826
|
+
const parts = match[1].split(",").map((s) => s.trim());
|
|
827
|
+
return parts.length >= 4 && parseFloat(parts[3]) === 0;
|
|
828
|
+
}
|
|
829
|
+
function snapshotRectsOverlap(a, b) {
|
|
830
|
+
return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
|
|
831
|
+
}
|
|
832
|
+
function indexLayoutNodes(doc, strings) {
|
|
833
|
+
const byNode = /* @__PURE__ */ new Map();
|
|
834
|
+
const nodeIndex = doc.layout?.nodeIndex ?? [];
|
|
835
|
+
const bounds = doc.layout?.bounds ?? [];
|
|
836
|
+
const paintOrders = doc.layout?.paintOrders ?? [];
|
|
837
|
+
const styleRows = doc.layout?.styles ?? [];
|
|
838
|
+
for (let k = 0; k < nodeIndex.length; k += 1) {
|
|
839
|
+
const ni = nodeIndex[k];
|
|
840
|
+
const paint = paintOrders[k] ?? 0;
|
|
841
|
+
const existing = byNode.get(ni);
|
|
842
|
+
if (existing) {
|
|
843
|
+
if (paint > existing.paint) existing.paint = paint;
|
|
844
|
+
continue;
|
|
845
|
+
}
|
|
846
|
+
const b = bounds[k] ?? [0, 0, 0, 0];
|
|
847
|
+
const styles = {};
|
|
848
|
+
const row = styleRows[k] ?? [];
|
|
849
|
+
COVERAGE_STYLE_KEYS.forEach((key, j) => {
|
|
850
|
+
styles[key] = row[j] === void 0 ? "" : strings[row[j]];
|
|
851
|
+
});
|
|
852
|
+
byNode.set(ni, { rect: { x: b[0], y: b[1], w: b[2], h: b[3] }, paint, styles });
|
|
853
|
+
}
|
|
854
|
+
return byNode;
|
|
855
|
+
}
|
|
856
|
+
function nodeIndexForBackendId(nodes, backendId) {
|
|
857
|
+
const backendIds = nodes.backendNodeId ?? [];
|
|
858
|
+
for (let i = 0; i < backendIds.length; i += 1) if (backendIds[i] === backendId) return i;
|
|
859
|
+
return -1;
|
|
860
|
+
}
|
|
861
|
+
function isSnapshotAncestor(parentIndex, a, b) {
|
|
862
|
+
let cursor = parentIndex[b];
|
|
863
|
+
while (cursor >= 0) {
|
|
864
|
+
if (cursor === a) return true;
|
|
865
|
+
cursor = parentIndex[cursor];
|
|
866
|
+
}
|
|
867
|
+
return false;
|
|
868
|
+
}
|
|
869
|
+
function snapshotNodeHasOwnContent(nodes, strings, i) {
|
|
870
|
+
const tag = (strings[nodes.nodeName[i]] ?? "").toUpperCase();
|
|
871
|
+
if (["IMG", "SVG", "CANVAS", "VIDEO", "INPUT", "TEXTAREA", "SELECT", "PICTURE"].includes(tag)) return true;
|
|
872
|
+
const parentIndex = nodes.parentIndex ?? [];
|
|
873
|
+
const nodeType = nodes.nodeType ?? [];
|
|
874
|
+
const nodeValue = nodes.nodeValue ?? [];
|
|
875
|
+
for (let c = 0; c < parentIndex.length; c += 1) {
|
|
876
|
+
if (parentIndex[c] === i && nodeType[c] === 3) {
|
|
877
|
+
const v = nodeValue[c];
|
|
878
|
+
if (v >= 0 && (strings[v] ?? "").trim().length > 0) return true;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
return false;
|
|
882
|
+
}
|
|
883
|
+
function snapshotNodePaintsNothing(styles, hasOwnContent) {
|
|
884
|
+
if (styles.opacity === "0") return true;
|
|
885
|
+
if (styles.visibility === "hidden") return true;
|
|
886
|
+
const noBackground = colorAlphaIsZero(styles["background-color"] ?? "") && (styles["background-image"] === "none" || !styles["background-image"]);
|
|
887
|
+
const noBorder = ["border-top-width", "border-right-width", "border-bottom-width", "border-left-width"].every((key) => (styles[key] || "0px") === "0px");
|
|
888
|
+
return noBackground && noBorder && !hasOwnContent;
|
|
889
|
+
}
|
|
890
|
+
function describeSnapshotNode(nodes, strings, i) {
|
|
891
|
+
const tag = (strings[nodes.nodeName[i]] ?? "").toLowerCase();
|
|
892
|
+
const attrs = nodes.attributes?.[i] ?? [];
|
|
893
|
+
let id = "";
|
|
894
|
+
let classes = "";
|
|
895
|
+
for (let a = 0; a + 1 < attrs.length; a += 2) {
|
|
896
|
+
const name = strings[attrs[a]];
|
|
897
|
+
const value = strings[attrs[a + 1]] ?? "";
|
|
898
|
+
if (name === "id") id = value ? `#${value}` : "";
|
|
899
|
+
else if (name === "class") classes = value ? `.${value.trim().split(/\s+/).join(".")}` : "";
|
|
900
|
+
}
|
|
901
|
+
const pseudo = nodes.pseudoType?.[i] !== void 0 && nodes.pseudoType[i] !== null ? `::${strings[nodes.pseudoType[i]] ?? "pseudo"}` : "";
|
|
902
|
+
return `<${tag}${id}${classes}${pseudo}>`;
|
|
903
|
+
}
|
|
904
|
+
function collectCoverageCandidates(doc, strings, targetNodeIndex) {
|
|
905
|
+
const nodes = doc.nodes;
|
|
906
|
+
const parentIndex = nodes.parentIndex ?? [];
|
|
907
|
+
const nodeType = nodes.nodeType ?? [];
|
|
908
|
+
const layout = indexLayoutNodes(doc, strings);
|
|
909
|
+
const target = layout.get(targetNodeIndex);
|
|
910
|
+
if (!target) return { targetPaint: 0, targetRect: { x: 0, y: 0, w: 0, h: 0 }, covers: [] };
|
|
911
|
+
const covers = [];
|
|
912
|
+
for (const [ni, rec] of layout) {
|
|
913
|
+
if (nodeType[ni] !== 1) continue;
|
|
914
|
+
if (ni === targetNodeIndex) continue;
|
|
915
|
+
if (isSnapshotAncestor(parentIndex, ni, targetNodeIndex) || isSnapshotAncestor(parentIndex, targetNodeIndex, ni)) continue;
|
|
916
|
+
if (!snapshotRectsOverlap(rec.rect, target.rect)) continue;
|
|
917
|
+
const hasContent = snapshotNodeHasOwnContent(nodes, strings, ni);
|
|
918
|
+
if (snapshotNodePaintsNothing(rec.styles, hasContent)) continue;
|
|
919
|
+
covers.push({ description: describeSnapshotNode(nodes, strings, ni), paint: rec.paint, rect: rec.rect });
|
|
920
|
+
}
|
|
921
|
+
return { targetPaint: target.paint, targetRect: target.rect, covers };
|
|
922
|
+
}
|
|
923
|
+
async function captureCoverage(page, selector) {
|
|
924
|
+
const context = page.context?.();
|
|
925
|
+
if (!context || typeof context.newCDPSession !== "function") {
|
|
926
|
+
throw new Error(
|
|
927
|
+
"notCovered requires a Chromium CDP session (page.context().newCDPSession); it is Chromium-only."
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
const cdp = await context.newCDPSession(page);
|
|
931
|
+
try {
|
|
932
|
+
await cdp.send("DOM.enable");
|
|
933
|
+
const { root } = await cdp.send("DOM.getDocument", { depth: -1 });
|
|
934
|
+
const { nodeId } = await cdp.send("DOM.querySelector", { nodeId: root.nodeId, selector });
|
|
935
|
+
if (!nodeId) throw new Error(`Expected "${selector}" to exist for notCovered.`);
|
|
936
|
+
const described = await cdp.send("DOM.describeNode", { nodeId });
|
|
937
|
+
const backendId = described.node?.backendNodeId;
|
|
938
|
+
if (backendId == null) throw new Error(`Could not resolve backendNodeId for "${selector}".`);
|
|
939
|
+
const snapshot = await cdp.send("DOMSnapshot.captureSnapshot", {
|
|
940
|
+
computedStyles: [...COVERAGE_STYLE_KEYS],
|
|
941
|
+
includePaintOrder: true,
|
|
942
|
+
includeDOMRects: true
|
|
943
|
+
});
|
|
944
|
+
const doc = snapshot.documents?.[0];
|
|
945
|
+
if (!doc) throw new Error("DOMSnapshot returned no documents.");
|
|
946
|
+
const targetNodeIndex = nodeIndexForBackendId(doc.nodes, backendId);
|
|
947
|
+
if (targetNodeIndex < 0) {
|
|
948
|
+
throw new Error(`Expected "${selector}" to be laid out and painted for notCovered, but it was not in the paint tree.`);
|
|
949
|
+
}
|
|
950
|
+
return collectCoverageCandidates(doc, snapshot.strings ?? [], targetNodeIndex);
|
|
951
|
+
} finally {
|
|
952
|
+
await cdp.detach().catch(() => void 0);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
637
955
|
function createExpect(page, outputDir) {
|
|
638
956
|
async function waitForVisible(selector) {
|
|
639
957
|
const locator = page.locator(selector).first();
|
|
@@ -648,6 +966,64 @@ function createExpect(page, outputDir) {
|
|
|
648
966
|
throw new Error(`Expected "${selector}" to be visible.`);
|
|
649
967
|
}
|
|
650
968
|
}
|
|
969
|
+
async function hitTestAtPoint(selector, x, y) {
|
|
970
|
+
const locator = page.locator(selector).first();
|
|
971
|
+
const raw = await locator.evaluate((target, coords) => {
|
|
972
|
+
const hit = document.elementFromPoint(coords.x, coords.y);
|
|
973
|
+
if (!hit) {
|
|
974
|
+
return { found: false, tag: null, id: null, classes: [], isTarget: false, isDescendantOfTarget: false, isAncestorOfTarget: false };
|
|
975
|
+
}
|
|
976
|
+
return {
|
|
977
|
+
found: true,
|
|
978
|
+
tag: hit.tagName,
|
|
979
|
+
id: hit.id || null,
|
|
980
|
+
classes: Array.from(hit.classList),
|
|
981
|
+
isTarget: hit === target,
|
|
982
|
+
isDescendantOfTarget: hit !== target && target.contains(hit),
|
|
983
|
+
isAncestorOfTarget: hit !== target && hit.contains(target)
|
|
984
|
+
};
|
|
985
|
+
}, { x, y });
|
|
986
|
+
return {
|
|
987
|
+
...raw,
|
|
988
|
+
point: { x, y },
|
|
989
|
+
description: describeHitElement(raw)
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
async function resolvePoint(selector, point) {
|
|
993
|
+
if (point && point !== "center") return point;
|
|
994
|
+
const rect = await firstRect(page, selector);
|
|
995
|
+
if (!rect) throw new Error(`Expected "${selector}" to exist.`);
|
|
996
|
+
return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
|
|
997
|
+
}
|
|
998
|
+
async function inFrontOfByPaintOrder(topSelector, lowerSelector) {
|
|
999
|
+
const orders = await capturePaintOrders(page, [topSelector, lowerSelector]);
|
|
1000
|
+
const top = orders.get(topSelector);
|
|
1001
|
+
const lower = orders.get(lowerSelector);
|
|
1002
|
+
if (!top.found || top.paintOrder === null) {
|
|
1003
|
+
throw new Error(`Expected "${topSelector}" to have a paint-order entry, but it was not found in the paint tree (not laid out / not painted).`);
|
|
1004
|
+
}
|
|
1005
|
+
if (!lower.found || lower.paintOrder === null) {
|
|
1006
|
+
throw new Error(`Expected "${lowerSelector}" to have a paint-order entry, but it was not found in the paint tree (not laid out / not painted).`);
|
|
1007
|
+
}
|
|
1008
|
+
if (top.paintOrder > lower.paintOrder) return;
|
|
1009
|
+
const topRect = await firstRect(page, topSelector);
|
|
1010
|
+
const lowerRect = await firstRect(page, lowerSelector);
|
|
1011
|
+
const overlapNote = rectsOverlap(topRect, lowerRect) ? "" : ` (Note: the rects do not overlap, so there is no shared pixel where front/behind is visually observable \u2014 this is a global paint-order verdict.)`;
|
|
1012
|
+
throw new Error(
|
|
1013
|
+
`Expected "${topSelector}" (paint order ${top.paintOrder}) to paint in front of "${lowerSelector}" (paint order ${lower.paintOrder}), but it paints at or behind it (higher paint order == on top).${overlapNote}`
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
async function inFrontOfAtPoint(topSelector, lowerSelector, at) {
|
|
1017
|
+
const topHit = await hitTestAtPoint(topSelector, at.x, at.y);
|
|
1018
|
+
const lowerHit = await hitTestAtPoint(lowerSelector, at.x, at.y);
|
|
1019
|
+
const hitsTop = topHit.isTarget || topHit.isDescendantOfTarget;
|
|
1020
|
+
const hitsLower = lowerHit.isTarget || lowerHit.isDescendantOfTarget;
|
|
1021
|
+
if (!hitsTop || hitsLower) {
|
|
1022
|
+
throw new Error(
|
|
1023
|
+
`Expected "${topSelector}" to be painted in front of "${lowerSelector}" at (${at.x.toFixed(1)}, ${at.y.toFixed(1)}), but the top-most painted element there was ${topHit.description} (is/descends-from "${topSelector}": ${hitsTop}; is/descends-from "${lowerSelector}": ${hitsLower}).`
|
|
1024
|
+
);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
651
1027
|
return {
|
|
652
1028
|
async visible(selector) {
|
|
653
1029
|
await waitForVisible(selector);
|
|
@@ -724,6 +1100,135 @@ function createExpect(page, outputDir) {
|
|
|
724
1100
|
throw new Error(`Expected "${topSelector}" z-index (${values.top}) to be above "${lowerSelector}" (${values.lower}).`);
|
|
725
1101
|
}
|
|
726
1102
|
},
|
|
1103
|
+
async rects(selectors) {
|
|
1104
|
+
const snapshots = [];
|
|
1105
|
+
for (const selector of selectors) {
|
|
1106
|
+
const rect = await firstRect(page, selector);
|
|
1107
|
+
if (!rect) throw new Error(`Expected "${selector}" to exist.`);
|
|
1108
|
+
snapshots.push({ selector, ...rect });
|
|
1109
|
+
}
|
|
1110
|
+
return snapshots;
|
|
1111
|
+
},
|
|
1112
|
+
async hitTest(selector, point = "center") {
|
|
1113
|
+
await waitForVisible(selector);
|
|
1114
|
+
const resolved = await resolvePoint(selector, point);
|
|
1115
|
+
return hitTestAtPoint(selector, resolved.x, resolved.y);
|
|
1116
|
+
},
|
|
1117
|
+
async sharedEdge(a, b, edge, options = {}) {
|
|
1118
|
+
await waitForVisible(a);
|
|
1119
|
+
await waitForVisible(b);
|
|
1120
|
+
const tolerance = options.tolerance ?? 1;
|
|
1121
|
+
const first = await firstRect(page, a);
|
|
1122
|
+
const second = await firstRect(page, b);
|
|
1123
|
+
if (!first || !second) throw new Error(`Expected both "${a}" and "${b}" to exist.`);
|
|
1124
|
+
const delta = Math.abs(first[edge] - second[edge]);
|
|
1125
|
+
if (delta > tolerance) {
|
|
1126
|
+
throw new Error(
|
|
1127
|
+
`Expected "${a}" and "${b}" to share ${edge} edge within ${tolerance}px, but delta was ${delta.toFixed(2)}px ("${a}".${edge}=${first[edge].toFixed(2)}, "${b}".${edge}=${second[edge].toFixed(2)}). Rects: ${a}=${JSON.stringify(first)}, ${b}=${JSON.stringify(second)}`
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
1130
|
+
},
|
|
1131
|
+
async sharedAxis(a, b, axis, options = {}) {
|
|
1132
|
+
await waitForVisible(a);
|
|
1133
|
+
await waitForVisible(b);
|
|
1134
|
+
const tolerance = options.tolerance ?? 1;
|
|
1135
|
+
const first = await firstRect(page, a);
|
|
1136
|
+
const second = await firstRect(page, b);
|
|
1137
|
+
if (!first || !second) throw new Error(`Expected both "${a}" and "${b}" to exist.`);
|
|
1138
|
+
const center = (rect) => axis === "centerX" ? rect.left + rect.width / 2 : rect.top + rect.height / 2;
|
|
1139
|
+
const firstCenter = center(first);
|
|
1140
|
+
const secondCenter = center(second);
|
|
1141
|
+
const delta = Math.abs(firstCenter - secondCenter);
|
|
1142
|
+
if (delta > tolerance) {
|
|
1143
|
+
throw new Error(
|
|
1144
|
+
`Expected "${a}" and "${b}" to share ${axis} within ${tolerance}px, but delta was ${delta.toFixed(2)}px ("${a}".${axis}=${firstCenter.toFixed(2)}, "${b}".${axis}=${secondCenter.toFixed(2)}). Rects: ${a}=${JSON.stringify(first)}, ${b}=${JSON.stringify(second)}`
|
|
1145
|
+
);
|
|
1146
|
+
}
|
|
1147
|
+
},
|
|
1148
|
+
async unoccluded(selector, options = {}) {
|
|
1149
|
+
await waitForVisible(selector);
|
|
1150
|
+
const resolved = await resolvePoint(selector, options.point ?? "center");
|
|
1151
|
+
const hit = await hitTestAtPoint(selector, resolved.x, resolved.y);
|
|
1152
|
+
if (!hit.found) {
|
|
1153
|
+
throw new Error(`Expected "${selector}" to receive pixels at (${resolved.x.toFixed(1)}, ${resolved.y.toFixed(1)}), but no element was painted there.`);
|
|
1154
|
+
}
|
|
1155
|
+
if (hit.isTarget || hit.isDescendantOfTarget || hit.isAncestorOfTarget) return;
|
|
1156
|
+
throw new Error(
|
|
1157
|
+
`Expected "${selector}" to be unoccluded at (${resolved.x.toFixed(1)}, ${resolved.y.toFixed(1)}), but the painted element there was ${hit.description} \u2014 a foreign element occluding the target.`
|
|
1158
|
+
);
|
|
1159
|
+
},
|
|
1160
|
+
async inFrontOf(topSelector, lowerSelector, options = {}) {
|
|
1161
|
+
await waitForVisible(topSelector);
|
|
1162
|
+
await waitForVisible(lowerSelector);
|
|
1163
|
+
if (options.at) {
|
|
1164
|
+
await inFrontOfAtPoint(topSelector, lowerSelector, options.at);
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
await inFrontOfByPaintOrder(topSelector, lowerSelector);
|
|
1168
|
+
},
|
|
1169
|
+
async notCovered(selector) {
|
|
1170
|
+
await waitForVisible(selector);
|
|
1171
|
+
const coverage = await captureCoverage(page, selector);
|
|
1172
|
+
const covering = coverage.covers.filter((c) => c.paint > coverage.targetPaint);
|
|
1173
|
+
if (covering.length === 0) return;
|
|
1174
|
+
const worst = covering.reduce((a, b) => b.paint > a.paint ? b : a);
|
|
1175
|
+
throw new Error(
|
|
1176
|
+
`Expected "${selector}" (paint order ${coverage.targetPaint}, rect ${JSON.stringify(coverage.targetRect)}) not to be covered, but ${worst.description} paints on top (paint order ${worst.paint}, rect ${JSON.stringify(worst.rect)}). notCovered tracks PAINTED coverage from the real paint tree; a foreign element with a higher paint order that overlaps the target and paints visible pixels (background/border/text/image \u2014 the transparent-wrapper heuristic did not exempt it) fails here.`
|
|
1177
|
+
);
|
|
1178
|
+
},
|
|
1179
|
+
async gapBetween(a, b, axis, options) {
|
|
1180
|
+
if (options.equals === void 0 && options.min === void 0 && options.max === void 0) {
|
|
1181
|
+
throw new Error("gapBetween requires at least one of { equals, min, max }.");
|
|
1182
|
+
}
|
|
1183
|
+
await waitForVisible(a);
|
|
1184
|
+
await waitForVisible(b);
|
|
1185
|
+
const tolerance = options.tolerance ?? 1;
|
|
1186
|
+
const first = await firstRect(page, a);
|
|
1187
|
+
const second = await firstRect(page, b);
|
|
1188
|
+
if (!first || !second) throw new Error(`Expected both "${a}" and "${b}" to exist.`);
|
|
1189
|
+
const gap = axisGap(first, second, axis);
|
|
1190
|
+
const allowsNegative = options.equals !== void 0 && options.equals < 0 || options.min !== void 0 && options.min < 0;
|
|
1191
|
+
const rectsNote = `Rects: ${a}=${JSON.stringify(first)}, ${b}=${JSON.stringify(second)}`;
|
|
1192
|
+
if (gap < -tolerance && !allowsNegative) {
|
|
1193
|
+
throw new Error(
|
|
1194
|
+
`Expected "${a}" and "${b}" to have a non-negative ${axis} gap, but their border boxes overlap by ${(-gap).toFixed(2)}px (gap ${gap.toFixed(2)}px). Pass a negative min/equals to assert an overlap intentionally. ${rectsNote}`
|
|
1195
|
+
);
|
|
1196
|
+
}
|
|
1197
|
+
const violation = gapConstraintViolation(gap, options, tolerance);
|
|
1198
|
+
if (violation) {
|
|
1199
|
+
throw new Error(
|
|
1200
|
+
`Expected "${a}" and "${b}" ${axis} gap to satisfy ${describeGapConstraint(options)} within ${tolerance}px, but ${violation} (actual gap ${gap.toFixed(2)}px). ${rectsNote}`
|
|
1201
|
+
);
|
|
1202
|
+
}
|
|
1203
|
+
},
|
|
1204
|
+
async evenlySpaced(selectors, axis, options = {}) {
|
|
1205
|
+
if (selectors.length < 3) {
|
|
1206
|
+
throw new Error(`evenlySpaced requires at least 3 elements, got ${selectors.length}.`);
|
|
1207
|
+
}
|
|
1208
|
+
const tolerance = options.tolerance ?? 1;
|
|
1209
|
+
const boxes = [];
|
|
1210
|
+
for (const selector of selectors) {
|
|
1211
|
+
await waitForVisible(selector);
|
|
1212
|
+
const rect = await firstRect(page, selector);
|
|
1213
|
+
if (!rect) throw new Error(`Expected "${selector}" to exist.`);
|
|
1214
|
+
boxes.push({ selector, rect });
|
|
1215
|
+
}
|
|
1216
|
+
const sorted = [...boxes].sort((x, y) => axisEdges(x.rect, axis).leading - axisEdges(y.rect, axis).leading);
|
|
1217
|
+
const gaps = sorted.slice(1).map((box, i) => ({
|
|
1218
|
+
from: sorted[i].selector,
|
|
1219
|
+
to: box.selector,
|
|
1220
|
+
gap: axisGap(sorted[i].rect, box.rect, axis)
|
|
1221
|
+
}));
|
|
1222
|
+
const widest = gaps.reduce((acc, g) => g.gap > acc.gap ? g : acc);
|
|
1223
|
+
const narrowest = gaps.reduce((acc, g) => g.gap < acc.gap ? g : acc);
|
|
1224
|
+
const spread = widest.gap - narrowest.gap;
|
|
1225
|
+
if (spread > tolerance) {
|
|
1226
|
+
const list = gaps.map((g) => `${g.from}\u2192${g.to}=${g.gap.toFixed(2)}px`).join(", ");
|
|
1227
|
+
throw new Error(
|
|
1228
|
+
`Expected elements to be evenly spaced along ${axis} within ${tolerance}px, but gap spread was ${spread.toFixed(2)}px (widest ${widest.from}\u2192${widest.to}=${widest.gap.toFixed(2)}px, narrowest ${narrowest.from}\u2192${narrowest.to}=${narrowest.gap.toFixed(2)}px). Gaps: ${list}`
|
|
1229
|
+
);
|
|
1230
|
+
}
|
|
1231
|
+
},
|
|
727
1232
|
async stableAfterAnimation(selector = "body", options = {}) {
|
|
728
1233
|
const frameCount = options.frameCount ?? 3;
|
|
729
1234
|
const timeoutMs = options.timeoutMs ?? 2e3;
|
|
@@ -783,7 +1288,13 @@ async function openMountPage(input) {
|
|
|
783
1288
|
const entryPath = writeEntry(input.runtime.tempDir, input.name, input.entrySource);
|
|
784
1289
|
const htmlPath = writeHtml(input.runtime.tempDir, input.name, entryPath, input.css);
|
|
785
1290
|
await installTraceBinding(input.runtime.page, input.traceRecorder, input.traceContext ?? {});
|
|
786
|
-
await installBridge(
|
|
1291
|
+
await installBridge(
|
|
1292
|
+
page,
|
|
1293
|
+
input.bridge.state,
|
|
1294
|
+
input.bridge.manifest,
|
|
1295
|
+
input.bridge.bridgeInit,
|
|
1296
|
+
input.runtime.transportCleanups
|
|
1297
|
+
);
|
|
787
1298
|
const mountedBridge = createMountedBridge(page, input.bridge.state);
|
|
788
1299
|
const context = {
|
|
789
1300
|
page,
|
|
@@ -858,7 +1369,8 @@ async function createHarness(config = {}) {
|
|
|
858
1369
|
server: workerRuntime.server,
|
|
859
1370
|
page,
|
|
860
1371
|
tempDir,
|
|
861
|
-
rootDir
|
|
1372
|
+
rootDir,
|
|
1373
|
+
transportCleanups: []
|
|
862
1374
|
};
|
|
863
1375
|
const css = (config.css ?? []).map((cssPath) => {
|
|
864
1376
|
const resolved = resolveFromRoot(rootDir, cssPath);
|
|
@@ -889,7 +1401,15 @@ async function createHarness(config = {}) {
|
|
|
889
1401
|
} else if (config.generatedApp) {
|
|
890
1402
|
const generatedApp = resolveFromRoot(rootDir, config.generatedApp);
|
|
891
1403
|
assertFileExists(generatedApp, "Generated app");
|
|
892
|
-
entrySource = buildGeneratedAppEntrySource({
|
|
1404
|
+
entrySource = buildGeneratedAppEntrySource({
|
|
1405
|
+
generatedApp,
|
|
1406
|
+
routeKey,
|
|
1407
|
+
route,
|
|
1408
|
+
navigationModule: resolveEsmModule(rootDir, "@noego/wood/navigation"),
|
|
1409
|
+
clientModule: resolveEsmModule(rootDir, "@noego/wood/client"),
|
|
1410
|
+
enableRendererTrace: config.trace?.enabled === true,
|
|
1411
|
+
traceMinLevel: config.trace?.minLevel ?? "debug"
|
|
1412
|
+
});
|
|
893
1413
|
} else {
|
|
894
1414
|
throw new Error("mountRoute requires either viewsConfig or generatedApp in browser.createHarness().");
|
|
895
1415
|
}
|
|
@@ -986,11 +1506,20 @@ async function createHarness(config = {}) {
|
|
|
986
1506
|
hooks: config.page
|
|
987
1507
|
});
|
|
988
1508
|
},
|
|
1509
|
+
async flushRendererTraces() {
|
|
1510
|
+
await page.evaluate(() => window.__woodBrowserTraceFlush?.()).catch(() => void 0);
|
|
1511
|
+
},
|
|
989
1512
|
async destroy() {
|
|
990
1513
|
if (destroyed) {
|
|
991
1514
|
return;
|
|
992
1515
|
}
|
|
993
1516
|
destroyed = true;
|
|
1517
|
+
for (const cleanup of runtime.transportCleanups.splice(0)) {
|
|
1518
|
+
try {
|
|
1519
|
+
cleanup();
|
|
1520
|
+
} catch {
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
994
1523
|
await Promise.all([
|
|
995
1524
|
waitForBoundedClose(page.close()),
|
|
996
1525
|
waitForBoundedClose(context.close())
|