feat : add analytics page
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { act } from "react";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { AnalyticsDashboard } from "./analytics-dashboard";
|
||||
import type { AnalyticsItem, PublicAnalytics } from "@/lib/analytics/model";
|
||||
|
||||
// Layout measurements belong to browser verification; the table exposes the same counts.
|
||||
vi.mock("@/components/ui/chart", () => ({
|
||||
ChartContainer: () => null, ChartTooltip: () => null, ChartTooltipContent: () => null,
|
||||
}));
|
||||
|
||||
const item = (key: string, guideCount: number, stygianCount: number): AnalyticsItem => ({
|
||||
key, name: key, imageUrl: null, rarity: null, guideCount, stygianCount,
|
||||
contributions: [{ guideId: "g0", teamSignature: "guide-team", count: guideCount }, { guideId: "s0", teamSignature: "stygian-team", count: stygianCount }],
|
||||
});
|
||||
const entries = [item("Shared", 2, 5), item("Stygian only", 0, 3)];
|
||||
const data: PublicAnalytics = {
|
||||
guides: [
|
||||
...Array.from({ length: 4 }, (_, index) => ({ id: `g${index}`, source: "guide" as const, name: `Guide ${index}` })),
|
||||
...Array.from({ length: 2 }, (_, index) => ({ id: `s${index}`, source: "stygian" as const, name: `Stygian ${index}` })),
|
||||
],
|
||||
teamGroups: [{ guideId: "g0", signature: "guide-team", count: 1 }, { guideId: "s0", signature: "stygian-team", count: 1 }],
|
||||
guideCount: 4, stygianScheduleCount: 2, guideTeamCount: 6, stygianTeamCount: 3,
|
||||
team: { character: entries, weapon: entries, artifact: entries },
|
||||
recommendation: { weapon: entries, artifact: entries },
|
||||
};
|
||||
let root: Root;
|
||||
let container: HTMLDivElement;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
||||
container = document.createElement("div");
|
||||
document.body.append(container);
|
||||
root = createRoot(container);
|
||||
});
|
||||
afterEach(async () => {
|
||||
await act(async () => root.unmount());
|
||||
container.remove();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
describe("public analytics dashboard", () => {
|
||||
it("starts with Stygian enabled and updates all five rankings and totals on toggle", async () => {
|
||||
await act(async () => root.render(<AnalyticsDashboard data={data} />));
|
||||
const toggle = container.querySelector<HTMLElement>('[role="switch"][aria-labelledby="include-stygian-label"]')!;
|
||||
expect(toggle.getAttribute("aria-checked")).toBe("true");
|
||||
expect(container.querySelector(`#${toggle.getAttribute("aria-labelledby")}`)?.textContent).toBe("รวมข้อมูล Stygian");
|
||||
expect(container.querySelectorAll("details[data-ranking]")).toHaveLength(5);
|
||||
expect(container.querySelectorAll("tbody tr")).toHaveLength(10);
|
||||
expect(container.querySelector('[aria-label="ภาพรวมข้อมูล"]')?.textContent).toContain("ทีมทั้งหมด2");
|
||||
expect(Array.from(container.querySelectorAll("tbody tr:first-child td:last-child"), (cell) => cell.textContent)).toEqual(Array(5).fill("7"));
|
||||
|
||||
const details = container.querySelector<HTMLDetailsElement>("details[data-ranking]")!;
|
||||
details.open = true;
|
||||
await act(async () => toggle.click());
|
||||
expect(toggle.getAttribute("aria-checked")).toBe("false");
|
||||
expect(details.open).toBe(true);
|
||||
expect(container.querySelectorAll("tbody tr")).toHaveLength(5);
|
||||
expect(container.textContent).not.toContain("Stygian only");
|
||||
expect(container.querySelector('[aria-label="ภาพรวมข้อมูล"]')?.textContent).toContain("ทีมทั้งหมด1");
|
||||
expect(container.querySelector('[aria-label="ภาพรวมข้อมูล"]')?.textContent).toContain("เวอร์ชัน Stygian0");
|
||||
expect(Array.from(container.querySelectorAll("tbody tr:first-child td:last-child"), (cell) => cell.textContent)).toEqual(Array(5).fill("2"));
|
||||
|
||||
await act(async () => root.render(<AnalyticsDashboard data={{ ...data, teamGroups: [...data.teamGroups, { guideId: "g1", signature: "new-team", count: 1 }] }} />));
|
||||
expect(toggle.getAttribute("aria-checked")).toBe("false");
|
||||
expect(container.querySelector('[aria-label="ภาพรวมข้อมูล"]')?.textContent).toContain("ทีมทั้งหมด2");
|
||||
});
|
||||
|
||||
it("shows empty states for sources without entries", async () => {
|
||||
await act(async () => root.render(<AnalyticsDashboard data={{
|
||||
...data, team: { character: [], weapon: [], artifact: [] }, recommendation: { weapon: [], artifact: [] },
|
||||
}} />));
|
||||
expect(container.querySelectorAll("details[data-ranking]")).toHaveLength(0);
|
||||
expect(container.querySelectorAll('[data-slot="empty-title"]')).toHaveLength(5);
|
||||
});
|
||||
|
||||
it("lets each guide be excluded and restored and supports repeated-team filtering", async () => {
|
||||
await act(async () => root.render(<AnalyticsDashboard data={data} />));
|
||||
const guide = container.querySelector<HTMLButtonElement>('[aria-label="นับไกด์ Guide 0"]')!;
|
||||
await act(async () => guide.click());
|
||||
expect(guide.getAttribute("aria-pressed")).toBe("false");
|
||||
expect(container.querySelector('[aria-label="ภาพรวมข้อมูล"]')?.textContent).toContain("ไกด์ตัวละคร3");
|
||||
expect(container.querySelector('[aria-label="ภาพรวมข้อมูล"]')?.textContent).toContain("ทีมทั้งหมด1");
|
||||
const reset = Array.from(container.querySelectorAll("button")).find((button) => button.textContent === "รวมทุกไกด์")!;
|
||||
await act(async () => reset.click());
|
||||
expect(guide.getAttribute("aria-pressed")).toBe("true");
|
||||
const deduplicate = container.querySelector<HTMLElement>('[role="switch"][aria-labelledby="ignore-repeated-teams-label"]')!;
|
||||
expect(deduplicate.getAttribute("aria-checked")).toBe("true");
|
||||
await act(async () => deduplicate.click());
|
||||
expect(deduplicate.getAttribute("aria-checked")).toBe("false");
|
||||
expect(container.querySelector('[aria-label="ภาพรวมข้อมูล"]')?.textContent).toContain("ทีมทั้งหมด2");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user