| // 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], |
| ), |
| ], |
| ), |
| ); |
| } |
| } |