Skip to content

Commit bcd8611

Browse files
neo-gpttobiu
andauthored
feat(test): mock main route and local storage in setup (#11578) (#11579)
* feat(test): mock main route and local storage in setup (#11578) * fix(test): use Neo namespace helper in setup mocks (#11578) --------- Co-authored-by: tobiu <tobiasuhlig78@gmail.com>
1 parent 39690b7 commit bcd8611

3 files changed

Lines changed: 202 additions & 1 deletion

File tree

learn/guides/testing/UnitTesting.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,19 @@ In a Unit Test:
7070
2. We import **both** the App Worker classes and the VDom Worker classes into the same Node.js scope.
7171
3. The `setup()` function mocks the messaging layer, allowing them to talk directly.
7272

73+
### Runtime Facade Mocks
74+
75+
`test/playwright/setup.mjs` also installs minimal runtime facades that production-shaped component trees expect during construction:
76+
77+
```javascript
78+
setup({
79+
mockMain : true, // provides Neo.Main.setRoute()
80+
mockLocalStorage: true // provides Neo.main.addon.LocalStorage
81+
});
82+
```
83+
84+
Both flags default to `true`. This lets downstream apps smoke-test a root `Neo.container.Viewport` with a real `Neo.controller.Component` controller, including `defaultHash` routing and persisted UI state reads, without wiring the browser main-thread runtime. Set either flag to `false` when a spec intentionally installs a richer route or storage mock before calling `setup()`.
85+
7386
### Understanding Imports (Critical)
7487

7588
Because we are bypassing the standard build/worker loading process, you must manually import the dependencies your test needs.

test/playwright/setup.mjs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,22 @@ globalThis.DOMRect = class DOMRect {
2828
// This file sets up the Node.js global scope to run Neo.mjs
2929
// VDOM unit tests without a browser or jsdom.
3030

31+
/**
32+
* Configures the single-threaded Playwright unit-test runtime with the minimal
33+
* Neo globals required by App Worker and VDom Worker classes.
34+
* @param {Object} options={}
35+
* @param {Object} options.appConfig={} App facade overrides.
36+
* @param {Boolean} options.mockLocalStorage=true True installs minimal `Neo.main.addon.LocalStorage` methods.
37+
* @param {Boolean} options.mockMain=true True installs a minimal `Neo.Main.setRoute()` method.
38+
* @param {Object} options.neoConfig={} Neo.config overrides.
39+
*/
3140
export function setup(options = {}) {
32-
const { neoConfig = {}, appConfig = {} } = options;
41+
const {
42+
appConfig = {},
43+
mockLocalStorage = true,
44+
mockMain = true,
45+
neoConfig = {}
46+
} = options;
3347

3448
const defaultNeoConfig = {
3549
environment : 'development',
@@ -55,6 +69,10 @@ export function setup(options = {}) {
5569
// Standardized Global Mocks to prevent cross-contamination in Playwright worker reuse
5670
Neo.applyDeltas ??= async () => {};
5771

72+
if (mockMain) {
73+
Neo.ns('Neo.Main', true).setRoute ??= () => {};
74+
}
75+
5876
Neo.main ??= {
5977
addon: {
6078
DragDrop: {},
@@ -82,6 +100,18 @@ export function setup(options = {}) {
82100
}
83101
};
84102

103+
if (mockLocalStorage) {
104+
const localStorage = Neo.ns('Neo.main.addon.LocalStorage', true);
105+
106+
localStorage.createLocalStorageItem ??= async () => {};
107+
localStorage.destroyLocalStorageItem ??= async () => {};
108+
localStorage.readLocalStorageItem ??= async ({key} = {}) => ({
109+
key,
110+
value: Array.isArray(key) ? Object.fromEntries(key.map(item => [item, null])) : null
111+
});
112+
localStorage.updateLocalStorageItem ??= async () => {};
113+
}
114+
85115
Neo.currentWorker ??= {
86116
getAddon: async () => ({
87117
register : () => {},
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import {setup} from '../../setup.mjs';
2+
3+
const appName = 'ContainerViewportSetupMocksTest';
4+
5+
setup({
6+
appConfig: {
7+
name: appName
8+
},
9+
mockLocalStorage: false,
10+
mockMain : false
11+
});
12+
13+
import {test, expect} from '@playwright/test';
14+
import Neo from '../../../../src/Neo.mjs';
15+
import * as core from '../../../../src/core/_export.mjs';
16+
import ComponentController from '../../../../src/controller/Component.mjs';
17+
import Viewport from '../../../../src/container/Viewport.mjs';
18+
19+
/**
20+
* @summary Test controller for exercising setup-level route and storage mocks.
21+
*
22+
* The controller combines default-hash routing with a construction-time LocalStorage
23+
* read to mirror production-shaped root viewport controllers in downstream apps.
24+
* @class Test.Unit.Container.ViewportSetupMocksController
25+
* @extends Neo.controller.Component
26+
*/
27+
class TestController extends ComponentController {
28+
static config = {
29+
className : 'Test.Unit.Container.ViewportSetupMocksController',
30+
defaultHash: '/home'
31+
}
32+
33+
onComponentConstructed() {
34+
this.storagePromise = Neo.main.addon.LocalStorage.readLocalStorageItem({
35+
key : ['viewportSetupTheme', 'viewportSetupLayout'],
36+
windowId: this.windowId
37+
});
38+
}
39+
}
40+
TestController = Neo.setupClass(TestController);
41+
42+
/**
43+
* @summary Test viewport for the setup runtime-facade smoke path.
44+
*
45+
* The viewport disables browser-only body mounting behavior so the unit test can
46+
* focus on controller construction without requiring a real main-thread DOM.
47+
* @class Test.Unit.Container.ViewportSetupMocksViewport
48+
* @extends Neo.container.Viewport
49+
*/
50+
class TestViewport extends Viewport {
51+
static config = {
52+
applyBodyCls: false,
53+
autoMount : false,
54+
className : 'Test.Unit.Container.ViewportSetupMocksViewport',
55+
controller : TestController
56+
}
57+
}
58+
TestViewport = Neo.setupClass(TestViewport);
59+
60+
test.describe('test/playwright/setup.mjs viewport mocks', () => {
61+
let previousMain, previousLocalStorage, viewport;
62+
63+
test.beforeEach(() => {
64+
previousMain = Neo.Main;
65+
previousLocalStorage = Neo.main?.addon?.LocalStorage;
66+
viewport = null;
67+
68+
delete Neo.Main;
69+
70+
if (Neo.main?.addon) {
71+
delete Neo.main.addon.LocalStorage;
72+
}
73+
});
74+
75+
test.afterEach(() => {
76+
viewport?.destroy();
77+
78+
if (previousMain === undefined) {
79+
delete Neo.Main;
80+
} else {
81+
Neo.Main = previousMain;
82+
}
83+
84+
if (previousLocalStorage === undefined) {
85+
delete Neo.main.addon.LocalStorage;
86+
} else {
87+
Neo.main.addon.LocalStorage = previousLocalStorage;
88+
}
89+
});
90+
91+
test('setup() provides default Neo.Main and LocalStorage mocks for viewport controller smoke tests', async () => {
92+
setup({
93+
appConfig: {
94+
name: appName
95+
}
96+
});
97+
98+
expect(Neo.Main.setRoute).toBeInstanceOf(Function);
99+
expect(Neo.main.addon.LocalStorage.readLocalStorageItem).toBeInstanceOf(Function);
100+
expect(Neo.main.addon.LocalStorage.updateLocalStorageItem).toBeInstanceOf(Function);
101+
expect(Neo.main.addon.LocalStorage.destroyLocalStorageItem).toBeInstanceOf(Function);
102+
103+
viewport = Neo.create(TestViewport, {appName});
104+
105+
await viewport.controller.ready();
106+
await viewport.controller.storagePromise;
107+
108+
expect(viewport.controller.isReady).toBe(true);
109+
});
110+
111+
test('setup() preserves pre-installed Neo.Main and LocalStorage mocks', async () => {
112+
const routeCalls = [];
113+
const storageReads = [];
114+
115+
Neo.Main = {
116+
setRoute: data => routeCalls.push(data)
117+
};
118+
119+
Neo.main.addon.LocalStorage = {
120+
destroyLocalStorageItem: async () => {},
121+
readLocalStorageItem: async data => {
122+
storageReads.push(data);
123+
return {key: data.key, value: Object.fromEntries(data.key.map(item => [item, null]))}
124+
},
125+
updateLocalStorageItem: async () => {}
126+
};
127+
128+
setup({
129+
appConfig: {
130+
name: appName
131+
}
132+
});
133+
134+
viewport = Neo.create(TestViewport, {appName});
135+
136+
await viewport.controller.ready();
137+
await viewport.controller.storagePromise;
138+
139+
expect(routeCalls).toEqual([{value: '/home', windowId: null}]);
140+
expect(storageReads).toEqual([{
141+
key : ['viewportSetupTheme', 'viewportSetupLayout'],
142+
windowId: null
143+
}]);
144+
});
145+
146+
test('setup() can opt out of Neo.Main and LocalStorage mocks', () => {
147+
setup({
148+
appConfig: {
149+
name: appName
150+
},
151+
mockLocalStorage: false,
152+
mockMain : false
153+
});
154+
155+
expect(Neo.Main).toBeUndefined();
156+
expect(Neo.main.addon.LocalStorage).toBeUndefined();
157+
});
158+
});

0 commit comments

Comments
 (0)