Skip to content

Commit 62c32be

Browse files
committed
Add snap_startTrace and snap_endTrace support to snaps-jest
1 parent 27c8e14 commit 62c32be

File tree

18 files changed

+612
-8
lines changed

18 files changed

+612
-8
lines changed

packages/snaps-jest/src/global.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,21 @@ interface SnapsMatchers {
107107
* });
108108
*/
109109
toTrackEvent(event?: unknown): void;
110+
111+
/**
112+
* Assert that the Snap started and ended a trace with the expected
113+
* parameters. This is equivalent to calling
114+
* `expect(response.tracked.traces).toContainEqual(span)`.
115+
*
116+
* @param trace - The expected trace parameters.
117+
* @throws If the snap did not end a trace with the expected parameters.
118+
* @example
119+
* const response = await request({ method: 'foo' });
120+
* expect(response).toTrace({
121+
* name: 'My Trace',
122+
* });
123+
*/
124+
toTrace(trace?: unknown): void;
110125
}
111126

112127
// Extend the `expect` interface with the new matchers. This is used when

packages/snaps-jest/src/matchers.test.tsx

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
toRespondWith,
1414
toRespondWithError,
1515
toSendNotification,
16+
toTrace,
1617
toTrackError,
1718
toTrackEvent,
1819
} from './matchers';
@@ -29,6 +30,7 @@ expect.extend({
2930
toRender,
3031
toTrackError,
3132
toTrackEvent,
33+
toTrace,
3234
});
3335

3436
describe('toRespondWith', () => {
@@ -949,3 +951,166 @@ describe('toTrackEvent', () => {
949951
});
950952
});
951953
});
954+
955+
describe('toTrace', () => {
956+
it('passes when the trace is correct', () => {
957+
expect(
958+
getMockResponse({
959+
tracked: {
960+
traces: [
961+
{
962+
id: '1',
963+
name: 'foo',
964+
parentContext: { bar: 'baz' },
965+
startTime: 1234567890,
966+
data: { qux: 'quux' },
967+
tags: { corge: 'grault' },
968+
},
969+
],
970+
},
971+
}),
972+
).toTrace({
973+
id: '1',
974+
name: 'foo',
975+
parentContext: { bar: 'baz' },
976+
startTime: 1234567890,
977+
data: { qux: 'quux' },
978+
tags: { corge: 'grault' },
979+
});
980+
});
981+
982+
it('passes when the partial trace is correct', () => {
983+
expect(
984+
getMockResponse({
985+
tracked: {
986+
traces: [
987+
{
988+
id: '1',
989+
name: 'foo',
990+
parentContext: { bar: 'baz' },
991+
startTime: 1234567890,
992+
},
993+
],
994+
},
995+
}),
996+
).toTrace({
997+
id: '1',
998+
name: 'foo',
999+
parentContext: { bar: 'baz' },
1000+
startTime: 1234567890,
1001+
});
1002+
});
1003+
1004+
it('passes when any trace is tracked', () => {
1005+
expect(
1006+
getMockResponse({
1007+
tracked: {
1008+
traces: [
1009+
{
1010+
id: '1',
1011+
name: 'foo',
1012+
parentContext: { bar: 'baz' },
1013+
startTime: 1234567890,
1014+
data: { qux: 'quux' },
1015+
tags: { corge: 'grault' },
1016+
},
1017+
],
1018+
},
1019+
}),
1020+
).toTrace();
1021+
});
1022+
1023+
it('fails when the trace is incorrect', () => {
1024+
expect(() =>
1025+
expect(
1026+
getMockResponse({
1027+
tracked: {
1028+
traces: [
1029+
{
1030+
id: '1',
1031+
name: 'foo',
1032+
parentContext: { bar: 'baz' },
1033+
startTime: 1234567890,
1034+
data: { qux: 'quux' },
1035+
tags: { corge: 'grault' },
1036+
},
1037+
],
1038+
},
1039+
}),
1040+
).toTrace({
1041+
id: '2',
1042+
name: 'foo',
1043+
parentContext: { bar: 'baz' },
1044+
startTime: 1234567890,
1045+
data: { qux: 'quux' },
1046+
tags: { corge: 'grault' },
1047+
}),
1048+
).toThrow('Received');
1049+
});
1050+
1051+
describe('not', () => {
1052+
it('passes when the trace does not match', () => {
1053+
expect(
1054+
getMockResponse({
1055+
tracked: {
1056+
traces: [
1057+
{
1058+
id: '1',
1059+
name: 'foo',
1060+
parentContext: { bar: 'baz' },
1061+
startTime: 1234567890,
1062+
data: { qux: 'quux' },
1063+
tags: { corge: 'grault' },
1064+
},
1065+
],
1066+
},
1067+
}),
1068+
).not.toTrace({
1069+
id: '2',
1070+
name: 'foo',
1071+
parentContext: { bar: 'baz' },
1072+
startTime: 1234567890,
1073+
data: { qux: 'quux' },
1074+
tags: { corge: 'grault' },
1075+
});
1076+
});
1077+
1078+
it('passes when there are no traces', () => {
1079+
expect(
1080+
getMockResponse({
1081+
tracked: {
1082+
traces: [],
1083+
},
1084+
}),
1085+
).not.toTrace();
1086+
});
1087+
1088+
it('fails when the trace matches', () => {
1089+
expect(() =>
1090+
expect(
1091+
getMockResponse({
1092+
tracked: {
1093+
traces: [
1094+
{
1095+
id: '1',
1096+
name: 'foo',
1097+
parentContext: { bar: 'baz' },
1098+
startTime: 1234567890,
1099+
data: { qux: 'quux' },
1100+
tags: { corge: 'grault' },
1101+
},
1102+
],
1103+
},
1104+
}),
1105+
).not.toTrace({
1106+
id: '1',
1107+
name: 'foo',
1108+
parentContext: { bar: 'baz' },
1109+
startTime: 1234567890,
1110+
data: { qux: 'quux' },
1111+
tags: { corge: 'grault' },
1112+
}),
1113+
).toThrow('Expected not to trace with data');
1114+
});
1115+
});
1116+
});

