Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
54c7b61
Mock the UI
fhennig May 27, 2026
69d9f0f
fix(website): resolve merge conflict in backendService getCollections
fhennig May 29, 2026
d582af6
feat(website): use getCollectionSummaries with userId/organism for pr…
fhennig May 29, 2026
a1a4a81
feat(website): replace variant select with downshift autocomplete com…
fhennig May 29, 2026
c01dbae
refactor(website): flatten WasapVariantFilter type and make Signature…
fhennig May 29, 2026
56d4135
feat(website): implement predefined variant mode data fetching
fhennig Jun 1, 2026
8175869
fix lint
fhennig Jun 1, 2026
bd4cefd
docs(website): update Variant Explorer info box for predefined varian…
fhennig Jun 1, 2026
f5e3c37
docs(website): add JSDoc to SignatureType
fhennig Jun 1, 2026
9b244de
refactor(website): move organism internalName into WasapPageConfig
fhennig Jun 1, 2026
832c653
fix(website): add missing signatureType to variant filter in spec
fhennig Jun 1, 2026
a234315
feat(website): add default collectionId to variant filterDefaults
fhennig Jun 1, 2026
f9248cc
docs(website): clarify variant names match collection seeder in comment
fhennig Jun 1, 2026
6ad2f5d
test(website): add variant mode URL round-trip tests for predefined s…
fhennig Jun 1, 2026
4748cd1
test(website): add browser tests for CollectionCombobox blur and clea…
fhennig Jun 1, 2026
20cfcff
fix(website): use userEvent.tab() to trigger blur in CollectionCombob…
fhennig Jun 1, 2026
e3104e2
test(website): add browser tests for predefined signature type in Var…
fhennig Jun 1, 2026
ffc2cbd
fix(website): fix import order in VariantExplorerFilter browser spec
fhennig Jun 1, 2026
3d7e0df
fix(website): use config default for signatureType fallback instead o…
fhennig Jun 2, 2026
b414599
fix(website): fix Downshift onBlur wiring and keyboard accessibility …
fhennig Jun 2, 2026
d3fb8c3
fix(website): restore tabIndex={-1} on clear button in CollectionComb…
fhennig Jun 2, 2026
9347caa
fm
fhennig Jun 2, 2026
022d3c0
perf(website): pre-sort CollectionCombobox items to avoid re-sorting …
fhennig Jun 3, 2026
ec7c3d9
feat(website): add tooltip to "Mutation not in parent" checkbox
fhennig Jun 3, 2026
b6c1893
fix(website): always serialize signatureType in URL, not only for pre…
fhennig Jun 3, 2026
6edc0d4
fix check
fhennig Jun 4, 2026
6968272
fm
fhennig Jun 4, 2026
bd6193e
Update website/src/components/pageStateSelectors/wasap/WasapPageState…
fhennig Jun 4, 2026
33d1a22
test fix
fhennig Jun 4, 2026
a67a3f8
useId()
fhennig Jun 8, 2026
e267af0
better error msg
fhennig Jun 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions website/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"cookie": "^1.1.1",
"dayjs": "^1.11.21",
"katex": "^0.17.0",
"downshift": "^9.3.3",
"patch-package": "^8.0.1",
"react": "^19.2.7",
"react-dom": "^19.2.7",
Expand Down
17 changes: 15 additions & 2 deletions website/src/backendApi/backendService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,8 +177,21 @@ export class BackendService extends ApiService {
return this.get({ url: `/users/${id}`, schema: publicUserSchema });
}

