From 52322c36494257e280c2d5e6cd65d78e8b3d5286 Mon Sep 17 00:00:00 2001 From: Bikram Choudhury Date: Wed, 24 Dec 2025 02:58:08 +0530 Subject: [PATCH] Add E2E tests for partition key change workflow --- .../PartitionKeyComponent.tsx | 4 +- .../PartitionKeyComponent.test.tsx.snap | 4 + .../ChangePartitionKeyPane.tsx | 17 +++- test/fx.ts | 9 ++ test/sql/query.spec.ts | 2 +- .../changePartitionKey.spec.ts | 98 +++++++++++++++++++ test/sql/scaleAndSettings/scale.spec.ts | 2 +- test/sql/scaleAndSettings/settings.spec.ts | 2 +- test/testData.ts | 16 ++- 9 files changed, 144 insertions(+), 10 deletions(-) create mode 100644 test/sql/scaleAndSettings/changePartitionKey.spec.ts diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/PartitionKeyComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/PartitionKeyComponent.tsx index a58bf50cd..336a0d972 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/PartitionKeyComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/PartitionKeyComponent.tsx @@ -187,7 +187,7 @@ export const PartitionKeyComponent: React.FC = ({ Current {partitionKeyName.toLowerCase()} Partitioning - + {partitionKeyValue} {isHierarchicalPartitionedContainer() ? "Hierarchical" : "Non-hierarchical"} @@ -199,6 +199,7 @@ export const PartitionKeyComponent: React.FC = ({ {!isReadOnly && ( <> = ({ {configContext.platform !== Platform.Emulator && ( = ({ {createNewContainer ? ( - + All configurations except for unique keys will be copied from the source container @@ -230,6 +230,7 @@ export const ChangePartitionKeyPane: React.FC = ({ = ({ = ({ type="text" id="addCollection-partitionKeyValue" key={`addCollection-partitionKeyValue_${index}`} + data-test={`new-container-sub-partition-key-input-${index}`} aria-required required size={40} @@ -327,6 +330,8 @@ export const ChangePartitionKeyPane: React.FC = ({ }} /> { @@ -339,6 +344,7 @@ export const ChangePartitionKeyPane: React.FC = ({ })} = Constants.BackendDefaults.maxNumMultiHashPartition} onClick={() => setSubPartitionKeys([...subPartitionKeys, ""])} @@ -346,7 +352,11 @@ export const ChangePartitionKeyPane: React.FC = ({ Add hierarchical partition key {subPartitionKeys.length > 0 && ( - + This feature allows you to partition your data with up to three levels of keys for better data distribution. Requires .NET V3, Java V4 SDK, or preview JavaScript V3 SDK.{" "} @@ -359,7 +369,7 @@ export const ChangePartitionKeyPane: React.FC = ({ ) : ( - + @@ -390,6 +400,7 @@ export const ChangePartitionKeyPane: React.FC = ({ }} defaultSelectedKey={targetCollectionId} responsiveMode={999} + ariaLabel="Existing Containers" /> )} diff --git a/test/fx.ts b/test/fx.ts index 56e571635..3731f6e1e 100644 --- a/test/fx.ts +++ b/test/fx.ts @@ -470,6 +470,15 @@ export class DataExplorer { return this.frame.getByTestId("notification-console/header-status"); } + async getDropdownItemByName(name: string, ariaLabel?: string): Promise { + const dropdownItemsWrapper = this.frame.locator("div.ms-Dropdown-items"); + if (ariaLabel) { + expect(await dropdownItemsWrapper.getAttribute("aria-label")).toEqual(ariaLabel); + } + const containerDropdownItems = dropdownItemsWrapper.locator("button.ms-Dropdown-item[role='option']"); + return containerDropdownItems.filter({ hasText: name }); + } + /** Waits for the Data Explorer app to load */ static async waitForExplorer(page: Page) { const iframeElement = await page.getByTestId("DataExplorerFrame").elementHandle(); diff --git a/test/sql/query.spec.ts b/test/sql/query.spec.ts index f574cd8fb..6368c4327 100644 --- a/test/sql/query.spec.ts +++ b/test/sql/query.spec.ts @@ -9,7 +9,7 @@ let queryTab: QueryTab = null!; let queryEditor: Editor = null!; test.beforeAll("Create Test Database", async () => { - context = await createTestSQLContainer(true); + context = await createTestSQLContainer({ includeTestData: true }); }); test.beforeEach("Open new query tab", async ({ page }) => { diff --git a/test/sql/scaleAndSettings/changePartitionKey.spec.ts b/test/sql/scaleAndSettings/changePartitionKey.spec.ts new file mode 100644 index 000000000..da9b422ef --- /dev/null +++ b/test/sql/scaleAndSettings/changePartitionKey.spec.ts @@ -0,0 +1,98 @@ +import { expect, Page, test } from "@playwright/test"; +import { DataExplorer, TestAccount } from "../../fx"; +import { createTestSQLContainer, TestContainerContext } from "../../testData"; + +test.describe("Change Partition Key", () => { + let pageInstance: Page; + let context: TestContainerContext = null!; + let explorer: DataExplorer = null!; + const newPartitionKeyPath = "/newPartitionKey"; + const newContainerId = "testcontainer_1"; + + test.beforeAll("Create Test Database", async () => { + context = await createTestSQLContainer(); + }); + + test.beforeEach("Open container settings", async ({ page }) => { + pageInstance = page; + explorer = await DataExplorer.open(page, TestAccount.SQL); + + // Click Scale & Settings and open Partition Key tab + await explorer.openScaleAndSettings(context); + const PartitionKeyTab = explorer.frame.getByTestId("settings-tab-header/PartitionKeyTab"); + await PartitionKeyTab.click(); + }); + + test.afterAll("Delete Test Database", async () => { + await context?.dispose(); + }); + + test("Change partition key path", async () => { + await expect(explorer.frame.getByText("/partitionKey")).toBeVisible(); + await expect(explorer.frame.getByText("Change partition key")).toBeVisible(); + await expect(explorer.frame.getByText(/To safeguard the integrity of/)).toBeVisible(); + await expect(explorer.frame.getByText(/To change the partition key/)).toBeVisible(); + + const changePartitionKeyButton = explorer.frame.getByTestId("change-partition-key-button"); + expect(changePartitionKeyButton).toBeVisible(); + await changePartitionKeyButton.click(); + + // Fill out new partition key form in the panel + const changePkPanel = explorer.frame.getByTestId(`Panel:Change partition key`); + await expect(changePkPanel.getByText(context.database.id)).toBeVisible(); + await expect(explorer.frame.getByRole("heading", { name: "Change partition key" })).toBeVisible(); + await expect(explorer.frame.getByText(/When changing a container/)).toBeVisible(); + + // Try to switch to new container + await expect(changePkPanel.getByText("New container")).toBeVisible(); + await expect(changePkPanel.getByText("Existing container")).toBeVisible(); + await expect(changePkPanel.getByTestId("new-container-id-input")).toBeVisible(); + + changePkPanel.getByTestId("new-container-id-input").fill(newContainerId); + await expect(changePkPanel.getByTestId("new-container-partition-key-input")).toBeVisible(); + changePkPanel.getByTestId("new-container-partition-key-input").fill(newPartitionKeyPath); + + await expect(changePkPanel.getByTestId("add-sub-partition-key-button")).toBeVisible(); + changePkPanel.getByTestId("add-sub-partition-key-button").click(); + await expect(changePkPanel.getByTestId("new-container-sub-partition-key-input-0")).toBeVisible(); + await expect(changePkPanel.getByTestId("remove-sub-partition-key-button-0")).toBeVisible(); + await expect(changePkPanel.getByTestId("hierarchical-partitioning-info-text")).toBeVisible(); + changePkPanel.getByTestId("new-container-sub-partition-key-input-0").fill("/customerId"); + + await changePkPanel.getByTestId("Panel/OkButton").click(); + + await pageInstance.waitForLoadState("networkidle"); + await expect(changePkPanel).not.toBeVisible({ timeout: 60 * 1000 }); + + // Verify partition key change job + const jobText = explorer.frame.getByText(/Partition key change job/); + await expect(jobText).toBeVisible(); + await expect(explorer.frame.locator(".ms-ProgressIndicator-itemName")).toContainText("Portal_testcontainer_1"); + + const jobRow = explorer.frame.locator(".ms-ProgressIndicator-itemDescription"); + await expect(jobRow.getByText("Completed")).toBeVisible({ timeout: 30 * 1000 }); + + const newContainerNode = await explorer.waitForContainerNode(context.database.id, newContainerId); + expect(newContainerNode).not.toBeNull(); + + // Now try to switch to existing container + await changePartitionKeyButton.click(); + await changePkPanel.getByText("Existing container").click(); + await changePkPanel.getByLabel("Use existing container").check(); + await changePkPanel.getByText("Choose an existing container").click(); + + const containerDropdownItem = await explorer.getDropdownItemByName(newContainerId, "Existing Containers"); + await containerDropdownItem.click(); + + await changePkPanel.getByTestId("Panel/OkButton").click(); + await explorer.frame.getByRole("button", { name: "Cancel" }).click(); + + // Dismiss overlay if it appears + const overlayFrame = explorer.frame.locator("#webpack-dev-server-client-overlay").first(); + if (await overlayFrame.count()) { + await overlayFrame.contentFrame().getByLabel("Dismiss").click(); + } + const cancelledJobRow = explorer.frame.getByTestId("Tab:tab0"); + await expect(cancelledJobRow.getByText("Cancelled")).toBeVisible({ timeout: 30 * 1000 }); + }); +}); diff --git a/test/sql/scaleAndSettings/scale.spec.ts b/test/sql/scaleAndSettings/scale.spec.ts index e3035a7dc..4937b1738 100644 --- a/test/sql/scaleAndSettings/scale.spec.ts +++ b/test/sql/scaleAndSettings/scale.spec.ts @@ -14,7 +14,7 @@ test.describe("Autoscale and Manual throughput", () => { let explorer: DataExplorer = null!; test.beforeAll("Create Test Database", async () => { - context = await createTestSQLContainer(true); + context = await createTestSQLContainer({ includeTestData: true }); }); test.beforeEach("Open container settings", async ({ page }) => { diff --git a/test/sql/scaleAndSettings/settings.spec.ts b/test/sql/scaleAndSettings/settings.spec.ts index 463cdacb7..894f5444b 100644 --- a/test/sql/scaleAndSettings/settings.spec.ts +++ b/test/sql/scaleAndSettings/settings.spec.ts @@ -7,7 +7,7 @@ test.describe("Settings under Scale & Settings", () => { let explorer: DataExplorer = null!; test.beforeAll("Create Test Database", async () => { - context = await createTestSQLContainer(true); + context = await createTestSQLContainer({ includeTestData: true }); }); test.beforeEach("Open Settings tab under Scale & Settings", async ({ page }) => { diff --git a/test/testData.ts b/test/testData.ts index 94d38941f..9729a90b4 100644 --- a/test/testData.ts +++ b/test/testData.ts @@ -74,8 +74,18 @@ export class TestContainerContext { } } -export async function createTestSQLContainer(includeTestData?: boolean) { - const databaseId = generateUniqueName("db"); +type createTestSqlContainerConfig = { + includeTestData?: boolean; + partitionKey?: string; + databaseName?: string; +}; + +export async function createTestSQLContainer({ + includeTestData = false, + partitionKey = "/partitionKey", + databaseName = "", +}: createTestSqlContainerConfig = {}) { + const databaseId = databaseName ? databaseName : generateUniqueName("db"); const containerId = "testcontainer"; // A unique container name isn't needed because the database is unique const credentials = getAzureCLICredentials(); const adaptedCredentials = new AzureIdentityCredentialAdapter(credentials); @@ -104,7 +114,7 @@ export async function createTestSQLContainer(includeTestData?: boolean) { try { const { container } = await database.containers.createIfNotExists({ id: containerId, - partitionKey: "/partitionKey", + partitionKey, }); if (includeTestData) { const batchCount = TestData.length / 100;