Skip to content

Commit 00bd25b

Browse files
committed
added async demo entry
1 parent a69a2bd commit 00bd25b

11 files changed

Lines changed: 407 additions & 0 deletions
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
## Component with async service
2+
3+
```bash
4+
npx ng g s twain
5+
```
6+
7+
Create `quote.ts`
8+
9+
```ts
10+
export interface Quote {
11+
id: number;
12+
quote: string;
13+
}
14+
```
15+
16+
Update `twain.service.ts`
17+
18+
```ts
19+
import { HttpClient } from "@angular/common/http";
20+
import { Injectable } from "@angular/core";
21+
import { Observable, catchError, throwError, map } from "rxjs";
22+
import { Quote } from "./quote";
23+
24+
@Injectable({
25+
providedIn: "root",
26+
})
27+
export class TwainService {
28+
// https://dummyjson.com/docs/quotes#quotes-single
29+
private url = "https://dummyjson.com/quotes/random";
30+
31+
constructor(private http: HttpClient) {}
32+
33+
getQuote(): Observable<string> {
34+
return this.http.get<Quote>(this.url).pipe(
35+
map((q: Quote) => q.quote),
36+
catchError((err) => throwError(() => "Can not get quote"))
37+
);
38+
}
39+
}
40+
```
41+
42+
```bash
43+
npx ng g c twain
44+
```
45+
46+
Update `twain.component.ts`
47+
48+
```ts
49+
import { AsyncPipe } from "@angular/common";
50+
import { Component, OnInit, signal } from "@angular/core";
51+
import { catchError, Observable, of, startWith } from "rxjs";
52+
import { TwainService } from "./twain.service";
53+
54+
@Component({
55+
selector: "app-twain",
56+
imports: [AsyncPipe],
57+
template: `
58+
<p class="twain">
59+
<i>{{ quote | async }}</i>
60+
</p>
61+
<button type="button" (click)="getQuote()">Next quote</button>
62+
@if (errorMessage()) {
63+
<p class="error">{{ errorMessage() }}</p>
64+
}
65+
`,
66+
styles: [".twain { font-style: italic; } .error { color: red; }"],
67+
})
68+
export class TwainComponent implements OnInit {
69+
errorMessage = signal("");
70+
quote?: Observable<string>;
71+
72+
constructor(private twainService: TwainService) {}
73+
74+
ngOnInit(): void {
75+
this.getQuote();
76+
}
77+
78+
getQuote() {
79+
this.errorMessage.set("");
80+
this.quote = this.twainService.getQuote().pipe(
81+
startWith("..."),
82+
catchError((err: string) => {
83+
this.errorMessage.set(err);
84+
return of("...");
85+
})
86+
);
87+
}
88+
}
89+
```
90+
91+
### Testing with a spy
92+
93+
When testing a component, only the service's public API should matter. In general, tests themselves should not make calls to remote servers.
94+
95+
Update `twain.component.spec.ts`
96+
97+
- [spyOn](https://jasmine.github.io/api/edge/global.html#spyOn)
98+
99+
```ts
100+
import { ComponentFixture, TestBed } from "@angular/core/testing";
101+
102+
import { TwainComponent } from "./twain.component";
103+
import { TwainService } from "./twain.service";
104+
import { of, throwError } from "rxjs";
105+
import { provideHttpClient } from "@angular/common/http";
106+
107+
describe("TwainComponent", () => {
108+
let component: TwainComponent;
109+
let fixture: ComponentFixture<TwainComponent>;
110+
let testQuote: string;
111+
let getQuoteSpy: jasmine.Spy;
112+
113+
beforeEach(() => {
114+
TestBed.configureTestingModule({
115+
imports: [TwainComponent],
116+
providers: [TwainService, provideHttpClient()],
117+
});
118+
testQuote = "Test Quote";
119+
120+
// Create a fake TwainService object with `getQuote()` spy
121+
const twainService = TestBed.inject(TwainService);
122+
getQuoteSpy = spyOn(twainService, "getQuote").and.returnValue(
123+
of(testQuote)
124+
);
125+
126+
fixture = TestBed.createComponent(TwainComponent);
127+
component = fixture.componentInstance;
128+
});
129+
130+
it("should display error when TwainService fails", () => {
131+
getQuoteSpy.and.returnValue(throwError(() => "Test fails"));
132+
fixture.detectChanges(); // ngOnInit
133+
const errorElement = fixture.nativeElement.querySelector(".error");
134+
expect(errorElement.textContent)
135+
.withContext("should display error")
136+
.toContain("Test fails");
137+
});
138+
139+
it("should show quote after getQuote", () => {
140+
fixture.detectChanges(); // ngOnInit()
141+
const element = fixture.nativeElement.querySelector(".twain");
142+
expect(element.textContent)
143+
.withContext("should show quote")
144+
.toBe(testQuote);
145+
});
146+
});
147+
```
148+
149+
```bash
150+
npx ng test --include app/twain/twain.component.spec.ts
151+
```
152+
153+
The spy is designed such that any call to `getQuote` receives an observable with a test quote. Unlike the real `getQuote()` method, this spy bypasses the server and returns a synchronous observable whose value as available immediately.
154+
155+
> **NOTE**: It is best to limit the usage of spies to only what is necessary for the test. Creating mocks or spies for more than what's necessary can be brittle. As the component and injectable evolves, the unrelated tests can fail because they no longer mock enough behaviors that would otherwise not affect the test.
156+
157+
Notice that we are not able to test the transition between '...' to the 'test quote' text. Why is that? Basically our real service behaves as async and the Spy is behaving as sync, no time for transition here
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
# Component with inputs and outputs
2+
3+
A component with inputs and outputs typically appears inside the view template of a host component. The host uses a property binding to set the input property and an event binding to listen to events raised by the output property.
4+
5+
The testing goal is to verify that such bindings work as expected. The tests should set input values and listen for output events.
6+
7+
```bash
8+
npx ng g c dashboard
9+
```
10+
11+
```bash
12+
npx ng g c dashboard/dashboard-quote --flat
13+
```
14+
15+
Update `dashnoard-quote.component.ts`
16+
17+
```ts
18+
import { Component, input, output } from "@angular/core";
19+
import { Quote } from "../twain/quote";
20+
21+
@Component({
22+
selector: "app-dashboard-quote",
23+
imports: [],
24+
template: `
25+
<button type="button" (click)="click()" class="quote">
26+
{{ quote().quote }}
27+
</button>
28+
`,
29+
styleUrl: "./dashboard-quote.component.css",
30+
})
31+
export class DashboardQuoteComponent {
32+
quote = input.required<Quote>();
33+
selected = output<Quote>();
34+
35+
click() {
36+
this.selected.emit(this.quote());
37+
}
38+
}
39+
```
40+
41+
Update `dashboard.component.html`
42+
43+
```html
44+
<h2>{{ title }}</h2>
45+
46+
<div class="grid grid-pad">
47+
@for (quote of quotes; track quote) {
48+
<app-dashboard-quote
49+
class="col-1-4"
50+
[quote]="quote"
51+
(selected)="gotoDetail($any($event))"
52+
></app-dashboard-quote>
53+
}
54+
</div>
55+
```
56+
57+
Update `dashboard.component.ts`
58+
59+
```ts
60+
import { Component, OnInit } from "@angular/core";
61+
import { Quote } from "../twain/quote";
62+
import { TwainService } from "../twain/twain.service";
63+
import { DashboardQuoteComponent } from "./dashboard-quote.component";
64+
65+
@Component({
66+
selector: "app-dashboard",
67+
imports: [DashboardQuoteComponent],
68+
templateUrl: "./dashboard.component.html",
69+
styleUrl: "./dashboard.component.css",
70+
})
71+
export class DashboardComponent implements OnInit {
72+
quotes: Quote[] = [];
73+
74+
constructor(private quoteService: TwainService) {}
75+
76+
ngOnInit(): void {
77+
this.quoteService
78+
.getQuotes()
79+
.subscribe((quotes) => (this.quotes = quotes.slice(1, 5)));
80+
}
81+
82+
gotoDetail(quote: Quote) {
83+
// TODO: Navigate to detail page
84+
console.log(quote);
85+
}
86+
87+
get title() {
88+
const cnt = this.quotes.length;
89+
return cnt === 0 ? "No quotes" : `Top ${cnt} quotes`;
90+
}
91+
}
92+
```
93+
94+
### Test `DashboardQuoteComponent` standalone
95+
96+
Update `dashboard-quote.component.spec.ts`
97+
98+
```ts
99+
import { ComponentFixture, TestBed } from "@angular/core/testing";
100+
101+
import { DashboardQuoteComponent } from "./dashboard-quote.component";
102+
import { Quote } from "../twain/quote";
103+
104+
describe("DashboardQuoteComponent", () => {
105+
let component: DashboardQuoteComponent;
106+
let fixture: ComponentFixture<DashboardQuoteComponent>;
107+
108+
beforeEach(() => {
109+
TestBed.configureTestingModule({
110+
imports: [DashboardQuoteComponent],
111+
});
112+
113+
fixture = TestBed.createComponent(DashboardQuoteComponent);
114+
component = fixture.componentInstance;
115+
});
116+
117+
it("should display quote", async () => {
118+
const quoteEl = fixture.nativeElement.querySelector(".quote");
119+
const expectedQuote: Quote = {
120+
id: 1,
121+
quote: "All people live expecting die",
122+
};
123+
fixture.componentRef.setInput("quote", expectedQuote);
124+
fixture.detectChanges();
125+
await fixture.whenStable();
126+
expect(quoteEl.textContent).toContain(expectedQuote.quote);
127+
});
128+
});
129+
```
130+
131+
### Clicking
132+
133+
Update `dashboard-quote.component.spec.ts`
134+
135+
```ts
136+
it("should raise selected event when clicked (triggerEventHandler)", async () => {
137+
const quoteDe = fixture.debugElement.query(By.css(".quote"));
138+
const expectedQuote: Quote = {
139+
id: 1,
140+
quote: "All people live expecting die",
141+
};
142+
fixture.componentRef.setInput("quote", expectedQuote);
143+
fixture.detectChanges();
144+
await fixture.whenStable();
145+
146+
let selectedQuote: Quote | undefined;
147+
component.selected.subscribe((quote: Quote) => (selectedQuote = quote));
148+
quoteDe.triggerEventHandler("click");
149+
expect(selectedQuote).toBe(expectedQuote);
150+
});
151+
```
152+
153+
The component's selected property returns an `EventEmitter`, which looks like an RxJS synchronous `Observable` to consumers. The test subscribes to it _explicitly_ just as the host component does _implicitly_.
154+
155+
### `triggerEventHandler`
156+
157+
The Angular `DebugElement.triggerEventHandler` can raise _any data-bound event_ by its event name. The second parameter is the event object passed to the handler.
158+
159+
### Click the element
160+
161+
```ts
162+
it("should raise selected event when clicked (element.click)", async () => {
163+
const quoteEl = fixture.nativeElement.querySelector(".quote");
164+
const expectedQuote: Quote = {
165+
id: 1,
166+
quote: "All people live expecting die",
167+
};
168+
fixture.componentRef.setInput("quote", expectedQuote);
169+
fixture.detectChanges();
170+
await fixture.whenStable();
171+
172+
let selectedQuote: Quote | undefined;
173+
component.selected.subscribe((quote: Quote) => (selectedQuote = quote));
174+
175+
quoteEl.click();
176+
expect(selectedQuote).toBe(expectedQuote);
177+
});
178+
```
179+
180+
> Call native's element own `click()`
File renamed without changes.

05-testing/02-angular-2025/07-nested-components.md renamed to 05-testing/02-angular-2025/solution/application/src/app/dashboard/dashboard-quote.component.css

File renamed without changes.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<p>dashboard-quote works!</p>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
3+
import { DashboardQuoteComponent } from './dashboard-quote.component';
4+
5+
describe('DashboardQuoteComponent', () => {
6+
let component: DashboardQuoteComponent;
7+
let fixture: ComponentFixture<DashboardQuoteComponent>;
8+
9+
beforeEach(async () => {
10+
await TestBed.configureTestingModule({
11+
imports: [DashboardQuoteComponent]
12+
})
13+
.compileComponents();
14+
15+
fixture = TestBed.createComponent(DashboardQuoteComponent);
16+
component = fixture.componentInstance;
17+
fixture.detectChanges();
18+
});
19+
20+
it('should create', () => {
21+
expect(component).toBeTruthy();
22+
});
23+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { Component } from '@angular/core';
2+
3+
@Component({
4+
selector: 'app-dashboard-quote',
5+
imports: [],
6+
templateUrl: './dashboard-quote.component.html',
7+
styleUrl: './dashboard-quote.component.css'
8+
})
9+
export class DashboardQuoteComponent {
10+
11+
}

05-testing/02-angular-2025/solution/application/src/app/dashboard/dashboard.component.css

Whitespace-only changes.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<p>dashboard works!</p>

0 commit comments

Comments
 (0)