From 416a632e2cfc6aaba58d1e2416d66f433fe829f6 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Tue, 27 Sep 2022 14:48:09 +0200 Subject: [PATCH 1/9] feature: basic configure api --- src/config.ts | 27 +++++++++++++++++++++++++++ src/pure.ts | 2 ++ 2 files changed, 29 insertions(+) create mode 100644 src/config.ts diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 000000000..184bc3cae --- /dev/null +++ b/src/config.ts @@ -0,0 +1,27 @@ +export type Config = { + /** Default timeout, in ms, for `waitFor` and `findBy*` queries. */ + asyncUtilTimeout: number; +}; + +const defaultConfig: Config = { + asyncUtilTimeout: 1000, +}; + +let config = { + ...defaultConfig, +}; + +export function configure(options: Partial) { + config = { + ...config, + ...options, + }; +} + +export function resetToDefault() { + config = defaultConfig; +} + +export function getConfig() { + return config; +} diff --git a/src/pure.ts b/src/pure.ts index c3f448249..0d5124897 100644 --- a/src/pure.ts +++ b/src/pure.ts @@ -16,9 +16,11 @@ export type { RenderResult as RenderAPI, } from './render'; export type { RenderHookOptions, RenderHookResult } from './renderHook'; +export type { Config } from './config'; export { act }; export { cleanup }; +export { configure, resetToDefault } from './config'; export { fireEvent }; export { render }; export { waitFor }; From 2c86ad15b9b77aa665042658b0bfb465f54c6b01 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Tue, 27 Sep 2022 14:48:28 +0200 Subject: [PATCH 2/9] test: config module --- src/__tests__/config.test.ts | 22 ++++++++++++++++++++++ src/config.ts | 2 +- src/pure.ts | 2 +- 3 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 src/__tests__/config.test.ts diff --git a/src/__tests__/config.test.ts b/src/__tests__/config.test.ts new file mode 100644 index 000000000..7a8b7de48 --- /dev/null +++ b/src/__tests__/config.test.ts @@ -0,0 +1,22 @@ +import { getConfig, configure, resetToDefaults } from '../config'; + +beforeEach(() => { + resetToDefaults(); +}); + +test('getConfig() returns existing configuration', () => { + expect(getConfig().asyncUtilTimeout).toEqual(1000); +}); + +test('configure() overrides existing values', () => { + configure({ asyncUtilTimeout: 5000 }); + expect(getConfig().asyncUtilTimeout).toEqual(5000); +}); + +test('resetToDefaults() resets to defaults', () => { + configure({ asyncUtilTimeout: 5000 }); + expect(getConfig().asyncUtilTimeout).toEqual(5000); + + resetToDefaults(); + expect(getConfig().asyncUtilTimeout).toEqual(1000); +}); diff --git a/src/config.ts b/src/config.ts index 184bc3cae..940f367df 100644 --- a/src/config.ts +++ b/src/config.ts @@ -18,7 +18,7 @@ export function configure(options: Partial) { }; } -export function resetToDefault() { +export function resetToDefaults() { config = defaultConfig; } diff --git a/src/pure.ts b/src/pure.ts index 0d5124897..00ed3d5ea 100644 --- a/src/pure.ts +++ b/src/pure.ts @@ -20,7 +20,7 @@ export type { Config } from './config'; export { act }; export { cleanup }; -export { configure, resetToDefault } from './config'; +export { configure, resetToDefaults } from './config'; export { fireEvent }; export { render }; export { waitFor }; From ce11d48ce74044cb3d00ae2d132a6efaac04f596 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Tue, 27 Sep 2022 15:05:16 +0200 Subject: [PATCH 3/9] refactor: apply to waitFor with tests --- src/__tests__/waitFor.test.tsx | 163 +++++---------------------------- src/waitFor.ts | 4 +- 2 files changed, 26 insertions(+), 141 deletions(-) diff --git a/src/__tests__/waitFor.test.tsx b/src/__tests__/waitFor.test.tsx index 534b43bdd..3b1051d89 100644 --- a/src/__tests__/waitFor.test.tsx +++ b/src/__tests__/waitFor.test.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { Text, TouchableOpacity, View, Pressable } from 'react-native'; -import { fireEvent, render, waitFor } from '..'; +import { fireEvent, render, waitFor, configure, resetToDefaults } from '..'; class Banana extends React.Component { changeFresh = () => { @@ -19,6 +19,10 @@ class Banana extends React.Component { } } +beforeEach(() => { + resetToDefaults(); +}); + class BananaContainer extends React.Component<{}, any> { state = { fresh: false }; @@ -38,18 +42,6 @@ afterEach(() => { jest.useRealTimers(); }); -test('waits for element until it stops throwing', async () => { - const { getByText, queryByText } = render(); - - fireEvent.press(getByText('Change freshness!')); - - expect(queryByText('Fresh')).toBeNull(); - - const freshBananaText = await waitFor(() => getByText('Fresh')); - - expect(freshBananaText.props.children).toBe('Fresh'); -}); - test('waits for element until timeout is met', async () => { const { getByText } = render(); @@ -64,135 +56,28 @@ test('waits for element until timeout is met', async () => { await waitFor(() => getByText('Fresh')); }); -test('waits for element with custom interval', async () => { - const mockFn = jest.fn(() => { - throw Error('test'); - }); - - try { - await waitFor(() => mockFn(), { timeout: 400, interval: 200 }); - } catch (e) { - // suppress - } +test('waitFor defaults to asyncWaitTimeout config option', async () => { + configure({ asyncUtilTimeout: 100 }); + const { getByText } = render(); - expect(mockFn).toHaveBeenCalledTimes(2); -}); + fireEvent.press(getByText('Change freshness!')); + await expect(waitFor(() => getByText('Fresh'))).rejects.toThrow(); -// this component is convoluted on purpose. It is not a good react pattern, but it is valid -// react code that will run differently between different react versions (17 and 18), so we need -// explicit tests for it -const Comp = ({ onPress }: { onPress: () => void }) => { - const [state, setState] = React.useState(false); - - React.useEffect(() => { - if (state) { - onPress(); - } - }, [state, onPress]); - - return ( - { - await Promise.resolve(); - setState(true); - }} - > - Trigger - - ); -}; - -test('waits for async event with fireEvent', async () => { - const spy = jest.fn(); - const { getByText } = render(); - - fireEvent.press(getByText('Trigger')); - - await waitFor(() => { - expect(spy).toHaveBeenCalled(); - }); + // Async action ends after 300ms and we only waited 100ms, so we need to wait + // for the remaining async actions to finish + await waitFor(() => getByText('Fresh'), { timeout: 1000 }); }); -test.each([false, true])( - 'waits for element until it stops throwing using fake timers (legacyFakeTimers = %s)', - async (legacyFakeTimers) => { - jest.useFakeTimers({ legacyFakeTimers }); - const { getByText, queryByText } = render(); - - fireEvent.press(getByText('Change freshness!')); - expect(queryByText('Fresh')).toBeNull(); - - jest.advanceTimersByTime(300); - const freshBananaText = await waitFor(() => getByText('Fresh')); - - expect(freshBananaText.props.children).toBe('Fresh'); - } -); - -test.each([false, true])( - 'waits for assertion until timeout is met with fake timers (legacyFakeTimers = %s)', - async (legacyFakeTimers) => { - jest.useFakeTimers({ legacyFakeTimers }); - - const mockFn = jest.fn(() => { - throw Error('test'); - }); +test('waitFor timeout option takes precendence over `asyncWaitTimeout` config option', async () => { + configure({ asyncUtilTimeout: 2000 }); + const { getByText } = render(); - try { - await waitFor(() => mockFn(), { timeout: 400, interval: 200 }); - } catch (error) { - // suppress - } + fireEvent.press(getByText('Change freshness!')); + await expect( + waitFor(() => getByText('Fresh'), { timeout: 100 }) + ).rejects.toThrow(); - expect(mockFn).toHaveBeenCalledTimes(3); - } -); - -test.each([false, true])( - 'waits for assertion until timeout is met with fake timers (legacyFakeTimers = %s)', - async (legacyFakeTimers) => { - jest.useFakeTimers({ legacyFakeTimers }); - - const mockErrorFn = jest.fn(() => { - throw Error('test'); - }); - - const mockHandleFn = jest.fn((e) => e); - - try { - await waitFor(() => mockErrorFn(), { - timeout: 400, - interval: 200, - onTimeout: mockHandleFn, - }); - } catch (error) { - // suppress - } - - expect(mockErrorFn).toHaveBeenCalledTimes(3); - expect(mockHandleFn).toHaveBeenCalledTimes(1); - } -); - -test.each([false, true])( - 'awaiting something that succeeds before timeout works with fake timers (legacyFakeTimers = %s)', - async (legacyFakeTimers) => { - jest.useFakeTimers({ legacyFakeTimers }); - - let calls = 0; - const mockFn = jest.fn(() => { - calls += 1; - if (calls < 3) { - throw Error('test'); - } - }); - - try { - await waitFor(() => mockFn(), { timeout: 400, interval: 200 }); - } catch (error) { - // suppress - } - - expect(mockFn).toHaveBeenCalledTimes(3); - } -); + // Async action ends after 300ms and we only waited 100ms, so we need to wait + // for the remaining async actions to finish + await waitFor(() => getByText('Fresh')); +}); diff --git a/src/waitFor.ts b/src/waitFor.ts index 69d3295db..9a59531da 100644 --- a/src/waitFor.ts +++ b/src/waitFor.ts @@ -1,5 +1,6 @@ /* globals jest */ import act, { setReactActEnvironment, getIsReactActEnvironment } from './act'; +import { getConfig } from './config'; import { ErrorWithStack, copyStackTrace } from './helpers/errors'; import { setTimeout, @@ -9,7 +10,6 @@ import { } from './helpers/timers'; import { checkReactVersionAtLeast } from './react-versions'; -const DEFAULT_TIMEOUT = 1000; const DEFAULT_INTERVAL = 50; export type WaitForOptions = { @@ -22,7 +22,7 @@ export type WaitForOptions = { function waitForInternal( expectation: () => T, { - timeout = DEFAULT_TIMEOUT, + timeout = getConfig().asyncUtilTimeout, interval = DEFAULT_INTERVAL, stackTraceError, onTimeout, From 9d8eee9f275b9d9b111d63817bed6c29ee706545 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Tue, 27 Sep 2022 15:05:22 +0200 Subject: [PATCH 4/9] docs: --- website/docs/API.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/website/docs/API.md b/website/docs/API.md index bdc64622e..703e7d735 100644 --- a/website/docs/API.md +++ b/website/docs/API.md @@ -45,6 +45,10 @@ title: API - [Examples](#examples) - [With `initialProps`](#with-initialprops) - [With `wrapper`](#with-wrapper) +- [Configuration](#configuration) + - [`configure`](#configure) + - [`asyncUtilTimeout` option](#asyncutiltimeout-option) + - [`resetToDefaults()`](#resettodefaults) - [Accessibility](#accessibility) - [`isInaccessible`](#isinaccessible) @@ -714,6 +718,30 @@ it('should use context value', () => { }); ``` + +## Configuration + +### `configure` + +```ts +type Config = { + asyncUtilTimeout: number; +}; + +function configure(options: Partial) {} +``` + +#### `asyncUtilTimeout` option + +Default timeout, in ms, for async helper functions (`waitFor`, `waitForElementToBeRemoved`) and `findBy*` queries. + + +### `resetToDefaults()` + +```ts +function resetToDefaults() {} +``` + ## Accessibility ### `isInaccessible` From 0195e002ad6b8949ba5b3c9ab8c28a4b64be820b Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Tue, 27 Sep 2022 15:10:27 +0200 Subject: [PATCH 5/9] chore: flow types --- typings/index.flow.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/typings/index.flow.js b/typings/index.flow.js index a0f9acbc8..2658eda2c 100644 --- a/typings/index.flow.js +++ b/typings/index.flow.js @@ -367,6 +367,13 @@ declare module '@testing-library/react-native' { declare export var waitForElementToBeRemoved: WaitForElementToBeRemovedFunction; + declare interface Config { + asyncUtilTimeout: number; + } + + declare export var configure: (options: $Shape) => void; + declare export var resetToDefaults: () => void; + declare export var act: (callback: () => void) => Thenable; declare export var within: (instance: ReactTestInstance) => Queries; declare export var getQueriesForElement: ( From 0daa8a81d66b6597123c139561a5d762e85f183e Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Tue, 27 Sep 2022 15:13:51 +0200 Subject: [PATCH 6/9] docs: tweaks --- website/docs/API.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/API.md b/website/docs/API.md index 703e7d735..6d8adb34f 100644 --- a/website/docs/API.md +++ b/website/docs/API.md @@ -733,7 +733,7 @@ function configure(options: Partial) {} #### `asyncUtilTimeout` option -Default timeout, in ms, for async helper functions (`waitFor`, `waitForElementToBeRemoved`) and `findBy*` queries. +Default timeout, in ms, for async helper functions (`waitFor`, `waitForElementToBeRemoved`) and `findBy*` queries. Defaults to 1000 ms. ### `resetToDefaults()` From 701426dd0b495417a83ee87930bd133a3c2d8953 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Tue, 27 Sep 2022 15:14:34 +0200 Subject: [PATCH 7/9] fix: lint --- src/__tests__/waitFor.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/__tests__/waitFor.test.tsx b/src/__tests__/waitFor.test.tsx index 3b1051d89..e45ef882d 100644 --- a/src/__tests__/waitFor.test.tsx +++ b/src/__tests__/waitFor.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Text, TouchableOpacity, View, Pressable } from 'react-native'; +import { Text, TouchableOpacity, View } from 'react-native'; import { fireEvent, render, waitFor, configure, resetToDefaults } from '..'; class Banana extends React.Component { From 7a05fd8a71cfe15f5b2ac8979e3df728cd651f2e Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Tue, 27 Sep 2022 15:19:51 +0200 Subject: [PATCH 8/9] refactor: restore removed tests --- src/__tests__/waitFor.test.tsx | 147 ++++++++++++++++++++++++++++++++- 1 file changed, 146 insertions(+), 1 deletion(-) diff --git a/src/__tests__/waitFor.test.tsx b/src/__tests__/waitFor.test.tsx index e45ef882d..a35ebf171 100644 --- a/src/__tests__/waitFor.test.tsx +++ b/src/__tests__/waitFor.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Text, TouchableOpacity, View } from 'react-native'; +import { Text, TouchableOpacity, View, Pressable } from 'react-native'; import { fireEvent, render, waitFor, configure, resetToDefaults } from '..'; class Banana extends React.Component { @@ -42,6 +42,18 @@ afterEach(() => { jest.useRealTimers(); }); +test('waits for element until it stops throwing', async () => { + const { getByText, queryByText } = render(); + + fireEvent.press(getByText('Change freshness!')); + + expect(queryByText('Fresh')).toBeNull(); + + const freshBananaText = await waitFor(() => getByText('Fresh')); + + expect(freshBananaText.props.children).toBe('Fresh'); +}); + test('waits for element until timeout is met', async () => { const { getByText } = render(); @@ -81,3 +93,136 @@ test('waitFor timeout option takes precendence over `asyncWaitTimeout` config op // for the remaining async actions to finish await waitFor(() => getByText('Fresh')); }); + +test('waits for element with custom interval', async () => { + const mockFn = jest.fn(() => { + throw Error('test'); + }); + + try { + await waitFor(() => mockFn(), { timeout: 400, interval: 200 }); + } catch (e) { + // suppress + } + + expect(mockFn).toHaveBeenCalledTimes(2); +}); + +// this component is convoluted on purpose. It is not a good react pattern, but it is valid +// react code that will run differently between different react versions (17 and 18), so we need +// explicit tests for it +const Comp = ({ onPress }: { onPress: () => void }) => { + const [state, setState] = React.useState(false); + + React.useEffect(() => { + if (state) { + onPress(); + } + }, [state, onPress]); + + return ( + { + await Promise.resolve(); + setState(true); + }} + > + Trigger + + ); +}; + +test('waits for async event with fireEvent', async () => { + const spy = jest.fn(); + const { getByText } = render(); + + fireEvent.press(getByText('Trigger')); + + await waitFor(() => { + expect(spy).toHaveBeenCalled(); + }); +}); + +test.each([false, true])( + 'waits for element until it stops throwing using fake timers (legacyFakeTimers = %s)', + async (legacyFakeTimers) => { + jest.useFakeTimers({ legacyFakeTimers }); + const { getByText, queryByText } = render(); + + fireEvent.press(getByText('Change freshness!')); + expect(queryByText('Fresh')).toBeNull(); + + jest.advanceTimersByTime(300); + const freshBananaText = await waitFor(() => getByText('Fresh')); + + expect(freshBananaText.props.children).toBe('Fresh'); + } +); + +test.each([false, true])( + 'waits for assertion until timeout is met with fake timers (legacyFakeTimers = %s)', + async (legacyFakeTimers) => { + jest.useFakeTimers({ legacyFakeTimers }); + + const mockFn = jest.fn(() => { + throw Error('test'); + }); + + try { + await waitFor(() => mockFn(), { timeout: 400, interval: 200 }); + } catch (error) { + // suppress + } + + expect(mockFn).toHaveBeenCalledTimes(3); + } +); + +test.each([false, true])( + 'waits for assertion until timeout is met with fake timers (legacyFakeTimers = %s)', + async (legacyFakeTimers) => { + jest.useFakeTimers({ legacyFakeTimers }); + + const mockErrorFn = jest.fn(() => { + throw Error('test'); + }); + + const mockHandleFn = jest.fn((e) => e); + + try { + await waitFor(() => mockErrorFn(), { + timeout: 400, + interval: 200, + onTimeout: mockHandleFn, + }); + } catch (error) { + // suppress + } + + expect(mockErrorFn).toHaveBeenCalledTimes(3); + expect(mockHandleFn).toHaveBeenCalledTimes(1); + } +); + +test.each([false, true])( + 'awaiting something that succeeds before timeout works with fake timers (legacyFakeTimers = %s)', + async (legacyFakeTimers) => { + jest.useFakeTimers({ legacyFakeTimers }); + + let calls = 0; + const mockFn = jest.fn(() => { + calls += 1; + if (calls < 3) { + throw Error('test'); + } + }); + + try { + await waitFor(() => mockFn(), { timeout: 400, interval: 200 }); + } catch (error) { + // suppress + } + + expect(mockFn).toHaveBeenCalledTimes(3); + } +); From 9d24cc0a909a9ad9d1a483b343ed97174bec1cf0 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Tue, 27 Sep 2022 15:21:15 +0200 Subject: [PATCH 9/9] refactor: self code review --- src/__tests__/config.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/__tests__/config.test.ts b/src/__tests__/config.test.ts index 7a8b7de48..8d54d0925 100644 --- a/src/__tests__/config.test.ts +++ b/src/__tests__/config.test.ts @@ -8,12 +8,12 @@ test('getConfig() returns existing configuration', () => { expect(getConfig().asyncUtilTimeout).toEqual(1000); }); -test('configure() overrides existing values', () => { +test('configure() overrides existing config values', () => { configure({ asyncUtilTimeout: 5000 }); expect(getConfig().asyncUtilTimeout).toEqual(5000); }); -test('resetToDefaults() resets to defaults', () => { +test('resetToDefaults() resets config to defaults', () => { configure({ asyncUtilTimeout: 5000 }); expect(getConfig().asyncUtilTimeout).toEqual(5000);