@rozenite/network-activity-plugin 1.9.0 → 1.10.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/CHANGELOG.md +43 -0
- package/dist/devtools/App.html +2 -2
- package/dist/devtools/assets/{App-m6xge0az.css → App-CUXU0mup.css} +152 -2
- package/dist/devtools/assets/{App-hSoryVpJ.js → App-DsimzJvx.js} +6827 -970
- package/dist/react-native/chunks/boot-recording.cjs +138 -14
- package/dist/react-native/chunks/boot-recording.js +138 -14
- package/dist/react-native/chunks/get-nitro-module.cjs +4 -1
- package/dist/react-native/chunks/get-nitro-module.js +4 -1
- package/dist/react-native/chunks/useNetworkActivityDevTools.require.cjs +20 -1
- package/dist/react-native/chunks/useNetworkActivityDevTools.require.js +20 -1
- package/dist/react-native/index.d.ts +37 -1
- package/dist/rozenite.json +1 -1
- package/dist/sdk/index.d.ts +37 -1
- package/package.json +12 -7
- package/src/react-native/agent/use-network-activity-agent-tools.ts +22 -4
- package/src/react-native/http/__tests__/http-utils.test.ts +228 -0
- package/src/react-native/http/http-utils.ts +208 -25
- package/src/react-native/network-inspector.ts +2 -2
- package/src/react-native/nitro-fetch/get-nitro-module.ts +5 -1
- package/src/react-native/nitro-fetch/nitro-network-inspector.ts +8 -2
- package/src/shared/http-events.ts +40 -1
- package/src/ui/components/CodeBlock.tsx +45 -1
- package/src/ui/components/FilterBar.tsx +366 -58
- package/src/ui/components/HexView.tsx +54 -0
- package/src/ui/components/MetadataCard.tsx +95 -0
- package/src/ui/components/RequestList.tsx +192 -34
- package/src/ui/components/SidePanel.tsx +42 -1
- package/src/ui/components/ViewToggle.tsx +44 -0
- package/src/ui/components/XmlTree.tsx +160 -0
- package/src/ui/components/__tests__/CodeBlock.test.tsx +89 -0
- package/src/ui/components/__tests__/HexView.test.tsx +41 -0
- package/src/ui/components/__tests__/MetadataCard.test.tsx +107 -0
- package/src/ui/components/__tests__/ViewToggle.test.tsx +80 -0
- package/src/ui/components/__tests__/XmlTree.test.tsx +149 -0
- package/src/ui/response-renderers/__tests__/binary-too-large.test.tsx +56 -0
- package/src/ui/response-renderers/__tests__/binary.test.tsx +96 -0
- package/src/ui/response-renderers/__tests__/dispatch.test.ts +124 -0
- package/src/ui/response-renderers/__tests__/html.test.tsx +101 -0
- package/src/ui/response-renderers/__tests__/image.test.tsx +73 -0
- package/src/ui/response-renderers/__tests__/json.test.tsx +95 -0
- package/src/ui/response-renderers/__tests__/svg.test.tsx +46 -0
- package/src/ui/response-renderers/__tests__/xml.test.tsx +100 -0
- package/src/ui/response-renderers/binary-too-large.tsx +36 -0
- package/src/ui/response-renderers/binary.tsx +31 -0
- package/src/ui/response-renderers/empty.tsx +14 -0
- package/src/ui/response-renderers/html.tsx +36 -0
- package/src/ui/response-renderers/image.tsx +37 -0
- package/src/ui/response-renderers/index.ts +55 -0
- package/src/ui/response-renderers/json.tsx +40 -0
- package/src/ui/response-renderers/svg.tsx +27 -0
- package/src/ui/response-renderers/text-fallback.tsx +14 -0
- package/src/ui/response-renderers/types.ts +38 -0
- package/src/ui/response-renderers/unknown.tsx +18 -0
- package/src/ui/response-renderers/xml.tsx +46 -0
- package/src/ui/state/derived.ts +12 -0
- package/src/ui/state/model.ts +6 -1
- package/src/ui/state/store.ts +39 -2
- package/src/ui/tabs/InitiatorTab.tsx +230 -0
- package/src/ui/tabs/ResponseTab.tsx +80 -97
- package/src/ui/tabs/__tests__/ResponseTab.test.tsx +102 -0
- package/src/ui/utils/__tests__/download.test.ts +115 -0
- package/src/ui/utils/__tests__/hex.test.ts +84 -0
- package/src/ui/utils/__tests__/symbolication.test.ts +207 -0
- package/src/ui/utils/download.ts +154 -0
- package/src/ui/utils/hex.ts +59 -0
- package/src/ui/utils/initiator.ts +136 -0
- package/src/ui/utils/symbolication.ts +248 -0
- package/src/ui/views/InspectorView.tsx +8 -5
- package/src/utils/__tests__/getContentTypeMimeType.test.ts +34 -0
- package/src/utils/getContentTypeMimeType.ts +14 -0
- package/vite.config.ts +5 -1
- package/vitest.setup.ts +31 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import type { Initiator, InitiatorStackFrame } from '../../shared/client';
|
|
2
|
+
|
|
3
|
+
type ReactNativeStackFrame = {
|
|
4
|
+
methodName: string;
|
|
5
|
+
file: string | null | undefined;
|
|
6
|
+
lineNumber: number | null | undefined;
|
|
7
|
+
column: number | null | undefined;
|
|
8
|
+
collapse?: boolean;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type SymbolicatedStackTrace = {
|
|
12
|
+
stack: ReadonlyArray<ReactNativeStackFrame>;
|
|
13
|
+
codeFrame?: Initiator['codeFrame'];
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type SymbolicateStackTrace = (
|
|
17
|
+
stack: ReadonlyArray<ReactNativeStackFrame>,
|
|
18
|
+
) => Promise<SymbolicatedStackTrace>;
|
|
19
|
+
|
|
20
|
+
const normalizeFunctionName = (functionName?: string) => {
|
|
21
|
+
const trimmedFunctionName = functionName?.trim();
|
|
22
|
+
|
|
23
|
+
return trimmedFunctionName &&
|
|
24
|
+
trimmedFunctionName !== '<anonymous>' &&
|
|
25
|
+
trimmedFunctionName !== 'anonymous' &&
|
|
26
|
+
trimmedFunctionName !== '<unknown>'
|
|
27
|
+
? trimmedFunctionName
|
|
28
|
+
: undefined;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const getGeneratedFrameLocation = (frame: InitiatorStackFrame) => ({
|
|
32
|
+
url: frame.generatedUrl ?? frame.url,
|
|
33
|
+
lineNumber: frame.generatedLineNumber ?? frame.lineNumber,
|
|
34
|
+
columnNumber: frame.generatedColumnNumber ?? frame.columnNumber,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const isGeneratedBundleUrl = (url: string) =>
|
|
38
|
+
/[^/]+\.bundle(?:[/?#]|$)/.test(url);
|
|
39
|
+
|
|
40
|
+
const canSymbolicateStack = (stack?: InitiatorStackFrame[]) =>
|
|
41
|
+
stack?.some((frame) =>
|
|
42
|
+
getGeneratedFrameLocation(frame).url?.startsWith('http'),
|
|
43
|
+
) ?? false;
|
|
44
|
+
|
|
45
|
+
const toReactNativeStackFrame = (
|
|
46
|
+
frame: InitiatorStackFrame,
|
|
47
|
+
): ReactNativeStackFrame | null => {
|
|
48
|
+
const generatedLocation = getGeneratedFrameLocation(frame);
|
|
49
|
+
|
|
50
|
+
if (!generatedLocation.url) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
methodName: frame.functionName ?? '<anonymous>',
|
|
56
|
+
file: generatedLocation.url,
|
|
57
|
+
lineNumber: generatedLocation.lineNumber,
|
|
58
|
+
column: generatedLocation.columnNumber,
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const fromSymbolicatedStackFrame = (
|
|
63
|
+
frame: ReactNativeStackFrame,
|
|
64
|
+
generatedFrame: InitiatorStackFrame = {},
|
|
65
|
+
): InitiatorStackFrame => {
|
|
66
|
+
const generatedLocation = getGeneratedFrameLocation(generatedFrame);
|
|
67
|
+
const sourceUrl =
|
|
68
|
+
frame.file &&
|
|
69
|
+
frame.file !== generatedLocation.url &&
|
|
70
|
+
!isGeneratedBundleUrl(frame.file)
|
|
71
|
+
? frame.file
|
|
72
|
+
: undefined;
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
functionName:
|
|
76
|
+
normalizeFunctionName(frame.methodName) ?? generatedFrame.functionName,
|
|
77
|
+
url: sourceUrl,
|
|
78
|
+
lineNumber: sourceUrl ? (frame.lineNumber ?? undefined) : undefined,
|
|
79
|
+
columnNumber: sourceUrl ? (frame.column ?? undefined) : undefined,
|
|
80
|
+
generatedUrl: generatedLocation.url,
|
|
81
|
+
generatedLineNumber: generatedLocation.lineNumber,
|
|
82
|
+
generatedColumnNumber: generatedLocation.columnNumber,
|
|
83
|
+
isCollapsed: frame.collapse,
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const getComparableSourcePath = (url?: string) =>
|
|
88
|
+
url?.split(/[?#]/)[0].replace(/^file:\/\//, '');
|
|
89
|
+
|
|
90
|
+
const ANSI_SEQUENCE_PATTERN = new RegExp(
|
|
91
|
+
[
|
|
92
|
+
'[\\u001b\\u009b][[\\]()#;?]*',
|
|
93
|
+
'(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\\u0007)',
|
|
94
|
+
'|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))',
|
|
95
|
+
].join(''),
|
|
96
|
+
'g',
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const stripAnsiSequences = (value: string) =>
|
|
100
|
+
// Metro returns code frames formatted for terminals. DevTools renders them
|
|
101
|
+
// as plain text, so terminal control sequences need to be removed.
|
|
102
|
+
value.replace(ANSI_SEQUENCE_PATTERN, '');
|
|
103
|
+
|
|
104
|
+
const sanitizeCodeFrame = (
|
|
105
|
+
codeFrame: Initiator['codeFrame'] | undefined,
|
|
106
|
+
) => {
|
|
107
|
+
if (!codeFrame) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
...codeFrame,
|
|
113
|
+
content: stripAnsiSequences(codeFrame.content),
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const isSameSourcePath = (left?: string, right?: string) => {
|
|
118
|
+
const leftPath = getComparableSourcePath(left);
|
|
119
|
+
const rightPath = getComparableSourcePath(right);
|
|
120
|
+
|
|
121
|
+
if (!leftPath || !rightPath) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return leftPath.endsWith(rightPath) || rightPath.endsWith(leftPath);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const getSourceFrameForCodeFrame = (
|
|
129
|
+
stack: InitiatorStackFrame[],
|
|
130
|
+
codeFrame: Initiator['codeFrame'] | undefined,
|
|
131
|
+
) => {
|
|
132
|
+
if (!codeFrame?.fileName) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
stack.find((frame) => isSameSourcePath(codeFrame.fileName, frame.url)) ??
|
|
138
|
+
null
|
|
139
|
+
);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const getCodeFrameForSourceFrame = (
|
|
143
|
+
codeFrame: Initiator['codeFrame'] | undefined,
|
|
144
|
+
sourceFrame: InitiatorStackFrame | undefined,
|
|
145
|
+
) => {
|
|
146
|
+
if (!codeFrame || !isSameSourcePath(codeFrame.fileName, sourceFrame?.url)) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return sanitizeCodeFrame(codeFrame);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const getPreferredSourceFrame = (
|
|
154
|
+
stack: InitiatorStackFrame[],
|
|
155
|
+
codeFrame: Initiator['codeFrame'] | undefined,
|
|
156
|
+
) =>
|
|
157
|
+
getSourceFrameForCodeFrame(stack, codeFrame) ??
|
|
158
|
+
stack.find((frame) => frame.url && !frame.isCollapsed) ??
|
|
159
|
+
stack.find((frame) => frame.url) ??
|
|
160
|
+
stack[0];
|
|
161
|
+
|
|
162
|
+
const getSymbolicationEndpoint = () => {
|
|
163
|
+
if (typeof window === 'undefined') {
|
|
164
|
+
throw new Error('Unable to resolve Metro symbolication endpoint');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return new URL('/symbolicate', window.location.origin).toString();
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
export const symbolicateStackTraceWithMetro: SymbolicateStackTrace = async (
|
|
171
|
+
stack,
|
|
172
|
+
) => {
|
|
173
|
+
const response = await fetch(getSymbolicationEndpoint(), {
|
|
174
|
+
method: 'POST',
|
|
175
|
+
headers: {
|
|
176
|
+
'Content-Type': 'application/json',
|
|
177
|
+
},
|
|
178
|
+
body: JSON.stringify({ stack }),
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
if (!response.ok) {
|
|
182
|
+
throw new Error(
|
|
183
|
+
`Metro symbolication failed with status ${response.status}`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return response.json() as Promise<SymbolicatedStackTrace>;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
export const symbolicateInitiator = async (
|
|
191
|
+
initiator: Initiator,
|
|
192
|
+
symbolicateStackTrace: SymbolicateStackTrace = symbolicateStackTraceWithMetro,
|
|
193
|
+
): Promise<Initiator | null> => {
|
|
194
|
+
if (!canSymbolicateStack(initiator.stack)) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const generatedStackFrames =
|
|
199
|
+
initiator.stack
|
|
200
|
+
?.map(toReactNativeStackFrame)
|
|
201
|
+
.filter((frame): frame is ReactNativeStackFrame => frame !== null) ?? [];
|
|
202
|
+
|
|
203
|
+
if (generatedStackFrames.length === 0) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
const symbolicatedStackTrace =
|
|
209
|
+
await symbolicateStackTrace(generatedStackFrames);
|
|
210
|
+
|
|
211
|
+
const symbolicatedStack = symbolicatedStackTrace.stack.map((frame, index) =>
|
|
212
|
+
fromSymbolicatedStackFrame(frame, initiator.stack?.[index]),
|
|
213
|
+
);
|
|
214
|
+
const sourceFrame = getPreferredSourceFrame(
|
|
215
|
+
symbolicatedStack,
|
|
216
|
+
symbolicatedStackTrace.codeFrame,
|
|
217
|
+
);
|
|
218
|
+
const hasSourceMappedFrame = symbolicatedStack.some((frame) => frame.url);
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
...initiator,
|
|
222
|
+
type: sourceFrame?.url ? 'script' : initiator.type,
|
|
223
|
+
functionName: sourceFrame?.functionName,
|
|
224
|
+
url: sourceFrame?.url,
|
|
225
|
+
lineNumber: sourceFrame?.lineNumber,
|
|
226
|
+
columnNumber: sourceFrame?.columnNumber,
|
|
227
|
+
generatedUrl: sourceFrame?.generatedUrl ?? initiator.generatedUrl,
|
|
228
|
+
generatedLineNumber:
|
|
229
|
+
sourceFrame?.generatedLineNumber ?? initiator.generatedLineNumber,
|
|
230
|
+
generatedColumnNumber:
|
|
231
|
+
sourceFrame?.generatedColumnNumber ?? initiator.generatedColumnNumber,
|
|
232
|
+
stack: symbolicatedStack,
|
|
233
|
+
codeFrame: getCodeFrameForSourceFrame(
|
|
234
|
+
symbolicatedStackTrace.codeFrame,
|
|
235
|
+
sourceFrame,
|
|
236
|
+
),
|
|
237
|
+
symbolicationStatus: hasSourceMappedFrame ? 'complete' : 'unavailable',
|
|
238
|
+
symbolicationError: undefined,
|
|
239
|
+
};
|
|
240
|
+
} catch (error) {
|
|
241
|
+
return {
|
|
242
|
+
...initiator,
|
|
243
|
+
symbolicationStatus: 'failed',
|
|
244
|
+
symbolicationError:
|
|
245
|
+
error instanceof Error ? error.message : 'Unable to symbolicate stack',
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
};
|
|
@@ -2,7 +2,11 @@ import { useEffect, useState } from 'react';
|
|
|
2
2
|
import { Toolbar } from '../components/Toolbar';
|
|
3
3
|
import { RequestList } from '../components/RequestList';
|
|
4
4
|
import { SidePanel } from '../components/SidePanel';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
createDefaultFilter,
|
|
7
|
+
FilterBar,
|
|
8
|
+
FilterState,
|
|
9
|
+
} from '../components/FilterBar';
|
|
6
10
|
import { NetworkActivityDevToolsClient } from '../../shared/client';
|
|
7
11
|
import {
|
|
8
12
|
useNetworkActivityClientManagement,
|
|
@@ -20,10 +24,9 @@ export const InspectorView = ({ client }: InspectorViewProps) => {
|
|
|
20
24
|
const clientManagement = useNetworkActivityClientManagement();
|
|
21
25
|
const hasSelectedRequest = useHasSelectedRequest();
|
|
22
26
|
const overrides = useOverrides();
|
|
23
|
-
const [filter, setFilter] = useState<FilterState>(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
});
|
|
27
|
+
const [filter, setFilter] = useState<FilterState>(() =>
|
|
28
|
+
createDefaultFilter(),
|
|
29
|
+
);
|
|
27
30
|
|
|
28
31
|
useEffect(() => {
|
|
29
32
|
if (!client) {
|
|
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest';
|
|
|
2
2
|
import {
|
|
3
3
|
getContentTypeMime,
|
|
4
4
|
isJsonContentType,
|
|
5
|
+
isXmlContentType,
|
|
5
6
|
} from '../getContentTypeMimeType';
|
|
6
7
|
|
|
7
8
|
describe('getContentTypeMimeType', () => {
|
|
@@ -28,4 +29,37 @@ describe('getContentTypeMimeType', () => {
|
|
|
28
29
|
}),
|
|
29
30
|
).toBe('Application/LD+JSON');
|
|
30
31
|
});
|
|
32
|
+
|
|
33
|
+
it('recognizes application/xml and text/xml with parameters', () => {
|
|
34
|
+
expect(isXmlContentType('application/xml')).toBe(true);
|
|
35
|
+
expect(isXmlContentType('application/xml; charset=utf-8')).toBe(true);
|
|
36
|
+
expect(isXmlContentType('text/xml')).toBe(true);
|
|
37
|
+
expect(isXmlContentType('text/xml; charset=utf-8')).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('recognizes RFC 7303 +xml content types (Atom, RSS, SOAP, XHTML, SVG, ...)', () => {
|
|
41
|
+
expect(isXmlContentType('application/atom+xml')).toBe(true);
|
|
42
|
+
expect(isXmlContentType('application/rss+xml')).toBe(true);
|
|
43
|
+
expect(isXmlContentType('application/soap+xml; charset=utf-8')).toBe(true);
|
|
44
|
+
expect(isXmlContentType('application/xhtml+xml')).toBe(true);
|
|
45
|
+
// SVG is XML by structure — the predicate matches it. The registry
|
|
46
|
+
// gives svgRenderer an earlier slot so it claims SVG first; that
|
|
47
|
+
// ordering is verified separately in dispatch.test.ts.
|
|
48
|
+
expect(isXmlContentType('image/svg+xml')).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('is case-insensitive (normalizeContentType lowercases)', () => {
|
|
52
|
+
expect(isXmlContentType('Application/XML')).toBe(true);
|
|
53
|
+
expect(isXmlContentType('Application/Atom+XML')).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('rejects non-xml content types', () => {
|
|
57
|
+
expect(isXmlContentType('text/plain')).toBe(false);
|
|
58
|
+
expect(isXmlContentType('text/html')).toBe(false);
|
|
59
|
+
expect(isXmlContentType('application/json')).toBe(false);
|
|
60
|
+
expect(isXmlContentType('application/xmlfoo')).toBe(false);
|
|
61
|
+
expect(isXmlContentType(undefined)).toBe(false);
|
|
62
|
+
expect(isXmlContentType(null)).toBe(false);
|
|
63
|
+
expect(isXmlContentType('')).toBe(false);
|
|
64
|
+
});
|
|
31
65
|
});
|
|
@@ -29,3 +29,17 @@ export function isJsonContentType(contentType: string | null | undefined) {
|
|
|
29
29
|
|
|
30
30
|
return mimeType === 'application/json' || mimeType.endsWith('+json');
|
|
31
31
|
}
|
|
32
|
+
|
|
33
|
+
export function isXmlContentType(contentType: string | null | undefined) {
|
|
34
|
+
if (!contentType) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const mimeType = normalizeContentType(contentType);
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
mimeType === 'application/xml' ||
|
|
42
|
+
mimeType === 'text/xml' ||
|
|
43
|
+
mimeType.endsWith('+xml')
|
|
44
|
+
);
|
|
45
|
+
}
|
package/vite.config.ts
CHANGED
|
@@ -8,8 +8,12 @@ export default defineConfig({
|
|
|
8
8
|
plugins: [rozenitePlugin()],
|
|
9
9
|
test: {
|
|
10
10
|
passWithNoTests: true,
|
|
11
|
+
setupFiles: ['./vitest.setup.ts'],
|
|
11
12
|
alias: {
|
|
12
|
-
'@rozenite/agent-shared': resolve(
|
|
13
|
+
'@rozenite/agent-shared': resolve(
|
|
14
|
+
__dirname,
|
|
15
|
+
'../agent-shared/src/index.ts',
|
|
16
|
+
),
|
|
13
17
|
},
|
|
14
18
|
},
|
|
15
19
|
base: './',
|
package/vitest.setup.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { afterEach, vi } from 'vitest';
|
|
3
|
+
import { cleanup } from '@testing-library/react';
|
|
4
|
+
import '@testing-library/jest-dom/vitest';
|
|
5
|
+
|
|
6
|
+
afterEach(() => {
|
|
7
|
+
cleanup();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
// `react-virtuoso` doesn't render rows in jsdom — the underlying
|
|
11
|
+
// element-resize observation never fires without a real browser
|
|
12
|
+
// layout. Replace it with a non-virtualized passthrough so RTL tests
|
|
13
|
+
// can assert on the actual row content. Production code is unaffected.
|
|
14
|
+
vi.mock('react-virtuoso', () => ({
|
|
15
|
+
Virtuoso: ({
|
|
16
|
+
totalCount,
|
|
17
|
+
itemContent,
|
|
18
|
+
className,
|
|
19
|
+
}: {
|
|
20
|
+
totalCount: number;
|
|
21
|
+
itemContent: (index: number) => React.ReactNode;
|
|
22
|
+
className?: string;
|
|
23
|
+
}) =>
|
|
24
|
+
React.createElement(
|
|
25
|
+
'div',
|
|
26
|
+
{ 'data-testid': 'virtuoso-mock', className },
|
|
27
|
+
...Array.from({ length: totalCount }, (_, i) =>
|
|
28
|
+
React.createElement('div', { key: i }, itemContent(i)),
|
|
29
|
+
),
|
|
30
|
+
),
|
|
31
|
+
}));
|