You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
409 lines
10 KiB
409 lines
10 KiB
import { describe, it, expect, vi, beforeEach, afterEach, beforeAll, afterAll } from "vitest";
|
|
import { render, screen, fireEvent } from "@testing-library/react";
|
|
import { CostDialog } from "@/components/CostDialog";
|
|
import { PredictedCostResult } from "@/utils/costCalculator";
|
|
|
|
// Mock the workflow store
|
|
const mockResetIncurredCost = vi.fn();
|
|
const mockUseWorkflowStore = vi.fn();
|
|
|
|
vi.mock("@/store/workflowStore", () => ({
|
|
useWorkflowStore: (selector?: (state: unknown) => unknown) => {
|
|
if (selector) {
|
|
return mockUseWorkflowStore(selector);
|
|
}
|
|
return mockUseWorkflowStore((s: unknown) => s);
|
|
},
|
|
}));
|
|
|
|
// Mock confirm
|
|
const mockConfirm = vi.fn(() => true);
|
|
|
|
describe("CostDialog", () => {
|
|
beforeAll(() => {
|
|
vi.stubGlobal("confirm", mockConfirm);
|
|
});
|
|
|
|
afterAll(() => {
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
mockConfirm.mockReturnValue(true); // Reset default return value
|
|
mockUseWorkflowStore.mockImplementation((selector) => {
|
|
const state = {
|
|
resetIncurredCost: mockResetIncurredCost,
|
|
};
|
|
return selector(state);
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
const createPredictedCost = (overrides: Partial<PredictedCostResult> = {}): PredictedCostResult => ({
|
|
totalCost: 0.50,
|
|
breakdown: [
|
|
{
|
|
model: "nano-banana",
|
|
resolution: "1K",
|
|
count: 5,
|
|
unitCost: 0.039,
|
|
subtotal: 0.195,
|
|
},
|
|
{
|
|
model: "nano-banana-pro",
|
|
resolution: "2K",
|
|
count: 2,
|
|
unitCost: 0.134,
|
|
subtotal: 0.268,
|
|
},
|
|
],
|
|
nodeCount: 7,
|
|
...overrides,
|
|
});
|
|
|
|
describe("Basic Rendering", () => {
|
|
it("should render dialog with title", () => {
|
|
render(
|
|
<CostDialog
|
|
predictedCost={createPredictedCost()}
|
|
incurredCost={0}
|
|
onClose={vi.fn()}
|
|
/>
|
|
);
|
|
|
|
expect(screen.getByText("Workflow Costs")).toBeInTheDocument();
|
|
});
|
|
|
|
it("should render close button", () => {
|
|
const { container } = render(
|
|
<CostDialog
|
|
predictedCost={createPredictedCost()}
|
|
incurredCost={0}
|
|
onClose={vi.fn()}
|
|
/>
|
|
);
|
|
|
|
// Close button has an SVG with X icon
|
|
const closeButton = container.querySelector("button");
|
|
expect(closeButton).toBeInTheDocument();
|
|
});
|
|
|
|
it("should render Predicted Cost section", () => {
|
|
render(
|
|
<CostDialog
|
|
predictedCost={createPredictedCost()}
|
|
incurredCost={0}
|
|
onClose={vi.fn()}
|
|
/>
|
|
);
|
|
|
|
expect(screen.getByText("Predicted Cost")).toBeInTheDocument();
|
|
});
|
|
|
|
it("should render Incurred Cost section", () => {
|
|
render(
|
|
<CostDialog
|
|
predictedCost={createPredictedCost()}
|
|
incurredCost={0}
|
|
onClose={vi.fn()}
|
|
/>
|
|
);
|
|
|
|
expect(screen.getByText("Incurred Cost")).toBeInTheDocument();
|
|
});
|
|
|
|
it("should render Pricing Reference section", () => {
|
|
render(
|
|
<CostDialog
|
|
predictedCost={createPredictedCost()}
|
|
incurredCost={0}
|
|
onClose={vi.fn()}
|
|
/>
|
|
);
|
|
|
|
expect(screen.getByText("Pricing Reference:")).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe("Cost Display", () => {
|
|
it("should display formatted predicted cost", () => {
|
|
render(
|
|
<CostDialog
|
|
predictedCost={createPredictedCost({ totalCost: 1.25 })}
|
|
incurredCost={0}
|
|
onClose={vi.fn()}
|
|
/>
|
|
);
|
|
|
|
expect(screen.getByText("$1.25")).toBeInTheDocument();
|
|
});
|
|
|
|
it("should display formatted incurred cost", () => {
|
|
render(
|
|
<CostDialog
|
|
predictedCost={createPredictedCost()}
|
|
incurredCost={2.50}
|
|
onClose={vi.fn()}
|
|
/>
|
|
);
|
|
|
|
expect(screen.getByText("$2.50")).toBeInTheDocument();
|
|
});
|
|
|
|
it("should display $0.00 for zero costs", () => {
|
|
render(
|
|
<CostDialog
|
|
predictedCost={createPredictedCost({ totalCost: 0 })}
|
|
incurredCost={0}
|
|
onClose={vi.fn()}
|
|
/>
|
|
);
|
|
|
|
// Should have two $0.00 - one for predicted, one for incurred
|
|
const zeroValues = screen.getAllByText("$0.00");
|
|
expect(zeroValues.length).toBe(2);
|
|
});
|
|
});
|
|
|
|
describe("Cost Breakdown", () => {
|
|
it("should render per-model cost rows", () => {
|
|
render(
|
|
<CostDialog
|
|
predictedCost={createPredictedCost()}
|
|
incurredCost={0}
|
|
onClose={vi.fn()}
|
|
/>
|
|
);
|
|
|
|
// Check for model count and name
|
|
expect(screen.getByText(/5x Nano Banana/)).toBeInTheDocument();
|
|
expect(screen.getByText(/2x Nano Banana Pro/)).toBeInTheDocument();
|
|
});
|
|
|
|
it("should show resolution for Nano Banana Pro models", () => {
|
|
render(
|
|
<CostDialog
|
|
predictedCost={createPredictedCost()}
|
|
incurredCost={0}
|
|
onClose={vi.fn()}
|
|
/>
|
|
);
|
|
|
|
// Nano Banana Pro should show resolution
|
|
expect(screen.getByText(/Nano Banana Pro.*\(2K\)/)).toBeInTheDocument();
|
|
});
|
|
|
|
it("should display subtotal for each model type", () => {
|
|
render(
|
|
<CostDialog
|
|
predictedCost={createPredictedCost()}
|
|
incurredCost={0}
|
|
onClose={vi.fn()}
|
|
/>
|
|
);
|
|
|
|
// Check for subtotals
|
|
expect(screen.getByText("$0.20")).toBeInTheDocument(); // 0.195 rounded
|
|
expect(screen.getByText("$0.27")).toBeInTheDocument(); // 0.268 rounded
|
|
});
|
|
});
|
|
|
|
describe("Empty State", () => {
|
|
it("should show empty state when no generation nodes exist", () => {
|
|
render(
|
|
<CostDialog
|
|
predictedCost={createPredictedCost({
|
|
totalCost: 0,
|
|
breakdown: [],
|
|
nodeCount: 0
|
|
})}
|
|
incurredCost={0}
|
|
onClose={vi.fn()}
|
|
/>
|
|
);
|
|
|
|
expect(screen.getByText("No generation nodes in workflow")).toBeInTheDocument();
|
|
});
|
|
|
|
it("should not show breakdown section when nodeCount is 0", () => {
|
|
render(
|
|
<CostDialog
|
|
predictedCost={createPredictedCost({
|
|
totalCost: 0,
|
|
breakdown: [],
|
|
nodeCount: 0
|
|
})}
|
|
incurredCost={0}
|
|
onClose={vi.fn()}
|
|
/>
|
|
);
|
|
|
|
expect(screen.queryByText(/5x Nano Banana/)).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe("Reset Costs Button", () => {
|
|
it("should not show reset button when incurredCost is 0", () => {
|
|
render(
|
|
<CostDialog
|
|
predictedCost={createPredictedCost()}
|
|
incurredCost={0}
|
|
onClose={vi.fn()}
|
|
/>
|
|
);
|
|
|
|
expect(screen.queryByText("Reset to $0.00")).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("should show reset button when incurredCost is greater than 0", () => {
|
|
render(
|
|
<CostDialog
|
|
predictedCost={createPredictedCost()}
|
|
incurredCost={1.00}
|
|
onClose={vi.fn()}
|
|
/>
|
|
);
|
|
|
|
expect(screen.getByText("Reset to $0.00")).toBeInTheDocument();
|
|
});
|
|
|
|
it("should show confirmation dialog when reset is clicked", () => {
|
|
render(
|
|
<CostDialog
|
|
predictedCost={createPredictedCost()}
|
|
incurredCost={1.00}
|
|
onClose={vi.fn()}
|
|
/>
|
|
);
|
|
|
|
fireEvent.click(screen.getByText("Reset to $0.00"));
|
|
|
|
expect(mockConfirm).toHaveBeenCalledWith("Reset incurred cost to $0.00?");
|
|
});
|
|
|
|
it("should call resetIncurredCost when confirmed", () => {
|
|
mockConfirm.mockReturnValue(true);
|
|
|
|
render(
|
|
<CostDialog
|
|
predictedCost={createPredictedCost()}
|
|
incurredCost={1.00}
|
|
onClose={vi.fn()}
|
|
/>
|
|
);
|
|
|
|
fireEvent.click(screen.getByText("Reset to $0.00"));
|
|
|
|
expect(mockResetIncurredCost).toHaveBeenCalled();
|
|
});
|
|
|
|
it("should not call resetIncurredCost when cancelled", () => {
|
|
mockConfirm.mockReturnValue(false);
|
|
|
|
render(
|
|
<CostDialog
|
|
predictedCost={createPredictedCost()}
|
|
incurredCost={1.00}
|
|
onClose={vi.fn()}
|
|
/>
|
|
);
|
|
|
|
fireEvent.click(screen.getByText("Reset to $0.00"));
|
|
|
|
expect(mockResetIncurredCost).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("Close Behavior", () => {
|
|
it("should call onClose when close button is clicked", () => {
|
|
const onClose = vi.fn();
|
|
|
|
const { container } = render(
|
|
<CostDialog
|
|
predictedCost={createPredictedCost()}
|
|
incurredCost={0}
|
|
onClose={onClose}
|
|
/>
|
|
);
|
|
|
|
// Click the close button (first button in the dialog)
|
|
const closeButton = container.querySelector("button");
|
|
fireEvent.click(closeButton!);
|
|
|
|
expect(onClose).toHaveBeenCalled();
|
|
});
|
|
|
|
it("should call onClose when Escape key is pressed", () => {
|
|
const onClose = vi.fn();
|
|
|
|
render(
|
|
<CostDialog
|
|
predictedCost={createPredictedCost()}
|
|
incurredCost={0}
|
|
onClose={onClose}
|
|
/>
|
|
);
|
|
|
|
fireEvent.keyDown(window, { key: "Escape" });
|
|
|
|
expect(onClose).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("Pricing Reference", () => {
|
|
it("should display nano-banana pricing", () => {
|
|
render(
|
|
<CostDialog
|
|
predictedCost={createPredictedCost()}
|
|
incurredCost={0}
|
|
onClose={vi.fn()}
|
|
/>
|
|
);
|
|
|
|
expect(screen.getByText(/Nano Banana \(Flash\):/)).toBeInTheDocument();
|
|
});
|
|
|
|
it("should display nano-banana-pro pricing tiers", () => {
|
|
render(
|
|
<CostDialog
|
|
predictedCost={createPredictedCost()}
|
|
incurredCost={0}
|
|
onClose={vi.fn()}
|
|
/>
|
|
);
|
|
|
|
expect(screen.getByText(/Nano Banana Pro 1K\/2K:/)).toBeInTheDocument();
|
|
expect(screen.getByText(/Nano Banana Pro 4K:/)).toBeInTheDocument();
|
|
});
|
|
|
|
it("should display currency note", () => {
|
|
render(
|
|
<CostDialog
|
|
predictedCost={createPredictedCost()}
|
|
incurredCost={0}
|
|
onClose={vi.fn()}
|
|
/>
|
|
);
|
|
|
|
expect(screen.getByText("All prices in USD")).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe("Incurred Cost Description", () => {
|
|
it("should display description for incurred costs", () => {
|
|
render(
|
|
<CostDialog
|
|
predictedCost={createPredictedCost()}
|
|
incurredCost={0}
|
|
onClose={vi.fn()}
|
|
/>
|
|
);
|
|
|
|
expect(screen.getByText("Actual API spend from successful generations")).toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|
|
|