Skip to content

Commit

Permalink
Implement not for runtime tests (#5813)
Browse files Browse the repository at this point in the history
## Summary

Add possibility to negate some of the matchers. I've decided that there
is no need to negate snapshot matchers, so I've handled just the other
functions. Both API and implementation are inspired with jest tests.

<img width="405" alt="image"
src="https://github.com/software-mansion/react-native-reanimated/assets/56199675/172724e8-26d3-45cd-bba5-c742469f392e">

## Test plan
All runtime tests work

---------

Co-authored-by: Latropos <aleksandracynk@Aleksandras-MacBook-Pro-3.local>
  • Loading branch information
Latropos and Latropos committed Mar 29, 2024
1 parent 008464c commit 1c12714
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 63 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ComparisonMode, TestValue } from './types';
import { TestValue } from './types';

export const RUNTIME_TEST_ERRORS = {
UNDEFINED_TEST_SUITE: 'Undefined test suite context',
Expand Down Expand Up @@ -26,14 +26,3 @@ export function color(

return `${COLOR_CODES[color]}${value}\x1b[0m`;
}

export function defaultTestErrorLog(
expected: TestValue,
received: TestValue,
mode: ComparisonMode
) {
const coloredExpected = color(expected, 'green');
const coloredReceived = color(received, 'red');
const coloredMode = color(mode, 'yellow');
return `Expected ${coloredExpected} received ${coloredReceived}, mode: ${coloredMode}`;
}
163 changes: 114 additions & 49 deletions app/src/examples/RuntimeTests/ReanimatedRuntimeTestsRunner/Matchers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getComparator } from './Comparators';
import { color, defaultTestErrorLog } from './LogMessageUtils';
import { color } from './LogMessageUtils';
import {
ComparisonMode,
OperationUpdate,
Expand All @@ -8,10 +8,20 @@ import {
TrackerCallCount,
} from './types';

type MatcherFunction = (
currentValue: TestValue,
expectedValue: TestValue,
...additionalArgs: Array<unknown>
) => {
pass: boolean;
message: string;
};

export class Matchers {
constructor(private currentValue: TestValue, private testCase: TestCase) {}
private _negation = false;
constructor(private _currentValue: TestValue, private _testCase: TestCase) {}

private assertValueIsCallTracker(
private static _assertValueIsCallTracker(
value: TrackerCallCount | TestValue
): asserts value is TrackerCallCount {
if (
Expand All @@ -22,64 +32,119 @@ export class Matchers {
}
}

public toBe(expectedValue: TestValue, comparisonMode = ComparisonMode.AUTO) {
private _toBeMatcher: MatcherFunction = (
currentValue: TestValue,
expectedValue: TestValue,
comparisonModeUnknown: unknown
) => {
const comparisonMode: ComparisonMode =
typeof comparisonModeUnknown === 'string' &&
comparisonModeUnknown in ComparisonMode
? (comparisonModeUnknown as ComparisonMode)
: ComparisonMode.AUTO;

const isEqual = getComparator(comparisonMode);
if (!isEqual(expectedValue, this.currentValue)) {
this.testCase.errors.push(
defaultTestErrorLog(expectedValue, this.currentValue, comparisonMode)
);
}
}

public toBeCalled(times = 1) {
this.assertValueIsCallTracker(this.currentValue);
const callsCount = this.currentValue.onUI + this.currentValue.onJS;
if (callsCount !== times) {
const name = color(this.currentValue.name, 'green');
const expected = color(times, 'green');
const received = color(callsCount, 'red');
this.testCase.errors.push(
`Expected ${name} to be called ${expected} times, but was called ${received} times`
);
}
}
const coloredExpected = color(expectedValue, 'green');
const coloredReceived = color(currentValue, 'red');
const coloredMode = color(comparisonMode, 'yellow');

return {
pass: isEqual(expectedValue, currentValue),
message: `Expected${
this._negation ? ' NOT' : ''
} ${coloredExpected} received ${coloredReceived}, mode: ${coloredMode}`,
};
};

private _toBeCalledMatcher: MatcherFunction = (
currentValue: TestValue,
times = 1
) => {
Matchers._assertValueIsCallTracker(currentValue);
const callsCount = currentValue.onUI + currentValue.onJS;
const name = color(currentValue.name, 'green');
const expected = color(times, 'green');
const received = color(callsCount, 'red');
return {
pass: callsCount === times,
message: `Expected ${name}${
this._negation ? ' NOT' : ''
} to be called ${expected} times, but was called ${received} times`,
};
};

public toBeCalledUI(times = 1) {
this.assertValueIsCallTracker(this.currentValue);
if (this.currentValue.onUI !== times) {
const name = color(this.currentValue.name, 'green');
const threadName = color('UI thread', 'cyan');
const expected = color(times, 'green');
const received = color(this.currentValue.onUI, 'red');
this.testCase.errors.push(
`Expected ${name} to be called ${expected} times on ${threadName}, but was called ${received} times`
private _toBeCalledUIMatcher: MatcherFunction = (
currentValue: TestValue,
times = 1
) => {
Matchers._assertValueIsCallTracker(currentValue);
const callsCount = currentValue.onUI;
const name = color(currentValue.name, 'green');
const threadName = color('UI thread', 'cyan');
const expected = color(times, 'green');
const received = color(callsCount, 'red');

return {
pass: callsCount === times,
message: `Expected ${name}${
this._negation ? ' NOT' : ''
} to be called ${expected} times on ${threadName}, but was called ${received} times`,
};
};

private _toBeCalledJSMatcher: MatcherFunction = (
currentValue: TestValue,
times = 1
) => {
Matchers._assertValueIsCallTracker(currentValue);
const callsCount = currentValue.onJS;
const name = color(currentValue.name, 'green');
const threadName = color('JS thread', 'cyan');
const expected = color(times, 'green');
const received = color(callsCount, 'red');

return {
pass: callsCount === times,
message: `Expected ${name}${
this._negation ? ' NOT' : ''
} to be called ${expected} times on ${threadName}, but was called ${received} times`,
};
};

private decorateMatcher(matcher: MatcherFunction) {
return (expectedValue: TestValue, ...args: Array<unknown>) => {
const { pass, message } = matcher(
this._currentValue,
expectedValue,
...args
);
}
if ((!pass && !this._negation) || (pass && this._negation)) {
this._testCase.errors.push(message);
}
};
}

public toBeCalledJS(times = 1) {
this.assertValueIsCallTracker(this.currentValue);
if (this.currentValue.onJS !== times) {
const name = color(this.currentValue.name, 'green');
const threadName = color('UI thread', 'cyan');
const expected = color(times, 'green');
const received = color(this.currentValue.onUI, 'red');
this.testCase.errors.push(
`Expected ${name} to be called ${expected} times on ${threadName}, but was called ${received} times`
);
}
public toBe = this.decorateMatcher(this._toBeMatcher);
public toBeCalled = this.decorateMatcher(this._toBeCalledMatcher);
public toBeCalledUI = this.decorateMatcher(this._toBeCalledUIMatcher);
public toBeCalledJS = this.decorateMatcher(this._toBeCalledJSMatcher);

get not() {
this._negation = true;
return this;
}

public toMatchSnapshot(expectedSnapshots: Array<Record<string, unknown>>) {
const capturedSnapshots = this.currentValue as Array<
public toMatchSnapshots(expectedSnapshots: Array<Record<string, unknown>>) {
const capturedSnapshots = this._currentValue as Array<
Record<string, unknown>
>;
if (capturedSnapshots.length !== expectedSnapshots.length) {
const errorMessage = this.formatMismatchLengthErrorMessage(
expectedSnapshots.length,
capturedSnapshots.length
);
this.testCase.errors.push(errorMessage);
this._testCase.errors.push(errorMessage);
}
let errorString = '';
expectedSnapshots.forEach(
Expand All @@ -97,7 +162,7 @@ export class Matchers {
}
);
if (errorString !== '') {
this.testCase.errors.push('Snapshot mismatch: \n' + errorString);
this._testCase.errors.push('Snapshot mismatch: \n' + errorString);
}
}

Expand All @@ -109,7 +174,7 @@ export class Matchers {
Updates applied through `_updateProps` are not synchronously applied to the native side. Instead, they are batched and applied at the end of each frame. Therefore, it is not allowed 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.
*/
let errorString = '';
const jsUpdates = this.currentValue as Array<OperationUpdate>;
const jsUpdates = this._currentValue as Array<OperationUpdate>;
for (let i = 0; i < jsUpdates.length; i++) {
errorString += this.compareJsAndNativeSnapshot(
jsUpdates,
Expand All @@ -123,7 +188,7 @@ export class Matchers {
} snapshots\n`;
}
if (errorString !== '') {
this.testCase.errors.push('Native snapshot mismatch: \n' + errorString);
this._testCase.errors.push('Native snapshot mismatch: \n' + errorString);
}
}

Expand Down
20 changes: 18 additions & 2 deletions app/src/examples/RuntimeTests/tests/Animations.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,20 @@ describe('Tests of animations', () => {
expect(await component.getAnimatedStyle('width')).toBe('123');
});

test('withTiming - not - expect error', async () => {
await render(<AnimatedComponent />);
const component = getTestComponent('AnimatedComponent');
await wait(600);
expect(await component.getAnimatedStyle('width')).not.toBe('100');
});

test('withTiming - with not', async () => {
await render(<AnimatedComponent />);
const component = getTestComponent('AnimatedComponent');
await wait(600);
expect(await component.getAnimatedStyle('width')).not.toBe('123');
});

test('withTiming - expect pass', async () => {
await render(<AnimatedComponent />);
const component = getTestComponent('AnimatedComponent');
Expand Down Expand Up @@ -200,7 +214,9 @@ describe('Tests of animations', () => {
const updatesContainer = await recordAnimationUpdates();
await render(<AnimatedComponent />);
await wait(1000);
expect(updatesContainer.getUpdates()).toMatchSnapshot(Snapshots.animation3);
expect(updatesContainer.getUpdates()).toMatchSnapshots(
Snapshots.animation3
);
expect(updatesContainer.getUpdates()).toMatchNativeSnapshots(
await updatesContainer.getNativeSnapshots()
);
Expand All @@ -211,7 +227,7 @@ describe('Tests of animations', () => {
const updatesContainer = await recordAnimationUpdates();
await render(<LayoutAnimation />);
await wait(600);
expect(updatesContainer.getUpdates()).toMatchSnapshot(
expect(updatesContainer.getUpdates()).toMatchSnapshots(
Snapshots.layoutAnimation
);
});
Expand Down

0 comments on commit 1c12714

Please sign in to comment.