public async getCollectionSummaries(requestParams: { organism?: string; excludeSystemCollections?: boolean } = {}) {
return this.get({ url: '/collections', requestParams, schema: z.array(collectionSummarySchema) });
public async getCollectionSummaries({
organism,
userId,
excludeSystemCollections,
}: { organism?: string; userId?: number; excludeSystemCollections?: boolean } = {}) {
const requestParams: Record<string, string> = {};
if (organism !== undefined) requestParams.organism = organism;
if (userId !== undefined) requestParams.userId = String(userId);
if (excludeSystemCollections !== undefined)
requestParams.excludeSystemCollections = String(excludeSystemCollections);
return this.get({
url: '/collections',
requestParams: Object.keys(requestParams).length > 0 ? requestParams : undefined,
schema: z.array(collectionSummarySchema),
});
}

public async getCollections({ organism }: { organism?: string } = {}) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,12 +144,12 @@ export function ExplorationModeInfo() {
</li>
<li>
<span className='font-semibold text-gray-900'>Variant Explorer:</span> track variant-specific
mutations over time. Variant-specific mutations are computed based on user parameters and filtering
clinical sequences from{' '}
mutations over time. Variants can be defined in two ways: by selecting a predefined variant from a
curated list, or by filtering clinical sequences from{' '}
<a className='link' href='https://cov-spectrum.org'>
CovSpectrum
</a>
.
</a>{' '}
based on user parameters.
</li>
<li>
<span className='font-semibold text-gray-900'>Untracked Mutations:</span> novel mutations not yet
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useQuery } from '@tanstack/react-query';
import { type Dispatch, type SetStateAction, useState } from 'react';

import { getBackendServiceForClientside } from '../../../backendApi/backendService';
import { ApplyFilterButton } from '../ApplyFilterButton';
import { DynamicDateFilter } from '../DynamicDateFilter';
import { SelectorHeadline } from '../SelectorHeadline';
Expand Down Expand Up @@ -101,6 +102,24 @@ export function WasapPageStateSelector({
},
});

const predefinedVariantsQueryResult = useQuery({
enabled: config.variantAnalysisModeEnabled && config.predefinedVariantsSource !== undefined,
queryKey: ['predefinedVariants', config.variantAnalysisModeEnabled && config.predefinedVariantsSource],
queryFn: async () => {
if (!config.variantAnalysisModeEnabled || config.predefinedVariantsSource === undefined) {
throw Error(
'This predefined variants query was called despite it being disabled. This should not happen.',
);
}
const { collectionsUserId, collectionsTag } = config.predefinedVariantsSource;
const collections = await getBackendServiceForClientside().getCollectionSummaries({
userId: collectionsUserId,
organism: config.internalName,
});
return collections.filter((c) => c.description?.includes(collectionsTag) ?? false);
},
});

