diff --git a/frontend/src/app/card-row/card-row.component.html b/frontend/src/app/card-row/card-row.component.html index 42301209673db46dd7a6c413ccd04b6bd375f749..0f0d89877b44004f2ff90cc4669ad2fa29a87d9b 100644 --- a/frontend/src/app/card-row/card-row.component.html +++ b/frontend/src/app/card-row/card-row.component.html @@ -1,4 +1,4 @@ -<ng-container *ngIf="(test && value === undefined) || (value)"> +<ng-container *ngIf="shouldShow()"> <div class="row row-sep"> <div class="col-4 field my-2"> {{ label }} diff --git a/frontend/src/app/card-row/card-row.component.spec.ts b/frontend/src/app/card-row/card-row.component.spec.ts index 95ab00d83c82f44f1dd4c222345ef3e7dfe75f0d..2c78f8ff82e7f1a8e0a48c573121e47086688eef 100644 --- a/frontend/src/app/card-row/card-row.component.spec.ts +++ b/frontend/src/app/card-row/card-row.component.spec.ts @@ -1,25 +1,128 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { async, TestBed } from '@angular/core/testing'; import { CardRowComponent } from './card-row.component'; +import { ComponentTester, speculoosMatchers } from 'ngx-speculoos'; +import { Component, ViewChild } from '@angular/core'; + +class CardRowComponentTester extends ComponentTester<CardRowComponent> { + constructor() { + super(CardRowComponent); + } + + get rowDiv() { + return this.element('div.row'); + } + + get labelDiv() { + return this.element('div.field'); + } + + get valueDiv() { + return this.element('div.col'); + } +} + + +/** + * Test gpds-card-row with a provided `ng-template` + */ +@Component({ + selector: 'gpds-test', + template: ` + <gpds-card-row> + <ng-template> + <strong>Value HTML template</strong> + </ng-template> + </gpds-card-row>` +}) +class CardRowWithTemplateComponent { + @ViewChild(CardRowComponent) component: CardRowComponent; +} describe('CardRowComponent', () => { - let component: CardRowComponent; - let fixture: ComponentFixture<CardRowComponent>; + beforeEach(() => jasmine.addMatchers(speculoosMatchers)); - beforeEach(async(() => { + beforeEach(async(() => TestBed.configureTestingModule({ - declarations: [CardRowComponent] - }) - .compileComponents(); - })); + declarations: [CardRowComponent, CardRowWithTemplateComponent] + }).compileComponents() + )); - beforeEach(() => { - fixture = TestBed.createComponent(CardRowComponent); - component = fixture.componentInstance; - fixture.detectChanges(); + + it('should hide falsy value', () => { + const tester = new CardRowComponentTester(); + tester.componentInstance.label = 'Label1'; + tester.componentInstance.value = null; + tester.detectChanges(); + + expect(tester.rowDiv).toBeFalsy(); + }); + + it('should show truthy value', () => { + const tester = new CardRowComponentTester(); + tester.componentInstance.label = 'Label1'; + tester.componentInstance.value = 'Value1'; + tester.detectChanges(); + + expect(tester.rowDiv).toBeTruthy(); + expect(tester.labelDiv).toContainText(tester.componentInstance.label); + expect(tester.valueDiv).toContainText(tester.componentInstance.value); + }); + + it('should hide falsy test', () => { + const tester = new CardRowComponentTester(); + tester.componentInstance.label = 'Label1'; + tester.componentInstance.value = 'Value1'; + tester.componentInstance.test = false; + tester.detectChanges(); + + expect(tester.rowDiv).toBeFalsy(); + }); + + it('should hide truthy test, falsy value', () => { + const tester = new CardRowComponentTester(); + tester.componentInstance.label = 'Label1'; + tester.componentInstance.value = ''; + tester.componentInstance.test = true; + tester.detectChanges(); + + expect(tester.rowDiv).toBeFalsy(); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('should show truthy test, truthy value', () => { + const tester = new CardRowComponentTester(); + tester.componentInstance.label = 'Label1'; + tester.componentInstance.value = 'Value1'; + tester.componentInstance.test = true; + tester.detectChanges(); + + expect(tester.rowDiv).toBeTruthy(); + expect(tester.labelDiv).toContainText(tester.componentInstance.label); + expect(tester.valueDiv).toContainText(tester.componentInstance.value); }); + + + it('should hide falsy test, provided template', async(() => { + const fixture = TestBed.createComponent(CardRowWithTemplateComponent); + fixture.componentInstance.component.label = 'Label1'; + fixture.componentInstance.component.test = ''; + fixture.detectChanges(); + + const element: HTMLElement = fixture.nativeElement; + expect(element.querySelector('div.row')).toBeFalsy(); + })); + + + it('should show truthy test, provided template', async(() => { + const fixture = TestBed.createComponent(CardRowWithTemplateComponent); + const component = fixture.componentInstance.component; + component.label = 'Label2'; + component.test = true; + fixture.detectChanges(); + + const element: HTMLElement = fixture.nativeElement; + expect(element.querySelector('div.row')).toBeTruthy(); + expect(element.querySelector('div.field').textContent).toContain(component.label); + expect(element.querySelector('div.col').textContent).toContain('Value HTML template'); + })); }); diff --git a/frontend/src/app/card-row/card-row.component.ts b/frontend/src/app/card-row/card-row.component.ts index 9def776858b0589e60ae47c14ce1047fcc2a1c76..51e69d8b05b7ce7736f0805cd28c735d9d02906e 100644 --- a/frontend/src/app/card-row/card-row.component.ts +++ b/frontend/src/app/card-row/card-row.component.ts @@ -13,4 +13,13 @@ export class CardRowComponent { @ContentChild(TemplateRef) template: TemplateRef<any>; + shouldShow(): boolean { + return this.test && ( + // Value not provided and template provided + (this.value === undefined && this.template !== undefined) + || + // Or value truthy + !!this.value + ); + } } diff --git a/frontend/src/app/card-section/card-section.component.spec.ts b/frontend/src/app/card-section/card-section.component.spec.ts index 874070e7d85edb2aa22393ae5ab055ea5cb0b5aa..c756a49452350b7ffddcb86486178dd9881b7eb0 100644 --- a/frontend/src/app/card-section/card-section.component.spec.ts +++ b/frontend/src/app/card-section/card-section.component.spec.ts @@ -1,25 +1,61 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { async, TestBed } from '@angular/core/testing'; import { CardSectionComponent } from './card-section.component'; +import { speculoosMatchers } from 'ngx-speculoos'; +import { Component, ViewChild } from '@angular/core'; + + +/** + * Test gpds-card-section with a provided `ng-template` + */ +@Component({ + selector: 'gpds-test', + template: ` + <gpds-card-section> + <ng-template> + <div class="test-body">Body HTML template</div> + </ng-template> + </gpds-card-section>` +}) +class CardSectionTestWrapperComponent { + @ViewChild(CardSectionComponent) component: CardSectionComponent; +} describe('CardSectionComponent', () => { - let component: CardSectionComponent; - let fixture: ComponentFixture<CardSectionComponent>; + beforeEach(() => jasmine.addMatchers(speculoosMatchers)); - beforeEach(async(() => { + beforeEach(async(() => TestBed.configureTestingModule({ - declarations: [CardSectionComponent] - }) - .compileComponents(); + declarations: [CardSectionComponent, CardSectionTestWrapperComponent] + }).compileComponents() + )); + + it('should hide falsy test', async(() => { + const fixture = TestBed.createComponent(CardSectionTestWrapperComponent); + const component = fixture.componentInstance.component; + component.header = 'Header1'; + component.test = ''; + fixture.detectChanges(); + + const element: HTMLElement = fixture.nativeElement; + const cardDiv = element.querySelector('div.card'); + expect(cardDiv).toBeFalsy(); })); - beforeEach(() => { - fixture = TestBed.createComponent(CardSectionComponent); - component = fixture.componentInstance; + + it('should show truthy test', async(() => { + const fixture = TestBed.createComponent(CardSectionTestWrapperComponent); + const component = fixture.componentInstance.component; + component.header = 'Header2'; + component.test = true; fixture.detectChanges(); - }); - it('should create', () => { - expect(component).toBeTruthy(); - }); + const element: HTMLElement = fixture.nativeElement; + const cardDiv = element.querySelector('div.card'); + const headerDiv = element.querySelector('div.card-header'); + const bodyDiv = element.querySelector('div.test-body'); + expect(cardDiv).toBeTruthy(); + expect(headerDiv.textContent).toContain(component.header); + expect(bodyDiv.textContent).toContain('Body HTML template'); + })); }); diff --git a/frontend/src/app/card-table/card-table.component.spec.ts b/frontend/src/app/card-table/card-table.component.spec.ts index e21f25a54632d5ec9310ad09b42518d824f37dbf..da131453876bf0ceefdaafbf9bd6d1b854bdb290 100644 --- a/frontend/src/app/card-table/card-table.component.spec.ts +++ b/frontend/src/app/card-table/card-table.component.spec.ts @@ -1,25 +1,78 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { async, TestBed } from '@angular/core/testing'; import { CardTableComponent } from './card-table.component'; +import { Component, ViewChild } from '@angular/core'; + +/** + * Test gpds-card-table with a simple provided row `ng-template` + */ +@Component({ + selector: 'gpds-test', + template: ` + <gpds-card-table> + <ng-template let-row> + <tr> + <td>{{ row[0] }}</td> + <td>{{ row[1] }}</td> + <td>{{ row[2] }}</td> + </tr> + </ng-template> + </gpds-card-table>` +}) +class CardTableTestWrapperComponent { + @ViewChild(CardTableComponent) component: CardTableComponent; +} describe('CardTableComponent', () => { - let component: CardTableComponent; - let fixture: ComponentFixture<CardTableComponent>; - beforeEach(async(() => { + beforeEach(async(() => TestBed.configureTestingModule({ - declarations: [CardTableComponent] - }) - .compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(CardTableComponent); - component = fixture.componentInstance; + declarations: [CardTableComponent, CardTableTestWrapperComponent] + }).compileComponents() + )); + + const headers = [ + 'h1', 'h2', 'h3' + ]; + const rows = [ + ['a', 'b', 'c'], + ['d', 'e', 'f'], + ['g', 'h', 'i'], + ]; + + it('should hide headers and show rows', () => { + const fixture = TestBed.createComponent(CardTableTestWrapperComponent); + const component = fixture.componentInstance.component; + component.headers = null; + component.rows = rows; fixture.detectChanges(); + + const element: HTMLElement = fixture.nativeElement; + const thead = element.querySelector('thead'); + expect(thead).toBeFalsy(); + + const tds = element.querySelectorAll('td'); + expect(tds.length).toBe(9); + expect(tds[0].textContent).toContain(rows[0][0]); + expect(tds[5].textContent).toContain(rows[1][2]); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('should show headers and rows', () => { + const fixture = TestBed.createComponent(CardTableTestWrapperComponent); + const component = fixture.componentInstance.component; + component.headers = headers; + component.rows = rows; + fixture.detectChanges(); + + const element: HTMLElement = fixture.nativeElement; + const ths = element.querySelectorAll('thead th'); + expect(ths.length).toBe(3); + expect(ths[0].textContent).toContain(headers[0]); + expect(ths[2].textContent).toContain(headers[2]); + + const tds = element.querySelectorAll('td'); + expect(tds.length).toBe(9); + expect(tds[0].textContent).toContain(rows[0][0]); + expect(tds[5].textContent).toContain(rows[1][2]); }); }); diff --git a/frontend/src/app/form/form.component.html b/frontend/src/app/form/form.component.html index f0ffe68003cb3aa53f209e2d2dbaf9c583cfe4c2..8afaa32eb25f3fd1322ecc6651f584edebdcc2c6 100644 --- a/frontend/src/app/form/form.component.html +++ b/frontend/src/app/form/form.component.html @@ -1,22 +1,22 @@ <ul class="nav nav-tabs"> <li class="nav-item"> <a tabindex="0" - class="nav-link {{ activeTab == 'Germplasm' ? 'active' : ''}}" - (click)="activeTab='Germplasm'"> - Germplasm + class="nav-link germplasm {{ getNavClass(tabs.GERMPLASM) }}" + (click)="activeTab=tabs.GERMPLASM"> + {{ tabs.GERMPLASM }} </a> </li> <li class="nav-item"> <a tabindex="1" - class="nav-link {{ activeTab == 'Variable' ? 'active' : ''}}" - (click)="activeTab='Variable'"> - Trait + class="nav-link trait {{ getNavClass(tabs.TRAIT) }}" + (click)="activeTab=tabs.TRAIT"> + {{ tabs.TRAIT }} </a> </li> </ul> <!-- Germplasm tab --> -<div class="{{ activeTab == 'Germplasm' ? 'visible' : 'd-none' }}"> +<div class="germplasm {{ getTabClass(tabs.GERMPLASM) }}"> <!-- Input for the crops field --> <div class="form-group row pt-3"> <label for="crops" class="col-sm-4"> @@ -70,8 +70,8 @@ </div> </div> -<!-- Variable tab --> -<div class="{{ activeTab == 'Variable' ? 'visible' : 'd-none' }}"> +<!-- Trait tab --> +<div class="trait {{ getTabClass(tabs.TRAIT) }}"> <gpds-trait-ontology-widget [criteria$]="criteria$" (initialized)="traitWidgetInitialized.emit($event)"> diff --git a/frontend/src/app/form/form.component.spec.ts b/frontend/src/app/form/form.component.spec.ts index 54ced3d310c6020efa3a41e9db34b0165ac107ca..64edc68383f9f1a955177b69a8db00d9962d41f5 100644 --- a/frontend/src/app/form/form.component.spec.ts +++ b/frontend/src/app/form/form.component.spec.ts @@ -1,33 +1,71 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { async, TestBed } from '@angular/core/testing'; import { FormComponent } from './form.component'; -import { Component, NO_ERRORS_SCHEMA } from '@angular/core'; - +import { Component, EventEmitter, Input, Output } from '@angular/core'; +/** + * Mock gpds-suggestion-field + */ @Component({ selector: 'gpds-suggestion-field', template: '<br/>' }) -class MockFieldComponent { +class MockSuggestionFieldComponent { + @Input() criteria$: any; +} + +/** + * Mock gpds-trait-ontology-widget + */ +@Component({ + selector: 'gpds-trait-ontology-widget', + template: '<br/>' +}) +class MockTraitWidgetComponent { + @Input() criteria$: any; + @Output() initialized = new EventEmitter(); } describe('FormComponent', () => { - let component: FormComponent; - let fixture: ComponentFixture<FormComponent>; - beforeEach(() => { + beforeEach(async(() => TestBed.configureTestingModule({ - declarations: [FormComponent, MockFieldComponent], - schemas: [NO_ERRORS_SCHEMA] - }).compileComponents(); + declarations: [FormComponent, MockSuggestionFieldComponent, MockTraitWidgetComponent], + }).compileComponents() + )); - fixture = TestBed.createComponent(FormComponent); - component = fixture.componentInstance; + it('should switch tabs', async(() => { + const fixture = TestBed.createComponent(FormComponent); fixture.detectChanges(); - }); - it('should create', () => { + const element: HTMLElement = fixture.nativeElement; + const germplasmNav: HTMLElement = element.querySelector('a.germplasm'); + const germplasmTab: HTMLElement = element.querySelector('div.germplasm'); + const traitNav: HTMLElement = element.querySelector('a.trait'); + const traitTab: HTMLElement = element.querySelector('div.trait'); + + // Check default tab is active + expect(germplasmNav.getAttribute('class')).toContain('active'); + expect(germplasmTab.getAttribute('class')).toContain('visible'); + expect(traitNav.getAttribute('class')).not.toContain('active'); + expect(traitTab.getAttribute('class')).toContain('d-none'); + + traitNav.click(); + fixture.detectChanges(); + + // Check tab switched + expect(traitNav.getAttribute('class')).toContain('active'); + expect(traitTab.getAttribute('class')).toContain('visible'); + expect(germplasmNav.getAttribute('class')).not.toContain('active'); + expect(germplasmTab.getAttribute('class')).toContain('d-none'); + + germplasmNav.click(); fixture.detectChanges(); - expect(component).toBeTruthy(); - }); + + // Check tab switched back + expect(germplasmNav.getAttribute('class')).toContain('active'); + expect(germplasmTab.getAttribute('class')).toContain('visible'); + expect(traitNav.getAttribute('class')).not.toContain('active'); + expect(traitTab.getAttribute('class')).toContain('d-none'); + })); }); diff --git a/frontend/src/app/form/form.component.ts b/frontend/src/app/form/form.component.ts index b546c41a84870bf690f8fc60c1751a898ff00838..c2d37198bea1f6cf5bf70dc923555cd94be995f2 100644 --- a/frontend/src/app/form/form.component.ts +++ b/frontend/src/app/form/form.component.ts @@ -2,6 +2,11 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; import { DataDiscoveryCriteria } from '../models/data-discovery.model'; import { BehaviorSubject } from 'rxjs'; +enum Tabs { + GERMPLASM = 'Germplasm', + TRAIT = 'Trait' +} + @Component({ selector: 'gpds-form', templateUrl: './form.component.html', @@ -10,5 +15,18 @@ import { BehaviorSubject } from 'rxjs'; export class FormComponent { @Input() criteria$: BehaviorSubject<DataDiscoveryCriteria>; @Output() traitWidgetInitialized = new EventEmitter(); - activeTab = 'Germplasm'; + + // Default active tab + activeTab: Tabs = Tabs.GERMPLASM; + + // to give access in HTML template + tabs = Tabs; + + getNavClass(tab: Tabs) { + return this.activeTab === tab ? 'active' : ''; + } + + getTabClass(tab: Tabs) { + return this.activeTab === tab ? 'visible' : 'd-none'; + } } diff --git a/frontend/src/app/site-card/site-card.component.ts b/frontend/src/app/site-card/site-card.component.ts index 0705744e73ececc2b0b0bef39dad3febdcdfc859..67888dcf1284bad1c061ddd166bd168e73e4cdcf 100644 --- a/frontend/src/app/site-card/site-card.component.ts +++ b/frontend/src/app/site-card/site-card.component.ts @@ -2,7 +2,7 @@ import { Component, OnInit } from '@angular/core'; import { BrapiService } from '../brapi.service'; import { ActivatedRoute } from '@angular/router'; import { BrapiLocation } from '../models/brapi.model'; -import { KeyValueObject } from '../utils'; +import { KeyValueObject, toKeyValueObjects } from '../utils'; import { DataDiscoverySource } from '../models/data-discovery.model'; import { GnpisService } from '../gnpis.service'; @@ -30,7 +30,7 @@ export class SiteCardComponent implements OnInit { this.location = response.result; this.additionalInfos = []; if (this.location.additionalInfo) { - this.manageAdditionalInfo(KeyValueObject.fromObject(this.location.additionalInfo).sort()); + this.manageAdditionalInfo(toKeyValueObjects(this.location.additionalInfo).sort()); } const sourceURI = location['schema:includedInDataCatalog']; // TODO Remove the condition when the field includedInDataCatalog will be added to URGI study. diff --git a/frontend/src/app/study-card/study-card.component.ts b/frontend/src/app/study-card/study-card.component.ts index 27188c0189f62ab41e9f2020ab3c8603377c0f9b..8b85644152ceb9519f958973250b2bfaa322ac7f 100644 --- a/frontend/src/app/study-card/study-card.component.ts +++ b/frontend/src/app/study-card/study-card.component.ts @@ -5,7 +5,7 @@ import { BrapiGermplasm, BrapiObservationVariable, BrapiStudy, BrapiTrial } from import { GnpisService } from '../gnpis.service'; import { DataDiscoverySource } from '../models/data-discovery.model'; -import { KeyValueObject } from '../utils'; +import { KeyValueObject, toKeyValueObjects } from '../utils'; @Component({ selector: 'gpds-study-card', @@ -43,7 +43,7 @@ export class StudyCardComponent implements OnInit { this.additionalInfos = []; if (this.study.additionalInfo) { - this.additionalInfos = KeyValueObject.fromObject(this.study.additionalInfo).sort(); + this.additionalInfos = toKeyValueObjects(this.study.additionalInfo).sort(); } // Get study trials diff --git a/frontend/src/app/utils.spec.ts b/frontend/src/app/utils.spec.ts index 104068ba0aad25cb4d38fee6382513a84034823f..c1d7e6e13d87d6bde404cc55e762422d0b7a51d2 100644 --- a/frontend/src/app/utils.spec.ts +++ b/frontend/src/app/utils.spec.ts @@ -1,10 +1,10 @@ -import { KeyValueObject } from './utils'; +import { KeyValueObject, toKeyValueObjects } from './utils'; describe('KeyValueObject', () => { it('should convert JS object to array of KeyValueObject', () => { - const actual = KeyValueObject.fromObject({ + const actual = toKeyValueObjects({ 'a': '1', 'b': '2', 'c': null, @@ -13,10 +13,10 @@ describe('KeyValueObject', () => { 'f': '3' }); - const expected = [ - new KeyValueObject('a', '1'), - new KeyValueObject('b', '2'), - new KeyValueObject('f', '3'), + const expected: KeyValueObject[] = [ + { key: 'a', value: '1' }, + { key: 'b', value: '2' }, + { key: 'f', value: '3' }, ]; expect(actual).toEqual(expected); diff --git a/frontend/src/app/utils.ts b/frontend/src/app/utils.ts index 939ddd372352b866e7bdfd07d25355d67f7c983a..85f6ea9fcfbae1c795578d018f2fe57ee09dfef2 100644 --- a/frontend/src/app/utils.ts +++ b/frontend/src/app/utils.ts @@ -8,19 +8,18 @@ export function asArray<T>(obj: T | T[]): T[] { return [obj]; } -export class KeyValueObject { - public key: string; - public value: string; - - constructor(key: string, value: string) { - this.key = key; - this.value = value; - } +export interface KeyValueObject { + key: string; + value: string; +} - static fromObject(o: { [key: string]: string }): KeyValueObject[] { - return Object.entries(o) +/** + * Transform an object with string keys and values to a list of `KeyValueObject`. + * Also makes sure the keys and values are truthy. + */ +export function toKeyValueObjects(object: Record<string, string>): KeyValueObject[] { + return Object.entries(object) .filter(([key, value]) => !!key && !!value) - .map(([key, value]) => new KeyValueObject(key, value)); - } + .map(([key, value]) => ({ key, value })); }