diff --git a/.vs/CopilotSnapshots/548E56D5117B4249B654F2DE5EA27C25/049D74CB6EC3174D9ACE5C016AC60AE5/39D77E4DA1804736E39178139E5048A6 b/.vs/CopilotSnapshots/548E56D5117B4249B654F2DE5EA27C25/049D74CB6EC3174D9ACE5C016AC60AE5/39D77E4DA1804736E39178139E5048A6 deleted file mode 100644 index 906282863..000000000 --- a/.vs/CopilotSnapshots/548E56D5117B4249B654F2DE5EA27C25/049D74CB6EC3174D9ACE5C016AC60AE5/39D77E4DA1804736E39178139E5048A6 +++ /dev/null @@ -1,229 +0,0 @@ -import { Locator, expect, test } from "@playwright/test"; -import { - CommandBarButton, - DataExplorer, - ONE_MINUTE_MS, - TEST_AUTOSCALE_MAX_THROUGHPUT_RU_4K, - TEST_MANUAL_THROUGHPUT_RU, - TestAccount, -} from "../../fx"; -import { TestDatabaseContext, createTestDB } from "../../testData"; - -test.describe("Database with Shared Throughput", () => { - let dbContext: TestDatabaseContext = null!; - let explorer: DataExplorer = null!; - const containerId = "sharedcontainer"; - - // Helper methods - const getThroughputInput = (type: "manual" | "autopilot"): Locator => { - return explorer.frame.getByTestId(`${type}-throughput-input`); - }; - - test.afterEach("Delete Test Database", async () => { - await dbContext?.dispose(); - }); - - test.describe("Manual Throughput Tests", () => { - test.beforeEach(async ({ page }) => { - explorer = await DataExplorer.open(page, TestAccount.SQL); - }); - - test("Create database with shared manual throughput and verify Scale node in UI", async () => { - test.setTimeout(120000); // 2 minutes timeout - // Create database with shared manual throughput (400 RU/s) - dbContext = await createTestDB({ throughput: 400 }); - - // Verify database node appears in the tree - const databaseNode = await explorer.waitForNode(dbContext.database.id); - expect(databaseNode).toBeDefined(); - - // Expand the database node to see child nodes - await databaseNode.expand(); - - // Verify that "Scale" node appears under the database - const scaleNode = await explorer.waitForNode(`${dbContext.database.id}/Scale`); - expect(scaleNode).toBeDefined(); - await expect(scaleNode.element).toBeVisible(); - }); - - test("Add container to shared database without dedicated throughput", async () => { - // Create database with shared manual throughput - dbContext = await createTestDB({ throughput: 400 }); - - // Wait for the database to appear in the tree - await explorer.waitForNode(dbContext.database.id); - - // Add a container to the shared database via UI - const newContainerButton = await explorer.globalCommandButton("New Container"); - await newContainerButton.click(); - - await explorer.whilePanelOpen( - "New Container", - async (panel, okButton) => { - // Select "Use existing" database - const useExistingRadio = panel.getByRole("radio", { name: /Use existing/i }); - await useExistingRadio.click(); - - // Select the database from dropdown using the new data-testid - const databaseDropdown = panel.getByRole("combobox", { name: "Choose an existing database" }); - await databaseDropdown.click(); - - await explorer.frame.getByRole("option", { name: dbContext.database.id }).click(); - // Now you can target the specific database option by its data-testid - //await panel.getByTestId(`database-option-${dbContext.database.id}`).click(); - // Fill container id - await panel.getByRole("textbox", { name: "Container id, Example Container1" }).fill(containerId); - - // Fill partition key - await panel.getByRole("textbox", { name: "Partition key" }).fill("/pk"); - - // Ensure "Provision dedicated throughput" is NOT checked - const dedicatedThroughputCheckbox = panel.getByRole("checkbox", { - name: /Provision dedicated throughput for this container/i, - }); - - if (await dedicatedThroughputCheckbox.isVisible()) { - const isChecked = await dedicatedThroughputCheckbox.isChecked(); - if (isChecked) { - await dedicatedThroughputCheckbox.uncheck(); - } - } - - await okButton.click(); - }, - { closeTimeout: 5 * ONE_MINUTE_MS }, - ); - - // Verify container was created under the database - const containerNode = await explorer.waitForContainerNode(dbContext.database.id, containerId); - expect(containerNode).toBeDefined(); - }); - - test("Scale shared database manual throughput", async () => { - // Create database with shared manual throughput (400 RU/s) - dbContext = await createTestDB({ throughput: 400 }); - - // Navigate to the scale settings by clicking the "Scale" node in the tree - const databaseNode = await explorer.waitForNode(dbContext.database.id); - await databaseNode.expand(); - const scaleNode = await explorer.waitForNode(`${dbContext.database.id}/Scale`); - await scaleNode.element.click(); - - // Update manual throughput from 400 to 800 - await getThroughputInput("manual").fill(TEST_MANUAL_THROUGHPUT_RU.toString()); - - // Save changes - await explorer.commandBarButton(CommandBarButton.Save).click(); - - // Verify success message - await expect(explorer.getConsoleHeaderStatus()).toContainText( - `Successfully updated offer for database ${dbContext.database.id}`, - { - timeout: 2 * ONE_MINUTE_MS, - }, - ); - }); - - test("Scale shared database from manual to autoscale", async () => { - // Create database with shared manual throughput (400 RU/s) - dbContext = await createTestDB({ throughput: 400 }); - - // Open database settings by clicking the "Scale" node - const databaseNode = await explorer.waitForNode(dbContext.database.id); - await databaseNode.expand(); - const scaleNode = await explorer.waitForNode(`${dbContext.database.id}/Scale`); - await scaleNode.element.click(); - - // Switch to Autoscale - const autoscaleRadio = explorer.frame.getByText("Autoscale", { exact: true }); - await autoscaleRadio.click(); - - // Set autoscale max throughput to 1000 - //await getThroughputInput("autopilot").fill(TEST_AUTOSCALE_THROUGHPUT_RU.toString()); - - // Save changes - await explorer.commandBarButton(CommandBarButton.Save).click(); - - await expect(explorer.getConsoleHeaderStatus()).toContainText( - `Successfully updated offer for database ${dbContext.database.id}`, - { - timeout: 2 * ONE_MINUTE_MS, - }, - ); - }); - }); - - test.describe("Autoscale Throughput Tests", () => { - test.beforeEach(async ({ page }) => { - explorer = await DataExplorer.open(page, TestAccount.SQL); - }); - - test("Create database with shared autoscale throughput and verify Scale node in UI", async () => { - test.setTimeout(120000); // 2 minutes timeout - - // Create database with shared autoscale throughput (max 1000 RU/s) - dbContext = await createTestDB({ maxThroughput: 1000 }); - - // Verify database node appears - const databaseNode = await explorer.waitForNode(dbContext.database.id); - expect(databaseNode).toBeDefined(); - - // Expand the database node to see child nodes - await databaseNode.expand(); - - // Verify that "Scale" node appears under the database - const scaleNode = await explorer.waitForNode(`${dbContext.database.id}/Scale`); - expect(scaleNode).toBeDefined(); - await expect(scaleNode.element).toBeVisible(); - }); - - test("Scale shared database autoscale throughput", async () => { - // Create database with shared autoscale throughput (max 1000 RU/s) - dbContext = await createTestDB({ maxThroughput: 1000 }); - - // Open database settings - const databaseNode = await explorer.waitForNode(dbContext.database.id); - await databaseNode.expand(); - const scaleNode = await explorer.waitForNode(`${dbContext.database.id}/Scale`); - await scaleNode.element.click(); - - // Update autoscale max throughput from 1000 to 4000 - await getThroughputInput("autopilot").fill(TEST_AUTOSCALE_MAX_THROUGHPUT_RU_4K.toString()); - - // Save changes - await explorer.commandBarButton(CommandBarButton.Save).click(); - - // Verify success message - await expect(explorer.getConsoleHeaderStatus()).toContainText( - `Successfully updated offer for database ${dbContext.database.id}`, - { - timeout: 2 * ONE_MINUTE_MS, - }, - ); - }); - - test("Scale shared database from autoscale to manual", async () => { - // Create database with shared autoscale throughput (max 1000 RU/s) - dbContext = await createTestDB({ maxThroughput: 1000 }); - - // Open database settings - const databaseNode = await explorer.waitForNode(dbContext.database.id); - await databaseNode.expand(); - const scaleNode = await explorer.waitForNode(`${dbContext.database.id}/Scale`); - await scaleNode.element.click(); - - // Switch to Manual - const manualRadio = explorer.frame.getByText("Manual", { exact: true }); - await manualRadio.click(); - - // Save changes - await explorer.commandBarButton(CommandBarButton.Save).click(); - - // Verify success message - await expect(explorer.getConsoleHeaderStatus()).toContainText( - `Successfully updated offer for database ${dbContext.database.id}`, - { timeout: 2 * ONE_MINUTE_MS }, - ); - }); - }); -}); diff --git a/.vs/CopilotSnapshots/548E56D5117B4249B654F2DE5EA27C25/049D74CB6EC3174D9ACE5C016AC60AE5/76CE5452071DA53B49DA7250DF70857E b/.vs/CopilotSnapshots/548E56D5117B4249B654F2DE5EA27C25/049D74CB6EC3174D9ACE5C016AC60AE5/76CE5452071DA53B49DA7250DF70857E deleted file mode 100644 index 7110f2c25..000000000 --- a/.vs/CopilotSnapshots/548E56D5117B4249B654F2DE5EA27C25/049D74CB6EC3174D9ACE5C016AC60AE5/76CE5452071DA53B49DA7250DF70857E +++ /dev/null @@ -1,239 +0,0 @@ -import { Locator, expect, test } from "@playwright/test"; -import { - CommandBarButton, - DataExplorer, - ONE_MINUTE_MS, - TEST_AUTOSCALE_MAX_THROUGHPUT_RU_4K, - TEST_MANUAL_THROUGHPUT_RU, - TestAccount, -} from "../../fx"; -import { TestDatabaseContext, createTestDB } from "../../testData"; - -test.describe("Database with Shared Throughput", () => { - let dbContext: TestDatabaseContext = null!; - let explorer: DataExplorer = null!; - const containerId = "sharedcontainer"; - - // Helper methods - const getThroughputInput = (type: "manual" | "autopilot"): Locator => { - return explorer.frame.getByTestId(`${type}-throughput-input`); - }; - - test.afterEach("Delete Test Database", async () => { - try { - await dbContext?.dispose(); - } catch (error) { - // Ignore cleanup errors if browser/page was already closed due to timeout - console.warn("Cleanup error (possibly due to test timeout):", error); - } - }); - - test.describe("Manual Throughput Tests", () => { - test.beforeEach(async ({ page }) => { - explorer = await DataExplorer.open(page, TestAccount.SQL); - }); - - test("Create database with shared manual throughput and verify Scale node in UI", async () => { - test.setTimeout(120000); // 2 minutes timeout - // Create database with shared manual throughput (400 RU/s) - dbContext = await createTestDB({ throughput: 400 }); - - // Verify database node appears in the tree - const databaseNode = await explorer.waitForNode(dbContext.database.id); - expect(databaseNode).toBeDefined(); - - // Expand the database node to see child nodes - await databaseNode.expand(); - - // Verify that "Scale" node appears under the database - const scaleNode = await explorer.waitForNode(`${dbContext.database.id}/Scale`); - expect(scaleNode).toBeDefined(); - await expect(scaleNode.element).toBeVisible(); - }); - - test("Add container to shared database without dedicated throughput", async () => { - test.setTimeout(5 * ONE_MINUTE_MS); // 5 minutes timeout - // Create database with shared manual throughput - dbContext = await createTestDB({ throughput: 400 }); - - // Wait for the database to appear in the tree - await explorer.waitForNode(dbContext.database.id); - - // Add a container to the shared database via UI - const newContainerButton = await explorer.globalCommandButton("New Container"); - await newContainerButton.click(); - - await explorer.whilePanelOpen( - "New Container", - async (panel, okButton) => { - // Select "Use existing" database - const useExistingRadio = panel.getByRole("radio", { name: /Use existing/i }); - await useExistingRadio.click(); - - // Select the database from dropdown using the new data-testid - const databaseDropdown = panel.getByRole("combobox", { name: "Choose an existing database" }); - await databaseDropdown.click(); - - await explorer.frame.getByRole("option", { name: dbContext.database.id }).click(); - // Now you can target the specific database option by its data-testid - //await panel.getByTestId(`database-option-${dbContext.database.id}`).click(); - // Fill container id - await panel.getByRole("textbox", { name: "Container id, Example Container1" }).fill(containerId); - - // Fill partition key - await panel.getByRole("textbox", { name: "Partition key" }).fill("/pk"); - - // Ensure "Provision dedicated throughput" is NOT checked - const dedicatedThroughputCheckbox = panel.getByRole("checkbox", { - name: /Provision dedicated throughput for this container/i, - }); - - if (await dedicatedThroughputCheckbox.isVisible()) { - const isChecked = await dedicatedThroughputCheckbox.isChecked(); - if (isChecked) { - await dedicatedThroughputCheckbox.uncheck(); - } - } - - await okButton.click(); - }, - { closeTimeout: 5 * ONE_MINUTE_MS }, - ); - - // Verify container was created under the database - const containerNode = await explorer.waitForContainerNode(dbContext.database.id, containerId); - expect(containerNode).toBeDefined(); - }); - - test("Scale shared database manual throughput", async () => { - test.setTimeout(3 * ONE_MINUTE_MS); // 3 minutes timeout - // Create database with shared manual throughput (400 RU/s) - dbContext = await createTestDB({ throughput: 400 }); - - // Navigate to the scale settings by clicking the "Scale" node in the tree - const databaseNode = await explorer.waitForNode(dbContext.database.id); - await databaseNode.expand(); - const scaleNode = await explorer.waitForNode(`${dbContext.database.id}/Scale`); - await scaleNode.element.click(); - - // Update manual throughput from 400 to 800 - await getThroughputInput("manual").fill(TEST_MANUAL_THROUGHPUT_RU.toString()); - - // Save changes - await explorer.commandBarButton(CommandBarButton.Save).click(); - - // Verify success message - await expect(explorer.getConsoleHeaderStatus()).toContainText( - `Successfully updated offer for database ${dbContext.database.id}`, - { - timeout: 2 * ONE_MINUTE_MS, - }, - ); - }); - - test("Scale shared database from manual to autoscale", async () => { - test.setTimeout(3 * ONE_MINUTE_MS); // 3 minutes timeout - // Create database with shared manual throughput (400 RU/s) - dbContext = await createTestDB({ throughput: 400 }); - - // Open database settings by clicking the "Scale" node - const databaseNode = await explorer.waitForNode(dbContext.database.id); - await databaseNode.expand(); - const scaleNode = await explorer.waitForNode(`${dbContext.database.id}/Scale`); - await scaleNode.element.click(); - - // Switch to Autoscale - const autoscaleRadio = explorer.frame.getByText("Autoscale", { exact: true }); - await autoscaleRadio.click(); - - // Set autoscale max throughput to 1000 - //await getThroughputInput("autopilot").fill(TEST_AUTOSCALE_THROUGHPUT_RU.toString()); - - // Save changes - await explorer.commandBarButton(CommandBarButton.Save).click(); - - await expect(explorer.getConsoleHeaderStatus()).toContainText( - `Successfully updated offer for database ${dbContext.database.id}`, - { - timeout: 2 * ONE_MINUTE_MS, - }, - ); - }); - }); - - test.describe("Autoscale Throughput Tests", () => { - test.beforeEach(async ({ page }) => { - explorer = await DataExplorer.open(page, TestAccount.SQL); - }); - - test("Create database with shared autoscale throughput and verify Scale node in UI", async () => { - test.setTimeout(120000); // 2 minutes timeout - - // Create database with shared autoscale throughput (max 1000 RU/s) - dbContext = await createTestDB({ maxThroughput: 1000 }); - - // Verify database node appears - const databaseNode = await explorer.waitForNode(dbContext.database.id); - expect(databaseNode).toBeDefined(); - - // Expand the database node to see child nodes - await databaseNode.expand(); - - // Verify that "Scale" node appears under the database - const scaleNode = await explorer.waitForNode(`${dbContext.database.id}/Scale`); - expect(scaleNode).toBeDefined(); - await expect(scaleNode.element).toBeVisible(); - }); - - test("Scale shared database autoscale throughput", async () => { - test.setTimeout(3 * ONE_MINUTE_MS); // 3 minutes timeout - // Create database with shared autoscale throughput (max 1000 RU/s) - dbContext = await createTestDB({ maxThroughput: 1000 }); - - // Open database settings - const databaseNode = await explorer.waitForNode(dbContext.database.id); - await databaseNode.expand(); - const scaleNode = await explorer.waitForNode(`${dbContext.database.id}/Scale`); - await scaleNode.element.click(); - - // Update autoscale max throughput from 1000 to 4000 - await getThroughputInput("autopilot").fill(TEST_AUTOSCALE_MAX_THROUGHPUT_RU_4K.toString()); - - // Save changes - await explorer.commandBarButton(CommandBarButton.Save).click(); - - // Verify success message - await expect(explorer.getConsoleHeaderStatus()).toContainText( - `Successfully updated offer for database ${dbContext.database.id}`, - { - timeout: 2 * ONE_MINUTE_MS, - }, - ); - }); - - test("Scale shared database from autoscale to manual", async () => { - test.setTimeout(3 * ONE_MINUTE_MS); // 3 minutes timeout - // Create database with shared autoscale throughput (max 1000 RU/s) - dbContext = await createTestDB({ maxThroughput: 1000 }); - - // Open database settings - const databaseNode = await explorer.waitForNode(dbContext.database.id); - await databaseNode.expand(); - const scaleNode = await explorer.waitForNode(`${dbContext.database.id}/Scale`); - await scaleNode.element.click(); - - // Switch to Manual - const manualRadio = explorer.frame.getByText("Manual", { exact: true }); - await manualRadio.click(); - - // Save changes - await explorer.commandBarButton(CommandBarButton.Save).click(); - - // Verify success message - await expect(explorer.getConsoleHeaderStatus()).toContainText( - `Successfully updated offer for database ${dbContext.database.id}`, - { timeout: 2 * ONE_MINUTE_MS }, - ); - }); - }); -}); diff --git a/.vs/CopilotSnapshots/548E56D5117B4249B654F2DE5EA27C25/049D74CB6EC3174D9ACE5C016AC60AE5/A536882AB0DBA450A09BC67D815A0A27 b/.vs/CopilotSnapshots/548E56D5117B4249B654F2DE5EA27C25/049D74CB6EC3174D9ACE5C016AC60AE5/A536882AB0DBA450A09BC67D815A0A27 deleted file mode 100644 index 906282863..000000000 --- a/.vs/CopilotSnapshots/548E56D5117B4249B654F2DE5EA27C25/049D74CB6EC3174D9ACE5C016AC60AE5/A536882AB0DBA450A09BC67D815A0A27 +++ /dev/null @@ -1,229 +0,0 @@ -import { Locator, expect, test } from "@playwright/test"; -import { - CommandBarButton, - DataExplorer, - ONE_MINUTE_MS, - TEST_AUTOSCALE_MAX_THROUGHPUT_RU_4K, - TEST_MANUAL_THROUGHPUT_RU, - TestAccount, -} from "../../fx"; -import { TestDatabaseContext, createTestDB } from "../../testData"; - -test.describe("Database with Shared Throughput", () => { - let dbContext: TestDatabaseContext = null!; - let explorer: DataExplorer = null!; - const containerId = "sharedcontainer"; - - // Helper methods - const getThroughputInput = (type: "manual" | "autopilot"): Locator => { - return explorer.frame.getByTestId(`${type}-throughput-input`); - }; - - test.afterEach("Delete Test Database", async () => { - await dbContext?.dispose(); - }); - - test.describe("Manual Throughput Tests", () => { - test.beforeEach(async ({ page }) => { - explorer = await DataExplorer.open(page, TestAccount.SQL); - }); - - test("Create database with shared manual throughput and verify Scale node in UI", async () => { - test.setTimeout(120000); // 2 minutes timeout - // Create database with shared manual throughput (400 RU/s) - dbContext = await createTestDB({ throughput: 400 }); - - // Verify database node appears in the tree - const databaseNode = await explorer.waitForNode(dbContext.database.id); - expect(databaseNode).toBeDefined(); - - // Expand the database node to see child nodes - await databaseNode.expand(); - - // Verify that "Scale" node appears under the database - const scaleNode = await explorer.waitForNode(`${dbContext.database.id}/Scale`); - expect(scaleNode).toBeDefined(); - await expect(scaleNode.element).toBeVisible(); - }); - - test("Add container to shared database without dedicated throughput", async () => { - // Create database with shared manual throughput - dbContext = await createTestDB({ throughput: 400 }); - - // Wait for the database to appear in the tree - await explorer.waitForNode(dbContext.database.id); - - // Add a container to the shared database via UI - const newContainerButton = await explorer.globalCommandButton("New Container"); - await newContainerButton.click(); - - await explorer.whilePanelOpen( - "New Container", - async (panel, okButton) => { - // Select "Use existing" database - const useExistingRadio = panel.getByRole("radio", { name: /Use existing/i }); - await useExistingRadio.click(); - - // Select the database from dropdown using the new data-testid - const databaseDropdown = panel.getByRole("combobox", { name: "Choose an existing database" }); - await databaseDropdown.click(); - - await explorer.frame.getByRole("option", { name: dbContext.database.id }).click(); - // Now you can target the specific database option by its data-testid - //await panel.getByTestId(`database-option-${dbContext.database.id}`).click(); - // Fill container id - await panel.getByRole("textbox", { name: "Container id, Example Container1" }).fill(containerId); - - // Fill partition key - await panel.getByRole("textbox", { name: "Partition key" }).fill("/pk"); - - // Ensure "Provision dedicated throughput" is NOT checked - const dedicatedThroughputCheckbox = panel.getByRole("checkbox", { - name: /Provision dedicated throughput for this container/i, - }); - - if (await dedicatedThroughputCheckbox.isVisible()) { - const isChecked = await dedicatedThroughputCheckbox.isChecked(); - if (isChecked) { - await dedicatedThroughputCheckbox.uncheck(); - } - } - - await okButton.click(); - }, - { closeTimeout: 5 * ONE_MINUTE_MS }, - ); - - // Verify container was created under the database - const containerNode = await explorer.waitForContainerNode(dbContext.database.id, containerId); - expect(containerNode).toBeDefined(); - }); - - test("Scale shared database manual throughput", async () => { - // Create database with shared manual throughput (400 RU/s) - dbContext = await createTestDB({ throughput: 400 }); - - // Navigate to the scale settings by clicking the "Scale" node in the tree - const databaseNode = await explorer.waitForNode(dbContext.database.id); - await databaseNode.expand(); - const scaleNode = await explorer.waitForNode(`${dbContext.database.id}/Scale`); - await scaleNode.element.click(); - - // Update manual throughput from 400 to 800 - await getThroughputInput("manual").fill(TEST_MANUAL_THROUGHPUT_RU.toString()); - - // Save changes - await explorer.commandBarButton(CommandBarButton.Save).click(); - - // Verify success message - await expect(explorer.getConsoleHeaderStatus()).toContainText( - `Successfully updated offer for database ${dbContext.database.id}`, - { - timeout: 2 * ONE_MINUTE_MS, - }, - ); - }); - - test("Scale shared database from manual to autoscale", async () => { - // Create database with shared manual throughput (400 RU/s) - dbContext = await createTestDB({ throughput: 400 }); - - // Open database settings by clicking the "Scale" node - const databaseNode = await explorer.waitForNode(dbContext.database.id); - await databaseNode.expand(); - const scaleNode = await explorer.waitForNode(`${dbContext.database.id}/Scale`); - await scaleNode.element.click(); - - // Switch to Autoscale - const autoscaleRadio = explorer.frame.getByText("Autoscale", { exact: true }); - await autoscaleRadio.click(); - - // Set autoscale max throughput to 1000 - //await getThroughputInput("autopilot").fill(TEST_AUTOSCALE_THROUGHPUT_RU.toString()); - - // Save changes - await explorer.commandBarButton(CommandBarButton.Save).click(); - - await expect(explorer.getConsoleHeaderStatus()).toContainText( - `Successfully updated offer for database ${dbContext.database.id}`, - { - timeout: 2 * ONE_MINUTE_MS, - }, - ); - }); - }); - - test.describe("Autoscale Throughput Tests", () => { - test.beforeEach(async ({ page }) => { - explorer = await DataExplorer.open(page, TestAccount.SQL); - }); - - test("Create database with shared autoscale throughput and verify Scale node in UI", async () => { - test.setTimeout(120000); // 2 minutes timeout - - // Create database with shared autoscale throughput (max 1000 RU/s) - dbContext = await createTestDB({ maxThroughput: 1000 }); - - // Verify database node appears - const databaseNode = await explorer.waitForNode(dbContext.database.id); - expect(databaseNode).toBeDefined(); - - // Expand the database node to see child nodes - await databaseNode.expand(); - - // Verify that "Scale" node appears under the database - const scaleNode = await explorer.waitForNode(`${dbContext.database.id}/Scale`); - expect(scaleNode).toBeDefined(); - await expect(scaleNode.element).toBeVisible(); - }); - - test("Scale shared database autoscale throughput", async () => { - // Create database with shared autoscale throughput (max 1000 RU/s) - dbContext = await createTestDB({ maxThroughput: 1000 }); - - // Open database settings - const databaseNode = await explorer.waitForNode(dbContext.database.id); - await databaseNode.expand(); - const scaleNode = await explorer.waitForNode(`${dbContext.database.id}/Scale`); - await scaleNode.element.click(); - - // Update autoscale max throughput from 1000 to 4000 - await getThroughputInput("autopilot").fill(TEST_AUTOSCALE_MAX_THROUGHPUT_RU_4K.toString()); - - // Save changes - await explorer.commandBarButton(CommandBarButton.Save).click(); - - // Verify success message - await expect(explorer.getConsoleHeaderStatus()).toContainText( - `Successfully updated offer for database ${dbContext.database.id}`, - { - timeout: 2 * ONE_MINUTE_MS, - }, - ); - }); - - test("Scale shared database from autoscale to manual", async () => { - // Create database with shared autoscale throughput (max 1000 RU/s) - dbContext = await createTestDB({ maxThroughput: 1000 }); - - // Open database settings - const databaseNode = await explorer.waitForNode(dbContext.database.id); - await databaseNode.expand(); - const scaleNode = await explorer.waitForNode(`${dbContext.database.id}/Scale`); - await scaleNode.element.click(); - - // Switch to Manual - const manualRadio = explorer.frame.getByText("Manual", { exact: true }); - await manualRadio.click(); - - // Save changes - await explorer.commandBarButton(CommandBarButton.Save).click(); - - // Verify success message - await expect(explorer.getConsoleHeaderStatus()).toContainText( - `Successfully updated offer for database ${dbContext.database.id}`, - { timeout: 2 * ONE_MINUTE_MS }, - ); - }); - }); -}); diff --git a/.vs/CopilotSnapshots/548E56D5117B4249B654F2DE5EA27C25/049D74CB6EC3174D9ACE5C016AC60AE5/BDC0B1042C11B58A5259E50EBB63DC01 b/.vs/CopilotSnapshots/548E56D5117B4249B654F2DE5EA27C25/049D74CB6EC3174D9ACE5C016AC60AE5/BDC0B1042C11B58A5259E50EBB63DC01 deleted file mode 100644 index e9d5e2325..000000000 --- a/.vs/CopilotSnapshots/548E56D5117B4249B654F2DE5EA27C25/049D74CB6EC3174D9ACE5C016AC60AE5/BDC0B1042C11B58A5259E50EBB63DC01 +++ /dev/null @@ -1,702 +0,0 @@ -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; - -export interface TestNameOptions { - length?: number; - timestampped?: boolean; - prefixed?: boolean; -} - -export function generateUniqueName(baseName: string, options?: TestNameOptions): string { - const length = options?.length ?? 1; - const timestamp = options?.timestampped === undefined ? true : options.timestampped; - const prefixed = options?.prefixed === undefined ? true : options.prefixed; - - const prefix = prefixed ? "t_" : ""; - const suffix = timestamp ? `_${Date.now()}` : ""; - return `${prefix}${baseName}${crypto.randomBytes(length).toString("hex")}${suffix}`; -} - -export function getAzureCLICredentials(): DefaultAzureCredential { - return new DefaultAzureCredential(); -} - -export async function getAzureCLICredentialsToken(): Promise { - const credentials = getAzureCLICredentials(); - const token = (await credentials.getToken("https://management.core.windows.net//.default"))?.token || ""; - return token; -} - -export enum TestAccount { - Tables = "Tables", - Cassandra = "Cassandra", - Gremlin = "Gremlin", - Mongo = "Mongo", - MongoReadonly = "MongoReadOnly", - Mongo32 = "Mongo32", - SQL = "SQL", - SQLReadOnly = "SQLReadOnly", - SQLContainerCopyOnly = "SQLContainerCopyOnly", -} - -export const defaultAccounts: Record = { - [TestAccount.Tables]: "github-e2etests-tables", - [TestAccount.Cassandra]: "github-e2etests-cassandra", - [TestAccount.Gremlin]: "github-e2etests-gremlin", - [TestAccount.Mongo]: "github-e2etests-mongo", - [TestAccount.MongoReadonly]: "github-e2etests-mongo-readonly", - [TestAccount.Mongo32]: "github-e2etests-mongo32", - [TestAccount.SQL]: "github-e2etests-sql", - [TestAccount.SQLReadOnly]: "github-e2etests-sql-readonly", - [TestAccount.SQLContainerCopyOnly]: "github-e2etests-sql-containercopyonly", -}; - -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_MANUAL_THROUGHPUT_RU = 800; -export const TEST_AUTOSCALE_MAX_THROUGHPUT_RU_2K = 2000; -export const TEST_AUTOSCALE_MAX_THROUGHPUT_RU_4K = 4000; -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) { - const actualPrefix = process.env.DE_TEST_ACCOUNT_PREFIX.endsWith("-") - ? process.env.DE_TEST_ACCOUNT_PREFIX - : `${process.env.DE_TEST_ACCOUNT_PREFIX}-`; - return `${actualPrefix}${accountType.toLocaleLowerCase()}`; - } -} - -export function getAccountName(accountType: TestAccount) { - return ( - process.env[`DE_TEST_ACCOUNT_NAME_${accountType.toLocaleUpperCase()}`] ?? - tryGetStandardName(accountType) ?? - defaultAccounts[accountType] - ); -} - -type TestExplorerUrlOptions = { - iframeSrc?: string; - enablecontainercopy?: boolean; -}; - -export async function getTestExplorerUrl(accountType: TestAccount, options?: TestExplorerUrlOptions): Promise { - const { iframeSrc, enablecontainercopy } = options ?? {}; - - // We can't retrieve AZ CLI credentials from the browser so we get them here. - const token = await getAzureCLICredentialsToken(); - const accountName = getAccountName(accountType); - const params = new URLSearchParams(); - params.set("accountName", accountName); - params.set("resourceGroup", resourceGroupName); - params.set("subscriptionId", subscriptionId); - params.set("token", token); - - // There seem to be occasional CORS issues with calling the copilot APIs (/api/tokens/sampledataconnection/v2, for example) - // For now, since we don't test copilot, we can disable the copilot APIs by setting the feature flag to false. - params.set("feature.enableCopilot", "false"); - - const nosqlRbacToken = process.env.NOSQL_TESTACCOUNT_TOKEN; - const nosqlReadOnlyRbacToken = process.env.NOSQL_READONLY_TESTACCOUNT_TOKEN; - const nosqlContainerCopyRbacToken = process.env.NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN; - const tableRbacToken = process.env.TABLE_TESTACCOUNT_TOKEN; - const gremlinRbacToken = process.env.GREMLIN_TESTACCOUNT_TOKEN; - const cassandraRbacToken = process.env.CASSANDRA_TESTACCOUNT_TOKEN; - const mongoRbacToken = process.env.MONGO_TESTACCOUNT_TOKEN; - const mongo32RbacToken = process.env.MONGO32_TESTACCOUNT_TOKEN; - const mongoReadOnlyRbacToken = process.env.MONGO_READONLY_TESTACCOUNT_TOKEN; - - switch (accountType) { - case TestAccount.SQL: - if (nosqlRbacToken) { - params.set("nosqlRbacToken", nosqlRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.SQLContainerCopyOnly: - if (nosqlContainerCopyRbacToken) { - params.set("nosqlRbacToken", nosqlContainerCopyRbacToken); - params.set("enableaaddataplane", "true"); - } - if (enablecontainercopy) { - params.set("enablecontainercopy", "true"); - } - break; - - case TestAccount.SQLReadOnly: - if (nosqlReadOnlyRbacToken) { - params.set("nosqlReadOnlyRbacToken", nosqlReadOnlyRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.Tables: - if (tableRbacToken) { - params.set("tableRbacToken", tableRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.Gremlin: - if (gremlinRbacToken) { - params.set("gremlinRbacToken", gremlinRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.Cassandra: - if (cassandraRbacToken) { - params.set("cassandraRbacToken", cassandraRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.Mongo: - if (mongoRbacToken) { - params.set("mongoRbacToken", mongoRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.Mongo32: - if (mongo32RbacToken) { - params.set("mongo32RbacToken", mongo32RbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.MongoReadonly: - if (mongoReadOnlyRbacToken) { - params.set("mongoReadOnlyRbacToken", mongoReadOnlyRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - } - - if (iframeSrc) { - params.set("iframeSrc", iframeSrc); - } - - return `https://localhost:1234/testExplorer.html?${params.toString()}`; -} - -type DropdownItemExpectations = { - ariaLabel?: string; - itemCount?: number; -}; - -type DropdownItemMatcher = { - name?: string; - position?: number; -}; - -export async function getDropdownItemByNameOrPosition( - frame: Frame, - matcher?: DropdownItemMatcher, - expectedOptions?: DropdownItemExpectations, -): Promise { - const dropdownItemsWrapper = frame.locator("div.ms-Dropdown-items"); - if (expectedOptions?.ariaLabel) { - expect(await dropdownItemsWrapper.getAttribute("aria-label")).toEqual(expectedOptions.ariaLabel); - } - if (expectedOptions?.itemCount) { - const items = dropdownItemsWrapper.locator("button.ms-Dropdown-item[role='option']"); - await expect(items).toHaveCount(expectedOptions.itemCount); - } - const containerDropdownItems = dropdownItemsWrapper.locator("button.ms-Dropdown-item[role='option']"); - if (matcher?.name) { - return containerDropdownItems.filter({ hasText: matcher.name }); - } else if (matcher?.position !== undefined) { - return containerDropdownItems.nth(matcher.position); - } - // Return first item if no matcher is provided - return containerDropdownItems.first(); -} - -/** Helper class that provides locator methods for TreeNode elements, on top of a Locator */ -class TreeNode { - constructor( - public element: Locator, - public frame: Frame, - public id: string, - ) {} - - async openContextMenu(): Promise { - await this.element.click({ button: "right" }); - } - - contextMenuItem(name: string): Locator { - return this.frame.getByTestId(`TreeNode/ContextMenuItem:${name}`); - } - - async expand(): Promise { - const treeNodeContainer = this.frame.getByTestId(`TreeNodeContainer:${this.id}`); - const tree = this.frame.getByTestId(`Tree:${this.id}`); - - // eslint-disable-next-line prefer-arrow/prefer-arrow-functions - const expandNode = async () => { - if ((await treeNodeContainer.getAttribute("aria-expanded")) !== "true") { - // Click the node, to trigger loading and expansion - await this.element.click(); - } - - // Try three times to wait for the node to expand. - for (let i = 0; i < RETRY_COUNT; i++) { - try { - await tree.waitFor({ state: "visible" }); - // The tree has expanded, let's get out of here - return true; - } catch { - // Just try again - if ((await treeNodeContainer.getAttribute("aria-expanded")) !== "true") { - // We might have collapsed the node, try expanding it again, then retry. - await this.element.click(); - } - } - } - return false; - }; - - if (await expandNode()) { - return; - } - - // The tree never expanded. OR, it may have expanded in between when we found the "ExpandIcon" and when we clicked it (it's happened before) - // So, let's try one more time to expand it. - if (!(await expandNode())) { - // The tree never expanded. This is a problem. - throw new Error(`Node ${this.id} did not expand after clicking it.`); - } - - // We did it. It took a lot of weird messing around, but we expanded a tree node... I hope. - } -} - -export class Editor { - constructor( - public frame: Frame, - public locator: Locator, - ) {} - - text(): Promise { - return this.locator.evaluate((e) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const win = e.ownerDocument.defaultView as any; - if (win._monaco_getEditorContentForElement) { - return win._monaco_getEditorContentForElement(e); - } - return null; - }); - } - - async setText(text: string): Promise { - // We trust that Monaco can handle the keyboard, and it's _extremely_ flaky to try and enter text using browser commands. - // So we use a hook we installed in 'window' to set the content of the editor. - - // NOTE: This function is serialized and sent to the browser for execution - // So you can't use any variables from the outer scope, but we can send a string (via the second argument to evaluate) - await this.locator.evaluate((e, content) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const win = e.ownerDocument.defaultView as any; - if (win._monaco_setEditorContentForElement) { - win._monaco_setEditorContentForElement(e, content); - } - }, text); - - expect(await this.text()).toEqual(text); - } -} - -export class QueryTab { - resultsPane: Locator; - resultsView: Locator; - executeCTA: Locator; - errorList: Locator; - queryStatsList: Locator; - resultsEditor: Editor; - resultsTab: Locator; - queryStatsTab: Locator; - constructor( - public frame: Frame, - public tabId: string, - public tab: Locator, - public locator: Locator, - ) { - this.resultsPane = locator.getByTestId("QueryTab/ResultsPane"); - this.resultsView = locator.getByTestId("QueryTab/ResultsPane/ResultsView"); - this.executeCTA = locator.getByTestId("QueryTab/ResultsPane/ExecuteCTA"); - this.errorList = locator.getByTestId("QueryTab/ResultsPane/ErrorList"); - this.resultsEditor = new Editor(this.frame, this.resultsView.getByTestId("EditorReact/Host/Loaded")); - this.queryStatsList = locator.getByTestId("QueryTab/ResultsPane/ResultsView/QueryStatsList"); - this.resultsTab = this.resultsView.getByTestId("QueryTab/ResultsPane/ResultsView/ResultsTab"); - this.queryStatsTab = this.resultsView.getByTestId("QueryTab/ResultsPane/ResultsView/QueryStatsTab"); - } - - editor(): Editor { - const locator = this.locator.getByTestId("EditorReact/Host/Loaded"); - return new Editor(this.frame, locator); - } -} - -export class DocumentsTab { - documentsFilter: Locator; - documentsListPane: Locator; - documentResultsPane: Locator; - resultsEditor: Editor; - loadMoreButton: Locator; - filterInput: Locator; - filterButton: Locator; - - constructor( - public frame: Frame, - public tabId: string, - public tab: Locator, - public locator: Locator, - ) { - this.documentsFilter = this.locator.getByTestId("DocumentsTab/Filter"); - this.documentsListPane = this.locator.getByTestId("DocumentsTab/DocumentsPane"); - this.documentResultsPane = this.locator.getByTestId("DocumentsTab/ResultsPane"); - this.resultsEditor = new Editor(this.frame, this.documentResultsPane.getByTestId("EditorReact/Host/Loaded")); - this.loadMoreButton = this.documentsListPane.getByTestId("DocumentsTab/LoadMore"); - this.filterInput = this.documentsFilter.getByTestId("DocumentsTab/FilterInput"); - this.filterButton = this.documentsFilter.getByTestId("DocumentsTab/ApplyFilter"); - } - - async setFilter(text: string) { - await this.filterInput.fill(text); - } -} - -type PanelOpenOptions = { - closeTimeout?: number; -}; - -export enum CommandBarButton { - Save = "Save", - Delete = "Delete", - Execute = "Execute", - ExecuteQuery = "Execute Query", - UploadItem = "Upload Item", - NewDocument = "New Document", -} - -/** Helper class that provides locator methods for DataExplorer components, on top of a Frame */ -export class DataExplorer { - constructor(public frame: Frame) {} - - tab(tabId: string): Locator { - return this.frame.getByTestId(`Tab:${tabId}`); - } - - queryTab(tabId: string): QueryTab { - const tab = this.tab(tabId); - const queryTab = tab.getByTestId("QueryTab"); - return new QueryTab(this.frame, tabId, tab, queryTab); - } - - documentsTab(tabId: string): DocumentsTab { - const tab = this.tab(tabId); - const documentsTab = tab.getByTestId("DocumentsTab"); - return new DocumentsTab(this.frame, tabId, tab, documentsTab); - } - - /** Select the primary global command button. - * - * There's only a single "primary" button, but we still require you to pass the label to confirm you're selecting the right button. - */ - async globalCommandButton(label: string): Promise { - await this.frame.getByTestId("GlobalCommands").click(); - return this.frame.getByRole("menuitem", { name: label }); - } - - /** Select the command bar button with the specified label */ - commandBarButton(commandBarButton: CommandBarButton): Locator { - return this.frame.getByTestId(`CommandBar/Button:${commandBarButton}`).and(this.frame.locator("css=button")); - } - - dialogButton(label: string): Locator { - return this.frame.getByTestId(`DialogButton:${label}`).and(this.frame.locator("css=button")); - } - - /** Select the side panel with the specified title */ - panel(title: string): Locator { - return this.frame.getByTestId(`Panel:${title}`); - } - - async waitForNode(treeNodeId: string): Promise { - const node = this.treeNode(treeNodeId); - - // Is the node already visible? - if (await node.element.isVisible()) { - return node; - } - - // No, try refreshing the tree - const refreshButton = this.frame.getByTestId("Sidebar/RefreshButton"); - await refreshButton.click(); - - // Try a few times to find the node - for (let i = 0; i < RETRY_COUNT; i++) { - try { - await node.element.waitFor(); - return node; - } catch { - // Just try again - } - } - - // We tried 3 times, but the node never appeared - throw new Error(`Node ${treeNodeId} not found and did not appear after refreshing.`); - } - - async waitForContainerNode(databaseId: string, containerId: string): Promise { - const databaseNode = await this.waitForNode(databaseId); - - // The container node may be auto-expanded. Wait 5s for that to happen - try { - const containerNode = this.treeNode(`${databaseId}/${containerId}`); - await containerNode.element.waitFor({ state: "visible", timeout: 5 * 1000 }); - return containerNode; - } catch { - // It didn't auto-expand, that's fine, we'll expand it ourselves - } - - // Ok, expand the database node. - await databaseNode.expand(); - - return await this.waitForNode(`${databaseId}/${containerId}`); - } - - async waitForContainerItemsNode(databaseId: string, containerId: string): Promise { - return await this.waitForNode(`${databaseId}/${containerId}/Items`); - } - - async waitForContainerDocumentsNode(databaseId: string, containerId: string): Promise { - return await this.waitForNode(`${databaseId}/${containerId}/Documents`); - } - - async waitForCommandBarButton(label: CommandBarButton, timeout?: number): Promise { - const commandBar = this.commandBarButton(label); - await commandBar.waitFor({ state: "visible", timeout }); - return commandBar; - } - - async waitForDialogButton(label: string, timeout?: number): Promise { - const dialogButton = this.dialogButton(label); - await dialogButton.waitFor({ timeout }); - return dialogButton; - } - - /** Select the tree node with the specified id */ - treeNode(id: string): TreeNode { - return new TreeNode(this.frame.getByTestId(`TreeNode:${id}`), this.frame, id); - } - - /** Waits for the panel with the specified title to be open, then runs the provided callback. After the callback completes, waits for the panel to close. */ - async whilePanelOpen( - title: string, - action: (panel: Locator, okButton: Locator) => Promise, - options?: PanelOpenOptions, - ): Promise { - options ||= {}; - - const panel = this.panel(title); - await panel.waitFor(); - const okButton = panel.getByTestId("Panel/OkButton"); - await action(panel, okButton); - 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 */ - getConsoleHeaderStatus(): Locator { - return this.frame.getByTestId("notification-console/header-status"); - } - - async expandNotificationConsole(): Promise { - await this.setNotificationConsoleExpanded(true); - } - - async collapseNotificationConsole(): Promise { - await this.setNotificationConsoleExpanded(false); - } - - async setNotificationConsoleExpanded(expanded: boolean): Promise { - const notificationConsoleToggleButton = this.frame.getByTestId("NotificationConsole/ExpandCollapseButton"); - const alt = await notificationConsoleToggleButton.locator("img").getAttribute("alt"); - - // When expanded, the icon says "Collapse icon" - if (expanded && alt === "Expand icon") { - await notificationConsoleToggleButton.click(); - } else if (!expanded && alt === "Collapse icon") { - await notificationConsoleToggleButton.click(); - } - } - - async getNotificationConsoleMessages(): Promise { - await this.setNotificationConsoleExpanded(true); - return this.frame.getByTestId("NotificationConsole/Contents"); - } - - async getDropdownItemByName(name: string, ariaLabel?: string): Promise { - const dropdownItemsWrapper = this.frame.locator("div.ms-Dropdown-items"); - if (ariaLabel) { - expect(await dropdownItemsWrapper.getAttribute("aria-label")).toEqual(ariaLabel); - } - const containerDropdownItems = dropdownItemsWrapper.locator("button.ms-Dropdown-item[role='option']"); - return containerDropdownItems.filter({ hasText: name }); - } - - /** Waits for the Data Explorer app to load */ - static async waitForExplorer(page: Page, options?: TestExplorerUrlOptions): Promise { - const iframeElement = await page.getByTestId("DataExplorerFrame").elementHandle(); - if (iframeElement === null) { - throw new Error("Explorer iframe not found"); - } - - const explorerFrame = await iframeElement.contentFrame(); - - if (explorerFrame === null) { - throw new Error("Explorer frame not found"); - } - - if (!options?.enablecontainercopy) { - await explorerFrame?.getByTestId("DataExplorerRoot").waitFor(); - } - - return new DataExplorer(explorerFrame); - } - - /** Opens the Data Explorer app using the specified test account (and optionally, the provided IFRAME src url). */ - static async open(page: Page, testAccount: TestAccount, iframeSrc?: string): Promise { - const url = await getTestExplorerUrl(testAccount, { iframeSrc }); - await page.goto(url); - return DataExplorer.waitForExplorer(page); - } -} - -export async function waitForApiResponse( - page: Page, - urlPattern: string, - method?: string, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - payloadValidator?: (payload: any) => boolean, -) { - try { - // Check if page is still valid before waiting - if (page.isClosed()) { - throw new Error(`Page is closed, cannot wait for API response: ${urlPattern}`); - } - - return page.waitForResponse( - async (response) => { - const request = response.request(); - - if (!request.url().includes(urlPattern)) { - return false; - } - - if (method && request.method() !== method) { - return false; - } - - if (payloadValidator && (request.method() === "POST" || request.method() === "PUT")) { - const postData = request.postData(); - if (postData) { - try { - const payload = JSON.parse(postData); - return payloadValidator(payload); - } catch { - return false; - } - } - } - return true; - }, - { timeout: 60 * 1000 }, - ); - } catch (error) { - if (error instanceof Error && error.message.includes("Target page, context or browser has been closed")) { - console.warn("Page was closed while waiting for API response:", urlPattern); - throw new Error(`Page closed while waiting for API response: ${urlPattern}`); - } - throw error; - } -} -export async function interceptAndInspectApiRequest( - page: Page, - urlPattern: string, - method: string = "POST", - error: Error, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - errorValidator: (url?: string, payload?: any) => boolean, -): Promise { - await page.route( - (url) => url.pathname.includes(urlPattern), - async (route, request) => { - if (request.method() !== method) { - await route.continue(); - return; - } - const postData = request.postData(); - if (postData) { - try { - const payload = JSON.parse(postData); - if (errorValidator && errorValidator(request.url(), payload)) { - await route.fulfill({ - status: 409, - contentType: "application/json", - body: JSON.stringify({ - code: "Conflict", - message: error.message, - }), - }); - return; - } - } catch (err) { - if (err instanceof Error && err.message.includes("not allowed")) { - throw err; - } - } - } - - await route.continue(); - }, - ); -} - -export class ContainerCopy { - constructor( - public frame: Frame, - public wrapper: Locator, - ) {} - - static async waitForContainerCopy(page: Page): Promise { - const explorerFrame = await DataExplorer.waitForExplorer(page, { enablecontainercopy: true }); - const containerCopyWrapper = explorerFrame.frame.locator("div#containerCopyWrapper"); - return new ContainerCopy(explorerFrame.frame, containerCopyWrapper); - } - - static async open(page: Page, testAccount: TestAccount, iframeSrc?: string): Promise { - const url = await getTestExplorerUrl(testAccount, { iframeSrc, enablecontainercopy: true }); - await page.goto(url); - return ContainerCopy.waitForContainerCopy(page); - } -} diff --git a/.vs/CopilotSnapshots/548E56D5117B4249B654F2DE5EA27C25/96BE2FBCD6372F42971253EC90544052/37107C32C08AA184BD281864D44763B6 b/.vs/CopilotSnapshots/548E56D5117B4249B654F2DE5EA27C25/96BE2FBCD6372F42971253EC90544052/37107C32C08AA184BD281864D44763B6 deleted file mode 100644 index e9d5e2325..000000000 --- a/.vs/CopilotSnapshots/548E56D5117B4249B654F2DE5EA27C25/96BE2FBCD6372F42971253EC90544052/37107C32C08AA184BD281864D44763B6 +++ /dev/null @@ -1,702 +0,0 @@ -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; - -export interface TestNameOptions { - length?: number; - timestampped?: boolean; - prefixed?: boolean; -} - -export function generateUniqueName(baseName: string, options?: TestNameOptions): string { - const length = options?.length ?? 1; - const timestamp = options?.timestampped === undefined ? true : options.timestampped; - const prefixed = options?.prefixed === undefined ? true : options.prefixed; - - const prefix = prefixed ? "t_" : ""; - const suffix = timestamp ? `_${Date.now()}` : ""; - return `${prefix}${baseName}${crypto.randomBytes(length).toString("hex")}${suffix}`; -} - -export function getAzureCLICredentials(): DefaultAzureCredential { - return new DefaultAzureCredential(); -} - -export async function getAzureCLICredentialsToken(): Promise { - const credentials = getAzureCLICredentials(); - const token = (await credentials.getToken("https://management.core.windows.net//.default"))?.token || ""; - return token; -} - -export enum TestAccount { - Tables = "Tables", - Cassandra = "Cassandra", - Gremlin = "Gremlin", - Mongo = "Mongo", - MongoReadonly = "MongoReadOnly", - Mongo32 = "Mongo32", - SQL = "SQL", - SQLReadOnly = "SQLReadOnly", - SQLContainerCopyOnly = "SQLContainerCopyOnly", -} - -export const defaultAccounts: Record = { - [TestAccount.Tables]: "github-e2etests-tables", - [TestAccount.Cassandra]: "github-e2etests-cassandra", - [TestAccount.Gremlin]: "github-e2etests-gremlin", - [TestAccount.Mongo]: "github-e2etests-mongo", - [TestAccount.MongoReadonly]: "github-e2etests-mongo-readonly", - [TestAccount.Mongo32]: "github-e2etests-mongo32", - [TestAccount.SQL]: "github-e2etests-sql", - [TestAccount.SQLReadOnly]: "github-e2etests-sql-readonly", - [TestAccount.SQLContainerCopyOnly]: "github-e2etests-sql-containercopyonly", -}; - -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_MANUAL_THROUGHPUT_RU = 800; -export const TEST_AUTOSCALE_MAX_THROUGHPUT_RU_2K = 2000; -export const TEST_AUTOSCALE_MAX_THROUGHPUT_RU_4K = 4000; -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) { - const actualPrefix = process.env.DE_TEST_ACCOUNT_PREFIX.endsWith("-") - ? process.env.DE_TEST_ACCOUNT_PREFIX - : `${process.env.DE_TEST_ACCOUNT_PREFIX}-`; - return `${actualPrefix}${accountType.toLocaleLowerCase()}`; - } -} - -export function getAccountName(accountType: TestAccount) { - return ( - process.env[`DE_TEST_ACCOUNT_NAME_${accountType.toLocaleUpperCase()}`] ?? - tryGetStandardName(accountType) ?? - defaultAccounts[accountType] - ); -} - -type TestExplorerUrlOptions = { - iframeSrc?: string; - enablecontainercopy?: boolean; -}; - -export async function getTestExplorerUrl(accountType: TestAccount, options?: TestExplorerUrlOptions): Promise { - const { iframeSrc, enablecontainercopy } = options ?? {}; - - // We can't retrieve AZ CLI credentials from the browser so we get them here. - const token = await getAzureCLICredentialsToken(); - const accountName = getAccountName(accountType); - const params = new URLSearchParams(); - params.set("accountName", accountName); - params.set("resourceGroup", resourceGroupName); - params.set("subscriptionId", subscriptionId); - params.set("token", token); - - // There seem to be occasional CORS issues with calling the copilot APIs (/api/tokens/sampledataconnection/v2, for example) - // For now, since we don't test copilot, we can disable the copilot APIs by setting the feature flag to false. - params.set("feature.enableCopilot", "false"); - - const nosqlRbacToken = process.env.NOSQL_TESTACCOUNT_TOKEN; - const nosqlReadOnlyRbacToken = process.env.NOSQL_READONLY_TESTACCOUNT_TOKEN; - const nosqlContainerCopyRbacToken = process.env.NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN; - const tableRbacToken = process.env.TABLE_TESTACCOUNT_TOKEN; - const gremlinRbacToken = process.env.GREMLIN_TESTACCOUNT_TOKEN; - const cassandraRbacToken = process.env.CASSANDRA_TESTACCOUNT_TOKEN; - const mongoRbacToken = process.env.MONGO_TESTACCOUNT_TOKEN; - const mongo32RbacToken = process.env.MONGO32_TESTACCOUNT_TOKEN; - const mongoReadOnlyRbacToken = process.env.MONGO_READONLY_TESTACCOUNT_TOKEN; - - switch (accountType) { - case TestAccount.SQL: - if (nosqlRbacToken) { - params.set("nosqlRbacToken", nosqlRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.SQLContainerCopyOnly: - if (nosqlContainerCopyRbacToken) { - params.set("nosqlRbacToken", nosqlContainerCopyRbacToken); - params.set("enableaaddataplane", "true"); - } - if (enablecontainercopy) { - params.set("enablecontainercopy", "true"); - } - break; - - case TestAccount.SQLReadOnly: - if (nosqlReadOnlyRbacToken) { - params.set("nosqlReadOnlyRbacToken", nosqlReadOnlyRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.Tables: - if (tableRbacToken) { - params.set("tableRbacToken", tableRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.Gremlin: - if (gremlinRbacToken) { - params.set("gremlinRbacToken", gremlinRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.Cassandra: - if (cassandraRbacToken) { - params.set("cassandraRbacToken", cassandraRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.Mongo: - if (mongoRbacToken) { - params.set("mongoRbacToken", mongoRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.Mongo32: - if (mongo32RbacToken) { - params.set("mongo32RbacToken", mongo32RbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.MongoReadonly: - if (mongoReadOnlyRbacToken) { - params.set("mongoReadOnlyRbacToken", mongoReadOnlyRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - } - - if (iframeSrc) { - params.set("iframeSrc", iframeSrc); - } - - return `https://localhost:1234/testExplorer.html?${params.toString()}`; -} - -type DropdownItemExpectations = { - ariaLabel?: string; - itemCount?: number; -}; - -type DropdownItemMatcher = { - name?: string; - position?: number; -}; - -export async function getDropdownItemByNameOrPosition( - frame: Frame, - matcher?: DropdownItemMatcher, - expectedOptions?: DropdownItemExpectations, -): Promise { - const dropdownItemsWrapper = frame.locator("div.ms-Dropdown-items"); - if (expectedOptions?.ariaLabel) { - expect(await dropdownItemsWrapper.getAttribute("aria-label")).toEqual(expectedOptions.ariaLabel); - } - if (expectedOptions?.itemCount) { - const items = dropdownItemsWrapper.locator("button.ms-Dropdown-item[role='option']"); - await expect(items).toHaveCount(expectedOptions.itemCount); - } - const containerDropdownItems = dropdownItemsWrapper.locator("button.ms-Dropdown-item[role='option']"); - if (matcher?.name) { - return containerDropdownItems.filter({ hasText: matcher.name }); - } else if (matcher?.position !== undefined) { - return containerDropdownItems.nth(matcher.position); - } - // Return first item if no matcher is provided - return containerDropdownItems.first(); -} - -/** Helper class that provides locator methods for TreeNode elements, on top of a Locator */ -class TreeNode { - constructor( - public element: Locator, - public frame: Frame, - public id: string, - ) {} - - async openContextMenu(): Promise { - await this.element.click({ button: "right" }); - } - - contextMenuItem(name: string): Locator { - return this.frame.getByTestId(`TreeNode/ContextMenuItem:${name}`); - } - - async expand(): Promise { - const treeNodeContainer = this.frame.getByTestId(`TreeNodeContainer:${this.id}`); - const tree = this.frame.getByTestId(`Tree:${this.id}`); - - // eslint-disable-next-line prefer-arrow/prefer-arrow-functions - const expandNode = async () => { - if ((await treeNodeContainer.getAttribute("aria-expanded")) !== "true") { - // Click the node, to trigger loading and expansion - await this.element.click(); - } - - // Try three times to wait for the node to expand. - for (let i = 0; i < RETRY_COUNT; i++) { - try { - await tree.waitFor({ state: "visible" }); - // The tree has expanded, let's get out of here - return true; - } catch { - // Just try again - if ((await treeNodeContainer.getAttribute("aria-expanded")) !== "true") { - // We might have collapsed the node, try expanding it again, then retry. - await this.element.click(); - } - } - } - return false; - }; - - if (await expandNode()) { - return; - } - - // The tree never expanded. OR, it may have expanded in between when we found the "ExpandIcon" and when we clicked it (it's happened before) - // So, let's try one more time to expand it. - if (!(await expandNode())) { - // The tree never expanded. This is a problem. - throw new Error(`Node ${this.id} did not expand after clicking it.`); - } - - // We did it. It took a lot of weird messing around, but we expanded a tree node... I hope. - } -} - -export class Editor { - constructor( - public frame: Frame, - public locator: Locator, - ) {} - - text(): Promise { - return this.locator.evaluate((e) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const win = e.ownerDocument.defaultView as any; - if (win._monaco_getEditorContentForElement) { - return win._monaco_getEditorContentForElement(e); - } - return null; - }); - } - - async setText(text: string): Promise { - // We trust that Monaco can handle the keyboard, and it's _extremely_ flaky to try and enter text using browser commands. - // So we use a hook we installed in 'window' to set the content of the editor. - - // NOTE: This function is serialized and sent to the browser for execution - // So you can't use any variables from the outer scope, but we can send a string (via the second argument to evaluate) - await this.locator.evaluate((e, content) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const win = e.ownerDocument.defaultView as any; - if (win._monaco_setEditorContentForElement) { - win._monaco_setEditorContentForElement(e, content); - } - }, text); - - expect(await this.text()).toEqual(text); - } -} - -export class QueryTab { - resultsPane: Locator; - resultsView: Locator; - executeCTA: Locator; - errorList: Locator; - queryStatsList: Locator; - resultsEditor: Editor; - resultsTab: Locator; - queryStatsTab: Locator; - constructor( - public frame: Frame, - public tabId: string, - public tab: Locator, - public locator: Locator, - ) { - this.resultsPane = locator.getByTestId("QueryTab/ResultsPane"); - this.resultsView = locator.getByTestId("QueryTab/ResultsPane/ResultsView"); - this.executeCTA = locator.getByTestId("QueryTab/ResultsPane/ExecuteCTA"); - this.errorList = locator.getByTestId("QueryTab/ResultsPane/ErrorList"); - this.resultsEditor = new Editor(this.frame, this.resultsView.getByTestId("EditorReact/Host/Loaded")); - this.queryStatsList = locator.getByTestId("QueryTab/ResultsPane/ResultsView/QueryStatsList"); - this.resultsTab = this.resultsView.getByTestId("QueryTab/ResultsPane/ResultsView/ResultsTab"); - this.queryStatsTab = this.resultsView.getByTestId("QueryTab/ResultsPane/ResultsView/QueryStatsTab"); - } - - editor(): Editor { - const locator = this.locator.getByTestId("EditorReact/Host/Loaded"); - return new Editor(this.frame, locator); - } -} - -export class DocumentsTab { - documentsFilter: Locator; - documentsListPane: Locator; - documentResultsPane: Locator; - resultsEditor: Editor; - loadMoreButton: Locator; - filterInput: Locator; - filterButton: Locator; - - constructor( - public frame: Frame, - public tabId: string, - public tab: Locator, - public locator: Locator, - ) { - this.documentsFilter = this.locator.getByTestId("DocumentsTab/Filter"); - this.documentsListPane = this.locator.getByTestId("DocumentsTab/DocumentsPane"); - this.documentResultsPane = this.locator.getByTestId("DocumentsTab/ResultsPane"); - this.resultsEditor = new Editor(this.frame, this.documentResultsPane.getByTestId("EditorReact/Host/Loaded")); - this.loadMoreButton = this.documentsListPane.getByTestId("DocumentsTab/LoadMore"); - this.filterInput = this.documentsFilter.getByTestId("DocumentsTab/FilterInput"); - this.filterButton = this.documentsFilter.getByTestId("DocumentsTab/ApplyFilter"); - } - - async setFilter(text: string) { - await this.filterInput.fill(text); - } -} - -type PanelOpenOptions = { - closeTimeout?: number; -}; - -export enum CommandBarButton { - Save = "Save", - Delete = "Delete", - Execute = "Execute", - ExecuteQuery = "Execute Query", - UploadItem = "Upload Item", - NewDocument = "New Document", -} - -/** Helper class that provides locator methods for DataExplorer components, on top of a Frame */ -export class DataExplorer { - constructor(public frame: Frame) {} - - tab(tabId: string): Locator { - return this.frame.getByTestId(`Tab:${tabId}`); - } - - queryTab(tabId: string): QueryTab { - const tab = this.tab(tabId); - const queryTab = tab.getByTestId("QueryTab"); - return new QueryTab(this.frame, tabId, tab, queryTab); - } - - documentsTab(tabId: string): DocumentsTab { - const tab = this.tab(tabId); - const documentsTab = tab.getByTestId("DocumentsTab"); - return new DocumentsTab(this.frame, tabId, tab, documentsTab); - } - - /** Select the primary global command button. - * - * There's only a single "primary" button, but we still require you to pass the label to confirm you're selecting the right button. - */ - async globalCommandButton(label: string): Promise { - await this.frame.getByTestId("GlobalCommands").click(); - return this.frame.getByRole("menuitem", { name: label }); - } - - /** Select the command bar button with the specified label */ - commandBarButton(commandBarButton: CommandBarButton): Locator { - return this.frame.getByTestId(`CommandBar/Button:${commandBarButton}`).and(this.frame.locator("css=button")); - } - - dialogButton(label: string): Locator { - return this.frame.getByTestId(`DialogButton:${label}`).and(this.frame.locator("css=button")); - } - - /** Select the side panel with the specified title */ - panel(title: string): Locator { - return this.frame.getByTestId(`Panel:${title}`); - } - - async waitForNode(treeNodeId: string): Promise { - const node = this.treeNode(treeNodeId); - - // Is the node already visible? - if (await node.element.isVisible()) { - return node; - } - - // No, try refreshing the tree - const refreshButton = this.frame.getByTestId("Sidebar/RefreshButton"); - await refreshButton.click(); - - // Try a few times to find the node - for (let i = 0; i < RETRY_COUNT; i++) { - try { - await node.element.waitFor(); - return node; - } catch { - // Just try again - } - } - - // We tried 3 times, but the node never appeared - throw new Error(`Node ${treeNodeId} not found and did not appear after refreshing.`); - } - - async waitForContainerNode(databaseId: string, containerId: string): Promise { - const databaseNode = await this.waitForNode(databaseId); - - // The container node may be auto-expanded. Wait 5s for that to happen - try { - const containerNode = this.treeNode(`${databaseId}/${containerId}`); - await containerNode.element.waitFor({ state: "visible", timeout: 5 * 1000 }); - return containerNode; - } catch { - // It didn't auto-expand, that's fine, we'll expand it ourselves - } - - // Ok, expand the database node. - await databaseNode.expand(); - - return await this.waitForNode(`${databaseId}/${containerId}`); - } - - async waitForContainerItemsNode(databaseId: string, containerId: string): Promise { - return await this.waitForNode(`${databaseId}/${containerId}/Items`); - } - - async waitForContainerDocumentsNode(databaseId: string, containerId: string): Promise { - return await this.waitForNode(`${databaseId}/${containerId}/Documents`); - } - - async waitForCommandBarButton(label: CommandBarButton, timeout?: number): Promise { - const commandBar = this.commandBarButton(label); - await commandBar.waitFor({ state: "visible", timeout }); - return commandBar; - } - - async waitForDialogButton(label: string, timeout?: number): Promise { - const dialogButton = this.dialogButton(label); - await dialogButton.waitFor({ timeout }); - return dialogButton; - } - - /** Select the tree node with the specified id */ - treeNode(id: string): TreeNode { - return new TreeNode(this.frame.getByTestId(`TreeNode:${id}`), this.frame, id); - } - - /** Waits for the panel with the specified title to be open, then runs the provided callback. After the callback completes, waits for the panel to close. */ - async whilePanelOpen( - title: string, - action: (panel: Locator, okButton: Locator) => Promise, - options?: PanelOpenOptions, - ): Promise { - options ||= {}; - - const panel = this.panel(title); - await panel.waitFor(); - const okButton = panel.getByTestId("Panel/OkButton"); - await action(panel, okButton); - 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 */ - getConsoleHeaderStatus(): Locator { - return this.frame.getByTestId("notification-console/header-status"); - } - - async expandNotificationConsole(): Promise { - await this.setNotificationConsoleExpanded(true); - } - - async collapseNotificationConsole(): Promise { - await this.setNotificationConsoleExpanded(false); - } - - async setNotificationConsoleExpanded(expanded: boolean): Promise { - const notificationConsoleToggleButton = this.frame.getByTestId("NotificationConsole/ExpandCollapseButton"); - const alt = await notificationConsoleToggleButton.locator("img").getAttribute("alt"); - - // When expanded, the icon says "Collapse icon" - if (expanded && alt === "Expand icon") { - await notificationConsoleToggleButton.click(); - } else if (!expanded && alt === "Collapse icon") { - await notificationConsoleToggleButton.click(); - } - } - - async getNotificationConsoleMessages(): Promise { - await this.setNotificationConsoleExpanded(true); - return this.frame.getByTestId("NotificationConsole/Contents"); - } - - async getDropdownItemByName(name: string, ariaLabel?: string): Promise { - const dropdownItemsWrapper = this.frame.locator("div.ms-Dropdown-items"); - if (ariaLabel) { - expect(await dropdownItemsWrapper.getAttribute("aria-label")).toEqual(ariaLabel); - } - const containerDropdownItems = dropdownItemsWrapper.locator("button.ms-Dropdown-item[role='option']"); - return containerDropdownItems.filter({ hasText: name }); - } - - /** Waits for the Data Explorer app to load */ - static async waitForExplorer(page: Page, options?: TestExplorerUrlOptions): Promise { - const iframeElement = await page.getByTestId("DataExplorerFrame").elementHandle(); - if (iframeElement === null) { - throw new Error("Explorer iframe not found"); - } - - const explorerFrame = await iframeElement.contentFrame(); - - if (explorerFrame === null) { - throw new Error("Explorer frame not found"); - } - - if (!options?.enablecontainercopy) { - await explorerFrame?.getByTestId("DataExplorerRoot").waitFor(); - } - - return new DataExplorer(explorerFrame); - } - - /** Opens the Data Explorer app using the specified test account (and optionally, the provided IFRAME src url). */ - static async open(page: Page, testAccount: TestAccount, iframeSrc?: string): Promise { - const url = await getTestExplorerUrl(testAccount, { iframeSrc }); - await page.goto(url); - return DataExplorer.waitForExplorer(page); - } -} - -export async function waitForApiResponse( - page: Page, - urlPattern: string, - method?: string, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - payloadValidator?: (payload: any) => boolean, -) { - try { - // Check if page is still valid before waiting - if (page.isClosed()) { - throw new Error(`Page is closed, cannot wait for API response: ${urlPattern}`); - } - - return page.waitForResponse( - async (response) => { - const request = response.request(); - - if (!request.url().includes(urlPattern)) { - return false; - } - - if (method && request.method() !== method) { - return false; - } - - if (payloadValidator && (request.method() === "POST" || request.method() === "PUT")) { - const postData = request.postData(); - if (postData) { - try { - const payload = JSON.parse(postData); - return payloadValidator(payload); - } catch { - return false; - } - } - } - return true; - }, - { timeout: 60 * 1000 }, - ); - } catch (error) { - if (error instanceof Error && error.message.includes("Target page, context or browser has been closed")) { - console.warn("Page was closed while waiting for API response:", urlPattern); - throw new Error(`Page closed while waiting for API response: ${urlPattern}`); - } - throw error; - } -} -export async function interceptAndInspectApiRequest( - page: Page, - urlPattern: string, - method: string = "POST", - error: Error, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - errorValidator: (url?: string, payload?: any) => boolean, -): Promise { - await page.route( - (url) => url.pathname.includes(urlPattern), - async (route, request) => { - if (request.method() !== method) { - await route.continue(); - return; - } - const postData = request.postData(); - if (postData) { - try { - const payload = JSON.parse(postData); - if (errorValidator && errorValidator(request.url(), payload)) { - await route.fulfill({ - status: 409, - contentType: "application/json", - body: JSON.stringify({ - code: "Conflict", - message: error.message, - }), - }); - return; - } - } catch (err) { - if (err instanceof Error && err.message.includes("not allowed")) { - throw err; - } - } - } - - await route.continue(); - }, - ); -} - -export class ContainerCopy { - constructor( - public frame: Frame, - public wrapper: Locator, - ) {} - - static async waitForContainerCopy(page: Page): Promise { - const explorerFrame = await DataExplorer.waitForExplorer(page, { enablecontainercopy: true }); - const containerCopyWrapper = explorerFrame.frame.locator("div#containerCopyWrapper"); - return new ContainerCopy(explorerFrame.frame, containerCopyWrapper); - } - - static async open(page: Page, testAccount: TestAccount, iframeSrc?: string): Promise { - const url = await getTestExplorerUrl(testAccount, { iframeSrc, enablecontainercopy: true }); - await page.goto(url); - return ContainerCopy.waitForContainerCopy(page); - } -} diff --git a/.vs/CopilotSnapshots/548E56D5117B4249B654F2DE5EA27C25/96BE2FBCD6372F42971253EC90544052/B708CF3CBCBAD0788B92EF15E6396B0D b/.vs/CopilotSnapshots/548E56D5117B4249B654F2DE5EA27C25/96BE2FBCD6372F42971253EC90544052/B708CF3CBCBAD0788B92EF15E6396B0D deleted file mode 100644 index 9c0dd0d7a..000000000 --- a/.vs/CopilotSnapshots/548E56D5117B4249B654F2DE5EA27C25/96BE2FBCD6372F42971253EC90544052/B708CF3CBCBAD0788B92EF15E6396B0D +++ /dev/null @@ -1,703 +0,0 @@ -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; - -export interface TestNameOptions { - length?: number; - timestampped?: boolean; - prefixed?: boolean; -} - -export function generateUniqueName(baseName: string, options?: TestNameOptions): string { - const length = options?.length ?? 1; - const timestamp = options?.timestampped === undefined ? true : options.timestampped; - const prefixed = options?.prefixed === undefined ? true : options.prefixed; - - const prefix = prefixed ? "t_" : ""; - const suffix = timestamp ? `_${Date.now()}` : ""; - return `${prefix}${baseName}${crypto.randomBytes(length).toString("hex")}${suffix}`; -} - -export function getAzureCLICredentials(): DefaultAzureCredential { - return new DefaultAzureCredential(); -} - -export async function getAzureCLICredentialsToken(): Promise { - const credentials = getAzureCLICredentials(); - const token = (await credentials.getToken("https://management.core.windows.net//.default"))?.token || ""; - return token; -} - -export enum TestAccount { - Tables = "Tables", - Cassandra = "Cassandra", - Gremlin = "Gremlin", - Mongo = "Mongo", - MongoReadonly = "MongoReadOnly", - Mongo32 = "Mongo32", - SQL = "SQL", - SQLReadOnly = "SQLReadOnly", - SQLContainerCopyOnly = "SQLContainerCopyOnly", -} - -export const defaultAccounts: Record = { - [TestAccount.Tables]: "github-e2etests-tables", - [TestAccount.Cassandra]: "github-e2etests-cassandra", - [TestAccount.Gremlin]: "github-e2etests-gremlin", - [TestAccount.Mongo]: "github-e2etests-mongo", - [TestAccount.MongoReadonly]: "github-e2etests-mongo-readonly", - [TestAccount.Mongo32]: "github-e2etests-mongo32", - [TestAccount.SQL]: "github-e2etests-sql", - [TestAccount.SQLReadOnly]: "github-e2etests-sql-readonly", - [TestAccount.SQLContainerCopyOnly]: "github-e2etests-sql-containercopyonly", -}; - -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_MANUAL_THROUGHPUT_RU = 800; -export const TEST_AUTOSCALE_MAX_THROUGHPUT_RU_2K = 2000; -export const TEST_AUTOSCALE_MAX_THROUGHPUT_RU_4K = 4000; -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) { - const actualPrefix = process.env.DE_TEST_ACCOUNT_PREFIX.endsWith("-") - ? process.env.DE_TEST_ACCOUNT_PREFIX - : `${process.env.DE_TEST_ACCOUNT_PREFIX}-`; - return `${actualPrefix}${accountType.toLocaleLowerCase()}`; - } -} - -export function getAccountName(accountType: TestAccount) { - return ( - process.env[`DE_TEST_ACCOUNT_NAME_${accountType.toLocaleUpperCase()}`] ?? - tryGetStandardName(accountType) ?? - defaultAccounts[accountType] - ); -} - -type TestExplorerUrlOptions = { - iframeSrc?: string; - enablecontainercopy?: boolean; -}; - -export async function getTestExplorerUrl(accountType: TestAccount, options?: TestExplorerUrlOptions): Promise { - const { iframeSrc, enablecontainercopy } = options ?? {}; - - // We can't retrieve AZ CLI credentials from the browser so we get them here. - const token = await getAzureCLICredentialsToken(); - const accountName = getAccountName(accountType); - const params = new URLSearchParams(); - params.set("accountName", accountName); - params.set("resourceGroup", resourceGroupName); - params.set("subscriptionId", subscriptionId); - params.set("token", token); - - // There seem to be occasional CORS issues with calling the copilot APIs (/api/tokens/sampledataconnection/v2, for example) - // For now, since we don't test copilot, we can disable the copilot APIs by setting the feature flag to false. - params.set("feature.enableCopilot", "false"); - - const nosqlRbacToken = process.env.NOSQL_TESTACCOUNT_TOKEN; - const nosqlReadOnlyRbacToken = process.env.NOSQL_READONLY_TESTACCOUNT_TOKEN; - const nosqlContainerCopyRbacToken = process.env.NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN; - const tableRbacToken = process.env.TABLE_TESTACCOUNT_TOKEN; - const gremlinRbacToken = process.env.GREMLIN_TESTACCOUNT_TOKEN; - const cassandraRbacToken = process.env.CASSANDRA_TESTACCOUNT_TOKEN; - const mongoRbacToken = process.env.MONGO_TESTACCOUNT_TOKEN; - const mongo32RbacToken = process.env.MONGO32_TESTACCOUNT_TOKEN; - const mongoReadOnlyRbacToken = process.env.MONGO_READONLY_TESTACCOUNT_TOKEN; - - switch (accountType) { - case TestAccount.SQL: - if (nosqlRbacToken) { - params.set("nosqlRbacToken", nosqlRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.SQLContainerCopyOnly: - if (nosqlContainerCopyRbacToken) { - params.set("nosqlRbacToken", nosqlContainerCopyRbacToken); - params.set("enableaaddataplane", "true"); - } - if (enablecontainercopy) { - params.set("enablecontainercopy", "true"); - } - break; - - case TestAccount.SQLReadOnly: - if (nosqlReadOnlyRbacToken) { - params.set("nosqlReadOnlyRbacToken", nosqlReadOnlyRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.Tables: - if (tableRbacToken) { - params.set("tableRbacToken", tableRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.Gremlin: - if (gremlinRbacToken) { - params.set("gremlinRbacToken", gremlinRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.Cassandra: - if (cassandraRbacToken) { - params.set("cassandraRbacToken", cassandraRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.Mongo: - if (mongoRbacToken) { - params.set("mongoRbacToken", mongoRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.Mongo32: - if (mongo32RbacToken) { - params.set("mongo32RbacToken", mongo32RbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.MongoReadonly: - if (mongoReadOnlyRbacToken) { - params.set("mongoReadOnlyRbacToken", mongoReadOnlyRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - } - - if (iframeSrc) { - params.set("iframeSrc", iframeSrc); - } - - return `https://localhost:1234/testExplorer.html?${params.toString()}`; -} - -type DropdownItemExpectations = { - ariaLabel?: string; - itemCount?: number; -}; - -type DropdownItemMatcher = { - name?: string; - position?: number; -}; - -export async function getDropdownItemByNameOrPosition( - frame: Frame, - matcher?: DropdownItemMatcher, - expectedOptions?: DropdownItemExpectations, -): Promise { - const dropdownItemsWrapper = frame.locator("div.ms-Dropdown-items"); - if (expectedOptions?.ariaLabel) { - expect(await dropdownItemsWrapper.getAttribute("aria-label")).toEqual(expectedOptions.ariaLabel); - } - if (expectedOptions?.itemCount) { - const items = dropdownItemsWrapper.locator("button.ms-Dropdown-item[role='option']"); - await expect(items).toHaveCount(expectedOptions.itemCount); - } - const containerDropdownItems = dropdownItemsWrapper.locator("button.ms-Dropdown-item[role='option']"); - if (matcher?.name) { - return containerDropdownItems.filter({ hasText: matcher.name }); - } else if (matcher?.position !== undefined) { - return containerDropdownItems.nth(matcher.position); - } - // Return first item if no matcher is provided - return containerDropdownItems.first(); -} - -/** Helper class that provides locator methods for TreeNode elements, on top of a Locator */ -class TreeNode { - constructor( - public element: Locator, - public frame: Frame, - public id: string, - ) {} - - async openContextMenu(): Promise { - await this.element.click({ button: "right" }); - } - - contextMenuItem(name: string): Locator { - return this.frame.getByTestId(`TreeNode/ContextMenuItem:${name}`); - } - - async expand(): Promise { - const treeNodeContainer = this.frame.getByTestId(`TreeNodeContainer:${this.id}`); - const tree = this.frame.getByTestId(`Tree:${this.id}`); - - // eslint-disable-next-line prefer-arrow/prefer-arrow-functions - const expandNode = async () => { - if ((await treeNodeContainer.getAttribute("aria-expanded")) !== "true") { - // Click the node, to trigger loading and expansion - await this.element.click(); - } - - // Try three times to wait for the node to expand. - for (let i = 0; i < RETRY_COUNT; i++) { - try { - // Use a longer timeout (30s) since expanding may require loading children from the server - await tree.waitFor({ state: "visible", timeout: 30 * 1000 }); - // The tree has expanded, let's get out of here - return true; - } catch { - // Just try again - if ((await treeNodeContainer.getAttribute("aria-expanded")) !== "true") { - // We might have collapsed the node, try expanding it again, then retry. - await this.element.click(); - } - } - } - return false; - }; - - if (await expandNode()) { - return; - } - - // The tree never expanded. OR, it may have expanded in between when we found the "ExpandIcon" and when we clicked it (it's happened before) - // So, let's try one more time to expand it. - if (!(await expandNode())) { - // The tree never expanded. This is a problem. - throw new Error(`Node ${this.id} did not expand after clicking it.`); - } - - // We did it. It took a lot of weird messing around, but we expanded a tree node... I hope. - } -} - -export class Editor { - constructor( - public frame: Frame, - public locator: Locator, - ) {} - - text(): Promise { - return this.locator.evaluate((e) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const win = e.ownerDocument.defaultView as any; - if (win._monaco_getEditorContentForElement) { - return win._monaco_getEditorContentForElement(e); - } - return null; - }); - } - - async setText(text: string): Promise { - // We trust that Monaco can handle the keyboard, and it's _extremely_ flaky to try and enter text using browser commands. - // So we use a hook we installed in 'window' to set the content of the editor. - - // NOTE: This function is serialized and sent to the browser for execution - // So you can't use any variables from the outer scope, but we can send a string (via the second argument to evaluate) - await this.locator.evaluate((e, content) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const win = e.ownerDocument.defaultView as any; - if (win._monaco_setEditorContentForElement) { - win._monaco_setEditorContentForElement(e, content); - } - }, text); - - expect(await this.text()).toEqual(text); - } -} - -export class QueryTab { - resultsPane: Locator; - resultsView: Locator; - executeCTA: Locator; - errorList: Locator; - queryStatsList: Locator; - resultsEditor: Editor; - resultsTab: Locator; - queryStatsTab: Locator; - constructor( - public frame: Frame, - public tabId: string, - public tab: Locator, - public locator: Locator, - ) { - this.resultsPane = locator.getByTestId("QueryTab/ResultsPane"); - this.resultsView = locator.getByTestId("QueryTab/ResultsPane/ResultsView"); - this.executeCTA = locator.getByTestId("QueryTab/ResultsPane/ExecuteCTA"); - this.errorList = locator.getByTestId("QueryTab/ResultsPane/ErrorList"); - this.resultsEditor = new Editor(this.frame, this.resultsView.getByTestId("EditorReact/Host/Loaded")); - this.queryStatsList = locator.getByTestId("QueryTab/ResultsPane/ResultsView/QueryStatsList"); - this.resultsTab = this.resultsView.getByTestId("QueryTab/ResultsPane/ResultsView/ResultsTab"); - this.queryStatsTab = this.resultsView.getByTestId("QueryTab/ResultsPane/ResultsView/QueryStatsTab"); - } - - editor(): Editor { - const locator = this.locator.getByTestId("EditorReact/Host/Loaded"); - return new Editor(this.frame, locator); - } -} - -export class DocumentsTab { - documentsFilter: Locator; - documentsListPane: Locator; - documentResultsPane: Locator; - resultsEditor: Editor; - loadMoreButton: Locator; - filterInput: Locator; - filterButton: Locator; - - constructor( - public frame: Frame, - public tabId: string, - public tab: Locator, - public locator: Locator, - ) { - this.documentsFilter = this.locator.getByTestId("DocumentsTab/Filter"); - this.documentsListPane = this.locator.getByTestId("DocumentsTab/DocumentsPane"); - this.documentResultsPane = this.locator.getByTestId("DocumentsTab/ResultsPane"); - this.resultsEditor = new Editor(this.frame, this.documentResultsPane.getByTestId("EditorReact/Host/Loaded")); - this.loadMoreButton = this.documentsListPane.getByTestId("DocumentsTab/LoadMore"); - this.filterInput = this.documentsFilter.getByTestId("DocumentsTab/FilterInput"); - this.filterButton = this.documentsFilter.getByTestId("DocumentsTab/ApplyFilter"); - } - - async setFilter(text: string) { - await this.filterInput.fill(text); - } -} - -type PanelOpenOptions = { - closeTimeout?: number; -}; - -export enum CommandBarButton { - Save = "Save", - Delete = "Delete", - Execute = "Execute", - ExecuteQuery = "Execute Query", - UploadItem = "Upload Item", - NewDocument = "New Document", -} - -/** Helper class that provides locator methods for DataExplorer components, on top of a Frame */ -export class DataExplorer { - constructor(public frame: Frame) {} - - tab(tabId: string): Locator { - return this.frame.getByTestId(`Tab:${tabId}`); - } - - queryTab(tabId: string): QueryTab { - const tab = this.tab(tabId); - const queryTab = tab.getByTestId("QueryTab"); - return new QueryTab(this.frame, tabId, tab, queryTab); - } - - documentsTab(tabId: string): DocumentsTab { - const tab = this.tab(tabId); - const documentsTab = tab.getByTestId("DocumentsTab"); - return new DocumentsTab(this.frame, tabId, tab, documentsTab); - } - - /** Select the primary global command button. - * - * There's only a single "primary" button, but we still require you to pass the label to confirm you're selecting the right button. - */ - async globalCommandButton(label: string): Promise { - await this.frame.getByTestId("GlobalCommands").click(); - return this.frame.getByRole("menuitem", { name: label }); - } - - /** Select the command bar button with the specified label */ - commandBarButton(commandBarButton: CommandBarButton): Locator { - return this.frame.getByTestId(`CommandBar/Button:${commandBarButton}`).and(this.frame.locator("css=button")); - } - - dialogButton(label: string): Locator { - return this.frame.getByTestId(`DialogButton:${label}`).and(this.frame.locator("css=button")); - } - - /** Select the side panel with the specified title */ - panel(title: string): Locator { - return this.frame.getByTestId(`Panel:${title}`); - } - - async waitForNode(treeNodeId: string): Promise { - const node = this.treeNode(treeNodeId); - - // Is the node already visible? - if (await node.element.isVisible()) { - return node; - } - - // No, try refreshing the tree - const refreshButton = this.frame.getByTestId("Sidebar/RefreshButton"); - await refreshButton.click(); - - // Try a few times to find the node - for (let i = 0; i < RETRY_COUNT; i++) { - try { - await node.element.waitFor(); - return node; - } catch { - // Just try again - } - } - - // We tried 3 times, but the node never appeared - throw new Error(`Node ${treeNodeId} not found and did not appear after refreshing.`); - } - - async waitForContainerNode(databaseId: string, containerId: string): Promise { - const databaseNode = await this.waitForNode(databaseId); - - // The container node may be auto-expanded. Wait 5s for that to happen - try { - const containerNode = this.treeNode(`${databaseId}/${containerId}`); - await containerNode.element.waitFor({ state: "visible", timeout: 5 * 1000 }); - return containerNode; - } catch { - // It didn't auto-expand, that's fine, we'll expand it ourselves - } - - // Ok, expand the database node. - await databaseNode.expand(); - - return await this.waitForNode(`${databaseId}/${containerId}`); - } - - async waitForContainerItemsNode(databaseId: string, containerId: string): Promise { - return await this.waitForNode(`${databaseId}/${containerId}/Items`); - } - - async waitForContainerDocumentsNode(databaseId: string, containerId: string): Promise { - return await this.waitForNode(`${databaseId}/${containerId}/Documents`); - } - - async waitForCommandBarButton(label: CommandBarButton, timeout?: number): Promise { - const commandBar = this.commandBarButton(label); - await commandBar.waitFor({ state: "visible", timeout }); - return commandBar; - } - - async waitForDialogButton(label: string, timeout?: number): Promise { - const dialogButton = this.dialogButton(label); - await dialogButton.waitFor({ timeout }); - return dialogButton; - } - - /** Select the tree node with the specified id */ - treeNode(id: string): TreeNode { - return new TreeNode(this.frame.getByTestId(`TreeNode:${id}`), this.frame, id); - } - - /** Waits for the panel with the specified title to be open, then runs the provided callback. After the callback completes, waits for the panel to close. */ - async whilePanelOpen( - title: string, - action: (panel: Locator, okButton: Locator) => Promise, - options?: PanelOpenOptions, - ): Promise { - options ||= {}; - - const panel = this.panel(title); - await panel.waitFor(); - const okButton = panel.getByTestId("Panel/OkButton"); - await action(panel, okButton); - 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 */ - getConsoleHeaderStatus(): Locator { - return this.frame.getByTestId("notification-console/header-status"); - } - - async expandNotificationConsole(): Promise { - await this.setNotificationConsoleExpanded(true); - } - - async collapseNotificationConsole(): Promise { - await this.setNotificationConsoleExpanded(false); - } - - async setNotificationConsoleExpanded(expanded: boolean): Promise { - const notificationConsoleToggleButton = this.frame.getByTestId("NotificationConsole/ExpandCollapseButton"); - const alt = await notificationConsoleToggleButton.locator("img").getAttribute("alt"); - - // When expanded, the icon says "Collapse icon" - if (expanded && alt === "Expand icon") { - await notificationConsoleToggleButton.click(); - } else if (!expanded && alt === "Collapse icon") { - await notificationConsoleToggleButton.click(); - } - } - - async getNotificationConsoleMessages(): Promise { - await this.setNotificationConsoleExpanded(true); - return this.frame.getByTestId("NotificationConsole/Contents"); - } - - async getDropdownItemByName(name: string, ariaLabel?: string): Promise { - const dropdownItemsWrapper = this.frame.locator("div.ms-Dropdown-items"); - if (ariaLabel) { - expect(await dropdownItemsWrapper.getAttribute("aria-label")).toEqual(ariaLabel); - } - const containerDropdownItems = dropdownItemsWrapper.locator("button.ms-Dropdown-item[role='option']"); - return containerDropdownItems.filter({ hasText: name }); - } - - /** Waits for the Data Explorer app to load */ - static async waitForExplorer(page: Page, options?: TestExplorerUrlOptions): Promise { - const iframeElement = await page.getByTestId("DataExplorerFrame").elementHandle(); - if (iframeElement === null) { - throw new Error("Explorer iframe not found"); - } - - const explorerFrame = await iframeElement.contentFrame(); - - if (explorerFrame === null) { - throw new Error("Explorer frame not found"); - } - - if (!options?.enablecontainercopy) { - await explorerFrame?.getByTestId("DataExplorerRoot").waitFor(); - } - - return new DataExplorer(explorerFrame); - } - - /** Opens the Data Explorer app using the specified test account (and optionally, the provided IFRAME src url). */ - static async open(page: Page, testAccount: TestAccount, iframeSrc?: string): Promise { - const url = await getTestExplorerUrl(testAccount, { iframeSrc }); - await page.goto(url); - return DataExplorer.waitForExplorer(page); - } -} - -export async function waitForApiResponse( - page: Page, - urlPattern: string, - method?: string, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - payloadValidator?: (payload: any) => boolean, -) { - try { - // Check if page is still valid before waiting - if (page.isClosed()) { - throw new Error(`Page is closed, cannot wait for API response: ${urlPattern}`); - } - - return page.waitForResponse( - async (response) => { - const request = response.request(); - - if (!request.url().includes(urlPattern)) { - return false; - } - - if (method && request.method() !== method) { - return false; - } - - if (payloadValidator && (request.method() === "POST" || request.method() === "PUT")) { - const postData = request.postData(); - if (postData) { - try { - const payload = JSON.parse(postData); - return payloadValidator(payload); - } catch { - return false; - } - } - } - return true; - }, - { timeout: 60 * 1000 }, - ); - } catch (error) { - if (error instanceof Error && error.message.includes("Target page, context or browser has been closed")) { - console.warn("Page was closed while waiting for API response:", urlPattern); - throw new Error(`Page closed while waiting for API response: ${urlPattern}`); - } - throw error; - } -} -export async function interceptAndInspectApiRequest( - page: Page, - urlPattern: string, - method: string = "POST", - error: Error, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - errorValidator: (url?: string, payload?: any) => boolean, -): Promise { - await page.route( - (url) => url.pathname.includes(urlPattern), - async (route, request) => { - if (request.method() !== method) { - await route.continue(); - return; - } - const postData = request.postData(); - if (postData) { - try { - const payload = JSON.parse(postData); - if (errorValidator && errorValidator(request.url(), payload)) { - await route.fulfill({ - status: 409, - contentType: "application/json", - body: JSON.stringify({ - code: "Conflict", - message: error.message, - }), - }); - return; - } - } catch (err) { - if (err instanceof Error && err.message.includes("not allowed")) { - throw err; - } - } - } - - await route.continue(); - }, - ); -} - -export class ContainerCopy { - constructor( - public frame: Frame, - public wrapper: Locator, - ) {} - - static async waitForContainerCopy(page: Page): Promise { - const explorerFrame = await DataExplorer.waitForExplorer(page, { enablecontainercopy: true }); - const containerCopyWrapper = explorerFrame.frame.locator("div#containerCopyWrapper"); - return new ContainerCopy(explorerFrame.frame, containerCopyWrapper); - } - - static async open(page: Page, testAccount: TestAccount, iframeSrc?: string): Promise { - const url = await getTestExplorerUrl(testAccount, { iframeSrc, enablecontainercopy: true }); - await page.goto(url); - return ContainerCopy.waitForContainerCopy(page); - } -} diff --git a/.vs/CopilotSnapshots/548E56D5117B4249B654F2DE5EA27C25/96BE2FBCD6372F42971253EC90544052/BDC0B1042C11B58A5259E50EBB63DC01 b/.vs/CopilotSnapshots/548E56D5117B4249B654F2DE5EA27C25/96BE2FBCD6372F42971253EC90544052/BDC0B1042C11B58A5259E50EBB63DC01 deleted file mode 100644 index e9d5e2325..000000000 --- a/.vs/CopilotSnapshots/548E56D5117B4249B654F2DE5EA27C25/96BE2FBCD6372F42971253EC90544052/BDC0B1042C11B58A5259E50EBB63DC01 +++ /dev/null @@ -1,702 +0,0 @@ -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; - -export interface TestNameOptions { - length?: number; - timestampped?: boolean; - prefixed?: boolean; -} - -export function generateUniqueName(baseName: string, options?: TestNameOptions): string { - const length = options?.length ?? 1; - const timestamp = options?.timestampped === undefined ? true : options.timestampped; - const prefixed = options?.prefixed === undefined ? true : options.prefixed; - - const prefix = prefixed ? "t_" : ""; - const suffix = timestamp ? `_${Date.now()}` : ""; - return `${prefix}${baseName}${crypto.randomBytes(length).toString("hex")}${suffix}`; -} - -export function getAzureCLICredentials(): DefaultAzureCredential { - return new DefaultAzureCredential(); -} - -export async function getAzureCLICredentialsToken(): Promise { - const credentials = getAzureCLICredentials(); - const token = (await credentials.getToken("https://management.core.windows.net//.default"))?.token || ""; - return token; -} - -export enum TestAccount { - Tables = "Tables", - Cassandra = "Cassandra", - Gremlin = "Gremlin", - Mongo = "Mongo", - MongoReadonly = "MongoReadOnly", - Mongo32 = "Mongo32", - SQL = "SQL", - SQLReadOnly = "SQLReadOnly", - SQLContainerCopyOnly = "SQLContainerCopyOnly", -} - -export const defaultAccounts: Record = { - [TestAccount.Tables]: "github-e2etests-tables", - [TestAccount.Cassandra]: "github-e2etests-cassandra", - [TestAccount.Gremlin]: "github-e2etests-gremlin", - [TestAccount.Mongo]: "github-e2etests-mongo", - [TestAccount.MongoReadonly]: "github-e2etests-mongo-readonly", - [TestAccount.Mongo32]: "github-e2etests-mongo32", - [TestAccount.SQL]: "github-e2etests-sql", - [TestAccount.SQLReadOnly]: "github-e2etests-sql-readonly", - [TestAccount.SQLContainerCopyOnly]: "github-e2etests-sql-containercopyonly", -}; - -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_MANUAL_THROUGHPUT_RU = 800; -export const TEST_AUTOSCALE_MAX_THROUGHPUT_RU_2K = 2000; -export const TEST_AUTOSCALE_MAX_THROUGHPUT_RU_4K = 4000; -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) { - const actualPrefix = process.env.DE_TEST_ACCOUNT_PREFIX.endsWith("-") - ? process.env.DE_TEST_ACCOUNT_PREFIX - : `${process.env.DE_TEST_ACCOUNT_PREFIX}-`; - return `${actualPrefix}${accountType.toLocaleLowerCase()}`; - } -} - -export function getAccountName(accountType: TestAccount) { - return ( - process.env[`DE_TEST_ACCOUNT_NAME_${accountType.toLocaleUpperCase()}`] ?? - tryGetStandardName(accountType) ?? - defaultAccounts[accountType] - ); -} - -type TestExplorerUrlOptions = { - iframeSrc?: string; - enablecontainercopy?: boolean; -}; - -export async function getTestExplorerUrl(accountType: TestAccount, options?: TestExplorerUrlOptions): Promise { - const { iframeSrc, enablecontainercopy } = options ?? {}; - - // We can't retrieve AZ CLI credentials from the browser so we get them here. - const token = await getAzureCLICredentialsToken(); - const accountName = getAccountName(accountType); - const params = new URLSearchParams(); - params.set("accountName", accountName); - params.set("resourceGroup", resourceGroupName); - params.set("subscriptionId", subscriptionId); - params.set("token", token); - - // There seem to be occasional CORS issues with calling the copilot APIs (/api/tokens/sampledataconnection/v2, for example) - // For now, since we don't test copilot, we can disable the copilot APIs by setting the feature flag to false. - params.set("feature.enableCopilot", "false"); - - const nosqlRbacToken = process.env.NOSQL_TESTACCOUNT_TOKEN; - const nosqlReadOnlyRbacToken = process.env.NOSQL_READONLY_TESTACCOUNT_TOKEN; - const nosqlContainerCopyRbacToken = process.env.NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN; - const tableRbacToken = process.env.TABLE_TESTACCOUNT_TOKEN; - const gremlinRbacToken = process.env.GREMLIN_TESTACCOUNT_TOKEN; - const cassandraRbacToken = process.env.CASSANDRA_TESTACCOUNT_TOKEN; - const mongoRbacToken = process.env.MONGO_TESTACCOUNT_TOKEN; - const mongo32RbacToken = process.env.MONGO32_TESTACCOUNT_TOKEN; - const mongoReadOnlyRbacToken = process.env.MONGO_READONLY_TESTACCOUNT_TOKEN; - - switch (accountType) { - case TestAccount.SQL: - if (nosqlRbacToken) { - params.set("nosqlRbacToken", nosqlRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.SQLContainerCopyOnly: - if (nosqlContainerCopyRbacToken) { - params.set("nosqlRbacToken", nosqlContainerCopyRbacToken); - params.set("enableaaddataplane", "true"); - } - if (enablecontainercopy) { - params.set("enablecontainercopy", "true"); - } - break; - - case TestAccount.SQLReadOnly: - if (nosqlReadOnlyRbacToken) { - params.set("nosqlReadOnlyRbacToken", nosqlReadOnlyRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.Tables: - if (tableRbacToken) { - params.set("tableRbacToken", tableRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.Gremlin: - if (gremlinRbacToken) { - params.set("gremlinRbacToken", gremlinRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.Cassandra: - if (cassandraRbacToken) { - params.set("cassandraRbacToken", cassandraRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.Mongo: - if (mongoRbacToken) { - params.set("mongoRbacToken", mongoRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.Mongo32: - if (mongo32RbacToken) { - params.set("mongo32RbacToken", mongo32RbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.MongoReadonly: - if (mongoReadOnlyRbacToken) { - params.set("mongoReadOnlyRbacToken", mongoReadOnlyRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - } - - if (iframeSrc) { - params.set("iframeSrc", iframeSrc); - } - - return `https://localhost:1234/testExplorer.html?${params.toString()}`; -} - -type DropdownItemExpectations = { - ariaLabel?: string; - itemCount?: number; -}; - -type DropdownItemMatcher = { - name?: string; - position?: number; -}; - -export async function getDropdownItemByNameOrPosition( - frame: Frame, - matcher?: DropdownItemMatcher, - expectedOptions?: DropdownItemExpectations, -): Promise { - const dropdownItemsWrapper = frame.locator("div.ms-Dropdown-items"); - if (expectedOptions?.ariaLabel) { - expect(await dropdownItemsWrapper.getAttribute("aria-label")).toEqual(expectedOptions.ariaLabel); - } - if (expectedOptions?.itemCount) { - const items = dropdownItemsWrapper.locator("button.ms-Dropdown-item[role='option']"); - await expect(items).toHaveCount(expectedOptions.itemCount); - } - const containerDropdownItems = dropdownItemsWrapper.locator("button.ms-Dropdown-item[role='option']"); - if (matcher?.name) { - return containerDropdownItems.filter({ hasText: matcher.name }); - } else if (matcher?.position !== undefined) { - return containerDropdownItems.nth(matcher.position); - } - // Return first item if no matcher is provided - return containerDropdownItems.first(); -} - -/** Helper class that provides locator methods for TreeNode elements, on top of a Locator */ -class TreeNode { - constructor( - public element: Locator, - public frame: Frame, - public id: string, - ) {} - - async openContextMenu(): Promise { - await this.element.click({ button: "right" }); - } - - contextMenuItem(name: string): Locator { - return this.frame.getByTestId(`TreeNode/ContextMenuItem:${name}`); - } - - async expand(): Promise { - const treeNodeContainer = this.frame.getByTestId(`TreeNodeContainer:${this.id}`); - const tree = this.frame.getByTestId(`Tree:${this.id}`); - - // eslint-disable-next-line prefer-arrow/prefer-arrow-functions - const expandNode = async () => { - if ((await treeNodeContainer.getAttribute("aria-expanded")) !== "true") { - // Click the node, to trigger loading and expansion - await this.element.click(); - } - - // Try three times to wait for the node to expand. - for (let i = 0; i < RETRY_COUNT; i++) { - try { - await tree.waitFor({ state: "visible" }); - // The tree has expanded, let's get out of here - return true; - } catch { - // Just try again - if ((await treeNodeContainer.getAttribute("aria-expanded")) !== "true") { - // We might have collapsed the node, try expanding it again, then retry. - await this.element.click(); - } - } - } - return false; - }; - - if (await expandNode()) { - return; - } - - // The tree never expanded. OR, it may have expanded in between when we found the "ExpandIcon" and when we clicked it (it's happened before) - // So, let's try one more time to expand it. - if (!(await expandNode())) { - // The tree never expanded. This is a problem. - throw new Error(`Node ${this.id} did not expand after clicking it.`); - } - - // We did it. It took a lot of weird messing around, but we expanded a tree node... I hope. - } -} - -export class Editor { - constructor( - public frame: Frame, - public locator: Locator, - ) {} - - text(): Promise { - return this.locator.evaluate((e) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const win = e.ownerDocument.defaultView as any; - if (win._monaco_getEditorContentForElement) { - return win._monaco_getEditorContentForElement(e); - } - return null; - }); - } - - async setText(text: string): Promise { - // We trust that Monaco can handle the keyboard, and it's _extremely_ flaky to try and enter text using browser commands. - // So we use a hook we installed in 'window' to set the content of the editor. - - // NOTE: This function is serialized and sent to the browser for execution - // So you can't use any variables from the outer scope, but we can send a string (via the second argument to evaluate) - await this.locator.evaluate((e, content) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const win = e.ownerDocument.defaultView as any; - if (win._monaco_setEditorContentForElement) { - win._monaco_setEditorContentForElement(e, content); - } - }, text); - - expect(await this.text()).toEqual(text); - } -} - -export class QueryTab { - resultsPane: Locator; - resultsView: Locator; - executeCTA: Locator; - errorList: Locator; - queryStatsList: Locator; - resultsEditor: Editor; - resultsTab: Locator; - queryStatsTab: Locator; - constructor( - public frame: Frame, - public tabId: string, - public tab: Locator, - public locator: Locator, - ) { - this.resultsPane = locator.getByTestId("QueryTab/ResultsPane"); - this.resultsView = locator.getByTestId("QueryTab/ResultsPane/ResultsView"); - this.executeCTA = locator.getByTestId("QueryTab/ResultsPane/ExecuteCTA"); - this.errorList = locator.getByTestId("QueryTab/ResultsPane/ErrorList"); - this.resultsEditor = new Editor(this.frame, this.resultsView.getByTestId("EditorReact/Host/Loaded")); - this.queryStatsList = locator.getByTestId("QueryTab/ResultsPane/ResultsView/QueryStatsList"); - this.resultsTab = this.resultsView.getByTestId("QueryTab/ResultsPane/ResultsView/ResultsTab"); - this.queryStatsTab = this.resultsView.getByTestId("QueryTab/ResultsPane/ResultsView/QueryStatsTab"); - } - - editor(): Editor { - const locator = this.locator.getByTestId("EditorReact/Host/Loaded"); - return new Editor(this.frame, locator); - } -} - -export class DocumentsTab { - documentsFilter: Locator; - documentsListPane: Locator; - documentResultsPane: Locator; - resultsEditor: Editor; - loadMoreButton: Locator; - filterInput: Locator; - filterButton: Locator; - - constructor( - public frame: Frame, - public tabId: string, - public tab: Locator, - public locator: Locator, - ) { - this.documentsFilter = this.locator.getByTestId("DocumentsTab/Filter"); - this.documentsListPane = this.locator.getByTestId("DocumentsTab/DocumentsPane"); - this.documentResultsPane = this.locator.getByTestId("DocumentsTab/ResultsPane"); - this.resultsEditor = new Editor(this.frame, this.documentResultsPane.getByTestId("EditorReact/Host/Loaded")); - this.loadMoreButton = this.documentsListPane.getByTestId("DocumentsTab/LoadMore"); - this.filterInput = this.documentsFilter.getByTestId("DocumentsTab/FilterInput"); - this.filterButton = this.documentsFilter.getByTestId("DocumentsTab/ApplyFilter"); - } - - async setFilter(text: string) { - await this.filterInput.fill(text); - } -} - -type PanelOpenOptions = { - closeTimeout?: number; -}; - -export enum CommandBarButton { - Save = "Save", - Delete = "Delete", - Execute = "Execute", - ExecuteQuery = "Execute Query", - UploadItem = "Upload Item", - NewDocument = "New Document", -} - -/** Helper class that provides locator methods for DataExplorer components, on top of a Frame */ -export class DataExplorer { - constructor(public frame: Frame) {} - - tab(tabId: string): Locator { - return this.frame.getByTestId(`Tab:${tabId}`); - } - - queryTab(tabId: string): QueryTab { - const tab = this.tab(tabId); - const queryTab = tab.getByTestId("QueryTab"); - return new QueryTab(this.frame, tabId, tab, queryTab); - } - - documentsTab(tabId: string): DocumentsTab { - const tab = this.tab(tabId); - const documentsTab = tab.getByTestId("DocumentsTab"); - return new DocumentsTab(this.frame, tabId, tab, documentsTab); - } - - /** Select the primary global command button. - * - * There's only a single "primary" button, but we still require you to pass the label to confirm you're selecting the right button. - */ - async globalCommandButton(label: string): Promise { - await this.frame.getByTestId("GlobalCommands").click(); - return this.frame.getByRole("menuitem", { name: label }); - } - - /** Select the command bar button with the specified label */ - commandBarButton(commandBarButton: CommandBarButton): Locator { - return this.frame.getByTestId(`CommandBar/Button:${commandBarButton}`).and(this.frame.locator("css=button")); - } - - dialogButton(label: string): Locator { - return this.frame.getByTestId(`DialogButton:${label}`).and(this.frame.locator("css=button")); - } - - /** Select the side panel with the specified title */ - panel(title: string): Locator { - return this.frame.getByTestId(`Panel:${title}`); - } - - async waitForNode(treeNodeId: string): Promise { - const node = this.treeNode(treeNodeId); - - // Is the node already visible? - if (await node.element.isVisible()) { - return node; - } - - // No, try refreshing the tree - const refreshButton = this.frame.getByTestId("Sidebar/RefreshButton"); - await refreshButton.click(); - - // Try a few times to find the node - for (let i = 0; i < RETRY_COUNT; i++) { - try { - await node.element.waitFor(); - return node; - } catch { - // Just try again - } - } - - // We tried 3 times, but the node never appeared - throw new Error(`Node ${treeNodeId} not found and did not appear after refreshing.`); - } - - async waitForContainerNode(databaseId: string, containerId: string): Promise { - const databaseNode = await this.waitForNode(databaseId); - - // The container node may be auto-expanded. Wait 5s for that to happen - try { - const containerNode = this.treeNode(`${databaseId}/${containerId}`); - await containerNode.element.waitFor({ state: "visible", timeout: 5 * 1000 }); - return containerNode; - } catch { - // It didn't auto-expand, that's fine, we'll expand it ourselves - } - - // Ok, expand the database node. - await databaseNode.expand(); - - return await this.waitForNode(`${databaseId}/${containerId}`); - } - - async waitForContainerItemsNode(databaseId: string, containerId: string): Promise { - return await this.waitForNode(`${databaseId}/${containerId}/Items`); - } - - async waitForContainerDocumentsNode(databaseId: string, containerId: string): Promise { - return await this.waitForNode(`${databaseId}/${containerId}/Documents`); - } - - async waitForCommandBarButton(label: CommandBarButton, timeout?: number): Promise { - const commandBar = this.commandBarButton(label); - await commandBar.waitFor({ state: "visible", timeout }); - return commandBar; - } - - async waitForDialogButton(label: string, timeout?: number): Promise { - const dialogButton = this.dialogButton(label); - await dialogButton.waitFor({ timeout }); - return dialogButton; - } - - /** Select the tree node with the specified id */ - treeNode(id: string): TreeNode { - return new TreeNode(this.frame.getByTestId(`TreeNode:${id}`), this.frame, id); - } - - /** Waits for the panel with the specified title to be open, then runs the provided callback. After the callback completes, waits for the panel to close. */ - async whilePanelOpen( - title: string, - action: (panel: Locator, okButton: Locator) => Promise, - options?: PanelOpenOptions, - ): Promise { - options ||= {}; - - const panel = this.panel(title); - await panel.waitFor(); - const okButton = panel.getByTestId("Panel/OkButton"); - await action(panel, okButton); - 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 */ - getConsoleHeaderStatus(): Locator { - return this.frame.getByTestId("notification-console/header-status"); - } - - async expandNotificationConsole(): Promise { - await this.setNotificationConsoleExpanded(true); - } - - async collapseNotificationConsole(): Promise { - await this.setNotificationConsoleExpanded(false); - } - - async setNotificationConsoleExpanded(expanded: boolean): Promise { - const notificationConsoleToggleButton = this.frame.getByTestId("NotificationConsole/ExpandCollapseButton"); - const alt = await notificationConsoleToggleButton.locator("img").getAttribute("alt"); - - // When expanded, the icon says "Collapse icon" - if (expanded && alt === "Expand icon") { - await notificationConsoleToggleButton.click(); - } else if (!expanded && alt === "Collapse icon") { - await notificationConsoleToggleButton.click(); - } - } - - async getNotificationConsoleMessages(): Promise { - await this.setNotificationConsoleExpanded(true); - return this.frame.getByTestId("NotificationConsole/Contents"); - } - - async getDropdownItemByName(name: string, ariaLabel?: string): Promise { - const dropdownItemsWrapper = this.frame.locator("div.ms-Dropdown-items"); - if (ariaLabel) { - expect(await dropdownItemsWrapper.getAttribute("aria-label")).toEqual(ariaLabel); - } - const containerDropdownItems = dropdownItemsWrapper.locator("button.ms-Dropdown-item[role='option']"); - return containerDropdownItems.filter({ hasText: name }); - } - - /** Waits for the Data Explorer app to load */ - static async waitForExplorer(page: Page, options?: TestExplorerUrlOptions): Promise { - const iframeElement = await page.getByTestId("DataExplorerFrame").elementHandle(); - if (iframeElement === null) { - throw new Error("Explorer iframe not found"); - } - - const explorerFrame = await iframeElement.contentFrame(); - - if (explorerFrame === null) { - throw new Error("Explorer frame not found"); - } - - if (!options?.enablecontainercopy) { - await explorerFrame?.getByTestId("DataExplorerRoot").waitFor(); - } - - return new DataExplorer(explorerFrame); - } - - /** Opens the Data Explorer app using the specified test account (and optionally, the provided IFRAME src url). */ - static async open(page: Page, testAccount: TestAccount, iframeSrc?: string): Promise { - const url = await getTestExplorerUrl(testAccount, { iframeSrc }); - await page.goto(url); - return DataExplorer.waitForExplorer(page); - } -} - -export async function waitForApiResponse( - page: Page, - urlPattern: string, - method?: string, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - payloadValidator?: (payload: any) => boolean, -) { - try { - // Check if page is still valid before waiting - if (page.isClosed()) { - throw new Error(`Page is closed, cannot wait for API response: ${urlPattern}`); - } - - return page.waitForResponse( - async (response) => { - const request = response.request(); - - if (!request.url().includes(urlPattern)) { - return false; - } - - if (method && request.method() !== method) { - return false; - } - - if (payloadValidator && (request.method() === "POST" || request.method() === "PUT")) { - const postData = request.postData(); - if (postData) { - try { - const payload = JSON.parse(postData); - return payloadValidator(payload); - } catch { - return false; - } - } - } - return true; - }, - { timeout: 60 * 1000 }, - ); - } catch (error) { - if (error instanceof Error && error.message.includes("Target page, context or browser has been closed")) { - console.warn("Page was closed while waiting for API response:", urlPattern); - throw new Error(`Page closed while waiting for API response: ${urlPattern}`); - } - throw error; - } -} -export async function interceptAndInspectApiRequest( - page: Page, - urlPattern: string, - method: string = "POST", - error: Error, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - errorValidator: (url?: string, payload?: any) => boolean, -): Promise { - await page.route( - (url) => url.pathname.includes(urlPattern), - async (route, request) => { - if (request.method() !== method) { - await route.continue(); - return; - } - const postData = request.postData(); - if (postData) { - try { - const payload = JSON.parse(postData); - if (errorValidator && errorValidator(request.url(), payload)) { - await route.fulfill({ - status: 409, - contentType: "application/json", - body: JSON.stringify({ - code: "Conflict", - message: error.message, - }), - }); - return; - } - } catch (err) { - if (err instanceof Error && err.message.includes("not allowed")) { - throw err; - } - } - } - - await route.continue(); - }, - ); -} - -export class ContainerCopy { - constructor( - public frame: Frame, - public wrapper: Locator, - ) {} - - static async waitForContainerCopy(page: Page): Promise { - const explorerFrame = await DataExplorer.waitForExplorer(page, { enablecontainercopy: true }); - const containerCopyWrapper = explorerFrame.frame.locator("div#containerCopyWrapper"); - return new ContainerCopy(explorerFrame.frame, containerCopyWrapper); - } - - static async open(page: Page, testAccount: TestAccount, iframeSrc?: string): Promise { - const url = await getTestExplorerUrl(testAccount, { iframeSrc, enablecontainercopy: true }); - await page.goto(url); - return ContainerCopy.waitForContainerCopy(page); - } -} diff --git a/.vs/CopilotSnapshots/548E56D5117B4249B654F2DE5EA27C25/BC13077299BF7643B6C9034B8CDDB754/BDC0B1042C11B58A5259E50EBB63DC01 b/.vs/CopilotSnapshots/548E56D5117B4249B654F2DE5EA27C25/BC13077299BF7643B6C9034B8CDDB754/BDC0B1042C11B58A5259E50EBB63DC01 deleted file mode 100644 index 9c0dd0d7a..000000000 --- a/.vs/CopilotSnapshots/548E56D5117B4249B654F2DE5EA27C25/BC13077299BF7643B6C9034B8CDDB754/BDC0B1042C11B58A5259E50EBB63DC01 +++ /dev/null @@ -1,703 +0,0 @@ -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; - -export interface TestNameOptions { - length?: number; - timestampped?: boolean; - prefixed?: boolean; -} - -export function generateUniqueName(baseName: string, options?: TestNameOptions): string { - const length = options?.length ?? 1; - const timestamp = options?.timestampped === undefined ? true : options.timestampped; - const prefixed = options?.prefixed === undefined ? true : options.prefixed; - - const prefix = prefixed ? "t_" : ""; - const suffix = timestamp ? `_${Date.now()}` : ""; - return `${prefix}${baseName}${crypto.randomBytes(length).toString("hex")}${suffix}`; -} - -export function getAzureCLICredentials(): DefaultAzureCredential { - return new DefaultAzureCredential(); -} - -export async function getAzureCLICredentialsToken(): Promise { - const credentials = getAzureCLICredentials(); - const token = (await credentials.getToken("https://management.core.windows.net//.default"))?.token || ""; - return token; -} - -export enum TestAccount { - Tables = "Tables", - Cassandra = "Cassandra", - Gremlin = "Gremlin", - Mongo = "Mongo", - MongoReadonly = "MongoReadOnly", - Mongo32 = "Mongo32", - SQL = "SQL", - SQLReadOnly = "SQLReadOnly", - SQLContainerCopyOnly = "SQLContainerCopyOnly", -} - -export const defaultAccounts: Record = { - [TestAccount.Tables]: "github-e2etests-tables", - [TestAccount.Cassandra]: "github-e2etests-cassandra", - [TestAccount.Gremlin]: "github-e2etests-gremlin", - [TestAccount.Mongo]: "github-e2etests-mongo", - [TestAccount.MongoReadonly]: "github-e2etests-mongo-readonly", - [TestAccount.Mongo32]: "github-e2etests-mongo32", - [TestAccount.SQL]: "github-e2etests-sql", - [TestAccount.SQLReadOnly]: "github-e2etests-sql-readonly", - [TestAccount.SQLContainerCopyOnly]: "github-e2etests-sql-containercopyonly", -}; - -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_MANUAL_THROUGHPUT_RU = 800; -export const TEST_AUTOSCALE_MAX_THROUGHPUT_RU_2K = 2000; -export const TEST_AUTOSCALE_MAX_THROUGHPUT_RU_4K = 4000; -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) { - const actualPrefix = process.env.DE_TEST_ACCOUNT_PREFIX.endsWith("-") - ? process.env.DE_TEST_ACCOUNT_PREFIX - : `${process.env.DE_TEST_ACCOUNT_PREFIX}-`; - return `${actualPrefix}${accountType.toLocaleLowerCase()}`; - } -} - -export function getAccountName(accountType: TestAccount) { - return ( - process.env[`DE_TEST_ACCOUNT_NAME_${accountType.toLocaleUpperCase()}`] ?? - tryGetStandardName(accountType) ?? - defaultAccounts[accountType] - ); -} - -type TestExplorerUrlOptions = { - iframeSrc?: string; - enablecontainercopy?: boolean; -}; - -export async function getTestExplorerUrl(accountType: TestAccount, options?: TestExplorerUrlOptions): Promise { - const { iframeSrc, enablecontainercopy } = options ?? {}; - - // We can't retrieve AZ CLI credentials from the browser so we get them here. - const token = await getAzureCLICredentialsToken(); - const accountName = getAccountName(accountType); - const params = new URLSearchParams(); - params.set("accountName", accountName); - params.set("resourceGroup", resourceGroupName); - params.set("subscriptionId", subscriptionId); - params.set("token", token); - - // There seem to be occasional CORS issues with calling the copilot APIs (/api/tokens/sampledataconnection/v2, for example) - // For now, since we don't test copilot, we can disable the copilot APIs by setting the feature flag to false. - params.set("feature.enableCopilot", "false"); - - const nosqlRbacToken = process.env.NOSQL_TESTACCOUNT_TOKEN; - const nosqlReadOnlyRbacToken = process.env.NOSQL_READONLY_TESTACCOUNT_TOKEN; - const nosqlContainerCopyRbacToken = process.env.NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN; - const tableRbacToken = process.env.TABLE_TESTACCOUNT_TOKEN; - const gremlinRbacToken = process.env.GREMLIN_TESTACCOUNT_TOKEN; - const cassandraRbacToken = process.env.CASSANDRA_TESTACCOUNT_TOKEN; - const mongoRbacToken = process.env.MONGO_TESTACCOUNT_TOKEN; - const mongo32RbacToken = process.env.MONGO32_TESTACCOUNT_TOKEN; - const mongoReadOnlyRbacToken = process.env.MONGO_READONLY_TESTACCOUNT_TOKEN; - - switch (accountType) { - case TestAccount.SQL: - if (nosqlRbacToken) { - params.set("nosqlRbacToken", nosqlRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.SQLContainerCopyOnly: - if (nosqlContainerCopyRbacToken) { - params.set("nosqlRbacToken", nosqlContainerCopyRbacToken); - params.set("enableaaddataplane", "true"); - } - if (enablecontainercopy) { - params.set("enablecontainercopy", "true"); - } - break; - - case TestAccount.SQLReadOnly: - if (nosqlReadOnlyRbacToken) { - params.set("nosqlReadOnlyRbacToken", nosqlReadOnlyRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.Tables: - if (tableRbacToken) { - params.set("tableRbacToken", tableRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.Gremlin: - if (gremlinRbacToken) { - params.set("gremlinRbacToken", gremlinRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.Cassandra: - if (cassandraRbacToken) { - params.set("cassandraRbacToken", cassandraRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.Mongo: - if (mongoRbacToken) { - params.set("mongoRbacToken", mongoRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.Mongo32: - if (mongo32RbacToken) { - params.set("mongo32RbacToken", mongo32RbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.MongoReadonly: - if (mongoReadOnlyRbacToken) { - params.set("mongoReadOnlyRbacToken", mongoReadOnlyRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - } - - if (iframeSrc) { - params.set("iframeSrc", iframeSrc); - } - - return `https://localhost:1234/testExplorer.html?${params.toString()}`; -} - -type DropdownItemExpectations = { - ariaLabel?: string; - itemCount?: number; -}; - -type DropdownItemMatcher = { - name?: string; - position?: number; -}; - -export async function getDropdownItemByNameOrPosition( - frame: Frame, - matcher?: DropdownItemMatcher, - expectedOptions?: DropdownItemExpectations, -): Promise { - const dropdownItemsWrapper = frame.locator("div.ms-Dropdown-items"); - if (expectedOptions?.ariaLabel) { - expect(await dropdownItemsWrapper.getAttribute("aria-label")).toEqual(expectedOptions.ariaLabel); - } - if (expectedOptions?.itemCount) { - const items = dropdownItemsWrapper.locator("button.ms-Dropdown-item[role='option']"); - await expect(items).toHaveCount(expectedOptions.itemCount); - } - const containerDropdownItems = dropdownItemsWrapper.locator("button.ms-Dropdown-item[role='option']"); - if (matcher?.name) { - return containerDropdownItems.filter({ hasText: matcher.name }); - } else if (matcher?.position !== undefined) { - return containerDropdownItems.nth(matcher.position); - } - // Return first item if no matcher is provided - return containerDropdownItems.first(); -} - -/** Helper class that provides locator methods for TreeNode elements, on top of a Locator */ -class TreeNode { - constructor( - public element: Locator, - public frame: Frame, - public id: string, - ) {} - - async openContextMenu(): Promise { - await this.element.click({ button: "right" }); - } - - contextMenuItem(name: string): Locator { - return this.frame.getByTestId(`TreeNode/ContextMenuItem:${name}`); - } - - async expand(): Promise { - const treeNodeContainer = this.frame.getByTestId(`TreeNodeContainer:${this.id}`); - const tree = this.frame.getByTestId(`Tree:${this.id}`); - - // eslint-disable-next-line prefer-arrow/prefer-arrow-functions - const expandNode = async () => { - if ((await treeNodeContainer.getAttribute("aria-expanded")) !== "true") { - // Click the node, to trigger loading and expansion - await this.element.click(); - } - - // Try three times to wait for the node to expand. - for (let i = 0; i < RETRY_COUNT; i++) { - try { - // Use a longer timeout (30s) since expanding may require loading children from the server - await tree.waitFor({ state: "visible", timeout: 30 * 1000 }); - // The tree has expanded, let's get out of here - return true; - } catch { - // Just try again - if ((await treeNodeContainer.getAttribute("aria-expanded")) !== "true") { - // We might have collapsed the node, try expanding it again, then retry. - await this.element.click(); - } - } - } - return false; - }; - - if (await expandNode()) { - return; - } - - // The tree never expanded. OR, it may have expanded in between when we found the "ExpandIcon" and when we clicked it (it's happened before) - // So, let's try one more time to expand it. - if (!(await expandNode())) { - // The tree never expanded. This is a problem. - throw new Error(`Node ${this.id} did not expand after clicking it.`); - } - - // We did it. It took a lot of weird messing around, but we expanded a tree node... I hope. - } -} - -export class Editor { - constructor( - public frame: Frame, - public locator: Locator, - ) {} - - text(): Promise { - return this.locator.evaluate((e) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const win = e.ownerDocument.defaultView as any; - if (win._monaco_getEditorContentForElement) { - return win._monaco_getEditorContentForElement(e); - } - return null; - }); - } - - async setText(text: string): Promise { - // We trust that Monaco can handle the keyboard, and it's _extremely_ flaky to try and enter text using browser commands. - // So we use a hook we installed in 'window' to set the content of the editor. - - // NOTE: This function is serialized and sent to the browser for execution - // So you can't use any variables from the outer scope, but we can send a string (via the second argument to evaluate) - await this.locator.evaluate((e, content) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const win = e.ownerDocument.defaultView as any; - if (win._monaco_setEditorContentForElement) { - win._monaco_setEditorContentForElement(e, content); - } - }, text); - - expect(await this.text()).toEqual(text); - } -} - -export class QueryTab { - resultsPane: Locator; - resultsView: Locator; - executeCTA: Locator; - errorList: Locator; - queryStatsList: Locator; - resultsEditor: Editor; - resultsTab: Locator; - queryStatsTab: Locator; - constructor( - public frame: Frame, - public tabId: string, - public tab: Locator, - public locator: Locator, - ) { - this.resultsPane = locator.getByTestId("QueryTab/ResultsPane"); - this.resultsView = locator.getByTestId("QueryTab/ResultsPane/ResultsView"); - this.executeCTA = locator.getByTestId("QueryTab/ResultsPane/ExecuteCTA"); - this.errorList = locator.getByTestId("QueryTab/ResultsPane/ErrorList"); - this.resultsEditor = new Editor(this.frame, this.resultsView.getByTestId("EditorReact/Host/Loaded")); - this.queryStatsList = locator.getByTestId("QueryTab/ResultsPane/ResultsView/QueryStatsList"); - this.resultsTab = this.resultsView.getByTestId("QueryTab/ResultsPane/ResultsView/ResultsTab"); - this.queryStatsTab = this.resultsView.getByTestId("QueryTab/ResultsPane/ResultsView/QueryStatsTab"); - } - - editor(): Editor { - const locator = this.locator.getByTestId("EditorReact/Host/Loaded"); - return new Editor(this.frame, locator); - } -} - -export class DocumentsTab { - documentsFilter: Locator; - documentsListPane: Locator; - documentResultsPane: Locator; - resultsEditor: Editor; - loadMoreButton: Locator; - filterInput: Locator; - filterButton: Locator; - - constructor( - public frame: Frame, - public tabId: string, - public tab: Locator, - public locator: Locator, - ) { - this.documentsFilter = this.locator.getByTestId("DocumentsTab/Filter"); - this.documentsListPane = this.locator.getByTestId("DocumentsTab/DocumentsPane"); - this.documentResultsPane = this.locator.getByTestId("DocumentsTab/ResultsPane"); - this.resultsEditor = new Editor(this.frame, this.documentResultsPane.getByTestId("EditorReact/Host/Loaded")); - this.loadMoreButton = this.documentsListPane.getByTestId("DocumentsTab/LoadMore"); - this.filterInput = this.documentsFilter.getByTestId("DocumentsTab/FilterInput"); - this.filterButton = this.documentsFilter.getByTestId("DocumentsTab/ApplyFilter"); - } - - async setFilter(text: string) { - await this.filterInput.fill(text); - } -} - -type PanelOpenOptions = { - closeTimeout?: number; -}; - -export enum CommandBarButton { - Save = "Save", - Delete = "Delete", - Execute = "Execute", - ExecuteQuery = "Execute Query", - UploadItem = "Upload Item", - NewDocument = "New Document", -} - -/** Helper class that provides locator methods for DataExplorer components, on top of a Frame */ -export class DataExplorer { - constructor(public frame: Frame) {} - - tab(tabId: string): Locator { - return this.frame.getByTestId(`Tab:${tabId}`); - } - - queryTab(tabId: string): QueryTab { - const tab = this.tab(tabId); - const queryTab = tab.getByTestId("QueryTab"); - return new QueryTab(this.frame, tabId, tab, queryTab); - } - - documentsTab(tabId: string): DocumentsTab { - const tab = this.tab(tabId); - const documentsTab = tab.getByTestId("DocumentsTab"); - return new DocumentsTab(this.frame, tabId, tab, documentsTab); - } - - /** Select the primary global command button. - * - * There's only a single "primary" button, but we still require you to pass the label to confirm you're selecting the right button. - */ - async globalCommandButton(label: string): Promise { - await this.frame.getByTestId("GlobalCommands").click(); - return this.frame.getByRole("menuitem", { name: label }); - } - - /** Select the command bar button with the specified label */ - commandBarButton(commandBarButton: CommandBarButton): Locator { - return this.frame.getByTestId(`CommandBar/Button:${commandBarButton}`).and(this.frame.locator("css=button")); - } - - dialogButton(label: string): Locator { - return this.frame.getByTestId(`DialogButton:${label}`).and(this.frame.locator("css=button")); - } - - /** Select the side panel with the specified title */ - panel(title: string): Locator { - return this.frame.getByTestId(`Panel:${title}`); - } - - async waitForNode(treeNodeId: string): Promise { - const node = this.treeNode(treeNodeId); - - // Is the node already visible? - if (await node.element.isVisible()) { - return node; - } - - // No, try refreshing the tree - const refreshButton = this.frame.getByTestId("Sidebar/RefreshButton"); - await refreshButton.click(); - - // Try a few times to find the node - for (let i = 0; i < RETRY_COUNT; i++) { - try { - await node.element.waitFor(); - return node; - } catch { - // Just try again - } - } - - // We tried 3 times, but the node never appeared - throw new Error(`Node ${treeNodeId} not found and did not appear after refreshing.`); - } - - async waitForContainerNode(databaseId: string, containerId: string): Promise { - const databaseNode = await this.waitForNode(databaseId); - - // The container node may be auto-expanded. Wait 5s for that to happen - try { - const containerNode = this.treeNode(`${databaseId}/${containerId}`); - await containerNode.element.waitFor({ state: "visible", timeout: 5 * 1000 }); - return containerNode; - } catch { - // It didn't auto-expand, that's fine, we'll expand it ourselves - } - - // Ok, expand the database node. - await databaseNode.expand(); - - return await this.waitForNode(`${databaseId}/${containerId}`); - } - - async waitForContainerItemsNode(databaseId: string, containerId: string): Promise { - return await this.waitForNode(`${databaseId}/${containerId}/Items`); - } - - async waitForContainerDocumentsNode(databaseId: string, containerId: string): Promise { - return await this.waitForNode(`${databaseId}/${containerId}/Documents`); - } - - async waitForCommandBarButton(label: CommandBarButton, timeout?: number): Promise { - const commandBar = this.commandBarButton(label); - await commandBar.waitFor({ state: "visible", timeout }); - return commandBar; - } - - async waitForDialogButton(label: string, timeout?: number): Promise { - const dialogButton = this.dialogButton(label); - await dialogButton.waitFor({ timeout }); - return dialogButton; - } - - /** Select the tree node with the specified id */ - treeNode(id: string): TreeNode { - return new TreeNode(this.frame.getByTestId(`TreeNode:${id}`), this.frame, id); - } - - /** Waits for the panel with the specified title to be open, then runs the provided callback. After the callback completes, waits for the panel to close. */ - async whilePanelOpen( - title: string, - action: (panel: Locator, okButton: Locator) => Promise, - options?: PanelOpenOptions, - ): Promise { - options ||= {}; - - const panel = this.panel(title); - await panel.waitFor(); - const okButton = panel.getByTestId("Panel/OkButton"); - await action(panel, okButton); - 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 */ - getConsoleHeaderStatus(): Locator { - return this.frame.getByTestId("notification-console/header-status"); - } - - async expandNotificationConsole(): Promise { - await this.setNotificationConsoleExpanded(true); - } - - async collapseNotificationConsole(): Promise { - await this.setNotificationConsoleExpanded(false); - } - - async setNotificationConsoleExpanded(expanded: boolean): Promise { - const notificationConsoleToggleButton = this.frame.getByTestId("NotificationConsole/ExpandCollapseButton"); - const alt = await notificationConsoleToggleButton.locator("img").getAttribute("alt"); - - // When expanded, the icon says "Collapse icon" - if (expanded && alt === "Expand icon") { - await notificationConsoleToggleButton.click(); - } else if (!expanded && alt === "Collapse icon") { - await notificationConsoleToggleButton.click(); - } - } - - async getNotificationConsoleMessages(): Promise { - await this.setNotificationConsoleExpanded(true); - return this.frame.getByTestId("NotificationConsole/Contents"); - } - - async getDropdownItemByName(name: string, ariaLabel?: string): Promise { - const dropdownItemsWrapper = this.frame.locator("div.ms-Dropdown-items"); - if (ariaLabel) { - expect(await dropdownItemsWrapper.getAttribute("aria-label")).toEqual(ariaLabel); - } - const containerDropdownItems = dropdownItemsWrapper.locator("button.ms-Dropdown-item[role='option']"); - return containerDropdownItems.filter({ hasText: name }); - } - - /** Waits for the Data Explorer app to load */ - static async waitForExplorer(page: Page, options?: TestExplorerUrlOptions): Promise { - const iframeElement = await page.getByTestId("DataExplorerFrame").elementHandle(); - if (iframeElement === null) { - throw new Error("Explorer iframe not found"); - } - - const explorerFrame = await iframeElement.contentFrame(); - - if (explorerFrame === null) { - throw new Error("Explorer frame not found"); - } - - if (!options?.enablecontainercopy) { - await explorerFrame?.getByTestId("DataExplorerRoot").waitFor(); - } - - return new DataExplorer(explorerFrame); - } - - /** Opens the Data Explorer app using the specified test account (and optionally, the provided IFRAME src url). */ - static async open(page: Page, testAccount: TestAccount, iframeSrc?: string): Promise { - const url = await getTestExplorerUrl(testAccount, { iframeSrc }); - await page.goto(url); - return DataExplorer.waitForExplorer(page); - } -} - -export async function waitForApiResponse( - page: Page, - urlPattern: string, - method?: string, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - payloadValidator?: (payload: any) => boolean, -) { - try { - // Check if page is still valid before waiting - if (page.isClosed()) { - throw new Error(`Page is closed, cannot wait for API response: ${urlPattern}`); - } - - return page.waitForResponse( - async (response) => { - const request = response.request(); - - if (!request.url().includes(urlPattern)) { - return false; - } - - if (method && request.method() !== method) { - return false; - } - - if (payloadValidator && (request.method() === "POST" || request.method() === "PUT")) { - const postData = request.postData(); - if (postData) { - try { - const payload = JSON.parse(postData); - return payloadValidator(payload); - } catch { - return false; - } - } - } - return true; - }, - { timeout: 60 * 1000 }, - ); - } catch (error) { - if (error instanceof Error && error.message.includes("Target page, context or browser has been closed")) { - console.warn("Page was closed while waiting for API response:", urlPattern); - throw new Error(`Page closed while waiting for API response: ${urlPattern}`); - } - throw error; - } -} -export async function interceptAndInspectApiRequest( - page: Page, - urlPattern: string, - method: string = "POST", - error: Error, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - errorValidator: (url?: string, payload?: any) => boolean, -): Promise { - await page.route( - (url) => url.pathname.includes(urlPattern), - async (route, request) => { - if (request.method() !== method) { - await route.continue(); - return; - } - const postData = request.postData(); - if (postData) { - try { - const payload = JSON.parse(postData); - if (errorValidator && errorValidator(request.url(), payload)) { - await route.fulfill({ - status: 409, - contentType: "application/json", - body: JSON.stringify({ - code: "Conflict", - message: error.message, - }), - }); - return; - } - } catch (err) { - if (err instanceof Error && err.message.includes("not allowed")) { - throw err; - } - } - } - - await route.continue(); - }, - ); -} - -export class ContainerCopy { - constructor( - public frame: Frame, - public wrapper: Locator, - ) {} - - static async waitForContainerCopy(page: Page): Promise { - const explorerFrame = await DataExplorer.waitForExplorer(page, { enablecontainercopy: true }); - const containerCopyWrapper = explorerFrame.frame.locator("div#containerCopyWrapper"); - return new ContainerCopy(explorerFrame.frame, containerCopyWrapper); - } - - static async open(page: Page, testAccount: TestAccount, iframeSrc?: string): Promise { - const url = await getTestExplorerUrl(testAccount, { iframeSrc, enablecontainercopy: true }); - await page.goto(url); - return ContainerCopy.waitForContainerCopy(page); - } -} diff --git a/.vs/CopilotSnapshots/548E56D5117B4249B654F2DE5EA27C25/BC13077299BF7643B6C9034B8CDDB754/FB327B239331C5DD2358B4E83D63183E b/.vs/CopilotSnapshots/548E56D5117B4249B654F2DE5EA27C25/BC13077299BF7643B6C9034B8CDDB754/FB327B239331C5DD2358B4E83D63183E deleted file mode 100644 index 9c0dd0d7a..000000000 --- a/.vs/CopilotSnapshots/548E56D5117B4249B654F2DE5EA27C25/BC13077299BF7643B6C9034B8CDDB754/FB327B239331C5DD2358B4E83D63183E +++ /dev/null @@ -1,703 +0,0 @@ -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; - -export interface TestNameOptions { - length?: number; - timestampped?: boolean; - prefixed?: boolean; -} - -export function generateUniqueName(baseName: string, options?: TestNameOptions): string { - const length = options?.length ?? 1; - const timestamp = options?.timestampped === undefined ? true : options.timestampped; - const prefixed = options?.prefixed === undefined ? true : options.prefixed; - - const prefix = prefixed ? "t_" : ""; - const suffix = timestamp ? `_${Date.now()}` : ""; - return `${prefix}${baseName}${crypto.randomBytes(length).toString("hex")}${suffix}`; -} - -export function getAzureCLICredentials(): DefaultAzureCredential { - return new DefaultAzureCredential(); -} - -export async function getAzureCLICredentialsToken(): Promise { - const credentials = getAzureCLICredentials(); - const token = (await credentials.getToken("https://management.core.windows.net//.default"))?.token || ""; - return token; -} - -export enum TestAccount { - Tables = "Tables", - Cassandra = "Cassandra", - Gremlin = "Gremlin", - Mongo = "Mongo", - MongoReadonly = "MongoReadOnly", - Mongo32 = "Mongo32", - SQL = "SQL", - SQLReadOnly = "SQLReadOnly", - SQLContainerCopyOnly = "SQLContainerCopyOnly", -} - -export const defaultAccounts: Record = { - [TestAccount.Tables]: "github-e2etests-tables", - [TestAccount.Cassandra]: "github-e2etests-cassandra", - [TestAccount.Gremlin]: "github-e2etests-gremlin", - [TestAccount.Mongo]: "github-e2etests-mongo", - [TestAccount.MongoReadonly]: "github-e2etests-mongo-readonly", - [TestAccount.Mongo32]: "github-e2etests-mongo32", - [TestAccount.SQL]: "github-e2etests-sql", - [TestAccount.SQLReadOnly]: "github-e2etests-sql-readonly", - [TestAccount.SQLContainerCopyOnly]: "github-e2etests-sql-containercopyonly", -}; - -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_MANUAL_THROUGHPUT_RU = 800; -export const TEST_AUTOSCALE_MAX_THROUGHPUT_RU_2K = 2000; -export const TEST_AUTOSCALE_MAX_THROUGHPUT_RU_4K = 4000; -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) { - const actualPrefix = process.env.DE_TEST_ACCOUNT_PREFIX.endsWith("-") - ? process.env.DE_TEST_ACCOUNT_PREFIX - : `${process.env.DE_TEST_ACCOUNT_PREFIX}-`; - return `${actualPrefix}${accountType.toLocaleLowerCase()}`; - } -} - -export function getAccountName(accountType: TestAccount) { - return ( - process.env[`DE_TEST_ACCOUNT_NAME_${accountType.toLocaleUpperCase()}`] ?? - tryGetStandardName(accountType) ?? - defaultAccounts[accountType] - ); -} - -type TestExplorerUrlOptions = { - iframeSrc?: string; - enablecontainercopy?: boolean; -}; - -export async function getTestExplorerUrl(accountType: TestAccount, options?: TestExplorerUrlOptions): Promise { - const { iframeSrc, enablecontainercopy } = options ?? {}; - - // We can't retrieve AZ CLI credentials from the browser so we get them here. - const token = await getAzureCLICredentialsToken(); - const accountName = getAccountName(accountType); - const params = new URLSearchParams(); - params.set("accountName", accountName); - params.set("resourceGroup", resourceGroupName); - params.set("subscriptionId", subscriptionId); - params.set("token", token); - - // There seem to be occasional CORS issues with calling the copilot APIs (/api/tokens/sampledataconnection/v2, for example) - // For now, since we don't test copilot, we can disable the copilot APIs by setting the feature flag to false. - params.set("feature.enableCopilot", "false"); - - const nosqlRbacToken = process.env.NOSQL_TESTACCOUNT_TOKEN; - const nosqlReadOnlyRbacToken = process.env.NOSQL_READONLY_TESTACCOUNT_TOKEN; - const nosqlContainerCopyRbacToken = process.env.NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN; - const tableRbacToken = process.env.TABLE_TESTACCOUNT_TOKEN; - const gremlinRbacToken = process.env.GREMLIN_TESTACCOUNT_TOKEN; - const cassandraRbacToken = process.env.CASSANDRA_TESTACCOUNT_TOKEN; - const mongoRbacToken = process.env.MONGO_TESTACCOUNT_TOKEN; - const mongo32RbacToken = process.env.MONGO32_TESTACCOUNT_TOKEN; - const mongoReadOnlyRbacToken = process.env.MONGO_READONLY_TESTACCOUNT_TOKEN; - - switch (accountType) { - case TestAccount.SQL: - if (nosqlRbacToken) { - params.set("nosqlRbacToken", nosqlRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.SQLContainerCopyOnly: - if (nosqlContainerCopyRbacToken) { - params.set("nosqlRbacToken", nosqlContainerCopyRbacToken); - params.set("enableaaddataplane", "true"); - } - if (enablecontainercopy) { - params.set("enablecontainercopy", "true"); - } - break; - - case TestAccount.SQLReadOnly: - if (nosqlReadOnlyRbacToken) { - params.set("nosqlReadOnlyRbacToken", nosqlReadOnlyRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.Tables: - if (tableRbacToken) { - params.set("tableRbacToken", tableRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.Gremlin: - if (gremlinRbacToken) { - params.set("gremlinRbacToken", gremlinRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.Cassandra: - if (cassandraRbacToken) { - params.set("cassandraRbacToken", cassandraRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.Mongo: - if (mongoRbacToken) { - params.set("mongoRbacToken", mongoRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.Mongo32: - if (mongo32RbacToken) { - params.set("mongo32RbacToken", mongo32RbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.MongoReadonly: - if (mongoReadOnlyRbacToken) { - params.set("mongoReadOnlyRbacToken", mongoReadOnlyRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - } - - if (iframeSrc) { - params.set("iframeSrc", iframeSrc); - } - - return `https://localhost:1234/testExplorer.html?${params.toString()}`; -} - -type DropdownItemExpectations = { - ariaLabel?: string; - itemCount?: number; -}; - -type DropdownItemMatcher = { - name?: string; - position?: number; -}; - -export async function getDropdownItemByNameOrPosition( - frame: Frame, - matcher?: DropdownItemMatcher, - expectedOptions?: DropdownItemExpectations, -): Promise { - const dropdownItemsWrapper = frame.locator("div.ms-Dropdown-items"); - if (expectedOptions?.ariaLabel) { - expect(await dropdownItemsWrapper.getAttribute("aria-label")).toEqual(expectedOptions.ariaLabel); - } - if (expectedOptions?.itemCount) { - const items = dropdownItemsWrapper.locator("button.ms-Dropdown-item[role='option']"); - await expect(items).toHaveCount(expectedOptions.itemCount); - } - const containerDropdownItems = dropdownItemsWrapper.locator("button.ms-Dropdown-item[role='option']"); - if (matcher?.name) { - return containerDropdownItems.filter({ hasText: matcher.name }); - } else if (matcher?.position !== undefined) { - return containerDropdownItems.nth(matcher.position); - } - // Return first item if no matcher is provided - return containerDropdownItems.first(); -} - -/** Helper class that provides locator methods for TreeNode elements, on top of a Locator */ -class TreeNode { - constructor( - public element: Locator, - public frame: Frame, - public id: string, - ) {} - - async openContextMenu(): Promise { - await this.element.click({ button: "right" }); - } - - contextMenuItem(name: string): Locator { - return this.frame.getByTestId(`TreeNode/ContextMenuItem:${name}`); - } - - async expand(): Promise { - const treeNodeContainer = this.frame.getByTestId(`TreeNodeContainer:${this.id}`); - const tree = this.frame.getByTestId(`Tree:${this.id}`); - - // eslint-disable-next-line prefer-arrow/prefer-arrow-functions - const expandNode = async () => { - if ((await treeNodeContainer.getAttribute("aria-expanded")) !== "true") { - // Click the node, to trigger loading and expansion - await this.element.click(); - } - - // Try three times to wait for the node to expand. - for (let i = 0; i < RETRY_COUNT; i++) { - try { - // Use a longer timeout (30s) since expanding may require loading children from the server - await tree.waitFor({ state: "visible", timeout: 30 * 1000 }); - // The tree has expanded, let's get out of here - return true; - } catch { - // Just try again - if ((await treeNodeContainer.getAttribute("aria-expanded")) !== "true") { - // We might have collapsed the node, try expanding it again, then retry. - await this.element.click(); - } - } - } - return false; - }; - - if (await expandNode()) { - return; - } - - // The tree never expanded. OR, it may have expanded in between when we found the "ExpandIcon" and when we clicked it (it's happened before) - // So, let's try one more time to expand it. - if (!(await expandNode())) { - // The tree never expanded. This is a problem. - throw new Error(`Node ${this.id} did not expand after clicking it.`); - } - - // We did it. It took a lot of weird messing around, but we expanded a tree node... I hope. - } -} - -export class Editor { - constructor( - public frame: Frame, - public locator: Locator, - ) {} - - text(): Promise { - return this.locator.evaluate((e) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const win = e.ownerDocument.defaultView as any; - if (win._monaco_getEditorContentForElement) { - return win._monaco_getEditorContentForElement(e); - } - return null; - }); - } - - async setText(text: string): Promise { - // We trust that Monaco can handle the keyboard, and it's _extremely_ flaky to try and enter text using browser commands. - // So we use a hook we installed in 'window' to set the content of the editor. - - // NOTE: This function is serialized and sent to the browser for execution - // So you can't use any variables from the outer scope, but we can send a string (via the second argument to evaluate) - await this.locator.evaluate((e, content) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const win = e.ownerDocument.defaultView as any; - if (win._monaco_setEditorContentForElement) { - win._monaco_setEditorContentForElement(e, content); - } - }, text); - - expect(await this.text()).toEqual(text); - } -} - -export class QueryTab { - resultsPane: Locator; - resultsView: Locator; - executeCTA: Locator; - errorList: Locator; - queryStatsList: Locator; - resultsEditor: Editor; - resultsTab: Locator; - queryStatsTab: Locator; - constructor( - public frame: Frame, - public tabId: string, - public tab: Locator, - public locator: Locator, - ) { - this.resultsPane = locator.getByTestId("QueryTab/ResultsPane"); - this.resultsView = locator.getByTestId("QueryTab/ResultsPane/ResultsView"); - this.executeCTA = locator.getByTestId("QueryTab/ResultsPane/ExecuteCTA"); - this.errorList = locator.getByTestId("QueryTab/ResultsPane/ErrorList"); - this.resultsEditor = new Editor(this.frame, this.resultsView.getByTestId("EditorReact/Host/Loaded")); - this.queryStatsList = locator.getByTestId("QueryTab/ResultsPane/ResultsView/QueryStatsList"); - this.resultsTab = this.resultsView.getByTestId("QueryTab/ResultsPane/ResultsView/ResultsTab"); - this.queryStatsTab = this.resultsView.getByTestId("QueryTab/ResultsPane/ResultsView/QueryStatsTab"); - } - - editor(): Editor { - const locator = this.locator.getByTestId("EditorReact/Host/Loaded"); - return new Editor(this.frame, locator); - } -} - -export class DocumentsTab { - documentsFilter: Locator; - documentsListPane: Locator; - documentResultsPane: Locator; - resultsEditor: Editor; - loadMoreButton: Locator; - filterInput: Locator; - filterButton: Locator; - - constructor( - public frame: Frame, - public tabId: string, - public tab: Locator, - public locator: Locator, - ) { - this.documentsFilter = this.locator.getByTestId("DocumentsTab/Filter"); - this.documentsListPane = this.locator.getByTestId("DocumentsTab/DocumentsPane"); - this.documentResultsPane = this.locator.getByTestId("DocumentsTab/ResultsPane"); - this.resultsEditor = new Editor(this.frame, this.documentResultsPane.getByTestId("EditorReact/Host/Loaded")); - this.loadMoreButton = this.documentsListPane.getByTestId("DocumentsTab/LoadMore"); - this.filterInput = this.documentsFilter.getByTestId("DocumentsTab/FilterInput"); - this.filterButton = this.documentsFilter.getByTestId("DocumentsTab/ApplyFilter"); - } - - async setFilter(text: string) { - await this.filterInput.fill(text); - } -} - -type PanelOpenOptions = { - closeTimeout?: number; -}; - -export enum CommandBarButton { - Save = "Save", - Delete = "Delete", - Execute = "Execute", - ExecuteQuery = "Execute Query", - UploadItem = "Upload Item", - NewDocument = "New Document", -} - -/** Helper class that provides locator methods for DataExplorer components, on top of a Frame */ -export class DataExplorer { - constructor(public frame: Frame) {} - - tab(tabId: string): Locator { - return this.frame.getByTestId(`Tab:${tabId}`); - } - - queryTab(tabId: string): QueryTab { - const tab = this.tab(tabId); - const queryTab = tab.getByTestId("QueryTab"); - return new QueryTab(this.frame, tabId, tab, queryTab); - } - - documentsTab(tabId: string): DocumentsTab { - const tab = this.tab(tabId); - const documentsTab = tab.getByTestId("DocumentsTab"); - return new DocumentsTab(this.frame, tabId, tab, documentsTab); - } - - /** Select the primary global command button. - * - * There's only a single "primary" button, but we still require you to pass the label to confirm you're selecting the right button. - */ - async globalCommandButton(label: string): Promise { - await this.frame.getByTestId("GlobalCommands").click(); - return this.frame.getByRole("menuitem", { name: label }); - } - - /** Select the command bar button with the specified label */ - commandBarButton(commandBarButton: CommandBarButton): Locator { - return this.frame.getByTestId(`CommandBar/Button:${commandBarButton}`).and(this.frame.locator("css=button")); - } - - dialogButton(label: string): Locator { - return this.frame.getByTestId(`DialogButton:${label}`).and(this.frame.locator("css=button")); - } - - /** Select the side panel with the specified title */ - panel(title: string): Locator { - return this.frame.getByTestId(`Panel:${title}`); - } - - async waitForNode(treeNodeId: string): Promise { - const node = this.treeNode(treeNodeId); - - // Is the node already visible? - if (await node.element.isVisible()) { - return node; - } - - // No, try refreshing the tree - const refreshButton = this.frame.getByTestId("Sidebar/RefreshButton"); - await refreshButton.click(); - - // Try a few times to find the node - for (let i = 0; i < RETRY_COUNT; i++) { - try { - await node.element.waitFor(); - return node; - } catch { - // Just try again - } - } - - // We tried 3 times, but the node never appeared - throw new Error(`Node ${treeNodeId} not found and did not appear after refreshing.`); - } - - async waitForContainerNode(databaseId: string, containerId: string): Promise { - const databaseNode = await this.waitForNode(databaseId); - - // The container node may be auto-expanded. Wait 5s for that to happen - try { - const containerNode = this.treeNode(`${databaseId}/${containerId}`); - await containerNode.element.waitFor({ state: "visible", timeout: 5 * 1000 }); - return containerNode; - } catch { - // It didn't auto-expand, that's fine, we'll expand it ourselves - } - - // Ok, expand the database node. - await databaseNode.expand(); - - return await this.waitForNode(`${databaseId}/${containerId}`); - } - - async waitForContainerItemsNode(databaseId: string, containerId: string): Promise { - return await this.waitForNode(`${databaseId}/${containerId}/Items`); - } - - async waitForContainerDocumentsNode(databaseId: string, containerId: string): Promise { - return await this.waitForNode(`${databaseId}/${containerId}/Documents`); - } - - async waitForCommandBarButton(label: CommandBarButton, timeout?: number): Promise { - const commandBar = this.commandBarButton(label); - await commandBar.waitFor({ state: "visible", timeout }); - return commandBar; - } - - async waitForDialogButton(label: string, timeout?: number): Promise { - const dialogButton = this.dialogButton(label); - await dialogButton.waitFor({ timeout }); - return dialogButton; - } - - /** Select the tree node with the specified id */ - treeNode(id: string): TreeNode { - return new TreeNode(this.frame.getByTestId(`TreeNode:${id}`), this.frame, id); - } - - /** Waits for the panel with the specified title to be open, then runs the provided callback. After the callback completes, waits for the panel to close. */ - async whilePanelOpen( - title: string, - action: (panel: Locator, okButton: Locator) => Promise, - options?: PanelOpenOptions, - ): Promise { - options ||= {}; - - const panel = this.panel(title); - await panel.waitFor(); - const okButton = panel.getByTestId("Panel/OkButton"); - await action(panel, okButton); - 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 */ - getConsoleHeaderStatus(): Locator { - return this.frame.getByTestId("notification-console/header-status"); - } - - async expandNotificationConsole(): Promise { - await this.setNotificationConsoleExpanded(true); - } - - async collapseNotificationConsole(): Promise { - await this.setNotificationConsoleExpanded(false); - } - - async setNotificationConsoleExpanded(expanded: boolean): Promise { - const notificationConsoleToggleButton = this.frame.getByTestId("NotificationConsole/ExpandCollapseButton"); - const alt = await notificationConsoleToggleButton.locator("img").getAttribute("alt"); - - // When expanded, the icon says "Collapse icon" - if (expanded && alt === "Expand icon") { - await notificationConsoleToggleButton.click(); - } else if (!expanded && alt === "Collapse icon") { - await notificationConsoleToggleButton.click(); - } - } - - async getNotificationConsoleMessages(): Promise { - await this.setNotificationConsoleExpanded(true); - return this.frame.getByTestId("NotificationConsole/Contents"); - } - - async getDropdownItemByName(name: string, ariaLabel?: string): Promise { - const dropdownItemsWrapper = this.frame.locator("div.ms-Dropdown-items"); - if (ariaLabel) { - expect(await dropdownItemsWrapper.getAttribute("aria-label")).toEqual(ariaLabel); - } - const containerDropdownItems = dropdownItemsWrapper.locator("button.ms-Dropdown-item[role='option']"); - return containerDropdownItems.filter({ hasText: name }); - } - - /** Waits for the Data Explorer app to load */ - static async waitForExplorer(page: Page, options?: TestExplorerUrlOptions): Promise { - const iframeElement = await page.getByTestId("DataExplorerFrame").elementHandle(); - if (iframeElement === null) { - throw new Error("Explorer iframe not found"); - } - - const explorerFrame = await iframeElement.contentFrame(); - - if (explorerFrame === null) { - throw new Error("Explorer frame not found"); - } - - if (!options?.enablecontainercopy) { - await explorerFrame?.getByTestId("DataExplorerRoot").waitFor(); - } - - return new DataExplorer(explorerFrame); - } - - /** Opens the Data Explorer app using the specified test account (and optionally, the provided IFRAME src url). */ - static async open(page: Page, testAccount: TestAccount, iframeSrc?: string): Promise { - const url = await getTestExplorerUrl(testAccount, { iframeSrc }); - await page.goto(url); - return DataExplorer.waitForExplorer(page); - } -} - -export async function waitForApiResponse( - page: Page, - urlPattern: string, - method?: string, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - payloadValidator?: (payload: any) => boolean, -) { - try { - // Check if page is still valid before waiting - if (page.isClosed()) { - throw new Error(`Page is closed, cannot wait for API response: ${urlPattern}`); - } - - return page.waitForResponse( - async (response) => { - const request = response.request(); - - if (!request.url().includes(urlPattern)) { - return false; - } - - if (method && request.method() !== method) { - return false; - } - - if (payloadValidator && (request.method() === "POST" || request.method() === "PUT")) { - const postData = request.postData(); - if (postData) { - try { - const payload = JSON.parse(postData); - return payloadValidator(payload); - } catch { - return false; - } - } - } - return true; - }, - { timeout: 60 * 1000 }, - ); - } catch (error) { - if (error instanceof Error && error.message.includes("Target page, context or browser has been closed")) { - console.warn("Page was closed while waiting for API response:", urlPattern); - throw new Error(`Page closed while waiting for API response: ${urlPattern}`); - } - throw error; - } -} -export async function interceptAndInspectApiRequest( - page: Page, - urlPattern: string, - method: string = "POST", - error: Error, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - errorValidator: (url?: string, payload?: any) => boolean, -): Promise { - await page.route( - (url) => url.pathname.includes(urlPattern), - async (route, request) => { - if (request.method() !== method) { - await route.continue(); - return; - } - const postData = request.postData(); - if (postData) { - try { - const payload = JSON.parse(postData); - if (errorValidator && errorValidator(request.url(), payload)) { - await route.fulfill({ - status: 409, - contentType: "application/json", - body: JSON.stringify({ - code: "Conflict", - message: error.message, - }), - }); - return; - } - } catch (err) { - if (err instanceof Error && err.message.includes("not allowed")) { - throw err; - } - } - } - - await route.continue(); - }, - ); -} - -export class ContainerCopy { - constructor( - public frame: Frame, - public wrapper: Locator, - ) {} - - static async waitForContainerCopy(page: Page): Promise { - const explorerFrame = await DataExplorer.waitForExplorer(page, { enablecontainercopy: true }); - const containerCopyWrapper = explorerFrame.frame.locator("div#containerCopyWrapper"); - return new ContainerCopy(explorerFrame.frame, containerCopyWrapper); - } - - static async open(page: Page, testAccount: TestAccount, iframeSrc?: string): Promise { - const url = await getTestExplorerUrl(testAccount, { iframeSrc, enablecontainercopy: true }); - await page.goto(url); - return ContainerCopy.waitForContainerCopy(page); - } -} diff --git a/.vs/CopilotSnapshots/548E56D5117B4249B654F2DE5EA27C25/F622CD2C03D62348869FA84BB1647168/06FFEB74F9428A8CCD2BF1741B12A42C b/.vs/CopilotSnapshots/548E56D5117B4249B654F2DE5EA27C25/F622CD2C03D62348869FA84BB1647168/06FFEB74F9428A8CCD2BF1741B12A42C deleted file mode 100644 index 906282863..000000000 --- a/.vs/CopilotSnapshots/548E56D5117B4249B654F2DE5EA27C25/F622CD2C03D62348869FA84BB1647168/06FFEB74F9428A8CCD2BF1741B12A42C +++ /dev/null @@ -1,229 +0,0 @@ -import { Locator, expect, test } from "@playwright/test"; -import { - CommandBarButton, - DataExplorer, - ONE_MINUTE_MS, - TEST_AUTOSCALE_MAX_THROUGHPUT_RU_4K, - TEST_MANUAL_THROUGHPUT_RU, - TestAccount, -} from "../../fx"; -import { TestDatabaseContext, createTestDB } from "../../testData"; - -test.describe("Database with Shared Throughput", () => { - let dbContext: TestDatabaseContext = null!; - let explorer: DataExplorer = null!; - const containerId = "sharedcontainer"; - - // Helper methods - const getThroughputInput = (type: "manual" | "autopilot"): Locator => { - return explorer.frame.getByTestId(`${type}-throughput-input`); - }; - - test.afterEach("Delete Test Database", async () => { - await dbContext?.dispose(); - }); - - test.describe("Manual Throughput Tests", () => { - test.beforeEach(async ({ page }) => { - explorer = await DataExplorer.open(page, TestAccount.SQL); - }); - - test("Create database with shared manual throughput and verify Scale node in UI", async () => { - test.setTimeout(120000); // 2 minutes timeout - // Create database with shared manual throughput (400 RU/s) - dbContext = await createTestDB({ throughput: 400 }); - - // Verify database node appears in the tree - const databaseNode = await explorer.waitForNode(dbContext.database.id); - expect(databaseNode).toBeDefined(); - - // Expand the database node to see child nodes - await databaseNode.expand(); - - // Verify that "Scale" node appears under the database - const scaleNode = await explorer.waitForNode(`${dbContext.database.id}/Scale`); - expect(scaleNode).toBeDefined(); - await expect(scaleNode.element).toBeVisible(); - }); - - test("Add container to shared database without dedicated throughput", async () => { - // Create database with shared manual throughput - dbContext = await createTestDB({ throughput: 400 }); - - // Wait for the database to appear in the tree - await explorer.waitForNode(dbContext.database.id); - - // Add a container to the shared database via UI - const newContainerButton = await explorer.globalCommandButton("New Container"); - await newContainerButton.click(); - - await explorer.whilePanelOpen( - "New Container", - async (panel, okButton) => { - // Select "Use existing" database - const useExistingRadio = panel.getByRole("radio", { name: /Use existing/i }); - await useExistingRadio.click(); - - // Select the database from dropdown using the new data-testid - const databaseDropdown = panel.getByRole("combobox", { name: "Choose an existing database" }); - await databaseDropdown.click(); - - await explorer.frame.getByRole("option", { name: dbContext.database.id }).click(); - // Now you can target the specific database option by its data-testid - //await panel.getByTestId(`database-option-${dbContext.database.id}`).click(); - // Fill container id - await panel.getByRole("textbox", { name: "Container id, Example Container1" }).fill(containerId); - - // Fill partition key - await panel.getByRole("textbox", { name: "Partition key" }).fill("/pk"); - - // Ensure "Provision dedicated throughput" is NOT checked - const dedicatedThroughputCheckbox = panel.getByRole("checkbox", { - name: /Provision dedicated throughput for this container/i, - }); - - if (await dedicatedThroughputCheckbox.isVisible()) { - const isChecked = await dedicatedThroughputCheckbox.isChecked(); - if (isChecked) { - await dedicatedThroughputCheckbox.uncheck(); - } - } - - await okButton.click(); - }, - { closeTimeout: 5 * ONE_MINUTE_MS }, - ); - - // Verify container was created under the database - const containerNode = await explorer.waitForContainerNode(dbContext.database.id, containerId); - expect(containerNode).toBeDefined(); - }); - - test("Scale shared database manual throughput", async () => { - // Create database with shared manual throughput (400 RU/s) - dbContext = await createTestDB({ throughput: 400 }); - - // Navigate to the scale settings by clicking the "Scale" node in the tree - const databaseNode = await explorer.waitForNode(dbContext.database.id); - await databaseNode.expand(); - const scaleNode = await explorer.waitForNode(`${dbContext.database.id}/Scale`); - await scaleNode.element.click(); - - // Update manual throughput from 400 to 800 - await getThroughputInput("manual").fill(TEST_MANUAL_THROUGHPUT_RU.toString()); - - // Save changes - await explorer.commandBarButton(CommandBarButton.Save).click(); - - // Verify success message - await expect(explorer.getConsoleHeaderStatus()).toContainText( - `Successfully updated offer for database ${dbContext.database.id}`, - { - timeout: 2 * ONE_MINUTE_MS, - }, - ); - }); - - test("Scale shared database from manual to autoscale", async () => { - // Create database with shared manual throughput (400 RU/s) - dbContext = await createTestDB({ throughput: 400 }); - - // Open database settings by clicking the "Scale" node - const databaseNode = await explorer.waitForNode(dbContext.database.id); - await databaseNode.expand(); - const scaleNode = await explorer.waitForNode(`${dbContext.database.id}/Scale`); - await scaleNode.element.click(); - - // Switch to Autoscale - const autoscaleRadio = explorer.frame.getByText("Autoscale", { exact: true }); - await autoscaleRadio.click(); - - // Set autoscale max throughput to 1000 - //await getThroughputInput("autopilot").fill(TEST_AUTOSCALE_THROUGHPUT_RU.toString()); - - // Save changes - await explorer.commandBarButton(CommandBarButton.Save).click(); - - await expect(explorer.getConsoleHeaderStatus()).toContainText( - `Successfully updated offer for database ${dbContext.database.id}`, - { - timeout: 2 * ONE_MINUTE_MS, - }, - ); - }); - }); - - test.describe("Autoscale Throughput Tests", () => { - test.beforeEach(async ({ page }) => { - explorer = await DataExplorer.open(page, TestAccount.SQL); - }); - - test("Create database with shared autoscale throughput and verify Scale node in UI", async () => { - test.setTimeout(120000); // 2 minutes timeout - - // Create database with shared autoscale throughput (max 1000 RU/s) - dbContext = await createTestDB({ maxThroughput: 1000 }); - - // Verify database node appears - const databaseNode = await explorer.waitForNode(dbContext.database.id); - expect(databaseNode).toBeDefined(); - - // Expand the database node to see child nodes - await databaseNode.expand(); - - // Verify that "Scale" node appears under the database - const scaleNode = await explorer.waitForNode(`${dbContext.database.id}/Scale`); - expect(scaleNode).toBeDefined(); - await expect(scaleNode.element).toBeVisible(); - }); - - test("Scale shared database autoscale throughput", async () => { - // Create database with shared autoscale throughput (max 1000 RU/s) - dbContext = await createTestDB({ maxThroughput: 1000 }); - - // Open database settings - const databaseNode = await explorer.waitForNode(dbContext.database.id); - await databaseNode.expand(); - const scaleNode = await explorer.waitForNode(`${dbContext.database.id}/Scale`); - await scaleNode.element.click(); - - // Update autoscale max throughput from 1000 to 4000 - await getThroughputInput("autopilot").fill(TEST_AUTOSCALE_MAX_THROUGHPUT_RU_4K.toString()); - - // Save changes - await explorer.commandBarButton(CommandBarButton.Save).click(); - - // Verify success message - await expect(explorer.getConsoleHeaderStatus()).toContainText( - `Successfully updated offer for database ${dbContext.database.id}`, - { - timeout: 2 * ONE_MINUTE_MS, - }, - ); - }); - - test("Scale shared database from autoscale to manual", async () => { - // Create database with shared autoscale throughput (max 1000 RU/s) - dbContext = await createTestDB({ maxThroughput: 1000 }); - - // Open database settings - const databaseNode = await explorer.waitForNode(dbContext.database.id); - await databaseNode.expand(); - const scaleNode = await explorer.waitForNode(`${dbContext.database.id}/Scale`); - await scaleNode.element.click(); - - // Switch to Manual - const manualRadio = explorer.frame.getByText("Manual", { exact: true }); - await manualRadio.click(); - - // Save changes - await explorer.commandBarButton(CommandBarButton.Save).click(); - - // Verify success message - await expect(explorer.getConsoleHeaderStatus()).toContainText( - `Successfully updated offer for database ${dbContext.database.id}`, - { timeout: 2 * ONE_MINUTE_MS }, - ); - }); - }); -}); diff --git a/.vs/CopilotSnapshots/548E56D5117B4249B654F2DE5EA27C25/F622CD2C03D62348869FA84BB1647168/39D77E4DA1804736E39178139E5048A6 b/.vs/CopilotSnapshots/548E56D5117B4249B654F2DE5EA27C25/F622CD2C03D62348869FA84BB1647168/39D77E4DA1804736E39178139E5048A6 deleted file mode 100644 index 906282863..000000000 --- a/.vs/CopilotSnapshots/548E56D5117B4249B654F2DE5EA27C25/F622CD2C03D62348869FA84BB1647168/39D77E4DA1804736E39178139E5048A6 +++ /dev/null @@ -1,229 +0,0 @@ -import { Locator, expect, test } from "@playwright/test"; -import { - CommandBarButton, - DataExplorer, - ONE_MINUTE_MS, - TEST_AUTOSCALE_MAX_THROUGHPUT_RU_4K, - TEST_MANUAL_THROUGHPUT_RU, - TestAccount, -} from "../../fx"; -import { TestDatabaseContext, createTestDB } from "../../testData"; - -test.describe("Database with Shared Throughput", () => { - let dbContext: TestDatabaseContext = null!; - let explorer: DataExplorer = null!; - const containerId = "sharedcontainer"; - - // Helper methods - const getThroughputInput = (type: "manual" | "autopilot"): Locator => { - return explorer.frame.getByTestId(`${type}-throughput-input`); - }; - - test.afterEach("Delete Test Database", async () => { - await dbContext?.dispose(); - }); - - test.describe("Manual Throughput Tests", () => { - test.beforeEach(async ({ page }) => { - explorer = await DataExplorer.open(page, TestAccount.SQL); - }); - - test("Create database with shared manual throughput and verify Scale node in UI", async () => { - test.setTimeout(120000); // 2 minutes timeout - // Create database with shared manual throughput (400 RU/s) - dbContext = await createTestDB({ throughput: 400 }); - - // Verify database node appears in the tree - const databaseNode = await explorer.waitForNode(dbContext.database.id); - expect(databaseNode).toBeDefined(); - - // Expand the database node to see child nodes - await databaseNode.expand(); - - // Verify that "Scale" node appears under the database - const scaleNode = await explorer.waitForNode(`${dbContext.database.id}/Scale`); - expect(scaleNode).toBeDefined(); - await expect(scaleNode.element).toBeVisible(); - }); - - test("Add container to shared database without dedicated throughput", async () => { - // Create database with shared manual throughput - dbContext = await createTestDB({ throughput: 400 }); - - // Wait for the database to appear in the tree - await explorer.waitForNode(dbContext.database.id); - - // Add a container to the shared database via UI - const newContainerButton = await explorer.globalCommandButton("New Container"); - await newContainerButton.click(); - - await explorer.whilePanelOpen( - "New Container", - async (panel, okButton) => { - // Select "Use existing" database - const useExistingRadio = panel.getByRole("radio", { name: /Use existing/i }); - await useExistingRadio.click(); - - // Select the database from dropdown using the new data-testid - const databaseDropdown = panel.getByRole("combobox", { name: "Choose an existing database" }); - await databaseDropdown.click(); - - await explorer.frame.getByRole("option", { name: dbContext.database.id }).click(); - // Now you can target the specific database option by its data-testid - //await panel.getByTestId(`database-option-${dbContext.database.id}`).click(); - // Fill container id - await panel.getByRole("textbox", { name: "Container id, Example Container1" }).fill(containerId); - - // Fill partition key - await panel.getByRole("textbox", { name: "Partition key" }).fill("/pk"); - - // Ensure "Provision dedicated throughput" is NOT checked - const dedicatedThroughputCheckbox = panel.getByRole("checkbox", { - name: /Provision dedicated throughput for this container/i, - }); - - if (await dedicatedThroughputCheckbox.isVisible()) { - const isChecked = await dedicatedThroughputCheckbox.isChecked(); - if (isChecked) { - await dedicatedThroughputCheckbox.uncheck(); - } - } - - await okButton.click(); - }, - { closeTimeout: 5 * ONE_MINUTE_MS }, - ); - - // Verify container was created under the database - const containerNode = await explorer.waitForContainerNode(dbContext.database.id, containerId); - expect(containerNode).toBeDefined(); - }); - - test("Scale shared database manual throughput", async () => { - // Create database with shared manual throughput (400 RU/s) - dbContext = await createTestDB({ throughput: 400 }); - - // Navigate to the scale settings by clicking the "Scale" node in the tree - const databaseNode = await explorer.waitForNode(dbContext.database.id); - await databaseNode.expand(); - const scaleNode = await explorer.waitForNode(`${dbContext.database.id}/Scale`); - await scaleNode.element.click(); - - // Update manual throughput from 400 to 800 - await getThroughputInput("manual").fill(TEST_MANUAL_THROUGHPUT_RU.toString()); - - // Save changes - await explorer.commandBarButton(CommandBarButton.Save).click(); - - // Verify success message - await expect(explorer.getConsoleHeaderStatus()).toContainText( - `Successfully updated offer for database ${dbContext.database.id}`, - { - timeout: 2 * ONE_MINUTE_MS, - }, - ); - }); - - test("Scale shared database from manual to autoscale", async () => { - // Create database with shared manual throughput (400 RU/s) - dbContext = await createTestDB({ throughput: 400 }); - - // Open database settings by clicking the "Scale" node - const databaseNode = await explorer.waitForNode(dbContext.database.id); - await databaseNode.expand(); - const scaleNode = await explorer.waitForNode(`${dbContext.database.id}/Scale`); - await scaleNode.element.click(); - - // Switch to Autoscale - const autoscaleRadio = explorer.frame.getByText("Autoscale", { exact: true }); - await autoscaleRadio.click(); - - // Set autoscale max throughput to 1000 - //await getThroughputInput("autopilot").fill(TEST_AUTOSCALE_THROUGHPUT_RU.toString()); - - // Save changes - await explorer.commandBarButton(CommandBarButton.Save).click(); - - await expect(explorer.getConsoleHeaderStatus()).toContainText( - `Successfully updated offer for database ${dbContext.database.id}`, - { - timeout: 2 * ONE_MINUTE_MS, - }, - ); - }); - }); - - test.describe("Autoscale Throughput Tests", () => { - test.beforeEach(async ({ page }) => { - explorer = await DataExplorer.open(page, TestAccount.SQL); - }); - - test("Create database with shared autoscale throughput and verify Scale node in UI", async () => { - test.setTimeout(120000); // 2 minutes timeout - - // Create database with shared autoscale throughput (max 1000 RU/s) - dbContext = await createTestDB({ maxThroughput: 1000 }); - - // Verify database node appears - const databaseNode = await explorer.waitForNode(dbContext.database.id); - expect(databaseNode).toBeDefined(); - - // Expand the database node to see child nodes - await databaseNode.expand(); - - // Verify that "Scale" node appears under the database - const scaleNode = await explorer.waitForNode(`${dbContext.database.id}/Scale`); - expect(scaleNode).toBeDefined(); - await expect(scaleNode.element).toBeVisible(); - }); - - test("Scale shared database autoscale throughput", async () => { - // Create database with shared autoscale throughput (max 1000 RU/s) - dbContext = await createTestDB({ maxThroughput: 1000 }); - - // Open database settings - const databaseNode = await explorer.waitForNode(dbContext.database.id); - await databaseNode.expand(); - const scaleNode = await explorer.waitForNode(`${dbContext.database.id}/Scale`); - await scaleNode.element.click(); - - // Update autoscale max throughput from 1000 to 4000 - await getThroughputInput("autopilot").fill(TEST_AUTOSCALE_MAX_THROUGHPUT_RU_4K.toString()); - - // Save changes - await explorer.commandBarButton(CommandBarButton.Save).click(); - - // Verify success message - await expect(explorer.getConsoleHeaderStatus()).toContainText( - `Successfully updated offer for database ${dbContext.database.id}`, - { - timeout: 2 * ONE_MINUTE_MS, - }, - ); - }); - - test("Scale shared database from autoscale to manual", async () => { - // Create database with shared autoscale throughput (max 1000 RU/s) - dbContext = await createTestDB({ maxThroughput: 1000 }); - - // Open database settings - const databaseNode = await explorer.waitForNode(dbContext.database.id); - await databaseNode.expand(); - const scaleNode = await explorer.waitForNode(`${dbContext.database.id}/Scale`); - await scaleNode.element.click(); - - // Switch to Manual - const manualRadio = explorer.frame.getByText("Manual", { exact: true }); - await manualRadio.click(); - - // Save changes - await explorer.commandBarButton(CommandBarButton.Save).click(); - - // Verify success message - await expect(explorer.getConsoleHeaderStatus()).toContainText( - `Successfully updated offer for database ${dbContext.database.id}`, - { timeout: 2 * ONE_MINUTE_MS }, - ); - }); - }); -}); diff --git a/.vs/CopilotSnapshots/548E56D5117B4249B654F2DE5EA27C25/F622CD2C03D62348869FA84BB1647168/4A297F95ADD1E83DB8F022AF3B1B1E74 b/.vs/CopilotSnapshots/548E56D5117B4249B654F2DE5EA27C25/F622CD2C03D62348869FA84BB1647168/4A297F95ADD1E83DB8F022AF3B1B1E74 deleted file mode 100644 index 9222cb557..000000000 --- a/.vs/CopilotSnapshots/548E56D5117B4249B654F2DE5EA27C25/F622CD2C03D62348869FA84BB1647168/4A297F95ADD1E83DB8F022AF3B1B1E74 +++ /dev/null @@ -1,230 +0,0 @@ -import { Locator, expect, test } from "@playwright/test"; -import { - CommandBarButton, - DataExplorer, - ONE_MINUTE_MS, - TEST_AUTOSCALE_MAX_THROUGHPUT_RU_4K, - TEST_MANUAL_THROUGHPUT_RU, - TestAccount, -} from "../../fx"; -import { TestDatabaseContext, createTestDB } from "../../testData"; - -test.describe("Database with Shared Throughput", () => { - let dbContext: TestDatabaseContext = null!; - let explorer: DataExplorer = null!; - const containerId = "sharedcontainer"; - - // Helper methods - const getThroughputInput = (type: "manual" | "autopilot"): Locator => { - return explorer.frame.getByTestId(`${type}-throughput-input`); - }; - - test.afterEach("Delete Test Database", async () => { - await dbContext?.dispose(); - }); - - test.describe("Manual Throughput Tests", () => { - test.beforeEach(async ({ page }) => { - explorer = await DataExplorer.open(page, TestAccount.SQL); - }); - - test("Create database with shared manual throughput and verify Scale node in UI", async () => { - test.setTimeout(120000); // 2 minutes timeout - // Create database with shared manual throughput (400 RU/s) - dbContext = await createTestDB({ throughput: 400 }); - - // Verify database node appears in the tree - const databaseNode = await explorer.waitForNode(dbContext.database.id); - expect(databaseNode).toBeDefined(); - - // Expand the database node to see child nodes - await databaseNode.expand(); - - // Verify that "Scale" node appears under the database - const scaleNode = await explorer.waitForNode(`${dbContext.database.id}/Scale`); - expect(scaleNode).toBeDefined(); - await expect(scaleNode.element).toBeVisible(); - }); - - test("Add container to shared database without dedicated throughput", async () => { - // Create database with shared manual throughput - dbContext = await createTestDB({ throughput: 400 }); - - // Wait for the database to appear in the tree - await explorer.waitForNode(dbContext.database.id); - - // Add a container to the shared database via UI - const newContainerButton = await explorer.globalCommandButton("New Container"); - await newContainerButton.click(); - - await explorer.whilePanelOpen( - "New Container", - async (panel, okButton) => { - // Select "Use existing" database - const useExistingRadio = panel.getByRole("radio", { name: /Use existing/i }); - await useExistingRadio.click(); - - // Select the database from dropdown using the new data-testid - const databaseDropdown = panel.getByRole("combobox", { name: "Choose an existing database" }); - await databaseDropdown.click(); - - await explorer.frame.getByRole("option", { name: dbContext.database.id }).click(); - // Now you can target the specific database option by its data-testid - //await panel.getByTestId(`database-option-${dbContext.database.id}`).click(); - // Fill container id - await panel.getByRole("textbox", { name: "Container id, Example Container1" }).fill(containerId); - - // Fill partition key - await panel.getByRole("textbox", { name: "Partition key" }).fill("/pk"); - - // Ensure "Provision dedicated throughput" is NOT checked - const dedicatedThroughputCheckbox = panel.getByRole("checkbox", { - name: /Provision dedicated throughput for this container/i, - }); - - if (await dedicatedThroughputCheckbox.isVisible()) { - const isChecked = await dedicatedThroughputCheckbox.isChecked(); - if (isChecked) { - await dedicatedThroughputCheckbox.uncheck(); - } - } - - await okButton.click(); - }, - { closeTimeout: 5 * ONE_MINUTE_MS }, - ); - - // Verify container was created under the database - const containerNode = await explorer.waitForContainerNode(dbContext.database.id, containerId); - expect(containerNode).toBeDefined(); - }); - - test("Scale shared database manual throughput", async () => { - // Create database with shared manual throughput (400 RU/s) - dbContext = await createTestDB({ throughput: 400 }); - - // Navigate to the scale settings by clicking the "Scale" node in the tree - const databaseNode = await explorer.waitForNode(dbContext.database.id); - await databaseNode.expand(); - const scaleNode = await explorer.waitForNode(`${dbContext.database.id}/Scale`); - await scaleNode.element.click(); - - // Update manual throughput from 400 to 800 - await getThroughputInput("manual").fill(TEST_MANUAL_THROUGHPUT_RU.toString()); - - // Save changes - await explorer.commandBarButton(CommandBarButton.Save).click(); - - // Verify success message - await expect(explorer.getConsoleHeaderStatus()).toContainText( - `Successfully updated offer for database ${dbContext.database.id}`, - { - timeout: 2 * ONE_MINUTE_MS, - }, - ); - }); - - test("Scale shared database from manual to autoscale", async () => { - // Create database with shared manual throughput (400 RU/s) - dbContext = await createTestDB({ throughput: 400 }); - - // Open database settings by clicking the "Scale" node - const databaseNode = await explorer.waitForNode(dbContext.database.id); - await databaseNode.expand(); - const scaleNode = await explorer.waitForNode(`${dbContext.database.id}/Scale`); - await scaleNode.element.click(); - - // Switch to Autoscale - const autoscaleRadio = explorer.frame.getByText("Autoscale", { exact: true }); - await autoscaleRadio.click(); - - // Set autoscale max throughput to 1000 - //await getThroughputInput("autopilot").fill(TEST_AUTOSCALE_THROUGHPUT_RU.toString()); - - // Save changes - await explorer.commandBarButton(CommandBarButton.Save).click(); - - await expect(explorer.getConsoleHeaderStatus()).toContainText( - `Successfully updated offer for database ${dbContext.database.id}`, - { - timeout: 2 * ONE_MINUTE_MS, - }, - ); - }); - }); - - test.describe("Autoscale Throughput Tests", () => { - test.beforeEach(async ({ page }) => { - explorer = await DataExplorer.open(page, TestAccount.SQL); - }); - - test("Create database with shared autoscale throughput and verify Scale node in UI", async () => { - test.setTimeout(120000); // 2 minutes timeout - - // Create database with shared autoscale throughput (max 1000 RU/s) - dbContext = await createTestDB({ maxThroughput: 1000 }); - - // Verify database node appears - const databaseNode = await explorer.waitForNode(dbContext.database.id); - expect(databaseNode).toBeDefined(); - - // Expand the database node to see child nodes - await databaseNode.expand(); - - // Verify that "Scale" node appears under the database - const scaleNode = await explorer.waitForNode(`${dbContext.database.id}/Scale`); - expect(scaleNode).toBeDefined(); - await expect(scaleNode.element).toBeVisible(); - }); - - test("Scale shared database autoscale throughput", async () => { - // Create database with shared autoscale throughput (max 1000 RU/s) - dbContext = await createTestDB({ maxThroughput: 1000 }); - - // Open database settings - const databaseNode = await explorer.waitForNode(dbContext.database.id); - await databaseNode.expand(); - const scaleNode = await explorer.waitForNode(`${dbContext.database.id}/Scale`); - await scaleNode.element.click(); - - // Update autoscale max throughput from 1000 to 4000 - await getThroughputInput("autopilot").fill(TEST_AUTOSCALE_MAX_THROUGHPUT_RU_4K.toString()); - - // Save changes - await explorer.commandBarButton(CommandBarButton.Save).click(); - - // Verify success message - await expect(explorer.getConsoleHeaderStatus()).toContainText( - `Successfully updated offer for database ${dbContext.database.id}`, - { - timeout: 2 * ONE_MINUTE_MS, - }, - ); - }); - - test("Scale shared database from autoscale to manual", async () => { - test.setTimeout(3 * ONE_MINUTE_MS); // 3 minutes timeout - // Create database with shared autoscale throughput (max 1000 RU/s) - dbContext = await createTestDB({ maxThroughput: 1000 }); - - // Open database settings - const databaseNode = await explorer.waitForNode(dbContext.database.id); - await databaseNode.expand(); - const scaleNode = await explorer.waitForNode(`${dbContext.database.id}/Scale`); - await scaleNode.element.click(); - - // Switch to Manual - const manualRadio = explorer.frame.getByText("Manual", { exact: true }); - await manualRadio.click(); - - // Save changes - await explorer.commandBarButton(CommandBarButton.Save).click(); - - // Verify success message - await expect(explorer.getConsoleHeaderStatus()).toContainText( - `Successfully updated offer for database ${dbContext.database.id}`, - { timeout: 2 * ONE_MINUTE_MS }, - ); - }); - }); -}); diff --git a/.vs/CopilotSnapshots/548E56D5117B4249B654F2DE5EA27C25/F622CD2C03D62348869FA84BB1647168/BDC0B1042C11B58A5259E50EBB63DC01 b/.vs/CopilotSnapshots/548E56D5117B4249B654F2DE5EA27C25/F622CD2C03D62348869FA84BB1647168/BDC0B1042C11B58A5259E50EBB63DC01 deleted file mode 100644 index e9d5e2325..000000000 --- a/.vs/CopilotSnapshots/548E56D5117B4249B654F2DE5EA27C25/F622CD2C03D62348869FA84BB1647168/BDC0B1042C11B58A5259E50EBB63DC01 +++ /dev/null @@ -1,702 +0,0 @@ -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; - -export interface TestNameOptions { - length?: number; - timestampped?: boolean; - prefixed?: boolean; -} - -export function generateUniqueName(baseName: string, options?: TestNameOptions): string { - const length = options?.length ?? 1; - const timestamp = options?.timestampped === undefined ? true : options.timestampped; - const prefixed = options?.prefixed === undefined ? true : options.prefixed; - - const prefix = prefixed ? "t_" : ""; - const suffix = timestamp ? `_${Date.now()}` : ""; - return `${prefix}${baseName}${crypto.randomBytes(length).toString("hex")}${suffix}`; -} - -export function getAzureCLICredentials(): DefaultAzureCredential { - return new DefaultAzureCredential(); -} - -export async function getAzureCLICredentialsToken(): Promise { - const credentials = getAzureCLICredentials(); - const token = (await credentials.getToken("https://management.core.windows.net//.default"))?.token || ""; - return token; -} - -export enum TestAccount { - Tables = "Tables", - Cassandra = "Cassandra", - Gremlin = "Gremlin", - Mongo = "Mongo", - MongoReadonly = "MongoReadOnly", - Mongo32 = "Mongo32", - SQL = "SQL", - SQLReadOnly = "SQLReadOnly", - SQLContainerCopyOnly = "SQLContainerCopyOnly", -} - -export const defaultAccounts: Record = { - [TestAccount.Tables]: "github-e2etests-tables", - [TestAccount.Cassandra]: "github-e2etests-cassandra", - [TestAccount.Gremlin]: "github-e2etests-gremlin", - [TestAccount.Mongo]: "github-e2etests-mongo", - [TestAccount.MongoReadonly]: "github-e2etests-mongo-readonly", - [TestAccount.Mongo32]: "github-e2etests-mongo32", - [TestAccount.SQL]: "github-e2etests-sql", - [TestAccount.SQLReadOnly]: "github-e2etests-sql-readonly", - [TestAccount.SQLContainerCopyOnly]: "github-e2etests-sql-containercopyonly", -}; - -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_MANUAL_THROUGHPUT_RU = 800; -export const TEST_AUTOSCALE_MAX_THROUGHPUT_RU_2K = 2000; -export const TEST_AUTOSCALE_MAX_THROUGHPUT_RU_4K = 4000; -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) { - const actualPrefix = process.env.DE_TEST_ACCOUNT_PREFIX.endsWith("-") - ? process.env.DE_TEST_ACCOUNT_PREFIX - : `${process.env.DE_TEST_ACCOUNT_PREFIX}-`; - return `${actualPrefix}${accountType.toLocaleLowerCase()}`; - } -} - -export function getAccountName(accountType: TestAccount) { - return ( - process.env[`DE_TEST_ACCOUNT_NAME_${accountType.toLocaleUpperCase()}`] ?? - tryGetStandardName(accountType) ?? - defaultAccounts[accountType] - ); -} - -type TestExplorerUrlOptions = { - iframeSrc?: string; - enablecontainercopy?: boolean; -}; - -export async function getTestExplorerUrl(accountType: TestAccount, options?: TestExplorerUrlOptions): Promise { - const { iframeSrc, enablecontainercopy } = options ?? {}; - - // We can't retrieve AZ CLI credentials from the browser so we get them here. - const token = await getAzureCLICredentialsToken(); - const accountName = getAccountName(accountType); - const params = new URLSearchParams(); - params.set("accountName", accountName); - params.set("resourceGroup", resourceGroupName); - params.set("subscriptionId", subscriptionId); - params.set("token", token); - - // There seem to be occasional CORS issues with calling the copilot APIs (/api/tokens/sampledataconnection/v2, for example) - // For now, since we don't test copilot, we can disable the copilot APIs by setting the feature flag to false. - params.set("feature.enableCopilot", "false"); - - const nosqlRbacToken = process.env.NOSQL_TESTACCOUNT_TOKEN; - const nosqlReadOnlyRbacToken = process.env.NOSQL_READONLY_TESTACCOUNT_TOKEN; - const nosqlContainerCopyRbacToken = process.env.NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN; - const tableRbacToken = process.env.TABLE_TESTACCOUNT_TOKEN; - const gremlinRbacToken = process.env.GREMLIN_TESTACCOUNT_TOKEN; - const cassandraRbacToken = process.env.CASSANDRA_TESTACCOUNT_TOKEN; - const mongoRbacToken = process.env.MONGO_TESTACCOUNT_TOKEN; - const mongo32RbacToken = process.env.MONGO32_TESTACCOUNT_TOKEN; - const mongoReadOnlyRbacToken = process.env.MONGO_READONLY_TESTACCOUNT_TOKEN; - - switch (accountType) { - case TestAccount.SQL: - if (nosqlRbacToken) { - params.set("nosqlRbacToken", nosqlRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.SQLContainerCopyOnly: - if (nosqlContainerCopyRbacToken) { - params.set("nosqlRbacToken", nosqlContainerCopyRbacToken); - params.set("enableaaddataplane", "true"); - } - if (enablecontainercopy) { - params.set("enablecontainercopy", "true"); - } - break; - - case TestAccount.SQLReadOnly: - if (nosqlReadOnlyRbacToken) { - params.set("nosqlReadOnlyRbacToken", nosqlReadOnlyRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.Tables: - if (tableRbacToken) { - params.set("tableRbacToken", tableRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.Gremlin: - if (gremlinRbacToken) { - params.set("gremlinRbacToken", gremlinRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.Cassandra: - if (cassandraRbacToken) { - params.set("cassandraRbacToken", cassandraRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.Mongo: - if (mongoRbacToken) { - params.set("mongoRbacToken", mongoRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.Mongo32: - if (mongo32RbacToken) { - params.set("mongo32RbacToken", mongo32RbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.MongoReadonly: - if (mongoReadOnlyRbacToken) { - params.set("mongoReadOnlyRbacToken", mongoReadOnlyRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - } - - if (iframeSrc) { - params.set("iframeSrc", iframeSrc); - } - - return `https://localhost:1234/testExplorer.html?${params.toString()}`; -} - -type DropdownItemExpectations = { - ariaLabel?: string; - itemCount?: number; -}; - -type DropdownItemMatcher = { - name?: string; - position?: number; -}; - -export async function getDropdownItemByNameOrPosition( - frame: Frame, - matcher?: DropdownItemMatcher, - expectedOptions?: DropdownItemExpectations, -): Promise { - const dropdownItemsWrapper = frame.locator("div.ms-Dropdown-items"); - if (expectedOptions?.ariaLabel) { - expect(await dropdownItemsWrapper.getAttribute("aria-label")).toEqual(expectedOptions.ariaLabel); - } - if (expectedOptions?.itemCount) { - const items = dropdownItemsWrapper.locator("button.ms-Dropdown-item[role='option']"); - await expect(items).toHaveCount(expectedOptions.itemCount); - } - const containerDropdownItems = dropdownItemsWrapper.locator("button.ms-Dropdown-item[role='option']"); - if (matcher?.name) { - return containerDropdownItems.filter({ hasText: matcher.name }); - } else if (matcher?.position !== undefined) { - return containerDropdownItems.nth(matcher.position); - } - // Return first item if no matcher is provided - return containerDropdownItems.first(); -} - -/** Helper class that provides locator methods for TreeNode elements, on top of a Locator */ -class TreeNode { - constructor( - public element: Locator, - public frame: Frame, - public id: string, - ) {} - - async openContextMenu(): Promise { - await this.element.click({ button: "right" }); - } - - contextMenuItem(name: string): Locator { - return this.frame.getByTestId(`TreeNode/ContextMenuItem:${name}`); - } - - async expand(): Promise { - const treeNodeContainer = this.frame.getByTestId(`TreeNodeContainer:${this.id}`); - const tree = this.frame.getByTestId(`Tree:${this.id}`); - - // eslint-disable-next-line prefer-arrow/prefer-arrow-functions - const expandNode = async () => { - if ((await treeNodeContainer.getAttribute("aria-expanded")) !== "true") { - // Click the node, to trigger loading and expansion - await this.element.click(); - } - - // Try three times to wait for the node to expand. - for (let i = 0; i < RETRY_COUNT; i++) { - try { - await tree.waitFor({ state: "visible" }); - // The tree has expanded, let's get out of here - return true; - } catch { - // Just try again - if ((await treeNodeContainer.getAttribute("aria-expanded")) !== "true") { - // We might have collapsed the node, try expanding it again, then retry. - await this.element.click(); - } - } - } - return false; - }; - - if (await expandNode()) { - return; - } - - // The tree never expanded. OR, it may have expanded in between when we found the "ExpandIcon" and when we clicked it (it's happened before) - // So, let's try one more time to expand it. - if (!(await expandNode())) { - // The tree never expanded. This is a problem. - throw new Error(`Node ${this.id} did not expand after clicking it.`); - } - - // We did it. It took a lot of weird messing around, but we expanded a tree node... I hope. - } -} - -export class Editor { - constructor( - public frame: Frame, - public locator: Locator, - ) {} - - text(): Promise { - return this.locator.evaluate((e) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const win = e.ownerDocument.defaultView as any; - if (win._monaco_getEditorContentForElement) { - return win._monaco_getEditorContentForElement(e); - } - return null; - }); - } - - async setText(text: string): Promise { - // We trust that Monaco can handle the keyboard, and it's _extremely_ flaky to try and enter text using browser commands. - // So we use a hook we installed in 'window' to set the content of the editor. - - // NOTE: This function is serialized and sent to the browser for execution - // So you can't use any variables from the outer scope, but we can send a string (via the second argument to evaluate) - await this.locator.evaluate((e, content) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const win = e.ownerDocument.defaultView as any; - if (win._monaco_setEditorContentForElement) { - win._monaco_setEditorContentForElement(e, content); - } - }, text); - - expect(await this.text()).toEqual(text); - } -} - -export class QueryTab { - resultsPane: Locator; - resultsView: Locator; - executeCTA: Locator; - errorList: Locator; - queryStatsList: Locator; - resultsEditor: Editor; - resultsTab: Locator; - queryStatsTab: Locator; - constructor( - public frame: Frame, - public tabId: string, - public tab: Locator, - public locator: Locator, - ) { - this.resultsPane = locator.getByTestId("QueryTab/ResultsPane"); - this.resultsView = locator.getByTestId("QueryTab/ResultsPane/ResultsView"); - this.executeCTA = locator.getByTestId("QueryTab/ResultsPane/ExecuteCTA"); - this.errorList = locator.getByTestId("QueryTab/ResultsPane/ErrorList"); - this.resultsEditor = new Editor(this.frame, this.resultsView.getByTestId("EditorReact/Host/Loaded")); - this.queryStatsList = locator.getByTestId("QueryTab/ResultsPane/ResultsView/QueryStatsList"); - this.resultsTab = this.resultsView.getByTestId("QueryTab/ResultsPane/ResultsView/ResultsTab"); - this.queryStatsTab = this.resultsView.getByTestId("QueryTab/ResultsPane/ResultsView/QueryStatsTab"); - } - - editor(): Editor { - const locator = this.locator.getByTestId("EditorReact/Host/Loaded"); - return new Editor(this.frame, locator); - } -} - -export class DocumentsTab { - documentsFilter: Locator; - documentsListPane: Locator; - documentResultsPane: Locator; - resultsEditor: Editor; - loadMoreButton: Locator; - filterInput: Locator; - filterButton: Locator; - - constructor( - public frame: Frame, - public tabId: string, - public tab: Locator, - public locator: Locator, - ) { - this.documentsFilter = this.locator.getByTestId("DocumentsTab/Filter"); - this.documentsListPane = this.locator.getByTestId("DocumentsTab/DocumentsPane"); - this.documentResultsPane = this.locator.getByTestId("DocumentsTab/ResultsPane"); - this.resultsEditor = new Editor(this.frame, this.documentResultsPane.getByTestId("EditorReact/Host/Loaded")); - this.loadMoreButton = this.documentsListPane.getByTestId("DocumentsTab/LoadMore"); - this.filterInput = this.documentsFilter.getByTestId("DocumentsTab/FilterInput"); - this.filterButton = this.documentsFilter.getByTestId("DocumentsTab/ApplyFilter"); - } - - async setFilter(text: string) { - await this.filterInput.fill(text); - } -} - -type PanelOpenOptions = { - closeTimeout?: number; -}; - -export enum CommandBarButton { - Save = "Save", - Delete = "Delete", - Execute = "Execute", - ExecuteQuery = "Execute Query", - UploadItem = "Upload Item", - NewDocument = "New Document", -} - -/** Helper class that provides locator methods for DataExplorer components, on top of a Frame */ -export class DataExplorer { - constructor(public frame: Frame) {} - - tab(tabId: string): Locator { - return this.frame.getByTestId(`Tab:${tabId}`); - } - - queryTab(tabId: string): QueryTab { - const tab = this.tab(tabId); - const queryTab = tab.getByTestId("QueryTab"); - return new QueryTab(this.frame, tabId, tab, queryTab); - } - - documentsTab(tabId: string): DocumentsTab { - const tab = this.tab(tabId); - const documentsTab = tab.getByTestId("DocumentsTab"); - return new DocumentsTab(this.frame, tabId, tab, documentsTab); - } - - /** Select the primary global command button. - * - * There's only a single "primary" button, but we still require you to pass the label to confirm you're selecting the right button. - */ - async globalCommandButton(label: string): Promise { - await this.frame.getByTestId("GlobalCommands").click(); - return this.frame.getByRole("menuitem", { name: label }); - } - - /** Select the command bar button with the specified label */ - commandBarButton(commandBarButton: CommandBarButton): Locator { - return this.frame.getByTestId(`CommandBar/Button:${commandBarButton}`).and(this.frame.locator("css=button")); - } - - dialogButton(label: string): Locator { - return this.frame.getByTestId(`DialogButton:${label}`).and(this.frame.locator("css=button")); - } - - /** Select the side panel with the specified title */ - panel(title: string): Locator { - return this.frame.getByTestId(`Panel:${title}`); - } - - async waitForNode(treeNodeId: string): Promise { - const node = this.treeNode(treeNodeId); - - // Is the node already visible? - if (await node.element.isVisible()) { - return node; - } - - // No, try refreshing the tree - const refreshButton = this.frame.getByTestId("Sidebar/RefreshButton"); - await refreshButton.click(); - - // Try a few times to find the node - for (let i = 0; i < RETRY_COUNT; i++) { - try { - await node.element.waitFor(); - return node; - } catch { - // Just try again - } - } - - // We tried 3 times, but the node never appeared - throw new Error(`Node ${treeNodeId} not found and did not appear after refreshing.`); - } - - async waitForContainerNode(databaseId: string, containerId: string): Promise { - const databaseNode = await this.waitForNode(databaseId); - - // The container node may be auto-expanded. Wait 5s for that to happen - try { - const containerNode = this.treeNode(`${databaseId}/${containerId}`); - await containerNode.element.waitFor({ state: "visible", timeout: 5 * 1000 }); - return containerNode; - } catch { - // It didn't auto-expand, that's fine, we'll expand it ourselves - } - - // Ok, expand the database node. - await databaseNode.expand(); - - return await this.waitForNode(`${databaseId}/${containerId}`); - } - - async waitForContainerItemsNode(databaseId: string, containerId: string): Promise { - return await this.waitForNode(`${databaseId}/${containerId}/Items`); - } - - async waitForContainerDocumentsNode(databaseId: string, containerId: string): Promise { - return await this.waitForNode(`${databaseId}/${containerId}/Documents`); - } - - async waitForCommandBarButton(label: CommandBarButton, timeout?: number): Promise { - const commandBar = this.commandBarButton(label); - await commandBar.waitFor({ state: "visible", timeout }); - return commandBar; - } - - async waitForDialogButton(label: string, timeout?: number): Promise { - const dialogButton = this.dialogButton(label); - await dialogButton.waitFor({ timeout }); - return dialogButton; - } - - /** Select the tree node with the specified id */ - treeNode(id: string): TreeNode { - return new TreeNode(this.frame.getByTestId(`TreeNode:${id}`), this.frame, id); - } - - /** Waits for the panel with the specified title to be open, then runs the provided callback. After the callback completes, waits for the panel to close. */ - async whilePanelOpen( - title: string, - action: (panel: Locator, okButton: Locator) => Promise, - options?: PanelOpenOptions, - ): Promise { - options ||= {}; - - const panel = this.panel(title); - await panel.waitFor(); - const okButton = panel.getByTestId("Panel/OkButton"); - await action(panel, okButton); - 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 */ - getConsoleHeaderStatus(): Locator { - return this.frame.getByTestId("notification-console/header-status"); - } - - async expandNotificationConsole(): Promise { - await this.setNotificationConsoleExpanded(true); - } - - async collapseNotificationConsole(): Promise { - await this.setNotificationConsoleExpanded(false); - } - - async setNotificationConsoleExpanded(expanded: boolean): Promise { - const notificationConsoleToggleButton = this.frame.getByTestId("NotificationConsole/ExpandCollapseButton"); - const alt = await notificationConsoleToggleButton.locator("img").getAttribute("alt"); - - // When expanded, the icon says "Collapse icon" - if (expanded && alt === "Expand icon") { - await notificationConsoleToggleButton.click(); - } else if (!expanded && alt === "Collapse icon") { - await notificationConsoleToggleButton.click(); - } - } - - async getNotificationConsoleMessages(): Promise { - await this.setNotificationConsoleExpanded(true); - return this.frame.getByTestId("NotificationConsole/Contents"); - } - - async getDropdownItemByName(name: string, ariaLabel?: string): Promise { - const dropdownItemsWrapper = this.frame.locator("div.ms-Dropdown-items"); - if (ariaLabel) { - expect(await dropdownItemsWrapper.getAttribute("aria-label")).toEqual(ariaLabel); - } - const containerDropdownItems = dropdownItemsWrapper.locator("button.ms-Dropdown-item[role='option']"); - return containerDropdownItems.filter({ hasText: name }); - } - - /** Waits for the Data Explorer app to load */ - static async waitForExplorer(page: Page, options?: TestExplorerUrlOptions): Promise { - const iframeElement = await page.getByTestId("DataExplorerFrame").elementHandle(); - if (iframeElement === null) { - throw new Error("Explorer iframe not found"); - } - - const explorerFrame = await iframeElement.contentFrame(); - - if (explorerFrame === null) { - throw new Error("Explorer frame not found"); - } - - if (!options?.enablecontainercopy) { - await explorerFrame?.getByTestId("DataExplorerRoot").waitFor(); - } - - return new DataExplorer(explorerFrame); - } - - /** Opens the Data Explorer app using the specified test account (and optionally, the provided IFRAME src url). */ - static async open(page: Page, testAccount: TestAccount, iframeSrc?: string): Promise { - const url = await getTestExplorerUrl(testAccount, { iframeSrc }); - await page.goto(url); - return DataExplorer.waitForExplorer(page); - } -} - -export async function waitForApiResponse( - page: Page, - urlPattern: string, - method?: string, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - payloadValidator?: (payload: any) => boolean, -) { - try { - // Check if page is still valid before waiting - if (page.isClosed()) { - throw new Error(`Page is closed, cannot wait for API response: ${urlPattern}`); - } - - return page.waitForResponse( - async (response) => { - const request = response.request(); - - if (!request.url().includes(urlPattern)) { - return false; - } - - if (method && request.method() !== method) { - return false; - } - - if (payloadValidator && (request.method() === "POST" || request.method() === "PUT")) { - const postData = request.postData(); - if (postData) { - try { - const payload = JSON.parse(postData); - return payloadValidator(payload); - } catch { - return false; - } - } - } - return true; - }, - { timeout: 60 * 1000 }, - ); - } catch (error) { - if (error instanceof Error && error.message.includes("Target page, context or browser has been closed")) { - console.warn("Page was closed while waiting for API response:", urlPattern); - throw new Error(`Page closed while waiting for API response: ${urlPattern}`); - } - throw error; - } -} -export async function interceptAndInspectApiRequest( - page: Page, - urlPattern: string, - method: string = "POST", - error: Error, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - errorValidator: (url?: string, payload?: any) => boolean, -): Promise { - await page.route( - (url) => url.pathname.includes(urlPattern), - async (route, request) => { - if (request.method() !== method) { - await route.continue(); - return; - } - const postData = request.postData(); - if (postData) { - try { - const payload = JSON.parse(postData); - if (errorValidator && errorValidator(request.url(), payload)) { - await route.fulfill({ - status: 409, - contentType: "application/json", - body: JSON.stringify({ - code: "Conflict", - message: error.message, - }), - }); - return; - } - } catch (err) { - if (err instanceof Error && err.message.includes("not allowed")) { - throw err; - } - } - } - - await route.continue(); - }, - ); -} - -export class ContainerCopy { - constructor( - public frame: Frame, - public wrapper: Locator, - ) {} - - static async waitForContainerCopy(page: Page): Promise { - const explorerFrame = await DataExplorer.waitForExplorer(page, { enablecontainercopy: true }); - const containerCopyWrapper = explorerFrame.frame.locator("div#containerCopyWrapper"); - return new ContainerCopy(explorerFrame.frame, containerCopyWrapper); - } - - static async open(page: Page, testAccount: TestAccount, iframeSrc?: string): Promise { - const url = await getTestExplorerUrl(testAccount, { iframeSrc, enablecontainercopy: true }); - await page.goto(url); - return ContainerCopy.waitForContainerCopy(page); - } -} diff --git a/.vs/CopilotSnapshots/548E56D5117B4249B654F2DE5EA27C25/state.mpack b/.vs/CopilotSnapshots/548E56D5117B4249B654F2DE5EA27C25/state.mpack deleted file mode 100644 index 7ce6172b0..000000000 Binary files a/.vs/CopilotSnapshots/548E56D5117B4249B654F2DE5EA27C25/state.mpack and /dev/null differ diff --git a/.vs/CopilotSnapshots/D94A63B0B7BF1348926679F1C2D0F6F9/8C84C14214C34148A24F6EDEC13E1656/76EC84F90637547DE632CC6E34850A12 b/.vs/CopilotSnapshots/D94A63B0B7BF1348926679F1C2D0F6F9/8C84C14214C34148A24F6EDEC13E1656/76EC84F90637547DE632CC6E34850A12 deleted file mode 100644 index 9c0dd0d7a..000000000 --- a/.vs/CopilotSnapshots/D94A63B0B7BF1348926679F1C2D0F6F9/8C84C14214C34148A24F6EDEC13E1656/76EC84F90637547DE632CC6E34850A12 +++ /dev/null @@ -1,703 +0,0 @@ -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; - -export interface TestNameOptions { - length?: number; - timestampped?: boolean; - prefixed?: boolean; -} - -export function generateUniqueName(baseName: string, options?: TestNameOptions): string { - const length = options?.length ?? 1; - const timestamp = options?.timestampped === undefined ? true : options.timestampped; - const prefixed = options?.prefixed === undefined ? true : options.prefixed; - - const prefix = prefixed ? "t_" : ""; - const suffix = timestamp ? `_${Date.now()}` : ""; - return `${prefix}${baseName}${crypto.randomBytes(length).toString("hex")}${suffix}`; -} - -export function getAzureCLICredentials(): DefaultAzureCredential { - return new DefaultAzureCredential(); -} - -export async function getAzureCLICredentialsToken(): Promise { - const credentials = getAzureCLICredentials(); - const token = (await credentials.getToken("https://management.core.windows.net//.default"))?.token || ""; - return token; -} - -export enum TestAccount { - Tables = "Tables", - Cassandra = "Cassandra", - Gremlin = "Gremlin", - Mongo = "Mongo", - MongoReadonly = "MongoReadOnly", - Mongo32 = "Mongo32", - SQL = "SQL", - SQLReadOnly = "SQLReadOnly", - SQLContainerCopyOnly = "SQLContainerCopyOnly", -} - -export const defaultAccounts: Record = { - [TestAccount.Tables]: "github-e2etests-tables", - [TestAccount.Cassandra]: "github-e2etests-cassandra", - [TestAccount.Gremlin]: "github-e2etests-gremlin", - [TestAccount.Mongo]: "github-e2etests-mongo", - [TestAccount.MongoReadonly]: "github-e2etests-mongo-readonly", - [TestAccount.Mongo32]: "github-e2etests-mongo32", - [TestAccount.SQL]: "github-e2etests-sql", - [TestAccount.SQLReadOnly]: "github-e2etests-sql-readonly", - [TestAccount.SQLContainerCopyOnly]: "github-e2etests-sql-containercopyonly", -}; - -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_MANUAL_THROUGHPUT_RU = 800; -export const TEST_AUTOSCALE_MAX_THROUGHPUT_RU_2K = 2000; -export const TEST_AUTOSCALE_MAX_THROUGHPUT_RU_4K = 4000; -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) { - const actualPrefix = process.env.DE_TEST_ACCOUNT_PREFIX.endsWith("-") - ? process.env.DE_TEST_ACCOUNT_PREFIX - : `${process.env.DE_TEST_ACCOUNT_PREFIX}-`; - return `${actualPrefix}${accountType.toLocaleLowerCase()}`; - } -} - -export function getAccountName(accountType: TestAccount) { - return ( - process.env[`DE_TEST_ACCOUNT_NAME_${accountType.toLocaleUpperCase()}`] ?? - tryGetStandardName(accountType) ?? - defaultAccounts[accountType] - ); -} - -type TestExplorerUrlOptions = { - iframeSrc?: string; - enablecontainercopy?: boolean; -}; - -export async function getTestExplorerUrl(accountType: TestAccount, options?: TestExplorerUrlOptions): Promise { - const { iframeSrc, enablecontainercopy } = options ?? {}; - - // We can't retrieve AZ CLI credentials from the browser so we get them here. - const token = await getAzureCLICredentialsToken(); - const accountName = getAccountName(accountType); - const params = new URLSearchParams(); - params.set("accountName", accountName); - params.set("resourceGroup", resourceGroupName); - params.set("subscriptionId", subscriptionId); - params.set("token", token); - - // There seem to be occasional CORS issues with calling the copilot APIs (/api/tokens/sampledataconnection/v2, for example) - // For now, since we don't test copilot, we can disable the copilot APIs by setting the feature flag to false. - params.set("feature.enableCopilot", "false"); - - const nosqlRbacToken = process.env.NOSQL_TESTACCOUNT_TOKEN; - const nosqlReadOnlyRbacToken = process.env.NOSQL_READONLY_TESTACCOUNT_TOKEN; - const nosqlContainerCopyRbacToken = process.env.NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN; - const tableRbacToken = process.env.TABLE_TESTACCOUNT_TOKEN; - const gremlinRbacToken = process.env.GREMLIN_TESTACCOUNT_TOKEN; - const cassandraRbacToken = process.env.CASSANDRA_TESTACCOUNT_TOKEN; - const mongoRbacToken = process.env.MONGO_TESTACCOUNT_TOKEN; - const mongo32RbacToken = process.env.MONGO32_TESTACCOUNT_TOKEN; - const mongoReadOnlyRbacToken = process.env.MONGO_READONLY_TESTACCOUNT_TOKEN; - - switch (accountType) { - case TestAccount.SQL: - if (nosqlRbacToken) { - params.set("nosqlRbacToken", nosqlRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.SQLContainerCopyOnly: - if (nosqlContainerCopyRbacToken) { - params.set("nosqlRbacToken", nosqlContainerCopyRbacToken); - params.set("enableaaddataplane", "true"); - } - if (enablecontainercopy) { - params.set("enablecontainercopy", "true"); - } - break; - - case TestAccount.SQLReadOnly: - if (nosqlReadOnlyRbacToken) { - params.set("nosqlReadOnlyRbacToken", nosqlReadOnlyRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.Tables: - if (tableRbacToken) { - params.set("tableRbacToken", tableRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.Gremlin: - if (gremlinRbacToken) { - params.set("gremlinRbacToken", gremlinRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.Cassandra: - if (cassandraRbacToken) { - params.set("cassandraRbacToken", cassandraRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.Mongo: - if (mongoRbacToken) { - params.set("mongoRbacToken", mongoRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.Mongo32: - if (mongo32RbacToken) { - params.set("mongo32RbacToken", mongo32RbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.MongoReadonly: - if (mongoReadOnlyRbacToken) { - params.set("mongoReadOnlyRbacToken", mongoReadOnlyRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - } - - if (iframeSrc) { - params.set("iframeSrc", iframeSrc); - } - - return `https://localhost:1234/testExplorer.html?${params.toString()}`; -} - -type DropdownItemExpectations = { - ariaLabel?: string; - itemCount?: number; -}; - -type DropdownItemMatcher = { - name?: string; - position?: number; -}; - -export async function getDropdownItemByNameOrPosition( - frame: Frame, - matcher?: DropdownItemMatcher, - expectedOptions?: DropdownItemExpectations, -): Promise { - const dropdownItemsWrapper = frame.locator("div.ms-Dropdown-items"); - if (expectedOptions?.ariaLabel) { - expect(await dropdownItemsWrapper.getAttribute("aria-label")).toEqual(expectedOptions.ariaLabel); - } - if (expectedOptions?.itemCount) { - const items = dropdownItemsWrapper.locator("button.ms-Dropdown-item[role='option']"); - await expect(items).toHaveCount(expectedOptions.itemCount); - } - const containerDropdownItems = dropdownItemsWrapper.locator("button.ms-Dropdown-item[role='option']"); - if (matcher?.name) { - return containerDropdownItems.filter({ hasText: matcher.name }); - } else if (matcher?.position !== undefined) { - return containerDropdownItems.nth(matcher.position); - } - // Return first item if no matcher is provided - return containerDropdownItems.first(); -} - -/** Helper class that provides locator methods for TreeNode elements, on top of a Locator */ -class TreeNode { - constructor( - public element: Locator, - public frame: Frame, - public id: string, - ) {} - - async openContextMenu(): Promise { - await this.element.click({ button: "right" }); - } - - contextMenuItem(name: string): Locator { - return this.frame.getByTestId(`TreeNode/ContextMenuItem:${name}`); - } - - async expand(): Promise { - const treeNodeContainer = this.frame.getByTestId(`TreeNodeContainer:${this.id}`); - const tree = this.frame.getByTestId(`Tree:${this.id}`); - - // eslint-disable-next-line prefer-arrow/prefer-arrow-functions - const expandNode = async () => { - if ((await treeNodeContainer.getAttribute("aria-expanded")) !== "true") { - // Click the node, to trigger loading and expansion - await this.element.click(); - } - - // Try three times to wait for the node to expand. - for (let i = 0; i < RETRY_COUNT; i++) { - try { - // Use a longer timeout (30s) since expanding may require loading children from the server - await tree.waitFor({ state: "visible", timeout: 30 * 1000 }); - // The tree has expanded, let's get out of here - return true; - } catch { - // Just try again - if ((await treeNodeContainer.getAttribute("aria-expanded")) !== "true") { - // We might have collapsed the node, try expanding it again, then retry. - await this.element.click(); - } - } - } - return false; - }; - - if (await expandNode()) { - return; - } - - // The tree never expanded. OR, it may have expanded in between when we found the "ExpandIcon" and when we clicked it (it's happened before) - // So, let's try one more time to expand it. - if (!(await expandNode())) { - // The tree never expanded. This is a problem. - throw new Error(`Node ${this.id} did not expand after clicking it.`); - } - - // We did it. It took a lot of weird messing around, but we expanded a tree node... I hope. - } -} - -export class Editor { - constructor( - public frame: Frame, - public locator: Locator, - ) {} - - text(): Promise { - return this.locator.evaluate((e) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const win = e.ownerDocument.defaultView as any; - if (win._monaco_getEditorContentForElement) { - return win._monaco_getEditorContentForElement(e); - } - return null; - }); - } - - async setText(text: string): Promise { - // We trust that Monaco can handle the keyboard, and it's _extremely_ flaky to try and enter text using browser commands. - // So we use a hook we installed in 'window' to set the content of the editor. - - // NOTE: This function is serialized and sent to the browser for execution - // So you can't use any variables from the outer scope, but we can send a string (via the second argument to evaluate) - await this.locator.evaluate((e, content) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const win = e.ownerDocument.defaultView as any; - if (win._monaco_setEditorContentForElement) { - win._monaco_setEditorContentForElement(e, content); - } - }, text); - - expect(await this.text()).toEqual(text); - } -} - -export class QueryTab { - resultsPane: Locator; - resultsView: Locator; - executeCTA: Locator; - errorList: Locator; - queryStatsList: Locator; - resultsEditor: Editor; - resultsTab: Locator; - queryStatsTab: Locator; - constructor( - public frame: Frame, - public tabId: string, - public tab: Locator, - public locator: Locator, - ) { - this.resultsPane = locator.getByTestId("QueryTab/ResultsPane"); - this.resultsView = locator.getByTestId("QueryTab/ResultsPane/ResultsView"); - this.executeCTA = locator.getByTestId("QueryTab/ResultsPane/ExecuteCTA"); - this.errorList = locator.getByTestId("QueryTab/ResultsPane/ErrorList"); - this.resultsEditor = new Editor(this.frame, this.resultsView.getByTestId("EditorReact/Host/Loaded")); - this.queryStatsList = locator.getByTestId("QueryTab/ResultsPane/ResultsView/QueryStatsList"); - this.resultsTab = this.resultsView.getByTestId("QueryTab/ResultsPane/ResultsView/ResultsTab"); - this.queryStatsTab = this.resultsView.getByTestId("QueryTab/ResultsPane/ResultsView/QueryStatsTab"); - } - - editor(): Editor { - const locator = this.locator.getByTestId("EditorReact/Host/Loaded"); - return new Editor(this.frame, locator); - } -} - -export class DocumentsTab { - documentsFilter: Locator; - documentsListPane: Locator; - documentResultsPane: Locator; - resultsEditor: Editor; - loadMoreButton: Locator; - filterInput: Locator; - filterButton: Locator; - - constructor( - public frame: Frame, - public tabId: string, - public tab: Locator, - public locator: Locator, - ) { - this.documentsFilter = this.locator.getByTestId("DocumentsTab/Filter"); - this.documentsListPane = this.locator.getByTestId("DocumentsTab/DocumentsPane"); - this.documentResultsPane = this.locator.getByTestId("DocumentsTab/ResultsPane"); - this.resultsEditor = new Editor(this.frame, this.documentResultsPane.getByTestId("EditorReact/Host/Loaded")); - this.loadMoreButton = this.documentsListPane.getByTestId("DocumentsTab/LoadMore"); - this.filterInput = this.documentsFilter.getByTestId("DocumentsTab/FilterInput"); - this.filterButton = this.documentsFilter.getByTestId("DocumentsTab/ApplyFilter"); - } - - async setFilter(text: string) { - await this.filterInput.fill(text); - } -} - -type PanelOpenOptions = { - closeTimeout?: number; -}; - -export enum CommandBarButton { - Save = "Save", - Delete = "Delete", - Execute = "Execute", - ExecuteQuery = "Execute Query", - UploadItem = "Upload Item", - NewDocument = "New Document", -} - -/** Helper class that provides locator methods for DataExplorer components, on top of a Frame */ -export class DataExplorer { - constructor(public frame: Frame) {} - - tab(tabId: string): Locator { - return this.frame.getByTestId(`Tab:${tabId}`); - } - - queryTab(tabId: string): QueryTab { - const tab = this.tab(tabId); - const queryTab = tab.getByTestId("QueryTab"); - return new QueryTab(this.frame, tabId, tab, queryTab); - } - - documentsTab(tabId: string): DocumentsTab { - const tab = this.tab(tabId); - const documentsTab = tab.getByTestId("DocumentsTab"); - return new DocumentsTab(this.frame, tabId, tab, documentsTab); - } - - /** Select the primary global command button. - * - * There's only a single "primary" button, but we still require you to pass the label to confirm you're selecting the right button. - */ - async globalCommandButton(label: string): Promise { - await this.frame.getByTestId("GlobalCommands").click(); - return this.frame.getByRole("menuitem", { name: label }); - } - - /** Select the command bar button with the specified label */ - commandBarButton(commandBarButton: CommandBarButton): Locator { - return this.frame.getByTestId(`CommandBar/Button:${commandBarButton}`).and(this.frame.locator("css=button")); - } - - dialogButton(label: string): Locator { - return this.frame.getByTestId(`DialogButton:${label}`).and(this.frame.locator("css=button")); - } - - /** Select the side panel with the specified title */ - panel(title: string): Locator { - return this.frame.getByTestId(`Panel:${title}`); - } - - async waitForNode(treeNodeId: string): Promise { - const node = this.treeNode(treeNodeId); - - // Is the node already visible? - if (await node.element.isVisible()) { - return node; - } - - // No, try refreshing the tree - const refreshButton = this.frame.getByTestId("Sidebar/RefreshButton"); - await refreshButton.click(); - - // Try a few times to find the node - for (let i = 0; i < RETRY_COUNT; i++) { - try { - await node.element.waitFor(); - return node; - } catch { - // Just try again - } - } - - // We tried 3 times, but the node never appeared - throw new Error(`Node ${treeNodeId} not found and did not appear after refreshing.`); - } - - async waitForContainerNode(databaseId: string, containerId: string): Promise { - const databaseNode = await this.waitForNode(databaseId); - - // The container node may be auto-expanded. Wait 5s for that to happen - try { - const containerNode = this.treeNode(`${databaseId}/${containerId}`); - await containerNode.element.waitFor({ state: "visible", timeout: 5 * 1000 }); - return containerNode; - } catch { - // It didn't auto-expand, that's fine, we'll expand it ourselves - } - - // Ok, expand the database node. - await databaseNode.expand(); - - return await this.waitForNode(`${databaseId}/${containerId}`); - } - - async waitForContainerItemsNode(databaseId: string, containerId: string): Promise { - return await this.waitForNode(`${databaseId}/${containerId}/Items`); - } - - async waitForContainerDocumentsNode(databaseId: string, containerId: string): Promise { - return await this.waitForNode(`${databaseId}/${containerId}/Documents`); - } - - async waitForCommandBarButton(label: CommandBarButton, timeout?: number): Promise { - const commandBar = this.commandBarButton(label); - await commandBar.waitFor({ state: "visible", timeout }); - return commandBar; - } - - async waitForDialogButton(label: string, timeout?: number): Promise { - const dialogButton = this.dialogButton(label); - await dialogButton.waitFor({ timeout }); - return dialogButton; - } - - /** Select the tree node with the specified id */ - treeNode(id: string): TreeNode { - return new TreeNode(this.frame.getByTestId(`TreeNode:${id}`), this.frame, id); - } - - /** Waits for the panel with the specified title to be open, then runs the provided callback. After the callback completes, waits for the panel to close. */ - async whilePanelOpen( - title: string, - action: (panel: Locator, okButton: Locator) => Promise, - options?: PanelOpenOptions, - ): Promise { - options ||= {}; - - const panel = this.panel(title); - await panel.waitFor(); - const okButton = panel.getByTestId("Panel/OkButton"); - await action(panel, okButton); - 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 */ - getConsoleHeaderStatus(): Locator { - return this.frame.getByTestId("notification-console/header-status"); - } - - async expandNotificationConsole(): Promise { - await this.setNotificationConsoleExpanded(true); - } - - async collapseNotificationConsole(): Promise { - await this.setNotificationConsoleExpanded(false); - } - - async setNotificationConsoleExpanded(expanded: boolean): Promise { - const notificationConsoleToggleButton = this.frame.getByTestId("NotificationConsole/ExpandCollapseButton"); - const alt = await notificationConsoleToggleButton.locator("img").getAttribute("alt"); - - // When expanded, the icon says "Collapse icon" - if (expanded && alt === "Expand icon") { - await notificationConsoleToggleButton.click(); - } else if (!expanded && alt === "Collapse icon") { - await notificationConsoleToggleButton.click(); - } - } - - async getNotificationConsoleMessages(): Promise { - await this.setNotificationConsoleExpanded(true); - return this.frame.getByTestId("NotificationConsole/Contents"); - } - - async getDropdownItemByName(name: string, ariaLabel?: string): Promise { - const dropdownItemsWrapper = this.frame.locator("div.ms-Dropdown-items"); - if (ariaLabel) { - expect(await dropdownItemsWrapper.getAttribute("aria-label")).toEqual(ariaLabel); - } - const containerDropdownItems = dropdownItemsWrapper.locator("button.ms-Dropdown-item[role='option']"); - return containerDropdownItems.filter({ hasText: name }); - } - - /** Waits for the Data Explorer app to load */ - static async waitForExplorer(page: Page, options?: TestExplorerUrlOptions): Promise { - const iframeElement = await page.getByTestId("DataExplorerFrame").elementHandle(); - if (iframeElement === null) { - throw new Error("Explorer iframe not found"); - } - - const explorerFrame = await iframeElement.contentFrame(); - - if (explorerFrame === null) { - throw new Error("Explorer frame not found"); - } - - if (!options?.enablecontainercopy) { - await explorerFrame?.getByTestId("DataExplorerRoot").waitFor(); - } - - return new DataExplorer(explorerFrame); - } - - /** Opens the Data Explorer app using the specified test account (and optionally, the provided IFRAME src url). */ - static async open(page: Page, testAccount: TestAccount, iframeSrc?: string): Promise { - const url = await getTestExplorerUrl(testAccount, { iframeSrc }); - await page.goto(url); - return DataExplorer.waitForExplorer(page); - } -} - -export async function waitForApiResponse( - page: Page, - urlPattern: string, - method?: string, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - payloadValidator?: (payload: any) => boolean, -) { - try { - // Check if page is still valid before waiting - if (page.isClosed()) { - throw new Error(`Page is closed, cannot wait for API response: ${urlPattern}`); - } - - return page.waitForResponse( - async (response) => { - const request = response.request(); - - if (!request.url().includes(urlPattern)) { - return false; - } - - if (method && request.method() !== method) { - return false; - } - - if (payloadValidator && (request.method() === "POST" || request.method() === "PUT")) { - const postData = request.postData(); - if (postData) { - try { - const payload = JSON.parse(postData); - return payloadValidator(payload); - } catch { - return false; - } - } - } - return true; - }, - { timeout: 60 * 1000 }, - ); - } catch (error) { - if (error instanceof Error && error.message.includes("Target page, context or browser has been closed")) { - console.warn("Page was closed while waiting for API response:", urlPattern); - throw new Error(`Page closed while waiting for API response: ${urlPattern}`); - } - throw error; - } -} -export async function interceptAndInspectApiRequest( - page: Page, - urlPattern: string, - method: string = "POST", - error: Error, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - errorValidator: (url?: string, payload?: any) => boolean, -): Promise { - await page.route( - (url) => url.pathname.includes(urlPattern), - async (route, request) => { - if (request.method() !== method) { - await route.continue(); - return; - } - const postData = request.postData(); - if (postData) { - try { - const payload = JSON.parse(postData); - if (errorValidator && errorValidator(request.url(), payload)) { - await route.fulfill({ - status: 409, - contentType: "application/json", - body: JSON.stringify({ - code: "Conflict", - message: error.message, - }), - }); - return; - } - } catch (err) { - if (err instanceof Error && err.message.includes("not allowed")) { - throw err; - } - } - } - - await route.continue(); - }, - ); -} - -export class ContainerCopy { - constructor( - public frame: Frame, - public wrapper: Locator, - ) {} - - static async waitForContainerCopy(page: Page): Promise { - const explorerFrame = await DataExplorer.waitForExplorer(page, { enablecontainercopy: true }); - const containerCopyWrapper = explorerFrame.frame.locator("div#containerCopyWrapper"); - return new ContainerCopy(explorerFrame.frame, containerCopyWrapper); - } - - static async open(page: Page, testAccount: TestAccount, iframeSrc?: string): Promise { - const url = await getTestExplorerUrl(testAccount, { iframeSrc, enablecontainercopy: true }); - await page.goto(url); - return ContainerCopy.waitForContainerCopy(page); - } -} diff --git a/.vs/CopilotSnapshots/D94A63B0B7BF1348926679F1C2D0F6F9/8C84C14214C34148A24F6EDEC13E1656/88CD68F6C261251F69442B2733965940 b/.vs/CopilotSnapshots/D94A63B0B7BF1348926679F1C2D0F6F9/8C84C14214C34148A24F6EDEC13E1656/88CD68F6C261251F69442B2733965940 deleted file mode 100644 index 262545aa1..000000000 --- a/.vs/CopilotSnapshots/D94A63B0B7BF1348926679F1C2D0F6F9/8C84C14214C34148A24F6EDEC13E1656/88CD68F6C261251F69442B2733965940 +++ /dev/null @@ -1,702 +0,0 @@ -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; - -export interface TestNameOptions { - length?: number; - timestampped?: boolean; - prefixed?: boolean; -} - -export function generateUniqueName(baseName: string, options?: TestNameOptions): string { - const length = options?.length ?? 1; - const timestamp = options?.timestampped === undefined ? true : options.timestampped; - const prefixed = options?.prefixed === undefined ? true : options.prefixed; - - const prefix = prefixed ? "t_" : ""; - const suffix = timestamp ? `_${Date.now()}` : ""; - return `${prefix}${baseName}${crypto.randomBytes(length).toString("hex")}${suffix}`; -} - -export function getAzureCLICredentials(): DefaultAzureCredential { - return new DefaultAzureCredential(); -} - -export async function getAzureCLICredentialsToken(): Promise { - const credentials = getAzureCLICredentials(); - const token = (await credentials.getToken("https://management.core.windows.net//.default"))?.token || ""; - return token; -} - -export enum TestAccount { - Tables = "Tables", - Cassandra = "Cassandra", - Gremlin = "Gremlin", - Mongo = "Mongo", - MongoReadonly = "MongoReadOnly", - Mongo32 = "Mongo32", - SQL = "SQL", - SQLReadOnly = "SQLReadOnly", - SQLContainerCopyOnly = "SQLContainerCopyOnly", -} - -export const defaultAccounts: Record = { - [TestAccount.Tables]: "github-e2etests-tables", - [TestAccount.Cassandra]: "github-e2etests-cassandra", - [TestAccount.Gremlin]: "github-e2etests-gremlin", - [TestAccount.Mongo]: "github-e2etests-mongo", - [TestAccount.MongoReadonly]: "github-e2etests-mongo-readonly", - [TestAccount.Mongo32]: "github-e2etests-mongo32", - [TestAccount.SQL]: "github-e2etests-sql", - [TestAccount.SQLReadOnly]: "github-e2etests-sql-readonly", - [TestAccount.SQLContainerCopyOnly]: "github-e2etests-sql-containercopyonly", -}; - -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_MANUAL_THROUGHPUT_RU = 800; -export const TEST_AUTOSCALE_MAX_THROUGHPUT_RU_2K = 2000; -export const TEST_AUTOSCALE_MAX_THROUGHPUT_RU_4K = 4000; -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) { - const actualPrefix = process.env.DE_TEST_ACCOUNT_PREFIX.endsWith("-") - ? process.env.DE_TEST_ACCOUNT_PREFIX - : `${process.env.DE_TEST_ACCOUNT_PREFIX}-`; - return `${actualPrefix}${accountType.toLocaleLowerCase()}`; - } -} - -export function getAccountName(accountType: TestAccount) { - return ( - process.env[`DE_TEST_ACCOUNT_NAME_${accountType.toLocaleUpperCase()}`] ?? - tryGetStandardName(accountType) ?? - defaultAccounts[accountType] - ); -} - -type TestExplorerUrlOptions = { - iframeSrc?: string; - enablecontainercopy?: boolean; -}; - -export async function getTestExplorerUrl(accountType: TestAccount, options?: TestExplorerUrlOptions): Promise { - const { iframeSrc, enablecontainercopy } = options ?? {}; - - // We can't retrieve AZ CLI credentials from the browser so we get them here. - const token = await getAzureCLICredentialsToken(); - const accountName = getAccountName(accountType); - const params = new URLSearchParams(); - params.set("accountName", accountName); - params.set("resourceGroup", resourceGroupName); - params.set("subscriptionId", subscriptionId); - params.set("token", token); - - // There seem to be occasional CORS issues with calling the copilot APIs (/api/tokens/sampledataconnection/v2, for example) - // For now, since we don't test copilot, we can disable the copilot APIs by setting the feature flag to false. - params.set("feature.enableCopilot", "false"); - - const nosqlRbacToken = process.env.NOSQL_TESTACCOUNT_TOKEN; - const nosqlReadOnlyRbacToken = process.env.NOSQL_READONLY_TESTACCOUNT_TOKEN; - const nosqlContainerCopyRbacToken = process.env.NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN; - const tableRbacToken = process.env.TABLE_TESTACCOUNT_TOKEN; - const gremlinRbacToken = process.env.GREMLIN_TESTACCOUNT_TOKEN; - const cassandraRbacToken = process.env.CASSANDRA_TESTACCOUNT_TOKEN; - const mongoRbacToken = process.env.MONGO_TESTACCOUNT_TOKEN; - const mongo32RbacToken = process.env.MONGO32_TESTACCOUNT_TOKEN; - const mongoReadOnlyRbacToken = process.env.MONGO_READONLY_TESTACCOUNT_TOKEN; - - switch (accountType) { - case TestAccount.SQL: - if (nosqlRbacToken) { - params.set("nosqlRbacToken", nosqlRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.SQLContainerCopyOnly: - if (nosqlContainerCopyRbacToken) { - params.set("nosqlRbacToken", nosqlContainerCopyRbacToken); - params.set("enableaaddataplane", "true"); - } - if (enablecontainercopy) { - params.set("enablecontainercopy", "true"); - } - break; - - case TestAccount.SQLReadOnly: - if (nosqlReadOnlyRbacToken) { - params.set("nosqlReadOnlyRbacToken", nosqlReadOnlyRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.Tables: - if (tableRbacToken) { - params.set("tableRbacToken", tableRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.Gremlin: - if (gremlinRbacToken) { - params.set("gremlinRbacToken", gremlinRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.Cassandra: - if (cassandraRbacToken) { - params.set("cassandraRbacToken", cassandraRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.Mongo: - if (mongoRbacToken) { - params.set("mongoRbacToken", mongoRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.Mongo32: - if (mongo32RbacToken) { - params.set("mongo32RbacToken", mongo32RbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.MongoReadonly: - if (mongoReadOnlyRbacToken) { - params.set("mongoReadOnlyRbacToken", mongoReadOnlyRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - } - - if (iframeSrc) { - params.set("iframeSrc", iframeSrc); - } - - return `https://localhost:1234/testExplorer.html?${params.toString()}`; -} - -type DropdownItemExpectations = { - ariaLabel?: string; - itemCount?: number; -}; - -type DropdownItemMatcher = { - name?: string; - position?: number; -}; - -export async function getDropdownItemByNameOrPosition( - frame: Frame, - matcher?: DropdownItemMatcher, - expectedOptions?: DropdownItemExpectations, -): Promise { - const dropdownItemsWrapper = frame.locator("div.ms-Dropdown-items"); - if (expectedOptions?.ariaLabel) { - expect(await dropdownItemsWrapper.getAttribute("aria-label")).toEqual(expectedOptions.ariaLabel); - } - if (expectedOptions?.itemCount) { - const items = dropdownItemsWrapper.locator("button.ms-Dropdown-item[role='option']"); - await expect(items).toHaveCount(expectedOptions.itemCount); - } - const containerDropdownItems = dropdownItemsWrapper.locator("button.ms-Dropdown-item[role='option']"); - if (matcher?.name) { - return containerDropdownItems.filter({ hasText: matcher.name }); - } else if (matcher?.position !== undefined) { - return containerDropdownItems.nth(matcher.position); - } - // Return first item if no matcher is provided - return containerDropdownItems.first(); -} - -/** Helper class that provides locator methods for TreeNode elements, on top of a Locator */ -class TreeNode { - constructor( - public element: Locator, - public frame: Frame, - public id: string, - ) {} - - async openContextMenu(): Promise { - await this.element.click({ button: "right" }); - } - - contextMenuItem(name: string): Locator { - return this.frame.getByTestId(`TreeNode/ContextMenuItem:${name}`); - } - - async expand(): Promise { - const treeNodeContainer = this.frame.getByTestId(`TreeNodeContainer:${this.id}`); - const tree = this.frame.getByTestId(`Tree:${this.id}`); - - // eslint-disable-next-line prefer-arrow/prefer-arrow-functions - const expandNode = async () => { - if ((await treeNodeContainer.getAttribute("aria-expanded")) !== "true") { - // Click the node, to trigger loading and expansion - await this.element.click(); - } - - // Try three times to wait for the node to expand. - for (let i = 0; i < RETRY_COUNT; i++) { - try { - await tree.waitFor({ state: "visible", timeout: 3 * 1000 }); - // The tree has expanded, let's get out of here - return true; - } catch { - // Just try again - if ((await treeNodeContainer.getAttribute("aria-expanded")) !== "true") { - // We might have collapsed the node, try expanding it again, then retry. - await this.element.click(); - } - } - } - return false; - }; - - if (await expandNode()) { - return; - } - - // The tree never expanded. OR, it may have expanded in between when we found the "ExpandIcon" and when we clicked it (it's happened before) - // So, let's try one more time to expand it. - if (!(await expandNode())) { - // The tree never expanded. This is a problem. - throw new Error(`Node ${this.id} did not expand after clicking it.`); - } - - // We did it. It took a lot of weird messing around, but we expanded a tree node... I hope. - } -} - -export class Editor { - constructor( - public frame: Frame, - public locator: Locator, - ) {} - - text(): Promise { - return this.locator.evaluate((e) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const win = e.ownerDocument.defaultView as any; - if (win._monaco_getEditorContentForElement) { - return win._monaco_getEditorContentForElement(e); - } - return null; - }); - } - - async setText(text: string): Promise { - // We trust that Monaco can handle the keyboard, and it's _extremely_ flaky to try and enter text using browser commands. - // So we use a hook we installed in 'window' to set the content of the editor. - - // NOTE: This function is serialized and sent to the browser for execution - // So you can't use any variables from the outer scope, but we can send a string (via the second argument to evaluate) - await this.locator.evaluate((e, content) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const win = e.ownerDocument.defaultView as any; - if (win._monaco_setEditorContentForElement) { - win._monaco_setEditorContentForElement(e, content); - } - }, text); - - expect(await this.text()).toEqual(text); - } -} - -export class QueryTab { - resultsPane: Locator; - resultsView: Locator; - executeCTA: Locator; - errorList: Locator; - queryStatsList: Locator; - resultsEditor: Editor; - resultsTab: Locator; - queryStatsTab: Locator; - constructor( - public frame: Frame, - public tabId: string, - public tab: Locator, - public locator: Locator, - ) { - this.resultsPane = locator.getByTestId("QueryTab/ResultsPane"); - this.resultsView = locator.getByTestId("QueryTab/ResultsPane/ResultsView"); - this.executeCTA = locator.getByTestId("QueryTab/ResultsPane/ExecuteCTA"); - this.errorList = locator.getByTestId("QueryTab/ResultsPane/ErrorList"); - this.resultsEditor = new Editor(this.frame, this.resultsView.getByTestId("EditorReact/Host/Loaded")); - this.queryStatsList = locator.getByTestId("QueryTab/ResultsPane/ResultsView/QueryStatsList"); - this.resultsTab = this.resultsView.getByTestId("QueryTab/ResultsPane/ResultsView/ResultsTab"); - this.queryStatsTab = this.resultsView.getByTestId("QueryTab/ResultsPane/ResultsView/QueryStatsTab"); - } - - editor(): Editor { - const locator = this.locator.getByTestId("EditorReact/Host/Loaded"); - return new Editor(this.frame, locator); - } -} - -export class DocumentsTab { - documentsFilter: Locator; - documentsListPane: Locator; - documentResultsPane: Locator; - resultsEditor: Editor; - loadMoreButton: Locator; - filterInput: Locator; - filterButton: Locator; - - constructor( - public frame: Frame, - public tabId: string, - public tab: Locator, - public locator: Locator, - ) { - this.documentsFilter = this.locator.getByTestId("DocumentsTab/Filter"); - this.documentsListPane = this.locator.getByTestId("DocumentsTab/DocumentsPane"); - this.documentResultsPane = this.locator.getByTestId("DocumentsTab/ResultsPane"); - this.resultsEditor = new Editor(this.frame, this.documentResultsPane.getByTestId("EditorReact/Host/Loaded")); - this.loadMoreButton = this.documentsListPane.getByTestId("DocumentsTab/LoadMore"); - this.filterInput = this.documentsFilter.getByTestId("DocumentsTab/FilterInput"); - this.filterButton = this.documentsFilter.getByTestId("DocumentsTab/ApplyFilter"); - } - - async setFilter(text: string) { - await this.filterInput.fill(text); - } -} - -type PanelOpenOptions = { - closeTimeout?: number; -}; - -export enum CommandBarButton { - Save = "Save", - Delete = "Delete", - Execute = "Execute", - ExecuteQuery = "Execute Query", - UploadItem = "Upload Item", - NewDocument = "New Document", -} - -/** Helper class that provides locator methods for DataExplorer components, on top of a Frame */ -export class DataExplorer { - constructor(public frame: Frame) {} - - tab(tabId: string): Locator { - return this.frame.getByTestId(`Tab:${tabId}`); - } - - queryTab(tabId: string): QueryTab { - const tab = this.tab(tabId); - const queryTab = tab.getByTestId("QueryTab"); - return new QueryTab(this.frame, tabId, tab, queryTab); - } - - documentsTab(tabId: string): DocumentsTab { - const tab = this.tab(tabId); - const documentsTab = tab.getByTestId("DocumentsTab"); - return new DocumentsTab(this.frame, tabId, tab, documentsTab); - } - - /** Select the primary global command button. - * - * There's only a single "primary" button, but we still require you to pass the label to confirm you're selecting the right button. - */ - async globalCommandButton(label: string): Promise { - await this.frame.getByTestId("GlobalCommands").click(); - return this.frame.getByRole("menuitem", { name: label }); - } - - /** Select the command bar button with the specified label */ - commandBarButton(commandBarButton: CommandBarButton): Locator { - return this.frame.getByTestId(`CommandBar/Button:${commandBarButton}`).and(this.frame.locator("css=button")); - } - - dialogButton(label: string): Locator { - return this.frame.getByTestId(`DialogButton:${label}`).and(this.frame.locator("css=button")); - } - - /** Select the side panel with the specified title */ - panel(title: string): Locator { - return this.frame.getByTestId(`Panel:${title}`); - } - - async waitForNode(treeNodeId: string): Promise { - const node = this.treeNode(treeNodeId); - - // Is the node already visible? - if (await node.element.isVisible()) { - return node; - } - - // No, try refreshing the tree - const refreshButton = this.frame.getByTestId("Sidebar/RefreshButton"); - await refreshButton.click(); - - // Try a few times to find the node - for (let i = 0; i < RETRY_COUNT; i++) { - try { - await node.element.waitFor(); - return node; - } catch { - // Just try again - } - } - - // We tried 3 times, but the node never appeared - throw new Error(`Node ${treeNodeId} not found and did not appear after refreshing.`); - } - - async waitForContainerNode(databaseId: string, containerId: string): Promise { - const databaseNode = await this.waitForNode(databaseId); - - // The container node may be auto-expanded. Wait 5s for that to happen - try { - const containerNode = this.treeNode(`${databaseId}/${containerId}`); - await containerNode.element.waitFor({ state: "visible", timeout: 5 * 1000 }); - return containerNode; - } catch { - // It didn't auto-expand, that's fine, we'll expand it ourselves - } - - // Ok, expand the database node. - await databaseNode.expand(); - - return await this.waitForNode(`${databaseId}/${containerId}`); - } - - async waitForContainerItemsNode(databaseId: string, containerId: string): Promise { - return await this.waitForNode(`${databaseId}/${containerId}/Items`); - } - - async waitForContainerDocumentsNode(databaseId: string, containerId: string): Promise { - return await this.waitForNode(`${databaseId}/${containerId}/Documents`); - } - - async waitForCommandBarButton(label: CommandBarButton, timeout?: number): Promise { - const commandBar = this.commandBarButton(label); - await commandBar.waitFor({ state: "visible", timeout }); - return commandBar; - } - - async waitForDialogButton(label: string, timeout?: number): Promise { - const dialogButton = this.dialogButton(label); - await dialogButton.waitFor({ timeout }); - return dialogButton; - } - - /** Select the tree node with the specified id */ - treeNode(id: string): TreeNode { - return new TreeNode(this.frame.getByTestId(`TreeNode:${id}`), this.frame, id); - } - - /** Waits for the panel with the specified title to be open, then runs the provided callback. After the callback completes, waits for the panel to close. */ - async whilePanelOpen( - title: string, - action: (panel: Locator, okButton: Locator) => Promise, - options?: PanelOpenOptions, - ): Promise { - options ||= {}; - - const panel = this.panel(title); - await panel.waitFor(); - const okButton = panel.getByTestId("Panel/OkButton"); - await action(panel, okButton); - 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 */ - getConsoleHeaderStatus(): Locator { - return this.frame.getByTestId("notification-console/header-status"); - } - - async expandNotificationConsole(): Promise { - await this.setNotificationConsoleExpanded(true); - } - - async collapseNotificationConsole(): Promise { - await this.setNotificationConsoleExpanded(false); - } - - async setNotificationConsoleExpanded(expanded: boolean): Promise { - const notificationConsoleToggleButton = this.frame.getByTestId("NotificationConsole/ExpandCollapseButton"); - const alt = await notificationConsoleToggleButton.locator("img").getAttribute("alt"); - - // When expanded, the icon says "Collapse icon" - if (expanded && alt === "Expand icon") { - await notificationConsoleToggleButton.click(); - } else if (!expanded && alt === "Collapse icon") { - await notificationConsoleToggleButton.click(); - } - } - - async getNotificationConsoleMessages(): Promise { - await this.setNotificationConsoleExpanded(true); - return this.frame.getByTestId("NotificationConsole/Contents"); - } - - async getDropdownItemByName(name: string, ariaLabel?: string): Promise { - const dropdownItemsWrapper = this.frame.locator("div.ms-Dropdown-items"); - if (ariaLabel) { - expect(await dropdownItemsWrapper.getAttribute("aria-label")).toEqual(ariaLabel); - } - const containerDropdownItems = dropdownItemsWrapper.locator("button.ms-Dropdown-item[role='option']"); - return containerDropdownItems.filter({ hasText: name }); - } - - /** Waits for the Data Explorer app to load */ - static async waitForExplorer(page: Page, options?: TestExplorerUrlOptions): Promise { - const iframeElement = await page.getByTestId("DataExplorerFrame").elementHandle(); - if (iframeElement === null) { - throw new Error("Explorer iframe not found"); - } - - const explorerFrame = await iframeElement.contentFrame(); - - if (explorerFrame === null) { - throw new Error("Explorer frame not found"); - } - - if (!options?.enablecontainercopy) { - await explorerFrame?.getByTestId("DataExplorerRoot").waitFor(); - } - - return new DataExplorer(explorerFrame); - } - - /** Opens the Data Explorer app using the specified test account (and optionally, the provided IFRAME src url). */ - static async open(page: Page, testAccount: TestAccount, iframeSrc?: string): Promise { - const url = await getTestExplorerUrl(testAccount, { iframeSrc }); - await page.goto(url); - return DataExplorer.waitForExplorer(page); - } -} - -export async function waitForApiResponse( - page: Page, - urlPattern: string, - method?: string, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - payloadValidator?: (payload: any) => boolean, -) { - try { - // Check if page is still valid before waiting - if (page.isClosed()) { - throw new Error(`Page is closed, cannot wait for API response: ${urlPattern}`); - } - - return page.waitForResponse( - async (response) => { - const request = response.request(); - - if (!request.url().includes(urlPattern)) { - return false; - } - - if (method && request.method() !== method) { - return false; - } - - if (payloadValidator && (request.method() === "POST" || request.method() === "PUT")) { - const postData = request.postData(); - if (postData) { - try { - const payload = JSON.parse(postData); - return payloadValidator(payload); - } catch { - return false; - } - } - } - return true; - }, - { timeout: 60 * 1000 }, - ); - } catch (error) { - if (error instanceof Error && error.message.includes("Target page, context or browser has been closed")) { - console.warn("Page was closed while waiting for API response:", urlPattern); - throw new Error(`Page closed while waiting for API response: ${urlPattern}`); - } - throw error; - } -} -export async function interceptAndInspectApiRequest( - page: Page, - urlPattern: string, - method: string = "POST", - error: Error, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - errorValidator: (url?: string, payload?: any) => boolean, -): Promise { - await page.route( - (url) => url.pathname.includes(urlPattern), - async (route, request) => { - if (request.method() !== method) { - await route.continue(); - return; - } - const postData = request.postData(); - if (postData) { - try { - const payload = JSON.parse(postData); - if (errorValidator && errorValidator(request.url(), payload)) { - await route.fulfill({ - status: 409, - contentType: "application/json", - body: JSON.stringify({ - code: "Conflict", - message: error.message, - }), - }); - return; - } - } catch (err) { - if (err instanceof Error && err.message.includes("not allowed")) { - throw err; - } - } - } - - await route.continue(); - }, - ); -} - -export class ContainerCopy { - constructor( - public frame: Frame, - public wrapper: Locator, - ) {} - - static async waitForContainerCopy(page: Page): Promise { - const explorerFrame = await DataExplorer.waitForExplorer(page, { enablecontainercopy: true }); - const containerCopyWrapper = explorerFrame.frame.locator("div#containerCopyWrapper"); - return new ContainerCopy(explorerFrame.frame, containerCopyWrapper); - } - - static async open(page: Page, testAccount: TestAccount, iframeSrc?: string): Promise { - const url = await getTestExplorerUrl(testAccount, { iframeSrc, enablecontainercopy: true }); - await page.goto(url); - return ContainerCopy.waitForContainerCopy(page); - } -} diff --git a/.vs/CopilotSnapshots/D94A63B0B7BF1348926679F1C2D0F6F9/8C84C14214C34148A24F6EDEC13E1656/BDC0B1042C11B58A5259E50EBB63DC01 b/.vs/CopilotSnapshots/D94A63B0B7BF1348926679F1C2D0F6F9/8C84C14214C34148A24F6EDEC13E1656/BDC0B1042C11B58A5259E50EBB63DC01 deleted file mode 100644 index 262545aa1..000000000 --- a/.vs/CopilotSnapshots/D94A63B0B7BF1348926679F1C2D0F6F9/8C84C14214C34148A24F6EDEC13E1656/BDC0B1042C11B58A5259E50EBB63DC01 +++ /dev/null @@ -1,702 +0,0 @@ -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; - -export interface TestNameOptions { - length?: number; - timestampped?: boolean; - prefixed?: boolean; -} - -export function generateUniqueName(baseName: string, options?: TestNameOptions): string { - const length = options?.length ?? 1; - const timestamp = options?.timestampped === undefined ? true : options.timestampped; - const prefixed = options?.prefixed === undefined ? true : options.prefixed; - - const prefix = prefixed ? "t_" : ""; - const suffix = timestamp ? `_${Date.now()}` : ""; - return `${prefix}${baseName}${crypto.randomBytes(length).toString("hex")}${suffix}`; -} - -export function getAzureCLICredentials(): DefaultAzureCredential { - return new DefaultAzureCredential(); -} - -export async function getAzureCLICredentialsToken(): Promise { - const credentials = getAzureCLICredentials(); - const token = (await credentials.getToken("https://management.core.windows.net//.default"))?.token || ""; - return token; -} - -export enum TestAccount { - Tables = "Tables", - Cassandra = "Cassandra", - Gremlin = "Gremlin", - Mongo = "Mongo", - MongoReadonly = "MongoReadOnly", - Mongo32 = "Mongo32", - SQL = "SQL", - SQLReadOnly = "SQLReadOnly", - SQLContainerCopyOnly = "SQLContainerCopyOnly", -} - -export const defaultAccounts: Record = { - [TestAccount.Tables]: "github-e2etests-tables", - [TestAccount.Cassandra]: "github-e2etests-cassandra", - [TestAccount.Gremlin]: "github-e2etests-gremlin", - [TestAccount.Mongo]: "github-e2etests-mongo", - [TestAccount.MongoReadonly]: "github-e2etests-mongo-readonly", - [TestAccount.Mongo32]: "github-e2etests-mongo32", - [TestAccount.SQL]: "github-e2etests-sql", - [TestAccount.SQLReadOnly]: "github-e2etests-sql-readonly", - [TestAccount.SQLContainerCopyOnly]: "github-e2etests-sql-containercopyonly", -}; - -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_MANUAL_THROUGHPUT_RU = 800; -export const TEST_AUTOSCALE_MAX_THROUGHPUT_RU_2K = 2000; -export const TEST_AUTOSCALE_MAX_THROUGHPUT_RU_4K = 4000; -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) { - const actualPrefix = process.env.DE_TEST_ACCOUNT_PREFIX.endsWith("-") - ? process.env.DE_TEST_ACCOUNT_PREFIX - : `${process.env.DE_TEST_ACCOUNT_PREFIX}-`; - return `${actualPrefix}${accountType.toLocaleLowerCase()}`; - } -} - -export function getAccountName(accountType: TestAccount) { - return ( - process.env[`DE_TEST_ACCOUNT_NAME_${accountType.toLocaleUpperCase()}`] ?? - tryGetStandardName(accountType) ?? - defaultAccounts[accountType] - ); -} - -type TestExplorerUrlOptions = { - iframeSrc?: string; - enablecontainercopy?: boolean; -}; - -export async function getTestExplorerUrl(accountType: TestAccount, options?: TestExplorerUrlOptions): Promise { - const { iframeSrc, enablecontainercopy } = options ?? {}; - - // We can't retrieve AZ CLI credentials from the browser so we get them here. - const token = await getAzureCLICredentialsToken(); - const accountName = getAccountName(accountType); - const params = new URLSearchParams(); - params.set("accountName", accountName); - params.set("resourceGroup", resourceGroupName); - params.set("subscriptionId", subscriptionId); - params.set("token", token); - - // There seem to be occasional CORS issues with calling the copilot APIs (/api/tokens/sampledataconnection/v2, for example) - // For now, since we don't test copilot, we can disable the copilot APIs by setting the feature flag to false. - params.set("feature.enableCopilot", "false"); - - const nosqlRbacToken = process.env.NOSQL_TESTACCOUNT_TOKEN; - const nosqlReadOnlyRbacToken = process.env.NOSQL_READONLY_TESTACCOUNT_TOKEN; - const nosqlContainerCopyRbacToken = process.env.NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN; - const tableRbacToken = process.env.TABLE_TESTACCOUNT_TOKEN; - const gremlinRbacToken = process.env.GREMLIN_TESTACCOUNT_TOKEN; - const cassandraRbacToken = process.env.CASSANDRA_TESTACCOUNT_TOKEN; - const mongoRbacToken = process.env.MONGO_TESTACCOUNT_TOKEN; - const mongo32RbacToken = process.env.MONGO32_TESTACCOUNT_TOKEN; - const mongoReadOnlyRbacToken = process.env.MONGO_READONLY_TESTACCOUNT_TOKEN; - - switch (accountType) { - case TestAccount.SQL: - if (nosqlRbacToken) { - params.set("nosqlRbacToken", nosqlRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.SQLContainerCopyOnly: - if (nosqlContainerCopyRbacToken) { - params.set("nosqlRbacToken", nosqlContainerCopyRbacToken); - params.set("enableaaddataplane", "true"); - } - if (enablecontainercopy) { - params.set("enablecontainercopy", "true"); - } - break; - - case TestAccount.SQLReadOnly: - if (nosqlReadOnlyRbacToken) { - params.set("nosqlReadOnlyRbacToken", nosqlReadOnlyRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.Tables: - if (tableRbacToken) { - params.set("tableRbacToken", tableRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.Gremlin: - if (gremlinRbacToken) { - params.set("gremlinRbacToken", gremlinRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.Cassandra: - if (cassandraRbacToken) { - params.set("cassandraRbacToken", cassandraRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.Mongo: - if (mongoRbacToken) { - params.set("mongoRbacToken", mongoRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.Mongo32: - if (mongo32RbacToken) { - params.set("mongo32RbacToken", mongo32RbacToken); - params.set("enableaaddataplane", "true"); - } - break; - - case TestAccount.MongoReadonly: - if (mongoReadOnlyRbacToken) { - params.set("mongoReadOnlyRbacToken", mongoReadOnlyRbacToken); - params.set("enableaaddataplane", "true"); - } - break; - } - - if (iframeSrc) { - params.set("iframeSrc", iframeSrc); - } - - return `https://localhost:1234/testExplorer.html?${params.toString()}`; -} - -type DropdownItemExpectations = { - ariaLabel?: string; - itemCount?: number; -}; - -type DropdownItemMatcher = { - name?: string; - position?: number; -}; - -export async function getDropdownItemByNameOrPosition( - frame: Frame, - matcher?: DropdownItemMatcher, - expectedOptions?: DropdownItemExpectations, -): Promise { - const dropdownItemsWrapper = frame.locator("div.ms-Dropdown-items"); - if (expectedOptions?.ariaLabel) { - expect(await dropdownItemsWrapper.getAttribute("aria-label")).toEqual(expectedOptions.ariaLabel); - } - if (expectedOptions?.itemCount) { - const items = dropdownItemsWrapper.locator("button.ms-Dropdown-item[role='option']"); - await expect(items).toHaveCount(expectedOptions.itemCount); - } - const containerDropdownItems = dropdownItemsWrapper.locator("button.ms-Dropdown-item[role='option']"); - if (matcher?.name) { - return containerDropdownItems.filter({ hasText: matcher.name }); - } else if (matcher?.position !== undefined) { - return containerDropdownItems.nth(matcher.position); - } - // Return first item if no matcher is provided - return containerDropdownItems.first(); -} - -/** Helper class that provides locator methods for TreeNode elements, on top of a Locator */ -class TreeNode { - constructor( - public element: Locator, - public frame: Frame, - public id: string, - ) {} - - async openContextMenu(): Promise { - await this.element.click({ button: "right" }); - } - - contextMenuItem(name: string): Locator { - return this.frame.getByTestId(`TreeNode/ContextMenuItem:${name}`); - } - - async expand(): Promise { - const treeNodeContainer = this.frame.getByTestId(`TreeNodeContainer:${this.id}`); - const tree = this.frame.getByTestId(`Tree:${this.id}`); - - // eslint-disable-next-line prefer-arrow/prefer-arrow-functions - const expandNode = async () => { - if ((await treeNodeContainer.getAttribute("aria-expanded")) !== "true") { - // Click the node, to trigger loading and expansion - await this.element.click(); - } - - // Try three times to wait for the node to expand. - for (let i = 0; i < RETRY_COUNT; i++) { - try { - await tree.waitFor({ state: "visible", timeout: 3 * 1000 }); - // The tree has expanded, let's get out of here - return true; - } catch { - // Just try again - if ((await treeNodeContainer.getAttribute("aria-expanded")) !== "true") { - // We might have collapsed the node, try expanding it again, then retry. - await this.element.click(); - } - } - } - return false; - }; - - if (await expandNode()) { - return; - } - - // The tree never expanded. OR, it may have expanded in between when we found the "ExpandIcon" and when we clicked it (it's happened before) - // So, let's try one more time to expand it. - if (!(await expandNode())) { - // The tree never expanded. This is a problem. - throw new Error(`Node ${this.id} did not expand after clicking it.`); - } - - // We did it. It took a lot of weird messing around, but we expanded a tree node... I hope. - } -} - -export class Editor { - constructor( - public frame: Frame, - public locator: Locator, - ) {} - - text(): Promise { - return this.locator.evaluate((e) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const win = e.ownerDocument.defaultView as any; - if (win._monaco_getEditorContentForElement) { - return win._monaco_getEditorContentForElement(e); - } - return null; - }); - } - - async setText(text: string): Promise { - // We trust that Monaco can handle the keyboard, and it's _extremely_ flaky to try and enter text using browser commands. - // So we use a hook we installed in 'window' to set the content of the editor. - - // NOTE: This function is serialized and sent to the browser for execution - // So you can't use any variables from the outer scope, but we can send a string (via the second argument to evaluate) - await this.locator.evaluate((e, content) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const win = e.ownerDocument.defaultView as any; - if (win._monaco_setEditorContentForElement) { - win._monaco_setEditorContentForElement(e, content); - } - }, text); - - expect(await this.text()).toEqual(text); - } -} - -export class QueryTab { - resultsPane: Locator; - resultsView: Locator; - executeCTA: Locator; - errorList: Locator; - queryStatsList: Locator; - resultsEditor: Editor; - resultsTab: Locator; - queryStatsTab: Locator; - constructor( - public frame: Frame, - public tabId: string, - public tab: Locator, - public locator: Locator, - ) { - this.resultsPane = locator.getByTestId("QueryTab/ResultsPane"); - this.resultsView = locator.getByTestId("QueryTab/ResultsPane/ResultsView"); - this.executeCTA = locator.getByTestId("QueryTab/ResultsPane/ExecuteCTA"); - this.errorList = locator.getByTestId("QueryTab/ResultsPane/ErrorList"); - this.resultsEditor = new Editor(this.frame, this.resultsView.getByTestId("EditorReact/Host/Loaded")); - this.queryStatsList = locator.getByTestId("QueryTab/ResultsPane/ResultsView/QueryStatsList"); - this.resultsTab = this.resultsView.getByTestId("QueryTab/ResultsPane/ResultsView/ResultsTab"); - this.queryStatsTab = this.resultsView.getByTestId("QueryTab/ResultsPane/ResultsView/QueryStatsTab"); - } - - editor(): Editor { - const locator = this.locator.getByTestId("EditorReact/Host/Loaded"); - return new Editor(this.frame, locator); - } -} - -export class DocumentsTab { - documentsFilter: Locator; - documentsListPane: Locator; - documentResultsPane: Locator; - resultsEditor: Editor; - loadMoreButton: Locator; - filterInput: Locator; - filterButton: Locator; - - constructor( - public frame: Frame, - public tabId: string, - public tab: Locator, - public locator: Locator, - ) { - this.documentsFilter = this.locator.getByTestId("DocumentsTab/Filter"); - this.documentsListPane = this.locator.getByTestId("DocumentsTab/DocumentsPane"); - this.documentResultsPane = this.locator.getByTestId("DocumentsTab/ResultsPane"); - this.resultsEditor = new Editor(this.frame, this.documentResultsPane.getByTestId("EditorReact/Host/Loaded")); - this.loadMoreButton = this.documentsListPane.getByTestId("DocumentsTab/LoadMore"); - this.filterInput = this.documentsFilter.getByTestId("DocumentsTab/FilterInput"); - this.filterButton = this.documentsFilter.getByTestId("DocumentsTab/ApplyFilter"); - } - - async setFilter(text: string) { - await this.filterInput.fill(text); - } -} - -type PanelOpenOptions = { - closeTimeout?: number; -}; - -export enum CommandBarButton { - Save = "Save", - Delete = "Delete", - Execute = "Execute", - ExecuteQuery = "Execute Query", - UploadItem = "Upload Item", - NewDocument = "New Document", -} - -/** Helper class that provides locator methods for DataExplorer components, on top of a Frame */ -export class DataExplorer { - constructor(public frame: Frame) {} - - tab(tabId: string): Locator { - return this.frame.getByTestId(`Tab:${tabId}`); - } - - queryTab(tabId: string): QueryTab { - const tab = this.tab(tabId); - const queryTab = tab.getByTestId("QueryTab"); - return new QueryTab(this.frame, tabId, tab, queryTab); - } - - documentsTab(tabId: string): DocumentsTab { - const tab = this.tab(tabId); - const documentsTab = tab.getByTestId("DocumentsTab"); - return new DocumentsTab(this.frame, tabId, tab, documentsTab); - } - - /** Select the primary global command button. - * - * There's only a single "primary" button, but we still require you to pass the label to confirm you're selecting the right button. - */ - async globalCommandButton(label: string): Promise { - await this.frame.getByTestId("GlobalCommands").click(); - return this.frame.getByRole("menuitem", { name: label }); - } - - /** Select the command bar button with the specified label */ - commandBarButton(commandBarButton: CommandBarButton): Locator { - return this.frame.getByTestId(`CommandBar/Button:${commandBarButton}`).and(this.frame.locator("css=button")); - } - - dialogButton(label: string): Locator { - return this.frame.getByTestId(`DialogButton:${label}`).and(this.frame.locator("css=button")); - } - - /** Select the side panel with the specified title */ - panel(title: string): Locator { - return this.frame.getByTestId(`Panel:${title}`); - } - - async waitForNode(treeNodeId: string): Promise { - const node = this.treeNode(treeNodeId); - - // Is the node already visible? - if (await node.element.isVisible()) { - return node; - } - - // No, try refreshing the tree - const refreshButton = this.frame.getByTestId("Sidebar/RefreshButton"); - await refreshButton.click(); - - // Try a few times to find the node - for (let i = 0; i < RETRY_COUNT; i++) { - try { - await node.element.waitFor(); - return node; - } catch { - // Just try again - } - } - - // We tried 3 times, but the node never appeared - throw new Error(`Node ${treeNodeId} not found and did not appear after refreshing.`); - } - - async waitForContainerNode(databaseId: string, containerId: string): Promise { - const databaseNode = await this.waitForNode(databaseId); - - // The container node may be auto-expanded. Wait 5s for that to happen - try { - const containerNode = this.treeNode(`${databaseId}/${containerId}`); - await containerNode.element.waitFor({ state: "visible", timeout: 5 * 1000 }); - return containerNode; - } catch { - // It didn't auto-expand, that's fine, we'll expand it ourselves - } - - // Ok, expand the database node. - await databaseNode.expand(); - - return await this.waitForNode(`${databaseId}/${containerId}`); - } - - async waitForContainerItemsNode(databaseId: string, containerId: string): Promise { - return await this.waitForNode(`${databaseId}/${containerId}/Items`); - } - - async waitForContainerDocumentsNode(databaseId: string, containerId: string): Promise { - return await this.waitForNode(`${databaseId}/${containerId}/Documents`); - } - - async waitForCommandBarButton(label: CommandBarButton, timeout?: number): Promise { - const commandBar = this.commandBarButton(label); - await commandBar.waitFor({ state: "visible", timeout }); - return commandBar; - } - - async waitForDialogButton(label: string, timeout?: number): Promise { - const dialogButton = this.dialogButton(label); - await dialogButton.waitFor({ timeout }); - return dialogButton; - } - - /** Select the tree node with the specified id */ - treeNode(id: string): TreeNode { - return new TreeNode(this.frame.getByTestId(`TreeNode:${id}`), this.frame, id); - } - - /** Waits for the panel with the specified title to be open, then runs the provided callback. After the callback completes, waits for the panel to close. */ - async whilePanelOpen( - title: string, - action: (panel: Locator, okButton: Locator) => Promise, - options?: PanelOpenOptions, - ): Promise { - options ||= {}; - - const panel = this.panel(title); - await panel.waitFor(); - const okButton = panel.getByTestId("Panel/OkButton"); - await action(panel, okButton); - 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 */ - getConsoleHeaderStatus(): Locator { - return this.frame.getByTestId("notification-console/header-status"); - } - - async expandNotificationConsole(): Promise { - await this.setNotificationConsoleExpanded(true); - } - - async collapseNotificationConsole(): Promise { - await this.setNotificationConsoleExpanded(false); - } - - async setNotificationConsoleExpanded(expanded: boolean): Promise { - const notificationConsoleToggleButton = this.frame.getByTestId("NotificationConsole/ExpandCollapseButton"); - const alt = await notificationConsoleToggleButton.locator("img").getAttribute("alt"); - - // When expanded, the icon says "Collapse icon" - if (expanded && alt === "Expand icon") { - await notificationConsoleToggleButton.click(); - } else if (!expanded && alt === "Collapse icon") { - await notificationConsoleToggleButton.click(); - } - } - - async getNotificationConsoleMessages(): Promise { - await this.setNotificationConsoleExpanded(true); - return this.frame.getByTestId("NotificationConsole/Contents"); - } - - async getDropdownItemByName(name: string, ariaLabel?: string): Promise { - const dropdownItemsWrapper = this.frame.locator("div.ms-Dropdown-items"); - if (ariaLabel) { - expect(await dropdownItemsWrapper.getAttribute("aria-label")).toEqual(ariaLabel); - } - const containerDropdownItems = dropdownItemsWrapper.locator("button.ms-Dropdown-item[role='option']"); - return containerDropdownItems.filter({ hasText: name }); - } - - /** Waits for the Data Explorer app to load */ - static async waitForExplorer(page: Page, options?: TestExplorerUrlOptions): Promise { - const iframeElement = await page.getByTestId("DataExplorerFrame").elementHandle(); - if (iframeElement === null) { - throw new Error("Explorer iframe not found"); - } - - const explorerFrame = await iframeElement.contentFrame(); - - if (explorerFrame === null) { - throw new Error("Explorer frame not found"); - } - - if (!options?.enablecontainercopy) { - await explorerFrame?.getByTestId("DataExplorerRoot").waitFor(); - } - - return new DataExplorer(explorerFrame); - } - - /** Opens the Data Explorer app using the specified test account (and optionally, the provided IFRAME src url). */ - static async open(page: Page, testAccount: TestAccount, iframeSrc?: string): Promise { - const url = await getTestExplorerUrl(testAccount, { iframeSrc }); - await page.goto(url); - return DataExplorer.waitForExplorer(page); - } -} - -export async function waitForApiResponse( - page: Page, - urlPattern: string, - method?: string, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - payloadValidator?: (payload: any) => boolean, -) { - try { - // Check if page is still valid before waiting - if (page.isClosed()) { - throw new Error(`Page is closed, cannot wait for API response: ${urlPattern}`); - } - - return page.waitForResponse( - async (response) => { - const request = response.request(); - - if (!request.url().includes(urlPattern)) { - return false; - } - - if (method && request.method() !== method) { - return false; - } - - if (payloadValidator && (request.method() === "POST" || request.method() === "PUT")) { - const postData = request.postData(); - if (postData) { - try { - const payload = JSON.parse(postData); - return payloadValidator(payload); - } catch { - return false; - } - } - } - return true; - }, - { timeout: 60 * 1000 }, - ); - } catch (error) { - if (error instanceof Error && error.message.includes("Target page, context or browser has been closed")) { - console.warn("Page was closed while waiting for API response:", urlPattern); - throw new Error(`Page closed while waiting for API response: ${urlPattern}`); - } - throw error; - } -} -export async function interceptAndInspectApiRequest( - page: Page, - urlPattern: string, - method: string = "POST", - error: Error, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - errorValidator: (url?: string, payload?: any) => boolean, -): Promise { - await page.route( - (url) => url.pathname.includes(urlPattern), - async (route, request) => { - if (request.method() !== method) { - await route.continue(); - return; - } - const postData = request.postData(); - if (postData) { - try { - const payload = JSON.parse(postData); - if (errorValidator && errorValidator(request.url(), payload)) { - await route.fulfill({ - status: 409, - contentType: "application/json", - body: JSON.stringify({ - code: "Conflict", - message: error.message, - }), - }); - return; - } - } catch (err) { - if (err instanceof Error && err.message.includes("not allowed")) { - throw err; - } - } - } - - await route.continue(); - }, - ); -} - -export class ContainerCopy { - constructor( - public frame: Frame, - public wrapper: Locator, - ) {} - - static async waitForContainerCopy(page: Page): Promise { - const explorerFrame = await DataExplorer.waitForExplorer(page, { enablecontainercopy: true }); - const containerCopyWrapper = explorerFrame.frame.locator("div#containerCopyWrapper"); - return new ContainerCopy(explorerFrame.frame, containerCopyWrapper); - } - - static async open(page: Page, testAccount: TestAccount, iframeSrc?: string): Promise { - const url = await getTestExplorerUrl(testAccount, { iframeSrc, enablecontainercopy: true }); - await page.goto(url); - return ContainerCopy.waitForContainerCopy(page); - } -} diff --git a/.vs/CopilotSnapshots/D94A63B0B7BF1348926679F1C2D0F6F9/state.mpack b/.vs/CopilotSnapshots/D94A63B0B7BF1348926679F1C2D0F6F9/state.mpack deleted file mode 100644 index 24eee73cd..000000000 Binary files a/.vs/CopilotSnapshots/D94A63B0B7BF1348926679F1C2D0F6F9/state.mpack and /dev/null differ diff --git a/.vs/ProjectSettings.json b/.vs/ProjectSettings.json deleted file mode 100644 index f8b488856..000000000 --- a/.vs/ProjectSettings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "CurrentProjectSetting": null -} \ No newline at end of file diff --git a/.vs/VSWorkspaceState.json b/.vs/VSWorkspaceState.json deleted file mode 100644 index e9f6a167d..000000000 --- a/.vs/VSWorkspaceState.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "ExpandedNodes": [ - "" - ], - "SelectedNode": "\\utils", - "PreviewInSolutionExplorer": false -} \ No newline at end of file diff --git a/.vs/cosmos-explorer.slnx/FileContentIndex/094519c6-1a5b-4adf-bffa-370670d65672.vsidx b/.vs/cosmos-explorer.slnx/FileContentIndex/094519c6-1a5b-4adf-bffa-370670d65672.vsidx deleted file mode 100644 index bc1921957..000000000 Binary files a/.vs/cosmos-explorer.slnx/FileContentIndex/094519c6-1a5b-4adf-bffa-370670d65672.vsidx and /dev/null differ diff --git a/.vs/cosmos-explorer.slnx/FileContentIndex/393b64f7-3776-4c53-a0ae-59b642adf304.vsidx b/.vs/cosmos-explorer.slnx/FileContentIndex/393b64f7-3776-4c53-a0ae-59b642adf304.vsidx deleted file mode 100644 index e18d66823..000000000 Binary files a/.vs/cosmos-explorer.slnx/FileContentIndex/393b64f7-3776-4c53-a0ae-59b642adf304.vsidx and /dev/null differ diff --git a/.vs/cosmos-explorer.slnx/FileContentIndex/53c17dc0-d230-402a-87bf-e04fc2b680f0.vsidx b/.vs/cosmos-explorer.slnx/FileContentIndex/53c17dc0-d230-402a-87bf-e04fc2b680f0.vsidx deleted file mode 100644 index 90f4d5f06..000000000 Binary files a/.vs/cosmos-explorer.slnx/FileContentIndex/53c17dc0-d230-402a-87bf-e04fc2b680f0.vsidx and /dev/null differ diff --git a/.vs/cosmos-explorer.slnx/FileContentIndex/6d8db478-e113-47cc-b2ac-a5735e54484b.vsidx b/.vs/cosmos-explorer.slnx/FileContentIndex/6d8db478-e113-47cc-b2ac-a5735e54484b.vsidx deleted file mode 100644 index 9bd6648c5..000000000 Binary files a/.vs/cosmos-explorer.slnx/FileContentIndex/6d8db478-e113-47cc-b2ac-a5735e54484b.vsidx and /dev/null differ diff --git a/.vs/cosmos-explorer.slnx/FileContentIndex/8de9df71-a4ad-4e18-9ff2-76551d7a1b62.vsidx b/.vs/cosmos-explorer.slnx/FileContentIndex/8de9df71-a4ad-4e18-9ff2-76551d7a1b62.vsidx deleted file mode 100644 index eddd41fdf..000000000 Binary files a/.vs/cosmos-explorer.slnx/FileContentIndex/8de9df71-a4ad-4e18-9ff2-76551d7a1b62.vsidx and /dev/null differ diff --git a/.vs/cosmos-explorer.slnx/FileContentIndex/bf9da8bf-cf23-41b4-8edd-31fcc6973de6.vsidx b/.vs/cosmos-explorer.slnx/FileContentIndex/bf9da8bf-cf23-41b4-8edd-31fcc6973de6.vsidx deleted file mode 100644 index 5e8bdcfdc..000000000 Binary files a/.vs/cosmos-explorer.slnx/FileContentIndex/bf9da8bf-cf23-41b4-8edd-31fcc6973de6.vsidx and /dev/null differ diff --git a/.vs/cosmos-explorer.slnx/config/applicationhost.config b/.vs/cosmos-explorer.slnx/config/applicationhost.config deleted file mode 100644 index 0d88f0db3..000000000 --- a/.vs/cosmos-explorer.slnx/config/applicationhost.config +++ /dev/null @@ -1,1016 +0,0 @@ - - - - - - - -
-
-
-
-
-
-
-
- - - -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
- -
-
-
-
-
-
- -
-
-
-
-
- -
-
-
- -
-
- -
-
- -
-
-
- - -
-
-
-
-
-
- -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.vs/cosmos-explorer.slnx/copilot-chat/2e5ec468/sessions/b0634ad9-bfb7-4813-9266-79f1c2d0f6f9 b/.vs/cosmos-explorer.slnx/copilot-chat/2e5ec468/sessions/b0634ad9-bfb7-4813-9266-79f1c2d0f6f9 deleted file mode 100644 index 66cd9c05f..000000000 Binary files a/.vs/cosmos-explorer.slnx/copilot-chat/2e5ec468/sessions/b0634ad9-bfb7-4813-9266-79f1c2d0f6f9 and /dev/null differ diff --git a/.vs/cosmos-explorer.slnx/copilot-chat/3bf6af7d/sessions/3c45e421-9f5e-4a6b-ac2d-8336fac4a365 b/.vs/cosmos-explorer.slnx/copilot-chat/3bf6af7d/sessions/3c45e421-9f5e-4a6b-ac2d-8336fac4a365 deleted file mode 100644 index 8fccc7176..000000000 Binary files a/.vs/cosmos-explorer.slnx/copilot-chat/3bf6af7d/sessions/3c45e421-9f5e-4a6b-ac2d-8336fac4a365 and /dev/null differ diff --git a/.vs/cosmos-explorer.slnx/copilot-chat/3bf6af7d/sessions/5a583057-3675-443a-a280-bde9b502525c b/.vs/cosmos-explorer.slnx/copilot-chat/3bf6af7d/sessions/5a583057-3675-443a-a280-bde9b502525c deleted file mode 100644 index cf321d447..000000000 Binary files a/.vs/cosmos-explorer.slnx/copilot-chat/3bf6af7d/sessions/5a583057-3675-443a-a280-bde9b502525c and /dev/null differ diff --git a/.vs/cosmos-explorer.slnx/copilot-chat/3bf6af7d/sessions/789645ca-6927-4190-8fce-0453e6d9e810 b/.vs/cosmos-explorer.slnx/copilot-chat/3bf6af7d/sessions/789645ca-6927-4190-8fce-0453e6d9e810 deleted file mode 100644 index ecb1c8d31..000000000 Binary files a/.vs/cosmos-explorer.slnx/copilot-chat/3bf6af7d/sessions/789645ca-6927-4190-8fce-0453e6d9e810 and /dev/null differ diff --git a/.vs/cosmos-explorer.slnx/copilot-chat/3bf6af7d/sessions/a60f5d5b-a040-440a-a221-4fb20f0e91c9 b/.vs/cosmos-explorer.slnx/copilot-chat/3bf6af7d/sessions/a60f5d5b-a040-440a-a221-4fb20f0e91c9 deleted file mode 100644 index 7f32c93f8..000000000 Binary files a/.vs/cosmos-explorer.slnx/copilot-chat/3bf6af7d/sessions/a60f5d5b-a040-440a-a221-4fb20f0e91c9 and /dev/null differ diff --git a/.vs/cosmos-explorer.slnx/copilot-chat/3bf6af7d/sessions/aaee2f9c-0370-4b22-a917-5196d9bfc46e b/.vs/cosmos-explorer.slnx/copilot-chat/3bf6af7d/sessions/aaee2f9c-0370-4b22-a917-5196d9bfc46e deleted file mode 100644 index c77f49581..000000000 Binary files a/.vs/cosmos-explorer.slnx/copilot-chat/3bf6af7d/sessions/aaee2f9c-0370-4b22-a917-5196d9bfc46e and /dev/null differ diff --git a/.vs/cosmos-explorer.slnx/copilot-chat/3bf6af7d/sessions/d5568e54-7b11-4942-b654-f2de5ea27c25 b/.vs/cosmos-explorer.slnx/copilot-chat/3bf6af7d/sessions/d5568e54-7b11-4942-b654-f2de5ea27c25 deleted file mode 100644 index b96c3bf3a..000000000 Binary files a/.vs/cosmos-explorer.slnx/copilot-chat/3bf6af7d/sessions/d5568e54-7b11-4942-b654-f2de5ea27c25 and /dev/null differ diff --git a/.vs/cosmos-explorer.slnx/copilot-chat/3bf6af7d/sessions/d822ae7a-02b6-4cc3-b6cb-7e44e67fb576 b/.vs/cosmos-explorer.slnx/copilot-chat/3bf6af7d/sessions/d822ae7a-02b6-4cc3-b6cb-7e44e67fb576 deleted file mode 100644 index 0f9e3f4b8..000000000 Binary files a/.vs/cosmos-explorer.slnx/copilot-chat/3bf6af7d/sessions/d822ae7a-02b6-4cc3-b6cb-7e44e67fb576 and /dev/null differ diff --git a/.vs/cosmos-explorer.slnx/copilot-chat/3bf6af7d/sessions/ecd00b42-06ad-4140-bf41-d50da8f9f5b6 b/.vs/cosmos-explorer.slnx/copilot-chat/3bf6af7d/sessions/ecd00b42-06ad-4140-bf41-d50da8f9f5b6 deleted file mode 100644 index c56d95f9a..000000000 Binary files a/.vs/cosmos-explorer.slnx/copilot-chat/3bf6af7d/sessions/ecd00b42-06ad-4140-bf41-d50da8f9f5b6 and /dev/null differ diff --git a/.vs/cosmos-explorer.slnx/v18/.wsuo b/.vs/cosmos-explorer.slnx/v18/.wsuo deleted file mode 100644 index 3ad90af73..000000000 Binary files a/.vs/cosmos-explorer.slnx/v18/.wsuo and /dev/null differ diff --git a/.vs/cosmos-explorer.slnx/v18/DocumentLayout.backup.json b/.vs/cosmos-explorer.slnx/v18/DocumentLayout.backup.json deleted file mode 100644 index 10f49ae06..000000000 --- a/.vs/cosmos-explorer.slnx/v18/DocumentLayout.backup.json +++ /dev/null @@ -1,217 +0,0 @@ -{ - "Version": 1, - "WorkspaceRootPath": "C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\", - "Documents": [ - { - "AbsoluteMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\testData.ts||{0F2454B1-A556-402D-A7D0-1FDE7F99DEE0}", - "RelativeMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|solutionrelative:test\\testData.ts||{0F2454B1-A556-402D-A7D0-1FDE7F99DEE0}" - }, - { - "AbsoluteMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\sql\\scaleAndSettings\\sharedThroughput.spec.ts||{0F2454B1-A556-402D-A7D0-1FDE7F99DEE0}", - "RelativeMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|solutionrelative:test\\sql\\scaleAndSettings\\sharedThroughput.spec.ts||{0F2454B1-A556-402D-A7D0-1FDE7F99DEE0}" - }, - { - "AbsoluteMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\mongo\\pagination.spec.ts||{0F2454B1-A556-402D-A7D0-1FDE7F99DEE0}", - "RelativeMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|solutionrelative:test\\mongo\\pagination.spec.ts||{0F2454B1-A556-402D-A7D0-1FDE7F99DEE0}" - }, - { - "AbsoluteMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\sql\\container.spec.ts||{0F2454B1-A556-402D-A7D0-1FDE7F99DEE0}", - "RelativeMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|solutionrelative:test\\sql\\container.spec.ts||{0F2454B1-A556-402D-A7D0-1FDE7F99DEE0}" - }, - { - "AbsoluteMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\sql\\query.spec.ts||{0F2454B1-A556-402D-A7D0-1FDE7F99DEE0}", - "RelativeMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|solutionrelative:test\\sql\\query.spec.ts||{0F2454B1-A556-402D-A7D0-1FDE7F99DEE0}" - }, - { - "AbsoluteMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\src\\Common\\MongoProxyClient.ts||{0F2454B1-A556-402D-A7D0-1FDE7F99DEE0}", - "RelativeMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|solutionrelative:src\\Common\\MongoProxyClient.ts||{0F2454B1-A556-402D-A7D0-1FDE7F99DEE0}" - }, - { - "AbsoluteMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\sql\\scaleAndSettings\\scale.spec.ts||{0F2454B1-A556-402D-A7D0-1FDE7F99DEE0}", - "RelativeMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|solutionrelative:test\\sql\\scaleAndSettings\\scale.spec.ts||{0F2454B1-A556-402D-A7D0-1FDE7F99DEE0}" - }, - { - "AbsoluteMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\sql\\scaleAndSettings\\settings.spec.ts||{0F2454B1-A556-402D-A7D0-1FDE7F99DEE0}", - "RelativeMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|solutionrelative:test\\sql\\scaleAndSettings\\settings.spec.ts||{0F2454B1-A556-402D-A7D0-1FDE7F99DEE0}" - }, - { - "AbsoluteMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\sql\\scaleAndSettings\\computedProperties.spec.ts||{0F2454B1-A556-402D-A7D0-1FDE7F99DEE0}", - "RelativeMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|solutionrelative:test\\sql\\scaleAndSettings\\computedProperties.spec.ts||{0F2454B1-A556-402D-A7D0-1FDE7F99DEE0}" - }, - { - "AbsoluteMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\sql\\containercopy.spec.ts||{0F2454B1-A556-402D-A7D0-1FDE7F99DEE0}", - "RelativeMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|solutionrelative:test\\sql\\containercopy.spec.ts||{0F2454B1-A556-402D-A7D0-1FDE7F99DEE0}" - }, - { - "AbsoluteMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\README.md||{EFC0BB08-EA7D-40C6-A696-C870411A895B}", - "RelativeMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|solutionrelative:test\\README.md||{EFC0BB08-EA7D-40C6-A696-C870411A895B}" - }, - { - "AbsoluteMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\resources\\README.md||{EFC0BB08-EA7D-40C6-A696-C870411A895B}", - "RelativeMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|solutionrelative:test\\resources\\README.md||{EFC0BB08-EA7D-40C6-A696-C870411A895B}" - } - ], - "DocumentGroupContainers": [ - { - "Orientation": 0, - "VerticalTabListWidth": 256, - "DocumentGroups": [ - { - "DockedWidth": 200, - "SelectedChildIndex": 1, - "Children": [ - { - "$type": "Bookmark", - "Name": "ST:0:0:{56df62a4-05a3-4e5b-aa1a-99371ccfb997}" - }, - { - "$type": "Document", - "DocumentIndex": 0, - "Title": "testData.ts", - "DocumentMoniker": "C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\testData.ts", - "RelativeDocumentMoniker": "test\\testData.ts", - "ToolTip": "C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\testData.ts", - "RelativeToolTip": "test\\testData.ts", - "ViewState": "AgIAAGsAAACol5mZmZknwLAAAAAAAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.003213|", - "WhenOpened": "2026-01-15T01:02:33.958Z", - "EditorCaption": "" - }, - { - "$type": "Document", - "DocumentIndex": 4, - "Title": "query.spec.ts", - "DocumentMoniker": "C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\sql\\query.spec.ts", - "RelativeDocumentMoniker": "test\\sql\\query.spec.ts", - "ToolTip": "C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\sql\\query.spec.ts", - "RelativeToolTip": "test\\sql\\query.spec.ts", - "ViewState": "AgIAAAcAAAAAMzMzMzMrwB0AAABNAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.003213|", - "WhenOpened": "2026-01-14T00:33:23.32Z" - }, - { - "$type": "Document", - "DocumentIndex": 8, - "Title": "computedProperties.spec.ts", - "DocumentMoniker": "C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\sql\\scaleAndSettings\\computedProperties.spec.ts", - "RelativeDocumentMoniker": "test\\sql\\scaleAndSettings\\computedProperties.spec.ts", - "ToolTip": "C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\sql\\scaleAndSettings\\computedProperties.spec.ts", - "RelativeToolTip": "test\\sql\\scaleAndSettings\\computedProperties.spec.ts", - "ViewState": "AgIAABkAAAAAAAAAAAAuwC4AAAAHAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.003213|", - "WhenOpened": "2026-01-13T21:25:14.962Z" - }, - { - "$type": "Document", - "DocumentIndex": 1, - "Title": "sharedThroughput.spec.ts", - "DocumentMoniker": "C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\sql\\scaleAndSettings\\sharedThroughput.spec.ts", - "RelativeDocumentMoniker": "test\\sql\\scaleAndSettings\\sharedThroughput.spec.ts", - "ToolTip": "C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\sql\\scaleAndSettings\\sharedThroughput.spec.ts", - "RelativeToolTip": "test\\sql\\scaleAndSettings\\sharedThroughput.spec.ts", - "ViewState": "AgIAAAAAAAAAAAAAAAAAAAkAAAA+AAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.003213|", - "WhenOpened": "2026-01-13T20:50:42.998Z" - }, - { - "$type": "Document", - "DocumentIndex": 2, - "Title": "pagination.spec.ts", - "DocumentMoniker": "C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\mongo\\pagination.spec.ts", - "RelativeDocumentMoniker": "test\\mongo\\pagination.spec.ts", - "ToolTip": "C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\mongo\\pagination.spec.ts", - "RelativeToolTip": "test\\mongo\\pagination.spec.ts", - "ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.003213|", - "WhenOpened": "2026-01-14T04:48:29.613Z" - }, - { - "$type": "Document", - "DocumentIndex": 3, - "Title": "container.spec.ts", - "DocumentMoniker": "C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\sql\\container.spec.ts", - "RelativeDocumentMoniker": "test\\sql\\container.spec.ts", - "ToolTip": "C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\sql\\container.spec.ts", - "RelativeToolTip": "test\\sql\\container.spec.ts", - "ViewState": "AgIAAAAAAAAAAAAAAAAAAAQAAAAGAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.003213|", - "WhenOpened": "2026-01-14T04:05:21.4Z" - }, - { - "$type": "Document", - "DocumentIndex": 5, - "Title": "MongoProxyClient.ts", - "DocumentMoniker": "C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\src\\Common\\MongoProxyClient.ts", - "RelativeDocumentMoniker": "src\\Common\\MongoProxyClient.ts", - "ToolTip": "C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\src\\Common\\MongoProxyClient.ts", - "RelativeToolTip": "src\\Common\\MongoProxyClient.ts", - "ViewState": "AgIAABMAAABAZmZmZqYvwCgAAABRAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.003213|", - "WhenOpened": "2026-01-09T07:08:06.873Z" - }, - { - "$type": "Document", - "DocumentIndex": 7, - "Title": "settings.spec.ts", - "DocumentMoniker": "C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\sql\\scaleAndSettings\\settings.spec.ts", - "RelativeDocumentMoniker": "test\\sql\\scaleAndSettings\\settings.spec.ts", - "ToolTip": "C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\sql\\scaleAndSettings\\settings.spec.ts", - "RelativeToolTip": "test\\sql\\scaleAndSettings\\settings.spec.ts", - "ViewState": "AgIAAAsAAAAAAAAAAAAuwCQAAAAGAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.003213|", - "WhenOpened": "2026-01-13T21:25:50.263Z" - }, - { - "$type": "Document", - "DocumentIndex": 6, - "Title": "scale.spec.ts", - "DocumentMoniker": "C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\sql\\scaleAndSettings\\scale.spec.ts", - "RelativeDocumentMoniker": "test\\sql\\scaleAndSettings\\scale.spec.ts", - "ToolTip": "C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\sql\\scaleAndSettings\\scale.spec.ts", - "RelativeToolTip": "test\\sql\\scaleAndSettings\\scale.spec.ts", - "ViewState": "AgIAAAgAAAAAAAAAAAAuwB0AAAAHAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.003213|", - "WhenOpened": "2026-01-13T21:25:27.321Z" - }, - { - "$type": "Document", - "DocumentIndex": 9, - "Title": "containercopy.spec.ts", - "DocumentMoniker": "C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\sql\\containercopy.spec.ts", - "RelativeDocumentMoniker": "test\\sql\\containercopy.spec.ts", - "ToolTip": "C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\sql\\containercopy.spec.ts", - "RelativeToolTip": "test\\sql\\containercopy.spec.ts", - "ViewState": "AgIAABsAAAAAAAAAAAAuwDAAAAAIAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.003213|", - "WhenOpened": "2026-01-13T21:25:09.469Z" - }, - { - "$type": "Document", - "DocumentIndex": 10, - "Title": "README.md", - "DocumentMoniker": "C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\README.md", - "RelativeDocumentMoniker": "test\\README.md", - "ToolTip": "C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\README.md", - "RelativeToolTip": "test\\README.md", - "ViewState": "AgIAADcAAADAlpmZmdkhwF4AAAAAAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.001818|", - "WhenOpened": "2026-01-13T20:44:59.892Z" - }, - { - "$type": "Document", - "DocumentIndex": 11, - "Title": "README.md", - "DocumentMoniker": "C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\resources\\README.md", - "RelativeDocumentMoniker": "test\\resources\\README.md", - "ToolTip": "C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\resources\\README.md", - "RelativeToolTip": "test\\resources\\README.md", - "ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.001818|", - "WhenOpened": "2026-01-13T20:44:58.329Z" - } - ] - } - ] - } - ] -} \ No newline at end of file diff --git a/.vs/cosmos-explorer.slnx/v18/DocumentLayout.json b/.vs/cosmos-explorer.slnx/v18/DocumentLayout.json deleted file mode 100644 index 66d9549f0..000000000 --- a/.vs/cosmos-explorer.slnx/v18/DocumentLayout.json +++ /dev/null @@ -1,235 +0,0 @@ -{ - "Version": 1, - "WorkspaceRootPath": "C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\", - "Documents": [ - { - "AbsoluteMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\fx.ts||{0F2454B1-A556-402D-A7D0-1FDE7F99DEE0}", - "RelativeMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|solutionrelative:test\\fx.ts||{0F2454B1-A556-402D-A7D0-1FDE7F99DEE0}" - }, - { - "AbsoluteMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\sql\\scaleAndSettings\\sharedThroughput.spec.ts||{0F2454B1-A556-402D-A7D0-1FDE7F99DEE0}", - "RelativeMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|solutionrelative:test\\sql\\scaleAndSettings\\sharedThroughput.spec.ts||{0F2454B1-A556-402D-A7D0-1FDE7F99DEE0}" - }, - { - "AbsoluteMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\testData.ts||{0F2454B1-A556-402D-A7D0-1FDE7F99DEE0}", - "RelativeMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|solutionrelative:test\\testData.ts||{0F2454B1-A556-402D-A7D0-1FDE7F99DEE0}" - }, - { - "AbsoluteMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\mongo\\pagination.spec.ts||{0F2454B1-A556-402D-A7D0-1FDE7F99DEE0}", - "RelativeMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|solutionrelative:test\\mongo\\pagination.spec.ts||{0F2454B1-A556-402D-A7D0-1FDE7F99DEE0}" - }, - { - "AbsoluteMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\sql\\container.spec.ts||{0F2454B1-A556-402D-A7D0-1FDE7F99DEE0}", - "RelativeMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|solutionrelative:test\\sql\\container.spec.ts||{0F2454B1-A556-402D-A7D0-1FDE7F99DEE0}" - }, - { - "AbsoluteMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\sql\\query.spec.ts||{0F2454B1-A556-402D-A7D0-1FDE7F99DEE0}", - "RelativeMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|solutionrelative:test\\sql\\query.spec.ts||{0F2454B1-A556-402D-A7D0-1FDE7F99DEE0}" - }, - { - "AbsoluteMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\src\\Common\\MongoProxyClient.ts||{0F2454B1-A556-402D-A7D0-1FDE7F99DEE0}", - "RelativeMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|solutionrelative:src\\Common\\MongoProxyClient.ts||{0F2454B1-A556-402D-A7D0-1FDE7F99DEE0}" - }, - { - "AbsoluteMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\sql\\scaleAndSettings\\scale.spec.ts||{0F2454B1-A556-402D-A7D0-1FDE7F99DEE0}", - "RelativeMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|solutionrelative:test\\sql\\scaleAndSettings\\scale.spec.ts||{0F2454B1-A556-402D-A7D0-1FDE7F99DEE0}" - }, - { - "AbsoluteMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\sql\\scaleAndSettings\\settings.spec.ts||{0F2454B1-A556-402D-A7D0-1FDE7F99DEE0}", - "RelativeMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|solutionrelative:test\\sql\\scaleAndSettings\\settings.spec.ts||{0F2454B1-A556-402D-A7D0-1FDE7F99DEE0}" - }, - { - "AbsoluteMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\sql\\scaleAndSettings\\computedProperties.spec.ts||{0F2454B1-A556-402D-A7D0-1FDE7F99DEE0}", - "RelativeMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|solutionrelative:test\\sql\\scaleAndSettings\\computedProperties.spec.ts||{0F2454B1-A556-402D-A7D0-1FDE7F99DEE0}" - }, - { - "AbsoluteMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\sql\\containercopy.spec.ts||{0F2454B1-A556-402D-A7D0-1FDE7F99DEE0}", - "RelativeMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|solutionrelative:test\\sql\\containercopy.spec.ts||{0F2454B1-A556-402D-A7D0-1FDE7F99DEE0}" - }, - { - "AbsoluteMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\README.md||{EFC0BB08-EA7D-40C6-A696-C870411A895B}", - "RelativeMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|solutionrelative:test\\README.md||{EFC0BB08-EA7D-40C6-A696-C870411A895B}" - }, - { - "AbsoluteMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\resources\\README.md||{EFC0BB08-EA7D-40C6-A696-C870411A895B}", - "RelativeMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|solutionrelative:test\\resources\\README.md||{EFC0BB08-EA7D-40C6-A696-C870411A895B}" - } - ], - "DocumentGroupContainers": [ - { - "Orientation": 0, - "VerticalTabListWidth": 256, - "DocumentGroups": [ - { - "DockedWidth": 200, - "SelectedChildIndex": 1, - "Children": [ - { - "$type": "Bookmark", - "Name": "ST:0:0:{56df62a4-05a3-4e5b-aa1a-99371ccfb997}" - }, - { - "$type": "Document", - "DocumentIndex": 0, - "Title": "fx.ts", - "DocumentMoniker": "C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\fx.ts", - "RelativeDocumentMoniker": "test\\fx.ts", - "ToolTip": "C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\fx.ts", - "RelativeToolTip": "test\\fx.ts", - "ViewState": "AgIAAGsCAAAANzMzMzMMwHMCAAANAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.003213|", - "WhenOpened": "2026-02-05T00:31:11.555Z", - "EditorCaption": "" - }, - { - "$type": "Document", - "DocumentIndex": 2, - "Title": "testData.ts", - "DocumentMoniker": "C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\testData.ts", - "RelativeDocumentMoniker": "test\\testData.ts", - "ToolTip": "C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\testData.ts", - "RelativeToolTip": "test\\testData.ts", - "ViewState": "AgIAAGoAAACol5mZmZknwK8AAAAAAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.003213|", - "WhenOpened": "2026-01-15T01:02:33.958Z", - "EditorCaption": "" - }, - { - "$type": "Document", - "DocumentIndex": 5, - "Title": "query.spec.ts", - "DocumentMoniker": "C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\sql\\query.spec.ts", - "RelativeDocumentMoniker": "test\\sql\\query.spec.ts", - "ToolTip": "C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\sql\\query.spec.ts", - "RelativeToolTip": "test\\sql\\query.spec.ts", - "ViewState": "AgIAAAcAAAAAMzMzMzMrwB0AAABNAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.003213|", - "WhenOpened": "2026-01-14T00:33:23.32Z" - }, - { - "$type": "Document", - "DocumentIndex": 9, - "Title": "computedProperties.spec.ts", - "DocumentMoniker": "C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\sql\\scaleAndSettings\\computedProperties.spec.ts", - "RelativeDocumentMoniker": "test\\sql\\scaleAndSettings\\computedProperties.spec.ts", - "ToolTip": "C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\sql\\scaleAndSettings\\computedProperties.spec.ts", - "RelativeToolTip": "test\\sql\\scaleAndSettings\\computedProperties.spec.ts", - "ViewState": "AgIAABkAAAAAAAAAAAAuwC4AAAAHAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.003213|", - "WhenOpened": "2026-01-13T21:25:14.962Z" - }, - { - "$type": "Document", - "DocumentIndex": 1, - "Title": "sharedThroughput.spec.ts", - "DocumentMoniker": "C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\sql\\scaleAndSettings\\sharedThroughput.spec.ts", - "RelativeDocumentMoniker": "test\\sql\\scaleAndSettings\\sharedThroughput.spec.ts", - "ToolTip": "C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\sql\\scaleAndSettings\\sharedThroughput.spec.ts", - "RelativeToolTip": "test\\sql\\scaleAndSettings\\sharedThroughput.spec.ts", - "ViewState": "AgIAAJMAAAAASAAAAIAZwJwAAAApAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.003213|", - "WhenOpened": "2026-01-13T20:50:42.998Z", - "EditorCaption": "" - }, - { - "$type": "Document", - "DocumentIndex": 3, - "Title": "pagination.spec.ts", - "DocumentMoniker": "C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\mongo\\pagination.spec.ts", - "RelativeDocumentMoniker": "test\\mongo\\pagination.spec.ts", - "ToolTip": "C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\mongo\\pagination.spec.ts", - "RelativeToolTip": "test\\mongo\\pagination.spec.ts", - "ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.003213|", - "WhenOpened": "2026-01-14T04:48:29.613Z" - }, - { - "$type": "Document", - "DocumentIndex": 4, - "Title": "container.spec.ts", - "DocumentMoniker": "C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\sql\\container.spec.ts", - "RelativeDocumentMoniker": "test\\sql\\container.spec.ts", - "ToolTip": "C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\sql\\container.spec.ts", - "RelativeToolTip": "test\\sql\\container.spec.ts", - "ViewState": "AgIAAAAAAAAAAAAAAAAAAAQAAAAGAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.003213|", - "WhenOpened": "2026-01-14T04:05:21.4Z" - }, - { - "$type": "Document", - "DocumentIndex": 6, - "Title": "MongoProxyClient.ts", - "DocumentMoniker": "C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\src\\Common\\MongoProxyClient.ts", - "RelativeDocumentMoniker": "src\\Common\\MongoProxyClient.ts", - "ToolTip": "C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\src\\Common\\MongoProxyClient.ts", - "RelativeToolTip": "src\\Common\\MongoProxyClient.ts", - "ViewState": "AgIAABMAAABAZmZmZqYvwCgAAABRAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.003213|", - "WhenOpened": "2026-01-09T07:08:06.873Z" - }, - { - "$type": "Document", - "DocumentIndex": 8, - "Title": "settings.spec.ts", - "DocumentMoniker": "C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\sql\\scaleAndSettings\\settings.spec.ts", - "RelativeDocumentMoniker": "test\\sql\\scaleAndSettings\\settings.spec.ts", - "ToolTip": "C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\sql\\scaleAndSettings\\settings.spec.ts", - "RelativeToolTip": "test\\sql\\scaleAndSettings\\settings.spec.ts", - "ViewState": "AgIAAAsAAAAAAAAAAAAuwCQAAAAGAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.003213|", - "WhenOpened": "2026-01-13T21:25:50.263Z" - }, - { - "$type": "Document", - "DocumentIndex": 7, - "Title": "scale.spec.ts", - "DocumentMoniker": "C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\sql\\scaleAndSettings\\scale.spec.ts", - "RelativeDocumentMoniker": "test\\sql\\scaleAndSettings\\scale.spec.ts", - "ToolTip": "C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\sql\\scaleAndSettings\\scale.spec.ts", - "RelativeToolTip": "test\\sql\\scaleAndSettings\\scale.spec.ts", - "ViewState": "AgIAAAgAAAAAAAAAAAAuwB0AAAAHAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.003213|", - "WhenOpened": "2026-01-13T21:25:27.321Z" - }, - { - "$type": "Document", - "DocumentIndex": 10, - "Title": "containercopy.spec.ts", - "DocumentMoniker": "C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\sql\\containercopy.spec.ts", - "RelativeDocumentMoniker": "test\\sql\\containercopy.spec.ts", - "ToolTip": "C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\sql\\containercopy.spec.ts", - "RelativeToolTip": "test\\sql\\containercopy.spec.ts", - "ViewState": "AgIAABsAAAAAAAAAAAAuwDAAAAAIAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.003213|", - "WhenOpened": "2026-01-13T21:25:09.469Z" - }, - { - "$type": "Document", - "DocumentIndex": 11, - "Title": "README.md", - "DocumentMoniker": "C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\README.md", - "RelativeDocumentMoniker": "test\\README.md", - "ToolTip": "C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\README.md", - "RelativeToolTip": "test\\README.md", - "ViewState": "AgIAADcAAADAlpmZmdkhwF4AAAAAAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.001818|", - "WhenOpened": "2026-01-13T20:44:59.892Z" - }, - { - "$type": "Document", - "DocumentIndex": 12, - "Title": "README.md", - "DocumentMoniker": "C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\resources\\README.md", - "RelativeDocumentMoniker": "test\\resources\\README.md", - "ToolTip": "C:\\Users\\sindhuba\\workspace\\DataExplorer\\cosmos-explorer\\test\\resources\\README.md", - "RelativeToolTip": "test\\resources\\README.md", - "ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.001818|", - "WhenOpened": "2026-01-13T20:44:58.329Z" - } - ] - } - ] - } - ] -} \ No newline at end of file diff --git a/.vs/cosmos-explorer/CopilotIndices/18.0.934.24903/CodeChunks.db b/.vs/cosmos-explorer/CopilotIndices/18.0.934.24903/CodeChunks.db deleted file mode 100644 index 0734908fe..000000000 Binary files a/.vs/cosmos-explorer/CopilotIndices/18.0.934.24903/CodeChunks.db and /dev/null differ diff --git a/.vs/cosmos-explorer/CopilotIndices/18.0.934.24903/SemanticSymbols.db b/.vs/cosmos-explorer/CopilotIndices/18.0.934.24903/SemanticSymbols.db deleted file mode 100644 index 5b1ec5710..000000000 Binary files a/.vs/cosmos-explorer/CopilotIndices/18.0.934.24903/SemanticSymbols.db and /dev/null differ diff --git a/.vs/slnx.sqlite b/.vs/slnx.sqlite index e369da08c..2891048dc 100644 Binary files a/.vs/slnx.sqlite and b/.vs/slnx.sqlite differ diff --git a/debug-editor-no-update-1767841423350.png b/debug-editor-no-update-1767841423350.png deleted file mode 100644 index 3345fc1c3..000000000 Binary files a/debug-editor-no-update-1767841423350.png and /dev/null differ diff --git a/debug-no-querytab-1767825118042.png b/debug-no-querytab-1767825118042.png deleted file mode 100644 index dd66e6865..000000000 Binary files a/debug-no-querytab-1767825118042.png and /dev/null differ diff --git a/debug-no-querytab-1767826112580.png b/debug-no-querytab-1767826112580.png deleted file mode 100644 index 33ad41577..000000000 Binary files a/debug-no-querytab-1767826112580.png and /dev/null differ diff --git a/debug-no-querytab-1767826639181.png b/debug-no-querytab-1767826639181.png deleted file mode 100644 index f3f6eeb33..000000000 Binary files a/debug-no-querytab-1767826639181.png and /dev/null differ diff --git a/debug-pagination-broken-1767841424562.png b/debug-pagination-broken-1767841424562.png deleted file mode 100644 index 3345fc1c3..000000000 Binary files a/debug-pagination-broken-1767841424562.png and /dev/null differ diff --git a/debug-pagination-stuck-1767840650565.png b/debug-pagination-stuck-1767840650565.png deleted file mode 100644 index 8609a2f56..000000000 Binary files a/debug-pagination-stuck-1767840650565.png and /dev/null differ diff --git a/package-lock.json b/package-lock.json index c53ef7c56..e4d926a9f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@azure/cosmos": "4.7.0", "@azure/cosmos-language-service": "0.0.5", "@azure/identity": "4.5.0", - "@azure/msal-browser": "^5.2.0", + "@azure/msal-browser": "2.14.2", "@babel/plugin-proposal-class-properties": "7.12.1", "@babel/plugin-proposal-decorators": "7.12.12", "@fluentui/react": "8.119.0", @@ -583,22 +583,21 @@ "license": "0BSD" }, "node_modules/@azure/msal-browser": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-5.6.0.tgz", - "integrity": "sha512-LLqyAtpQNfnATQKnplg/dKJaigxGaaMPrp003ZWGnWwsAmmtzk7xcHEVykCu/4FMyyIfn66NPPzxS9DHrg/UOA==", + "version": "2.14.2", "license": "MIT", "dependencies": { - "@azure/msal-common": "16.4.0" + "@azure/msal-common": "^4.3.0" }, "engines": { "node": ">=0.8.0" } }, "node_modules/@azure/msal-common": { - "version": "16.4.0", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.4.0.tgz", - "integrity": "sha512-twXt09PYtj1PffNNIAzQlrBd0DS91cdA6i1gAfzJ6BnPM4xNk5k9q/5xna7jLIjU3Jnp0slKYtucshGM8OGNAw==", + "version": "4.5.1", "license": "MIT", + "dependencies": { + "debug": "^4.1.1" + }, "engines": { "node": ">=0.8.0" } diff --git a/package.json b/package.json index 8663912d1..d6c8734ff 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "@azure/cosmos": "4.7.0", "@azure/cosmos-language-service": "0.0.5", "@azure/identity": "4.5.0", - "@azure/msal-browser": "^5.2.0", + "@azure/msal-browser": "2.14.2", "@babel/plugin-proposal-class-properties": "7.12.1", "@babel/plugin-proposal-decorators": "7.12.12", "@fluentui/react": "8.119.0", diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index 7ce652ef3..3b4295bf7 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -278,7 +278,7 @@ export default class Explorer { updateUserContext({ aadToken: aadToken }); useDataPlaneRbac.setState({ aadTokenUpdated: true }); } catch (error) { - if (error instanceof msal.AuthError && error.errorCode === msal.BrowserAuthErrorCodes.popupWindowError) { + if (error instanceof msal.AuthError && error.errorCode === msal.BrowserAuthErrorMessage.popUpWindowError.code) { logConsoleError( "We were unable to establish authorization for this account, due to pop-ups being disabled in the browser.\nPlease enable pop-ups for this site and try again", ); diff --git a/src/Explorer/Panes/SettingsPane/SettingsPane.tsx b/src/Explorer/Panes/SettingsPane/SettingsPane.tsx index 2af58a004..69cca5fc5 100644 --- a/src/Explorer/Panes/SettingsPane/SettingsPane.tsx +++ b/src/Explorer/Panes/SettingsPane/SettingsPane.tsx @@ -1,17 +1,20 @@ -import { AuthError as msalAuthError, BrowserAuthErrorCodes as msalBrowserAuthErrorCodes } from "@azure/msal-browser"; import { - Checkbox, - ChoiceGroup, - DefaultButton, - Dropdown, - IChoiceGroupOption, - IDropdownOption, - ISpinButtonStyles, - IToggleStyles, - Position, - SpinButton, - Stack, - Toggle, + AuthError as msalAuthError, + BrowserAuthErrorMessage as msalBrowserAuthErrorMessage, +} from "@azure/msal-browser"; +import { + Checkbox, + ChoiceGroup, + DefaultButton, + Dropdown, + IChoiceGroupOption, + IDropdownOption, + ISpinButtonStyles, + IToggleStyles, + Position, + SpinButton, + Stack, + Toggle, } from "@fluentui/react"; import { Accordion, AccordionHeader, AccordionItem, AccordionPanel, makeStyles } from "@fluentui/react-components"; import { AuthType } from "AuthType"; @@ -23,20 +26,20 @@ import { useDialog } from "Explorer/Controls/Dialog"; import { useDatabases } from "Explorer/useDatabases"; import { isFabric, isFabricNative } from "Platform/Fabric/FabricUtil"; import { - AppStateComponentNames, - deleteAllStates, - deleteState, - hasState, - loadState, - saveState, + AppStateComponentNames, + deleteAllStates, + deleteState, + hasState, + loadState, + saveState, } from "Shared/AppStatePersistenceUtility"; import { - DefaultRUThreshold, - LocalStorageUtility, - StorageKey, - getDefaultQueryResultsView, - getRUThreshold, - ruThresholdEnabled as isRUThresholdEnabled, + DefaultRUThreshold, + LocalStorageUtility, + StorageKey, + getDefaultQueryResultsView, + getRUThreshold, + ruThresholdEnabled as isRUThresholdEnabled, } from "Shared/StorageUtility"; import * as StringUtility from "Shared/StringUtility"; import { updateUserContext, userContext } from "UserContext"; @@ -312,7 +315,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ } catch (authError) { if ( authError instanceof msalAuthError && - authError.errorCode === msalBrowserAuthErrorCodes.popupWindowError + authError.errorCode === msalBrowserAuthErrorMessage.popUpWindowError.code ) { logConsoleError( `We were unable to establish authorization for this account, due to pop-ups being disabled in the browser.\nPlease enable pop-ups for this site and click on "Login for Entra ID" button`, diff --git a/src/Utils/AuthorizationUtils.ts b/src/Utils/AuthorizationUtils.ts index d4c42a216..84d4cfcc4 100644 --- a/src/Utils/AuthorizationUtils.ts +++ b/src/Utils/AuthorizationUtils.ts @@ -49,9 +49,6 @@ export function decryptJWTToken(token: string) { } export async function getMsalInstance() { - // Compute the redirect bridge URL for MSAL v5 COOP handling - const redirectBridgeUrl = `${window.location.origin}/redirectBridge.html`; - const msalConfig: msal.Configuration = { cache: { cacheLocation: "localStorage", @@ -59,18 +56,14 @@ export async function getMsalInstance() { auth: { authority: `${configContext.AAD_ENDPOINT}organizations`, clientId: "203f1145-856a-4232-83d4-a43568fba23d", - // MSAL v5 requires redirect bridge for popup/silent flows (CG alert MVS-2026-vmmw-f85q) - redirectUri: redirectBridgeUrl, }, }; - if (process.env.NODE_ENV === "development" && !window.location.hostname.includes("localhost")) { - msalConfig.auth.redirectUri = "https://dataexplorer-dev.azurewebsites.net/redirectBridge.html"; + if (process.env.NODE_ENV === "development") { + msalConfig.auth.redirectUri = "https://dataexplorer-dev.azurewebsites.net"; } const msalInstance = new msal.PublicClientApplication(msalConfig); - // v3+ requires explicit initialization before using MSAL APIs - await msalInstance.initialize(); return msalInstance; } diff --git a/src/hooks/useAADAuth.ts b/src/hooks/useAADAuth.ts index 146aff8a6..6a927dca0 100644 --- a/src/hooks/useAADAuth.ts +++ b/src/hooks/useAADAuth.ts @@ -59,12 +59,9 @@ export function useAADAuth(config?: ConfigContext): ReturnType { return; } - // Use redirect bridge for MSAL v5 COOP handling (CG alert MVS-2026-vmmw-f85q) - const redirectBridgeUrl = `${window.location.origin}/redirectBridge.html`; - try { const response = await msalInstance.loginPopup({ - redirectUri: redirectBridgeUrl, + redirectUri: config.msalRedirectURI, scopes: [], }); setLoggedIn(); @@ -92,11 +89,9 @@ export function useAADAuth(config?: ConfigContext): ReturnType { if (!msalInstance || !config) { return; } - // Use redirect bridge for MSAL v5 COOP handling (CG alert MVS-2026-vmmw-f85q) - const redirectBridgeUrl = `${window.location.origin}/redirectBridge.html`; try { const response = await msalInstance.loginPopup({ - redirectUri: redirectBridgeUrl, + redirectUri: config.msalRedirectURI, authority: `${config.AAD_ENDPOINT}${id}`, scopes: [], }); @@ -125,7 +120,7 @@ export function useAADAuth(config?: ConfigContext): ReturnType { setArmToken(armToken); setAuthFailure(null); } catch (error) { - if (error instanceof msal.AuthError && error.errorCode === msal.BrowserAuthErrorCodes.popupWindowError) { + if (error instanceof msal.AuthError && error.errorCode === msal.BrowserAuthErrorMessage.popUpWindowError.code) { // This error can occur when acquireTokenWithMsal() has attempted to acquire token interactively // and user has popups disabled in browser. This fails as the popup is not the result of a explicit user // action. In this case, we display the failure and a link to repeat the operation. Clicking on the diff --git a/src/redirectBridge.html b/src/redirectBridge.html deleted file mode 100644 index 7f4ada149..000000000 --- a/src/redirectBridge.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - Authentication Redirect - - - -
-

Processing authentication...

-
- - diff --git a/src/redirectBridge.ts b/src/redirectBridge.ts deleted file mode 100644 index ffeab752a..000000000 --- a/src/redirectBridge.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * MSAL COOP Redirect Bridge - * - * This page handles the authentication response from the Identity Provider (IdP) - * and broadcasts it to the main application frame. Required for msal-browser v5+ - * to securely handle auth responses when the IdP sets Cross-Origin-Opener-Policy headers. - * - * Security Note: This file must be bundled with your application, NOT loaded from a CDN. - * - * CG Alert: MVS-2026-vmmw-f85q - */ -import { broadcastResponseToMainFrame } from "@azure/msal-browser/redirect-bridge"; - -broadcastResponseToMainFrame().catch((error: unknown) => { - console.error("MSAL redirect bridge error:", error); -}); diff --git a/src/types/msal-browser-redirect-bridge.d.ts b/src/types/msal-browser-redirect-bridge.d.ts deleted file mode 100644 index 33d10d4a5..000000000 --- a/src/types/msal-browser-redirect-bridge.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// Type declarations for @azure/msal-browser subpath exports -// Required because tsconfig uses moduleResolution: "node" which doesn't support exports field - -declare module "@azure/msal-browser/redirect-bridge" { - /** - * Processes the authentication response from the redirect URL. - * For SSO and popup scenarios broadcasts it to the main frame. - * For redirect scenario navigates to the home page. - */ - export function broadcastResponseToMainFrame(navigationClient?: unknown): Promise; -} diff --git a/webpack.config.js b/webpack.config.js index 006c96da4..7dcc89828 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -116,7 +116,6 @@ module.exports = function (_env = {}, argv = {}) { galleryViewer: "./src/GalleryViewer/GalleryViewer.tsx", selfServe: "./src/SelfServe/SelfServe.tsx", connectToGitHub: "./src/GitHub/GitHubConnector.ts", - redirectBridge: "./src/redirectBridge.ts", ...(mode !== "production" && { testExplorer: "./test/testExplorer/TestExplorer.ts" }), }; @@ -166,11 +165,6 @@ module.exports = function (_env = {}, argv = {}) { template: "src/SelfServe/selfServe.html", chunks: ["selfServe"], }), - new HtmlWebpackPlugin({ - filename: "redirectBridge.html", - template: "src/redirectBridge.html", - chunks: ["redirectBridge"], - }), ...(mode !== "production" ? [ new HtmlWebpackPlugin({