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
84 changed files with 1694 additions and 3933 deletions

View File

@@ -14,7 +14,7 @@ export default defineConfig({
trace: "off", trace: "off",
video: "off", video: "off",
screenshot: "on", screenshot: "on",
testIdAttribute: "data-testid", testIdAttribute: "data-test",
contextOptions: { contextOptions: {
ignoreHTTPSErrors: true, ignoreHTTPSErrors: true,
}, },

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

@@ -76,10 +76,12 @@ const mockedRbacUtils = RbacUtils as jest.Mocked<typeof RbacUtils>;
const mockedCopyJobPrerequisitesCache = CopyJobPrerequisitesCacheModule as jest.Mocked< const mockedCopyJobPrerequisitesCache = CopyJobPrerequisitesCacheModule as jest.Mocked<
typeof CopyJobPrerequisitesCacheModule typeof CopyJobPrerequisitesCacheModule
>; >;
interface TestWrapperProps { interface TestWrapperProps {
state: CopyJobContextState; state: CopyJobContextState;
onResult?: (result: PermissionGroupConfig[]) => void; onResult?: (result: PermissionGroupConfig[]) => void;
} }
const TestWrapper: React.FC<TestWrapperProps> = ({ state, onResult }) => { const TestWrapper: React.FC<TestWrapperProps> = ({ state, onResult }) => {
const result = usePermissionSections(state); const result = usePermissionSections(state);

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

@@ -214,9 +214,9 @@ export const Dialog: FC = () => {
{contentHtml} {contentHtml}
{progressIndicatorProps && <ProgressIndicator {...progressIndicatorProps} />} {progressIndicatorProps && <ProgressIndicator {...progressIndicatorProps} />}
<DialogFooter> <DialogFooter>
<PrimaryButton {...primaryButtonProps} data-testid={`DialogButton:${primaryButtonText}`} /> <PrimaryButton {...primaryButtonProps} data-test={`DialogButton:${primaryButtonText}`} />
{secondaryButtonProps && ( {secondaryButtonProps && (
<DefaultButton {...secondaryButtonProps} data-testid={`DialogButton:${secondaryButtonText}`} /> <DefaultButton {...secondaryButtonProps} data-test={`DialogButton:${secondaryButtonText}`} />
)} )}
</DialogFooter> </DialogFooter>
</FluentDialog> </FluentDialog>

View File

@@ -137,7 +137,7 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
<Spinner size={SpinnerSize.large} className={this.props.spinnerClassName || "spinner"} /> <Spinner size={SpinnerSize.large} className={this.props.spinnerClassName || "spinner"} />
)} )}
<div <div
data-testid="EditorReact/Host/Unloaded" data-test="EditorReact/Host/Unloaded"
className={this.props.className || "jsonEditor"} className={this.props.className || "jsonEditor"}
style={this.props.monacoContainerStyles} style={this.props.monacoContainerStyles}
ref={(elt: HTMLElement) => this.setRef(elt)} ref={(elt: HTMLElement) => this.setRef(elt)}
@@ -148,7 +148,7 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
protected configureEditor(editor: monaco.editor.IStandaloneCodeEditor) { protected configureEditor(editor: monaco.editor.IStandaloneCodeEditor) {
this.editor = editor; this.editor = editor;
this.rootNode.dataset["testid"] = "EditorReact/Host/Loaded"; this.rootNode.dataset["test"] = "EditorReact/Host/Loaded";
// In development, we want to be able to access the editor instance from the console // In development, we want to be able to access the editor instance from the console
if (process.env.NODE_ENV === "development") { if (process.env.NODE_ENV === "development") {

View File

@@ -193,7 +193,7 @@ export const InputDataList: FC<InputDataListProps> = ({
<> <>
<Input <Input
id="filterInput" id="filterInput"
data-testid={"DocumentsTab/FilterInput"} data-test={"DocumentsTab/FilterInput"}
ref={inputRef} ref={inputRef}
type="text" type="text"
size="small" size="small"

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

@@ -209,7 +209,6 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
checked={isAutoscaleSelected} checked={isAutoscaleSelected}
type="radio" type="radio"
role="radio" role="radio"
data-testid="ThroughputInput/ThroughputMode:Autoscale"
tabIndex={0} tabIndex={0}
onChange={(e) => handleOnChangeMode(e, "Autoscale")} onChange={(e) => handleOnChangeMode(e, "Autoscale")}
/> />
@@ -225,7 +224,6 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
type="radio" type="radio"
aria-required={true} aria-required={true}
role="radio" role="radio"
data-testid="ThroughputInput/ThroughputMode:Manual"
tabIndex={0} tabIndex={0}
onChange={(e) => handleOnChangeMode(e, "Manual")} onChange={(e) => handleOnChangeMode(e, "Manual")}
/> />
@@ -288,7 +286,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
</Stack> </Stack>
<TextField <TextField
id="autoscaleRUValueField" id="autoscaleRUValueField"
data-testid="ThroughputInput/AutoscaleRUInput" data-test="autoscaleRUInput"
type="number" type="number"
styles={{ styles={{
fieldGroup: { width: 100, height: 27, flexShrink: 0 }, fieldGroup: { width: 100, height: 27, flexShrink: 0 },
@@ -354,7 +352,6 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
} }
> >
<TextField <TextField
data-testid="ThroughputInput/ManualThroughputInput"
type="number" type="number"
styles={{ styles={{
fieldGroup: { width: 300, height: 27 }, fieldGroup: { width: 300, height: 27 },

View File

@@ -682,7 +682,6 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
aria-required={true} aria-required={true}
checked={true} checked={true}
className="throughputInputRadioBtn" className="throughputInputRadioBtn"
data-testid="ThroughputInput/ThroughputMode:Autoscale"
id="Autoscale-input" id="Autoscale-input"
onChange={[Function]} onChange={[Function]}
role="radio" role="radio"
@@ -700,7 +699,6 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
aria-required={true} aria-required={true}
checked={false} checked={false}
className="throughputInputRadioBtn" className="throughputInputRadioBtn"
data-testid="ThroughputInput/ThroughputMode:Manual"
id="Manual-input" id="Manual-input"
onChange={[Function]} onChange={[Function]}
role="radio" role="radio"
@@ -2146,7 +2144,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
</Stack> </Stack>
<StyledTextFieldBase <StyledTextFieldBase
ariaLabel="Container max RU/s" ariaLabel="Container max RU/s"
data-testid="ThroughputInput/AutoscaleRUInput" data-test="autoscaleRUInput"
errorMessage="" errorMessage=""
id="autoscaleRUValueField" id="autoscaleRUValueField"
key=".0:$.$.1" key=".0:$.$.1"
@@ -2173,7 +2171,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
> >
<TextFieldBase <TextFieldBase
ariaLabel="Container max RU/s" ariaLabel="Container max RU/s"
data-testid="ThroughputInput/AutoscaleRUInput" data-test="autoscaleRUInput"
deferredValidationTime={200} deferredValidationTime={200}
errorMessage="" errorMessage=""
id="autoscaleRUValueField" id="autoscaleRUValueField"
@@ -2474,7 +2472,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
aria-invalid={false} aria-invalid={false}
aria-label="Container max RU/s" aria-label="Container max RU/s"
className="ms-TextField-field field-124" className="ms-TextField-field field-124"
data-testid="ThroughputInput/AutoscaleRUInput" data-test="autoscaleRUInput"
id="autoscaleRUValueField" id="autoscaleRUValueField"
max="9007199254740991" max="9007199254740991"
min={1000} min={1000}

View File

@@ -139,7 +139,7 @@ export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
const contextMenuItems = (node.contextMenu ?? []).map((menuItem) => ( const contextMenuItems = (node.contextMenu ?? []).map((menuItem) => (
<MenuItem <MenuItem
data-testid={`TreeNode/ContextMenuItem:${menuItem.label}`} data-test={`TreeNode/ContextMenuItem:${menuItem.label}`}
disabled={menuItem.isDisabled} disabled={menuItem.isDisabled}
key={menuItem.label} key={menuItem.label}
onClick={() => menuItem.onClick(contextMenuRef)} onClick={() => menuItem.onClick(contextMenuRef)}
@@ -160,14 +160,14 @@ export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
const expandIcon = isLoading ? ( const expandIcon = isLoading ? (
<Spinner size="extra-tiny" /> <Spinner size="extra-tiny" />
) : !isBranch ? undefined : openItems.includes(treeNodeId) ? ( ) : !isBranch ? undefined : openItems.includes(treeNodeId) ? (
<ChevronDown20Regular data-testid="TreeNode/CollapseIcon" /> <ChevronDown20Regular data-test="TreeNode/CollapseIcon" />
) : ( ) : (
<ChevronRight20Regular data-testid="TreeNode/ExpandIcon" /> <ChevronRight20Regular data-text="TreeNode/ExpandIcon" />
); );
const treeItem = ( const treeItem = (
<TreeItem <TreeItem
data-testid={`TreeNodeContainer:${treeNodeId}`} data-test={`TreeNodeContainer:${treeNodeId}`}
value={treeNodeId} value={treeNodeId}
itemType={isBranch ? "branch" : "leaf"} itemType={isBranch ? "branch" : "leaf"}
onOpenChange={onOpenChange} onOpenChange={onOpenChange}
@@ -179,7 +179,7 @@ export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
shouldShowAsSelected && treeStyles.selectedItem, shouldShowAsSelected && treeStyles.selectedItem,
node.className && treeStyles[node.className], node.className && treeStyles[node.className],
)} )}
data-testid={`TreeNode:${treeNodeId}`} data-test={`TreeNode:${treeNodeId}`}
actions={ actions={
contextMenuItems.length > 0 && { contextMenuItems.length > 0 && {
className: treeStyles.actionsButtonContainer, className: treeStyles.actionsButtonContainer,
@@ -189,13 +189,13 @@ export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
<Button <Button
aria-label="More options" aria-label="More options"
className={mergeClasses(treeStyles.actionsButton, shouldShowAsSelected && treeStyles.selectedItem)} className={mergeClasses(treeStyles.actionsButton, shouldShowAsSelected && treeStyles.selectedItem)}
data-testid="TreeNode/ContextMenuTrigger" data-test="TreeNode/ContextMenuTrigger"
appearance="subtle" appearance="subtle"
ref={contextMenuRef} ref={contextMenuRef}
icon={<MoreHorizontal20Regular />} icon={<MoreHorizontal20Regular />}
/> />
</MenuTrigger> </MenuTrigger>
<MenuPopover data-testid={`TreeNode/ContextMenu:${treeNodeId}`}> <MenuPopover data-test={`TreeNode/ContextMenu:${treeNodeId}`}>
<MenuList>{contextMenuItems}</MenuList> <MenuList>{contextMenuItems}</MenuList>
</MenuPopover> </MenuPopover>
</Menu> </Menu>
@@ -208,7 +208,7 @@ export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
<span className={treeStyles.nodeLabel}>{node.label}</span> <span className={treeStyles.nodeLabel}>{node.label}</span>
</TreeItemLayout> </TreeItemLayout>
{!node.isLoading && node.children?.length > 0 && ( {!node.isLoading && node.children?.length > 0 && (
<Tree data-testid={`Tree:${treeNodeId}`} className={treeStyles.tree}> <Tree data-test={`Tree:${treeNodeId}`} className={treeStyles.tree}>
{getSortedChildren(node).map((childNode: TreeNode) => ( {getSortedChildren(node).map((childNode: TreeNode) => (
<TreeNodeComponent <TreeNodeComponent
openItems={openItems} openItems={openItems}

View File

@@ -3,7 +3,7 @@
exports[`TreeNodeComponent does not render children if the node is loading 1`] = ` exports[`TreeNodeComponent does not render children if the node is loading 1`] = `
<TreeItem <TreeItem
className="" className=""
data-testid="TreeNodeContainer:root" data-test="TreeNodeContainer:root"
itemType="branch" itemType="branch"
onOpenChange={[Function]} onOpenChange={[Function]}
value="root" value="root"
@@ -11,10 +11,10 @@ exports[`TreeNodeComponent does not render children if the node is loading 1`] =
<TreeItemLayout <TreeItemLayout
actions={false} actions={false}
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-testid="TreeNode:root" data-test="TreeNode:root"
expandIcon={ expandIcon={
<ChevronRight20Regular <ChevronRight20Regular
data-testid="TreeNode/ExpandIcon" data-text="TreeNode/ExpandIcon"
/> />
} }
iconBefore={ iconBefore={
@@ -112,7 +112,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
> >
<TreeItem <TreeItem
className="" className=""
data-testid="TreeNodeContainer:root" data-test="TreeNodeContainer:root"
itemType="branch" itemType="branch"
onOpenChange={[Function]} onOpenChange={[Function]}
value="root" value="root"
@@ -122,7 +122,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
aria-level={0} aria-level={0}
className="fui-TreeItem r15xhw3a" className="fui-TreeItem r15xhw3a"
data-fui-tree-item-value="root" data-fui-tree-item-value="root"
data-testid="TreeNodeContainer:root" data-test="TreeNodeContainer:root"
onChange={[Function]} onChange={[Function]}
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}
@@ -144,7 +144,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
<svg <svg
aria-hidden="true" aria-hidden="true"
class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0" class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0"
data-testid="TreeNode/ExpandIcon" data-text="TreeNode/ExpandIcon"
fill="currentColor" fill="currentColor"
height="20" height="20"
viewBox="0 0 20 20" viewBox="0 0 20 20"
@@ -164,7 +164,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
"layoutRef": { "layoutRef": {
"current": <div "current": <div
class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-testid="TreeNode:root" data-test="TreeNode:root"
> >
<div <div
aria-hidden="true" aria-hidden="true"
@@ -173,7 +173,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
<svg <svg
aria-hidden="true" aria-hidden="true"
class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0" class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0"
data-testid="TreeNode/ExpandIcon" data-text="TreeNode/ExpandIcon"
fill="currentColor" fill="currentColor"
height="20" height="20"
viewBox="0 0 20 20" viewBox="0 0 20 20"
@@ -220,13 +220,13 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
aria-level="0" aria-level="0"
class="fui-TreeItem r15xhw3a" class="fui-TreeItem r15xhw3a"
data-fui-tree-item-value="root" data-fui-tree-item-value="root"
data-testid="TreeNodeContainer:root" data-test="TreeNodeContainer:root"
role="treeitem" role="treeitem"
tabindex="-1" tabindex="-1"
> >
<div <div
class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-testid="TreeNode:root" data-test="TreeNode:root"
> >
<div <div
aria-hidden="true" aria-hidden="true"
@@ -235,7 +235,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
<svg <svg
aria-hidden="true" aria-hidden="true"
class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0" class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0"
data-testid="TreeNode/ExpandIcon" data-text="TreeNode/ExpandIcon"
fill="currentColor" fill="currentColor"
height="20" height="20"
viewBox="0 0 20 20" viewBox="0 0 20 20"
@@ -270,7 +270,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
</div> </div>
<div <div
class="fui-Tree rnv2ez3 ___17a32do_7zrvj80 f1acs6jw f11qra4b fepn2xe f1nbblvp f19d5ny4 fzz4f4n" class="fui-Tree rnv2ez3 ___17a32do_7zrvj80 f1acs6jw f11qra4b fepn2xe f1nbblvp f19d5ny4 fzz4f4n"
data-testid="Tree:root" data-test="Tree:root"
role="tree" role="tree"
> >
<div <div
@@ -278,13 +278,13 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
aria-level="1" aria-level="1"
class="fui-TreeItem r15xhw3a ___jer8zr0_6no3ah0 f10bgyvd" class="fui-TreeItem r15xhw3a ___jer8zr0_6no3ah0 f10bgyvd"
data-fui-tree-item-value="root/child1Label" data-fui-tree-item-value="root/child1Label"
data-testid="TreeNodeContainer:root/child1Label" data-test="TreeNodeContainer:root/child1Label"
role="treeitem" role="treeitem"
tabindex="0" tabindex="0"
> >
<div <div
class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-testid="TreeNode:root/child1Label" data-test="TreeNode:root/child1Label"
> >
<div <div
aria-hidden="true" aria-hidden="true"
@@ -293,7 +293,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
<svg <svg
aria-hidden="true" aria-hidden="true"
class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0" class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0"
data-testid="TreeNode/ExpandIcon" data-text="TreeNode/ExpandIcon"
fill="currentColor" fill="currentColor"
height="20" height="20"
viewBox="0 0 20 20" viewBox="0 0 20 20"
@@ -332,13 +332,13 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
aria-level="1" aria-level="1"
class="fui-TreeItem r15xhw3a ___jer8zr0_6no3ah0 f10bgyvd" class="fui-TreeItem r15xhw3a ___jer8zr0_6no3ah0 f10bgyvd"
data-fui-tree-item-value="root/child2LoadingLabel" data-fui-tree-item-value="root/child2LoadingLabel"
data-testid="TreeNodeContainer:root/child2LoadingLabel" data-test="TreeNodeContainer:root/child2LoadingLabel"
role="treeitem" role="treeitem"
tabindex="-1" tabindex="-1"
> >
<div <div
class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-testid="TreeNode:root/child2LoadingLabel" data-test="TreeNode:root/child2LoadingLabel"
> >
<div <div
aria-hidden="true" aria-hidden="true"
@@ -347,7 +347,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
<svg <svg
aria-hidden="true" aria-hidden="true"
class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0" class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0"
data-testid="TreeNode/ExpandIcon" data-text="TreeNode/ExpandIcon"
fill="currentColor" fill="currentColor"
height="20" height="20"
viewBox="0 0 20 20" viewBox="0 0 20 20"
@@ -385,13 +385,13 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
aria-level="1" aria-level="1"
class="fui-TreeItem r15xhw3a ___jer8zr0_6no3ah0 f10bgyvd" class="fui-TreeItem r15xhw3a ___jer8zr0_6no3ah0 f10bgyvd"
data-fui-tree-item-value="root/child3ExpandingLabel" data-fui-tree-item-value="root/child3ExpandingLabel"
data-testid="TreeNodeContainer:root/child3ExpandingLabel" data-test="TreeNodeContainer:root/child3ExpandingLabel"
role="treeitem" role="treeitem"
tabindex="-1" tabindex="-1"
> >
<div <div
class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_vz3p260 fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_vz3p260 fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-testid="TreeNode:root/child3ExpandingLabel" data-test="TreeNode:root/child3ExpandingLabel"
> >
<div <div
aria-hidden="true" aria-hidden="true"
@@ -441,10 +441,10 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
<TreeItemLayout <TreeItemLayout
actions={false} actions={false}
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-testid="TreeNode:root" data-test="TreeNode:root"
expandIcon={ expandIcon={
<ChevronRight20Regular <ChevronRight20Regular
data-testid="TreeNode/ExpandIcon" data-text="TreeNode/ExpandIcon"
/> />
} }
iconBefore={ iconBefore={
@@ -457,19 +457,19 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
> >
<div <div
className="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" className="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-testid="TreeNode:root" data-test="TreeNode:root"
> >
<div <div
aria-hidden={true} aria-hidden={true}
className="fui-TreeItemLayout__expandIcon rh4pu5o" className="fui-TreeItemLayout__expandIcon rh4pu5o"
> >
<ChevronRight20Regular <ChevronRight20Regular
data-testid="TreeNode/ExpandIcon" data-text="TreeNode/ExpandIcon"
> >
<svg <svg
aria-hidden={true} aria-hidden={true}
className="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0" className="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0"
data-testid="TreeNode/ExpandIcon" data-text="TreeNode/ExpandIcon"
fill="currentColor" fill="currentColor"
height="20" height="20"
viewBox="0 0 20 20" viewBox="0 0 20 20"
@@ -506,7 +506,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
</TreeItemLayout> </TreeItemLayout>
<Tree <Tree
className="___17a32do_0000000 f1acs6jw f11qra4b fepn2xe f1nbblvp f19d5ny4 fzz4f4n" className="___17a32do_0000000 f1acs6jw f11qra4b fepn2xe f1nbblvp f19d5ny4 fzz4f4n"
data-testid="Tree:root" data-test="Tree:root"
> >
<TreeProvider <TreeProvider
value={ value={
@@ -574,7 +574,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
> >
<div <div
className="fui-Tree rnv2ez3 ___17a32do_7zrvj80 f1acs6jw f11qra4b fepn2xe f1nbblvp f19d5ny4 fzz4f4n" className="fui-Tree rnv2ez3 ___17a32do_7zrvj80 f1acs6jw f11qra4b fepn2xe f1nbblvp f19d5ny4 fzz4f4n"
data-testid="Tree:root" data-test="Tree:root"
role="tree" role="tree"
> >
<TreeNodeComponent <TreeNodeComponent
@@ -610,7 +610,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
> >
<TreeItem <TreeItem
className="" className=""
data-testid="TreeNodeContainer:root/child1Label" data-test="TreeNodeContainer:root/child1Label"
itemType="branch" itemType="branch"
onOpenChange={[Function]} onOpenChange={[Function]}
value="root/child1Label" value="root/child1Label"
@@ -620,7 +620,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
aria-level={1} aria-level={1}
className="fui-TreeItem r15xhw3a ___jer8zr0_6no3ah0 f10bgyvd" className="fui-TreeItem r15xhw3a ___jer8zr0_6no3ah0 f10bgyvd"
data-fui-tree-item-value="root/child1Label" data-fui-tree-item-value="root/child1Label"
data-testid="TreeNodeContainer:root/child1Label" data-test="TreeNodeContainer:root/child1Label"
onChange={[Function]} onChange={[Function]}
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}
@@ -642,7 +642,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
<svg <svg
aria-hidden="true" aria-hidden="true"
class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0" class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0"
data-testid="TreeNode/ExpandIcon" data-text="TreeNode/ExpandIcon"
fill="currentColor" fill="currentColor"
height="20" height="20"
viewBox="0 0 20 20" viewBox="0 0 20 20"
@@ -662,7 +662,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
"layoutRef": { "layoutRef": {
"current": <div "current": <div
class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-testid="TreeNode:root/child1Label" data-test="TreeNode:root/child1Label"
> >
<div <div
aria-hidden="true" aria-hidden="true"
@@ -671,7 +671,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
<svg <svg
aria-hidden="true" aria-hidden="true"
class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0" class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0"
data-testid="TreeNode/ExpandIcon" data-text="TreeNode/ExpandIcon"
fill="currentColor" fill="currentColor"
height="20" height="20"
viewBox="0 0 20 20" viewBox="0 0 20 20"
@@ -718,13 +718,13 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
aria-level="1" aria-level="1"
class="fui-TreeItem r15xhw3a ___jer8zr0_6no3ah0 f10bgyvd" class="fui-TreeItem r15xhw3a ___jer8zr0_6no3ah0 f10bgyvd"
data-fui-tree-item-value="root/child1Label" data-fui-tree-item-value="root/child1Label"
data-testid="TreeNodeContainer:root/child1Label" data-test="TreeNodeContainer:root/child1Label"
role="treeitem" role="treeitem"
tabindex="0" tabindex="0"
> >
<div <div
class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-testid="TreeNode:root/child1Label" data-test="TreeNode:root/child1Label"
> >
<div <div
aria-hidden="true" aria-hidden="true"
@@ -733,7 +733,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
<svg <svg
aria-hidden="true" aria-hidden="true"
class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0" class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0"
data-testid="TreeNode/ExpandIcon" data-text="TreeNode/ExpandIcon"
fill="currentColor" fill="currentColor"
height="20" height="20"
viewBox="0 0 20 20" viewBox="0 0 20 20"
@@ -775,10 +775,10 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
<TreeItemLayout <TreeItemLayout
actions={false} actions={false}
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-testid="TreeNode:root/child1Label" data-test="TreeNode:root/child1Label"
expandIcon={ expandIcon={
<ChevronRight20Regular <ChevronRight20Regular
data-testid="TreeNode/ExpandIcon" data-text="TreeNode/ExpandIcon"
/> />
} }
iconBefore={ iconBefore={
@@ -791,19 +791,19 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
> >
<div <div
className="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" className="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-testid="TreeNode:root/child1Label" data-test="TreeNode:root/child1Label"
> >
<div <div
aria-hidden={true} aria-hidden={true}
className="fui-TreeItemLayout__expandIcon rh4pu5o" className="fui-TreeItemLayout__expandIcon rh4pu5o"
> >
<ChevronRight20Regular <ChevronRight20Regular
data-testid="TreeNode/ExpandIcon" data-text="TreeNode/ExpandIcon"
> >
<svg <svg
aria-hidden={true} aria-hidden={true}
className="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0" className="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0"
data-testid="TreeNode/ExpandIcon" data-text="TreeNode/ExpandIcon"
fill="currentColor" fill="currentColor"
height="20" height="20"
viewBox="0 0 20 20" viewBox="0 0 20 20"
@@ -840,7 +840,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
</TreeItemLayout> </TreeItemLayout>
<Tree <Tree
className="___17a32do_0000000 f1acs6jw f11qra4b fepn2xe f1nbblvp f19d5ny4 fzz4f4n" className="___17a32do_0000000 f1acs6jw f11qra4b fepn2xe f1nbblvp f19d5ny4 fzz4f4n"
data-testid="Tree:root/child1Label" data-test="Tree:root/child1Label"
> >
<TreeProvider <TreeProvider
value={ value={
@@ -881,7 +881,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
> >
<TreeItem <TreeItem
className="" className=""
data-testid="TreeNodeContainer:root/child2LoadingLabel" data-test="TreeNodeContainer:root/child2LoadingLabel"
itemType="branch" itemType="branch"
onOpenChange={[Function]} onOpenChange={[Function]}
value="root/child2LoadingLabel" value="root/child2LoadingLabel"
@@ -891,7 +891,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
aria-level={1} aria-level={1}
className="fui-TreeItem r15xhw3a ___jer8zr0_6no3ah0 f10bgyvd" className="fui-TreeItem r15xhw3a ___jer8zr0_6no3ah0 f10bgyvd"
data-fui-tree-item-value="root/child2LoadingLabel" data-fui-tree-item-value="root/child2LoadingLabel"
data-testid="TreeNodeContainer:root/child2LoadingLabel" data-test="TreeNodeContainer:root/child2LoadingLabel"
onChange={[Function]} onChange={[Function]}
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}
@@ -913,7 +913,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
<svg <svg
aria-hidden="true" aria-hidden="true"
class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0" class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0"
data-testid="TreeNode/ExpandIcon" data-text="TreeNode/ExpandIcon"
fill="currentColor" fill="currentColor"
height="20" height="20"
viewBox="0 0 20 20" viewBox="0 0 20 20"
@@ -933,7 +933,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
"layoutRef": { "layoutRef": {
"current": <div "current": <div
class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-testid="TreeNode:root/child2LoadingLabel" data-test="TreeNode:root/child2LoadingLabel"
> >
<div <div
aria-hidden="true" aria-hidden="true"
@@ -942,7 +942,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
<svg <svg
aria-hidden="true" aria-hidden="true"
class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0" class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0"
data-testid="TreeNode/ExpandIcon" data-text="TreeNode/ExpandIcon"
fill="currentColor" fill="currentColor"
height="20" height="20"
viewBox="0 0 20 20" viewBox="0 0 20 20"
@@ -989,13 +989,13 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
aria-level="1" aria-level="1"
class="fui-TreeItem r15xhw3a ___jer8zr0_6no3ah0 f10bgyvd" class="fui-TreeItem r15xhw3a ___jer8zr0_6no3ah0 f10bgyvd"
data-fui-tree-item-value="root/child2LoadingLabel" data-fui-tree-item-value="root/child2LoadingLabel"
data-testid="TreeNodeContainer:root/child2LoadingLabel" data-test="TreeNodeContainer:root/child2LoadingLabel"
role="treeitem" role="treeitem"
tabindex="-1" tabindex="-1"
> >
<div <div
class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-testid="TreeNode:root/child2LoadingLabel" data-test="TreeNode:root/child2LoadingLabel"
> >
<div <div
aria-hidden="true" aria-hidden="true"
@@ -1004,7 +1004,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
<svg <svg
aria-hidden="true" aria-hidden="true"
class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0" class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0"
data-testid="TreeNode/ExpandIcon" data-text="TreeNode/ExpandIcon"
fill="currentColor" fill="currentColor"
height="20" height="20"
viewBox="0 0 20 20" viewBox="0 0 20 20"
@@ -1046,10 +1046,10 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
<TreeItemLayout <TreeItemLayout
actions={false} actions={false}
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-testid="TreeNode:root/child2LoadingLabel" data-test="TreeNode:root/child2LoadingLabel"
expandIcon={ expandIcon={
<ChevronRight20Regular <ChevronRight20Regular
data-testid="TreeNode/ExpandIcon" data-text="TreeNode/ExpandIcon"
/> />
} }
iconBefore={ iconBefore={
@@ -1062,19 +1062,19 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
> >
<div <div
className="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" className="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-testid="TreeNode:root/child2LoadingLabel" data-test="TreeNode:root/child2LoadingLabel"
> >
<div <div
aria-hidden={true} aria-hidden={true}
className="fui-TreeItemLayout__expandIcon rh4pu5o" className="fui-TreeItemLayout__expandIcon rh4pu5o"
> >
<ChevronRight20Regular <ChevronRight20Regular
data-testid="TreeNode/ExpandIcon" data-text="TreeNode/ExpandIcon"
> >
<svg <svg
aria-hidden={true} aria-hidden={true}
className="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0" className="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0"
data-testid="TreeNode/ExpandIcon" data-text="TreeNode/ExpandIcon"
fill="currentColor" fill="currentColor"
height="20" height="20"
viewBox="0 0 20 20" viewBox="0 0 20 20"
@@ -1140,7 +1140,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
> >
<TreeItem <TreeItem
className="" className=""
data-testid="TreeNodeContainer:root/child3ExpandingLabel" data-test="TreeNodeContainer:root/child3ExpandingLabel"
itemType="leaf" itemType="leaf"
onOpenChange={[Function]} onOpenChange={[Function]}
value="root/child3ExpandingLabel" value="root/child3ExpandingLabel"
@@ -1149,7 +1149,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
aria-level={1} aria-level={1}
className="fui-TreeItem r15xhw3a ___jer8zr0_6no3ah0 f10bgyvd" className="fui-TreeItem r15xhw3a ___jer8zr0_6no3ah0 f10bgyvd"
data-fui-tree-item-value="root/child3ExpandingLabel" data-fui-tree-item-value="root/child3ExpandingLabel"
data-testid="TreeNodeContainer:root/child3ExpandingLabel" data-test="TreeNodeContainer:root/child3ExpandingLabel"
onChange={[Function]} onChange={[Function]}
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}
@@ -1188,7 +1188,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
"layoutRef": { "layoutRef": {
"current": <div "current": <div
class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_vz3p260 fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_vz3p260 fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-testid="TreeNode:root/child3ExpandingLabel" data-test="TreeNode:root/child3ExpandingLabel"
> >
<div <div
aria-hidden="true" aria-hidden="true"
@@ -1240,13 +1240,13 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
aria-level="1" aria-level="1"
class="fui-TreeItem r15xhw3a ___jer8zr0_6no3ah0 f10bgyvd" class="fui-TreeItem r15xhw3a ___jer8zr0_6no3ah0 f10bgyvd"
data-fui-tree-item-value="root/child3ExpandingLabel" data-fui-tree-item-value="root/child3ExpandingLabel"
data-testid="TreeNodeContainer:root/child3ExpandingLabel" data-test="TreeNodeContainer:root/child3ExpandingLabel"
role="treeitem" role="treeitem"
tabindex="-1" tabindex="-1"
> >
<div <div
class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_vz3p260 fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_vz3p260 fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-testid="TreeNode:root/child3ExpandingLabel" data-test="TreeNode:root/child3ExpandingLabel"
> >
<div <div
aria-hidden="true" aria-hidden="true"
@@ -1294,7 +1294,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
<TreeItemLayout <TreeItemLayout
actions={false} actions={false}
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-testid="TreeNode:root/child3ExpandingLabel" data-test="TreeNode:root/child3ExpandingLabel"
iconBefore={ iconBefore={
<img <img
alt="" alt=""
@@ -1305,7 +1305,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
> >
<div <div
className="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_vz3p260 fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" className="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_vz3p260 fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-testid="TreeNode:root/child3ExpandingLabel" data-test="TreeNode:root/child3ExpandingLabel"
> >
<div <div
aria-hidden={true} aria-hidden={true}
@@ -1345,7 +1345,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
exports[`TreeNodeComponent renders a loading spinner if the node is loading: loaded 1`] = ` exports[`TreeNodeComponent renders a loading spinner if the node is loading: loaded 1`] = `
<TreeItem <TreeItem
className="" className=""
data-testid="TreeNodeContainer:root" data-test="TreeNodeContainer:root"
itemType="leaf" itemType="leaf"
onOpenChange={[Function]} onOpenChange={[Function]}
value="root" value="root"
@@ -1353,7 +1353,7 @@ exports[`TreeNodeComponent renders a loading spinner if the node is loading: loa
<TreeItemLayout <TreeItemLayout
actions={false} actions={false}
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-testid="TreeNode:root" data-test="TreeNode:root"
iconBefore={ iconBefore={
<img <img
alt="" alt=""
@@ -1374,7 +1374,7 @@ exports[`TreeNodeComponent renders a loading spinner if the node is loading: loa
exports[`TreeNodeComponent renders a loading spinner if the node is loading: loading 1`] = ` exports[`TreeNodeComponent renders a loading spinner if the node is loading: loading 1`] = `
<TreeItem <TreeItem
className="" className=""
data-testid="TreeNodeContainer:root" data-test="TreeNodeContainer:root"
itemType="leaf" itemType="leaf"
onOpenChange={[Function]} onOpenChange={[Function]}
value="root" value="root"
@@ -1382,7 +1382,7 @@ exports[`TreeNodeComponent renders a loading spinner if the node is loading: loa
<TreeItemLayout <TreeItemLayout
actions={false} actions={false}
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-testid="TreeNode:root" data-test="TreeNode:root"
expandIcon={ expandIcon={
<Spinner <Spinner
size="extra-tiny" size="extra-tiny"
@@ -1408,7 +1408,7 @@ exports[`TreeNodeComponent renders a loading spinner if the node is loading: loa
exports[`TreeNodeComponent renders a node as expandable if it has empty, but defined, children array 1`] = ` exports[`TreeNodeComponent renders a node as expandable if it has empty, but defined, children array 1`] = `
<TreeItem <TreeItem
className="" className=""
data-testid="TreeNodeContainer:root" data-test="TreeNodeContainer:root"
itemType="branch" itemType="branch"
onOpenChange={[Function]} onOpenChange={[Function]}
value="root" value="root"
@@ -1416,10 +1416,10 @@ exports[`TreeNodeComponent renders a node as expandable if it has empty, but def
<TreeItemLayout <TreeItemLayout
actions={false} actions={false}
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-testid="TreeNode:root" data-test="TreeNode:root"
expandIcon={ expandIcon={
<ChevronRight20Regular <ChevronRight20Regular
data-testid="TreeNode/ExpandIcon" data-text="TreeNode/ExpandIcon"
/> />
} }
iconBefore={ iconBefore={
@@ -1450,7 +1450,7 @@ exports[`TreeNodeComponent renders a node with a menu 1`] = `
> >
<TreeItem <TreeItem
className="" className=""
data-testid="TreeNodeContainer:root" data-test="TreeNodeContainer:root"
itemType="leaf" itemType="leaf"
onOpenChange={[Function]} onOpenChange={[Function]}
value="root" value="root"
@@ -1468,22 +1468,22 @@ exports[`TreeNodeComponent renders a node with a menu 1`] = `
appearance="subtle" appearance="subtle"
aria-label="More options" aria-label="More options"
className="___1pg0eu5_pgl3ex0 f1twygmj" className="___1pg0eu5_pgl3ex0 f1twygmj"
data-testid="TreeNode/ContextMenuTrigger" data-test="TreeNode/ContextMenuTrigger"
icon={<MoreHorizontal20Regular />} icon={<MoreHorizontal20Regular />}
/> />
</MenuTrigger> </MenuTrigger>
<MenuPopover <MenuPopover
data-testid="TreeNode/ContextMenu:root" data-test="TreeNode/ContextMenu:root"
> >
<MenuList> <MenuList>
<MenuItem <MenuItem
data-testid="TreeNode/ContextMenuItem:enabledItem" data-test="TreeNode/ContextMenuItem:enabledItem"
onClick={[Function]} onClick={[Function]}
> >
enabledItem enabledItem
</MenuItem> </MenuItem>
<MenuItem <MenuItem
data-testid="TreeNode/ContextMenuItem:disabledItem" data-test="TreeNode/ContextMenuItem:disabledItem"
disabled={true} disabled={true}
onClick={[Function]} onClick={[Function]}
> >
@@ -1496,7 +1496,7 @@ exports[`TreeNodeComponent renders a node with a menu 1`] = `
} }
} }
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-testid="TreeNode:root" data-test="TreeNode:root"
iconBefore={ iconBefore={
<img <img
alt="" alt=""
@@ -1516,14 +1516,14 @@ exports[`TreeNodeComponent renders a node with a menu 1`] = `
<MenuPopover> <MenuPopover>
<MenuList> <MenuList>
<MenuItem <MenuItem
data-testid="TreeNode/ContextMenuItem:enabledItem" data-test="TreeNode/ContextMenuItem:enabledItem"
key="enabledItem" key="enabledItem"
onClick={[Function]} onClick={[Function]}
> >
enabledItem enabledItem
</MenuItem> </MenuItem>
<MenuItem <MenuItem
data-testid="TreeNode/ContextMenuItem:disabledItem" data-test="TreeNode/ContextMenuItem:disabledItem"
disabled={true} disabled={true}
key="disabledItem" key="disabledItem"
onClick={[Function]} onClick={[Function]}
@@ -1538,7 +1538,7 @@ exports[`TreeNodeComponent renders a node with a menu 1`] = `
exports[`TreeNodeComponent renders a single node 1`] = ` exports[`TreeNodeComponent renders a single node 1`] = `
<TreeItem <TreeItem
className="" className=""
data-testid="TreeNodeContainer:root" data-test="TreeNodeContainer:root"
itemType="leaf" itemType="leaf"
onOpenChange={[Function]} onOpenChange={[Function]}
value="root" value="root"
@@ -1546,7 +1546,7 @@ exports[`TreeNodeComponent renders a single node 1`] = `
<TreeItemLayout <TreeItemLayout
actions={false} actions={false}
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-testid="TreeNode:root" data-test="TreeNode:root"
iconBefore={ iconBefore={
<img <img
alt="" alt=""
@@ -1567,7 +1567,7 @@ exports[`TreeNodeComponent renders a single node 1`] = `
exports[`TreeNodeComponent renders an icon if the node has one 1`] = ` exports[`TreeNodeComponent renders an icon if the node has one 1`] = `
<TreeItem <TreeItem
className="" className=""
data-testid="TreeNodeContainer:root" data-test="TreeNodeContainer:root"
itemType="leaf" itemType="leaf"
onOpenChange={[Function]} onOpenChange={[Function]}
value="root" value="root"
@@ -1575,7 +1575,7 @@ exports[`TreeNodeComponent renders an icon if the node has one 1`] = `
<TreeItemLayout <TreeItemLayout
actions={false} actions={false}
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-testid="TreeNode:root" data-test="TreeNode:root"
iconBefore={ iconBefore={
<img <img
alt="" alt=""
@@ -1596,7 +1596,7 @@ exports[`TreeNodeComponent renders an icon if the node has one 1`] = `
exports[`TreeNodeComponent renders selected parent node as selected if no descendant nodes are selected 1`] = ` exports[`TreeNodeComponent renders selected parent node as selected if no descendant nodes are selected 1`] = `
<TreeItem <TreeItem
className="" className=""
data-testid="TreeNodeContainer:root" data-test="TreeNodeContainer:root"
itemType="branch" itemType="branch"
onOpenChange={[Function]} onOpenChange={[Function]}
value="root" value="root"
@@ -1604,10 +1604,10 @@ exports[`TreeNodeComponent renders selected parent node as selected if no descen
<TreeItemLayout <TreeItemLayout
actions={false} actions={false}
className="___rq9vxg0_1ykn2d2 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1nfm20t f1do9gdl" className="___rq9vxg0_1ykn2d2 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1nfm20t f1do9gdl"
data-testid="TreeNode:root" data-test="TreeNode:root"
expandIcon={ expandIcon={
<ChevronRight20Regular <ChevronRight20Regular
data-testid="TreeNode/ExpandIcon" data-text="TreeNode/ExpandIcon"
/> />
} }
iconBefore={ iconBefore={
@@ -1626,7 +1626,7 @@ exports[`TreeNodeComponent renders selected parent node as selected if no descen
</TreeItemLayout> </TreeItemLayout>
<Tree <Tree
className="___17a32do_0000000 f1acs6jw f11qra4b fepn2xe f1nbblvp f19d5ny4 fzz4f4n" className="___17a32do_0000000 f1acs6jw f11qra4b fepn2xe f1nbblvp f19d5ny4 fzz4f4n"
data-testid="Tree:root" data-test="Tree:root"
> >
<TreeNodeComponent <TreeNodeComponent
key="child1Label" key="child1Label"
@@ -1679,7 +1679,7 @@ exports[`TreeNodeComponent renders selected parent node as selected if no descen
exports[`TreeNodeComponent renders selected parent node as unselected if any descendant node is selected 1`] = ` exports[`TreeNodeComponent renders selected parent node as unselected if any descendant node is selected 1`] = `
<TreeItem <TreeItem
className="" className=""
data-testid="TreeNodeContainer:root" data-test="TreeNodeContainer:root"
itemType="branch" itemType="branch"
onOpenChange={[Function]} onOpenChange={[Function]}
value="root" value="root"
@@ -1687,10 +1687,10 @@ exports[`TreeNodeComponent renders selected parent node as unselected if any des
<TreeItemLayout <TreeItemLayout
actions={false} actions={false}
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-testid="TreeNode:root" data-test="TreeNode:root"
expandIcon={ expandIcon={
<ChevronRight20Regular <ChevronRight20Regular
data-testid="TreeNode/ExpandIcon" data-text="TreeNode/ExpandIcon"
/> />
} }
iconBefore={ iconBefore={
@@ -1709,7 +1709,7 @@ exports[`TreeNodeComponent renders selected parent node as unselected if any des
</TreeItemLayout> </TreeItemLayout>
<Tree <Tree
className="___17a32do_0000000 f1acs6jw f11qra4b fepn2xe f1nbblvp f19d5ny4 fzz4f4n" className="___17a32do_0000000 f1acs6jw f11qra4b fepn2xe f1nbblvp f19d5ny4 fzz4f4n"
data-testid="Tree:root" data-test="Tree:root"
> >
<TreeNodeComponent <TreeNodeComponent
key="child1Label" key="child1Label"
@@ -1763,7 +1763,7 @@ exports[`TreeNodeComponent renders selected parent node as unselected if any des
exports[`TreeNodeComponent renders single selected leaf node as selected 1`] = ` exports[`TreeNodeComponent renders single selected leaf node as selected 1`] = `
<TreeItem <TreeItem
className="" className=""
data-testid="TreeNodeContainer:root" data-test="TreeNodeContainer:root"
itemType="leaf" itemType="leaf"
onOpenChange={[Function]} onOpenChange={[Function]}
value="root" value="root"
@@ -1771,7 +1771,7 @@ exports[`TreeNodeComponent renders single selected leaf node as selected 1`] = `
<TreeItemLayout <TreeItemLayout
actions={false} actions={false}
className="___rq9vxg0_1ykn2d2 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1nfm20t f1do9gdl" className="___rq9vxg0_1ykn2d2 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1nfm20t f1do9gdl"
data-testid="TreeNode:root" data-test="TreeNode:root"
iconBefore={ iconBefore={
<img <img
alt="" alt=""

View File

@@ -76,7 +76,7 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
name: label, name: label,
disabled: btn.disabled, disabled: btn.disabled,
ariaLabel: btn.ariaLabel, ariaLabel: btn.ariaLabel,
"data-testid": `CommandBar/Button:${label}`, "data-test": `CommandBar/Button:${label}`,
buttonStyles: { buttonStyles: {
root: { root: {
backgroundColor: backgroundColor, backgroundColor: backgroundColor,

View File

@@ -127,13 +127,13 @@ 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>
</span> </span>
</div> </div>
<div className="expandCollapseButton" data-testid="NotificationConsole/ExpandCollapseButton"> <div className="expandCollapseButton" data-test="NotificationConsole/ExpandCollapseButton">
<img <img
src={this.props.isConsoleExpanded ? ChevronDownIcon : ChevronUpIcon} src={this.props.isConsoleExpanded ? ChevronDownIcon : ChevronUpIcon}
alt={this.props.isConsoleExpanded ? "Collapse icon" : "Expand icon"} alt={this.props.isConsoleExpanded ? "Collapse icon" : "Expand icon"}
@@ -145,7 +145,7 @@ export class NotificationConsoleComponent extends React.Component<
height={this.props.isConsoleExpanded ? "auto" : 0} height={this.props.isConsoleExpanded ? "auto" : 0}
onAnimationEnd={this.onConsoleWasExpanded} onAnimationEnd={this.onConsoleWasExpanded}
> >
<div data-testid="NotificationConsole/Contents" className="notificationConsoleContents"> <div data-test="NotificationConsole/Contents" className="notificationConsoleContents">
<div className="notificationConsoleControls"> <div className="notificationConsoleControls">
<Dropdown <Dropdown
label="Filter:" label="Filter:"

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"
@@ -88,7 +89,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
</div> </div>
<div <div
className="expandCollapseButton" className="expandCollapseButton"
data-testid="NotificationConsole/ExpandCollapseButton" data-test="NotificationConsole/ExpandCollapseButton"
> >
<img <img
alt="Expand icon" alt="Expand icon"
@@ -122,7 +123,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
> >
<div <div
className="notificationConsoleContents" className="notificationConsoleContents"
data-testid="NotificationConsole/Contents" data-test="NotificationConsole/Contents"
> >
<div <div
className="notificationConsoleControls" className="notificationConsoleControls"
@@ -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"
@@ -273,7 +275,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
</div> </div>
<div <div
className="expandCollapseButton" className="expandCollapseButton"
data-testid="NotificationConsole/ExpandCollapseButton" data-test="NotificationConsole/ExpandCollapseButton"
> >
<img <img
alt="Expand icon" alt="Expand icon"
@@ -307,7 +309,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
> >
<div <div
className="notificationConsoleContents" className="notificationConsoleContents"
data-testid="NotificationConsole/Contents" data-test="NotificationConsole/Contents"
> >
<div <div
className="notificationConsoleControls" className="notificationConsoleControls"

View File

@@ -56,16 +56,16 @@ export class StatusBar extends React.Component<Props> {
return ( return (
<BarContainer> <BarContainer>
<Bar data-testid="notebookStatusBar"> <Bar data-test="notebookStatusBar">
<RightStatus> <RightStatus>
{this.props.lastSaved ? ( {this.props.lastSaved ? (
<p data-testid="saveStatus"> Last saved {distanceInWordsToNow(this.props.lastSaved)} </p> <p data-test="saveStatus"> Last saved {distanceInWordsToNow(this.props.lastSaved)} </p>
) : ( ) : (
<p> Not saved yet </p> <p> Not saved yet </p>
)} )}
</RightStatus> </RightStatus>
<LeftStatus> <LeftStatus>
<p data-testid="kernelStatus"> <p data-test="kernelStatus">
{name} | {this.props.kernelStatus} {name} | {this.props.kernelStatus}
</p> </p>
</LeftStatus> </LeftStatus>

View File

@@ -301,7 +301,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
type="radio" type="radio"
role="radio" role="radio"
id="databaseCreateNew" id="databaseCreateNew"
data-testid="AddCollectionPanel/DatabaseRadio:CreateNew"
tabIndex={0} tabIndex={0}
onChange={this.onCreateNewDatabaseRadioBtnChange.bind(this)} onChange={this.onCreateNewDatabaseRadioBtnChange.bind(this)}
/> />
@@ -315,7 +314,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
name="databaseType" name="databaseType"
type="radio" type="radio"
role="radio" role="radio"
data-testid="AddCollectionPanel/DatabaseRadio:UseExisting"
tabIndex={0} tabIndex={0}
onChange={this.onUseExistingDatabaseRadioBtnChange.bind(this)} onChange={this.onUseExistingDatabaseRadioBtnChange.bind(this)}
/> />
@@ -339,7 +337,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
size={40} size={40}
className="panelTextField" className="panelTextField"
aria-label="New database id, Type a new database id" aria-label="New database id, Type a new database id"
data-testid="AddCollectionPanel/DatabaseId"
tabIndex={0} tabIndex={0}
value={this.state.newDatabaseId} value={this.state.newDatabaseId}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
@@ -349,20 +346,18 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{!isServerlessAccount() && ( {!isServerlessAccount() && (
<Stack horizontal> <Stack horizontal>
<div data-testid="AddCollectionPanel/SharedThroughputCheckbox"> <Checkbox
<Checkbox label={`Share throughput across ${getCollectionName(true).toLocaleLowerCase()}`}
label={`Share throughput across ${getCollectionName(true).toLocaleLowerCase()}`} checked={this.state.isSharedThroughputChecked}
checked={this.state.isSharedThroughputChecked} styles={{
styles={{ text: { fontSize: 12 },
text: { fontSize: 12 }, checkbox: { width: 12, height: 12 },
checkbox: { width: 12, height: 12 }, label: { padding: 0, alignItems: "center" },
label: { padding: 0, alignItems: "center" }, }}
}} onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) =>
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) => this.setState({ isSharedThroughputChecked: isChecked })
this.setState({ isSharedThroughputChecked: isChecked }) }
} />
/>
</div>
<TooltipHost <TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge} directionalHint={DirectionalHint.bottomLeftEdge}
content={`Throughput configured at the database level will be shared across all ${getCollectionName( content={`Throughput configured at the database level will be shared across all ${getCollectionName(
@@ -401,7 +396,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{!this.state.createNewDatabase && ( {!this.state.createNewDatabase && (
<Dropdown <Dropdown
ariaLabel="Choose an existing database" ariaLabel="Choose an existing database"
data-testid="AddCollectionPanel/ExistingDatabaseDropdown"
styles={{ title: { height: 27, lineHeight: 27 }, dropdownItem: { fontSize: 12 } }} styles={{ title: { height: 27, lineHeight: 27 }, dropdownItem: { fontSize: 12 } }}
style={{ width: 300, fontSize: 12 }} style={{ width: 300, fontSize: 12 }}
placeholder="Choose an existing database" placeholder="Choose an existing database"
@@ -449,7 +443,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
placeholder={`e.g., ${getCollectionName()}1`} placeholder={`e.g., ${getCollectionName()}1`}
size={40} size={40}
className="panelTextField" className="panelTextField"
data-testid="AddCollectionPanel/CollectionId"
aria-label={`${getCollectionName()} id, Example ${getCollectionName()}1`} aria-label={`${getCollectionName()} id, Example ${getCollectionName()}1`}
value={this.state.collectionId} value={this.state.collectionId}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
@@ -583,7 +576,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<input <input
type="text" type="text"
id="addCollection-partitionKeyValue" id="addCollection-partitionKeyValue"
data-testid="AddCollectionPanel/PartitionKey"
aria-required aria-required
required required
size={40} size={40}
@@ -620,7 +612,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<input <input
type="text" type="text"
id="addCollection-partitionKeyValue" id="addCollection-partitionKeyValue"
data-testid="AddCollectionPanel/PartitionKey"
key={`addCollection-partitionKeyValue_${index}`} key={`addCollection-partitionKeyValue_${index}`}
aria-required aria-required
required required
@@ -738,7 +729,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
)} )}
{!isFabricNative() && userContext.apiType === "SQL" && ( {!isFabricNative() && userContext.apiType === "SQL" && (
<Stack style={{ marginTop: -2, marginBottom: -4 }} data-testid="AddCollectionPanel/UniqueKeysSection"> <Stack style={{ marginTop: -2, marginBottom: -4 }}>
{UniqueKeysHeader()} {UniqueKeysHeader()}
{this.state.uniqueKeys.map((uniqueKey: string, i: number): JSX.Element => { {this.state.uniqueKeys.map((uniqueKey: string, i: number): JSX.Element => {
return ( return (
@@ -752,7 +743,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
: "Comma separated paths e.g. /firstName,/address/zipCode" : "Comma separated paths e.g. /firstName,/address/zipCode"
} }
className="panelTextField" className="panelTextField"
data-testid="AddCollectionPanel/UniqueKey"
value={uniqueKey} value={uniqueKey}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => { onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
const uniqueKeys = this.state.uniqueKeys.map((uniqueKey: string, j: number) => { const uniqueKeys = this.state.uniqueKeys.map((uniqueKey: string, j: number) => {
@@ -779,7 +769,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<ActionButton <ActionButton
iconProps={{ iconName: "Add" }} iconProps={{ iconName: "Add" }}
data-testid="AddCollectionPanel/AddUniqueKeyButton"
styles={{ root: { padding: 0 }, label: { fontSize: 12 } }} styles={{ root: { padding: 0 }, label: { fontSize: 12 } }}
onClick={() => this.setState({ uniqueKeys: [...this.state.uniqueKeys, ""] })} onClick={() => this.setState({ uniqueKeys: [...this.state.uniqueKeys, ""] })}
> >

View File

@@ -56,7 +56,6 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
aria-label="Create new database" aria-label="Create new database"
checked={true} checked={true}
className="panelRadioBtn" className="panelRadioBtn"
data-testid="AddCollectionPanel/DatabaseRadio:CreateNew"
id="databaseCreateNew" id="databaseCreateNew"
name="databaseType" name="databaseType"
onChange={[Function]} onChange={[Function]}
@@ -74,7 +73,6 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
aria-label="Use existing database" aria-label="Use existing database"
checked={false} checked={false}
className="panelRadioBtn" className="panelRadioBtn"
data-testid="AddCollectionPanel/DatabaseRadio:UseExisting"
name="databaseType" name="databaseType"
onChange={[Function]} onChange={[Function]}
role="radio" role="radio"
@@ -96,7 +94,6 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
aria-required={true} aria-required={true}
autoComplete="off" autoComplete="off"
className="panelTextField" className="panelTextField"
data-testid="AddCollectionPanel/DatabaseId"
id="newDatabaseId" id="newDatabaseId"
name="newDatabaseId" name="newDatabaseId"
onChange={[Function]} onChange={[Function]}
@@ -112,30 +109,26 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
<Stack <Stack
horizontal={true} horizontal={true}
> >
<div <StyledCheckboxBase
data-testid="AddCollectionPanel/SharedThroughputCheckbox" checked={false}
> label="Share throughput across containers"
<StyledCheckboxBase onChange={[Function]}
checked={false} styles={
label="Share throughput across containers" {
onChange={[Function]} "checkbox": {
styles={ "height": 12,
{ "width": 12,
"checkbox": { },
"height": 12, "label": {
"width": 12, "alignItems": "center",
}, "padding": 0,
"label": { },
"alignItems": "center", "text": {
"padding": 0, "fontSize": 12,
}, },
"text": {
"fontSize": 12,
},
}
} }
/> }
</div> />
<StyledTooltipHostBase <StyledTooltipHostBase
content="Throughput configured at the database level will be shared across all containers within the database." content="Throughput configured at the database level will be shared across all containers within the database."
directionalHint={4} directionalHint={4}
@@ -198,7 +191,6 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
aria-required={true} aria-required={true}
autoComplete="off" autoComplete="off"
className="panelTextField" className="panelTextField"
data-testid="AddCollectionPanel/CollectionId"
id="collectionId" id="collectionId"
name="collectionId" name="collectionId"
onChange={[Function]} onChange={[Function]}
@@ -260,7 +252,6 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
aria-label="Partition key" aria-label="Partition key"
aria-required={true} aria-required={true}
className="panelTextField" className="panelTextField"
data-testid="AddCollectionPanel/PartitionKey"
id="addCollection-partitionKeyValue" id="addCollection-partitionKeyValue"
onChange={[Function]} onChange={[Function]}
pattern=".*" pattern=".*"
@@ -313,7 +304,6 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
setThroughputValue={[Function]} setThroughputValue={[Function]}
/> />
<Stack <Stack
data-testid="AddCollectionPanel/UniqueKeysSection"
style={ style={
{ {
"marginBottom": -4, "marginBottom": -4,
@@ -348,7 +338,6 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
</StyledTooltipHostBase> </StyledTooltipHostBase>
</Stack> </Stack>
<CustomizedActionButton <CustomizedActionButton
data-testid="AddCollectionPanel/AddUniqueKeyButton"
iconProps={ iconProps={
{ {
"iconName": "Add", "iconName": "Add",

View File

@@ -199,7 +199,6 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
{keyspaceCreateNew && ( {keyspaceCreateNew && (
<Stack className="panelGroupSpacing"> <Stack className="panelGroupSpacing">
<TextField <TextField
data-testid="AddCollectionPanel/DatabaseId"
aria-required="true" aria-required="true"
required={true} required={true}
autoComplete="off" autoComplete="off"
@@ -216,20 +215,16 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
{!isServerlessAccount() && ( {!isServerlessAccount() && (
<Stack horizontal> <Stack horizontal>
<div data-testid="AddCollectionPanel/SharedThroughputCheckbox"> <Checkbox
<Checkbox label="Provision shared throughput"
label="Provision shared throughput" checked={isKeyspaceShared}
checked={isKeyspaceShared} styles={{
styles={{ text: { fontSize: 12 },
text: { fontSize: 12 }, checkbox: { width: 12, height: 12 },
checkbox: { width: 12, height: 12 }, label: { padding: 0, alignItems: "center" },
label: { padding: 0, alignItems: "center" }, }}
}} onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) => setIsKeyspaceShared(isChecked)}
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) => />
setIsKeyspaceShared(isChecked)
}
/>
</div>
<InfoTooltip> <InfoTooltip>
Provisioned throughput at the keyspace level will be shared across unlimited number of tables within Provisioned throughput at the keyspace level will be shared across unlimited number of tables within
the keyspace the keyspace
@@ -292,7 +287,6 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
{`CREATE TABLE ${keyspaceCreateNew ? newKeyspaceId : existingKeyspaceId}.`} {`CREATE TABLE ${keyspaceCreateNew ? newKeyspaceId : existingKeyspaceId}.`}
</Text> </Text>
<TextField <TextField
data-testid="AddCollectionPanel/CollectionId"
underlined underlined
styles={getTextFieldStyles({ fontSize: 12, width: 150 })} styles={getTextFieldStyles({ fontSize: 12, width: 150 })}
aria-required="true" aria-required="true"

View File

@@ -120,7 +120,6 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
<Text variant="small">Confirm by typing the {collectionName.toLowerCase()} id</Text> <Text variant="small">Confirm by typing the {collectionName.toLowerCase()} id</Text>
<TextField <TextField
id="confirmCollectionId" id="confirmCollectionId"
data-testid="DeleteCollectionConfirmationPane/ConfirmInput"
autoFocus autoFocus
value={inputCollectionName} value={inputCollectionName}
styles={{ fieldGroup: { width: 300 } }} styles={{ fieldGroup: { width: 300 } }}

View File

@@ -42,7 +42,6 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
<StyledTextFieldBase <StyledTextFieldBase
ariaLabel="Confirm by typing the container id" ariaLabel="Confirm by typing the container id"
autoFocus={true} autoFocus={true}
data-testid="DeleteCollectionConfirmationPane/ConfirmInput"
id="confirmCollectionId" id="confirmCollectionId"
onChange={[Function]} onChange={[Function]}
required={true} required={true}
@@ -58,7 +57,6 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
<TextFieldBase <TextFieldBase
ariaLabel="Confirm by typing the container id" ariaLabel="Confirm by typing the container id"
autoFocus={true} autoFocus={true}
data-testid="DeleteCollectionConfirmationPane/ConfirmInput"
deferredValidationTime={200} deferredValidationTime={200}
id="confirmCollectionId" id="confirmCollectionId"
onChange={[Function]} onChange={[Function]}
@@ -355,7 +353,6 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
aria-label="Confirm by typing the container id" aria-label="Confirm by typing the container id"
autoFocus={true} autoFocus={true}
className="ms-TextField-field field-113" className="ms-TextField-field field-113"
data-testid="DeleteCollectionConfirmationPane/ConfirmInput"
id="confirmCollectionId" id="confirmCollectionId"
onBlur={[Function]} onBlur={[Function]}
onChange={[Function]} onChange={[Function]}
@@ -382,7 +379,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
> >
<CustomizedPrimaryButton <CustomizedPrimaryButton
ariaLabel="OK" ariaLabel="OK"
data-testid="Panel/OkButton" data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
text="OK" text="OK"
@@ -393,7 +390,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
> >
<PrimaryButton <PrimaryButton
ariaLabel="OK" ariaLabel="OK"
data-testid="Panel/OkButton" data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
styles={ styles={
@@ -684,7 +681,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
> >
<CustomizedDefaultButton <CustomizedDefaultButton
ariaLabel="OK" ariaLabel="OK"
data-testid="Panel/OkButton" data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@@ -980,7 +977,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
> >
<DefaultButton <DefaultButton
ariaLabel="OK" ariaLabel="OK"
data-testid="Panel/OkButton" data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@@ -1274,7 +1271,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
<BaseButton <BaseButton
ariaLabel="OK" ariaLabel="OK"
baseClassName="ms-Button" baseClassName="ms-Button"
data-testid="Panel/OkButton" data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@@ -2160,7 +2157,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
aria-label="OK" aria-label="OK"
className="ms-Button ms-Button--primary root-122" className="ms-Button ms-Button--primary root-122"
data-is-focusable={true} data-is-focusable={true}
data-testid="Panel/OkButton" data-test="Panel/OkButton"
id="sidePanelOkButton" id="sidePanelOkButton"
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}

View File

@@ -135,7 +135,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
<Text variant="small">{confirmDatabase}</Text> <Text variant="small">{confirmDatabase}</Text>
<TextField <TextField
id="confirmDatabaseId" id="confirmDatabaseId"
data-testid="DeleteDatabaseConfirmationPanel/ConfirmInput" data-test="Input:confirmDatabaseId"
autoFocus autoFocus
styles={{ fieldGroup: { width: 300 } }} styles={{ fieldGroup: { width: 300 } }}
onChange={(event, newInput?: string) => { onChange={(event, newInput?: string) => {

View File

@@ -5312,7 +5312,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
> >
<CustomizedPrimaryButton <CustomizedPrimaryButton
ariaLabel="Execute" ariaLabel="Execute"
data-testid="Panel/OkButton" data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
text="Execute" text="Execute"
@@ -5323,7 +5323,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
> >
<PrimaryButton <PrimaryButton
ariaLabel="Execute" ariaLabel="Execute"
data-testid="Panel/OkButton" data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
styles={ styles={
@@ -5614,7 +5614,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
> >
<CustomizedDefaultButton <CustomizedDefaultButton
ariaLabel="Execute" ariaLabel="Execute"
data-testid="Panel/OkButton" data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@@ -5910,7 +5910,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
> >
<DefaultButton <DefaultButton
ariaLabel="Execute" ariaLabel="Execute"
data-testid="Panel/OkButton" data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@@ -6204,7 +6204,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
<BaseButton <BaseButton
ariaLabel="Execute" ariaLabel="Execute"
baseClassName="ms-Button" baseClassName="ms-Button"
data-testid="Panel/OkButton" data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@@ -7090,7 +7090,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
aria-label="Execute" aria-label="Execute"
className="ms-Button ms-Button--primary root-148" className="ms-Button ms-Button--primary root-148"
data-is-focusable={true} data-is-focusable={true}
data-testid="Panel/OkButton" data-test="Panel/OkButton"
id="sidePanelOkButton" id="sidePanelOkButton"
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}

View File

@@ -54,7 +54,7 @@ export class PanelContainerComponent extends React.Component<PanelContainerProps
return ( return (
<Panel <Panel
data-testid={`Panel:${this.props.headerText}`} data-test={`Panel:${this.props.headerText}`}
headerText={this.props.headerText} headerText={this.props.headerText}
isOpen={this.props.isOpen} isOpen={this.props.isOpen}
onDismiss={this.onDissmiss} onDismiss={this.onDissmiss}

View File

@@ -16,7 +16,7 @@ export const PanelFooterComponent: React.FunctionComponent<PanelFooterProps> = (
<PrimaryButton <PrimaryButton
type="submit" type="submit"
id="sidePanelOkButton" id="sidePanelOkButton"
data-testid="Panel/OkButton" data-test="Panel/OkButton"
text={buttonLabel} text={buttonLabel}
ariaLabel={buttonLabel} ariaLabel={buttonLabel}
disabled={!!isButtonDisabled} disabled={!!isButtonDisabled}

View File

@@ -21,7 +21,7 @@ exports[`Right Pane Form should render Default properly 1`] = `
> >
<CustomizedPrimaryButton <CustomizedPrimaryButton
ariaLabel="Load" ariaLabel="Load"
data-testid="Panel/OkButton" data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
text="Load" text="Load"
@@ -32,7 +32,7 @@ exports[`Right Pane Form should render Default properly 1`] = `
> >
<PrimaryButton <PrimaryButton
ariaLabel="Load" ariaLabel="Load"
data-testid="Panel/OkButton" data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
styles={ styles={
@@ -323,7 +323,7 @@ exports[`Right Pane Form should render Default properly 1`] = `
> >
<CustomizedDefaultButton <CustomizedDefaultButton
ariaLabel="Load" ariaLabel="Load"
data-testid="Panel/OkButton" data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@@ -619,7 +619,7 @@ exports[`Right Pane Form should render Default properly 1`] = `
> >
<DefaultButton <DefaultButton
ariaLabel="Load" ariaLabel="Load"
data-testid="Panel/OkButton" data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@@ -913,7 +913,7 @@ exports[`Right Pane Form should render Default properly 1`] = `
<BaseButton <BaseButton
ariaLabel="Load" ariaLabel="Load"
baseClassName="ms-Button" baseClassName="ms-Button"
data-testid="Panel/OkButton" data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@@ -1799,7 +1799,7 @@ exports[`Right Pane Form should render Default properly 1`] = `
aria-label="Load" aria-label="Load"
className="ms-Button ms-Button--primary root-109" className="ms-Button ms-Button--primary root-109"
data-is-focusable={true} data-is-focusable={true}
data-testid="Panel/OkButton" data-test="Panel/OkButton"
id="sidePanelOkButton" id="sidePanelOkButton"
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}

View File

@@ -688,7 +688,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
> >
<CustomizedPrimaryButton <CustomizedPrimaryButton
ariaLabel="Create" ariaLabel="Create"
data-testid="Panel/OkButton" data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
text="Create" text="Create"
@@ -699,7 +699,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
> >
<PrimaryButton <PrimaryButton
ariaLabel="Create" ariaLabel="Create"
data-testid="Panel/OkButton" data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
styles={ styles={
@@ -990,7 +990,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
> >
<CustomizedDefaultButton <CustomizedDefaultButton
ariaLabel="Create" ariaLabel="Create"
data-testid="Panel/OkButton" data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@@ -1286,7 +1286,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
> >
<DefaultButton <DefaultButton
ariaLabel="Create" ariaLabel="Create"
data-testid="Panel/OkButton" data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@@ -1580,7 +1580,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
<BaseButton <BaseButton
ariaLabel="Create" ariaLabel="Create"
baseClassName="ms-Button" baseClassName="ms-Button"
data-testid="Panel/OkButton" data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@@ -2466,7 +2466,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
aria-label="Create" aria-label="Create"
className="ms-Button ms-Button--primary root-128" className="ms-Button ms-Button--primary root-128"
data-is-focusable={true} data-is-focusable={true}
data-testid="Panel/OkButton" data-test="Panel/OkButton"
id="sidePanelOkButton" id="sidePanelOkButton"
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}

View File

@@ -1258,7 +1258,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
> >
<CustomizedPrimaryButton <CustomizedPrimaryButton
ariaLabel="OK" ariaLabel="OK"
data-testid="Panel/OkButton" data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
text="OK" text="OK"
@@ -1269,7 +1269,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
> >
<PrimaryButton <PrimaryButton
ariaLabel="OK" ariaLabel="OK"
data-testid="Panel/OkButton" data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
styles={ styles={
@@ -1560,7 +1560,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
> >
<CustomizedDefaultButton <CustomizedDefaultButton
ariaLabel="OK" ariaLabel="OK"
data-testid="Panel/OkButton" data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@@ -1856,7 +1856,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
> >
<DefaultButton <DefaultButton
ariaLabel="OK" ariaLabel="OK"
data-testid="Panel/OkButton" data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@@ -2150,7 +2150,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
<BaseButton <BaseButton
ariaLabel="OK" ariaLabel="OK"
baseClassName="ms-Button" baseClassName="ms-Button"
data-testid="Panel/OkButton" data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@@ -3036,7 +3036,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
aria-label="OK" aria-label="OK"
className="ms-Button ms-Button--primary root-125" className="ms-Button ms-Button--primary root-125"
data-is-focusable={true} data-is-focusable={true}
data-testid="Panel/OkButton" data-test="Panel/OkButton"
id="sidePanelOkButton" id="sidePanelOkButton"
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}

View File

@@ -369,7 +369,7 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
> >
<CustomizedPrimaryButton <CustomizedPrimaryButton
ariaLabel="Add Entity" ariaLabel="Add Entity"
data-testid="Panel/OkButton" data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
text="Add Entity" text="Add Entity"
@@ -380,7 +380,7 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
> >
<PrimaryButton <PrimaryButton
ariaLabel="Add Entity" ariaLabel="Add Entity"
data-testid="Panel/OkButton" data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
styles={ styles={
@@ -671,7 +671,7 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
> >
<CustomizedDefaultButton <CustomizedDefaultButton
ariaLabel="Add Entity" ariaLabel="Add Entity"
data-testid="Panel/OkButton" data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@@ -967,7 +967,7 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
> >
<DefaultButton <DefaultButton
ariaLabel="Add Entity" ariaLabel="Add Entity"
data-testid="Panel/OkButton" data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@@ -1261,7 +1261,7 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
<BaseButton <BaseButton
ariaLabel="Add Entity" ariaLabel="Add Entity"
baseClassName="ms-Button" baseClassName="ms-Button"
data-testid="Panel/OkButton" data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@@ -2147,7 +2147,7 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
aria-label="Add Entity" aria-label="Add Entity"
className="ms-Button ms-Button--primary root-113" className="ms-Button ms-Button--primary root-113"
data-is-focusable={true} data-is-focusable={true}
data-testid="Panel/OkButton" data-test="Panel/OkButton"
id="sidePanelOkButton" id="sidePanelOkButton"
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}

View File

@@ -375,7 +375,7 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = `
> >
<CustomizedPrimaryButton <CustomizedPrimaryButton
ariaLabel="Update" ariaLabel="Update"
data-testid="Panel/OkButton" data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
text="Update" text="Update"
@@ -386,7 +386,7 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = `
> >
<PrimaryButton <PrimaryButton
ariaLabel="Update" ariaLabel="Update"
data-testid="Panel/OkButton" data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
styles={ styles={
@@ -677,7 +677,7 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = `
> >
<CustomizedDefaultButton <CustomizedDefaultButton
ariaLabel="Update" ariaLabel="Update"
data-testid="Panel/OkButton" data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@@ -973,7 +973,7 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = `
> >
<DefaultButton <DefaultButton
ariaLabel="Update" ariaLabel="Update"
data-testid="Panel/OkButton" data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@@ -1267,7 +1267,7 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = `
<BaseButton <BaseButton
ariaLabel="Update" ariaLabel="Update"
baseClassName="ms-Button" baseClassName="ms-Button"
data-testid="Panel/OkButton" data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@@ -2153,7 +2153,7 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = `
aria-label="Update" aria-label="Update"
className="ms-Button ms-Button--primary root-113" className="ms-Button ms-Button--primary root-113"
data-is-focusable={true} data-is-focusable={true}
data-testid="Panel/OkButton" data-test="Panel/OkButton"
id="sidePanelOkButton" id="sidePanelOkButton"
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}

View File

@@ -367,7 +367,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
<StyledTextFieldBase <StyledTextFieldBase
ariaLabel="Confirm by typing the Database id (name)" ariaLabel="Confirm by typing the Database id (name)"
autoFocus={true} autoFocus={true}
data-testid="DeleteDatabaseConfirmationPanel/ConfirmInput" data-test="Input:confirmDatabaseId"
id="confirmDatabaseId" id="confirmDatabaseId"
onChange={[Function]} onChange={[Function]}
required={true} required={true}
@@ -382,7 +382,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
<TextFieldBase <TextFieldBase
ariaLabel="Confirm by typing the Database id (name)" ariaLabel="Confirm by typing the Database id (name)"
autoFocus={true} autoFocus={true}
data-testid="DeleteDatabaseConfirmationPanel/ConfirmInput" data-test="Input:confirmDatabaseId"
deferredValidationTime={200} deferredValidationTime={200}
id="confirmDatabaseId" id="confirmDatabaseId"
onChange={[Function]} onChange={[Function]}
@@ -678,7 +678,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
aria-label="Confirm by typing the Database id (name)" aria-label="Confirm by typing the Database id (name)"
autoFocus={true} autoFocus={true}
className="ms-TextField-field field-117" className="ms-TextField-field field-117"
data-testid="DeleteDatabaseConfirmationPanel/ConfirmInput" data-test="Input:confirmDatabaseId"
id="confirmDatabaseId" id="confirmDatabaseId"
onBlur={[Function]} onBlur={[Function]}
onChange={[Function]} onChange={[Function]}
@@ -1054,7 +1054,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
> >
<CustomizedPrimaryButton <CustomizedPrimaryButton
ariaLabel="OK" ariaLabel="OK"
data-testid="Panel/OkButton" data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
text="OK" text="OK"
@@ -1065,7 +1065,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
> >
<PrimaryButton <PrimaryButton
ariaLabel="OK" ariaLabel="OK"
data-testid="Panel/OkButton" data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
styles={ styles={
@@ -1356,7 +1356,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
> >
<CustomizedDefaultButton <CustomizedDefaultButton
ariaLabel="OK" ariaLabel="OK"
data-testid="Panel/OkButton" data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@@ -1652,7 +1652,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
> >
<DefaultButton <DefaultButton
ariaLabel="OK" ariaLabel="OK"
data-testid="Panel/OkButton" data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@@ -1946,7 +1946,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
<BaseButton <BaseButton
ariaLabel="OK" ariaLabel="OK"
baseClassName="ms-Button" baseClassName="ms-Button"
data-testid="Panel/OkButton" data-test="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@@ -2832,7 +2832,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
aria-label="OK" aria-label="OK"
className="ms-Button ms-Button--primary root-130" className="ms-Button ms-Button--primary root-130"
data-is-focusable={true} data-is-focusable={true}
data-testid="Panel/OkButton" data-test="Panel/OkButton"
id="sidePanelOkButton" id="sidePanelOkButton"
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}

View File

@@ -4,7 +4,7 @@ exports[`PaneContainerComponent test should be resize if notification console is
<StyledPanelBase <StyledPanelBase
closeButtonAriaLabel="Close test" closeButtonAriaLabel="Close test"
customWidth="440px" customWidth="440px"
data-testid="Panel:test" data-test="Panel:test"
headerClassName="panelHeader" headerClassName="panelHeader"
headerText="test" headerText="test"
isFooterAtBottom={true} isFooterAtBottom={true}
@@ -43,7 +43,7 @@ exports[`PaneContainerComponent test should not render console with panel 1`] =
<StyledPanelBase <StyledPanelBase
closeButtonAriaLabel="Close test" closeButtonAriaLabel="Close test"
customWidth="440px" customWidth="440px"
data-testid="Panel:test" data-test="Panel:test"
headerClassName="panelHeader" headerClassName="panelHeader"
headerText="test" headerText="test"
isFooterAtBottom={true} isFooterAtBottom={true}
@@ -84,7 +84,7 @@ exports[`PaneContainerComponent test should render with panel content and header
<StyledPanelBase <StyledPanelBase
closeButtonAriaLabel="Close test" closeButtonAriaLabel="Close test"
customWidth="440px" customWidth="440px"
data-testid="Panel:test" data-test="Panel:test"
headerClassName="panelHeader" headerClassName="panelHeader"
headerText="test" headerText="test"
isFooterAtBottom={true} isFooterAtBottom={true}

View File

@@ -242,14 +242,9 @@ const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
} }
return ( return (
<div className={styles.globalCommandsContainer} data-testid="GlobalCommands"> <div className={styles.globalCommandsContainer} data-test="GlobalCommands">
{actions.length === 1 ? ( {actions.length === 1 ? (
<Button <Button icon={primaryAction.icon} onClick={onPrimaryActionClick} ref={primaryFocusableRef}>
data-testid={`GlobalCommands/Button:${primaryAction.label}`}
icon={primaryAction.icon}
onClick={onPrimaryActionClick}
ref={primaryFocusableRef}
>
{primaryAction.label} {primaryAction.label}
</Button> </Button>
) : ( ) : (
@@ -258,12 +253,8 @@ const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
{(triggerProps: MenuButtonProps) => ( {(triggerProps: MenuButtonProps) => (
<div ref={setGlobalCommandButton}> <div ref={setGlobalCommandButton}>
<SplitButton <SplitButton
data-testid={`GlobalCommands/Button:${primaryAction.label}`}
menuButton={{ ...triggerProps, "aria-label": "More commands" }} menuButton={{ ...triggerProps, "aria-label": "More commands" }}
primaryActionButton={{ primaryActionButton={{ onClick: onPrimaryActionClick, ref: primaryFocusableRef }}
onClick: onPrimaryActionClick,
ref: primaryFocusableRef,
}}
className={styles.globalCommandsSplitButton} className={styles.globalCommandsSplitButton}
icon={primaryAction.icon} icon={primaryAction.icon}
> >
@@ -385,7 +376,7 @@ export const SidebarContainer: React.FC<SidebarProps> = ({ explorer }) => {
{!isFabricNative() && ( {!isFabricNative() && (
<button <button
type="button" type="button"
data-testid="Sidebar/RefreshButton" data-test="Sidebar/RefreshButton"
className={styles.floatingControlButton} className={styles.floatingControlButton}
disabled={loading} disabled={loading}
title="Refresh" title="Refresh"

View File

@@ -2146,8 +2146,8 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
return ( return (
<CosmosFluentProvider className={styles.container}> <CosmosFluentProvider className={styles.container}>
<div data-testid={"DocumentsTab"} className="tab-pane active" role="tabpanel" style={{ display: "flex" }}> <div data-test={"DocumentsTab"} className="tab-pane active" role="tabpanel" style={{ display: "flex" }}>
<div data-testid={"DocumentsTab/Filter"} className={`${styles.filterRow} ${styles.smallScreenContent}`}> <div data-test={"DocumentsTab/Filter"} className={`${styles.filterRow} ${styles.smallScreenContent}`}>
{!isPreferredApiMongoDB && <span> SELECT * FROM c </span>} {!isPreferredApiMongoDB && <span> SELECT * FROM c </span>}
<InputDataList <InputDataList
dropdownOptions={getFilterChoices()} dropdownOptions={getFilterChoices()}
@@ -2164,7 +2164,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
/> />
<Button <Button
appearance="primary" appearance="primary"
data-testid={"DocumentsTab/ApplyFilter"} data-test={"DocumentsTab/ApplyFilter"}
size="small" size="small"
onClick={() => { onClick={() => {
if (isExecuting) { if (isExecuting) {
@@ -2191,7 +2191,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
> >
<Allotment.Pane preferredSize={`${tabStateData.leftPaneWidthPercent}%`} minSize={55}> <Allotment.Pane preferredSize={`${tabStateData.leftPaneWidthPercent}%`} minSize={55}>
<div <div
data-testid={"DocumentsTab/DocumentsPane"} data-test={"DocumentsTab/DocumentsPane"}
style={{ height: "100%", width: "100%", overflow: "hidden" }} style={{ height: "100%", width: "100%", overflow: "hidden" }}
ref={tableContainerRef} ref={tableContainerRef}
> >
@@ -2237,7 +2237,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
{tableItems.length > 0 && ( {tableItems.length > 0 && (
<a <a
className={styles.loadMore} className={styles.loadMore}
data-testid={"DocumentsTab/LoadMore"} data-test={"DocumentsTab/LoadMore"}
role="button" role="button"
tabIndex={0} tabIndex={0}
onClick={() => loadNextPage(documentsIterator.iterator, false)} onClick={() => loadNextPage(documentsIterator.iterator, false)}
@@ -2249,7 +2249,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
</div> </div>
</Allotment.Pane> </Allotment.Pane>
<Allotment.Pane minSize={30}> <Allotment.Pane minSize={30}>
<div data-testid={"DocumentsTab/ResultsPane"} style={{ height: "100%", width: "100%" }}> <div data-test={"DocumentsTab/ResultsPane"} style={{ height: "100%", width: "100%" }}>
{isTabActive && selectedDocumentContent && selectedRows.size <= 1 && ( {isTabActive && selectedDocumentContent && selectedRows.size <= 1 && (
<EditorReact <EditorReact
language={"json"} language={"json"}

View File

@@ -6,7 +6,7 @@ exports[`Documents tab (noSql API) when rendered should render the page 1`] = `
> >
<div <div
className="tab-pane active" className="tab-pane active"
data-testid="DocumentsTab" data-test="DocumentsTab"
role="tabpanel" role="tabpanel"
style={ style={
{ {
@@ -16,7 +16,7 @@ exports[`Documents tab (noSql API) when rendered should render the page 1`] = `
> >
<div <div
className="___11ktxfv_0000000 f1o614cb fy9rknc f22iagw fsnqrgy f1f5gg8d fjodcmx f122n59 f1f09k3d fg706s2 frpde29 ___1ngl8o6_0000000 fz7mnu6 fl3egqs flhmrkm" className="___11ktxfv_0000000 f1o614cb fy9rknc f22iagw fsnqrgy f1f5gg8d fjodcmx f122n59 f1f09k3d fg706s2 frpde29 ___1ngl8o6_0000000 fz7mnu6 fl3egqs flhmrkm"
data-testid="DocumentsTab/Filter" data-test="DocumentsTab/Filter"
> >
<span> <span>
SELECT * FROM c SELECT * FROM c
@@ -51,7 +51,7 @@ exports[`Documents tab (noSql API) when rendered should render the page 1`] = `
<Button <Button
appearance="primary" appearance="primary"
aria-label="Apply filter" aria-label="Apply filter"
data-testid="DocumentsTab/ApplyFilter" data-test="DocumentsTab/ApplyFilter"
disabled={false} disabled={false}
onClick={[Function]} onClick={[Function]}
size="small" size="small"
@@ -68,7 +68,7 @@ exports[`Documents tab (noSql API) when rendered should render the page 1`] = `
preferredSize="35%" preferredSize="35%"
> >
<div <div
data-testid="DocumentsTab/DocumentsPane" data-test="DocumentsTab/DocumentsPane"
style={ style={
{ {
"height": "100%", "height": "100%",
@@ -130,7 +130,7 @@ exports[`Documents tab (noSql API) when rendered should render the page 1`] = `
minSize={30} minSize={30}
> >
<div <div
data-testid="DocumentsTab/ResultsPane" data-test="DocumentsTab/ResultsPane"
style={ style={
{ {
"height": "100%", "height": "100%",

View File

@@ -116,7 +116,7 @@ export const ErrorList: React.FC<{ errors: QueryError[] }> = ({ errors }) => {
return ( return (
<DataGrid <DataGrid
data-testid="QueryTab/ResultsPane/ErrorList" data-test="QueryTab/ResultsPane/ErrorList"
items={errors} items={errors}
columns={columns} columns={columns}
sortable sortable
@@ -131,9 +131,9 @@ export const ErrorList: React.FC<{ errors: QueryError[] }> = ({ errors }) => {
</DataGridHeader> </DataGridHeader>
<DataGridBody<QueryError>> <DataGridBody<QueryError>>
{({ item, rowId }) => ( {({ item, rowId }) => (
<DataGridRow<QueryError> key={rowId} data-testid={`Row:${rowId}`}> <DataGridRow<QueryError> key={rowId} data-test={`Row:${rowId}`}>
{({ columnId, renderCell }) => ( {({ columnId, renderCell }) => (
<DataGridCell data-testid={`Row:${rowId}/Column:${columnId}`}>{renderCell(item)}</DataGridCell> <DataGridCell data-test={`Row:${rowId}/Column:${columnId}`}>{renderCell(item)}</DataGridCell>
)} )}
</DataGridRow> </DataGridRow>
)} )}

View File

@@ -3,13 +3,13 @@ import QueryError from "Common/QueryError";
import { IndeterminateProgressBar } from "Explorer/Controls/IndeterminateProgressBar"; import { IndeterminateProgressBar } from "Explorer/Controls/IndeterminateProgressBar";
import { MessageBanner } from "Explorer/Controls/MessageBanner"; import { MessageBanner } from "Explorer/Controls/MessageBanner";
import { useQueryTabStyles } from "Explorer/Tabs/QueryTab/Styles"; import { useQueryTabStyles } from "Explorer/Tabs/QueryTab/Styles";
import useZoomLevel from "hooks/useZoomLevel";
import React from "react"; import React from "react";
import { conditionalClass } from "Utils/StyleUtils";
import RunQuery from "../../../../images/RunQuery.png"; import RunQuery from "../../../../images/RunQuery.png";
import { QueryResults } from "../../../Contracts/ViewModels"; import { QueryResults } from "../../../Contracts/ViewModels";
import { ErrorList } from "./ErrorList"; import { ErrorList } from "./ErrorList";
import { ResultsView } from "./ResultsView"; import { ResultsView } from "./ResultsView";
import useZoomLevel from "hooks/useZoomLevel";
import { conditionalClass } from "Utils/StyleUtils";
export interface ResultsViewProps { export interface ResultsViewProps {
isMongoDB: boolean; isMongoDB: boolean;
@@ -27,7 +27,7 @@ const ExecuteQueryCallToAction: React.FC = () => {
const styles = useQueryTabStyles(); const styles = useQueryTabStyles();
const isZoomed = useZoomLevel(); const isZoomed = useZoomLevel();
return ( return (
<div data-testid="QueryTab/ResultsPane/ExecuteCTA" className={styles.executeCallToAction}> <div data-test="QueryTab/ResultsPane/ExecuteCTA" className={styles.executeCallToAction}>
<div> <div>
<p> <p>
<img <img
@@ -54,7 +54,7 @@ export const QueryResultSection: React.FC<QueryResultProps> = ({
const maybeSubQuery = queryEditorContent && /.*\(.*SELECT.*\)/i.test(queryEditorContent); const maybeSubQuery = queryEditorContent && /.*\(.*SELECT.*\)/i.test(queryEditorContent);
return ( return (
<div data-testid="QueryTab/ResultsPane" className={styles.queryResultsPanel}> <div data-test="QueryTab/ResultsPane" className={styles.queryResultsPanel}>
{isExecuting && <IndeterminateProgressBar />} {isExecuting && <IndeterminateProgressBar />}
<MessageBanner <MessageBanner
messageId="QueryEditor.EmptyMongoQuery" messageId="QueryEditor.EmptyMongoQuery"

View File

@@ -64,7 +64,7 @@ describe("QueryTabComponent", () => {
const { container } = render(<QueryTabComponent {...propsMock} />); const { container } = render(<QueryTabComponent {...propsMock} />);
const launchCopilotButton = container.querySelector('[data-testid="QueryTab/ResultsPane/ExecuteCTA"]'); const launchCopilotButton = container.querySelector('[data-test="QueryTab/ResultsPane/ExecuteCTA"]');
fireEvent.keyDown(launchCopilotButton, { key: "c", altKey: true }); fireEvent.keyDown(launchCopilotButton, { key: "c", altKey: true });
expect(mockStore.setShowCopilotSidebar).toHaveBeenCalledWith(true); expect(mockStore.setShowCopilotSidebar).toHaveBeenCalledWith(true);

View File

@@ -746,7 +746,7 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
}} }}
> >
<Allotment.Pane <Allotment.Pane
data-testid="QueryTab/EditorPane" data-test="QueryTab/EditorPane"
preferredSize={ preferredSize={
this.state.queryViewSizePercent !== undefined ? `${this.state.queryViewSizePercent}%` : undefined this.state.queryViewSizePercent !== undefined ? `${this.state.queryViewSizePercent}%` : undefined
} }
@@ -813,7 +813,7 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
render(): JSX.Element { render(): JSX.Element {
const shouldScaleElements = this.state.showCopilotSidebar && this.isCopilotTabActive; const shouldScaleElements = this.state.showCopilotSidebar && this.isCopilotTabActive;
return ( return (
<div data-testid="QueryTab" style={{ display: "flex", flexDirection: "row", height: "100%" }}> <div data-test="QueryTab" style={{ display: "flex", flexDirection: "row", height: "100%" }}>
<div style={{ width: shouldScaleElements ? "70%" : "100%", height: "100%" }}> <div style={{ width: shouldScaleElements ? "70%" : "100%", height: "100%" }}>
{this.getEditorAndQueryResult()} {this.getEditorAndQueryResult()}
</div> </div>

View File

@@ -489,7 +489,7 @@ const QueryStatsTab: React.FC<Pick<ResultsViewProps, "queryResults">> = ({ query
return ( return (
<div className={styles.metricsGridContainer}> <div className={styles.metricsGridContainer}>
<DataGrid <DataGrid
data-testid="QueryTab/ResultsPane/ResultsView/QueryStatsList" data-test="QueryTab/ResultsPane/ResultsView/QueryStatsList"
className={styles.queryStatsGrid} className={styles.queryStatsGrid}
items={generateQueryStatsItems()} items={generateQueryStatsItems()}
columns={columns} columns={columns}
@@ -504,9 +504,9 @@ const QueryStatsTab: React.FC<Pick<ResultsViewProps, "queryResults">> = ({ query
</DataGridHeader> </DataGridHeader>
<DataGridBody<IDocument>> <DataGridBody<IDocument>>
{({ item, rowId }) => ( {({ item, rowId }) => (
<DataGridRow<IDocument> key={rowId} data-testid={`Row:${rowId}`}> <DataGridRow<IDocument> key={rowId} data-test={`Row:${rowId}`}>
{({ columnId, renderCell }) => ( {({ columnId, renderCell }) => (
<DataGridCell data-testid={`Row:${rowId}/Column:${columnId}`}>{renderCell(item)}</DataGridCell> <DataGridCell data-test={`Row:${rowId}/Column:${columnId}`}>{renderCell(item)}</DataGridCell>
)} )}
</DataGridRow> </DataGridRow>
)} )}
@@ -532,17 +532,17 @@ export const ResultsView: React.FC<ResultsViewProps> = ({ isMongoDB, queryResult
}, []); }, []);
return ( return (
<div data-testid="QueryTab/ResultsPane/ResultsView" className={styles.queryResultsTabPanel}> <div data-test="QueryTab/ResultsPane/ResultsView" className={styles.queryResultsTabPanel}>
<TabList selectedValue={activeTab} onTabSelect={onTabSelect}> <TabList selectedValue={activeTab} onTabSelect={onTabSelect}>
<Tab <Tab
data-testid="QueryTab/ResultsPane/ResultsView/ResultsTab" data-test="QueryTab/ResultsPane/ResultsView/ResultsTab"
id={ResultsTabs.Results} id={ResultsTabs.Results}
value={ResultsTabs.Results} value={ResultsTabs.Results}
> >
Results Results
</Tab> </Tab>
<Tab <Tab
data-testid="QueryTab/ResultsPane/ResultsView/QueryStatsTab" data-test="QueryTab/ResultsPane/ResultsView/QueryStatsTab"
id={ResultsTabs.QueryStats} id={ResultsTabs.QueryStats}
value={ResultsTabs.QueryStats} value={ResultsTabs.QueryStats}
> >

View File

@@ -237,14 +237,14 @@ function TabPane({ tab, active }: { tab: Tab; active: boolean }) {
if (tab) { if (tab) {
if ("render" in tab) { if ("render" in tab) {
return ( return (
<div data-testid={`Tab:${tab.tabId}`} {...attrs}> <div data-test={`Tab:${tab.tabId}`} {...attrs}>
{tab.render()} {tab.render()}
</div> </div>
); );
} }
} }
return <div data-testid={`Tab:${tab.tabId}`} {...attrs} ref={ref} data-bind="html:html" />; return <div data-test={`Tab:${tab.tabId}`} {...attrs} ref={ref} data-bind="html:html" />;
} }
const onKeyPressReactTab = (e: KeyboardEvent, tabKind: ReactTabKind): void => { const onKeyPressReactTab = (e: KeyboardEvent, tabKind: ReactTabKind): void => {

View File

@@ -129,7 +129,7 @@ const App: React.FunctionComponent = () => {
// Setting key is needed so React will re-render this element on any account change // Setting key is needed so React will re-render this element on any account change
key={databaseAccount?.id || encryptedTokenMetadata?.accountName || authType} key={databaseAccount?.id || encryptedTokenMetadata?.accountName || authType}
ref={ref} ref={ref}
data-testid="DataExplorerFrame" data-test="DataExplorerFrame"
id="explorerMenu" id="explorerMenu"
name="explorer" name="explorer"
className="iframe" className="iframe"

View File

@@ -103,7 +103,7 @@ const App: React.FunctionComponent = () => {
return ( return (
<KeyboardShortcutRoot> <KeyboardShortcutRoot>
<div className="flexContainer" aria-hidden="false" data-testid="DataExplorerRoot"> <div className="flexContainer" aria-hidden="false" data-test="DataExplorerRoot">
{userContext.features.enableContainerCopy && userContext.apiType === "SQL" ? ( {userContext.features.enableContainerCopy && userContext.apiType === "SQL" ? (
<ContainerCopyPanel explorer={explorer} /> <ContainerCopyPanel explorer={explorer} />
) : ( ) : (

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

@@ -114,7 +114,7 @@ export const ConnectExplorer: React.FunctionComponent<Props> = ({
<div id="connectWithAad"> <div id="connectWithAad">
<input className="filterbtnstyle" type="button" value="Sign In" onClick={login} /> <input className="filterbtnstyle" type="button" value="Sign In" onClick={login} />
{enableConnectionStringLogin && ( {enableConnectionStringLogin && (
<p className="switchConnectTypeText" data-testid="Link:SwitchConnectionType" onClick={showForm}> <p className="switchConnectTypeText" data-test="Link:SwitchConnectionType" onClick={showForm}>
Connect to your account with connection string Connect to your account with connection string
</p> </p>
)} )}

View File

@@ -463,7 +463,7 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
); );
} }
return ( return (
<div style={{ overflowX: "auto" }} data-testid="DataExplorerRoot"> <div style={{ overflowX: "auto" }} data-test="DataExplorerRoot">
<Stack tokens={containerStackTokens}> <Stack tokens={containerStackTokens}>
<Stack.Item> <Stack.Item>
<CommandBar styles={commandBarStyles} items={this.getCommandBarItems()} /> <CommandBar styles={commandBarStyles} items={this.getCommandBarItems()} />

View File

@@ -2,7 +2,7 @@
exports[`SelfServeComponent message bar and spinner snapshots 1`] = ` exports[`SelfServeComponent message bar and spinner snapshots 1`] = `
<div <div
data-testid="DataExplorerRoot" data-test="DataExplorerRoot"
style={ style={
{ {
"overflowX": "auto", "overflowX": "auto",
@@ -339,7 +339,7 @@ exports[`SelfServeComponent message bar and spinner snapshots 1`] = `
exports[`SelfServeComponent message bar and spinner snapshots 2`] = ` exports[`SelfServeComponent message bar and spinner snapshots 2`] = `
<div <div
data-testid="DataExplorerRoot" data-test="DataExplorerRoot"
style={ style={
{ {
"overflowX": "auto", "overflowX": "auto",
@@ -734,7 +734,7 @@ exports[`SelfServeComponent message bar and spinner snapshots 2`] = `
exports[`SelfServeComponent message bar and spinner snapshots 3`] = ` exports[`SelfServeComponent message bar and spinner snapshots 3`] = `
<div <div
data-testid="DataExplorerRoot" data-test="DataExplorerRoot"
style={ style={
{ {
"overflowX": "auto", "overflowX": "auto",
@@ -835,7 +835,7 @@ exports[`SelfServeComponent message bar and spinner snapshots 4`] = `
exports[`SelfServeComponent should render and honor save, discard, refresh actions 1`] = ` exports[`SelfServeComponent should render and honor save, discard, refresh actions 1`] = `
<div <div
data-testid="DataExplorerRoot" data-test="DataExplorerRoot"
style={ style={
{ {
"overflowX": "auto", "overflowX": "auto",

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,114 +1,50 @@
import { expect, test } from "@playwright/test"; import { expect, test } from "@playwright/test";
import { DataExplorer, TEST_AUTOSCALE_THROUGHPUT_RU, TestAccount, generateUniqueName } from "../fx"; import { DataExplorer, TEST_AUTOSCALE_THROUGHPUT_RU, TestAccount, generateUniqueName } from "../fx";
import {
deleteContainer,
deleteKeyspace,
openAndFillCreateCassandraTablePanel,
} from "../helpers/containerCreationHelpers";
test("Cassandra: Keyspace and table CRUD", async ({ page }) => { test("Cassandra keyspace and table CRUD", async ({ page }) => {
const keyspaceId = generateUniqueName("keyspace"); const keyspaceId = generateUniqueName("db");
const tableId = generateUniqueName("table"); const tableId = "testtable"; // A unique table name isn't needed because the keyspace is unique
const explorer = await DataExplorer.open(page, TestAccount.Cassandra); const explorer = await DataExplorer.open(page, TestAccount.Cassandra);
// Create await explorer.globalCommandButton("New Table").click();
await openAndFillCreateCassandraTablePanel(explorer, { await explorer.whilePanelOpen(
keyspaceId, "Add Table",
tableId, async (panel, okButton) => {
isAutoscale: true, await panel.getByPlaceholder("Type a new keyspace id").fill(keyspaceId);
throughputValue: TEST_AUTOSCALE_THROUGHPUT_RU, await panel.getByPlaceholder("Enter table Id").fill(tableId);
}); await panel.getByTestId("autoscaleRUInput").fill(TEST_AUTOSCALE_THROUGHPUT_RU.toString());
await okButton.click();
},
{ closeTimeout: 5 * 60 * 1000 },
);
const keyspaceNode = await explorer.waitForNode(keyspaceId); const keyspaceNode = await explorer.waitForNode(keyspaceId);
const tableNode = await explorer.waitForContainerNode(keyspaceId, tableId); const tableNode = await explorer.waitForContainerNode(keyspaceId, tableId);
await expect(tableNode.element).toBeAttached();
// Delete table await tableNode.openContextMenu();
await deleteContainer(explorer, keyspaceId, tableId, "Delete Table"); await tableNode.contextMenuItem("Delete Table").click();
await explorer.whilePanelOpen(
"Delete Table",
async (panel, okButton) => {
await panel.getByRole("textbox", { name: "Confirm by typing the table id" }).fill(tableId);
await okButton.click();
},
{ closeTimeout: 5 * 60 * 1000 },
);
await expect(tableNode.element).not.toBeAttached(); await expect(tableNode.element).not.toBeAttached();
// Delete keyspace await keyspaceNode.openContextMenu();
await deleteKeyspace(explorer, keyspaceId); await keyspaceNode.contextMenuItem("Delete Keyspace").click();
await expect(keyspaceNode.element).not.toBeAttached(); await explorer.whilePanelOpen(
}); "Delete Keyspace",
async (panel, okButton) => {
test("Cassandra: New keyspace shared throughput", async ({ page }) => { await panel.getByRole("textbox", { name: "Confirm by typing the Keyspace id" }).fill(keyspaceId);
const keyspaceId = generateUniqueName("keyspace"); await okButton.click();
const tableId = generateUniqueName("table"); },
{ closeTimeout: 5 * 60 * 1000 },
const explorer = await DataExplorer.open(page, TestAccount.Cassandra); );
await openAndFillCreateCassandraTablePanel(explorer, {
keyspaceId,
tableId,
useSharedThroughput: true,
});
const keyspaceNode = await explorer.waitForNode(keyspaceId);
const tableNode = await explorer.waitForContainerNode(keyspaceId, tableId);
await expect(tableNode.element).toBeAttached();
// Cleanup
await deleteKeyspace(explorer, keyspaceId);
await expect(keyspaceNode.element).not.toBeAttached();
});
test("Cassandra: Manual throughput", async ({ page }) => {
const keyspaceId = generateUniqueName("keyspace");
const tableId = generateUniqueName("table");
const manualThroughput = 400;
const explorer = await DataExplorer.open(page, TestAccount.Cassandra);
await openAndFillCreateCassandraTablePanel(explorer, {
keyspaceId,
tableId,
isAutoscale: false,
throughputValue: manualThroughput,
});
const keyspaceNode = await explorer.waitForNode(keyspaceId);
const tableNode = await explorer.waitForContainerNode(keyspaceId, tableId);
await expect(tableNode.element).toBeAttached();
// Cleanup
await deleteKeyspace(explorer, keyspaceId);
await expect(keyspaceNode.element).not.toBeAttached();
});
test("Cassandra: Multiple tables in keyspace", async ({ page }) => {
const keyspaceId = generateUniqueName("keyspace");
const table1Id = generateUniqueName("table");
const table2Id = generateUniqueName("table");
const explorer = await DataExplorer.open(page, TestAccount.Cassandra);
// Create first table
await openAndFillCreateCassandraTablePanel(explorer, {
keyspaceId,
tableId: table1Id,
isAutoscale: true,
throughputValue: TEST_AUTOSCALE_THROUGHPUT_RU,
});
const keyspaceNode = await explorer.waitForNode(keyspaceId);
await explorer.waitForContainerNode(keyspaceId, table1Id);
// Create second table in same keyspace
await openAndFillCreateCassandraTablePanel(explorer, {
keyspaceId,
tableId: table2Id,
isAutoscale: true,
throughputValue: TEST_AUTOSCALE_THROUGHPUT_RU,
});
await explorer.waitForContainerNode(keyspaceId, table2Id);
// Cleanup
await deleteKeyspace(explorer, keyspaceId);
await expect(keyspaceNode.element).not.toBeAttached(); await expect(keyspaceNode.element).not.toBeAttached();
}); });

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) {}
@@ -344,12 +353,12 @@ export class DataExplorer {
* There's only a single "primary" button, but we still require you to pass the label to confirm you're selecting the right button. * There's only a single "primary" button, but we still require you to pass the label to confirm you're selecting the right button.
*/ */
globalCommandButton(label: string): Locator { globalCommandButton(label: string): Locator {
return this.frame.getByTestId(`GlobalCommands/Button:${label}`); return this.frame.getByTestId("GlobalCommands").getByText(label);
} }
/** 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,108 +1,22 @@
import { expect, test } from "@playwright/test"; import { expect, test } from "@playwright/test";
import { DataExplorer, TEST_AUTOSCALE_THROUGHPUT_RU, TestAccount, generateUniqueName } from "../fx"; import { DataExplorer, TEST_AUTOSCALE_THROUGHPUT_RU, TestAccount, generateUniqueName } from "../fx";
import {
GREMLIN_CONFIG,
deleteContainer,
deleteDatabase,
openAndFillCreateContainerPanel,
} from "../helpers/containerCreationHelpers";
test("Gremlin: Database and graph CRUD", async ({ page }) => { test("Gremlin graph CRUD", async ({ page }) => {
const databaseId = generateUniqueName("db"); const databaseId = generateUniqueName("db");
const graphId = generateUniqueName("graph"); const graphId = "testgraph"; // A unique graph name isn't needed because the database is unique
const explorer = await DataExplorer.open(page, TestAccount.Gremlin);
// Create
await openAndFillCreateContainerPanel(explorer, GREMLIN_CONFIG, {
databaseId,
containerId: graphId,
partitionKey: "/pk",
isAutoscale: true,
throughputValue: TEST_AUTOSCALE_THROUGHPUT_RU,
});
const databaseNode = await explorer.waitForNode(databaseId);
const graphNode = await explorer.waitForContainerNode(databaseId, graphId);
await expect(graphNode.element).toBeAttached();
// Delete graph
await deleteContainer(explorer, databaseId, graphId, "Delete Graph");
await expect(graphNode.element).not.toBeAttached();
// Delete database
await deleteDatabase(explorer, databaseId);
await expect(databaseNode.element).not.toBeAttached();
});
test("Gremlin: New database shared throughput", async ({ page }) => {
const databaseId = generateUniqueName("db");
const graphId = generateUniqueName("graph");
const explorer = await DataExplorer.open(page, TestAccount.Gremlin);
await openAndFillCreateContainerPanel(explorer, GREMLIN_CONFIG, {
databaseId,
containerId: graphId,
partitionKey: "/pk",
useSharedThroughput: true,
});
const databaseNode = await explorer.waitForNode(databaseId);
const graphNode = await explorer.waitForContainerNode(databaseId, graphId);
await expect(graphNode.element).toBeAttached();
// Cleanup
await deleteDatabase(explorer, databaseId);
await expect(databaseNode.element).not.toBeAttached();
});
test("Gremlin: Manual throughput", async ({ page }) => {
const databaseId = generateUniqueName("db");
const graphId = generateUniqueName("graph");
const manualThroughput = 400;
const explorer = await DataExplorer.open(page, TestAccount.Gremlin);
await openAndFillCreateContainerPanel(explorer, GREMLIN_CONFIG, {
databaseId,
containerId: graphId,
partitionKey: "/pk",
isAutoscale: false,
throughputValue: manualThroughput,
});
const databaseNode = await explorer.waitForNode(databaseId);
const graphNode = await explorer.waitForContainerNode(databaseId, graphId);
await expect(graphNode.element).toBeAttached();
// Cleanup
await deleteDatabase(explorer, databaseId);
await expect(databaseNode.element).not.toBeAttached();
});
test("Gremlin: No unique keys support", async ({ page }) => {
const databaseId = generateUniqueName("db");
const graphId = generateUniqueName("graph");
const explorer = await DataExplorer.open(page, TestAccount.Gremlin); const explorer = await DataExplorer.open(page, TestAccount.Gremlin);
// Create new database and graph
await explorer.globalCommandButton("New Graph").click(); await explorer.globalCommandButton("New Graph").click();
await explorer.whilePanelOpen( await explorer.whilePanelOpen(
"New Graph", "New Graph",
async (panel, okButton) => { async (panel, okButton) => {
await panel.getByTestId("AddCollectionPanel/DatabaseId").fill(databaseId); await panel.getByPlaceholder("Type a new database id").fill(databaseId);
await panel.getByTestId("AddCollectionPanel/CollectionId").fill(graphId); await panel.getByRole("textbox", { name: "Graph id, Example Graph1" }).fill(graphId);
await panel.getByRole("textbox", { name: "Partition key" }).fill("/pk"); await panel.getByRole("textbox", { name: "Partition key" }).fill("/pk");
await panel.getByTestId("ThroughputInput/AutoscaleRUInput").fill(TEST_AUTOSCALE_THROUGHPUT_RU.toString()); await panel.getByTestId("autoscaleRUInput").fill(TEST_AUTOSCALE_THROUGHPUT_RU.toString());
// Verify unique key button is not present (Gremlin-specific API limitation)
const uniqueKeyButton = panel.getByTestId("AddCollectionPanel/AddUniqueKeyButton");
await expect(uniqueKeyButton).not.toBeVisible();
await okButton.click(); await okButton.click();
}, },
{ closeTimeout: 5 * 60 * 1000 }, { closeTimeout: 5 * 60 * 1000 },
@@ -110,9 +24,29 @@ test("Gremlin: No unique keys support", async ({ page }) => {
const databaseNode = await explorer.waitForNode(databaseId); const databaseNode = await explorer.waitForNode(databaseId);
const graphNode = await explorer.waitForContainerNode(databaseId, graphId); const graphNode = await explorer.waitForContainerNode(databaseId, graphId);
await expect(graphNode.element).toBeAttached();
// Cleanup await graphNode.openContextMenu();
await deleteDatabase(explorer, databaseId); await graphNode.contextMenuItem("Delete Graph").click();
await explorer.whilePanelOpen(
"Delete Graph",
async (panel, okButton) => {
await panel.getByRole("textbox", { name: "Confirm by typing the graph id" }).fill(graphId);
await okButton.click();
},
{ closeTimeout: 5 * 60 * 1000 },
);
await expect(graphNode.element).not.toBeAttached();
await databaseNode.openContextMenu();
await databaseNode.contextMenuItem("Delete Database").click();
await explorer.whilePanelOpen(
"Delete Database",
async (panel, okButton) => {
await panel.getByRole("textbox", { name: "Confirm by typing the Database id" }).fill(databaseId);
await okButton.click();
},
{ closeTimeout: 5 * 60 * 1000 },
);
await expect(databaseNode.element).not.toBeAttached(); await expect(databaseNode.element).not.toBeAttached();
}); });

View File

@@ -1,323 +0,0 @@
import { Locator } from "@playwright/test";
import { DataExplorer, TestAccount } from "../fx";
/**
* Container creation test API configuration
* Defines labels and selectors specific to each Cosmos DB API
*/
export interface ApiConfig {
account: TestAccount;
commandLabel: string; // "New Container", "New Collection", "New Graph", "New Table"
containerIdLabel: string; // "Container id", "Collection id", "Graph id", "Table id"
panelTitle: string; // "New Container", "New Collection", "New Graph", "Add Table"
databaseIdPlaceholder: string; // "Type a new keyspace id" for Cassandra, etc.
containerIdPlaceholder: string;
partitionKeyLabel?: string; // "Partition key", "Shard key", or undefined for Tables
partitionKeyPlaceholder?: string;
confirmDeleteLabel: string; // "Confirm by typing the [container/collection/table/graph] id"
databaseName?: string; // "TablesDB" for Tables, undefined for others
supportsUniqueKeys: boolean;
}
export const SQL_CONFIG: ApiConfig = {
account: TestAccount.SQL,
commandLabel: "New Container",
containerIdLabel: "Container id, Example Container1",
panelTitle: "New Container",
databaseIdPlaceholder: "Type a new database id",
containerIdPlaceholder: "e.g., Container1",
partitionKeyLabel: "Partition key",
partitionKeyPlaceholder: "/pk",
confirmDeleteLabel: "Confirm by typing the container id",
supportsUniqueKeys: true,
};
export const MONGO_CONFIG: ApiConfig = {
account: TestAccount.Mongo,
commandLabel: "New Collection",
containerIdLabel: "Collection id, Example Collection1",
panelTitle: "New Collection",
databaseIdPlaceholder: "Type a new database id",
containerIdPlaceholder: "e.g., Collection1",
partitionKeyLabel: "Shard key",
partitionKeyPlaceholder: "pk",
confirmDeleteLabel: "Confirm by typing the collection id",
supportsUniqueKeys: false,
};
export const MONGO32_CONFIG: ApiConfig = {
...MONGO_CONFIG,
account: TestAccount.Mongo32,
};
export const GREMLIN_CONFIG: ApiConfig = {
account: TestAccount.Gremlin,
commandLabel: "New Graph",
containerIdLabel: "Graph id, Example Graph1",
panelTitle: "New Graph",
databaseIdPlaceholder: "Type a new database id",
containerIdPlaceholder: "e.g., Graph1",
partitionKeyLabel: "Partition key",
partitionKeyPlaceholder: "/pk",
confirmDeleteLabel: "Confirm by typing the graph id",
supportsUniqueKeys: false,
};
export const TABLES_CONFIG: ApiConfig = {
account: TestAccount.Tables,
commandLabel: "New Table",
containerIdLabel: "Table id, Example Table1",
panelTitle: "New Table",
databaseIdPlaceholder: "", // Not used
containerIdPlaceholder: "e.g., Table1",
confirmDeleteLabel: "Confirm by typing the table id",
databaseName: "TablesDB",
supportsUniqueKeys: false,
};
export const CASSANDRA_CONFIG: ApiConfig = {
account: TestAccount.Cassandra,
commandLabel: "New Table",
containerIdLabel: "Enter table Id",
panelTitle: "Add Table",
databaseIdPlaceholder: "Type a new keyspace id",
containerIdPlaceholder: "Enter table Id",
confirmDeleteLabel: "Confirm by typing the table id",
supportsUniqueKeys: false,
};
/**
* Fills database selection in the panel
* Automatically selects "Create new" and fills the database ID
*/
export async function fillDatabaseSelection(panel: Locator, databaseId: string): Promise<void> {
// Wait for the radio button to be visible and click it (more reliable than check for custom styled radios)
await panel.getByTestId("AddCollectionPanel/DatabaseRadio:CreateNew").waitFor({ state: "visible" });
await panel.getByTestId("AddCollectionPanel/DatabaseRadio:CreateNew").click();
await panel.getByTestId("AddCollectionPanel/DatabaseId").fill(databaseId);
}
/**
* Fills existing database selection
* Selects "Use existing" and clicks the dropdown to select the database
*/
export async function fillExistingDatabaseSelection(panel: Locator, databaseId: string): Promise<void> {
await panel.getByTestId("AddCollectionPanel/DatabaseRadio:UseExisting").waitFor({ state: "visible" });
await panel.getByTestId("AddCollectionPanel/DatabaseRadio:UseExisting").click();
await panel.getByTestId("AddCollectionPanel/ExistingDatabaseDropdown").click();
await panel.locator(`text=${databaseId}`).click();
}
/**
* Fills container/collection/graph/table details
*/
export async function fillContainerDetails(
panel: Locator,
containerId: string,
partitionKey: string | undefined,
): Promise<void> {
await panel.getByTestId("AddCollectionPanel/CollectionId").fill(containerId);
if (partitionKey) {
await panel.getByTestId("AddCollectionPanel/PartitionKey").first().fill(partitionKey);
}
}
/**
* Fills Cassandra-specific table details
* (keyspace and table IDs are separate for Cassandra)
*/
export async function fillCassandraTableDetails(panel: Locator, keyspaceId: string, tableId: string): Promise<void> {
await panel.getByTestId("AddCollectionPanel/DatabaseId").fill(keyspaceId);
await panel.getByTestId("AddCollectionPanel/CollectionId").fill(tableId);
}
/**
* Sets throughput mode and value
* @param isAutoscale - if true, sets autoscale mode; if false, sets manual mode
*/
export async function setThroughput(panel: Locator, isAutoscale: boolean, throughputValue: number): Promise<void> {
const testId = isAutoscale ? "ThroughputInput/ThroughputMode:Autoscale" : "ThroughputInput/ThroughputMode:Manual";
await panel.getByTestId(testId).check();
if (isAutoscale) {
await panel.getByTestId("ThroughputInput/AutoscaleRUInput").fill(throughputValue.toString());
} else {
await panel.getByTestId("ThroughputInput/ManualThroughputInput").fill(throughputValue.toString());
}
}
/**
* Adds a unique key to the container (SQL/Mongo only)
*/
export async function addUniqueKey(panel: Locator, uniqueKeyValue: string): Promise<void> {
// Scroll to find the unique key section
await panel.getByTestId("AddCollectionPanel/UniqueKeysSection").scrollIntoViewIfNeeded();
// Click the "Add unique key" button
await panel.getByTestId("AddCollectionPanel/AddUniqueKeyButton").click();
// Fill in the unique key value
const uniqueKeyInput = panel.getByTestId("AddCollectionPanel/UniqueKey").first();
await uniqueKeyInput.fill(uniqueKeyValue);
}
/**
* Deletes a database and waits for it to disappear from the tree
*/
export async function deleteDatabase(
explorer: DataExplorer,
databaseId: string,
databaseNodeName: string = databaseId,
): Promise<void> {
const databaseNode = await explorer.waitForNode(databaseNodeName);
await databaseNode.openContextMenu();
await databaseNode.contextMenuItem("Delete Database").click();
await explorer.whilePanelOpen(
"Delete Database",
async (panel: Locator, okButton: Locator) => {
await panel.getByTestId("DeleteDatabaseConfirmationPanel/ConfirmInput").fill(databaseId);
await okButton.click();
},
{ closeTimeout: 5 * 60 * 1000 },
);
}
/**
* Deletes a keyspace (Cassandra only)
*/
export async function deleteKeyspace(explorer: DataExplorer, keyspaceId: string): Promise<void> {
const keyspaceNode = await explorer.waitForNode(keyspaceId);
await keyspaceNode.openContextMenu();
await keyspaceNode.contextMenuItem("Delete Keyspace").click();
await explorer.whilePanelOpen(
"Delete Keyspace",
async (panel: Locator, okButton: Locator) => {
await panel.getByTestId("DeleteCollectionConfirmationPane/ConfirmInput").fill(keyspaceId);
await okButton.click();
},
{ closeTimeout: 5 * 60 * 1000 },
);
}
/**
* Deletes a container/collection/graph/table
*/
export async function deleteContainer(
explorer: DataExplorer,
databaseId: string,
containerId: string,
deleteLabel: string, // "Delete Container", "Delete Collection", etc.
): Promise<void> {
const containerNode = await explorer.waitForContainerNode(databaseId, containerId);
await containerNode.openContextMenu();
await containerNode.contextMenuItem(deleteLabel).click();
await explorer.whilePanelOpen(
deleteLabel,
async (panel: Locator, okButton: Locator) => {
// All container/collection/graph/table deletes use same panel with test ID
await panel.getByTestId("DeleteCollectionConfirmationPane/ConfirmInput").fill(containerId);
await okButton.click();
},
{ closeTimeout: 5 * 60 * 1000 },
);
}
/**
* Opens the create container dialog and fills in the form based on scenario
*/
export async function openAndFillCreateContainerPanel(
explorer: DataExplorer,
config: ApiConfig,
options: {
databaseId: string;
containerId: string;
partitionKey?: string;
useExistingDatabase?: boolean;
isAutoscale?: boolean;
throughputValue?: number;
uniqueKey?: string;
useSharedThroughput?: boolean;
},
): Promise<void> {
await explorer.globalCommandButton(config.commandLabel).click();
await explorer.whilePanelOpen(
config.panelTitle,
async (panel, okButton) => {
// Database selection
if (options.useExistingDatabase) {
await fillExistingDatabaseSelection(panel, options.databaseId);
} else {
await fillDatabaseSelection(panel, options.databaseId);
}
// Shared throughput checkbox (if applicable)
if (options.useSharedThroughput) {
await panel
.getByTestId("AddCollectionPanel/SharedThroughputCheckbox")
.getByRole("checkbox")
.check({ force: true });
}
// Container details
await fillContainerDetails(panel, options.containerId, options.partitionKey);
// Throughput (only if not using shared throughput)
if (!options.useSharedThroughput) {
const isAutoscale = options.isAutoscale !== false;
const throughputValue = options.throughputValue || 1000;
await setThroughput(panel, isAutoscale, throughputValue);
}
// Unique keys (if applicable)
if (options.uniqueKey && config.supportsUniqueKeys) {
await addUniqueKey(panel, options.uniqueKey);
}
await okButton.click();
},
{ closeTimeout: 5 * 60 * 1000 },
);
}
/**
* Opens the create table dialog for Cassandra and fills in the form
* Cassandra has a different UI pattern than other APIs
*/
export async function openAndFillCreateCassandraTablePanel(
explorer: DataExplorer,
options: {
keyspaceId: string;
tableId: string;
isAutoscale?: boolean;
throughputValue?: number;
useSharedThroughput?: boolean;
},
): Promise<void> {
await explorer.globalCommandButton("New Table").click();
await explorer.whilePanelOpen(
"Add Table",
async (panel, okButton) => {
// Fill Cassandra-specific table details
await fillCassandraTableDetails(panel, options.keyspaceId, options.tableId);
// Shared throughput checkbox (if applicable)
if (options.useSharedThroughput) {
await panel
.getByTestId("AddCollectionPanel/SharedThroughputCheckbox")
.getByRole("checkbox")
.check({ force: true });
}
// Throughput (only if not using shared throughput)
if (!options.useSharedThroughput) {
const isAutoscale = options.isAutoscale !== false;
const throughputValue = options.throughputValue || 1000;
await setThroughput(panel, isAutoscale, throughputValue);
}
await okButton.click();
},
{ closeTimeout: 5 * 60 * 1000 },
);
}

View File

@@ -1,118 +1,58 @@
import { expect, test } from "@playwright/test"; import { expect, test } from "@playwright/test";
import { DataExplorer, TEST_AUTOSCALE_THROUGHPUT_RU, TestAccount, generateUniqueName } from "../fx"; import { DataExplorer, TEST_AUTOSCALE_THROUGHPUT_RU, TestAccount, generateUniqueName } from "../fx";
import {
MONGO32_CONFIG,
MONGO_CONFIG,
deleteContainer,
deleteDatabase,
openAndFillCreateContainerPanel,
} from "../helpers/containerCreationHelpers";
( (
[ [
["latest API version", MONGO_CONFIG], ["latest API version", TestAccount.Mongo],
["3.2 API", MONGO32_CONFIG], ["3.2 API", TestAccount.Mongo32],
] as [string, typeof MONGO_CONFIG][] ] as [string, TestAccount][]
).forEach(([apiVersionDescription, config]) => { ).forEach(([apiVersionDescription, accountType]) => {
test(`Mongo: Database and collection CRUD using ${apiVersionDescription}`, async ({ page }) => { test(`Mongo CRUD using ${apiVersionDescription}`, async ({ page }) => {
const databaseId = generateUniqueName("db"); const databaseId = generateUniqueName("db");
const collectionId = generateUniqueName("collection"); const collectionId = "testcollection"; // A unique collection name isn't needed because the database is unique
const explorer = await DataExplorer.open(page, config.account); const explorer = await DataExplorer.open(page, accountType);
// Create await explorer.globalCommandButton("New Collection").click();
await openAndFillCreateContainerPanel(explorer, config, { await explorer.whilePanelOpen(
databaseId, "New Collection",
containerId: collectionId, async (panel, okButton) => {
partitionKey: "pk", await panel.getByPlaceholder("Type a new database id").fill(databaseId);
isAutoscale: true, await panel.getByRole("textbox", { name: "Collection id, Example Collection1" }).fill(collectionId);
throughputValue: TEST_AUTOSCALE_THROUGHPUT_RU, await panel.getByRole("textbox", { name: "Shard key" }).fill("pk");
}); await panel.getByTestId("autoscaleRUInput").fill(TEST_AUTOSCALE_THROUGHPUT_RU.toString());
await okButton.click();
},
{ closeTimeout: 5 * 60 * 1000 },
);
const databaseNode = await explorer.waitForNode(databaseId); const databaseNode = await explorer.waitForNode(databaseId);
const collectionNode = await explorer.waitForContainerNode(databaseId, collectionId); const collectionNode = await explorer.waitForContainerNode(databaseId, collectionId);
await expect(collectionNode.element).toBeAttached();
// Delete collection await collectionNode.openContextMenu();
await deleteContainer(explorer, databaseId, collectionId, "Delete Collection"); await collectionNode.contextMenuItem("Delete Collection").click();
await explorer.whilePanelOpen(
"Delete Collection",
async (panel, okButton) => {
await panel.getByRole("textbox", { name: "Confirm by typing the collection id" }).fill(collectionId);
await okButton.click();
},
{ closeTimeout: 5 * 60 * 1000 },
);
await expect(collectionNode.element).not.toBeAttached(); await expect(collectionNode.element).not.toBeAttached();
// Delete database await databaseNode.openContextMenu();
await deleteDatabase(explorer, databaseId); await databaseNode.contextMenuItem("Delete Database").click();
await explorer.whilePanelOpen(
"Delete Database",
async (panel, okButton) => {
await panel.getByRole("textbox", { name: "Confirm by typing the Database id" }).fill(databaseId);
await okButton.click();
},
{ closeTimeout: 5 * 60 * 1000 },
);
await expect(databaseNode.element).not.toBeAttached(); await expect(databaseNode.element).not.toBeAttached();
}); });
}); });
test("Mongo: New database shared throughput", async ({ page }) => {
const databaseId = generateUniqueName("db");
const collectionId = generateUniqueName("collection");
const explorer = await DataExplorer.open(page, TestAccount.Mongo);
await openAndFillCreateContainerPanel(explorer, MONGO_CONFIG, {
databaseId,
containerId: collectionId,
partitionKey: "pk",
useSharedThroughput: true,
});
const databaseNode = await explorer.waitForNode(databaseId);
const collectionNode = await explorer.waitForContainerNode(databaseId, collectionId);
await expect(collectionNode.element).toBeAttached();
// Cleanup
await deleteDatabase(explorer, databaseId);
await expect(databaseNode.element).not.toBeAttached();
});
test("Mongo: Unique keys", async ({ page }) => {
const databaseId = generateUniqueName("db");
const collectionId = generateUniqueName("collection");
const explorer = await DataExplorer.open(page, TestAccount.Mongo);
await openAndFillCreateContainerPanel(explorer, MONGO_CONFIG, {
databaseId,
containerId: collectionId,
partitionKey: "pk",
isAutoscale: true,
throughputValue: TEST_AUTOSCALE_THROUGHPUT_RU,
uniqueKey: "email",
});
const databaseNode = await explorer.waitForNode(databaseId);
const collectionNode = await explorer.waitForContainerNode(databaseId, collectionId);
await expect(collectionNode.element).toBeAttached();
// Cleanup
await deleteDatabase(explorer, databaseId);
await expect(databaseNode.element).not.toBeAttached();
});
test("Mongo: Manual throughput", async ({ page }) => {
const databaseId = generateUniqueName("db");
const collectionId = generateUniqueName("collection");
const manualThroughput = 400;
const explorer = await DataExplorer.open(page, TestAccount.Mongo);
await openAndFillCreateContainerPanel(explorer, MONGO_CONFIG, {
databaseId,
containerId: collectionId,
partitionKey: "pk",
isAutoscale: false,
throughputValue: manualThroughput,
});
const databaseNode = await explorer.waitForNode(databaseId);
const collectionNode = await explorer.waitForContainerNode(databaseId, collectionId);
await expect(collectionNode.element).toBeAttached();
// Cleanup
await deleteDatabase(explorer, databaseId);
await expect(databaseNode.element).not.toBeAttached();
});

View File

@@ -1,110 +1,51 @@
import { expect, test } from "@playwright/test"; import { expect, test } from "@playwright/test";
import { DataExplorer, TEST_AUTOSCALE_THROUGHPUT_RU, TestAccount, generateUniqueName } from "../fx"; import { DataExplorer, TEST_AUTOSCALE_THROUGHPUT_RU, TestAccount, generateUniqueName } from "../fx";
import {
SQL_CONFIG,
deleteContainer,
deleteDatabase,
openAndFillCreateContainerPanel,
} from "../helpers/containerCreationHelpers";
test("SQL: Database and container CRUD", async ({ page }) => { test("SQL database and container CRUD", async ({ page }) => {
const databaseId = generateUniqueName("db"); const databaseId = generateUniqueName("db");
const containerId = generateUniqueName("container"); const containerId = "testcontainer"; // A unique container name isn't needed because the database is unique
const explorer = await DataExplorer.open(page, TestAccount.SQL); const explorer = await DataExplorer.open(page, TestAccount.SQL);
// Create await explorer.globalCommandButton("New Container").click();
await openAndFillCreateContainerPanel(explorer, SQL_CONFIG, { await explorer.whilePanelOpen(
databaseId, "New Container",
containerId, async (panel, okButton) => {
partitionKey: "/pk", await panel.getByPlaceholder("Type a new database id").fill(databaseId);
isAutoscale: true, await panel.getByRole("textbox", { name: "Container id, Example Container1" }).fill(containerId);
throughputValue: TEST_AUTOSCALE_THROUGHPUT_RU, await panel.getByRole("textbox", { name: "Partition key" }).fill("/pk");
}); await panel.getByTestId("autoscaleRUInput").fill(TEST_AUTOSCALE_THROUGHPUT_RU.toString());
await okButton.click();
},
{ closeTimeout: 5 * 60 * 1000 },
);
const databaseNode = await explorer.waitForNode(databaseId); const databaseNode = await explorer.waitForNode(databaseId);
const containerNode = await explorer.waitForContainerNode(databaseId, containerId); const containerNode = await explorer.waitForContainerNode(databaseId, containerId);
await expect(containerNode.element).toBeAttached();
// Delete container await containerNode.openContextMenu();
await deleteContainer(explorer, databaseId, containerId, "Delete Container"); await containerNode.contextMenuItem("Delete Container").click();
await explorer.whilePanelOpen(
"Delete Container",
async (panel, okButton) => {
await panel.getByRole("textbox", { name: "Confirm by typing the container id" }).fill(containerId);
await okButton.click();
},
{ closeTimeout: 5 * 60 * 1000 },
);
await expect(containerNode.element).not.toBeAttached(); await expect(containerNode.element).not.toBeAttached();
// Delete database await databaseNode.openContextMenu();
await deleteDatabase(explorer, databaseId); await databaseNode.contextMenuItem("Delete Database").click();
await expect(databaseNode.element).not.toBeAttached(); await explorer.whilePanelOpen(
}); "Delete Database",
async (panel, okButton) => {
test("SQL: New database shared throughput", async ({ page }) => { await panel.getByRole("textbox", { name: "Confirm by typing the database id" }).fill(databaseId);
const databaseId = generateUniqueName("db"); await okButton.click();
const containerId = generateUniqueName("container"); },
{ closeTimeout: 5 * 60 * 1000 },
const explorer = await DataExplorer.open(page, TestAccount.SQL); );
await openAndFillCreateContainerPanel(explorer, SQL_CONFIG, {
databaseId,
containerId,
partitionKey: "/pk",
useSharedThroughput: true,
});
const databaseNode = await explorer.waitForNode(databaseId);
const containerNode = await explorer.waitForContainerNode(databaseId, containerId);
await expect(containerNode.element).toBeAttached();
// Cleanup
await deleteDatabase(explorer, databaseId);
await expect(databaseNode.element).not.toBeAttached();
});
test("SQL: Unique keys", async ({ page }) => {
const databaseId = generateUniqueName("db");
const containerId = generateUniqueName("container");
const explorer = await DataExplorer.open(page, TestAccount.SQL);
await openAndFillCreateContainerPanel(explorer, SQL_CONFIG, {
databaseId,
containerId,
partitionKey: "/pk",
isAutoscale: true,
throughputValue: TEST_AUTOSCALE_THROUGHPUT_RU,
uniqueKey: "/email,/username",
});
const databaseNode = await explorer.waitForNode(databaseId);
const containerNode = await explorer.waitForContainerNode(databaseId, containerId);
await expect(containerNode.element).toBeAttached();
// Cleanup
await deleteDatabase(explorer, databaseId);
await expect(databaseNode.element).not.toBeAttached();
});
test("SQL: Manual throughput", async ({ page }) => {
const databaseId = generateUniqueName("db");
const containerId = generateUniqueName("container");
const manualThroughput = 400;
const explorer = await DataExplorer.open(page, TestAccount.SQL);
await openAndFillCreateContainerPanel(explorer, SQL_CONFIG, {
databaseId,
containerId,
partitionKey: "/pk",
isAutoscale: false,
throughputValue: manualThroughput,
});
const databaseNode = await explorer.waitForNode(databaseId);
const containerNode = await explorer.waitForContainerNode(databaseId, containerId);
await expect(containerNode.element).toBeAttached();
// Cleanup
await deleteDatabase(explorer, databaseId);
await expect(databaseNode.element).not.toBeAttached(); await expect(databaseNode.element).not.toBeAttached();
}); });

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,
});
});
});

View File

@@ -1,116 +1,35 @@
import { expect, test } from "@playwright/test"; import { expect, test } from "@playwright/test";
import { DataExplorer, TEST_AUTOSCALE_THROUGHPUT_RU, TestAccount, generateUniqueName } from "../fx"; import { DataExplorer, TEST_AUTOSCALE_THROUGHPUT_RU, TestAccount, generateUniqueName } from "../fx";
import { TABLES_CONFIG, deleteContainer, openAndFillCreateContainerPanel } from "../helpers/containerCreationHelpers";
test("Tables: CRUD", async ({ page }) => { test("Tables CRUD", async ({ page }) => {
const tableId = generateUniqueName("table"); const tableId = generateUniqueName("table"); // A unique table name IS needed because the database is shared when using Table Storage.
const explorer = await DataExplorer.open(page, TestAccount.Tables); const explorer = await DataExplorer.open(page, TestAccount.Tables);
// Create await explorer.globalCommandButton("New Table").click();
await openAndFillCreateContainerPanel(explorer, TABLES_CONFIG, { await explorer.whilePanelOpen(
databaseId: "TablesDB", "New Table",
containerId: tableId, async (panel, okButton) => {
isAutoscale: true, await panel.getByRole("textbox", { name: "Table id, Example Table1" }).fill(tableId);
throughputValue: TEST_AUTOSCALE_THROUGHPUT_RU, await panel.getByTestId("autoscaleRUInput").fill(TEST_AUTOSCALE_THROUGHPUT_RU.toString());
}); await okButton.click();
},
{ closeTimeout: 5 * 60 * 1000 },
);
const tableNode = await explorer.waitForContainerNode("TablesDB", tableId); const tableNode = await explorer.waitForContainerNode("TablesDB", tableId);
await expect(tableNode.element).toBeAttached();
// Delete table await tableNode.openContextMenu();
await deleteContainer(explorer, "TablesDB", tableId, "Delete Table"); await tableNode.contextMenuItem("Delete Table").click();
await expect(tableNode.element).not.toBeAttached(); await explorer.whilePanelOpen(
}); "Delete Table",
async (panel, okButton) => {
test("Tables: New database shared throughput", async ({ page }) => { await panel.getByRole("textbox", { name: "Confirm by typing the table id" }).fill(tableId);
const tableId = generateUniqueName("table"); await okButton.click();
},
const explorer = await DataExplorer.open(page, TestAccount.Tables); { closeTimeout: 5 * 60 * 1000 },
);
await openAndFillCreateContainerPanel(explorer, TABLES_CONFIG, {
databaseId: "TablesDB",
containerId: tableId,
useSharedThroughput: true,
});
const tableNode = await explorer.waitForContainerNode("TablesDB", tableId);
await expect(tableNode.element).toBeAttached();
// Cleanup
await deleteContainer(explorer, "TablesDB", tableId, "Delete Table");
await expect(tableNode.element).not.toBeAttached();
});
test("Tables: Manual throughput", async ({ page }) => {
const tableId = generateUniqueName("table");
const manualThroughput = 400;
const explorer = await DataExplorer.open(page, TestAccount.Tables);
await openAndFillCreateContainerPanel(explorer, TABLES_CONFIG, {
databaseId: "TablesDB",
containerId: tableId,
isAutoscale: false,
throughputValue: manualThroughput,
});
const tableNode = await explorer.waitForContainerNode("TablesDB", tableId);
await expect(tableNode.element).toBeAttached();
// Cleanup
await deleteContainer(explorer, "TablesDB", tableId, "Delete Table");
await expect(tableNode.element).not.toBeAttached();
});
test("Tables: Multiple tables in TablesDB", async ({ page }) => {
const table1Id = generateUniqueName("table");
const table2Id = generateUniqueName("table");
const explorer = await DataExplorer.open(page, TestAccount.Tables);
// Create first table
await openAndFillCreateContainerPanel(explorer, TABLES_CONFIG, {
databaseId: "TablesDB",
containerId: table1Id,
isAutoscale: true,
throughputValue: TEST_AUTOSCALE_THROUGHPUT_RU,
});
await explorer.waitForContainerNode("TablesDB", table1Id);
// Create second table
await openAndFillCreateContainerPanel(explorer, TABLES_CONFIG, {
databaseId: "TablesDB",
containerId: table2Id,
isAutoscale: true,
throughputValue: TEST_AUTOSCALE_THROUGHPUT_RU,
});
await explorer.waitForContainerNode("TablesDB", table2Id);
// Cleanup
await deleteContainer(explorer, "TablesDB", table1Id, "Delete Table");
await deleteContainer(explorer, "TablesDB", table2Id, "Delete Table");
});
test("Tables: No partition key support", async ({ page }) => {
const tableId = generateUniqueName("table");
const explorer = await DataExplorer.open(page, TestAccount.Tables);
await openAndFillCreateContainerPanel(explorer, TABLES_CONFIG, {
databaseId: "TablesDB",
containerId: tableId,
isAutoscale: true,
throughputValue: TEST_AUTOSCALE_THROUGHPUT_RU,
});
const tableNode = await explorer.waitForContainerNode("TablesDB", tableId);
await expect(tableNode.element).toBeAttached();
// Cleanup
await deleteContainer(explorer, "TablesDB", tableId, "Delete Table");
await expect(tableNode.element).not.toBeAttached(); await expect(tableNode.element).not.toBeAttached();
}); });

View File

@@ -134,7 +134,7 @@ const initTestExplorer = async (): Promise<void> => {
); );
iframe.id = "explorerMenu"; iframe.id = "explorerMenu";
iframe.name = "explorer"; iframe.name = "explorer";
iframe.setAttribute("data-testid", "DataExplorerFrame"); iframe.setAttribute("data-test", "DataExplorerFrame");
iframe.classList.add("iframe"); iframe.classList.add("iframe");
iframe.title = "explorer"; iframe.title = "explorer";
iframe.src = iframeSrc; // CodeQL [SM03712] Not used in production, only for testing purposes iframe.src = iframeSrc; // CodeQL [SM03712] Not used in production, only for testing purposes