Files
Jyotisha/frontend/tests/self-edit-people-20260928.test.tsx
T

158 lines
8.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import React from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { PeopleArchiveView, type PeopleArchiveViewProps, type PersonRow } from "../src/components/people/people-page.tsx";
import { emptyProfile, type Profile } from "../src/lib/home-types.ts";
import { saveSelfGender, saveSelfProfile, selfProfilePatchBody } from "../src/lib/self-profile-save.ts";
Object.assign(globalThis, { React });
/*
* BUG-1081 / TASK-self-edit-avatar-menu-20260928 S1: the owner's birth data
* had no edit entry anywhere after /people replaced the settings chart pane.
* Fictional people only (AGENTS §8).
*/
const peopleSource = readFileSync(new URL("../src/components/people/people-page.tsx", import.meta.url), "utf8");
const onboardingSource = readFileSync(new URL("../src/hooks/use-profile-onboarding.ts", import.meta.url), "utf8");
const selfProfile: Profile = {
...emptyProfile,
name: "示例甲",
date: "1994-05-18",
time: "07:40",
reportedTime: "07:40",
birthTimeSource: "family_exact",
birthTimeStatus: "accepted",
countryCode: "CN",
birthPlaceLabel: "中国 · 浙江省 · 杭州市 · 西湖区",
latitude: 30.27,
longitude: 120.15,
timezoneId: "Asia/Shanghai",
timezoneOffset: 8,
timezoneSource: "iana",
ayanamsa: "raman",
gender: "female",
};
const otherProfile: Profile = { ...selfProfile, name: "示例乙", gender: null };
const people: PersonRow[] = [
{ id: "self", role: "self", profile: selfProfile, incomplete: false },
{ id: "11111111-1111-4111-8111-111111111111", role: "other", profile: otherProfile, incomplete: false },
];
function render(overrides: Partial<PeopleArchiveViewProps> = {}) {
const noop = () => {};
const asyncNoop = async () => {};
const props: PeopleArchiveViewProps = {
people, subjectId: "self", selected: people[0]!, selectedId: "self", view: "detail", editing: false,
draft: emptyProfile, pendingDelete: null, saving: false, atLimit: false,
onSelect: noop, onBack: noop, onAdd: noop, onEdit: noop, onCancelEdit: noop, onDraftChange: noop,
saveOther: asyncNoop, saveSelf: asyncNoop, askDelete: asyncNoop, onCancelDelete: noop, confirmDelete: asyncNoop, openPerson: noop,
...overrides,
};
return renderToStaticMarkup(React.createElement(PeopleArchiveView, props));
}
type Call = { url: string; init: RequestInit };
function fakeFetch(status: number, body: unknown, calls: Call[]): typeof fetch {
return (async (url: string | URL | Request, init?: RequestInit) => {
calls.push({ url: String(url), init: init ?? {} });
return new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
}) as typeof fetch;
}
test("本人 detail offers 编辑 and still says it cannot be deleted", () => {
const detail = render();
assert.match(detail, /class="people-archive-foot"><p>本人不能删除<\/p><button type="button" class="people-archive-secondary">编辑<\/button>/);
assert.doesNotMatch(detail, /删除这个人|确认删除/);
});
test("本人 edit view: its own title, gender like everyone, and no delete area", () => {
const editing = render({ editing: true, draft: selfProfile });
assert.match(editing, /编辑我的资料/);
assert.doesNotMatch(editing, /编辑人物|填报的出生时间保存后不能再改/);
assert.equal(editing.includes("role=\"radio\""), true);
assert.doesNotMatch(editing, /people-archive-danger|删除这个人/);
const other = render({ editing: true, selected: people[1]!, selectedId: people[1]!.id, draft: otherProfile });
assert.match(other, /编辑人物/);
assert.match(other, /删除这个人/);
});
test("the owner's birth data goes to PATCH /api/account with its time source and never with gender", async () => {
const body = selfProfilePatchBody(selfProfile);
assert.equal(body.birth_time_source, "family_exact");
assert.equal(body.reported_birth_time, "07:40");
assert.equal(body.birth_place_label, "中国 · 浙江省 · 杭州市 · 西湖区");
assert.equal("gender" in body, false, "birth-data saves never carry gender");
const calls: Call[] = [];
const fingerprint = "0123456789abcdef0123456789abcdef";
const saved = await saveSelfProfile(selfProfile, fakeFetch(200, {
birthTime: { status: "accepted", activeTime: "07:40" },
chartBirth: { fingerprint },
}, calls));
assert.equal(calls.length, 1);
assert.equal(calls[0]!.url, "/api/account");
assert.equal(calls[0]!.init.method, "PATCH");
assert.equal(JSON.parse(String(calls[0]!.init.body)).birth_time_source, "family_exact");
assert.equal(saved.fingerprint, fingerprint);
assert.equal(saved.profile.name, "示例甲");
});
test("a refused save surfaces the route's message; gender is its own one-field PATCH", async () => {
await assert.rejects(
saveSelfProfile(selfProfile, fakeFetch(400, { error: "修改出生资料时必须同时说明时间来源" }, [])),
/修改出生资料时必须同时说明时间来源/,
);
await assert.rejects(saveSelfProfile(selfProfile, fakeFetch(500, null, [])), /账户资料暂时无法保存。/);
const calls: Call[] = [];
await saveSelfGender("male", fakeFetch(200, {}, calls));
assert.deepEqual(JSON.parse(String(calls[0]!.init.body)), { gender: "male" });
});
test("星盘档案 saves 本人 through the account write, not chart_profiles, and drops every stale copy", () => {
const selfSave = peopleSource.slice(peopleSource.indexOf("async function saveSelf"), peopleSource.indexOf("async function askDelete"));
assert.ok(selfSave.length > 0);
assert.match(selfSave, /await saveSelfProfile\(draft\)/);
assert.match(selfSave, /saveSelfGender\(gender\)/);
assert.doesNotMatch(selfSave, /chart-profiles|role: "self"/);
assert.match(selfSave, /pinChartSnapshotIdentity\(/);
assert.match(selfSave, /clearHomeWarmSnapshot\(\)/);
assert.match(selfSave, /invalidateEphemerisPage\(\)/);
assert.match(selfSave, /setAccount\(/);
assert.match(selfSave, /invalidateSubjectCatalog\(\)/);
// 原值: /setNotice\(caught instanceof Error/(失败写进页面 notice,且曾以绿色成功样式显示,BUG-1121)
// 新值: /notifyError\(caught instanceof Error/(失败是 error toast)
// 原因: 2026-09-30 产品「报错和提示不要占页面布局」
assert.match(selfSave, /notifyError\(caught instanceof Error/);
});
test("guard: the product keeps at least one edit entry for the owner's birth data (BUG-1081)", () => {
// Home's `saveProfile` had no caller left once /people replaced the settings
// pane, and nothing noticed. The entry now lives on /people, on the same
// shared write the home onboarding uses.
assert.match(peopleSource, /onClick=\{\(\) => onEdit\(selected\.profile\)\}>编辑</);
assert.doesNotMatch(peopleSource, /selected\.role === "self" \? <p>本人不能删除<\/p> : \(/);
assert.match(onboardingSource, /await saveSelfProfile\(nextProfile\)/);
assert.doesNotMatch(onboardingSource, /async function saveProfile/);
});
// TASK-mobile-chart-and-confirmed-edit-20260929 M2: once the server drops a
// confirmed minute, the saved profile carries the new truth, not the old one.
test("saving a new time over a confirmed minute takes the server's new status and minute", async () => {
const confirmedSelf: Profile = { ...selfProfile, time: "07:52", birthTimeStatus: "confirmed" };
const accepted = await saveSelfProfile({ ...confirmedSelf, reportedTime: "08:05" }, fakeFetch(200, {
birthTime: { status: "accepted", activeTime: "08:05:00", activeDate: null, activeTimezoneOffset: null },
}, []));
assert.equal(accepted.profile.birthTimeStatus, "accepted");
assert.equal(accepted.profile.time, "08:05");
const reported = await saveSelfProfile({ ...confirmedSelf, birthTimeSource: "period_only", reportedTime: "" }, fakeFetch(200, {
birthTime: { status: "reported", activeTime: null, activeDate: null, activeTimezoneOffset: null },
}, []));
assert.equal(reported.profile.birthTimeStatus, "reported");
assert.equal(reported.profile.time, "");
});