@noego/wood 0.1.3 → 0.2.1
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 +35 -0
- package/dist/client/index.cjs +1 -1
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/testing/browser.cjs +789 -0
- package/dist/testing/browser.cjs.map +1 -0
- package/dist/testing/browser.d.cts +183 -0
- package/dist/testing/browser.d.ts +183 -0
- package/dist/testing/browser.js +754 -0
- package/dist/testing/browser.js.map +1 -0
- package/dist/testing/create_controller_harness.cjs +2 -2
- package/dist/testing/create_controller_harness.cjs.map +1 -1
- package/dist/testing/create_test_router.cjs +1 -1
- package/dist/testing/create_test_router.cjs.map +1 -1
- package/dist/testing/index.cjs +10 -5
- package/dist/testing/index.cjs.map +1 -1
- package/dist/testing/index.d.cts +1 -0
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.js +8 -2
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/mock_electron.cjs +59 -39
- package/dist/testing/mock_electron.cjs.map +1 -1
- package/dist/testing/mock_electron.d.cts +2 -2
- package/dist/testing/mock_electron.d.ts +2 -2
- package/dist/testing/mock_electron.js +59 -39
- package/dist/testing/mock_electron.js.map +1 -1
- package/docs/browser-testing.md +212 -0
- package/package.json +7 -1
|
@@ -1,66 +1,86 @@
|
|
|
1
|
-
|
|
1
|
+
function createMockFunction(implementation) {
|
|
2
|
+
let currentImplementation = implementation ?? (() => void 0);
|
|
3
|
+
const mock = ((...args) => {
|
|
4
|
+
mock.calls.push(args);
|
|
5
|
+
return currentImplementation(...args);
|
|
6
|
+
});
|
|
7
|
+
mock.calls = [];
|
|
8
|
+
mock.mockImplementation = (nextImplementation) => {
|
|
9
|
+
currentImplementation = nextImplementation;
|
|
10
|
+
return mock;
|
|
11
|
+
};
|
|
12
|
+
mock.mockResolvedValue = (value) => {
|
|
13
|
+
currentImplementation = () => Promise.resolve(value);
|
|
14
|
+
return mock;
|
|
15
|
+
};
|
|
16
|
+
mock.mockReturnValue = (value) => {
|
|
17
|
+
currentImplementation = () => value;
|
|
18
|
+
return mock;
|
|
19
|
+
};
|
|
20
|
+
return mock;
|
|
21
|
+
}
|
|
2
22
|
function mockElectron() {
|
|
3
23
|
const mocks = {
|
|
4
24
|
ipcMain: {
|
|
5
|
-
handle:
|
|
6
|
-
removeHandler:
|
|
7
|
-
on:
|
|
25
|
+
handle: createMockFunction(),
|
|
26
|
+
removeHandler: createMockFunction(),
|
|
27
|
+
on: createMockFunction()
|
|
8
28
|
},
|
|
9
29
|
ipcRenderer: {
|
|
10
|
-
invoke:
|
|
11
|
-
send:
|
|
12
|
-
on:
|
|
13
|
-
removeListener:
|
|
30
|
+
invoke: createMockFunction().mockResolvedValue(void 0),
|
|
31
|
+
send: createMockFunction(),
|
|
32
|
+
on: createMockFunction(),
|
|
33
|
+
removeListener: createMockFunction()
|
|
14
34
|
},
|
|
15
|
-
BrowserWindow:
|
|
16
|
-
loadURL:
|
|
17
|
-
loadFile:
|
|
35
|
+
BrowserWindow: createMockFunction().mockImplementation(() => ({
|
|
36
|
+
loadURL: createMockFunction().mockResolvedValue(void 0),
|
|
37
|
+
loadFile: createMockFunction().mockResolvedValue(void 0),
|
|
18
38
|
webContents: {
|
|
19
|
-
send:
|
|
20
|
-
reload:
|
|
21
|
-
on:
|
|
39
|
+
send: createMockFunction(),
|
|
40
|
+
reload: createMockFunction(),
|
|
41
|
+
on: createMockFunction()
|
|
22
42
|
},
|
|
23
|
-
on:
|
|
24
|
-
once:
|
|
25
|
-
close:
|
|
26
|
-
focus:
|
|
27
|
-
show:
|
|
28
|
-
destroy:
|
|
29
|
-
isDestroyed:
|
|
43
|
+
on: createMockFunction(),
|
|
44
|
+
once: createMockFunction(),
|
|
45
|
+
close: createMockFunction(),
|
|
46
|
+
focus: createMockFunction(),
|
|
47
|
+
show: createMockFunction(),
|
|
48
|
+
destroy: createMockFunction(),
|
|
49
|
+
isDestroyed: createMockFunction().mockReturnValue(false)
|
|
30
50
|
})),
|
|
31
51
|
app: {
|
|
32
|
-
getPath:
|
|
33
|
-
on:
|
|
34
|
-
once:
|
|
35
|
-
whenReady:
|
|
52
|
+
getPath: createMockFunction((name) => `/mock/${name}`),
|
|
53
|
+
on: createMockFunction(),
|
|
54
|
+
once: createMockFunction(),
|
|
55
|
+
whenReady: createMockFunction().mockResolvedValue(void 0),
|
|
36
56
|
isPackaged: false,
|
|
37
|
-
quit:
|
|
57
|
+
quit: createMockFunction()
|
|
38
58
|
},
|
|
39
59
|
dialog: {
|
|
40
|
-
showOpenDialog:
|
|
41
|
-
showSaveDialog:
|
|
42
|
-
showMessageBox:
|
|
60
|
+
showOpenDialog: createMockFunction().mockResolvedValue({ filePaths: [], canceled: false }),
|
|
61
|
+
showSaveDialog: createMockFunction().mockResolvedValue({ filePath: "", canceled: false }),
|
|
62
|
+
showMessageBox: createMockFunction().mockResolvedValue({ response: 0 })
|
|
43
63
|
},
|
|
44
64
|
shell: {
|
|
45
|
-
openExternal:
|
|
46
|
-
openPath:
|
|
65
|
+
openExternal: createMockFunction().mockResolvedValue(void 0),
|
|
66
|
+
openPath: createMockFunction().mockResolvedValue("")
|
|
47
67
|
},
|
|
48
68
|
safeStorage: {
|
|
49
|
-
encryptString:
|
|
50
|
-
decryptString:
|
|
51
|
-
isEncryptionAvailable:
|
|
69
|
+
encryptString: createMockFunction().mockReturnValue(Buffer.from("")),
|
|
70
|
+
decryptString: createMockFunction().mockReturnValue(""),
|
|
71
|
+
isEncryptionAvailable: createMockFunction().mockReturnValue(true)
|
|
52
72
|
},
|
|
53
73
|
nativeTheme: {
|
|
54
74
|
shouldUseDarkColors: false,
|
|
55
|
-
on:
|
|
75
|
+
on: createMockFunction()
|
|
56
76
|
},
|
|
57
77
|
globalShortcut: {
|
|
58
|
-
register:
|
|
59
|
-
unregister:
|
|
60
|
-
unregisterAll:
|
|
78
|
+
register: createMockFunction().mockReturnValue(true),
|
|
79
|
+
unregister: createMockFunction(),
|
|
80
|
+
unregisterAll: createMockFunction()
|
|
61
81
|
},
|
|
62
82
|
contextBridge: {
|
|
63
|
-
exposeInMainWorld:
|
|
83
|
+
exposeInMainWorld: createMockFunction()
|
|
64
84
|
}
|
|
65
85
|
};
|
|
66
86
|
return mocks;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/testing/mock_electron.ts"],"sourcesContent":["
|
|
1
|
+
{"version":3,"sources":["../../src/testing/mock_electron.ts"],"sourcesContent":["type MockFunction = ((...args: any[]) => any) & {\n calls: any[][];\n mockImplementation: (implementation: (...args: any[]) => any) => MockFunction;\n mockResolvedValue: (value: any) => MockFunction;\n mockReturnValue: (value: any) => MockFunction;\n};\n\nfunction createMockFunction(implementation?: (...args: any[]) => any): MockFunction {\n let currentImplementation = implementation ?? (() => undefined);\n const mock = ((...args: any[]) => {\n mock.calls.push(args);\n return currentImplementation(...args);\n }) as MockFunction;\n mock.calls = [];\n mock.mockImplementation = (nextImplementation: (...args: any[]) => any) => {\n currentImplementation = nextImplementation;\n return mock;\n };\n mock.mockResolvedValue = (value: any) => {\n currentImplementation = () => Promise.resolve(value);\n return mock;\n };\n mock.mockReturnValue = (value: any) => {\n currentImplementation = () => value;\n return mock;\n };\n return mock;\n}\n\n/**\n * Comprehensive Electron mock for tests.\n * Call this in your test setup or beforeAll.\n */\nexport function mockElectron(): Record<string, any> {\n // This returns the mock object for use with a test runner's module mock factory.\n const mocks = {\n ipcMain: {\n handle: createMockFunction(),\n removeHandler: createMockFunction(),\n on: createMockFunction(),\n },\n ipcRenderer: {\n invoke: createMockFunction().mockResolvedValue(undefined),\n send: createMockFunction(),\n on: createMockFunction(),\n removeListener: createMockFunction(),\n },\n BrowserWindow: createMockFunction().mockImplementation(() => ({\n loadURL: createMockFunction().mockResolvedValue(undefined),\n loadFile: createMockFunction().mockResolvedValue(undefined),\n webContents: {\n send: createMockFunction(),\n reload: createMockFunction(),\n on: createMockFunction(),\n },\n on: createMockFunction(),\n once: createMockFunction(),\n close: createMockFunction(),\n focus: createMockFunction(),\n show: createMockFunction(),\n destroy: createMockFunction(),\n isDestroyed: createMockFunction().mockReturnValue(false),\n })),\n app: {\n getPath: createMockFunction((name: string) => `/mock/${name}`),\n on: createMockFunction(),\n once: createMockFunction(),\n whenReady: createMockFunction().mockResolvedValue(undefined),\n isPackaged: false,\n quit: createMockFunction(),\n },\n dialog: {\n showOpenDialog: createMockFunction().mockResolvedValue({ filePaths: [], canceled: false }),\n showSaveDialog: createMockFunction().mockResolvedValue({ filePath: '', canceled: false }),\n showMessageBox: createMockFunction().mockResolvedValue({ response: 0 }),\n },\n shell: {\n openExternal: createMockFunction().mockResolvedValue(undefined),\n openPath: createMockFunction().mockResolvedValue(''),\n },\n safeStorage: {\n encryptString: createMockFunction().mockReturnValue(Buffer.from('')),\n decryptString: createMockFunction().mockReturnValue(''),\n isEncryptionAvailable: createMockFunction().mockReturnValue(true),\n },\n nativeTheme: {\n shouldUseDarkColors: false,\n on: createMockFunction(),\n },\n globalShortcut: {\n register: createMockFunction().mockReturnValue(true),\n unregister: createMockFunction(),\n unregisterAll: createMockFunction(),\n },\n contextBridge: {\n exposeInMainWorld: createMockFunction(),\n },\n };\n\n return mocks;\n}\n\n/**\n * Get a module mock factory for Electron.\n * Usage: vi.mock('electron', () => getElectronMockFactory())\n */\nexport function getElectronMockFactory(): Record<string, any> {\n return mockElectron();\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# Browser Testing
|
|
2
|
+
|
|
3
|
+
Wood includes a browser harness for tests that need real Svelte rendering,
|
|
4
|
+
real CSS layout, browser APIs, or animation timing without booting a full
|
|
5
|
+
Electron app.
|
|
6
|
+
|
|
7
|
+
Use it when the behavior is visual or renderer-bound:
|
|
8
|
+
|
|
9
|
+
- a Svelte component must mount in Chromium
|
|
10
|
+
- layered layouts need real positioning
|
|
11
|
+
- a page needs Wood route, controller, and navigation wiring
|
|
12
|
+
- z-index, clipping, viewport placement, or animation stability matters
|
|
13
|
+
- the test needs a controlled Wood bridge instead of real Electron IPC
|
|
14
|
+
|
|
15
|
+
Do not use it for pure state or service behavior. Those tests should stay in
|
|
16
|
+
Node and exercise the nearest owned boundary through the IoC container.
|
|
17
|
+
|
|
18
|
+
## Import
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { browser } from '@noego/wood/testing';
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The public testing entry point intentionally exports `browser` from
|
|
25
|
+
`@noego/wood/testing`. There is no extra `/browser` segment in normal test
|
|
26
|
+
imports.
|
|
27
|
+
|
|
28
|
+
## Create a Harness
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
const harness = await browser.createHarness({
|
|
32
|
+
rootDir: process.cwd(),
|
|
33
|
+
componentDir: 'ui',
|
|
34
|
+
css: ['ui/app.css'],
|
|
35
|
+
bridge: {
|
|
36
|
+
operations: {
|
|
37
|
+
'settings.get': () => ({ readSpeed: 0 }),
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
// mount components, trees, or routes here
|
|
44
|
+
} finally {
|
|
45
|
+
await harness.destroy();
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`createHarness` starts a Vite dev server with the Svelte plugin, launches a
|
|
50
|
+
Playwright browser, installs a Wood bridge fixture, and writes temporary mount
|
|
51
|
+
entries under the configured `rootDir`.
|
|
52
|
+
|
|
53
|
+
## Mount a Component
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
const mounted = await harness.mountComponent('ui/components/StatusPanel.svelte', {
|
|
57
|
+
props: {
|
|
58
|
+
title: 'Ready',
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
await mounted.expect.visible('[data-testid="status-panel"]');
|
|
63
|
+
await mounted.expect.insideViewport('[data-testid="status-panel"]');
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`mountComponent` takes a component file path, not an imported component object.
|
|
67
|
+
The browser runtime imports and compiles the Svelte module through Vite. A Node
|
|
68
|
+
test cannot pass an imported Svelte component class across the Playwright
|
|
69
|
+
process boundary.
|
|
70
|
+
|
|
71
|
+
## Mount a Route
|
|
72
|
+
|
|
73
|
+
Use `viewsConfig` when the test needs Wood route metadata, controller creation,
|
|
74
|
+
layouts, route params, or query values to exist before the page controller is
|
|
75
|
+
constructed.
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
const harness = await browser.createHarness({
|
|
79
|
+
rootDir: process.cwd(),
|
|
80
|
+
componentDir: 'ui',
|
|
81
|
+
viewsConfig: 'ui/openapi/views.yaml',
|
|
82
|
+
css: ['ui/app.css'],
|
|
83
|
+
bridge: {
|
|
84
|
+
operations: {
|
|
85
|
+
'settings.get': () => ({ readSpeed: 0 }),
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const page = await harness.mountRoute('main.settings', {
|
|
91
|
+
route: {
|
|
92
|
+
params: {},
|
|
93
|
+
query: {
|
|
94
|
+
tab: 'appearance',
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
await page.expect.visible('[data-testid="settings-page"]');
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`mountRoute` can also boot a generated Wood app by passing `generatedApp`, but
|
|
103
|
+
`viewsConfig` is preferred for route-level tests because it gives the harness
|
|
104
|
+
the route manifest directly. Generated app mounting is useful for smoke tests
|
|
105
|
+
against generated renderer output.
|
|
106
|
+
|
|
107
|
+
## Mount a Layered Tree
|
|
108
|
+
|
|
109
|
+
Use `mountTree` when the test needs layout layering but not a full views YAML
|
|
110
|
+
route.
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
const mounted = await harness.mountTree({
|
|
114
|
+
layouts: [
|
|
115
|
+
{
|
|
116
|
+
component: 'ui/layouts/AppShell.svelte',
|
|
117
|
+
props: {
|
|
118
|
+
data: { sidebarOpen: true },
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
view: {
|
|
123
|
+
component: 'ui/pages/Inbox.svelte',
|
|
124
|
+
props: {
|
|
125
|
+
data: { selectedThreadId: 'thread-1' },
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
await mounted.expect.notOverlapping('[data-testid="sidebar"]', '[data-testid="composer"]');
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`mountTree` is a framework harness for Wood's layout composition. App-specific
|
|
134
|
+
fixture builders can wrap it, but Wood should not know app concepts such as
|
|
135
|
+
chat threads, settings themes, or domain-specific ready states.
|
|
136
|
+
|
|
137
|
+
## Bridge Fixtures
|
|
138
|
+
|
|
139
|
+
The harness installs a Wood bridge fixture before the page loads.
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
const mounted = await harness.mountComponent('ui/SaveButton.svelte', {
|
|
143
|
+
bridge: {
|
|
144
|
+
operations: {
|
|
145
|
+
'document.save': async (input) => ({ ok: true, input }),
|
|
146
|
+
},
|
|
147
|
+
events: {
|
|
148
|
+
'document.changed': { id: 'doc-1' },
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
await mounted.click('[data-testid="save"]');
|
|
154
|
+
|
|
155
|
+
expect(mounted.bridge.calls('document.save')).toHaveLength(1);
|
|
156
|
+
|
|
157
|
+
await mounted.bridge.emit('document.changed', { id: 'doc-2' });
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Operations are keyed by the same channel shape Wood exposes on the renderer
|
|
161
|
+
bridge, for example `settings.get` or `messages.send`. Static values are
|
|
162
|
+
returned directly. Functions receive the operation input and a small context
|
|
163
|
+
with the channel and recorded calls.
|
|
164
|
+
|
|
165
|
+
## Assertions
|
|
166
|
+
|
|
167
|
+
Every mounted surface exposes Playwright's `page` and `locator`, plus
|
|
168
|
+
Wood-specific assertions for visual bugs:
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
await mounted.expect.visible('.popover');
|
|
172
|
+
await mounted.expect.hidden('.menu');
|
|
173
|
+
await mounted.expect.insideViewport('.composer', { margin: 8 });
|
|
174
|
+
await mounted.expect.notClipped('.popover');
|
|
175
|
+
await mounted.expect.notOverlapping('.toast', '.composer');
|
|
176
|
+
await mounted.expect.zIndexAbove('.modal', '.backdrop');
|
|
177
|
+
await mounted.expect.stableAfterAnimation('.drawer');
|
|
178
|
+
await mounted.expect.screenshot('settings-drawer');
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Screenshots are written to `test-results/wood-browser`.
|
|
182
|
+
|
|
183
|
+
## Lifecycle
|
|
184
|
+
|
|
185
|
+
Destroy the harness after each test:
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
const harnesses: Array<{ destroy(): Promise<void> }> = [];
|
|
189
|
+
|
|
190
|
+
afterEach(async () => {
|
|
191
|
+
while (harnesses.length > 0) {
|
|
192
|
+
await harnesses.pop()?.destroy();
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Mounted surfaces also expose `destroy()`, but the harness owns the browser and
|
|
198
|
+
Vite server. In most tests, destroying the harness is the important cleanup.
|
|
199
|
+
|
|
200
|
+
## Boundary
|
|
201
|
+
|
|
202
|
+
The browser harness is not a full Electron app:
|
|
203
|
+
|
|
204
|
+
- Electron IPC is replaced by the Wood bridge fixture.
|
|
205
|
+
- Main-process services do not boot unless the app explicitly reaches them
|
|
206
|
+
through a test-controlled operation.
|
|
207
|
+
- Browser-only bugs are observable because Chromium renders the page.
|
|
208
|
+
- Electron-only bugs still need an Electron + Playwright test.
|
|
209
|
+
|
|
210
|
+
This boundary is intentional. It gives component, layout, and page tests a real
|
|
211
|
+
browser while keeping setup small enough that developers can write the test
|
|
212
|
+
without constructing a whole application runtime.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noego/wood",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"dist",
|
|
19
19
|
"bin",
|
|
20
20
|
"loaders",
|
|
21
|
+
"docs",
|
|
21
22
|
"src/components/",
|
|
22
23
|
"src/client/controller_contract.ts",
|
|
23
24
|
"src/controller/controller_lifecycle.ts",
|
|
@@ -98,16 +99,21 @@
|
|
|
98
99
|
"rxjs": "^7.x"
|
|
99
100
|
},
|
|
100
101
|
"peerDependencies": {
|
|
102
|
+
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
|
101
103
|
"electron": ">=28.0.0",
|
|
102
104
|
"playwright": "^1.40.0",
|
|
103
105
|
"svelte": "^5.x"
|
|
104
106
|
},
|
|
105
107
|
"peerDependenciesMeta": {
|
|
108
|
+
"@sveltejs/vite-plugin-svelte": {
|
|
109
|
+
"optional": true
|
|
110
|
+
},
|
|
106
111
|
"playwright": {
|
|
107
112
|
"optional": true
|
|
108
113
|
}
|
|
109
114
|
},
|
|
110
115
|
"devDependencies": {
|
|
116
|
+
"@sveltejs/vite-plugin-svelte": "^4.0.4",
|
|
111
117
|
"@types/js-yaml": "^4.x",
|
|
112
118
|
"@types/node": "^20.10.4",
|
|
113
119
|
"electron-builder": "^25.x",
|