Files
Jyotisha/frontend/tests/report-row-delete.test.tsx
T
jesse-uxandClaude Code 4d801e53c3
Independent Staging Quality Gate / validate (push) Successful in 16m28s
Independent Staging Quality Gate / publish (push) Successful in 3m33s
fix: integrate people archive, report reader and western chart corrections
Validate on Linux Node 22 and PostgreSQL 17: 3894 frontend tests and 64 database tests pass, with no removed test names or new failures. Preserve static Home, bounded gzip, assertion-change records and manual acceptance gaps.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-25 20:41:16 +08:00

99 lines
6.4 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 { bindCurrentSubjectAccount } = await import("../src/lib/current-subject.ts");
bindCurrentSubjectAccount("fictional-report-owner", []);
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(); }
});