Skip to content

Commit

Permalink
feat(website): don't display the segment name if there is only one se…
Browse files Browse the repository at this point in the history
…gment in the sequence viewer #803
  • Loading branch information
fengelniederhammer committed Jan 25, 2024
1 parent 288a07b commit c6f475f
Show file tree
Hide file tree
Showing 3 changed files with 261 additions and 54 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { render, screen, waitFor } from '@testing-library/react';
import React from 'react';
import { act } from 'react-dom/test-utils';
import { beforeEach, describe, expect, test, vi } from 'vitest';

import { SequencesContainer } from './SequencesContainer.tsx';
import { mockRequest, testConfig, testOrganism } from '../../../vitest.setup.ts';

vi.mock('../../config', () => ({
getLapisUrl: vi.fn().mockReturnValue('http://lapis.dummy'),
}));

const queryClient = new QueryClient();
const accessionVersion = 'accession';

function renderSequenceViewer({
nucleotideSegmentNames,
genes,
}: Pick<React.ComponentProps<typeof SequencesContainer>, 'nucleotideSegmentNames' | 'genes'>) {
render(
<QueryClientProvider client={queryClient}>
<SequencesContainer
organism={testOrganism}
accessionVersion={accessionVersion}
clientConfig={testConfig.public}
genes={genes}
nucleotideSegmentNames={nucleotideSegmentNames}
/>
</QueryClientProvider>,
);
}

const multiSegmentName = 'main2';

const singleSegmentSequence = 'SingleSegmentSequence';
const multiSegmentSequence = 'MultiSegmentSequence';
const unalignedSingleSegmentSequence = 'UnalignedSingleSegmentSequence';
const unalignedMultiSegmentSequence = 'UnalignedMultiSegmentSequence';

describe('SequencesContainer', () => {
beforeEach(() => {
mockRequest.lapis.alignedNucleotideSequences(200, `>some\n${singleSegmentSequence}`);
mockRequest.lapis.alignedNucleotideSequencesMultiSegment(
200,
`>some\n${multiSegmentSequence}`,
multiSegmentName,
);
mockRequest.lapis.unalignedNucleotideSequences(200, `>some\n${unalignedSingleSegmentSequence}`);
mockRequest.lapis.unalignedNucleotideSequencesMultiSegment(200, '', 'main');
mockRequest.lapis.unalignedNucleotideSequencesMultiSegment(
200,
`>some\n${unalignedMultiSegmentSequence}`,
multiSegmentName,
);
});

test('should render single segmented sequence', async () => {
renderSequenceViewer({
nucleotideSegmentNames: ['main'],
genes: [],
});

click('Load sequences');

click('Aligned');
await waitFor(() => {
expect(screen.getByText(singleSegmentSequence)).toBeVisible();
});

click('Sequence');
await waitFor(() => {
expect(screen.getByText("LAPIS v2 doesn't support unaligned nucleotide sequences yet")).toBeVisible();
});
});

test('should render multi segmented sequence', async () => {
renderSequenceViewer({
nucleotideSegmentNames: ['main', multiSegmentName],
genes: [],
});

click('Load sequences');

click(`${multiSegmentName} (aligned)`);
await waitFor(() => {
expect(screen.getByText(multiSegmentSequence)).toBeVisible();
});

click(`${multiSegmentName} (unaligned)`);
await waitFor(() => {
expect(screen.getByText("LAPIS v2 doesn't support unaligned nucleotide sequences yet")).toBeVisible();
});
});

function click(name: string) {
act(() => screen.getByRole('button', { name }).click());
}
});
191 changes: 137 additions & 54 deletions website/src/components/SequenceDetailsPage/SequencesContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { type FC, useState } from 'react';
import { type Dispatch, type FC, type SetStateAction, useState } from 'react';

import { SequencesViewer } from './SequenceViewer';
import type { ClientConfig } from '../../types/runtimeConfig';
Expand All @@ -12,8 +11,7 @@ import {
type SequenceType,
unalignedSequenceSegment,
} from '../../utils/sequenceTypeHelpers';

const queryClient = new QueryClient();
import { withQueryProvider } from '../common/withProvider.tsx';

