From 365334d0aba668e45a6068720db9642e8acdf750 Mon Sep 17 00:00:00 2001 From: Aleksandra Cynk Date: Mon, 15 Jul 2024 14:38:40 +0200 Subject: [PATCH 1/3] Implement to throw --- .../matchers/Matchers.ts | 15 ++- .../matchers/rawMatchers.ts | 99 +++++++++++++++++-- .../ReanimatedRuntimeTestsRunner/types.ts | 3 +- .../tests/TestsOfTestingFramework.test.tsx | 77 ++++++++++++++- 4 files changed, 183 insertions(+), 11 deletions(-) diff --git a/apps/common-app/src/examples/RuntimeTests/ReanimatedRuntimeTestsRunner/matchers/Matchers.ts b/apps/common-app/src/examples/RuntimeTests/ReanimatedRuntimeTestsRunner/matchers/Matchers.ts index ab195e09fff..add305ef599 100644 --- a/apps/common-app/src/examples/RuntimeTests/ReanimatedRuntimeTestsRunner/matchers/Matchers.ts +++ b/apps/common-app/src/examples/RuntimeTests/ReanimatedRuntimeTestsRunner/matchers/Matchers.ts @@ -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'; @@ -21,7 +22,7 @@ export class Matchers { return this; } - private decorateMatcher(matcher: Matcher) { + private decorateMatcher(matcher: Matcher) { return (...args: MatcherArgs) => { const { pass, message } = matcher(this._currentValue, this._negation, ...args); if ((!pass && !this._negation) || (pass && this._negation)) { @@ -30,8 +31,18 @@ export class Matchers { }; } + private decorateAsyncMatcher(matcher: AsyncMatcher) { + 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); diff --git a/apps/common-app/src/examples/RuntimeTests/ReanimatedRuntimeTestsRunner/matchers/rawMatchers.ts b/apps/common-app/src/examples/RuntimeTests/ReanimatedRuntimeTestsRunner/matchers/rawMatchers.ts index 2b9be1e642e..2683a95fc9d 100644 --- a/apps/common-app/src/examples/RuntimeTests/ReanimatedRuntimeTestsRunner/matchers/rawMatchers.ts +++ b/apps/common-app/src/examples/RuntimeTests/ReanimatedRuntimeTestsRunner/matchers/rawMatchers.ts @@ -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 = ( +export type Matcher = ( currentValue: TestValue, negation: boolean, ...args: Args -) => { - pass: boolean; - message: string; -}; +) => MatcherReturn; + +export type AsyncMatcher = ( + currentValue: TestValue, + negation: boolean, + ...args: Args +) => Promise; 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.`, + ); } } @@ -90,3 +103,75 @@ export const toBeCalledUIMatcher: Matcher = (currentValue, negat export const toBeCalledJSMatcher: Matcher = (currentValue, negation, times) => { return toBeCalledOnThreadMatcher(currentValue, negation, times, 'JS'); }; + +export const toThrowMatcher: AsyncMatcher = 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, (negation: boolean, message?: string) => MatcherReturn]> { + 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]; +} diff --git a/apps/common-app/src/examples/RuntimeTests/ReanimatedRuntimeTestsRunner/types.ts b/apps/common-app/src/examples/RuntimeTests/ReanimatedRuntimeTestsRunner/types.ts index 1e2f5cd5d38..bac186339b2 100644 --- a/apps/common-app/src/examples/RuntimeTests/ReanimatedRuntimeTestsRunner/types.ts +++ b/apps/common-app/src/examples/RuntimeTests/ReanimatedRuntimeTestsRunner/types.ts @@ -104,7 +104,8 @@ export type TestValue = | null | undefined | boolean - | OperationUpdate; + | OperationUpdate + | (() => unknown); export type NullableTestValue = TestValue | null | undefined; diff --git a/apps/common-app/src/examples/RuntimeTests/tests/TestsOfTestingFramework.test.tsx b/apps/common-app/src/examples/RuntimeTests/tests/TestsOfTestingFramework.test.tsx index 889a7278a30..da1757de68d 100644 --- a/apps/common-app/src/examples/RuntimeTests/tests/TestsOfTestingFramework.test.tsx +++ b/apps/common-app/src/examples/RuntimeTests/tests/TestsOfTestingFramework.test.tsx @@ -151,7 +151,7 @@ function WarningComponent() { return ; } -describe.skip('Tests of Test Framework', () => { +describe('Tests of Test Framework', () => { test('withTiming - expect error', async () => { await render(); const component = getTestComponent('BrownComponent'); @@ -271,4 +271,79 @@ describe.skip('Tests of Test Framework', () => { test.warn('warning test', 'message', async () => { await render(); }); + + describe('Test .toThrow()', () => { + test('Warn with no error message - expect pass', async () => { + await render(); + await expect(() => { + console.warn('OH, NO!'); + }).toThrow(); + }); + + test('Warn with no error message - expect error', async () => { + await render(); + await expect(() => {}).toThrow(); + }); + + test('Warn with no error message and negation - expect pass', async () => { + await render(); + await expect(() => {}).not.toThrow(); + }); + + test('Warn with with error message - expect pass', async () => { + await render(); + await expect(() => { + console.warn('OH, NO!'); + }).toThrow('OH, NO!'); + }); + + test('Warn with with error message - expect error', async () => { + await render(); + await expect(() => { + console.warn('OH, NO!'); + }).toThrow('OH, YES!'); + }); + + test('console.error with no error message - expect pass', async () => { + await render(); + await expect(() => { + console.error('OH, NO!'); + }).toThrow(); + }); + + test('console.error with with error message - expect pass', async () => { + await render(); + await expect(() => { + console.error('OH, NO!'); + }).toThrow('OH, NO!'); + }); + + test('console.error with with error message - expect error', async () => { + await render(); + await expect(() => { + console.error('OH, NO!'); + }).toThrow('OH, YES!'); + }); + + test('Throw error with no error message - expect pass', async () => { + await render(); + await expect(() => { + throw new Error('OH, NO!'); + }).toThrow(); + }); + + test('Throw error with with error message - expect pass', async () => { + await render(); + await expect(() => { + throw new Error('OH, NO!'); + }).toThrow('OH, NO!'); + }); + + test('Throw error with with error message - expect error', async () => { + await render(); + await expect(() => { + throw new Error('OH, NO!'); + }).toThrow('OH, YES!'); + }); + }); }); From 9b2cee98cbc4cf1a14fffc0b05fff866b095c050 Mon Sep 17 00:00:00 2001 From: Aleksandra Cynk Date: Tue, 30 Jul 2024 10:33:34 +0200 Subject: [PATCH 2/3] Code review --- .../ReJest/matchers/rawMatchers.ts | 18 ++++++++++++++++-- .../tests/TestsOfTestingFramework.test.tsx | 12 ------------ 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/apps/common-app/src/examples/RuntimeTests/ReJest/matchers/rawMatchers.ts b/apps/common-app/src/examples/RuntimeTests/ReJest/matchers/rawMatchers.ts index 99ebda2c236..a3c47247b45 100644 --- a/apps/common-app/src/examples/RuntimeTests/ReJest/matchers/rawMatchers.ts +++ b/apps/common-app/src/examples/RuntimeTests/ReJest/matchers/rawMatchers.ts @@ -110,7 +110,19 @@ export const toThrowMatcher: AsyncMatcher = async (currentValue, ne } const [restoreConsole, checkErrors] = await mockConsole(); - await currentValue(); + try { + await currentValue(); + } catch (e) { + const message = (e as Error)?.message || ''; + const correctMessage = errorMessage ? errorMessage === message : true; + + return { + pass: correctMessage, + message: `Function was expected${negation ? ' NOT' : ''} to throw the message "${green(errorMessage)}"${ + negation ? '' : `, but received "${red(message)}` + }"`, + }; + } await restoreConsole(); return checkErrors(negation, errorMessage); }; @@ -162,7 +174,9 @@ async function mockConsole(): Promise<[() => Promise, (negation: boolean, let errorMessage = ''; if (!correctCallNumber) { - errorMessage = `Function was expected${negation ? ' NOT' : ''} to throw exactly one error or warning.`; + errorMessage = `Function was expected${negation ? ' NOT' : ''} to throw exactly one error or warning, got ${red( + count, + )}.`; } if (!correctMessage) { errorMessage = `Function was expected${negation ? ' NOT' : ''} to throw the message "${green(expectedMessage)}"${ diff --git a/apps/common-app/src/examples/RuntimeTests/tests/TestsOfTestingFramework.test.tsx b/apps/common-app/src/examples/RuntimeTests/tests/TestsOfTestingFramework.test.tsx index 3c5f733ad30..64a3abe0f6f 100644 --- a/apps/common-app/src/examples/RuntimeTests/tests/TestsOfTestingFramework.test.tsx +++ b/apps/common-app/src/examples/RuntimeTests/tests/TestsOfTestingFramework.test.tsx @@ -274,73 +274,61 @@ describe('Tests of Test Framework', () => { describe('Test .toThrow()', () => { test('Warn with no error message - expect pass', async () => { - await render(); await expect(() => { console.warn('OH, NO!'); }).toThrow(); }); test('Warn with no error message - expect error', async () => { - await render(); await expect(() => {}).toThrow(); }); test('Warn with no error message and negation - expect pass', async () => { - await render(); await expect(() => {}).not.toThrow(); }); test('Warn with with error message - expect pass', async () => { - await render(); await expect(() => { console.warn('OH, NO!'); }).toThrow('OH, NO!'); }); test('Warn with with error message - expect error', async () => { - await render(); await expect(() => { console.warn('OH, NO!'); }).toThrow('OH, YES!'); }); test('console.error with no error message - expect pass', async () => { - await render(); await expect(() => { console.error('OH, NO!'); }).toThrow(); }); test('console.error with with error message - expect pass', async () => { - await render(); await expect(() => { console.error('OH, NO!'); }).toThrow('OH, NO!'); }); - test('console.error with with error message - expect error', async () => { - await render(); await expect(() => { console.error('OH, NO!'); }).toThrow('OH, YES!'); }); test('Throw error with no error message - expect pass', async () => { - await render(); await expect(() => { throw new Error('OH, NO!'); }).toThrow(); }); test('Throw error with with error message - expect pass', async () => { - await render(); await expect(() => { throw new Error('OH, NO!'); }).toThrow('OH, NO!'); }); test('Throw error with with error message - expect error', async () => { - await render(); await expect(() => { throw new Error('OH, NO!'); }).toThrow('OH, YES!'); From 4f39c0f9b01ebe730e1096b79606f5d23fd4aa1d Mon Sep 17 00:00:00 2001 From: Aleksandra Cynk Date: Tue, 30 Jul 2024 13:01:15 +0200 Subject: [PATCH 3/3] Code review --- .../ReJest/matchers/rawMatchers.ts | 63 +++++++++---------- 1 file changed, 29 insertions(+), 34 deletions(-) diff --git a/apps/common-app/src/examples/RuntimeTests/ReJest/matchers/rawMatchers.ts b/apps/common-app/src/examples/RuntimeTests/ReJest/matchers/rawMatchers.ts index a3c47247b45..a309cc0cc8d 100644 --- a/apps/common-app/src/examples/RuntimeTests/ReJest/matchers/rawMatchers.ts +++ b/apps/common-app/src/examples/RuntimeTests/ReJest/matchers/rawMatchers.ts @@ -104,30 +104,40 @@ export const toBeCalledJSMatcher: Matcher = (currentValue, negat return toBeCalledOnThreadMatcher(currentValue, negation, times, 'JS'); }; -export const toThrowMatcher: AsyncMatcher = async (currentValue, negation, errorMessage) => { - if (typeof currentValue !== 'function') { - return { pass: false, message: `${currentValue?.toString()} is not a function` }; +export const toThrowMatcher: AsyncMatcher = async (throwingFunction, negation, errorMessage) => { + if (typeof throwingFunction !== 'function') { + return { pass: false, message: `${throwingFunction?.toString()} is not a function` }; } - const [restoreConsole, checkErrors] = await mockConsole(); + const [restoreConsole, getCapturedConsoleErrors] = await mockConsole(); + let thrownException = false; + let thrownExceptionMessage = null; try { - await currentValue(); + await throwingFunction(); } catch (e) { - const message = (e as Error)?.message || ''; - const correctMessage = errorMessage ? errorMessage === message : true; - - return { - pass: correctMessage, - message: `Function was expected${negation ? ' NOT' : ''} to throw the message "${green(errorMessage)}"${ - negation ? '' : `, but received "${red(message)}` - }"`, - }; + thrownException = true; + thrownExceptionMessage = (e as Error)?.message || ''; } await restoreConsole(); - return checkErrors(negation, errorMessage); + + 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, (negation: boolean, message?: string) => MatcherReturn]> { +async function mockConsole(): Promise< + [() => Promise, () => { consoleErrorCount: number; consoleErrorMessage: string }] +> { const syncUIRunner = new SyncUIRunner(); let counterJS = 0; @@ -167,25 +177,10 @@ async function mockConsole(): Promise<[() => Promise, (negation: boolean, }); }; - const checkErrors = (negation: boolean, expectedMessage?: string) => { + const getCapturedConsoleErrors = () => { 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, got ${red( - count, - )}.`; - } - 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 { consoleErrorCount: count, consoleErrorMessage: recordedMessage.value }; }; - return [restoreConsole, checkErrors]; + return [restoreConsole, getCapturedConsoleErrors]; }