/
test-utils.ts
151 lines (135 loc) · 4.09 KB
/
test-utils.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
import * as parser from '../src';
import { TSESTreeOptions } from '../src/parser-options';
export function parseCodeAndGenerateServices(
code: string,
config: TSESTreeOptions,
): parser.ParseAndGenerateServicesResult<parser.TSESTreeOptions> {
return parser.parseAndGenerateServices(code, config);
}
/**
* Returns a function which can be used as the callback of a Jest test() block,
* and which performs an assertion on the snapshot for the given code and config.
* @param code The source code to parse
* @param config the parser configuration
* @param generateServices Flag determining whether to generate ast maps and program or not
* @returns callback for Jest it() block
*/
export function createSnapshotTestBlock(
code: string,
config: TSESTreeOptions,
generateServices?: true,
): jest.ProvidesCallback {
/**
* @returns the AST object
*/
function parse(): parser.TSESTree.Program {
const ast = generateServices
? parser.parseAndGenerateServices(code, config).ast
: parser.parse(code, config);
return deeplyCopy(ast);
}
return (): void => {
try {
const result = parse();
expect(result).toMatchSnapshot();
} catch (e) {
/**
* If we are deliberately throwing because of encountering an unknown
* AST_NODE_TYPE, we rethrow to cause the test to fail
*/
if (e.message.match('Unknown AST_NODE_TYPE')) {
throw new Error(e);
}
expect(parse).toThrowErrorMatchingSnapshot();
}
};
}
export function formatSnapshotName(
filename: string,
fixturesDir: string,
fileExtension = '.js',
): string {
return `fixtures/${filename
.replace(fixturesDir + '/', '')
.replace(fileExtension, '')}`;
}
/**
* Check if file extension is one used for jsx
* @param fileType
*/
export function isJSXFileType(fileType: string): boolean {
if (fileType.startsWith('.')) {
fileType = fileType.slice(1);
}
return fileType === 'js' || fileType === 'jsx' || fileType === 'tsx';
}
/**
* Returns a raw copy of the typescript AST
* @param ast the AST object
* @returns copy of the AST object
*/
export function deeplyCopy<T>(ast: T): T {
return omitDeep(ast) as T;
}
type UnknownObject = Record<string, unknown>;
function isObjectLike(value: unknown | null): value is UnknownObject {
return (
typeof value === 'object' && !(value instanceof RegExp) && value !== null
);
}
/**
* Removes the given keys from the given AST object recursively
* @param root A JavaScript object to remove keys from
* @param keysToOmit Names and predicate functions use to determine what keys to omit from the final object
* @param selectors advance ast modifications
* @returns formatted object
*/
export function omitDeep<T = UnknownObject>(
root: T,
keysToOmit: { key: string; predicate: (value: unknown) => boolean }[] = [],
selectors: Record<
string,
(node: UnknownObject, parent: UnknownObject | null) => void
> = {},
): UnknownObject {
function shouldOmit(keyName: string, val: unknown): boolean {
if (keysToOmit?.length) {
return keysToOmit.some(
keyConfig => keyConfig.key === keyName && keyConfig.predicate(val),
);
}
return false;
}
function visit(
oNode: UnknownObject,
parent: UnknownObject | null,
): UnknownObject {
if (!Array.isArray(oNode) && !isObjectLike(oNode)) {
return oNode;
}
const node = { ...oNode };
for (const prop in node) {
if (Object.prototype.hasOwnProperty.call(node, prop)) {
if (shouldOmit(prop, node[prop]) || typeof node[prop] === 'undefined') {
delete node[prop];
continue;
}
const child = node[prop];
if (Array.isArray(child)) {
const value = [];
for (const el of child) {
value.push(visit(el, node));
}
node[prop] = value;
} else if (isObjectLike(child)) {
node[prop] = visit(child, node);
}
}
}
if (typeof node.type === 'string' && node.type in selectors) {
selectors[node.type](node, parent);
}
return node;
}
return visit(root as UnknownObject, null);
}