Refactor Container Copy dropdowns with integrated state management (#2279)

This commit is contained in:
BChoudhury-ms
2025-12-15 12:25:05 +05:30
committed by GitHub
parent d67c1a0464
commit bc7e8a71ca
22 changed files with 955 additions and 2707 deletions

View File

@@ -433,7 +433,7 @@ describe("CopyJobActions", () => {
(dataTransferService.listByDatabaseAccount as jest.Mock).mockRejectedValue(abortError); (dataTransferService.listByDatabaseAccount as jest.Mock).mockRejectedValue(abortError);
await expect(getCopyJobs()).rejects.toMatchObject({ await expect(getCopyJobs()).rejects.toMatchObject({
message: expect.stringContaining("Please wait for the current fetch request to complete"), message: expect.stringContaining("Previous copy job request was cancelled."),
}); });
}); });

View File

@@ -124,8 +124,7 @@ export const getCopyJobs = async (): Promise<CopyJobType[]> => {
const errorContent = JSON.stringify(error.content || error.message || error); const errorContent = JSON.stringify(error.content || error.message || error);
if (errorContent.includes("signal is aborted without reason")) { if (errorContent.includes("signal is aborted without reason")) {
throw { throw {
message: message: "Previous copy job request was cancelled.",
"Please wait for the current fetch request to complete. The previous copy job fetch request was aborted.",
}; };
} else { } else {
throw error; throw error;

View File

@@ -162,10 +162,10 @@ export default {
viewDetails: "View Details", viewDetails: "View Details",
}, },
Status: { Status: {
Pending: "Pending", Pending: "Queued",
InProgress: "In Progress", InProgress: "Running",
Running: "In Progress", Running: "Running",
Partitioning: "In Progress", Partitioning: "Running",
Paused: "Paused", Paused: "Paused",
Completed: "Completed", Completed: "Completed",
Failed: "Failed", Failed: "Failed",

View File

@@ -59,15 +59,8 @@ describe("CopyJobContext", () => {
jobName: "", jobName: "",
migrationType: CopyJobMigrationType.Offline, migrationType: CopyJobMigrationType.Offline,
source: { source: {
subscription: { subscription: null,
subscriptionId: "test-subscription-id", account: null,
},
account: {
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/test-account",
name: "test-account",
location: "East US",
kind: "GlobalDocumentDB",
},
databaseId: "", databaseId: "",
containerId: "", containerId: "",
}, },
@@ -605,8 +598,8 @@ describe("CopyJobContext", () => {
</CopyJobContextProvider>, </CopyJobContextProvider>,
); );
expect(contextValue.copyJobState.source.subscription.subscriptionId).toBe("test-subscription-id"); expect(contextValue.copyJobState.source?.subscription?.subscriptionId).toBeUndefined();
expect(contextValue.copyJobState.source.account.name).toBe("test-account"); expect(contextValue.copyJobState.source?.account?.name).toBeUndefined();
}); });
it("should initialize target with userContext values", () => { it("should initialize target with userContext values", () => {

View File

@@ -1,5 +1,4 @@
import Explorer from "Explorer/Explorer"; import Explorer from "Explorer/Explorer";
import { Subscription } from "Contracts/DataModels";
import React from "react"; import React from "react";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import { CopyJobMigrationType } from "../Enums/CopyJobEnums"; import { CopyJobMigrationType } from "../Enums/CopyJobEnums";
@@ -24,10 +23,8 @@ const getInitialCopyJobState = (): CopyJobContextState => {
jobName: "", jobName: "",
migrationType: CopyJobMigrationType.Offline, migrationType: CopyJobMigrationType.Offline,
source: { source: {
subscription: { subscription: null,
subscriptionId: userContext.subscriptionId || "", account: null,
} as Subscription,
account: userContext.databaseAccount || null,
databaseId: "", databaseId: "",
containerId: "", containerId: "",
}, },

View File

@@ -147,7 +147,7 @@ export function isEqual(prevJobs: CopyJobType[], newJobs: CopyJobType[]): boolea
} }
const truncateLength = 5; const truncateLength = 5;
const truncateName = (name: string, length: number = truncateLength): string => { export const truncateName = (name: string, length: number = truncateLength): string => {
return name.length <= length ? name : name.slice(0, length); return name.length <= length ? name : name.slice(0, length);
}; };

View File

@@ -1,219 +1,409 @@
import "@testing-library/jest-dom"; import "@testing-library/jest-dom";
import { render } from "@testing-library/react"; import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import React from "react"; import React from "react";
import { DropdownOptionType } from "../../../../Types/CopyJobTypes"; import { configContext, Platform } from "../../../../../../ConfigContext";
import { DatabaseAccount } from "../../../../../../Contracts/DataModels";
import * as useDatabaseAccountsHook from "../../../../../../hooks/useDatabaseAccounts";
import { apiType, userContext } from "../../../../../../UserContext";
import ContainerCopyMessages from "../../../../ContainerCopyMessages";
import { CopyJobContext } from "../../../../Context/CopyJobContext";
import { CopyJobMigrationType } from "../../../../Enums/CopyJobEnums";
import { CopyJobContextProviderType, CopyJobContextState } from "../../../../Types/CopyJobTypes";
import { AccountDropdown } from "./AccountDropdown"; import { AccountDropdown } from "./AccountDropdown";
describe("AccountDropdown", () => { jest.mock("../../../../../../hooks/useDatabaseAccounts");
const mockOnChange = jest.fn(); jest.mock("../../../../../../UserContext", () => ({
userContext: {
databaseAccount: null as DatabaseAccount | null,
},
apiType: jest.fn(),
}));
jest.mock("../../../../../../ConfigContext", () => ({
configContext: {
platform: "Portal",
},
Platform: {
Portal: "Portal",
Hosted: "Hosted",
},
}));
const mockAccountOptions: DropdownOptionType[] = [ const mockUseDatabaseAccounts = useDatabaseAccountsHook.useDatabaseAccounts as jest.MockedFunction<
{ typeof useDatabaseAccountsHook.useDatabaseAccounts
key: "account-1", >;
text: "Development Account",
data: { describe("AccountDropdown", () => {
id: "account-1", const mockSetCopyJobState = jest.fn();
name: "Development Account", const mockCopyJobState = {
jobName: "",
migrationType: CopyJobMigrationType.Offline,
source: {
subscription: {
subscriptionId: "test-subscription-id",
displayName: "Test Subscription",
},
account: null,
databaseId: "",
containerId: "",
},
target: {
subscriptionId: "",
account: null,
databaseId: "",
containerId: "",
},
sourceReadAccessFromTarget: false,
} as CopyJobContextState;
const mockCopyJobContextValue = {
copyJobState: mockCopyJobState,
setCopyJobState: mockSetCopyJobState,
flow: null,
setFlow: jest.fn(),
contextError: null,
setContextError: jest.fn(),
resetCopyJobState: jest.fn(),
} as CopyJobContextProviderType;
const mockDatabaseAccount1: DatabaseAccount = {
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/account1",
name: "test-account-1",
kind: "GlobalDocumentDB",
location: "East US", location: "East US",
resourceGroup: "dev-rg", type: "Microsoft.DocumentDB/databaseAccounts",
kind: "GlobalDocumentDB", tags: {},
properties: { properties: {
documentEndpoint: "https://dev-account.documents.azure.com:443/", documentEndpoint: "https://account1.documents.azure.com:443/",
provisioningState: "Succeeded", capabilities: [],
consistencyPolicy: { enableMultipleWriteLocations: false,
defaultConsistencyLevel: "Session",
}, },
}, };
},
}, const mockDatabaseAccount2: DatabaseAccount = {
{ id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/account2",
key: "account-2", name: "test-account-2",
text: "Production Account",
data: {
id: "account-2",
name: "Production Account",
location: "West US 2",
resourceGroup: "prod-rg",
kind: "GlobalDocumentDB", kind: "GlobalDocumentDB",
location: "West US",
type: "Microsoft.DocumentDB/databaseAccounts",
tags: {},
properties: { properties: {
documentEndpoint: "https://prod-account.documents.azure.com:443/", documentEndpoint: "https://account2.documents.azure.com:443/",
provisioningState: "Succeeded", capabilities: [],
consistencyPolicy: { enableMultipleWriteLocations: false,
defaultConsistencyLevel: "Strong",
}, },
}, };
},
}, const mockNonSqlAccount: DatabaseAccount = {
{ id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/mongo-account",
key: "account-3", name: "mongo-account",
text: "Testing Account", kind: "MongoDB",
data: {
id: "account-3",
name: "Testing Account",
location: "Central US", location: "Central US",
resourceGroup: "test-rg", type: "Microsoft.DocumentDB/databaseAccounts",
kind: "GlobalDocumentDB", tags: {},
properties: { properties: {
documentEndpoint: "https://test-account.documents.azure.com:443/", documentEndpoint: "https://mongo-account.documents.azure.com:443/",
provisioningState: "Succeeded", capabilities: [],
consistencyPolicy: { enableMultipleWriteLocations: false,
defaultConsistencyLevel: "Eventual",
}, },
}, };
},
}, const renderWithContext = (contextValue = mockCopyJobContextValue) => {
]; return render(
<CopyJobContext.Provider value={contextValue}>
<AccountDropdown />
</CopyJobContext.Provider>,
);
};
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
(apiType as jest.MockedFunction<any>).mockImplementation((account: DatabaseAccount) => {
return account.kind === "MongoDB" ? "MongoDB" : "SQL";
});
}); });
describe("Snapshot Testing", () => { describe("Rendering", () => {
it("matches snapshot with all account options", () => { it("should render dropdown with correct label and placeholder", () => {
const { container } = render( mockUseDatabaseAccounts.mockReturnValue([]);
<AccountDropdown options={mockAccountOptions} disabled={false} onChange={mockOnChange} />,
renderWithContext();
expect(
screen.getByText(`${ContainerCopyMessages.sourceAccountDropdownLabel}:`, { exact: true }),
).toBeInTheDocument();
expect(screen.getByRole("combobox")).toHaveAttribute(
"aria-label",
ContainerCopyMessages.sourceAccountDropdownLabel,
); );
expect(container.firstChild).toMatchSnapshot();
}); });
it("matches snapshot with selected account", () => { it("should render disabled dropdown when no subscription is selected", () => {
const { container } = render( mockUseDatabaseAccounts.mockReturnValue([]);
<AccountDropdown const contextWithoutSubscription = {
options={mockAccountOptions} ...mockCopyJobContextValue,
selectedKey="account-2" copyJobState: {
disabled={false} ...mockCopyJobState,
onChange={mockOnChange} source: {
/>, ...mockCopyJobState.source,
); subscription: null,
},
} as CopyJobContextState,
};
expect(container.firstChild).toMatchSnapshot(); renderWithContext(contextWithoutSubscription);
const dropdown = screen.getByRole("combobox");
expect(dropdown).toHaveAttribute("aria-disabled", "true");
}); });
it("matches snapshot with disabled dropdown", () => { it("should render disabled dropdown when no accounts are available", () => {
const { container } = render( mockUseDatabaseAccounts.mockReturnValue([]);
<AccountDropdown
options={mockAccountOptions}
selectedKey="account-1"
disabled={true}
onChange={mockOnChange}
/>,
);
expect(container.firstChild).toMatchSnapshot(); renderWithContext();
const dropdown = screen.getByRole("combobox");
expect(dropdown).toHaveAttribute("aria-disabled", "true");
}); });
it("matches snapshot with empty options", () => { it("should render enabled dropdown when accounts are available", () => {
const { container } = render(<AccountDropdown options={[]} disabled={false} onChange={mockOnChange} />); mockUseDatabaseAccounts.mockReturnValue([mockDatabaseAccount1, mockDatabaseAccount2]);
expect(container.firstChild).toMatchSnapshot(); renderWithContext();
const dropdown = screen.getByRole("combobox");
expect(dropdown).toHaveAttribute("aria-disabled", "false");
});
}); });
it("matches snapshot with single option", () => { describe("Account filtering", () => {
const { container } = render( it("should filter accounts to only show SQL API accounts", () => {
<AccountDropdown const allAccounts = [mockDatabaseAccount1, mockDatabaseAccount2, mockNonSqlAccount];
options={[mockAccountOptions[0]]} mockUseDatabaseAccounts.mockReturnValue(allAccounts);
selectedKey="account-1"
disabled={false}
onChange={mockOnChange}
/>,
);
expect(container.firstChild).toMatchSnapshot(); renderWithContext();
expect(mockUseDatabaseAccounts).toHaveBeenCalledWith("test-subscription-id");
expect(apiType as jest.MockedFunction<any>).toHaveBeenCalledWith(mockDatabaseAccount1);
expect(apiType as jest.MockedFunction<any>).toHaveBeenCalledWith(mockDatabaseAccount2);
expect(apiType as jest.MockedFunction<any>).toHaveBeenCalledWith(mockNonSqlAccount);
});
}); });
it("matches snapshot with special characters in options", () => { describe("Account selection", () => {
const specialOptions = [ it("should auto-select the first SQL account when no account is currently selected", async () => {
{ mockUseDatabaseAccounts.mockReturnValue([mockDatabaseAccount1, mockDatabaseAccount2]);
key: "special",
text: 'Account with & <special> "characters"', renderWithContext();
data: {
id: "special", await waitFor(() => {
name: 'Account with & <special> "characters"', expect(mockSetCopyJobState).toHaveBeenCalledWith(expect.any(Function));
location: "East US", });
const stateUpdateFunction = mockSetCopyJobState.mock.calls[0][0];
const newState = stateUpdateFunction(mockCopyJobState);
expect(newState.source.account).toBe(mockDatabaseAccount1);
});
it("should auto-select predefined account from userContext if available", async () => {
const userContextAccount = {
...mockDatabaseAccount2,
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/account2",
};
(userContext as any).databaseAccount = userContextAccount;
mockUseDatabaseAccounts.mockReturnValue([mockDatabaseAccount1, mockDatabaseAccount2]);
renderWithContext();
await waitFor(() => {
expect(mockSetCopyJobState).toHaveBeenCalledWith(expect.any(Function));
});
const stateUpdateFunction = mockSetCopyJobState.mock.calls[0][0];
const newState = stateUpdateFunction(mockCopyJobState);
expect(newState.source.account).toBe(mockDatabaseAccount2);
});
it("should keep current account if it exists in the filtered list", async () => {
const contextWithSelectedAccount = {
...mockCopyJobContextValue,
copyJobState: {
...mockCopyJobState,
source: {
...mockCopyJobState.source,
account: mockDatabaseAccount1,
}, },
}, },
]; };
const { container } = render( mockUseDatabaseAccounts.mockReturnValue([mockDatabaseAccount1, mockDatabaseAccount2]);
<AccountDropdown options={specialOptions} disabled={false} onChange={mockOnChange} />,
);
expect(container.firstChild).toMatchSnapshot(); renderWithContext(contextWithSelectedAccount);
await waitFor(() => {
expect(mockSetCopyJobState).toHaveBeenCalledWith(expect.any(Function));
}); });
it("matches snapshot with long account name", () => { const stateUpdateFunction = mockSetCopyJobState.mock.calls[0][0];
const longNameOption = [ const newState = stateUpdateFunction(contextWithSelectedAccount.copyJobState);
{ expect(newState).toBe(contextWithSelectedAccount.copyJobState);
key: "long",
text: "This is an extremely long account name that tests how the component handles text overflow and layout constraints in the dropdown",
data: {
id: "long",
name: "This is an extremely long account name that tests how the component handles text overflow and layout constraints in the dropdown",
location: "North Central US",
},
},
];
const { container } = render(
<AccountDropdown options={longNameOption} selectedKey="long" disabled={false} onChange={mockOnChange} />,
);
expect(container.firstChild).toMatchSnapshot();
}); });
it("matches snapshot with disabled state and no selection", () => { it("should handle account change when user selects different account", async () => {
const { container } = render( mockUseDatabaseAccounts.mockReturnValue([mockDatabaseAccount1, mockDatabaseAccount2]);
<AccountDropdown options={mockAccountOptions} disabled={true} onChange={mockOnChange} />,
);
expect(container.firstChild).toMatchSnapshot(); renderWithContext();
const dropdown = screen.getByRole("combobox");
fireEvent.click(dropdown);
await waitFor(() => {
const option = screen.getByText("test-account-2");
fireEvent.click(option);
}); });
it("matches snapshot with multiple account types", () => { expect(mockSetCopyJobState).toHaveBeenCalledWith(expect.any(Function));
const mixedAccountOptions = [ });
{ });
key: "sql-account",
text: "SQL API Account",
data: {
id: "sql-account",
name: "SQL API Account",
kind: "GlobalDocumentDB",
location: "East US",
},
},
{
key: "mongo-account",
text: "MongoDB Account",
data: {
id: "mongo-account",
name: "MongoDB Account",
kind: "MongoDB",
location: "West US",
},
},
{
key: "cassandra-account",
text: "Cassandra Account",
data: {
id: "cassandra-account",
name: "Cassandra Account",
kind: "Cassandra",
location: "Central US",
},
},
];
const { container } = render( describe("ID normalization", () => {
<AccountDropdown it("should normalize account ID for Portal platform", () => {
options={mixedAccountOptions} const portalAccount = {
selectedKey="mongo-account" ...mockDatabaseAccount1,
disabled={false} id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/account1",
onChange={mockOnChange} };
/>,
);
expect(container.firstChild).toMatchSnapshot(); (configContext as any).platform = Platform.Portal;
mockUseDatabaseAccounts.mockReturnValue([portalAccount]);
const contextWithSelectedAccount = {
...mockCopyJobContextValue,
copyJobState: {
...mockCopyJobState,
source: {
...mockCopyJobState.source,
account: portalAccount,
},
},
};
renderWithContext(contextWithSelectedAccount);
const dropdown = screen.getByRole("combobox");
expect(dropdown).toMatchSnapshot();
});
it("should normalize account ID for Hosted platform", () => {
const hostedAccount = {
...mockDatabaseAccount1,
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/account1",
};
(configContext as any).platform = Platform.Hosted;
mockUseDatabaseAccounts.mockReturnValue([hostedAccount]);
const contextWithSelectedAccount = {
...mockCopyJobContextValue,
copyJobState: {
...mockCopyJobState,
source: {
...mockCopyJobState.source,
account: hostedAccount,
},
},
};
renderWithContext(contextWithSelectedAccount);
const dropdown = screen.getByRole("combobox");
expect(dropdown).toBeInTheDocument();
});
});
describe("Edge cases", () => {
it("should handle empty account list gracefully", () => {
mockUseDatabaseAccounts.mockReturnValue([]);
renderWithContext();
const dropdown = screen.getByRole("combobox");
expect(dropdown).toHaveAttribute("aria-disabled", "true");
});
it("should handle null account list gracefully", () => {
mockUseDatabaseAccounts.mockReturnValue(null as any);
renderWithContext();
const dropdown = screen.getByRole("combobox");
expect(dropdown).toHaveAttribute("aria-disabled", "true");
});
it("should handle undefined subscription ID", () => {
const contextWithoutSubscription = {
...mockCopyJobContextValue,
copyJobState: {
...mockCopyJobState,
source: {
...mockCopyJobState.source,
subscription: null,
},
} as CopyJobContextState,
};
mockUseDatabaseAccounts.mockReturnValue([]);
renderWithContext(contextWithoutSubscription);
expect(mockUseDatabaseAccounts).toHaveBeenCalledWith(undefined);
});
it("should not update state if account is already selected and the same", async () => {
const selectedAccount = mockDatabaseAccount1;
const contextWithSelectedAccount = {
...mockCopyJobContextValue,
copyJobState: {
...mockCopyJobState,
source: {
...mockCopyJobState.source,
account: selectedAccount,
},
},
};
mockUseDatabaseAccounts.mockReturnValue([mockDatabaseAccount1, mockDatabaseAccount2]);
renderWithContext(contextWithSelectedAccount);
await waitFor(() => {
expect(mockSetCopyJobState).toHaveBeenCalledWith(expect.any(Function));
});
const stateUpdateFunction = mockSetCopyJobState.mock.calls[0][0];
const newState = stateUpdateFunction(contextWithSelectedAccount.copyJobState);
expect(newState).toBe(contextWithSelectedAccount.copyJobState);
});
});
describe("Accessibility", () => {
it("should have proper aria-label", () => {
mockUseDatabaseAccounts.mockReturnValue([mockDatabaseAccount1]);
renderWithContext();
const dropdown = screen.getByRole("combobox");
expect(dropdown).toHaveAttribute("aria-label", ContainerCopyMessages.sourceAccountDropdownLabel);
});
it("should have required attribute", () => {
mockUseDatabaseAccounts.mockReturnValue([mockDatabaseAccount1]);
renderWithContext();
const dropdown = screen.getByRole("combobox");
expect(dropdown).toHaveAttribute("aria-required", "true");
}); });
}); });
}); });

View File

@@ -1,31 +1,91 @@
/* eslint-disable react/prop-types */ /* eslint-disable react/prop-types */
/* eslint-disable react/display-name */ /* eslint-disable react/display-name */
import { Dropdown } from "@fluentui/react"; import { Dropdown } from "@fluentui/react";
import React from "react"; import { configContext, Platform } from "ConfigContext";
import React, { useEffect } from "react";
import { DatabaseAccount } from "../../../../../../Contracts/DataModels";
import { useDatabaseAccounts } from "../../../../../../hooks/useDatabaseAccounts";
import { apiType, userContext } from "../../../../../../UserContext";
import ContainerCopyMessages from "../../../../ContainerCopyMessages"; import ContainerCopyMessages from "../../../../ContainerCopyMessages";
import { DropdownOptionType } from "../../../../Types/CopyJobTypes"; import { useCopyJobContext } from "../../../../Context/CopyJobContext";
import FieldRow from "../../Components/FieldRow"; import FieldRow from "../../Components/FieldRow";
interface AccountDropdownProps { interface AccountDropdownProps {}
options: DropdownOptionType[];
selectedKey?: string;
disabled: boolean;
onChange: (_ev?: React.FormEvent, option?: DropdownOptionType) => void;
}
export const AccountDropdown: React.FC<AccountDropdownProps> = React.memo( const normalizeAccountId = (id: string) => {
({ options, selectedKey, disabled, onChange }) => ( if (configContext.platform === Platform.Portal) {
return id.replace("/Microsoft.DocumentDb/", "/Microsoft.DocumentDB/");
} else if (configContext.platform === Platform.Hosted) {
return id.replace("/Microsoft.DocumentDB/", "/Microsoft.DocumentDb/");
} else {
return id;
}
};
export const AccountDropdown: React.FC<AccountDropdownProps> = () => {
const { copyJobState, setCopyJobState } = useCopyJobContext();
const selectedSubscriptionId = copyJobState?.source?.subscription?.subscriptionId;
const allAccounts: DatabaseAccount[] = useDatabaseAccounts(selectedSubscriptionId);
const sqlApiOnlyAccounts: DatabaseAccount[] = (allAccounts || []).filter((account) => apiType(account) === "SQL");
const updateCopyJobState = (newAccount: DatabaseAccount) => {
setCopyJobState((prevState) => {
if (prevState.source?.account?.id !== newAccount.id) {
return {
...prevState,
source: {
...prevState.source,
account: newAccount,
},
};
}
return prevState;
});
};
useEffect(() => {
if (sqlApiOnlyAccounts && sqlApiOnlyAccounts.length > 0 && selectedSubscriptionId) {
const currentAccountId = copyJobState?.source?.account?.id;
const predefinedAccountId = userContext.databaseAccount?.id;
const selectedAccountId = currentAccountId || predefinedAccountId;
const targetAccount: DatabaseAccount | null =
sqlApiOnlyAccounts.find((account) => account.id === selectedAccountId) || null;
updateCopyJobState(targetAccount || sqlApiOnlyAccounts[0]);
}
}, [sqlApiOnlyAccounts?.length, selectedSubscriptionId]);
const accountOptions =
sqlApiOnlyAccounts?.map((account) => ({
key: normalizeAccountId(account.id),
text: account.name,
data: account,
})) || [];
const handleAccountChange = (_ev?: React.FormEvent, option?: (typeof accountOptions)[0]) => {
const selectedAccount = option?.data as DatabaseAccount;
if (selectedAccount) {
updateCopyJobState(selectedAccount);
}
};
const isAccountDropdownDisabled = !selectedSubscriptionId || accountOptions.length === 0;
const selectedAccountId = normalizeAccountId(copyJobState?.source?.account?.id ?? "");
return (
<FieldRow label={ContainerCopyMessages.sourceAccountDropdownLabel}> <FieldRow label={ContainerCopyMessages.sourceAccountDropdownLabel}>
<Dropdown <Dropdown
placeholder={ContainerCopyMessages.sourceAccountDropdownPlaceholder} placeholder={ContainerCopyMessages.sourceAccountDropdownPlaceholder}
ariaLabel={ContainerCopyMessages.sourceAccountDropdownLabel} ariaLabel={ContainerCopyMessages.sourceAccountDropdownLabel}
options={options} options={accountOptions}
disabled={disabled} disabled={isAccountDropdownDisabled}
required required
selectedKey={selectedKey} selectedKey={selectedAccountId}
onChange={onChange} onChange={handleAccountChange}
data-test="account-dropdown"
/> />
</FieldRow> </FieldRow>
),
(prev, next) => prev.options.length === next.options.length && prev.selectedKey === next.selectedKey,
); );
};

View File

@@ -1,118 +1,295 @@
import "@testing-library/jest-dom"; import "@testing-library/jest-dom";
import { render } from "@testing-library/react"; import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
import React from "react"; import React from "react";
import { DropdownOptionType } from "../../../../Types/CopyJobTypes"; import { Subscription } from "../../../../../../Contracts/DataModels";
import Explorer from "../../../../../Explorer";
import CopyJobContextProvider from "../../../../Context/CopyJobContext";
import { SubscriptionDropdown } from "./SubscriptionDropdown"; import { SubscriptionDropdown } from "./SubscriptionDropdown";
describe("SubscriptionDropdown", () => { jest.mock("../../../../../../hooks/useSubscriptions");
const mockOnChange = jest.fn(); jest.mock("../../../../../../UserContext");
jest.mock("../../../../ContainerCopyMessages");
const mockSubscriptionOptions: DropdownOptionType[] = [ const mockUseSubscriptions = jest.requireMock("../../../../../../hooks/useSubscriptions").useSubscriptions;
const mockUserContext = jest.requireMock("../../../../../../UserContext").userContext;
const mockContainerCopyMessages = jest.requireMock("../../../../ContainerCopyMessages").default;
mockContainerCopyMessages.subscriptionDropdownLabel = "Subscription";
mockContainerCopyMessages.subscriptionDropdownPlaceholder = "Select a subscription";
describe("SubscriptionDropdown", () => {
let mockExplorer: Explorer;
const mockSubscriptions: Subscription[] = [
{ {
key: "sub-1",
text: "Development Subscription",
data: {
subscriptionId: "sub-1", subscriptionId: "sub-1",
displayName: "Development Subscription", displayName: "Subscription One",
authorizationSource: "RoleBased", state: "Enabled",
subscriptionPolicies: { tenantId: "tenant-1",
quotaId: "quota-1",
spendingLimit: "Off",
locationPlacementId: "loc-1",
},
},
}, },
{ {
key: "sub-2",
text: "Production Subscription",
data: {
subscriptionId: "sub-2", subscriptionId: "sub-2",
displayName: "Production Subscription", displayName: "Subscription Two",
authorizationSource: "RoleBased", state: "Enabled",
subscriptionPolicies: { tenantId: "tenant-1",
quotaId: "quota-2",
spendingLimit: "On",
locationPlacementId: "loc-2",
},
},
}, },
{ {
key: "sub-3",
text: "Testing Subscription",
data: {
subscriptionId: "sub-3", subscriptionId: "sub-3",
displayName: "Testing Subscription", displayName: "Another Subscription",
authorizationSource: "Legacy", state: "Enabled",
subscriptionPolicies: { tenantId: "tenant-1",
quotaId: "quota-3",
spendingLimit: "Off",
locationPlacementId: "loc-3",
},
},
}, },
]; ];
const renderWithProvider = (children: React.ReactNode) => {
return render(<CopyJobContextProvider explorer={mockExplorer}>{children}</CopyJobContextProvider>);
};
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
mockExplorer = {} as Explorer;
mockUseSubscriptions.mockReturnValue(mockSubscriptions);
mockUserContext.subscriptionId = "sub-1";
}); });
describe("Snapshot Testing", () => { describe("Rendering", () => {
it("matches snapshot with all subscription options", () => { it("should render subscription dropdown with correct attributes", () => {
const { container } = render(<SubscriptionDropdown options={mockSubscriptionOptions} onChange={mockOnChange} />); renderWithProvider(<SubscriptionDropdown />);
expect(container.firstChild).toMatchSnapshot(); const dropdown = screen.getByRole("combobox");
expect(dropdown).toBeInTheDocument();
expect(dropdown).toHaveAttribute("aria-label", "Subscription");
expect(dropdown).toHaveAttribute("data-test", "subscription-dropdown");
expect(dropdown).toBeRequired();
}); });
it("matches snapshot with selected subscription", () => { it("should render field label correctly", () => {
const { container } = render( renderWithProvider(<SubscriptionDropdown />);
<SubscriptionDropdown options={mockSubscriptionOptions} selectedKey="sub-2" onChange={mockOnChange} />,
expect(screen.getByText("Subscription:")).toBeInTheDocument();
});
it("should show placeholder when no subscription is selected", async () => {
mockUserContext.subscriptionId = "";
mockUseSubscriptions.mockReturnValue([]);
renderWithProvider(<SubscriptionDropdown />);
await waitFor(() => {
const dropdown = screen.getByRole("combobox");
expect(dropdown).toHaveTextContent("Select a subscription");
});
});
});
describe("Subscription Options", () => {
it("should populate dropdown with available subscriptions", async () => {
renderWithProvider(<SubscriptionDropdown />);
const dropdown = screen.getByRole("combobox");
fireEvent.click(dropdown);
await waitFor(() => {
expect(screen.getByText("Subscription One", { selector: ".ms-Dropdown-optionText" })).toBeInTheDocument();
expect(screen.getByText("Subscription Two", { selector: ".ms-Dropdown-optionText" })).toBeInTheDocument();
expect(screen.getByText("Another Subscription", { selector: ".ms-Dropdown-optionText" })).toBeInTheDocument();
});
});
it("should handle empty subscriptions list", () => {
mockUseSubscriptions.mockReturnValue([]);
renderWithProvider(<SubscriptionDropdown />);
const dropdown = screen.getByRole("combobox");
expect(dropdown).toBeInTheDocument();
expect(dropdown).toHaveTextContent("Select a subscription");
});
it("should handle undefined subscriptions", () => {
mockUseSubscriptions.mockReturnValue(undefined);
renderWithProvider(<SubscriptionDropdown />);
const dropdown = screen.getByRole("combobox");
expect(dropdown).toBeInTheDocument();
expect(dropdown).toHaveTextContent("Select a subscription");
});
});
describe("Selection Logic", () => {
it("should auto-select subscription based on userContext.subscriptionId on mount", async () => {
mockUserContext.subscriptionId = "sub-2";
renderWithProvider(<SubscriptionDropdown />);
await waitFor(() => {
const dropdown = screen.getByRole("combobox");
expect(dropdown).toHaveTextContent("Subscription Two");
});
});
it("should maintain current selection when subscriptions list updates with same subscription", async () => {
renderWithProvider(<SubscriptionDropdown />);
await waitFor(() => {
const dropdown = screen.getByRole("combobox");
expect(dropdown).toHaveTextContent("Subscription One");
});
act(() => {
mockUseSubscriptions.mockReturnValue([...mockSubscriptions]);
});
await waitFor(() => {
const dropdown = screen.getByRole("combobox");
expect(dropdown).toHaveTextContent("Subscription One");
});
});
it("should prioritize current copyJobState subscription over userContext subscription", async () => {
mockUserContext.subscriptionId = "sub-2";
const { rerender } = renderWithProvider(<SubscriptionDropdown />);
await waitFor(() => {
const dropdown = screen.getByRole("combobox");
expect(dropdown).toHaveTextContent("Subscription Two");
});
const dropdown = screen.getByRole("combobox");
fireEvent.click(dropdown);
await waitFor(() => {
const option = screen.getByText("Another Subscription");
fireEvent.click(option);
});
rerender(
<CopyJobContextProvider explorer={mockExplorer}>
<SubscriptionDropdown />
</CopyJobContextProvider>,
); );
expect(container.firstChild).toMatchSnapshot(); await waitFor(() => {
const dropdown = screen.getByRole("combobox");
expect(dropdown).toHaveTextContent("Another Subscription");
});
}); });
it("matches snapshot with empty options", () => { it("should handle subscription selection change", async () => {
const { container } = render(<SubscriptionDropdown options={[]} onChange={mockOnChange} />); renderWithProvider(<SubscriptionDropdown />);
expect(container.firstChild).toMatchSnapshot(); const dropdown = screen.getByRole("combobox");
fireEvent.click(dropdown);
await waitFor(() => {
const option = screen.getByText("Subscription Two");
fireEvent.click(option);
}); });
it("matches snapshot with single option", () => { await waitFor(() => {
const { container } = render( expect(dropdown).toHaveTextContent("Subscription Two");
<SubscriptionDropdown options={[mockSubscriptionOptions[0]]} selectedKey="sub-1" onChange={mockOnChange} />, });
);
expect(container.firstChild).toMatchSnapshot();
}); });
it("matches snapshot with special characters in options", () => { it("should not auto-select if target subscription not found in list", async () => {
const specialOptions = [ mockUserContext.subscriptionId = "non-existent-sub";
{
key: "special",
text: 'Subscription with & <special> "characters"',
data: { subscriptionId: "special" },
},
];
const { container } = render(<SubscriptionDropdown options={specialOptions} onChange={mockOnChange} />); renderWithProvider(<SubscriptionDropdown />);
expect(container.firstChild).toMatchSnapshot(); await waitFor(() => {
}); const dropdown = screen.getByRole("combobox");
expect(dropdown).toHaveTextContent("Select a subscription");
it("matches snapshot with long subscription name", () => { });
const longNameOption = [ });
{ });
key: "long",
text: "This is an extremely long subscription name that tests how the component handles text overflow and layout constraints", describe("Context State Management", () => {
data: { subscriptionId: "long" }, it("should update copyJobState when subscription is selected", async () => {
}, renderWithProvider(<SubscriptionDropdown />);
];
const dropdown = screen.getByRole("combobox");
const { container } = render( fireEvent.click(dropdown);
<SubscriptionDropdown options={longNameOption} selectedKey="long" onChange={mockOnChange} />,
); await waitFor(() => {
const option = screen.getByText("Subscription Two");
expect(container.firstChild).toMatchSnapshot(); fireEvent.click(option);
});
await waitFor(() => {
expect(dropdown).toHaveTextContent("Subscription Two");
});
});
it("should reset account when subscription changes", async () => {
renderWithProvider(<SubscriptionDropdown />);
await waitFor(() => {
const dropdown = screen.getByRole("combobox");
expect(dropdown).toHaveTextContent("Subscription One");
});
const dropdown = screen.getByRole("combobox");
fireEvent.click(dropdown);
await waitFor(() => {
const option = screen.getByText("Subscription Two");
fireEvent.click(option);
});
await waitFor(() => {
expect(dropdown).toHaveTextContent("Subscription Two");
});
});
it("should not update state if same subscription is selected", async () => {
renderWithProvider(<SubscriptionDropdown />);
await waitFor(() => {
const dropdown = screen.getByRole("combobox");
expect(dropdown).toHaveTextContent("Subscription One");
});
const dropdown = screen.getByRole("combobox");
fireEvent.click(dropdown);
await waitFor(() => {
const option = screen.getByText("Subscription One", { selector: ".ms-Dropdown-optionText" });
fireEvent.click(option);
});
await waitFor(() => {
expect(dropdown).toHaveTextContent("Subscription One");
});
});
});
describe("Edge Cases", () => {
it("should handle subscription change event with option missing data", async () => {
renderWithProvider(<SubscriptionDropdown />);
const dropdown = screen.getByRole("combobox");
fireEvent.click(dropdown);
expect(dropdown).toBeInTheDocument();
});
it("should handle subscriptions loading state", () => {
mockUseSubscriptions.mockReturnValue(undefined);
renderWithProvider(<SubscriptionDropdown />);
const dropdown = screen.getByRole("combobox");
expect(dropdown).toBeInTheDocument();
expect(dropdown).toHaveTextContent("Select a subscription");
});
it("should work when both userContext.subscriptionId and copyJobState subscription are null", () => {
mockUserContext.subscriptionId = "";
renderWithProvider(<SubscriptionDropdown />);
const dropdown = screen.getByRole("combobox");
expect(dropdown).toBeInTheDocument();
expect(dropdown).toHaveTextContent("Select a subscription");
}); });
}); });
}); });