packages/snaps-jest/src/matchers.ts

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type {
99
Component,
1010
NotificationType,
1111
TrackableError,
12+
TraceRequest,
1213
} from '@metamask/snaps-sdk';
1314
import type { JSXElement, SnapNode } from '@metamask/snaps-sdk/jsx';
1415
import { isJSXElementUnsafe, JSXElementStruct } from '@metamask/snaps-sdk/jsx';
@@ -376,7 +377,8 @@ export const toTrackError: MatcherFunction<
376377

377378
const errorValidator = (error: SnapResponse['tracked']['errors'][number]) => {
378379
if (!errorData) {
379-
// If no error data is provided, we just check for the existence of an error.
380+
// If no error data is provided, we just check for the existence of an
381+
// error.
380382
return true;
381383
}
382384

@@ -411,7 +413,8 @@ export const toTrackEvent: MatcherFunction<[eventData?: Json | undefined]> =
411413
event: SnapResponse['tracked']['events'][number],
412414
) => {
413415
if (!eventData) {
414-
// If no event data is provided, we just check for the existence of an event.
416+
// If no event data is provided, we just check for the existence of an
417+
// event.
415418
return true;
416419
}
417420

@@ -438,11 +441,48 @@ export const toTrackEvent: MatcherFunction<[eventData?: Json | undefined]> =
438441
return { message, pass };
439442
};
440443

444+
export const toTrace: MatcherFunction<[traceData?: TraceRequest]> = function (
445+
actual,
446+
traceData,
447+
) {
448+
assertActualIsSnapResponse(actual, 'toTrace');
449+
450+
const traceValidator = (trace: SnapResponse['tracked']['traces'][number]) => {
451+
if (!traceData) {
452+
// If no trace data is provided, we just check for the existence of a
453+
// trace.
454+
return true;
455+
}
456+
457+
return this.equals(trace, traceData);
458+
};
459+
460+
const { traces } = actual.tracked;
461+
const pass = traces.some(traceValidator);
462+
463+
const message = pass
464+
? () =>
465+
`${this.utils.matcherHint('.not.toTrace')}\n\n` +
466+
`Expected not to trace with data: ${this.utils.printExpected(
467+
traceData,
468+
)}\n` +
469+
`Received traces: ${this.utils.printReceived(traces)}`
470+
: () =>
471+
`${this.utils.matcherHint('.toTrace')}\n\n` +
472+
`Expected to trace with data: ${this.utils.printExpected(
473+
traceData,
474+
)}\n` +
475+
`Received traces: ${this.utils.printReceived(traces)}`;
476+
477+
return { message, pass };
478+
};
479+
441480
expect.extend({
442481
toRespondWith,
443482
toRespondWithError,
444483
toSendNotification,
445484
toRender,
446485
toTrackError,
447486
toTrackEvent,
487+
toTrace,
448488
});

