diff --git a/src/Common/ErrorHandlingUtils.ts b/src/Common/ErrorHandlingUtils.ts index bfa321fe9..ea25d8e34 100644 --- a/src/Common/ErrorHandlingUtils.ts +++ b/src/Common/ErrorHandlingUtils.ts @@ -7,16 +7,27 @@ import { HttpStatusCodes } from "./Constants"; import { logError } from "./Logger"; import { sendMessage } from "./MessageHandler"; -export const handleError = (error: string | ARMError | Error, area: string, consoleErrorPrefix?: string): void => { +export interface HandleErrorOptions { + /** Optional redacted error to use for telemetry logging instead of the original error */ + redactedError?: string | ARMError | Error; +} + +export const handleError = ( + error: string | ARMError | Error, + area: string, + consoleErrorPrefix?: string, + options?: HandleErrorOptions, +): void => { const errorMessage = getErrorMessage(error); const errorCode = error instanceof ARMError ? error.code : undefined; - // logs error to data explorer console + // logs error to data explorer console (always shows original, non-redacted message) const consoleErrorMessage = consoleErrorPrefix ? `${consoleErrorPrefix}:\n ${errorMessage}` : errorMessage; logConsoleError(consoleErrorMessage); - // logs error to both app insight and kusto - logError(errorMessage, area, errorCode); + // logs error to both app insight and kusto (use redacted message if provided) + const telemetryErrorMessage = options?.redactedError ? getErrorMessage(options.redactedError) : errorMessage; + logError(telemetryErrorMessage, area, errorCode); // checks for errors caused by firewall and sends them to portal to handle sendNotificationForError(errorMessage, errorCode); diff --git a/src/Common/dataAccess/deleteDocument.ts b/src/Common/dataAccess/deleteDocument.ts index 9d4b8a396..b8a4a0430 100644 --- a/src/Common/dataAccess/deleteDocument.ts +++ b/src/Common/dataAccess/deleteDocument.ts @@ -44,7 +44,8 @@ export const deleteDocuments = async ( documentIds: DocumentId[], abortSignal: AbortSignal, ): Promise => { - const clearMessage = logConsoleProgress(`Deleting ${documentIds.length} ${getEntityName(true)}`); + const totalCount = documentIds.length; + const clearMessage = logConsoleProgress(`Deleting ${totalCount} ${getEntityName(true)}`); try { const v2Container = await client().database(collection.databaseId).container(collection.id()); @@ -83,11 +84,7 @@ export const deleteDocuments = async ( const flatAllResult = Array.prototype.concat.apply([], allResult); return flatAllResult; } catch (error) { - handleError( - error, - "DeleteDocuments", - `Error while deleting ${documentIds.length} ${getEntityName(documentIds.length > 1)}`, - ); + handleError(error, "DeleteDocuments", `Error while deleting ${totalCount} ${getEntityName(totalCount > 1)}`); throw error; } finally { clearMessage(); diff --git a/src/Common/dataAccess/queryDocumentsPage.test.ts b/src/Common/dataAccess/queryDocumentsPage.test.ts new file mode 100644 index 000000000..adf68bd02 --- /dev/null +++ b/src/Common/dataAccess/queryDocumentsPage.test.ts @@ -0,0 +1,171 @@ +import { redactSyntaxErrorMessage } from "./queryDocumentsPage"; + +/* Typical error to redact looks like this (the message property contains a JSON string with nested structure): +{ + "message": "{\"code\":\"BadRequest\",\"message\":\"{\\\"errors\\\":[{\\\"severity\\\":\\\"Error\\\",\\\"location\\\":{\\\"start\\\":0,\\\"end\\\":5},\\\"code\\\":\\\"SC1001\\\",\\\"message\\\":\\\"Syntax error, incorrect syntax near 'Crazy'.\\\"}]}\\r\\nActivityId: d5424e10-51bd-46f7-9aec-7b40bed36f17, Windows/10.0.20348 cosmos-netstandard-sdk/3.18.0\"}" +} +*/ + +// Helper to create the nested error structure that matches what the SDK returns +const createNestedError = ( + errors: Array<{ severity?: string; location?: { start: number; end: number }; code: string; message: string }>, + activityId: string = "test-activity-id", +): { message: string } => { + const innerErrorsJson = JSON.stringify({ errors }); + const innerMessage = `${innerErrorsJson}\r\n${activityId}`; + const outerJson = JSON.stringify({ code: "BadRequest", message: innerMessage }); + return { message: outerJson }; +}; + +// Helper to parse the redacted result +const parseRedactedResult = (result: { message: string }) => { + const outerParsed = JSON.parse(result.message); + const [innerErrorsJson, activityIdPart] = outerParsed.message.split("\r\n"); + const innerErrors = JSON.parse(innerErrorsJson); + return { outerParsed, innerErrors, activityIdPart }; +}; + +describe("redactSyntaxErrorMessage", () => { + it("should redact SC1001 error message", () => { + const error = createNestedError( + [ + { + severity: "Error", + location: { start: 0, end: 5 }, + code: "SC1001", + message: "Syntax error, incorrect syntax near 'Crazy'.", + }, + ], + "ActivityId: d5424e10-51bd-46f7-9aec-7b40bed36f17", + ); + + const result = redactSyntaxErrorMessage(error) as { message: string }; + const { outerParsed, innerErrors, activityIdPart } = parseRedactedResult(result); + + expect(outerParsed.code).toBe("BadRequest"); + expect(innerErrors.errors[0].message).toBe("__REDACTED__"); + expect(activityIdPart).toContain("ActivityId: d5424e10-51bd-46f7-9aec-7b40bed36f17"); + }); + + it("should redact SC2001 error message", () => { + const error = createNestedError( + [ + { + severity: "Error", + location: { start: 0, end: 10 }, + code: "SC2001", + message: "Some sensitive syntax error message.", + }, + ], + "ActivityId: abc123", + ); + + const result = redactSyntaxErrorMessage(error) as { message: string }; + const { outerParsed, innerErrors, activityIdPart } = parseRedactedResult(result); + + expect(outerParsed.code).toBe("BadRequest"); + expect(innerErrors.errors[0].message).toBe("__REDACTED__"); + expect(activityIdPart).toContain("ActivityId: abc123"); + }); + + it("should redact multiple errors with SC1001 and SC2001 codes", () => { + const error = createNestedError( + [ + { severity: "Error", code: "SC1001", message: "First error" }, + { severity: "Error", code: "SC2001", message: "Second error" }, + ], + "ActivityId: xyz", + ); + + const result = redactSyntaxErrorMessage(error) as { message: string }; + const { innerErrors } = parseRedactedResult(result); + + expect(innerErrors.errors[0].message).toBe("__REDACTED__"); + expect(innerErrors.errors[1].message).toBe("__REDACTED__"); + }); + + it("should not redact errors with other codes", () => { + const error = createNestedError( + [{ severity: "Error", code: "SC9999", message: "This should not be redacted." }], + "ActivityId: test123", + ); + + const result = redactSyntaxErrorMessage(error); + + expect(result).toBe(error); // Should return original error unchanged + }); + + it("should not modify non-BadRequest errors", () => { + const innerMessage = JSON.stringify({ errors: [{ code: "SC1001", message: "Should not be redacted" }] }); + const error = { + message: JSON.stringify({ code: "NotFound", message: innerMessage }), + }; + + const result = redactSyntaxErrorMessage(error); + + expect(result).toBe(error); + }); + + it("should handle errors without message property", () => { + const error = { code: "BadRequest" }; + + const result = redactSyntaxErrorMessage(error); + + expect(result).toBe(error); + }); + + it("should handle non-object errors", () => { + const stringError = "Simple string error"; + const nullError: null = null; + const undefinedError: undefined = undefined; + + expect(redactSyntaxErrorMessage(stringError)).toBe(stringError); + expect(redactSyntaxErrorMessage(nullError)).toBe(nullError); + expect(redactSyntaxErrorMessage(undefinedError)).toBe(undefinedError); + }); + + it("should handle malformed JSON in message", () => { + const error = { + message: "not valid json", + }; + + const result = redactSyntaxErrorMessage(error); + + expect(result).toBe(error); + }); + + it("should handle message without ActivityId suffix", () => { + const innerErrorsJson = JSON.stringify({ + errors: [{ severity: "Error", code: "SC1001", message: "Syntax error near something." }], + }); + const error = { + message: JSON.stringify({ code: "BadRequest", message: innerErrorsJson + "\r\n" }), + }; + + const result = redactSyntaxErrorMessage(error) as { message: string }; + const { innerErrors } = parseRedactedResult(result); + + expect(innerErrors.errors[0].message).toBe("__REDACTED__"); + }); + + it("should preserve other error properties", () => { + const baseError = createNestedError([{ code: "SC1001", message: "Error" }], "ActivityId: test"); + const error = { + ...baseError, + statusCode: 400, + additionalInfo: "extra data", + }; + + const result = redactSyntaxErrorMessage(error) as { + message: string; + statusCode: number; + additionalInfo: string; + }; + + expect(result.statusCode).toBe(400); + expect(result.additionalInfo).toBe("extra data"); + + const { innerErrors } = parseRedactedResult(result); + expect(innerErrors.errors[0].message).toBe("__REDACTED__"); + }); +}); diff --git a/src/Common/dataAccess/queryDocumentsPage.ts b/src/Common/dataAccess/queryDocumentsPage.ts index 556ed290c..b5ec9c684 100644 --- a/src/Common/dataAccess/queryDocumentsPage.ts +++ b/src/Common/dataAccess/queryDocumentsPage.ts @@ -4,6 +4,51 @@ import { getEntityName } from "../DocumentUtility"; import { handleError } from "../ErrorHandlingUtils"; import { MinimalQueryIterator, nextPage } from "../IteratorUtilities"; +// Redact sensitive information from BadRequest errors with specific codes +export const redactSyntaxErrorMessage = (error: unknown): unknown => { + const codesToRedact = ["SC1001", "SC2001"]; + + try { + // Handle error objects with a message property + if (error && typeof error === "object" && "message" in error) { + const errorObj = error as { code?: string; message?: string }; + if (typeof errorObj.message === "string") { + // Parse the inner JSON from the message + const innerJson = JSON.parse(errorObj.message); + if (innerJson.code === "BadRequest" && typeof innerJson.message === "string") { + const [innerErrorsJson, activityIdPart] = innerJson.message.split("\r\n"); + const innerErrorsObj = JSON.parse(innerErrorsJson); + if (Array.isArray(innerErrorsObj.errors)) { + let modified = false; + innerErrorsObj.errors = innerErrorsObj.errors.map((err: { code?: string; message?: string }) => { + if (err.code && codesToRedact.includes(err.code)) { + modified = true; + return { ...err, message: "__REDACTED__" }; + } + return err; + }); + + if (modified) { + // Reconstruct the message with the redacted content + const redactedMessage = JSON.stringify(innerErrorsObj) + `\r\n${activityIdPart}`; + const redactedError = { + ...error, + message: JSON.stringify({ ...innerJson, message: redactedMessage }), + body: undefined as unknown, // Clear body to avoid sensitive data + }; + return redactedError; + } + } + } + } + } + } catch { + // If parsing fails, return the original error + } + + return error; +}; + export const queryDocumentsPage = async ( resourceName: string, documentsIterator: MinimalQueryIterator, @@ -18,7 +63,12 @@ export const queryDocumentsPage = async ( logConsoleInfo(`Successfully fetched ${itemCount} ${entityName} for container ${resourceName}`); return result; } catch (error) { - handleError(error, "QueryDocumentsPage", `Failed to query ${entityName} for container ${resourceName}`); + // Redact sensitive information for telemetry while showing original in console + const redactedError = redactSyntaxErrorMessage(error); + + handleError(error, "QueryDocumentsPage", `Failed to query ${entityName} for container ${resourceName}`, { + redactedError: redactedError as Error, + }); throw error; } finally { clearMessage(); diff --git a/src/Explorer/ContainerCopy/MonitorCopyJobs/Components/CopyJobsList.test.tsx b/src/Explorer/ContainerCopy/MonitorCopyJobs/Components/CopyJobsList.test.tsx index c3b723265..64778f2ff 100644 --- a/src/Explorer/ContainerCopy/MonitorCopyJobs/Components/CopyJobsList.test.tsx +++ b/src/Explorer/ContainerCopy/MonitorCopyJobs/Components/CopyJobsList.test.tsx @@ -11,9 +11,17 @@ jest.mock("../../Actions/CopyJobActions", () => ({ jest.mock("./CopyJobColumns", () => ({ getColumns: jest.fn(() => [ + { + key: "LastUpdatedTime", + name: "Date & time", + fieldName: "LastUpdatedTime", + minWidth: 140, + maxWidth: 300, + isResizable: true, + }, { key: "Name", - name: "Name", + name: "Job name", fieldName: "Name", minWidth: 140, maxWidth: 300, @@ -165,6 +173,165 @@ describe("CopyJobsList", () => { expect(screen.getByTestId("action-menu-job-2")).toBeInTheDocument(); expect(screen.getByTestId("action-menu-job-3")).toBeInTheDocument(); }); + + it("renders filter TextField with data-test attribute", () => { + render(); + + const filterTextField = document.querySelector('[data-test="CopyJobsList/FilterTextField"]'); + expect(filterTextField).toBeInTheDocument(); + }); + + it("renders search TextField with correct placeholder", () => { + render(); + + const searchInput = screen.getByPlaceholderText("Search jobs..."); + expect(searchInput).toBeInTheDocument(); + }); + }); + + describe("Filtering", () => { + it("filters jobs by Name when text is entered", async () => { + render(); + + const filterInput = screen.getByPlaceholderText("Search jobs..."); + fireEvent.change(filterInput, { target: { value: "Job 1" } }); + + await waitFor(() => { + expect(screen.getByText("Test Job 1")).toBeInTheDocument(); + expect(screen.queryByText("Test Job 2")).not.toBeInTheDocument(); + expect(screen.queryByText("Test Job 3")).not.toBeInTheDocument(); + }); + }); + + it("filters jobs case-insensitively", async () => { + render(); + + const filterInput = screen.getByPlaceholderText("Search jobs..."); + fireEvent.change(filterInput, { target: { value: "test job 1" } }); + + await waitFor(() => { + expect(screen.getByText("Test Job 1")).toBeInTheDocument(); + expect(screen.queryByText("Test Job 2")).not.toBeInTheDocument(); + }); + }); + + it("shows all jobs when filter text is empty", async () => { + render(); + + const filterInput = screen.getByPlaceholderText("Search jobs..."); + fireEvent.change(filterInput, { target: { value: "Job 1" } }); + + await waitFor(() => { + expect(screen.queryByText("Test Job 2")).not.toBeInTheDocument(); + }); + + fireEvent.change(filterInput, { target: { value: "" } }); + + await waitFor(() => { + expect(screen.getByText("Test Job 1")).toBeInTheDocument(); + expect(screen.getByText("Test Job 2")).toBeInTheDocument(); + expect(screen.getByText("Test Job 3")).toBeInTheDocument(); + }); + }); + + it("filters jobs by Status across all columns", async () => { + render(); + + const filterInput = screen.getByPlaceholderText("Search jobs..."); + fireEvent.change(filterInput, { target: { value: CopyJobStatusType.Running } }); + + await waitFor(() => { + expect(screen.getByText("Test Job 1")).toBeInTheDocument(); + expect(screen.queryByText("Test Job 2")).not.toBeInTheDocument(); + expect(screen.queryByText("Test Job 3")).not.toBeInTheDocument(); + }); + }); + + it("filters jobs by Mode across all columns", async () => { + render(); + + const filterInput = screen.getByPlaceholderText("Search jobs..."); + fireEvent.change(filterInput, { target: { value: "Offline" } }); + + await waitFor(() => { + expect(screen.queryByText("Test Job 1")).not.toBeInTheDocument(); + expect(screen.getByText("Test Job 2")).toBeInTheDocument(); + expect(screen.queryByText("Test Job 3")).not.toBeInTheDocument(); + }); + }); + + it("shows no results when filter matches no jobs", async () => { + render(); + + const filterInput = screen.getByPlaceholderText("Search jobs..."); + fireEvent.change(filterInput, { target: { value: "NonExistentJob" } }); + + await waitFor(() => { + expect(screen.queryByText("Test Job 1")).not.toBeInTheDocument(); + expect(screen.queryByText("Test Job 2")).not.toBeInTheDocument(); + expect(screen.queryByText("Test Job 3")).not.toBeInTheDocument(); + }); + }); + + it("filters by partial text match", async () => { + render(); + + const filterInput = screen.getByPlaceholderText("Search jobs..."); + fireEvent.change(filterInput, { target: { value: "Test" } }); + + await waitFor(() => { + expect(screen.getByText("Test Job 1")).toBeInTheDocument(); + expect(screen.getByText("Test Job 2")).toBeInTheDocument(); + expect(screen.getByText("Test Job 3")).toBeInTheDocument(); + }); + }); + + it("resets pagination when filter changes", async () => { + const manyJobs: CopyJobType[] = Array.from({ length: 25 }, (_, i) => ({ + ...mockJobs[0], + ID: `job-${i + 1}`, + Name: `Test Job ${i + 1}`, + })); + + render(); + + // Navigate to page 2 + fireEvent.click(screen.getByLabelText("Go to next page")); + + await waitFor(() => { + expect(screen.getByText("Showing 11 - 20 of 25 items")).toBeInTheDocument(); + }); + + // Apply filter - should reset to page 1 + const filterInput = screen.getByPlaceholderText("Search jobs..."); + fireEvent.change(filterInput, { target: { value: "Job 1" } }); + + await waitFor(() => { + // Filtered results show from the beginning + expect(screen.getByText("Test Job 1")).toBeInTheDocument(); + }); + }); + + it("updates filtered count in pager", async () => { + const manyJobs: CopyJobType[] = Array.from({ length: 25 }, (_, i) => ({ + ...mockJobs[0], + ID: `job-${i + 1}`, + Name: i < 5 ? `Alpha Job ${i + 1}` : `Beta Job ${i + 1}`, + })); + + render(); + + expect(screen.getByText("Showing 1 - 10 of 25 items")).toBeInTheDocument(); + + const filterInput = screen.getByPlaceholderText("Search jobs..."); + fireEvent.change(filterInput, { target: { value: "Alpha" } }); + + await waitFor(() => { + expect(screen.queryByText("Showing 1 - 10 of 25 items")).not.toBeInTheDocument(); + // Pager should not be visible since filtered results (5) are less than page size (10) + expect(screen.queryByLabelText("Go to next page")).not.toBeInTheDocument(); + }); + }); }); describe("Pagination", () => { @@ -342,7 +509,7 @@ describe("CopyJobsList", () => { describe("Component Props", () => { it("uses default page size when not provided", () => { - const manyJobs: CopyJobType[] = Array.from({ length: 12 }, (_, i) => ({ + const manyJobs: CopyJobType[] = Array.from({ length: 20 }, (_, i) => ({ ...mockJobs[0], ID: `job-${i + 1}`, Name: `Test Job ${i + 1}`, @@ -351,7 +518,7 @@ describe("CopyJobsList", () => { render(); expect(screen.getByLabelText("Go to next page")).toBeInTheDocument(); - expect(screen.getByText("Showing 1 - 10 of 12 items")).toBeInTheDocument(); + expect(screen.getByText("Showing 1 - 15 of 20 items")).toBeInTheDocument(); }); it("passes correct props to getColumns function", async () => { @@ -440,7 +607,33 @@ describe("CopyJobsList", () => { render(); }).not.toThrow(); - expect(screen.getByText("Showing 1 - 10 of 1000 items")).toBeInTheDocument(); + expect(screen.getByText("Showing 1 - 15 of 1000 items")).toBeInTheDocument(); + }); + + it("handles filtering with null or undefined values gracefully", async () => { + const jobsWithNullValues: CopyJobType[] = [ + { + ...mockJobs[0], + ID: "job-with-values", + Name: "Valid Job", + }, + { + ...mockJobs[1], + ID: "job-null-name", + Name: undefined as unknown as string, + }, + ]; + + expect(() => { + render(); + }).not.toThrow(); + + const filterInput = screen.getByPlaceholderText("Search jobs..."); + fireEvent.change(filterInput, { target: { value: "Valid" } }); + + await waitFor(() => { + expect(screen.getByText("Valid Job")).toBeInTheDocument(); + }); }); }); }); diff --git a/src/Explorer/ContainerCopy/MonitorCopyJobs/Components/CopyJobsList.tsx b/src/Explorer/ContainerCopy/MonitorCopyJobs/Components/CopyJobsList.tsx index a263ac137..dcdfd1033 100644 --- a/src/Explorer/ContainerCopy/MonitorCopyJobs/Components/CopyJobsList.tsx +++ b/src/Explorer/ContainerCopy/MonitorCopyJobs/Components/CopyJobsList.tsx @@ -12,8 +12,9 @@ import { Stack, Sticky, StickyPositionType, + TextField, } from "@fluentui/react"; -import React, { useEffect } from "react"; +import React, { useEffect, useMemo } from "react"; import Pager from "../../../../Common/Pager"; import { useThemeStore } from "../../../../hooks/useTheme"; import { getThemeTokens } from "../../../Theme/ThemeUtil"; @@ -30,9 +31,15 @@ interface CopyJobsListProps { const styles = { container: { height: "100%" } as React.CSSProperties, stackItem: { position: "relative", marginBottom: "20px" } as React.CSSProperties, + filterContainer: { + margin: "15px 5px", + }, }; -const PAGE_SIZE = 10; +const PAGE_SIZE = 15; + +// Columns to search across +const searchableFields = ["Name", "Status", "LastUpdatedTime", "Mode"]; const CopyJobsList: React.FC = ({ jobs, handleActionClick, pageSize = PAGE_SIZE }) => { const isDarkMode = useThemeStore((state) => state.isDarkMode); @@ -41,6 +48,23 @@ const CopyJobsList: React.FC = ({ jobs, handleActionClick, pa const [sortedJobs, setSortedJobs] = React.useState(jobs); const [sortedColumnKey, setSortedColumnKey] = React.useState(undefined); const [isSortedDescending, setIsSortedDescending] = React.useState(false); + const [filterText, setFilterText] = React.useState(""); + + const filteredJobs = useMemo(() => { + if (!filterText) { + return sortedJobs; + } + const lowerFilterText = filterText.toLowerCase(); + return sortedJobs.filter((job: any) => { + return searchableFields.some((field) => { + const value = job[field]; + if (value === undefined || value === null) { + return false; + } + return String(value).toLowerCase().includes(lowerFilterText); + }); + }); + }, [sortedJobs, filterText]); useEffect(() => { setSortedJobs(jobs); @@ -64,7 +88,15 @@ const CopyJobsList: React.FC = ({ jobs, handleActionClick, pa setStartIndex(0); }; - const columns: IColumn[] = getColumns(handleSort, handleActionClick, sortedColumnKey, isSortedDescending); + const sortableColumns: IColumn[] = getColumns(handleSort, handleActionClick, sortedColumnKey, isSortedDescending); + + const handleFilterTextChange = ( + _event: React.FormEvent, + newValue?: string, + ) => { + setFilterText(newValue || ""); + setStartIndex(0); + }; const _handleRowClick = (job: CopyJobType) => { openCopyJobDetailsPanel(job); @@ -81,14 +113,25 @@ const CopyJobsList: React.FC = ({ jobs, handleActionClick, pa return (
+ +
+ +
+
= ({ jobs, handleActionClick, pa /> - {sortedJobs.length > pageSize && ( + {filteredJobs.length > pageSize && ( { setStartIndex(startIdx); diff --git a/src/Explorer/ContainerCopy/containerCopyStyles.less b/src/Explorer/ContainerCopy/containerCopyStyles.less index 6f99f4055..9cc625860 100644 --- a/src/Explorer/ContainerCopy/containerCopyStyles.less +++ b/src/Explorer/ContainerCopy/containerCopyStyles.less @@ -1,5 +1,27 @@ @import "../../../less/Common/Constants.less"; +.themedTextFieldStyles() { + .ms-TextField { + .ms-TextField-fieldGroup { + background-color: var(--colorNeutralBackground1); + border-color: var(--colorNeutralStroke1); + } + + .ms-TextField-field { + color: var(--colorNeutralForeground1); + background-color: var(--colorNeutralBackground1); + + &::placeholder { + color: var(--colorNeutralForeground4); + } + } + + .ms-Label { + color: var(--colorNeutralForeground1); + } + } +} + // Common theme-aware classes .themeText { color: var(--colorNeutralForeground1); @@ -119,25 +141,8 @@ filter: invert(1); } - .ms-TextField { - .ms-TextField-fieldGroup { - background-color: var(--colorNeutralBackground1); - border-color: var(--colorNeutralStroke1); - } + .themedTextFieldStyles(); - .ms-TextField-field { - color: var(--colorNeutralForeground1); - background-color: var(--colorNeutralBackground1); - - &::placeholder { - color: var(--colorNeutralForeground4); - } - } - - .ms-Label { - color: var(--colorNeutralForeground1); - } - } .migrationTypeDescription { p { color: var(--colorNeutralForeground1); @@ -173,6 +178,11 @@ width: 100%; max-width: 100%; margin: 0 auto; + + body.isDarkMode & { + .themedTextFieldStyles(); + } + .ms-DetailsList { width: 100%; diff --git a/test/sql/containercopy/offlineMigration.spec.ts b/test/sql/containercopy/offlineMigration.spec.ts index e9de9e00b..e99610cb4 100644 --- a/test/sql/containercopy/offlineMigration.spec.ts +++ b/test/sql/containercopy/offlineMigration.spec.ts @@ -1,258 +1,262 @@ -// /* eslint-disable @typescript-eslint/no-explicit-any */ -// import { expect, Frame, Locator, Page, test } from "@playwright/test"; -// import { truncateName } from "../../../src/Explorer/ContainerCopy/CopyJobUtils"; -// import { -// ContainerCopy, -// getAccountName, -// getDropdownItemByNameOrPosition, -// interceptAndInspectApiRequest, -// TestAccount, -// waitForApiResponse, -// } from "../../fx"; -// import { createMultipleTestContainers } from "../../testData"; +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { expect, Frame, Locator, Page, test } from "@playwright/test"; +import { truncateName } from "../../../src/Explorer/ContainerCopy/CopyJobUtils"; +import { + ContainerCopy, + getAccountName, + getDropdownItemByNameOrPosition, + interceptAndInspectApiRequest, + TestAccount, + waitForApiResponse, +} from "../../fx"; +import { createMultipleTestContainers } from "../../testData"; -// test.describe("Container Copy - Offline Migration", () => { -// let page: Page; -// let wrapper: Locator; -// let panel: Locator; -// let frame: Frame; -// let expectedJobName: string; -// let targetAccountName: string; -// let expectedSubscriptionName: string; -// let expectedCopyJobNameInitial: string; +test.describe("Container Copy - Offline Migration", () => { + let page: Page; + let wrapper: Locator; + let panel: Locator; + let frame: Frame; + let expectedJobName: string; + let targetAccountName: string; + let expectedSubscriptionName: string; + let expectedCopyJobNameInitial: string; -// test.beforeEach("Setup for offline migration test", async ({ browser }) => { -// await createMultipleTestContainers({ accountType: TestAccount.SQLContainerCopyOnly, containerCount: 2 }); + test.beforeEach("Setup for offline migration test", async ({ browser }) => { + await createMultipleTestContainers({ accountType: TestAccount.SQLContainerCopyOnly, containerCount: 2 }); -// page = await browser.newPage(); -// ({ wrapper, frame } = await ContainerCopy.open(page, TestAccount.SQLContainerCopyOnly)); -// expectedJobName = `offline_test_job_${Date.now()}`; -// targetAccountName = getAccountName(TestAccount.SQLContainerCopyOnly); -// }); + page = await browser.newPage(); + ({ wrapper, frame } = await ContainerCopy.open(page, TestAccount.SQLContainerCopyOnly)); + expectedJobName = `offline_test_job_${Date.now()}`; + targetAccountName = getAccountName(TestAccount.SQLContainerCopyOnly); + }); -// test.afterEach("Cleanup after offline migration test", async () => { -// await page.unroute(/.*/, (route) => route.continue()); -// await page.close(); -// }); + test.afterEach("Cleanup after offline migration test", async () => { + await page.unroute(/.*/, (route) => route.continue()); + await page.close(); + }); -// test("Successfully create and manage offline migration copy job", async () => { -// expect(wrapper).not.toBeNull(); -// await wrapper.locator(".commandBarContainer").waitFor({ state: "visible" }); + test("Successfully create and manage offline migration copy job", async () => { + expect(wrapper).not.toBeNull(); + await wrapper.locator(".commandBarContainer").waitFor({ state: "visible" }); -// // Open Create Copy Job panel -// const createCopyJobButton = wrapper.getByTestId("CommandBar/Button:Create Copy Job"); -// await expect(createCopyJobButton).toBeVisible(); -// await createCopyJobButton.click(); -// panel = frame.getByTestId("Panel:Create copy job"); -// await expect(panel).toBeVisible(); + // Open Create Copy Job panel + const createCopyJobButton = wrapper.getByTestId("CommandBar/Button:Create Copy Job"); + await expect(createCopyJobButton).toBeVisible(); + await createCopyJobButton.click(); + panel = frame.getByTestId("Panel:Create copy job"); + await expect(panel).toBeVisible(); -// // Reduced wait time for better performance -// await page.waitForTimeout(2000); + // Reduced wait time for better performance + await page.waitForTimeout(2000); -// // Setup subscription and account -// const subscriptionDropdown = panel.getByTestId("subscription-dropdown"); -// const expectedAccountName = targetAccountName; -// expectedSubscriptionName = await subscriptionDropdown.locator("span.ms-Dropdown-title").innerText(); + // Setup subscription and account + const subscriptionDropdown = panel.getByTestId("subscription-dropdown"); + const expectedAccountName = targetAccountName; + expectedSubscriptionName = await subscriptionDropdown.locator("span.ms-Dropdown-title").innerText(); -// await subscriptionDropdown.click(); -// const subscriptionItem = await getDropdownItemByNameOrPosition( -// frame, -// { name: expectedSubscriptionName }, -// { ariaLabel: "Subscription" }, -// ); -// await subscriptionItem.click(); + await subscriptionDropdown.click(); + const subscriptionItem = await getDropdownItemByNameOrPosition( + frame, + { name: expectedSubscriptionName }, + { ariaLabel: "Subscription" }, + ); + await subscriptionItem.click(); -// // Select account -// const accountDropdown = panel.getByTestId("account-dropdown"); -// await expect(accountDropdown).toHaveText(new RegExp(expectedAccountName)); -// await accountDropdown.click(); + // Select account + const accountDropdown = panel.getByTestId("account-dropdown"); + await expect(accountDropdown).toHaveText(new RegExp(expectedAccountName)); + await accountDropdown.click(); -// const accountItem = await getDropdownItemByNameOrPosition( -// frame, -// { name: expectedAccountName }, -// { ariaLabel: "Account" }, -// ); -// await accountItem.click(); + const accountItem = await getDropdownItemByNameOrPosition( + frame, + { name: expectedAccountName }, + { ariaLabel: "Account" }, + ); + await accountItem.click(); -// // Test offline migration mode toggle functionality -// const migrationTypeContainer = panel.getByTestId("migration-type"); + // Test offline migration mode toggle functionality + const migrationTypeContainer = panel.getByTestId("migration-type"); -// // First test online mode (should show permissions screen) -// const onlineCopyRadioButton = migrationTypeContainer.getByRole("radio", { name: /Online mode/i }); -// await onlineCopyRadioButton.click({ force: true }); -// await expect(migrationTypeContainer.getByTestId("migration-type-description-online")).toBeVisible(); + // First test online mode (should show permissions screen) + const onlineCopyRadioButton = migrationTypeContainer.getByRole("radio", { name: /Online mode/i }); + await onlineCopyRadioButton.click({ force: true }); + await expect(migrationTypeContainer.getByTestId("migration-type-description-online")).toBeVisible(); -// await panel.getByRole("button", { name: "Next" }).click(); -// await expect(panel.getByTestId("Panel:AssignPermissionsContainer")).toBeVisible(); -// await expect(panel.getByText("Online container copy", { exact: true })).toBeVisible(); + await panel.getByRole("button", { name: "Next" }).click(); + await expect(panel.getByTestId("Panel:AssignPermissionsContainer")).toBeVisible(); + await expect(panel.getByText("Online container copy", { exact: true })).toBeVisible(); -// // Go back and switch to offline mode -// await panel.getByRole("button", { name: "Previous" }).click(); + // Go back and switch to offline mode + await panel.getByRole("button", { name: "Previous" }).click(); -// const offlineCopyRadioButton = migrationTypeContainer.getByRole("radio", { name: /Offline mode/i }); -// await offlineCopyRadioButton.click({ force: true }); -// await expect(migrationTypeContainer.getByTestId("migration-type-description-offline")).toBeVisible(); + const offlineCopyRadioButton = migrationTypeContainer.getByRole("radio", { name: /Offline mode/i }); + await offlineCopyRadioButton.click({ force: true }); + await expect(migrationTypeContainer.getByTestId("migration-type-description-offline")).toBeVisible(); -// await panel.getByRole("button", { name: "Next" }).click(); + await panel.getByRole("button", { name: "Next" }).click(); -// // Verify we skip permissions screen in offline mode -// await expect(panel.getByTestId("Panel:SelectSourceAndTargetContainers")).toBeVisible(); -// await expect(panel.getByTestId("Panel:AssignPermissionsContainer")).not.toBeVisible(); + // Verify we skip permissions screen in offline mode + await expect(panel.getByTestId("Panel:SelectSourceAndTargetContainers")).toBeVisible(); + await expect(panel.getByTestId("Panel:AssignPermissionsContainer")).not.toBeVisible(); -// // Test source and target container selection with validation -// const sourceContainerDropdown = panel.getByTestId("source-containerDropdown"); -// expect(sourceContainerDropdown).toBeVisible(); -// await expect(sourceContainerDropdown).toHaveClass(/(^|\s)is-disabled(\s|$)/); + // Test source and target container selection with validation + const sourceContainerDropdown = panel.getByTestId("source-containerDropdown"); + expect(sourceContainerDropdown).toBeVisible(); + await expect(sourceContainerDropdown).toHaveClass(/(^|\s)is-disabled(\s|$)/); -// // Select source database first (containers are disabled until database is selected) -// const sourceDatabaseDropdown = panel.getByTestId("source-databaseDropdown"); -// await sourceDatabaseDropdown.click(); -// const sourceDbDropdownItem = await getDropdownItemByNameOrPosition( -// frame, -// { position: 0 }, -// { ariaLabel: "Database" }, -// ); -// await sourceDbDropdownItem.click(); + // Select source database first (containers are disabled until database is selected) + const sourceDatabaseDropdown = panel.getByTestId("source-databaseDropdown"); + await sourceDatabaseDropdown.click(); + const sourceDbDropdownItem = await getDropdownItemByNameOrPosition( + frame, + { position: 0 }, + { ariaLabel: "Database" }, + ); + await sourceDbDropdownItem.click(); -// // Now container dropdown should be enabled -// await expect(sourceContainerDropdown).not.toHaveClass(/(^|\s)is-disabled(\s|$)/); -// await sourceContainerDropdown.click(); -// const sourceContainerDropdownItem = await getDropdownItemByNameOrPosition( -// frame, -// { position: 0 }, -// { ariaLabel: "Container" }, -// ); -// await sourceContainerDropdownItem.click(); + // Now container dropdown should be enabled + await expect(sourceContainerDropdown).not.toHaveClass(/(^|\s)is-disabled(\s|$)/); + await sourceContainerDropdown.click(); + const sourceContainerDropdownItem = await getDropdownItemByNameOrPosition( + frame, + { position: 0 }, + { ariaLabel: "Container" }, + ); + await sourceContainerDropdownItem.click(); -// // Test target container selection -// const targetContainerDropdown = panel.getByTestId("target-containerDropdown"); -// expect(targetContainerDropdown).toBeVisible(); -// await expect(targetContainerDropdown).toHaveClass(/(^|\s)is-disabled(\s|$)/); + // Test target container selection + const targetContainerDropdown = panel.getByTestId("target-containerDropdown"); + expect(targetContainerDropdown).toBeVisible(); + await expect(targetContainerDropdown).toHaveClass(/(^|\s)is-disabled(\s|$)/); -// const targetDatabaseDropdown = panel.getByTestId("target-databaseDropdown"); -// await targetDatabaseDropdown.click(); -// const targetDbDropdownItem = await getDropdownItemByNameOrPosition( -// frame, -// { position: 0 }, -// { ariaLabel: "Database" }, -// ); -// await targetDbDropdownItem.click(); + const targetDatabaseDropdown = panel.getByTestId("target-databaseDropdown"); + await targetDatabaseDropdown.click(); + const targetDbDropdownItem = await getDropdownItemByNameOrPosition( + frame, + { position: 0 }, + { ariaLabel: "Database" }, + ); + await targetDbDropdownItem.click(); -// await expect(targetContainerDropdown).not.toHaveClass(/(^|\s)is-disabled(\s|$)/); -// await targetContainerDropdown.click(); + await expect(targetContainerDropdown).not.toHaveClass(/(^|\s)is-disabled(\s|$)/); + await targetContainerDropdown.click(); -// // First try selecting the same container (should show error) -// const targetContainerDropdownItem1 = await getDropdownItemByNameOrPosition( -// frame, -// { position: 0 }, -// { ariaLabel: "Container" }, -// ); -// await targetContainerDropdownItem1.click(); + // First try selecting the same container (should show error) + const targetContainerDropdownItem1 = await getDropdownItemByNameOrPosition( + frame, + { position: 0 }, + { ariaLabel: "Container" }, + ); + await targetContainerDropdownItem1.click(); -// await panel.getByRole("button", { name: "Next" }).click(); + await panel.getByRole("button", { name: "Next" }).click(); -// // Verify validation error for same source and target containers -// const errorContainer = panel.getByTestId("Panel:ErrorContainer"); -// await expect(errorContainer).toBeVisible(); -// await expect(errorContainer).toHaveText(/Source and destination containers cannot be the same/i); + // Verify validation error for same source and target containers + const errorContainer = panel.getByTestId("Panel:ErrorContainer"); + await expect(errorContainer).toBeVisible(); + await expect(errorContainer).toHaveText(/Source and destination containers cannot be the same/i); -// // Select different target container -// await targetContainerDropdown.click(); -// const targetContainerDropdownItem2 = await getDropdownItemByNameOrPosition( -// frame, -// { position: 1 }, -// { ariaLabel: "Container" }, -// ); -// await targetContainerDropdownItem2.click(); + // Select different target container + await targetContainerDropdown.click(); + const targetContainerDropdownItem2 = await getDropdownItemByNameOrPosition( + frame, + { position: 1 }, + { ariaLabel: "Container" }, + ); + await targetContainerDropdownItem2.click(); -// // Generate expected job name based on selections -// const selectedSourceDatabase = await sourceDatabaseDropdown.innerText(); -// const selectedSourceContainer = await sourceContainerDropdown.innerText(); -// const selectedTargetDatabase = await targetDatabaseDropdown.innerText(); -// const selectedTargetContainer = await targetContainerDropdown.innerText(); -// expectedCopyJobNameInitial = `${truncateName(selectedSourceDatabase)}.${truncateName( -// selectedSourceContainer, -// )}_${truncateName(selectedTargetDatabase)}.${truncateName(selectedTargetContainer)}`; + // Generate expected job name based on selections + const selectedSourceDatabase = await sourceDatabaseDropdown.innerText(); + const selectedSourceContainer = await sourceContainerDropdown.innerText(); + const selectedTargetDatabase = await targetDatabaseDropdown.innerText(); + const selectedTargetContainer = await targetContainerDropdown.innerText(); + expectedCopyJobNameInitial = `${truncateName(selectedSourceDatabase)}.${truncateName( + selectedSourceContainer, + )}_${truncateName(selectedTargetDatabase)}.${truncateName(selectedTargetContainer)}`; -// await panel.getByRole("button", { name: "Next" }).click(); + await panel.getByRole("button", { name: "Next" }).click(); -// // Error should disappear and preview should be visible -// await expect(errorContainer).not.toBeVisible(); -// await expect(panel.getByTestId("Panel:PreviewCopyJob")).toBeVisible(); + // Error should disappear and preview should be visible + await expect(errorContainer).not.toBeVisible(); + await expect(panel.getByTestId("Panel:PreviewCopyJob")).toBeVisible(); -// // Verify job preview details -// const previewContainer = panel.getByTestId("Panel:PreviewCopyJob"); -// await expect(previewContainer).toBeVisible(); -// await expect(previewContainer.getByTestId("source-subscription-name")).toHaveText(expectedSubscriptionName); -// await expect(previewContainer.getByTestId("source-account-name")).toHaveText(expectedAccountName); + // Verify job preview details + const previewContainer = panel.getByTestId("Panel:PreviewCopyJob"); + await expect(previewContainer).toBeVisible(); + await expect(previewContainer.getByTestId("source-subscription-name")).toHaveText(expectedSubscriptionName); + await expect(previewContainer.getByTestId("source-account-name")).toHaveText(expectedAccountName); -// const jobNameInput = previewContainer.getByTestId("job-name-textfield"); -// await expect(jobNameInput).toHaveValue(new RegExp(expectedCopyJobNameInitial)); + const jobNameInput = previewContainer.getByTestId("job-name-textfield"); + await expect(jobNameInput).toHaveValue(new RegExp(expectedCopyJobNameInitial)); -// const primaryBtn = panel.getByRole("button", { name: "Copy", exact: true }); -// await expect(primaryBtn).not.toHaveClass(/(^|\s)is-disabled(\s|$)/); + const primaryBtn = panel.getByRole("button", { name: "Copy", exact: true }); + await expect(primaryBtn).not.toHaveClass(/(^|\s)is-disabled(\s|$)/); -// // Test invalid job name validation (spaces not allowed) -// await jobNameInput.fill("test job name"); -// await expect(primaryBtn).toHaveClass(/(^|\s)is-disabled(\s|$)/); + // Test invalid job name validation (spaces not allowed) + await jobNameInput.fill("test job name"); + await expect(primaryBtn).toHaveClass(/(^|\s)is-disabled(\s|$)/); -// // Test duplicate job name error handling -// const duplicateJobName = "test-job-name-1"; -// await jobNameInput.fill(duplicateJobName); + // Test duplicate job name error handling + const duplicateJobName = "test-job-name-1"; + await jobNameInput.fill(duplicateJobName); -// const copyButton = panel.getByRole("button", { name: "Copy", exact: true }); -// const expectedErrorMessage = `Duplicate job name '${duplicateJobName}'`; + const copyButton = panel.getByRole("button", { name: "Copy", exact: true }); + const expectedErrorMessage = `Duplicate job name '${duplicateJobName}'`; -// await interceptAndInspectApiRequest( -// page, -// `${expectedAccountName}/dataTransferJobs/${duplicateJobName}`, -// "PUT", -// new Error(expectedErrorMessage), -// (url?: string) => url?.includes(duplicateJobName) ?? false, -// ); + await interceptAndInspectApiRequest( + page, + `${expectedAccountName}/dataTransferJobs/${duplicateJobName}`, + "PUT", + new Error(expectedErrorMessage), + (url?: string) => url?.includes(duplicateJobName) ?? false, + ); -// let errorThrown = false; -// try { -// await copyButton.click(); -// await page.waitForTimeout(2000); -// } catch (error: any) { -// errorThrown = true; -// expect(error.message).toContain("not allowed"); -// } + let errorThrown = false; + try { + await copyButton.click(); + await page.waitForTimeout(2000); + } catch (error: any) { + errorThrown = true; + expect(error.message).toContain("not allowed"); + } -// if (!errorThrown) { -// const errorContainer = panel.getByTestId("Panel:ErrorContainer"); -// await expect(errorContainer).toBeVisible(); -// await expect(errorContainer).toHaveText(new RegExp(expectedErrorMessage, "i")); -// } + if (!errorThrown) { + const errorContainer = panel.getByTestId("Panel:ErrorContainer"); + await expect(errorContainer).toBeVisible(); + await expect(errorContainer).toHaveText(new RegExp(expectedErrorMessage, "i")); + } -// await expect(panel).toBeVisible(); + await expect(panel).toBeVisible(); -// // Test successful job creation with valid job name -// const validJobName = expectedJobName; + // Test successful job creation with valid job name + const validJobName = expectedJobName; -// const copyJobCreationPromise = waitForApiResponse( -// page, -// `${expectedAccountName}/dataTransferJobs/${validJobName}`, -// "PUT", -// ); + const copyJobCreationPromise = waitForApiResponse( + page, + `${expectedAccountName}/dataTransferJobs/${validJobName}`, + "PUT", + ); -// await jobNameInput.fill(validJobName); -// await expect(copyButton).not.toHaveClass(/(^|\s)is-disabled(\s|$)/); + await jobNameInput.fill(validJobName); + await expect(copyButton).not.toHaveClass(/(^|\s)is-disabled(\s|$)/); -// await copyButton.click(); + await copyButton.click(); -// const response = await copyJobCreationPromise; -// expect(response.ok()).toBe(true); + const response = await copyJobCreationPromise; + expect(response.ok()).toBe(true); -// // Verify panel closes and job appears in the list -// await expect(panel).not.toBeVisible({ timeout: 5000 }); + // Verify panel closes and job appears in the list + await expect(panel).not.toBeVisible(); -// const jobsListContainer = wrapper.locator(".CopyJobListContainer .ms-DetailsList-contentWrapper .ms-List-page"); -// await jobsListContainer.waitFor({ state: "visible", timeout: 5000 }); + const filterTextField = wrapper.getByTestId("CopyJobsList/FilterTextField"); + await filterTextField.waitFor({ state: "visible" }); + await filterTextField.fill(validJobName); -// const jobItem = jobsListContainer.getByText(validJobName); -// await jobItem.waitFor({ state: "visible", timeout: 5000 }); -// await expect(jobItem).toBeVisible(); -// }); -// }); + const jobsListContainer = wrapper.locator(".CopyJobListContainer .ms-DetailsList-contentWrapper .ms-List-page"); + await jobsListContainer.waitFor({ state: "visible" }); + + const jobItem = jobsListContainer.getByText(validJobName); + await jobItem.waitFor({ state: "visible" }); + await expect(jobItem).toBeVisible(); + }); +}); diff --git a/test/sql/containercopy/onlineMigration.spec.ts b/test/sql/containercopy/onlineMigration.spec.ts index 0d1bd0209..e11b3decd 100644 --- a/test/sql/containercopy/onlineMigration.spec.ts +++ b/test/sql/containercopy/onlineMigration.spec.ts @@ -1,185 +1,189 @@ -// /* eslint-disable @typescript-eslint/no-explicit-any */ -// import { expect, Frame, Locator, Page, test } from "@playwright/test"; -// import { -// ContainerCopy, -// getAccountName, -// getDropdownItemByNameOrPosition, -// TestAccount, -// waitForApiResponse, -// } from "../../fx"; -// import { createMultipleTestContainers } from "../../testData"; +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { expect, Frame, Locator, Page, test } from "@playwright/test"; +import { + ContainerCopy, + getAccountName, + getDropdownItemByNameOrPosition, + TestAccount, + waitForApiResponse, +} from "../../fx"; +import { createMultipleTestContainers } from "../../testData"; -// test.describe("Container Copy - Online Migration", () => { -// let page: Page; -// let wrapper: Locator; -// let panel: Locator; -// let frame: Frame; -// let targetAccountName: string; +test.describe("Container Copy - Online Migration", () => { + let page: Page; + let wrapper: Locator; + let panel: Locator; + let frame: Frame; + let targetAccountName: string; -// test.beforeEach("Setup for online migration test", async ({ browser }) => { -// await createMultipleTestContainers({ accountType: TestAccount.SQLContainerCopyOnly, containerCount: 2 }); + test.beforeEach("Setup for online migration test", async ({ browser }) => { + await createMultipleTestContainers({ accountType: TestAccount.SQLContainerCopyOnly, containerCount: 2 }); -// page = await browser.newPage(); -// ({ wrapper, frame } = await ContainerCopy.open(page, TestAccount.SQLContainerCopyOnly)); -// targetAccountName = getAccountName(TestAccount.SQLContainerCopyOnly); -// }); + page = await browser.newPage(); + ({ wrapper, frame } = await ContainerCopy.open(page, TestAccount.SQLContainerCopyOnly)); + targetAccountName = getAccountName(TestAccount.SQLContainerCopyOnly); + }); -// test.afterEach("Cleanup after online migration test", async () => { -// await page.unroute(/.*/, (route) => route.continue()); -// await page.close(); -// }); + test.afterEach("Cleanup after online migration test", async () => { + await page.unroute(/.*/, (route) => route.continue()); + await page.close(); + }); -// test("Successfully create and manage online migration copy job", async () => { -// expect(wrapper).not.toBeNull(); -// await wrapper.locator(".commandBarContainer").waitFor({ state: "visible" }); + test("Successfully create and manage online migration copy job", async () => { + expect(wrapper).not.toBeNull(); + await wrapper.locator(".commandBarContainer").waitFor({ state: "visible" }); -// // Open Create Copy Job panel -// const createCopyJobButton = wrapper.getByTestId("CommandBar/Button:Create Copy Job"); -// await expect(createCopyJobButton).toBeVisible(); -// await createCopyJobButton.click(); -// panel = frame.getByTestId("Panel:Create copy job"); -// await expect(panel).toBeVisible(); + // Open Create Copy Job panel + const createCopyJobButton = wrapper.getByTestId("CommandBar/Button:Create Copy Job"); + await expect(createCopyJobButton).toBeVisible(); + await createCopyJobButton.click(); + panel = frame.getByTestId("Panel:Create copy job"); + await expect(panel).toBeVisible(); -// // Reduced wait time for better performance -// await page.waitForTimeout(1000); + // Reduced wait time for better performance + await page.waitForTimeout(1000); -// // Enable online migration mode -// const migrationTypeContainer = panel.getByTestId("migration-type"); -// const onlineCopyRadioButton = migrationTypeContainer.getByRole("radio", { name: /Online mode/i }); -// await onlineCopyRadioButton.click({ force: true }); + // Enable online migration mode + const migrationTypeContainer = panel.getByTestId("migration-type"); + const onlineCopyRadioButton = migrationTypeContainer.getByRole("radio", { name: /Online mode/i }); + await onlineCopyRadioButton.click({ force: true }); -// await expect(migrationTypeContainer.getByTestId("migration-type-description-online")).toBeVisible(); + await expect(migrationTypeContainer.getByTestId("migration-type-description-online")).toBeVisible(); -// await panel.getByRole("button", { name: "Next" }).click(); + await panel.getByRole("button", { name: "Next" }).click(); -// // Verify permissions screen is shown for online migration -// const permissionScreen = panel.getByTestId("Panel:AssignPermissionsContainer"); -// await expect(permissionScreen).toBeVisible(); -// await expect(permissionScreen.getByText("Online container copy", { exact: true })).toBeVisible(); + // Verify permissions screen is shown for online migration + const permissionScreen = panel.getByTestId("Panel:AssignPermissionsContainer"); + await expect(permissionScreen).toBeVisible(); + await expect(permissionScreen.getByText("Online container copy", { exact: true })).toBeVisible(); -// // Skip permissions setup and proceed to container selection -// await panel.getByRole("button", { name: "Next" }).click(); + // Skip permissions setup and proceed to container selection + await panel.getByRole("button", { name: "Next" }).click(); -// // Configure source and target containers for online migration -// const sourceDatabaseDropdown = panel.getByTestId("source-databaseDropdown"); -// await sourceDatabaseDropdown.click(); -// const sourceDbDropdownItem = await getDropdownItemByNameOrPosition( -// frame, -// { position: 0 }, -// { ariaLabel: "Database" }, -// ); -// await sourceDbDropdownItem.click(); + // Configure source and target containers for online migration + const sourceDatabaseDropdown = panel.getByTestId("source-databaseDropdown"); + await sourceDatabaseDropdown.click(); + const sourceDbDropdownItem = await getDropdownItemByNameOrPosition( + frame, + { position: 0 }, + { ariaLabel: "Database" }, + ); + await sourceDbDropdownItem.click(); -// const sourceContainerDropdown = panel.getByTestId("source-containerDropdown"); -// await sourceContainerDropdown.click(); -// const sourceContainerDropdownItem = await getDropdownItemByNameOrPosition( -// frame, -// { position: 0 }, -// { ariaLabel: "Container" }, -// ); -// await sourceContainerDropdownItem.click(); + const sourceContainerDropdown = panel.getByTestId("source-containerDropdown"); + await sourceContainerDropdown.click(); + const sourceContainerDropdownItem = await getDropdownItemByNameOrPosition( + frame, + { position: 0 }, + { ariaLabel: "Container" }, + ); + await sourceContainerDropdownItem.click(); -// const targetDatabaseDropdown = panel.getByTestId("target-databaseDropdown"); -// await targetDatabaseDropdown.click(); -// const targetDbDropdownItem = await getDropdownItemByNameOrPosition( -// frame, -// { position: 0 }, -// { ariaLabel: "Database" }, -// ); -// await targetDbDropdownItem.click(); + const targetDatabaseDropdown = panel.getByTestId("target-databaseDropdown"); + await targetDatabaseDropdown.click(); + const targetDbDropdownItem = await getDropdownItemByNameOrPosition( + frame, + { position: 0 }, + { ariaLabel: "Database" }, + ); + await targetDbDropdownItem.click(); -// const targetContainerDropdown = panel.getByTestId("target-containerDropdown"); -// await targetContainerDropdown.click(); -// const targetContainerDropdownItem = await getDropdownItemByNameOrPosition( -// frame, -// { position: 1 }, -// { ariaLabel: "Container" }, -// ); -// await targetContainerDropdownItem.click(); + const targetContainerDropdown = panel.getByTestId("target-containerDropdown"); + await targetContainerDropdown.click(); + const targetContainerDropdownItem = await getDropdownItemByNameOrPosition( + frame, + { position: 1 }, + { ariaLabel: "Container" }, + ); + await targetContainerDropdownItem.click(); -// await panel.getByRole("button", { name: "Next" }).click(); + await panel.getByRole("button", { name: "Next" }).click(); -// // Verify job preview and create the online migration job -// const previewContainer = panel.getByTestId("Panel:PreviewCopyJob"); -// await expect(previewContainer.getByTestId("source-account-name")).toHaveText(targetAccountName); + // Verify job preview and create the online migration job + const previewContainer = panel.getByTestId("Panel:PreviewCopyJob"); + await expect(previewContainer.getByTestId("source-account-name")).toHaveText(targetAccountName); -// const jobNameInput = previewContainer.getByTestId("job-name-textfield"); -// const onlineMigrationJobName = await jobNameInput.inputValue(); + const jobNameInput = previewContainer.getByTestId("job-name-textfield"); + const onlineMigrationJobName = await jobNameInput.inputValue(); -// const copyButton = panel.getByRole("button", { name: "Copy", exact: true }); + const copyButton = panel.getByRole("button", { name: "Copy", exact: true }); -// const copyJobCreationPromise = waitForApiResponse( -// page, -// `${targetAccountName}/dataTransferJobs/${onlineMigrationJobName}`, -// "PUT", -// ); -// await copyButton.click(); -// await page.waitForTimeout(1000); // Reduced wait time + const copyJobCreationPromise = waitForApiResponse( + page, + `${targetAccountName}/dataTransferJobs/${onlineMigrationJobName}`, + "PUT", + ); + await copyButton.click(); + await page.waitForTimeout(1000); // Reduced wait time -// const response = await copyJobCreationPromise; -// expect(response.ok()).toBe(true); + const response = await copyJobCreationPromise; + expect(response.ok()).toBe(true); -// // Verify panel closes and job appears in the list -// await expect(panel).not.toBeVisible({ timeout: 5000 }); + // Verify panel closes and job appears in the list + await expect(panel).not.toBeVisible(); -// const jobsListContainer = wrapper.locator(".CopyJobListContainer .ms-DetailsList-contentWrapper .ms-List-page"); -// await jobsListContainer.waitFor({ state: "visible", timeout: 5000 }); + const filterTextField = wrapper.getByTestId("CopyJobsList/FilterTextField"); + await filterTextField.waitFor({ state: "visible" }); + await filterTextField.fill(onlineMigrationJobName); -// let jobRow, statusCell, actionMenuButton; -// jobRow = jobsListContainer.locator(".ms-DetailsRow", { hasText: onlineMigrationJobName }); -// statusCell = jobRow.locator("[data-automationid='DetailsRowCell'][data-automation-key='CopyJobStatus']"); -// await jobRow.waitFor({ state: "visible", timeout: 5000 }); + const jobsListContainer = wrapper.locator(".CopyJobListContainer .ms-DetailsList-contentWrapper .ms-List-page"); + await jobsListContainer.waitFor({ state: "visible" }); -// // Verify job status changes to queued state -// await expect(statusCell).toContainText(/running|queued|pending/i, { timeout: 5000 }); + let jobRow, statusCell, actionMenuButton; + jobRow = jobsListContainer.locator(".ms-DetailsRow", { hasText: onlineMigrationJobName }); + statusCell = jobRow.locator("[data-automationid='DetailsRowCell'][data-automation-key='CopyJobStatus']"); + await jobRow.waitFor({ state: "visible" }); -// // Test job lifecycle management through action menu -// actionMenuButton = wrapper.getByTestId(`CopyJobActionMenu/Button:${onlineMigrationJobName}`); -// await actionMenuButton.click(); + // Verify job status changes to queued state + await expect(statusCell).toContainText(/running|queued|pending/i); -// // Test pause functionality -// const pauseAction = frame.locator(".ms-ContextualMenu-list button:has-text('Pause')"); -// await pauseAction.click(); + // Test job lifecycle management through action menu + actionMenuButton = wrapper.getByTestId(`CopyJobActionMenu/Button:${onlineMigrationJobName}`); + await actionMenuButton.click(); -// const pauseResponse = await waitForApiResponse( -// page, -// `${targetAccountName}/dataTransferJobs/${onlineMigrationJobName}/pause`, -// "POST", -// ); -// expect(pauseResponse.ok()).toBe(true); + // Test pause functionality + const pauseAction = frame.locator(".ms-ContextualMenu-list button:has-text('Pause')"); + await pauseAction.click(); -// // Verify job status changes to paused -// jobRow = jobsListContainer.locator(".ms-DetailsRow", { hasText: onlineMigrationJobName }); -// await jobRow.waitFor({ state: "visible", timeout: 5000 }); -// statusCell = jobRow.locator("[data-automationid='DetailsRowCell'][data-automation-key='CopyJobStatus']"); -// await expect(statusCell).toContainText(/paused/i, { timeout: 5000 }); -// await page.waitForTimeout(1000); + const pauseResponse = await waitForApiResponse( + page, + `${targetAccountName}/dataTransferJobs/${onlineMigrationJobName}/pause`, + "POST", + ); + expect(pauseResponse.ok()).toBe(true); -// // Test cancel job functionality -// actionMenuButton = wrapper.getByTestId(`CopyJobActionMenu/Button:${onlineMigrationJobName}`); -// await actionMenuButton.click(); -// await frame.locator(".ms-ContextualMenu-list button:has-text('Cancel')").click(); + // Verify job status changes to paused + jobRow = jobsListContainer.locator(".ms-DetailsRow", { hasText: onlineMigrationJobName }); + await jobRow.waitFor({ state: "visible", timeout: 5000 }); + statusCell = jobRow.locator("[data-automationid='DetailsRowCell'][data-automation-key='CopyJobStatus']"); + await expect(statusCell).toContainText(/paused/i, { timeout: 5000 }); + await page.waitForTimeout(1000); -// // Verify cancellation confirmation dialog -// await expect(frame.locator(".ms-Dialog-main")).toBeVisible({ timeout: 2000 }); -// await expect(frame.locator(".ms-Dialog-main")).toContainText(onlineMigrationJobName); + // Test cancel job functionality + actionMenuButton = wrapper.getByTestId(`CopyJobActionMenu/Button:${onlineMigrationJobName}`); + await actionMenuButton.click(); + await frame.locator(".ms-ContextualMenu-list button:has-text('Cancel')").click(); -// const cancelDialogButton = frame.locator(".ms-Dialog-main").getByTestId("DialogButton:Cancel"); -// await expect(cancelDialogButton).toBeVisible(); -// await cancelDialogButton.click(); -// await expect(frame.locator(".ms-Dialog-main")).not.toBeVisible(); + // Verify cancellation confirmation dialog + await expect(frame.locator(".ms-Dialog-main")).toBeVisible({ timeout: 2000 }); + await expect(frame.locator(".ms-Dialog-main")).toContainText(onlineMigrationJobName); -// actionMenuButton = wrapper.getByTestId(`CopyJobActionMenu/Button:${onlineMigrationJobName}`); -// await actionMenuButton.click(); -// await frame.locator(".ms-ContextualMenu-list button:has-text('Cancel')").click(); + const cancelDialogButton = frame.locator(".ms-Dialog-main").getByTestId("DialogButton:Cancel"); + await expect(cancelDialogButton).toBeVisible(); + await cancelDialogButton.click(); + await expect(frame.locator(".ms-Dialog-main")).not.toBeVisible(); -// const confirmDialogButton = frame.locator(".ms-Dialog-main").getByTestId("DialogButton:Confirm"); -// await expect(confirmDialogButton).toBeVisible(); -// await confirmDialogButton.click(); + actionMenuButton = wrapper.getByTestId(`CopyJobActionMenu/Button:${onlineMigrationJobName}`); + await actionMenuButton.click(); + await frame.locator(".ms-ContextualMenu-list button:has-text('Cancel')").click(); -// // Verify final job status is cancelled -// jobRow = jobsListContainer.locator(".ms-DetailsRow", { hasText: onlineMigrationJobName }); -// statusCell = jobRow.locator("[data-automationid='DetailsRowCell'][data-automation-key='CopyJobStatus']"); -// await expect(statusCell).toContainText(/cancelled/i, { timeout: 5000 }); -// }); -// }); + const confirmDialogButton = frame.locator(".ms-Dialog-main").getByTestId("DialogButton:Confirm"); + await expect(confirmDialogButton).toBeVisible(); + await confirmDialogButton.click(); + + // Verify final job status is cancelled + jobRow = jobsListContainer.locator(".ms-DetailsRow", { hasText: onlineMigrationJobName }); + statusCell = jobRow.locator("[data-automationid='DetailsRowCell'][data-automation-key='CopyJobStatus']"); + await expect(statusCell).toContainText(/cancelled/i, { timeout: 5000 }); + }); +}); diff --git a/test/sql/containercopy/permissionsScreen.spec.ts b/test/sql/containercopy/permissionsScreen.spec.ts index fa7d3e199..f592bf4c7 100644 --- a/test/sql/containercopy/permissionsScreen.spec.ts +++ b/test/sql/containercopy/permissionsScreen.spec.ts @@ -134,7 +134,7 @@ test.describe("Container Copy - Permission Screen Verification", () => { const pitrBtn = accordionPanel.getByTestId("pointInTimeRestore:PrimaryBtn"); await expect(pitrBtn).toBeVisible(); - await pitrBtn.click(); + await pitrBtn.click({ force: true }); // Verify new page opens with correct URL pattern page.context().on("page", async (newPage) => { @@ -246,7 +246,7 @@ test.describe("Container Copy - Permission Screen Verification", () => { const toggleButton = crossAccordionPanel.getByTestId("btn-toggle"); await expect(toggleButton).toBeVisible(); - await toggleButton.click(); + await toggleButton.click({ force: true }); // Verify popover functionality const popover = frame.locator("[data-test='popover-container']"); @@ -257,7 +257,7 @@ test.describe("Container Copy - Permission Screen Verification", () => { await expect(yesButton).toBeVisible(); await expect(noButton).toBeVisible(); - await yesButton.click(); + await yesButton.click({ force: true }); // Verify loading states await expect(loadingOverlay).toBeVisible(); @@ -265,6 +265,6 @@ test.describe("Container Copy - Permission Screen Verification", () => { await expect(popover).toBeHidden({ timeout: 10 * 1000 }); // Cancel the panel to clean up - await panel.getByRole("button", { name: "Cancel" }).click(); + await panel.getByRole("button", { name: "Cancel" }).click({ force: true }); }); });