|
1 |
| -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ |
2 |
| -import { getCurrentHub } from '@sentry/core'; |
3 |
| -import type { ReplayRecordingData, Transport } from '@sentry/types'; |
4 |
| -import { TextEncoder } from 'util'; |
5 |
| - |
6 |
| -import type { ReplayContainer, Session } from './src/types'; |
7 |
| - |
8 |
| -// eslint-disable-next-line @typescript-eslint/no-explicit-any |
9 |
| -(global as any).TextEncoder = TextEncoder; |
10 |
| - |
11 |
| -type MockTransport = jest.MockedFunction<Transport['send']>; |
12 |
| - |
13 | 1 | jest.mock('./src/util/isBrowser', () => {
|
14 | 2 | return {
|
15 | 3 | isBrowser: () => true,
|
16 | 4 | };
|
17 | 5 | });
|
18 |
| - |
19 |
| -type EnvelopeHeader = { |
20 |
| - event_id: string; |
21 |
| - sent_at: string; |
22 |
| - sdk: { |
23 |
| - name: string; |
24 |
| - version?: string; |
25 |
| - }; |
26 |
| -}; |
27 |
| - |
28 |
| -type ReplayEventHeader = { type: 'replay_event' }; |
29 |
| -type ReplayEventPayload = Record<string, unknown>; |
30 |
| -type RecordingHeader = { type: 'replay_recording'; length: number }; |
31 |
| -type RecordingPayloadHeader = Record<string, unknown>; |
32 |
| -type SentReplayExpected = { |
33 |
| - envelopeHeader?: EnvelopeHeader; |
34 |
| - replayEventHeader?: ReplayEventHeader; |
35 |
| - replayEventPayload?: ReplayEventPayload; |
36 |
| - recordingHeader?: RecordingHeader; |
37 |
| - recordingPayloadHeader?: RecordingPayloadHeader; |
38 |
| - recordingData?: ReplayRecordingData; |
39 |
| -}; |
40 |
| - |
41 |
| -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type |
42 |
| -const toHaveSameSession = function (received: jest.Mocked<ReplayContainer>, expected: undefined | Session) { |
43 |
| - const pass = this.equals(received.session?.id, expected?.id) as boolean; |
44 |
| - |
45 |
| - const options = { |
46 |
| - isNot: this.isNot, |
47 |
| - promise: this.promise, |
48 |
| - }; |
49 |
| - |
50 |
| - return { |
51 |
| - pass, |
52 |
| - message: () => |
53 |
| - `${this.utils.matcherHint( |
54 |
| - 'toHaveSameSession', |
55 |
| - undefined, |
56 |
| - undefined, |
57 |
| - options, |
58 |
| - )}\n\n${this.utils.printDiffOrStringify(expected, received.session, 'Expected', 'Received')}`, |
59 |
| - }; |
60 |
| -}; |
61 |
| - |
62 |
| -type Result = { |
63 |
| - passed: boolean; |
64 |
| - key: string; |
65 |
| - expectedVal: SentReplayExpected[keyof SentReplayExpected]; |
66 |
| - actualVal: SentReplayExpected[keyof SentReplayExpected]; |
67 |
| -}; |
68 |
| -type Call = [ |
69 |
| - EnvelopeHeader, |
70 |
| - [ |
71 |
| - [ReplayEventHeader | undefined, ReplayEventPayload | undefined], |
72 |
| - [RecordingHeader | undefined, RecordingPayloadHeader | undefined], |
73 |
| - ], |
74 |
| -]; |
75 |
| -type CheckCallForSentReplayResult = { pass: boolean; call: Call | undefined; results: Result[] }; |
76 |
| - |
77 |
| -function checkCallForSentReplay( |
78 |
| - call: Call | undefined, |
79 |
| - expected?: SentReplayExpected | { sample: SentReplayExpected; inverse: boolean }, |
80 |
| -): CheckCallForSentReplayResult { |
81 |
| - const envelopeHeader = call?.[0]; |
82 |
| - const envelopeItems = call?.[1] || [[], []]; |
83 |
| - const [[replayEventHeader, replayEventPayload], [recordingHeader, recordingPayload] = []] = envelopeItems; |
84 |
| - |
85 |
| - // @ts-ignore recordingPayload is always a string in our tests |
86 |
| - const [recordingPayloadHeader, recordingData] = recordingPayload?.split('\n') || []; |
87 |
| - |
88 |
| - const actualObj: Required<SentReplayExpected> = { |
89 |
| - // @ts-ignore Custom envelope |
90 |
| - envelopeHeader: envelopeHeader, |
91 |
| - // @ts-ignore Custom envelope |
92 |
| - replayEventHeader: replayEventHeader, |
93 |
| - // @ts-ignore Custom envelope |
94 |
| - replayEventPayload: replayEventPayload, |
95 |
| - // @ts-ignore Custom envelope |
96 |
| - recordingHeader: recordingHeader, |
97 |
| - recordingPayloadHeader: recordingPayloadHeader && JSON.parse(recordingPayloadHeader), |
98 |
| - recordingData, |
99 |
| - }; |
100 |
| - |
101 |
| - const isObjectContaining = expected && 'sample' in expected && 'inverse' in expected; |
102 |
| - const expectedObj = isObjectContaining |
103 |
| - ? (expected as { sample: SentReplayExpected }).sample |
104 |
| - : (expected as SentReplayExpected); |
105 |
| - |
106 |
| - if (isObjectContaining) { |
107 |
| - console.warn('`expect.objectContaining` is unnecessary when using the `toHaveSentReplay` matcher'); |
108 |
| - } |
109 |
| - |
110 |
| - const results = expected |
111 |
| - ? Object.keys(expectedObj) |
112 |
| - .map(key => { |
113 |
| - const actualVal = actualObj[key as keyof SentReplayExpected]; |
114 |
| - const expectedVal = expectedObj[key as keyof SentReplayExpected]; |
115 |
| - const passed = !expectedVal || this.equals(actualVal, expectedVal); |
116 |
| - |
117 |
| - return { passed, key, expectedVal, actualVal }; |
118 |
| - }) |
119 |
| - .filter(({ passed }) => !passed) |
120 |
| - : []; |
121 |
| - |
122 |
| - const pass = Boolean(call && (!expected || results.length === 0)); |
123 |
| - |
124 |
| - return { |
125 |
| - pass, |
126 |
| - call, |
127 |
| - results, |
128 |
| - }; |
129 |
| -} |
130 |
| - |
131 |
| -/** |
132 |
| - * Only want calls that send replay events, i.e. ignore error events |
133 |
| - */ |
134 |
| -// eslint-disable-next-line @typescript-eslint/no-explicit-any |
135 |
| -function getReplayCalls(calls: any[][][]): any[][][] { |
136 |
| - return calls |
137 |
| - .map(call => { |
138 |
| - const arg = call[0]; |
139 |
| - if (arg.length !== 2) { |
140 |
| - return []; |
141 |
| - } |
142 |
| - |
143 |
| - if (!arg[1][0].find(({ type }: { type: string }) => ['replay_event', 'replay_recording'].includes(type))) { |
144 |
| - return []; |
145 |
| - } |
146 |
| - |
147 |
| - return [arg]; |
148 |
| - }) |
149 |
| - .filter(Boolean); |
150 |
| -} |
151 |
| - |
152 |
| -/** |
153 |
| - * Checks all calls to `fetch` and ensures a replay was uploaded by |
154 |
| - * checking the `fetch()` request's body. |
155 |
| - */ |
156 |
| -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type |
157 |
| -const toHaveSentReplay = function ( |
158 |
| - _received: jest.Mocked<ReplayContainer>, |
159 |
| - expected?: SentReplayExpected | { sample: SentReplayExpected; inverse: boolean }, |
160 |
| -) { |
161 |
| - const { calls } = (getCurrentHub().getClient()?.getTransport()?.send as MockTransport).mock; |
162 |
| - |
163 |
| - let result: CheckCallForSentReplayResult; |
164 |
| - |
165 |
| - const expectedKeysLength = expected |
166 |
| - ? ('sample' in expected ? Object.keys(expected.sample) : Object.keys(expected)).length |
167 |
| - : 0; |
168 |
| - |
169 |
| - const replayCalls = getReplayCalls(calls); |
170 |
| - |
171 |
| - for (const currentCall of replayCalls) { |
172 |
| - result = checkCallForSentReplay.call(this, currentCall[0], expected); |
173 |
| - if (result.pass) { |
174 |
| - break; |
175 |
| - } |
176 |
| - |
177 |
| - // stop on the first call where any of the expected obj passes |
178 |
| - if (result.results.length < expectedKeysLength) { |
179 |
| - break; |
180 |
| - } |
181 |
| - } |
182 |
| - |
183 |
| - // @ts-ignore use before assigned |
184 |
| - const { results, call, pass } = result; |
185 |
| - |
186 |
| - const options = { |
187 |
| - isNot: this.isNot, |
188 |
| - promise: this.promise, |
189 |
| - }; |
190 |
| - |
191 |
| - return { |
192 |
| - pass, |
193 |
| - message: () => |
194 |
| - !call |
195 |
| - ? pass |
196 |
| - ? 'Expected Replay to not have been sent, but a request was attempted' |
197 |
| - : 'Expected Replay to have been sent, but a request was not attempted' |
198 |
| - : `${this.utils.matcherHint('toHaveSentReplay', undefined, undefined, options)}\n\n${results |
199 |
| - .map(({ key, expectedVal, actualVal }: Result) => |
200 |
| - this.utils.printDiffOrStringify( |
201 |
| - expectedVal, |
202 |
| - actualVal, |
203 |
| - `Expected (key: ${key})`, |
204 |
| - `Received (key: ${key})`, |
205 |
| - ), |
206 |
| - ) |
207 |
| - .join('\n')}`, |
208 |
| - }; |
209 |
| -}; |
210 |
| - |
211 |
| -/** |
212 |
| - * Checks the last call to `fetch` and ensures a replay was uploaded by |
213 |
| - * checking the `fetch()` request's body. |
214 |
| - */ |
215 |
| -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type |
216 |
| -const toHaveLastSentReplay = function ( |
217 |
| - _received: jest.Mocked<ReplayContainer>, |
218 |
| - expected?: SentReplayExpected | { sample: SentReplayExpected; inverse: boolean }, |
219 |
| -) { |
220 |
| - const { calls } = (getCurrentHub().getClient()?.getTransport()?.send as MockTransport).mock; |
221 |
| - const replayCalls = getReplayCalls(calls); |
222 |
| - |
223 |
| - const lastCall = replayCalls[calls.length - 1]?.[0]; |
224 |
| - |
225 |
| - const { results, call, pass } = checkCallForSentReplay.call(this, lastCall, expected); |
226 |
| - |
227 |
| - const options = { |
228 |
| - isNot: this.isNot, |
229 |
| - promise: this.promise, |
230 |
| - }; |
231 |
| - |
232 |
| - return { |
233 |
| - pass, |
234 |
| - message: () => |
235 |
| - !call |
236 |
| - ? pass |
237 |
| - ? 'Expected Replay to not have been sent, but a request was attempted' |
238 |
| - : 'Expected Replay to have last been sent, but a request was not attempted' |
239 |
| - : `${this.utils.matcherHint('toHaveSentReplay', undefined, undefined, options)}\n\n${results |
240 |
| - .map(({ key, expectedVal, actualVal }: Result) => |
241 |
| - this.utils.printDiffOrStringify( |
242 |
| - expectedVal, |
243 |
| - actualVal, |
244 |
| - `Expected (key: ${key})`, |
245 |
| - `Received (key: ${key})`, |
246 |
| - ), |
247 |
| - ) |
248 |
| - .join('\n')}`, |
249 |
| - }; |
250 |
| -}; |
251 |
| - |
252 |
| -expect.extend({ |
253 |
| - toHaveSameSession, |
254 |
| - toHaveSentReplay, |
255 |
| - toHaveLastSentReplay, |
256 |
| -}); |
257 |
| - |
258 |
| -declare global { |
259 |
| - // eslint-disable-next-line @typescript-eslint/no-namespace |
260 |
| - namespace jest { |
261 |
| - interface AsymmetricMatchers { |
262 |
| - toHaveSentReplay(expected?: SentReplayExpected): void; |
263 |
| - toHaveLastSentReplay(expected?: SentReplayExpected): void; |
264 |
| - toHaveSameSession(expected: undefined | Session): void; |
265 |
| - } |
266 |
| - interface Matchers<R> { |
267 |
| - toHaveSentReplay(expected?: SentReplayExpected): R; |
268 |
| - toHaveLastSentReplay(expected?: SentReplayExpected): R; |
269 |
| - toHaveSameSession(expected: undefined | Session): R; |
270 |
| - } |
271 |
| - } |
272 |
| -} |
0 commit comments