packages/snaps-jest/src/test-utils/response.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,11 @@ export function getMockResponse({
2525
tracked = {
2626
errors: [],
2727
events: [],
28+
traces: [],
2829
},
2930
getInterface,
3031
}: Omit<Partial<SnapResponseWithInterface>, 'tracked'> & {
31-
tracked?: {
32-
errors?: SnapResponse['tracked']['errors'];
33-
events?: SnapResponse['tracked']['events'];
34-
};
32+
tracked?: Partial<SnapResponse['tracked']>;
3533
}): SnapResponse {
3634
return {
3735
id,
@@ -40,6 +38,7 @@ export function getMockResponse({
4038
tracked: {
4139
errors: [],
4240
events: [],
41+
traces: [],
4342
...tracked,
4443
},
4544
...(getInterface ? { getInterface } : {}),
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { getEndTraceImplementation } from './end-trace';
2+
import { createStore, startTrace } from '../../store';
3+
import { getMockOptions } from '../../test-utils';
4+
5+
describe('getEndTraceImplementation', () => {
6+
it('returns the implementation of the `endTrace` hook', async () => {
7+
const { store, runSaga } = createStore(getMockOptions());
8+
store.dispatch(
9+
startTrace({
10+
name: 'Test Trace',
11+
id: 'test-trace-id',
12+
}),
13+
);
14+
15+
const fn = getEndTraceImplementation(runSaga);
16+
17+
expect(
18+
fn({
19+
name: 'Test Trace',
20+
id: 'test-trace-id',
21+
}),
22+
).toBeNull();
23+
24+
expect(store.getState().trackables.pendingTraces).toHaveLength(0);
25+
expect(store.getState().trackables.traces).toHaveLength(1);
26+
expect(store.getState().trackables.traces[0]).toStrictEqual({
27+
name: 'Test Trace',
28+
id: 'test-trace-id',
29+
});
30+
});
31+
});
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import type { EndTraceParams } from '@metamask/snaps-sdk';
2+
import type { SagaIterator } from 'redux-saga';
3+
import { put } from 'redux-saga/effects';
4+
5+
import type { RunSagaFunction } from '../../store';
6+
import { endTrace } from '../../store';
7+
8+
/**
9+
* End a performance trace.
10+
*
11+
* @param event - The performance trace to end.
12+
* @returns `null`.
13+
* @yields Adds the event to the store.
14+
*/
15+
function* endTraceImplementation(event: EndTraceParams): SagaIterator {
16+
yield put(endTrace(event));
17+
return null;
18+
}
19+
20+
/**
21+
* Get a method that can be used to end a performance trace.
22+
*
23+
* @param runSaga - A function to run a saga outside the usual Redux flow.
24+
* @returns A method that can be used to end a performance trace.
25+
*/
26+
export function getEndTraceImplementation(runSaga: RunSagaFunction) {
27+
return (...args: Parameters<typeof endTraceImplementation>) => {
28+
return runSaga(endTraceImplementation, ...args).result();
29+
};
30+
}

packages/snaps-simulation/src/methods/hooks/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export * from './end-trace';
12
export * from './get-entropy-sources';
23
export * from './get-mnemonic';
34
export * from './get-preferences';
@@ -6,6 +7,7 @@ export * from './interface';
67
export * from './notifications';
78
export * from './permitted';
89
export * from './request-user-approval';
10+
export * from './start-trace';
911
export * from './state';
1012
export * from './track-error';
1113
export * from './track-event';

0 commit comments

Comments
 (0)