View File

@@ -1,29 +1,79 @@
/* eslint-disable react/prop-types */ /* eslint-disable react/prop-types */
/* eslint-disable react/display-name */ /* eslint-disable react/display-name */
import { Dropdown } from "@fluentui/react"; import { Dropdown } from "@fluentui/react";
import React from "react"; import React, { useEffect } from "react";
import { Subscription } from "../../../../../../Contracts/DataModels";
import { useSubscriptions } from "../../../../../../hooks/useSubscriptions";
import { userContext } from "../../../../../../UserContext";
import ContainerCopyMessages from "../../../../ContainerCopyMessages"; import ContainerCopyMessages from "../../../../ContainerCopyMessages";
import { DropdownOptionType } from "../../../../Types/CopyJobTypes"; import { useCopyJobContext } from "../../../../Context/CopyJobContext";
import FieldRow from "../../Components/FieldRow"; import FieldRow from "../../Components/FieldRow";
interface SubscriptionDropdownProps { interface SubscriptionDropdownProps {}
options: DropdownOptionType[];
selectedKey?: string;
onChange: (_ev?: React.FormEvent, option?: DropdownOptionType) => void;
}
export const SubscriptionDropdown: React.FC<SubscriptionDropdownProps> = React.memo( export const SubscriptionDropdown: React.FC<SubscriptionDropdownProps> = React.memo(() => {
({ options, selectedKey, onChange }) => ( const { copyJobState, setCopyJobState } = useCopyJobContext();
const subscriptions: Subscription[] = useSubscriptions();
const updateCopyJobState = (newSubscription: Subscription) => {
setCopyJobState((prevState) => {
if (prevState.source?.subscription?.subscriptionId !== newSubscription.subscriptionId) {
return {
...prevState,
source: {
...prevState.source,
subscription: newSubscription,
account: null,
},
};
}
return prevState;
});
};
useEffect(() => {
if (subscriptions && subscriptions.length > 0) {
const currentSubscriptionId = copyJobState?.source?.subscription?.subscriptionId;
const predefinedSubscriptionId = userContext.subscriptionId;
const selectedSubscriptionId = currentSubscriptionId || predefinedSubscriptionId;
const targetSubscription: Subscription | null =
subscriptions.find((sub) => sub.subscriptionId === selectedSubscriptionId) || null;
if (targetSubscription) {
updateCopyJobState(targetSubscription);
}
}
}, [subscriptions?.length]);
const subscriptionOptions =
subscriptions?.map((sub) => ({
key: sub.subscriptionId,
text: sub.displayName,
data: sub,
})) || [];
const handleSubscriptionChange = (_ev?: React.FormEvent, option?: (typeof subscriptionOptions)[0]) => {
const selectedSubscription = option?.data as Subscription;
if (selectedSubscription) {
updateCopyJobState(selectedSubscription);
}
};
const selectedSubscriptionId = copyJobState?.source?.subscription?.subscriptionId;
return (
<FieldRow label={ContainerCopyMessages.subscriptionDropdownLabel}> <FieldRow label={ContainerCopyMessages.subscriptionDropdownLabel}>
<Dropdown <Dropdown
placeholder={ContainerCopyMessages.subscriptionDropdownPlaceholder} placeholder={ContainerCopyMessages.subscriptionDropdownPlaceholder}
ariaLabel={ContainerCopyMessages.subscriptionDropdownLabel} ariaLabel={ContainerCopyMessages.subscriptionDropdownLabel}
options={options} data-test="subscription-dropdown"
options={subscriptionOptions}
required required
selectedKey={selectedKey} selectedKey={selectedSubscriptionId}
onChange={onChange} onChange={handleSubscriptionChange}
/> />
</FieldRow> </FieldRow>
),
(prev, next) => prev.options.length === next.options.length && prev.selectedKey === next.selectedKey,
); );
});

