-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: adding SeqvarVariantValidatorCard (#55)
- Loading branch information
Showing
3 changed files
with
337 additions
and
0 deletions.
There are no files selected for viewing
118 changes: 118 additions & 0 deletions
118
src/components/SeqvarVariantValidatorCard/SeqvarVariantValidatorCard.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
import fs from 'fs' | ||
import path from 'path' | ||
import { beforeEach, describe, expect, it, vi } from 'vitest' | ||
import createFetchMock from 'vitest-fetch-mock' | ||
import { nextTick } from 'vue' | ||
|
||
import { setupMountedComponents } from '@/lib/testUtils' | ||
|
||
import type { Seqvar } from '../../lib/genomicVars' | ||
import SeqvarVariantValidatorCard from './SeqvarVariantValidatorCard.vue' | ||
|
||
/** Example Sequence Variant */ | ||
const seqvar: Seqvar = { | ||
genomeBuild: 'grch37', | ||
chrom: '17', | ||
pos: 43044295, | ||
del: 'G', | ||
ins: 'A', | ||
userRepr: 'grch37-17-43044295-G-A' | ||
} | ||
/** Fixture with response from API. */ | ||
const responseManeBrca1Json = JSON.parse( | ||
fs.readFileSync( | ||
path.resolve(__dirname, '../../api/variantValidator/fixture.maneResponse.BRCA1.json'), | ||
'utf8' | ||
) | ||
) | ||
|
||
/** Initialize mock for `fetch()`. */ | ||
const fetchMocker = createFetchMock(vi) | ||
|
||
describe.concurrent('SeqvarVariantValidatorCard.vue', async () => { | ||
beforeEach(() => { | ||
fetchMocker.enableMocks() | ||
fetchMocker.resetMocks() | ||
}) | ||
|
||
it('renders the card on success', async () => { | ||
// arrange: | ||
// mock `fetch()` | ||
fetchMocker.mockResponseOnce(JSON.stringify(responseManeBrca1Json)) | ||
// mount the component | ||
const { wrapper } = await setupMountedComponents( | ||
{ component: SeqvarVariantValidatorCard }, | ||
{ | ||
props: { | ||
seqvar: structuredClone(seqvar) | ||
} | ||
} | ||
) | ||
|
||
// act: | ||
expect(wrapper.text()).toContain('Retrieve Predictions from VariantValidator.org') // guard | ||
const submitButton = wrapper.find('button') | ||
expect(submitButton.exists()).toBe(true) // guard | ||
submitButton.trigger('click') | ||
await nextTick() | ||
|
||
// assert: | ||
expect(wrapper.text()).toContain('Loading...') | ||
const icon = wrapper.find('.v-progress-circular') | ||
expect(icon.exists()).toBe(true) | ||
}) | ||
|
||
it('renders the card with invalid data', async () => { | ||
// arrange: | ||
// mock `fetch()` | ||
fetchMocker.mockResponseOnce(JSON.stringify({ foo: 'foo' })) | ||
// mount the component | ||
const { wrapper } = await setupMountedComponents( | ||
{ component: SeqvarVariantValidatorCard }, | ||
{ | ||
props: { | ||
seqvar: structuredClone(seqvar) | ||
} | ||
} | ||
) | ||
|
||
// act: | ||
expect(wrapper.text()).toContain('Retrieve Predictions from VariantValidator.org') // guard | ||
const submitButton = wrapper.find('button') | ||
expect(submitButton.exists()).toBe(true) // guard | ||
submitButton.trigger('click') | ||
await nextTick() | ||
|
||
// assert: | ||
expect(wrapper.text()).toContain('Loading...') | ||
const icon = wrapper.find('.v-progress-circular') | ||
expect(icon.exists()).toBe(true) | ||
}) | ||
|
||
it('renders the card with response', async () => { | ||
// arrange: | ||
// mock `fetch()` | ||
fetchMocker.mockRejectOnce(new Error('failed to fetch from VariantValidator')) | ||
// mount the component | ||
const { wrapper } = await setupMountedComponents( | ||
{ component: SeqvarVariantValidatorCard }, | ||
{ | ||
props: { | ||
seqvar: structuredClone(seqvar) | ||
} | ||
} | ||
) | ||
|
||
// act: | ||
expect(wrapper.text()).toContain('Retrieve Predictions from VariantValidator.org') // guard | ||
const submitButton = wrapper.find('button') | ||
expect(submitButton.exists()).toBe(true) // guard | ||
submitButton.trigger('click') | ||
await nextTick() | ||
|
||
// assert: | ||
expect(wrapper.text()).toContain('Loading...') | ||
const icon = wrapper.find('.v-progress-circular') | ||
expect(icon.exists()).toBe(true) | ||
}) | ||
}) |
25 changes: 25 additions & 0 deletions
25
src/components/SeqvarVariantValidatorCard/SeqvarVariantValidatorCard.stories.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import type { Meta, StoryObj } from '@storybook/vue3' | ||
|
||
import { SeqvarImpl } from '../../lib/genomicVars' | ||
import SeqvarVariantValidatorCard from './SeqvarVariantValidatorCard.vue' | ||
|
||
const seqvarBrca1 = new SeqvarImpl('grch37', '18', 41215920, 'G', 'T') | ||
|
||
const meta = { | ||
title: 'Seqvar/SeqvarVariantValidatorCard', | ||
component: SeqvarVariantValidatorCard, | ||
tags: ['autodocs'], | ||
argTypes: { | ||
seqvar: { control: { type: 'object' } } | ||
}, | ||
args: { seqvar: seqvarBrca1 } | ||
} satisfies Meta<typeof SeqvarVariantValidatorCard> | ||
|
||
export default meta | ||
type Story = StoryObj<typeof meta> | ||
|
||
export const BRCA1: Story = { | ||
args: { | ||
seqvar: seqvarBrca1 | ||
} | ||
} |
194 changes: 194 additions & 0 deletions
194
src/components/SeqvarVariantValidatorCard/SeqvarVariantValidatorCard.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,194 @@ | ||
<script setup lang="ts"> | ||
import { ref } from 'vue' | ||
import { VariantValidatorClient } from '../../api/variantValidator' | ||
import { type Seqvar } from '../../lib/genomicVars' | ||
import DocsLink from '../DocsLink/DocsLink.vue' | ||
enum VariantValidatorStates { | ||
Initial = 0, | ||
Running = 1, | ||
Done = 2, | ||
Error = 3 | ||
} | ||
interface Props { | ||
seqvar?: Seqvar | ||
} | ||
const props = defineProps<Props>() | ||
const variantValidatorState = ref<VariantValidatorStates>(VariantValidatorStates.Initial) | ||
const variantValidatorResults = ref<any>(null) | ||
const primaryAssemblyLoci = ref<any | null>(null) | ||
const variantValidatorClient = new VariantValidatorClient() | ||
interface KeyValue { | ||
key: string | ||
value: string | ||
} | ||
const queryVariantValidatorApi = async () => { | ||
if (!props.seqvar) { | ||
// Short-circuit unless `props.seqvar` is defined. | ||
return | ||
} | ||
variantValidatorState.value = VariantValidatorStates.Running | ||
variantValidatorResults.value = null | ||
primaryAssemblyLoci.value = null | ||
try { | ||
const res = await variantValidatorClient.fetchVvResults(props.seqvar) | ||
const items: KeyValue[] = [] | ||
let metadata = null | ||
for (const key in res) { | ||
const value = res[key] | ||
if (primaryAssemblyLoci.value === null) { | ||
primaryAssemblyLoci.value = value.primary_assembly_loci | ||
} | ||
if (value?.submitted_variant?.length) { | ||
items.push({ | ||
key, | ||
value | ||
}) | ||
} else if (key == 'metadata') { | ||
metadata = value | ||
} | ||
} | ||
variantValidatorResults.value = { items, metadata } | ||
variantValidatorState.value = VariantValidatorStates.Done | ||
} catch (err) { | ||
variantValidatorState.value = VariantValidatorStates.Error | ||
return | ||
} | ||
} | ||
</script> | ||
|
||
<template> | ||
<template v-if="!seqvar"> | ||
<v-skeleton-loader type="card" /> | ||
</template> | ||
<v-card v-else> | ||
<v-card-title class="pb-0 pr-2"> | ||
Variant Validator | ||
<DocsLink anchor="variant-validator" /> | ||
</v-card-title> | ||
<v-card-subtitle class="text-overline"> | ||
Retrieve Predictions from VariantValidator.org | ||
</v-card-subtitle> | ||
<v-card-text class="mt-0 pt-0"> | ||
<div v-if="variantValidatorState === VariantValidatorStates.Running"> | ||
<div class="alert alert-info pt-3"> | ||
<v-progress-circular indeterminate class="mr-3" /> | ||
Loading... | ||
</div> | ||
</div> | ||
<template v-else-if="variantValidatorState === VariantValidatorStates.Done"> | ||
<v-list class="d-flex flex-row"> | ||
<v-list-item class="px-0 mr-6"> | ||
<v-list-item-title> VariantValidator HGVS Version </v-list-item-title> | ||
<v-list-item-subtitle> | ||
{{ variantValidatorResults.metadata?.variantvalidator_hgvs_version ?? 'N/A' }} | ||
</v-list-item-subtitle> | ||
</v-list-item> | ||
<v-list-item class="px-0"> | ||
<v-list-item-title> VariantValidator Version </v-list-item-title> | ||
<v-list-item-subtitle> | ||
{{ variantValidatorResults.metadata?.variantvalidator_version ?? 'N/A' }} | ||
</v-list-item-subtitle> | ||
</v-list-item> | ||
</v-list> | ||
|
||
<div class="text-overline">Transcript Variants</div> | ||
|
||
<v-table density="compact"> | ||
<thead> | ||
<tr> | ||
<th class="font-weight-bold">Gene Symbol</th> | ||
<th class="font-weight-bold">Transcript Variant</th> | ||
<th class="font-weight-bold">Protein Variant</th> | ||
</tr> | ||
</thead> | ||
<tbody> | ||
<template v-for="{ key, value } in variantValidatorResults.items" :key="key"> | ||
<template v-if="!value.gene_symbol?.length && value.validation_warnings?.length"> | ||
<tr> | ||
<td colspan="3" class="font-italic text-center"> | ||
{{ value.validation_warnings.join(', ') }} | ||
</td> | ||
</tr> | ||
</template> | ||
<tr> | ||
<td>{{ value.gene_symbol }}</td> | ||
<td>{{ value.hgvs_transcript_variant }}</td> | ||
<td>{{ value.hgvs_predicted_protein_consequence?.slr || '—' }}</td> | ||
</tr> | ||
</template> | ||
</tbody> | ||
</v-table> | ||
|
||
<div class="text-overline">Genomic Variants</div> | ||
|
||
<v-table density="compact"> | ||
<thead> | ||
<tr> | ||
<th class="font-weight-bold">Variant Description</th> | ||
<th class="font-weight-bold">VCF Description</th> | ||
</tr> | ||
</thead> | ||
<tbody> | ||
<tr v-if="primaryAssemblyLoci?.grch37?.hgvs_genomic_description"> | ||
<td> | ||
{{ primaryAssemblyLoci?.grch37.hgvs_genomic_description }} | ||
</td> | ||
<td> | ||
GRCh37:{{ primaryAssemblyLoci?.grch37?.vcf?.chr }}:{{ | ||
primaryAssemblyLoci?.grch37?.vcf?.pos | ||
}}:{{ primaryAssemblyLoci?.grch37?.vcf?.ref }}:{{ | ||
primaryAssemblyLoci?.grch37?.vcf?.alt | ||
}} | ||
</td> | ||
</tr> | ||
<tr v-if="primaryAssemblyLoci?.grch38?.hgvs_genomic_description"> | ||
<td> | ||
{{ primaryAssemblyLoci?.grch38.hgvs_genomic_description }} | ||
</td> | ||
<td> | ||
GRCh38:{{ primaryAssemblyLoci?.grch38?.vcf?.chr }}:{{ | ||
primaryAssemblyLoci?.grch38?.vcf?.pos | ||
}}:{{ primaryAssemblyLoci?.grch38?.vcf?.ref }}:{{ | ||
primaryAssemblyLoci?.grch38?.vcf?.alt | ||
}} | ||
</td> | ||
</tr> | ||
</tbody> | ||
</v-table> | ||
</template> | ||
<div v-else-if="variantValidatorState === VariantValidatorStates.Error"> | ||
<v-alert color="error" icon="$error" title="Problem Querying VariantValidator.org"> | ||
An error occurred while querying the VariantValidator API. Please try again later. | ||
</v-alert> | ||
</div> | ||
<div class="mt-3"> | ||
<v-btn | ||
prepend-icon="mdi-cloud-upload-outline" | ||
variant="tonal" | ||
rounded="sm" | ||
@click="queryVariantValidatorApi()" | ||
> | ||
Submit to VariantValidator.org | ||
</v-btn> | ||
</div> | ||
</v-card-text> | ||
</v-card> | ||
</template> | ||
|
||
<style scoped> | ||
.variant-validator-result-tab { | ||
float: left; | ||
} | ||
</style> |