Skip to content

Commit

Permalink
Implement toThrow (#6267)
Browse files Browse the repository at this point in the history
## Summary
In the future the matcher `toThrow` should replace .FAILING and .WARNING
test decorators.
It allows more flexibility, as it is easy to add .not modificator and
test that given code doesn't throw unexpected errors.


## Test plan
  • Loading branch information
Latropos authored Jul 31, 2024
1 parent 5a3855a commit d998770
Show file tree
Hide file tree
Showing 4 changed files with 179 additions and 10 deletions.
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import type { TestCase, TestValue } from '../types';
import type { Matcher, MatcherArguments } from './rawMatchers';
import type { AsyncMatcher, AsyncMatcherArguments, Matcher, SyncMatcherArguments } from './rawMatchers';
import {
toBeMatcher,
toBeWithinRangeMatcher,
toBeCalledMatcher,
toBeCalledUIMatcher,
toBeCalledJSMatcher,
toThrowMatcher,
toBeNullableMatcher,
} from './rawMatchers';
import { compareSnapshots } from './snapshotMatchers';
Expand All @@ -21,7 +22,7 @@ export class Matchers {
return this;
}

private decorateMatcher<MatcherArgs extends MatcherArguments>(matcher: Matcher<MatcherArgs>) {
private decorateMatcher<MatcherArgs extends SyncMatcherArguments>(matcher: Matcher<MatcherArgs>) {
return (...args: MatcherArgs) => {
const { pass, message } = matcher(this._currentValue, this._negation, ...args);
if ((!pass && !this._negation) || (pass && this._negation)) {
Expand All @@ -30,9 +31,19 @@ export class Matchers {
};
}

private decorateAsyncMatcher<MatcherArgs extends AsyncMatcherArguments>(matcher: AsyncMatcher<MatcherArgs>) {
return async (...args: MatcherArgs) => {
const { pass, message } = await matcher(this._currentValue, this._negation, ...args);
if ((!pass && !this._negation) || (pass && this._negation)) {
this._testCase.errors.push(message);
}
};
}

public toBe = this.decorateMatcher(toBeMatcher);
public toBeNullable = this.decorateMatcher(toBeNullableMatcher);
public toBeWithinRange = this.decorateMatcher(toBeWithinRangeMatcher);
public toThrow = this.decorateAsyncMatcher(toThrowMatcher);
public toBeCalled = this.decorateMatcher(toBeCalledMatcher);
public toBeCalledUI = this.decorateMatcher(toBeCalledUIMatcher);
public toBeCalledJS = this.decorateMatcher(toBeCalledJSMatcher);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,40 @@
import { makeMutable } from 'react-native-reanimated';
import { cyan, green, red, yellow } from '../utils/stringFormatUtils';
import type { TestValue, TrackerCallCount } from '../types';
import { ComparisonMode } from '../types';
import { getComparator } from './Comparators';
import { SyncUIRunner } from '../utils/SyncUIRunner';

type ToBeArgs = [TestValue, ComparisonMode?];
export type ToThrowArgs = [string?];
type ToBeNullArgs = [];
type ToBeWithinRangeArgs = [number, number];
type ToBeCalledArgs = [number];

export type MatcherArguments = ToBeArgs | ToBeNullArgs | ToBeCalledArgs | ToBeWithinRangeArgs;
export type SyncMatcherArguments = ToBeArgs | ToBeNullArgs | ToBeCalledArgs | ToBeWithinRangeArgs;
export type AsyncMatcherArguments = ToThrowArgs;
export type MatcherReturn = {
pass: boolean;
message: string;
};

export type Matcher<Args extends MatcherArguments> = (
export type Matcher<Args extends SyncMatcherArguments> = (
currentValue: TestValue,
negation: boolean,
...args: Args
) => {
pass: boolean;
message: string;
};
) => MatcherReturn;

export type AsyncMatcher<Args extends AsyncMatcherArguments> = (
currentValue: TestValue,
negation: boolean,
...args: Args
) => Promise<MatcherReturn>;

function assertValueIsCallTracker(value: TrackerCallCount | TestValue): asserts value is TrackerCallCount {
if (typeof value !== 'object' || !(value !== null && 'name' in value && 'onJS' in value && 'onUI' in value)) {
throw Error('Invalid value');
throw Error(
`Invalid value \`${value?.toString()}\`, expected a CallTracker. Use CallTracker returned by function \`getTrackerCallCount\` instead.`,
);
}
}

Expand Down Expand Up @@ -101,3 +114,84 @@ export const toBeCalledUIMatcher: Matcher<ToBeCalledArgs> = (currentValue, negat
export const toBeCalledJSMatcher: Matcher<ToBeCalledArgs> = (currentValue, negation, times) => {
return toBeCalledOnThreadMatcher(currentValue, negation, times, 'JS');
};

export const toThrowMatcher: AsyncMatcher<ToThrowArgs> = async (throwingFunction, negation, errorMessage) => {
if (typeof throwingFunction !== 'function') {
return { pass: false, message: `${throwingFunction?.toString()} is not a function` };
}
const [restoreConsole, getCapturedConsoleErrors] = await mockConsole();
let thrownException = false;
let thrownExceptionMessage = null;

try {
await throwingFunction();
} catch (e) {
thrownException = true;
thrownExceptionMessage = (e as Error)?.message || '';
}
await restoreConsole();

const { consoleErrorCount, consoleErrorMessage } = getCapturedConsoleErrors();
const errorWasThrown = thrownException || consoleErrorCount >= 1;
const capturedMessage = thrownExceptionMessage || consoleErrorMessage;
const messageIsCorrect = errorMessage ? errorMessage === capturedMessage : true;

return {
pass: errorWasThrown && messageIsCorrect,
message: messageIsCorrect
? `Function was expected${negation ? ' NOT' : ''} to throw error or warning`
: `Function was expected${negation ? ' NOT' : ''} to throw the message "${green(errorMessage)}"${
negation ? '' : `, but received "${red(capturedMessage)}`
}"`,
};
};

async function mockConsole(): Promise<
[() => Promise<void>, () => { consoleErrorCount: number; consoleErrorMessage: string }]
> {
const syncUIRunner = new SyncUIRunner();
let counterJS = 0;

const counterUI = makeMutable(0);
const recordedMessage = makeMutable('');

const originalError = console.error;
const originalWarning = console.warn;

const incrementJS = () => {
counterJS++;
};
const mockedConsoleFunction = (message: string) => {
'worklet';
if (_WORKLET) {
counterUI.value++;
} else {
incrementJS();
}
recordedMessage.value = message.split('\n\nThis error is located at:')[0];
};
console.error = mockedConsoleFunction;
console.warn = mockedConsoleFunction;
await syncUIRunner.runOnUIBlocking(() => {
'worklet';
console.error = mockedConsoleFunction;
console.warn = mockedConsoleFunction;
});

const restoreConsole = async () => {
console.error = originalError;
console.warn = originalWarning;
await syncUIRunner.runOnUIBlocking(() => {
'worklet';
console.error = originalError;
console.warn = originalWarning;
});
};

const getCapturedConsoleErrors = () => {
const count = counterUI.value + counterJS;
return { consoleErrorCount: count, consoleErrorMessage: recordedMessage.value };
};

return [restoreConsole, getCapturedConsoleErrors];
}
3 changes: 2 additions & 1 deletion apps/common-app/src/examples/RuntimeTests/ReJest/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,8 @@ export type TestValue =
| boolean
| null
| undefined
| OperationUpdate;
| OperationUpdate
| (() => unknown);

export type TestConfiguration = {
render: Dispatch<SetStateAction<ReactNode | null>>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -298,4 +298,67 @@ describe('Tests of Test Framework', () => {
test.warn('warning test', 'message', async () => {
await render(<WarningComponent />);
});

describe('Test .toThrow()', () => {
test('Warn with no error message - expect pass', async () => {
await expect(() => {
console.warn('OH, NO!');
}).toThrow();
});

test('Warn with no error message - expect error', async () => {
await expect(() => {}).toThrow();
});

test('Warn with no error message and negation - expect pass', async () => {
await expect(() => {}).not.toThrow();
});

test('Warn with with error message - expect pass', async () => {
await expect(() => {
console.warn('OH, NO!');
}).toThrow('OH, NO!');
});

test('Warn with with error message - expect error', async () => {
await expect(() => {
console.warn('OH, NO!');
}).toThrow('OH, YES!');
});

test('console.error with no error message - expect pass', async () => {
await expect(() => {
console.error('OH, NO!');
}).toThrow();
});

test('console.error with with error message - expect pass', async () => {
await expect(() => {
console.error('OH, NO!');
}).toThrow('OH, NO!');
});
test('console.error with with error message - expect error', async () => {
await expect(() => {
console.error('OH, NO!');
}).toThrow('OH, YES!');
});

test('Throw error with no error message - expect pass', async () => {
await expect(() => {
throw new Error('OH, NO!');
}).toThrow();
});

test('Throw error with with error message - expect pass', async () => {
await expect(() => {
throw new Error('OH, NO!');
}).toThrow('OH, NO!');
});

test('Throw error with with error message - expect error', async () => {
await expect(() => {
throw new Error('OH, NO!');
}).toThrow('OH, YES!');
});
});
});

0 comments on commit d998770

Please sign in to comment.