Compare commits

...

3 Commits

Author SHA1 Message Date
Sakshi Gupta
156f748b5e removed NotebookViewer file 2025-12-15 23:45:45 +05:30
BChoudhury-ms
bc7e8a71ca Refactor Container Copy dropdowns with integrated state management (#2279) 2025-12-15 12:25:05 +05:30
asier-isayas
d67c1a0464 Add playwright tests (#2274)
* Add playwright tests for Autoscale/Manual Throughpout and TTL

* fix unit tests and lint

* fix unit tests

* fix tests

* fix autoscale selector

* changed throughput above limit

---------

Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2025-12-10 11:02:31 -08:00
36 changed files with 1270 additions and 2812 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 = {
location: "East US", jobName: "",
resourceGroup: "dev-rg", migrationType: CopyJobMigrationType.Offline,
kind: "GlobalDocumentDB", source: {
properties: { subscription: {
documentEndpoint: "https://dev-account.documents.azure.com:443/", subscriptionId: "test-subscription-id",
provisioningState: "Succeeded", displayName: "Test Subscription",
consistencyPolicy: {
defaultConsistencyLevel: "Session",
},
},
}, },
account: null,
databaseId: "",
containerId: "",
}, },
{ target: {
key: "account-2", subscriptionId: "",
text: "Production Account", account: null,
data: { databaseId: "",
id: "account-2", containerId: "",
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",
},
},
},
}, },
{ sourceReadAccessFromTarget: false,
key: "account-3", } as CopyJobContextState;
text: "Testing Account",
data: { const mockCopyJobContextValue = {
id: "account-3", copyJobState: mockCopyJobState,
name: "Testing Account", setCopyJobState: mockSetCopyJobState,
location: "Central US", flow: null,
resourceGroup: "test-rg", setFlow: jest.fn(),
kind: "GlobalDocumentDB", contextError: null,
properties: { setContextError: jest.fn(),
documentEndpoint: "https://test-account.documents.azure.com:443/", resetCopyJobState: jest.fn(),
provisioningState: "Succeeded", } as CopyJobContextProviderType;
consistencyPolicy: {
defaultConsistencyLevel: "Eventual", 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(
<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");
});
});
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<any>).toHaveBeenCalledWith(mockDatabaseAccount1);
expect(apiType as jest.MockedFunction<any>).toHaveBeenCalledWith(mockDatabaseAccount2);
expect(apiType as jest.MockedFunction<any>).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", () => { it("should auto-select predefined account from userContext if available", async () => {
const { container } = render( const userContextAccount = {
<AccountDropdown ...mockDatabaseAccount2,
options={[mockAccountOptions[0]]} id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/account2",
selectedKey="account-1" };
disabled={false}
onChange={mockOnChange}
/>,
);
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", () => { it("should keep current account if it exists in the filtered list", async () => {
const specialOptions = [ const contextWithSelectedAccount = {
{ ...mockCopyJobContextValue,
key: "special", copyJobState: {
text: 'Account with & <special> "characters"', ...mockCopyJobState,
data: { source: {
id: "special", ...mockCopyJobState.source,
name: 'Account with & <special> "characters"', account: mockDatabaseAccount1,
location: "East US",
}, },
}, },
]; };
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));
});
const stateUpdateFunction = mockSetCopyJobState.mock.calls[0][0];
const newState = stateUpdateFunction(contextWithSelectedAccount.copyJobState);
expect(newState).toBe(contextWithSelectedAccount.copyJobState);
}); });
it("matches snapshot with long account name", () => { it("should handle account change when user selects different account", async () => {
const longNameOption = [ mockUseDatabaseAccounts.mockReturnValue([mockDatabaseAccount1, mockDatabaseAccount2]);
{
key: "long", renderWithContext();
text: "This is an extremely long account name that tests how the component handles text overflow and layout constraints in the dropdown",
data: { const dropdown = screen.getByRole("combobox");
id: "long", fireEvent.click(dropdown);
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", 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);
<AccountDropdown options={longNameOption} selectedKey="long" disabled={false} onChange={mockOnChange} />,
);
expect(container.firstChild).toMatchSnapshot(); const dropdown = screen.getByRole("combobox");
expect(dropdown).toMatchSnapshot();
}); });
it("matches snapshot with disabled state and no selection", () => { it("should normalize account ID for Hosted platform", () => {
const { container } = render( const hostedAccount = {
<AccountDropdown options={mockAccountOptions} disabled={true} onChange={mockOnChange} />, ...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", () => { it("should handle null account list gracefully", () => {
const mixedAccountOptions = [ mockUseDatabaseAccounts.mockReturnValue(null as any);
{
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( renderWithContext();
<AccountDropdown
options={mixedAccountOptions}
selectedKey="mongo-account"
disabled={false}
onChange={mockOnChange}
/>,
);
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");
}); });
}); });
}); });

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", subscriptionId: "sub-1",
text: "Development Subscription", displayName: "Subscription One",
data: { state: "Enabled",
subscriptionId: "sub-1", tenantId: "tenant-1",
displayName: "Development Subscription",
authorizationSource: "RoleBased",
subscriptionPolicies: {
quotaId: "quota-1",
spendingLimit: "Off",
locationPlacementId: "loc-1",
},
},
}, },
{ {
key: "sub-2", subscriptionId: "sub-2",
text: "Production Subscription", displayName: "Subscription Two",
data: { state: "Enabled",
subscriptionId: "sub-2", tenantId: "tenant-1",
displayName: "Production Subscription",
authorizationSource: "RoleBased",
subscriptionPolicies: {
quotaId: "quota-2",
spendingLimit: "On",
locationPlacementId: "loc-2",
},
},
}, },
{ {
key: "sub-3", subscriptionId: "sub-3",
text: "Testing Subscription", displayName: "Another Subscription",
data: { state: "Enabled",
subscriptionId: "sub-3", tenantId: "tenant-1",
displayName: "Testing Subscription",
authorizationSource: "Legacy",
subscriptionPolicies: {
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);
});
await waitFor(() => {
expect(dropdown).toHaveTextContent("Subscription Two");
});
}); });
it("matches snapshot with single option", () => { it("should not auto-select if target subscription not found in list", async () => {
const { container } = render( mockUserContext.subscriptionId = "non-existent-sub";
<SubscriptionDropdown options={[mockSubscriptionOptions[0]]} selectedKey="sub-1" onChange={mockOnChange} />,
);
expect(container.firstChild).toMatchSnapshot(); renderWithProvider(<SubscriptionDropdown />);
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(<SubscriptionDropdown />);
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", () => { it("should reset account when subscription changes", async () => {
const specialOptions = [ renderWithProvider(<SubscriptionDropdown />);
{
key: "special",
text: 'Subscription with & <special> "characters"',
data: { subscriptionId: "special" },
},
];
const { container } = render(<SubscriptionDropdown options={specialOptions} onChange={mockOnChange} />); 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", () => { it("should not update state if same subscription is selected", async () => {
const longNameOption = [ renderWithProvider(<SubscriptionDropdown />);
{
key: "long",
text: "This is an extremely long subscription name that tests how the component handles text overflow and layout constraints",
data: { subscriptionId: "long" },
},
];
const { container } = render( await waitFor(() => {
<SubscriptionDropdown options={longNameOption} selectedKey="long" onChange={mockOnChange} />, 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(<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 <div
class="ms-Stack flex-row css-109" aria-disabled="false"
aria-expanded="false"
aria-haspopup="listbox"
aria-label="Account"
aria-required="true"
class="ms-Dropdown is-required dropdown-132"
data-is-focusable="true"
data-ktp-target="true"
data-test="account-dropdown"
id="Dropdown21"
role="combobox"
tabindex="0"
> >
<div <span
class="ms-StackItem flex-fixed-width css-110" aria-invalid="false"
class="ms-Dropdown-title title-137"
id="Dropdown21-option"
> >
<label test-account-1
class="field-label " </span>
> <span
Account class="ms-Dropdown-caretDownWrapper caretDownWrapper-134"
:
</label>
</div>
<div
class="ms-StackItem flex-grow-col css-110"
> >
<div <i
class="ms-Dropdown-container" aria-hidden="true"
class="ms-Dropdown-caretDown caretDown-136"
data-icon-name="ChevronDown"
> >
<div
aria-disabled="false" </i>
aria-expanded="false" </span>
aria-haspopup="listbox"
aria-label="Account"
aria-required="true"
class="ms-Dropdown is-required dropdown-111"
data-is-focusable="true"
data-ktp-target="true"
id="Dropdown0"
role="combobox"
tabindex="0"
>
<span
aria-invalid="false"
class="ms-Dropdown-title ms-Dropdown-titleIsPlaceHolder title-112"
id="Dropdown0-option"
>
Select an account
</span>
<span
class="ms-Dropdown-caretDownWrapper caretDownWrapper-113"
>
<i
aria-hidden="true"
class="ms-Dropdown-caretDown caretDown-131"
data-icon-name="ChevronDown"
>
</i>
</span>
</div>
</div>
</div>
</div>
`;
exports[`AccountDropdown Snapshot Testing matches snapshot with disabled dropdown 1`] = `
<div
class="ms-Stack flex-row css-109"
>
<div
class="ms-StackItem flex-fixed-width css-110"
>
<label
class="field-label "
>
Account
:
</label>
</div>
<div
class="ms-StackItem flex-grow-col css-110"
>
<div
class="ms-Dropdown-container"
>
<div
aria-disabled="true"
aria-expanded="false"
aria-haspopup="listbox"
aria-label="Account"
aria-required="true"
class="ms-Dropdown is-disabled is-required dropdown-133"
data-is-focusable="false"
data-ktp-target="true"
id="Dropdown2"
role="combobox"
tabindex="-1"
>
<span
aria-invalid="false"
class="ms-Dropdown-title title-138"
id="Dropdown2-option"
>
Development Account
</span>
<span
class="ms-Dropdown-caretDownWrapper caretDownWrapper-135"
>
<i
aria-hidden="true"
class="ms-Dropdown-caretDown caretDown-137"
data-icon-name="ChevronDown"
>
</i>
</span>
</div>
</div>
</div>
</div>
`;
exports[`AccountDropdown Snapshot Testing matches snapshot with disabled state and no selection 1`] = `
<div
class="ms-Stack flex-row css-109"
>
<div
class="ms-StackItem flex-fixed-width css-110"
>
<label
class="field-label "
>
Account
:
</label>
</div>
<div
class="ms-StackItem flex-grow-col css-110"
>
<div
class="ms-Dropdown-container"
>
<div
aria-disabled="true"
aria-expanded="false"
aria-haspopup="listbox"
aria-label="Account"
aria-required="true"
class="ms-Dropdown is-disabled is-required dropdown-133"
data-is-focusable="false"
data-ktp-target="true"
id="Dropdown7"
role="combobox"
tabindex="-1"
>
<span
aria-invalid="false"
class="ms-Dropdown-title ms-Dropdown-titleIsPlaceHolder title-134"
id="Dropdown7-option"
>
Select an account
</span>
<span
class="ms-Dropdown-caretDownWrapper caretDownWrapper-135"
>
<i
aria-hidden="true"
class="ms-Dropdown-caretDown caretDown-137"
data-icon-name="ChevronDown"
>
</i>
</span>
</div>
</div>
</div>
</div>
`;
exports[`AccountDropdown Snapshot Testing matches snapshot with empty options 1`] = `
<div
class="ms-Stack flex-row css-109"
>
<div
class="ms-StackItem flex-fixed-width css-110"
>
<label
class="field-label "
>
Account
:
</label>
</div>
<div
class="ms-StackItem flex-grow-col css-110"
>
<div
class="ms-Dropdown-container"
>
<div
aria-disabled="false"
aria-expanded="false"
aria-haspopup="listbox"
aria-label="Account"
aria-required="true"
class="ms-Dropdown is-required dropdown-111"
data-is-focusable="true"
data-ktp-target="true"
id="Dropdown3"
role="combobox"
tabindex="0"
>
<span
aria-invalid="false"
class="ms-Dropdown-title ms-Dropdown-titleIsPlaceHolder title-112"
id="Dropdown3-option"
>
Select an account
</span>
<span
class="ms-Dropdown-caretDownWrapper caretDownWrapper-113"
>
<i
aria-hidden="true"
class="ms-Dropdown-caretDown caretDown-131"
data-icon-name="ChevronDown"
>
</i>
</span>
</div>
</div>
</div>
</div>
`;
exports[`AccountDropdown Snapshot Testing matches snapshot with long account name 1`] = `
<div
class="ms-Stack flex-row css-109"
>
<div
class="ms-StackItem flex-fixed-width css-110"
>
<label
class="field-label "
>
Account
:
</label>
</div>
<div
class="ms-StackItem flex-grow-col css-110"
>
<div
class="ms-Dropdown-container"
>
<div
aria-disabled="false"
aria-expanded="false"
aria-haspopup="listbox"
aria-label="Account"
aria-required="true"
class="ms-Dropdown is-required dropdown-111"
data-is-focusable="true"
data-ktp-target="true"
id="Dropdown6"
role="combobox"
tabindex="0"
>
<span
aria-invalid="false"
class="ms-Dropdown-title title-132"
id="Dropdown6-option"
>
This is an extremely long account name that tests how the component handles text overflow and layout constraints in the dropdown
</span>
<span
class="ms-Dropdown-caretDownWrapper caretDownWrapper-113"
>
<i
aria-hidden="true"
class="ms-Dropdown-caretDown caretDown-131"
data-icon-name="ChevronDown"
>
</i>
</span>
</div>
</div>
</div>
</div>
`;
exports[`AccountDropdown Snapshot Testing matches snapshot with multiple account types 1`] = `
<div
class="ms-Stack flex-row css-109"
>
<div
class="ms-StackItem flex-fixed-width css-110"
>
<label
class="field-label "
>
Account
:
</label>
</div>
<div
class="ms-StackItem flex-grow-col css-110"
>
<div
class="ms-Dropdown-container"
>
<div
aria-disabled="false"
aria-expanded="false"
aria-haspopup="listbox"
aria-label="Account"
aria-required="true"
class="ms-Dropdown is-required dropdown-111"
data-is-focusable="true"
data-ktp-target="true"
id="Dropdown8"
role="combobox"
tabindex="0"
>
<span
aria-invalid="false"
class="ms-Dropdown-title title-132"
id="Dropdown8-option"
>
MongoDB Account
</span>
<span
class="ms-Dropdown-caretDownWrapper caretDownWrapper-113"
>
<i
aria-hidden="true"
class="ms-Dropdown-caretDown caretDown-131"
data-icon-name="ChevronDown"
>
</i>
</span>
</div>
</div>
</div>
</div>
`;
exports[`AccountDropdown Snapshot Testing matches snapshot with selected account 1`] = `
<div
class="ms-Stack flex-row css-109"
>
<div
class="ms-StackItem flex-fixed-width css-110"
>
<label
class="field-label "
>
Account
:
</label>
</div>
<div
class="ms-StackItem flex-grow-col css-110"
>
<div
class="ms-Dropdown-container"
>
<div
aria-disabled="false"
aria-expanded="false"
aria-haspopup="listbox"
aria-label="Account"
aria-required="true"
class="ms-Dropdown is-required dropdown-111"
data-is-focusable="true"
data-ktp-target="true"
id="Dropdown1"
role="combobox"
tabindex="0"
>
<span
aria-invalid="false"
class="ms-Dropdown-title title-132"
id="Dropdown1-option"
>
Production Account
</span>
<span
class="ms-Dropdown-caretDownWrapper caretDownWrapper-113"
>
<i
aria-hidden="true"
class="ms-Dropdown-caretDown caretDown-131"
data-icon-name="ChevronDown"
>
</i>
</span>
</div>
</div>
</div>
</div>
`;
exports[`AccountDropdown Snapshot Testing matches snapshot with single option 1`] = `
<div
class="ms-Stack flex-row css-109"
>
<div
class="ms-StackItem flex-fixed-width css-110"
>
<label
class="field-label "
>
Account
:
</label>
</div>
<div
class="ms-StackItem flex-grow-col css-110"
>
<div
class="ms-Dropdown-container"
>
<div
aria-disabled="false"
aria-expanded="false"
aria-haspopup="listbox"
aria-label="Account"
aria-required="true"
class="ms-Dropdown is-required dropdown-111"
data-is-focusable="true"
data-ktp-target="true"
id="Dropdown4"
role="combobox"
tabindex="0"
>
<span
aria-invalid="false"
class="ms-Dropdown-title title-132"
id="Dropdown4-option"
>
Development Account
</span>
<span
class="ms-Dropdown-caretDownWrapper caretDownWrapper-113"
>
<i
aria-hidden="true"
class="ms-Dropdown-caretDown caretDown-131"
data-icon-name="ChevronDown"
>
</i>
</span>
</div>
</div>
</div>
</div>
`;
exports[`AccountDropdown Snapshot Testing matches snapshot with special characters in options 1`] = `
<div
class="ms-Stack flex-row css-109"
>
<div
class="ms-StackItem flex-fixed-width css-110"
>
<label
class="field-label "
>
Account
:
</label>
</div>
<div
class="ms-StackItem flex-grow-col css-110"
>
<div
class="ms-Dropdown-container"
>
<div
aria-disabled="false"
aria-expanded="false"
aria-haspopup="listbox"
aria-label="Account"
aria-required="true"
class="ms-Dropdown is-required dropdown-111"
data-is-focusable="true"
data-ktp-target="true"
id="Dropdown5"
role="combobox"
tabindex="0"
>
<span
aria-invalid="false"
class="ms-Dropdown-title ms-Dropdown-titleIsPlaceHolder title-112"
id="Dropdown5-option"
>
Select an account
</span>
<span
class="ms-Dropdown-caretDownWrapper caretDownWrapper-113"
>
<i
aria-hidden="true"
class="ms-Dropdown-caretDown caretDown-131"
data-icon-name="ChevronDown"
>
</i>
</span>
</div>
</div>
</div>
</div> </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>; copyJobState: {
const mockApiType = apiType as jest.MockedFunction<typeof apiType>; jobName: "",
migrationType: CopyJobMigrationType.Online,
import { useDropdownOptions, useEventHandlers } from "./Utils/selectAccountUtils"; source: {
const mockUseDropdownOptions = useDropdownOptions as jest.MockedFunction<typeof useDropdownOptions>; subscription: null as any,
const mockUseEventHandlers = useEventHandlers as jest.MockedFunction<typeof useEventHandlers>; account: null as any,
databaseId: "",
const mockSubscriptions = [ containerId: "",
{ },
subscriptionId: "sub-1", target: {
displayName: "Test Subscription 1", subscriptionId: "",
authorizationSource: "RoleBased", account: null as any,
subscriptionPolicies: { databaseId: "",
quotaId: "quota-1", containerId: "",
spendingLimit: "Off", },
locationPlacementId: "loc-1", sourceReadAccessFromTarget: false,
}, },
}, setCopyJobState: mockSetCopyJobState,
{ flow: { currentScreen: "selectAccount" },
subscriptionId: "sub-2", setFlow: jest.fn(),
displayName: "Test Subscription 2", contextError: null,
authorizationSource: "RoleBased", setContextError: jest.fn(),
subscriptionPolicies: { explorer: {} as any,
quotaId: "quota-2", resetCopyJobState: jest.fn(),
spendingLimit: "On", };
locationPlacementId: "loc-2",
},
},
] as Subscription[];
const mockAccounts = [
{
id: "/subscriptions/sub-1/resourceGroups/rg-1/providers/Microsoft.DocumentDB/databaseAccounts/account-1",
name: "test-cosmos-account-1",
location: "East US",
kind: "GlobalDocumentDB",
properties: {
documentEndpoint: "https://account-1.documents.azure.com/",
capabilities: [],
enableFreeTier: false,
},
},
{
id: "/subscriptions/sub-1/resourceGroups/rg-1/providers/Microsoft.DocumentDB/databaseAccounts/account-2",
name: "test-cosmos-account-2",
location: "West US",
kind: "MongoDB",
properties: {
documentEndpoint: "https://account-2.documents.azure.com/",
capabilities: [],
},
},
] as DatabaseAccount[];
const mockDropdownOptions = {
subscriptionOptions: [
{ key: "sub-1", text: "Test Subscription 1", data: mockSubscriptions[0] },
{ key: "sub-2", text: "Test Subscription 2", data: mockSubscriptions[1] },
],
accountOptions: [{ key: mockAccounts[0].id, text: mockAccounts[0].name, data: mockAccounts[0] }],
};
const mockEventHandlers = {
handleSelectSourceAccount: jest.fn(),
handleMigrationTypeChange: jest.fn(),
};
let mockContextValue = {
copyJobState: {
jobName: "",
migrationType: CopyJobMigrationType.Offline,
source: {
subscription: null,
account: null,
databaseId: "",
containerId: "",
},
target: {
subscriptionId: "",
account: null,
databaseId: "",
containerId: "",
},
sourceReadAccessFromTarget: false,
} as CopyJobContextState,
setCopyJobState: jest.fn(),
flow: null,
setFlow: jest.fn(),
contextError: null,
setContextError: jest.fn(),
resetCopyJobState: jest.fn(),
explorer: {} as any,
} as CopyJobContextProviderType;
describe("SelectAccount Component", () => {
beforeEach(() => { 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", () => {
(useCopyJobContext as jest.Mock).mockReturnValue({
...defaultContextValue,
copyJobState: {
...defaultContextValue.copyJobState,
migrationType: CopyJobMigrationType.Online,
},
});
render(<SelectAccount />); render(<SelectAccount />);
expect(mockUseSubscriptions).toHaveBeenCalledTimes(1);
const checkbox = screen.getByTestId("migration-checkbox-input");
expect(checkbox).not.toBeChecked();
}); });
it("should call useDatabaseAccounts with selected subscription ID", () => { it("should display migration type checkbox as checked when migrationType is Offline", () => {
mockContextValue.copyJobState.source.subscription = mockSubscriptions[0]; (useCopyJobContext as jest.Mock).mockReturnValue({
...defaultContextValue,
copyJobState: {
...defaultContextValue.copyJobState,
migrationType: CopyJobMigrationType.Offline,
},
});
render(<SelectAccount />); render(<SelectAccount />);
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", () => { it("should call setCopyJobState with Online migration type when checkbox is unchecked", () => {
render(<SelectAccount />); (useCopyJobContext as jest.Mock).mockReturnValue({
expect(mockUseDatabaseAccounts).toHaveBeenCalledWith(undefined); ...defaultContextValue,
}); copyJobState: {
...defaultContextValue.copyJobState,
it("should filter accounts to SQL API only", () => { migrationType: CopyJobMigrationType.Offline,
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);
});
it("should call useEventHandlers with setCopyJobState", () => {
render(<SelectAccount />);
expect(mockUseEventHandlers).toHaveBeenCalledWith(mockContextValue.setCopyJobState);
});
});
describe("Event Handling", () => {
it("should handle subscription selection", () => {
render(<SelectAccount />);
const subscriptionOption = screen.getByTestId("subscription-option-sub-1");
fireEvent.click(subscriptionOption);
expect(mockEventHandlers.handleSelectSourceAccount).toHaveBeenCalledWith("subscription", mockSubscriptions[0]);
});
it("should handle account selection", () => {
render(<SelectAccount />);
const accountOption = screen.getByTestId(`account-option-${mockAccounts[0].id}`);
fireEvent.click(accountOption);
expect(mockEventHandlers.handleSelectSourceAccount).toHaveBeenCalledWith("account", mockAccounts[0]);
});
it("should handle migration type change", () => {
render(<SelectAccount />);
const checkbox = screen.getByTestId("migration-checkbox-input"); 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));
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", () => { describe("Performance and Optimization", () => {
it("should disable account dropdown when no subscription is selected", () => { it("should maintain referential equality of handler functions between renders", async () => {
render(<SelectAccount />); const { rerender } = render(<SelectAccount />);
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", () => { rerender(<SelectAccount />);
mockContextValue.copyJobState.source.subscription = mockSubscriptions[0];
render(<SelectAccount />); const secondRenderHandler = migrationCheckbox.mock.calls[migrationCheckbox.mock.calls.length - 1][0].onChange;
expect(screen.getByTestId("account-dropdown")).toHaveAttribute("data-disabled", "false"); expect(firstRenderHandler).toBe(secondRenderHandler);
});
});
describe("Component Props", () => {
it("should pass correct props to SubscriptionDropdown", () => {
render(<SelectAccount />);
const dropdown = screen.getByTestId("subscription-dropdown");
expect(dropdown).not.toHaveAttribute("data-selected");
});
it("should pass selected subscription ID to SubscriptionDropdown", () => {
mockContextValue.copyJobState.source.subscription = mockSubscriptions[0];
render(<SelectAccount />);
const dropdown = screen.getByTestId("subscription-dropdown");
expect(dropdown).toHaveAttribute("data-selected", "sub-1");
});
it("should pass correct props to AccountDropdown", () => {
render(<SelectAccount />);
const dropdown = screen.getByTestId("account-dropdown");
expect(dropdown).not.toHaveAttribute("data-selected");
expect(dropdown).toHaveAttribute("data-disabled", "true");
});
it("should pass selected account ID to AccountDropdown", () => {
mockContextValue.copyJobState.source.account = mockAccounts[0];
render(<SelectAccount />);
const dropdown = screen.getByTestId("account-dropdown");
expect(dropdown).toHaveAttribute("data-selected", mockAccounts[0].id);
});
it("should pass correct checked state to MigrationTypeCheckbox", () => {
mockContextValue.copyJobState.migrationType = CopyJobMigrationType.Offline;
render(<SelectAccount />);
const checkbox = screen.getByTestId("migration-type-checkbox");
expect(checkbox).toHaveAttribute("data-checked", "true");
});
});
describe("Edge Cases", () => {
it("should handle empty subscriptions array", () => {
mockUseSubscriptions.mockReturnValue([]);
mockUseDropdownOptions.mockReturnValue({
subscriptionOptions: [],
accountOptions: [],
});
const { container } = render(<SelectAccount />);
expect(container).toMatchSnapshot();
});
it("should handle empty accounts array", () => {
mockUseDatabaseAccounts.mockReturnValue([]);
mockUseDropdownOptions.mockReturnValue({
subscriptionOptions: mockDropdownOptions.subscriptionOptions,
accountOptions: [],
});
const { container } = render(<SelectAccount />);
expect(container).toMatchSnapshot();
});
it("should handle null subscription in context", () => {
mockContextValue.copyJobState.source.subscription = null;
const { container } = render(<SelectAccount />);
expect(container).toMatchSnapshot();
});
it("should handle null account in context", () => {
mockContextValue.copyJobState.source.account = null;
const { container } = render(<SelectAccount />);
expect(container).toMatchSnapshot();
});
it("should handle undefined subscriptions from hook", () => {
mockUseSubscriptions.mockReturnValue(undefined as any);
mockUseDropdownOptions.mockReturnValue({
subscriptionOptions: [],
accountOptions: [],
});
const { container } = render(<SelectAccount />);
expect(container).toMatchSnapshot();
});
it("should handle undefined accounts from hook", () => {
mockUseDatabaseAccounts.mockReturnValue(undefined as any);
mockUseDropdownOptions.mockReturnValue({
subscriptionOptions: mockDropdownOptions.subscriptionOptions,
accountOptions: [],
});
const { container } = render(<SelectAccount />);
expect(container).toMatchSnapshot();
});
it("should filter out non-SQL accounts correctly", () => {
const mixedAccounts = [
{ ...mockAccounts[0], kind: "GlobalDocumentDB" },
{ ...mockAccounts[1], kind: "MongoDB" },
];
mockUseDatabaseAccounts.mockReturnValue(mixedAccounts);
mockApiType.mockImplementation((account) => (account.kind === "GlobalDocumentDB" ? "SQL" : "Mongo"));
render(<SelectAccount />);
expect(mockApiType).toHaveBeenCalledTimes(2);
const sqlOnlyAccounts = mixedAccounts.filter((account) => apiType(account) === "SQL");
expect(mockUseDropdownOptions).toHaveBeenCalledWith(mockSubscriptions, sqlOnlyAccounts);
});
});
describe("Complete Workflow", () => {
it("should render complete workflow with all selections", () => {
mockContextValue.copyJobState.source.subscription = mockSubscriptions[0];
mockContextValue.copyJobState.source.account = mockAccounts[0];
mockContextValue.copyJobState.migrationType = CopyJobMigrationType.Online;
const { container } = render(<SelectAccount />);
expect(screen.getByTestId("subscription-dropdown")).toHaveAttribute("data-selected", "sub-1");
expect(screen.getByTestId("account-dropdown")).toHaveAttribute("data-selected", mockAccounts[0].id);
expect(screen.getByTestId("account-dropdown")).toHaveAttribute("data-disabled", "false");
expect(screen.getByTestId("migration-type-checkbox")).toHaveAttribute("data-checked", "false");
expect(container).toMatchSnapshot();
}); });
}); });
}); });

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
class="css-110"
> >
<span> Please select a source account from which to copy.
Select your source account and subscription </span>
</span> <div
<div data-testid="subscription-dropdown"
data-selected="sub-1" >
data-testid="subscription-dropdown" Subscription Dropdown
>
<div
data-testid="subscription-option-sub-1"
>
Test Subscription 1
</div>
<div
data-testid="subscription-option-sub-2"
>
Test Subscription 2
</div>
</div>
<div
data-disabled="false"
data-selected="/subscriptions/sub-1/resourceGroups/rg-1/providers/Microsoft.DocumentDB/databaseAccounts/account-1"
data-testid="account-dropdown"
>
<div
data-testid="account-option-/subscriptions/sub-1/resourceGroups/rg-1/providers/Microsoft.DocumentDB/databaseAccounts/account-1"
>
test-cosmos-account-1
</div>
</div>
<div
data-checked="false"
data-testid="migration-type-checkbox"
>
<input
data-testid="migration-checkbox-input"
type="checkbox"
/>
</div>
</div> </div>
</div>
`;
exports[`SelectAccount Component Edge Cases should handle empty accounts array 1`] = `
<div>
<div <div
class="ms-Stack selectAccountContainer css-109" data-testid="account-dropdown"
> >
<span> Account Dropdown
Select your source account and subscription </div>
</span> <div
<div data-testid="migration-type-checkbox"
data-testid="subscription-dropdown" >
> <input
<div aria-label="Migration Type Checkbox"
data-testid="subscription-option-sub-1" data-testid="migration-checkbox-input"
> type="checkbox"
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 Copy container in offline mode
data-checked="true"
data-testid="migration-type-checkbox"
>
<input
checked=""
data-testid="migration-checkbox-input"
type="checkbox"
/>
</div>
</div>
</div>
`;
exports[`SelectAccount Component Edge Cases should handle empty subscriptions array 1`] = `
<div>
<div
class="ms-Stack selectAccountContainer css-109"
>
<span>
Select your source account and subscription
</span>
<div
data-testid="subscription-dropdown"
/>
<div
data-disabled="true"
data-testid="account-dropdown"
/>
<div
data-checked="true"
data-testid="migration-type-checkbox"
>
<input
checked=""
data-testid="migration-checkbox-input"
type="checkbox"
/>
</div>
</div>
</div>
`;
exports[`SelectAccount Component Edge Cases should handle null account in context 1`] = `
<div>
<div
class="ms-Stack selectAccountContainer css-109"
>
<span>
Select your source account and subscription
</span>
<div
data-testid="subscription-dropdown"
>
<div
data-testid="subscription-option-sub-1"
>
Test Subscription 1
</div>
<div
data-testid="subscription-option-sub-2"
>
Test Subscription 2
</div>
</div>
<div
data-disabled="true"
data-testid="account-dropdown"
>
<div
data-testid="account-option-/subscriptions/sub-1/resourceGroups/rg-1/providers/Microsoft.DocumentDB/databaseAccounts/account-1"
>
test-cosmos-account-1
</div>
</div>
<div
data-checked="true"
data-testid="migration-type-checkbox"
>
<input
checked=""
data-testid="migration-checkbox-input"
type="checkbox"
/>
</div>
</div>
</div>
`;
exports[`SelectAccount Component Edge Cases should handle null subscription in context 1`] = `
<div>
<div
class="ms-Stack selectAccountContainer css-109"
>
<span>
Select your source account and subscription
</span>
<div
data-testid="subscription-dropdown"
>
<div
data-testid="subscription-option-sub-1"
>
Test Subscription 1
</div>
<div
data-testid="subscription-option-sub-2"
>
Test Subscription 2
</div>
</div>
<div
data-disabled="true"
data-testid="account-dropdown"
>
<div
data-testid="account-option-/subscriptions/sub-1/resourceGroups/rg-1/providers/Microsoft.DocumentDB/databaseAccounts/account-1"
>
test-cosmos-account-1
</div>
</div>
<div
data-checked="true"
data-testid="migration-type-checkbox"
>
<input
checked=""
data-testid="migration-checkbox-input"
type="checkbox"
/>
</div>
</div>
</div>
`;
exports[`SelectAccount Component Edge Cases should handle undefined accounts from hook 1`] = `
<div>
<div
class="ms-Stack selectAccountContainer css-109"
>
<span>
Select your source account and subscription
</span>
<div
data-testid="subscription-dropdown"
>
<div
data-testid="subscription-option-sub-1"
>
Test Subscription 1
</div>
<div
data-testid="subscription-option-sub-2"
>
Test Subscription 2
</div>
</div>
<div
data-disabled="true"
data-testid="account-dropdown"
/>
<div
data-checked="true"
data-testid="migration-type-checkbox"
>
<input
checked=""
data-testid="migration-checkbox-input"
type="checkbox"
/>
</div>
</div>
</div>
`;
exports[`SelectAccount Component Edge Cases should handle undefined subscriptions from hook 1`] = `
<div>
<div
class="ms-Stack selectAccountContainer css-109"
>
<span>
Select your source account and subscription
</span>
<div
data-testid="subscription-dropdown"
/>
<div
data-disabled="true"
data-testid="account-dropdown"
/>
<div
data-checked="true"
data-testid="migration-type-checkbox"
>
<input
checked=""
data-testid="migration-checkbox-input"
type="checkbox"
/>
</div>
</div>
</div>
`;
exports[`SelectAccount Component Rendering should render component with default state 1`] = `
<div>
<div
class="ms-Stack selectAccountContainer css-109"
>
<span>
Select your source account and subscription
</span>
<div
data-testid="subscription-dropdown"
>
<div
data-testid="subscription-option-sub-1"
>
Test Subscription 1
</div>
<div
data-testid="subscription-option-sub-2"
>
Test Subscription 2
</div>
</div>
<div
data-disabled="true"
data-testid="account-dropdown"
>
<div
data-testid="account-option-/subscriptions/sub-1/resourceGroups/rg-1/providers/Microsoft.DocumentDB/databaseAccounts/account-1"
>
test-cosmos-account-1
</div>
</div>
<div
data-checked="true"
data-testid="migration-type-checkbox"
>
<input
checked=""
data-testid="migration-checkbox-input"
type="checkbox"
/>
</div>
</div>
</div>
`;
exports[`SelectAccount Component Rendering should render with offline migration type checked 1`] = `
<div>
<div
class="ms-Stack selectAccountContainer css-109"
>
<span>
Select your source account and subscription
</span>
<div
data-testid="subscription-dropdown"
>
<div
data-testid="subscription-option-sub-1"
>
Test Subscription 1
</div>
<div
data-testid="subscription-option-sub-2"
>
Test Subscription 2
</div>
</div>
<div
data-disabled="true"
data-testid="account-dropdown"
>
<div
data-testid="account-option-/subscriptions/sub-1/resourceGroups/rg-1/providers/Microsoft.DocumentDB/databaseAccounts/account-1"
>
test-cosmos-account-1
</div>
</div>
<div
data-checked="true"
data-testid="migration-type-checkbox"
>
<input
checked=""
data-testid="migration-checkbox-input"
type="checkbox"
/>
</div>
</div>
</div>
`;
exports[`SelectAccount Component Rendering should render with online migration type unchecked 1`] = `
<div>
<div
class="ms-Stack selectAccountContainer css-109"
>
<span>
Select your source account and subscription
</span>
<div
data-testid="subscription-dropdown"
>
<div
data-testid="subscription-option-sub-1"
>
Test Subscription 1
</div>
<div
data-testid="subscription-option-sub-2"
>
Test Subscription 2
</div>
</div>
<div
data-disabled="true"
data-testid="account-dropdown"
>
<div
data-testid="account-option-/subscriptions/sub-1/resourceGroups/rg-1/providers/Microsoft.DocumentDB/databaseAccounts/account-1"
>
test-cosmos-account-1
</div>
</div>
<div
data-checked="false"
data-testid="migration-type-checkbox"
>
<input
data-testid="migration-checkbox-input"
type="checkbox"
/>
</div>
</div>
</div>
`;
exports[`SelectAccount Component Rendering should render with selected account 1`] = `
<div>
<div
class="ms-Stack selectAccountContainer css-109"
>
<span>
Select your source account and subscription
</span>
<div
data-selected="sub-1"
data-testid="subscription-dropdown"
>
<div
data-testid="subscription-option-sub-1"
>
Test Subscription 1
</div>
<div
data-testid="subscription-option-sub-2"
>
Test Subscription 2
</div>
</div>
<div
data-disabled="false"
data-selected="/subscriptions/sub-1/resourceGroups/rg-1/providers/Microsoft.DocumentDB/databaseAccounts/account-1"
data-testid="account-dropdown"
>
<div
data-testid="account-option-/subscriptions/sub-1/resourceGroups/rg-1/providers/Microsoft.DocumentDB/databaseAccounts/account-1"
>
test-cosmos-account-1
</div>
</div>
<div
data-checked="true"
data-testid="migration-type-checkbox"
>
<input
checked=""
data-testid="migration-checkbox-input"
type="checkbox"
/>
</div>
</div>
</div>
`;
exports[`SelectAccount Component Rendering should render with selected subscription 1`] = `
<div>
<div
class="ms-Stack selectAccountContainer css-109"
>
<span>
Select your source account and subscription
</span>
<div
data-selected="sub-1"
data-testid="subscription-dropdown"
>
<div
data-testid="subscription-option-sub-1"
>
Test Subscription 1
</div>
<div
data-testid="subscription-option-sub-2"
>
Test Subscription 2
</div>
</div>
<div
data-disabled="false"
data-testid="account-dropdown"
>
<div
data-testid="account-option-/subscriptions/sub-1/resourceGroups/rg-1/providers/Microsoft.DocumentDB/databaseAccounts/account-1"
>
test-cosmos-account-1
</div>
</div>
<div
data-checked="true"
data-testid="migration-type-checkbox"
>
<input
checked=""
data-testid="migration-checkbox-input"
type="checkbox"
/>
</div>
</div> </div>
</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) {
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 { } 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;
}; };

View File

@@ -1482,6 +1482,9 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
itemKey: SettingsV2TabTypes[tab.tab], itemKey: SettingsV2TabTypes[tab.tab],
style: { marginTop: 20 }, style: { marginTop: 20 },
headerText: getTabTitle(tab.tab), headerText: getTabTitle(tab.tab),
headerButtonProps: {
"data-test": `settings-tab-header/${SettingsV2TabTypes[tab.tab]}`,
},
}; };
return ( return (

View File

@@ -127,9 +127,9 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
}; };
private ttlChoiceGroupOptions: IChoiceGroupOption[] = [ private ttlChoiceGroupOptions: IChoiceGroupOption[] = [
{ key: TtlType.Off, text: "Off" }, { key: TtlType.Off, text: "Off", ariaLabel: "ttl-off-option" },
{ key: TtlType.OnNoDefault, text: "On (no default)" }, { key: TtlType.OnNoDefault, text: "On (no default)", ariaLabel: "ttl-on-no-default-option" },
{ key: TtlType.On, text: "On" }, { key: TtlType.On, text: "On", ariaLabel: "ttl-on-option" },
]; ];
public getTtlValue = (value: string): TtlType => { public getTtlValue = (value: string): TtlType => {
@@ -223,6 +223,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
onChange={this.onTimeToLiveSecondsChange} onChange={this.onTimeToLiveSecondsChange}
suffix="second(s)" suffix="second(s)"
ariaLabel={`Time to live in seconds`} ariaLabel={`Time to live in seconds`}
data-test="ttl-input"
/> />
)} )}
</Stack> </Stack>

