Skip to content

Commit 8ec5f2b

Browse files
committed
tweaks
1 parent 70ca5ee commit 8ec5f2b

File tree

5 files changed

+106
-115
lines changed

5 files changed

+106
-115
lines changed

src/__tests__/wait-for.test.tsx

Lines changed: 87 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as React from 'react';
22
import { Pressable, Text, TouchableOpacity, View } from 'react-native';
33

44
import { configure, fireEvent, render, screen, waitFor } from '..';
5-
import { useTimerType } from '../test-utils/timers';
5+
import { setupTimeType, TimerType } from '../test-utils/timers';
66

77
beforeEach(() => {
88
jest.useRealTimers();
@@ -24,26 +24,26 @@ test('waits for expect() assertion to pass', async () => {
2424
expect(mockFunction).toHaveBeenCalledTimes(1);
2525
});
2626

27-
test.each([
28-
{ timerType: 'real' as const },
29-
{ timerType: 'fake' as const },
30-
{ timerType: 'fake-legacy' as const },
31-
])('waits for query with $timerType timers', async ({ timerType }) => {
32-
function AsyncComponent() {
33-
const [text, setText] = React.useState('Loading...');
27+
test.each(['real', 'fake', 'fake-legacy'] as const)(
28+
'waits for query with %s timers',
29+
async (timerType) => {
30+
setupTimeType(timerType as TimerType);
31+
function AsyncComponent() {
32+
const [text, setText] = React.useState('Loading...');
3433

35-
React.useEffect(() => {
36-
setTimeout(() => setText('Loaded'), 100);
37-
}, []);
34+
React.useEffect(() => {
35+
setTimeout(() => setText('Loaded'), 100);
36+
}, []);
3837

39-
return <Text>{text}</Text>;
40-
}
38+
return <Text>{text}</Text>;
39+
}
4140

42-
useTimerType(timerType);
43-
await render(<AsyncComponent />);
44-
await waitFor(() => screen.getByText('Loaded'));
45-
expect(screen.getByText('Loaded')).toBeOnTheScreen();
46-
});
41+
setupTimeType(timerType);
42+
await render(<AsyncComponent />);
43+
await waitFor(() => screen.getByText('Loaded'));
44+
expect(screen.getByText('Loaded')).toBeOnTheScreen();
45+
},
46+
);
4747

4848
test('throws timeout error when condition never becomes true', async () => {
4949
function Component() {
@@ -68,6 +68,17 @@ test('uses custom error from onTimeout callback when timeout occurs', async () =
6868
).rejects.toThrow(customErrorMessage);
6969
});
7070

71+
test('onTimeout callback returning falsy value keeps original error', async () => {
72+
await render(<View />);
73+
// When onTimeout returns null/undefined/false, the original error should be kept (line 181 false branch)
74+
await expect(
75+
waitFor(() => screen.getByText('Never appears'), {
76+
timeout: 100,
77+
onTimeout: () => null as any,
78+
}),
79+
).rejects.toThrow('Unable to find an element with text: Never appears');
80+
});
81+
7182
test('throws TypeError when expectation is not a function', async () => {
7283
await expect(waitFor(null as any)).rejects.toThrow(
7384
'Received `expectation` arg must be a function',
@@ -152,7 +163,9 @@ test('throws timeout error with fake timers when condition never becomes true',
152163

153164
await jest.advanceTimersByTimeAsync(100);
154165

155-
await expect(waitForPromise).rejects.toThrow('Unable to find an element with text: Never appears');
166+
await expect(waitForPromise).rejects.toThrow(
167+
'Unable to find an element with text: Never appears',
168+
);
156169
});
157170

158171
test('throws generic timeout error when promise rejects with falsy value until timeout', async () => {
@@ -178,73 +191,61 @@ test('waits for element with custom interval', async () => {
178191
});
179192

180193
test('waitFor defaults to asyncUtilTimeout config option', async () => {
181-
class BananaContainer extends React.Component<object, any> {
182-
state = { fresh: false };
194+
function Component() {
195+
const [active, setActive] = React.useState(false);
183196

184-
onChangeFresh = async () => {
185-
await new Promise((resolve) => setTimeout(resolve, 300));
186-
this.setState({ fresh: true });
197+
const handlePress = () => {
198+
setTimeout(() => setActive(true), 300);
187199
};
188200

189-
render() {
190-
return (
191-
<View>
192-
{this.state.fresh && <Text>Fresh</Text>}
193-
<TouchableOpacity onPress={this.onChangeFresh}>
194-
<Text>Change freshness!</Text>
195-
</TouchableOpacity>
196-
</View>
197-
);
198-
}
201+
return (
202+
<View>
203+
{active && <Text>Active</Text>}
204+
<Pressable onPress={handlePress}>
205+
<Text>Activate</Text>
206+
</Pressable>
207+
</View>
208+
);
199209
}
200210

201211
configure({ asyncUtilTimeout: 100 });
202-
await render(<BananaContainer />);
203-
204-
fireEvent.press(screen.getByText('Change freshness!'));
205-
206-
expect(screen.queryByText('Fresh')).toBeNull();
207-
208-
await expect(waitFor(() => screen.getByText('Fresh'))).rejects.toThrow();
212+
await render(<Component />);
213+
await fireEvent.press(screen.getByText('Activate'));
214+
expect(screen.queryByText('Active')).toBeNull();
215+
await expect(waitFor(() => screen.getByText('Active'))).rejects.toThrow();
209216

210217
// Async action ends after 300ms and we only waited 100ms, so we need to wait
211218
// for the remaining async actions to finish
212-
await waitFor(() => screen.getByText('Fresh'), { timeout: 1000 });
219+
await waitFor(() => screen.getByText('Active'), { timeout: 1000 });
213220
});
214221

215222
test('waitFor timeout option takes precedence over asyncUtilTimeout config option', async () => {
216-
class BananaContainer extends React.Component<object, any> {
217-
state = { fresh: false };
223+
function AsyncTextToggle() {
224+
const [active, setActive] = React.useState(false);
218225

219-
onChangeFresh = async () => {
220-
await new Promise((resolve) => setTimeout(resolve, 300));
221-
this.setState({ fresh: true });
226+
const handlePress = () => {
227+
setTimeout(() => setActive(true), 300);
222228
};
223229

224-
render() {
225-
return (
226-
<View>
227-
{this.state.fresh && <Text>Fresh</Text>}
228-
<TouchableOpacity onPress={this.onChangeFresh}>
229-
<Text>Change freshness!</Text>
230-
</TouchableOpacity>
231-
</View>
232-
);
233-
}
230+
return (
231+
<View>
232+
{active && <Text>Active</Text>}
233+
<TouchableOpacity onPress={handlePress}>
234+
<Text>Activate</Text>
235+
</TouchableOpacity>
236+
</View>
237+
);
234238
}
235239

236240
configure({ asyncUtilTimeout: 2000 });
237-
await render(<BananaContainer />);
238-
239-
fireEvent.press(screen.getByText('Change freshness!'));
240-
241-
expect(screen.queryByText('Fresh')).toBeNull();
242-
243-
await expect(waitFor(() => screen.getByText('Fresh'), { timeout: 100 })).rejects.toThrow();
241+
await render(<AsyncTextToggle />);
242+
await fireEvent.press(screen.getByText('Activate'));
243+
expect(screen.queryByText('Active')).toBeNull();
244+
await expect(waitFor(() => screen.getByText('Active'), { timeout: 100 })).rejects.toThrow();
244245

245246
// Async action ends after 300ms and we only waited 100ms, so we need to wait
246247
// for the remaining async actions to finish
247-
await waitFor(() => screen.getByText('Fresh'));
248+
await waitFor(() => screen.getByText('Active'));
248249
});
249250

250251
test('waits for async event with fireEvent', async () => {
@@ -279,18 +280,12 @@ test('waits for async event with fireEvent', async () => {
279280
});
280281
});
281282

282-
test.each([
283-
[false, false],
284-
[true, false],
285-
[true, true],
286-
])(
287-
'flushes scheduled updates before returning (fakeTimers = %s, legacyFakeTimers = %s)',
288-
async (fakeTimers, legacyFakeTimers) => {
289-
if (fakeTimers) {
290-
jest.useFakeTimers({ legacyFakeTimers });
291-
}
283+
test.each(['real', 'fake', 'fake-legacy'] as const)(
284+
'flushes scheduled updates before returning (timerType = %s)',
285+
async (timerType) => {
286+
setupTimeType(timerType);
292287

293-
function Apple({ onPress }: { onPress: (color: string) => void }) {
288+
function Component({ onPress }: { onPress: (color: string) => void }) {
294289
const [color, setColor] = React.useState('green');
295290
const [syncedColor, setSyncedColor] = React.useState(color);
296291

@@ -316,7 +311,7 @@ test.each([
316311
}
317312

318313
const onPress = jest.fn();
319-
await render(<Apple onPress={onPress} />);
314+
await render(<Component onPress={onPress} />);
320315

321316
// Required: this `waitFor` will succeed on first check, because the "root" view is there
322317
// since the initial mount.
@@ -332,27 +327,27 @@ test.each([
332327
},
333328
);
334329

335-
test.each([true, false])(
336-
'it should not depend on real time when using fake timers (legacyFakeTimers = %s)',
337-
async (legacyFakeTimers) => {
338-
jest.useFakeTimers({ legacyFakeTimers });
330+
test.each(['fake', 'fake-legacy'] as const)(
331+
'it should not depend on real time when using %s timers',
332+
async (timerType) => {
333+
setupTimeType(timerType);
339334
const WAIT_FOR_INTERVAL = 20;
340335
const WAIT_FOR_TIMEOUT = WAIT_FOR_INTERVAL * 5;
341336

342-
const blockThread = (timeToBlockThread: number, legacyFakeTimers: boolean) => {
337+
const blockThread = (timeToBlockThread: number, timerType: TimerType): void => {
343338
jest.useRealTimers();
344339
const end = Date.now() + timeToBlockThread;
345340

346341
while (Date.now() < end) {
347342
// do nothing
348343
}
349344

350-
jest.useFakeTimers({ legacyFakeTimers });
345+
setupTimeType(timerType);
351346
};
352347

353348
const mockErrorFn = jest.fn(() => {
354349
// Wait 2 times interval so that check time is longer than interval
355-
blockThread(WAIT_FOR_INTERVAL * 2, legacyFakeTimers);
350+
blockThread(WAIT_FOR_INTERVAL * 2, timerType);
356351
throw new Error('test');
357352
});
358353

@@ -371,10 +366,10 @@ test.each([true, false])(
371366
},
372367
);
373368

374-
test.each([false, true])(
375-
'waits for assertion until timeout is met with fake timers and interval (legacyFakeTimers = %s)',
376-
async (legacyFakeTimers) => {
377-
jest.useFakeTimers({ legacyFakeTimers });
369+
test.each(['fake', 'fake-legacy'] as const)(
370+
'waits for assertion until timeout is met with %s timers and interval',
371+
async (timerType) => {
372+
setupTimeType(timerType);
378373

379374
const mockFn = jest.fn(() => {
380375
throw Error('test');
@@ -390,10 +385,10 @@ test.each([false, true])(
390385
},
391386
);
392387

393-
test.each([false, true])(
394-
'waits for assertion until timeout is met with fake timers, interval, and onTimeout (legacyFakeTimers = %s)',
395-
async (legacyFakeTimers) => {
396-
jest.useFakeTimers({ legacyFakeTimers });
388+
test.each(['fake', 'fake-legacy'] as const)(
389+
'waits for assertion until timeout is met with %s timers, interval, and onTimeout',
390+
async (timerType) => {
391+
setupTimeType(timerType);
397392

398393
const mockErrorFn = jest.fn(() => {
399394
throw Error('test');

src/helpers/__tests__/errors.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { copyStackTrace, ErrorWithStack } from '../errors';
1+
import { copyStackTraceIfNeeded, ErrorWithStack } from '../errors';
22

33
describe('ErrorWithStack', () => {
44
test('should create an error with message', () => {
@@ -35,15 +35,15 @@ describe('copyStackTrace', () => {
3535
const source = new Error('Source error');
3636
source.stack = 'Error: Source error\n at test.js:1:1';
3737

38-
copyStackTrace(target, source);
38+
copyStackTraceIfNeeded(target, source);
3939
expect(target.stack).toBe('Error: Target error\n at test.js:1:1');
4040

4141
const target2 = new Error('Target error');
4242
const source2 = new Error('Source error');
4343
source2.stack =
4444
'Error: Source error\n at test.js:1:1\nError: Source error\n at test.js:2:2';
4545

46-
copyStackTrace(target2, source2);
46+
copyStackTraceIfNeeded(target2, source2);
4747
// Should replace only the first occurrence
4848
expect(target2.stack).toBe(
4949
'Error: Target error\n at test.js:1:1\nError: Source error\n at test.js:2:2',
@@ -55,20 +55,20 @@ describe('copyStackTrace', () => {
5555
const source = new Error('Source error');
5656
source.stack = 'Error: Source error\n at test.js:1:1';
5757

58-
copyStackTrace(targetNotError, source);
58+
copyStackTraceIfNeeded(targetNotError, source);
5959
expect(targetNotError).toEqual({ message: 'Not an error' });
6060

6161
const target = new Error('Target error');
6262
const originalStack = target.stack;
6363
const sourceNotError = { message: 'Not an error' };
6464

65-
copyStackTrace(target, sourceNotError as Error);
65+
copyStackTraceIfNeeded(target, sourceNotError as Error);
6666
expect(target.stack).toBe(originalStack);
6767

6868
const sourceNoStack = new Error('Source error');
6969
delete sourceNoStack.stack;
7070

71-
copyStackTrace(target, sourceNoStack);
71+
copyStackTraceIfNeeded(target, sourceNoStack);
7272
expect(target.stack).toBe(originalStack);
7373
});
7474
});

src/helpers/errors.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ export class ErrorWithStack extends Error {
88
}
99
}
1010

11-
export function copyStackTrace(target: unknown, stackTraceSource: Error) {
12-
if (target instanceof Error && stackTraceSource.stack) {
11+
export function copyStackTraceIfNeeded(target: unknown, stackTraceSource: Error | undefined) {
12+
if (stackTraceSource != null && target instanceof Error && stackTraceSource.stack) {
1313
target.stack = stackTraceSource.stack.replace(stackTraceSource.message, target.message);
1414
}
1515
}

src/test-utils/timers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export type TimerType = 'real' | 'fake' | 'fake-legacy';
22

3-
export function useTimerType(type: TimerType): void {
3+
export function setupTimeType(type: TimerType): void {
44
if (type === 'fake-legacy') {
55
jest.useFakeTimers({ legacyFakeTimers: true });
66
} else if (type === 'fake') {

0 commit comments

Comments
 (0)