state is stored, and duplicated, at various places in our app
src/app/recipe/recipe-data.service.ts
getRecipes$(name?: string, chef?: string, ingredient?: string) {
let params = new HttpParams();
params = name ? params.append('name', name) : params;
params = chef ? params.append('chef', chef) : params;
params = ingredient ? params.append('ingredientName', ingredient) : params;
return this.http.get(`${environment.apiUrl}/recipes/`, { params }).pipe(
catchError(this.handleError),
map((list: any[]): Recipe[] => list.map(Recipe.fromJSON))
);
}
src/app/recipe/recipe-list/recipe-list.component.ts
export class RecipeListComponent implements OnInit {
public filterRecipeName: string = '';
public recipes: Recipe[];
constructor(
private _recipeDataService: RecipeDataService,
private _router: Router,
private _route: ActivatedRoute
... ) {}
ngOnInit() {
this.filterRecipe$
.pipe(
distinctUntilChanged(),
debounceTime(250)
...)
.subscribe(val => {
const params = val ? { queryParams: { filter: val } } : undefined;
this._router.navigate(['/recipe/list'], params);
...});
this._route.queryParams.subscribe(params => {
this._recipeDataService
.getRecipes$(params['filter'])
.pipe(
catchError((err) => {
this.errorMessage = err;
return EMPTY;
})
)
.subscribe(val => (this.recipes = val));
if (params['filter']) {
this.filterRecipeName = params['filter'];
}
});
}
}
src/app/recipe/recipe-list/recipe-list.component.html
<mat-card>
<mat-form-field>
<input
matInput
(keyup)="filterRecipe$.next($event.target.value)"
placeholder="filter"
type="text"
data-cy="filterInput"
[value]="filterRecipeName"
/>
</mat-form-field>
</mat-card>
<div *ngIf="recipes$ | async as recipes; else loadingOrError">
<div>
<div
class="recipe"
*ngFor="let recipe of recipes | recipeFilter: filterRecipeName"
>
<app-recipe [recipe]="recipe" data-cy="recipeCard"></app-recipe>
</div>
</div>
</div>
e13393e
cypress/support/commands.js
Cypress.Commands.add('login', () => {
const email = 'recipemaster@hogent.be';
cy.request({
method: 'POST',
url: '/api/account',
body: { email, password: 'P@ssword1111' },
}).then((res) => localStorage.setItem('currentUser', res.body));
});
cypress/integration/recipelist.spec.js
describe('Recipe List tests', function () {
beforeEach(function () {
cy.login();
});
it('delayed response brings state out of sync', () => {
cy.server();
cy.route({
method: 'GET',
url: '/api/recipes',
status: 200,
response: 'fixture:recipes.json'
});
cy.route({
delay: 2000,
method: 'GET',
url: '/api/recipes/?name=sp',
status: 200,
response: 'fixture:spaghetti.json'
}).as('getSPrecipes');
cy.route({
method: 'GET',
url: '/api/recipes/?name=la',
status: 200,
response: 'fixture:lasagna.json'
}).as('getLArecipes');
// ... all the stub routes
cy.visit('/');
cy.get('[data-cy=filterInput]').type('sp');
cy.wait(300);
cy.get('[data-cy=filterInput]').type('{backspace}{backspace}la');
cy.wait(['@getSPrecipes', '@getLArecipes']);
cy.get('[data-cy=recipeCard]').should('have.length', 1);
cy.get('[data-cy=recipe-title]').should('contain', 'Lasagna');
});
});
eba887f
const greetPeople$ = of('Destiny', 'Melody', 'Candy');
greetPeople$
.pipe(map(name => `hi ${name}, nice to meet you!`))
.subscribe(result => console.log(`${result}`));
const http = {
talkToMe$(name) {
return of(`Hi ${name}, nice to meet you!`,
`Is ${name} your real name or your stripper name?`);
}
};
greetPeople$
.pipe(
mapmergeMap(name => http.talkToMe$(name)),
mergeAll()
)
.subscribe(result => console.log(`${result}`)resultObservable => resultObservable.subscribe(result => console.log(`${result}`)));
// [Object object]
// [Object object]
// [Object object]
// Hi Destiny, nice to meet you!
// Is Destiny your real name or your stripper name?
// Hi Melody, nice to meet you!
// Is Melody your real name or your stripper name?
// Hi Candy, nice to meet you!
// Is Candy your real name or your stripper name?
http.talkToMe$('Shaniah').subscribe(console.log);
// Hi Shaniah, nice to meet you!
// Is Shaniah your real name or your stripper name?
// hi Destiny, nice to meet you!
// hi Melody, nice to meet you!
// hi Candy, nice to meet you!
src/app/recipe/recipe-list/recipe-list.component.ts
this._route.queryParams.subscribe.pipe(
switchMap map(params => {
if (params['filter']) {
this.filterRecipeName = params['filter'];
}
return this._recipeDataService.getRecipes$(params['filter']);
})
)
.pipe(
catchError((err) => {
this.errorMessage = err;
return EMPTY;
})
)
.subscribe(val => {
this.recipes = val;
});
if (params['filter']) {
this.filterRecipeName = params['filter'];
}
});
4ab3cf9
src/app/recipe/recipe-list.component.ts
constructor( ... ) {
this._fetchRecipes$ = this._route.queryParams
.pipe(
switchMap((newParams) => {
if (newParams['filter']) {
this.filterRecipeName = newParams['filter'];
}
return this._recipeDataService.getRecipes$(newParams['filter']);
})
)
.pipe(
catchError((err) => {
this.errorMessage = err;
return EMPTY;
})
);
.subscribe((val) => {
this.recipes = val;
});
}
get recipes$(): Observable<Recipe[]> {
return this._fetchRecipes$;
}
src/app/recipe/recipe-data.service.ts
private _reloadRecipes$ = new BehaviorSubject<boolean>(true);
deleteRecipe(recipe: Recipe) {
return this.http
.delete(`${environment.apiUrl}/recipes/${recipe.id}`)
.pipe(tap(console.log), catchError(this.handleError))
.subscribe(() => {
this._reloadRecipes$.next(true);
});
}
getRecipes$(name?: string, chef?: string, ingredient?: string) {
return this._reloadRecipes$.pipe(
switchMap(() => this.fetchRecipes$(name, chef, ingredient))
);
}
fetchRecipes$(name?: string, chef?: string, ingredient?: string) {
let params = new HttpParams();
params = name ? params.append('name', name) : params;
params = chef ? params.append('chef', chef) : params;
params = ingredient ? params.append('ingredientName', ingredient) : params;
return this.http.get(`${environment.apiUrl}/recipes/`, { params }).pipe(
catchError(this.handleError),
map((list: any[]): Recipe[] => list.map(Recipe.fromJSON))
);
}
4095479