From bc7e8a71cac58f1fe3f372e3198ace491fc455dd Mon Sep 17 00:00:00 2001 From: BChoudhury-ms Date: Mon, 15 Dec 2025 12:25:05 +0530 Subject: [PATCH] Refactor Container Copy dropdowns with integrated state management (#2279) --- .../Actions/CopyJobActions.test.tsx | 2 +- .../ContainerCopy/Actions/CopyJobActions.tsx | 3 +- .../ContainerCopy/ContainerCopyMessages.ts | 8 +- .../Context/CopyJobContext.test.tsx | 15 +- .../ContainerCopy/Context/CopyJobContext.tsx | 7 +- src/Explorer/ContainerCopy/CopyJobUtils.ts | 2 +- .../Components/AccountDropdown.test.tsx | 530 +++++++++++------ .../Components/AccountDropdown.tsx | 94 ++- .../Components/SubscriptionDropdown.test.tsx | 335 ++++++++--- .../Components/SubscriptionDropdown.tsx | 80 ++- .../AccountDropdown.test.tsx.snap | 533 +----------------- .../SubscriptionDropdown.test.tsx.snap | 337 ----------- .../SelectAccount/SelectAccount.test.tsx | 518 ++++------------- .../Screens/SelectAccount/SelectAccount.tsx | 41 +- .../Utils/selectAccountUtils.test.tsx | 526 ----------------- .../Utils/selectAccountUtils.tsx | 80 --- .../__snapshots__/SelectAccount.test.tsx.snap | 524 +---------------- .../Utils/useCopyJobNavigation.ts | 1 + .../Components/CopyJobStatusWithIcon.test.tsx | 8 +- .../CopyJobStatusWithIcon.test.tsx.snap | 8 +- .../MonitorCopyJobs/MonitorCopyJobs.tsx | 4 +- .../ContainerCopy/Types/CopyJobTypes.ts | 6 +- 22 files changed, 955 insertions(+), 2707 deletions(-) delete mode 100644 src/Explorer/ContainerCopy/CreateCopyJob/Screens/SelectAccount/Components/__snapshots__/SubscriptionDropdown.test.tsx.snap delete mode 100644 src/Explorer/ContainerCopy/CreateCopyJob/Screens/SelectAccount/Utils/selectAccountUtils.test.tsx delete mode 100644 src/Explorer/ContainerCopy/CreateCopyJob/Screens/SelectAccount/Utils/selectAccountUtils.tsx diff --git a/src/Explorer/ContainerCopy/Actions/CopyJobActions.test.tsx b/src/Explorer/ContainerCopy/Actions/CopyJobActions.test.tsx index adc404e66..a1885e062 100644 --- a/src/Explorer/ContainerCopy/Actions/CopyJobActions.test.tsx +++ b/src/Explorer/ContainerCopy/Actions/CopyJobActions.test.tsx @@ -433,7 +433,7 @@ describe("CopyJobActions", () => { (dataTransferService.listByDatabaseAccount as jest.Mock).mockRejectedValue(abortError); 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."), }); }); diff --git a/src/Explorer/ContainerCopy/Actions/CopyJobActions.tsx b/src/Explorer/ContainerCopy/Actions/CopyJobActions.tsx index 6ca871a22..e2e6b6fc7 100644 --- a/src/Explorer/ContainerCopy/Actions/CopyJobActions.tsx +++ b/src/Explorer/ContainerCopy/Actions/CopyJobActions.tsx @@ -124,8 +124,7 @@ export const getCopyJobs = async (): Promise => { const errorContent = JSON.stringify(error.content || error.message || error); if (errorContent.includes("signal is aborted without reason")) { throw { - message: - "Please wait for the current fetch request to complete. The previous copy job fetch request was aborted.", + message: "Previous copy job request was cancelled.", }; } else { throw error; diff --git a/src/Explorer/ContainerCopy/ContainerCopyMessages.ts b/src/Explorer/ContainerCopy/ContainerCopyMessages.ts index 936c923b6..27175de68 100644 --- a/src/Explorer/ContainerCopy/ContainerCopyMessages.ts +++ b/src/Explorer/ContainerCopy/ContainerCopyMessages.ts @@ -162,10 +162,10 @@ export default { viewDetails: "View Details", }, Status: { - Pending: "Pending", - InProgress: "In Progress", - Running: "In Progress", - Partitioning: "In Progress", + Pending: "Queued", + InProgress: "Running", + Running: "Running", + Partitioning: "Running", Paused: "Paused", Completed: "Completed", Failed: "Failed", diff --git a/src/Explorer/ContainerCopy/Context/CopyJobContext.test.tsx b/src/Explorer/ContainerCopy/Context/CopyJobContext.test.tsx index d3776f07c..7a0e7d874 100644 --- a/src/Explorer/ContainerCopy/Context/CopyJobContext.test.tsx +++ b/src/Explorer/ContainerCopy/Context/CopyJobContext.test.tsx @@ -59,15 +59,8 @@ describe("CopyJobContext", () => { jobName: "", migrationType: CopyJobMigrationType.Offline, source: { - subscription: { - subscriptionId: "test-subscription-id", - }, - account: { - id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/test-account", - name: "test-account", - location: "East US", - kind: "GlobalDocumentDB", - }, + subscription: null, + account: null, databaseId: "", containerId: "", }, @@ -605,8 +598,8 @@ describe("CopyJobContext", () => { , ); - expect(contextValue.copyJobState.source.subscription.subscriptionId).toBe("test-subscription-id"); - expect(contextValue.copyJobState.source.account.name).toBe("test-account"); + expect(contextValue.copyJobState.source?.subscription?.subscriptionId).toBeUndefined(); + expect(contextValue.copyJobState.source?.account?.name).toBeUndefined(); }); it("should initialize target with userContext values", () => { diff --git a/src/Explorer/ContainerCopy/Context/CopyJobContext.tsx b/src/Explorer/ContainerCopy/Context/CopyJobContext.tsx index dab4bd3c0..ddb936dcf 100644 --- a/src/Explorer/ContainerCopy/Context/CopyJobContext.tsx +++ b/src/Explorer/ContainerCopy/Context/CopyJobContext.tsx @@ -1,5 +1,4 @@ import Explorer from "Explorer/Explorer"; -import { Subscription } from "Contracts/DataModels"; import React from "react"; import { userContext } from "UserContext"; import { CopyJobMigrationType } from "../Enums/CopyJobEnums"; @@ -24,10 +23,8 @@ const getInitialCopyJobState = (): CopyJobContextState => { jobName: "", migrationType: CopyJobMigrationType.Offline, source: { - subscription: { - subscriptionId: userContext.subscriptionId || "", - } as Subscription, - account: userContext.databaseAccount || null, + subscription: null, + account: null, databaseId: "", containerId: "", }, diff --git a/src/Explorer/ContainerCopy/CopyJobUtils.ts b/src/Explorer/ContainerCopy/CopyJobUtils.ts index 75cc4acd2..a84b3d461 100644 --- a/src/Explorer/ContainerCopy/CopyJobUtils.ts +++ b/src/Explorer/ContainerCopy/CopyJobUtils.ts @@ -147,7 +147,7 @@ export function isEqual(prevJobs: CopyJobType[], newJobs: CopyJobType[]): boolea } 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); }; diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/SelectAccount/Components/AccountDropdown.test.tsx b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/SelectAccount/Components/AccountDropdown.test.tsx index 806f80d5b..a25663813 100644 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/SelectAccount/Components/AccountDropdown.test.tsx +++ b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/SelectAccount/Components/AccountDropdown.test.tsx @@ -1,219 +1,409 @@ 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 { 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"; -describe("AccountDropdown", () => { - const mockOnChange = jest.fn(); +jest.mock("../../../../../../hooks/useDatabaseAccounts"); +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[] = [ - { - key: "account-1", - text: "Development Account", - data: { - id: "account-1", - name: "Development Account", - location: "East US", - resourceGroup: "dev-rg", - kind: "GlobalDocumentDB", - properties: { - documentEndpoint: "https://dev-account.documents.azure.com:443/", - provisioningState: "Succeeded", - consistencyPolicy: { - defaultConsistencyLevel: "Session", - }, - }, +const mockUseDatabaseAccounts = useDatabaseAccountsHook.useDatabaseAccounts as jest.MockedFunction< + typeof useDatabaseAccountsHook.useDatabaseAccounts +>; + +describe("AccountDropdown", () => { + const mockSetCopyJobState = jest.fn(); + const mockCopyJobState = { + jobName: "", + migrationType: CopyJobMigrationType.Offline, + source: { + subscription: { + subscriptionId: "test-subscription-id", + displayName: "Test Subscription", }, + account: null, + databaseId: "", + containerId: "", }, - { - key: "account-2", - text: "Production Account", - data: { - id: "account-2", - name: "Production Account", - location: "West US 2", - resourceGroup: "prod-rg", - kind: "GlobalDocumentDB", - properties: { - documentEndpoint: "https://prod-account.documents.azure.com:443/", - provisioningState: "Succeeded", - consistencyPolicy: { - defaultConsistencyLevel: "Strong", - }, - }, - }, + target: { + subscriptionId: "", + account: null, + databaseId: "", + containerId: "", }, - { - key: "account-3", - text: "Testing Account", - data: { - id: "account-3", - name: "Testing Account", - location: "Central US", - resourceGroup: "test-rg", - kind: "GlobalDocumentDB", - properties: { - documentEndpoint: "https://test-account.documents.azure.com:443/", - provisioningState: "Succeeded", - consistencyPolicy: { - defaultConsistencyLevel: "Eventual", - }, - }, - }, + 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", + type: "Microsoft.DocumentDB/databaseAccounts", + tags: {}, + properties: { + documentEndpoint: "https://account1.documents.azure.com:443/", + capabilities: [], + enableMultipleWriteLocations: false, }, - ]; + }; + + const mockDatabaseAccount2: DatabaseAccount = { + id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/account2", + name: "test-account-2", + kind: "GlobalDocumentDB", + location: "West US", + type: "Microsoft.DocumentDB/databaseAccounts", + tags: {}, + properties: { + documentEndpoint: "https://account2.documents.azure.com:443/", + capabilities: [], + enableMultipleWriteLocations: false, + }, + }; + + const mockNonSqlAccount: DatabaseAccount = { + id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/mongo-account", + name: "mongo-account", + kind: "MongoDB", + location: "Central US", + type: "Microsoft.DocumentDB/databaseAccounts", + tags: {}, + properties: { + documentEndpoint: "https://mongo-account.documents.azure.com:443/", + capabilities: [], + enableMultipleWriteLocations: false, + }, + }; + + const renderWithContext = (contextValue = mockCopyJobContextValue) => { + return render( + + + , + ); + }; beforeEach(() => { jest.clearAllMocks(); + (apiType as jest.MockedFunction).mockImplementation((account: DatabaseAccount) => { + return account.kind === "MongoDB" ? "MongoDB" : "SQL"; + }); }); - describe("Snapshot Testing", () => { - it("matches snapshot with all account options", () => { - const { container } = render( - , + describe("Rendering", () => { + it("should render dropdown with correct label and placeholder", () => { + mockUseDatabaseAccounts.mockReturnValue([]); + + 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", () => { - const { container } = render( - , - ); + it("should render disabled dropdown when no subscription is selected", () => { + mockUseDatabaseAccounts.mockReturnValue([]); + const contextWithoutSubscription = { + ...mockCopyJobContextValue, + copyJobState: { + ...mockCopyJobState, + 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", () => { - const { container } = render( - , - ); + it("should render disabled dropdown when no accounts are available", () => { + mockUseDatabaseAccounts.mockReturnValue([]); - expect(container.firstChild).toMatchSnapshot(); + renderWithContext(); + + const dropdown = screen.getByRole("combobox"); + expect(dropdown).toHaveAttribute("aria-disabled", "true"); }); - it("matches snapshot with empty options", () => { - const { container } = render(); + it("should render enabled dropdown when accounts are available", () => { + mockUseDatabaseAccounts.mockReturnValue([mockDatabaseAccount1, mockDatabaseAccount2]); - expect(container.firstChild).toMatchSnapshot(); + renderWithContext(); + + const dropdown = screen.getByRole("combobox"); + expect(dropdown).toHaveAttribute("aria-disabled", "false"); + }); + }); + + describe("Account filtering", () => { + it("should filter accounts to only show SQL API accounts", () => { + const allAccounts = [mockDatabaseAccount1, mockDatabaseAccount2, mockNonSqlAccount]; + mockUseDatabaseAccounts.mockReturnValue(allAccounts); + + renderWithContext(); + + expect(mockUseDatabaseAccounts).toHaveBeenCalledWith("test-subscription-id"); + + expect(apiType as jest.MockedFunction).toHaveBeenCalledWith(mockDatabaseAccount1); + expect(apiType as jest.MockedFunction).toHaveBeenCalledWith(mockDatabaseAccount2); + expect(apiType as jest.MockedFunction).toHaveBeenCalledWith(mockNonSqlAccount); + }); + }); + + describe("Account selection", () => { + it("should auto-select the first SQL account when no account is currently selected", async () => { + 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(mockDatabaseAccount1); }); - it("matches snapshot with single option", () => { - const { container } = render( - , - ); + 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", + }; - expect(container.firstChild).toMatchSnapshot(); + (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("matches snapshot with special characters in options", () => { - const specialOptions = [ - { - key: "special", - text: 'Account with & "characters"', - data: { - id: "special", - name: 'Account with & "characters"', - location: "East US", + 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]); - expect(container.firstChild).toMatchSnapshot(); + 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); }); - it("matches snapshot with long account name", () => { - const longNameOption = [ - { - key: "long", - text: "This is an extremely long account name that tests how the component handles text overflow and layout constraints in the dropdown", - data: { - id: "long", - name: "This is an extremely long account name that tests how the component handles text overflow and layout constraints in the dropdown", - location: "North Central US", + it("should handle account change when user selects different account", async () => { + mockUseDatabaseAccounts.mockReturnValue([mockDatabaseAccount1, mockDatabaseAccount2]); + + renderWithContext(); + + const dropdown = screen.getByRole("combobox"); + fireEvent.click(dropdown); + + await waitFor(() => { + const option = screen.getByText("test-account-2"); + fireEvent.click(option); + }); + + expect(mockSetCopyJobState).toHaveBeenCalledWith(expect.any(Function)); + }); + }); + + describe("ID normalization", () => { + it("should normalize account ID for Portal platform", () => { + const portalAccount = { + ...mockDatabaseAccount1, + id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/account1", + }; + + (configContext as any).platform = Platform.Portal; + mockUseDatabaseAccounts.mockReturnValue([portalAccount]); + + const contextWithSelectedAccount = { + ...mockCopyJobContextValue, + copyJobState: { + ...mockCopyJobState, + source: { + ...mockCopyJobState.source, + account: portalAccount, }, }, - ]; + }; - const { container } = render( - , - ); + renderWithContext(contextWithSelectedAccount); - expect(container.firstChild).toMatchSnapshot(); + const dropdown = screen.getByRole("combobox"); + expect(dropdown).toMatchSnapshot(); }); - it("matches snapshot with disabled state and no selection", () => { - const { container } = render( - , - ); + it("should normalize account ID for Hosted platform", () => { + const hostedAccount = { + ...mockDatabaseAccount1, + id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/account1", + }; - expect(container.firstChild).toMatchSnapshot(); + (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("matches snapshot with multiple account types", () => { - const mixedAccountOptions = [ - { - key: "sql-account", - text: "SQL API Account", - data: { - id: "sql-account", - name: "SQL API Account", - kind: "GlobalDocumentDB", - location: "East US", - }, - }, - { - key: "mongo-account", - text: "MongoDB Account", - data: { - id: "mongo-account", - name: "MongoDB Account", - kind: "MongoDB", - location: "West US", - }, - }, - { - key: "cassandra-account", - text: "Cassandra Account", - data: { - id: "cassandra-account", - name: "Cassandra Account", - kind: "Cassandra", - location: "Central US", - }, - }, - ]; + it("should handle null account list gracefully", () => { + mockUseDatabaseAccounts.mockReturnValue(null as any); - const { container } = render( - , - ); + renderWithContext(); - expect(container.firstChild).toMatchSnapshot(); + 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"); }); }); }); diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/SelectAccount/Components/AccountDropdown.tsx b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/SelectAccount/Components/AccountDropdown.tsx index b24aed7b3..f585c860f 100644 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/SelectAccount/Components/AccountDropdown.tsx +++ b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/SelectAccount/Components/AccountDropdown.tsx @@ -1,31 +1,91 @@ /* eslint-disable react/prop-types */ /* eslint-disable react/display-name */ 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 { DropdownOptionType } from "../../../../Types/CopyJobTypes"; +import { useCopyJobContext } from "../../../../Context/CopyJobContext"; import FieldRow from "../../Components/FieldRow"; -interface AccountDropdownProps { - options: DropdownOptionType[]; - selectedKey?: string; - disabled: boolean; - onChange: (_ev?: React.FormEvent, option?: DropdownOptionType) => void; -} +interface AccountDropdownProps {} -export const AccountDropdown: React.FC = React.memo( - ({ options, selectedKey, disabled, onChange }) => ( +const normalizeAccountId = (id: string) => { + 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 = () => { + 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 ( - ), - (prev, next) => prev.options.length === next.options.length && prev.selectedKey === next.selectedKey, -); + ); +}; diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/SelectAccount/Components/SubscriptionDropdown.test.tsx b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/SelectAccount/Components/SubscriptionDropdown.test.tsx index 4d905a5d2..c9356b8e5 100644 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/SelectAccount/Components/SubscriptionDropdown.test.tsx +++ b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/SelectAccount/Components/SubscriptionDropdown.test.tsx @@ -1,118 +1,295 @@ 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 { DropdownOptionType } from "../../../../Types/CopyJobTypes"; +import { Subscription } from "../../../../../../Contracts/DataModels"; +import Explorer from "../../../../../Explorer"; +import CopyJobContextProvider from "../../../../Context/CopyJobContext"; import { SubscriptionDropdown } from "./SubscriptionDropdown"; -describe("SubscriptionDropdown", () => { - const mockOnChange = jest.fn(); +jest.mock("../../../../../../hooks/useSubscriptions"); +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", - displayName: "Development Subscription", - authorizationSource: "RoleBased", - subscriptionPolicies: { - quotaId: "quota-1", - spendingLimit: "Off", - locationPlacementId: "loc-1", - }, - }, + subscriptionId: "sub-1", + displayName: "Subscription One", + state: "Enabled", + tenantId: "tenant-1", }, { - key: "sub-2", - text: "Production Subscription", - data: { - subscriptionId: "sub-2", - displayName: "Production Subscription", - authorizationSource: "RoleBased", - subscriptionPolicies: { - quotaId: "quota-2", - spendingLimit: "On", - locationPlacementId: "loc-2", - }, - }, + subscriptionId: "sub-2", + displayName: "Subscription Two", + state: "Enabled", + tenantId: "tenant-1", }, { - key: "sub-3", - text: "Testing Subscription", - data: { - subscriptionId: "sub-3", - displayName: "Testing Subscription", - authorizationSource: "Legacy", - subscriptionPolicies: { - quotaId: "quota-3", - spendingLimit: "Off", - locationPlacementId: "loc-3", - }, - }, + subscriptionId: "sub-3", + displayName: "Another Subscription", + state: "Enabled", + tenantId: "tenant-1", }, ]; + const renderWithProvider = (children: React.ReactNode) => { + return render({children}); + }; + beforeEach(() => { jest.clearAllMocks(); + mockExplorer = {} as Explorer; + + mockUseSubscriptions.mockReturnValue(mockSubscriptions); + mockUserContext.subscriptionId = "sub-1"; }); - describe("Snapshot Testing", () => { - it("matches snapshot with all subscription options", () => { - const { container } = render(); + describe("Rendering", () => { + it("should render subscription dropdown with correct attributes", () => { + renderWithProvider(); - 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", () => { - const { container } = render( - , + it("should render field label correctly", () => { + renderWithProvider(); + + expect(screen.getByText("Subscription:")).toBeInTheDocument(); + }); + + it("should show placeholder when no subscription is selected", async () => { + mockUserContext.subscriptionId = ""; + mockUseSubscriptions.mockReturnValue([]); + + renderWithProvider(); + + 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(); + + 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(); + + const dropdown = screen.getByRole("combobox"); + expect(dropdown).toBeInTheDocument(); + expect(dropdown).toHaveTextContent("Select a subscription"); + }); + + it("should handle undefined subscriptions", () => { + mockUseSubscriptions.mockReturnValue(undefined); + + renderWithProvider(); + + 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(); + + 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(); + + 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(); + + 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( + + + , ); - expect(container.firstChild).toMatchSnapshot(); + await waitFor(() => { + const dropdown = screen.getByRole("combobox"); + expect(dropdown).toHaveTextContent("Another Subscription"); + }); }); - it("matches snapshot with empty options", () => { - const { container } = render(); + it("should handle subscription selection change", async () => { + renderWithProvider(); - expect(container.firstChild).toMatchSnapshot(); + 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("matches snapshot with single option", () => { - const { container } = render( - , - ); + it("should not auto-select if target subscription not found in list", async () => { + mockUserContext.subscriptionId = "non-existent-sub"; - expect(container.firstChild).toMatchSnapshot(); + renderWithProvider(); + + await waitFor(() => { + const dropdown = screen.getByRole("combobox"); + expect(dropdown).toHaveTextContent("Select a subscription"); + }); + }); + }); + + describe("Context State Management", () => { + it("should update copyJobState when subscription is selected", async () => { + renderWithProvider(); + + 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("matches snapshot with special characters in options", () => { - const specialOptions = [ - { - key: "special", - text: 'Subscription with & "characters"', - data: { subscriptionId: "special" }, - }, - ]; + it("should reset account when subscription changes", async () => { + renderWithProvider(); - const { container } = render(); + await waitFor(() => { + const dropdown = screen.getByRole("combobox"); + expect(dropdown).toHaveTextContent("Subscription One"); + }); + const dropdown = screen.getByRole("combobox"); + fireEvent.click(dropdown); - expect(container.firstChild).toMatchSnapshot(); + await waitFor(() => { + const option = screen.getByText("Subscription Two"); + fireEvent.click(option); + }); + + await waitFor(() => { + expect(dropdown).toHaveTextContent("Subscription Two"); + }); }); - it("matches snapshot with long subscription name", () => { - const longNameOption = [ - { - key: "long", - text: "This is an extremely long subscription name that tests how the component handles text overflow and layout constraints", - data: { subscriptionId: "long" }, - }, - ]; + it("should not update state if same subscription is selected", async () => { + renderWithProvider(); - const { container } = render( - , - ); + await waitFor(() => { + const dropdown = screen.getByRole("combobox"); + expect(dropdown).toHaveTextContent("Subscription One"); + }); - expect(container.firstChild).toMatchSnapshot(); + 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(); + + const dropdown = screen.getByRole("combobox"); + fireEvent.click(dropdown); + expect(dropdown).toBeInTheDocument(); + }); + + it("should handle subscriptions loading state", () => { + mockUseSubscriptions.mockReturnValue(undefined); + + renderWithProvider(); + + 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(); + + const dropdown = screen.getByRole("combobox"); + expect(dropdown).toBeInTheDocument(); + expect(dropdown).toHaveTextContent("Select a subscription"); }); }); }); diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/SelectAccount/Components/SubscriptionDropdown.tsx b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/SelectAccount/Components/SubscriptionDropdown.tsx index 2627918a6..9d38c2f57 100644 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/SelectAccount/Components/SubscriptionDropdown.tsx +++ b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/SelectAccount/Components/SubscriptionDropdown.tsx @@ -1,29 +1,79 @@ /* eslint-disable react/prop-types */ /* eslint-disable react/display-name */ 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 { DropdownOptionType } from "../../../../Types/CopyJobTypes"; +import { useCopyJobContext } from "../../../../Context/CopyJobContext"; import FieldRow from "../../Components/FieldRow"; -interface SubscriptionDropdownProps { - options: DropdownOptionType[]; - selectedKey?: string; - onChange: (_ev?: React.FormEvent, option?: DropdownOptionType) => void; -} +interface SubscriptionDropdownProps {} -export const SubscriptionDropdown: React.FC = React.memo( - ({ options, selectedKey, onChange }) => ( +export const SubscriptionDropdown: React.FC = React.memo(() => { + 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 ( - ), - (prev, next) => prev.options.length === next.options.length && prev.selectedKey === next.selectedKey, -); + ); +}); diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/SelectAccount/Components/__snapshots__/AccountDropdown.test.tsx.snap b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/SelectAccount/Components/__snapshots__/AccountDropdown.test.tsx.snap index 379e4e0ab..6c33d4711 100644 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/SelectAccount/Components/__snapshots__/AccountDropdown.test.tsx.snap +++ b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/SelectAccount/Components/__snapshots__/AccountDropdown.test.tsx.snap @@ -1,514 +1,37 @@ // 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`] = ` -`; - -exports[`AccountDropdown Snapshot Testing matches snapshot with disabled dropdown 1`] = ` -
-
- -
-
-
- -
-
-
-`; - -exports[`AccountDropdown Snapshot Testing matches snapshot with disabled state and no selection 1`] = ` -
-
- -
-
-
- -
-
-
-`; - -exports[`AccountDropdown Snapshot Testing matches snapshot with empty options 1`] = ` -
-
- -
-
-
- -
-
-
-`; - -exports[`AccountDropdown Snapshot Testing matches snapshot with long account name 1`] = ` -
-
- -
-
-
- -
-
-
-`; - -exports[`AccountDropdown Snapshot Testing matches snapshot with multiple account types 1`] = ` -
-
- -
-
-
- -
-
-
-`; - -exports[`AccountDropdown Snapshot Testing matches snapshot with selected account 1`] = ` -
-
- -
-
-
- -
-
-
-`; - -exports[`AccountDropdown Snapshot Testing matches snapshot with single option 1`] = ` -
-
- -
-
-
- -
-
-
-`; - -exports[`AccountDropdown Snapshot Testing matches snapshot with special characters in options 1`] = ` -
-
- -
-
-
- -
-
+  + +
`; diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/SelectAccount/Components/__snapshots__/SubscriptionDropdown.test.tsx.snap b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/SelectAccount/Components/__snapshots__/SubscriptionDropdown.test.tsx.snap deleted file mode 100644 index 7a9f657be..000000000 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/SelectAccount/Components/__snapshots__/SubscriptionDropdown.test.tsx.snap +++ /dev/null @@ -1,337 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`SubscriptionDropdown Snapshot Testing matches snapshot with all subscription options 1`] = ` -
-
- -
-
-
- -
-
-
-`; - -exports[`SubscriptionDropdown Snapshot Testing matches snapshot with empty options 1`] = ` -
-
- -
-
-
- -
-
-
-`; - -exports[`SubscriptionDropdown Snapshot Testing matches snapshot with long subscription name 1`] = ` -
-
- -
-
-
- -
-
-
-`; - -exports[`SubscriptionDropdown Snapshot Testing matches snapshot with selected subscription 1`] = ` -
-
- -
-
-
- -
-
-
-`; - -exports[`SubscriptionDropdown Snapshot Testing matches snapshot with single option 1`] = ` -
-
- -
-
-
- -
-
-
-`; - -exports[`SubscriptionDropdown Snapshot Testing matches snapshot with special characters in options 1`] = ` -
-
- -
-
-
- -
-
-
-`; diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/SelectAccount/SelectAccount.test.tsx b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/SelectAccount/SelectAccount.test.tsx index 2fdd0529c..5fb556c3c 100644 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/SelectAccount/SelectAccount.test.tsx +++ b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/SelectAccount/SelectAccount.test.tsx @@ -1,480 +1,170 @@ import "@testing-library/jest-dom"; import { fireEvent, render, screen } from "@testing-library/react"; import React from "react"; -import { apiType } from "UserContext"; -import { DatabaseAccount, Subscription } from "../../../../../Contracts/DataModels"; -import { useDatabaseAccounts } from "../../../../../hooks/useDatabaseAccounts"; -import { useSubscriptions } from "../../../../../hooks/useSubscriptions"; +import { useCopyJobContext } from "../../../Context/CopyJobContext"; import { CopyJobMigrationType } from "../../../Enums/CopyJobEnums"; -import { CopyJobContextProviderType, CopyJobContextState } from "../../../Types/CopyJobTypes"; +import { CopyJobContextProviderType } from "../../../Types/CopyJobTypes"; import SelectAccount from "./SelectAccount"; -jest.mock("UserContext", () => ({ - apiType: jest.fn(), -})); - -jest.mock("../../../../../hooks/useDatabaseAccounts"); -jest.mock("../../../../../hooks/useSubscriptions"); jest.mock("../../../Context/CopyJobContext", () => ({ - useCopyJobContext: () => mockContextValue, -})); - -jest.mock("./Utils/selectAccountUtils", () => ({ - useDropdownOptions: jest.fn(), - useEventHandlers: jest.fn(), + useCopyJobContext: jest.fn(), })); jest.mock("./Components/SubscriptionDropdown", () => ({ - SubscriptionDropdown: jest.fn(({ options, selectedKey, onChange, ...props }) => ( -
- {options?.map((option: any) => ( -
onChange?.(undefined, option)} - > - {option.text} -
- ))} -
- )), + SubscriptionDropdown: jest.fn(() =>
Subscription Dropdown
), })); jest.mock("./Components/AccountDropdown", () => ({ - AccountDropdown: jest.fn(({ options, selectedKey, disabled, onChange, ...props }) => ( -
- {options?.map((option: any) => ( -
onChange?.(undefined, option)} - > - {option.text} -
- ))} -
- )), + AccountDropdown: jest.fn(() =>
Account Dropdown
), })); jest.mock("./Components/MigrationTypeCheckbox", () => ({ - MigrationTypeCheckbox: jest.fn(({ checked, onChange, ...props }) => ( -
+ MigrationTypeCheckbox: jest.fn(({ checked, onChange }: { checked: boolean; onChange: () => void }) => ( +
onChange?.(e, e.target.checked)} + onChange={onChange} data-testid="migration-checkbox-input" + aria-label="Migration Type Checkbox" /> + Copy container in offline mode
)), })); -jest.mock("../../../ContainerCopyMessages", () => ({ - selectAccountDescription: "Select your source account and subscription", -})); +describe("SelectAccount", () => { + const mockSetCopyJobState = jest.fn(); -const mockUseDatabaseAccounts = useDatabaseAccounts as jest.MockedFunction; -const mockUseSubscriptions = useSubscriptions as jest.MockedFunction; -const mockApiType = apiType as jest.MockedFunction; - -import { useDropdownOptions, useEventHandlers } from "./Utils/selectAccountUtils"; -const mockUseDropdownOptions = useDropdownOptions as jest.MockedFunction; -const mockUseEventHandlers = useEventHandlers as jest.MockedFunction; - -const mockSubscriptions = [ - { - subscriptionId: "sub-1", - displayName: "Test Subscription 1", - authorizationSource: "RoleBased", - subscriptionPolicies: { - quotaId: "quota-1", - spendingLimit: "Off", - locationPlacementId: "loc-1", + const defaultContextValue: CopyJobContextProviderType = { + copyJobState: { + jobName: "", + migrationType: CopyJobMigrationType.Online, + source: { + subscription: null as any, + account: null as any, + databaseId: "", + containerId: "", + }, + target: { + subscriptionId: "", + account: null as any, + databaseId: "", + containerId: "", + }, + sourceReadAccessFromTarget: false, }, - }, - { - subscriptionId: "sub-2", - displayName: "Test Subscription 2", - authorizationSource: "RoleBased", - subscriptionPolicies: { - quotaId: "quota-2", - spendingLimit: "On", - locationPlacementId: "loc-2", - }, - }, -] as Subscription[]; + setCopyJobState: mockSetCopyJobState, + flow: { currentScreen: "selectAccount" }, + setFlow: jest.fn(), + contextError: null, + setContextError: jest.fn(), + explorer: {} as any, + resetCopyJobState: jest.fn(), + }; -const mockAccounts = [ - { - id: "/subscriptions/sub-1/resourceGroups/rg-1/providers/Microsoft.DocumentDB/databaseAccounts/account-1", - name: "test-cosmos-account-1", - location: "East US", - kind: "GlobalDocumentDB", - properties: { - documentEndpoint: "https://account-1.documents.azure.com/", - capabilities: [], - enableFreeTier: false, - }, - }, - { - id: "/subscriptions/sub-1/resourceGroups/rg-1/providers/Microsoft.DocumentDB/databaseAccounts/account-2", - name: "test-cosmos-account-2", - location: "West US", - kind: "MongoDB", - properties: { - documentEndpoint: "https://account-2.documents.azure.com/", - capabilities: [], - }, - }, -] as DatabaseAccount[]; - -const mockDropdownOptions = { - subscriptionOptions: [ - { key: "sub-1", text: "Test Subscription 1", data: mockSubscriptions[0] }, - { key: "sub-2", text: "Test Subscription 2", data: mockSubscriptions[1] }, - ], - accountOptions: [{ key: mockAccounts[0].id, text: mockAccounts[0].name, data: mockAccounts[0] }], -}; - -const mockEventHandlers = { - handleSelectSourceAccount: jest.fn(), - handleMigrationTypeChange: jest.fn(), -}; - -let mockContextValue = { - copyJobState: { - jobName: "", - migrationType: CopyJobMigrationType.Offline, - source: { - subscription: null, - account: null, - databaseId: "", - containerId: "", - }, - target: { - subscriptionId: "", - account: null, - databaseId: "", - containerId: "", - }, - sourceReadAccessFromTarget: false, - } as CopyJobContextState, - setCopyJobState: jest.fn(), - flow: null, - setFlow: jest.fn(), - contextError: null, - setContextError: jest.fn(), - resetCopyJobState: jest.fn(), - explorer: {} as any, -} as CopyJobContextProviderType; - -describe("SelectAccount Component", () => { beforeEach(() => { jest.clearAllMocks(); - - mockContextValue = { - copyJobState: { - jobName: "", - migrationType: CopyJobMigrationType.Offline, - source: { - subscription: null, - account: null, - databaseId: "", - containerId: "", - }, - target: { - subscriptionId: "", - account: null, - databaseId: "", - containerId: "", - }, - sourceReadAccessFromTarget: false, - } as CopyJobContextState, - setCopyJobState: jest.fn(), - flow: null, - setFlow: jest.fn(), - contextError: null, - setContextError: jest.fn(), - resetCopyJobState: jest.fn(), - explorer: {} as any, - }; - - mockUseSubscriptions.mockReturnValue(mockSubscriptions); - mockUseDatabaseAccounts.mockReturnValue(mockAccounts); - mockApiType.mockReturnValue("SQL"); - mockUseDropdownOptions.mockReturnValue(mockDropdownOptions); - mockUseEventHandlers.mockReturnValue(mockEventHandlers); + (useCopyJobContext as jest.Mock).mockReturnValue(defaultContextValue); }); - describe("Rendering", () => { - it("should render component with default state", () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + describe("Component Rendering", () => { + it("should render the component with all required elements", () => { const { container } = render(); - 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("account-dropdown")).toBeInTheDocument(); expect(screen.getByTestId("migration-type-checkbox")).toBeInTheDocument(); - expect(container).toMatchSnapshot(); }); - it("should render with selected subscription", () => { - mockContextValue.copyJobState.source.subscription = mockSubscriptions[0]; - + it("should render correctly with snapshot", () => { const { container } = render(); - - 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(); - - 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(); - - 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(); - - expect(screen.getByTestId("migration-type-checkbox")).toHaveAttribute("data-checked", "false"); - expect(container).toMatchSnapshot(); + expect(container.firstChild).toMatchSnapshot(); }); }); - describe("Hook Integration", () => { - it("should call useSubscriptions hook", () => { + describe("Migration Type Functionality", () => { + it("should display migration type checkbox as unchecked when migrationType is Online", () => { + (useCopyJobContext as jest.Mock).mockReturnValue({ + ...defaultContextValue, + copyJobState: { + ...defaultContextValue.copyJobState, + migrationType: CopyJobMigrationType.Online, + }, + }); + render(); - expect(mockUseSubscriptions).toHaveBeenCalledTimes(1); + + const checkbox = screen.getByTestId("migration-checkbox-input"); + expect(checkbox).not.toBeChecked(); }); - it("should call useDatabaseAccounts with selected subscription ID", () => { - mockContextValue.copyJobState.source.subscription = mockSubscriptions[0]; + it("should display migration type checkbox as checked when migrationType is Offline", () => { + (useCopyJobContext as jest.Mock).mockReturnValue({ + ...defaultContextValue, + copyJobState: { + ...defaultContextValue.copyJobState, + migrationType: CopyJobMigrationType.Offline, + }, + }); + render(); - expect(mockUseDatabaseAccounts).toHaveBeenCalledWith("sub-1"); + const checkbox = screen.getByTestId("migration-checkbox-input"); + expect(checkbox).toBeChecked(); }); - it("should call useDatabaseAccounts with undefined when no subscription selected", () => { - render(); - expect(mockUseDatabaseAccounts).toHaveBeenCalledWith(undefined); - }); - - it("should filter accounts to SQL API only", () => { - mockApiType.mockReturnValueOnce("SQL").mockReturnValueOnce("Mongo"); - render(); - - 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")); + it("should call setCopyJobState with Online migration type when checkbox is unchecked", () => { + (useCopyJobContext as jest.Mock).mockReturnValue({ + ...defaultContextValue, + copyJobState: { + ...defaultContextValue.copyJobState, + migrationType: CopyJobMigrationType.Offline, + }, + }); render(); - expect(mockUseDropdownOptions).toHaveBeenCalledWith(mockSubscriptions, sqlOnlyAccounts); - }); - - it("should call useEventHandlers with setCopyJobState", () => { - render(); - expect(mockUseEventHandlers).toHaveBeenCalledWith(mockContextValue.setCopyJobState); - }); - }); - - describe("Event Handling", () => { - it("should handle subscription selection", () => { - render(); - - const subscriptionOption = screen.getByTestId("subscription-option-sub-1"); - fireEvent.click(subscriptionOption); - - expect(mockEventHandlers.handleSelectSourceAccount).toHaveBeenCalledWith("subscription", mockSubscriptions[0]); - }); - - it("should handle account selection", () => { - render(); - - const accountOption = screen.getByTestId(`account-option-${mockAccounts[0].id}`); - fireEvent.click(accountOption); - - expect(mockEventHandlers.handleSelectSourceAccount).toHaveBeenCalledWith("account", mockAccounts[0]); - }); - - it("should handle migration type change", () => { - render(); const checkbox = screen.getByTestId("migration-checkbox-input"); fireEvent.click(checkbox); - expect(mockEventHandlers.handleMigrationTypeChange).toHaveBeenCalledWith(expect.any(Object), false); + expect(mockSetCopyJobState).toHaveBeenCalledWith(expect.any(Function)); + + const updateFunction = mockSetCopyJobState.mock.calls[0][0]; + const previousState = { + ...defaultContextValue.copyJobState, + migrationType: CopyJobMigrationType.Offline, + }; + const result = updateFunction(previousState); + + expect(result).toEqual({ + ...previousState, + migrationType: CopyJobMigrationType.Online, + }); }); }); - describe("Dropdown States", () => { - it("should disable account dropdown when no subscription is selected", () => { - render(); + describe("Performance and Optimization", () => { + it("should maintain referential equality of handler functions between renders", async () => { + const { rerender } = render(); - expect(screen.getByTestId("account-dropdown")).toHaveAttribute("data-disabled", "true"); - }); + const migrationCheckbox = (await import("./Components/MigrationTypeCheckbox")).MigrationTypeCheckbox as jest.Mock; + const firstRenderHandler = migrationCheckbox.mock.calls[migrationCheckbox.mock.calls.length - 1][0].onChange; - it("should enable account dropdown when subscription is selected", () => { - mockContextValue.copyJobState.source.subscription = mockSubscriptions[0]; + rerender(); - render(); + const secondRenderHandler = migrationCheckbox.mock.calls[migrationCheckbox.mock.calls.length - 1][0].onChange; - expect(screen.getByTestId("account-dropdown")).toHaveAttribute("data-disabled", "false"); - }); - }); - - describe("Component Props", () => { - it("should pass correct props to SubscriptionDropdown", () => { - render(); - - const dropdown = screen.getByTestId("subscription-dropdown"); - expect(dropdown).not.toHaveAttribute("data-selected"); - }); - - it("should pass selected subscription ID to SubscriptionDropdown", () => { - mockContextValue.copyJobState.source.subscription = mockSubscriptions[0]; - - render(); - - const dropdown = screen.getByTestId("subscription-dropdown"); - expect(dropdown).toHaveAttribute("data-selected", "sub-1"); - }); - - it("should pass correct props to AccountDropdown", () => { - render(); - - 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(); - - 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(); - - 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(); - expect(container).toMatchSnapshot(); - }); - - it("should handle empty accounts array", () => { - mockUseDatabaseAccounts.mockReturnValue([]); - mockUseDropdownOptions.mockReturnValue({ - subscriptionOptions: mockDropdownOptions.subscriptionOptions, - accountOptions: [], - }); - - const { container } = render(); - expect(container).toMatchSnapshot(); - }); - - it("should handle null subscription in context", () => { - mockContextValue.copyJobState.source.subscription = null; - - const { container } = render(); - expect(container).toMatchSnapshot(); - }); - - it("should handle null account in context", () => { - mockContextValue.copyJobState.source.account = null; - - const { container } = render(); - expect(container).toMatchSnapshot(); - }); - - it("should handle undefined subscriptions from hook", () => { - mockUseSubscriptions.mockReturnValue(undefined as any); - mockUseDropdownOptions.mockReturnValue({ - subscriptionOptions: [], - accountOptions: [], - }); - - const { container } = render(); - expect(container).toMatchSnapshot(); - }); - - it("should handle undefined accounts from hook", () => { - mockUseDatabaseAccounts.mockReturnValue(undefined as any); - mockUseDropdownOptions.mockReturnValue({ - subscriptionOptions: mockDropdownOptions.subscriptionOptions, - accountOptions: [], - }); - - const { container } = render(); - 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(); - 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(); - - 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(); + expect(firstRenderHandler).toBe(secondRenderHandler); }); }); }); diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/SelectAccount/SelectAccount.tsx b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/SelectAccount/SelectAccount.tsx index 17f323413..ba1072de7 100644 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/SelectAccount/SelectAccount.tsx +++ b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/SelectAccount/SelectAccount.tsx @@ -1,52 +1,37 @@ -/* eslint-disable react/display-name */ -import { Stack } from "@fluentui/react"; +import { Stack, Text } from "@fluentui/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 { useCopyJobContext } from "../../../Context/CopyJobContext"; import { CopyJobMigrationType } from "../../../Enums/CopyJobEnums"; import { AccountDropdown } from "./Components/AccountDropdown"; import { MigrationTypeCheckbox } from "./Components/MigrationTypeCheckbox"; import { SubscriptionDropdown } from "./Components/SubscriptionDropdown"; -import { useDropdownOptions, useEventHandlers } from "./Utils/selectAccountUtils"; const SelectAccount = React.memo(() => { const { copyJobState, setCopyJobState } = useCopyJobContext(); - const selectedSubscriptionId = copyJobState?.source?.subscription?.subscriptionId; - const selectedSourceAccountId = copyJobState?.source?.account?.id; - const subscriptions: Subscription[] = useSubscriptions(); - const allAccounts: DatabaseAccount[] = useDatabaseAccounts(selectedSubscriptionId); - const sqlApiOnlyAccounts: DatabaseAccount[] = allAccounts?.filter((account) => apiType(account) === "SQL"); - - const { subscriptionOptions, accountOptions } = useDropdownOptions(subscriptions, sqlApiOnlyAccounts); - const { handleSelectSourceAccount, handleMigrationTypeChange } = useEventHandlers(setCopyJobState); + const handleMigrationTypeChange = (_ev?: React.FormEvent, checked?: boolean) => { + setCopyJobState((prevState) => ({ + ...prevState, + migrationType: checked ? CopyJobMigrationType.Offline : CopyJobMigrationType.Online, + })); + }; const migrationTypeChecked = copyJobState?.migrationType === CopyJobMigrationType.Offline; return ( - - {ContainerCopyMessages.selectAccountDescription} + + {ContainerCopyMessages.selectAccountDescription} - handleSelectSourceAccount("subscription", option?.data)} - /> + - handleSelectSourceAccount("account", option?.data)} - /> + ); }); +SelectAccount.displayName = "SelectAccount"; + export default SelectAccount; diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/SelectAccount/Utils/selectAccountUtils.test.tsx b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/SelectAccount/Utils/selectAccountUtils.test.tsx deleted file mode 100644 index 5b1ef6028..000000000 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/SelectAccount/Utils/selectAccountUtils.test.tsx +++ /dev/null @@ -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 ( -
-
{result.subscriptionOptions.length}
-
{result.accountOptions.length}
-
- ); -}; - -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 ( -
- - - -
- ); -}; - -describe("selectAccountUtils", () => { - describe("useDropdownOptions", () => { - it("should return empty arrays when subscriptions and accounts are undefined", () => { - let capturedResult: any; - - render( - { - capturedResult = result; - }} - />, - ); - - expect(capturedResult).toEqual({ - subscriptionOptions: [], - accountOptions: [], - }); - }); - - it("should return empty arrays when subscriptions and accounts are empty arrays", () => { - let capturedResult: any; - - render( - { - capturedResult = result; - }} - />, - ); - - expect(capturedResult).toEqual({ - subscriptionOptions: [], - accountOptions: [], - }); - }); - - it("should transform subscriptions into dropdown options correctly", () => { - let capturedResult: any; - - render( - { - 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( - { - 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( - { - 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( - , - ); - - fireEvent.click(getByTestId("select-subscription-button")); - - expect(mockSetCopyJobState).toHaveBeenCalledWith(expect.any(Function)); - expect(mockSetValidationCache).toHaveBeenCalledWith(new Map()); - - 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( - , - ); - - fireEvent.click(getByTestId("select-account-button")); - - expect(mockSetCopyJobState).toHaveBeenCalledWith(expect.any(Function)); - expect(mockSetValidationCache).toHaveBeenCalledWith(new Map()); - - 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( - { - 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( - { - 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(); - - fireEvent.click(getByTestId("migration-type-button")); - - expect(mockSetCopyJobState).toHaveBeenCalledWith(expect.any(Function)); - expect(mockSetValidationCache).toHaveBeenCalledWith(new Map()); - - 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( - { - 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( - { - 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( - { - 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); - }); - }); -}); diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/SelectAccount/Utils/selectAccountUtils.tsx b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/SelectAccount/Utils/selectAccountUtils.tsx deleted file mode 100644 index b054e63d3..000000000 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/SelectAccount/Utils/selectAccountUtils.tsx +++ /dev/null @@ -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()); - }; - - const handleMigrationTypeChange = React.useCallback((_ev?: React.FormEvent, checked?: boolean) => { - setCopyJobState((prevState: CopyJobContextState) => ({ - ...prevState, - migrationType: checked ? CopyJobMigrationType.Offline : CopyJobMigrationType.Online, - })); - setValidationCache(new Map()); - }, []); - - return { handleSelectSourceAccount, handleMigrationTypeChange }; -} diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/SelectAccount/__snapshots__/SelectAccount.test.tsx.snap b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/SelectAccount/__snapshots__/SelectAccount.test.tsx.snap index b40198330..90a8ddc2b 100644 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/SelectAccount/__snapshots__/SelectAccount.test.tsx.snap +++ b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/SelectAccount/__snapshots__/SelectAccount.test.tsx.snap @@ -1,510 +1,34 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`SelectAccount Component Complete Workflow should render complete workflow with all selections 1`] = ` -
-
+ - - Select your source account and subscription - -
-
- Test Subscription 1 -
-
- Test Subscription 2 -
-
-
-
- test-cosmos-account-1 -
-
-
- -
+ Please select a source account from which to copy. +
+
+ Subscription Dropdown
-
-`; - -exports[`SelectAccount Component Edge Cases should handle empty accounts array 1`] = ` -
- - Select your source account and subscription - -
-
- Test Subscription 1 -
-
- Test Subscription 2 -
-
-
+
+ -
- -
-
-
-`; - -exports[`SelectAccount Component Edge Cases should handle empty subscriptions array 1`] = ` -
-
- - Select your source account and subscription - -
-
-
- -
-
-
-`; - -exports[`SelectAccount Component Edge Cases should handle null account in context 1`] = ` -
-
- - Select your source account and subscription - -
-
- Test Subscription 1 -
-
- Test Subscription 2 -
-
-
-
- test-cosmos-account-1 -
-
-
- -
-
-
-`; - -exports[`SelectAccount Component Edge Cases should handle null subscription in context 1`] = ` -
-
- - Select your source account and subscription - -
-
- Test Subscription 1 -
-
- Test Subscription 2 -
-
-
-
- test-cosmos-account-1 -
-
-
- -
-
-
-`; - -exports[`SelectAccount Component Edge Cases should handle undefined accounts from hook 1`] = ` -
-
- - Select your source account and subscription - -
-
- Test Subscription 1 -
-
- Test Subscription 2 -
-
-
-
- -
-
-
-`; - -exports[`SelectAccount Component Edge Cases should handle undefined subscriptions from hook 1`] = ` -
-
- - Select your source account and subscription - -
-
-
- -
-
-
-`; - -exports[`SelectAccount Component Rendering should render component with default state 1`] = ` -
-
- - Select your source account and subscription - -
-
- Test Subscription 1 -
-
- Test Subscription 2 -
-
-
-
- test-cosmos-account-1 -
-
-
- -
-
-
-`; - -exports[`SelectAccount Component Rendering should render with offline migration type checked 1`] = ` -
-
- - Select your source account and subscription - -
-
- Test Subscription 1 -
-
- Test Subscription 2 -
-
-
-
- test-cosmos-account-1 -
-
-
- -
-
-
-`; - -exports[`SelectAccount Component Rendering should render with online migration type unchecked 1`] = ` -
-
- - Select your source account and subscription - -
-
- Test Subscription 1 -
-
- Test Subscription 2 -
-
-
-
- test-cosmos-account-1 -
-
-
- -
-
-
-`; - -exports[`SelectAccount Component Rendering should render with selected account 1`] = ` -
-
- - Select your source account and subscription - -
-
- Test Subscription 1 -
-
- Test Subscription 2 -
-
-
-
- test-cosmos-account-1 -
-
-
- -
-
-
-`; - -exports[`SelectAccount Component Rendering should render with selected subscription 1`] = ` -
-
- - Select your source account and subscription - -
-
- Test Subscription 1 -
-
- Test Subscription 2 -
-
-
-
- test-cosmos-account-1 -
-
-
- -
+ Copy container in offline mode
`; diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Utils/useCopyJobNavigation.ts b/src/Explorer/ContainerCopy/CreateCopyJob/Utils/useCopyJobNavigation.ts index dd8059547..6419f9471 100644 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Utils/useCopyJobNavigation.ts +++ b/src/Explorer/ContainerCopy/CreateCopyJob/Utils/useCopyJobNavigation.ts @@ -39,6 +39,7 @@ export function useCopyJobNavigation() { const [state, dispatch] = useReducer(navigationReducer, { screenHistory: [SCREEN_KEYS.SelectAccount] }); const handlePrevious = useCallback(() => { + setContextError(null); dispatch({ type: "PREVIOUS" }); }, [dispatch]); diff --git a/src/Explorer/ContainerCopy/MonitorCopyJobs/Components/CopyJobStatusWithIcon.test.tsx b/src/Explorer/ContainerCopy/MonitorCopyJobs/Components/CopyJobStatusWithIcon.test.tsx index 1c4528e68..836ce3643 100644 --- a/src/Explorer/ContainerCopy/MonitorCopyJobs/Components/CopyJobStatusWithIcon.test.tsx +++ b/src/Explorer/ContainerCopy/MonitorCopyJobs/Components/CopyJobStatusWithIcon.test.tsx @@ -52,7 +52,7 @@ describe("CopyJobStatusWithIcon", () => { const spinner = container.querySelector('[class*="ms-Spinner"]'); expect(spinner).toBeInTheDocument(); - expect(container).toHaveTextContent("In Progress"); + expect(container).toHaveTextContent("Running"); expect(container.firstChild).toMatchSnapshot(); }); }); @@ -83,18 +83,18 @@ describe("CopyJobStatusWithIcon", () => { it("provides meaningful text content for screen readers", () => { const { container } = render(); - expect(container).toHaveTextContent("In Progress"); + expect(container).toHaveTextContent("Running"); }); }); describe("Icon and Status Mapping", () => { it("renders correct status text based on mapping", () => { const statusMappings = [ - { status: CopyJobStatusType.Pending, expectedText: "Pending" }, + { status: CopyJobStatusType.Pending, expectedText: "Queued" }, { status: CopyJobStatusType.Paused, expectedText: "Paused" }, { status: CopyJobStatusType.Failed, expectedText: "Failed" }, { status: CopyJobStatusType.Completed, expectedText: "Completed" }, - { status: CopyJobStatusType.Running, expectedText: "In Progress" }, + { status: CopyJobStatusType.Running, expectedText: "Running" }, ]; statusMappings.forEach(({ status, expectedText }) => { diff --git a/src/Explorer/ContainerCopy/MonitorCopyJobs/Components/__snapshots__/CopyJobStatusWithIcon.test.tsx.snap b/src/Explorer/ContainerCopy/MonitorCopyJobs/Components/__snapshots__/CopyJobStatusWithIcon.test.tsx.snap index 003766a26..9940ee7e9 100644 --- a/src/Explorer/ContainerCopy/MonitorCopyJobs/Components/__snapshots__/CopyJobStatusWithIcon.test.tsx.snap +++ b/src/Explorer/ContainerCopy/MonitorCopyJobs/Components/__snapshots__/CopyJobStatusWithIcon.test.tsx.snap @@ -15,7 +15,7 @@ exports[`CopyJobStatusWithIcon Spinner Status Types renders InProgress with spin - In Progress + Running
`; @@ -35,7 +35,7 @@ exports[`CopyJobStatusWithIcon Spinner Status Types renders Partitioning with sp - In Progress + Running
`; @@ -55,7 +55,7 @@ exports[`CopyJobStatusWithIcon Spinner Status Types renders Running with spinner - In Progress + Running
`; @@ -181,7 +181,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders - Pending + Queued
`; diff --git a/src/Explorer/ContainerCopy/MonitorCopyJobs/MonitorCopyJobs.tsx b/src/Explorer/ContainerCopy/MonitorCopyJobs/MonitorCopyJobs.tsx index fc4085189..56ec498f8 100644 --- a/src/Explorer/ContainerCopy/MonitorCopyJobs/MonitorCopyJobs.tsx +++ b/src/Explorer/ContainerCopy/MonitorCopyJobs/MonitorCopyJobs.tsx @@ -44,7 +44,9 @@ const MonitorCopyJobs = forwardRef(({ return isEqual(prevJobs, normalizedResponse) ? prevJobs : normalizedResponse; }); } catch (error) { - setError(error.message || "Failed to load copy jobs. Please try again later."); + if (error.message !== "Previous copy job request was cancelled.") { + setError(error.message || "Failed to load copy jobs. Please try again later."); + } } finally { if (isFirstFetchRef.current) { setLoading(false); diff --git a/src/Explorer/ContainerCopy/Types/CopyJobTypes.ts b/src/Explorer/ContainerCopy/Types/CopyJobTypes.ts index e9ebbd0da..85ef612fb 100644 --- a/src/Explorer/ContainerCopy/Types/CopyJobTypes.ts +++ b/src/Explorer/ContainerCopy/Types/CopyJobTypes.ts @@ -56,14 +56,14 @@ export interface CopyJobContextState { migrationType: CopyJobMigrationType; sourceReadAccessFromTarget?: boolean; source: { - subscription: Subscription; - account: DatabaseAccount; + subscription: Subscription | null; + account: DatabaseAccount | null; databaseId: string; containerId: string; }; target: { subscriptionId: string; - account: DatabaseAccount; + account: DatabaseAccount | null; databaseId: string; containerId: string; };