Files
Jyotisha/frontend/tests/onboarding-hint-observer.test.tsx
T
Jesse_ChenandClaude Opus 5.5 de9d4826c1 #111 第 3 轮:建档不退回加载、右栏能拖宽、清单不迟到、月亮问句与别人的上升句(BUG-1455~1459)
- 建档重读人物名单时保留旧名单,失败也不卡在加载画面(BUG-1455)
- 对话右栏改用自己的拖宽把手,侧栏与右栏都能用方向键(BUG-1456/1457)
- 启程清单胶囊和首页一起出来,读状态时用上次的样子占位(BUG-1458)
- 起步第一问与别人的天空过场读一次已算好的星盘取星座,上升只在不会换座时用(BUG-1459,产品 10-27 同意)
- 补回删减的断言与三栏说明;登录代码改回原样,替身放进测试;删旧校正画面的无用样式
- 首屏 +7.54% 产品已接受,写进 DESIGN

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LutgzyJqNhPpZy1nFsJtN3
2026-10-27 10:24:57 +08:00

150 lines
5.1 KiB
TypeScript

import "./install-test-document";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import { OnboardingHintLayer } from "../src/components/onboarding-hint-layer.tsx";
import { shouldAttachHintObserver, type HintFacts } from "../src/lib/onboarding-hints.ts";
import { OnboardingJourneyContext, type OnboardingJourney } from "../src/lib/onboarding-journey.tsx";
import { ONBOARDING_HINT_IDS } from "../src/lib/onboarding-state.ts";
import { createClientLifecycleHarness } from "./react-client-lifecycle-test-support.ts";
const layerSource = readFileSync(new URL("../src/components/onboarding-hint-layer.tsx", import.meta.url), "utf8");
function facts(patch: Partial<HintFacts> = {}): HintFacts {
return {
hintsSeen: [],
followUpArmed: false,
chartVisited: false,
sidebarOpen: false,
activeChartTab: null,
synReady: false,
ephemerisArmed: false,
blocked: false,
anchors: {},
...patch,
};
}
function journey(hintWatch: boolean): OnboardingJourney {
return {
showChecklist: false,
checklistPending: false,
userId: "",
items: [],
label: "",
celebration: false,
activeHint: null,
hintText: "",
showChartDot: false,
tabHasDot: () => false,
dismissChecklist: () => {},
dismissHint: async () => true,
go: () => {},
setChartTab: () => {},
markChartVisited: () => {},
setChecklistOpen: () => {},
setLive: () => {},
registerHome: () => {},
hintWatch,
motionEnabled: false,
};
}
function installObserverCount() {
let live = 0;
class CountingObserver {
constructor(_callback: MutationCallback) { live += 1; }
observe() {}
disconnect() { live -= 1; }
takeRecords() { return []; }
}
globalThis.MutationObserver = CountingObserver as unknown as typeof MutationObserver;
return () => live;
}
test("a full hint list or a closed checklist attaches no observer", async () => {
assert.doesNotMatch(layerSource, /observe\(\s*document\.body,\s*\{[^}]*subtree:\s*true/);
const armed = facts({ followUpArmed: true, sidebarOpen: true });
assert.equal(shouldAttachHintObserver({
checklistClosed: false,
streaming: false,
facts: facts({ hintsSeen: [...ONBOARDING_HINT_IDS], followUpArmed: true }),
}), false);
assert.equal(shouldAttachHintObserver({ checklistClosed: true, streaming: false, facts: armed }), false);
assert.equal(shouldAttachHintObserver({ checklistClosed: false, streaming: false, facts: armed }), true);
const live = installObserverCount();
const harness = createClientLifecycleHarness();
try {
await harness.render(
<OnboardingJourneyContext.Provider value={journey(shouldAttachHintObserver({
checklistClosed: false,
streaming: false,
facts: facts({ hintsSeen: [...ONBOARDING_HINT_IDS], followUpArmed: true }),
}))}>
<OnboardingHintLayer />
</OnboardingJourneyContext.Provider>,
);
await harness.idle();
await harness.render(
<OnboardingJourneyContext.Provider value={journey(shouldAttachHintObserver({
checklistClosed: true,
streaming: false,
facts: armed,
}))}>
<OnboardingHintLayer />
</OnboardingJourneyContext.Provider>,
);
await harness.idle();
assert.equal(live(), 0);
assert.equal(harness.errors.length, 0, harness.errors.map(String).join("\n"));
} finally {
await harness.close();
}
});
function StreamingPage({ count, hintWatch }: { count: number; hintWatch: boolean }) {
return (
<OnboardingJourneyContext.Provider value={journey(hintWatch)}>
<OnboardingHintLayer />
<div>{Array.from({ length: count }, (_, index) => <span key={index}>字</span>)}</div>
</OnboardingJourneyContext.Provider>
);
}
test("two hundred streaming fragments do not scan for hints", async () => {
const armed = facts({ followUpArmed: true, sidebarOpen: true });
assert.equal(shouldAttachHintObserver({ checklistClosed: false, streaming: true, facts: armed }), false);
const harness = createClientLifecycleHarness();
let streaming = false;
let scans = 0;
const countScan = (selector: string) => {
if (streaming && selector.includes("data-onboarding")) scans += 1;
return selector.includes("All") ? [] : null;
};
harness.document.querySelector = ((selector: string) => countScan(selector)) as typeof harness.document.querySelector;
harness.document.querySelectorAll = ((selector: string) => countScan(selector) ?? []) as typeof harness.document.querySelectorAll;
try {
await harness.render(<StreamingPage count={0} hintWatch={shouldAttachHintObserver({
checklistClosed: false,
streaming: false,
facts: armed,
})} />);
await harness.idle();
streaming = true;
for (let count = 1; count <= 200; count += 1) {
await harness.render(<StreamingPage count={count} hintWatch={shouldAttachHintObserver({
checklistClosed: false,
streaming: true,
facts: armed,
})} />);
}
await harness.idle();
assert.equal(scans, 0);
assert.equal(harness.errors.length, 0, harness.errors.map(String).join("\n"));
} finally {
await harness.close();
}
});