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,295 @@
import "@testing-library/jest-dom";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { DatabaseAccount } from "Contracts/DataModels";
import { CopyJobContextProviderType } from "Explorer/ContainerCopy/Types/CopyJobTypes";
import React from "react";
import { updateSystemIdentity } from "../../../../../Utils/arm/identityUtils";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { CopyJobContext } from "../../../Context/CopyJobContext";
import AddManagedIdentity from "./AddManagedIdentity";
jest.mock("../../../../../Utils/arm/identityUtils", () => ({
updateSystemIdentity: jest.fn(),
}));
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;
},
}));
jest.mock("../../../CopyJobUtils", () => ({
getAccountDetailsFromResourceId: jest.fn(() => ({
subscriptionId: "test-subscription-id",
resourceGroup: "test-resource-group",
accountName: "test-account-name",
})),
}));
jest.mock("../../../../../Common/Logger", () => ({
logError: jest.fn(),
}));
const mockUpdateSystemIdentity = updateSystemIdentity as jest.MockedFunction<typeof updateSystemIdentity>;
describe("AddManagedIdentity", () => {
const mockCopyJobState = {
jobName: "test-job",
migrationType: "Offline" as any,
source: {
subscription: { subscriptionId: "source-sub-id" },
account: { id: "source-account-id", name: "source-account-name" },
databaseId: "source-db",
containerId: "source-container",
},
target: {
subscriptionId: "target-sub-id",
account: {
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/test-account",
name: "test-target-account",
},
databaseId: "target-db",
containerId: "target-container",
},
sourceReadAccessFromTarget: false,
};
const mockContextValue = {
copyJobState: mockCopyJobState,
setCopyJobState: jest.fn(),
flow: { currentScreen: "AssignPermissions" },
setFlow: jest.fn(),
resetCopyJobState: jest.fn(),
explorer: {} as any,
contextError: "",
setContextError: jest.fn(),
} as unknown as CopyJobContextProviderType;
const renderWithContext = (contextValue = mockContextValue) => {
return render(
<CopyJobContext.Provider value={contextValue}>
<AddManagedIdentity />
</CopyJobContext.Provider>,
);
};
beforeEach(() => {
jest.clearAllMocks();
mockUpdateSystemIdentity.mockResolvedValue({
id: "updated-account-id",
name: "updated-account-name",
} as any);
});
describe("Snapshot Tests", () => {
it("renders initial state correctly", () => {
const { container } = renderWithContext();
expect(container.firstChild).toMatchSnapshot();
});
it("renders with toggle on and popover visible", () => {
const { container } = renderWithContext();
const toggle = screen.getByRole("switch");
fireEvent.click(toggle);
expect(container.firstChild).toMatchSnapshot();
});
it("renders loading state", async () => {
mockUpdateSystemIdentity.mockImplementation(
() => new Promise((resolve) => setTimeout(() => resolve({} as any), 100)),
);
const { container } = renderWithContext();
const toggle = screen.getByRole("switch");
fireEvent.click(toggle);
const primaryButton = screen.getByText("Yes");
fireEvent.click(primaryButton);
expect(container.firstChild).toMatchSnapshot();
});
});
describe("Component Rendering", () => {
it("renders all required elements", () => {
renderWithContext();
expect(screen.getByText(ContainerCopyMessages.addManagedIdentity.description)).toBeInTheDocument();
expect(screen.getByText(ContainerCopyMessages.addManagedIdentity.descriptionHrefText)).toBeInTheDocument();
expect(screen.getByRole("switch")).toBeInTheDocument();
});
it("renders description link with correct href", () => {
renderWithContext();
const link = screen.getByText(ContainerCopyMessages.addManagedIdentity.descriptionHrefText);
expect(link.closest("a")).toHaveAttribute("href", ContainerCopyMessages.addManagedIdentity.descriptionHref);
expect(link.closest("a")).toHaveAttribute("target", "_blank");
expect(link.closest("a")).toHaveAttribute("rel", "noopener noreferrer");
});
it("toggle shows correct initial state", () => {
renderWithContext();
const toggle = screen.getByRole("switch");
expect(toggle).not.toBeChecked();
});
});
describe("Toggle Functionality", () => {
it("toggles state when clicked", () => {
renderWithContext();
const toggle = screen.getByRole("switch");
expect(toggle).not.toBeChecked();
fireEvent.click(toggle);
expect(toggle).toBeChecked();
fireEvent.click(toggle);
expect(toggle).not.toBeChecked();
});
it("shows popover when toggle is on", () => {
renderWithContext();
const toggle = screen.getByRole("switch");
fireEvent.click(toggle);
expect(screen.getByText(ContainerCopyMessages.addManagedIdentity.enablementTitle)).toBeInTheDocument();
});
it("hides popover when toggle is off", () => {
renderWithContext();
const toggle = screen.getByRole("switch");
fireEvent.click(toggle);
fireEvent.click(toggle);
expect(screen.queryByText(ContainerCopyMessages.addManagedIdentity.enablementTitle)).not.toBeInTheDocument();
});
});
describe("Popover Functionality", () => {
beforeEach(() => {
renderWithContext();
const toggle = screen.getByRole("switch");
fireEvent.click(toggle);
});
it("displays correct enablement description with account name", () => {
const expectedDescription = ContainerCopyMessages.addManagedIdentity.enablementDescription(
mockCopyJobState.target.account.name,
);
expect(screen.getByText(expectedDescription)).toBeInTheDocument();
});
it("calls handleAddSystemIdentity when primary button clicked", async () => {
const primaryButton = screen.getByText("Yes");
fireEvent.click(primaryButton);
await waitFor(() => {
expect(mockUpdateSystemIdentity).toHaveBeenCalledWith(
"test-subscription-id",
"test-resource-group",
"test-account-name",
);
});
});
it.skip("closes popover when cancel button clicked", () => {
const cancelButton = screen.getByText("Cancel");
fireEvent.click(cancelButton);
expect(screen.queryByText(ContainerCopyMessages.addManagedIdentity.enablementTitle)).not.toBeInTheDocument();
const toggle = screen.getByRole("switch");
expect(toggle).not.toBeChecked();
});
});
describe("Managed Identity Operations", () => {
it("successfully updates system identity", async () => {
const setCopyJobState = jest.fn();
const contextWithMockSetter = {
...mockContextValue,
setCopyJobState,
};
renderWithContext(contextWithMockSetter);
const toggle = screen.getByRole("switch");
fireEvent.click(toggle);
const primaryButton = screen.getByText("Yes");
fireEvent.click(primaryButton);
await waitFor(() => {
expect(mockUpdateSystemIdentity).toHaveBeenCalled();
});
await waitFor(() => {
expect(setCopyJobState).toHaveBeenCalledWith(expect.any(Function));
});
});
it("handles error during identity update", async () => {
const setContextError = jest.fn();
const contextWithErrorHandler = {
...mockContextValue,
setContextError,
};
const errorMessage = "Failed to update identity";
mockUpdateSystemIdentity.mockRejectedValue(new Error(errorMessage));
renderWithContext(contextWithErrorHandler);
const toggle = screen.getByRole("switch");
fireEvent.click(toggle);
const primaryButton = screen.getByText("Yes");
fireEvent.click(primaryButton);
await waitFor(() => {
expect(setContextError).toHaveBeenCalledWith(errorMessage);
});
});
});
describe("Edge Cases", () => {
it("handles missing target account gracefully", () => {
const contextWithoutTargetAccount = {
...mockContextValue,
copyJobState: {
...mockCopyJobState,
target: {
...mockCopyJobState.target,
account: null as DatabaseAccount | null,
},
},
} as unknown as CopyJobContextProviderType;
expect(() => renderWithContext(contextWithoutTargetAccount)).not.toThrow();
});
});
});

View File

@@ -0,0 +1,503 @@
import "@testing-library/jest-dom";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import React from "react";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { CopyJobContext } from "../../../Context/CopyJobContext";
import { CopyJobContextProviderType } from "../../../Types/CopyJobTypes";
import AddReadPermissionToDefaultIdentity from "./AddReadPermissionToDefaultIdentity";
jest.mock("../../../../../Common/Logger", () => ({
logError: jest.fn(),
}));
jest.mock("../../../../../Utils/arm/RbacUtils", () => ({
assignRole: jest.fn(),
}));
jest.mock("../../../CopyJobUtils", () => ({
getAccountDetailsFromResourceId: jest.fn(),
}));
jest.mock("../Components/InfoTooltip", () => {
const MockInfoTooltip = ({ content }: { content: React.ReactNode }) => {
return <div data-testid="info-tooltip">{content}</div>;
};
MockInfoTooltip.displayName = "MockInfoTooltip";
return MockInfoTooltip;
});
jest.mock("../Components/PopoverContainer", () => {
const MockPopoverContainer = ({
isLoading,
visible,
title,
onCancel,
onPrimary,
children,
}: {
isLoading?: boolean;
visible: boolean;
title: string;
onCancel: () => void;
onPrimary: () => void;
children: React.ReactNode;
}) => {
if (!visible) {
return null;
}
return (
<div data-testid="popover-message" data-loading={isLoading}>
<div data-testid="popover-title">{title}</div>
<div data-testid="popover-content">{children}</div>
<button onClick={onCancel} data-testid="popover-cancel">
Cancel
</button>
<button onClick={onPrimary} data-testid="popover-primary">
Primary
</button>
</div>
);
};
MockPopoverContainer.displayName = "MockPopoverContainer";
return MockPopoverContainer;
});
jest.mock("./hooks/useToggle", () => {
return jest.fn();
});
import { Subscription } from "Contracts/DataModels";
import { CopyJobMigrationType } from "Explorer/ContainerCopy/Enums/CopyJobEnums";
import { logError } from "../../../../../Common/Logger";
import { assignRole, RoleAssignmentType } from "../../../../../Utils/arm/RbacUtils";
import { getAccountDetailsFromResourceId } from "../../../CopyJobUtils";
import useToggle from "./hooks/useToggle";
describe("AddReadPermissionToDefaultIdentity Component", () => {
const mockUseToggle = useToggle as jest.MockedFunction<typeof useToggle>;
const mockAssignRole = assignRole as jest.MockedFunction<typeof assignRole>;
const mockGetAccountDetailsFromResourceId = getAccountDetailsFromResourceId as jest.MockedFunction<
typeof getAccountDetailsFromResourceId
>;
const mockLogError = logError as jest.MockedFunction<typeof logError>;
const mockContextValue: CopyJobContextProviderType = {
copyJobState: {
jobName: "test-job",
migrationType: CopyJobMigrationType.Offline,
source: {
subscription: { subscriptionId: "source-sub-id" } as Subscription,
account: {
id: "/subscriptions/source-sub-id/resourceGroups/source-rg/providers/Microsoft.DocumentDB/databaseAccounts/source-account",
name: "source-account",
location: "East US",
kind: "GlobalDocumentDB",
type: "Microsoft.DocumentDB/databaseAccounts",
properties: {
documentEndpoint: "https://source-account.documents.azure.com:443/",
},
},
databaseId: "source-db",
containerId: "source-container",
},
target: {
subscriptionId: "target-sub-id",
account: {
id: "/subscriptions/target-sub-id/resourceGroups/target-rg/providers/Microsoft.DocumentDB/databaseAccounts/target-account",
name: "target-account",
location: "West US",
kind: "GlobalDocumentDB",
type: "Microsoft.DocumentDB/databaseAccounts",
properties: {
documentEndpoint: "https://target-account.documents.azure.com:443/",
},
identity: {
principalId: "target-principal-id",
type: "SystemAssigned",
},
},
databaseId: "target-db",
containerId: "target-container",
},
sourceReadAccessFromTarget: false,
},
setCopyJobState: jest.fn(),
setContextError: jest.fn(),
contextError: null,
flow: null,
setFlow: jest.fn(),
resetCopyJobState: jest.fn(),
explorer: {} as any,
};
const renderComponent = (contextValue = mockContextValue) => {
return render(
<CopyJobContext.Provider value={contextValue}>
<AddReadPermissionToDefaultIdentity />
</CopyJobContext.Provider>,
);
};
beforeEach(() => {
jest.clearAllMocks();
mockUseToggle.mockReturnValue([false, jest.fn()]);
});
describe("Rendering", () => {
it("should render correctly with default state", () => {
const { container } = renderComponent();
expect(container).toMatchSnapshot();
});
it("should render correctly when toggle is on", () => {
mockUseToggle.mockReturnValue([true, jest.fn()]);
const { container } = renderComponent();
expect(container).toMatchSnapshot();
});
it("should render correctly with different context states", () => {
const contextWithError = {
...mockContextValue,
contextError: "Test error message",
};
const { container } = renderComponent(contextWithError);
expect(container).toMatchSnapshot();
});
it("should render correctly when sourceReadAccessFromTarget is true", () => {
const contextWithAccess = {
...mockContextValue,
copyJobState: {
...mockContextValue.copyJobState,
sourceReadAccessFromTarget: true,
},
};
const { container } = renderComponent(contextWithAccess);
expect(container).toMatchSnapshot();
});
});
describe("Component Structure", () => {
it("should display the description text", () => {
renderComponent();
expect(screen.getByText(ContainerCopyMessages.readPermissionAssigned.description)).toBeInTheDocument();
});
it("should display the info tooltip", () => {
renderComponent();
expect(screen.getByTestId("info-tooltip")).toBeInTheDocument();
});
it("should display the toggle component", () => {
renderComponent();
expect(screen.getByRole("switch")).toBeInTheDocument();
});
});
describe("Toggle Interaction", () => {
it("should call onToggle when toggle is clicked", () => {
const mockOnToggle = jest.fn();
mockUseToggle.mockReturnValue([false, mockOnToggle]);
renderComponent();
const toggle = screen.getByRole("switch");
fireEvent.click(toggle);
expect(mockOnToggle).toHaveBeenCalledTimes(1);
});
it("should show popover when toggle is turned on", () => {
mockUseToggle.mockReturnValue([true, jest.fn()]);
renderComponent();
expect(screen.getByTestId("popover-message")).toBeInTheDocument();
expect(screen.getByTestId("popover-title")).toHaveTextContent(
ContainerCopyMessages.readPermissionAssigned.popoverTitle,
);
expect(screen.getByTestId("popover-content")).toHaveTextContent(
ContainerCopyMessages.readPermissionAssigned.popoverDescription,
);
});
it("should not show popover when toggle is turned off", () => {
mockUseToggle.mockReturnValue([false, jest.fn()]);
renderComponent();
expect(screen.queryByTestId("popover-message")).not.toBeInTheDocument();
});
});
describe("Popover Interactions", () => {
beforeEach(() => {
mockUseToggle.mockReturnValue([true, jest.fn()]);
});
it("should call onToggle with false when cancel button is clicked", () => {
const mockOnToggle = jest.fn();
mockUseToggle.mockReturnValue([true, mockOnToggle]);
renderComponent();
const cancelButton = screen.getByTestId("popover-cancel");
fireEvent.click(cancelButton);
expect(mockOnToggle).toHaveBeenCalledWith(null, false);
});
it("should call handleAddReadPermission when primary button is clicked", async () => {
mockGetAccountDetailsFromResourceId.mockReturnValue({
subscriptionId: "source-sub-id",
resourceGroup: "source-rg",
accountName: "source-account",
});
mockAssignRole.mockResolvedValue({ id: "role-assignment-id" } as RoleAssignmentType);
renderComponent();
const primaryButton = screen.getByTestId("popover-primary");
fireEvent.click(primaryButton);
await waitFor(() => {
expect(mockGetAccountDetailsFromResourceId).toHaveBeenCalledWith(
"/subscriptions/source-sub-id/resourceGroups/source-rg/providers/Microsoft.DocumentDB/databaseAccounts/source-account",
);
});
});
});
describe("handleAddReadPermission Function", () => {
beforeEach(() => {
mockUseToggle.mockReturnValue([true, jest.fn()]);
});
it("should successfully assign role and update context", async () => {
mockGetAccountDetailsFromResourceId.mockReturnValue({
subscriptionId: "source-sub-id",
resourceGroup: "source-rg",
accountName: "source-account",
});
mockAssignRole.mockResolvedValue({ id: "role-assignment-id" } as RoleAssignmentType);
renderComponent();
const primaryButton = screen.getByTestId("popover-primary");
fireEvent.click(primaryButton);
await waitFor(() => {
expect(mockAssignRole).toHaveBeenCalledWith(
"source-sub-id",
"source-rg",
"source-account",
"target-principal-id",
);
});
await waitFor(() => {
expect(mockContextValue.setCopyJobState).toHaveBeenCalledWith(expect.any(Function));
});
});
it("should handle error when assignRole fails", async () => {
mockGetAccountDetailsFromResourceId.mockReturnValue({
subscriptionId: "source-sub-id",
resourceGroup: "source-rg",
accountName: "source-account",
});
mockAssignRole.mockRejectedValue(new Error("Permission denied"));
renderComponent();
const primaryButton = screen.getByTestId("popover-primary");
fireEvent.click(primaryButton);
await waitFor(() => {
expect(mockLogError).toHaveBeenCalledWith(
"Permission denied",
"CopyJob/AddReadPermissionToDefaultIdentity.handleAddReadPermission",
);
});
await waitFor(() => {
expect(mockContextValue.setContextError).toHaveBeenCalledWith("Permission denied");
});
});
it("should handle error without message", async () => {
mockGetAccountDetailsFromResourceId.mockReturnValue({
subscriptionId: "source-sub-id",
resourceGroup: "source-rg",
accountName: "source-account",
});
mockAssignRole.mockRejectedValue({});
renderComponent();
const primaryButton = screen.getByTestId("popover-primary");
fireEvent.click(primaryButton);
await waitFor(() => {
expect(mockLogError).toHaveBeenCalledWith(
"Error assigning read permission to default identity. Please try again later.",
"CopyJob/AddReadPermissionToDefaultIdentity.handleAddReadPermission",
);
});
await waitFor(() => {
expect(mockContextValue.setContextError).toHaveBeenCalledWith(
"Error assigning read permission to default identity. Please try again later.",
);
});
});
it("should show loading state during role assignment", async () => {
mockGetAccountDetailsFromResourceId.mockReturnValue({
subscriptionId: "source-sub-id",
resourceGroup: "source-rg",
accountName: "source-account",
});
mockAssignRole.mockImplementation(
() => new Promise((resolve) => setTimeout(() => resolve({ id: "role-id" } as RoleAssignmentType), 100)),
);
renderComponent();
const primaryButton = screen.getByTestId("popover-primary");
fireEvent.click(primaryButton);
await waitFor(() => {
expect(screen.getByTestId("popover-message")).toHaveAttribute("data-loading", "true");
});
});
it.skip("should not assign role when assignRole returns falsy", async () => {
mockGetAccountDetailsFromResourceId.mockReturnValue({
subscriptionId: "source-sub-id",
resourceGroup: "source-rg",
accountName: "source-account",
});
mockAssignRole.mockResolvedValue(null);
renderComponent();
const primaryButton = screen.getByTestId("popover-primary");
fireEvent.click(primaryButton);
await waitFor(() => {
expect(mockAssignRole).toHaveBeenCalled();
});
expect(mockContextValue.setCopyJobState).not.toHaveBeenCalled();
});
});
describe("Edge Cases", () => {
it("should handle missing target account identity", () => {
const contextWithoutIdentity = {
...mockContextValue,
copyJobState: {
...mockContextValue.copyJobState,
target: {
...mockContextValue.copyJobState.target,
account: {
...mockContextValue.copyJobState.target.account!,
identity: undefined as any,
},
},
},
};
const { container } = renderComponent(contextWithoutIdentity);
expect(container).toMatchSnapshot();
});
it("should handle missing source account", () => {
const contextWithoutSource = {
...mockContextValue,
copyJobState: {
...mockContextValue.copyJobState,
source: {
...mockContextValue.copyJobState.source,
account: null as any,
},
},
};
const { container } = renderComponent(contextWithoutSource);
expect(container).toMatchSnapshot();
});
it("should handle empty string principal ID", async () => {
const contextWithEmptyPrincipal = {
...mockContextValue,
copyJobState: {
...mockContextValue.copyJobState,
target: {
...mockContextValue.copyJobState.target,
account: {
...mockContextValue.copyJobState.target.account!,
identity: {
principalId: "",
type: "SystemAssigned",
},
},
},
},
};
mockUseToggle.mockReturnValue([true, jest.fn()]);
mockGetAccountDetailsFromResourceId.mockReturnValue({
subscriptionId: "source-sub-id",
resourceGroup: "source-rg",
accountName: "source-account",
});
mockAssignRole.mockResolvedValue({ id: "role-assignment-id" } as RoleAssignmentType);
renderComponent(contextWithEmptyPrincipal);
const primaryButton = screen.getByTestId("popover-primary");
fireEvent.click(primaryButton);
await waitFor(() => {
expect(mockAssignRole).toHaveBeenCalledWith("source-sub-id", "source-rg", "source-account", "");
});
});
});
describe("Component Integration", () => {
it("should work with all context updates", async () => {
const setCopyJobStateMock = jest.fn();
const setContextErrorMock = jest.fn();
const fullContextValue = {
...mockContextValue,
setCopyJobState: setCopyJobStateMock,
setContextError: setContextErrorMock,
};
mockUseToggle.mockReturnValue([true, jest.fn()]);
mockGetAccountDetailsFromResourceId.mockReturnValue({
subscriptionId: "source-sub-id",
resourceGroup: "source-rg",
accountName: "source-account",
});
mockAssignRole.mockResolvedValue({ id: "role-assignment-id" } as RoleAssignmentType);
renderComponent(fullContextValue);
const primaryButton = screen.getByTestId("popover-primary");
fireEvent.click(primaryButton);
await waitFor(() => {
expect(setCopyJobStateMock).toHaveBeenCalledWith(expect.any(Function));
});
const setCopyJobStateCall = setCopyJobStateMock.mock.calls[0][0];
const updatedState = setCopyJobStateCall(mockContextValue.copyJobState);
expect(updatedState).toEqual({
...mockContextValue.copyJobState,
sourceReadAccessFromTarget: true,
});
});
});
});

View File

@@ -0,0 +1,379 @@
import "@testing-library/jest-dom";
import { render, RenderResult } from "@testing-library/react";
import React from "react";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { CopyJobContext } from "../../../Context/CopyJobContext";
import { CopyJobMigrationType } from "../../../Enums/CopyJobEnums";
import { CopyJobContextProviderType, CopyJobContextState } from "../../../Types/CopyJobTypes";
import AssignPermissions from "./AssignPermissions";
jest.mock("../../Utils/useCopyJobPrerequisitesCache", () => ({
useCopyJobPrerequisitesCache: () => ({
validationCache: new Map<string, boolean>(),
setValidationCache: jest.fn(),
}),
}));
jest.mock("../../../CopyJobUtils", () => ({
isIntraAccountCopy: jest.fn((sourceId: string, targetId: string) => sourceId === targetId),
}));
jest.mock("./hooks/usePermissionsSection", () => ({
__esModule: true,
default: jest.fn((): any[] => []),
}));
jest.mock("../../../../../Common/ShimmerTree/ShimmerTree", () => {
const MockShimmerTree = (props: any) => {
return (
<div data-testid="shimmer-tree" {...props}>
Loading...
</div>
);
};
MockShimmerTree.displayName = "MockShimmerTree";
return MockShimmerTree;
});
jest.mock("./AddManagedIdentity", () => {
const MockAddManagedIdentity = () => {
return <div data-testid="add-managed-identity">Add Managed Identity Component</div>;
};
MockAddManagedIdentity.displayName = "MockAddManagedIdentity";
return MockAddManagedIdentity;
});
jest.mock("./AddReadPermissionToDefaultIdentity", () => {
const MockAddReadPermissionToDefaultIdentity = () => {
return <div data-testid="add-read-permission">Add Read Permission Component</div>;
};
MockAddReadPermissionToDefaultIdentity.displayName = "MockAddReadPermissionToDefaultIdentity";
return MockAddReadPermissionToDefaultIdentity;
});
jest.mock("./DefaultManagedIdentity", () => {
const MockDefaultManagedIdentity = () => {
return <div data-testid="default-managed-identity">Default Managed Identity Component</div>;
};
MockDefaultManagedIdentity.displayName = "MockDefaultManagedIdentity";
return MockDefaultManagedIdentity;
});
jest.mock("./OnlineCopyEnabled", () => {
const MockOnlineCopyEnabled = () => {
return <div data-testid="online-copy-enabled">Online Copy Enabled Component</div>;
};
MockOnlineCopyEnabled.displayName = "MockOnlineCopyEnabled";
return MockOnlineCopyEnabled;
});
jest.mock("./PointInTimeRestore", () => {
const MockPointInTimeRestore = () => {
return <div data-testid="point-in-time-restore">Point In Time Restore Component</div>;
};
MockPointInTimeRestore.displayName = "MockPointInTimeRestore";
return MockPointInTimeRestore;
});
jest.mock("../../../../../../images/successfulPopup.svg", () => "checkmark-icon");
jest.mock("../../../../../../images/warning.svg", () => "warning-icon");
describe("AssignPermissions Component", () => {
const mockExplorer = {} as any;
const createMockCopyJobState = (overrides: Partial<CopyJobContextState> = {}): CopyJobContextState => ({
jobName: "test-job",
migrationType: CopyJobMigrationType.Offline,
source: {
subscription: { subscriptionId: "source-sub" } as any,
account: { id: "source-account", name: "Source Account" } as any,
databaseId: "source-db",
containerId: "source-container",
},
target: {
subscriptionId: "target-sub",
account: { id: "target-account", name: "Target Account" } as any,
databaseId: "target-db",
containerId: "target-container",
},
sourceReadAccessFromTarget: false,
...overrides,
});
const createMockContextValue = (copyJobState: CopyJobContextState): CopyJobContextProviderType => ({
contextError: null,
setContextError: jest.fn(),
copyJobState,
setCopyJobState: jest.fn(),
flow: null,
setFlow: jest.fn(),
resetCopyJobState: jest.fn(),
explorer: mockExplorer,
});
const renderWithContext = (copyJobState: CopyJobContextState): RenderResult => {
const contextValue = createMockContextValue(copyJobState);
return render(
<CopyJobContext.Provider value={contextValue}>
<AssignPermissions />
</CopyJobContext.Provider>,
);
};
beforeEach(() => {
jest.clearAllMocks();
});
describe("Rendering", () => {
it("should render without crashing with offline migration", () => {
const copyJobState = createMockCopyJobState();
const { container } = renderWithContext(copyJobState);
expect(container.firstChild).toBeTruthy();
expect(container).toMatchSnapshot();
});
it("should render without crashing with online migration", () => {
const copyJobState = createMockCopyJobState({
migrationType: CopyJobMigrationType.Online,
});
const { container } = renderWithContext(copyJobState);
expect(container.firstChild).toBeTruthy();
expect(container).toMatchSnapshot();
});
it("should display shimmer tree when no permission groups are available", () => {
const copyJobState = createMockCopyJobState();
const { getByTestId } = renderWithContext(copyJobState);
expect(getByTestId("shimmer-tree")).toBeInTheDocument();
});
it("should display cross account description for different accounts", () => {
const copyJobState = createMockCopyJobState();
const { getByText } = renderWithContext(copyJobState);
expect(getByText(ContainerCopyMessages.assignPermissions.crossAccountDescription)).toBeInTheDocument();
});
it("should display intra account description for same accounts with online migration", async () => {
const { isIntraAccountCopy } = await import("../../../CopyJobUtils");
(isIntraAccountCopy as jest.Mock).mockReturnValue(true);
const copyJobState = createMockCopyJobState({
migrationType: CopyJobMigrationType.Online,
source: {
subscription: { subscriptionId: "same-sub" } as any,
account: { id: "same-account", name: "Same Account" } as any,
databaseId: "source-db",
containerId: "source-container",
},
target: {
subscriptionId: "same-sub",
account: { id: "same-account", name: "Same Account" } as any,
databaseId: "target-db",
containerId: "target-container",
},
});
const { getByText } = renderWithContext(copyJobState);
expect(
getByText(ContainerCopyMessages.assignPermissions.intraAccountOnlineDescription("Same Account")),
).toBeInTheDocument();
});
});
describe("Permission Groups", () => {
it("should render permission groups when available", async () => {
const mockUsePermissionSections = (await import("./hooks/usePermissionsSection")).default as jest.Mock;
mockUsePermissionSections.mockReturnValue([
{
id: "crossAccountConfigs",
title: "Cross Account Configuration",
description: "Configure permissions for cross-account copy",
sections: [
{
id: "addManagedIdentity",
title: "Add Managed Identity",
Component: () => <div data-testid="add-managed-identity">Add Managed Identity Component</div>,
disabled: false,
completed: true,
},
{
id: "readPermissionAssigned",
title: "Read Permission Assigned",
Component: () => <div data-testid="add-read-permission">Add Read Permission Component</div>,
disabled: false,
completed: false,
},
],
},
]);
const copyJobState = createMockCopyJobState();
const { container } = renderWithContext(copyJobState);
expect(container).toMatchSnapshot();
});
it("should render online migration specific groups", async () => {
const mockUsePermissionSections = (await import("./hooks/usePermissionsSection")).default as jest.Mock;
mockUsePermissionSections.mockReturnValue([
{
id: "onlineConfigs",
title: "Online Configuration",
description: "Configure settings for online migration",
sections: [
{
id: "pointInTimeRestore",
title: "Point In Time Restore",
Component: () => <div data-testid="point-in-time-restore">Point In Time Restore Component</div>,
disabled: false,
completed: true,
},
{
id: "onlineCopyEnabled",
title: "Online Copy Enabled",
Component: () => <div data-testid="online-copy-enabled">Online Copy Enabled Component</div>,
disabled: false,
completed: false,
},
],
},
]);
const copyJobState = createMockCopyJobState({
migrationType: CopyJobMigrationType.Online,
});
const { container } = renderWithContext(copyJobState);
expect(container).toMatchSnapshot();
});
it("should render multiple permission groups", async () => {
const mockUsePermissionSections = (await import("./hooks/usePermissionsSection")).default as jest.Mock;
mockUsePermissionSections.mockReturnValue([
{
id: "crossAccountConfigs",
title: "Cross Account Configuration",
description: "Configure permissions for cross-account copy",
sections: [
{
id: "addManagedIdentity",
title: "Add Managed Identity",
Component: () => <div data-testid="add-managed-identity">Add Managed Identity Component</div>,
disabled: false,
completed: true,
},
],
},
{
id: "onlineConfigs",
title: "Online Configuration",
description: "Configure settings for online migration",
sections: [
{
id: "onlineCopyEnabled",
title: "Online Copy Enabled",
Component: () => <div data-testid="online-copy-enabled">Online Copy Enabled Component</div>,
disabled: false,
completed: false,
},
],
},
]);
const copyJobState = createMockCopyJobState({
migrationType: CopyJobMigrationType.Online,
});
const { container, getByText } = renderWithContext(copyJobState);
expect(getByText("Cross Account Configuration")).toBeInTheDocument();
expect(getByText("Online Configuration")).toBeInTheDocument();
expect(container).toMatchSnapshot();
});
});
describe("Accordion Behavior", () => {
it("should render accordion sections with proper status icons", async () => {
const mockUsePermissionSections = (await import("./hooks/usePermissionsSection")).default as jest.Mock;
mockUsePermissionSections.mockReturnValue([
{
id: "testGroup",
title: "Test Group",
description: "Test Description",
sections: [
{
id: "completedSection",
title: "Completed Section",
Component: () => <div>Completed Component</div>,
disabled: false,
completed: true,
},
{
id: "incompleteSection",
title: "Incomplete Section",
Component: () => <div>Incomplete Component</div>,
disabled: false,
completed: false,
},
{
id: "disabledSection",
title: "Disabled Section",
Component: () => <div>Disabled Component</div>,
disabled: true,
completed: false,
},
],
},
]);
const copyJobState = createMockCopyJobState();
const { container, getByText, getAllByRole } = renderWithContext(copyJobState);
expect(getByText("Completed Section")).toBeInTheDocument();
expect(getByText("Incomplete Section")).toBeInTheDocument();
expect(getByText("Disabled Section")).toBeInTheDocument();
const images = getAllByRole("img");
expect(images.length).toBeGreaterThan(0);
expect(container).toMatchSnapshot();
});
});
describe("Edge Cases", () => {
it("should handle missing account names", () => {
const copyJobState = createMockCopyJobState({
source: {
subscription: { subscriptionId: "source-sub" } as any,
account: { id: "source-account" } as any,
databaseId: "source-db",
containerId: "source-container",
},
});
const { container } = renderWithContext(copyJobState);
expect(container).toMatchSnapshot();
});
it("should calculate correct indent levels for offline migration", () => {
const copyJobState = createMockCopyJobState({
migrationType: CopyJobMigrationType.Offline,
});
const { container } = renderWithContext(copyJobState);
expect(container).toMatchSnapshot();
});
it("should calculate correct indent levels for online migration", () => {
const copyJobState = createMockCopyJobState({
migrationType: CopyJobMigrationType.Online,
});
const { container } = renderWithContext(copyJobState);
expect(container).toMatchSnapshot();
});
});
});

View File

@@ -0,0 +1,355 @@
import "@testing-library/jest-dom";
import { fireEvent, render, screen } from "@testing-library/react";
import React from "react";
import { updateDefaultIdentity } from "../../../../../Utils/arm/identityUtils";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { CopyJobContext } from "../../../Context/CopyJobContext";
import DefaultManagedIdentity from "./DefaultManagedIdentity";
jest.mock("./hooks/useManagedIdentity");
jest.mock("./hooks/useToggle");
jest.mock("../../../../../Utils/arm/identityUtils", () => ({
updateDefaultIdentity: jest.fn(),
}));
jest.mock("../Components/InfoTooltip", () => {
const MockInfoTooltip = ({ content }: { content: React.ReactNode }) => {
return <div data-testid="info-tooltip">{content}</div>;
};
MockInfoTooltip.displayName = "MockInfoTooltip";
return MockInfoTooltip;
});
jest.mock("../Components/PopoverContainer", () => {
const MockPopoverContainer = ({
children,
isLoading,
visible,
title,
onCancel,
onPrimary,
}: {
children: React.ReactNode;
isLoading: boolean;
visible: boolean;
title: string;
onCancel: () => void;
onPrimary: () => void;
}) => {
if (!visible) {
return null;
}
return (
<div data-testid="popover-message">
<div data-testid="popover-title">{title}</div>
<div data-testid="popover-content">{children}</div>
<div data-testid="popover-loading">{isLoading ? "Loading" : "Not Loading"}</div>
<button data-testid="popover-cancel" onClick={onCancel}>
Cancel
</button>
<button data-testid="popover-primary" onClick={onPrimary}>
Primary
</button>
</div>
);
};
MockPopoverContainer.displayName = "MockPopoverContainer";
return MockPopoverContainer;
});
import { DatabaseAccount } from "Contracts/DataModels";
import { CopyJobContextProviderType } from "Explorer/ContainerCopy/Types/CopyJobTypes";
import useManagedIdentity from "./hooks/useManagedIdentity";
import useToggle from "./hooks/useToggle";
const mockUseManagedIdentity = useManagedIdentity as jest.MockedFunction<typeof useManagedIdentity>;
const mockUseToggle = useToggle as jest.MockedFunction<typeof useToggle>;
describe("DefaultManagedIdentity", () => {
const mockCopyJobContextValue = {
copyJobState: {
target: {
account: {
name: "test-cosmos-account",
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/test-cosmos-account",
},
},
},
setCopyJobState: jest.fn(),
setContextError: jest.fn(),
contextError: "",
flow: {},
setFlow: jest.fn(),
resetCopyJobState: jest.fn(),
explorer: {} as any,
};
const mockHandleAddSystemIdentity = jest.fn();
const mockOnToggle = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
mockUseManagedIdentity.mockReturnValue({
loading: false,
handleAddSystemIdentity: mockHandleAddSystemIdentity,
});
mockUseToggle.mockReturnValue([false, mockOnToggle]);
});
const renderComponent = (contextValue = mockCopyJobContextValue) => {
return render(
<CopyJobContext.Provider value={contextValue as unknown as CopyJobContextProviderType}>
<DefaultManagedIdentity />
</CopyJobContext.Provider>,
);
};
describe("Rendering", () => {
it("should render correctly with default state", () => {
const { container } = renderComponent();
expect(container).toMatchSnapshot();
});
it("should render the description with account name", () => {
renderComponent();
const description = screen.getByText(
/Set the system-assigned managed identity as default for "test-cosmos-account"/,
);
expect(description).toBeInTheDocument();
});
it("should render the info tooltip", () => {
renderComponent();
const tooltip = screen.getByTestId("info-tooltip");
expect(tooltip).toBeInTheDocument();
expect(tooltip).toHaveTextContent("Learn more about");
expect(tooltip).toHaveTextContent("Default Managed Identities.");
});
it("should render the toggle button with correct initial state", () => {
renderComponent();
const toggle = screen.getByRole("switch");
expect(toggle).toBeInTheDocument();
expect(toggle).not.toBeChecked();
});
it("should not show popover when toggle is false", () => {
renderComponent();
const popover = screen.queryByTestId("popover-message");
expect(popover).not.toBeInTheDocument();
});
});
describe("Toggle Interactions", () => {
it("should call onToggle when toggle is clicked", () => {
renderComponent();
const toggle = screen.getByRole("switch");
fireEvent.click(toggle);
expect(mockOnToggle).toHaveBeenCalledTimes(1);
});
it("should show popover when toggle is true", () => {
mockUseToggle.mockReturnValue([true, mockOnToggle]);
renderComponent();
const popover = screen.getByTestId("popover-message");
expect(popover).toBeInTheDocument();
const title = screen.getByTestId("popover-title");
expect(title).toHaveTextContent(ContainerCopyMessages.defaultManagedIdentity.popoverTitle);
const content = screen.getByTestId("popover-content");
expect(content).toHaveTextContent(
/Assign the system-assigned managed identity as the default for "test-cosmos-account"/,
);
});
it("should render toggle with checked state when toggle is true", () => {
mockUseToggle.mockReturnValue([true, mockOnToggle]);
const { container } = renderComponent();
expect(container).toMatchSnapshot();
});
});
describe("Loading States", () => {
it("should show loading state in popover when loading is true", () => {
mockUseToggle.mockReturnValue([true, mockOnToggle]);
mockUseManagedIdentity.mockReturnValue({
loading: true,
handleAddSystemIdentity: mockHandleAddSystemIdentity,
});
renderComponent();
const loadingIndicator = screen.getByTestId("popover-loading");
expect(loadingIndicator).toHaveTextContent("Loading");
});
it("should not show loading state when loading is false", () => {
mockUseToggle.mockReturnValue([true, mockOnToggle]);
renderComponent();
const loadingIndicator = screen.getByTestId("popover-loading");
expect(loadingIndicator).toHaveTextContent("Not Loading");
});
it("should render loading state snapshot", () => {
mockUseToggle.mockReturnValue([true, mockOnToggle]);
mockUseManagedIdentity.mockReturnValue({
loading: true,
handleAddSystemIdentity: mockHandleAddSystemIdentity,
});
const { container } = renderComponent();
expect(container).toMatchSnapshot();
});
});
describe("Popover Interactions", () => {
beforeEach(() => {
mockUseToggle.mockReturnValue([true, mockOnToggle]);
});
it("should call onToggle with false when cancel button is clicked", () => {
renderComponent();
const cancelButton = screen.getByTestId("popover-cancel");
fireEvent.click(cancelButton);
expect(mockOnToggle).toHaveBeenCalledWith(null, false);
});
it("should call handleAddSystemIdentity when primary button is clicked", () => {
renderComponent();
const primaryButton = screen.getByTestId("popover-primary");
fireEvent.click(primaryButton);
expect(mockHandleAddSystemIdentity).toHaveBeenCalledTimes(1);
});
it("should handle primary button click correctly when loading", async () => {
mockUseManagedIdentity.mockReturnValue({
loading: true,
handleAddSystemIdentity: mockHandleAddSystemIdentity,
});
renderComponent();
const primaryButton = screen.getByTestId("popover-primary");
fireEvent.click(primaryButton);
expect(mockHandleAddSystemIdentity).toHaveBeenCalledTimes(1);
});
});
describe("Edge Cases", () => {
it("should handle missing account name gracefully", () => {
const contextValueWithoutAccount = {
...mockCopyJobContextValue,
copyJobState: {
target: {
account: {
name: "",
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/",
},
},
},
};
const { container } = renderComponent(contextValueWithoutAccount);
expect(container).toMatchSnapshot();
});
it("should handle null account", () => {
const contextValueWithNullAccount = {
...mockCopyJobContextValue,
copyJobState: {
target: {
account: null as DatabaseAccount | null,
},
},
};
const { container } = renderComponent(contextValueWithNullAccount);
expect(container).toMatchSnapshot();
});
});
describe("Hook Integration", () => {
it("should pass updateDefaultIdentity to useManagedIdentity hook", () => {
renderComponent();
expect(mockUseManagedIdentity).toHaveBeenCalledWith(updateDefaultIdentity);
});
it("should initialize useToggle with false", () => {
renderComponent();
expect(mockUseToggle).toHaveBeenCalledWith(false);
});
});
describe("Accessibility", () => {
it("should have proper ARIA attributes", () => {
renderComponent();
const toggle = screen.getByRole("switch");
expect(toggle).toBeInTheDocument();
});
it("should have proper link accessibility", () => {
renderComponent();
const link = screen.getByRole("link");
expect(link).toHaveAttribute("target", "_blank");
expect(link).toHaveAttribute("rel", "noopener noreferrer");
});
});
describe("Component Structure", () => {
it("should have correct CSS class", () => {
const { container } = renderComponent();
const componentContainer = container.querySelector(".defaultManagedIdentityContainer");
expect(componentContainer).toBeInTheDocument();
});
it("should render all required FluentUI components", () => {
renderComponent();
expect(screen.getByRole("switch")).toBeInTheDocument();
expect(screen.getByRole("link")).toBeInTheDocument();
});
});
describe("Messages and Text Content", () => {
it("should display correct toggle button text", () => {
renderComponent();
const onText = screen.queryByText(ContainerCopyMessages.toggleBtn.onText);
const offText = screen.queryByText(ContainerCopyMessages.toggleBtn.offText);
expect(onText || offText).toBeTruthy();
});
it("should display correct link text in tooltip", () => {
renderComponent();
const linkText = screen.getByText(ContainerCopyMessages.defaultManagedIdentity.tooltip.hrefText);
expect(linkText).toBeInTheDocument();
});
});
});

View File

@@ -27,7 +27,7 @@ const DefaultManagedIdentity: React.FC<AddManagedIdentityProps> = () => {
return (
<Stack className="defaultManagedIdentityContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>
<div className="toggle-label">
{ContainerCopyMessages.defaultManagedIdentity.description(copyJobState?.target?.account.name)} &nbsp;
{ContainerCopyMessages.defaultManagedIdentity.description(copyJobState?.target?.account?.name)} &nbsp;
<InfoTooltip content={managedIdentityTooltip} />
</div>
<Toggle
@@ -48,7 +48,7 @@ const DefaultManagedIdentity: React.FC<AddManagedIdentityProps> = () => {
onCancel={() => onToggle(null, false)}
onPrimary={handleAddSystemIdentity}
>
{ContainerCopyMessages.defaultManagedIdentity.popoverDescription(copyJobState?.target?.account.name)}
{ContainerCopyMessages.defaultManagedIdentity.popoverDescription(copyJobState?.target?.account?.name)}
</PopoverMessage>
</Stack>
);

View File

@@ -0,0 +1,579 @@
import "@testing-library/jest-dom";
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { DatabaseAccount } from "Contracts/DataModels";
import { CopyJobContextProviderType } from "Explorer/ContainerCopy/Types/CopyJobTypes";
import React from "react";
import { fetchDatabaseAccount } from "Utils/arm/databaseAccountUtils";
import { CapabilityNames } from "../../../../../Common/Constants";
import { logError } from "../../../../../Common/Logger";
import { update as updateDatabaseAccount } from "../../../../../Utils/arm/generatedClients/cosmos/databaseAccounts";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { CopyJobContext } from "../../../Context/CopyJobContext";
import OnlineCopyEnabled from "./OnlineCopyEnabled";
jest.mock("Utils/arm/databaseAccountUtils", () => ({
fetchDatabaseAccount: jest.fn(),
}));
jest.mock("../../../../../Utils/arm/generatedClients/cosmos/databaseAccounts", () => ({
update: jest.fn(),
}));
jest.mock("../../../../../Common/Logger", () => ({
logError: jest.fn(),
}));
jest.mock("../../../../../Common/LoadingOverlay", () => {
const MockLoadingOverlay = ({ isLoading, label }: { isLoading: boolean; label: string }) => {
return isLoading ? <div data-testid="loading-overlay">{label}</div> : null;
};
MockLoadingOverlay.displayName = "MockLoadingOverlay";
return MockLoadingOverlay;
});
const mockFetchDatabaseAccount = fetchDatabaseAccount as jest.MockedFunction<typeof fetchDatabaseAccount>;
const mockUpdateDatabaseAccount = updateDatabaseAccount as jest.MockedFunction<typeof updateDatabaseAccount>;
const mockLogError = logError as jest.MockedFunction<typeof logError>;
describe("OnlineCopyEnabled", () => {
const mockSetContextError = jest.fn();
const mockSetCopyJobState = jest.fn();
const mockSourceAccount: DatabaseAccount = {
id: "/subscriptions/test-sub-id/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/test-account",
name: "test-account",
location: "East US",
type: "Microsoft.DocumentDB/databaseAccounts",
kind: "GlobalDocumentDB",
properties: {
capabilities: [],
enableAllVersionsAndDeletesChangeFeed: false,
locations: [],
writeLocations: [],
readLocations: [],
},
};
const mockCopyJobContextValue = {
copyJobState: {
source: {
account: mockSourceAccount,
},
},
setCopyJobState: mockSetCopyJobState,
setContextError: mockSetContextError,
contextError: "",
flow: { currentScreen: "" },
setFlow: jest.fn(),
resetCopyJobState: jest.fn(),
explorer: {} as any,
} as unknown as CopyJobContextProviderType;
beforeEach(() => {
jest.clearAllMocks();
jest.clearAllTimers();
jest.useFakeTimers();
});
afterEach(() => {
jest.runOnlyPendingTimers();
jest.useRealTimers();
});
const renderComponent = (contextValue = mockCopyJobContextValue) => {
return render(
<CopyJobContext.Provider value={contextValue}>
<OnlineCopyEnabled />
</CopyJobContext.Provider>,
);
};
describe("Rendering", () => {
it("should render correctly with initial state", () => {
const { container } = renderComponent();
expect(container).toMatchSnapshot();
});
it("should render the description with account name", () => {
renderComponent();
const description = screen.getByText(ContainerCopyMessages.onlineCopyEnabled.description("test-account"));
expect(description).toBeInTheDocument();
});
it("should render the learn more link", () => {
renderComponent();
const link = screen.getByRole("link", {
name: ContainerCopyMessages.onlineCopyEnabled.hrefText,
});
expect(link).toBeInTheDocument();
expect(link).toHaveAttribute("href", ContainerCopyMessages.onlineCopyEnabled.href);
expect(link).toHaveAttribute("target", "_blank");
expect(link).toHaveAttribute("rel", "noopener noreferrer");
});
it("should render the enable button with correct text when not loading", () => {
renderComponent();
const button = screen.getByRole("button", {
name: ContainerCopyMessages.onlineCopyEnabled.buttonText,
});
expect(button).toBeInTheDocument();
expect(button).not.toBeDisabled();
});
it("should not show loading overlay initially", () => {
renderComponent();
const loadingOverlay = screen.queryByTestId("loading-overlay");
expect(loadingOverlay).not.toBeInTheDocument();
});
it("should not show refresh button initially", () => {
renderComponent();
const refreshButton = screen.queryByRole("button", {
name: ContainerCopyMessages.refreshButtonLabel,
});
expect(refreshButton).not.toBeInTheDocument();
});
});
describe("Enable Online Copy Flow", () => {
it("should handle complete enable online copy flow successfully", async () => {
const accountAfterChangeFeedUpdate = {
...mockSourceAccount,
properties: {
...mockSourceAccount.properties,
enableAllVersionsAndDeletesChangeFeed: true,
},
};
const accountWithOnlineCopyEnabled: DatabaseAccount = {
...accountAfterChangeFeedUpdate,
properties: {
...accountAfterChangeFeedUpdate.properties,
capabilities: [{ name: CapabilityNames.EnableOnlineCopyFeature, description: "Enables online copy feature" }],
},
};
mockFetchDatabaseAccount
.mockResolvedValueOnce(mockSourceAccount)
.mockResolvedValueOnce(accountWithOnlineCopyEnabled);
mockUpdateDatabaseAccount.mockResolvedValue({} as any);
renderComponent();
const enableButton = screen.getByRole("button", {
name: ContainerCopyMessages.onlineCopyEnabled.buttonText,
});
await act(async () => {
fireEvent.click(enableButton);
});
expect(screen.getByTestId("loading-overlay")).toBeInTheDocument();
await waitFor(() => {
expect(mockFetchDatabaseAccount).toHaveBeenCalledWith("test-sub-id", "test-rg", "test-account");
});
await waitFor(() => {
expect(mockUpdateDatabaseAccount).toHaveBeenCalledWith("test-sub-id", "test-rg", "test-account", {
properties: {
enableAllVersionsAndDeletesChangeFeed: true,
},
});
});
await waitFor(() => {
expect(mockUpdateDatabaseAccount).toHaveBeenCalledWith("test-sub-id", "test-rg", "test-account", {
properties: {
capabilities: [{ name: CapabilityNames.EnableOnlineCopyFeature }],
},
});
});
});
it("should skip change feed enablement if already enabled", async () => {
const accountWithChangeFeedEnabled = {
...mockSourceAccount,
properties: {
...mockSourceAccount.properties,
enableAllVersionsAndDeletesChangeFeed: true,
},
};
const accountWithOnlineCopyEnabled: DatabaseAccount = {
...accountWithChangeFeedEnabled,
properties: {
...accountWithChangeFeedEnabled.properties,
capabilities: [{ name: CapabilityNames.EnableOnlineCopyFeature, description: "Enables online copy feature" }],
},
};
mockFetchDatabaseAccount
.mockResolvedValueOnce(accountWithChangeFeedEnabled)
.mockResolvedValueOnce(accountWithOnlineCopyEnabled);
mockUpdateDatabaseAccount.mockResolvedValue({} as any);
renderComponent();
const enableButton = screen.getByRole("button", {
name: ContainerCopyMessages.onlineCopyEnabled.buttonText,
});
await act(async () => {
fireEvent.click(enableButton);
});
await waitFor(() => {
expect(mockUpdateDatabaseAccount).toHaveBeenCalledTimes(1);
expect(mockUpdateDatabaseAccount).toHaveBeenCalledWith("test-sub-id", "test-rg", "test-account", {
properties: {
capabilities: [{ name: CapabilityNames.EnableOnlineCopyFeature }],
},
});
});
});
it("should show correct loading messages during the process", async () => {
mockFetchDatabaseAccount.mockResolvedValue(mockSourceAccount);
mockUpdateDatabaseAccount.mockImplementation(() => new Promise(() => {}));
renderComponent();
const enableButton = screen.getByRole("button", {
name: ContainerCopyMessages.onlineCopyEnabled.buttonText,
});
await act(async () => {
fireEvent.click(enableButton);
});
await waitFor(() => {
expect(mockFetchDatabaseAccount).toHaveBeenCalled();
});
await waitFor(() => {
expect(
screen.getByText(ContainerCopyMessages.onlineCopyEnabled.enablingOnlineCopySpinnerLabel("test-account")),
).toBeInTheDocument();
});
});
it("should handle error during update operations", async () => {
const errorMessage = "Failed to update account";
mockFetchDatabaseAccount.mockResolvedValue(mockSourceAccount);
mockUpdateDatabaseAccount.mockRejectedValue(new Error(errorMessage));
renderComponent();
const enableButton = screen.getByRole("button", {
name: ContainerCopyMessages.onlineCopyEnabled.buttonText,
});
await act(async () => {
fireEvent.click(enableButton);
});
await waitFor(() => {
expect(mockLogError).toHaveBeenCalledWith(errorMessage, "CopyJob/OnlineCopyEnabled.handleOnlineCopyEnable");
expect(mockSetContextError).toHaveBeenCalledWith(errorMessage);
});
expect(screen.queryByTestId("loading-overlay")).not.toBeInTheDocument();
});
it("should handle refresh button click", async () => {
const accountWithOnlineCopyEnabled: DatabaseAccount = {
...mockSourceAccount,
properties: {
...mockSourceAccount.properties,
capabilities: [{ name: CapabilityNames.EnableOnlineCopyFeature, description: "Enables online copy feature" }],
},
};
mockFetchDatabaseAccount
.mockResolvedValueOnce(mockSourceAccount)
.mockResolvedValueOnce(mockSourceAccount)
.mockResolvedValueOnce(accountWithOnlineCopyEnabled);
mockUpdateDatabaseAccount.mockResolvedValue({} as any);
renderComponent();
const enableButton = screen.getByRole("button", {
name: ContainerCopyMessages.onlineCopyEnabled.buttonText,
});
await act(async () => {
fireEvent.click(enableButton);
});
await act(async () => {
jest.advanceTimersByTime(10 * 60 * 1000);
});
const refreshButton = screen.getByRole("button", {
name: ContainerCopyMessages.refreshButtonLabel,
});
await act(async () => {
fireEvent.click(refreshButton);
});
expect(screen.getByTestId("loading-overlay")).toBeInTheDocument();
await waitFor(() => {
expect(mockSetCopyJobState).toHaveBeenCalled();
});
});
});
describe("Account Validation and State Updates", () => {
it("should update state when account capabilities change", async () => {
const accountWithOnlineCopyEnabled: DatabaseAccount = {
...mockSourceAccount,
properties: {
...mockSourceAccount.properties,
capabilities: [{ name: CapabilityNames.EnableOnlineCopyFeature, description: "Enables online copy feature" }],
},
};
mockFetchDatabaseAccount.mockResolvedValue(accountWithOnlineCopyEnabled);
mockUpdateDatabaseAccount.mockResolvedValue({} as any);
renderComponent();
const enableButton = screen.getByRole("button", {
name: ContainerCopyMessages.onlineCopyEnabled.buttonText,
});
await act(async () => {
fireEvent.click(enableButton);
});
await act(async () => {
jest.advanceTimersByTime(30000);
});
await waitFor(() => {
expect(mockSetCopyJobState).toHaveBeenCalledWith(expect.any(Function));
});
const stateUpdateFunction = mockSetCopyJobState.mock.calls[0][0];
const newState = stateUpdateFunction({
source: { account: mockSourceAccount },
});
expect(newState.source.account).toEqual(accountWithOnlineCopyEnabled);
});
it("should not update state when account capabilities remain unchanged", async () => {
mockFetchDatabaseAccount.mockResolvedValue(mockSourceAccount);
mockUpdateDatabaseAccount.mockResolvedValue({} as any);
renderComponent();
const enableButton = screen.getByRole("button", {
name: ContainerCopyMessages.onlineCopyEnabled.buttonText,
});
await act(async () => {
fireEvent.click(enableButton);
});
await act(async () => {
jest.advanceTimersByTime(30000);
});
expect(mockSetCopyJobState).not.toHaveBeenCalled();
});
});
describe("Button States and Interactions", () => {
it("should disable button during loading", async () => {
mockFetchDatabaseAccount.mockImplementation(() => new Promise(() => {}));
renderComponent();
const enableButton = screen.getByRole("button", {
name: ContainerCopyMessages.onlineCopyEnabled.buttonText,
});
await act(async () => {
fireEvent.click(enableButton);
});
const loadingButton = screen.getByRole("button");
expect(loadingButton).toBeDisabled();
});
it("should show sync icon during loading", async () => {
mockFetchDatabaseAccount.mockImplementation(() => new Promise(() => {}));
renderComponent();
const enableButton = screen.getByRole("button", {
name: ContainerCopyMessages.onlineCopyEnabled.buttonText,
});
await act(async () => {
fireEvent.click(enableButton);
});
const loadingButton = screen.getByRole("button");
expect(loadingButton.querySelector("[data-icon-name='SyncStatusSolid']")).toBeInTheDocument();
});
it("should disable refresh button during loading", async () => {
mockFetchDatabaseAccount.mockResolvedValue(mockSourceAccount);
mockUpdateDatabaseAccount.mockResolvedValue({} as any);
renderComponent();
const enableButton = screen.getByRole("button", {
name: ContainerCopyMessages.onlineCopyEnabled.buttonText,
});
await act(async () => {
fireEvent.click(enableButton);
});
await act(async () => {
jest.advanceTimersByTime(10 * 60 * 1000);
});
mockFetchDatabaseAccount.mockImplementation(() => new Promise(() => {}));
const refreshButton = screen.getByRole("button", {
name: ContainerCopyMessages.refreshButtonLabel,
});
await act(async () => {
fireEvent.click(refreshButton);
});
expect(refreshButton).toBeDisabled();
});
});
describe("Edge Cases", () => {
it("should handle missing account name gracefully", () => {
const contextWithoutAccountName = {
...mockCopyJobContextValue,
copyJobState: {
source: {
account: {
...mockSourceAccount,
name: "",
},
},
},
} as CopyJobContextProviderType;
const { container } = renderComponent(contextWithoutAccountName);
expect(container).toMatchSnapshot();
});
it("should handle null account", () => {
const contextWithNullAccount = {
...mockCopyJobContextValue,
copyJobState: {
source: {
account: null as DatabaseAccount | null,
},
},
} as CopyJobContextProviderType;
const { container } = renderComponent(contextWithNullAccount);
expect(container).toMatchSnapshot();
});
it("should handle account with existing online copy capability", () => {
const accountWithExistingCapability = {
...mockSourceAccount,
properties: {
...mockSourceAccount.properties,
capabilities: [{ name: CapabilityNames.EnableOnlineCopyFeature }, { name: "SomeOtherCapability" }],
},
};
const contextWithExistingCapability = {
...mockCopyJobContextValue,
copyJobState: {
source: {
account: accountWithExistingCapability,
},
},
} as CopyJobContextProviderType;
const { container } = renderComponent(contextWithExistingCapability);
expect(container).toMatchSnapshot();
});
it("should handle account with no capabilities array", () => {
const accountWithNoCapabilities = {
...mockSourceAccount,
properties: {
...mockSourceAccount.properties,
capabilities: undefined,
},
} as DatabaseAccount;
const contextWithNoCapabilities = {
...mockCopyJobContextValue,
copyJobState: {
source: {
account: accountWithNoCapabilities,
},
},
} as CopyJobContextProviderType;
renderComponent(contextWithNoCapabilities);
const enableButton = screen.getByRole("button", {
name: ContainerCopyMessages.onlineCopyEnabled.buttonText,
});
expect(enableButton).toBeInTheDocument();
});
});
describe("Accessibility", () => {
it("should have proper button role and accessibility attributes", () => {
renderComponent();
const button = screen.getByRole("button", {
name: ContainerCopyMessages.onlineCopyEnabled.buttonText,
});
expect(button).toBeInTheDocument();
});
it("should have proper link accessibility", () => {
renderComponent();
const link = screen.getByRole("link");
expect(link).toHaveAttribute("target", "_blank");
expect(link).toHaveAttribute("rel", "noopener noreferrer");
});
});
describe("CSS Classes and Styling", () => {
it("should apply correct CSS class to container", () => {
const { container } = renderComponent();
const onlineCopyContainer = container.querySelector(".onlineCopyContainer");
expect(onlineCopyContainer).toBeInTheDocument();
});
it("should apply fullWidth class to buttons", () => {
renderComponent();
const button = screen.getByRole("button");
expect(button).toHaveClass("fullWidth");
});
});
});

View File

@@ -32,7 +32,7 @@ const OnlineCopyEnabled: React.FC = () => {
subscriptionId: sourceSubscriptionId,
resourceGroup: sourceResourceGroup,
accountName: sourceAccountName,
} = getAccountDetailsFromResourceId(selectedSourceAccount?.id);
} = getAccountDetailsFromResourceId(selectedSourceAccount?.id) || {};
const handleFetchAccount = async () => {
try {
@@ -91,12 +91,6 @@ const OnlineCopyEnabled: React.FC = () => {
});
}
setLoaderMessage(ContainerCopyMessages.onlineCopyEnabled.enablingOnlineCopySpinnerLabel(sourceAccountName));
await updateDatabaseAccount(sourceSubscriptionId, sourceResourceGroup, sourceAccountName, {
properties: {
enableAllVersionsAndDeletesChangeFeed: true,
},
});
await updateDatabaseAccount(sourceSubscriptionId, sourceResourceGroup, sourceAccountName, {
properties: {
capabilities: [...sourceAccountCapabilities, { name: CapabilityNames.EnableOnlineCopyFeature }],

View File

@@ -0,0 +1,341 @@
import "@testing-library/jest-dom";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { logError } from "Common/Logger";
import { DatabaseAccount } from "Contracts/DataModels";
import React from "react";
import { fetchDatabaseAccount } from "Utils/arm/databaseAccountUtils";
import { CopyJobContext } from "../../../Context/CopyJobContext";
import { CopyJobMigrationType } from "../../../Enums/CopyJobEnums";
import { CopyJobContextProviderType, CopyJobContextState } from "../../../Types/CopyJobTypes";
import PointInTimeRestore from "./PointInTimeRestore";
jest.mock("Utils/arm/databaseAccountUtils");
jest.mock("Common/Logger");
const mockFetchDatabaseAccount = fetchDatabaseAccount as jest.MockedFunction<typeof fetchDatabaseAccount>;
const mockLogError = logError as jest.MockedFunction<typeof logError>;
const mockWindowOpen = jest.fn();
Object.defineProperty(window, "open", {
value: mockWindowOpen,
writable: true,
});
global.clearInterval = jest.fn();
global.clearTimeout = jest.fn();
describe("PointInTimeRestore", () => {
const mockSourceAccount: DatabaseAccount = {
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/test-account",
name: "test-account",
type: "Microsoft.DocumentDB/databaseAccounts",
location: "East US",
properties: {
backupPolicy: {
type: "Continuous",
},
},
} as DatabaseAccount;
const mockUpdatedAccount: DatabaseAccount = {
...mockSourceAccount,
properties: {
backupPolicy: {
type: "Periodic",
},
},
} as DatabaseAccount;
const defaultCopyJobState = {
jobName: "test-job",
migrationType: CopyJobMigrationType.Offline,
source: {
subscription: { subscriptionId: "test-sub", displayName: "Test Subscription" },
account: mockSourceAccount,
databaseId: "test-db",
containerId: "test-container",
},
target: {
subscriptionId: "test-sub",
account: mockSourceAccount,
databaseId: "target-db",
containerId: "target-container",
},
sourceReadAccessFromTarget: false,
} as CopyJobContextState;
const mockSetCopyJobState = jest.fn();
const createMockContext = (overrides?: Partial<CopyJobContextProviderType>): CopyJobContextProviderType => ({
copyJobState: defaultCopyJobState,
setCopyJobState: mockSetCopyJobState,
flow: null,
setFlow: jest.fn(),
contextError: null,
setContextError: jest.fn(),
resetCopyJobState: jest.fn(),
...overrides,
});
const renderWithContext = (contextValue: CopyJobContextProviderType) => {
return render(
<CopyJobContext.Provider value={contextValue}>
<PointInTimeRestore />
</CopyJobContext.Provider>,
);
};
beforeEach(() => {
jest.clearAllMocks();
mockFetchDatabaseAccount.mockClear();
mockLogError.mockClear();
mockWindowOpen.mockClear();
mockSetCopyJobState.mockClear();
});
afterEach(() => {
jest.clearAllTimers();
});
describe("Initial Render", () => {
it("should render correctly with default props", () => {
const mockContext = createMockContext();
const { container } = renderWithContext(mockContext);
expect(container).toMatchSnapshot();
});
it("should display the correct description with account name", () => {
const mockContext = createMockContext();
renderWithContext(mockContext);
expect(screen.getByText(/test-account/)).toBeInTheDocument();
});
it("should show the primary action button with correct text", () => {
const mockContext = createMockContext();
renderWithContext(mockContext);
const button = screen.getByRole("button");
expect(button).toBeInTheDocument();
expect(button).not.toBeDisabled();
});
it("should render with empty account name gracefully", () => {
const contextWithoutAccount = createMockContext({
copyJobState: {
...defaultCopyJobState,
source: {
...defaultCopyJobState.source,
account: { ...mockSourceAccount, name: "" },
},
},
});
const { container } = renderWithContext(contextWithoutAccount);
expect(container).toMatchSnapshot();
});
});
describe("Button Interactions", () => {
it("should open window and start monitoring when button is clicked", () => {
const mockContext = createMockContext();
renderWithContext(mockContext);
const button = screen.getByRole("button");
fireEvent.click(button);
expect(mockWindowOpen).toHaveBeenCalledWith(
expect.stringMatching(
/#resource\/subscriptions\/test-sub\/resourceGroups\/test-rg\/providers\/Microsoft.DocumentDB\/databaseAccounts\/test-account\/backupRestore$/,
),
"_blank",
);
});
it("should disable button and show loading state after click", () => {
const mockContext = createMockContext();
renderWithContext(mockContext);
const button = screen.getByRole("button");
fireEvent.click(button);
expect(button).toBeDisabled();
expect(screen.getByText(/Please wait while we process your request/)).toBeInTheDocument();
});
it("should show refresh button when timeout occurs", async () => {
jest.useFakeTimers();
const mockContext = createMockContext();
renderWithContext(mockContext);
const button = screen.getByRole("button");
fireEvent.click(button);
jest.advanceTimersByTime(10 * 60 * 1000 + 1000);
await waitFor(() => {
expect(screen.getByText(/Refresh/)).toBeInTheDocument();
});
jest.useRealTimers();
});
it("should fetch account periodically after button click", async () => {
jest.useFakeTimers();
mockFetchDatabaseAccount.mockResolvedValue(mockUpdatedAccount);
const mockContext = createMockContext();
renderWithContext(mockContext);
const button = screen.getByRole("button");
fireEvent.click(button);
jest.advanceTimersByTime(30 * 1000);
await waitFor(() => {
expect(mockFetchDatabaseAccount).toHaveBeenCalledWith("test-sub", "test-rg", "test-account");
});
jest.useRealTimers();
});
it("should not update context when account validation fails", async () => {
jest.useFakeTimers();
mockFetchDatabaseAccount.mockResolvedValue(mockSourceAccount);
const mockContext = createMockContext();
renderWithContext(mockContext);
const button = screen.getByRole("button");
fireEvent.click(button);
jest.advanceTimersByTime(30 * 1000);
await waitFor(() => {
expect(mockFetchDatabaseAccount).toHaveBeenCalled();
});
expect(mockSetCopyJobState).not.toHaveBeenCalled();
jest.useRealTimers();
});
});
describe("Refresh Button Functionality", () => {
it("should handle refresh button click", async () => {
jest.useFakeTimers();
mockFetchDatabaseAccount.mockResolvedValue(mockUpdatedAccount);
const mockContext = createMockContext();
renderWithContext(mockContext);
const button = screen.getByRole("button");
fireEvent.click(button);
jest.advanceTimersByTime(10 * 60 * 1000 + 1000);
await waitFor(() => {
const refreshButton = screen.getByText(/Refresh/);
expect(refreshButton).toBeInTheDocument();
});
const refreshButton = screen.getByText(/Refresh/);
fireEvent.click(refreshButton);
await waitFor(() => {
expect(mockFetchDatabaseAccount).toHaveBeenCalledWith("test-sub", "test-rg", "test-account");
});
jest.useRealTimers();
});
it("should show loading state during refresh", async () => {
jest.useFakeTimers();
mockFetchDatabaseAccount.mockImplementation(
() => new Promise((resolve) => setTimeout(() => resolve(mockUpdatedAccount), 1000)),
);
const mockContext = createMockContext();
renderWithContext(mockContext);
const button = screen.getByRole("button");
fireEvent.click(button);
jest.advanceTimersByTime(10 * 60 * 1000 + 1000);
await waitFor(() => {
expect(screen.getByText(/Refresh/)).toBeInTheDocument();
});
const refreshButton = screen.getByText(/Refresh/);
fireEvent.click(refreshButton);
expect(screen.getByText(/Please wait while we process your request/)).toBeInTheDocument();
jest.useRealTimers();
});
});
describe("Edge Cases", () => {
it("should handle missing source account gracefully", () => {
const contextWithoutSourceAccount = createMockContext({
copyJobState: {
...defaultCopyJobState,
source: {
...defaultCopyJobState.source,
account: null as any,
},
},
});
const { container } = renderWithContext(contextWithoutSourceAccount);
expect(container).toMatchSnapshot();
});
it("should handle missing account ID gracefully", () => {
const contextWithoutAccountId = createMockContext({
copyJobState: {
...defaultCopyJobState,
source: {
...defaultCopyJobState.source,
account: { ...mockSourceAccount, id: undefined as any },
},
},
});
const { container } = renderWithContext(contextWithoutAccountId);
expect(container).toMatchSnapshot();
});
});
describe("Snapshots", () => {
it("should match snapshot in loading state", () => {
const mockContext = createMockContext();
const { container } = renderWithContext(mockContext);
const button = screen.getByRole("button");
fireEvent.click(button);
expect(container).toMatchSnapshot();
});
it("should match snapshot with refresh button", async () => {
jest.useFakeTimers();
const mockContext = createMockContext();
const { container } = renderWithContext(mockContext);
const button = screen.getByRole("button");
fireEvent.click(button);
jest.advanceTimersByTime(10 * 60 * 1000 + 1000);
await waitFor(() => {
expect(screen.getByText(/Refresh/)).toBeInTheDocument();
});
expect(container).toMatchSnapshot();
jest.useRealTimers();
});
});
});

View File

@@ -31,7 +31,11 @@ const PointInTimeRestore: React.FC = () => {
const [showRefreshButton, setShowRefreshButton] = useState(false);
const intervalRef = useRef<NodeJS.Timeout | null>(null);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const { copyJobState: { source } = {}, setCopyJobState } = useCopyJobContext();
const { copyJobState: { source } = {}, setCopyJobState, setContextError } = useCopyJobContext();
if (!source?.account?.id) {
setContextError("Invalid source account. Please select a valid source account for Point-in-Time Restore.");
return null;
}
const sourceAccountLink = buildResourceLink(source?.account);
const featureUrl = `${sourceAccountLink}/backupRestore`;
const selectedSourceAccount = source?.account;
@@ -39,7 +43,7 @@ const PointInTimeRestore: React.FC = () => {
subscriptionId: sourceSubscriptionId,
resourceGroup: sourceResourceGroup,
accountName: sourceAccountName,
} = getAccountDetailsFromResourceId(selectedSourceAccount?.id);
} = getAccountDetailsFromResourceId(selectedSourceAccount?.id) || {};
useEffect(() => {
return () => {

View File

@@ -0,0 +1,406 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AddManagedIdentity Snapshot Tests renders initial state correctly 1`] = `
<div
class="ms-Stack addManagedIdentityContainer css-109"
>
<span
class="css-110"
>
A system-assigned managed identity is restricted to one per resource and is tied to the lifecycle of this resource. Once enabled, you can grant permissions to the managed identity by using Azure role-based access control (Azure RBAC). The managed identity is authenticated with Microsoft Entra ID, so you dont have to store any credentials in code.
<a
class="ms-Link root-111"
href="https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview"
rel="noopener noreferrer"
target="_blank"
>
Learn more about Managed identities.
</a>
 
<div
class="ms-TooltipHost root-105"
role="none"
>
<div
class="ms-Image root-112"
style="width: 14px; height: 14px;"
>
<img
alt="Information"
class="ms-Image-image ms-Image-image--portrait is-notLoaded is-fadeIn image-113"
src="[object Object]"
/>
</div>
<div
hidden=""
id="tooltip0"
style="position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0px; border: 0px; overflow: hidden; white-space: nowrap;"
>
<span
class="css-110"
>
Learn more about
 
<a
class="ms-Link root-111"
href="https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview"
rel="noopener noreferrer"
target="_blank"
>
Managed Identities.
</a>
</span>
</div>
</div>
</span>
<div
class="ms-Toggle is-enabled root-114"
>
<div
class="ms-Toggle-innerContainer container-116"
>
<button
aria-checked="false"
aria-labelledby="Toggle1-stateText"
class="ms-Toggle-background pill-117"
data-is-focusable="true"
data-ktp-target="true"
id="Toggle1"
role="switch"
type="button"
>
<span
class="ms-Toggle-thumb thumb-118"
/>
</button>
<label
class="ms-Label ms-Toggle-stateText text-120"
for="Toggle1"
id="Toggle1-stateText"
>
Off
</label>
</div>
</div>
</div>
`;
exports[`AddManagedIdentity Snapshot Tests renders loading state 1`] = `
<div
class="ms-Stack addManagedIdentityContainer css-109"
>
<span
class="css-110"
>
A system-assigned managed identity is restricted to one per resource and is tied to the lifecycle of this resource. Once enabled, you can grant permissions to the managed identity by using Azure role-based access control (Azure RBAC). The managed identity is authenticated with Microsoft Entra ID, so you dont have to store any credentials in code.
<a
class="ms-Link root-111"
href="https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview"
rel="noopener noreferrer"
target="_blank"
>
Learn more about Managed identities.
</a>
 
<div
class="ms-TooltipHost root-105"
role="none"
>
<div
class="ms-Image root-112"
style="width: 14px; height: 14px;"
>
<img
alt="Information"
class="ms-Image-image ms-Image-image--portrait is-notLoaded is-fadeIn image-113"
src="[object Object]"
/>
</div>
<div
hidden=""
id="tooltip10"
style="position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0px; border: 0px; overflow: hidden; white-space: nowrap;"
>
<span
class="css-110"
>
Learn more about
 
<a
class="ms-Link root-111"
href="https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview"
rel="noopener noreferrer"
target="_blank"
>
Managed Identities.
</a>
</span>
</div>
</div>
</span>
<div
class="ms-Toggle is-checked is-enabled root-114"
>
<div
class="ms-Toggle-innerContainer container-116"
>
<button
aria-checked="true"
aria-labelledby="Toggle11-stateText"
class="ms-Toggle-background pill-121"
data-is-focusable="true"
data-ktp-target="true"
id="Toggle11"
role="switch"
type="button"
>
<span
class="ms-Toggle-thumb thumb-122"
/>
</button>
<label
class="ms-Label ms-Toggle-stateText text-120"
for="Toggle11"
id="Toggle11-stateText"
>
On
</label>
</div>
</div>
<div
class="ms-Stack popover-container foreground loading css-123"
style="max-width: 450px;"
>
<div
class="ms-Overlay root-135"
>
<div
class="ms-Spinner root-137"
>
<div
class="ms-Spinner-circle ms-Spinner--large circle-138"
/>
<div
class="ms-Spinner-label label-139"
>
Please wait while we process your request...
</div>
</div>
</div>
<span
class="css-124"
style="font-weight: 600;"
>
Enable system assigned managed identity
</span>
<span
class="css-110"
>
Enable system-assigned managed identity on the test-target-account. To confirm, click the "Yes" button.
</span>
<div
class="ms-Stack css-125"
>
<button
aria-disabled="true"
class="ms-Button ms-Button--primary is-disabled root-140"
data-is-focusable="false"
disabled=""
type="button"
>
<span
class="ms-Button-flexContainer flexContainer-127"
data-automationid="splitbuttonprimary"
>
<span
class="ms-Button-textContainer textContainer-128"
>
<span
class="ms-Button-label label-130"
id="id__12"
>
Yes
</span>
</span>
</span>
</button>
<button
aria-disabled="true"
class="ms-Button ms-Button--default is-disabled root-143"
data-is-focusable="false"
disabled=""
type="button"
>
<span
class="ms-Button-flexContainer flexContainer-127"
data-automationid="splitbuttonprimary"
>
<span
class="ms-Button-textContainer textContainer-128"
>
<span
class="ms-Button-label label-130"
id="id__15"
>
No
</span>
</span>
</span>
</button>
</div>
</div>
</div>
`;
exports[`AddManagedIdentity Snapshot Tests renders with toggle on and popover visible 1`] = `
<div
class="ms-Stack addManagedIdentityContainer css-109"
>
<span
class="css-110"
>
A system-assigned managed identity is restricted to one per resource and is tied to the lifecycle of this resource. Once enabled, you can grant permissions to the managed identity by using Azure role-based access control (Azure RBAC). The managed identity is authenticated with Microsoft Entra ID, so you dont have to store any credentials in code.
<a
class="ms-Link root-111"
href="https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview"
rel="noopener noreferrer"
target="_blank"
>
Learn more about Managed identities.
</a>
 
<div
class="ms-TooltipHost root-105"
role="none"
>
<div
class="ms-Image root-112"
style="width: 14px; height: 14px;"
>
<img
alt="Information"
class="ms-Image-image ms-Image-image--portrait is-notLoaded is-fadeIn image-113"
src="[object Object]"
/>
</div>
<div
hidden=""
id="tooltip2"
style="position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0px; border: 0px; overflow: hidden; white-space: nowrap;"
>
<span
class="css-110"
>
Learn more about
 
<a
class="ms-Link root-111"
href="https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview"
rel="noopener noreferrer"
target="_blank"
>
Managed Identities.
</a>
</span>
</div>
</div>
</span>
<div
class="ms-Toggle is-checked is-enabled root-114"
>
<div
class="ms-Toggle-innerContainer container-116"
>
<button
aria-checked="true"
aria-labelledby="Toggle3-stateText"
class="ms-Toggle-background pill-121"
data-is-focusable="true"
data-ktp-target="true"
id="Toggle3"
role="switch"
type="button"
>
<span
class="ms-Toggle-thumb thumb-122"
/>
</button>
<label
class="ms-Label ms-Toggle-stateText text-120"
for="Toggle3"
id="Toggle3-stateText"
>
On
</label>
</div>
</div>
<div
class="ms-Stack popover-container foreground css-123"
style="max-width: 450px;"
>
<span
class="css-124"
style="font-weight: 600;"
>
Enable system assigned managed identity
</span>
<span
class="css-110"
>
Enable system-assigned managed identity on the test-target-account. To confirm, click the "Yes" button.
</span>
<div
class="ms-Stack css-125"
>
<button
class="ms-Button ms-Button--primary root-126"
data-is-focusable="true"
type="button"
>
<span
class="ms-Button-flexContainer flexContainer-127"
data-automationid="splitbuttonprimary"
>
<span
class="ms-Button-textContainer textContainer-128"
>
<span
class="ms-Button-label label-130"
id="id__4"
>
Yes
</span>
</span>
</span>
</button>
<button
class="ms-Button ms-Button--default root-134"
data-is-focusable="true"
type="button"
>
<span
class="ms-Button-flexContainer flexContainer-127"
data-automationid="splitbuttonprimary"
>
<span
class="ms-Button-textContainer textContainer-128"
>
<span
class="ms-Button-label label-130"
id="id__7"
>
No
</span>
</span>
</span>
</button>
</div>
</div>
</div>
`;

View File

@@ -0,0 +1,398 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle missing source account 1`] = `
<div>
<div
class="ms-Stack defaultManagedIdentityContainer css-109"
>
<span
class="toggle-label css-110"
>
To allow data copy from source to the destination container, provide read access of the source account to the default identity of the destination account.
<div
data-testid="info-tooltip"
>
<span
class="css-110"
>
Learn more about
 
<a
class="ms-Link root-111"
href="https://learn.microsoft.com/azure/cosmos-db/nosql/how-to-connect-role-based-access-control"
rel="noopener noreferrer"
target="_blank"
>
Read permissions.
</a>
</span>
</div>
</span>
<div
class="ms-Toggle is-enabled root-112"
>
<div
class="ms-Toggle-innerContainer container-114"
>
<button
aria-checked="false"
aria-labelledby="Toggle17-stateText"
class="ms-Toggle-background pill-115"
data-is-focusable="true"
data-ktp-target="true"
id="Toggle17"
role="switch"
type="button"
>
<span
class="ms-Toggle-thumb thumb-116"
/>
</button>
<label
class="ms-Label ms-Toggle-stateText text-118"
for="Toggle17"
id="Toggle17-stateText"
>
Off
</label>
</div>
</div>
</div>
</div>
`;
exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle missing target account identity 1`] = `
<div>
<div
class="ms-Stack defaultManagedIdentityContainer css-109"
>
<span
class="toggle-label css-110"
>
To allow data copy from source to the destination container, provide read access of the source account to the default identity of the destination account.
<div
data-testid="info-tooltip"
>
<span
class="css-110"
>
Learn more about
 
<a
class="ms-Link root-111"
href="https://learn.microsoft.com/azure/cosmos-db/nosql/how-to-connect-role-based-access-control"
rel="noopener noreferrer"
target="_blank"
>
Read permissions.
</a>
</span>
</div>
</span>
<div
class="ms-Toggle is-enabled root-112"
>
<div
class="ms-Toggle-innerContainer container-114"
>
<button
aria-checked="false"
aria-labelledby="Toggle16-stateText"
class="ms-Toggle-background pill-115"
data-is-focusable="true"
data-ktp-target="true"
id="Toggle16"
role="switch"
type="button"
>
<span
class="ms-Toggle-thumb thumb-116"
/>
</button>
<label
class="ms-Label ms-Toggle-stateText text-118"
for="Toggle16"
id="Toggle16-stateText"
>
Off
</label>
</div>
</div>
</div>
</div>
`;
exports[`AddReadPermissionToDefaultIdentity Component Rendering should render correctly when sourceReadAccessFromTarget is true 1`] = `
<div>
<div
class="ms-Stack defaultManagedIdentityContainer css-109"
>
<span
class="toggle-label css-110"
>
To allow data copy from source to the destination container, provide read access of the source account to the default identity of the destination account.
<div
data-testid="info-tooltip"
>
<span
class="css-110"
>
Learn more about
 
<a
class="ms-Link root-111"
href="https://learn.microsoft.com/azure/cosmos-db/nosql/how-to-connect-role-based-access-control"
rel="noopener noreferrer"
target="_blank"
>
Read permissions.
</a>
</span>
</div>
</span>
<div
class="ms-Toggle is-enabled root-112"
>
<div
class="ms-Toggle-innerContainer container-114"
>
<button
aria-checked="false"
aria-labelledby="Toggle3-stateText"
class="ms-Toggle-background pill-115"
data-is-focusable="true"
data-ktp-target="true"
id="Toggle3"
role="switch"
type="button"
>
<span
class="ms-Toggle-thumb thumb-116"
/>
</button>
<label
class="ms-Label ms-Toggle-stateText text-118"
for="Toggle3"
id="Toggle3-stateText"
>
Off
</label>
</div>
</div>
</div>
</div>
`;
exports[`AddReadPermissionToDefaultIdentity Component Rendering should render correctly when toggle is on 1`] = `
<div>
<div
class="ms-Stack defaultManagedIdentityContainer css-109"
>
<span
class="toggle-label css-110"
>
To allow data copy from source to the destination container, provide read access of the source account to the default identity of the destination account.
<div
data-testid="info-tooltip"
>
<span
class="css-110"
>
Learn more about
 
<a
class="ms-Link root-111"
href="https://learn.microsoft.com/azure/cosmos-db/nosql/how-to-connect-role-based-access-control"
rel="noopener noreferrer"
target="_blank"
>
Read permissions.
</a>
</span>
</div>
</span>
<div
class="ms-Toggle is-checked is-enabled root-112"
>
<div
class="ms-Toggle-innerContainer container-114"
>
<button
aria-checked="true"
aria-labelledby="Toggle1-stateText"
class="ms-Toggle-background pill-119"
data-is-focusable="true"
data-ktp-target="true"
id="Toggle1"
role="switch"
type="button"
>
<span
class="ms-Toggle-thumb thumb-120"
/>
</button>
<label
class="ms-Label ms-Toggle-stateText text-118"
for="Toggle1"
id="Toggle1-stateText"
>
On
</label>
</div>
</div>
<div
data-loading="false"
data-testid="popover-message"
>
<div
data-testid="popover-title"
>
Read permissions assigned to default identity.
</div>
<div
data-testid="popover-content"
>
Assign read permissions of the source account to the default identity of the destination account. To confirm click the “Yes” button.
</div>
<button
data-testid="popover-cancel"
>
Cancel
</button>
<button
data-testid="popover-primary"
>
Primary
</button>
</div>
</div>
</div>
`;
exports[`AddReadPermissionToDefaultIdentity Component Rendering should render correctly with default state 1`] = `
<div>
<div
class="ms-Stack defaultManagedIdentityContainer css-109"
>
<span
class="toggle-label css-110"
>
To allow data copy from source to the destination container, provide read access of the source account to the default identity of the destination account.
<div
data-testid="info-tooltip"
>
<span
class="css-110"
>
Learn more about
 
<a
class="ms-Link root-111"
href="https://learn.microsoft.com/azure/cosmos-db/nosql/how-to-connect-role-based-access-control"
rel="noopener noreferrer"
target="_blank"
>
Read permissions.
</a>
</span>
</div>
</span>
<div
class="ms-Toggle is-enabled root-112"
>
<div
class="ms-Toggle-innerContainer container-114"
>
<button
aria-checked="false"
aria-labelledby="Toggle0-stateText"
class="ms-Toggle-background pill-115"
data-is-focusable="true"
data-ktp-target="true"
id="Toggle0"
role="switch"
type="button"
>
<span
class="ms-Toggle-thumb thumb-116"
/>
</button>
<label
class="ms-Label ms-Toggle-stateText text-118"
for="Toggle0"
id="Toggle0-stateText"
>
Off
</label>
</div>
</div>
</div>
</div>
`;
exports[`AddReadPermissionToDefaultIdentity Component Rendering should render correctly with different context states 1`] = `
<div>
<div
class="ms-Stack defaultManagedIdentityContainer css-109"
>
<span
class="toggle-label css-110"
>
To allow data copy from source to the destination container, provide read access of the source account to the default identity of the destination account.
<div
data-testid="info-tooltip"
>
<span
class="css-110"
>
Learn more about
 
<a
class="ms-Link root-111"
href="https://learn.microsoft.com/azure/cosmos-db/nosql/how-to-connect-role-based-access-control"
rel="noopener noreferrer"
target="_blank"
>
Read permissions.
</a>
</span>
</div>
</span>
<div
class="ms-Toggle is-enabled root-112"
>
<div
class="ms-Toggle-innerContainer container-114"
>
<button
aria-checked="false"
aria-labelledby="Toggle2-stateText"
class="ms-Toggle-background pill-115"
data-is-focusable="true"
data-ktp-target="true"
id="Toggle2"
role="switch"
type="button"
>
<span
class="ms-Toggle-thumb thumb-116"
/>
</button>
<label
class="ms-Label ms-Toggle-stateText text-118"
for="Toggle2"
id="Toggle2-stateText"
>
Off
</label>
</div>
</div>
</div>
</div>
`;

View File

@@ -0,0 +1,369 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DefaultManagedIdentity Edge Cases should handle missing account name gracefully 1`] = `
<div>
<div
class="ms-Stack defaultManagedIdentityContainer css-109"
>
<div
class="toggle-label"
>
Set the system-assigned managed identity as default for "" by switching it on.
 
<div
data-testid="info-tooltip"
>
<span
class="css-110"
>
Learn more about
 
<a
class="ms-Link root-111"
href="https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview"
rel="noopener noreferrer"
target="_blank"
>
Default Managed Identities.
</a>
</span>
</div>
</div>
<div
class="ms-Toggle is-enabled root-112"
>
<div
class="ms-Toggle-innerContainer container-114"
>
<button
aria-checked="false"
aria-labelledby="Toggle14-stateText"
class="ms-Toggle-background pill-115"
data-is-focusable="true"
data-ktp-target="true"
id="Toggle14"
role="switch"
type="button"
>
<span
class="ms-Toggle-thumb thumb-116"
/>
</button>
<label
class="ms-Label ms-Toggle-stateText text-118"
for="Toggle14"
id="Toggle14-stateText"
>
Off
</label>
</div>
</div>
</div>
</div>
`;
exports[`DefaultManagedIdentity Edge Cases should handle null account 1`] = `
<div>
<div
class="ms-Stack defaultManagedIdentityContainer css-109"
>
<div
class="toggle-label"
>
Set the system-assigned managed identity as default for "undefined" by switching it on.
 
<div
data-testid="info-tooltip"
>
<span
class="css-110"
>
Learn more about
 
<a
class="ms-Link root-111"
href="https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview"
rel="noopener noreferrer"
target="_blank"
>
Default Managed Identities.
</a>
</span>
</div>
</div>
<div
class="ms-Toggle is-enabled root-112"
>
<div
class="ms-Toggle-innerContainer container-114"
>
<button
aria-checked="false"
aria-labelledby="Toggle15-stateText"
class="ms-Toggle-background pill-115"
data-is-focusable="true"
data-ktp-target="true"
id="Toggle15"
role="switch"
type="button"
>
<span
class="ms-Toggle-thumb thumb-116"
/>
</button>
<label
class="ms-Label ms-Toggle-stateText text-118"
for="Toggle15"
id="Toggle15-stateText"
>
Off
</label>
</div>
</div>
</div>
</div>
`;
exports[`DefaultManagedIdentity Loading States should render loading state snapshot 1`] = `
<div>
<div
class="ms-Stack defaultManagedIdentityContainer css-109"
>
<div
class="toggle-label"
>
Set the system-assigned managed identity as default for "test-cosmos-account" by switching it on.
 
<div
data-testid="info-tooltip"
>
<span
class="css-110"
>
Learn more about
 
<a
class="ms-Link root-111"
href="https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview"
rel="noopener noreferrer"
target="_blank"
>
Default Managed Identities.
</a>
</span>
</div>
</div>
<div
class="ms-Toggle is-checked is-enabled root-112"
>
<div
class="ms-Toggle-innerContainer container-114"
>
<button
aria-checked="true"
aria-labelledby="Toggle10-stateText"
class="ms-Toggle-background pill-119"
data-is-focusable="true"
data-ktp-target="true"
id="Toggle10"
role="switch"
type="button"
>
<span
class="ms-Toggle-thumb thumb-120"
/>
</button>
<label
class="ms-Label ms-Toggle-stateText text-118"
for="Toggle10"
id="Toggle10-stateText"
>
On
</label>
</div>
</div>
<div
data-testid="popover-message"
>
<div
data-testid="popover-title"
>
System assigned managed identity set as default
</div>
<div
data-testid="popover-content"
>
Assign the system-assigned managed identity as the default for "test-cosmos-account". To confirm, click the "Yes" button.
</div>
<div
data-testid="popover-loading"
>
Loading
</div>
<button
data-testid="popover-cancel"
>
Cancel
</button>
<button
data-testid="popover-primary"
>
Primary
</button>
</div>
</div>
</div>
`;
exports[`DefaultManagedIdentity Rendering should render correctly with default state 1`] = `
<div>
<div
class="ms-Stack defaultManagedIdentityContainer css-109"
>
<div
class="toggle-label"
>
Set the system-assigned managed identity as default for "test-cosmos-account" by switching it on.
 
<div
data-testid="info-tooltip"
>
<span
class="css-110"
>
Learn more about
 
<a
class="ms-Link root-111"
href="https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview"
rel="noopener noreferrer"
target="_blank"
>
Default Managed Identities.
</a>
</span>
</div>
</div>
<div
class="ms-Toggle is-enabled root-112"
>
<div
class="ms-Toggle-innerContainer container-114"
>
<button
aria-checked="false"
aria-labelledby="Toggle0-stateText"
class="ms-Toggle-background pill-115"
data-is-focusable="true"
data-ktp-target="true"
id="Toggle0"
role="switch"
type="button"
>
<span
class="ms-Toggle-thumb thumb-116"
/>
</button>
<label
class="ms-Label ms-Toggle-stateText text-118"
for="Toggle0"
id="Toggle0-stateText"
>
Off
</label>
</div>
</div>
</div>
</div>
`;
exports[`DefaultManagedIdentity Toggle Interactions should render toggle with checked state when toggle is true 1`] = `
<div>
<div
class="ms-Stack defaultManagedIdentityContainer css-109"
>
<div
class="toggle-label"
>
Set the system-assigned managed identity as default for "test-cosmos-account" by switching it on.
 
<div
data-testid="info-tooltip"
>
<span
class="css-110"
>
Learn more about
 
<a
class="ms-Link root-111"
href="https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview"
rel="noopener noreferrer"
target="_blank"
>
Default Managed Identities.
</a>
</span>
</div>
</div>
<div
class="ms-Toggle is-checked is-enabled root-112"
>
<div
class="ms-Toggle-innerContainer container-114"
>
<button
aria-checked="true"
aria-labelledby="Toggle7-stateText"
class="ms-Toggle-background pill-119"
data-is-focusable="true"
data-ktp-target="true"
id="Toggle7"
role="switch"
type="button"
>
<span
class="ms-Toggle-thumb thumb-120"
/>
</button>
<label
class="ms-Label ms-Toggle-stateText text-118"
for="Toggle7"
id="Toggle7-stateText"
>
On
</label>
</div>
</div>
<div
data-testid="popover-message"
>
<div
data-testid="popover-title"
>
System assigned managed identity set as default
</div>
<div
data-testid="popover-content"
>
Assign the system-assigned managed identity as the default for "test-cosmos-account". To confirm, click the "Yes" button.
</div>
<div
data-testid="popover-loading"
>
Not Loading
</div>
<button
data-testid="popover-cancel"
>
Cancel
</button>
<button
data-testid="popover-primary"
>
Primary
</button>
</div>
</div>
</div>
`;

View File

@@ -0,0 +1,193 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`OnlineCopyEnabled Edge Cases should handle account with existing online copy capability 1`] = `
<div>
<div
class="ms-Stack onlineCopyContainer css-109"
>
<div
class="ms-StackItem info-message css-110"
>
Enable online container copy by clicking the button below on your "test-account" account.
<a
class="ms-Link root-111"
href="https://learn.microsoft.com/en-us/azure/cosmos-db/container-copy?tabs=online-copy&pivots=api-nosql#enable-online-copy"
rel="noopener noreferrer"
target="_blank"
>
Learn more about online copy jobs
</a>
</div>
<div
class="ms-StackItem css-110"
>
<button
class="ms-Button ms-Button--primary fullWidth root-112"
data-is-focusable="true"
type="button"
>
<span
class="ms-Button-flexContainer flexContainer-113"
data-automationid="splitbuttonprimary"
>
<span
class="ms-Button-textContainer textContainer-114"
>
<span
class="ms-Button-label label-116"
id="id__54"
>
Enable Online Copy
</span>
</span>
</span>
</button>
</div>
</div>
</div>
`;
exports[`OnlineCopyEnabled Edge Cases should handle missing account name gracefully 1`] = `
<div>
<div
class="ms-Stack onlineCopyContainer css-109"
>
<div
class="ms-StackItem info-message css-110"
>
Enable online container copy by clicking the button below on your "" account.
<a
class="ms-Link root-111"
href="https://learn.microsoft.com/en-us/azure/cosmos-db/container-copy?tabs=online-copy&pivots=api-nosql#enable-online-copy"
rel="noopener noreferrer"
target="_blank"
>
Learn more about online copy jobs
</a>
</div>
<div
class="ms-StackItem css-110"
>
<button
class="ms-Button ms-Button--primary fullWidth root-112"
data-is-focusable="true"
type="button"
>
<span
class="ms-Button-flexContainer flexContainer-113"
data-automationid="splitbuttonprimary"
>
<span
class="ms-Button-textContainer textContainer-114"
>
<span
class="ms-Button-label label-116"
id="id__48"
>
Enable Online Copy
</span>
</span>
</span>
</button>
</div>
</div>
</div>
`;
exports[`OnlineCopyEnabled Edge Cases should handle null account 1`] = `
<div>
<div
class="ms-Stack onlineCopyContainer css-109"
>
<div
class="ms-StackItem info-message css-110"
>
Enable online container copy by clicking the button below on your "" account.
<a
class="ms-Link root-111"
href="https://learn.microsoft.com/en-us/azure/cosmos-db/container-copy?tabs=online-copy&pivots=api-nosql#enable-online-copy"
rel="noopener noreferrer"
target="_blank"
>
Learn more about online copy jobs
</a>
</div>
<div
class="ms-StackItem css-110"
>
<button
class="ms-Button ms-Button--primary fullWidth root-112"
data-is-focusable="true"
type="button"
>
<span
class="ms-Button-flexContainer flexContainer-113"
data-automationid="splitbuttonprimary"
>
<span
class="ms-Button-textContainer textContainer-114"
>
<span
class="ms-Button-label label-116"
id="id__51"
>
Enable Online Copy
</span>
</span>
</span>
</button>
</div>
</div>
</div>
`;
exports[`OnlineCopyEnabled Rendering should render correctly with initial state 1`] = `
<div>
<div
class="ms-Stack onlineCopyContainer css-109"
>
<div
class="ms-StackItem info-message css-110"
>
Enable online container copy by clicking the button below on your "test-account" account.
<a
class="ms-Link root-111"
href="https://learn.microsoft.com/en-us/azure/cosmos-db/container-copy?tabs=online-copy&pivots=api-nosql#enable-online-copy"
rel="noopener noreferrer"
target="_blank"
>
Learn more about online copy jobs
</a>
</div>
<div
class="ms-StackItem css-110"
>
<button
class="ms-Button ms-Button--primary fullWidth root-112"
data-is-focusable="true"
type="button"
>
<span
class="ms-Button-flexContainer flexContainer-113"
data-automationid="splitbuttonprimary"
>
<span
class="ms-Button-textContainer textContainer-114"
>
<span
class="ms-Button-label label-116"
id="id__0"
>
Enable Online Copy
</span>
</span>
</span>
</button>
</div>
</div>
</div>
`;

View File

@@ -0,0 +1,333 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PointInTimeRestore Edge Cases should handle missing account ID gracefully 1`] = `<div />`;
exports[`PointInTimeRestore Edge Cases should handle missing source account gracefully 1`] = `<div />`;
exports[`PointInTimeRestore Initial Render should render correctly with default props 1`] = `
<div>
<div
class="ms-Stack pointInTimeRestoreContainer css-109"
>
<div
class="ms-StackItem toggle-label css-110"
>
To facilitate online container copy jobs, please update your "test-account" backup policy from periodic to continuous backup. Enabling continuous backup is required for this functionality.
<div
class="ms-TooltipHost root-105"
role="none"
>
<div
class="ms-Image root-111"
style="width: 14px; height: 14px;"
>
<img
alt="Information"
class="ms-Image-image ms-Image-image--portrait is-notLoaded is-fadeIn image-112"
src="[object Object]"
/>
</div>
<div
hidden=""
id="tooltip0"
style="position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0px; border: 0px; overflow: hidden; white-space: nowrap;"
>
<span
class="css-113"
>
Learn more about
 
<a
class="ms-Link root-114"
href="https://learn.microsoft.com/en-us/azure/cosmos-db/continuous-backup-restore-introduction"
rel="noopener noreferrer"
target="_blank"
>
Continuous Backup
</a>
</span>
</div>
</div>
</div>
<div
class="ms-StackItem css-110"
>
<button
class="ms-Button ms-Button--primary fullWidth root-115"
data-is-focusable="true"
type="button"
>
<span
class="ms-Button-flexContainer flexContainer-116"
data-automationid="splitbuttonprimary"
>
<span
class="ms-Button-textContainer textContainer-117"
>
<span
class="ms-Button-label label-119"
id="id__1"
>
Enable Point In Time Restore
</span>
</span>
</span>
</button>
</div>
</div>
</div>
`;
exports[`PointInTimeRestore Initial Render should render with empty account name gracefully 1`] = `
<div>
<div
class="ms-Stack pointInTimeRestoreContainer css-109"
>
<div
class="ms-StackItem toggle-label css-110"
>
To facilitate online container copy jobs, please update your "" backup policy from periodic to continuous backup. Enabling continuous backup is required for this functionality.
<div
class="ms-TooltipHost root-105"
role="none"
>
<div
class="ms-Image root-111"
style="width: 14px; height: 14px;"
>
<img
alt="Information"
class="ms-Image-image ms-Image-image--portrait is-notLoaded is-fadeIn image-112"
src="[object Object]"
/>
</div>
<div
hidden=""
id="tooltip12"
style="position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0px; border: 0px; overflow: hidden; white-space: nowrap;"
>
<span
class="css-113"
>
Learn more about
 
<a
class="ms-Link root-114"
href="https://learn.microsoft.com/en-us/azure/cosmos-db/continuous-backup-restore-introduction"
rel="noopener noreferrer"
target="_blank"
>
Continuous Backup
</a>
</span>
</div>
</div>
</div>
<div
class="ms-StackItem css-110"
>
<button
class="ms-Button ms-Button--primary fullWidth root-115"
data-is-focusable="true"
type="button"
>
<span
class="ms-Button-flexContainer flexContainer-116"
data-automationid="splitbuttonprimary"
>
<span
class="ms-Button-textContainer textContainer-117"
>
<span
class="ms-Button-label label-119"
id="id__13"
>
Enable Point In Time Restore
</span>
</span>
</span>
</button>
</div>
</div>
</div>
`;
exports[`PointInTimeRestore Snapshots should match snapshot in loading state 1`] = `
<div>
<div
class="ms-Stack pointInTimeRestoreContainer css-109"
>
<div
class="ms-Overlay root-123"
>
<div
class="ms-Spinner root-125"
>
<div
class="ms-Spinner-circle ms-Spinner--large circle-126"
/>
<div
class="ms-Spinner-label label-127"
>
Please wait while we process your request...
</div>
</div>
</div>
<div
class="ms-StackItem toggle-label css-110"
>
To facilitate online container copy jobs, please update your "test-account" backup policy from periodic to continuous backup. Enabling continuous backup is required for this functionality.
<div
class="ms-TooltipHost root-105"
role="none"
>
<div
class="ms-Image root-111"
style="width: 14px; height: 14px;"
>
<img
alt="Information"
class="ms-Image-image ms-Image-image--portrait is-notLoaded is-fadeIn image-112"
src="[object Object]"
/>
</div>
<div
hidden=""
id="tooltip44"
style="position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0px; border: 0px; overflow: hidden; white-space: nowrap;"
>
<span
class="css-113"
>
Learn more about
 
<a
class="ms-Link root-114"
href="https://learn.microsoft.com/en-us/azure/cosmos-db/continuous-backup-restore-introduction"
rel="noopener noreferrer"
target="_blank"
>
Continuous Backup
</a>
</span>
</div>
</div>
</div>
<div
class="ms-StackItem css-110"
>
<button
aria-disabled="true"
class="ms-Button ms-Button--primary is-disabled fullWidth root-128"
data-is-focusable="false"
disabled=""
type="button"
>
<span
class="ms-Button-flexContainer flexContainer-116"
data-automationid="splitbuttonprimary"
>
<i
aria-hidden="true"
class="ms-Icon root-105 css-132 ms-Button-icon icon-129"
data-icon-name="SyncStatusSolid"
style="font-family: "FabricMDL2Icons-16";"
>
</i>
<span
class="ms-Button-label label-119"
id="id__45"
/>
</span>
</button>
</div>
</div>
</div>
`;
exports[`PointInTimeRestore Snapshots should match snapshot with refresh button 1`] = `
<div>
<div
class="ms-Stack pointInTimeRestoreContainer css-109"
>
<div
class="ms-StackItem toggle-label css-110"
>
To facilitate online container copy jobs, please update your "test-account" backup policy from periodic to continuous backup. Enabling continuous backup is required for this functionality.
<div
class="ms-TooltipHost root-105"
role="none"
>
<div
class="ms-Image root-111"
style="width: 14px; height: 14px;"
>
<img
alt="Information"
class="ms-Image-image ms-Image-image--portrait is-notLoaded is-fadeIn image-112"
src="[object Object]"
/>
</div>
<div
hidden=""
id="tooltip48"
style="position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0px; border: 0px; overflow: hidden; white-space: nowrap;"
>
<span
class="css-113"
>
Learn more about
 
<a
class="ms-Link root-114"
href="https://learn.microsoft.com/en-us/azure/cosmos-db/continuous-backup-restore-introduction"
rel="noopener noreferrer"
target="_blank"
>
Continuous Backup
</a>
</span>
</div>
</div>
</div>
<div
class="ms-StackItem css-110"
>
<button
class="ms-Button ms-Button--primary fullWidth root-115"
data-is-focusable="true"
type="button"
>
<span
class="ms-Button-flexContainer flexContainer-116"
data-automationid="splitbuttonprimary"
>
<i
aria-hidden="true"
class="ms-Icon root-105 css-134 ms-Button-icon icon-118"
data-icon-name="Refresh"
style="font-family: "FabricMDL2Icons-0";"
>
</i>
<span
class="ms-Button-textContainer textContainer-117"
>
<span
class="ms-Button-label label-119"
id="id__49"
>
Refresh
</span>
</span>
</span>
</button>
</div>
</div>
</div>
`;

View File

@@ -0,0 +1,255 @@
import "@testing-library/jest-dom";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import React from "react";
import { logError } from "../../../../../../Common/Logger";
import { DatabaseAccount } from "../../../../../../Contracts/DataModels";
import Explorer from "../../../../../Explorer";
import CopyJobContextProvider, { useCopyJobContext } from "../../../../Context/CopyJobContext";
import { getAccountDetailsFromResourceId } from "../../../../CopyJobUtils";
import useManagedIdentity from "./useManagedIdentity";
jest.mock("../../../../CopyJobUtils");
jest.mock("../../../../../../Common/Logger");
const mockGetAccountDetailsFromResourceId = getAccountDetailsFromResourceId as jest.MockedFunction<
typeof getAccountDetailsFromResourceId
>;
const mockLogError = logError as jest.MockedFunction<typeof logError>;
const mockDatabaseAccount: DatabaseAccount = {
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/test-account",
name: "test-account",
location: "East US",
type: "Microsoft.DocumentDB/databaseAccounts",
kind: "GlobalDocumentDB",
properties: {
documentEndpoint: "https://test-account.documents.azure.com:443/",
},
} as DatabaseAccount;
interface TestComponentProps {
updateIdentityFn: (
subscriptionId: string,
resourceGroup?: string,
accountName?: string,
) => Promise<DatabaseAccount | undefined>;
onError?: (error: string) => void;
}
const TestComponent: React.FC<TestComponentProps> = ({ updateIdentityFn, onError }) => {
const { loading, handleAddSystemIdentity } = useManagedIdentity(updateIdentityFn);
const { contextError } = useCopyJobContext();
React.useEffect(() => {
if (contextError && onError) {
onError(contextError);
}
}, [contextError, onError]);
const handleClick = async () => {
await handleAddSystemIdentity();
};
return (
<div>
<button onClick={handleClick} disabled={loading} data-testid="add-identity-button">
{loading ? "Loading..." : "Add System Identity"}
</button>
<div data-testid="loading-status">{loading ? "true" : "false"}</div>
{contextError && <div data-testid="error-message">{contextError}</div>}
</div>
);
};
const TestWrapper: React.FC<TestComponentProps> = (props) => {
const mockExplorer = new Explorer();
return (
<CopyJobContextProvider explorer={mockExplorer}>
<TestComponent {...props} />
</CopyJobContextProvider>
);
};
describe("useManagedIdentity", () => {
const mockUpdateIdentityFn = jest.fn();
const mockOnError = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
mockGetAccountDetailsFromResourceId.mockReturnValue({
subscriptionId: "test-subscription",
resourceGroup: "test-resource-group",
accountName: "test-account-name",
});
});
afterEach(() => {
jest.restoreAllMocks();
});
it("should initialize with loading false", () => {
render(<TestWrapper updateIdentityFn={mockUpdateIdentityFn} onError={mockOnError} />);
expect(screen.getByTestId("loading-status")).toHaveTextContent("false");
expect(screen.getByTestId("add-identity-button")).toHaveTextContent("Add System Identity");
expect(screen.getByTestId("add-identity-button")).not.toBeDisabled();
});
it("should show loading state when handleAddSystemIdentity is called", async () => {
mockUpdateIdentityFn.mockImplementation(
() => new Promise((resolve) => setTimeout(() => resolve(mockDatabaseAccount), 100)),
);
render(<TestWrapper updateIdentityFn={mockUpdateIdentityFn} onError={mockOnError} />);
const button = screen.getByTestId("add-identity-button");
fireEvent.click(button);
expect(screen.getByTestId("loading-status")).toHaveTextContent("true");
expect(button).toHaveTextContent("Loading...");
expect(button).toBeDisabled();
});
it("should call updateIdentityFn with correct parameters", async () => {
mockUpdateIdentityFn.mockResolvedValue(mockDatabaseAccount);
render(<TestWrapper updateIdentityFn={mockUpdateIdentityFn} onError={mockOnError} />);
const button = screen.getByTestId("add-identity-button");
fireEvent.click(button);
await waitFor(() => {
expect(mockUpdateIdentityFn).toHaveBeenCalledWith(
"test-subscription",
"test-resource-group",
"test-account-name",
);
});
});
it("should handle successful identity update", async () => {
const updatedAccount = {
...mockDatabaseAccount,
properties: {
...mockDatabaseAccount.properties,
identity: { type: "SystemAssigned" },
},
};
mockUpdateIdentityFn.mockResolvedValue(updatedAccount);
render(<TestWrapper updateIdentityFn={mockUpdateIdentityFn} onError={mockOnError} />);
const button = screen.getByTestId("add-identity-button");
fireEvent.click(button);
await waitFor(() => {
expect(mockUpdateIdentityFn).toHaveBeenCalled();
});
expect(screen.queryByTestId("error-message")).toBeNull();
});
it("should handle error when updateIdentityFn fails", async () => {
const errorMessage = "Failed to update identity";
mockUpdateIdentityFn.mockRejectedValue(new Error(errorMessage));
render(<TestWrapper updateIdentityFn={mockUpdateIdentityFn} onError={mockOnError} />);
const button = screen.getByTestId("add-identity-button");
fireEvent.click(button);
await waitFor(() => {
expect(screen.getByTestId("error-message")).toHaveTextContent(errorMessage);
});
expect(mockLogError).toHaveBeenCalledWith(errorMessage, "CopyJob/useManagedIdentity.handleAddSystemIdentity");
expect(mockOnError).toHaveBeenCalledWith(errorMessage);
});
it("should handle error without message", async () => {
const errorWithoutMessage = {} as Error;
mockUpdateIdentityFn.mockRejectedValue(errorWithoutMessage);
render(<TestWrapper updateIdentityFn={mockUpdateIdentityFn} onError={mockOnError} />);
const button = screen.getByTestId("add-identity-button");
fireEvent.click(button);
await waitFor(() => {
expect(screen.getByTestId("error-message")).toHaveTextContent(
"Error enabling system-assigned managed identity. Please try again later.",
);
});
expect(mockLogError).toHaveBeenCalledWith(
"Error enabling system-assigned managed identity. Please try again later.",
"CopyJob/useManagedIdentity.handleAddSystemIdentity",
);
});
it("should handle case when getAccountDetailsFromResourceId returns null", async () => {
mockGetAccountDetailsFromResourceId.mockReturnValue(null);
mockUpdateIdentityFn.mockResolvedValue(undefined);
render(<TestWrapper updateIdentityFn={mockUpdateIdentityFn} onError={mockOnError} />);
const button = screen.getByTestId("add-identity-button");
fireEvent.click(button);
await waitFor(() => {
expect(mockUpdateIdentityFn).toHaveBeenCalledWith(undefined, undefined, undefined);
});
});
it("should handle case when updateIdentityFn returns undefined", async () => {
mockUpdateIdentityFn.mockResolvedValue(undefined);
render(<TestWrapper updateIdentityFn={mockUpdateIdentityFn} onError={mockOnError} />);
const button = screen.getByTestId("add-identity-button");
fireEvent.click(button);
await waitFor(() => {
expect(mockUpdateIdentityFn).toHaveBeenCalled();
});
expect(screen.queryByTestId("error-message")).toBeNull();
});
it("should call getAccountDetailsFromResourceId with target account id", async () => {
mockUpdateIdentityFn.mockResolvedValue(mockDatabaseAccount);
render(<TestWrapper updateIdentityFn={mockUpdateIdentityFn} onError={mockOnError} />);
const button = screen.getByTestId("add-identity-button");
fireEvent.click(button);
await waitFor(() => {
expect(mockGetAccountDetailsFromResourceId).toHaveBeenCalled();
});
const callArgs = mockGetAccountDetailsFromResourceId.mock.calls[0];
expect(callArgs).toBeDefined();
});
it("should reset loading state on error", async () => {
const errorMessage = "Network error";
mockUpdateIdentityFn.mockRejectedValue(new Error(errorMessage));
render(<TestWrapper updateIdentityFn={mockUpdateIdentityFn} onError={mockOnError} />);
const button = screen.getByTestId("add-identity-button");
fireEvent.click(button);
expect(screen.getByTestId("loading-status")).toHaveTextContent("true");
await waitFor(() => {
expect(screen.getByTestId("error-message")).toHaveTextContent(errorMessage);
});
expect(screen.getByTestId("loading-status")).toHaveTextContent("false");
expect(button).not.toBeDisabled();
expect(button).toHaveTextContent("Add System Identity");
});
});

View File

@@ -31,7 +31,7 @@ const useManagedIdentity = (
subscriptionId: targetSubscriptionId,
resourceGroup: targetResourceGroup,
accountName: targetAccountName,
} = getAccountDetailsFromResourceId(selectedTargetAccount?.id);
} = getAccountDetailsFromResourceId(selectedTargetAccount?.id) || {};
const updatedAccount = await updateIdentityFn(targetSubscriptionId, targetResourceGroup, targetAccountName);
if (updatedAccount) {

View File

@@ -0,0 +1,691 @@
import "@testing-library/jest-dom";
import { render, screen, waitFor } from "@testing-library/react";
import React from "react";
import { noop } from "underscore";
import { CapabilityNames } from "../../../../../../Common/Constants";
import * as RbacUtils from "../../../../../../Utils/arm/RbacUtils";
import {
BackupPolicyType,
CopyJobMigrationType,
DefaultIdentityType,
IdentityType,
} from "../../../../Enums/CopyJobEnums";
import { CopyJobContextState } from "../../../../Types/CopyJobTypes";
import * as CopyJobPrerequisitesCacheModule from "../../../Utils/useCopyJobPrerequisitesCache";
import usePermissionSections, {
checkTargetHasReaderRoleOnSource,
PermissionGroupConfig,
SECTION_IDS,
} from "./usePermissionsSection";
jest.mock("../../../../../../Utils/arm/RbacUtils");
jest.mock("../../../Utils/useCopyJobPrerequisitesCache");
jest.mock("../../../../CopyJobUtils", () => ({
getAccountDetailsFromResourceId: jest.fn(() => ({
subscriptionId: "sub-123",
resourceGroup: "rg-test",
accountName: "account-test",
})),
getContainerIdentifiers: jest.fn((container: any) => ({
accountId: container?.account?.id || "default-account-id",
})),
isIntraAccountCopy: jest.fn((sourceId: string, targetId: string) => sourceId === targetId),
}));
jest.mock("../AddManagedIdentity", () => {
const MockAddManagedIdentity = () => {
return <div data-testid="add-managed-identity">AddManagedIdentity</div>;
};
MockAddManagedIdentity.displayName = "MockAddManagedIdentity";
return MockAddManagedIdentity;
});
jest.mock("../AddReadPermissionToDefaultIdentity", () => {
const MockAddReadPermissionToDefaultIdentity = () => {
return <div data-testid="add-read-permission">AddReadPermissionToDefaultIdentity</div>;
};
MockAddReadPermissionToDefaultIdentity.displayName = "MockAddReadPermissionToDefaultIdentity";
return MockAddReadPermissionToDefaultIdentity;
});
jest.mock("../DefaultManagedIdentity", () => {
const MockDefaultManagedIdentity = () => {
return <div data-testid="default-managed-identity">DefaultManagedIdentity</div>;
};
MockDefaultManagedIdentity.displayName = "MockDefaultManagedIdentity";
return MockDefaultManagedIdentity;
});
jest.mock("../OnlineCopyEnabled", () => {
const MockOnlineCopyEnabled = () => {
return <div data-testid="online-copy-enabled">OnlineCopyEnabled</div>;
};
MockOnlineCopyEnabled.displayName = "MockOnlineCopyEnabled";
return MockOnlineCopyEnabled;
});
jest.mock("../PointInTimeRestore", () => {
const MockPointInTimeRestore = () => {
return <div data-testid="point-in-time-restore">PointInTimeRestore</div>;
};
MockPointInTimeRestore.displayName = "MockPointInTimeRestore";
return MockPointInTimeRestore;
});
const mockedRbacUtils = RbacUtils as jest.Mocked<typeof RbacUtils>;
const mockedCopyJobPrerequisitesCache = CopyJobPrerequisitesCacheModule as jest.Mocked<
typeof CopyJobPrerequisitesCacheModule
>;
interface TestWrapperProps {
state: CopyJobContextState;
onResult?: (result: PermissionGroupConfig[]) => void;
}
const TestWrapper: React.FC<TestWrapperProps> = ({ state, onResult }) => {
const result = usePermissionSections(state);
React.useEffect(() => {
if (onResult) {
onResult(result);
}
}, [result, onResult]);
return (
<div data-testid="test-wrapper">
<div data-testid="groups-count">{result.length}</div>
{result.map((group) => (
<div key={group.id} data-testid={`group-${group.id}`}>
<h3>{group.title}</h3>
<p>{group.description}</p>
{group.sections.map((section) => (
<div key={section.id} data-testid={`section-${section.id}`}>
<span data-testid={`section-${section.id}-completed`}>
{section.completed?.toString() || "undefined"}
</span>
<span data-testid={`section-${section.id}-disabled`}>{section.disabled.toString()}</span>
</div>
))}
</div>
))}
</div>
);
};
describe("usePermissionsSection", () => {
let mockValidationCache: Map<string, boolean>;
let mockSetValidationCache: jest.Mock;
const createMockState = (overrides: Partial<CopyJobContextState> = {}): CopyJobContextState => ({
jobName: "test-job",
migrationType: CopyJobMigrationType.Offline,
source: {
account: {
id: "source-account-id",
name: "source-account",
properties: {
backupPolicy: {
type: BackupPolicyType.Periodic,
},
capabilities: [],
},
location: "",
type: "",
kind: "",
},
subscription: undefined,
databaseId: "",
containerId: "",
},
target: {
account: {
id: "target-account-id",
name: "target-account",
identity: {
type: IdentityType.None,
principalId: "principal-123",
},
properties: {
defaultIdentity: DefaultIdentityType.FirstPartyIdentity,
},
location: "",
type: "",
kind: "",
},
subscriptionId: "",
databaseId: "",
containerId: "",
},
...overrides,
});
beforeEach(() => {
mockValidationCache = new Map();
mockSetValidationCache = jest.fn();
mockedCopyJobPrerequisitesCache.useCopyJobPrerequisitesCache.mockReturnValue({
validationCache: mockValidationCache,
setValidationCache: mockSetValidationCache,
});
mockedRbacUtils.fetchRoleAssignments.mockResolvedValue([]);
mockedRbacUtils.fetchRoleDefinitions.mockResolvedValue([]);
});
afterEach(() => {
jest.clearAllMocks();
});
describe("Cross-account copy scenarios", () => {
it("should return cross-account configuration for different accounts", async () => {
const state = createMockState();
let capturedResult: PermissionGroupConfig[] = [];
render(<TestWrapper state={state} onResult={(result) => (capturedResult = result)} />);
await waitFor(() => {
expect(screen.getByTestId("groups-count")).toHaveTextContent("1");
});
expect(capturedResult).toHaveLength(1);
expect(capturedResult[0].id).toBe("crossAccountConfigs");
expect(capturedResult[0].sections).toHaveLength(3);
expect(capturedResult[0].sections.map((s) => s.id)).toEqual([
SECTION_IDS.addManagedIdentity,
SECTION_IDS.defaultManagedIdentity,
SECTION_IDS.readPermissionAssigned,
]);
});
it("should not return cross-account configuration for same account (intra-account copy)", async () => {
const state = createMockState({
source: {
account: {
id: "same-account-id",
name: "same-account",
properties: undefined,
location: "",
type: "",
kind: "",
},
subscription: undefined,
databaseId: "",
containerId: "",
},
target: {
account: {
id: "same-account-id",
name: "same-account",
identity: { type: IdentityType.None, principalId: "principal-123" },
properties: { defaultIdentity: DefaultIdentityType.FirstPartyIdentity },
location: "",
type: "",
kind: "",
},
subscriptionId: "",
databaseId: "",
containerId: "",
},
});
let capturedResult: PermissionGroupConfig[] = [];
render(<TestWrapper state={state} onResult={(result) => (capturedResult = result)} />);
await waitFor(() => {
expect(screen.getByTestId("groups-count")).toHaveTextContent("0");
});
expect(capturedResult).toHaveLength(0);
});
});
describe("Online copy scenarios", () => {
it("should return online configuration for online migration", async () => {
const state = createMockState({
migrationType: CopyJobMigrationType.Online,
});
let capturedResult: PermissionGroupConfig[] = [];
render(<TestWrapper state={state} onResult={(result) => (capturedResult = result)} />);
await waitFor(() => {
expect(screen.getByTestId("groups-count")).toHaveTextContent("2");
});
const onlineGroup = capturedResult.find((g) => g.id === "onlineConfigs");
expect(onlineGroup).toBeDefined();
expect(onlineGroup?.sections).toHaveLength(2);
expect(onlineGroup?.sections.map((s) => s.id)).toEqual([
SECTION_IDS.pointInTimeRestore,
SECTION_IDS.onlineCopyEnabled,
]);
});
it("should not return online configuration for offline migration", async () => {
const state = createMockState({
migrationType: CopyJobMigrationType.Offline,
});
let capturedResult: PermissionGroupConfig[] = [];
render(<TestWrapper state={state} onResult={(result) => (capturedResult = result)} />);
await waitFor(() => {
expect(screen.getByTestId("groups-count")).toHaveTextContent("1");
});
const onlineGroup = capturedResult.find((g) => g.id === "onlineConfigs");
expect(onlineGroup).toBeUndefined();
});
});
describe("Section validation", () => {
it("should validate addManagedIdentity section correctly", async () => {
const stateWithSystemAssigned = createMockState({
target: {
account: {
id: "target-account-id",
name: "target-account",
identity: {
type: IdentityType.SystemAssigned,
principalId: "principal-123",
},
properties: {
defaultIdentity: DefaultIdentityType.FirstPartyIdentity,
},
location: "",
type: "",
kind: "",
},
subscriptionId: "",
databaseId: "",
containerId: "",
},
});
let capturedResult: PermissionGroupConfig[] = [];
render(<TestWrapper state={stateWithSystemAssigned} onResult={(result) => (capturedResult = result)} />);
await waitFor(() => {
expect(screen.getByTestId(`section-${SECTION_IDS.addManagedIdentity}-completed`)).toHaveTextContent("true");
});
const crossAccountGroup = capturedResult.find((g) => g.id === "crossAccountConfigs");
const addManagedIdentitySection = crossAccountGroup?.sections.find(
(s) => s.id === SECTION_IDS.addManagedIdentity,
);
expect(addManagedIdentitySection?.completed).toBe(true);
});
it("should validate defaultManagedIdentity section correctly", async () => {
const stateWithSystemAssignedIdentity = createMockState({
target: {
account: {
id: "target-account-id",
name: "target-account",
identity: {
type: IdentityType.SystemAssigned,
principalId: "principal-123",
},
properties: {
defaultIdentity: DefaultIdentityType.SystemAssignedIdentity,
},
location: "",
type: "",
kind: "",
},
subscriptionId: "",
databaseId: "",
containerId: "",
},
});
let capturedResult: PermissionGroupConfig[] = [];
render(<TestWrapper state={stateWithSystemAssignedIdentity} onResult={(result) => (capturedResult = result)} />);
await waitFor(() => {
expect(screen.getByTestId(`section-${SECTION_IDS.defaultManagedIdentity}-completed`)).toHaveTextContent("true");
});
const crossAccountGroup = capturedResult.find((g) => g.id === "crossAccountConfigs");
const defaultManagedIdentitySection = crossAccountGroup?.sections.find(
(s) => s.id === SECTION_IDS.defaultManagedIdentity,
);
expect(defaultManagedIdentitySection?.completed).toBe(true);
});
it("should validate readPermissionAssigned section with reader role", async () => {
const mockRoleDefinitions: RbacUtils.RoleDefinitionType[] = [
{
id: "role-1",
name: "Custom Role",
permissions: [
{
dataActions: [
"Microsoft.DocumentDB/databaseAccounts/readMetadata",
"Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/read",
],
},
],
assignableScopes: [],
resourceGroup: "",
roleName: "",
type: "",
typePropertiesType: "",
},
];
mockedRbacUtils.fetchRoleAssignments.mockResolvedValue([{ roleDefinitionId: "role-def-1" }] as any);
mockedRbacUtils.fetchRoleDefinitions.mockResolvedValue(mockRoleDefinitions);
const state = createMockState({
target: {
account: {
id: "target-account-id",
name: "target-account",
identity: {
type: IdentityType.SystemAssigned,
principalId: "principal-123",
},
properties: {
defaultIdentity: DefaultIdentityType.SystemAssignedIdentity,
},
location: "",
type: "",
kind: "",
},
subscriptionId: "",
databaseId: "",
containerId: "",
},
});
render(<TestWrapper state={state} onResult={noop} />);
await waitFor(() => {
expect(screen.getByTestId(`section-${SECTION_IDS.readPermissionAssigned}-completed`)).toHaveTextContent("true");
});
expect(mockedRbacUtils.fetchRoleAssignments).toHaveBeenCalledWith(
"sub-123",
"rg-test",
"account-test",
"principal-123",
);
});
it("should validate pointInTimeRestore section for continuous backup", async () => {
const state = createMockState({
migrationType: CopyJobMigrationType.Online,
source: {
account: {
id: "source-account-id",
name: "source-account",
properties: {
backupPolicy: {
type: BackupPolicyType.Continuous,
},
capabilities: [],
},
location: "",
type: "",
kind: "",
},
subscription: undefined,
databaseId: "",
containerId: "",
},
});
let capturedResult: PermissionGroupConfig[] = [];
render(<TestWrapper state={state} onResult={(result) => (capturedResult = result)} />);
await waitFor(() => {
expect(screen.getByTestId(`section-${SECTION_IDS.pointInTimeRestore}-completed`)).toHaveTextContent("true");
});
const onlineGroup = capturedResult.find((g) => g.id === "onlineConfigs");
const pointInTimeSection = onlineGroup?.sections.find((s) => s.id === SECTION_IDS.pointInTimeRestore);
expect(pointInTimeSection?.completed).toBe(true);
});
it("should validate onlineCopyEnabled section with proper capability", async () => {
const state = createMockState({
migrationType: CopyJobMigrationType.Online,
source: {
account: {
id: "source-account-id",
name: "source-account",
properties: {
backupPolicy: {
type: BackupPolicyType.Continuous,
},
capabilities: [
{
name: CapabilityNames.EnableOnlineCopyFeature,
description: "",
},
],
},
location: "",
type: "",
kind: "",
},
subscription: undefined,
databaseId: "",
containerId: "",
},
});
let capturedResult: PermissionGroupConfig[] = [];
render(<TestWrapper state={state} onResult={(result) => (capturedResult = result)} />);
await waitFor(() => {
expect(screen.getByTestId(`section-${SECTION_IDS.onlineCopyEnabled}-completed`)).toHaveTextContent("true");
});
const onlineGroup = capturedResult.find((g) => g.id === "onlineConfigs");
const onlineCopySection = onlineGroup?.sections.find((s) => s.id === SECTION_IDS.onlineCopyEnabled);
expect(onlineCopySection?.completed).toBe(true);
});
});
describe("Validation caching", () => {
it("should use cached validation results", async () => {
mockValidationCache.set(SECTION_IDS.addManagedIdentity, true);
mockValidationCache.set(SECTION_IDS.defaultManagedIdentity, true);
const state = createMockState();
render(<TestWrapper state={state} onResult={noop} />);
await waitFor(() => {
expect(screen.getByTestId(`section-${SECTION_IDS.addManagedIdentity}-completed`)).toHaveTextContent("true");
});
expect(screen.getByTestId(`section-${SECTION_IDS.defaultManagedIdentity}-completed`)).toHaveTextContent("true");
});
it("should clear online job validation cache when migration type changes to offline", async () => {
mockValidationCache.set(SECTION_IDS.pointInTimeRestore, true);
mockValidationCache.set(SECTION_IDS.onlineCopyEnabled, true);
const state = createMockState({
migrationType: CopyJobMigrationType.Offline,
});
render(<TestWrapper state={state} />);
await waitFor(() => {
expect(screen.getByTestId("groups-count")).toHaveTextContent("1");
});
expect(mockSetValidationCache).toHaveBeenCalled();
});
});
describe("Sequential validation within groups", () => {
it("should stop validation at first failure within a group", async () => {
const state = createMockState({
target: {
account: {
id: "target-account-id",
name: "target-account",
identity: {
type: IdentityType.None,
principalId: "principal-123",
},
properties: {
defaultIdentity: DefaultIdentityType.FirstPartyIdentity,
},
location: "",
type: "",
kind: "",
},
subscriptionId: "",
databaseId: "",
containerId: "",
},
});
let capturedResult: PermissionGroupConfig[] = [];
render(<TestWrapper state={state} onResult={(result) => (capturedResult = result)} />);
await waitFor(() => {
expect(screen.getByTestId(`section-${SECTION_IDS.addManagedIdentity}-completed`)).toHaveTextContent("false");
});
const crossAccountGroup = capturedResult.find((g) => g.id === "crossAccountConfigs");
expect(crossAccountGroup?.sections[0].completed).toBe(false);
expect(crossAccountGroup?.sections[1].completed).toBe(false);
expect(crossAccountGroup?.sections[2].completed).toBe(false);
});
});
});
describe("checkTargetHasReaderRoleOnSource", () => {
it("should return true for built-in Reader role", () => {
const roleDefinitions: RbacUtils.RoleDefinitionType[] = [
{
id: "role-1",
name: "00000000-0000-0000-0000-000000000001",
permissions: [],
assignableScopes: [],
resourceGroup: "",
roleName: "",
type: "",
typePropertiesType: "",
},
];
const result = checkTargetHasReaderRoleOnSource(roleDefinitions);
expect(result).toBe(true);
});
it("should return true for custom role with required data actions", () => {
const roleDefinitions: RbacUtils.RoleDefinitionType[] = [
{
id: "role-1",
name: "Custom Reader Role",
permissions: [
{
dataActions: [
"Microsoft.DocumentDB/databaseAccounts/readMetadata",
"Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/read",
],
},
],
assignableScopes: [],
resourceGroup: "",
roleName: "",
type: "",
typePropertiesType: "",
},
];
const result = checkTargetHasReaderRoleOnSource(roleDefinitions);
expect(result).toBe(true);
});
it("should return false for role without required permissions", () => {
const roleDefinitions: RbacUtils.RoleDefinitionType[] = [
{
id: "role-1",
name: "Insufficient Role",
permissions: [
{
dataActions: ["Microsoft.DocumentDB/databaseAccounts/readMetadata"],
},
],
assignableScopes: [],
resourceGroup: "",
roleName: "",
type: "",
typePropertiesType: "",
},
];
const result = checkTargetHasReaderRoleOnSource(roleDefinitions);
expect(result).toBe(false);
});
it("should return false for empty role definitions", () => {
const result = checkTargetHasReaderRoleOnSource([]);
expect(result).toBe(false);
});
it("should return false for role definitions without permissions", () => {
const roleDefinitions: RbacUtils.RoleDefinitionType[] = [
{
id: "role-1",
name: "No Permissions Role",
permissions: [],
assignableScopes: [],
resourceGroup: "",
roleName: "",
type: "",
typePropertiesType: "",
},
];
const result = checkTargetHasReaderRoleOnSource(roleDefinitions);
expect(result).toBe(false);
});
it("should handle multiple roles and return true if any has sufficient permissions", () => {
const roleDefinitions: RbacUtils.RoleDefinitionType[] = [
{
id: "role-1",
name: "Insufficient Role",
permissions: [
{
dataActions: ["Microsoft.DocumentDB/databaseAccounts/readMetadata"],
},
],
assignableScopes: [],
resourceGroup: "",
roleName: "",
type: "",
typePropertiesType: "",
},
{
id: "role-2",
name: "00000000-0000-0000-0000-000000000001",
permissions: [],
assignableScopes: [],
resourceGroup: "",
roleName: "",
type: "",
typePropertiesType: "",
},
];
const result = checkTargetHasReaderRoleOnSource(roleDefinitions);
expect(result).toBe(true);
});
});

View File

@@ -0,0 +1,78 @@
import { fireEvent, render, screen } from "@testing-library/react";
import React from "react";
import useToggle from "./useToggle";
const TestToggleComponent: React.FC<{ initialState?: boolean }> = ({ initialState }) => {
const [state, onToggle] = useToggle(initialState);
return (
<div>
<span data-testid="toggle-state">{state ? "true" : "false"}</span>
<button data-testid="toggle-button" onClick={() => onToggle(null, !state)}>
Toggle
</button>
<button data-testid="set-true-button" onClick={() => onToggle(null, true)}>
Set True
</button>
<button data-testid="set-false-button" onClick={() => onToggle(null, false)}>
Set False
</button>
</div>
);
};
describe("useToggle hook", () => {
it("should initialize with false as default", () => {
render(<TestToggleComponent />);
const stateElement = screen.getByTestId("toggle-state");
expect(stateElement.textContent).toBe("false");
});
it("should initialize with provided initial state", () => {
render(<TestToggleComponent initialState={true} />);
const stateElement = screen.getByTestId("toggle-state");
expect(stateElement.textContent).toBe("true");
});
it("should toggle state when onToggle is called with opposite value", () => {
render(<TestToggleComponent />);
const stateElement = screen.getByTestId("toggle-state");
const toggleButton = screen.getByTestId("toggle-button");
expect(stateElement.textContent).toBe("false");
fireEvent.click(toggleButton);
expect(stateElement.textContent).toBe("true");
fireEvent.click(toggleButton);
expect(stateElement.textContent).toBe("false");
});
it("should handle undefined checked parameter gracefully", () => {
const TestUndefinedComponent: React.FC = () => {
const [state, onToggle] = useToggle(false);
return (
<div>
<span data-testid="toggle-state">{state ? "true" : "false"}</span>
<button data-testid="undefined-button" onClick={() => onToggle(null, undefined)}>
Set Undefined
</button>
</div>
);
};
render(<TestUndefinedComponent />);
const stateElement = screen.getByTestId("toggle-state");
const undefinedButton = screen.getByTestId("undefined-button");
expect(stateElement.textContent).toBe("false");
fireEvent.click(undefinedButton);
expect(stateElement.textContent).toBe("false");
});
});

View File

@@ -0,0 +1,251 @@
import "@testing-library/jest-dom";
import { render, screen } from "@testing-library/react";
import React from "react";
import FieldRow from "./FieldRow";
describe("FieldRow", () => {
const mockChildContent = "Test Child Content";
const testLabel = "Test Label";
const customClassName = "custom-label-class";
describe("Component Rendering", () => {
it("renders the component with correct structure", () => {
const { container } = render(
<FieldRow label={testLabel}>
<div>{mockChildContent}</div>
</FieldRow>,
);
expect(container.firstChild).toHaveClass("flex-row");
expect(screen.getByText(`${testLabel}:`)).toBeInTheDocument();
expect(screen.getByText(mockChildContent)).toBeInTheDocument();
});
it("renders children content correctly", () => {
render(
<FieldRow label={testLabel}>
<input type="text" data-testid="test-input" />
<button data-testid="test-button">Click me</button>
</FieldRow>,
);
expect(screen.getByTestId("test-input")).toBeInTheDocument();
expect(screen.getByTestId("test-button")).toBeInTheDocument();
});
it("renders complex children components correctly", () => {
const ComplexChild = () => (
<div>
<span>Nested content</span>
<input type="text" placeholder="Enter value" />
</div>
);
render(
<FieldRow label={testLabel}>
<ComplexChild />
</FieldRow>,
);
expect(screen.getByText("Nested content")).toBeInTheDocument();
expect(screen.getByPlaceholderText("Enter value")).toBeInTheDocument();
});
it("does not render label when not provided", () => {
const { container } = render(
<FieldRow>
<div>{mockChildContent}</div>
</FieldRow>,
);
expect(container.querySelector("label")).not.toBeInTheDocument();
expect(screen.getByText(mockChildContent)).toBeInTheDocument();
});
it("applies custom label className when provided", () => {
render(
<FieldRow label={testLabel} labelClassName={customClassName}>
<div>{mockChildContent}</div>
</FieldRow>,
);
const label = screen.getByText(`${testLabel}:`);
expect(label).toHaveClass("field-label", customClassName);
});
});
describe("CSS Classes and Styling", () => {
it("applies default CSS classes correctly", () => {
const { container } = render(
<FieldRow label={testLabel}>
<div>{mockChildContent}</div>
</FieldRow>,
);
const mainContainer = container.firstChild as Element;
expect(mainContainer).toHaveClass("flex-row");
const labelContainer = container.querySelector(".flex-fixed-width");
expect(labelContainer).toBeInTheDocument();
const childContainer = container.querySelector(".flex-grow-col");
expect(childContainer).toBeInTheDocument();
const label = screen.getByText(`${testLabel}:`);
expect(label).toHaveClass("field-label");
});
});
describe("Layout and Structure", () => {
it("uses horizontal Stack with space-between alignment", () => {
const { container } = render(
<FieldRow label={testLabel}>
<div>{mockChildContent}</div>
</FieldRow>,
);
const mainContainer = container.firstChild as Element;
expect(mainContainer).toHaveClass("flex-row");
});
it("positions label in fixed-width container with center alignment", () => {
const { container } = render(
<FieldRow label={testLabel}>
<div>{mockChildContent}</div>
</FieldRow>,
);
const labelContainer = container.querySelector(".flex-fixed-width");
expect(labelContainer).toBeInTheDocument();
expect(labelContainer).toContainElement(screen.getByText(`${testLabel}:`));
});
it("positions children in grow container with center alignment", () => {
const { container } = render(
<FieldRow label={testLabel}>
<div data-testid="child-content">{mockChildContent}</div>
</FieldRow>,
);
const childContainer = container.querySelector(".flex-grow-col");
expect(childContainer).toBeInTheDocument();
expect(childContainer).toContainElement(screen.getByTestId("child-content"));
});
it("maintains layout when no label is provided", () => {
const { container } = render(
<FieldRow>
<div data-testid="child-content">{mockChildContent}</div>
</FieldRow>,
);
expect(container.firstChild).toHaveClass("flex-row");
expect(container.querySelector(".flex-fixed-width")).not.toBeInTheDocument();
const childContainer = container.querySelector(".flex-grow-col");
expect(childContainer).toBeInTheDocument();
expect(childContainer).toContainElement(screen.getByTestId("child-content"));
});
});
describe("Edge Cases and Error Handling", () => {
it("handles null children gracefully", () => {
render(<FieldRow label={testLabel}>{null}</FieldRow>);
expect(screen.getByText(`${testLabel}:`)).toBeInTheDocument();
});
it("handles zero as children", () => {
render(<FieldRow label={testLabel}>{0}</FieldRow>);
expect(screen.getByText(`${testLabel}:`)).toBeInTheDocument();
expect(screen.getByText("0")).toBeInTheDocument();
});
it("handles empty string as children", () => {
render(<FieldRow label={testLabel}>{""}</FieldRow>);
expect(screen.getByText(`${testLabel}:`)).toBeInTheDocument();
});
it("handles array of children", () => {
render(<FieldRow label={testLabel}>{[<span key="1">First</span>, <span key="2">Second</span>]}</FieldRow>);
expect(screen.getByText(`${testLabel}:`)).toBeInTheDocument();
expect(screen.getByText("First")).toBeInTheDocument();
expect(screen.getByText("Second")).toBeInTheDocument();
});
});
describe("Snapshot Testing", () => {
it("matches snapshot with minimal props", () => {
const { container } = render(
<FieldRow>
<input type="text" placeholder="Simple input" />
</FieldRow>,
);
expect(container.firstChild).toMatchSnapshot();
});
it("matches snapshot with label only", () => {
const { container } = render(
<FieldRow label="Database Name">
<input type="text" placeholder="Enter database name" />
</FieldRow>,
);
expect(container.firstChild).toMatchSnapshot();
});
it("matches snapshot with custom className", () => {
const { container } = render(
<FieldRow label="Container Name" labelClassName="custom-style">
<select>
<option>Option 1</option>
<option>Option 2</option>
</select>
</FieldRow>,
);
expect(container.firstChild).toMatchSnapshot();
});
it("matches snapshot with complex children", () => {
const { container } = render(
<FieldRow label="Advanced Settings" labelClassName="advanced-label">
<div>
<input type="checkbox" id="enable-feature" />
<label htmlFor="enable-feature">Enable advanced feature</label>
<button type="button">Configure</button>
</div>
</FieldRow>,
);
expect(container.firstChild).toMatchSnapshot();
});
it("matches snapshot with no label", () => {
const { container } = render(
<FieldRow>
<div>
<h4>Section Title</h4>
<p>Section description goes here</p>
</div>
</FieldRow>,
);
expect(container.firstChild).toMatchSnapshot();
});
it("matches snapshot with empty label", () => {
const { container } = render(
<FieldRow label="">
<button type="submit">Submit Form</button>
</FieldRow>,
);
expect(container.firstChild).toMatchSnapshot();
});
});
});

View File

@@ -0,0 +1,39 @@
import "@testing-library/jest-dom";
import { render } from "@testing-library/react";
import React from "react";
import InfoTooltip from "./InfoTooltip";
describe("InfoTooltip", () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe("Component Rendering", () => {
it("should render null when no content is provided", () => {
const { container } = render(<InfoTooltip />);
expect(container.firstChild).toBeNull();
});
it("should render null when content is undefined", () => {
const { container } = render(<InfoTooltip content={undefined} />);
expect(container.firstChild).toBeNull();
});
it("should render tooltip with image when content is provided", () => {
const { container } = render(<InfoTooltip content="Test tooltip content" />);
expect(container).toMatchSnapshot();
});
it("should render with JSX element content", () => {
const jsxContent = (
<div>
<strong>Important:</strong> This is a JSX tooltip
</div>
);
const { container } = render(<InfoTooltip content={jsxContent} />);
expect(container).toMatchSnapshot();
});
});
});

View File

@@ -0,0 +1,112 @@
import "@testing-library/jest-dom";
import { fireEvent, render, screen } from "@testing-library/react";
import React from "react";
import NavigationControls from "./NavigationControls";
describe("NavigationControls", () => {
const defaultProps = {
primaryBtnText: "Next",
onPrimary: jest.fn(),
onPrevious: jest.fn(),
onCancel: jest.fn(),
isPrimaryDisabled: false,
isPreviousDisabled: false,
};
beforeEach(() => {
jest.clearAllMocks();
});
it("renders all buttons with correct text", () => {
render(<NavigationControls {...defaultProps} />);
expect(screen.getByText("Next")).toBeInTheDocument();
expect(screen.getByText("Previous")).toBeInTheDocument();
expect(screen.getByText("Cancel")).toBeInTheDocument();
});
it("renders primary button with custom text", () => {
const customProps = {
...defaultProps,
primaryBtnText: "Complete",
};
render(<NavigationControls {...customProps} />);
expect(screen.getByText("Complete")).toBeInTheDocument();
expect(screen.queryByText("Next")).not.toBeInTheDocument();
});
it("calls onPrimary when primary button is clicked", () => {
render(<NavigationControls {...defaultProps} />);
fireEvent.click(screen.getByText("Next"));
expect(defaultProps.onPrimary).toHaveBeenCalledTimes(1);
});
it("calls onPrevious when previous button is clicked", () => {
render(<NavigationControls {...defaultProps} />);
fireEvent.click(screen.getByText("Previous"));
expect(defaultProps.onPrevious).toHaveBeenCalledTimes(1);
});
it("calls onCancel when cancel button is clicked", () => {
render(<NavigationControls {...defaultProps} />);
fireEvent.click(screen.getByText("Cancel"));
expect(defaultProps.onCancel).toHaveBeenCalledTimes(1);
});
it("disables primary button when isPrimaryDisabled is true", () => {
const disabledProps = {
...defaultProps,
isPrimaryDisabled: true,
};
render(<NavigationControls {...disabledProps} />);
const primaryButton = screen.getByText("Next").closest("button");
expect(primaryButton).toHaveAttribute("aria-disabled", "true");
expect(primaryButton).toHaveAttribute("data-is-focusable", "true");
});
it("disables previous button when isPreviousDisabled is true", () => {
const disabledProps = {
...defaultProps,
isPreviousDisabled: true,
};
render(<NavigationControls {...disabledProps} />);
const previousButton = screen.getByText("Previous").closest("button");
expect(previousButton).toHaveAttribute("aria-disabled", "true");
expect(previousButton).toHaveAttribute("data-is-focusable", "true");
});
it("does not call onPrimary when disabled primary button is clicked", () => {
const disabledProps = {
...defaultProps,
isPrimaryDisabled: true,
};
render(<NavigationControls {...disabledProps} />);
fireEvent.click(screen.getByText("Next"));
expect(defaultProps.onPrimary).not.toHaveBeenCalled();
});
it("does not call onPrevious when disabled previous button is clicked", () => {
const disabledProps = {
...defaultProps,
isPreviousDisabled: true,
};
render(<NavigationControls {...disabledProps} />);
fireEvent.click(screen.getByText("Previous"));
expect(defaultProps.onPrevious).not.toHaveBeenCalled();
});
it("enables both buttons when neither is disabled", () => {
render(<NavigationControls {...defaultProps} />);
expect(screen.getByText("Next").closest("button")).not.toHaveAttribute("aria-disabled");
expect(screen.getByText("Previous").closest("button")).not.toHaveAttribute("aria-disabled");
expect(screen.getByText("Cancel").closest("button")).not.toHaveAttribute("aria-disabled");
});
});

View File

@@ -0,0 +1,251 @@
import "@testing-library/jest-dom";
import { fireEvent, render, screen } from "@testing-library/react";
import React from "react";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import PopoverMessage from "./PopoverContainer";
jest.mock("../../../../../Common/LoadingOverlay", () => {
const MockLoadingOverlay = ({ isLoading, label }: { isLoading: boolean; label: string }) => {
return isLoading ? <div data-testid="loading-overlay" aria-label={label} /> : null;
};
MockLoadingOverlay.displayName = "MockLoadingOverlay";
return MockLoadingOverlay;
});
describe("PopoverMessage Component", () => {
const defaultProps = {
visible: true,
title: "Test Title",
onCancel: jest.fn(),
onPrimary: jest.fn(),
children: <div>Test content</div>,
};
beforeEach(() => {
jest.clearAllMocks();
});
describe("Rendering", () => {
it("should render correctly when visible", () => {
const { container } = render(<PopoverMessage {...defaultProps} />);
expect(container).toMatchSnapshot();
});
it("should render correctly when not visible", () => {
const { container } = render(<PopoverMessage {...defaultProps} visible={false} />);
expect(container).toMatchSnapshot();
});
it("should render correctly with loading state", () => {
const { container } = render(<PopoverMessage {...defaultProps} isLoading={true} />);
expect(container).toMatchSnapshot();
});
it("should render correctly with different title", () => {
const { container } = render(<PopoverMessage {...defaultProps} title="Custom Title" />);
expect(container).toMatchSnapshot();
});
it("should render correctly with different children content", () => {
const customChildren = (
<div>
<p>First paragraph</p>
<p>Second paragraph</p>
</div>
);
const { container } = render(<PopoverMessage {...defaultProps}>{customChildren}</PopoverMessage>);
expect(container).toMatchSnapshot();
});
});
describe("Visibility", () => {
it("should not render anything when visible is false", () => {
render(<PopoverMessage {...defaultProps} visible={false} />);
expect(screen.queryByText("Test Title")).not.toBeInTheDocument();
expect(screen.queryByText("Test content")).not.toBeInTheDocument();
});
it("should render content when visible is true", () => {
render(<PopoverMessage {...defaultProps} />);
expect(screen.getByText("Test Title")).toBeInTheDocument();
expect(screen.getByText("Test content")).toBeInTheDocument();
});
});
describe("Title Display", () => {
it("should display the provided title", () => {
render(<PopoverMessage {...defaultProps} title="Custom Popover Title" />);
expect(screen.getByText("Custom Popover Title")).toBeInTheDocument();
});
it("should handle empty title", () => {
render(<PopoverMessage {...defaultProps} title="" />);
expect(screen.queryByText("Test Title")).not.toBeInTheDocument();
});
});
describe("Children Content", () => {
it("should render children content", () => {
const customChildren = <span>Custom child content</span>;
render(<PopoverMessage {...defaultProps}>{customChildren}</PopoverMessage>);
expect(screen.getByText("Custom child content")).toBeInTheDocument();
});
it("should render complex children content", () => {
const complexChildren = (
<div>
<h3>Heading</h3>
<ul>
<li>Item 1</li>
<li>Item 2</li>
</ul>
</div>
);
render(<PopoverMessage {...defaultProps}>{complexChildren}</PopoverMessage>);
expect(screen.getByText("Heading")).toBeInTheDocument();
expect(screen.getByText("Item 1")).toBeInTheDocument();
expect(screen.getByText("Item 2")).toBeInTheDocument();
});
});
describe("Button Interactions", () => {
it("should call onPrimary when Yes button is clicked", () => {
const onPrimaryMock = jest.fn();
render(<PopoverMessage {...defaultProps} onPrimary={onPrimaryMock} />);
const yesButton = screen.getByText("Yes");
fireEvent.click(yesButton);
expect(onPrimaryMock).toHaveBeenCalledTimes(1);
});
it("should call onCancel when No button is clicked", () => {
const onCancelMock = jest.fn();
render(<PopoverMessage {...defaultProps} onCancel={onCancelMock} />);
const noButton = screen.getByText("No");
fireEvent.click(noButton);
expect(onCancelMock).toHaveBeenCalledTimes(1);
});
it("should not call handlers multiple times on rapid clicks", () => {
const onPrimaryMock = jest.fn();
const onCancelMock = jest.fn();
render(<PopoverMessage {...defaultProps} onPrimary={onPrimaryMock} onCancel={onCancelMock} />);
const yesButton = screen.getByText("Yes");
const noButton = screen.getByText("No");
fireEvent.click(yesButton);
fireEvent.click(yesButton);
fireEvent.click(noButton);
fireEvent.click(noButton);
expect(onPrimaryMock).toHaveBeenCalledTimes(2);
expect(onCancelMock).toHaveBeenCalledTimes(2);
});
});
describe("Loading State", () => {
test("should show loading overlay when isLoading is true", () => {
render(<PopoverMessage {...defaultProps} isLoading={true} />);
expect(screen.getByTestId("loading-overlay")).toBeInTheDocument();
});
it("should not show loading overlay when isLoading is false", () => {
render(<PopoverMessage {...defaultProps} isLoading={false} />);
expect(screen.queryByTestId("loading-overlay")).not.toBeInTheDocument();
});
it("should disable buttons when loading", () => {
render(<PopoverMessage {...defaultProps} isLoading={true} />);
const yesButton = screen.getByText("Yes").closest("button");
const noButton = screen.getByText("No").closest("button");
expect(yesButton).toHaveAttribute("aria-disabled", "true");
expect(noButton).toHaveAttribute("aria-disabled", "true");
});
it("should enable buttons when not loading", () => {
render(<PopoverMessage {...defaultProps} isLoading={false} />);
const yesButton = screen.getByText("Yes").closest("button");
const noButton = screen.getByText("No").closest("button");
expect(yesButton).not.toHaveAttribute("aria-disabled");
expect(noButton).not.toHaveAttribute("aria-disabled");
});
it("should use correct loading overlay label", () => {
render(<PopoverMessage {...defaultProps} isLoading={true} />);
const loadingOverlay = screen.getByTestId("loading-overlay");
expect(loadingOverlay).toHaveAttribute("aria-label", ContainerCopyMessages.popoverOverlaySpinnerLabel);
});
});
describe("Default Props", () => {
it("should handle missing isLoading prop (defaults to false)", () => {
const propsWithoutLoading = { ...defaultProps };
delete (propsWithoutLoading as any).isLoading;
render(<PopoverMessage {...propsWithoutLoading} />);
expect(screen.queryByTestId("loading-overlay")).not.toBeInTheDocument();
expect(screen.getByText("Yes")).not.toBeDisabled();
expect(screen.getByText("No")).not.toBeDisabled();
});
});
describe("CSS Classes and Styling", () => {
it("should apply correct CSS classes", () => {
const { container } = render(<PopoverMessage {...defaultProps} />);
const popoverContainer = container.querySelector(".popover-container");
expect(popoverContainer).toHaveClass("foreground");
});
it("should apply loading class when isLoading is true", () => {
const { container } = render(<PopoverMessage {...defaultProps} isLoading={true} />);
const popoverContainer = container.querySelector(".popover-container");
expect(popoverContainer).toHaveClass("loading");
});
it("should not apply loading class when isLoading is false", () => {
const { container } = render(<PopoverMessage {...defaultProps} isLoading={false} />);
const popoverContainer = container.querySelector(".popover-container");
expect(popoverContainer).not.toHaveClass("loading");
});
});
describe("Edge Cases", () => {
it("should handle undefined children", () => {
const propsWithUndefinedChildren = { ...defaultProps, children: undefined as React.ReactNode };
const { container } = render(<PopoverMessage {...propsWithUndefinedChildren} />);
expect(container).toMatchSnapshot();
});
it("should handle null children", () => {
const propsWithNullChildren = { ...defaultProps, children: null as React.ReactNode };
const { container } = render(<PopoverMessage {...propsWithNullChildren} />);
expect(container).toMatchSnapshot();
});
it("should handle empty string title", () => {
const propsWithEmptyTitle = { ...defaultProps, title: "" };
const { container } = render(<PopoverMessage {...propsWithEmptyTitle} />);
expect(container).toMatchSnapshot();
});
it("should handle very long title", () => {
const longTitle =
"This is a very long title that might cause layout issues or text wrapping in the popover component";
const propsWithLongTitle = { ...defaultProps, title: longTitle };
const { container } = render(<PopoverMessage {...propsWithLongTitle} />);
expect(container).toMatchSnapshot();
});
});
});

View File

@@ -0,0 +1,145 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FieldRow Snapshot Testing matches snapshot with complex children 1`] = `
<div
class="ms-Stack flex-row css-109"
>
<div
class="ms-StackItem flex-fixed-width css-110"
>
<label
class="field-label advanced-label"
>
Advanced Settings
:
</label>
</div>
<div
class="ms-StackItem flex-grow-col css-110"
>
<div>
<input
id="enable-feature"
type="checkbox"
/>
<label
for="enable-feature"
>
Enable advanced feature
</label>
<button
type="button"
>
Configure
</button>
</div>
</div>
</div>
`;
exports[`FieldRow Snapshot Testing matches snapshot with custom className 1`] = `
<div
class="ms-Stack flex-row css-109"
>
<div
class="ms-StackItem flex-fixed-width css-110"
>
<label
class="field-label custom-style"
>
Container Name
:
</label>
</div>
<div
class="ms-StackItem flex-grow-col css-110"
>
<select>
<option>
Option 1
</option>
<option>
Option 2
</option>
</select>
</div>
</div>
`;
exports[`FieldRow Snapshot Testing matches snapshot with empty label 1`] = `
<div
class="ms-Stack flex-row css-109"
>
<div
class="ms-StackItem flex-grow-col css-110"
>
<button
type="submit"
>
Submit Form
</button>
</div>
</div>
`;
exports[`FieldRow Snapshot Testing matches snapshot with label only 1`] = `
<div
class="ms-Stack flex-row css-109"
>
<div
class="ms-StackItem flex-fixed-width css-110"
>
<label
class="field-label "
>
Database Name
:
</label>
</div>
<div
class="ms-StackItem flex-grow-col css-110"
>
<input
placeholder="Enter database name"
type="text"
/>
</div>
</div>
`;
exports[`FieldRow Snapshot Testing matches snapshot with minimal props 1`] = `
<div
class="ms-Stack flex-row css-109"
>
<div
class="ms-StackItem flex-grow-col css-110"
>
<input
placeholder="Simple input"
type="text"
/>
</div>
</div>
`;
exports[`FieldRow Snapshot Testing matches snapshot with no label 1`] = `
<div
class="ms-Stack flex-row css-109"
>
<div
class="ms-StackItem flex-grow-col css-110"
>
<div>
<h4>
Section Title
</h4>
<p>
Section description goes here
</p>
</div>
</div>
</div>
`;

View File

@@ -0,0 +1,60 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`InfoTooltip Component Rendering should render tooltip with image when content is provided 1`] = `
<div>
<div
class="ms-TooltipHost root-105"
role="none"
>
<div
class="ms-Image root-109"
style="width: 14px; height: 14px;"
>
<img
alt="Information"
class="ms-Image-image ms-Image-image--portrait is-notLoaded is-fadeIn image-110"
src="[object Object]"
/>
</div>
<div
hidden=""
id="tooltip0"
style="position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0px; border: 0px; overflow: hidden; white-space: nowrap;"
>
Test tooltip content
</div>
</div>
</div>
`;
exports[`InfoTooltip Component Rendering should render with JSX element content 1`] = `
<div>
<div
class="ms-TooltipHost root-105"
role="none"
>
<div
class="ms-Image root-109"
style="width: 14px; height: 14px;"
>
<img
alt="Information"
class="ms-Image-image ms-Image-image--portrait is-notLoaded is-fadeIn image-110"
src="[object Object]"
/>
</div>
<div
hidden=""
id="tooltip1"
style="position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0px; border: 0px; overflow: hidden; white-space: nowrap;"
>
<div>
<strong>
Important:
</strong>
This is a JSX tooltip
</div>
</div>
</div>
</div>
`;

View File

@@ -0,0 +1,552 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PopoverMessage Component Edge Cases should handle empty string title 1`] = `
<div>
<div
class="ms-Stack popover-container foreground css-109"
style="max-width: 450px;"
>
<span
class="css-110"
style="font-weight: 600;"
/>
<span
class="css-111"
>
<div>
Test content
</div>
</span>
<div
class="ms-Stack css-112"
>
<button
class="ms-Button ms-Button--primary root-113"
data-is-focusable="true"
type="button"
>
<span
class="ms-Button-flexContainer flexContainer-114"
data-automationid="splitbuttonprimary"
>
<span
class="ms-Button-textContainer textContainer-115"
>
<span
class="ms-Button-label label-117"
id="id__138"
>
Yes
</span>
</span>
</span>
</button>
<button
class="ms-Button ms-Button--default root-121"
data-is-focusable="true"
type="button"
>
<span
class="ms-Button-flexContainer flexContainer-114"
data-automationid="splitbuttonprimary"
>
<span
class="ms-Button-textContainer textContainer-115"
>
<span
class="ms-Button-label label-117"
id="id__141"
>
No
</span>
</span>
</span>
</button>
</div>
</div>
</div>
`;
exports[`PopoverMessage Component Edge Cases should handle null children 1`] = `
<div>
<div
class="ms-Stack popover-container foreground css-109"
style="max-width: 450px;"
>
<span
class="css-110"
style="font-weight: 600;"
>
Test Title
</span>
<div
class="ms-Stack css-112"
>
<button
class="ms-Button ms-Button--primary root-113"
data-is-focusable="true"
type="button"
>
<span
class="ms-Button-flexContainer flexContainer-114"
data-automationid="splitbuttonprimary"
>
<span
class="ms-Button-textContainer textContainer-115"
>
<span
class="ms-Button-label label-117"
id="id__132"
>
Yes
</span>
</span>
</span>
</button>
<button
class="ms-Button ms-Button--default root-121"
data-is-focusable="true"
type="button"
>
<span
class="ms-Button-flexContainer flexContainer-114"
data-automationid="splitbuttonprimary"
>
<span
class="ms-Button-textContainer textContainer-115"
>
<span
class="ms-Button-label label-117"
id="id__135"
>
No
</span>
</span>
</span>
</button>
</div>
</div>
</div>
`;
exports[`PopoverMessage Component Edge Cases should handle undefined children 1`] = `
<div>
<div
class="ms-Stack popover-container foreground css-109"
style="max-width: 450px;"
>
<span
class="css-110"
style="font-weight: 600;"
>
Test Title
</span>
<div
class="ms-Stack css-112"
>
<button
class="ms-Button ms-Button--primary root-113"
data-is-focusable="true"
type="button"
>
<span
class="ms-Button-flexContainer flexContainer-114"
data-automationid="splitbuttonprimary"
>
<span
class="ms-Button-textContainer textContainer-115"
>
<span
class="ms-Button-label label-117"
id="id__126"
>
Yes
</span>
</span>
</span>
</button>
<button
class="ms-Button ms-Button--default root-121"
data-is-focusable="true"
type="button"
>
<span
class="ms-Button-flexContainer flexContainer-114"
data-automationid="splitbuttonprimary"
>
<span
class="ms-Button-textContainer textContainer-115"
>
<span
class="ms-Button-label label-117"
id="id__129"
>
No
</span>
</span>
</span>
</button>
</div>
</div>
</div>
`;
exports[`PopoverMessage Component Edge Cases should handle very long title 1`] = `
<div>
<div
class="ms-Stack popover-container foreground css-109"
style="max-width: 450px;"
>
<span
class="css-110"
style="font-weight: 600;"
>
This is a very long title that might cause layout issues or text wrapping in the popover component
</span>
<span
class="css-111"
>
<div>
Test content
</div>
</span>
<div
class="ms-Stack css-112"
>
<button
class="ms-Button ms-Button--primary root-113"
data-is-focusable="true"
type="button"
>
<span
class="ms-Button-flexContainer flexContainer-114"
data-automationid="splitbuttonprimary"
>
<span
class="ms-Button-textContainer textContainer-115"
>
<span
class="ms-Button-label label-117"
id="id__144"
>
Yes
</span>
</span>
</span>
</button>
<button
class="ms-Button ms-Button--default root-121"
data-is-focusable="true"
type="button"
>
<span
class="ms-Button-flexContainer flexContainer-114"
data-automationid="splitbuttonprimary"
>
<span
class="ms-Button-textContainer textContainer-115"
>
<span
class="ms-Button-label label-117"
id="id__147"
>
No
</span>
</span>
</span>
</button>
</div>
</div>
</div>
`;
exports[`PopoverMessage Component Rendering should render correctly when not visible 1`] = `<div />`;
exports[`PopoverMessage Component Rendering should render correctly when visible 1`] = `
<div>
<div
class="ms-Stack popover-container foreground css-109"
style="max-width: 450px;"
>
<span
class="css-110"
style="font-weight: 600;"
>
Test Title
</span>
<span
class="css-111"
>
<div>
Test content
</div>
</span>
<div
class="ms-Stack css-112"
>
<button
class="ms-Button ms-Button--primary root-113"
data-is-focusable="true"
type="button"
>
<span
class="ms-Button-flexContainer flexContainer-114"
data-automationid="splitbuttonprimary"
>
<span
class="ms-Button-textContainer textContainer-115"
>
<span
class="ms-Button-label label-117"
id="id__0"
>
Yes
</span>
</span>
</span>
</button>
<button
class="ms-Button ms-Button--default root-121"
data-is-focusable="true"
type="button"
>
<span
class="ms-Button-flexContainer flexContainer-114"
data-automationid="splitbuttonprimary"
>
<span
class="ms-Button-textContainer textContainer-115"
>
<span
class="ms-Button-label label-117"
id="id__3"
>
No
</span>
</span>
</span>
</button>
</div>
</div>
</div>
`;
exports[`PopoverMessage Component Rendering should render correctly with different children content 1`] = `
<div>
<div
class="ms-Stack popover-container foreground css-109"
style="max-width: 450px;"
>
<span
class="css-110"
style="font-weight: 600;"
>
Test Title
</span>
<span
class="css-111"
>
<div>
<p>
First paragraph
</p>
<p>
Second paragraph
</p>
</div>
</span>
<div
class="ms-Stack css-112"
>
<button
class="ms-Button ms-Button--primary root-113"
data-is-focusable="true"
type="button"
>
<span
class="ms-Button-flexContainer flexContainer-114"
data-automationid="splitbuttonprimary"
>
<span
class="ms-Button-textContainer textContainer-115"
>
<span
class="ms-Button-label label-117"
id="id__18"
>
Yes
</span>
</span>
</span>
</button>
<button
class="ms-Button ms-Button--default root-121"
data-is-focusable="true"
type="button"
>
<span
class="ms-Button-flexContainer flexContainer-114"
data-automationid="splitbuttonprimary"
>
<span
class="ms-Button-textContainer textContainer-115"
>
<span
class="ms-Button-label label-117"
id="id__21"
>
No
</span>
</span>
</span>
</button>
</div>
</div>
</div>
`;
exports[`PopoverMessage Component Rendering should render correctly with different title 1`] = `
<div>
<div
class="ms-Stack popover-container foreground css-109"
style="max-width: 450px;"
>
<span
class="css-110"
style="font-weight: 600;"
>
Custom Title
</span>
<span
class="css-111"
>
<div>
Test content
</div>
</span>
<div
class="ms-Stack css-112"
>
<button
class="ms-Button ms-Button--primary root-113"
data-is-focusable="true"
type="button"
>
<span
class="ms-Button-flexContainer flexContainer-114"
data-automationid="splitbuttonprimary"
>
<span
class="ms-Button-textContainer textContainer-115"
>
<span
class="ms-Button-label label-117"
id="id__12"
>
Yes
</span>
</span>
</span>
</button>
<button
class="ms-Button ms-Button--default root-121"
data-is-focusable="true"
type="button"
>
<span
class="ms-Button-flexContainer flexContainer-114"
data-automationid="splitbuttonprimary"
>
<span
class="ms-Button-textContainer textContainer-115"
>
<span
class="ms-Button-label label-117"
id="id__15"
>
No
</span>
</span>
</span>
</button>
</div>
</div>
</div>
`;
exports[`PopoverMessage Component Rendering should render correctly with loading state 1`] = `
<div>
<div
class="ms-Stack popover-container foreground loading css-109"
style="max-width: 450px;"
>
<div
aria-label="Please wait while we process your request..."
data-testid="loading-overlay"
/>
<span
class="css-110"
style="font-weight: 600;"
>
Test Title
</span>
<span
class="css-111"
>
<div>
Test content
</div>
</span>
<div
class="ms-Stack css-112"
>
<button
aria-disabled="true"
class="ms-Button ms-Button--primary is-disabled root-122"
data-is-focusable="false"
disabled=""
type="button"
>
<span
class="ms-Button-flexContainer flexContainer-114"
data-automationid="splitbuttonprimary"
>
<span
class="ms-Button-textContainer textContainer-115"
>
<span
class="ms-Button-label label-117"
id="id__6"
>
Yes
</span>
</span>
</span>
</button>
<button
aria-disabled="true"
class="ms-Button ms-Button--default is-disabled root-125"
data-is-focusable="false"
disabled=""
type="button"
>
<span
class="ms-Button-flexContainer flexContainer-114"
data-automationid="splitbuttonprimary"
>
<span
class="ms-Button-textContainer textContainer-115"
>
<span
class="ms-Button-label label-117"
id="id__9"
>
No
</span>
</span>
</span>
</button>
</div>
</div>
</div>
`;

View File

@@ -0,0 +1,261 @@
import "@testing-library/jest-dom";
import { render, screen, waitFor } from "@testing-library/react";
import { CopyJobMigrationType } from "Explorer/ContainerCopy/Enums/CopyJobEnums";
import { CopyJobContextProviderType } from "Explorer/ContainerCopy/Types/CopyJobTypes";
import Explorer from "Explorer/Explorer";
import { useSidePanel } from "hooks/useSidePanel";
import React from "react";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { useCopyJobContext } from "../../../Context/CopyJobContext";
import AddCollectionPanelWrapper from "./AddCollectionPanelWrapper";
jest.mock("hooks/useSidePanel");
jest.mock("../../../Context/CopyJobContext");
jest.mock("../../../../Panes/AddCollectionPanel/AddCollectionPanel", () => ({
AddCollectionPanel: ({
explorer,
isCopyJobFlow,
onSubmitSuccess,
}: {
explorer?: Explorer;
isCopyJobFlow: boolean;
onSubmitSuccess: (data: { databaseId: string; collectionId: string }) => void;
}) => (
<div data-testid="add-collection-panel">
<div data-testid="explorer-prop">{explorer ? "explorer-present" : "no-explorer"}</div>
<div data-testid="copy-job-flow">{isCopyJobFlow ? "true" : "false"}</div>
<button
data-testid="submit-button"
onClick={() => onSubmitSuccess({ databaseId: "test-db", collectionId: "test-collection" })}
>
Submit
</button>
</div>
),
}));
jest.mock("immer", () => ({
produce: jest.fn((updater) => (state: any) => {
const draft = { ...state };
updater(draft);
return draft;
}),
}));
const mockUseSidePanel = useSidePanel as jest.MockedFunction<typeof useSidePanel>;
const mockUseCopyJobContext = useCopyJobContext as jest.MockedFunction<typeof useCopyJobContext>;
describe("AddCollectionPanelWrapper", () => {
const mockSetCopyJobState = jest.fn();
const mockGoBack = jest.fn();
const mockSetHeaderText = jest.fn();
const mockExplorer = {} as Explorer;
const mockSidePanelState = {
isOpen: false,
panelWidth: "440px",
hasConsole: true,
headerText: "",
setHeaderText: mockSetHeaderText,
openSidePanel: jest.fn(),
closeSidePanel: jest.fn(),
setPanelHasConsole: jest.fn(),
};
const mockCopyJobContextValue = {
contextError: null,
setContextError: jest.fn(),
copyJobState: {
jobName: "",
migrationType: CopyJobMigrationType.Offline,
source: {
subscription: { subscriptionId: "" },
account: null,
databaseId: "",
containerId: "",
},
target: {
subscriptionId: "",
account: null,
databaseId: "",
containerId: "",
},
sourceReadAccessFromTarget: false,
},
setCopyJobState: mockSetCopyJobState,
flow: null,
setFlow: jest.fn(),
resetCopyJobState: jest.fn(),
explorer: mockExplorer,
} as unknown as CopyJobContextProviderType;
beforeEach(() => {
jest.clearAllMocks();
mockUseSidePanel.mockReturnValue(mockSidePanelState);
mockUseSidePanel.getState = jest.fn().mockReturnValue(mockSidePanelState);
mockUseCopyJobContext.mockReturnValue(mockCopyJobContextValue);
});
afterEach(() => {
jest.restoreAllMocks();
});
describe("Component Rendering", () => {
it("should render correctly with all required elements", () => {
const { container } = render(<AddCollectionPanelWrapper />);
expect(container.querySelector(".addCollectionPanelWrapper")).toBeInTheDocument();
expect(container.querySelector(".addCollectionPanelHeader")).toBeInTheDocument();
expect(container.querySelector(".addCollectionPanelBody")).toBeInTheDocument();
expect(screen.getByText(ContainerCopyMessages.createNewContainerSubHeading)).toBeInTheDocument();
expect(screen.getByTestId("add-collection-panel")).toBeInTheDocument();
});
it("should match snapshot", () => {
const { container } = render(<AddCollectionPanelWrapper />);
expect(container).toMatchSnapshot();
});
it("should match snapshot with explorer prop", () => {
const { container } = render(<AddCollectionPanelWrapper explorer={mockExplorer} />);
expect(container).toMatchSnapshot();
});
it("should match snapshot with goBack prop", () => {
const { container } = render(<AddCollectionPanelWrapper goBack={mockGoBack} />);
expect(container).toMatchSnapshot();
});
it("should match snapshot with both props", () => {
const { container } = render(<AddCollectionPanelWrapper explorer={mockExplorer} goBack={mockGoBack} />);
expect(container).toMatchSnapshot();
});
});
describe("Side Panel Header Management", () => {
it("should set header text to create container heading on mount", () => {
render(<AddCollectionPanelWrapper />);
expect(mockSetHeaderText).toHaveBeenCalledWith(ContainerCopyMessages.createContainerHeading);
});
it("should reset header text to create copy job panel title on unmount", () => {
const { unmount } = render(<AddCollectionPanelWrapper />);
unmount();
expect(mockSetHeaderText).toHaveBeenCalledWith(ContainerCopyMessages.createCopyJobPanelTitle);
});
it("should not change header text if already set correctly", () => {
const modifiedSidePanelState = {
...mockSidePanelState,
headerText: ContainerCopyMessages.createContainerHeading,
};
mockUseSidePanel.getState = jest.fn().mockReturnValue(modifiedSidePanelState);
render(<AddCollectionPanelWrapper />);
expect(mockSetHeaderText).not.toHaveBeenCalled();
});
});
describe("AddCollectionPanel Integration", () => {
it("should pass explorer prop to AddCollectionPanel", () => {
render(<AddCollectionPanelWrapper explorer={mockExplorer} />);
expect(screen.getByTestId("explorer-prop")).toHaveTextContent("explorer-present");
});
it("should pass undefined explorer to AddCollectionPanel when not provided", () => {
render(<AddCollectionPanelWrapper />);
expect(screen.getByTestId("explorer-prop")).toHaveTextContent("no-explorer");
});
it("should pass isCopyJobFlow as true to AddCollectionPanel", () => {
render(<AddCollectionPanelWrapper />);
expect(screen.getByTestId("copy-job-flow")).toHaveTextContent("true");
});
});
describe("Collection Success Handler", () => {
it("should update copy job state when handleAddCollectionSuccess is called", async () => {
render(<AddCollectionPanelWrapper goBack={mockGoBack} />);
const submitButton = screen.getByTestId("submit-button");
submitButton.click();
await waitFor(() => {
expect(mockSetCopyJobState).toHaveBeenCalledTimes(1);
});
const stateUpdater = mockSetCopyJobState.mock.calls[0][0];
const mockState = {
target: { databaseId: "", containerId: "" },
};
const updatedState = stateUpdater(mockState);
expect(updatedState.target.databaseId).toBe("test-db");
expect(updatedState.target.containerId).toBe("test-collection");
});
it("should call goBack when handleAddCollectionSuccess is called and goBack is provided", async () => {
render(<AddCollectionPanelWrapper goBack={mockGoBack} />);
const submitButton = screen.getByTestId("submit-button");
submitButton.click();
await waitFor(() => {
expect(mockGoBack).toHaveBeenCalledTimes(1);
});
});
it("should not call goBack when handleAddCollectionSuccess is called and goBack is not provided", async () => {
render(<AddCollectionPanelWrapper />);
const submitButton = screen.getByTestId("submit-button");
submitButton.click();
await waitFor(() => {
expect(mockSetCopyJobState).toHaveBeenCalledTimes(1);
});
expect(mockGoBack).not.toHaveBeenCalled();
});
});
describe("Error Handling", () => {
it("should handle missing setCopyJobState gracefully", () => {
const mockCopyJobContextValueWithoutSetState = {
...mockCopyJobContextValue,
setCopyJobState: undefined as any,
};
mockUseCopyJobContext.mockReturnValue(mockCopyJobContextValueWithoutSetState);
expect(() => render(<AddCollectionPanelWrapper />)).not.toThrow();
});
});
describe("Component Lifecycle", () => {
it("should properly cleanup on unmount", () => {
const { unmount } = render(<AddCollectionPanelWrapper />);
expect(mockSetHeaderText).toHaveBeenCalledWith(ContainerCopyMessages.createContainerHeading);
mockSetHeaderText.mockClear();
unmount();
expect(mockSetHeaderText).toHaveBeenCalledWith(ContainerCopyMessages.createCopyJobPanelTitle);
});
it("should re-render correctly when props change", () => {
const { rerender } = render(<AddCollectionPanelWrapper />);
expect(screen.getByTestId("explorer-prop")).toHaveTextContent("no-explorer");
rerender(<AddCollectionPanelWrapper explorer={mockExplorer} />);
expect(screen.getByTestId("explorer-prop")).toHaveTextContent("explorer-present");
});
});
});

View File

@@ -0,0 +1,165 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AddCollectionPanelWrapper Component Rendering should match snapshot 1`] = `
<div>
<div
class="ms-Stack addCollectionPanelWrapper css-109"
>
<div
class="ms-StackItem addCollectionPanelHeader css-110"
>
<span
class="css-111"
>
Select the properties for your container.
</span>
</div>
<div
class="ms-StackItem addCollectionPanelBody css-110"
>
<div
data-testid="add-collection-panel"
>
<div
data-testid="explorer-prop"
>
no-explorer
</div>
<div
data-testid="copy-job-flow"
>
true
</div>
<button
data-testid="submit-button"
>
Submit
</button>
</div>
</div>
</div>
</div>
`;
exports[`AddCollectionPanelWrapper Component Rendering should match snapshot with both props 1`] = `
<div>
<div
class="ms-Stack addCollectionPanelWrapper css-109"
>
<div
class="ms-StackItem addCollectionPanelHeader css-110"
>
<span
class="css-111"
>
Select the properties for your container.
</span>
</div>
<div
class="ms-StackItem addCollectionPanelBody css-110"
>
<div
data-testid="add-collection-panel"
>
<div
data-testid="explorer-prop"
>
explorer-present
</div>
<div
data-testid="copy-job-flow"
>
true
</div>
<button
data-testid="submit-button"
>
Submit
</button>
</div>
</div>
</div>
</div>
`;
exports[`AddCollectionPanelWrapper Component Rendering should match snapshot with explorer prop 1`] = `
<div>
<div
class="ms-Stack addCollectionPanelWrapper css-109"
>
<div
class="ms-StackItem addCollectionPanelHeader css-110"
>
<span
class="css-111"
>
Select the properties for your container.
</span>
</div>
<div
class="ms-StackItem addCollectionPanelBody css-110"
>
<div
data-testid="add-collection-panel"
>
<div
data-testid="explorer-prop"
>
explorer-present
</div>
<div
data-testid="copy-job-flow"
>
true
</div>
<button
data-testid="submit-button"
>
Submit
</button>
</div>
</div>
</div>
</div>
`;
exports[`AddCollectionPanelWrapper Component Rendering should match snapshot with goBack prop 1`] = `
<div>
<div
class="ms-Stack addCollectionPanelWrapper css-109"
>
<div
class="ms-StackItem addCollectionPanelHeader css-110"
>
<span
class="css-111"
>
Select the properties for your container.
</span>
</div>
<div
class="ms-StackItem addCollectionPanelBody css-110"
>
<div
data-testid="add-collection-panel"
>
<div
data-testid="explorer-prop"
>
no-explorer
</div>
<div
data-testid="copy-job-flow"
>
true
</div>
<button
data-testid="submit-button"
>
Submit
</button>
</div>
</div>
</div>
</div>
`;

View File

@@ -0,0 +1,426 @@
import "@testing-library/jest-dom";
import { fireEvent, render, screen } from "@testing-library/react";
import React from "react";
import CreateCopyJobScreens from "./CreateCopyJobScreens";
jest.mock("../../Context/CopyJobContext", () => ({
useCopyJobContext: jest.fn(),
}));
jest.mock("../Utils/useCopyJobNavigation", () => ({
useCopyJobNavigation: jest.fn(),
}));
jest.mock("./Components/NavigationControls", () => {
const MockedNavigationControls = ({
primaryBtnText,
onPrimary,
onPrevious,
onCancel,
isPrimaryDisabled,
isPreviousDisabled,
}: {
primaryBtnText: string;
onPrimary: () => void;
onPrevious: () => void;
onCancel: () => void;
isPrimaryDisabled: boolean;
isPreviousDisabled: boolean;
}) => (
<div data-testid="navigation-controls">
<button data-testid="primary-button" onClick={onPrimary} disabled={isPrimaryDisabled}>
{primaryBtnText}
</button>
<button data-testid="previous-button" onClick={onPrevious} disabled={isPreviousDisabled}>
Previous
</button>
<button data-testid="cancel-button" onClick={onCancel}>
Cancel
</button>
</div>
);
return MockedNavigationControls;
});
import { useCopyJobContext } from "../../Context/CopyJobContext";
import { useCopyJobNavigation } from "../Utils/useCopyJobNavigation";
const createMockNavigationHook = (overrides = {}) => ({
currentScreen: {
key: "SelectAccount",
component: <div data-testid="mock-screen">Mock Screen Component</div>,
},
isPrimaryDisabled: false,
isPreviousDisabled: true,
handlePrimary: jest.fn(),
handlePrevious: jest.fn(),
handleCancel: jest.fn(),
primaryBtnText: "Next",
showAddCollectionPanel: jest.fn(),
...overrides,
});
const createMockContext = (overrides = {}) => ({
contextError: "",
setContextError: jest.fn(),
copyJobState: {},
setCopyJobState: jest.fn(),
flow: {},
setFlow: jest.fn(),
resetCopyJobState: jest.fn(),
explorer: {},
...overrides,
});
describe("CreateCopyJobScreens", () => {
const mockNavigationHook = createMockNavigationHook();
const mockContext = createMockContext();
beforeEach(() => {
jest.clearAllMocks();
(useCopyJobNavigation as jest.Mock).mockReturnValue(mockNavigationHook);
(useCopyJobContext as jest.Mock).mockReturnValue(mockContext);
});
describe("Rendering", () => {
test("should render without error", () => {
render(<CreateCopyJobScreens />);
expect(screen.getByTestId("mock-screen")).toBeInTheDocument();
expect(screen.getByTestId("navigation-controls")).toBeInTheDocument();
});
test("should render current screen component", () => {
const customScreen = <div data-testid="custom-screen">Custom Screen Content</div>;
(useCopyJobNavigation as jest.Mock).mockReturnValue(
createMockNavigationHook({
currentScreen: { component: customScreen },
}),
);
render(<CreateCopyJobScreens />);
expect(screen.getByTestId("custom-screen")).toBeInTheDocument();
expect(screen.getByText("Custom Screen Content")).toBeInTheDocument();
});
test("should have correct CSS classes", () => {
const { container } = render(<CreateCopyJobScreens />);
const mainContainer = container.querySelector(".createCopyJobScreensContainer");
const contentContainer = container.querySelector(".createCopyJobScreensContent");
const footerContainer = container.querySelector(".createCopyJobScreensFooter");
expect(mainContainer).toBeInTheDocument();
expect(contentContainer).toBeInTheDocument();
expect(footerContainer).toBeInTheDocument();
});
});
describe("Error Message Bar", () => {
test("should not show error message bar when no error", () => {
render(<CreateCopyJobScreens />);
expect(screen.queryByRole("region")).not.toBeInTheDocument();
});
test("should show error message bar when context error exists", () => {
const errorMessage = "Something went wrong";
(useCopyJobContext as jest.Mock).mockReturnValue(
createMockContext({
contextError: errorMessage,
}),
);
render(<CreateCopyJobScreens />);
const messageBar = screen.getByRole("region");
expect(messageBar).toBeInTheDocument();
expect(messageBar).toHaveClass("createCopyJobErrorMessageBar");
});
test("should have correct error message bar properties", () => {
const errorMessage = "Test error message";
(useCopyJobContext as jest.Mock).mockReturnValue(
createMockContext({
contextError: errorMessage,
}),
);
render(<CreateCopyJobScreens />);
const messageBar = screen.getByRole("region");
expect(messageBar).toHaveClass("createCopyJobErrorMessageBar");
});
test("should call setContextError when dismiss button is clicked", () => {
const mockSetContextError = jest.fn();
(useCopyJobContext as jest.Mock).mockReturnValue(
createMockContext({
contextError: "Test error",
setContextError: mockSetContextError,
}),
);
render(<CreateCopyJobScreens />);
const dismissButton = screen.getByLabelText("Close");
fireEvent.click(dismissButton);
expect(mockSetContextError).toHaveBeenCalledWith(null);
});
test("should show overflow button with correct aria label", () => {
(useCopyJobContext as jest.Mock).mockReturnValue(
createMockContext({
contextError: "A very long error message that should trigger overflow behavior",
}),
);
render(<CreateCopyJobScreens />);
const overflowButton = screen.getByLabelText("See more");
expect(overflowButton).toBeInTheDocument();
});
});
describe("Navigation Controls Integration", () => {
test("should pass correct props to NavigationControls", () => {
const mockHook = createMockNavigationHook({
primaryBtnText: "Create",
isPrimaryDisabled: true,
isPreviousDisabled: false,
});
(useCopyJobNavigation as jest.Mock).mockReturnValue(mockHook);
render(<CreateCopyJobScreens />);
const primaryButton = screen.getByTestId("primary-button");
const previousButton = screen.getByTestId("previous-button");
expect(primaryButton).toHaveTextContent("Create");
expect(primaryButton).toBeDisabled();
expect(previousButton).not.toBeDisabled();
});
test("should call navigation handlers when buttons are clicked", () => {
const mockHandlePrimary = jest.fn();
const mockHandlePrevious = jest.fn();
const mockHandleCancel = jest.fn();
(useCopyJobNavigation as jest.Mock).mockReturnValue(
createMockNavigationHook({
handlePrimary: mockHandlePrimary,
handlePrevious: mockHandlePrevious,
handleCancel: mockHandleCancel,
isPrimaryDisabled: false,
isPreviousDisabled: false,
}),
);
render(<CreateCopyJobScreens />);
fireEvent.click(screen.getByTestId("primary-button"));
fireEvent.click(screen.getByTestId("previous-button"));
fireEvent.click(screen.getByTestId("cancel-button"));
expect(mockHandlePrimary).toHaveBeenCalledTimes(1);
expect(mockHandlePrevious).toHaveBeenCalledTimes(1);
expect(mockHandleCancel).toHaveBeenCalledTimes(1);
});
});
describe("Screen Component Props", () => {
test("should pass showAddCollectionPanel prop to screen component", () => {
const mockShowAddCollectionPanel = jest.fn();
const TestScreen = ({ showAddCollectionPanel }: { showAddCollectionPanel: () => void }) => (
<div>
<button data-testid="add-collection-btn" onClick={showAddCollectionPanel}>
Add Collection
</button>
</div>
);
(useCopyJobNavigation as jest.Mock).mockReturnValue(
createMockNavigationHook({
currentScreen: { component: <TestScreen showAddCollectionPanel={() => {}} /> },
showAddCollectionPanel: mockShowAddCollectionPanel,
}),
);
render(<CreateCopyJobScreens />);
const addButton = screen.getByTestId("add-collection-btn");
expect(addButton).toBeInTheDocument();
});
test("should handle screen component without props", () => {
const SimpleScreen = () => <div data-testid="simple-screen">Simple Screen</div>;
(useCopyJobNavigation as jest.Mock).mockReturnValue(
createMockNavigationHook({
currentScreen: { component: <SimpleScreen /> },
}),
);
expect(() => render(<CreateCopyJobScreens />)).not.toThrow();
expect(screen.getByTestId("simple-screen")).toBeInTheDocument();
});
});
describe("Layout and Structure", () => {
test("should maintain vertical layout with space-between alignment", () => {
const { container } = render(<CreateCopyJobScreens />);
const stackContainer = container.querySelector(".createCopyJobScreensContainer");
expect(stackContainer).toBeInTheDocument();
});
test("should have content area above navigation controls", () => {
const { container } = render(<CreateCopyJobScreens />);
const content = container.querySelector(".createCopyJobScreensContent");
const footer = container.querySelector(".createCopyJobScreensFooter");
expect(content).toBeInTheDocument();
expect(footer).toBeInTheDocument();
const contentIndex = Array.from(container.querySelectorAll("*")).indexOf(content!);
const footerIndex = Array.from(container.querySelectorAll("*")).indexOf(footer!);
expect(contentIndex).toBeLessThan(footerIndex);
});
});
describe("Error Scenarios", () => {
test("should handle missing current screen gracefully", () => {
(useCopyJobNavigation as jest.Mock).mockReturnValue(
createMockNavigationHook({
currentScreen: null,
}),
);
expect(() => render(<CreateCopyJobScreens />)).toThrow();
});
test("should handle missing screen component", () => {
(useCopyJobNavigation as jest.Mock).mockReturnValue(
createMockNavigationHook({
currentScreen: { key: "test", component: null },
}),
);
expect(() => render(<CreateCopyJobScreens />)).toThrow();
});
test("should render with valid screen component", () => {
(useCopyJobNavigation as jest.Mock).mockReturnValue(
createMockNavigationHook({
currentScreen: {
key: "test",
component: <div data-testid="valid-screen">Valid Screen</div>,
},
}),
);
expect(() => render(<CreateCopyJobScreens />)).not.toThrow();
expect(screen.getByTestId("valid-screen")).toBeInTheDocument();
});
test("should handle context hook throwing error", () => {
(useCopyJobContext as jest.Mock).mockImplementation(() => {
throw new Error("Context not available");
});
expect(() => render(<CreateCopyJobScreens />)).toThrow("Context not available");
});
test("should handle navigation hook throwing error", () => {
(useCopyJobNavigation as jest.Mock).mockImplementation(() => {
throw new Error("Navigation not available");
});
expect(() => render(<CreateCopyJobScreens />)).toThrow("Navigation not available");
});
});
describe("Multiple Error States", () => {
test("should handle error message changes", () => {
const mockSetContextError = jest.fn();
const { rerender } = render(<CreateCopyJobScreens />);
expect(screen.queryByRole("region")).not.toBeInTheDocument();
(useCopyJobContext as jest.Mock).mockReturnValue(
createMockContext({
contextError: "First error",
setContextError: mockSetContextError,
}),
);
rerender(<CreateCopyJobScreens />);
expect(screen.getByRole("region")).toBeInTheDocument();
(useCopyJobContext as jest.Mock).mockReturnValue(
createMockContext({
contextError: "Second error",
setContextError: mockSetContextError,
}),
);
rerender(<CreateCopyJobScreens />);
expect(screen.getByRole("region")).toBeInTheDocument();
(useCopyJobContext as jest.Mock).mockReturnValue(
createMockContext({
contextError: null,
setContextError: mockSetContextError,
}),
);
rerender(<CreateCopyJobScreens />);
expect(screen.queryByRole("region")).not.toBeInTheDocument();
});
});
describe("Accessibility", () => {
test("should have proper ARIA labels for message bar", () => {
(useCopyJobContext as jest.Mock).mockReturnValue(
createMockContext({
contextError: "Test error",
}),
);
render(<CreateCopyJobScreens />);
const dismissButton = screen.getByLabelText("Close");
const overflowButton = screen.getByLabelText("See more");
expect(dismissButton).toBeInTheDocument();
expect(overflowButton).toBeInTheDocument();
});
test("should have proper region role for message bar", () => {
(useCopyJobContext as jest.Mock).mockReturnValue(
createMockContext({
contextError: "Test error",
}),
);
render(<CreateCopyJobScreens />);
const messageRegion = screen.getByRole("region");
expect(messageRegion).toBeInTheDocument();
const alert = screen.getByRole("alert");
expect(alert).toBeInTheDocument();
});
});
describe("Component Integration", () => {
test("should integrate with both context and navigation hooks", () => {
const mockContext = createMockContext({
contextError: "Integration test error",
});
const mockNavigation = createMockNavigationHook({
primaryBtnText: "Integration Test",
isPrimaryDisabled: true,
});
(useCopyJobContext as jest.Mock).mockReturnValue(mockContext);
(useCopyJobNavigation as jest.Mock).mockReturnValue(mockNavigation);
render(<CreateCopyJobScreens />);
expect(screen.getByRole("region")).toBeInTheDocument();
expect(screen.getByText("Integration Test")).toBeInTheDocument();
});
});
});

View File

@@ -0,0 +1,95 @@
import { shallow } from "enzyme";
import Explorer from "Explorer/Explorer";
import React from "react";
import CreateCopyJobScreensProvider from "./CreateCopyJobScreensProvider";
jest.mock("../../Context/CopyJobContext", () => ({
__esModule: true,
default: ({ children, explorer }: { children: React.ReactNode; explorer: Explorer }) => (
<div data-testid="copy-job-context-provider" data-explorer={explorer ? "explorer-instance" : "null"}>
{children}
</div>
),
}));
jest.mock("./CreateCopyJobScreens", () => ({
__esModule: true,
default: () => <div data-testid="create-copy-job-screens">CreateCopyJobScreens</div>,
}));
const mockExplorer = {
databaseAccount: {
id: "test-account",
name: "test-account-name",
location: "East US",
type: "DocumentDB",
kind: "GlobalDocumentDB",
properties: {
documentEndpoint: "https://test-account.documents.azure.com:443/",
gremlinEndpoint: "https://test-account.gremlin.cosmosdb.azure.com:443/",
tableEndpoint: "https://test-account.table.cosmosdb.azure.com:443/",
cassandraEndpoint: "https://test-account.cassandra.cosmosdb.azure.com:443/",
},
},
subscriptionId: "test-subscription-id",
resourceGroup: "test-resource-group",
} as unknown as Explorer;
describe("CreateCopyJobScreensProvider", () => {
beforeEach(() => {
jest.clearAllMocks();
});
it("should render with explorer prop", () => {
const wrapper = shallow(<CreateCopyJobScreensProvider explorer={mockExplorer} />);
expect(wrapper).toMatchSnapshot();
});
it("should render with null explorer", () => {
const wrapper = shallow(<CreateCopyJobScreensProvider explorer={null as unknown as Explorer} />);
expect(wrapper).toMatchSnapshot();
});
it("should render with undefined explorer", () => {
const wrapper = shallow(<CreateCopyJobScreensProvider explorer={undefined as unknown as Explorer} />);
expect(wrapper).toMatchSnapshot();
});
it("should not crash with minimal explorer object", () => {
const minimalExplorer = {} as Explorer;
expect(() => {
const wrapper = shallow(<CreateCopyJobScreensProvider explorer={minimalExplorer} />);
expect(wrapper).toBeDefined();
}).not.toThrow();
});
it("should match snapshot for default render", () => {
const wrapper = shallow(<CreateCopyJobScreensProvider explorer={mockExplorer} />);
expect(wrapper).toMatchSnapshot("default-render");
});
it("should match snapshot for edge cases", () => {
const emptyExplorer = {} as Explorer;
const wrapperEmpty = shallow(<CreateCopyJobScreensProvider explorer={emptyExplorer} />);
expect(wrapperEmpty).toMatchSnapshot("empty-explorer");
const partialExplorer = {
databaseAccount: { id: "partial-account" },
} as unknown as Explorer;
const wrapperPartial = shallow(<CreateCopyJobScreensProvider explorer={partialExplorer} />);
expect(wrapperPartial).toMatchSnapshot("partial-explorer");
});
describe("Error Boundaries and Edge Cases", () => {
it("should handle React rendering errors gracefully", () => {
const edgeCases = [null, undefined, {}, { invalidProperty: "test" }];
edgeCases.forEach((explorerCase) => {
expect(() => {
shallow(<CreateCopyJobScreensProvider explorer={explorerCase as unknown as Explorer} />);
}).not.toThrow();
});
});
});
});

View File

@@ -0,0 +1,366 @@
import "@testing-library/jest-dom";
import { fireEvent, render, waitFor } from "@testing-library/react";
import { Subscription } from "Contracts/DataModels";
import React from "react";
import { CopyJobContext } from "../../../Context/CopyJobContext";
import { CopyJobMigrationType } from "../../../Enums/CopyJobEnums";
import { CopyJobContextProviderType, CopyJobContextState } from "../../../Types/CopyJobTypes";
import PreviewCopyJob from "./PreviewCopyJob";
jest.mock("./Utils/PreviewCopyJobUtils", () => ({
getPreviewCopyJobDetailsListColumns: () => [
{
key: "sourcedbname",
name: "Source Database",
fieldName: "sourceDatabaseName",
minWidth: 130,
maxWidth: 140,
},
{
key: "sourcecolname",
name: "Source Container",
fieldName: "sourceContainerName",
minWidth: 130,
maxWidth: 140,
},
{
key: "targetdbname",
name: "Destination Database",
fieldName: "targetDatabaseName",
minWidth: 130,
maxWidth: 140,
},
{
key: "targetcolname",
name: "Destination Container",
fieldName: "targetContainerName",
minWidth: 130,
maxWidth: 140,
},
],
}));
jest.mock("../../../CopyJobUtils", () => ({
getDefaultJobName: jest.fn((selectedDatabaseAndContainers) => {
if (selectedDatabaseAndContainers.length === 1) {
const { sourceDatabaseName, sourceContainerName, targetDatabaseName, targetContainerName } =
selectedDatabaseAndContainers[0];
return `${sourceDatabaseName}.${sourceContainerName}_${targetDatabaseName}.${targetContainerName}_123456789`;
}
return "";
}),
}));
describe("PreviewCopyJob", () => {
const mockSetCopyJobState = jest.fn();
const mockSetContextError = jest.fn();
const mockSetFlow = jest.fn();
const mockResetCopyJobState = jest.fn();
const mockSubscription: Subscription = {
subscriptionId: "test-subscription-id",
displayName: "Test Subscription",
state: "Enabled",
subscriptionPolicies: {
locationPlacementId: "test",
quotaId: "test",
},
authorizationSource: "test",
};
const mockDatabaseAccount = {
id: "/subscriptions/test-subscription-id/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/test-account",
name: "test-account",
location: "East US",
type: "Microsoft.DocumentDB/databaseAccounts",
kind: "GlobalDocumentDB",
properties: {
documentEndpoint: "https://test-account.documents.azure.com:443/",
gremlinEndpoint: "https://test-account.gremlin.cosmosdb.azure.com:443/",
tableEndpoint: "https://test-account.table.cosmosdb.azure.com:443/",
cassandraEndpoint: "https://test-account.cassandra.cosmosdb.azure.com:443/",
},
};
const createMockContext = (overrides: Partial<CopyJobContextState> = {}): CopyJobContextProviderType => {
const defaultState: CopyJobContextState = {
jobName: "",
migrationType: CopyJobMigrationType.Offline,
source: {
subscription: mockSubscription,
account: mockDatabaseAccount,
databaseId: "source-database",
containerId: "source-container",
},
target: {
subscriptionId: "test-subscription-id",
account: mockDatabaseAccount,
databaseId: "target-database",
containerId: "target-container",
},
sourceReadAccessFromTarget: false,
...overrides,
};
return {
contextError: null,
setContextError: mockSetContextError,
copyJobState: defaultState,
setCopyJobState: mockSetCopyJobState,
flow: null,
setFlow: mockSetFlow,
resetCopyJobState: mockResetCopyJobState,
explorer: {} as any,
};
};
beforeEach(() => {
jest.clearAllMocks();
});
it("should render with default state and empty job name", () => {
const mockContext = createMockContext();
const { container } = render(
<CopyJobContext.Provider value={mockContext}>
<PreviewCopyJob />
</CopyJobContext.Provider>,
);
expect(container.firstChild).toMatchSnapshot();
});
it("should render with pre-filled job name", () => {
const mockContext = createMockContext({
jobName: "custom-job-name-123",
});
const { container } = render(
<CopyJobContext.Provider value={mockContext}>
<PreviewCopyJob />
</CopyJobContext.Provider>,
);
expect(container.firstChild).toMatchSnapshot();
});
it("should render with missing source subscription information", () => {
const mockContext = createMockContext({
source: {
subscription: undefined,
account: mockDatabaseAccount,
databaseId: "source-database",
containerId: "source-container",
},
});
const { container } = render(
<CopyJobContext.Provider value={mockContext}>
<PreviewCopyJob />
</CopyJobContext.Provider>,
);
expect(container.firstChild).toMatchSnapshot();
});
it("should render with missing source account information", () => {
const mockContext = createMockContext({
source: {
subscription: mockSubscription,
account: null,
databaseId: "source-database",
containerId: "source-container",
},
});
const { container } = render(
<CopyJobContext.Provider value={mockContext}>
<PreviewCopyJob />
</CopyJobContext.Provider>,
);
expect(container.firstChild).toMatchSnapshot();
});
it("should render with undefined database and container names", () => {
const mockContext = createMockContext({
source: {
subscription: mockSubscription,
account: mockDatabaseAccount,
databaseId: "",
containerId: "",
},
target: {
subscriptionId: "test-subscription-id",
account: mockDatabaseAccount,
databaseId: "",
containerId: "",
},
});
const { container } = render(
<CopyJobContext.Provider value={mockContext}>
<PreviewCopyJob />
</CopyJobContext.Provider>,
);
expect(container.firstChild).toMatchSnapshot();
});
it("should render with long subscription and account names", () => {
const longNameSubscription: Subscription = {
...mockSubscription,
displayName: "This is a very long subscription name that might cause display issues if not handled properly",
};
const longNameAccount = {
...mockDatabaseAccount,
name: "this-is-a-very-long-database-account-name-that-might-cause-display-issues",
};
const mockContext = createMockContext({
source: {
subscription: longNameSubscription,
account: longNameAccount,
databaseId: "long-database-name-for-testing-purposes",
containerId: "long-container-name-for-testing-purposes",
},
});
const { container } = render(
<CopyJobContext.Provider value={mockContext}>
<PreviewCopyJob />
</CopyJobContext.Provider>,
);
expect(container.firstChild).toMatchSnapshot();
});
it("should render with online migration type", () => {
const mockContext = createMockContext({
migrationType: CopyJobMigrationType.Online,
jobName: "online-migration-job",
});
const { container } = render(
<CopyJobContext.Provider value={mockContext}>
<PreviewCopyJob />
</CopyJobContext.Provider>,
);
expect(container.firstChild).toMatchSnapshot();
});
it("should handle special characters in database and container names", () => {
const mockContext = createMockContext({
source: {
subscription: mockSubscription,
account: mockDatabaseAccount,
databaseId: "test-db_with@special#chars",
containerId: "test-container_with@special#chars",
},
target: {
subscriptionId: "test-subscription-id",
account: mockDatabaseAccount,
databaseId: "target-db_with@special#chars",
containerId: "target-container_with@special#chars",
},
jobName: "job-with@special#chars_123",
});
const { container } = render(
<CopyJobContext.Provider value={mockContext}>
<PreviewCopyJob />
</CopyJobContext.Provider>,
);
expect(container.firstChild).toMatchSnapshot();
});
it("should render component with cross-subscription setup", () => {
const targetAccount = {
...mockDatabaseAccount,
id: "/subscriptions/target-subscription-id/resourceGroups/target-rg/providers/Microsoft.DocumentDB/databaseAccounts/target-account",
name: "target-account",
};
const mockContext = createMockContext({
target: {
subscriptionId: "target-subscription-id",
account: targetAccount,
databaseId: "target-database",
containerId: "target-container",
},
sourceReadAccessFromTarget: true,
});
const { container } = render(
<CopyJobContext.Provider value={mockContext}>
<PreviewCopyJob />
</CopyJobContext.Provider>,
);
expect(container.firstChild).toMatchSnapshot();
});
it("should call setCopyJobState with default job name on mount", async () => {
const mockContext = createMockContext();
render(
<CopyJobContext.Provider value={mockContext}>
<PreviewCopyJob />
</CopyJobContext.Provider>,
);
await waitFor(() => {
expect(mockSetCopyJobState).toHaveBeenCalledWith(expect.any(Function));
});
});
it("should update job name when text field is changed", async () => {
const mockContext = createMockContext({
jobName: "initial-job-name",
});
const { getByDisplayValue } = render(
<CopyJobContext.Provider value={mockContext}>
<PreviewCopyJob />
</CopyJobContext.Provider>,
);
const jobNameInput = getByDisplayValue("initial-job-name");
fireEvent.change(jobNameInput, { target: { value: "updated-job-name" } });
expect(mockSetCopyJobState).toHaveBeenCalledWith(expect.any(Function));
});
it("should handle empty job name input", () => {
const mockContext = createMockContext({
jobName: "existing-name",
});
const { getByDisplayValue } = render(
<CopyJobContext.Provider value={mockContext}>
<PreviewCopyJob />
</CopyJobContext.Provider>,
);
const jobNameInput = getByDisplayValue("existing-name");
fireEvent.change(jobNameInput, { target: { value: "" } });
expect(mockSetCopyJobState).toHaveBeenCalledWith(expect.any(Function));
});
it("should display proper field labels from ContainerCopyMessages", () => {
const mockContext = createMockContext();
const { getByText } = render(
<CopyJobContext.Provider value={mockContext}>
<PreviewCopyJob />
</CopyJobContext.Provider>,
);
expect(getByText(/Job name/i)).toBeInTheDocument();
expect(getByText(/Source subscription/i)).toBeInTheDocument();
expect(getByText(/Source account/i)).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,8 @@
import { getPreviewCopyJobDetailsListColumns } from "./PreviewCopyJobUtils";
describe("PreviewCopyJobUtils", () => {
it("should return correctly formatted columns for preview copy job details list", () => {
const columns = getPreviewCopyJobDetailsListColumns();
expect(columns).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,62 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PreviewCopyJobUtils should return correctly formatted columns for preview copy job details list 1`] = `
[
{
"fieldName": "sourceDatabaseName",
"key": "sourcedbname",
"maxWidth": 140,
"minWidth": 130,
"name": "Source database",
"styles": {
"root": {
"lineHeight": "1.2",
"whiteSpace": "normal",
"wordBreak": "break-word",
},
},
},
{
"fieldName": "sourceContainerName",
"key": "sourcecolname",
"maxWidth": 140,
"minWidth": 130,
"name": "Source container",
"styles": {
"root": {
"lineHeight": "1.2",
"whiteSpace": "normal",
"wordBreak": "break-word",
},
},
},
{
"fieldName": "targetDatabaseName",
"key": "targetdbname",
"maxWidth": 140,
"minWidth": 130,
"name": "Destination database",
"styles": {
"root": {
"lineHeight": "1.2",
"whiteSpace": "normal",
"wordBreak": "break-word",
},
},
},
{
"fieldName": "targetContainerName",
"key": "targetcolname",
"maxWidth": 140,
"minWidth": 130,
"name": "Destination container",
"styles": {
"root": {
"lineHeight": "1.2",
"whiteSpace": "normal",
"wordBreak": "break-word",
},
},
},
]
`;

View File

@@ -0,0 +1,219 @@
import "@testing-library/jest-dom";
import { render } from "@testing-library/react";
import React from "react";
import { DropdownOptionType } from "../../../../Types/CopyJobTypes";
import { AccountDropdown } from "./AccountDropdown";
describe("AccountDropdown", () => {
const mockOnChange = jest.fn();
const mockAccountOptions: DropdownOptionType[] = [
{
key: "account-1",
text: "Development Account",
data: {
id: "account-1",
name: "Development Account",
location: "East US",
resourceGroup: "dev-rg",
kind: "GlobalDocumentDB",
properties: {
documentEndpoint: "https://dev-account.documents.azure.com:443/",
provisioningState: "Succeeded",
consistencyPolicy: {
defaultConsistencyLevel: "Session",
},
},
},
},
{
key: "account-2",
text: "Production Account",
data: {
id: "account-2",
name: "Production Account",
location: "West US 2",
resourceGroup: "prod-rg",
kind: "GlobalDocumentDB",
properties: {
documentEndpoint: "https://prod-account.documents.azure.com:443/",
provisioningState: "Succeeded",
consistencyPolicy: {
defaultConsistencyLevel: "Strong",
},
},
},
},
{
key: "account-3",
text: "Testing Account",
data: {
id: "account-3",
name: "Testing Account",
location: "Central US",
resourceGroup: "test-rg",
kind: "GlobalDocumentDB",
properties: {
documentEndpoint: "https://test-account.documents.azure.com:443/",
provisioningState: "Succeeded",
consistencyPolicy: {
defaultConsistencyLevel: "Eventual",
},
},
},
},
];
beforeEach(() => {
jest.clearAllMocks();
});
describe("Snapshot Testing", () => {
it("matches snapshot with all account options", () => {
const { container } = render(
<AccountDropdown options={mockAccountOptions} disabled={false} onChange={mockOnChange} />,
);
expect(container.firstChild).toMatchSnapshot();
});
it("matches snapshot with selected account", () => {
const { container } = render(
<AccountDropdown
options={mockAccountOptions}
selectedKey="account-2"
disabled={false}
onChange={mockOnChange}
/>,
);
expect(container.firstChild).toMatchSnapshot();
});
it("matches snapshot with disabled dropdown", () => {
const { container } = render(
<AccountDropdown
options={mockAccountOptions}
selectedKey="account-1"
disabled={true}
onChange={mockOnChange}
/>,
);
expect(container.firstChild).toMatchSnapshot();
});
it("matches snapshot with empty options", () => {
const { container } = render(<AccountDropdown options={[]} disabled={false} onChange={mockOnChange} />);
expect(container.firstChild).toMatchSnapshot();
});
it("matches snapshot with single option", () => {
const { container } = render(
<AccountDropdown
options={[mockAccountOptions[0]]}
selectedKey="account-1"
disabled={false}
onChange={mockOnChange}
/>,
);
expect(container.firstChild).toMatchSnapshot();
});
it("matches snapshot with special characters in options", () => {
const specialOptions = [
{
key: "special",
text: 'Account with & <special> "characters"',
data: {
id: "special",
name: 'Account with & <special> "characters"',
location: "East US",
},
},
];
const { container } = render(
<AccountDropdown options={specialOptions} disabled={false} onChange={mockOnChange} />,
);
expect(container.firstChild).toMatchSnapshot();
});
it("matches snapshot with long account name", () => {
const longNameOption = [
{
key: "long",
text: "This is an extremely long account name that tests how the component handles text overflow and layout constraints in the dropdown",
data: {
id: "long",
name: "This is an extremely long account name that tests how the component handles text overflow and layout constraints in the dropdown",
location: "North Central US",
},
},
];
const { container } = render(
<AccountDropdown options={longNameOption} selectedKey="long" disabled={false} onChange={mockOnChange} />,
);
expect(container.firstChild).toMatchSnapshot();
});
it("matches snapshot with disabled state and no selection", () => {
const { container } = render(
<AccountDropdown options={mockAccountOptions} disabled={true} onChange={mockOnChange} />,
);
expect(container.firstChild).toMatchSnapshot();
});
it("matches snapshot with multiple account types", () => {
const mixedAccountOptions = [
{
key: "sql-account",
text: "SQL API Account",
data: {
id: "sql-account",
name: "SQL API Account",
kind: "GlobalDocumentDB",
location: "East US",
},
},
{
key: "mongo-account",
text: "MongoDB Account",
data: {
id: "mongo-account",
name: "MongoDB Account",
kind: "MongoDB",
location: "West US",
},
},
{
key: "cassandra-account",
text: "Cassandra Account",
data: {
id: "cassandra-account",
name: "Cassandra Account",
kind: "Cassandra",
location: "Central US",
},
},
];
const { container } = render(
<AccountDropdown
options={mixedAccountOptions}
selectedKey="mongo-account"
disabled={false}
onChange={mockOnChange}
/>,
);
expect(container.firstChild).toMatchSnapshot();
});
});
});

View File

@@ -0,0 +1,72 @@
import "@testing-library/jest-dom";
import { render, screen } from "@testing-library/react";
import React from "react";
import { MigrationTypeCheckbox } from "./MigrationTypeCheckbox";
describe("MigrationTypeCheckbox", () => {
const mockOnChange = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
});
afterEach(() => {
jest.clearAllMocks();
});
describe("Component Rendering", () => {
it("should render with default props (unchecked state)", () => {
const { container } = render(<MigrationTypeCheckbox checked={false} onChange={mockOnChange} />);
expect(container.firstChild).toMatchSnapshot();
});
it("should render in checked state", () => {
const { container } = render(<MigrationTypeCheckbox checked={true} onChange={mockOnChange} />);
expect(container.firstChild).toMatchSnapshot();
});
it("should display the correct label text", () => {
render(<MigrationTypeCheckbox checked={false} onChange={mockOnChange} />);
const checkbox = screen.getByRole("checkbox");
expect(checkbox).toBeInTheDocument();
const label = screen.getByText("Copy container in offline mode");
expect(label).toBeInTheDocument();
});
it("should have correct accessibility attributes when checked", () => {
render(<MigrationTypeCheckbox checked={true} onChange={mockOnChange} />);
const checkbox = screen.getByRole("checkbox");
expect(checkbox).toBeChecked();
expect(checkbox).toHaveAttribute("checked");
});
});
describe("FluentUI Integration", () => {
it("should render FluentUI Checkbox component correctly", () => {
render(<MigrationTypeCheckbox checked={false} onChange={mockOnChange} />);
const checkbox = screen.getByRole("checkbox");
expect(checkbox).toBeInTheDocument();
expect(checkbox).toHaveAttribute("type", "checkbox");
});
it("should render FluentUI Stack component correctly", () => {
render(<MigrationTypeCheckbox checked={false} onChange={mockOnChange} />);
const stackContainer = document.querySelector(".migrationTypeRow");
expect(stackContainer).toBeInTheDocument();
});
it("should apply FluentUI Stack horizontal alignment correctly", () => {
const { container } = render(<MigrationTypeCheckbox checked={false} onChange={mockOnChange} />);
const stackContainer = container.querySelector(".migrationTypeRow");
expect(stackContainer).toBeInTheDocument();
});
});
});

View File

@@ -0,0 +1,118 @@
import "@testing-library/jest-dom";
import { render } from "@testing-library/react";
import React from "react";
import { DropdownOptionType } from "../../../../Types/CopyJobTypes";
import { SubscriptionDropdown } from "./SubscriptionDropdown";
describe("SubscriptionDropdown", () => {
const mockOnChange = jest.fn();
const mockSubscriptionOptions: DropdownOptionType[] = [
{
key: "sub-1",
text: "Development Subscription",
data: {
subscriptionId: "sub-1",
displayName: "Development Subscription",
authorizationSource: "RoleBased",
subscriptionPolicies: {
quotaId: "quota-1",
spendingLimit: "Off",
locationPlacementId: "loc-1",
},
},
},
{
key: "sub-2",
text: "Production Subscription",
data: {
subscriptionId: "sub-2",
displayName: "Production Subscription",
authorizationSource: "RoleBased",
subscriptionPolicies: {
quotaId: "quota-2",
spendingLimit: "On",
locationPlacementId: "loc-2",
},
},
},
{
key: "sub-3",
text: "Testing Subscription",
data: {
subscriptionId: "sub-3",
displayName: "Testing Subscription",
authorizationSource: "Legacy",
subscriptionPolicies: {
quotaId: "quota-3",
spendingLimit: "Off",
locationPlacementId: "loc-3",
},
},
},
];
beforeEach(() => {
jest.clearAllMocks();
});
describe("Snapshot Testing", () => {
it("matches snapshot with all subscription options", () => {
const { container } = render(<SubscriptionDropdown options={mockSubscriptionOptions} onChange={mockOnChange} />);
expect(container.firstChild).toMatchSnapshot();
});
it("matches snapshot with selected subscription", () => {
const { container } = render(
<SubscriptionDropdown options={mockSubscriptionOptions} selectedKey="sub-2" onChange={mockOnChange} />,
);
expect(container.firstChild).toMatchSnapshot();
});
it("matches snapshot with empty options", () => {
const { container } = render(<SubscriptionDropdown options={[]} onChange={mockOnChange} />);
expect(container.firstChild).toMatchSnapshot();
});
it("matches snapshot with single option", () => {
const { container } = render(
<SubscriptionDropdown options={[mockSubscriptionOptions[0]]} selectedKey="sub-1" onChange={mockOnChange} />,
);
expect(container.firstChild).toMatchSnapshot();
});
it("matches snapshot with special characters in options", () => {
const specialOptions = [
{
key: "special",
text: 'Subscription with & <special> "characters"',
data: { subscriptionId: "special" },
},
];
const { container } = render(<SubscriptionDropdown options={specialOptions} onChange={mockOnChange} />);
expect(container.firstChild).toMatchSnapshot();
});
it("matches snapshot with long subscription name", () => {
const longNameOption = [
{
key: "long",
text: "This is an extremely long subscription name that tests how the component handles text overflow and layout constraints",
data: { subscriptionId: "long" },
},
];
const { container } = render(
<SubscriptionDropdown options={longNameOption} selectedKey="long" onChange={mockOnChange} />,
);
expect(container.firstChild).toMatchSnapshot();
});
});
});

View File

@@ -0,0 +1,514 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AccountDropdown Snapshot Testing matches snapshot with all account options 1`] = `
<div
class="ms-Stack flex-row css-109"
>
<div
class="ms-StackItem flex-fixed-width css-110"
>
<label
class="field-label "
>
Account
:
</label>
</div>
<div
class="ms-StackItem flex-grow-col css-110"
>
<div
class="ms-Dropdown-container"
>
<div
aria-disabled="false"
aria-expanded="false"
aria-haspopup="listbox"
aria-label="Account"
aria-required="true"
class="ms-Dropdown is-required dropdown-111"
data-is-focusable="true"
data-ktp-target="true"
id="Dropdown0"
role="combobox"
tabindex="0"
>
<span
aria-invalid="false"
class="ms-Dropdown-title ms-Dropdown-titleIsPlaceHolder title-112"
id="Dropdown0-option"
>
Select an account
</span>
<span
class="ms-Dropdown-caretDownWrapper caretDownWrapper-113"
>
<i
aria-hidden="true"
class="ms-Dropdown-caretDown caretDown-131"
data-icon-name="ChevronDown"
>
</i>
</span>
</div>
</div>
</div>
</div>
`;
exports[`AccountDropdown Snapshot Testing matches snapshot with disabled dropdown 1`] = `
<div
class="ms-Stack flex-row css-109"
>
<div
class="ms-StackItem flex-fixed-width css-110"
>
<label
class="field-label "
>
Account
:
</label>
</div>
<div
class="ms-StackItem flex-grow-col css-110"
>
<div
class="ms-Dropdown-container"
>
<div
aria-disabled="true"
aria-expanded="false"
aria-haspopup="listbox"
aria-label="Account"
aria-required="true"
class="ms-Dropdown is-disabled is-required dropdown-133"
data-is-focusable="false"
data-ktp-target="true"
id="Dropdown2"
role="combobox"
tabindex="-1"
>
<span
aria-invalid="false"
class="ms-Dropdown-title title-138"
id="Dropdown2-option"
>
Development Account
</span>
<span
class="ms-Dropdown-caretDownWrapper caretDownWrapper-135"
>
<i
aria-hidden="true"
class="ms-Dropdown-caretDown caretDown-137"
data-icon-name="ChevronDown"
>
</i>
</span>
</div>
</div>
</div>
</div>
`;
exports[`AccountDropdown Snapshot Testing matches snapshot with disabled state and no selection 1`] = `
<div
class="ms-Stack flex-row css-109"
>
<div
class="ms-StackItem flex-fixed-width css-110"
>
<label
class="field-label "
>
Account
:
</label>
</div>
<div
class="ms-StackItem flex-grow-col css-110"
>
<div
class="ms-Dropdown-container"
>
<div
aria-disabled="true"
aria-expanded="false"
aria-haspopup="listbox"
aria-label="Account"
aria-required="true"
class="ms-Dropdown is-disabled is-required dropdown-133"
data-is-focusable="false"
data-ktp-target="true"
id="Dropdown7"
role="combobox"
tabindex="-1"
>
<span
aria-invalid="false"
class="ms-Dropdown-title ms-Dropdown-titleIsPlaceHolder title-134"
id="Dropdown7-option"
>
Select an account
</span>
<span
class="ms-Dropdown-caretDownWrapper caretDownWrapper-135"
>
<i
aria-hidden="true"
class="ms-Dropdown-caretDown caretDown-137"
data-icon-name="ChevronDown"
>
</i>
</span>
</div>
</div>
</div>
</div>
`;
exports[`AccountDropdown Snapshot Testing matches snapshot with empty options 1`] = `
<div
class="ms-Stack flex-row css-109"
>
<div
class="ms-StackItem flex-fixed-width css-110"
>
<label
class="field-label "
>
Account
:
</label>
</div>
<div
class="ms-StackItem flex-grow-col css-110"
>
<div
class="ms-Dropdown-container"
>
<div
aria-disabled="false"
aria-expanded="false"
aria-haspopup="listbox"
aria-label="Account"
aria-required="true"
class="ms-Dropdown is-required dropdown-111"
data-is-focusable="true"
data-ktp-target="true"
id="Dropdown3"
role="combobox"
tabindex="0"
>
<span
aria-invalid="false"
class="ms-Dropdown-title ms-Dropdown-titleIsPlaceHolder title-112"
id="Dropdown3-option"
>
Select an account
</span>
<span
class="ms-Dropdown-caretDownWrapper caretDownWrapper-113"
>
<i
aria-hidden="true"
class="ms-Dropdown-caretDown caretDown-131"
data-icon-name="ChevronDown"
>
</i>
</span>
</div>
</div>
</div>
</div>
`;
exports[`AccountDropdown Snapshot Testing matches snapshot with long account name 1`] = `
<div
class="ms-Stack flex-row css-109"
>
<div
class="ms-StackItem flex-fixed-width css-110"
>
<label
class="field-label "
>
Account
:
</label>
</div>
<div
class="ms-StackItem flex-grow-col css-110"
>
<div
class="ms-Dropdown-container"
>
<div
aria-disabled="false"
aria-expanded="false"
aria-haspopup="listbox"
aria-label="Account"
aria-required="true"
class="ms-Dropdown is-required dropdown-111"
data-is-focusable="true"
data-ktp-target="true"
id="Dropdown6"
role="combobox"
tabindex="0"
>
<span
aria-invalid="false"
class="ms-Dropdown-title title-132"
id="Dropdown6-option"
>
This is an extremely long account name that tests how the component handles text overflow and layout constraints in the dropdown
</span>
<span
class="ms-Dropdown-caretDownWrapper caretDownWrapper-113"
>
<i
aria-hidden="true"
class="ms-Dropdown-caretDown caretDown-131"
data-icon-name="ChevronDown"
>
</i>
</span>
</div>
</div>
</div>
</div>
`;
exports[`AccountDropdown Snapshot Testing matches snapshot with multiple account types 1`] = `
<div
class="ms-Stack flex-row css-109"
>
<div
class="ms-StackItem flex-fixed-width css-110"
>
<label
class="field-label "
>
Account
:
</label>
</div>
<div
class="ms-StackItem flex-grow-col css-110"
>
<div
class="ms-Dropdown-container"
>
<div
aria-disabled="false"
aria-expanded="false"
aria-haspopup="listbox"
aria-label="Account"
aria-required="true"
class="ms-Dropdown is-required dropdown-111"
data-is-focusable="true"
data-ktp-target="true"
id="Dropdown8"
role="combobox"
tabindex="0"
>
<span
aria-invalid="false"
class="ms-Dropdown-title title-132"
id="Dropdown8-option"
>
MongoDB Account
</span>
<span
class="ms-Dropdown-caretDownWrapper caretDownWrapper-113"
>
<i
aria-hidden="true"
class="ms-Dropdown-caretDown caretDown-131"
data-icon-name="ChevronDown"
>
</i>
</span>
</div>
</div>
</div>
</div>
`;
exports[`AccountDropdown Snapshot Testing matches snapshot with selected account 1`] = `
<div
class="ms-Stack flex-row css-109"
>
<div
class="ms-StackItem flex-fixed-width css-110"
>
<label
class="field-label "
>
Account
:
</label>
</div>
<div
class="ms-StackItem flex-grow-col css-110"
>
<div
class="ms-Dropdown-container"
>
<div
aria-disabled="false"
aria-expanded="false"
aria-haspopup="listbox"
aria-label="Account"
aria-required="true"
class="ms-Dropdown is-required dropdown-111"
data-is-focusable="true"
data-ktp-target="true"
id="Dropdown1"
role="combobox"
tabindex="0"
>
<span
aria-invalid="false"
class="ms-Dropdown-title title-132"
id="Dropdown1-option"
>
Production Account
</span>
<span
class="ms-Dropdown-caretDownWrapper caretDownWrapper-113"
>
<i
aria-hidden="true"
class="ms-Dropdown-caretDown caretDown-131"
data-icon-name="ChevronDown"
>
</i>
</span>
</div>
</div>
</div>
</div>
`;
exports[`AccountDropdown Snapshot Testing matches snapshot with single option 1`] = `
<div
class="ms-Stack flex-row css-109"
>
<div
class="ms-StackItem flex-fixed-width css-110"
>
<label
class="field-label "
>
Account
:
</label>
</div>
<div
class="ms-StackItem flex-grow-col css-110"
>
<div
class="ms-Dropdown-container"
>
<div
aria-disabled="false"
aria-expanded="false"
aria-haspopup="listbox"
aria-label="Account"
aria-required="true"
class="ms-Dropdown is-required dropdown-111"
data-is-focusable="true"
data-ktp-target="true"
id="Dropdown4"
role="combobox"
tabindex="0"
>
<span
aria-invalid="false"
class="ms-Dropdown-title title-132"
id="Dropdown4-option"
>
Development Account
</span>
<span
class="ms-Dropdown-caretDownWrapper caretDownWrapper-113"
>
<i
aria-hidden="true"
class="ms-Dropdown-caretDown caretDown-131"
data-icon-name="ChevronDown"
>
</i>
</span>
</div>
</div>
</div>
</div>
`;
exports[`AccountDropdown Snapshot Testing matches snapshot with special characters in options 1`] = `
<div
class="ms-Stack flex-row css-109"
>
<div
class="ms-StackItem flex-fixed-width css-110"
>
<label
class="field-label "
>
Account
:
</label>
</div>
<div
class="ms-StackItem flex-grow-col css-110"
>
<div
class="ms-Dropdown-container"
>
<div
aria-disabled="false"
aria-expanded="false"
aria-haspopup="listbox"
aria-label="Account"
aria-required="true"
class="ms-Dropdown is-required dropdown-111"
data-is-focusable="true"
data-ktp-target="true"
id="Dropdown5"
role="combobox"
tabindex="0"
>
<span
aria-invalid="false"
class="ms-Dropdown-title ms-Dropdown-titleIsPlaceHolder title-112"
id="Dropdown5-option"
>
Select an account
</span>
<span
class="ms-Dropdown-caretDownWrapper caretDownWrapper-113"
>
<i
aria-hidden="true"
class="ms-Dropdown-caretDown caretDown-131"
data-icon-name="ChevronDown"
>
</i>
</span>
</div>
</div>
</div>
</div>
`;

View File

@@ -0,0 +1,80 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`MigrationTypeCheckbox Component Rendering should render in checked state 1`] = `
<div
class="ms-Stack migrationTypeRow css-109"
>
<div
class="ms-Checkbox is-checked is-enabled root-119"
>
<input
checked=""
class="input-111"
data-ktp-execute-target="true"
id="checkbox-1"
type="checkbox"
/>
<label
class="ms-Checkbox-label label-112"
for="checkbox-1"
>
<div
class="ms-Checkbox-checkbox checkbox-120"
data-ktp-target="true"
>
<i
aria-hidden="true"
class="ms-Checkbox-checkmark checkmark-122"
data-icon-name="CheckMark"
>
</i>
</div>
<span
class="ms-Checkbox-text text-115"
>
Copy container in offline mode
</span>
</label>
</div>
</div>
`;
exports[`MigrationTypeCheckbox Component Rendering should render with default props (unchecked state) 1`] = `
<div
class="ms-Stack migrationTypeRow css-109"
>
<div
class="ms-Checkbox is-enabled root-110"
>
<input
class="input-111"
data-ktp-execute-target="true"
id="checkbox-0"
type="checkbox"
/>
<label
class="ms-Checkbox-label label-112"
for="checkbox-0"
>
<div
class="ms-Checkbox-checkbox checkbox-113"
data-ktp-target="true"
>
<i
aria-hidden="true"
class="ms-Checkbox-checkmark checkmark-118"
data-icon-name="CheckMark"
>
</i>
</div>
<span
class="ms-Checkbox-text text-115"
>
Copy container in offline mode
</span>
</label>
</div>
</div>
`;

View File

@@ -0,0 +1,337 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SubscriptionDropdown Snapshot Testing matches snapshot with all subscription options 1`] = `
<div
class="ms-Stack flex-row css-109"
>
<div
class="ms-StackItem flex-fixed-width css-110"
>
<label
class="field-label "
>
Subscription
:
</label>
</div>
<div
class="ms-StackItem flex-grow-col css-110"
>
<div
class="ms-Dropdown-container"
>
<div
aria-expanded="false"
aria-haspopup="listbox"
aria-label="Subscription"
aria-required="true"
class="ms-Dropdown is-required dropdown-111"
data-is-focusable="true"
data-ktp-target="true"
id="Dropdown0"
role="combobox"
tabindex="0"
>
<span
aria-invalid="false"
class="ms-Dropdown-title ms-Dropdown-titleIsPlaceHolder title-112"
id="Dropdown0-option"
>
Select a subscription
</span>
<span
class="ms-Dropdown-caretDownWrapper caretDownWrapper-113"
>
<i
aria-hidden="true"
class="ms-Dropdown-caretDown caretDown-131"
data-icon-name="ChevronDown"
>
</i>
</span>
</div>
</div>
</div>
</div>
`;
exports[`SubscriptionDropdown Snapshot Testing matches snapshot with empty options 1`] = `
<div
class="ms-Stack flex-row css-109"
>
<div
class="ms-StackItem flex-fixed-width css-110"
>
<label
class="field-label "
>
Subscription
:
</label>
</div>
<div
class="ms-StackItem flex-grow-col css-110"
>
<div
class="ms-Dropdown-container"
>
<div
aria-expanded="false"
aria-haspopup="listbox"
aria-label="Subscription"
aria-required="true"
class="ms-Dropdown is-required dropdown-111"
data-is-focusable="true"
data-ktp-target="true"
id="Dropdown2"
role="combobox"
tabindex="0"
>
<span
aria-invalid="false"
class="ms-Dropdown-title ms-Dropdown-titleIsPlaceHolder title-112"
id="Dropdown2-option"
>
Select a subscription
</span>
<span
class="ms-Dropdown-caretDownWrapper caretDownWrapper-113"
>
<i
aria-hidden="true"
class="ms-Dropdown-caretDown caretDown-131"
data-icon-name="ChevronDown"
>
</i>
</span>
</div>
</div>
</div>
</div>
`;
exports[`SubscriptionDropdown Snapshot Testing matches snapshot with long subscription name 1`] = `
<div
class="ms-Stack flex-row css-109"
>
<div
class="ms-StackItem flex-fixed-width css-110"
>
<label
class="field-label "
>
Subscription
:
</label>
</div>
<div
class="ms-StackItem flex-grow-col css-110"
>
<div
class="ms-Dropdown-container"
>
<div
aria-expanded="false"
aria-haspopup="listbox"
aria-label="Subscription"
aria-required="true"
class="ms-Dropdown is-required dropdown-111"
data-is-focusable="true"
data-ktp-target="true"
id="Dropdown5"
role="combobox"
tabindex="0"
>
<span
aria-invalid="false"
class="ms-Dropdown-title title-132"
id="Dropdown5-option"
>
This is an extremely long subscription name that tests how the component handles text overflow and layout constraints
</span>
<span
class="ms-Dropdown-caretDownWrapper caretDownWrapper-113"
>
<i
aria-hidden="true"
class="ms-Dropdown-caretDown caretDown-131"
data-icon-name="ChevronDown"
>
</i>
</span>
</div>
</div>
</div>
</div>
`;
exports[`SubscriptionDropdown Snapshot Testing matches snapshot with selected subscription 1`] = `
<div
class="ms-Stack flex-row css-109"
>
<div
class="ms-StackItem flex-fixed-width css-110"
>
<label
class="field-label "
>
Subscription
:
</label>
</div>
<div
class="ms-StackItem flex-grow-col css-110"
>
<div
class="ms-Dropdown-container"
>
<div
aria-expanded="false"
aria-haspopup="listbox"
aria-label="Subscription"
aria-required="true"
class="ms-Dropdown is-required dropdown-111"
data-is-focusable="true"
data-ktp-target="true"
id="Dropdown1"
role="combobox"
tabindex="0"
>
<span
aria-invalid="false"
class="ms-Dropdown-title title-132"
id="Dropdown1-option"
>
Production Subscription
</span>
<span
class="ms-Dropdown-caretDownWrapper caretDownWrapper-113"
>
<i
aria-hidden="true"
class="ms-Dropdown-caretDown caretDown-131"
data-icon-name="ChevronDown"
>
</i>
</span>
</div>
</div>
</div>
</div>
`;
exports[`SubscriptionDropdown Snapshot Testing matches snapshot with single option 1`] = `
<div
class="ms-Stack flex-row css-109"
>
<div
class="ms-StackItem flex-fixed-width css-110"
>
<label
class="field-label "
>
Subscription
:
</label>
</div>
<div
class="ms-StackItem flex-grow-col css-110"
>
<div
class="ms-Dropdown-container"
>
<div
aria-expanded="false"
aria-haspopup="listbox"
aria-label="Subscription"
aria-required="true"
class="ms-Dropdown is-required dropdown-111"
data-is-focusable="true"
data-ktp-target="true"
id="Dropdown3"
role="combobox"
tabindex="0"
>
<span
aria-invalid="false"
class="ms-Dropdown-title title-132"
id="Dropdown3-option"
>
Development Subscription
</span>
<span
class="ms-Dropdown-caretDownWrapper caretDownWrapper-113"
>
<i
aria-hidden="true"
class="ms-Dropdown-caretDown caretDown-131"
data-icon-name="ChevronDown"
>
</i>
</span>
</div>
</div>
</div>
</div>
`;
exports[`SubscriptionDropdown Snapshot Testing matches snapshot with special characters in options 1`] = `
<div
class="ms-Stack flex-row css-109"
>
<div
class="ms-StackItem flex-fixed-width css-110"
>
<label
class="field-label "
>
Subscription
:
</label>
</div>
<div
class="ms-StackItem flex-grow-col css-110"
>
<div
class="ms-Dropdown-container"
>
<div
aria-expanded="false"
aria-haspopup="listbox"
aria-label="Subscription"
aria-required="true"
class="ms-Dropdown is-required dropdown-111"
data-is-focusable="true"
data-ktp-target="true"
id="Dropdown4"
role="combobox"
tabindex="0"
>
<span
aria-invalid="false"
class="ms-Dropdown-title ms-Dropdown-titleIsPlaceHolder title-112"
id="Dropdown4-option"
>
Select a subscription
</span>
<span
class="ms-Dropdown-caretDownWrapper caretDownWrapper-113"
>
<i
aria-hidden="true"
class="ms-Dropdown-caretDown caretDown-131"
data-icon-name="ChevronDown"
>
</i>
</span>
</div>
</div>
</div>
</div>
`;

View File

@@ -0,0 +1,480 @@
import "@testing-library/jest-dom";
import { fireEvent, render, screen } from "@testing-library/react";
import React from "react";
import { apiType } from "UserContext";
import { DatabaseAccount, Subscription } from "../../../../../Contracts/DataModels";
import { useDatabaseAccounts } from "../../../../../hooks/useDatabaseAccounts";
import { useSubscriptions } from "../../../../../hooks/useSubscriptions";
import { CopyJobMigrationType } from "../../../Enums/CopyJobEnums";
import { CopyJobContextProviderType, CopyJobContextState } from "../../../Types/CopyJobTypes";
import SelectAccount from "./SelectAccount";
jest.mock("UserContext", () => ({
apiType: jest.fn(),
}));
jest.mock("../../../../../hooks/useDatabaseAccounts");
jest.mock("../../../../../hooks/useSubscriptions");
jest.mock("../../../Context/CopyJobContext", () => ({
useCopyJobContext: () => mockContextValue,
}));
jest.mock("./Utils/selectAccountUtils", () => ({
useDropdownOptions: jest.fn(),
useEventHandlers: jest.fn(),
}));
jest.mock("./Components/SubscriptionDropdown", () => ({
SubscriptionDropdown: jest.fn(({ options, selectedKey, onChange, ...props }) => (
<div data-testid="subscription-dropdown" data-selected={selectedKey} {...props}>
{options?.map((option: any) => (
<div
key={option.key}
data-testid={`subscription-option-${option.key}`}
onClick={() => onChange?.(undefined, option)}
>
{option.text}
</div>
))}
</div>
)),
}));
jest.mock("./Components/AccountDropdown", () => ({
AccountDropdown: jest.fn(({ options, selectedKey, disabled, onChange, ...props }) => (
<div data-testid="account-dropdown" data-selected={selectedKey} data-disabled={disabled} {...props}>
{options?.map((option: any) => (
<div
key={option.key}
data-testid={`account-option-${option.key}`}
onClick={() => onChange?.(undefined, option)}
>
{option.text}
</div>
))}
</div>
)),
}));
jest.mock("./Components/MigrationTypeCheckbox", () => ({
MigrationTypeCheckbox: jest.fn(({ checked, onChange, ...props }) => (
<div data-testid="migration-type-checkbox" data-checked={checked} {...props}>
<input
type="checkbox"
checked={checked}
onChange={(e) => onChange?.(e, e.target.checked)}
data-testid="migration-checkbox-input"
/>
</div>
)),
}));
jest.mock("../../../ContainerCopyMessages", () => ({
selectAccountDescription: "Select your source account and subscription",
}));
const mockUseDatabaseAccounts = useDatabaseAccounts as jest.MockedFunction<typeof useDatabaseAccounts>;
const mockUseSubscriptions = useSubscriptions as jest.MockedFunction<typeof useSubscriptions>;
const mockApiType = apiType as jest.MockedFunction<typeof apiType>;
import { useDropdownOptions, useEventHandlers } from "./Utils/selectAccountUtils";
const mockUseDropdownOptions = useDropdownOptions as jest.MockedFunction<typeof useDropdownOptions>;
const mockUseEventHandlers = useEventHandlers as jest.MockedFunction<typeof useEventHandlers>;
const mockSubscriptions = [
{
subscriptionId: "sub-1",
displayName: "Test Subscription 1",
authorizationSource: "RoleBased",
subscriptionPolicies: {
quotaId: "quota-1",
spendingLimit: "Off",
locationPlacementId: "loc-1",
},
},
{
subscriptionId: "sub-2",
displayName: "Test Subscription 2",
authorizationSource: "RoleBased",
subscriptionPolicies: {
quotaId: "quota-2",
spendingLimit: "On",
locationPlacementId: "loc-2",
},
},
] as Subscription[];
const mockAccounts = [
{
id: "/subscriptions/sub-1/resourceGroups/rg-1/providers/Microsoft.DocumentDB/databaseAccounts/account-1",
name: "test-cosmos-account-1",
location: "East US",
kind: "GlobalDocumentDB",
properties: {
documentEndpoint: "https://account-1.documents.azure.com/",
capabilities: [],
enableFreeTier: false,
},
},
{
id: "/subscriptions/sub-1/resourceGroups/rg-1/providers/Microsoft.DocumentDB/databaseAccounts/account-2",
name: "test-cosmos-account-2",
location: "West US",
kind: "MongoDB",
properties: {
documentEndpoint: "https://account-2.documents.azure.com/",
capabilities: [],
},
},
] as DatabaseAccount[];
const mockDropdownOptions = {
subscriptionOptions: [
{ key: "sub-1", text: "Test Subscription 1", data: mockSubscriptions[0] },
{ key: "sub-2", text: "Test Subscription 2", data: mockSubscriptions[1] },
],
accountOptions: [{ key: mockAccounts[0].id, text: mockAccounts[0].name, data: mockAccounts[0] }],
};
const mockEventHandlers = {
handleSelectSourceAccount: jest.fn(),
handleMigrationTypeChange: jest.fn(),
};
let mockContextValue = {
copyJobState: {
jobName: "",
migrationType: CopyJobMigrationType.Offline,
source: {
subscription: null,
account: null,
databaseId: "",
containerId: "",
},
target: {
subscriptionId: "",
account: null,
databaseId: "",
containerId: "",
},
sourceReadAccessFromTarget: false,
} as CopyJobContextState,
setCopyJobState: jest.fn(),
flow: null,
setFlow: jest.fn(),
contextError: null,
setContextError: jest.fn(),
resetCopyJobState: jest.fn(),
explorer: {} as any,
} as CopyJobContextProviderType;
describe("SelectAccount Component", () => {
beforeEach(() => {
jest.clearAllMocks();
mockContextValue = {
copyJobState: {
jobName: "",
migrationType: CopyJobMigrationType.Offline,
source: {
subscription: null,
account: null,
databaseId: "",
containerId: "",
},
target: {
subscriptionId: "",
account: null,
databaseId: "",
containerId: "",
},
sourceReadAccessFromTarget: false,
} as CopyJobContextState,
setCopyJobState: jest.fn(),
flow: null,
setFlow: jest.fn(),
contextError: null,
setContextError: jest.fn(),
resetCopyJobState: jest.fn(),
explorer: {} as any,
};
mockUseSubscriptions.mockReturnValue(mockSubscriptions);
mockUseDatabaseAccounts.mockReturnValue(mockAccounts);
mockApiType.mockReturnValue("SQL");
mockUseDropdownOptions.mockReturnValue(mockDropdownOptions);
mockUseEventHandlers.mockReturnValue(mockEventHandlers);
});
describe("Rendering", () => {
it("should render component with default state", () => {
const { container } = render(<SelectAccount />);
expect(screen.getByText("Select your source account and subscription")).toBeInTheDocument();
expect(screen.getByTestId("subscription-dropdown")).toBeInTheDocument();
expect(screen.getByTestId("account-dropdown")).toBeInTheDocument();
expect(screen.getByTestId("migration-type-checkbox")).toBeInTheDocument();
expect(container).toMatchSnapshot();
});
it("should render with selected subscription", () => {
mockContextValue.copyJobState.source.subscription = mockSubscriptions[0];
const { container } = render(<SelectAccount />);
expect(screen.getByTestId("subscription-dropdown")).toHaveAttribute("data-selected", "sub-1");
expect(container).toMatchSnapshot();
});
it("should render with selected account", () => {
mockContextValue.copyJobState.source.subscription = mockSubscriptions[0];
mockContextValue.copyJobState.source.account = mockAccounts[0];
const { container } = render(<SelectAccount />);
expect(screen.getByTestId("account-dropdown")).toHaveAttribute("data-selected", mockAccounts[0].id);
expect(container).toMatchSnapshot();
});
it("should render with offline migration type checked", () => {
mockContextValue.copyJobState.migrationType = CopyJobMigrationType.Offline;
const { container } = render(<SelectAccount />);
expect(screen.getByTestId("migration-type-checkbox")).toHaveAttribute("data-checked", "true");
expect(container).toMatchSnapshot();
});
it("should render with online migration type unchecked", () => {
mockContextValue.copyJobState.migrationType = CopyJobMigrationType.Online;
const { container } = render(<SelectAccount />);
expect(screen.getByTestId("migration-type-checkbox")).toHaveAttribute("data-checked", "false");
expect(container).toMatchSnapshot();
});
});
describe("Hook Integration", () => {
it("should call useSubscriptions hook", () => {
render(<SelectAccount />);
expect(mockUseSubscriptions).toHaveBeenCalledTimes(1);
});
it("should call useDatabaseAccounts with selected subscription ID", () => {
mockContextValue.copyJobState.source.subscription = mockSubscriptions[0];
render(<SelectAccount />);
expect(mockUseDatabaseAccounts).toHaveBeenCalledWith("sub-1");
});
it("should call useDatabaseAccounts with undefined when no subscription selected", () => {
render(<SelectAccount />);
expect(mockUseDatabaseAccounts).toHaveBeenCalledWith(undefined);
});
it("should filter accounts to SQL API only", () => {
mockApiType.mockReturnValueOnce("SQL").mockReturnValueOnce("Mongo");
render(<SelectAccount />);
expect(mockApiType).toHaveBeenCalledTimes(2);
expect(mockApiType).toHaveBeenCalledWith(mockAccounts[0]);
expect(mockApiType).toHaveBeenCalledWith(mockAccounts[1]);
});
it("should call useDropdownOptions with correct parameters", () => {
const sqlOnlyAccounts = [mockAccounts[0]]; // Only SQL account
mockApiType.mockImplementation((account) => (account === mockAccounts[0] ? "SQL" : "Mongo"));
render(<SelectAccount />);
expect(mockUseDropdownOptions).toHaveBeenCalledWith(mockSubscriptions, sqlOnlyAccounts);
});
it("should call useEventHandlers with setCopyJobState", () => {
render(<SelectAccount />);
expect(mockUseEventHandlers).toHaveBeenCalledWith(mockContextValue.setCopyJobState);
});
});
describe("Event Handling", () => {
it("should handle subscription selection", () => {
render(<SelectAccount />);
const subscriptionOption = screen.getByTestId("subscription-option-sub-1");
fireEvent.click(subscriptionOption);
expect(mockEventHandlers.handleSelectSourceAccount).toHaveBeenCalledWith("subscription", mockSubscriptions[0]);
});
it("should handle account selection", () => {
render(<SelectAccount />);
const accountOption = screen.getByTestId(`account-option-${mockAccounts[0].id}`);
fireEvent.click(accountOption);
expect(mockEventHandlers.handleSelectSourceAccount).toHaveBeenCalledWith("account", mockAccounts[0]);
});
it("should handle migration type change", () => {
render(<SelectAccount />);
const checkbox = screen.getByTestId("migration-checkbox-input");
fireEvent.click(checkbox);
expect(mockEventHandlers.handleMigrationTypeChange).toHaveBeenCalledWith(expect.any(Object), false);
});
});
describe("Dropdown States", () => {
it("should disable account dropdown when no subscription is selected", () => {
render(<SelectAccount />);
expect(screen.getByTestId("account-dropdown")).toHaveAttribute("data-disabled", "true");
});
it("should enable account dropdown when subscription is selected", () => {
mockContextValue.copyJobState.source.subscription = mockSubscriptions[0];
render(<SelectAccount />);
expect(screen.getByTestId("account-dropdown")).toHaveAttribute("data-disabled", "false");
});
});
describe("Component Props", () => {
it("should pass correct props to SubscriptionDropdown", () => {
render(<SelectAccount />);
const dropdown = screen.getByTestId("subscription-dropdown");
expect(dropdown).not.toHaveAttribute("data-selected");
});
it("should pass selected subscription ID to SubscriptionDropdown", () => {
mockContextValue.copyJobState.source.subscription = mockSubscriptions[0];
render(<SelectAccount />);
const dropdown = screen.getByTestId("subscription-dropdown");
expect(dropdown).toHaveAttribute("data-selected", "sub-1");
});
it("should pass correct props to AccountDropdown", () => {
render(<SelectAccount />);
const dropdown = screen.getByTestId("account-dropdown");
expect(dropdown).not.toHaveAttribute("data-selected");
expect(dropdown).toHaveAttribute("data-disabled", "true");
});
it("should pass selected account ID to AccountDropdown", () => {
mockContextValue.copyJobState.source.account = mockAccounts[0];
render(<SelectAccount />);
const dropdown = screen.getByTestId("account-dropdown");
expect(dropdown).toHaveAttribute("data-selected", mockAccounts[0].id);
});
it("should pass correct checked state to MigrationTypeCheckbox", () => {
mockContextValue.copyJobState.migrationType = CopyJobMigrationType.Offline;
render(<SelectAccount />);
const checkbox = screen.getByTestId("migration-type-checkbox");
expect(checkbox).toHaveAttribute("data-checked", "true");
});
});
describe("Edge Cases", () => {
it("should handle empty subscriptions array", () => {
mockUseSubscriptions.mockReturnValue([]);
mockUseDropdownOptions.mockReturnValue({
subscriptionOptions: [],
accountOptions: [],
});
const { container } = render(<SelectAccount />);
expect(container).toMatchSnapshot();
});
it("should handle empty accounts array", () => {
mockUseDatabaseAccounts.mockReturnValue([]);
mockUseDropdownOptions.mockReturnValue({
subscriptionOptions: mockDropdownOptions.subscriptionOptions,
accountOptions: [],
});
const { container } = render(<SelectAccount />);
expect(container).toMatchSnapshot();
});
it("should handle null subscription in context", () => {
mockContextValue.copyJobState.source.subscription = null;
const { container } = render(<SelectAccount />);
expect(container).toMatchSnapshot();
});
it("should handle null account in context", () => {
mockContextValue.copyJobState.source.account = null;
const { container } = render(<SelectAccount />);
expect(container).toMatchSnapshot();
});
it("should handle undefined subscriptions from hook", () => {
mockUseSubscriptions.mockReturnValue(undefined as any);
mockUseDropdownOptions.mockReturnValue({
subscriptionOptions: [],
accountOptions: [],
});
const { container } = render(<SelectAccount />);
expect(container).toMatchSnapshot();
});
it("should handle undefined accounts from hook", () => {
mockUseDatabaseAccounts.mockReturnValue(undefined as any);
mockUseDropdownOptions.mockReturnValue({
subscriptionOptions: mockDropdownOptions.subscriptionOptions,
accountOptions: [],
});
const { container } = render(<SelectAccount />);
expect(container).toMatchSnapshot();
});
it("should filter out non-SQL accounts correctly", () => {
const mixedAccounts = [
{ ...mockAccounts[0], kind: "GlobalDocumentDB" },
{ ...mockAccounts[1], kind: "MongoDB" },
];
mockUseDatabaseAccounts.mockReturnValue(mixedAccounts);
mockApiType.mockImplementation((account) => (account.kind === "GlobalDocumentDB" ? "SQL" : "Mongo"));
render(<SelectAccount />);
expect(mockApiType).toHaveBeenCalledTimes(2);
const sqlOnlyAccounts = mixedAccounts.filter((account) => apiType(account) === "SQL");
expect(mockUseDropdownOptions).toHaveBeenCalledWith(mockSubscriptions, sqlOnlyAccounts);
});
});
describe("Complete Workflow", () => {
it("should render complete workflow with all selections", () => {
mockContextValue.copyJobState.source.subscription = mockSubscriptions[0];
mockContextValue.copyJobState.source.account = mockAccounts[0];
mockContextValue.copyJobState.migrationType = CopyJobMigrationType.Online;
const { container } = render(<SelectAccount />);
expect(screen.getByTestId("subscription-dropdown")).toHaveAttribute("data-selected", "sub-1");
expect(screen.getByTestId("account-dropdown")).toHaveAttribute("data-selected", mockAccounts[0].id);
expect(screen.getByTestId("account-dropdown")).toHaveAttribute("data-disabled", "false");
expect(screen.getByTestId("migration-type-checkbox")).toHaveAttribute("data-checked", "false");
expect(container).toMatchSnapshot();
});
});
});

View File

@@ -0,0 +1,526 @@
import "@testing-library/jest-dom";
import { fireEvent, render } from "@testing-library/react";
import React from "react";
import { noop } from "underscore";
import { DatabaseAccount, Subscription } from "../../../../../../Contracts/DataModels";
import { CopyJobMigrationType } from "../../../../Enums/CopyJobEnums";
import { CopyJobContextState } from "../../../../Types/CopyJobTypes";
import { useDropdownOptions, useEventHandlers } from "./selectAccountUtils";
jest.mock("../../../Utils/useCopyJobPrerequisitesCache", () => ({
useCopyJobPrerequisitesCache: jest.fn(() => ({
setValidationCache: jest.fn(),
})),
}));
const mockSubscriptions: Subscription[] = [
{
subscriptionId: "sub-1",
displayName: "Test Subscription 1",
state: "Enabled",
subscriptionPolicies: {
locationPlacementId: "test",
quotaId: "test",
spendingLimit: "Off",
},
},
{
subscriptionId: "sub-2",
displayName: "Test Subscription 2",
state: "Enabled",
subscriptionPolicies: {
locationPlacementId: "test",
quotaId: "test",
spendingLimit: "Off",
},
},
];
const mockAccounts: DatabaseAccount[] = [
{
id: "account-1",
name: "Test Account 1",
location: "East US",
type: "Microsoft.DocumentDB/databaseAccounts",
kind: "GlobalDocumentDB",
properties: {
documentEndpoint: "https://test1.documents.azure.com:443/",
gremlinEndpoint: "https://test1.gremlin.cosmosdb.azure.com:443/",
tableEndpoint: "https://test1.table.cosmosdb.azure.com:443/",
cassandraEndpoint: "https://test1.cassandra.cosmosdb.azure.com:443/",
capabilities: [],
writeLocations: [],
readLocations: [],
locations: [],
ipRules: [],
enableMultipleWriteLocations: false,
isVirtualNetworkFilterEnabled: false,
enableFreeTier: false,
enableAnalyticalStorage: false,
publicNetworkAccess: "Enabled",
defaultIdentity: "",
disableLocalAuth: false,
},
},
{
id: "account-2",
name: "Test Account 2",
location: "West US",
type: "Microsoft.DocumentDB/databaseAccounts",
kind: "GlobalDocumentDB",
properties: {
documentEndpoint: "https://test2.documents.azure.com:443/",
gremlinEndpoint: "https://test2.gremlin.cosmosdb.azure.com:443/",
tableEndpoint: "https://test2.table.cosmosdb.azure.com:443/",
cassandraEndpoint: "https://test2.cassandra.cosmosdb.azure.com:443/",
capabilities: [],
writeLocations: [],
readLocations: [],
locations: [],
enableMultipleWriteLocations: false,
isVirtualNetworkFilterEnabled: false,
enableFreeTier: false,
enableAnalyticalStorage: false,
publicNetworkAccess: "Enabled",
defaultIdentity: "",
disableLocalAuth: false,
},
},
];
const DropdownOptionsTestComponent: React.FC<{
subscriptions: Subscription[];
accounts: DatabaseAccount[];
onResult?: (result: { subscriptionOptions: any[]; accountOptions: any[] }) => void;
}> = ({ subscriptions, accounts, onResult }) => {
const result = useDropdownOptions(subscriptions, accounts);
React.useEffect(() => {
if (onResult) {
onResult(result);
}
}, [result, onResult]);
return (
<div>
<div data-testid="subscription-options-count">{result.subscriptionOptions.length}</div>
<div data-testid="account-options-count">{result.accountOptions.length}</div>
</div>
);
};
const EventHandlersTestComponent: React.FC<{
setCopyJobState: jest.Mock;
onResult?: (result: any) => void;
}> = ({ setCopyJobState, onResult }) => {
const result = useEventHandlers(setCopyJobState);
React.useEffect(() => {
if (onResult) {
onResult(result);
}
}, [result, onResult]);
return (
<div>
<button
data-testid="select-subscription-button"
onClick={() => result.handleSelectSourceAccount("subscription", mockSubscriptions[0] as any)}
>
Select Subscription
</button>
<button
data-testid="select-account-button"
onClick={() => result.handleSelectSourceAccount("account", mockAccounts[0] as any)}
>
Select Account
</button>
<button data-testid="migration-type-button" onClick={(e) => result.handleMigrationTypeChange(e, true)}>
Change Migration Type
</button>
</div>
);
};
describe("selectAccountUtils", () => {
describe("useDropdownOptions", () => {
it("should return empty arrays when subscriptions and accounts are undefined", () => {
let capturedResult: any;
render(
<DropdownOptionsTestComponent
subscriptions={undefined as any}
accounts={undefined as any}
onResult={(result) => {
capturedResult = result;
}}
/>,
);
expect(capturedResult).toEqual({
subscriptionOptions: [],
accountOptions: [],
});
});
it("should return empty arrays when subscriptions and accounts are empty arrays", () => {
let capturedResult: any;
render(
<DropdownOptionsTestComponent
subscriptions={[]}
accounts={[]}
onResult={(result) => {
capturedResult = result;
}}
/>,
);
expect(capturedResult).toEqual({
subscriptionOptions: [],
accountOptions: [],
});
});
it("should transform subscriptions into dropdown options correctly", () => {
let capturedResult: any;
render(
<DropdownOptionsTestComponent
subscriptions={mockSubscriptions}
accounts={[]}
onResult={(result) => {
capturedResult = result;
}}
/>,
);
expect(capturedResult.subscriptionOptions).toHaveLength(2);
expect(capturedResult.subscriptionOptions[0]).toEqual({
key: "sub-1",
text: "Test Subscription 1",
data: mockSubscriptions[0],
});
expect(capturedResult.subscriptionOptions[1]).toEqual({
key: "sub-2",
text: "Test Subscription 2",
data: mockSubscriptions[1],
});
});
it("should transform accounts into dropdown options correctly", () => {
let capturedResult: any;
render(
<DropdownOptionsTestComponent
subscriptions={[]}
accounts={mockAccounts}
onResult={(result) => {
capturedResult = result;
}}
/>,
);
expect(capturedResult.accountOptions).toHaveLength(2);
expect(capturedResult.accountOptions[0]).toEqual({
key: "account-1",
text: "Test Account 1",
data: mockAccounts[0],
});
expect(capturedResult.accountOptions[1]).toEqual({
key: "account-2",
text: "Test Account 2",
data: mockAccounts[1],
});
});
it("should handle both subscriptions and accounts correctly", () => {
let capturedResult: any;
render(
<DropdownOptionsTestComponent
subscriptions={mockSubscriptions}
accounts={mockAccounts}
onResult={(result) => {
capturedResult = result;
}}
/>,
);
expect(capturedResult.subscriptionOptions).toHaveLength(2);
expect(capturedResult.accountOptions).toHaveLength(2);
});
});
describe("useEventHandlers", () => {
let mockSetCopyJobState: jest.Mock;
let mockSetValidationCache: jest.Mock;
beforeEach(async () => {
mockSetCopyJobState = jest.fn();
mockSetValidationCache = jest.fn();
const { useCopyJobPrerequisitesCache } = await import("../../../Utils/useCopyJobPrerequisitesCache");
(useCopyJobPrerequisitesCache as unknown as jest.Mock).mockReturnValue({
setValidationCache: mockSetValidationCache,
});
});
afterEach(() => {
jest.clearAllMocks();
});
it("should handle subscription selection correctly", () => {
const { getByTestId } = render(
<EventHandlersTestComponent setCopyJobState={mockSetCopyJobState} onResult={noop} />,
);
fireEvent.click(getByTestId("select-subscription-button"));
expect(mockSetCopyJobState).toHaveBeenCalledWith(expect.any(Function));
expect(mockSetValidationCache).toHaveBeenCalledWith(new Map<string, boolean>());
const stateUpdater = mockSetCopyJobState.mock.calls[0][0];
const mockPrevState: CopyJobContextState = {
source: {
subscription: null,
account: { id: "existing-account" } as any,
},
migrationType: CopyJobMigrationType.Online,
} as any;
const newState = stateUpdater(mockPrevState);
expect(newState).toEqual({
source: {
subscription: mockSubscriptions[0],
account: null,
},
migrationType: CopyJobMigrationType.Online,
});
});
it("should handle account selection correctly", () => {
const { getByTestId } = render(
<EventHandlersTestComponent setCopyJobState={mockSetCopyJobState} onResult={noop} />,
);
fireEvent.click(getByTestId("select-account-button"));
expect(mockSetCopyJobState).toHaveBeenCalledWith(expect.any(Function));
expect(mockSetValidationCache).toHaveBeenCalledWith(new Map<string, boolean>());
const stateUpdater = mockSetCopyJobState.mock.calls[0][0];
const mockPrevState: CopyJobContextState = {
source: {
subscription: { subscriptionId: "existing-sub" } as any,
account: null,
},
migrationType: CopyJobMigrationType.Online,
} as any;
const newState = stateUpdater(mockPrevState);
expect(newState).toEqual({
source: {
subscription: { subscriptionId: "existing-sub" },
account: mockAccounts[0],
},
migrationType: CopyJobMigrationType.Online,
});
});
it("should handle subscription selection with undefined data", () => {
let capturedHandlers: any;
render(
<EventHandlersTestComponent
setCopyJobState={mockSetCopyJobState}
onResult={(result) => {
capturedHandlers = result;
}}
/>,
);
capturedHandlers.handleSelectSourceAccount("subscription", undefined);
expect(mockSetCopyJobState).toHaveBeenCalledWith(expect.any(Function));
const stateUpdater = mockSetCopyJobState.mock.calls[0][0];
const mockPrevState: CopyJobContextState = {
source: {
subscription: { subscriptionId: "existing-sub" } as any,
account: { id: "existing-account" } as any,
},
migrationType: CopyJobMigrationType.Online,
} as any;
const newState = stateUpdater(mockPrevState);
expect(newState).toEqual({
source: {
subscription: null,
account: null,
},
migrationType: CopyJobMigrationType.Online,
});
});
it("should handle account selection with undefined data", () => {
let capturedHandlers: any;
render(
<EventHandlersTestComponent
setCopyJobState={mockSetCopyJobState}
onResult={(result) => {
capturedHandlers = result;
}}
/>,
);
capturedHandlers.handleSelectSourceAccount("account", undefined);
expect(mockSetCopyJobState).toHaveBeenCalledWith(expect.any(Function));
const stateUpdater = mockSetCopyJobState.mock.calls[0][0];
const mockPrevState: CopyJobContextState = {
source: {
subscription: { subscriptionId: "existing-sub" } as any,
account: { id: "existing-account" } as any,
},
migrationType: CopyJobMigrationType.Online,
} as any;
const newState = stateUpdater(mockPrevState);
expect(newState).toEqual({
source: {
subscription: { subscriptionId: "existing-sub" },
account: null,
},
migrationType: CopyJobMigrationType.Online,
});
});
it("should handle migration type change to offline", () => {
const { getByTestId } = render(<EventHandlersTestComponent setCopyJobState={mockSetCopyJobState} />);
fireEvent.click(getByTestId("migration-type-button"));
expect(mockSetCopyJobState).toHaveBeenCalledWith(expect.any(Function));
expect(mockSetValidationCache).toHaveBeenCalledWith(new Map<string, boolean>());
const stateUpdater = mockSetCopyJobState.mock.calls[0][0];
const mockPrevState: CopyJobContextState = {
source: {
subscription: null,
account: null,
},
migrationType: CopyJobMigrationType.Online,
} as any;
const newState = stateUpdater(mockPrevState);
expect(newState).toEqual({
source: {
subscription: null,
account: null,
},
migrationType: CopyJobMigrationType.Offline,
});
});
it("should handle migration type change to online when checked is false", () => {
let capturedHandlers: any;
render(
<EventHandlersTestComponent
setCopyJobState={mockSetCopyJobState}
onResult={(result) => {
capturedHandlers = result;
}}
/>,
);
capturedHandlers.handleMigrationTypeChange(undefined, false);
expect(mockSetCopyJobState).toHaveBeenCalledWith(expect.any(Function));
const stateUpdater = mockSetCopyJobState.mock.calls[0][0];
const mockPrevState: CopyJobContextState = {
source: {
subscription: null,
account: null,
},
migrationType: CopyJobMigrationType.Offline,
} as any;
const newState = stateUpdater(mockPrevState);
expect(newState).toEqual({
source: {
subscription: null,
account: null,
},
migrationType: CopyJobMigrationType.Online,
});
});
it("should preserve other state properties when updating", () => {
let capturedHandlers: any;
render(
<EventHandlersTestComponent
setCopyJobState={mockSetCopyJobState}
onResult={(result) => {
capturedHandlers = result;
}}
/>,
);
capturedHandlers.handleSelectSourceAccount("subscription", mockSubscriptions[0] as Subscription);
const stateUpdater = mockSetCopyJobState.mock.calls[0][0];
const mockPrevState = {
jobName: "Test Job",
source: {
subscription: null,
account: null,
databaseId: "test-database-id",
containerId: "test-container-id",
},
migrationType: CopyJobMigrationType.Online,
target: {
account: { id: "dest-account" } as DatabaseAccount,
databaseId: "test-database-id",
containerId: "test-container-id",
subscriptionId: "dest-sub-id",
},
} as CopyJobContextState;
const newState = stateUpdater(mockPrevState);
expect(newState.target).toEqual(mockPrevState.target);
});
it("should return the same state for unknown selection type", () => {
let capturedHandlers: any;
render(
<EventHandlersTestComponent
setCopyJobState={mockSetCopyJobState}
onResult={(result) => {
capturedHandlers = result;
}}
/>,
);
capturedHandlers.handleSelectSourceAccount("unknown" as any, mockSubscriptions[0] as any);
const stateUpdater = mockSetCopyJobState.mock.calls[0][0];
const mockPrevState: CopyJobContextState = {
source: {
subscription: { subscriptionId: "existing-sub" } as any,
account: { id: "existing-account" } as any,
},
migrationType: CopyJobMigrationType.Online,
} as any;
const newState = stateUpdater(mockPrevState);
expect(newState).toEqual(mockPrevState);
});
});
});

View File

@@ -18,9 +18,16 @@ export function useDropdownOptions(
data: sub,
})) || [];
const normalizeAccountId = (id: string) => {
if (!id) {
return id;
}
return id.replace(/\/Microsoft\.DocumentDb\//i, "/Microsoft.DocumentDB/");
};
const accountOptions =
accounts?.map((account) => ({
key: account.id,
key: normalizeAccountId(account.id),
text: account.name,
data: account,
})) || [];

View File

@@ -0,0 +1,510 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SelectAccount Component Complete Workflow should render complete workflow with all selections 1`] = `
<div>
<div
class="ms-Stack selectAccountContainer css-109"
>
<span>
Select your source account and subscription
</span>
<div
data-selected="sub-1"
data-testid="subscription-dropdown"
>
<div
data-testid="subscription-option-sub-1"
>
Test Subscription 1
</div>
<div
data-testid="subscription-option-sub-2"
>
Test Subscription 2
</div>
</div>
<div
data-disabled="false"
data-selected="/subscriptions/sub-1/resourceGroups/rg-1/providers/Microsoft.DocumentDB/databaseAccounts/account-1"
data-testid="account-dropdown"
>
<div
data-testid="account-option-/subscriptions/sub-1/resourceGroups/rg-1/providers/Microsoft.DocumentDB/databaseAccounts/account-1"
>
test-cosmos-account-1
</div>
</div>
<div
data-checked="false"
data-testid="migration-type-checkbox"
>
<input
data-testid="migration-checkbox-input"
type="checkbox"
/>
</div>
</div>
</div>
`;
exports[`SelectAccount Component Edge Cases should handle empty accounts array 1`] = `
<div>
<div
class="ms-Stack selectAccountContainer css-109"
>
<span>
Select your source account and subscription
</span>
<div
data-testid="subscription-dropdown"
>
<div
data-testid="subscription-option-sub-1"
>
Test Subscription 1
</div>
<div
data-testid="subscription-option-sub-2"
>
Test Subscription 2
</div>
</div>
<div
data-disabled="true"
data-testid="account-dropdown"
/>
<div
data-checked="true"
data-testid="migration-type-checkbox"
>
<input
checked=""
data-testid="migration-checkbox-input"
type="checkbox"
/>
</div>
</div>
</div>
`;
exports[`SelectAccount Component Edge Cases should handle empty subscriptions array 1`] = `
<div>
<div
class="ms-Stack selectAccountContainer css-109"
>
<span>
Select your source account and subscription
</span>
<div
data-testid="subscription-dropdown"
/>
<div
data-disabled="true"
data-testid="account-dropdown"
/>
<div
data-checked="true"
data-testid="migration-type-checkbox"
>
<input
checked=""
data-testid="migration-checkbox-input"
type="checkbox"
/>
</div>
</div>
</div>
`;
exports[`SelectAccount Component Edge Cases should handle null account in context 1`] = `
<div>
<div
class="ms-Stack selectAccountContainer css-109"
>
<span>
Select your source account and subscription
</span>
<div
data-testid="subscription-dropdown"
>
<div
data-testid="subscription-option-sub-1"
>
Test Subscription 1
</div>
<div
data-testid="subscription-option-sub-2"
>
Test Subscription 2
</div>
</div>
<div
data-disabled="true"
data-testid="account-dropdown"
>
<div
data-testid="account-option-/subscriptions/sub-1/resourceGroups/rg-1/providers/Microsoft.DocumentDB/databaseAccounts/account-1"
>
test-cosmos-account-1
</div>
</div>
<div
data-checked="true"
data-testid="migration-type-checkbox"
>
<input
checked=""
data-testid="migration-checkbox-input"
type="checkbox"
/>
</div>
</div>
</div>
`;
exports[`SelectAccount Component Edge Cases should handle null subscription in context 1`] = `
<div>
<div
class="ms-Stack selectAccountContainer css-109"
>
<span>
Select your source account and subscription
</span>
<div
data-testid="subscription-dropdown"
>
<div
data-testid="subscription-option-sub-1"
>
Test Subscription 1
</div>
<div
data-testid="subscription-option-sub-2"
>
Test Subscription 2
</div>
</div>
<div
data-disabled="true"
data-testid="account-dropdown"
>
<div
data-testid="account-option-/subscriptions/sub-1/resourceGroups/rg-1/providers/Microsoft.DocumentDB/databaseAccounts/account-1"
>
test-cosmos-account-1
</div>
</div>
<div
data-checked="true"
data-testid="migration-type-checkbox"
>
<input
checked=""
data-testid="migration-checkbox-input"
type="checkbox"
/>
</div>
</div>
</div>
`;
exports[`SelectAccount Component Edge Cases should handle undefined accounts from hook 1`] = `
<div>
<div
class="ms-Stack selectAccountContainer css-109"
>
<span>
Select your source account and subscription
</span>
<div
data-testid="subscription-dropdown"
>
<div
data-testid="subscription-option-sub-1"
>
Test Subscription 1
</div>
<div
data-testid="subscription-option-sub-2"
>
Test Subscription 2
</div>
</div>
<div
data-disabled="true"
data-testid="account-dropdown"
/>
<div
data-checked="true"
data-testid="migration-type-checkbox"
>
<input
checked=""
data-testid="migration-checkbox-input"
type="checkbox"
/>
</div>
</div>
</div>
`;
exports[`SelectAccount Component Edge Cases should handle undefined subscriptions from hook 1`] = `
<div>
<div
class="ms-Stack selectAccountContainer css-109"
>
<span>
Select your source account and subscription
</span>
<div
data-testid="subscription-dropdown"
/>
<div
data-disabled="true"
data-testid="account-dropdown"
/>
<div
data-checked="true"
data-testid="migration-type-checkbox"
>
<input
checked=""
data-testid="migration-checkbox-input"
type="checkbox"
/>
</div>
</div>
</div>
`;
exports[`SelectAccount Component Rendering should render component with default state 1`] = `
<div>
<div
class="ms-Stack selectAccountContainer css-109"
>
<span>
Select your source account and subscription
</span>
<div
data-testid="subscription-dropdown"
>
<div
data-testid="subscription-option-sub-1"
>
Test Subscription 1
</div>
<div
data-testid="subscription-option-sub-2"
>
Test Subscription 2
</div>
</div>
<div
data-disabled="true"
data-testid="account-dropdown"
>
<div
data-testid="account-option-/subscriptions/sub-1/resourceGroups/rg-1/providers/Microsoft.DocumentDB/databaseAccounts/account-1"
>
test-cosmos-account-1
</div>
</div>
<div
data-checked="true"
data-testid="migration-type-checkbox"
>
<input
checked=""
data-testid="migration-checkbox-input"
type="checkbox"
/>
</div>
</div>
</div>
`;
exports[`SelectAccount Component Rendering should render with offline migration type checked 1`] = `
<div>
<div
class="ms-Stack selectAccountContainer css-109"
>
<span>
Select your source account and subscription
</span>
<div
data-testid="subscription-dropdown"
>
<div
data-testid="subscription-option-sub-1"
>
Test Subscription 1
</div>
<div
data-testid="subscription-option-sub-2"
>
Test Subscription 2
</div>
</div>
<div
data-disabled="true"
data-testid="account-dropdown"
>
<div
data-testid="account-option-/subscriptions/sub-1/resourceGroups/rg-1/providers/Microsoft.DocumentDB/databaseAccounts/account-1"
>
test-cosmos-account-1
</div>
</div>
<div
data-checked="true"
data-testid="migration-type-checkbox"
>
<input
checked=""
data-testid="migration-checkbox-input"
type="checkbox"
/>
</div>
</div>
</div>
`;
exports[`SelectAccount Component Rendering should render with online migration type unchecked 1`] = `
<div>
<div
class="ms-Stack selectAccountContainer css-109"
>
<span>
Select your source account and subscription
</span>
<div
data-testid="subscription-dropdown"
>
<div
data-testid="subscription-option-sub-1"
>
Test Subscription 1
</div>
<div
data-testid="subscription-option-sub-2"
>
Test Subscription 2
</div>
</div>
<div
data-disabled="true"
data-testid="account-dropdown"
>
<div
data-testid="account-option-/subscriptions/sub-1/resourceGroups/rg-1/providers/Microsoft.DocumentDB/databaseAccounts/account-1"
>
test-cosmos-account-1
</div>
</div>
<div
data-checked="false"
data-testid="migration-type-checkbox"
>
<input
data-testid="migration-checkbox-input"
type="checkbox"
/>
</div>
</div>
</div>
`;
exports[`SelectAccount Component Rendering should render with selected account 1`] = `
<div>
<div
class="ms-Stack selectAccountContainer css-109"
>
<span>
Select your source account and subscription
</span>
<div
data-selected="sub-1"
data-testid="subscription-dropdown"
>
<div
data-testid="subscription-option-sub-1"
>
Test Subscription 1
</div>
<div
data-testid="subscription-option-sub-2"
>
Test Subscription 2
</div>
</div>
<div
data-disabled="false"
data-selected="/subscriptions/sub-1/resourceGroups/rg-1/providers/Microsoft.DocumentDB/databaseAccounts/account-1"
data-testid="account-dropdown"
>
<div
data-testid="account-option-/subscriptions/sub-1/resourceGroups/rg-1/providers/Microsoft.DocumentDB/databaseAccounts/account-1"
>
test-cosmos-account-1
</div>
</div>
<div
data-checked="true"
data-testid="migration-type-checkbox"
>
<input
checked=""
data-testid="migration-checkbox-input"
type="checkbox"
/>
</div>
</div>
</div>
`;
exports[`SelectAccount Component Rendering should render with selected subscription 1`] = `
<div>
<div
class="ms-Stack selectAccountContainer css-109"
>
<span>
Select your source account and subscription
</span>
<div
data-selected="sub-1"
data-testid="subscription-dropdown"
>
<div
data-testid="subscription-option-sub-1"
>
Test Subscription 1
</div>
<div
data-testid="subscription-option-sub-2"
>
Test Subscription 2
</div>
</div>
<div
data-disabled="false"
data-testid="account-dropdown"
>
<div
data-testid="account-option-/subscriptions/sub-1/resourceGroups/rg-1/providers/Microsoft.DocumentDB/databaseAccounts/account-1"
>
test-cosmos-account-1
</div>
</div>
<div
data-checked="true"
data-testid="migration-type-checkbox"
>
<input
checked=""
data-testid="migration-checkbox-input"
type="checkbox"
/>
</div>
</div>
</div>
`;

View File

@@ -0,0 +1,330 @@
import { fireEvent, render } from "@testing-library/react";
import React from "react";
import { CopyJobMigrationType } from "../../../../Enums/CopyJobEnums";
import { CopyJobContextState, DropdownOptionType } from "../../../../Types/CopyJobTypes";
import { dropDownChangeHandler } from "./DropDownChangeHandler";
const createMockInitialState = (): CopyJobContextState => ({
jobName: "test-job",
migrationType: CopyJobMigrationType.Offline,
sourceReadAccessFromTarget: false,
source: {
subscription: {
subscriptionId: "source-sub-id",
displayName: "Source Subscription",
state: "Enabled",
subscriptionPolicies: {
locationPlacementId: "test",
quotaId: "test",
spendingLimit: "Off",
},
authorizationSource: "test",
},
account: {
id: "source-account-id",
name: "source-account",
location: "East US",
type: "Microsoft.DocumentDB/databaseAccounts",
kind: "DocumentDB",
properties: {
documentEndpoint: "https://source.documents.azure.com:443/",
cassandraEndpoint: undefined,
gremlinEndpoint: undefined,
tableEndpoint: undefined,
writeLocations: [],
readLocations: [],
enableMultipleWriteLocations: false,
isVirtualNetworkFilterEnabled: false,
enableFreeTier: false,
enableAnalyticalStorage: false,
backupPolicy: undefined,
disableLocalAuth: false,
capacity: undefined,
enablePriorityBasedExecution: false,
publicNetworkAccess: "Enabled",
enableMaterializedViews: false,
},
systemData: undefined,
},
databaseId: "source-db",
containerId: "source-container",
},
target: {
subscriptionId: "target-sub-id",
account: {
id: "target-account-id",
name: "target-account",
location: "West US",
type: "Microsoft.DocumentDB/databaseAccounts",
kind: "DocumentDB",
properties: {
documentEndpoint: "https://target.documents.azure.com:443/",
cassandraEndpoint: undefined,
gremlinEndpoint: undefined,
tableEndpoint: undefined,
writeLocations: [],
readLocations: [],
enableMultipleWriteLocations: false,
isVirtualNetworkFilterEnabled: false,
enableFreeTier: false,
enableAnalyticalStorage: false,
backupPolicy: undefined,
disableLocalAuth: false,
capacity: undefined,
enablePriorityBasedExecution: false,
publicNetworkAccess: "Enabled",
enableMaterializedViews: false,
},
systemData: undefined,
},
databaseId: "target-db",
containerId: "target-container",
},
});
interface TestComponentProps {
initialState: CopyJobContextState;
onStateChange: (state: CopyJobContextState) => void;
}
const TestComponent: React.FC<TestComponentProps> = ({ initialState, onStateChange }) => {
const [state, setState] = React.useState<CopyJobContextState>(initialState);
const handler = dropDownChangeHandler(setState);
React.useEffect(() => {
onStateChange(state);
}, [state, onStateChange]);
return (
<div>
<button
data-testid="source-database-btn"
onClick={() =>
handler("sourceDatabase")({} as React.FormEvent, { key: "new-source-db", text: "New Source DB", data: {} })
}
>
Source Database
</button>
<button
data-testid="source-container-btn"
onClick={() =>
handler("sourceContainer")({} as React.FormEvent, {
key: "new-source-container",
text: "New Source Container",
data: {},
})
}
>
Source Container
</button>
<button
data-testid="target-database-btn"
onClick={() =>
handler("targetDatabase")({} as React.FormEvent, { key: "new-target-db", text: "New Target DB", data: {} })
}
>
Target Database
</button>
<button
data-testid="target-container-btn"
onClick={() =>
handler("targetContainer")({} as React.FormEvent, {
key: "new-target-container",
text: "New Target Container",
data: {},
})
}
>
Target Container
</button>
</div>
);
};
describe("dropDownChangeHandler", () => {
let capturedState: CopyJobContextState;
let initialState: CopyJobContextState;
beforeEach(() => {
initialState = createMockInitialState();
capturedState = initialState;
});
const renderTestComponent = () => {
return render(
<TestComponent
initialState={initialState}
onStateChange={(state) => {
capturedState = state;
}}
/>,
);
};
describe("sourceDatabase dropdown change", () => {
it("should update source database and reset source container", () => {
const { getByTestId } = renderTestComponent();
fireEvent.click(getByTestId("source-database-btn"));
expect(capturedState.source.databaseId).toBe("new-source-db");
expect(capturedState.source.containerId).toBeUndefined();
expect(capturedState.source.subscription).toEqual(initialState.source.subscription);
expect(capturedState.source.account).toEqual(initialState.source.account);
expect(capturedState.target).toEqual(initialState.target);
});
it("should maintain other state properties when updating source database", () => {
const { getByTestId } = renderTestComponent();
fireEvent.click(getByTestId("source-database-btn"));
expect(capturedState.jobName).toBe(initialState.jobName);
expect(capturedState.migrationType).toBe(initialState.migrationType);
expect(capturedState.sourceReadAccessFromTarget).toBe(initialState.sourceReadAccessFromTarget);
});
});
describe("sourceContainer dropdown change", () => {
it("should update source container only", () => {
const { getByTestId } = renderTestComponent();
fireEvent.click(getByTestId("source-container-btn"));
expect(capturedState.source.containerId).toBe("new-source-container");
expect(capturedState.source.databaseId).toBe(initialState.source.databaseId);
expect(capturedState.source.subscription).toEqual(initialState.source.subscription);
expect(capturedState.source.account).toEqual(initialState.source.account);
expect(capturedState.target).toEqual(initialState.target);
});
it("should not affect database selection when updating container", () => {
const { getByTestId } = renderTestComponent();
fireEvent.click(getByTestId("source-container-btn"));
expect(capturedState.source.databaseId).toBe("source-db");
});
});
describe("targetDatabase dropdown change", () => {
it("should update target database and reset target container", () => {
const { getByTestId } = renderTestComponent();
fireEvent.click(getByTestId("target-database-btn"));
expect(capturedState.target.databaseId).toBe("new-target-db");
expect(capturedState.target.containerId).toBeUndefined();
expect(capturedState.target.subscriptionId).toBe(initialState.target.subscriptionId);
expect(capturedState.target.account).toEqual(initialState.target.account);
expect(capturedState.source).toEqual(initialState.source);
});
it("should maintain other state properties when updating target database", () => {
const { getByTestId } = renderTestComponent();
fireEvent.click(getByTestId("target-database-btn"));
expect(capturedState.jobName).toBe(initialState.jobName);
expect(capturedState.migrationType).toBe(initialState.migrationType);
expect(capturedState.sourceReadAccessFromTarget).toBe(initialState.sourceReadAccessFromTarget);
});
});
describe("targetContainer dropdown change", () => {
it("should update target container only", () => {
const { getByTestId } = renderTestComponent();
fireEvent.click(getByTestId("target-container-btn"));
expect(capturedState.target.containerId).toBe("new-target-container");
expect(capturedState.target.databaseId).toBe(initialState.target.databaseId);
expect(capturedState.target.subscriptionId).toBe(initialState.target.subscriptionId);
expect(capturedState.target.account).toEqual(initialState.target.account);
expect(capturedState.source).toEqual(initialState.source);
});
it("should not affect database selection when updating container", () => {
const { getByTestId } = renderTestComponent();
fireEvent.click(getByTestId("target-container-btn"));
expect(capturedState.target.databaseId).toBe("target-db");
});
});
describe("edge cases and error scenarios", () => {
it("should handle empty string keys", () => {
renderTestComponent();
const handler = dropDownChangeHandler((updater) => {
const newState = typeof updater === "function" ? updater(capturedState) : updater;
capturedState = newState;
return capturedState;
});
const mockEvent = {} as React.FormEvent;
const mockOption: DropdownOptionType = { key: "", text: "Empty Option", data: {} };
handler("sourceDatabase")(mockEvent, mockOption);
expect(capturedState.source.databaseId).toBe("");
expect(capturedState.source.containerId).toBeUndefined();
});
it("should handle special characters in keys", () => {
renderTestComponent();
const handler = dropDownChangeHandler((updater) => {
const newState = typeof updater === "function" ? updater(capturedState) : updater;
capturedState = newState;
return capturedState;
});
const mockEvent = {} as React.FormEvent;
const mockOption: DropdownOptionType = {
key: "test-db-with-special-chars-@#$%",
text: "Special DB",
data: {},
};
handler("sourceDatabase")(mockEvent, mockOption);
expect(capturedState.source.databaseId).toBe("test-db-with-special-chars-@#$%");
expect(capturedState.source.containerId).toBeUndefined();
});
it("should handle numeric keys", () => {
renderTestComponent();
const handler = dropDownChangeHandler((updater) => {
const newState = typeof updater === "function" ? updater(capturedState) : updater;
capturedState = newState;
return capturedState;
});
const mockEvent = {} as React.FormEvent;
const mockOption: DropdownOptionType = { key: "12345", text: "Numeric Option", data: {} };
handler("targetContainer")(mockEvent, mockOption);
expect(capturedState.target.containerId).toBe("12345");
});
it.skip("should handle invalid dropdown type gracefully", () => {
const handler = dropDownChangeHandler((updater) => {
const newState = typeof updater === "function" ? updater(capturedState) : updater;
capturedState = newState;
return capturedState;
});
const mockEvent = {} as React.FormEvent;
const mockOption: DropdownOptionType = { key: "test-value", text: "Test Option", data: {} };
const invalidHandler = handler as any;
invalidHandler("invalidType")(mockEvent, mockOption);
expect(capturedState).toEqual(initialState);
});
});
});

View File

@@ -0,0 +1,484 @@
import "@testing-library/jest-dom";
import { render, screen } from "@testing-library/react";
import { DatabaseModel } from "Contracts/DataModels";
import React from "react";
import Explorer from "../../../../../Explorer/Explorer";
import CopyJobContextProvider from "../../../Context/CopyJobContext";
import { CopyJobMigrationType } from "../../../Enums/CopyJobEnums";
import SelectSourceAndTargetContainers from "./SelectSourceAndTargetContainers";
jest.mock("../../../../../hooks/useDatabases", () => ({
useDatabases: jest.fn(),
}));
jest.mock("../../../../../hooks/useDataContainers", () => ({
useDataContainers: jest.fn(),
}));
jest.mock("../../../ContainerCopyMessages", () => ({
__esModule: true,
default: {
selectSourceAndTargetContainersDescription: "Select source and target containers for migration",
sourceContainerSubHeading: "Source Container",
targetContainerSubHeading: "Target Container",
},
}));
jest.mock("./Events/DropDownChangeHandler", () => ({
dropDownChangeHandler: jest.fn(() => () => jest.fn()),
}));
jest.mock("./memoizedData", () => ({
useSourceAndTargetData: jest.fn(),
}));
jest.mock("UserContext", () => ({
userContext: {
subscriptionId: "test-subscription-id",
databaseAccount: {
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/test-account",
name: "test-account",
location: "East US",
kind: "GlobalDocumentDB",
},
},
}));
import { useDatabases } from "../../../../../hooks/useDatabases";
import { useDataContainers } from "../../../../../hooks/useDataContainers";
import { dropDownChangeHandler } from "./Events/DropDownChangeHandler";
import { useSourceAndTargetData } from "./memoizedData";
const mockUseDatabases = useDatabases as jest.MockedFunction<typeof useDatabases>;
const mockUseDataContainers = useDataContainers as jest.MockedFunction<typeof useDataContainers>;
const mockDropDownChangeHandler = dropDownChangeHandler as jest.MockedFunction<typeof dropDownChangeHandler>;
const mockUseSourceAndTargetData = useSourceAndTargetData as jest.MockedFunction<typeof useSourceAndTargetData>;
describe("SelectSourceAndTargetContainers", () => {
let mockExplorer: Explorer;
let mockShowAddCollectionPanel: jest.Mock;
let mockOnDropdownChange: jest.Mock;
const mockDatabases: DatabaseModel[] = [
{ id: "db1", name: "Database1" } as DatabaseModel,
{ id: "db2", name: "Database2" } as DatabaseModel,
];
const mockContainers: DatabaseModel[] = [
{ id: "container1", name: "Container1" } as DatabaseModel,
{ id: "container2", name: "Container2" } as DatabaseModel,
];
const mockCopyJobState = {
jobName: "",
migrationType: CopyJobMigrationType.Offline,
source: {
subscription: { subscriptionId: "test-subscription-id" },
account: {
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/test-account",
name: "test-account",
},
databaseId: "db1",
containerId: "container1",
},
target: {
subscriptionId: "test-subscription-id",
account: {
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/test-account",
name: "test-account",
},
databaseId: "db2",
containerId: "container2",
},
sourceReadAccessFromTarget: false,
};
const mockMemoizedData = {
source: mockCopyJobState.source,
target: mockCopyJobState.target,
sourceDbParams: ["test-sub", "test-rg", "test-account", "SQL"] as const,
sourceContainerParams: ["test-sub", "test-rg", "test-account", "db1", "SQL"] as const,
targetDbParams: ["test-sub", "test-rg", "test-account", "SQL"] as const,
targetContainerParams: ["test-sub", "test-rg", "test-account", "db2", "SQL"] as const,
};
beforeEach(() => {
mockExplorer = {} as Explorer;
mockShowAddCollectionPanel = jest.fn();
mockOnDropdownChange = jest.fn();
mockUseDatabases.mockReturnValue(mockDatabases);
mockUseDataContainers.mockReturnValue(mockContainers);
mockUseSourceAndTargetData.mockReturnValue(mockMemoizedData as ReturnType<typeof useSourceAndTargetData>);
mockDropDownChangeHandler.mockReturnValue(() => mockOnDropdownChange);
});
afterEach(() => {
jest.clearAllMocks();
});
const renderWithContext = (component: React.ReactElement) => {
return render(<CopyJobContextProvider explorer={mockExplorer}>{component}</CopyJobContextProvider>);
};
describe("Component Rendering", () => {
it("should render without crashing", () => {
renderWithContext(<SelectSourceAndTargetContainers />);
expect(screen.getByText("Select source and target containers for migration")).toBeInTheDocument();
});
it("should render description text", () => {
renderWithContext(<SelectSourceAndTargetContainers />);
expect(screen.getByText("Select source and target containers for migration")).toBeInTheDocument();
});
it("should render source container section", () => {
renderWithContext(<SelectSourceAndTargetContainers />);
expect(screen.getByText("Source Container")).toBeInTheDocument();
});
it("should render target container section", () => {
renderWithContext(<SelectSourceAndTargetContainers />);
expect(screen.getByText("Target Container")).toBeInTheDocument();
});
it("should return null when source is not available", () => {
mockUseSourceAndTargetData.mockReturnValue({
...mockMemoizedData,
source: null,
} as ReturnType<typeof useSourceAndTargetData>);
const { container } = renderWithContext(<SelectSourceAndTargetContainers />);
expect(container.firstChild).toBeNull();
});
it("should call useDatabases hooks with correct parameters", () => {
renderWithContext(<SelectSourceAndTargetContainers />);
expect(mockUseDatabases).toHaveBeenCalledWith(...mockMemoizedData.sourceDbParams);
expect(mockUseDatabases).toHaveBeenCalledWith(...mockMemoizedData.targetDbParams);
});
it("should call useDataContainers hooks with correct parameters", () => {
renderWithContext(<SelectSourceAndTargetContainers />);
expect(mockUseDataContainers).toHaveBeenCalledWith(...mockMemoizedData.sourceContainerParams);
expect(mockUseDataContainers).toHaveBeenCalledWith(...mockMemoizedData.targetContainerParams);
});
});
describe("Database Options", () => {
it("should create source database options from useDatabases data", () => {
renderWithContext(<SelectSourceAndTargetContainers />);
expect(mockUseDatabases).toHaveBeenCalled();
});
it("should create target database options from useDatabases data", () => {
renderWithContext(<SelectSourceAndTargetContainers />);
expect(mockUseDatabases).toHaveBeenCalled();
});
it("should handle empty database list", () => {
mockUseDatabases.mockReturnValue([]);
renderWithContext(<SelectSourceAndTargetContainers />);
expect(mockUseDatabases).toHaveBeenCalled();
});
it("should handle undefined database list", () => {
mockUseDatabases.mockReturnValue(undefined);
renderWithContext(<SelectSourceAndTargetContainers />);
expect(mockUseDatabases).toHaveBeenCalled();
});
});
describe("Container Options", () => {
it("should create source container options from useDataContainers data", () => {
renderWithContext(<SelectSourceAndTargetContainers />);
expect(mockUseDataContainers).toHaveBeenCalled();
});
it("should create target container options from useDataContainers data", () => {
renderWithContext(<SelectSourceAndTargetContainers />);
expect(mockUseDataContainers).toHaveBeenCalled();
});
it("should handle empty container list", () => {
mockUseDataContainers.mockReturnValue([]);
renderWithContext(<SelectSourceAndTargetContainers />);
expect(mockUseDataContainers).toHaveBeenCalled();
});
it("should handle undefined container list", () => {
mockUseDataContainers.mockReturnValue(undefined);
renderWithContext(<SelectSourceAndTargetContainers />);
expect(mockUseDataContainers).toHaveBeenCalled();
});
});
describe("Event Handlers", () => {
it("should call dropDownChangeHandler with setCopyJobState", () => {
renderWithContext(<SelectSourceAndTargetContainers />);
expect(mockDropDownChangeHandler).toHaveBeenCalledWith(expect.any(Function));
});
it("should create dropdown change handlers for different types", () => {
renderWithContext(<SelectSourceAndTargetContainers />);
expect(mockDropDownChangeHandler).toHaveBeenCalled();
});
});
describe("Component Props", () => {
it("should pass showAddCollectionPanel to DatabaseContainerSection", () => {
renderWithContext(<SelectSourceAndTargetContainers showAddCollectionPanel={mockShowAddCollectionPanel} />);
expect(screen.getByText("Target Container")).toBeInTheDocument();
});
it("should render without showAddCollectionPanel prop", () => {
renderWithContext(<SelectSourceAndTargetContainers />);
expect(screen.getByText("Source Container")).toBeInTheDocument();
expect(screen.getByText("Target Container")).toBeInTheDocument();
});
});
describe("Memoization", () => {
it("should memoize source database options", () => {
const { rerender } = renderWithContext(<SelectSourceAndTargetContainers />);
expect(mockUseDatabases).toHaveBeenCalled();
rerender(
<CopyJobContextProvider explorer={mockExplorer}>
<SelectSourceAndTargetContainers />
</CopyJobContextProvider>,
);
expect(mockUseDatabases).toHaveBeenCalled();
});
it("should memoize target database options", () => {
const { rerender } = renderWithContext(<SelectSourceAndTargetContainers />);
expect(mockUseDatabases).toHaveBeenCalled();
rerender(
<CopyJobContextProvider explorer={mockExplorer}>
<SelectSourceAndTargetContainers />
</CopyJobContextProvider>,
);
expect(mockUseDatabases).toHaveBeenCalled();
});
it("should memoize source container options", () => {
const { rerender } = renderWithContext(<SelectSourceAndTargetContainers />);
expect(mockUseDataContainers).toHaveBeenCalled();
rerender(
<CopyJobContextProvider explorer={mockExplorer}>
<SelectSourceAndTargetContainers />
</CopyJobContextProvider>,
);
expect(mockUseDataContainers).toHaveBeenCalled();
});
it("should memoize target container options", () => {
const { rerender } = renderWithContext(<SelectSourceAndTargetContainers />);
expect(mockUseDataContainers).toHaveBeenCalled();
rerender(
<CopyJobContextProvider explorer={mockExplorer}>
<SelectSourceAndTargetContainers />
</CopyJobContextProvider>,
);
expect(mockUseDataContainers).toHaveBeenCalled();
});
});
describe("Database Container Section Props", () => {
it("should pass correct props to source DatabaseContainerSection", () => {
renderWithContext(<SelectSourceAndTargetContainers />);
expect(screen.getByText("Source Container")).toBeInTheDocument();
});
it("should pass correct props to target DatabaseContainerSection", () => {
renderWithContext(<SelectSourceAndTargetContainers showAddCollectionPanel={mockShowAddCollectionPanel} />);
expect(screen.getByText("Target Container")).toBeInTheDocument();
});
it("should disable source container dropdown when no database is selected", () => {
mockUseSourceAndTargetData.mockReturnValue({
...mockMemoizedData,
source: {
...mockMemoizedData.source,
databaseId: "",
},
} as ReturnType<typeof useSourceAndTargetData>);
renderWithContext(<SelectSourceAndTargetContainers />);
expect(screen.getByText("Source Container")).toBeInTheDocument();
});
it("should disable target container dropdown when no database is selected", () => {
mockUseSourceAndTargetData.mockReturnValue({
...mockMemoizedData,
target: {
...mockMemoizedData.target,
databaseId: "",
},
} as ReturnType<typeof useSourceAndTargetData>);
renderWithContext(<SelectSourceAndTargetContainers />);
expect(screen.getByText("Target Container")).toBeInTheDocument();
});
});
describe("Error Handling", () => {
it("should handle hooks returning null gracefully", () => {
mockUseDatabases.mockReturnValue(null);
mockUseDataContainers.mockReturnValue(null);
renderWithContext(<SelectSourceAndTargetContainers />);
expect(screen.getByText("Select source and target containers for migration")).toBeInTheDocument();
});
it("should handle hooks throwing errors gracefully", () => {
const originalError = console.error;
console.error = jest.fn();
mockUseDatabases.mockImplementation(() => {
throw new Error("Database fetch error");
});
expect(() => {
renderWithContext(<SelectSourceAndTargetContainers />);
}).toThrow();
console.error = originalError;
});
it("should handle missing source data gracefully", () => {
mockUseSourceAndTargetData.mockReturnValue({
...mockMemoizedData,
source: undefined,
} as ReturnType<typeof useSourceAndTargetData>);
const { container } = renderWithContext(<SelectSourceAndTargetContainers />);
expect(container.firstChild).toBeNull();
});
});
describe("Integration with CopyJobContext", () => {
it("should use CopyJobContext for state management", () => {
renderWithContext(<SelectSourceAndTargetContainers />);
expect(mockUseSourceAndTargetData).toHaveBeenCalled();
});
it("should respond to context state changes", () => {
const { rerender } = renderWithContext(<SelectSourceAndTargetContainers />);
mockUseSourceAndTargetData.mockReturnValue({
...mockMemoizedData,
source: {
...mockMemoizedData.source,
databaseId: "different-db",
},
} as ReturnType<typeof useSourceAndTargetData>);
rerender(
<CopyJobContextProvider explorer={mockExplorer}>
<SelectSourceAndTargetContainers />
</CopyJobContextProvider>,
);
expect(mockUseSourceAndTargetData).toHaveBeenCalled();
});
});
describe("Stack Layout", () => {
it("should render with correct Stack className", () => {
const { container } = renderWithContext(<SelectSourceAndTargetContainers />);
const stackElement = container.querySelector(".selectSourceAndTargetContainers");
expect(stackElement).toBeInTheDocument();
});
it("should apply correct spacing tokens", () => {
renderWithContext(<SelectSourceAndTargetContainers />);
expect(screen.getByText("Select source and target containers for migration")).toBeInTheDocument();
});
});
describe("Component Structure", () => {
it("should render description, source section, and target section in correct order", () => {
renderWithContext(<SelectSourceAndTargetContainers />);
const description = screen.getByText("Select source and target containers for migration");
const sourceSection = screen.getByText("Source Container");
const targetSection = screen.getByText("Target Container");
expect(description).toBeInTheDocument();
expect(sourceSection).toBeInTheDocument();
expect(targetSection).toBeInTheDocument();
});
it("should maintain component hierarchy", () => {
const { container } = renderWithContext(<SelectSourceAndTargetContainers />);
const mainContainer = container.querySelector(".selectSourceAndTargetContainers");
expect(mainContainer).toBeInTheDocument();
});
});
describe("Performance", () => {
it("should not cause unnecessary re-renders when props don't change", () => {
const { rerender } = renderWithContext(<SelectSourceAndTargetContainers />);
rerender(
<CopyJobContextProvider explorer={mockExplorer}>
<SelectSourceAndTargetContainers />
</CopyJobContextProvider>,
);
expect(mockUseSourceAndTargetData).toHaveBeenCalled();
});
it("should handle rapid state changes efficiently", () => {
const { rerender } = renderWithContext(<SelectSourceAndTargetContainers />);
for (let i = 0; i < 5; i++) {
mockUseSourceAndTargetData.mockReturnValue({
...mockMemoizedData,
source: {
...mockMemoizedData.source,
databaseId: `db-${i}`,
},
} as ReturnType<typeof useSourceAndTargetData>);
rerender(
<CopyJobContextProvider explorer={mockExplorer}>
<SelectSourceAndTargetContainers />
</CopyJobContextProvider>,
);
}
expect(mockUseSourceAndTargetData).toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,452 @@
import "@testing-library/jest-dom";
import { fireEvent, render, screen } from "@testing-library/react";
import React from "react";
import ContainerCopyMessages from "../../../../ContainerCopyMessages";
import { DatabaseContainerSectionProps, DropdownOptionType } from "../../../../Types/CopyJobTypes";
import { DatabaseContainerSection } from "./DatabaseContainerSection";
describe("DatabaseContainerSection", () => {
const mockDatabaseOnChange = jest.fn();
const mockContainerOnChange = jest.fn();
const mockHandleOnDemandCreateContainer = jest.fn();
const mockDatabaseOptions: DropdownOptionType[] = [
{ key: "db1", text: "Database 1", data: { id: "db1" } },
{ key: "db2", text: "Database 2", data: { id: "db2" } },
{ key: "db3", text: "Database 3", data: { id: "db3" } },
];
const mockContainerOptions: DropdownOptionType[] = [
{ key: "container1", text: "Container 1", data: { id: "container1" } },
{ key: "container2", text: "Container 2", data: { id: "container2" } },
{ key: "container3", text: "Container 3", data: { id: "container3" } },
];
const defaultProps: DatabaseContainerSectionProps = {
heading: "Source container",
databaseOptions: mockDatabaseOptions,
selectedDatabase: "db1",
databaseDisabled: false,
databaseOnChange: mockDatabaseOnChange,
containerOptions: mockContainerOptions,
selectedContainer: "container1",
containerDisabled: false,
containerOnChange: mockContainerOnChange,
};
beforeEach(() => {
jest.clearAllMocks();
});
describe("Component Rendering", () => {
it("renders the component with correct structure", () => {
const { container } = render(<DatabaseContainerSection {...defaultProps} />);
expect(container.firstChild).toHaveClass("databaseContainerSection");
expect(screen.getByText("Source container")).toBeInTheDocument();
});
it("renders heading correctly", () => {
render(<DatabaseContainerSection {...defaultProps} />);
const heading = screen.getByText("Source container");
expect(heading).toBeInTheDocument();
expect(heading.tagName).toBe("LABEL");
expect(heading).toHaveClass("subHeading");
});
it("renders database dropdown with correct properties", () => {
render(<DatabaseContainerSection {...defaultProps} />);
const databaseDropdown = screen.getByRole("combobox", {
name: ContainerCopyMessages.databaseDropdownLabel,
});
expect(databaseDropdown).toBeInTheDocument();
expect(databaseDropdown).toHaveAttribute("aria-label", ContainerCopyMessages.databaseDropdownLabel);
expect(databaseDropdown).not.toBeDisabled();
});
it("renders container dropdown with correct properties", () => {
render(<DatabaseContainerSection {...defaultProps} />);
const containerDropdown = screen.getByRole("combobox", {
name: ContainerCopyMessages.containerDropdownLabel,
});
expect(containerDropdown).toBeInTheDocument();
expect(containerDropdown).toHaveAttribute("aria-label", ContainerCopyMessages.containerDropdownLabel);
expect(containerDropdown).not.toBeDisabled();
});
it("renders database label correctly", () => {
render(<DatabaseContainerSection {...defaultProps} />);
expect(screen.getByText(`${ContainerCopyMessages.databaseDropdownLabel}:`)).toBeInTheDocument();
});
it("renders container label correctly", () => {
render(<DatabaseContainerSection {...defaultProps} />);
expect(screen.getByText(`${ContainerCopyMessages.containerDropdownLabel}:`)).toBeInTheDocument();
});
it("does not render create container button when handleOnDemandCreateContainer is not provided", () => {
render(<DatabaseContainerSection {...defaultProps} />);
expect(screen.queryByText(ContainerCopyMessages.createContainerButtonLabel)).not.toBeInTheDocument();
});
it("renders create container button when handleOnDemandCreateContainer is provided", () => {
const propsWithCreateHandler = {
...defaultProps,
handleOnDemandCreateContainer: mockHandleOnDemandCreateContainer,
};
const { container } = render(<DatabaseContainerSection {...propsWithCreateHandler} />);
const createButton = container.querySelector(".create-container-link-btn");
expect(createButton).toBeInTheDocument();
expect(createButton).toHaveTextContent(ContainerCopyMessages.createContainerButtonLabel);
});
});
describe("Dropdown States", () => {
it("renders database dropdown as disabled when databaseDisabled is true", () => {
const propsWithDisabledDatabase = {
...defaultProps,
databaseDisabled: true,
};
render(<DatabaseContainerSection {...propsWithDisabledDatabase} />);
const databaseDropdown = screen.getByRole("combobox", {
name: ContainerCopyMessages.databaseDropdownLabel,
});
expect(databaseDropdown).toHaveAttribute("aria-disabled", "true");
});
it("renders container dropdown as disabled when containerDisabled is true", () => {
const propsWithDisabledContainer = {
...defaultProps,
containerDisabled: true,
};
render(<DatabaseContainerSection {...propsWithDisabledContainer} />);
const containerDropdown = screen.getByRole("combobox", {
name: ContainerCopyMessages.containerDropdownLabel,
});
expect(containerDropdown).toHaveAttribute("aria-disabled", "true");
});
it("handles falsy values for disabled props correctly", () => {
const propsWithFalsyDisabled = {
...defaultProps,
databaseDisabled: undefined,
containerDisabled: null,
} as DatabaseContainerSectionProps;
render(<DatabaseContainerSection {...propsWithFalsyDisabled} />);
const databaseDropdown = screen.getByRole("combobox", {
name: ContainerCopyMessages.databaseDropdownLabel,
});
const containerDropdown = screen.getByRole("combobox", {
name: ContainerCopyMessages.containerDropdownLabel,
});
expect(databaseDropdown).not.toHaveAttribute("aria-disabled", "true");
expect(containerDropdown).not.toHaveAttribute("aria-disabled", "true");
});
});
describe("User Interactions", () => {
it("calls databaseOnChange when database dropdown selection changes", () => {
render(<DatabaseContainerSection {...defaultProps} />);
const databaseDropdown = screen.getByRole("combobox", {
name: ContainerCopyMessages.databaseDropdownLabel,
});
fireEvent.click(databaseDropdown);
expect(databaseDropdown).toHaveAttribute("aria-label", ContainerCopyMessages.databaseDropdownLabel);
});
it("calls containerOnChange when container dropdown selection changes", () => {
render(<DatabaseContainerSection {...defaultProps} />);
const containerDropdown = screen.getByRole("combobox", {
name: ContainerCopyMessages.containerDropdownLabel,
});
fireEvent.click(containerDropdown);
expect(containerDropdown).toHaveAttribute("aria-label", ContainerCopyMessages.containerDropdownLabel);
});
it("calls handleOnDemandCreateContainer when create container button is clicked", () => {
const propsWithCreateHandler = {
...defaultProps,
handleOnDemandCreateContainer: mockHandleOnDemandCreateContainer,
};
render(<DatabaseContainerSection {...propsWithCreateHandler} />);
const createButton = screen.getByText(ContainerCopyMessages.createContainerButtonLabel);
fireEvent.click(createButton);
expect(mockHandleOnDemandCreateContainer).toHaveBeenCalledTimes(1);
expect(mockHandleOnDemandCreateContainer).toHaveBeenCalledWith();
});
});
describe("Props Validation", () => {
it("renders with different heading text", () => {
const propsWithDifferentHeading = {
...defaultProps,
heading: "Target container",
};
render(<DatabaseContainerSection {...propsWithDifferentHeading} />);
expect(screen.getByText("Target container")).toBeInTheDocument();
expect(screen.queryByText("Source container")).not.toBeInTheDocument();
});
it("renders with different selected values", () => {
const propsWithDifferentSelections = {
...defaultProps,
selectedDatabase: "db2",
selectedContainer: "container3",
};
render(<DatabaseContainerSection {...propsWithDifferentSelections} />);
expect(screen.getByText("Source container")).toBeInTheDocument();
});
it("renders with empty options arrays", () => {
const propsWithEmptyOptions = {
...defaultProps,
databaseOptions: [],
containerOptions: [],
} as DatabaseContainerSectionProps;
render(<DatabaseContainerSection {...propsWithEmptyOptions} />);
const databaseDropdown = screen.getByRole("combobox", {
name: ContainerCopyMessages.databaseDropdownLabel,
});
const containerDropdown = screen.getByRole("combobox", {
name: ContainerCopyMessages.containerDropdownLabel,
});
expect(databaseDropdown).toBeInTheDocument();
expect(containerDropdown).toBeInTheDocument();
});
});
describe("Accessibility", () => {
it("has proper ARIA labels for dropdowns", () => {
render(<DatabaseContainerSection {...defaultProps} />);
const databaseDropdown = screen.getByRole("combobox", {
name: ContainerCopyMessages.databaseDropdownLabel,
});
const containerDropdown = screen.getByRole("combobox", {
name: ContainerCopyMessages.containerDropdownLabel,
});
expect(databaseDropdown).toHaveAttribute("aria-label", ContainerCopyMessages.databaseDropdownLabel);
expect(containerDropdown).toHaveAttribute("aria-label", ContainerCopyMessages.containerDropdownLabel);
});
it("has proper required attributes for dropdowns", () => {
render(<DatabaseContainerSection {...defaultProps} />);
const databaseDropdown = screen.getByRole("combobox", {
name: ContainerCopyMessages.databaseDropdownLabel,
});
const containerDropdown = screen.getByRole("combobox", {
name: ContainerCopyMessages.containerDropdownLabel,
});
expect(databaseDropdown).toHaveAttribute("aria-required", "true");
expect(containerDropdown).toHaveAttribute("aria-required", "true");
});
it("maintains proper label associations", () => {
render(<DatabaseContainerSection {...defaultProps} />);
expect(screen.getByText(`${ContainerCopyMessages.databaseDropdownLabel}:`)).toBeInTheDocument();
expect(screen.getByText(`${ContainerCopyMessages.containerDropdownLabel}:`)).toBeInTheDocument();
});
});
describe("Edge Cases", () => {
it("handles undefined optional props gracefully", () => {
const minimalProps: DatabaseContainerSectionProps = {
heading: "Test Heading",
databaseOptions: mockDatabaseOptions,
selectedDatabase: "db1",
databaseOnChange: mockDatabaseOnChange,
containerOptions: mockContainerOptions,
selectedContainer: "container1",
containerOnChange: mockContainerOnChange,
};
render(<DatabaseContainerSection {...minimalProps} />);
expect(screen.getByText("Test Heading")).toBeInTheDocument();
expect(screen.queryByText(ContainerCopyMessages.createContainerButtonLabel)).not.toBeInTheDocument();
});
it("handles empty string selections", () => {
const propsWithEmptySelections = {
...defaultProps,
selectedDatabase: "",
selectedContainer: "",
};
render(<DatabaseContainerSection {...propsWithEmptySelections} />);
expect(screen.getByText("Source container")).toBeInTheDocument();
});
it("renders correctly with long option texts", () => {
const longOptions = [
{
key: "long1",
text: "This is a very long database name that might wrap to multiple lines in the dropdown",
data: { id: "long1" },
},
];
const propsWithLongOptions = {
...defaultProps,
databaseOptions: longOptions,
containerOptions: longOptions,
selectedDatabase: "long1",
selectedContainer: "long1",
};
render(<DatabaseContainerSection {...propsWithLongOptions} />);
expect(screen.getByText("Source container")).toBeInTheDocument();
});
});
describe("Component Structure", () => {
it("has correct CSS classes applied", () => {
const { container } = render(<DatabaseContainerSection {...defaultProps} />);
const mainContainer = container.querySelector(".databaseContainerSection");
expect(mainContainer).toBeInTheDocument();
const subHeading = screen.getByText("Source container");
expect(subHeading).toHaveClass("subHeading");
});
it("maintains proper component hierarchy", () => {
const { container } = render(<DatabaseContainerSection {...defaultProps} />);
const mainStack = container.querySelector(".databaseContainerSection");
expect(mainStack).toBeInTheDocument();
const fieldRows = container.querySelectorAll(".flex-row");
expect(fieldRows.length).toBe(2);
});
it("renders create button in correct position when provided", () => {
const propsWithCreateHandler = {
...defaultProps,
handleOnDemandCreateContainer: mockHandleOnDemandCreateContainer,
};
const { container } = render(<DatabaseContainerSection {...propsWithCreateHandler} />);
const createButton = screen.getByText(ContainerCopyMessages.createContainerButtonLabel);
expect(createButton).toBeInTheDocument();
const containerSection = container.querySelector(".databaseContainerSection");
expect(containerSection).toContainElement(createButton);
});
it("displays correct create container button label", () => {
const propsWithCreateHandler = {
...defaultProps,
handleOnDemandCreateContainer: mockHandleOnDemandCreateContainer,
};
render(<DatabaseContainerSection {...propsWithCreateHandler} />);
expect(screen.getByText(ContainerCopyMessages.createContainerButtonLabel)).toBeInTheDocument();
});
});
describe("Snapshot Testing", () => {
it("matches snapshot with minimal props", () => {
const minimalProps: DatabaseContainerSectionProps = {
heading: "Source Container",
databaseOptions: [{ key: "db1", text: "Database 1", data: { id: "db1" } }],
selectedDatabase: "db1",
databaseOnChange: jest.fn(),
containerOptions: [{ key: "c1", text: "Container 1", data: { id: "c1" } }],
selectedContainer: "c1",
containerOnChange: jest.fn(),
};
const { container } = render(<DatabaseContainerSection {...minimalProps} />);
expect(container.firstChild).toMatchSnapshot();
});
it("matches snapshot with all props including create container handler", () => {
const fullProps: DatabaseContainerSectionProps = {
heading: "Target Container",
databaseOptions: mockDatabaseOptions,
selectedDatabase: "db2",
databaseDisabled: false,
databaseOnChange: jest.fn(),
containerOptions: mockContainerOptions,
selectedContainer: "container2",
containerDisabled: false,
containerOnChange: jest.fn(),
handleOnDemandCreateContainer: jest.fn(),
};
const { container } = render(<DatabaseContainerSection {...fullProps} />);
expect(container.firstChild).toMatchSnapshot();
});
it("matches snapshot with disabled states", () => {
const disabledProps: DatabaseContainerSectionProps = {
heading: "Disabled Section",
databaseOptions: mockDatabaseOptions,
selectedDatabase: "db1",
databaseDisabled: true,
databaseOnChange: jest.fn(),
containerOptions: mockContainerOptions,
selectedContainer: "container1",
containerDisabled: true,
containerOnChange: jest.fn(),
};
const { container } = render(<DatabaseContainerSection {...disabledProps} />);
expect(container.firstChild).toMatchSnapshot();
});
it("matches snapshot with empty options", () => {
const emptyOptionsProps: DatabaseContainerSectionProps = {
heading: "Empty Options",
databaseOptions: [],
selectedDatabase: "",
databaseOnChange: jest.fn(),
containerOptions: [],
selectedContainer: "",
containerOnChange: jest.fn(),
};
const { container } = render(<DatabaseContainerSection {...emptyOptionsProps} />);
expect(container.firstChild).toMatchSnapshot();
});
});
});

View File

@@ -0,0 +1,518 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DatabaseContainerSection Snapshot Testing matches snapshot with all props including create container handler 1`] = `
<div
class="ms-Stack databaseContainerSection css-109"
>
<label
class="subHeading"
>
Target Container
</label>
<div
class="ms-Stack flex-row css-110"
>
<div
class="ms-StackItem flex-fixed-width css-111"
>
<label
class="field-label "
>
Database
:
</label>
</div>
<div
class="ms-StackItem flex-grow-col css-111"
>
<div
class="ms-Dropdown-container"
>
<div
aria-disabled="false"
aria-expanded="false"
aria-haspopup="listbox"
aria-label="Database"
aria-required="true"
class="ms-Dropdown is-required dropdown-112"
data-is-focusable="true"
data-ktp-target="true"
id="Dropdown98"
role="combobox"
tabindex="0"
>
<span
aria-invalid="false"
class="ms-Dropdown-title title-134"
id="Dropdown98-option"
>
Database 2
</span>
<span
class="ms-Dropdown-caretDownWrapper caretDownWrapper-114"
>
<i
aria-hidden="true"
class="ms-Dropdown-caretDown caretDown-132"
data-icon-name="ChevronDown"
>
</i>
</span>
</div>
</div>
</div>
</div>
<div
class="ms-Stack flex-row css-110"
>
<div
class="ms-StackItem flex-fixed-width css-111"
>
<label
class="field-label "
>
Container
:
</label>
</div>
<div
class="ms-StackItem flex-grow-col css-111"
>
<div
class="ms-Stack css-133"
>
<div
class="ms-Dropdown-container"
>
<div
aria-disabled="false"
aria-expanded="false"
aria-haspopup="listbox"
aria-label="Container"
aria-required="true"
class="ms-Dropdown is-required dropdown-112"
data-is-focusable="true"
data-ktp-target="true"
id="Dropdown99"
role="combobox"
tabindex="0"
>
<span
aria-invalid="false"
class="ms-Dropdown-title title-134"
id="Dropdown99-option"
>
Container 2
</span>
<span
class="ms-Dropdown-caretDownWrapper caretDownWrapper-114"
>
<i
aria-hidden="true"
class="ms-Dropdown-caretDown caretDown-132"
data-icon-name="ChevronDown"
>
</i>
</span>
</div>
</div>
<button
class="ms-Button ms-Button--action ms-Button--command create-container-link-btn root-135"
data-is-focusable="true"
type="button"
>
<span
class="ms-Button-flexContainer flexContainer-136"
data-automationid="splitbuttonprimary"
>
<span
class="ms-Button-textContainer textContainer-137"
>
<span
class="ms-Button-label label-139"
id="id__100"
>
Create a new container
</span>
</span>
</span>
</button>
</div>
</div>
</div>
</div>
`;
exports[`DatabaseContainerSection Snapshot Testing matches snapshot with disabled states 1`] = `
<div
class="ms-Stack databaseContainerSection css-109"
>
<label
class="subHeading"
>
Disabled Section
</label>
<div
class="ms-Stack flex-row css-110"
>
<div
class="ms-StackItem flex-fixed-width css-111"
>
<label
class="field-label "
>
Database
:
</label>
</div>
<div
class="ms-StackItem flex-grow-col css-111"
>
<div
class="ms-Dropdown-container"
>
<div
aria-disabled="true"
aria-expanded="false"
aria-haspopup="listbox"
aria-label="Database"
aria-required="true"
class="ms-Dropdown is-disabled is-required dropdown-143"
data-is-focusable="false"
data-ktp-target="true"
id="Dropdown103"
role="combobox"
tabindex="-1"
>
<span
aria-invalid="false"
class="ms-Dropdown-title title-148"
id="Dropdown103-option"
>
Database 1
</span>
<span
class="ms-Dropdown-caretDownWrapper caretDownWrapper-145"
>
<i
aria-hidden="true"
class="ms-Dropdown-caretDown caretDown-147"
data-icon-name="ChevronDown"
>
</i>
</span>
</div>
</div>
</div>
</div>
<div
class="ms-Stack flex-row css-110"
>
<div
class="ms-StackItem flex-fixed-width css-111"
>
<label
class="field-label "
>
Container
:
</label>
</div>
<div
class="ms-StackItem flex-grow-col css-111"
>
<div
class="ms-Stack css-133"
>
<div
class="ms-Dropdown-container"
>
<div
aria-disabled="true"
aria-expanded="false"
aria-haspopup="listbox"
aria-label="Container"
aria-required="true"
class="ms-Dropdown is-disabled is-required dropdown-143"
data-is-focusable="false"
data-ktp-target="true"
id="Dropdown104"
role="combobox"
tabindex="-1"
>
<span
aria-invalid="false"
class="ms-Dropdown-title title-148"
id="Dropdown104-option"
>
Container 1
</span>
<span
class="ms-Dropdown-caretDownWrapper caretDownWrapper-145"
>
<i
aria-hidden="true"
class="ms-Dropdown-caretDown caretDown-147"
data-icon-name="ChevronDown"
>
</i>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`DatabaseContainerSection Snapshot Testing matches snapshot with empty options 1`] = `
<div
class="ms-Stack databaseContainerSection css-109"
>
<label
class="subHeading"
>
Empty Options
</label>
<div
class="ms-Stack flex-row css-110"
>
<div
class="ms-StackItem flex-fixed-width css-111"
>
<label
class="field-label "
>
Database
:
</label>
</div>
<div
class="ms-StackItem flex-grow-col css-111"
>
<div
class="ms-Dropdown-container"
>
<div
aria-disabled="false"
aria-expanded="false"
aria-haspopup="listbox"
aria-label="Database"
aria-required="true"
class="ms-Dropdown is-required dropdown-112"
data-is-focusable="true"
data-ktp-target="true"
id="Dropdown105"
role="combobox"
tabindex="0"
>
<span
aria-invalid="false"
class="ms-Dropdown-title ms-Dropdown-titleIsPlaceHolder title-113"
id="Dropdown105-option"
>
Select a database
</span>
<span
class="ms-Dropdown-caretDownWrapper caretDownWrapper-114"
>
<i
aria-hidden="true"
class="ms-Dropdown-caretDown caretDown-132"
data-icon-name="ChevronDown"
>
</i>
</span>
</div>
</div>
</div>
</div>
<div
class="ms-Stack flex-row css-110"
>
<div
class="ms-StackItem flex-fixed-width css-111"
>
<label
class="field-label "
>
Container
:
</label>
</div>
<div
class="ms-StackItem flex-grow-col css-111"
>
<div
class="ms-Stack css-133"
>
<div
class="ms-Dropdown-container"
>
<div
aria-disabled="false"
aria-expanded="false"
aria-haspopup="listbox"
aria-label="Container"
aria-required="true"
class="ms-Dropdown is-required dropdown-112"
data-is-focusable="true"
data-ktp-target="true"
id="Dropdown106"
role="combobox"
tabindex="0"
>
<span
aria-invalid="false"
class="ms-Dropdown-title ms-Dropdown-titleIsPlaceHolder title-113"
id="Dropdown106-option"
>
Select a container
</span>
<span
class="ms-Dropdown-caretDownWrapper caretDownWrapper-114"
>
<i
aria-hidden="true"
class="ms-Dropdown-caretDown caretDown-132"
data-icon-name="ChevronDown"
>
</i>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`DatabaseContainerSection Snapshot Testing matches snapshot with minimal props 1`] = `
<div
class="ms-Stack databaseContainerSection css-109"
>
<label
class="subHeading"
>
Source Container
</label>
<div
class="ms-Stack flex-row css-110"
>
<div
class="ms-StackItem flex-fixed-width css-111"
>
<label
class="field-label "
>
Database
:
</label>
</div>
<div
class="ms-StackItem flex-grow-col css-111"
>
<div
class="ms-Dropdown-container"
>
<div
aria-disabled="false"
aria-expanded="false"
aria-haspopup="listbox"
aria-label="Database"
aria-required="true"
class="ms-Dropdown is-required dropdown-112"
data-is-focusable="true"
data-ktp-target="true"
id="Dropdown96"
role="combobox"
tabindex="0"
>
<span
aria-invalid="false"
class="ms-Dropdown-title title-134"
id="Dropdown96-option"
>
Database 1
</span>
<span
class="ms-Dropdown-caretDownWrapper caretDownWrapper-114"
>
<i
aria-hidden="true"
class="ms-Dropdown-caretDown caretDown-132"
data-icon-name="ChevronDown"
>
</i>
</span>
</div>
</div>
</div>
</div>
<div
class="ms-Stack flex-row css-110"
>
<div
class="ms-StackItem flex-fixed-width css-111"
>
<label
class="field-label "
>
Container
:
</label>
</div>
<div
class="ms-StackItem flex-grow-col css-111"
>
<div
class="ms-Stack css-133"
>
<div
class="ms-Dropdown-container"
>
<div
aria-disabled="false"
aria-expanded="false"
aria-haspopup="listbox"
aria-label="Container"
aria-required="true"
class="ms-Dropdown is-required dropdown-112"
data-is-focusable="true"
data-ktp-target="true"
id="Dropdown97"
role="combobox"
tabindex="0"
>
<span
aria-invalid="false"
class="ms-Dropdown-title title-134"
id="Dropdown97-option"
>
Container 1
</span>
<span
class="ms-Dropdown-caretDownWrapper caretDownWrapper-114"
>
<i
aria-hidden="true"
class="ms-Dropdown-caretDown caretDown-132"
data-icon-name="ChevronDown"
>
</i>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
`;

View File

@@ -0,0 +1,387 @@
import "@testing-library/jest-dom";
import { render } from "@testing-library/react";
import React from "react";
import { DatabaseAccount, Subscription } from "../../../../../Contracts/DataModels";
import { CopyJobMigrationType } from "../../../Enums/CopyJobEnums";
import { CopyJobContextState } from "../../../Types/CopyJobTypes";
import { useSourceAndTargetData } from "./memoizedData";
jest.mock("../../../CopyJobUtils", () => ({
getAccountDetailsFromResourceId: jest.fn(),
}));
import { getAccountDetailsFromResourceId } from "../../../CopyJobUtils";
const mockGetAccountDetailsFromResourceId = getAccountDetailsFromResourceId as jest.MockedFunction<
typeof getAccountDetailsFromResourceId
>;
interface TestComponentProps {
copyJobState: CopyJobContextState | null;
onResult?: (result: any) => void;
}
const TestComponent: React.FC<TestComponentProps> = ({ copyJobState, onResult }) => {
const result = useSourceAndTargetData(copyJobState);
React.useEffect(() => {
onResult?.(result);
}, [result, onResult]);
return <div data-testid="test-component">Test Component</div>;
};
describe("useSourceAndTargetData", () => {
const mockSubscription: Subscription = {
subscriptionId: "test-subscription-id",
displayName: "Test Subscription",
state: "Enabled",
subscriptionPolicies: null,
authorizationSource: "RoleBased",
};
const mockSourceAccount: DatabaseAccount = {
id: "/subscriptions/source-sub-id/resourceGroups/source-rg/providers/Microsoft.DocumentDB/databaseAccounts/source-account",
name: "source-account",
location: "East US",
type: "Microsoft.DocumentDB/databaseAccounts",
kind: "GlobalDocumentDB",
properties: {
documentEndpoint: "https://source-account.documents.azure.com:443/",
capabilities: [],
locations: [],
},
};
const mockTargetAccount: DatabaseAccount = {
id: "/subscriptions/target-sub-id/resourceGroups/target-rg/providers/Microsoft.DocumentDB/databaseAccounts/target-account",
name: "target-account",
location: "West US",
type: "Microsoft.DocumentDB/databaseAccounts",
kind: "GlobalDocumentDB",
properties: {
documentEndpoint: "https://target-account.documents.azure.com:443/",
capabilities: [],
locations: [],
},
};
const mockCopyJobState: CopyJobContextState = {
jobName: "test-job",
migrationType: CopyJobMigrationType.Offline,
sourceReadAccessFromTarget: false,
source: {
subscription: mockSubscription,
account: mockSourceAccount,
databaseId: "source-db",
containerId: "source-container",
},
target: {
subscriptionId: "target-subscription-id",
account: mockTargetAccount,
databaseId: "target-db",
containerId: "target-container",
},
};
beforeEach(() => {
mockGetAccountDetailsFromResourceId.mockImplementation((accountId) => {
if (accountId === mockSourceAccount.id) {
return {
subscriptionId: "source-sub-id",
resourceGroup: "source-rg",
accountName: "source-account",
};
} else if (accountId === mockTargetAccount.id) {
return {
subscriptionId: "target-sub-id",
resourceGroup: "target-rg",
accountName: "target-account",
};
}
return null;
});
});
afterEach(() => {
jest.clearAllMocks();
});
describe("Hook Execution", () => {
it("should return correct data structure when copyJobState is provided", () => {
let hookResult: any = null;
const onResult = jest.fn((result) => {
hookResult = result;
});
render(<TestComponent copyJobState={mockCopyJobState} onResult={onResult} />);
expect(onResult).toHaveBeenCalled();
expect(hookResult).toBeDefined();
expect(hookResult).toHaveProperty("source");
expect(hookResult).toHaveProperty("target");
expect(hookResult).toHaveProperty("sourceDbParams");
expect(hookResult).toHaveProperty("sourceContainerParams");
expect(hookResult).toHaveProperty("targetDbParams");
expect(hookResult).toHaveProperty("targetContainerParams");
});
it("should call getAccountDetailsFromResourceId with correct parameters", () => {
render(<TestComponent copyJobState={mockCopyJobState} />);
expect(mockGetAccountDetailsFromResourceId).toHaveBeenCalledWith(mockSourceAccount.id);
expect(mockGetAccountDetailsFromResourceId).toHaveBeenCalledWith(mockTargetAccount.id);
expect(mockGetAccountDetailsFromResourceId).toHaveBeenCalledTimes(2);
});
it("should return source and target objects from copyJobState", () => {
let hookResult: any = null;
const onResult = jest.fn((result) => {
hookResult = result;
});
render(<TestComponent copyJobState={mockCopyJobState} onResult={onResult} />);
expect(hookResult.source).toEqual(mockCopyJobState.source);
expect(hookResult.target).toEqual(mockCopyJobState.target);
});
it("should construct sourceDbParams array correctly", () => {
let hookResult: any = null;
const onResult = jest.fn((result) => {
hookResult = result;
});
render(<TestComponent copyJobState={mockCopyJobState} onResult={onResult} />);
expect(hookResult.sourceDbParams).toEqual(["source-sub-id", "source-rg", "source-account", "SQL"]);
});
it("should construct sourceContainerParams array correctly", () => {
let hookResult: any = null;
const onResult = jest.fn((result) => {
hookResult = result;
});
render(<TestComponent copyJobState={mockCopyJobState} onResult={onResult} />);
expect(hookResult.sourceContainerParams).toEqual([
"source-sub-id",
"source-rg",
"source-account",
"source-db",
"SQL",
]);
});
it("should construct targetDbParams array correctly", () => {
let hookResult: any = null;
const onResult = jest.fn((result) => {
hookResult = result;
});
render(<TestComponent copyJobState={mockCopyJobState} onResult={onResult} />);
expect(hookResult.targetDbParams).toEqual(["target-sub-id", "target-rg", "target-account", "SQL"]);
});
it("should construct targetContainerParams array correctly", () => {
let hookResult: any = null;
const onResult = jest.fn((result) => {
hookResult = result;
});
render(<TestComponent copyJobState={mockCopyJobState} onResult={onResult} />);
expect(hookResult.targetContainerParams).toEqual([
"target-sub-id",
"target-rg",
"target-account",
"target-db",
"SQL",
]);
});
});
describe("Memoization and Performance", () => {
it("should work with React strict mode (double invocation)", () => {
let hookResult: any = null;
const onResult = jest.fn((result) => {
hookResult = result;
});
const { rerender } = render(<TestComponent copyJobState={mockCopyJobState} onResult={onResult} />);
const firstResult = { ...hookResult };
rerender(<TestComponent copyJobState={mockCopyJobState} onResult={onResult} />);
const secondResult = { ...hookResult };
expect(firstResult).toEqual(secondResult);
});
it("should handle component re-renders gracefully", () => {
let renderCount = 0;
const onResult = jest.fn(() => {
renderCount++;
});
const { rerender } = render(<TestComponent copyJobState={mockCopyJobState} onResult={onResult} />);
for (let i = 0; i < 5; i++) {
rerender(<TestComponent copyJobState={mockCopyJobState} onResult={onResult} />);
}
expect(renderCount).toBeGreaterThan(0);
expect(mockGetAccountDetailsFromResourceId).toHaveBeenCalled();
});
it("should recalculate when copyJobState changes", () => {
let hookResult: any = null;
const onResult = jest.fn((result) => {
hookResult = result;
});
const { rerender } = render(<TestComponent copyJobState={mockCopyJobState} onResult={onResult} />);
const firstResult = { ...hookResult };
const updatedState = {
...mockCopyJobState,
source: {
...mockCopyJobState.source,
databaseId: "updated-source-db",
},
};
rerender(<TestComponent copyJobState={updatedState} onResult={onResult} />);
const secondResult = { ...hookResult };
expect(firstResult.sourceContainerParams[3]).toBe("source-db");
expect(secondResult.sourceContainerParams[3]).toBe("updated-source-db");
});
});
describe("Complex State Scenarios", () => {
it("should handle state with only source defined", () => {
const sourceOnlyState = {
...mockCopyJobState,
target: undefined as any,
};
let hookResult: any = null;
const onResult = jest.fn((result) => {
hookResult = result;
});
render(<TestComponent copyJobState={sourceOnlyState} onResult={onResult} />);
expect(hookResult.source).toBeDefined();
expect(hookResult.target).toBeUndefined();
expect(hookResult.sourceDbParams).toEqual(["source-sub-id", "source-rg", "source-account", "SQL"]);
expect(hookResult.targetDbParams).toEqual([undefined, undefined, undefined, "SQL"]);
});
it("should handle state with only target defined", () => {
const targetOnlyState = {
...mockCopyJobState,
source: undefined as any,
};
let hookResult: any = null;
const onResult = jest.fn((result) => {
hookResult = result;
});
render(<TestComponent copyJobState={targetOnlyState} onResult={onResult} />);
expect(hookResult.source).toBeUndefined();
expect(hookResult.target).toBeDefined();
expect(hookResult.sourceDbParams).toEqual([undefined, undefined, undefined, "SQL"]);
expect(hookResult.targetDbParams).toEqual(["target-sub-id", "target-rg", "target-account", "SQL"]);
});
it("should handle state with missing database IDs", () => {
const stateWithoutDbIds = {
...mockCopyJobState,
source: {
...mockCopyJobState.source,
databaseId: undefined as any,
},
target: {
...mockCopyJobState.target,
databaseId: undefined as any,
},
};
let hookResult: any = null;
const onResult = jest.fn((result) => {
hookResult = result;
});
render(<TestComponent copyJobState={stateWithoutDbIds} onResult={onResult} />);
expect(hookResult.sourceContainerParams[3]).toBeUndefined();
expect(hookResult.targetContainerParams[3]).toBeUndefined();
});
it("should handle state with missing accounts", () => {
const stateWithoutAccounts = {
...mockCopyJobState,
source: {
...mockCopyJobState.source,
account: undefined as any,
},
target: {
...mockCopyJobState.target,
account: undefined as any,
},
};
let hookResult: any = null;
const onResult = jest.fn((result) => {
hookResult = result;
});
render(<TestComponent copyJobState={stateWithoutAccounts} onResult={onResult} />);
expect(mockGetAccountDetailsFromResourceId).toHaveBeenCalledWith(undefined);
expect(hookResult.sourceDbParams).toEqual([undefined, undefined, undefined, "SQL"]);
expect(hookResult.targetDbParams).toEqual([undefined, undefined, undefined, "SQL"]);
});
});
describe("Hook Return Value Structure", () => {
it("should return an object with exactly 6 properties", () => {
let hookResult: any = null;
const onResult = jest.fn((result) => {
hookResult = result;
});
render(<TestComponent copyJobState={mockCopyJobState} onResult={onResult} />);
const keys = Object.keys(hookResult);
expect(keys).toHaveLength(6);
expect(keys).toContain("source");
expect(keys).toContain("target");
expect(keys).toContain("sourceDbParams");
expect(keys).toContain("sourceContainerParams");
expect(keys).toContain("targetDbParams");
expect(keys).toContain("targetContainerParams");
});
it("should not return undefined properties when state is valid", () => {
let hookResult: any = null;
const onResult = jest.fn((result) => {
hookResult = result;
});
render(<TestComponent copyJobState={mockCopyJobState} onResult={onResult} />);
expect(hookResult.source).toBeDefined();
expect(hookResult.target).toBeDefined();
expect(hookResult.sourceDbParams).toBeDefined();
expect(hookResult.sourceContainerParams).toBeDefined();
expect(hookResult.targetDbParams).toBeDefined();
expect(hookResult.targetContainerParams).toBeDefined();
});
});
});

View File

@@ -9,12 +9,12 @@ export function useSourceAndTargetData(copyJobState: CopyJobContextState) {
subscriptionId: sourceSubscriptionId,
resourceGroup: sourceResourceGroup,
accountName: sourceAccountName,
} = getAccountDetailsFromResourceId(selectedSourceAccount?.id);
} = getAccountDetailsFromResourceId(selectedSourceAccount?.id) || {};
const {
subscriptionId: targetSubscriptionId,
resourceGroup: targetResourceGroup,
accountName: targetAccountName,
} = getAccountDetailsFromResourceId(selectedTargetAccount?.id);
} = getAccountDetailsFromResourceId(selectedTargetAccount?.id) || {};
const sourceDbParams = [sourceSubscriptionId, sourceResourceGroup, sourceAccountName, "SQL"] as DatabaseParams;
const sourceContainerParams = [

View File

@@ -0,0 +1,89 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CreateCopyJobScreensProvider should match snapshot for default render: default-render 1`] = `
<default
explorer={
{
"databaseAccount": {
"id": "test-account",
"kind": "GlobalDocumentDB",
"location": "East US",
"name": "test-account-name",
"properties": {
"cassandraEndpoint": "https://test-account.cassandra.cosmosdb.azure.com:443/",
"documentEndpoint": "https://test-account.documents.azure.com:443/",
"gremlinEndpoint": "https://test-account.gremlin.cosmosdb.azure.com:443/",
"tableEndpoint": "https://test-account.table.cosmosdb.azure.com:443/",
},
"type": "DocumentDB",
},
"resourceGroup": "test-resource-group",
"subscriptionId": "test-subscription-id",
}
}
>
<default />
</default>
`;
exports[`CreateCopyJobScreensProvider should match snapshot for edge cases: empty-explorer 1`] = `
<default
explorer={{}}
>
<default />
</default>
`;
exports[`CreateCopyJobScreensProvider should match snapshot for edge cases: partial-explorer 1`] = `
<default
explorer={
{
"databaseAccount": {
"id": "partial-account",
},
}
}
>
<default />
</default>
`;
exports[`CreateCopyJobScreensProvider should render with explorer prop 1`] = `
<default
explorer={
{
"databaseAccount": {
"id": "test-account",
"kind": "GlobalDocumentDB",
"location": "East US",
"name": "test-account-name",
"properties": {
"cassandraEndpoint": "https://test-account.cassandra.cosmosdb.azure.com:443/",
"documentEndpoint": "https://test-account.documents.azure.com:443/",
"gremlinEndpoint": "https://test-account.gremlin.cosmosdb.azure.com:443/",
"tableEndpoint": "https://test-account.table.cosmosdb.azure.com:443/",
},
"type": "DocumentDB",
},
"resourceGroup": "test-resource-group",
"subscriptionId": "test-subscription-id",
}
}
>
<default />
</default>
`;
exports[`CreateCopyJobScreensProvider should render with null explorer 1`] = `
<default
explorer={null}
>
<default />
</default>
`;
exports[`CreateCopyJobScreensProvider should render with undefined explorer 1`] = `
<default>
<default />
</default>
`;

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 = {