@noego/wood 0.6.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/dist/browser-test-setup.cjs +29 -0
- package/dist/browser-test-setup.cjs.map +1 -0
- package/dist/browser-test-setup.d.cts +8 -0
- package/dist/browser-test-setup.d.ts +8 -0
- package/dist/browser-test-setup.js +9 -0
- package/dist/browser-test-setup.js.map +1 -0
- package/dist/client/global_polyfill.d.cts +1 -0
- package/dist/client/global_polyfill.d.ts +1 -0
- package/dist/client/index.cjs +7 -0
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.d.cts +7 -4
- package/dist/client/index.d.ts +7 -4
- package/dist/client/index.js +6 -1
- package/dist/client/index.js.map +1 -1
- package/dist/codegen/renderer_app_generator.cjs +97 -24
- package/dist/codegen/renderer_app_generator.cjs.map +1 -1
- package/dist/codegen/renderer_app_generator.js +97 -24
- package/dist/codegen/renderer_app_generator.js.map +1 -1
- package/dist/codegen/type_generator.cjs +9 -0
- package/dist/codegen/type_generator.cjs.map +1 -1
- package/dist/codegen/type_generator.js +9 -0
- package/dist/codegen/type_generator.js.map +1 -1
- package/dist/config/config_parser.cjs +10 -0
- package/dist/config/config_parser.cjs.map +1 -1
- package/dist/config/config_parser.js +10 -0
- package/dist/config/config_parser.js.map +1 -1
- package/dist/fetch.cjs +38 -0
- package/dist/fetch.cjs.map +1 -0
- package/dist/fetch.d.cts +7 -0
- package/dist/fetch.d.ts +7 -0
- package/dist/fetch.js +12 -0
- package/dist/fetch.js.map +1 -0
- package/dist/frontend/frontend_page.cjs +3 -3
- package/dist/frontend/frontend_page.cjs.map +1 -1
- package/dist/frontend/frontend_page.d.cts +1 -1
- package/dist/frontend/frontend_page.d.ts +1 -1
- package/dist/frontend/frontend_page.js +3 -3
- package/dist/frontend/frontend_page.js.map +1 -1
- package/dist/frontend/frontend_surface.cjs +31 -17
- package/dist/frontend/frontend_surface.cjs.map +1 -1
- package/dist/frontend/frontend_surface.d.cts +12 -2
- package/dist/frontend/frontend_surface.d.ts +12 -2
- package/dist/frontend/frontend_surface.js +37 -17
- package/dist/frontend/frontend_surface.js.map +1 -1
- package/dist/frontend/index.d.cts +2 -0
- package/dist/frontend/index.d.ts +2 -0
- package/dist/index.cjs +5 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +25 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/loader/index.cjs +19 -2
- package/dist/loader/index.cjs.map +1 -1
- package/dist/loader/index.d.cts +1 -0
- package/dist/loader/index.d.ts +1 -0
- package/dist/loader/index.js +19 -1
- package/dist/loader/index.js.map +1 -1
- package/dist/loader/loader_orchestration.cjs +82 -0
- package/dist/loader/loader_orchestration.cjs.map +1 -0
- package/dist/loader/loader_orchestration.d.cts +66 -0
- package/dist/loader/loader_orchestration.d.ts +66 -0
- package/dist/loader/loader_orchestration.js +51 -0
- package/dist/loader/loader_orchestration.js.map +1 -0
- package/dist/loader/loader_runner.cjs +46 -41
- package/dist/loader/loader_runner.cjs.map +1 -1
- package/dist/loader/loader_runner.d.cts +12 -6
- package/dist/loader/loader_runner.d.ts +12 -6
- package/dist/loader/loader_runner.js +50 -41
- package/dist/loader/loader_runner.js.map +1 -1
- package/dist/native_browser.types-BY2daUOS.d.ts +189 -0
- package/dist/native_browser.types-vWXtE4rM.d.cts +189 -0
- package/dist/navigation/index.cjs +15 -2
- package/dist/navigation/index.cjs.map +1 -1
- package/dist/navigation/index.d.cts +2 -0
- package/dist/navigation/index.d.ts +2 -0
- package/dist/navigation/index.js +8 -1
- package/dist/navigation/index.js.map +1 -1
- package/dist/runtime/get_container.cjs +2 -0
- package/dist/runtime/get_container.cjs.map +1 -1
- package/dist/runtime/get_container.js +2 -0
- package/dist/runtime/get_container.js.map +1 -1
- package/dist/runtime/index.cjs +5 -0
- package/dist/runtime/index.cjs.map +1 -1
- package/dist/runtime/index.d.cts +2 -0
- package/dist/runtime/index.d.ts +2 -0
- package/dist/runtime/index.js +3 -0
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/runtime_capabilities.cjs +35 -0
- package/dist/runtime/runtime_capabilities.cjs.map +1 -0
- package/dist/runtime/runtime_capabilities.d.cts +6 -0
- package/dist/runtime/runtime_capabilities.d.ts +6 -0
- package/dist/runtime/runtime_capabilities.js +11 -0
- package/dist/runtime/runtime_capabilities.js.map +1 -0
- package/dist/runtime/runtime_module.cjs +2 -0
- package/dist/runtime/runtime_module.cjs.map +1 -1
- package/dist/runtime/runtime_module.js +2 -0
- package/dist/runtime/runtime_module.js.map +1 -1
- package/dist/test-wood.cjs +150 -0
- package/dist/test-wood.cjs.map +1 -0
- package/dist/test-wood.d.cts +62 -0
- package/dist/test-wood.d.ts +62 -0
- package/dist/test-wood.js +115 -0
- package/dist/test-wood.js.map +1 -0
- package/dist/testing/browser.cjs +92 -163
- package/dist/testing/browser.cjs.map +1 -1
- package/dist/testing/browser.d.cts +14 -13
- package/dist/testing/browser.d.ts +14 -13
- package/dist/testing/browser.js +88 -162
- package/dist/testing/browser.js.map +1 -1
- package/dist/testing/create_controller_harness.d.cts +2 -1
- package/dist/testing/create_controller_harness.d.ts +2 -1
- package/dist/testing/create_router_bridge.cjs +19 -15
- package/dist/testing/create_router_bridge.cjs.map +1 -1
- package/dist/testing/create_router_bridge.d.cts +8 -1
- package/dist/testing/create_router_bridge.d.ts +8 -1
- package/dist/testing/create_router_bridge.js +18 -15
- package/dist/testing/create_router_bridge.js.map +1 -1
- package/dist/testing/index.cjs +12 -4
- package/dist/testing/index.cjs.map +1 -1
- package/dist/testing/index.d.cts +11 -5
- package/dist/testing/index.d.ts +11 -5
- package/dist/testing/index.js +9 -3
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/native_browser/browser_pool.cjs +31 -0
- package/dist/testing/native_browser/browser_pool.cjs.map +1 -0
- package/dist/testing/native_browser/browser_pool.d.cts +1 -0
- package/dist/testing/native_browser/browser_pool.d.ts +1 -0
- package/dist/testing/native_browser/browser_pool.js +6 -0
- package/dist/testing/native_browser/browser_pool.js.map +1 -0
- package/dist/testing/native_browser/happy_dom_tab.cjs +107 -0
- package/dist/testing/native_browser/happy_dom_tab.cjs.map +1 -0
- package/dist/testing/native_browser/happy_dom_tab.d.cts +35 -0
- package/dist/testing/native_browser/happy_dom_tab.d.ts +35 -0
- package/dist/testing/native_browser/happy_dom_tab.js +73 -0
- package/dist/testing/native_browser/happy_dom_tab.js.map +1 -0
- package/dist/testing/native_browser/happy_dom_transfer.cjs +29 -0
- package/dist/testing/native_browser/happy_dom_transfer.cjs.map +1 -0
- package/dist/testing/native_browser/happy_dom_transfer.d.cts +1 -0
- package/dist/testing/native_browser/happy_dom_transfer.d.ts +1 -0
- package/dist/testing/native_browser/happy_dom_transfer.js +5 -0
- package/dist/testing/native_browser/happy_dom_transfer.js.map +1 -0
- package/dist/testing/native_browser/host_transport.cjs +89 -0
- package/dist/testing/native_browser/host_transport.cjs.map +1 -0
- package/dist/testing/native_browser/host_transport.d.cts +44 -0
- package/dist/testing/native_browser/host_transport.d.ts +44 -0
- package/dist/testing/native_browser/host_transport.js +65 -0
- package/dist/testing/native_browser/host_transport.js.map +1 -0
- package/dist/testing/native_browser/native_browser.types.cjs +17 -0
- package/dist/testing/native_browser/native_browser.types.cjs.map +1 -0
- package/dist/testing/native_browser/native_browser.types.d.cts +31 -0
- package/dist/testing/native_browser/native_browser.types.d.ts +31 -0
- package/dist/testing/native_browser/native_browser.types.js +1 -0
- package/dist/testing/native_browser/native_browser.types.js.map +1 -0
- package/dist/testing/native_browser/native_browser_environment.cjs +143 -0
- package/dist/testing/native_browser/native_browser_environment.cjs.map +1 -0
- package/dist/testing/native_browser/native_browser_environment.d.cts +36 -0
- package/dist/testing/native_browser/native_browser_environment.d.ts +36 -0
- package/dist/testing/native_browser/native_browser_environment.js +109 -0
- package/dist/testing/native_browser/native_browser_environment.js.map +1 -0
- package/dist/testing/native_browser/native_browser_lifecycle.cjs +31 -0
- package/dist/testing/native_browser/native_browser_lifecycle.cjs.map +1 -0
- package/dist/testing/native_browser/native_browser_lifecycle.d.cts +1 -0
- package/dist/testing/native_browser/native_browser_lifecycle.d.ts +1 -0
- package/dist/testing/native_browser/native_browser_lifecycle.js +6 -0
- package/dist/testing/native_browser/native_browser_lifecycle.js.map +1 -0
- package/dist/testing/native_browser/playwright_tab.cjs +124 -0
- package/dist/testing/native_browser/playwright_tab.cjs.map +1 -0
- package/dist/testing/native_browser/playwright_tab.d.cts +35 -0
- package/dist/testing/native_browser/playwright_tab.d.ts +35 -0
- package/dist/testing/native_browser/playwright_tab.js +100 -0
- package/dist/testing/native_browser/playwright_tab.js.map +1 -0
- package/dist/testing/native_browser/renderer_compilation.cjs +149 -0
- package/dist/testing/native_browser/renderer_compilation.cjs.map +1 -0
- package/dist/testing/native_browser/renderer_compilation.d.cts +35 -0
- package/dist/testing/native_browser/renderer_compilation.d.ts +35 -0
- package/dist/testing/native_browser/renderer_compilation.js +115 -0
- package/dist/testing/native_browser/renderer_compilation.js.map +1 -0
- package/dist/testing/native_browser/renderer_entry.cjs +148 -0
- package/dist/testing/native_browser/renderer_entry.cjs.map +1 -0
- package/dist/testing/native_browser/renderer_entry.d.cts +13 -0
- package/dist/testing/native_browser/renderer_entry.d.ts +13 -0
- package/dist/testing/native_browser/renderer_entry.js +114 -0
- package/dist/testing/native_browser/renderer_entry.js.map +1 -0
- package/dist/testing/registration_only_root.cjs +61 -5
- package/dist/testing/registration_only_root.cjs.map +1 -1
- package/dist/testing/registration_only_root.d.cts +25 -4
- package/dist/testing/registration_only_root.d.ts +25 -4
- package/dist/testing/registration_only_root.js +60 -5
- package/dist/testing/registration_only_root.js.map +1 -1
- package/dist/testing/runner_importer.cjs +75 -0
- package/dist/testing/runner_importer.cjs.map +1 -0
- package/dist/testing/runner_importer.d.cts +8 -0
- package/dist/testing/runner_importer.d.ts +8 -0
- package/dist/testing/runner_importer.js +41 -0
- package/dist/testing/runner_importer.js.map +1 -0
- package/dist/testing/test_wood_config.cjs +387 -272
- package/dist/testing/test_wood_config.cjs.map +1 -1
- package/dist/testing/test_wood_config.d.cts +18 -123
- package/dist/testing/test_wood_config.d.ts +18 -123
- package/dist/testing/test_wood_config.js +396 -270
- package/dist/testing/test_wood_config.js.map +1 -1
- package/dist/testing/wood_config_source.cjs +12 -0
- package/dist/testing/wood_config_source.cjs.map +1 -1
- package/dist/testing/wood_config_source.d.cts +10 -0
- package/dist/testing/wood_config_source.d.ts +10 -0
- package/dist/testing/wood_config_source.js +12 -0
- package/dist/testing/wood_config_source.js.map +1 -1
- package/dist/testing/wood_env_renderer.cjs +28 -3
- package/dist/testing/wood_env_renderer.cjs.map +1 -1
- package/dist/testing/wood_env_renderer.d.cts +12 -1
- package/dist/testing/wood_env_renderer.d.ts +12 -1
- package/dist/testing/wood_env_renderer.js +28 -3
- package/dist/testing/wood_env_renderer.js.map +1 -1
- package/dist/testing/wood_lazy_runtime.cjs +109 -0
- package/dist/testing/wood_lazy_runtime.cjs.map +1 -0
- package/dist/testing/wood_lazy_runtime.d.cts +36 -0
- package/dist/testing/wood_lazy_runtime.d.ts +36 -0
- package/dist/testing/wood_lazy_runtime.js +84 -0
- package/dist/testing/wood_lazy_runtime.js.map +1 -0
- package/dist/testing/wood_test_selection.cjs +34 -55
- package/dist/testing/wood_test_selection.cjs.map +1 -1
- package/dist/testing/wood_test_selection.d.cts +18 -24
- package/dist/testing/wood_test_selection.d.ts +18 -24
- package/dist/testing/wood_test_selection.js +30 -54
- package/dist/testing/wood_test_selection.js.map +1 -1
- package/dist/testing-api.cjs +17 -0
- package/dist/testing-api.cjs.map +1 -0
- package/dist/testing-api.d.cts +18 -0
- package/dist/testing-api.d.ts +18 -0
- package/dist/testing-api.js +1 -0
- package/dist/testing-api.js.map +1 -0
- package/dist/types/config.cjs.map +1 -1
- package/dist/types/config.d.cts +4 -0
- package/dist/types/config.d.ts +4 -0
- package/docs/native-browser-testing.md +164 -0
- package/package.json +31 -5
- package/src/components/NavigationShell.browser.test.ts +10 -10
- package/src/components/NavigationShell.cleanup.browser.test.ts +57 -0
- package/src/components/NavigationShell.scopes.browser.test.ts +5 -5
- package/src/components/NavigationShell.svelte +50 -24
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# Native renderer test hosts
|
|
2
|
+
|
|
3
|
+
`testWood(config)` now accepts an explicit renderer host. This is application
|
|
4
|
+
composition and lifecycle support, not a new browser-testing language.
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
import { testWood } from '@noego/wood';
|
|
8
|
+
import { resourceCase } from '@noego/testing';
|
|
9
|
+
import { it, expect } from 'vitest';
|
|
10
|
+
|
|
11
|
+
it('renders a conversation', resourceCase(async () => {
|
|
12
|
+
const env = await testWood('./wood.config.yml')
|
|
13
|
+
.select({main: {}, renderer: {path: ['main.chat']}})
|
|
14
|
+
.browser()
|
|
15
|
+
.use(preparedDatabase.stub)
|
|
16
|
+
.build();
|
|
17
|
+
|
|
18
|
+
const page = await env.open('main.chat', {
|
|
19
|
+
params: {id: '42'},
|
|
20
|
+
query: {filter: 'active'},
|
|
21
|
+
});
|
|
22
|
+
// The return value IS playwright.Page, not a proxy or Wood surface.
|
|
23
|
+
await page.getByRole('button', {name: 'Send'}).click();
|
|
24
|
+
await expect.poll(() => page.locator('.message').count()).toBe(1);
|
|
25
|
+
}));
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The test still declares any necessary schema/JSON data and application seams.
|
|
29
|
+
There is no automatic migration execution or copied application database.
|
|
30
|
+
|
|
31
|
+
## Host and types
|
|
32
|
+
|
|
33
|
+
`.browser()` and `.browser('playwright')` select Playwright/Chromium. `open()`
|
|
34
|
+
returns `Promise<Page>` from Playwright. Use the ordinary locator, evaluate,
|
|
35
|
+
screenshot, context and page APIs. The test runner's assertions remain the test
|
|
36
|
+
runner's; Wood does not wrap them.
|
|
37
|
+
|
|
38
|
+
`.browser('happy_dom')` selects a real `Window` from happy-dom:
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
const env = await testWood('./wood.config.yml')
|
|
42
|
+
.select({main: {}, renderer: {path: ['main.chat']}})
|
|
43
|
+
.browser('happy_dom')
|
|
44
|
+
.build();
|
|
45
|
+
const window = await env.open('main.chat');
|
|
46
|
+
window.document.querySelector('button')?.click();
|
|
47
|
+
await env.settle();
|
|
48
|
+
expect(window.document.querySelector('.message')?.textContent).toBe('Sent');
|
|
49
|
+
await env.dispose();
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The chosen type survives `.select()`, `.use()`, and replacement methods in
|
|
53
|
+
any order. These native environments intentionally do not expose Wood's old
|
|
54
|
+
`click`, `fill`, `exists`, `frontend`, or `rendererRoot` surfaces. Main `api`,
|
|
55
|
+
`rawApi`, `get`, `controller`, and the main root remain available. There is no
|
|
56
|
+
phantom second renderer container in the Node test process.
|
|
57
|
+
|
|
58
|
+
No `.browser()` call preserves the pre-existing testWood behavior, including
|
|
59
|
+
runner-provided DOM hosting, during migration. This additive release does not
|
|
60
|
+
change every old `open()` call's return type.
|
|
61
|
+
|
|
62
|
+
## One browser for the run, many isolated tabs
|
|
63
|
+
|
|
64
|
+
Use the framework's Vitest global setup for one Chromium BrowserServer across
|
|
65
|
+
all fork workers:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
export default defineConfig({
|
|
69
|
+
test: {
|
|
70
|
+
environment: 'node',
|
|
71
|
+
globalSetup: ['@noego/wood/browser-test-setup'],
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
An existing custom global setup can instead call
|
|
77
|
+
`startSharedWoodBrowser(rootDir)` from `@noego/wood/testing` and return its
|
|
78
|
+
`dispose()` as part of runner teardown. An existing `WOOD_BROWSER_WS_ENDPOINT`
|
|
79
|
+
is borrowed, not replaced or closed by a second setup owner.
|
|
80
|
+
|
|
81
|
+
Every built environment lazily owns its own BrowserContext. Each `open()`
|
|
82
|
+
creates a Page in that context; tabs belonging to that environment share its
|
|
83
|
+
browser storage and main backend, while different environments do not share
|
|
84
|
+
cookies/storage or backend roots. `env.dispose()` closes only its pages and
|
|
85
|
+
context. The runner closes the shared Chromium server. Ordinary cases should close their
|
|
86
|
+
environment or Page, not call `close()` on the borrowed shared Browser. With no global setup,
|
|
87
|
+
Wood uses a worker-local browser pool; that is reuse per worker, not a promise
|
|
88
|
+
of one browser across processes. An explicit shared endpoint that fails to
|
|
89
|
+
connect produces an error rather than silently launching another Chromium.
|
|
90
|
+
|
|
91
|
+
Happy-dom needs no Chromium. Each open owns a separate native Window/VM realm,
|
|
92
|
+
DOM, timers and storage. Windows from one environment share the main backend.
|
|
93
|
+
No Node global `window`/`document` swapping is performed. This is test-state
|
|
94
|
+
isolation, **not a security sandbox**: evaluate only trusted application/test
|
|
95
|
+
code. Happy-dom's JavaScript-evaluation safety warning is not suppressed.
|
|
96
|
+
|
|
97
|
+
## What the framework assembles
|
|
98
|
+
|
|
99
|
+
The native host consumes the original Wood config, the selected views/layouts,
|
|
100
|
+
production controllers and companion data loaders. It uses the production
|
|
101
|
+
renderer generator and Vite's Svelte pipeline to compile the selected renderer.
|
|
102
|
+
Only compiled code is cached; dependency changes invalidate it. Each native
|
|
103
|
+
realm creates its own real renderer container and navigation state.
|
|
104
|
+
|
|
105
|
+
Optional global CSS belongs in application configuration, not a test harness:
|
|
106
|
+
|
|
107
|
+
```yaml
|
|
108
|
+
renderer:
|
|
109
|
+
componentDir: ui
|
|
110
|
+
openapi: ui/stitch.yaml
|
|
111
|
+
controllers: ui/controllers
|
|
112
|
+
css: [ui/styles.css]
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The bridge is built from the production operation catalogue. Renderer calls
|
|
116
|
+
enter the same selected main router, validation, middleware, IoC scope and
|
|
117
|
+
controller as `env.api`. Out-of-aperture main calls and renderer `open()` calls
|
|
118
|
+
fail; importing a complete typed API does not widen selection.
|
|
119
|
+
|
|
120
|
+
A compose hook may publish backend events through its standard `context.events`
|
|
121
|
+
source, or return its existing `events` source. The native host subscribes to
|
|
122
|
+
that source automatically. Initial subscription registration completes before
|
|
123
|
+
`open()` resolves; RPC waits for admitted subscriptions too. The framework does
|
|
124
|
+
not fabricate replayed events to hide late-subscription bugs.
|
|
125
|
+
|
|
126
|
+
Renderer trace deliveries use Wood's trace ingress. `env.settle()` drains host
|
|
127
|
+
bridge deliveries; a configured application trace persistence writer still owns
|
|
128
|
+
its durable commit barrier. There is no pretend durable batch acknowledgement.
|
|
129
|
+
|
|
130
|
+
## Lifecycle and scope of this first version
|
|
131
|
+
|
|
132
|
+
Building records application composition without opening a tab or Window.
|
|
133
|
+
First `open()` compiles the selected renderer and acquires the host. Native
|
|
134
|
+
Playwright reload remounts the renderer without duplicating subscriptions.
|
|
135
|
+
Closing an environment fences new opens, joins admitted opens, attempts all
|
|
136
|
+
cleanup steps, and reports cleanup failures without abandoning other resources.
|
|
137
|
+
Page mount errors include the route and browser diagnostics.
|
|
138
|
+
|
|
139
|
+
Main-process `class/function/method/value/middleware` seams retain the existing
|
|
140
|
+
NoEgo contract. **Renderer-side Node replacement objects/closures and `.load()`
|
|
141
|
+
overrides are not transferred into native realms in this first version.** Those
|
|
142
|
+
requests fail rather than silently applying to an unused Node renderer. Keep
|
|
143
|
+
such tests on the existing in-process renderer lane until a separately designed
|
|
144
|
+
renderer-replacement transport exists. Production renderer controllers and
|
|
145
|
+
loaders themselves do run in both new hosts.
|
|
146
|
+
|
|
147
|
+
Native browser hosts do not claim real Electron IPC, window controls, GPU or
|
|
148
|
+
OS integration. They are not an emulator for Electron-only methods.
|
|
149
|
+
|
|
150
|
+
Existing `browser.createHarness()`, application adapters, and old tests remain
|
|
151
|
+
available until an explicit later migration. Do not mass-convert the suite as
|
|
152
|
+
part of adopting this foundation.
|
|
153
|
+
|
|
154
|
+
## Validation
|
|
155
|
+
|
|
156
|
+
- `npm run typecheck`
|
|
157
|
+
- `npm run build`
|
|
158
|
+
- `npx tsc -p qualification/native_browser/tsconfig.json`
|
|
159
|
+
- `npx vitest run src/testing/native_browser/native_browser.test.ts`
|
|
160
|
+
- `npm test` (the Wood runner now owns its shared browser globally)
|
|
161
|
+
|
|
162
|
+
The native contract tests use real Chromium and real happy-dom, not mocked
|
|
163
|
+
browser/page implementations. The declaration fixture checks native return
|
|
164
|
+
types, chain preservation and rejection of the wrong host's APIs.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noego/wood",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -47,6 +47,12 @@
|
|
|
47
47
|
"import": "./dist/testing/index.js",
|
|
48
48
|
"default": "./dist/testing/index.js"
|
|
49
49
|
},
|
|
50
|
+
"./testing-api": {
|
|
51
|
+
"types": "./dist/testing-api.d.ts",
|
|
52
|
+
"require": "./dist/testing-api.cjs",
|
|
53
|
+
"import": "./dist/testing-api.js",
|
|
54
|
+
"default": "./dist/testing-api.js"
|
|
55
|
+
},
|
|
50
56
|
"./trace-testing": {
|
|
51
57
|
"types": "./dist/trace-testing/index.d.ts",
|
|
52
58
|
"require": "./dist/trace-testing/index.cjs",
|
|
@@ -117,7 +123,17 @@
|
|
|
117
123
|
"./navigation-shell": "./src/components/NavigationShell.svelte",
|
|
118
124
|
"./components": "./src/components/index.js",
|
|
119
125
|
"./loader": "./loaders/loader.mjs",
|
|
120
|
-
"./package.json": "./package.json"
|
|
126
|
+
"./package.json": "./package.json",
|
|
127
|
+
"./browser-test-setup": {
|
|
128
|
+
"types": "./dist/browser-test-setup.d.ts",
|
|
129
|
+
"import": "./dist/browser-test-setup.js",
|
|
130
|
+
"require": "./dist/browser-test-setup.cjs"
|
|
131
|
+
},
|
|
132
|
+
"./fetch": {
|
|
133
|
+
"types": "./dist/fetch.d.ts",
|
|
134
|
+
"import": "./dist/fetch.js",
|
|
135
|
+
"require": "./dist/fetch.cjs"
|
|
136
|
+
}
|
|
121
137
|
},
|
|
122
138
|
"scripts": {
|
|
123
139
|
"build": "tsup",
|
|
@@ -129,6 +145,7 @@
|
|
|
129
145
|
"dependencies": {
|
|
130
146
|
"@noego/ioc": "^0.8.0",
|
|
131
147
|
"@noego/logger": "^0.2.0",
|
|
148
|
+
"@noego/runtime": "^0.2.0",
|
|
132
149
|
"@noego/stitch": "^1.1.0",
|
|
133
150
|
"@noego/telemetry": "^0.2.0",
|
|
134
151
|
"@noego/trace": "^0.2.0",
|
|
@@ -142,11 +159,13 @@
|
|
|
142
159
|
"rxjs": "^7.x"
|
|
143
160
|
},
|
|
144
161
|
"peerDependencies": {
|
|
145
|
-
"@noego/testing": ">=0.
|
|
162
|
+
"@noego/testing": ">=0.7.0",
|
|
146
163
|
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
|
147
164
|
"electron": ">=28.0.0",
|
|
165
|
+
"happy-dom": "^20.14.0",
|
|
148
166
|
"playwright": "^1.40.0",
|
|
149
|
-
"svelte": "^5.x"
|
|
167
|
+
"svelte": "^5.x",
|
|
168
|
+
"vite": "^5.0.0"
|
|
150
169
|
},
|
|
151
170
|
"peerDependenciesMeta": {
|
|
152
171
|
"@sveltejs/vite-plugin-svelte": {
|
|
@@ -157,10 +176,16 @@
|
|
|
157
176
|
},
|
|
158
177
|
"@noego/testing": {
|
|
159
178
|
"optional": true
|
|
179
|
+
},
|
|
180
|
+
"happy-dom": {
|
|
181
|
+
"optional": true
|
|
182
|
+
},
|
|
183
|
+
"vite": {
|
|
184
|
+
"optional": true
|
|
160
185
|
}
|
|
161
186
|
},
|
|
162
187
|
"devDependencies": {
|
|
163
|
-
"@noego/testing": "^0.
|
|
188
|
+
"@noego/testing": "^0.7.0",
|
|
164
189
|
"@sveltejs/vite-plugin-svelte": "^4.0.4",
|
|
165
190
|
"@types/better-sqlite3": "^9.6.0",
|
|
166
191
|
"@types/js-yaml": "^4.x",
|
|
@@ -169,6 +194,7 @@
|
|
|
169
194
|
"electron-builder": "^25.x",
|
|
170
195
|
"electron-vite": "^2.x",
|
|
171
196
|
"esbuild": "^0.25.x",
|
|
197
|
+
"happy-dom": "^20.14.0",
|
|
172
198
|
"playwright": "^1.40.0",
|
|
173
199
|
"tsup": "^8.5.0",
|
|
174
200
|
"typescript": "^5.8.3",
|
|
@@ -2,7 +2,7 @@ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { afterEach, describe, expect, test } from 'vitest';
|
|
5
|
-
import {
|
|
5
|
+
import { createBrowserFixture } from '../testing/browser';
|
|
6
6
|
|
|
7
7
|
type ReproSnapshot = {
|
|
8
8
|
pageCreateCount: number;
|
|
@@ -142,7 +142,7 @@ describe('NavigationShell failed transition recovery', () => {
|
|
|
142
142
|
|
|
143
143
|
const pageErrors: string[] = [];
|
|
144
144
|
const consoleErrors: string[] = [];
|
|
145
|
-
const harness = await
|
|
145
|
+
const harness = await createBrowserFixture({
|
|
146
146
|
rootDir: woodRoot,
|
|
147
147
|
browser: { headless: true },
|
|
148
148
|
page: {
|
|
@@ -158,7 +158,7 @@ describe('NavigationShell failed transition recovery', () => {
|
|
|
158
158
|
const mounted = await harness.mountComponent(
|
|
159
159
|
path.join(rootDir, 'ui', 'NavigationShellRepro.svelte'),
|
|
160
160
|
);
|
|
161
|
-
await mounted.locator('[data-repro-view]').waitFor({ state: 'visible', timeout: 5_000 }).catch(
|
|
161
|
+
await mounted.page.locator('[data-repro-view]').waitFor({ state: 'visible', timeout: 5_000 }).catch(
|
|
162
162
|
async (error: unknown) => {
|
|
163
163
|
throw new Error(
|
|
164
164
|
`${error instanceof Error ? error.message : String(error)}\n` +
|
|
@@ -170,16 +170,16 @@ describe('NavigationShell failed transition recovery', () => {
|
|
|
170
170
|
);
|
|
171
171
|
|
|
172
172
|
await expect.poll(async () => {
|
|
173
|
-
return (await mounted.locator('[data-repro-view]').textContent())?.replace(/\s+/g, ' ').trim();
|
|
173
|
+
return (await mounted.page.locator('[data-repro-view]').textContent())?.replace(/\s+/g, ' ').trim();
|
|
174
174
|
}).toBe('thread=1;controller=initial-page');
|
|
175
175
|
|
|
176
|
-
await mounted.evaluate(() => {
|
|
176
|
+
await mounted.page.evaluate(() => {
|
|
177
177
|
(window as unknown as {
|
|
178
178
|
__WOOD_NAVIGATION_REPRO_GO__: (threadId: string) => void;
|
|
179
179
|
}).__WOOD_NAVIGATION_REPRO_GO__('2');
|
|
180
180
|
});
|
|
181
181
|
await expect.poll(async () => {
|
|
182
|
-
return await mounted.evaluate(() => {
|
|
182
|
+
return await mounted.page.evaluate(() => {
|
|
183
183
|
return (window as unknown as {
|
|
184
184
|
__WOOD_NAVIGATION_REPRO__: { layoutCreateCount: number };
|
|
185
185
|
}).__WOOD_NAVIGATION_REPRO__.layoutCreateCount;
|
|
@@ -187,23 +187,23 @@ describe('NavigationShell failed transition recovery', () => {
|
|
|
187
187
|
}).toBe(1);
|
|
188
188
|
await expect.poll(() => pageErrors.length + consoleErrors.length).toBeGreaterThan(0);
|
|
189
189
|
|
|
190
|
-
await mounted.evaluate(() => {
|
|
190
|
+
await mounted.page.evaluate(() => {
|
|
191
191
|
(window as unknown as {
|
|
192
192
|
__WOOD_NAVIGATION_REPRO_GO__: (threadId: string) => void;
|
|
193
193
|
}).__WOOD_NAVIGATION_REPRO_GO__('2');
|
|
194
194
|
});
|
|
195
195
|
await expect.poll(async () => {
|
|
196
|
-
return await mounted.evaluate(() => {
|
|
196
|
+
return await mounted.page.evaluate(() => {
|
|
197
197
|
return (window as unknown as {
|
|
198
198
|
__WOOD_NAVIGATION_REPRO__: { pageCreateCount: number };
|
|
199
199
|
}).__WOOD_NAVIGATION_REPRO__.pageCreateCount;
|
|
200
200
|
});
|
|
201
201
|
}).toBe(2);
|
|
202
202
|
await expect.poll(async () => {
|
|
203
|
-
return (await mounted.locator('[data-repro-view]').textContent())?.replace(/\s+/g, ' ').trim();
|
|
203
|
+
return (await mounted.page.locator('[data-repro-view]').textContent())?.replace(/\s+/g, ' ').trim();
|
|
204
204
|
}).toBe('thread=2;controller=page-2');
|
|
205
205
|
|
|
206
|
-
const snapshot = await mounted.evaluate<ReproSnapshot>(() => {
|
|
206
|
+
const snapshot = await mounted.page.evaluate<ReproSnapshot>(() => {
|
|
207
207
|
const state = (window as unknown as {
|
|
208
208
|
__WOOD_NAVIGATION_REPRO__: Omit<ReproSnapshot, 'rendered' | 'pageErrors'>;
|
|
209
209
|
}).__WOOD_NAVIGATION_REPRO__;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { expect, test } from 'vitest';
|
|
5
|
+
import { resourceCase } from '@noego/testing';
|
|
6
|
+
import { createBrowserFixture } from '../testing/browser';
|
|
7
|
+
|
|
8
|
+
test('retains every renderer cleanup failure and disposes remaining owners once', resourceCase(async (resources) => {
|
|
9
|
+
const root = mkdtempSync(path.join(os.tmpdir(), 'wood-shell-cleanup-'));
|
|
10
|
+
resources.own({ dispose: () => rmSync(root, { recursive: true, force: true }) });
|
|
11
|
+
const woodRoot = path.resolve(__dirname, '../..');
|
|
12
|
+
writeFileSync(path.join(root, 'View.svelte'), '<main>cleanup probe</main>');
|
|
13
|
+
writeFileSync(path.join(root, 'Probe.svelte'), `<script lang="ts">
|
|
14
|
+
import '/@fs/${woodRoot}/src/client/global_polyfill.ts';
|
|
15
|
+
import NavigationShell from '/@fs/${woodRoot}/src/components/NavigationShell.svelte';
|
|
16
|
+
import View from './View.svelte';
|
|
17
|
+
const ledger: string[] = [];
|
|
18
|
+
const controllerError = new Error('controller failed');
|
|
19
|
+
const layoutError = new Error('layout scope failed');
|
|
20
|
+
const pageError = new Error('page scope failed');
|
|
21
|
+
const observerError = new Error('observer failed');
|
|
22
|
+
const entry = { key: 'main.probe', windowName: 'main', pageName: 'probe',
|
|
23
|
+
layoutPaths: [], layoutControllers: [], view: View, layouts: [] };
|
|
24
|
+
const initialControllerSet = {
|
|
25
|
+
controller: { dispose() { ledger.push('controller'); throw controllerError; } },
|
|
26
|
+
layoutControllers: [],
|
|
27
|
+
layoutScopes: [{ async dispose() { ledger.push('layout'); throw layoutError; } }],
|
|
28
|
+
pageScope: { async dispose() { ledger.push('page'); throw pageError; } },
|
|
29
|
+
};
|
|
30
|
+
let shell: any;
|
|
31
|
+
(window as any).__CHECK_CLEANUP__ = async () => {
|
|
32
|
+
const first = shell.disposeNavigation();
|
|
33
|
+
const same = first === shell.disposeNavigation();
|
|
34
|
+
try { await first; throw new Error('missing failure'); }
|
|
35
|
+
catch (error: any) {
|
|
36
|
+
const flatten = (value: any): any[] => value?.errors ? value.errors.flatMap(flatten) : [value];
|
|
37
|
+
return { same, ledger, errors: flatten(error).map((value: any) => value.message),
|
|
38
|
+
original: flatten(error).includes(controllerError) && flatten(error).includes(layoutError)
|
|
39
|
+
&& flatten(error).includes(pageError) && flatten(error).includes(observerError) };
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
</script>
|
|
43
|
+
<NavigationShell bind:this={shell} manifest={{ [entry.key]: entry }} initialEntry={entry}
|
|
44
|
+
{initialControllerSet} getViewManifestEntryFn={() => entry}
|
|
45
|
+
onLifecycleError={() => { throw observerError; }} />`);
|
|
46
|
+
const harness = await createBrowserFixture({ rootDir: woodRoot, browser: { headless: true } });
|
|
47
|
+
resources.own({ dispose: () => harness.destroy() });
|
|
48
|
+
const mounted = await harness.mountComponent(path.join(root, 'Probe.svelte'));
|
|
49
|
+
await mounted.page.locator('main').waitFor({ state: 'visible' });
|
|
50
|
+
const result = await mounted.page.evaluate(() => (window as any).__CHECK_CLEANUP__());
|
|
51
|
+
expect(result).toEqual({
|
|
52
|
+
same: true,
|
|
53
|
+
ledger: ['controller', 'layout', 'page'],
|
|
54
|
+
errors: ['controller failed', 'observer failed', 'layout scope failed', 'observer failed', 'page scope failed', 'observer failed'],
|
|
55
|
+
original: true,
|
|
56
|
+
});
|
|
57
|
+
}), 60_000);
|
|
@@ -2,7 +2,7 @@ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { afterEach, describe, expect, test } from 'vitest';
|
|
5
|
-
import {
|
|
5
|
+
import { createBrowserFixture } from '../testing/browser';
|
|
6
6
|
|
|
7
7
|
type ScopeReproSnapshot = {
|
|
8
8
|
scopeDisposeCounts: Record<string, number>;
|
|
@@ -184,7 +184,7 @@ describe('NavigationShell renderer scope ownership', () => {
|
|
|
184
184
|
|
|
185
185
|
const pageErrors: string[] = [];
|
|
186
186
|
const consoleErrors: string[] = [];
|
|
187
|
-
const harness = await
|
|
187
|
+
const harness = await createBrowserFixture({
|
|
188
188
|
rootDir: woodRoot,
|
|
189
189
|
browser: { headless: true },
|
|
190
190
|
page: {
|
|
@@ -200,7 +200,7 @@ describe('NavigationShell renderer scope ownership', () => {
|
|
|
200
200
|
const mounted = await harness.mountComponent(
|
|
201
201
|
path.join(rootDir, 'ui', 'NavigationShellScopesRepro.svelte'),
|
|
202
202
|
);
|
|
203
|
-
await mounted.locator('[data-repro-view]').waitFor({ state: 'visible', timeout: 5_000 }).catch(
|
|
203
|
+
await mounted.page.locator('[data-repro-view]').waitFor({ state: 'visible', timeout: 5_000 }).catch(
|
|
204
204
|
async (error: unknown) => {
|
|
205
205
|
throw new Error(
|
|
206
206
|
`${error instanceof Error ? error.message : String(error)}\n` +
|
|
@@ -223,10 +223,10 @@ describe('NavigationShell renderer scope ownership', () => {
|
|
|
223
223
|
};
|
|
224
224
|
|
|
225
225
|
const renderedController = async () =>
|
|
226
|
-
(await mounted.locator('[data-repro-view]').textContent())?.replace(/\s+/g, ' ').trim();
|
|
226
|
+
(await mounted.page.locator('[data-repro-view]').textContent())?.replace(/\s+/g, ' ').trim();
|
|
227
227
|
|
|
228
228
|
const snapshot = async (): Promise<ScopeReproSnapshot> => {
|
|
229
|
-
const result = await mounted.evaluate<Omit<ScopeReproSnapshot, 'rendered' | 'pageErrors'> & { rendered: string }>(() => {
|
|
229
|
+
const result = await mounted.page.evaluate<Omit<ScopeReproSnapshot, 'rendered' | 'pageErrors'> & { rendered: string }>(() => {
|
|
230
230
|
const state = (window as unknown as {
|
|
231
231
|
__WOOD_SCOPE_REPRO__: Omit<ScopeReproSnapshot, 'rendered' | 'pageErrors'>;
|
|
232
232
|
}).__WOOD_SCOPE_REPRO__;
|
|
@@ -36,7 +36,8 @@
|
|
|
36
36
|
pageScope?: ScopeLike | null;
|
|
37
37
|
layoutScopes?: Array<ScopeLike | null>;
|
|
38
38
|
};
|
|
39
|
-
type
|
|
39
|
+
type RouteInput = { params: Record<string, string>; query: Record<string, string> };
|
|
40
|
+
type CreateControllerSetFn = (entry: ViewManifestEntry, preservedDepth?: number, route?: RouteInput) => Promise<ControllerSet>;
|
|
40
41
|
type GetViewManifestEntryFn = (viewKey: string) => ViewManifestEntry | null;
|
|
41
42
|
type NavigationOptions = {
|
|
42
43
|
preserveLayouts?: boolean;
|
|
@@ -54,6 +55,7 @@
|
|
|
54
55
|
let {
|
|
55
56
|
manifest,
|
|
56
57
|
initialEntry,
|
|
58
|
+
initialRoute = { params: {}, query: {} },
|
|
57
59
|
initialController,
|
|
58
60
|
initialLayoutControllers,
|
|
59
61
|
createViewControllerFn,
|
|
@@ -65,6 +67,7 @@
|
|
|
65
67
|
}: {
|
|
66
68
|
manifest: Record<string, ViewManifestEntry>;
|
|
67
69
|
initialEntry: ViewManifestEntry;
|
|
70
|
+
initialRoute?: RouteInput;
|
|
68
71
|
initialController?: unknown;
|
|
69
72
|
initialLayoutControllers?: Array<unknown>;
|
|
70
73
|
createViewControllerFn?: CreateViewControllerFn;
|
|
@@ -110,9 +113,9 @@
|
|
|
110
113
|
let activeEntry = $state(getInitialEntry());
|
|
111
114
|
let activeController = $state(getInitialController());
|
|
112
115
|
let activeLayoutControllers: Array<unknown> = $state(getInitialLayoutControllers());
|
|
113
|
-
let activeParams: Record<string, unknown> = $state({});
|
|
114
|
-
let activeQuery: Record<string, unknown> = $state({});
|
|
115
|
-
let activePageKey = $state(createNavigationMountKey(getInitialEntry().key,
|
|
116
|
+
let activeParams: Record<string, unknown> = $state({ ...initialRoute.params });
|
|
117
|
+
let activeQuery: Record<string, unknown> = $state({ ...initialRoute.query });
|
|
118
|
+
let activePageKey = $state(createNavigationMountKey(getInitialEntry().key, initialRoute.params));
|
|
116
119
|
let activeLayoutKeys: string[] = $state(
|
|
117
120
|
getInitialEntry().layoutPaths.map((_, index) =>
|
|
118
121
|
createLayoutMountKey(getInitialEntry(), index, activePageKey),
|
|
@@ -242,10 +245,17 @@
|
|
|
242
245
|
await cleanupAll(layoutControllers.map(controller => () => disposeControllerLifecycle(controller)), 'Wood layout cleanup failed');
|
|
243
246
|
}
|
|
244
247
|
|
|
248
|
+
// Navigation may recover for the user, but the owning host must still observe
|
|
249
|
+
// every failure when it settles and disposes this renderer.
|
|
250
|
+
const lifecycleErrors: unknown[] = [];
|
|
245
251
|
function reportNavigationError(context: string, error: unknown): void {
|
|
252
|
+
lifecycleErrors.push(error);
|
|
246
253
|
const message = error instanceof Error ? error.message : String(error);
|
|
247
254
|
console.error(`[wood] ${context}: ${message}`);
|
|
248
|
-
try { onLifecycleError?.(error); } catch (observerError) {
|
|
255
|
+
try { onLifecycleError?.(error); } catch (observerError) {
|
|
256
|
+
lifecycleErrors.push(observerError);
|
|
257
|
+
console.error('[wood] lifecycle error observer failed', observerError);
|
|
258
|
+
}
|
|
249
259
|
}
|
|
250
260
|
|
|
251
261
|
async function disposeStagedControllers(
|
|
@@ -266,11 +276,13 @@
|
|
|
266
276
|
async function createTransitionControllers(
|
|
267
277
|
entry: ViewManifestEntry,
|
|
268
278
|
preservedDepth: number,
|
|
279
|
+
route: RouteInput,
|
|
269
280
|
): Promise<ControllerSet> {
|
|
270
281
|
if (createControllerSetFn) {
|
|
271
282
|
// The scope-aware factory owns its own failure cleanup (it disposes
|
|
272
|
-
// the scopes it created before rethrowing).
|
|
273
|
-
|
|
283
|
+
// the scopes it created before rethrowing). The route feeds the
|
|
284
|
+
// page/layout data loaders and controller initialize().
|
|
285
|
+
return createControllerSetFn(entry, preservedDepth, route);
|
|
274
286
|
}
|
|
275
287
|
|
|
276
288
|
const [controllerResult, layoutControllersResult] = await Promise.allSettled([
|
|
@@ -337,7 +349,10 @@
|
|
|
337
349
|
const previousLayoutControllers = [...activeLayoutControllers];
|
|
338
350
|
const previousPageScope = activePageScope;
|
|
339
351
|
const previousLayoutScopes = [...activeLayoutScopes];
|
|
340
|
-
const nextSet = await createTransitionControllers(entry, preservedDepth
|
|
352
|
+
const nextSet = await createTransitionControllers(entry, preservedDepth, {
|
|
353
|
+
params: { ...navEntry.params },
|
|
354
|
+
query: { ...navEntry.query },
|
|
355
|
+
});
|
|
341
356
|
const { controller, layoutControllers } = nextSet;
|
|
342
357
|
|
|
343
358
|
if (destroyed) {
|
|
@@ -477,23 +492,34 @@
|
|
|
477
492
|
navigation.replace(route, {}, {});
|
|
478
493
|
});
|
|
479
494
|
|
|
480
|
-
|
|
495
|
+
let navigationDisposal: Promise<void> | undefined;
|
|
496
|
+
/** Awaitable lifecycle hook for the framework host, including staged navigation. */
|
|
497
|
+
export function disposeNavigation(): Promise<void> {
|
|
498
|
+
if (navigationDisposal) return navigationDisposal;
|
|
481
499
|
destroyed = true;
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
500
|
+
navigationDisposal = Promise.resolve().then(async () => {
|
|
501
|
+
try { unsubscribe(); } catch (error) { reportNavigationError('navigation unsubscribe failed', error); }
|
|
502
|
+
try { unsubscribeWindowNavigate?.(); } catch (error) { reportNavigationError('window navigation unsubscribe failed', error); }
|
|
503
|
+
await navigationTransitionQueue;
|
|
504
|
+
await disposeActiveControllers();
|
|
505
|
+
for (const scope of activeLayoutScopes) await disposeScope(scope, 'teardown layout scope disposal failed');
|
|
506
|
+
await disposeScope(activePageScope, 'teardown page scope disposal failed');
|
|
507
|
+
for (const retained of retainedPageScopes) {
|
|
508
|
+
await disposeScope(retained.scope, 'teardown retained scope disposal failed');
|
|
509
|
+
}
|
|
510
|
+
retainedPageScopes = [];
|
|
511
|
+
if (lifecycleErrors.length) {
|
|
512
|
+
throw Object.assign(new Error('Wood navigation lifecycle failed'), {
|
|
513
|
+
errors: [...lifecycleErrors], cause: lifecycleErrors[0],
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
return navigationDisposal;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
onDestroy(() => {
|
|
521
|
+
// Svelte does not await onDestroy; the host awaits the same memoized promise.
|
|
522
|
+
void disposeNavigation().catch((error: unknown) => console.error('[wood] navigation teardown failed', error));
|
|
497
523
|
});
|
|
498
524
|
</script>
|
|
499
525
|
|