From d67c1a0464cf0c50ea791289bba7be8b063c8e92 Mon Sep 17 00:00:00 2001 From: asier-isayas Date: Wed, 10 Dec 2025 14:02:31 -0500 Subject: [PATCH] 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 --- .../Controls/Settings/SettingsComponent.tsx | 3 + .../SubSettingsComponent.tsx | 7 +- .../ThroughputInputAutoPilotV3Component.tsx | 16 ++- ...putInputAutoPilotV3Component.test.tsx.snap | 8 ++ .../SubSettingsComponent.test.tsx.snap | 19 +++ .../SettingsComponent.test.tsx.snap | 35 +++++ .../NotificationConsoleComponent.tsx | 2 +- ...NotificationConsoleComponent.test.tsx.snap | 2 + test/README.md | 4 +- test/fx.ts | 29 +++- test/sql/query.spec.ts | 8 +- test/sql/scaleAndSettings/scale.spec.ts | 129 ++++++++++++++++++ test/sql/scaleAndSettings/settings.spec.ts | 70 ++++++++++ 13 files changed, 315 insertions(+), 17 deletions(-) create mode 100644 test/sql/scaleAndSettings/scale.spec.ts create mode 100644 test/sql/scaleAndSettings/settings.spec.ts diff --git a/src/Explorer/Controls/Settings/SettingsComponent.tsx b/src/Explorer/Controls/Settings/SettingsComponent.tsx index c3c7be1cb..9a87f5b0c 100644 --- a/src/Explorer/Controls/Settings/SettingsComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsComponent.tsx @@ -1482,6 +1482,9 @@ export class SettingsComponent extends React.Component { @@ -223,6 +223,7 @@ export class SubSettingsComponent extends React.Component )} diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx index 16ec09f80..3d258fc56 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx @@ -503,7 +503,9 @@ export class ThroughputInputAutoPilotV3Component extends React.Component< {this.props.instantMaximumThroughput.toLocaleString()} - {this.props.softAllowedMaximumThroughput.toLocaleString()} + + {this.props.softAllowedMaximumThroughput.toLocaleString()} + { const sanitizedValue = getSanitizedInputValue(value); - return sanitizedValue % 1000 - ? "Throughput value must be in increments of 1000" - : this.props.throughputError; + const errorMessage: string = + sanitizedValue % 1000 ? "Throughput value must be in increments of 1000" : this.props.throughputError; + return {errorMessage}; }} validateOnLoad={false} + data-test="autopilot-throughput-input" /> @@ -650,7 +653,10 @@ export class ThroughputInputAutoPilotV3Component extends React.Component< } onChange={this.onThroughputChange} min={this.props.minimum} - errorMessage={this.props.throughputError} + onGetErrorMessage={() => { + return {this.props.throughputError}; + }} + data-test="manual-throughput-input" /> )} diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/__snapshots__/ThroughputInputAutoPilotV3Component.test.tsx.snap b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/__snapshots__/ThroughputInputAutoPilotV3Component.test.tsx.snap index f4e300d91..7b144745e 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/__snapshots__/ThroughputInputAutoPilotV3Component.test.tsx.snap +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/__snapshots__/ThroughputInputAutoPilotV3Component.test.tsx.snap @@ -273,6 +273,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = ` /> - + {this.state.headerStatus} diff --git a/src/Explorer/Menus/NotificationConsole/__snapshots__/NotificationConsoleComponent.test.tsx.snap b/src/Explorer/Menus/NotificationConsole/__snapshots__/NotificationConsoleComponent.test.tsx.snap index 5d471f177..83009fd99 100644 --- a/src/Explorer/Menus/NotificationConsole/__snapshots__/NotificationConsoleComponent.test.tsx.snap +++ b/src/Explorer/Menus/NotificationConsole/__snapshots__/NotificationConsoleComponent.test.tsx.snap @@ -78,6 +78,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = ` /> = { 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 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) { if (process.env.DE_TEST_ACCOUNT_PREFIX) { @@ -319,6 +323,11 @@ type PanelOpenOptions = { closeTimeout?: number; }; +export enum CommandBarButton { + Save = "Save", + ExecuteQuery = "Execute Query", +} + /** Helper class that provides locator methods for DataExplorer components, on top of a Frame */ export class DataExplorer { constructor(public frame: Frame) {} @@ -348,8 +357,8 @@ export class DataExplorer { } /** Select the command bar button with the specified label */ - commandBarButton(label: string): Locator { - return this.frame.getByTestId(`CommandBar/Button:${label}`).and(this.frame.locator("css=button")); + commandBarButton(commandBarButton: CommandBarButton): Locator { + return this.frame.getByTestId(`CommandBar/Button:${commandBarButton}`).and(this.frame.locator("css=button")); } dialogButton(label: string): Locator { @@ -445,6 +454,22 @@ export class DataExplorer { await panel.waitFor({ state: "detached", timeout: options.closeTimeout }); } + /** Opens the Scale & Settings panel for the specified container */ + async openScaleAndSettings(context: TestContainerContext): Promise { + 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 */ 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 5872346bb..f574cd8fb 100644 --- a/test/sql/query.spec.ts +++ b/test/sql/query.spec.ts @@ -1,6 +1,6 @@ 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"; let context: TestContainerContext = null!; @@ -37,7 +37,7 @@ test.afterAll("Delete Test Database", async () => { test("Query results", async () => { // Run the query and verify the results await queryEditor.locator.click(); - const executeQueryButton = explorer.commandBarButton("Execute Query"); + const executeQueryButton = explorer.commandBarButton(CommandBarButton.ExecuteQuery); await executeQueryButton.click(); await expect(queryTab.resultsEditor.locator).toBeAttached({ timeout: 60 * 1000 }); @@ -59,7 +59,7 @@ test("Query results", async () => { test("Query stats", async () => { // Run the query and verify the results await queryEditor.locator.click(); - const executeQueryButton = explorer.commandBarButton("Execute Query"); + const executeQueryButton = explorer.commandBarButton(CommandBarButton.ExecuteQuery); await executeQueryButton.click(); 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"); // Run the query and verify the results - const executeQueryButton = explorer.commandBarButton("Execute Query"); + const executeQueryButton = explorer.commandBarButton(CommandBarButton.ExecuteQuery); await executeQueryButton.click(); await expect(queryTab.errorList).toBeAttached({ timeout: 60 * 1000 }); diff --git a/test/sql/scaleAndSettings/scale.spec.ts b/test/sql/scaleAndSettings/scale.spec.ts new file mode 100644 index 000000000..e3035a7dc --- /dev/null +++ b/test/sql/scaleAndSettings/scale.spec.ts @@ -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 => { + 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, + }, + ); + }; +}); diff --git a/test/sql/scaleAndSettings/settings.spec.ts b/test/sql/scaleAndSettings/settings.spec.ts new file mode 100644 index 000000000..463cdacb7 --- /dev/null +++ b/test/sql/scaleAndSettings/settings.spec.ts @@ -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, + }); + }); +});