diff --git a/explorer/e2e/anvil/anvil-main-export-button.spec.ts b/explorer/e2e/anvil/anvil-main-export-button.spec.ts new file mode 100644 index 000000000..e89868694 --- /dev/null +++ b/explorer/e2e/anvil/anvil-main-export-button.spec.ts @@ -0,0 +1,18 @@ +import test from "@playwright/test"; +import { + testIndexExportDetails, + testIndexExportWorkflow, +} from "../testFunctions"; +import { ANVIL_TABS } from "./anvil-tabs"; + +test("Check that the export button on the index page functions as expected, on the Files tab", async ({ + page, +}) => { + await testIndexExportWorkflow(page, ANVIL_TABS.FILES); +}); + +test("Check that figures in the details tab on the index export page matches figures on the index page, on the BioSamples tab", async ({ + page, +}) => { + await testIndexExportDetails(page, ANVIL_TABS.BIOSAMPLES); +}); diff --git a/explorer/e2e/anvil/anvil-tabs.ts b/explorer/e2e/anvil/anvil-tabs.ts index 997d53d8e..595111f5a 100644 --- a/explorer/e2e/anvil/anvil-tabs.ts +++ b/explorer/e2e/anvil/anvil-tabs.ts @@ -1,6 +1,7 @@ /* eslint-disable sonarjs/no-duplicate-string -- ignoring duplicate strings here */ import { AnvilCMGTabCollection, + IndexExportButtons, TabCollectionKeys, TabDescription, } from "../testInterfaces"; @@ -45,9 +46,23 @@ export const REPORTED_ETHNICITY_INDEX = 10; const ANVIL_CMG_SEARCH_FILTERS_PLACEHOLDER_TEXT = "Search all filters..."; +export const anvilIndexExportButtons: IndexExportButtons = { + detailsName: "Selected Data Summary", + detailsToCheck: ["BioSamples", "Donors", "Files"], + exportActionButtonText: "Download Manifest", + exportOptionButtonText: "Request File Manifest", + exportRequestButtonText: "Prepare Manifest", + firstLandingMessage: + "Download a File Manifest with Metadata for the Selected Data", + indexExportButtonText: "Export", + secondLandingMessage: "Confirm Organism Type and Manifest File Formats", + secondLoadingMessage: "Your manifest will be ready shortly...", +}; + export const ANVIL_TABS: AnvilCMGTabCollection = { ACTIVITIES: { emptyFirstColumn: false, + indexExportPage: anvilIndexExportButtons, maxPages: 25, preselectedColumns: ANVIL_ACTIVITIES_PRESELECTED_COLUMNS_BY_NAME, searchFiltersPlaceholderText: ANVIL_CMG_SEARCH_FILTERS_PLACEHOLDER_TEXT, @@ -57,6 +72,7 @@ export const ANVIL_TABS: AnvilCMGTabCollection = { }, BIOSAMPLES: { emptyFirstColumn: false, + indexExportPage: anvilIndexExportButtons, maxPages: 25, preselectedColumns: ANVIL_BIOSAMPLES_PRESELECTED_COLUMNS_BY_NAME, searchFiltersPlaceholderText: ANVIL_CMG_SEARCH_FILTERS_PLACEHOLDER_TEXT, @@ -132,6 +148,7 @@ export const ANVIL_TABS: AnvilCMGTabCollection = { }, ], emptyFirstColumn: false, + indexExportPage: anvilIndexExportButtons, maxPages: 25, preselectedColumns: ANVIL_DATASETS_PRESELECTED_COLUMNS_BY_NAME, searchFiltersPlaceholderText: ANVIL_CMG_SEARCH_FILTERS_PLACEHOLDER_TEXT, @@ -141,6 +158,7 @@ export const ANVIL_TABS: AnvilCMGTabCollection = { }, DONORS: { emptyFirstColumn: false, + indexExportPage: anvilIndexExportButtons, maxPages: 25, preselectedColumns: ANVIL_DONORS_PRESELECTED_COLUMNS_BY_NAME, searchFiltersPlaceholderText: ANVIL_CMG_SEARCH_FILTERS_PLACEHOLDER_TEXT, @@ -150,6 +168,7 @@ export const ANVIL_TABS: AnvilCMGTabCollection = { }, FILES: { emptyFirstColumn: true, + indexExportPage: anvilIndexExportButtons, maxPages: 25, preselectedColumns: ANVIL_FILES_PRESELECTED_COLUMNS_BY_NAME, searchFiltersPlaceholderText: ANVIL_CMG_SEARCH_FILTERS_PLACEHOLDER_TEXT, diff --git a/explorer/e2e/testFunctions.ts b/explorer/e2e/testFunctions.ts index 977029c00..9446dd1cd 100644 --- a/explorer/e2e/testFunctions.ts +++ b/explorer/e2e/testFunctions.ts @@ -768,7 +768,9 @@ export async function testExportBackpage( ): Promise { if ( tab.backpageExportButtons === undefined || - tab.backpageAccessTags === undefined + tab.backpageAccessTags === undefined || + tab.backpageExportButtons?.firstLoadingMessage === undefined || + tab.backpageExportButtons?.secondLandingMessage === undefined ) { // Fail if this test is ran on a tab without defined backpages await expect(false); @@ -1052,6 +1054,158 @@ export async function testBackpageDetails( return true; } +type DownloadResult = { + filename: string; + url: string; +}; + +/** + * Attempt to download a file, confirm that it succeeds, and get the filename and file url + * @param page - a Playwright Page object + * @param downloadActionLocator - a locator that initiates the file download when clicked + * @returns - an object containing the url and filename + */ +async function checkDownloadAndReturnLink( + page: Page, + downloadActionLocator: Locator +): Promise { + const downloadPromise = page.waitForEvent("download"); + await downloadActionLocator.click(); + const download = await downloadPromise; + const downloadFilename = download.suggestedFilename(); + const downloadUrl = download.url(); + return { + filename: downloadFilename, + url: downloadUrl, + }; +} + +export async function testIndexExportWorkflow( + page: Page, + tab: TabDescription +): Promise { + if (tab?.indexExportPage === undefined) { + console.log( + "testIndexExportWorkflow Error: indexExportPage not specified for given tab, so test cannot run" + ); + return false; + } + await page.goto(tab.url); + const exportButtonLocator = page.getByRole("link", { + name: tab.indexExportPage.indexExportButtonText, + }); + await expect(exportButtonLocator).toBeVisible(); + await exportButtonLocator.click(); + await expect( + page.getByText(tab.indexExportPage.firstLandingMessage ?? "") + ).toBeVisible(); + await expect( + page.getByRole("link", { + name: tab.indexExportPage.exportOptionButtonText, + }) + ).toBeEnabled(); + await page + .getByRole("link", { name: tab.indexExportPage.exportOptionButtonText }) + .click(); + const exportRequestButtonLocator = page.getByRole("button", { + name: tab.indexExportPage.exportRequestButtonText, + }); + await expect(exportRequestButtonLocator).toBeEnabled(); + // TODO: below is code copied from #4080, refactor this to a separate function to call that instead + // Expect there to be exactly one table on the backpage + await expect(page.getByRole("table")).toHaveCount(1); + const allNonTableCheckboxLocators = await page + .locator("input[type='checkbox']:not(table input[type='checkbox'])") + .all(); + for (const checkboxLocator of allNonTableCheckboxLocators) { + await checkboxLocator.click(); + await expect(checkboxLocator).toBeChecked(); + await expect(checkboxLocator).toBeEnabled({ timeout: 10000 }); + } + // Check the second checkbox in the table (this should be the checkbox after the "select all checkbox") + const tableLocator = page.getByRole("table"); + const allInTableCheckboxLocators = await tableLocator + .getByRole("checkbox") + .all(); + const secondCheckboxInTableLocator = allInTableCheckboxLocators[1]; + await secondCheckboxInTableLocator.click(); + await expect(secondCheckboxInTableLocator).toBeChecked(); + await expect(secondCheckboxInTableLocator).toBeEnabled({ timeout: 10000 }); + // Make sure that no other checkboxes are selected + const otherInTableCheckboxLocators = [ + allInTableCheckboxLocators[0], + ...allInTableCheckboxLocators.slice(2), + ]; + for (const otherCheckboxLocator of otherInTableCheckboxLocators) { + await expect(otherCheckboxLocator).not.toBeChecked(); + await expect(otherCheckboxLocator).toBeEnabled(); + } + // Click the Export Request button + await expect(exportRequestButtonLocator).toBeEnabled({ timeout: 10000 }); + await exportRequestButtonLocator.click(); + if (tab.indexExportPage?.secondLoadingMessage !== undefined) { + await expect( + page.getByText(tab.indexExportPage.secondLoadingMessage, { + exact: true, + }) + ).toBeVisible(); + } + // END copying from #4080 + const exportActionButtonLocator = page.getByRole("link", { + name: tab.indexExportPage?.exportActionButtonText, + }); + await expect(exportActionButtonLocator).toBeEnabled(); + const downloadResult = await checkDownloadAndReturnLink( + page, + exportActionButtonLocator + ); + console.log(downloadResult); + return true; + //TODO: validate the results from the downnload +} + +export async function testIndexExportDetails( + page: Page, + tab: TabDescription +): Promise { + if (tab?.indexExportPage === undefined) { + console.log( + "testIndexExportDetails Error: indexExportPage not specified for given tab, so test cannot run" + ); + return false; + } + await page.goto(tab.url); + //await expect(getFirstRowNthColumnCellLocator(page, 0)).toBeVisible(); + const headers: { header: string; value: string }[] = []; + const indexExportButtonLocator = page.getByRole("link", { + name: tab.indexExportPage.indexExportButtonText, + }); + await expect(indexExportButtonLocator).toBeVisible(); + for (const detail of tab.indexExportPage.detailsToCheck) { + // This Regexp gets a decimal number, some whitespace, then the name of the detail, matching how the detail box appears to Playwright. + const detailBoxRegexp = RegExp(`^([0-9]+\\.[0-9]+k)\\s*${detail}$`); + console.log(await page.getByText(detailBoxRegexp).innerText()); + // This gets the detail's value. The .trim() is necessary since innertext adds extraneous whitespace on Webkit + headers.push({ + header: detail, + value: ((await page.getByText(detailBoxRegexp).innerText()) + .trim() + .match(detailBoxRegexp) ?? ["", "ERROR"])[1], + }); + } + await indexExportButtonLocator.click(); + for (const headerValue of headers) { + // Expect the correct value to be below the correct header in the dataset values table + await expect( + page + .locator(`:below(:text('${headerValue.header}'))`) + .getByText(headerValue.value) + .first() + ).toBeVisible(); + } + return true; +} + const PAGE_COUNT_REGEX = /Page [0-9]+ of [0-9]+/; const BACK_BUTTON_TEST_ID = "WestRoundedIcon"; const FORWARD_BUTTON_TEST_ID = "EastRoundedIcon"; diff --git a/explorer/e2e/testInterfaces.ts b/explorer/e2e/testInterfaces.ts index 6be55ada1..1ebc21e6f 100644 --- a/explorer/e2e/testInterfaces.ts +++ b/explorer/e2e/testInterfaces.ts @@ -7,6 +7,7 @@ export interface TabDescription { backpageExportButtons?: BackpageExportButtons; backpageHeaders?: BackpageHeader[]; emptyFirstColumn: boolean; + indexExportPage?: IndexExportButtons; maxPages?: number; preselectedColumns: StringToColumnDescription; searchFiltersPlaceholderText: string; @@ -49,14 +50,27 @@ export interface BackpageAccessTags { grantedShortName: string; } -export interface BackpageExportButtons { - accessNotGrantedMessage: string; +export interface ExportButtonInfo { detailsName: string; exportActionButtonText: string; exportRequestButtonText: string; + firstLandingMessage?: string; //TODO: rename to requestLoadingMessage + firstLoadingMessage?: string; + secondLandingMessage?: string; //TODO: rename to actionLaodingMessage + secondLoadingMessage?: string; +} + +export interface BackpageExportButtons extends ExportButtonInfo { + accessNotGrantedMessage: string; exportTabName: string; exportUrlRegExp: RegExp; - firstLoadingMessage: string; newTabMessage: string; - secondLandingMessage: string; +} + +//TODO: might need to make it so that there's an interface with indexExportButtonText +// and a list of other objects that go to different export pages for this to work with HCA +export interface IndexExportButtons extends ExportButtonInfo { + detailsToCheck: string[]; + exportOptionButtonText: string; + indexExportButtonText: string; }