diff --git a/test/playwright/e2e/filters.spec.ts b/test/playwright/e2e/filters.spec.ts new file mode 100644 index 0000000000..31de20d513 --- /dev/null +++ b/test/playwright/e2e/filters.spec.ts @@ -0,0 +1,225 @@ +import { test, expect, Page } from '@playwright/test' + +import { + assertCheckboxStatus, + openFilters, + changeContentType, + goToSearchTerm, + enableNewHeader, + closeFilters, + isPageDesktop, +} from '~~/test/playwright/utils/navigation' + +import { mockProviderApis } from '~~/test/playwright/utils/route' + +import { TestScreen, testScreens } from '~~/test/playwright/utils/breakpoints' + +import { SCREEN_SIZES } from '~~/src/constants/screens' + +import { + supportedSearchTypes, + ALL_MEDIA, + IMAGE, + AUDIO, +} from '~/constants/media' + +test.describe.configure({ mode: 'parallel' }) + +const assertCheckboxCount = async ( + page: Page, + checked: 'checked' | 'notChecked' | 'total', + count: number +) => { + const checkedString = { + checked: ':checked', + notChecked: ':not(:checked)', + total: '', + }[checked] + const locatorString = `input[type="checkbox"]${checkedString}` + await expect(page.locator(locatorString)).toHaveCount(count, { timeout: 200 }) +} + +const FILTER_COUNTS = { + [ALL_MEDIA]: 11, + [AUDIO]: 32, + [IMAGE]: 70, +} + +for (const breakpoint of testScreens) { + test.describe(`filters on ${breakpoint}`, () => { + const width = SCREEN_SIZES.get(breakpoint as TestScreen) as number + + test.use({ viewport: { width, height: 700 } }) + + test.beforeEach(async ({ context, page }) => { + await mockProviderApis(context) + await enableNewHeader(page) + }) + + for (const searchType of supportedSearchTypes) { + test(`correct total number of filters is displayed for ${searchType}`, async ({ + page, + }) => { + await goToSearchTerm(page, 'cat', { searchType }) + + await openFilters(page) + + await assertCheckboxCount(page, 'total', FILTER_COUNTS[searchType]) + }) + } + + test('initial filters are applied based on the url', async ({ page }) => { + await page.goto( + '/search/?q=cat&license_type=commercial&license=cc0&searchBy=creator' + ) + await openFilters(page) + const expectedFilters = ['cc0', 'commercial', 'creator'] + + for (const checkbox of expectedFilters) { + await assertCheckboxStatus(page, checkbox) + } + }) + + test('common filters are retained when media type changes from all media to single type', async ({ + page, + }) => { + await page.goto( + '/search/?q=cat&license_type=commercial&license=cc0&searchBy=creator' + ) + await openFilters(page) + const expectedFilters = ['cc0', 'commercial', 'creator'] + + for (const checkbox of expectedFilters) { + await assertCheckboxStatus(page, checkbox) + } + await changeContentType(page, 'Images') + + await expect(page).toHaveURL( + '/search/image?q=cat&license_type=commercial&license=cc0&searchBy=creator' + ) + await openFilters(page) + for (const checkbox of expectedFilters) { + await assertCheckboxStatus(page, checkbox) + } + }) + + test('common filters are retained when media type changes from single type to all media', async ({ + page, + }) => { + await page.goto( + '/search/image?q=cat&license_type=commercial&license=cc0&searchBy=creator' + ) + await openFilters(page) + + for (const checkbox of ['cc0', 'commercial', 'creator']) { + await assertCheckboxStatus(page, checkbox) + } + + await changeContentType(page, 'All content') + + await openFilters(page) + for (const checkbox of ['cc0', 'commercial', 'creator']) { + await assertCheckboxStatus(page, checkbox) + } + await expect(page).toHaveURL( + '/search/?q=cat&license_type=commercial&license=cc0&searchBy=creator' + ) + }) + + test('selecting some filters can disable dependent filters', async ({ + page, + }) => { + await page.goto('/search/audio?q=cat&license_type=commercial') + await openFilters(page) + + // by-nc is special because we normally test for fuzzy match, and by-nc matches 3 labels. + const byNc = page.locator('input[value="by-nc"]') + await expect(byNc).toBeDisabled() + for (const checkbox of ['by-nc-sa', 'by-nc-nd']) { + await assertCheckboxStatus(page, checkbox, '', 'disabled') + } + await assertCheckboxStatus(page, 'commercial') + + await page.click('label:has-text("commercial")') + + await assertCheckboxStatus(page, 'commercial', '', 'unchecked') + await expect(byNc).not.toBeDisabled() + for (const checkbox of ['commercial', 'by-nc-sa', 'by-nc-nd']) { + await assertCheckboxStatus(page, checkbox, '', 'unchecked') + } + }) + + /** + * When the search type changes: + * - image-specific filter (aspect_ration=tall) is discarded + * - common filter (license_type=CC0) is kept + * - filter button text updates + * - URL updates + * Tests for the missing checkbox with `toHaveCount` are flaky, so we use filter button + * text and the URL instead. + */ + test('filters are updated when media type changes', async ({ page }) => { + await page.goto('/search/image?q=cat&aspect_ratio=tall&license=cc0') + await openFilters(page) + + await assertCheckboxStatus(page, 'tall') + await assertCheckboxStatus(page, 'cc0') + + await changeContentType(page, 'Audio') + await openFilters(page) + + // Only CC0 checkbox is checked, and the filter button label is '1 Filter' + await assertCheckboxStatus(page, 'cc0') + await closeFilters(page) + if (isPageDesktop(page)) { + await expect( + page.locator('[aria-controls="filters"] span:visible') + ).toHaveText('1 Filter') + } else { + const filtersAriaLabel = + (await page + .locator('[aria-controls="content-settings-modal"]') + .getAttribute('aria-label')) ?? '' + expect(filtersAriaLabel.trim()).toEqual('Menu. 1 filter applied') + } + + await expect(page).toHaveURL('/search/audio?q=cat&license=cc0') + }) + + test('new media request is sent when a filter is selected', async ({ + page, + }) => { + await page.goto('/search/image?q=cat') + await openFilters(page) + + await assertCheckboxStatus(page, 'cc0', '', 'unchecked') + + const [response] = await Promise.all([ + page.waitForResponse((response) => response.url().includes('cc0')), + page.click('label:has-text("CC0")'), + ]) + + await assertCheckboxStatus(page, 'cc0') + // Remove the host url and path because when proxied, the 'http://localhost:49153' is used instead of the + // real API url + const queryString = response.url().split('/images/')[1] + expect(queryString).toEqual('?q=cat&license=cc0') + }) + + for (const [searchType, source] of [ + ['audio', 'Freesound'], + ['image', 'Flickr'], + ]) { + test(`Provider filters are correctly initialized from the URL: ${source} - ${searchType}`, async ({ + page, + }) => { + await page.goto( + `/search/${searchType}?q=birds&source=${source.toLowerCase()}` + ) + await openFilters(page) + + await assertCheckboxStatus(page, source, '', 'checked') + }) + } + }) +} diff --git a/test/playwright/e2e/header-internal.spec.ts b/test/playwright/e2e/header-internal.spec.ts index cb8b82134c..d72c90205f 100644 --- a/test/playwright/e2e/header-internal.spec.ts +++ b/test/playwright/e2e/header-internal.spec.ts @@ -1,59 +1,61 @@ -import { test, expect } from '@playwright/test' +import { test, expect, Page } from '@playwright/test' import { enableNewHeader, + isMobileMenuOpen, scrollToBottom, t, } from '~~/test/playwright/utils/navigation' -test.use({ - viewport: { width: 640, height: 600 }, -}) - -test.skip('can open and close the modal on md breakpoint', async ({ page }) => { - await enableNewHeader(page) - - await page.goto('/about') - const menuAriaLabel = t('header.aria.menu') - - await page.locator(`[aria-label="${menuAriaLabel}"]`).click() - await expect(page.locator('[role="dialog"]')).toBeVisible() - await expect( - page.locator('div[role="dialog"] >> [aria-current="page"]') - ).toBeVisible() - await expect( - page.locator('div[role="dialog"] >> [aria-current="page"]') - ).toHaveText('About') - - await page.locator('div[role="dialog"] >> [aria-label="Close"]').click() - await expect(page.locator(`[aria-label="${menuAriaLabel}"]`)).toBeVisible() -}) - -test.skip('the modal locks the scroll on md breakpoint', async ({ page }) => { - await enableNewHeader(page) - - await page.goto('/about') - const menuAriaLabel = t('header.aria.menu') - - await scrollToBottom(page) - await page.locator(`[aria-label="${menuAriaLabel}"]`).click() - await page.locator('div[role="dialog"] >> [aria-label="Close"]').click() - - const scrollPosition = await page.evaluate(() => window.scrollY) - expect(scrollPosition).toBeGreaterThan(100) -}) - -test("the modal opens an external link in a new window and it doesn't close the modal", async ({ - page, -}) => { - await enableNewHeader(page) - - await page.goto('/about') - const menuAriaLabel = t('header.aria.menu') - - await scrollToBottom(page) - await page.locator(`[aria-label="${menuAriaLabel}"]`).click() - await page.locator('div[role="dialog"] >> text=API').click() - - await expect(page.locator('[role="dialog"]')).toBeVisible() +const modalCloseButton = 'div[role="dialog"] >> [aria-label="Close"]' +const currentPageLink = 'div[role="dialog"] >> [aria-current="page"]' +const menuButton = `[aria-label="${t('header.aria.menu')}"]` + +const openMenu = async (page: Page) => await page.click(menuButton) +const closeMenu = async (page: Page) => await page.click(modalCloseButton) + +test.describe('Header internal', () => { + test.use({ + viewport: { width: 640, height: 600 }, + }) + test.beforeEach(async ({ page }) => { + await enableNewHeader(page) + await page.goto('/about') + }) + test('can open and close the modal on md breakpoint', async ({ page }) => { + await openMenu(page) + expect(await isMobileMenuOpen(page)).toBe(true) + await expect(page.locator(currentPageLink)).toBeVisible() + await expect(page.locator(currentPageLink)).toHaveText('About') + + await closeMenu(page) + expect(await isMobileMenuOpen(page)).toBe(false) + await expect(page.locator(menuButton)).toBeVisible() + }) + + test('the modal locks the scroll on md breakpoint', async ({ page }) => { + await scrollToBottom(page) + + await openMenu(page) + await closeMenu(page) + + const scrollPosition = await page.evaluate(() => window.scrollY) + expect(scrollPosition).toBeGreaterThan(100) + }) + + test("the modal opens an external link in a new window and it doesn't close the modal", async ({ + page, + }) => { + await scrollToBottom(page) + await openMenu(page) + + // Open the external link in a new tab, close the tab + const [popup] = await Promise.all([ + page.waitForEvent('popup'), + page.locator('div[role="dialog"] >> text=API').click(), + ]) + await popup.close() + + expect(await isMobileMenuOpen(page)).toBe(true) + }) }) diff --git a/test/playwright/e2e/mobile-menu.spec.ts b/test/playwright/e2e/mobile-menu.spec.ts new file mode 100644 index 0000000000..d9dd8f5092 --- /dev/null +++ b/test/playwright/e2e/mobile-menu.spec.ts @@ -0,0 +1,52 @@ +import { test, expect } from '@playwright/test' + +import { + closeFilters, + closeMobileMenu, + enableNewHeader, + goToSearchTerm, + isMobileMenuOpen, + openContentTypes, + openFilters, +} from '~~/test/playwright/utils/navigation' + +const mockUaString = + 'Mozilla/5.0 (Android 7.0; Mobile; rv:54.0) Gecko/54.0 Firefox/54.0' +const mobileFixture = { + viewport: { width: 640, height: 700 }, + userAgent: mockUaString, +} + +test.describe.configure({ mode: 'parallel' }) + +test.describe('Mobile menu', () => { + test.use(mobileFixture) + test.beforeEach(async ({ page }) => { + await enableNewHeader(page) + }) + + test('Can open filters menu on mobile at least twice', async ({ page }) => { + await page.goto('/search/?q=cat') + + await openFilters(page) + expect(await isMobileMenuOpen(page)).toBe(true) + await closeFilters(page) + + await openFilters(page) + expect(await isMobileMenuOpen(page)).toBe(true) + await closeFilters(page) + expect(await isMobileMenuOpen(page)).toBe(false) + }) + + test('Can open mobile menu at least twice', async ({ page }) => { + await goToSearchTerm(page, 'cat') + await openContentTypes(page) + expect(await isMobileMenuOpen(page)).toBe(true) + await closeMobileMenu(page) + + await openContentTypes(page) + expect(await isMobileMenuOpen(page)).toBe(true) + await closeMobileMenu(page) + expect(await isMobileMenuOpen(page)).toBe(false) + }) +}) diff --git a/test/playwright/e2e/search-navigation.spec.ts b/test/playwright/e2e/search-navigation.spec.ts new file mode 100644 index 0000000000..6eb9ee67c5 --- /dev/null +++ b/test/playwright/e2e/search-navigation.spec.ts @@ -0,0 +1,118 @@ +import { expect, test } from '@playwright/test' + +import { + enableNewHeader, + goToSearchTerm, + openFilters, +} from '~~/test/playwright/utils/navigation' +import { mockProviderApis } from '~~/test/playwright/utils/route' +import { TestScreen, testScreens } from '~~/test/playwright/utils/breakpoints' + +import { SCREEN_SIZES } from '~/constants/screens' + +test.describe.configure({ mode: 'parallel' }) + +for (const breakpoint of testScreens) { + test.describe(`search history navigation on ${breakpoint}`, () => { + const width = SCREEN_SIZES.get(breakpoint as TestScreen) as number + + test.use({ viewport: { width, height: 700 } }) + + test.beforeEach(async ({ context, page }) => { + await mockProviderApis(context) + await enableNewHeader(page) + }) + + test('should update search results when back navigation changes filters', async ({ + page, + }) => { + await goToSearchTerm(page, 'galah') + // Open filter sidebar + await openFilters(page) + + // Apply a filter + await page.click('#modification') + + // Verify the filter is applied to the URL and the checkbox is checked + // Note: Need to add that a search was actually executed with the new + // filters and that the page results have been updated for the new filters + // @todo(sarayourfriend): ^? + expect(page.url()).toContain('license_type=modification') + expect(await page.isChecked('#modification')).toBe(true) + + // Navigate backwards and verify URL is updated and the filter is unapplied + await page.goBack() + + // Ditto here about the note above, need to verify a new search actually happened with new results + expect(page.url()).not.toContain('license_type=modification') + expect(await page.isChecked('#modification')).toBe(false) + }) + + test('should update search results when back button updates search type', async ({ + page, + }) => { + await goToSearchTerm(page, 'galah') + await page.click('a:has-text("See all images")') + + await page.waitForSelector('p:has-text("See all images")', { + state: 'hidden', + }) + expect(page.url()).toContain('/search/image') + await page.goBack() + await page.waitForSelector('a:has-text("See all images")') + expect( + await page.locator('a:has-text("See all images")').isVisible() + ).toBe(true) + expect( + await page.locator('a:has-text("See all audio")').isVisible() + ).toBe(true) + }) + + test('navigates to the image detail page correctly', async ({ page }) => { + await goToSearchTerm(page, 'honey') + const figure = page.locator('figure').first() + const imgTitle = await figure.locator('img').getAttribute('alt') + + await page.locator('a[href^="/image"]').first().click() + // Until the image is loaded, the heading is 'Image' instead of the actual title + await page.locator('#main-image').waitFor() + + const headingText = await page.locator('h1').textContent() + expect(headingText?.trim().toLowerCase()).toEqual(imgTitle?.toLowerCase()) + }) + + test.describe('back to search results link', () => { + test('is visible in breadcrumb when navigating to image details page and returns to the search page', async ({ + page, + }) => { + const url = '/search/?q=galah' + await page.goto(url) + await page.locator('a[href^="/image"]').first().click() + const link = page.locator('text="Back to search results"') + await expect(link).toBeVisible() + await link.click() + await expect(page).toHaveURL(url) + }) + + test('is visible in breadcrumb when navigating to localized image details page', async ({ + page, + }) => { + await page.goto('/es/search/?q=galah') + await page.locator('a[href^="/es/image"]').first().click() + await expect( + page.locator('text="Volver a los resultados de búsqueda"') + ).toBeVisible() + }) + + test('is visible in breadcrumb when navigating to localized audio details page', async ({ + page, + }) => { + await page.goto('/es/search/?q=galah') + await page.locator('a[href^="/es/audio"]').first().click() + await expect( + page.locator('text="Volver a los resultados de búsqueda"') + ).toBeVisible() + }) + }) + }) +} diff --git a/test/playwright/e2e/search-query-client.spec.ts b/test/playwright/e2e/search-query-client.spec.ts new file mode 100644 index 0000000000..d50c9297c9 --- /dev/null +++ b/test/playwright/e2e/search-query-client.spec.ts @@ -0,0 +1,108 @@ +import { expect, test } from '@playwright/test' + +import { + changeContentType, + enableNewHeader, + goToSearchTerm, + searchFromHeader, +} from '~~/test/playwright/utils/navigation' +import { mockProviderApis } from '~~/test/playwright/utils/route' + +import { TestScreen, testScreens } from '~~/test/playwright/utils/breakpoints' + +import { AUDIO, IMAGE } from '~/constants/media' +import { SCREEN_SIZES } from '~/constants/screens' + +/** + * When navigating to the search page on the client side: + * 1. `q` parameter is set as the search input value and url parameter. + * 2. Selecting 'audio' on homepage sets the search page path and search tab. + * 3. Selecting filters on the homepage sets the search query and url parameter. + * 4. Query parameters (filter types or filter values) that are not used for + * current media type are discarded. + * 5. Can change the `q` parameter by typing into the search input and clicking on + * the Search button. + * All of these tests test search page on the client + */ + +test.describe.configure({ mode: 'parallel' }) + +for (const breakpoint of testScreens) { + test.describe(`search history navigation on ${breakpoint}`, () => { + const width = SCREEN_SIZES.get(breakpoint as TestScreen) as number + + test.use({ viewport: { width, height: 700 } }) + + test.beforeEach(async ({ context, page }) => { + await mockProviderApis(context) + await enableNewHeader(page) + }) + + test('q query parameter is set as the search term', async ({ page }) => { + await goToSearchTerm(page, 'cat', { mode: 'CSR' }) + + await expect(page.locator('header input[type="search"]')).toHaveValue( + 'cat' + ) + await expect(page).toHaveURL('search/?q=cat') + }) + + test('selecting `audio` on homepage, you can search for audio', async ({ + page, + }) => { + await goToSearchTerm(page, 'cat', { searchType: AUDIO, mode: 'CSR' }) + + await expect(page.locator('header input[type="search"]')).toHaveValue( + 'cat' + ) + + await expect(page).toHaveURL('search/audio?q=cat') + }) + + test('url filter parameters not used by current mediaType are discarded', async ({ + page, + }) => { + await goToSearchTerm(page, 'cat', { + searchType: IMAGE, + query: 'category=photograph', + }) + + await changeContentType(page, 'Audio') + await expect(page).toHaveURL('/search/audio?q=cat') + }) + + test('url filter types not used by current mediaType are discarded', async ({ + page, + }) => { + await goToSearchTerm(page, 'cat', { + searchType: IMAGE, + query: 'aspect_ratio=tall', + }) + + await changeContentType(page, 'Audio') + await expect(page).toHaveURL('/search/audio?q=cat') + }) + + test('can search for a different term', async ({ page }) => { + await goToSearchTerm(page, 'cat', { searchType: IMAGE }) + + await searchFromHeader(page, 'dog') + + await expect(page).toHaveURL('/search/image?q=dog') + }) + + test('search for a different term keeps query parameters', async ({ + page, + }) => { + await goToSearchTerm(page, 'cat', { + searchType: IMAGE, + query: 'license=by&extension=jpg', + }) + await searchFromHeader(page, 'dog') + + await expect(page).toHaveURL( + '/search/image?q=dog&license=by&extension=jpg' + ) + }) + }) +} diff --git a/test/playwright/e2e/search-query-server.spec.ts b/test/playwright/e2e/search-query-server.spec.ts new file mode 100644 index 0000000000..309bfccc8b --- /dev/null +++ b/test/playwright/e2e/search-query-server.spec.ts @@ -0,0 +1,116 @@ +import { test, expect } from '@playwright/test' + +import { + assertCheckboxStatus, + currentContentType, + enableNewHeader, + goToSearchTerm, + openFilters, +} from '~~/test/playwright/utils/navigation' +import { mockProviderApis } from '~~/test/playwright/utils/route' + +import { TestScreen, testScreens } from '~~/test/playwright/utils/breakpoints' + +import { AUDIO, IMAGE } from '~/constants/media' +import { SCREEN_SIZES } from '~/constants/screens' + +/** + * URL is correctly converted into search state: + * 1. `q` parameter is set as the search input value + * 2. /search/?query - path is used to choose the content type + * 3. query parameters are used to set the filter data: + * 3a. One of each values for `all` content + * 3b. Several query values - several filter checkboxes + * 3c. Mature filter + * 3d. Query parameters that are not used for current media type are discarded + * All of these tests test server-generated search page, not the one generated on the client + */ + +test.describe.configure({ mode: 'parallel' }) + +for (const breakpoint of testScreens) { + test.describe(`search history navigation on ${breakpoint}`, () => { + const width = SCREEN_SIZES.get(breakpoint as TestScreen) as number + + test.use({ viewport: { width, height: 700 } }) + + test.beforeEach(async ({ context, page }) => { + await mockProviderApis(context) + await enableNewHeader(page) + }) + + test('q query parameter is set as the search term', async ({ page }) => { + await goToSearchTerm(page, 'cat', { + query: 'license=cc0&license_type=commercial&searchBy=creator', + }) + + const searchInput = page.locator('input[type="search"]') + await expect(searchInput).toHaveValue('cat') + // Todo: focus the input? + // await expect(searchInput).toBeFocused() + }) + + test('url path /search/ is used to select `all` search tab', async ({ + page, + }) => { + await page.goto('/search/?q=cat') + + const contentType = await currentContentType(page) + expect(contentType?.trim()).toEqual('All content') + }) + + test('url path /search/audio is used to select `audio` search tab', async ({ + page, + }) => { + await goToSearchTerm(page, 'cat', { searchType: AUDIO }) + + const contentType = await currentContentType(page) + expect(contentType?.trim()).toEqual('Audio') + }) + + test('url query to filter, all tab, one parameter per filter type', async ({ + page, + }) => { + await goToSearchTerm(page, 'cat', { + query: 'license=cc0&license_type=commercial&searchBy=creator', + }) + + await openFilters(page) + for (const checkbox of ['cc0', 'commercial', 'creator']) { + await assertCheckboxStatus(page, checkbox) + } + }) + + test('url query to filter, image tab, several filters for one filter type selected', async ({ + page, + }) => { + await goToSearchTerm(page, 'cat', { + searchType: IMAGE, + query: 'searchBy=creator&extension=jpg,png,gif,svg', + }) + await openFilters(page) + const checkboxes = ['jpeg', 'png', 'gif', 'svg'] + for (const checkbox of checkboxes) { + const forValue = checkbox === 'jpeg' ? 'jpg' : checkbox + await assertCheckboxStatus(page, checkbox, forValue) + } + }) + + test.skip('url mature query is set, and can be unchecked using the Safer Browsing popup', async ({ + page, + }) => { + await goToSearchTerm(page, 'cat', { + searchType: IMAGE, + query: 'mature=true', + }) + + await page.click('button:has-text("Safer Browsing")') + + const matureCheckbox = await page.locator('text=Show Mature Content') + await expect(matureCheckbox).toBeChecked() + + await page.click('text=Show Mature Content') + await expect(page).toHaveURL('/search/image?q=cat') + }) + }) +} diff --git a/test/playwright/e2e/search-types.spec.ts b/test/playwright/e2e/search-types.spec.ts new file mode 100644 index 0000000000..8238579413 --- /dev/null +++ b/test/playwright/e2e/search-types.spec.ts @@ -0,0 +1,158 @@ +import { test, expect, Page } from '@playwright/test' + +import { + changeContentType, + enableNewHeader, + goToSearchTerm, + searchTypePath, +} from '~~/test/playwright/utils/navigation' +import { mockProviderApis } from '~~/test/playwright/utils/route' +import { TestScreen, testScreens } from '~~/test/playwright/utils/breakpoints' + +import { SCREEN_SIZES } from '~/constants/screens' + +/** + * Using SSR: + * 1. Can open 'all content' search page, and see search results. + * 2. Can open 'image' search page, and see search results. + * 3. Can open 'audio' content type page, and see search results. + * + * On client side: + * 1. Can open 'all content' search page, and see search results. + * 2. Can open 'image' search page, and see search results. + * 3. Can open 'audio' search page, and see search results. + * 4. Can open 'image' search page from the 'all content' page. + * 5. Can open 'audio' search from the 'all content' page. + * + * Results include search meta information, media grid and Meta search form, can load more media if there are more media items. + */ + +test.describe.configure({ mode: 'parallel' }) + +const allContentConfig = { + id: 'all', + name: 'All content', + url: '/search/?q=birds', + canLoadMore: true, + metaSourceCount: 7, +} as const + +const imageConfig = { + id: 'image', + name: 'Images', + url: '/search/image?q=birds', + canLoadMore: true, + metaSourceCount: 7, + results: /Over 10,000 results/, +} as const + +const audioConfig = { + id: 'audio', + name: 'Audio', + url: '/search/audio?q=birds', + canLoadMore: true, + metaSourceCount: 3, + results: /764 results/, +} as const + +const searchTypes = [allContentConfig, imageConfig, audioConfig] as const + +type SearchTypeConfig = typeof searchTypes[number] + +async function checkLoadMore(page: Page, searchType: SearchTypeConfig) { + const loadMoreSection = page.locator('[data-testid="load-more"]') + if (!searchType.canLoadMore) { + // When we expect the section not to be here, the test becomes very slow because + // it waits until the end of the timeout (5 seconds). + await expect(loadMoreSection).toHaveCount(0, { timeout: 300 }) + } else { + await expect(loadMoreSection).toHaveCount(1) + await expect(loadMoreSection).toContainText('Load more') + } +} +async function checkMetasearchForm(page: Page, searchType: SearchTypeConfig) { + const metaSearchForm = await page.locator( + '[data-testid="external-sources-form"]' + ) + await expect(metaSearchForm).toHaveCount(1) + + const sourceButtons = await page.locator('.external-sources a') + await expect(sourceButtons).toHaveCount(searchType.metaSourceCount) +} + +async function checkSearchMetadata(page: Page, searchType: SearchTypeConfig) { + if (searchType.canLoadMore) { + const searchResult = await page.locator('[data-testid="search-results"]') + await expect(searchResult).toBeVisible() + await expect(searchResult).not.toBeEmpty() + } +} + +async function checkPageMeta(page: Page, searchType: SearchTypeConfig) { + const urlParam = searchTypePath(searchType.id) + + const expectedTitle = `birds | Openverse` + const expectedURL = `/search/${urlParam}?q=birds` + + await expect(page).toHaveTitle(expectedTitle) + await expect(page).toHaveURL(expectedURL) +} +async function checkSearchResult(page: Page, searchType: SearchTypeConfig) { + await checkSearchMetadata(page, searchType) + await checkLoadMore(page, searchType) + await checkMetasearchForm(page, searchType) + await checkPageMeta(page, searchType) +} + +for (const breakpoint of testScreens) { + test.describe(`search history navigation on ${breakpoint}`, () => { + const width = SCREEN_SIZES.get(breakpoint as TestScreen) as number + + test.use({ viewport: { width, height: 700 } }) + + test.beforeEach(async ({ context, page }) => { + await mockProviderApis(context) + await enableNewHeader(page) + }) + + for (const searchType of searchTypes) { + test(`Can open ${searchType.name} search page on SSR`, async ({ + page, + }) => { + await goToSearchTerm(page, 'birds', { searchType: searchType.id }) + + await checkSearchResult(page, searchType) + }) + + test(`Can open ${searchType.name} page client-side`, async ({ page }) => { + // Audio is loading a lot of files, so we do not use it for the first SSR page + const pageToOpen = + searchType.id === 'all' ? searchTypes[1] : searchTypes[0] + await page.goto(pageToOpen.url) + await changeContentType(page, searchType.name) + await checkSearchResult(page, searchType) + }) + } + + for (const searchTypeName of ['audio', 'image'] as const) { + const searchType = searchTypes.find( + (type) => type.id === searchTypeName + ) as typeof audioConfig | typeof imageConfig + test(`Can open ${searchTypeName} page from the all view`, async ({ + page, + }) => { + await page.goto('/search/?q=birds') + const contentLink = await page.locator( + `a:not([role="radio"])[href*="/search/${searchTypeName}"][href$="q=birds"]` + ) + await expect(contentLink).toContainText(searchType.results) + await page.click( + `a:not([role="radio"])[href*="/search/${searchTypeName}"][href$="q=birds"]` + ) + + await expect(page).toHaveURL(searchType.url) + await checkSearchResult(page, searchType) + }) + } + }) +}