diff --git a/website/src/components/views/compareSideBySide/GenericCompareSideBySideDataDisplay.tsx b/website/src/components/views/compareSideBySide/GenericCompareSideBySideDataDisplay.tsx new file mode 100644 index 00000000..4b84d3b4 --- /dev/null +++ b/website/src/components/views/compareSideBySide/GenericCompareSideBySideDataDisplay.tsx @@ -0,0 +1,85 @@ +import { type FC, useMemo } from 'react'; + +import type { OrganismsConfig } from '../../../config.ts'; +import { chooseGranularityBasedOnDateRange } from '../../../util/chooseGranularityBasedOnDateRange.ts'; +import { ComponentHeight } from '../../../views/OrganismConstants.ts'; +import type { DatasetAndVariantData } from '../../../views/View.ts'; +import { toLapisFilterWithoutVariant } from '../../../views/pageStateHandlers/toLapisFilterWithoutVariant.ts'; +import { type OrganismWithViewKey, Routing } from '../../../views/routing.ts'; +import { compareSideBySideViewKey } from '../../../views/viewKeys.ts'; +import { GsAggregate } from '../../genspectrum/GsAggregate.tsx'; +import { GsMutations } from '../../genspectrum/GsMutations.tsx'; +import { GsPrevalenceOverTime } from '../../genspectrum/GsPrevalenceOverTime.tsx'; + +export type GenericCompareSideBySideDataDisplayProps = { + organismViewKey: `${OrganismWithViewKey}.${typeof compareSideBySideViewKey}`; + organismsConfig: OrganismsConfig; + datasetAndVariantData: DatasetAndVariantData; + hideMutationComponents?: boolean; +}; + +export const GenericCompareSideBySideDataDisplay: FC = ({ + organismViewKey, + organismsConfig, + datasetAndVariantData, + hideMutationComponents, +}) => { + const view = useMemo( + () => new Routing(organismsConfig).getOrganismView(organismViewKey), + [organismsConfig, organismViewKey], + ); + + const { datasetFilter, variantFilter } = datasetAndVariantData; + + const datasetLapisFilter = toLapisFilterWithoutVariant(datasetFilter, view.organismConstants.additionalFilters); + const timeGranularity = chooseGranularityBasedOnDateRange({ + earliestDate: new Date(view.organismConstants.earliestDate), + dateRange: datasetFilter.dateFilters[view.organismConstants.mainDateField], + }); + const numeratorFilter = view.pageStateHandler.variantFilterToLapisFilter(datasetFilter, variantFilter); + + return ( + <> + + {hideMutationComponents !== true && ( + <> + + + + )} + + {view.organismConstants.aggregatedVisualizations.compareSideBySide.map(({ label, fields, views }) => ( + + ))} + + ); +}; diff --git a/website/src/components/views/compareSideBySide/GenericCompareSideBySidePage.astro b/website/src/components/views/compareSideBySide/GenericCompareSideBySidePage.astro index 81058b69..5fd6fcb3 100644 --- a/website/src/components/views/compareSideBySide/GenericCompareSideBySidePage.astro +++ b/website/src/components/views/compareSideBySide/GenericCompareSideBySidePage.astro @@ -1,16 +1,11 @@ --- +import { GenericCompareSideBySideDataDisplay } from './GenericCompareSideBySideDataDisplay.tsx'; import { toDownloadLink } from './toDownloadLink'; import { isStaging, getDashboardsConfig, getLapisUrl } from '../../../config'; import OrganismViewPageLayout from '../../../layouts/OrganismPage/OrganismViewPageLayout.astro'; -import { chooseGranularityBasedOnDateRange } from '../../../util/chooseGranularityBasedOnDateRange'; -import { ComponentHeight } from '../../../views/OrganismConstants'; -import { toLapisFilterWithoutVariant } from '../../../views/pageStateHandlers/toLapisFilterWithoutVariant'; import { type OrganismViewKey, type OrganismWithViewKey } from '../../../views/routing'; import { ServerSide } from '../../../views/serverSideRouting'; import { compareSideBySideViewKey } from '../../../views/viewKeys'; -import GsAggregate from '../../genspectrum/GsAggregate.astro'; -import GsMutations from '../../genspectrum/GsMutations.astro'; -import GsPrevalenceOverTime from '../../genspectrum/GsPrevalenceOverTime.astro'; import { CompareSideBySidePageStateSelector } from '../../pageStateSelectors/CompareSideBySidePageStateSelector'; import { CompareSideBySideSelectorFallback } from '../../pageStateSelectors/FallbackElement'; @@ -26,6 +21,8 @@ const view = ServerSide.routing.getOrganismView(organismViewKey); const pageState = view.pageStateHandler.parsePageStateFromUrl(Astro.url); +const organismConfig = getDashboardsConfig().dashboards.organisms; + const downloadLinks = [...pageState.filters.entries()].map(toDownloadLink(view.pageStateHandler, organism)); --- @@ -33,20 +30,7 @@ const downloadLinks = [...pageState.filters.entries()].map(toDownloadLink(view.p
{ - Array.from(pageState.filters).map(([id, { variantFilter, datasetFilter }]) => { - const datasetLapisFilter = toLapisFilterWithoutVariant( - datasetFilter, - view.organismConstants.additionalFilters, - ); - const timeGranularity = chooseGranularityBasedOnDateRange({ - earliestDate: new Date(view.organismConstants.earliestDate), - dateRange: datasetFilter.dateFilters[view.organismConstants.mainDateField], - }); - const numeratorFilter = view.pageStateHandler.variantFilterToLapisFilter( - datasetFilter, - variantFilter, - ); - + Array.from(pageState.filters).map(([id, datasetAndVariantData]) => { return (
@@ -63,7 +47,7 @@ const downloadLinks = [...pageState.filters.entries()].map(toDownloadLink(view.p
- - {!hideMutationComponents && ( - <> - - - - )} - - {view.organismConstants.aggregatedVisualizations.compareSideBySide.map( - ({ label, fields, views }) => ( - - ), - )}
); }) diff --git a/website/tests/CompareSideBySidePage.ts b/website/tests/CompareSideBySidePage.ts new file mode 100644 index 00000000..3b93102a --- /dev/null +++ b/website/tests/CompareSideBySidePage.ts @@ -0,0 +1,50 @@ +import { expect } from '@playwright/test'; + +import { ViewPage } from './ViewPage.ts'; +import { paths } from '../src/types/Organism.ts'; +import { type OrganismWithViewKey } from '../src/views/routing'; +import { compareSideBySideViewKey } from '../src/views/viewKeys'; + +export class CompareSideBySidePage extends ViewPage { + public readonly removeColumnButton = this.page.getByRole('link', { name: 'Remove column' }); + public readonly addColumnButton = this.page.getByRole('link', { name: 'Add column' }); + public readonly mutationField = this.page.getByRole('combobox', { name: 'Enter a mutation', exact: false }); + + public async goto(organism: OrganismWithViewKey) { + await this.page.goto(`${paths[organism].basePath}/compare-side-by-side`); + } + + public async addColumn(options: { + dateRangeOption: string; + expectedColumnCount: number; + lineage?: string; + lineageFieldPlaceholder?: string; + mutation?: string; + }) { + await expect(this.addColumnButton).toBeVisible(); + + await this.addColumnButton.click(); + await expect(this.mutationField).toHaveCount(options.expectedColumnCount); + + await this.page + .locator('gs-date-range-filter') + .getByRole('combobox') + .last() + .selectOption(options.dateRangeOption); + + if (options.lineage && options.lineageFieldPlaceholder) { + const { lineage, lineageFieldPlaceholder } = options; + + const lineageFieldLocator = this.page.getByPlaceholder(lineageFieldPlaceholder); + await expect(lineageFieldLocator).toHaveCount(options.expectedColumnCount); + + await this.fillLineageField(lineageFieldLocator.last(), lineage); + } + + if (options.mutation) { + await this.fillMutationField(this.mutationField.last(), options.mutation); + } + + await this.page.getByRole('button', { name: 'Apply filters' }).last().click(); + } +} diff --git a/website/tests/CompareToBaselinePage.ts b/website/tests/CompareToBaselinePage.ts index 9f41d278..a5a091b3 100644 --- a/website/tests/CompareToBaselinePage.ts +++ b/website/tests/CompareToBaselinePage.ts @@ -24,15 +24,11 @@ export class CompareToBaselinePage extends CompareVariantsPage { const { lineage, lineageFieldPlaceholder } = options; const lineageFieldLocator = this.page.getByPlaceholder(lineageFieldPlaceholder); - await lineageFieldLocator.first().fill(lineage); - - const selectedLineage = this.page.getByRole('option', { name: lineage, exact: false }); - await selectedLineage.first().click(); + await this.fillLineageField(lineageFieldLocator.first(), lineage); } if (options.mutation) { - await this.mutationField.first().fill(options.mutation); - await this.mutationField.first().press('Enter'); + await this.fillMutationField(this.mutationField.first(), options.mutation); } } } diff --git a/website/tests/CompareVariantsPage.ts b/website/tests/CompareVariantsPage.ts index 16b7585a..d566f074 100644 --- a/website/tests/CompareVariantsPage.ts +++ b/website/tests/CompareVariantsPage.ts @@ -33,15 +33,11 @@ export class CompareVariantsPage extends ViewPage { const lineageFieldLocator = this.page.getByPlaceholder(lineageFieldPlaceholder); await expect(lineageFieldLocator).toHaveCount(numberMutationFields + 1); - await lineageFieldLocator.last().fill(lineage); - - const selectedLineage = this.page.getByRole('option', { name: lineage, exact: false }); - await selectedLineage.first().click(); + await this.fillLineageField(lineageFieldLocator.last(), lineage); } if (options.mutation) { - await this.mutationField.last().fill(options.mutation); - await this.mutationField.last().press('Enter'); + await this.fillMutationField(this.mutationField.last(), options.mutation); } } } diff --git a/website/tests/ViewPage.ts b/website/tests/ViewPage.ts index be146eb7..43ee2549 100644 --- a/website/tests/ViewPage.ts +++ b/website/tests/ViewPage.ts @@ -1,4 +1,4 @@ -import { expect, type Page } from '@playwright/test'; +import { expect, type Locator, type Page } from '@playwright/test'; import { type Organism } from '../src/types/Organism.ts'; @@ -23,4 +23,15 @@ export abstract class ViewPage { public async selectDateRange(dateRangeOption: string) { await this.page.locator('gs-date-range-filter').getByRole('combobox').first().selectOption(dateRangeOption); } + + public async fillLineageField(locator: Locator, lineage: string) { + await locator.fill(lineage); + const selectedLineage = this.page.getByRole('option', { name: lineage, exact: false }); + await selectedLineage.first().click(); + } + + public async fillMutationField(locator: Locator, mutation: string) { + await locator.first().fill(mutation); + await locator.first().press('Enter'); + } } diff --git a/website/tests/compareSideBySide.spec.ts b/website/tests/compareSideBySide.spec.ts new file mode 100644 index 00000000..2ccfa925 --- /dev/null +++ b/website/tests/compareSideBySide.spec.ts @@ -0,0 +1,38 @@ +import { expect } from '@playwright/test'; + +import { test } from './e2e.fixture.ts'; +import { organismOptions, organismsWithView } from './helpers.ts'; +import { compareSideBySideViewKey } from '../src/views/viewKeys'; + +test.describe('The Compare Side-By-Side page', () => { + for (const organism of organismsWithView(compareSideBySideViewKey)) { + test(`should show diagrams after selecting a variant in both columns - ${organism}`, async ({ + compareSideBySidePage, + }) => { + const options = organismOptions[organism]; + + await compareSideBySidePage.goto(organism); + await expect(compareSideBySidePage.removeColumnButton).not.toBeVisible(); + await expect(compareSideBySidePage.addColumnButton).toBeVisible(); + + await expect(compareSideBySidePage.diagramTitle('Prevalence Over Time')).not.toBeVisible(); + + await compareSideBySidePage.addColumn({ + dateRangeOption: 'All times', + expectedColumnCount: 1, + ...options, + }); + await expect(compareSideBySidePage.removeColumnButton).not.toBeVisible(); + + await compareSideBySidePage.addColumn({ + dateRangeOption: 'All times', + expectedColumnCount: 2, + ...options, + }); + await expect(compareSideBySidePage.removeColumnButton).toHaveCount(2); + + await expect(compareSideBySidePage.diagramTitle('Prevalence Over Time')).toHaveCount(2); + await compareSideBySidePage.expectToSeeNoComponentErrors(); + }); + } +}); diff --git a/website/tests/e2e.fixture.ts b/website/tests/e2e.fixture.ts index 6a655f33..3e4dbf6d 100644 --- a/website/tests/e2e.fixture.ts +++ b/website/tests/e2e.fixture.ts @@ -1,5 +1,6 @@ import { test as base } from '@playwright/test'; +import { CompareSideBySidePage } from './CompareSideBySidePage.ts'; import { CompareToBaselinePage } from './CompareToBaselinePage.ts'; import { CompareVariantsPage } from './CompareVariantsPage.ts'; import { SequencingEffortsPage } from './SequencingEffortsPage.ts'; @@ -8,6 +9,7 @@ type E2EFixture = { compareVariantsPage: CompareVariantsPage; sequencingEffortsPage: SequencingEffortsPage; compareToBaselinePage: CompareToBaselinePage; + compareSideBySidePage: CompareSideBySidePage; }; export const test = base.extend({ @@ -20,4 +22,7 @@ export const test = base.extend({ compareToBaselinePage: async ({ page }, use) => { await use(new CompareToBaselinePage(page)); }, + compareSideBySidePage: async ({ page }, use) => { + await use(new CompareSideBySidePage(page)); + }, }); diff --git a/website/tests/helpers.ts b/website/tests/helpers.ts index 2e3edba4..5dfda525 100644 --- a/website/tests/helpers.ts +++ b/website/tests/helpers.ts @@ -8,9 +8,11 @@ export function organismsWithView(viewKey: ViewKey): Org export const organismOptions = { [Organisms.covid]: { lineage: 'JN.1*', lineageFieldPlaceholder: 'Nextclade pango lineage' }, + [Organisms.influenzaA]: { lineage: 'H1', lineageFieldPlaceholder: 'HA subtype' }, [Organisms.h5n1]: { lineage: '2.3.4.4b', lineageFieldPlaceholder: 'Clade' }, [Organisms.h1n1pdm]: { lineage: '6B.1A.5a.2a.1', lineageFieldPlaceholder: 'Clade HA' }, [Organisms.h3n2]: { lineage: '3C.2a1b', lineageFieldPlaceholder: 'Clade HA' }, + [Organisms.influenzaB]: { lineage: 'vic', lineageFieldPlaceholder: 'Lineage' }, [Organisms.victoria]: { lineage: 'V1A.3a.2', lineageFieldPlaceholder: 'Clade HA' }, [Organisms.westNile]: { lineage: '1A', lineageFieldPlaceholder: 'Lineage' }, [Organisms.rsvA]: { lineage: 'A.D.5.2', lineageFieldPlaceholder: 'Lineage' },