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,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 = [