Fix container copy permissions E2E reliability and popover stacking

Keep confirmation popovers above adjacent accordion headers. Target the actual switch, mock destination role APIs with ARM response shapes, and synchronize PITR and role assignment completion. Fix initialization hook formatting.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Dmitrii Shilov
2026-09-10 18:34:29 +02:00
parent 9bec5edb66
commit 75d2fc20b4
3 changed files with 89 additions and 167 deletions
@@ -125,6 +125,11 @@
opacity: 0.8; opacity: 0.8;
} }
} }
.accordionPanel {
// Keep confirmation popovers above neighboring accordion headers.
position: relative;
z-index: 1;
}
.foreground { .foreground {
z-index: 10; z-index: 10;
background-color: var(--colorNeutralBackground2); background-color: var(--colorNeutralBackground2);
+1 -4
View File
@@ -111,10 +111,7 @@ export function useKnockoutExplorer(platform: Platform): Explorer {
effect().catch((error) => { effect().catch((error) => {
// configurePortal now rejects instead of hanging forever, so this is reachable. // configurePortal now rejects instead of hanging forever, so this is reachable.
// Without a handler it would surface only as an unhandled rejection. // Without a handler it would surface only as an unhandled rejection.
Logger.logError( Logger.logError(error instanceof Error ? error.message : String(error), "useKnockoutExplorer/configure");
error instanceof Error ? error.message : String(error),
"useKnockoutExplorer/configure",
);
}); });
}, [platform]); }, [platform]);
+83 -163
View File
@@ -1,6 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { expect, Frame, Locator, Page, test } from "@playwright/test"; import { expect, Frame, Locator, Page, test } from "@playwright/test";
import { set } from "lodash";
import { ContainerCopy, getAccountName, TestAccount } from "../../fx"; import { ContainerCopy, getAccountName, TestAccount } from "../../fx";
const VISIBLE_TIMEOUT_MS = 30 * 1000; const VISIBLE_TIMEOUT_MS = 30 * 1000;
@@ -12,16 +10,76 @@ test.describe("Container Copy - Permission Screen Verification", () => {
let frame: Frame; let frame: Frame;
let sourceAccountName: string; let sourceAccountName: string;
let targetAccountName: string; let targetAccountName: string;
let backupPolicyType: "Periodic" | "Continuous";
let roleAssigned: boolean;
let completeRoleAssignment: () => void;
test.beforeEach("Setup for each test", async ({ browser }) => { test.beforeEach("Setup for each test", async ({ browser }) => {
page = await browser.newPage(); page = await browser.newPage();
({ wrapper, frame } = await ContainerCopy.open(page, TestAccount.SQL));
targetAccountName = getAccountName(TestAccount.SQLContainerCopyOnly); targetAccountName = getAccountName(TestAccount.SQLContainerCopyOnly);
sourceAccountName = getAccountName(TestAccount.SQL); sourceAccountName = getAccountName(TestAccount.SQL);
backupPolicyType = "Periodic";
roleAssigned = false;
// Keep prerequisites independent of the shared accounts' current configuration.
await page.route(`**/Microsoft.DocumentDB/databaseAccounts/${sourceAccountName}?*`, async (route) => {
expect(route.request().method()).toBe("GET");
const response = await route.fetch();
expect(response.ok()).toBeTruthy();
const account = await response.json();
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
...account,
identity: { type: "SystemAssigned", principalId: "00-11-22-33" },
properties: {
...account.properties,
defaultIdentity: "SystemAssignedIdentity",
backupPolicy: { type: backupPolicyType },
capabilities: [{ name: "EnableOnlineContainerCopy" }],
},
}),
});
});
const roleAssignmentCompleted = new Promise<void>((resolve) => {
completeRoleAssignment = resolve;
});
await page.route(
`**/Microsoft.DocumentDB/databaseAccounts/${targetAccountName}/sqlRoleAssignments**`,
async (route) => {
const accountScope = new URL(route.request().url()).pathname.split("/sqlRoleAssignments")[0];
const roleDefinitionId = `${accountScope}/sqlRoleDefinitions/00000000-0000-0000-0000-000000000002`;
const assignment = {
id: `${accountScope}/sqlRoleAssignments/test-assignment`,
name: "test-assignment",
type: "Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments",
properties: { principalId: "00-11-22-33", roleDefinitionId, scope: `${accountScope}/` },
};
if (route.request().method() === "PUT") {
expect(route.request().postDataJSON().properties).toEqual(assignment.properties);
await roleAssignmentCompleted;
roleAssigned = true;
await route.fulfill({ json: assignment });
} else {
expect(route.request().method()).toBe("GET");
await route.fulfill({ json: { value: roleAssigned ? [assignment] : [] } });
}
},
);
await page.route(
`**/Microsoft.DocumentDB/databaseAccounts/${targetAccountName}/sqlRoleDefinitions/*`,
async (route) => {
await route.fulfill({ json: { name: "00000000-0000-0000-0000-000000000002" } });
},
);
({ wrapper, frame } = await ContainerCopy.open(page, TestAccount.SQL));
}); });
test.afterEach("Cleanup after each test", async () => { test.afterEach("Cleanup after each test", async () => {
await page.unrouteAll({ behavior: "ignoreErrors" }); completeRoleAssignment?.();
await page.unrouteAll({ behavior: "wait" });
await page.close(); await page.close();
}); });
@@ -36,36 +94,6 @@ test.describe("Container Copy - Permission Screen Verification", () => {
await expect(wrapper.getByTestId("CommandBar/Button:Refresh")).toBeVisible(); await expect(wrapper.getByTestId("CommandBar/Button:Refresh")).toBeVisible();
await expect(wrapper.getByTestId("CommandBar/Button:Feedback")).toBeVisible(); await expect(wrapper.getByTestId("CommandBar/Button:Feedback")).toBeVisible();
// Mock Resource Graph API — fires on auto-subscription selection to populate account dropdown
await page.route(
"https://management.azure.com/providers/Microsoft.ResourceGraph/resources?api-version=2021-03-01",
async (route) => {
const request = route.request();
if (
request.method() === "POST" &&
(request.postDataJSON()?.query as string) ===
"resources | where type =~ 'microsoft.documentdb/databaseaccounts'"
) {
const response = await route.fetch();
const responseData = await response.json();
if (responseData.data && Array.isArray(responseData.data)) {
responseData.data = responseData.data.map((d: any) => {
d.properties.backupPolicy.type = "Periodic";
return d;
});
}
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(responseData),
});
} else {
await route.continue();
}
},
{ times: 2 },
);
// Open the Create Copy Job panel // Open the Create Copy Job panel
await createCopyJobButton.click(); await createCopyJobButton.click();
panel = frame.getByTestId("Panel:Create copy job"); panel = frame.getByTestId("Panel:Create copy job");
@@ -79,15 +107,7 @@ test.describe("Container Copy - Permission Screen Verification", () => {
const dropdownItemsWrapper = frame.locator("div.ms-Dropdown-items"); const dropdownItemsWrapper = frame.locator("div.ms-Dropdown-items");
expect(await dropdownItemsWrapper.getAttribute("aria-label")).toEqual("Account"); expect(await dropdownItemsWrapper.getAttribute("aria-label")).toEqual("Account");
const allDropdownItems = await dropdownItemsWrapper.locator(`button.ms-Dropdown-item[role='option']`).all(); await dropdownItemsWrapper.getByRole("option", { name: targetAccountName, exact: true }).click();
for (const item of allDropdownItems) {
const testContent = (await item.textContent()) ?? "";
if (testContent.trim() === targetAccountName.trim()) {
await item.click();
break;
}
}
// Enable online migration mode // Enable online migration mode
const migrationTypeContainer = panel.getByTestId("migration-type"); const migrationTypeContainer = panel.getByTestId("migration-type");
@@ -103,41 +123,6 @@ test.describe("Container Copy - Permission Screen Verification", () => {
await expect(permissionScreen.getByText("Online container copy", { exact: true })).toBeVisible(); await expect(permissionScreen.getByText("Online container copy", { exact: true })).toBeVisible();
await expect(permissionScreen.getByText("Cross-account container copy", { exact: true })).toBeVisible(); await expect(permissionScreen.getByText("Cross-account container copy", { exact: true })).toBeVisible();
// Setup API mocking for the source account
await page.route(`**/Microsoft.DocumentDB/databaseAccounts/${sourceAccountName}**`, async (route) => {
const mockData = {
identity: {
type: "SystemAssigned",
principalId: "00-11-22-33",
},
properties: {
defaultIdentity: "SystemAssignedIdentity",
backupPolicy: {
type: "Continuous",
},
capabilities: [{ name: "EnableOnlineContainerCopy" }],
},
};
if (route.request().method() === "GET") {
const response = await route.fetch();
const actualData = await response.json();
const mergedData = { ...actualData };
set(mergedData, "identity", mockData.identity);
set(mergedData, "properties.defaultIdentity", mockData.properties.defaultIdentity);
set(mergedData, "properties.backupPolicy", mockData.properties.backupPolicy);
set(mergedData, "properties.capabilities", mockData.properties.capabilities);
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(mergedData),
});
} else {
await route.continue();
}
});
// Verify Point-in-Time Restore functionality // Verify Point-in-Time Restore functionality
const expandedOnlineAccordionHeader = permissionScreen const expandedOnlineAccordionHeader = permissionScreen
.getByTestId("permission-group-container-onlineConfigs") .getByTestId("permission-group-container-onlineConfigs")
@@ -157,16 +142,13 @@ test.describe("Container Copy - Permission Screen Verification", () => {
const pitrBtn = accordionPanel.getByTestId("pointInTimeRestore:PrimaryBtn"); const pitrBtn = accordionPanel.getByTestId("pointInTimeRestore:PrimaryBtn");
await expect(pitrBtn).toBeVisible(); await expect(pitrBtn).toBeVisible();
const popupPromise = page.waitForEvent("popup");
await pitrBtn.click({ force: true }); await pitrBtn.click({ force: true });
const popup = await popupPromise;
// Verify new page opens with correct URL pattern await expect(popup).toHaveURL(
page.context().on("page", async (newPage) => { new RegExp(`/providers/Microsoft\\.(DocumentDB|DocumentDb)/databaseAccounts/${sourceAccountName}/backupRestore`),
const expectedUrlEndPattern = new RegExp( );
`/providers/Microsoft.(DocumentDB|DocumentDb)/databaseAccounts/${sourceAccountName}/backupRestore`, await popup.close();
);
expect(newPage.url()).toMatch(expectedUrlEndPattern);
await newPage.close();
});
const loadingOverlay = frame.locator("[data-test='loading-overlay']"); const loadingOverlay = frame.locator("[data-test='loading-overlay']");
await expect(loadingOverlay).toBeVisible(); await expect(loadingOverlay).toBeVisible();
@@ -180,81 +162,12 @@ test.describe("Container Copy - Permission Screen Verification", () => {
await expect(refreshBtn).toBeVisible({ timeout: 5000 }); await expect(refreshBtn).toBeVisible({ timeout: 5000 });
await expect(pitrBtn).not.toBeVisible(); await expect(pitrBtn).not.toBeVisible();
// Setup additional API mocks for role assignments and permissions // Complete PITR explicitly, instead of racing its polling response against the next section.
// In the redesigned flow, role assignments are checked on the SOURCE account (current account = sourceAccountName). backupPolicyType = "Continuous";
// The destination account (selectedAccountName) manages identity; source account holds the role assignments. await refreshBtn.click({ force: true });
await page.route( await expect(
`**/Microsoft.DocumentDB/databaseAccounts/${sourceAccountName}/sqlRoleAssignments*`, permissionScreen.getByTestId("permission-group-container-onlineConfigs").getByAltText("Warning icon"),
async (route) => { ).toHaveCount(0);
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
value: [
{
principalId: "00-11-22-33",
roleDefinitionId: `Microsoft.DocumentDB/databaseAccounts/${sourceAccountName}/77-88-99`,
},
],
}),
});
},
);
await page.route("**/Microsoft.DocumentDB/databaseAccounts/*/77-88-99**", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
value: [
{
// Built-in Cosmos DB Data Contributor role (read-write), required by checkTargetHasReadWriteRoleOnSource
name: "00000000-0000-0000-0000-000000000002",
},
],
}),
});
});
await page.route(`**/Microsoft.DocumentDB/databaseAccounts/${sourceAccountName}**`, async (route) => {
const mockData = {
identity: {
type: "SystemAssigned",
principalId: "00-11-22-33",
},
properties: {
defaultIdentity: "SystemAssignedIdentity",
backupPolicy: {
type: "Continuous",
},
capabilities: [{ name: "EnableOnlineContainerCopy" }],
},
};
if (route.request().method() === "PATCH") {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ status: "Succeeded" }),
});
} else if (route.request().method() === "GET") {
const response = await route.fetch();
const actualData = await response.json();
const mergedData = { ...actualData };
set(mergedData, "identity", mockData.identity);
set(mergedData, "properties.defaultIdentity", mockData.properties.defaultIdentity);
set(mergedData, "properties.backupPolicy", mockData.properties.backupPolicy);
set(mergedData, "properties.capabilities", mockData.properties.capabilities);
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(mergedData),
});
} else {
await route.continue();
}
});
// Verify cross-account permissions functionality // Verify cross-account permissions functionality
const expandedCrossAccordionHeader = permissionScreen const expandedCrossAccordionHeader = permissionScreen
@@ -270,9 +183,11 @@ test.describe("Container Copy - Permission Screen Verification", () => {
.locator("[role='tabpanel'], .fui-AccordionPanel, [data-test*='panel']") .locator("[role='tabpanel'], .fui-AccordionPanel, [data-test*='panel']")
.first(); .first();
const toggleButton = crossAccordionPanel.getByTestId("btn-toggle"); const toggleButton = crossAccordionPanel.getByRole("switch");
await expect(toggleButton).toBeVisible(); await expect(toggleButton).toBeVisible();
await toggleButton.scrollIntoViewIfNeeded();
await toggleButton.click({ force: true }); await toggleButton.click({ force: true });
await expect(toggleButton).toBeChecked();
// Verify popover functionality // Verify popover functionality
const popover = frame.locator("[data-test='popover-container']"); const popover = frame.locator("[data-test='popover-container']");
@@ -283,12 +198,17 @@ test.describe("Container Copy - Permission Screen Verification", () => {
await expect(yesButton).toBeVisible(); await expect(yesButton).toBeVisible();
await expect(noButton).toBeVisible(); await expect(noButton).toBeVisible();
await yesButton.scrollIntoViewIfNeeded();
await yesButton.click({ force: true }); await yesButton.click({ force: true });
// Verify loading states // Verify loading states
await expect(loadingOverlay).toBeVisible(); await expect(loadingOverlay).toBeVisible();
completeRoleAssignment();
await expect(loadingOverlay).toBeHidden({ timeout: 10 * 1000 }); await expect(loadingOverlay).toBeHidden({ timeout: 10 * 1000 });
await expect(popover).toBeHidden({ timeout: 10 * 1000 }); await expect(popover).toBeHidden({ timeout: 10 * 1000 });
await expect(
permissionScreen.getByTestId("permission-group-container-crossAccountConfigs").getByAltText("Warning icon"),
).toHaveCount(0);
// Cancel the panel to clean up // Cancel the panel to clean up
await panel.getByRole("button", { name: "Cancel" }).click({ force: true }); await panel.getByRole("button", { name: "Cancel" }).click({ force: true });