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