mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
[tests] remove jest fixture tests
--- Remove jest fixture tests in favor of snap runner. Main reasons: - maintaining feature flags and compatible behavior required syncing all changes to 3 files (`generateTestsFromFixtures`, `compiler-test`, and `compiler-worker`) - jest snapshot test file causes rebase conflicts on most rebases - speed 🙌 $ time yarn test compiler-test (the extra test here is `has a consistent extension for input fixtures`) ``` Test Suites: 1 passed, 1 total Tests: 37 skipped, 480 passed, 517 total Snapshots: 479 passed, 479 total Time: 27.668 s Ran all test suites matching /compiler-test/i. ✨ Done in 43.18s. yarn test compiler-test 57.05s user 3.85s system 139% cpu 43.546 total ``` $ time yarn snap ``` 478 Tests, 478 Passed, 0 Failed ✨ Done in 13.12s. yarn snap 53.96s user 9.35s system 468% cpu 13.518 total ``` Jest and snap should have the same set of features: - report test failures via exit status (used by Git Actions) - watch mode - breakpoints + `debugger` statements - note that `--sync` is not required for this - skip `todo.` prefixed fixtures - fixtures in nested directories e.g. `rules-of-hooks/testname.js` - filter mode (via editing `testfilter.txt`) - filter + debug mode (1) edit `testfilter.txt` to filter out all but one test (2) add `@debug` pragma to the first line of the test testfilter.txt ```js // @only testfixture_basename1 testfixture_basename2 ```
This commit is contained in:
@@ -1,138 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
import path from "path";
|
||||
import { runReactForgetBabelPlugin } from "../Babel/RunReactForgetBabelPlugin";
|
||||
import { toggleLogging } from "../Utils/logger";
|
||||
import { Effect, ValueKind } from "../index";
|
||||
import generateTestsFromFixtures from "./test-utils/generateTestsFromFixtures";
|
||||
|
||||
type TestOutput = {
|
||||
js: string;
|
||||
};
|
||||
|
||||
function wrapWithTripleBackticks(s: string, ext?: string) {
|
||||
return `\`\`\`${ext ?? ""}
|
||||
${s}
|
||||
\`\`\``;
|
||||
}
|
||||
|
||||
describe("React Forget", () => {
|
||||
const originalConsoleError = console.error;
|
||||
generateTestsFromFixtures(
|
||||
path.join(__dirname, "fixtures", "compiler"),
|
||||
(input, file, options) => {
|
||||
const seenConsoleErrors: Array<string> = [];
|
||||
let items: Array<TestOutput> = [];
|
||||
let error: Error | null = null;
|
||||
if (options.debug) {
|
||||
toggleLogging(options.debug);
|
||||
}
|
||||
// Mock console.error so we can record it in test output
|
||||
console.error = jest.fn((...messages: Array<string>) => {
|
||||
seenConsoleErrors.push(...messages);
|
||||
});
|
||||
try {
|
||||
items.push({
|
||||
js: runReactForgetBabelPlugin(input, file, options.language, {
|
||||
enableOnlyOnUseForgetDirective:
|
||||
options.enableOnlyOnUseForgetDirective,
|
||||
environment: {
|
||||
memoizeJsxElements:
|
||||
options.environment?.memoizeJsxElements ?? true,
|
||||
customHooks: new Map([
|
||||
[
|
||||
"useFreeze",
|
||||
{
|
||||
name: "useFreeze",
|
||||
kind: "Custom",
|
||||
valueKind: ValueKind.Frozen,
|
||||
effectKind: Effect.Freeze,
|
||||
},
|
||||
],
|
||||
]),
|
||||
validateHooksUsage: true,
|
||||
inlineUseMemo: options.environment?.inlineUseMemo ?? false,
|
||||
enableFunctionCallSignatureOptimizations: true,
|
||||
enableAssumeHooksFollowRulesOfReact:
|
||||
options.environment?.enableAssumeHooksFollowRulesOfReact ??
|
||||
false,
|
||||
},
|
||||
logger: null,
|
||||
gating: options.gating,
|
||||
instrumentForget: options.instrumentForget,
|
||||
panicOnBailout: options.panicOnBailout,
|
||||
isDev: true,
|
||||
}).code,
|
||||
});
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
// Promote console errors so they can be recorded in fixture output
|
||||
for (const consoleError of seenConsoleErrors) {
|
||||
if (error != null) {
|
||||
error.message = `${error.message}\n\n${consoleError}`;
|
||||
} else {
|
||||
error = new Error(consoleError);
|
||||
error.name = "ConsoleError";
|
||||
}
|
||||
}
|
||||
|
||||
let outputs: Array<string>;
|
||||
|
||||
const expectError = file.startsWith("error.");
|
||||
if (expectError) {
|
||||
if (error === null) {
|
||||
throw new Error(
|
||||
`Expected an error to be thrown for fixture: '${file}', remove the 'error.' prefix if an error is not expected.`
|
||||
);
|
||||
} else {
|
||||
outputs = [...formatOutput(items), formatErrorOutput(error)];
|
||||
}
|
||||
} else {
|
||||
if (error !== null) {
|
||||
error.message = `Expected fixture '${file}' to succeed but it failed with error:\n\n${error.message}`;
|
||||
throw error;
|
||||
}
|
||||
if (items === null || items.length === 0) {
|
||||
throw new Error(`Expected at least one output for file '${file}'.`);
|
||||
}
|
||||
outputs = formatOutput(items);
|
||||
}
|
||||
return `
|
||||
## Input
|
||||
|
||||
${wrapWithTripleBackticks(input, "javascript")}
|
||||
|
||||
${outputs.join("\n")}
|
||||
`;
|
||||
}
|
||||
);
|
||||
console.error = originalConsoleError;
|
||||
});
|
||||
|
||||
function formatErrorOutput(error: Error): string {
|
||||
error.message = error.message.replace(/^\/.*?:\s/, "");
|
||||
return `
|
||||
## Error
|
||||
|
||||
${wrapWithTripleBackticks(error.message)}
|
||||
`;
|
||||
}
|
||||
|
||||
function formatOutput(items: Array<TestOutput>): Array<string> {
|
||||
return items.map(({ js }) => {
|
||||
return `
|
||||
## Code
|
||||
|
||||
${wrapWithTripleBackticks(js, "javascript")}
|
||||
`.trim();
|
||||
});
|
||||
}
|
||||
@@ -1,292 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
/* global expect,test */
|
||||
|
||||
import fs from "fs";
|
||||
import glob from "glob";
|
||||
import path from "path";
|
||||
import {
|
||||
ExternalFunction,
|
||||
InstrumentForgetOptions,
|
||||
PluginOptions,
|
||||
} from "../../Babel/PluginOptions";
|
||||
|
||||
const EXPECT_SUFFIX = ".expect.md";
|
||||
|
||||
const PROJECT_ROOT = path.dirname(path.dirname(__dirname));
|
||||
|
||||
expect.extend({
|
||||
accessSnapshotState(_anything, cb) {
|
||||
cb(this["snapshotState"]._updateSnapshot);
|
||||
return {
|
||||
pass: true,
|
||||
message: () => "",
|
||||
};
|
||||
},
|
||||
toHaveNoUnmatchedSnapshots(unmatchedSnapshots, fixturesPath) {
|
||||
return {
|
||||
pass: unmatchedSnapshots.length === 0,
|
||||
message: () => {
|
||||
const unmatchedSnapshotsText = unmatchedSnapshots
|
||||
.map((file: string) => path.join(fixturesPath, file))
|
||||
.join("\n * ");
|
||||
return (
|
||||
`Found ${EXPECT_SUFFIX} files without corresponding inputs:\n* ${unmatchedSnapshotsText}` +
|
||||
`\n\nRun 'npm test -- -u' to remove these extra ${EXPECT_SUFFIX} files`
|
||||
);
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
type FixtureTestOptions = {
|
||||
debug: boolean;
|
||||
language: "flow" | "typescript";
|
||||
};
|
||||
export default function generateTestsFromFixtures(
|
||||
fixturesPath: string,
|
||||
transform: (
|
||||
input: string,
|
||||
file: any,
|
||||
options: FixtureTestOptions & PluginOptions
|
||||
) => string
|
||||
) {
|
||||
let files: Array<string>;
|
||||
try {
|
||||
files = glob.sync("**/*.{js,md}", {
|
||||
cwd: fixturesPath,
|
||||
});
|
||||
} catch (e) {
|
||||
if (e.code === "ENOENT") {
|
||||
files = [];
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
const fixtures = matchInputOutputFixtures(files, fixturesPath);
|
||||
|
||||
const relativeFixturesPath = path.relative(PROJECT_ROOT, fixturesPath);
|
||||
describe(relativeFixturesPath, () => {
|
||||
test("has a consistent extension for input fixtures", () => {
|
||||
const extensions = Array.from(
|
||||
new Set(
|
||||
Array.from(fixtures.values())
|
||||
.map((entry) =>
|
||||
entry.input != null ? path.extname(entry.input) : null
|
||||
)
|
||||
.filter(Boolean)
|
||||
)
|
||||
);
|
||||
expect(extensions).toEqual(extensions.slice(0, 1));
|
||||
});
|
||||
|
||||
describe("fixtures", () => {
|
||||
for (const {
|
||||
basename,
|
||||
input: inputFile,
|
||||
output: outputFile,
|
||||
} of Array.from(fixtures.values())) {
|
||||
let testCommand = test;
|
||||
let input: string | null = null;
|
||||
let debug = false;
|
||||
let enableOnlyOnUseForgetDirective = false;
|
||||
let gating: ExternalFunction | null = null;
|
||||
let instrumentForget: InstrumentForgetOptions | null = null;
|
||||
let inlineUseMemo = true;
|
||||
let panicOnBailout = true;
|
||||
let memoizeJsxElements = true;
|
||||
let enableAssumeHooksFollowRulesOfReact = false;
|
||||
let enableTreatHooksAsFunctions = true;
|
||||
|
||||
if (inputFile != null) {
|
||||
input = fs.readFileSync(inputFile, "utf8");
|
||||
const lines = input.split("\n");
|
||||
if (lines[0]!.indexOf("@only") !== -1) {
|
||||
testCommand = test.only;
|
||||
}
|
||||
if (lines[0]!.indexOf("@skip") !== -1) {
|
||||
testCommand = test.skip;
|
||||
}
|
||||
if (lines[0]!.indexOf("@debug") !== -1) {
|
||||
debug = true;
|
||||
}
|
||||
if (lines[0]!.indexOf("@forgetDirective") !== -1) {
|
||||
enableOnlyOnUseForgetDirective = true;
|
||||
}
|
||||
if (lines[0]!.indexOf("@gating") !== -1) {
|
||||
gating = {
|
||||
source: "ReactForgetFeatureFlag",
|
||||
importSpecifierName: "isForgetEnabled_Fixtures",
|
||||
};
|
||||
}
|
||||
if (lines[0]!.indexOf("@instrumentForget") !== -1) {
|
||||
instrumentForget = {
|
||||
gating: {
|
||||
source: "ReactInstrumentForgetFeatureFlag",
|
||||
importSpecifierName: "isInstrumentForgetEnabled_Fixtures",
|
||||
},
|
||||
instrumentFn: {
|
||||
source: "react-forget-runtime",
|
||||
importSpecifierName: "useRenderCounter",
|
||||
},
|
||||
};
|
||||
}
|
||||
if (lines[0]!.indexOf("@inlineUseMemo false") !== -1) {
|
||||
inlineUseMemo = false;
|
||||
}
|
||||
if (lines[0]!.indexOf("@panicOnBailout false") !== -1) {
|
||||
panicOnBailout = false;
|
||||
}
|
||||
if (lines[0]!.indexOf("@memoizeJsxElements false") !== -1) {
|
||||
memoizeJsxElements = false;
|
||||
}
|
||||
if (
|
||||
lines[0]!.indexOf("@enableAssumeHooksFollowRulesOfReact true") !==
|
||||
-1
|
||||
) {
|
||||
enableAssumeHooksFollowRulesOfReact = true;
|
||||
}
|
||||
if (lines[0]!.indexOf("@enableTreatHooksAsFunctions false") !== -1) {
|
||||
enableTreatHooksAsFunctions = false;
|
||||
}
|
||||
}
|
||||
|
||||
testCommand(basename, () => {
|
||||
let receivedOutput;
|
||||
if (input !== null) {
|
||||
receivedOutput = transform(input, basename, {
|
||||
environment: {
|
||||
inlineUseMemo,
|
||||
memoizeJsxElements,
|
||||
enableAssumeHooksFollowRulesOfReact,
|
||||
enableTreatHooksAsFunctions,
|
||||
},
|
||||
logger: null,
|
||||
debug,
|
||||
enableOnlyOnUseForgetDirective,
|
||||
gating,
|
||||
instrumentForget,
|
||||
language: parseLanguage(input),
|
||||
panicOnBailout,
|
||||
isDev: true,
|
||||
});
|
||||
} else {
|
||||
receivedOutput = "<<input deleted>>";
|
||||
}
|
||||
|
||||
// Use a standard snapshot for the expected output so that the snapshot fails unless the
|
||||
// value matches
|
||||
expect(receivedOutput).toMatchSnapshot();
|
||||
|
||||
// Determine whether the snapshot is in update mode or only creating snapshots for new inputs
|
||||
// to update the .expect file in parallel with updating the snapshot itself.
|
||||
const snapshotUpdateMode = determineSnapshotMode();
|
||||
if (outputFile != null) {
|
||||
const outputExists = fs.existsSync(outputFile);
|
||||
if (
|
||||
snapshotUpdateMode === "all" ||
|
||||
(snapshotUpdateMode === "new" && !outputExists)
|
||||
) {
|
||||
if (inputFile != null) {
|
||||
fs.writeFileSync(outputFile, receivedOutput, "utf8");
|
||||
} else {
|
||||
fs.unlinkSync(outputFile);
|
||||
}
|
||||
} else {
|
||||
// As a sanity check, make sure that the current output matches the .expect file
|
||||
const actualOutput = fs.readFileSync(outputFile, "utf8");
|
||||
expect(receivedOutput).toEqual(actualOutput);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const FlowPragmas = [/\/\/\s@flow$/gm, /\*\s@flow$/gm];
|
||||
function parseLanguage(source: string): "flow" | "typescript" {
|
||||
let useFlow: boolean = false;
|
||||
for (const flowPragma of FlowPragmas) {
|
||||
useFlow ||= !!source.match(flowPragma);
|
||||
}
|
||||
return useFlow ? "flow" : "typescript";
|
||||
}
|
||||
|
||||
function determineSnapshotMode() {
|
||||
// Determine which snapshot mode we're in: ignoring snapshots,
|
||||
// updating new files only, or updating all files
|
||||
let updateSnapshots = "none";
|
||||
// @ts-ignore
|
||||
expect(null).accessSnapshotState((_updateSnapshots) => {
|
||||
updateSnapshots = _updateSnapshots;
|
||||
});
|
||||
const updateSnapshotEnvVariable = process.env["UPDATE_SNAPSHOTS"];
|
||||
if (
|
||||
updateSnapshotEnvVariable === "1" ||
|
||||
updateSnapshotEnvVariable === "all"
|
||||
) {
|
||||
console.log(
|
||||
"Updating all snapshots due to UPDATE_SNAPSHOTS environment variable being set"
|
||||
);
|
||||
updateSnapshots = "all";
|
||||
} else {
|
||||
// @ts-ignore
|
||||
expect(updateSnapshotEnvVariable).toEqual();
|
||||
}
|
||||
expect(updateSnapshots).toEqual(expect.stringMatching(/none|new|all/));
|
||||
return updateSnapshots;
|
||||
}
|
||||
|
||||
function matchInputOutputFixtures(files: string[], fixturesPath: string) {
|
||||
const fixtures: Map<
|
||||
string,
|
||||
{ basename: string; input: string | null; output: string | null }
|
||||
> = new Map();
|
||||
for (const file of files) {
|
||||
const isOutput = file.endsWith(EXPECT_SUFFIX);
|
||||
const basename = path.basename(
|
||||
file,
|
||||
isOutput ? EXPECT_SUFFIX : path.extname(file)
|
||||
);
|
||||
let entry = fixtures.get(basename);
|
||||
if (entry === undefined) {
|
||||
entry = { basename, input: null, output: null };
|
||||
fixtures.set(basename, entry);
|
||||
}
|
||||
const resolvedPath = path.format({
|
||||
dir: fixturesPath,
|
||||
name: file,
|
||||
});
|
||||
if (isOutput) {
|
||||
entry.output = resolvedPath;
|
||||
} else {
|
||||
if (entry.input !== null) {
|
||||
throw new Error(
|
||||
"Found multiple inputs with the basename '" +
|
||||
basename +
|
||||
"': " +
|
||||
entry.input +
|
||||
" and " +
|
||||
resolvedPath
|
||||
);
|
||||
}
|
||||
entry.input = resolvedPath;
|
||||
const outputName = `${basename}${EXPECT_SUFFIX}`;
|
||||
const outputFile = path.join(
|
||||
fixturesPath,
|
||||
path.dirname(file),
|
||||
outputName
|
||||
);
|
||||
entry.output = outputFile;
|
||||
}
|
||||
}
|
||||
return fixtures;
|
||||
}
|
||||
Reference in New Issue
Block a user