From 25c3fe67ead60e061a2fb03b28d8f96924d5c013 Mon Sep 17 00:00:00 2001 From: Manuel Holtgrewe Date: Mon, 29 Jan 2024 14:33:46 +0100 Subject: [PATCH] feat: adding GeneConditionsCard (#32) --- .../GeneConditionsCard.spec.ts | 157 ++++ .../GeneConditionsCard.stories.ts | 55 ++ .../GeneConditionsCard/GeneConditionsCard.vue | 805 ++++++++++++++++++ src/components/GeneConditionsCard/types.ts | 9 + 4 files changed, 1026 insertions(+) create mode 100644 src/components/GeneConditionsCard/GeneConditionsCard.spec.ts create mode 100644 src/components/GeneConditionsCard/GeneConditionsCard.stories.ts create mode 100644 src/components/GeneConditionsCard/GeneConditionsCard.vue create mode 100644 src/components/GeneConditionsCard/types.ts diff --git a/src/components/GeneConditionsCard/GeneConditionsCard.spec.ts b/src/components/GeneConditionsCard/GeneConditionsCard.spec.ts new file mode 100644 index 0000000..623778a --- /dev/null +++ b/src/components/GeneConditionsCard/GeneConditionsCard.spec.ts @@ -0,0 +1,157 @@ +import fs from 'fs' +import path from 'path' +import { describe, expect, it, test } from 'vitest' +import { nextTick } from 'vue' + +import { setupMountedComponents } from '../../lib/testUtils' +import { Record as GeneInfoRecord } from '../../pbs/annonars/genes/base' +import GeneConditionsCard from './GeneConditionsCard.vue' + +// Load fixture data for gene TGDS (little data) and BRCA1 (lots of data). +const geneInfoTgds = GeneInfoRecord.fromJsonString( + fs.readFileSync( + path.resolve(__dirname, '../GenePathogenicityCard/fixture.geneInfo.TGDS.json'), + 'utf8' + ) +) +const geneInfoBrca1 = GeneInfoRecord.fromJsonString( + fs.readFileSync( + path.resolve(__dirname, '../GenePathogenicityCard/fixture.geneInfo.BRCA1.json'), + 'utf8' + ) +) + +describe.concurrent('GeneConditionsCard.vue', async () => { + test.each([ + ['TGDS', geneInfoTgds], + ['BRCA1', geneInfoBrca1] + ])( + 'renders the ConditionsCard information for %s', + async (_geneSymbol: string, geneInfo: GeneInfoRecord) => { + // arrange: + const { wrapper } = await setupMountedComponents( + { component: GeneConditionsCard }, + { + props: { + geneInfo, + hpoTerms: [] + } + } + ) + + // act: nothing, only test rendering + + // assert: + expect(wrapper.text()).toContain('Associated Conditions') + } + ) + + it('expands via "More".', async () => { + // arrange: + const { wrapper } = await setupMountedComponents( + { component: GeneConditionsCard }, + { + props: { + geneInfo: geneInfoTgds, + hpoTerms: [] + } + } + ) + + // act: + expect(wrapper.text()).not.toContain('Orphanet Disorders') // guard + expect(wrapper.text()).not.toContain('OMIM Diseases') // guard + const expandButton = wrapper.find('#conditions-card-expand-button') + expect(expandButton.exists()).toBe(true) // guard + await expandButton.trigger('click') + + // assert: + expect(wrapper.text()).toContain('Orphanet Disorders') + expect(wrapper.text()).toContain('OMIM Diseases') + }) + + // skipping this test as we cannot properly emit for VSwitch + it.skip('shows numerical values for HPO terms.', async () => { + // arrange: + const { wrapper } = await setupMountedComponents( + { component: GeneConditionsCard }, + { + props: { + geneInfo: geneInfoTgds, + hpoTerms: [ + { + term_id: 'HP:0000001', + name: 'Example HPO Term' + }, + { + term_id: 'HP:0000002', + name: 'Example HPO Term 2' + } + ] + } + } + ) + + // act: + expect(wrapper.text()).toContain('Example HPO Term') // guard + expect(wrapper.text()).toContain('Example HPO Term 2') // guard + expect(wrapper.text()).not.toContain('HP:0000001') // guard + expect(wrapper.text()).not.toContain('HP:0000002') // guard + + const vSwitches = wrapper.findAllComponents({ name: 'VSwitch' }) + expect(vSwitches.length).toBe(2) // guard + const showTermsIdSwitch = vSwitches.find((vSwitch) => vSwitch.text() === 'numeric terms') + expect(showTermsIdSwitch?.exists()).toBe(true) // guard + + await showTermsIdSwitch!.trigger('click') + await showTermsIdSwitch!.vm.$emit('change', true) + await showTermsIdSwitch!.vm.$emit('click') + await nextTick() + + // assert: + expect(showTermsIdSwitch?.vm.$props.value).toBe(true) + console.log(wrapper.text()) + expect(wrapper.text()).toContain('HP:0000001') + expect(wrapper.text()).toContain('HP:0000002') + }) + + // skipping this test as we cannot properly emit for VSwitch + it.skip('shows links for HPO terms.', async () => { + // arrange: + const { wrapper } = await setupMountedComponents( + { component: GeneConditionsCard }, + { + props: { + geneInfo: geneInfoBrca1, + hpoTerms: [ + { + term_id: 'HP:0000001', + name: 'Example HPO Term' + }, + { + term_id: 'HP:0000002', + name: 'Example HPO Term 2' + } + ] + } + } + ) + + // act: + expect(wrapper.text()).toContain('Example HPO Term') // guard + expect(wrapper.text()).toContain('Example HPO Term 2') // guard + expect(wrapper.html()).toContain('https://hpo.jax.org/app/browse/term/HP:0000001') // guard + + const vSwitches = wrapper.findAllComponents({ name: 'VSwitch' }) + expect(vSwitches.length).toBe(2) // guard + const showTermsIdSwitch = vSwitches.find((vSwitch) => vSwitch.text() === 'show links') + expect(showTermsIdSwitch?.exists()).toBe(true) // guard + + await showTermsIdSwitch?.vm.$emit('change', false) + await showTermsIdSwitch?.vm.$emit('click') + + // assert: + expect(showTermsIdSwitch?.vm.$props.value).toBe(false) + expect(wrapper.html()).not.toContain('https://hpo.jax.org/app/browse/term/HP:0000001') + }) +}) diff --git a/src/components/GeneConditionsCard/GeneConditionsCard.stories.ts b/src/components/GeneConditionsCard/GeneConditionsCard.stories.ts new file mode 100644 index 0000000..779f6ae --- /dev/null +++ b/src/components/GeneConditionsCard/GeneConditionsCard.stories.ts @@ -0,0 +1,55 @@ +import { JsonValue } from '@protobuf-ts/runtime' +import type { Meta, StoryObj } from '@storybook/vue3' + +import { Record as GeneInfoRecord } from '../../pbs/annonars/genes/base' +import geneInfoBrca1Json from '../GenePathogenicityCard/fixture.geneInfo.BRCA1.json' +import geneInfoTgdsJson from '../GenePathogenicityCard/fixture.geneInfo.TGDS.json' +import GeneConditionsCard from './GeneConditionsCard.vue' + +// Here, fixture data is loaded via `import` from JSON file. Reading the file +// as in the tests fails with "process is not defined" error in the browser. + +// @ts-ignore +const geneInfoTgds = GeneInfoRecord.fromJson(geneInfoTgdsJson as JsonValue) +// @ts-ignore +const geneInfoBrca1 = GeneInfoRecord.fromJson(geneInfoBrca1Json as JsonValue) + +const meta = { + title: 'Gene/GeneConditionsCard', + component: GeneConditionsCard, + tags: ['autodocs'], + argTypes: { + geneInfo: { control: { type: 'object' } }, + hpoTerms: { control: { type: 'object' } } + }, + args: { + geneInfo: geneInfoTgds, + hpoTerms: [ + { + term_id: 'HP:0001654', + name: 'Abnormal heart valve morphology' + } + ] + } +} satisfies Meta + +export default meta +type Story = StoryObj + +export const TGDS: Story = { + args: { + geneInfo: geneInfoTgds + } +} + +export const BRCA1: Story = { + args: { + geneInfo: geneInfoBrca1 + } +} + +export const Undefined: Story = { + args: { + geneInfo: undefined + } +} diff --git a/src/components/GeneConditionsCard/GeneConditionsCard.vue b/src/components/GeneConditionsCard/GeneConditionsCard.vue new file mode 100644 index 0000000..b106ba7 --- /dev/null +++ b/src/components/GeneConditionsCard/GeneConditionsCard.vue @@ -0,0 +1,805 @@ + + + diff --git a/src/components/GeneConditionsCard/types.ts b/src/components/GeneConditionsCard/types.ts new file mode 100644 index 0000000..15f173f --- /dev/null +++ b/src/components/GeneConditionsCard/types.ts @@ -0,0 +1,9 @@ +/** Representation of an HPO term. */ +export interface HpoTerm { + /** Term identifier. */ + term_id: string + /** Term name. */ + name: string + /** Term definition, if any. */ + definition?: string +}