[results feed] Add tests for try-results UI angular component

Change-Id: If3cbb84f5f77ad6bca30d615fd3b30410ba9322b
TBR: athom@google.com
Reviewed-on: https://dart-review.googlesource.com/c/dart_ci/+/131081
Reviewed-by: William Hesse <whesse@google.com>
diff --git a/results_feed/lib/src/components/try_results_component.html b/results_feed/lib/src/components/try_results_component.html
index bf9c4e3..ab18155 100644
--- a/results_feed/lib/src/components/try_results_component.html
+++ b/results_feed/lib/src/components/try_results_component.html
@@ -15,7 +15,8 @@
       [selected]="selected"
       failuresOnly>
   </results-selector-panel>
-  <div *ngFor="let c of changeGroup.comments">
+  <div *ngFor="let c of changeGroup.comments"
+      class="comment">
     <b>{{c.approvedText()}}</b>
     <span class="nowrap">{{formattedDate(c.created)}}</span>
     {{formattedEmail(c.author)}}<br>
diff --git a/results_feed/lib/src/services/firestore_service.dart b/results_feed/lib/src/services/firestore_service.dart
index bc1c6c1..134cc90 100644
--- a/results_feed/lib/src/services/firestore_service.dart
+++ b/results_feed/lib/src/services/firestore_service.dart
@@ -226,7 +226,7 @@
   Future logIn() async {
     try {
       await app.auth().signInWithEmailAndPassword(
-          'dartresultsfeedtestuser@example.com', r'');
+          'dartresultsfeedtestaccount2@example.com', r'');
       // Password must be entered locally before testing.
       // Because this is running in a browser, cannot read password from
       // a file or environment variable. Investigate what testing framework
@@ -267,4 +267,15 @@
       await batch.commit();
     }
   }
+
+  Future<void> deleteCommentsForReview(int review) async {
+    final snapshot = await app
+        .firestore()
+        .collection('comments')
+        .where('review', '==', review)
+        .get();
+    for (final document in snapshot.docs) {
+      await document.ref.delete();
+    }
+  }
 }
