Commit a50d8d55 authored by Exbrayat Cédric's avatar Exbrayat Cédric
Browse files

feat(search): ui beginning

parent 1a2a88cc
......@@ -23,7 +23,7 @@
"src/assets"
],
"styles": [
"src/styles.css"
"src/styles.scss"
],
"scripts": []
},
......@@ -73,7 +73,7 @@
"tsConfig": "src/tsconfig.spec.json",
"karmaConfig": "src/karma.conf.js",
"styles": [
"src/styles.css"
"src/styles.scss"
],
"scripts": [],
"assets": [
......@@ -124,5 +124,10 @@
}
}
},
"schematics": {
"@schematics/angular:component": {
"styleext": "scss"
}
},
"defaultProject": "rare-frontend"
}
......@@ -20,15 +20,15 @@
"@angular/platform-browser": "6.0.9",
"@angular/platform-browser-dynamic": "6.0.9",
"@angular/router": "6.0.9",
"bootstrap": "4.1.2",
"core-js": "2.5.7",
"rxjs": "6.2.2",
"zone.js": "0.8.26"
},
"devDependencies": {
"@angular/compiler-cli": "6.0.9",
"@angular-devkit/build-angular": "0.6.8",
"typescript": "2.7.2",
"@angular/cli": "6.0.8",
"@angular/compiler-cli": "6.0.9",
"@angular/language-service": "6.0.9",
"@types/jasmine": "2.8.8",
"@types/jasminewd2": "2.0.3",
......@@ -41,8 +41,10 @@
"karma-coverage-istanbul-reporter": "2.0.1",
"karma-jasmine": "1.1.2",
"karma-jasmine-html-reporter": "1.2.0",
"ngx-speculoos": "0.2.3",
"protractor": "5.4.0",
"ts-node": "7.0.0",
"tslint": "5.11.0"
"tslint": "5.11.0",
"typescript": "2.7.2"
}
}
<!--The content below is only a placeholder and can be replaced.-->
<div style="text-align:center">
<h1>
Welcome to {{ title }}!
</h1>
<img width="300" alt="Angular Logo" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTAgMjUwIj4KICAgIDxwYXRoIGZpbGw9IiNERDAwMzEiIGQ9Ik0xMjUgMzBMMzEuOSA2My4ybDE0LjIgMTIzLjFMMTI1IDIzMGw3OC45LTQzLjcgMTQuMi0xMjMuMXoiIC8+CiAgICA8cGF0aCBmaWxsPSIjQzMwMDJGIiBkPSJNMTI1IDMwdjIyLjItLjFWMjMwbDc4LjktNDMuNyAxNC4yLTEyMy4xTDEyNSAzMHoiIC8+CiAgICA8cGF0aCAgZmlsbD0iI0ZGRkZGRiIgZD0iTTEyNSA1Mi4xTDY2LjggMTgyLjZoMjEuN2wxMS43LTI5LjJoNDkuNGwxMS43IDI5LjJIMTgzTDEyNSA1Mi4xem0xNyA4My4zaC0zNGwxNy00MC45IDE3IDQwLjl6IiAvPgogIDwvc3ZnPg==">
<div class="container">
<router-outlet></router-outlet>
</div>
<h2>Here are some links to help you start: </h2>
<ul>
<li>
<h2><a target="_blank" rel="noopener" href="https://angular.io/tutorial">Tour of Heroes</a></h2>
</li>
<li>
<h2><a target="_blank" rel="noopener" href="https://github.com/angular/angular-cli/wiki">CLI Documentation</a></h2>
</li>
<li>
<h2><a target="_blank" rel="noopener" href="https://blog.angular.io/">Angular blog</a></h2>
</li>
</ul>
import { TestBed, async } from '@angular/core/testing';
import { TestBed } from '@angular/core/testing';
import { AppComponent } from './app.component';
import { RouterTestingModule } from '@angular/router/testing';
describe('AppComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent
],
}).compileComponents();
beforeEach(() => TestBed.configureTestingModule({
imports: [RouterTestingModule],
declarations: [AppComponent]
}));
it('should create the app', async(() => {
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
}));
it(`should have as title 'rare'`, async(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app.title).toEqual('rare');
}));
it('should render title in a h1 tag', async(() => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('h1').textContent).toContain('Welcome to rare!');
}));
});
});
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { HttpClientModule } from '@angular/common/http';
import { ReactiveFormsModule } from '@angular/forms';
import { routes } from './app.routes';
import { AppComponent } from './app.component';
import { HomeComponent } from './home/home.component';
import { SearchComponent } from './search/search.component';
import { GeneticResourcesComponent } from './genetic-resources/genetic-resources.component';
import { GeneticResourceComponent } from './genetic-resource/genetic-resource.component';
@NgModule({
declarations: [
AppComponent
AppComponent,
HomeComponent,
SearchComponent,
GeneticResourcesComponent,
GeneticResourceComponent
],
imports: [
BrowserModule
BrowserModule,
RouterModule.forRoot(routes),
ReactiveFormsModule,
HttpClientModule
],
providers: [],
bootstrap: [AppComponent]
......
import { Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { SearchComponent } from './search/search.component';
export const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'search', component: SearchComponent }
];
<div>
<div class="row">
<div class="col title">
<a class="main-link"
[href]="geneticResource.dataURL ? geneticResource.dataURL : geneticResource.portalURL">
<strong class="name">{{ geneticResource.name }}</strong>
</a> - {{ geneticResource.pillarName }}
</div>
</div>
<div class="row">
<div class="col">
<a class="datasource-link" [href]="geneticResource.portalURL">{{ geneticResource.databaseSource }}</a>
</div>
<div class="col type">
{{ geneticResource.materialType?.join(', ') }}
</div>
</div>
<div class="row">
<div class="col description" *ngIf="descriptionCollapsed; else descriptionExpanded">
{{ geneticResource.description | slice:0:255 }}<button class="btn btn-link" (click)="toggleDescription()" *ngIf="geneticResource.description?.length > 255">... (Voir tout)</button>
</div>
<ng-template #descriptionExpanded>
<div class="col full-description">
{{ geneticResource.description }}<button class="btn btn-link" (click)="toggleDescription()">Réduire</button>
</div>
</ng-template>
</div>
</div>
import { TestBed } from '@angular/core/testing';
import { ComponentTester, speculoosMatchers } from 'ngx-speculoos';
import { GeneticResourceComponent } from './genetic-resource.component';
import { toGeneticResource } from '../models/test-model-generators';
describe('GeneticResourceComponent', () => {
class GeneticResourceComponentTester extends ComponentTester<GeneticResourceComponent> {
constructor() {
super(GeneticResourceComponent);
}
get title() {
return this.element('.title');
}
get link() {
return this.element('.main-link');
}
get datasourceLink() {
return this.element('.datasource-link');
}
get type() {
return this.element('.type');
}
get description() {
return this.element('.description');
}
get fullDescriptionLink() {
return this.element('.description button');
}
get fullDescription() {
return this.element('.full-description');
}
get shortDescriptionLink() {
return this.element('.full-description button');
}
}
beforeEach(() => TestBed.configureTestingModule({
declarations: [GeneticResourceComponent]
}));
beforeEach(() => jasmine.addMatchers(speculoosMatchers));
it('should display a resource', () => {
const tester = new GeneticResourceComponentTester();
const component = tester.componentInstance;
// given a resource
const resource = toGeneticResource('Bacteria');
component.geneticResource = resource;
tester.detectChanges();
// then we should display it
expect(tester.title).toContainText(`${resource.name} - ${resource.pillarName}`);
expect(tester.link).toContainText(resource.name);
expect(tester.link.attr('href')).toBe(resource.dataURL);
expect(tester.datasourceLink).toContainText(resource.databaseSource);
expect(tester.datasourceLink.attr('href')).toBe(resource.portalURL);
expect(tester.type).toContainText(resource.materialType[0]);
expect(tester.description).toContainText(resource.description);
expect(tester.fullDescriptionLink).toBeNull();
expect(tester.fullDescription).toBeNull();
expect(tester.shortDescriptionLink).toBeNull();
});
it('should have a link to portal if data url is null or empty', () => {
const tester = new GeneticResourceComponentTester();
const component = tester.componentInstance;
// given a resource with no data url
const resource = toGeneticResource('Bacteria');
resource.dataURL = null;
component.geneticResource = resource;
tester.detectChanges();
// then we should link to portal url
expect(tester.link.attr('href')).toBe(resource.portalURL);
});
it('should display several types properly', () => {
const tester = new GeneticResourceComponentTester();
const component = tester.componentInstance;
// given a resource with several types
const resource = toGeneticResource('Bacteria');
resource.materialType = ['type1', 'type2'];
component.geneticResource = resource;
tester.detectChanges();
// then we should them
expect(tester.type).toContainText('type1, type2');
});
it('should have truncate the long description and allow to display it fully', () => {
const tester = new GeneticResourceComponentTester();
const component = tester.componentInstance;
// given a resource with a long description
const resource = toGeneticResource('Bacteria');
resource.description = Array(500).fill('a').join('');
component.geneticResource = resource;
tester.detectChanges();
// then we should truncate it
expect(tester.fullDescriptionLink).not.toBeNull();
const linkContent = '... (Voir tout)';
expect(tester.fullDescriptionLink).toContainText(linkContent);
expect(tester.description.textContent.length).toBe(256 + linkContent.length);
// when we click on the link
tester.fullDescriptionLink.dispatchEventOfType('click');
// then we should display the full description
expect(tester.fullDescription).not.toBeNull();
expect(tester.fullDescription).toContainText(resource.description);
expect(tester.shortDescriptionLink).not.toBeNull();
expect(tester.shortDescriptionLink).toContainText('Réduire');
expect(tester.description).toBeNull();
expect(tester.fullDescriptionLink).toBeNull();
});
});
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { GeneticResourceModel } from '../models/genetic-resource.model';
@Component({
selector: 'rare-genetic-resource',
templateUrl: './genetic-resource.component.html',
styleUrls: ['./genetic-resource.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class GeneticResourceComponent {
descriptionCollapsed = true;
@Input() geneticResource: GeneticResourceModel;
toggleDescription() {
this.descriptionCollapsed = !this.descriptionCollapsed;
}
}
<div class="mt-5">
<!-- if there are results to display -->
<div *ngIf="geneticResources?.content?.length; else noResults">
<div class="mt-4" *ngFor="let geneticResource of geneticResources.content">
<rare-genetic-resource [geneticResource]="geneticResource"></rare-genetic-resource>
</div>
</div>
<!-- else we display a simple message -->
<ng-template #noResults>
<div id="no-results">Pas de résultat.</div>
</ng-template>
</div>
import { TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { ReactiveFormsModule } from '@angular/forms';
import { ComponentTester } from 'ngx-speculoos';
import { GeneticResourcesComponent } from './genetic-resources.component';
import { GeneticResourceComponent } from '../genetic-resource/genetic-resource.component';
import { toGeneticResource, toSinglePage } from '../models/test-model-generators';
describe('GeneticResourcesComponent', () => {
class GeneticResourcesComponentTester extends ComponentTester<GeneticResourcesComponent> {
constructor() {
super(GeneticResourcesComponent);
}
get results() {
return this.debugElement.queryAll(By.directive(GeneticResourceComponent));
}
get noResults() {
return this.nativeElement.querySelector('#no-results');
}
}
beforeEach(() => TestBed.configureTestingModule({
imports: [ReactiveFormsModule],
declarations: [GeneticResourcesComponent, GeneticResourceComponent]
}));
it('should display no results if null', () => {
const tester = new GeneticResourcesComponentTester();
// given no results
tester.detectChanges();
// then it should display a message
expect(tester.results.length).toBe(0);
expect(tester.noResults).not.toBeNull();
});
it('should display no results if empty', () => {
const tester = new GeneticResourcesComponentTester();
const component = tester.componentInstance;
// given no results
component.geneticResources = toSinglePage([]);
tester.detectChanges();
// then it should display a message
expect(tester.results.length).toBe(0);
expect(tester.noResults).not.toBeNull();
});
it('should display results if there are some', () => {
const tester = new GeneticResourcesComponentTester();
const component = tester.componentInstance;
// given no results
const bacteria1 = toGeneticResource('Bacteria1');
const bacteria2 = toGeneticResource('Bacteria2');
component.geneticResources = toSinglePage([bacteria1, bacteria2]);
tester.detectChanges();
// then it should display each result
expect(tester.noResults).toBeNull();
expect(tester.results.length).toBe(2);
const result1 = tester.results[0].componentInstance as GeneticResourceComponent;
expect(result1.geneticResource).toBe(bacteria1);
const result2 = tester.results[1].componentInstance as GeneticResourceComponent;
expect(result2.geneticResource).toBe(bacteria2);
});
});
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { Page } from '../models/page';
import { GeneticResourceModel } from '../models/genetic-resource.model';
@Component({
selector: 'rare-genetic-resources',
templateUrl: './genetic-resources.component.html',
styleUrls: ['./genetic-resources.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class GeneticResourcesComponent {
@Input() geneticResources: Page<GeneticResourceModel>;
}
<div class="mt-5">
<form class="input-group"[formGroup]="searchForm" (ngSubmit)="search()">
<input class="form-control form-control-lg" type="text"
placeholder="Exemples: canis lupus, levure fromage"
formControlName="search">
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="submit">Recherche</button>
</div>
</form>
</div>
import { TestBed } from '@angular/core/testing';
import { ReactiveFormsModule } from '@angular/forms';
import { RouterTestingModule } from '@angular/router/testing';
import { Router } from '@angular/router';
import { ComponentTester, speculoosMatchers } from 'ngx-speculoos';
import { HomeComponent } from './home.component';
class HomeComponentTester extends ComponentTester<HomeComponent> {
constructor() {
super(HomeComponent);
}
get searchBar() {
return this.input('input');
}
get searchButton() {
return this.button('button');
}
}
describe('HomeComponent', () => {
beforeEach(() => TestBed.configureTestingModule({
imports: [ReactiveFormsModule, RouterTestingModule],
declarations: [HomeComponent]
}));
beforeEach(() => jasmine.addMatchers(speculoosMatchers));
it('should navigate to search when a query is entered', () => {
// given a component
const router = TestBed.get(Router) as Router;
spyOn(router, 'navigate');
const component = new HomeComponent(router);
// with a query
const query = 'Bacteria';
component.searchForm.get('search').setValue(query);
// when searching
component.search();
// then it should redirect to the search with correct parameters
expect(router.navigate).toHaveBeenCalledWith(['/search'], { queryParams: { query } });
});
it('should display a search bar and trigger a search', () => {
// given a component
const tester = new HomeComponentTester();
const component = tester.componentInstance;
spyOn(component, 'search');
// then it should display the search bar containing that query
tester.detectChanges();
expect(tester.searchBar).toHaveValue('');
// with a query
const query = 'Bacteria';
tester.searchBar.fillWith(query);
// trigger search
tester.searchButton.click();
expect(component.search).toHaveBeenCalled();
expect(component.searchForm.get('search').value).toBe(query);
});
});
import { Component } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { Router } from '@angular/router';
@Component({
selector: 'rare-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss']
})
export class HomeComponent {
searchForm: FormGroup;
constructor(private router: Router) {
this.searchForm = new FormGroup({
search: new FormControl()
});
}
search() {
this.router.navigate(['/search'], {
queryParams: {
query: this.searchForm.get('search').value
}
});
}
}
export interface GeneticResourceModel {
identifier: string;
name: string;
description: string;
pillarName: string;
databaseSource: string;
portalURL: string;
dataURL: string;
domain: string;
taxon: Array<string>;
family: Array<string>;
genus: Array<string>;
species: Array<string>;
materialType: Array<string>;
biotopeType: Array<string>;
countryOfOrigin: string;
originLatitude: number;
originLongitude: number;