110 lines
3.9 KiB
TypeScript
110 lines
3.9 KiB
TypeScript
import React from "react";
|
|
import "@testing-library/jest-dom/vitest";
|
|
import { fireEvent, render, screen, within } from "@testing-library/react";
|
|
import { BrowserRouter as Router } from "react-router";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { describe, expect, test, vi } from "vitest";
|
|
import AnalyticsOverview from "./AnalyticsOverview";
|
|
|
|
vi.mock("react-i18next", () => ({
|
|
useTranslation: () => ({ t: (key: string) => key, i18n: { language: "de" } }),
|
|
}));
|
|
|
|
vi.mock("i18next", () => ({
|
|
t: (key: string) => key,
|
|
}));
|
|
|
|
vi.mock("~/utils/data/useRootData", () => ({
|
|
useRootData: () => ({ authenticated: false }),
|
|
}));
|
|
|
|
const overview = {
|
|
uniqueVisitors: 1234,
|
|
pageViews: 34,
|
|
events: 5,
|
|
liveVisitors: 1,
|
|
top: {
|
|
httpReferrers: [{ name: "Direct", count: 1234 }],
|
|
urls: [
|
|
{ name: null, count: 10 },
|
|
{ name: "/pricing", count: 9 },
|
|
{ name: "/docs", count: 4 },
|
|
],
|
|
routes: [{ name: "/portals/:portal", count: 3 }],
|
|
sources: [{ name: "newsletter", count: 6 }],
|
|
via: [{ name: "partner", count: 4 }],
|
|
medium: [{ name: "email", count: 6 }],
|
|
campaign: [{ name: "spring", count: 2 }],
|
|
user: [],
|
|
},
|
|
} as any;
|
|
|
|
function renderOverview() {
|
|
return render(
|
|
<Router>
|
|
<AnalyticsOverview overview={overview} withUsers={false} />
|
|
</Router>
|
|
);
|
|
}
|
|
|
|
describe("AnalyticsOverview", () => {
|
|
test("renders top-item tabs with accessible state and mobile-friendly sizing", () => {
|
|
renderOverview();
|
|
|
|
const pagesTab = screen.getByRole("tab", { name: "analytics.pages" });
|
|
const routesTab = screen.getByRole("tab", { name: "analytics.routes" });
|
|
const tablist = pagesTab.closest('[role="tablist"]');
|
|
|
|
expect(tablist).toHaveClass("min-w-0", "overflow-x-auto");
|
|
expect(pagesTab).toHaveClass("min-h-10", "shrink-0", "whitespace-nowrap");
|
|
expect(pagesTab).toHaveAttribute("aria-selected", "true");
|
|
expect(routesTab).toHaveAttribute("aria-selected", "false");
|
|
|
|
const panel = screen.getByRole("tabpanel", { name: "analytics.pages" });
|
|
expect(panel).toHaveAttribute("id", pagesTab.getAttribute("aria-controls"));
|
|
expect(within(panel).getByText("shared.notSet")).toBeInTheDocument();
|
|
expect(within(panel).getByText("/pricing")).toBeInTheDocument();
|
|
|
|
fireEvent.click(routesTab);
|
|
|
|
expect(routesTab).toHaveAttribute("aria-selected", "true");
|
|
expect(screen.getByRole("tabpanel", { name: "analytics.routes" })).toHaveTextContent("/portals/:portal");
|
|
});
|
|
|
|
test("supports keyboard tab selection", () => {
|
|
renderOverview();
|
|
|
|
const pagesTab = screen.getByRole("tab", { name: "analytics.pages" });
|
|
fireEvent.keyDown(pagesTab, { key: "ArrowRight" });
|
|
|
|
expect(screen.getByRole("tab", { name: "analytics.routes" })).toHaveAttribute("aria-selected", "true");
|
|
});
|
|
|
|
test("formats overview numbers with the active locale", () => {
|
|
renderOverview();
|
|
|
|
expect(screen.getAllByText("1.234").length).toBeGreaterThanOrEqual(2);
|
|
});
|
|
|
|
test("keeps analytics overview free of legacy number formatters", () => {
|
|
const sourceRoot = path.resolve(process.cwd(), "apps/web/app/components/analytics");
|
|
const overviewSource = fs.readFileSync(path.join(sourceRoot, "AnalyticsOverview.tsx"), "utf8");
|
|
const chartSource = fs.readFileSync(path.join(sourceRoot, "AnalyticsOverviewChart.tsx"), "utf8");
|
|
|
|
expect(`${overviewSource}\n${chartSource}`).not.toContain("NumberUtils");
|
|
});
|
|
|
|
test("precomputes top-item max counts once per item set", () => {
|
|
const source = fs.readFileSync(path.resolve(process.cwd(), "apps/web/app/components/analytics/AnalyticsOverview.tsx"), "utf8");
|
|
|
|
expect(source).toContain("useMemo");
|
|
expect(source).toContain("const maxCount = useMemo");
|
|
expect(source).toContain('fallbackName: t("shared.notSet")');
|
|
expect(source).not.toContain('fallbackName: "?"');
|
|
expect(source).not.toContain('fallbackName: "none"');
|
|
expect(source).not.toContain("const counts = items.map");
|
|
expect(source).not.toContain("Math.max(...counts)");
|
|
});
|
|
});
|