/** * Feedback tab behavior tests for AdminConsolePanel. Covers feedback detail * rendering, community grouping, status (honest and read-only), severed * community, list refetch on back-nav, and canMutate gates. */ import assert from "node:assert/strict"; import { afterEach, test } from "node:test"; import { act, fireEvent, setIpcHandler, resetTestState, mountPanel, settle, CM_ORIGIN, CM_PUBKEY, capturedToasts, capturedErrorToasts, makeCmFalseFeedback, } from "./adminConsolePanelTestHelpers.jsdom.mjs"; afterEach(resetTestState); // ── Feedback fixture builder ────────────────────────────────────────────────── // // Builds a matched (summary, detail) pair with explicit overrides for the // fields that vary across feedback test scenarios. Reload handlers, IDs, // counters, and per-test assertions stay test-local. function makeFeedbackFixtures({ id = "00000000-0000-0000-0000-000000000099", communityId = "comm-1", communityHost = "relay.example.com", submitterPubkey = "sub-fixture", category = "bug", bodySummary = "Fixture feedback summary", body = "Fixture feedback full body", status = "new", eventId = "feedevent-fixture", eventCreatedAt = "2024-06-01T09:00:00Z", receivedAt = "2024-06-01T09:00:00Z", } = {}) { const summary = { id, communityId, communityHost, submitterPubkey, category, bodySummary, status, receivedAt, }; const detail = { id, communityId, communityHost, eventId, submitterPubkey, category, body, status, tags: [], eventCreatedAt, receivedAt, }; return { summary, detail }; } test("feedback-detail-renders-structured-fields: FeedbackDetail shows field layout, not raw JSON", async () => { // Verifies item 3: the feedback detail view renders data-testid='feedback-detail-fields'. // Lives here (jsdom) because tab switching and item navigation require fireEvent.click. // // Mutation evidence: revert FeedbackFields →
{JSON.stringify(...)}
// → this test goes red ("feedback-detail-fields element must render"). const origin = "https://admin.example.com"; const pubkey = "6".repeat(64); // Summary shape returned by GET /admin/feedback (FeedbackSummary wire type). const feedbackSummary = { id: "00000000-0000-0000-0000-000000000011", communityId: "00000000-0000-0000-0000-000000000022", communityHost: "relay.example.com", submitterPubkey: "submitter001pubkey", category: "bug", bodySummary: "App crashes on startup", status: "new", receivedAt: "2024-05-01T09:00:05Z", }; // Full AdminFeedbackDto shape returned by GET /admin/feedback/:id. const feedbackDetail = { id: "00000000-0000-0000-0000-000000000011", communityId: "00000000-0000-0000-0000-000000000022", communityHost: "relay.example.com", eventId: "feedevent001", submitterPubkey: "submitter001pubkey", category: "bug", body: "App crashes on startup — full detail body text", status: "new", tags: [], eventCreatedAt: "2024-05-01T09:00:00Z", receivedAt: "2024-05-01T09:00:05Z", }; setIpcHandler("admin_list_reports", () => Promise.resolve([])); setIpcHandler("admin_list_feedback", () => Promise.resolve([feedbackSummary]), ); setIpcHandler("admin_get_feedback", () => Promise.resolve(feedbackDetail)); const { container, doRender, unmount } = mountPanel({ origin, pubkey }); await doRender(); await settle(30); // Click the Feedback tab. const feedbackTab = container.querySelector( "[data-testid='admin-tab-feedback']", ); assert.ok(feedbackTab, "Feedback tab must be present"); await act(async () => { fireEvent.click(feedbackTab); await new Promise((r) => setTimeout(r, 30)); }); await settle(30); // Pre-navigation: list row shows the summary body text (bodySummary rendered). // Mutation seam: render `body` instead of `bodySummary` → red because summary // fixture has no `body` field → row title is blank. const listText = container.textContent ?? ""; assert.ok( listText.includes("App crashes on startup"), `list row must show bodySummary before navigation; got: ${listText.slice(0, 400)}`, ); // Navigate into the feedback detail — click the first non-tab button. const allButtons = container.querySelectorAll("button"); for (const btn of allButtons) { const testid = btn.getAttribute("data-testid") ?? ""; if (testid.match(/^(admin-tab|community-badge)/)) continue; await act(async () => { fireEvent.click(btn); await new Promise((r) => setTimeout(r, 30)); }); break; } await settle(30); const fields = container.querySelector( "[data-testid='feedback-detail-fields']", ); assert.ok( fields !== null, "feedback-detail-fields element must render — JSON dump not replaced", ); const text = container.textContent ?? ""; assert.ok( !text.includes('"body":'), `raw JSON must not be rendered in feedback detail; got: ${text.slice(0, 400)}`, ); // Real DTO fields must render. assert.ok( text.includes("submitter001pubkey"), `submitterPubkey must render; got: ${text.slice(0, 600)}`, ); assert.ok( text.includes("bug"), `category must render; got: ${text.slice(0, 600)}`, ); assert.ok( text.includes("App crashes on startup"), `body must render; got: ${text.slice(0, 600)}`, ); // Fake fields must NOT appear. assert.ok( !text.includes("appVersion"), `invented 'appVersion' field must not render; got: ${text.slice(0, 400)}`, ); assert.ok( !text.includes("authorPubkey"), `invented 'authorPubkey' field must not render; got: ${text.slice(0, 400)}`, ); // Relative timestamp: formatTimestamp output must match "Xm/h/d ago (...)" shape. // The fixture receivedAt is far in the past, so it will be "Nd ago (...)". assert.ok( /\d+[mhd] ago \(/.test(text) || text.includes("just now ("), `relative timestamp must render in "Nm/h/d ago (...)" format; got: ${text.slice(0, 600)}`, ); await unmount(); }); // ── NIP-11 auto-discovery ───────────────────────────────────────────────── test("feedback-grouped-by-community: multi-community feedback renders per-community headings", async () => { // Same grouping contract for the Feedback tab. // // Mutation evidence: revert FeedbackTab to a flat