View File

@@ -503,7 +503,9 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
<span style={{ float: "left", transform: "translateX(-50%)" }}> <span style={{ float: "left", transform: "translateX(-50%)" }}>
{this.props.instantMaximumThroughput.toLocaleString()} {this.props.instantMaximumThroughput.toLocaleString()}
</span> </span>
<span style={{ float: "right" }}>{this.props.softAllowedMaximumThroughput.toLocaleString()}</span> <span style={{ float: "right" }} data-test="soft-allowed-maximum-throughput">
{this.props.softAllowedMaximumThroughput.toLocaleString()}
</span>
</Stack.Item> </Stack.Item>
</Stack> </Stack>
<ProgressIndicator <ProgressIndicator
@@ -626,11 +628,12 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
min={autoPilotThroughput1K} min={autoPilotThroughput1K}
onGetErrorMessage={(value: string) => { onGetErrorMessage={(value: string) => {
const sanitizedValue = getSanitizedInputValue(value); const sanitizedValue = getSanitizedInputValue(value);
return sanitizedValue % 1000 const errorMessage: string =
? "Throughput value must be in increments of 1000" sanitizedValue % 1000 ? "Throughput value must be in increments of 1000" : this.props.throughputError;
: this.props.throughputError; return <span data-test="autopilot-throughput-input-error">{errorMessage}</span>;
}} }}
validateOnLoad={false} validateOnLoad={false}
data-test="autopilot-throughput-input"
/> />
</Stack> </Stack>
</Stack> </Stack>
@@ -650,7 +653,10 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
} }
onChange={this.onThroughputChange} onChange={this.onThroughputChange}
min={this.props.minimum} min={this.props.minimum}
errorMessage={this.props.throughputError} onGetErrorMessage={() => {
return <span data-test="manual-throughput-input-error">{this.props.throughputError}</span>;
}}
data-test="manual-throughput-input"
/> />
)} )}
</> </>

