+
File upload status
{
+ let context: TestContainerContext = null!;
+ const uploadDocumentFilePath: string = path.join(__dirname, "uploadDocument.json");
+
+ test.beforeEach("Create Test Database and Open documents tab", async ({ page }) => {
+ context = await createTestSQLContainer();
+ explorer = await DataExplorer.open(page, TestAccount.SQL);
+
+ const containerNode = await explorer.waitForContainerNode(context.database.id, context.container.id);
+ await containerNode.expand();
+
+ const containerMenuNode = await explorer.waitForContainerItemsNode(context.database.id, context.container.id);
+ await containerMenuNode.element.click();
+ });
+
+ test.afterEach("Delete Test Database and Upload Document Temp Directory", async () => {
+ if (existsSync(uploadDocumentFilePath)) {
+ // Delete the temp directory after test
+ unlinkSync(uploadDocumentFilePath);
+ }
+ await context?.dispose();
+ });
+
+ test("upload document", async ({}, testInfo) => {
+ // Create file to upload
+ const TestDataJsonString: string = JSON.stringify(TestData, null, 2);
+ writeFileSync(uploadDocumentFilePath, TestDataJsonString);
+
+ const uploadItemCommandBar = explorer.commandBarButton(CommandBarButton.UploadItem);
+ await uploadItemCommandBar.click();
+
+ // Select file to upload
+ await explorer.frame.setInputFiles("#importFileInput", uploadDocumentFilePath);
+
+ const uploadButton = explorer.frame.getByTestId("Panel/OkButton");
+ await uploadButton.click();
+
+ // Verify upload success message
+ const fileUploadStatusExpected: string = `${partitionCount * itemsPerPartition} created, 0 throttled, 0 errors`;
+ const fileUploadStatus = explorer.frame.getByTestId("file-upload-status");
+ await expect(fileUploadStatus).toContainText(fileUploadStatusExpected, {
+ timeout: ONE_MINUTE_MS,
+ });
+ });
+
+ test("upload same document twice", async ({}, testInfo) => {
+ // Create file to upload
+ const TestDataJsonString: string = JSON.stringify(TestData, null, 2);
+ writeFileSync(uploadDocumentFilePath, TestDataJsonString);
+
+ const uploadItemCommandBar = explorer.commandBarButton(CommandBarButton.UploadItem);
+ await uploadItemCommandBar.click();
+
+ // Select file to upload
+ await explorer.frame.setInputFiles("#importFileInput", uploadDocumentFilePath);
+
+ const uploadButton = explorer.frame.getByTestId("Panel/OkButton");
+ await uploadButton.click();
+
+ // Verify upload success message
+ const fileUploadStatusExpected: string = `${partitionCount * itemsPerPartition} created, 0 throttled, 0 errors`;
+ const fileUploadStatus = explorer.frame.getByTestId("file-upload-status");
+ await expect(fileUploadStatus).toContainText(fileUploadStatusExpected, {
+ timeout: ONE_MINUTE_MS,
+ });
+
+ // Select file to upload again
+ await explorer.frame.setInputFiles("#importFileInput", uploadDocumentFilePath);
+ await uploadButton.click();
+
+ // Verify upload failure message
+ const errorIcon = explorer.frame.getByRole("img", { name: "error" });
+ await expect(errorIcon).toBeVisible({ timeout: ONE_MINUTE_MS });
+ await expect(fileUploadStatus).toContainText(
+ `0 created, 0 throttled, ${partitionCount * itemsPerPartition} errors`,
+ {
+ timeout: ONE_MINUTE_MS,
+ },
+ );
+ });
+
+ test("upload invalid json", async ({}, testInfo) => {
+ // Create file to upload
+ let TestDataJsonString: string = JSON.stringify(TestData, null, 2);
+ // Remove the first '[' so that it becomes invalid json
+ TestDataJsonString = TestDataJsonString.substring(1);
+ writeFileSync(uploadDocumentFilePath, TestDataJsonString);
+
+ const uploadItemCommandBar = explorer.commandBarButton(CommandBarButton.UploadItem);
+ await uploadItemCommandBar.click();
+
+ // Select file to upload
+ await explorer.frame.setInputFiles("#importFileInput", uploadDocumentFilePath);
+
+ const uploadButton = explorer.frame.getByTestId("Panel/OkButton");
+ await uploadButton.click();
+
+ // Verify upload failure message
+ const fileUploadStatusExpected: string = "Unexpected non-whitespace character after JSON";
+ const fileUploadErrorList = explorer.frame.getByLabel("error list");
+ await expect(fileUploadErrorList).toContainText(fileUploadStatusExpected, {
+ timeout: ONE_MINUTE_MS,
+ });
+ });
+});
diff --git a/test/sql/scaleAndSettings/computedProperties.spec.ts b/test/sql/scaleAndSettings/computedProperties.spec.ts
new file mode 100644
index 000000000..eaeede054
--- /dev/null
+++ b/test/sql/scaleAndSettings/computedProperties.spec.ts
@@ -0,0 +1,103 @@
+import { expect, test } from "@playwright/test";
+import * as DataModels from "../../../src/Contracts/DataModels";
+import { CommandBarButton, DataExplorer, ONE_MINUTE_MS, TestAccount } from "../../fx";
+import { createTestSQLContainer, TestContainerContext } from "../../testData";
+
+test.describe("Computed Properties", () => {
+ 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 Settings tab
+ await explorer.openScaleAndSettings(context);
+ const computedPropertiesTab = explorer.frame.getByTestId("settings-tab-header/ComputedPropertiesTab");
+ await computedPropertiesTab.click();
+ });
+
+ test.afterAll("Delete Test Database", async () => {
+ await context?.dispose();
+ });
+
+ test("Add valid computed property", async ({ page }) => {
+ await clearComputedPropertiesTextBoxContent({ page });
+
+ // Create computed property
+ const computedProperties: DataModels.ComputedProperties = [
+ {
+ name: "cp_lowerName",
+ query: "SELECT VALUE LOWER(c.name) FROM c",
+ },
+ ];
+ const computedPropertiesString: string = JSON.stringify(computedProperties);
+ await page.keyboard.type(computedPropertiesString);
+
+ // Save changes
+ const saveButton = explorer.commandBarButton(CommandBarButton.Save);
+ await expect(saveButton).toBeEnabled();
+ await saveButton.click();
+ await expect(explorer.getConsoleMessage()).toContainText(`Successfully updated container ${context.container.id}`, {
+ timeout: ONE_MINUTE_MS,
+ });
+ });
+
+ test("Add computed property with invalid query", async ({ page }) => {
+ await clearComputedPropertiesTextBoxContent({ page });
+
+ // Create computed property with no VALUE keyword in query
+ const computedProperties: DataModels.ComputedProperties = [
+ {
+ name: "cp_lowerName",
+ query: "SELECT LOWER(c.name) FROM c",
+ },
+ ];
+ const computedPropertiesString: string = JSON.stringify(computedProperties);
+ await page.keyboard.type(computedPropertiesString);
+
+ // Save changes
+ const saveButton = explorer.commandBarButton(CommandBarButton.Save);
+ await expect(saveButton).toBeEnabled();
+ await saveButton.click();
+ await expect(explorer.getConsoleMessage()).toContainText(`Failed to update container ${context.container.id}`, {
+ timeout: ONE_MINUTE_MS,
+ });
+ });
+
+ test("Add computed property with invalid json", async ({ page }) => {
+ await clearComputedPropertiesTextBoxContent({ page });
+
+ // Create computed property with no VALUE keyword in query
+ const computedProperties: DataModels.ComputedProperties = [
+ {
+ name: "cp_lowerName",
+ query: "SELECT LOWER(c.name) FROM c",
+ },
+ ];
+ const computedPropertiesString: string = JSON.stringify(computedProperties);
+ await page.keyboard.type(computedPropertiesString + "]");
+
+ // Save button should remain disabled due to invalid json
+ const saveButton = explorer.commandBarButton(CommandBarButton.Save);
+ await expect(saveButton).toBeDisabled();
+ });
+
+ const clearComputedPropertiesTextBoxContent = async ({ page }): Promise => {
+ // Get computed properties text box
+ const computedPropertiesTextBox = explorer.frame.getByRole("textbox", { name: "Computed properties" });
+ await computedPropertiesTextBox.waitFor();
+ const computedPropertiesEditor = explorer.frame.getByTestId("computed-properties-editor");
+ await computedPropertiesEditor.click();
+
+ // Clear existing content
+ const isMac: boolean = process.platform === "darwin";
+ await page.keyboard.press(isMac ? "Meta+A" : "Control+A");
+ await page.keyboard.press("Backspace");
+ };
+});
diff --git a/test/sql/scaleAndSettings/settings.spec.ts b/test/sql/scaleAndSettings/settings.spec.ts
index 463cdacb7..3b00d31c2 100644
--- a/test/sql/scaleAndSettings/settings.spec.ts
+++ b/test/sql/scaleAndSettings/settings.spec.ts
@@ -15,7 +15,7 @@ test.describe("Settings under Scale & Settings", () => {
const containerNode = await explorer.waitForContainerNode(context.database.id, context.container.id);
await containerNode.expand();
- // Click Scale & Settings and open Scale tab
+ // Click Scale & Settings and open Settings tab
await explorer.openScaleAndSettings(context);
const settingsTab = explorer.frame.getByTestId("settings-tab-header/SubSettingsTab");
await settingsTab.click();
@@ -25,46 +25,86 @@ test.describe("Settings under Scale & Settings", () => {
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();
+ test.describe("Set TTL", () => {
+ 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,
+ 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,
+ },
+ );
});
});
- test("Update TTL to On (with user entry)", async () => {
- const ttlOnRadioButton = explorer.frame.getByRole("radio", { name: "ttl-on-option" });
- await ttlOnRadioButton.click();
+ test.describe("Set Geospatial Config", () => {
+ test("Set Geospatial Config to Geometry then Geography", async () => {
+ const geometryRadioButton = explorer.frame.getByRole("radio", { name: "geometry-option" });
+ await geometryRadioButton.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,
+ },
+ );
- await explorer.commandBarButton(CommandBarButton.Save).click();
- await expect(explorer.getConsoleMessage()).toContainText(`Successfully updated container ${context.container.id}`, {
- timeout: ONE_MINUTE_MS,
- });
- });
+ const geographyRadioButton = explorer.frame.getByRole("radio", { name: "geography-option" });
+ await geographyRadioButton.click();
- 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,
+ await explorer.commandBarButton(CommandBarButton.Save).click();
+ await expect(explorer.getConsoleMessage()).toContainText(
+ `Successfully updated container ${context.container.id}`,
+ {
+ timeout: ONE_MINUTE_MS,
+ },
+ );
});
});
});
diff --git a/test/testData.ts b/test/testData.ts
index 94d38941f..352b5cd94 100644
--- a/test/testData.ts
+++ b/test/testData.ts
@@ -37,27 +37,35 @@ export interface PartitionKey {
value: string | null;
}
-const partitionCount = 4;
+export const partitionCount = 4;
// If we increase this number, we need to split bulk creates into multiple batches.
// Bulk operations are limited to 100 items per partition.
-const itemsPerPartition = 100;
+export const itemsPerPartition = 100;
function createTestItems(): TestItem[] {
const items: TestItem[] = [];
for (let i = 0; i < partitionCount; i++) {
for (let j = 0; j < itemsPerPartition; j++) {
- const id = crypto.randomBytes(32).toString("base64");
+ const id = createSafeRandomString(32);
items.push({
id,
partitionKey: `partition_${i}`,
- randomData: crypto.randomBytes(32).toString("base64"),
+ randomData: createSafeRandomString(32),
});
}
}
return items;
}
+// Document IDs cannot contain '/', '\', or '#'
+function createSafeRandomString(byteLength: number): string {
+ return crypto
+ .randomBytes(byteLength)
+ .toString("base64")
+ .replace(/[\/\\#]/g, "_");
+}
+
export const TestData: TestItem[] = createTestItems();
export class TestContainerContext {