From f1e11b9ef2e7daafcf39ea1011b5e0639921914a Mon Sep 17 00:00:00 2001 From: clau <1281581+dualcnhq@users.noreply.github.com> Date: Wed, 31 Jan 2024 07:51:40 +0800 Subject: [PATCH] Collated Search Page tests (#14) --------- Co-authored-by: IA Jim --- .prettierc | 4 + README.md | 20 +- package.json | 5 +- tests/books/base-test.spec.ts | 17 +- tests/fixtures.ts | 28 ++ tests/login/login.spec.ts | 8 +- tests/models.ts | 58 ++++ tests/music/music.spec.ts | 35 ++- tests/pageObjects/collection-facets.ts | 133 ++++++++++ tests/pageObjects/infinite-scroller.ts | 306 ++++++++++++++++++++++ tests/pageObjects/search-page.ts | 151 +++++++++++ tests/{shared => pageObjects}/sort-bar.ts | 75 +++--- tests/search/search-faceting.spec.ts | 147 +++++++++++ tests/search/search-layout.spec.ts | 117 +++++++++ tests/search/search-page.spec.ts | 123 +++++---- tests/search/search-page.ts | 88 ------- tests/shared/collection-facets.ts | 29 -- tests/shared/infiinite-scroller.ts | 54 ---- tests/utils.ts | 24 ++ 19 files changed, 1145 insertions(+), 277 deletions(-) create mode 100644 .prettierc create mode 100644 tests/fixtures.ts create mode 100644 tests/models.ts create mode 100644 tests/pageObjects/collection-facets.ts create mode 100644 tests/pageObjects/infinite-scroller.ts create mode 100644 tests/pageObjects/search-page.ts rename tests/{shared => pageObjects}/sort-bar.ts (58%) create mode 100644 tests/search/search-faceting.spec.ts create mode 100644 tests/search/search-layout.spec.ts delete mode 100644 tests/search/search-page.ts delete mode 100644 tests/shared/collection-facets.ts delete mode 100644 tests/shared/infiinite-scroller.ts create mode 100644 tests/utils.ts diff --git a/.prettierc b/.prettierc new file mode 100644 index 00000000..a20502b7 --- /dev/null +++ b/.prettierc @@ -0,0 +1,4 @@ +{ + "singleQuote": true, + "trailingComma": "all" +} diff --git a/README.md b/README.md index 94d80394..9566c4ad 100644 --- a/README.md +++ b/README.md @@ -13,13 +13,31 @@ - run command to install Playwright browser libs: `npx install playwright` - run command to run all the tests: `npm run test` -## Running individual tests + +## Running individual tests by category - run books tests: `npm run test:books` - run login tests: `npm run test:login` - run music tests: `npm run test:music` - run search tests: `npm run test:search` + +## Running tests using VSCode Playwright plugin + +- install [VSCode Playwright plugin](https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright) to run specific tests using VSCode + +## Running specific test spec by file: + +- run command format: `npx playwright test ` +- sample: `npx playwright test tests/search/search-layout.spec.ts` + + +## Running specific test spec by file in debug mode: + +- run command format: `npx playwright test --debug` +- sample: `npx playwright test tests/search/search-layout.spec.ts --debug` + + ## View tests execution result - run: `npm run show:report` diff --git a/package.json b/package.json index 1174cc57..ca118f38 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,10 @@ "test:search": "npx playwright test tests/search", "test:books": "npx playwright test tests/books", "test:login": "npx playwright test tests/login", - "test:music": "npx playwright test tests/music" + "test:music": "npx playwright test tests/music", + "format": "prettier --write \"tests/**/*.ts\"", + "lint": "prettier --check \"tests/**/*.ts\"", + "typecheck": "node node_modules/typescript/bin/tsc --noEmit" }, "keywords": [], "author": "", diff --git a/tests/books/base-test.spec.ts b/tests/books/base-test.spec.ts index 479405ce..2eedfc61 100644 --- a/tests/books/base-test.spec.ts +++ b/tests/books/base-test.spec.ts @@ -54,7 +54,9 @@ test('On load, pages fit fully inside of the BookReaderâ„¢', async () => { const brContainerBox = await brContainer.boundingBox(); // images do not get cropped vertically - expect(brContainerBox?.height).toBeLessThanOrEqual(Number(brShellBox?.height)); + expect(brContainerBox?.height).toBeLessThanOrEqual( + Number(brShellBox?.height), + ); // images do not get cropped horizontally expect(brContainerBox?.width).toBeLessThanOrEqual(Number(brShellBox?.width)); }); @@ -88,9 +90,13 @@ test.describe('Test bookreader navigations', () => { const brContainerBox = await brContainer.boundingBox(); // images do not get cropped vertically - expect(brContainerBox?.height).toBeLessThanOrEqual(Number(brShellBox?.height)); + expect(brContainerBox?.height).toBeLessThanOrEqual( + Number(brShellBox?.height), + ); // images do not get cropped horizontally - expect(brContainerBox?.width).toBeLessThanOrEqual(Number(brShellBox?.width)); + expect(brContainerBox?.width).toBeLessThanOrEqual( + Number(brShellBox?.width), + ); }); test('2. nav menu displays properly', async () => { @@ -146,7 +152,7 @@ test.describe('Test bookreader navigations', () => { await goNext.click(); await page.waitForTimeout(PAGE_FLIP_WAIT_TIME); - const onLoadBrState = brContainer.nth(0);// .child(0); + const onLoadBrState = brContainer.nth(0); // .child(0); const initialImages = onLoadBrState.locator('img'); // .find('img'); const origImg1Src = await initialImages.nth(0).getAttribute('src'); const origImg2Src = await initialImages.nth(-1).getAttribute('src'); @@ -155,7 +161,7 @@ test.describe('Test bookreader navigations', () => { await page.waitForTimeout(PAGE_FLIP_WAIT_TIME); const nextBrState = brContainer.nth(0); - const prevImages = nextBrState.locator('img'); + const prevImages = nextBrState.locator('img'); const prevImg1Src = await prevImages.nth(0).getAttribute('src'); const prevImg2Src = await prevImages.nth(-1).getAttribute('src'); @@ -190,5 +196,4 @@ test.describe('Test bookreader navigations', () => { expect(await isPageInUrl()).toEqual(true); expect(await isModeInUrl('2up')).toEqual(true); }); - }); diff --git a/tests/fixtures.ts b/tests/fixtures.ts new file mode 100644 index 00000000..6654a9d5 --- /dev/null +++ b/tests/fixtures.ts @@ -0,0 +1,28 @@ +import { test as base } from '@playwright/test'; + +import { SearchPage } from './pageObjects/search-page'; + +type PageFixtures = { + searchPage: SearchPage; +}; + +export const test = base.extend({ + searchPage: async ({ page }, use) => { + // Set up the fixture. + const searchPage = new SearchPage(page); + await searchPage.visit(); + await searchPage.queryFor('cats'); + + await page.route(/(analytics|fonts)/, route => { + route.abort(); + }); + + // Use the fixture value in the test. + await use(searchPage); + + // Clean up the fixture. + await page.close(); + }, +}); + +export { expect } from '@playwright/test'; diff --git a/tests/login/login.spec.ts b/tests/login/login.spec.ts index 89495615..372bf911 100644 --- a/tests/login/login.spec.ts +++ b/tests/login/login.spec.ts @@ -9,8 +9,12 @@ test('page load - check login page fields elements', async ({ page }) => { const loginFormElement = boxRow.locator('.login-form-element'); const formLoginFields = loginFormElement.locator('.iaform.login-form'); const inputEmail = loginFormElement.locator('.form-element.input-email'); - const inputPassword = loginFormElement.locator('.form-element.input-password'); - const btnLogin = loginFormElement.locator('.btn.btn-primary.btn-submit.input-submit.js-submit-login'); + const inputPassword = loginFormElement.locator( + '.form-element.input-password', + ); + const btnLogin = loginFormElement.locator( + '.btn.btn-primary.btn-submit.input-submit.js-submit-login', + ); expect(await loginFormElement.count()).toEqual(1); expect(await inputEmail.count()).toEqual(1); diff --git a/tests/models.ts b/tests/models.ts new file mode 100644 index 00000000..54805daf --- /dev/null +++ b/tests/models.ts @@ -0,0 +1,58 @@ +export type DateMetadataLabel = { + filter: string; + date: string; +}; + +export type LayoutViewMode = 'tile' | 'list' | 'compact'; + +export enum LayoutViewModeLocator { + TILE = '#grid-button', + LIST = '#list-detail-button', + COMPACT = '#list-compact-button', +} + +export enum SearchOption { + METADATA = `Search metadata`, + TEXT = `Search text contents`, + TV = `Search TV news captions`, + RADIO = `Search radio transcripts`, + WEB = `Search archived web sites`, +} + +export type SortFilter = + | 'Weekly views' + | 'All-time views' + | 'Title' + | 'Date published' + | 'Date archived' + | 'Date reviewed' + | 'Date added' + | 'Creator'; + +export type SortOrder = 'ascending' | 'descending'; + +export const SortFilterURL = { + 'Weekly views': 'week', + 'All-time views': 'downloads', + Title: 'title', + 'Date published': 'date', + 'Date archived': 'publicdate', + 'Date reviewed': 'reviewdate', + 'Date added': 'addeddate', + Creator: 'creator', +}; + +export enum FacetGroupLocatorLabel { + DATE = 'date-picker-label', + MEDIATYPE = 'facet-group-header-label-mediatype', + LENDING = 'facet-group-header-label-lending', + YEAR = 'facet-group-header-label-year', + SUBJECT = 'facet-group-header-label-subject', + COLLECTION = 'facet-group-header-label-collection', + CREATOR = 'facet-group-header-label-creator', + LANGUAGE = 'facet-group-header-label-language', +} + +export type FacetType = 'positive' | 'negative'; + +export type ViewFacetGroup = 'tile-title' | 'list-date'; diff --git a/tests/music/music.spec.ts b/tests/music/music.spec.ts index 3619e0e0..8d1360e3 100644 --- a/tests/music/music.spec.ts +++ b/tests/music/music.spec.ts @@ -5,38 +5,42 @@ const trackListDetails = [ { number: '1', title: 'Squeaking Door', - length: '00:06' + length: '00:06', }, { number: '2', title: 'Steps', - length: '00:03' + length: '00:03', }, { number: '3', title: 'Steps', - length: '00:03' + length: '00:03', }, ]; test('Play 3 mystery sound effects', async ({ page }) => { - await page.goto('https://archive.org/details/cd_mystery-sound-effects_gateway-gecordings'); + await page.goto( + 'https://archive.org/details/cd_mystery-sound-effects_gateway-gecordings', + ); const iaMusicTheater = page.locator('ia-music-theater'); expect(await iaMusicTheater.count()).toEqual(1); const musicTheater = page.locator('#music-theater'); expect(await musicTheater.count()).toEqual(1); - + // player controls const channelSelector = musicTheater.locator('channel-selector'); const channelSelectorRadio = channelSelector.locator('#radio'); expect(await channelSelector.count()).toEqual(1); expect(await channelSelectorRadio.count()).toEqual(1); - expect(await channelSelectorRadio.locator('#selector-title').count()).toEqual(1); + expect(await channelSelectorRadio.locator('#selector-title').count()).toEqual( + 1, + ); const rows = channelSelectorRadio.getByRole('listitem'); expect(await rows.count()).toEqual(2); await expect(rows).toHaveText(['Player', 'Webamp']); - + // photo-viewer const iauxPhotoViewer = musicTheater.locator('iaux-photo-viewer'); expect(await iauxPhotoViewer.count()).toEqual(1); @@ -58,18 +62,25 @@ test('Play 3 mystery sound effects', async ({ page }) => { const trackTitle = trackListButtons.nth(i).locator('.track-title'); const trackLength = trackListButtons.nth(i).locator('.track-length'); - expect(await trackNumber.textContent()).toContain(trackListDetails[i].number); + expect(await trackNumber.textContent()).toContain( + trackListDetails[i].number, + ); expect(await trackTitle.textContent()).toContain(trackListDetails[i].title); - expect(await trackLength.textContent()).toContain(trackListDetails[i].length); + expect(await trackLength.textContent()).toContain( + trackListDetails[i].length, + ); } // select button tracks await trackListButtons.nth(1).click(); - await page.waitForURL('https://archive.org/details/cd_mystery-sound-effects_gateway-gecordings/disc1/02.+Gateway+Gecordings+-+Steps.flac'); + await page.waitForURL( + 'https://archive.org/details/cd_mystery-sound-effects_gateway-gecordings/disc1/02.+Gateway+Gecordings+-+Steps.flac', + ); await trackListButtons.nth(2).click(); - await page.waitForURL('https://archive.org/details/cd_mystery-sound-effects_gateway-gecordings/disc1/03.+Gateway+Gecordings+-+Steps.flac'); + await page.waitForURL( + 'https://archive.org/details/cd_mystery-sound-effects_gateway-gecordings/disc1/03.+Gateway+Gecordings+-+Steps.flac', + ); await page.close(); }); - diff --git a/tests/pageObjects/collection-facets.ts b/tests/pageObjects/collection-facets.ts new file mode 100644 index 00000000..f4e2ab29 --- /dev/null +++ b/tests/pageObjects/collection-facets.ts @@ -0,0 +1,133 @@ +import { type Page, type Locator, expect } from '@playwright/test'; + +import { FacetGroupLocatorLabel, FacetType } from '../models'; + +export class CollectionFacets { + readonly page: Page; + readonly collectionFacets: Locator; + readonly resultsTotal: Locator; + readonly modalManager: Locator; + readonly moreFacetsContent: Locator; + + public constructor(page: Page) { + this.page = page; + this.collectionFacets = page.locator('collection-facets'); + this.resultsTotal = page.locator('#facets-header-container #results-total'); + + this.modalManager = page.locator('modal-manager'); + this.moreFacetsContent = page.locator('more-facets-content'); + } + + async checkResultCount() { + await expect(this.page.getByText('Searching')).toBeVisible(); + await expect(this.resultsTotal).toBeVisible(); + } + + async assertFacetGroupCount() { + await this.page.waitForLoadState('networkidle'); + await this.page.waitForTimeout(1000); + + const facetGroups = this.collectionFacets.locator('facets-template'); + expect(await facetGroups.count()).toEqual(7); + } + + async selectFacetByGroup( + group: FacetGroupLocatorLabel, + facetLabel: string, + facetType: FacetType, + ) { + await this.page.waitForLoadState('networkidle'); + await this.page.waitForTimeout(1000); + + const facetContent = await this.getFacetGroupContainer(group); + if (facetContent) { + if (facetType === 'positive') { + const facetRow = facetContent.getByRole('checkbox', { + name: facetLabel, + }); + await facetRow.check(); + return; + } + + if (facetType === 'negative') { + const facetRows = await facetContent + .locator('facets-template > div.facets-on-page facet-row') + .all(); + if (facetRows.length !== 0) { + for (let x = 0; x < facetRows.length; x++) { + const facetRowLabel = facetRows[x].locator( + 'div.facet-row-container > div.facet-checkboxes > label', + ); + const facetRowTitle = await facetRowLabel.getAttribute('title'); + if (facetRowTitle === `Hide mediatype: ${facetLabel}`) { + await facetRowLabel.click(); + return; + } + } + } + } + } + } + + async clickMoreInFacetGroup(group: FacetGroupLocatorLabel) { + const facetContent = await this.getFacetGroupContainer(group); + if (facetContent) { + const btnMore = facetContent.locator('button'); + await btnMore.click(); + } + } + + async selectFacetsInModal(facetLabels: string[]) { + await this.page.waitForLoadState(); + + const btnApplyFilters = this.moreFacetsContent.locator( + '#more-facets > div.footer > button.btn.btn-submit', + ); + for (let i = 0; i < facetLabels.length; i++) { + // wait for the promise to resolve before advancing the for loop + const facetRow = this.moreFacetsContent + .locator('#more-facets') + .getByRole('checkbox', { name: facetLabels[i] }); + await facetRow.check(); + } + await btnApplyFilters.click(); + } + + async fillUpYearFilters(startDate: string, endDate: string) { + await this.page.waitForLoadState('networkidle'); + await this.page.waitForTimeout(2000); + + const facetContent = await this.getFacetGroupContainer( + FacetGroupLocatorLabel.DATE, + ); + if (facetContent) { + const datePickerContainer = facetContent.locator( + 'histogram-date-range #container > div.inner-container > #inputs', + ); + const minYear = datePickerContainer.locator('input#date-min'); + const maxYear = datePickerContainer.locator('input#date-max'); + + await minYear.fill(startDate); + await maxYear.fill(endDate); + await maxYear.press('Enter'); + } + } + + async getFacetGroupContainer( + group: FacetGroupLocatorLabel, + ): Promise { + const facetGroups = await this.collectionFacets + .locator('#container > section.facet-group') + .all(); + + for (let i = 0; i < facetGroups.length; i++) { + const facetHeader = await facetGroups[i].getAttribute('aria-labelledby'); + if (facetHeader === group) { + return group === FacetGroupLocatorLabel.DATE + ? facetGroups[i] + : facetGroups[i].locator('div.facet-group-content'); + } + } + return null; + } +} diff --git a/tests/pageObjects/infinite-scroller.ts b/tests/pageObjects/infinite-scroller.ts new file mode 100644 index 00000000..44d14128 --- /dev/null +++ b/tests/pageObjects/infinite-scroller.ts @@ -0,0 +1,306 @@ +import { type Page, type Locator, expect } from '@playwright/test'; + +import { SortBar } from './sort-bar'; + +import { + DateMetadataLabel, + LayoutViewMode, + SortOrder, + SortFilter, + ViewFacetGroup, + LayoutViewModeLocator, +} from '../models'; + +import { datesSorted, viewsSorted } from '../utils'; + +export class InfiniteScroller { + readonly page: Page; + readonly infiniteScroller: Locator; + readonly infiniteScrollerSectionContainer: Locator; + readonly displayStyleSelector: Locator; + readonly displayStyleSelectorOptions: Locator; + readonly firstItemTile: Locator; + + readonly sortBar: SortBar; + + public constructor(page: Page) { + this.page = page; + + this.infiniteScroller = page.locator('infinite-scroller'); + this.infiniteScrollerSectionContainer = + this.infiniteScroller.locator('#container'); + + this.sortBar = new SortBar(page); + const sortBarSection = this.sortBar.sortFilterBar; + this.displayStyleSelector = sortBarSection.locator( + 'div#display-style-selector', + ); + this.displayStyleSelectorOptions = + this.displayStyleSelector.locator('ul > li'); + this.firstItemTile = this.infiniteScrollerSectionContainer + .locator('article') + .nth(0); + } + + async awaitLoadingState() { + await this.page.waitForLoadState('networkidle'); + await this.page.waitForTimeout(2000); + } + + async clickViewMode(viewModeLocator: LayoutViewModeLocator) { + await this.displayStyleSelectorOptions.locator(viewModeLocator).click(); + } + + async assertLayoutViewModeChange(viewMode: LayoutViewMode) { + switch (viewMode) { + case 'tile': + await expect( + this.displayStyleSelector.locator('#grid-button'), + ).toHaveClass('active'); + expect(await expect(this.infiniteScroller).toHaveClass(/grid/)); + return; + case 'list': + await expect( + this.displayStyleSelector.locator('#list-detail-button'), + ).toHaveClass('active'); + expect(await expect(this.infiniteScroller).toHaveClass(/list-detail/)); + return; + case 'compact': + await expect( + this.displayStyleSelector.locator('#list-compact-button'), + ).toHaveClass('active'); + expect(await expect(this.infiniteScroller).toHaveClass(/list-compact/)); + return; + default: + return; + } + } + + async hoverToFirstItem() { + await this.awaitLoadingState(); + expect(await this.firstItemTile.count()).toBe(1); + + await this.firstItemTile.hover(); + await expect(this.firstItemTile.locator('tile-hover-pane')).toBeVisible(); + } + + async assertTileHoverPaneTitleIsSameWithItemTile() { + const textFirstItemTile = await this.firstItemTile + .locator('#title > h4') + .first() + .innerText(); + const textTileHoverPane = await this.firstItemTile + .locator('tile-hover-pane #title > a') + .innerText(); + expect(textFirstItemTile).toEqual(textTileHoverPane); + } + + async clickFirstResultAndCheckRedirectToDetailsPage() { + await this.page.waitForLoadState('networkidle'); + expect(await this.firstItemTile.count()).toBe(1); + + // Get item tile link to compare with the redirect URL + const itemLink = await this.firstItemTile + .locator('a') + .first() + .getAttribute('href'); + const pattern = new RegExp(`${itemLink}`); + await this.firstItemTile.click(); + + await this.page.waitForLoadState(); + await expect(this.page).toHaveURL(pattern); + } + + // TODO: per sort filter and sort order + view mode??? + async checkSortingResults( + filter: SortFilter, + order: SortOrder, + displayItemCount: Number, + ) { + // This test is only applicable in tile view mode for "views" filters + if (filter === 'Weekly views' || filter === 'All-time views') { + await this.awaitLoadingState(); + const tileStatsViews = await this.getTileStatsViewCountTitles( + displayItemCount, + ); + + const isAllViews = tileStatsViews.every(stat => + stat.includes(filter.toLowerCase()), + ); + const arrViewCount: Number[] = tileStatsViews.map(stat => + Number(stat.split(' ')[0]), + ); + const isSortedCorrectly = viewsSorted(order, arrViewCount); + + expect(isAllViews).toBeTruthy(); + expect(isSortedCorrectly).toBeTruthy(); + } + + // This test is only applicable in list view mode for "Date" filters + if ( + filter === 'Date published' || + filter === 'Date archived' || + filter === 'Date added' || + filter === 'Date reviewed' + ) { + await this.awaitLoadingState(); + const dateMetadataLabels = await this.getDateMetadataLabels( + displayItemCount, + ); + // Parse date sort filter to check list of date labels from page item results + // => Published, Archived, Added, Reviewed + const checkFilterText = filter + .split('Date ')[1] + .replace(/^./, str => str.toUpperCase()); + const isDateFilter = dateMetadataLabels.every( + date => date.filter === checkFilterText, + ); + const isSortedCorrectly = datesSorted(order, dateMetadataLabels); + + expect(isDateFilter).toBeTruthy(); + expect(isSortedCorrectly).toBeTruthy(); + } + } + + async checkIncludedFacetedResults( + viewFacetType: ViewFacetGroup, + facetLabels: string[], + toInclude: boolean, + displayItemCount: Number, + ) { + await this.awaitLoadingState(); + const facetedResults = await this.getFacetedResultsByViewFacetGroup( + viewFacetType, + displayItemCount, + ); + if (facetedResults) { + const isAllFacettedCorrectly = facetLabels.some(label => { + return toInclude + ? facetedResults.includes(label) + : !facetedResults.includes(label); + }); + expect(isAllFacettedCorrectly).toBeTruthy(); + } + } + + // Getters + async getTileStatsViewCountTitles( + displayItemCount: Number, + ): Promise { + const arrTileStatsTitle: string[] = []; + const allItems = await this.infiniteScrollerSectionContainer + .locator('article') + .all(); + + // Load first 10 items and get tile stats views title + let index = 0; + while (index !== displayItemCount) { + const collectionTileCount = await allItems[index] + .locator('a > collection-tile') + .count(); + const itemTileCount = await allItems[index] + .locator('a > item-tile') + .count(); + + if (collectionTileCount === 1 && itemTileCount === 0) { + console.log('it is a collection tile - do nothing for now'); + expect.soft(collectionTileCount).toBe(1); + expect.soft(itemTileCount).toBe(0); + } else if (collectionTileCount === 0 && itemTileCount === 1) { + expect.soft(collectionTileCount).toBe(0); + expect.soft(itemTileCount).toBe(1); + // Get view count from tile-stats row + const tileStatsTitle = await allItems[index] + .locator('#stats-row > li:nth-child(2)') + .getAttribute('title'); + if (tileStatsTitle) arrTileStatsTitle.push(tileStatsTitle); + } else { + console.log('it is not a collection-tile nor an item-tile'); + expect.soft(collectionTileCount).toBe(0); + expect.soft(itemTileCount).toBe(0); + } + + index++; + } + + return arrTileStatsTitle; + } + + async getDateMetadataLabels( + displayItemCount: Number, + ): Promise { + const arrDateLine: DateMetadataLabel[] = []; + const allItems = await this.infiniteScrollerSectionContainer + .locator('article') + .all(); + + let index = 0; + while (index !== displayItemCount) { + // Load items and get tileStats views based on displayItemCount + // There can be 2 date metadata in a row if filter is either Date archived, Date reviewed, or Date added + // eg. Published: Nov 15, 2023 - Archived: Jan 19, 2024 + // We always want the last one since it will correspond to the current "sort by" field + + const dateSpanLabel = await allItems[index] + .locator('#dates-line > div.metadata') + .last() + .innerText(); + + if (dateSpanLabel) { + // Need to split date filter and date format value: Published: 2150 or Published: Nov 15, 2023 + // Sample object: { filter: 'Published', date: '2150' } + const strSplitColonSpace = dateSpanLabel.split(': '); + const objDateLine = { + filter: strSplitColonSpace[0], + date: strSplitColonSpace[1], + }; + arrDateLine.push(objDateLine); + } + + index++; + } + + return arrDateLine; + } + + async getTileIconTitle(displayItemCount: Number): Promise { + const arrTileIconTitle: string[] = []; + const allItems = await this.infiniteScrollerSectionContainer + .locator('article') + .all(); + + let index = 0; + while (index !== displayItemCount) { + // Load items based on displayItemCount + // Get mediatype-icon title from tile-stats row + const tileIcon = allItems[index].locator( + '#stats-row > li:nth-child(1) > mediatype-icon > #icon', + ); + const tileIconTitle = await tileIcon.getAttribute('title'); + if (tileIconTitle) arrTileIconTitle.push(tileIconTitle); + + index++; + } + + return arrTileIconTitle; + } + + async getFacetedResultsByViewFacetGroup( + viewFacetType: ViewFacetGroup, + displayItemCount: Number, + ): Promise { + switch (viewFacetType) { + case 'tile-title': + return await this.getTileIconTitle(displayItemCount); + + case 'list-date': + const dateLabels = await this.getDateMetadataLabels(displayItemCount); + if (dateLabels) return dateLabels.map(label => label.date); + + return null; + + default: + return null; + } + } +} diff --git a/tests/pageObjects/search-page.ts b/tests/pageObjects/search-page.ts new file mode 100644 index 00000000..1e2ef8cb --- /dev/null +++ b/tests/pageObjects/search-page.ts @@ -0,0 +1,151 @@ +import { type Page, type Locator, expect } from '@playwright/test'; + +import { CollectionFacets } from './collection-facets'; +import { InfiniteScroller } from './infinite-scroller'; +import { SortBar } from './sort-bar'; + +import { SearchOption, SortOrder, SortFilter, SortFilterURL } from '../models'; + +const PAGE_TIMEOUT = 3000; + +export class SearchPage { + readonly url: string = 'https://archive.org/search'; + readonly page: Page; + readonly btnCollectionSearchInputGo: Locator; + readonly btnCollectionSearchInputCollapser: Locator; + readonly btnClearAllFilters: Locator; + readonly emptyPlaceholder: Locator; + readonly emptyPlaceholderTitleText: Locator; + readonly formInputSearchPage: Locator; + readonly formInputRadioPage: Locator; + readonly formInputTVPage: Locator; + readonly formInputWaybackPage: Locator; + + readonly collectionFacets: CollectionFacets; + readonly infiniteScroller: InfiniteScroller; + readonly sortBar: SortBar; + + public constructor(page: Page) { + this.page = page; + + this.btnCollectionSearchInputGo = page.locator( + 'collection-search-input #go-button', + ); + this.btnCollectionSearchInputCollapser = page.locator( + 'collection-search-input #button-collapser', + ); + this.btnClearAllFilters = page.locator( + '#facets-header-container div.clear-filters-btn-row button', + ); + this.emptyPlaceholder = page.locator('empty-placeholder'); + this.emptyPlaceholderTitleText = this.emptyPlaceholder.locator('h2.title'); + + this.formInputSearchPage = page.locator( + 'collection-search-input #text-input', + ); + this.formInputRadioPage = page.locator( + '#searchform > div > div:nth-child(1) > input', + ); + this.formInputTVPage = page.locator( + '#searchform > div > div:nth-child(1) > input', + ); + this.formInputWaybackPage = page.locator( + 'input.rbt-input-main.form-control.rbt-input', + ); + + this.collectionFacets = new CollectionFacets(this.page); + this.infiniteScroller = new InfiniteScroller(this.page); + this.sortBar = new SortBar(this.page); + } + + async visit() { + await this.page.goto(this.url); + } + + async checkEmptyPagePlaceholder() { + await expect(this.emptyPlaceholder).toBeVisible(); + await expect(this.emptyPlaceholderTitleText).toBeVisible(); + } + + async queryFor(query: string) { + await this.formInputSearchPage.fill(query); + await this.formInputSearchPage.press('Enter'); + await this.page.waitForLoadState('networkidle'); + } + + async clickClearAllFilters() { + await expect(this.btnClearAllFilters).toBeVisible(); + await this.btnClearAllFilters.click(); + } + + async assertClearAllFiltersNotVisible() { + await this.page.waitForLoadState(); + await expect(this.btnClearAllFilters).not.toBeVisible(); + } + + async clickSearchInputOption(option: SearchOption) { + await expect(this.btnCollectionSearchInputGo).toBeVisible(); + await expect(this.formInputSearchPage).toBeVisible(); + + await this.formInputSearchPage.click(); + await this.page.waitForLoadState('networkidle'); + await this.page.waitForTimeout(PAGE_TIMEOUT); + await expect( + this.btnCollectionSearchInputCollapser.getByText(option), + ).toBeVisible(); + await this.btnCollectionSearchInputCollapser.getByText(option).click(); + } + + async checkTVPage(query: string) { + await this.page.waitForLoadState('networkidle'); + await this.page.waitForTimeout(PAGE_TIMEOUT); + expect(await this.page.title()).toContain('Internet Archive TV NEWS'); + await expect( + this.page.getByRole('link', { name: 'TV News Archive', exact: true }), + ).toBeVisible(); + await expect( + this.page.getByRole('heading', { name: 'Search' }), + ).toBeVisible(); + await expect(this.formInputTVPage).toBeVisible(); + expect(await this.formInputTVPage.inputValue()).toContain(query); + } + + async checkRadioPage(query: string) { + await this.page.waitForLoadState('networkidle'); + await this.page.waitForTimeout(PAGE_TIMEOUT); + await expect(this.formInputRadioPage).toBeVisible(); + expect(await this.formInputRadioPage.inputValue()).toContain(query); + } + + async checkWaybackPage(query: string) { + await this.page.waitForLoadState('networkidle'); + await this.page.waitForTimeout(PAGE_TIMEOUT); + expect(await this.page.title()).toContain('Wayback Machine'); + await expect(this.formInputWaybackPage).toBeVisible(); + expect(await this.formInputWaybackPage.inputValue()).toContain(query); + } + + async goBackToSearchPage() { + await this.visit(); + } + + async checkCompactViewModeListLineDateHeaders(filter: SortFilter) { + const checkFilterText = filter + .split('Date ')[1] + .replace(/^./, (str: string) => str.toUpperCase()); + expect( + await this.page + .locator('tile-list-compact-header #list-line-header #date') + .innerText(), + ).toContain(checkFilterText); + } + + async checkURLParamsWithSortFilter(filter: SortFilter, order: SortOrder) { + const sortFilterURL = + order === 'descending' + ? `-${SortFilterURL[filter]}` + : SortFilterURL[filter]; + const urlPatternCheck = new RegExp(`sort=${sortFilterURL}`); + await expect(this.page).toHaveURL(urlPatternCheck); + } +} diff --git a/tests/shared/sort-bar.ts b/tests/pageObjects/sort-bar.ts similarity index 58% rename from tests/shared/sort-bar.ts rename to tests/pageObjects/sort-bar.ts index f4f91826..0735d0ec 100644 --- a/tests/shared/sort-bar.ts +++ b/tests/pageObjects/sort-bar.ts @@ -1,5 +1,7 @@ import { type Page, type Locator, expect } from '@playwright/test'; +import { SortOrder } from '../models'; + export class SortBar { readonly page: Page; readonly sortFilterBar: Locator; @@ -14,32 +16,34 @@ export class SortBar { this.sortFilterBar = page.locator('sort-filter-bar section#sort-bar'); this.sortSelector = this.sortFilterBar.locator('ul#desktop-sort-selector'); this.btnSortDirection = this.sortFilterBar.locator('.sort-direction-icon'); - this.srSortText = this.sortFilterBar.locator('button.sort-direction-selector span.sr-only'); + this.srSortText = this.sortFilterBar.locator( + 'button.sort-direction-selector span.sr-only', + ); } - async buttonClick (sortName: string) { + async buttonClick(sortName: string) { await this.page.getByRole('button', { name: sortName }).click(); } - async caratButtonClick (sortName: string) { - await this.page.getByRole('button', { name: sortName, }).getByRole('button').click(); + async caratButtonClick(sortName: string) { + await this.page + .getByRole('button', { name: sortName }) + .getByRole('button') + .click(); } - async textClick (name: string) { + async textClick(name: string) { await this.page.getByText(name).first().click(); } - async applySortBy (filter: string, direction: string) { + async applySortFilter(filter: string) { const flatSortTextList = ['Relevance', 'Title', 'Creator']; - const viewsDropdown = this.sortSelector.locator('li #views-dropdown'); - const dateDropdown = this.sortSelector.locator('li #date-dropdown'); - const viewsDropdownText = await viewsDropdown.innerText(); - const dateDropdownText = await dateDropdown.innerText(); - if (!flatSortTextList.includes(filter)) { - const _toggleOption = filter.includes('views') ? viewsDropdownText : dateDropdownText; - + const _toggleOption = filter.includes('views') + ? await this.sortSelector.locator('li #views-dropdown').innerText() + : await this.sortSelector.locator('li #date-dropdown').innerText(); + if (filter === _toggleOption) { await this.textClick(filter); } else { @@ -49,16 +53,23 @@ export class SortBar { } else { await this.buttonClick(filter); } + } - await this.page.waitForLoadState() - await this.checkAlphaBarVisibility(filter); - - this.clickSortDirection(direction); + async clickSortDirection(sortOrder: SortOrder) { + // TODO: may still need to find better way to check sort order + const currentSortText = await this.srSortText.innerText(); + const oppositeSortText = + sortOrder === 'ascending' ? 'descending' : 'ascending'; - // TODO: add test to check the actual items loaded if it's in a correct order + if (currentSortText.includes(sortOrder)) { + await this.btnSortDirection.click(); + await expect(this.srSortText).toContainText( + `Change to ${oppositeSortText} sort`, + ); + } } - async checkAlphaBarVisibility (filter: string) { + async checkAlphaBarVisibility(filter: string) { if (!['Title', 'Creator'].includes(filter)) { await expect(this.alphaBar).not.toBeVisible(); } else { @@ -66,41 +77,27 @@ export class SortBar { } } - async clickSortDirection (direction: string) { - // TODO: may still need to find better way to check sort direction - const currentSortText = await this.srSortText.innerText(); - const oppositeSortText = direction === 'ascending' ? 'descending' : 'ascending'; - - if (currentSortText.includes(direction)) { - await this.btnSortDirection.click(); - await expect(this.srSortText).toContainText(`Change to ${oppositeSortText} sort`); - } - + async clickAlphaBarLetterByPosition(pos: number) { await this.page.waitForLoadState(); - } + await this.page.waitForTimeout(3000); - async clickAlphaBarLetterByPosition (pos: number) { const alphabet = [...'ABCDEFGHIJKLMNOPQRSTUVWXYZ']; const nthLetter = this.alphaBar.locator('#container ul > li').nth(pos); const letterSelected = this.alphaBar.locator('#container ul > li.selected'); - + await nthLetter.click(); // Note: assertion .toEqual has deep equality error in webkit expect(await nthLetter.innerText()).toContain(alphabet[pos]); expect(await letterSelected.count()).toEqual(1); - - await this.page.waitForLoadState(); - await this.page.waitForTimeout(3000); } - async clearAlphaBarFilter () { + async clearAlphaBarFilter() { const letterSelected = this.alphaBar.locator('#container ul > li.selected'); expect(await letterSelected.count()).toEqual(0); } - async alphaSortBarNotVisibile () { + async alphaSortBarNotVisibile() { await expect(this.alphaBar).not.toBeVisible(); - } - + } } diff --git a/tests/search/search-faceting.spec.ts b/tests/search/search-faceting.spec.ts new file mode 100644 index 00000000..753fbde4 --- /dev/null +++ b/tests/search/search-faceting.spec.ts @@ -0,0 +1,147 @@ +import { test } from '../fixtures'; + +import { FacetGroupLocatorLabel, LayoutViewModeLocator } from '../models'; + +test('Facets appear', async ({ searchPage }) => { + await test.step('Assert facet group headers count', async () => { + await searchPage.collectionFacets.assertFacetGroupCount(); + }); +}); + +test(`Facets for "movies" in Media Type facet group`, async ({ + searchPage, +}) => { + await test.step(`Select "movies" from inside "Media Type" facet group`, async () => { + await searchPage.collectionFacets.selectFacetByGroup( + FacetGroupLocatorLabel.MEDIATYPE, + 'movies', + 'positive', + ); + }); + + await test.step(`Check the first 10 results for "Movie" results`, async () => { + // checking the tileIcon title for now which is set in a `Title case` format + await searchPage.infiniteScroller.checkIncludedFacetedResults( + 'tile-title', + ['Movie', 'Data'], + true, + 10, + ); + }); +}); + +test(`Clear facet filters`, async ({ searchPage }) => { + await test.step(`Select "data" from inside "Media Type" facet group`, async () => { + await searchPage.collectionFacets.selectFacetByGroup( + FacetGroupLocatorLabel.MEDIATYPE, + 'data', + 'positive', + ); + }); + + await test.step(`Check the first 10 results for "Data" results`, async () => { + // checking the tileIcon title for now which is set in a `Title case` format + await searchPage.infiniteScroller.checkIncludedFacetedResults( + 'tile-title', + ['Data'], + true, + 10, + ); + }); + + await test.step(`Click "Clear all filters"`, async () => { + await searchPage.clickClearAllFilters(); + }); + + await test.step(`Assert "Clear all filters" is not visible`, async () => { + await searchPage.assertClearAllFiltersNotVisible(); + }); +}); + +test(`Select Year Published range via date picker`, async ({ searchPage }) => { + await test.step(`Enter 2014 in start date text field (leftmost text box)`, async () => { + // TODO: still not able to locate histogram date-input fields + await searchPage.collectionFacets.fillUpYearFilters('2014', '2015'); + }); + + await test.step('New results will be fetched', async () => { + await searchPage.collectionFacets.checkResultCount(); + }); + + // it's easier to check dates in list view mode + await test.step('Switch to list view mode', async () => { + await searchPage.infiniteScroller.clickViewMode(LayoutViewModeLocator.LIST); + }); + + await test.step(`Check the first 10 results Published texts are ONLY 2014 or 2015`, async () => { + await searchPage.infiniteScroller.checkIncludedFacetedResults( + 'list-date', + ['2014', '2015'], + true, + 10, + ); + }); +}); + +test(`Negative facet to exclude "audio"`, async ({ searchPage }) => { + await test.step(`Select "eye" icon near "audio" from inside "Media Type" facet group`, async () => { + await searchPage.collectionFacets.selectFacetByGroup( + FacetGroupLocatorLabel.MEDIATYPE, + 'audio', + 'negative', + ); + }); + + await test.step(`Check the first 7 results for "Audio" results`, async () => { + // checking the tileIcon title for now which is set in a `Title case` format + await searchPage.infiniteScroller.checkIncludedFacetedResults( + 'tile-title', + ['Audio'], + false, + 7, + ); + }); +}); + +test(`Filter for title beginning with "X"`, async ({ searchPage }) => { + test.info().annotations.push({ + type: 'Test', + description: 'This test is still incomplete', + }); + + await test.step(`Select "Title" from the sort bar`, async () => { + await searchPage.sortBar.applySortFilter('Title'); + }); + + await test.step(`Select "X" from alphabet picker`, async () => { + await searchPage.sortBar.clickAlphaBarLetterByPosition(23); + }); + + await test.step(`Results' titles ONLY begin with "X"`, async () => { + // TODO + }); +}); + +test(`Facets can be selected via "Select filters" modal`, async ({ + searchPage, +}) => { + await test.step(`Click "More" button under Media type facet group`, async () => { + await searchPage.collectionFacets.clickMoreInFacetGroup( + FacetGroupLocatorLabel.MEDIATYPE, + ); + }); + + await test.step(`Select "audio" and "texts" from inside "Media Type" facet group`, async () => { + await searchPage.collectionFacets.selectFacetsInModal(['audio', 'texts']); + }); + + await test.step(`Check the first 10 results for "Audio" & "Texts" results`, async () => { + // checking the tileIcon title for now which is set in a `Title case` format + await searchPage.infiniteScroller.checkIncludedFacetedResults( + 'tile-title', + ['Audio', 'Text'], + true, + 10, + ); + }); +}); diff --git a/tests/search/search-layout.spec.ts b/tests/search/search-layout.spec.ts new file mode 100644 index 00000000..e6debd36 --- /dev/null +++ b/tests/search/search-layout.spec.ts @@ -0,0 +1,117 @@ +import { test } from '../fixtures'; + +import { LayoutViewModeLocator } from '../models'; + +test('Tile, List, and Compact layout buttons change layout', async ({ + searchPage, +}) => { + await test.step('Click list view mode and check if it displays correctly', async () => { + await searchPage.infiniteScroller.clickViewMode(LayoutViewModeLocator.LIST); + await searchPage.infiniteScroller.assertLayoutViewModeChange('list'); + }); + + await test.step('Click compact view mode and check if it displays correctly', async () => { + await searchPage.infiniteScroller.clickViewMode( + LayoutViewModeLocator.COMPACT, + ); + await searchPage.infiniteScroller.assertLayoutViewModeChange('compact'); + }); + + await test.step('Click tile view mode and check if it displays correctly', async () => { + await searchPage.infiniteScroller.clickViewMode(LayoutViewModeLocator.TILE); + await searchPage.infiniteScroller.assertLayoutViewModeChange('tile'); + }); +}); + +test('Tile hover pane appears', async ({ searchPage }) => { + await test.step('Hover first item tile and check for title', async () => { + await searchPage.infiniteScroller.hoverToFirstItem(); + }); + + await test.step('Check title text inside tile-hover-pane and item-tile', async () => { + await searchPage.infiniteScroller.assertTileHoverPaneTitleIsSameWithItemTile(); + }); +}); + +test('Clicking on an item tile takes you to the item page', async ({ + searchPage, +}) => { + await test.step('Click first item result and check if it directs to details page', async () => { + await searchPage.infiniteScroller.clickFirstResultAndCheckRedirectToDetailsPage(); + }); +}); + +test('Sort by All-time views in Tile view', async ({ searchPage }) => { + await test.step('Switch to tile view mode', async () => { + await searchPage.infiniteScroller.clickViewMode(LayoutViewModeLocator.TILE); + }); + + await test.step('Sort by All-time views - descending order', async () => { + await searchPage.sortBar.applySortFilter('All-time views'); + await searchPage.sortBar.clickSortDirection('descending'); + }); + + await test.step('Check the first 10 results if sort filters were applied', async () => { + await searchPage.infiniteScroller.checkSortingResults( + 'All-time views', + 'descending', + 10, + ); + }); + + await test.step('Check if URL changed with correct sort filter and sort order param', async () => { + await searchPage.checkURLParamsWithSortFilter( + 'All-time views', + 'descending', + ); + }); +}); + +test('Sort by Date published in List view', async ({ searchPage }) => { + await test.step('Switch to list view mode', async () => { + await searchPage.infiniteScroller.clickViewMode(LayoutViewModeLocator.LIST); + }); + + await test.step('Sort by Date published - descending order', async () => { + await searchPage.sortBar.applySortFilter('Date published'); + await searchPage.sortBar.clickSortDirection('descending'); + }); + + await test.step('Check the first 10 results if sort filters were applied', async () => { + await searchPage.infiniteScroller.checkSortingResults( + 'Date published', + 'descending', + 10, + ); + }); + + await test.step('Check if URL changed with correct sort filter and sort order param', async () => { + await searchPage.checkURLParamsWithSortFilter( + 'Date published', + 'descending', + ); + }); +}); + +test('Sort by Date archived (ascending) in Compact view', async ({ + searchPage, +}) => { + await test.step('Switch to compact view mode', async () => { + await searchPage.infiniteScroller.clickViewMode( + LayoutViewModeLocator.COMPACT, + ); + }); + + await test.step('Sort by Date archived - ascending order', async () => { + await searchPage.sortBar.applySortFilter('Date archived'); + await searchPage.sortBar.clickSortDirection('ascending'); + }); + + await test.step('Check list column headers for sort filter', async () => { + await searchPage.checkCompactViewModeListLineDateHeaders('Date archived'); + }); + + await test.step('Check if URL changed with correct sort filter and sort order param', async () => { + await searchPage.checkURLParamsWithSortFilter('Date archived', 'ascending'); + }); +}); diff --git a/tests/search/search-page.spec.ts b/tests/search/search-page.spec.ts index 89d86838..6c72fd14 100644 --- a/tests/search/search-page.spec.ts +++ b/tests/search/search-page.spec.ts @@ -1,88 +1,121 @@ -import { test, expect } from '@playwright/test'; +import { test } from '@playwright/test'; -import { SearchPage } from './search-page'; - -import { SortBar } from '../shared/sort-bar'; +import { SearchOption } from '../models'; +import { SearchPage } from '../pageObjects/search-page'; let searchPage: SearchPage; -let sortBar: SortBar; -test.describe('Metadata - Search page results display', () => { +test.describe('Basic Search tests', () => { test.describe.configure({ mode: 'serial' }); - test('Go to search page and do a simple query', async ({ browser }) => { + test(`"Begin searching" page displays prior to searching`, async ({ + browser, + }) => { const browserPage = await browser.newPage(); searchPage = new SearchPage(browserPage); - sortBar = new SortBar(searchPage.page); - await searchPage.visit(); - await searchPage.search('cats'); + await test.step(`Go to archive.org/search URL`, async () => { + await searchPage.visit(); + }); + + await test.step(`Check if the empty page placeholder is displayed`, async () => { + await searchPage.checkEmptyPagePlaceholder(); + }); }); - test.describe('Facets navigation', async () => { - test('Should display result count and different facet groups', async () => { - await searchPage.checkFacetGroups(); + test('Do simple metadata search', async () => { + await test.step(`Select search option for metadata search`, async () => { + await searchPage.clickSearchInputOption(SearchOption.METADATA); }); - }) - test('Navigate through different search view modes', async () => { - await searchPage.navigateThruInfiniteScrollerViewModes(); - }); + await test.step(`Search for cats`, async () => { + await searchPage.queryFor('cats'); + }); - test.describe('Sorting results', async() => { - test('Sort by weekly views, descending order', async () => { - await searchPage.navigateSortBy('Weekly views', 'descending'); + await test.step(`Searching and search result count should be displayed`, async () => { + await searchPage.collectionFacets.checkResultCount(); }); + }); - test('Sort by relevance, descending order', async () => { - await searchPage.navigateSortBy('Relevance', 'descending'); + test('Do simple text contents search', async () => { + await test.step(`Select search option for text search`, async () => { + await searchPage.clickSearchInputOption(SearchOption.TEXT); }); - test('Sort by all-time views, ascending order', async () => { - await searchPage.navigateSortBy('All-time views', 'ascending'); + await test.step(`Search for dogs`, async () => { + await searchPage.queryFor('dogs'); }); - test('Sort by title, descending order', async() => { - await searchPage.navigateSortBy('Title', 'descending'); + await test.step(`Searching and search result count should be displayed`, async () => { + await searchPage.collectionFacets.checkResultCount(); }); + }); - test('Sort by date published, ascending order', async () => { - await searchPage.navigateSortBy('Date published', 'ascending'); + test('Do simple TV search', async () => { + await test.step(`Select search option for text search`, async () => { + await searchPage.clickSearchInputOption(SearchOption.TV); }); - test('Sort by date archived, ascending order', async () => { - await searchPage.navigateSortBy('Date archived', 'ascending'); + await test.step(`Search for iguanas`, async () => { + await searchPage.queryFor('iguanas'); }); - test('Sort by date reviewed, ascending order', async () => { - await searchPage.navigateSortBy('Date reviewed', 'ascending'); + await test.step(`Check TV page is displayed`, async () => { + await searchPage.checkTVPage('iguanas'); }); - test('Sort by date added, ascending order', async () => { - await searchPage.navigateSortBy('Date added', 'ascending'); + await test.step(`Go back to search page from TV search page`, async () => { + await searchPage.goBackToSearchPage(); }); + }); - test('Sort by creator, ascending order', async () => { - await searchPage.navigateSortBy('Creator', 'descending'); + test('Do simple radio search', async () => { + await test.step(`Select search option for text search`, async () => { + await searchPage.clickSearchInputOption(SearchOption.RADIO); }); - test('Sort by creator name that starts with letter B', async () => { - await sortBar.clickAlphaBarLetterByPosition(1); + await test.step(`Search for iguanas`, async () => { + await searchPage.queryFor('rabbits'); }); - test('Sort by creator name that starts with letter K', async () => { - await sortBar.clickAlphaBarLetterByPosition(10); + await test.step(`Check Radio search page is displayed`, async () => { + await searchPage.checkRadioPage('rabbits'); }); - test('Clear applied creator name letter sort filter', async () => { - await searchPage.clearAllFilters(); + await test.step(`Go back to search page from Radio search page`, async () => { + await searchPage.goBackToSearchPage(); }); }); - test.describe('Search type options', async () => { - test('Should display different collection search input options', async () => { - await searchPage.checkSearchInputOptions(); + test('Do simple web search', async () => { + await test.step(`Select search option for text search`, async () => { + await searchPage.clickSearchInputOption(SearchOption.WEB); + }); + + await test.step(`Search for parrots`, async () => { + await searchPage.queryFor('parrots'); + }); + + await test.step(`Check Wayback search page is displayed`, async () => { + await searchPage.checkWaybackPage('parrots'); + }); + + await test.step(`Go back to search page from Wayback search page`, async () => { + await searchPage.goBackToSearchPage(); }); }); + test('No results page displays when no results', async () => { + await test.step(`Search for a query that we expect will return no results at all`, async () => { + await searchPage.queryFor('catsshfksahfkjhfkjsdhfkiewhkdsfahkjhfkjsda'); + }); + + await test.step(`Check if the empty page placeholder is displayed`, async () => { + await searchPage.checkEmptyPagePlaceholder(); + }); + + await test.step('Close page browser after running all tests', async () => { + await searchPage.page.close(); + }); + }); }); diff --git a/tests/search/search-page.ts b/tests/search/search-page.ts deleted file mode 100644 index 9ffdfbd1..00000000 --- a/tests/search/search-page.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { type Page, type Locator, expect } from '@playwright/test'; - -import { CollectionFacets } from '../shared/collection-facets'; -import { InfiniteScroller } from '../shared/infiinite-scroller'; -import { SortBar } from '../shared/sort-bar'; - -export class SearchPage { - readonly url: string = 'https://archive.org/search'; - readonly page: Page; - readonly inputSearch: Locator; - readonly collectionSearchInput: Locator - readonly btnCollectionSearchInputGo: Locator; - readonly btnCollectionSearchInputCollapser: Locator; - readonly btnClearAllFilters: Locator; - - readonly collectionFacets: CollectionFacets; - readonly infiniteScroller: InfiniteScroller; - readonly sortBar: SortBar; - - public constructor(page: Page) { - this.page = page; - this.inputSearch = page.getByRole('textbox', { - name: 'Search the Archive. Filters and Advanced Search available below.' - }); - this.collectionSearchInput = page.locator('collection-search-input'); - this.btnCollectionSearchInputGo = page.locator('collection-search-input #go-button'); - this.btnCollectionSearchInputCollapser = page.locator('collection-search-input #button-collapser'); - this.btnClearAllFilters = page.locator('#facets-header-container .clear-filters-btn'); - - this.collectionFacets = new CollectionFacets(this.page); - this.infiniteScroller = new InfiniteScroller(this.page); - this.sortBar = new SortBar(this.page); - } - - async visit() { - await this.page.goto(this.url); - } - - async search(query: string) { - await this.inputSearch.fill(query); - await this.inputSearch.press('Enter'); - await this.page.waitForLoadState(); - } - - async displayResultCount () { - await this.collectionFacets.checkResultCount(); - } - - async checkFacetGroups() { - await this.displayResultCount(); - await this.collectionFacets.checkFacetGroups(); - } - - async navigateThruInfiniteScrollerViewModes () { - await this.infiniteScroller.clickGridView(); - await this.infiniteScroller.clickListView(); - await this.infiniteScroller.clickListCompactView(); - } - - async navigateSortBy (filter: string, direction: string) { - await this.sortBar.applySortBy(filter, direction); - await this.displayResultCount(); - } - - async clearAllFilters () { - await expect(this.btnClearAllFilters).toBeVisible(); - await this.btnClearAllFilters.click(); - await this.sortBar.clearAlphaBarFilter(); - await this.displayResultCount(); - await expect(this.btnClearAllFilters).not.toBeVisible(); - } - - async checkSearchInputOptions () { - await expect(this.collectionSearchInput).toBeVisible(); - await expect(this.btnCollectionSearchInputGo).toBeVisible(); - await expect(this.btnCollectionSearchInputCollapser).toBeVisible(); - - const options = this.btnCollectionSearchInputCollapser.locator('ul > li > label > span'); - await expect(options).toHaveText([ - `Search metadata`, - `Search text contents`, - `Search TV news captions`, - `Search radio transcripts`, - `Search archived web sites`, - ]); - } - -} diff --git a/tests/shared/collection-facets.ts b/tests/shared/collection-facets.ts deleted file mode 100644 index c0f62e26..00000000 --- a/tests/shared/collection-facets.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { type Page, type Locator, expect } from '@playwright/test'; - -export class CollectionFacets { - - readonly page: Page; - readonly collectionFacets: Locator; - - public constructor(page: Page) { - this.page = page; - this.collectionFacets = page.locator('collection-facets'); - } - - async checkResultCount() { - await expect(this.page.getByText('Searching')).toBeVisible(); - await this.page.waitForTimeout(5000); - await expect(this.page.getByText('Results')).toBeVisible(); - } - - async checkFacetGroups() { - const facetsContainer = this.collectionFacets.locator('#container'); - const facetGroups = facetsContainer.locator('section.facet-group'); - const headerTitles = facetsContainer.locator('h3'); - - // assert facet group header count - expect(await facetGroups.count()).toEqual(8); - expect(await headerTitles.count()).toEqual(8); - } - -} diff --git a/tests/shared/infiinite-scroller.ts b/tests/shared/infiinite-scroller.ts deleted file mode 100644 index 8955a3ce..00000000 --- a/tests/shared/infiinite-scroller.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { type Page, type Locator, expect } from '@playwright/test'; - -import { SortBar } from './sort-bar'; - -export class InfiniteScroller { - - readonly page: Page; - readonly infiniteScroller: Locator; - readonly displayStyleSelector: Locator; - readonly displayStyleSelectorOptions: Locator; - - readonly sortBar: SortBar; - - public constructor(page: Page) { - this.page = page; - this.sortBar = new SortBar(page); - - this.infiniteScroller = page.locator('infinite-scroller'); - - const sortBarSection = this.sortBar.sortFilterBar; - this.displayStyleSelector = sortBarSection.locator('div#display-style-selector'); - this.displayStyleSelectorOptions = this.displayStyleSelector.locator('ul > li'); - } - - async isViewModesVisible () { - // return await this.displayStyleSelector.isVisible(); - await expect(this.displayStyleSelector).toBeVisible(); - } - - async clickGridView() { - await this.displayStyleSelectorOptions.nth(0).click(); - expect(await expect(this.infiniteScroller).toHaveClass(/grid/)); - await expect(this.infiniteScroller.locator('item-tile').first()).toBeVisible(); - await expect(this.infiniteScroller.locator('tile-list').first()).not.toBeVisible(); - await expect(this.infiniteScroller.locator('tile-list-compact').first()).not.toBeVisible(); - } - - async clickListView() { - await this.displayStyleSelectorOptions.nth(1).click(); - await expect(this.infiniteScroller).toHaveClass(/list-detail/); - await expect(this.infiniteScroller.locator('item-tile').first()).not.toBeVisible(); - await expect(this.infiniteScroller.locator('tile-list').first()).toBeVisible(); - await expect(this.infiniteScroller.locator('tile-list-compact').first()).not.toBeVisible(); - } - - async clickListCompactView() { - await this.displayStyleSelectorOptions.nth(2).click(); - await expect(this.infiniteScroller).toHaveClass(/list-compact/); - await expect(this.infiniteScroller.locator('item-tile').first()).not.toBeVisible(); - await expect(this.infiniteScroller.locator('tile-list').first()).not.toBeVisible(); - await expect(this.infiniteScroller.locator('tile-list-compact').first()).toBeVisible(); - } - -} diff --git a/tests/utils.ts b/tests/utils.ts new file mode 100644 index 00000000..4fad5f7f --- /dev/null +++ b/tests/utils.ts @@ -0,0 +1,24 @@ +import { SortOrder, DateMetadataLabel } from './models'; + +export function viewsSorted(order: SortOrder, arr: Number[]): boolean { + if (order === 'ascending') { + return arr.every((x, i) => i === 0 || x >= arr[i - 1]); + } else { + return arr.every((x, i) => i === 0 || x <= arr[i - 1]); + } +} + +export function datesSorted( + order: SortOrder, + arr: DateMetadataLabel[], +): boolean { + if (order === 'ascending') { + return arr.every( + (x, i) => i === 0 || new Date(x.date) >= new Date(arr[i - 1].date), + ); + } else { + return arr.every( + (x, i) => i === 0 || new Date(x.date) <= new Date(arr[i - 1].date), + ); + } +}