| // 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:fixnum/fixnum.dart'; |
| import 'package:flutter/material.dart'; |
| import 'package:flutter_dashboard/logic/qualified_task.dart'; |
| import 'package:flutter_dashboard/model/commit.pb.dart'; |
| import 'package:flutter_dashboard/model/commit_status.pb.dart'; |
| import 'package:flutter_dashboard/model/task.pb.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/golden.dart'; |
| import '../utils/task_icons.dart'; |
| |
| class TestGrid extends StatelessWidget { |
| const TestGrid({ |
| required this.buildState, |
| required this.task, |
| super.key, |
| }); |
| |
| final BuildState buildState; |
| final Task task; |
| |
| @override |
| Widget build(BuildContext context) { |
| return Material( |
| child: TaskGrid( |
| buildState: buildState, |
| commitStatuses: <CommitStatus>[ |
| CommitStatus() |
| ..commit = (Commit() |
| ..author = 'Fats Domino' |
| ..sha = '24e8c0a2') |
| ..tasks.addAll(<Task>[task]), |
| ], |
| ), |
| ); |
| } |
| } |
| |
| const double _cellSize = 36; |
| |
| void main() { |
| final DateTime nowTime = DateTime.utc(2020, 9, 1, 12, 30); |
| final DateTime createTime = nowTime.subtract(const Duration(minutes: 52)); |
| final DateTime startTime = nowTime.subtract(const Duration(minutes: 50)); |
| final DateTime finishTime = nowTime.subtract(const Duration(minutes: 10)); |
| |
| Int64 int64FromDateTime(DateTime time) => Int64(time.millisecondsSinceEpoch); |
| |
| late FakeBuildState buildState; |
| |
| setUp(() { |
| buildState = FakeBuildState(); |
| when(buildState.authService.isAuthenticated).thenReturn(true); |
| }); |
| |
| testWidgets('TaskOverlay shows on click', (WidgetTester tester) async { |
| await precacheTaskIcons(tester); |
| |
| final Task expectedTask = Task() |
| ..attempts = 3 |
| ..stageName = StageName.luci |
| ..name = 'Tasky McTaskFace' |
| ..reservedForAgentId = 'Agenty McAgentFace' |
| ..isFlaky = false // As opposed to the next test. |
| ..status = TaskBox.statusFailed |
| ..createTimestamp = int64FromDateTime(createTime) |
| ..startTimestamp = int64FromDateTime(startTime) |
| ..endTimestamp = int64FromDateTime(finishTime); |
| |
| final String expectedTaskInfoString = 'Attempts: ${expectedTask.attempts}\n' |
| 'Run time: 40 minutes\n' |
| 'Queue time: 2 minutes'; |
| |
| await tester.pumpWidget( |
| Now.fixed( |
| dateTime: nowTime, |
| 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.name), findsNothing); |
| expect(find.text(expectedTaskInfoString), findsNothing); |
| expect(find.text(expectedTask.reservedForAgentId), 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.name), 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.name), findsNothing); |
| expect(find.text(expectedTaskInfoString), findsNothing); |
| expect(find.text(expectedTask.reservedForAgentId), 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 Task flakyTask = Task() |
| ..attempts = 3 |
| ..stageName = StageName.luci |
| ..name = 'Tasky McTaskFace' |
| ..isFlaky = true // This is the point of this test. |
| ..status = TaskBox.statusFailed |
| ..createTimestamp = int64FromDateTime(createTime) |
| ..startTimestamp = int64FromDateTime(startTime) |
| ..endTimestamp = int64FromDateTime(finishTime); |
| |
| final String flakyTaskInfoString = 'Attempts: ${flakyTask.attempts}\n' |
| 'Run time: 40 minutes\n' |
| 'Queue time: 2 minutes\n' |
| 'Flaky: true'; |
| |
| await tester.pumpWidget( |
| Now.fixed( |
| dateTime: nowTime, |
| child: TaskBox( |
| cellSize: _cellSize, |
| child: MaterialApp( |
| theme: ThemeData(useMaterial3: false), |
| home: Scaffold( |
| body: TestGrid( |
| buildState: buildState, |
| task: flakyTask, |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| await tester.pump(); |
| |
| expect(find.text(flakyTask.name), findsNothing); |
| expect(find.text(flakyTaskInfoString), findsNothing); |
| expect(find.text(flakyTask.reservedForAgentId), 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.name), 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 DateTime createTime = nowTime.subtract(const Duration(minutes: 11)); |
| final DateTime startTime = nowTime.subtract(const Duration(minutes: 9)); |
| final DateTime finishTime = nowTime.subtract(const Duration(minutes: 1)); |
| |
| final Task timeTask = Task() |
| ..attempts = 1 |
| ..stageName = StageName.luci |
| ..name = 'Tasky McTaskFace' |
| ..isFlaky = false |
| ..createTimestamp = int64FromDateTime(createTime) |
| ..startTimestamp = int64FromDateTime(startTime) |
| ..endTimestamp = int64FromDateTime(finishTime); |
| |
| final String timeTaskInfoString = 'Attempts: ${timeTask.attempts}\n' |
| 'Run time: 8 minutes\n' |
| 'Queue time: 2 minutes'; |
| |
| await tester.pumpWidget( |
| Now.fixed( |
| dateTime: nowTime, |
| 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 DateTime createTime = nowTime.subtract(const Duration(minutes: 11)); |
| final DateTime startTime = nowTime.subtract(const Duration(minutes: 9)); |
| |
| final Task timeTask = Task() |
| ..attempts = 1 |
| ..stageName = StageName.luci |
| ..name = 'Tasky McTaskFace' |
| ..status = TaskBox.statusInProgress |
| ..isFlaky = false |
| ..createTimestamp = int64FromDateTime(createTime) |
| ..startTimestamp = int64FromDateTime(startTime); |
| |
| final String timeTaskInfoString = 'Attempts: ${timeTask.attempts}\n' |
| 'Running for 9 minutes\n' |
| 'Queue time: 2 minutes'; |
| |
| await tester.pumpWidget( |
| Now.fixed( |
| dateTime: nowTime, |
| 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 queueing task', (WidgetTester tester) async { |
| /// Create a queue time of 2 minutes |
| final DateTime createTime = nowTime.subtract(const Duration(minutes: 2)); |
| |
| final Task timeTask = Task() |
| ..attempts = 1 |
| ..stageName = StageName.luci |
| ..name = 'Tasky McTaskFace' |
| ..status = TaskBox.statusNew |
| ..isFlaky = false |
| ..createTimestamp = int64FromDateTime(createTime); |
| |
| final String timeTaskInfoString = 'Attempts: ${timeTask.attempts}\n' |
| 'Queueing for 2 minutes'; |
| |
| await tester.pumpWidget( |
| Now.fixed( |
| dateTime: nowTime, |
| 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: nowTime, |
| child: TaskBox( |
| cellSize: _cellSize, |
| child: MaterialApp( |
| theme: ThemeData(useMaterial3: false), |
| home: Scaffold( |
| body: TestGrid( |
| buildState: buildState, |
| task: Task() |
| ..stageName = 'luci' |
| ..status = TaskBox.statusSucceeded, |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| 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: nowTime, |
| child: TaskBox( |
| cellSize: _cellSize, |
| child: MaterialApp( |
| home: Scaffold( |
| body: TestGrid( |
| buildState: buildState, |
| task: Task() |
| ..stageName = 'chromebot' |
| ..status = TaskBox.statusSucceeded |
| ..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: nowTime, |
| child: TaskBox( |
| cellSize: _cellSize, |
| child: MaterialApp( |
| home: Scaffold( |
| body: TestGrid( |
| buildState: buildState, |
| task: Task() |
| ..stageName = 'dart-internal' |
| ..status = TaskBox.statusSucceeded |
| ..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: RERUN button disabled when user !isAuthenticated', (WidgetTester tester) async { |
| final Task expectedTask = Task() |
| ..attempts = 3 |
| ..stageName = StageName.luci |
| ..name = 'Tasky McTaskFace' |
| ..reservedForAgentId = 'Agenty McAgentFace' |
| ..isFlaky = false; |
| |
| final FakeBuildState buildState = FakeBuildState(rerunTaskResult: true); |
| when(buildState.authService.isAuthenticated).thenReturn(false); |
| |
| await tester.pumpWidget( |
| Now.fixed( |
| dateTime: nowTime, |
| 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 ProgressButton? 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 Task expectedTask = Task() |
| ..attempts = 3 |
| ..stageName = StageName.luci |
| ..name = 'Tasky McTaskFace' |
| ..reservedForAgentId = 'Agenty McAgentFace' |
| ..isFlaky = false; |
| |
| final FakeBuildState buildState = FakeBuildState(rerunTaskResult: true); |
| when(buildState.authService.isAuthenticated).thenReturn(true); |
| |
| await tester.pumpWidget( |
| Now.fixed( |
| dateTime: nowTime, |
| 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 Task expectedTask = Task() |
| ..attempts = 3 |
| ..stageName = StageName.luci |
| ..name = 'Tasky McTaskFace' |
| ..reservedForAgentId = 'Agenty McAgentFace' |
| ..isFlaky = false |
| ..status = TaskBox.statusNew; |
| |
| final FakeBuildState buildState = FakeBuildState(rerunTaskResult: false); |
| when(buildState.authService.isAuthenticated).thenReturn(true); |
| |
| await tester.pumpWidget( |
| Now.fixed( |
| dateTime: nowTime, |
| 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 Size normalSize = Size(800, 600); |
| const Size 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), |
| ); |
| }); |
| } |