Skip to content

Commit

Permalink
feat: adding SeqvarVariantValidatorCard (#55)
Browse files Browse the repository at this point in the history
  • Loading branch information
holtgrewe authored Jan 31, 2024
1 parent c42bff1 commit 7d08b24
Show file tree
Hide file tree
Showing 3 changed files with 337 additions and 0 deletions.
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)
})
})
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
}
}
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 || '&mdash;' }}</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>

0 comments on commit 7d08b24

Please sign in to comment.