mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-18 00:11:56 +00:00
Added comprehensive unit test coverage for Container Copy jobs (#2275)
* copy job uts * unit test coverage * lint fix * normalize account dropdown id
This commit is contained in:
@@ -32,6 +32,12 @@ module.exports = {
|
||||
extends: ["plugin:jest/recommended"],
|
||||
plugins: ["jest"],
|
||||
},
|
||||
{
|
||||
files: ["src/Explorer/ContainerCopy/**/*.{test,spec}.{ts,tsx}"],
|
||||
rules: {
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
},
|
||||
},
|
||||
],
|
||||
rules: {
|
||||
"no-console": ["error", { allow: ["error", "warn", "dir"] }],
|
||||
|
||||
52
src/Common/LoadingOverlay.test.tsx
Normal file
52
src/Common/LoadingOverlay.test.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { render } from "@testing-library/react";
|
||||
import React from "react";
|
||||
import LoadingOverlay from "./LoadingOverlay";
|
||||
|
||||
describe("LoadingOverlay", () => {
|
||||
const defaultProps = {
|
||||
isLoading: true,
|
||||
label: "Loading...",
|
||||
};
|
||||
|
||||
it("should render loading overlay when isLoading is true", () => {
|
||||
const { container } = render(<LoadingOverlay {...defaultProps} />);
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should render loading overlay with custom label", () => {
|
||||
const customProps = {
|
||||
isLoading: true,
|
||||
label: "Processing your request...",
|
||||
};
|
||||
const { container } = render(<LoadingOverlay {...customProps} />);
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should render loading overlay with empty label", () => {
|
||||
const emptyLabelProps = {
|
||||
isLoading: true,
|
||||
label: "",
|
||||
};
|
||||
const { container } = render(<LoadingOverlay {...emptyLabelProps} />);
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should return null when isLoading is false", () => {
|
||||
const notLoadingProps = {
|
||||
isLoading: false,
|
||||
label: "Loading...",
|
||||
};
|
||||
const { container } = render(<LoadingOverlay {...notLoadingProps} />);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it("should handle long labels properly", () => {
|
||||
const longLabelProps = {
|
||||
isLoading: true,
|
||||
label:
|
||||
"This is a very long loading message that might span multiple lines and should still render correctly in the loading overlay component",
|
||||
};
|
||||
const { container } = render(<LoadingOverlay {...longLabelProps} />);
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
73
src/Common/__snapshots__/LoadingOverlay.test.tsx.snap
Normal file
73
src/Common/__snapshots__/LoadingOverlay.test.tsx.snap
Normal file
@@ -0,0 +1,73 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`LoadingOverlay should handle long labels properly 1`] = `
|
||||
<div
|
||||
class="ms-Overlay root-109"
|
||||
>
|
||||
<div
|
||||
class="ms-Spinner root-111"
|
||||
>
|
||||
<div
|
||||
class="ms-Spinner-circle ms-Spinner--large circle-112"
|
||||
/>
|
||||
<div
|
||||
class="ms-Spinner-label label-113"
|
||||
>
|
||||
This is a very long loading message that might span multiple lines and should still render correctly in the loading overlay component
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`LoadingOverlay should render loading overlay when isLoading is true 1`] = `
|
||||
<div
|
||||
class="ms-Overlay root-109"
|
||||
>
|
||||
<div
|
||||
class="ms-Spinner root-111"
|
||||
>
|
||||
<div
|
||||
class="ms-Spinner-circle ms-Spinner--large circle-112"
|
||||
/>
|
||||
<div
|
||||
class="ms-Spinner-label label-113"
|
||||
>
|
||||
Loading...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`LoadingOverlay should render loading overlay with custom label 1`] = `
|
||||
<div
|
||||
class="ms-Overlay root-109"
|
||||
>
|
||||
<div
|
||||
class="ms-Spinner root-111"
|
||||
>
|
||||
<div
|
||||
class="ms-Spinner-circle ms-Spinner--large circle-112"
|
||||
/>
|
||||
<div
|
||||
class="ms-Spinner-label label-113"
|
||||
>
|
||||
Processing your request...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`LoadingOverlay should render loading overlay with empty label 1`] = `
|
||||
<div
|
||||
class="ms-Overlay root-109"
|
||||
>
|
||||
<div
|
||||
class="ms-Spinner root-111"
|
||||
>
|
||||
<div
|
||||
class="ms-Spinner-circle ms-Spinner--large circle-112"
|
||||
/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
729
src/Explorer/ContainerCopy/Actions/CopyJobActions.test.tsx
Normal file
729
src/Explorer/ContainerCopy/Actions/CopyJobActions.test.tsx
Normal file
@@ -0,0 +1,729 @@
|
||||
import "@testing-library/jest-dom";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import * as Logger from "../../../Common/Logger";
|
||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||
import * as dataTransferService from "../../../Utils/arm/generatedClients/dataTransferService/dataTransferJobs";
|
||||
import * as CopyJobUtils from "../CopyJobUtils";
|
||||
import CreateCopyJobScreensProvider from "../CreateCopyJob/Screens/CreateCopyJobScreensProvider";
|
||||
import { CopyJobActions, CopyJobStatusType } from "../Enums/CopyJobEnums";
|
||||
import CopyJobDetails from "../MonitorCopyJobs/Components/CopyJobDetails";
|
||||
import { MonitorCopyJobsRefState } from "../MonitorCopyJobs/MonitorCopyJobRefState";
|
||||
import { CopyJobContextState, CopyJobType } from "../Types/CopyJobTypes";
|
||||
import {
|
||||
getCopyJobs,
|
||||
openCopyJobDetailsPanel,
|
||||
openCreateCopyJobPanel,
|
||||
submitCreateCopyJob,
|
||||
updateCopyJobStatus,
|
||||
} from "./CopyJobActions";
|
||||
|
||||
jest.mock("UserContext", () => ({
|
||||
userContext: {
|
||||
databaseAccount: {
|
||||
id: "/subscriptions/sub-123/resourceGroups/rg-test/providers/Microsoft.DocumentDB/databaseAccounts/test-account",
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../../../hooks/useSidePanel");
|
||||
jest.mock("../../../Common/Logger");
|
||||
jest.mock("../../../Utils/arm/generatedClients/dataTransferService/dataTransferJobs");
|
||||
jest.mock("../MonitorCopyJobs/MonitorCopyJobRefState");
|
||||
jest.mock("../CopyJobUtils");
|
||||
|
||||
describe("CopyJobActions", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("openCreateCopyJobPanel", () => {
|
||||
it("should open side panel with correct parameters", () => {
|
||||
const mockExplorer = {} as Explorer;
|
||||
const mockSetPanelHasConsole = jest.fn();
|
||||
const mockOpenSidePanel = jest.fn();
|
||||
|
||||
(useSidePanel.getState as jest.Mock).mockReturnValue({
|
||||
setPanelHasConsole: mockSetPanelHasConsole,
|
||||
openSidePanel: mockOpenSidePanel,
|
||||
});
|
||||
|
||||
openCreateCopyJobPanel(mockExplorer);
|
||||
|
||||
expect(mockSetPanelHasConsole).toHaveBeenCalledWith(false);
|
||||
expect(mockOpenSidePanel).toHaveBeenCalledWith(expect.any(String), expect.any(Object), "650px");
|
||||
});
|
||||
|
||||
it("should render CreateCopyJobScreensProvider in side panel", () => {
|
||||
const mockExplorer = {} as Explorer;
|
||||
const mockOpenSidePanel = jest.fn();
|
||||
|
||||
(useSidePanel.getState as jest.Mock).mockReturnValue({
|
||||
setPanelHasConsole: jest.fn(),
|
||||
openSidePanel: mockOpenSidePanel,
|
||||
});
|
||||
|
||||
openCreateCopyJobPanel(mockExplorer);
|
||||
|
||||
const sidePanelContent = mockOpenSidePanel.mock.calls[0][1];
|
||||
expect(sidePanelContent.type).toBe(CreateCopyJobScreensProvider);
|
||||
expect(sidePanelContent.props.explorer).toBe(mockExplorer);
|
||||
});
|
||||
});
|
||||
|
||||
describe("openCopyJobDetailsPanel", () => {
|
||||
it("should open side panel with job details", () => {
|
||||
const mockJob: CopyJobType = {
|
||||
ID: "1",
|
||||
Mode: "online",
|
||||
Name: "test-job",
|
||||
Status: CopyJobStatusType.InProgress,
|
||||
CompletionPercentage: 50,
|
||||
Duration: "01 hours, 30 minutes, 45 seconds",
|
||||
LastUpdatedTime: "1/1/2025, 10:00:00 AM",
|
||||
timestamp: 1704106800000,
|
||||
Source: {
|
||||
component: "CosmosDBSql",
|
||||
databaseName: "source-db",
|
||||
containerName: "source-container",
|
||||
},
|
||||
Destination: {
|
||||
component: "CosmosDBSql",
|
||||
databaseName: "target-db",
|
||||
containerName: "target-container",
|
||||
},
|
||||
};
|
||||
|
||||
const mockSetPanelHasConsole = jest.fn();
|
||||
const mockOpenSidePanel = jest.fn();
|
||||
|
||||
(useSidePanel.getState as jest.Mock).mockReturnValue({
|
||||
setPanelHasConsole: mockSetPanelHasConsole,
|
||||
openSidePanel: mockOpenSidePanel,
|
||||
});
|
||||
|
||||
openCopyJobDetailsPanel(mockJob);
|
||||
|
||||
expect(mockSetPanelHasConsole).toHaveBeenCalledWith(false);
|
||||
expect(mockOpenSidePanel).toHaveBeenCalledWith(expect.stringContaining("test-job"), expect.any(Object), "650px");
|
||||
});
|
||||
|
||||
it("should render CopyJobDetails component with correct job", () => {
|
||||
const mockJob: CopyJobType = {
|
||||
ID: "1",
|
||||
Mode: "offline",
|
||||
Name: "test-job-2",
|
||||
Status: CopyJobStatusType.Completed,
|
||||
CompletionPercentage: 100,
|
||||
Duration: "02 hours, 15 minutes, 30 seconds",
|
||||
LastUpdatedTime: "1/2/2025, 11:00:00 AM",
|
||||
timestamp: 1704193200000,
|
||||
Source: {
|
||||
component: "CosmosDBSql",
|
||||
databaseName: "source-db",
|
||||
containerName: "source-container",
|
||||
},
|
||||
Destination: {
|
||||
component: "CosmosDBSql",
|
||||
databaseName: "target-db",
|
||||
containerName: "target-container",
|
||||
},
|
||||
};
|
||||
|
||||
const mockOpenSidePanel = jest.fn();
|
||||
|
||||
(useSidePanel.getState as jest.Mock).mockReturnValue({
|
||||
setPanelHasConsole: jest.fn(),
|
||||
openSidePanel: mockOpenSidePanel,
|
||||
});
|
||||
|
||||
openCopyJobDetailsPanel(mockJob);
|
||||
|
||||
const sidePanelContent = mockOpenSidePanel.mock.calls[0][1];
|
||||
expect(sidePanelContent.type).toBe(CopyJobDetails);
|
||||
expect(sidePanelContent.props.job).toBe(mockJob);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getCopyJobs", () => {
|
||||
beforeEach(() => {
|
||||
(CopyJobUtils.getAccountDetailsFromResourceId as jest.Mock).mockReturnValue({
|
||||
subscriptionId: "sub-123",
|
||||
resourceGroup: "rg-test",
|
||||
accountName: "test-account",
|
||||
});
|
||||
});
|
||||
|
||||
it("should fetch and format copy jobs successfully", async () => {
|
||||
const mockResponse = {
|
||||
value: [
|
||||
{
|
||||
properties: {
|
||||
jobName: "job-1",
|
||||
status: "InProgress",
|
||||
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
|
||||
processedCount: 50,
|
||||
totalCount: 100,
|
||||
mode: "online",
|
||||
duration: "01:30:45",
|
||||
source: {
|
||||
component: "CosmosDBSql",
|
||||
databaseName: "source-db",
|
||||
containerName: "source-container",
|
||||
},
|
||||
destination: {
|
||||
component: "CosmosDBSql",
|
||||
databaseName: "target-db",
|
||||
containerName: "target-container",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
(dataTransferService.listByDatabaseAccount as jest.Mock).mockResolvedValue(mockResponse);
|
||||
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
|
||||
formattedDateTime: "1/1/2025, 10:00:00 AM",
|
||||
timestamp: 1704106800000,
|
||||
});
|
||||
(CopyJobUtils.convertTime as jest.Mock).mockReturnValue("01 hours, 30 minutes, 45 seconds");
|
||||
(CopyJobUtils.convertToCamelCase as jest.Mock).mockReturnValue("InProgress");
|
||||
|
||||
const result = await getCopyJobs();
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toMatchObject({
|
||||
ID: "1",
|
||||
Name: "job-1",
|
||||
Status: "InProgress",
|
||||
CompletionPercentage: 50,
|
||||
Mode: "online",
|
||||
});
|
||||
});
|
||||
|
||||
it("should filter jobs by CosmosDBSql component", async () => {
|
||||
const mockResponse = {
|
||||
value: [
|
||||
{
|
||||
properties: {
|
||||
jobName: "sql-job",
|
||||
status: "Completed",
|
||||
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
|
||||
processedCount: 100,
|
||||
totalCount: 100,
|
||||
mode: "offline",
|
||||
duration: "02:00:00",
|
||||
source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" },
|
||||
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
|
||||
},
|
||||
},
|
||||
{
|
||||
properties: {
|
||||
jobName: "other-job",
|
||||
status: "Completed",
|
||||
lastUpdatedUtcTime: "2025-01-01T11:00:00Z",
|
||||
processedCount: 100,
|
||||
totalCount: 100,
|
||||
mode: "offline",
|
||||
duration: "01:00:00",
|
||||
source: { component: "OtherComponent", databaseName: "db1", containerName: "c1" },
|
||||
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
(dataTransferService.listByDatabaseAccount as jest.Mock).mockResolvedValue(mockResponse);
|
||||
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
|
||||
formattedDateTime: "1/1/2025, 10:00:00 AM",
|
||||
timestamp: 1704106800000,
|
||||
});
|
||||
(CopyJobUtils.convertTime as jest.Mock).mockReturnValue("02 hours");
|
||||
(CopyJobUtils.convertToCamelCase as jest.Mock).mockReturnValue("Completed");
|
||||
|
||||
const result = await getCopyJobs();
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].Name).toBe("sql-job");
|
||||
});
|
||||
|
||||
it("should sort jobs by last updated time (newest first)", async () => {
|
||||
const mockResponse = {
|
||||
value: [
|
||||
{
|
||||
properties: {
|
||||
jobName: "older-job",
|
||||
status: "Completed",
|
||||
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
|
||||
processedCount: 100,
|
||||
totalCount: 100,
|
||||
mode: "offline",
|
||||
duration: "01:00:00",
|
||||
source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" },
|
||||
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
|
||||
},
|
||||
},
|
||||
{
|
||||
properties: {
|
||||
jobName: "newer-job",
|
||||
status: "InProgress",
|
||||
lastUpdatedUtcTime: "2025-01-02T10:00:00Z",
|
||||
processedCount: 50,
|
||||
totalCount: 100,
|
||||
mode: "online",
|
||||
duration: "00:30:00",
|
||||
source: { component: "CosmosDBSql", databaseName: "db3", containerName: "c3" },
|
||||
destination: { component: "CosmosDBSql", databaseName: "db4", containerName: "c4" },
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
(dataTransferService.listByDatabaseAccount as jest.Mock).mockResolvedValue(mockResponse);
|
||||
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
|
||||
formattedDateTime: "1/1/2025, 10:00:00 AM",
|
||||
timestamp: 1704106800000,
|
||||
});
|
||||
(CopyJobUtils.convertTime as jest.Mock).mockReturnValue("01 hours");
|
||||
(CopyJobUtils.convertToCamelCase as jest.Mock).mockReturnValue("Completed");
|
||||
|
||||
const result = await getCopyJobs();
|
||||
|
||||
expect(result[0].Name).toBe("newer-job");
|
||||
expect(result[1].Name).toBe("older-job");
|
||||
});
|
||||
|
||||
it("should calculate completion percentage correctly", async () => {
|
||||
const mockResponse = {
|
||||
value: [
|
||||
{
|
||||
properties: {
|
||||
jobName: "job-1",
|
||||
status: "InProgress",
|
||||
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
|
||||
processedCount: 75,
|
||||
totalCount: 100,
|
||||
mode: "online",
|
||||
duration: "01:00:00",
|
||||
source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" },
|
||||
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
(dataTransferService.listByDatabaseAccount as jest.Mock).mockResolvedValue(mockResponse);
|
||||
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
|
||||
formattedDateTime: "1/1/2025, 10:00:00 AM",
|
||||
timestamp: 1704106800000,
|
||||
});
|
||||
(CopyJobUtils.convertTime as jest.Mock).mockReturnValue("01 hours");
|
||||
(CopyJobUtils.convertToCamelCase as jest.Mock).mockReturnValue("InProgress");
|
||||
|
||||
const result = await getCopyJobs();
|
||||
|
||||
expect(result[0].CompletionPercentage).toBe(75);
|
||||
});
|
||||
|
||||
it("should handle zero total count gracefully", async () => {
|
||||
const mockResponse = {
|
||||
value: [
|
||||
{
|
||||
properties: {
|
||||
jobName: "job-1",
|
||||
status: "Pending",
|
||||
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
|
||||
processedCount: 0,
|
||||
totalCount: 0,
|
||||
mode: "online",
|
||||
duration: "00:00:00",
|
||||
source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" },
|
||||
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
(dataTransferService.listByDatabaseAccount as jest.Mock).mockResolvedValue(mockResponse);
|
||||
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
|
||||
formattedDateTime: "1/1/2025, 10:00:00 AM",
|
||||
timestamp: 1704106800000,
|
||||
});
|
||||
(CopyJobUtils.convertTime as jest.Mock).mockReturnValue("0 seconds");
|
||||
(CopyJobUtils.convertToCamelCase as jest.Mock).mockReturnValue("Pending");
|
||||
|
||||
const result = await getCopyJobs();
|
||||
|
||||
expect(result[0].CompletionPercentage).toBe(0);
|
||||
});
|
||||
|
||||
it("should extract error messages if present", async () => {
|
||||
const mockError = {
|
||||
message: "Error message line 1\r\n\r\nError message line 2",
|
||||
code: "ErrorCode123",
|
||||
};
|
||||
const mockResponse = {
|
||||
value: [
|
||||
{
|
||||
properties: {
|
||||
jobName: "failed-job",
|
||||
status: "Failed",
|
||||
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
|
||||
processedCount: 50,
|
||||
totalCount: 100,
|
||||
mode: "offline",
|
||||
duration: "00:30:00",
|
||||
source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" },
|
||||
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
|
||||
error: mockError,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
(dataTransferService.listByDatabaseAccount as jest.Mock).mockResolvedValue(mockResponse);
|
||||
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
|
||||
formattedDateTime: "1/1/2025, 10:00:00 AM",
|
||||
timestamp: 1704106800000,
|
||||
});
|
||||
(CopyJobUtils.convertTime as jest.Mock).mockReturnValue("30 minutes");
|
||||
(CopyJobUtils.convertToCamelCase as jest.Mock).mockReturnValue("Failed");
|
||||
(CopyJobUtils.extractErrorMessage as jest.Mock).mockReturnValue({
|
||||
message: "Error message line 1",
|
||||
code: "ErrorCode123",
|
||||
});
|
||||
|
||||
const result = await getCopyJobs();
|
||||
|
||||
expect(result[0].Error).toEqual({
|
||||
message: "Error message line 1",
|
||||
code: "ErrorCode123",
|
||||
});
|
||||
expect(CopyJobUtils.extractErrorMessage).toHaveBeenCalledWith(mockError);
|
||||
});
|
||||
|
||||
it("should abort previous request when new request is made", async () => {
|
||||
const mockAbortController = {
|
||||
abort: jest.fn(),
|
||||
signal: {} as AbortSignal,
|
||||
};
|
||||
(global as any).AbortController = jest.fn(() => mockAbortController);
|
||||
|
||||
(dataTransferService.listByDatabaseAccount as jest.Mock).mockResolvedValue({ value: [] });
|
||||
|
||||
getCopyJobs();
|
||||
expect(mockAbortController.abort).not.toHaveBeenCalled();
|
||||
|
||||
getCopyJobs();
|
||||
expect(mockAbortController.abort).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should throw error for invalid response format", async () => {
|
||||
(dataTransferService.listByDatabaseAccount as jest.Mock).mockResolvedValue({
|
||||
value: "not-an-array",
|
||||
});
|
||||
|
||||
await expect(getCopyJobs()).rejects.toThrow("Invalid migration job status response: Expected an array of jobs.");
|
||||
});
|
||||
|
||||
it("should handle abort signal error", async () => {
|
||||
const abortError = {
|
||||
message: "Aborted",
|
||||
content: JSON.stringify({ message: "signal is aborted without reason" }),
|
||||
};
|
||||
(dataTransferService.listByDatabaseAccount as jest.Mock).mockRejectedValue(abortError);
|
||||
|
||||
await expect(getCopyJobs()).rejects.toMatchObject({
|
||||
message: expect.stringContaining("Please wait for the current fetch request to complete"),
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle generic errors", async () => {
|
||||
const genericError = new Error("Network error");
|
||||
(dataTransferService.listByDatabaseAccount as jest.Mock).mockRejectedValue(genericError);
|
||||
|
||||
await expect(getCopyJobs()).rejects.toThrow("Network error");
|
||||
});
|
||||
});
|
||||
|
||||
describe("submitCreateCopyJob", () => {
|
||||
let mockRefreshJobList: jest.Mock;
|
||||
let mockOnSuccess: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
mockRefreshJobList = jest.fn();
|
||||
mockOnSuccess = jest.fn();
|
||||
|
||||
(CopyJobUtils.getAccountDetailsFromResourceId as jest.Mock).mockReturnValue({
|
||||
subscriptionId: "sub-123",
|
||||
resourceGroup: "rg-test",
|
||||
accountName: "test-account",
|
||||
});
|
||||
|
||||
(MonitorCopyJobsRefState.getState as jest.Mock).mockReturnValue({
|
||||
ref: { refreshJobList: mockRefreshJobList },
|
||||
});
|
||||
});
|
||||
|
||||
it("should create intra-account copy job successfully", async () => {
|
||||
const mockState: CopyJobContextState = {
|
||||
jobName: "test-job",
|
||||
migrationType: "online" as any,
|
||||
source: {
|
||||
subscription: {} as any,
|
||||
account: { id: "account-1", name: "source-account" } as any,
|
||||
databaseId: "source-db",
|
||||
containerId: "source-container",
|
||||
},
|
||||
target: {
|
||||
subscriptionId: "sub-123",
|
||||
account: { id: "account-1", name: "target-account" } as any,
|
||||
databaseId: "target-db",
|
||||
containerId: "target-container",
|
||||
},
|
||||
};
|
||||
|
||||
(CopyJobUtils.isIntraAccountCopy as jest.Mock).mockReturnValue(true);
|
||||
(dataTransferService.create as jest.Mock).mockResolvedValue({ id: "job-id" });
|
||||
|
||||
await submitCreateCopyJob(mockState, mockOnSuccess);
|
||||
|
||||
expect(dataTransferService.create).toHaveBeenCalledWith(
|
||||
"sub-123",
|
||||
"rg-test",
|
||||
"test-account",
|
||||
"test-job",
|
||||
expect.objectContaining({
|
||||
properties: expect.objectContaining({
|
||||
source: expect.objectContaining({
|
||||
component: "CosmosDBSql",
|
||||
databaseName: "source-db",
|
||||
containerName: "source-container",
|
||||
}),
|
||||
destination: expect.objectContaining({
|
||||
component: "CosmosDBSql",
|
||||
databaseName: "target-db",
|
||||
containerName: "target-container",
|
||||
}),
|
||||
mode: "online",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
const callArgs = (dataTransferService.create as jest.Mock).mock.calls[0][4];
|
||||
expect(callArgs.properties.source.remoteAccountName).toBeUndefined();
|
||||
|
||||
expect(mockRefreshJobList).toHaveBeenCalled();
|
||||
expect(mockOnSuccess).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should create inter-account copy job with source account name", async () => {
|
||||
const mockState: CopyJobContextState = {
|
||||
jobName: "cross-account-job",
|
||||
migrationType: "offline" as any,
|
||||
source: {
|
||||
subscription: {} as any,
|
||||
account: { id: "account-1", name: "source-account" } as any,
|
||||
databaseId: "source-db",
|
||||
containerId: "source-container",
|
||||
},
|
||||
target: {
|
||||
subscriptionId: "sub-456",
|
||||
account: { id: "account-2", name: "target-account" } as any,
|
||||
databaseId: "target-db",
|
||||
containerId: "target-container",
|
||||
},
|
||||
};
|
||||
|
||||
(CopyJobUtils.isIntraAccountCopy as jest.Mock).mockReturnValue(false);
|
||||
(dataTransferService.create as jest.Mock).mockResolvedValue({ id: "job-id" });
|
||||
|
||||
await submitCreateCopyJob(mockState, mockOnSuccess);
|
||||
|
||||
const callArgs = (dataTransferService.create as jest.Mock).mock.calls[0][4];
|
||||
expect(callArgs.properties.source.remoteAccountName).toBe("source-account");
|
||||
expect(mockOnSuccess).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should handle errors and log them", async () => {
|
||||
const mockState: CopyJobContextState = {
|
||||
jobName: "failing-job",
|
||||
migrationType: "online" as any,
|
||||
source: {
|
||||
subscription: {} as any,
|
||||
account: { id: "account-1", name: "source-account" } as any,
|
||||
databaseId: "source-db",
|
||||
containerId: "source-container",
|
||||
},
|
||||
target: {
|
||||
subscriptionId: "sub-123",
|
||||
account: { id: "account-1", name: "target-account" } as any,
|
||||
databaseId: "target-db",
|
||||
containerId: "target-container",
|
||||
},
|
||||
};
|
||||
|
||||
const mockError = new Error("API Error");
|
||||
(CopyJobUtils.isIntraAccountCopy as jest.Mock).mockReturnValue(true);
|
||||
(dataTransferService.create as jest.Mock).mockRejectedValue(mockError);
|
||||
|
||||
await expect(submitCreateCopyJob(mockState, mockOnSuccess)).rejects.toThrow("API Error");
|
||||
|
||||
expect(Logger.logError).toHaveBeenCalledWith("API Error", "CopyJob/CopyJobActions.submitCreateCopyJob");
|
||||
expect(mockOnSuccess).not.toHaveBeenCalled();
|
||||
expect(mockRefreshJobList).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should handle errors without message", async () => {
|
||||
const mockState: CopyJobContextState = {
|
||||
jobName: "test-job",
|
||||
migrationType: "online" as any,
|
||||
source: {
|
||||
subscription: {} as any,
|
||||
account: { id: "account-1", name: "source-account" } as any,
|
||||
databaseId: "source-db",
|
||||
containerId: "source-container",
|
||||
},
|
||||
target: {
|
||||
subscriptionId: "sub-123",
|
||||
account: { id: "account-1", name: "target-account" } as any,
|
||||
databaseId: "target-db",
|
||||
containerId: "target-container",
|
||||
},
|
||||
};
|
||||
|
||||
(CopyJobUtils.isIntraAccountCopy as jest.Mock).mockReturnValue(true);
|
||||
(dataTransferService.create as jest.Mock).mockRejectedValue({});
|
||||
|
||||
await expect(submitCreateCopyJob(mockState, mockOnSuccess)).rejects.toEqual({});
|
||||
|
||||
expect(Logger.logError).toHaveBeenCalledWith(
|
||||
"Error submitting create copy job. Please try again later.",
|
||||
"CopyJob/CopyJobActions.submitCreateCopyJob",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateCopyJobStatus", () => {
|
||||
const mockJob: CopyJobType = {
|
||||
ID: "1",
|
||||
Mode: "online",
|
||||
Name: "test-job",
|
||||
Status: CopyJobStatusType.InProgress,
|
||||
CompletionPercentage: 50,
|
||||
Duration: "01 hours, 30 minutes",
|
||||
LastUpdatedTime: "1/1/2025, 10:00:00 AM",
|
||||
timestamp: 1704106800000,
|
||||
Source: {
|
||||
component: "CosmosDBSql",
|
||||
databaseName: "source-db",
|
||||
containerName: "source-container",
|
||||
},
|
||||
Destination: {
|
||||
component: "CosmosDBSql",
|
||||
databaseName: "target-db",
|
||||
containerName: "target-container",
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
(CopyJobUtils.getAccountDetailsFromResourceId as jest.Mock).mockReturnValue({
|
||||
subscriptionId: "sub-123",
|
||||
resourceGroup: "rg-test",
|
||||
accountName: "test-account",
|
||||
});
|
||||
});
|
||||
|
||||
it("should pause a job successfully", async () => {
|
||||
const mockResponse = { id: "job-id", properties: { status: "Paused" } };
|
||||
(dataTransferService.pause as jest.Mock).mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await updateCopyJobStatus(mockJob, CopyJobActions.pause);
|
||||
|
||||
expect(dataTransferService.pause).toHaveBeenCalledWith("sub-123", "rg-test", "test-account", "test-job");
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
it("should resume a job successfully", async () => {
|
||||
const mockResponse = { id: "job-id", properties: { status: "InProgress" } };
|
||||
(dataTransferService.resume as jest.Mock).mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await updateCopyJobStatus(mockJob, CopyJobActions.resume);
|
||||
|
||||
expect(dataTransferService.resume).toHaveBeenCalledWith("sub-123", "rg-test", "test-account", "test-job");
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
it("should cancel a job successfully", async () => {
|
||||
const mockResponse = { id: "job-id", properties: { status: "Cancelled" } };
|
||||
(dataTransferService.cancel as jest.Mock).mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await updateCopyJobStatus(mockJob, CopyJobActions.cancel);
|
||||
|
||||
expect(dataTransferService.cancel).toHaveBeenCalledWith("sub-123", "rg-test", "test-account", "test-job");
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
it("should complete a job successfully", async () => {
|
||||
const mockResponse = { id: "job-id", properties: { status: "Completed" } };
|
||||
(dataTransferService.complete as jest.Mock).mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await updateCopyJobStatus(mockJob, CopyJobActions.complete);
|
||||
|
||||
expect(dataTransferService.complete).toHaveBeenCalledWith("sub-123", "rg-test", "test-account", "test-job");
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
it("should handle case-insensitive action names", async () => {
|
||||
const mockResponse = { id: "job-id", properties: { status: "Paused" } };
|
||||
(dataTransferService.pause as jest.Mock).mockResolvedValue(mockResponse);
|
||||
|
||||
await updateCopyJobStatus(mockJob, "PAUSE");
|
||||
|
||||
expect(dataTransferService.pause).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should throw error for unsupported action", async () => {
|
||||
await expect(updateCopyJobStatus(mockJob, "invalid-action")).rejects.toThrow(
|
||||
"Unsupported action: invalid-action",
|
||||
);
|
||||
|
||||
expect(Logger.logError).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should normalize error messages with status types", async () => {
|
||||
const mockError = {
|
||||
message: "Job must be in 'Running' or 'InProgress' state",
|
||||
content: { error: "State error" },
|
||||
};
|
||||
(dataTransferService.pause as jest.Mock).mockRejectedValue(mockError);
|
||||
|
||||
await expect(updateCopyJobStatus(mockJob, CopyJobActions.pause)).rejects.toEqual(mockError);
|
||||
|
||||
const loggedMessage = (Logger.logError as jest.Mock).mock.calls[0][0];
|
||||
expect(loggedMessage).toContain("Error updating copy job status");
|
||||
});
|
||||
|
||||
it("should log error with correct context", async () => {
|
||||
const mockError = new Error("Network failure");
|
||||
(dataTransferService.resume as jest.Mock).mockRejectedValue(mockError);
|
||||
|
||||
await expect(updateCopyJobStatus(mockJob, CopyJobActions.resume)).rejects.toThrow("Network failure");
|
||||
|
||||
expect(Logger.logError).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Error updating copy job status"),
|
||||
"CopyJob/CopyJobActions.updateCopyJobStatus",
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle errors with content property", async () => {
|
||||
const mockError = {
|
||||
content: { message: "Content error message" },
|
||||
};
|
||||
(dataTransferService.cancel as jest.Mock).mockRejectedValue(mockError);
|
||||
|
||||
await expect(updateCopyJobStatus(mockJob, CopyJobActions.cancel)).rejects.toEqual(mockError);
|
||||
|
||||
expect(Logger.logError).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
185
src/Explorer/ContainerCopy/CommandBar/CopyJobCommandBar.test.tsx
Normal file
185
src/Explorer/ContainerCopy/CommandBar/CopyJobCommandBar.test.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
import "@testing-library/jest-dom";
|
||||
import { render } from "@testing-library/react";
|
||||
import React from "react";
|
||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
import Explorer from "../../Explorer";
|
||||
import * as CommandBarUtil from "../../Menus/CommandBar/CommandBarUtil";
|
||||
import CopyJobCommandBar from "./CopyJobCommandBar";
|
||||
import * as Utils from "./Utils";
|
||||
|
||||
jest.mock("../MonitorCopyJobs/MonitorCopyJobRefState");
|
||||
jest.mock("../../Menus/CommandBar/CommandBarUtil");
|
||||
jest.mock("./Utils");
|
||||
|
||||
describe("CopyJobCommandBar", () => {
|
||||
let mockExplorer: Explorer;
|
||||
let mockConvertButton: jest.MockedFunction<typeof CommandBarUtil.convertButton>;
|
||||
let mockGetCommandBarButtons: jest.MockedFunction<typeof Utils.getCommandBarButtons>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockExplorer = {} as Explorer;
|
||||
|
||||
mockConvertButton = CommandBarUtil.convertButton as jest.MockedFunction<typeof CommandBarUtil.convertButton>;
|
||||
mockGetCommandBarButtons = Utils.getCommandBarButtons as jest.MockedFunction<typeof Utils.getCommandBarButtons>;
|
||||
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should render without crashing", () => {
|
||||
mockGetCommandBarButtons.mockReturnValue([]);
|
||||
mockConvertButton.mockReturnValue([]);
|
||||
|
||||
const { container } = render(<CopyJobCommandBar explorer={mockExplorer} />);
|
||||
expect(container.querySelector(".commandBarContainer")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call getCommandBarButtons with explorer", () => {
|
||||
mockGetCommandBarButtons.mockReturnValue([]);
|
||||
mockConvertButton.mockReturnValue([]);
|
||||
|
||||
render(<CopyJobCommandBar explorer={mockExplorer} />);
|
||||
|
||||
expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer);
|
||||
expect(mockGetCommandBarButtons).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should call convertButton with command bar items and background color", () => {
|
||||
const mockCommandButtonProps: CommandButtonComponentProps[] = [
|
||||
{
|
||||
iconSrc: "icon.svg",
|
||||
iconAlt: "Test Icon",
|
||||
onCommandClick: jest.fn(),
|
||||
commandButtonLabel: "Test Button",
|
||||
ariaLabel: "Test Button Aria Label",
|
||||
tooltipText: "Test Tooltip",
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
},
|
||||
];
|
||||
mockGetCommandBarButtons.mockReturnValue(mockCommandButtonProps);
|
||||
mockConvertButton.mockReturnValue([]);
|
||||
|
||||
render(<CopyJobCommandBar explorer={mockExplorer} />);
|
||||
|
||||
expect(mockConvertButton).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should render FluentCommandBar with correct aria label", () => {
|
||||
mockGetCommandBarButtons.mockReturnValue([]);
|
||||
mockConvertButton.mockReturnValue([]);
|
||||
|
||||
const { getByRole } = render(<CopyJobCommandBar explorer={mockExplorer} />);
|
||||
|
||||
const commandBar = getByRole("menubar", { hidden: true });
|
||||
expect(commandBar).toHaveAttribute("aria-label", "Use left and right arrow keys to navigate between commands");
|
||||
});
|
||||
|
||||
it("should render FluentCommandBar with converted items", () => {
|
||||
const mockCommandButtonProps: CommandButtonComponentProps[] = [
|
||||
{
|
||||
iconSrc: "icon1.svg",
|
||||
iconAlt: "Test Icon 1",
|
||||
onCommandClick: jest.fn(),
|
||||
commandButtonLabel: "Test Button 1",
|
||||
ariaLabel: "Test Button 1 Aria Label",
|
||||
tooltipText: "Test Tooltip 1",
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
iconSrc: "icon2.svg",
|
||||
iconAlt: "Test Icon 2",
|
||||
onCommandClick: jest.fn(),
|
||||
commandButtonLabel: "Test Button 2",
|
||||
ariaLabel: "Test Button 2 Aria Label",
|
||||
tooltipText: "Test Tooltip 2",
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
},
|
||||
];
|
||||
|
||||
const mockFluentItems = [
|
||||
{
|
||||
key: "button1",
|
||||
text: "Test Button 1",
|
||||
iconProps: { iconName: "Add" },
|
||||
},
|
||||
{
|
||||
key: "button2",
|
||||
text: "Test Button 2",
|
||||
iconProps: { iconName: "Feedback" },
|
||||
},
|
||||
];
|
||||
|
||||
mockGetCommandBarButtons.mockReturnValue(mockCommandButtonProps);
|
||||
mockConvertButton.mockReturnValue(mockFluentItems);
|
||||
|
||||
const { container } = render(<CopyJobCommandBar explorer={mockExplorer} />);
|
||||
|
||||
expect(mockConvertButton).toHaveBeenCalledTimes(1);
|
||||
expect(container.querySelector(".commandBarContainer")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle multiple command bar buttons", () => {
|
||||
const mockCommandButtonProps: CommandButtonComponentProps[] = [
|
||||
{
|
||||
iconSrc: "create.svg",
|
||||
iconAlt: "Create",
|
||||
onCommandClick: jest.fn(),
|
||||
commandButtonLabel: "Create Copy Job",
|
||||
ariaLabel: "Create Copy Job",
|
||||
tooltipText: "Create Copy Job",
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
iconSrc: "refresh.svg",
|
||||
iconAlt: "Refresh",
|
||||
onCommandClick: jest.fn(),
|
||||
commandButtonLabel: "Refresh",
|
||||
ariaLabel: "Refresh",
|
||||
tooltipText: "Refresh",
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
iconSrc: "feedback.svg",
|
||||
iconAlt: "Feedback",
|
||||
onCommandClick: jest.fn(),
|
||||
commandButtonLabel: "Feedback",
|
||||
ariaLabel: "Feedback",
|
||||
tooltipText: "Feedback",
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
},
|
||||
];
|
||||
|
||||
mockGetCommandBarButtons.mockReturnValue(mockCommandButtonProps);
|
||||
mockConvertButton.mockReturnValue([
|
||||
{ key: "create", text: "Create Copy Job" },
|
||||
{ key: "refresh", text: "Refresh" },
|
||||
{ key: "feedback", text: "Feedback" },
|
||||
]);
|
||||
|
||||
render(<CopyJobCommandBar explorer={mockExplorer} />);
|
||||
|
||||
expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer);
|
||||
expect(mockConvertButton.mock.calls[0][0]).toEqual(mockCommandButtonProps);
|
||||
});
|
||||
|
||||
it("should re-render when explorer prop changes", () => {
|
||||
const mockExplorer1 = { id: "explorer1" } as unknown as Explorer;
|
||||
const mockExplorer2 = { id: "explorer2" } as unknown as Explorer;
|
||||
|
||||
mockGetCommandBarButtons.mockReturnValue([]);
|
||||
mockConvertButton.mockReturnValue([]);
|
||||
|
||||
const { rerender } = render(<CopyJobCommandBar explorer={mockExplorer1} />);
|
||||
expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer1);
|
||||
|
||||
rerender(<CopyJobCommandBar explorer={mockExplorer2} />);
|
||||
|
||||
expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer2);
|
||||
expect(mockGetCommandBarButtons).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
@@ -28,4 +28,6 @@ const CopyJobCommandBar: React.FC<ContainerCopyProps> = ({ explorer }) => {
|
||||
);
|
||||
};
|
||||
|
||||
CopyJobCommandBar.displayName = "CopyJobCommandBar";
|
||||
|
||||
export default CopyJobCommandBar;
|
||||
|
||||
268
src/Explorer/ContainerCopy/CommandBar/Utils.test.ts
Normal file
268
src/Explorer/ContainerCopy/CommandBar/Utils.test.ts
Normal file
@@ -0,0 +1,268 @@
|
||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
import Explorer from "../../Explorer";
|
||||
import * as Actions from "../Actions/CopyJobActions";
|
||||
import { MonitorCopyJobsRefState } from "../MonitorCopyJobs/MonitorCopyJobRefState";
|
||||
import { getCommandBarButtons } from "./Utils";
|
||||
|
||||
jest.mock("../../../ConfigContext", () => ({
|
||||
configContext: {
|
||||
platform: "Portal",
|
||||
},
|
||||
Platform: {
|
||||
Portal: "Portal",
|
||||
Emulator: "Emulator",
|
||||
Hosted: "Hosted",
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../Actions/CopyJobActions", () => ({
|
||||
openCreateCopyJobPanel: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../MonitorCopyJobs/MonitorCopyJobRefState", () => ({
|
||||
MonitorCopyJobsRefState: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("CommandBar Utils", () => {
|
||||
let mockExplorer: Explorer;
|
||||
let mockOpenContainerCopyFeedbackBlade: jest.Mock;
|
||||
let mockRefreshJobList: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
mockOpenContainerCopyFeedbackBlade = jest.fn();
|
||||
mockRefreshJobList = jest.fn();
|
||||
|
||||
mockExplorer = {
|
||||
openContainerCopyFeedbackBlade: mockOpenContainerCopyFeedbackBlade,
|
||||
} as unknown as Explorer;
|
||||
|
||||
(MonitorCopyJobsRefState as unknown as jest.Mock).mockImplementation((selector) => {
|
||||
const state = {
|
||||
ref: {
|
||||
refreshJobList: mockRefreshJobList,
|
||||
},
|
||||
};
|
||||
return selector(state);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getCommandBarButtons", () => {
|
||||
it("should return an array of command button props", () => {
|
||||
const buttons = getCommandBarButtons(mockExplorer);
|
||||
|
||||
expect(buttons).toBeDefined();
|
||||
expect(Array.isArray(buttons)).toBe(true);
|
||||
expect(buttons.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("should include create copy job button", () => {
|
||||
const buttons = getCommandBarButtons(mockExplorer);
|
||||
const createButton = buttons[0];
|
||||
|
||||
expect(createButton).toBeDefined();
|
||||
expect(createButton.commandButtonLabel).toBeUndefined();
|
||||
expect(createButton.ariaLabel).toBe("Create a new container copy job");
|
||||
expect(createButton.tooltipText).toBe("Create Copy Job");
|
||||
expect(createButton.hasPopup).toBe(false);
|
||||
expect(createButton.disabled).toBe(false);
|
||||
});
|
||||
|
||||
it("should include refresh button", () => {
|
||||
const buttons = getCommandBarButtons(mockExplorer);
|
||||
const refreshButton = buttons[1];
|
||||
|
||||
expect(refreshButton).toBeDefined();
|
||||
expect(refreshButton.ariaLabel).toBe("Refresh copy jobs");
|
||||
expect(refreshButton.tooltipText).toBe("Refresh");
|
||||
expect(refreshButton.disabled).toBe(false);
|
||||
});
|
||||
|
||||
it("should include feedback button when platform is Portal", () => {
|
||||
const buttons = getCommandBarButtons(mockExplorer);
|
||||
|
||||
expect(buttons.length).toBe(3);
|
||||
|
||||
const feedbackButton = buttons[2];
|
||||
expect(feedbackButton).toBeDefined();
|
||||
expect(feedbackButton.ariaLabel).toBe("Provide feedback on copy jobs");
|
||||
expect(feedbackButton.tooltipText).toBe("Feedback");
|
||||
expect(feedbackButton.disabled).toBe(false);
|
||||
});
|
||||
|
||||
it("should not include feedback button when platform is not Portal", async () => {
|
||||
jest.resetModules();
|
||||
jest.doMock("../../../ConfigContext", () => ({
|
||||
configContext: {
|
||||
platform: "Emulator",
|
||||
},
|
||||
Platform: {
|
||||
Portal: "Portal",
|
||||
Emulator: "Emulator",
|
||||
Hosted: "Hosted",
|
||||
},
|
||||
}));
|
||||
|
||||
const { getCommandBarButtons: getCommandBarButtonsEmulator } = await import("./Utils");
|
||||
const buttons = getCommandBarButtonsEmulator(mockExplorer);
|
||||
|
||||
expect(buttons.length).toBe(2);
|
||||
});
|
||||
|
||||
it("should call openCreateCopyJobPanel when create button is clicked", () => {
|
||||
const buttons = getCommandBarButtons(mockExplorer);
|
||||
const createButton = buttons[0];
|
||||
|
||||
createButton.onCommandClick({} as React.SyntheticEvent);
|
||||
|
||||
expect(Actions.openCreateCopyJobPanel).toHaveBeenCalledWith(mockExplorer);
|
||||
expect(Actions.openCreateCopyJobPanel).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should call refreshJobList when refresh button is clicked", () => {
|
||||
const buttons = getCommandBarButtons(mockExplorer);
|
||||
const refreshButton = buttons[1];
|
||||
|
||||
refreshButton.onCommandClick({} as React.SyntheticEvent);
|
||||
|
||||
expect(mockRefreshJobList).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should call openContainerCopyFeedbackBlade when feedback button is clicked", () => {
|
||||
const buttons = getCommandBarButtons(mockExplorer);
|
||||
const feedbackButton = buttons[2];
|
||||
|
||||
feedbackButton.onCommandClick({} as React.SyntheticEvent);
|
||||
|
||||
expect(mockOpenContainerCopyFeedbackBlade).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should return buttons with correct icon sources", () => {
|
||||
const buttons = getCommandBarButtons(mockExplorer);
|
||||
|
||||
expect(buttons[0].iconSrc).toBeDefined();
|
||||
expect(buttons[0].iconAlt).toBe("Create Copy Job");
|
||||
|
||||
expect(buttons[1].iconSrc).toBeDefined();
|
||||
expect(buttons[1].iconAlt).toBe("Refresh");
|
||||
|
||||
expect(buttons[2].iconSrc).toBeDefined();
|
||||
expect(buttons[2].iconAlt).toBe("Feedback");
|
||||
});
|
||||
|
||||
it("should handle null MonitorCopyJobsRefState ref gracefully", () => {
|
||||
(MonitorCopyJobsRefState as unknown as jest.Mock).mockImplementationOnce((selector) => {
|
||||
const state: { ref: null } = { ref: null };
|
||||
return selector(state);
|
||||
});
|
||||
|
||||
const buttons = getCommandBarButtons(mockExplorer);
|
||||
const refreshButton = buttons[1];
|
||||
|
||||
expect(() => refreshButton.onCommandClick({} as React.SyntheticEvent)).not.toThrow();
|
||||
});
|
||||
|
||||
it("should set hasPopup to false for all buttons", () => {
|
||||
const buttons = getCommandBarButtons(mockExplorer);
|
||||
|
||||
buttons.forEach((button) => {
|
||||
expect(button.hasPopup).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it("should set commandButtonLabel to undefined for all buttons", () => {
|
||||
const buttons = getCommandBarButtons(mockExplorer);
|
||||
|
||||
buttons.forEach((button) => {
|
||||
expect(button.commandButtonLabel).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("should respect disabled state when provided", () => {
|
||||
const buttons = getCommandBarButtons(mockExplorer);
|
||||
|
||||
buttons.forEach((button) => {
|
||||
expect(button.disabled).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it("should return CommandButtonComponentProps with all required properties", () => {
|
||||
const buttons = getCommandBarButtons(mockExplorer);
|
||||
|
||||
buttons.forEach((button: CommandButtonComponentProps) => {
|
||||
expect(button).toHaveProperty("iconSrc");
|
||||
expect(button).toHaveProperty("iconAlt");
|
||||
expect(button).toHaveProperty("onCommandClick");
|
||||
expect(button).toHaveProperty("commandButtonLabel");
|
||||
expect(button).toHaveProperty("ariaLabel");
|
||||
expect(button).toHaveProperty("tooltipText");
|
||||
expect(button).toHaveProperty("hasPopup");
|
||||
expect(button).toHaveProperty("disabled");
|
||||
});
|
||||
});
|
||||
|
||||
it("should maintain button order: create, refresh, feedback", () => {
|
||||
const buttons = getCommandBarButtons(mockExplorer);
|
||||
|
||||
expect(buttons[0].tooltipText).toBe("Create Copy Job");
|
||||
expect(buttons[1].tooltipText).toBe("Refresh");
|
||||
expect(buttons[2].tooltipText).toBe("Feedback");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Button click handlers", () => {
|
||||
it("should execute click handlers without errors", () => {
|
||||
const buttons = getCommandBarButtons(mockExplorer);
|
||||
|
||||
buttons.forEach((button) => {
|
||||
expect(() => button.onCommandClick({} as React.SyntheticEvent)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
it("should call correct action for each button", () => {
|
||||
const buttons = getCommandBarButtons(mockExplorer);
|
||||
|
||||
buttons[0].onCommandClick({} as React.SyntheticEvent);
|
||||
expect(Actions.openCreateCopyJobPanel).toHaveBeenCalledWith(mockExplorer);
|
||||
|
||||
buttons[1].onCommandClick({} as React.SyntheticEvent);
|
||||
expect(mockRefreshJobList).toHaveBeenCalled();
|
||||
|
||||
buttons[2].onCommandClick({} as React.SyntheticEvent);
|
||||
expect(mockOpenContainerCopyFeedbackBlade).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Accessibility", () => {
|
||||
it("should have aria labels for all buttons", () => {
|
||||
const buttons = getCommandBarButtons(mockExplorer);
|
||||
|
||||
buttons.forEach((button) => {
|
||||
expect(button.ariaLabel).toBeDefined();
|
||||
expect(typeof button.ariaLabel).toBe("string");
|
||||
expect(button.ariaLabel.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
it("should have tooltip text for all buttons", () => {
|
||||
const buttons = getCommandBarButtons(mockExplorer);
|
||||
|
||||
buttons.forEach((button) => {
|
||||
expect(button.tooltipText).toBeDefined();
|
||||
expect(typeof button.tooltipText).toBe("string");
|
||||
expect(button.tooltipText.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
it("should have icon alt text for all buttons", () => {
|
||||
const buttons = getCommandBarButtons(mockExplorer);
|
||||
|
||||
buttons.forEach((button) => {
|
||||
expect(button.iconAlt).toBeDefined();
|
||||
expect(typeof button.iconAlt).toBe("string");
|
||||
expect(button.iconAlt.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -17,7 +17,7 @@ function getCopyJobBtns(explorer: Explorer): CopyJobCommandBarBtnType[] {
|
||||
iconSrc: AddIcon,
|
||||
label: ContainerCopyMessages.createCopyJobButtonLabel,
|
||||
ariaLabel: ContainerCopyMessages.createCopyJobButtonAriaLabel,
|
||||
onClick: Actions.openCreateCopyJobPanel.bind(null, explorer),
|
||||
onClick: () => Actions.openCreateCopyJobPanel(explorer),
|
||||
},
|
||||
{
|
||||
key: "refresh",
|
||||
|
||||
@@ -89,7 +89,7 @@ export default {
|
||||
enablementTitle: "Enable system assigned managed identity",
|
||||
enablementDescription: (accountName: string) =>
|
||||
accountName
|
||||
? `Enable system-assigned managed identity on the ${accountName}. To confirm, click the "Yes" button. `
|
||||
? `Enable system-assigned managed identity on the ${accountName}. To confirm, click the "Yes" button.`
|
||||
: "",
|
||||
},
|
||||
defaultManagedIdentity: {
|
||||
@@ -116,7 +116,7 @@ export default {
|
||||
},
|
||||
popoverTitle: "Read permissions assigned to default identity.",
|
||||
popoverDescription:
|
||||
"Assign read permissions of the source account to the default identity of the destination account. To confirm click the “Yes” button. ",
|
||||
"Assign read permissions of the source account to the default identity of the destination account. To confirm click the “Yes” button.",
|
||||
},
|
||||
pointInTimeRestore: {
|
||||
title: "Point In Time Restore enabled",
|
||||
|
||||
131
src/Explorer/ContainerCopy/ContainerCopyPanel.test.tsx
Normal file
131
src/Explorer/ContainerCopy/ContainerCopyPanel.test.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import "@testing-library/jest-dom";
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import React from "react";
|
||||
import Explorer from "../Explorer";
|
||||
import ContainerCopyPanel from "./ContainerCopyPanel";
|
||||
import { MonitorCopyJobsRefState } from "./MonitorCopyJobs/MonitorCopyJobRefState";
|
||||
|
||||
jest.mock("./CommandBar/CopyJobCommandBar", () => {
|
||||
const MockCopyJobCommandBar = () => {
|
||||
return <div data-testid="copy-job-command-bar">CopyJobCommandBar</div>;
|
||||
};
|
||||
MockCopyJobCommandBar.displayName = "CopyJobCommandBar";
|
||||
return MockCopyJobCommandBar;
|
||||
});
|
||||
|
||||
jest.mock("./MonitorCopyJobs/MonitorCopyJobs", () => {
|
||||
const React = jest.requireActual("react");
|
||||
const MockMonitorCopyJobs = React.forwardRef((_props: any, ref: any) => {
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
refreshJobList: jest.fn(),
|
||||
}));
|
||||
return <div data-testid="monitor-copy-jobs">MonitorCopyJobs</div>;
|
||||
});
|
||||
MockMonitorCopyJobs.displayName = "MonitorCopyJobs";
|
||||
return MockMonitorCopyJobs;
|
||||
});
|
||||
|
||||
jest.mock("./MonitorCopyJobs/MonitorCopyJobRefState", () => ({
|
||||
MonitorCopyJobsRefState: {
|
||||
getState: jest.fn(() => ({
|
||||
setRef: jest.fn(),
|
||||
})),
|
||||
},
|
||||
}));
|
||||
|
||||
describe("ContainerCopyPanel", () => {
|
||||
let mockExplorer: Explorer;
|
||||
let mockSetRef: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
mockExplorer = {} as Explorer;
|
||||
|
||||
mockSetRef = jest.fn();
|
||||
(MonitorCopyJobsRefState.getState as jest.Mock).mockReturnValue({
|
||||
setRef: mockSetRef,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("renders the component with correct structure", () => {
|
||||
render(<ContainerCopyPanel explorer={mockExplorer} />);
|
||||
|
||||
const wrapper = document.querySelector("#containerCopyWrapper");
|
||||
expect(wrapper).toBeInTheDocument();
|
||||
expect(wrapper).toHaveClass("flexContainer", "hideOverflows");
|
||||
});
|
||||
|
||||
it("renders CopyJobCommandBar component", () => {
|
||||
render(<ContainerCopyPanel explorer={mockExplorer} />);
|
||||
|
||||
const commandBar = screen.getByTestId("copy-job-command-bar");
|
||||
expect(commandBar).toBeInTheDocument();
|
||||
expect(commandBar).toHaveTextContent("CopyJobCommandBar");
|
||||
});
|
||||
|
||||
it("renders MonitorCopyJobs component", () => {
|
||||
render(<ContainerCopyPanel explorer={mockExplorer} />);
|
||||
|
||||
const monitorCopyJobs = screen.getByTestId("monitor-copy-jobs");
|
||||
expect(monitorCopyJobs).toBeInTheDocument();
|
||||
expect(monitorCopyJobs).toHaveTextContent("MonitorCopyJobs");
|
||||
});
|
||||
|
||||
it("passes explorer prop to child components", () => {
|
||||
render(<ContainerCopyPanel explorer={mockExplorer} />);
|
||||
|
||||
expect(screen.getByTestId("copy-job-command-bar")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("monitor-copy-jobs")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("sets the MonitorCopyJobs ref in the state on mount", async () => {
|
||||
render(<ContainerCopyPanel explorer={mockExplorer} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSetRef).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
const refArgument = mockSetRef.mock.calls[0][0];
|
||||
expect(refArgument).toBeDefined();
|
||||
expect(refArgument).toHaveProperty("refreshJobList");
|
||||
expect(typeof refArgument.refreshJobList).toBe("function");
|
||||
});
|
||||
|
||||
it("updates the ref state when monitorCopyJobsRef changes", async () => {
|
||||
const { rerender } = render(<ContainerCopyPanel explorer={mockExplorer} />);
|
||||
await waitFor(() => {
|
||||
expect(mockSetRef).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
mockSetRef.mockClear();
|
||||
rerender(<ContainerCopyPanel explorer={mockExplorer} />);
|
||||
});
|
||||
|
||||
it("handles missing explorer prop gracefully", () => {
|
||||
const { container } = render(<ContainerCopyPanel explorer={undefined as any} />);
|
||||
expect(container.querySelector("#containerCopyWrapper")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("applies correct CSS classes to wrapper", () => {
|
||||
render(<ContainerCopyPanel explorer={mockExplorer} />);
|
||||
|
||||
const wrapper = document.querySelector("#containerCopyWrapper");
|
||||
expect(wrapper).toHaveClass("flexContainer");
|
||||
expect(wrapper).toHaveClass("hideOverflows");
|
||||
});
|
||||
|
||||
it("maintains ref across re-renders", async () => {
|
||||
const { rerender } = render(<ContainerCopyPanel explorer={mockExplorer} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSetRef).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const firstCallRef = mockSetRef.mock.calls[0][0];
|
||||
const newExplorer = {} as Explorer;
|
||||
rerender(<ContainerCopyPanel explorer={newExplorer} />);
|
||||
expect(mockSetRef.mock.calls[0][0]).toBe(firstCallRef);
|
||||
});
|
||||
});
|
||||
@@ -20,4 +20,6 @@ const ContainerCopyPanel: React.FC<ContainerCopyProps> = ({ explorer }) => {
|
||||
);
|
||||
};
|
||||
|
||||
ContainerCopyPanel.displayName = "ContainerCopyPanel";
|
||||
|
||||
export default ContainerCopyPanel;
|
||||
|
||||
667
src/Explorer/ContainerCopy/Context/CopyJobContext.test.tsx
Normal file
667
src/Explorer/ContainerCopy/Context/CopyJobContext.test.tsx
Normal file
@@ -0,0 +1,667 @@
|
||||
import "@testing-library/jest-dom";
|
||||
import { act, render, screen } from "@testing-library/react";
|
||||
import React from "react";
|
||||
import Explorer from "../../Explorer";
|
||||
import { CopyJobMigrationType } from "../Enums/CopyJobEnums";
|
||||
import CopyJobContextProvider, { CopyJobContext, useCopyJobContext } from "./CopyJobContext";
|
||||
|
||||
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",
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe("CopyJobContext", () => {
|
||||
let mockExplorer: Explorer;
|
||||
|
||||
beforeEach(() => {
|
||||
mockExplorer = {} as Explorer;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("CopyJobContextProvider", () => {
|
||||
it("should render children correctly", () => {
|
||||
render(
|
||||
<CopyJobContextProvider explorer={mockExplorer}>
|
||||
<div data-testid="test-child">Test Child</div>
|
||||
</CopyJobContextProvider>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("test-child")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("test-child")).toHaveTextContent("Test Child");
|
||||
});
|
||||
|
||||
it("should initialize with default state", () => {
|
||||
let contextValue: any;
|
||||
|
||||
render(
|
||||
<CopyJobContextProvider explorer={mockExplorer}>
|
||||
<CopyJobContext.Consumer>
|
||||
{(value) => {
|
||||
contextValue = value;
|
||||
return null;
|
||||
}}
|
||||
</CopyJobContext.Consumer>
|
||||
</CopyJobContextProvider>,
|
||||
);
|
||||
|
||||
expect(contextValue).toBeDefined();
|
||||
expect(contextValue.copyJobState).toEqual({
|
||||
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",
|
||||
location: "East US",
|
||||
kind: "GlobalDocumentDB",
|
||||
},
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
target: {
|
||||
subscriptionId: "test-subscription-id",
|
||||
account: {
|
||||
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/test-account",
|
||||
name: "test-account",
|
||||
location: "East US",
|
||||
kind: "GlobalDocumentDB",
|
||||
},
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
sourceReadAccessFromTarget: false,
|
||||
});
|
||||
expect(contextValue.flow).toBeNull();
|
||||
expect(contextValue.contextError).toBeNull();
|
||||
expect(contextValue.explorer).toBe(mockExplorer);
|
||||
});
|
||||
|
||||
it("should provide setCopyJobState function", () => {
|
||||
let contextValue: any;
|
||||
|
||||
render(
|
||||
<CopyJobContextProvider explorer={mockExplorer}>
|
||||
<CopyJobContext.Consumer>
|
||||
{(value) => {
|
||||
contextValue = value;
|
||||
return null;
|
||||
}}
|
||||
</CopyJobContext.Consumer>
|
||||
</CopyJobContextProvider>,
|
||||
);
|
||||
|
||||
expect(contextValue.setCopyJobState).toBeDefined();
|
||||
expect(typeof contextValue.setCopyJobState).toBe("function");
|
||||
});
|
||||
|
||||
it("should provide setFlow function", () => {
|
||||
let contextValue: any;
|
||||
|
||||
render(
|
||||
<CopyJobContextProvider explorer={mockExplorer}>
|
||||
<CopyJobContext.Consumer>
|
||||
{(value) => {
|
||||
contextValue = value;
|
||||
return null;
|
||||
}}
|
||||
</CopyJobContext.Consumer>
|
||||
</CopyJobContextProvider>,
|
||||
);
|
||||
|
||||
expect(contextValue.setFlow).toBeDefined();
|
||||
expect(typeof contextValue.setFlow).toBe("function");
|
||||
});
|
||||
|
||||
it("should provide setContextError function", () => {
|
||||
let contextValue: any;
|
||||
|
||||
render(
|
||||
<CopyJobContextProvider explorer={mockExplorer}>
|
||||
<CopyJobContext.Consumer>
|
||||
{(value) => {
|
||||
contextValue = value;
|
||||
return null;
|
||||
}}
|
||||
</CopyJobContext.Consumer>
|
||||
</CopyJobContextProvider>,
|
||||
);
|
||||
|
||||
expect(contextValue.setContextError).toBeDefined();
|
||||
expect(typeof contextValue.setContextError).toBe("function");
|
||||
});
|
||||
|
||||
it("should provide resetCopyJobState function", () => {
|
||||
let contextValue: any;
|
||||
|
||||
render(
|
||||
<CopyJobContextProvider explorer={mockExplorer}>
|
||||
<CopyJobContext.Consumer>
|
||||
{(value) => {
|
||||
contextValue = value;
|
||||
return null;
|
||||
}}
|
||||
</CopyJobContext.Consumer>
|
||||
</CopyJobContextProvider>,
|
||||
);
|
||||
|
||||
expect(contextValue.resetCopyJobState).toBeDefined();
|
||||
expect(typeof contextValue.resetCopyJobState).toBe("function");
|
||||
});
|
||||
|
||||
it("should update copyJobState when setCopyJobState is called", () => {
|
||||
let contextValue: any;
|
||||
|
||||
const TestComponent = (): JSX.Element => {
|
||||
const context = useCopyJobContext();
|
||||
contextValue = context;
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() =>
|
||||
context.setCopyJobState({
|
||||
...context.copyJobState,
|
||||
jobName: "test-job",
|
||||
migrationType: CopyJobMigrationType.Online,
|
||||
})
|
||||
}
|
||||
>
|
||||
Update Job
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
render(
|
||||
<CopyJobContextProvider explorer={mockExplorer}>
|
||||
<TestComponent />
|
||||
</CopyJobContextProvider>,
|
||||
);
|
||||
|
||||
const button = screen.getByText("Update Job");
|
||||
act(() => {
|
||||
button.click();
|
||||
});
|
||||
|
||||
expect(contextValue.copyJobState.jobName).toBe("test-job");
|
||||
expect(contextValue.copyJobState.migrationType).toBe(CopyJobMigrationType.Online);
|
||||
});
|
||||
|
||||
it("should update flow when setFlow is called", () => {
|
||||
let contextValue: any;
|
||||
|
||||
const TestComponent = (): JSX.Element => {
|
||||
const context = useCopyJobContext();
|
||||
contextValue = context;
|
||||
|
||||
const handleSetFlow = (): void => {
|
||||
context.setFlow({ currentScreen: "source-selection" });
|
||||
};
|
||||
|
||||
return <button onClick={handleSetFlow}>Set Flow</button>;
|
||||
};
|
||||
|
||||
render(
|
||||
<CopyJobContextProvider explorer={mockExplorer}>
|
||||
<TestComponent />
|
||||
</CopyJobContextProvider>,
|
||||
);
|
||||
|
||||
expect(contextValue.flow).toBeNull();
|
||||
|
||||
const button = screen.getByText("Set Flow");
|
||||
act(() => {
|
||||
button.click();
|
||||
});
|
||||
|
||||
expect(contextValue.flow).toEqual({ currentScreen: "source-selection" });
|
||||
});
|
||||
|
||||
it("should update contextError when setContextError is called", () => {
|
||||
let contextValue: any;
|
||||
|
||||
const TestComponent = (): JSX.Element => {
|
||||
const context = useCopyJobContext();
|
||||
contextValue = context;
|
||||
|
||||
return <button onClick={() => context.setContextError("Test error message")}>Set Error</button>;
|
||||
};
|
||||
|
||||
render(
|
||||
<CopyJobContextProvider explorer={mockExplorer}>
|
||||
<TestComponent />
|
||||
</CopyJobContextProvider>,
|
||||
);
|
||||
|
||||
expect(contextValue.contextError).toBeNull();
|
||||
|
||||
const button = screen.getByText("Set Error");
|
||||
act(() => {
|
||||
button.click();
|
||||
});
|
||||
|
||||
expect(contextValue.contextError).toBe("Test error message");
|
||||
});
|
||||
|
||||
it("should reset copyJobState when resetCopyJobState is called", () => {
|
||||
let contextValue: any;
|
||||
|
||||
const TestComponent = (): JSX.Element => {
|
||||
const context = useCopyJobContext();
|
||||
contextValue = context;
|
||||
|
||||
const handleUpdate = (): void => {
|
||||
context.setCopyJobState({
|
||||
...context.copyJobState,
|
||||
jobName: "modified-job",
|
||||
migrationType: CopyJobMigrationType.Online,
|
||||
source: {
|
||||
...context.copyJobState.source,
|
||||
databaseId: "test-db",
|
||||
containerId: "test-container",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<button onClick={handleUpdate}>Update</button>
|
||||
<button onClick={context.resetCopyJobState}>Reset</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
render(
|
||||
<CopyJobContextProvider explorer={mockExplorer}>
|
||||
<TestComponent />
|
||||
</CopyJobContextProvider>,
|
||||
);
|
||||
|
||||
const updateButton = screen.getByText("Update");
|
||||
act(() => {
|
||||
updateButton.click();
|
||||
});
|
||||
|
||||
expect(contextValue.copyJobState.jobName).toBe("modified-job");
|
||||
expect(contextValue.copyJobState.migrationType).toBe(CopyJobMigrationType.Online);
|
||||
expect(contextValue.copyJobState.source.databaseId).toBe("test-db");
|
||||
|
||||
const resetButton = screen.getByText("Reset");
|
||||
act(() => {
|
||||
resetButton.click();
|
||||
});
|
||||
|
||||
expect(contextValue.copyJobState.jobName).toBe("");
|
||||
expect(contextValue.copyJobState.migrationType).toBe(CopyJobMigrationType.Offline);
|
||||
expect(contextValue.copyJobState.source.databaseId).toBe("");
|
||||
expect(contextValue.copyJobState.source.containerId).toBe("");
|
||||
});
|
||||
|
||||
it("should maintain explorer reference", () => {
|
||||
let contextValue: any;
|
||||
|
||||
render(
|
||||
<CopyJobContextProvider explorer={mockExplorer}>
|
||||
<CopyJobContext.Consumer>
|
||||
{(value) => {
|
||||
contextValue = value;
|
||||
return null;
|
||||
}}
|
||||
</CopyJobContext.Consumer>
|
||||
</CopyJobContextProvider>,
|
||||
);
|
||||
|
||||
expect(contextValue.explorer).toBe(mockExplorer);
|
||||
});
|
||||
|
||||
it("should handle multiple state updates correctly", () => {
|
||||
let contextValue: any;
|
||||
|
||||
const TestComponent = (): JSX.Element => {
|
||||
const context = useCopyJobContext();
|
||||
contextValue = context;
|
||||
|
||||
return (
|
||||
<>
|
||||
<button onClick={() => context.setCopyJobState({ ...context.copyJobState, jobName: "job-1" })}>
|
||||
Update 1
|
||||
</button>
|
||||
<button onClick={() => context.setFlow({ currentScreen: "screen-1" })}>Flow 1</button>
|
||||
<button onClick={() => context.setContextError("error-1")}>Error 1</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
render(
|
||||
<CopyJobContextProvider explorer={mockExplorer}>
|
||||
<TestComponent />
|
||||
</CopyJobContextProvider>,
|
||||
);
|
||||
|
||||
act(() => {
|
||||
screen.getByText("Update 1").click();
|
||||
});
|
||||
expect(contextValue.copyJobState.jobName).toBe("job-1");
|
||||
|
||||
act(() => {
|
||||
screen.getByText("Flow 1").click();
|
||||
});
|
||||
expect(contextValue.flow).toEqual({ currentScreen: "screen-1" });
|
||||
|
||||
act(() => {
|
||||
screen.getByText("Error 1").click();
|
||||
});
|
||||
expect(contextValue.contextError).toBe("error-1");
|
||||
});
|
||||
|
||||
it("should handle partial state updates", () => {
|
||||
let contextValue: any;
|
||||
|
||||
const TestComponent = (): JSX.Element => {
|
||||
const context = useCopyJobContext();
|
||||
contextValue = context;
|
||||
|
||||
const handlePartialUpdate = (): void => {
|
||||
context.setCopyJobState((prev) => ({
|
||||
...prev,
|
||||
jobName: "partial-update",
|
||||
}));
|
||||
};
|
||||
|
||||
return <button onClick={handlePartialUpdate}>Partial Update</button>;
|
||||
};
|
||||
|
||||
render(
|
||||
<CopyJobContextProvider explorer={mockExplorer}>
|
||||
<TestComponent />
|
||||
</CopyJobContextProvider>,
|
||||
);
|
||||
|
||||
const initialState = { ...contextValue.copyJobState };
|
||||
|
||||
act(() => {
|
||||
screen.getByText("Partial Update").click();
|
||||
});
|
||||
|
||||
expect(contextValue.copyJobState.jobName).toBe("partial-update");
|
||||
expect(contextValue.copyJobState.migrationType).toBe(initialState.migrationType);
|
||||
expect(contextValue.copyJobState.source).toEqual(initialState.source);
|
||||
expect(contextValue.copyJobState.target).toEqual(initialState.target);
|
||||
});
|
||||
});
|
||||
|
||||
describe("useCopyJobContext", () => {
|
||||
it("should return context value when used within provider", () => {
|
||||
let contextValue: any;
|
||||
|
||||
const TestComponent = (): null => {
|
||||
const context = useCopyJobContext();
|
||||
contextValue = context;
|
||||
return null;
|
||||
};
|
||||
|
||||
render(
|
||||
<CopyJobContextProvider explorer={mockExplorer}>
|
||||
<TestComponent />
|
||||
</CopyJobContextProvider>,
|
||||
);
|
||||
|
||||
expect(contextValue).toBeDefined();
|
||||
expect(contextValue.copyJobState).toBeDefined();
|
||||
expect(contextValue.setCopyJobState).toBeDefined();
|
||||
expect(contextValue.flow).toBeNull();
|
||||
expect(contextValue.setFlow).toBeDefined();
|
||||
expect(contextValue.contextError).toBeNull();
|
||||
expect(contextValue.setContextError).toBeDefined();
|
||||
expect(contextValue.resetCopyJobState).toBeDefined();
|
||||
expect(contextValue.explorer).toBe(mockExplorer);
|
||||
});
|
||||
|
||||
it("should throw error when used outside provider", () => {
|
||||
const originalError = console.error;
|
||||
console.error = jest.fn();
|
||||
|
||||
const TestComponent = (): null => {
|
||||
useCopyJobContext();
|
||||
return null;
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
render(<TestComponent />);
|
||||
}).toThrow("useCopyJobContext must be used within a CopyJobContextProvider");
|
||||
|
||||
console.error = originalError;
|
||||
});
|
||||
|
||||
it("should allow updating state through hook", () => {
|
||||
let contextValue: any;
|
||||
|
||||
const TestComponent = (): JSX.Element => {
|
||||
const context = useCopyJobContext();
|
||||
contextValue = context;
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() =>
|
||||
context.setCopyJobState({
|
||||
...context.copyJobState,
|
||||
jobName: "hook-test-job",
|
||||
})
|
||||
}
|
||||
>
|
||||
Update
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
render(
|
||||
<CopyJobContextProvider explorer={mockExplorer}>
|
||||
<TestComponent />
|
||||
</CopyJobContextProvider>,
|
||||
);
|
||||
|
||||
act(() => {
|
||||
screen.getByText("Update").click();
|
||||
});
|
||||
|
||||
expect(contextValue.copyJobState.jobName).toBe("hook-test-job");
|
||||
});
|
||||
|
||||
it("should allow resetting state through hook", () => {
|
||||
let contextValue: any;
|
||||
|
||||
const TestComponent = (): JSX.Element => {
|
||||
const context = useCopyJobContext();
|
||||
contextValue = context;
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={() =>
|
||||
context.setCopyJobState({
|
||||
...context.copyJobState,
|
||||
jobName: "modified",
|
||||
source: {
|
||||
...context.copyJobState.source,
|
||||
databaseId: "modified-db",
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
Modify
|
||||
</button>
|
||||
<button onClick={() => context.resetCopyJobState()}>Reset</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
render(
|
||||
<CopyJobContextProvider explorer={mockExplorer}>
|
||||
<TestComponent />
|
||||
</CopyJobContextProvider>,
|
||||
);
|
||||
|
||||
act(() => {
|
||||
screen.getByText("Modify").click();
|
||||
});
|
||||
|
||||
expect(contextValue.copyJobState.jobName).toBe("modified");
|
||||
expect(contextValue.copyJobState.source.databaseId).toBe("modified-db");
|
||||
|
||||
act(() => {
|
||||
screen.getByText("Reset").click();
|
||||
});
|
||||
|
||||
expect(contextValue.copyJobState.jobName).toBe("");
|
||||
expect(contextValue.copyJobState.source.databaseId).toBe("");
|
||||
});
|
||||
|
||||
it("should maintain state consistency across multiple components", () => {
|
||||
let contextValue1: any;
|
||||
let contextValue2: any;
|
||||
|
||||
const TestComponent1 = (): JSX.Element => {
|
||||
const context = useCopyJobContext();
|
||||
contextValue1 = context;
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() =>
|
||||
context.setCopyJobState({
|
||||
...context.copyJobState,
|
||||
jobName: "shared-job",
|
||||
})
|
||||
}
|
||||
>
|
||||
Update From Component 1
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const TestComponent2 = (): JSX.Element => {
|
||||
const context = useCopyJobContext();
|
||||
contextValue2 = context;
|
||||
return <div data-testid="component-2">Component 2</div>;
|
||||
};
|
||||
|
||||
render(
|
||||
<CopyJobContextProvider explorer={mockExplorer}>
|
||||
<TestComponent1 />
|
||||
<TestComponent2 />
|
||||
</CopyJobContextProvider>,
|
||||
);
|
||||
|
||||
expect(contextValue1.copyJobState).toEqual(contextValue2.copyJobState);
|
||||
|
||||
act(() => {
|
||||
screen.getByText("Update From Component 1").click();
|
||||
});
|
||||
|
||||
expect(contextValue1.copyJobState.jobName).toBe("shared-job");
|
||||
expect(contextValue2.copyJobState.jobName).toBe("shared-job");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Initial State", () => {
|
||||
it("should initialize with offline migration type", () => {
|
||||
let contextValue: any;
|
||||
|
||||
render(
|
||||
<CopyJobContextProvider explorer={mockExplorer}>
|
||||
<CopyJobContext.Consumer>
|
||||
{(value) => {
|
||||
contextValue = value;
|
||||
return null;
|
||||
}}
|
||||
</CopyJobContext.Consumer>
|
||||
</CopyJobContextProvider>,
|
||||
);
|
||||
|
||||
expect(contextValue.copyJobState.migrationType).toBe(CopyJobMigrationType.Offline);
|
||||
});
|
||||
|
||||
it("should initialize source with userContext values", () => {
|
||||
let contextValue: any;
|
||||
|
||||
render(
|
||||
<CopyJobContextProvider explorer={mockExplorer}>
|
||||
<CopyJobContext.Consumer>
|
||||
{(value) => {
|
||||
contextValue = value;
|
||||
return null;
|
||||
}}
|
||||
</CopyJobContext.Consumer>
|
||||
</CopyJobContextProvider>,
|
||||
);
|
||||
|
||||
expect(contextValue.copyJobState.source.subscription.subscriptionId).toBe("test-subscription-id");
|
||||
expect(contextValue.copyJobState.source.account.name).toBe("test-account");
|
||||
});
|
||||
|
||||
it("should initialize target with userContext values", () => {
|
||||
let contextValue: any;
|
||||
|
||||
render(
|
||||
<CopyJobContextProvider explorer={mockExplorer}>
|
||||
<CopyJobContext.Consumer>
|
||||
{(value) => {
|
||||
contextValue = value;
|
||||
return null;
|
||||
}}
|
||||
</CopyJobContext.Consumer>
|
||||
</CopyJobContextProvider>,
|
||||
);
|
||||
|
||||
expect(contextValue.copyJobState.target.subscriptionId).toBe("test-subscription-id");
|
||||
expect(contextValue.copyJobState.target.account.name).toBe("test-account");
|
||||
});
|
||||
|
||||
it("should initialize sourceReadAccessFromTarget as false", () => {
|
||||
let contextValue: any;
|
||||
|
||||
render(
|
||||
<CopyJobContextProvider explorer={mockExplorer}>
|
||||
<CopyJobContext.Consumer>
|
||||
{(value) => {
|
||||
contextValue = value;
|
||||
return null;
|
||||
}}
|
||||
</CopyJobContext.Consumer>
|
||||
</CopyJobContextProvider>,
|
||||
);
|
||||
|
||||
expect(contextValue.copyJobState.sourceReadAccessFromTarget).toBe(false);
|
||||
});
|
||||
|
||||
it("should initialize with empty database and container ids", () => {
|
||||
let contextValue: any;
|
||||
|
||||
render(
|
||||
<CopyJobContextProvider explorer={mockExplorer}>
|
||||
<CopyJobContext.Consumer>
|
||||
{(value) => {
|
||||
contextValue = value;
|
||||
return null;
|
||||
}}
|
||||
</CopyJobContext.Consumer>
|
||||
</CopyJobContextProvider>,
|
||||
);
|
||||
|
||||
expect(contextValue.copyJobState.source.databaseId).toBe("");
|
||||
expect(contextValue.copyJobState.source.containerId).toBe("");
|
||||
expect(contextValue.copyJobState.target.databaseId).toBe("");
|
||||
expect(contextValue.copyJobState.target.containerId).toBe("");
|
||||
});
|
||||
});
|
||||
});
|
||||
490
src/Explorer/ContainerCopy/CopyJobUtils.test.ts
Normal file
490
src/Explorer/ContainerCopy/CopyJobUtils.test.ts
Normal file
@@ -0,0 +1,490 @@
|
||||
import { DatabaseAccount } from "Contracts/DataModels";
|
||||
import * as CopyJobUtils from "./CopyJobUtils";
|
||||
import { CopyJobContextState, CopyJobErrorType, CopyJobType } from "./Types/CopyJobTypes";
|
||||
|
||||
describe("CopyJobUtils", () => {
|
||||
describe("buildResourceLink", () => {
|
||||
const mockResource: DatabaseAccount = {
|
||||
id: "/subscriptions/sub123/resourceGroups/rg1/providers/Microsoft.DocumentDB/databaseAccounts/account1",
|
||||
name: "account1",
|
||||
location: "eastus",
|
||||
type: "Microsoft.DocumentDB/databaseAccounts",
|
||||
kind: "GlobalDocumentDB",
|
||||
properties: {},
|
||||
};
|
||||
|
||||
let originalLocation: Location;
|
||||
|
||||
beforeEach(() => {
|
||||
originalLocation = window.location;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
(window as any).location = originalLocation;
|
||||
});
|
||||
|
||||
it("should build resource link with Azure portal endpoint", () => {
|
||||
delete (window as any).location;
|
||||
(window as any).location = {
|
||||
...originalLocation,
|
||||
origin: "https://portal.azure.com",
|
||||
ancestorOrigins: ["https://portal.azure.com"] as any,
|
||||
} as Location;
|
||||
|
||||
const link = CopyJobUtils.buildResourceLink(mockResource);
|
||||
expect(link).toBe(
|
||||
"https://portal.azure.com/#resource/subscriptions/sub123/resourceGroups/rg1/providers/Microsoft.DocumentDB/databaseAccounts/account1",
|
||||
);
|
||||
});
|
||||
|
||||
it("should replace cosmos.azure with portal.azure", () => {
|
||||
delete (window as any).location;
|
||||
(window as any).location = {
|
||||
...originalLocation,
|
||||
origin: "https://cosmos.azure.com",
|
||||
ancestorOrigins: ["https://cosmos.azure.com"] as any,
|
||||
} as Location;
|
||||
|
||||
const link = CopyJobUtils.buildResourceLink(mockResource);
|
||||
expect(link).toContain("https://portal.azure.com");
|
||||
});
|
||||
|
||||
it("should use Azure portal endpoint for localhost", () => {
|
||||
delete (window as any).location;
|
||||
(window as any).location = {
|
||||
...originalLocation,
|
||||
origin: "http://localhost:1234",
|
||||
ancestorOrigins: ["http://localhost:1234"] as any,
|
||||
} as Location;
|
||||
|
||||
const link = CopyJobUtils.buildResourceLink(mockResource);
|
||||
expect(link).toContain("https://ms.portal.azure.com");
|
||||
});
|
||||
|
||||
it("should remove trailing slash from origin", () => {
|
||||
delete (window as any).location;
|
||||
(window as any).location = {
|
||||
...originalLocation,
|
||||
origin: "https://portal.azure.com/",
|
||||
ancestorOrigins: ["https://portal.azure.com/"] as any,
|
||||
} as Location;
|
||||
|
||||
const link = CopyJobUtils.buildResourceLink(mockResource);
|
||||
expect(link).toBe(
|
||||
"https://portal.azure.com/#resource/subscriptions/sub123/resourceGroups/rg1/providers/Microsoft.DocumentDB/databaseAccounts/account1",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildDataTransferJobPath", () => {
|
||||
it("should build basic path without jobName or action", () => {
|
||||
const path = CopyJobUtils.buildDataTransferJobPath({
|
||||
subscriptionId: "sub123",
|
||||
resourceGroup: "rg1",
|
||||
accountName: "account1",
|
||||
});
|
||||
|
||||
expect(path).toBe(
|
||||
"/subscriptions/sub123/resourceGroups/rg1/providers/Microsoft.DocumentDB/databaseAccounts/account1/dataTransferJobs",
|
||||
);
|
||||
});
|
||||
|
||||
it("should build path with jobName", () => {
|
||||
const path = CopyJobUtils.buildDataTransferJobPath({
|
||||
subscriptionId: "sub123",
|
||||
resourceGroup: "rg1",
|
||||
accountName: "account1",
|
||||
jobName: "job1",
|
||||
});
|
||||
|
||||
expect(path).toBe(
|
||||
"/subscriptions/sub123/resourceGroups/rg1/providers/Microsoft.DocumentDB/databaseAccounts/account1/dataTransferJobs/job1",
|
||||
);
|
||||
});
|
||||
|
||||
it("should build path with jobName and action", () => {
|
||||
const path = CopyJobUtils.buildDataTransferJobPath({
|
||||
subscriptionId: "sub123",
|
||||
resourceGroup: "rg1",
|
||||
accountName: "account1",
|
||||
jobName: "job1",
|
||||
action: "cancel",
|
||||
});
|
||||
|
||||
expect(path).toBe(
|
||||
"/subscriptions/sub123/resourceGroups/rg1/providers/Microsoft.DocumentDB/databaseAccounts/account1/dataTransferJobs/job1/cancel",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("convertTime", () => {
|
||||
it("should convert time string with hours, minutes, and seconds", () => {
|
||||
const result = CopyJobUtils.convertTime("02:30:45");
|
||||
expect(result).toBe("02 hours, 30 minutes, 45 seconds");
|
||||
});
|
||||
|
||||
it("should convert time string with only seconds", () => {
|
||||
const result = CopyJobUtils.convertTime("00:00:30");
|
||||
expect(result).toBe("30 seconds");
|
||||
});
|
||||
|
||||
it("should convert time string with only minutes and seconds", () => {
|
||||
const result = CopyJobUtils.convertTime("00:05:15");
|
||||
expect(result).toBe("05 minutes, 15 seconds");
|
||||
});
|
||||
|
||||
it("should round seconds", () => {
|
||||
const result = CopyJobUtils.convertTime("00:00:45.678");
|
||||
expect(result).toBe("46 seconds");
|
||||
});
|
||||
|
||||
it("should return '0 seconds' for zero time", () => {
|
||||
const result = CopyJobUtils.convertTime("00:00:00");
|
||||
expect(result).toBe("0 seconds");
|
||||
});
|
||||
|
||||
it("should return null for invalid time format", () => {
|
||||
const result = CopyJobUtils.convertTime("invalid");
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should return null for incomplete time string", () => {
|
||||
const result = CopyJobUtils.convertTime("10:30");
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should pad single digit values", () => {
|
||||
const result = CopyJobUtils.convertTime("1:5:9");
|
||||
expect(result).toBe("01 hours, 05 minutes, 09 seconds");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatUTCDateTime", () => {
|
||||
it("should format valid UTC date string", () => {
|
||||
const result = CopyJobUtils.formatUTCDateTime("2025-11-26T10:30:00Z");
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.formattedDateTime).toContain("11/26/25, 10:30:00 AM");
|
||||
expect(result?.timestamp).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("should return null for invalid date string", () => {
|
||||
const result = CopyJobUtils.formatUTCDateTime("invalid-date");
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should return timestamp for valid date", () => {
|
||||
const result = CopyJobUtils.formatUTCDateTime("2025-01-01T00:00:00Z");
|
||||
expect(result).not.toBeNull();
|
||||
expect(typeof result?.timestamp).toBe("number");
|
||||
expect(result?.timestamp).toBe(new Date("2025-01-01T00:00:00Z").getTime());
|
||||
});
|
||||
});
|
||||
|
||||
describe("convertToCamelCase", () => {
|
||||
it("should convert string to camel case", () => {
|
||||
const result = CopyJobUtils.convertToCamelCase("hello world");
|
||||
expect(result).toBe("HelloWorld");
|
||||
});
|
||||
|
||||
it("should handle single word", () => {
|
||||
const result = CopyJobUtils.convertToCamelCase("hello");
|
||||
expect(result).toBe("Hello");
|
||||
});
|
||||
|
||||
it("should handle multiple spaces", () => {
|
||||
const result = CopyJobUtils.convertToCamelCase("hello world test");
|
||||
expect(result).toBe("HelloWorldTest");
|
||||
});
|
||||
|
||||
it("should handle mixed case input", () => {
|
||||
const result = CopyJobUtils.convertToCamelCase("HELLO WORLD");
|
||||
expect(result).toBe("HelloWorld");
|
||||
});
|
||||
|
||||
it("should handle empty string", () => {
|
||||
const result = CopyJobUtils.convertToCamelCase("");
|
||||
expect(result).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("extractErrorMessage", () => {
|
||||
it("should extract first part of error message before line breaks", () => {
|
||||
const error: CopyJobErrorType = {
|
||||
message: "Error occurred\r\n\r\nAdditional details\r\n\r\nMore info",
|
||||
code: "500",
|
||||
};
|
||||
|
||||
const result = CopyJobUtils.extractErrorMessage(error);
|
||||
expect(result.message).toBe("Error occurred");
|
||||
expect(result.code).toBe("500");
|
||||
});
|
||||
|
||||
it("should return same message if no line breaks", () => {
|
||||
const error: CopyJobErrorType = {
|
||||
message: "Simple error message",
|
||||
code: "404",
|
||||
};
|
||||
|
||||
const result = CopyJobUtils.extractErrorMessage(error);
|
||||
expect(result.message).toBe("Simple error message");
|
||||
expect(result.code).toBe("404");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAccountDetailsFromResourceId", () => {
|
||||
it("should extract account details from valid resource ID", () => {
|
||||
const resourceId =
|
||||
"/subscriptions/sub123/resourceGroups/rg1/providers/Microsoft.DocumentDB/databaseAccounts/account1";
|
||||
const details = CopyJobUtils.getAccountDetailsFromResourceId(resourceId);
|
||||
|
||||
expect(details).toEqual({
|
||||
subscriptionId: "sub123",
|
||||
resourceGroup: "rg1",
|
||||
accountName: "account1",
|
||||
});
|
||||
});
|
||||
|
||||
it("should be case insensitive", () => {
|
||||
const resourceId =
|
||||
"/subscriptions/sub123/resourceGroups/rg1/providers/microsoft.documentdb/databaseAccounts/account1";
|
||||
const details = CopyJobUtils.getAccountDetailsFromResourceId(resourceId);
|
||||
|
||||
expect(details).toEqual({
|
||||
subscriptionId: "sub123",
|
||||
resourceGroup: "rg1",
|
||||
accountName: "account1",
|
||||
});
|
||||
});
|
||||
|
||||
it("should return null for undefined resource ID", () => {
|
||||
const details = CopyJobUtils.getAccountDetailsFromResourceId(undefined);
|
||||
expect(details).toBeNull();
|
||||
});
|
||||
|
||||
it("should return null for invalid resource ID", () => {
|
||||
const details = CopyJobUtils.getAccountDetailsFromResourceId("invalid-resource-id");
|
||||
expect(details).toEqual({ accountName: undefined, resourceGroup: undefined, subscriptionId: undefined });
|
||||
});
|
||||
});
|
||||
|
||||
describe("getContainerIdentifiers", () => {
|
||||
it("should extract container identifiers", () => {
|
||||
const container = {
|
||||
account: {
|
||||
id: "/subscriptions/sub123/resourceGroups/rg1/providers/Microsoft.DocumentDB/databaseAccounts/account1",
|
||||
name: "account1",
|
||||
location: "eastus",
|
||||
type: "Microsoft.DocumentDB/databaseAccounts",
|
||||
kind: "GlobalDocumentDB",
|
||||
properties: {},
|
||||
},
|
||||
databaseId: "db1",
|
||||
containerId: "container1",
|
||||
} as CopyJobContextState["source"];
|
||||
|
||||
const identifiers = CopyJobUtils.getContainerIdentifiers(container);
|
||||
expect(identifiers).toEqual({
|
||||
accountId: container.account.id,
|
||||
databaseId: "db1",
|
||||
containerId: "container1",
|
||||
});
|
||||
});
|
||||
|
||||
it("should return empty strings for undefined values", () => {
|
||||
const container = {
|
||||
account: undefined,
|
||||
databaseId: undefined,
|
||||
containerId: undefined,
|
||||
} as CopyJobContextState["source"];
|
||||
|
||||
const identifiers = CopyJobUtils.getContainerIdentifiers(container);
|
||||
expect(identifiers).toEqual({
|
||||
accountId: "",
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("isIntraAccountCopy", () => {
|
||||
const sourceAccountId =
|
||||
"/subscriptions/sub123/resourceGroups/rg1/providers/Microsoft.DocumentDB/databaseAccounts/account1";
|
||||
const targetAccountId =
|
||||
"/subscriptions/sub123/resourceGroups/rg1/providers/Microsoft.DocumentDB/databaseAccounts/account1";
|
||||
const differentAccountId =
|
||||
"/subscriptions/sub456/resourceGroups/rg2/providers/Microsoft.DocumentDB/databaseAccounts/account2";
|
||||
|
||||
it("should return true for same account", () => {
|
||||
const result = CopyJobUtils.isIntraAccountCopy(sourceAccountId, targetAccountId);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false for different accounts", () => {
|
||||
const result = CopyJobUtils.isIntraAccountCopy(sourceAccountId, differentAccountId);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for different subscriptions", () => {
|
||||
const differentSubId =
|
||||
"/subscriptions/sub999/resourceGroups/rg1/providers/Microsoft.DocumentDB/databaseAccounts/account1";
|
||||
const result = CopyJobUtils.isIntraAccountCopy(sourceAccountId, differentSubId);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for different resource groups", () => {
|
||||
const differentRgId =
|
||||
"/subscriptions/sub123/resourceGroups/rg999/providers/Microsoft.DocumentDB/databaseAccounts/account1";
|
||||
const result = CopyJobUtils.isIntraAccountCopy(sourceAccountId, differentRgId);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for undefined source", () => {
|
||||
const result = CopyJobUtils.isIntraAccountCopy(undefined, targetAccountId);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for undefined target", () => {
|
||||
const result = CopyJobUtils.isIntraAccountCopy(sourceAccountId, undefined);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isEqual", () => {
|
||||
const createMockJob = (name: string, status: string): CopyJobType => ({
|
||||
ID: name,
|
||||
Mode: "Online",
|
||||
Name: name,
|
||||
Status: status as any,
|
||||
CompletionPercentage: 50,
|
||||
Duration: "00:05:00",
|
||||
LastUpdatedTime: "2025-11-26T10:00:00Z",
|
||||
timestamp: Date.now(),
|
||||
Source: {} as any,
|
||||
Destination: {} as any,
|
||||
});
|
||||
|
||||
it("should return true for equal job arrays", () => {
|
||||
const jobs1 = [createMockJob("job1", "Running"), createMockJob("job2", "Completed")];
|
||||
const jobs2 = [createMockJob("job1", "Running"), createMockJob("job2", "Completed")];
|
||||
|
||||
const result = CopyJobUtils.isEqual(jobs1, jobs2);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false for different lengths", () => {
|
||||
const jobs1 = [createMockJob("job1", "Running")];
|
||||
const jobs2 = [createMockJob("job1", "Running"), createMockJob("job2", "Completed")];
|
||||
|
||||
const result = CopyJobUtils.isEqual(jobs1, jobs2);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for different status", () => {
|
||||
const jobs1 = [createMockJob("job1", "Running")];
|
||||
const jobs2 = [createMockJob("job1", "Completed")];
|
||||
|
||||
const result = CopyJobUtils.isEqual(jobs1, jobs2);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for missing job in second array", () => {
|
||||
const jobs1 = [createMockJob("job1", "Running")];
|
||||
const jobs2 = [createMockJob("job2", "Running")];
|
||||
|
||||
const result = CopyJobUtils.isEqual(jobs1, jobs2);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return true for empty arrays", () => {
|
||||
const result = CopyJobUtils.isEqual([], []);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getDefaultJobName", () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(Date.prototype, "getTime").mockReturnValue(1234567890);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should generate default job name for single container", () => {
|
||||
const containers = [
|
||||
{
|
||||
sourceDatabaseName: "sourceDb",
|
||||
sourceContainerName: "sourceCont",
|
||||
targetDatabaseName: "targetDb",
|
||||
targetContainerName: "targetCont",
|
||||
},
|
||||
];
|
||||
|
||||
const jobName = CopyJobUtils.getDefaultJobName(containers);
|
||||
expect(jobName).toBe("sourc.sourc_targe.targe_1234567890");
|
||||
});
|
||||
|
||||
it("should truncate long names", () => {
|
||||
const containers = [
|
||||
{
|
||||
sourceDatabaseName: "veryLongSourceDatabaseName",
|
||||
sourceContainerName: "veryLongSourceContainerName",
|
||||
targetDatabaseName: "veryLongTargetDatabaseName",
|
||||
targetContainerName: "veryLongTargetContainerName",
|
||||
},
|
||||
];
|
||||
|
||||
const jobName = CopyJobUtils.getDefaultJobName(containers);
|
||||
expect(jobName).toBe("veryL.veryL_veryL.veryL_1234567890");
|
||||
});
|
||||
|
||||
it("should return empty string for multiple containers", () => {
|
||||
const containers = [
|
||||
{
|
||||
sourceDatabaseName: "db1",
|
||||
sourceContainerName: "cont1",
|
||||
targetDatabaseName: "db2",
|
||||
targetContainerName: "cont2",
|
||||
},
|
||||
{
|
||||
sourceDatabaseName: "db3",
|
||||
sourceContainerName: "cont3",
|
||||
targetDatabaseName: "db4",
|
||||
targetContainerName: "cont4",
|
||||
},
|
||||
];
|
||||
|
||||
const jobName = CopyJobUtils.getDefaultJobName(containers);
|
||||
expect(jobName).toBe("");
|
||||
});
|
||||
|
||||
it("should return empty string for empty array", () => {
|
||||
const jobName = CopyJobUtils.getDefaultJobName([]);
|
||||
expect(jobName).toBe("");
|
||||
});
|
||||
|
||||
it("should handle short names without truncation", () => {
|
||||
const containers = [
|
||||
{
|
||||
sourceDatabaseName: "src",
|
||||
sourceContainerName: "cont",
|
||||
targetDatabaseName: "tgt",
|
||||
targetContainerName: "dest",
|
||||
},
|
||||
];
|
||||
|
||||
const jobName = CopyJobUtils.getDefaultJobName(containers);
|
||||
expect(jobName).toBe("src.cont_tgt.dest_1234567890");
|
||||
});
|
||||
});
|
||||
|
||||
describe("constants", () => {
|
||||
it("should have correct COSMOS_SQL_COMPONENT value", () => {
|
||||
expect(CopyJobUtils.COSMOS_SQL_COMPONENT).toBe("CosmosDBSql");
|
||||
});
|
||||
|
||||
it("should have correct COPY_JOB_API_VERSION value", () => {
|
||||
expect(CopyJobUtils.COPY_JOB_API_VERSION).toBe("2025-05-01-preview");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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)}
|
||||
{ContainerCopyMessages.defaultManagedIdentity.description(copyJobState?.target?.account?.name)}
|
||||
<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>
|
||||
);
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 }],
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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 don’t 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 don’t 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 don’t 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>
|
||||
`;
|
||||
@@ -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>
|
||||
`;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
`;
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
`;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
})) || [];
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 = {
|
||||
|
||||
@@ -11,6 +11,7 @@ export enum IdentityType {
|
||||
|
||||
export enum DefaultIdentityType {
|
||||
SystemAssignedIdentity = "systemassignedidentity",
|
||||
FirstPartyIdentity = "FirstPartyIdentity",
|
||||
}
|
||||
|
||||
export enum BackupPolicyType {
|
||||
|
||||
@@ -0,0 +1,611 @@
|
||||
import "@testing-library/jest-dom";
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import React from "react";
|
||||
import { CopyJobActions, CopyJobMigrationType, CopyJobStatusType } from "../../Enums/CopyJobEnums";
|
||||
import { CopyJobType, HandleJobActionClickType } from "../../Types/CopyJobTypes";
|
||||
import CopyJobActionMenu from "./CopyJobActionMenu";
|
||||
|
||||
jest.mock("../../ContainerCopyMessages", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
MonitorJobs: {
|
||||
Columns: {
|
||||
actions: "Actions",
|
||||
},
|
||||
Actions: {
|
||||
pause: "Pause",
|
||||
resume: "Resume",
|
||||
cancel: "Cancel",
|
||||
complete: "Complete",
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe("CopyJobActionMenu", () => {
|
||||
const createMockJob = (overrides: Partial<CopyJobType> = {}): CopyJobType =>
|
||||
({
|
||||
ID: "test-job-id",
|
||||
Mode: CopyJobMigrationType.Offline,
|
||||
Name: "Test Job",
|
||||
Status: CopyJobStatusType.InProgress,
|
||||
CompletionPercentage: 50,
|
||||
Duration: "00:10:30",
|
||||
LastUpdatedTime: "2025-01-01T10:00:00Z",
|
||||
timestamp: Date.now(),
|
||||
Source: {
|
||||
databaseName: "sourceDb",
|
||||
collectionName: "sourceContainer",
|
||||
component: "source",
|
||||
},
|
||||
Destination: {
|
||||
databaseName: "targetDb",
|
||||
collectionName: "targetContainer",
|
||||
component: "destination",
|
||||
},
|
||||
...overrides,
|
||||
}) as CopyJobType;
|
||||
|
||||
const mockHandleClick: HandleJobActionClickType = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("Component Rendering", () => {
|
||||
it("should render the action menu button for active jobs", () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.InProgress });
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
expect(actionButton).toBeInTheDocument();
|
||||
expect(actionButton).toHaveAttribute("aria-label", "Actions");
|
||||
expect(actionButton).toHaveAttribute("title", "Actions");
|
||||
});
|
||||
|
||||
it("should not render anything for completed jobs", () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.Completed });
|
||||
|
||||
const { container } = render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it("should not render anything for cancelled jobs", () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.Cancelled });
|
||||
|
||||
const { container } = render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it("should not render anything for failed jobs", () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.Failed });
|
||||
|
||||
const { container } = render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it("should not render anything for faulted jobs", () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.Faulted });
|
||||
|
||||
const { container } = render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Menu Items for Different Job Statuses", () => {
|
||||
it("should show pause and cancel actions for InProgress jobs", () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.InProgress });
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
expect(screen.getByText("Pause")).toBeInTheDocument();
|
||||
expect(screen.getByText("Cancel")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Resume")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show resume and cancel actions for Paused jobs", () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.Paused });
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
expect(screen.getByText("Resume")).toBeInTheDocument();
|
||||
expect(screen.getByText("Cancel")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Pause")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show pause and cancel actions for Pending jobs", () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.Pending });
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
expect(screen.getByText("Pause")).toBeInTheDocument();
|
||||
expect(screen.getByText("Cancel")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Resume")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show only resume action for Skipped jobs", () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.Skipped });
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
expect(screen.getByText("Resume")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Pause")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Cancel")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show pause and cancel actions for Running jobs", () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.Running });
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
expect(screen.getByText("Pause")).toBeInTheDocument();
|
||||
expect(screen.getByText("Cancel")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Resume")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show pause and cancel actions for Partitioning jobs", () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.Partitioning });
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
expect(screen.getByText("Pause")).toBeInTheDocument();
|
||||
expect(screen.getByText("Cancel")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Resume")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Online Mode Complete Action", () => {
|
||||
it("should show complete action for online InProgress jobs", () => {
|
||||
const job = createMockJob({
|
||||
Status: CopyJobStatusType.InProgress,
|
||||
Mode: CopyJobMigrationType.Online,
|
||||
});
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
expect(screen.getByText("Complete")).toBeInTheDocument();
|
||||
expect(screen.getByText("Pause")).toBeInTheDocument();
|
||||
expect(screen.getByText("Cancel")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show complete action for online Running jobs", () => {
|
||||
const job = createMockJob({
|
||||
Status: CopyJobStatusType.Running,
|
||||
Mode: CopyJobMigrationType.Online,
|
||||
});
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
expect(screen.getByText("Complete")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show complete action for online Partitioning jobs", () => {
|
||||
const job = createMockJob({
|
||||
Status: CopyJobStatusType.Partitioning,
|
||||
Mode: CopyJobMigrationType.Online,
|
||||
});
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
expect(screen.getByText("Complete")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not show complete action for offline jobs", () => {
|
||||
const job = createMockJob({
|
||||
Status: CopyJobStatusType.InProgress,
|
||||
Mode: CopyJobMigrationType.Offline,
|
||||
});
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
expect(screen.queryByText("Complete")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle case-insensitive online mode detection", () => {
|
||||
const job = createMockJob({
|
||||
Status: CopyJobStatusType.InProgress,
|
||||
Mode: "ONLINE",
|
||||
});
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
expect(screen.getByText("Complete")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Action Click Handling", () => {
|
||||
it("should call handleClick when pause action is clicked", () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.InProgress });
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
const pauseButton = screen.getByText("Pause");
|
||||
fireEvent.click(pauseButton);
|
||||
|
||||
expect(mockHandleClick).toHaveBeenCalledWith(job, CopyJobActions.pause, expect.any(Function));
|
||||
});
|
||||
|
||||
it("should call handleClick when cancel action is clicked", () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.InProgress });
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
const cancelButton = screen.getByText("Cancel");
|
||||
fireEvent.click(cancelButton);
|
||||
|
||||
expect(mockHandleClick).toHaveBeenCalledWith(job, CopyJobActions.cancel, expect.any(Function));
|
||||
});
|
||||
|
||||
it("should call handleClick when resume action is clicked", () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.Paused });
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
const resumeButton = screen.getByText("Resume");
|
||||
fireEvent.click(resumeButton);
|
||||
|
||||
expect(mockHandleClick).toHaveBeenCalledWith(job, CopyJobActions.resume, expect.any(Function));
|
||||
});
|
||||
|
||||
it("should call handleClick when complete action is clicked", () => {
|
||||
const job = createMockJob({
|
||||
Status: CopyJobStatusType.InProgress,
|
||||
Mode: CopyJobMigrationType.Online,
|
||||
});
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
const completeButton = screen.getByText("Complete");
|
||||
fireEvent.click(completeButton);
|
||||
|
||||
expect(mockHandleClick).toHaveBeenCalledWith(job, CopyJobActions.complete, expect.any(Function));
|
||||
});
|
||||
});
|
||||
|
||||
describe("Disabled States During Updates", () => {
|
||||
const TestComponentWrapper: React.FC<{
|
||||
job: CopyJobType;
|
||||
initialUpdatingState?: { jobName: string; action: string } | null;
|
||||
}> = ({ job, initialUpdatingState = null }) => {
|
||||
const stateUpdater = React.useState(initialUpdatingState);
|
||||
const setUpdatingJobAction = stateUpdater[1];
|
||||
|
||||
const testHandleClick: HandleJobActionClickType = (job, action, setUpdatingJobActionCallback) => {
|
||||
setUpdatingJobActionCallback({ jobName: job.Name, action });
|
||||
setUpdatingJobAction({ jobName: job.Name, action });
|
||||
};
|
||||
|
||||
return <CopyJobActionMenu job={job} handleClick={testHandleClick} />;
|
||||
};
|
||||
|
||||
it("should disable pause action when job is being paused", async () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.InProgress });
|
||||
|
||||
render(<TestComponentWrapper job={job} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
const pauseButton = screen.getByText("Pause");
|
||||
fireEvent.click(pauseButton);
|
||||
fireEvent.click(actionButton);
|
||||
const pauseButtonAfterClick = screen.getByText("Pause");
|
||||
expect(pauseButtonAfterClick).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not disable actions for different jobs when one is updating", () => {
|
||||
const job1 = createMockJob({ Name: "Job1", Status: CopyJobStatusType.InProgress });
|
||||
const job2 = createMockJob({ Name: "Job2", Status: CopyJobStatusType.InProgress });
|
||||
|
||||
const { rerender } = render(<TestComponentWrapper job={job1} />);
|
||||
let actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
fireEvent.click(screen.getByText("Pause"));
|
||||
rerender(<TestComponentWrapper job={job2} />);
|
||||
|
||||
actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
expect(screen.getByText("Pause")).toBeInTheDocument();
|
||||
expect(screen.getByText("Cancel")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should properly handle multiple action types being disabled for the same job", () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.InProgress });
|
||||
render(<TestComponentWrapper job={job} />);
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
|
||||
fireEvent.click(actionButton);
|
||||
fireEvent.click(screen.getByText("Pause"));
|
||||
|
||||
fireEvent.click(actionButton);
|
||||
fireEvent.click(screen.getByText("Cancel"));
|
||||
|
||||
fireEvent.click(actionButton);
|
||||
expect(screen.getByText("Pause")).toBeInTheDocument();
|
||||
expect(screen.getByText("Cancel")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle complete action disabled state for online jobs", () => {
|
||||
const job = createMockJob({
|
||||
Status: CopyJobStatusType.InProgress,
|
||||
Mode: CopyJobMigrationType.Online,
|
||||
});
|
||||
|
||||
render(<TestComponentWrapper job={job} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
const completeButton = screen.getByText("Complete");
|
||||
fireEvent.click(completeButton);
|
||||
|
||||
fireEvent.click(actionButton);
|
||||
expect(screen.getByText("Complete")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Edge Cases", () => {
|
||||
it("should handle undefined mode gracefully", () => {
|
||||
const job = createMockJob({
|
||||
Status: CopyJobStatusType.InProgress,
|
||||
Mode: undefined as any,
|
||||
});
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
expect(screen.queryByText("Complete")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("Pause")).toBeInTheDocument();
|
||||
expect(screen.getByText("Cancel")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle null mode gracefully", () => {
|
||||
const job = createMockJob({
|
||||
Status: CopyJobStatusType.InProgress,
|
||||
Mode: null as any,
|
||||
});
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
expect(screen.queryByText("Complete")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle empty string mode gracefully", () => {
|
||||
const job = createMockJob({
|
||||
Status: CopyJobStatusType.InProgress,
|
||||
Mode: "",
|
||||
});
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
expect(screen.queryByText("Complete")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should return all base items for unknown status", () => {
|
||||
const job = createMockJob({ Status: "UnknownStatus" as any });
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
expect(screen.getByText("Pause")).toBeInTheDocument();
|
||||
expect(screen.getByText("Cancel")).toBeInTheDocument();
|
||||
expect(screen.getByText("Resume")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Icon and Accessibility", () => {
|
||||
it("should have correct icon and accessibility attributes", () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.InProgress });
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
|
||||
expect(actionButton).toHaveAttribute("aria-label", "Actions");
|
||||
expect(actionButton).toHaveAttribute("title", "Actions");
|
||||
|
||||
const moreIcon = actionButton.querySelector('[data-icon-name="More"]');
|
||||
expect(moreIcon || actionButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should have correct menu item icons", () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.InProgress });
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
expect(screen.getByText("Pause")).toBeInTheDocument();
|
||||
expect(screen.getByText("Cancel")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Component State Management", () => {
|
||||
it("should manage updating job action state correctly", () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.InProgress });
|
||||
const mockHandleClickWithState: HandleJobActionClickType = jest.fn((job, action, setUpdatingJobAction) => {
|
||||
setUpdatingJobAction({ jobName: job.Name, action });
|
||||
});
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClickWithState} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
const pauseButton = screen.getByText("Pause");
|
||||
fireEvent.click(pauseButton);
|
||||
|
||||
expect(mockHandleClickWithState).toHaveBeenCalledWith(job, CopyJobActions.pause, expect.any(Function));
|
||||
});
|
||||
|
||||
it("should handle rapid successive clicks properly", () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.InProgress });
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
const pauseButton = screen.getByText("Pause");
|
||||
fireEvent.click(pauseButton);
|
||||
|
||||
fireEvent.click(actionButton);
|
||||
const pauseButton2 = screen.getByText("Pause");
|
||||
fireEvent.click(pauseButton2);
|
||||
|
||||
fireEvent.click(actionButton);
|
||||
const pauseButton3 = screen.getByText("Pause");
|
||||
fireEvent.click(pauseButton3);
|
||||
|
||||
expect(mockHandleClick).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Integration Tests", () => {
|
||||
it("should work correctly with different job names", () => {
|
||||
const jobWithLongName = createMockJob({
|
||||
Name: "Very Long Job Name That Might Cause UI Issues",
|
||||
Status: CopyJobStatusType.InProgress,
|
||||
});
|
||||
|
||||
render(<CopyJobActionMenu job={jobWithLongName} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
const pauseButton = screen.getByText("Pause");
|
||||
fireEvent.click(pauseButton);
|
||||
|
||||
expect(mockHandleClick).toHaveBeenCalledWith(jobWithLongName, CopyJobActions.pause, expect.any(Function));
|
||||
});
|
||||
|
||||
it("should handle special characters in job names", () => {
|
||||
const jobWithSpecialChars = createMockJob({
|
||||
Name: "Job-Name_With$pecial#Characters!@",
|
||||
Status: CopyJobStatusType.Paused,
|
||||
});
|
||||
|
||||
render(<CopyJobActionMenu job={jobWithSpecialChars} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
const resumeButton = screen.getByText("Resume");
|
||||
fireEvent.click(resumeButton);
|
||||
|
||||
expect(mockHandleClick).toHaveBeenCalledWith(jobWithSpecialChars, CopyJobActions.resume, expect.any(Function));
|
||||
});
|
||||
|
||||
it("should maintain consistent behavior across re-renders", () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.InProgress });
|
||||
const { rerender } = render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
let actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
expect(actionButton).toBeInTheDocument();
|
||||
|
||||
rerender(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
expect(actionButton).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(actionButton);
|
||||
expect(screen.getByText("Pause")).toBeInTheDocument();
|
||||
expect(screen.getByText("Cancel")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle prop changes correctly", () => {
|
||||
const job1 = createMockJob({ Status: CopyJobStatusType.InProgress });
|
||||
const job2 = createMockJob({ Status: CopyJobStatusType.Paused });
|
||||
|
||||
const { rerender } = render(<CopyJobActionMenu job={job1} handleClick={mockHandleClick} />);
|
||||
|
||||
let actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
expect(actionButton).toBeInTheDocument();
|
||||
|
||||
rerender(<CopyJobActionMenu job={job2} handleClick={mockHandleClick} />);
|
||||
|
||||
actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
expect(screen.getByText("Resume")).toBeInTheDocument();
|
||||
expect(screen.getByText("Cancel")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Pause")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Performance and Memory", () => {
|
||||
it("should not create memory leaks with multiple renders", () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.InProgress });
|
||||
const { unmount } = render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
expect(() => unmount()).not.toThrow();
|
||||
});
|
||||
|
||||
it("should handle null/undefined props gracefully", () => {
|
||||
const incompleteJob = {
|
||||
...createMockJob({ Status: CopyJobStatusType.InProgress }),
|
||||
Name: undefined as any,
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
render(<CopyJobActionMenu job={incompleteJob} handleClick={mockHandleClick} />);
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,449 @@
|
||||
import { IColumn } from "@fluentui/react";
|
||||
import "@testing-library/jest-dom";
|
||||
import { render } from "@testing-library/react";
|
||||
import React from "react";
|
||||
import ContainerCopyMessages from "../../ContainerCopyMessages";
|
||||
import { CopyJobStatusType } from "../../Enums/CopyJobEnums";
|
||||
import { CopyJobType, HandleJobActionClickType } from "../../Types/CopyJobTypes";
|
||||
import { getColumns } from "./CopyJobColumns";
|
||||
|
||||
jest.mock("./CopyJobActionMenu", () => {
|
||||
const MockCopyJobActionMenu = ({ job }: { job: CopyJobType }) => {
|
||||
return <div data-testid={`action-menu-${job.Name}`}>Action Menu</div>;
|
||||
};
|
||||
MockCopyJobActionMenu.displayName = "MockCopyJobActionMenu";
|
||||
return MockCopyJobActionMenu;
|
||||
});
|
||||
|
||||
jest.mock("./CopyJobStatusWithIcon", () => {
|
||||
const MockCopyJobStatusWithIcon = ({ status }: { status: CopyJobStatusType }) => {
|
||||
return <div data-testid={`status-icon-${status}`}>Status: {status}</div>;
|
||||
};
|
||||
MockCopyJobStatusWithIcon.displayName = "MockCopyJobStatusWithIcon";
|
||||
return MockCopyJobStatusWithIcon;
|
||||
});
|
||||
|
||||
describe("CopyJobColumns", () => {
|
||||
type OnColumnClickType = IColumn & { onColumnClick: () => void };
|
||||
const mockHandleSort = jest.fn();
|
||||
const mockHandleActionClick: HandleJobActionClickType = jest.fn();
|
||||
|
||||
const mockJob = {
|
||||
ID: "test-job-id",
|
||||
Mode: "Online",
|
||||
Name: "Test Job Name",
|
||||
Status: CopyJobStatusType.InProgress,
|
||||
CompletionPercentage: 75,
|
||||
Duration: "00:05:30",
|
||||
LastUpdatedTime: "2024-12-01T10:30:00Z",
|
||||
timestamp: 1701426600000,
|
||||
Source: {
|
||||
databaseName: "test-source-db",
|
||||
containerName: "test-source-container",
|
||||
component: "CosmosDBSql",
|
||||
},
|
||||
Destination: {
|
||||
databaseName: "test-dest-db",
|
||||
containerName: "test-dest-container",
|
||||
component: "CosmosDBSql",
|
||||
},
|
||||
} as CopyJobType;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("getColumns", () => {
|
||||
it("should return an array of IColumn objects", () => {
|
||||
const columns = getColumns(mockHandleSort, mockHandleActionClick, undefined, false);
|
||||
|
||||
expect(columns).toBeDefined();
|
||||
expect(Array.isArray(columns)).toBe(true);
|
||||
expect(columns.length).toBe(6);
|
||||
|
||||
columns.forEach((column: IColumn) => {
|
||||
expect(column).toHaveProperty("key");
|
||||
expect(column).toHaveProperty("name");
|
||||
expect(column).toHaveProperty("minWidth");
|
||||
expect(column).toHaveProperty("maxWidth");
|
||||
expect(column).toHaveProperty("isResizable");
|
||||
});
|
||||
});
|
||||
|
||||
it("should have correct column keys", () => {
|
||||
const columns = getColumns(mockHandleSort, mockHandleActionClick, undefined, false);
|
||||
|
||||
const expectedKeys = ["LastUpdatedTime", "Name", "Mode", "CompletionPercentage", "CopyJobStatus", "Actions"];
|
||||
const actualKeys = columns.map((column) => column.key);
|
||||
|
||||
expect(actualKeys).toEqual(expectedKeys);
|
||||
});
|
||||
|
||||
it("should have correct column names from ContainerCopyMessages", () => {
|
||||
const columns = getColumns(mockHandleSort, mockHandleActionClick, undefined, false);
|
||||
|
||||
expect(columns[0].name).toBe(ContainerCopyMessages.MonitorJobs.Columns.lastUpdatedTime);
|
||||
expect(columns[1].name).toBe(ContainerCopyMessages.MonitorJobs.Columns.name);
|
||||
expect(columns[2].name).toBe(ContainerCopyMessages.MonitorJobs.Columns.mode);
|
||||
expect(columns[3].name).toBe(ContainerCopyMessages.MonitorJobs.Columns.completionPercentage);
|
||||
expect(columns[4].name).toBe(ContainerCopyMessages.MonitorJobs.Columns.status);
|
||||
expect(columns[5].name).toBe("");
|
||||
});
|
||||
|
||||
it("should configure sortable columns correctly when no sort is applied", () => {
|
||||
const columns = getColumns(mockHandleSort, mockHandleActionClick, undefined, false);
|
||||
|
||||
expect(columns[0].isSorted).toBe(false); // LastUpdatedTime
|
||||
expect(columns[1].isSorted).toBe(false); // Name
|
||||
expect(columns[2].isSorted).toBe(false); // Mode
|
||||
expect(columns[3].isSorted).toBe(false); // CompletionPercentage
|
||||
expect(columns[4].isSorted).toBe(false); // CopyJobStatus
|
||||
});
|
||||
|
||||
it("should configure sorted column correctly when sort is applied", () => {
|
||||
const columns = getColumns(mockHandleSort, mockHandleActionClick, "Name", true);
|
||||
|
||||
expect(columns[1].isSorted).toBe(true);
|
||||
expect(columns[1].isSortedDescending).toBe(true);
|
||||
|
||||
expect(columns[0].isSorted).toBe(false);
|
||||
expect(columns[2].isSorted).toBe(false);
|
||||
expect(columns[3].isSorted).toBe(false);
|
||||
expect(columns[4].isSorted).toBe(false);
|
||||
});
|
||||
|
||||
it("should handle timestamp sorting for LastUpdatedTime column", () => {
|
||||
const columns = getColumns(mockHandleSort, mockHandleActionClick, "timestamp", false);
|
||||
|
||||
expect(columns[0].isSorted).toBe(true);
|
||||
expect(columns[0].isSortedDescending).toBe(false);
|
||||
});
|
||||
|
||||
it("should call handleSort with correct column keys when column headers are clicked", () => {
|
||||
const columns = getColumns(mockHandleSort, mockHandleActionClick, undefined, false);
|
||||
|
||||
(columns[0] as OnColumnClickType).onColumnClick?.();
|
||||
expect(mockHandleSort).toHaveBeenCalledWith("timestamp");
|
||||
|
||||
(columns[1] as OnColumnClickType).onColumnClick();
|
||||
expect(mockHandleSort).toHaveBeenCalledWith("Name");
|
||||
|
||||
(columns[2] as OnColumnClickType).onColumnClick();
|
||||
expect(mockHandleSort).toHaveBeenCalledWith("Mode");
|
||||
|
||||
(columns[3] as OnColumnClickType).onColumnClick();
|
||||
expect(mockHandleSort).toHaveBeenCalledWith("CompletionPercentage");
|
||||
|
||||
(columns[4] as OnColumnClickType).onColumnClick();
|
||||
expect(mockHandleSort).toHaveBeenCalledWith("Status");
|
||||
|
||||
expect(mockHandleSort).toHaveBeenCalledTimes(5);
|
||||
});
|
||||
|
||||
it("should have correct column widths and resizability", () => {
|
||||
const columns = getColumns(mockHandleSort, mockHandleActionClick, undefined, false);
|
||||
|
||||
expect(columns[0].minWidth).toBe(140); // LastUpdatedTime
|
||||
expect(columns[0].maxWidth).toBe(300);
|
||||
expect(columns[0].isResizable).toBe(true);
|
||||
|
||||
expect(columns[1].minWidth).toBe(140); // Name
|
||||
expect(columns[1].maxWidth).toBe(300);
|
||||
expect(columns[1].isResizable).toBe(true);
|
||||
|
||||
expect(columns[2].minWidth).toBe(90); // Mode
|
||||
expect(columns[2].maxWidth).toBe(200);
|
||||
expect(columns[2].isResizable).toBe(true);
|
||||
|
||||
expect(columns[3].minWidth).toBe(110); // CompletionPercentage
|
||||
expect(columns[3].maxWidth).toBe(200);
|
||||
expect(columns[3].isResizable).toBe(true);
|
||||
|
||||
expect(columns[4].minWidth).toBe(130); // CopyJobStatus
|
||||
expect(columns[4].maxWidth).toBe(200);
|
||||
expect(columns[4].isResizable).toBe(true);
|
||||
|
||||
expect(columns[5].minWidth).toBe(80); // Actions
|
||||
expect(columns[5].maxWidth).toBe(200);
|
||||
expect(columns[5].isResizable).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Column Render Functions", () => {
|
||||
let columns: IColumn[];
|
||||
|
||||
beforeEach(() => {
|
||||
columns = getColumns(mockHandleSort, mockHandleActionClick, undefined, false);
|
||||
});
|
||||
|
||||
describe("Name column render function", () => {
|
||||
it("should render job name with correct styling", () => {
|
||||
const nameColumn = columns.find((col) => col.key === "Name");
|
||||
expect(nameColumn?.onRender).toBeDefined();
|
||||
|
||||
const rendered = nameColumn?.onRender?.(mockJob);
|
||||
const { container } = render(<div>{rendered}</div>);
|
||||
|
||||
const jobNameElement = container.querySelector(".jobNameLink");
|
||||
expect(jobNameElement).toBeInTheDocument();
|
||||
expect(jobNameElement).toHaveTextContent("Test Job Name");
|
||||
});
|
||||
|
||||
it("should handle empty job name", () => {
|
||||
const nameColumn = columns.find((col) => col.key === "Name");
|
||||
const jobWithEmptyName = { ...mockJob, Name: "" };
|
||||
|
||||
const rendered = nameColumn?.onRender?.(jobWithEmptyName);
|
||||
const { container } = render(<div>{rendered}</div>);
|
||||
|
||||
const jobNameElement = container.querySelector(".jobNameLink");
|
||||
expect(jobNameElement).toBeInTheDocument();
|
||||
expect(jobNameElement).toHaveTextContent("");
|
||||
});
|
||||
|
||||
it("should handle special characters in job name", () => {
|
||||
const nameColumn = columns.find((col) => col.key === "Name");
|
||||
const jobWithSpecialName = { ...mockJob, Name: "Test & <Job> 'Name' \"With\" Special Characters" };
|
||||
|
||||
const rendered = nameColumn?.onRender?.(jobWithSpecialName);
|
||||
const { container } = render(<div>{rendered}</div>);
|
||||
|
||||
const jobNameElement = container.querySelector(".jobNameLink");
|
||||
expect(jobNameElement).toBeInTheDocument();
|
||||
expect(jobNameElement).toHaveTextContent("Test & <Job> 'Name' \"With\" Special Characters");
|
||||
});
|
||||
});
|
||||
|
||||
describe("CompletionPercentage column render function", () => {
|
||||
it("should render completion percentage with % symbol", () => {
|
||||
const completionColumn = columns.find((col) => col.key === "CompletionPercentage");
|
||||
expect(completionColumn?.onRender).toBeDefined();
|
||||
|
||||
const result = completionColumn?.onRender?.(mockJob);
|
||||
expect(result).toBe("75%");
|
||||
});
|
||||
|
||||
it("should handle 0% completion", () => {
|
||||
const completionColumn = columns.find((col) => col.key === "CompletionPercentage");
|
||||
const jobWithZeroCompletion = { ...mockJob, CompletionPercentage: 0 };
|
||||
|
||||
const result = completionColumn?.onRender?.(jobWithZeroCompletion);
|
||||
expect(result).toBe("0%");
|
||||
});
|
||||
|
||||
it("should handle 100% completion", () => {
|
||||
const completionColumn = columns.find((col) => col.key === "CompletionPercentage");
|
||||
const jobWithFullCompletion = { ...mockJob, CompletionPercentage: 100 };
|
||||
|
||||
const result = completionColumn?.onRender?.(jobWithFullCompletion);
|
||||
expect(result).toBe("100%");
|
||||
});
|
||||
|
||||
it("should handle decimal completion percentages", () => {
|
||||
const completionColumn = columns.find((col) => col.key === "CompletionPercentage");
|
||||
const jobWithDecimalCompletion = { ...mockJob, CompletionPercentage: 75.5 };
|
||||
|
||||
const result = completionColumn?.onRender?.(jobWithDecimalCompletion);
|
||||
expect(result).toBe("75.5%");
|
||||
});
|
||||
|
||||
it("should handle negative completion percentages", () => {
|
||||
const completionColumn = columns.find((col) => col.key === "CompletionPercentage");
|
||||
const jobWithNegativeCompletion = { ...mockJob, CompletionPercentage: -5 };
|
||||
|
||||
const result = completionColumn?.onRender?.(jobWithNegativeCompletion);
|
||||
expect(result).toBe("-5%");
|
||||
});
|
||||
});
|
||||
|
||||
describe("CopyJobStatus column render function", () => {
|
||||
it("should render CopyJobStatusWithIcon component", () => {
|
||||
const statusColumn = columns.find((col) => col.key === "CopyJobStatus");
|
||||
expect(statusColumn?.onRender).toBeDefined();
|
||||
|
||||
const rendered = statusColumn?.onRender?.(mockJob);
|
||||
const { container } = render(<div>{rendered}</div>);
|
||||
|
||||
const statusIcon = container.querySelector(`[data-testid="status-icon-${mockJob.Status}"]`);
|
||||
expect(statusIcon).toBeInTheDocument();
|
||||
expect(statusIcon).toHaveTextContent(`Status: ${mockJob.Status}`);
|
||||
});
|
||||
|
||||
it("should handle different job statuses", () => {
|
||||
const statusColumn = columns.find((col) => col.key === "CopyJobStatus");
|
||||
|
||||
Object.values(CopyJobStatusType).forEach((status) => {
|
||||
const jobWithStatus = { ...mockJob, Status: status };
|
||||
const rendered = statusColumn?.onRender?.(jobWithStatus);
|
||||
const { container } = render(<div>{rendered}</div>);
|
||||
|
||||
const statusIcon = container.querySelector(`[data-testid="status-icon-${status}"]`);
|
||||
expect(statusIcon).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Actions column render function", () => {
|
||||
it("should render CopyJobActionMenu component", () => {
|
||||
const actionsColumn = columns.find((col) => col.key === "Actions");
|
||||
expect(actionsColumn?.onRender).toBeDefined();
|
||||
|
||||
const rendered = actionsColumn?.onRender?.(mockJob);
|
||||
const { container } = render(<div>{rendered}</div>);
|
||||
|
||||
const actionMenu = container.querySelector(`[data-testid="action-menu-${mockJob.Name}"]`);
|
||||
expect(actionMenu).toBeInTheDocument();
|
||||
expect(actionMenu).toHaveTextContent("Action Menu");
|
||||
});
|
||||
|
||||
it("should pass correct props to CopyJobActionMenu", () => {
|
||||
const actionsColumn = columns.find((col) => col.key === "Actions");
|
||||
const rendered = actionsColumn?.onRender?.(mockJob);
|
||||
|
||||
expect(rendered).toBeDefined();
|
||||
expect(React.isValidElement(rendered)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Column Field Names", () => {
|
||||
it("should have correct fieldName properties", () => {
|
||||
const columns = getColumns(mockHandleSort, mockHandleActionClick, undefined, false);
|
||||
|
||||
expect(columns[0].fieldName).toBe("LastUpdatedTime");
|
||||
expect(columns[1].fieldName).toBe("Name");
|
||||
expect(columns[2].fieldName).toBe("Mode");
|
||||
expect(columns[3].fieldName).toBe("CompletionPercentage");
|
||||
expect(columns[4].fieldName).toBe("Status");
|
||||
expect(columns[5].fieldName).toBeUndefined(); // Actions column doesn't have fieldName
|
||||
});
|
||||
});
|
||||
|
||||
describe("Different Sort Configurations", () => {
|
||||
it("should handle ascending sort", () => {
|
||||
const columns = getColumns(mockHandleSort, mockHandleActionClick, "Name", false);
|
||||
|
||||
const nameColumn = columns.find((col) => col.key === "Name");
|
||||
expect(nameColumn?.isSorted).toBe(true);
|
||||
expect(nameColumn?.isSortedDescending).toBe(false);
|
||||
});
|
||||
|
||||
it("should handle descending sort", () => {
|
||||
const columns = getColumns(mockHandleSort, mockHandleActionClick, "Mode", true);
|
||||
|
||||
const modeColumn = columns.find((col) => col.key === "Mode");
|
||||
expect(modeColumn?.isSorted).toBe(true);
|
||||
expect(modeColumn?.isSortedDescending).toBe(true);
|
||||
});
|
||||
|
||||
it("should handle sort on CompletionPercentage column", () => {
|
||||
const columns = getColumns(mockHandleSort, mockHandleActionClick, "CompletionPercentage", false);
|
||||
|
||||
const completionColumn = columns.find((col) => col.key === "CompletionPercentage");
|
||||
expect(completionColumn?.isSorted).toBe(true);
|
||||
expect(completionColumn?.isSortedDescending).toBe(false);
|
||||
|
||||
const nameColumn = columns.find((col) => col.key === "Name");
|
||||
expect(nameColumn?.isSorted).toBe(false);
|
||||
});
|
||||
|
||||
it("should handle sort on Status column", () => {
|
||||
const columns = getColumns(mockHandleSort, mockHandleActionClick, "Status", true);
|
||||
|
||||
const statusColumn = columns.find((col) => col.key === "CopyJobStatus");
|
||||
expect(statusColumn?.isSorted).toBe(true);
|
||||
expect(statusColumn?.isSortedDescending).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Edge Cases", () => {
|
||||
it("should handle undefined sortedColumnKey", () => {
|
||||
const columns = getColumns(mockHandleSort, mockHandleActionClick, undefined, false);
|
||||
|
||||
const sortableColumns = columns.filter((col) => col.key !== "Actions");
|
||||
sortableColumns.forEach((column) => {
|
||||
expect(column.isSorted).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle null job object in render functions gracefully", () => {
|
||||
const columns = getColumns(mockHandleSort, mockHandleActionClick, undefined, false);
|
||||
|
||||
const nameColumn = columns.find((col) => col.key === "Name");
|
||||
expect(() => {
|
||||
nameColumn?.onRender?.(null as any);
|
||||
}).toThrow();
|
||||
|
||||
const completionColumn = columns.find((col) => col.key === "CompletionPercentage");
|
||||
expect(() => {
|
||||
completionColumn?.onRender?.(null as any);
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it("should handle job object with missing properties", () => {
|
||||
const incompleteJob = {
|
||||
Name: "Incomplete Job",
|
||||
} as CopyJobType;
|
||||
|
||||
const columns = getColumns(mockHandleSort, mockHandleActionClick, undefined, false);
|
||||
|
||||
const nameColumn = columns.find((col) => col.key === "Name");
|
||||
const rendered = nameColumn?.onRender?.(incompleteJob);
|
||||
const { container } = render(<div>{rendered}</div>);
|
||||
|
||||
const jobNameElement = container.querySelector(".jobNameLink");
|
||||
expect(jobNameElement).toHaveTextContent("Incomplete Job");
|
||||
});
|
||||
|
||||
it("should handle unknown sortedColumnKey", () => {
|
||||
const columns = getColumns(mockHandleSort, mockHandleActionClick, "UnknownColumn", false);
|
||||
|
||||
const sortableColumns = columns.filter((col) => col.key !== "Actions");
|
||||
sortableColumns.forEach((column) => {
|
||||
expect(column.isSorted).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Accessibility", () => {
|
||||
it("should have Actions column without name for accessibility", () => {
|
||||
const columns = getColumns(mockHandleSort, mockHandleActionClick, undefined, false);
|
||||
|
||||
const actionsColumn = columns.find((col) => col.key === "Actions");
|
||||
expect(actionsColumn?.name).toBe("");
|
||||
});
|
||||
|
||||
it("should maintain column structure for screen readers", () => {
|
||||
const columns = getColumns(mockHandleSort, mockHandleActionClick, undefined, false);
|
||||
|
||||
const columnsWithNames = columns.filter((col) => col.key !== "Actions");
|
||||
columnsWithNames.forEach((column) => {
|
||||
expect(column.name).toBeTruthy();
|
||||
expect(typeof column.name).toBe("string");
|
||||
expect(column.name.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Function References", () => {
|
||||
it("should maintain function reference stability", () => {
|
||||
const columns1 = getColumns(mockHandleSort, mockHandleActionClick, undefined, false);
|
||||
const columns2 = getColumns(mockHandleSort, mockHandleActionClick, undefined, false);
|
||||
|
||||
(columns1[0] as OnColumnClickType).onColumnClick?.();
|
||||
(columns2[0] as OnColumnClickType).onColumnClick?.();
|
||||
|
||||
expect(mockHandleSort).toHaveBeenCalledTimes(2);
|
||||
expect(mockHandleSort).toHaveBeenNthCalledWith(1, "timestamp");
|
||||
expect(mockHandleSort).toHaveBeenNthCalledWith(2, "timestamp");
|
||||
});
|
||||
|
||||
it("should call handleActionClick when action menu is rendered", () => {
|
||||
const columns = getColumns(mockHandleSort, mockHandleActionClick, undefined, false);
|
||||
const actionsColumn = columns.find((col) => col.key === "Actions");
|
||||
|
||||
const rendered = actionsColumn?.onRender?.(mockJob);
|
||||
expect(React.isValidElement(rendered)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,383 @@
|
||||
import "@testing-library/jest-dom";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import React from "react";
|
||||
import { CopyJobStatusType } from "../../Enums/CopyJobEnums";
|
||||
import { CopyJobType } from "../../Types/CopyJobTypes";
|
||||
import CopyJobDetails from "./CopyJobDetails";
|
||||
|
||||
jest.mock("./CopyJobStatusWithIcon", () => {
|
||||
const MockCopyJobStatusWithIcon = ({ status }: { status: CopyJobStatusType }) => {
|
||||
return <span data-testid="copy-job-status-icon">{status}</span>;
|
||||
};
|
||||
MockCopyJobStatusWithIcon.displayName = "MockCopyJobStatusWithIcon";
|
||||
return MockCopyJobStatusWithIcon;
|
||||
});
|
||||
|
||||
jest.mock("../../ContainerCopyMessages", () => ({
|
||||
errorTitle: "Error Details",
|
||||
sourceDatabaseLabel: "Source Database",
|
||||
sourceContainerLabel: "Source Container",
|
||||
targetDatabaseLabel: "Destination Database",
|
||||
targetContainerLabel: "Destination Container",
|
||||
sourceAccountLabel: "Source Account",
|
||||
MonitorJobs: {
|
||||
Columns: {
|
||||
lastUpdatedTime: "Date & time",
|
||||
status: "Status",
|
||||
mode: "Mode",
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe("CopyJobDetails", () => {
|
||||
const mockBasicJob: CopyJobType = {
|
||||
ID: "test-job-1",
|
||||
Mode: "Offline",
|
||||
Name: "test-job-1",
|
||||
Status: CopyJobStatusType.InProgress,
|
||||
CompletionPercentage: 50,
|
||||
Duration: "10 minutes",
|
||||
LastUpdatedTime: "2024-01-01T10:00:00Z",
|
||||
timestamp: 1704110400000,
|
||||
Source: {
|
||||
component: "CosmosDBSql",
|
||||
databaseName: "sourceDb",
|
||||
containerName: "sourceContainer",
|
||||
remoteAccountName: "sourceAccount",
|
||||
},
|
||||
Destination: {
|
||||
component: "CosmosDBSql",
|
||||
databaseName: "targetDb",
|
||||
containerName: "targetContainer",
|
||||
remoteAccountName: "targetAccount",
|
||||
},
|
||||
};
|
||||
|
||||
const mockJobWithError: CopyJobType = {
|
||||
...mockBasicJob,
|
||||
ID: "test-job-error",
|
||||
Status: CopyJobStatusType.Failed,
|
||||
Error: {
|
||||
message: "Failed to connect to source database",
|
||||
code: "CONNECTION_ERROR",
|
||||
},
|
||||
};
|
||||
|
||||
const mockJobWithNullValues: CopyJobType = {
|
||||
...mockBasicJob,
|
||||
ID: "test-job-null",
|
||||
Source: {
|
||||
component: "CosmosDBSql",
|
||||
databaseName: undefined,
|
||||
containerName: undefined,
|
||||
remoteAccountName: undefined,
|
||||
},
|
||||
Destination: {
|
||||
component: "CosmosDBSql",
|
||||
databaseName: undefined,
|
||||
containerName: undefined,
|
||||
remoteAccountName: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
describe("Basic Rendering", () => {
|
||||
it("renders the component with correct structure", () => {
|
||||
render(<CopyJobDetails job={mockBasicJob} />);
|
||||
|
||||
const container = screen.getByTestId("copy-job-details");
|
||||
expect(container).toBeInTheDocument();
|
||||
expect(container).toHaveClass("copyJobDetailsContainer");
|
||||
});
|
||||
|
||||
it("displays job details without error when no error exists", () => {
|
||||
render(<CopyJobDetails job={mockBasicJob} />);
|
||||
|
||||
expect(screen.queryByTestId("error-stack")).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId("selectedcollection-stack")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders all required job information fields", () => {
|
||||
render(<CopyJobDetails job={mockBasicJob} />);
|
||||
|
||||
expect(screen.getByText("Date & time")).toBeInTheDocument();
|
||||
expect(screen.getByText("2024-01-01T10:00:00Z")).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText("Source Account")).toBeInTheDocument();
|
||||
expect(screen.getByText("sourceAccount")).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText("Mode")).toBeInTheDocument();
|
||||
expect(screen.getByText("Offline")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders the DetailsList with correct job data", () => {
|
||||
render(<CopyJobDetails job={mockBasicJob} />);
|
||||
|
||||
expect(screen.getByText("sourceDb")).toBeInTheDocument();
|
||||
expect(screen.getByText("sourceContainer")).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText("targetDb")).toBeInTheDocument();
|
||||
expect(screen.getByText("targetContainer")).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByTestId("copy-job-status-icon")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("copy-job-status-icon")).toHaveTextContent("InProgress");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Error Handling", () => {
|
||||
it("displays error section when job has error", () => {
|
||||
render(<CopyJobDetails job={mockJobWithError} />);
|
||||
|
||||
const errorStack = screen.getByTestId("error-stack");
|
||||
expect(errorStack).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText("Error Details")).toBeInTheDocument();
|
||||
expect(screen.getByText("Failed to connect to source database")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not display error section when job has no error", () => {
|
||||
render(<CopyJobDetails job={mockBasicJob} />);
|
||||
|
||||
expect(screen.queryByTestId("error-stack")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Error Details")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Null/Undefined Value Handling", () => {
|
||||
it("displays 'N/A' for null or undefined source values", () => {
|
||||
render(<CopyJobDetails job={mockJobWithNullValues} />);
|
||||
|
||||
const nATexts = screen.getAllByText("N/A");
|
||||
expect(nATexts).toHaveLength(4);
|
||||
});
|
||||
|
||||
it("handles null remote account name gracefully", () => {
|
||||
render(<CopyJobDetails job={mockJobWithNullValues} />);
|
||||
expect(screen.getByTestId("copy-job-details")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("handles empty status gracefully", () => {
|
||||
const jobWithEmptyStatus: CopyJobType = {
|
||||
...mockBasicJob,
|
||||
Status: "" as CopyJobStatusType,
|
||||
};
|
||||
|
||||
render(<CopyJobDetails job={jobWithEmptyStatus} />);
|
||||
|
||||
expect(screen.getByTestId("copy-job-status-icon")).toHaveTextContent("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Different Job Statuses", () => {
|
||||
const statusTestCases = [
|
||||
CopyJobStatusType.Pending,
|
||||
CopyJobStatusType.Running,
|
||||
CopyJobStatusType.Paused,
|
||||
CopyJobStatusType.Completed,
|
||||
CopyJobStatusType.Failed,
|
||||
CopyJobStatusType.Cancelled,
|
||||
];
|
||||
|
||||
statusTestCases.forEach((status) => {
|
||||
it(`renders correctly for ${status} status`, () => {
|
||||
const jobWithStatus: CopyJobType = {
|
||||
...mockBasicJob,
|
||||
Status: status,
|
||||
};
|
||||
|
||||
render(<CopyJobDetails job={jobWithStatus} />);
|
||||
|
||||
expect(screen.getByTestId("copy-job-status-icon")).toHaveTextContent(status);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Component Memoization", () => {
|
||||
it("re-renders when job ID changes", () => {
|
||||
render(<CopyJobDetails job={mockBasicJob} />);
|
||||
|
||||
expect(screen.getByText(CopyJobStatusType.InProgress)).toBeInTheDocument();
|
||||
|
||||
const updatedJob: CopyJobType = {
|
||||
...mockBasicJob,
|
||||
Status: CopyJobStatusType.Completed,
|
||||
};
|
||||
|
||||
render(<CopyJobDetails job={updatedJob} />);
|
||||
|
||||
expect(screen.getByText(CopyJobStatusType.Completed)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("re-renders when error changes", () => {
|
||||
const { rerender } = render(<CopyJobDetails job={mockBasicJob} />);
|
||||
|
||||
expect(screen.queryByTestId("error-stack")).not.toBeInTheDocument();
|
||||
|
||||
rerender(<CopyJobDetails job={mockJobWithError} />);
|
||||
|
||||
expect(screen.getByTestId("error-stack")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not re-render when other props change but ID and Error stay same", () => {
|
||||
const jobWithSameIdAndError = {
|
||||
...mockBasicJob,
|
||||
Mode: "Online",
|
||||
CompletionPercentage: 75,
|
||||
};
|
||||
|
||||
const { rerender } = render(<CopyJobDetails job={mockBasicJob} />);
|
||||
rerender(<CopyJobDetails job={jobWithSameIdAndError} />);
|
||||
expect(screen.getByTestId("copy-job-details")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Data Transformation", () => {
|
||||
it("correctly transforms job data for DetailsList items", () => {
|
||||
render(<CopyJobDetails job={mockBasicJob} />);
|
||||
expect(screen.getByText("sourceContainer")).toBeInTheDocument();
|
||||
expect(screen.getByText("sourceDb")).toBeInTheDocument();
|
||||
expect(screen.getByText("targetContainer")).toBeInTheDocument();
|
||||
expect(screen.getByText("targetDb")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("copy-job-status-icon")).toHaveTextContent("InProgress");
|
||||
});
|
||||
|
||||
it("handles complex job data structure", () => {
|
||||
const complexJob: CopyJobType = {
|
||||
...mockBasicJob,
|
||||
Source: {
|
||||
component: "CosmosDBSql",
|
||||
databaseName: "complex-source-db-with-hyphens",
|
||||
containerName: "complex_source_container_with_underscores",
|
||||
remoteAccountName: "complex.source.account",
|
||||
},
|
||||
Destination: {
|
||||
component: "CosmosDBSql",
|
||||
databaseName: "complex-target-db-with-hyphens",
|
||||
containerName: "complex_target_container_with_underscores",
|
||||
remoteAccountName: "complex.target.account",
|
||||
},
|
||||
};
|
||||
|
||||
render(<CopyJobDetails job={complexJob} />);
|
||||
|
||||
expect(screen.getByText("complex-source-db-with-hyphens")).toBeInTheDocument();
|
||||
expect(screen.getByText("complex_source_container_with_underscores")).toBeInTheDocument();
|
||||
expect(screen.getByText("complex-target-db-with-hyphens")).toBeInTheDocument();
|
||||
expect(screen.getByText("complex_target_container_with_underscores")).toBeInTheDocument();
|
||||
expect(screen.getByText("complex.source.account")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("DetailsList Configuration", () => {
|
||||
it("configures DetailsList with correct layout mode", () => {
|
||||
render(<CopyJobDetails job={mockBasicJob} />);
|
||||
expect(screen.getByText("sourceContainer")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders all expected column data", () => {
|
||||
render(<CopyJobDetails job={mockBasicJob} />);
|
||||
expect(screen.getByText("sourceDb")).toBeInTheDocument();
|
||||
expect(screen.getByText("sourceContainer")).toBeInTheDocument();
|
||||
expect(screen.getByText("targetDb")).toBeInTheDocument();
|
||||
expect(screen.getByText("targetContainer")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("copy-job-status-icon")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Accessibility", () => {
|
||||
it("has proper data-testid attributes", () => {
|
||||
render(<CopyJobDetails job={mockJobWithError} />);
|
||||
|
||||
expect(screen.getByTestId("copy-job-details")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("error-stack")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("selectedcollection-stack")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders semantic HTML structure", () => {
|
||||
render(<CopyJobDetails job={mockBasicJob} />);
|
||||
|
||||
const container = screen.getByTestId("copy-job-details");
|
||||
expect(container).toBeInTheDocument();
|
||||
|
||||
const nestedStack = screen.getByTestId("selectedcollection-stack");
|
||||
expect(nestedStack).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("CSS and Styling", () => {
|
||||
it("applies correct CSS classes", () => {
|
||||
render(<CopyJobDetails job={mockBasicJob} />);
|
||||
|
||||
const container = screen.getByTestId("copy-job-details");
|
||||
expect(container).toHaveClass("copyJobDetailsContainer");
|
||||
});
|
||||
|
||||
it("applies correct styling to error text", () => {
|
||||
render(<CopyJobDetails job={mockJobWithError} />);
|
||||
|
||||
const errorText = screen.getByText("Failed to connect to source database");
|
||||
expect(errorText).toHaveStyle({ whiteSpace: "pre-wrap" });
|
||||
});
|
||||
|
||||
it("applies bold styling to heading texts", () => {
|
||||
render(<CopyJobDetails job={mockBasicJob} />);
|
||||
|
||||
const dateTimeHeading = screen.getByText("Date & time");
|
||||
const sourceAccountHeading = screen.getByText("Source Account");
|
||||
const modeHeading = screen.getByText("Mode");
|
||||
|
||||
expect(dateTimeHeading).toHaveClass("bold");
|
||||
expect(sourceAccountHeading).toHaveClass("bold");
|
||||
expect(modeHeading).toHaveClass("bold");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Edge Cases", () => {
|
||||
it("handles job with minimal required data", () => {
|
||||
const minimalJob = {
|
||||
ID: "minimal",
|
||||
Mode: "",
|
||||
Name: "",
|
||||
Status: CopyJobStatusType.Pending,
|
||||
CompletionPercentage: 0,
|
||||
Duration: "",
|
||||
LastUpdatedTime: "",
|
||||
timestamp: 0,
|
||||
Source: {
|
||||
component: "CosmosDBSql",
|
||||
},
|
||||
Destination: {
|
||||
component: "CosmosDBSql",
|
||||
},
|
||||
} as CopyJobType;
|
||||
|
||||
render(<CopyJobDetails job={minimalJob} />);
|
||||
|
||||
expect(screen.getByTestId("copy-job-details")).toBeInTheDocument();
|
||||
expect(screen.getAllByText("N/A")).toHaveLength(4);
|
||||
});
|
||||
|
||||
it("handles very long text values", () => {
|
||||
const longTextJob: CopyJobType = {
|
||||
...mockBasicJob,
|
||||
Source: {
|
||||
...mockBasicJob.Source,
|
||||
databaseName: "very-long-database-name-that-might-cause-layout-issues-in-the-ui-component",
|
||||
containerName: "very-long-container-name-that-might-cause-layout-issues-in-the-ui-component",
|
||||
remoteAccountName: "very-long-account-name-that-might-cause-layout-issues-in-the-ui-component",
|
||||
},
|
||||
Error: {
|
||||
message:
|
||||
"This is a very long error message that contains multiple sentences and might span several lines when displayed in the user interface. It should handle line breaks and maintain readability even with extensive content.",
|
||||
code: "LONG_ERROR",
|
||||
},
|
||||
};
|
||||
|
||||
render(<CopyJobDetails job={longTextJob} />);
|
||||
|
||||
expect(
|
||||
screen.getByText("very-long-database-name-that-might-cause-layout-issues-in-the-ui-component"),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText(/This is a very long error message/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,162 @@
|
||||
import "@testing-library/jest-dom";
|
||||
import { render } from "@testing-library/react";
|
||||
import React from "react";
|
||||
import { CopyJobStatusType } from "../../Enums/CopyJobEnums";
|
||||
import CopyJobStatusWithIcon from "./CopyJobStatusWithIcon";
|
||||
|
||||
jest.mock("@fluentui/react", () => ({
|
||||
...jest.requireActual("@fluentui/react"),
|
||||
getTheme: () => ({
|
||||
semanticColors: {
|
||||
bodySubtext: "#666666",
|
||||
errorIcon: "#d13438",
|
||||
successIcon: "#107c10",
|
||||
},
|
||||
palette: {
|
||||
themePrimary: "#0078d4",
|
||||
},
|
||||
}),
|
||||
mergeStyles: () => "mocked-styles",
|
||||
mergeStyleSets: (styleSet: any) => {
|
||||
const result: any = {};
|
||||
Object.keys(styleSet).forEach((key) => {
|
||||
result[key] = "mocked-style-" + key;
|
||||
});
|
||||
return result;
|
||||
},
|
||||
}));
|
||||
|
||||
describe("CopyJobStatusWithIcon", () => {
|
||||
describe("Static Icon Status Types - Snapshot Tests", () => {
|
||||
const staticIconStatuses = [
|
||||
CopyJobStatusType.Pending,
|
||||
CopyJobStatusType.Paused,
|
||||
CopyJobStatusType.Skipped,
|
||||
CopyJobStatusType.Cancelled,
|
||||
CopyJobStatusType.Failed,
|
||||
CopyJobStatusType.Faulted,
|
||||
CopyJobStatusType.Completed,
|
||||
];
|
||||
|
||||
test.each(staticIconStatuses)("renders %s status correctly", (status) => {
|
||||
const { container } = render(<CopyJobStatusWithIcon status={status} />);
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Spinner Status Types", () => {
|
||||
const spinnerStatuses = [CopyJobStatusType.Running, CopyJobStatusType.InProgress, CopyJobStatusType.Partitioning];
|
||||
|
||||
test.each(spinnerStatuses)("renders %s with spinner and expected text", (status) => {
|
||||
const { container } = render(<CopyJobStatusWithIcon status={status} />);
|
||||
|
||||
const spinner = container.querySelector('[class*="ms-Spinner"]');
|
||||
expect(spinner).toBeInTheDocument();
|
||||
expect(container).toHaveTextContent("In Progress");
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("PropTypes Validation", () => {
|
||||
it("has correct display name", () => {
|
||||
expect(CopyJobStatusWithIcon.displayName).toBe("CopyJobStatusWithIcon");
|
||||
});
|
||||
it("accepts all valid CopyJobStatusType values", () => {
|
||||
const allStatuses = Object.values(CopyJobStatusType);
|
||||
|
||||
allStatuses.forEach((status) => {
|
||||
expect(() => {
|
||||
render(<CopyJobStatusWithIcon status={status} />);
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Accessibility", () => {
|
||||
it("provides proper aria-label for icon elements", () => {
|
||||
const { container } = render(<CopyJobStatusWithIcon status={CopyJobStatusType.Failed} />);
|
||||
|
||||
const icon = container.querySelector('[class*="ms-Icon"]');
|
||||
expect(icon).toHaveAttribute("aria-label", CopyJobStatusType.Failed);
|
||||
});
|
||||
|
||||
it("provides meaningful text content for screen readers", () => {
|
||||
const { container } = render(<CopyJobStatusWithIcon status={CopyJobStatusType.InProgress} />);
|
||||
|
||||
expect(container).toHaveTextContent("In Progress");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Icon and Status Mapping", () => {
|
||||
it("renders correct status text based on mapping", () => {
|
||||
const statusMappings = [
|
||||
{ status: CopyJobStatusType.Pending, expectedText: "Pending" },
|
||||
{ status: CopyJobStatusType.Paused, expectedText: "Paused" },
|
||||
{ status: CopyJobStatusType.Failed, expectedText: "Failed" },
|
||||
{ status: CopyJobStatusType.Completed, expectedText: "Completed" },
|
||||
{ status: CopyJobStatusType.Running, expectedText: "In Progress" },
|
||||
];
|
||||
|
||||
statusMappings.forEach(({ status, expectedText }) => {
|
||||
const { container, unmount } = render(<CopyJobStatusWithIcon status={status} />);
|
||||
expect(container).toHaveTextContent(expectedText);
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
it("renders icons for static status types", () => {
|
||||
const staticStatuses = [
|
||||
CopyJobStatusType.Pending,
|
||||
CopyJobStatusType.Paused,
|
||||
CopyJobStatusType.Failed,
|
||||
CopyJobStatusType.Completed,
|
||||
];
|
||||
|
||||
staticStatuses.forEach((status) => {
|
||||
const { container, unmount } = render(<CopyJobStatusWithIcon status={status} />);
|
||||
const icon = container.querySelector('[class*="ms-Icon"]');
|
||||
const spinner = container.querySelector('[class*="ms-Spinner"]');
|
||||
|
||||
expect(icon).toBeInTheDocument();
|
||||
expect(spinner).not.toBeInTheDocument();
|
||||
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
it("renders spinners for progress status types", () => {
|
||||
const progressStatuses = [
|
||||
CopyJobStatusType.Running,
|
||||
CopyJobStatusType.InProgress,
|
||||
CopyJobStatusType.Partitioning,
|
||||
];
|
||||
|
||||
progressStatuses.forEach((status) => {
|
||||
const { container, unmount } = render(<CopyJobStatusWithIcon status={status} />);
|
||||
const icon = container.querySelector('[class*="ms-Icon"]');
|
||||
const spinner = container.querySelector('[class*="ms-Spinner"]');
|
||||
|
||||
expect(spinner).toBeInTheDocument();
|
||||
expect(icon).not.toBeInTheDocument();
|
||||
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Performance", () => {
|
||||
it("does not cause unnecessary re-renders with same props", () => {
|
||||
const renderSpy = jest.fn();
|
||||
const TestWrapper = ({ status }: { status: CopyJobStatusType }) => {
|
||||
renderSpy();
|
||||
return <CopyJobStatusWithIcon status={status} />;
|
||||
};
|
||||
|
||||
const { rerender } = render(<TestWrapper status={CopyJobStatusType.Pending} />);
|
||||
expect(renderSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
rerender(<TestWrapper status={CopyJobStatusType.Pending} />);
|
||||
expect(renderSpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
jest.mock("../../Actions/CopyJobActions");
|
||||
|
||||
import "@testing-library/jest-dom";
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import React from "react";
|
||||
import * as Actions from "../../Actions/CopyJobActions";
|
||||
import ContainerCopyMessages from "../../ContainerCopyMessages";
|
||||
import CopyJobsNotFound from "./CopyJobs.NotFound";
|
||||
|
||||
describe("CopyJobsNotFound", () => {
|
||||
let mockExplorer: Explorer;
|
||||
|
||||
beforeEach(() => {
|
||||
mockExplorer = {} as Explorer;
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should render the component with correct elements", () => {
|
||||
const { container, getByText } = render(<CopyJobsNotFound explorer={mockExplorer} />);
|
||||
|
||||
const image = container.querySelector(".notFoundContainer .ms-Image");
|
||||
expect(image).toBeInTheDocument();
|
||||
expect(image).toHaveAttribute("style", "width: 100px; height: 100px;");
|
||||
expect(getByText(ContainerCopyMessages.noCopyJobsTitle)).toBeInTheDocument();
|
||||
|
||||
const button = screen.getByRole("button", {
|
||||
name: ContainerCopyMessages.createCopyJobButtonText,
|
||||
});
|
||||
expect(button).toBeInTheDocument();
|
||||
expect(button).toHaveClass("createCopyJobButton");
|
||||
});
|
||||
|
||||
it("should render with correct container classes", () => {
|
||||
const { container } = render(<CopyJobsNotFound explorer={mockExplorer} />);
|
||||
|
||||
const notFoundContainer = container.querySelector(".notFoundContainer");
|
||||
expect(notFoundContainer).toBeInTheDocument();
|
||||
expect(notFoundContainer).toHaveClass("flexContainer", "centerContent");
|
||||
});
|
||||
|
||||
it("should call openCreateCopyJobPanel when button is clicked", () => {
|
||||
const openCreateCopyJobPanelSpy = jest.spyOn(Actions, "openCreateCopyJobPanel");
|
||||
|
||||
render(<CopyJobsNotFound explorer={mockExplorer} />);
|
||||
|
||||
const button = screen.getByRole("button", {
|
||||
name: ContainerCopyMessages.createCopyJobButtonText,
|
||||
});
|
||||
|
||||
fireEvent.click(button);
|
||||
|
||||
expect(openCreateCopyJobPanelSpy).toHaveBeenCalledTimes(1);
|
||||
expect(openCreateCopyJobPanelSpy).toHaveBeenCalledWith(mockExplorer);
|
||||
});
|
||||
|
||||
it("should render ActionButton with correct props", () => {
|
||||
render(<CopyJobsNotFound explorer={mockExplorer} />);
|
||||
|
||||
const button = screen.getByRole("button", {
|
||||
name: ContainerCopyMessages.createCopyJobButtonText,
|
||||
});
|
||||
|
||||
expect(button).toBeInTheDocument();
|
||||
expect(button.textContent).toBe(ContainerCopyMessages.createCopyJobButtonText);
|
||||
});
|
||||
|
||||
it("should use memo to prevent unnecessary re-renders", () => {
|
||||
const { rerender } = render(<CopyJobsNotFound explorer={mockExplorer} />);
|
||||
rerender(<CopyJobsNotFound explorer={mockExplorer} />);
|
||||
expect(screen.getByRole("heading", { level: 4 })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -17,7 +17,7 @@ const CopyJobsNotFound: React.FC<CopyJobsNotFoundProps> = ({ explorer }) => {
|
||||
<ActionButton
|
||||
allowDisabledFocus
|
||||
className="createCopyJobButton"
|
||||
onClick={Actions.openCreateCopyJobPanel.bind(null, explorer)}
|
||||
onClick={() => Actions.openCreateCopyJobPanel(explorer)}
|
||||
>
|
||||
{ContainerCopyMessages.createCopyJobButtonText}
|
||||
</ActionButton>
|
||||
|
||||
@@ -0,0 +1,446 @@
|
||||
import "@testing-library/jest-dom";
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import React from "react";
|
||||
import { CopyJobStatusType } from "../../Enums/CopyJobEnums";
|
||||
import { CopyJobType, HandleJobActionClickType } from "../../Types/CopyJobTypes";
|
||||
import CopyJobsList from "./CopyJobsList";
|
||||
|
||||
jest.mock("../../Actions/CopyJobActions", () => ({
|
||||
openCopyJobDetailsPanel: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("./CopyJobColumns", () => ({
|
||||
getColumns: jest.fn(() => [
|
||||
{
|
||||
key: "Name",
|
||||
name: "Name",
|
||||
fieldName: "Name",
|
||||
minWidth: 140,
|
||||
maxWidth: 300,
|
||||
isResizable: true,
|
||||
onRender: (job: CopyJobType) => <span className="jobNameLink">{job.Name}</span>,
|
||||
},
|
||||
{
|
||||
key: "Status",
|
||||
name: "Status",
|
||||
fieldName: "Status",
|
||||
minWidth: 130,
|
||||
maxWidth: 200,
|
||||
isResizable: true,
|
||||
onRender: (job: CopyJobType) => <span>{job.Status}</span>,
|
||||
},
|
||||
{
|
||||
key: "CompletionPercentage",
|
||||
name: "Progress",
|
||||
fieldName: "CompletionPercentage",
|
||||
minWidth: 110,
|
||||
maxWidth: 200,
|
||||
isResizable: true,
|
||||
onRender: (job: CopyJobType) => <span>{job.CompletionPercentage}%</span>,
|
||||
},
|
||||
{
|
||||
key: "Actions",
|
||||
name: "Actions",
|
||||
minWidth: 80,
|
||||
maxWidth: 200,
|
||||
isResizable: true,
|
||||
onRender: (job: CopyJobType) => <button data-testid={`action-menu-${job.ID}`}>Actions</button>,
|
||||
},
|
||||
]),
|
||||
}));
|
||||
|
||||
// Sample test data
|
||||
const mockJobs: CopyJobType[] = [
|
||||
{
|
||||
ID: "job-1",
|
||||
Mode: "Live",
|
||||
Name: "Test Job 1",
|
||||
Status: CopyJobStatusType.Running,
|
||||
CompletionPercentage: 45,
|
||||
Duration: "00:05:30",
|
||||
LastUpdatedTime: "2025-01-01 10:00:00",
|
||||
timestamp: 1704110400000,
|
||||
Source: {
|
||||
component: "CosmosDBSql",
|
||||
remoteAccountName: "source-account",
|
||||
databaseName: "sourceDb",
|
||||
containerName: "sourceContainer",
|
||||
},
|
||||
Destination: {
|
||||
component: "CosmosDBSql",
|
||||
databaseName: "targetDb",
|
||||
containerName: "targetContainer",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "job-2",
|
||||
Mode: "Offline",
|
||||
Name: "Test Job 2",
|
||||
Status: CopyJobStatusType.Completed,
|
||||
CompletionPercentage: 100,
|
||||
Duration: "00:15:45",
|
||||
LastUpdatedTime: "2025-01-01 11:00:00",
|
||||
timestamp: 1704114000000,
|
||||
Source: {
|
||||
component: "CosmosDBSql",
|
||||
remoteAccountName: "source-account-2",
|
||||
databaseName: "sourceDb2",
|
||||
containerName: "sourceContainer2",
|
||||
},
|
||||
Destination: {
|
||||
component: "CosmosDBSql",
|
||||
databaseName: "targetDb2",
|
||||
containerName: "targetContainer2",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "job-3",
|
||||
Mode: "Live",
|
||||
Name: "Test Job 3",
|
||||
Status: CopyJobStatusType.Failed,
|
||||
CompletionPercentage: 25,
|
||||
Duration: "00:02:15",
|
||||
LastUpdatedTime: "2025-01-01 09:30:00",
|
||||
timestamp: 1704108600000,
|
||||
Error: {
|
||||
message: "Connection timeout",
|
||||
code: "TIMEOUT_ERROR",
|
||||
},
|
||||
Source: {
|
||||
component: "CosmosDBSql",
|
||||
remoteAccountName: "source-account-3",
|
||||
databaseName: "sourceDb3",
|
||||
containerName: "sourceContainer3",
|
||||
},
|
||||
Destination: {
|
||||
component: "CosmosDBSql",
|
||||
databaseName: "targetDb3",
|
||||
containerName: "targetContainer3",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const mockHandleActionClick: HandleJobActionClickType = jest.fn();
|
||||
|
||||
describe("CopyJobsList", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("Rendering", () => {
|
||||
it("renders empty list when no jobs provided", () => {
|
||||
render(<CopyJobsList jobs={[]} handleActionClick={mockHandleActionClick} />);
|
||||
|
||||
expect(screen.queryByText("Test Job 1")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders jobs list with provided jobs", () => {
|
||||
render(<CopyJobsList jobs={mockJobs} handleActionClick={mockHandleActionClick} />);
|
||||
|
||||
expect(screen.getByText("Test Job 1")).toBeInTheDocument();
|
||||
expect(screen.getByText("Test Job 2")).toBeInTheDocument();
|
||||
expect(screen.getByText("Test Job 3")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders job statuses correctly", () => {
|
||||
render(<CopyJobsList jobs={mockJobs} handleActionClick={mockHandleActionClick} />);
|
||||
|
||||
expect(screen.getByText(CopyJobStatusType.Running)).toBeInTheDocument();
|
||||
expect(screen.getByText(CopyJobStatusType.Completed)).toBeInTheDocument();
|
||||
expect(screen.getByText(CopyJobStatusType.Failed)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders completion percentages correctly", () => {
|
||||
render(<CopyJobsList jobs={mockJobs} handleActionClick={mockHandleActionClick} />);
|
||||
|
||||
expect(screen.getByText("45%")).toBeInTheDocument();
|
||||
expect(screen.getByText("100%")).toBeInTheDocument();
|
||||
expect(screen.getByText("25%")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders action menus for each job", () => {
|
||||
render(<CopyJobsList jobs={mockJobs} handleActionClick={mockHandleActionClick} />);
|
||||
|
||||
expect(screen.getByTestId("action-menu-job-1")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("action-menu-job-2")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("action-menu-job-3")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Pagination", () => {
|
||||
it("shows pager when jobs exceed page size", () => {
|
||||
const manyJobs: CopyJobType[] = Array.from({ length: 15 }, (_, i) => ({
|
||||
...mockJobs[0],
|
||||
ID: `job-${i + 1}`,
|
||||
Name: `Test Job ${i + 1}`,
|
||||
}));
|
||||
|
||||
render(<CopyJobsList jobs={manyJobs} handleActionClick={mockHandleActionClick} pageSize={10} />);
|
||||
|
||||
expect(screen.getByLabelText("Go to first page")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Go to previous page")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Go to next page")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Go to last page")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not show pager when jobs are within page size", () => {
|
||||
render(<CopyJobsList jobs={mockJobs} handleActionClick={mockHandleActionClick} pageSize={10} />);
|
||||
|
||||
expect(screen.queryByLabelText("Go to first page")).not.toBeInTheDocument();
|
||||
expect(screen.queryByLabelText("Go to previous page")).not.toBeInTheDocument();
|
||||
expect(screen.queryByLabelText("Go to next page")).not.toBeInTheDocument();
|
||||
expect(screen.queryByLabelText("Go to last page")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("displays correct page information", () => {
|
||||
const manyJobs: CopyJobType[] = Array.from({ length: 25 }, (_, i) => ({
|
||||
...mockJobs[0],
|
||||
ID: `job-${i + 1}`,
|
||||
Name: `Test Job ${i + 1}`,
|
||||
}));
|
||||
|
||||
render(<CopyJobsList jobs={manyJobs} handleActionClick={mockHandleActionClick} pageSize={10} />);
|
||||
|
||||
expect(screen.getByText("Showing 1 - 10 of 25 items")).toBeInTheDocument();
|
||||
expect(screen.getByText("Page 1 of 3")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("navigates to next page correctly", async () => {
|
||||
const manyJobs: CopyJobType[] = Array.from({ length: 15 }, (_, i) => ({
|
||||
...mockJobs[0],
|
||||
ID: `job-${i + 1}`,
|
||||
Name: `Test Job ${i + 1}`,
|
||||
}));
|
||||
|
||||
render(<CopyJobsList jobs={manyJobs} handleActionClick={mockHandleActionClick} pageSize={10} />);
|
||||
|
||||
expect(screen.getByText("Test Job 1")).toBeInTheDocument();
|
||||
expect(screen.getByText("Test Job 10")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Test Job 11")).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByLabelText("Go to next page"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText("Test Job 1")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("Test Job 11")).toBeInTheDocument();
|
||||
expect(screen.getByText("Test Job 15")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("uses custom page size when provided", () => {
|
||||
const manyJobs: CopyJobType[] = Array.from({ length: 8 }, (_, i) => ({
|
||||
...mockJobs[0],
|
||||
ID: `job-${i + 1}`,
|
||||
Name: `Test Job ${i + 1}`,
|
||||
}));
|
||||
|
||||
render(<CopyJobsList jobs={manyJobs} handleActionClick={mockHandleActionClick} pageSize={5} />);
|
||||
|
||||
expect(screen.getByLabelText("Go to next page")).toBeInTheDocument();
|
||||
expect(screen.getByText("Showing 1 - 5 of 8 items")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Sorting", () => {
|
||||
it("sorts jobs by name in ascending order", async () => {
|
||||
const unsortedJobs = [
|
||||
{ ...mockJobs[0], Name: "Z Job" },
|
||||
{ ...mockJobs[1], Name: "A Job" },
|
||||
{ ...mockJobs[2], Name: "M Job" },
|
||||
];
|
||||
|
||||
render(<CopyJobsList jobs={unsortedJobs} handleActionClick={mockHandleActionClick} />);
|
||||
|
||||
const rows = screen.getAllByText(/Job$/);
|
||||
expect(rows[0]).toHaveTextContent("Z Job");
|
||||
expect(rows[1]).toHaveTextContent("A Job");
|
||||
expect(rows[2]).toHaveTextContent("M Job");
|
||||
});
|
||||
|
||||
it("resets pagination to first page after sorting", async () => {
|
||||
const manyJobs: CopyJobType[] = Array.from({ length: 15 }, (_, i) => ({
|
||||
...mockJobs[0],
|
||||
ID: `job-${i + 1}`,
|
||||
Name: `Job ${String.fromCharCode(90 - i)}`,
|
||||
}));
|
||||
|
||||
render(<CopyJobsList jobs={manyJobs} handleActionClick={mockHandleActionClick} pageSize={10} />);
|
||||
|
||||
fireEvent.click(screen.getByLabelText("Go to next page"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Showing 11 - 15 of 15 items")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("updates jobs list when jobs prop changes", async () => {
|
||||
const { rerender } = render(<CopyJobsList jobs={[mockJobs[0]]} handleActionClick={mockHandleActionClick} />);
|
||||
|
||||
expect(screen.getByText("Test Job 1")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Test Job 2")).not.toBeInTheDocument();
|
||||
|
||||
rerender(<CopyJobsList jobs={mockJobs} handleActionClick={mockHandleActionClick} />);
|
||||
|
||||
expect(screen.getByText("Test Job 1")).toBeInTheDocument();
|
||||
expect(screen.getByText("Test Job 2")).toBeInTheDocument();
|
||||
expect(screen.getByText("Test Job 3")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("resets start index when jobs change", async () => {
|
||||
const manyJobs: CopyJobType[] = Array.from({ length: 15 }, (_, i) => ({
|
||||
...mockJobs[0],
|
||||
ID: `job-${i + 1}`,
|
||||
Name: `Test Job ${i + 1}`,
|
||||
}));
|
||||
|
||||
const { rerender } = render(
|
||||
<CopyJobsList jobs={manyJobs} handleActionClick={mockHandleActionClick} pageSize={10} />,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByLabelText("Go to next page"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Showing 11 - 15 of 15 items")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const newJobs = [mockJobs[0], mockJobs[1]];
|
||||
rerender(<CopyJobsList jobs={newJobs} handleActionClick={mockHandleActionClick} pageSize={10} />);
|
||||
|
||||
expect(screen.queryByLabelText("Go to next page")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Row Interactions", () => {
|
||||
it("calls openCopyJobDetailsPanel when row is clicked", async () => {
|
||||
const { openCopyJobDetailsPanel } = await import("../../Actions/CopyJobActions");
|
||||
|
||||
render(<CopyJobsList jobs={mockJobs} handleActionClick={mockHandleActionClick} />);
|
||||
|
||||
const jobNameElement = screen.getByText("Test Job 1");
|
||||
const rowElement = jobNameElement.closest('[role="row"]') || jobNameElement.closest("div");
|
||||
|
||||
if (rowElement) {
|
||||
fireEvent.click(rowElement);
|
||||
} else {
|
||||
fireEvent.click(jobNameElement);
|
||||
}
|
||||
|
||||
await waitFor(() => {
|
||||
expect(openCopyJobDetailsPanel).toHaveBeenCalledWith(mockJobs[0]);
|
||||
});
|
||||
});
|
||||
|
||||
it("applies cursor pointer style to rows", () => {
|
||||
render(<CopyJobsList jobs={mockJobs} handleActionClick={mockHandleActionClick} />);
|
||||
|
||||
const jobNameElement = screen.getByText("Test Job 1");
|
||||
const rowElement = jobNameElement.closest("div");
|
||||
|
||||
expect(rowElement).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Component Props", () => {
|
||||
it("uses default page size when not provided", () => {
|
||||
const manyJobs: CopyJobType[] = Array.from({ length: 12 }, (_, i) => ({
|
||||
...mockJobs[0],
|
||||
ID: `job-${i + 1}`,
|
||||
Name: `Test Job ${i + 1}`,
|
||||
}));
|
||||
|
||||
render(<CopyJobsList jobs={manyJobs} handleActionClick={mockHandleActionClick} />);
|
||||
|
||||
expect(screen.getByLabelText("Go to next page")).toBeInTheDocument();
|
||||
expect(screen.getByText("Showing 1 - 10 of 12 items")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("passes correct props to getColumns function", async () => {
|
||||
const { getColumns } = await import("./CopyJobColumns");
|
||||
|
||||
render(<CopyJobsList jobs={mockJobs} handleActionClick={mockHandleActionClick} />);
|
||||
|
||||
expect(getColumns).toHaveBeenCalledWith(
|
||||
expect.any(Function), // handleSort
|
||||
mockHandleActionClick, // handleActionClick
|
||||
undefined, // sortedColumnKey
|
||||
false, // isSortedDescending
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Accessibility", () => {
|
||||
it("renders with proper ARIA attributes", () => {
|
||||
render(<CopyJobsList jobs={mockJobs} handleActionClick={mockHandleActionClick} />);
|
||||
|
||||
const detailsList = screen.getByRole("grid");
|
||||
expect(detailsList).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("has accessible pager controls", () => {
|
||||
const manyJobs: CopyJobType[] = Array.from({ length: 15 }, (_, i) => ({
|
||||
...mockJobs[0],
|
||||
ID: `job-${i + 1}`,
|
||||
Name: `Test Job ${i + 1}`,
|
||||
}));
|
||||
|
||||
render(<CopyJobsList jobs={manyJobs} handleActionClick={mockHandleActionClick} pageSize={10} />);
|
||||
|
||||
expect(screen.getByLabelText("Go to first page")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Go to previous page")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Go to next page")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Go to last page")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Error Handling", () => {
|
||||
it("handles empty jobs array gracefully", () => {
|
||||
expect(() => {
|
||||
render(<CopyJobsList jobs={[]} handleActionClick={mockHandleActionClick} />);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("handles jobs with missing optional properties", () => {
|
||||
const incompleteJob: CopyJobType = {
|
||||
ID: "incomplete-job",
|
||||
Mode: "Live",
|
||||
Name: "Incomplete Job",
|
||||
Status: CopyJobStatusType.Running,
|
||||
CompletionPercentage: 0,
|
||||
Duration: "00:00:00",
|
||||
LastUpdatedTime: "2025-01-01 12:00:00",
|
||||
timestamp: 1704117600000,
|
||||
Source: {
|
||||
component: "CosmosDBSql",
|
||||
remoteAccountName: "source-account",
|
||||
databaseName: "sourceDb",
|
||||
containerName: "sourceContainer",
|
||||
},
|
||||
Destination: {
|
||||
component: "CosmosDBSql",
|
||||
databaseName: "targetDb",
|
||||
containerName: "targetContainer",
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
render(<CopyJobsList jobs={[incompleteJob]} handleActionClick={mockHandleActionClick} />);
|
||||
}).not.toThrow();
|
||||
|
||||
expect(screen.getByText("Incomplete Job")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("handles very large job lists", () => {
|
||||
const largeJobsList: CopyJobType[] = Array.from({ length: 1000 }, (_, i) => ({
|
||||
...mockJobs[0],
|
||||
ID: `job-${i + 1}`,
|
||||
Name: `Job ${i + 1}`,
|
||||
}));
|
||||
|
||||
expect(() => {
|
||||
render(<CopyJobsList jobs={largeJobsList} handleActionClick={mockHandleActionClick} />);
|
||||
}).not.toThrow();
|
||||
|
||||
expect(screen.getByText("Showing 1 - 10 of 1000 items")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,208 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CopyJobStatusWithIcon Spinner Status Types renders InProgress with spinner and expected text 1`] = `
|
||||
<div
|
||||
class="ms-Stack css-109"
|
||||
>
|
||||
<div
|
||||
class="ms-Spinner root-122"
|
||||
style="margin-right: 8px;"
|
||||
>
|
||||
<div
|
||||
class="ms-Spinner-circle ms-Spinner--small circle-123"
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
class="css-112"
|
||||
>
|
||||
In Progress
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`CopyJobStatusWithIcon Spinner Status Types renders Partitioning with spinner and expected text 1`] = `
|
||||
<div
|
||||
class="ms-Stack css-109"
|
||||
>
|
||||
<div
|
||||
class="ms-Spinner root-122"
|
||||
style="margin-right: 8px;"
|
||||
>
|
||||
<div
|
||||
class="ms-Spinner-circle ms-Spinner--small circle-123"
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
class="css-112"
|
||||
>
|
||||
In Progress
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`CopyJobStatusWithIcon Spinner Status Types renders Running with spinner and expected text 1`] = `
|
||||
<div
|
||||
class="ms-Stack css-109"
|
||||
>
|
||||
<div
|
||||
class="ms-Spinner root-122"
|
||||
style="margin-right: 8px;"
|
||||
>
|
||||
<div
|
||||
class="ms-Spinner-circle ms-Spinner--small circle-123"
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
class="css-112"
|
||||
>
|
||||
In Progress
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders Cancelled status correctly 1`] = `
|
||||
<div
|
||||
class="ms-Stack css-109"
|
||||
>
|
||||
<i
|
||||
aria-label="Cancelled"
|
||||
class="ms-Icon root-105 css-118 mocked-style-Cancelled"
|
||||
data-icon-name="StatusErrorFull"
|
||||
role="img"
|
||||
style="font-family: "FabricMDL2Icons-4";"
|
||||
>
|
||||
|
||||
</i>
|
||||
<span
|
||||
class="css-112"
|
||||
>
|
||||
Cancelled
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders Completed status correctly 1`] = `
|
||||
<div
|
||||
class="ms-Stack css-109"
|
||||
>
|
||||
<i
|
||||
aria-label="Completed"
|
||||
class="ms-Icon root-105 css-120 mocked-style-Completed"
|
||||
data-icon-name="CompletedSolid"
|
||||
role="img"
|
||||
style="font-family: "FabricMDL2Icons-5";"
|
||||
>
|
||||
|
||||
</i>
|
||||
<span
|
||||
class="css-112"
|
||||
>
|
||||
Completed
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders Failed status correctly 1`] = `
|
||||
<div
|
||||
class="ms-Stack css-109"
|
||||
>
|
||||
<i
|
||||
aria-label="Failed"
|
||||
class="ms-Icon root-105 css-118 mocked-style-Failed"
|
||||
data-icon-name="StatusErrorFull"
|
||||
role="img"
|
||||
style="font-family: "FabricMDL2Icons-4";"
|
||||
>
|
||||
|
||||
</i>
|
||||
<span
|
||||
class="css-112"
|
||||
>
|
||||
Failed
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders Faulted status correctly 1`] = `
|
||||
<div
|
||||
class="ms-Stack css-109"
|
||||
>
|
||||
<i
|
||||
aria-label="Faulted"
|
||||
class="ms-Icon root-105 css-118 mocked-style-Faulted"
|
||||
data-icon-name="StatusErrorFull"
|
||||
role="img"
|
||||
style="font-family: "FabricMDL2Icons-4";"
|
||||
>
|
||||
|
||||
</i>
|
||||
<span
|
||||
class="css-112"
|
||||
>
|
||||
Failed
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders Paused status correctly 1`] = `
|
||||
<div
|
||||
class="ms-Stack css-109"
|
||||
>
|
||||
<i
|
||||
aria-label="Paused"
|
||||
class="ms-Icon root-105 css-114 mocked-style-Paused"
|
||||
data-icon-name="CirclePause"
|
||||
role="img"
|
||||
style="font-family: "FabricMDL2Icons-11";"
|
||||
>
|
||||
|
||||
</i>
|
||||
<span
|
||||
class="css-112"
|
||||
>
|
||||
Paused
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders Pending status correctly 1`] = `
|
||||
<div
|
||||
class="ms-Stack css-109"
|
||||
>
|
||||
<i
|
||||
aria-label="Pending"
|
||||
class="ms-Icon root-105 css-111 mocked-style-Pending"
|
||||
data-icon-name="Clock"
|
||||
role="img"
|
||||
style="font-family: "FabricMDL2Icons-2";"
|
||||
>
|
||||
|
||||
</i>
|
||||
<span
|
||||
class="css-112"
|
||||
>
|
||||
Pending
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders Skipped status correctly 1`] = `
|
||||
<div
|
||||
class="ms-Stack css-109"
|
||||
>
|
||||
<i
|
||||
aria-label="Skipped"
|
||||
class="ms-Icon root-105 css-116 mocked-style-Skipped"
|
||||
data-icon-name="StatusCircleBlock2"
|
||||
role="img"
|
||||
style="font-family: "FabricMDL2Icons-9";"
|
||||
>
|
||||
|
||||
</i>
|
||||
<span
|
||||
class="css-112"
|
||||
>
|
||||
Cancelled
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,122 @@
|
||||
import { MonitorCopyJobsRefState } from "./MonitorCopyJobRefState";
|
||||
import { MonitorCopyJobsRef } from "./MonitorCopyJobs";
|
||||
|
||||
describe("MonitorCopyJobsRefState", () => {
|
||||
beforeEach(() => {
|
||||
MonitorCopyJobsRefState.setState({ ref: null });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should initialize with null ref", () => {
|
||||
const state = MonitorCopyJobsRefState.getState();
|
||||
expect(state.ref).toBeNull();
|
||||
});
|
||||
|
||||
it("should set ref using setRef", () => {
|
||||
const mockRef: MonitorCopyJobsRef = {
|
||||
refreshJobList: jest.fn(),
|
||||
};
|
||||
|
||||
const state = MonitorCopyJobsRefState.getState();
|
||||
state.setRef(mockRef);
|
||||
|
||||
const updatedState = MonitorCopyJobsRefState.getState();
|
||||
expect(updatedState.ref).toBe(mockRef);
|
||||
expect(updatedState.ref).toEqual(mockRef);
|
||||
});
|
||||
|
||||
it("should allow setting ref to null", () => {
|
||||
const mockRef: MonitorCopyJobsRef = {
|
||||
refreshJobList: jest.fn(),
|
||||
};
|
||||
|
||||
MonitorCopyJobsRefState.getState().setRef(mockRef);
|
||||
expect(MonitorCopyJobsRefState.getState().ref).toBe(mockRef);
|
||||
|
||||
MonitorCopyJobsRefState.getState().setRef(null);
|
||||
expect(MonitorCopyJobsRefState.getState().ref).toBeNull();
|
||||
});
|
||||
|
||||
it("should call refreshJobList method on the stored ref", () => {
|
||||
const mockRefreshJobList = jest.fn();
|
||||
const mockRef: MonitorCopyJobsRef = {
|
||||
refreshJobList: mockRefreshJobList,
|
||||
};
|
||||
|
||||
MonitorCopyJobsRefState.getState().setRef(mockRef);
|
||||
|
||||
const state = MonitorCopyJobsRefState.getState();
|
||||
state.ref?.refreshJobList();
|
||||
|
||||
expect(mockRefreshJobList).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should handle calling refreshJobList when ref is null", () => {
|
||||
MonitorCopyJobsRefState.setState({ ref: null });
|
||||
|
||||
const state = MonitorCopyJobsRefState.getState();
|
||||
expect(state.ref).toBeNull();
|
||||
|
||||
expect(() => {
|
||||
state.ref?.refreshJobList();
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("should allow partial state updates", () => {
|
||||
const mockRef: MonitorCopyJobsRef = {
|
||||
refreshJobList: jest.fn(),
|
||||
};
|
||||
|
||||
MonitorCopyJobsRefState.setState({ ref: mockRef });
|
||||
const state1 = MonitorCopyJobsRefState.getState();
|
||||
expect(state1.ref).toBe(mockRef);
|
||||
expect(state1.setRef).toBeDefined();
|
||||
|
||||
const newMockRef: MonitorCopyJobsRef = {
|
||||
refreshJobList: jest.fn(),
|
||||
};
|
||||
MonitorCopyJobsRefState.setState({ ref: newMockRef });
|
||||
const state2 = MonitorCopyJobsRefState.getState();
|
||||
expect(state2.ref).toBe(newMockRef);
|
||||
expect(state2.setRef).toBeDefined();
|
||||
});
|
||||
|
||||
it("should handle multiple subscribers", () => {
|
||||
const mockSubscriber1 = jest.fn();
|
||||
const mockSubscriber2 = jest.fn();
|
||||
|
||||
const unsubscribe1 = MonitorCopyJobsRefState.subscribe(mockSubscriber1);
|
||||
const unsubscribe2 = MonitorCopyJobsRefState.subscribe(mockSubscriber2);
|
||||
|
||||
const mockRef: MonitorCopyJobsRef = {
|
||||
refreshJobList: jest.fn(),
|
||||
};
|
||||
|
||||
MonitorCopyJobsRefState.getState().setRef(mockRef);
|
||||
|
||||
expect(mockSubscriber1).toHaveBeenCalled();
|
||||
expect(mockSubscriber2).toHaveBeenCalled();
|
||||
|
||||
unsubscribe1();
|
||||
unsubscribe2();
|
||||
});
|
||||
|
||||
it("should not notify unsubscribed listeners", () => {
|
||||
const mockSubscriber = jest.fn();
|
||||
|
||||
const unsubscribe = MonitorCopyJobsRefState.subscribe(mockSubscriber);
|
||||
unsubscribe();
|
||||
|
||||
const mockRef: MonitorCopyJobsRef = {
|
||||
refreshJobList: jest.fn(),
|
||||
};
|
||||
|
||||
mockSubscriber.mockClear();
|
||||
MonitorCopyJobsRefState.getState().setRef(mockRef);
|
||||
|
||||
expect(mockSubscriber).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,435 @@
|
||||
import "@testing-library/jest-dom";
|
||||
import { act, render, screen, waitFor } from "@testing-library/react";
|
||||
import React from "react";
|
||||
import { DataTransferJobGetResults } from "Utils/arm/generatedClients/dataTransferService/types";
|
||||
import Explorer from "../../Explorer";
|
||||
import * as CopyJobActions from "../Actions/CopyJobActions";
|
||||
import { CopyJobStatusType } from "../Enums/CopyJobEnums";
|
||||
import { CopyJobType } from "../Types/CopyJobTypes";
|
||||
import MonitorCopyJobs from "./MonitorCopyJobs";
|
||||
|
||||
jest.mock("Common/ShimmerTree/ShimmerTree", () => {
|
||||
const MockShimmerTree = () => {
|
||||
return <div data-testid="shimmer-tree">Loading...</div>;
|
||||
};
|
||||
MockShimmerTree.displayName = "MockShimmerTree";
|
||||
return MockShimmerTree;
|
||||
});
|
||||
|
||||
jest.mock("./Components/CopyJobsList", () => {
|
||||
const MockCopyJobsList = ({ jobs }: any) => {
|
||||
return <div data-testid="copy-jobs-list">Jobs: {jobs.length}</div>;
|
||||
};
|
||||
MockCopyJobsList.displayName = "MockCopyJobsList";
|
||||
return MockCopyJobsList;
|
||||
});
|
||||
|
||||
jest.mock("./Components/CopyJobs.NotFound", () => {
|
||||
const MockCopyJobsNotFound = () => {
|
||||
return <div data-testid="copy-jobs-not-found">No jobs found</div>;
|
||||
};
|
||||
MockCopyJobsNotFound.displayName = "MockCopyJobsNotFound";
|
||||
return MockCopyJobsNotFound;
|
||||
});
|
||||
|
||||
jest.mock("../Actions/CopyJobActions", () => ({
|
||||
getCopyJobs: jest.fn(),
|
||||
updateCopyJobStatus: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("MonitorCopyJobs", () => {
|
||||
let mockExplorer: Explorer;
|
||||
const mockGetCopyJobs = CopyJobActions.getCopyJobs as jest.MockedFunction<typeof CopyJobActions.getCopyJobs>;
|
||||
const mockUpdateCopyJobStatus = CopyJobActions.updateCopyJobStatus as jest.MockedFunction<
|
||||
typeof CopyJobActions.updateCopyJobStatus
|
||||
>;
|
||||
|
||||
const mockJobs: CopyJobType[] = [
|
||||
{
|
||||
ID: "1",
|
||||
Mode: "Offline",
|
||||
Name: "test-job-1",
|
||||
Status: CopyJobStatusType.InProgress,
|
||||
CompletionPercentage: 50,
|
||||
Duration: "10 minutes",
|
||||
LastUpdatedTime: "1/1/2024, 10:00:00 AM",
|
||||
timestamp: 1704110400000,
|
||||
Source: {
|
||||
component: "CosmosDBSql",
|
||||
databaseName: "db1",
|
||||
containerName: "container1",
|
||||
},
|
||||
Destination: {
|
||||
component: "CosmosDBSql",
|
||||
databaseName: "db2",
|
||||
containerName: "container2",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "2",
|
||||
Mode: "Online",
|
||||
Name: "test-job-2",
|
||||
Status: CopyJobStatusType.Completed,
|
||||
CompletionPercentage: 100,
|
||||
Duration: "20 minutes",
|
||||
LastUpdatedTime: "1/1/2024, 11:00:00 AM",
|
||||
timestamp: 1704114000000,
|
||||
Source: {
|
||||
component: "CosmosDBSql",
|
||||
databaseName: "db3",
|
||||
containerName: "container3",
|
||||
},
|
||||
Destination: {
|
||||
component: "CosmosDBSql",
|
||||
databaseName: "db4",
|
||||
containerName: "container4",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
mockExplorer = {} as Explorer;
|
||||
mockGetCopyJobs.mockResolvedValue(mockJobs);
|
||||
mockUpdateCopyJobStatus.mockResolvedValue({
|
||||
id: "test-id",
|
||||
type: "Microsoft.DocumentDB/databaseAccounts/dataTransferJobs",
|
||||
properties: {
|
||||
jobName: "test-job-1",
|
||||
status: "Paused",
|
||||
lastUpdatedUtcTime: "2024-01-01T10:00:00Z",
|
||||
processedCount: 500,
|
||||
totalCount: 1000,
|
||||
mode: "Offline",
|
||||
duration: "00:10:00",
|
||||
source: {
|
||||
databaseName: "db1",
|
||||
containerName: "container1",
|
||||
component: "CosmosDBSql",
|
||||
},
|
||||
destination: {
|
||||
databaseName: "db2",
|
||||
containerName: "container2",
|
||||
component: "CosmosDBSql",
|
||||
},
|
||||
error: {
|
||||
message: "",
|
||||
code: "",
|
||||
},
|
||||
},
|
||||
} as DataTransferJobGetResults);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("Initial Rendering", () => {
|
||||
it("renders the component with correct structure", async () => {
|
||||
render(<MonitorCopyJobs explorer={mockExplorer} />);
|
||||
|
||||
const container = document.querySelector(".monitorCopyJobs");
|
||||
expect(container).toBeInTheDocument();
|
||||
expect(container).toHaveClass("flexContainer");
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGetCopyJobs).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("displays shimmer while loading initially", () => {
|
||||
render(<MonitorCopyJobs explorer={mockExplorer} />);
|
||||
|
||||
expect(screen.getByTestId("shimmer-tree")).toBeInTheDocument();
|
||||
expect(screen.getByText("Loading...")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("fetches jobs on mount", async () => {
|
||||
render(<MonitorCopyJobs explorer={mockExplorer} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGetCopyJobs).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Job List Display", () => {
|
||||
it("displays job list when jobs are loaded", async () => {
|
||||
render(<MonitorCopyJobs explorer={mockExplorer} />);
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(screen.getByTestId("copy-jobs-list")).toBeInTheDocument();
|
||||
},
|
||||
{ timeout: 3000 },
|
||||
);
|
||||
|
||||
expect(screen.getByText("Jobs: 2")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("shimmer-tree")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("displays not found component when no jobs exist", async () => {
|
||||
mockGetCopyJobs.mockResolvedValue([]);
|
||||
|
||||
render(<MonitorCopyJobs explorer={mockExplorer} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("copy-jobs-not-found")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.getByText("No jobs found")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("copy-jobs-list")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("passes correct jobs to CopyJobsList component", async () => {
|
||||
render(<MonitorCopyJobs explorer={mockExplorer} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("copy-jobs-list")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText("Jobs: 2")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("updates job status when action is triggered", async () => {
|
||||
const ref = React.createRef<any>();
|
||||
render(<MonitorCopyJobs explorer={mockExplorer} ref={ref} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("copy-jobs-list")).toBeInTheDocument();
|
||||
});
|
||||
expect(mockJobs[0].Status).toBe(CopyJobStatusType.InProgress);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Error Handling", () => {
|
||||
it("displays error message when fetch fails", async () => {
|
||||
const errorMessage = "Failed to load copy jobs. Please try again later.";
|
||||
mockGetCopyJobs.mockRejectedValue(new Error(errorMessage));
|
||||
|
||||
render(<MonitorCopyJobs explorer={mockExplorer} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(errorMessage)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.queryByTestId("shimmer-tree")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("allows dismissing error message", async () => {
|
||||
mockGetCopyJobs.mockRejectedValue(new Error("Failed to load copy jobs"));
|
||||
const { container } = render(<MonitorCopyJobs explorer={mockExplorer} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Failed to load copy jobs/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const dismissButton = container.querySelector('[aria-label="Close"]');
|
||||
if (dismissButton) {
|
||||
dismissButton.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
}
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText(/Failed to load copy jobs/)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("displays custom error message from getCopyJobs", async () => {
|
||||
const customError = { message: "Custom error occurred" };
|
||||
mockGetCopyJobs.mockRejectedValue(customError);
|
||||
|
||||
render(<MonitorCopyJobs explorer={mockExplorer} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Custom error occurred")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("displays error when job action update fails", async () => {
|
||||
mockUpdateCopyJobStatus.mockRejectedValue(new Error("Update failed"));
|
||||
|
||||
const ref = React.createRef<any>();
|
||||
render(<MonitorCopyJobs explorer={mockExplorer} ref={ref} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("copy-jobs-list")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const mockHandleActionClick = jest.fn(async (job, action, setUpdatingJobAction) => {
|
||||
setUpdatingJobAction({ jobName: job.Name, action });
|
||||
await mockUpdateCopyJobStatus(job, action);
|
||||
});
|
||||
|
||||
await expect(mockHandleActionClick(mockJobs[0], "pause", jest.fn())).rejects.toThrow("Update failed");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Polling and Refresh", () => {
|
||||
it.skip("polls for jobs at regular intervals", async () => {
|
||||
render(<MonitorCopyJobs explorer={mockExplorer} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGetCopyJobs).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(30000);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGetCopyJobs).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(30000);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGetCopyJobs).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
});
|
||||
|
||||
it("stops polling when component unmounts", async () => {
|
||||
const { unmount } = render(<MonitorCopyJobs explorer={mockExplorer} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGetCopyJobs).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
unmount();
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(60000);
|
||||
});
|
||||
|
||||
expect(mockGetCopyJobs).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("refreshes job list via ref", async () => {
|
||||
const ref = React.createRef<any>();
|
||||
render(<MonitorCopyJobs explorer={mockExplorer} ref={ref} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGetCopyJobs).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
ref.current?.refreshJobList();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGetCopyJobs).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
it("prevents refresh when update is in progress", async () => {
|
||||
const ref = React.createRef<any>();
|
||||
render(<MonitorCopyJobs explorer={mockExplorer} ref={ref} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("copy-jobs-list")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
mockUpdateCopyJobStatus.mockImplementation(
|
||||
() =>
|
||||
new Promise((resolve) =>
|
||||
setTimeout(
|
||||
() =>
|
||||
resolve({
|
||||
id: "test-id",
|
||||
type: "Microsoft.DocumentDB/databaseAccounts/dataTransferJobs",
|
||||
properties: {
|
||||
jobName: "test-job-1",
|
||||
status: "Paused",
|
||||
lastUpdatedUtcTime: "2024-01-01T10:00:00Z",
|
||||
processedCount: 500,
|
||||
totalCount: 1000,
|
||||
mode: "Offline",
|
||||
duration: "00:10:00",
|
||||
source: {
|
||||
databaseName: "db1",
|
||||
collectionName: "container1",
|
||||
component: "CosmosDBSql",
|
||||
},
|
||||
destination: {
|
||||
databaseName: "db2",
|
||||
collectionName: "container2",
|
||||
component: "CosmosDBSql",
|
||||
},
|
||||
error: {
|
||||
message: "",
|
||||
code: "",
|
||||
},
|
||||
},
|
||||
} as DataTransferJobGetResults),
|
||||
5000,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(ref.current).toHaveProperty("refreshJobList");
|
||||
expect(typeof ref.current.refreshJobList).toBe("function");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Edge Cases", () => {
|
||||
it("handles empty job array", async () => {
|
||||
mockGetCopyJobs.mockResolvedValue([]);
|
||||
|
||||
render(<MonitorCopyJobs explorer={mockExplorer} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("copy-jobs-not-found")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("handles null response from getCopyJobs gracefully", async () => {
|
||||
mockGetCopyJobs.mockResolvedValue(null as any);
|
||||
|
||||
render(<MonitorCopyJobs explorer={mockExplorer} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("copy-jobs-not-found")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("handles explorer prop correctly", () => {
|
||||
const { rerender } = render(<MonitorCopyJobs explorer={mockExplorer} />);
|
||||
|
||||
const newExplorer = {} as Explorer;
|
||||
rerender(<MonitorCopyJobs explorer={newExplorer} />);
|
||||
|
||||
expect(document.querySelector(".monitorCopyJobs")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Ref Handle", () => {
|
||||
it("exposes refreshJobList method through ref", () => {
|
||||
const ref = React.createRef<any>();
|
||||
render(<MonitorCopyJobs explorer={mockExplorer} ref={ref} />);
|
||||
|
||||
expect(ref.current).toBeDefined();
|
||||
expect(ref.current).toHaveProperty("refreshJobList");
|
||||
expect(typeof ref.current.refreshJobList).toBe("function");
|
||||
});
|
||||
|
||||
it("refreshJobList triggers getCopyJobs", async () => {
|
||||
const ref = React.createRef<any>();
|
||||
render(<MonitorCopyJobs explorer={mockExplorer} ref={ref} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGetCopyJobs).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
ref.current?.refreshJobList();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGetCopyJobs).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Action Callback", () => {
|
||||
it("provides handleActionClick callback to CopyJobsList", async () => {
|
||||
render(<MonitorCopyJobs explorer={mockExplorer} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("copy-jobs-list")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -39,8 +39,9 @@ const MonitorCopyJobs = forwardRef<MonitorCopyJobsRef, MonitorCopyJobsProps>(({
|
||||
setError(null);
|
||||
|
||||
const response = await getCopyJobs();
|
||||
const normalizedResponse = response || [];
|
||||
setJobs((prevJobs) => {
|
||||
return isEqual(prevJobs, response) ? prevJobs : response;
|
||||
return isEqual(prevJobs, normalizedResponse) ? prevJobs : normalizedResponse;
|
||||
});
|
||||
} catch (error) {
|
||||
setError(error.message || "Failed to load copy jobs. Please try again later.");
|
||||
@@ -97,29 +98,27 @@ const MonitorCopyJobs = forwardRef<MonitorCopyJobsRef, MonitorCopyJobsProps>(({
|
||||
[],
|
||||
);
|
||||
|
||||
const renderJobsList = () => {
|
||||
if (loading) {
|
||||
return null;
|
||||
}
|
||||
if (jobs.length > 0) {
|
||||
return <CopyJobsList jobs={jobs} handleActionClick={handleActionClick} />;
|
||||
}
|
||||
return <CopyJobsNotFound explorer={explorer} />;
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack className="monitorCopyJobs flexContainer">
|
||||
{loading && (
|
||||
<ShimmerTree indentLevels={SHIMMER_INDENT_LEVELS} style={{ width: "100%", padding: "1rem 2.5rem" }} />
|
||||
)}
|
||||
{error && (
|
||||
<MessageBar messageBarType={MessageBarType.error} isMultiline={false} onDismiss={() => setError(null)}>
|
||||
<MessageBar
|
||||
messageBarType={MessageBarType.error}
|
||||
isMultiline={false}
|
||||
onDismiss={() => setError(null)}
|
||||
dismissButtonAriaLabel="Close"
|
||||
>
|
||||
{error}
|
||||
</MessageBar>
|
||||
)}
|
||||
{renderJobsList()}
|
||||
{!loading && jobs.length > 0 && <CopyJobsList jobs={jobs} handleActionClick={handleActionClick} />}
|
||||
{!loading && jobs.length === 0 && <CopyJobsNotFound explorer={explorer} />}
|
||||
</Stack>
|
||||
);
|
||||
});
|
||||
|
||||
MonitorCopyJobs.displayName = "MonitorCopyJobs";
|
||||
|
||||
export default MonitorCopyJobs;
|
||||
|
||||
Reference in New Issue
Block a user