Files
Jyotisha/frontend/tests/report-row-delete.test.tsx
T
jesse-ux fabe0114dd
Independent Staging Quality Gate / validate (push) Successful in 10m5s
Independent Staging Quality Gate / publish (push) Successful in 3m23s
fix(chart): keep subject-scoped failures visible and follow the new routes
A 401 for the current person was stored, then hidden because the subject stamp only happened on success. Session, report, and birth contracts now follow the subject query and the shared birth lookup. Admin reset fixtures can no longer insert a retired self mirror.
2026-09-25 04:51:49 +08:00

97 lines
6.3 KiB
TypeScript

import assert from "node:assert/strict";
import test from "node:test";
import React from "react";
import { AppRouterContext, type AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime";
import { SidebarProvider } from "../src/components/ui/sidebar";
import { ReportRowDelete } from "../src/components/personal-report/report-row-delete";
import { PersonalReportCenter, rowMeta } from "../src/components/personal-report/personal-report-center";
import { readReports, invalidateReportsPage, peekReportsPage, refreshReportsPage, writeReportsPage, type ReportListItem } from "../src/lib/secondary-page-data";
import { createClientLifecycleHarness } from "./react-client-lifecycle-test-support";
Object.assign(globalThis, { React });
type Harness = ReturnType<typeof createClientLifecycleHarness>;
const buttons = (h: Harness, text: string) => h.elements().filter(node => node.tagName === "BUTTON" && node.text === text);
function deferred<T>() { let resolve!: (value: T) => void; const promise = new Promise<T>(done => { resolve = done; }); return { promise, resolve }; }
// Explicitly fictional list metadata, not an engine-contract fixture.
const row: ReportListItem = { id: "fictional-report", reportType: "personal_full", presentationMode: "default", depth: "standard", themes: ["career"], status: "ready", failureCode: null, createdAt: "2026-09-24T12:00:00Z", completedAt: null };
test("report metadata localizes depth and themes while preserving unknown identifiers", () => {
assert.equal(rowMeta(row), "2026年9月24日 · 标准深度 · 事业");
assert.equal(rowMeta({ ...row, depth: "future-depth", themes: ["future-theme"], createdAt: "invalid" }), "时间未知 · future-depth · future-theme");
assert.match(rowMeta({ ...row, themes: [] }), /综合主题$/);
assert.match(rowMeta(readReports({ reports: [{ ...row, depth: "future-depth" }] })[0]), /future-depth/);
});
test("row delete is confirmed inline, cancelled without request and retains failure for retry", async () => {
const h = createClientLifecycleHarness(); const fetchBefore = globalThis.fetch;
let calls = 0; let removed = 0;
globalThis.fetch = async () => { calls += 1; return new Response(null, { status: 500 }); };
try {
await h.render(<ReportRowDelete reportId={row.id} onDeleted={() => { removed += 1; }} />);
await h.event(buttons(h, "删除")[0]);
assert.match(h.container.text, /确定删除这份报告?/);
await h.event(buttons(h, "取消")[0]);
assert.equal(calls, 0);
assert.equal(document.activeElement, h.elements().find(node => node.tagName === "SUMMARY"));
await h.event(buttons(h, "删除")[0]);
await h.event(buttons(h, "删除")[1]);
await h.idle();
assert.match(h.container.text, /报告没能删除,请稍后重试。/);
assert.equal(removed, 0); assert.equal(calls, 1);
globalThis.fetch = async () => { throw new Error("offline"); };
await h.event(buttons(h, "删除")[1]); await h.idle();
assert.match(h.container.text, /报告没能删除,请稍后重试。/);
assert.equal(removed, 0);
} finally { globalThis.fetch = fetchBefore; await h.close(); }
});
test("row delete sends one same-origin DELETE despite repeated confirmation clicks", async () => {
const h = createClientLifecycleHarness(); const fetchBefore = globalThis.fetch;
const response = deferred<Response>(); let calls = 0; const removed: string[] = [];
globalThis.fetch = async (url, options) => { assert.equal(url, `/api/reports/${row.id}`); assert.equal(options?.method, "DELETE"); assert.equal(options?.credentials, "same-origin"); calls += 1; return response.promise; };
try {
await h.render(<ReportRowDelete reportId={row.id} onDeleted={id => removed.push(id)} />);
await h.event(buttons(h, "删除")[0]);
const confirm = buttons(h, "删除")[1];
await h.event(confirm); await h.event(confirm);
assert.equal(calls, 1); assert.match(h.container.text, /删除中/);
response.resolve(new Response(null, { status: 204 })); await h.idle();
assert.deepEqual(removed, [row.id]);
} finally { globalThis.fetch = fetchBefore; await h.close(); }
});
test("report cache invalidation rejects stale GET writes and stale finally keeps newer flight", async () => {
const fetchBefore = globalThis.fetch; const first = deferred<Response>(); const second = deferred<Response>(); let calls = 0;
invalidateReportsPage();
globalThis.fetch = () => (++calls === 1 ? first.promise : second.promise);
try {
const stale = refreshReportsPage(); invalidateReportsPage(); const fresh = refreshReportsPage();
first.resolve(Response.json({ reports: [row] })); await stale;
assert.equal(peekReportsPage(), null);
assert.equal(refreshReportsPage(), fresh); assert.equal(calls, 2);
second.resolve(Response.json({ reports: [] })); await fresh;
assert.deepEqual(peekReportsPage(), { kind: "ready", reports: [] });
} finally { globalThis.fetch = fetchBefore; invalidateReportsPage(); }
});
test("mounted report center removes deleted row and an older GET cannot resurrect it", async () => {
const h = createClientLifecycleHarness(); const fetchBefore = globalThis.fetch;
const stale = deferred<Response>(); let gets = 0;
writeReportsPage({ kind: "ready", reports: [row] });
globalThis.fetch = async (url, options) => {
if (options?.method === "DELETE") return new Response(null, { status: 204 });
if (String(url).startsWith("/api/reports")) { gets += 1; return stale.promise; }
return Response.json({}, { status: 401 });
};
try {
const router = { bfcacheId: "test", push() {}, replace() {}, refresh() {}, prefetch() {}, back() {}, forward() {} } as AppRouterInstance;
await h.render(<AppRouterContext.Provider value={router}><SidebarProvider open><PersonalReportCenter /></SidebarProvider></AppRouterContext.Provider>); await h.idle();
assert.equal(gets, 1); assert.match(h.container.text, /标准深度/);
await h.event(buttons(h, "删除")[0]); await h.event(buttons(h, "删除")[1]); await h.idle();
assert.doesNotMatch(h.container.text, /标准深度/); assert.equal(peekReportsPage(), null);
stale.resolve(Response.json({ reports: [row] })); await h.idle();
assert.doesNotMatch(h.container.text, /标准深度/); assert.equal(peekReportsPage(), null);
assert.deepEqual(h.errors, []);
} finally { globalThis.fetch = fetchBefore; invalidateReportsPage(); await h.close(); }
});