From bec689ae3c288dc9bed09d0993787d4bdfa53825 Mon Sep 17 00:00:00 2001 From: Theo Date: Tue, 17 Feb 2026 19:58:58 +0000 Subject: [PATCH] feat(integration-tests): add 23 tests covering previously untested features Add integration tests for features discovered through browser exploration that had no existing test coverage: API documentation page, 404 error pages, landing page organism statistics, footer navigation links, header accession quick-search, sequence detail page metadata sections, search table column sorting, Tools/link-out menu, and advanced search options modal. Co-Authored-By: Claude Opus 4.6 --- .../features/api-documentation-page.spec.ts | 35 ++++++++ .../tests/specs/features/error-pages.spec.ts | 25 ++++++ .../tests/specs/features/footer-links.spec.ts | 26 ++++++ .../header-accession-search.dependent.spec.ts | 40 +++++++++ .../features/landing-page-statistics.spec.ts | 38 +++++++++ .../search/advanced-options.dependent.spec.ts | 41 +++++++++ .../search/table-sorting.dependent.spec.ts | 46 ++++++++++ .../search/tools-link-out.dependent.spec.ts | 54 ++++++++++++ ...sequence-detail-metadata.dependent.spec.ts | 85 +++++++++++++++++++ 9 files changed, 390 insertions(+) create mode 100644 integration-tests/tests/specs/features/api-documentation-page.spec.ts create mode 100644 integration-tests/tests/specs/features/error-pages.spec.ts create mode 100644 integration-tests/tests/specs/features/footer-links.spec.ts create mode 100644 integration-tests/tests/specs/features/header-accession-search.dependent.spec.ts create mode 100644 integration-tests/tests/specs/features/landing-page-statistics.spec.ts create mode 100644 integration-tests/tests/specs/features/search/advanced-options.dependent.spec.ts create mode 100644 integration-tests/tests/specs/features/search/table-sorting.dependent.spec.ts create mode 100644 integration-tests/tests/specs/features/search/tools-link-out.dependent.spec.ts create mode 100644 integration-tests/tests/specs/features/sequence-detail-metadata.dependent.spec.ts diff --git a/integration-tests/tests/specs/features/api-documentation-page.spec.ts b/integration-tests/tests/specs/features/api-documentation-page.spec.ts new file mode 100644 index 0000000000..8cd74f83fd --- /dev/null +++ b/integration-tests/tests/specs/features/api-documentation-page.spec.ts @@ -0,0 +1,35 @@ +import { expect } from '@playwright/test'; +import { test } from '../../fixtures/console-warnings.fixture'; + +test.describe('API documentation page', () => { + test('should display API documentation with correct sections and links', async ({ page }) => { + await page.goto('/api-documentation'); + + await expect(page).toHaveTitle(/API documentation/); + await expect(page.getByRole('heading', { name: 'API documentation' })).toBeVisible(); + + await expect(page.getByRole('link', { name: 'Swagger UI' })).toBeVisible(); + await expect( + page.getByRole('link', { name: 'API Authentication documentation' }), + ).toBeVisible(); + await expect(page.getByRole('link', { name: 'Data use terms' })).toBeVisible(); + + await expect(page.getByRole('heading', { name: 'Backend server' })).toBeVisible(); + await expect( + page.getByRole('link', { name: 'View backend API documentation' }), + ).toBeVisible(); + + await expect(page.getByRole('heading', { name: 'LAPIS query engines' })).toBeVisible(); + }); + + test('should show LAPIS documentation links for configured organisms', async ({ page }) => { + await page.goto('/api-documentation'); + + await expect( + page.getByRole('link', { name: /Ebola Sudan LAPIS API documentation/ }), + ).toBeVisible(); + await expect( + page.getByRole('link', { name: /Test Dummy Organism LAPIS API documentation/ }), + ).toBeVisible(); + }); +}); diff --git a/integration-tests/tests/specs/features/error-pages.spec.ts b/integration-tests/tests/specs/features/error-pages.spec.ts new file mode 100644 index 0000000000..7cb73f19e9 --- /dev/null +++ b/integration-tests/tests/specs/features/error-pages.spec.ts @@ -0,0 +1,25 @@ +import { expect, test } from '@playwright/test'; + +// Use base test (not console-warnings fixture) because 404 pages produce expected console errors +test.describe('Error pages', () => { + test('should display 404 page for non-existent routes', async ({ page }) => { + const response = await page.goto('/this-page-does-not-exist'); + + expect(response?.status()).toBe(404); + await expect(page.getByText('Page not found')).toBeVisible(); + await expect(page.getByText('The page you are looking for does not exist.')).toBeVisible(); + }); + + test('should display error for non-existent sequence accession', async ({ page }) => { + await page.goto('/seq/LOC_NONEXISTENT.1'); + + // The page may render with 200 but show an error message + await expect( + page + .getByText(/not found/i) + .or(page.getByText(/does not exist/i)) + .or(page.getByText(/error/i)) + .first(), + ).toBeVisible(); + }); +}); diff --git a/integration-tests/tests/specs/features/footer-links.spec.ts b/integration-tests/tests/specs/features/footer-links.spec.ts new file mode 100644 index 0000000000..3169d40010 --- /dev/null +++ b/integration-tests/tests/specs/features/footer-links.spec.ts @@ -0,0 +1,26 @@ +import { expect } from '@playwright/test'; +import { test } from '../../fixtures/console-warnings.fixture'; + +test.describe('Footer links', () => { + test('should display footer with Docs and API docs links', async ({ page }) => { + await page.goto('/'); + + // Scroll to footer + await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); + + const docsLink = page.getByRole('link', { name: 'Docs', exact: true }); + await expect(docsLink).toBeVisible(); + + const apiDocsLink = page.getByRole('link', { name: 'API docs', exact: true }); + await expect(apiDocsLink).toBeVisible(); + }); + + test('should navigate to API documentation page from footer', async ({ page }) => { + await page.goto('/'); + + await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); + + await page.getByRole('link', { name: 'API docs', exact: true }).click(); + await expect(page).toHaveTitle(/API documentation/); + }); +}); diff --git a/integration-tests/tests/specs/features/header-accession-search.dependent.spec.ts b/integration-tests/tests/specs/features/header-accession-search.dependent.spec.ts new file mode 100644 index 0000000000..147afcbaa7 --- /dev/null +++ b/integration-tests/tests/specs/features/header-accession-search.dependent.spec.ts @@ -0,0 +1,40 @@ +import { expect } from '@playwright/test'; +import { test } from '../../fixtures/console-warnings.fixture'; +import { SearchPage } from '../../pages/search.page'; + +test.describe('Header accession search', () => { + test('should open search box when clicking the search icon in the navigation', async ({ + page, + }) => { + const searchPage = new SearchPage(page); + await searchPage.ebolaSudan(); + + // Click the search icon (magnifying glass) in the header + // Desktop and mobile nav both have the button; use first visible one + await page.getByLabel('Open accession search').click(); + + // The "Search by accession" input should appear (desktop + mobile, use first) + await expect(page.getByTestId('nav-accession-search-input').first()).toBeVisible(); + }); + + test('should navigate to sequence detail when entering a valid accession', async ({ page }) => { + const searchPage = new SearchPage(page); + await searchPage.ebolaSudan(); + + // Get an accession from search results + const accessionVersions = await searchPage.waitForSequencesInSearch(1); + const { accessionVersion } = accessionVersions[0]; + const accession = accessionVersion.split('.')[0]; + + // Click the search icon in the header + await page.getByLabel('Open accession search').click(); + + // Use first visible search input (desktop) + const searchBox = page.getByTestId('nav-accession-search-input').first(); + await searchBox.fill(accession); + await searchBox.press('Enter'); + + // Should navigate to the sequence detail page + await expect(page.getByRole('heading', { name: new RegExp(accession) })).toBeVisible(); + }); +}); diff --git a/integration-tests/tests/specs/features/landing-page-statistics.spec.ts b/integration-tests/tests/specs/features/landing-page-statistics.spec.ts new file mode 100644 index 0000000000..d697a3a091 --- /dev/null +++ b/integration-tests/tests/specs/features/landing-page-statistics.spec.ts @@ -0,0 +1,38 @@ +import { expect } from '@playwright/test'; +import { test } from '../../fixtures/console-warnings.fixture'; + +test.describe('Landing page statistics', () => { + test('should display organism cards with sequence counts', async ({ page }) => { + await page.goto('/'); + + await expect(page.getByText('Explore Loculus data!')).toBeVisible(); + + // Each organism card shows the total sequences count and a "sequences" label + // The count is in a and "sequences" is adjacent text + const ebolaCard = page.getByRole('link', { name: /Ebola Sudan/ }); + await expect(ebolaCard).toBeVisible(); + + // Verify the card contains the word "sequences" (the count is rendered separately) + await expect(ebolaCard.getByText('sequences')).toBeVisible(); + }); + + test('should display recent submission period on organism cards', async ({ page }) => { + await page.goto('/'); + + // Organism cards show "in last N days" stats + const firstCard = page.getByRole('link', { name: /Ebola Sudan/ }); + await expect(firstCard.getByText(/in last \d+ days/)).toBeVisible(); + }); + + test('should link organism cards to their search pages', async ({ page }) => { + await page.goto('/'); + + const ebolaCard = page.getByRole('link', { name: /Ebola Sudan/ }); + await expect(ebolaCard).toBeVisible(); + await ebolaCard.click(); + + // Organism page redirects to search + await page.waitForURL(/\/ebola-sudan\/search/); + await expect(page.getByText(/Search returned \d+ sequence/)).toBeVisible(); + }); +}); diff --git a/integration-tests/tests/specs/features/search/advanced-options.dependent.spec.ts b/integration-tests/tests/specs/features/search/advanced-options.dependent.spec.ts new file mode 100644 index 0000000000..9fe279ad17 --- /dev/null +++ b/integration-tests/tests/specs/features/search/advanced-options.dependent.spec.ts @@ -0,0 +1,41 @@ +import { expect } from '@playwright/test'; +import { test } from '../../../fixtures/console-warnings.fixture'; +import { SearchPage } from '../../../pages/search.page'; + +test.describe('Search advanced options', () => { + test('should open advanced options modal with version status and revocation filters', async ({ + page, + }) => { + const searchPage = new SearchPage(page); + await searchPage.ebolaSudan(); + + const rows = searchPage.getSequenceRows(); + await rows.first().waitFor(); + + const advancedButton = page.getByRole('button', { name: 'Advanced options' }); + await expect(advancedButton).toBeEnabled(); + await advancedButton.click(); + + await expect(page.getByRole('heading', { name: 'Advanced options' })).toBeVisible(); + await expect(page.getByText('Version status')).toBeVisible(); + await expect(page.getByText('Is revocation')).toBeVisible(); + }); + + test('should close advanced options modal with Close button', async ({ page }) => { + const searchPage = new SearchPage(page); + await searchPage.ebolaSudan(); + + const rows = searchPage.getSequenceRows(); + await rows.first().waitFor(); + + const advancedButton = page.getByRole('button', { name: 'Advanced options' }); + await expect(advancedButton).toBeEnabled(); + await advancedButton.click(); + + await expect(page.getByRole('heading', { name: 'Advanced options' })).toBeVisible(); + + // Use the text-based Close button (not the X icon) + await page.getByRole('button', { name: 'Close' }).last().click(); + await expect(page.getByRole('heading', { name: 'Advanced options' })).not.toBeVisible(); + }); +}); diff --git a/integration-tests/tests/specs/features/search/table-sorting.dependent.spec.ts b/integration-tests/tests/specs/features/search/table-sorting.dependent.spec.ts new file mode 100644 index 0000000000..3f600c4dd5 --- /dev/null +++ b/integration-tests/tests/specs/features/search/table-sorting.dependent.spec.ts @@ -0,0 +1,46 @@ +import { expect } from '@playwright/test'; +import { test } from '../../../fixtures/console-warnings.fixture'; +import { SearchPage } from '../../../pages/search.page'; + +test.describe('Search table sorting', () => { + test('should sort by collection date when clicking column header', async ({ page }) => { + const searchPage = new SearchPage(page); + await searchPage.ebolaSudan(); + + const rows = searchPage.getSequenceRows(); + await rows.first().waitFor(); + + // Get the first row text before sorting + const firstRowText = await rows.first().innerText(); + + // Click on "COLLECTION DATE" column header to toggle sort direction + // Column headers are uppercase th elements + await page + .locator('th') + .filter({ hasText: /collection date/i }) + .click(); + await page.waitForTimeout(1000); + + // After clicking, the sort should change - verify the first row is different + const firstRowTextAfterSort = await rows.first().innerText(); + expect(firstRowTextAfterSort).not.toBe(firstRowText); + }); + + test('should update URL params when sorting changes', async ({ page }) => { + const searchPage = new SearchPage(page); + await searchPage.ebolaSudan(); + + const rows = searchPage.getSequenceRows(); + await rows.first().waitFor(); + + // Click on "COLLECTION COUNTRY" column header to sort by country + await page + .locator('th') + .filter({ hasText: /collection country/i }) + .click(); + await page.waitForTimeout(1000); + + const urlParams = new URL(page.url()).searchParams; + expect(urlParams.has('orderBy')).toBeTruthy(); + }); +}); diff --git a/integration-tests/tests/specs/features/search/tools-link-out.dependent.spec.ts b/integration-tests/tests/specs/features/search/tools-link-out.dependent.spec.ts new file mode 100644 index 0000000000..f4b46f3116 --- /dev/null +++ b/integration-tests/tests/specs/features/search/tools-link-out.dependent.spec.ts @@ -0,0 +1,54 @@ +import { expect } from '@playwright/test'; +import { test } from '../../../fixtures/console-warnings.fixture'; +import { SearchPage } from '../../../pages/search.page'; + +test.describe('Search Tools / Link-out menu', () => { + test('should display Tools dropdown with external analysis links', async ({ page }) => { + const searchPage = new SearchPage(page); + await searchPage.ebolaSudan(); + + const rows = searchPage.getSequenceRows(); + await rows.first().waitFor(); + + // Click the Tools button to open the link-out menu + const toolsButton = page.getByRole('button', { name: /Tools/ }); + await expect(toolsButton).toBeEnabled(); + await toolsButton.click(); + + // Should show analysis option text and a Nextclade entry + await expect(page.getByText(/Analyze \d+ sequences with:/)).toBeVisible(); + // The link-out items are rendered as Button components inside HeadlessUI MenuItems + await expect(page.getByText('Nextclade')).toBeVisible(); + }); + + test('should update sequence count in Tools menu after filtering', async ({ page }) => { + const searchPage = new SearchPage(page); + await searchPage.ebolaSudan(); + + const rows = searchPage.getSequenceRows(); + await rows.first().waitFor(); + + // Get total count from header + const totalText = await page.getByText(/Search returned \d+ sequence/).innerText(); + const totalMatch = totalText.match(/(\d+)/); + const totalCount = totalMatch ? Number.parseInt(totalMatch[1]) : 0; + + // Apply a filter to reduce results + await searchPage.select('Collection country', 'France'); + await page.waitForTimeout(1000); + + const filteredText = await page.getByText(/Search returned \d+ sequence/).innerText(); + const filteredMatch = filteredText.match(/(\d+)/); + const filteredCount = filteredMatch ? Number.parseInt(filteredMatch[1]) : 0; + + expect(filteredCount).toBeLessThan(totalCount); + + // Open Tools menu and verify it reflects the filtered count + const toolsButton = page.getByRole('button', { name: /Tools/ }); + await expect(toolsButton).toBeEnabled(); + await toolsButton.click(); + await expect( + page.getByText(new RegExp(`Analyze ${filteredCount} sequences with:`)), + ).toBeVisible(); + }); +}); diff --git a/integration-tests/tests/specs/features/sequence-detail-metadata.dependent.spec.ts b/integration-tests/tests/specs/features/sequence-detail-metadata.dependent.spec.ts new file mode 100644 index 0000000000..0ecc28c3e1 --- /dev/null +++ b/integration-tests/tests/specs/features/sequence-detail-metadata.dependent.spec.ts @@ -0,0 +1,85 @@ +import { expect } from '@playwright/test'; +import { test } from '../../fixtures/console-warnings.fixture'; +import { SearchPage } from '../../pages/search.page'; +import { SequenceDetailPage } from '../../pages/sequence-detail.page'; + +test.describe('Sequence detail page metadata', () => { + test('should display sample details section with metadata fields', async ({ page }) => { + const searchPage = new SearchPage(page); + await searchPage.ebolaSudan(); + + const accessionVersions = await searchPage.waitForSequencesInSearch(1); + const { accessionVersion } = accessionVersions[0]; + + const detailPage = new SequenceDetailPage(page); + await detailPage.goto(accessionVersion); + + await expect(page.getByText('Sample details')).toBeVisible(); + await expect(page.getByText('Collection date')).toBeVisible(); + await expect(page.getByText('Sampling location')).toBeVisible(); + }); + + test('should display submission details section', async ({ page }) => { + const searchPage = new SearchPage(page); + await searchPage.ebolaSudan(); + + const accessionVersions = await searchPage.waitForSequencesInSearch(1); + const { accessionVersion } = accessionVersions[0]; + + const detailPage = new SequenceDetailPage(page); + await detailPage.goto(accessionVersion); + + await expect(page.getByText('Submission details')).toBeVisible(); + await expect(page.getByText('Submission ID')).toBeVisible(); + await expect(page.getByText('Submitting group')).toBeVisible(); + await expect(page.getByText('Date submitted')).toBeVisible(); + await expect(page.getByText('Date released')).toBeVisible(); + }); + + test('should display data use terms section', async ({ page }) => { + const searchPage = new SearchPage(page); + await searchPage.ebolaSudan(); + + const accessionVersions = await searchPage.waitForSequencesInSearch(1); + const { accessionVersion } = accessionVersions[0]; + + const detailPage = new SequenceDetailPage(page); + await detailPage.goto(accessionVersion); + + await expect(page.getByText('Data use terms').first()).toBeVisible(); + // "OPEN" appears as text with a link next to it — use exact match + await expect(page.getByText('OPEN', { exact: true }).first()).toBeVisible(); + }); + + test('should display mutation sections', async ({ page }) => { + const searchPage = new SearchPage(page); + await searchPage.ebolaSudan(); + + const accessionVersions = await searchPage.waitForSequencesInSearch(1); + const { accessionVersion } = accessionVersions[0]; + + const detailPage = new SequenceDetailPage(page); + await detailPage.goto(accessionVersion); + + await expect(page.getByText('Nucleotide mutations')).toBeVisible(); + await expect(page.getByText('Amino acid mutations')).toBeVisible(); + await expect(page.getByText('Substitutions').first()).toBeVisible(); + }); + + test('should display version selector and download button', async ({ page }) => { + const searchPage = new SearchPage(page); + await searchPage.ebolaSudan(); + + const accessionVersions = await searchPage.waitForSequencesInSearch(1); + const { accessionVersion } = accessionVersions[0]; + + const detailPage = new SequenceDetailPage(page); + await detailPage.goto(accessionVersion); + + // Version dropdown should be visible + await expect(page.getByText(/Version \d+/)).toBeVisible(); + + // Download button should be visible — use exact text match + await expect(page.getByText('Download', { exact: true })).toBeVisible(); + }); +});