return (
<div className='flex flex-col gap-4'>
<SelectorHeadline>Filter dataset</SelectorHeadline>
Expand Down Expand Up @@ -181,6 +200,12 @@ export function WasapPageStateSelector({
setPageState={setVariantFilter}
clinicalSequenceLapisBaseUrl={config.clinicalLapis.lapisBaseUrl}
clinicalSequenceLapisLineageField={config.clinicalLapis.lineageField}
predefinedVariantsQueryResult={
config.predefinedVariantsSource !== undefined
? predefinedVariantsQueryResult
: undefined
}
predefinedVariantsLabel={config.predefinedVariantsSource?.variantSourceLabel}
/>
);
case 'resistance':
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
import { userEvent } from '@vitest/browser/context';
import { type UseQueryResult } from '@tanstack/react-query';
import { page, userEvent } from '@vitest/browser/context';
import { describe, expect, vi } from 'vitest';
import { render } from 'vitest-browser-react';

import { VariantExplorerFilter } from './VariantExplorerFilter';
import { DUMMY_LAPIS_URL, type LapisRouteMocker } from '../../../../../routeMocker';
import { it } from '../../../../../test-extend';
import type { CollectionSummary } from '../../../../types/Collection';
import type { WasapVariantFilter } from '../../../views/wasap/wasapPageConfig';

const DUMMY_LAPIS_URL_2 = 'http://lapis2.dummy';

const DUMMY_COLLECTIONS: CollectionSummary[] = [
{ id: 1, name: 'XBB.1.5', ownedBy: 1, organism: 'SARS-CoV-2', description: null, variantCount: 5 },
{ id: 2, name: 'JN.1', ownedBy: 1, organism: 'SARS-CoV-2', description: null, variantCount: 3 },
];
const mockPredefinedQueryResult = { data: DUMMY_COLLECTIONS } as unknown as UseQueryResult<CollectionSummary[]>;

describe('VariantExplorerFilter', () => {
const defaultPageState: WasapVariantFilter = {
mode: 'variant',
signatureType: 'computed',
sequenceType: 'nucleotide',
variant: undefined,
minProportion: 0.05,
Expand All @@ -20,6 +29,11 @@ describe('VariantExplorerFilter', () => {
timeFrame: 'all',
};

const predefinedPageState: WasapVariantFilter = {
...defaultPageState,
signatureType: 'predefined',
};

it('calls setPageState when changing sequence type', async ({ routeMockers: { lapis } }) => {
setupLapisMocks(lapis);
const mockSetPageState = vi.fn();
Expand Down Expand Up @@ -98,6 +112,87 @@ describe('VariantExplorerFilter', () => {
minProportion: 0.1,
});
});

it('calls setPageState when switching to predefined signature type', async ({ routeMockers: { lapis } }) => {
setupLapisMocks(lapis);
const mockSetPageState = vi.fn();

render(
<gs-app lapis={DUMMY_LAPIS_URL}>
<VariantExplorerFilter
pageState={defaultPageState}
setPageState={mockSetPageState}
clinicalSequenceLapisBaseUrl={DUMMY_LAPIS_URL_2}
clinicalSequenceLapisLineageField='pangoLineage'
predefinedVariantsQueryResult={mockPredefinedQueryResult}
/>
</gs-app>,
);

const variantSourceSelect = page.getByRole('combobox').first();
await variantSourceSelect.selectOptions('predefined');

expect(mockSetPageState).toHaveBeenCalledWith({
...defaultPageState,
signatureType: 'predefined',
});
});

it('calls setPageState when selecting a predefined collection', async ({ routeMockers: { lapis } }) => {
setupLapisMocks(lapis);
const mockSetPageState = vi.fn();

const { getByRole } = render(
<gs-app lapis={DUMMY_LAPIS_URL}>
<VariantExplorerFilter
pageState={predefinedPageState}
setPageState={mockSetPageState}
clinicalSequenceLapisBaseUrl={DUMMY_LAPIS_URL_2}
clinicalSequenceLapisLineageField='pangoLineage'
predefinedVariantsQueryResult={mockPredefinedQueryResult}
/>
</gs-app>,
);

const collectionInput = page.getByPlaceholder('Select variant');
await collectionInput.click();
await userEvent.type(collectionInput, 'XBB');

const option = await vi.waitFor(() => getByRole('option', { name: 'XBB.1.5', exact: true }));
await option.click();

await vi.waitFor(() => {
expect(mockSetPageState).toHaveBeenCalledWith({
...predefinedPageState,
collectionId: 1,
});
});
});

it('calls setPageState when toggling "Mutation not in parent"', async ({ routeMockers: { lapis } }) => {
setupLapisMocks(lapis);
const mockSetPageState = vi.fn();

const { getByLabelText } = render(
<gs-app lapis={DUMMY_LAPIS_URL}>
<VariantExplorerFilter
pageState={predefinedPageState}
setPageState={mockSetPageState}
clinicalSequenceLapisBaseUrl={DUMMY_LAPIS_URL_2}
clinicalSequenceLapisLineageField='pangoLineage'
predefinedVariantsQueryResult={mockPredefinedQueryResult}
/>
</gs-app>,
);

const checkbox = getByLabelText('Mutation not in parent');
await checkbox.click();

expect(mockSetPageState).toHaveBeenCalledWith({
...predefinedPageState,
newMutationsOnly: true,
});
});
});

function setupLapisMocks(lapisRouteMocker: LapisRouteMocker) {
Expand Down
Loading