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:
BChoudhury-ms
2025-12-09 09:35:58 +05:30
committed by GitHub
parent ca858c08fb
commit a714ef02c0
80 changed files with 23899 additions and 35 deletions

View File

@@ -0,0 +1,324 @@
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);
});
});
});
});

View File

@@ -0,0 +1,334 @@
import "@testing-library/jest-dom";
import { act, render, screen } from "@testing-library/react";
import React from "react";
import { useCopyJobPrerequisitesCache } from "./useCopyJobPrerequisitesCache";
describe("useCopyJobPrerequisitesCache", () => {
let hookResult: any;
const TestComponent = ({ onHookUpdate }: { onHookUpdate?: () => void }): JSX.Element => {
hookResult = useCopyJobPrerequisitesCache();
React.useEffect(() => {
if (onHookUpdate) {
onHookUpdate();
}
}, [onHookUpdate]);
return (
<div>
<span data-testid="cache-size">{hookResult.validationCache.size}</span>
<button
data-testid="set-cache-button"
onClick={() => {
const testCache = new Map<string, boolean>();
testCache.set("test-key", true);
hookResult.setValidationCache(testCache);
}}
>
Set Cache
</button>
<button
data-testid="clear-cache-button"
onClick={() => {
hookResult.setValidationCache(new Map<string, boolean>());
}}
>
Clear Cache
</button>
</div>
);
};
afterEach(() => {
if (hookResult) {
act(() => {
hookResult.setValidationCache(new Map<string, boolean>());
});
}
});
it("should initialize with an empty validation cache", () => {
render(<TestComponent />);
expect(hookResult.validationCache).toBeInstanceOf(Map);
expect(hookResult.validationCache.size).toBe(0);
expect(screen.getByTestId("cache-size")).toHaveTextContent("0");
});
it("should provide a setValidationCache function", () => {
render(<TestComponent />);
expect(typeof hookResult.setValidationCache).toBe("function");
});
it("should update validation cache when setValidationCache is called", () => {
render(<TestComponent />);
const testCache = new Map<string, boolean>();
testCache.set("test-key", true);
testCache.set("another-key", false);
act(() => {
hookResult.setValidationCache(testCache);
});
expect(hookResult.validationCache).toBe(testCache);
expect(hookResult.validationCache.size).toBe(2);
expect(hookResult.validationCache.get("test-key")).toBe(true);
expect(hookResult.validationCache.get("another-key")).toBe(false);
expect(screen.getByTestId("cache-size")).toHaveTextContent("2");
});
it("should replace the entire validation cache when setValidationCache is called", () => {
render(<TestComponent />);
const initialCache = new Map<string, boolean>();
initialCache.set("initial-key", true);
act(() => {
hookResult.setValidationCache(initialCache);
});
expect(hookResult.validationCache.get("initial-key")).toBe(true);
expect(screen.getByTestId("cache-size")).toHaveTextContent("1");
const newCache = new Map<string, boolean>();
newCache.set("new-key", false);
act(() => {
hookResult.setValidationCache(newCache);
});
expect(hookResult.validationCache.get("initial-key")).toBeUndefined();
expect(hookResult.validationCache.get("new-key")).toBe(false);
expect(hookResult.validationCache.size).toBe(1);
expect(screen.getByTestId("cache-size")).toHaveTextContent("1");
});
it("should handle empty Map updates", () => {
render(<TestComponent />);
const initialCache = new Map<string, boolean>();
initialCache.set("test-key", true);
act(() => {
hookResult.setValidationCache(initialCache);
});
expect(hookResult.validationCache.size).toBe(1);
expect(screen.getByTestId("cache-size")).toHaveTextContent("1");
act(() => {
screen.getByTestId("clear-cache-button").click();
});
expect(hookResult.validationCache.size).toBe(0);
expect(screen.getByTestId("cache-size")).toHaveTextContent("0");
});
it("should maintain state across multiple hook instances (global store behavior)", () => {
let firstHookResult: any;
let secondHookResult: any;
const FirstComponent = (): JSX.Element => {
firstHookResult = useCopyJobPrerequisitesCache();
return <div data-testid="first-component">First</div>;
};
const SecondComponent = (): JSX.Element => {
secondHookResult = useCopyJobPrerequisitesCache();
return <div data-testid="second-component">Second</div>;
};
render(
<div>
<FirstComponent />
<SecondComponent />
</div>,
);
const testCache = new Map<string, boolean>();
testCache.set("shared-key", true);
act(() => {
firstHookResult.setValidationCache(testCache);
});
expect(secondHookResult.validationCache.get("shared-key")).toBe(true);
expect(secondHookResult.validationCache.size).toBe(1);
expect(firstHookResult.validationCache.get("shared-key")).toBe(true);
expect(firstHookResult.validationCache.size).toBe(1);
});
it("should allow updates from different hook instances", () => {
let firstHookResult: any;
let secondHookResult: any;
const FirstComponent = (): JSX.Element => {
firstHookResult = useCopyJobPrerequisitesCache();
return (
<button
data-testid="first-update"
onClick={() => {
const testCache = new Map<string, boolean>();
testCache.set("key-from-first", true);
firstHookResult.setValidationCache(testCache);
}}
>
Update from First
</button>
);
};
const SecondComponent = (): JSX.Element => {
secondHookResult = useCopyJobPrerequisitesCache();
return (
<button
data-testid="second-update"
onClick={() => {
const testCache = new Map<string, boolean>();
testCache.set("key-from-second", false);
secondHookResult.setValidationCache(testCache);
}}
>
Update from Second
</button>
);
};
render(
<div>
<FirstComponent />
<SecondComponent />
</div>,
);
act(() => {
screen.getByTestId("first-update").click();
});
expect(secondHookResult.validationCache.get("key-from-first")).toBe(true);
act(() => {
screen.getByTestId("second-update").click();
});
expect(firstHookResult.validationCache.get("key-from-second")).toBe(false);
expect(firstHookResult.validationCache.get("key-from-first")).toBeUndefined();
});
it("should handle complex validation scenarios", () => {
const ComplexTestComponent = (): JSX.Element => {
hookResult = useCopyJobPrerequisitesCache();
const handleComplexUpdate = () => {
const complexCache = new Map<string, boolean>();
complexCache.set("database-validation", true);
complexCache.set("container-validation", true);
complexCache.set("network-validation", false);
complexCache.set("authentication-validation", true);
complexCache.set("permission-validation", false);
hookResult.setValidationCache(complexCache);
};
return (
<button data-testid="complex-update" onClick={handleComplexUpdate}>
Set Complex Cache
</button>
);
};
render(<ComplexTestComponent />);
act(() => {
screen.getByTestId("complex-update").click();
});
expect(hookResult.validationCache.size).toBe(5);
expect(hookResult.validationCache.get("database-validation")).toBe(true);
expect(hookResult.validationCache.get("container-validation")).toBe(true);
expect(hookResult.validationCache.get("network-validation")).toBe(false);
expect(hookResult.validationCache.get("authentication-validation")).toBe(true);
expect(hookResult.validationCache.get("permission-validation")).toBe(false);
});
it("should handle edge case keys", () => {
const EdgeCaseTestComponent = (): JSX.Element => {
hookResult = useCopyJobPrerequisitesCache();
const handleEdgeCaseUpdate = () => {
const edgeCaseCache = new Map<string, boolean>();
edgeCaseCache.set("", true);
edgeCaseCache.set(" ", false);
edgeCaseCache.set("special-chars!@#$%^&*()", true);
edgeCaseCache.set("very-long-key-".repeat(10), false);
edgeCaseCache.set("unicode-key-🔑", true);
hookResult.setValidationCache(edgeCaseCache);
};
return (
<button data-testid="edge-case-update" onClick={handleEdgeCaseUpdate}>
Set Edge Case Cache
</button>
);
};
render(<EdgeCaseTestComponent />);
act(() => {
screen.getByTestId("edge-case-update").click();
});
expect(hookResult.validationCache.size).toBe(5);
expect(hookResult.validationCache.get("")).toBe(true);
expect(hookResult.validationCache.get(" ")).toBe(false);
expect(hookResult.validationCache.get("special-chars!@#$%^&*()")).toBe(true);
expect(hookResult.validationCache.get("very-long-key-".repeat(10))).toBe(false);
expect(hookResult.validationCache.get("unicode-key-🔑")).toBe(true);
});
it("should handle setting the same cache reference without errors", () => {
let testCache: Map<string, boolean>;
const SameReferenceTestComponent = (): JSX.Element => {
hookResult = useCopyJobPrerequisitesCache();
const handleFirstUpdate = () => {
testCache = new Map<string, boolean>();
testCache.set("test-key", true);
hookResult.setValidationCache(testCache);
};
const handleSecondUpdate = () => {
hookResult.setValidationCache(testCache);
};
return (
<div>
<button data-testid="first-update" onClick={handleFirstUpdate}>
First Update
</button>
<button data-testid="second-update" onClick={handleSecondUpdate}>
Second Update (Same Reference)
</button>
<span data-testid="cache-content">{hookResult.validationCache.get("test-key")?.toString()}</span>
</div>
);
};
render(<SameReferenceTestComponent />);
act(() => {
screen.getByTestId("first-update").click();
});
expect(hookResult.validationCache.get("test-key")).toBe(true);
expect(screen.getByTestId("cache-content")).toHaveTextContent("true");
act(() => {
screen.getByTestId("second-update").click();
});
expect(hookResult.validationCache).toBe(testCache);
expect(hookResult.validationCache.get("test-key")).toBe(true);
expect(screen.getByTestId("cache-content")).toHaveTextContent("true");
});
});