diff --git a/results_feed/test/page_objects/results_panel_po.dart b/results_feed/test/page_objects/results_panel_po.dart
new file mode 100644
index 0000000..fef21e5
--- /dev/null
+++ b/results_feed/test/page_objects/results_panel_po.dart
@@ -0,0 +1,126 @@
+// Copyright (c) 2020, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:pageloader/html.dart';
+
+part 'results_panel_po.g.dart';
+
+/// The ResultsSelectorPanel and ResultsPanel components are very similar,
+/// but the ResultsSelectorPanel has checkboxes to select tests.
+/// I am using a common PageObject interface ResultsPanelPO to interact
+/// with page objects for both components.  Because of code generation,
+/// I can't share common code easily between the two classes by extending
+/// ResultsPanelPO. Using a mixin to share implementations has many
+/// complications in this case, so is not worth it for these two classes.
+@PageObject()
+abstract class ResultsPanelPO {
+  ResultsPanelPO();
+  factory ResultsPanelPO.create(PageLoaderElement context) =
+      $ResultsPanelPO.create;
+
+  @ByCss('results-panel > div')
+  List<PageLoaderElement> get _configurationGroups;
+
+  PageLoaderElement configurationGroup(String searchText) {
+    return _configurationGroups.firstWhere(
+        (group) => group.innerText.contains(searchText),
+        orElse: () => null);
+  }
+
+  PageLoaderElement resultsGroup(
+          PageLoaderElement configurationGroup, String resultsText) =>
+      _resultGroups(configurationGroup).firstWhere(
+          (resultGroup) => resultGroup.innerText.contains(resultsText),
+          orElse: () => null);
+
+  List<PageLoaderElement> _resultGroups(PageLoaderElement configurationGroup) {
+    return configurationGroup.getElementsByCss('results-panel > div > div');
+  }
+
+  List<PageLoaderElement> _results(PageLoaderElement resultGroup) {
+    return resultGroup
+        .getElementsByCss('results-panel > div > div > span.indent');
+  }
+
+  List<List<List<PageLoaderElement>>> _getResults() => [
+        for (final configurationGroup in _configurationGroups)
+          [
+            for (final resultGroup in _resultGroups(configurationGroup))
+              _results(resultGroup)
+          ]
+      ];
+
+  List<List<List<PageLoaderElement>>> _cachedResults;
+
+  List<List<List<PageLoaderElement>>> get results =>
+      _cachedResults ??= _getResults();
+
+  List<List<List<String>>> testNames() => [
+        for (final configurationGroup in results)
+          [
+            for (final resultGroup in configurationGroup)
+              [for (final result in resultGroup) result.innerText]
+          ]
+      ];
+}
+
+String checked(PageLoaderElement checkbox) =>
+    checkbox.attributes['aria-checked'];
+
+PageLoaderElement checkbox(PageLoaderElement parent) =>
+    parent.getElementsByCss('material-checkbox').first;
+
+@PageObject()
+abstract class ResultsSelectorPanelPO implements ResultsPanelPO {
+  ResultsSelectorPanelPO();
+
+  factory ResultsSelectorPanelPO.create(PageLoaderElement context) =
+      $ResultsSelectorPanelPO.create;
+
+  @ByCss('results-selector-panel > div')
+  List<PageLoaderElement> get _configurationGroups;
+
+  List<PageLoaderElement> _resultGroups(PageLoaderElement configurationGroup) {
+    return configurationGroup
+        .getElementsByCss('results-selector-panel > div > div.indent');
+  }
+
+  PageLoaderElement configurationGroup(String searchText) {
+    return _configurationGroups.firstWhere(
+        (group) => group.innerText.contains(searchText),
+        orElse: () => null);
+  }
+
+  PageLoaderElement resultsGroup(
+          PageLoaderElement configurationGroup, String resultsText) =>
+      _resultGroups(configurationGroup).firstWhere(
+          (resultGroup) => resultGroup.innerText.contains(resultsText),
+          orElse: () => null);
+
+  List<PageLoaderElement> _results(PageLoaderElement resultGroup) {
+    return resultGroup.getElementsByCss(
+        'results-selector-panel > div > div.indent > span.indent');
+  }
+
+  List<List<List<PageLoaderElement>>> _getResults() => [
+        for (final configurationGroup in _configurationGroups)
+          [
+            for (final resultGroup in _resultGroups(configurationGroup))
+              _results(resultGroup)
+          ]
+      ];
+
+  List<List<List<PageLoaderElement>>> _cachedResults;
+
+  List<List<List<PageLoaderElement>>> get results =>
+      _cachedResults ??= _getResults();
+
+  List<List<List<String>>> testNames() => [
+        for (final configurationGroup in results)
+          [
+            for (final resultGroup in configurationGroup)
+              [for (final result in resultGroup) result.innerText]
+          ]
+      ];
+}
diff --git a/results_feed/test/page_objects/try_results_po.dart b/results_feed/test/page_objects/try_results_po.dart
new file mode 100644
index 0000000..0e74f43
--- /dev/null
+++ b/results_feed/test/page_objects/try_results_po.dart
@@ -0,0 +1,58 @@
+// Copyright (c) 2020, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:pageloader/html.dart';
+
+import 'results_panel_po.dart';
+
+part 'try_results_po.g.dart';
+
+@PageObject()
+abstract class TryResultsPO {
+  TryResultsPO();
+  factory TryResultsPO.create(PageLoaderElement context) = $TryResultsPO.create;
+
+  @First(ByTagName('results-panel'))
+  PageLoaderElement get _resultsPanel;
+
+  @First(ByTagName('results-selector-panel'))
+  PageLoaderElement get _resultsSelectorPanel;
+
+  ResultsPanelPO get resultsPanel => _resultsPanel.exists
+      ? ResultsPanelPO.create(_resultsPanel)
+      : ResultsSelectorPanelPO.create(_resultsSelectorPanel);
+
+  @ByTagName('material-button')
+  List<PageLoaderElement> get _buttons;
+
+  @First(ByCss('material-input textarea'))
+  PageLoaderElement get _commentField;
+
+  PageLoaderElement get commentField => _commentField;
+
+  @ByCss('try-results  div  div.comment')
+  List<PageLoaderElement> get _comments;
+
+  List<PageLoaderElement> get comments => _comments;
+
+  PageLoaderElement _buttonCalled(String text) =>
+      _buttons.firstWhere((final button) => button.innerText == text,
+          orElse: () => null);
+
+  PageLoaderElement get approveCommentButton =>
+      _buttonCalled("Approve/Comment ...");
+  PageLoaderElement get cancelButton => _buttonCalled("Cancel");
+  PageLoaderElement get revokeButton =>
+      _buttonCalled("Revoke Selected Approvals");
+  PageLoaderElement get commentOnlyButton =>
+      _buttonCalled("Comment without Approving");
+  PageLoaderElement get approveButton => _buttonCalled("Approve");
+
+  void clickApproveComment() {
+    approveCommentButton.click();
+    for (final button in _buttons) {
+      print(button.innerText);
+    }
+  }
+}
diff --git a/results_feed/test/tools/fetch_document_as_json.dart b/results_feed/test/tools/fetch_document_as_json.dart
new file mode 100644
index 0000000..697a163
--- /dev/null
+++ b/results_feed/test/tools/fetch_document_as_json.dart
@@ -0,0 +1,55 @@
+// Copyright (c) 2020, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// This is a utility script to grab Firestore documents as JSON objects,
+// used when preparing sample data for our test code.
+
+import 'dart:convert';
+import 'dart:io';
+import 'package:http/http.dart';
+
+const host = 'firestore.googleapis.com';
+const project = 'dart-ci';
+const urlBase =
+    'https://$host/v1/projects/$project/databases/(default)/documents';
+
+void main(List<String> args) async {
+  if (args.isEmpty) {
+    print("Run command with a list of Firestore document references as args.");
+    exit(0);
+  }
+  final client = Client();
+  final documents = {};
+
+  for (String reference in args) {
+    final result = await client.get(Uri.parse("$urlBase/$reference"));
+    final json = jsonDecode(result.body);
+    final fields = json['fields'];
+    documents[reference] = {
+      for (final field in fields.keys) field: toValue(fields[field])
+    };
+  }
+  print(jsonEncode(documents));
+}
+
+dynamic toValue(dynamic valueJson) {
+  final dynamic result = parseFirstNonNullValue(valueJson, {
+    'stringValue': (String x) => x,
+    'integerValue': (String x) => int.parse(x),
+    'booleanValue': (bool x) => x,
+    'arrayValue': (m) => m['values'].map(toValue).toList()
+  });
+  if (result == null) {
+    print("Unknown value type $valueJson");
+    exit(1);
+  }
+  return result;
+}
+
+dynamic parseFirstNonNullValue(
+    Map<String, dynamic> value, Map<String, Function> types) {
+  for (final type in types.keys) {
+    if (value.containsKey(type)) return types[type](value[type]);
+  }
+}
diff --git a/results_feed/test/try_results_sample_data.dart b/results_feed/test/try_results_sample_data.dart
new file mode 100644
index 0000000..5ad512c
--- /dev/null
+++ b/results_feed/test/try_results_sample_data.dart
@@ -0,0 +1,78 @@
+// Copyright (c) 2020, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// Sample database records used by try_results_test.dart
+
+final createComponentReview = 123;
+final createComponentPatchset = 2;
+final commentId1 = 'sampleId00001';
+final commentText = 'test comment approving a test';
+
+final tryResultsCreateComponentSampleData = <String, dynamic>{
+  "reviews/$createComponentReview": {
+    "subject": "Test review for try_results_test 'create component'"
+  },
+  "reviews/$createComponentReview/patchsets/1": {
+    "description": "initial upload",
+    "kind": "REWORK",
+    "number": 1,
+    "patchset_group": 1,
+  },
+  "reviews/$createComponentReview/patchsets/2": {
+    "description": "initial upload",
+    "kind": "REWORK",
+    "number": 2,
+    "patchset_group": 1,
+  },
+  "comments/$commentId1": {
+    "author": "user@example.com",
+    "created": DateTime.parse("2019-11-20 20:18:00Z"),
+    "comment": "Sample comment approving a test",
+    "approved": true,
+    "results": [],
+    "review": 123
+  },
+  "try_results/try_result_1": {
+    "approved": false,
+    "review": 123,
+    "configurations": ["unittest-asserts-release-linux"],
+    "name": "pkg/front_end/test/fasta/analyze_test",
+    "patchset": 2,
+    "result": "Fail",
+    "expected": "Pass",
+    "previous_result": "Pass"
+  },
+  "try_results/try_result_2": {
+    "approved": false,
+    "review": 123,
+    "configurations": ["analyzer-asserts-win", "analyzer-asserts-linux"],
+    "name": "pkg/front_end/test/fasta/analyze_test",
+    "patchset": 2,
+    "result": "CompileTimeError",
+    "expected": "Pass",
+    "previous_result": "Pass"
+  },
+  "try_results/try_result_3": {
+    "approved": false,
+    "review": 123,
+    "configurations": ["analyzer-asserts-win", "analyzer-asserts-linux"],
+    "name": "sample_suite/sample_test",
+    "patchset": 2,
+    "result": "RuntimeError",
+    "expected": "Pass",
+    "previous_result": "Pass"
+  },
+  "try_results/try_result_4": {
+    "approved": true,
+    "review": 123,
+    "configurations": ["unittest-asserts-release-linux"],
+    "name": "sample_suite/second_test",
+    "patchset": 2,
+    "result": "Fail",
+    "expected": "Pass",
+    "previous_result": "Pass"
+  },
+};
+
+final tryResultsCreateComponentSampleDataMerges = <String, dynamic>{};
diff --git a/results_feed/test/try_results_test.dart b/results_feed/test/try_results_test.dart
new file mode 100644
index 0000000..2755582
--- /dev/null
+++ b/results_feed/test/try_results_test.dart
@@ -0,0 +1,147 @@
+// Copyright (c) 2020, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@TestOn('browser')
+import 'package:angular/di.dart';
+import 'package:angular_test/angular_test.dart';
+import 'package:dart_results_feed/src/components/try_results_component.dart';
+import 'package:dart_results_feed/src/components/try_results_component.template.dart'
+    as ng;
+import 'package:dart_results_feed/src/services/firestore_service.dart';
+import 'package:pageloader/html.dart';
+import 'package:pageloader/testing.dart';
+
+import 'package:test/test.dart';
+
+import 'try_results_test.template.dart' as self;
+import 'try_results_sample_data.dart';
+import 'page_objects/results_panel_po.dart';
+import 'page_objects/try_results_po.dart';
+
+// pub run build_runner test --fail-on-severe -- -p chrome comments_test.dart
+
+@GenerateInjector([
+  ClassProvider(FirestoreService, useClass: TestingFirestoreService),
+])
+final InjectorFactory rootInjector = self.rootInjector$Injector;
+
+void main() {
+  final testBed = NgTestBed.forComponent<TryResultsComponent>(
+      ng.TryResultsComponentNgFactory,
+      rootInjector: rootInjector);
+
+  tearDown(() async {
+    await disposeAnyRunningTest();
+  });
+
+  test('create component', () async {
+    TestingFirestoreService firestore;
+    final fixture =
+        await testBed.create(beforeComponentCreated: (Injector injector) async {
+      firestore =
+          injector.provideType<TestingFirestoreService>(FirestoreService);
+      await firestore.getFirebaseClient();
+      await firestore.writeDocumentsFrom(tryResultsCreateComponentSampleData);
+      await firestore
+          .mergeDocumentsFrom(tryResultsCreateComponentSampleDataMerges);
+    });
+    await fixture.update((TryResultsComponent tryResultsComponent) {
+      tryResultsComponent.review = createComponentReview;
+      tryResultsComponent.patchset = createComponentPatchset;
+    });
+    await fixture.assertOnlyInstance.update();
+    await fixture.update();
+
+    var context = HtmlPageLoaderElement.createFromElement(fixture.rootElement);
+    var tryResultsPO = TryResultsPO.create(context);
+    var results = tryResultsPO.resultsPanel;
+    expect(results, isA<ResultsPanelPO>());
+    expect(results, isNot(isA<ResultsSelectorPanelPO>()));
+
+    expect(results.testNames(), [
+      [
+        ['pkg/front_end/test/fasta/analyze_test'],
+        ['sample_suite/sample_test']
+      ],
+      [
+        ['pkg/front_end/test/fasta/analyze_test', '✔ sample_suite/second_test']
+      ]
+    ]);
+    expect(tryResultsPO.approveCommentButton, exists);
+    expect(tryResultsPO.cancelButton, isNull);
+    expect(tryResultsPO.revokeButton, isNull);
+    expect(tryResultsPO.commentOnlyButton, isNull);
+    expect(tryResultsPO.approveButton, isNull);
+
+    await fixture.update((_) => tryResultsPO.approveCommentButton.click());
+    // The waiting in fixture.update is not enough when we replace
+    // the ResultsPanel with a ResultsSelectorPanel.
+    await Future.delayed(Duration(seconds: 1), () => null);
+
+    results = tryResultsPO.resultsPanel;
+    expect(results, isA<ResultsSelectorPanelPO>());
+    expect(results.testNames(), [
+      [
+        ['check_box pkg/front_end/test/fasta/analyze_test'],
+        ['check_box sample_suite/sample_test']
+      ],
+      [
+        [
+          'check_box pkg/front_end/test/fasta/analyze_test',
+          'check_box ✔ sample_suite/second_test'
+        ]
+      ]
+    ]);
+    expect(tryResultsPO.approveCommentButton, isNull);
+    expect(tryResultsPO.cancelButton, exists);
+    expect(tryResultsPO.revokeButton, exists);
+    expect(tryResultsPO.commentOnlyButton, exists);
+    expect(tryResultsPO.approveButton, exists);
+
+    final analyzerGroup = results.configurationGroup('analyzer...');
+    final analyzerGroupCheckbox = checkbox(analyzerGroup);
+    final secondResultGroup = results.resultsGroup(
+        analyzerGroup, 'Pass -> CompileTimeError (expected Pass)');
+    final secondResultGroupCheckbox = checkbox(secondResultGroup);
+    expect(analyzerGroupCheckbox, exists);
+    expect(secondResultGroupCheckbox, exists);
+    expect(checked(analyzerGroupCheckbox), 'true');
+    expect(checked(secondResultGroupCheckbox), 'true');
+    await fixture.update((_) => analyzerGroupCheckbox.click());
+    expect(checked(analyzerGroupCheckbox), 'false');
+    expect(checked(secondResultGroupCheckbox), 'false');
+    await fixture.update((_) => secondResultGroupCheckbox.click());
+    expect(checked(analyzerGroupCheckbox), 'mixed');
+    expect(checked(secondResultGroupCheckbox), 'true');
+
+    await fixture.update((_) => tryResultsPO.commentField.type(commentText));
+
+    await fixture.update((_) => tryResultsPO.approveButton.click());
+    await Future.delayed(Duration(seconds: 1), () => null);
+    // The waiting in fixture.update is not enough when we replace
+    // the ResultsSelectorPanel with a ResultsPanel.
+    results = tryResultsPO.resultsPanel;
+    expect(results, isA<ResultsPanelPO>());
+    expect(results, isNot(isA<ResultsSelectorPanelPO>()));
+
+    expect(results.testNames(), [
+      [
+        ['✔ pkg/front_end/test/fasta/analyze_test'],
+        ['sample_suite/sample_test']
+      ],
+      [
+        [
+          '✔ pkg/front_end/test/fasta/analyze_test',
+          '✔ sample_suite/second_test'
+        ]
+      ]
+    ]);
+    expect(tryResultsPO.comments.last.innerText,
+        stringContainsInOrder(['approved', commentText]));
+
+    await firestore.writeDocumentsFrom(tryResultsCreateComponentSampleData,
+        delete: true);
+    await firestore.deleteCommentsForReview(createComponentReview);
+  });
+}
diff --git a/results_feed/test/try_results_test.html b/results_feed/test/try_results_test.html
new file mode 100644
index 0000000..f48ffa4
--- /dev/null
+++ b/results_feed/test/try_results_test.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<!-- comments_test.html -->
+<html>
+  <head>
+    <title>Try Results test</title>
+     <script src="https://www.gstatic.com/firebasejs/5.9.2/firebase-app.js"></script>
+      <script src="https://www.gstatic.com/firebasejs/5.9.2/firebase-auth.js"></script>
+      <script src="https://www.gstatic.com/firebasejs/5.9.2/firebase-firestore.js"></script>
+      <link rel="x-dart-test" href="try_results_test.dart">
+    <script src="packages/test/dart.js"></script>
+  </head>
+  <body>
+    // ...
+  </body>
+</html>
diff --git a/results_feed/web/main.dart b/results_feed/web/main.dart
index bceb6d7..9329437 100644
--- a/results_feed/web/main.dart
+++ b/results_feed/web/main.dart
@@ -10,7 +10,7 @@
 import 'main.template.dart' as self;
 
 // Local testing use
-/// @GenerateInjector([ClassProvider(FirestoreService, useClass: TestingFirestoreService), ...routerProvidersHash])
+// @GenerateInjector([ClassProvider(FirestoreService, useClass: TestingFirestoreService), ...routerProvidersHash])
 // Use for deploying on staging website:
 // @GenerateInjector([ClassProvider(FirestoreService, useClass: StagingFirestoreService), ...routerProviders])