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

Implement toThrow #6267

Merged
merged 6 commits into from
Jul 31, 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,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();
Latropos marked this conversation as resolved.
Show resolved Hide resolved

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!');
});
});
});