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 1 commit
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,12 +1,13 @@
import { color } from '../stringFormatUtils';
import type { TestCase, TestValue, NullableTestValue } from '../types';
import type { Matcher, MatcherArguments } from './rawMatchers';
import type { AsyncMatcher, AsyncMatcherArguments, Matcher, SyncMatcherArguments } from './rawMatchers';
import {
toBeMatcher,
toBeWithinRangeMatcher,
toBeCalledMatcher,
toBeCalledUIMatcher,
toBeCalledJSMatcher,
toThrowMatcher,
} from './rawMatchers';
import type { SingleViewSnapshot } from './snapshotMatchers';
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,8 +31,18 @@ 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 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,26 +1,39 @@
import { makeMutable } from 'react-native-reanimated';
import { cyan, green, red, yellow } from '../stringFormatUtils';
import { SyncUIRunner } from '../SyncUIRunner';
import type { TestValue, TrackerCallCount } from '../types';
import { ComparisonMode } from '../types';
import { getComparator } from './Comparators';

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

export type MatcherArguments = ToBeArgs | ToBeCalledArgs | ToBeWithinRangeArgs;
export type SyncMatcherArguments = ToBeArgs | 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 @@ -90,3 +103,75 @@ 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 (currentValue, negation, errorMessage) => {
if (typeof currentValue !== 'function') {
return { pass: false, message: `${currentValue?.toString()} is not a function` };
}
const [restoreConsole, checkErrors] = await mockConsole();

await currentValue();
await restoreConsole();
return checkErrors(negation, errorMessage);
};

async function mockConsole(): Promise<[() => Promise<void>, (negation: boolean, message?: string) => MatcherReturn]> {
Latropos marked this conversation as resolved.
Show resolved Hide resolved
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 checkErrors = (negation: boolean, expectedMessage?: string) => {
const count = counterUI.value + counterJS;
const correctMessage = expectedMessage ? expectedMessage === recordedMessage.value : true;
const correctCallNumber = count === 1;

let errorMessage = '';
if (!correctCallNumber) {
errorMessage = `Function was expected${negation ? ' NOT' : ''} to throw exactly one error or warning.`;
}
if (!correctMessage) {
errorMessage = `Function was expected${negation ? ' NOT' : ''} to throw the message "${green(expectedMessage)}"${
negation ? '' : `, but received "${red(recordedMessage.value)}`
}"`;
}

return { pass: correctCallNumber && correctMessage, message: errorMessage };
};

return [restoreConsole, checkErrors];
}
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,8 @@ export type TestValue =
| null
| undefined
| boolean
| OperationUpdate;
| OperationUpdate
| (() => unknown);

export type NullableTestValue = TestValue | null | undefined;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ function WarningComponent() {
return <View />;
}

describe.skip('Tests of Test Framework', () => {
describe('Tests of Test Framework', () => {
test('withTiming - expect error', async () => {
await render(<AnimatedComponent />);
const component = getTestComponent('BrownComponent');
Expand Down Expand Up @@ -271,4 +271,79 @@ describe.skip('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 render(<AnimatedComponent />);
await expect(() => {
console.warn('OH, NO!');
}).toThrow();
});

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

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

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

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

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

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

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

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

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

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