[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])