View File

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

View File

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

View File

@@ -1,480 +1,170 @@
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";
import { apiType } from "UserContext"; import { useCopyJobContext } from "../../../Context/CopyJobContext";
import { DatabaseAccount, Subscription } from "../../../../../Contracts/DataModels";
import { useDatabaseAccounts } from "../../../../../hooks/useDatabaseAccounts";
import { useSubscriptions } from "../../../../../hooks/useSubscriptions";
import { CopyJobMigrationType } from "../../../Enums/CopyJobEnums"; import { CopyJobMigrationType } from "../../../Enums/CopyJobEnums";
import { CopyJobContextProviderType, CopyJobContextState } from "../../../Types/CopyJobTypes"; import { CopyJobContextProviderType } from "../../../Types/CopyJobTypes";
import SelectAccount from "./SelectAccount"; import SelectAccount from "./SelectAccount";
jest.mock("UserContext", () => ({
apiType: jest.fn(),
}));
jest.mock("../../../../../hooks/useDatabaseAccounts");
jest.mock("../../../../../hooks/useSubscriptions");
jest.mock("../../../Context/CopyJobContext", () => ({ jest.mock("../../../Context/CopyJobContext", () => ({
useCopyJobContext: () => mockContextValue, useCopyJobContext: jest.fn(),
}));
jest.mock("./Utils/selectAccountUtils", () => ({
useDropdownOptions: jest.fn(),
useEventHandlers: jest.fn(),
})); }));
jest.mock("./Components/SubscriptionDropdown", () => ({ jest.mock("./Components/SubscriptionDropdown", () => ({
SubscriptionDropdown: jest.fn(({ options, selectedKey, onChange, ...props }) => ( SubscriptionDropdown: jest.fn(() => <div data-testid="subscription-dropdown">Subscription Dropdown</div>),
<div data-testid="subscription-dropdown" data-selected={selectedKey} {...props}>
{options?.map((option: any) => (
<div
key={option.key}
data-testid={`subscription-option-${option.key}`}
onClick={() => onChange?.(undefined, option)}
>
{option.text}
</div>
))}
</div>
)),
})); }));
jest.mock("./Components/AccountDropdown", () => ({ jest.mock("./Components/AccountDropdown", () => ({
AccountDropdown: jest.fn(({ options, selectedKey, disabled, onChange, ...props }) => ( AccountDropdown: jest.fn(() => <div data-testid="account-dropdown">Account Dropdown</div>),
<div data-testid="account-dropdown" data-selected={selectedKey} data-disabled={disabled} {...props}>
{options?.map((option: any) => (
<div
key={option.key}
data-testid={`account-option-${option.key}`}
onClick={() => onChange?.(undefined, option)}
>
{option.text}
</div>
))}
</div>
)),
})); }));
jest.mock("./Components/MigrationTypeCheckbox", () => ({ jest.mock("./Components/MigrationTypeCheckbox", () => ({
MigrationTypeCheckbox: jest.fn(({ checked, onChange, ...props }) => ( MigrationTypeCheckbox: jest.fn(({ checked, onChange }: { checked: boolean; onChange: () => void }) => (
<div data-testid="migration-type-checkbox" data-checked={checked} {...props}> <div data-testid="migration-type-checkbox">
<input <input
type="checkbox" type="checkbox"
checked={checked} checked={checked}
onChange={(e) => onChange?.(e, e.target.checked)} onChange={onChange}
data-testid="migration-checkbox-input" data-testid="migration-checkbox-input"
aria-label="Migration Type Checkbox"
/> />
Copy container in offline mode
</div> </div>
)), )),
})); }));
jest.mock("../../../ContainerCopyMessages", () => ({ describe("SelectAccount", () => {
selectAccountDescription: "Select your source account and subscription", const mockSetCopyJobState = jest.fn();
}));
const mockUseDatabaseAccounts = useDatabaseAccounts as jest.MockedFunction<typeof useDatabaseAccounts>; const defaultContextValue: CopyJobContextProviderType = {
const mockUseSubscriptions = useSubscriptions as jest.MockedFunction<typeof useSubscriptions>;
const mockApiType = apiType as jest.MockedFunction<typeof apiType>;
import { useDropdownOptions, useEventHandlers } from "./Utils/selectAccountUtils";
const mockUseDropdownOptions = useDropdownOptions as jest.MockedFunction<typeof useDropdownOptions>;
const mockUseEventHandlers = useEventHandlers as jest.MockedFunction<typeof useEventHandlers>;
const mockSubscriptions = [
{
subscriptionId: "sub-1",
displayName: "Test Subscription 1",
authorizationSource: "RoleBased",
subscriptionPolicies: {
quotaId: "quota-1",
spendingLimit: "Off",
locationPlacementId: "loc-1",
},
},
{
subscriptionId: "sub-2",
displayName: "Test Subscription 2",
authorizationSource: "RoleBased",
subscriptionPolicies: {
quotaId: "quota-2",
spendingLimit: "On",
locationPlacementId: "loc-2",
},
},
] as Subscription[];
const mockAccounts = [
{
id: "/subscriptions/sub-1/resourceGroups/rg-1/providers/Microsoft.DocumentDB/databaseAccounts/account-1",
name: "test-cosmos-account-1",
location: "East US",
kind: "GlobalDocumentDB",
properties: {
documentEndpoint: "https://account-1.documents.azure.com/",
capabilities: [],
enableFreeTier: false,
},
},
{
id: "/subscriptions/sub-1/resourceGroups/rg-1/providers/Microsoft.DocumentDB/databaseAccounts/account-2",
name: "test-cosmos-account-2",
location: "West US",
kind: "MongoDB",
properties: {
documentEndpoint: "https://account-2.documents.azure.com/",
capabilities: [],
},
},
] as DatabaseAccount[];
const mockDropdownOptions = {
subscriptionOptions: [
{ key: "sub-1", text: "Test Subscription 1", data: mockSubscriptions[0] },
{ key: "sub-2", text: "Test Subscription 2", data: mockSubscriptions[1] },
],
accountOptions: [{ key: mockAccounts[0].id, text: mockAccounts[0].name, data: mockAccounts[0] }],
};
const mockEventHandlers = {
handleSelectSourceAccount: jest.fn(),
handleMigrationTypeChange: jest.fn(),
};
let mockContextValue = {
copyJobState: { copyJobState: {
jobName: "", jobName: "",
migrationType: CopyJobMigrationType.Offline, migrationType: CopyJobMigrationType.Online,
source: { source: {
subscription: null, subscription: null as any,
account: null, account: null as any,
databaseId: "", databaseId: "",
containerId: "", containerId: "",
}, },
target: { target: {
subscriptionId: "", subscriptionId: "",
account: null, account: null as any,
databaseId: "", databaseId: "",
containerId: "", containerId: "",
}, },
sourceReadAccessFromTarget: false, sourceReadAccessFromTarget: false,
} as CopyJobContextState, },
setCopyJobState: jest.fn(), setCopyJobState: mockSetCopyJobState,
flow: null, flow: { currentScreen: "selectAccount" },
setFlow: jest.fn(), setFlow: jest.fn(),
contextError: null, contextError: null,
setContextError: jest.fn(), setContextError: jest.fn(),
resetCopyJobState: jest.fn(),
explorer: {} as any, explorer: {} as any,
} as CopyJobContextProviderType; resetCopyJobState: jest.fn(),
};
describe("SelectAccount Component", () => {
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
(useCopyJobContext as jest.Mock).mockReturnValue(defaultContextValue);
mockContextValue = {
copyJobState: {
jobName: "",
migrationType: CopyJobMigrationType.Offline,
source: {
subscription: null,
account: null,
databaseId: "",
containerId: "",
},
target: {
subscriptionId: "",
account: null,
databaseId: "",
containerId: "",
},
sourceReadAccessFromTarget: false,
} as CopyJobContextState,
setCopyJobState: jest.fn(),
flow: null,
setFlow: jest.fn(),
contextError: null,
setContextError: jest.fn(),
resetCopyJobState: jest.fn(),
explorer: {} as any,
};
mockUseSubscriptions.mockReturnValue(mockSubscriptions);
mockUseDatabaseAccounts.mockReturnValue(mockAccounts);
mockApiType.mockReturnValue("SQL");
mockUseDropdownOptions.mockReturnValue(mockDropdownOptions);
mockUseEventHandlers.mockReturnValue(mockEventHandlers);
}); });
describe("Rendering", () => { afterEach(() => {
it("should render component with default state", () => { jest.clearAllMocks();
});
describe("Component Rendering", () => {
it("should render the component with all required elements", () => {
const { container } = render(<SelectAccount />); const { container } = render(<SelectAccount />);
expect(screen.getByText("Select your source account and subscription")).toBeInTheDocument(); expect(container.firstChild).toHaveAttribute("data-test", "Panel:SelectAccountContainer");
expect(container.firstChild).toHaveClass("selectAccountContainer");
expect(screen.getByText(/Please select a source account from which to copy/i)).toBeInTheDocument();
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-checkbox")).toBeInTheDocument();
expect(container).toMatchSnapshot();
}); });
it("should render with selected subscription", () => { it("should render correctly with snapshot", () => {
mockContextValue.copyJobState.source.subscription = mockSubscriptions[0];
const { container } = render(<SelectAccount />); const { container } = render(<SelectAccount />);
expect(container.firstChild).toMatchSnapshot();
expect(screen.getByTestId("subscription-dropdown")).toHaveAttribute("data-selected", "sub-1");
expect(container).toMatchSnapshot();
});
it("should render with selected account", () => {
mockContextValue.copyJobState.source.subscription = mockSubscriptions[0];
mockContextValue.copyJobState.source.account = mockAccounts[0];
const { container } = render(<SelectAccount />);
expect(screen.getByTestId("account-dropdown")).toHaveAttribute("data-selected", mockAccounts[0].id);
expect(container).toMatchSnapshot();
});
it("should render with offline migration type checked", () => {
mockContextValue.copyJobState.migrationType = CopyJobMigrationType.Offline;
const { container } = render(<SelectAccount />);
expect(screen.getByTestId("migration-type-checkbox")).toHaveAttribute("data-checked", "true");
expect(container).toMatchSnapshot();
});
it("should render with online migration type unchecked", () => {
mockContextValue.copyJobState.migrationType = CopyJobMigrationType.Online;
const { container } = render(<SelectAccount />);
expect(screen.getByTestId("migration-type-checkbox")).toHaveAttribute("data-checked", "false");
expect(container).toMatchSnapshot();
}); });
}); });
describe("Hook Integration", () => { describe("Migration Type Functionality", () => {
it("should call useSubscriptions hook", () => { it("should display migration type checkbox as unchecked when migrationType is Online", () => {
render(<SelectAccount />); (useCopyJobContext as jest.Mock).mockReturnValue({
expect(mockUseSubscriptions).toHaveBeenCalledTimes(1); ...defaultContextValue,
copyJobState: {
...defaultContextValue.copyJobState,
migrationType: CopyJobMigrationType.Online,
},
}); });
it("should call useDatabaseAccounts with selected subscription ID", () => {
mockContextValue.copyJobState.source.subscription = mockSubscriptions[0];
render(<SelectAccount />);
expect(mockUseDatabaseAccounts).toHaveBeenCalledWith("sub-1");
});
it("should call useDatabaseAccounts with undefined when no subscription selected", () => {
render(<SelectAccount />);
expect(mockUseDatabaseAccounts).toHaveBeenCalledWith(undefined);
});
it("should filter accounts to SQL API only", () => {
mockApiType.mockReturnValueOnce("SQL").mockReturnValueOnce("Mongo");
render(<SelectAccount />);
expect(mockApiType).toHaveBeenCalledTimes(2);
expect(mockApiType).toHaveBeenCalledWith(mockAccounts[0]);
expect(mockApiType).toHaveBeenCalledWith(mockAccounts[1]);
});
it("should call useDropdownOptions with correct parameters", () => {
const sqlOnlyAccounts = [mockAccounts[0]]; // Only SQL account
mockApiType.mockImplementation((account) => (account === mockAccounts[0] ? "SQL" : "Mongo"));
render(<SelectAccount />); render(<SelectAccount />);
expect(mockUseDropdownOptions).toHaveBeenCalledWith(mockSubscriptions, sqlOnlyAccounts); const checkbox = screen.getByTestId("migration-checkbox-input");
expect(checkbox).not.toBeChecked();
}); });
it("should call useEventHandlers with setCopyJobState", () => { it("should display migration type checkbox as checked when migrationType is Offline", () => {
render(<SelectAccount />); (useCopyJobContext as jest.Mock).mockReturnValue({
expect(mockUseEventHandlers).toHaveBeenCalledWith(mockContextValue.setCopyJobState); ...defaultContextValue,
}); copyJobState: {
...defaultContextValue.copyJobState,
migrationType: CopyJobMigrationType.Offline,
},
}); });
describe("Event Handling", () => {
it("should handle subscription selection", () => {
render(<SelectAccount />); render(<SelectAccount />);
const subscriptionOption = screen.getByTestId("subscription-option-sub-1"); const checkbox = screen.getByTestId("migration-checkbox-input");
fireEvent.click(subscriptionOption); expect(checkbox).toBeChecked();
expect(mockEventHandlers.handleSelectSourceAccount).toHaveBeenCalledWith("subscription", mockSubscriptions[0]);
}); });
it("should handle account selection", () => { it("should call setCopyJobState with Online migration type when checkbox is unchecked", () => {
render(<SelectAccount />); (useCopyJobContext as jest.Mock).mockReturnValue({
...defaultContextValue,
const accountOption = screen.getByTestId(`account-option-${mockAccounts[0].id}`); copyJobState: {
fireEvent.click(accountOption); ...defaultContextValue.copyJobState,
migrationType: CopyJobMigrationType.Offline,
expect(mockEventHandlers.handleSelectSourceAccount).toHaveBeenCalledWith("account", mockAccounts[0]); },
}); });
it("should handle migration type change", () => {
render(<SelectAccount />); render(<SelectAccount />);
const checkbox = screen.getByTestId("migration-checkbox-input"); const checkbox = screen.getByTestId("migration-checkbox-input");
fireEvent.click(checkbox); fireEvent.click(checkbox);
expect(mockEventHandlers.handleMigrationTypeChange).toHaveBeenCalledWith(expect.any(Object), false); expect(mockSetCopyJobState).toHaveBeenCalledWith(expect.any(Function));
});
});
describe("Dropdown States", () => { const updateFunction = mockSetCopyJobState.mock.calls[0][0];
it("should disable account dropdown when no subscription is selected", () => { const previousState = {
render(<SelectAccount />); ...defaultContextValue.copyJobState,
migrationType: CopyJobMigrationType.Offline,
};
const result = updateFunction(previousState);
expect(screen.getByTestId("account-dropdown")).toHaveAttribute("data-disabled", "true"); expect(result).toEqual({
}); ...previousState,
migrationType: CopyJobMigrationType.Online,
it("should enable account dropdown when subscription is selected", () => { });
mockContextValue.copyJobState.source.subscription = mockSubscriptions[0]; });
});
render(<SelectAccount />);
describe("Performance and Optimization", () => {
expect(screen.getByTestId("account-dropdown")).toHaveAttribute("data-disabled", "false"); it("should maintain referential equality of handler functions between renders", async () => {
}); const { rerender } = render(<SelectAccount />);
});
const migrationCheckbox = (await import("./Components/MigrationTypeCheckbox")).MigrationTypeCheckbox as jest.Mock;
describe("Component Props", () => { const firstRenderHandler = migrationCheckbox.mock.calls[migrationCheckbox.mock.calls.length - 1][0].onChange;
it("should pass correct props to SubscriptionDropdown", () => {
render(<SelectAccount />); rerender(<SelectAccount />);
const dropdown = screen.getByTestId("subscription-dropdown"); const secondRenderHandler = migrationCheckbox.mock.calls[migrationCheckbox.mock.calls.length - 1][0].onChange;
expect(dropdown).not.toHaveAttribute("data-selected");
}); expect(firstRenderHandler).toBe(secondRenderHandler);
it("should pass selected subscription ID to SubscriptionDropdown", () => {
mockContextValue.copyJobState.source.subscription = mockSubscriptions[0];
render(<SelectAccount />);
const dropdown = screen.getByTestId("subscription-dropdown");
expect(dropdown).toHaveAttribute("data-selected", "sub-1");
});
it("should pass correct props to AccountDropdown", () => {
render(<SelectAccount />);
const dropdown = screen.getByTestId("account-dropdown");
expect(dropdown).not.toHaveAttribute("data-selected");
expect(dropdown).toHaveAttribute("data-disabled", "true");
});
it("should pass selected account ID to AccountDropdown", () => {
mockContextValue.copyJobState.source.account = mockAccounts[0];
render(<SelectAccount />);
const dropdown = screen.getByTestId("account-dropdown");
expect(dropdown).toHaveAttribute("data-selected", mockAccounts[0].id);
});
it("should pass correct checked state to MigrationTypeCheckbox", () => {
mockContextValue.copyJobState.migrationType = CopyJobMigrationType.Offline;
render(<SelectAccount />);
const checkbox = screen.getByTestId("migration-type-checkbox");
expect(checkbox).toHaveAttribute("data-checked", "true");
});
});
describe("Edge Cases", () => {
it("should handle empty subscriptions array", () => {
mockUseSubscriptions.mockReturnValue([]);
mockUseDropdownOptions.mockReturnValue({
subscriptionOptions: [],
accountOptions: [],
});
const { container } = render(<SelectAccount />);
expect(container).toMatchSnapshot();
});
it("should handle empty accounts array", () => {
mockUseDatabaseAccounts.mockReturnValue([]);
mockUseDropdownOptions.mockReturnValue({
subscriptionOptions: mockDropdownOptions.subscriptionOptions,
accountOptions: [],
});
const { container } = render(<SelectAccount />);
expect(container).toMatchSnapshot();
});
it("should handle null subscription in context", () => {
mockContextValue.copyJobState.source.subscription = null;
const { container } = render(<SelectAccount />);
expect(container).toMatchSnapshot();
});
it("should handle null account in context", () => {
mockContextValue.copyJobState.source.account = null;
const { container } = render(<SelectAccount />);
expect(container).toMatchSnapshot();
});
it("should handle undefined subscriptions from hook", () => {
mockUseSubscriptions.mockReturnValue(undefined as any);
mockUseDropdownOptions.mockReturnValue({
subscriptionOptions: [],
accountOptions: [],
});
const { container } = render(<SelectAccount />);
expect(container).toMatchSnapshot();
});
it("should handle undefined accounts from hook", () => {
mockUseDatabaseAccounts.mockReturnValue(undefined as any);
mockUseDropdownOptions.mockReturnValue({
subscriptionOptions: mockDropdownOptions.subscriptionOptions,
accountOptions: [],
});
const { container } = render(<SelectAccount />);
expect(container).toMatchSnapshot();
});
it("should filter out non-SQL accounts correctly", () => {
const mixedAccounts = [
{ ...mockAccounts[0], kind: "GlobalDocumentDB" },
{ ...mockAccounts[1], kind: "MongoDB" },
];
mockUseDatabaseAccounts.mockReturnValue(mixedAccounts);
mockApiType.mockImplementation((account) => (account.kind === "GlobalDocumentDB" ? "SQL" : "Mongo"));
render(<SelectAccount />);
expect(mockApiType).toHaveBeenCalledTimes(2);
const sqlOnlyAccounts = mixedAccounts.filter((account) => apiType(account) === "SQL");
expect(mockUseDropdownOptions).toHaveBeenCalledWith(mockSubscriptions, sqlOnlyAccounts);
});
});
describe("Complete Workflow", () => {
it("should render complete workflow with all selections", () => {
mockContextValue.copyJobState.source.subscription = mockSubscriptions[0];
mockContextValue.copyJobState.source.account = mockAccounts[0];
mockContextValue.copyJobState.migrationType = CopyJobMigrationType.Online;
const { container } = render(<SelectAccount />);
expect(screen.getByTestId("subscription-dropdown")).toHaveAttribute("data-selected", "sub-1");
expect(screen.getByTestId("account-dropdown")).toHaveAttribute("data-selected", mockAccounts[0].id);
expect(screen.getByTestId("account-dropdown")).toHaveAttribute("data-disabled", "false");
expect(screen.getByTestId("migration-type-checkbox")).toHaveAttribute("data-checked", "false");
expect(container).toMatchSnapshot();
}); });
}); });
}); });

