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>
This commit is contained in:
asier-isayas
2025-12-10 14:02:31 -05:00
committed by GitHub
parent 5b7d1a74af
commit d67c1a0464
13 changed files with 315 additions and 17 deletions

View File

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

View File

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

View File

@@ -503,7 +503,9 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
<span style={{ float: "left", transform: "translateX(-50%)" }}>
{this.props.instantMaximumThroughput.toLocaleString()}
</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>
<ProgressIndicator
@@ -626,11 +628,12 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
min={autoPilotThroughput1K}
onGetErrorMessage={(value: string) => {
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 <span data-test="autopilot-throughput-input-error">{errorMessage}</span>;
}}
validateOnLoad={false}
data-test="autopilot-throughput-input"
/>
</Stack>
</Stack>
@@ -650,7 +653,10 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
}
onChange={this.onThroughputChange}
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>
<StyledTextFieldBase
data-test="autopilot-throughput-input"
disabled={true}
id="autopilotInput"
key="auto pilot throughput input"
@@ -333,6 +334,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
5,000
</span>
<span
data-test="soft-allowed-maximum-throughput"
style={
{
"float": "right",
@@ -752,11 +754,13 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
}
>
<StyledTextFieldBase
data-test="manual-throughput-input"
disabled={false}
id="throughputInput"
key="provisioned throughput input"
min={10000}
onChange={[Function]}
onGetErrorMessage={[Function]}
required={true}
step={100}
styles={
@@ -811,6 +815,7 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
5,000
</span>
<span
data-test="soft-allowed-maximum-throughput"
style={
{
"float": "right",
@@ -1206,11 +1211,13 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
}
>
<StyledTextFieldBase
data-test="manual-throughput-input"
disabled={false}
id="throughputInput"
key="provisioned throughput input"
min={10000}
onChange={[Function]}
onGetErrorMessage={[Function]}
required={true}
step={100}
styles={
@@ -1265,6 +1272,7 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
5,000
</span>
<span
data-test="soft-allowed-maximum-throughput"
style={
{
"float": "right",

View File

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

View File

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

View File

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

View File

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

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:
```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:

View File

@@ -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;
@@ -55,6 +56,9 @@ export const defaultAccounts: Record<TestAccount, string> = {
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<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 */
static async waitForExplorer(page: Page) {
const iframeElement = await page.getByTestId("DataExplorerFrame").elementHandle();

View File

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

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