Skip to content

Commit dcb10f7

Browse files
committed
feat(replay): Add toHaveLastSentReplay jest matcher
Really we renamed the previous `toHaveSentReplay` -> `toHaveLastSentReplay` and added `toHaveSentReplay` to match all calls to transport.
1 parent d9d8429 commit dcb10f7

File tree

5 files changed

+162
-98
lines changed

5 files changed

+162
-98
lines changed

packages/replay/jest.setup.ts

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

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

@@ -71,20 +77,13 @@ const toHaveSameSession = function (received: jest.Mocked<ReplayContainer>, expe
7177
};
7278
};
7379

74-
/**
75-
* Checks the last call to `fetch` and ensures a replay was uploaded by
76-
* checking the `fetch()` request's body.
77-
*/
78-
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
79-
const toHaveSentReplay = function (
80-
_received: jest.Mocked<ReplayContainer>,
81-
expected?: SentReplayExpected | { sample: SentReplayExpected; inverse: boolean },
82-
) {
83-
const { calls } = (getCurrentHub().getClient()?.getTransport()?.send as MockTransport).mock;
84-
const lastCall = calls[calls.length - 1]?.[0];
80+
type Result = {passed: boolean, key: string, expectedVal:SentReplayExpected[keyof SentReplayExpected], actualVal: SentReplayExpected[keyof SentReplayExpected]};
81+
type Call = [EnvelopeHeader, [[ReplayEventHeader|undefined, ReplayEventPayload|undefined], [RecordingHeader|undefined, RecordingPayloadHeader|undefined]]];
82+
type CheckCallForSentReplayResult = {pass: boolean, call: Call|undefined, results: Result[]}
8583

86-
const envelopeHeader = lastCall?.[0];
87-
const envelopeItems = lastCall?.[1] || [[], []];
84+
function checkCallForSentReplay(call: Call|undefined, expected?: SentReplayExpected | { sample: SentReplayExpected; inverse: boolean }): CheckCallForSentReplayResult {
85+
const envelopeHeader = call?.[0];
86+
const envelopeItems = call?.[1] || [[], []];
8887
const [[replayEventHeader, replayEventPayload], [recordingHeader, recordingPayload] = []] = envelopeItems;
8988

9089
// @ts-ignore recordingPayload is always a string in our tests
@@ -117,53 +116,123 @@ const toHaveSentReplay = function (
117116
.map(key => {
118117
const actualVal = actualObj[key as keyof SentReplayExpected];
119118
const expectedVal = expectedObj[key as keyof SentReplayExpected];
120-
const matches = !expectedVal || this.equals(actualVal, expectedVal);
119+
const passed = !expectedVal || this.equals(actualVal, expectedVal);
121120

122-
return [matches, key, expectedVal, actualVal];
121+
return {passed, key, expectedVal, actualVal};
123122
})
124-
.filter(([passed]) => !passed)
123+
.filter(({passed}) => !passed)
125124
: [];
126125

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

129160
const options = {
130161
isNot: this.isNot,
131162
promise: this.promise,
132163
};
133164

134-
const allPass = payloadPassed;
135-
136165
return {
137-
pass: allPass,
166+
pass,
138167
message: () =>
139-
!lastCall
140-
? allPass
168+
!call
169+
? pass
141170
? 'Expected Replay to not have been sent, but a request was attempted'
142171
: 'Expected Replay to have been sent, but a request was not attempted'
143172
: `${this.utils.matcherHint('toHaveSentReplay', undefined, undefined, options)}\n\n${results
144173
.map(
145-
([, key, expected, actual]) =>
146-
`Expected (key: ${key}): ${payloadPassed ? 'not ' : ''}${this.utils.printExpected(expected)}\n` +
147-
`Received (key: ${key}): ${this.utils.printReceived(actual)}`,
174+
({key, expectedVal, actualVal}: Result) =>
175+
`Expected (key: ${key}): ${pass ? 'not ' : ''}${this.utils.printExpected(expectedVal)}\n` +
176+
`Received (key: ${key}): ${this.utils.printReceived(actualVal)}`,
177+
)
178+
.join('\n')}`,
179+
};
180+
};
181+
182+
/**
183+
* Checks the last call to `fetch` and ensures a replay was uploaded by
184+
* checking the `fetch()` request's body.
185+
*/
186+
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
187+
const toHaveLastSentReplay = function (
188+
_received: jest.Mocked<ReplayContainer>,
189+
expected?: SentReplayExpected | { sample: SentReplayExpected; inverse: boolean },
190+
) {
191+
const { calls } = (getCurrentHub().getClient()?.getTransport()?.send as MockTransport).mock;
192+
const lastCall = calls[calls.length - 1]?.[0];
193+
194+
const {results, call, pass} = checkCallForSentReplay.call(this, lastCall, expected);
195+
196+
const options = {
197+
isNot: this.isNot,
198+
promise: this.promise,
199+
};
200+
201+
return {
202+
pass,
203+
message: () =>
204+
!call
205+
? pass
206+
? 'Expected Replay to not have been sent, but a request was attempted'
207+
: 'Expected Replay to have last been sent, but a request was not attempted'
208+
: `${this.utils.matcherHint('toHaveSentReplay', undefined, undefined, options)}\n\n${results
209+
.map(
210+
({key, expectedVal, actualVal}: Result) =>
211+
`Expected (key: ${key}): ${pass ? 'not ' : ''}${this.utils.printExpected(expectedVal)}\n` +
212+
`Received (key: ${key}): ${this.utils.printReceived(actualVal)}`,
148213
)
149214
.join('\n')}`,
150215
};
151216
};
152217

218+
153219
expect.extend({
154220
toHaveSameSession,
155221
toHaveSentReplay,
222+
toHaveLastSentReplay,
156223
});
157224

158225
declare global {
159226
// eslint-disable-next-line @typescript-eslint/no-namespace
160227
namespace jest {
161228
interface AsymmetricMatchers {
162229
toHaveSentReplay(expected?: SentReplayExpected): void;
230+
toHaveLastSentReplay(expected?: SentReplayExpected): void;
163231
toHaveSameSession(expected: undefined | Session): void;
164232
}
165233
interface Matchers<R> {
166234
toHaveSentReplay(expected?: SentReplayExpected): R;
235+
toHaveLastSentReplay(expected?: SentReplayExpected): R;
167236
toHaveSameSession(expected: undefined | Session): R;
168237
}
169238
}

0 commit comments

Comments
 (0)