Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
221 changes: 221 additions & 0 deletions cypress/component/StudyAssets/clinical_trial_list.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
import React from 'react'
import { mount } from 'cypress/react'
import ClinicalTrialAddEdit from 'src/components/clinical_trial_list/ClinicalTrialAddEdit'
import ClinicalTrialList from 'src/components/clinical_trial_list/ClinicalTrialList'
import ClinicalTrialRow from 'src/components/clinical_trial_list/ClinicalTrialRow'
import ClinicalTrialSummary from 'src/components/clinical_trial_list/ClinicalTrialSummary'
import {
ClinicalTrial,
ClinicalTrialStatus,
ClinicalTrialInterventionType,
ClinicalTrialPhase,
} from 'src/types/model'

const sampleTrial: ClinicalTrial = {
clinicalTrialId: 'ct1',
studyId: 's1',
title: 'Baseline Trial',
registry: 'ClinicalTrials.gov',
identifier: 'NCT00000001',
status: ClinicalTrialStatus.COMPLETED,
sponsor: 'NIH',
startDate: '2024-01-01',
endDate: '2025-12-31',
interventionType: ClinicalTrialInterventionType.DRUG,
description: 'Desc',
phase: ClinicalTrialPhase.PHASE2,
url: 'https://example.com/trial',
tags: ['oncology', 'phase2'],
}

const ClinicalTrialListHarness: React.FC<{ initial: ClinicalTrial[] }> = ({ initial }) => {
const [items, setItems] = React.useState<ClinicalTrial[]>(initial)
return (
<ClinicalTrialList
clinicalTrials={items}
columnsToShow={['title']}
onClinicalTrialChange={setItems}
disabled={false}
/>
)
}

describe('ClinicalTrialAddEdit', () => {
it('disables Add until required fields filled then adds', () => {
const added: ClinicalTrial[] = []
mount(
<ClinicalTrialAddEdit
id={-1}
clinicalTrial={undefined}
clinicalTrials={[]}
closeAction={cy.stub().as('close')}
onClinicalTrialChange={(cts) => { added.splice(0, added.length, ...cts) }}
/>,
)
cy.get('.collaborator-form-add-save-button').should('be.disabled')
cy.get('#title').type('My Trial')
cy.get('#registry').type('Registry X')
cy.get('#identifier').type('ID123')
cy.get('#status').click()
cy.get('#status').type('Completed{enter}')
cy.get('#sponsor').type('Sponsor Y')
cy.get('#startDate').type('2024-06-01')
cy.get('#interventionType').click()
cy.get('#interventionType').type('Drug{enter}')
cy.get('#phase').click()
cy.get('#phase').type('Phase 2{enter}')
cy.get('#url').type('https://trial.example.com')

cy.get('.collaborator-form-add-save-button').should('not.be.disabled').click()
cy.wrap(null).then(() => {
expect(added.length).to.eq(1)
expect(added[0].title).to.eq('My Trial')
expect(added[0].identifier).to.eq('ID123')
expect(added[0].status).to.eq(ClinicalTrialStatus.COMPLETED)
})
})

it('edits existing trial and saves changes', () => {
const trials: ClinicalTrial[] = [sampleTrial]
mount(
<ClinicalTrialAddEdit
id={0}
clinicalTrial={sampleTrial}
clinicalTrials={trials}
closeAction={cy.stub().as('close')}
onClinicalTrialChange={(updated) => {
expect(updated[0].title).to.eq('Baseline Trial Edited')
expect(updated[0].phase).to.eq(ClinicalTrialPhase.PHASE2)
}}
/>,
)
cy.get('#title').clear()
cy.get('#title').type('Baseline Trial Edited')
cy.get('.collaborator-form-add-save-button').click()
})
})

describe('ClinicalTrialSummary', () => {
it('renders tags and date range', () => {
mount(
<ClinicalTrialSummary
clinicalTrial={sampleTrial}
columnsToShow={[
'title',
'registry',
'identifier',
'status',
'sponsor',
'dateRange',
'interventionType',
'phase',
'url',
'tags',
]}
editAction={cy.stub()}
deleteAction={cy.stub()}
disabled={false}
/>,
)
cy.contains('Baseline Trial').should('exist')
cy.contains('ClinicalTrials.gov').should('exist')
cy.contains('NCT00000001').should('exist')
cy.contains(/Completed/i).should('exist')
cy.contains('NIH').should('exist')
cy.contains('2024-01-01 → 2025-12-31').should('exist')
cy.contains(/Drug/i).should('exist')
cy.contains(/Phase II|Phase 2/i).should('exist')
cy.contains('https://example.com/trial').should('exist')
cy.contains('oncology, phase2').should('exist')
})
})

