mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-22 11:14:05 +00:00
Added comprehensive unit test coverage for Container Copy jobs (#2275)
* copy job uts * unit test coverage * lint fix * normalize account dropdown id
This commit is contained in:
@@ -0,0 +1,611 @@
|
||||
import "@testing-library/jest-dom";
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import React from "react";
|
||||
import { CopyJobActions, CopyJobMigrationType, CopyJobStatusType } from "../../Enums/CopyJobEnums";
|
||||
import { CopyJobType, HandleJobActionClickType } from "../../Types/CopyJobTypes";
|
||||
import CopyJobActionMenu from "./CopyJobActionMenu";
|
||||
|
||||
jest.mock("../../ContainerCopyMessages", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
MonitorJobs: {
|
||||
Columns: {
|
||||
actions: "Actions",
|
||||
},
|
||||
Actions: {
|
||||
pause: "Pause",
|
||||
resume: "Resume",
|
||||
cancel: "Cancel",
|
||||
complete: "Complete",
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe("CopyJobActionMenu", () => {
|
||||
const createMockJob = (overrides: Partial<CopyJobType> = {}): CopyJobType =>
|
||||
({
|
||||
ID: "test-job-id",
|
||||
Mode: CopyJobMigrationType.Offline,
|
||||
Name: "Test Job",
|
||||
Status: CopyJobStatusType.InProgress,
|
||||
CompletionPercentage: 50,
|
||||
Duration: "00:10:30",
|
||||
LastUpdatedTime: "2025-01-01T10:00:00Z",
|
||||
timestamp: Date.now(),
|
||||
Source: {
|
||||
databaseName: "sourceDb",
|
||||
collectionName: "sourceContainer",
|
||||
component: "source",
|
||||
},
|
||||
Destination: {
|
||||
databaseName: "targetDb",
|
||||
collectionName: "targetContainer",
|
||||
component: "destination",
|
||||
},
|
||||
...overrides,
|
||||
}) as CopyJobType;
|
||||
|
||||
const mockHandleClick: HandleJobActionClickType = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("Component Rendering", () => {
|
||||
it("should render the action menu button for active jobs", () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.InProgress });
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
expect(actionButton).toBeInTheDocument();
|
||||
expect(actionButton).toHaveAttribute("aria-label", "Actions");
|
||||
expect(actionButton).toHaveAttribute("title", "Actions");
|
||||
});
|
||||
|
||||
it("should not render anything for completed jobs", () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.Completed });
|
||||
|
||||
const { container } = render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it("should not render anything for cancelled jobs", () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.Cancelled });
|
||||
|
||||
const { container } = render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it("should not render anything for failed jobs", () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.Failed });
|
||||
|
||||
const { container } = render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it("should not render anything for faulted jobs", () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.Faulted });
|
||||
|
||||
const { container } = render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Menu Items for Different Job Statuses", () => {
|
||||
it("should show pause and cancel actions for InProgress jobs", () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.InProgress });
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
expect(screen.getByText("Pause")).toBeInTheDocument();
|
||||
expect(screen.getByText("Cancel")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Resume")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show resume and cancel actions for Paused jobs", () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.Paused });
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
expect(screen.getByText("Resume")).toBeInTheDocument();
|
||||
expect(screen.getByText("Cancel")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Pause")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show pause and cancel actions for Pending jobs", () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.Pending });
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
expect(screen.getByText("Pause")).toBeInTheDocument();
|
||||
expect(screen.getByText("Cancel")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Resume")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show only resume action for Skipped jobs", () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.Skipped });
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
expect(screen.getByText("Resume")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Pause")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Cancel")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show pause and cancel actions for Running jobs", () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.Running });
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
expect(screen.getByText("Pause")).toBeInTheDocument();
|
||||
expect(screen.getByText("Cancel")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Resume")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show pause and cancel actions for Partitioning jobs", () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.Partitioning });
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
expect(screen.getByText("Pause")).toBeInTheDocument();
|
||||
expect(screen.getByText("Cancel")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Resume")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Online Mode Complete Action", () => {
|
||||
it("should show complete action for online InProgress jobs", () => {
|
||||
const job = createMockJob({
|
||||
Status: CopyJobStatusType.InProgress,
|
||||
Mode: CopyJobMigrationType.Online,
|
||||
});
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
expect(screen.getByText("Complete")).toBeInTheDocument();
|
||||
expect(screen.getByText("Pause")).toBeInTheDocument();
|
||||
expect(screen.getByText("Cancel")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show complete action for online Running jobs", () => {
|
||||
const job = createMockJob({
|
||||
Status: CopyJobStatusType.Running,
|
||||
Mode: CopyJobMigrationType.Online,
|
||||
});
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
expect(screen.getByText("Complete")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show complete action for online Partitioning jobs", () => {
|
||||
const job = createMockJob({
|
||||
Status: CopyJobStatusType.Partitioning,
|
||||
Mode: CopyJobMigrationType.Online,
|
||||
});
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
expect(screen.getByText("Complete")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not show complete action for offline jobs", () => {
|
||||
const job = createMockJob({
|
||||
Status: CopyJobStatusType.InProgress,
|
||||
Mode: CopyJobMigrationType.Offline,
|
||||
});
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
expect(screen.queryByText("Complete")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle case-insensitive online mode detection", () => {
|
||||
const job = createMockJob({
|
||||
Status: CopyJobStatusType.InProgress,
|
||||
Mode: "ONLINE",
|
||||
});
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
expect(screen.getByText("Complete")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Action Click Handling", () => {
|
||||
it("should call handleClick when pause action is clicked", () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.InProgress });
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
const pauseButton = screen.getByText("Pause");
|
||||
fireEvent.click(pauseButton);
|
||||
|
||||
expect(mockHandleClick).toHaveBeenCalledWith(job, CopyJobActions.pause, expect.any(Function));
|
||||
});
|
||||
|
||||
it("should call handleClick when cancel action is clicked", () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.InProgress });
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
const cancelButton = screen.getByText("Cancel");
|
||||
fireEvent.click(cancelButton);
|
||||
|
||||
expect(mockHandleClick).toHaveBeenCalledWith(job, CopyJobActions.cancel, expect.any(Function));
|
||||
});
|
||||
|
||||
it("should call handleClick when resume action is clicked", () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.Paused });
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
const resumeButton = screen.getByText("Resume");
|
||||
fireEvent.click(resumeButton);
|
||||
|
||||
expect(mockHandleClick).toHaveBeenCalledWith(job, CopyJobActions.resume, expect.any(Function));
|
||||
});
|
||||
|
||||
it("should call handleClick when complete action is clicked", () => {
|
||||
const job = createMockJob({
|
||||
Status: CopyJobStatusType.InProgress,
|
||||
Mode: CopyJobMigrationType.Online,
|
||||
});
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
const completeButton = screen.getByText("Complete");
|
||||
fireEvent.click(completeButton);
|
||||
|
||||
expect(mockHandleClick).toHaveBeenCalledWith(job, CopyJobActions.complete, expect.any(Function));
|
||||
});
|
||||
});
|
||||
|
||||
describe("Disabled States During Updates", () => {
|
||||
const TestComponentWrapper: React.FC<{
|
||||
job: CopyJobType;
|
||||
initialUpdatingState?: { jobName: string; action: string } | null;
|
||||
}> = ({ job, initialUpdatingState = null }) => {
|
||||
const stateUpdater = React.useState(initialUpdatingState);
|
||||
const setUpdatingJobAction = stateUpdater[1];
|
||||
|
||||
const testHandleClick: HandleJobActionClickType = (job, action, setUpdatingJobActionCallback) => {
|
||||
setUpdatingJobActionCallback({ jobName: job.Name, action });
|
||||
setUpdatingJobAction({ jobName: job.Name, action });
|
||||
};
|
||||
|
||||
return <CopyJobActionMenu job={job} handleClick={testHandleClick} />;
|
||||
};
|
||||
|
||||
it("should disable pause action when job is being paused", async () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.InProgress });
|
||||
|
||||
render(<TestComponentWrapper job={job} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
const pauseButton = screen.getByText("Pause");
|
||||
fireEvent.click(pauseButton);
|
||||
fireEvent.click(actionButton);
|
||||
const pauseButtonAfterClick = screen.getByText("Pause");
|
||||
expect(pauseButtonAfterClick).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not disable actions for different jobs when one is updating", () => {
|
||||
const job1 = createMockJob({ Name: "Job1", Status: CopyJobStatusType.InProgress });
|
||||
const job2 = createMockJob({ Name: "Job2", Status: CopyJobStatusType.InProgress });
|
||||
|
||||
const { rerender } = render(<TestComponentWrapper job={job1} />);
|
||||
let actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
fireEvent.click(screen.getByText("Pause"));
|
||||
rerender(<TestComponentWrapper job={job2} />);
|
||||
|
||||
actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
expect(screen.getByText("Pause")).toBeInTheDocument();
|
||||
expect(screen.getByText("Cancel")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should properly handle multiple action types being disabled for the same job", () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.InProgress });
|
||||
render(<TestComponentWrapper job={job} />);
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
|
||||
fireEvent.click(actionButton);
|
||||
fireEvent.click(screen.getByText("Pause"));
|
||||
|
||||
fireEvent.click(actionButton);
|
||||
fireEvent.click(screen.getByText("Cancel"));
|
||||
|
||||
fireEvent.click(actionButton);
|
||||
expect(screen.getByText("Pause")).toBeInTheDocument();
|
||||
expect(screen.getByText("Cancel")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle complete action disabled state for online jobs", () => {
|
||||
const job = createMockJob({
|
||||
Status: CopyJobStatusType.InProgress,
|
||||
Mode: CopyJobMigrationType.Online,
|
||||
});
|
||||
|
||||
render(<TestComponentWrapper job={job} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
const completeButton = screen.getByText("Complete");
|
||||
fireEvent.click(completeButton);
|
||||
|
||||
fireEvent.click(actionButton);
|
||||
expect(screen.getByText("Complete")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Edge Cases", () => {
|
||||
it("should handle undefined mode gracefully", () => {
|
||||
const job = createMockJob({
|
||||
Status: CopyJobStatusType.InProgress,
|
||||
Mode: undefined as any,
|
||||
});
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
expect(screen.queryByText("Complete")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("Pause")).toBeInTheDocument();
|
||||
expect(screen.getByText("Cancel")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle null mode gracefully", () => {
|
||||
const job = createMockJob({
|
||||
Status: CopyJobStatusType.InProgress,
|
||||
Mode: null as any,
|
||||
});
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
expect(screen.queryByText("Complete")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle empty string mode gracefully", () => {
|
||||
const job = createMockJob({
|
||||
Status: CopyJobStatusType.InProgress,
|
||||
Mode: "",
|
||||
});
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
expect(screen.queryByText("Complete")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should return all base items for unknown status", () => {
|
||||
const job = createMockJob({ Status: "UnknownStatus" as any });
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
expect(screen.getByText("Pause")).toBeInTheDocument();
|
||||
expect(screen.getByText("Cancel")).toBeInTheDocument();
|
||||
expect(screen.getByText("Resume")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Icon and Accessibility", () => {
|
||||
it("should have correct icon and accessibility attributes", () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.InProgress });
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
|
||||
expect(actionButton).toHaveAttribute("aria-label", "Actions");
|
||||
expect(actionButton).toHaveAttribute("title", "Actions");
|
||||
|
||||
const moreIcon = actionButton.querySelector('[data-icon-name="More"]');
|
||||
expect(moreIcon || actionButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should have correct menu item icons", () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.InProgress });
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
expect(screen.getByText("Pause")).toBeInTheDocument();
|
||||
expect(screen.getByText("Cancel")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Component State Management", () => {
|
||||
it("should manage updating job action state correctly", () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.InProgress });
|
||||
const mockHandleClickWithState: HandleJobActionClickType = jest.fn((job, action, setUpdatingJobAction) => {
|
||||
setUpdatingJobAction({ jobName: job.Name, action });
|
||||
});
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClickWithState} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
const pauseButton = screen.getByText("Pause");
|
||||
fireEvent.click(pauseButton);
|
||||
|
||||
expect(mockHandleClickWithState).toHaveBeenCalledWith(job, CopyJobActions.pause, expect.any(Function));
|
||||
});
|
||||
|
||||
it("should handle rapid successive clicks properly", () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.InProgress });
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
const pauseButton = screen.getByText("Pause");
|
||||
fireEvent.click(pauseButton);
|
||||
|
||||
fireEvent.click(actionButton);
|
||||
const pauseButton2 = screen.getByText("Pause");
|
||||
fireEvent.click(pauseButton2);
|
||||
|
||||
fireEvent.click(actionButton);
|
||||
const pauseButton3 = screen.getByText("Pause");
|
||||
fireEvent.click(pauseButton3);
|
||||
|
||||
expect(mockHandleClick).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Integration Tests", () => {
|
||||
it("should work correctly with different job names", () => {
|
||||
const jobWithLongName = createMockJob({
|
||||
Name: "Very Long Job Name That Might Cause UI Issues",
|
||||
Status: CopyJobStatusType.InProgress,
|
||||
});
|
||||
|
||||
render(<CopyJobActionMenu job={jobWithLongName} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
const pauseButton = screen.getByText("Pause");
|
||||
fireEvent.click(pauseButton);
|
||||
|
||||
expect(mockHandleClick).toHaveBeenCalledWith(jobWithLongName, CopyJobActions.pause, expect.any(Function));
|
||||
});
|
||||
|
||||
it("should handle special characters in job names", () => {
|
||||
const jobWithSpecialChars = createMockJob({
|
||||
Name: "Job-Name_With$pecial#Characters!@",
|
||||
Status: CopyJobStatusType.Paused,
|
||||
});
|
||||
|
||||
render(<CopyJobActionMenu job={jobWithSpecialChars} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
const resumeButton = screen.getByText("Resume");
|
||||
fireEvent.click(resumeButton);
|
||||
|
||||
expect(mockHandleClick).toHaveBeenCalledWith(jobWithSpecialChars, CopyJobActions.resume, expect.any(Function));
|
||||
});
|
||||
|
||||
it("should maintain consistent behavior across re-renders", () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.InProgress });
|
||||
const { rerender } = render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
let actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
expect(actionButton).toBeInTheDocument();
|
||||
|
||||
rerender(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
expect(actionButton).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(actionButton);
|
||||
expect(screen.getByText("Pause")).toBeInTheDocument();
|
||||
expect(screen.getByText("Cancel")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle prop changes correctly", () => {
|
||||
const job1 = createMockJob({ Status: CopyJobStatusType.InProgress });
|
||||
const job2 = createMockJob({ Status: CopyJobStatusType.Paused });
|
||||
|
||||
const { rerender } = render(<CopyJobActionMenu job={job1} handleClick={mockHandleClick} />);
|
||||
|
||||
let actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
expect(actionButton).toBeInTheDocument();
|
||||
|
||||
rerender(<CopyJobActionMenu job={job2} handleClick={mockHandleClick} />);
|
||||
|
||||
actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
expect(screen.getByText("Resume")).toBeInTheDocument();
|
||||
expect(screen.getByText("Cancel")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Pause")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Performance and Memory", () => {
|
||||
it("should not create memory leaks with multiple renders", () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.InProgress });
|
||||
const { unmount } = render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
expect(() => unmount()).not.toThrow();
|
||||
});
|
||||
|
||||
it("should handle null/undefined props gracefully", () => {
|
||||
const incompleteJob = {
|
||||
...createMockJob({ Status: CopyJobStatusType.InProgress }),
|
||||
Name: undefined as any,
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
render(<CopyJobActionMenu job={incompleteJob} handleClick={mockHandleClick} />);
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,449 @@
|
||||
import { IColumn } from "@fluentui/react";
|
||||
import "@testing-library/jest-dom";
|
||||
import { render } from "@testing-library/react";
|
||||
import React from "react";
|
||||
import ContainerCopyMessages from "../../ContainerCopyMessages";
|
||||
import { CopyJobStatusType } from "../../Enums/CopyJobEnums";
|
||||
import { CopyJobType, HandleJobActionClickType } from "../../Types/CopyJobTypes";
|
||||
import { getColumns } from "./CopyJobColumns";
|
||||
|
||||
jest.mock("./CopyJobActionMenu", () => {
|
||||
const MockCopyJobActionMenu = ({ job }: { job: CopyJobType }) => {
|
||||
return <div data-testid={`action-menu-${job.Name}`}>Action Menu</div>;
|
||||
};
|
||||
MockCopyJobActionMenu.displayName = "MockCopyJobActionMenu";
|
||||
return MockCopyJobActionMenu;
|
||||
});
|
||||
|
||||
jest.mock("./CopyJobStatusWithIcon", () => {
|
||||
const MockCopyJobStatusWithIcon = ({ status }: { status: CopyJobStatusType }) => {
|
||||
return <div data-testid={`status-icon-${status}`}>Status: {status}</div>;
|
||||
};
|
||||
MockCopyJobStatusWithIcon.displayName = "MockCopyJobStatusWithIcon";
|
||||
return MockCopyJobStatusWithIcon;
|
||||
});
|
||||
|
||||
describe("CopyJobColumns", () => {
|
||||
type OnColumnClickType = IColumn & { onColumnClick: () => void };
|
||||
const mockHandleSort = jest.fn();
|
||||
const mockHandleActionClick: HandleJobActionClickType = jest.fn();
|
||||
|
||||
const mockJob = {
|
||||
ID: "test-job-id",
|
||||
Mode: "Online",
|
||||
Name: "Test Job Name",
|
||||
Status: CopyJobStatusType.InProgress,
|
||||
CompletionPercentage: 75,
|
||||
Duration: "00:05:30",
|
||||
LastUpdatedTime: "2024-12-01T10:30:00Z",
|
||||
timestamp: 1701426600000,
|
||||
Source: {
|
||||
databaseName: "test-source-db",
|
||||
containerName: "test-source-container",
|
||||
component: "CosmosDBSql",
|
||||
},
|
||||
Destination: {
|
||||
databaseName: "test-dest-db",
|
||||
containerName: "test-dest-container",
|
||||
component: "CosmosDBSql",
|
||||
},
|
||||
} as CopyJobType;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("getColumns", () => {
|
||||
it("should return an array of IColumn objects", () => {
|
||||
const columns = getColumns(mockHandleSort, mockHandleActionClick, undefined, false);
|
||||
|
||||
expect(columns).toBeDefined();
|
||||
expect(Array.isArray(columns)).toBe(true);
|
||||
expect(columns.length).toBe(6);
|
||||
|
||||
columns.forEach((column: IColumn) => {
|
||||
expect(column).toHaveProperty("key");
|
||||
expect(column).toHaveProperty("name");
|
||||
expect(column).toHaveProperty("minWidth");
|
||||
expect(column).toHaveProperty("maxWidth");
|
||||
expect(column).toHaveProperty("isResizable");
|
||||
});
|
||||
});
|
||||
|
||||
it("should have correct column keys", () => {
|
||||
const columns = getColumns(mockHandleSort, mockHandleActionClick, undefined, false);
|
||||
|
||||
const expectedKeys = ["LastUpdatedTime", "Name", "Mode", "CompletionPercentage", "CopyJobStatus", "Actions"];
|
||||
const actualKeys = columns.map((column) => column.key);
|
||||
|
||||
expect(actualKeys).toEqual(expectedKeys);
|
||||
});
|
||||
|
||||
it("should have correct column names from ContainerCopyMessages", () => {
|
||||
const columns = getColumns(mockHandleSort, mockHandleActionClick, undefined, false);
|
||||
|
||||
expect(columns[0].name).toBe(ContainerCopyMessages.MonitorJobs.Columns.lastUpdatedTime);
|
||||
expect(columns[1].name).toBe(ContainerCopyMessages.MonitorJobs.Columns.name);
|
||||
expect(columns[2].name).toBe(ContainerCopyMessages.MonitorJobs.Columns.mode);
|
||||
expect(columns[3].name).toBe(ContainerCopyMessages.MonitorJobs.Columns.completionPercentage);
|
||||
expect(columns[4].name).toBe(ContainerCopyMessages.MonitorJobs.Columns.status);
|
||||
expect(columns[5].name).toBe("");
|
||||
});
|
||||
|
||||
it("should configure sortable columns correctly when no sort is applied", () => {
|
||||
const columns = getColumns(mockHandleSort, mockHandleActionClick, undefined, false);
|
||||
|
||||
expect(columns[0].isSorted).toBe(false); // LastUpdatedTime
|
||||
expect(columns[1].isSorted).toBe(false); // Name
|
||||
expect(columns[2].isSorted).toBe(false); // Mode
|
||||
expect(columns[3].isSorted).toBe(false); // CompletionPercentage
|
||||
expect(columns[4].isSorted).toBe(false); // CopyJobStatus
|
||||
});
|
||||
|
||||
it("should configure sorted column correctly when sort is applied", () => {
|
||||
const columns = getColumns(mockHandleSort, mockHandleActionClick, "Name", true);
|
||||
|
||||
expect(columns[1].isSorted).toBe(true);
|
||||
expect(columns[1].isSortedDescending).toBe(true);
|
||||
|
||||
expect(columns[0].isSorted).toBe(false);
|
||||
expect(columns[2].isSorted).toBe(false);
|
||||
expect(columns[3].isSorted).toBe(false);
|
||||
expect(columns[4].isSorted).toBe(false);
|
||||
});
|
||||
|
||||
it("should handle timestamp sorting for LastUpdatedTime column", () => {
|
||||
const columns = getColumns(mockHandleSort, mockHandleActionClick, "timestamp", false);
|
||||
|
||||
expect(columns[0].isSorted).toBe(true);
|
||||
expect(columns[0].isSortedDescending).toBe(false);
|
||||
});
|
||||
|
||||
it("should call handleSort with correct column keys when column headers are clicked", () => {
|
||||
const columns = getColumns(mockHandleSort, mockHandleActionClick, undefined, false);
|
||||
|
||||
(columns[0] as OnColumnClickType).onColumnClick?.();
|
||||
expect(mockHandleSort).toHaveBeenCalledWith("timestamp");
|
||||
|
||||
(columns[1] as OnColumnClickType).onColumnClick();
|
||||
expect(mockHandleSort).toHaveBeenCalledWith("Name");
|
||||
|
||||
(columns[2] as OnColumnClickType).onColumnClick();
|
||||
expect(mockHandleSort).toHaveBeenCalledWith("Mode");
|
||||
|
||||
(columns[3] as OnColumnClickType).onColumnClick();
|
||||
expect(mockHandleSort).toHaveBeenCalledWith("CompletionPercentage");
|
||||
|
||||
(columns[4] as OnColumnClickType).onColumnClick();
|
||||
expect(mockHandleSort).toHaveBeenCalledWith("Status");
|
||||
|
||||
expect(mockHandleSort).toHaveBeenCalledTimes(5);
|
||||
});
|
||||
|
||||
it("should have correct column widths and resizability", () => {
|
||||
const columns = getColumns(mockHandleSort, mockHandleActionClick, undefined, false);
|
||||
|
||||
expect(columns[0].minWidth).toBe(140); // LastUpdatedTime
|
||||
expect(columns[0].maxWidth).toBe(300);
|
||||
expect(columns[0].isResizable).toBe(true);
|
||||
|
||||
expect(columns[1].minWidth).toBe(140); // Name
|
||||
expect(columns[1].maxWidth).toBe(300);
|
||||
expect(columns[1].isResizable).toBe(true);
|
||||
|
||||
expect(columns[2].minWidth).toBe(90); // Mode
|
||||
expect(columns[2].maxWidth).toBe(200);
|
||||
expect(columns[2].isResizable).toBe(true);
|
||||
|
||||
expect(columns[3].minWidth).toBe(110); // CompletionPercentage
|
||||
expect(columns[3].maxWidth).toBe(200);
|
||||
expect(columns[3].isResizable).toBe(true);
|
||||
|
||||
expect(columns[4].minWidth).toBe(130); // CopyJobStatus
|
||||
expect(columns[4].maxWidth).toBe(200);
|
||||
expect(columns[4].isResizable).toBe(true);
|
||||
|
||||
expect(columns[5].minWidth).toBe(80); // Actions
|
||||
expect(columns[5].maxWidth).toBe(200);
|
||||
expect(columns[5].isResizable).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Column Render Functions", () => {
|
||||
let columns: IColumn[];
|
||||
|
||||
beforeEach(() => {
|
||||
columns = getColumns(mockHandleSort, mockHandleActionClick, undefined, false);
|
||||
});
|
||||
|
||||
describe("Name column render function", () => {
|
||||
it("should render job name with correct styling", () => {
|
||||
const nameColumn = columns.find((col) => col.key === "Name");
|
||||
expect(nameColumn?.onRender).toBeDefined();
|
||||
|
||||
const rendered = nameColumn?.onRender?.(mockJob);
|
||||
const { container } = render(<div>{rendered}</div>);
|
||||
|
||||
const jobNameElement = container.querySelector(".jobNameLink");
|
||||
expect(jobNameElement).toBeInTheDocument();
|
||||
expect(jobNameElement).toHaveTextContent("Test Job Name");
|
||||
});
|
||||
|
||||
it("should handle empty job name", () => {
|
||||
const nameColumn = columns.find((col) => col.key === "Name");
|
||||
const jobWithEmptyName = { ...mockJob, Name: "" };
|
||||
|
||||
const rendered = nameColumn?.onRender?.(jobWithEmptyName);
|
||||
const { container } = render(<div>{rendered}</div>);
|
||||
|
||||
const jobNameElement = container.querySelector(".jobNameLink");
|
||||
expect(jobNameElement).toBeInTheDocument();
|
||||
expect(jobNameElement).toHaveTextContent("");
|
||||
});
|
||||
|
||||
it("should handle special characters in job name", () => {
|
||||
const nameColumn = columns.find((col) => col.key === "Name");
|
||||
const jobWithSpecialName = { ...mockJob, Name: "Test & <Job> 'Name' \"With\" Special Characters" };
|
||||
|
||||
const rendered = nameColumn?.onRender?.(jobWithSpecialName);
|
||||
const { container } = render(<div>{rendered}</div>);
|
||||
|
||||
const jobNameElement = container.querySelector(".jobNameLink");
|
||||
expect(jobNameElement).toBeInTheDocument();
|
||||
expect(jobNameElement).toHaveTextContent("Test & <Job> 'Name' \"With\" Special Characters");
|
||||
});
|
||||
});
|
||||
|
||||
describe("CompletionPercentage column render function", () => {
|
||||
it("should render completion percentage with % symbol", () => {
|
||||
const completionColumn = columns.find((col) => col.key === "CompletionPercentage");
|
||||
expect(completionColumn?.onRender).toBeDefined();
|
||||
|
||||
const result = completionColumn?.onRender?.(mockJob);
|
||||
expect(result).toBe("75%");
|
||||
});
|
||||
|
||||
it("should handle 0% completion", () => {
|
||||
const completionColumn = columns.find((col) => col.key === "CompletionPercentage");
|
||||
const jobWithZeroCompletion = { ...mockJob, CompletionPercentage: 0 };
|
||||
|
||||
const result = completionColumn?.onRender?.(jobWithZeroCompletion);
|
||||
expect(result).toBe("0%");
|
||||
});
|
||||
|
||||
it("should handle 100% completion", () => {
|
||||
const completionColumn = columns.find((col) => col.key === "CompletionPercentage");
|
||||
const jobWithFullCompletion = { ...mockJob, CompletionPercentage: 100 };
|
||||
|
||||
const result = completionColumn?.onRender?.(jobWithFullCompletion);
|
||||
expect(result).toBe("100%");
|
||||
});
|
||||
|
||||
it("should handle decimal completion percentages", () => {
|
||||
const completionColumn = columns.find((col) => col.key === "CompletionPercentage");
|
||||
const jobWithDecimalCompletion = { ...mockJob, CompletionPercentage: 75.5 };
|
||||
|
||||
const result = completionColumn?.onRender?.(jobWithDecimalCompletion);
|
||||
expect(result).toBe("75.5%");
|
||||
});
|
||||
|
||||
it("should handle negative completion percentages", () => {
|
||||
const completionColumn = columns.find((col) => col.key === "CompletionPercentage");
|
||||
const jobWithNegativeCompletion = { ...mockJob, CompletionPercentage: -5 };
|
||||
|
||||
const result = completionColumn?.onRender?.(jobWithNegativeCompletion);
|
||||
expect(result).toBe("-5%");
|
||||
});
|
||||
});
|
||||
|
||||
describe("CopyJobStatus column render function", () => {
|
||||
it("should render CopyJobStatusWithIcon component", () => {
|
||||
const statusColumn = columns.find((col) => col.key === "CopyJobStatus");
|
||||
expect(statusColumn?.onRender).toBeDefined();
|
||||
|
||||
const rendered = statusColumn?.onRender?.(mockJob);
|
||||
const { container } = render(<div>{rendered}</div>);
|
||||
|
||||
const statusIcon = container.querySelector(`[data-testid="status-icon-${mockJob.Status}"]`);
|
||||
expect(statusIcon).toBeInTheDocument();
|
||||
expect(statusIcon).toHaveTextContent(`Status: ${mockJob.Status}`);
|
||||
});
|
||||
|
||||
it("should handle different job statuses", () => {
|
||||
const statusColumn = columns.find((col) => col.key === "CopyJobStatus");
|
||||
|
||||
Object.values(CopyJobStatusType).forEach((status) => {
|
||||
const jobWithStatus = { ...mockJob, Status: status };
|
||||
const rendered = statusColumn?.onRender?.(jobWithStatus);
|
||||
const { container } = render(<div>{rendered}</div>);
|
||||
|
||||
const statusIcon = container.querySelector(`[data-testid="status-icon-${status}"]`);
|
||||
expect(statusIcon).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Actions column render function", () => {
|
||||
it("should render CopyJobActionMenu component", () => {
|
||||
const actionsColumn = columns.find((col) => col.key === "Actions");
|
||||
expect(actionsColumn?.onRender).toBeDefined();
|
||||
|
||||
const rendered = actionsColumn?.onRender?.(mockJob);
|
||||
const { container } = render(<div>{rendered}</div>);
|
||||
|
||||
const actionMenu = container.querySelector(`[data-testid="action-menu-${mockJob.Name}"]`);
|
||||
expect(actionMenu).toBeInTheDocument();
|
||||
expect(actionMenu).toHaveTextContent("Action Menu");
|
||||
});
|
||||
|
||||
it("should pass correct props to CopyJobActionMenu", () => {
|
||||
const actionsColumn = columns.find((col) => col.key === "Actions");
|
||||
const rendered = actionsColumn?.onRender?.(mockJob);
|
||||
|
||||
expect(rendered).toBeDefined();
|
||||
expect(React.isValidElement(rendered)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Column Field Names", () => {
|
||||
it("should have correct fieldName properties", () => {
|
||||
const columns = getColumns(mockHandleSort, mockHandleActionClick, undefined, false);
|
||||
|
||||
expect(columns[0].fieldName).toBe("LastUpdatedTime");
|
||||
expect(columns[1].fieldName).toBe("Name");
|
||||
expect(columns[2].fieldName).toBe("Mode");
|
||||
expect(columns[3].fieldName).toBe("CompletionPercentage");
|
||||
expect(columns[4].fieldName).toBe("Status");
|
||||
expect(columns[5].fieldName).toBeUndefined(); // Actions column doesn't have fieldName
|
||||
});
|
||||
});
|
||||
|
||||
describe("Different Sort Configurations", () => {
|
||||
it("should handle ascending sort", () => {
|
||||
const columns = getColumns(mockHandleSort, mockHandleActionClick, "Name", false);
|
||||
|
||||
const nameColumn = columns.find((col) => col.key === "Name");
|
||||
expect(nameColumn?.isSorted).toBe(true);
|
||||
expect(nameColumn?.isSortedDescending).toBe(false);
|
||||
});
|
||||
|
||||
it("should handle descending sort", () => {
|
||||
const columns = getColumns(mockHandleSort, mockHandleActionClick, "Mode", true);
|
||||
|
||||
const modeColumn = columns.find((col) => col.key === "Mode");
|
||||
expect(modeColumn?.isSorted).toBe(true);
|
||||
expect(modeColumn?.isSortedDescending).toBe(true);
|
||||
});
|
||||
|
||||
it("should handle sort on CompletionPercentage column", () => {
|
||||
const columns = getColumns(mockHandleSort, mockHandleActionClick, "CompletionPercentage", false);
|
||||
|
||||
const completionColumn = columns.find((col) => col.key === "CompletionPercentage");
|
||||
expect(completionColumn?.isSorted).toBe(true);
|
||||
expect(completionColumn?.isSortedDescending).toBe(false);
|
||||
|
||||
const nameColumn = columns.find((col) => col.key === "Name");
|
||||
expect(nameColumn?.isSorted).toBe(false);
|
||||
});
|
||||
|
||||
it("should handle sort on Status column", () => {
|
||||
const columns = getColumns(mockHandleSort, mockHandleActionClick, "Status", true);
|
||||
|
||||
const statusColumn = columns.find((col) => col.key === "CopyJobStatus");
|
||||
expect(statusColumn?.isSorted).toBe(true);
|
||||
expect(statusColumn?.isSortedDescending).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Edge Cases", () => {
|
||||
it("should handle undefined sortedColumnKey", () => {
|
||||
const columns = getColumns(mockHandleSort, mockHandleActionClick, undefined, false);
|
||||
|
||||
const sortableColumns = columns.filter((col) => col.key !== "Actions");
|
||||
sortableColumns.forEach((column) => {
|
||||
expect(column.isSorted).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle null job object in render functions gracefully", () => {
|
||||
const columns = getColumns(mockHandleSort, mockHandleActionClick, undefined, false);
|
||||
|
||||
const nameColumn = columns.find((col) => col.key === "Name");
|
||||
expect(() => {
|
||||
nameColumn?.onRender?.(null as any);
|
||||
}).toThrow();
|
||||
|
||||
const completionColumn = columns.find((col) => col.key === "CompletionPercentage");
|
||||
expect(() => {
|
||||
completionColumn?.onRender?.(null as any);
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it("should handle job object with missing properties", () => {
|
||||
const incompleteJob = {
|
||||
Name: "Incomplete Job",
|
||||
} as CopyJobType;
|
||||
|
||||
const columns = getColumns(mockHandleSort, mockHandleActionClick, undefined, false);
|
||||
|
||||
const nameColumn = columns.find((col) => col.key === "Name");
|
||||
const rendered = nameColumn?.onRender?.(incompleteJob);
|
||||
const { container } = render(<div>{rendered}</div>);
|
||||
|
||||
const jobNameElement = container.querySelector(".jobNameLink");
|
||||
expect(jobNameElement).toHaveTextContent("Incomplete Job");
|
||||
});
|
||||
|
||||
it("should handle unknown sortedColumnKey", () => {
|
||||
const columns = getColumns(mockHandleSort, mockHandleActionClick, "UnknownColumn", false);
|
||||
|
||||
const sortableColumns = columns.filter((col) => col.key !== "Actions");
|
||||
sortableColumns.forEach((column) => {
|
||||
expect(column.isSorted).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Accessibility", () => {
|
||||
it("should have Actions column without name for accessibility", () => {
|
||||
const columns = getColumns(mockHandleSort, mockHandleActionClick, undefined, false);
|
||||
|
||||
const actionsColumn = columns.find((col) => col.key === "Actions");
|
||||
expect(actionsColumn?.name).toBe("");
|
||||
});
|
||||
|
||||
it("should maintain column structure for screen readers", () => {
|
||||
const columns = getColumns(mockHandleSort, mockHandleActionClick, undefined, false);
|
||||
|
||||
const columnsWithNames = columns.filter((col) => col.key !== "Actions");
|
||||
columnsWithNames.forEach((column) => {
|
||||
expect(column.name).toBeTruthy();
|
||||
expect(typeof column.name).toBe("string");
|
||||
expect(column.name.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Function References", () => {
|
||||
it("should maintain function reference stability", () => {
|
||||
const columns1 = getColumns(mockHandleSort, mockHandleActionClick, undefined, false);
|
||||
const columns2 = getColumns(mockHandleSort, mockHandleActionClick, undefined, false);
|
||||
|
||||
(columns1[0] as OnColumnClickType).onColumnClick?.();
|
||||
(columns2[0] as OnColumnClickType).onColumnClick?.();
|
||||
|
||||
expect(mockHandleSort).toHaveBeenCalledTimes(2);
|
||||
expect(mockHandleSort).toHaveBeenNthCalledWith(1, "timestamp");
|
||||
expect(mockHandleSort).toHaveBeenNthCalledWith(2, "timestamp");
|
||||
});
|
||||
|
||||
it("should call handleActionClick when action menu is rendered", () => {
|
||||
const columns = getColumns(mockHandleSort, mockHandleActionClick, undefined, false);
|
||||
const actionsColumn = columns.find((col) => col.key === "Actions");
|
||||
|
||||
const rendered = actionsColumn?.onRender?.(mockJob);
|
||||
expect(React.isValidElement(rendered)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,383 @@
|
||||
import "@testing-library/jest-dom";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import React from "react";
|
||||
import { CopyJobStatusType } from "../../Enums/CopyJobEnums";
|
||||
import { CopyJobType } from "../../Types/CopyJobTypes";
|
||||
import CopyJobDetails from "./CopyJobDetails";
|
||||
|
||||
jest.mock("./CopyJobStatusWithIcon", () => {
|
||||
const MockCopyJobStatusWithIcon = ({ status }: { status: CopyJobStatusType }) => {
|
||||
return <span data-testid="copy-job-status-icon">{status}</span>;
|
||||
};
|
||||
MockCopyJobStatusWithIcon.displayName = "MockCopyJobStatusWithIcon";
|
||||
return MockCopyJobStatusWithIcon;
|
||||
});
|
||||
|
||||
jest.mock("../../ContainerCopyMessages", () => ({
|
||||
errorTitle: "Error Details",
|
||||
sourceDatabaseLabel: "Source Database",
|
||||
sourceContainerLabel: "Source Container",
|
||||
targetDatabaseLabel: "Destination Database",
|
||||
targetContainerLabel: "Destination Container",
|
||||
sourceAccountLabel: "Source Account",
|
||||
MonitorJobs: {
|
||||
Columns: {
|
||||
lastUpdatedTime: "Date & time",
|
||||
status: "Status",
|
||||
mode: "Mode",
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe("CopyJobDetails", () => {
|
||||
const mockBasicJob: CopyJobType = {
|
||||
ID: "test-job-1",
|
||||
Mode: "Offline",
|
||||
Name: "test-job-1",
|
||||
Status: CopyJobStatusType.InProgress,
|
||||
CompletionPercentage: 50,
|
||||
Duration: "10 minutes",
|
||||
LastUpdatedTime: "2024-01-01T10:00:00Z",
|
||||
timestamp: 1704110400000,
|
||||
Source: {
|
||||
component: "CosmosDBSql",
|
||||
databaseName: "sourceDb",
|
||||
containerName: "sourceContainer",
|
||||
remoteAccountName: "sourceAccount",
|
||||
},
|
||||
Destination: {
|
||||
component: "CosmosDBSql",
|
||||
databaseName: "targetDb",
|
||||
containerName: "targetContainer",
|
||||
remoteAccountName: "targetAccount",
|
||||
},
|
||||
};
|
||||
|
||||
const mockJobWithError: CopyJobType = {
|
||||
...mockBasicJob,
|
||||
ID: "test-job-error",
|
||||
Status: CopyJobStatusType.Failed,
|
||||
Error: {
|
||||
message: "Failed to connect to source database",
|
||||
code: "CONNECTION_ERROR",
|
||||
},
|
||||
};
|
||||
|
||||
const mockJobWithNullValues: CopyJobType = {
|
||||
...mockBasicJob,
|
||||
ID: "test-job-null",
|
||||
Source: {
|
||||
component: "CosmosDBSql",
|
||||
databaseName: undefined,
|
||||
containerName: undefined,
|
||||
remoteAccountName: undefined,
|
||||
},
|
||||
Destination: {
|
||||
component: "CosmosDBSql",
|
||||
databaseName: undefined,
|
||||
containerName: undefined,
|
||||
remoteAccountName: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
describe("Basic Rendering", () => {
|
||||
it("renders the component with correct structure", () => {
|
||||
render(<CopyJobDetails job={mockBasicJob} />);
|
||||
|
||||
const container = screen.getByTestId("copy-job-details");
|
||||
expect(container).toBeInTheDocument();
|
||||
expect(container).toHaveClass("copyJobDetailsContainer");
|
||||
});
|
||||
|
||||
it("displays job details without error when no error exists", () => {
|
||||
render(<CopyJobDetails job={mockBasicJob} />);
|
||||
|
||||
expect(screen.queryByTestId("error-stack")).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId("selectedcollection-stack")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders all required job information fields", () => {
|
||||
render(<CopyJobDetails job={mockBasicJob} />);
|
||||
|
||||
expect(screen.getByText("Date & time")).toBeInTheDocument();
|
||||
expect(screen.getByText("2024-01-01T10:00:00Z")).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText("Source Account")).toBeInTheDocument();
|
||||
expect(screen.getByText("sourceAccount")).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText("Mode")).toBeInTheDocument();
|
||||
expect(screen.getByText("Offline")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders the DetailsList with correct job data", () => {
|
||||
render(<CopyJobDetails job={mockBasicJob} />);
|
||||
|
||||
expect(screen.getByText("sourceDb")).toBeInTheDocument();
|
||||
expect(screen.getByText("sourceContainer")).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText("targetDb")).toBeInTheDocument();
|
||||
expect(screen.getByText("targetContainer")).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByTestId("copy-job-status-icon")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("copy-job-status-icon")).toHaveTextContent("InProgress");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Error Handling", () => {
|
||||
it("displays error section when job has error", () => {
|
||||
render(<CopyJobDetails job={mockJobWithError} />);
|
||||
|
||||
const errorStack = screen.getByTestId("error-stack");
|
||||
expect(errorStack).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText("Error Details")).toBeInTheDocument();
|
||||
expect(screen.getByText("Failed to connect to source database")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not display error section when job has no error", () => {
|
||||
render(<CopyJobDetails job={mockBasicJob} />);
|
||||
|
||||
expect(screen.queryByTestId("error-stack")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Error Details")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Null/Undefined Value Handling", () => {
|
||||
it("displays 'N/A' for null or undefined source values", () => {
|
||||
render(<CopyJobDetails job={mockJobWithNullValues} />);
|
||||
|
||||
const nATexts = screen.getAllByText("N/A");
|
||||
expect(nATexts).toHaveLength(4);
|
||||
});
|
||||
|
||||
it("handles null remote account name gracefully", () => {
|
||||
render(<CopyJobDetails job={mockJobWithNullValues} />);
|
||||
expect(screen.getByTestId("copy-job-details")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("handles empty status gracefully", () => {
|
||||
const jobWithEmptyStatus: CopyJobType = {
|
||||
...mockBasicJob,
|
||||
Status: "" as CopyJobStatusType,
|
||||
};
|
||||
|
||||
render(<CopyJobDetails job={jobWithEmptyStatus} />);
|
||||
|
||||
expect(screen.getByTestId("copy-job-status-icon")).toHaveTextContent("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Different Job Statuses", () => {
|
||||
const statusTestCases = [
|
||||
CopyJobStatusType.Pending,
|
||||
CopyJobStatusType.Running,
|
||||
CopyJobStatusType.Paused,
|
||||
CopyJobStatusType.Completed,
|
||||
CopyJobStatusType.Failed,
|
||||
CopyJobStatusType.Cancelled,
|
||||
];
|
||||
|
||||
statusTestCases.forEach((status) => {
|
||||
it(`renders correctly for ${status} status`, () => {
|
||||
const jobWithStatus: CopyJobType = {
|
||||
...mockBasicJob,
|
||||
Status: status,
|
||||
};
|
||||
|
||||
render(<CopyJobDetails job={jobWithStatus} />);
|
||||
|
||||
expect(screen.getByTestId("copy-job-status-icon")).toHaveTextContent(status);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Component Memoization", () => {
|
||||
it("re-renders when job ID changes", () => {
|
||||
render(<CopyJobDetails job={mockBasicJob} />);
|
||||
|
||||
expect(screen.getByText(CopyJobStatusType.InProgress)).toBeInTheDocument();
|
||||
|
||||
const updatedJob: CopyJobType = {
|
||||
...mockBasicJob,
|
||||
Status: CopyJobStatusType.Completed,
|
||||
};
|
||||
|
||||
render(<CopyJobDetails job={updatedJob} />);
|
||||
|
||||
expect(screen.getByText(CopyJobStatusType.Completed)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("re-renders when error changes", () => {
|
||||
const { rerender } = render(<CopyJobDetails job={mockBasicJob} />);
|
||||
|
||||
expect(screen.queryByTestId("error-stack")).not.toBeInTheDocument();
|
||||
|
||||
rerender(<CopyJobDetails job={mockJobWithError} />);
|
||||
|
||||
expect(screen.getByTestId("error-stack")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not re-render when other props change but ID and Error stay same", () => {
|
||||
const jobWithSameIdAndError = {
|
||||
...mockBasicJob,
|
||||
Mode: "Online",
|
||||
CompletionPercentage: 75,
|
||||
};
|
||||
|
||||
const { rerender } = render(<CopyJobDetails job={mockBasicJob} />);
|
||||
rerender(<CopyJobDetails job={jobWithSameIdAndError} />);
|
||||
expect(screen.getByTestId("copy-job-details")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Data Transformation", () => {
|
||||
it("correctly transforms job data for DetailsList items", () => {
|
||||
render(<CopyJobDetails job={mockBasicJob} />);
|
||||
expect(screen.getByText("sourceContainer")).toBeInTheDocument();
|
||||
expect(screen.getByText("sourceDb")).toBeInTheDocument();
|
||||
expect(screen.getByText("targetContainer")).toBeInTheDocument();
|
||||
expect(screen.getByText("targetDb")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("copy-job-status-icon")).toHaveTextContent("InProgress");
|
||||
});
|
||||
|
||||
it("handles complex job data structure", () => {
|
||||
const complexJob: CopyJobType = {
|
||||
...mockBasicJob,
|
||||
Source: {
|
||||
component: "CosmosDBSql",
|
||||
databaseName: "complex-source-db-with-hyphens",
|
||||
containerName: "complex_source_container_with_underscores",
|
||||
remoteAccountName: "complex.source.account",
|
||||
},
|
||||
Destination: {
|
||||
component: "CosmosDBSql",
|
||||
databaseName: "complex-target-db-with-hyphens",
|
||||
containerName: "complex_target_container_with_underscores",
|
||||
remoteAccountName: "complex.target.account",
|
||||
},
|
||||
};
|
||||
|
||||
render(<CopyJobDetails job={complexJob} />);
|
||||
|
||||
expect(screen.getByText("complex-source-db-with-hyphens")).toBeInTheDocument();
|
||||
expect(screen.getByText("complex_source_container_with_underscores")).toBeInTheDocument();
|
||||
expect(screen.getByText("complex-target-db-with-hyphens")).toBeInTheDocument();
|
||||
expect(screen.getByText("complex_target_container_with_underscores")).toBeInTheDocument();
|
||||
expect(screen.getByText("complex.source.account")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("DetailsList Configuration", () => {
|
||||
it("configures DetailsList with correct layout mode", () => {
|
||||
render(<CopyJobDetails job={mockBasicJob} />);
|
||||
expect(screen.getByText("sourceContainer")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders all expected column data", () => {
|
||||
render(<CopyJobDetails job={mockBasicJob} />);
|
||||
expect(screen.getByText("sourceDb")).toBeInTheDocument();
|
||||
expect(screen.getByText("sourceContainer")).toBeInTheDocument();
|
||||
expect(screen.getByText("targetDb")).toBeInTheDocument();
|
||||
expect(screen.getByText("targetContainer")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("copy-job-status-icon")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Accessibility", () => {
|
||||
it("has proper data-testid attributes", () => {
|
||||
render(<CopyJobDetails job={mockJobWithError} />);
|
||||
|
||||
expect(screen.getByTestId("copy-job-details")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("error-stack")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("selectedcollection-stack")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders semantic HTML structure", () => {
|
||||
render(<CopyJobDetails job={mockBasicJob} />);
|
||||
|
||||
const container = screen.getByTestId("copy-job-details");
|
||||
expect(container).toBeInTheDocument();
|
||||
|
||||
const nestedStack = screen.getByTestId("selectedcollection-stack");
|
||||
expect(nestedStack).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("CSS and Styling", () => {
|
||||
it("applies correct CSS classes", () => {
|
||||
render(<CopyJobDetails job={mockBasicJob} />);
|
||||
|
||||
const container = screen.getByTestId("copy-job-details");
|
||||
expect(container).toHaveClass("copyJobDetailsContainer");
|
||||
});
|
||||
|
||||
it("applies correct styling to error text", () => {
|
||||
render(<CopyJobDetails job={mockJobWithError} />);
|
||||
|
||||
const errorText = screen.getByText("Failed to connect to source database");
|
||||
expect(errorText).toHaveStyle({ whiteSpace: "pre-wrap" });
|
||||
});
|
||||
|
||||
it("applies bold styling to heading texts", () => {
|
||||
render(<CopyJobDetails job={mockBasicJob} />);
|
||||
|
||||
const dateTimeHeading = screen.getByText("Date & time");
|
||||
const sourceAccountHeading = screen.getByText("Source Account");
|
||||
const modeHeading = screen.getByText("Mode");
|
||||
|
||||
expect(dateTimeHeading).toHaveClass("bold");
|
||||
expect(sourceAccountHeading).toHaveClass("bold");
|
||||
expect(modeHeading).toHaveClass("bold");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Edge Cases", () => {
|
||||
it("handles job with minimal required data", () => {
|
||||
const minimalJob = {
|
||||
ID: "minimal",
|
||||
Mode: "",
|
||||
Name: "",
|
||||
Status: CopyJobStatusType.Pending,
|
||||
CompletionPercentage: 0,
|
||||
Duration: "",
|
||||
LastUpdatedTime: "",
|
||||
timestamp: 0,
|
||||
Source: {
|
||||
component: "CosmosDBSql",
|
||||
},
|
||||
Destination: {
|
||||
component: "CosmosDBSql",
|
||||
},
|
||||
} as CopyJobType;
|
||||
|
||||
render(<CopyJobDetails job={minimalJob} />);
|
||||
|
||||
expect(screen.getByTestId("copy-job-details")).toBeInTheDocument();
|
||||
expect(screen.getAllByText("N/A")).toHaveLength(4);
|
||||
});
|
||||
|
||||
it("handles very long text values", () => {
|
||||
const longTextJob: CopyJobType = {
|
||||
...mockBasicJob,
|
||||
Source: {
|
||||
...mockBasicJob.Source,
|
||||
databaseName: "very-long-database-name-that-might-cause-layout-issues-in-the-ui-component",
|
||||
containerName: "very-long-container-name-that-might-cause-layout-issues-in-the-ui-component",
|
||||
remoteAccountName: "very-long-account-name-that-might-cause-layout-issues-in-the-ui-component",
|
||||
},
|
||||
Error: {
|
||||
message:
|
||||
"This is a very long error message that contains multiple sentences and might span several lines when displayed in the user interface. It should handle line breaks and maintain readability even with extensive content.",
|
||||
code: "LONG_ERROR",
|
||||
},
|
||||
};
|
||||
|
||||
render(<CopyJobDetails job={longTextJob} />);
|
||||
|
||||
expect(
|
||||
screen.getByText("very-long-database-name-that-might-cause-layout-issues-in-the-ui-component"),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText(/This is a very long error message/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,162 @@
|
||||
import "@testing-library/jest-dom";
|
||||
import { render } from "@testing-library/react";
|
||||
import React from "react";
|
||||
import { CopyJobStatusType } from "../../Enums/CopyJobEnums";
|
||||
import CopyJobStatusWithIcon from "./CopyJobStatusWithIcon";
|
||||
|
||||
jest.mock("@fluentui/react", () => ({
|
||||
...jest.requireActual("@fluentui/react"),
|
||||
getTheme: () => ({
|
||||
semanticColors: {
|
||||
bodySubtext: "#666666",
|
||||
errorIcon: "#d13438",
|
||||
successIcon: "#107c10",
|
||||
},
|
||||
palette: {
|
||||
themePrimary: "#0078d4",
|
||||
},
|
||||
}),
|
||||
mergeStyles: () => "mocked-styles",
|
||||
mergeStyleSets: (styleSet: any) => {
|
||||
const result: any = {};
|
||||
Object.keys(styleSet).forEach((key) => {
|
||||
result[key] = "mocked-style-" + key;
|
||||
});
|
||||
return result;
|
||||
},
|
||||
}));
|
||||
|
||||
describe("CopyJobStatusWithIcon", () => {
|
||||
describe("Static Icon Status Types - Snapshot Tests", () => {
|
||||
const staticIconStatuses = [
|
||||
CopyJobStatusType.Pending,
|
||||
CopyJobStatusType.Paused,
|
||||
CopyJobStatusType.Skipped,
|
||||
CopyJobStatusType.Cancelled,
|
||||
CopyJobStatusType.Failed,
|
||||
CopyJobStatusType.Faulted,
|
||||
CopyJobStatusType.Completed,
|
||||
];
|
||||
|
||||
test.each(staticIconStatuses)("renders %s status correctly", (status) => {
|
||||
const { container } = render(<CopyJobStatusWithIcon status={status} />);
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Spinner Status Types", () => {
|
||||
const spinnerStatuses = [CopyJobStatusType.Running, CopyJobStatusType.InProgress, CopyJobStatusType.Partitioning];
|
||||
|
||||
test.each(spinnerStatuses)("renders %s with spinner and expected text", (status) => {
|
||||
const { container } = render(<CopyJobStatusWithIcon status={status} />);
|
||||
|
||||
const spinner = container.querySelector('[class*="ms-Spinner"]');
|
||||
expect(spinner).toBeInTheDocument();
|
||||
expect(container).toHaveTextContent("In Progress");
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("PropTypes Validation", () => {
|
||||
it("has correct display name", () => {
|
||||
expect(CopyJobStatusWithIcon.displayName).toBe("CopyJobStatusWithIcon");
|
||||
});
|
||||
it("accepts all valid CopyJobStatusType values", () => {
|
||||
const allStatuses = Object.values(CopyJobStatusType);
|
||||
|
||||
allStatuses.forEach((status) => {
|
||||
expect(() => {
|
||||
render(<CopyJobStatusWithIcon status={status} />);
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Accessibility", () => {
|
||||
it("provides proper aria-label for icon elements", () => {
|
||||
const { container } = render(<CopyJobStatusWithIcon status={CopyJobStatusType.Failed} />);
|
||||
|
||||
const icon = container.querySelector('[class*="ms-Icon"]');
|
||||
expect(icon).toHaveAttribute("aria-label", CopyJobStatusType.Failed);
|
||||
});
|
||||
|
||||
it("provides meaningful text content for screen readers", () => {
|
||||
const { container } = render(<CopyJobStatusWithIcon status={CopyJobStatusType.InProgress} />);
|
||||
|
||||
expect(container).toHaveTextContent("In Progress");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Icon and Status Mapping", () => {
|
||||
it("renders correct status text based on mapping", () => {
|
||||
const statusMappings = [
|
||||
{ status: CopyJobStatusType.Pending, expectedText: "Pending" },
|
||||
{ status: CopyJobStatusType.Paused, expectedText: "Paused" },
|
||||
{ status: CopyJobStatusType.Failed, expectedText: "Failed" },
|
||||
{ status: CopyJobStatusType.Completed, expectedText: "Completed" },
|
||||
{ status: CopyJobStatusType.Running, expectedText: "In Progress" },
|
||||
];
|
||||
|
||||
statusMappings.forEach(({ status, expectedText }) => {
|
||||
const { container, unmount } = render(<CopyJobStatusWithIcon status={status} />);
|
||||
expect(container).toHaveTextContent(expectedText);
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
it("renders icons for static status types", () => {
|
||||
const staticStatuses = [
|
||||
CopyJobStatusType.Pending,
|
||||
CopyJobStatusType.Paused,
|
||||
CopyJobStatusType.Failed,
|
||||
CopyJobStatusType.Completed,
|
||||
];
|
||||
|
||||
staticStatuses.forEach((status) => {
|
||||
const { container, unmount } = render(<CopyJobStatusWithIcon status={status} />);
|
||||
const icon = container.querySelector('[class*="ms-Icon"]');
|
||||
const spinner = container.querySelector('[class*="ms-Spinner"]');
|
||||
|
||||
expect(icon).toBeInTheDocument();
|
||||
expect(spinner).not.toBeInTheDocument();
|
||||
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
it("renders spinners for progress status types", () => {
|
||||
const progressStatuses = [
|
||||
CopyJobStatusType.Running,
|
||||
CopyJobStatusType.InProgress,
|
||||
CopyJobStatusType.Partitioning,
|
||||
];
|
||||
|
||||
progressStatuses.forEach((status) => {
|
||||
const { container, unmount } = render(<CopyJobStatusWithIcon status={status} />);
|
||||
const icon = container.querySelector('[class*="ms-Icon"]');
|
||||
const spinner = container.querySelector('[class*="ms-Spinner"]');
|
||||
|
||||
expect(spinner).toBeInTheDocument();
|
||||
expect(icon).not.toBeInTheDocument();
|
||||
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Performance", () => {
|
||||
it("does not cause unnecessary re-renders with same props", () => {
|
||||
const renderSpy = jest.fn();
|
||||
const TestWrapper = ({ status }: { status: CopyJobStatusType }) => {
|
||||
renderSpy();
|
||||
return <CopyJobStatusWithIcon status={status} />;
|
||||
};
|
||||
|
||||
const { rerender } = render(<TestWrapper status={CopyJobStatusType.Pending} />);
|
||||
expect(renderSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
rerender(<TestWrapper status={CopyJobStatusType.Pending} />);
|
||||
expect(renderSpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
jest.mock("../../Actions/CopyJobActions");
|
||||
|
||||
import "@testing-library/jest-dom";
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import React from "react";
|
||||
import * as Actions from "../../Actions/CopyJobActions";
|
||||
import ContainerCopyMessages from "../../ContainerCopyMessages";
|
||||
import CopyJobsNotFound from "./CopyJobs.NotFound";
|
||||
|
||||
describe("CopyJobsNotFound", () => {
|
||||
let mockExplorer: Explorer;
|
||||
|
||||
beforeEach(() => {
|
||||
mockExplorer = {} as Explorer;
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should render the component with correct elements", () => {
|
||||
const { container, getByText } = render(<CopyJobsNotFound explorer={mockExplorer} />);
|
||||
|
||||
const image = container.querySelector(".notFoundContainer .ms-Image");
|
||||
expect(image).toBeInTheDocument();
|
||||
expect(image).toHaveAttribute("style", "width: 100px; height: 100px;");
|
||||
expect(getByText(ContainerCopyMessages.noCopyJobsTitle)).toBeInTheDocument();
|
||||
|
||||
const button = screen.getByRole("button", {
|
||||
name: ContainerCopyMessages.createCopyJobButtonText,
|
||||
});
|
||||
expect(button).toBeInTheDocument();
|
||||
expect(button).toHaveClass("createCopyJobButton");
|
||||
});
|
||||
|
||||
it("should render with correct container classes", () => {
|
||||
const { container } = render(<CopyJobsNotFound explorer={mockExplorer} />);
|
||||
|
||||
const notFoundContainer = container.querySelector(".notFoundContainer");
|
||||
expect(notFoundContainer).toBeInTheDocument();
|
||||
expect(notFoundContainer).toHaveClass("flexContainer", "centerContent");
|
||||
});
|
||||
|
||||
it("should call openCreateCopyJobPanel when button is clicked", () => {
|
||||
const openCreateCopyJobPanelSpy = jest.spyOn(Actions, "openCreateCopyJobPanel");
|
||||
|
||||
render(<CopyJobsNotFound explorer={mockExplorer} />);
|
||||
|
||||
const button = screen.getByRole("button", {
|
||||
name: ContainerCopyMessages.createCopyJobButtonText,
|
||||
});
|
||||
|
||||
fireEvent.click(button);
|
||||
|
||||
expect(openCreateCopyJobPanelSpy).toHaveBeenCalledTimes(1);
|
||||
expect(openCreateCopyJobPanelSpy).toHaveBeenCalledWith(mockExplorer);
|
||||
});
|
||||
|
||||
it("should render ActionButton with correct props", () => {
|
||||
render(<CopyJobsNotFound explorer={mockExplorer} />);
|
||||
|
||||
const button = screen.getByRole("button", {
|
||||
name: ContainerCopyMessages.createCopyJobButtonText,
|
||||
});
|
||||
|
||||
expect(button).toBeInTheDocument();
|
||||
expect(button.textContent).toBe(ContainerCopyMessages.createCopyJobButtonText);
|
||||
});
|
||||
|
||||
it("should use memo to prevent unnecessary re-renders", () => {
|
||||
const { rerender } = render(<CopyJobsNotFound explorer={mockExplorer} />);
|
||||
rerender(<CopyJobsNotFound explorer={mockExplorer} />);
|
||||
expect(screen.getByRole("heading", { level: 4 })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -17,7 +17,7 @@ const CopyJobsNotFound: React.FC<CopyJobsNotFoundProps> = ({ explorer }) => {
|
||||
<ActionButton
|
||||
allowDisabledFocus
|
||||
className="createCopyJobButton"
|
||||
onClick={Actions.openCreateCopyJobPanel.bind(null, explorer)}
|
||||
onClick={() => Actions.openCreateCopyJobPanel(explorer)}
|
||||
>
|
||||
{ContainerCopyMessages.createCopyJobButtonText}
|
||||
</ActionButton>
|
||||
|
||||
@@ -0,0 +1,446 @@
|
||||
import "@testing-library/jest-dom";
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import React from "react";
|
||||
import { CopyJobStatusType } from "../../Enums/CopyJobEnums";
|
||||
import { CopyJobType, HandleJobActionClickType } from "../../Types/CopyJobTypes";
|
||||
import CopyJobsList from "./CopyJobsList";
|
||||
|
||||
jest.mock("../../Actions/CopyJobActions", () => ({
|
||||
openCopyJobDetailsPanel: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("./CopyJobColumns", () => ({
|
||||
getColumns: jest.fn(() => [
|
||||
{
|
||||
key: "Name",
|
||||
name: "Name",
|
||||
fieldName: "Name",
|
||||
minWidth: 140,
|
||||
maxWidth: 300,
|
||||
isResizable: true,
|
||||
onRender: (job: CopyJobType) => <span className="jobNameLink">{job.Name}</span>,
|
||||
},
|
||||
{
|
||||
key: "Status",
|
||||
name: "Status",
|
||||
fieldName: "Status",
|
||||
minWidth: 130,
|
||||
maxWidth: 200,
|
||||
isResizable: true,
|
||||
onRender: (job: CopyJobType) => <span>{job.Status}</span>,
|
||||
},
|
||||
{
|
||||
key: "CompletionPercentage",
|
||||
name: "Progress",
|
||||
fieldName: "CompletionPercentage",
|
||||
minWidth: 110,
|
||||
maxWidth: 200,
|
||||
isResizable: true,
|
||||
onRender: (job: CopyJobType) => <span>{job.CompletionPercentage}%</span>,
|
||||
},
|
||||
{
|
||||
key: "Actions",
|
||||
name: "Actions",
|
||||
minWidth: 80,
|
||||
maxWidth: 200,
|
||||
isResizable: true,
|
||||
onRender: (job: CopyJobType) => <button data-testid={`action-menu-${job.ID}`}>Actions</button>,
|
||||
},
|
||||
]),
|
||||
}));
|
||||
|
||||
// Sample test data
|
||||
const mockJobs: CopyJobType[] = [
|
||||
{
|
||||
ID: "job-1",
|
||||
Mode: "Live",
|
||||
Name: "Test Job 1",
|
||||
Status: CopyJobStatusType.Running,
|
||||
CompletionPercentage: 45,
|
||||
Duration: "00:05:30",
|
||||
LastUpdatedTime: "2025-01-01 10:00:00",
|
||||
timestamp: 1704110400000,
|
||||
Source: {
|
||||
component: "CosmosDBSql",
|
||||
remoteAccountName: "source-account",
|
||||
databaseName: "sourceDb",
|
||||
containerName: "sourceContainer",
|
||||
},
|
||||
Destination: {
|
||||
component: "CosmosDBSql",
|
||||
databaseName: "targetDb",
|
||||
containerName: "targetContainer",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "job-2",
|
||||
Mode: "Offline",
|
||||
Name: "Test Job 2",
|
||||
Status: CopyJobStatusType.Completed,
|
||||
CompletionPercentage: 100,
|
||||
Duration: "00:15:45",
|
||||
LastUpdatedTime: "2025-01-01 11:00:00",
|
||||
timestamp: 1704114000000,
|
||||
Source: {
|
||||
component: "CosmosDBSql",
|
||||
remoteAccountName: "source-account-2",
|
||||
databaseName: "sourceDb2",
|
||||
containerName: "sourceContainer2",
|
||||
},
|
||||
Destination: {
|
||||
component: "CosmosDBSql",
|
||||
databaseName: "targetDb2",
|
||||
containerName: "targetContainer2",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "job-3",
|
||||
Mode: "Live",
|
||||
Name: "Test Job 3",
|
||||
Status: CopyJobStatusType.Failed,
|
||||
CompletionPercentage: 25,
|
||||
Duration: "00:02:15",
|
||||
LastUpdatedTime: "2025-01-01 09:30:00",
|
||||
timestamp: 1704108600000,
|
||||
Error: {
|
||||
message: "Connection timeout",
|
||||
code: "TIMEOUT_ERROR",
|
||||
},
|
||||
Source: {
|
||||
component: "CosmosDBSql",
|
||||
remoteAccountName: "source-account-3",
|
||||
databaseName: "sourceDb3",
|
||||
containerName: "sourceContainer3",
|
||||
},
|
||||
Destination: {
|
||||
component: "CosmosDBSql",
|
||||
databaseName: "targetDb3",
|
||||
containerName: "targetContainer3",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const mockHandleActionClick: HandleJobActionClickType = jest.fn();
|
||||
|
||||
describe("CopyJobsList", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("Rendering", () => {
|
||||
it("renders empty list when no jobs provided", () => {
|
||||
render(<CopyJobsList jobs={[]} handleActionClick={mockHandleActionClick} />);
|
||||
|
||||
expect(screen.queryByText("Test Job 1")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders jobs list with provided jobs", () => {
|
||||
render(<CopyJobsList jobs={mockJobs} handleActionClick={mockHandleActionClick} />);
|
||||
|
||||
expect(screen.getByText("Test Job 1")).toBeInTheDocument();
|
||||
expect(screen.getByText("Test Job 2")).toBeInTheDocument();
|
||||
expect(screen.getByText("Test Job 3")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders job statuses correctly", () => {
|
||||
render(<CopyJobsList jobs={mockJobs} handleActionClick={mockHandleActionClick} />);
|
||||
|
||||
expect(screen.getByText(CopyJobStatusType.Running)).toBeInTheDocument();
|
||||
expect(screen.getByText(CopyJobStatusType.Completed)).toBeInTheDocument();
|
||||
expect(screen.getByText(CopyJobStatusType.Failed)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders completion percentages correctly", () => {
|
||||
render(<CopyJobsList jobs={mockJobs} handleActionClick={mockHandleActionClick} />);
|
||||
|
||||
expect(screen.getByText("45%")).toBeInTheDocument();
|
||||
expect(screen.getByText("100%")).toBeInTheDocument();
|
||||
expect(screen.getByText("25%")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders action menus for each job", () => {
|
||||
render(<CopyJobsList jobs={mockJobs} handleActionClick={mockHandleActionClick} />);
|
||||
|
||||
expect(screen.getByTestId("action-menu-job-1")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("action-menu-job-2")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("action-menu-job-3")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Pagination", () => {
|
||||
it("shows pager when jobs exceed page size", () => {
|
||||
const manyJobs: CopyJobType[] = Array.from({ length: 15 }, (_, i) => ({
|
||||
...mockJobs[0],
|
||||
ID: `job-${i + 1}`,
|
||||
Name: `Test Job ${i + 1}`,
|
||||
}));
|
||||
|
||||
render(<CopyJobsList jobs={manyJobs} handleActionClick={mockHandleActionClick} pageSize={10} />);
|
||||
|
||||
expect(screen.getByLabelText("Go to first page")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Go to previous page")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Go to next page")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Go to last page")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not show pager when jobs are within page size", () => {
|
||||
render(<CopyJobsList jobs={mockJobs} handleActionClick={mockHandleActionClick} pageSize={10} />);
|
||||
|
||||
expect(screen.queryByLabelText("Go to first page")).not.toBeInTheDocument();
|
||||
expect(screen.queryByLabelText("Go to previous page")).not.toBeInTheDocument();
|
||||
expect(screen.queryByLabelText("Go to next page")).not.toBeInTheDocument();
|
||||
expect(screen.queryByLabelText("Go to last page")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("displays correct page information", () => {
|
||||
const manyJobs: CopyJobType[] = Array.from({ length: 25 }, (_, i) => ({
|
||||
...mockJobs[0],
|
||||
ID: `job-${i + 1}`,
|
||||
Name: `Test Job ${i + 1}`,
|
||||
}));
|
||||
|
||||
render(<CopyJobsList jobs={manyJobs} handleActionClick={mockHandleActionClick} pageSize={10} />);
|
||||
|
||||
expect(screen.getByText("Showing 1 - 10 of 25 items")).toBeInTheDocument();
|
||||
expect(screen.getByText("Page 1 of 3")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("navigates to next page correctly", async () => {
|
||||
const manyJobs: CopyJobType[] = Array.from({ length: 15 }, (_, i) => ({
|
||||
...mockJobs[0],
|
||||
ID: `job-${i + 1}`,
|
||||
Name: `Test Job ${i + 1}`,
|
||||
}));
|
||||
|
||||
render(<CopyJobsList jobs={manyJobs} handleActionClick={mockHandleActionClick} pageSize={10} />);
|
||||
|
||||
expect(screen.getByText("Test Job 1")).toBeInTheDocument();
|
||||
expect(screen.getByText("Test Job 10")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Test Job 11")).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByLabelText("Go to next page"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText("Test Job 1")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("Test Job 11")).toBeInTheDocument();
|
||||
expect(screen.getByText("Test Job 15")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("uses custom page size when provided", () => {
|
||||
const manyJobs: CopyJobType[] = Array.from({ length: 8 }, (_, i) => ({
|
||||
...mockJobs[0],
|
||||
ID: `job-${i + 1}`,
|
||||
Name: `Test Job ${i + 1}`,
|
||||
}));
|
||||
|
||||
render(<CopyJobsList jobs={manyJobs} handleActionClick={mockHandleActionClick} pageSize={5} />);
|
||||
|
||||
expect(screen.getByLabelText("Go to next page")).toBeInTheDocument();
|
||||
expect(screen.getByText("Showing 1 - 5 of 8 items")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Sorting", () => {
|
||||
it("sorts jobs by name in ascending order", async () => {
|
||||
const unsortedJobs = [
|
||||
{ ...mockJobs[0], Name: "Z Job" },
|
||||
{ ...mockJobs[1], Name: "A Job" },
|
||||
{ ...mockJobs[2], Name: "M Job" },
|
||||
];
|
||||
|
||||
render(<CopyJobsList jobs={unsortedJobs} handleActionClick={mockHandleActionClick} />);
|
||||
|
||||
const rows = screen.getAllByText(/Job$/);
|
||||
expect(rows[0]).toHaveTextContent("Z Job");
|
||||
expect(rows[1]).toHaveTextContent("A Job");
|
||||
expect(rows[2]).toHaveTextContent("M Job");
|
||||
});
|
||||
|
||||
it("resets pagination to first page after sorting", async () => {
|
||||
const manyJobs: CopyJobType[] = Array.from({ length: 15 }, (_, i) => ({
|
||||
...mockJobs[0],
|
||||
ID: `job-${i + 1}`,
|
||||
Name: `Job ${String.fromCharCode(90 - i)}`,
|
||||
}));
|
||||
|
||||
render(<CopyJobsList jobs={manyJobs} handleActionClick={mockHandleActionClick} pageSize={10} />);
|
||||
|
||||
fireEvent.click(screen.getByLabelText("Go to next page"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Showing 11 - 15 of 15 items")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("updates jobs list when jobs prop changes", async () => {
|
||||
const { rerender } = render(<CopyJobsList jobs={[mockJobs[0]]} handleActionClick={mockHandleActionClick} />);
|
||||
|
||||
expect(screen.getByText("Test Job 1")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Test Job 2")).not.toBeInTheDocument();
|
||||
|
||||
rerender(<CopyJobsList jobs={mockJobs} handleActionClick={mockHandleActionClick} />);
|
||||
|
||||
expect(screen.getByText("Test Job 1")).toBeInTheDocument();
|
||||
expect(screen.getByText("Test Job 2")).toBeInTheDocument();
|
||||
expect(screen.getByText("Test Job 3")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("resets start index when jobs change", async () => {
|
||||
const manyJobs: CopyJobType[] = Array.from({ length: 15 }, (_, i) => ({
|
||||
...mockJobs[0],
|
||||
ID: `job-${i + 1}`,
|
||||
Name: `Test Job ${i + 1}`,
|
||||
}));
|
||||
|
||||
const { rerender } = render(
|
||||
<CopyJobsList jobs={manyJobs} handleActionClick={mockHandleActionClick} pageSize={10} />,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByLabelText("Go to next page"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Showing 11 - 15 of 15 items")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const newJobs = [mockJobs[0], mockJobs[1]];
|
||||
rerender(<CopyJobsList jobs={newJobs} handleActionClick={mockHandleActionClick} pageSize={10} />);
|
||||
|
||||
expect(screen.queryByLabelText("Go to next page")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Row Interactions", () => {
|
||||
it("calls openCopyJobDetailsPanel when row is clicked", async () => {
|
||||
const { openCopyJobDetailsPanel } = await import("../../Actions/CopyJobActions");
|
||||
|
||||
render(<CopyJobsList jobs={mockJobs} handleActionClick={mockHandleActionClick} />);
|
||||
|
||||
const jobNameElement = screen.getByText("Test Job 1");
|
||||
const rowElement = jobNameElement.closest('[role="row"]') || jobNameElement.closest("div");
|
||||
|
||||
if (rowElement) {
|
||||
fireEvent.click(rowElement);
|
||||
} else {
|
||||
fireEvent.click(jobNameElement);
|
||||
}
|
||||
|
||||
await waitFor(() => {
|
||||
expect(openCopyJobDetailsPanel).toHaveBeenCalledWith(mockJobs[0]);
|
||||
});
|
||||
});
|
||||
|
||||
it("applies cursor pointer style to rows", () => {
|
||||
render(<CopyJobsList jobs={mockJobs} handleActionClick={mockHandleActionClick} />);
|
||||
|
||||
const jobNameElement = screen.getByText("Test Job 1");
|
||||
const rowElement = jobNameElement.closest("div");
|
||||
|
||||
expect(rowElement).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Component Props", () => {
|
||||
it("uses default page size when not provided", () => {
|
||||
const manyJobs: CopyJobType[] = Array.from({ length: 12 }, (_, i) => ({
|
||||
...mockJobs[0],
|
||||
ID: `job-${i + 1}`,
|
||||
Name: `Test Job ${i + 1}`,
|
||||
}));
|
||||
|
||||
render(<CopyJobsList jobs={manyJobs} handleActionClick={mockHandleActionClick} />);
|
||||
|
||||
expect(screen.getByLabelText("Go to next page")).toBeInTheDocument();
|
||||
expect(screen.getByText("Showing 1 - 10 of 12 items")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("passes correct props to getColumns function", async () => {
|
||||
const { getColumns } = await import("./CopyJobColumns");
|
||||
|
||||
render(<CopyJobsList jobs={mockJobs} handleActionClick={mockHandleActionClick} />);
|
||||
|
||||
expect(getColumns).toHaveBeenCalledWith(
|
||||
expect.any(Function), // handleSort
|
||||
mockHandleActionClick, // handleActionClick
|
||||
undefined, // sortedColumnKey
|
||||
false, // isSortedDescending
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Accessibility", () => {
|
||||
it("renders with proper ARIA attributes", () => {
|
||||
render(<CopyJobsList jobs={mockJobs} handleActionClick={mockHandleActionClick} />);
|
||||
|
||||
const detailsList = screen.getByRole("grid");
|
||||
expect(detailsList).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("has accessible pager controls", () => {
|
||||
const manyJobs: CopyJobType[] = Array.from({ length: 15 }, (_, i) => ({
|
||||
...mockJobs[0],
|
||||
ID: `job-${i + 1}`,
|
||||
Name: `Test Job ${i + 1}`,
|
||||
}));
|
||||
|
||||
render(<CopyJobsList jobs={manyJobs} handleActionClick={mockHandleActionClick} pageSize={10} />);
|
||||
|
||||
expect(screen.getByLabelText("Go to first page")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Go to previous page")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Go to next page")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Go to last page")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Error Handling", () => {
|
||||
it("handles empty jobs array gracefully", () => {
|
||||
expect(() => {
|
||||
render(<CopyJobsList jobs={[]} handleActionClick={mockHandleActionClick} />);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("handles jobs with missing optional properties", () => {
|
||||
const incompleteJob: CopyJobType = {
|
||||
ID: "incomplete-job",
|
||||
Mode: "Live",
|
||||
Name: "Incomplete Job",
|
||||
Status: CopyJobStatusType.Running,
|
||||
CompletionPercentage: 0,
|
||||
Duration: "00:00:00",
|
||||
LastUpdatedTime: "2025-01-01 12:00:00",
|
||||
timestamp: 1704117600000,
|
||||
Source: {
|
||||
component: "CosmosDBSql",
|
||||
remoteAccountName: "source-account",
|
||||
databaseName: "sourceDb",
|
||||
containerName: "sourceContainer",
|
||||
},
|
||||
Destination: {
|
||||
component: "CosmosDBSql",
|
||||
databaseName: "targetDb",
|
||||
containerName: "targetContainer",
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
render(<CopyJobsList jobs={[incompleteJob]} handleActionClick={mockHandleActionClick} />);
|
||||
}).not.toThrow();
|
||||
|
||||
expect(screen.getByText("Incomplete Job")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("handles very large job lists", () => {
|
||||
const largeJobsList: CopyJobType[] = Array.from({ length: 1000 }, (_, i) => ({
|
||||
...mockJobs[0],
|
||||
ID: `job-${i + 1}`,
|
||||
Name: `Job ${i + 1}`,
|
||||
}));
|
||||
|
||||
expect(() => {
|
||||
render(<CopyJobsList jobs={largeJobsList} handleActionClick={mockHandleActionClick} />);
|
||||
}).not.toThrow();
|
||||
|
||||
expect(screen.getByText("Showing 1 - 10 of 1000 items")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,208 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CopyJobStatusWithIcon Spinner Status Types renders InProgress with spinner and expected text 1`] = `
|
||||
<div
|
||||
class="ms-Stack css-109"
|
||||
>
|
||||
<div
|
||||
class="ms-Spinner root-122"
|
||||
style="margin-right: 8px;"
|
||||
>
|
||||
<div
|
||||
class="ms-Spinner-circle ms-Spinner--small circle-123"
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
class="css-112"
|
||||
>
|
||||
In Progress
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`CopyJobStatusWithIcon Spinner Status Types renders Partitioning with spinner and expected text 1`] = `
|
||||
<div
|
||||
class="ms-Stack css-109"
|
||||
>
|
||||
<div
|
||||
class="ms-Spinner root-122"
|
||||
style="margin-right: 8px;"
|
||||
>
|
||||
<div
|
||||
class="ms-Spinner-circle ms-Spinner--small circle-123"
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
class="css-112"
|
||||
>
|
||||
In Progress
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`CopyJobStatusWithIcon Spinner Status Types renders Running with spinner and expected text 1`] = `
|
||||
<div
|
||||
class="ms-Stack css-109"
|
||||
>
|
||||
<div
|
||||
class="ms-Spinner root-122"
|
||||
style="margin-right: 8px;"
|
||||
>
|
||||
<div
|
||||
class="ms-Spinner-circle ms-Spinner--small circle-123"
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
class="css-112"
|
||||
>
|
||||
In Progress
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders Cancelled status correctly 1`] = `
|
||||
<div
|
||||
class="ms-Stack css-109"
|
||||
>
|
||||
<i
|
||||
aria-label="Cancelled"
|
||||
class="ms-Icon root-105 css-118 mocked-style-Cancelled"
|
||||
data-icon-name="StatusErrorFull"
|
||||
role="img"
|
||||
style="font-family: "FabricMDL2Icons-4";"
|
||||
>
|
||||
|
||||
</i>
|
||||
<span
|
||||
class="css-112"
|
||||
>
|
||||
Cancelled
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders Completed status correctly 1`] = `
|
||||
<div
|
||||
class="ms-Stack css-109"
|
||||
>
|
||||
<i
|
||||
aria-label="Completed"
|
||||
class="ms-Icon root-105 css-120 mocked-style-Completed"
|
||||
data-icon-name="CompletedSolid"
|
||||
role="img"
|
||||
style="font-family: "FabricMDL2Icons-5";"
|
||||
>
|
||||
|
||||
</i>
|
||||
<span
|
||||
class="css-112"
|
||||
>
|
||||
Completed
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders Failed status correctly 1`] = `
|
||||
<div
|
||||
class="ms-Stack css-109"
|
||||
>
|
||||
<i
|
||||
aria-label="Failed"
|
||||
class="ms-Icon root-105 css-118 mocked-style-Failed"
|
||||
data-icon-name="StatusErrorFull"
|
||||
role="img"
|
||||
style="font-family: "FabricMDL2Icons-4";"
|
||||
>
|
||||
|
||||
</i>
|
||||
<span
|
||||
class="css-112"
|
||||
>
|
||||
Failed
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders Faulted status correctly 1`] = `
|
||||
<div
|
||||
class="ms-Stack css-109"
|
||||
>
|
||||
<i
|
||||
aria-label="Faulted"
|
||||
class="ms-Icon root-105 css-118 mocked-style-Faulted"
|
||||
data-icon-name="StatusErrorFull"
|
||||
role="img"
|
||||
style="font-family: "FabricMDL2Icons-4";"
|
||||
>
|
||||
|
||||
</i>
|
||||
<span
|
||||
class="css-112"
|
||||
>
|
||||
Failed
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders Paused status correctly 1`] = `
|
||||
<div
|
||||
class="ms-Stack css-109"
|
||||
>
|
||||
<i
|
||||
aria-label="Paused"
|
||||
class="ms-Icon root-105 css-114 mocked-style-Paused"
|
||||
data-icon-name="CirclePause"
|
||||
role="img"
|
||||
style="font-family: "FabricMDL2Icons-11";"
|
||||
>
|
||||
|
||||
</i>
|
||||
<span
|
||||
class="css-112"
|
||||
>
|
||||
Paused
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders Pending status correctly 1`] = `
|
||||
<div
|
||||
class="ms-Stack css-109"
|
||||
>
|
||||
<i
|
||||
aria-label="Pending"
|
||||
class="ms-Icon root-105 css-111 mocked-style-Pending"
|
||||
data-icon-name="Clock"
|
||||
role="img"
|
||||
style="font-family: "FabricMDL2Icons-2";"
|
||||
>
|
||||
|
||||
</i>
|
||||
<span
|
||||
class="css-112"
|
||||
>
|
||||
Pending
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders Skipped status correctly 1`] = `
|
||||
<div
|
||||
class="ms-Stack css-109"
|
||||
>
|
||||
<i
|
||||
aria-label="Skipped"
|
||||
class="ms-Icon root-105 css-116 mocked-style-Skipped"
|
||||
data-icon-name="StatusCircleBlock2"
|
||||
role="img"
|
||||
style="font-family: "FabricMDL2Icons-9";"
|
||||
>
|
||||
|
||||
</i>
|
||||
<span
|
||||
class="css-112"
|
||||
>
|
||||
Cancelled
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,122 @@
|
||||
import { MonitorCopyJobsRefState } from "./MonitorCopyJobRefState";
|
||||
import { MonitorCopyJobsRef } from "./MonitorCopyJobs";
|
||||
|
||||
describe("MonitorCopyJobsRefState", () => {
|
||||
beforeEach(() => {
|
||||
MonitorCopyJobsRefState.setState({ ref: null });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should initialize with null ref", () => {
|
||||
const state = MonitorCopyJobsRefState.getState();
|
||||
expect(state.ref).toBeNull();
|
||||
});
|
||||
|
||||
it("should set ref using setRef", () => {
|
||||
const mockRef: MonitorCopyJobsRef = {
|
||||
refreshJobList: jest.fn(),
|
||||
};
|
||||
|
||||
const state = MonitorCopyJobsRefState.getState();
|
||||
state.setRef(mockRef);
|
||||
|
||||
const updatedState = MonitorCopyJobsRefState.getState();
|
||||
expect(updatedState.ref).toBe(mockRef);
|
||||
expect(updatedState.ref).toEqual(mockRef);
|
||||
});
|
||||
|
||||
it("should allow setting ref to null", () => {
|
||||
const mockRef: MonitorCopyJobsRef = {
|
||||
refreshJobList: jest.fn(),
|
||||
};
|
||||
|
||||
MonitorCopyJobsRefState.getState().setRef(mockRef);
|
||||
expect(MonitorCopyJobsRefState.getState().ref).toBe(mockRef);
|
||||
|
||||
MonitorCopyJobsRefState.getState().setRef(null);
|
||||
expect(MonitorCopyJobsRefState.getState().ref).toBeNull();
|
||||
});
|
||||
|
||||
it("should call refreshJobList method on the stored ref", () => {
|
||||
const mockRefreshJobList = jest.fn();
|
||||
const mockRef: MonitorCopyJobsRef = {
|
||||
refreshJobList: mockRefreshJobList,
|
||||
};
|
||||
|
||||
MonitorCopyJobsRefState.getState().setRef(mockRef);
|
||||
|
||||
const state = MonitorCopyJobsRefState.getState();
|
||||
state.ref?.refreshJobList();
|
||||
|
||||
expect(mockRefreshJobList).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should handle calling refreshJobList when ref is null", () => {
|
||||
MonitorCopyJobsRefState.setState({ ref: null });
|
||||
|
||||
const state = MonitorCopyJobsRefState.getState();
|
||||
expect(state.ref).toBeNull();
|
||||
|
||||
expect(() => {
|
||||
state.ref?.refreshJobList();
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("should allow partial state updates", () => {
|
||||
const mockRef: MonitorCopyJobsRef = {
|
||||
refreshJobList: jest.fn(),
|
||||
};
|
||||
|
||||
MonitorCopyJobsRefState.setState({ ref: mockRef });
|
||||
const state1 = MonitorCopyJobsRefState.getState();
|
||||
expect(state1.ref).toBe(mockRef);
|
||||
expect(state1.setRef).toBeDefined();
|
||||
|
||||
const newMockRef: MonitorCopyJobsRef = {
|
||||
refreshJobList: jest.fn(),
|
||||
};
|
||||
MonitorCopyJobsRefState.setState({ ref: newMockRef });
|
||||
const state2 = MonitorCopyJobsRefState.getState();
|
||||
expect(state2.ref).toBe(newMockRef);
|
||||
expect(state2.setRef).toBeDefined();
|
||||
});
|
||||
|
||||
it("should handle multiple subscribers", () => {
|
||||
const mockSubscriber1 = jest.fn();
|
||||
const mockSubscriber2 = jest.fn();
|
||||
|
||||
const unsubscribe1 = MonitorCopyJobsRefState.subscribe(mockSubscriber1);
|
||||
const unsubscribe2 = MonitorCopyJobsRefState.subscribe(mockSubscriber2);
|
||||
|
||||
const mockRef: MonitorCopyJobsRef = {
|
||||
refreshJobList: jest.fn(),
|
||||
};
|
||||
|
||||
MonitorCopyJobsRefState.getState().setRef(mockRef);
|
||||
|
||||
expect(mockSubscriber1).toHaveBeenCalled();
|
||||
expect(mockSubscriber2).toHaveBeenCalled();
|
||||
|
||||
unsubscribe1();
|
||||
unsubscribe2();
|
||||
});
|
||||
|
||||
it("should not notify unsubscribed listeners", () => {
|
||||
const mockSubscriber = jest.fn();
|
||||
|
||||
const unsubscribe = MonitorCopyJobsRefState.subscribe(mockSubscriber);
|
||||
unsubscribe();
|
||||
|
||||
const mockRef: MonitorCopyJobsRef = {
|
||||
refreshJobList: jest.fn(),
|
||||
};
|
||||
|
||||
mockSubscriber.mockClear();
|
||||
MonitorCopyJobsRefState.getState().setRef(mockRef);
|
||||
|
||||
expect(mockSubscriber).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,435 @@
|
||||
import "@testing-library/jest-dom";
|
||||
import { act, render, screen, waitFor } from "@testing-library/react";
|
||||
import React from "react";
|
||||
import { DataTransferJobGetResults } from "Utils/arm/generatedClients/dataTransferService/types";
|
||||
import Explorer from "../../Explorer";
|
||||
import * as CopyJobActions from "../Actions/CopyJobActions";
|
||||
import { CopyJobStatusType } from "../Enums/CopyJobEnums";
|
||||
import { CopyJobType } from "../Types/CopyJobTypes";
|
||||
import MonitorCopyJobs from "./MonitorCopyJobs";
|
||||
|
||||
jest.mock("Common/ShimmerTree/ShimmerTree", () => {
|
||||
const MockShimmerTree = () => {
|
||||
return <div data-testid="shimmer-tree">Loading...</div>;
|
||||
};
|
||||
MockShimmerTree.displayName = "MockShimmerTree";
|
||||
return MockShimmerTree;
|
||||
});
|
||||
|
||||
jest.mock("./Components/CopyJobsList", () => {
|
||||
const MockCopyJobsList = ({ jobs }: any) => {
|
||||
return <div data-testid="copy-jobs-list">Jobs: {jobs.length}</div>;
|
||||
};
|
||||
MockCopyJobsList.displayName = "MockCopyJobsList";
|
||||
return MockCopyJobsList;
|
||||
});
|
||||
|
||||
jest.mock("./Components/CopyJobs.NotFound", () => {
|
||||
const MockCopyJobsNotFound = () => {
|
||||
return <div data-testid="copy-jobs-not-found">No jobs found</div>;
|
||||
};
|
||||
MockCopyJobsNotFound.displayName = "MockCopyJobsNotFound";
|
||||
return MockCopyJobsNotFound;
|
||||
});
|
||||
|
||||
jest.mock("../Actions/CopyJobActions", () => ({
|
||||
getCopyJobs: jest.fn(),
|
||||
updateCopyJobStatus: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("MonitorCopyJobs", () => {
|
||||
let mockExplorer: Explorer;
|
||||
const mockGetCopyJobs = CopyJobActions.getCopyJobs as jest.MockedFunction<typeof CopyJobActions.getCopyJobs>;
|
||||
const mockUpdateCopyJobStatus = CopyJobActions.updateCopyJobStatus as jest.MockedFunction<
|
||||
typeof CopyJobActions.updateCopyJobStatus
|
||||
>;
|
||||
|
||||
const mockJobs: CopyJobType[] = [
|
||||
{
|
||||
ID: "1",
|
||||
Mode: "Offline",
|
||||
Name: "test-job-1",
|
||||
Status: CopyJobStatusType.InProgress,
|
||||
CompletionPercentage: 50,
|
||||
Duration: "10 minutes",
|
||||
LastUpdatedTime: "1/1/2024, 10:00:00 AM",
|
||||
timestamp: 1704110400000,
|
||||
Source: {
|
||||
component: "CosmosDBSql",
|
||||
databaseName: "db1",
|
||||
containerName: "container1",
|
||||
},
|
||||
Destination: {
|
||||
component: "CosmosDBSql",
|
||||
databaseName: "db2",
|
||||
containerName: "container2",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "2",
|
||||
Mode: "Online",
|
||||
Name: "test-job-2",
|
||||
Status: CopyJobStatusType.Completed,
|
||||
CompletionPercentage: 100,
|
||||
Duration: "20 minutes",
|
||||
LastUpdatedTime: "1/1/2024, 11:00:00 AM",
|
||||
timestamp: 1704114000000,
|
||||
Source: {
|
||||
component: "CosmosDBSql",
|
||||
databaseName: "db3",
|
||||
containerName: "container3",
|
||||
},
|
||||
Destination: {
|
||||
component: "CosmosDBSql",
|
||||
databaseName: "db4",
|
||||
containerName: "container4",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
mockExplorer = {} as Explorer;
|
||||
mockGetCopyJobs.mockResolvedValue(mockJobs);
|
||||
mockUpdateCopyJobStatus.mockResolvedValue({
|
||||
id: "test-id",
|
||||
type: "Microsoft.DocumentDB/databaseAccounts/dataTransferJobs",
|
||||
properties: {
|
||||
jobName: "test-job-1",
|
||||
status: "Paused",
|
||||
lastUpdatedUtcTime: "2024-01-01T10:00:00Z",
|
||||
processedCount: 500,
|
||||
totalCount: 1000,
|
||||
mode: "Offline",
|
||||
duration: "00:10:00",
|
||||
source: {
|
||||
databaseName: "db1",
|
||||
containerName: "container1",
|
||||
component: "CosmosDBSql",
|
||||
},
|
||||
destination: {
|
||||
databaseName: "db2",
|
||||
containerName: "container2",
|
||||
component: "CosmosDBSql",
|
||||
},
|
||||
error: {
|
||||
message: "",
|
||||
code: "",
|
||||
},
|
||||
},
|
||||
} as DataTransferJobGetResults);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("Initial Rendering", () => {
|
||||
it("renders the component with correct structure", async () => {
|
||||
render(<MonitorCopyJobs explorer={mockExplorer} />);
|
||||
|
||||
const container = document.querySelector(".monitorCopyJobs");
|
||||
expect(container).toBeInTheDocument();
|
||||
expect(container).toHaveClass("flexContainer");
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGetCopyJobs).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("displays shimmer while loading initially", () => {
|
||||
render(<MonitorCopyJobs explorer={mockExplorer} />);
|
||||
|
||||
expect(screen.getByTestId("shimmer-tree")).toBeInTheDocument();
|
||||
expect(screen.getByText("Loading...")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("fetches jobs on mount", async () => {
|
||||
render(<MonitorCopyJobs explorer={mockExplorer} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGetCopyJobs).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Job List Display", () => {
|
||||
it("displays job list when jobs are loaded", async () => {
|
||||
render(<MonitorCopyJobs explorer={mockExplorer} />);
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(screen.getByTestId("copy-jobs-list")).toBeInTheDocument();
|
||||
},
|
||||
{ timeout: 3000 },
|
||||
);
|
||||
|
||||
expect(screen.getByText("Jobs: 2")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("shimmer-tree")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("displays not found component when no jobs exist", async () => {
|
||||
mockGetCopyJobs.mockResolvedValue([]);
|
||||
|
||||
render(<MonitorCopyJobs explorer={mockExplorer} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("copy-jobs-not-found")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.getByText("No jobs found")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("copy-jobs-list")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("passes correct jobs to CopyJobsList component", async () => {
|
||||
render(<MonitorCopyJobs explorer={mockExplorer} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("copy-jobs-list")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText("Jobs: 2")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("updates job status when action is triggered", async () => {
|
||||
const ref = React.createRef<any>();
|
||||
render(<MonitorCopyJobs explorer={mockExplorer} ref={ref} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("copy-jobs-list")).toBeInTheDocument();
|
||||
});
|
||||
expect(mockJobs[0].Status).toBe(CopyJobStatusType.InProgress);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Error Handling", () => {
|
||||
it("displays error message when fetch fails", async () => {
|
||||
const errorMessage = "Failed to load copy jobs. Please try again later.";
|
||||
mockGetCopyJobs.mockRejectedValue(new Error(errorMessage));
|
||||
|
||||
render(<MonitorCopyJobs explorer={mockExplorer} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(errorMessage)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.queryByTestId("shimmer-tree")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("allows dismissing error message", async () => {
|
||||
mockGetCopyJobs.mockRejectedValue(new Error("Failed to load copy jobs"));
|
||||
const { container } = render(<MonitorCopyJobs explorer={mockExplorer} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Failed to load copy jobs/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const dismissButton = container.querySelector('[aria-label="Close"]');
|
||||
if (dismissButton) {
|
||||
dismissButton.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
}
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText(/Failed to load copy jobs/)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("displays custom error message from getCopyJobs", async () => {
|
||||
const customError = { message: "Custom error occurred" };
|
||||
mockGetCopyJobs.mockRejectedValue(customError);
|
||||
|
||||
render(<MonitorCopyJobs explorer={mockExplorer} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Custom error occurred")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("displays error when job action update fails", async () => {
|
||||
mockUpdateCopyJobStatus.mockRejectedValue(new Error("Update failed"));
|
||||
|
||||
const ref = React.createRef<any>();
|
||||
render(<MonitorCopyJobs explorer={mockExplorer} ref={ref} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("copy-jobs-list")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const mockHandleActionClick = jest.fn(async (job, action, setUpdatingJobAction) => {
|
||||
setUpdatingJobAction({ jobName: job.Name, action });
|
||||
await mockUpdateCopyJobStatus(job, action);
|
||||
});
|
||||
|
||||
await expect(mockHandleActionClick(mockJobs[0], "pause", jest.fn())).rejects.toThrow("Update failed");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Polling and Refresh", () => {
|
||||
it.skip("polls for jobs at regular intervals", async () => {
|
||||
render(<MonitorCopyJobs explorer={mockExplorer} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGetCopyJobs).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(30000);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGetCopyJobs).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(30000);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGetCopyJobs).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
});
|
||||
|
||||
it("stops polling when component unmounts", async () => {
|
||||
const { unmount } = render(<MonitorCopyJobs explorer={mockExplorer} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGetCopyJobs).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
unmount();
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(60000);
|
||||
});
|
||||
|
||||
expect(mockGetCopyJobs).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("refreshes job list via ref", async () => {
|
||||
const ref = React.createRef<any>();
|
||||
render(<MonitorCopyJobs explorer={mockExplorer} ref={ref} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGetCopyJobs).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
ref.current?.refreshJobList();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGetCopyJobs).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
it("prevents refresh when update is in progress", async () => {
|
||||
const ref = React.createRef<any>();
|
||||
render(<MonitorCopyJobs explorer={mockExplorer} ref={ref} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("copy-jobs-list")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
mockUpdateCopyJobStatus.mockImplementation(
|
||||
() =>
|
||||
new Promise((resolve) =>
|
||||
setTimeout(
|
||||
() =>
|
||||
resolve({
|
||||
id: "test-id",
|
||||
type: "Microsoft.DocumentDB/databaseAccounts/dataTransferJobs",
|
||||
properties: {
|
||||
jobName: "test-job-1",
|
||||
status: "Paused",
|
||||
lastUpdatedUtcTime: "2024-01-01T10:00:00Z",
|
||||
processedCount: 500,
|
||||
totalCount: 1000,
|
||||
mode: "Offline",
|
||||
duration: "00:10:00",
|
||||
source: {
|
||||
databaseName: "db1",
|
||||
collectionName: "container1",
|
||||
component: "CosmosDBSql",
|
||||
},
|
||||
destination: {
|
||||
databaseName: "db2",
|
||||
collectionName: "container2",
|
||||
component: "CosmosDBSql",
|
||||
},
|
||||
error: {
|
||||
message: "",
|
||||
code: "",
|
||||
},
|
||||
},
|
||||
} as DataTransferJobGetResults),
|
||||
5000,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(ref.current).toHaveProperty("refreshJobList");
|
||||
expect(typeof ref.current.refreshJobList).toBe("function");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Edge Cases", () => {
|
||||
it("handles empty job array", async () => {
|
||||
mockGetCopyJobs.mockResolvedValue([]);
|
||||
|
||||
render(<MonitorCopyJobs explorer={mockExplorer} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("copy-jobs-not-found")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("handles null response from getCopyJobs gracefully", async () => {
|
||||
mockGetCopyJobs.mockResolvedValue(null as any);
|
||||
|
||||
render(<MonitorCopyJobs explorer={mockExplorer} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("copy-jobs-not-found")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("handles explorer prop correctly", () => {
|
||||
const { rerender } = render(<MonitorCopyJobs explorer={mockExplorer} />);
|
||||
|
||||
const newExplorer = {} as Explorer;
|
||||
rerender(<MonitorCopyJobs explorer={newExplorer} />);
|
||||
|
||||
expect(document.querySelector(".monitorCopyJobs")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Ref Handle", () => {
|
||||
it("exposes refreshJobList method through ref", () => {
|
||||
const ref = React.createRef<any>();
|
||||
render(<MonitorCopyJobs explorer={mockExplorer} ref={ref} />);
|
||||
|
||||
expect(ref.current).toBeDefined();
|
||||
expect(ref.current).toHaveProperty("refreshJobList");
|
||||
expect(typeof ref.current.refreshJobList).toBe("function");
|
||||
});
|
||||
|
||||
it("refreshJobList triggers getCopyJobs", async () => {
|
||||
const ref = React.createRef<any>();
|
||||
render(<MonitorCopyJobs explorer={mockExplorer} ref={ref} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGetCopyJobs).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
ref.current?.refreshJobList();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGetCopyJobs).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Action Callback", () => {
|
||||
it("provides handleActionClick callback to CopyJobsList", async () => {
|
||||
render(<MonitorCopyJobs explorer={mockExplorer} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("copy-jobs-list")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -39,8 +39,9 @@ const MonitorCopyJobs = forwardRef<MonitorCopyJobsRef, MonitorCopyJobsProps>(({
|
||||
setError(null);
|
||||
|
||||
const response = await getCopyJobs();
|
||||
const normalizedResponse = response || [];
|
||||
setJobs((prevJobs) => {
|
||||
return isEqual(prevJobs, response) ? prevJobs : response;
|
||||
return isEqual(prevJobs, normalizedResponse) ? prevJobs : normalizedResponse;
|
||||
});
|
||||
} catch (error) {
|
||||
setError(error.message || "Failed to load copy jobs. Please try again later.");
|
||||
@@ -97,29 +98,27 @@ const MonitorCopyJobs = forwardRef<MonitorCopyJobsRef, MonitorCopyJobsProps>(({
|
||||
[],
|
||||
);
|
||||
|
||||
const renderJobsList = () => {
|
||||
if (loading) {
|
||||
return null;
|
||||
}
|
||||
if (jobs.length > 0) {
|
||||
return <CopyJobsList jobs={jobs} handleActionClick={handleActionClick} />;
|
||||
}
|
||||
return <CopyJobsNotFound explorer={explorer} />;
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack className="monitorCopyJobs flexContainer">
|
||||
{loading && (
|
||||
<ShimmerTree indentLevels={SHIMMER_INDENT_LEVELS} style={{ width: "100%", padding: "1rem 2.5rem" }} />
|
||||
)}
|
||||
{error && (
|
||||
<MessageBar messageBarType={MessageBarType.error} isMultiline={false} onDismiss={() => setError(null)}>
|
||||
<MessageBar
|
||||
messageBarType={MessageBarType.error}
|
||||
isMultiline={false}
|
||||
onDismiss={() => setError(null)}
|
||||
dismissButtonAriaLabel="Close"
|
||||
>
|
||||
{error}
|
||||
</MessageBar>
|
||||
)}
|
||||
{renderJobsList()}
|
||||
{!loading && jobs.length > 0 && <CopyJobsList jobs={jobs} handleActionClick={handleActionClick} />}
|
||||
{!loading && jobs.length === 0 && <CopyJobsNotFound explorer={explorer} />}
|
||||
</Stack>
|
||||
);
|
||||
});
|
||||
|
||||
MonitorCopyJobs.displayName = "MonitorCopyJobs";
|
||||
|
||||
export default MonitorCopyJobs;
|
||||
|
||||
Reference in New Issue
Block a user