Skip to content

Commit 8a2c8c1

Browse files
committed
internal: Fix timing based test race conditions
1 parent 07dce8f commit 8a2c8c1

File tree

2 files changed

+34
-44
lines changed

2 files changed

+34
-44
lines changed

packages/react/src/__tests__/integration-optimistic-endpoint.web.tsx

Lines changed: 30 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Endpoint, Entity } from '@data-client/endpoint';
22
import { CacheProvider } from '@data-client/react';
33
import { DataProvider as ExternalDataProvider } from '@data-client/react/redux';
4-
import { jest } from '@jest/globals';
4+
import { afterEach, jest } from '@jest/globals';
55
import {
66
OptimisticArticleResource,
77
ArticleResourceWithOtherListUrl,
@@ -17,7 +17,7 @@ import { useContext } from 'react';
1717

1818
import { makeRenderDataClient, act } from '../../../test';
1919
import { StateContext } from '../context';
20-
import { useCache, useController, useSuspense } from '../hooks';
20+
import { useCache, useSuspense } from '../hooks';
2121
import { useError } from '../hooks';
2222
import {
2323
payload,
@@ -446,6 +446,7 @@ describe.each([
446446
});
447447

448448
it('should clear only earlier optimistic updates when a promise resolves', async () => {
449+
jest.useFakeTimers({ legacyFakeTimers: false });
449450
const params = { id: payload.id };
450451
const { result, controller } = renderDataClient(
451452
() => {
@@ -462,9 +463,7 @@ describe.each([
462463
],
463464
},
464465
);
465-
await act(async () => {
466-
await new Promise(resolve => setTimeout(resolve, 0));
467-
});
466+
jest.advanceTimersByTime(23);
468467

469468
const fetches: Promise<any>[] = [];
470469
const resolves: ((v: any) => void)[] = [];
@@ -495,9 +494,7 @@ describe.each([
495494
content: 'firstoptimistic',
496495
}),
497496
);
498-
await act(async () => {
499-
await new Promise(resolve => setTimeout(resolve, 1));
500-
});
497+
jest.advanceTimersByTime(23);
501498

502499
// second optimistic
503500
act(() => {
@@ -524,9 +521,7 @@ describe.each([
524521
content: 'firstoptimistic',
525522
}),
526523
);
527-
await act(async () => {
528-
await new Promise(resolve => setTimeout(resolve, 0));
529-
});
524+
jest.advanceTimersByTime(23);
530525

531526
// third optimistic
532527
act(() => {
@@ -556,9 +551,7 @@ describe.each([
556551
);
557552

558553
// resolve second request while first is in flight
559-
act(() => {
560-
setTimeout(() => resolves[1]({ ...payload, title: 'second' }), 1);
561-
});
554+
resolves[1]({ ...payload, title: 'second' });
562555
await act(() => fetches[1]);
563556

564557
// first and second optimistic should be cleared with only third optimistic left to be layerd
@@ -573,16 +566,10 @@ describe.each([
573566

574567
// resolve the first fetch; but the second fetch happened after so we use it first since this is default 'first fetchedAt' behavior
575568
// this can be solved by either canceling requests or having server send the total order
576-
act(() => {
577-
setTimeout(
578-
() =>
579-
resolves[0]({
580-
...payload,
581-
title: 'first',
582-
content: 'first',
583-
}),
584-
1,
585-
);
569+
resolves[0]({
570+
...payload,
571+
title: 'first',
572+
content: 'first',
586573
});
587574
await act(() => fetches[0]);
588575
expect(result.current.article).toEqual(
@@ -592,9 +579,12 @@ describe.each([
592579
tags: ['thirdoptimistic'],
593580
}),
594581
);
582+
jest.useRealTimers();
583+
await renderDataClient.allSettled();
595584
});
596585

597586
it('should clear optimistic when server response resolves in order', async () => {
587+
jest.useFakeTimers({ legacyFakeTimers: false });
598588
const params = { id: payload.id };
599589
const { result, controller } = renderDataClient(
600590
() => {
@@ -610,9 +600,7 @@ describe.each([
610600
],
611601
},
612602
);
613-
await act(async () => {
614-
await new Promise(resolve => setTimeout(resolve, 1));
615-
});
603+
jest.advanceTimersByTime(23);
616604

617605
const fetches: Promise<any>[] = [];
618606
const resolves: ((v: any) => void)[] = [];
@@ -643,9 +631,7 @@ describe.each([
643631
content: 'firstoptimistic',
644632
}),
645633
);
646-
await act(async () => {
647-
await new Promise(resolve => setTimeout(resolve, 0));
648-
});
634+
jest.advanceTimersByTime(23);
649635

650636
// second optimistic
651637
act(() => {
@@ -672,9 +658,7 @@ describe.each([
672658
content: 'firstoptimistic',
673659
}),
674660
);
675-
await act(async () => {
676-
await new Promise(resolve => setTimeout(resolve, 0));
677-
});
661+
jest.advanceTimersByTime(23);
678662

679663
// third optimistic
680664
act(() => {
@@ -702,11 +686,10 @@ describe.each([
702686
tags: ['thirdoptimistic'],
703687
}),
704688
);
689+
jest.advanceTimersByTime(23);
705690

706691
// resolve first request
707-
act(() => {
708-
setTimeout(() => resolves[0]({ ...payload, content: 'first' }), 0);
709-
});
692+
resolves[0]({ ...payload, content: 'first' });
710693
await act(() => fetches[0]);
711694

712695
// replace optimistic with response
@@ -725,7 +708,7 @@ describe.each([
725708
title: 'second',
726709
content: 'first',
727710
});
728-
await act(() => fetches[0]);
711+
await act(() => fetches[1]);
729712
expect(result.current).toEqual(
730713
CoolerArticle.fromJS({
731714
...payload,
@@ -742,7 +725,7 @@ describe.each([
742725
content: 'first',
743726
tags: ['third'],
744727
});
745-
await act(() => fetches[0]);
728+
await act(() => fetches[2]);
746729
expect(result.current).toEqual(
747730
CoolerArticle.fromJS({
748731
...payload,
@@ -751,6 +734,8 @@ describe.each([
751734
tags: ['third'],
752735
}),
753736
);
737+
jest.useRealTimers();
738+
await renderDataClient.allSettled();
754739
});
755740

756741
describe('race conditions', () => {
@@ -1033,11 +1018,16 @@ describe.each([
10331018
});
10341019

10351020
describe('with timestamps', () => {
1021+
beforeEach(() => {
1022+
jest.useFakeTimers({ legacyFakeTimers: false });
1023+
});
1024+
afterEach(() => {
1025+
jest.useRealTimers();
1026+
});
1027+
afterEach;
10361028
it.each([VisSettingsResource, VisSettingsResourceFromMixin])(
10371029
'should handle out of order server responses (%#)',
10381030
async VisResource => {
1039-
jest.useFakeTimers({ legacyFakeTimers: false });
1040-
10411031
const initVis = {
10421032
id: 5,
10431033
visType: 'graph',
@@ -1166,7 +1156,6 @@ describe.each([
11661156
expect(result.current.vis).toEqual(finalObject);
11671157

11681158
fetchSpy.mockClear();
1169-
jest.useRealTimers();
11701159
await renderDataClient.allSettled();
11711160
},
11721161
);

website/src/components/Playground/editor-types/react.d.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ declare namespace React {
132132
type JSXElementConstructor<P> =
133133
| ((
134134
props: P,
135-
) => ReactNode)
135+
) => ReactNode | Promise<ReactNode>)
136136
// constructor signature must match React.Component
137137
| (new(props: P) => Component<any, any>);
138138

@@ -1036,7 +1036,7 @@ declare namespace React {
10361036
* ```
10371037
*/
10381038
interface FunctionComponent<P = {}> {
1039-
(props: P): ReactNode;
1039+
(props: P): ReactNode | Promise<ReactNode>;
10401040
/**
10411041
* Ignored by React.
10421042
* @deprecated Only kept in types for backwards compatibility. Will be removed in a future major release.
@@ -1835,10 +1835,11 @@ declare namespace React {
18351835
* A good example of this is a text input.
18361836
*
18371837
* @param value The value that is going to be deferred
1838+
* @param initialValue A value to use during the initial render of a component. If this option is omitted, `useDeferredValue` will not defer during the initial render, because there’s no previous version of `value` that it can render instead.
18381839
*
18391840
* @see {@link https://react.dev/reference/react/useDeferredValue}
18401841
*/
1841-
export function useDeferredValue<T>(value: T): T;
1842+
export function useDeferredValue<T>(value: T, initialValue?: T): T;
18421843

18431844
/**
18441845
* Allows components to avoid undesirable loading states by waiting for content to load

0 commit comments

Comments
 (0)