Skip to content

Commit aafa7cb

Browse files
authored
feat(replay): Add toHaveLastSentReplay jest matcher (#6467)
Really we renamed the previous `toHaveSentReplay` -> `toHaveLastSentReplay` and added `toHaveSentReplay` to match all calls to transport.
1 parent d9c6887 commit aafa7cb

File tree

5 files changed

+173
-95
lines changed

5 files changed

+173
-95
lines changed

packages/replay/jest.setup.ts

Lines changed: 117 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -36,19 +36,25 @@ afterEach(() => {
3636
(client.getTransport()?.send as MockTransport).mockClear();
3737
});
3838

39-
type SentReplayExpected = {
40-
envelopeHeader?: {
41-
event_id: string;
42-
sent_at: string;
43-
sdk: {
44-
name: string;
45-
version?: string;
46-
};
39+
type EnvelopeHeader = {
40+
event_id: string;
41+
sent_at: string;
42+
sdk: {
43+
name: string;
44+
version?: string;
4745
};
48-
replayEventHeader?: { type: 'replay_event' };
49-
replayEventPayload?: Record<string, unknown>;
50-
recordingHeader?: { type: 'replay_recording'; length: number };
51-
recordingPayloadHeader?: Record<string, unknown>;
46+
};
47+
48+
type ReplayEventHeader = { type: 'replay_event' };
49+
type ReplayEventPayload = Record<string, unknown>;
50+
type RecordingHeader = { type: 'replay_recording'; length: number };
51+
type RecordingPayloadHeader = Record<string, unknown>;
52+
type SentReplayExpected = {
53+
envelopeHeader?: EnvelopeHeader;
54+
replayEventHeader?: ReplayEventHeader;
55+
replayEventPayload?: ReplayEventPayload;
56+
recordingHeader?: RecordingHeader;
57+
recordingPayloadHeader?: RecordingPayloadHeader;
5258
events?: string | Uint8Array;
5359
};
5460

@@ -70,20 +76,27 @@ const toHaveSameSession = function (received: jest.Mocked<ReplayContainer>, expe
7076
};
7177
};
7278

73-
/**
74-
* Checks the last call to `fetch` and ensures a replay was uploaded by
75-
* checking the `fetch()` request's body.
76-
*/
77-
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
78-
const toHaveSentReplay = function (
79-
_received: jest.Mocked<ReplayContainer>,
79+
type Result = {
80+
passed: boolean;
81+
key: string;
82+
expectedVal: SentReplayExpected[keyof SentReplayExpected];
83+
actualVal: SentReplayExpected[keyof SentReplayExpected];
84+
};
85+
type Call = [
86+
EnvelopeHeader,
87+
[
88+
[ReplayEventHeader | undefined, ReplayEventPayload | undefined],
89+
[RecordingHeader | undefined, RecordingPayloadHeader | undefined],
90+
],
91+
];
92+
type CheckCallForSentReplayResult = { pass: boolean; call: Call | undefined; results: Result[] };
93+
94+
function checkCallForSentReplay(
95+
call: Call | undefined,
8096
expected?: SentReplayExpected | { sample: SentReplayExpected; inverse: boolean },
81-
) {
82-
const { calls } = (getCurrentHub().getClient()?.getTransport()?.send as MockTransport).mock;
83-
const lastCall = calls[calls.length - 1]?.[0];
84-
85-
const envelopeHeader = lastCall?.[0];
86-
const envelopeItems = lastCall?.[1] || [[], []];
97+
): CheckCallForSentReplayResult {
98+
const envelopeHeader = call?.[0];
99+
const envelopeItems = call?.[1] || [[], []];
87100
const [[replayEventHeader, replayEventPayload], [recordingHeader, recordingPayload] = []] = envelopeItems;
88101

89102
// @ts-ignore recordingPayload is always a string in our tests
@@ -116,34 +129,98 @@ const toHaveSentReplay = function (
116129
.map(key => {
117130
const actualVal = actualObj[key as keyof SentReplayExpected];
118131
const expectedVal = expectedObj[key as keyof SentReplayExpected];
119-
const matches = !expectedVal || this.equals(actualVal, expectedVal);
132+
const passed = !expectedVal || this.equals(actualVal, expectedVal);
120133

121-
return [matches, key, expectedVal, actualVal];
134+
return { passed, key, expectedVal, actualVal };
122135
})
123-
.filter(([passed]) => !passed)
136+
.filter(({ passed }) => !passed)
124137
: [];
125138

126-
const payloadPassed = Boolean(lastCall && (!expected || results.length === 0));
139+
const pass = Boolean(call && (!expected || results.length === 0));
140+
141+
return {
142+
pass,
143+
call,
144+
results,
145+
};
146+
}
147+
148+
/**
149+
* Checks all calls to `fetch` and ensures a replay was uploaded by
150+
* checking the `fetch()` request's body.
151+
*/
152+
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
153+
const toHaveSentReplay = function (
154+
_received: jest.Mocked<ReplayContainer>,
155+
expected?: SentReplayExpected | { sample: SentReplayExpected; inverse: boolean },
156+
) {
157+
const { calls } = (getCurrentHub().getClient()?.getTransport()?.send as MockTransport).mock;
158+
159+
let result: CheckCallForSentReplayResult;
160+
161+
for (const currentCall of calls) {
162+
result = checkCallForSentReplay.call(this, currentCall[0], expected);
163+
if (result.pass) {
164+
break;
165+
}
166+
}
167+
168+
// @ts-ignore use before assigned
169+
const { results, call, pass } = result;
127170

128171
const options = {
129172
isNot: this.isNot,
130173
promise: this.promise,
131174
};
132175

133-
const allPass = payloadPassed;
134-
135176
return {
136-
pass: allPass,
177+
pass,
137178
message: () =>
138-
!lastCall
139-
? allPass
179+
!call
180+
? pass
140181
? 'Expected Replay to not have been sent, but a request was attempted'
141182
: 'Expected Replay to have been sent, but a request was not attempted'
142183
: `${this.utils.matcherHint('toHaveSentReplay', undefined, undefined, options)}\n\n${results
143184
.map(
144-
([, key, expected, actual]) =>
145-
`Expected (key: ${key}): ${payloadPassed ? 'not ' : ''}${this.utils.printExpected(expected)}\n` +
146-
`Received (key: ${key}): ${this.utils.printReceived(actual)}`,
185+
({ key, expectedVal, actualVal }: Result) =>
186+
`Expected (key: ${key}): ${pass ? 'not ' : ''}${this.utils.printExpected(expectedVal)}\n` +
187+
`Received (key: ${key}): ${this.utils.printReceived(actualVal)}`,
188+
)
189+
.join('\n')}`,
190+
};
191+
};
192+
193+
/**
194+
* Checks the last call to `fetch` and ensures a replay was uploaded by
195+
* checking the `fetch()` request's body.
196+
*/
197+
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
198+
const toHaveLastSentReplay = function (
199+
_received: jest.Mocked<ReplayContainer>,
200+
expected?: SentReplayExpected | { sample: SentReplayExpected; inverse: boolean },
201+
) {
202+
const { calls } = (getCurrentHub().getClient()?.getTransport()?.send as MockTransport).mock;
203+
const lastCall = calls[calls.length - 1]?.[0];
204+
205+
const { results, call, pass } = checkCallForSentReplay.call(this, lastCall, expected);
206+
207+
const options = {
208+
isNot: this.isNot,
209+
promise: this.promise,
210+
};
211+
212+
return {
213+
pass,
214+
message: () =>
215+
!call
216+
? pass
217+
? 'Expected Replay to not have been sent, but a request was attempted'
218+
: 'Expected Replay to have last been sent, but a request was not attempted'
219+
: `${this.utils.matcherHint('toHaveSentReplay', undefined, undefined, options)}\n\n${results
220+
.map(
221+
({ key, expectedVal, actualVal }: Result) =>
222+
`Expected (key: ${key}): ${pass ? 'not ' : ''}${this.utils.printExpected(expectedVal)}\n` +
223+
`Received (key: ${key}): ${this.utils.printReceived(actualVal)}`,
147224
)
148225
.join('\n')}`,
149226
};
@@ -152,17 +229,20 @@ const toHaveSentReplay = function (
152229
expect.extend({
153230
toHaveSameSession,
154231
toHaveSentReplay,
232+
toHaveLastSentReplay,
155233
});
156234

157235
declare global {
158236
// eslint-disable-next-line @typescript-eslint/no-namespace
159237
namespace jest {
160238
interface AsymmetricMatchers {
161239
toHaveSentReplay(expected?: SentReplayExpected): void;
240+
toHaveLastSentReplay(expected?: SentReplayExpected): void;
162241
toHaveSameSession(expected: undefined | Session): void;
163242
}
164243
interface Matchers<R> {
165244
toHaveSentReplay(expected?: SentReplayExpected): R;
245+
toHaveLastSentReplay(expected?: SentReplayExpected): R;
166246
toHaveSameSession(expected: undefined | Session): R;
167247
}
168248
}

0 commit comments

Comments
 (0)