diff --git a/.gitignore b/.gitignore index 22d1d31ef..10257ffc8 100644 --- a/.gitignore +++ b/.gitignore @@ -304,16 +304,14 @@ coverage *.sw? # Playwright +playwright-report/ ui-spa/playwright/test-results/ -ui-spa/playwright/playwright-report/ ui-spa/blob-report/ ui-spa/playwright/.cache/ -ui-spa/playwright-report/ ui-spa/playwright/integration-test-results.xml ui-spa/playwright/.nyc_output ui-spa/unit-test-results.xml e2e/pw/test-results/ -e2e/pw/playwright-report/ e2e/pw/.auth/ e2e/pw/.state/ e2e/pw/test-evidence/ @@ -334,7 +332,7 @@ nuget.config # postman *.postman_environment.json -# E2E Postman Tests +# E2E Tests e2e/*/secrets.config.ps1 e2e/*/*.secrets.config.ps1 e2e/postman/*.postman_environment @@ -344,6 +342,7 @@ e2e/postman/LCCTestEnvironment_updated.* e2e/postman/*_updated.postman_collection.json e2e/postman/*postman_collection_updated.json + # Environment files — keep templates tracked, ignore environment-specific # values anywhere in the repo (e.g. e2e/pw/.env.local). .env.* diff --git a/devops-pipelines/move-playwright-test.yml b/devops-pipelines/move-playwright-test.yml new file mode 100644 index 000000000..58498a83c --- /dev/null +++ b/devops-pipelines/move-playwright-test.yml @@ -0,0 +1,86 @@ +schedules: + - cron: '0 1 * * Tue-Sat' + displayName: 'Weekday 1am UTC test' + branches: + include: + - main + always: true + +trigger: none + +parameters: + - name: environment + type: string + default: staging + - name: agentPool + type: string + default: "LACC PreProd Pool" + - name: testFileSizeMb + type: number + default: 2048 # 2GB + +jobs: + - job: runMoveTest + displayName: "Run Move Test with Playwright" + pool: ${{ parameters.agentPool }} + variables: + - group: lacc-automated-testing-${{ parameters.environment }} + - group: lacc-backend-config-${{ parameters.environment }} + - name: nodeVersion + value: "20.x" + - name: workingDir + value: "$(System.DefaultWorkingDirectory)/e2e/pw" + + steps: + - task: UseNode@1 + displayName: "Use Node.js" + inputs: + version: "$(nodeVersion)" + + - task: Npm@1 + displayName: "Install NPM Dependencies" + inputs: + command: "ci" + workingDir: "$(workingDir)" + + - script: node_modules/.bin/playwright install --with-deps chromium + displayName: "Install Browsers" + workingDirectory: $(workingDir) + + - script: | + npx --no-install --ignore-scripts \ + playwright test egress-to-netapp-move-large-default.spec.ts + workingDirectory: $(workingDir) + displayName: "Run E2E Tests" + env: + CI: true + TEST_FILE_SIZE_MB: ${{ parameters.testFileSizeMb }} + BASE_URL: $(RedirectUrlLccUi) + CMS_LOGIN_PAGE: "$(LccApiBaseUrl)/api/tactical/login" + EGRESS_BASE_URL: $(EgressOptionsUrl) + TENANT_ID: $(TenantId) + LCC_API_CLIENT_ID: $(CallingAppValidAudience) + E2E_AD_USER: $(AadUserName) + E2E_AD_PASSWORD: $(AadUserPassword) + CMS_USERNAME: $(CmsUserName) + CMS_PASSWORD: $(CmsUserPassword) + EGRESS_SERVICE_ACCOUNT_AUTH: $(EgressServiceAccountAuth) + DEFAULT_WORKSPACE_ID: $(E2eEgressWorkspaceId) + DEFAULT_WORKSPACE_NAME: $(E2eEgressWorkspaceName) + DEFAULT_CASE_URN: $(E2eTestsCaseUrn) + DEFAULT_CASE_ID: $(E2eTestsCaseId) + LCC_API_BASE_URL: $(LccApiBaseUrl) + NETAPP_OPERATION_NAME: $(E2eNetAppFolderName) + + - task: PublishTestResults@2 + displayName: "Publish Test Results (JUnit)" + inputs: + testResultsFormat: JUnit + testResultsFiles: "$(workingDir)/playwright-report/e2e-test-report.xml" + publishRunAttachments: false + condition: succeededOrFailed() + + - publish: "$(workingDir)/playwright-report" + artifact: "$(System.JobId)-e2e-playwright-report" + displayName: "Publish Report Artifact" + condition: succeededOrFailed() diff --git a/e2e/pw/fixtures/setup-helper-default.ts b/e2e/pw/fixtures/setup-helper-default.ts index aa9598bac..f5db3581d 100644 --- a/e2e/pw/fixtures/setup-helper-default.ts +++ b/e2e/pw/fixtures/setup-helper-default.ts @@ -4,11 +4,12 @@ import { authenticateEgress, createFolder, uploadFile, + getUploadedFile, } from "../helpers/egress-api"; import { TacticalLoginPage } from "../pages/TacticalLoginPage"; import { AzureADLoginPage } from "../pages/AzureADLoginPage"; import { CaseSearchPage } from "../pages/CaseSearchPage"; -import type { TestSetupResult, UploadedFile } from "../helpers/types"; +import type { TestSetupResult } from "../helpers/types"; export interface DefaultSetupOptions { fileSizeMb?: number; @@ -75,7 +76,8 @@ export async function setupDefaultTestData( console.log( `[2/3] Ensuring subfolder ${uploadSubfolder} exists in source + destination...` ); - await createFolder( + // Capture the source folder id for Move test verification + const sourceSubfolderId = await createFolder( config.egressBaseUrl, egressToken, workspaceId, @@ -98,7 +100,7 @@ export async function setupDefaultTestData( `[3/3] Uploading ${fileCount} test file(s) of ${fileSizeMb}MB to ${workspaceName} (${workspaceId}) at ${uploadPath}...` ); const fileSizeBytes = fileSizeMb * 1024 * 1024; - const files: UploadedFile[] = []; + const uploadIds: string[] = []; for (let i = 1; i <= fileCount; i++) { const timestamp = new Date() @@ -107,7 +109,7 @@ export async function setupDefaultTestData( .slice(0, 19); const fileName = `generated-${fileSizeMb}MB-${timestamp}-file${i}.txt`; console.log(` Uploading ${fileName} (${i}/${fileCount})...`); - const file = await uploadFile( + const uploadId = await uploadFile( config.egressBaseUrl, egressToken, workspaceId, @@ -115,9 +117,25 @@ export async function setupDefaultTestData( fileName, uploadPath ); - files.push(file); + uploadIds.push(uploadId); } + console.log (" Getting the uploaded file ID(s)...") + const files = await Promise.all( + uploadIds.map(uploadId => + getUploadedFile( + config.egressBaseUrl, + egressToken, + workspaceId, + uploadId, + { + timeoutMs: Math.max(30000, fileSizeMb * 15000), + retryDelay: Math.min(10000,Math.max(2000, fileSizeMb * 5)), + } + ) + ) + ); + console.log("=== Upload Complete ===\n"); console.log( @@ -177,6 +195,9 @@ export async function setupDefaultTestData( caseUrn, caseId: Number.isFinite(caseIdNum) ? caseIdNum : undefined, uploadSubfolder, + uploadPath, + sourceSubfolderId, destinationSubfolderId, + egressToken, }; } diff --git a/e2e/pw/fixtures/setup-helper.ts b/e2e/pw/fixtures/setup-helper.ts index afa4316a0..504889f22 100644 --- a/e2e/pw/fixtures/setup-helper.ts +++ b/e2e/pw/fixtures/setup-helper.ts @@ -6,13 +6,14 @@ import { createWorkspace, addUserToWorkspace, uploadFile, + getUploadedFile, } from "../helpers/egress-api"; import { getAuthTokens } from "../helpers/auth-api"; import { registerCase } from "../helpers/case-api"; import { TacticalLoginPage } from "../pages/TacticalLoginPage"; import { AzureADLoginPage } from "../pages/AzureADLoginPage"; import { CaseSearchPage } from "../pages/CaseSearchPage"; -import type { TestSetupResult, UploadedFile } from "../helpers/types"; +import type { TestSetupResult } from "../helpers/types"; export interface SetupOptions { fileSizeMb?: number; @@ -73,7 +74,7 @@ export async function setupTestData( `[5/5] Uploading ${fileCount} test file(s) of ${fileSizeMb}MB each...`, ); const fileSizeBytes = fileSizeMb * 1024 * 1024; - const files: UploadedFile[] = []; + const uploadIds: string[] = []; for (let i = 1; i <= fileCount; i++) { const timestamp = new Date() @@ -82,16 +83,32 @@ export async function setupTestData( .slice(0, 19); const fileName = `generated-${fileSizeMb}MB-${timestamp}-file${i}.txt`; console.log(` Uploading ${fileName} (${i}/${fileCount})...`); - const file = await uploadFile( + const uploadId = await uploadFile( config.egressBaseUrl, egressToken, workspaceId, fileSizeBytes, fileName, ); - files.push(file); + uploadIds.push(uploadId); } + console.log (" Getting the uploaded file ID(s)...") + const files = await Promise.all( + uploadIds.map(uploadId => + getUploadedFile( + config.egressBaseUrl, + egressToken, + workspaceId, + uploadId, + { + timeoutMs: Math.max(30000, fileSizeMb * 15000), + retryDelay: Math.min(10000,Math.max(2000, fileSizeMb * 5)), + } + ) + ) + ); + console.log("=== Workspace Setup Complete ===\n"); // Step 2: Get auth tokens and register a fresh case diff --git a/e2e/pw/fixtures/teardown-helper.ts b/e2e/pw/fixtures/teardown-helper.ts index 589220de0..ce144aa5c 100644 --- a/e2e/pw/fixtures/teardown-helper.ts +++ b/e2e/pw/fixtures/teardown-helper.ts @@ -56,8 +56,8 @@ export async function teardownTestData(ctx: TeardownContext): Promise { )); const fileIds = ctx.files - .map((f) => f.id) - .filter((id): id is string => !!id); + .map((f) => f.fileId) + .filter((fileId): fileId is string => !!fileId); await deleteFiles(config.egressBaseUrl, token, ctx.workspaceId, fileIds); if (ctx.destinationSubfolderId) { diff --git a/e2e/pw/fixtures/test-fixtures-register-case.ts b/e2e/pw/fixtures/test-fixtures-register-case.ts index ac49a0fb1..d28416ad4 100644 --- a/e2e/pw/fixtures/test-fixtures-register-case.ts +++ b/e2e/pw/fixtures/test-fixtures-register-case.ts @@ -6,9 +6,10 @@ import { authenticateEgress, createFolder, uploadFile, + getUploadedFile, } from "../helpers/egress-api"; import { REGISTER_CASE_NETAPP_FOLDER } from "../helpers/constants"; -import type { TestSetupResult, UploadedFile } from "../helpers/types"; +import type { TestSetupResult } from "../helpers/types"; import { STATE_FILE, type RegisterCaseSharedState, @@ -90,14 +91,15 @@ export const test = base.extend< ` Uploading ${testOptions.fileCount} x ${testOptions.fileSizeMb}MB file(s) to ${uploadPath}...` ); const fileSizeBytes = testOptions.fileSizeMb * 1024 * 1024; - const files: UploadedFile[] = []; + const uploadIds: string[] = []; + for (let i = 1; i <= testOptions.fileCount; i++) { const timestamp = new Date() .toISOString() .replace(/[:.]/g, "-") .slice(0, 19); const fileName = `generated-${testOptions.fileSizeMb}MB-${timestamp}-file${i}.txt`; - const file = await uploadFile( + const uploadId = await uploadFile( config.egressBaseUrl, token, shared.workspace.id, @@ -105,9 +107,25 @@ export const test = base.extend< fileName, uploadPath ); - files.push(file); + uploadIds.push(uploadId); } + console.log (" Getting the uploaded file ID(s)...\n") + const files = await Promise.all( + uploadIds.map(uploadId => + getUploadedFile( + config.egressBaseUrl, + token, + shared.workspace.id, + uploadId, + { + timeoutMs: Math.max(30000, testOptions.fileSizeMb * 15000), + retryDelay: Math.min(10000,Math.max(2000, testOptions.fileSizeMb * 5)), + }, + ) + ) + ); + // Refresh the tactical + AD session per test and wait for the search // radios to be enabled before handing control to the spec. This mirrors // the manual flow and avoids HTTP 400 on /api/v1/case-search when @@ -119,6 +137,7 @@ export const test = base.extend< caseUrn: shared.caseUrn, files, uploadSubfolder, + caseId: shared.caseId, }); // Per-test teardown. On failure we leave the uploaded files in the @@ -134,7 +153,7 @@ export const test = base.extend< netAppFolder: REGISTER_CASE_NETAPP_FOLDER, caseId: shared.caseId, testInfo, - egressToken: token, + egressToken: token }); }, { timeout: 300_000 }], }); diff --git a/e2e/pw/helpers/egress-api.ts b/e2e/pw/helpers/egress-api.ts index 006ac7e63..6a7691772 100644 --- a/e2e/pw/helpers/egress-api.ts +++ b/e2e/pw/helpers/egress-api.ts @@ -234,7 +234,7 @@ export async function uploadFile( fileName: string, folderPath: string = "4. Served Evidence/", chunkSizeMB: number = 5 -): Promise { +): Promise { // Step 1: Initiate upload const initiateResponse = await fetch( `${baseUrl}/api/v1/workspaces/${workspaceId}/uploads`, @@ -335,14 +335,60 @@ export async function uploadFile( ); } - // Egress returns the file record on completion. Fall back to uploadId if - // the response shape changes so callers that need an id for teardown - // always get something to work with. - const completeData = await completeResponse.json().catch(() => ({})); - const fileId: string = completeData?.id ?? uploadId; + console.log(` Upload complete: ${uploadId}`); + return uploadId; +} + +export async function getUploadedFile( + baseUrl: string, + token: string, + workspaceId: string, + uploadId: string, + { + timeoutMs = 60000, + retryDelay = 2000, + }: { + timeoutMs?: number, + retryDelay?: number, + } = {} +): Promise{ + const start = Date.now(); + let i = 1 + + while (Date.now() - start < timeoutMs) { + const response = await fetch( + `${baseUrl}/api/v1/workspaces/${workspaceId}/uploads/${uploadId}?view=full`, + { + headers: { + Authorization: `Basic ${token}`, + }, + } + ); + + if (!response.ok) { + throw new Error( + ` Failed to get upload status (${response.status})` + ); + } + + const status = await response.json(); + + if (status.file_id) { + console.log(` Upload complete. File ID found: ${status.file_id}`) + return { + fileId: status.file_id, + fileName: status.file_name, + fileSize: status.file_size, + parentFolderId: status.parent_folder_id + }; + } + + await new Promise(r => setTimeout(r, retryDelay)); + } - console.log(` Upload complete: ${fileId}`); - return { id: fileId, fileName, fileSize: fileSizeBytes }; + throw new Error( + ` Timed out waiting for upload ${uploadId}` + ); } /** @@ -425,6 +471,37 @@ export async function listEgressWorkspaceFilesByFolderId( return []; } +/** + * Checks whether a specific file still exists in a workspace by its file id. + * GET /api/v1/workspaces/{workspaceId}/files/{fileId}: 200 => exists, + * 404 => gone. (Same endpoint the backend uses to open a document stream — + * see EgressRequestFactory.GetWorkspaceDocumentRequest.) + * + * Unlike listing a folder, this is a single deterministic call keyed on the + * exact id, so it needs no retry/settle — ideal for asserting a Move removed + * its source. It's fast when the file is gone (immediate 404 instead of the + * folder-listing retries) and it won't mask a genuine delete miss the way a + * retrying listing would. + */ +export async function egressFileExistsById( + baseUrl: string, + token: string, + workspaceId: string, + fileId: string +): Promise { + const response = await fetch( + `${baseUrl}/api/v1/workspaces/${workspaceId}/files/${fileId}`, + { headers: { Authorization: `Basic ${token}` } } + ); + if (response.status === 404) return false; + if (response.ok) return true; + + const text = await response.text(); + throw new Error( + `Egress file lookup failed (${response.status}) for '${fileId}': ${text.slice(0, 200)}` + ); +} + /** * Best-effort bulk file delete. Logs and swallows errors so teardown never * fails a passing test — the dated subfolder + manual sweep acts as a @@ -493,4 +570,3 @@ export async function deleteWorkspace( ); } } - diff --git a/e2e/pw/helpers/transfer-verify.ts b/e2e/pw/helpers/transfer-verify.ts new file mode 100644 index 000000000..a724399d9 --- /dev/null +++ b/e2e/pw/helpers/transfer-verify.ts @@ -0,0 +1,133 @@ +import { loadEnvConfig } from "../helpers/env-config"; +import { + listEgressWorkspaceFilesByFolderId, + egressFileExistsById, + authenticateEgress, +} from "./egress-api"; +import { getAzureADToken } from "./auth-api"; +import { expect } from "@playwright/test"; + +const config =loadEnvConfig() + +export async function verifyNetAppFileSizeByName( + filePath: string, + caseId: number, + expectedSizeBytes: number, + netAppOperationName: string = config.netAppOperationName, + accessToken?: string | undefined, +): Promise { + if (!accessToken) { + accessToken = await getAzureADToken( + config.tenantId, + config.lccApiClientId, + config.e2eAdUser, + config.e2eAdPassword, + ); + } + + const response = await fetch( + `${config.lccApiBaseUrl}/api/v1/netapp/search?case-id=${caseId}&query=${encodeURIComponent(filePath)}`, + { + method: "GET", + headers: { + Authorization: `Bearer ${accessToken}`, + } + } + ); + + if (!response.ok) { + const text = await response.text(); + throw new Error( + `NetApp search failed (${response.status}) for '${filePath}': ${text.slice(0, 200)}` + ); + } + + const data = (await response.json()).data; + let file: { + key: string; + type: "File" | "Folder"; + size: number; + lastModified: string; + }; + + switch (data.length) { + case 0: + throw new Error(`No match found for '${filePath}'`); + case 1: + file = data[0]; + break; + default: + throw new Error(`Search response must not match more than a single file.`); + } + + const folderPrefix = netAppOperationName.endsWith("/") + ? netAppOperationName + : `${netAppOperationName}/`; + + const fullPath = `${folderPrefix}${filePath}`; + + if (file.key !== fullPath) { + throw new Error( + `The file path returned does not match '${fullPath}'.\n` + + `Returned: '${file.key}'.` + ); + } + + expect( + file.size, + `NetApp file '${filePath}' has unexpected size` + ).toBe(expectedSizeBytes); +} + +export async function isFileInEgress( + workspaceId: string, + folderId: string, + fileName: string, + egressToken?: string | undefined, +): Promise { + if (!egressToken) { + egressToken = await authenticateEgress( + config.egressBaseUrl, + config.egressServiceAccountAuth, + ) + } + + const files = await listEgressWorkspaceFilesByFolderId( + config.egressBaseUrl, + egressToken, + workspaceId, + folderId, + true, + ); + + return files.some(f => f.fileName === fileName); +} + +/** + * Whether a file still exists in Egress, checked by its exact file id rather + * than by listing a folder and matching names. Prefer this for "the Move + * removed its source" assertions: it's a single deterministic call (no + * folder-listing retries), so it returns immediately when the file is gone and + * doesn't mask a genuine delete miss. Only usable where the file id is known + * (e.g. the source file captured at upload) — the copy-destination existence + * check has no id and must still list by name. + */ +export async function isFileInEgressById( + workspaceId: string, + fileId: string, + egressToken?: string | undefined, +): Promise { + if (!egressToken) { + egressToken = await authenticateEgress( + config.egressBaseUrl, + config.egressServiceAccountAuth, + ); + } + + return egressFileExistsById( + config.egressBaseUrl, + egressToken, + workspaceId, + fileId, + ); +} diff --git a/e2e/pw/helpers/types.ts b/e2e/pw/helpers/types.ts index 5e431f024..976a16051 100644 --- a/e2e/pw/helpers/types.ts +++ b/e2e/pw/helpers/types.ts @@ -7,9 +7,10 @@ export interface UploadedFile { // Egress file id from the completed-upload response. Used by teardown // (deleteFile) to remove the file after the test passes. Optional because // older callers may not capture it. - id?: string; + fileId: string; fileName: string; fileSize: number; + parentFolderId: string; } export interface TestSetupResult { @@ -26,12 +27,20 @@ export interface TestSetupResult { // "4. Served Evidence//", NetApp->Egress copies land in // "2. Counsel only//". Undefined for register-case mode. uploadSubfolder?: string; + // Egress folder path for "4. Counsel only//". + // Used by the Move (large file) test for clear logging. + uploadPath?: string; + // Egress folder id for "4. Counsel only//", captured at + // createFolder time. Used by the Move (large file) test to verify files were + // deleted from the source folder after transfer. + sourceSubfolderId?: string; // Egress folder id for "2. Counsel only//", captured at // createFolder time. Used by per-test teardown to list and delete files // the LCC backend wrote there during NetApp->Egress copy specs. Undefined // when the folder already existed at setup time (rare timestamp // collision); teardown skips destination cleanup in that case. destinationSubfolderId?: string; + egressToken?: string; } export interface AuthTokens { diff --git a/e2e/pw/pages/TransferMaterialsTabV1.ts b/e2e/pw/pages/TransferMaterialsTabV1.ts index 860b8e7c4..4d6c4b9b3 100644 --- a/e2e/pw/pages/TransferMaterialsTabV1.ts +++ b/e2e/pw/pages/TransferMaterialsTabV1.ts @@ -1,6 +1,5 @@ import { expect, Locator } from "@playwright/test"; import { TransferMaterialsTabApi } from "./TransferMaterialsTabApi"; -import { TransferDestinationPage } from "./TransferDestinationPage"; import { BaseTransferMaterialsTab } from "./BaseTransferMaterialsTab"; // The new screen navigates to one of these routes on a failed or @@ -11,11 +10,11 @@ const TRANSFER_ERROR_ROUTE = /** * New-screen (v1) Transfer Materials page object, selected by * `getTransferMaterialsTab` when `TRANSFER_MATERIALS_V1` is on. Differs from the - * old screen: NetApp table renamed "shared drive"; Copy/Move are - * `Copy selected` / `Move selected` buttons; direction toggles via a - * `View Shared Drive` / `View Egress` link; no confirm modal (Copy/Move navigate - * to a destination-tree page, driven by `TransferDestinationPage`); errors use - * the routes above. Egress-side helpers come from `BaseTransferMaterialsTab`. + * old screen: NetApp table renamed "shared drive"; direction toggles via a + * `View Shared Drive` / `View Egress` link; errors use the routes above. + * Copy/Move is driven from the destination panel's inline inset button + * (` to `) and confirmed via the "Transfer confirmation alert + * modal" ("Continue"). Egress-side helpers come from `BaseTransferMaterialsTab`. */ export class TransferMaterialsTabV1 extends BaseTransferMaterialsTab @@ -30,11 +29,20 @@ export class TransferMaterialsTabV1 .first(); } - /** Click the shared Copy/Move control. Renders in a top and bottom bar (target - * the first); Move only appears when the source is Egress. */ - private async clickTransferControl(action: "Copy" | "Move"): Promise { + /** Click the inline Copy/Move control in the destination panel's inset. Each + * panel carries its own inset: the shared-drive (netapp) inset drives + * Egress -> Shared Drive, the egress inset drives Shared Drive -> Egress. The + * button's accessible name is ` to ` (e.g. + * "Move to Automation-Testing"), so match on the ` to ` prefix. */ + private async clickTransferControl( + action: "Copy" | "Move", + direction: "egressToNetApp" | "netAppToEgress", + ): Promise { + const inset = + direction === "egressToNetApp" ? "netapp-inset-text" : "egress-inset-text"; await this.page - .getByRole("button", { name: `${action} selected` }) + .getByTestId(inset) + .getByRole("button", { name: new RegExp(`^${action} to `) }) .first() .click(); } @@ -116,24 +124,29 @@ export class TransferMaterialsTabV1 async selectAction( action: "Copy" | "Move", - _direction?: "egressToNetApp" | "netAppToEgress", + direction: "egressToNetApp" | "netAppToEgress" = "egressToNetApp", ): Promise { - // New screen: one shared control for both directions (Move renders only when - // the source is Egress), so the direction is implied by the current source. - await this.clickTransferControl(action); + // The transfer target is the folder currently shown in the destination + // panel (its inset reads " to "); the direction selects + // which panel's inset holds the control. + await this.clickTransferControl(action, direction); } /** - * Confirm the transfer. No modal on the new screen: Copy/Move already - * navigated to the destination tree — pick the first selectable folder (the - * connected root) and click the ` to ` button. `action` must - * match the Copy/Move just initiated (the button label depends on it). + * Confirm the transfer. Clicking Copy/Move opens a "Transfer confirmation + * alert modal" (dialog, named via aria-labelledby) — there is no + * destination-tree page. "Continue" stays disabled until the "I want to + * N file(s) to " acknowledgement checkbox is ticked + * (present on every transfer, not just duplicates), so check it first, then + * Continue. */ - async confirmTransfer(action: "Copy" | "Move"): Promise { - const destination = new TransferDestinationPage(this.page); - await destination.waitForLoaded(); - await destination.selectFirstSelectableFolder(); - await destination.confirm(action); + async confirmTransfer(_action: "Copy" | "Move"): Promise { + const modal = this.page.getByRole("dialog", { + name: "Transfer confirmation alert modal", + }); + await modal.waitFor({ state: "visible", timeout: 30_000 }); + await modal.getByRole("checkbox").check(); + await modal.getByRole("button", { name: "Continue" }).click(); } /** diff --git a/e2e/pw/playwright.config.ts b/e2e/pw/playwright.config.ts index bbcce38b0..eeca0e7e5 100644 --- a/e2e/pw/playwright.config.ts +++ b/e2e/pw/playwright.config.ts @@ -67,6 +67,7 @@ export default defineConfig({ { name: "default-mode-tests", testMatch: "**/*-default.spec.ts", + testIgnore: "egress-to-netapp-move-large-default.spec.ts", use: { ...devices["Desktop Chrome"] }, }, diff --git a/e2e/pw/scripts/upload-to-workspace.ts b/e2e/pw/scripts/upload-to-workspace.ts index 03dd59ba2..7135007da 100644 --- a/e2e/pw/scripts/upload-to-workspace.ts +++ b/e2e/pw/scripts/upload-to-workspace.ts @@ -67,7 +67,7 @@ async function main() { .replace(/[:.]/g, "-") .slice(0, 19); const fileName = `generated-${fileSizeMb}MB-${timestamp}-file${i}.txt`; - const file = await uploadFile( + const uploadId = await uploadFile( config.egressBaseUrl, token, workspaceId, @@ -75,7 +75,7 @@ async function main() { fileName, uploadPath ); - uploaded.push({ id: file.id, fileName: file.fileName }); + uploaded.push({ id: uploadId, fileName }); } console.log("\n=== Done ==="); diff --git a/e2e/pw/tests/egress-to-netapp-copy-default.spec.ts b/e2e/pw/tests/egress-to-netapp-copy-default.spec.ts index 32d5981e0..512cf036a 100644 --- a/e2e/pw/tests/egress-to-netapp-copy-default.spec.ts +++ b/e2e/pw/tests/egress-to-netapp-copy-default.spec.ts @@ -4,6 +4,7 @@ import { SearchResultsPage } from "../pages/SearchResultsPage"; import { CaseManagementPage } from "../pages/CaseManagementPage"; import { getTransferMaterialsTab } from "../pages/getTransferMaterialsTab"; import { ActivityLogTab } from "../pages/ActivityLogTab"; +import { verifyNetAppFileSizeByName } from "../helpers/transfer-verify"; test.describe("Egress to NetApp Copy (Default Mode)", () => { test("should copy files from Egress to NetApp using existing case", async ({ @@ -71,5 +72,15 @@ test.describe("Egress to NetApp Copy (Default Mode)", () => { await activityLog.expandFileList(); await activityLog.downloadCsv(); await activityLog.verifyDownloadSuccess(); + + // Step 9: Confirm complete files exist in shared drive + for (const file of testData.files) { + console.log(`\nVerifying file '${file.fileName}' exists in NetApp in its original size (${file.fileSize} bytes)`) + await verifyNetAppFileSizeByName( + file.fileName, + testData.caseId!, + file.fileSize + ); + } }); }); diff --git a/e2e/pw/tests/egress-to-netapp-copy-large-default.spec.ts b/e2e/pw/tests/egress-to-netapp-copy-large-default.spec.ts index fb75adb9e..911d4fcf4 100644 --- a/e2e/pw/tests/egress-to-netapp-copy-large-default.spec.ts +++ b/e2e/pw/tests/egress-to-netapp-copy-large-default.spec.ts @@ -5,6 +5,7 @@ import { SearchResultsPage } from "../pages/SearchResultsPage"; import { CaseManagementPage } from "../pages/CaseManagementPage"; import { getTransferMaterialsTab } from "../pages/getTransferMaterialsTab"; import { ActivityLogTab } from "../pages/ActivityLogTab"; +import { verifyNetAppFileSizeByName } from "../helpers/transfer-verify"; const fileSize = loadEnvConfig().largeTestFileSizeMb; @@ -69,5 +70,15 @@ test.describe(`Egress to NetApp Copy - Large File ${fileSize}MB (Default Mode)`, await activityLog.expandFileList(); await activityLog.downloadCsv(); await activityLog.verifyDownloadSuccess(); + + // Step 9: Confirm complete files exist in shared drive + for (const file of testData.files) { + console.log(`\nVerifying file '${file.fileName}' exists in NetApp in its original size (${file.fileSize} bytes)`) + await verifyNetAppFileSizeByName( + file.fileName, + testData.caseId!, + file.fileSize, + ); + } }); }); diff --git a/e2e/pw/tests/egress-to-netapp-copy-large.spec.ts b/e2e/pw/tests/egress-to-netapp-copy-large.spec.ts index ce04dc7ec..6b3c84602 100644 --- a/e2e/pw/tests/egress-to-netapp-copy-large.spec.ts +++ b/e2e/pw/tests/egress-to-netapp-copy-large.spec.ts @@ -4,6 +4,7 @@ import { SearchResultsPage } from "../pages/SearchResultsPage"; import { CaseManagementPage } from "../pages/CaseManagementPage"; import { getTransferMaterialsTab } from "../pages/getTransferMaterialsTab"; import { ActivityLogTab } from "../pages/ActivityLogTab"; +import { verifyNetAppFileSizeByName } from "../helpers/transfer-verify"; test.describe("Egress to NetApp Copy - Large File (200MB)", () => { test.use({ testOptions: { fileSizeMb: 200, fileCount: 1 } }); @@ -62,5 +63,16 @@ test.describe("Egress to NetApp Copy - Large File (200MB)", () => { await activityLog.expandFileList(); await activityLog.downloadCsv(); await activityLog.verifyDownloadSuccess(); + + // Step 6: Confirm complete files exist in shared drive + for (const file of testData.files) { + console.log(`\nVerifying file '${file.fileName}' exists in NetApp in its original size (${file.fileSize} bytes)`) + await verifyNetAppFileSizeByName( + file.fileName, + testData.caseId!, + file.fileSize, + "Automation-Testing", + ); + } }); }); diff --git a/e2e/pw/tests/egress-to-netapp-copy-multifile.spec.ts b/e2e/pw/tests/egress-to-netapp-copy-multifile.spec.ts index 23ce9aec5..0e18407ea 100644 --- a/e2e/pw/tests/egress-to-netapp-copy-multifile.spec.ts +++ b/e2e/pw/tests/egress-to-netapp-copy-multifile.spec.ts @@ -4,6 +4,7 @@ import { SearchResultsPage } from "../pages/SearchResultsPage"; import { CaseManagementPage } from "../pages/CaseManagementPage"; import { getTransferMaterialsTab } from "../pages/getTransferMaterialsTab"; import { ActivityLogTab } from "../pages/ActivityLogTab"; +import { verifyNetAppFileSizeByName } from "../helpers/transfer-verify"; test.describe("Egress to NetApp Copy - Multiple Files (10MB x 3)", () => { test.use({ testOptions: { fileSizeMb: 10, fileCount: 3 } }); @@ -67,5 +68,16 @@ test.describe("Egress to NetApp Copy - Multiple Files (10MB x 3)", () => { await activityLog.expandFileList(); await activityLog.downloadCsv(); await activityLog.verifyDownloadSuccess(); + + // Step 6: Confirm complete files exist in shared drive + for (const file of testData.files) { + console.log(`\nVerifying file '${file.fileName}' exists in NetApp in its original size (${file.fileSize} bytes)`) + await verifyNetAppFileSizeByName( + file.fileName, + testData.caseId!, + file.fileSize, + "Automation-Testing", + ); + } }); }); diff --git a/e2e/pw/tests/egress-to-netapp-copy.spec.ts b/e2e/pw/tests/egress-to-netapp-copy.spec.ts index c98ed9014..90bc8d0e8 100644 --- a/e2e/pw/tests/egress-to-netapp-copy.spec.ts +++ b/e2e/pw/tests/egress-to-netapp-copy.spec.ts @@ -4,6 +4,7 @@ import { SearchResultsPage } from "../pages/SearchResultsPage"; import { CaseManagementPage } from "../pages/CaseManagementPage"; import { getTransferMaterialsTab } from "../pages/getTransferMaterialsTab"; import { ActivityLogTab } from "../pages/ActivityLogTab"; +import { verifyNetAppFileSizeByName } from "../helpers/transfer-verify"; test.describe("Egress to NetApp Copy", () => { test.use({ testOptions: { fileSizeMb: 100, fileCount: 1 } }); @@ -54,5 +55,16 @@ test.describe("Egress to NetApp Copy", () => { await activityLog.expandFileList(); await activityLog.downloadCsv(); await activityLog.verifyDownloadSuccess(); + + // Step 6: Confirm complete files exist in shared drive + for (const file of testData.files) { + console.log(`\nVerifying file '${file.fileName}' exists in NetApp in its original size (${file.fileSize} bytes)`) + await verifyNetAppFileSizeByName( + file.fileName, + testData.caseId!, + file.fileSize, + "Automation-Testing", + ); + } }); }); diff --git a/e2e/pw/tests/egress-to-netapp-move-default.spec.ts b/e2e/pw/tests/egress-to-netapp-move-default.spec.ts index 4cea05cca..06e5dd2d3 100644 --- a/e2e/pw/tests/egress-to-netapp-move-default.spec.ts +++ b/e2e/pw/tests/egress-to-netapp-move-default.spec.ts @@ -4,6 +4,9 @@ import { SearchResultsPage } from "../pages/SearchResultsPage"; import { CaseManagementPage } from "../pages/CaseManagementPage"; import { getTransferMaterialsTab } from "../pages/getTransferMaterialsTab"; import { ActivityLogTab } from "../pages/ActivityLogTab"; +import { verifyNetAppFileSizeByName, isFileInEgressById } from "../helpers/transfer-verify"; +import { expect } from "@playwright/test"; + test.describe("Egress to NetApp Move (Default Mode)", () => { test("should move files from Egress to NetApp using existing case", async ({ @@ -72,5 +75,35 @@ test.describe("Egress to NetApp Move (Default Mode)", () => { await activityLog.expandFileList(); await activityLog.downloadCsv(); await activityLog.verifyDownloadSuccess(); + + // Step 9: Confirm complete files exist in shared drive + for (const file of testData.files) { + console.log(`\nVerifying file '${file.fileName}' exists in NetApp in its original size (${file.fileSize} bytes)`) + await verifyNetAppFileSizeByName( + file.fileName, + testData.caseId!, + file.fileSize, + ); + } + + // Step 10: Confirm files removed from Egress + for (const file of testData.files) { + console.log(`\nVerifying file '${file.fileName}' has been deleted from source '${testData.uploadPath}'.`) + await test.step( + `Verify file '${file.fileName}' is no longer present in Egress`, + async () => { + const exists = await isFileInEgressById( + testData.workspace.id, + file.fileId, + testData.egressToken, + ); + + expect( + exists, + `File '${file.fileName}' still exists in Egress` + ).toBeFalsy(); + } + ); + } }); }); diff --git a/e2e/pw/tests/egress-to-netapp-move-large-default.spec.ts b/e2e/pw/tests/egress-to-netapp-move-large-default.spec.ts new file mode 100644 index 000000000..c0719bbd7 --- /dev/null +++ b/e2e/pw/tests/egress-to-netapp-move-large-default.spec.ts @@ -0,0 +1,108 @@ +import { test } from "../fixtures/test-fixtures-default"; +import { CaseSearchPage } from "../pages/CaseSearchPage"; +import { SearchResultsPage } from "../pages/SearchResultsPage"; +import { CaseManagementPage } from "../pages/CaseManagementPage"; +import { TransferMaterialsTab } from "../pages/TransferMaterialsTab"; +import { ActivityLogTab } from "../pages/ActivityLogTab"; +import { verifyNetAppFileSizeByName, isFileInEgressById } from "../helpers/transfer-verify"; +import { expect } from "@playwright/test"; + +test.describe("Egress to NetApp Move (Default Mode)", () => { + test("should move files from Egress to NetApp using existing case", async ({ + page, + testData, + }) => { + test.setTimeout(900_000); + const { caseUrn, uploadSubfolder } = testData; + + // Step 1: Search for case by URN + const caseSearch = new CaseSearchPage(page); + await caseSearch.searchByUrn(caseUrn); + + // Step 2: Click View on already-connected case + const searchResults = new SearchResultsPage(page); + await searchResults.waitForResults(); + await searchResults.clickCaseAction(caseUrn); + + // Step 3: Navigate to Transfer Materials tab + const caseMgmt = new CaseManagementPage(page); + await caseMgmt.waitForLoad(); + await caseMgmt.switchToTab("transfer-materials"); + + // Step 4: Select files from Egress panel and initiate Move + const transferTab = new TransferMaterialsTab(page); + await transferTab.waitForEgressFiles(); + await transferTab.navigateToFolder("4. Served Evidence"); + await transferTab.waitForEgressFiles(); + if (uploadSubfolder) { + await transferTab.navigateToFolder(uploadSubfolder); + await transferTab.waitForEgressFiles(); + } + + // Wait for the just-uploaded file to be indexed before selecting. + // Egress doesn't auto-refresh the file list, so the helper reloads + + // re-navigates on each retry. + const sourceFolderPath = uploadSubfolder + ? ["4. Served Evidence", uploadSubfolder] + : ["4. Served Evidence"]; + await transferTab.waitForEgressFileByName( + testData.files[testData.files.length - 1].fileName, + sourceFolderPath + ); + + // Select by name (not index) so we don't pick a stranger's old file + // that happened to land at row 0. + for (const file of testData.files) { + await transferTab.selectEgressFileByName(file.fileName); + } + + await transferTab.selectAction("Move"); + + // Step 5: Confirm transfer + await transferTab.confirmTransfer("Move"); + + // Step 6: Wait for transfer to complete (10 min timeout) + await transferTab.waitForTransferComplete(600_000); + + // Step 7: Verify in Activity Log + await caseMgmt.switchToTab("activity-log"); + const activityLog = new ActivityLogTab(page); + await activityLog.waitForLogs(); + await activityLog.verifyTransferLogged("Move", uploadSubfolder!); + + // Step 8: Download CSV and verify + await activityLog.expandFileList(); + await activityLog.downloadCsv(); + await activityLog.verifyDownloadSuccess(); + + // Step 9: Confirm complete files exist in shared drive + for (const file of testData.files) { + console.log(`\nVerifying file '${file.fileName}' exists in NetApp in its original size (${file.fileSize} bytes)`) + await verifyNetAppFileSizeByName( + file.fileName, + testData.caseId!, + file.fileSize, + ); + } + + // Step 10: Confirm files removed from Egress + for (const file of testData.files) { + console.log(`\nVerifying file '${file.fileName}' has been deleted from source '${testData.uploadPath}'.`) + await test.step( + `Verify file '${file.fileName}' is no longer present in Egress`, + async () => { + const exists = await isFileInEgressById( + testData.workspace.id, + file.fileId, + testData.egressToken, + ); + + expect( + exists, + `File '${file.fileName}' still exists in Egress` + ).toBeFalsy(); + } + ); + } + }); +}); diff --git a/e2e/pw/tests/egress-to-netapp-move.spec.ts b/e2e/pw/tests/egress-to-netapp-move.spec.ts index 95302b623..707433fc6 100644 --- a/e2e/pw/tests/egress-to-netapp-move.spec.ts +++ b/e2e/pw/tests/egress-to-netapp-move.spec.ts @@ -4,6 +4,8 @@ import { SearchResultsPage } from "../pages/SearchResultsPage"; import { CaseManagementPage } from "../pages/CaseManagementPage"; import { getTransferMaterialsTab } from "../pages/getTransferMaterialsTab"; import { ActivityLogTab } from "../pages/ActivityLogTab"; +import { verifyNetAppFileSizeByName, isFileInEgressById } from "../helpers/transfer-verify"; +import { expect } from "@playwright/test"; test.describe("Egress to NetApp Move", () => { test.use({ testOptions: { fileSizeMb: 100, fileCount: 1 } }); @@ -57,5 +59,36 @@ test.describe("Egress to NetApp Move", () => { await activityLog.expandFileList(); await activityLog.downloadCsv(); await activityLog.verifyDownloadSuccess(); + + // Step 6: Confirm complete files exist in shared drive + for (const file of testData.files) { + console.log(`\nVerifying file '${file.fileName}' exists in NetApp in its original size (${file.fileSize} bytes)`) + await verifyNetAppFileSizeByName( + file.fileName, + testData.caseId!, + file.fileSize, + "Automation-Testing", + ); + } + + // Step 7: Confirm files removed from Egress + for (const file of testData.files) { + console.log(`\nVerifying file '${file.fileName}' has been deleted from source '${testData.uploadPath}'.`) + await test.step( + `Verify file '${file.fileName}' is no longer present in Egress`, + async () => { + const exists = await isFileInEgressById( + testData.workspace.id, + file.fileId, + testData.egressToken, + ); + + expect( + exists, + `File '${file.fileName}' still exists in Egress` + ).toBeFalsy(); + } + ); + } }); }); diff --git a/e2e/pw/tests/netapp-to-egress-copy-default.spec.ts b/e2e/pw/tests/netapp-to-egress-copy-default.spec.ts index 033dd21a7..19f0246b1 100644 --- a/e2e/pw/tests/netapp-to-egress-copy-default.spec.ts +++ b/e2e/pw/tests/netapp-to-egress-copy-default.spec.ts @@ -7,6 +7,9 @@ import { TransferDestinationPage } from "../pages/TransferDestinationPage"; import { ActivityLogTab } from "../pages/ActivityLogTab"; import { loadEnvConfig } from "../helpers/env-config"; import { NETAPP_FIXTURE_FILENAME } from "../helpers/constants"; +import { isFileInEgress } from "../helpers/transfer-verify"; +import { expect } from "@playwright/test"; + test.describe("NetApp to Egress Copy (Default Mode)", () => { test("should copy files from NetApp to Egress using existing case", async ({ @@ -71,5 +74,25 @@ test.describe("NetApp to Egress Copy (Default Mode)", () => { await activityLog.expandFileList(); await activityLog.downloadCsv(); await activityLog.verifyDownloadSuccess(); + + console.log(`Subfolder ID: ${testData.destinationSubfolderId}`) + + // Confirm file exists in Egress + console.log(`\nVerifying file '${NETAPP_FIXTURE_FILENAME}' exists in destination '2. Counsel only/${testData.uploadSubfolder}'.`) + await test.step( + `Verify file '${NETAPP_FIXTURE_FILENAME}' is present in Egress`, + async () => { + const exists = await isFileInEgress( + testData.workspace.id, + testData.destinationSubfolderId!, + NETAPP_FIXTURE_FILENAME, + ); + + expect( + exists, + `File '${NETAPP_FIXTURE_FILENAME}' could not be found in destination.` + ).toBeTruthy(); + } + ); }); }); diff --git a/e2e/pw/tests/netapp-to-egress-copy.spec.ts b/e2e/pw/tests/netapp-to-egress-copy.spec.ts index eb20ecb23..4ca14bb24 100644 --- a/e2e/pw/tests/netapp-to-egress-copy.spec.ts +++ b/e2e/pw/tests/netapp-to-egress-copy.spec.ts @@ -6,6 +6,8 @@ import { CaseManagementPage } from "../pages/CaseManagementPage"; import { getTransferMaterialsTab } from "../pages/getTransferMaterialsTab"; import { TransferDestinationPage } from "../pages/TransferDestinationPage"; import { ActivityLogTab } from "../pages/ActivityLogTab"; +import { isFileInEgress } from "../helpers/transfer-verify"; +import { expect } from "@playwright/test"; test.describe("NetApp to Egress Copy", () => { test.use({ testOptions: { fileSizeMb: 100, fileCount: 1 } }); @@ -80,5 +82,7 @@ test.describe("NetApp to Egress Copy", () => { await activityLog.expandFileList(); await activityLog.downloadCsv(); await activityLog.verifyDownloadSuccess(); + + // No further verification added as currently we're not using a predictable file name }); }); diff --git a/e2e/pw/tests/seed-netapp-fixture.setup.ts b/e2e/pw/tests/seed-netapp-fixture.setup.ts index a8719dc7c..e7597b84e 100644 --- a/e2e/pw/tests/seed-netapp-fixture.setup.ts +++ b/e2e/pw/tests/seed-netapp-fixture.setup.ts @@ -58,7 +58,7 @@ setup("seed lcc-e2e-fixture-source.txt to NetApp", async ({ page }) => { SEED_PARENT, SEED_SUBFOLDER, ); - const uploaded = await uploadFile( + const uploadId = await uploadFile( config.egressBaseUrl, egressToken, config.defaultWorkspaceId, @@ -137,12 +137,12 @@ setup("seed lcc-e2e-fixture-source.txt to NetApp", async ({ page }) => { await transferTab.selectNetAppFileByExactName(NETAPP_FIXTURE_FILENAME); console.log("Deleting Egress-side seed source..."); - if (uploaded.id) { + if (uploadId) { await deleteFiles( config.egressBaseUrl, egressToken, config.defaultWorkspaceId, - [uploaded.id], + [uploadId], ); }