View File

@@ -273,6 +273,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
/> />
</Stack> </Stack>
<StyledTextFieldBase <StyledTextFieldBase
data-test="autopilot-throughput-input"
disabled={true} disabled={true}
id="autopilotInput" id="autopilotInput"
key="auto pilot throughput input" key="auto pilot throughput input"
@@ -333,6 +334,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
5,000 5,000
</span> </span>
<span <span
data-test="soft-allowed-maximum-throughput"
style={ style={
{ {
"float": "right", "float": "right",
@@ -752,11 +754,13 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
} }
> >
<StyledTextFieldBase <StyledTextFieldBase
data-test="manual-throughput-input"
disabled={false} disabled={false}
id="throughputInput" id="throughputInput"
key="provisioned throughput input" key="provisioned throughput input"
min={10000} min={10000}
onChange={[Function]} onChange={[Function]}
onGetErrorMessage={[Function]}
required={true} required={true}
step={100} step={100}
styles={ styles={
@@ -811,6 +815,7 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
5,000 5,000
</span> </span>
<span <span
data-test="soft-allowed-maximum-throughput"
style={ style={
{ {
"float": "right", "float": "right",
@@ -1206,11 +1211,13 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
} }
> >
<StyledTextFieldBase <StyledTextFieldBase
data-test="manual-throughput-input"
disabled={false} disabled={false}
id="throughputInput" id="throughputInput"
key="provisioned throughput input" key="provisioned throughput input"
min={10000} min={10000}
onChange={[Function]} onChange={[Function]}
onGetErrorMessage={[Function]}
required={true} required={true}
step={100} step={100}
styles={ styles={
@@ -1265,6 +1272,7 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
5,000 5,000
</span> </span>
<span <span
data-test="soft-allowed-maximum-throughput"
style={ style={
{ {
"float": "right", "float": "right",

View File

@@ -22,14 +22,17 @@ exports[`SubSettingsComponent analyticalTimeToLive hidden 1`] = `
options={ options={
[ [
{ {
"ariaLabel": "ttl-off-option",
"key": "off", "key": "off",
"text": "Off", "text": "Off",
}, },
{ {
"ariaLabel": "ttl-on-no-default-option",
"key": "on-nodefault", "key": "on-nodefault",
"text": "On (no default)", "text": "On (no default)",
}, },
{ {
"ariaLabel": "ttl-on-option",
"key": "on", "key": "on",
"text": "On", "text": "On",
}, },
@@ -63,6 +66,7 @@ exports[`SubSettingsComponent analyticalTimeToLive hidden 1`] = `
/> />
<StyledTextFieldBase <StyledTextFieldBase
ariaLabel="Time to live in seconds" ariaLabel="Time to live in seconds"
data-test="ttl-input"
id="timeToLiveSeconds" id="timeToLiveSeconds"
max={2147483647} max={2147483647}
min={1} min={1}
@@ -284,14 +288,17 @@ exports[`SubSettingsComponent analyticalTimeToLiveSeconds hidden 1`] = `
options={ options={
[ [
{ {
"ariaLabel": "ttl-off-option",
"key": "off", "key": "off",
"text": "Off", "text": "Off",
}, },
{ {
"ariaLabel": "ttl-on-no-default-option",
"key": "on-nodefault", "key": "on-nodefault",
"text": "On (no default)", "text": "On (no default)",
}, },
{ {
"ariaLabel": "ttl-on-option",
"key": "on", "key": "on",
"text": "On", "text": "On",
}, },
@@ -325,6 +332,7 @@ exports[`SubSettingsComponent analyticalTimeToLiveSeconds hidden 1`] = `
/> />
<StyledTextFieldBase <StyledTextFieldBase
ariaLabel="Time to live in seconds" ariaLabel="Time to live in seconds"
data-test="ttl-input"
id="timeToLiveSeconds" id="timeToLiveSeconds"
max={2147483647} max={2147483647}
min={1} min={1}
@@ -601,14 +609,17 @@ exports[`SubSettingsComponent changeFeedPolicy hidden 1`] = `
options={ options={
[ [
{ {
"ariaLabel": "ttl-off-option",
"key": "off", "key": "off",
"text": "Off", "text": "Off",
}, },
{ {
"ariaLabel": "ttl-on-no-default-option",
"key": "on-nodefault", "key": "on-nodefault",
"text": "On (no default)", "text": "On (no default)",
}, },
{ {
"ariaLabel": "ttl-on-option",
"key": "on", "key": "on",
"text": "On", "text": "On",
}, },
@@ -642,6 +653,7 @@ exports[`SubSettingsComponent changeFeedPolicy hidden 1`] = `
/> />
<StyledTextFieldBase <StyledTextFieldBase
ariaLabel="Time to live in seconds" ariaLabel="Time to live in seconds"
data-test="ttl-input"
id="timeToLiveSeconds" id="timeToLiveSeconds"
max={2147483647} max={2147483647}
min={1} min={1}
@@ -878,14 +890,17 @@ exports[`SubSettingsComponent renders 1`] = `
options={ options={
[ [
{ {
"ariaLabel": "ttl-off-option",
"key": "off", "key": "off",
"text": "Off", "text": "Off",
}, },
{ {
"ariaLabel": "ttl-on-no-default-option",
"key": "on-nodefault", "key": "on-nodefault",
"text": "On (no default)", "text": "On (no default)",
}, },
{ {
"ariaLabel": "ttl-on-option",
"key": "on", "key": "on",
"text": "On", "text": "On",
}, },
@@ -919,6 +934,7 @@ exports[`SubSettingsComponent renders 1`] = `
/> />
<StyledTextFieldBase <StyledTextFieldBase
ariaLabel="Time to live in seconds" ariaLabel="Time to live in seconds"
data-test="ttl-input"
id="timeToLiveSeconds" id="timeToLiveSeconds"
max={2147483647} max={2147483647}
min={1} min={1}
@@ -1220,14 +1236,17 @@ exports[`SubSettingsComponent timeToLiveSeconds hidden 1`] = `
options={ options={
[ [
{ {
"ariaLabel": "ttl-off-option",
"key": "off", "key": "off",
"text": "Off", "text": "Off",
}, },
{ {
"ariaLabel": "ttl-on-no-default-option",
"key": "on-nodefault", "key": "on-nodefault",
"text": "On (no default)", "text": "On (no default)",
}, },
{ {
"ariaLabel": "ttl-on-option",
"key": "on", "key": "on",
"text": "On", "text": "On",
}, },

View File

@@ -12,6 +12,11 @@ exports[`SettingsComponent renders 1`] = `
selectedKey="ScaleTab" selectedKey="ScaleTab"
> >
<PivotItem <PivotItem
headerButtonProps={
{
"data-test": "settings-tab-header/ScaleTab",
}
}
headerText="Scale" headerText="Scale"
itemKey="ScaleTab" itemKey="ScaleTab"
key="ScaleTab" key="ScaleTab"
@@ -102,6 +107,11 @@ exports[`SettingsComponent renders 1`] = `
/> />
</PivotItem> </PivotItem>
<PivotItem <PivotItem
headerButtonProps={
{
"data-test": "settings-tab-header/SubSettingsTab",
}
}
headerText="Settings" headerText="Settings"
itemKey="SubSettingsTab" itemKey="SubSettingsTab"
key="SubSettingsTab" key="SubSettingsTab"
@@ -201,6 +211,11 @@ exports[`SettingsComponent renders 1`] = `
/> />
</PivotItem> </PivotItem>
<PivotItem <PivotItem
headerButtonProps={
{
"data-test": "settings-tab-header/ContainerVectorPolicyTab",
}
}
headerText="Container Policies" headerText="Container Policies"
itemKey="ContainerVectorPolicyTab" itemKey="ContainerVectorPolicyTab"
key="ContainerVectorPolicyTab" key="ContainerVectorPolicyTab"
@@ -227,6 +242,11 @@ exports[`SettingsComponent renders 1`] = `
/> />
</PivotItem> </PivotItem>
<PivotItem <PivotItem
headerButtonProps={
{
"data-test": "settings-tab-header/IndexingPolicyTab",
}
}
headerText="Indexing Policy" headerText="Indexing Policy"
itemKey="IndexingPolicyTab" itemKey="IndexingPolicyTab"
key="IndexingPolicyTab" key="IndexingPolicyTab"
@@ -263,6 +283,11 @@ exports[`SettingsComponent renders 1`] = `
/> />
</PivotItem> </PivotItem>
<PivotItem <PivotItem
headerButtonProps={
{
"data-test": "settings-tab-header/PartitionKeyTab",
}
}
headerText="Partition Keys (preview)" headerText="Partition Keys (preview)"
itemKey="PartitionKeyTab" itemKey="PartitionKeyTab"
key="PartitionKeyTab" key="PartitionKeyTab"
@@ -370,6 +395,11 @@ exports[`SettingsComponent renders 1`] = `
/> />
</PivotItem> </PivotItem>
<PivotItem <PivotItem
headerButtonProps={
{
"data-test": "settings-tab-header/ComputedPropertiesTab",
}
}
headerText="Computed Properties" headerText="Computed Properties"
itemKey="ComputedPropertiesTab" itemKey="ComputedPropertiesTab"
key="ComputedPropertiesTab" key="ComputedPropertiesTab"
@@ -404,6 +434,11 @@ exports[`SettingsComponent renders 1`] = `
/> />
</PivotItem> </PivotItem>
<PivotItem <PivotItem
headerButtonProps={
{
"data-test": "settings-tab-header/GlobalSecondaryIndexTab",
}
}
headerText="Global Secondary Index (Preview)" headerText="Global Secondary Index (Preview)"
itemKey="GlobalSecondaryIndexTab" itemKey="GlobalSecondaryIndexTab"
key="GlobalSecondaryIndexTab" key="GlobalSecondaryIndexTab"

View File

@@ -127,7 +127,7 @@ export class NotificationConsoleComponent extends React.Component<
</span> </span>
</span> </span>
<span className="consoleSplitter" /> <span className="consoleSplitter" />
<span className="headerStatus"> <span className="headerStatus" data-test="notification-console/header-status">
<span className="headerStatusEllipsis" aria-live="assertive" aria-atomic="true"> <span className="headerStatusEllipsis" aria-live="assertive" aria-atomic="true">
{this.state.headerStatus} {this.state.headerStatus}
</span> </span>

View File

@@ -78,6 +78,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
/> />
<span <span
className="headerStatus" className="headerStatus"
data-test="notification-console/header-status"
> >
<span <span
aria-atomic="true" aria-atomic="true"
@@ -261,6 +262,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
/> />
<span <span
className="headerStatus" className="headerStatus"
data-test="notification-console/header-status"
> >
<span <span
aria-atomic="true" aria-atomic="true"

View File

@@ -1,88 +0,0 @@
import { initializeIcons } from "@fluentui/react";
import "bootstrap/dist/css/bootstrap.css";
import React from "react";
import * as ReactDOM from "react-dom";
import { configContext, initializeConfiguration } from "../ConfigContext";
import { GalleryHeaderComponent } from "../Explorer/Controls/Header/GalleryHeaderComponent";
import { GalleryTab } from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent";
import {
NotebookViewerComponent,
NotebookViewerComponentProps,
} from "../Explorer/Controls/NotebookViewer/NotebookViewerComponent";
import * as FileSystemUtil from "../Explorer/Notebook/FileSystemUtil";
import { IGalleryItem, JunoClient } from "../Juno/JunoClient";
import * as GalleryUtils from "../Utils/GalleryUtils";
const onInit = async () => {
initializeIcons();
await initializeConfiguration();
const galleryViewerProps = GalleryUtils.getGalleryViewerProps(window.location.search);
const notebookViewerProps = GalleryUtils.getNotebookViewerProps(window.location.search);
let backNavigationText: string;
let onBackClick: () => void;
if (galleryViewerProps.selectedTab !== undefined) {
backNavigationText = GalleryUtils.getTabTitle(galleryViewerProps.selectedTab);
onBackClick = () =>
(window.location.href = `${configContext.hostedExplorerURL}gallery.html?tab=${
GalleryTab[galleryViewerProps.selectedTab]
}`);
}
const hideInputs = notebookViewerProps.hideInputs;
const notebookUrl = decodeURIComponent(notebookViewerProps.notebookUrl);
const galleryItemId = notebookViewerProps.galleryItemId;
let galleryItem: IGalleryItem;
if (galleryItemId) {
const junoClient = new JunoClient();
const galleryItemJunoResponse = await junoClient.getNotebookInfo(galleryItemId);
galleryItem = galleryItemJunoResponse.data;
}
// The main purpose of hiding the prompt is to hide everything when hiding inputs.
// It is generally not very useful to just hide the prompt.
const hidePrompts = hideInputs;
render(notebookUrl, backNavigationText, hideInputs, hidePrompts, galleryItem, onBackClick);
};
const render = (
notebookUrl: string,
backNavigationText: string,
hideInputs?: boolean,
hidePrompts?: boolean,
galleryItem?: IGalleryItem,
onBackClick?: () => void,
) => {
const props: NotebookViewerComponentProps = {
junoClient: galleryItem ? new JunoClient() : undefined,
notebookUrl,
galleryItem,
backNavigationText,
hideInputs,
hidePrompts,
onBackClick: onBackClick,
onTagClick: undefined,
};
if (galleryItem) {
document.title = FileSystemUtil.stripExtension(galleryItem.name, "ipynb");
}
const element = (
<>
<header>
<GalleryHeaderComponent />
</header>
<div style={{ marginLeft: 120, marginRight: 120 }}>
<NotebookViewerComponent {...props} />
</div>
</>
);
ReactDOM.render(element, document.getElementById("notebookContent"));
};
// Entry point
window.addEventListener("load", onInit);

View File

@@ -40,13 +40,13 @@ To use this script, there are a few prerequisites that must be done at least onc
5. Ensure you have a Resource Group _ready_ to deploy into, the deploy script requires an existing resource group. This resource group should be named `[username]-e2e-testing`, where `[username]` is your Windows username, (**Microsoft employees:** This should be your alias). The easiest way to do this is by running the `create-resource-group.ps1` script, specifying the Subscription (Name or ID) and Location in which you want to create the Resource Group. For example: 5. Ensure you have a Resource Group _ready_ to deploy into, the deploy script requires an existing resource group. This resource group should be named `[username]-e2e-testing`, where `[username]` is your Windows username, (**Microsoft employees:** This should be your alias). The easiest way to do this is by running the `create-resource-group.ps1` script, specifying the Subscription (Name or ID) and Location in which you want to create the Resource Group. For example:
```powershell ```powershell
.\test\resources\create-resource-group.ps1 -SubscriptionName "My Subscription" -Location "West US 3" .\test\resources\create-resource-group.ps1 -SubscriptionId "My Subscription Id" -Location "West US 3"
``` ```
Then, whenever you want to create/update the resources, you can run the `deploy.ps1` script in the `resources` directory. As long as you're using the default naming convention (`[username]-e2e-testing`), you just need to specify the Subscription. For example: Then, whenever you want to create/update the resources, you can run the `deploy.ps1` script in the `resources` directory. As long as you're using the default naming convention (`[username]-e2e-testing`), you just need to specify the Subscription. For example:
```powershell ```powershell
.\test\resources\deploy.ps1 -SubscriptionName "My Subscription" .\test\resources\deploy.ps1 -Subscription "My Subscription"
``` ```
You'll get a confirmation prompt before anything is deployed: You'll get a confirmation prompt before anything is deployed:

View File

@@ -1,6 +1,7 @@
import { DefaultAzureCredential } from "@azure/identity"; import { DefaultAzureCredential } from "@azure/identity";
import { Frame, Locator, Page, expect } from "@playwright/test"; import { Frame, Locator, Page, expect } from "@playwright/test";
import crypto from "crypto"; import crypto from "crypto";
import { TestContainerContext } from "./testData";
const RETRY_COUNT = 3; const RETRY_COUNT = 3;
@@ -55,6 +56,9 @@ export const defaultAccounts: Record<TestAccount, string> = {
export const resourceGroupName = process.env.DE_TEST_RESOURCE_GROUP ?? "de-e2e-tests"; export const resourceGroupName = process.env.DE_TEST_RESOURCE_GROUP ?? "de-e2e-tests";
export const subscriptionId = process.env.DE_TEST_SUBSCRIPTION_ID ?? "69e02f2d-f059-4409-9eac-97e8a276ae2c"; export const subscriptionId = process.env.DE_TEST_SUBSCRIPTION_ID ?? "69e02f2d-f059-4409-9eac-97e8a276ae2c";
export const TEST_AUTOSCALE_THROUGHPUT_RU = 1000; export const TEST_AUTOSCALE_THROUGHPUT_RU = 1000;
export const TEST_AUTOSCALE_MAX_THROUGHPUT_RU_2K = 2000;
export const TEST_MANUAL_THROUGHPUT_RU_2K = 2000;
export const ONE_MINUTE_MS: number = 60 * 1000;
function tryGetStandardName(accountType: TestAccount) { function tryGetStandardName(accountType: TestAccount) {
if (process.env.DE_TEST_ACCOUNT_PREFIX) { if (process.env.DE_TEST_ACCOUNT_PREFIX) {
@@ -319,6 +323,11 @@ type PanelOpenOptions = {
closeTimeout?: number; closeTimeout?: number;
}; };
export enum CommandBarButton {
Save = "Save",
ExecuteQuery = "Execute Query",
}
/** Helper class that provides locator methods for DataExplorer components, on top of a Frame */ /** Helper class that provides locator methods for DataExplorer components, on top of a Frame */
export class DataExplorer { export class DataExplorer {
constructor(public frame: Frame) {} constructor(public frame: Frame) {}
@@ -348,8 +357,8 @@ export class DataExplorer {
} }
/** Select the command bar button with the specified label */ /** Select the command bar button with the specified label */
commandBarButton(label: string): Locator { commandBarButton(commandBarButton: CommandBarButton): Locator {
return this.frame.getByTestId(`CommandBar/Button:${label}`).and(this.frame.locator("css=button")); return this.frame.getByTestId(`CommandBar/Button:${commandBarButton}`).and(this.frame.locator("css=button"));
} }
dialogButton(label: string): Locator { dialogButton(label: string): Locator {
@@ -445,6 +454,22 @@ export class DataExplorer {
await panel.waitFor({ state: "detached", timeout: options.closeTimeout }); await panel.waitFor({ state: "detached", timeout: options.closeTimeout });
} }
/** Opens the Scale & Settings panel for the specified container */
async openScaleAndSettings(context: TestContainerContext): Promise<void> {
const containerNode = await this.waitForContainerNode(context.database.id, context.container.id);
await containerNode.expand();
const scaleAndSettingsButton = this.frame.getByTestId(
`TreeNode:${context.database.id}/${context.container.id}/Scale & Settings`,
);
await scaleAndSettingsButton.click();
}
/** Gets the console message element */
getConsoleMessage(): Locator {
return this.frame.getByTestId("notification-console/header-status");
}
/** Waits for the Data Explorer app to load */ /** Waits for the Data Explorer app to load */
static async waitForExplorer(page: Page) { static async waitForExplorer(page: Page) {
const iframeElement = await page.getByTestId("DataExplorerFrame").elementHandle(); const iframeElement = await page.getByTestId("DataExplorerFrame").elementHandle();

View File

@@ -1,6 +1,6 @@
import { expect, test } from "@playwright/test"; import { expect, test } from "@playwright/test";
import { DataExplorer, Editor, QueryTab, TestAccount } from "../fx"; import { CommandBarButton, DataExplorer, Editor, QueryTab, TestAccount } from "../fx";
import { TestContainerContext, TestItem, createTestSQLContainer } from "../testData"; import { TestContainerContext, TestItem, createTestSQLContainer } from "../testData";
let context: TestContainerContext = null!; let context: TestContainerContext = null!;
@@ -37,7 +37,7 @@ test.afterAll("Delete Test Database", async () => {
test("Query results", async () => { test("Query results", async () => {
// Run the query and verify the results // Run the query and verify the results
await queryEditor.locator.click(); await queryEditor.locator.click();
const executeQueryButton = explorer.commandBarButton("Execute Query"); const executeQueryButton = explorer.commandBarButton(CommandBarButton.ExecuteQuery);
await executeQueryButton.click(); await executeQueryButton.click();
await expect(queryTab.resultsEditor.locator).toBeAttached({ timeout: 60 * 1000 }); await expect(queryTab.resultsEditor.locator).toBeAttached({ timeout: 60 * 1000 });
@@ -59,7 +59,7 @@ test("Query results", async () => {
test("Query stats", async () => { test("Query stats", async () => {
// Run the query and verify the results // Run the query and verify the results
await queryEditor.locator.click(); await queryEditor.locator.click();
const executeQueryButton = explorer.commandBarButton("Execute Query"); const executeQueryButton = explorer.commandBarButton(CommandBarButton.ExecuteQuery);
await executeQueryButton.click(); await executeQueryButton.click();
await expect(queryTab.resultsEditor.locator).toBeAttached({ timeout: 60 * 1000 }); await expect(queryTab.resultsEditor.locator).toBeAttached({ timeout: 60 * 1000 });
@@ -77,7 +77,7 @@ test("Query errors", async () => {
await queryEditor.setText("SELECT\n glarb(c.id),\n blarg(c.id)\nFROM c"); await queryEditor.setText("SELECT\n glarb(c.id),\n blarg(c.id)\nFROM c");
// Run the query and verify the results // Run the query and verify the results
const executeQueryButton = explorer.commandBarButton("Execute Query"); const executeQueryButton = explorer.commandBarButton(CommandBarButton.ExecuteQuery);
await executeQueryButton.click(); await executeQueryButton.click();
await expect(queryTab.errorList).toBeAttached({ timeout: 60 * 1000 }); await expect(queryTab.errorList).toBeAttached({ timeout: 60 * 1000 });

View File

@@ -0,0 +1,129 @@
import { expect, Locator, test } from "@playwright/test";
import {
CommandBarButton,
DataExplorer,
ONE_MINUTE_MS,
TEST_AUTOSCALE_MAX_THROUGHPUT_RU_2K,
TEST_MANUAL_THROUGHPUT_RU_2K,
TestAccount,
} from "../../fx";
import { createTestSQLContainer, TestContainerContext } from "../../testData";
test.describe("Autoscale and Manual throughput", () => {
let context: TestContainerContext = null!;
let explorer: DataExplorer = null!;
test.beforeAll("Create Test Database", async () => {
context = await createTestSQLContainer(true);
});
test.beforeEach("Open container settings", async ({ page }) => {
explorer = await DataExplorer.open(page, TestAccount.SQL);
// Click Scale & Settings and open Scale tab
await explorer.openScaleAndSettings(context);
const scaleTab = explorer.frame.getByTestId("settings-tab-header/ScaleTab");
await scaleTab.click();
});
test.afterAll("Delete Test Database", async () => {
await context?.dispose();
});
test("Update autoscale max throughput", async () => {
// By default the created container has manual throughput (Containers created via JS SDK v4.7.0 cannot be created with autoscale throughput)
await switchManualToAutoscaleThroughput();
// Update autoscale max throughput
await getThroughputInput("autopilot").fill(TEST_AUTOSCALE_MAX_THROUGHPUT_RU_2K.toString());
// Save
await explorer.commandBarButton(CommandBarButton.Save).click();
// Read console message
await expect(explorer.getConsoleMessage()).toContainText(
`Successfully updated offer for collection ${context.container.id}`,
{
timeout: 2 * ONE_MINUTE_MS,
},
);
});
test("Update autoscale max throughput passed allowed limit", async () => {
// By default the created container has manual throughput (Containers created via JS SDK v4.7.0 cannot be created with autoscale throughput)
await switchManualToAutoscaleThroughput();
// Get soft allowed max throughput and remove commas
const softAllowedMaxThroughputString = await explorer.frame
.getByTestId("soft-allowed-maximum-throughput")
.innerText();
const softAllowedMaxThroughput = Number(softAllowedMaxThroughputString.replace(/,/g, ""));
// Try to set autoscale max throughput above allowed limit
await getThroughputInput("autopilot").fill((softAllowedMaxThroughput * 10).toString());
await expect(explorer.commandBarButton(CommandBarButton.Save)).toBeDisabled();
await expect(getThroughputInputErrorMessage("autopilot")).toContainText(
"This update isn't possible because it would increase the total throughput",
);
});
test("Update autoscale max throughput with invalid increment", async () => {
// By default the created container has manual throughput (Containers created via JS SDK v4.7.0 cannot be created with autoscale throughput)
await switchManualToAutoscaleThroughput();
// Try to set autoscale max throughput with invalid increment
await getThroughputInput("autopilot").fill("1100");
await expect(explorer.commandBarButton(CommandBarButton.Save)).toBeDisabled();
await expect(getThroughputInputErrorMessage("autopilot")).toContainText(
"Throughput value must be in increments of 1000",
);
});
test("Update manual throughput", async () => {
await getThroughputInput("manual").fill(TEST_MANUAL_THROUGHPUT_RU_2K.toString());
await explorer.commandBarButton(CommandBarButton.Save).click();
await expect(explorer.getConsoleMessage()).toContainText(
`Successfully updated offer for collection ${context.container.id}`,
{
timeout: 2 * ONE_MINUTE_MS,
},
);
});
test("Update manual throughput passed allowed limit", async () => {
// Get soft allowed max throughput and remove commas
const softAllowedMaxThroughputString = await explorer.frame
.getByTestId("soft-allowed-maximum-throughput")
.innerText();
const softAllowedMaxThroughput = Number(softAllowedMaxThroughputString.replace(/,/g, ""));
// Try to set manual throughput above allowed limit
await getThroughputInput("manual").fill((softAllowedMaxThroughput * 10).toString());
await expect(explorer.commandBarButton(CommandBarButton.Save)).toBeDisabled();
await expect(getThroughputInputErrorMessage("manual")).toContainText(
"This update isn't possible because it would increase the total throughput",
);
});
// Helper methods
const getThroughputInput = (type: "manual" | "autopilot"): Locator => {
return explorer.frame.getByTestId(`${type}-throughput-input`);
};
const getThroughputInputErrorMessage = (type: "manual" | "autopilot"): Locator => {
return explorer.frame.getByTestId(`${type}-throughput-input-error`);
};
const switchManualToAutoscaleThroughput = async (): Promise<void> => {
const autoscaleRadioButton = explorer.frame.getByText("Autoscale", { exact: true });
await autoscaleRadioButton.click();
await expect(explorer.commandBarButton(CommandBarButton.Save)).toBeEnabled();
await explorer.commandBarButton(CommandBarButton.Save).click();
await expect(explorer.getConsoleMessage()).toContainText(
`Successfully updated offer for collection ${context.container.id}`,
{
timeout: ONE_MINUTE_MS,
},
);
};
});

View File

@@ -0,0 +1,70 @@
import { expect, test } from "@playwright/test";
import { CommandBarButton, DataExplorer, ONE_MINUTE_MS, TestAccount } from "../../fx";
import { createTestSQLContainer, TestContainerContext } from "../../testData";
test.describe("Settings under Scale & Settings", () => {
let context: TestContainerContext = null!;
let explorer: DataExplorer = null!;
test.beforeAll("Create Test Database", async () => {
context = await createTestSQLContainer(true);
});
test.beforeEach("Open Settings tab under Scale & Settings", async ({ page }) => {
explorer = await DataExplorer.open(page, TestAccount.SQL);
const containerNode = await explorer.waitForContainerNode(context.database.id, context.container.id);
await containerNode.expand();
// Click Scale & Settings and open Scale tab
await explorer.openScaleAndSettings(context);
const settingsTab = explorer.frame.getByTestId("settings-tab-header/SubSettingsTab");
await settingsTab.click();
});
test.afterAll("Delete Test Database", async () => {
await context?.dispose();
});
test("Update TTL to On (no default)", async () => {
const ttlOnNoDefaultRadioButton = explorer.frame.getByRole("radio", { name: "ttl-on-no-default-option" });
await ttlOnNoDefaultRadioButton.click();
await explorer.commandBarButton(CommandBarButton.Save).click();
await expect(explorer.getConsoleMessage()).toContainText(`Successfully updated container ${context.container.id}`, {
timeout: ONE_MINUTE_MS,
});
});
test("Update TTL to On (with user entry)", async () => {
const ttlOnRadioButton = explorer.frame.getByRole("radio", { name: "ttl-on-option" });
await ttlOnRadioButton.click();
// Enter TTL seconds
const ttlInput = explorer.frame.getByTestId("ttl-input");
await ttlInput.fill("30000");
await explorer.commandBarButton(CommandBarButton.Save).click();
await expect(explorer.getConsoleMessage()).toContainText(`Successfully updated container ${context.container.id}`, {
timeout: ONE_MINUTE_MS,
});
});
test("Update TTL to Off", async () => {
// By default TTL is set to off so we need to first set it to On
const ttlOnNoDefaultRadioButton = explorer.frame.getByRole("radio", { name: "ttl-on-no-default-option" });
await ttlOnNoDefaultRadioButton.click();
await explorer.commandBarButton(CommandBarButton.Save).click();
await expect(explorer.getConsoleMessage()).toContainText(`Successfully updated container ${context.container.id}`, {
timeout: ONE_MINUTE_MS,
});
// Set it to Off
const ttlOffRadioButton = explorer.frame.getByRole("radio", { name: "ttl-off-option" });
await ttlOffRadioButton.click();
await explorer.commandBarButton(CommandBarButton.Save).click();
await expect(explorer.getConsoleMessage()).toContainText(`Successfully updated container ${context.container.id}`, {
timeout: ONE_MINUTE_MS,
});
});
});