Skip to content

Commit c9f8969

Browse files
committed
test(motion): 补 select 状态半的重渲染判据
1 parent 8f6031b commit c9f8969

1 file changed

Lines changed: 41 additions & 8 deletions

File tree

‎app/src/motion/intensity.test.ts‎

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,19 @@
1010
* 口径:`useMotionIntensity` 用 `react-dom/server` 的 `renderToString` 真跑 hook(node 下唯一入口)。
1111
* ⚠️ 服务端**不执行** `useEffect` ⇒ 「挂载即把档位落到 `<html>`」这条由 T7 的 jsdom 组件判据承担
1212
* (T7 V2 断 `documentElement.dataset.motion`);本文件判的是**初值**与 `select` 通道。
13+
* 🔴 `select` 的 **state 半**怎么判(R18.2 / P-22 通条 · R21.1 指派 T6b):`renderToString` **返回之后**
14+
* 再调 `select` 是**空操作** —— Fizz 的 `dispatchAction` 只在 `componentIdentity ===
15+
* currentlyRenderingComponent` 时才入队,否则整段什么都不做(react-dom 19.2.8
16+
* `react-dom-server-legacy.node.development.js:3971` 逐字)⇒ 渲染期捕获的 `seen` **永远读到切换前
17+
* 的值**,删 `intensity.ts:56` 的 `setValue(next)` 仍 12/12 绿(T6 评审 I-1 实测)。
18+
* 正解 = 在**首次渲染期内**提交一次切换:React 的「渲染期更新」会让 Fizz **立即重跑本组件**
19+
* (`didScheduleRenderPhaseUpdate` ⇒ 再调一次 `Component`),重跑时 `useState` 读的是
20+
* `basicStateReducer` 应用后的**新值** ⇒ 才真的观测到 state;删 `setValue(next)` 时这一轮**不会
21+
* 发生**,断言当场红。见 `renderProbeSwitching`。
1322
* 副作用:临时改写 `globalThis` 上的 `window` / `document` / `localStorage`(逐条还原)。
14-
* 边界:`el.dataset.motion` 用**假元素**判写入路径;真实 `<html>` 的同一断言见 T7。
23+
* 边界:① `el.dataset.motion` 用**假元素**判写入路径,真实 `<html>` 的同一断言见 T7;
24+
* ② 本文件的切换发生在**渲染期**(node 环境唯一可观测的形态);**事件处理器里**(渲染之后)的
25+
* 切换由 T7 的 jsdom 判据承担(`MotionIntensityControl.test.tsx` ⑤)—— 两侧互补,都不可删。
1526
*/
1627
import { createElement } from "react";
1728
import { renderToString } from "react-dom/server";
@@ -77,6 +88,28 @@ function renderProbe(): { value: MotionIntensity; select: (v: MotionIntensity) =
7788
return { value: seen, select };
7889
}
7990

91+
/**
92+
* 真跑一次 hook,并在**首次渲染期内**提交一次切换(`select(next)`)⇒ 取回**逐次渲染**读到的档位。
93+
* @ai-context Why 必须这样(R18.2 / P-22 通条):`renderToString` **返回之后**调 `select` 在 SSR 下是
94+
* **空操作**(Fizz 的 `dispatchAction` 要求 `componentIdentity === currentlyRenderingComponent`
95+
* 才入队)⇒ 那样捕获的 `seen` 永远读到切换**前**的值,是空真判据。渲染期内提交才会被记为
96+
* 「渲染期更新」,React 因此**立即重跑本组件**;重跑时 `useState` 返回的是应用该更新后的**新值**
97+
* ⇒ 这才是「切换后重新渲染所得的值」(删 `setValue(next)` ⇒ 不会重跑 ⇒ 只读到 1 项)。
98+
* @ai-context 副作用:走 `select` 的完整路径(`setValue` + `writeIntensity`)⇒ 也会写内存桩。
99+
* 边界:恰提交**一次**(`seen.length === 1` 守卫)⇒ 不触碰 React 的「最多重渲染 25 次」上限。
100+
*/
101+
function renderProbeSwitching(next: MotionIntensity): MotionIntensity[] {
102+
const seen: MotionIntensity[] = [];
103+
const Probe = (): string => {
104+
const [value, select] = useMotionIntensity();
105+
seen.push(value);
106+
if (seen.length === 1) select(next);
107+
return value;
108+
};
109+
renderToString(createElement(Probe));
110+
return seen;
111+
}
112+
80113
describe("初值跟随系统(§8.5 逐字:reduce ⇒ eco,否则 standard)", () => {
81114
it("defaultIntensity:reduce ⇒ eco;非 reduce / 无 mql ⇒ standard", () => {
82115
expect(defaultIntensity({ matches: true })).toBe("eco");
@@ -132,14 +165,14 @@ describe("持久化:键逐字 motion:intensity(照 useViewMemory 范式:
132165
expect(readIntensity(denied)).toBeNull();
133166
});
134167

135-
it("hook:已存值优先于系统初值;`select` 同时写状态与持久化", () => {
168+
it("hook:已存值优先于系统初值;`select` 切换后**重渲染**读到新档,且已写持久化", () => {
136169
const storage = fakeStorage("memory", "rich");
137-
const seen = withGlobal("localStorage", storage, () => {
138-
const { value, select } = renderProbe();
139-
select("eco");
140-
return value;
141-
});
142-
expect(seen, "已存的选择必须盖过系统初值(此处系统未 reduce ⇒ 初值本会是 standard)").toBe("rich");
170+
const seen = withGlobal("localStorage", storage, () => renderProbeSwitching("eco"));
171+
expect(seen[0], "已存的选择必须盖过系统初值(此处系统未 reduce ⇒ 初值本会是 standard)").toBe("rich");
172+
expect(
173+
seen,
174+
'`select("eco")` 必须真的改了 state:重渲染后读到 eco。删掉 intensity.ts 的 `setValue(next)` ⇒ 不重渲染 ⇒ 这里只剩 ["rich"]',
175+
).toEqual(["rich", "eco"]);
143176
expect(storage.getItem(MOTION_INTENSITY_KEY)).toBe("eco");
144177
});
145178
});

0 commit comments

Comments
 (0)