mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Add .toMatchSnapshot() (#48029)
Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/48029 Changelog: [Internal] Adding snapshot support for rendered output only for now. This will only work if snapshot is created beforehand by hand. # Next steps * Create snapshot when no prior snapshot is available * Pass and update if instructed Reviewed By: christophpurrer Differential Revision: D66601387 fbshipit-source-id: fe528cded43c5ba36d314bd9af8e3fb84b98ac3e
This commit is contained in:
committed by
Facebook GitHub Bot
parent
a4a2c2867a
commit
a8a136f3ef
@@ -85,6 +85,7 @@
|
||||
"inquirer": "^7.1.0",
|
||||
"jest": "^29.6.3",
|
||||
"jest-diff": "^29.7.0",
|
||||
"jest-snapshot": "^29.7.0",
|
||||
"jest-junit": "^10.0.0",
|
||||
"jscodeshift": "^0.14.0",
|
||||
"metro-babel-register": "^0.81.0",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
* @oncall react_native
|
||||
*/
|
||||
|
||||
import type {SnapshotConfig} from '../runtime/snapshotContext';
|
||||
import type {FantomTestConfigJsOnlyFeatureFlags} from './getFantomTestConfig';
|
||||
|
||||
module.exports = function entrypointTemplate({
|
||||
@@ -16,11 +17,13 @@ module.exports = function entrypointTemplate({
|
||||
setupModulePath,
|
||||
featureFlagsModulePath,
|
||||
featureFlags,
|
||||
snapshotConfig,
|
||||
}: {
|
||||
testPath: string,
|
||||
setupModulePath: string,
|
||||
featureFlagsModulePath: string,
|
||||
featureFlags: FantomTestConfigJsOnlyFeatureFlags,
|
||||
snapshotConfig: SnapshotConfig,
|
||||
}): string {
|
||||
return `/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
@@ -47,6 +50,6 @@ ${Object.entries(featureFlags)
|
||||
: ''
|
||||
}
|
||||
|
||||
registerTest(() => require('${testPath}'));
|
||||
registerTest(() => require('${testPath}'), ${JSON.stringify(snapshotConfig)});
|
||||
`;
|
||||
};
|
||||
|
||||
+24
-2
@@ -24,6 +24,7 @@ import {
|
||||
import fs from 'fs';
|
||||
// $FlowExpectedError[untyped-import]
|
||||
import {formatResultsErrors} from 'jest-message-util';
|
||||
import {SnapshotState, buildSnapshotResolver} from 'jest-snapshot';
|
||||
import Metro from 'metro';
|
||||
import nullthrows from 'nullthrows';
|
||||
import path from 'path';
|
||||
@@ -90,12 +91,29 @@ function generateBytecodeBundle({
|
||||
}
|
||||
|
||||
module.exports = async function runTest(
|
||||
globalConfig: {...},
|
||||
config: {...},
|
||||
globalConfig: {
|
||||
updateSnapshot: 'all' | 'new' | 'none',
|
||||
...
|
||||
},
|
||||
config: {
|
||||
rootDir: string,
|
||||
prettierPath: string,
|
||||
snapshotFormat: {...},
|
||||
...
|
||||
},
|
||||
environment: {...},
|
||||
runtime: {...},
|
||||
testPath: string,
|
||||
): mixed {
|
||||
const snapshotResolver = await buildSnapshotResolver(config);
|
||||
const snapshotPath = snapshotResolver.resolveSnapshotPath(testPath);
|
||||
const snapshotState = new SnapshotState(snapshotPath, {
|
||||
updateSnapshot: globalConfig.updateSnapshot,
|
||||
snapshotFormat: config.snapshotFormat,
|
||||
prettierPath: config.prettierPath,
|
||||
rootDir: config.rootDir,
|
||||
});
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
const testConfig = getFantomTestConfig(testPath);
|
||||
@@ -115,6 +133,10 @@ module.exports = async function runTest(
|
||||
setupModulePath: `${path.relative(BUILD_OUTPUT_PATH, setupModulePath)}`,
|
||||
featureFlagsModulePath: `${path.relative(BUILD_OUTPUT_PATH, featureFlagsModulePath)}`,
|
||||
featureFlags: testConfig.flags.jsOnly,
|
||||
snapshotConfig: {
|
||||
updateSnapshot: snapshotState._updateSnapshot,
|
||||
data: snapshotState._initialData,
|
||||
},
|
||||
});
|
||||
|
||||
const entrypointPath = path.join(
|
||||
|
||||
+28
@@ -10,8 +10,12 @@
|
||||
*/
|
||||
|
||||
import {ensureMockFunction} from './mocks';
|
||||
import {snapshotContext} from './snapshotContext';
|
||||
import deepEqual from 'deep-equal';
|
||||
import {diff} from 'jest-diff';
|
||||
import {format, plugins} from 'pretty-format';
|
||||
|
||||
const COMPARISON_EQUALS_STRING = 'Compared values have no visual difference.';
|
||||
|
||||
class ErrorWithCustomBlame extends Error {
|
||||
// Initially 5 to ignore all the frames from Babel helpers to instantiate this
|
||||
@@ -249,6 +253,30 @@ class Expect {
|
||||
}
|
||||
}
|
||||
|
||||
toMatchSnapshot(expected?: string): void {
|
||||
if (this.#isNot) {
|
||||
throw new ErrorWithCustomBlame(
|
||||
'Snapshot matchers cannot be used with not.',
|
||||
).blameToPreviousFrame();
|
||||
}
|
||||
|
||||
const [err, currentSnapshot] = snapshotContext.getSnapshot(expected);
|
||||
if (err != null) {
|
||||
throw new ErrorWithCustomBlame(err).blameToPreviousFrame();
|
||||
}
|
||||
|
||||
const receivedValue = format(this.#received, {
|
||||
plugins: [plugins.ReactElement],
|
||||
});
|
||||
const result =
|
||||
diff(currentSnapshot, receivedValue) ?? 'Failed to compare outputs';
|
||||
if (result !== COMPARISON_EQUALS_STRING) {
|
||||
throw new ErrorWithCustomBlame(
|
||||
`Expected to match snapshot.\n${result}`,
|
||||
).blameToPreviousFrame();
|
||||
}
|
||||
}
|
||||
|
||||
#isExpectedResult(pass: boolean): boolean {
|
||||
return this.#isNot ? !pass : pass;
|
||||
}
|
||||
|
||||
+10
-1
@@ -9,8 +9,11 @@
|
||||
* @oncall react_native
|
||||
*/
|
||||
|
||||
import type {SnapshotConfig} from './snapshotContext';
|
||||
|
||||
import expect from './expect';
|
||||
import {createMockFunction} from './mocks';
|
||||
import {setupSnapshotConfig, snapshotContext} from './snapshotContext';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
export type TestCaseResult = {
|
||||
@@ -152,6 +155,7 @@ function executeTests() {
|
||||
};
|
||||
|
||||
test.result = result;
|
||||
snapshotContext.setTargetTest(result.fullName);
|
||||
|
||||
if (!test.isSkipped && (!hasFocusedTests || test.isFocused)) {
|
||||
let status;
|
||||
@@ -189,7 +193,12 @@ global.$$RunTests$$ = () => {
|
||||
executeTests();
|
||||
};
|
||||
|
||||
export function registerTest(setUpTest: () => void) {
|
||||
export function registerTest(
|
||||
setUpTest: () => void,
|
||||
snapshotConfig: SnapshotConfig,
|
||||
) {
|
||||
setupSnapshotConfig(snapshotConfig);
|
||||
|
||||
runWithGuard(() => {
|
||||
setUpTest();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @flow strict-local
|
||||
* @format
|
||||
* @oncall react_native
|
||||
*/
|
||||
|
||||
export type SnapshotConfig = {
|
||||
updateSnapshot: 'all' | 'new' | 'none',
|
||||
data: {[key: string]: string},
|
||||
};
|
||||
|
||||
let snapshotConfig: ?SnapshotConfig;
|
||||
|
||||
// Destructure [err, value] from the return value of getSnapshot
|
||||
type SnapshotResponse = [null, string] | [string, void];
|
||||
|
||||
class SnapshotState {
|
||||
#callCount: number = 0;
|
||||
#testFullName: string;
|
||||
|
||||
constructor(name: string) {
|
||||
this.#testFullName = name;
|
||||
}
|
||||
|
||||
getSnapshot(label: ?string): SnapshotResponse {
|
||||
const snapshotKey = `${this.#testFullName}${
|
||||
label != null ? `: ${label}` : ''
|
||||
} ${++this.#callCount}`;
|
||||
|
||||
if (snapshotConfig == null) {
|
||||
return [
|
||||
'Snapshot config is not set. Did you forget to call `setupSnapshotConfig`?',
|
||||
undefined,
|
||||
];
|
||||
}
|
||||
|
||||
if (snapshotConfig.data[snapshotKey] == null) {
|
||||
return [
|
||||
`Expected to have snapshot \`${snapshotKey}\` but it was not found.`,
|
||||
undefined,
|
||||
];
|
||||
}
|
||||
|
||||
return [null, snapshotConfig.data[snapshotKey]];
|
||||
}
|
||||
}
|
||||
|
||||
class SnapshotContext {
|
||||
#snapshotState: ?SnapshotState = null;
|
||||
|
||||
setTargetTest(testFullName: string) {
|
||||
this.#snapshotState = new SnapshotState(testFullName);
|
||||
}
|
||||
|
||||
getSnapshot(label: ?string): SnapshotResponse {
|
||||
return (
|
||||
this.#snapshotState?.getSnapshot(label) ?? [
|
||||
'Snapshot state is not set, call `setTargetTest()` first',
|
||||
undefined,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const snapshotContext: SnapshotContext = new SnapshotContext();
|
||||
|
||||
export function setupSnapshotConfig(config: SnapshotConfig) {
|
||||
snapshotConfig = config;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`expect toMatchSnapshot() primitive types 1`] = `null`
|
||||
|
||||
exports[`expect toMatchSnapshot() primitive types 2`] = `1`
|
||||
|
||||
exports[`expect toMatchSnapshot() primitive types 3`] = `"foo"`
|
||||
|
||||
exports[`expect toMatchSnapshot() complex types 1`] = `Object {
|
||||
"foo": "bar",
|
||||
}`
|
||||
|
||||
exports[`expect toMatchSnapshot() complex types 2`] = `<span>
|
||||
hello
|
||||
</span>`
|
||||
|
||||
exports[`expect toMatchSnapshot() named snapshots: named snapshot 1`] = `Object {
|
||||
"a": "b",
|
||||
}`
|
||||
@@ -9,6 +9,8 @@
|
||||
* @oncall react_native
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
function ensureError(fn: () => void): void {
|
||||
try {
|
||||
fn();
|
||||
@@ -512,4 +514,21 @@ describe('expect', () => {
|
||||
expect(1).not.toBeGreaterThanOrEqual('string value');
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
describe('toMatchSnapshot()', () => {
|
||||
test('primitive types', () => {
|
||||
expect(null).toMatchSnapshot();
|
||||
expect(1).toMatchSnapshot();
|
||||
expect('foo').toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('complex types', () => {
|
||||
expect({foo: 'bar'}).toMatchSnapshot();
|
||||
expect(<span>hello</span>).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('named snapshots', () => {
|
||||
expect({a: 'b'}).toMatchSnapshot('named snapshot');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user