Skip to content

Commit 8746cee

Browse files
committed
tests for combatant and combatantdetail
1 parent de32785 commit 8746cee

5 files changed

Lines changed: 349 additions & 14 deletions

File tree

src/components/combatant.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
The Combatant component, responsible for rendering a single combatant.
33
Rendered by the Battle component.
44
5-
The combatant component has a form where the user can load data for a Github user
5+
The combatant component has a form where the user can load data for a Github user.
66
If a user is loaded it will display that data through a CombatantDetail component,
77
and output the startcount to the parent.
88
*/
@@ -18,13 +18,11 @@ type CombatantMode = 'empty' | 'loading' | 'error' | 'data';
1818
@Component({
1919
selector: 'combatant',
2020
template: `
21-
<input placeholder="Github user name" [(ngModel)]="field" (keyup.enter)="loadData()">
22-
<button (click)="loadData()" [disabled]="!canLoad">Load</button>
23-
<div *ngIf="mode === 'loading'">...loading...</div>
24-
<div *ngIf="mode === 'error'">Oh no, something wen't wrong :(</div>
25-
<div *ngIf="mode === 'data'">
26-
<combatantdetail [data]="data"></combatantdetail>
27-
</div>
21+
<input class="qa-github-input" placeholder="Github user name" [(ngModel)]="field" (keyup.enter)="loadData()">
22+
<button class="qa-load-button" (click)="loadData()" [disabled]="!canLoad">Load</button>
23+
<div *ngIf="mode === 'loading'" class="qa-load-indicator">...loading...</div>
24+
<div *ngIf="mode === 'error'" class="qa-error-indicator">Oh no, something wen't wrong :(</div>
25+
<combatantdetail *ngIf="mode === 'data'" [data]="data"></combatantdetail>
2826
`,
2927
styles: [`
3028
:host {

src/components/combatantdetail.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,15 @@ import { CombatantInfo } from '../types';
1212
@Component({
1313
selector: 'combatantdetail',
1414
template: `
15-
<p>
16-
<a href="http://github.com/{{data.id}}" target="_blank">{{data.id}}</a> has {{data.repos.stars}} stars across
15+
<p class="qa-basic-info">
16+
<a href="http://github.com/{{data.id}}" class="qa-github-link" target="_blank">{{data.id}}</a> has {{data.repos.stars}} stars across
1717
{{data.repos.repos}} repos.
18-
<span *ngIf="data.repos.mostStarred.name">
18+
<span *ngIf="data.repos.mostStarred.name" class="qa-most-starred">
1919
The most popular repo is <a href="http://github.com/{{data.id}}/{{data.repos.mostStarred.name}}" target="_blank">
2020
{{data.repos.mostStarred.name}}</a> with {{data.repos.mostStarred.stargazers_count}} stars.
2121
</span>
2222
</p>
23-
<div *ngIf="data.repos.repos">
23+
<div class="qa-language" *ngIf="data.repos.repos">
2424
<p>Here's a language breakdown:</p>
2525
<table>
2626
<thead><tr><th>Language</th><th>Count</th></tr></thead>
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
/******************************************
2+
Unit tests for the Combatant component. We need to test...
3+
4+
* template
5+
* stars output
6+
* combatantdetail child input
7+
* battleservice usage
8+
9+
/*******************************************/
10+
11+
// --------------- Child component stubs -----------------
12+
13+
import { Component, Input } from '@angular/core';
14+
15+
@Component({
16+
selector: 'combatantdetail',
17+
template: ''
18+
})
19+
class FakeCombatantDetail {
20+
@Input() data: CombatantInfo
21+
}
22+
23+
// --------------- Service mocks ---------------
24+
25+
import * as sinon from 'sinon';
26+
27+
const fakeBattleServiceObservable = {
28+
subscribe: sinon.stub()
29+
};
30+
31+
const fakeBattleService = {
32+
battleInfoForUser: sinon.stub().returns(fakeBattleServiceObservable)
33+
};
34+
35+
// --------------- Test config ---------------
36+
37+
import { FormsModule } from '@angular/forms';
38+
import { CommonModule } from '@angular/common';
39+
40+
import { CombatantComponent } from '../components/combatant';
41+
import { BattleService } from '../services/battleservice';
42+
43+
const testModuleConfig = {
44+
imports: [FormsModule, CommonModule],
45+
declarations: [CombatantComponent, FakeCombatantDetail],
46+
providers: [{provide: BattleService, useValue: fakeBattleService}]
47+
}
48+
49+
// --------------- Test suite ---------------
50+
51+
import { TestBed, getTestBed, ComponentFixture } from '@angular/core/testing';
52+
import { By } from '@angular/platform-browser';
53+
import { expect } from 'chai';
54+
import { DebugElement } from '@angular/core';
55+
import { CombatantInfo, CombatantRepoInfo } from '../types';
56+
57+
let fixture: ComponentFixture<CombatantComponent>
58+
let instance: CombatantComponent
59+
let debugElement: DebugElement
60+
let nativeElement: HTMLElement;
61+
let starsListener = sinon.stub();
62+
63+
describe('CombatantComponent', () => {
64+
before(() => TestBed.configureTestingModule(testModuleConfig));
65+
after(() => getTestBed().resetTestingModule());
66+
67+
beforeEach(() => {
68+
sinon.resetHistory();
69+
fixture = TestBed.createComponent(CombatantComponent);
70+
debugElement = fixture.debugElement;
71+
instance = debugElement.componentInstance;
72+
instance.stars.subscribe(starsListener); // To be able to test the stars output
73+
nativeElement = debugElement.nativeElement;
74+
fixture.detectChanges();
75+
});
76+
77+
it('should instantiate ok', () => {
78+
expect(instance).to.exist;
79+
});
80+
81+
it('should start with no indicators, no details and a disabled button', () => {
82+
expect(nativeElement.querySelector('.qa-load-indicator')).to.not.exist;
83+
expect(nativeElement.querySelector('.qa-error-indicator')).to.not.exist;
84+
expect(nativeElement.querySelector('combatantdetail')).to.not.exist;
85+
expect(nativeElement.querySelector('.qa-load-button[disabled]')).to.exist;
86+
});
87+
88+
it('should not call service if we click button when field is empty', () => {
89+
nativeElement.querySelector(".qa-load-button").dispatchEvent(new Event('click'));
90+
fixture.detectChanges();
91+
92+
expect(fakeBattleService.battleInfoForUser.called).to.be.false;
93+
});
94+
95+
it('should not emit anything to stars output', () => {
96+
expect(starsListener.called).to.be.false;
97+
});
98+
99+
describe('calling the service', () => {
100+
const fieldContent = Math.random.toString();
101+
102+
beforeEach(() => {
103+
const field: HTMLInputElement = debugElement.query(By.css('.qa-github-input')).nativeElement;
104+
field.value = fieldContent;
105+
field.dispatchEvent(new Event('input'));
106+
fixture.detectChanges();
107+
nativeElement.querySelector(".qa-load-button").dispatchEvent(new Event('click'));
108+
fixture.detectChanges();
109+
});
110+
111+
it('should call service with form content when button clicked', ()=> {
112+
expect(fakeBattleService.battleInfoForUser.called).to.be.true;
113+
expect(fakeBattleService.battleInfoForUser.firstCall.args[0]).to.equal(fieldContent);
114+
});
115+
116+
it('should subscribe to success and fail for the provided observable', () => {
117+
expect(fakeBattleServiceObservable.subscribe.called).to.be.true;
118+
expect(fakeBattleServiceObservable.subscribe.firstCall.args[0]).to.be.a('function');
119+
expect(fakeBattleServiceObservable.subscribe.firstCall.args[1]).to.be.a('function');
120+
});
121+
122+
it('should show a loading indicator', ()=> {
123+
expect(nativeElement.querySelector('.qa-load-indicator')).to.exist;
124+
});
125+
126+
it('should disable the button again', ()=> {
127+
expect(nativeElement.querySelector('.qa-load-button[disabled]')).to.exist;
128+
});
129+
130+
it('should emit null to stars output', ()=> {
131+
expect(starsListener.callCount).to.equal(1);
132+
expect(starsListener.firstCall.args[0]).to.equal(null);
133+
});
134+
135+
describe('success', () => {
136+
type RecursivePartial<T> = { [P in keyof T]?: RecursivePartial<T[P]>; }; // https://stackoverflow.com/a/47914631
137+
const fakeReply: RecursivePartial<CombatantInfo> = {
138+
repos: {
139+
stars: Math.ceil(Math.random() * 1000)
140+
}
141+
};
142+
143+
beforeEach(() => {
144+
const serviceSuccessCallback = fakeBattleServiceObservable.subscribe.firstCall.args[0];
145+
serviceSuccessCallback(fakeReply);
146+
fixture.detectChanges();
147+
});
148+
149+
it('should emit the received count to stars output', () => {
150+
expect(starsListener.callCount).to.equal(2); // first was when loading, second is the success
151+
expect(starsListener.lastCall.args[0]).to.equal(fakeReply.repos.stars);
152+
});
153+
154+
it('should clear the contents of the field', () => {
155+
expect(debugElement.query(By.css('.qa-github-input')).nativeElement.value).to.equal('');
156+
});
157+
158+
it('should stop showing a loading indicator', () => {
159+
expect(nativeElement.querySelector('.qa-load-indicator')).to.not.exist;
160+
});
161+
162+
it('should send data to combatantdetail child', () => {
163+
expect(nativeElement.querySelector('combatantdetail')).to.exist;
164+
const detailInstance: FakeCombatantDetail = debugElement.query(By.css('combatantdetail')).componentInstance;
165+
expect(detailInstance.data).to.equal(fakeReply);
166+
});
167+
});
168+
169+
describe('fail', () => {
170+
const fakeError = new Error('KABOOM');
171+
172+
beforeEach(() => {
173+
const serviceFailCallback = fakeBattleServiceObservable.subscribe.firstCall.args[1];
174+
serviceFailCallback(fakeError);
175+
fixture.detectChanges();
176+
});
177+
178+
it('should show an error indicator instead of the loading indicator', () => {
179+
expect(nativeElement.querySelector('.qa-load-indicator')).to.not.exist;
180+
expect(nativeElement.querySelector('.qa-error-indicator')).to.exist;
181+
});
182+
183+
it('should emit null to stars output', ()=> {
184+
expect(starsListener.callCount).to.equal(2); // first was when loading, second is the fail
185+
expect(starsListener.lastCall.args[0]).to.equal(null);
186+
});
187+
});
188+
});
189+
});
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/******************************************
2+
Unit tests for the CombatantDetail component. We need to test...
3+
4+
* template for different data inputs
5+
6+
/*******************************************/
7+
8+
// --------------- Test config ---------------
9+
10+
import { CommonModule } from '@angular/common';
11+
12+
import { CombatantDetailComponent } from '../components/combatantdetail';
13+
14+
const testModuleConfig = {
15+
imports: [CommonModule],
16+
declarations: [CombatantDetailComponent],
17+
}
18+
19+
// ------------ Moch input data -------------
20+
21+
type RecursivePartial<T> = { [P in keyof T]?: RecursivePartial<T[P]>; }; // https://stackoverflow.com/a/47914631
22+
import { CombatantInfo } from '../types';
23+
24+
const basicData: RecursivePartial<CombatantInfo> = {
25+
id: Math.random().toString(),
26+
repos: {
27+
stars: Math.ceil(Math.random()*666),
28+
mostStarred: {}
29+
}
30+
};
31+
32+
// --------------- Test suite ---------------
33+
34+
import { TestBed, getTestBed, ComponentFixture } from '@angular/core/testing';
35+
import { By } from '@angular/platform-browser';
36+
import { expect } from 'chai';
37+
import { DebugElement } from '@angular/core';
38+
39+
let fixture: ComponentFixture<CombatantDetailComponent>
40+
let instance: CombatantDetailComponent
41+
let debugElement: DebugElement
42+
let nativeElement: HTMLElement;
43+
44+
describe('CombatantDetailComponent', () => {
45+
before(() => TestBed.configureTestingModule(testModuleConfig));
46+
after(() => getTestBed().resetTestingModule());
47+
48+
beforeEach(() => {
49+
fixture = TestBed.createComponent(CombatantDetailComponent);
50+
debugElement = fixture.debugElement;
51+
instance = debugElement.componentInstance;
52+
// @ts-ignore (to let us pass partial data)
53+
instance.data = basicData;
54+
nativeElement = debugElement.nativeElement;
55+
fixture.detectChanges();
56+
});
57+
58+
it('should instantiate ok', () => {
59+
expect(instance).to.exist;
60+
});
61+
62+
it('should render a correct github link', () => {
63+
const link: HTMLAnchorElement = nativeElement.querySelector('.qa-github-link');
64+
expect(link.getAttribute('href')).to.equal(`http://github.com/${basicData.id}`);
65+
expect(link.innerHTML).to.equal(basicData.id);
66+
});
67+
68+
it('should include star count', () => {
69+
const info: HTMLElement = nativeElement.querySelector('.qa-basic-info');
70+
expect(info.innerHTML).to.include(`has ${basicData.repos.stars} stars`);
71+
});
72+
73+
it('should not render a section for most starred', () => {
74+
expect(nativeElement.querySelector('.qa-most-starred')).to.not.exist;
75+
});
76+
77+
it('should not render a language section', () => {
78+
expect(nativeElement.querySelector('.qa-language')).to.not.exist;
79+
});
80+
81+
describe('when data for most starred', () => {
82+
const dataWithMostStarred: RecursivePartial<CombatantInfo> = {
83+
id: Math.random().toString(),
84+
repos: {
85+
mostStarred: {
86+
name: Math.random().toString(),
87+
stargazers_count: Math.ceil(Math.random()*666),
88+
}
89+
}
90+
};
91+
92+
beforeEach(() => {
93+
// @ts-ignore (to let us pass partial data)
94+
instance.data = dataWithMostStarred;
95+
fixture.detectChanges();
96+
});
97+
98+
it('should render a section for most starred', () => {
99+
expect(nativeElement.querySelector('.qa-most-starred')).to.exist;
100+
});
101+
102+
it('should include star count for that repo', () => {
103+
const info: HTMLElement = nativeElement.querySelector('.qa-most-starred');
104+
expect(info.innerHTML).to.include(`with ${dataWithMostStarred.repos.mostStarred.stargazers_count} stars`);
105+
});
106+
107+
it('should have a correct link to that repo', () => {
108+
const link: HTMLAnchorElement = nativeElement.querySelector('.qa-most-starred a');
109+
const expectedURL = `http://github.com/${dataWithMostStarred.id}/${dataWithMostStarred.repos.mostStarred.name}`;
110+
expect(link.getAttribute('href')).to.equal(expectedURL);
111+
expect(link.innerHTML).to.include(dataWithMostStarred.repos.mostStarred.name);
112+
});
113+
});
114+
115+
describe('the language table', () => {
116+
const dataWithLangCount: RecursivePartial<CombatantInfo> = {
117+
repos: {
118+
mostStarred: {},
119+
repos: 5, // just needs to be more than 1
120+
languages: {
121+
last: 1,
122+
first: 10,
123+
middle: 5
124+
}
125+
}
126+
};
127+
128+
beforeEach(() => {
129+
// @ts-ignore (to let us pass partial data)
130+
instance.data = dataWithLangCount;
131+
fixture.detectChanges();
132+
});
133+
134+
it('should render a language section', () => {
135+
expect(nativeElement.querySelector('.qa-language')).to.exist;
136+
});
137+
138+
it('should render a sorted table', () => {
139+
const tableBody = nativeElement.querySelector('.qa-language tbody');
140+
expect(tableBody.querySelector('tr:first-child td:first-child').innerHTML).to.equal('first');
141+
expect(tableBody.querySelector('tr:first-child td:last-child').innerHTML).to.equal(dataWithLangCount.repos.languages.first.toString());
142+
expect(tableBody.querySelector('tr:nth-child(2) td:first-child').innerHTML).to.equal('middle');
143+
expect(tableBody.querySelector('tr:nth-child(2) td:last-child').innerHTML).to.equal(dataWithLangCount.repos.languages.middle.toString());
144+
expect(tableBody.querySelector('tr:last-child td:first-child').innerHTML).to.equal('last');
145+
expect(tableBody.querySelector('tr:last-child td:last-child').innerHTML).to.equal(dataWithLangCount.repos.languages.last.toString());
146+
});
147+
});
148+
});

src/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import * as firebase from 'firebase/app';
2-
import { User } from '@firebase/auth-types';
31

42
// ------- Types provided by our AuthService ------
53

4+
import { User } from '@firebase/auth-types';
5+
66
export interface AuthInfo {
77
token?: string
88
user?: User | null

0 commit comments

Comments
 (0)