FN-7044: split Command Center area tests
Split the oversized Command Center area test suite into focused files while preserving shared fixtures and coverage. - Extract shared date ranges, fixtures, and helpers into areas.test-harness.tsx. - Move GitHub and Signals area assertions into a dedicated sibling suite. - Keep the remaining areas.test.tsx below the 2000-line cap with existing coverage intact. Files changed: .../areas/__tests__/areas.github-signals.test.tsx | 406 +++++++++++++ .../areas/__tests__/areas.test-harness.tsx | 302 ++++++++++ .../command-center/areas/__tests__/areas.test.tsx | 648 +-------------------- 3 files changed, 730 insertions(+), 626 deletions(-) Fusion-Task-Id: FN-7044 Fusion-Task-Lineage: 63886ec9-e9c3-410e-bbc6-c5116cddb168
This commit is contained in:
@@ -0,0 +1,406 @@
|
||||
/*
|
||||
FNXC:CommandCenter 2026-06-25-00:00:
|
||||
FN-7044 extracts the GitHub and Signals Command Center area suites from areas.test.tsx so each focused test file remains under the 2000-line hard cap while preserving every assertion.
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { render, screen, fireEvent, waitFor, within } from "@testing-library/react";
|
||||
|
||||
// Mock the api() helper so the areas fetch deterministic fixtures. Keep these Vitest hoisted mocks inline in each split suite to avoid TDZ failures.
|
||||
const mocks = vi.hoisted(() => ({
|
||||
api: vi.fn(),
|
||||
backfillGithubSourceIssueClosedAt: vi.fn(),
|
||||
backfillCommitAssociationDiffStats: vi.fn(),
|
||||
fetchOrgTree: vi.fn(),
|
||||
fetchExecutorStats: vi.fn(),
|
||||
fetchSettings: vi.fn(),
|
||||
updateSettings: vi.fn(),
|
||||
toggleEnginePause: vi.fn(),
|
||||
appSettings: { globalPaused: false, enginePaused: false },
|
||||
}));
|
||||
const apiMock = mocks.api;
|
||||
const backfillGithubSourceIssueClosedAtMock = mocks.backfillGithubSourceIssueClosedAt;
|
||||
const backfillCommitAssociationDiffStatsMock = mocks.backfillCommitAssociationDiffStats;
|
||||
const fetchOrgTreeMock = mocks.fetchOrgTree;
|
||||
const fetchExecutorStatsMock = mocks.fetchExecutorStats;
|
||||
const toggleEnginePauseMock = mocks.toggleEnginePause;
|
||||
const appSettingsMock = mocks.appSettings;
|
||||
vi.mock("../../../../api/legacy", () => ({
|
||||
api: (path: string, opts?: RequestInit) => mocks.api(path, opts),
|
||||
apiBackfillGithubSourceIssueClosedAt: (options?: { offset?: number; limit?: number }, projectId?: string) =>
|
||||
mocks.backfillGithubSourceIssueClosedAt(options, projectId),
|
||||
backfillCommitAssociationDiffStats: (options?: { dryRun?: boolean }, projectId?: string) =>
|
||||
mocks.backfillCommitAssociationDiffStats(options, projectId),
|
||||
fetchOrgTree: mocks.fetchOrgTree,
|
||||
fetchExecutorStats: mocks.fetchExecutorStats,
|
||||
fetchSettings: mocks.fetchSettings,
|
||||
updateSettings: mocks.updateSettings,
|
||||
}));
|
||||
|
||||
vi.mock("../../../../hooks/useAppSettings", () => ({
|
||||
useAppSettings: () => ({
|
||||
globalPaused: mocks.appSettings.globalPaused,
|
||||
enginePaused: mocks.appSettings.enginePaused,
|
||||
toggleEnginePause: mocks.toggleEnginePause,
|
||||
}),
|
||||
}));
|
||||
|
||||
import { GithubArea } from "../GithubArea";
|
||||
import { SignalsArea } from "../SignalsArea";
|
||||
import { customRange, githubFixture, range7d } from "./areas.test-harness";
|
||||
|
||||
beforeEach(() => {
|
||||
apiMock.mockReset();
|
||||
backfillGithubSourceIssueClosedAtMock.mockReset();
|
||||
backfillCommitAssociationDiffStatsMock.mockReset();
|
||||
fetchOrgTreeMock.mockReset();
|
||||
fetchOrgTreeMock.mockResolvedValue([]);
|
||||
fetchExecutorStatsMock.mockReset();
|
||||
fetchExecutorStatsMock.mockResolvedValue({
|
||||
globalPause: false,
|
||||
enginePaused: false,
|
||||
maxConcurrent: 2,
|
||||
lastActivityAt: "2026-06-19T12:00:00.000Z",
|
||||
});
|
||||
mocks.fetchSettings.mockReset();
|
||||
mocks.fetchSettings.mockResolvedValue({ heartbeatMultiplier: 1 });
|
||||
mocks.updateSettings.mockReset();
|
||||
mocks.updateSettings.mockResolvedValue({});
|
||||
toggleEnginePauseMock.mockReset();
|
||||
appSettingsMock.globalPaused = false;
|
||||
appSettingsMock.enginePaused = false;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe("GithubArea", () => {
|
||||
it("renders filed/fixed/net stats, daily trend, and by-repo bars", async () => {
|
||||
apiMock.mockResolvedValue(githubFixture());
|
||||
render(<GithubArea range={range7d} />);
|
||||
|
||||
await screen.findByTestId("cc-area-github");
|
||||
expect(screen.getByTestId("cc-github-filed").textContent).toContain("5");
|
||||
expect(screen.getByTestId("cc-github-fixed").textContent).toContain("3");
|
||||
expect(screen.getByTestId("cc-github-net").textContent).toContain("2");
|
||||
expect(screen.getByTestId("cc-github-pie")).toBeTruthy();
|
||||
expect(screen.getByTestId("cc-github-line")).toBeTruthy();
|
||||
expect(screen.getByRole("img", { name: "Filed vs fixed share" })).toBeTruthy();
|
||||
expect(screen.getByRole("img", { name: "Filed vs fixed line" })).toBeTruthy();
|
||||
expect(screen.getByTestId("cc-github-daily-trend")).toBeTruthy();
|
||||
expect(screen.getByRole("img", { name: "Filed" })).toBeTruthy();
|
||||
expect(screen.getByRole("img", { name: "Fixed" })).toBeTruthy();
|
||||
const repoChart = screen.getByRole("list", { name: "By repository" });
|
||||
expect(within(repoChart).getByText("acme/alpha")).toBeTruthy();
|
||||
expect(within(repoChart).getByLabelText("acme/alpha: 4 filed / 1 fixed")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders resolved issues with safe outbound links and approximation labels", async () => {
|
||||
apiMock.mockResolvedValue({
|
||||
...githubFixture(),
|
||||
resolved: [
|
||||
{
|
||||
taskId: "FN-100",
|
||||
taskTitle: "Fix alpha crash",
|
||||
repo: "acme/alpha",
|
||||
issueNumber: 123,
|
||||
url: "https://github.com/acme/alpha/issues/123",
|
||||
resolvedAt: "2026-06-10T12:34:56.000Z",
|
||||
resolvedAtExact: true,
|
||||
},
|
||||
{
|
||||
taskId: "FN-101",
|
||||
taskTitle: "Patch unknown import",
|
||||
repo: "(unknown)",
|
||||
issueNumber: null,
|
||||
url: null,
|
||||
resolvedAt: "2026-06-09T08:00:00.000Z",
|
||||
resolvedAtExact: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
render(<GithubArea range={range7d} />);
|
||||
|
||||
const section = await screen.findByTestId("cc-github-resolved");
|
||||
expect(section.textContent).toContain("Resolved issues");
|
||||
expect(section.textContent).toContain("acme/alpha#123");
|
||||
expect(section.textContent).toContain("Fix alpha crash");
|
||||
expect(section.textContent).toContain("FN-100");
|
||||
expect(section.textContent).toContain("(unknown)");
|
||||
expect(section.textContent).toContain("Patch unknown import");
|
||||
expect(section.textContent).toContain("approx");
|
||||
expect(section.textContent).toContain("2026");
|
||||
|
||||
const linkedIssue = within(section).getByRole("link", { name: "Open GitHub issue acme/alpha#123" });
|
||||
expect(linkedIssue.getAttribute("href")).toBe("https://github.com/acme/alpha/issues/123");
|
||||
expect(linkedIssue.getAttribute("target")).toBe("_blank");
|
||||
expect(linkedIssue.getAttribute("rel")).toBe("noopener noreferrer");
|
||||
expect(within(section).queryByRole("link", { name: /unknown/i })).toBeNull();
|
||||
});
|
||||
|
||||
it("omits the resolved issues section for an empty resolved list", async () => {
|
||||
apiMock.mockResolvedValue({ ...githubFixture(), resolved: [] });
|
||||
|
||||
render(<GithubArea range={range7d} />);
|
||||
|
||||
await screen.findByTestId("cc-area-github");
|
||||
expect(screen.queryByTestId("cc-github-resolved")).toBeNull();
|
||||
expect(screen.getByTestId("cc-area-github").textContent).not.toContain("NaN");
|
||||
});
|
||||
|
||||
it("renders the empty state without empty chart shells", async () => {
|
||||
apiMock.mockResolvedValue({ ...githubFixture(), filed: 0, fixed: 0, net: 0, daily: [], byRepo: [] });
|
||||
render(<GithubArea range={range7d} />);
|
||||
|
||||
await screen.findByTestId("cc-area-github");
|
||||
expect(screen.getByTestId("cc-area-github").textContent).toContain("No GitHub issue activity");
|
||||
expect(screen.getByTestId("cc-github-backfill-button")).toBeTruthy();
|
||||
expect(screen.queryByTestId("cc-github-pie")).toBeNull();
|
||||
expect(screen.queryByTestId("cc-github-line")).toBeNull();
|
||||
expect(screen.queryByTestId("cc-github-daily-trend")).toBeNull();
|
||||
expect(screen.queryByTestId("cc-github-by-repo")).toBeNull();
|
||||
});
|
||||
|
||||
it("renders loading and error states", async () => {
|
||||
apiMock.mockImplementationOnce(() => new Promise(() => undefined));
|
||||
const { unmount } = render(<GithubArea range={range7d} />);
|
||||
expect(screen.getByTestId("cc-area-github-loading")).toBeTruthy();
|
||||
expect(screen.queryByTestId("cc-github-pie")).toBeNull();
|
||||
expect(screen.queryByTestId("cc-github-line")).toBeNull();
|
||||
unmount();
|
||||
|
||||
apiMock.mockRejectedValueOnce(new Error("github failed"));
|
||||
render(<GithubArea range={range7d} />);
|
||||
await screen.findByTestId("cc-area-github-error");
|
||||
expect(screen.getByTestId("cc-area-github-error").textContent).toContain("github failed");
|
||||
expect(screen.queryByTestId("cc-github-pie")).toBeNull();
|
||||
expect(screen.queryByTestId("cc-github-line")).toBeNull();
|
||||
});
|
||||
|
||||
it("handles undefined chart arrays and zero values without NaN output", async () => {
|
||||
apiMock.mockResolvedValue({ ...githubFixture(), filed: 1, fixed: 0, net: 1, daily: undefined, byRepo: undefined });
|
||||
render(<GithubArea range={range7d} />);
|
||||
|
||||
await screen.findByTestId("cc-area-github");
|
||||
expect(screen.getByTestId("cc-github-pie")).toBeTruthy();
|
||||
expect(screen.queryByTestId("cc-github-line")).toBeNull();
|
||||
expect(screen.queryByTestId("cc-github-daily-trend")).toBeNull();
|
||||
expect(screen.queryByTestId("cc-github-by-repo")).toBeNull();
|
||||
expect(screen.getByTestId("cc-area-github").textContent).not.toContain("NaN");
|
||||
});
|
||||
|
||||
it("keeps GitHub recharts safe for single-item and non-finite daily data", async () => {
|
||||
apiMock.mockResolvedValue({
|
||||
...githubFixture(),
|
||||
filed: 1,
|
||||
fixed: 1,
|
||||
net: 0,
|
||||
daily: [{ date: "2026-06-08", filed: Number.NaN, fixed: Number.POSITIVE_INFINITY }],
|
||||
byRepo: [{ repo: "acme/broken", filed: Number.NaN, fixed: -1 }],
|
||||
});
|
||||
render(<GithubArea range={range7d} />);
|
||||
|
||||
await screen.findByTestId("cc-area-github");
|
||||
expect(screen.getByTestId("cc-github-pie")).toBeTruthy();
|
||||
expect(screen.getByTestId("cc-github-line")).toBeTruthy();
|
||||
expect(screen.getByTestId("cc-area-github").textContent).not.toContain("NaN");
|
||||
expect(screen.getByTestId("cc-area-github").textContent).not.toContain("Infinity");
|
||||
});
|
||||
|
||||
it("rejects an inverted custom range client-side without fetching", async () => {
|
||||
render(<GithubArea range={customRange("2026-06-10", "2026-06-01")} />);
|
||||
await waitFor(() => expect(apiMock).not.toHaveBeenCalled());
|
||||
});
|
||||
|
||||
it("runs a single backfill batch and renders accumulated result counts", async () => {
|
||||
apiMock.mockResolvedValue(githubFixture());
|
||||
backfillGithubSourceIssueClosedAtMock.mockResolvedValueOnce({
|
||||
scanned: 4,
|
||||
filled: 2,
|
||||
skipped: 1,
|
||||
errors: 0,
|
||||
hasMore: false,
|
||||
});
|
||||
|
||||
render(<GithubArea range={range7d} />);
|
||||
await screen.findByTestId("cc-area-github");
|
||||
|
||||
fireEvent.click(screen.getByTestId("cc-github-backfill-button"));
|
||||
|
||||
await screen.findByText(/Backfill complete/i);
|
||||
expect(backfillGithubSourceIssueClosedAtMock).toHaveBeenCalledWith({ offset: 0, limit: 100 }, undefined);
|
||||
const result = screen.getByTestId("cc-github-backfill-result");
|
||||
expect(result.textContent).toContain("Scanned 4, filled 2, skipped 1, errors 0");
|
||||
});
|
||||
|
||||
it("paginates multi-batch backfills with advancing offsets and summed counts", async () => {
|
||||
apiMock.mockResolvedValue(githubFixture());
|
||||
backfillGithubSourceIssueClosedAtMock
|
||||
.mockResolvedValueOnce({ scanned: 100, filled: 4, skipped: 90, errors: 1, hasMore: true })
|
||||
.mockResolvedValueOnce({ scanned: 25, filled: 3, skipped: 22, errors: 0, hasMore: false });
|
||||
|
||||
render(<GithubArea range={range7d} />);
|
||||
await screen.findByTestId("cc-area-github");
|
||||
fireEvent.click(screen.getByTestId("cc-github-backfill-button"));
|
||||
|
||||
await waitFor(() => expect(backfillGithubSourceIssueClosedAtMock).toHaveBeenCalledTimes(2));
|
||||
expect(backfillGithubSourceIssueClosedAtMock).toHaveBeenNthCalledWith(1, { offset: 0, limit: 100 }, undefined);
|
||||
expect(backfillGithubSourceIssueClosedAtMock).toHaveBeenNthCalledWith(2, { offset: 100, limit: 100 }, undefined);
|
||||
const result = await screen.findByTestId("cc-github-backfill-result");
|
||||
expect(result.textContent).toContain("Scanned 125, filled 7, skipped 112, errors 1");
|
||||
expect(result.className).toContain("cc-github-backfill-status--error");
|
||||
});
|
||||
|
||||
it("shows the all-zero backfill as nothing to backfill instead of an error", async () => {
|
||||
apiMock.mockResolvedValue(githubFixture());
|
||||
backfillGithubSourceIssueClosedAtMock.mockResolvedValueOnce({
|
||||
scanned: 0,
|
||||
filled: 0,
|
||||
skipped: 0,
|
||||
errors: 0,
|
||||
hasMore: false,
|
||||
});
|
||||
|
||||
render(<GithubArea range={range7d} />);
|
||||
await screen.findByTestId("cc-area-github");
|
||||
fireEvent.click(screen.getByTestId("cc-github-backfill-button"));
|
||||
|
||||
const result = await screen.findByTestId("cc-github-backfill-result");
|
||||
expect(result.textContent).toContain("Nothing to backfill");
|
||||
expect(result.className).not.toContain("cc-github-backfill-status--error");
|
||||
});
|
||||
|
||||
it("surfaces nonzero backfill error counts without throwing", async () => {
|
||||
apiMock.mockResolvedValue(githubFixture());
|
||||
backfillGithubSourceIssueClosedAtMock.mockResolvedValueOnce({
|
||||
scanned: 8,
|
||||
filled: 1,
|
||||
skipped: 5,
|
||||
errors: 2,
|
||||
hasMore: false,
|
||||
});
|
||||
|
||||
render(<GithubArea range={range7d} />);
|
||||
await screen.findByTestId("cc-area-github");
|
||||
fireEvent.click(screen.getByTestId("cc-github-backfill-button"));
|
||||
|
||||
const result = await screen.findByTestId("cc-github-backfill-result");
|
||||
expect(result.textContent).toContain("errors 2");
|
||||
expect(result.className).toContain("cc-github-backfill-status--error");
|
||||
});
|
||||
|
||||
it("captures endpoint failures in local error UI", async () => {
|
||||
apiMock.mockResolvedValue(githubFixture());
|
||||
backfillGithubSourceIssueClosedAtMock.mockRejectedValueOnce(new Error("endpoint failed"));
|
||||
|
||||
render(<GithubArea range={range7d} />);
|
||||
await screen.findByTestId("cc-area-github");
|
||||
fireEvent.click(screen.getByTestId("cc-github-backfill-button"));
|
||||
|
||||
const result = await screen.findByTestId("cc-github-backfill-result");
|
||||
expect(result.textContent).toContain("endpoint failed");
|
||||
expect(result.className).toContain("cc-github-backfill-status--error");
|
||||
});
|
||||
|
||||
it("disables and guards the button while a backfill is in flight", async () => {
|
||||
apiMock.mockResolvedValue(githubFixture());
|
||||
let resolveBackfill: ((value: { scanned: number; filled: number; skipped: number; errors: number; hasMore: boolean }) => void) | null = null;
|
||||
backfillGithubSourceIssueClosedAtMock.mockImplementationOnce(
|
||||
() => new Promise((resolve) => {
|
||||
resolveBackfill = resolve;
|
||||
}),
|
||||
);
|
||||
|
||||
render(<GithubArea range={range7d} />);
|
||||
await screen.findByTestId("cc-area-github");
|
||||
const button = screen.getByTestId("cc-github-backfill-button") as HTMLButtonElement;
|
||||
fireEvent.click(button);
|
||||
|
||||
await waitFor(() => expect(button.disabled).toBe(true));
|
||||
fireEvent.click(button);
|
||||
expect(backfillGithubSourceIssueClosedAtMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
resolveBackfill?.({ scanned: 1, filled: 1, skipped: 0, errors: 0, hasMore: false });
|
||||
await screen.findByText(/Backfill complete/i);
|
||||
});
|
||||
|
||||
it("stops a pathological always-has-more response at the max iteration guard", async () => {
|
||||
apiMock.mockResolvedValue(githubFixture());
|
||||
backfillGithubSourceIssueClosedAtMock.mockResolvedValue({
|
||||
scanned: 1,
|
||||
filled: 0,
|
||||
skipped: 1,
|
||||
errors: 0,
|
||||
hasMore: true,
|
||||
});
|
||||
|
||||
render(<GithubArea range={range7d} />);
|
||||
await screen.findByTestId("cc-area-github");
|
||||
fireEvent.click(screen.getByTestId("cc-github-backfill-button"));
|
||||
|
||||
const result = await screen.findByText(/safety limit/i);
|
||||
expect(result.textContent).toContain("safety limit");
|
||||
expect(backfillGithubSourceIssueClosedAtMock).toHaveBeenCalledTimes(1000);
|
||||
expect(screen.getByTestId("cc-github-backfill-result").textContent).toContain("Scanned 1000");
|
||||
});
|
||||
});
|
||||
|
||||
// FN-6684 Mission Control decision: no extra pie/line test here because MissionControlPanel already renders the live SDLC Funnel for its only quantitative distribution; adding a pie would duplicate that affordance.
|
||||
describe("SignalsArea", () => {
|
||||
it("renders the empty state (not an error) when the signals endpoint is missing", async () => {
|
||||
apiMock.mockRejectedValue(new Error("API returned HTML instead of JSON (404)"));
|
||||
render(<SignalsArea range={range7d} />);
|
||||
await screen.findByTestId("cc-area-signals-empty");
|
||||
// Must not surface the error UI.
|
||||
expect(screen.queryByTestId("cc-area-signals-error")).toBeNull();
|
||||
expect(screen.queryByTestId("cc-signals-pie")).toBeNull();
|
||||
});
|
||||
|
||||
it("renders signal metrics and status pie when data is present", async () => {
|
||||
apiMock.mockResolvedValue({
|
||||
totalSignals: 8,
|
||||
open: 3,
|
||||
resolved: 5,
|
||||
mttr: { value: 42, unavailable: false },
|
||||
bySource: [{ source: "sentry", count: 8 }],
|
||||
bySeverity: [{ severity: "error", count: 8 }],
|
||||
});
|
||||
render(<SignalsArea range={range7d} />);
|
||||
await screen.findByTestId("cc-area-signals");
|
||||
expect(screen.getByTestId("cc-signals-total").textContent).toContain("8");
|
||||
expect(screen.getByTestId("cc-signals-mttr").textContent).toContain("42");
|
||||
expect(screen.getByTestId("cc-signals-pie")).toBeTruthy();
|
||||
expect(screen.getByRole("img", { name: "Signal status share" })).toBeTruthy();
|
||||
});
|
||||
|
||||
it("keeps signals pie safe for single-item and non-finite source/severity data", async () => {
|
||||
apiMock.mockResolvedValue({
|
||||
totalSignals: 1,
|
||||
open: 1,
|
||||
resolved: 0,
|
||||
mttr: { value: null, unavailable: true },
|
||||
bySource: [{ source: "broken", count: Number.NaN }],
|
||||
bySeverity: [{ severity: "broken", count: Number.POSITIVE_INFINITY }],
|
||||
});
|
||||
render(<SignalsArea range={range7d} />);
|
||||
await screen.findByTestId("cc-area-signals");
|
||||
expect(screen.getByTestId("cc-signals-pie")).toBeTruthy();
|
||||
expect(screen.getByTestId("cc-area-signals").textContent).not.toContain("NaN");
|
||||
expect(screen.getByTestId("cc-area-signals").textContent).not.toContain("Infinity");
|
||||
});
|
||||
|
||||
it("renders settled zero signals without a pie shell", async () => {
|
||||
apiMock.mockResolvedValue({
|
||||
totalSignals: 0,
|
||||
open: 0,
|
||||
resolved: 0,
|
||||
mttr: { value: null, unavailable: true },
|
||||
bySource: [],
|
||||
bySeverity: [],
|
||||
});
|
||||
render(<SignalsArea range={range7d} />);
|
||||
await screen.findByTestId("cc-area-signals-empty");
|
||||
expect(screen.queryByTestId("cc-signals-pie")).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,302 @@
|
||||
/*
|
||||
FNXC:CommandCenter 2026-06-25-00:00:
|
||||
FN-7044 extracted shared Command Center area test fixtures into this harness so focused sibling suites can stay under the 2000-line hard cap without duplicating fixture logic or moving Vitest hoisted mocks.
|
||||
*/
|
||||
import { expect, vi } from "vitest";
|
||||
import { screen, within } from "@testing-library/react";
|
||||
import type { OrgTreeNode } from "@fusion/core";
|
||||
import type { DateRange } from "../../DateRangePicker";
|
||||
|
||||
export const range7d: DateRange = { from: "2026-06-08", to: null, preset: "7d" };
|
||||
export const customRange = (from: string, to: string): DateRange => ({ from, to, preset: "custom" });
|
||||
|
||||
export function providerIconIn(element: HTMLElement, provider: string): Element | null {
|
||||
return element.querySelector(`.provider-icon[data-provider="${provider}"]`);
|
||||
}
|
||||
|
||||
export function tokenFixture(totalTokens = 1500) {
|
||||
return {
|
||||
from: "2026-06-08",
|
||||
to: null,
|
||||
groupBy: "model",
|
||||
totals: {
|
||||
inputTokens: Math.round(totalTokens * 0.6),
|
||||
outputTokens: Math.round(totalTokens * 0.3),
|
||||
cachedTokens: Math.round(totalTokens * 0.1),
|
||||
cacheWriteTokens: 0,
|
||||
totalTokens,
|
||||
nTasks: totalTokens > 0 ? 5 : 0,
|
||||
},
|
||||
cost: { usd: 12.5, unavailable: false, stale: false },
|
||||
series: [
|
||||
{
|
||||
bucket: "2026-06-08",
|
||||
inputTokens: 400,
|
||||
outputTokens: 200,
|
||||
cachedTokens: 0,
|
||||
cacheWriteTokens: 0,
|
||||
totalTokens: 600,
|
||||
nTasks: 2,
|
||||
cost: { usd: 4.5, unavailable: false, stale: false },
|
||||
},
|
||||
{
|
||||
bucket: "2026-06-09",
|
||||
inputTokens: 600,
|
||||
outputTokens: 300,
|
||||
cachedTokens: 0,
|
||||
cacheWriteTokens: 0,
|
||||
totalTokens: 900,
|
||||
nTasks: 3,
|
||||
cost: { usd: 8, unavailable: false, stale: false },
|
||||
},
|
||||
],
|
||||
groups: [
|
||||
{
|
||||
key: "gpt-4o",
|
||||
inputTokens: 600,
|
||||
outputTokens: 300,
|
||||
cachedTokens: 100,
|
||||
cacheWriteTokens: 0,
|
||||
totalTokens: 900,
|
||||
nTasks: 3,
|
||||
cost: { usd: 9.0, unavailable: false, stale: false },
|
||||
},
|
||||
{
|
||||
key: "claude-sonnet",
|
||||
inputTokens: 400,
|
||||
outputTokens: 200,
|
||||
cachedTokens: 100,
|
||||
cacheWriteTokens: 0,
|
||||
totalTokens: 600,
|
||||
nTasks: 2,
|
||||
cost: { usd: 3.5, unavailable: false, stale: false },
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export function githubFixture() {
|
||||
return {
|
||||
from: "2026-06-08",
|
||||
to: null,
|
||||
filed: 5,
|
||||
fixed: 3,
|
||||
net: 2,
|
||||
daily: [
|
||||
{ date: "2026-06-08", filed: 2, fixed: 1 },
|
||||
{ date: "2026-06-09", filed: 3, fixed: 2 },
|
||||
],
|
||||
byRepo: [
|
||||
{ repo: "acme/alpha", filed: 4, fixed: 1 },
|
||||
{ repo: "acme/beta", filed: 1, fixed: 2 },
|
||||
],
|
||||
resolved: [],
|
||||
};
|
||||
}
|
||||
|
||||
export function productivityFixture() {
|
||||
return {
|
||||
from: "2026-06-08",
|
||||
to: null,
|
||||
modifiedFiles: 7,
|
||||
commits: 4,
|
||||
pullRequests: 2,
|
||||
byLanguage: [{ language: "TypeScript", count: 7 }],
|
||||
loc: { value: null, unavailable: true },
|
||||
hoursSaved: { value: null, unavailable: true },
|
||||
taskDuration: {
|
||||
completedTasks: 3,
|
||||
averageMs: 90 * 60 * 1000,
|
||||
medianMs: 60 * 60 * 1000,
|
||||
p90Ms: 2 * 60 * 60 * 1000,
|
||||
totalMs: 270 * 60 * 1000,
|
||||
unavailable: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function installElementClientWidth(width: number) {
|
||||
return vi.spyOn(HTMLElement.prototype, "clientWidth", "get").mockReturnValue(width);
|
||||
}
|
||||
|
||||
export function agentNode(id: string, name: string, children: OrgTreeNode[] = [], title = "Team Lead"): OrgTreeNode {
|
||||
return {
|
||||
agent: {
|
||||
id,
|
||||
name,
|
||||
title,
|
||||
role: "executor",
|
||||
state: "idle",
|
||||
createdAt: "2026-06-19T00:00:00.000Z",
|
||||
updatedAt: "2026-06-19T00:00:00.000Z",
|
||||
metadata: {},
|
||||
},
|
||||
children,
|
||||
};
|
||||
}
|
||||
|
||||
export function emptyTeamFixture() {
|
||||
return {
|
||||
from: null,
|
||||
to: null,
|
||||
totals: {
|
||||
tokens: { inputTokens: 0, outputTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, totalTokens: 0, nTasks: 0 },
|
||||
cost: { usd: null, unavailable: false, stale: false },
|
||||
filesChanged: 0,
|
||||
tasksCompleted: 0,
|
||||
tasksInProgress: 0,
|
||||
tasksInReview: 0,
|
||||
},
|
||||
agents: [],
|
||||
};
|
||||
}
|
||||
|
||||
export function populatedTeamFixture(totalTokens = 1500) {
|
||||
return {
|
||||
...emptyTeamFixture(),
|
||||
totals: {
|
||||
tokens: {
|
||||
inputTokens: Math.round(totalTokens * 0.6),
|
||||
outputTokens: Math.round(totalTokens * 0.3),
|
||||
cachedTokens: Math.round(totalTokens * 0.1),
|
||||
cacheWriteTokens: 0,
|
||||
totalTokens,
|
||||
nTasks: 2,
|
||||
},
|
||||
cost: { usd: 4.25, unavailable: false, stale: false },
|
||||
filesChanged: 7,
|
||||
tasksCompleted: 3,
|
||||
tasksInProgress: 1,
|
||||
tasksInReview: 0,
|
||||
},
|
||||
agents: [
|
||||
{
|
||||
agentId: "agent-alpha",
|
||||
agentName: "Alpha Agent",
|
||||
role: "executor",
|
||||
state: "running",
|
||||
tokens: {
|
||||
inputTokens: Math.round(totalTokens * 0.6),
|
||||
outputTokens: Math.round(totalTokens * 0.3),
|
||||
cachedTokens: Math.round(totalTokens * 0.1),
|
||||
cacheWriteTokens: 0,
|
||||
totalTokens,
|
||||
nTasks: 2,
|
||||
},
|
||||
cost: { usd: 4.25, unavailable: false, stale: false },
|
||||
filesChanged: 7,
|
||||
tasksCompleted: 3,
|
||||
tasksInProgress: 1,
|
||||
tasksInReview: 0,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export function activityFixture() {
|
||||
return {
|
||||
from: "2026-06-08",
|
||||
to: null,
|
||||
sessions: 4,
|
||||
messages: 12,
|
||||
activeNodes: 3,
|
||||
activeAgents: 2,
|
||||
agentRuns: { total: 8, active: 1, completed: 6, failed: 1 },
|
||||
daily: [
|
||||
{ day: "2026-06-08", messages: 2, activeNodes: 1, activeAgents: 1, agentRuns: 2 },
|
||||
{ day: "2026-06-09", messages: 4, activeNodes: 2, activeAgents: 1, agentRuns: 3 },
|
||||
{ day: "2026-06-10", messages: 6, activeNodes: 3, activeAgents: 2, agentRuns: 3 },
|
||||
],
|
||||
stickiness: 0.5,
|
||||
mttr: { value: null, unavailable: true, sampleCount: 0 },
|
||||
monitor: {
|
||||
mttr: { value: null, unavailable: true, sampleCount: 0 },
|
||||
incidentsOpened: 0,
|
||||
incidentsResolved: 0,
|
||||
openIncidents: 0,
|
||||
deployments: 0,
|
||||
},
|
||||
funnel: {
|
||||
stages: [],
|
||||
enteredInRange: 0,
|
||||
doneInRange: 0,
|
||||
completionRate: null,
|
||||
throughputPerDay: 0,
|
||||
rangeDays: 7,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function expectRechartsWrapperWithin(testId: string, label: string): void {
|
||||
const section = screen.getByTestId(testId);
|
||||
const chart = within(section).getByRole("img", { name: label });
|
||||
expect(chart.classList.contains("cc-recharts-chart") || chart.classList.contains("cc-recharts-empty")).toBe(true);
|
||||
expect(chart.outerHTML).not.toMatch(/NaN|Infinity/);
|
||||
}
|
||||
|
||||
export function expectBarFillsFinite(testId: string): void {
|
||||
const section = screen.getByTestId(testId);
|
||||
for (const fill of Array.from(section.querySelectorAll<HTMLElement>(".cc-bar-fill"))) {
|
||||
expect(fill.style.width).toMatch(/^\d+(?:\.\d+)?%$/);
|
||||
expect(fill.style.width).not.toMatch(/NaN|Infinity/);
|
||||
}
|
||||
}
|
||||
|
||||
export function expectSparklineHeightsFinite(testId: string): void {
|
||||
const section = screen.getByTestId(testId);
|
||||
for (const bar of Array.from(section.querySelectorAll<HTMLElement>(".cc-sparkline-bar"))) {
|
||||
expect(bar.style.height).toMatch(/^\d+(?:\.\d+)?%$/);
|
||||
expect(bar.style.height).not.toMatch(/NaN|Infinity/);
|
||||
}
|
||||
}
|
||||
|
||||
export function viewBoxNumbers(chart: Element): [number, number, number, number] {
|
||||
return (chart.getAttribute("viewBox") ?? "")
|
||||
.split(/\s+/)
|
||||
.map(Number) as [number, number, number, number];
|
||||
}
|
||||
|
||||
export function expectSvgLinePointsInsideViewBox(testId: string, label: string): void {
|
||||
const section = screen.getByTestId(testId);
|
||||
const chart = within(section).getByRole("img", { name: label });
|
||||
const [, , viewBoxWidth, viewBoxHeight] = viewBoxNumbers(chart);
|
||||
for (const point of Array.from(chart.querySelectorAll(".cc-line-chart-point"))) {
|
||||
const cx = Number(point.getAttribute("cx"));
|
||||
const cy = Number(point.getAttribute("cy"));
|
||||
const r = Number(point.getAttribute("r"));
|
||||
expect(cx).toBeGreaterThanOrEqual(r);
|
||||
expect(cx).toBeLessThanOrEqual(viewBoxWidth - r);
|
||||
expect(cy).toBeGreaterThanOrEqual(r);
|
||||
expect(cy).toBeLessThanOrEqual(viewBoxHeight - r);
|
||||
}
|
||||
}
|
||||
|
||||
export function expectSvgLineFillsBoxAndKeepsRoundMarkers(testId: string, label: string): void {
|
||||
const section = screen.getByTestId(testId);
|
||||
const chart = within(section).getByRole("img", { name: label });
|
||||
const [, , viewBoxWidth, viewBoxHeight] = viewBoxNumbers(chart);
|
||||
const line = chart.querySelector(".cc-line-chart-path");
|
||||
const pointPairs = (line?.getAttribute("points") ?? "")
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.map((pair) => pair.split(",").map(Number) as [number, number]);
|
||||
expect(viewBoxWidth).toBeGreaterThan(viewBoxHeight);
|
||||
expect(chart.getAttribute("preserveAspectRatio")).toBe("none");
|
||||
expect(chart.getAttribute("viewBox")).not.toBe("0 0 100 100");
|
||||
expect(chart.querySelectorAll(".cc-line-chart-point").length).toBeGreaterThan(0);
|
||||
expect(pointPairs[0]?.[0]).toBe(3);
|
||||
expect(pointPairs.at(-1)?.[0]).toBe(viewBoxWidth - 3);
|
||||
}
|
||||
|
||||
export function pluginActivationFixture(overrides: Partial<{ activations: number; unavailable: boolean }> = {}) {
|
||||
const activations = overrides.activations ?? 0;
|
||||
const unavailable = overrides.unavailable ?? true;
|
||||
return {
|
||||
from: "2026-06-08",
|
||||
to: null,
|
||||
activations,
|
||||
byPlugin: unavailable ? [] : [{ pluginId: "fusion-plugin-example", count: activations }],
|
||||
unavailable,
|
||||
};
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
/*
|
||||
FNXC:CommandCenter 2026-06-16-09:42:
|
||||
Command Center area component tests (PR #1683). Pin loading/error/unavailable-vs-zero rendering for each analytics area against mocked fixtures so the "—" sentinel and cost-unavailable contracts can't regress.
|
||||
|
||||
FNXC:CommandCenter 2026-06-25-00:00:
|
||||
FN-7044 split shared fixtures into areas.test-harness.tsx so this suite can be partitioned into focused sibling files under the 2000-line hard cap without losing coverage.
|
||||
*/
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { render, screen, fireEvent, waitFor, within, act, renderHook } from "@testing-library/react";
|
||||
import type { OrgTreeNode } from "@fusion/core";
|
||||
|
||||
// Mock the api() helper so the areas fetch deterministic fixtures.
|
||||
const mocks = vi.hoisted(() => ({
|
||||
@@ -50,235 +52,31 @@ vi.mock("../../../../hooks/useAppSettings", () => ({
|
||||
import { TokensArea } from "../TokensArea";
|
||||
import { ToolsArea } from "../ToolsArea";
|
||||
import { ProductivityArea } from "../ProductivityArea";
|
||||
import { GithubArea } from "../GithubArea";
|
||||
import { SignalsArea } from "../SignalsArea";
|
||||
import { TeamArea } from "../TeamArea";
|
||||
import { ActivityArea } from "../ActivityArea";
|
||||
import { EcosystemArea } from "../EcosystemArea";
|
||||
import { useAnalyticsArea } from "../useAnalyticsArea";
|
||||
import { ConfirmDialogProvider } from "../../../../hooks/useConfirm";
|
||||
import { rangeQuery } from "../areaShared";
|
||||
import { defaultPresets, rangeFromPreset, type DateRange } from "../../DateRangePicker";
|
||||
|
||||
const range7d: DateRange = { from: "2026-06-08", to: null, preset: "7d" };
|
||||
const customRange = (from: string, to: string): DateRange => ({ from, to, preset: "custom" });
|
||||
|
||||
function providerIconIn(element: HTMLElement, provider: string): Element | null {
|
||||
return element.querySelector(`.provider-icon[data-provider="${provider}"]`);
|
||||
}
|
||||
|
||||
function tokenFixture(totalTokens = 1500) {
|
||||
return {
|
||||
from: "2026-06-08",
|
||||
to: null,
|
||||
groupBy: "model",
|
||||
totals: {
|
||||
inputTokens: Math.round(totalTokens * 0.6),
|
||||
outputTokens: Math.round(totalTokens * 0.3),
|
||||
cachedTokens: Math.round(totalTokens * 0.1),
|
||||
cacheWriteTokens: 0,
|
||||
totalTokens,
|
||||
nTasks: totalTokens > 0 ? 5 : 0,
|
||||
},
|
||||
cost: { usd: 12.5, unavailable: false, stale: false },
|
||||
series: [
|
||||
{
|
||||
bucket: "2026-06-08",
|
||||
inputTokens: 400,
|
||||
outputTokens: 200,
|
||||
cachedTokens: 0,
|
||||
cacheWriteTokens: 0,
|
||||
totalTokens: 600,
|
||||
nTasks: 2,
|
||||
cost: { usd: 4.5, unavailable: false, stale: false },
|
||||
},
|
||||
{
|
||||
bucket: "2026-06-09",
|
||||
inputTokens: 600,
|
||||
outputTokens: 300,
|
||||
cachedTokens: 0,
|
||||
cacheWriteTokens: 0,
|
||||
totalTokens: 900,
|
||||
nTasks: 3,
|
||||
cost: { usd: 8, unavailable: false, stale: false },
|
||||
},
|
||||
],
|
||||
groups: [
|
||||
{
|
||||
key: "gpt-4o",
|
||||
inputTokens: 600,
|
||||
outputTokens: 300,
|
||||
cachedTokens: 100,
|
||||
cacheWriteTokens: 0,
|
||||
totalTokens: 900,
|
||||
nTasks: 3,
|
||||
cost: { usd: 9.0, unavailable: false, stale: false },
|
||||
},
|
||||
{
|
||||
key: "claude-sonnet",
|
||||
inputTokens: 400,
|
||||
outputTokens: 200,
|
||||
cachedTokens: 100,
|
||||
cacheWriteTokens: 0,
|
||||
totalTokens: 600,
|
||||
nTasks: 2,
|
||||
cost: { usd: 3.5, unavailable: false, stale: false },
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function githubFixture() {
|
||||
return {
|
||||
from: "2026-06-08",
|
||||
to: null,
|
||||
filed: 5,
|
||||
fixed: 3,
|
||||
net: 2,
|
||||
daily: [
|
||||
{ date: "2026-06-08", filed: 2, fixed: 1 },
|
||||
{ date: "2026-06-09", filed: 3, fixed: 2 },
|
||||
],
|
||||
byRepo: [
|
||||
{ repo: "acme/alpha", filed: 4, fixed: 1 },
|
||||
{ repo: "acme/beta", filed: 1, fixed: 2 },
|
||||
],
|
||||
resolved: [],
|
||||
};
|
||||
}
|
||||
|
||||
function productivityFixture() {
|
||||
return {
|
||||
from: "2026-06-08",
|
||||
to: null,
|
||||
modifiedFiles: 7,
|
||||
commits: 4,
|
||||
pullRequests: 2,
|
||||
byLanguage: [{ language: "TypeScript", count: 7 }],
|
||||
loc: { value: null, unavailable: true },
|
||||
hoursSaved: { value: null, unavailable: true },
|
||||
taskDuration: {
|
||||
completedTasks: 3,
|
||||
averageMs: 90 * 60 * 1000,
|
||||
medianMs: 60 * 60 * 1000,
|
||||
p90Ms: 2 * 60 * 60 * 1000,
|
||||
totalMs: 270 * 60 * 1000,
|
||||
unavailable: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function installElementClientWidth(width: number) {
|
||||
return vi.spyOn(HTMLElement.prototype, "clientWidth", "get").mockReturnValue(width);
|
||||
}
|
||||
|
||||
function agentNode(id: string, name: string, children: OrgTreeNode[] = [], title = "Team Lead"): OrgTreeNode {
|
||||
return {
|
||||
agent: {
|
||||
id,
|
||||
name,
|
||||
title,
|
||||
role: "executor",
|
||||
state: "idle",
|
||||
createdAt: "2026-06-19T00:00:00.000Z",
|
||||
updatedAt: "2026-06-19T00:00:00.000Z",
|
||||
metadata: {},
|
||||
},
|
||||
children,
|
||||
};
|
||||
}
|
||||
|
||||
function emptyTeamFixture() {
|
||||
return {
|
||||
from: null,
|
||||
to: null,
|
||||
totals: {
|
||||
tokens: { inputTokens: 0, outputTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, totalTokens: 0, nTasks: 0 },
|
||||
cost: { usd: null, unavailable: false, stale: false },
|
||||
filesChanged: 0,
|
||||
tasksCompleted: 0,
|
||||
tasksInProgress: 0,
|
||||
tasksInReview: 0,
|
||||
},
|
||||
agents: [],
|
||||
};
|
||||
}
|
||||
|
||||
function populatedTeamFixture(totalTokens = 1500) {
|
||||
return {
|
||||
...emptyTeamFixture(),
|
||||
totals: {
|
||||
tokens: {
|
||||
inputTokens: Math.round(totalTokens * 0.6),
|
||||
outputTokens: Math.round(totalTokens * 0.3),
|
||||
cachedTokens: Math.round(totalTokens * 0.1),
|
||||
cacheWriteTokens: 0,
|
||||
totalTokens,
|
||||
nTasks: 2,
|
||||
},
|
||||
cost: { usd: 4.25, unavailable: false, stale: false },
|
||||
filesChanged: 7,
|
||||
tasksCompleted: 3,
|
||||
tasksInProgress: 1,
|
||||
tasksInReview: 0,
|
||||
},
|
||||
agents: [
|
||||
{
|
||||
agentId: "agent-alpha",
|
||||
agentName: "Alpha Agent",
|
||||
role: "executor",
|
||||
state: "running",
|
||||
tokens: {
|
||||
inputTokens: Math.round(totalTokens * 0.6),
|
||||
outputTokens: Math.round(totalTokens * 0.3),
|
||||
cachedTokens: Math.round(totalTokens * 0.1),
|
||||
cacheWriteTokens: 0,
|
||||
totalTokens,
|
||||
nTasks: 2,
|
||||
},
|
||||
cost: { usd: 4.25, unavailable: false, stale: false },
|
||||
filesChanged: 7,
|
||||
tasksCompleted: 3,
|
||||
tasksInProgress: 1,
|
||||
tasksInReview: 0,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function activityFixture() {
|
||||
return {
|
||||
from: "2026-06-08",
|
||||
to: null,
|
||||
sessions: 4,
|
||||
messages: 12,
|
||||
activeNodes: 3,
|
||||
activeAgents: 2,
|
||||
agentRuns: { total: 8, active: 1, completed: 6, failed: 1 },
|
||||
daily: [
|
||||
{ day: "2026-06-08", messages: 2, activeNodes: 1, activeAgents: 1, agentRuns: 2 },
|
||||
{ day: "2026-06-09", messages: 4, activeNodes: 2, activeAgents: 1, agentRuns: 3 },
|
||||
{ day: "2026-06-10", messages: 6, activeNodes: 3, activeAgents: 2, agentRuns: 3 },
|
||||
],
|
||||
stickiness: 0.5,
|
||||
mttr: { value: null, unavailable: true, sampleCount: 0 },
|
||||
monitor: {
|
||||
mttr: { value: null, unavailable: true, sampleCount: 0 },
|
||||
incidentsOpened: 0,
|
||||
incidentsResolved: 0,
|
||||
openIncidents: 0,
|
||||
deployments: 0,
|
||||
},
|
||||
funnel: {
|
||||
stages: [],
|
||||
enteredInRange: 0,
|
||||
doneInRange: 0,
|
||||
completionRate: null,
|
||||
throughputPerDay: 0,
|
||||
rangeDays: 7,
|
||||
},
|
||||
};
|
||||
}
|
||||
import { defaultPresets, rangeFromPreset } from "../../DateRangePicker";
|
||||
import {
|
||||
activityFixture,
|
||||
agentNode,
|
||||
customRange,
|
||||
emptyTeamFixture,
|
||||
expectBarFillsFinite,
|
||||
expectRechartsWrapperWithin,
|
||||
expectSparklineHeightsFinite,
|
||||
expectSvgLineFillsBoxAndKeepsRoundMarkers,
|
||||
expectSvgLinePointsInsideViewBox,
|
||||
installElementClientWidth,
|
||||
pluginActivationFixture,
|
||||
populatedTeamFixture,
|
||||
productivityFixture,
|
||||
providerIconIn,
|
||||
range7d,
|
||||
tokenFixture,
|
||||
} from "./areas.test-harness";
|
||||
|
||||
beforeEach(() => {
|
||||
apiMock.mockReset();
|
||||
@@ -306,67 +104,6 @@ afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
function expectRechartsWrapperWithin(testId: string, label: string): void {
|
||||
const section = screen.getByTestId(testId);
|
||||
const chart = within(section).getByRole("img", { name: label });
|
||||
expect(chart.classList.contains("cc-recharts-chart") || chart.classList.contains("cc-recharts-empty")).toBe(true);
|
||||
expect(chart.outerHTML).not.toMatch(/NaN|Infinity/);
|
||||
}
|
||||
|
||||
function expectBarFillsFinite(testId: string): void {
|
||||
const section = screen.getByTestId(testId);
|
||||
for (const fill of Array.from(section.querySelectorAll<HTMLElement>(".cc-bar-fill"))) {
|
||||
expect(fill.style.width).toMatch(/^\d+(?:\.\d+)?%$/);
|
||||
expect(fill.style.width).not.toMatch(/NaN|Infinity/);
|
||||
}
|
||||
}
|
||||
|
||||
function expectSparklineHeightsFinite(testId: string): void {
|
||||
const section = screen.getByTestId(testId);
|
||||
for (const bar of Array.from(section.querySelectorAll<HTMLElement>(".cc-sparkline-bar"))) {
|
||||
expect(bar.style.height).toMatch(/^\d+(?:\.\d+)?%$/);
|
||||
expect(bar.style.height).not.toMatch(/NaN|Infinity/);
|
||||
}
|
||||
}
|
||||
|
||||
function viewBoxNumbers(chart: Element): [number, number, number, number] {
|
||||
return (chart.getAttribute("viewBox") ?? "")
|
||||
.split(/\s+/)
|
||||
.map(Number) as [number, number, number, number];
|
||||
}
|
||||
|
||||
function expectSvgLinePointsInsideViewBox(testId: string, label: string): void {
|
||||
const section = screen.getByTestId(testId);
|
||||
const chart = within(section).getByRole("img", { name: label });
|
||||
const [, , viewBoxWidth, viewBoxHeight] = viewBoxNumbers(chart);
|
||||
for (const point of Array.from(chart.querySelectorAll(".cc-line-chart-point"))) {
|
||||
const cx = Number(point.getAttribute("cx"));
|
||||
const cy = Number(point.getAttribute("cy"));
|
||||
const r = Number(point.getAttribute("r"));
|
||||
expect(cx).toBeGreaterThanOrEqual(r);
|
||||
expect(cx).toBeLessThanOrEqual(viewBoxWidth - r);
|
||||
expect(cy).toBeGreaterThanOrEqual(r);
|
||||
expect(cy).toBeLessThanOrEqual(viewBoxHeight - r);
|
||||
}
|
||||
}
|
||||
|
||||
function expectSvgLineFillsBoxAndKeepsRoundMarkers(testId: string, label: string): void {
|
||||
const section = screen.getByTestId(testId);
|
||||
const chart = within(section).getByRole("img", { name: label });
|
||||
const [, , viewBoxWidth, viewBoxHeight] = viewBoxNumbers(chart);
|
||||
const line = chart.querySelector(".cc-line-chart-path");
|
||||
const pointPairs = (line?.getAttribute("points") ?? "")
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.map((pair) => pair.split(",").map(Number) as [number, number]);
|
||||
expect(viewBoxWidth).toBeGreaterThan(viewBoxHeight);
|
||||
expect(chart.getAttribute("preserveAspectRatio")).toBe("none");
|
||||
expect(chart.getAttribute("viewBox")).not.toBe("0 0 100 100");
|
||||
expect(chart.querySelectorAll(".cc-line-chart-point").length).toBeGreaterThan(0);
|
||||
expect(pointPairs[0]?.[0]).toBe(3);
|
||||
expect(pointPairs.at(-1)?.[0]).toBe(viewBoxWidth - 3);
|
||||
}
|
||||
|
||||
describe("rangeQuery / rangeFromPreset", () => {
|
||||
it("serializes every default preset into a distinct server-resolvable query", () => {
|
||||
@@ -1626,17 +1363,6 @@ describe("TeamArea", () => {
|
||||
});
|
||||
});
|
||||
|
||||
function pluginActivationFixture(overrides: Partial<{ activations: number; unavailable: boolean }> = {}) {
|
||||
const activations = overrides.activations ?? 0;
|
||||
const unavailable = overrides.unavailable ?? true;
|
||||
return {
|
||||
from: "2026-06-08",
|
||||
to: null,
|
||||
activations,
|
||||
byPlugin: unavailable ? [] : [{ pluginId: "fusion-plugin-example", count: activations }],
|
||||
unavailable,
|
||||
};
|
||||
}
|
||||
|
||||
function mockEcosystemResponses(tokens: unknown, activations: unknown): void {
|
||||
apiMock.mockImplementation((path: string) => {
|
||||
@@ -1735,333 +1461,3 @@ describe("EcosystemArea", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("GithubArea", () => {
|
||||
it("renders filed/fixed/net stats, daily trend, and by-repo bars", async () => {
|
||||
apiMock.mockResolvedValue(githubFixture());
|
||||
render(<GithubArea range={range7d} />);
|
||||
|
||||
await screen.findByTestId("cc-area-github");
|
||||
expect(screen.getByTestId("cc-github-filed").textContent).toContain("5");
|
||||
expect(screen.getByTestId("cc-github-fixed").textContent).toContain("3");
|
||||
expect(screen.getByTestId("cc-github-net").textContent).toContain("2");
|
||||
expect(screen.getByTestId("cc-github-pie")).toBeTruthy();
|
||||
expect(screen.getByTestId("cc-github-line")).toBeTruthy();
|
||||
expect(screen.getByRole("img", { name: "Filed vs fixed share" })).toBeTruthy();
|
||||
expect(screen.getByRole("img", { name: "Filed vs fixed line" })).toBeTruthy();
|
||||
expect(screen.getByTestId("cc-github-daily-trend")).toBeTruthy();
|
||||
expect(screen.getByRole("img", { name: "Filed" })).toBeTruthy();
|
||||
expect(screen.getByRole("img", { name: "Fixed" })).toBeTruthy();
|
||||
const repoChart = screen.getByRole("list", { name: "By repository" });
|
||||
expect(within(repoChart).getByText("acme/alpha")).toBeTruthy();
|
||||
expect(within(repoChart).getByLabelText("acme/alpha: 4 filed / 1 fixed")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders resolved issues with safe outbound links and approximation labels", async () => {
|
||||
apiMock.mockResolvedValue({
|
||||
...githubFixture(),
|
||||
resolved: [
|
||||
{
|
||||
taskId: "FN-100",
|
||||
taskTitle: "Fix alpha crash",
|
||||
repo: "acme/alpha",
|
||||
issueNumber: 123,
|
||||
url: "https://github.com/acme/alpha/issues/123",
|
||||
resolvedAt: "2026-06-10T12:34:56.000Z",
|
||||
resolvedAtExact: true,
|
||||
},
|
||||
{
|
||||
taskId: "FN-101",
|
||||
taskTitle: "Patch unknown import",
|
||||
repo: "(unknown)",
|
||||
issueNumber: null,
|
||||
url: null,
|
||||
resolvedAt: "2026-06-09T08:00:00.000Z",
|
||||
resolvedAtExact: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
render(<GithubArea range={range7d} />);
|
||||
|
||||
const section = await screen.findByTestId("cc-github-resolved");
|
||||
expect(section.textContent).toContain("Resolved issues");
|
||||
expect(section.textContent).toContain("acme/alpha#123");
|
||||
expect(section.textContent).toContain("Fix alpha crash");
|
||||
expect(section.textContent).toContain("FN-100");
|
||||
expect(section.textContent).toContain("(unknown)");
|
||||
expect(section.textContent).toContain("Patch unknown import");
|
||||
expect(section.textContent).toContain("approx");
|
||||
expect(section.textContent).toContain("2026");
|
||||
|
||||
const linkedIssue = within(section).getByRole("link", { name: "Open GitHub issue acme/alpha#123" });
|
||||
expect(linkedIssue.getAttribute("href")).toBe("https://github.com/acme/alpha/issues/123");
|
||||
expect(linkedIssue.getAttribute("target")).toBe("_blank");
|
||||
expect(linkedIssue.getAttribute("rel")).toBe("noopener noreferrer");
|
||||
expect(within(section).queryByRole("link", { name: /unknown/i })).toBeNull();
|
||||
});
|
||||
|
||||
it("omits the resolved issues section for an empty resolved list", async () => {
|
||||
apiMock.mockResolvedValue({ ...githubFixture(), resolved: [] });
|
||||
|
||||
render(<GithubArea range={range7d} />);
|
||||
|
||||
await screen.findByTestId("cc-area-github");
|
||||
expect(screen.queryByTestId("cc-github-resolved")).toBeNull();
|
||||
expect(screen.getByTestId("cc-area-github").textContent).not.toContain("NaN");
|
||||
});
|
||||
|
||||
it("renders the empty state without empty chart shells", async () => {
|
||||
apiMock.mockResolvedValue({ ...githubFixture(), filed: 0, fixed: 0, net: 0, daily: [], byRepo: [] });
|
||||
render(<GithubArea range={range7d} />);
|
||||
|
||||
await screen.findByTestId("cc-area-github");
|
||||
expect(screen.getByTestId("cc-area-github").textContent).toContain("No GitHub issue activity");
|
||||
expect(screen.getByTestId("cc-github-backfill-button")).toBeTruthy();
|
||||
expect(screen.queryByTestId("cc-github-pie")).toBeNull();
|
||||
expect(screen.queryByTestId("cc-github-line")).toBeNull();
|
||||
expect(screen.queryByTestId("cc-github-daily-trend")).toBeNull();
|
||||
expect(screen.queryByTestId("cc-github-by-repo")).toBeNull();
|
||||
});
|
||||
|
||||
it("renders loading and error states", async () => {
|
||||
apiMock.mockImplementationOnce(() => new Promise(() => undefined));
|
||||
const { unmount } = render(<GithubArea range={range7d} />);
|
||||
expect(screen.getByTestId("cc-area-github-loading")).toBeTruthy();
|
||||
expect(screen.queryByTestId("cc-github-pie")).toBeNull();
|
||||
expect(screen.queryByTestId("cc-github-line")).toBeNull();
|
||||
unmount();
|
||||
|
||||
apiMock.mockRejectedValueOnce(new Error("github failed"));
|
||||
render(<GithubArea range={range7d} />);
|
||||
await screen.findByTestId("cc-area-github-error");
|
||||
expect(screen.getByTestId("cc-area-github-error").textContent).toContain("github failed");
|
||||
expect(screen.queryByTestId("cc-github-pie")).toBeNull();
|
||||
expect(screen.queryByTestId("cc-github-line")).toBeNull();
|
||||
});
|
||||
|
||||
it("handles undefined chart arrays and zero values without NaN output", async () => {
|
||||
apiMock.mockResolvedValue({ ...githubFixture(), filed: 1, fixed: 0, net: 1, daily: undefined, byRepo: undefined });
|
||||
render(<GithubArea range={range7d} />);
|
||||
|
||||
await screen.findByTestId("cc-area-github");
|
||||
expect(screen.getByTestId("cc-github-pie")).toBeTruthy();
|
||||
expect(screen.queryByTestId("cc-github-line")).toBeNull();
|
||||
expect(screen.queryByTestId("cc-github-daily-trend")).toBeNull();
|
||||
expect(screen.queryByTestId("cc-github-by-repo")).toBeNull();
|
||||
expect(screen.getByTestId("cc-area-github").textContent).not.toContain("NaN");
|
||||
});
|
||||
|
||||
it("keeps GitHub recharts safe for single-item and non-finite daily data", async () => {
|
||||
apiMock.mockResolvedValue({
|
||||
...githubFixture(),
|
||||
filed: 1,
|
||||
fixed: 1,
|
||||
net: 0,
|
||||
daily: [{ date: "2026-06-08", filed: Number.NaN, fixed: Number.POSITIVE_INFINITY }],
|
||||
byRepo: [{ repo: "acme/broken", filed: Number.NaN, fixed: -1 }],
|
||||
});
|
||||
render(<GithubArea range={range7d} />);
|
||||
|
||||
await screen.findByTestId("cc-area-github");
|
||||
expect(screen.getByTestId("cc-github-pie")).toBeTruthy();
|
||||
expect(screen.getByTestId("cc-github-line")).toBeTruthy();
|
||||
expect(screen.getByTestId("cc-area-github").textContent).not.toContain("NaN");
|
||||
expect(screen.getByTestId("cc-area-github").textContent).not.toContain("Infinity");
|
||||
});
|
||||
|
||||
it("rejects an inverted custom range client-side without fetching", async () => {
|
||||
render(<GithubArea range={customRange("2026-06-10", "2026-06-01")} />);
|
||||
await waitFor(() => expect(apiMock).not.toHaveBeenCalled());
|
||||
});
|
||||
|
||||
it("runs a single backfill batch and renders accumulated result counts", async () => {
|
||||
apiMock.mockResolvedValue(githubFixture());
|
||||
backfillGithubSourceIssueClosedAtMock.mockResolvedValueOnce({
|
||||
scanned: 4,
|
||||
filled: 2,
|
||||
skipped: 1,
|
||||
errors: 0,
|
||||
hasMore: false,
|
||||
});
|
||||
|
||||
render(<GithubArea range={range7d} />);
|
||||
await screen.findByTestId("cc-area-github");
|
||||
|
||||
fireEvent.click(screen.getByTestId("cc-github-backfill-button"));
|
||||
|
||||
await screen.findByText(/Backfill complete/i);
|
||||
expect(backfillGithubSourceIssueClosedAtMock).toHaveBeenCalledWith({ offset: 0, limit: 100 }, undefined);
|
||||
const result = screen.getByTestId("cc-github-backfill-result");
|
||||
expect(result.textContent).toContain("Scanned 4, filled 2, skipped 1, errors 0");
|
||||
});
|
||||
|
||||
it("paginates multi-batch backfills with advancing offsets and summed counts", async () => {
|
||||
apiMock.mockResolvedValue(githubFixture());
|
||||
backfillGithubSourceIssueClosedAtMock
|
||||
.mockResolvedValueOnce({ scanned: 100, filled: 4, skipped: 90, errors: 1, hasMore: true })
|
||||
.mockResolvedValueOnce({ scanned: 25, filled: 3, skipped: 22, errors: 0, hasMore: false });
|
||||
|
||||
render(<GithubArea range={range7d} />);
|
||||
await screen.findByTestId("cc-area-github");
|
||||
fireEvent.click(screen.getByTestId("cc-github-backfill-button"));
|
||||
|
||||
await waitFor(() => expect(backfillGithubSourceIssueClosedAtMock).toHaveBeenCalledTimes(2));
|
||||
expect(backfillGithubSourceIssueClosedAtMock).toHaveBeenNthCalledWith(1, { offset: 0, limit: 100 }, undefined);
|
||||
expect(backfillGithubSourceIssueClosedAtMock).toHaveBeenNthCalledWith(2, { offset: 100, limit: 100 }, undefined);
|
||||
const result = await screen.findByTestId("cc-github-backfill-result");
|
||||
expect(result.textContent).toContain("Scanned 125, filled 7, skipped 112, errors 1");
|
||||
expect(result.className).toContain("cc-github-backfill-status--error");
|
||||
});
|
||||
|
||||
it("shows the all-zero backfill as nothing to backfill instead of an error", async () => {
|
||||
apiMock.mockResolvedValue(githubFixture());
|
||||
backfillGithubSourceIssueClosedAtMock.mockResolvedValueOnce({
|
||||
scanned: 0,
|
||||
filled: 0,
|
||||
skipped: 0,
|
||||
errors: 0,
|
||||
hasMore: false,
|
||||
});
|
||||
|
||||
render(<GithubArea range={range7d} />);
|
||||
await screen.findByTestId("cc-area-github");
|
||||
fireEvent.click(screen.getByTestId("cc-github-backfill-button"));
|
||||
|
||||
const result = await screen.findByTestId("cc-github-backfill-result");
|
||||
expect(result.textContent).toContain("Nothing to backfill");
|
||||
expect(result.className).not.toContain("cc-github-backfill-status--error");
|
||||
});
|
||||
|
||||
it("surfaces nonzero backfill error counts without throwing", async () => {
|
||||
apiMock.mockResolvedValue(githubFixture());
|
||||
backfillGithubSourceIssueClosedAtMock.mockResolvedValueOnce({
|
||||
scanned: 8,
|
||||
filled: 1,
|
||||
skipped: 5,
|
||||
errors: 2,
|
||||
hasMore: false,
|
||||
});
|
||||
|
||||
render(<GithubArea range={range7d} />);
|
||||
await screen.findByTestId("cc-area-github");
|
||||
fireEvent.click(screen.getByTestId("cc-github-backfill-button"));
|
||||
|
||||
const result = await screen.findByTestId("cc-github-backfill-result");
|
||||
expect(result.textContent).toContain("errors 2");
|
||||
expect(result.className).toContain("cc-github-backfill-status--error");
|
||||
});
|
||||
|
||||
it("captures endpoint failures in local error UI", async () => {
|
||||
apiMock.mockResolvedValue(githubFixture());
|
||||
backfillGithubSourceIssueClosedAtMock.mockRejectedValueOnce(new Error("endpoint failed"));
|
||||
|
||||
render(<GithubArea range={range7d} />);
|
||||
await screen.findByTestId("cc-area-github");
|
||||
fireEvent.click(screen.getByTestId("cc-github-backfill-button"));
|
||||
|
||||
const result = await screen.findByTestId("cc-github-backfill-result");
|
||||
expect(result.textContent).toContain("endpoint failed");
|
||||
expect(result.className).toContain("cc-github-backfill-status--error");
|
||||
});
|
||||
|
||||
it("disables and guards the button while a backfill is in flight", async () => {
|
||||
apiMock.mockResolvedValue(githubFixture());
|
||||
let resolveBackfill: ((value: { scanned: number; filled: number; skipped: number; errors: number; hasMore: boolean }) => void) | null = null;
|
||||
backfillGithubSourceIssueClosedAtMock.mockImplementationOnce(
|
||||
() => new Promise((resolve) => {
|
||||
resolveBackfill = resolve;
|
||||
}),
|
||||
);
|
||||
|
||||
render(<GithubArea range={range7d} />);
|
||||
await screen.findByTestId("cc-area-github");
|
||||
const button = screen.getByTestId("cc-github-backfill-button") as HTMLButtonElement;
|
||||
fireEvent.click(button);
|
||||
|
||||
await waitFor(() => expect(button.disabled).toBe(true));
|
||||
fireEvent.click(button);
|
||||
expect(backfillGithubSourceIssueClosedAtMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
resolveBackfill?.({ scanned: 1, filled: 1, skipped: 0, errors: 0, hasMore: false });
|
||||
await screen.findByText(/Backfill complete/i);
|
||||
});
|
||||
|
||||
it("stops a pathological always-has-more response at the max iteration guard", async () => {
|
||||
apiMock.mockResolvedValue(githubFixture());
|
||||
backfillGithubSourceIssueClosedAtMock.mockResolvedValue({
|
||||
scanned: 1,
|
||||
filled: 0,
|
||||
skipped: 1,
|
||||
errors: 0,
|
||||
hasMore: true,
|
||||
});
|
||||
|
||||
render(<GithubArea range={range7d} />);
|
||||
await screen.findByTestId("cc-area-github");
|
||||
fireEvent.click(screen.getByTestId("cc-github-backfill-button"));
|
||||
|
||||
const result = await screen.findByText(/safety limit/i);
|
||||
expect(result.textContent).toContain("safety limit");
|
||||
expect(backfillGithubSourceIssueClosedAtMock).toHaveBeenCalledTimes(1000);
|
||||
expect(screen.getByTestId("cc-github-backfill-result").textContent).toContain("Scanned 1000");
|
||||
});
|
||||
});
|
||||
|
||||
// FN-6684 Mission Control decision: no extra pie/line test here because MissionControlPanel already renders the live SDLC Funnel for its only quantitative distribution; adding a pie would duplicate that affordance.
|
||||
describe("SignalsArea", () => {
|
||||
it("renders the empty state (not an error) when the signals endpoint is missing", async () => {
|
||||
apiMock.mockRejectedValue(new Error("API returned HTML instead of JSON (404)"));
|
||||
render(<SignalsArea range={range7d} />);
|
||||
await screen.findByTestId("cc-area-signals-empty");
|
||||
// Must not surface the error UI.
|
||||
expect(screen.queryByTestId("cc-area-signals-error")).toBeNull();
|
||||
expect(screen.queryByTestId("cc-signals-pie")).toBeNull();
|
||||
});
|
||||
|
||||
it("renders signal metrics and status pie when data is present", async () => {
|
||||
apiMock.mockResolvedValue({
|
||||
totalSignals: 8,
|
||||
open: 3,
|
||||
resolved: 5,
|
||||
mttr: { value: 42, unavailable: false },
|
||||
bySource: [{ source: "sentry", count: 8 }],
|
||||
bySeverity: [{ severity: "error", count: 8 }],
|
||||
});
|
||||
render(<SignalsArea range={range7d} />);
|
||||
await screen.findByTestId("cc-area-signals");
|
||||
expect(screen.getByTestId("cc-signals-total").textContent).toContain("8");
|
||||
expect(screen.getByTestId("cc-signals-mttr").textContent).toContain("42");
|
||||
expect(screen.getByTestId("cc-signals-pie")).toBeTruthy();
|
||||
expect(screen.getByRole("img", { name: "Signal status share" })).toBeTruthy();
|
||||
});
|
||||
|
||||
it("keeps signals pie safe for single-item and non-finite source/severity data", async () => {
|
||||
apiMock.mockResolvedValue({
|
||||
totalSignals: 1,
|
||||
open: 1,
|
||||
resolved: 0,
|
||||
mttr: { value: null, unavailable: true },
|
||||
bySource: [{ source: "broken", count: Number.NaN }],
|
||||
bySeverity: [{ severity: "broken", count: Number.POSITIVE_INFINITY }],
|
||||
});
|
||||
render(<SignalsArea range={range7d} />);
|
||||
await screen.findByTestId("cc-area-signals");
|
||||
expect(screen.getByTestId("cc-signals-pie")).toBeTruthy();
|
||||
expect(screen.getByTestId("cc-area-signals").textContent).not.toContain("NaN");
|
||||
expect(screen.getByTestId("cc-area-signals").textContent).not.toContain("Infinity");
|
||||
});
|
||||
|
||||
it("renders settled zero signals without a pie shell", async () => {
|
||||
apiMock.mockResolvedValue({
|
||||
totalSignals: 0,
|
||||
open: 0,
|
||||
resolved: 0,
|
||||
mttr: { value: null, unavailable: true },
|
||||
bySource: [],
|
||||
bySeverity: [],
|
||||
});
|
||||
render(<SignalsArea range={range7d} />);
|
||||
await screen.findByTestId("cc-area-signals-empty");
|
||||
expect(screen.queryByTestId("cc-signals-pie")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user