Home and the chat chart no longer re-render on every streamed frame (issue #123, BUG-1472)
Home subscribed to the whole streamingReply object, so each animation frame of an answer re-ran the page (header, picker, composer, overlays) and the desktop natal panel under it. ChatStreamingTranscript now subscribes for its session and feeds ChatTranscript; Home reads one derived boolean for the stop label. ChatChartSplit is memoized. 100 frames: Home <=2 commits (was >=100), panel 0. Two source-contract tests now pin the new component instead of the old names. The onboarding typewriter still ticks at Home level (out of this ticket). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
9a0ce76d48
commit
ad801a5db7
@@ -1,5 +1,10 @@
|
||||
# 印度占星 Skill 更新日志
|
||||
|
||||
## 2026-10-28 — 回答出字时页面不再整页重画
|
||||
|
||||
- 回答逐字出来时,只有回答那一块在更新;顶栏、输入框和右侧本命盘不再每一帧跟着重画(以前最多每秒 60 次)。长回答时电脑更省力,滚动和点按更跟手。看到的内容不变。
|
||||
- Skill 版本不变(BUG-1472,issue #123)。
|
||||
|
||||
## 2026-10-28 — 个人长报告生成快约四分之一
|
||||
|
||||
- 第一次生成一份完整的个人长报告,本机实测从约 26 秒降到约 20 秒;报告内容一字不变。
|
||||
|
||||
@@ -20080,3 +20080,19 @@
|
||||
- 复发自:无
|
||||
- 修复版本:分支 `codex/report-fast-default-20261028`。
|
||||
|
||||
|
||||
## BUG-1472 | 回答出字时首页整页和右侧本命盘每帧重画
|
||||
|
||||
- 状态:resolved(代码 + 回归测试;待部署)
|
||||
- 首次发现 / 最近更新:2026-10-28 / 2026-10-28(issue #118 代码体检第 13 条 → #123)
|
||||
- 影响面:首页对话(`frontend/src/app/(app)/page.tsx` `Home`)、桌面端默认打开的本命盘面板(`chat-chart-split.tsx`)。
|
||||
- 现象:回答逐字出来时,整个首页(顶栏、人物选择、余额、输入框、模型选择等)和右侧整张星盘跟着每一帧重画,最多 60 次/秒;一条 1,500 字的回答约 375 次。外观和内容没有变化,只是白耗主线程。
|
||||
- 触发条件:已登录首页,任意一次流式回答;桌面端本命盘面板打开时更重。
|
||||
- 根因:`Home` 用 `useShallow` 订阅了整个 `streamingReply` 对象,流式缓冲每帧提交一个新对象,`Home` 整体重跑;`ChatChartSplit` 没有 `memo`,跟着 `ChatChartFrame` 的 children 变化重画。BUG-1329 只修了侧栏重注册,`Home` 自身的每帧重渲染留着。
|
||||
- 修复:新增 `components/chat-streaming-transcript.tsx`。`ChatStreamingTranscript` 自己按会话订阅流式回复并喂给 `ChatTranscript`;`Home` 只订阅派生布尔值 `useStreamingReplyHasText`(停止按钮文案用,一次回答最多变两次)。`ChatChartSplit` 包 `memo`(props 都是 store 值)。`page.tsx` 少 19 行,未新增 useState / useRef。
|
||||
- 验证:新测试 `frontend/tests/home-stream-rerender-20261028.test.tsx`:100 帧真实写 store,旧接法 Home 提交 ≥100 次;新接法 Home 自身提交 ≤2 次,面板 0 次,正文仍逐帧更新并显示完整;停止文案在出字前 / 后正确切换;源码断言 `page.tsx` 不再读 `streamingReply`、`ChatChartSplit` 是 memo。改前代码上源码断言失败。tsc 0、相关文件 lint 0。
|
||||
- 防复发:上述测试。流式状态只让消息区订阅;页面级组件只订阅会变得很少的派生值。
|
||||
- 未做:新手引导打字机(`setPresetMessageLength` 每 26 ms 一次)仍在 Home 顶层。它被 `use-profile-onboarding` 的 8 处重置驱动,其中几处是「同一句话重放」,下放需要改重置语义,超出本单验收;只在建档那几秒发生。
|
||||
- 关联:BUG-1329(侧栏每帧重注册)、BUG-1130。
|
||||
- 复发自:无
|
||||
- 修复版本:分支 `codex/home-stream-rerender-20261028`。
|
||||
|
||||
@@ -18,3 +18,8 @@
|
||||
9. 点盘型条右边的 ≡:「星盘类型」标题在左、叉号在右,同一行;电脑上整块面板完整可见。
|
||||
10. 手机聊天页点开盘面:顶边没有白线,盘离屏幕两边更近。
|
||||
|
||||
## #123 回答出字时页面不再整页重画(BUG-1472)
|
||||
|
||||
11. 电脑打开首页、右侧本命盘开着,问一个会写长回答的问题:回答照常逐字出来,右侧盘不闪、不跳;出字过程中拖动右侧盘的宽度、点盘上的宫位都跟手。
|
||||
12. 回答还没出字时点「停止」:提示是「停止回答并申请退回本次点数」;出了一些字以后再问一次并点停止:提示变成「停止回答,保留已生成内容并退回本次点数」。
|
||||
|
||||
|
||||
@@ -25,7 +25,8 @@ import {
|
||||
import { ConversationalBirthTimeRectification } from "@/components/conversational-birth-time-rectification";
|
||||
import { declaredBirthTime } from "@/lib/rectification-surface-state";
|
||||
import { toggleChatMessageFeedback } from "@/components/chat-message-actions";
|
||||
import { ChatTranscript, type ChatTranscriptActions } from "@/components/chat-transcript";
|
||||
import type { ChatTranscriptActions } from "@/components/chat-transcript";
|
||||
import { ChatStreamingTranscript, useStreamingReplyHasText } from "@/components/chat-streaming-transcript";
|
||||
import { ChatProfilePicker } from "@/components/chat-profile-picker";
|
||||
import { ChatChartFrame, ChatChartToggle } from "@/components/chat-chart-frame";
|
||||
import { ModelSelector } from "@/components/model-selector";
|
||||
@@ -244,7 +245,6 @@ export default function Home() {
|
||||
setPendingSessionId,
|
||||
pendingRequestId,
|
||||
setPendingRequestId,
|
||||
streamingReply,
|
||||
setStreamingReply,
|
||||
messageFeedback,
|
||||
setMessageFeedback,
|
||||
@@ -262,7 +262,6 @@ export default function Home() {
|
||||
setPendingSessionId: state.setPendingSessionId,
|
||||
pendingRequestId: state.pendingRequestId,
|
||||
setPendingRequestId: state.setPendingRequestId,
|
||||
streamingReply: state.streamingReply,
|
||||
setStreamingReply: state.setStreamingReply,
|
||||
messageFeedback: state.messageFeedback,
|
||||
setMessageFeedback: state.setMessageFeedback,
|
||||
@@ -421,19 +420,7 @@ export default function Home() {
|
||||
|| rectificationMutationPending
|
||||
|| !account
|
||||
|| !modelCatalog;
|
||||
const activeStreamingText = streamingReply && streamingReply.sessionId === activeSession?.id ? streamingReply.text : "";
|
||||
const activeStreamingActivity = streamingReply && streamingReply.sessionId === activeSession?.id
|
||||
? streamingReply.activity
|
||||
: undefined;
|
||||
const activeStreamingThinking = streamingReply && streamingReply.sessionId === activeSession?.id
|
||||
? streamingReply.thinkingText
|
||||
: undefined;
|
||||
const activeStreamingSections = streamingReply && streamingReply.sessionId === activeSession?.id
|
||||
? streamingReply.thinkingSections
|
||||
: undefined;
|
||||
const activeStreamingTimeline = streamingReply && streamingReply.sessionId === activeSession?.id
|
||||
? streamingReply.timeline
|
||||
: undefined;
|
||||
const activeStreamingHasText = useStreamingReplyHasText(activeSession?.id);
|
||||
const activeReplyOutcome = replyOutcome && replyOutcome.sessionId === activeSession?.id ? replyOutcome : null;
|
||||
const replyPhase: ChatReplyPhase = isLoading
|
||||
? consultationPhase === "recovering" ? "recovering" : "generating"
|
||||
@@ -1319,15 +1306,9 @@ export default function Home() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="message-list" aria-busy={isLoading}>
|
||||
<ChatTranscript
|
||||
<ChatStreamingTranscript
|
||||
messages={activeSession.messages}
|
||||
loading={isLoading}
|
||||
streamingText={activeStreamingText}
|
||||
streamingActivity={activeStreamingActivity}
|
||||
streamingThinking={activeStreamingThinking}
|
||||
streamingSections={activeStreamingSections}
|
||||
streamingTimeline={activeStreamingTimeline}
|
||||
streamingResponseKind={streamingReply?.sessionId === activeSession?.id ? streamingReply?.responseKind : undefined}
|
||||
sessionId={activeSession.id}
|
||||
sessionType={activeSession.sessionType}
|
||||
theme={activeSession.theme}
|
||||
@@ -1382,8 +1363,8 @@ export default function Home() {
|
||||
submitLabel={!synastryChat && !profileComplete && shownOnboardingStep === "name" ? "确认称呼" : "发送"}
|
||||
submitBlocked={synastryClosed || subjectDeleted || Boolean(pendingSessionId) || Boolean(queuedDraft) || sessionMessagesLoading || rectificationSurfaceOpen || activeRectificationSession || !account || !modelCatalog || (!synastryChat && !profileComplete && (shownOnboardingStep !== "name" || !presetMessageFinished || profileSaving))}
|
||||
stopVisible={isLoading}
|
||||
stopLabel={consultationPhase === "undo" ? "撤回发送,本次不扣点" : activeStreamingText ? "停止回答,保留已生成内容并退回本次点数" : "停止回答并申请退回本次点数"}
|
||||
stopTitle={consultationPhase === "undo" ? "撤回发送,本次不扣点" : activeStreamingText ? "停止回答,保留现有内容并申请退回本次点数" : "停止回答"}
|
||||
stopLabel={consultationPhase === "undo" ? "撤回发送,本次不扣点" : activeStreamingHasText ? "停止回答,保留已生成内容并退回本次点数" : "停止回答并申请退回本次点数"}
|
||||
stopTitle={consultationPhase === "undo" ? "撤回发送,本次不扣点" : activeStreamingHasText ? "停止回答,保留现有内容并申请退回本次点数" : "停止回答"}
|
||||
onSubmit={submit}
|
||||
onChange={(event) => {
|
||||
setDraft(event.target.value);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
|
||||
import { ChatNatalChart } from "@/components/chat-natal-chart";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useDragWidth } from "@/hooks/use-drag-width";
|
||||
@@ -8,9 +10,10 @@ import { CHAT_CHART_WIDTH_MAX, CHAT_CHART_WIDTH_MIN } from "@/lib/chat-chart-pan
|
||||
/**
|
||||
* The right-hand natal column. The chat column stays outside it, so loading this
|
||||
* never remounts the chat (BUG-1448). Its left edge drags or takes the arrow
|
||||
* keys; dragging below the minimum closes the panel (BUG-1456).
|
||||
* keys; dragging below the minimum closes the panel (BUG-1456). Memoized: its
|
||||
* props are store values, so a Home render alone never redraws the chart (issue #123).
|
||||
*/
|
||||
export function ChatChartSplit({
|
||||
export const ChatChartSplit = memo(function ChatChartSplit({
|
||||
open,
|
||||
width,
|
||||
subjectId,
|
||||
@@ -45,4 +48,4 @@ export function ChatChartSplit({
|
||||
<ChatNatalChart subjectId={subjectId} />
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
"use client";
|
||||
|
||||
import { ChatTranscript, type ChatTranscriptProps } from "@/components/chat-transcript";
|
||||
import type { StreamingReply } from "@/lib/home-types";
|
||||
import { useSessionStore } from "@/stores/session-store";
|
||||
|
||||
type StreamingFields = "streamingText" | "streamingActivity" | "streamingThinking" | "streamingSections" | "streamingTimeline" | "streamingResponseKind";
|
||||
|
||||
function replyFor(state: { streamingReply: StreamingReply | null }, sessionId: string | undefined) {
|
||||
const reply = state.streamingReply;
|
||||
return reply && reply.sessionId === sessionId ? reply : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The transcript is the only subscriber to the streaming reply (issue #123).
|
||||
* The stream commits once per animation frame; when Home subscribed, every
|
||||
* frame re-rendered the whole page and the desktop chart panel with it.
|
||||
*/
|
||||
export function ChatStreamingTranscript(props: Omit<ChatTranscriptProps, StreamingFields>) {
|
||||
const reply = useSessionStore((state) => replyFor(state, props.sessionId));
|
||||
return (
|
||||
<ChatTranscript
|
||||
{...props}
|
||||
streamingText={reply?.text ?? ""}
|
||||
streamingActivity={reply?.activity}
|
||||
streamingThinking={reply?.thinkingText}
|
||||
streamingSections={reply?.thinkingSections}
|
||||
streamingTimeline={reply?.timeline}
|
||||
streamingResponseKind={reply?.responseKind}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/** Whether the active reply has text yet; flips at most twice per answer, so Home can read it. */
|
||||
export function useStreamingReplyHasText(sessionId: string | undefined) {
|
||||
return useSessionStore((state) => Boolean(replyFor(state, sessionId)?.text));
|
||||
}
|
||||
@@ -161,7 +161,10 @@ test("the streaming reply announces state transitions, never streamed deltas", (
|
||||
|
||||
// And: the old start-only region that lived inside the busy message list is gone.
|
||||
assert.doesNotMatch(pageSource, /<span className="sr-only" aria-live="polite">\{isLoading \? "Jyotisha 正在回答" : ""\}<\/span>/);
|
||||
assert.match(pageSource, /<div className="message-list" aria-busy=\{isLoading\}>\n\s*<ChatTranscript/);
|
||||
// 原值:忙碌列表里直接是 `<ChatTranscript`
|
||||
// 新值:`<ChatStreamingTranscript`,它自己订阅流式回复再渲染 ChatTranscript
|
||||
// 原因:issue #123(BUG-1472)Home 不再随每帧流式重画
|
||||
assert.match(pageSource, /<div className="message-list" aria-busy=\{isLoading\}>\n\s*<ChatStreamingTranscript/);
|
||||
});
|
||||
|
||||
test("the reply phase covers start, completion and every terminal state", () => {
|
||||
|
||||
@@ -10,6 +10,7 @@ const globalStyles = readFileSync(new URL("../src/app/globals.css", import.meta.
|
||||
const messageRowSource = readFileSync(new URL("../src/components/chat-message-row.tsx", import.meta.url), "utf8");
|
||||
const activitySource = readFileSync(new URL("../src/components/agent-activity-status.tsx", import.meta.url), "utf8");
|
||||
const timelineSource = readFileSync(new URL("../src/components/consultation-run-timeline.tsx", import.meta.url), "utf8");
|
||||
const streamingTranscriptSource = readFileSync(new URL("../src/components/chat-streaming-transcript.tsx", import.meta.url), "utf8");
|
||||
const reducerSource = readFileSync(new URL("../src/lib/consultation-run-timeline.ts", import.meta.url), "utf8");
|
||||
|
||||
const previousMessages = [
|
||||
@@ -123,7 +124,10 @@ test("shows honest agent activity states before and during streamed text", () =>
|
||||
assert.match(pageSource, /createNdjsonParser/);
|
||||
assert.match(pageSource, /event.type === "thinking.section"/);
|
||||
assert.match(pageSource, /event.type === "thinking.delta"/);
|
||||
assert.match(pageSource, /activeStreamingSections/);
|
||||
// 原值:page.tsx 里有 `activeStreamingSections`
|
||||
// 新值:流式的思考分节由 ChatStreamingTranscript 自己订阅后传给 ChatTranscript
|
||||
// 原因:issue #123(BUG-1472)Home 不再订阅流式回复,避免每帧整页重画
|
||||
assert.match(streamingTranscriptSource, /streamingSections=\{reply\?\.thinkingSections\}/);
|
||||
assert.match(pageSource, /streamedThinking/);
|
||||
assert.match(messageRowSource, /showLiveActivity/);
|
||||
// Former locks on `ConsultationThinkingReport`, `hasTrace`, `activityTrace={message.activityTrace}`,
|
||||
@@ -145,7 +149,8 @@ test("shows honest agent activity states before and during streamed text", () =>
|
||||
assert.match(pageSource, /\.\.\.\(thinkingSections\.length \? \{ thinkingSections \} : \{\}\)/);
|
||||
assert.match(pageSource, /reduceConsultationTimeline/);
|
||||
assert.match(pageSource, /timeline: timelineState\.rows/);
|
||||
assert.match(pageSource, /activeStreamingTimeline/);
|
||||
// 原值:page.tsx 里有 `activeStreamingTimeline`;新值与原因同上(issue #123)
|
||||
assert.match(streamingTranscriptSource, /streamingTimeline=\{reply\?\.timeline\}/);
|
||||
assert.match(messageRowSource, /ConsultationRunTimeline/);
|
||||
assert.match(messageRowSource, /message\.timeline/);
|
||||
assert.match(timelineSource, /BookOpen/);
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* Issue #123: while an answer streams, only the transcript follows the reply.
|
||||
* Home used to subscribe to `streamingReply`, so every frame re-rendered the
|
||||
* whole page and the desktop chart panel under it.
|
||||
*/
|
||||
import "./install-test-document";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
import { createRef, memo, Profiler, useEffect, type ProfilerOnRenderCallback, type ReactNode } from "react";
|
||||
|
||||
import { ChatChartSplit } from "../src/components/chat-chart-split.tsx";
|
||||
import { ChatStreamingTranscript, useStreamingReplyHasText } from "../src/components/chat-streaming-transcript.tsx";
|
||||
import { ChatTranscript, type ChatTranscriptActions, type ChatTranscriptProps } from "../src/components/chat-transcript.tsx";
|
||||
import type { ChatMessage } from "../src/lib/chat-message-view.ts";
|
||||
import { useSessionStore } from "../src/stores/session-store.ts";
|
||||
import { createClientLifecycleHarness } from "./react-client-lifecycle-test-support.ts";
|
||||
|
||||
const SESSION = "session-stream";
|
||||
const FRAMES = 100;
|
||||
const messages: ChatMessage[] = [{ role: "user", text: "这个月工作上要注意什么?" }];
|
||||
|
||||
function transcriptProps(): Omit<ChatTranscriptProps, "streamingText"> {
|
||||
const actionsRef = createRef<ChatTranscriptActions>() as ChatTranscriptProps["actionsRef"];
|
||||
actionsRef.current = { onFeedback() {}, onCopy() {}, onRegenerate() {}, onFollowUp() {}, onStartRectification() {} };
|
||||
return {
|
||||
messages,
|
||||
loading: true,
|
||||
sessionId: SESSION,
|
||||
sessionType: "consultation",
|
||||
theme: "career",
|
||||
messageFeedback: {},
|
||||
copiedMessageKey: null,
|
||||
cancellationPending: false,
|
||||
productEntrypointsDisabled: true,
|
||||
actionsRef,
|
||||
};
|
||||
}
|
||||
|
||||
// Stands in for the chart drawing: counts its own renders without fetching a chart.
|
||||
const panelRenders = { count: 0 };
|
||||
const PanelStandIn = memo(function PanelStandIn({ subjectId }: { subjectId: string }) {
|
||||
useEffect(() => { panelRenders.count += 1; });
|
||||
return <aside data-subject={subjectId} />;
|
||||
});
|
||||
|
||||
async function streamFrames(Page: () => ReactNode) {
|
||||
const harness = createClientLifecycleHarness();
|
||||
const commits = { home: 0 };
|
||||
const onRender: ProfilerOnRenderCallback = (id) => { if (id === "home") commits.home += 1; };
|
||||
useSessionStore.getState().setStreamingReply({ sessionId: SESSION, text: "" });
|
||||
try {
|
||||
await harness.render(<Profiler id="home" onRender={onRender}><Page /></Profiler>);
|
||||
await harness.idle();
|
||||
assert.equal(harness.errors.length, 0);
|
||||
commits.home = 0;
|
||||
panelRenders.count = 0;
|
||||
let text = "";
|
||||
for (let frame = 1; frame <= FRAMES; frame += 1) {
|
||||
text += "字字";
|
||||
await harness.update(() => useSessionStore.getState().setStreamingReply({ sessionId: SESSION, text }));
|
||||
}
|
||||
const shown = harness.container.text.includes(text);
|
||||
return { homeCommits: commits.home, panelRenders: panelRenders.count, transcriptShowsText: shown };
|
||||
} finally {
|
||||
await harness.close();
|
||||
useSessionStore.getState().setStreamingReply(null);
|
||||
}
|
||||
}
|
||||
|
||||
function HomeBefore() {
|
||||
// The pre-#123 wiring: Home subscribes to the whole reply object.
|
||||
const reply = useSessionStore((state) => state.streamingReply);
|
||||
const text = reply?.sessionId === SESSION ? reply.text : "";
|
||||
return (
|
||||
<>
|
||||
<PanelStandIn subjectId="self" />
|
||||
<span data-stop={text ? "keep" : "refund"} />
|
||||
<ChatTranscript {...transcriptProps()} streamingText={text} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function HomeAfter() {
|
||||
const hasText = useStreamingReplyHasText(SESSION);
|
||||
return (
|
||||
<>
|
||||
<PanelStandIn subjectId="self" />
|
||||
<span data-stop={hasText ? "keep" : "refund"} />
|
||||
<ChatStreamingTranscript {...transcriptProps()} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
test("100 streamed frames: the old wiring re-rendered Home every frame, the panel now not at all", async () => {
|
||||
const before = await streamFrames(HomeBefore);
|
||||
const after = await streamFrames(HomeAfter);
|
||||
assert.ok(before.homeCommits >= FRAMES, `the old wiring committed Home ${before.homeCommits} times`);
|
||||
// The Profiler counts its subtree, so the transcript's own per-frame commits land
|
||||
// under "home" as well; Home's own renders are counted in the next test.
|
||||
assert.equal(after.panelRenders, 0, `panel rendered ${after.panelRenders} times`);
|
||||
assert.equal(after.transcriptShowsText, true, "the streamed text is still on screen");
|
||||
assert.equal(before.transcriptShowsText, true);
|
||||
});
|
||||
|
||||
test("Home renders at most twice while the transcript follows every frame", async () => {
|
||||
const counts = { home: 0, transcript: 0 };
|
||||
const onTranscript: ProfilerOnRenderCallback = () => { counts.transcript += 1; };
|
||||
function Page() {
|
||||
const hasText = useStreamingReplyHasText(SESSION);
|
||||
// A dependency-free effect runs once per commit of this component itself.
|
||||
useEffect(() => { counts.home += 1; });
|
||||
return (
|
||||
<>
|
||||
<span data-stop={hasText ? "keep" : "refund"} />
|
||||
<Profiler id="transcript" onRender={onTranscript}><ChatStreamingTranscript {...transcriptProps()} /></Profiler>
|
||||
</>
|
||||
);
|
||||
}
|
||||
await streamFrames(Page);
|
||||
// Subtract the mount (one commit each).
|
||||
const home = counts.home - 1;
|
||||
assert.ok(home <= 2, `Home rendered ${home} times over ${FRAMES} frames`);
|
||||
assert.ok(home <= FRAMES / 10);
|
||||
assert.ok(counts.transcript - 1 >= FRAMES, "the transcript still repaints every frame");
|
||||
});
|
||||
|
||||
test("the stop label still knows whether text has arrived", async () => {
|
||||
const seen: boolean[] = [];
|
||||
function Probe() {
|
||||
seen.push(useStreamingReplyHasText(SESSION));
|
||||
return null;
|
||||
}
|
||||
await streamFrames(Probe);
|
||||
assert.equal(seen[0], false);
|
||||
assert.equal(seen.at(-1), true);
|
||||
assert.ok(seen.length <= 3, `probe rendered ${seen.length} times`);
|
||||
});
|
||||
|
||||
test("Home no longer subscribes to the streaming reply, and the chart split is memoized", () => {
|
||||
const page = readFileSync(new URL("../src/app/(app)/page.tsx", import.meta.url), "utf8");
|
||||
assert.doesNotMatch(page, /state\.streamingReply\b/);
|
||||
assert.doesNotMatch(page, /\bstreamingReply\b(?!:)/);
|
||||
assert.match(page, /<ChatStreamingTranscript\b/);
|
||||
assert.match(page, /useStreamingReplyHasText\(activeSession\?\.id\)/);
|
||||
assert.equal((ChatChartSplit as unknown as { $$typeof: symbol }).$$typeof, Symbol.for("react.memo"));
|
||||
});
|
||||
Reference in New Issue
Block a user