/**
* @vitest-environment happy-dom
*/
import type { ReactNode } from "react";
import { act } from "react";
import { createRoot } from "react-dom/client";
import { NextIntlClientProvider } from "next-intl";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { UsageLogsFilters } from "@/app/[locale]/dashboard/logs/_components/usage-logs-filters";
import dashboardMessages from "../../messages/en/dashboard.json";
const originalCreateObjectURL = globalThis.URL.createObjectURL;
const originalRevokeObjectURL = globalThis.URL.revokeObjectURL;
const originalAnchorClick = HTMLAnchorElement.prototype.click;
const {
downloadUsageLogsExportMock,
getUsageLogsExportStatusMock,
startUsageLogsExportMock,
toastErrorMock,
toastSuccessMock,
} = vi.hoisted(() => ({
startUsageLogsExportMock: vi.fn(),
getUsageLogsExportStatusMock: vi.fn(),
downloadUsageLogsExportMock: vi.fn(),
toastSuccessMock: vi.fn(),
toastErrorMock: vi.fn(),
}));
vi.mock("@/actions/usage-logs", () => ({
startUsageLogsExport: startUsageLogsExportMock,
getUsageLogsExportStatus: getUsageLogsExportStatusMock,
downloadUsageLogsExport: downloadUsageLogsExportMock,
}));
vi.mock("sonner", () => ({
toast: {
success: toastSuccessMock,
error: toastErrorMock,
},
}));
vi.mock("@/app/[locale]/dashboard/logs/_components/filters/active-filters-display", () => ({
ActiveFiltersDisplay: () =>
,
}));
vi.mock("@/app/[locale]/dashboard/logs/_components/filters/filter-section", () => ({
FilterSection: ({ children }: { children: ReactNode }) => {children}
,
}));
vi.mock("@/app/[locale]/dashboard/logs/_components/filters/identity-filters", () => ({
IdentityFilters: () => ,
}));
vi.mock("@/app/[locale]/dashboard/logs/_components/filters/quick-filters-bar", () => ({
QuickFiltersBar: () => ,
}));
vi.mock("@/app/[locale]/dashboard/logs/_components/filters/request-filters", () => ({
RequestFilters: ({
onFiltersChange,
}: {
onFiltersChange: (filters: Record) => void;
}) => (
),
}));
vi.mock("@/app/[locale]/dashboard/logs/_components/filters/status-filters", () => ({
StatusFilters: () => ,
}));
vi.mock("@/app/[locale]/dashboard/logs/_components/filters/time-filters", () => ({
TimeFilters: () => ,
}));
function renderWithIntl(node: ReactNode) {
const container = document.createElement("div");
document.body.appendChild(container);
const root = createRoot(container);
act(() => {
root.render(
{node}
);
});
return {
container,
unmount: () => {
act(() => root.unmount());
container.remove();
},
};
}
async function actClick(el: Element | null) {
if (!el) throw new Error("element not found");
await act(async () => {
el.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
}
async function flushPromises() {
await act(async () => {
await Promise.resolve();
await Promise.resolve();
});
}
describe("UsageLogsFilters export progress UI", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.useFakeTimers();
globalThis.URL.createObjectURL = vi.fn(() => "blob:usage-logs");
globalThis.URL.revokeObjectURL = vi.fn();
HTMLAnchorElement.prototype.click = vi.fn();
});
afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
globalThis.URL.createObjectURL = originalCreateObjectURL;
globalThis.URL.revokeObjectURL = originalRevokeObjectURL;
HTMLAnchorElement.prototype.click = originalAnchorClick;
document.body.innerHTML = "";
});
test("shows export progress while polling and downloads when completed", async () => {
startUsageLogsExportMock.mockResolvedValue({ ok: true, data: { jobId: "job-1" } });
getUsageLogsExportStatusMock
.mockResolvedValueOnce({
ok: true,
data: {
jobId: "job-1",
status: "running",
processedRows: 50,
totalRows: 200,
progressPercent: 25,
},
})
.mockResolvedValueOnce({
ok: true,
data: {
jobId: "job-1",
status: "completed",
processedRows: 200,
totalRows: 200,
progressPercent: 100,
},
});
downloadUsageLogsExportMock.mockResolvedValue({ ok: true, data: "\uFEFFTime,User\n" });
const { container, unmount } = renderWithIntl(
{}}
onReset={() => {}}
/>
);
const exportButton = Array.from(container.querySelectorAll("button")).find(
(button) => (button.textContent || "").trim() === "Export"
);
await actClick(exportButton ?? null);
await flushPromises();
expect(container.textContent).toContain("Exported 50 / 200");
expect(container.textContent).toContain("25%");
await act(async () => {
await vi.advanceTimersByTimeAsync(800);
});
await flushPromises();
expect(downloadUsageLogsExportMock).toHaveBeenCalledWith("job-1");
expect(toastSuccessMock).toHaveBeenCalledWith("Export completed successfully");
expect(toastErrorMock).not.toHaveBeenCalled();
unmount();
});
test("exports the applied filters instead of unapplied local draft filters", async () => {
startUsageLogsExportMock.mockResolvedValue({ ok: true, data: { jobId: "job-2" } });
getUsageLogsExportStatusMock.mockResolvedValueOnce({
ok: true,
data: {
jobId: "job-2",
status: "completed",
processedRows: 1,
totalRows: 1,
progressPercent: 100,
},
});
downloadUsageLogsExportMock.mockResolvedValue({ ok: true, data: "\uFEFFTime,User\n" });
const { container, unmount } = renderWithIntl(
{}}
onReset={() => {}}
/>
);
await actClick(container.querySelector("[data-testid='request-filters']"));
const exportButton = Array.from(container.querySelectorAll("button")).find(
(button) => (button.textContent || "").trim() === "Export"
);
await actClick(exportButton ?? null);
await flushPromises();
expect(startUsageLogsExportMock).toHaveBeenCalledWith({ sessionId: "draft-session" });
unmount();
});
});