@assistant-ui/tap 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -6
- package/dist/core/ResourceFiber.d.ts +5 -5
- package/dist/core/ResourceFiber.d.ts.map +1 -1
- package/dist/core/ResourceFiber.js +26 -18
- package/dist/core/ResourceFiber.js.map +1 -1
- package/dist/core/createTapRoot.d.ts +9 -0
- package/dist/core/createTapRoot.d.ts.map +1 -0
- package/dist/core/createTapRoot.js +27 -0
- package/dist/core/createTapRoot.js.map +1 -0
- package/dist/core/helpers/commit.d.ts +1 -1
- package/dist/core/helpers/commit.d.ts.map +1 -1
- package/dist/core/helpers/commit.js +6 -1
- package/dist/core/helpers/commit.js.map +1 -1
- package/dist/core/helpers/execution-context.d.ts +4 -5
- package/dist/core/helpers/execution-context.d.ts.map +1 -1
- package/dist/core/helpers/execution-context.js +1 -7
- package/dist/core/helpers/execution-context.js.map +1 -1
- package/dist/core/helpers/root.d.ts +3 -2
- package/dist/core/helpers/root.d.ts.map +1 -1
- package/dist/core/helpers/root.js +19 -15
- package/dist/core/helpers/root.js.map +1 -1
- package/dist/core/react-dispatcher.d.ts.map +1 -1
- package/dist/core/react-dispatcher.js +17 -16
- package/dist/core/react-dispatcher.js.map +1 -1
- package/dist/core/resource.d.ts +2 -4
- package/dist/core/resource.d.ts.map +1 -1
- package/dist/core/resource.js +5 -10
- package/dist/core/resource.js.map +1 -1
- package/dist/core/scheduler.d.ts +2 -2
- package/dist/core/scheduler.d.ts.map +1 -1
- package/dist/core/scheduler.js +2 -2
- package/dist/core/scheduler.js.map +1 -1
- package/dist/core/types.d.ts +27 -25
- package/dist/core/types.d.ts.map +1 -1
- package/dist/hooks/useResource.d.ts +2 -2
- package/dist/hooks/useResource.d.ts.map +1 -1
- package/dist/hooks/useResource.js +14 -20
- package/dist/hooks/useResource.js.map +1 -1
- package/dist/hooks/useResources.d.ts +1 -1
- package/dist/hooks/useResources.d.ts.map +1 -1
- package/dist/hooks/useResources.js +18 -27
- package/dist/hooks/useResources.js.map +1 -1
- package/dist/hooks/useTapHost.d.ts +21 -0
- package/dist/hooks/useTapHost.d.ts.map +1 -0
- package/dist/hooks/useTapHost.js +30 -0
- package/dist/hooks/useTapHost.js.map +1 -0
- package/dist/hooks/useTapRoot.d.ts +18 -0
- package/dist/hooks/useTapRoot.d.ts.map +1 -0
- package/dist/hooks/useTapRoot.js +77 -0
- package/dist/hooks/useTapRoot.js.map +1 -0
- package/dist/hooks/utils/depsShallowEqual.d.ts.map +1 -1
- package/dist/hooks/utils/depsShallowEqual.js +5 -2
- package/dist/hooks/utils/depsShallowEqual.js.map +1 -1
- package/dist/hooks/utils/useCell.d.ts +2 -2
- package/dist/hooks/utils/useCell.d.ts.map +1 -1
- package/dist/hooks/utils/useCell.js.map +1 -1
- package/dist/hooks/utils/useDevStrictMode.d.ts +5 -0
- package/dist/hooks/utils/useDevStrictMode.d.ts.map +1 -0
- package/dist/hooks/utils/useDevStrictMode.js +25 -0
- package/dist/hooks/utils/useDevStrictMode.js.map +1 -0
- package/dist/hooks/utils/useRenderMemo.d.ts +5 -0
- package/dist/hooks/utils/useRenderMemo.d.ts.map +1 -0
- package/dist/hooks/utils/useRenderMemo.js +25 -0
- package/dist/hooks/utils/useRenderMemo.js.map +1 -0
- package/dist/hooks/utils/useResourceFiberHostUtils.d.ts +10 -0
- package/dist/hooks/utils/useResourceFiberHostUtils.d.ts.map +1 -0
- package/dist/hooks/utils/useResourceFiberHostUtils.js +46 -0
- package/dist/hooks/utils/useResourceFiberHostUtils.js.map +1 -0
- package/dist/index.d.ts +7 -4
- package/dist/index.js +7 -4
- package/dist/{hooks → react-hooks}/index.d.ts +6 -6
- package/dist/{hooks → react-hooks}/index.js +5 -5
- package/dist/{hooks → react-hooks}/use.d.ts +1 -1
- package/dist/{hooks → react-hooks}/use.d.ts.map +1 -1
- package/dist/{hooks → react-hooks}/use.js +1 -1
- package/dist/react-hooks/use.js.map +1 -0
- package/dist/{hooks → react-hooks}/useCallback.d.ts +1 -1
- package/dist/react-hooks/useCallback.d.ts.map +1 -0
- package/dist/{hooks → react-hooks}/useCallback.js +1 -1
- package/dist/react-hooks/useCallback.js.map +1 -0
- package/dist/{hooks → react-hooks}/useEffect.d.ts +1 -1
- package/dist/react-hooks/useEffect.d.ts.map +1 -0
- package/dist/react-hooks/useEffect.js +35 -0
- package/dist/react-hooks/useEffect.js.map +1 -0
- package/dist/{hooks → react-hooks}/useEffectEvent.d.ts +1 -1
- package/dist/react-hooks/useEffectEvent.d.ts.map +1 -0
- package/dist/{hooks → react-hooks}/useEffectEvent.js +2 -2
- package/dist/react-hooks/useEffectEvent.js.map +1 -0
- package/dist/{hooks → react-hooks}/useMemo.d.ts +1 -1
- package/dist/react-hooks/useMemo.d.ts.map +1 -0
- package/dist/{hooks → react-hooks}/useMemo.js +3 -3
- package/dist/react-hooks/useMemo.js.map +1 -0
- package/dist/{hooks → react-hooks}/useMemoCache.d.ts +1 -1
- package/dist/react-hooks/useMemoCache.d.ts.map +1 -0
- package/dist/{hooks → react-hooks}/useMemoCache.js +1 -1
- package/dist/react-hooks/useMemoCache.js.map +1 -0
- package/dist/react-hooks/useReducer.d.ts +9 -0
- package/dist/react-hooks/useReducer.d.ts.map +1 -0
- package/dist/react-hooks/useReducer.js +120 -0
- package/dist/react-hooks/useReducer.js.map +1 -0
- package/dist/{hooks → react-hooks}/useRef.d.ts +1 -1
- package/dist/react-hooks/useRef.d.ts.map +1 -0
- package/dist/{hooks → react-hooks}/useRef.js +1 -1
- package/dist/react-hooks/useRef.js.map +1 -0
- package/dist/{hooks → react-hooks}/useState.d.ts +1 -1
- package/dist/react-hooks/useState.d.ts.map +1 -0
- package/dist/{hooks → react-hooks}/useState.js +3 -3
- package/dist/react-hooks/useState.js.map +1 -0
- package/dist/react-shim/index.d.ts +8 -10
- package/dist/react-shim/index.d.ts.map +1 -1
- package/dist/react-shim/index.js +19 -19
- package/dist/react-shim/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/basic/resourceHandle.test.ts +32 -22
- package/src/__tests__/basic/tapEffect.basic.test.ts +8 -8
- package/src/__tests__/basic/tapReducer.basic.test.ts +16 -14
- package/src/__tests__/basic/tapResources.basic.test.ts +19 -16
- package/src/__tests__/basic/tapState.basic.test.ts +11 -11
- package/src/__tests__/bench/hosts.bench.tsx +124 -0
- package/src/__tests__/bench/tree.bench.tsx +166 -0
- package/src/__tests__/errors/errors.effect-errors.test.ts +12 -13
- package/src/__tests__/errors/errors.render-errors.test.ts +65 -22
- package/src/__tests__/lifecycle/lifecycle.dependencies.test.ts +19 -19
- package/src/__tests__/lifecycle/lifecycle.mount-unmount.test.ts +14 -14
- package/src/__tests__/parity/describeParity.tsx +217 -0
- package/src/__tests__/parity/parity.adversarial.test.tsx +375 -0
- package/src/__tests__/parity/parity.basics.test.tsx +281 -0
- package/src/__tests__/parity/parity.divergences.test.tsx +208 -0
- package/src/__tests__/parity/parity.smoke.test.tsx +43 -0
- package/src/__tests__/react/concurrent-mode.test.tsx +10 -6
- package/src/__tests__/react/concurrent-pending-updates.test.tsx +351 -0
- package/src/__tests__/react/concurrent-render-phase.test.tsx +350 -0
- package/src/__tests__/react/react-shim.test.tsx +1 -1
- package/src/__tests__/react/useResource.test.tsx +41 -26
- package/src/__tests__/react/useTapHost.test.tsx +233 -0
- package/src/__tests__/react-dispatcher.test.ts +4 -4
- package/src/__tests__/rules/rules.hook-count.test.ts +21 -21
- package/src/__tests__/rules/rules.hook-order.test.ts +17 -17
- package/src/__tests__/strictmode/strictmode-parity.test.tsx +420 -0
- package/src/__tests__/strictmode/strictmode.test.ts +39 -209
- package/src/__tests__/test-utils.ts +33 -23
- package/src/core/ResourceFiber.ts +43 -35
- package/src/core/createTapRoot.ts +45 -0
- package/src/core/helpers/commit.ts +12 -2
- package/src/core/helpers/execution-context.ts +4 -13
- package/src/core/helpers/root.ts +24 -12
- package/src/core/react-dispatcher.ts +14 -13
- package/src/core/resource.ts +5 -20
- package/src/core/scheduler.ts +1 -1
- package/src/core/types.ts +27 -21
- package/src/hooks/useResource.ts +18 -27
- package/src/hooks/useResources.ts +18 -42
- package/src/hooks/useTapHost.ts +60 -0
- package/src/hooks/useTapRoot.ts +135 -0
- package/src/hooks/utils/depsShallowEqual.ts +12 -2
- package/src/hooks/utils/useCell.ts +2 -2
- package/src/hooks/utils/useDevStrictMode.ts +34 -0
- package/src/hooks/utils/useRenderMemo.ts +27 -0
- package/src/hooks/utils/useResourceFiberHostUtils.ts +61 -0
- package/src/index.ts +6 -3
- package/src/{hooks → react-hooks}/index.ts +4 -4
- package/src/react-hooks/useEffect.ts +58 -0
- package/src/{hooks → react-hooks}/useMemo.ts +1 -1
- package/src/react-hooks/useReducer.ts +254 -0
- package/src/{hooks → react-hooks}/useState.ts +2 -2
- package/src/react-shim/index.ts +24 -13
- package/dist/core/createResourceRoot.d.ts +0 -11
- package/dist/core/createResourceRoot.d.ts.map +0 -1
- package/dist/core/createResourceRoot.js +0 -31
- package/dist/core/createResourceRoot.js.map +0 -1
- package/dist/core/helpers/callResourceFn.d.ts +0 -1
- package/dist/core/helpers/callResourceFn.js +0 -19
- package/dist/core/helpers/callResourceFn.js.map +0 -1
- package/dist/hooks/use.js.map +0 -1
- package/dist/hooks/useCallback.d.ts.map +0 -1
- package/dist/hooks/useCallback.js.map +0 -1
- package/dist/hooks/useEffect.d.ts.map +0 -1
- package/dist/hooks/useEffect.js +0 -40
- package/dist/hooks/useEffect.js.map +0 -1
- package/dist/hooks/useEffectEvent.d.ts.map +0 -1
- package/dist/hooks/useEffectEvent.js.map +0 -1
- package/dist/hooks/useMemo.d.ts.map +0 -1
- package/dist/hooks/useMemo.js.map +0 -1
- package/dist/hooks/useMemoCache.d.ts.map +0 -1
- package/dist/hooks/useMemoCache.js.map +0 -1
- package/dist/hooks/useReducer.d.ts +0 -21
- package/dist/hooks/useReducer.d.ts.map +0 -1
- package/dist/hooks/useReducer.js +0 -81
- package/dist/hooks/useReducer.js.map +0 -1
- package/dist/hooks/useRef.d.ts.map +0 -1
- package/dist/hooks/useRef.js.map +0 -1
- package/dist/hooks/useResourceRoot.d.ts +0 -20
- package/dist/hooks/useResourceRoot.d.ts.map +0 -1
- package/dist/hooks/useResourceRoot.js +0 -77
- package/dist/hooks/useResourceRoot.js.map +0 -1
- package/dist/hooks/useState.d.ts.map +0 -1
- package/dist/hooks/useState.js.map +0 -1
- package/dist/react/hooks.d.ts +0 -25
- package/dist/react/hooks.d.ts.map +0 -1
- package/dist/react/hooks.js +0 -69
- package/dist/react/hooks.js.map +0 -1
- package/src/__tests__/strictmode/react-strictmode-behavior.test.tsx +0 -920
- package/src/__tests__/strictmode/react-strictmode-rerender-sources.test.tsx +0 -488
- package/src/__tests__/strictmode/tap-strictmode-rerender-sources.test.ts +0 -687
- package/src/core/createResourceRoot.ts +0 -53
- package/src/core/helpers/callResourceFn.ts +0 -21
- package/src/hooks/useEffect.ts +0 -72
- package/src/hooks/useReducer.ts +0 -160
- package/src/hooks/useResourceRoot.ts +0 -130
- package/src/react/hooks.ts +0 -112
- /package/src/{hooks → react-hooks}/use.ts +0 -0
- /package/src/{hooks → react-hooks}/useCallback.ts +0 -0
- /package/src/{hooks → react-hooks}/useEffectEvent.ts +0 -0
- /package/src/{hooks → react-hooks}/useMemoCache.ts +0 -0
- /package/src/{hooks → react-hooks}/useRef.ts +0 -0
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Differential StrictMode parity suite.
|
|
3
|
+
*
|
|
4
|
+
* tap's hooks are React's hooks (imported from "react" and resolved through the
|
|
5
|
+
* dispatcher inside resource renders), so the same scenario body can run in
|
|
6
|
+
* three worlds:
|
|
7
|
+
*
|
|
8
|
+
* 1. react — as a component under <StrictMode>
|
|
9
|
+
* 2. bridge — as a resource hosted via useResource inside <StrictMode>
|
|
10
|
+
* 3. tap root — as a resource under createTapRoot (self-emulated strict mode)
|
|
11
|
+
*
|
|
12
|
+
* Each test runs the scenario in the react world to capture the expected event
|
|
13
|
+
* log, then asserts the tap worlds produce the identical log. React is the
|
|
14
|
+
* source of truth; there are no hand-maintained expected sequences to drift.
|
|
15
|
+
*/
|
|
16
|
+
/* oxlint-disable react/exhaustive-deps -- intentional missing-dep patterns are part of the scenarios */
|
|
17
|
+
|
|
18
|
+
import { describe, it, expect, afterEach } from "vitest";
|
|
19
|
+
import {
|
|
20
|
+
StrictMode,
|
|
21
|
+
useEffect,
|
|
22
|
+
useMemo,
|
|
23
|
+
useReducer,
|
|
24
|
+
useRef,
|
|
25
|
+
useState,
|
|
26
|
+
} from "react";
|
|
27
|
+
import { render, act, cleanup } from "@testing-library/react";
|
|
28
|
+
import { resource } from "../../core/resource";
|
|
29
|
+
import { createTapRoot } from "../../core/createTapRoot";
|
|
30
|
+
import { flushTapSync } from "../../core/scheduler";
|
|
31
|
+
import { useResource } from "../../index";
|
|
32
|
+
import { cleanupAllResources } from "../test-utils";
|
|
33
|
+
|
|
34
|
+
type Log = (event: string) => void;
|
|
35
|
+
|
|
36
|
+
type DriveContext = {
|
|
37
|
+
/** Latest value returned by the scenario body. */
|
|
38
|
+
api: () => any;
|
|
39
|
+
/** Dispatch an update like an event handler would (act / flushTapSync). */
|
|
40
|
+
act: (fn: () => void) => void;
|
|
41
|
+
/** Wait for async work (promises, scheduler flushes) to settle. */
|
|
42
|
+
settle: () => Promise<void>;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
type Scenario = {
|
|
46
|
+
name: string;
|
|
47
|
+
use: (log: Log) => any;
|
|
48
|
+
drive?: (ctx: DriveContext) => void | Promise<void>;
|
|
49
|
+
unmountAtEnd?: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Documented divergence: in the bridge world, a dispatch rides through the
|
|
52
|
+
* host's React reducer so tap state stays replayable under concurrent React.
|
|
53
|
+
* The dispatch-time eager invocation of an updater is therefore deferred to
|
|
54
|
+
* the host's next render. Only the ORDER of invocations differs (and only
|
|
55
|
+
* for impure updaters, which the log is a detector for); the invocation
|
|
56
|
+
* multiset and final state must still match React exactly.
|
|
57
|
+
*/
|
|
58
|
+
bridgeDefersEagerInvocation?: boolean;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const settleDelay = () => new Promise<void>((r) => setTimeout(r, 30));
|
|
62
|
+
|
|
63
|
+
/** Shared React driver for the `react` and `bridge` worlds. */
|
|
64
|
+
const runInReact = async (
|
|
65
|
+
scenario: Scenario,
|
|
66
|
+
useBody: (log: Log) => any,
|
|
67
|
+
): Promise<string[]> => {
|
|
68
|
+
const events: string[] = [];
|
|
69
|
+
// The run ends before the view is torn down; stop logging so the teardown's
|
|
70
|
+
// effect cleanups don't leak into the captured log.
|
|
71
|
+
let done = false;
|
|
72
|
+
const log: Log = (e) => void (done || events.push(e));
|
|
73
|
+
let api: any;
|
|
74
|
+
function Probe() {
|
|
75
|
+
api = useBody(log);
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
const view = render(
|
|
79
|
+
<StrictMode>
|
|
80
|
+
<Probe />
|
|
81
|
+
</StrictMode>,
|
|
82
|
+
);
|
|
83
|
+
await scenario.drive?.({
|
|
84
|
+
api: () => api,
|
|
85
|
+
act: (fn) => act(fn),
|
|
86
|
+
settle: () => act(settleDelay),
|
|
87
|
+
});
|
|
88
|
+
if (scenario.unmountAtEnd) view.unmount();
|
|
89
|
+
done = true;
|
|
90
|
+
return events;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const runReact = (scenario: Scenario) =>
|
|
94
|
+
runInReact(scenario, (log) => scenario.use(log));
|
|
95
|
+
|
|
96
|
+
const runBridge = (scenario: Scenario) => {
|
|
97
|
+
const useScenario = (props: { log: Log }) => scenario.use(props.log);
|
|
98
|
+
const Scenario = resource(useScenario);
|
|
99
|
+
return runInReact(scenario, (log) => useResource(Scenario({ log })));
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const runTapRoot = async (scenario: Scenario): Promise<string[]> => {
|
|
103
|
+
const events: string[] = [];
|
|
104
|
+
const log: Log = (e) => events.push(e);
|
|
105
|
+
const root = createTapRoot(function Root() {
|
|
106
|
+
return scenario.use(log);
|
|
107
|
+
});
|
|
108
|
+
await scenario.drive?.({
|
|
109
|
+
api: () => root.getValue(),
|
|
110
|
+
act: (fn) => flushTapSync(fn),
|
|
111
|
+
settle: settleDelay,
|
|
112
|
+
});
|
|
113
|
+
if (scenario.unmountAtEnd) root.unmount();
|
|
114
|
+
return events;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const scenarios: Scenario[] = [
|
|
118
|
+
{
|
|
119
|
+
name: "mount: double render, useState initializer ghost-invoked, first result kept",
|
|
120
|
+
use: (log) => {
|
|
121
|
+
const [a] = useState(() => {
|
|
122
|
+
log("init-a");
|
|
123
|
+
return 1;
|
|
124
|
+
});
|
|
125
|
+
const [b] = useState(() => {
|
|
126
|
+
log("init-b");
|
|
127
|
+
return 2;
|
|
128
|
+
});
|
|
129
|
+
log(`render a=${a} b=${b}`);
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: "useMemo: ghost-invoked when computing, cached across passes and re-renders",
|
|
134
|
+
use: (log) => {
|
|
135
|
+
const [n, setN] = useState(0);
|
|
136
|
+
useMemo(() => {
|
|
137
|
+
log(`memo n=${n}`);
|
|
138
|
+
return n;
|
|
139
|
+
}, [n]);
|
|
140
|
+
log(`render n=${n}`);
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
if (n === 0) setN(1);
|
|
143
|
+
}, [n]);
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: "memo identity is stable across the strict double render",
|
|
148
|
+
use: (log) => {
|
|
149
|
+
const obj = useMemo(() => ({}), []);
|
|
150
|
+
const first = useRef(obj);
|
|
151
|
+
log(`identity-stable=${first.current === obj}`);
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: "useReducer: initializer ghost-invoked, first result kept",
|
|
156
|
+
use: (log) => {
|
|
157
|
+
let initCount = 0;
|
|
158
|
+
const [state] = useReducer(
|
|
159
|
+
(s: number, a: number) => s + a,
|
|
160
|
+
0,
|
|
161
|
+
(arg: number) => {
|
|
162
|
+
initCount++;
|
|
163
|
+
log(`init-${initCount}`);
|
|
164
|
+
return arg + initCount * 10;
|
|
165
|
+
},
|
|
166
|
+
);
|
|
167
|
+
log(`render state=${state}`);
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
name: "useReducer: dispatch reducer ghost-invoked",
|
|
172
|
+
use: (log) => {
|
|
173
|
+
const countRef = useRef(0);
|
|
174
|
+
const [state, dispatch] = useReducer((s: number, _a: number) => {
|
|
175
|
+
countRef.current++;
|
|
176
|
+
const result = countRef.current * 100;
|
|
177
|
+
log(`reducer-${countRef.current} state=${s} -> ${result}`);
|
|
178
|
+
return result;
|
|
179
|
+
}, 0);
|
|
180
|
+
log(`render state=${state}`);
|
|
181
|
+
return { dispatch };
|
|
182
|
+
},
|
|
183
|
+
drive: ({ api, act }) => {
|
|
184
|
+
act(() => api().dispatch(1));
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
name: "effects cycle mount → unmount → mount",
|
|
189
|
+
use: (log) => {
|
|
190
|
+
const [n] = useState(0);
|
|
191
|
+
useEffect(() => {
|
|
192
|
+
log("e1-mount");
|
|
193
|
+
return () => log("e1-unmount");
|
|
194
|
+
});
|
|
195
|
+
useEffect(() => {
|
|
196
|
+
log("e2-mount");
|
|
197
|
+
return () => log("e2-unmount");
|
|
198
|
+
}, []);
|
|
199
|
+
useEffect(() => {
|
|
200
|
+
log(`e3-mount n=${n}`);
|
|
201
|
+
return () => log(`e3-unmount n=${n}`);
|
|
202
|
+
}, [n]);
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
name: "setState in effect",
|
|
207
|
+
use: (log) => {
|
|
208
|
+
const [count, setCount] = useState(0);
|
|
209
|
+
log(`render ${count}`);
|
|
210
|
+
useEffect(() => {
|
|
211
|
+
log(`effect ${count}`);
|
|
212
|
+
if (count === 0) setCount(1);
|
|
213
|
+
return () => log(`cleanup ${count}`);
|
|
214
|
+
}, [count]);
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
name: "event-handler setState: single double render, updater ghost-invoked",
|
|
219
|
+
use: (log) => {
|
|
220
|
+
const [count, setCount] = useState(0);
|
|
221
|
+
log(`render ${count}`);
|
|
222
|
+
return {
|
|
223
|
+
increment: () =>
|
|
224
|
+
setCount((prev) => {
|
|
225
|
+
log(`updater prev=${prev}`);
|
|
226
|
+
return prev + 1;
|
|
227
|
+
}),
|
|
228
|
+
};
|
|
229
|
+
},
|
|
230
|
+
drive: ({ api, act }) => {
|
|
231
|
+
act(() => api().increment());
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
name: "event-handler setState: multiple setStates batch into one render",
|
|
236
|
+
use: (log) => {
|
|
237
|
+
const [a, setA] = useState(0);
|
|
238
|
+
const [b, setB] = useState(0);
|
|
239
|
+
log(`render a=${a} b=${b}`);
|
|
240
|
+
return {
|
|
241
|
+
both: () => {
|
|
242
|
+
setA(1);
|
|
243
|
+
setB(2);
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
},
|
|
247
|
+
drive: ({ api, act }) => {
|
|
248
|
+
act(() => api().both());
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
name: "async setState from a promise scheduled in an effect",
|
|
253
|
+
use: (log) => {
|
|
254
|
+
const [count, setCount] = useState(0);
|
|
255
|
+
log(`render ${count}`);
|
|
256
|
+
useEffect(() => {
|
|
257
|
+
if (count === 0) {
|
|
258
|
+
void Promise.resolve().then(() => {
|
|
259
|
+
log("promise");
|
|
260
|
+
setCount(1);
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}, [count]);
|
|
264
|
+
},
|
|
265
|
+
drive: ({ settle }) => settle(),
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
name: "async setState from a setTimeout scheduled in an effect",
|
|
269
|
+
use: (log) => {
|
|
270
|
+
const [count, setCount] = useState(0);
|
|
271
|
+
log(`render ${count}`);
|
|
272
|
+
useEffect(() => {
|
|
273
|
+
if (count === 0) {
|
|
274
|
+
setTimeout(() => {
|
|
275
|
+
log("timeout");
|
|
276
|
+
setCount(1);
|
|
277
|
+
}, 5);
|
|
278
|
+
}
|
|
279
|
+
}, [count]);
|
|
280
|
+
},
|
|
281
|
+
drive: ({ settle }) => settle(),
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
name: "setState from the first strict effect mount survives its cleanup",
|
|
285
|
+
use: (log) => {
|
|
286
|
+
const [count, setCount] = useState(0);
|
|
287
|
+
const runs = useRef(0);
|
|
288
|
+
log(`render ${count}`);
|
|
289
|
+
useEffect(() => {
|
|
290
|
+
runs.current++;
|
|
291
|
+
const n = runs.current;
|
|
292
|
+
log(`mount#${n} count=${count}`);
|
|
293
|
+
if (n === 1) setCount(1);
|
|
294
|
+
return () => log(`cleanup#${n}`);
|
|
295
|
+
}, []);
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
name: "setState from both strict effect mounts: last value wins",
|
|
300
|
+
use: (log) => {
|
|
301
|
+
const [count, setCount] = useState(0);
|
|
302
|
+
const runs = useRef(0);
|
|
303
|
+
log(`render ${count}`);
|
|
304
|
+
useEffect(() => {
|
|
305
|
+
runs.current++;
|
|
306
|
+
const n = runs.current;
|
|
307
|
+
log(`mount#${n} count=${count}`);
|
|
308
|
+
setCount(n === 1 ? 1 : 2);
|
|
309
|
+
return () => log(`cleanup#${n}`);
|
|
310
|
+
}, []);
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
name: "updater setState from both strict effect mounts chains",
|
|
315
|
+
use: (log) => {
|
|
316
|
+
const [count, setCount] = useState(0);
|
|
317
|
+
const runs = useRef(0);
|
|
318
|
+
log(`render ${count}`);
|
|
319
|
+
useEffect(() => {
|
|
320
|
+
runs.current++;
|
|
321
|
+
const n = runs.current;
|
|
322
|
+
setCount((prev) => {
|
|
323
|
+
log(`updater#${n} prev=${prev}`);
|
|
324
|
+
return prev + n;
|
|
325
|
+
});
|
|
326
|
+
}, []);
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
name: "useReducer: dispatching the same state",
|
|
331
|
+
use: (log) => {
|
|
332
|
+
const [state, dispatch] = useReducer((s: number) => s, 42);
|
|
333
|
+
log(`render ${state}`);
|
|
334
|
+
return { dispatch };
|
|
335
|
+
},
|
|
336
|
+
drive: ({ api, act }) => {
|
|
337
|
+
act(() => api().dispatch(0));
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
name: "updater returning a different value per invocation",
|
|
342
|
+
bridgeDefersEagerInvocation: true,
|
|
343
|
+
use: (log) => {
|
|
344
|
+
const [count, setCount] = useState(0);
|
|
345
|
+
const calls = useRef(0);
|
|
346
|
+
log(`render ${count}`);
|
|
347
|
+
useEffect(() => {
|
|
348
|
+
log("effect mount");
|
|
349
|
+
setCount((prev) => {
|
|
350
|
+
calls.current++;
|
|
351
|
+
log(`updater call #${calls.current} with prev=${prev}`);
|
|
352
|
+
return calls.current === 1 ? 100 : 200;
|
|
353
|
+
});
|
|
354
|
+
return () => log("effect cleanup");
|
|
355
|
+
}, []);
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
name: "unmount runs cleanups",
|
|
360
|
+
use: (log) => {
|
|
361
|
+
useEffect(() => {
|
|
362
|
+
log("mount");
|
|
363
|
+
return () => log("unmount");
|
|
364
|
+
}, []);
|
|
365
|
+
},
|
|
366
|
+
unmountAtEnd: true,
|
|
367
|
+
},
|
|
368
|
+
];
|
|
369
|
+
|
|
370
|
+
describe("StrictMode parity (React vs tap)", () => {
|
|
371
|
+
afterEach(() => {
|
|
372
|
+
cleanupAllResources();
|
|
373
|
+
cleanup();
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
for (const scenario of scenarios) {
|
|
377
|
+
describe(scenario.name, () => {
|
|
378
|
+
it("tap-in-React bridge matches React", async () => {
|
|
379
|
+
const reactLog = await runReact(scenario);
|
|
380
|
+
cleanup();
|
|
381
|
+
const bridgeLog = await runBridge(scenario);
|
|
382
|
+
if (scenario.bridgeDefersEagerInvocation) {
|
|
383
|
+
expect([...bridgeLog].sort()).toEqual([...reactLog].sort());
|
|
384
|
+
} else {
|
|
385
|
+
expect(bridgeLog).toEqual(reactLog);
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it("tap root matches React", async () => {
|
|
390
|
+
const reactLog = await runReact(scenario);
|
|
391
|
+
cleanup();
|
|
392
|
+
const tapLog = await runTapRoot(scenario);
|
|
393
|
+
expect(tapLog).toEqual(reactLog);
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
describe("render-phase update: setState during render", () => {
|
|
400
|
+
afterEach(() => {
|
|
401
|
+
cleanupAllResources();
|
|
402
|
+
cleanup();
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
const use = (log: Log) => {
|
|
406
|
+
const [count, setCount] = useState(0);
|
|
407
|
+
log(`render ${count}`);
|
|
408
|
+
if (count === 0) setCount(1);
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
it("React re-renders with the new state", async () => {
|
|
412
|
+
const events = await runInReact({ name: "", use }, (log) => use(log));
|
|
413
|
+
expect(events).toEqual(["render 0", "render 1", "render 1"]);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it("tap matches", async () => {
|
|
417
|
+
const events = await runTapRoot({ name: "", use });
|
|
418
|
+
expect(events).toEqual(["render 0", "render 1", "render 1"]);
|
|
419
|
+
});
|
|
420
|
+
});
|