fix(home): onboarding slot uses a module-level lazy (no component chosen during render); warm-snapshot tests load the city table like the boot does
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
31d0737897
commit
e2c3791fad
@@ -1,22 +1,38 @@
|
||||
"use client";
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
import { lazy, Suspense } from "react";
|
||||
|
||||
import type { HomeOnboardingShellProps } from "@/components/home-onboarding-shell";
|
||||
import type { HomeOnboardingShell, HomeOnboardingShellProps } from "@/components/home-onboarding-shell";
|
||||
import { loadedHomeOnboardingShell, loadHomeOnboardingShell } from "@/lib/home-onboarding-shell-loader";
|
||||
|
||||
// Only reached when the preload below did not finish (or failed) before the
|
||||
// shell had to paint; renders nothing until the chunk arrives, never a spinner.
|
||||
const DynamicHomeOnboardingShell = dynamic(loadHomeOnboardingShell, { loading: () => null });
|
||||
type ShellModule = { default: typeof HomeOnboardingShell };
|
||||
|
||||
/**
|
||||
* The onboarding shell (birth-place picker, China city table, birth-time
|
||||
* intake) is only for people whose profile is incomplete, so it left the home
|
||||
* first-screen bundle (TASK-home-first-load T7, BUG-1130). The cold boot
|
||||
* preloads it before reveal when the profile is incomplete, so this renders the
|
||||
* loaded component directly and the reveal shows it in the same frame.
|
||||
* preloads it before reveal when the profile is incomplete; `lazy` is then
|
||||
* handed an already-settled thenable, which React resolves synchronously, so
|
||||
* the reveal shows the shell in the same frame. Without the preload it loads
|
||||
* behind an empty fallback — never a spinner.
|
||||
*/
|
||||
const LazyHomeOnboardingShell = lazy((): Promise<ShellModule> => {
|
||||
const loaded = loadedHomeOnboardingShell();
|
||||
if (loaded) {
|
||||
// A thenable that calls back synchronously: React marks the lazy resolved
|
||||
// before it would suspend.
|
||||
const settled: PromiseLike<ShellModule> = {
|
||||
then: (resolve) => Promise.resolve(resolve ? resolve({ default: loaded }) : { default: loaded }) as never,
|
||||
};
|
||||
return settled as Promise<ShellModule>;
|
||||
}
|
||||
return loadHomeOnboardingShell().then((Shell) => ({ default: Shell }));
|
||||
});
|
||||
|
||||
export function HomeOnboardingShellSlot(props: HomeOnboardingShellProps) {
|
||||
const Loaded = loadedHomeOnboardingShell();
|
||||
return Loaded ? <Loaded {...props} /> : <DynamicHomeOnboardingShell {...props} />;
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<LazyHomeOnboardingShell {...props} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
+33
-1
@@ -2,6 +2,8 @@ import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
import vm from "node:vm";
|
||||
import React from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
import * as acorn from "acorn";
|
||||
|
||||
@@ -10,7 +12,10 @@ import { BOOT_ACCOUNT_PATH, BOOT_CHART_PROFILES_PATH, BOOT_MODELS_PATH, BOOT_REA
|
||||
import { chinaLocations } from "../src/data/china-locations.ts";
|
||||
import { ensureChinaLocations, loadedChinaCountry, profileNeedsChinaLocations, resetChinaLocationsForTests } from "../src/lib/china-locations-client.ts";
|
||||
import { readProfile, selectedBirthPlace } from "../src/lib/home-profile.ts";
|
||||
import { emptyProfile, previewModelCatalog, type Account } from "../src/lib/home-types.ts";
|
||||
import { HomeOnboardingShellSlot } from "../src/components/home-onboarding-shell-slot.tsx";
|
||||
import type { BirthTimeGuidedController } from "../src/hooks/use-birth-time-guided-journey.ts";
|
||||
import { loadHomeOnboardingShell } from "../src/lib/home-onboarding-shell-loader.ts";
|
||||
import { emptyProfile, presetOnboardingMessage, previewModelCatalog, type Account } from "../src/lib/home-types.ts";
|
||||
import { resetRectificationEntrySummaryReadForTests, startRectificationEntrySummaryRead, takeRectificationEntrySummary } from "../src/lib/rectification-entry-summary-read.ts";
|
||||
import { runHomeBootstrap, type HomeBootstrapDeps } from "../src/lib/home-bootstrap-run.ts";
|
||||
import { loadSessionList, type SessionListBoot } from "../src/lib/session-list-context.tsx";
|
||||
@@ -19,6 +24,7 @@ import { installWindow } from "./home-bootstrap-fake-timers.ts";
|
||||
// TASK-home-first-load-20260930 T1 (BUG-1127): the cold boot reads the account,
|
||||
// the people list and the remembered person's sessions in one round. Synthetic
|
||||
// data only.
|
||||
Object.assign(globalThis, { React });
|
||||
|
||||
const ACCOUNT_ID = "11111111-1111-4111-8111-111111111127";
|
||||
const OTHER_ID = "33333333-3333-4333-8333-333333333127";
|
||||
@@ -452,3 +458,29 @@ test("an incomplete profile waits for the onboarding shell before reveal; a comp
|
||||
}
|
||||
restoreStorage();
|
||||
});
|
||||
|
||||
test("a preloaded onboarding shell renders in the reveal frame, not behind an empty fallback (T7)", async () => {
|
||||
await loadHomeOnboardingShell();
|
||||
const markup = renderToStaticMarkup(
|
||||
<HomeOnboardingShellSlot
|
||||
profileComplete={false}
|
||||
profileDraft={emptyProfile}
|
||||
setProfileDraft={() => undefined}
|
||||
onboardingStep="name"
|
||||
presetMessageLength={presetOnboardingMessage.length}
|
||||
currentOnboardingMessage={presetOnboardingMessage}
|
||||
onboardingJustCompleted={false}
|
||||
onboardingCardReady={false}
|
||||
presetMessageFinished
|
||||
birthTimeJourney={null}
|
||||
birthTimeAssessmentPhase={null}
|
||||
birthTimeError=""
|
||||
birthTimeGuided={{} as BirthTimeGuidedController}
|
||||
profileSaving={false}
|
||||
saveOnboardingBirth={() => undefined}
|
||||
saveOnboardingPlace={() => undefined}
|
||||
retryBirthTimeAssessment={() => undefined}
|
||||
/>,
|
||||
);
|
||||
assert.match(markup, /你好,我是 Jyotisha/);
|
||||
});
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
type HomeWarmStartInput,
|
||||
} from "../src/lib/home-warm-start.ts";
|
||||
import type { Account, ChatSession, Profile } from "../src/lib/home-types.ts";
|
||||
import { ensureChinaLocations } from "../src/lib/china-locations-client.ts";
|
||||
|
||||
// BUG-1040 T1: the warm snapshot store and the warm-start decision, as pure
|
||||
// functions. Synthetic ids and profiles only.
|
||||
@@ -77,6 +78,12 @@ function withStorage<T>(run: () => T): T {
|
||||
}
|
||||
}
|
||||
|
||||
// 原值:夹具 selfProfile 只有省市编码,直接可判为资料完整。
|
||||
// 新值:用例前先加载城市表(ensureChinaLocations),断言不变。
|
||||
// 原因:TASK-home-first-load T7 把城市表移出首屏,只有编码的老资料要等表加载后才能解析地点;
|
||||
// 正式流程在启动时(人物目录读完之前)就会加载,这里补上同样的一步。
|
||||
test.before(async () => { await ensureChinaLocations(); });
|
||||
|
||||
test("write → read returns the snapshot for the same account", () => {
|
||||
clearHomeWarmSnapshot();
|
||||
writeHomeWarmSnapshot(snapshotFor(USER));
|
||||
@@ -123,7 +130,10 @@ test("sign-out and the provider's signed-out and account-change branches clear t
|
||||
const provider = readFileSync(new URL("../src/lib/session-list-context.tsx", import.meta.url), "utf8");
|
||||
assert.match(provider, /accountResult\.status === 401\) \{\n\s+clearHomeWarmSnapshot\(\);/);
|
||||
assert.match(provider, /sessionResponse\.status === 401\) \{\n\s+clearHomeWarmSnapshot\(\);/);
|
||||
assert.match(provider, /readCurrentSubjectAccountId\(\) !== accountPayload\.user\.id\) \{\n(?:\s+\/\/.*\n)*\s+clearHomeWarmSnapshot\(\);/);
|
||||
// 原值:/readCurrentSubjectAccountId\(\) !== accountPayload\.user\.id\) \{ … clearHomeWarmSnapshot\(\);/
|
||||
// 新值:先 `const boundAccount = readCurrentSubjectAccountId();`,再 `boundAccount !== accountPayload.user.id) { … clearHomeWarmSnapshot();`
|
||||
// 原因:TASK-home-first-load T1 要用同一个值判断「是否已有绑定账户」(首登不作废刚读到的人物目录);换账户仍先清快照。
|
||||
assert.match(provider, /const boundAccount = readCurrentSubjectAccountId\(\);\n\s+if \(boundAccount !== accountPayload\.user\.id\) \{\n(?:\s+\/\/.*\n)*\s+clearHomeWarmSnapshot\(\);/);
|
||||
});
|
||||
|
||||
test("the snapshot lives in memory only", () => {
|
||||
|
||||
Reference in New Issue
Block a user