diff --git a/.gitignore b/.gitignore index f2af3e318..e04a5d82b 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,4 @@ common/styles/navbar.css /playwright-report/ /playwright/.cache/ test/playwright/.auth +test/playwright/.download diff --git a/test/e2e/utils/record-helpers.js b/test/e2e/utils/record-helpers.js index ffedb11c2..3b494186b 100644 --- a/test/e2e/utils/record-helpers.js +++ b/test/e2e/utils/record-helpers.js @@ -442,11 +442,12 @@ exports.testPresentation = function (tableParams) { /** * opens the share and cite popup and test the content. The acceptable input: * { + * title: "title", // required * permalink: "the permalink", // required * hasVersionedLink: boolean, // whether versioned link is present or not * verifyVersionedLink: boolean, // if true, we will test the versioned link too. * citation: string, // (optional) pass null if citation should not be displayed. - * bintextFile: string, // (optional) the location of the bibtext file so we can delete it after downloading it + * bibtextFile: string, // (optional) the location of the bibtext file so we can delete it after downloading it * } */ exports.testSharePopup = function (sharePopupParams) { diff --git a/test/playwright/locators/modal.ts b/test/playwright/locators/modal.ts index 2ec522cfa..2a87d1d38 100644 --- a/test/playwright/locators/modal.ts +++ b/test/playwright/locators/modal.ts @@ -1,11 +1,76 @@ import { Locator, Page } from '@playwright/test'; export default class ModalLocators { - static getProfileModal(page: Page) : Locator { + + // ------------ modal container getters ------------ // + + static getProfileModal(page: Page): Locator { return page.locator('.profile-popup'); } - static getCloseBtn (modal: Locator) : Locator { + static getShareCiteModal(page: Page): Locator { + return page.locator('.chaise-share-citation-modal'); + } + + // ------------- common modal functions -------------- // + + static getModalTitle(modal: Locator) { + return modal.locator('.modal-title'); + } + + static getModalText(modal: Locator) { + return modal.locator('.modal-body'); + } + + static getCloseBtn(modal: Locator): Locator { return modal.locator('.modal-close'); } + + + // --------- share cite related functions ------------ // + + static async waitForCitation(modal: Locator, timeout?: number): Promise { + await modal.locator('.citation-loader').waitFor({ state: 'detached', timeout }); + } + + static getVersionedLinkElement(modal: Locator): Locator { + return modal.locator('.share-modal-versioned-link'); + } + + static getLiveLinkElement(modal: Locator): Locator { + return modal.locator('.share-modal-live-link'); + } + + static getModalListElements(modal: Locator): Locator { + return ModalLocators.getModalText(modal).locator('li'); + } + + static getShareLinkHeader(modal: Locator): Locator { + return modal.locator('.share-modal-links h2'); + } + + static getShareLinkSubHeaders(modal: Locator): Locator { + return modal.locator('.share-modal-links h3'); + } + + static getShareLinkCopyBtns(modal: Locator): Locator { + return modal.locator('.share-modal-links .chaise-copy-to-clipboard-btn') + } + + static getCitationHeader(modal: Locator): Locator { + return modal.locator('.share-modal-citation h2'); + } + + static getDownloadCitationHeader(modal: Locator): Locator { + return modal.locator('.share-modal-download-citation h3'); + } + + static getCitationText(modal: Locator): Locator { + return modal.locator('.share-modal-citation-text'); + } + + static getBibtex(modal: Locator): Locator { + return modal.locator('.bibtex-download-btn'); + } + } diff --git a/test/playwright/locators/navbar.ts b/test/playwright/locators/navbar.ts index 9295e3d5e..f0fc5122f 100644 --- a/test/playwright/locators/navbar.ts +++ b/test/playwright/locators/navbar.ts @@ -18,36 +18,40 @@ export default class NavbarLocators { return banner.locator('.markdown-container'); } - static getBannerDismissBtn(key: string, page: Page) { + static getBannerDismissBtn(key: string, page: Page): Locator { const banner = NavbarLocators.getBanner(key, page); return banner.locator('.close'); } - static getBrandImage(page: Page) { + static getBrandImage(page: Page): Locator { return page.locator('#brand-image') } - static getBrandText(page: Page) { + static getBrandText(page: Page): Locator { return page.locator('#brand-text') } - static getUsername(page: Page) { + static getUsername(page: Page): Locator { return page.locator('.username-display'); } - static getMenu(page: Page) { + static getMenu(page: Page): Locator { return page.locator('.navbar-menu-options'); } - static getLoginMenu(page: Page) { + static getLoginMenuContainer(page: Page): Locator{ + return NavbarLocators.getContainer(page).locator('.login-menu-options'); + } + + static getLoginMenu(page: Page): Locator { return page.locator('.username-display > div.dropdown-menu'); } - static getProfileLink(page: Page) { + static getProfileLink(page: Page): Locator { return page.locator('#profile-link'); } - static getLogoutLink(page: Page) { + static getLogoutLink(page: Page): Locator { return page.locator('#logout-link'); } } diff --git a/test/playwright/locators/page.ts b/test/playwright/locators/page.ts index 5f3884488..5e59cf7bf 100644 --- a/test/playwright/locators/page.ts +++ b/test/playwright/locators/page.ts @@ -1,9 +1,15 @@ -import { Locator, BrowserContext } from '@playwright/test'; +import { Page, Locator, BrowserContext, expect } from '@playwright/test'; +import { DOWNLOAD_FOLDER } from '@isrd-isi-edu/chaise/test/playwright/setup/playwright.parameters'; export default class PageLocators { /** * click on the given link and return the opened page instance. + * + * Example: + * const newPage = await PageLocators.clickNewTabLink(someButton, context); + * await newPage.waitForURL('someURL'); + * await newPage.close(); */ static async clickNewTabLink(locator: Locator, context: BrowserContext) { const pagePromise = context.waitForEvent('page'); @@ -12,4 +18,30 @@ export default class PageLocators { await newPage.waitForLoadState(); return newPage; } + + /** + * click on the given element and verify that it initiated a download + * @param locator the button that will be clicked + * @param expectedFileName pass undefined if you don't want to test the actual file path and just test that something was downloaded. + * @param page the page object + */ + static async clickAndVerifyDownload(locator: Locator, expectedFileName: string | undefined, page: Page) { + const downloadPromise = page.waitForEvent('download'); + await locator.click(); + const download = await downloadPromise; + const filename = download.suggestedFilename(); + + // Wait for the download process to complete and save the downloaded file somewhere. + await download.saveAs(DOWNLOAD_FOLDER + '/' + filename); + + if (expectedFileName) { + expect.soft(filename).toEqual(expectedFileName); + } + + await download.delete(); + } + + static async getClipboardContent(page: Page) : Promise { + return await page.evaluate('navigator.clipboard.readText()'); + } } diff --git a/test/playwright/locators/record.ts b/test/playwright/locators/record.ts index 05944ec42..5d4d318cb 100644 --- a/test/playwright/locators/record.ts +++ b/test/playwright/locators/record.ts @@ -2,10 +2,11 @@ import { makeSafeIdAttr } from '@isrd-isi-edu/chaise/src/utils/string-utils'; import { Locator, Page } from '@playwright/test'; export default class RecordLocators { - static async waitForRecordPageReady(page: Page) { - await RecordLocators.getEntityTitleElement(page).waitFor({ state: 'visible' }); - await RecordLocators.getMainSectionTable(page).waitFor({ state: 'visible' }); - await RecordLocators.getRelatedSectionSpinner(page).waitFor({ state: 'detached' }) + static async waitForRecordPageReady(page: Page, timeout?: number) { + await RecordLocators.getEntityTitleElement(page).waitFor({ state: 'visible', timeout }); + await RecordLocators.getMainSectionTable(page).waitFor({ state: 'visible', timeout }); + await RecordLocators.getRelatedSectionSpinner(page).waitFor({ state: 'detached', timeout }) + await RecordLocators.getTableOfContentsRelatedSpinner(page).waitFor({ state: 'detached', timeout }) } static getEntityTitleElement(page: Page): Locator { @@ -16,17 +17,60 @@ export default class RecordLocators { return page.locator('.record-main-section-table'); } + static getEntityRelatedTable(page: Page, displayname: string): Locator { + displayname = makeSafeIdAttr(displayname); + return page.locator(`#entity-${displayname}`); + } + static getRelatedSectionSpinner(page: Page): Locator { return page.locator('.related-section-spinner'); } + static getTableOfContentsRelatedSpinner(page: Page): Locator { + return page.locator('#rt-toc-loading'); + } + + static getDisplayedRelatedTableTitles(page: Page): Locator { + return page.locator('.chaise-accordion:not(.forced-hidden) .chaise-accordion-header .chaise-accordion-displayname') + } + static getRelatedTableAccordion(page: Page, displayname: string): Locator { displayname = makeSafeIdAttr(displayname); return page.locator(`#rt-heading-${displayname}`); } + static getRelatedTableHeading(page: Page, displayname: string): Locator { + return RecordLocators.getRelatedTableAccordion(page, displayname).locator('.panel-heading'); + } + + static getRelatedTableSectionHeader(page: Page, displayname: string): Locator { + return RecordLocators.getRelatedTableHeading(page, displayname).locator('.chaise-accordion-header'); + } + + static getRelatedTableSectionHeaderDisplayname(page: Page, displayname: string): Locator { + return RecordLocators.getRelatedTableHeading(page, displayname).locator('.chaise-accordion-header .chaise-accordion-displayname'); + } + + static getRelatedTableInlineComment(page: Page, displayname: string): Locator { + return RecordLocators.getRelatedTableAccordion(page, displayname).locator('.inline-tooltip'); + } + static getRelatedTableAccordionContent(page: Page, displayname: string): Locator { const acc = RecordLocators.getRelatedTableAccordion(page, displayname); return acc.locator('.accordion-collapse'); } + + static getMoreResultsLink(page: Page, displayname: string, isInline?: boolean): Locator { + const loc = isInline ? RecordLocators.getEntityRelatedTable(page, displayname) : RecordLocators.getRelatedTableAccordion(page, displayname); + return loc.locator('.more-results-link'); + } + + static getSidePanelHeadings(page: Page): Locator { + return page.locator('.columns-container li.toc-heading'); + } + + static getShareButton(page: Page): Locator { + return page.locator('.share-cite-btn'); + } + } diff --git a/test/playwright/locators/recordset.ts b/test/playwright/locators/recordset.ts index d7d6d195a..edc3a9b2e 100644 --- a/test/playwright/locators/recordset.ts +++ b/test/playwright/locators/recordset.ts @@ -1,16 +1,26 @@ import { Locator, Page } from '@playwright/test'; export default class RecordsetLocators { - static async waitForRecordsetPageReady(page: Page) { - await RecordsetLocators.getRecordSetTable(page).waitFor({ state: 'visible' }); + static async waitForRecordsetPageReady(page: Page, timeout?: number): Promise { + await RecordsetLocators.getRecordSetTable(page).waitFor({ state: 'visible', timeout }); + + await page.locator('.recordest-main-spinner').waitFor({ state: 'detached', timeout }); + } + + static async waitForAggregates(page: Page, timeout?: number): Promise { + await page.locator('.table-column-spinner').waitFor({ state: 'hidden', timeout }); } - static async waitForAggregates(page: Page) { - page.locator('.table-column-spinner').waitFor({ state: 'hidden' }); + static getPageTitleElement(page: Page): Locator { + return page.locator('page-title'); } static getRecordSetTable(page: Page): Locator { return page.locator('.recordset-table'); } + static getFacetFilters(page: Page): Locator { + return page.locator('.chiclets-container .filter-chiclet'); + } + } diff --git a/test/playwright/setup/playwright.configuration.ts b/test/playwright/setup/playwright.configuration.ts index abebb7d33..425ddb485 100644 --- a/test/playwright/setup/playwright.configuration.ts +++ b/test/playwright/setup/playwright.configuration.ts @@ -20,6 +20,11 @@ const getConfig = (options: TestOptions) => { const reporterFolder = resolve(__dirname, `./../../../playwright-report/${options.testName}`); + const extraBrowserParams = { + storageState: STORAGE_STATE, + permissions: ['clipboard-read', 'clipboard-write'] + }; + const config = defineConfig({ testMatch: options.testMatch, @@ -39,7 +44,10 @@ const getConfig = (options: TestOptions) => { retries: 0, // Opt out of parallel tests on CI. - // workers: process.env.CI ? 1 : undefined, + workers: process.env.CI ? 4 : undefined, + + // the outputDir is used for screenshot or other tests that use a file, so we should define it anyways + outputDir: resolve(__dirname, `./../../../playwright-output/${options.testName}`), // Reporter to use reporter: process.env.CI ? [ @@ -70,21 +78,18 @@ const getConfig = (options: TestOptions) => { { name: 'chromium', dependencies: ['pretest'], - use: { - ...devices['Desktop Chrome'], - storageState: STORAGE_STATE - }, - }, - { - name: 'firefox', - dependencies: ['pretest'], - use: { ...devices['Desktop Firefox'], storageState: STORAGE_STATE }, - }, - { - name: 'webkit', - dependencies: ['pretest'], - use: { ...devices['Desktop Safari'], storageState: STORAGE_STATE }, + use: { ...devices['Desktop Chrome'], ...extraBrowserParams }, }, + // { + // name: 'firefox', + // dependencies: ['pretest'], + // use: { ...devices['Desktop Firefox'], ...extraBrowserParams }, + // }, + // { + // name: 'webkit', + // dependencies: ['pretest'], + // use: { ...devices['Desktop Safari'], ...extraBrowserParams }, + // }, ], }); diff --git a/test/playwright/setup/playwright.parameters.ts b/test/playwright/setup/playwright.parameters.ts index e735f4b8e..a5a11f6bb 100644 --- a/test/playwright/setup/playwright.parameters.ts +++ b/test/playwright/setup/playwright.parameters.ts @@ -1,8 +1,13 @@ +import fs from 'fs'; + import { isObjectAndNotNull } from '@isrd-isi-edu/chaise/src/utils/type-utils'; import { resolve } from 'path'; +import { TestInfo } from '@playwright/test'; export const ERMREST_URL = process.env.ERMREST_URL; +export const CHAISE_BASE_URL = process.env.CHAISE_BASE_URL; + /** * return the catalog created for tests. * @@ -10,7 +15,7 @@ export const ERMREST_URL = process.env.ERMREST_URL; * * (populated during setup) */ -export const getCatalogID = (projectName?: string, dontLogError?: boolean) : string | any | null => { +export const getCatalogID = (projectName?: string, dontLogError?: boolean): string | any | null => { try { const obj = JSON.parse(process.env.CATALOG_ID!); if (!isObjectAndNotNull(obj) || (projectName && !obj[projectName])) { @@ -51,6 +56,44 @@ export const setCatalogID = (projectName: string, catalogId: string) => { */ export const ENTITIES_PATH = 'entities.json'; +/** + * return the row values based on the given criteria. useful for finding the system generated value of columns. + */ +export const getEntityRow = (testInfo: TestInfo, schema: string, table: string, row: { column: string, value: string }[]) => { + let match, entities; + try { + const fileContent = fs.readFileSync(ENTITIES_PATH, { encoding: 'utf8', flag: 'r' }); + const data = JSON.parse(fileContent); + entities = data[testInfo.project.name][schema][table]; + if (!Array.isArray(entities)) { + throw new Error('saved value is not an array.'); + } + } catch (exp) { + console.log(`the entities file is eaither missing or doesn't have the proper value. path=${ENTITIES_PATH}`); + console.log(exp); + return null; + } + + for (let i = 0; i < entities.length; i++) { + const entity = entities[i]; + // identifying information for entity could be multiple columns of data + // which is the case for assocation tables + for (let j = 0; j < row.length; j++) { + // eslint-disable-next-line eqeqeq + if (entity[row[j].column] == row[j].value) { + match = entity; + } else { + match = null; + // move on to next entity + break; + } + } + if (match) break; + } + return match; +} + + export const PRESET_PROJECT_NAME = 'pretest'; /** @@ -58,7 +101,7 @@ export const PRESET_PROJECT_NAME = 'pretest'; * (populated during setup) */ export const getMainUserSessionObject = () => { - return JSON.parse(process.env.WEBAUTHN_SESSION!); + return JSON.parse(process.env.WEBAUTHN_SESSION!); } @@ -66,3 +109,6 @@ export const getMainUserSessionObject = () => { * the file that contains the logged in browser state */ export const STORAGE_STATE = resolve(__dirname, '../.auth/user.json'); + + +export const DOWNLOAD_FOLDER = resolve(__dirname, '../.download'); diff --git a/test/playwright/setup/playwright.setup.ts b/test/playwright/setup/playwright.setup.ts index 634b01822..0872ae836 100644 --- a/test/playwright/setup/playwright.setup.ts +++ b/test/playwright/setup/playwright.setup.ts @@ -143,7 +143,7 @@ async function createCatalog(testConfiguration: any, projectNames: string[], isM } // merge all the schema configurations - const catalog: any = {}, schemas: any = {}; + const catalog: any = {}, schemas: any = {}, entities : any = {}; schemaConfigurations.forEach((config: any) => { // copy annotations and ACLs over to the submitted catalog object @@ -177,19 +177,10 @@ async function createCatalog(testConfiguration: any, projectNames: string[], isM console.log(`catalog with id ${res.catalogId} created for project ${p}`); setCatalogID(p, res.catalogId); - // TODO capture entities per project - // const entities = res.entities; - // fs.writeFile(ENTITIES_PATH, JSON.stringify(entities), 'utf8', function (err) { - // if (err) { - // console.log('couldn\'t write entities.'); - // console.log(err); - // reject(new Error('Unable to import data')); - // } else { - // console.log('created entities file for schemas'); - // resolve(true); - // } - // }); - + if (!(p in entities)) { + entities[p] = {}; + } + entities[p] = res.entities; } catch (exp) { console.log(exp); reject(new Error('Unable to import data')); @@ -197,6 +188,18 @@ async function createCatalog(testConfiguration: any, projectNames: string[], isM } } + // write to file so it's easier to find. + fs.writeFile(ENTITIES_PATH, JSON.stringify(entities), 'utf8', function (err) { + if (err) { + console.log('couldn\'t write entities.'); + console.log(err); + reject(new Error('Unable to import data')); + } else { + console.log('created entities file for schemas'); + resolve(true); + } + }); + resolve(true); }); diff --git a/test/playwright/specs/all-features/record/related-table.spec.ts b/test/playwright/specs/all-features/record/related-table.spec.ts index 26f2b4e9d..d53188450 100644 --- a/test/playwright/specs/all-features/record/related-table.spec.ts +++ b/test/playwright/specs/all-features/record/related-table.spec.ts @@ -1,10 +1,12 @@ -import { test, expect } from '@playwright/test'; +import moment from 'moment'; +import { test, expect, TestInfo } from '@playwright/test'; // locators import RecordLocators from '@isrd-isi-edu/chaise/test/playwright/locators/record'; // utils -import { getCatalogID } from '@isrd-isi-edu/chaise/test/playwright/setup/playwright.parameters'; +import { CHAISE_BASE_URL, getCatalogID, getEntityRow } from '@isrd-isi-edu/chaise/test/playwright/setup/playwright.parameters'; +import { testShareCiteModal } from '@isrd-isi-edu/chaise/test/playwright/utils/record-utils'; const testParams = { schemaName: 'product-unordered-related-tables-links', @@ -51,11 +53,13 @@ const testParams = { }; test.describe('Related tables', () => { + const keys = []; + keys.push(testParams.key.name + testParams.key.operator + testParams.key.value); + const URL_PATH = `${testParams.schemaName}:${testParams.table_name}/${keys.join('&')}`; test.beforeEach(async ({ page, baseURL }, testInfo) => { - const keys = []; - keys.push(testParams.key.name + testParams.key.operator + testParams.key.value); - const PAGE_URL = `/record/#${getCatalogID(testInfo.project.name)}/${testParams.schemaName}:${testParams.table_name}/${keys.join('&')}`; + + const PAGE_URL = `/record/#${getCatalogID(testInfo.project.name)}/${URL_PATH}`; await page.goto(`${baseURL}${PAGE_URL}`); @@ -63,6 +67,40 @@ test.describe('Related tables', () => { }); // TODO + test('overal structure of the page', async ({ page }) => { + await test.step('table of contents should be displayed properly and in correct order', async () => { + await expect.soft(RecordLocators.getSidePanelHeadings(page)).toHaveCount(testParams.tocHeaders.length); + await expect.soft(RecordLocators.getSidePanelHeadings(page)).toHaveText(testParams.tocHeaders); + }); + + await test.step('related entities should show in the expected order', async () => { + await expect.soft(RecordLocators.getDisplayedRelatedTableTitles(page)).toHaveCount(testParams.headers.length); + await expect.soft(RecordLocators.getDisplayedRelatedTableTitles(page)).toHaveText(testParams.headers); + }); + }); + + test('share popup when the citation annotation has wait_for of all-outbound', async ({ page, baseURL }, testInfo) => { + const keyValues = [{ column: testParams.key.name, value: testParams.key.value }]; + const ridValue = getEntityRow(testInfo, testParams.schemaName, testParams.table_name, keyValues).RID; + const link = `${baseURL}/record/#${getCatalogID(testInfo.project.name)}/${testParams.schemaName}:${testParams.table_name}/RID=${ridValue}`; + await testShareCiteModal( + page, + { + title: 'Share and Cite', + link, + // the table has history-capture: false + hasVersionedLink: false, + verifyVersionedLink: false, + citation: [ + 'Super 8 North Hollywood Motel, accommodation_outbound1_outbound1 two ', + 'https://www.kayak.com/hotels/Super-8-North-Hollywood-c31809-h40498/2016-06-09/2016-06-10/2guests ', + `(${moment().format('YYYY')}).`, + ].join(''), + // we don't need to test this here as well (it has been tested in record presentation) + bibtextFile: `accommodation_${ridValue}.bib`, + } + ); + }); }); diff --git a/test/playwright/specs/delete-prohibited/navbar/catalog-chaise-config.spec.ts b/test/playwright/specs/delete-prohibited/navbar/catalog-chaise-config.spec.ts index 245423e7e..dd332e329 100644 --- a/test/playwright/specs/delete-prohibited/navbar/catalog-chaise-config.spec.ts +++ b/test/playwright/specs/delete-prohibited/navbar/catalog-chaise-config.spec.ts @@ -12,7 +12,7 @@ test.describe('Navbar', () => { test('when navbar is visible', async ({ page, baseURL, context }, testInfo) => { const navbar = NavbarLocators.getContainer(page); - const loginMenuOption = navbar.locator('.login-menu-options'); + const loginMenuOption = NavbarLocators.getLoginMenuContainer(page); await test.step('navbar should be visible on load.', async () => { const PAGE_URL = `/recordset/#${getCatalogID(testInfo.project.name)}/catalog-config-navbar:config-table`; diff --git a/test/playwright/utils/record-utils.ts b/test/playwright/utils/record-utils.ts index 5f55a1d53..ca3ec468f 100644 --- a/test/playwright/utils/record-utils.ts +++ b/test/playwright/utils/record-utils.ts @@ -1,11 +1,22 @@ -import { test } from '@playwright/test'; +import { Page, TestInfo, expect, test } from '@playwright/test'; +// locators +import RecordLocators from '@isrd-isi-edu/chaise/test/playwright/locators/record'; +import RecordsetLocators from '@isrd-isi-edu/chaise/test/playwright/locators/recordset'; +import PageLocators from '@isrd-isi-edu/chaise/test/playwright/locators/page'; +import ModalLocators from '@isrd-isi-edu/chaise/test/playwright/locators/modal'; + +// utils +import { getCatalogID } from '@isrd-isi-edu/chaise/test/playwright/setup/playwright.parameters'; +import { Modal } from 'react-bootstrap'; type RelatedTableTestParam = { testTitle: string, name: string, schemaName: string, displayname: string, + inlineComment?: string, + count: number, canEdit: boolean, canCreate: boolean, @@ -18,7 +29,7 @@ type RelatedTableTestParam = { viewMore?: { name: string, displayname: string, - filter?: string, + filter: string, }, rowValues?: string[], rowViewPaths?: string[], @@ -32,8 +43,198 @@ type RelatedTableTestParam = { testDelete?: boolean, }; -export const testRelatedTable = (params: RelatedTableTestParam) => { - test(params.testTitle, async ({page}) => { - // - }) +export const testRelatedTablePresentation = (pagePath: string, params: RelatedTableTestParam) => { + test.beforeEach(async ({ page, baseURL }, testInfo) => { + const PAGE_URL = `/recordset/#${getCatalogID(testInfo.project.name)}/${pagePath}`; + await page.goto(`${baseURL}${PAGE_URL}`); + + await RecordLocators.waitForRecordPageReady(page); + }); + + + test('basic features', async ({ page }) => { + test.skip(!!params.isInline && !params.inlineComment); + + if (!params.isInline) { + await test.step('title should be correct', async () => { + const titleEl = RecordLocators.getRelatedTableSectionHeaderDisplayname(page, params.displayname); + await expect.soft(titleEl).toHaveText(params.displayname); + }); + } + + if (params.inlineComment) { + await test.step('inline comment should be displayed.', async () => { + const inlineComment = RecordLocators.getRelatedTableInlineComment(page, params.displayname); + // we have to have this otherwise typescript will complain + if (!params.inlineComment) return; + await expect.soft(inlineComment).toHaveText(params.inlineComment); + }); + } + }); + + test(`table level actions for ${params.displayname}`, async ({ page, context }) => { + + await test.step('Explore button', async () => { + const exploreButton = RecordLocators.getMoreResultsLink(page, params.displayname, params.isInline); + + await test.step('should be displayed.', async () => { + await expect.soft(exploreButton).toBeVisible(); + }); + + + if (params.viewMore) { + await test.step('should go to the recordset app with correct set of filters', async () => { + const newPage = await PageLocators.clickNewTabLink(exploreButton, context); + await newPage.waitForURL('**/recordset/**'); + await RecordsetLocators.waitForRecordsetPageReady(newPage); + + await expect.soft(RecordsetLocators.getPageTitleElement(newPage), 'recordset title missmatch.').toHaveText(params.viewMore!.displayname); + + const chiclets = RecordsetLocators.getFacetFilters(page); + await expect.soft(chiclets, 'filter didn\'t show up').toHaveCount(1); + await expect.soft(chiclets.first(), 'filter missmatch').toHaveText(params.viewMore!.filter) + + await newPage.close(); + }); + + } + + }); + + // if (typeof params.canEdit === 'boolean') { + // } + + + }); +} + + +type ShareCiteModalParams = { + /** + * the modal title + */ + title: string, + /** + * the main link + */ + link: string, + /** + * whether versioned link is present or not + */ + hasVersionedLink: boolean, + /** + * if true, we will test the versioned link too. + */ + verifyVersionedLink: boolean, + /** + * pass `null` citation should not be displayed. + */ + citation: string | null, + /** + * the location of the bibtext file so we can delete it after downloading it + */ + bibtextFile?: string, +} + +/** + * test share cite modal features. + * + * @param params + * @param URLPath if you want the test case to navigate before testing, pass the url path. otherwise skip this. + */ +export const testShareCiteModal = async (page: Page, params: ShareCiteModalParams, URLPath?: string) => { + + // test('for share & citation modal', async ({ page, baseURL }, testInfo) => { + + // const expectedLink = typeof params.link === 'string' ? params.link : params.link(baseURL, testInfo); + const expectedLink = params.link; + + const shareBtn = RecordLocators.getShareButton(page); + const shareCiteModal = ModalLocators.getShareCiteModal(page); + + await test.step('share button should be available after the page is fully loaded.', async () => { + // if (URLPath) { + // const PAGE_URL = `/record/#${getCatalogID(testInfo.project.name)}/${URLPath}`; + // await page.goto(`${baseURL}${PAGE_URL}`); + // } + + await RecordLocators.waitForRecordPageReady(page); + + await shareBtn.waitFor({ state: 'visible' }); + }); + + await test.step('should show the share dialog when clicking the share button.', async () => { + await shareBtn.click(); + + await expect(shareCiteModal).toBeVisible(); + + await expect.soft(ModalLocators.getModalTitle(shareCiteModal)).toHaveText(params.title) + + // share link + citation + bibtext or just share link + const count = params.citation ? 3 : 1; + await expect.soft(ModalLocators.getModalListElements(shareCiteModal)).toHaveCount(count); + + await expect.soft(ModalLocators.getShareLinkHeader(shareCiteModal)).toHaveText('Share Link'); + }); + + await test.step('should have proper links.', async () => { + const expectedHeaders: string | string[] = params.hasVersionedLink ? ['Versioned Link', 'Live Link'] : ['Live Link']; + await expect.soft(ModalLocators.getShareLinkSubHeaders(shareCiteModal)).toHaveText(expectedHeaders); + + await expect.soft(ModalLocators.getLiveLinkElement(shareCiteModal)).toHaveText(expectedLink); + + if (params.verifyVersionedLink) { + // we cannot actually test the versioned link, so we're just making sure that it starts with the link + await expect.soft(ModalLocators.getVersionedLinkElement(shareCiteModal)).toContainText(expectedLink); + } + }); + + await test.step('copy to clipboard buttons should be available and work', async () => { + const btns = ModalLocators.getShareLinkCopyBtns(shareCiteModal); + + await expect.soft(btns).toHaveCount(params.hasVersionedLink ? 2 : 1); + + // TODO test copy to clipboard + const liveBtn = btns.nth(params.hasVersionedLink ? 1 : 0); + await liveBtn.click(); + + await page.pause(); + + let clipboardText = await PageLocators.getClipboardContent(page); + expect.soft(clipboardText).toBe(expectedLink); + + if (params.verifyVersionedLink) { + await btns.first().click(); + + clipboardText = await PageLocators.getClipboardContent(page); + expect.soft(clipboardText).toBe(expectedLink); + } + }); + + if (params.citation) { + await test.step('should have a citation present', async () => { + // verify citation + await expect.soft(ModalLocators.getCitationHeader(shareCiteModal)).toHaveText('Data Citation'); + await expect.soft(ModalLocators.getCitationText(shareCiteModal)).toHaveText(params.citation!); + + // verify bibtex + await expect.soft(ModalLocators.getDownloadCitationHeader(shareCiteModal)).toHaveText('Download Data Citation:'); + await expect.soft(ModalLocators.getBibtex(shareCiteModal)).toHaveText('BibTex'); + }); + } + + if (params.bibtextFile) { + await test.step('should download the citation in BibTex format.', async () => { + const btn = ModalLocators.getBibtex(shareCiteModal); + await PageLocators.clickAndVerifyDownload(btn, params.bibtextFile, page); + }); + } + + await test.step('clicking on close button should close the modal.', async () => { + await ModalLocators.getCloseBtn(shareCiteModal).click(); + + await shareCiteModal.waitFor({ state: 'detached' }); + }); + + // }); }