Mirror of https://github.com/roostorg/coop
github.com/roostorg/coop
1// eslint-disable-next-line import/no-extraneous-dependencies
2import jestSnapshot from 'jest-snapshot';
3// eslint-disable-next-line import/no-extraneous-dependencies
4import jsonPath from 'jsonpath';
5import lodash from 'lodash';
6
7const { toMatchSnapshot } = jestSnapshot;
8const { set } = lodash;
9
10interface CustomMatchers<R = unknown> {
11 /**
12 * This assertion works like `toMatchSnapshot`, except that it makes it
13 * easier to define many asymmetric property matchers at once, to better
14 * support cases where many keys in the snapshot have dynamic values.
15 *
16 * For example, the data being snapshotted might be a list of newly-created
17 * objects, each of which has a dynamically-generated id. In that case, you
18 * could use this assertion to easily require that every id is a string:
19 *
20 * `toMatchDynamicSnapshot({ '$[*].id': expect.any(String) })`.
21 *
22 * In the above, the `$[*].id` key is a jsonpath expression that matches the
23 * id property of every object in the root list.
24 */
25 toMatchDynamicSnapshot(propertyMatchers: object, hint?: string): R;
26}
27
28declare global {
29 // eslint-disable-next-line @typescript-eslint/no-namespace
30 namespace jest {
31 // Normally, an empty interface in TS is pointless but, in this case, we're
32 // actually taking advantage of declaration merging (on Expect, Matchers,
33 // and InverseAsymmetricMatchers) to make those interfaces, which are defined
34 // elsewhere extend our CustomMatchers interface.
35 // eslint-disable-next-line @typescript-eslint/no-empty-object-type
36 interface Expect extends CustomMatchers {}
37 // eslint-disable-next-line @typescript-eslint/no-empty-object-type
38 interface Matchers<R> extends CustomMatchers<R> {}
39 // eslint-disable-next-line @typescript-eslint/no-empty-object-type
40 interface InverseAsymmetricMatchers extends CustomMatchers {}
41 }
42}
43
44expect.extend({
45 toMatchDynamicSnapshot(received, propertyMatchers: object, hint?: string) {
46 // Treat property matcher keys as jsonpath queries
47 // if they start with a $ and contain a dot.
48 const isJsonPath = (it: string) => it[0] === '$' && it.includes('.');
49 const generatedPropertyMatchers = { ...propertyMatchers };
50
51 Object.keys(propertyMatchers).forEach((k) => {
52 const key = k as keyof typeof generatedPropertyMatchers &
53 keyof typeof propertyMatchers;
54
55 if (isJsonPath(k)) {
56 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
57 delete generatedPropertyMatchers[key];
58 jsonPath.paths(received, k).forEach((path) => {
59 set(generatedPropertyMatchers, path.slice(1), propertyMatchers[key]);
60 });
61 }
62 });
63
64 return (toMatchSnapshot as any).call(
65 this,
66 received,
67 generatedPropertyMatchers,
68 hint,
69 );
70 },
71});