/
fixtures.test.ts
194 lines (174 loc) · 5.51 KB
/
fixtures.test.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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
import fs from 'fs';
import glob = require('glob');
import makeDir from 'make-dir';
import path from 'path';
import type { AnalyzeOptions } from './test-utils';
import { parseAndAnalyze } from './test-utils';
// Assign a segment set to this variable to limit the test to only this segment
// This is super helpful if you need to debug why a specific fixture isn't producing the correct output
// eg. ['type-declaration', 'signatures', 'method-generic'] will only test /type-declaration/signatures/method-generic.ts
// prettier-ignore
const ONLY = [].join(path.sep);
const FIXTURES_DIR = path.resolve(__dirname, 'fixtures');
const fixtures = glob
.sync('**/*.{js,ts,jsx,tsx}', {
cwd: FIXTURES_DIR,
absolute: true,
ignore: ['fixtures.test.ts'],
})
.map(absolute => {
const relative = path.relative(FIXTURES_DIR, absolute);
const { name, dir, ext } = path.parse(relative);
const segments = dir.split(path.sep);
const snapshotPath = path.join(FIXTURES_DIR, dir);
return {
absolute,
name,
ext,
segments,
snapshotPath,
snapshotFile: path.join(snapshotPath, `${name}${ext}.shot`),
};
});
const FOUR_SLASH = /^\/\/\/\/[ ]+@(\w+)[ ]*=[ ]*(.+)$/;
const QUOTED_STRING = /^["'](.+?)['"]$/;
type ALLOWED_VALUE = ['boolean' | 'number' | 'string', Set<unknown>?];
const ALLOWED_OPTIONS: Map<string, ALLOWED_VALUE> = new Map<
keyof AnalyzeOptions,
ALLOWED_VALUE
>([
['globalReturn', ['boolean']],
['impliedStrict', ['boolean']],
['jsxPragma', ['string']],
['jsxFragmentName', ['string']],
['sourceType', ['string', new Set(['module', 'script'])]],
]);
function nestDescribe(
fixture: (typeof fixtures)[number],
segments = fixture.segments,
): void {
if (segments.length > 0) {
describe(segments[0], () => {
nestDescribe(fixture, segments.slice(1));
});
} else {
const test = (): void => {
const contents = fs.readFileSync(fixture.absolute, 'utf8');
const lines = contents.split('\n');
const options: Record<string, unknown> = {
lib: [],
};
/*
* What's all this!?
*
* To help with configuring individual tests, each test may use a four-slash comment to configure the scope manager
* This is just a rudimentary "parser" for said comments.
*/
for (const line of lines) {
if (!line.startsWith('////')) {
continue;
}
const match = FOUR_SLASH.exec(line);
if (!match) {
throw new Error(`Four-slash did not match expected format: ${line}`);
}
const [, key, rawValue] = match;
const type = ALLOWED_OPTIONS.get(key);
if (!type) {
throw new Error(`Unknown option ${key}`);
}
let value: unknown = rawValue;
switch (type[0]) {
case 'string': {
const strmatch = QUOTED_STRING.exec(rawValue);
if (strmatch) {
value = strmatch[1];
}
break;
}
case 'number': {
const parsed = parseFloat(rawValue);
if (isNaN(parsed)) {
throw new Error(
`Expected a number for ${key}, but got ${rawValue}`,
);
}
value = parsed;
break;
}
case 'boolean': {
if (rawValue === 'true') {
value = true;
} else if (rawValue === 'false') {
value = false;
} else {
throw new Error(
`Expected a boolean for ${key}, but got ${rawValue}`,
);
}
break;
}
}
if (type[1] && !type[1].has(value)) {
throw new Error(
`Expected value for ${key} to be one of (${Array.from(type[1]).join(
' | ',
)}), but got ${value as string}`,
);
}
if (value === 'true') {
options[key] = true;
} else if (value === 'false') {
options[key] = false;
} else {
options[key] = value;
}
}
try {
makeDir.sync(fixture.snapshotPath);
} catch (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
e: any
) {
if ('code' in e && e.code === 'EEXIST') {
// already exists - ignored
} else {
throw e;
}
}
try {
const { scopeManager } = parseAndAnalyze(contents, options, {
jsx: fixture.ext.endsWith('x'),
});
expect(scopeManager).toMatchSpecificSnapshot(fixture.snapshotFile);
} catch (e) {
expect(e).toMatchSpecificSnapshot(fixture.snapshotFile);
}
};
if ([...fixture.segments, fixture.name].join(path.sep) === ONLY) {
// eslint-disable-next-line jest/no-focused-tests
it.only(fixture.name, test);
} else {
it(fixture.name, test);
}
}
}
fixtures.forEach(f => nestDescribe(f));
if (ONLY === '') {
// ensure that the snapshots are cleaned up, because jest-specific-snapshot won't do this check
const snapshots = glob.sync(`${FIXTURES_DIR}/**/*.shot`).map(absolute => {
const relative = path.relative(FIXTURES_DIR, absolute);
const { name, dir } = path.parse(relative);
return {
relative,
fixturePath: path.join(FIXTURES_DIR, dir, name),
};
});
describe('ast snapshots should have an associated test', () => {
for (const snap of snapshots) {
it(snap.relative, () => {
expect(fs.existsSync(snap.fixturePath)).toBeTruthy();
});
}
});
}