| // 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 'package:cocoon_common/rpc_model.dart'; | |
| import 'package:cocoon_common/task_status.dart'; | |
| import 'package:flutter/material.dart'; | |
| import 'package:flutter_dashboard/logic/task_grid_filter.dart'; | |
| import 'package:flutter_dashboard/state/build.dart'; | |
| import 'package:flutter_dashboard/widgets/error_brook_watcher.dart'; | |
| import 'package:flutter_dashboard/widgets/luci_task_attempt_summary.dart'; | |
| import 'package:flutter_dashboard/widgets/now.dart'; | |
| import 'package:flutter_dashboard/widgets/progress_button.dart'; | |
| import 'package:flutter_dashboard/widgets/state_provider.dart'; | |
| import 'package:flutter_dashboard/widgets/task_box.dart'; | |
| import 'package:flutter_dashboard/widgets/task_grid.dart'; | |
| import 'package:flutter_dashboard/widgets/task_overlay.dart'; | |
| import 'package:flutter_test/flutter_test.dart'; | |
| import 'package:mockito/mockito.dart'; | |
| import '../utils/fake_build.dart'; | |
| import '../utils/generate_commit_for_tests.dart'; | |
| import '../utils/generate_task_for_tests.dart'; | |
| import '../utils/golden.dart'; | |
| import '../utils/task_icons.dart'; | |
| const double _cellSize = 36; | |
| void main() { | |
| late FakeBuildState buildState; | |
| setUp(() { | |
| buildState = FakeBuildState(); | |
| when(buildState.authService.isAuthenticated).thenReturn(true); | |
| }); | |
| testWidgets('TaskOverlay shows on click', (WidgetTester tester) async { | |
| await precacheTaskIcons(tester); | |
| final expectedTask = generateTaskForTest( | |
| status: TaskStatus.failed, | |
| attempts: 3, | |
| ); | |
| final expectedTaskInfoString = | |
| 'Attempts: 3\n' | |
| 'Queued for 2 minutes\n' | |
| 'Ran for 48 minutes'; | |
| await tester.pumpWidget( | |
| Now.fixed( | |
| dateTime: utc$2020_9_1_12_30, | |
| child: TaskBox( | |
| cellSize: _cellSize, | |
| child: MaterialApp( | |
| theme: ThemeData(useMaterial3: false), | |
| home: Scaffold( | |
| body: _TestGrid(buildState: buildState, task: expectedTask), | |
| ), | |
| ), | |
| ), | |
| ), | |
| ); | |
| await tester.pump(); | |
| expect(find.text(expectedTask.builderName), findsNothing); | |
| expect(find.text(expectedTaskInfoString), findsNothing); | |
| await expectGoldenMatches( | |
| find.byType(MaterialApp), | |
| 'task_overlay_test.normal_overlay_closed.png', | |
| ); | |
| await tester.tapAt(const Offset(_cellSize * 1.5, _cellSize * 1.5)); | |
| await tester.pump(); | |
| expect(find.text(expectedTask.builderName), findsOneWidget); | |
| expect(find.text(expectedTaskInfoString), findsOneWidget); | |
| await expectGoldenMatches( | |
| find.byType(MaterialApp), | |
| 'task_overlay_test.normal_overlay_open.png', | |
| ); | |
| // Since the overlay positions itself below the middle of the widget, | |
| // it is safe to click the widget to close it again. | |
| await tester.tapAt(const Offset(_cellSize * 1.5, _cellSize * 1.5)); | |
| await tester.pump(); | |
| expect(find.text(expectedTask.builderName), findsNothing); | |
| expect(find.text(expectedTaskInfoString), findsNothing); | |
| await expectGoldenMatches( | |
| find.byType(MaterialApp), | |
| 'task_overlay_test.normal_overlay_closed.png', | |
| ); | |
| }); | |
| testWidgets('TaskOverlay shows when flaky is true', ( | |
| WidgetTester tester, | |
| ) async { | |
| await precacheTaskIcons(tester); | |
| final flakyTask = generateTaskForTest( | |
| status: TaskStatus.failed, | |
| attempts: 3, | |
| bringup: true, | |
| ); | |
| final flakyTaskInfoString = | |
| 'Attempts: 3\n' | |
| 'Queued for 2 minutes\n' | |
| 'Ran for 48 minutes\n' | |
| 'Bringup'; | |
| await tester.pumpWidget( | |
| Now.fixed( | |
| dateTime: utc$2020_9_1_12_30, | |
| child: TaskBox( | |
| cellSize: _cellSize, | |
| child: MaterialApp( | |
| theme: ThemeData(useMaterial3: false), | |
| home: Scaffold( | |
| body: _TestGrid( | |
| buildState: buildState, | |
| task: flakyTask, | |
| // Otherwise the task is not rendered at all. | |
| filter: TaskGridFilter()..showBringup = true, | |
| ), | |
| ), | |
| ), | |
| ), | |
| ), | |
| ); | |
| await tester.pump(); | |
| expect(find.text(flakyTask.builderName), findsNothing); | |
| expect(find.text(flakyTaskInfoString), findsNothing); | |
| await expectGoldenMatches( | |
| find.byType(MaterialApp), | |
| 'task_overlay_test.flaky_overlay_closed.png', | |
| ); | |
| await tester.tapAt(const Offset(_cellSize * 1.5, _cellSize * 1.5)); | |
| await tester.pump(); | |
| expect(find.text(flakyTask.builderName), findsOneWidget); | |
| expect(find.text(flakyTaskInfoString), findsOneWidget); | |
| await expectGoldenMatches( | |
| find.byType(MaterialApp), | |
| 'task_overlay_test.flaky_overlay_open.png', | |
| ); | |
| }); | |
| testWidgets('TaskOverlay computes durations correctly for completed task', ( | |
| WidgetTester tester, | |
| ) async { | |
| /// Create a queue time of 2 minutes, run time of 8 minutes | |
| final createTime = utc$2020_9_1_12_30.subtract(const Duration(minutes: 11)); | |
| final startTime = utc$2020_9_1_12_30.subtract(const Duration(minutes: 9)); | |
| final finishTime = utc$2020_9_1_12_30.subtract(const Duration(minutes: 1)); | |
| final timeTask = generateTaskForTest( | |
| status: TaskStatus.succeeded, | |
| createTime: createTime, | |
| startTime: startTime, | |
| finishTime: finishTime, | |
| ); | |
| final timeTaskInfoString = | |
| 'Attempts: 1\n' | |
| 'Queued for 2 minutes\n' | |
| 'Ran for 8 minutes'; | |
| await tester.pumpWidget( | |
| Now.fixed( | |
| dateTime: utc$2020_9_1_12_30, | |
| child: TaskBox( | |
| cellSize: _cellSize, | |
| child: MaterialApp( | |
| home: Scaffold( | |
| body: _TestGrid(buildState: buildState, task: timeTask), | |
| ), | |
| ), | |
| ), | |
| ), | |
| ); | |
| expect(find.text(timeTaskInfoString), findsNothing); | |
| // open the overlay to show the task summary | |
| await tester.tapAt(const Offset(_cellSize * 1.5, _cellSize * 1.5)); | |
| await tester.pump(); | |
| expect(find.text(timeTaskInfoString), findsOneWidget); | |
| }); | |
| testWidgets('TaskOverlay computes durations correctly for running task', ( | |
| WidgetTester tester, | |
| ) async { | |
| /// Create a queue time of 2 minutes, running time of 9 minutes | |
| final createTime = utc$2020_9_1_12_30.subtract(const Duration(minutes: 11)); | |
| final startTime = utc$2020_9_1_12_30.subtract(const Duration(minutes: 9)); | |
| final timeTask = generateTaskForTest( | |
| status: TaskStatus.inProgress, | |
| createTime: createTime, | |
| startTime: startTime, | |
| ); | |
| final timeTaskInfoString = | |
| 'Attempts: 1\n' | |
| 'Queued for 2 minutes\n' | |
| 'Running for 9 minutes'; | |
| await tester.pumpWidget( | |
| Now.fixed( | |
| dateTime: utc$2020_9_1_12_30, | |
| child: TaskBox( | |
| cellSize: _cellSize, | |
| child: MaterialApp( | |
| home: Scaffold( | |
| body: _TestGrid(buildState: buildState, task: timeTask), | |
| ), | |
| ), | |
| ), | |
| ), | |
| ); | |
| expect(find.text(timeTaskInfoString), findsNothing); | |
| // open the overlay to show the task summary | |
| await tester.tapAt(const Offset(_cellSize * 1.5, _cellSize * 1.5)); | |
| await tester.pump(); | |
| expect(find.text(timeTaskInfoString), findsOneWidget); | |
| }); | |
| testWidgets('TaskOverlay computes durations correctly for waiting task', ( | |
| WidgetTester tester, | |
| ) async { | |
| /// Create a queue time of 2 minutes | |
| final createTime = utc$2020_9_1_12_30.subtract(const Duration(minutes: 2)); | |
| final timeTask = generateTaskForTest( | |
| status: TaskStatus.waitingForBackfill, | |
| createTime: createTime, | |
| ); | |
| final timeTaskInfoString = | |
| 'Attempts: 1\n' | |
| 'Waiting for backfill for 2 minutes\n'; | |
| await tester.pumpWidget( | |
| Now.fixed( | |
| dateTime: utc$2020_9_1_12_30, | |
| child: TaskBox( | |
| cellSize: _cellSize, | |
| child: MaterialApp( | |
| home: Scaffold( | |
| body: _TestGrid(buildState: buildState, task: timeTask), | |
| ), | |
| ), | |
| ), | |
| ), | |
| ); | |
| expect(find.text(timeTaskInfoString), findsNothing); | |
| // open the overlay to show the task summary | |
| await tester.tapAt(const Offset(_cellSize * 1.5, _cellSize * 1.5)); | |
| await tester.pump(); | |
| expect(find.text(timeTaskInfoString), findsOneWidget); | |
| }); | |
| testWidgets('TaskOverlay computes durations correctly for queuing task', ( | |
| WidgetTester tester, | |
| ) async { | |
| /// Create a queue time of 2 minutes | |
| final createTime = utc$2020_9_1_12_30.subtract(const Duration(minutes: 11)); | |
| final timeTask = generateTaskForTest( | |
| status: TaskStatus.inProgress, | |
| createTime: createTime, | |
| buildNumberList: [], | |
| ); | |
| final timeTaskInfoString = | |
| 'Attempts: 1\n' | |
| 'Queuing for 11 minutes\n'; | |
| await tester.pumpWidget( | |
| Now.fixed( | |
| dateTime: utc$2020_9_1_12_30, | |
| child: TaskBox( | |
| cellSize: _cellSize, | |
| child: MaterialApp( | |
| home: Scaffold( | |
| body: _TestGrid(buildState: buildState, task: timeTask), | |
| ), | |
| ), | |
| ), | |
| ), | |
| ); | |
| expect(find.text(timeTaskInfoString), findsNothing); | |
| // open the overlay to show the task summary | |
| await tester.tapAt(const Offset(_cellSize * 1.5, _cellSize * 1.5)); | |
| await tester.pump(); | |
| expect(find.text(timeTaskInfoString), findsOneWidget); | |
| }); | |
| testWidgets('TaskOverlay shows the right message for nondevicelab tasks', ( | |
| WidgetTester tester, | |
| ) async { | |
| await precacheTaskIcons(tester); | |
| await tester.pumpWidget( | |
| Now.fixed( | |
| dateTime: utc$2020_9_1_12_30, | |
| child: TaskBox( | |
| cellSize: _cellSize, | |
| child: MaterialApp( | |
| theme: ThemeData(useMaterial3: false), | |
| home: Scaffold( | |
| body: _TestGrid( | |
| buildState: buildState, | |
| task: generateTaskForTest(status: TaskStatus.succeeded), | |
| ), | |
| ), | |
| ), | |
| ), | |
| ), | |
| ); | |
| await tester.pump(); | |
| await expectGoldenMatches( | |
| find.byType(MaterialApp), | |
| 'task_overlay_test.nondevicelab_closed.png', | |
| ); | |
| await tester.tapAt(const Offset(_cellSize * 1.5, _cellSize * 1.5)); | |
| await tester.pump(); | |
| await expectGoldenMatches( | |
| find.byType(MaterialApp), | |
| 'task_overlay_test.nondevicelab_open.png', | |
| ); | |
| }); | |
| testWidgets('TaskOverlay shows TaskAttemptSummary for Luci tasks', ( | |
| WidgetTester tester, | |
| ) async { | |
| await tester.pumpWidget( | |
| Now.fixed( | |
| dateTime: utc$2020_9_1_12_30, | |
| child: TaskBox( | |
| cellSize: _cellSize, | |
| child: MaterialApp( | |
| home: Scaffold( | |
| body: _TestGrid( | |
| buildState: buildState, | |
| task: generateTaskForTest( | |
| status: TaskStatus.succeeded, | |
| buildNumberList: [123], | |
| ), | |
| ), | |
| ), | |
| ), | |
| ), | |
| ), | |
| ); | |
| expect(find.byType(LuciTaskAttemptSummary), findsNothing); | |
| await tester.tapAt(const Offset(_cellSize * 1.5, _cellSize * 1.5)); | |
| await tester.pump(); | |
| expect(find.byType(LuciTaskAttemptSummary), findsOneWidget); | |
| }); | |
| testWidgets('TaskOverlay shows TaskAttemptSummary for dart-internal tasks', ( | |
| WidgetTester tester, | |
| ) async { | |
| await tester.pumpWidget( | |
| Now.fixed( | |
| dateTime: utc$2020_9_1_12_30, | |
| child: TaskBox( | |
| cellSize: _cellSize, | |
| child: MaterialApp( | |
| home: Scaffold( | |
| body: _TestGrid( | |
| buildState: buildState, | |
| task: generateTaskForTest( | |
| status: TaskStatus.succeeded, | |
| buildNumberList: [123], | |
| builderName: 'Linux flutter_release_builder', | |
| ), | |
| ), | |
| ), | |
| ), | |
| ), | |
| ), | |
| ); | |
| expect(find.byType(LuciTaskAttemptSummary), findsNothing); | |
| await tester.tapAt(const Offset(_cellSize * 1.5, _cellSize * 1.5)); | |
| await tester.pump(); | |
| expect(find.byType(LuciTaskAttemptSummary), findsOneWidget); | |
| }); | |
| testWidgets('TaskOverlay: RERUN button disabled when user !isAuthenticated', ( | |
| WidgetTester tester, | |
| ) async { | |
| final expectedTask = generateTaskForTest( | |
| status: TaskStatus.succeeded, | |
| attempts: 3, | |
| ); | |
| final buildState = FakeBuildState(rerunTaskResult: true); | |
| when(buildState.authService.isAuthenticated).thenReturn(false); | |
| await tester.pumpWidget( | |
| Now.fixed( | |
| dateTime: utc$2020_9_1_12_30, | |
| child: TaskBox( | |
| cellSize: _cellSize, | |
| child: MaterialApp( | |
| home: Scaffold( | |
| body: _TestGrid(buildState: buildState, task: expectedTask), | |
| ), | |
| ), | |
| ), | |
| ), | |
| ); | |
| // Open the overlay | |
| await tester.tapAt(const Offset(_cellSize * 1.5, _cellSize * 1.5)); | |
| await tester.pump(); | |
| final rerun = tester | |
| .element(find.text('RERUN')) | |
| .findAncestorWidgetOfExactType<ProgressButton>(); | |
| expect(rerun, isNotNull, reason: 'The rerun button should exist.'); | |
| expect( | |
| rerun!.onPressed, | |
| isNull, | |
| reason: 'The rerun button should be disabled.', | |
| ); | |
| }); | |
| testWidgets('TaskOverlay: successful rerun shows success snackbar message', ( | |
| WidgetTester tester, | |
| ) async { | |
| final expectedTask = generateTaskForTest( | |
| status: TaskStatus.succeeded, | |
| attempts: 3, | |
| ); | |
| final buildState = FakeBuildState(rerunTaskResult: true); | |
| when(buildState.authService.isAuthenticated).thenReturn(true); | |
| await tester.pumpWidget( | |
| Now.fixed( | |
| dateTime: utc$2020_9_1_12_30, | |
| child: TaskBox( | |
| cellSize: _cellSize, | |
| child: MaterialApp( | |
| home: Scaffold( | |
| body: _TestGrid(buildState: buildState, task: expectedTask), | |
| ), | |
| ), | |
| ), | |
| ), | |
| ); | |
| // Open the overlay | |
| await tester.tapAt(const Offset(_cellSize * 1.5, _cellSize * 1.5)); | |
| await tester.pump(); | |
| expect(find.text(TaskOverlayContents.rerunErrorMessage), findsNothing); | |
| expect(find.text(TaskOverlayContents.rerunSuccessMessage), findsNothing); | |
| // Click the rerun task button | |
| await tester.tap(find.text('RERUN')); | |
| await tester.pump(); | |
| await tester.pump(const Duration(milliseconds: 750)); // open animation | |
| expect(find.text(TaskOverlayContents.rerunErrorMessage), findsNothing); | |
| expect(find.text(TaskOverlayContents.rerunSuccessMessage), findsOneWidget); | |
| // Snackbar message should go away after its duration | |
| await tester.pump(TaskOverlayContents.rerunSnackBarDuration); | |
| await tester.pump(const Duration(milliseconds: 1500)); // close animation | |
| expect(find.text(TaskOverlayContents.rerunErrorMessage), findsNothing); | |
| expect(find.text(TaskOverlayContents.rerunSuccessMessage), findsNothing); | |
| }); | |
| testWidgets('failed rerun shows errorBrook snackbar message', ( | |
| WidgetTester tester, | |
| ) async { | |
| final expectedTask = generateTaskForTest( | |
| status: TaskStatus.waitingForBackfill, | |
| attempts: 3, | |
| ); | |
| final buildState = FakeBuildState(rerunTaskResult: false); | |
| when(buildState.authService.isAuthenticated).thenReturn(true); | |
| await tester.pumpWidget( | |
| Now.fixed( | |
| dateTime: utc$2020_9_1_12_30, | |
| child: TaskBox( | |
| cellSize: _cellSize, | |
| child: MaterialApp( | |
| home: ValueProvider<BuildState>( | |
| value: buildState, | |
| child: Scaffold( | |
| body: ErrorBrookWatcher( | |
| errors: buildState.errors, | |
| child: _TestGrid(buildState: buildState, task: expectedTask), | |
| ), | |
| ), | |
| ), | |
| ), | |
| ), | |
| ), | |
| ); | |
| await tester.tapAt(const Offset(_cellSize * 1.5, _cellSize * 1.5)); | |
| // await tester.tap(find.byType(LatticeCell)); | |
| // await tester.tap(find.byType(TaskOverlayContents)); | |
| await tester.pump(); | |
| // Click the rerun task button | |
| await tester.tap(find.text('RERUN')); | |
| await tester.pump(); | |
| await tester.pump(const Duration(milliseconds: 750)); // open animation | |
| expect(find.text(TaskOverlayContents.rerunErrorMessage), findsOneWidget); | |
| expect(find.text(TaskOverlayContents.rerunSuccessMessage), findsNothing); | |
| // 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(TaskOverlayContents.rerunErrorMessage), findsNothing); | |
| expect(find.text(TaskOverlayContents.rerunSuccessMessage), findsNothing); | |
| }); | |
| test('TaskOverlayEntryPositionDelegate.positionDependentBox', () async { | |
| const normalSize = Size(800, 600); | |
| const childSize = Size(300, 180); | |
| // Window is too small, center. | |
| expect( | |
| TaskOverlayEntryPositionDelegate.positionDependentBox( | |
| size: const Size(250, 150), | |
| childSize: childSize, | |
| cellSize: _cellSize, | |
| target: const Offset(50.0, 50.0), | |
| ), | |
| const Offset(-25.0, 10.0), | |
| ); | |
| // Normal positioning, below and to right. | |
| expect( | |
| TaskOverlayEntryPositionDelegate.positionDependentBox( | |
| size: normalSize, | |
| childSize: childSize, | |
| cellSize: _cellSize, | |
| target: const Offset(50.0, 50.0), | |
| ), | |
| const Offset(50.0, 82.4), | |
| ); | |
| // Doesn't fit in right, below and to left. | |
| expect( | |
| TaskOverlayEntryPositionDelegate.positionDependentBox( | |
| size: normalSize, | |
| childSize: childSize, | |
| cellSize: _cellSize, | |
| target: const Offset(590.0, 50.0), | |
| ), | |
| const Offset(490.0, 82.4), | |
| ); | |
| // Doesn't fit below, above and to right. | |
| expect( | |
| TaskOverlayEntryPositionDelegate.positionDependentBox( | |
| size: normalSize, | |
| childSize: childSize, | |
| cellSize: _cellSize, | |
| target: const Offset(50.0, 500.0), | |
| ), | |
| const Offset(50.0, 320.0), | |
| ); | |
| // Above and to left. | |
| expect( | |
| TaskOverlayEntryPositionDelegate.positionDependentBox( | |
| size: normalSize, | |
| childSize: childSize, | |
| cellSize: _cellSize, | |
| target: const Offset(590.0, 500.0), | |
| ), | |
| const Offset(490.0, 320.0), | |
| ); | |
| }); | |
| } | |
| final class _TestGrid extends StatelessWidget { | |
| const _TestGrid({required this.buildState, required this.task, this.filter}); | |
| final BuildState buildState; | |
| final Task task; | |
| final TaskGridFilter? filter; | |
| @override | |
| Widget build(BuildContext context) { | |
| return Material( | |
| child: TaskGrid( | |
| filter: filter, | |
| buildState: buildState, | |
| commitStatuses: <CommitStatus>[ | |
| CommitStatus( | |
| commit: generateCommitForTest( | |
| author: 'Fats Domino', | |
| sha: '24e8c0a2', | |
| ), | |
| tasks: [task], | |
| ), | |
| ], | |
| ), | |
| ); | |
| } | |
| } |