Skip to content

Commit 3b287b3

Browse files
committed
add custom jest matchers
1 parent 2c159b8 commit 3b287b3

File tree

9 files changed

+552
-0
lines changed

9 files changed

+552
-0
lines changed

jest-setup.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,7 @@ import { TextEncoder, TextDecoder } from 'util';
44

55
global.TextEncoder = TextEncoder;
66
global.TextDecoder = TextDecoder;
7+
8+
import { matchers } from './src/gcopypaste/public/test/matchers';
9+
10+
expect.extend(matchers);
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Observable } from 'rxjs';
2+
3+
import { toEmitValues } from './toEmitValues';
4+
import { toEmitValuesWith } from './toEmitValuesWith';
5+
import { ObservableMatchers } from './types';
6+
7+
export const matchers: ObservableMatchers<void, Observable<any>> = {
8+
toEmitValues,
9+
toEmitValuesWith,
10+
};
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import { interval, Observable, of, throwError } from 'rxjs';
2+
import { map, mergeMap, take } from 'rxjs/operators';
3+
4+
import { OBSERVABLE_TEST_TIMEOUT_IN_MS } from './types';
5+
6+
describe('toEmitValues matcher', () => {
7+
describe('failing tests', () => {
8+
describe('passing null in expect', () => {
9+
it('should fail', async () => {
10+
const observable = null as unknown as Observable<number>;
11+
12+
const rejects = expect(() => expect(observable).toEmitValues([1, 2, 3])).rejects;
13+
await rejects.toThrow();
14+
});
15+
});
16+
17+
describe('passing undefined in expect', () => {
18+
it('should fail', async () => {
19+
const observable = undefined as unknown as Observable<number>;
20+
21+
const rejects = expect(() => expect(observable).toEmitValues([1, 2, 3])).rejects;
22+
await rejects.toThrow();
23+
});
24+
});
25+
26+
describe('passing number instead of Observable in expect', () => {
27+
it('should fail', async () => {
28+
const observable = 1 as unknown as Observable<number>;
29+
30+
const rejects = expect(() => expect(observable).toEmitValues([1, 2, 3])).rejects;
31+
await rejects.toThrow();
32+
});
33+
});
34+
35+
describe('wrong number of emitted values', () => {
36+
it('should fail', async () => {
37+
const observable = interval(10).pipe(take(3));
38+
39+
const rejects = expect(() => expect(observable).toEmitValues([0, 1])).rejects;
40+
await rejects.toThrow();
41+
});
42+
});
43+
44+
describe('wrong emitted values', () => {
45+
it('should fail', async () => {
46+
const observable = interval(10).pipe(take(3));
47+
48+
const rejects = expect(() => expect(observable).toEmitValues([1, 2, 3])).rejects;
49+
await rejects.toThrow();
50+
});
51+
});
52+
53+
describe('wrong emitted value types', () => {
54+
it('should fail', async () => {
55+
const observable = interval(10).pipe(take(3)) as unknown as Observable<string>;
56+
57+
const rejects = expect(() => expect(observable).toEmitValues(['0', '1', '2'])).rejects;
58+
await rejects.toThrow();
59+
});
60+
});
61+
62+
describe(`observable that does not complete within ${OBSERVABLE_TEST_TIMEOUT_IN_MS}ms`, () => {
63+
it('should fail', async () => {
64+
const observable = interval(600);
65+
66+
const rejects = expect(() => expect(observable).toEmitValues([0])).rejects;
67+
await rejects.toThrow();
68+
});
69+
});
70+
});
71+
72+
describe('passing tests', () => {
73+
describe('correct emitted values', () => {
74+
it('should pass with correct message', async () => {
75+
const observable = interval(10).pipe(take(3));
76+
await expect(observable).toEmitValues([0, 1, 2]);
77+
});
78+
});
79+
80+
describe('using nested arrays', () => {
81+
it('should pass with correct message', async () => {
82+
const observable = interval(10).pipe(
83+
map((interval) => [{ text: interval.toString(), value: interval }]),
84+
take(3)
85+
);
86+
await expect(observable).toEmitValues([
87+
[{ text: '0', value: 0 }],
88+
[{ text: '1', value: 1 }],
89+
[{ text: '2', value: 2 }],
90+
]);
91+
});
92+
});
93+
94+
describe('using nested objects', () => {
95+
it('should pass with correct message', async () => {
96+
const observable = interval(10).pipe(
97+
map((interval) => ({ inner: { text: interval.toString(), value: interval } })),
98+
take(3)
99+
);
100+
await expect(observable).toEmitValues([
101+
{ inner: { text: '0', value: 0 } },
102+
{ inner: { text: '1', value: 1 } },
103+
{ inner: { text: '2', value: 2 } },
104+
]);
105+
});
106+
});
107+
108+
describe('correct emitted values with throw', () => {
109+
it('should pass with correct message', async () => {
110+
const observable = interval(10).pipe(
111+
map((interval) => {
112+
if (interval > 1) {
113+
throw 'an error';
114+
}
115+
116+
return interval;
117+
})
118+
) as unknown as Observable<string | number>;
119+
120+
await expect(observable).toEmitValues([0, 1, 'an error']);
121+
});
122+
});
123+
124+
describe('correct emitted values with throwError', () => {
125+
it('should pass with correct message', async () => {
126+
const observable = interval(10).pipe(
127+
mergeMap((interval) => {
128+
if (interval === 1) {
129+
return throwError('an error');
130+
}
131+
132+
return of(interval);
133+
})
134+
) as unknown as Observable<string | number>;
135+
136+
await expect(observable).toEmitValues([0, 'an error']);
137+
});
138+
});
139+
});
140+
});
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { matcherHint, printExpected, printReceived } from 'jest-matcher-utils';
2+
import { isEqual } from 'lodash';
3+
import { Observable, Subscription } from 'rxjs';
4+
5+
import { expectObservable, forceObservableCompletion } from './utils';
6+
7+
function passMessage(received: unknown[], expected: unknown[]) {
8+
return `${matcherHint('.not.toEmitValues')}
9+
10+
Expected observable to emit values:
11+
${printExpected(expected)}
12+
Received:
13+
${printReceived(received)}
14+
`;
15+
}
16+
17+
function failMessage(received: unknown[], expected: unknown[]) {
18+
return `${matcherHint('.toEmitValues')}
19+
20+
Expected observable to emit values:
21+
${printExpected(expected)}
22+
Received:
23+
${printReceived(received)}
24+
`;
25+
}
26+
27+
function tryExpectations(received: unknown[], expected: unknown[]): jest.CustomMatcherResult {
28+
try {
29+
if (received.length !== expected.length) {
30+
return {
31+
pass: false,
32+
message: () => failMessage(received, expected),
33+
};
34+
}
35+
36+
for (let index = 0; index < received.length; index++) {
37+
const left = received[index];
38+
const right = expected[index];
39+
40+
if (!isEqual(left, right)) {
41+
return {
42+
pass: false,
43+
message: () => failMessage(received, expected),
44+
};
45+
}
46+
}
47+
48+
return {
49+
pass: true,
50+
message: () => passMessage(received, expected),
51+
};
52+
} catch (err) {
53+
const message = err instanceof Error ? err.message : 'An unknown error occurred';
54+
return {
55+
pass: false,
56+
message: () => message,
57+
};
58+
}
59+
}
60+
61+
export function toEmitValues(received: Observable<unknown>, expected: unknown[]): Promise<jest.CustomMatcherResult> {
62+
const failsChecks = expectObservable(received);
63+
if (failsChecks) {
64+
return Promise.resolve(failsChecks);
65+
}
66+
67+
return new Promise((resolve) => {
68+
const receivedValues: unknown[] = [];
69+
const subscription = new Subscription();
70+
71+
subscription.add(
72+
received.subscribe({
73+
next: (value) => {
74+
receivedValues.push(value);
75+
},
76+
error: (err) => {
77+
receivedValues.push(err);
78+
subscription.unsubscribe();
79+
resolve(tryExpectations(receivedValues, expected));
80+
},
81+
complete: () => {
82+
subscription.unsubscribe();
83+
resolve(tryExpectations(receivedValues, expected));
84+
},
85+
})
86+
);
87+
88+
forceObservableCompletion(subscription, resolve);
89+
});
90+
}

0 commit comments

Comments
 (0)