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