diff --git a/src/app/features/genes/components/gene-nominated-targets/gene-nominated-targets.component.spec.ts b/src/app/features/genes/components/gene-nominated-targets/gene-nominated-targets.component.spec.ts index 5cdd541e..96d9c5ba 100644 --- a/src/app/features/genes/components/gene-nominated-targets/gene-nominated-targets.component.spec.ts +++ b/src/app/features/genes/components/gene-nominated-targets/gene-nominated-targets.component.spec.ts @@ -10,6 +10,9 @@ import { HttpClientTestingModule } from '@angular/common/http/testing'; // -------------------------------------------------------------------------- // import { GeneNominatedTargetsComponent } from './'; import { ApiService, HelperService } from '../../../../core/services'; +import { Gene, GenesResponse } from '../../../../models'; +import { geneMock1, targetNominationMock1 } from '../../../../testing/gene-mocks'; +import { of } from 'rxjs'; // -------------------------------------------------------------------------- // // Tests @@ -17,6 +20,12 @@ import { ApiService, HelperService } from '../../../../core/services'; describe('Component: Gene Nominated Targets', () => { let fixture: ComponentFixture; let component: GeneNominatedTargetsComponent; + let element: HTMLElement; + let mockApiService: ApiService; + + const COLUMN_INDICES = { + 'cohort_study': 4 + }; beforeEach(async () => { await TestBed.configureTestingModule({ @@ -29,10 +38,120 @@ describe('Component: Gene Nominated Targets', () => { beforeEach(async () => { fixture = TestBed.createComponent(GeneNominatedTargetsComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); + const setUp = (genes: Gene[]) => { + const genesResponse: GenesResponse = { + items: genes + }; + mockApiService = TestBed.inject(ApiService); + spyOn(mockApiService, 'getNominatedGenes').and.returnValue( + of(genesResponse) + ); + fixture.detectChanges(); + element = fixture.nativeElement; + + expect(mockApiService.getNominatedGenes).toHaveBeenCalled(); + + const table = element.querySelector('table'); + expect(table).not.toBeNull(); + + const rows = Array.from( + table?.querySelectorAll('tbody tr') || [] + ) as HTMLTableRowElement[]; + + return { rows }; + }; + it('should create', () => { + setUp([geneMock1]); expect(component).toBeTruthy(); }); + + it('should not show null study values', () => { + const gene: Gene = { + ...geneMock1, + target_nominations: [ + { ...targetNominationMock1, study: null }, + { ...targetNominationMock1, study: 'XYZ Study, ABC Study' }, + { ...targetNominationMock1, study: '' }, + { ...targetNominationMock1, study: 'Study 123, Study 456' }, + ], + }; + const { rows } = setUp([gene]); + expect(rows.length).toBe(1); + + const cols = rows[0].cells; + expect(cols.length).toBeGreaterThan(COLUMN_INDICES.cohort_study); + + expect(cols[COLUMN_INDICES.cohort_study].textContent?.trim()).toEqual( + 'ABC Study, Study 123, Study 456, XYZ Study' + ); + }); + + it('should display sorted, unique study values', () => { + const expectedStudyString = 'ACT, Banner, BLSA, Kronos, MSBB, ROSMAP'; + const { rows } = setUp([geneMock1]); + + expect(rows.length).toBe(1); + + const cols = rows[0].cells; + expect(cols.length).toBeGreaterThan(COLUMN_INDICES.cohort_study); + + expect(cols[COLUMN_INDICES.cohort_study].textContent?.trim()).toEqual( + expectedStudyString + ); + }); + + it('should correctly flatten comma separated arrays', () => { + setUp([]); + + expect(component.commaFlattenArray([])).toEqual([]); + + expect( + component.commaFlattenArray(['ACT, BLSA, Banner', 'ACT, BLSA, Banner']) + ).toEqual(['ACT', 'BLSA', 'Banner', 'ACT', 'BLSA', 'Banner']); + + expect(component.commaFlattenArray(['A, B, C', 'D', 'E, F, G, H'])).toEqual( + ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'] + ); + + expect(component.commaFlattenArray(['A', 'B', 'C'])).toEqual([ + 'A', + 'B', + 'C', + ]); + }); + + it('should correctly format display values', () => { + setUp([]); + + expect( + component.getCommaSeparatedStringOfUniqueSortedValues([]) + ).toEqual(''); + + expect( + component.getCommaSeparatedStringOfUniqueSortedValues([ + 'ACT', + 'BLSA', + 'Banner', + 'ACT', + 'BLSA', + 'Banner', + ]) + ).toEqual('ACT, Banner, BLSA'); + + expect( + component.getCommaSeparatedStringOfUniqueSortedValues([ + 'Z', + 'Y', + 'X', + 'A', + 'B', + 'C', + 'B', + 'C', + ]) + ).toEqual('A, B, C, X, Y, Z'); + }); }); diff --git a/src/app/features/genes/components/gene-nominated-targets/gene-nominated-targets.component.ts b/src/app/features/genes/components/gene-nominated-targets/gene-nominated-targets.component.ts index befd6a8b..7db6dccb 100644 --- a/src/app/features/genes/components/gene-nominated-targets/gene-nominated-targets.component.ts +++ b/src/app/features/genes/components/gene-nominated-targets/gene-nominated-targets.component.ts @@ -97,8 +97,8 @@ export class GeneNominatedTargetsComponent implements OnInit { // First map all entries nested in the data to a new array if (de.target_nominations?.length) { teamsArray = de.target_nominations.map((nt: TargetNomination) => nt.team); - studyArray = de.target_nominations.map( - (nt: TargetNomination) => nt.study + studyArray = this.removeNullAndEmptyStrings( + de.target_nominations.map((nt: TargetNomination) => nt.study) ); programsArray = de.target_nominations.map( (nt: TargetNomination) => nt.source @@ -120,37 +120,14 @@ export class GeneNominatedTargetsComponent implements OnInit { inputDataArray = this.commaFlattenArray(inputDataArray); // Populate targetNomination display fields - de.teams_display_value = ''; - if (teamsArray.length) { - de.teams_display_value = teamsArray - .filter(this.getUnique) - .sort((a: string, b: string) => a.localeCompare(b)) - .join(', '); - } - - de.study_display_value = ''; - if (teamsArray.length) { - de.study_display_value = studyArray - .filter(this.getUnique) - .sort((a: string, b: string) => a.localeCompare(b)) - .join(', '); - } - - de.programs_display_value = ''; - if (programsArray.length) { - de.programs_display_value = programsArray - .filter(this.getUnique) - .sort((a: string, b: string) => a.localeCompare(b)) - .join(', '); - } - - de.input_data_display_value = ''; - if (inputDataArray.length) { - de.input_data_display_value = inputDataArray - .filter(this.getUnique) - .sort((a: string, b: string) => a.localeCompare(b)) - .join(', '); - } + de.teams_display_value = + this.getCommaSeparatedStringOfUniqueSortedValues(teamsArray); + de.study_display_value = + this.getCommaSeparatedStringOfUniqueSortedValues(studyArray); + de.programs_display_value = + this.getCommaSeparatedStringOfUniqueSortedValues(programsArray); + de.input_data_display_value = + this.getCommaSeparatedStringOfUniqueSortedValues(inputDataArray); de.initial_nomination_display_value = initialNominationArray.length ? Math.min(...initialNominationArray) @@ -185,31 +162,37 @@ export class GeneNominatedTargetsComponent implements OnInit { }); } + removeNullAndEmptyStrings(items: (string | null)[]) { + return items.filter((item) => Boolean(item)) as string[]; + } + getUnique(value: string, index: number, self: any) { return self.indexOf(value) === index; } - commaFlattenArray(array: any[]): any[] { - const finalArray: any[] = []; - if (array.length) { - array.forEach((t) => { - if (t) { - const i = t.indexOf(', '); - if (i > -1) { - const tmpArray = t.split(', '); - finalArray.push(tmpArray[0]); - finalArray.push(tmpArray[1]); - } else { - finalArray.push(t); - } - } else { - finalArray.push(''); - } - }); - array = finalArray; - } + commaFlattenArray(array: string[]): string[] { + const finalArray: string[] = []; + array.forEach((t) => { + const i = t.indexOf(', '); + if (i > -1) { + const tmpArray = t.split(', '); + tmpArray.forEach((val) => finalArray.push(val)); + } else { + finalArray.push(t); + } + }); + return finalArray; + } - return array; + getCommaSeparatedStringOfUniqueSortedValues(inputArray: string[]) { + let display_value = ''; + if (inputArray.length) { + display_value = inputArray + .filter(this.getUnique) + .sort((a: string, b: string) => a.localeCompare(b)) + .join(', '); + } + return display_value; } onSearch(event: any) { diff --git a/src/app/models/genes.ts b/src/app/models/genes.ts index 55f0f6ef..6f8e5939 100644 --- a/src/app/models/genes.ts +++ b/src/app/models/genes.ts @@ -19,7 +19,7 @@ export interface TargetNomination { predicted_therapeutic_direction: string; data_used_to_support_target_selection: string; data_synapseid: string; - study: string; + study: string | null; input_data: string; validation_study_details: string; initial_nomination: number; diff --git a/src/app/testing/gene-mocks.ts b/src/app/testing/gene-mocks.ts index e8062f86..c26bdea0 100644 --- a/src/app/testing/gene-mocks.ts +++ b/src/app/testing/gene-mocks.ts @@ -1,6 +1,24 @@ /* eslint-disable */ -import { Gene, GCTGene } from '../models'; +import { Gene, GCTGene, TargetNomination } from '../models'; + +export const targetNominationMock1: TargetNomination = { + source: 'Treat-AD', + team: 'Emory-Sage-SGC', + rank: '7', + hgnc_symbol: 'MSN', + target_choice_justification: + 'MSN was identified as a potential driver protein based on protein coexpression analysis. The group of proteins coexpressed with MSN is conserved across the 3 datasets considered, is enriched for inflammatory processes, and for protein products of genes near loci previously associated with AD risk. MSN has increased abundance in AD across all 3 cohorts examined, and progressively increases in asymptomatic (prodromal) AD to symptomatic AD, and also correlates with both hallmark AD pathology scores (CERAD for amyloid burden; and Braak for Tau extent of spread). MSN is highly expressed as a marker of disease-associated microglia and/or endothelial cell types.', + predicted_therapeutic_direction: + 'Antagonism predicted to reduce disease progression. Phosphorylation downstream of Rho/Rock influences actin, focal adhesion binding; may have redundancy with EZR and RDX, complicating targeting. MSN-directed therapeutics that improve microglial motility and/or phagocytosis competence would reduce abeta/amyloid plaque burden.', + data_used_to_support_target_selection: + 'Discovery quantitative proteomics of FrCx \r\n WPCNA of multiple and consensus cohorts\r\n ANOVA', + data_synapseid: 'syn17008058', + study: 'ACT, BLSA, Banner', + input_data: 'Protein', + validation_study_details: 'validation studies ongoing', + initial_nomination: 2018, +}; export const geneMock1: Gene = { _id: '628ea1be0e8d04279fdbaa26',