Skip to content

Commit

Permalink
[ENH] Enabled pheno level session for response and query (#112)
Browse files Browse the repository at this point in the history
* WIP UI updates

* Implemented pheno sessions

- Updated QueryForm component and its component test
- Updated App component
- Added a test for sessions fields

* Updated `generateTSVString` function to include new session data

* Updated mocked responses to include session data

* Added a test for session data in participant.tsv

* Refactored `APIRequests` e2e test
  • Loading branch information
rmanaem authored Apr 11, 2024
1 parent a4de207 commit 7ba65bf
Show file tree
Hide file tree
Showing 9 changed files with 138 additions and 46 deletions.
20 changes: 14 additions & 6 deletions cypress/component/QueryForm.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ const props = {
sex: null,
diagnosis: null,
isControl: false,
minNumSessions: null,
minNumImagingSessions: null,
minNumPhenotypicSessions: null,
setIsControl: () => {},
assessmentTool: null,
imagingModality: null,
Expand All @@ -60,7 +61,8 @@ describe('QueryForm', () => {
sex={props.sex}
diagnosis={props.diagnosis}
isControl={props.isControl}
minNumSessions={props.minNumSessions}
minNumImagingSessions={props.minNumImagingSessions}
minNumPhenotypicSessions={props.minNumPhenotypicSessions}
setIsControl={props.setIsControl}
assessmentTool={props.assessmentTool}
imagingModality={props.imagingModality}
Expand All @@ -77,7 +79,10 @@ describe('QueryForm', () => {
cy.get('[data-cy="Sex-categorical-field"]').should('be.visible');
cy.get('[data-cy="Diagnosis-categorical-field"]').should('be.visible');
cy.get('[data-cy="healthy-control-checkbox"]').should('be.visible');
cy.get('[data-cy="Minimum number of sessions-continuous-field"]').should('be.visible');
cy.get('[data-cy="Minimum number of imaging sessions-continuous-field"]').should('be.visible');
cy.get('[data-cy="Minimum number of phenotypic sessions-continuous-field"]').should(
'be.visible'
);
cy.get('[data-cy="Assessment tool-categorical-field"]').should('be.visible');
cy.get('[data-cy="Imaging modality-categorical-field"]').should('be.visible');
cy.get('[data-cy="submit-query-button"]').should('be.visible');
Expand All @@ -95,7 +100,8 @@ describe('QueryForm', () => {
sex={props.sex}
diagnosis={props.diagnosis}
isControl={props.isControl}
minNumSessions={props.minNumSessions}
minNumImagingSessions={props.minNumImagingSessions}
minNumPhenotypicSessions={props.minNumPhenotypicSessions}
setIsControl={props.setIsControl}
assessmentTool={props.assessmentTool}
imagingModality={props.imagingModality}
Expand Down Expand Up @@ -125,7 +131,8 @@ describe('QueryForm', () => {
sex={props.sex}
diagnosis={props.diagnosis}
isControl={props.isControl}
minNumSessions={props.minNumSessions}
minNumImagingSessions={props.minNumImagingSessions}
minNumPhenotypicSessions={props.minNumPhenotypicSessions}
setIsControl={props.setIsControl}
assessmentTool={props.assessmentTool}
imagingModality={props.imagingModality}
Expand All @@ -151,7 +158,8 @@ describe('QueryForm', () => {
sex={props.sex}
diagnosis={props.diagnosis}
isControl={props.isControl}
minNumSessions={props.minNumSessions}
minNumImagingSessions={props.minNumImagingSessions}
minNumPhenotypicSessions={props.minNumPhenotypicSessions}
setIsControl={props.setIsControl}
assessmentTool={props.assessmentTool}
imagingModality={props.imagingModality}
Expand Down
9 changes: 8 additions & 1 deletion cypress/e2e/APIRequests.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,15 @@ describe('Successful API query requests', () => {
it('Intercepts the request sent to the API and asserts over the request url', () => {
cy.get('[data-cy="Minimum age-continuous-field"]').type('10');
cy.get('[data-cy="Maximum age-continuous-field"]').type('30');
cy.get('[data-cy="Minimum number of imaging sessions-continuous-field"]').type('2');
cy.get('[data-cy="Minimum number of phenotypic sessions-continuous-field"]').type('3');
cy.get('[data-cy="submit-query-button"]').click();
cy.wait('@call').its('request.url').should('contains', 'min_age=10&max_age=30');
cy.wait('@call')
.its('request.url')
.should('contain', 'min_age=10')
.and('contain', 'max_age=30')
.and('contain', 'min_num_imaging_sessions=2')
.and('contain', 'min_num_phenotypic_sessions=3');
});
});

Expand Down
47 changes: 44 additions & 3 deletions cypress/e2e/ResultsTSV.cy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { mixedResponse } from '../fixtures/mocked-responses';
import { mixedResponse, unprotectedResponse } from '../fixtures/mocked-responses';

describe('Results TSV', () => {
it('Removes a newline character from a dataset name in the downloaded dataset-level results file', () => {
Expand Down Expand Up @@ -37,10 +37,51 @@ describe('Results TSV', () => {
const rows = fileContent.split('\n');

const datasetProtected = rows[1];
const datasetNotProtected = rows[2];
const datasetNotProtected = rows[3];

expect(datasetProtected.split('\t')[7]).to.equal('protected');
expect(datasetNotProtected.split('\t')[7]).to.equal('/ds004116/sub-300100');
expect(datasetNotProtected.split('\t')[8]).to.equal('/ds004116/sub-300101');
});
});
it('Checks whether the rows in the participant.tsv file generated according to session_type', () => {
cy.intercept('query/?*', unprotectedResponse).as('call');
cy.visit('/');
cy.get('[data-cy="submit-query-button"]').click();
cy.wait('@call');
cy.get('[data-cy="select-all-checkbox"]').find('input').check();
cy.get('[data-cy="dataset-level-download-results-button"]').click();
cy.get('[data-cy="participant-level-download-results-button"]').click();
cy.readFile('cypress/downloads/participant-level-results.tsv').then((fileContent) => {
const rows = fileContent.split('\n');

const phenotypicSession = rows[1];
const imagingSession = rows[2];

expect(phenotypicSession.split('\t')[3]).to.equal(
'http://neurobagel.org/vocab/PhenotypicSession'
);
expect(phenotypicSession.split('\t')[4]).to.equal('10.4');
expect(phenotypicSession.split('\t')[5]).to.equal(
'http://purl.bioontology.org/ontology/SNOMEDCT/248152002'
);
expect(phenotypicSession.split('\t')[6]).to.equal(
'http://purl.bioontology.org/ontology/SNOMEDCT/370143000'
);
expect(phenotypicSession.split('\t')[7]).to.equal(
'https://www.cognitiveatlas.org/task/id/trm_4f2419c4a1646'
);
expect(phenotypicSession.split('\t')[8]).to.equal('');
expect(phenotypicSession.split('\t')[11]).to.equal('');

expect(imagingSession.split('\t')[3]).to.equal('http://neurobagel.org/vocab/ImagingSession');
expect(imagingSession.split('\t')[4]).to.equal('');
expect(imagingSession.split('\t')[5]).to.equal('');
expect(imagingSession.split('\t')[6]).to.equal('');
expect(imagingSession.split('\t')[7]).to.equal('');
expect(imagingSession.split('\t')[8]).to.equal('/ds004116/sub-300101');
expect(imagingSession.split('\t')[11]).to.equal(
'http://purl.org/nidash/nidm#FlowWeighted, http://purl.org/nidash/nidm#T2Weighted'
);
});
});
});
29 changes: 18 additions & 11 deletions cypress/fixtures/mocked-responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,24 +29,25 @@ const unprotectedDatasetSnippet = {
{
sub_id: 'sub-300100',
session_id: 'ses-nb01',
num_sessions: '1',
num_matching_phenotypic_sessions: '1',
num_matching_imaging_sessions: '0',
session_type: 'http://neurobagel.org/vocab/PhenotypicSession',
age: '10.4',
sex: 'http://purl.bioontology.org/ontology/SNOMEDCT/248152002',
diagnosis: [null],
diagnosis: ['http://purl.bioontology.org/ontology/SNOMEDCT/370143000'],
subject_group: null,
assessment: [null],
image_modal: [
'http://purl.org/nidash/nidm#FlowWeighted',
'http://purl.org/nidash/nidm#T2Weighted',
],
session_file_path: '/ds004116/sub-300100',
assessment: ['https://www.cognitiveatlas.org/task/id/trm_4f2419c4a1646'],
image_modal: [null],
session_file_path: null,
},
{
sub_id: 'sub-300101',
session_id: 'ses-nb01',
num_sessions: '1',
age: '10.4',
sex: 'http://purl.bioontology.org/ontology/SNOMEDCT/248152002',
num_matching_phenotypic_sessions: '0',
num_matching_imaging_sessions: '1',
session_type: 'http://neurobagel.org/vocab/ImagingSession',
age: null,
sex: null,
diagnosis: [null],
subject_group: null,
assessment: [null],
Expand Down Expand Up @@ -80,6 +81,12 @@ export const protectedResponse2 = {
nodes_response_status: 'success',
};

export const unprotectedResponse = {
errors: [],
responses: [unprotectedDatasetSnippet],
nodes_response_status: 'success',
};

// Protected Response with a dataset name containing a newline
// character and a mix of protected and unprotected results
export const mixedResponse = {
Expand Down
22 changes: 17 additions & 5 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ function App() {
const [sex, setSex] = useState<FieldInput>(null);
const [diagnosis, setDiagnosis] = useState<FieldInput>(null);
const [isControl, setIsControl] = useState<boolean>(false);
const [minNumSessions, setMinNumSessions] = useState<number | null>(null);
const [minNumImagingSessions, setMinNumSessions] = useState<number | null>(null);
const [minNumPhenotypicSessions, setMinNumPhenotypicSessions] = useState<number | null>(null);
const [assessmentTool, setAssessmentTool] = useState<FieldInput>(null);
const [imagingModality, setImagingModality] = useState<FieldInput>(null);
const [searchParams, setSearchParams] = useSearchParams();
Expand Down Expand Up @@ -190,9 +191,12 @@ function App() {
case 'Maximum age':
setMaxAge(value);
break;
case 'Minimum number of sessions':
case 'Minimum number of imaging sessions':
setMinNumSessions(value);
break;
case 'Minimum number of phenotypic sessions':
setMinNumPhenotypicSessions(value);
break;
default:
break;
}
Expand Down Expand Up @@ -236,7 +240,14 @@ function App() {
setQueryParam('sex', sex, queryParams);
setQueryParam('diagnosis', isControl ? null : diagnosis, queryParams);
queryParams.set('is_control', isControl ? 'true' : '');
queryParams.set('min_num_sessions', minNumSessions ? minNumSessions.toString() : '');
queryParams.set(
'min_num_imaging_sessions',
minNumImagingSessions ? minNumImagingSessions.toString() : ''
);
queryParams.set(
'min_num_phenotypic_sessions',
minNumPhenotypicSessions ? minNumPhenotypicSessions.toString() : ''
);
setQueryParam('assessment', assessmentTool, queryParams);
setQueryParam('image_modal', imagingModality, queryParams);

Expand Down Expand Up @@ -320,7 +331,7 @@ function App() {
</>
)}

<div className="grid grid-cols-4 grid-rows-1 gap-4">
<div className="grid grid-cols-4 gap-4">
<div>
<QueryForm
availableNodes={availableNodes}
Expand All @@ -332,7 +343,8 @@ function App() {
sex={sex}
diagnosis={diagnosis}
isControl={isControl}
minNumSessions={minNumSessions}
minNumImagingSessions={minNumImagingSessions}
minNumPhenotypicSessions={minNumPhenotypicSessions}
setIsControl={setIsControl}
assessmentTool={assessmentTool}
imagingModality={imagingModality}
Expand Down
4 changes: 2 additions & 2 deletions src/components/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,14 @@ function Navbar() {
<Badge badgeContent={latestReleaseTag}>
<Typography variant="h5">Neurobagel Query</Typography>
</Badge>
<Typography variant="body1" className="text-gray-500">
<Typography className="text-gray-500">
Define and find cohorts at the subject level
</Typography>
</div>
</div>
<div className="flex">
<IconButton size="small" href="https://neurobagel.org/query_tool/" target="_blank">
Documentation
<Typography>Documentation</Typography>
</IconButton>
<IconButton href="https://github.com/neurobagel/react-query-tool/" target="_blank">
<GitHubIcon />
Expand Down
29 changes: 20 additions & 9 deletions src/components/QueryForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ function QueryForm({
sex,
diagnosis,
isControl,
minNumSessions,
minNumImagingSessions,
minNumPhenotypicSessions,
setIsControl,
assessmentTool,
imagingModality,
Expand All @@ -40,7 +41,8 @@ function QueryForm({
diagnosis: FieldInput;
isControl: boolean;
setIsControl: (value: boolean) => void;
minNumSessions: number | null;
minNumImagingSessions: number | null;
minNumPhenotypicSessions: number | null;
assessmentTool: FieldInput;
imagingModality: FieldInput;
updateCategoricalQueryParams: (label: string, value: FieldInput) => void;
Expand All @@ -64,21 +66,23 @@ function QueryForm({

const minAgeHelperText: string = validateContinuousValue(minAge);
const maxAgeHelperText: string = validateContinuousValue(maxAge);
const minNumSessionsHelperText: string = validateContinuousValue(minNumSessions);
const minNumImagingSessionsHelperText: string = validateContinuousValue(minNumImagingSessions);
const minNumPhenotypicSessionsHelperText: string =
validateContinuousValue(minNumPhenotypicSessions);

const minAgeExceedsMaxAge: boolean = minAge && maxAge ? minAge > maxAge : false;
const disableSubmit: boolean =
minAgeExceedsMaxAge ||
minAgeHelperText !== '' ||
maxAgeHelperText !== '' ||
minNumSessionsHelperText !== '';
minNumImagingSessionsHelperText !== '';

return (
<div
className={
isFederationAPI
? 'grid grid-cols-2 grid-rows-9 gap-2'
: 'grid grid-cols-2 grid-rows-8 gap-2'
: 'grid grid-cols-2 grid-rows-10 gap-2'
}
>
{isFederationAPI && (
Expand Down Expand Up @@ -153,20 +157,27 @@ function QueryForm({
</div>
<div className={isFederationAPI ? 'col-span-2 row-start-6' : 'col-span-2 row-start-5'}>
<ContinuousField
helperText={minNumSessionsHelperText}
label="Minimum number of sessions"
helperText={minNumImagingSessionsHelperText}
label="Minimum number of imaging sessions"
onFieldChange={updateContinuousQueryParams}
/>
</div>
<div className={isFederationAPI ? 'col-span-2 row-start-7' : 'col-span-2 row-start-6'}>
<ContinuousField
helperText={minNumPhenotypicSessionsHelperText}
label="Minimum number of phenotypic sessions"
onFieldChange={updateContinuousQueryParams}
/>
</div>
<div className={isFederationAPI ? 'col-span-2 row-start-8' : 'col-span-2 row-start-7'}>
<CategoricalField
label="Assessment tool"
options={assessmentOptions.map((a) => ({ label: a.Label, id: a.TermURL }))}
onFieldChange={(label, value) => updateCategoricalQueryParams(label, value)}
inputValue={assessmentTool}
/>
</div>
<div className={isFederationAPI ? 'col-span-2 row-start-8' : 'col-span-2 row-start-7'}>
<div className={isFederationAPI ? 'col-span-2 row-start-9' : 'col-span-2 row-start-8'}>
<CategoricalField
label="Imaging modality"
options={Object.entries(modalities).map(([, value]) => ({
Expand All @@ -177,7 +188,7 @@ function QueryForm({
inputValue={imagingModality}
/>
</div>
<div className={isFederationAPI ? 'row-start-9' : 'row-start-8'}>
<div className={isFederationAPI ? 'row-start-10' : 'row-start-9'}>
<Button
data-cy="submit-query-button"
disabled={disableSubmit}
Expand Down
Loading

0 comments on commit 7ba65bf

Please sign in to comment.