diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..91528d9
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,12 @@
+# Files and directories created by pub
+.packages
+.pub/
+build/
+# Remove the following pattern if you wish to check in your lock file
+pubspec.lock
+
+# Directory created by dartdoc
+doc/api/
+
+*.iml
+.idea/
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..60e86fc
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,5 @@
+FROM nginx
+
+COPY ./build/web /usr/share/nginx/html
+
+EXPOSE 80
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..b17d2a3
--- /dev/null
+++ b/README.md
@@ -0,0 +1,26 @@
+## Tour of Heroes: HTTP
+
+Welcome to the example app used in the
+[Tour of Heroes: HTTP](https://webdev.dartlang.org/angular/tutorial/toh-pt6) page
+of [Dart for the web](https://webdev.dartlang.org).
+
+You can run a [hosted copy](https://webdev.dartlang.org/examples/toh-6) of this
+sample. Or run your own copy:
+
+1. Create a local copy of this repo (use the "Clone or download" button above).
+2. Get the dependencies: `pub get`
+3. Launch a development server: `pub serve`
+4. In a browser, open [http://localhost:8080](http://localhost:8080)
+
+In Dartium, you'll see the app right away. In other modern browsers,
+you'll have to wait a bit while pub converts the app.
+
+---
+
+*Note:* The content of this repository is generated from the
+[Angular docs repository][docs repo] by running the
+[dart-doc-syncer](//github.com/dart-lang/dart-doc-syncer) tool.
+If you find a problem with this sample's code, please open an [issue][].
+
+[docs repo]: //github.com/dart-lang/site-webdev/tree/master/examples/ng/doc/toh-6
+[issue]: //github.com/dart-lang/site-webdev/issues/new?title=examples/ng/doc/toh-6
diff --git a/analysis_options.yaml b/analysis_options.yaml
new file mode 100644
index 0000000..97f0908
--- /dev/null
+++ b/analysis_options.yaml
@@ -0,0 +1,15 @@
+analyzer:
+ strong-mode: true
+# exclude:
+# - path/to/excluded/files/**
+
+# Lint rules and documentation, see http://dart-lang.github.io/linter/lints
+linter:
+ rules:
+ - cancel_subscriptions
+ - hash_and_equals
+ - iterable_contains_unrelated_type
+ - list_remove_unrelated_type
+ - test_types_in_equals
+ - unrelated_type_equality_checks
+ - valid_regexps
diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml
new file mode 100644
index 0000000..5ee639d
--- /dev/null
+++ b/k8s/deployment.yaml
@@ -0,0 +1,47 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: web-service
+ namespace: aqueduct-tutorial
+spec:
+ selector:
+ app: aqueduct-tutorial
+ ports:
+ - port: 80
+ targetPort: 80
+---
+apiVersion: apps/v1beta1
+kind: Deployment
+metadata:
+ name: web-deployment
+ namespace: aqueduct-tutorial
+spec:
+ replicas: 1
+ template:
+ metadata:
+ labels:
+ app: aqueduct-tutorial
+ spec:
+ containers:
+ - name: aqueduct-tutorial
+ imagePullPolicy: Always
+ image: $IMAGE_NAME
+ ports:
+ - containerPort: 80
+---
+apiVersion: extensions/v1beta1
+kind: Ingress
+metadata:
+ name: tutorial-ingress
+ namespace: aqueduct-tutorial
+ annotations:
+ kubernetes.io/ingress.class: "nginx"
+spec:
+ rules:
+ - host: aqueduct-tutorial.stablekernel.io
+ http:
+ paths:
+ - path: /
+ backend:
+ serviceName: web-service
+ servicePort: 80
\ No newline at end of file
diff --git a/lib/app_component.css b/lib/app_component.css
new file mode 100644
index 0000000..b71b158
--- /dev/null
+++ b/lib/app_component.css
@@ -0,0 +1,28 @@
+h1 {
+ font-size: 1.2em;
+ color: #999;
+ margin-bottom: 0;
+}
+h2 {
+ font-size: 2em;
+ margin-top: 0;
+ padding-top: 0;
+}
+nav a {
+ padding: 5px 10px;
+ text-decoration: none;
+ margin-top: 10px;
+ display: inline-block;
+ background-color: #eee;
+ border-radius: 4px;
+}
+nav a:visited, a:link {
+ color: #607D8B;
+}
+nav a:hover {
+ color: #039be5;
+ background-color: #CFD8DC;
+}
+nav a.router-link-active {
+ color: #039be5;
+}
diff --git a/lib/app_component.dart b/lib/app_component.dart
new file mode 100644
index 0000000..6d117fa
--- /dev/null
+++ b/lib/app_component.dart
@@ -0,0 +1,34 @@
+import 'package:angular/angular.dart';
+import 'package:angular_router/angular_router.dart';
+
+import 'package:angular_tour_of_heroes/src/heroes_component.dart';
+import 'package:angular_tour_of_heroes/src/hero_service.dart';
+import 'package:angular_tour_of_heroes/src/dashboard_component.dart';
+import 'package:angular_tour_of_heroes/src/hero_detail_component.dart';
+
+@Component(
+ selector: 'my-app',
+ template: '''
+
{{title}}
+
+ ''',
+ styleUrls: const ['app_component.css'],
+ directives: const [ROUTER_DIRECTIVES],
+ providers: const [HeroService],
+)
+@RouteConfig(const [
+ const Route(
+ path: '/dashboard',
+ name: 'Dashboard',
+ component: DashboardComponent,
+ useAsDefault: true),
+ const Route(
+ path: '/detail/:id', name: 'HeroDetail', component: HeroDetailComponent),
+ const Route(path: '/heroes', name: 'Heroes', component: HeroesComponent)
+])
+class AppComponent {
+ final title = 'Tour of Heroes';
+}
diff --git a/lib/in_memory_data_service.dart b/lib/in_memory_data_service.dart
new file mode 100644
index 0000000..528d72f
--- /dev/null
+++ b/lib/in_memory_data_service.dart
@@ -0,0 +1,79 @@
+// Note: MockClient constructor API forces all InMemoryDataService members to
+// be static.
+import 'dart:async';
+import 'dart:convert';
+import 'dart:math';
+
+import 'package:angular/angular.dart';
+import 'package:http/http.dart';
+import 'package:http/testing.dart';
+
+import 'src/hero.dart';
+
+@Injectable()
+class InMemoryDataService extends MockClient {
+ static final _initialHeroes = [
+ {'id': 11, 'name': 'Mr. Nice'},
+ {'id': 12, 'name': 'Narco'},
+ {'id': 13, 'name': 'Bombasto'},
+ {'id': 14, 'name': 'Celeritas'},
+ {'id': 15, 'name': 'Magneta'},
+ {'id': 16, 'name': 'RubberMan'},
+ {'id': 17, 'name': 'Dynama'},
+ {'id': 18, 'name': 'Dr IQ'},
+ {'id': 19, 'name': 'Magma'},
+ {'id': 20, 'name': 'Tornado'}
+ ];
+ static List _heroesDb;
+ static int _nextId;
+
+ static Future _handler(Request request) async {
+ if (_heroesDb == null) resetDb();
+ var data;
+ switch (request.method) {
+ case 'GET':
+ final id =
+ int.parse(request.url.pathSegments.last, onError: (_) => null);
+ if (id != null) {
+ data = _heroesDb
+ .firstWhere((hero) => hero.id == id); // throws if no match
+ } else {
+ String prefix = request.url.queryParameters['name'] ?? '';
+ final regExp = new RegExp(prefix, caseSensitive: false);
+ data = _heroesDb.where((hero) => hero.name.contains(regExp)).toList();
+ }
+ break;
+ case 'POST':
+ var name = JSON.decode(request.body)['name'];
+ var newHero = new Hero(_nextId++, name);
+ _heroesDb.add(newHero);
+ data = newHero;
+ break;
+ case 'PUT':
+ var heroChanges = new Hero.fromJson(JSON.decode(request.body));
+ var targetHero = _heroesDb.firstWhere((h) => h.id == heroChanges.id);
+ targetHero.name = heroChanges.name;
+ data = targetHero;
+ break;
+ case 'DELETE':
+ var id = int.parse(request.url.pathSegments.last);
+ _heroesDb.removeWhere((hero) => hero.id == id);
+ // No data, so leave it as null.
+ break;
+ default:
+ throw 'Unimplemented HTTP method ${request.method}';
+ }
+ return new Response(JSON.encode({'data': data}), 200,
+ headers: {'content-type': 'application/json'});
+ }
+
+ static resetDb() {
+ _heroesDb = _initialHeroes.map((json) => new Hero.fromJson(json)).toList();
+ _nextId = _heroesDb.map((hero) => hero.id).fold(0, max) + 1;
+ }
+
+ static String lookUpName(int id) =>
+ _heroesDb.firstWhere((hero) => hero.id == id, orElse: null)?.name;
+
+ InMemoryDataService() : super(_handler);
+}
diff --git a/lib/src/dashboard_component.css b/lib/src/dashboard_component.css
new file mode 100644
index 0000000..096cec7
--- /dev/null
+++ b/lib/src/dashboard_component.css
@@ -0,0 +1,61 @@
+[class*='col-'] {
+ float: left;
+ padding-right: 20px;
+ padding-bottom: 20px;
+}
+[class*='col-']:last-of-type {
+ padding-right: 0;
+}
+a {
+ text-decoration: none;
+}
+*, *:after, *:before {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+h3 {
+ text-align: center; margin-bottom: 0;
+}
+h4 {
+ position: relative;
+}
+.grid {
+ margin: 0;
+}
+.col-1-4 {
+ width: 25%;
+}
+.module {
+ padding: 20px;
+ text-align: center;
+ color: #eee;
+ max-height: 120px;
+ min-width: 120px;
+ background-color: #607D8B;
+ border-radius: 2px;
+}
+.module:hover {
+ background-color: #EEE;
+ cursor: pointer;
+ color: #607d8b;
+}
+.grid-pad {
+ padding: 10px 0;
+}
+.grid-pad > [class*='col-']:last-of-type {
+ padding-right: 20px;
+}
+@media (max-width: 600px) {
+ .module {
+ font-size: 10px;
+ max-height: 75px; }
+}
+@media (max-width: 1024px) {
+ .grid {
+ margin: 0;
+ }
+ .module {
+ min-width: 60px;
+ }
+}
diff --git a/lib/src/dashboard_component.dart b/lib/src/dashboard_component.dart
new file mode 100644
index 0000000..576554e
--- /dev/null
+++ b/lib/src/dashboard_component.dart
@@ -0,0 +1,26 @@
+import 'dart:async';
+
+import 'package:angular/angular.dart';
+import 'package:angular_router/angular_router.dart';
+
+import 'hero.dart';
+import 'hero_service.dart';
+import 'hero_search_component.dart';
+
+@Component(
+ selector: 'my-dashboard',
+ templateUrl: 'dashboard_component.html',
+ styleUrls: const ['dashboard_component.css'],
+ directives: const [CORE_DIRECTIVES, HeroSearchComponent, ROUTER_DIRECTIVES],
+)
+class DashboardComponent implements OnInit {
+ List heroes;
+
+ final HeroService _heroService;
+
+ DashboardComponent(this._heroService);
+
+ Future ngOnInit() async {
+ heroes = (await _heroService.getHeroes()).skip(1).take(4).toList();
+ }
+}
diff --git a/lib/src/dashboard_component.html b/lib/src/dashboard_component.html
new file mode 100644
index 0000000..62cb65b
--- /dev/null
+++ b/lib/src/dashboard_component.html
@@ -0,0 +1,9 @@
+Top Heroes
+
+
diff --git a/lib/src/hero.dart b/lib/src/hero.dart
new file mode 100644
index 0000000..c419070
--- /dev/null
+++ b/lib/src/hero.dart
@@ -0,0 +1,13 @@
+class Hero {
+ final int id;
+ String name;
+
+ Hero(this.id, this.name);
+
+ factory Hero.fromJson(Map hero) =>
+ new Hero(_toInt(hero['id']), hero['name']);
+
+ Map toJson() => {'id': id, 'name': name};
+}
+
+int _toInt(id) => id is int ? id : int.parse(id);
diff --git a/lib/src/hero_detail_component.css b/lib/src/hero_detail_component.css
new file mode 100644
index 0000000..f6139ba
--- /dev/null
+++ b/lib/src/hero_detail_component.css
@@ -0,0 +1,29 @@
+label {
+ display: inline-block;
+ width: 3em;
+ margin: .5em 0;
+ color: #607D8B;
+ font-weight: bold;
+}
+input {
+ height: 2em;
+ font-size: 1em;
+ padding-left: .4em;
+}
+button {
+ margin-top: 20px;
+ font-family: Arial;
+ background-color: #eee;
+ border: none;
+ padding: 5px 10px;
+ border-radius: 4px;
+ cursor: pointer; cursor: hand;
+}
+button:hover {
+ background-color: #cfd8dc;
+}
+button:disabled {
+ background-color: #eee;
+ color: #ccc;
+ cursor: auto;
+}
diff --git a/lib/src/hero_detail_component.dart b/lib/src/hero_detail_component.dart
new file mode 100644
index 0000000..022a9d2
--- /dev/null
+++ b/lib/src/hero_detail_component.dart
@@ -0,0 +1,36 @@
+import 'dart:async';
+
+import 'package:angular/angular.dart';
+import 'package:angular_forms/angular_forms.dart';
+import 'package:angular_router/angular_router.dart';
+
+import 'hero.dart';
+import 'hero_service.dart';
+
+@Component(
+ selector: 'hero-detail',
+ templateUrl: 'hero_detail_component.html',
+ styleUrls: const ['hero_detail_component.css'],
+ directives: const [CORE_DIRECTIVES, formDirectives],
+)
+class HeroDetailComponent implements OnInit {
+ Hero hero;
+ final HeroService _heroService;
+ final RouteParams _routeParams;
+ final Location _location;
+
+ HeroDetailComponent(this._heroService, this._routeParams, this._location);
+
+ Future ngOnInit() async {
+ var _id = _routeParams.get('id');
+ var id = int.parse(_id ?? '', onError: (_) => null);
+ if (id != null) hero = await (_heroService.getHero(id));
+ }
+
+ Future save() async {
+ await _heroService.update(hero);
+ goBack();
+ }
+
+ void goBack() => _location.back();
+}
diff --git a/lib/src/hero_detail_component.html b/lib/src/hero_detail_component.html
new file mode 100644
index 0000000..5b0f9ff
--- /dev/null
+++ b/lib/src/hero_detail_component.html
@@ -0,0 +1,11 @@
+
+
{{hero.name}} details!
+
+ {{hero.id}}
+
+
+
+
+
+
+
diff --git a/lib/src/hero_search_component.css b/lib/src/hero_search_component.css
new file mode 100644
index 0000000..cca46f8
--- /dev/null
+++ b/lib/src/hero_search_component.css
@@ -0,0 +1,14 @@
+.search-result {
+ border-bottom: 1px solid gray;
+ border-left: 1px solid gray;
+ border-right: 1px solid gray;
+ width:195px;
+ height: 20px;
+ padding: 5px;
+ background-color: white;
+ cursor: pointer;
+}
+#search-box {
+ width: 200px;
+ height: 20px;
+}
diff --git a/lib/src/hero_search_component.dart b/lib/src/hero_search_component.dart
new file mode 100644
index 0000000..73b15ea
--- /dev/null
+++ b/lib/src/hero_search_component.dart
@@ -0,0 +1,49 @@
+import 'dart:async';
+
+import 'package:angular/angular.dart';
+import 'package:angular_router/angular_router.dart';
+import 'package:stream_transform/stream_transform.dart';
+
+import 'hero_search_service.dart';
+import 'hero.dart';
+
+@Component(
+ selector: 'hero-search',
+ templateUrl: 'hero_search_component.html',
+ styleUrls: const ['hero_search_component.css'],
+ directives: const [CORE_DIRECTIVES],
+ providers: const [HeroSearchService],
+ pipes: const [COMMON_PIPES],
+)
+class HeroSearchComponent implements OnInit {
+ HeroSearchService _heroSearchService;
+ Router _router;
+
+ Stream> heroes;
+ StreamController _searchTerms = new StreamController.broadcast();
+
+ HeroSearchComponent(this._heroSearchService, this._router) {}
+
+ // Push a search term into the stream.
+ void search(String term) => _searchTerms.add(term);
+
+ Future ngOnInit() async {
+ heroes = _searchTerms.stream
+ .transform(debounce(new Duration(milliseconds: 300)))
+ .distinct()
+ .transform(switchMap((term) => term.isEmpty
+ ? new Stream>.fromIterable([[]])
+ : _heroSearchService.search(term).asStream()))
+ .handleError((e) {
+ print(e); // for demo purposes only
+ });
+ }
+
+ void gotoDetail(Hero hero) {
+ var link = [
+ 'HeroDetail',
+ {'id': hero.id.toString()}
+ ];
+ _router.navigate(link);
+ }
+}
diff --git a/lib/src/hero_search_component.html b/lib/src/hero_search_component.html
new file mode 100644
index 0000000..e6848e8
--- /dev/null
+++ b/lib/src/hero_search_component.html
@@ -0,0 +1,12 @@
+
diff --git a/lib/src/hero_search_service.dart b/lib/src/hero_search_service.dart
new file mode 100644
index 0000000..a683f80
--- /dev/null
+++ b/lib/src/hero_search_service.dart
@@ -0,0 +1,32 @@
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:angular/angular.dart';
+import 'package:http/http.dart';
+
+import 'hero.dart';
+
+@Injectable()
+class HeroSearchService {
+ final Client _http;
+
+ HeroSearchService(this._http);
+
+ Future> search(String term) async {
+ try {
+ final response = await _http.get('http://localhost:8888/heroes?name=$term');
+ return _extractData(response)
+ .map((json) => new Hero.fromJson(json))
+ .toList();
+ } catch (e) {
+ throw _handleError(e);
+ }
+ }
+
+ dynamic _extractData(Response resp) => JSON.decode(resp.body);
+
+ Exception _handleError(dynamic e) {
+ print(e); // for demo purposes only
+ return new Exception('Server error; cause: $e');
+ }
+}
diff --git a/lib/src/hero_service.dart b/lib/src/hero_service.dart
new file mode 100644
index 0000000..b19cb55
--- /dev/null
+++ b/lib/src/hero_service.dart
@@ -0,0 +1,75 @@
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:angular/angular.dart';
+import 'package:http/http.dart';
+
+import 'hero.dart';
+
+@Injectable()
+class HeroService {
+ static final _headers = {'Content-Type': 'application/json'};
+ static const _heroesUrl = 'http://localhost:8888/heroes'; // URL to web API
+
+ final Client _http;
+
+ HeroService(this._http);
+
+ Future> getHeroes() async {
+ try {
+ final response = await _http.get(_heroesUrl);
+ final heroes = _extractData(response)
+ .map((value) => new Hero.fromJson(value))
+ .toList();
+ return heroes;
+ } catch (e) {
+ throw _handleError(e);
+ }
+ }
+
+ dynamic _extractData(Response resp) => JSON.decode(resp.body);
+
+ Exception _handleError(dynamic e) {
+ print(e); // for demo purposes only
+ return new Exception('Server error; cause: $e');
+ }
+
+ Future getHero(int id) async {
+ try {
+ final response = await _http.get('$_heroesUrl/$id');
+ return new Hero.fromJson(_extractData(response));
+ } catch (e) {
+ throw _handleError(e);
+ }
+ }
+
+ Future create(String name) async {
+ try {
+ final response = await _http.post(_heroesUrl,
+ headers: _headers, body: JSON.encode({'name': name}));
+ return new Hero.fromJson(_extractData(response));
+ } catch (e) {
+ throw _handleError(e);
+ }
+ }
+
+ Future update(Hero hero) async {
+ try {
+ final url = '$_heroesUrl/${hero.id}';
+ final response =
+ await _http.put(url, headers: _headers, body: JSON.encode(hero));
+ return new Hero.fromJson(_extractData(response));
+ } catch (e) {
+ throw _handleError(e);
+ }
+ }
+
+ Future delete(int id) async {
+ try {
+ final url = '$_heroesUrl/$id';
+ await _http.delete(url, headers: _headers);
+ } catch (e) {
+ throw _handleError(e);
+ }
+ }
+}
diff --git a/lib/src/heroes_component.css b/lib/src/heroes_component.css
new file mode 100644
index 0000000..c81f0e4
--- /dev/null
+++ b/lib/src/heroes_component.css
@@ -0,0 +1,66 @@
+.selected {
+ background-color: #CFD8DC !important;
+ color: white;
+}
+.heroes {
+ margin: 0 0 2em 0;
+ list-style-type: none;
+ padding: 0;
+ width: 15em;
+}
+.heroes li {
+ cursor: pointer;
+ position: relative;
+ left: 0;
+ background-color: #EEE;
+ margin: .5em;
+ padding: .3em 0;
+ height: 1.6em;
+ border-radius: 4px;
+}
+.heroes li:hover {
+ color: #607D8B;
+ background-color: #DDD;
+ left: .1em;
+}
+.heroes li.selected:hover {
+ background-color: #BBD8DC !important;
+ color: white;
+}
+.heroes .text {
+ position: relative;
+ top: -3px;
+}
+.heroes .badge {
+ display: inline-block;
+ font-size: small;
+ color: white;
+ padding: 0.8em 0.7em 0 0.7em;
+ background-color: #607D8B;
+ line-height: 1em;
+ position: relative;
+ left: -1px;
+ top: -4px;
+ height: 1.8em;
+ margin-right: .8em;
+ border-radius: 4px 0 0 4px;
+}
+button {
+ font-family: Arial;
+ background-color: #eee;
+ border: none;
+ padding: 5px 10px;
+ border-radius: 4px;
+ cursor: pointer;
+ cursor: hand;
+}
+button:hover {
+ background-color: #cfd8dc;
+}
+button.delete {
+ float:right;
+ margin-top: 2px;
+ margin-right: .8em;
+ background-color: gray !important;
+ color:white;
+}
diff --git a/lib/src/heroes_component.dart b/lib/src/heroes_component.dart
new file mode 100644
index 0000000..40a1f91
--- /dev/null
+++ b/lib/src/heroes_component.dart
@@ -0,0 +1,50 @@
+import 'dart:async';
+
+import 'package:angular/angular.dart';
+import 'package:angular_router/angular_router.dart';
+
+import 'hero.dart';
+import 'hero_detail_component.dart';
+import 'hero_service.dart';
+
+@Component(
+ selector: 'my-heroes',
+ templateUrl: 'heroes_component.html',
+ styleUrls: const ['heroes_component.css'],
+ directives: const [CORE_DIRECTIVES, HeroDetailComponent],
+ pipes: const [COMMON_PIPES],
+)
+class HeroesComponent implements OnInit {
+ final HeroService _heroService;
+ final Router _router;
+ List heroes;
+ Hero selectedHero;
+
+ HeroesComponent(this._heroService, this._router);
+
+ Future getHeroes() async {
+ heroes = await _heroService.getHeroes();
+ }
+
+ Future add(String name) async {
+ name = name.trim();
+ if (name.isEmpty) return;
+ heroes.add(await _heroService.create(name));
+ selectedHero = null;
+ }
+
+ Future delete(Hero hero) async {
+ await _heroService.delete(hero.id);
+ heroes.remove(hero);
+ if (selectedHero == hero) selectedHero = null;
+ }
+
+ void ngOnInit() => getHeroes();
+
+ void onSelect(Hero hero) => selectedHero = hero;
+
+ Future gotoDetail() => _router.navigate([
+ 'HeroDetail',
+ {'id': selectedHero.id.toString()}
+ ]);
+}
diff --git a/lib/src/heroes_component.html b/lib/src/heroes_component.html
new file mode 100644
index 0000000..27cdf99
--- /dev/null
+++ b/lib/src/heroes_component.html
@@ -0,0 +1,22 @@
+My Heroes
+
+
+
+
+
+ -
+ {{hero.id}}
+ {{hero.name}}
+
+
+
+
+
+ {{selectedHero.name | uppercase}} is my hero
+
+
+
diff --git a/pubspec.yaml b/pubspec.yaml
new file mode 100644
index 0000000..2a8babb
--- /dev/null
+++ b/pubspec.yaml
@@ -0,0 +1,27 @@
+name: angular_tour_of_heroes
+description: Tour of Heroes
+version: 0.0.1
+environment:
+ sdk: '>=1.24.0 <2.0.0'
+dependencies:
+ angular: ^4.0.0
+ angular_forms: ^1.0.0
+ angular_router: ^1.0.2
+ http: ^0.11.0
+ stream_transform: ^0.0.6
+
+dev_dependencies:
+ angular_test: ^1.0.0
+ browser: ^0.10.0
+ dart_to_js_script_rewriter: ^1.0.1
+ mockito: ^2.0.2
+ test: ^0.12.21
+
+transformers:
+- angular:
+ entry_points:
+ - web/main.dart
+ - test/**_test.dart
+- test/pub_serve:
+ $include: test/**_test.dart
+- dart_to_js_script_rewriter
diff --git a/test/README.md b/test/README.md
new file mode 100644
index 0000000..cb4d4b2
--- /dev/null
+++ b/test/README.md
@@ -0,0 +1,7 @@
+Changes/additions relative to toh-5:
+
+- Test bed: add provider for `Client`.
+- Reset (mock) DB during setup.
+- Test waiting for element update before proceeding.
+- Test interaction with (mock) db backend.
+- Use `@WithVisibleText()` PO field annotation.
\ No newline at end of file
diff --git a/test/all_test.dart b/test/all_test.dart
new file mode 100644
index 0000000..036e63e
--- /dev/null
+++ b/test/all_test.dart
@@ -0,0 +1,19 @@
+@Tags(const ['aot'])
+@TestOn('browser')
+library heroes_test;
+
+import 'package:angular/angular.dart';
+import 'package:test/test.dart';
+
+import 'dashboard.dart' as dashboard;
+import 'heroes.dart' as heroes;
+import 'hero_detail.dart' as hero_detail;
+import 'hero_search.dart' as hero_search;
+
+@AngularEntrypoint()
+void main() {
+ group('dashboard:', dashboard.main);
+ group('heroes:', heroes.main);
+ group('hero detail:', hero_detail.main);
+ group('hero search:', hero_search.main);
+}
diff --git a/test/all_test.html b/test/all_test.html
new file mode 100644
index 0000000..4c191d1
--- /dev/null
+++ b/test/all_test.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+ Tests
+
+
+
+
+
+
+
+
+
+
diff --git a/test/app_po.dart b/test/app_po.dart
new file mode 100644
index 0000000..0fbcce6
--- /dev/null
+++ b/test/app_po.dart
@@ -0,0 +1,17 @@
+import 'dart:async';
+
+import 'package:pageloader/objects.dart';
+import 'utils.dart';
+
+class AppPO {
+ @ByTagName('h1')
+ PageLoaderElement _h1;
+
+ @ByCss('nav a')
+ List _tabLinks;
+
+ Future get pageTitle => _h1.visibleText;
+
+ Future> get tabTitles =>
+ inIndexOrder(_tabLinks.map((el) => el.visibleText)).toList();
+}
diff --git a/test/app_test.dart b/test/app_test.dart
new file mode 100644
index 0000000..b67827c
--- /dev/null
+++ b/test/app_test.dart
@@ -0,0 +1,55 @@
+@Skip('AppComponent tests need bootstrap equivalent for the Router init')
+@Tags(const ['aot'])
+@TestOn('browser')
+
+import 'package:angular/angular.dart';
+import 'package:angular_router/angular_router.dart';
+import 'package:angular_test/angular_test.dart';
+import 'package:angular_tour_of_heroes/app_component.dart';
+import 'package:mockito/mockito.dart';
+import 'package:test/test.dart';
+
+import 'app_po.dart';
+
+NgTestFixture fixture;
+AppPO appPO;
+
+final mockPlatformLocation = new MockPlatformLocation();
+
+class MockPlatformLocation extends Mock implements PlatformLocation {}
+
+@AngularEntrypoint()
+void main() {
+ final providers = [
+ provide(APP_BASE_HREF, useValue: '/'),
+ provide(ROUTER_PRIMARY_COMPONENT, useValue: AppComponent),
+ provide(PlatformLocation, useValue: mockPlatformLocation),
+ ];
+
+ final testBed = new NgTestBed().addProviders(providers);
+
+ setUpAll(() async {
+ // Seems like we'd need to do something equivalent to:
+ // bootstrap(AppComponent);
+ });
+
+ setUp(() async {
+ fixture = await testBed.create();
+ appPO = await fixture.resolvePageObject(AppPO);
+ });
+
+ tearDown(disposeAnyRunningTest);
+
+ group('Basics:', basicTests);
+}
+
+void basicTests() {
+ test('page title', () async {
+ expect(await appPO.pageTitle, 'Tour of Heroes');
+ });
+
+ test('tab titles', () async {
+ final expectTitles = ['Dashboard', 'Heroes'];
+ expect(await appPO.tabTitles, expectTitles);
+ });
+}
diff --git a/test/dashboard.dart b/test/dashboard.dart
new file mode 100644
index 0000000..313bd2c
--- /dev/null
+++ b/test/dashboard.dart
@@ -0,0 +1,91 @@
+@Tags(const ['aot'])
+@TestOn('browser')
+
+import 'dart:async';
+import 'package:angular/angular.dart';
+import 'package:angular_router/angular_router.dart';
+import 'package:angular_test/angular_test.dart';
+import 'package:angular_tour_of_heroes/app_component.dart';
+import 'package:angular_tour_of_heroes/in_memory_data_service.dart';
+import 'package:angular_tour_of_heroes/src/dashboard_component.dart';
+import 'package:angular_tour_of_heroes/src/hero_service.dart';
+import 'package:http/http.dart';
+import 'package:mockito/mockito.dart';
+import 'package:test/test.dart';
+
+import 'dashboard_po.dart';
+
+NgTestFixture fixture;
+DashboardPO po;
+
+final mockPlatformLocation = new MockPlatformLocation();
+
+class MockPlatformLocation extends Mock implements PlatformLocation {}
+
+@AngularEntrypoint()
+void main() {
+ final providers = new List.from(ROUTER_PROVIDERS)
+ ..addAll([
+ provide(APP_BASE_HREF, useValue: '/'),
+ provide(Client, useClass: InMemoryDataService),
+ provide(ROUTER_PRIMARY_COMPONENT, useValue: AppComponent),
+ provide(PlatformLocation, useValue: mockPlatformLocation),
+ HeroService,
+ ]);
+ final testBed = new NgTestBed().addProviders(providers);
+
+ setUpAll(() async {
+ when(mockPlatformLocation.pathname).thenReturn('');
+ when(mockPlatformLocation.search).thenReturn('');
+ when(mockPlatformLocation.hash).thenReturn('');
+ when(mockPlatformLocation.getBaseHrefFromDOM()).thenReturn('');
+ });
+
+ setUp(() async {
+ fixture = await testBed.create();
+ po = await fixture.resolvePageObject(DashboardPO);
+ });
+
+ tearDown(disposeAnyRunningTest);
+
+ test('title', () async {
+ expect(await po.title, 'Top Heroes');
+ });
+
+ test('show top heroes', () async {
+ final expectedNames = ['Narco', 'Bombasto', 'Celeritas', 'Magneta'];
+ expect(await po.heroNames, expectedNames);
+ });
+
+ test('select hero and navigate to detail', () async {
+ clearInteractions(mockPlatformLocation);
+ await po.selectHero(3);
+ final c = verify(mockPlatformLocation.pushState(any, any, captureAny));
+ expect(c.captured.single, '/detail/15');
+ });
+
+ test('no search no heroes', () async {
+ expect(await po.heroesFound, []);
+ });
+
+ group('Search hero:', heroSearchTests);
+}
+
+void heroSearchTests() {
+ final matchedHeroNames = [
+ 'Magneta',
+ 'RubberMan',
+ 'Dynama',
+ 'Magma',
+ ];
+
+ setUp(() async {
+ await po.search.type('ma');
+ await new Future.delayed(const Duration(seconds: 1));
+ po = await fixture.resolvePageObject(DashboardPO);
+ });
+
+ test('list matching heroes', () async {
+ expect(await po.heroesFound, matchedHeroNames);
+ });
+}
diff --git a/test/dashboard_po.dart b/test/dashboard_po.dart
new file mode 100644
index 0000000..a02b10c
--- /dev/null
+++ b/test/dashboard_po.dart
@@ -0,0 +1,31 @@
+import 'dart:async';
+
+import 'package:pageloader/objects.dart';
+import 'utils.dart';
+
+class DashboardPO {
+ @FirstByCss('h3')
+ PageLoaderElement _title;
+
+ @ByTagName('a')
+ List _heroes;
+
+ @ByTagName('input')
+ PageLoaderElement search;
+
+ @ByCss('div[id="search-component"] div div')
+ List _heroesFound;
+
+ @ByCss('div[id="search-component"]')
+ PageLoaderElement heroSearchDiv;
+
+ Future get title => _title.visibleText;
+
+ Future> get heroNames =>
+ inIndexOrder(_heroes.map((el) => el.visibleText)).toList();
+
+ Future selectHero(int index) => _heroes[index].click();
+
+ Future> get heroesFound =>
+ inIndexOrder(_heroesFound.map((el) => el.visibleText)).toList();
+}
diff --git a/test/hero_detail.dart b/test/hero_detail.dart
new file mode 100644
index 0000000..b1a8c2e
--- /dev/null
+++ b/test/hero_detail.dart
@@ -0,0 +1,98 @@
+@Tags(const ['aot'])
+@TestOn('browser')
+
+import 'package:angular/angular.dart';
+import 'package:angular_router/angular_router.dart';
+import 'package:angular_test/angular_test.dart';
+import 'package:angular_tour_of_heroes/in_memory_data_service.dart';
+import 'package:angular_tour_of_heroes/src/hero_detail_component.dart';
+import 'package:angular_tour_of_heroes/src/hero_service.dart';
+import 'package:http/http.dart';
+import 'package:mockito/mockito.dart';
+import 'package:test/test.dart';
+
+import 'hero_detail_po.dart';
+
+NgTestFixture fixture;
+HeroDetailPO po;
+
+class MockPlatformLocation extends Mock implements PlatformLocation {}
+
+final mockPlatformLocation = new MockPlatformLocation();
+
+@AngularEntrypoint()
+void main() {
+ final baseProviders = new List.from(ROUTER_PROVIDERS)
+ ..addAll([
+ provide(APP_BASE_HREF, useValue: '/'),
+ provide(Client, useClass: InMemoryDataService),
+ provide(PlatformLocation, useValue: mockPlatformLocation),
+ provide(RouteParams, useValue: new RouteParams({})),
+ HeroService,
+ ]);
+ final testBed =
+ new NgTestBed().addProviders(baseProviders);
+
+ setUp(() {
+ InMemoryDataService.resetDb();
+ });
+
+ tearDown(disposeAnyRunningTest);
+
+ test('No initial hero results in an empty view', () async {
+ fixture = await testBed.create();
+ expect(fixture.rootElement.text.trim(), '');
+ });
+
+ const targetHero = const {'id': 15, 'name': 'Magneta'};
+
+ group('${targetHero['name']} initial hero:', () {
+ const nameSuffix = 'X';
+ final Map updatedHero = {
+ 'id': targetHero['id'],
+ 'name': "${targetHero['name']}$nameSuffix"
+ };
+
+ setUp(() async {
+ final groupTestBed = testBed.fork().addProviders([
+ provide(RouteParams,
+ useValue: new RouteParams({'id': targetHero['id'].toString()}))
+ ]);
+ fixture = await groupTestBed.create();
+ po = await fixture.resolvePageObject(HeroDetailPO);
+ });
+
+ test('show hero details', () async {
+ expect(await po.heroFromDetails, targetHero);
+ });
+
+ test('back button', () async {
+ await po.back();
+ verify(mockPlatformLocation.back());
+ });
+
+ group('Update name:', () {
+ setUp(() async {
+ await po.type(nameSuffix);
+ });
+
+ test('show updated name', () async {
+ expect(await po.heroFromDetails, updatedHero);
+ });
+
+ test('discard changes', () async {
+ await po.back();
+ verify(mockPlatformLocation.back());
+ final name = InMemoryDataService.lookUpName(targetHero['id']);
+ expect(name, targetHero['name']);
+ });
+
+ test('save changes and go back', () async {
+ await po.save();
+ verify(mockPlatformLocation.back());
+ final name = InMemoryDataService.lookUpName(targetHero['id']);
+ expect(name, updatedHero['name']);
+ });
+ });
+ });
+}
diff --git a/test/hero_detail_po.dart b/test/hero_detail_po.dart
new file mode 100644
index 0000000..b09d38d
--- /dev/null
+++ b/test/hero_detail_po.dart
@@ -0,0 +1,37 @@
+import 'dart:async';
+
+import 'package:pageloader/objects.dart';
+import 'utils.dart';
+
+class HeroDetailPO {
+ @FirstByCss('div h2')
+ PageLoaderElement _title; // e.g. 'Mr Freeze details!'
+
+ @FirstByCss('div div')
+ PageLoaderElement _id;
+
+ @ByTagName('input')
+ PageLoaderElement _input;
+
+ @ByTagName('button')
+ @WithVisibleText('Back')
+ PageLoaderElement _back;
+
+ @ByTagName('button')
+ @WithVisibleText('Save')
+ PageLoaderElement _save;
+
+ Future