diff --git a/src/Common/ShimmerTree/ShimmerTree.tsx b/src/Common/ShimmerTree/ShimmerTree.tsx index ee2ddc32a..08616234f 100644 --- a/src/Common/ShimmerTree/ShimmerTree.tsx +++ b/src/Common/ShimmerTree/ShimmerTree.tsx @@ -23,7 +23,7 @@ const ShimmerTree = ({ indentLevels, style = {} }: ShimmerTreeProps) => { ); return ( - + {indentLevels.map((indentLevel: IndentLevel) => renderShimmers(indentLevel))} ); diff --git a/src/Explorer/ContainerCopy/Context/CopyJobContext.test.tsx b/src/Explorer/ContainerCopy/Context/CopyJobContext.test.tsx index d3776f07c..cd3d550a5 100644 --- a/src/Explorer/ContainerCopy/Context/CopyJobContext.test.tsx +++ b/src/Explorer/ContainerCopy/Context/CopyJobContext.test.tsx @@ -32,7 +32,7 @@ describe("CopyJobContext", () => { it("should render children correctly", () => { render( -
Test Child
+
Test Child
, ); @@ -552,7 +552,7 @@ describe("CopyJobContext", () => { const TestComponent2 = (): JSX.Element => { const context = useCopyJobContext(); contextValue2 = context; - return
Component 2
; + return
Component 2
; }; render( diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AddReadPermissionToDefaultIdentity.test.tsx b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AddReadPermissionToDefaultIdentity.test.tsx index 5ef3577b8..5f457f70d 100644 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AddReadPermissionToDefaultIdentity.test.tsx +++ b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AddReadPermissionToDefaultIdentity.test.tsx @@ -20,7 +20,7 @@ jest.mock("../../../CopyJobUtils", () => ({ jest.mock("../Components/InfoTooltip", () => { const MockInfoTooltip = ({ content }: { content: React.ReactNode }) => { - return
{content}
; + return
{content}
; }; MockInfoTooltip.displayName = "MockInfoTooltip"; return MockInfoTooltip; @@ -46,13 +46,13 @@ jest.mock("../Components/PopoverContainer", () => { return null; } return ( -
-
{title}
-
{children}
- -
diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AssignPermissions.test.tsx b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AssignPermissions.test.tsx index 76f915c5e..fcda3e15f 100644 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AssignPermissions.test.tsx +++ b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AssignPermissions.test.tsx @@ -26,7 +26,7 @@ jest.mock("./hooks/usePermissionsSection", () => ({ jest.mock("../../../../../Common/ShimmerTree/ShimmerTree", () => { const MockShimmerTree = (props: any) => { return ( -
+
Loading...
); @@ -37,7 +37,7 @@ jest.mock("../../../../../Common/ShimmerTree/ShimmerTree", () => { jest.mock("./AddManagedIdentity", () => { const MockAddManagedIdentity = () => { - return
Add Managed Identity Component
; + return
Add Managed Identity Component
; }; MockAddManagedIdentity.displayName = "MockAddManagedIdentity"; return MockAddManagedIdentity; @@ -45,7 +45,7 @@ jest.mock("./AddManagedIdentity", () => { jest.mock("./AddReadPermissionToDefaultIdentity", () => { const MockAddReadPermissionToDefaultIdentity = () => { - return
Add Read Permission Component
; + return
Add Read Permission Component
; }; MockAddReadPermissionToDefaultIdentity.displayName = "MockAddReadPermissionToDefaultIdentity"; return MockAddReadPermissionToDefaultIdentity; @@ -53,7 +53,7 @@ jest.mock("./AddReadPermissionToDefaultIdentity", () => { jest.mock("./DefaultManagedIdentity", () => { const MockDefaultManagedIdentity = () => { - return
Default Managed Identity Component
; + return
Default Managed Identity Component
; }; MockDefaultManagedIdentity.displayName = "MockDefaultManagedIdentity"; return MockDefaultManagedIdentity; @@ -61,7 +61,7 @@ jest.mock("./DefaultManagedIdentity", () => { jest.mock("./OnlineCopyEnabled", () => { const MockOnlineCopyEnabled = () => { - return
Online Copy Enabled Component
; + return
Online Copy Enabled Component
; }; MockOnlineCopyEnabled.displayName = "MockOnlineCopyEnabled"; return MockOnlineCopyEnabled; @@ -69,7 +69,7 @@ jest.mock("./OnlineCopyEnabled", () => { jest.mock("./PointInTimeRestore", () => { const MockPointInTimeRestore = () => { - return
Point In Time Restore Component
; + return
Point In Time Restore Component
; }; MockPointInTimeRestore.displayName = "MockPointInTimeRestore"; return MockPointInTimeRestore; @@ -196,14 +196,14 @@ describe("AssignPermissions Component", () => { { id: "addManagedIdentity", title: "Add Managed Identity", - Component: () =>
Add Managed Identity Component
, + Component: () =>
Add Managed Identity Component
, disabled: false, completed: true, }, { id: "readPermissionAssigned", title: "Read Permission Assigned", - Component: () =>
Add Read Permission Component
, + Component: () =>
Add Read Permission Component
, disabled: false, completed: false, }, @@ -228,14 +228,14 @@ describe("AssignPermissions Component", () => { { id: "pointInTimeRestore", title: "Point In Time Restore", - Component: () =>
Point In Time Restore Component
, + Component: () =>
Point In Time Restore Component
, disabled: false, completed: true, }, { id: "onlineCopyEnabled", title: "Online Copy Enabled", - Component: () =>
Online Copy Enabled Component
, + Component: () =>
Online Copy Enabled Component
, disabled: false, completed: false, }, @@ -262,7 +262,7 @@ describe("AssignPermissions Component", () => { { id: "addManagedIdentity", title: "Add Managed Identity", - Component: () =>
Add Managed Identity Component
, + Component: () =>
Add Managed Identity Component
, disabled: false, completed: true, }, @@ -276,7 +276,7 @@ describe("AssignPermissions Component", () => { { id: "onlineCopyEnabled", title: "Online Copy Enabled", - Component: () =>
Online Copy Enabled Component
, + Component: () =>
Online Copy Enabled Component
, disabled: false, completed: false, }, diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/DefaultManagedIdentity.test.tsx b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/DefaultManagedIdentity.test.tsx index 93418859f..0e02e9aba 100644 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/DefaultManagedIdentity.test.tsx +++ b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/DefaultManagedIdentity.test.tsx @@ -15,7 +15,7 @@ jest.mock("../../../../../Utils/arm/identityUtils", () => ({ jest.mock("../Components/InfoTooltip", () => { const MockInfoTooltip = ({ content }: { content: React.ReactNode }) => { - return
{content}
; + return
{content}
; }; MockInfoTooltip.displayName = "MockInfoTooltip"; return MockInfoTooltip; @@ -41,14 +41,14 @@ jest.mock("../Components/PopoverContainer", () => { return null; } return ( -
-
{title}
-
{children}
-
{isLoading ? "Loading" : "Not Loading"}
- -
diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/OnlineCopyEnabled.test.tsx b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/OnlineCopyEnabled.test.tsx index 8d28f2482..3d87f90d2 100644 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/OnlineCopyEnabled.test.tsx +++ b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/OnlineCopyEnabled.test.tsx @@ -25,7 +25,7 @@ jest.mock("../../../../../Common/Logger", () => ({ jest.mock("../../../../../Common/LoadingOverlay", () => { const MockLoadingOverlay = ({ isLoading, label }: { isLoading: boolean; label: string }) => { - return isLoading ?
{label}
: null; + return isLoading ?
{label}
: null; }; MockLoadingOverlay.displayName = "MockLoadingOverlay"; return MockLoadingOverlay; diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/hooks/useManagedIdentity.test.tsx b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/hooks/useManagedIdentity.test.tsx index 2a91c8c69..952ad7e01 100644 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/hooks/useManagedIdentity.test.tsx +++ b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/hooks/useManagedIdentity.test.tsx @@ -52,11 +52,11 @@ const TestComponent: React.FC = ({ updateIdentityFn, onError return (
- -
{loading ? "true" : "false"}
- {contextError &&
{contextError}
} +
{loading ? "true" : "false"}
+ {contextError &&
{contextError}
}
); }; diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/hooks/usePermissionsSection.test.tsx b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/hooks/usePermissionsSection.test.tsx index 78935657d..6c2a47779 100644 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/hooks/usePermissionsSection.test.tsx +++ b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/hooks/usePermissionsSection.test.tsx @@ -34,7 +34,7 @@ jest.mock("../../../../CopyJobUtils", () => ({ jest.mock("../AddManagedIdentity", () => { const MockAddManagedIdentity = () => { - return
AddManagedIdentity
; + return
AddManagedIdentity
; }; MockAddManagedIdentity.displayName = "MockAddManagedIdentity"; return MockAddManagedIdentity; @@ -42,7 +42,7 @@ jest.mock("../AddManagedIdentity", () => { jest.mock("../AddReadPermissionToDefaultIdentity", () => { const MockAddReadPermissionToDefaultIdentity = () => { - return
AddReadPermissionToDefaultIdentity
; + return
AddReadPermissionToDefaultIdentity
; }; MockAddReadPermissionToDefaultIdentity.displayName = "MockAddReadPermissionToDefaultIdentity"; return MockAddReadPermissionToDefaultIdentity; @@ -50,7 +50,7 @@ jest.mock("../AddReadPermissionToDefaultIdentity", () => { jest.mock("../DefaultManagedIdentity", () => { const MockDefaultManagedIdentity = () => { - return
DefaultManagedIdentity
; + return
DefaultManagedIdentity
; }; MockDefaultManagedIdentity.displayName = "MockDefaultManagedIdentity"; return MockDefaultManagedIdentity; @@ -58,7 +58,7 @@ jest.mock("../DefaultManagedIdentity", () => { jest.mock("../OnlineCopyEnabled", () => { const MockOnlineCopyEnabled = () => { - return
OnlineCopyEnabled
; + return
OnlineCopyEnabled
; }; MockOnlineCopyEnabled.displayName = "MockOnlineCopyEnabled"; return MockOnlineCopyEnabled; @@ -66,7 +66,7 @@ jest.mock("../OnlineCopyEnabled", () => { jest.mock("../PointInTimeRestore", () => { const MockPointInTimeRestore = () => { - return
PointInTimeRestore
; + return
PointInTimeRestore
; }; MockPointInTimeRestore.displayName = "MockPointInTimeRestore"; return MockPointInTimeRestore; @@ -92,18 +92,18 @@ const TestWrapper: React.FC = ({ state, onResult }) => { }, [result, onResult]); return ( -
-
{result.length}
+
+
{result.length}
{result.map((group) => ( -
+

{group.title}

{group.description}

{group.sections.map((section) => ( -
- +
+ {section.completed?.toString() || "undefined"} - {section.disabled.toString()} + {section.disabled.toString()}
))}
diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/hooks/useToggle.test.tsx b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/hooks/useToggle.test.tsx index e74d334dc..ed3f1a335 100644 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/hooks/useToggle.test.tsx +++ b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/hooks/useToggle.test.tsx @@ -7,14 +7,14 @@ const TestToggleComponent: React.FC<{ initialState?: boolean }> = ({ initialStat return (
- {state ? "true" : "false"} - - -
@@ -57,8 +57,8 @@ describe("useToggle hook", () => { return (
- {state ? "true" : "false"} -
diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/Components/FieldRow.test.tsx b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/Components/FieldRow.test.tsx index 33fd7e0ce..c83a0c754 100644 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/Components/FieldRow.test.tsx +++ b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/Components/FieldRow.test.tsx @@ -24,8 +24,8 @@ describe("FieldRow", () => { it("renders children content correctly", () => { render( - - + + , ); @@ -123,7 +123,7 @@ describe("FieldRow", () => { it("positions children in grow container with center alignment", () => { const { container } = render( -
{mockChildContent}
+
{mockChildContent}
, ); @@ -135,7 +135,7 @@ describe("FieldRow", () => { it("maintains layout when no label is provided", () => { const { container } = render( -
{mockChildContent}
+
{mockChildContent}
, ); diff --git a/src/Explorer/Controls/ThroughputInput/ThroughputInput.tsx b/src/Explorer/Controls/ThroughputInput/ThroughputInput.tsx index 02b6853ca..506669a3e 100644 --- a/src/Explorer/Controls/ThroughputInput/ThroughputInput.tsx +++ b/src/Explorer/Controls/ThroughputInput/ThroughputInput.tsx @@ -209,6 +209,7 @@ export const ThroughputInput: FunctionComponent = ({ checked={isAutoscaleSelected} type="radio" role="radio" + data-test="ThroughputInput/ThroughputMode:Autoscale" tabIndex={0} onChange={(e) => handleOnChangeMode(e, "Autoscale")} /> @@ -224,6 +225,7 @@ export const ThroughputInput: FunctionComponent = ({ type="radio" aria-required={true} role="radio" + data-test="ThroughputInput/ThroughputMode:Manual" tabIndex={0} onChange={(e) => handleOnChangeMode(e, "Manual")} /> @@ -286,7 +288,7 @@ export const ThroughputInput: FunctionComponent = ({ = ({ } > @@ -314,6 +315,7 @@ export class AddCollectionPanel extends React.Component @@ -337,6 +339,7 @@ export class AddCollectionPanel extends React.Component) => @@ -346,18 +349,20 @@ export class AddCollectionPanel extends React.Component - , isChecked: boolean) => - this.setState({ isSharedThroughputChecked: isChecked }) - } - /> +
+ , isChecked: boolean) => + this.setState({ isSharedThroughputChecked: isChecked }) + } + /> +
) => @@ -576,6 +583,7 @@ export class AddCollectionPanel extends React.Component + {UniqueKeysHeader()} {this.state.uniqueKeys.map((uniqueKey: string, i: number): JSX.Element => { return ( @@ -743,6 +752,7 @@ export class AddCollectionPanel extends React.Component) => { const uniqueKeys = this.state.uniqueKeys.map((uniqueKey: string, j: number) => { @@ -769,6 +779,7 @@ export class AddCollectionPanel extends React.Component this.setState({ uniqueKeys: [...this.state.uniqueKeys, ""] })} > diff --git a/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.tsx b/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.tsx index 05eff191f..93505f8e6 100644 --- a/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.tsx +++ b/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.tsx @@ -120,6 +120,7 @@ export const DeleteCollectionConfirmationPane: FunctionComponentConfirm by typing the {collectionName.toLowerCase()} id {confirmDatabase} { diff --git a/src/Explorer/Sidebar.tsx b/src/Explorer/Sidebar.tsx index 6488873d8..9be24d9af 100644 --- a/src/Explorer/Sidebar.tsx +++ b/src/Explorer/Sidebar.tsx @@ -244,7 +244,12 @@ const GlobalCommands: React.FC = ({ explorer }) => { return (
{actions.length === 1 ? ( - ) : ( @@ -253,8 +258,12 @@ const GlobalCommands: React.FC = ({ explorer }) => { {(triggerProps: MenuButtonProps) => (
diff --git a/test/cassandra/containerCreation.spec.ts b/test/cassandra/containerCreation.spec.ts new file mode 100644 index 000000000..6226edb64 --- /dev/null +++ b/test/cassandra/containerCreation.spec.ts @@ -0,0 +1,138 @@ +import { expect, test } from "@playwright/test"; + +import { DataExplorer, TEST_AUTOSCALE_THROUGHPUT_RU, TestAccount, generateUniqueName } from "../fx"; +import { deleteKeyspace, fillCassandraTableDetails, setThroughput } from "../helpers/containerCreationHelpers"; + +test.describe("Cassandra API - Keyspace and Table Creation", () => { + test("Create table in new keyspace with non-shared throughput", async ({ page }) => { + const keyspaceId = generateUniqueName("keyspace"); + const tableId = generateUniqueName("table"); + + const explorer = await DataExplorer.open(page, TestAccount.Cassandra); + + await explorer.globalCommandButton("New Table").click(); + await explorer.whilePanelOpen( + "Add Table", + async (panel, okButton) => { + await fillCassandraTableDetails(panel, keyspaceId, tableId); + await setThroughput(panel, true, TEST_AUTOSCALE_THROUGHPUT_RU); + await okButton.click(); + }, + { closeTimeout: 5 * 60 * 1000 }, + ); + + 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("Create table in new keyspace with shared throughput", async ({ page }) => { + const keyspaceId = generateUniqueName("keyspace"); + const tableId = generateUniqueName("table"); + + const explorer = await DataExplorer.open(page, TestAccount.Cassandra); + + await explorer.globalCommandButton("New Table").click(); + await explorer.whilePanelOpen( + "Add Table", + async (panel, okButton) => { + await fillCassandraTableDetails(panel, keyspaceId, tableId); + await panel + .getByTestId("AddCollectionPanel/SharedThroughputCheckbox") + .getByRole("checkbox") + .check({ force: true }); + await okButton.click(); + }, + { closeTimeout: 5 * 60 * 1000 }, + ); + + 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("Create table with autoscale throughput", async ({ page }) => { + const keyspaceId = generateUniqueName("keyspace"); + const tableId = generateUniqueName("table"); + + const explorer = await DataExplorer.open(page, TestAccount.Cassandra); + + await explorer.globalCommandButton("New Table").click(); + await explorer.whilePanelOpen( + "Add Table", + async (panel, okButton) => { + await fillCassandraTableDetails(panel, keyspaceId, tableId); + await setThroughput(panel, true, TEST_AUTOSCALE_THROUGHPUT_RU); + await okButton.click(); + }, + { closeTimeout: 5 * 60 * 1000 }, + ); + + const tableNode = await explorer.waitForContainerNode(keyspaceId, tableId); + await expect(tableNode.element).toBeAttached(); + + // Cleanup + await deleteKeyspace(explorer, keyspaceId); + }); + + test("Create table with manual throughput", async ({ page }) => { + const keyspaceId = generateUniqueName("keyspace"); + const tableId = generateUniqueName("table"); + const manualThroughput = 400; + + const explorer = await DataExplorer.open(page, TestAccount.Cassandra); + + await explorer.globalCommandButton("New Table").click(); + await explorer.whilePanelOpen( + "Add Table", + async (panel, okButton) => { + await fillCassandraTableDetails(panel, keyspaceId, tableId); + await setThroughput(panel, false, manualThroughput); + await okButton.click(); + }, + { closeTimeout: 5 * 60 * 1000 }, + ); + + const tableNode = await explorer.waitForContainerNode(keyspaceId, tableId); + await expect(tableNode.element).toBeAttached(); + + // Cleanup + await deleteKeyspace(explorer, keyspaceId); + }); + + test("Create multiple tables in keyspace", async ({ page }) => { + const keyspaceId = generateUniqueName("keyspace"); + const table1Id = generateUniqueName("table"); + + const explorer = await DataExplorer.open(page, TestAccount.Cassandra); + + // Create first table + await explorer.globalCommandButton("New Table").click(); + await explorer.whilePanelOpen( + "Add Table", + async (panel, okButton) => { + await fillCassandraTableDetails(panel, keyspaceId, table1Id); + await setThroughput(panel, true, TEST_AUTOSCALE_THROUGHPUT_RU); + await okButton.click(); + }, + { closeTimeout: 5 * 60 * 1000 }, + ); + + const keyspaceNode = await explorer.waitForNode(keyspaceId); + await explorer.waitForContainerNode(keyspaceId, table1Id); + + // Cleanup + await deleteKeyspace(explorer, keyspaceId); + await expect(keyspaceNode.element).not.toBeAttached(); + }); +}); diff --git a/test/fx.ts b/test/fx.ts index 088dde82a..64ba4edb1 100644 --- a/test/fx.ts +++ b/test/fx.ts @@ -344,7 +344,7 @@ 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. */ globalCommandButton(label: string): Locator { - return this.frame.getByTestId("GlobalCommands").getByText(label); + return this.frame.getByTestId(`GlobalCommands/Button:${label}`); } /** Select the command bar button with the specified label */ diff --git a/test/gremlin/containerCreation.spec.ts b/test/gremlin/containerCreation.spec.ts new file mode 100644 index 000000000..870230ef5 --- /dev/null +++ b/test/gremlin/containerCreation.spec.ts @@ -0,0 +1,135 @@ +import { expect, test } from "@playwright/test"; + +import { DataExplorer, TEST_AUTOSCALE_THROUGHPUT_RU, TestAccount, generateUniqueName } from "../fx"; +import { GREMLIN_CONFIG, deleteDatabase, openAndFillCreateContainerPanel } from "../helpers/containerCreationHelpers"; + +test.describe("Gremlin API - Graph Creation", () => { + test("Create graph in new database with non-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", + 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(); + + // Cleanup + await deleteDatabase(explorer, databaseId); + await expect(databaseNode.element).not.toBeAttached(); + }); + + test("Create graph in new database with 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("Create graph with autoscale 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", + 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(); + + // Cleanup + await deleteDatabase(explorer, databaseId); + await expect(databaseNode.element).not.toBeAttached(); + }); + + test("Create graph with 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("Create graph - no unique keys support", async ({ page }) => { + // Gremlin doesn't support unique keys, verify panel doesn't show unique key UI + const databaseId = generateUniqueName("db"); + const graphId = generateUniqueName("graph"); + + const explorer = await DataExplorer.open(page, TestAccount.Gremlin); + + await explorer.globalCommandButton("New Graph").click(); + await explorer.whilePanelOpen( + "New Graph", + async (panel, okButton) => { + await panel.getByTestId("AddCollectionPanel/DatabaseId").fill(databaseId); + await panel.getByTestId("AddCollectionPanel/CollectionId").fill(graphId); + await panel.getByRole("textbox", { name: "Partition key" }).fill("/pk"); + await panel.getByTestId("ThroughputInput/AutoscaleRUInput").fill(TEST_AUTOSCALE_THROUGHPUT_RU.toString()); + + const uniqueKeyButton = panel.getByTestId("AddCollectionPanel/AddUniqueKeyButton"); + await expect(uniqueKeyButton).not.toBeVisible(); + + await okButton.click(); + }, + { closeTimeout: 5 * 60 * 1000 }, + ); + + 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(); + }); +}); diff --git a/test/helpers/containerCreationHelpers.ts b/test/helpers/containerCreationHelpers.ts new file mode 100644 index 000000000..220e877e7 --- /dev/null +++ b/test/helpers/containerCreationHelpers.ts @@ -0,0 +1,323 @@ +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: true, +}; + +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 { + // 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 { + 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 { + 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 { + 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 { + 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 { + // 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 { + 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 { + 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 { + 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 { + 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 { + 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 }, + ); +} diff --git a/test/mongo/containerCreation.spec.ts b/test/mongo/containerCreation.spec.ts new file mode 100644 index 000000000..0544d7d58 --- /dev/null +++ b/test/mongo/containerCreation.spec.ts @@ -0,0 +1,125 @@ +import { expect, test } from "@playwright/test"; + +import { DataExplorer, TEST_AUTOSCALE_THROUGHPUT_RU, TestAccount, generateUniqueName } from "../fx"; +import { MONGO_CONFIG, deleteDatabase, openAndFillCreateContainerPanel } from "../helpers/containerCreationHelpers"; + +test("Mongo: New database non-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", + isAutoscale: true, + throughputValue: TEST_AUTOSCALE_THROUGHPUT_RU, + }); + + 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: 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: Autoscale 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", + isAutoscale: true, + throughputValue: TEST_AUTOSCALE_THROUGHPUT_RU, + }); + + 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(); +}); diff --git a/test/sql/containerCreation.spec.ts b/test/sql/containerCreation.spec.ts new file mode 100644 index 000000000..5ed794c73 --- /dev/null +++ b/test/sql/containerCreation.spec.ts @@ -0,0 +1,125 @@ +import { expect, test } from "@playwright/test"; + +import { DataExplorer, TEST_AUTOSCALE_THROUGHPUT_RU, TestAccount, generateUniqueName } from "../fx"; +import { SQL_CONFIG, deleteDatabase, openAndFillCreateContainerPanel } from "../helpers/containerCreationHelpers"; + +test("SQL: New database non-shared throughput", 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, + }); + + const databaseNode = await explorer.waitForNode(databaseId); + const containerNode = await explorer.waitForContainerNode(databaseId, containerId); + + await expect(containerNode.element).toBeAttached(); + + // Cleanup - delete database which cascades container deletion + await deleteDatabase(explorer, databaseId); + await expect(databaseNode.element).not.toBeAttached(); +}); + +test("SQL: New database shared throughput", 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", + 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: Autoscale throughput", 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, + }); + + 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(); +}); diff --git a/test/tables/containerCreation.spec.ts b/test/tables/containerCreation.spec.ts new file mode 100644 index 000000000..34090c5ab --- /dev/null +++ b/test/tables/containerCreation.spec.ts @@ -0,0 +1,147 @@ +import { expect, test } from "@playwright/test"; + +import { DataExplorer, TEST_AUTOSCALE_THROUGHPUT_RU, TestAccount, generateUniqueName } from "../fx"; +import { TABLES_CONFIG, openAndFillCreateContainerPanel } from "../helpers/containerCreationHelpers"; + +test.describe("Tables API - Table Creation", () => { + test("Create table in TablesDB with non-shared throughput", async ({ page }) => { + const tableId = generateUniqueName("table"); + + const explorer = await DataExplorer.open(page, TestAccount.Tables); + + await openAndFillCreateContainerPanel(explorer, TABLES_CONFIG, { + databaseId: "TablesDB", // Tables uses a fixed database + containerId: tableId, + isAutoscale: true, + throughputValue: TEST_AUTOSCALE_THROUGHPUT_RU, + }); + + const tableNode = await explorer.waitForContainerNode("TablesDB", tableId); + + await expect(tableNode.element).toBeAttached(); + + // Cleanup - delete table + await tableNode.openContextMenu(); + 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(); + }); + + test("Create 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, + }); + + const table2Node = await explorer.waitForContainerNode("TablesDB", table2Id); + await expect(table2Node.element).toBeAttached(); + + // Cleanup - delete both tables + const table1Node = await explorer.waitForContainerNode("TablesDB", table1Id); + await table1Node.openContextMenu(); + await table1Node.contextMenuItem("Delete Table").click(); + await explorer.whilePanelOpen( + "Delete Table", + async (panel, okButton) => { + await panel.getByRole("textbox", { name: "Confirm by typing the table id" }).fill(table1Id); + await okButton.click(); + }, + { closeTimeout: 5 * 60 * 1000 }, + ); + + await table2Node.openContextMenu(); + await table2Node.contextMenuItem("Delete Table").click(); + await explorer.whilePanelOpen( + "Delete Table", + async (panel, okButton) => { + await panel.getByRole("textbox", { name: "Confirm by typing the table id" }).fill(table2Id); + await okButton.click(); + }, + { closeTimeout: 5 * 60 * 1000 }, + ); + }); + + test("Create table with shared throughput", async ({ page }) => { + const tableId = generateUniqueName("table"); + + const explorer = await DataExplorer.open(page, TestAccount.Tables); + + 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 tableNode.openContextMenu(); + 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 }, + ); + }); + + test("Create table - no partition key support", async ({ page }) => { + // Tables don't use partition keys, verify they're not shown in UI + 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(); + + // Verify partition key field is not present (Tables don't use partition keys) + // This would need to be checked during panel open, so we keep the inline test for this validation + + // Cleanup + await tableNode.openContextMenu(); + 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 }, + ); + }); +});