Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unify snapshots comparison #6295

Merged
merged 2 commits into from
Jul 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { makeMutable } from 'react-native-reanimated';
import type { Operation, OperationUpdate } from '../types';
import { isValidPropName } from '../types';
import type { MultiViewSnapshot, SingleViewSnapshot } from '../matchers/snapshotMatchers';
import type { TestComponent } from '../TestComponent';
import { SyncUIRunner } from '../utils/SyncUIRunner';
import { convertDecimalColor } from '../utils/util';

export type SingleViewSnapshot = Array<OperationUpdate>;
type MultiViewSnapshot = Record<number, SingleViewSnapshot>;

type JsUpdate = {
tag: number;
shadowNodeWrapper?: unknown;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import {
toBeCalledUIMatcher,
toBeCalledJSMatcher,
} from './rawMatchers';
import type { SingleViewSnapshot } from './snapshotMatchers';
import { compareSnapshots } from './snapshotMatchers';
import type { SingleViewSnapshot } from '../TestRunner/UpdatesContainer';

export class Matchers {
private _negation = false;
Expand All @@ -36,8 +36,8 @@ export class Matchers {
public toBeCalledUI = this.decorateMatcher(toBeCalledUIMatcher);
public toBeCalledJS = this.decorateMatcher(toBeCalledJSMatcher);

public toMatchSnapshots(expectedSnapshots: SingleViewSnapshot | Record<number, SingleViewSnapshot>) {
const capturedSnapshots = this._currentValue as SingleViewSnapshot | Record<number, SingleViewSnapshot>;
public toMatchSnapshots(expectedSnapshots: SingleViewSnapshot) {
const capturedSnapshots = this._currentValue as SingleViewSnapshot;
if (capturedSnapshots) {
const mismatchError = compareSnapshots(expectedSnapshots, capturedSnapshots, false);
if (mismatchError) {
Expand All @@ -48,11 +48,8 @@ export class Matchers {
}
}

public toMatchNativeSnapshots(
expectedSnapshots: SingleViewSnapshot | Record<number, SingleViewSnapshot>,
expectNegativeValueMismatch = false,
) {
const capturedSnapshots = this._currentValue as SingleViewSnapshot | Record<number, SingleViewSnapshot>;
public toMatchNativeSnapshots(expectedSnapshots: SingleViewSnapshot, expectNegativeValueMismatch = false) {
const capturedSnapshots = this._currentValue as SingleViewSnapshot;
const mismatchError = compareSnapshots(expectedSnapshots, capturedSnapshots, true, expectNegativeValueMismatch);
if (mismatchError) {
this._testCase.errors.push(mismatchError);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,47 @@ import { formatSnapshotMismatch, green, red, yellow } from '../utils/stringForma
import type { OperationUpdate, Mismatch } from '../types';
import { ComparisonMode, isValidPropName } from '../types';
import { getComparator, getComparisonModeForProp } from './Comparators';
import type { SingleViewSnapshot } from '../TestRunner/UpdatesContainer';

export type SingleViewSnapshot = Array<OperationUpdate>;
export type MultiViewSnapshot = Record<number, SingleViewSnapshot>;
export type Snapshot = SingleViewSnapshot | MultiViewSnapshot;

function isJsAndNativeSnapshotsEqual(
jsSnapshots: Array<OperationUpdate>,
nativeSnapshots: Array<OperationUpdate>,
i: number,
function compareSnapshot(
expectedSnapshot: OperationUpdate,
capturedSnapshot: OperationUpdate,
expectNegativeValueMismatch: boolean,
): boolean {
/**
) {
const keys = Object.keys(capturedSnapshot) as Array<keyof OperationUpdate>;
for (const key of keys) {
const jsValue = capturedSnapshot[key];
const nativeValue = expectedSnapshot[key];
const comparisonMode = isValidPropName(key) ? getComparisonModeForProp(key) : ComparisonMode.AUTO;
const isEqual = getComparator(comparisonMode);
const expectMismatch = jsValue < 0 && expectNegativeValueMismatch;
const valuesAreMatching = isEqual(jsValue, nativeValue);
if ((!valuesAreMatching && !expectMismatch) || (valuesAreMatching && expectMismatch)) {
return false;
}
}
return true;
}

function compareSingleViewSnapshots(
expectedSnapshots: SingleViewSnapshot, // native
capturedSnapshots: SingleViewSnapshot, // js
native: boolean,
expectNegativeValueMismatch = false,
) {
const mismatchedSnapshots: Array<Mismatch> = [];

const matchingLength = Math.abs(expectedSnapshots.length - capturedSnapshots.length) <= Number(native);
if (!matchingLength) {
return `Expected ${green(expectedSnapshots.length - Number(native))} snapshots, but received ${red(
capturedSnapshots.length,
)} snapshots\n`;
}

for (let i = 0; i < capturedSnapshots.length - 1; i++) {
if (native) {
const capturedSnapshot = capturedSnapshots[i];
/**
The TestRunner can collect two types of snapshots:
- JS snapshots: animation updates sent via `_updateProps`
- Native snapshots: snapshots obtained from the native side via `getViewProp`
Expand All @@ -23,25 +52,23 @@ function isJsAndNativeSnapshotsEqual(
to take a native snapshot immediately after the `_updateProps` call. To address this issue,
we need to wait for the next frame before capturing the native snapshot.
That's why native snapshots are one frame behind JS snapshots. To account for this delay,
one additional native snapshot is taken during the execution of the `getNativeSnapshots` function.
*/
one additional native snapshot is taken during the execution of the `getNativeSnapshots` function. */
const expectedSnapshot = expectedSnapshots[i + Number(native)];
const snapshotMatching = compareSnapshot(expectedSnapshot, capturedSnapshot, expectNegativeValueMismatch);

const jsSnapshot = jsSnapshots[i];
const nativeSnapshot = nativeSnapshots[i + 1];

const keys = Object.keys(jsSnapshot) as Array<keyof OperationUpdate>;
for (const key of keys) {
const jsValue = jsSnapshot[key];
const nativeValue = nativeSnapshot[key];
const comparisonMode = isValidPropName(key) ? getComparisonModeForProp(key) : ComparisonMode.AUTO;
const isEqual = getComparator(comparisonMode);
const expectMismatch = jsValue < 0 && expectNegativeValueMismatch;
const valuesAreMatching = isEqual(jsValue, nativeValue);
if ((!valuesAreMatching && !expectMismatch) || (valuesAreMatching && expectMismatch)) {
return false;
if (!snapshotMatching) {
mismatchedSnapshots.push({
index: i,
expectedSnapshot: expectedSnapshots[i + Number(native)],
capturedSnapshot: capturedSnapshots[i],
});
}
}
}
return true;

if (mismatchedSnapshots.length > 0) {
return formatSnapshotMismatch(mismatchedSnapshots, false);
}
}

/**
Expand All @@ -60,102 +87,51 @@ function compareSingleViewNativeSnapshots(
jsUpdates: Array<OperationUpdate>,
expectNegativeValueMismatch = false,
): string | undefined {
if (!nativeSnapshots || !jsUpdates) {
return `Missing snapshot`;
}
if (jsUpdates.length !== nativeSnapshots.length - 1 && jsUpdates.length !== nativeSnapshots.length) {
return `Expected ${green(jsUpdates.length)} snapshots, but received ${red(nativeSnapshots.length - 1)} snapshots\n`;
}
const mismatchedSnapshots: Array<Mismatch> = [];
for (let i = 0; i < jsUpdates.length - 1; i++) {
if (!isJsAndNativeSnapshotsEqual(jsUpdates, nativeSnapshots, i, expectNegativeValueMismatch)) {
mismatchedSnapshots.push({ index: i, expectedSnapshot: nativeSnapshots[i + 1], capturedSnapshot: jsUpdates[i] });
}
}
return mismatchedSnapshots.length === 0 ? undefined : formatSnapshotMismatch(mismatchedSnapshots, true);
return compareSingleViewSnapshots(nativeSnapshots, jsUpdates, true, expectNegativeValueMismatch);
}

function compareSingleViewJsSnapshots(
expectedSnapshots: SingleViewSnapshot,
capturedSnapshots: SingleViewSnapshot,
): string | undefined {
const mismatchedSnapshots: Array<Mismatch> = [];
if (expectedSnapshots.length !== capturedSnapshots.length) {
return `Expected ${green(expectedSnapshots.length)} snapshots, but received ${red(
capturedSnapshots.length,
)} snapshots\n`;
}
expectedSnapshots.forEach((expectedSnapshot: OperationUpdate, index: number) => {
const capturedSnapshot = capturedSnapshots[index];
const isEquals = getComparator(ComparisonMode.AUTO);
if (!isEquals(expectedSnapshot, capturedSnapshot)) {
mismatchedSnapshots.push({ index, expectedSnapshot, capturedSnapshot });
}
});
if (mismatchedSnapshots.length > 0) {
return formatSnapshotMismatch(mismatchedSnapshots, false);
return compareSingleViewSnapshots(expectedSnapshots, capturedSnapshots, false, false);
}

function isSingleViewSnapshot(snapshot: unknown): snapshot is SingleViewSnapshot {
if (!Array.isArray(snapshot)) {
return false;
}
const allElementsAreObjects = snapshot.reduce(
(accumulator, currentValue) => accumulator && typeof currentValue === 'object',
true,
);
return allElementsAreObjects;
}

export function compareSnapshots(
expectedSnapshots: SingleViewSnapshot | Record<number, SingleViewSnapshot>,
capturedSnapshots: SingleViewSnapshot | Record<number, SingleViewSnapshot>,
expectedSnapshots: unknown,
capturedSnapshots: unknown,
native: boolean,
expectNegativeValueMismatch = false,
): string | null {
let errorMessage = '';

const compareSingleViewSnapshots = (expected: SingleViewSnapshot, captured: SingleViewSnapshot) => {
return native
? compareSingleViewNativeSnapshots(expected, captured, expectNegativeValueMismatch)
: compareSingleViewJsSnapshots(expected, captured);
};
if (!isSingleViewSnapshot(expectedSnapshots)) {
errorMessage += `Unexpected type of expected snapshots: ${typeof capturedSnapshots}\n`;
} else if (!isSingleViewSnapshot(capturedSnapshots)) {
errorMessage += `Unexpected type of captured snapshots: ${typeof capturedSnapshots}\n`;
} else {
const compareSingleViewSnapshots = (expected: SingleViewSnapshot, captured: SingleViewSnapshot) => {
return native
? compareSingleViewNativeSnapshots(expected, captured, expectNegativeValueMismatch)
: compareSingleViewJsSnapshots(expected, captured);
};

if (Array.isArray(expectedSnapshots)) {
if (!Array.isArray(capturedSnapshots)) {
return `Expected snapshots of ${green('only one')} view, received snapshots of ${red(
Object.keys(capturedSnapshots).length,
)}`;
} else {
if (!Array.isArray(capturedSnapshots)) {
return `Unexpected type of captured snapshots: ${typeof capturedSnapshots}`;
}
const err = compareSingleViewSnapshots(expectedSnapshots, capturedSnapshots);
if (err) {
errorMessage = 'Snapshot mismatch: \n' + err;
}
const err = compareSingleViewSnapshots(expectedSnapshots, capturedSnapshots);
if (err) {
errorMessage = 'Snapshot mismatch: \n' + err;
}
}

if (!Array.isArray(expectedSnapshots) && typeof expectedSnapshots === 'object') {
const expectedViewNum = Object.keys(expectedSnapshots)?.length;
const capturedViewNum = Object.keys(capturedSnapshots)?.length;

if (Array.isArray(capturedSnapshots)) {
return `Expected snapshots of ${green(expectedViewNum)} views, received only ${red('one')}`;
} else {
if (!capturedSnapshots || typeof capturedSnapshots !== 'object') {
return `Unexpected type of captured snapshots: ${typeof capturedSnapshots}`;
}
if (expectedViewNum !== capturedViewNum) {
return `Expected snapshots of ${green(expectedViewNum)} views, received ${red(capturedViewNum)}`;
}
// order of view in snapshots is constant, so we can compare them one by one
for (let i = 0; i < expectedViewNum; i++) {
const viewErrorMessage = compareSingleViewSnapshots(expectedSnapshots[i], capturedSnapshots[i]);
if (viewErrorMessage) {
errorMessage += `Snapshot mismatch for view ${i}: \n` + viewErrorMessage;
}
}
}
}

if (!Array.isArray(expectedSnapshots) && typeof expectedSnapshots !== 'object') {
errorMessage = `Unexpected type of the ${native ? 'JS' : 'expected'} snapshot ` + red(typeof expectedSnapshots);
}
if (!Array.isArray(capturedSnapshots) && typeof capturedSnapshots !== 'object') {
errorMessage = `Unexpected type of snapshot ` + red(typeof expectedSnapshots);
}

return errorMessage !== '' ? `${native ? yellow('Native Snapshot: ') : ''}${errorMessage}` : null;
}