mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-23 02:41:39 +00:00
Compare commits
1 Commits
master
...
user/bchou
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
083f020bac |
@@ -41,8 +41,10 @@ describe("CopyJobActions", () => {
|
||||
const mockExplorer = {} as Explorer;
|
||||
const mockSetPanelHasConsole = jest.fn();
|
||||
const mockOpenSidePanel = jest.fn();
|
||||
const mockSetLightDismiss = jest.fn();
|
||||
|
||||
(useSidePanel.getState as jest.Mock).mockReturnValue({
|
||||
setLightDismiss: mockSetLightDismiss,
|
||||
setPanelHasConsole: mockSetPanelHasConsole,
|
||||
openSidePanel: mockOpenSidePanel,
|
||||
});
|
||||
@@ -50,6 +52,7 @@ describe("CopyJobActions", () => {
|
||||
openCreateCopyJobPanel(mockExplorer);
|
||||
|
||||
expect(mockSetPanelHasConsole).toHaveBeenCalledWith(false);
|
||||
expect(mockSetLightDismiss).toHaveBeenCalledWith(false);
|
||||
expect(mockOpenSidePanel).toHaveBeenCalledWith(expect.any(String), expect.any(Object), "650px");
|
||||
});
|
||||
|
||||
@@ -59,6 +62,7 @@ describe("CopyJobActions", () => {
|
||||
|
||||
(useSidePanel.getState as jest.Mock).mockReturnValue({
|
||||
setPanelHasConsole: jest.fn(),
|
||||
setLightDismiss: jest.fn(),
|
||||
openSidePanel: mockOpenSidePanel,
|
||||
});
|
||||
|
||||
@@ -94,9 +98,11 @@ describe("CopyJobActions", () => {
|
||||
};
|
||||
|
||||
const mockSetPanelHasConsole = jest.fn();
|
||||
const mockSetLightDismiss = jest.fn();
|
||||
const mockOpenSidePanel = jest.fn();
|
||||
|
||||
(useSidePanel.getState as jest.Mock).mockReturnValue({
|
||||
setLightDismiss: mockSetLightDismiss,
|
||||
setPanelHasConsole: mockSetPanelHasConsole,
|
||||
openSidePanel: mockOpenSidePanel,
|
||||
});
|
||||
@@ -104,6 +110,7 @@ describe("CopyJobActions", () => {
|
||||
openCopyJobDetailsPanel(mockJob);
|
||||
|
||||
expect(mockSetPanelHasConsole).toHaveBeenCalledWith(false);
|
||||
expect(mockSetLightDismiss).toHaveBeenCalledWith(true);
|
||||
expect(mockOpenSidePanel).toHaveBeenCalledWith(expect.stringContaining("test-job"), expect.any(Object), "650px");
|
||||
});
|
||||
|
||||
@@ -133,6 +140,7 @@ describe("CopyJobActions", () => {
|
||||
|
||||
(useSidePanel.getState as jest.Mock).mockReturnValue({
|
||||
setPanelHasConsole: jest.fn(),
|
||||
setLightDismiss: jest.fn(),
|
||||
openSidePanel: mockOpenSidePanel,
|
||||
});
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ import { CopyJobContextState, CopyJobError, CopyJobErrorType, CopyJobType } from
|
||||
export const openCreateCopyJobPanel = (explorer: Explorer) => {
|
||||
const sidePanelState = useSidePanel.getState();
|
||||
sidePanelState.setPanelHasConsole(false);
|
||||
sidePanelState.setLightDismiss(false);
|
||||
sidePanelState.openSidePanel(
|
||||
ContainerCopyMessages.createCopyJobPanelTitle,
|
||||
<CreateCopyJobScreensProvider explorer={explorer} />,
|
||||
@@ -44,6 +45,7 @@ export const openCreateCopyJobPanel = (explorer: Explorer) => {
|
||||
export const openCopyJobDetailsPanel = (job: CopyJobType) => {
|
||||
const sidePanelState = useSidePanel.getState();
|
||||
sidePanelState.setPanelHasConsole(false);
|
||||
sidePanelState.setLightDismiss(true);
|
||||
sidePanelState.openSidePanel(
|
||||
ContainerCopyMessages.copyJobDetailsPanelTitle(job.Name),
|
||||
<CopyJobDetails job={job} />,
|
||||
|
||||
@@ -25,7 +25,22 @@ export default {
|
||||
subscriptionDropdownPlaceholder: "Select a subscription",
|
||||
sourceAccountDropdownLabel: "Account",
|
||||
sourceAccountDropdownPlaceholder: "Select an account",
|
||||
migrationTypeCheckboxLabel: "Copy container in offline mode",
|
||||
migrationTypeOptions: {
|
||||
offline: {
|
||||
title: "Offline mode",
|
||||
description:
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
|
||||
learnMoreText: "Learn more",
|
||||
learnMoreHref: "https://learn.microsoft.com/en-us/azure/cosmos-db/container-copy?tabs=offline-copy",
|
||||
},
|
||||
online: {
|
||||
title: "Online mode",
|
||||
description:
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
|
||||
learnMoreText: "Learn more",
|
||||
learnMoreHref: "https://learn.microsoft.com/en-us/azure/cosmos-db/container-copy?tabs=offline-copy",
|
||||
},
|
||||
},
|
||||
|
||||
// Select Source and Target Containers Screen
|
||||
selectSourceAndTargetContainersDescription:
|
||||
@@ -173,5 +188,10 @@ export default {
|
||||
Skipped: "Cancelled",
|
||||
Cancelled: "Cancelled",
|
||||
},
|
||||
dialog: {
|
||||
heading: "Confirm Action",
|
||||
confirmButtonText: "OK",
|
||||
cancelButtonText: "Cancel",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
import "@testing-library/jest-dom";
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import React from "react";
|
||||
import ContainerCopyMessages from "../../../../ContainerCopyMessages";
|
||||
import { useCopyJobContext } from "../../../../Context/CopyJobContext";
|
||||
import { CopyJobMigrationType } from "../../../../Enums/CopyJobEnums";
|
||||
import { MigrationType } from "./MigrationType";
|
||||
|
||||
jest.mock("../../../../Context/CopyJobContext", () => ({
|
||||
useCopyJobContext: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("MigrationType", () => {
|
||||
const mockSetCopyJobState = jest.fn();
|
||||
|
||||
const defaultContextValue = {
|
||||
copyJobState: {
|
||||
jobName: "",
|
||||
migrationType: CopyJobMigrationType.Online,
|
||||
source: {
|
||||
subscription: null as any,
|
||||
account: null as any,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
target: {
|
||||
subscriptionId: "",
|
||||
account: null as any,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
sourceReadAccessFromTarget: false,
|
||||
},
|
||||
setCopyJobState: mockSetCopyJobState,
|
||||
flow: { currentScreen: "selectAccount" },
|
||||
setFlow: jest.fn(),
|
||||
contextError: "",
|
||||
setContextError: jest.fn(),
|
||||
explorer: {} as any,
|
||||
resetCopyJobState: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(useCopyJobContext as jest.Mock).mockReturnValue(defaultContextValue);
|
||||
});
|
||||
|
||||
describe("Component Rendering", () => {
|
||||
it("should render migration type component with radio buttons", () => {
|
||||
const { container } = render(<MigrationType />);
|
||||
|
||||
expect(screen.getByTestId("migration-type")).toBeInTheDocument();
|
||||
expect(screen.getByRole("radiogroup")).toBeInTheDocument();
|
||||
|
||||
const offlineRadio = screen.getByRole("radio", {
|
||||
name: ContainerCopyMessages.migrationTypeOptions.offline.title,
|
||||
});
|
||||
const onlineRadio = screen.getByRole("radio", { name: ContainerCopyMessages.migrationTypeOptions.online.title });
|
||||
|
||||
expect(offlineRadio).toBeInTheDocument();
|
||||
expect(onlineRadio).toBeInTheDocument();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should render with online mode selected by default", () => {
|
||||
render(<MigrationType />);
|
||||
|
||||
const onlineRadio = screen.getByRole("radio", { name: ContainerCopyMessages.migrationTypeOptions.online.title });
|
||||
const offlineRadio = screen.getByRole("radio", {
|
||||
name: ContainerCopyMessages.migrationTypeOptions.offline.title,
|
||||
});
|
||||
|
||||
expect(onlineRadio).toBeChecked();
|
||||
expect(offlineRadio).not.toBeChecked();
|
||||
});
|
||||
|
||||
it("should render with offline mode selected when state is offline", () => {
|
||||
(useCopyJobContext as jest.Mock).mockReturnValue({
|
||||
...defaultContextValue,
|
||||
copyJobState: {
|
||||
...defaultContextValue.copyJobState,
|
||||
migrationType: CopyJobMigrationType.Offline,
|
||||
},
|
||||
});
|
||||
|
||||
render(<MigrationType />);
|
||||
|
||||
const offlineRadio = screen.getByRole("radio", {
|
||||
name: ContainerCopyMessages.migrationTypeOptions.offline.title,
|
||||
});
|
||||
const onlineRadio = screen.getByRole("radio", { name: ContainerCopyMessages.migrationTypeOptions.online.title });
|
||||
|
||||
expect(offlineRadio).toBeChecked();
|
||||
expect(onlineRadio).not.toBeChecked();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Descriptions and Learn More Links", () => {
|
||||
it("should render online description and learn more link when online is selected", () => {
|
||||
render(<MigrationType />);
|
||||
|
||||
expect(screen.getByText(ContainerCopyMessages.migrationTypeOptions.online.description)).toBeInTheDocument();
|
||||
expect(screen.getByTestId("migration-type-description-online")).toBeInTheDocument();
|
||||
|
||||
const learnMoreLink = screen.getByRole("link", {
|
||||
name: ContainerCopyMessages.migrationTypeOptions.online.learnMoreText,
|
||||
});
|
||||
expect(learnMoreLink).toBeInTheDocument();
|
||||
expect(learnMoreLink).toHaveAttribute("href", ContainerCopyMessages.migrationTypeOptions.online.learnMoreHref);
|
||||
expect(learnMoreLink).toHaveAttribute("target", "_blank");
|
||||
expect(learnMoreLink).toHaveAttribute("rel", "noopener noreferrer");
|
||||
});
|
||||
|
||||
it("should render offline description and learn more link when offline is selected", () => {
|
||||
(useCopyJobContext as jest.Mock).mockReturnValue({
|
||||
...defaultContextValue,
|
||||
copyJobState: {
|
||||
...defaultContextValue.copyJobState,
|
||||
migrationType: CopyJobMigrationType.Offline,
|
||||
},
|
||||
});
|
||||
|
||||
render(<MigrationType />);
|
||||
|
||||
expect(screen.getByText(ContainerCopyMessages.migrationTypeOptions.offline.description)).toBeInTheDocument();
|
||||
expect(screen.getByTestId("migration-type-description-offline")).toBeInTheDocument();
|
||||
|
||||
const learnMoreLink = screen.getByRole("link", {
|
||||
name: ContainerCopyMessages.migrationTypeOptions.offline.learnMoreText,
|
||||
});
|
||||
expect(learnMoreLink).toBeInTheDocument();
|
||||
expect(learnMoreLink).toHaveAttribute("href", ContainerCopyMessages.migrationTypeOptions.offline.learnMoreHref);
|
||||
});
|
||||
});
|
||||
|
||||
describe("User Interactions", () => {
|
||||
it("should call setCopyJobState when offline radio button is clicked", () => {
|
||||
render(<MigrationType />);
|
||||
|
||||
const offlineRadio = screen.getByRole("radio", {
|
||||
name: ContainerCopyMessages.migrationTypeOptions.offline.title,
|
||||
});
|
||||
fireEvent.click(offlineRadio);
|
||||
|
||||
expect(mockSetCopyJobState).toHaveBeenCalledWith(expect.any(Function));
|
||||
|
||||
const updateFunction = mockSetCopyJobState.mock.calls[0][0];
|
||||
const result = updateFunction(defaultContextValue.copyJobState);
|
||||
|
||||
expect(result).toEqual({
|
||||
...defaultContextValue.copyJobState,
|
||||
migrationType: CopyJobMigrationType.Offline,
|
||||
});
|
||||
});
|
||||
|
||||
it("should call setCopyJobState when online radio button is clicked", () => {
|
||||
(useCopyJobContext as jest.Mock).mockReturnValue({
|
||||
...defaultContextValue,
|
||||
copyJobState: {
|
||||
...defaultContextValue.copyJobState,
|
||||
migrationType: CopyJobMigrationType.Offline,
|
||||
},
|
||||
});
|
||||
|
||||
render(<MigrationType />);
|
||||
|
||||
const onlineRadio = screen.getByRole("radio", { name: ContainerCopyMessages.migrationTypeOptions.online.title });
|
||||
fireEvent.click(onlineRadio);
|
||||
|
||||
expect(mockSetCopyJobState).toHaveBeenCalledWith(expect.any(Function));
|
||||
|
||||
const updateFunction = mockSetCopyJobState.mock.calls[0][0];
|
||||
const result = updateFunction({
|
||||
...defaultContextValue.copyJobState,
|
||||
migrationType: CopyJobMigrationType.Offline,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
...defaultContextValue.copyJobState,
|
||||
migrationType: CopyJobMigrationType.Online,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Accessibility", () => {
|
||||
it("should have proper ARIA attributes", () => {
|
||||
render(<MigrationType />);
|
||||
|
||||
const choiceGroup = screen.getByRole("radiogroup");
|
||||
expect(choiceGroup).toBeInTheDocument();
|
||||
expect(choiceGroup).toHaveAttribute("aria-labelledby", "migrationTypeChoiceGroup");
|
||||
});
|
||||
|
||||
it("should have proper radio button labels", () => {
|
||||
render(<MigrationType />);
|
||||
|
||||
expect(
|
||||
screen.getByRole("radio", { name: ContainerCopyMessages.migrationTypeOptions.offline.title }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("radio", { name: ContainerCopyMessages.migrationTypeOptions.online.title }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Edge Cases", () => {
|
||||
it("should handle undefined migration type gracefully", () => {
|
||||
(useCopyJobContext as jest.Mock).mockReturnValue({
|
||||
...defaultContextValue,
|
||||
copyJobState: {
|
||||
...defaultContextValue.copyJobState,
|
||||
migrationType: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
render(<MigrationType />);
|
||||
|
||||
expect(screen.getByTestId("migration-type")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("radio", { name: ContainerCopyMessages.migrationTypeOptions.offline.title }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("radio", { name: ContainerCopyMessages.migrationTypeOptions.online.title }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle null copyJobState gracefully", () => {
|
||||
(useCopyJobContext as jest.Mock).mockReturnValue({
|
||||
...defaultContextValue,
|
||||
copyJobState: null,
|
||||
});
|
||||
|
||||
render(<MigrationType />);
|
||||
|
||||
expect(screen.getByTestId("migration-type")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
/* eslint-disable react/display-name */
|
||||
import { ChoiceGroup, IChoiceGroupOption, Link, Stack, Text } from "@fluentui/react";
|
||||
import { useCopyJobContext } from "Explorer/ContainerCopy/Context/CopyJobContext";
|
||||
import React from "react";
|
||||
import ContainerCopyMessages from "../../../../ContainerCopyMessages";
|
||||
import { CopyJobMigrationType } from "../../../../Enums/CopyJobEnums";
|
||||
|
||||
interface MigrationTypeProps {}
|
||||
const options: IChoiceGroupOption[] = [
|
||||
{
|
||||
key: CopyJobMigrationType.Offline,
|
||||
text: ContainerCopyMessages.migrationTypeOptions.offline.title,
|
||||
styles: { root: { width: "33%" } },
|
||||
},
|
||||
{
|
||||
key: CopyJobMigrationType.Online,
|
||||
text: ContainerCopyMessages.migrationTypeOptions.online.title,
|
||||
styles: { root: { width: "33%" } },
|
||||
},
|
||||
];
|
||||
|
||||
export const MigrationType: React.FC<MigrationTypeProps> = React.memo(() => {
|
||||
const { copyJobState, setCopyJobState } = useCopyJobContext();
|
||||
const handleChange = (_ev?: React.FormEvent, option?: IChoiceGroupOption) => {
|
||||
if (option) {
|
||||
setCopyJobState((prevState) => ({
|
||||
...prevState,
|
||||
migrationType: option.key as CopyJobMigrationType,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const selectedKey = copyJobState?.migrationType ?? "";
|
||||
const selectedKeyLowercase = selectedKey.toLowerCase() as keyof typeof ContainerCopyMessages.migrationTypeOptions;
|
||||
const selectedKeyContent = ContainerCopyMessages.migrationTypeOptions[selectedKeyLowercase];
|
||||
|
||||
return (
|
||||
<Stack data-testid="migration-type" className="migrationTypeContainer">
|
||||
<Stack.Item>
|
||||
<ChoiceGroup
|
||||
selectedKey={selectedKey}
|
||||
options={options}
|
||||
onChange={handleChange}
|
||||
ariaLabelledBy="migrationTypeChoiceGroup"
|
||||
styles={{ flexContainer: { display: "flex" } }}
|
||||
/>
|
||||
</Stack.Item>
|
||||
{selectedKeyContent && (
|
||||
<Stack.Item styles={{ root: { marginTop: 10 } }}>
|
||||
<Text
|
||||
variant="small"
|
||||
className="migrationTypeDescription"
|
||||
data-testid={`migration-type-description-${selectedKeyLowercase}`}
|
||||
>
|
||||
{selectedKeyContent.description}{" "}
|
||||
<Link href={selectedKeyContent.learnMoreHref} target="_blank" rel="noopener noreferrer">
|
||||
{selectedKeyContent.learnMoreText}
|
||||
</Link>
|
||||
</Text>
|
||||
</Stack.Item>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
});
|
||||
@@ -1,72 +0,0 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,16 +0,0 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
/* eslint-disable react/display-name */
|
||||
import { Checkbox, Stack } from "@fluentui/react";
|
||||
import React from "react";
|
||||
import ContainerCopyMessages from "../../../../ContainerCopyMessages";
|
||||
|
||||
interface MigrationTypeCheckboxProps {
|
||||
checked: boolean;
|
||||
onChange: (_ev?: React.FormEvent, checked?: boolean) => void;
|
||||
}
|
||||
|
||||
export const MigrationTypeCheckbox: React.FC<MigrationTypeCheckboxProps> = React.memo(({ checked, onChange }) => (
|
||||
<Stack horizontal horizontalAlign="space-between" className="migrationTypeRow">
|
||||
<Checkbox label={ContainerCopyMessages.migrationTypeCheckboxLabel} checked={checked} onChange={onChange} />
|
||||
</Stack>
|
||||
));
|
||||
@@ -0,0 +1,98 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`MigrationType Component Rendering should render migration type component with radio buttons 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="ms-Stack migrationTypeContainer css-109"
|
||||
data-testid="migration-type"
|
||||
>
|
||||
<div
|
||||
class="ms-StackItem css-110"
|
||||
>
|
||||
<div
|
||||
class="ms-ChoiceFieldGroup root-111"
|
||||
>
|
||||
<div
|
||||
aria-labelledby="migrationTypeChoiceGroup"
|
||||
role="radiogroup"
|
||||
>
|
||||
<div
|
||||
class="ms-ChoiceFieldGroup-flexContainer flexContainer-112"
|
||||
>
|
||||
<div
|
||||
class="ms-ChoiceField root-113"
|
||||
>
|
||||
<div
|
||||
class="ms-ChoiceField-wrapper"
|
||||
>
|
||||
<input
|
||||
class="ms-ChoiceField-input input-114"
|
||||
id="ChoiceGroup0-offline"
|
||||
name="ChoiceGroup0"
|
||||
type="radio"
|
||||
/>
|
||||
<label
|
||||
class="ms-ChoiceField-field field-115"
|
||||
for="ChoiceGroup0-offline"
|
||||
>
|
||||
<span
|
||||
class="ms-ChoiceFieldLabel"
|
||||
id="ChoiceGroupLabel1-offline"
|
||||
>
|
||||
Offline mode
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ms-ChoiceField root-113"
|
||||
>
|
||||
<div
|
||||
class="ms-ChoiceField-wrapper"
|
||||
>
|
||||
<input
|
||||
checked=""
|
||||
class="ms-ChoiceField-input input-114"
|
||||
id="ChoiceGroup0-online"
|
||||
name="ChoiceGroup0"
|
||||
type="radio"
|
||||
/>
|
||||
<label
|
||||
class="ms-ChoiceField-field is-checked field-120"
|
||||
for="ChoiceGroup0-online"
|
||||
>
|
||||
<span
|
||||
class="ms-ChoiceFieldLabel"
|
||||
id="ChoiceGroupLabel1-online"
|
||||
>
|
||||
Online mode
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ms-StackItem css-123"
|
||||
>
|
||||
<span
|
||||
class="migrationTypeDescription css-124"
|
||||
data-testid="migration-type-description-online"
|
||||
>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
|
||||
|
||||
<a
|
||||
class="ms-Link root-125"
|
||||
href="https://learn.microsoft.com/en-us/azure/cosmos-db/container-copy?tabs=offline-copy"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,80 +0,0 @@
|
||||
// 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>
|
||||
`;
|
||||
@@ -1,5 +1,5 @@
|
||||
import "@testing-library/jest-dom";
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import React from "react";
|
||||
import { useCopyJobContext } from "../../../Context/CopyJobContext";
|
||||
import { CopyJobMigrationType } from "../../../Enums/CopyJobEnums";
|
||||
@@ -18,19 +18,8 @@ jest.mock("./Components/AccountDropdown", () => ({
|
||||
AccountDropdown: jest.fn(() => <div data-testid="account-dropdown">Account Dropdown</div>),
|
||||
}));
|
||||
|
||||
jest.mock("./Components/MigrationTypeCheckbox", () => ({
|
||||
MigrationTypeCheckbox: jest.fn(({ checked, onChange }: { checked: boolean; onChange: () => void }) => (
|
||||
<div data-testid="migration-type-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
onChange={onChange}
|
||||
data-testid="migration-checkbox-input"
|
||||
aria-label="Migration Type Checkbox"
|
||||
/>
|
||||
Copy container in offline mode
|
||||
</div>
|
||||
)),
|
||||
jest.mock("./Components/MigrationType", () => ({
|
||||
MigrationType: jest.fn(() => <div data-testid="migration-type">Migration Type</div>),
|
||||
}));
|
||||
|
||||
describe("SelectAccount", () => {
|
||||
@@ -83,7 +72,7 @@ describe("SelectAccount", () => {
|
||||
|
||||
expect(screen.getByTestId("subscription-dropdown")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("account-dropdown")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("migration-type-checkbox")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("migration-type")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render correctly with snapshot", () => {
|
||||
@@ -93,78 +82,20 @@ describe("SelectAccount", () => {
|
||||
});
|
||||
|
||||
describe("Migration Type Functionality", () => {
|
||||
it("should display migration type checkbox as unchecked when migrationType is Online", () => {
|
||||
(useCopyJobContext as jest.Mock).mockReturnValue({
|
||||
...defaultContextValue,
|
||||
copyJobState: {
|
||||
...defaultContextValue.copyJobState,
|
||||
migrationType: CopyJobMigrationType.Online,
|
||||
},
|
||||
});
|
||||
|
||||
it("should render migration type component", () => {
|
||||
render(<SelectAccount />);
|
||||
|
||||
const checkbox = screen.getByTestId("migration-checkbox-input");
|
||||
expect(checkbox).not.toBeChecked();
|
||||
});
|
||||
|
||||
it("should display migration type checkbox as checked when migrationType is Offline", () => {
|
||||
(useCopyJobContext as jest.Mock).mockReturnValue({
|
||||
...defaultContextValue,
|
||||
copyJobState: {
|
||||
...defaultContextValue.copyJobState,
|
||||
migrationType: CopyJobMigrationType.Offline,
|
||||
},
|
||||
});
|
||||
|
||||
render(<SelectAccount />);
|
||||
|
||||
const checkbox = screen.getByTestId("migration-checkbox-input");
|
||||
expect(checkbox).toBeChecked();
|
||||
});
|
||||
|
||||
it("should call setCopyJobState with Online migration type when checkbox is unchecked", () => {
|
||||
(useCopyJobContext as jest.Mock).mockReturnValue({
|
||||
...defaultContextValue,
|
||||
copyJobState: {
|
||||
...defaultContextValue.copyJobState,
|
||||
migrationType: CopyJobMigrationType.Offline,
|
||||
},
|
||||
});
|
||||
|
||||
render(<SelectAccount />);
|
||||
|
||||
const checkbox = screen.getByTestId("migration-checkbox-input");
|
||||
fireEvent.click(checkbox);
|
||||
|
||||
expect(mockSetCopyJobState).toHaveBeenCalledWith(expect.any(Function));
|
||||
|
||||
const updateFunction = mockSetCopyJobState.mock.calls[0][0];
|
||||
const previousState = {
|
||||
...defaultContextValue.copyJobState,
|
||||
migrationType: CopyJobMigrationType.Offline,
|
||||
};
|
||||
const result = updateFunction(previousState);
|
||||
|
||||
expect(result).toEqual({
|
||||
...previousState,
|
||||
migrationType: CopyJobMigrationType.Online,
|
||||
});
|
||||
const migrationTypeComponent = screen.getByTestId("migration-type");
|
||||
expect(migrationTypeComponent).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Performance and Optimization", () => {
|
||||
it("should maintain referential equality of handler functions between renders", async () => {
|
||||
it("should render without performance issues", () => {
|
||||
const { rerender } = render(<SelectAccount />);
|
||||
|
||||
const migrationCheckbox = (await import("./Components/MigrationTypeCheckbox")).MigrationTypeCheckbox as jest.Mock;
|
||||
const firstRenderHandler = migrationCheckbox.mock.calls[migrationCheckbox.mock.calls.length - 1][0].onChange;
|
||||
|
||||
rerender(<SelectAccount />);
|
||||
|
||||
const secondRenderHandler = migrationCheckbox.mock.calls[migrationCheckbox.mock.calls.length - 1][0].onChange;
|
||||
|
||||
expect(firstRenderHandler).toBe(secondRenderHandler);
|
||||
expect(screen.getByTestId("migration-type")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,24 +1,11 @@
|
||||
import { Stack, Text } from "@fluentui/react";
|
||||
import React from "react";
|
||||
import ContainerCopyMessages from "../../../ContainerCopyMessages";
|
||||
import { useCopyJobContext } from "../../../Context/CopyJobContext";
|
||||
import { CopyJobMigrationType } from "../../../Enums/CopyJobEnums";
|
||||
import { AccountDropdown } from "./Components/AccountDropdown";
|
||||
import { MigrationTypeCheckbox } from "./Components/MigrationTypeCheckbox";
|
||||
import { MigrationType } from "./Components/MigrationType";
|
||||
import { SubscriptionDropdown } from "./Components/SubscriptionDropdown";
|
||||
|
||||
const SelectAccount = React.memo(() => {
|
||||
const { copyJobState, setCopyJobState } = useCopyJobContext();
|
||||
|
||||
const handleMigrationTypeChange = (_ev?: React.FormEvent<HTMLElement>, checked?: boolean) => {
|
||||
setCopyJobState((prevState) => ({
|
||||
...prevState,
|
||||
migrationType: checked ? CopyJobMigrationType.Offline : CopyJobMigrationType.Online,
|
||||
}));
|
||||
};
|
||||
|
||||
const migrationTypeChecked = copyJobState?.migrationType === CopyJobMigrationType.Offline;
|
||||
|
||||
return (
|
||||
<Stack data-test="Panel:SelectAccountContainer" className="selectAccountContainer" tokens={{ childrenGap: 15 }}>
|
||||
<Text>{ContainerCopyMessages.selectAccountDescription}</Text>
|
||||
@@ -27,7 +14,7 @@ const SelectAccount = React.memo(() => {
|
||||
|
||||
<AccountDropdown />
|
||||
|
||||
<MigrationTypeCheckbox checked={migrationTypeChecked} onChange={handleMigrationTypeChange} />
|
||||
<MigrationType />
|
||||
</Stack>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -21,14 +21,9 @@ exports[`SelectAccount Component Rendering should render correctly with snapshot
|
||||
Account Dropdown
|
||||
</div>
|
||||
<div
|
||||
data-testid="migration-type-checkbox"
|
||||
data-testid="migration-type"
|
||||
>
|
||||
<input
|
||||
aria-label="Migration Type Checkbox"
|
||||
data-testid="migration-checkbox-input"
|
||||
type="checkbox"
|
||||
/>
|
||||
Copy container in offline mode
|
||||
Migration Type
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -5,6 +5,20 @@ import { CopyJobActions, CopyJobMigrationType, CopyJobStatusType } from "../../E
|
||||
import { CopyJobType, HandleJobActionClickType } from "../../Types/CopyJobTypes";
|
||||
import CopyJobActionMenu from "./CopyJobActionMenu";
|
||||
|
||||
const mockShowOkCancelModalDialog = jest.fn();
|
||||
const mockCloseDialog = jest.fn();
|
||||
const mockOpenDialog = jest.fn();
|
||||
|
||||
jest.mock("../../../Controls/Dialog", () => ({
|
||||
useDialog: {
|
||||
getState: () => ({
|
||||
showOkCancelModalDialog: mockShowOkCancelModalDialog,
|
||||
closeDialog: mockCloseDialog,
|
||||
openDialog: mockOpenDialog,
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../../ContainerCopyMessages", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
@@ -18,6 +32,11 @@ jest.mock("../../ContainerCopyMessages", () => ({
|
||||
cancel: "Cancel",
|
||||
complete: "Complete",
|
||||
},
|
||||
dialog: {
|
||||
heading: "Confirm Action",
|
||||
confirmButtonText: "OK",
|
||||
cancelButtonText: "Cancel",
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
@@ -50,6 +69,9 @@ describe("CopyJobActionMenu", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockShowOkCancelModalDialog.mockClear();
|
||||
mockCloseDialog.mockClear();
|
||||
mockOpenDialog.mockClear();
|
||||
});
|
||||
|
||||
describe("Component Rendering", () => {
|
||||
@@ -266,7 +288,29 @@ describe("CopyJobActionMenu", () => {
|
||||
expect(mockHandleClick).toHaveBeenCalledWith(job, CopyJobActions.pause, expect.any(Function));
|
||||
});
|
||||
|
||||
it("should call handleClick when cancel action is clicked", () => {
|
||||
it("should show confirmation dialog when cancel action is clicked", () => {
|
||||
const job = createMockJob({ Name: "Test Job", 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(mockShowOkCancelModalDialog).toHaveBeenCalledWith(
|
||||
"Confirm Action",
|
||||
null,
|
||||
"OK",
|
||||
expect.any(Function),
|
||||
"Cancel",
|
||||
null,
|
||||
expect.any(Object), // dialogBody content
|
||||
);
|
||||
});
|
||||
|
||||
it("should call handleClick when dialog is confirmed for cancel action", () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.InProgress });
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
@@ -277,6 +321,9 @@ describe("CopyJobActionMenu", () => {
|
||||
const cancelButton = screen.getByText("Cancel");
|
||||
fireEvent.click(cancelButton);
|
||||
|
||||
const [, , , onOkCallback] = mockShowOkCancelModalDialog.mock.calls[0];
|
||||
onOkCallback();
|
||||
|
||||
expect(mockHandleClick).toHaveBeenCalledWith(job, CopyJobActions.cancel, expect.any(Function));
|
||||
});
|
||||
|
||||
@@ -294,7 +341,33 @@ describe("CopyJobActionMenu", () => {
|
||||
expect(mockHandleClick).toHaveBeenCalledWith(job, CopyJobActions.resume, expect.any(Function));
|
||||
});
|
||||
|
||||
it("should call handleClick when complete action is clicked", () => {
|
||||
it("should show confirmation dialog when complete action is clicked", () => {
|
||||
const job = createMockJob({
|
||||
Name: "Test Online Job",
|
||||
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(mockShowOkCancelModalDialog).toHaveBeenCalledWith(
|
||||
"Confirm Action",
|
||||
null,
|
||||
"OK",
|
||||
expect.any(Function),
|
||||
"Cancel",
|
||||
null,
|
||||
expect.any(Object), // dialogBody content
|
||||
);
|
||||
});
|
||||
|
||||
it("should call handleClick when dialog is confirmed for complete action", () => {
|
||||
const job = createMockJob({
|
||||
Status: CopyJobStatusType.InProgress,
|
||||
Mode: CopyJobMigrationType.Online,
|
||||
@@ -308,10 +381,87 @@ describe("CopyJobActionMenu", () => {
|
||||
const completeButton = screen.getByText("Complete");
|
||||
fireEvent.click(completeButton);
|
||||
|
||||
const [, , , onOkCallback] = mockShowOkCancelModalDialog.mock.calls[0];
|
||||
onOkCallback();
|
||||
|
||||
expect(mockHandleClick).toHaveBeenCalledWith(job, CopyJobActions.complete, expect.any(Function));
|
||||
});
|
||||
});
|
||||
|
||||
describe("Dialog Body Content", () => {
|
||||
it("should pass correct dialog body content for cancel action", () => {
|
||||
const job = createMockJob({ Name: "MyTestJob", 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(mockShowOkCancelModalDialog).toHaveBeenCalledWith(
|
||||
"Confirm Action",
|
||||
null,
|
||||
"OK",
|
||||
expect.any(Function),
|
||||
"Cancel",
|
||||
null,
|
||||
expect.objectContaining({
|
||||
props: expect.objectContaining({
|
||||
tokens: expect.any(Object),
|
||||
children: expect.any(Array),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should pass correct dialog body content for complete action", () => {
|
||||
const job = createMockJob({
|
||||
Name: "OnlineTestJob",
|
||||
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(mockShowOkCancelModalDialog).toHaveBeenCalledWith(
|
||||
"Confirm Action",
|
||||
null,
|
||||
"OK",
|
||||
expect.any(Function),
|
||||
"Cancel",
|
||||
null,
|
||||
expect.objectContaining({
|
||||
props: expect.objectContaining({
|
||||
tokens: expect.any(Object),
|
||||
children: expect.any(Array),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should not show dialog body for actions without confirmation", () => {
|
||||
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(mockShowOkCancelModalDialog).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Disabled States During Updates", () => {
|
||||
const TestComponentWrapper: React.FC<{
|
||||
job: CopyJobType;
|
||||
@@ -339,8 +489,13 @@ describe("CopyJobActionMenu", () => {
|
||||
const pauseButton = screen.getByText("Pause");
|
||||
fireEvent.click(pauseButton);
|
||||
fireEvent.click(actionButton);
|
||||
const pauseButtonAfterClick = screen.getByText("Pause");
|
||||
|
||||
const pauseButtonAfterClick = screen.getByText("Pause").closest("button");
|
||||
expect(pauseButtonAfterClick).toBeInTheDocument();
|
||||
expect(pauseButtonAfterClick).toHaveAttribute("aria-disabled", "true");
|
||||
|
||||
const cancelButtonAfterClick = screen.getByText("Cancel").closest("button");
|
||||
expect(cancelButtonAfterClick).toHaveAttribute("aria-disabled", "true");
|
||||
});
|
||||
|
||||
it("should not disable actions for different jobs when one is updating", () => {
|
||||
@@ -360,22 +515,6 @@ describe("CopyJobActionMenu", () => {
|
||||
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,
|
||||
@@ -462,6 +601,7 @@ describe("CopyJobActionMenu", () => {
|
||||
|
||||
expect(actionButton).toHaveAttribute("aria-label", "Actions");
|
||||
expect(actionButton).toHaveAttribute("title", "Actions");
|
||||
expect(actionButton).toHaveAttribute("role", "button");
|
||||
|
||||
const moreIcon = actionButton.querySelector('[data-icon-name="More"]');
|
||||
expect(moreIcon || actionButton).toBeInTheDocument();
|
||||
@@ -608,4 +748,129 @@ describe("CopyJobActionMenu", () => {
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Complete Coverage Tests", () => {
|
||||
it("should handle all possible dialog scenarios", () => {
|
||||
const dialogTests = [
|
||||
{ action: CopyJobActions.cancel, status: CopyJobStatusType.InProgress, shouldShowDialog: true },
|
||||
{
|
||||
action: CopyJobActions.complete,
|
||||
status: CopyJobStatusType.InProgress,
|
||||
mode: CopyJobMigrationType.Online,
|
||||
shouldShowDialog: true,
|
||||
},
|
||||
{ action: CopyJobActions.pause, status: CopyJobStatusType.InProgress, shouldShowDialog: false },
|
||||
{ action: CopyJobActions.resume, status: CopyJobStatusType.Paused, shouldShowDialog: false },
|
||||
];
|
||||
|
||||
dialogTests.forEach(({ action, status, mode = CopyJobMigrationType.Offline, shouldShowDialog }, index) => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
const job = createMockJob({ Status: status, Mode: mode, Name: `DialogTestJob${index}` });
|
||||
const { unmount } = render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
const actionText = action.charAt(0).toUpperCase() + action.slice(1);
|
||||
if (screen.queryByText(actionText)) {
|
||||
fireEvent.click(screen.getByText(actionText));
|
||||
|
||||
if (shouldShowDialog) {
|
||||
expect(mockShowOkCancelModalDialog).toHaveBeenCalled();
|
||||
} else {
|
||||
expect(mockShowOkCancelModalDialog).not.toHaveBeenCalled();
|
||||
expect(mockHandleClick).toHaveBeenCalled();
|
||||
}
|
||||
}
|
||||
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
it("should verify component handles state updates correctly", () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.InProgress });
|
||||
const stateUpdater = jest.fn();
|
||||
|
||||
const testHandleClick: HandleJobActionClickType = (job, action, setUpdatingJobAction) => {
|
||||
setUpdatingJobAction({ jobName: job.Name, action });
|
||||
stateUpdater(job.Name, action);
|
||||
};
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={testHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
const pauseButton = screen.getByText("Pause");
|
||||
fireEvent.click(pauseButton);
|
||||
|
||||
expect(stateUpdater).toHaveBeenCalledWith(job.Name, CopyJobActions.pause);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Full Integration Coverage", () => {
|
||||
it("should test complete workflow for cancel action with dialog", () => {
|
||||
const job = createMockJob({ Name: "Integration Test Job", Status: CopyJobStatusType.InProgress });
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
expect(actionButton).toHaveAttribute("data-test", "CopyJobActionMenu/Button:Integration Test Job");
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
const cancelButton = screen.getByText("Cancel");
|
||||
fireEvent.click(cancelButton);
|
||||
|
||||
expect(mockShowOkCancelModalDialog).toHaveBeenCalledWith(
|
||||
"Confirm Action", // title
|
||||
null, // subText
|
||||
"OK", // okLabel
|
||||
expect.any(Function), // onOk
|
||||
"Cancel", // cancelLabel
|
||||
null, // onCancel
|
||||
expect.any(Object), // contentHtml (dialogBody)
|
||||
);
|
||||
|
||||
const onOkCallback = mockShowOkCancelModalDialog.mock.calls[0][3];
|
||||
onOkCallback();
|
||||
|
||||
expect(mockHandleClick).toHaveBeenCalledWith(job, CopyJobActions.cancel, expect.any(Function));
|
||||
});
|
||||
|
||||
it("should test complete workflow for complete action with dialog", () => {
|
||||
const job = createMockJob({
|
||||
Name: "Online Integration Job",
|
||||
Status: CopyJobStatusType.Running,
|
||||
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(mockShowOkCancelModalDialog).toHaveBeenCalled();
|
||||
|
||||
const dialogContent = mockShowOkCancelModalDialog.mock.calls[0][6];
|
||||
expect(dialogContent).toBeTruthy();
|
||||
|
||||
const onOkCallback = mockShowOkCancelModalDialog.mock.calls[0][3];
|
||||
onOkCallback();
|
||||
|
||||
expect(mockHandleClick).toHaveBeenCalledWith(job, CopyJobActions.complete, expect.any(Function));
|
||||
});
|
||||
|
||||
it("should maintain proper component lifecycle", () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.InProgress });
|
||||
const { rerender, unmount } = render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
rerender(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
expect(screen.getByRole("button", { name: "Actions" })).toBeInTheDocument();
|
||||
|
||||
expect(() => unmount()).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { IconButton, IContextualMenuProps } from "@fluentui/react";
|
||||
import { DirectionalHint, IconButton, IContextualMenuProps, Stack } from "@fluentui/react";
|
||||
import React from "react";
|
||||
import { useDialog } from "../../../Controls/Dialog";
|
||||
import ContainerCopyMessages from "../../ContainerCopyMessages";
|
||||
import { CopyJobActions, CopyJobMigrationType, CopyJobStatusType } from "../../Enums/CopyJobEnums";
|
||||
import { CopyJobType, HandleJobActionClickType } from "../../Types/CopyJobTypes";
|
||||
@@ -9,6 +10,35 @@ interface CopyJobActionMenuProps {
|
||||
handleClick: HandleJobActionClickType;
|
||||
}
|
||||
|
||||
const dialogBody = {
|
||||
[CopyJobActions.cancel]: (jobName: string) => (
|
||||
<Stack tokens={{ childrenGap: 10 }}>
|
||||
<Stack.Item>
|
||||
You are about to cancel <b>{jobName}</b>:
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
Cancelling this job will stop it immediately. Any running or pending steps will not be completed.
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<b>This action cannot be undone. Do you want to continue?</b>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
),
|
||||
[CopyJobActions.complete]: (jobName: string) => (
|
||||
<Stack tokens={{ childrenGap: 10 }}>
|
||||
<Stack.Item>
|
||||
You are about to complete <b>{jobName}</b>:
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
Completing this job will stop the continuous data synchronization between the source and destination containers.
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<b>This action cannot be undone. Do you want to continue?</b>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
),
|
||||
};
|
||||
|
||||
const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick }) => {
|
||||
const [updatingJobAction, setUpdatingJobAction] = React.useState<{ jobName: string; action: string } | null>(null);
|
||||
if (
|
||||
@@ -22,9 +52,22 @@ const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick
|
||||
return null;
|
||||
}
|
||||
|
||||
const showActionConfirmationDialog = (job: CopyJobType, action: CopyJobActions): void => {
|
||||
useDialog
|
||||
.getState()
|
||||
.showOkCancelModalDialog(
|
||||
ContainerCopyMessages.MonitorJobs.dialog.heading,
|
||||
null,
|
||||
ContainerCopyMessages.MonitorJobs.dialog.confirmButtonText,
|
||||
() => handleClick(job, action, setUpdatingJobAction),
|
||||
ContainerCopyMessages.MonitorJobs.dialog.cancelButtonText,
|
||||
null,
|
||||
action in dialogBody ? dialogBody[action as keyof typeof dialogBody](job.Name) : null,
|
||||
);
|
||||
};
|
||||
|
||||
const getMenuItems = (): IContextualMenuProps["items"] => {
|
||||
const isThisJobUpdating = updatingJobAction?.jobName === job.Name;
|
||||
const updatingAction = updatingJobAction?.action;
|
||||
|
||||
const baseItems = [
|
||||
{
|
||||
@@ -32,21 +75,21 @@ const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick
|
||||
text: ContainerCopyMessages.MonitorJobs.Actions.pause,
|
||||
iconProps: { iconName: "Pause" },
|
||||
onClick: () => handleClick(job, CopyJobActions.pause, setUpdatingJobAction),
|
||||
disabled: isThisJobUpdating && updatingAction === CopyJobActions.pause,
|
||||
disabled: isThisJobUpdating,
|
||||
},
|
||||
{
|
||||
key: CopyJobActions.cancel,
|
||||
text: ContainerCopyMessages.MonitorJobs.Actions.cancel,
|
||||
iconProps: { iconName: "Cancel" },
|
||||
onClick: () => handleClick(job, CopyJobActions.cancel, setUpdatingJobAction),
|
||||
disabled: isThisJobUpdating && updatingAction === CopyJobActions.cancel,
|
||||
onClick: () => showActionConfirmationDialog(job, CopyJobActions.cancel),
|
||||
disabled: isThisJobUpdating,
|
||||
},
|
||||
{
|
||||
key: CopyJobActions.resume,
|
||||
text: ContainerCopyMessages.MonitorJobs.Actions.resume,
|
||||
iconProps: { iconName: "Play" },
|
||||
onClick: () => handleClick(job, CopyJobActions.resume, setUpdatingJobAction),
|
||||
disabled: isThisJobUpdating && updatingAction === CopyJobActions.resume,
|
||||
disabled: isThisJobUpdating,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -67,8 +110,8 @@ const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick
|
||||
key: CopyJobActions.complete,
|
||||
text: ContainerCopyMessages.MonitorJobs.Actions.complete,
|
||||
iconProps: { iconName: "CheckMark" },
|
||||
onClick: () => handleClick(job, CopyJobActions.complete, setUpdatingJobAction),
|
||||
disabled: isThisJobUpdating && updatingAction === CopyJobActions.complete,
|
||||
onClick: () => showActionConfirmationDialog(job, CopyJobActions.complete),
|
||||
disabled: isThisJobUpdating,
|
||||
});
|
||||
}
|
||||
return filteredItems;
|
||||
@@ -85,8 +128,8 @@ const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick
|
||||
<IconButton
|
||||
role="button"
|
||||
iconProps={{ iconName: "More", styles: { root: { fontSize: "20px", fontWeight: "bold" } } }}
|
||||
menuProps={{ items: getMenuItems() }}
|
||||
menuIconProps={{ iconName: "" }}
|
||||
menuProps={{ items: getMenuItems(), directionalHint: DirectionalHint.leftTopEdge, directionalHintFixed: false }}
|
||||
menuIconProps={{ iconName: "", className: "hidden" }}
|
||||
ariaLabel={ContainerCopyMessages.MonitorJobs.Columns.actions}
|
||||
title={ContainerCopyMessages.MonitorJobs.Columns.actions}
|
||||
/>
|
||||
|
||||
@@ -10,7 +10,7 @@ import CopyJobsNotFound from "../MonitorCopyJobs/Components/CopyJobs.NotFound";
|
||||
import { CopyJobType, JobActionUpdatorType } from "../Types/CopyJobTypes";
|
||||
import CopyJobsList from "./Components/CopyJobsList";
|
||||
|
||||
const FETCH_INTERVAL_MS = 30 * 1000;
|
||||
const FETCH_INTERVAL_MS = 60 * 1000;
|
||||
const SHIMMER_INDENT_LEVELS: IndentLevel[] = Array(7).fill({ level: 0, width: "100%" });
|
||||
|
||||
interface MonitorCopyJobsProps {
|
||||
|
||||
@@ -121,6 +121,9 @@
|
||||
&:hover {
|
||||
background-color: @BaseMediumLow;
|
||||
}
|
||||
.ms-DetailsHeader-cellTitle {
|
||||
padding-left: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ describe("PaneContainerComponent test", () => {
|
||||
headerText: "test",
|
||||
panelContent: <div></div>,
|
||||
isOpen: true,
|
||||
isLightDismiss: true,
|
||||
hasConsole: false,
|
||||
isConsoleExpanded: false,
|
||||
};
|
||||
@@ -20,6 +21,7 @@ describe("PaneContainerComponent test", () => {
|
||||
headerText: "test",
|
||||
panelContent: <div></div>,
|
||||
isOpen: true,
|
||||
isLightDismiss: true,
|
||||
hasConsole: true,
|
||||
isConsoleExpanded: false,
|
||||
};
|
||||
@@ -32,6 +34,7 @@ describe("PaneContainerComponent test", () => {
|
||||
headerText: "test",
|
||||
panelContent: undefined,
|
||||
isOpen: true,
|
||||
isLightDismiss: true,
|
||||
hasConsole: true,
|
||||
isConsoleExpanded: false,
|
||||
};
|
||||
@@ -44,6 +47,7 @@ describe("PaneContainerComponent test", () => {
|
||||
headerText: "test",
|
||||
panelContent: <div></div>,
|
||||
isOpen: true,
|
||||
isLightDismiss: true,
|
||||
hasConsole: true,
|
||||
isConsoleExpanded: true,
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface PanelContainerProps {
|
||||
isConsoleExpanded: boolean;
|
||||
isOpen: boolean;
|
||||
hasConsole: boolean;
|
||||
isLightDismiss: boolean;
|
||||
isConsoleAnimationFinished?: boolean;
|
||||
panelWidth?: string;
|
||||
onRenderNavigationContent?: IRenderFunction<IPanelProps>;
|
||||
@@ -58,7 +59,7 @@ export class PanelContainerComponent extends React.Component<PanelContainerProps
|
||||
headerText={this.props.headerText}
|
||||
isOpen={this.props.isOpen}
|
||||
onDismiss={this.onDissmiss}
|
||||
isLightDismiss
|
||||
isLightDismiss={this.props.isLightDismiss}
|
||||
type={PanelType.custom}
|
||||
closeButtonAriaLabel={`Close ${this.props.headerText}`}
|
||||
customWidth={this.props.panelWidth ? this.props.panelWidth : "440px"}
|
||||
@@ -140,10 +141,11 @@ export class PanelContainerComponent extends React.Component<PanelContainerProps
|
||||
export const SidePanel: React.FC = () => {
|
||||
const isConsoleExpanded = useNotificationConsole((state) => state.isExpanded);
|
||||
const isConsoleAnimationFinished = useNotificationConsole((state) => state.consoleAnimationFinished);
|
||||
const { isOpen, hasConsole, panelContent, panelWidth, headerText } = useSidePanel((state) => {
|
||||
const { isOpen, hasConsole, isLightDismiss, panelContent, panelWidth, headerText } = useSidePanel((state) => {
|
||||
return {
|
||||
isOpen: state.isOpen,
|
||||
hasConsole: state.hasConsole,
|
||||
isLightDismiss: state.isLightDismiss,
|
||||
panelContent: state.panelContent,
|
||||
headerText: state.headerText,
|
||||
panelWidth: state.panelWidth,
|
||||
@@ -154,6 +156,7 @@ export const SidePanel: React.FC = () => {
|
||||
return (
|
||||
<PanelContainerComponent
|
||||
hasConsole={hasConsole}
|
||||
isLightDismiss={isLightDismiss}
|
||||
isOpen={isOpen}
|
||||
panelContent={panelContent}
|
||||
headerText={headerText}
|
||||
|
||||
@@ -4,20 +4,24 @@ export interface SidePanelState {
|
||||
isOpen: boolean;
|
||||
panelWidth: string;
|
||||
hasConsole: boolean;
|
||||
isLightDismiss: boolean;
|
||||
panelContent?: JSX.Element;
|
||||
headerText?: string;
|
||||
setHeaderText: (headerText: string) => void;
|
||||
openSidePanel: (headerText: string, panelContent: JSX.Element, panelWidth?: string, onClose?: () => void) => void;
|
||||
closeSidePanel: () => void;
|
||||
setPanelHasConsole: (hasConsole: boolean) => void;
|
||||
setLightDismiss: (isLightDismiss: boolean) => void;
|
||||
getRef?: React.RefObject<HTMLElement>; // Optional ref for focusing the last element.
|
||||
}
|
||||
export const useSidePanel: UseStore<SidePanelState> = create((set) => ({
|
||||
isOpen: false,
|
||||
panelWidth: "440px",
|
||||
hasConsole: true,
|
||||
isLightDismiss: true,
|
||||
setHeaderText: (headerText: string) => set((state) => ({ ...state, headerText })),
|
||||
setPanelHasConsole: (hasConsole: boolean) => set((state) => ({ ...state, hasConsole })),
|
||||
setLightDismiss: (isLightDismiss: boolean) => set((state) => ({ ...state, isLightDismiss })),
|
||||
openSidePanel: (headerText, panelContent, panelWidth = "440px") =>
|
||||
set((state) => ({ ...state, headerText, panelContent, panelWidth, isOpen: true })),
|
||||
closeSidePanel: () => {
|
||||
|
||||
Reference in New Issue
Block a user