- 建档重读人物名单时保留旧名单,失败也不卡在加载画面(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
93 lines
3.3 KiB
TypeScript
93 lines
3.3 KiB
TypeScript
/**
|
|
* BUG-1455: after the page has revealed with a people list, a reload must not
|
|
* drop the list (that put the whole chat back on 「正在载入账户」), and a failed
|
|
* reload keeps the old list. Synthetic account and person only.
|
|
*/
|
|
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import test from "node:test";
|
|
|
|
import {
|
|
currentSubjectCatalogReady,
|
|
loadSubjectCatalog,
|
|
peekSubjectCatalog,
|
|
refreshSubjectCatalog,
|
|
resetSubjectCatalogForTests,
|
|
subscribeSubjectCatalog,
|
|
} from "../src/lib/current-subject.ts";
|
|
import { emptyProfile, type Account } from "../src/lib/home-types.ts";
|
|
|
|
const account: Account = {
|
|
user: { id: "11111111-1111-4111-8111-111111111111", email: null },
|
|
profile: { ...emptyProfile, name: "Synthetic" },
|
|
avatar: null, credits: 10, isAdmin: false, adminUrl: null,
|
|
rectificationPriceCredits: 0, activeSubscription: null,
|
|
hasConfirmedBirthTime: false, hasUsableBirthTime: false,
|
|
};
|
|
|
|
const person = {
|
|
id: "person-1",
|
|
role: "other",
|
|
profile: { ...emptyProfile, name: "Synthetic Friend" },
|
|
updated_at: "2026-10-27T00:00:00.000Z",
|
|
};
|
|
|
|
function stubFetch(libraryResponses: Array<() => Response>) {
|
|
const original = globalThis.fetch;
|
|
let libraryCalls = 0;
|
|
globalThis.fetch = (async (input: unknown) => {
|
|
const url = String(input);
|
|
if (url === "/api/account") return new Response(JSON.stringify(account), { status: 200 });
|
|
if (url === "/api/chart-profiles") {
|
|
const next = libraryResponses[Math.min(libraryCalls, libraryResponses.length - 1)];
|
|
libraryCalls += 1;
|
|
return next();
|
|
}
|
|
return new Response("{}", { status: 404 });
|
|
}) as typeof fetch;
|
|
return () => { globalThis.fetch = original; };
|
|
}
|
|
|
|
const ok = () => new Response(JSON.stringify({ profiles: [person] }), { status: 200 });
|
|
const failed = () => new Response("{}", { status: 500 });
|
|
|
|
test("a refresh after reveal keeps the list ready while it reloads", async () => {
|
|
resetSubjectCatalogForTests();
|
|
const restore = stubFetch([ok]);
|
|
try {
|
|
await loadSubjectCatalog();
|
|
assert.equal(currentSubjectCatalogReady(), true);
|
|
const readiness: boolean[] = [];
|
|
const off = subscribeSubjectCatalog(() => readiness.push(currentSubjectCatalogReady()));
|
|
const pending = refreshSubjectCatalog();
|
|
assert.equal(currentSubjectCatalogReady(), true, "the page must not fall back to the loading screen");
|
|
assert.ok(peekSubjectCatalog()?.some((row) => row.id === "person-1"));
|
|
await pending;
|
|
off();
|
|
assert.ok(readiness.every(Boolean), `ready never drops: ${readiness.join(",")}`);
|
|
} finally {
|
|
restore();
|
|
resetSubjectCatalogForTests();
|
|
}
|
|
});
|
|
|
|
test("a failed refresh keeps the full list instead of a degraded one", async () => {
|
|
resetSubjectCatalogForTests();
|
|
const restore = stubFetch([ok, failed]);
|
|
try {
|
|
await loadSubjectCatalog();
|
|
await assert.rejects(refreshSubjectCatalog());
|
|
assert.equal(currentSubjectCatalogReady(), true);
|
|
assert.ok(peekSubjectCatalog()?.some((row) => row.id === "person-1"));
|
|
} finally {
|
|
restore();
|
|
resetSubjectCatalogForTests();
|
|
}
|
|
});
|
|
|
|
test("onboarding reloads the list without invalidating it", () => {
|
|
const source = readFileSync(new URL("../src/hooks/use-profile-onboarding.ts", import.meta.url), "utf8");
|
|
assert.doesNotMatch(source, /invalidateSubjectCatalog/);
|
|
assert.match(source, /refreshSubjectCatalog\(\)/);
|
|
});
|