@bleedingdev/modern-js-boundary-debugger 0.0.0 → 3.9.0-ultramodern.6
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/LICENSE +21 -0
- package/dist/cjs/boundary-debugger/index.js +317 -0
- package/dist/esm/boundary-debugger/index.mjs +277 -0
- package/dist/esm-node/boundary-debugger/index.mjs +278 -0
- package/dist/types/boundary-debugger/index.d.ts +28 -0
- package/package.json +58 -7
- package/rslib.config.mts +4 -0
- package/rstest.config.mts +36 -0
- package/src/boundary-debugger/index.tsx +427 -0
- package/tests/boundary-debugger/browser.test.ts +252 -0
- package/tests/boundary-debugger/client.test.tsx +297 -0
- package/tests/boundary-debugger/index.test.tsx +63 -0
- package/tsconfig.json +10 -0
- package/README.md +0 -3
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { rstest } from '@rstest/core';
|
|
2
|
+
import {
|
|
3
|
+
cleanup,
|
|
4
|
+
fireEvent,
|
|
5
|
+
render,
|
|
6
|
+
screen,
|
|
7
|
+
waitFor,
|
|
8
|
+
} from '@testing-library/react';
|
|
9
|
+
import React, { act } from 'react';
|
|
10
|
+
import ultramodernBoundaryDebuggerPlugin from '../../src/boundary-debugger';
|
|
11
|
+
|
|
12
|
+
let resizeObservers: FakeResizeObserver[] = [];
|
|
13
|
+
|
|
14
|
+
class FakeResizeObserver {
|
|
15
|
+
readonly elements = new Set<Element>();
|
|
16
|
+
|
|
17
|
+
private readonly callback: ResizeObserverCallback;
|
|
18
|
+
|
|
19
|
+
constructor(callback: ResizeObserverCallback) {
|
|
20
|
+
this.callback = callback;
|
|
21
|
+
resizeObservers.push(this);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
disconnect() {
|
|
25
|
+
this.elements.clear();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
observe(element: Element) {
|
|
29
|
+
this.elements.add(element);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
unobserve(element: Element) {
|
|
33
|
+
this.elements.delete(element);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
trigger() {
|
|
37
|
+
this.callback([], this as unknown as ResizeObserver);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe('ultramodern boundary debugger browser overlay', () => {
|
|
42
|
+
const originalGetBoundingClientRect =
|
|
43
|
+
HTMLElement.prototype.getBoundingClientRect;
|
|
44
|
+
const originalResizeObserver = globalThis.ResizeObserver;
|
|
45
|
+
const rects = new Map<string, DOMRect>();
|
|
46
|
+
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
resizeObservers = [];
|
|
49
|
+
globalThis.ResizeObserver =
|
|
50
|
+
FakeResizeObserver as unknown as typeof ResizeObserver;
|
|
51
|
+
rects.clear();
|
|
52
|
+
rects.set('primary-control', new DOMRect(72, 96, 180, 44));
|
|
53
|
+
rects.set('decide-surface', new DOMRect(64, 180, 720, 260));
|
|
54
|
+
rects.set('checkout-control', new DOMRect(116, 284, 220, 48));
|
|
55
|
+
Object.defineProperty(document.documentElement, 'scrollHeight', {
|
|
56
|
+
configurable: true,
|
|
57
|
+
get: () => 1200,
|
|
58
|
+
});
|
|
59
|
+
Object.defineProperty(HTMLElement.prototype, 'getBoundingClientRect', {
|
|
60
|
+
configurable: true,
|
|
61
|
+
value() {
|
|
62
|
+
const testId = this.getAttribute('data-testid');
|
|
63
|
+
return (
|
|
64
|
+
(testId ? rects.get(testId) : undefined) ?? new DOMRect(0, 0, 0, 0)
|
|
65
|
+
);
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
afterEach(() => {
|
|
71
|
+
cleanup();
|
|
72
|
+
rstest.restoreAllMocks();
|
|
73
|
+
Object.defineProperty(HTMLElement.prototype, 'getBoundingClientRect', {
|
|
74
|
+
configurable: true,
|
|
75
|
+
value: originalGetBoundingClientRect,
|
|
76
|
+
});
|
|
77
|
+
if (originalResizeObserver === undefined) {
|
|
78
|
+
delete (
|
|
79
|
+
globalThis as typeof globalThis & {
|
|
80
|
+
ResizeObserver?: typeof ResizeObserver;
|
|
81
|
+
}
|
|
82
|
+
).ResizeObserver;
|
|
83
|
+
} else {
|
|
84
|
+
globalThis.ResizeObserver = originalResizeObserver;
|
|
85
|
+
}
|
|
86
|
+
document.body.innerHTML = '';
|
|
87
|
+
document.documentElement.lang = '';
|
|
88
|
+
window.history.replaceState(null, '', '/');
|
|
89
|
+
window.localStorage?.clear();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('keeps controls usable when localStorage reads and writes are blocked', async () => {
|
|
93
|
+
const getItem = rstest
|
|
94
|
+
.spyOn(window.localStorage, 'getItem')
|
|
95
|
+
.mockImplementation(() => {
|
|
96
|
+
throw new Error('Storage is blocked');
|
|
97
|
+
});
|
|
98
|
+
const setItem = rstest
|
|
99
|
+
.spyOn(window.localStorage, 'setItem')
|
|
100
|
+
.mockImplementation(() => {
|
|
101
|
+
throw new Error('Storage is blocked');
|
|
102
|
+
});
|
|
103
|
+
let WrappedApp: React.ComponentType | undefined;
|
|
104
|
+
ultramodernBoundaryDebuggerPlugin({
|
|
105
|
+
enabledByDefault: true,
|
|
106
|
+
metadata: { appId: 'shell', boundaries: [], schemaVersion: 1 },
|
|
107
|
+
}).setup?.({
|
|
108
|
+
wrapRoot(factory: (App: React.ComponentType) => React.ComponentType) {
|
|
109
|
+
WrappedApp = factory(() => <main>app</main>);
|
|
110
|
+
},
|
|
111
|
+
} as any);
|
|
112
|
+
const App = WrappedApp!;
|
|
113
|
+
render(<App />);
|
|
114
|
+
|
|
115
|
+
const toggle = (await screen.findByLabelText(
|
|
116
|
+
'show team boundaries',
|
|
117
|
+
)) as HTMLInputElement;
|
|
118
|
+
await waitFor(() => expect(toggle.checked).toBe(true));
|
|
119
|
+
expect(getItem).toHaveBeenCalled();
|
|
120
|
+
expect(setItem).toHaveBeenCalledWith(
|
|
121
|
+
'modernjs:boundary-debugger:enabled',
|
|
122
|
+
'true',
|
|
123
|
+
);
|
|
124
|
+
fireEvent.click(toggle);
|
|
125
|
+
await waitFor(() => expect(toggle.checked).toBe(false));
|
|
126
|
+
expect(setItem).toHaveBeenLastCalledWith(
|
|
127
|
+
'modernjs:boundary-debugger:enabled',
|
|
128
|
+
'false',
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test.each([
|
|
133
|
+
['visible', '0', true, true, false],
|
|
134
|
+
['hidden-when-off', '0', true, false, false],
|
|
135
|
+
['hidden-when-off', '1', false, true, true],
|
|
136
|
+
['hidden', '1', false, false, true],
|
|
137
|
+
] as const)('preserves %s control visibility with query override %s', async (controlMode, query, stored, visible, enabled) => {
|
|
138
|
+
window.localStorage.setItem(
|
|
139
|
+
'modernjs:boundary-debugger:enabled',
|
|
140
|
+
String(stored),
|
|
141
|
+
);
|
|
142
|
+
window.history.replaceState(null, '', `/?modern-boundaries=${query}`);
|
|
143
|
+
document.documentElement.lang = 'cs-CZ';
|
|
144
|
+
let WrappedApp: React.ComponentType | undefined;
|
|
145
|
+
ultramodernBoundaryDebuggerPlugin({
|
|
146
|
+
controlMode,
|
|
147
|
+
metadata: {
|
|
148
|
+
appId: 'shell',
|
|
149
|
+
boundaries: [{ appId: 'checkout', mfName: 'verticalCheckout' }],
|
|
150
|
+
schemaVersion: 1,
|
|
151
|
+
},
|
|
152
|
+
}).setup?.({
|
|
153
|
+
wrapRoot(factory: (App: React.ComponentType) => React.ComponentType) {
|
|
154
|
+
WrappedApp = factory(() => (
|
|
155
|
+
<main
|
|
156
|
+
data-modern-boundary-id="verticalCheckout"
|
|
157
|
+
data-testid="checkout-control"
|
|
158
|
+
/>
|
|
159
|
+
));
|
|
160
|
+
},
|
|
161
|
+
} as any);
|
|
162
|
+
const App = WrappedApp!;
|
|
163
|
+
render(<App />);
|
|
164
|
+
|
|
165
|
+
await waitFor(() => {
|
|
166
|
+
expect(
|
|
167
|
+
window.localStorage.getItem('modernjs:boundary-debugger:enabled'),
|
|
168
|
+
).toBe(String(enabled));
|
|
169
|
+
expect(screen.queryByLabelText('zobrazit hranice týmů') !== null).toBe(
|
|
170
|
+
visible,
|
|
171
|
+
);
|
|
172
|
+
expect(
|
|
173
|
+
document.querySelectorAll('[data-modern-boundary-overlay]'),
|
|
174
|
+
).toHaveLength(enabled ? 1 : 0);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('disconnects observers and removes the original resize and scroll listeners on unmount', async () => {
|
|
179
|
+
const addListener = rstest.spyOn(window, 'addEventListener');
|
|
180
|
+
const removeListener = rstest.spyOn(window, 'removeEventListener');
|
|
181
|
+
const disconnectMutation = rstest.spyOn(
|
|
182
|
+
MutationObserver.prototype,
|
|
183
|
+
'disconnect',
|
|
184
|
+
);
|
|
185
|
+
let WrappedApp: React.ComponentType | undefined;
|
|
186
|
+
ultramodernBoundaryDebuggerPlugin({
|
|
187
|
+
enabledByDefault: true,
|
|
188
|
+
metadata: { appId: 'shell', boundaries: [], schemaVersion: 1 },
|
|
189
|
+
}).setup?.({
|
|
190
|
+
wrapRoot(factory: (App: React.ComponentType) => React.ComponentType) {
|
|
191
|
+
WrappedApp = factory(() => (
|
|
192
|
+
<main
|
|
193
|
+
data-modern-boundary-id="verticalCheckout"
|
|
194
|
+
data-testid="checkout-control"
|
|
195
|
+
/>
|
|
196
|
+
));
|
|
197
|
+
},
|
|
198
|
+
} as any);
|
|
199
|
+
const App = WrappedApp!;
|
|
200
|
+
const view = render(<App />);
|
|
201
|
+
await waitFor(() => {
|
|
202
|
+
expect(
|
|
203
|
+
document.querySelectorAll('[data-modern-boundary-overlay]'),
|
|
204
|
+
).toHaveLength(1);
|
|
205
|
+
});
|
|
206
|
+
const resizeListener = addListener.mock.calls.find(
|
|
207
|
+
([event]) => event === 'resize',
|
|
208
|
+
)?.[1];
|
|
209
|
+
const scrollListener = addListener.mock.calls.find(
|
|
210
|
+
([event]) => event === 'scroll',
|
|
211
|
+
)?.[1];
|
|
212
|
+
expect(resizeListener).toBeDefined();
|
|
213
|
+
expect(scrollListener).toBeDefined();
|
|
214
|
+
expect(resizeObservers.some(observer => observer.elements.size > 0)).toBe(
|
|
215
|
+
true,
|
|
216
|
+
);
|
|
217
|
+
view.unmount();
|
|
218
|
+
|
|
219
|
+
expect(disconnectMutation).toHaveBeenCalled();
|
|
220
|
+
expect(
|
|
221
|
+
resizeObservers.every(observer => observer.elements.size === 0),
|
|
222
|
+
).toBe(true);
|
|
223
|
+
expect(removeListener).toHaveBeenCalledWith('resize', resizeListener);
|
|
224
|
+
expect(removeListener).toHaveBeenCalledWith('scroll', scrollListener, true);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test('updates overlays when a boundary added after mount resizes', async () => {
|
|
228
|
+
let WrappedApp: React.ComponentType | undefined;
|
|
229
|
+
const plugin = ultramodernBoundaryDebuggerPlugin({
|
|
230
|
+
enabledByDefault: true,
|
|
231
|
+
metadata: {
|
|
232
|
+
appId: 'shell',
|
|
233
|
+
boundaries: [
|
|
234
|
+
{
|
|
235
|
+
appId: 'Checkout',
|
|
236
|
+
mfName: 'verticalCheckout',
|
|
237
|
+
},
|
|
238
|
+
],
|
|
239
|
+
schemaVersion: 1,
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
plugin.setup?.({
|
|
244
|
+
wrapRoot(factory: (App: React.ComponentType) => React.ComponentType) {
|
|
245
|
+
WrappedApp = factory(() => (
|
|
246
|
+
<main>
|
|
247
|
+
<div data-testid="dynamic-root" />
|
|
248
|
+
</main>
|
|
249
|
+
));
|
|
250
|
+
},
|
|
251
|
+
} as any);
|
|
252
|
+
|
|
253
|
+
expect(WrappedApp).toBeDefined();
|
|
254
|
+
const App = WrappedApp!;
|
|
255
|
+
render(<App />);
|
|
256
|
+
|
|
257
|
+
const boundary = document.createElement('section');
|
|
258
|
+
boundary.dataset.modernBoundaryId = 'verticalCheckout';
|
|
259
|
+
boundary.dataset.modernMfExpose = './LateCheckout';
|
|
260
|
+
boundary.setAttribute('data-testid', 'late-checkout');
|
|
261
|
+
rects.set('late-checkout', new DOMRect(24, 48, 160, 40));
|
|
262
|
+
|
|
263
|
+
act(() => {
|
|
264
|
+
screen.getByTestId('dynamic-root').appendChild(boundary);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
await waitFor(() => {
|
|
268
|
+
expect(
|
|
269
|
+
document.querySelector(
|
|
270
|
+
'[data-modern-boundary-overlay-label="Checkout"]',
|
|
271
|
+
),
|
|
272
|
+
).not.toBeNull();
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
const overlay = document.querySelector(
|
|
276
|
+
'[data-modern-boundary-overlay-label="Checkout"]',
|
|
277
|
+
) as HTMLElement;
|
|
278
|
+
expect(overlay.style.width).toBe('160px');
|
|
279
|
+
|
|
280
|
+
rects.set('late-checkout', new DOMRect(24, 48, 220, 56));
|
|
281
|
+
const boundaryObserver = resizeObservers.find(observer =>
|
|
282
|
+
observer.elements.has(boundary),
|
|
283
|
+
);
|
|
284
|
+
expect(boundaryObserver).toBeDefined();
|
|
285
|
+
act(() => {
|
|
286
|
+
boundaryObserver?.trigger();
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
await waitFor(() => {
|
|
290
|
+
const resizedOverlay = document.querySelector(
|
|
291
|
+
'[data-modern-boundary-overlay-label="Checkout"]',
|
|
292
|
+
) as HTMLElement | null;
|
|
293
|
+
expect(resizedOverlay?.style.width).toBe('220px');
|
|
294
|
+
expect(resizedOverlay?.style.height).toBe('56px');
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { ultramodernBoundaryDebuggerPlugin as exportedPlugin } from '@modern-js/boundary-debugger';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import ReactDomServer from 'react-dom/server';
|
|
4
|
+
import ultramodernBoundaryDebuggerPlugin from '../../src/boundary-debugger';
|
|
5
|
+
|
|
6
|
+
describe('ultramodern boundary debugger', () => {
|
|
7
|
+
it('exports the same plugin through the fork-owned package subpath without browser globals', () => {
|
|
8
|
+
expect(typeof window).toBe('undefined');
|
|
9
|
+
expect(typeof document).toBe('undefined');
|
|
10
|
+
expect(exportedPlugin).toBe(ultramodernBoundaryDebuggerPlugin);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('registers through the native runtime and preserves the application during SSR', () => {
|
|
14
|
+
const { runtimeContext } = runtime.run({
|
|
15
|
+
config: {},
|
|
16
|
+
plugins: [
|
|
17
|
+
exportedPlugin({
|
|
18
|
+
enabledByDefault: true,
|
|
19
|
+
metadata: { appId: 'shell', boundaries: [], schemaVersion: 1 },
|
|
20
|
+
}),
|
|
21
|
+
],
|
|
22
|
+
});
|
|
23
|
+
const App = runtimeContext.hooks.wrapRoot.call(() => (
|
|
24
|
+
<main>native app</main>
|
|
25
|
+
));
|
|
26
|
+
expect(ReactDomServer.renderToString(<App />)).toBe(
|
|
27
|
+
'<main>native app</main>',
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('does not render debug controls during SSR', () => {
|
|
32
|
+
let WrappedApp: React.ComponentType | undefined;
|
|
33
|
+
const plugin = ultramodernBoundaryDebuggerPlugin({
|
|
34
|
+
controlMode: 'visible',
|
|
35
|
+
enabledByDefault: true,
|
|
36
|
+
legacySelector: '[data-mf-remote]',
|
|
37
|
+
metadata: {
|
|
38
|
+
appId: 'shell',
|
|
39
|
+
boundaries: [
|
|
40
|
+
{
|
|
41
|
+
appId: 'catalog',
|
|
42
|
+
mfName: 'catalog',
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
schemaVersion: 1,
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
plugin.setup?.({
|
|
50
|
+
wrapRoot: (
|
|
51
|
+
factory: (App: React.ComponentType) => React.ComponentType,
|
|
52
|
+
) => {
|
|
53
|
+
WrappedApp = factory(() => <main>app</main>);
|
|
54
|
+
},
|
|
55
|
+
} as any);
|
|
56
|
+
|
|
57
|
+
expect(WrappedApp).toBeDefined();
|
|
58
|
+
const App = WrappedApp!;
|
|
59
|
+
expect(ReactDomServer.renderToString(<App />)).toBe('<main>app</main>');
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
import { runtime } from '@modern-js/plugin/runtime';
|
package/tsconfig.json
ADDED
package/README.md
DELETED
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
# @bleedingdev/modern-js-boundary-debugger
|
|
2
|
-
|
|
3
|
-
This 0.0.0 version only registers the package name so npm trusted publishing can be configured. It contains no runtime implementation. Use the subsequent UltraModern.js release, published from the repository workflow with provenance.
|