| // Copyright 2019 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:async'; |
| |
| import 'package:flutter/material.dart'; |
| import 'package:flutter_app_icons/flutter_app_icons_platform_interface.dart'; |
| import 'package:flutter_dashboard/build_dashboard_page.dart'; |
| import 'package:flutter_dashboard/model/branch.pb.dart'; |
| import 'package:flutter_dashboard/model/build_status_response.pb.dart'; |
| import 'package:flutter_dashboard/model/commit_status.pb.dart'; |
| import 'package:flutter_dashboard/service/cocoon.dart'; |
| import 'package:flutter_dashboard/service/dev_cocoon.dart'; |
| import 'package:flutter_dashboard/service/google_authentication.dart'; |
| import 'package:flutter_dashboard/state/build.dart'; |
| import 'package:flutter_dashboard/widgets/error_brook_watcher.dart'; |
| import 'package:flutter_dashboard/widgets/sign_in_button.dart'; |
| import 'package:flutter_dashboard/widgets/state_provider.dart'; |
| import 'package:flutter_dashboard/widgets/task_box.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| import 'package:mockito/mockito.dart'; |
| |
| import 'utils/fake_build.dart'; |
| import 'utils/fake_flutter_app_icons.dart'; |
| import 'utils/fake_google_account.dart'; |
| import 'utils/golden.dart'; |
| import 'utils/mocks.dart'; |
| import 'utils/output.dart'; |
| import 'utils/task_icons.dart'; |
| |
| void main() { |
| final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized(); |
| late MockGoogleSignInService fakeAuthService; |
| |
| final Type dropdownButtonType = DropdownButton<String>( |
| onChanged: (_) {}, |
| items: const <DropdownMenuItem<String>>[], |
| ).runtimeType; |
| |
| setUp(() { |
| binding.window.devicePixelRatioTestValue = 1.0; |
| binding.window.physicalSizeTestValue = const Size(1080, 2280); |
| // device pixel ratio of 1.0 works well on web app and emulator |
| // If not set, flutter test uses a Pixel 4 device pixel ratio of roughly 2.75, which doesn't quite work |
| // I am using the default settings of Pixel 4 in this test, as referenced in the link below |
| // https://android.googlesource.com/platform/external/qemu/+/b5b78438ae9ff3b90aafdab0f4f25585affc22fb/android/avd/hardware-properties.ini |
| fakeAuthService = MockGoogleSignInService(); |
| when(fakeAuthService.isAuthenticated).thenAnswer((_) => Future<bool>.value(true)); |
| when(fakeAuthService.user).thenReturn(FakeGoogleSignInAccount()); |
| |
| FlutterAppIconsPlatform.instance = FakeFlutterAppIcons(); |
| }); |
| |
| testWidgets('shows sign in button', (WidgetTester tester) async { |
| final MockCocoonService fakeCocoonService = MockCocoonService(); |
| throwOnMissingStub(fakeCocoonService); |
| when(fakeCocoonService.fetchFlutterBranches()).thenAnswer((_) => Completer<CocoonResponse<List<Branch>>>().future); |
| when(fakeCocoonService.fetchRepos()).thenAnswer((_) => Completer<CocoonResponse<List<String>>>().future); |
| when( |
| fakeCocoonService.fetchCommitStatuses( |
| branch: anyNamed('branch'), |
| repo: anyNamed('repo'), |
| ), |
| ).thenAnswer((_) => Completer<CocoonResponse<List<CommitStatus>>>().future); |
| when( |
| fakeCocoonService.fetchTreeBuildStatus( |
| branch: anyNamed('branch'), |
| repo: anyNamed('repo'), |
| ), |
| ).thenAnswer((_) => Completer<CocoonResponse<BuildStatusResponse>>().future); |
| |
| final BuildState buildState = BuildState( |
| cocoonService: fakeCocoonService, |
| authService: fakeAuthService, |
| ); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: ValueProvider<BuildState>( |
| value: buildState, |
| child: ValueProvider<GoogleSignInService>( |
| value: buildState.authService, |
| child: const BuildDashboardPage(), |
| ), |
| ), |
| ), |
| ); |
| expect(find.byType(SignInButton), findsOneWidget); |
| |
| await tester.pumpWidget(Container()); |
| buildState.dispose(); |
| }); |
| |
| testWidgets('shows settings button', (WidgetTester tester) async { |
| final BuildState fakeBuildState = FakeBuildState()..authService = fakeAuthService; |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: ValueProvider<BuildState>( |
| value: fakeBuildState, |
| child: ValueProvider<GoogleSignInService>( |
| value: fakeBuildState.authService, |
| child: const BuildDashboardPage(), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(find.byIcon(Icons.settings), findsOneWidget); |
| }); |
| |
| testWidgets('shows file a bug button', (WidgetTester tester) async { |
| final BuildState fakeBuildState = FakeBuildState()..authService = fakeAuthService; |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: ValueProvider<BuildState>( |
| value: fakeBuildState, |
| child: ValueProvider<GoogleSignInService>( |
| value: fakeBuildState.authService, |
| child: const BuildDashboardPage(), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(find.byIcon(Icons.bug_report), findsOneWidget); |
| }); |
| |
| testWidgets('shows key button & legend', (WidgetTester tester) async { |
| final BuildState fakeBuildState = FakeBuildState()..authService = fakeAuthService; |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: ValueProvider<BuildState>( |
| value: fakeBuildState, |
| child: ValueProvider<GoogleSignInService>( |
| value: fakeBuildState.authService, |
| child: const BuildDashboardPage(), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(find.byIcon(Icons.info_outline), findsOneWidget); |
| |
| await tester.tap(find.byIcon(Icons.info_outline)); |
| await tester.pump(); |
| |
| for (final String status in TaskBox.statusColor.keys) { |
| expect(find.text(status), findsOneWidget); |
| } |
| expect(find.text('Flaky'), findsOneWidget); |
| expect(find.text('Ran more than once'), findsOneWidget); |
| }); |
| |
| testWidgets('shows branch and repo dropdown button when screen is decently large', (WidgetTester tester) async { |
| final BuildState fakeBuildState = FakeBuildState()..authService = fakeAuthService; |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: ValueProvider<BuildState>( |
| value: fakeBuildState, |
| child: ValueProvider<GoogleSignInService>( |
| value: fakeBuildState.authService, |
| child: const BuildDashboardPage(), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(find.byType(dropdownButtonType), findsNWidgets(2)); |
| |
| expect(find.text('repo: '), findsOneWidget); |
| expect((tester.widget(find.byKey(const Key('repo dropdown'))) as DropdownButton).value, equals('flutter')); |
| |
| expect(find.text('branch: '), findsOneWidget); |
| expect((tester.widget(find.byKey(const Key('branch dropdown'))) as DropdownButton).value, equals('master')); |
| }); |
| |
| testWidgets('shows vacuum github commits button', (WidgetTester tester) async { |
| final BuildState fakeBuildState = FakeBuildState()..authService = fakeAuthService; |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: ValueProvider<BuildState>( |
| value: fakeBuildState, |
| child: ValueProvider<GoogleSignInService>( |
| value: fakeBuildState.authService, |
| child: const BuildDashboardPage(), |
| ), |
| ), |
| ), |
| ); |
| |
| // Open settings overlay |
| await tester.tap(find.byIcon(Icons.settings)); |
| await tester.pump(); |
| await tester.pump(const Duration(seconds: 1)); // Finish the menu animation. |
| |
| expect(find.text('Vacuum GitHub Commits'), findsOneWidget); |
| }); |
| |
| testWidgets('shows loading when fetch tree status is null', (WidgetTester tester) async { |
| final BuildState fakeBuildState = FakeBuildState() |
| ..isTreeBuilding = null |
| ..authService = fakeAuthService; |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: ValueProvider<BuildState>( |
| value: fakeBuildState, |
| child: ValueProvider<GoogleSignInService>( |
| value: fakeBuildState.authService, |
| child: const BuildDashboardPage(), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(find.text('Loading...'), findsOneWidget); |
| |
| final AppBar appbarWidget = find.byType(AppBar).evaluate().first.widget as AppBar; |
| expect(appbarWidget.backgroundColor, Colors.grey[850]); |
| expect(tester, meetsGuideline(textContrastGuideline)); |
| }); |
| |
| testWidgets('shows loading when fetch tree status is null, dark mode', (WidgetTester tester) async { |
| final BuildState fakeBuildState = FakeBuildState() |
| ..isTreeBuilding = null |
| ..authService = fakeAuthService; |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: ThemeData.dark(), |
| home: ValueProvider<BuildState>( |
| value: fakeBuildState, |
| child: ValueProvider<GoogleSignInService>( |
| value: fakeBuildState.authService, |
| child: const BuildDashboardPage(), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(find.text('Loading...'), findsOneWidget); |
| |
| final AppBar appbarWidget = find.byType(AppBar).evaluate().first.widget as AppBar; |
| expect(appbarWidget.backgroundColor, Colors.grey[850]); |
| expect(tester, meetsGuideline(textContrastGuideline)); |
| }); |
| |
| testWidgets('shows tree closed when fetch tree status is false', (WidgetTester tester) async { |
| final BuildState fakeBuildState = FakeBuildState() |
| ..isTreeBuilding = false |
| ..authService = fakeAuthService; |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: ValueProvider<BuildState>( |
| value: fakeBuildState, |
| child: ValueProvider<GoogleSignInService>( |
| value: fakeBuildState.authService, |
| child: const BuildDashboardPage(), |
| ), |
| ), |
| ), |
| ); |
| |
| // Verify the "Tree is Closed" message is wrapped in a [Tooltip]. |
| final Finder tooltipFinder = find.byWidgetPredicate((Widget widget) { |
| return widget is Tooltip && (widget.message?.contains('Tree is Closed') ?? false); |
| }); |
| expect(tooltipFinder, findsOneWidget); |
| |
| final AppBar appbarWidget = find.byType(AppBar).evaluate().first.widget as AppBar; |
| expect(appbarWidget.backgroundColor, Colors.red); |
| expect(tester, meetsGuideline(textContrastGuideline)); |
| }); |
| |
| testWidgets('shows tree closed when fetch tree status is false, dark mode', (WidgetTester tester) async { |
| final BuildState fakeBuildState = FakeBuildState() |
| ..isTreeBuilding = false |
| ..authService = fakeAuthService; |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: ThemeData.dark(), |
| home: ValueProvider<BuildState>( |
| value: fakeBuildState, |
| child: ValueProvider<GoogleSignInService>( |
| value: fakeBuildState.authService, |
| child: const BuildDashboardPage(), |
| ), |
| ), |
| ), |
| ); |
| |
| // Verify the "Tree is Closed" message is wrapped in a [Tooltip]. |
| final Finder tooltipFinder = find.byWidgetPredicate((Widget widget) { |
| return widget is Tooltip && (widget.message?.contains('Tree is Closed') ?? false); |
| }); |
| expect(tooltipFinder, findsOneWidget); |
| |
| final AppBar appbarWidget = find.byType(AppBar).evaluate().first.widget as AppBar; |
| expect(appbarWidget.backgroundColor, Colors.red[800]); |
| expect(tester, meetsGuideline(textContrastGuideline)); |
| }); |
| |
| testWidgets('shows tree open when fetch tree status is true', (WidgetTester tester) async { |
| final BuildState fakeBuildState = FakeBuildState() |
| ..isTreeBuilding = true |
| ..authService = fakeAuthService; |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: ValueProvider<BuildState>( |
| value: fakeBuildState, |
| child: ValueProvider<GoogleSignInService>( |
| value: fakeBuildState.authService, |
| child: const BuildDashboardPage(), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(find.text('Tree is Open'), findsOneWidget); |
| |
| final AppBar appbarWidget = find.byType(AppBar).evaluate().first.widget as AppBar; |
| expect(appbarWidget.backgroundColor, Colors.green); |
| expect(tester, meetsGuideline(textContrastGuideline)); |
| }); |
| |
| testWidgets('shows tree open when fetch tree status is true, dark mode', (WidgetTester tester) async { |
| final BuildState fakeBuildState = FakeBuildState() |
| ..isTreeBuilding = true |
| ..authService = fakeAuthService; |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: ThemeData.dark(), |
| home: ValueProvider<BuildState>( |
| value: fakeBuildState, |
| child: ValueProvider<GoogleSignInService>( |
| value: fakeBuildState.authService, |
| child: const BuildDashboardPage(), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(find.text('Tree is Open'), findsOneWidget); |
| |
| final AppBar appbarWidget = find.byType(AppBar).evaluate().first.widget as AppBar; |
| expect(appbarWidget.backgroundColor, Colors.green[800]); |
| expect(tester, meetsGuideline(textContrastGuideline)); |
| }); |
| |
| testWidgets('show error snackbar when error occurs', (WidgetTester tester) async { |
| String? lastError; |
| final FakeBuildState buildState = FakeBuildState() |
| ..errors.addListener((String message) => lastError = message) |
| ..authService = fakeAuthService; |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: ValueProvider<BuildState>( |
| value: buildState, |
| child: ValueProvider<GoogleSignInService>( |
| value: buildState.authService, |
| child: const BuildDashboardPage(), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(lastError, isNull); |
| |
| // propagate the error message |
| await checkOutput( |
| block: () async { |
| buildState.errors.send('ERROR'); |
| }, |
| output: <String>[ |
| 'ERROR', |
| ], |
| ); |
| await tester.pump(); |
| |
| await tester.pump(const Duration(milliseconds: 750)); // open animation for snackbar |
| |
| expect(find.text(lastError!), findsOneWidget); |
| |
| // Snackbar message should go away after its duration |
| await tester.pump(ErrorBrookWatcher.errorSnackbarDuration); // wait the duration |
| await tester.pump(); // schedule animation |
| await tester.pump(const Duration(milliseconds: 1500)); // close animation |
| |
| expect(find.text(lastError!), findsNothing); |
| }); |
| |
| testWidgets('TaskGridContainer with default Settings property sheet', (WidgetTester tester) async { |
| await precacheTaskIcons(tester); |
| final BuildState buildState = BuildState( |
| cocoonService: DevelopmentCocoonService(DateTime.utc(2020)), |
| authService: fakeAuthService, |
| ); |
| void listener1() {} |
| buildState.addListener(listener1); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: ValueProvider<BuildState>( |
| value: buildState, |
| child: ValueProvider<GoogleSignInService>( |
| value: buildState.authService, |
| child: const BuildDashboardPage(), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.tap(find.byIcon(Icons.settings)); |
| await tester.pump(); |
| |
| await expectGoldenMatches(find.byType(BuildDashboardPage), 'build_dashboard.defaultPropertySheet.png'); |
| expect(tester, meetsGuideline(textContrastGuideline)); |
| |
| await tester.pumpWidget(Container()); |
| buildState.dispose(); |
| }); |
| |
| testWidgets('TaskGridContainer with default Settings property sheet, dark mode', (WidgetTester tester) async { |
| await precacheTaskIcons(tester); |
| final BuildState buildState = BuildState( |
| cocoonService: DevelopmentCocoonService(DateTime.utc(2020)), |
| authService: fakeAuthService, |
| ); |
| void listener1() {} |
| buildState.addListener(listener1); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: ThemeData.dark(), |
| home: ValueProvider<BuildState>( |
| value: buildState, |
| child: ValueProvider<GoogleSignInService>( |
| value: buildState.authService, |
| child: const BuildDashboardPage(), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.tap(find.byIcon(Icons.settings)); |
| await tester.pump(); |
| |
| await expectGoldenMatches(find.byType(BuildDashboardPage), 'build_dashboard.defaultPropertySheet.dark.png'); |
| expect(tester, meetsGuideline(textContrastGuideline)); |
| |
| await tester.pumpWidget(Container()); |
| buildState.dispose(); |
| }); |
| |
| testWidgets('ensure smooth transition between invalid states', (WidgetTester tester) async { |
| final BuildState fakeBuildState = FakeBuildState()..authService = fakeAuthService; |
| BuildDashboardPage controlledBuildDashboardPage = const BuildDashboardPage( |
| queryParameters: { |
| 'repo': 'flutter', |
| 'branch': 'flutter-release', |
| }, |
| ); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: ValueProvider<BuildState>( |
| value: fakeBuildState, |
| child: ValueProvider<GoogleSignInService>( |
| value: fakeBuildState.authService, |
| child: controlledBuildDashboardPage, |
| ), |
| ), |
| ), |
| ); |
| |
| expect(find.byType(dropdownButtonType), findsNWidgets(2)); |
| // simulate a url request, which retriggers a rebuild of the widget |
| controlledBuildDashboardPage = const BuildDashboardPage( |
| queryParameters: { |
| 'repo': 'engine', |
| }, |
| ); |
| expect( |
| (tester.widget(find.byKey(const Key('branch dropdown'))) as DropdownButton).value, |
| equals('flutter-release'), |
| ); //invalid state: engine + flutter-release |
| await tester.pump(); //an invalid state will generate delayed network responses |
| |
| //if a delayed network request come in, from a previous invalid state: cocoon + engine - release, no exceptions should be raised |
| controlledBuildDashboardPage = const BuildDashboardPage( |
| queryParameters: { |
| 'repo': 'cocoon', |
| 'branch': 'engine-release', |
| }, |
| ); |
| }); |
| |
| testWidgets('shows branch and repo dropdown button in settings when screen is small', (WidgetTester tester) async { |
| binding.window.physicalSizeTestValue = const Size(500, 500); |
| binding.window.devicePixelRatioTestValue = 1.0; |
| final BuildState fakeBuildState = FakeBuildState()..authService = fakeAuthService; |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: ValueProvider<BuildState>( |
| value: fakeBuildState, |
| child: ValueProvider<GoogleSignInService>( |
| value: fakeBuildState.authService, |
| child: const BuildDashboardPage(), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(find.byType(dropdownButtonType), findsNothing); |
| |
| await tester.tap(find.byIcon(Icons.settings)); |
| await tester.pump(); |
| |
| expect(find.byType(dropdownButtonType), findsNWidgets(2)); |
| }); |
| } |