thread-master/apps/web/src/pages/RadarTodayPage.test.tsx

138 lines
4.0 KiB
TypeScript
Raw Normal View History

2026-08-03 05:52:02 +00:00
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { MemoryRouter } from "react-router-dom";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { KEYS } from "../data/mock/keys";
import { I18nProvider } from "../i18n/I18nContext";
import { translate } from "../lib/i18n/messages";
import type { Opportunity, RadarToday } from "../domain/types";
import { RadarTodayPage } from "./RadarTodayPage";
const t = (key: string, params?: Record<string, string | number>) => translate("zh-TW", key, params);
const backend = vi.hoisted(() => ({
today: null as RadarToday | null,
}));
function sampleOpp(id: string, band: "high" | "mid" | "low", score: number): Opportunity {
return {
id,
source: "threads",
external_id: id,
permalink: `https://example.com/${id}`,
author_handle: "seeker",
text: `商機內文 ${id}`,
posted_at: Date.now() * 1e6,
status: "qualified",
intent_score: score,
intent_band: band,
reasons: [
{ dimension: "authenticity", score: 20, reason: "真" },
{ dimension: "intent", score: 20, reason: "想買" },
{ dimension: "region", score: 10, reason: "地區" },
{ dimension: "freshness", score: 10, reason: "新" },
{ dimension: "fit", score: 10, reason: "合" },
],
region_match: "unknown",
freshness_hours: 2,
matched_terms: ["水電"],
created_at: Date.now() * 1e6,
};
}
vi.mock("../data/DataContext", () => ({
useRepos: () => ({
radar: {
async getToday() {
if (!backend.today) throw new Error("no today");
return backend.today;
},
async acceptOpportunity() {
return { opportunity_id: "x", status: "accepted" };
},
async dismissOpportunity(id: string) {
return sampleOpp(id, "high", 80);
},
async createReply() {
return {
id: "r1",
opportunity_id: "h1",
variant: "public_comment" as const,
text: "草稿",
created_at: 1,
};
},
async markReplyUsed() {
return {
reply: {
id: "r1",
opportunity_id: "h1",
variant: "public_comment" as const,
text: "草稿",
used_at: 2,
sent_channel: "manual_copy" as const,
created_at: 1,
},
};
},
async overrideOpportunity(id: string) {
return sampleOpp(id, "mid", 60);
},
},
}),
}));
function renderPage() {
return render(
<MemoryRouter>
<I18nProvider>
<RadarTodayPage />
</I18nProvider>
</MemoryRouter>,
);
}
describe("RadarTodayPage", () => {
beforeEach(() => {
backend.today = null;
localStorage.setItem(
KEYS.uiPrefs,
JSON.stringify({ locale: "zh-TW", currency: "TWD", theme: "system" }),
);
});
it("shows empty reason and CTA when total is 0", async () => {
backend.today = {
stats: { total: 0, high: 0, mid: 0, low: 0 },
high: [],
mid: [],
low: [],
truncated_count: 0,
empty_reason: "no_watch",
empty_hint: "建立訂閱",
};
renderPage();
expect(await screen.findByText("建立訂閱")).toBeTruthy();
expect(screen.getByRole("link", { name: t("radar.today.empty.goWatches") })).toBeTruthy();
});
it("groups high/mid and collapses low by default", async () => {
backend.today = {
stats: { total: 3, high: 1, mid: 1, low: 1 },
high: [sampleOpp("h1", "high", 90)],
mid: [sampleOpp("m1", "mid", 60)],
low: [sampleOpp("l1", "low", 30)],
truncated_count: 2,
};
renderPage();
expect(await screen.findByText("商機內文 h1")).toBeTruthy();
expect(screen.getByText("商機內文 m1")).toBeTruthy();
expect(screen.queryByText("商機內文 l1")).toBeNull();
expect(screen.getByText(t("radar.today.truncated", { n: 2 }), { exact: false })).toBeTruthy();
fireEvent.click(screen.getByRole("button", { name: /低意向/ }));
await waitFor(() => {
expect(screen.getByText("商機內文 l1")).toBeTruthy();
});
});
});