View File

@@ -0,0 +1,477 @@
import "@testing-library/jest-dom";
import { render, screen } from "@testing-library/react";
import React from "react";
import Explorer from "../../../Explorer";
import CopyJobContextProvider, { useCopyJobContext } from "../../Context/CopyJobContext";
import { CopyJobMigrationType } from "../../Enums/CopyJobEnums";
import { CopyJobContextState } from "../../Types/CopyJobTypes";
import { SCREEN_KEYS, useCreateCopyJobScreensList } from "./useCreateCopyJobScreensList";
jest.mock("../../Context/CopyJobContext", () => {
const actual = jest.requireActual("../../Context/CopyJobContext");
return {
__esModule: true,
...actual,
default: actual.default,
useCopyJobContext: jest.fn(),
};
});
jest.mock("../Screens/AssignPermissions/AssignPermissions", () => {
const MockAssignPermissions = () => {
return <div data-testid="assign-permissions">AssignPermissions</div>;
};
MockAssignPermissions.displayName = "MockAssignPermissions";
return MockAssignPermissions;
});
jest.mock("../Screens/CreateContainer/AddCollectionPanelWrapper", () => {
const MockAddCollectionPanelWrapper = () => {
return <div data-testid="add-collection-panel">AddCollectionPanelWrapper</div>;
};
MockAddCollectionPanelWrapper.displayName = "MockAddCollectionPanelWrapper";
return MockAddCollectionPanelWrapper;
});
jest.mock("../Screens/PreviewCopyJob/PreviewCopyJob", () => {
const MockPreviewCopyJob = () => {
return <div data-testid="preview-copy-job">PreviewCopyJob</div>;
};
MockPreviewCopyJob.displayName = "MockPreviewCopyJob";
return MockPreviewCopyJob;
});
jest.mock("../Screens/SelectAccount/SelectAccount", () => {
const MockSelectAccount = () => {
return <div data-testid="select-account">SelectAccount</div>;
};
MockSelectAccount.displayName = "MockSelectAccount";
return MockSelectAccount;
});
jest.mock("../Screens/SelectSourceAndTargetContainers/SelectSourceAndTargetContainers", () => {
const MockSelectSourceAndTargetContainers = () => {
return <div data-testid="select-source-target">SelectSourceAndTargetContainers</div>;
};
MockSelectSourceAndTargetContainers.displayName = "MockSelectSourceAndTargetContainers";
return MockSelectSourceAndTargetContainers;
});
const TestHookComponent: React.FC<{ goBack: () => void }> = ({ goBack }) => {
const screens = useCreateCopyJobScreensList(goBack);
return (
<div data-testid="test-hook-component">
{screens.map((screen, index) => (
<div key={screen.key} data-testid={`screen-${index}`}>
<div data-testid={`screen-key-${index}`}>{screen.key}</div>
<div data-testid={`screen-component-${index}`}>{screen.component}</div>
<div data-testid={`screen-validations-${index}`}>
{JSON.stringify(screen.validations.map((v) => v.message))}
</div>
</div>
))}
</div>
);
};
describe("useCreateCopyJobScreensList", () => {
const mockExplorer = {} as Explorer;
const mockGoBack = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
(useCopyJobContext as jest.Mock).mockReturnValue({
explorer: mockExplorer,
});
});
const renderWithContext = (component: React.ReactElement) => {
return render(<CopyJobContextProvider explorer={mockExplorer}>{component}</CopyJobContextProvider>);
};
describe("Hook behavior", () => {
it("should return screens list with correct keys and components", () => {
renderWithContext(<TestHookComponent goBack={mockGoBack} />);
expect(screen.getByTestId("test-hook-component")).toBeInTheDocument();
expect(screen.getByTestId("screen-key-0")).toHaveTextContent(SCREEN_KEYS.SelectAccount);
expect(screen.getByTestId("screen-key-1")).toHaveTextContent(SCREEN_KEYS.SelectSourceAndTargetContainers);
expect(screen.getByTestId("screen-key-2")).toHaveTextContent(SCREEN_KEYS.CreateCollection);
expect(screen.getByTestId("screen-key-3")).toHaveTextContent(SCREEN_KEYS.PreviewCopyJob);
expect(screen.getByTestId("screen-key-4")).toHaveTextContent(SCREEN_KEYS.AssignPermissions);
expect(screen.getByTestId("select-account")).toBeInTheDocument();
expect(screen.getByTestId("select-source-target")).toBeInTheDocument();
expect(screen.getByTestId("add-collection-panel")).toBeInTheDocument();
expect(screen.getByTestId("preview-copy-job")).toBeInTheDocument();
expect(screen.getByTestId("assign-permissions")).toBeInTheDocument();
});
it("should return exactly 5 screens in the correct order", () => {
renderWithContext(<TestHookComponent goBack={mockGoBack} />);
const screens = screen.getAllByTestId(/screen-\d+/);
expect(screens).toHaveLength(5);
});
it("should memoize results based on explorer dependency", () => {
const { rerender } = renderWithContext(<TestHookComponent goBack={mockGoBack} />);
const initialScreens = screen.getAllByTestId(/screen-key-\d+/).map((el) => el.textContent);
rerender(
<CopyJobContextProvider explorer={mockExplorer}>
<TestHookComponent goBack={mockGoBack} />
</CopyJobContextProvider>,
);
const rerenderScreens = screen.getAllByTestId(/screen-key-\d+/).map((el) => el.textContent);
expect(rerenderScreens).toEqual(initialScreens);
});
});
describe("Screen validations", () => {
describe("SelectAccount screen validation", () => {
it("should validate subscription and account presence", () => {
renderWithContext(<TestHookComponent goBack={mockGoBack} />);
const validationMessages = JSON.parse(screen.getByTestId("screen-validations-0").textContent || "[]");
expect(validationMessages).toContain("Please select a subscription and account to proceed");
});
it("should pass validation when subscription and account are present", () => {
const mockState: CopyJobContextState = {
jobName: "",
migrationType: CopyJobMigrationType.Offline,
source: {
subscription: { subscriptionId: "test-sub" } as any,
account: { name: "test-account" } as any,
databaseId: "",
containerId: "",
},
target: {
subscriptionId: "",
account: null as any,
databaseId: "",
containerId: "",
},
};
const ValidationTestComponent = () => {
const screens = useCreateCopyJobScreensList(mockGoBack);
const selectAccountScreen = screens.find((s) => s.key === SCREEN_KEYS.SelectAccount);
const isValid = selectAccountScreen?.validations[0]?.validate(mockState);
return <div data-testid="validation-result">{isValid ? "valid" : "invalid"}</div>;
};
renderWithContext(<ValidationTestComponent />);
expect(screen.getByTestId("validation-result")).toHaveTextContent("valid");
});
it("should fail validation when subscription is missing", () => {
const mockState: CopyJobContextState = {
jobName: "",
migrationType: CopyJobMigrationType.Offline,
source: {
subscription: null as any,
account: { name: "test-account" } as any,
databaseId: "",
containerId: "",
},
target: {
subscriptionId: "",
account: null as any,
databaseId: "",
containerId: "",
},
};
const ValidationTestComponent = () => {
const screens = useCreateCopyJobScreensList(mockGoBack);
const selectAccountScreen = screens.find((s) => s.key === SCREEN_KEYS.SelectAccount);
const isValid = selectAccountScreen?.validations[0]?.validate(mockState);
return <div data-testid="validation-result">{isValid ? "valid" : "invalid"}</div>;
};
renderWithContext(<ValidationTestComponent />);
expect(screen.getByTestId("validation-result")).toHaveTextContent("invalid");
});
});
describe("SelectSourceAndTargetContainers screen validation", () => {
it("should validate source and target containers", () => {
renderWithContext(<TestHookComponent goBack={mockGoBack} />);
const validationMessages = JSON.parse(screen.getByTestId("screen-validations-1").textContent || "[]");
expect(validationMessages).toContain("Please select source and target containers to proceed");
});
it("should pass validation when all required fields are present", () => {
const mockState: CopyJobContextState = {
jobName: "",
migrationType: CopyJobMigrationType.Offline,
source: {
subscription: null as any,
account: null as any,
databaseId: "source-db",
containerId: "source-container",
},
target: {
subscriptionId: "",
account: null as any,
databaseId: "target-db",
containerId: "target-container",
},
};
const ValidationTestComponent = () => {
const screens = useCreateCopyJobScreensList(mockGoBack);
const screen = screens.find((s) => s.key === SCREEN_KEYS.SelectSourceAndTargetContainers);
const isValid = screen?.validations[0]?.validate(mockState);
return <div data-testid="validation-result">{isValid ? "valid" : "invalid"}</div>;
};
renderWithContext(<ValidationTestComponent />);
expect(screen.getByTestId("validation-result")).toHaveTextContent("valid");
});
it("should fail validation when source database is missing", () => {
const mockState: CopyJobContextState = {
jobName: "",
migrationType: CopyJobMigrationType.Offline,
source: {
subscription: null as any,
account: null as any,
databaseId: "",
containerId: "source-container",
},
target: {
subscriptionId: "",
account: null as any,
databaseId: "target-db",
containerId: "target-container",
},
};
const ValidationTestComponent = () => {
const screens = useCreateCopyJobScreensList(mockGoBack);
const screen = screens.find((s) => s.key === SCREEN_KEYS.SelectSourceAndTargetContainers);
const isValid = screen?.validations[0]?.validate(mockState);
return <div data-testid="validation-result">{isValid ? "valid" : "invalid"}</div>;
};
renderWithContext(<ValidationTestComponent />);
expect(screen.getByTestId("validation-result")).toHaveTextContent("invalid");
});
});
describe("CreateCollection screen", () => {
it("should have no validations", () => {
renderWithContext(<TestHookComponent goBack={mockGoBack} />);
const validationMessages = JSON.parse(screen.getByTestId("screen-validations-2").textContent || "[]");
expect(validationMessages).toEqual([]);
});
});
describe("PreviewCopyJob screen validation", () => {
it("should validate job name format", () => {
renderWithContext(<TestHookComponent goBack={mockGoBack} />);
const validationMessages = JSON.parse(screen.getByTestId("screen-validations-3").textContent || "[]");
expect(validationMessages).toContain("Please enter a job name to proceed");
});
it("should pass validation with valid job name", () => {
const mockState: CopyJobContextState = {
jobName: "valid-job-name_123",
migrationType: CopyJobMigrationType.Offline,
source: {
subscription: null as any,
account: null as any,
databaseId: "",
containerId: "",
},
target: {
subscriptionId: "",
account: null as any,
databaseId: "",
containerId: "",
},
};
const ValidationTestComponent = () => {
const screens = useCreateCopyJobScreensList(mockGoBack);
const screen = screens.find((s) => s.key === SCREEN_KEYS.PreviewCopyJob);
const isValid = screen?.validations[0]?.validate(mockState);
return <div data-testid="validation-result">{isValid ? "valid" : "invalid"}</div>;
};
renderWithContext(<ValidationTestComponent />);
expect(screen.getByTestId("validation-result")).toHaveTextContent("valid");
});
it("should fail validation with invalid job name characters", () => {
const mockState: CopyJobContextState = {
jobName: "invalid job name with spaces!",
migrationType: CopyJobMigrationType.Offline,
source: {
subscription: null as any,
account: null as any,
databaseId: "",
containerId: "",
},
target: {
subscriptionId: "",
account: null as any,
databaseId: "",
containerId: "",
},
};
const ValidationTestComponent = () => {
const screens = useCreateCopyJobScreensList(mockGoBack);
const screen = screens.find((s) => s.key === SCREEN_KEYS.PreviewCopyJob);
const isValid = screen?.validations[0]?.validate(mockState);
return <div data-testid="validation-result">{isValid ? "valid" : "invalid"}</div>;
};
renderWithContext(<ValidationTestComponent />);
expect(screen.getByTestId("validation-result")).toHaveTextContent("invalid");
});
it("should fail validation with empty job name", () => {
const mockState: CopyJobContextState = {
jobName: "",
migrationType: CopyJobMigrationType.Offline,
source: {
subscription: null as any,
account: null as any,
databaseId: "",
containerId: "",
},
target: {
subscriptionId: "",
account: null as any,
databaseId: "",
containerId: "",
},
};
const ValidationTestComponent = () => {
const screens = useCreateCopyJobScreensList(mockGoBack);
const screen = screens.find((s) => s.key === SCREEN_KEYS.PreviewCopyJob);
const isValid = screen?.validations[0]?.validate(mockState);
return <div data-testid="validation-result">{isValid ? "valid" : "invalid"}</div>;
};
renderWithContext(<ValidationTestComponent />);
expect(screen.getByTestId("validation-result")).toHaveTextContent("invalid");
});
});
describe("AssignPermissions screen validation", () => {
it("should validate cache values", () => {
renderWithContext(<TestHookComponent goBack={mockGoBack} />);
const validationMessages = JSON.parse(screen.getByTestId("screen-validations-4").textContent || "[]");
expect(validationMessages).toContain("Please ensure all previous steps are valid to proceed");
});
it("should pass validation when all cache values are true", () => {
const mockCache = new Map([
["step1", true],
["step2", true],
["step3", true],
]);
const ValidationTestComponent = () => {
const screens = useCreateCopyJobScreensList(mockGoBack);
const screen = screens.find((s) => s.key === SCREEN_KEYS.AssignPermissions);
const isValid = screen?.validations[0]?.validate(mockCache);
return <div data-testid="validation-result">{isValid ? "valid" : "invalid"}</div>;
};
renderWithContext(<ValidationTestComponent />);
expect(screen.getByTestId("validation-result")).toHaveTextContent("valid");
});
it("should fail validation when cache is empty", () => {
const mockCache = new Map();
const ValidationTestComponent = () => {
const screens = useCreateCopyJobScreensList(mockGoBack);
const screen = screens.find((s) => s.key === SCREEN_KEYS.AssignPermissions);
const isValid = screen?.validations[0]?.validate(mockCache);
return <div data-testid="validation-result">{isValid ? "valid" : "invalid"}</div>;
};
renderWithContext(<ValidationTestComponent />);
expect(screen.getByTestId("validation-result")).toHaveTextContent("invalid");
});
it("should fail validation when any cache value is false", () => {
const mockCache = new Map([
["step1", true],
["step2", false],
["step3", true],
]);
const ValidationTestComponent = () => {
const screens = useCreateCopyJobScreensList(mockGoBack);
const screen = screens.find((s) => s.key === SCREEN_KEYS.AssignPermissions);
const isValid = screen?.validations[0]?.validate(mockCache);
return <div data-testid="validation-result">{isValid ? "valid" : "invalid"}</div>;
};
renderWithContext(<ValidationTestComponent />);
expect(screen.getByTestId("validation-result")).toHaveTextContent("invalid");
});
});
});
describe("SCREEN_KEYS constant", () => {
it("should export correct screen keys", () => {
expect(SCREEN_KEYS.CreateCollection).toBe("CreateCollection");
expect(SCREEN_KEYS.SelectAccount).toBe("SelectAccount");
expect(SCREEN_KEYS.SelectSourceAndTargetContainers).toBe("SelectSourceAndTargetContainers");
expect(SCREEN_KEYS.PreviewCopyJob).toBe("PreviewCopyJob");
expect(SCREEN_KEYS.AssignPermissions).toBe("AssignPermissions");
});
});
describe("Component props", () => {
it("should pass explorer to AddCollectionPanelWrapper", () => {
renderWithContext(<TestHookComponent goBack={mockGoBack} />);
expect(screen.getByTestId("add-collection-panel")).toBeInTheDocument();
});
it("should pass goBack function to AddCollectionPanelWrapper", () => {
renderWithContext(<TestHookComponent goBack={mockGoBack} />);
expect(screen.getByTestId("add-collection-panel")).toBeInTheDocument();
});
});
describe("Error handling", () => {
it("should handle context provider error gracefully", () => {
const consoleError = jest.spyOn(console, "error").mockImplementation(() => {});
(useCopyJobContext as jest.Mock).mockImplementation(() => {
throw new Error("Context not found");
});
expect(() => {
render(<TestHookComponent goBack={mockGoBack} />);
}).toThrow("Context not found");
consoleError.mockRestore();
});
});
});

View File

@@ -8,11 +8,11 @@ import SelectAccount from "../Screens/SelectAccount/SelectAccount";
import SelectSourceAndTargetContainers from "../Screens/SelectSourceAndTargetContainers/SelectSourceAndTargetContainers";
const SCREEN_KEYS = {
CreateCollection: "CreateCollection",
SelectAccount: "SelectAccount",
SelectSourceAndTargetContainers: "SelectSourceAndTargetContainers",
PreviewCopyJob: "PreviewCopyJob",
AssignPermissions: "AssignPermissions",
SelectSourceAndTargetContainers: "SelectSourceAndTargetContainers",
CreateCollection: "CreateCollection",
PreviewCopyJob: "PreviewCopyJob",
};
type Validation = {