describe('ClinicalTrialRow', () => {
it('shows summary when not in edit mode and triggers edit', () => {
mount(
<ClinicalTrialRow
id={0}
editMode={false}
clinicalTrial={sampleTrial}
clinicalTrials={[sampleTrial]}
columnsToShow={['title', 'status']}
editAction={cy.stub().as('edit')}
deleteAction={cy.stub()}
closeAction={cy.stub()}
onClinicalTrialChange={cy.stub()}
disabled={false}
/>,
)
cy.contains('Baseline Trial').should('exist')
cy.get('.glyphicon-pencil').click({ force: true })
cy.get('@edit').should('have.been.calledOnce')
})

it('renders edit form when editMode true', () => {
mount(
<ClinicalTrialRow
id={0}
editMode={true}
clinicalTrial={sampleTrial}
clinicalTrials={[sampleTrial]}
columnsToShow={['title']}
editAction={cy.stub()}
deleteAction={cy.stub()}
closeAction={cy.stub()}
onClinicalTrialChange={cy.stub()}
disabled={false}
/>,
)
cy.get('#title').should('have.value', 'Baseline Trial')
})
})

describe('ClinicalTrialList', () => {
it('adds a new clinical trial', () => {
const state: ClinicalTrial[] = []
mount(
<ClinicalTrialList
clinicalTrials={state}
columnsToShow={['title', 'status']}
onClinicalTrialChange={(cts) => { state.splice(0, state.length, ...cts) }}
disabled={false}
/>,
)
cy.get('#add-clinical-trial-btn').click()
cy.get('#title').type('Added Trial')
cy.get('#registry').type('Reg A')
cy.get('#identifier').type('ID999')
cy.get('#status').click()
cy.get('#status').type('Completed{enter}')
cy.get('#sponsor').type('Org Z')
cy.get('#startDate').type('2024-02-02')
cy.get('#interventionType').click()
cy.get('#interventionType').type('Device{enter}')
cy.get('#phase').click()
cy.get('#phase').type('Phase 3{enter}')
cy.get('#url').type('https://added.example.com')
cy.get('.collaborator-form-add-save-button').click({ force: true })
cy.wrap(null).then(() => {
expect(state.length).to.eq(1)
expect(state[0].title).to.eq('Added Trial')
expect(state[0].status).to.eq(ClinicalTrialStatus.COMPLETED)
})
})

it('deletes a clinical trial via modal confirmation', () => {
mount(<ClinicalTrialListHarness initial={[sampleTrial]} />)
cy.contains('Baseline Trial').should('exist')
cy.get('.glyphicon-trash').click({ force: true })
cy.get('.ReactModal__Content')
.should('be.visible')
.within(() => {
cy.get('button')
.filter(':visible')
.contains(/delete/i)
.click({ force: true })
})
cy.get('.ReactModal__Content').should('not.exist')
cy.contains('Baseline Trial').should('not.exist')
cy.get('.collaborator-summary-card').should('have.length', 0)
})
})
127 changes: 127 additions & 0 deletions cypress/component/utils/clinical_trial_enum_utils.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import {
parseLegacyStatus,
parseLegacyPhase,
parseLegacyInterventionType,
statusToDisplay,
phaseToDisplay,
interventionTypeToDisplay,
clinicalTrialStatusSelectOptions,
clinicalTrialPhaseSelectOptions,
clinicalTrialInterventionSelectOptions,
} from 'src/utils/ClinicalTrialEnumUtils'
import {
ClinicalTrialStatus,
ClinicalTrialPhase,
ClinicalTrialInterventionType,
} from 'src/types/model'

