@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,427 @@
|
|
|
1
|
+
import type { RuntimePlugin } from '@modern-js/plugin';
|
|
2
|
+
import type { ComponentType } from 'react';
|
|
3
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
export type BoundaryDebugEntry = {
|
|
6
|
+
appId: string;
|
|
7
|
+
color?: string;
|
|
8
|
+
label?: string;
|
|
9
|
+
mfName: string;
|
|
10
|
+
ownerTeam?: string;
|
|
11
|
+
packageName?: string;
|
|
12
|
+
role?: 'host' | 'vertical';
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type BoundaryDebugMetadata = {
|
|
16
|
+
appId: string;
|
|
17
|
+
boundaries: BoundaryDebugEntry[];
|
|
18
|
+
schemaVersion: 1;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type BoundaryDebuggerControlMode =
|
|
22
|
+
| 'visible'
|
|
23
|
+
| 'hidden-when-off'
|
|
24
|
+
| 'hidden';
|
|
25
|
+
|
|
26
|
+
export type BoundaryDebuggerPluginOptions = {
|
|
27
|
+
controlMode?: BoundaryDebuggerControlMode;
|
|
28
|
+
enabledByDefault?: boolean;
|
|
29
|
+
labels?: Record<string, { toggle: string }>;
|
|
30
|
+
legacySelector?: string;
|
|
31
|
+
metadata: BoundaryDebugMetadata;
|
|
32
|
+
storageKey?: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type BoundaryBox = {
|
|
36
|
+
color: string;
|
|
37
|
+
detail?: string;
|
|
38
|
+
height: number;
|
|
39
|
+
id: string;
|
|
40
|
+
label: string;
|
|
41
|
+
left: number;
|
|
42
|
+
top: number;
|
|
43
|
+
width: number;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const defaultStorageKey = 'modernjs:boundary-debugger:enabled';
|
|
47
|
+
const queryParamName = 'modern-boundaries';
|
|
48
|
+
const boundarySelector = '[data-modern-boundary-id]';
|
|
49
|
+
const defaultLabels: Record<string, { toggle: string }> = {
|
|
50
|
+
cs: { toggle: 'zobrazit hranice týmů' },
|
|
51
|
+
en: { toggle: 'show team boundaries' },
|
|
52
|
+
};
|
|
53
|
+
const palette = ['#ff5a5f', '#30e27a', '#f6cf45', '#7c8cff', '#29b6f6'];
|
|
54
|
+
|
|
55
|
+
const readStoredEnabled = (storageKey: string, fallback: boolean) => {
|
|
56
|
+
if (typeof window === 'undefined') {
|
|
57
|
+
return fallback;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const stored = window.localStorage.getItem(storageKey);
|
|
62
|
+
return stored === null ? fallback : stored === 'true';
|
|
63
|
+
} catch {
|
|
64
|
+
return fallback;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const writeStoredEnabled = (storageKey: string, enabled: boolean) => {
|
|
69
|
+
if (typeof window === 'undefined') {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
window.localStorage.setItem(storageKey, String(enabled));
|
|
75
|
+
} catch {
|
|
76
|
+
// Storage may be blocked in private, embedded, or policy-restricted contexts.
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const parseEnabledOverride = (value: string | null) => {
|
|
81
|
+
if (value === null) {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const normalized = value.toLowerCase();
|
|
86
|
+
if (normalized === '1' || normalized === 'true') {
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
if (normalized === '0' || normalized === 'false') {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return undefined;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const readQueryEnabledOverride = () => {
|
|
97
|
+
if (typeof window === 'undefined') {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
return parseEnabledOverride(
|
|
103
|
+
new URLSearchParams(window.location.search).get(queryParamName),
|
|
104
|
+
);
|
|
105
|
+
} catch {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const detectLanguage = () => {
|
|
111
|
+
if (typeof document === 'undefined') {
|
|
112
|
+
return 'en';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const htmlLanguage = document.documentElement.lang;
|
|
116
|
+
if (htmlLanguage !== '') {
|
|
117
|
+
return htmlLanguage.split('-')[0] || 'en';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (typeof window === 'undefined') {
|
|
121
|
+
return 'en';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return window.location.pathname.split('/').filter(Boolean)[0] || 'en';
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const hashBoundaryId = (id: string) => {
|
|
128
|
+
let hash = 0;
|
|
129
|
+
for (let index = 0; index < id.length; index++) {
|
|
130
|
+
hash = (hash * 31 + id.charCodeAt(index)) >>> 0;
|
|
131
|
+
}
|
|
132
|
+
return hash;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const formatRectKey = (rect: DOMRect) =>
|
|
136
|
+
[
|
|
137
|
+
Math.round(rect.left * 100) / 100,
|
|
138
|
+
Math.round(rect.top * 100) / 100,
|
|
139
|
+
Math.round(rect.width * 100) / 100,
|
|
140
|
+
Math.round(rect.height * 100) / 100,
|
|
141
|
+
].join(':');
|
|
142
|
+
|
|
143
|
+
const getBoundaryId = (element: HTMLElement) =>
|
|
144
|
+
element.dataset.modernBoundaryId ??
|
|
145
|
+
element.dataset.mfRemote ??
|
|
146
|
+
element.getAttribute('data-mf-remote') ??
|
|
147
|
+
undefined;
|
|
148
|
+
|
|
149
|
+
const collectBoundaryElements = (legacySelector?: string) => {
|
|
150
|
+
const elements = new Set<HTMLElement>();
|
|
151
|
+
for (const element of document.querySelectorAll<HTMLElement>(
|
|
152
|
+
boundarySelector,
|
|
153
|
+
)) {
|
|
154
|
+
elements.add(element);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (legacySelector === undefined || legacySelector === '') {
|
|
158
|
+
return Array.from(elements);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
for (const element of document.querySelectorAll<HTMLElement>(
|
|
163
|
+
legacySelector,
|
|
164
|
+
)) {
|
|
165
|
+
elements.add(element);
|
|
166
|
+
}
|
|
167
|
+
} catch {
|
|
168
|
+
// Ignore invalid optional legacy selectors; the standard selector still works.
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return Array.from(elements);
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
function BoundaryDebugger({
|
|
175
|
+
controlMode = 'visible',
|
|
176
|
+
enabledByDefault = false,
|
|
177
|
+
labels = defaultLabels,
|
|
178
|
+
legacySelector,
|
|
179
|
+
metadata,
|
|
180
|
+
storageKey = defaultStorageKey,
|
|
181
|
+
}: BoundaryDebuggerPluginOptions) {
|
|
182
|
+
const [mounted, setMounted] = useState(false);
|
|
183
|
+
const [enabled, setEnabled] = useState(false);
|
|
184
|
+
const [boxes, setBoxes] = useState<BoundaryBox[]>([]);
|
|
185
|
+
const boundaries = useMemo(
|
|
186
|
+
() =>
|
|
187
|
+
new Map(
|
|
188
|
+
metadata.boundaries.map((entry, index) => [
|
|
189
|
+
entry.mfName,
|
|
190
|
+
{
|
|
191
|
+
...entry,
|
|
192
|
+
color: entry.color ?? palette[index % palette.length],
|
|
193
|
+
label: entry.label ?? entry.appId,
|
|
194
|
+
},
|
|
195
|
+
]),
|
|
196
|
+
),
|
|
197
|
+
[metadata],
|
|
198
|
+
);
|
|
199
|
+
const language = mounted ? detectLanguage() : 'en';
|
|
200
|
+
const toggleLabel =
|
|
201
|
+
labels[language]?.toggle ??
|
|
202
|
+
labels.en?.toggle ??
|
|
203
|
+
defaultLabels.en?.toggle ??
|
|
204
|
+
'show team boundaries';
|
|
205
|
+
|
|
206
|
+
useEffect(() => {
|
|
207
|
+
setMounted(true);
|
|
208
|
+
const queryOverride = readQueryEnabledOverride();
|
|
209
|
+
setEnabled(
|
|
210
|
+
queryOverride ?? readStoredEnabled(storageKey, enabledByDefault),
|
|
211
|
+
);
|
|
212
|
+
}, [enabledByDefault, storageKey]);
|
|
213
|
+
|
|
214
|
+
useEffect(() => {
|
|
215
|
+
if (!mounted) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
writeStoredEnabled(storageKey, enabled);
|
|
219
|
+
}, [enabled, mounted, storageKey]);
|
|
220
|
+
|
|
221
|
+
useEffect(() => {
|
|
222
|
+
if (!enabled) {
|
|
223
|
+
setBoxes([]);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
let resizeObserver: ResizeObserver | undefined;
|
|
228
|
+
const observedElements = new Set<HTMLElement>();
|
|
229
|
+
const observeBoundaryElements = (elements: HTMLElement[]) => {
|
|
230
|
+
if (resizeObserver === undefined) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
for (const element of elements) {
|
|
234
|
+
if (observedElements.has(element)) {
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
observedElements.add(element);
|
|
238
|
+
resizeObserver.observe(element);
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const readBoxes = () => {
|
|
243
|
+
const elements = collectBoundaryElements(legacySelector);
|
|
244
|
+
observeBoundaryElements(elements);
|
|
245
|
+
const seenBoxes = new Set<string>();
|
|
246
|
+
const nextBoxes = elements
|
|
247
|
+
.map((element): BoundaryBox | undefined => {
|
|
248
|
+
const boundaryId = getBoundaryId(element);
|
|
249
|
+
if (boundaryId === undefined || boundaryId === '') {
|
|
250
|
+
return undefined;
|
|
251
|
+
}
|
|
252
|
+
const rect = element.getBoundingClientRect();
|
|
253
|
+
if (rect.width <= 0 || rect.height <= 0) {
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
256
|
+
const rectKey = formatRectKey(rect);
|
|
257
|
+
const boxKey = `${boundaryId}:${rectKey}`;
|
|
258
|
+
if (seenBoxes.has(boxKey)) {
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
seenBoxes.add(boxKey);
|
|
262
|
+
const boundary = boundaries.get(boundaryId);
|
|
263
|
+
const color =
|
|
264
|
+
boundary?.color ??
|
|
265
|
+
palette[hashBoundaryId(boundaryId) % palette.length];
|
|
266
|
+
const label = boundary?.label ?? boundary?.appId ?? boundaryId;
|
|
267
|
+
const expose = element.dataset.modernMfExpose;
|
|
268
|
+
const detail =
|
|
269
|
+
expose !== undefined &&
|
|
270
|
+
expose !== '' &&
|
|
271
|
+
expose !== label &&
|
|
272
|
+
expose !== boundaryId
|
|
273
|
+
? expose
|
|
274
|
+
: undefined;
|
|
275
|
+
const box: BoundaryBox = {
|
|
276
|
+
color,
|
|
277
|
+
height: rect.height,
|
|
278
|
+
id: boxKey,
|
|
279
|
+
label,
|
|
280
|
+
left: rect.left,
|
|
281
|
+
top: rect.top,
|
|
282
|
+
width: rect.width,
|
|
283
|
+
};
|
|
284
|
+
if (detail !== undefined) {
|
|
285
|
+
box.detail = detail;
|
|
286
|
+
}
|
|
287
|
+
return box;
|
|
288
|
+
})
|
|
289
|
+
.filter((box): box is BoundaryBox => box !== undefined);
|
|
290
|
+
setBoxes(nextBoxes);
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
resizeObserver =
|
|
294
|
+
typeof ResizeObserver === 'undefined'
|
|
295
|
+
? undefined
|
|
296
|
+
: new ResizeObserver(readBoxes);
|
|
297
|
+
readBoxes();
|
|
298
|
+
|
|
299
|
+
const mutationObserver = new MutationObserver(readBoxes);
|
|
300
|
+
mutationObserver.observe(document.body, {
|
|
301
|
+
childList: true,
|
|
302
|
+
subtree: true,
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
window.addEventListener('resize', readBoxes);
|
|
306
|
+
window.addEventListener('scroll', readBoxes, true);
|
|
307
|
+
|
|
308
|
+
return () => {
|
|
309
|
+
mutationObserver.disconnect();
|
|
310
|
+
resizeObserver?.disconnect();
|
|
311
|
+
window.removeEventListener('resize', readBoxes);
|
|
312
|
+
window.removeEventListener('scroll', readBoxes, true);
|
|
313
|
+
};
|
|
314
|
+
}, [boundaries, enabled, legacySelector]);
|
|
315
|
+
|
|
316
|
+
if (!mounted) {
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const shouldRenderToggle =
|
|
321
|
+
controlMode === 'visible' || (controlMode === 'hidden-when-off' && enabled);
|
|
322
|
+
|
|
323
|
+
return (
|
|
324
|
+
<>
|
|
325
|
+
{shouldRenderToggle ? (
|
|
326
|
+
<label
|
|
327
|
+
style={{
|
|
328
|
+
alignItems: 'center',
|
|
329
|
+
background: 'rgba(255, 255, 255, 0.96)',
|
|
330
|
+
border: '1px solid rgba(0, 0, 0, 0.1)',
|
|
331
|
+
borderRadius: 10,
|
|
332
|
+
boxShadow: '0 10px 28px rgba(0, 0, 0, 0.14)',
|
|
333
|
+
color: '#111827',
|
|
334
|
+
display: 'flex',
|
|
335
|
+
font: '600 13px/1.2 system-ui, sans-serif',
|
|
336
|
+
gap: 8,
|
|
337
|
+
left: 'max(12px, env(safe-area-inset-left))',
|
|
338
|
+
maxWidth: 'calc(100vw - 24px)',
|
|
339
|
+
padding: '9px 11px',
|
|
340
|
+
position: 'fixed',
|
|
341
|
+
top: 'max(12px, env(safe-area-inset-top))',
|
|
342
|
+
zIndex: 2147483000,
|
|
343
|
+
}}
|
|
344
|
+
>
|
|
345
|
+
<input
|
|
346
|
+
checked={enabled}
|
|
347
|
+
onChange={event => setEnabled(event.currentTarget.checked)}
|
|
348
|
+
type="checkbox"
|
|
349
|
+
/>
|
|
350
|
+
<span>{toggleLabel}</span>
|
|
351
|
+
</label>
|
|
352
|
+
) : null}
|
|
353
|
+
{enabled ? (
|
|
354
|
+
<div aria-hidden="true">
|
|
355
|
+
{boxes.map(box => (
|
|
356
|
+
<div
|
|
357
|
+
key={box.id}
|
|
358
|
+
data-modern-boundary-overlay=""
|
|
359
|
+
data-modern-boundary-overlay-id={box.id}
|
|
360
|
+
data-modern-boundary-overlay-label={box.label}
|
|
361
|
+
style={{
|
|
362
|
+
border: `2px solid ${box.color}`,
|
|
363
|
+
borderRadius: 8,
|
|
364
|
+
boxSizing: 'border-box',
|
|
365
|
+
boxShadow: `0 0 0 1px rgba(255,255,255,.72), 0 6px 20px color-mix(in srgb, ${box.color} 20%, transparent)`,
|
|
366
|
+
height: box.height,
|
|
367
|
+
left: box.left,
|
|
368
|
+
pointerEvents: 'none',
|
|
369
|
+
position: 'fixed',
|
|
370
|
+
top: box.top,
|
|
371
|
+
width: box.width,
|
|
372
|
+
zIndex: 2147482999,
|
|
373
|
+
}}
|
|
374
|
+
>
|
|
375
|
+
<span
|
|
376
|
+
style={{
|
|
377
|
+
background: box.color,
|
|
378
|
+
borderRadius: 999,
|
|
379
|
+
color: '#111827',
|
|
380
|
+
display: 'grid',
|
|
381
|
+
font: '800 11px/1.1 system-ui, sans-serif',
|
|
382
|
+
gap: 3,
|
|
383
|
+
maxWidth: 'min(280px, calc(100vw - 24px))',
|
|
384
|
+
padding: '5px 8px',
|
|
385
|
+
position: 'absolute',
|
|
386
|
+
right: 4,
|
|
387
|
+
top: 4,
|
|
388
|
+
whiteSpace: 'nowrap',
|
|
389
|
+
}}
|
|
390
|
+
>
|
|
391
|
+
<span>{box.label}</span>
|
|
392
|
+
{box.detail !== undefined && box.detail !== '' ? (
|
|
393
|
+
<span
|
|
394
|
+
style={{
|
|
395
|
+
font: '700 10px/1.1 system-ui, sans-serif',
|
|
396
|
+
opacity: 0.82,
|
|
397
|
+
overflow: 'hidden',
|
|
398
|
+
textOverflow: 'ellipsis',
|
|
399
|
+
}}
|
|
400
|
+
>
|
|
401
|
+
{box.detail}
|
|
402
|
+
</span>
|
|
403
|
+
) : null}
|
|
404
|
+
</span>
|
|
405
|
+
</div>
|
|
406
|
+
))}
|
|
407
|
+
</div>
|
|
408
|
+
) : null}
|
|
409
|
+
</>
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export const ultramodernBoundaryDebuggerPlugin = (
|
|
414
|
+
options: BoundaryDebuggerPluginOptions,
|
|
415
|
+
): RuntimePlugin<{}> => ({
|
|
416
|
+
name: '@modern-js/runtime/boundary-debugger',
|
|
417
|
+
setup: api => {
|
|
418
|
+
api.wrapRoot((App: ComponentType<any>) => (props: any) => (
|
|
419
|
+
<>
|
|
420
|
+
<App {...props} />
|
|
421
|
+
<BoundaryDebugger {...options} />
|
|
422
|
+
</>
|
|
423
|
+
));
|
|
424
|
+
},
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
export default ultramodernBoundaryDebuggerPlugin;
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { pathToFileURL } from 'node:url';
|
|
6
|
+
import { build } from 'esbuild';
|
|
7
|
+
|
|
8
|
+
type BrowserType = {
|
|
9
|
+
launch: (options?: Record<string, unknown>) => Promise<{
|
|
10
|
+
close: () => Promise<void>;
|
|
11
|
+
newPage: () => Promise<{
|
|
12
|
+
click: (selector: string) => Promise<void>;
|
|
13
|
+
evaluate: <T>(callback: () => T | Promise<T>) => Promise<T>;
|
|
14
|
+
goto: (url: string) => Promise<unknown>;
|
|
15
|
+
waitForFunction: (
|
|
16
|
+
callback: () => boolean,
|
|
17
|
+
arg?: unknown,
|
|
18
|
+
options?: Record<string, unknown>,
|
|
19
|
+
) => Promise<unknown>;
|
|
20
|
+
on: (event: string, handler: (value: any) => void) => void;
|
|
21
|
+
}>;
|
|
22
|
+
}>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const requireFromRstestBrowserFixture = createRequire(
|
|
26
|
+
path.resolve(
|
|
27
|
+
__dirname,
|
|
28
|
+
'../../../../../tests/integration/rstest/basic-app-rstest-browser/package.json',
|
|
29
|
+
),
|
|
30
|
+
);
|
|
31
|
+
const { chromium }: { chromium: BrowserType } =
|
|
32
|
+
requireFromRstestBrowserFixture('playwright');
|
|
33
|
+
|
|
34
|
+
describe('ultramodern boundary debugger chromium overlay', () => {
|
|
35
|
+
test('keeps overlays out of layout and renders Checkout ownership inside Decide', async () => {
|
|
36
|
+
const tempDir = mkdtempSync(
|
|
37
|
+
path.join(tmpdir(), 'modern-boundary-debugger-'),
|
|
38
|
+
);
|
|
39
|
+
const entryPath = path.join(tempDir, 'entry.tsx');
|
|
40
|
+
const bundlePath = path.join(tempDir, 'bundle.js');
|
|
41
|
+
const htmlPath = path.join(tempDir, 'index.html');
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
writeFileSync(
|
|
45
|
+
entryPath,
|
|
46
|
+
`import { runtime } from '@modern-js/plugin/runtime';
|
|
47
|
+
import React from 'react';
|
|
48
|
+
import { createRoot } from 'react-dom/client';
|
|
49
|
+
import ultramodernBoundaryDebuggerPlugin from ${JSON.stringify(
|
|
50
|
+
path.resolve(__dirname, '../../src/boundary-debugger/index.tsx'),
|
|
51
|
+
)};
|
|
52
|
+
|
|
53
|
+
globalThis.React = React;
|
|
54
|
+
|
|
55
|
+
function App() {
|
|
56
|
+
return (
|
|
57
|
+
<main style={{ minHeight: 1600, padding: 48 }}>
|
|
58
|
+
<button
|
|
59
|
+
data-testid="primary-control"
|
|
60
|
+
style={{ height: 44, width: 180 }}
|
|
61
|
+
type="button"
|
|
62
|
+
>
|
|
63
|
+
Decide
|
|
64
|
+
</button>
|
|
65
|
+
<section
|
|
66
|
+
data-modern-boundary-id="shellSuperApp"
|
|
67
|
+
data-modern-mf-expose="./Decide"
|
|
68
|
+
data-testid="decide-surface"
|
|
69
|
+
style={{
|
|
70
|
+
border: '1px solid #d1d5db',
|
|
71
|
+
height: 260,
|
|
72
|
+
marginTop: 48,
|
|
73
|
+
padding: 36,
|
|
74
|
+
width: 720,
|
|
75
|
+
}}
|
|
76
|
+
>
|
|
77
|
+
<button
|
|
78
|
+
data-modern-boundary-id="verticalCheckout"
|
|
79
|
+
data-modern-mf-expose="./Controls"
|
|
80
|
+
data-testid="checkout-control"
|
|
81
|
+
style={{ height: 48, width: 220 }}
|
|
82
|
+
type="button"
|
|
83
|
+
>
|
|
84
|
+
Pay now
|
|
85
|
+
</button>
|
|
86
|
+
</section>
|
|
87
|
+
</main>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const { runtimeContext } = runtime.run({
|
|
92
|
+
config: {},
|
|
93
|
+
plugins: [ultramodernBoundaryDebuggerPlugin({
|
|
94
|
+
metadata: {
|
|
95
|
+
appId: 'shell',
|
|
96
|
+
boundaries: [
|
|
97
|
+
{
|
|
98
|
+
appId: 'shell',
|
|
99
|
+
label: 'Decide',
|
|
100
|
+
mfName: 'shellSuperApp',
|
|
101
|
+
role: 'host',
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
appId: 'checkout',
|
|
105
|
+
label: 'Checkout',
|
|
106
|
+
mfName: 'verticalCheckout',
|
|
107
|
+
ownerTeam: 'checkout-platform',
|
|
108
|
+
role: 'vertical',
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
schemaVersion: 1,
|
|
112
|
+
},
|
|
113
|
+
})],
|
|
114
|
+
});
|
|
115
|
+
const WrappedApp = runtimeContext.hooks.wrapRoot.call(App);
|
|
116
|
+
|
|
117
|
+
createRoot(document.getElementById('root')).render(
|
|
118
|
+
React.createElement(WrappedApp),
|
|
119
|
+
);
|
|
120
|
+
`,
|
|
121
|
+
'utf8',
|
|
122
|
+
);
|
|
123
|
+
await build({
|
|
124
|
+
absWorkingDir: path.resolve(__dirname, '../..'),
|
|
125
|
+
bundle: true,
|
|
126
|
+
define: {
|
|
127
|
+
'process.env.NODE_ENV': '"test"',
|
|
128
|
+
},
|
|
129
|
+
entryPoints: [entryPath],
|
|
130
|
+
format: 'esm',
|
|
131
|
+
jsx: 'transform',
|
|
132
|
+
jsxFactory: 'React.createElement',
|
|
133
|
+
jsxFragment: 'React.Fragment',
|
|
134
|
+
nodePaths: [
|
|
135
|
+
path.resolve(__dirname, '../../node_modules'),
|
|
136
|
+
path.resolve(__dirname, '../../../../../node_modules'),
|
|
137
|
+
],
|
|
138
|
+
outfile: bundlePath,
|
|
139
|
+
platform: 'browser',
|
|
140
|
+
});
|
|
141
|
+
writeFileSync(
|
|
142
|
+
htmlPath,
|
|
143
|
+
`<!doctype html><html><body><div id="root"></div><script type="module">${readFileSync(
|
|
144
|
+
bundlePath,
|
|
145
|
+
'utf8',
|
|
146
|
+
)}</script></body></html>`,
|
|
147
|
+
'utf8',
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const browser = await chromium.launch({ headless: true });
|
|
151
|
+
try {
|
|
152
|
+
const page = await browser.newPage();
|
|
153
|
+
const browserErrors: string[] = [];
|
|
154
|
+
page.on('console', message => {
|
|
155
|
+
if (message.type?.() === 'error') {
|
|
156
|
+
browserErrors.push(message.text?.() ?? String(message));
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
page.on('pageerror', error => {
|
|
160
|
+
browserErrors.push(
|
|
161
|
+
error instanceof Error ? error.message : String(error),
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
await page.goto(pathToFileURL(htmlPath).href);
|
|
165
|
+
try {
|
|
166
|
+
await page.waitForFunction(
|
|
167
|
+
() =>
|
|
168
|
+
Boolean(
|
|
169
|
+
document.querySelector('[data-testid="primary-control"]'),
|
|
170
|
+
),
|
|
171
|
+
undefined,
|
|
172
|
+
{ timeout: 5000 },
|
|
173
|
+
);
|
|
174
|
+
} catch (error) {
|
|
175
|
+
const bodyHtml = await page.evaluate(() => document.body.innerHTML);
|
|
176
|
+
throw new Error(
|
|
177
|
+
`${error instanceof Error ? error.message : String(error)}\n` +
|
|
178
|
+
`browserErrors=${JSON.stringify(browserErrors)}\n` +
|
|
179
|
+
`body=${bodyHtml.slice(0, 500)}`,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
expect(browserErrors).toEqual([]);
|
|
183
|
+
|
|
184
|
+
const before = await page.evaluate(() => {
|
|
185
|
+
const control = document.querySelector(
|
|
186
|
+
'[data-testid="primary-control"]',
|
|
187
|
+
) as HTMLElement;
|
|
188
|
+
const rect = control.getBoundingClientRect();
|
|
189
|
+
return {
|
|
190
|
+
rect: {
|
|
191
|
+
height: rect.height,
|
|
192
|
+
left: rect.left,
|
|
193
|
+
top: rect.top,
|
|
194
|
+
width: rect.width,
|
|
195
|
+
},
|
|
196
|
+
scrollHeight: document.documentElement.scrollHeight,
|
|
197
|
+
};
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
await page.click('text=show team boundaries');
|
|
201
|
+
await page.waitForFunction(
|
|
202
|
+
() =>
|
|
203
|
+
document.querySelectorAll('[data-modern-boundary-overlay]')
|
|
204
|
+
.length === 2,
|
|
205
|
+
undefined,
|
|
206
|
+
{ timeout: 5000 },
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
const after = await page.evaluate(() => {
|
|
210
|
+
const control = document.querySelector(
|
|
211
|
+
'[data-testid="primary-control"]',
|
|
212
|
+
) as HTMLElement;
|
|
213
|
+
const rect = control.getBoundingClientRect();
|
|
214
|
+
const checkoutOverlay = document.querySelector(
|
|
215
|
+
'[data-modern-boundary-overlay-label="Checkout"]',
|
|
216
|
+
) as HTMLElement | null;
|
|
217
|
+
return {
|
|
218
|
+
checkoutOverlayStyle: checkoutOverlay
|
|
219
|
+
? {
|
|
220
|
+
boxSizing: checkoutOverlay.style.boxSizing,
|
|
221
|
+
pointerEvents: checkoutOverlay.style.pointerEvents,
|
|
222
|
+
position: checkoutOverlay.style.position,
|
|
223
|
+
}
|
|
224
|
+
: null,
|
|
225
|
+
checkoutOverlayText: checkoutOverlay?.textContent ?? '',
|
|
226
|
+
rect: {
|
|
227
|
+
height: rect.height,
|
|
228
|
+
left: rect.left,
|
|
229
|
+
top: rect.top,
|
|
230
|
+
width: rect.width,
|
|
231
|
+
},
|
|
232
|
+
scrollHeight: document.documentElement.scrollHeight,
|
|
233
|
+
};
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
expect(after.scrollHeight).toBe(before.scrollHeight);
|
|
237
|
+
expect(after.rect).toEqual(before.rect);
|
|
238
|
+
expect(after.checkoutOverlayStyle).toEqual({
|
|
239
|
+
boxSizing: 'border-box',
|
|
240
|
+
pointerEvents: 'none',
|
|
241
|
+
position: 'fixed',
|
|
242
|
+
});
|
|
243
|
+
expect(after.checkoutOverlayText).toContain('Checkout');
|
|
244
|
+
expect(after.checkoutOverlayText).toContain('./Controls');
|
|
245
|
+
} finally {
|
|
246
|
+
await browser.close();
|
|
247
|
+
}
|
|
248
|
+
} finally {
|
|
249
|
+
rmSync(tempDir, { force: true, recursive: true });
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
});
|