138 lines
4.0 KiB
TypeScript
138 lines
4.0 KiB
TypeScript
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();
|
|
});
|
|
});
|
|
});
|