Skip to content
This repository was archived by the owner on Dec 4, 2017. It is now read-only.

docs(hierarchical-di): Correct avoidance example (#3086) and more #3091

Merged
merged 2 commits into from
Jan 13, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 87 additions & 40 deletions public/docs/_examples/hierarchical-dependency-injection/e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -1,56 +1,103 @@
import { browser, element, by } from 'protractor';
'use strict'; // necessary for es6 output in node

describe('Hierarchical dependency injection', function () {
import { browser, by, element } from 'protractor';

beforeEach(function () {
describe('Hierarchical dependency injection', () => {

beforeAll(() => {
browser.get('');
});

it('should open with a card view', function () {
expect(element.all(by.cssContainingText('button', 'edit')).get(0).isDisplayed()).toBe(true,
'edit button should be displayed');
});
describe('Heroes Scenario', () => {
let page = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting usage of the Page Objects pattern. There really is no reason to have it separately in smaller tests.

Copy link
Contributor Author

@wardbell wardbell Jan 12, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My reason was that I was gathering together the queries I wanted to do in tests. Wasn't sure how many tests I would write but I was pretty sure the affordances should be involved.

Did it make the tests hard to read?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not at all, I think we should have more page objects like this in our e2e tests overall.

heroName: '',
income: '',

it('should have multiple heroes listed', function () {
expect(element.all(by.css('heroes-list li')).count()).toBeGreaterThan(1);
});
// queries
heroEl: element.all(by.css('heroes-list li')).get(0), // first hero
heroCardEl: element(by.css('heroes-list hero-tax-return')), // first hero tax-return
taxReturnNameEl: element.all(by.css('heroes-list hero-tax-return #name')).get(0),
incomeInputEl: element.all(by.css('heroes-list hero-tax-return input')).get(0),
cancelButtonEl: element(by.cssContainingText('heroes-list hero-tax-return button', 'Cancel')),
closeButtonEl: element(by.cssContainingText('heroes-list hero-tax-return button', 'Close')),
saveButtonEl: element(by.cssContainingText('heroes-list hero-tax-return button', 'Save'))
};

it('should change to editor view after selection', function () {
let editButtonEle = element.all(by.cssContainingText('button', 'edit')).get(0);
editButtonEle.click().then(function() {
expect(editButtonEle.isDisplayed()).toBe(false, 'edit button should be hidden after selection');
it('should list multiple heroes', () => {
expect(element.all(by.css('heroes-list li')).count()).toBeGreaterThan(1);
});

it('should show no hero tax-returns at the start', () => {
expect(element.all(by.css('heroes-list li hero-tax-return')).count()).toBe(0);
});

it('should open first hero in hero-tax-return view after click', () => {
page.heroEl.getText()
.then(val => {
page.heroName = val;
})
.then(() => page.heroEl.click())
.then(() => {
expect(page.heroCardEl.isDisplayed()).toBe(true);
});
});

it('hero tax-return should have first hero\'s name', () => {
// Not `page.tax-returnNameInputEl.getAttribute('value')` although later that is essential
expect(page.taxReturnNameEl.getText()).toEqual(page.heroName);
});

it('should be able to cancel change', () => {
page.incomeInputEl.clear()
.then(() => page.incomeInputEl.sendKeys('777'))
.then(() => {
expect(page.incomeInputEl.getAttribute('value')).toBe('777', 'income should be 777');
return page.cancelButtonEl.click();
})
.then(() => {
expect(page.incomeInputEl.getAttribute('value')).not.toBe('777', 'income should not be 777');
});
});

it('should be able to save change', () => {
page.incomeInputEl.clear()
.then(() => page.incomeInputEl.sendKeys('999'))
.then(() => {
expect(page.incomeInputEl.getAttribute('value')).toBe('999', 'income should be 999');
return page.saveButtonEl.click();
})
.then(() => {
expect(page.incomeInputEl.getAttribute('value')).toBe('999', 'income should still be 999');
});
});

it('should be able to close tax-return', () => {
page.saveButtonEl.click()
.then(() => {
expect(element.all(by.css('heroes-list li hero-tax-return')).count()).toBe(0);
});
});
});

it('should be able to save editor change', function () {
testEdit(true);
});

it('should be able to cancel editor change', function () {
testEdit(false);
describe('Villains Scenario', () => {
it('should list multiple villains', () => {
expect(element.all(by.css('villains-list li')).count()).toBeGreaterThan(1);
});
});

function testEdit(shouldSave: boolean) {
// select 2nd ele
let heroEle = element.all(by.css('heroes-list li')).get(1);
// get the 2nd span which is the name of the hero
let heroNameEle = heroEle.all(by.css('hero-card span')).get(1);
let editButtonEle = heroEle.element(by.cssContainingText('button', 'edit'));
editButtonEle.click().then(function() {
let inputEle = heroEle.element(by.css('hero-editor input'));
return inputEle.sendKeys('foo');
}).then(function() {
let buttonName = shouldSave ? 'save' : 'cancel';
let buttonEle = heroEle.element(by.cssContainingText('button', buttonName));
return buttonEle.click();
}).then(function() {
if (shouldSave) {
expect(heroNameEle.getText()).toContain('foo');
} else {
expect(heroNameEle.getText()).not.toContain('foo');
}
});
}
describe('Cars Scenario', () => {

it('A-component should use expected services', () => {
expect(element(by.css('a-car')).getText()).toContain('C1-E1-T1');
});

it('B-component should use expected services', () => {
expect(element(by.css('b-car')).getText()).toContain('C2-E2-T1');
});

it('C-component should use expected services', () => {
expect(element(by.css('c-car')).getText()).toContain('C3-E2-T1');
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Component } from '@angular/core';

@Component({
selector: 'my-app',
template: `
<label><input type="checkbox" [checked]="showHeroes" (change)="showHeroes=!showHeroes">Heroes</label>
<label><input type="checkbox" [checked]="showVillains" (change)="showVillains=!showVillains">Villains</label>
<label><input type="checkbox" [checked]="showCars" (change)="showCars=!showCars">Cars</label>

<h1>Hierarchical Dependency Injection</h1>

<heroes-list *ngIf="showHeroes"></heroes-list>
<villains-list *ngIf="showVillains"></villains-list>
<my-cars *ngIf="showCars"></my-cars>
`
})
export class AppComponent {
showCars = true;
showHeroes = true;
showVillains = true;
}
Original file line number Diff line number Diff line change
@@ -1,43 +1,33 @@
// #docregion
import { NgModule } from '@angular/core';
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { FormsModule } from '@angular/forms';

import { HeroesListComponent } from './heroes-list.component';
import { HeroEditorComponent } from './hero-editor.component';
import { HeroCardComponent } from './hero-card.component';
import { HeroesService } from './heroes.service';
import { AppComponent } from './app.component';
import { HeroTaxReturnComponent } from './hero-tax-return.component';
import { HeroesListComponent } from './heroes-list.component';
import { HeroesService } from './heroes.service';
import { VillainsListComponent } from './villains-list.component';

import { carComponents, carServices } from './car.components';

@NgModule({
imports: [
BrowserModule,
FormsModule
],
providers: [ HeroesService ],
providers: [
carServices,
HeroesService
],
declarations: [
AppComponent,
carComponents,
HeroesListComponent,
HeroCardComponent,
HeroEditorComponent
HeroTaxReturnComponent,
VillainsListComponent
],
bootstrap: [ HeroesListComponent ]
bootstrap: [ AppComponent ]
})
export class AppModule { }

/* Documentation artifact below
// #docregion bad-alternative
// Don't do this!
@NgModule({
imports: [
BrowserModule,
FormsModule
],
providers: [ HeroesService, RestoreService ],
declarations: [ HeroesListComponent ],
bootstrap: [
HeroesListComponent,
HeroCardComponent,
HeroEditorComponent
]
})
// #enddocregion bad-alternative
*/
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { Component } from '@angular/core';

import {
CarService, CarService2, CarService3,
EngineService, EngineService2, TiresService
} from './car.services';

////////// CCarComponent ////////////
@Component({
selector: 'c-car',
template: `<div>C: {{description}}</div>`,
providers: [
{ provide: CarService, useClass: CarService3 }
]
})
export class CCarComponent {
description: string;
constructor(carService: CarService) {
this.description = `${carService.getCar().description} (${carService.name})`;
}
}

////////// BCarComponent ////////////
@Component({
selector: 'b-car',
template: `
<div>B: {{description}}</div>
<c-car></c-car>
`,
providers: [
{ provide: CarService, useClass: CarService2 },
{ provide: EngineService, useClass: EngineService2 }
]
})
export class BCarComponent {
description: string;
constructor(carService: CarService) {
this.description = `${carService.getCar().description} (${carService.name})`;
}
}

////////// ACarComponent ////////////
@Component({
selector: 'a-car',
template: `
<div>A: {{description}}</div>
<b-car></b-car>`
})
export class ACarComponent {
description: string;
constructor(carService: CarService) {
this.description = `${carService.getCar().description} (${carService.name})`;
}
}
////////// CarsComponent ////////////
@Component({
selector: 'my-cars',
template: `
<h3>Cars</h3>
<a-car></a-car>`
})
export class CarsComponent { }

////////////////

export const carComponents = [
CarsComponent,
ACarComponent, BCarComponent, CCarComponent
];

// generic car-related services
export const carServices = [
CarService, EngineService, TiresService
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { Injectable } from '@angular/core';

/// Model ///
export class Car {
name = 'Avocado Motors';
constructor(public engine: Engine, public tires: Tires) { }

get description() {
return `${this.name} car with ` +
`${this.engine.cylinders} cylinders and ${this.tires.make} tires.`;
}
}

export class Engine {
cylinders = 4;
}

export class Tires {
make = 'Flintstone';
model = 'Square';
}

//// Engine services ///
@Injectable()
export class EngineService {
id = 'E1';
getEngine() { return new Engine(); }
}

@Injectable()
export class EngineService2 {
id = 'E2';
getEngine() {
const eng = new Engine();
eng.cylinders = 8;
return eng;
}
}

//// Tire services ///
@Injectable()
export class TiresService {
id = 'T1';
getTires() { return new Tires(); }
}

/// Car Services ///
@Injectable()
export class CarService {
id = 'C1';
constructor(
protected engineService: EngineService,
protected tiresService: TiresService) { }

getCar() {
return new Car(
this.engineService.getEngine(),
this.tiresService.getTires());
}

get name() {
return `${this.id}-${this.engineService.id}-${this.tiresService.id}`;
}
}

@Injectable()
export class CarService2 extends CarService {
id = 'C2';
constructor(
protected engineService: EngineService,
protected tiresService: TiresService) {
super(engineService, tiresService);
}
getCar() {
const car = super.getCar();
car.name = 'BamBam Motors, BroVan 2000';
return car;
}
}

@Injectable()
export class CarService3 extends CarService2 {
id = 'C3';
constructor(
protected engineService: EngineService,
protected tiresService: TiresService) {
super(engineService, tiresService);
}
getCar() {
const car = super.getCar();
car.name = 'Chizzamm Motors, Calico UltraMax Supreme';
return car;
}
}

Loading