1
-/**
2
- * Copyright (c) Meta Platforms, Inc. and affiliates.
3
- *
4
- * This source code is licensed under the MIT license found in the
5
- * LICENSE file in the root directory of this source tree.
6
- */
7
-
8
-"use strict";
9
-
10
-/* global expect,test */
11
-
12
-import fs from "fs";
13
-import glob from "glob";
14
-import path from "path";
15
-import {
16
- ExternalFunction,
17
- InstrumentForgetOptions,
18
- PluginOptions,
19
-} from "../../Babel/PluginOptions";
20
-
21
-const EXPECT_SUFFIX = ".expect.md";
22
-
23
-const PROJECT_ROOT = path.dirname(path.dirname(__dirname));
24
-
25
-expect.extend({
26
- accessSnapshotState(_anything, cb) {
27
- cb(this["snapshotState"]._updateSnapshot);
28
- return {
29
- pass: true,
30
- message: () => "",
31
- };
32
- },
33
- toHaveNoUnmatchedSnapshots(unmatchedSnapshots, fixturesPath) {
34
- return {
35
- pass: unmatchedSnapshots.length === 0,
36
- message: () => {
37
- const unmatchedSnapshotsText = unmatchedSnapshots
38
- .map((file: string) => path.join(fixturesPath, file))
39
- .join("\n * ");
40
- return (
41
- `Found ${EXPECT_SUFFIX} files without corresponding inputs:\n* ${unmatchedSnapshotsText}` +
42
- `\n\nRun 'npm test -- -u' to remove these extra ${EXPECT_SUFFIX} files`
43
- );
44
- },
45
- };
46
- },
47
-});
48
-
49
-type FixtureTestOptions = {
50
- debug: boolean;
51
- language: "flow" | "typescript";
52
-};
53
-export default function generateTestsFromFixtures(
54
- fixturesPath: string,
55
- transform: (
56
- input: string,
57
- file: any,
58
- options: FixtureTestOptions & PluginOptions
59
- ) => string
60
-) {
61
- let files: Array<string>;
62
- try {
63
- files = glob.sync("**/*.{js,md}", {
64
- cwd: fixturesPath,
65
- });
66
- } catch (e) {
67
- if (e.code === "ENOENT") {
68
- files = [];
69
- } else {
70
- throw e;
71
- }
72
- }
73
- const fixtures = matchInputOutputFixtures(files, fixturesPath);
74
-
75
- const relativeFixturesPath = path.relative(PROJECT_ROOT, fixturesPath);
76
- describe(relativeFixturesPath, () => {
77
- test("has a consistent extension for input fixtures", () => {
78
- const extensions = Array.from(
79
- new Set(
80
- Array.from(fixtures.values())
81
- .map((entry) =>
82
- entry.input != null ? path.extname(entry.input) : null
83
- )
84
- .filter(Boolean)
85
- )
86
- );
87
- expect(extensions).toEqual(extensions.slice(0, 1));
88
- });
89
-
90
- describe("fixtures", () => {
91
- for (const {
92
- basename,
93
- input: inputFile,
94
- output: outputFile,
95
- } of Array.from(fixtures.values())) {
96
- let testCommand = test;
97
- let input: string | null = null;
98
- let debug = false;
99
- let enableOnlyOnUseForgetDirective = false;
100
- let gating: ExternalFunction | null = null;
101
- let instrumentForget: InstrumentForgetOptions | null = null;
102
- let inlineUseMemo = true;
103
- let panicOnBailout = true;
104
- let memoizeJsxElements = true;
105
- let enableAssumeHooksFollowRulesOfReact = false;
106
- let enableTreatHooksAsFunctions = true;
107
-
108
- if (inputFile != null) {
109
- input = fs.readFileSync(inputFile, "utf8");
110
- const lines = input.split("\n");
111
- if (lines[0]!.indexOf("@only") !== -1) {
112
- testCommand = test.only;
113
- }
114
- if (lines[0]!.indexOf("@skip") !== -1) {
115
- testCommand = test.skip;
116
- }
117
- if (lines[0]!.indexOf("@debug") !== -1) {
118
- debug = true;
119
- }
120
- if (lines[0]!.indexOf("@forgetDirective") !== -1) {
121
- enableOnlyOnUseForgetDirective = true;
122
- }
123
- if (lines[0]!.indexOf("@gating") !== -1) {
124
- gating = {
125
- source: "ReactForgetFeatureFlag",
126
- importSpecifierName: "isForgetEnabled_Fixtures",
127
- };
128
- }
129
- if (lines[0]!.indexOf("@instrumentForget") !== -1) {
130
- instrumentForget = {
131
- gating: {
132
- source: "ReactInstrumentForgetFeatureFlag",
133
- importSpecifierName: "isInstrumentForgetEnabled_Fixtures",
134
- },
135
- instrumentFn: {
136
- source: "react-forget-runtime",
137
- importSpecifierName: "useRenderCounter",
138
- },
139
- };
140
- }
141
- if (lines[0]!.indexOf("@inlineUseMemo false") !== -1) {
142
- inlineUseMemo = false;
143
- }
144
- if (lines[0]!.indexOf("@panicOnBailout false") !== -1) {
145
- panicOnBailout = false;
146
- }
147
- if (lines[0]!.indexOf("@memoizeJsxElements false") !== -1) {
148
- memoizeJsxElements = false;
149
- }
150
- if (
151
- lines[0]!.indexOf("@enableAssumeHooksFollowRulesOfReact true") !==
152
- -1
153
- ) {
154
- enableAssumeHooksFollowRulesOfReact = true;
155
- }
156
- if (lines[0]!.indexOf("@enableTreatHooksAsFunctions false") !== -1) {
157
- enableTreatHooksAsFunctions = false;
158
- }
159
- }
160
-
161
- testCommand(basename, () => {
162
- let receivedOutput;
163
- if (input !== null) {
164
- receivedOutput = transform(input, basename, {
165
- environment: {
166
- inlineUseMemo,
167
- memoizeJsxElements,
168
- enableAssumeHooksFollowRulesOfReact,
169
- enableTreatHooksAsFunctions,
170
- },
171
- logger: null,
172
- debug,
173
- enableOnlyOnUseForgetDirective,
174
- gating,
175
- instrumentForget,
176
- language: parseLanguage(input),
177
- panicOnBailout,
178
- isDev: true,
179
- });
180
- } else {
181
- receivedOutput = "<<input deleted>>";
182
- }
183
-
184
- // Use a standard snapshot for the expected output so that the snapshot fails unless the
185
- // value matches
186
- expect(receivedOutput).toMatchSnapshot();
187
-
188
- // Determine whether the snapshot is in update mode or only creating snapshots for new inputs
189
- // to update the .expect file in parallel with updating the snapshot itself.
190
- const snapshotUpdateMode = determineSnapshotMode();
191
- if (outputFile != null) {
192
- const outputExists = fs.existsSync(outputFile);
193
- if (
194
- snapshotUpdateMode === "all" ||
195
- (snapshotUpdateMode === "new" && !outputExists)
196
- ) {
197
- if (inputFile != null) {
198
- fs.writeFileSync(outputFile, receivedOutput, "utf8");
199
- } else {
200
- fs.unlinkSync(outputFile);
201
- }
202
- } else {
203
- // As a sanity check, make sure that the current output matches the .expect file
204
- const actualOutput = fs.readFileSync(outputFile, "utf8");
205
- expect(receivedOutput).toEqual(actualOutput);
206
- }
207
- }
208
- });
209
- }
210
- });
211
- });
212
-}
213
-
214
-const FlowPragmas = [/\/\/\s@flow$/gm, /\*\s@flow$/gm];
215
-function parseLanguage(source: string): "flow" | "typescript" {
216
- let useFlow: boolean = false;
217
- for (const flowPragma of FlowPragmas) {
218
- useFlow ||= !!source.match(flowPragma);
219
- }
220
- return useFlow ? "flow" : "typescript";
221
-}
222
-
223
-function determineSnapshotMode() {
224
- // Determine which snapshot mode we're in: ignoring snapshots,
225
- // updating new files only, or updating all files
226
- let updateSnapshots = "none";
227
- // @ts-ignore
228
- expect(null).accessSnapshotState((_updateSnapshots) => {
229
- updateSnapshots = _updateSnapshots;
230
- });
231
- const updateSnapshotEnvVariable = process.env["UPDATE_SNAPSHOTS"];
232
- if (
233
- updateSnapshotEnvVariable === "1" ||
234
- updateSnapshotEnvVariable === "all"
235
- ) {
236
- console.log(
237
- "Updating all snapshots due to UPDATE_SNAPSHOTS environment variable being set"
238
- );
239
- updateSnapshots = "all";
240
- } else {
241
- // @ts-ignore
242
- expect(updateSnapshotEnvVariable).toEqual();
243
- }
244
- expect(updateSnapshots).toEqual(expect.stringMatching(/none|new|all/));
245
- return updateSnapshots;
246
-}
247
-
248
-function matchInputOutputFixtures(files: string[], fixturesPath: string) {
249
- const fixtures: Map<
250
- string,
251
- { basename: string; input: string | null; output: string | null }
252
- > = new Map();
253
- for (const file of files) {
254
- const isOutput = file.endsWith(EXPECT_SUFFIX);
255
- const basename = path.basename(
256
- file,
257
- isOutput ? EXPECT_SUFFIX : path.extname(file)
258
- );
259
- let entry = fixtures.get(basename);
260
- if (entry === undefined) {
261
- entry = { basename, input: null, output: null };
262
- fixtures.set(basename, entry);
263
- }
264
- const resolvedPath = path.format({
265
- dir: fixturesPath,
266
- name: file,
267
- });
268
- if (isOutput) {
269
- entry.output = resolvedPath;
270
- } else {
271
- if (entry.input !== null) {
272
- throw new Error(
273
- "Found multiple inputs with the basename '" +
274
- basename +
275
- "': " +
276
- entry.input +
277
- " and " +
278
- resolvedPath
279
- );
280
- }
281
- entry.input = resolvedPath;
282
- const outputName = `${basename}${EXPECT_SUFFIX}`;
283
- const outputFile = path.join(
284
- fixturesPath,
285
- path.dirname(file),
286
- outputName
287
- );
288
- entry.output = outputFile;
289
- }
290
- }
291
- return fixtures;
292
-}