type SequenceContainerProps = {
organism: string;
Expand All @@ -23,65 +21,150 @@ type SequenceContainerProps = {
nucleotideSegmentNames: [string, ...string[]];
};

export const SequencesContainer: FC<SequenceContainerProps> = ({
export const InnerSequencesContainer: FC<SequenceContainerProps> = ({
organism,
accessionVersion,
clientConfig,
genes,
nucleotideSegmentNames,
}) => {
const [loadSequences, setLoadSequences] = useState(false);
const [type, setType] = useState<SequenceType>(unalignedSequenceSegment(nucleotideSegmentNames[0]));
const [sequenceType, setSequenceType] = useState<SequenceType>(unalignedSequenceSegment(nucleotideSegmentNames[0]));

if (!loadSequences) {
return (
<button className='btn btn-sm m-4' onClick={() => setLoadSequences(true)}>
Load sequences
</button>
);
}

return (
<>
<SequenceTabs
nucleotideSegmentNames={nucleotideSegmentNames}
sequenceType={sequenceType}
setType={setSequenceType}
genes={genes}
/>
<div className='border p-4 max-w-[1000px]'>
<SequencesViewer
organism={organism}
accessionVersion={accessionVersion}
clientConfig={clientConfig}
sequenceType={sequenceType}
isMultiSegmented={isMultiSegmented(nucleotideSegmentNames)}
/>
</div>
</>
);
};

export const SequencesContainer = withQueryProvider(InnerSequencesContainer);

type NucleotideSequenceTabsProps = {
nucleotideSegmentNames: [string, ...string[]];
sequenceType: SequenceType;
setType: Dispatch<SetStateAction<SequenceType>>;
};

const SequenceTabs: FC<NucleotideSequenceTabsProps & { genes: string[] }> = ({
nucleotideSegmentNames,
genes,
sequenceType,
setType,
}) => (
<div className='tabs -mb-px tabs-lifted flex flex-wrap'>
<UnalignedNucleotideSequenceTabs
nucleotideSegmentNames={nucleotideSegmentNames}
sequenceType={sequenceType}
setType={setType}
/>
<AlignmentSequenceTabs
nucleotideSegmentNames={nucleotideSegmentNames}
sequenceType={sequenceType}
setType={setType}
/>
{genes.map((gene) => (
<Tab
isActive={isGeneSequence(gene, sequenceType)}
onClick={() => setType(geneSequence(gene))}
label={gene}
/>
))}
</div>
);

const UnalignedNucleotideSequenceTabs: FC<NucleotideSequenceTabsProps> = ({
nucleotideSegmentNames,
sequenceType,
setType,
}) => {
if (!isMultiSegmented(nucleotideSegmentNames)) {
const onlySegment = nucleotideSegmentNames[0];
return (
<Tab
key={onlySegment}
isActive={isUnalignedSequence(sequenceType)}
onClick={() => setType(unalignedSequenceSegment(onlySegment))}
label='Sequence'
/>
);
}

return (
<QueryClientProvider client={queryClient}>
{!loadSequences ? (
<button className='btn btn-sm m-4' onClick={() => setLoadSequences(true)}>
Load sequences
</button>
) : (
<>
<div className='tabs -mb-px tabs-lifted flex flex-wrap'>
{nucleotideSegmentNames.map((segmentName) => (
<button
key={segmentName}
className={`tab ${isUnalignedSequence(type) ? 'tab-active' : ''}`}
onClick={() => setType(unalignedSequenceSegment(segmentName))}
>
{segmentName} (unaligned)
</button>
))}
{nucleotideSegmentNames.map((segmentName) => (
<button
key={segmentName}
className={`tab ${isAlignedSequence(type) ? 'tab-active' : ''}`}
onClick={() => setType(alignedSequenceSegment(segmentName))}
>
{segmentName} (aligned)
</button>
))}
{genes.map((gene) => (
<button
key={gene}
className={`tab ${isGeneSequence(gene, type) ? 'tab-active' : ''}`}
onClick={() => setType(geneSequence(gene))}
>
{gene}
</button>
))}
</div>
<>
{nucleotideSegmentNames.map((segmentName) => (
<Tab
key={segmentName}
isActive={isUnalignedSequence(sequenceType)}
onClick={() => setType(unalignedSequenceSegment(segmentName))}
label={`${segmentName} (unaligned)`}
/>
))}
</>
);
};

<div className='border p-4 max-w-[1000px]'>
<SequencesViewer
organism={organism}
accessionVersion={accessionVersion}
clientConfig={clientConfig}
sequenceType={type}
isMultiSegmented={nucleotideSegmentNames.length > 1}
/>
</div>
</>
)}
</QueryClientProvider>
const AlignmentSequenceTabs: FC<NucleotideSequenceTabsProps> = ({ nucleotideSegmentNames, sequenceType, setType }) => {
if (!isMultiSegmented(nucleotideSegmentNames)) {
const onlySegment = nucleotideSegmentNames[0];
return (
<Tab
key={onlySegment}
isActive={isAlignedSequence(sequenceType)}
onClick={() => setType(alignedSequenceSegment(onlySegment))}
label='Aligned'
/>
);
}

return (
<>
{nucleotideSegmentNames.map((segmentName) => (
<Tab
key={segmentName}
isActive={isAlignedSequence(sequenceType)}
onClick={() => setType(alignedSequenceSegment(segmentName))}
label={`${segmentName} (aligned)`}
/>
))}
</>
);
};

type TabProps = {
isActive: boolean;
label: string;
onClick: () => void;
};

const Tab: FC<TabProps> = ({ isActive, label, onClick }) => (
<button className={`tab ${isActive ? 'tab-active' : ''}`} onClick={onClick}>
{label}
</button>
);

function isMultiSegmented(nucleotideSegmentNames: string[]) {
return nucleotideSegmentNames.length > 1;
}
25 changes: 25 additions & 0 deletions website/vitest.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,31 @@ const lapisRequestMocks = {
),
);
},
unalignedNucleotideSequences: (statusCode: number = 200, response: string | LapisError) => {
testServer.use(
http.post(`${testConfig.serverSide.lapisUrls.dummy}/sample/unalignedNucleotideSequences`, () => {
return new Response(JSON.stringify(response), {
status: statusCode,
});
}),
);
},
unalignedNucleotideSequencesMultiSegment: (
statusCode: number = 200,
response: string | LapisError,
segmentName: string,
) => {
testServer.use(
http.post(
`${testConfig.serverSide.lapisUrls.dummy}/sample/unalignedNucleotideSequences/${segmentName}`,
() => {
return new Response(JSON.stringify(response), {
status: statusCode,
});
},
),
);
},
nucleotideMutations: (statusCode: number = 200, response: MutationsResponse | LapisError) => {
testServer.use(
http.post(`${testConfig.serverSide.lapisUrls.dummy}/sample/nucleotideMutations`, () => {
Expand Down

0 comments on commit c6f475f

Please sign in to comment.