mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-05 18:47:41 +00:00
Compare commits
5 Commits
master
...
user/bchou
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf23889877 | ||
|
|
0ea8dffbf3 | ||
|
|
e03eb61117 | ||
|
|
2d7040d47c | ||
|
|
9b48b1ae56 |
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@@ -192,6 +192,9 @@ jobs:
|
|||||||
NOSQL_READONLY_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-readonly.documents.azure.com/.default" -o tsv --query accessToken)
|
NOSQL_READONLY_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-readonly.documents.azure.com/.default" -o tsv --query accessToken)
|
||||||
echo "::add-mask::$NOSQL_READONLY_TESTACCOUNT_TOKEN"
|
echo "::add-mask::$NOSQL_READONLY_TESTACCOUNT_TOKEN"
|
||||||
echo NOSQL_READONLY_TESTACCOUNT_TOKEN=$NOSQL_READONLY_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
echo NOSQL_READONLY_TESTACCOUNT_TOKEN=$NOSQL_READONLY_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||||
|
NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-containercopyonly.documents.azure.com/.default" -o tsv --query accessToken)
|
||||||
|
echo "::add-mask::$NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN"
|
||||||
|
echo NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN=$NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||||
TABLE_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-tables.documents.azure.com/.default" -o tsv --query accessToken)
|
TABLE_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-tables.documents.azure.com/.default" -o tsv --query accessToken)
|
||||||
echo "::add-mask::$TABLE_TESTACCOUNT_TOKEN"
|
echo "::add-mask::$TABLE_TESTACCOUNT_TOKEN"
|
||||||
echo TABLE_TESTACCOUNT_TOKEN=$TABLE_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
echo TABLE_TESTACCOUNT_TOKEN=$TABLE_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||||
|
|||||||
@@ -218,7 +218,6 @@ a:focus {
|
|||||||
|
|
||||||
.tabPanesContainer {
|
.tabPanesContainer {
|
||||||
overflow: auto !important;
|
overflow: auto !important;
|
||||||
display: flex;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs-container {
|
.tabs-container {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const LoadingOverlay: React.FC<LoadingOverlayProps> = ({ isLoading, label }) =>
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Overlay
|
<Overlay
|
||||||
|
data-test="loading-overlay"
|
||||||
styles={{
|
styles={{
|
||||||
root: {
|
root: {
|
||||||
backgroundColor: "rgba(255,255,255,0.9)",
|
backgroundColor: "rgba(255,255,255,0.9)",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
exports[`LoadingOverlay should handle long labels properly 1`] = `
|
exports[`LoadingOverlay should handle long labels properly 1`] = `
|
||||||
<div
|
<div
|
||||||
class="ms-Overlay root-109"
|
class="ms-Overlay root-109"
|
||||||
|
data-test="loading-overlay"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Spinner root-111"
|
class="ms-Spinner root-111"
|
||||||
@@ -22,6 +23,7 @@ exports[`LoadingOverlay should handle long labels properly 1`] = `
|
|||||||
exports[`LoadingOverlay should render loading overlay when isLoading is true 1`] = `
|
exports[`LoadingOverlay should render loading overlay when isLoading is true 1`] = `
|
||||||
<div
|
<div
|
||||||
class="ms-Overlay root-109"
|
class="ms-Overlay root-109"
|
||||||
|
data-test="loading-overlay"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Spinner root-111"
|
class="ms-Spinner root-111"
|
||||||
@@ -41,6 +43,7 @@ exports[`LoadingOverlay should render loading overlay when isLoading is true 1`]
|
|||||||
exports[`LoadingOverlay should render loading overlay with custom label 1`] = `
|
exports[`LoadingOverlay should render loading overlay with custom label 1`] = `
|
||||||
<div
|
<div
|
||||||
class="ms-Overlay root-109"
|
class="ms-Overlay root-109"
|
||||||
|
data-test="loading-overlay"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Spinner root-111"
|
class="ms-Spinner root-111"
|
||||||
@@ -60,6 +63,7 @@ exports[`LoadingOverlay should render loading overlay with custom label 1`] = `
|
|||||||
exports[`LoadingOverlay should render loading overlay with empty label 1`] = `
|
exports[`LoadingOverlay should render loading overlay with empty label 1`] = `
|
||||||
<div
|
<div
|
||||||
class="ms-Overlay root-109"
|
class="ms-Overlay root-109"
|
||||||
|
data-test="loading-overlay"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Spinner root-111"
|
class="ms-Spinner root-111"
|
||||||
|
|||||||
@@ -41,8 +41,10 @@ describe("CopyJobActions", () => {
|
|||||||
const mockExplorer = {} as Explorer;
|
const mockExplorer = {} as Explorer;
|
||||||
const mockSetPanelHasConsole = jest.fn();
|
const mockSetPanelHasConsole = jest.fn();
|
||||||
const mockOpenSidePanel = jest.fn();
|
const mockOpenSidePanel = jest.fn();
|
||||||
|
const mockSetLightDismiss = jest.fn();
|
||||||
|
|
||||||
(useSidePanel.getState as jest.Mock).mockReturnValue({
|
(useSidePanel.getState as jest.Mock).mockReturnValue({
|
||||||
|
setLightDismiss: mockSetLightDismiss,
|
||||||
setPanelHasConsole: mockSetPanelHasConsole,
|
setPanelHasConsole: mockSetPanelHasConsole,
|
||||||
openSidePanel: mockOpenSidePanel,
|
openSidePanel: mockOpenSidePanel,
|
||||||
});
|
});
|
||||||
@@ -50,6 +52,7 @@ describe("CopyJobActions", () => {
|
|||||||
openCreateCopyJobPanel(mockExplorer);
|
openCreateCopyJobPanel(mockExplorer);
|
||||||
|
|
||||||
expect(mockSetPanelHasConsole).toHaveBeenCalledWith(false);
|
expect(mockSetPanelHasConsole).toHaveBeenCalledWith(false);
|
||||||
|
expect(mockSetLightDismiss).toHaveBeenCalledWith(false);
|
||||||
expect(mockOpenSidePanel).toHaveBeenCalledWith(expect.any(String), expect.any(Object), "650px");
|
expect(mockOpenSidePanel).toHaveBeenCalledWith(expect.any(String), expect.any(Object), "650px");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -59,6 +62,7 @@ describe("CopyJobActions", () => {
|
|||||||
|
|
||||||
(useSidePanel.getState as jest.Mock).mockReturnValue({
|
(useSidePanel.getState as jest.Mock).mockReturnValue({
|
||||||
setPanelHasConsole: jest.fn(),
|
setPanelHasConsole: jest.fn(),
|
||||||
|
setLightDismiss: jest.fn(),
|
||||||
openSidePanel: mockOpenSidePanel,
|
openSidePanel: mockOpenSidePanel,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -94,9 +98,11 @@ describe("CopyJobActions", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const mockSetPanelHasConsole = jest.fn();
|
const mockSetPanelHasConsole = jest.fn();
|
||||||
|
const mockSetLightDismiss = jest.fn();
|
||||||
const mockOpenSidePanel = jest.fn();
|
const mockOpenSidePanel = jest.fn();
|
||||||
|
|
||||||
(useSidePanel.getState as jest.Mock).mockReturnValue({
|
(useSidePanel.getState as jest.Mock).mockReturnValue({
|
||||||
|
setLightDismiss: mockSetLightDismiss,
|
||||||
setPanelHasConsole: mockSetPanelHasConsole,
|
setPanelHasConsole: mockSetPanelHasConsole,
|
||||||
openSidePanel: mockOpenSidePanel,
|
openSidePanel: mockOpenSidePanel,
|
||||||
});
|
});
|
||||||
@@ -104,6 +110,7 @@ describe("CopyJobActions", () => {
|
|||||||
openCopyJobDetailsPanel(mockJob);
|
openCopyJobDetailsPanel(mockJob);
|
||||||
|
|
||||||
expect(mockSetPanelHasConsole).toHaveBeenCalledWith(false);
|
expect(mockSetPanelHasConsole).toHaveBeenCalledWith(false);
|
||||||
|
expect(mockSetLightDismiss).toHaveBeenCalledWith(true);
|
||||||
expect(mockOpenSidePanel).toHaveBeenCalledWith(expect.stringContaining("test-job"), expect.any(Object), "650px");
|
expect(mockOpenSidePanel).toHaveBeenCalledWith(expect.stringContaining("test-job"), expect.any(Object), "650px");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -133,6 +140,7 @@ describe("CopyJobActions", () => {
|
|||||||
|
|
||||||
(useSidePanel.getState as jest.Mock).mockReturnValue({
|
(useSidePanel.getState as jest.Mock).mockReturnValue({
|
||||||
setPanelHasConsole: jest.fn(),
|
setPanelHasConsole: jest.fn(),
|
||||||
|
setLightDismiss: jest.fn(),
|
||||||
openSidePanel: mockOpenSidePanel,
|
openSidePanel: mockOpenSidePanel,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import { CopyJobContextState, CopyJobError, CopyJobErrorType, CopyJobType } from
|
|||||||
export const openCreateCopyJobPanel = (explorer: Explorer) => {
|
export const openCreateCopyJobPanel = (explorer: Explorer) => {
|
||||||
const sidePanelState = useSidePanel.getState();
|
const sidePanelState = useSidePanel.getState();
|
||||||
sidePanelState.setPanelHasConsole(false);
|
sidePanelState.setPanelHasConsole(false);
|
||||||
|
sidePanelState.setLightDismiss(false);
|
||||||
sidePanelState.openSidePanel(
|
sidePanelState.openSidePanel(
|
||||||
ContainerCopyMessages.createCopyJobPanelTitle,
|
ContainerCopyMessages.createCopyJobPanelTitle,
|
||||||
<CreateCopyJobScreensProvider explorer={explorer} />,
|
<CreateCopyJobScreensProvider explorer={explorer} />,
|
||||||
@@ -44,6 +45,7 @@ export const openCreateCopyJobPanel = (explorer: Explorer) => {
|
|||||||
export const openCopyJobDetailsPanel = (job: CopyJobType) => {
|
export const openCopyJobDetailsPanel = (job: CopyJobType) => {
|
||||||
const sidePanelState = useSidePanel.getState();
|
const sidePanelState = useSidePanel.getState();
|
||||||
sidePanelState.setPanelHasConsole(false);
|
sidePanelState.setPanelHasConsole(false);
|
||||||
|
sidePanelState.setLightDismiss(true);
|
||||||
sidePanelState.openSidePanel(
|
sidePanelState.openSidePanel(
|
||||||
ContainerCopyMessages.copyJobDetailsPanelTitle(job.Name),
|
ContainerCopyMessages.copyJobDetailsPanelTitle(job.Name),
|
||||||
<CopyJobDetails job={job} />,
|
<CopyJobDetails job={job} />,
|
||||||
|
|||||||
@@ -25,7 +25,18 @@ export default {
|
|||||||
subscriptionDropdownPlaceholder: "Select a subscription",
|
subscriptionDropdownPlaceholder: "Select a subscription",
|
||||||
sourceAccountDropdownLabel: "Account",
|
sourceAccountDropdownLabel: "Account",
|
||||||
sourceAccountDropdownPlaceholder: "Select an account",
|
sourceAccountDropdownPlaceholder: "Select an account",
|
||||||
migrationTypeCheckboxLabel: "Copy container in offline mode",
|
migrationTypeOptions: {
|
||||||
|
offline: {
|
||||||
|
title: "Offline mode",
|
||||||
|
description:
|
||||||
|
"Offline container copy jobs let you copy data from a source container to a destination Cosmos DB container for supported APIs. To ensure data integrity between the source and destination, we recommend stopping updates on the source container before creating the copy job. Learn more about [offline copy jobs](https://learn.microsoft.com/azure/cosmos-db/how-to-container-copy?tabs=offline-copy&pivots=api-nosql).",
|
||||||
|
},
|
||||||
|
online: {
|
||||||
|
title: "Online mode",
|
||||||
|
description:
|
||||||
|
"Online container copy jobs let you copy data from a source container to a destination Cosmos DB NoSQL API container using the [All Versions and Delete](https://learn.microsoft.com/azure/cosmos-db/change-feed-modes?tabs=all-versions-and-deletes#all-versions-and-deletes-change-feed-mode-preview) change feed. This allows updates to continue on the source while data is copied. A brief downtime is required at the end to safely switch over client applications to the destination container. Learn more about [online copy jobs](https://learn.microsoft.com/azure/cosmos-db/container-copy?tabs=online-copy&pivots=api-nosql#getting-started).",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
// Select Source and Target Containers Screen
|
// Select Source and Target Containers Screen
|
||||||
selectSourceAndTargetContainersDescription:
|
selectSourceAndTargetContainersDescription:
|
||||||
@@ -173,5 +184,10 @@ export default {
|
|||||||
Skipped: "Cancelled",
|
Skipped: "Cancelled",
|
||||||
Cancelled: "Cancelled",
|
Cancelled: "Cancelled",
|
||||||
},
|
},
|
||||||
|
dialog: {
|
||||||
|
heading: "",
|
||||||
|
confirmButtonText: "Confirm",
|
||||||
|
cancelButtonText: "Cancel",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ const AddManagedIdentity: React.FC<AddManagedIdentityProps> = () => {
|
|||||||
<InfoTooltip content={managedIdentityTooltip} />
|
<InfoTooltip content={managedIdentityTooltip} />
|
||||||
</Text>
|
</Text>
|
||||||
<Toggle
|
<Toggle
|
||||||
|
data-test="btn-toggle"
|
||||||
checked={systemAssigned}
|
checked={systemAssigned}
|
||||||
onText={ContainerCopyMessages.toggleBtn.onText}
|
onText={ContainerCopyMessages.toggleBtn.onText}
|
||||||
offText={ContainerCopyMessages.toggleBtn.offText}
|
offText={ContainerCopyMessages.toggleBtn.offText}
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ const AddReadPermissionToDefaultIdentity: React.FC<AddReadPermissionToDefaultIde
|
|||||||
<InfoTooltip content={TooltipContent} />
|
<InfoTooltip content={TooltipContent} />
|
||||||
</Text>
|
</Text>
|
||||||
<Toggle
|
<Toggle
|
||||||
|
data-test="btn-toggle"
|
||||||
checked={readPermissionAssigned}
|
checked={readPermissionAssigned}
|
||||||
onText={ContainerCopyMessages.toggleBtn.onText}
|
onText={ContainerCopyMessages.toggleBtn.onText}
|
||||||
offText={ContainerCopyMessages.toggleBtn.offText}
|
offText={ContainerCopyMessages.toggleBtn.offText}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { useCopyJobPrerequisitesCache } from "../../Utils/useCopyJobPrerequisite
|
|||||||
import usePermissionSections, { PermissionGroupConfig, PermissionSectionConfig } from "./hooks/usePermissionsSection";
|
import usePermissionSections, { PermissionGroupConfig, PermissionSectionConfig } from "./hooks/usePermissionsSection";
|
||||||
|
|
||||||
const PermissionSection: React.FC<PermissionSectionConfig> = ({ id, title, Component, completed, disabled }) => (
|
const PermissionSection: React.FC<PermissionSectionConfig> = ({ id, title, Component, completed, disabled }) => (
|
||||||
<AccordionItem key={id} value={id} disabled={disabled}>
|
<AccordionItem key={id} value={id} disabled={disabled} data-test="accordion-item">
|
||||||
<AccordionHeader className="accordionHeader">
|
<AccordionHeader className="accordionHeader">
|
||||||
<Text className="accordionHeaderText" variant="medium">
|
<Text className="accordionHeaderText" variant="medium">
|
||||||
{title}
|
{title}
|
||||||
@@ -25,13 +25,13 @@ const PermissionSection: React.FC<PermissionSectionConfig> = ({ id, title, Compo
|
|||||||
height={completed ? 20 : 24}
|
height={completed ? 20 : 24}
|
||||||
/>
|
/>
|
||||||
</AccordionHeader>
|
</AccordionHeader>
|
||||||
<AccordionPanel aria-disabled={disabled} className="accordionPanel">
|
<AccordionPanel aria-disabled={disabled} className="accordionPanel" data-test="accordion-panel">
|
||||||
<Component />
|
<Component />
|
||||||
</AccordionPanel>
|
</AccordionPanel>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
);
|
);
|
||||||
|
|
||||||
const PermissionGroup: React.FC<PermissionGroupConfig> = ({ title, description, sections }) => {
|
const PermissionGroup: React.FC<PermissionGroupConfig> = ({ id, title, description, sections }) => {
|
||||||
const [openItems, setOpenItems] = React.useState<string[]>([]);
|
const [openItems, setOpenItems] = React.useState<string[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -44,6 +44,7 @@ const PermissionGroup: React.FC<PermissionGroupConfig> = ({ title, description,
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
|
data-test={`permission-group-container-${id}`}
|
||||||
tokens={{ childrenGap: 15 }}
|
tokens={{ childrenGap: 15 }}
|
||||||
styles={{
|
styles={{
|
||||||
root: {
|
root: {
|
||||||
@@ -99,7 +100,11 @@ const AssignPermissions = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack className="assignPermissionsContainer" tokens={{ childrenGap: 20 }}>
|
<Stack
|
||||||
|
data-test="Panel:AssignPermissionsContainer"
|
||||||
|
className="assignPermissionsContainer"
|
||||||
|
tokens={{ childrenGap: 20 }}
|
||||||
|
>
|
||||||
<Text variant="medium">
|
<Text variant="medium">
|
||||||
{isSameAccount && copyJobState.migrationType === CopyJobMigrationType.Online
|
{isSameAccount && copyJobState.migrationType === CopyJobMigrationType.Online
|
||||||
? ContainerCopyMessages.assignPermissions.intraAccountOnlineDescription(
|
? ContainerCopyMessages.assignPermissions.intraAccountOnlineDescription(
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ const DefaultManagedIdentity: React.FC<AddManagedIdentityProps> = () => {
|
|||||||
<InfoTooltip content={managedIdentityTooltip} />
|
<InfoTooltip content={managedIdentityTooltip} />
|
||||||
</div>
|
</div>
|
||||||
<Toggle
|
<Toggle
|
||||||
|
data-test="btn-toggle"
|
||||||
checked={defaultSystemAssigned}
|
checked={defaultSystemAssigned}
|
||||||
onText={ContainerCopyMessages.toggleBtn.onText}
|
onText={ContainerCopyMessages.toggleBtn.onText}
|
||||||
offText={ContainerCopyMessages.toggleBtn.offText}
|
offText={ContainerCopyMessages.toggleBtn.offText}
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ const PointInTimeRestore: React.FC = () => {
|
|||||||
<Stack.Item>
|
<Stack.Item>
|
||||||
{showRefreshButton ? (
|
{showRefreshButton ? (
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
|
data-test="pointInTimeRestore:RefreshBtn"
|
||||||
className="fullWidth"
|
className="fullWidth"
|
||||||
text={ContainerCopyMessages.refreshButtonLabel}
|
text={ContainerCopyMessages.refreshButtonLabel}
|
||||||
iconProps={{ iconName: "Refresh" }}
|
iconProps={{ iconName: "Refresh" }}
|
||||||
@@ -134,6 +135,7 @@ const PointInTimeRestore: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
|
data-test="pointInTimeRestore:PrimaryBtn"
|
||||||
className="fullWidth"
|
className="fullWidth"
|
||||||
text={loading ? "" : ContainerCopyMessages.pointInTimeRestore.buttonText}
|
text={loading ? "" : ContainerCopyMessages.pointInTimeRestore.buttonText}
|
||||||
{...(loading ? { iconProps: { iconName: "SyncStatusSolid" } } : {})}
|
{...(loading ? { iconProps: { iconName: "SyncStatusSolid" } } : {})}
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ exports[`AddManagedIdentity Snapshot Tests renders initial state correctly 1`] =
|
|||||||
class="ms-Toggle-background pill-117"
|
class="ms-Toggle-background pill-117"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
|
data-test="btn-toggle"
|
||||||
id="Toggle1"
|
id="Toggle1"
|
||||||
role="switch"
|
role="switch"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -154,6 +155,7 @@ exports[`AddManagedIdentity Snapshot Tests renders loading state 1`] = `
|
|||||||
class="ms-Toggle-background pill-121"
|
class="ms-Toggle-background pill-121"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
|
data-test="btn-toggle"
|
||||||
id="Toggle11"
|
id="Toggle11"
|
||||||
role="switch"
|
role="switch"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -173,10 +175,12 @@ exports[`AddManagedIdentity Snapshot Tests renders loading state 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack popover-container foreground loading css-123"
|
class="ms-Stack popover-container foreground loading css-123"
|
||||||
|
data-test="popover-container"
|
||||||
style="max-width: 450px;"
|
style="max-width: 450px;"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Overlay root-135"
|
class="ms-Overlay root-135"
|
||||||
|
data-test="loading-overlay"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Spinner root-137"
|
class="ms-Spinner root-137"
|
||||||
@@ -323,6 +327,7 @@ exports[`AddManagedIdentity Snapshot Tests renders with toggle on and popover vi
|
|||||||
class="ms-Toggle-background pill-121"
|
class="ms-Toggle-background pill-121"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
|
data-test="btn-toggle"
|
||||||
id="Toggle3"
|
id="Toggle3"
|
||||||
role="switch"
|
role="switch"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -342,6 +347,7 @@ exports[`AddManagedIdentity Snapshot Tests renders with toggle on and popover vi
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack popover-container foreground css-123"
|
class="ms-Stack popover-container foreground css-123"
|
||||||
|
data-test="popover-container"
|
||||||
style="max-width: 450px;"
|
style="max-width: 450px;"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle m
|
|||||||
class="ms-Toggle-background pill-115"
|
class="ms-Toggle-background pill-115"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
|
data-test="btn-toggle"
|
||||||
id="Toggle17"
|
id="Toggle17"
|
||||||
role="switch"
|
role="switch"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -103,6 +104,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle m
|
|||||||
class="ms-Toggle-background pill-115"
|
class="ms-Toggle-background pill-115"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
|
data-test="btn-toggle"
|
||||||
id="Toggle16"
|
id="Toggle16"
|
||||||
role="switch"
|
role="switch"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -165,6 +167,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
|
|||||||
class="ms-Toggle-background pill-115"
|
class="ms-Toggle-background pill-115"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
|
data-test="btn-toggle"
|
||||||
id="Toggle3"
|
id="Toggle3"
|
||||||
role="switch"
|
role="switch"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -227,6 +230,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
|
|||||||
class="ms-Toggle-background pill-119"
|
class="ms-Toggle-background pill-119"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
|
data-test="btn-toggle"
|
||||||
id="Toggle1"
|
id="Toggle1"
|
||||||
role="switch"
|
role="switch"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -314,6 +318,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
|
|||||||
class="ms-Toggle-background pill-115"
|
class="ms-Toggle-background pill-115"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
|
data-test="btn-toggle"
|
||||||
id="Toggle0"
|
id="Toggle0"
|
||||||
role="switch"
|
role="switch"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -376,6 +381,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
|
|||||||
class="ms-Toggle-background pill-115"
|
class="ms-Toggle-background pill-115"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
|
data-test="btn-toggle"
|
||||||
id="Toggle2"
|
id="Toggle2"
|
||||||
role="switch"
|
role="switch"
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ exports[`AssignPermissions Component Accordion Behavior should render accordion
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack assignPermissionsContainer css-109"
|
class="ms-Stack assignPermissionsContainer css-109"
|
||||||
|
data-test="Panel:AssignPermissionsContainer"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-110"
|
class="css-110"
|
||||||
@@ -15,6 +16,7 @@ exports[`AssignPermissions Component Accordion Behavior should render accordion
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack css-112"
|
class="ms-Stack css-112"
|
||||||
|
data-test="permission-group-container-testGroup"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack css-113"
|
class="ms-Stack css-113"
|
||||||
@@ -36,6 +38,7 @@ exports[`AssignPermissions Component Accordion Behavior should render accordion
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionItem"
|
class="fui-AccordionItem"
|
||||||
|
data-test="accordion-item"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
||||||
@@ -85,6 +88,7 @@ exports[`AssignPermissions Component Accordion Behavior should render accordion
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionItem"
|
class="fui-AccordionItem"
|
||||||
|
data-test="accordion-item"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
||||||
@@ -134,6 +138,7 @@ exports[`AssignPermissions Component Accordion Behavior should render accordion
|
|||||||
<div
|
<div
|
||||||
aria-disabled="false"
|
aria-disabled="false"
|
||||||
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
|
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
|
||||||
|
data-test="accordion-panel"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
Incomplete Component
|
Incomplete Component
|
||||||
@@ -142,6 +147,7 @@ exports[`AssignPermissions Component Accordion Behavior should render accordion
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionItem"
|
class="fui-AccordionItem"
|
||||||
|
data-test="accordion-item"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionHeader accordionHeader ___lyghz50_53x5ri0 f1s2aq7o f1c21dwh f1s184ao ft85np5 fwrgwhw"
|
class="fui-AccordionHeader accordionHeader ___lyghz50_53x5ri0 f1s2aq7o f1c21dwh f1s184ao ft85np5 fwrgwhw"
|
||||||
@@ -201,6 +207,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack assignPermissionsContainer css-109"
|
class="ms-Stack assignPermissionsContainer css-109"
|
||||||
|
data-test="Panel:AssignPermissionsContainer"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-110"
|
class="css-110"
|
||||||
@@ -212,6 +219,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack css-112"
|
class="ms-Stack css-112"
|
||||||
|
data-test="permission-group-container-testGroup"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack css-113"
|
class="ms-Stack css-113"
|
||||||
@@ -233,6 +241,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionItem"
|
class="fui-AccordionItem"
|
||||||
|
data-test="accordion-item"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
||||||
@@ -282,6 +291,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionItem"
|
class="fui-AccordionItem"
|
||||||
|
data-test="accordion-item"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
||||||
@@ -331,6 +341,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
|
|||||||
<div
|
<div
|
||||||
aria-disabled="false"
|
aria-disabled="false"
|
||||||
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
|
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
|
||||||
|
data-test="accordion-panel"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
Incomplete Component
|
Incomplete Component
|
||||||
@@ -339,6 +350,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionItem"
|
class="fui-AccordionItem"
|
||||||
|
data-test="accordion-item"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionHeader accordionHeader ___lyghz50_53x5ri0 f1s2aq7o f1c21dwh f1s184ao ft85np5 fwrgwhw"
|
class="fui-AccordionHeader accordionHeader ___lyghz50_53x5ri0 f1s2aq7o f1c21dwh f1s184ao ft85np5 fwrgwhw"
|
||||||
@@ -398,6 +410,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack assignPermissionsContainer css-109"
|
class="ms-Stack assignPermissionsContainer css-109"
|
||||||
|
data-test="Panel:AssignPermissionsContainer"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-110"
|
class="css-110"
|
||||||
@@ -409,6 +422,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack css-112"
|
class="ms-Stack css-112"
|
||||||
|
data-test="permission-group-container-testGroup"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack css-113"
|
class="ms-Stack css-113"
|
||||||
@@ -430,6 +444,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionItem"
|
class="fui-AccordionItem"
|
||||||
|
data-test="accordion-item"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
||||||
@@ -479,6 +494,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionItem"
|
class="fui-AccordionItem"
|
||||||
|
data-test="accordion-item"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
||||||
@@ -528,6 +544,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
|
|||||||
<div
|
<div
|
||||||
aria-disabled="false"
|
aria-disabled="false"
|
||||||
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
|
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
|
||||||
|
data-test="accordion-panel"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
Incomplete Component
|
Incomplete Component
|
||||||
@@ -536,6 +553,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionItem"
|
class="fui-AccordionItem"
|
||||||
|
data-test="accordion-item"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionHeader accordionHeader ___lyghz50_53x5ri0 f1s2aq7o f1c21dwh f1s184ao ft85np5 fwrgwhw"
|
class="fui-AccordionHeader accordionHeader ___lyghz50_53x5ri0 f1s2aq7o f1c21dwh f1s184ao ft85np5 fwrgwhw"
|
||||||
@@ -595,6 +613,7 @@ exports[`AssignPermissions Component Edge Cases should handle missing account na
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack assignPermissionsContainer css-109"
|
class="ms-Stack assignPermissionsContainer css-109"
|
||||||
|
data-test="Panel:AssignPermissionsContainer"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-110"
|
class="css-110"
|
||||||
@@ -606,6 +625,7 @@ exports[`AssignPermissions Component Edge Cases should handle missing account na
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack css-112"
|
class="ms-Stack css-112"
|
||||||
|
data-test="permission-group-container-testGroup"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack css-113"
|
class="ms-Stack css-113"
|
||||||
@@ -627,6 +647,7 @@ exports[`AssignPermissions Component Edge Cases should handle missing account na
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionItem"
|
class="fui-AccordionItem"
|
||||||
|
data-test="accordion-item"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
||||||
@@ -676,6 +697,7 @@ exports[`AssignPermissions Component Edge Cases should handle missing account na
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionItem"
|
class="fui-AccordionItem"
|
||||||
|
data-test="accordion-item"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
||||||
@@ -725,6 +747,7 @@ exports[`AssignPermissions Component Edge Cases should handle missing account na
|
|||||||
<div
|
<div
|
||||||
aria-disabled="false"
|
aria-disabled="false"
|
||||||
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
|
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
|
||||||
|
data-test="accordion-panel"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
Incomplete Component
|
Incomplete Component
|
||||||
@@ -733,6 +756,7 @@ exports[`AssignPermissions Component Edge Cases should handle missing account na
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionItem"
|
class="fui-AccordionItem"
|
||||||
|
data-test="accordion-item"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionHeader accordionHeader ___lyghz50_53x5ri0 f1s2aq7o f1c21dwh f1s184ao ft85np5 fwrgwhw"
|
class="fui-AccordionHeader accordionHeader ___lyghz50_53x5ri0 f1s2aq7o f1c21dwh f1s184ao ft85np5 fwrgwhw"
|
||||||
@@ -792,6 +816,7 @@ exports[`AssignPermissions Component Permission Groups should render multiple pe
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack assignPermissionsContainer css-109"
|
class="ms-Stack assignPermissionsContainer css-109"
|
||||||
|
data-test="Panel:AssignPermissionsContainer"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-110"
|
class="css-110"
|
||||||
@@ -803,6 +828,7 @@ exports[`AssignPermissions Component Permission Groups should render multiple pe
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack css-112"
|
class="ms-Stack css-112"
|
||||||
|
data-test="permission-group-container-crossAccountConfigs"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack css-113"
|
class="ms-Stack css-113"
|
||||||
@@ -824,6 +850,7 @@ exports[`AssignPermissions Component Permission Groups should render multiple pe
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionItem"
|
class="fui-AccordionItem"
|
||||||
|
data-test="accordion-item"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
||||||
@@ -875,6 +902,7 @@ exports[`AssignPermissions Component Permission Groups should render multiple pe
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack css-112"
|
class="ms-Stack css-112"
|
||||||
|
data-test="permission-group-container-onlineConfigs"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack css-113"
|
class="ms-Stack css-113"
|
||||||
@@ -896,6 +924,7 @@ exports[`AssignPermissions Component Permission Groups should render multiple pe
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionItem"
|
class="fui-AccordionItem"
|
||||||
|
data-test="accordion-item"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
||||||
@@ -945,6 +974,7 @@ exports[`AssignPermissions Component Permission Groups should render multiple pe
|
|||||||
<div
|
<div
|
||||||
aria-disabled="false"
|
aria-disabled="false"
|
||||||
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
|
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
|
||||||
|
data-test="accordion-panel"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
data-testid="online-copy-enabled"
|
data-testid="online-copy-enabled"
|
||||||
@@ -964,6 +994,7 @@ exports[`AssignPermissions Component Permission Groups should render online migr
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack assignPermissionsContainer css-109"
|
class="ms-Stack assignPermissionsContainer css-109"
|
||||||
|
data-test="Panel:AssignPermissionsContainer"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-110"
|
class="css-110"
|
||||||
@@ -975,6 +1006,7 @@ exports[`AssignPermissions Component Permission Groups should render online migr
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack css-112"
|
class="ms-Stack css-112"
|
||||||
|
data-test="permission-group-container-onlineConfigs"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack css-113"
|
class="ms-Stack css-113"
|
||||||
@@ -996,6 +1028,7 @@ exports[`AssignPermissions Component Permission Groups should render online migr
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionItem"
|
class="fui-AccordionItem"
|
||||||
|
data-test="accordion-item"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
||||||
@@ -1045,6 +1078,7 @@ exports[`AssignPermissions Component Permission Groups should render online migr
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionItem"
|
class="fui-AccordionItem"
|
||||||
|
data-test="accordion-item"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
||||||
@@ -1094,6 +1128,7 @@ exports[`AssignPermissions Component Permission Groups should render online migr
|
|||||||
<div
|
<div
|
||||||
aria-disabled="false"
|
aria-disabled="false"
|
||||||
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
|
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
|
||||||
|
data-test="accordion-panel"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
data-testid="online-copy-enabled"
|
data-testid="online-copy-enabled"
|
||||||
@@ -1113,6 +1148,7 @@ exports[`AssignPermissions Component Permission Groups should render permission
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack assignPermissionsContainer css-109"
|
class="ms-Stack assignPermissionsContainer css-109"
|
||||||
|
data-test="Panel:AssignPermissionsContainer"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-110"
|
class="css-110"
|
||||||
@@ -1124,6 +1160,7 @@ exports[`AssignPermissions Component Permission Groups should render permission
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack css-112"
|
class="ms-Stack css-112"
|
||||||
|
data-test="permission-group-container-crossAccountConfigs"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack css-113"
|
class="ms-Stack css-113"
|
||||||
@@ -1145,6 +1182,7 @@ exports[`AssignPermissions Component Permission Groups should render permission
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionItem"
|
class="fui-AccordionItem"
|
||||||
|
data-test="accordion-item"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
||||||
@@ -1194,6 +1232,7 @@ exports[`AssignPermissions Component Permission Groups should render permission
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionItem"
|
class="fui-AccordionItem"
|
||||||
|
data-test="accordion-item"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
||||||
@@ -1243,6 +1282,7 @@ exports[`AssignPermissions Component Permission Groups should render permission
|
|||||||
<div
|
<div
|
||||||
aria-disabled="false"
|
aria-disabled="false"
|
||||||
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
|
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
|
||||||
|
data-test="accordion-panel"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
data-testid="add-read-permission"
|
data-testid="add-read-permission"
|
||||||
@@ -1262,6 +1302,7 @@ exports[`AssignPermissions Component Rendering should render without crashing wi
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack assignPermissionsContainer css-109"
|
class="ms-Stack assignPermissionsContainer css-109"
|
||||||
|
data-test="Panel:AssignPermissionsContainer"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-110"
|
class="css-110"
|
||||||
@@ -1283,6 +1324,7 @@ exports[`AssignPermissions Component Rendering should render without crashing wi
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack assignPermissionsContainer css-109"
|
class="ms-Stack assignPermissionsContainer css-109"
|
||||||
|
data-test="Panel:AssignPermissionsContainer"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-110"
|
class="css-110"
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ exports[`DefaultManagedIdentity Edge Cases should handle missing account name gr
|
|||||||
class="ms-Toggle-background pill-115"
|
class="ms-Toggle-background pill-115"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
|
data-test="btn-toggle"
|
||||||
id="Toggle14"
|
id="Toggle14"
|
||||||
role="switch"
|
role="switch"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -103,6 +104,7 @@ exports[`DefaultManagedIdentity Edge Cases should handle null account 1`] = `
|
|||||||
class="ms-Toggle-background pill-115"
|
class="ms-Toggle-background pill-115"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
|
data-test="btn-toggle"
|
||||||
id="Toggle15"
|
id="Toggle15"
|
||||||
role="switch"
|
role="switch"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -165,6 +167,7 @@ exports[`DefaultManagedIdentity Loading States should render loading state snaps
|
|||||||
class="ms-Toggle-background pill-119"
|
class="ms-Toggle-background pill-119"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
|
data-test="btn-toggle"
|
||||||
id="Toggle10"
|
id="Toggle10"
|
||||||
role="switch"
|
role="switch"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -256,6 +259,7 @@ exports[`DefaultManagedIdentity Rendering should render correctly with default s
|
|||||||
class="ms-Toggle-background pill-115"
|
class="ms-Toggle-background pill-115"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
|
data-test="btn-toggle"
|
||||||
id="Toggle0"
|
id="Toggle0"
|
||||||
role="switch"
|
role="switch"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -318,6 +322,7 @@ exports[`DefaultManagedIdentity Toggle Interactions should render toggle with ch
|
|||||||
class="ms-Toggle-background pill-119"
|
class="ms-Toggle-background pill-119"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
|
data-test="btn-toggle"
|
||||||
id="Toggle7"
|
id="Toggle7"
|
||||||
role="switch"
|
role="switch"
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ exports[`PointInTimeRestore Initial Render should render correctly with default
|
|||||||
<button
|
<button
|
||||||
class="ms-Button ms-Button--primary fullWidth root-115"
|
class="ms-Button ms-Button--primary fullWidth root-115"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
|
data-test="pointInTimeRestore:PrimaryBtn"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -131,6 +132,7 @@ exports[`PointInTimeRestore Initial Render should render with empty account name
|
|||||||
<button
|
<button
|
||||||
class="ms-Button ms-Button--primary fullWidth root-115"
|
class="ms-Button ms-Button--primary fullWidth root-115"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
|
data-test="pointInTimeRestore:PrimaryBtn"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -161,6 +163,7 @@ exports[`PointInTimeRestore Snapshots should match snapshot in loading state 1`]
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Overlay root-123"
|
class="ms-Overlay root-123"
|
||||||
|
data-test="loading-overlay"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Spinner root-125"
|
class="ms-Spinner root-125"
|
||||||
@@ -223,6 +226,7 @@ exports[`PointInTimeRestore Snapshots should match snapshot in loading state 1`]
|
|||||||
aria-disabled="true"
|
aria-disabled="true"
|
||||||
class="ms-Button ms-Button--primary is-disabled fullWidth root-128"
|
class="ms-Button ms-Button--primary is-disabled fullWidth root-128"
|
||||||
data-is-focusable="false"
|
data-is-focusable="false"
|
||||||
|
data-test="pointInTimeRestore:PrimaryBtn"
|
||||||
disabled=""
|
disabled=""
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
@@ -301,6 +305,7 @@ exports[`PointInTimeRestore Snapshots should match snapshot with refresh button
|
|||||||
<button
|
<button
|
||||||
class="ms-Button ms-Button--primary fullWidth root-115"
|
class="ms-Button ms-Button--primary fullWidth root-115"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
|
data-test="pointInTimeRestore:RefreshBtn"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
|
|||||||
@@ -19,9 +19,21 @@ const NavigationControls: React.FC<NavigationControlsProps> = ({
|
|||||||
isPreviousDisabled,
|
isPreviousDisabled,
|
||||||
}) => (
|
}) => (
|
||||||
<Stack horizontal tokens={{ childrenGap: 20 }}>
|
<Stack horizontal tokens={{ childrenGap: 20 }}>
|
||||||
<PrimaryButton text={primaryBtnText} onClick={onPrimary} allowDisabledFocus disabled={isPrimaryDisabled} />
|
<PrimaryButton
|
||||||
<DefaultButton text="Previous" onClick={onPrevious} allowDisabledFocus disabled={isPreviousDisabled} />
|
data-test="copy-job-primary"
|
||||||
<DefaultButton text="Cancel" onClick={onCancel} />
|
text={primaryBtnText}
|
||||||
|
onClick={onPrimary}
|
||||||
|
allowDisabledFocus
|
||||||
|
disabled={isPrimaryDisabled}
|
||||||
|
/>
|
||||||
|
<DefaultButton
|
||||||
|
data-test="copy-job-previous"
|
||||||
|
text="Previous"
|
||||||
|
onClick={onPrevious}
|
||||||
|
allowDisabledFocus
|
||||||
|
disabled={isPreviousDisabled}
|
||||||
|
/>
|
||||||
|
<DefaultButton data-test="copy-job-cancel" text="Cancel" onClick={onCancel} />
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const PopoverContainer: React.FC<PopoverContainerProps> = React.memo(
|
|||||||
({ isLoading = false, title, children, onPrimary, onCancel }) => {
|
({ isLoading = false, title, children, onPrimary, onCancel }) => {
|
||||||
return (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
|
data-test="popover-container"
|
||||||
className={`popover-container foreground ${isLoading ? "loading" : ""}`}
|
className={`popover-container foreground ${isLoading ? "loading" : ""}`}
|
||||||
tokens={{ childrenGap: 20 }}
|
tokens={{ childrenGap: 20 }}
|
||||||
style={{ maxWidth: 450 }}
|
style={{ maxWidth: 450 }}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ exports[`PopoverMessage Component Edge Cases should handle empty string title 1`
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack popover-container foreground css-109"
|
class="ms-Stack popover-container foreground css-109"
|
||||||
|
data-test="popover-container"
|
||||||
style="max-width: 450px;"
|
style="max-width: 450px;"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -71,6 +72,7 @@ exports[`PopoverMessage Component Edge Cases should handle null children 1`] = `
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack popover-container foreground css-109"
|
class="ms-Stack popover-container foreground css-109"
|
||||||
|
data-test="popover-container"
|
||||||
style="max-width: 450px;"
|
style="max-width: 450px;"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -133,6 +135,7 @@ exports[`PopoverMessage Component Edge Cases should handle undefined children 1`
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack popover-container foreground css-109"
|
class="ms-Stack popover-container foreground css-109"
|
||||||
|
data-test="popover-container"
|
||||||
style="max-width: 450px;"
|
style="max-width: 450px;"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -195,6 +198,7 @@ exports[`PopoverMessage Component Edge Cases should handle very long title 1`] =
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack popover-container foreground css-109"
|
class="ms-Stack popover-container foreground css-109"
|
||||||
|
data-test="popover-container"
|
||||||
style="max-width: 450px;"
|
style="max-width: 450px;"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -266,6 +270,7 @@ exports[`PopoverMessage Component Rendering should render correctly when visible
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack popover-container foreground css-109"
|
class="ms-Stack popover-container foreground css-109"
|
||||||
|
data-test="popover-container"
|
||||||
style="max-width: 450px;"
|
style="max-width: 450px;"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -335,6 +340,7 @@ exports[`PopoverMessage Component Rendering should render correctly with differe
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack popover-container foreground css-109"
|
class="ms-Stack popover-container foreground css-109"
|
||||||
|
data-test="popover-container"
|
||||||
style="max-width: 450px;"
|
style="max-width: 450px;"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -409,6 +415,7 @@ exports[`PopoverMessage Component Rendering should render correctly with differe
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack popover-container foreground css-109"
|
class="ms-Stack popover-container foreground css-109"
|
||||||
|
data-test="popover-container"
|
||||||
style="max-width: 450px;"
|
style="max-width: 450px;"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -478,6 +485,7 @@ exports[`PopoverMessage Component Rendering should render correctly with loading
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack popover-container foreground loading css-109"
|
class="ms-Stack popover-container foreground loading css-109"
|
||||||
|
data-test="popover-container"
|
||||||
style="max-width: 450px;"
|
style="max-width: 450px;"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ const CreateCopyJobScreens: React.FC = () => {
|
|||||||
<Stack.Item className="createCopyJobScreensContent">
|
<Stack.Item className="createCopyJobScreensContent">
|
||||||
{contextError && (
|
{contextError && (
|
||||||
<MessageBar
|
<MessageBar
|
||||||
|
data-test="Panel:ErrorContainer"
|
||||||
className="createCopyJobErrorMessageBar"
|
className="createCopyJobErrorMessageBar"
|
||||||
messageBarType={MessageBarType.blocked}
|
messageBarType={MessageBarType.blocked}
|
||||||
isMultiline={false}
|
isMultiline={false}
|
||||||
|
|||||||
@@ -31,17 +31,17 @@ const PreviewCopyJob: React.FC = () => {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Stack tokens={{ childrenGap: 20 }} className="previewCopyJobContainer">
|
<Stack tokens={{ childrenGap: 20 }} className="previewCopyJobContainer" data-test="Panel:PreviewCopyJob">
|
||||||
<FieldRow label={ContainerCopyMessages.jobNameLabel}>
|
<FieldRow label={ContainerCopyMessages.jobNameLabel}>
|
||||||
<TextField value={jobName} onChange={onJobNameChange} />
|
<TextField data-test="job-name-textfield" value={jobName} onChange={onJobNameChange} />
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Text className="bold">{ContainerCopyMessages.sourceSubscriptionLabel}</Text>
|
<Text className="bold">{ContainerCopyMessages.sourceSubscriptionLabel}</Text>
|
||||||
<Text>{copyJobState.source?.subscription?.displayName}</Text>
|
<Text data-test="source-subscription-name">{copyJobState.source?.subscription?.displayName}</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Text className="bold">{ContainerCopyMessages.sourceAccountLabel}</Text>
|
<Text className="bold">{ContainerCopyMessages.sourceAccountLabel}</Text>
|
||||||
<Text>{copyJobState.source?.account?.name}</Text>
|
<Text data-test="source-account-name">{copyJobState.source?.account?.name}</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack>
|
<Stack>
|
||||||
<DetailsList
|
<DetailsList
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
exports[`PreviewCopyJob should handle special characters in database and container names 1`] = `
|
exports[`PreviewCopyJob should handle special characters in database and container names 1`] = `
|
||||||
<div
|
<div
|
||||||
class="ms-Stack previewCopyJobContainer css-109"
|
class="ms-Stack previewCopyJobContainer css-109"
|
||||||
|
data-test="Panel:PreviewCopyJob"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack flex-row css-110"
|
class="ms-Stack flex-row css-110"
|
||||||
@@ -32,6 +33,7 @@ exports[`PreviewCopyJob should handle special characters in database and contain
|
|||||||
<input
|
<input
|
||||||
aria-invalid="false"
|
aria-invalid="false"
|
||||||
class="ms-TextField-field field-115"
|
class="ms-TextField-field field-115"
|
||||||
|
data-test="job-name-textfield"
|
||||||
id="TextField84"
|
id="TextField84"
|
||||||
type="text"
|
type="text"
|
||||||
value="job-with@special#chars_123"
|
value="job-with@special#chars_123"
|
||||||
@@ -51,6 +53,7 @@ exports[`PreviewCopyJob should handle special characters in database and contain
|
|||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="css-125"
|
||||||
|
data-test="source-subscription-name"
|
||||||
>
|
>
|
||||||
Test Subscription
|
Test Subscription
|
||||||
</span>
|
</span>
|
||||||
@@ -65,6 +68,7 @@ exports[`PreviewCopyJob should handle special characters in database and contain
|
|||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="css-125"
|
||||||
|
data-test="source-account-name"
|
||||||
>
|
>
|
||||||
test-account
|
test-account
|
||||||
</span>
|
</span>
|
||||||
@@ -321,6 +325,7 @@ exports[`PreviewCopyJob should handle special characters in database and contain
|
|||||||
exports[`PreviewCopyJob should render component with cross-subscription setup 1`] = `
|
exports[`PreviewCopyJob should render component with cross-subscription setup 1`] = `
|
||||||
<div
|
<div
|
||||||
class="ms-Stack previewCopyJobContainer css-109"
|
class="ms-Stack previewCopyJobContainer css-109"
|
||||||
|
data-test="Panel:PreviewCopyJob"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack flex-row css-110"
|
class="ms-Stack flex-row css-110"
|
||||||
@@ -350,6 +355,7 @@ exports[`PreviewCopyJob should render component with cross-subscription setup 1`
|
|||||||
<input
|
<input
|
||||||
aria-invalid="false"
|
aria-invalid="false"
|
||||||
class="ms-TextField-field field-115"
|
class="ms-TextField-field field-115"
|
||||||
|
data-test="job-name-textfield"
|
||||||
id="TextField96"
|
id="TextField96"
|
||||||
type="text"
|
type="text"
|
||||||
value=""
|
value=""
|
||||||
@@ -369,6 +375,7 @@ exports[`PreviewCopyJob should render component with cross-subscription setup 1`
|
|||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="css-125"
|
||||||
|
data-test="source-subscription-name"
|
||||||
>
|
>
|
||||||
Test Subscription
|
Test Subscription
|
||||||
</span>
|
</span>
|
||||||
@@ -383,6 +390,7 @@ exports[`PreviewCopyJob should render component with cross-subscription setup 1`
|
|||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="css-125"
|
||||||
|
data-test="source-account-name"
|
||||||
>
|
>
|
||||||
test-account
|
test-account
|
||||||
</span>
|
</span>
|
||||||
@@ -639,6 +647,7 @@ exports[`PreviewCopyJob should render component with cross-subscription setup 1`
|
|||||||
exports[`PreviewCopyJob should render with default state and empty job name 1`] = `
|
exports[`PreviewCopyJob should render with default state and empty job name 1`] = `
|
||||||
<div
|
<div
|
||||||
class="ms-Stack previewCopyJobContainer css-109"
|
class="ms-Stack previewCopyJobContainer css-109"
|
||||||
|
data-test="Panel:PreviewCopyJob"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack flex-row css-110"
|
class="ms-Stack flex-row css-110"
|
||||||
@@ -668,6 +677,7 @@ exports[`PreviewCopyJob should render with default state and empty job name 1`]
|
|||||||
<input
|
<input
|
||||||
aria-invalid="false"
|
aria-invalid="false"
|
||||||
class="ms-TextField-field field-115"
|
class="ms-TextField-field field-115"
|
||||||
|
data-test="job-name-textfield"
|
||||||
id="TextField0"
|
id="TextField0"
|
||||||
type="text"
|
type="text"
|
||||||
value=""
|
value=""
|
||||||
@@ -687,6 +697,7 @@ exports[`PreviewCopyJob should render with default state and empty job name 1`]
|
|||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="css-125"
|
||||||
|
data-test="source-subscription-name"
|
||||||
>
|
>
|
||||||
Test Subscription
|
Test Subscription
|
||||||
</span>
|
</span>
|
||||||
@@ -701,6 +712,7 @@ exports[`PreviewCopyJob should render with default state and empty job name 1`]
|
|||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="css-125"
|
||||||
|
data-test="source-account-name"
|
||||||
>
|
>
|
||||||
test-account
|
test-account
|
||||||
</span>
|
</span>
|
||||||
@@ -957,6 +969,7 @@ exports[`PreviewCopyJob should render with default state and empty job name 1`]
|
|||||||
exports[`PreviewCopyJob should render with long subscription and account names 1`] = `
|
exports[`PreviewCopyJob should render with long subscription and account names 1`] = `
|
||||||
<div
|
<div
|
||||||
class="ms-Stack previewCopyJobContainer css-109"
|
class="ms-Stack previewCopyJobContainer css-109"
|
||||||
|
data-test="Panel:PreviewCopyJob"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack flex-row css-110"
|
class="ms-Stack flex-row css-110"
|
||||||
@@ -986,6 +999,7 @@ exports[`PreviewCopyJob should render with long subscription and account names 1
|
|||||||
<input
|
<input
|
||||||
aria-invalid="false"
|
aria-invalid="false"
|
||||||
class="ms-TextField-field field-115"
|
class="ms-TextField-field field-115"
|
||||||
|
data-test="job-name-textfield"
|
||||||
id="TextField60"
|
id="TextField60"
|
||||||
type="text"
|
type="text"
|
||||||
value=""
|
value=""
|
||||||
@@ -1005,6 +1019,7 @@ exports[`PreviewCopyJob should render with long subscription and account names 1
|
|||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="css-125"
|
||||||
|
data-test="source-subscription-name"
|
||||||
>
|
>
|
||||||
This is a very long subscription name that might cause display issues if not handled properly
|
This is a very long subscription name that might cause display issues if not handled properly
|
||||||
</span>
|
</span>
|
||||||
@@ -1019,6 +1034,7 @@ exports[`PreviewCopyJob should render with long subscription and account names 1
|
|||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="css-125"
|
||||||
|
data-test="source-account-name"
|
||||||
>
|
>
|
||||||
this-is-a-very-long-database-account-name-that-might-cause-display-issues
|
this-is-a-very-long-database-account-name-that-might-cause-display-issues
|
||||||
</span>
|
</span>
|
||||||
@@ -1275,6 +1291,7 @@ exports[`PreviewCopyJob should render with long subscription and account names 1
|
|||||||
exports[`PreviewCopyJob should render with missing source account information 1`] = `
|
exports[`PreviewCopyJob should render with missing source account information 1`] = `
|
||||||
<div
|
<div
|
||||||
class="ms-Stack previewCopyJobContainer css-109"
|
class="ms-Stack previewCopyJobContainer css-109"
|
||||||
|
data-test="Panel:PreviewCopyJob"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack flex-row css-110"
|
class="ms-Stack flex-row css-110"
|
||||||
@@ -1304,6 +1321,7 @@ exports[`PreviewCopyJob should render with missing source account information 1`
|
|||||||
<input
|
<input
|
||||||
aria-invalid="false"
|
aria-invalid="false"
|
||||||
class="ms-TextField-field field-115"
|
class="ms-TextField-field field-115"
|
||||||
|
data-test="job-name-textfield"
|
||||||
id="TextField36"
|
id="TextField36"
|
||||||
type="text"
|
type="text"
|
||||||
value=""
|
value=""
|
||||||
@@ -1323,6 +1341,7 @@ exports[`PreviewCopyJob should render with missing source account information 1`
|
|||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="css-125"
|
||||||
|
data-test="source-subscription-name"
|
||||||
>
|
>
|
||||||
Test Subscription
|
Test Subscription
|
||||||
</span>
|
</span>
|
||||||
@@ -1588,6 +1607,7 @@ exports[`PreviewCopyJob should render with missing source account information 1`
|
|||||||
exports[`PreviewCopyJob should render with missing source subscription information 1`] = `
|
exports[`PreviewCopyJob should render with missing source subscription information 1`] = `
|
||||||
<div
|
<div
|
||||||
class="ms-Stack previewCopyJobContainer css-109"
|
class="ms-Stack previewCopyJobContainer css-109"
|
||||||
|
data-test="Panel:PreviewCopyJob"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack flex-row css-110"
|
class="ms-Stack flex-row css-110"
|
||||||
@@ -1617,6 +1637,7 @@ exports[`PreviewCopyJob should render with missing source subscription informati
|
|||||||
<input
|
<input
|
||||||
aria-invalid="false"
|
aria-invalid="false"
|
||||||
class="ms-TextField-field field-115"
|
class="ms-TextField-field field-115"
|
||||||
|
data-test="job-name-textfield"
|
||||||
id="TextField24"
|
id="TextField24"
|
||||||
type="text"
|
type="text"
|
||||||
value=""
|
value=""
|
||||||
@@ -1645,6 +1666,7 @@ exports[`PreviewCopyJob should render with missing source subscription informati
|
|||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="css-125"
|
||||||
|
data-test="source-account-name"
|
||||||
>
|
>
|
||||||
test-account
|
test-account
|
||||||
</span>
|
</span>
|
||||||
@@ -1901,6 +1923,7 @@ exports[`PreviewCopyJob should render with missing source subscription informati
|
|||||||
exports[`PreviewCopyJob should render with online migration type 1`] = `
|
exports[`PreviewCopyJob should render with online migration type 1`] = `
|
||||||
<div
|
<div
|
||||||
class="ms-Stack previewCopyJobContainer css-109"
|
class="ms-Stack previewCopyJobContainer css-109"
|
||||||
|
data-test="Panel:PreviewCopyJob"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack flex-row css-110"
|
class="ms-Stack flex-row css-110"
|
||||||
@@ -1930,6 +1953,7 @@ exports[`PreviewCopyJob should render with online migration type 1`] = `
|
|||||||
<input
|
<input
|
||||||
aria-invalid="false"
|
aria-invalid="false"
|
||||||
class="ms-TextField-field field-115"
|
class="ms-TextField-field field-115"
|
||||||
|
data-test="job-name-textfield"
|
||||||
id="TextField72"
|
id="TextField72"
|
||||||
type="text"
|
type="text"
|
||||||
value="online-migration-job"
|
value="online-migration-job"
|
||||||
@@ -1949,6 +1973,7 @@ exports[`PreviewCopyJob should render with online migration type 1`] = `
|
|||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="css-125"
|
||||||
|
data-test="source-subscription-name"
|
||||||
>
|
>
|
||||||
Test Subscription
|
Test Subscription
|
||||||
</span>
|
</span>
|
||||||
@@ -1963,6 +1988,7 @@ exports[`PreviewCopyJob should render with online migration type 1`] = `
|
|||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="css-125"
|
||||||
|
data-test="source-account-name"
|
||||||
>
|
>
|
||||||
test-account
|
test-account
|
||||||
</span>
|
</span>
|
||||||
@@ -2219,6 +2245,7 @@ exports[`PreviewCopyJob should render with online migration type 1`] = `
|
|||||||
exports[`PreviewCopyJob should render with pre-filled job name 1`] = `
|
exports[`PreviewCopyJob should render with pre-filled job name 1`] = `
|
||||||
<div
|
<div
|
||||||
class="ms-Stack previewCopyJobContainer css-109"
|
class="ms-Stack previewCopyJobContainer css-109"
|
||||||
|
data-test="Panel:PreviewCopyJob"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack flex-row css-110"
|
class="ms-Stack flex-row css-110"
|
||||||
@@ -2248,6 +2275,7 @@ exports[`PreviewCopyJob should render with pre-filled job name 1`] = `
|
|||||||
<input
|
<input
|
||||||
aria-invalid="false"
|
aria-invalid="false"
|
||||||
class="ms-TextField-field field-115"
|
class="ms-TextField-field field-115"
|
||||||
|
data-test="job-name-textfield"
|
||||||
id="TextField12"
|
id="TextField12"
|
||||||
type="text"
|
type="text"
|
||||||
value="custom-job-name-123"
|
value="custom-job-name-123"
|
||||||
@@ -2267,6 +2295,7 @@ exports[`PreviewCopyJob should render with pre-filled job name 1`] = `
|
|||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="css-125"
|
||||||
|
data-test="source-subscription-name"
|
||||||
>
|
>
|
||||||
Test Subscription
|
Test Subscription
|
||||||
</span>
|
</span>
|
||||||
@@ -2281,6 +2310,7 @@ exports[`PreviewCopyJob should render with pre-filled job name 1`] = `
|
|||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="css-125"
|
||||||
|
data-test="source-account-name"
|
||||||
>
|
>
|
||||||
test-account
|
test-account
|
||||||
</span>
|
</span>
|
||||||
@@ -2537,6 +2567,7 @@ exports[`PreviewCopyJob should render with pre-filled job name 1`] = `
|
|||||||
exports[`PreviewCopyJob should render with undefined database and container names 1`] = `
|
exports[`PreviewCopyJob should render with undefined database and container names 1`] = `
|
||||||
<div
|
<div
|
||||||
class="ms-Stack previewCopyJobContainer css-109"
|
class="ms-Stack previewCopyJobContainer css-109"
|
||||||
|
data-test="Panel:PreviewCopyJob"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack flex-row css-110"
|
class="ms-Stack flex-row css-110"
|
||||||
@@ -2566,6 +2597,7 @@ exports[`PreviewCopyJob should render with undefined database and container name
|
|||||||
<input
|
<input
|
||||||
aria-invalid="false"
|
aria-invalid="false"
|
||||||
class="ms-TextField-field field-115"
|
class="ms-TextField-field field-115"
|
||||||
|
data-test="job-name-textfield"
|
||||||
id="TextField48"
|
id="TextField48"
|
||||||
type="text"
|
type="text"
|
||||||
value=""
|
value=""
|
||||||
@@ -2585,6 +2617,7 @@ exports[`PreviewCopyJob should render with undefined database and container name
|
|||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="css-125"
|
||||||
|
data-test="source-subscription-name"
|
||||||
>
|
>
|
||||||
Test Subscription
|
Test Subscription
|
||||||
</span>
|
</span>
|
||||||
@@ -2599,6 +2632,7 @@ exports[`PreviewCopyJob should render with undefined database and container name
|
|||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="css-125"
|
||||||
|
data-test="source-account-name"
|
||||||
>
|
>
|
||||||
test-account
|
test-account
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import ContainerCopyMessages from "../../../../ContainerCopyMessages";
|
|||||||
import { CopyJobContext } from "../../../../Context/CopyJobContext";
|
import { CopyJobContext } from "../../../../Context/CopyJobContext";
|
||||||
import { CopyJobMigrationType } from "../../../../Enums/CopyJobEnums";
|
import { CopyJobMigrationType } from "../../../../Enums/CopyJobEnums";
|
||||||
import { CopyJobContextProviderType, CopyJobContextState } from "../../../../Types/CopyJobTypes";
|
import { CopyJobContextProviderType, CopyJobContextState } from "../../../../Types/CopyJobTypes";
|
||||||
import { AccountDropdown } from "./AccountDropdown";
|
import { AccountDropdown, normalizeAccountId } from "./AccountDropdown";
|
||||||
|
|
||||||
jest.mock("../../../../../../hooks/useDatabaseAccounts");
|
jest.mock("../../../../../../hooks/useDatabaseAccounts");
|
||||||
jest.mock("../../../../../../UserContext", () => ({
|
jest.mock("../../../../../../UserContext", () => ({
|
||||||
@@ -202,13 +202,16 @@ describe("AccountDropdown", () => {
|
|||||||
|
|
||||||
const stateUpdateFunction = mockSetCopyJobState.mock.calls[0][0];
|
const stateUpdateFunction = mockSetCopyJobState.mock.calls[0][0];
|
||||||
const newState = stateUpdateFunction(mockCopyJobState);
|
const newState = stateUpdateFunction(mockCopyJobState);
|
||||||
expect(newState.source.account).toBe(mockDatabaseAccount1);
|
expect(newState.source.account).toEqual({
|
||||||
|
...mockDatabaseAccount1,
|
||||||
|
id: normalizeAccountId(mockDatabaseAccount1.id),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should auto-select predefined account from userContext if available", async () => {
|
it("should auto-select predefined account from userContext if available", async () => {
|
||||||
const userContextAccount = {
|
const userContextAccount = {
|
||||||
...mockDatabaseAccount2,
|
...mockDatabaseAccount2,
|
||||||
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/account2",
|
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/account2",
|
||||||
};
|
};
|
||||||
|
|
||||||
(userContext as any).databaseAccount = userContextAccount;
|
(userContext as any).databaseAccount = userContextAccount;
|
||||||
@@ -223,7 +226,10 @@ describe("AccountDropdown", () => {
|
|||||||
|
|
||||||
const stateUpdateFunction = mockSetCopyJobState.mock.calls[0][0];
|
const stateUpdateFunction = mockSetCopyJobState.mock.calls[0][0];
|
||||||
const newState = stateUpdateFunction(mockCopyJobState);
|
const newState = stateUpdateFunction(mockCopyJobState);
|
||||||
expect(newState.source.account).toBe(mockDatabaseAccount2);
|
expect(newState.source.account).toEqual({
|
||||||
|
...mockDatabaseAccount2,
|
||||||
|
id: normalizeAccountId(mockDatabaseAccount2.id),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should keep current account if it exists in the filtered list", async () => {
|
it("should keep current account if it exists in the filtered list", async () => {
|
||||||
@@ -248,7 +254,16 @@ describe("AccountDropdown", () => {
|
|||||||
|
|
||||||
const stateUpdateFunction = mockSetCopyJobState.mock.calls[0][0];
|
const stateUpdateFunction = mockSetCopyJobState.mock.calls[0][0];
|
||||||
const newState = stateUpdateFunction(contextWithSelectedAccount.copyJobState);
|
const newState = stateUpdateFunction(contextWithSelectedAccount.copyJobState);
|
||||||
expect(newState).toBe(contextWithSelectedAccount.copyJobState);
|
expect(newState).toEqual({
|
||||||
|
...contextWithSelectedAccount.copyJobState,
|
||||||
|
source: {
|
||||||
|
...contextWithSelectedAccount.copyJobState.source,
|
||||||
|
account: {
|
||||||
|
...mockDatabaseAccount1,
|
||||||
|
id: normalizeAccountId(mockDatabaseAccount1.id),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle account change when user selects different account", async () => {
|
it("should handle account change when user selects different account", async () => {
|
||||||
@@ -272,7 +287,7 @@ describe("AccountDropdown", () => {
|
|||||||
it("should normalize account ID for Portal platform", () => {
|
it("should normalize account ID for Portal platform", () => {
|
||||||
const portalAccount = {
|
const portalAccount = {
|
||||||
...mockDatabaseAccount1,
|
...mockDatabaseAccount1,
|
||||||
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/account1",
|
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/account1",
|
||||||
};
|
};
|
||||||
|
|
||||||
(configContext as any).platform = Platform.Portal;
|
(configContext as any).platform = Platform.Portal;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import FieldRow from "../../Components/FieldRow";
|
|||||||
|
|
||||||
interface AccountDropdownProps {}
|
interface AccountDropdownProps {}
|
||||||
|
|
||||||
const normalizeAccountId = (id: string) => {
|
export const normalizeAccountId = (id: string = "") => {
|
||||||
if (configContext.platform === Platform.Portal) {
|
if (configContext.platform === Platform.Portal) {
|
||||||
return id.replace("/Microsoft.DocumentDb/", "/Microsoft.DocumentDB/");
|
return id.replace("/Microsoft.DocumentDb/", "/Microsoft.DocumentDB/");
|
||||||
} else if (configContext.platform === Platform.Hosted) {
|
} else if (configContext.platform === Platform.Hosted) {
|
||||||
@@ -27,7 +27,12 @@ export const AccountDropdown: React.FC<AccountDropdownProps> = () => {
|
|||||||
|
|
||||||
const selectedSubscriptionId = copyJobState?.source?.subscription?.subscriptionId;
|
const selectedSubscriptionId = copyJobState?.source?.subscription?.subscriptionId;
|
||||||
const allAccounts: DatabaseAccount[] = useDatabaseAccounts(selectedSubscriptionId);
|
const allAccounts: DatabaseAccount[] = useDatabaseAccounts(selectedSubscriptionId);
|
||||||
const sqlApiOnlyAccounts: DatabaseAccount[] = (allAccounts || []).filter((account) => apiType(account) === "SQL");
|
const sqlApiOnlyAccounts = (allAccounts || [])
|
||||||
|
.filter((account) => apiType(account) === "SQL")
|
||||||
|
.map((account) => ({
|
||||||
|
...account,
|
||||||
|
id: normalizeAccountId(account.id),
|
||||||
|
}));
|
||||||
|
|
||||||
const updateCopyJobState = (newAccount: DatabaseAccount) => {
|
const updateCopyJobState = (newAccount: DatabaseAccount) => {
|
||||||
setCopyJobState((prevState) => {
|
setCopyJobState((prevState) => {
|
||||||
@@ -47,9 +52,8 @@ export const AccountDropdown: React.FC<AccountDropdownProps> = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (sqlApiOnlyAccounts && sqlApiOnlyAccounts.length > 0 && selectedSubscriptionId) {
|
if (sqlApiOnlyAccounts && sqlApiOnlyAccounts.length > 0 && selectedSubscriptionId) {
|
||||||
const currentAccountId = copyJobState?.source?.account?.id;
|
const currentAccountId = copyJobState?.source?.account?.id;
|
||||||
const predefinedAccountId = userContext.databaseAccount?.id;
|
const predefinedAccountId = normalizeAccountId(userContext.databaseAccount?.id);
|
||||||
const selectedAccountId = currentAccountId || predefinedAccountId;
|
const selectedAccountId = currentAccountId || predefinedAccountId;
|
||||||
|
|
||||||
const targetAccount: DatabaseAccount | null =
|
const targetAccount: DatabaseAccount | null =
|
||||||
sqlApiOnlyAccounts.find((account) => account.id === selectedAccountId) || null;
|
sqlApiOnlyAccounts.find((account) => account.id === selectedAccountId) || null;
|
||||||
updateCopyJobState(targetAccount || sqlApiOnlyAccounts[0]);
|
updateCopyJobState(targetAccount || sqlApiOnlyAccounts[0]);
|
||||||
@@ -58,7 +62,7 @@ export const AccountDropdown: React.FC<AccountDropdownProps> = () => {
|
|||||||
|
|
||||||
const accountOptions =
|
const accountOptions =
|
||||||
sqlApiOnlyAccounts?.map((account) => ({
|
sqlApiOnlyAccounts?.map((account) => ({
|
||||||
key: normalizeAccountId(account.id),
|
key: account.id,
|
||||||
text: account.name,
|
text: account.name,
|
||||||
data: account,
|
data: account,
|
||||||
})) || [];
|
})) || [];
|
||||||
|
|||||||
@@ -0,0 +1,241 @@
|
|||||||
|
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(container.querySelector("[data-test='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.getByTestId("migration-type-description-online")).toBeInTheDocument();
|
||||||
|
|
||||||
|
const learnMoreLink = screen.getByRole("link", {
|
||||||
|
name: "online copy jobs",
|
||||||
|
});
|
||||||
|
expect(learnMoreLink).toBeInTheDocument();
|
||||||
|
expect(learnMoreLink).toHaveAttribute(
|
||||||
|
"href",
|
||||||
|
"https://learn.microsoft.com/azure/cosmos-db/container-copy?tabs=online-copy&pivots=api-nosql#getting-started",
|
||||||
|
);
|
||||||
|
expect(learnMoreLink).toHaveAttribute("target", "_blank");
|
||||||
|
});
|
||||||
|
|
||||||
|
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.getByTestId("migration-type-description-offline")).toBeInTheDocument();
|
||||||
|
|
||||||
|
const learnMoreLink = screen.getByRole("link", {
|
||||||
|
name: "offline copy jobs",
|
||||||
|
});
|
||||||
|
expect(learnMoreLink).toBeInTheDocument();
|
||||||
|
expect(learnMoreLink).toHaveAttribute(
|
||||||
|
"href",
|
||||||
|
"https://learn.microsoft.com/azure/cosmos-db/how-to-container-copy?tabs=offline-copy&pivots=api-nosql",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { container } = render(<MigrationType />);
|
||||||
|
|
||||||
|
expect(container.querySelector("[data-test='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,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { container } = render(<MigrationType />);
|
||||||
|
|
||||||
|
expect(container.querySelector("[data-test='migration-type']")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
/* eslint-disable react/prop-types */
|
||||||
|
/* eslint-disable react/display-name */
|
||||||
|
import { ChoiceGroup, IChoiceGroupOption, Stack, Text } from "@fluentui/react";
|
||||||
|
import MarkdownRender from "@nteract/markdown";
|
||||||
|
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-test="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}`}
|
||||||
|
>
|
||||||
|
<MarkdownRender source={selectedKeyContent.description} linkTarget="_blank" />
|
||||||
|
</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,109 @@
|
|||||||
|
// 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-test="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"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="markdown-body "
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Online container copy jobs let you copy data from a source container to a destination Cosmos DB NoSQL API container using the
|
||||||
|
<a
|
||||||
|
href="https://learn.microsoft.com/azure/cosmos-db/change-feed-modes?tabs=all-versions-and-deletes#all-versions-and-deletes-change-feed-mode-preview"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
All Versions and Delete
|
||||||
|
</a>
|
||||||
|
change feed. This allows updates to continue on the source while data is copied. A brief downtime is required at the end to safely switch over client applications to the destination container. Learn more about
|
||||||
|
<a
|
||||||
|
href="https://learn.microsoft.com/azure/cosmos-db/container-copy?tabs=online-copy&pivots=api-nosql#getting-started"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
online copy jobs
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</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 "@testing-library/jest-dom";
|
||||||
import { fireEvent, render, screen } from "@testing-library/react";
|
import { render, screen } from "@testing-library/react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useCopyJobContext } from "../../../Context/CopyJobContext";
|
import { useCopyJobContext } from "../../../Context/CopyJobContext";
|
||||||
import { CopyJobMigrationType } from "../../../Enums/CopyJobEnums";
|
import { CopyJobMigrationType } from "../../../Enums/CopyJobEnums";
|
||||||
@@ -18,19 +18,8 @@ jest.mock("./Components/AccountDropdown", () => ({
|
|||||||
AccountDropdown: jest.fn(() => <div data-testid="account-dropdown">Account Dropdown</div>),
|
AccountDropdown: jest.fn(() => <div data-testid="account-dropdown">Account Dropdown</div>),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock("./Components/MigrationTypeCheckbox", () => ({
|
jest.mock("./Components/MigrationType", () => ({
|
||||||
MigrationTypeCheckbox: jest.fn(({ checked, onChange }: { checked: boolean; onChange: () => void }) => (
|
MigrationType: jest.fn(() => <div data-testid="migration-type">Migration Type</div>),
|
||||||
<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>
|
|
||||||
)),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe("SelectAccount", () => {
|
describe("SelectAccount", () => {
|
||||||
@@ -83,7 +72,7 @@ describe("SelectAccount", () => {
|
|||||||
|
|
||||||
expect(screen.getByTestId("subscription-dropdown")).toBeInTheDocument();
|
expect(screen.getByTestId("subscription-dropdown")).toBeInTheDocument();
|
||||||
expect(screen.getByTestId("account-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", () => {
|
it("should render correctly with snapshot", () => {
|
||||||
@@ -93,78 +82,20 @@ describe("SelectAccount", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("Migration Type Functionality", () => {
|
describe("Migration Type Functionality", () => {
|
||||||
it("should display migration type checkbox as unchecked when migrationType is Online", () => {
|
it("should render migration type component", () => {
|
||||||
(useCopyJobContext as jest.Mock).mockReturnValue({
|
|
||||||
...defaultContextValue,
|
|
||||||
copyJobState: {
|
|
||||||
...defaultContextValue.copyJobState,
|
|
||||||
migrationType: CopyJobMigrationType.Online,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
render(<SelectAccount />);
|
render(<SelectAccount />);
|
||||||
|
|
||||||
const checkbox = screen.getByTestId("migration-checkbox-input");
|
const migrationTypeComponent = screen.getByTestId("migration-type");
|
||||||
expect(checkbox).not.toBeChecked();
|
expect(migrationTypeComponent).toBeInTheDocument();
|
||||||
});
|
|
||||||
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Performance and Optimization", () => {
|
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 { 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 />);
|
rerender(<SelectAccount />);
|
||||||
|
|
||||||
const secondRenderHandler = migrationCheckbox.mock.calls[migrationCheckbox.mock.calls.length - 1][0].onChange;
|
expect(screen.getByTestId("migration-type")).toBeInTheDocument();
|
||||||
|
|
||||||
expect(firstRenderHandler).toBe(secondRenderHandler);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,24 +1,11 @@
|
|||||||
import { Stack, Text } from "@fluentui/react";
|
import { Stack, Text } from "@fluentui/react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import ContainerCopyMessages from "../../../ContainerCopyMessages";
|
import ContainerCopyMessages from "../../../ContainerCopyMessages";
|
||||||
import { useCopyJobContext } from "../../../Context/CopyJobContext";
|
|
||||||
import { CopyJobMigrationType } from "../../../Enums/CopyJobEnums";
|
|
||||||
import { AccountDropdown } from "./Components/AccountDropdown";
|
import { AccountDropdown } from "./Components/AccountDropdown";
|
||||||
import { MigrationTypeCheckbox } from "./Components/MigrationTypeCheckbox";
|
import { MigrationType } from "./Components/MigrationType";
|
||||||
import { SubscriptionDropdown } from "./Components/SubscriptionDropdown";
|
import { SubscriptionDropdown } from "./Components/SubscriptionDropdown";
|
||||||
|
|
||||||
const SelectAccount = React.memo(() => {
|
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 (
|
return (
|
||||||
<Stack data-test="Panel:SelectAccountContainer" className="selectAccountContainer" tokens={{ childrenGap: 15 }}>
|
<Stack data-test="Panel:SelectAccountContainer" className="selectAccountContainer" tokens={{ childrenGap: 15 }}>
|
||||||
<Text>{ContainerCopyMessages.selectAccountDescription}</Text>
|
<Text>{ContainerCopyMessages.selectAccountDescription}</Text>
|
||||||
@@ -27,7 +14,7 @@ const SelectAccount = React.memo(() => {
|
|||||||
|
|
||||||
<AccountDropdown />
|
<AccountDropdown />
|
||||||
|
|
||||||
<MigrationTypeCheckbox checked={migrationTypeChecked} onChange={handleMigrationTypeChange} />
|
<MigrationType />
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -21,14 +21,9 @@ exports[`SelectAccount Component Rendering should render correctly with snapshot
|
|||||||
Account Dropdown
|
Account Dropdown
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
data-testid="migration-type-checkbox"
|
data-testid="migration-type"
|
||||||
>
|
>
|
||||||
<input
|
Migration Type
|
||||||
aria-label="Migration Type Checkbox"
|
|
||||||
data-testid="migration-checkbox-input"
|
|
||||||
type="checkbox"
|
|
||||||
/>
|
|
||||||
Copy container in offline mode
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -47,7 +47,11 @@ const SelectSourceAndTargetContainers = ({ showAddCollectionPanel }: SelectSourc
|
|||||||
const onDropdownChange = dropDownChangeHandler(setCopyJobState);
|
const onDropdownChange = dropDownChangeHandler(setCopyJobState);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack className="selectSourceAndTargetContainers" tokens={{ childrenGap: 25 }}>
|
<Stack
|
||||||
|
data-test="Panel:SelectSourceAndTargetContainers"
|
||||||
|
className="selectSourceAndTargetContainers"
|
||||||
|
tokens={{ childrenGap: 25 }}
|
||||||
|
>
|
||||||
<span>{ContainerCopyMessages.selectSourceAndTargetContainersDescription}</span>
|
<span>{ContainerCopyMessages.selectSourceAndTargetContainersDescription}</span>
|
||||||
<DatabaseContainerSection
|
<DatabaseContainerSection
|
||||||
heading={ContainerCopyMessages.sourceContainerSubHeading}
|
heading={ContainerCopyMessages.sourceContainerSubHeading}
|
||||||
@@ -59,6 +63,7 @@ const SelectSourceAndTargetContainers = ({ showAddCollectionPanel }: SelectSourc
|
|||||||
selectedContainer={source?.containerId}
|
selectedContainer={source?.containerId}
|
||||||
containerDisabled={!source?.databaseId}
|
containerDisabled={!source?.databaseId}
|
||||||
containerOnChange={onDropdownChange("sourceContainer")}
|
containerOnChange={onDropdownChange("sourceContainer")}
|
||||||
|
sectionType="source"
|
||||||
/>
|
/>
|
||||||
<DatabaseContainerSection
|
<DatabaseContainerSection
|
||||||
heading={ContainerCopyMessages.targetContainerSubHeading}
|
heading={ContainerCopyMessages.targetContainerSubHeading}
|
||||||
@@ -71,6 +76,7 @@ const SelectSourceAndTargetContainers = ({ showAddCollectionPanel }: SelectSourc
|
|||||||
containerDisabled={!target?.databaseId}
|
containerDisabled={!target?.databaseId}
|
||||||
containerOnChange={onDropdownChange("targetContainer")}
|
containerOnChange={onDropdownChange("targetContainer")}
|
||||||
handleOnDemandCreateContainer={showAddCollectionPanel}
|
handleOnDemandCreateContainer={showAddCollectionPanel}
|
||||||
|
sectionType="target"
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ describe("DatabaseContainerSection", () => {
|
|||||||
selectedContainer: "container1",
|
selectedContainer: "container1",
|
||||||
containerDisabled: false,
|
containerDisabled: false,
|
||||||
containerOnChange: mockContainerOnChange,
|
containerOnChange: mockContainerOnChange,
|
||||||
|
sectionType: "source",
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -292,6 +293,7 @@ describe("DatabaseContainerSection", () => {
|
|||||||
containerOptions: mockContainerOptions,
|
containerOptions: mockContainerOptions,
|
||||||
selectedContainer: "container1",
|
selectedContainer: "container1",
|
||||||
containerOnChange: mockContainerOnChange,
|
containerOnChange: mockContainerOnChange,
|
||||||
|
sectionType: "source",
|
||||||
};
|
};
|
||||||
|
|
||||||
render(<DatabaseContainerSection {...minimalProps} />);
|
render(<DatabaseContainerSection {...minimalProps} />);
|
||||||
@@ -393,6 +395,7 @@ describe("DatabaseContainerSection", () => {
|
|||||||
containerOptions: [{ key: "c1", text: "Container 1", data: { id: "c1" } }],
|
containerOptions: [{ key: "c1", text: "Container 1", data: { id: "c1" } }],
|
||||||
selectedContainer: "c1",
|
selectedContainer: "c1",
|
||||||
containerOnChange: jest.fn(),
|
containerOnChange: jest.fn(),
|
||||||
|
sectionType: "source",
|
||||||
};
|
};
|
||||||
|
|
||||||
const { container } = render(<DatabaseContainerSection {...minimalProps} />);
|
const { container } = render(<DatabaseContainerSection {...minimalProps} />);
|
||||||
@@ -411,6 +414,7 @@ describe("DatabaseContainerSection", () => {
|
|||||||
containerDisabled: false,
|
containerDisabled: false,
|
||||||
containerOnChange: jest.fn(),
|
containerOnChange: jest.fn(),
|
||||||
handleOnDemandCreateContainer: jest.fn(),
|
handleOnDemandCreateContainer: jest.fn(),
|
||||||
|
sectionType: "target",
|
||||||
};
|
};
|
||||||
|
|
||||||
const { container } = render(<DatabaseContainerSection {...fullProps} />);
|
const { container } = render(<DatabaseContainerSection {...fullProps} />);
|
||||||
@@ -428,6 +432,7 @@ describe("DatabaseContainerSection", () => {
|
|||||||
selectedContainer: "container1",
|
selectedContainer: "container1",
|
||||||
containerDisabled: true,
|
containerDisabled: true,
|
||||||
containerOnChange: jest.fn(),
|
containerOnChange: jest.fn(),
|
||||||
|
sectionType: "target",
|
||||||
};
|
};
|
||||||
|
|
||||||
const { container } = render(<DatabaseContainerSection {...disabledProps} />);
|
const { container } = render(<DatabaseContainerSection {...disabledProps} />);
|
||||||
@@ -443,6 +448,7 @@ describe("DatabaseContainerSection", () => {
|
|||||||
containerOptions: [],
|
containerOptions: [],
|
||||||
selectedContainer: "",
|
selectedContainer: "",
|
||||||
containerOnChange: jest.fn(),
|
containerOnChange: jest.fn(),
|
||||||
|
sectionType: "target",
|
||||||
};
|
};
|
||||||
|
|
||||||
const { container } = render(<DatabaseContainerSection {...emptyOptionsProps} />);
|
const { container } = render(<DatabaseContainerSection {...emptyOptionsProps} />);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export const DatabaseContainerSection = ({
|
|||||||
containerDisabled,
|
containerDisabled,
|
||||||
containerOnChange,
|
containerOnChange,
|
||||||
handleOnDemandCreateContainer,
|
handleOnDemandCreateContainer,
|
||||||
|
sectionType,
|
||||||
}: DatabaseContainerSectionProps) => (
|
}: DatabaseContainerSectionProps) => (
|
||||||
<Stack tokens={{ childrenGap: 15 }} className="databaseContainerSection">
|
<Stack tokens={{ childrenGap: 15 }} className="databaseContainerSection">
|
||||||
<label className="subHeading">{heading}</label>
|
<label className="subHeading">{heading}</label>
|
||||||
@@ -27,6 +28,7 @@ export const DatabaseContainerSection = ({
|
|||||||
disabled={!!databaseDisabled}
|
disabled={!!databaseDisabled}
|
||||||
selectedKey={selectedDatabase}
|
selectedKey={selectedDatabase}
|
||||||
onChange={databaseOnChange}
|
onChange={databaseOnChange}
|
||||||
|
data-test={`${sectionType}-databaseDropdown`}
|
||||||
/>
|
/>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
<FieldRow label={ContainerCopyMessages.containerDropdownLabel}>
|
<FieldRow label={ContainerCopyMessages.containerDropdownLabel}>
|
||||||
@@ -39,6 +41,7 @@ export const DatabaseContainerSection = ({
|
|||||||
disabled={!!containerDisabled}
|
disabled={!!containerDisabled}
|
||||||
selectedKey={selectedContainer}
|
selectedKey={selectedContainer}
|
||||||
onChange={containerOnChange}
|
onChange={containerOnChange}
|
||||||
|
data-test={`${sectionType}-containerDropdown`}
|
||||||
/>
|
/>
|
||||||
{handleOnDemandCreateContainer && (
|
{handleOnDemandCreateContainer && (
|
||||||
<ActionButton className="create-container-link-btn" onClick={() => handleOnDemandCreateContainer()}>
|
<ActionButton className="create-container-link-btn" onClick={() => handleOnDemandCreateContainer()}>
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with all pro
|
|||||||
class="ms-Dropdown is-required dropdown-112"
|
class="ms-Dropdown is-required dropdown-112"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
|
data-test="target-databaseDropdown"
|
||||||
id="Dropdown98"
|
id="Dropdown98"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@@ -94,6 +95,7 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with all pro
|
|||||||
class="ms-Dropdown is-required dropdown-112"
|
class="ms-Dropdown is-required dropdown-112"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
|
data-test="target-containerDropdown"
|
||||||
id="Dropdown99"
|
id="Dropdown99"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@@ -182,6 +184,7 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with disable
|
|||||||
class="ms-Dropdown is-disabled is-required dropdown-143"
|
class="ms-Dropdown is-disabled is-required dropdown-143"
|
||||||
data-is-focusable="false"
|
data-is-focusable="false"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
|
data-test="target-databaseDropdown"
|
||||||
id="Dropdown103"
|
id="Dropdown103"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
@@ -239,6 +242,7 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with disable
|
|||||||
class="ms-Dropdown is-disabled is-required dropdown-143"
|
class="ms-Dropdown is-disabled is-required dropdown-143"
|
||||||
data-is-focusable="false"
|
data-is-focusable="false"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
|
data-test="target-containerDropdown"
|
||||||
id="Dropdown104"
|
id="Dropdown104"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
@@ -306,6 +310,7 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with empty o
|
|||||||
class="ms-Dropdown is-required dropdown-112"
|
class="ms-Dropdown is-required dropdown-112"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
|
data-test="target-databaseDropdown"
|
||||||
id="Dropdown105"
|
id="Dropdown105"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@@ -363,6 +368,7 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with empty o
|
|||||||
class="ms-Dropdown is-required dropdown-112"
|
class="ms-Dropdown is-required dropdown-112"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
|
data-test="target-containerDropdown"
|
||||||
id="Dropdown106"
|
id="Dropdown106"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@@ -430,6 +436,7 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with minimal
|
|||||||
class="ms-Dropdown is-required dropdown-112"
|
class="ms-Dropdown is-required dropdown-112"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
|
data-test="source-databaseDropdown"
|
||||||
id="Dropdown96"
|
id="Dropdown96"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@@ -487,6 +494,7 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with minimal
|
|||||||
class="ms-Dropdown is-required dropdown-112"
|
class="ms-Dropdown is-required dropdown-112"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
|
data-test="source-containerDropdown"
|
||||||
id="Dropdown97"
|
id="Dropdown97"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable jest/no-conditional-expect */
|
||||||
import "@testing-library/jest-dom";
|
import "@testing-library/jest-dom";
|
||||||
import { fireEvent, render, screen } from "@testing-library/react";
|
import { fireEvent, render, screen } from "@testing-library/react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
@@ -5,6 +6,20 @@ import { CopyJobActions, CopyJobMigrationType, CopyJobStatusType } from "../../E
|
|||||||
import { CopyJobType, HandleJobActionClickType } from "../../Types/CopyJobTypes";
|
import { CopyJobType, HandleJobActionClickType } from "../../Types/CopyJobTypes";
|
||||||
import CopyJobActionMenu from "./CopyJobActionMenu";
|
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", () => ({
|
jest.mock("../../ContainerCopyMessages", () => ({
|
||||||
__esModule: true,
|
__esModule: true,
|
||||||
default: {
|
default: {
|
||||||
@@ -18,6 +33,11 @@ jest.mock("../../ContainerCopyMessages", () => ({
|
|||||||
cancel: "Cancel",
|
cancel: "Cancel",
|
||||||
complete: "Complete",
|
complete: "Complete",
|
||||||
},
|
},
|
||||||
|
dialog: {
|
||||||
|
heading: "Confirm Action",
|
||||||
|
confirmButtonText: "Confirm",
|
||||||
|
cancelButtonText: "Cancel",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
@@ -50,6 +70,9 @@ describe("CopyJobActionMenu", () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
|
mockShowOkCancelModalDialog.mockClear();
|
||||||
|
mockCloseDialog.mockClear();
|
||||||
|
mockOpenDialog.mockClear();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Component Rendering", () => {
|
describe("Component Rendering", () => {
|
||||||
@@ -266,7 +289,29 @@ describe("CopyJobActionMenu", () => {
|
|||||||
expect(mockHandleClick).toHaveBeenCalledWith(job, CopyJobActions.pause, expect.any(Function));
|
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,
|
||||||
|
"Confirm",
|
||||||
|
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 });
|
const job = createMockJob({ Status: CopyJobStatusType.InProgress });
|
||||||
|
|
||||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||||
@@ -277,6 +322,9 @@ describe("CopyJobActionMenu", () => {
|
|||||||
const cancelButton = screen.getByText("Cancel");
|
const cancelButton = screen.getByText("Cancel");
|
||||||
fireEvent.click(cancelButton);
|
fireEvent.click(cancelButton);
|
||||||
|
|
||||||
|
const [, , , onOkCallback] = mockShowOkCancelModalDialog.mock.calls[0];
|
||||||
|
onOkCallback();
|
||||||
|
|
||||||
expect(mockHandleClick).toHaveBeenCalledWith(job, CopyJobActions.cancel, expect.any(Function));
|
expect(mockHandleClick).toHaveBeenCalledWith(job, CopyJobActions.cancel, expect.any(Function));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -294,7 +342,33 @@ describe("CopyJobActionMenu", () => {
|
|||||||
expect(mockHandleClick).toHaveBeenCalledWith(job, CopyJobActions.resume, expect.any(Function));
|
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,
|
||||||
|
"Confirm",
|
||||||
|
expect.any(Function),
|
||||||
|
"Cancel",
|
||||||
|
null,
|
||||||
|
expect.any(Object), // dialogBody content
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call handleClick when dialog is confirmed for complete action", () => {
|
||||||
const job = createMockJob({
|
const job = createMockJob({
|
||||||
Status: CopyJobStatusType.InProgress,
|
Status: CopyJobStatusType.InProgress,
|
||||||
Mode: CopyJobMigrationType.Online,
|
Mode: CopyJobMigrationType.Online,
|
||||||
@@ -308,10 +382,87 @@ describe("CopyJobActionMenu", () => {
|
|||||||
const completeButton = screen.getByText("Complete");
|
const completeButton = screen.getByText("Complete");
|
||||||
fireEvent.click(completeButton);
|
fireEvent.click(completeButton);
|
||||||
|
|
||||||
|
const [, , , onOkCallback] = mockShowOkCancelModalDialog.mock.calls[0];
|
||||||
|
onOkCallback();
|
||||||
|
|
||||||
expect(mockHandleClick).toHaveBeenCalledWith(job, CopyJobActions.complete, expect.any(Function));
|
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,
|
||||||
|
"Confirm",
|
||||||
|
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,
|
||||||
|
"Confirm",
|
||||||
|
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", () => {
|
describe("Disabled States During Updates", () => {
|
||||||
const TestComponentWrapper: React.FC<{
|
const TestComponentWrapper: React.FC<{
|
||||||
job: CopyJobType;
|
job: CopyJobType;
|
||||||
@@ -339,8 +490,13 @@ describe("CopyJobActionMenu", () => {
|
|||||||
const pauseButton = screen.getByText("Pause");
|
const pauseButton = screen.getByText("Pause");
|
||||||
fireEvent.click(pauseButton);
|
fireEvent.click(pauseButton);
|
||||||
fireEvent.click(actionButton);
|
fireEvent.click(actionButton);
|
||||||
const pauseButtonAfterClick = screen.getByText("Pause");
|
|
||||||
|
const pauseButtonAfterClick = screen.getByText("Pause").closest("button");
|
||||||
expect(pauseButtonAfterClick).toBeInTheDocument();
|
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", () => {
|
it("should not disable actions for different jobs when one is updating", () => {
|
||||||
@@ -360,22 +516,6 @@ describe("CopyJobActionMenu", () => {
|
|||||||
expect(screen.getByText("Cancel")).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", () => {
|
it("should handle complete action disabled state for online jobs", () => {
|
||||||
const job = createMockJob({
|
const job = createMockJob({
|
||||||
Status: CopyJobStatusType.InProgress,
|
Status: CopyJobStatusType.InProgress,
|
||||||
@@ -462,6 +602,7 @@ describe("CopyJobActionMenu", () => {
|
|||||||
|
|
||||||
expect(actionButton).toHaveAttribute("aria-label", "Actions");
|
expect(actionButton).toHaveAttribute("aria-label", "Actions");
|
||||||
expect(actionButton).toHaveAttribute("title", "Actions");
|
expect(actionButton).toHaveAttribute("title", "Actions");
|
||||||
|
expect(actionButton).toHaveAttribute("role", "button");
|
||||||
|
|
||||||
const moreIcon = actionButton.querySelector('[data-icon-name="More"]');
|
const moreIcon = actionButton.querySelector('[data-icon-name="More"]');
|
||||||
expect(moreIcon || actionButton).toBeInTheDocument();
|
expect(moreIcon || actionButton).toBeInTheDocument();
|
||||||
@@ -608,4 +749,129 @@ describe("CopyJobActionMenu", () => {
|
|||||||
}).not.toThrow();
|
}).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
|
||||||
|
"Confirm", // confirmLabel
|
||||||
|
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 React from "react";
|
||||||
|
import { useDialog } from "../../../Controls/Dialog";
|
||||||
import ContainerCopyMessages from "../../ContainerCopyMessages";
|
import ContainerCopyMessages from "../../ContainerCopyMessages";
|
||||||
import { CopyJobActions, CopyJobMigrationType, CopyJobStatusType } from "../../Enums/CopyJobEnums";
|
import { CopyJobActions, CopyJobMigrationType, CopyJobStatusType } from "../../Enums/CopyJobEnums";
|
||||||
import { CopyJobType, HandleJobActionClickType } from "../../Types/CopyJobTypes";
|
import { CopyJobType, HandleJobActionClickType } from "../../Types/CopyJobTypes";
|
||||||
@@ -9,6 +10,28 @@ interface CopyJobActionMenuProps {
|
|||||||
handleClick: HandleJobActionClickType;
|
handleClick: HandleJobActionClickType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const dialogBody = {
|
||||||
|
[CopyJobActions.cancel]: (jobName: string) => (
|
||||||
|
<Stack tokens={{ childrenGap: 10 }}>
|
||||||
|
<Stack.Item>
|
||||||
|
You are about to cancel <b>{jobName}</b> copy job.
|
||||||
|
</Stack.Item>
|
||||||
|
<Stack.Item>Cancelling will stop the job immediately.</Stack.Item>
|
||||||
|
</Stack>
|
||||||
|
),
|
||||||
|
[CopyJobActions.complete]: (jobName: string) => (
|
||||||
|
<Stack tokens={{ childrenGap: 10 }}>
|
||||||
|
<Stack.Item>
|
||||||
|
You are about to complete <b>{jobName}</b> copy job.
|
||||||
|
</Stack.Item>
|
||||||
|
<Stack.Item>
|
||||||
|
Once completed, continuous data copy will stop after any pending documents are processed. To maintain data
|
||||||
|
integrity, we recommend stopping updates to the source container before completing the job.
|
||||||
|
</Stack.Item>
|
||||||
|
</Stack>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick }) => {
|
const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick }) => {
|
||||||
const [updatingJobAction, setUpdatingJobAction] = React.useState<{ jobName: string; action: string } | null>(null);
|
const [updatingJobAction, setUpdatingJobAction] = React.useState<{ jobName: string; action: string } | null>(null);
|
||||||
if (
|
if (
|
||||||
@@ -22,9 +45,22 @@ const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick
|
|||||||
return null;
|
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 getMenuItems = (): IContextualMenuProps["items"] => {
|
||||||
const isThisJobUpdating = updatingJobAction?.jobName === job.Name;
|
const isThisJobUpdating = updatingJobAction?.jobName === job.Name;
|
||||||
const updatingAction = updatingJobAction?.action;
|
|
||||||
|
|
||||||
const baseItems = [
|
const baseItems = [
|
||||||
{
|
{
|
||||||
@@ -32,21 +68,21 @@ const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick
|
|||||||
text: ContainerCopyMessages.MonitorJobs.Actions.pause,
|
text: ContainerCopyMessages.MonitorJobs.Actions.pause,
|
||||||
iconProps: { iconName: "Pause" },
|
iconProps: { iconName: "Pause" },
|
||||||
onClick: () => handleClick(job, CopyJobActions.pause, setUpdatingJobAction),
|
onClick: () => handleClick(job, CopyJobActions.pause, setUpdatingJobAction),
|
||||||
disabled: isThisJobUpdating && updatingAction === CopyJobActions.pause,
|
disabled: isThisJobUpdating,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: CopyJobActions.cancel,
|
key: CopyJobActions.cancel,
|
||||||
text: ContainerCopyMessages.MonitorJobs.Actions.cancel,
|
text: ContainerCopyMessages.MonitorJobs.Actions.cancel,
|
||||||
iconProps: { iconName: "Cancel" },
|
iconProps: { iconName: "Cancel" },
|
||||||
onClick: () => handleClick(job, CopyJobActions.cancel, setUpdatingJobAction),
|
onClick: () => showActionConfirmationDialog(job, CopyJobActions.cancel),
|
||||||
disabled: isThisJobUpdating && updatingAction === CopyJobActions.cancel,
|
disabled: isThisJobUpdating,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: CopyJobActions.resume,
|
key: CopyJobActions.resume,
|
||||||
text: ContainerCopyMessages.MonitorJobs.Actions.resume,
|
text: ContainerCopyMessages.MonitorJobs.Actions.resume,
|
||||||
iconProps: { iconName: "Play" },
|
iconProps: { iconName: "Play" },
|
||||||
onClick: () => handleClick(job, CopyJobActions.resume, setUpdatingJobAction),
|
onClick: () => handleClick(job, CopyJobActions.resume, setUpdatingJobAction),
|
||||||
disabled: isThisJobUpdating && updatingAction === CopyJobActions.resume,
|
disabled: isThisJobUpdating,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -67,8 +103,8 @@ const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick
|
|||||||
key: CopyJobActions.complete,
|
key: CopyJobActions.complete,
|
||||||
text: ContainerCopyMessages.MonitorJobs.Actions.complete,
|
text: ContainerCopyMessages.MonitorJobs.Actions.complete,
|
||||||
iconProps: { iconName: "CheckMark" },
|
iconProps: { iconName: "CheckMark" },
|
||||||
onClick: () => handleClick(job, CopyJobActions.complete, setUpdatingJobAction),
|
onClick: () => showActionConfirmationDialog(job, CopyJobActions.complete),
|
||||||
disabled: isThisJobUpdating && updatingAction === CopyJobActions.complete,
|
disabled: isThisJobUpdating,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return filteredItems;
|
return filteredItems;
|
||||||
@@ -83,10 +119,11 @@ const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
|
data-test={`CopyJobActionMenu/Button:${job.Name}`}
|
||||||
role="button"
|
role="button"
|
||||||
iconProps={{ iconName: "More", styles: { root: { fontSize: "20px", fontWeight: "bold" } } }}
|
iconProps={{ iconName: "More", styles: { root: { fontSize: "20px", fontWeight: "bold" } } }}
|
||||||
menuProps={{ items: getMenuItems() }}
|
menuProps={{ items: getMenuItems(), directionalHint: DirectionalHint.leftTopEdge, directionalHintFixed: false }}
|
||||||
menuIconProps={{ iconName: "" }}
|
menuIconProps={{ iconName: "", className: "hidden" }}
|
||||||
ariaLabel={ContainerCopyMessages.MonitorJobs.Columns.actions}
|
ariaLabel={ContainerCopyMessages.MonitorJobs.Columns.actions}
|
||||||
title={ContainerCopyMessages.MonitorJobs.Columns.actions}
|
title={ContainerCopyMessages.MonitorJobs.Columns.actions}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ const CopyJobsList: React.FC<CopyJobsListProps> = ({ jobs, handleActionClick, pa
|
|||||||
<Stack.Item verticalFill={true} grow={1} shrink={1} style={styles.stackItem}>
|
<Stack.Item verticalFill={true} grow={1} shrink={1} style={styles.stackItem}>
|
||||||
<ScrollablePane scrollbarVisibility={ScrollbarVisibility.auto}>
|
<ScrollablePane scrollbarVisibility={ScrollbarVisibility.auto}>
|
||||||
<ShimmeredDetailsList
|
<ShimmeredDetailsList
|
||||||
|
className="CopyJobListContainer"
|
||||||
onRenderRow={_onRenderRow}
|
onRenderRow={_onRenderRow}
|
||||||
checkboxVisibility={2}
|
checkboxVisibility={2}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import CopyJobsNotFound from "../MonitorCopyJobs/Components/CopyJobs.NotFound";
|
|||||||
import { CopyJobType, JobActionUpdatorType } from "../Types/CopyJobTypes";
|
import { CopyJobType, JobActionUpdatorType } from "../Types/CopyJobTypes";
|
||||||
import CopyJobsList from "./Components/CopyJobsList";
|
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%" });
|
const SHIMMER_INDENT_LEVELS: IndentLevel[] = Array(7).fill({ level: 0, width: "100%" });
|
||||||
|
|
||||||
interface MonitorCopyJobsProps {
|
interface MonitorCopyJobsProps {
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export interface DatabaseContainerSectionProps {
|
|||||||
containerDisabled?: boolean;
|
containerDisabled?: boolean;
|
||||||
containerOnChange: (ev: React.FormEvent<HTMLDivElement>, option: DropdownOptionType) => void;
|
containerOnChange: (ev: React.FormEvent<HTMLDivElement>, option: DropdownOptionType) => void;
|
||||||
handleOnDemandCreateContainer?: () => void;
|
handleOnDemandCreateContainer?: () => void;
|
||||||
|
sectionType: "source" | "target";
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CopyJobContextState {
|
export interface CopyJobContextState {
|
||||||
|
|||||||
@@ -121,6 +121,9 @@
|
|||||||
&:hover {
|
&:hover {
|
||||||
background-color: @BaseMediumLow;
|
background-color: @BaseMediumLow;
|
||||||
}
|
}
|
||||||
|
.ms-DetailsHeader-cellTitle {
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ describe("PaneContainerComponent test", () => {
|
|||||||
headerText: "test",
|
headerText: "test",
|
||||||
panelContent: <div></div>,
|
panelContent: <div></div>,
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
|
isLightDismiss: true,
|
||||||
hasConsole: false,
|
hasConsole: false,
|
||||||
isConsoleExpanded: false,
|
isConsoleExpanded: false,
|
||||||
};
|
};
|
||||||
@@ -20,6 +21,7 @@ describe("PaneContainerComponent test", () => {
|
|||||||
headerText: "test",
|
headerText: "test",
|
||||||
panelContent: <div></div>,
|
panelContent: <div></div>,
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
|
isLightDismiss: true,
|
||||||
hasConsole: true,
|
hasConsole: true,
|
||||||
isConsoleExpanded: false,
|
isConsoleExpanded: false,
|
||||||
};
|
};
|
||||||
@@ -32,6 +34,7 @@ describe("PaneContainerComponent test", () => {
|
|||||||
headerText: "test",
|
headerText: "test",
|
||||||
panelContent: undefined,
|
panelContent: undefined,
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
|
isLightDismiss: true,
|
||||||
hasConsole: true,
|
hasConsole: true,
|
||||||
isConsoleExpanded: false,
|
isConsoleExpanded: false,
|
||||||
};
|
};
|
||||||
@@ -44,6 +47,7 @@ describe("PaneContainerComponent test", () => {
|
|||||||
headerText: "test",
|
headerText: "test",
|
||||||
panelContent: <div></div>,
|
panelContent: <div></div>,
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
|
isLightDismiss: true,
|
||||||
hasConsole: true,
|
hasConsole: true,
|
||||||
isConsoleExpanded: true,
|
isConsoleExpanded: true,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export interface PanelContainerProps {
|
|||||||
isConsoleExpanded: boolean;
|
isConsoleExpanded: boolean;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
hasConsole: boolean;
|
hasConsole: boolean;
|
||||||
|
isLightDismiss: boolean;
|
||||||
isConsoleAnimationFinished?: boolean;
|
isConsoleAnimationFinished?: boolean;
|
||||||
panelWidth?: string;
|
panelWidth?: string;
|
||||||
onRenderNavigationContent?: IRenderFunction<IPanelProps>;
|
onRenderNavigationContent?: IRenderFunction<IPanelProps>;
|
||||||
@@ -58,7 +59,7 @@ export class PanelContainerComponent extends React.Component<PanelContainerProps
|
|||||||
headerText={this.props.headerText}
|
headerText={this.props.headerText}
|
||||||
isOpen={this.props.isOpen}
|
isOpen={this.props.isOpen}
|
||||||
onDismiss={this.onDissmiss}
|
onDismiss={this.onDissmiss}
|
||||||
isLightDismiss
|
isLightDismiss={this.props.isLightDismiss}
|
||||||
type={PanelType.custom}
|
type={PanelType.custom}
|
||||||
closeButtonAriaLabel={`Close ${this.props.headerText}`}
|
closeButtonAriaLabel={`Close ${this.props.headerText}`}
|
||||||
customWidth={this.props.panelWidth ? this.props.panelWidth : "440px"}
|
customWidth={this.props.panelWidth ? this.props.panelWidth : "440px"}
|
||||||
@@ -140,10 +141,11 @@ export class PanelContainerComponent extends React.Component<PanelContainerProps
|
|||||||
export const SidePanel: React.FC = () => {
|
export const SidePanel: React.FC = () => {
|
||||||
const isConsoleExpanded = useNotificationConsole((state) => state.isExpanded);
|
const isConsoleExpanded = useNotificationConsole((state) => state.isExpanded);
|
||||||
const isConsoleAnimationFinished = useNotificationConsole((state) => state.consoleAnimationFinished);
|
const isConsoleAnimationFinished = useNotificationConsole((state) => state.consoleAnimationFinished);
|
||||||
const { isOpen, hasConsole, panelContent, panelWidth, headerText } = useSidePanel((state) => {
|
const { isOpen, hasConsole, isLightDismiss, panelContent, panelWidth, headerText } = useSidePanel((state) => {
|
||||||
return {
|
return {
|
||||||
isOpen: state.isOpen,
|
isOpen: state.isOpen,
|
||||||
hasConsole: state.hasConsole,
|
hasConsole: state.hasConsole,
|
||||||
|
isLightDismiss: state.isLightDismiss,
|
||||||
panelContent: state.panelContent,
|
panelContent: state.panelContent,
|
||||||
headerText: state.headerText,
|
headerText: state.headerText,
|
||||||
panelWidth: state.panelWidth,
|
panelWidth: state.panelWidth,
|
||||||
@@ -154,6 +156,7 @@ export const SidePanel: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<PanelContainerComponent
|
<PanelContainerComponent
|
||||||
hasConsole={hasConsole}
|
hasConsole={hasConsole}
|
||||||
|
isLightDismiss={isLightDismiss}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
panelContent={panelContent}
|
panelContent={panelContent}
|
||||||
headerText={headerText}
|
headerText={headerText}
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ const App = (): JSX.Element => {
|
|||||||
<>
|
<>
|
||||||
<ContainerCopyPanel explorer={explorer} />
|
<ContainerCopyPanel explorer={explorer} />
|
||||||
<SidePanel />
|
<SidePanel />
|
||||||
|
<Dialog />
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<DivExplorer explorer={explorer} />
|
<DivExplorer explorer={explorer} />
|
||||||
|
|||||||
@@ -4,20 +4,24 @@ export interface SidePanelState {
|
|||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
panelWidth: string;
|
panelWidth: string;
|
||||||
hasConsole: boolean;
|
hasConsole: boolean;
|
||||||
|
isLightDismiss: boolean;
|
||||||
panelContent?: JSX.Element;
|
panelContent?: JSX.Element;
|
||||||
headerText?: string;
|
headerText?: string;
|
||||||
setHeaderText: (headerText: string) => void;
|
setHeaderText: (headerText: string) => void;
|
||||||
openSidePanel: (headerText: string, panelContent: JSX.Element, panelWidth?: string, onClose?: () => void) => void;
|
openSidePanel: (headerText: string, panelContent: JSX.Element, panelWidth?: string, onClose?: () => void) => void;
|
||||||
closeSidePanel: () => void;
|
closeSidePanel: () => void;
|
||||||
setPanelHasConsole: (hasConsole: boolean) => void;
|
setPanelHasConsole: (hasConsole: boolean) => void;
|
||||||
|
setLightDismiss: (isLightDismiss: boolean) => void;
|
||||||
getRef?: React.RefObject<HTMLElement>; // Optional ref for focusing the last element.
|
getRef?: React.RefObject<HTMLElement>; // Optional ref for focusing the last element.
|
||||||
}
|
}
|
||||||
export const useSidePanel: UseStore<SidePanelState> = create((set) => ({
|
export const useSidePanel: UseStore<SidePanelState> = create((set) => ({
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
panelWidth: "440px",
|
panelWidth: "440px",
|
||||||
hasConsole: true,
|
hasConsole: true,
|
||||||
|
isLightDismiss: true,
|
||||||
setHeaderText: (headerText: string) => set((state) => ({ ...state, headerText })),
|
setHeaderText: (headerText: string) => set((state) => ({ ...state, headerText })),
|
||||||
setPanelHasConsole: (hasConsole: boolean) => set((state) => ({ ...state, hasConsole })),
|
setPanelHasConsole: (hasConsole: boolean) => set((state) => ({ ...state, hasConsole })),
|
||||||
|
setLightDismiss: (isLightDismiss: boolean) => set((state) => ({ ...state, isLightDismiss })),
|
||||||
openSidePanel: (headerText, panelContent, panelWidth = "440px") =>
|
openSidePanel: (headerText, panelContent, panelWidth = "440px") =>
|
||||||
set((state) => ({ ...state, headerText, panelContent, panelWidth, isOpen: true })),
|
set((state) => ({ ...state, headerText, panelContent, panelWidth, isOpen: true })),
|
||||||
closeSidePanel: () => {
|
closeSidePanel: () => {
|
||||||
|
|||||||
@@ -164,6 +164,9 @@ $ENV:NOSQL_TESTACCOUNT_TOKEN=az account get-access-token --scope "https://<accou
|
|||||||
# NoSQL API (Readonly)
|
# NoSQL API (Readonly)
|
||||||
$ENV:NOSQL_READONLY_TESTACCOUNT_TOKEN=az account get-access-token --scope "https://<account name>.documents.azure.com/.default" -o tsv --query accessToken
|
$ENV:NOSQL_READONLY_TESTACCOUNT_TOKEN=az account get-access-token --scope "https://<account name>.documents.azure.com/.default" -o tsv --query accessToken
|
||||||
|
|
||||||
|
# NoSQL API (Container Copy)
|
||||||
|
$ENV:NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN=az account get-access-token --scope "https://<account name>.documents.azure.com/.default" -o tsv --query accessToken
|
||||||
|
|
||||||
# Tables API
|
# Tables API
|
||||||
$ENV:TABLE_TESTACCOUNT_TOKEN=az account get-access-token --scope "https://<account name>.documents.azure.com/.default" -o tsv --query accessToken
|
$ENV:TABLE_TESTACCOUNT_TOKEN=az account get-access-token --scope "https://<account name>.documents.azure.com/.default" -o tsv --query accessToken
|
||||||
|
|
||||||
|
|||||||
167
test/fx.ts
167
test/fx.ts
@@ -11,7 +11,7 @@ export interface TestNameOptions {
|
|||||||
prefixed?: boolean;
|
prefixed?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateUniqueName(baseName, options?: TestNameOptions): string {
|
export function generateUniqueName(baseName: string, options?: TestNameOptions): string {
|
||||||
const length = options?.length ?? 1;
|
const length = options?.length ?? 1;
|
||||||
const timestamp = options?.timestampped === undefined ? true : options.timestampped;
|
const timestamp = options?.timestampped === undefined ? true : options.timestampped;
|
||||||
const prefixed = options?.prefixed === undefined ? true : options.prefixed;
|
const prefixed = options?.prefixed === undefined ? true : options.prefixed;
|
||||||
@@ -40,6 +40,7 @@ export enum TestAccount {
|
|||||||
Mongo32 = "Mongo32",
|
Mongo32 = "Mongo32",
|
||||||
SQL = "SQL",
|
SQL = "SQL",
|
||||||
SQLReadOnly = "SQLReadOnly",
|
SQLReadOnly = "SQLReadOnly",
|
||||||
|
SQLContainerCopyOnly = "SQLContainerCopyOnly",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultAccounts: Record<TestAccount, string> = {
|
export const defaultAccounts: Record<TestAccount, string> = {
|
||||||
@@ -51,6 +52,7 @@ export const defaultAccounts: Record<TestAccount, string> = {
|
|||||||
[TestAccount.Mongo32]: "github-e2etests-mongo32",
|
[TestAccount.Mongo32]: "github-e2etests-mongo32",
|
||||||
[TestAccount.SQL]: "github-e2etests-sql",
|
[TestAccount.SQL]: "github-e2etests-sql",
|
||||||
[TestAccount.SQLReadOnly]: "github-e2etests-sql-readonly",
|
[TestAccount.SQLReadOnly]: "github-e2etests-sql-readonly",
|
||||||
|
[TestAccount.SQLContainerCopyOnly]: "github-e2etests-sql-containercopyonly",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const resourceGroupName = process.env.DE_TEST_RESOURCE_GROUP ?? "de-e2e-tests";
|
export const resourceGroupName = process.env.DE_TEST_RESOURCE_GROUP ?? "de-e2e-tests";
|
||||||
@@ -77,7 +79,14 @@ export function getAccountName(accountType: TestAccount) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getTestExplorerUrl(accountType: TestAccount, iframeSrc?: string): Promise<string> {
|
type TestExplorerUrlOptions = {
|
||||||
|
iframeSrc?: string;
|
||||||
|
enablecontainercopy?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getTestExplorerUrl(accountType: TestAccount, options?: TestExplorerUrlOptions): Promise<string> {
|
||||||
|
const { iframeSrc, enablecontainercopy } = options ?? {};
|
||||||
|
|
||||||
// We can't retrieve AZ CLI credentials from the browser so we get them here.
|
// We can't retrieve AZ CLI credentials from the browser so we get them here.
|
||||||
const token = await getAzureCLICredentialsToken();
|
const token = await getAzureCLICredentialsToken();
|
||||||
const accountName = getAccountName(accountType);
|
const accountName = getAccountName(accountType);
|
||||||
@@ -93,6 +102,7 @@ export async function getTestExplorerUrl(accountType: TestAccount, iframeSrc?: s
|
|||||||
|
|
||||||
const nosqlRbacToken = process.env.NOSQL_TESTACCOUNT_TOKEN;
|
const nosqlRbacToken = process.env.NOSQL_TESTACCOUNT_TOKEN;
|
||||||
const nosqlReadOnlyRbacToken = process.env.NOSQL_READONLY_TESTACCOUNT_TOKEN;
|
const nosqlReadOnlyRbacToken = process.env.NOSQL_READONLY_TESTACCOUNT_TOKEN;
|
||||||
|
const nosqlContainerCopyRbacToken = process.env.NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN;
|
||||||
const tableRbacToken = process.env.TABLE_TESTACCOUNT_TOKEN;
|
const tableRbacToken = process.env.TABLE_TESTACCOUNT_TOKEN;
|
||||||
const gremlinRbacToken = process.env.GREMLIN_TESTACCOUNT_TOKEN;
|
const gremlinRbacToken = process.env.GREMLIN_TESTACCOUNT_TOKEN;
|
||||||
const cassandraRbacToken = process.env.CASSANDRA_TESTACCOUNT_TOKEN;
|
const cassandraRbacToken = process.env.CASSANDRA_TESTACCOUNT_TOKEN;
|
||||||
@@ -108,6 +118,16 @@ export async function getTestExplorerUrl(accountType: TestAccount, iframeSrc?: s
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case TestAccount.SQLContainerCopyOnly:
|
||||||
|
if (nosqlContainerCopyRbacToken) {
|
||||||
|
params.set("nosqlRbacToken", nosqlContainerCopyRbacToken);
|
||||||
|
params.set("enableaaddataplane", "true");
|
||||||
|
}
|
||||||
|
if (enablecontainercopy) {
|
||||||
|
params.set("enablecontainercopy", "true");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
case TestAccount.SQLReadOnly:
|
case TestAccount.SQLReadOnly:
|
||||||
if (nosqlReadOnlyRbacToken) {
|
if (nosqlReadOnlyRbacToken) {
|
||||||
params.set("nosqlReadOnlyRbacToken", nosqlReadOnlyRbacToken);
|
params.set("nosqlReadOnlyRbacToken", nosqlReadOnlyRbacToken);
|
||||||
@@ -165,6 +185,39 @@ export async function getTestExplorerUrl(accountType: TestAccount, iframeSrc?: s
|
|||||||
return `https://localhost:1234/testExplorer.html?${params.toString()}`;
|
return `https://localhost:1234/testExplorer.html?${params.toString()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DropdownItemExpectations = {
|
||||||
|
ariaLabel?: string;
|
||||||
|
itemCount?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type DropdownItemMatcher = {
|
||||||
|
name?: string;
|
||||||
|
position?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getDropdownItemByNameOrPosition(
|
||||||
|
frame: Frame,
|
||||||
|
matcher?: DropdownItemMatcher,
|
||||||
|
expectedOptions?: DropdownItemExpectations,
|
||||||
|
): Promise<Locator> {
|
||||||
|
const dropdownItemsWrapper = frame.locator("div.ms-Dropdown-items");
|
||||||
|
if (expectedOptions?.ariaLabel) {
|
||||||
|
expect(await dropdownItemsWrapper.getAttribute("aria-label")).toEqual(expectedOptions.ariaLabel);
|
||||||
|
}
|
||||||
|
if (expectedOptions?.itemCount) {
|
||||||
|
const items = dropdownItemsWrapper.locator("button.ms-Dropdown-item[role='option']");
|
||||||
|
await expect(items).toHaveCount(expectedOptions.itemCount);
|
||||||
|
}
|
||||||
|
const containerDropdownItems = dropdownItemsWrapper.locator("button.ms-Dropdown-item[role='option']");
|
||||||
|
if (matcher?.name) {
|
||||||
|
return containerDropdownItems.filter({ hasText: matcher.name });
|
||||||
|
} else if (matcher?.position !== undefined) {
|
||||||
|
return containerDropdownItems.nth(matcher.position);
|
||||||
|
}
|
||||||
|
// Return first item if no matcher is provided
|
||||||
|
return containerDropdownItems.first();
|
||||||
|
}
|
||||||
|
|
||||||
/** Helper class that provides locator methods for TreeNode elements, on top of a Locator */
|
/** Helper class that provides locator methods for TreeNode elements, on top of a Locator */
|
||||||
class TreeNode {
|
class TreeNode {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -470,17 +523,8 @@ export class DataExplorer {
|
|||||||
return this.frame.getByTestId("notification-console/header-status");
|
return this.frame.getByTestId("notification-console/header-status");
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDropdownItemByName(name: string, ariaLabel?: string): Promise<Locator> {
|
|
||||||
const dropdownItemsWrapper = this.frame.locator("div.ms-Dropdown-items");
|
|
||||||
if (ariaLabel) {
|
|
||||||
expect(await dropdownItemsWrapper.getAttribute("aria-label")).toEqual(ariaLabel);
|
|
||||||
}
|
|
||||||
const containerDropdownItems = dropdownItemsWrapper.locator("button.ms-Dropdown-item[role='option']");
|
|
||||||
return containerDropdownItems.filter({ hasText: name });
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Waits for the Data Explorer app to load */
|
/** Waits for the Data Explorer app to load */
|
||||||
static async waitForExplorer(page: Page) {
|
static async waitForExplorer(page: Page, options?: TestExplorerUrlOptions): Promise<DataExplorer> {
|
||||||
const iframeElement = await page.getByTestId("DataExplorerFrame").elementHandle();
|
const iframeElement = await page.getByTestId("DataExplorerFrame").elementHandle();
|
||||||
if (iframeElement === null) {
|
if (iframeElement === null) {
|
||||||
throw new Error("Explorer iframe not found");
|
throw new Error("Explorer iframe not found");
|
||||||
@@ -492,15 +536,110 @@ export class DataExplorer {
|
|||||||
throw new Error("Explorer frame not found");
|
throw new Error("Explorer frame not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
await explorerFrame?.getByTestId("DataExplorerRoot").waitFor();
|
if (!options?.enablecontainercopy) {
|
||||||
|
await explorerFrame?.getByTestId("DataExplorerRoot").waitFor();
|
||||||
|
}
|
||||||
|
|
||||||
return new DataExplorer(explorerFrame);
|
return new DataExplorer(explorerFrame);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Opens the Data Explorer app using the specified test account (and optionally, the provided IFRAME src url). */
|
/** Opens the Data Explorer app using the specified test account (and optionally, the provided IFRAME src url). */
|
||||||
static async open(page: Page, testAccount: TestAccount, iframeSrc?: string): Promise<DataExplorer> {
|
static async open(page: Page, testAccount: TestAccount, iframeSrc?: string): Promise<DataExplorer> {
|
||||||
const url = await getTestExplorerUrl(testAccount, iframeSrc);
|
const url = await getTestExplorerUrl(testAccount, { iframeSrc });
|
||||||
await page.goto(url);
|
await page.goto(url);
|
||||||
return DataExplorer.waitForExplorer(page);
|
return DataExplorer.waitForExplorer(page);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function waitForApiResponse(
|
||||||
|
page: Page,
|
||||||
|
urlPattern: string,
|
||||||
|
method?: string,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
payloadValidator?: (payload: any) => boolean,
|
||||||
|
) {
|
||||||
|
return page.waitForResponse(async (response) => {
|
||||||
|
const request = response.request();
|
||||||
|
|
||||||
|
if (!request.url().includes(urlPattern)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method && request.method() !== method) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payloadValidator && (request.method() === "POST" || request.method() === "PUT")) {
|
||||||
|
const postData = request.postData();
|
||||||
|
if (postData) {
|
||||||
|
try {
|
||||||
|
const payload = JSON.parse(postData);
|
||||||
|
return payloadValidator(payload);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export async function interceptAndInspectApiRequest(
|
||||||
|
page: Page,
|
||||||
|
urlPattern: string,
|
||||||
|
method: string = "POST",
|
||||||
|
error: Error,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
errorValidator: (url?: string, payload?: any) => boolean,
|
||||||
|
): Promise<void> {
|
||||||
|
await page.route(
|
||||||
|
(url) => url.pathname.includes(urlPattern),
|
||||||
|
async (route, request) => {
|
||||||
|
if (request.method() !== method) {
|
||||||
|
await route.continue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const postData = request.postData();
|
||||||
|
if (postData) {
|
||||||
|
try {
|
||||||
|
const payload = JSON.parse(postData);
|
||||||
|
if (errorValidator && errorValidator(request.url(), payload)) {
|
||||||
|
await route.fulfill({
|
||||||
|
status: 409,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify({
|
||||||
|
code: "Conflict",
|
||||||
|
message: error.message,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Error && err.message.includes("not allowed")) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await route.continue();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ContainerCopy {
|
||||||
|
constructor(
|
||||||
|
public frame: Frame,
|
||||||
|
public wrapper: Locator,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
static async waitForContainerCopy(page: Page): Promise<ContainerCopy> {
|
||||||
|
const explorerFrame = await DataExplorer.waitForExplorer(page, { enablecontainercopy: true });
|
||||||
|
const containerCopyWrapper = explorerFrame.frame.locator("div#containerCopyWrapper");
|
||||||
|
return new ContainerCopy(explorerFrame.frame, containerCopyWrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async open(page: Page, testAccount: TestAccount, iframeSrc?: string): Promise<ContainerCopy> {
|
||||||
|
const url = await getTestExplorerUrl(testAccount, { iframeSrc, enablecontainercopy: true });
|
||||||
|
await page.goto(url);
|
||||||
|
return ContainerCopy.waitForContainerCopy(page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
697
test/sql/containercopy.spec.ts
Normal file
697
test/sql/containercopy.spec.ts
Normal file
@@ -0,0 +1,697 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import { expect, Frame, Locator, Page, test } from "@playwright/test";
|
||||||
|
import { set } from "lodash";
|
||||||
|
import { DatabaseAccount, Subscription } from "../../src/Contracts/DataModels";
|
||||||
|
import { truncateName } from "../../src/Explorer/ContainerCopy/CopyJobUtils";
|
||||||
|
import {
|
||||||
|
ContainerCopy,
|
||||||
|
getAccountName,
|
||||||
|
getDropdownItemByNameOrPosition,
|
||||||
|
interceptAndInspectApiRequest,
|
||||||
|
TestAccount,
|
||||||
|
waitForApiResponse,
|
||||||
|
} from "../fx";
|
||||||
|
|
||||||
|
test.describe.configure({ mode: "serial" });
|
||||||
|
let page: Page;
|
||||||
|
let wrapper: Locator = null!;
|
||||||
|
let panel: Locator = null!;
|
||||||
|
let frame: Frame = null!;
|
||||||
|
let expectedCopyJobNameInitial: string = null!;
|
||||||
|
let expectedSourceSubscription: any = null!;
|
||||||
|
let expectedSourceAccount: DatabaseAccount = null!;
|
||||||
|
let expectedJobName: string = "";
|
||||||
|
let targetAccountName: string = "";
|
||||||
|
let expectedSourceAccountName: string = "";
|
||||||
|
|
||||||
|
test.beforeAll("Container Copy - Before All", async ({ browser }) => {
|
||||||
|
page = await browser.newPage();
|
||||||
|
({ wrapper, frame } = await ContainerCopy.open(page, TestAccount.SQLContainerCopyOnly));
|
||||||
|
expectedJobName = `test_job_${Date.now()}`;
|
||||||
|
targetAccountName = getAccountName(TestAccount.SQLContainerCopyOnly);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterEach("Container Copy - After Each", async () => {
|
||||||
|
await page.unroute(/.*/, (route) => route.continue());
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Loading and verifying the content of the page", async () => {
|
||||||
|
expect(wrapper).not.toBeNull();
|
||||||
|
await expect(wrapper.getByTestId("CommandBar/Button:Create Copy Job")).toBeVisible();
|
||||||
|
await expect(wrapper.getByTestId("CommandBar/Button:Refresh")).toBeVisible();
|
||||||
|
await expect(wrapper.getByTestId("CommandBar/Button:Feedback")).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Opening the Create Copy Job panel", async () => {
|
||||||
|
const createCopyJobButton = wrapper.getByTestId("CommandBar/Button:Create Copy Job");
|
||||||
|
await createCopyJobButton.click();
|
||||||
|
panel = frame.getByTestId("Panel:Create copy job");
|
||||||
|
await expect(panel).toBeVisible();
|
||||||
|
await expect(panel.getByRole("heading", { name: "Create copy job" })).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("select different account dropdown and choose online copy radio button", async () => {
|
||||||
|
const accountDropdown = panel.getByTestId("account-dropdown");
|
||||||
|
await accountDropdown.click();
|
||||||
|
|
||||||
|
const dropdownItemsWrapper = frame.locator("div.ms-Dropdown-items");
|
||||||
|
expect(await dropdownItemsWrapper.getAttribute("aria-label")).toEqual("Account");
|
||||||
|
|
||||||
|
const allDropdownItems = await dropdownItemsWrapper.locator(`button.ms-Dropdown-item[role='option']`).all();
|
||||||
|
|
||||||
|
const filteredItems = [];
|
||||||
|
for (const item of allDropdownItems) {
|
||||||
|
const testContent = (await item.textContent()) ?? "";
|
||||||
|
if (testContent.trim() !== targetAccountName.trim()) {
|
||||||
|
filteredItems.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filteredItems.length > 0) {
|
||||||
|
const firstDropdownItem = filteredItems[0];
|
||||||
|
expectedSourceAccountName = (await firstDropdownItem.textContent()) ?? "";
|
||||||
|
await firstDropdownItem.click();
|
||||||
|
} else {
|
||||||
|
throw new Error("No dropdown items available after filtering");
|
||||||
|
}
|
||||||
|
|
||||||
|
const migrationTypeContainer = panel.getByTestId("migration-type");
|
||||||
|
const onlineCopyRadioButton = migrationTypeContainer.getByRole("radio", { name: /Online mode/i });
|
||||||
|
await onlineCopyRadioButton.click();
|
||||||
|
|
||||||
|
await panel.getByRole("button", { name: "Next" }).click();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Verifying Assign Permissions panel for online copy", async () => {
|
||||||
|
const permissionScreen = panel.getByTestId("Panel:AssignPermissionsContainer");
|
||||||
|
await expect(permissionScreen).toBeVisible();
|
||||||
|
|
||||||
|
await expect(permissionScreen.getByText("Online container copy", { exact: true })).toBeVisible();
|
||||||
|
await expect(permissionScreen.getByText("Cross-account container copy", { exact: true })).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Verify Point-in-Time Restore timer and refresh button workflow", async () => {
|
||||||
|
await page.route(`**/Microsoft.DocumentDB/databaseAccounts/${expectedSourceAccountName}**`, async (route) => {
|
||||||
|
const mockData = {
|
||||||
|
identity: {
|
||||||
|
type: "SystemAssigned",
|
||||||
|
principalId: "00-11-22-33",
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
defaultIdentity: "SystemAssignedIdentity",
|
||||||
|
backupPolicy: {
|
||||||
|
type: "Continuous",
|
||||||
|
},
|
||||||
|
capabilities: [{ name: "EnableOnlineContainerCopy" }],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if (route.request().method() === "GET") {
|
||||||
|
const response = await route.fetch();
|
||||||
|
const actualData = await response.json();
|
||||||
|
const mergedData = { ...actualData };
|
||||||
|
|
||||||
|
set(mergedData, "identity", mockData.identity);
|
||||||
|
set(mergedData, "properties.defaultIdentity", mockData.properties.defaultIdentity);
|
||||||
|
set(mergedData, "properties.backupPolicy", mockData.properties.backupPolicy);
|
||||||
|
set(mergedData, "properties.capabilities", mockData.properties.capabilities);
|
||||||
|
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify(mergedData),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await route.continue();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const permissionScreen = panel.getByTestId("Panel:AssignPermissionsContainer");
|
||||||
|
await expect(permissionScreen).toBeVisible();
|
||||||
|
|
||||||
|
const expandedAccordionHeader = permissionScreen
|
||||||
|
.getByTestId("permission-group-container-onlineConfigs")
|
||||||
|
.locator("button[aria-expanded='true']");
|
||||||
|
await expect(expandedAccordionHeader).toBeVisible();
|
||||||
|
|
||||||
|
const accordionItem = expandedAccordionHeader
|
||||||
|
.locator("xpath=ancestor::*[contains(@class, 'fui-AccordionItem') or contains(@data-test, 'accordion-item')]")
|
||||||
|
.first();
|
||||||
|
|
||||||
|
const accordionPanel = accordionItem.locator("[role='tabpanel'], .fui-AccordionPanel, [data-test*='panel']").first();
|
||||||
|
|
||||||
|
await page.clock.install({ time: new Date("2024-01-01T10:00:00Z") });
|
||||||
|
|
||||||
|
const pitrBtn = accordionPanel.getByTestId("pointInTimeRestore:PrimaryBtn");
|
||||||
|
await expect(pitrBtn).toBeVisible();
|
||||||
|
await pitrBtn.click();
|
||||||
|
|
||||||
|
page.context().on("page", async (newPage) => {
|
||||||
|
const expectedUrlEndPattern = new RegExp(
|
||||||
|
`/providers/Microsoft.(DocumentDB|DocumentDb)/databaseAccounts/${expectedSourceAccountName}/backupRestore`,
|
||||||
|
);
|
||||||
|
expect(newPage.url()).toMatch(expectedUrlEndPattern);
|
||||||
|
await newPage.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
const loadingOverlay = frame.locator("[data-test='loading-overlay']");
|
||||||
|
await expect(loadingOverlay).toBeVisible();
|
||||||
|
|
||||||
|
const refreshBtn = accordionPanel.getByTestId("pointInTimeRestore:RefreshBtn");
|
||||||
|
await expect(refreshBtn).not.toBeVisible();
|
||||||
|
|
||||||
|
// Fast forward time by 11 minutes (11 * 60 * 1000ms = 660000ms)
|
||||||
|
await page.clock.fastForward(11 * 60 * 1000);
|
||||||
|
|
||||||
|
await expect(refreshBtn).toBeVisible();
|
||||||
|
await expect(pitrBtn).not.toBeVisible();
|
||||||
|
|
||||||
|
await refreshBtn.click();
|
||||||
|
await expect(loadingOverlay).toBeVisible();
|
||||||
|
|
||||||
|
await expect(loadingOverlay).toBeHidden({ timeout: 10 * 1000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Veify Popover & Loading Overlay on permission screen with API mocks and accordion interactions", async () => {
|
||||||
|
await page.route(
|
||||||
|
`**/Microsoft.DocumentDB/databaseAccounts/${expectedSourceAccountName}/sqlRoleAssignments*`,
|
||||||
|
async (route) => {
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify({
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
principalId: "00-11-22-33",
|
||||||
|
roleDefinitionId: `Microsoft.DocumentDB/databaseAccounts/${expectedSourceAccountName}/77-88-99`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.route("**/Microsoft.DocumentDB/databaseAccounts/*/77-88-99**", async (route) => {
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify({
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
name: "00000000-0000-0000-0000-000000000001",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.route(`**/Microsoft.DocumentDB/databaseAccounts/${targetAccountName}**`, async (route) => {
|
||||||
|
const mockData = {
|
||||||
|
identity: {
|
||||||
|
type: "SystemAssigned",
|
||||||
|
principalId: "00-11-22-33",
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
defaultIdentity: "SystemAssignedIdentity",
|
||||||
|
backupPolicy: {
|
||||||
|
type: "Continuous",
|
||||||
|
},
|
||||||
|
capabilities: [{ name: "EnableOnlineContainerCopy" }],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (route.request().method() === "PATCH") {
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify({ status: "Succeeded" }),
|
||||||
|
});
|
||||||
|
} else if (route.request().method() === "GET") {
|
||||||
|
// Get the actual response and merge with mock data
|
||||||
|
const response = await route.fetch();
|
||||||
|
const actualData = await response.json();
|
||||||
|
const mergedData = { ...actualData };
|
||||||
|
set(mergedData, "identity", mockData.identity);
|
||||||
|
set(mergedData, "properties.defaultIdentity", mockData.properties.defaultIdentity);
|
||||||
|
set(mergedData, "properties.backupPolicy", mockData.properties.backupPolicy);
|
||||||
|
set(mergedData, "properties.capabilities", mockData.properties.capabilities);
|
||||||
|
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify(mergedData),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await route.continue();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const permissionScreen = panel.getByTestId("Panel:AssignPermissionsContainer");
|
||||||
|
await expect(permissionScreen).toBeVisible();
|
||||||
|
|
||||||
|
const expandedAccordionHeader = permissionScreen
|
||||||
|
.getByTestId("permission-group-container-crossAccountConfigs")
|
||||||
|
.locator("button[aria-expanded='true']");
|
||||||
|
await expect(expandedAccordionHeader).toBeVisible();
|
||||||
|
|
||||||
|
const accordionItem = expandedAccordionHeader
|
||||||
|
.locator("xpath=ancestor::*[contains(@class, 'fui-AccordionItem') or contains(@data-test, 'accordion-item')]")
|
||||||
|
.first();
|
||||||
|
|
||||||
|
const accordionPanel = accordionItem.locator("[role='tabpanel'], .fui-AccordionPanel, [data-test*='panel']").first();
|
||||||
|
|
||||||
|
const toggleButton = accordionPanel.getByTestId("btn-toggle");
|
||||||
|
await expect(toggleButton).toBeVisible();
|
||||||
|
await toggleButton.click();
|
||||||
|
|
||||||
|
const popover = frame.locator("[data-test='popover-container']");
|
||||||
|
await expect(popover).toBeVisible();
|
||||||
|
|
||||||
|
const yesButton = popover.getByRole("button", { name: /Yes/i });
|
||||||
|
const noButton = popover.getByRole("button", { name: /No/i });
|
||||||
|
await expect(yesButton).toBeVisible();
|
||||||
|
await expect(noButton).toBeVisible();
|
||||||
|
|
||||||
|
await yesButton.click();
|
||||||
|
|
||||||
|
const loadingOverlay = frame.locator("[data-test='loading-overlay']");
|
||||||
|
await expect(loadingOverlay).toBeVisible();
|
||||||
|
|
||||||
|
await expect(loadingOverlay).toBeHidden({ timeout: 10 * 1000 });
|
||||||
|
await expect(popover).toBeHidden({ timeout: 10 * 1000 });
|
||||||
|
|
||||||
|
await panel.getByRole("button", { name: "Cancel" }).click();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Loading and verifying subscription & account dropdown", async () => {
|
||||||
|
const createCopyJobButton = wrapper.getByTestId("CommandBar/Button:Create Copy Job");
|
||||||
|
await createCopyJobButton.click();
|
||||||
|
panel = frame.getByTestId("Panel:Create copy job");
|
||||||
|
await expect(panel).toBeVisible();
|
||||||
|
|
||||||
|
const subscriptionPromise = waitForApiResponse(page, "/Microsoft.ResourceGraph/resources", "POST", (payload: any) => {
|
||||||
|
return (
|
||||||
|
payload.query.includes("resources | where type == 'microsoft.documentdb/databaseaccounts'") &&
|
||||||
|
payload.query.includes("| where type == 'microsoft.resources/subscriptions'")
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const accountPromise = waitForApiResponse(page, "/Microsoft.ResourceGraph/resources", "POST", (payload: any) => {
|
||||||
|
return payload.query.includes("resources | where type =~ 'microsoft.documentdb/databaseaccounts'");
|
||||||
|
});
|
||||||
|
|
||||||
|
const subscriptionResponse = await subscriptionPromise;
|
||||||
|
const data = await subscriptionResponse.json();
|
||||||
|
expect(subscriptionResponse.ok()).toBe(true);
|
||||||
|
|
||||||
|
const accountResponse = await accountPromise;
|
||||||
|
const accountData = await accountResponse.json();
|
||||||
|
expect(accountResponse.ok()).toBe(true);
|
||||||
|
|
||||||
|
const selectedSubscription = data.data.find(
|
||||||
|
(item: Subscription) => item.subscriptionId === process.env.DE_TEST_SUBSCRIPTION_ID,
|
||||||
|
);
|
||||||
|
|
||||||
|
const subscriptionDropdown = panel.getByTestId("subscription-dropdown");
|
||||||
|
await expect(subscriptionDropdown).toHaveText(new RegExp(selectedSubscription.subscriptionName));
|
||||||
|
await subscriptionDropdown.click();
|
||||||
|
|
||||||
|
const subscriptionItem = await getDropdownItemByNameOrPosition(
|
||||||
|
frame,
|
||||||
|
{ name: selectedSubscription.subscriptionName },
|
||||||
|
{ ariaLabel: "Subscription", itemCount: data.count },
|
||||||
|
);
|
||||||
|
await subscriptionItem.click();
|
||||||
|
|
||||||
|
const expectedAccountName = getAccountName(TestAccount.SQLContainerCopyOnly);
|
||||||
|
const selectedAccount = accountData.data.find((item: DatabaseAccount) => item.name === expectedAccountName);
|
||||||
|
|
||||||
|
const accountDropdown = panel.getByTestId("account-dropdown");
|
||||||
|
await expect(accountDropdown).toHaveText(new RegExp(expectedAccountName));
|
||||||
|
await accountDropdown.click();
|
||||||
|
|
||||||
|
const accountItem = await getDropdownItemByNameOrPosition(
|
||||||
|
frame,
|
||||||
|
{ name: expectedAccountName },
|
||||||
|
{ ariaLabel: "Account" },
|
||||||
|
);
|
||||||
|
await accountItem.click();
|
||||||
|
|
||||||
|
expectedSourceSubscription = selectedSubscription;
|
||||||
|
expectedSourceAccount = selectedAccount;
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Verifying online or offline content", async () => {
|
||||||
|
/**
|
||||||
|
* This test verifies the functionality of the migration type radio that toggles between
|
||||||
|
* online and offline container copy modes. It ensures that:
|
||||||
|
* 1. When online mode is selected, the user is directed to a permissions screen
|
||||||
|
* 2. When offline mode is selected, the user bypasses the permissions screen
|
||||||
|
* 3. The UI correctly reflects the selected migration type throughout the workflow
|
||||||
|
*/
|
||||||
|
const migrationTypeContainer = panel.getByTestId("migration-type");
|
||||||
|
const onlineCopyRadioButton = migrationTypeContainer.getByRole("radio", { name: /Online mode/i });
|
||||||
|
await onlineCopyRadioButton.click();
|
||||||
|
|
||||||
|
await panel.getByRole("button", { name: "Next" }).click();
|
||||||
|
|
||||||
|
await expect(panel.getByTestId("Panel:AssignPermissionsContainer")).toBeVisible();
|
||||||
|
await expect(panel.getByText("Online container copy", { exact: true })).toBeVisible();
|
||||||
|
await panel.getByRole("button", { name: "Previous" }).click();
|
||||||
|
|
||||||
|
const offlineCopyRadioButton = migrationTypeContainer.getByRole("radio", { name: /Offline mode/i });
|
||||||
|
await offlineCopyRadioButton.click();
|
||||||
|
await panel.getByRole("button", { name: "Next" }).click();
|
||||||
|
|
||||||
|
await expect(panel.getByTestId("Panel:SelectSourceAndTargetContainers")).toBeVisible();
|
||||||
|
await expect(panel.getByTestId("Panel:AssignPermissionsContainer")).not.toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Verifying source and target container selection", async () => {
|
||||||
|
const sourceContainerDropdown = panel.getByTestId("source-containerDropdown");
|
||||||
|
expect(sourceContainerDropdown).toBeVisible();
|
||||||
|
await expect(sourceContainerDropdown).toHaveClass(/(^|\s)is-disabled(\s|$)/);
|
||||||
|
|
||||||
|
const sourceDatabaseDropdown = panel.getByTestId("source-databaseDropdown");
|
||||||
|
await sourceDatabaseDropdown.click();
|
||||||
|
|
||||||
|
const sourceDbDropdownItem = await getDropdownItemByNameOrPosition(frame, { position: 0 }, { ariaLabel: "Database" });
|
||||||
|
await sourceDbDropdownItem.click();
|
||||||
|
|
||||||
|
await expect(sourceContainerDropdown).not.toHaveClass(/(^|\s)is-disabled(\s|$)/);
|
||||||
|
await sourceContainerDropdown.click();
|
||||||
|
const sourceContainerDropdownItem = await getDropdownItemByNameOrPosition(
|
||||||
|
frame,
|
||||||
|
{ position: 0 },
|
||||||
|
{ ariaLabel: "Container" },
|
||||||
|
);
|
||||||
|
await sourceContainerDropdownItem.click();
|
||||||
|
|
||||||
|
const targetContainerDropdown = panel.getByTestId("target-containerDropdown");
|
||||||
|
expect(targetContainerDropdown).toBeVisible();
|
||||||
|
await expect(targetContainerDropdown).toHaveClass(/(^|\s)is-disabled(\s|$)/);
|
||||||
|
|
||||||
|
const targetDatabaseDropdown = panel.getByTestId("target-databaseDropdown");
|
||||||
|
await targetDatabaseDropdown.click();
|
||||||
|
const targetDbDropdownItem = await getDropdownItemByNameOrPosition(frame, { position: 0 }, { ariaLabel: "Database" });
|
||||||
|
await targetDbDropdownItem.click();
|
||||||
|
|
||||||
|
await expect(targetContainerDropdown).not.toHaveClass(/(^|\s)is-disabled(\s|$)/);
|
||||||
|
await targetContainerDropdown.click();
|
||||||
|
const targetContainerDropdownItem1 = await getDropdownItemByNameOrPosition(
|
||||||
|
frame,
|
||||||
|
{ position: 0 },
|
||||||
|
{ ariaLabel: "Container" },
|
||||||
|
);
|
||||||
|
await targetContainerDropdownItem1.click();
|
||||||
|
|
||||||
|
await panel.getByRole("button", { name: "Next" }).click();
|
||||||
|
|
||||||
|
const errorContainer = panel.getByTestId("Panel:ErrorContainer");
|
||||||
|
await expect(errorContainer).toBeVisible();
|
||||||
|
await expect(errorContainer).toHaveText(/Source and destination containers cannot be the same/i);
|
||||||
|
|
||||||
|
// Reselect target container to be different from source container
|
||||||
|
await targetContainerDropdown.click();
|
||||||
|
const targetContainerDropdownItem2 = await getDropdownItemByNameOrPosition(
|
||||||
|
frame,
|
||||||
|
{ position: 1 },
|
||||||
|
{ ariaLabel: "Container" },
|
||||||
|
);
|
||||||
|
await targetContainerDropdownItem2.click();
|
||||||
|
|
||||||
|
const selectedSourceDatabase = await sourceDatabaseDropdown.innerText();
|
||||||
|
const selectedSourceContainer = await sourceContainerDropdown.innerText();
|
||||||
|
const selectedTargetDatabase = await targetDatabaseDropdown.innerText();
|
||||||
|
const selectedTargetContainer = await targetContainerDropdown.innerText();
|
||||||
|
expectedCopyJobNameInitial = `${truncateName(selectedSourceDatabase)}.${truncateName(
|
||||||
|
selectedSourceContainer,
|
||||||
|
)}_${truncateName(selectedTargetDatabase)}.${truncateName(selectedTargetContainer)}`;
|
||||||
|
|
||||||
|
await panel.getByRole("button", { name: "Next" }).click();
|
||||||
|
|
||||||
|
await expect(errorContainer).not.toBeVisible();
|
||||||
|
await expect(panel.getByTestId("Panel:PreviewCopyJob")).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Verifying the preview of the copy job", async () => {
|
||||||
|
const previewContainer = panel.getByTestId("Panel:PreviewCopyJob");
|
||||||
|
await expect(previewContainer).toBeVisible();
|
||||||
|
await expect(previewContainer.getByTestId("source-subscription-name")).toHaveText(
|
||||||
|
expectedSourceSubscription.subscriptionName,
|
||||||
|
);
|
||||||
|
await expect(previewContainer.getByTestId("source-account-name")).toHaveText(expectedSourceAccount.name);
|
||||||
|
const jobNameInput = previewContainer.getByTestId("job-name-textfield");
|
||||||
|
await expect(jobNameInput).toHaveValue(new RegExp(expectedCopyJobNameInitial));
|
||||||
|
const primaryBtn = panel.getByRole("button", { name: "Copy", exact: true });
|
||||||
|
await expect(primaryBtn).not.toHaveClass(/(^|\s)is-disabled(\s|$)/);
|
||||||
|
|
||||||
|
await jobNameInput.fill("test job name");
|
||||||
|
await expect(primaryBtn).toHaveClass(/(^|\s)is-disabled(\s|$)/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Testing API request interception with duplicate job name", async () => {
|
||||||
|
const previewContainer = panel.getByTestId("Panel:PreviewCopyJob");
|
||||||
|
const jobNameInput = previewContainer.getByTestId("job-name-textfield");
|
||||||
|
const duplicateJobName = "test-job-name-1";
|
||||||
|
await jobNameInput.fill(duplicateJobName);
|
||||||
|
|
||||||
|
const copyButton = panel.getByRole("button", { name: "Copy", exact: true });
|
||||||
|
const expectedErrorMessage = `Duplicate job name '${duplicateJobName}'`;
|
||||||
|
await interceptAndInspectApiRequest(
|
||||||
|
page,
|
||||||
|
`${expectedSourceAccount.name}/dataTransferJobs/${duplicateJobName}`,
|
||||||
|
"PUT",
|
||||||
|
new Error(expectedErrorMessage),
|
||||||
|
(url?: string) => url?.includes(duplicateJobName) ?? false,
|
||||||
|
);
|
||||||
|
|
||||||
|
let errorThrown = false;
|
||||||
|
try {
|
||||||
|
await copyButton.click();
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
} catch (error: any) {
|
||||||
|
errorThrown = true;
|
||||||
|
expect(error.message).toContain("not allowed");
|
||||||
|
}
|
||||||
|
if (!errorThrown) {
|
||||||
|
const errorContainer = panel.getByTestId("Panel:ErrorContainer");
|
||||||
|
await expect(errorContainer).toBeVisible();
|
||||||
|
await expect(errorContainer).toHaveText(new RegExp(expectedErrorMessage, "i"));
|
||||||
|
}
|
||||||
|
|
||||||
|
await expect(panel).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Testing API request success with valid job name and verifying copy job creation", async () => {
|
||||||
|
const previewContainer = panel.getByTestId("Panel:PreviewCopyJob");
|
||||||
|
const jobNameInput = previewContainer.getByTestId("job-name-textfield");
|
||||||
|
const copyButton = panel.getByRole("button", { name: "Copy", exact: true });
|
||||||
|
|
||||||
|
const validJobName = expectedJobName;
|
||||||
|
|
||||||
|
const copyJobCreationPromise = waitForApiResponse(
|
||||||
|
page,
|
||||||
|
`${expectedSourceAccount.name}/dataTransferJobs/${validJobName}`,
|
||||||
|
"PUT",
|
||||||
|
);
|
||||||
|
|
||||||
|
await jobNameInput.fill(validJobName);
|
||||||
|
await expect(copyButton).not.toHaveClass(/(^|\s)is-disabled(\s|$)/);
|
||||||
|
|
||||||
|
await copyButton.click();
|
||||||
|
|
||||||
|
const response = await copyJobCreationPromise;
|
||||||
|
expect(response.ok()).toBe(true);
|
||||||
|
|
||||||
|
await expect(panel).not.toBeVisible({ timeout: 10000 });
|
||||||
|
|
||||||
|
const jobsListContainer = wrapper.locator(".CopyJobListContainer .ms-DetailsList-contentWrapper .ms-List-page");
|
||||||
|
await jobsListContainer.waitFor({ state: "visible" });
|
||||||
|
|
||||||
|
const jobItem = jobsListContainer.getByText(validJobName);
|
||||||
|
await jobItem.waitFor({ state: "visible" });
|
||||||
|
await expect(jobItem).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.skip("Pause a running copy job", async () => {
|
||||||
|
const jobsListContainer = wrapper.locator(".CopyJobListContainer .ms-DetailsList-contentWrapper .ms-List-page");
|
||||||
|
await jobsListContainer.waitFor({ state: "visible" });
|
||||||
|
|
||||||
|
const firstJobRow = jobsListContainer.locator(".ms-DetailsRow", { hasText: expectedJobName });
|
||||||
|
await firstJobRow.waitFor({ state: "visible" });
|
||||||
|
|
||||||
|
const actionMenuButton = wrapper.getByTestId(`CopyJobActionMenu/Button:${expectedJobName}`);
|
||||||
|
await actionMenuButton.waitFor({ state: "visible" });
|
||||||
|
await actionMenuButton.click();
|
||||||
|
|
||||||
|
const pauseAction = frame.locator(".ms-ContextualMenu-list button:has-text('Pause')");
|
||||||
|
await pauseAction.waitFor({ state: "visible" });
|
||||||
|
await pauseAction.click();
|
||||||
|
|
||||||
|
const updatedJobRow = jobsListContainer.locator(".ms-DetailsRow").filter({ hasText: expectedJobName });
|
||||||
|
const statusCell = updatedJobRow.locator("[data-automationid='DetailsRowCell'][data-automation-key='CopyJobStatus']");
|
||||||
|
await expect(statusCell).toContainText(/paused/i, { timeout: 10000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test.skip("Resume a paused copy job", async () => {
|
||||||
|
const jobsListContainer = wrapper.locator(".CopyJobListContainer .ms-DetailsList-contentWrapper .ms-List-page");
|
||||||
|
await jobsListContainer.waitFor({ state: "visible" });
|
||||||
|
|
||||||
|
const pausedJobRow = jobsListContainer.locator(".ms-DetailsRow", { hasText: expectedJobName });
|
||||||
|
await pausedJobRow.waitFor({ state: "visible" });
|
||||||
|
|
||||||
|
const statusCell = pausedJobRow.locator("[data-automationid='DetailsRowCell'][data-automation-key='CopyJobStatus']");
|
||||||
|
await expect(statusCell).toContainText(/paused/i);
|
||||||
|
|
||||||
|
const actionMenuButton = wrapper.getByTestId(`CopyJobActionMenu/Button:${expectedJobName}`);
|
||||||
|
await actionMenuButton.waitFor({ state: "visible" });
|
||||||
|
await actionMenuButton.click();
|
||||||
|
|
||||||
|
const resumeAction = frame.locator(".ms-ContextualMenu-list button:has-text('Resume')");
|
||||||
|
await resumeAction.waitFor({ state: "visible" });
|
||||||
|
await resumeAction.click();
|
||||||
|
|
||||||
|
await expect(statusCell).toContainText(/running|queued/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Cancel a copy job", async () => {
|
||||||
|
// Create a new job specifically for cancellation testing
|
||||||
|
const cancelJobName = `cancel_test_job_${Date.now()}`;
|
||||||
|
|
||||||
|
// Navigate to create job panel
|
||||||
|
const createCopyJobButton = wrapper.getByTestId("CommandBar/Button:Create Copy Job");
|
||||||
|
await createCopyJobButton.click();
|
||||||
|
panel = frame.getByTestId("Panel:Create copy job");
|
||||||
|
|
||||||
|
// Skip to container selection (offline mode for faster creation)
|
||||||
|
await panel.getByRole("button", { name: "Next" }).click();
|
||||||
|
|
||||||
|
// Select source containers quickly
|
||||||
|
const sourceDatabaseDropdown = panel.getByTestId("source-databaseDropdown");
|
||||||
|
await sourceDatabaseDropdown.click();
|
||||||
|
const sourceDatabaseDropdownItem = await getDropdownItemByNameOrPosition(
|
||||||
|
frame,
|
||||||
|
{ position: 0 },
|
||||||
|
{ ariaLabel: "Database" },
|
||||||
|
);
|
||||||
|
await sourceDatabaseDropdownItem.click();
|
||||||
|
|
||||||
|
const sourceContainerDropdown = panel.getByTestId("source-containerDropdown");
|
||||||
|
await sourceContainerDropdown.click();
|
||||||
|
const sourceContainerDropdownItem = await getDropdownItemByNameOrPosition(
|
||||||
|
frame,
|
||||||
|
{ position: 0 },
|
||||||
|
{ ariaLabel: "Container" },
|
||||||
|
);
|
||||||
|
await sourceContainerDropdownItem.click();
|
||||||
|
|
||||||
|
// Select target containers
|
||||||
|
const targetDatabaseDropdown = panel.getByTestId("target-databaseDropdown");
|
||||||
|
await targetDatabaseDropdown.click();
|
||||||
|
const targetDatabaseDropdownItem = await getDropdownItemByNameOrPosition(
|
||||||
|
frame,
|
||||||
|
{ position: 0 },
|
||||||
|
{ ariaLabel: "Database" },
|
||||||
|
);
|
||||||
|
await targetDatabaseDropdownItem.click();
|
||||||
|
|
||||||
|
const targetContainerDropdown = panel.getByTestId("target-containerDropdown");
|
||||||
|
await targetContainerDropdown.click();
|
||||||
|
const targetContainerDropdownItem = await getDropdownItemByNameOrPosition(
|
||||||
|
frame,
|
||||||
|
{ position: 1 },
|
||||||
|
{ ariaLabel: "Container" },
|
||||||
|
);
|
||||||
|
await targetContainerDropdownItem.click();
|
||||||
|
|
||||||
|
await panel.getByRole("button", { name: "Next" }).click();
|
||||||
|
|
||||||
|
// Set job name and create
|
||||||
|
const jobNameInput = panel.getByTestId("job-name-textfield");
|
||||||
|
await jobNameInput.fill(cancelJobName);
|
||||||
|
|
||||||
|
const copyButton = panel.getByRole("button", { name: "Copy", exact: true });
|
||||||
|
|
||||||
|
// Create job and immediately start polling for it
|
||||||
|
await copyButton.click();
|
||||||
|
|
||||||
|
// Wait for panel to close and job list to refresh
|
||||||
|
await expect(panel).not.toBeVisible({ timeout: 10000 });
|
||||||
|
|
||||||
|
const jobsListContainer = wrapper.locator(".CopyJobListContainer .ms-DetailsList-contentWrapper .ms-List-page");
|
||||||
|
await jobsListContainer.waitFor({ state: "visible" });
|
||||||
|
|
||||||
|
// Rapid polling to catch the job in running state
|
||||||
|
let attempts = 0;
|
||||||
|
const maxAttempts = 50; // Try for ~5 seconds
|
||||||
|
let jobCancelled = false;
|
||||||
|
|
||||||
|
while (attempts < maxAttempts && !jobCancelled) {
|
||||||
|
try {
|
||||||
|
// Look for the job row
|
||||||
|
const jobRow = jobsListContainer.locator(".ms-DetailsRow", { hasText: cancelJobName });
|
||||||
|
|
||||||
|
if (await jobRow.isVisible({ timeout: 100 })) {
|
||||||
|
const statusCell = jobRow.locator("[data-automationid='DetailsRowCell'][data-automation-key='CopyJobStatus']");
|
||||||
|
const statusText = await statusCell.textContent({ timeout: 100 });
|
||||||
|
|
||||||
|
// If job is still running/queued, try to cancel it
|
||||||
|
if (statusText && /running|queued|pending/i.test(statusText)) {
|
||||||
|
const actionMenuButton = wrapper.getByTestId(`CopyJobActionMenu/Button:${cancelJobName}`);
|
||||||
|
await actionMenuButton.click({ timeout: 1000 });
|
||||||
|
|
||||||
|
const cancelAction = frame.locator(".ms-ContextualMenu-list button:has-text('Cancel')");
|
||||||
|
if (await cancelAction.isVisible({ timeout: 1000 })) {
|
||||||
|
await cancelAction.click();
|
||||||
|
await expect(frame.locator(".ms-Dialog-main")).toBeVisible({ timeout: 1000 });
|
||||||
|
await expect(frame.locator(".ms-Dialog-main")).toContainText(cancelJobName);
|
||||||
|
|
||||||
|
// Click cancel button on dialog to close it first
|
||||||
|
const cancelDialogButton = frame.locator(".ms-Dialog-main").getByTestId("DialogButton:Cancel");
|
||||||
|
await expect(cancelDialogButton).toBeVisible();
|
||||||
|
await cancelDialogButton.click();
|
||||||
|
|
||||||
|
// Click confirm button
|
||||||
|
await actionMenuButton.click({ timeout: 1000 });
|
||||||
|
await cancelAction.click();
|
||||||
|
const confirmDialogButton = frame.locator(".ms-Dialog-main").getByTestId("DialogButton:Confirm");
|
||||||
|
await confirmDialogButton.click();
|
||||||
|
|
||||||
|
// Verify cancellation
|
||||||
|
await expect(statusCell).toContainText(/cancelled|canceled|failed/i, { timeout: 5000 });
|
||||||
|
jobCancelled = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (statusText && /completed|succeeded|finished/i.test(statusText)) {
|
||||||
|
// Job completed too fast, skip the test
|
||||||
|
// console.log(`Job ${cancelJobName} completed too quickly to test cancellation`);
|
||||||
|
test.skip(true, "Job completed too quickly for cancellation test");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh the job list
|
||||||
|
const refreshButton = wrapper.getByTestId("CommandBar/Button:Refresh");
|
||||||
|
if (await refreshButton.isVisible({ timeout: 100 })) {
|
||||||
|
await refreshButton.click();
|
||||||
|
await page.waitForTimeout(100); // Small delay between attempts
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Continue trying if there's any error
|
||||||
|
}
|
||||||
|
|
||||||
|
attempts++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!jobCancelled) {
|
||||||
|
// If we couldn't cancel in time, at least verify the job was created
|
||||||
|
const jobRow = jobsListContainer.locator(".ms-DetailsRow", { hasText: cancelJobName });
|
||||||
|
await expect(jobRow).toBeVisible({ timeout: 5000 });
|
||||||
|
test.skip(true, "Could not catch job in running state for cancellation test");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterAll("Container Copy - After All", async () => {
|
||||||
|
await page.unroute(/.*/, (route) => route.continue());
|
||||||
|
await page.close();
|
||||||
|
});
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { expect, Page, test } from "@playwright/test";
|
import { expect, Page, test } from "@playwright/test";
|
||||||
import { DataExplorer, TestAccount } from "../../fx";
|
import { DataExplorer, getDropdownItemByNameOrPosition, TestAccount } from "../../fx";
|
||||||
import { createTestSQLContainer, TestContainerContext } from "../../testData";
|
import { createTestSQLContainer, TestContainerContext } from "../../testData";
|
||||||
|
|
||||||
test.describe("Change Partition Key", () => {
|
test.describe("Change Partition Key", () => {
|
||||||
@@ -81,7 +81,11 @@ test.describe("Change Partition Key", () => {
|
|||||||
await changePkPanel.getByLabel("Use existing container").check();
|
await changePkPanel.getByLabel("Use existing container").check();
|
||||||
await changePkPanel.getByText("Choose an existing container").click();
|
await changePkPanel.getByText("Choose an existing container").click();
|
||||||
|
|
||||||
const containerDropdownItem = await explorer.getDropdownItemByName(newContainerId, "Existing Containers");
|
const containerDropdownItem = await getDropdownItemByNameOrPosition(
|
||||||
|
explorer.frame,
|
||||||
|
{ name: newContainerId },
|
||||||
|
{ ariaLabel: "Existing Containers" },
|
||||||
|
);
|
||||||
await containerDropdownItem.click();
|
await containerDropdownItem.click();
|
||||||
|
|
||||||
await changePkPanel.getByTestId("Panel/OkButton").click();
|
await changePkPanel.getByTestId("Panel/OkButton").click();
|
||||||
|
|||||||
@@ -11,8 +11,12 @@ const accountName = urlSearchParams.get("accountName") || "portal-sql-runner-wes
|
|||||||
const selfServeType = urlSearchParams.get("selfServeType") || "example";
|
const selfServeType = urlSearchParams.get("selfServeType") || "example";
|
||||||
const iframeSrc = urlSearchParams.get("iframeSrc") || "explorer.html?platform=Portal&disablePortalInitCache";
|
const iframeSrc = urlSearchParams.get("iframeSrc") || "explorer.html?platform=Portal&disablePortalInitCache";
|
||||||
const authToken = urlSearchParams.get("token");
|
const authToken = urlSearchParams.get("token");
|
||||||
|
const enablecontainercopy = urlSearchParams.get("enablecontainercopy");
|
||||||
|
|
||||||
const nosqlRbacToken = urlSearchParams.get("nosqlRbacToken") || process.env.NOSQL_TESTACCOUNT_TOKEN || "";
|
const nosqlRbacToken =
|
||||||
|
urlSearchParams.get("nosqlRbacToken") ||
|
||||||
|
(enablecontainercopy ? process.env.NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN : process.env.NOSQL_TESTACCOUNT_TOKEN) ||
|
||||||
|
"";
|
||||||
const nosqlReadOnlyRbacToken =
|
const nosqlReadOnlyRbacToken =
|
||||||
urlSearchParams.get("nosqlReadOnlyRbacToken") || process.env.NOSQL_READONLY_TESTACCOUNT_TOKEN || "";
|
urlSearchParams.get("nosqlReadOnlyRbacToken") || process.env.NOSQL_READONLY_TESTACCOUNT_TOKEN || "";
|
||||||
const tableRbacToken = urlSearchParams.get("tableRbacToken") || process.env.TABLE_TESTACCOUNT_TOKEN || "";
|
const tableRbacToken = urlSearchParams.get("tableRbacToken") || process.env.TABLE_TESTACCOUNT_TOKEN || "";
|
||||||
@@ -83,6 +87,7 @@ const initTestExplorer = async (): Promise<void> => {
|
|||||||
authorizationToken: `Bearer ${authToken}`,
|
authorizationToken: `Bearer ${authToken}`,
|
||||||
aadToken: rbacToken,
|
aadToken: rbacToken,
|
||||||
features: {},
|
features: {},
|
||||||
|
containerCopyEnabled: enablecontainercopy === "true",
|
||||||
hasWriteAccess: true,
|
hasWriteAccess: true,
|
||||||
csmEndpoint: "https://management.azure.com",
|
csmEndpoint: "https://management.azure.com",
|
||||||
dnsSuffix: "documents.azure.com",
|
dnsSuffix: "documents.azure.com",
|
||||||
|
|||||||
Reference in New Issue
Block a user