describe('ClinicalTrialEnumUtils', () => {
describe('parseLegacyStatus', () => {
it('parses legacy display values', () => {
expect(parseLegacyStatus('Active, not recruiting')).to.eq(ClinicalTrialStatus.ACTIVE_NOT_RECRUITING)
expect(parseLegacyStatus('active, not Recruiting')).to.eq(ClinicalTrialStatus.ACTIVE_NOT_RECRUITING)
expect(parseLegacyStatus('Completed')).to.eq(ClinicalTrialStatus.COMPLETED)
expect(parseLegacyStatus('completed')).to.eq(ClinicalTrialStatus.COMPLETED)
})

it('returns fallback UNKNOWN for invalid input', () => {
expect(parseLegacyStatus('Invalid Status')).to.eq(ClinicalTrialStatus.UNKNOWN)
expect(parseLegacyStatus(undefined)).to.eq(ClinicalTrialStatus.UNKNOWN)
expect(parseLegacyStatus(null)).to.eq(ClinicalTrialStatus.UNKNOWN)
})
})

describe('parseLegacyInterventionType', () => {
it('parses legacy intervention types case-insensitively', () => {
expect(parseLegacyInterventionType('Dietary supplement')).to.eq(ClinicalTrialInterventionType.DIETARY_SUPPLEMENT)
expect(parseLegacyInterventionType('dietary Supplement')).to.eq(ClinicalTrialInterventionType.DIETARY_SUPPLEMENT)
expect(parseLegacyInterventionType('Device')).to.eq(ClinicalTrialInterventionType.DEVICE)
expect(parseLegacyInterventionType('device')).to.eq(ClinicalTrialInterventionType.DEVICE)
})

it('falls back to OTHER for unknown values', () => {
expect(parseLegacyInterventionType('UnknownType')).to.eq(ClinicalTrialInterventionType.OTHER)
})
})

describe('parseLegacyPhase', () => {
it('parses legacy phase values', () => {
expect(parseLegacyPhase('Phase 2')).to.eq(ClinicalTrialPhase.PHASE2)
expect(parseLegacyPhase('phase 2')).to.eq(ClinicalTrialPhase.PHASE2)
expect(parseLegacyPhase('Early Phase 1')).to.eq(ClinicalTrialPhase.EARLY_PHASE1)
})

it('falls back to NA for invalid values', () => {
expect(parseLegacyPhase('Phase X')).to.eq(ClinicalTrialPhase.NA)
})
})

describe('display mapping', () => {
it('maps status enum to display text', () => {
expect(statusToDisplay(ClinicalTrialStatus.RECRUITING)).to.eq('Recruiting')
expect(statusToDisplay(ClinicalTrialStatus.APPROVED_FOR_MARKETING)).to.eq('Approved for marketing')
})

it('maps intervention type enum to display text', () => {
expect(interventionTypeToDisplay(ClinicalTrialInterventionType.BEHAVIORAL)).to.eq('Behavioral')
expect(interventionTypeToDisplay(ClinicalTrialInterventionType.RADIATION)).to.eq('Radiation')
})

it('maps phase enum to display text', () => {
expect(phaseToDisplay(ClinicalTrialPhase.PHASE3)).to.eq('Phase 3')
expect(phaseToDisplay(ClinicalTrialPhase.EARLY_PHASE1)).to.eq('Early Phase 1')
})
})

describe('select options integrity', () => {
const findKey = (opts: { key: string }[], key: string) => opts.some(o => o.key === key)

it('status select options exclude fallback UNKNOWN', () => {
expect(findKey(clinicalTrialStatusSelectOptions, ClinicalTrialStatus.UNKNOWN)).to.equal(false)
})

it('intervention select options exclude fallback OTHER', () => {
expect(findKey(clinicalTrialInterventionSelectOptions, ClinicalTrialInterventionType.OTHER)).to.equal(false)
})

it('phase select options exclude fallback NA', () => {
expect(findKey(clinicalTrialPhaseSelectOptions, ClinicalTrialPhase.NA)).to.equal(false)
})

it('all option keys are unique per group', () => {
const unique = (arr: string[]) => new Set(arr).size === arr.length
expect(unique(clinicalTrialStatusSelectOptions.map(o => o.key))).to.equal(true)
expect(unique(clinicalTrialInterventionSelectOptions.map(o => o.key))).to.equal(true)
expect(unique(clinicalTrialPhaseSelectOptions.map(o => o.key))).to.equal(true)
})

it('displayText matches expected legacy values sample', () => {
const statusSample = clinicalTrialStatusSelectOptions.find(o => o.key === ClinicalTrialStatus.TERMINATED)
expect(statusSample?.displayText).to.eq('Terminated')
const interventionSample = clinicalTrialInterventionSelectOptions.find(o => o.key === ClinicalTrialInterventionType.DRUG)
expect(interventionSample?.displayText).to.eq('Drug')
const phaseSample = clinicalTrialPhaseSelectOptions.find(o => o.key === ClinicalTrialPhase.PHASE4)
expect(phaseSample?.displayText).to.eq('Phase 4')
})
})

describe('round-trip parsing and display', () => {
it('status round-trips legacy display -> enum -> display', () => {
const legacy = 'Active, not recruiting'
const enumVal = parseLegacyStatus(legacy)
expect(statusToDisplay(enumVal)).to.eq(legacy)
})

it('intervention type round-trips', () => {
const legacy = 'Combination product'
const enumVal = parseLegacyInterventionType(legacy)
expect(interventionTypeToDisplay(enumVal)).to.eq(legacy)
})

it('phase round-trips', () => {
const legacy = 'Phase 2'
const enumVal = parseLegacyPhase(legacy)
expect(phaseToDisplay(enumVal)).to.eq(legacy)
})
})
})
Loading
Loading