-
Notifications
You must be signed in to change notification settings - Fork 7
feat(integration-tests): add tests for uncovered UI features #5995
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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(); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -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 | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| await expect( | ||||||
| page | ||||||
| .getByText(/not found/i) | ||||||
| .or(page.getByText(/does not exist/i)) | ||||||
| .or(page.getByText(/error/i)) | ||||||
|
Comment on lines
+20
to
+21
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| .first(), | ||||||
| ).toBeVisible(); | ||||||
| }); | ||||||
| }); | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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/); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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(); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 <span class='font-bold'> 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(); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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(); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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', () => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if this could be flaky - can we wait for there to be multiple sequences? also potentially the first sequence will by chance be the earliest, maybe we should actually compare the date? |
||
| 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(); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess also here, should we ensure there are multiple sequences? |
||
|
|
||
| // 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(); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this might also flake as certain INSDC sequences do not have a collection date |
||
| 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(); | ||
| }); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the page should render with 200 and the message: