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..31e078d04 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/Menus/NotificationConsole/NotificationConsoleComponent.tsx b/src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.tsx index b96000cbb..35d6ed0d5 100644 --- a/src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.tsx +++ b/src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.tsx @@ -127,7 +127,7 @@ export class NotificationConsoleComponent extends React.Component< - + {this.state.headerStatus} @@ -208,6 +208,7 @@ export class NotificationConsoleComponent extends React.Component< {item.date} {item.message} + {console.log(item.message)} )); diff --git a/test/README.md b/test/README.md index 40c10dba1..06c695120 100644 --- a/test/README.md +++ b/test/README.md @@ -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: ```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: ```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: diff --git a/test/fx.ts b/test/fx.ts index e7458695c..b0d8b6a88 100644 --- a/test/fx.ts +++ b/test/fx.ts @@ -1,6 +1,7 @@ import { DefaultAzureCredential } from "@azure/identity"; import { Frame, Locator, Page, expect } from "@playwright/test"; import crypto from "crypto"; +import { TestContainerContext } from "./testData"; const RETRY_COUNT = 3; @@ -54,6 +55,9 @@ export const defaultAccounts: Record = { 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_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) { @@ -318,6 +322,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) {} @@ -347,8 +356,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 { @@ -444,6 +453,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..6a488b79e --- /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 + 1000).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 + 1000).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"); + 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..1c0c3876a --- /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, + }); + }); +});