View File

@@ -1,52 +1,37 @@
/* eslint-disable react/display-name */ import { Stack, Text } from "@fluentui/react";
import { Stack } from "@fluentui/react";
import React from "react"; import React from "react";
import { apiType } from "UserContext";
import { DatabaseAccount, Subscription } from "../../../../../Contracts/DataModels";
import { useDatabaseAccounts } from "../../../../../hooks/useDatabaseAccounts";
import { useSubscriptions } from "../../../../../hooks/useSubscriptions";
import ContainerCopyMessages from "../../../ContainerCopyMessages"; import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { useCopyJobContext } from "../../../Context/CopyJobContext"; import { useCopyJobContext } from "../../../Context/CopyJobContext";
import { CopyJobMigrationType } from "../../../Enums/CopyJobEnums"; import { CopyJobMigrationType } from "../../../Enums/CopyJobEnums";
import { AccountDropdown } from "./Components/AccountDropdown"; import { AccountDropdown } from "./Components/AccountDropdown";
import { MigrationTypeCheckbox } from "./Components/MigrationTypeCheckbox"; import { MigrationTypeCheckbox } from "./Components/MigrationTypeCheckbox";
import { SubscriptionDropdown } from "./Components/SubscriptionDropdown"; import { SubscriptionDropdown } from "./Components/SubscriptionDropdown";
import { useDropdownOptions, useEventHandlers } from "./Utils/selectAccountUtils";
const SelectAccount = React.memo(() => { const SelectAccount = React.memo(() => {
const { copyJobState, setCopyJobState } = useCopyJobContext(); const { copyJobState, setCopyJobState } = useCopyJobContext();
const selectedSubscriptionId = copyJobState?.source?.subscription?.subscriptionId;
const selectedSourceAccountId = copyJobState?.source?.account?.id;
const subscriptions: Subscription[] = useSubscriptions(); const handleMigrationTypeChange = (_ev?: React.FormEvent<HTMLElement>, checked?: boolean) => {
const allAccounts: DatabaseAccount[] = useDatabaseAccounts(selectedSubscriptionId); setCopyJobState((prevState) => ({
const sqlApiOnlyAccounts: DatabaseAccount[] = allAccounts?.filter((account) => apiType(account) === "SQL"); ...prevState,
migrationType: checked ? CopyJobMigrationType.Offline : CopyJobMigrationType.Online,
const { subscriptionOptions, accountOptions } = useDropdownOptions(subscriptions, sqlApiOnlyAccounts); }));
const { handleSelectSourceAccount, handleMigrationTypeChange } = useEventHandlers(setCopyJobState); };
const migrationTypeChecked = copyJobState?.migrationType === CopyJobMigrationType.Offline; const migrationTypeChecked = copyJobState?.migrationType === CopyJobMigrationType.Offline;
return ( return (
<Stack className="selectAccountContainer" tokens={{ childrenGap: 15 }}> <Stack data-test="Panel:SelectAccountContainer" className="selectAccountContainer" tokens={{ childrenGap: 15 }}>
<span>{ContainerCopyMessages.selectAccountDescription}</span> <Text>{ContainerCopyMessages.selectAccountDescription}</Text>
<SubscriptionDropdown <SubscriptionDropdown />
options={subscriptionOptions}
selectedKey={selectedSubscriptionId}
onChange={(_ev, option) => handleSelectSourceAccount("subscription", option?.data)}
/>
<AccountDropdown <AccountDropdown />
options={accountOptions}
selectedKey={selectedSourceAccountId}
disabled={!selectedSubscriptionId}
onChange={(_ev, option) => handleSelectSourceAccount("account", option?.data)}
/>
<MigrationTypeCheckbox checked={migrationTypeChecked} onChange={handleMigrationTypeChange} /> <MigrationTypeCheckbox checked={migrationTypeChecked} onChange={handleMigrationTypeChange} />
</Stack> </Stack>
); );
}); });
SelectAccount.displayName = "SelectAccount";
export default SelectAccount; export default SelectAccount;

