Files
cosmos-explorer/src/Explorer/ContainerCopy/CreateCopyJob/Utils/useCopyJobNavigation.test.tsx
BChoudhury-ms a714ef02c0 Added comprehensive unit test coverage for Container Copy jobs (#2275)
* copy job uts

* unit test coverage

* lint fix

* normalize account dropdown id
2025-12-09 09:35:58 +05:30

325 lines
11 KiB
TypeScript

import "@testing-library/jest-dom";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import React from "react";
import { CopyJobMigrationType } from "../../Enums/CopyJobEnums";
import { CopyJobContextState } from "../../Types/CopyJobTypes";
import { useCopyJobNavigation } from "./useCopyJobNavigation";
jest.mock("../../../../hooks/useSidePanel", () => ({
useSidePanel: {
getState: jest.fn(() => ({
closeSidePanel: jest.fn(),
})),
},
}));
jest.mock("../../Actions/CopyJobActions", () => ({
submitCreateCopyJob: jest.fn(),
}));
jest.mock("../../Context/CopyJobContext", () => ({
useCopyJobContext: jest.fn(),
}));
jest.mock("./useCopyJobPrerequisitesCache", () => ({
useCopyJobPrerequisitesCache: jest.fn(),
}));
jest.mock("./useCreateCopyJobScreensList", () => ({
SCREEN_KEYS: {
SelectAccount: "SelectAccount",
AssignPermissions: "AssignPermissions",
SelectSourceAndTargetContainers: "SelectSourceAndTargetContainers",
CreateCollection: "CreateCollection",
PreviewCopyJob: "PreviewCopyJob",
},
useCreateCopyJobScreensList: jest.fn(),
}));
jest.mock("../../CopyJobUtils", () => ({
getContainerIdentifiers: jest.fn(),
isIntraAccountCopy: jest.fn(),
}));
import { useSidePanel } from "../../../../hooks/useSidePanel";
import { submitCreateCopyJob } from "../../Actions/CopyJobActions";
import { useCopyJobContext } from "../../Context/CopyJobContext";
import { getContainerIdentifiers, isIntraAccountCopy } from "../../CopyJobUtils";
import { useCopyJobPrerequisitesCache } from "./useCopyJobPrerequisitesCache";
import { SCREEN_KEYS, useCreateCopyJobScreensList } from "./useCreateCopyJobScreensList";
const TestComponent: React.FC<{
onHookResult?: (result: ReturnType<typeof useCopyJobNavigation>) => void;
}> = ({ onHookResult }) => {
const hookResult = useCopyJobNavigation();
React.useEffect(() => {
onHookResult?.(hookResult);
}, [hookResult, onHookResult]);
return (
<div>
<div data-testid="current-screen">{hookResult.currentScreen?.key}</div>
<div data-testid="primary-disabled">{hookResult.isPrimaryDisabled.toString()}</div>
<div data-testid="previous-disabled">{hookResult.isPreviousDisabled.toString()}</div>
<div data-testid="primary-btn-text">{hookResult.primaryBtnText}</div>
<button data-testid="primary-btn" onClick={hookResult.handlePrimary} disabled={hookResult.isPrimaryDisabled}>
{hookResult.primaryBtnText}
</button>
<button data-testid="previous-btn" onClick={hookResult.handlePrevious} disabled={hookResult.isPreviousDisabled}>
Previous
</button>
<button data-testid="cancel-btn" onClick={hookResult.handleCancel}>
Cancel
</button>
{hookResult.currentScreen?.key === SCREEN_KEYS.SelectSourceAndTargetContainers && (
<button data-testid="add-collection-btn" onClick={hookResult.showAddCollectionPanel}>
Show Collection Panel
</button>
)}
</div>
);
};
describe("useCopyJobNavigation", () => {
const createMockCopyJobState = (overrides?: Partial<CopyJobContextState>): CopyJobContextState => ({
jobName: "test-job",
migrationType: CopyJobMigrationType.Offline,
source: {
subscription: { subscriptionId: "source-sub-id" } as any,
account: { id: "source-account-id", name: "Account-1" } as any,
databaseId: "source-db",
containerId: "source-container",
},
target: {
subscriptionId: "target-sub-id",
account: { id: "target-account-id", name: "Account-2" } as any,
databaseId: "target-db",
containerId: "target-container",
},
...overrides,
});
const createMockScreen = (key: string, validations: any[] = []) => ({
key,
component: <div>{key} Screen</div>,
validations,
});
const mockResetCopyJobState = jest.fn();
const mockSetContextError = jest.fn();
const mockCloseSidePanel = jest.fn();
const mockCopyJobState = createMockCopyJobState();
const mockValidationCache = new Map([
["validation1", true],
["validation2", true],
]);
const setupMocks = (screensList: any[] = [], isIntraAccount = false) => {
(useCopyJobContext as jest.Mock).mockReturnValue({
copyJobState: mockCopyJobState,
resetCopyJobState: mockResetCopyJobState,
setContextError: mockSetContextError,
});
(useCopyJobPrerequisitesCache as unknown as jest.Mock).mockReturnValue({
validationCache: mockValidationCache,
});
(useCreateCopyJobScreensList as jest.Mock).mockReturnValue(
screensList.length > 0 ? screensList : [createMockScreen(SCREEN_KEYS.SelectAccount)],
);
(useSidePanel.getState as jest.Mock).mockReturnValue({
closeSidePanel: mockCloseSidePanel,
});
(getContainerIdentifiers as jest.Mock).mockImplementation((container) => ({
accountId: container.account?.id,
databaseId: container.databaseId,
containerId: container.containerId,
}));
(isIntraAccountCopy as jest.Mock).mockReturnValue(isIntraAccount);
};
const clickPrimaryButton = () => fireEvent.click(screen.getByTestId("primary-btn"));
const clickPreviousButton = () => fireEvent.click(screen.getByTestId("previous-btn"));
const expectScreen = (screenKey: string) => {
expect(screen.getByTestId("current-screen")).toHaveTextContent(screenKey);
};
const expectPrimaryButtonText = (text: string) => {
expect(screen.getByTestId("primary-btn-text")).toHaveTextContent(text);
};
const expectPrimaryDisabled = (disabled: boolean) => {
expect(screen.getByTestId("primary-disabled")).toHaveTextContent(disabled.toString());
};
const navigateToScreen = (screenKey: string, clicks: number) => {
for (let i = 0; i < clicks; i++) {
clickPrimaryButton();
}
expectScreen(screenKey);
};
beforeEach(() => {
jest.clearAllMocks();
setupMocks();
});
describe("Initial state and navigation", () => {
test("should start with SelectAccount screen and disable previous button", () => {
render(<TestComponent />);
expectScreen(SCREEN_KEYS.SelectAccount);
expect(screen.getByTestId("previous-disabled")).toHaveTextContent("true");
});
test("should show Next button text by default", () => {
render(<TestComponent />);
expectPrimaryButtonText("Next");
});
test("should navigate through screens and show Create button for CreateCollection", () => {
const screens = [
createMockScreen(SCREEN_KEYS.SelectAccount),
createMockScreen(SCREEN_KEYS.SelectSourceAndTargetContainers),
createMockScreen(SCREEN_KEYS.CreateCollection),
];
setupMocks(screens, true);
render(<TestComponent />);
expectScreen(SCREEN_KEYS.SelectAccount);
clickPrimaryButton();
expectScreen(SCREEN_KEYS.SelectSourceAndTargetContainers);
expectPrimaryButtonText("Next");
fireEvent.click(screen.getByTestId("add-collection-btn"));
expectScreen(SCREEN_KEYS.CreateCollection);
expectPrimaryButtonText("Create");
clickPreviousButton();
expectScreen(SCREEN_KEYS.SelectSourceAndTargetContainers);
expectPrimaryButtonText("Next");
});
});
describe("Validation logic", () => {
test("should disable primary button when validations fail", () => {
const invalidScreen = createMockScreen(SCREEN_KEYS.SelectAccount, [
{ validate: () => false, message: "Invalid state" },
]);
setupMocks([invalidScreen]);
render(<TestComponent />);
expectPrimaryDisabled(true);
});
test("should enable primary button when all validations pass", () => {
const validScreen = createMockScreen(SCREEN_KEYS.SelectAccount, [
{ validate: () => true, message: "Valid state" },
]);
setupMocks([validScreen]);
render(<TestComponent />);
expectPrimaryDisabled(false);
});
test("should prevent navigation when source and target containers are identical", () => {
const screens = [
createMockScreen(SCREEN_KEYS.SelectAccount),
createMockScreen(SCREEN_KEYS.SelectSourceAndTargetContainers, [
{ validate: () => true, message: "Valid containers" },
]),
];
setupMocks(screens, true);
(getContainerIdentifiers as jest.Mock).mockImplementation(() => ({
accountId: "same-account",
databaseId: "same-db",
containerId: "same-container",
}));
render(<TestComponent />);
navigateToScreen(SCREEN_KEYS.SelectSourceAndTargetContainers, 1);
clickPrimaryButton();
expectScreen(SCREEN_KEYS.SelectSourceAndTargetContainers);
expect(mockSetContextError).toHaveBeenCalledWith(
"Source and destination containers cannot be the same. Please select different containers to proceed.",
);
});
});
describe("Copy job submission", () => {
const setupToPreviewScreen = () => {
const screens = [
createMockScreen(SCREEN_KEYS.SelectAccount),
createMockScreen(SCREEN_KEYS.SelectSourceAndTargetContainers),
createMockScreen(SCREEN_KEYS.PreviewCopyJob),
];
setupMocks(screens, true);
render(<TestComponent />);
navigateToScreen(SCREEN_KEYS.PreviewCopyJob, 2);
clickPrimaryButton();
};
test("should handle successful copy job submission", async () => {
(submitCreateCopyJob as jest.Mock).mockResolvedValue(undefined);
setupToPreviewScreen();
await waitFor(() => {
expect(submitCreateCopyJob).toHaveBeenCalledWith(mockCopyJobState, expect.any(Function));
});
});
test("should handle copy job submission error", async () => {
const error = new Error("Submission failed");
(submitCreateCopyJob as jest.Mock).mockRejectedValue(error);
setupToPreviewScreen();
await waitFor(() => {
expect(mockSetContextError).toHaveBeenCalledWith("Submission failed");
});
});
test("should handle unknown error during submission", async () => {
(submitCreateCopyJob as jest.Mock).mockRejectedValue("Unknown error");
setupToPreviewScreen();
await waitFor(() => {
expect(mockSetContextError).toHaveBeenCalledWith("Failed to create copy job. Please try again later.");
});
});
test("should disable buttons during loading", async () => {
let resolveSubmission: () => void;
const submissionPromise = new Promise<void>((resolve) => {
resolveSubmission = resolve;
});
(submitCreateCopyJob as jest.Mock).mockReturnValue(submissionPromise);
setupToPreviewScreen();
await waitFor(() => {
expectPrimaryDisabled(true);
});
resolveSubmission!();
await waitFor(() => {
expectPrimaryDisabled(false);
});
});
});
});