Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions src/app/agents/[agent]/page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,70 @@ describe("AgentDetailPage", () => {
const currentItem = screen.getByText(specialId, { selector: '[aria-current="page"]' });
expect(currentItem).toBeInTheDocument();
});

it("formats a large lifetime total with thousands separators", async () => {
mockApiGet.mockReset();
mockApiGet.mockImplementation((url: string) => {
if (url.endsWith("/total")) return Promise.resolve({ total: 1234567 }) as never;
if (url.endsWith("/usage")) return Promise.resolve({ items: [] }) as never;
return Promise.reject(new Error("Not found")) as never;
});

renderPage("agent-big");

const strong = await screen.findByText("1,234,567");
expect(strong.tagName).toBe("STRONG");
expect(strong.parentElement).toHaveTextContent("1,234,567 requests");
});

it("formats per-service totals with thousands separators", async () => {
mockApiGet.mockReset();
mockApiGet.mockImplementation((url: string) => {
if (url.endsWith("/total")) return Promise.resolve({ total: 0 }) as never;
if (url.endsWith("/usage"))
return Promise.resolve({
items: [
{ serviceId: "svc-a", total: 999 },
{ serviceId: "svc-b", total: 1500000 },
],
}) as never;
return Promise.reject(new Error("Not found")) as never;
});

renderPage("agent-svc");

await screen.findByText(/Lifetime total/i);

expect(screen.getByText("999 requests")).toBeInTheDocument();
expect(screen.getByText("1,500,000 requests")).toBeInTheDocument();
});

it("renders zero total without grouping", async () => {
mockApiGet.mockReset();
mockApiGet.mockImplementation((url: string) => {
if (url.endsWith("/total")) return Promise.resolve({ total: 0 }) as never;
if (url.endsWith("/usage")) return Promise.resolve({ items: [] }) as never;
return Promise.reject(new Error("Not found")) as never;
});

renderPage("agent-zero");

const strong = await screen.findByText("0");
expect(strong.tagName).toBe("STRONG");
});

it("does not render lifetime total when the optional total fetch fails", async () => {
mockApiGet.mockReset();
mockApiGet.mockImplementation((url: string) => {
if (url.endsWith("/total")) return Promise.reject(new Error("boom")) as never;
if (url.endsWith("/usage")) return Promise.resolve({ items: [] }) as never;
return Promise.reject(new Error("Not found")) as never;
});

renderPage("agent-no-total");

await screen.findByText(/No services consumed yet/i);

expect(screen.queryByText(/Lifetime total/i)).not.toBeInTheDocument();
});
});
5 changes: 3 additions & 2 deletions src/app/agents/[agent]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { useEffect, useState, use } from "react";
import { Breadcrumb } from "@/components/Breadcrumb";
import { apiGet } from "@/lib/apiClient";
import { formatRequests } from "@/lib/format";

type Usage = { agent: string; items: { serviceId: string; total: number }[] };

Expand Down Expand Up @@ -44,7 +45,7 @@ export default function AgentDetailPage({
)}
{total !== null && (
<p className="text-sm">
Lifetime total: <strong>{total}</strong> requests
Lifetime total: <strong>{formatRequests(total)}</strong> requests
</p>
)}
{items && items.length === 0 && (
Expand All @@ -55,7 +56,7 @@ export default function AgentDetailPage({
{items.map((s) => (
<li key={s.serviceId} className="flex items-center justify-between py-3 text-sm">
<span className="font-mono">{s.serviceId}</span>
<span>{s.total} requests</span>
<span>{formatRequests(s.total)} requests</span>
</li>
))}
</ul>
Expand Down
8 changes: 8 additions & 0 deletions src/app/services/new/page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -197,11 +197,19 @@ describe("NewServicePage", () => {
expect(submitButton).toBeDisabled();
expect(submitButton).toHaveTextContent("Saving…");

// Button should have aria-busy while loading
expect(submitButton).toHaveAttribute("aria-busy", "true");

// Resolve post request
resolvePost({});

await waitFor(() => {
expect(mockPush).toHaveBeenCalledWith("/services");
});

// aria-busy should be removed after submission
await waitFor(() => {
expect(submitButton).not.toHaveAttribute("aria-busy");
});
});
});
1 change: 1 addition & 0 deletions src/app/services/new/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export default function NewServicePage() {

<Button
type="submit"
loading={loading}
disabled={loading}
className="self-start"
>
Expand Down
3 changes: 3 additions & 0 deletions src/components/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,19 @@ const ring =

type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
variant?: Variant;
loading?: boolean;
};

export function Button({
variant = "primary",
loading = false,
className = "",
...rest
}: ButtonProps) {
return (
<button
{...rest}
aria-busy={loading || undefined}
className={`rounded-full px-5 py-2 text-sm font-medium disabled:opacity-50 disabled:cursor-not-allowed ${variants[variant]} ${ring} ${className}`}
/>
);
Expand Down
Loading