View File

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

View File

@@ -1,80 +0,0 @@
import React from "react";
import { DatabaseAccount, Subscription } from "../../../../../../Contracts/DataModels";
import { CopyJobMigrationType } from "../../../../Enums/CopyJobEnums";
import { CopyJobContextProviderType, CopyJobContextState, DropdownOptionType } from "../../../../Types/CopyJobTypes";
import { useCopyJobPrerequisitesCache } from "../../../Utils/useCopyJobPrerequisitesCache";
export function useDropdownOptions(
subscriptions: Subscription[],
accounts: DatabaseAccount[],
): {
subscriptionOptions: DropdownOptionType[];
accountOptions: DropdownOptionType[];
} {
const subscriptionOptions =
subscriptions?.map((sub) => ({
key: sub.subscriptionId,
text: sub.displayName,
data: sub,
})) || [];
const normalizeAccountId = (id: string) => {
if (!id) {
return id;
}
return id.replace(/\/Microsoft\.DocumentDb\//i, "/Microsoft.DocumentDB/");
};
const accountOptions =
accounts?.map((account) => ({
key: normalizeAccountId(account.id),
text: account.name,
data: account,
})) || [];
return { subscriptionOptions, accountOptions };
}
type setCopyJobStateType = CopyJobContextProviderType["setCopyJobState"];
export function useEventHandlers(setCopyJobState: setCopyJobStateType) {
const { setValidationCache } = useCopyJobPrerequisitesCache();
const handleSelectSourceAccount = (
type: "subscription" | "account",
data: (Subscription & DatabaseAccount) | undefined,
) => {
setCopyJobState((prevState: CopyJobContextState) => {
if (type === "subscription") {
return {
...prevState,
source: {
...prevState.source,
subscription: data || null,
account: null,
},
};
}
if (type === "account") {
return {
...prevState,
source: {
...prevState.source,
account: data || null,
},
};
}
return prevState;
});
setValidationCache(new Map<string, boolean>());
};
const handleMigrationTypeChange = React.useCallback((_ev?: React.FormEvent<HTMLElement>, checked?: boolean) => {
setCopyJobState((prevState: CopyJobContextState) => ({
...prevState,
migrationType: checked ? CopyJobMigrationType.Offline : CopyJobMigrationType.Online,
}));
setValidationCache(new Map<string, boolean>());
}, []);
return { handleSelectSourceAccount, handleMigrationTypeChange };
}

View File

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

View File

@@ -39,6 +39,7 @@ export function useCopyJobNavigation() {
const [state, dispatch] = useReducer(navigationReducer, { screenHistory: [SCREEN_KEYS.SelectAccount] }); const [state, dispatch] = useReducer(navigationReducer, { screenHistory: [SCREEN_KEYS.SelectAccount] });
const handlePrevious = useCallback(() => { const handlePrevious = useCallback(() => {
setContextError(null);
dispatch({ type: "PREVIOUS" }); dispatch({ type: "PREVIOUS" });
}, [dispatch]); }, [dispatch]);

View File

@@ -52,7 +52,7 @@ describe("CopyJobStatusWithIcon", () => {
const spinner = container.querySelector('[class*="ms-Spinner"]'); const spinner = container.querySelector('[class*="ms-Spinner"]');
expect(spinner).toBeInTheDocument(); expect(spinner).toBeInTheDocument();
expect(container).toHaveTextContent("In Progress"); expect(container).toHaveTextContent("Running");
expect(container.firstChild).toMatchSnapshot(); expect(container.firstChild).toMatchSnapshot();
}); });
}); });
@@ -83,18 +83,18 @@ describe("CopyJobStatusWithIcon", () => {
it("provides meaningful text content for screen readers", () => { it("provides meaningful text content for screen readers", () => {
const { container } = render(<CopyJobStatusWithIcon status={CopyJobStatusType.InProgress} />); const { container } = render(<CopyJobStatusWithIcon status={CopyJobStatusType.InProgress} />);
expect(container).toHaveTextContent("In Progress"); expect(container).toHaveTextContent("Running");
}); });
}); });
describe("Icon and Status Mapping", () => { describe("Icon and Status Mapping", () => {
it("renders correct status text based on mapping", () => { it("renders correct status text based on mapping", () => {
const statusMappings = [ const statusMappings = [
{ status: CopyJobStatusType.Pending, expectedText: "Pending" }, { status: CopyJobStatusType.Pending, expectedText: "Queued" },
{ status: CopyJobStatusType.Paused, expectedText: "Paused" }, { status: CopyJobStatusType.Paused, expectedText: "Paused" },
{ status: CopyJobStatusType.Failed, expectedText: "Failed" }, { status: CopyJobStatusType.Failed, expectedText: "Failed" },
{ status: CopyJobStatusType.Completed, expectedText: "Completed" }, { status: CopyJobStatusType.Completed, expectedText: "Completed" },
{ status: CopyJobStatusType.Running, expectedText: "In Progress" }, { status: CopyJobStatusType.Running, expectedText: "Running" },
]; ];
statusMappings.forEach(({ status, expectedText }) => { statusMappings.forEach(({ status, expectedText }) => {

View File

@@ -15,7 +15,7 @@ exports[`CopyJobStatusWithIcon Spinner Status Types renders InProgress with spin
<span <span
class="css-112" class="css-112"
> >
In Progress Running
</span> </span>
</div> </div>
`; `;
@@ -35,7 +35,7 @@ exports[`CopyJobStatusWithIcon Spinner Status Types renders Partitioning with sp
<span <span
class="css-112" class="css-112"
> >
In Progress Running
</span> </span>
</div> </div>
`; `;
@@ -55,7 +55,7 @@ exports[`CopyJobStatusWithIcon Spinner Status Types renders Running with spinner
<span <span
class="css-112" class="css-112"
> >
In Progress Running
</span> </span>
</div> </div>
`; `;
@@ -181,7 +181,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
<span <span
class="css-112" class="css-112"
> >
Pending Queued
</span> </span>
</div> </div>
`; `;

View File

@@ -44,7 +44,9 @@ const MonitorCopyJobs = forwardRef<MonitorCopyJobsRef, MonitorCopyJobsProps>(({
return isEqual(prevJobs, normalizedResponse) ? prevJobs : normalizedResponse; return isEqual(prevJobs, normalizedResponse) ? prevJobs : normalizedResponse;
}); });
} catch (error) { } catch (error) {
if (error.message !== "Previous copy job request was cancelled.") {
setError(error.message || "Failed to load copy jobs. Please try again later."); setError(error.message || "Failed to load copy jobs. Please try again later.");
}
} finally { } finally {
if (isFirstFetchRef.current) { if (isFirstFetchRef.current) {
setLoading(false); setLoading(false);

View File

@@ -56,14 +56,14 @@ export interface CopyJobContextState {
migrationType: CopyJobMigrationType; migrationType: CopyJobMigrationType;
sourceReadAccessFromTarget?: boolean; sourceReadAccessFromTarget?: boolean;
source: { source: {
subscription: Subscription; subscription: Subscription | null;
account: DatabaseAccount; account: DatabaseAccount | null;
databaseId: string; databaseId: string;
containerId: string; containerId: string;
}; };
target: { target: {
subscriptionId: string; subscriptionId: string;
account: DatabaseAccount; account: DatabaseAccount | null;
databaseId: string; databaseId: string;
containerId: string; containerId: string;
}; };