Skip to content

Commit 610bfab

Browse files
saagar210claude
andcommitted
test(components): cover Analytics section components individually
Add focused tests for the five Analytics siblings extracted in this PR: RatingDistribution (empty + populated), KbUsageTable (empty + click callback), QualityDrilldownExamples (null payload + populated + format helper), ResponseQualityPanel (empty + metric grid), PilotDiagnosticsSection (policy load + reject fallback). +11 tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 7a4567d commit 610bfab

5 files changed

Lines changed: 275 additions & 0 deletions

File tree

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// @vitest-environment jsdom
2+
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
3+
import { afterEach, describe, expect, it, vi } from "vitest";
4+
import { KbUsageTable } from "./KbUsageTable";
5+
6+
const articles = [
7+
{ document_id: "doc-1", title: "Remote Work Policy", usage_count: 12 },
8+
{ document_id: "doc-2", title: "VPN Setup Guide", usage_count: 7 },
9+
];
10+
11+
describe("KbUsageTable", () => {
12+
afterEach(() => cleanup());
13+
14+
it("renders an empty state when there are no articles", () => {
15+
render(<KbUsageTable articles={[]} />);
16+
expect(screen.getByText("No article usage data yet")).toBeTruthy();
17+
});
18+
19+
it("fires onArticleClick with the document_id when a row is clicked", () => {
20+
const onArticleClick = vi.fn();
21+
render(
22+
<KbUsageTable articles={articles} onArticleClick={onArticleClick} />,
23+
);
24+
25+
fireEvent.click(screen.getByText("VPN Setup Guide"));
26+
expect(onArticleClick).toHaveBeenCalledWith("doc-2");
27+
});
28+
});
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// @vitest-environment jsdom
2+
import { cleanup, render, screen, waitFor } from "@testing-library/react";
3+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
4+
import { PilotDiagnosticsSection } from "./PilotDiagnosticsSection";
5+
6+
const invokeMock = vi.fn();
7+
8+
vi.mock("@tauri-apps/api/core", () => ({
9+
invoke: (...args: unknown[]) => invokeMock(...args),
10+
}));
11+
12+
vi.mock("../Pilot", () => ({
13+
PilotDashboard: ({
14+
pilotLoggingEnabled,
15+
}: {
16+
pilotLoggingEnabled: boolean;
17+
}) => (
18+
<div data-testid="pilot-dashboard">
19+
enabled:{String(pilotLoggingEnabled)}
20+
</div>
21+
),
22+
PilotQueryTester: ({
23+
pilotLoggingEnabled,
24+
}: {
25+
pilotLoggingEnabled: boolean;
26+
}) => (
27+
<div data-testid="pilot-query-tester">
28+
enabled:{String(pilotLoggingEnabled)}
29+
</div>
30+
),
31+
}));
32+
33+
describe("PilotDiagnosticsSection", () => {
34+
beforeEach(() => {
35+
invokeMock.mockReset();
36+
});
37+
afterEach(() => cleanup());
38+
39+
it("loads the pilot logging policy and passes enabled=true to children", async () => {
40+
invokeMock.mockResolvedValue({
41+
enabled: true,
42+
retention_days: 14,
43+
max_rows: 500,
44+
});
45+
46+
render(<PilotDiagnosticsSection />);
47+
48+
await waitFor(() => {
49+
expect(invokeMock).toHaveBeenCalledWith("get_pilot_logging_policy");
50+
});
51+
52+
await waitFor(() => {
53+
expect(screen.getByTestId("pilot-query-tester").textContent).toContain(
54+
"enabled:true",
55+
);
56+
});
57+
});
58+
59+
it("falls back to a disabled policy when the invoke call rejects", async () => {
60+
invokeMock.mockRejectedValue(new Error("backend down"));
61+
62+
render(<PilotDiagnosticsSection />);
63+
64+
await waitFor(() => {
65+
expect(screen.getByTestId("pilot-dashboard").textContent).toContain(
66+
"enabled:false",
67+
);
68+
});
69+
});
70+
});
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// @vitest-environment jsdom
2+
import { cleanup, render, screen } from "@testing-library/react";
3+
import { afterEach, describe, expect, it } from "vitest";
4+
import {
5+
QualityDrilldownExamples,
6+
formatDrilldownMetric,
7+
} from "./QualityDrilldownExamples";
8+
9+
describe("formatDrilldownMetric", () => {
10+
it("formats each signal's metric string", () => {
11+
expect(formatDrilldownMetric("edit_ratio", 0.42)).toBe("42.0% edit ratio");
12+
expect(formatDrilldownMetric("time_to_draft", 12000)).toBe(
13+
"12.0s to draft",
14+
);
15+
expect(formatDrilldownMetric("copy_per_save", 99)).toBe(
16+
"Saved without copy",
17+
);
18+
expect(formatDrilldownMetric("edited_save_rate", 0.8)).toBe(
19+
"80.0% edit ratio",
20+
);
21+
});
22+
});
23+
24+
describe("QualityDrilldownExamples", () => {
25+
afterEach(() => cleanup());
26+
27+
it("renders null when there is no drilldown payload", () => {
28+
const { container } = render(
29+
<QualityDrilldownExamples signalId="edit_ratio" drilldown={null} />,
30+
);
31+
expect(container.innerHTML).toBe("");
32+
});
33+
34+
it("renders up to three draft examples for the given signal", () => {
35+
render(
36+
<QualityDrilldownExamples
37+
signalId="edit_ratio"
38+
drilldown={{
39+
edit_ratio: [
40+
{
41+
draft_id: "d1",
42+
created_at: "2026-04-01",
43+
metric_value: 0.5,
44+
draft_excerpt: "Drift example one",
45+
},
46+
{
47+
draft_id: "d2",
48+
created_at: "2026-04-02",
49+
metric_value: 0.7,
50+
draft_excerpt: "Drift example two",
51+
},
52+
],
53+
time_to_draft: [],
54+
copy_per_save: [],
55+
edited_save_rate: [],
56+
}}
57+
/>,
58+
);
59+
expect(screen.getByText("d1")).toBeTruthy();
60+
expect(screen.getByText("d2")).toBeTruthy();
61+
expect(screen.getByText("Drift example one")).toBeTruthy();
62+
});
63+
});
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// @vitest-environment jsdom
2+
import { cleanup, render, screen } from "@testing-library/react";
3+
import { afterEach, describe, expect, it } from "vitest";
4+
import type { AnalyticsSummary } from "../../hooks/useAnalytics";
5+
import { RatingDistribution } from "./RatingDistribution";
6+
7+
function makeSummary(
8+
overrides: Partial<AnalyticsSummary> = {},
9+
): AnalyticsSummary {
10+
return {
11+
responses_generated: 0,
12+
searches_performed: 0,
13+
drafts_saved: 0,
14+
average_rating: 0,
15+
total_ratings: 0,
16+
rating_distribution: [0, 0, 0, 0, 0],
17+
daily_counts: [],
18+
...overrides,
19+
} as AnalyticsSummary;
20+
}
21+
22+
describe("RatingDistribution", () => {
23+
afterEach(() => cleanup());
24+
25+
it("shows an empty state when there are zero ratings", () => {
26+
render(<RatingDistribution summary={makeSummary()} />);
27+
expect(screen.getByText("No ratings yet")).toBeTruthy();
28+
});
29+
30+
it("renders five rows with the real per-star counts", () => {
31+
render(
32+
<RatingDistribution
33+
summary={makeSummary({
34+
total_ratings: 10,
35+
rating_distribution: [1, 2, 0, 3, 4],
36+
})}
37+
/>,
38+
);
39+
40+
// 5 stars
41+
expect(screen.getByText("5 stars")).toBeTruthy();
42+
// 1 star (singular)
43+
expect(screen.getByText("1 star")).toBeTruthy();
44+
// Counts present (4 for 5-star, 1 for 1-star)
45+
const counts = screen.getAllByText(/^[0-9]+$/);
46+
expect(counts.length).toBeGreaterThanOrEqual(5);
47+
});
48+
});
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// @vitest-environment jsdom
2+
import { cleanup, render, screen } from "@testing-library/react";
3+
import { afterEach, describe, expect, it, vi } from "vitest";
4+
import { ResponseQualityPanel } from "./ResponseQualityPanel";
5+
import type { ResponseQualityThresholds } from "../../features/analytics/qualityThresholds";
6+
7+
vi.mock("../../features/analytics/qualityCoaching", () => ({
8+
buildResponseQualityCoaching: () => null,
9+
}));
10+
vi.mock("../../features/analytics/operatorScorecard", () => ({
11+
buildOperatorScorecard: () => null,
12+
}));
13+
vi.mock("../../features/inbox/queueModel", () => ({
14+
loadQueueHandoffSnapshot: () => null,
15+
}));
16+
17+
const thresholds: ResponseQualityThresholds = {
18+
edit_ratio_watch: 0.3,
19+
edit_ratio_action: 0.5,
20+
time_to_draft_watch_ms: 30000,
21+
time_to_draft_action_ms: 60000,
22+
copy_per_save_watch: 0.5,
23+
copy_per_save_action: 0.3,
24+
edited_save_rate_watch: 0.5,
25+
edited_save_rate_action: 0.7,
26+
};
27+
28+
describe("ResponseQualityPanel", () => {
29+
afterEach(() => cleanup());
30+
31+
it("shows the empty state when there are no snapshots", () => {
32+
render(
33+
<ResponseQualityPanel
34+
summary={null}
35+
thresholds={thresholds}
36+
drilldown={null}
37+
/>,
38+
);
39+
expect(
40+
screen.getByText("No response quality snapshots captured yet"),
41+
).toBeTruthy();
42+
});
43+
44+
it("renders the metric grid when a snapshot summary is provided", () => {
45+
render(
46+
<ResponseQualityPanel
47+
summary={{
48+
snapshots_count: 4,
49+
avg_word_count: 82,
50+
avg_edit_ratio: 0.12,
51+
edited_save_rate: 0.4,
52+
avg_time_to_draft_ms: 5400,
53+
median_time_to_draft_ms: 4200,
54+
copy_per_saved_ratio: 0.9,
55+
saved_count: 3,
56+
}}
57+
thresholds={thresholds}
58+
drilldown={null}
59+
/>,
60+
);
61+
expect(screen.getByText("Snapshots")).toBeTruthy();
62+
expect(screen.getByText("4")).toBeTruthy();
63+
expect(screen.getByText("Avg Words")).toBeTruthy();
64+
expect(screen.getByText("82")).toBeTruthy();
65+
});
66+
});

0 commit comments

Comments
 (0)