blob: 44ce0ce3de3f9f3b24586ecd5b15bbcc0ecad879 [file] [log] [blame]
// 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:typed_data';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_app_icons/flutter_app_icons_platform_interface.dart';
import 'package:flutter_dashboard/logic/qualified_task.dart';
import 'package:flutter_dashboard/logic/task_grid_filter.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/service/dev_cocoon.dart';
import 'package:flutter_dashboard/state/build.dart';
import 'package:flutter_dashboard/widgets/commit_box.dart';
import 'package:flutter_dashboard/widgets/lattice.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_icon.dart';
import 'package:flutter_test/flutter_test.dart';
import '../utils/fake_build.dart';
import '../utils/fake_flutter_app_icons.dart';
import '../utils/golden.dart';
import '../utils/mocks.dart';
import '../utils/task_icons.dart';
const double _cellSize = 36;
void main() {
setUp(() {
FlutterAppIconsPlatform.instance = FakeFlutterAppIcons();
});
testWidgets('TaskGridContainer shows loading indicator when statuses is empty', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: ValueProvider<BuildState>(
value: FakeBuildState(),
child: const Material(
child: TaskGridContainer(),
),
),
),
);
expect(find.byType(CircularProgressIndicator), findsOneWidget);
expect(find.byType(LatticeScrollView), findsNothing);
});
testWidgets('TaskGridContainer with DevelopmentCocoonService', (WidgetTester tester) async {
await precacheTaskIcons(tester);
final DevelopmentCocoonService service = DevelopmentCocoonService(DateTime.utc(2020));
final BuildState buildState = BuildState(
cocoonService: service,
authService: MockGoogleSignInService(),
);
void listener1() {}
buildState.addListener(listener1);
await tester.pumpWidget(
TaskBox(
cellSize: _cellSize,
child: MaterialApp(
theme: ThemeData(useMaterial3: false),
home: ValueProvider<BuildState>(
value: buildState,
child: const Material(
child: TaskGridContainer(),
),
),
),
),
);
await tester.pump();
final int commitCount = tester.elementList(find.byType(CommitBox)).length;
expect(commitCount, 16); // based on screen size this is how many show up
final double xPosition = tester.getTopLeft(find.byType(CommitBox).first).dx;
for (int index = 0; index < commitCount; index += 1) {
// All the x positions should match the first instance if they're all in the same column
expect(tester.getTopLeft(find.byType(CommitBox).at(index)).dx, xPosition);
}
await expectGoldenMatches(find.byType(TaskGrid), 'task_grid_test.dev.origin.png');
// Check if the LOADING... indicator appears.
service.paused = true;
await tester.drag(find.byType(TaskGrid), const Offset(0.0, -5000.0));
await tester.pumpAndSettle();
await expectGoldenMatches(find.byType(TaskGrid), 'task_grid_test.dev.scroll_y.png');
service.paused = false;
await tester.pumpAndSettle();
// Check the right edge after the data comes in.
service.paused = true;
await tester.drag(find.byType(TaskGrid), const Offset(-5000.0, 0.0));
await tester.pumpAndSettle();
await expectGoldenMatches(find.byType(TaskGrid), 'task_grid_test.dev.scroll_x.png');
service.paused = false;
await tester.pumpAndSettle();
await tester.pumpWidget(Container());
buildState.dispose();
});
testWidgets('TaskGridContainer supports mouse drag', (WidgetTester tester) async {
await precacheTaskIcons(tester);
final DevelopmentCocoonService service = DevelopmentCocoonService(DateTime.utc(2020));
final BuildState buildState = BuildState(
cocoonService: service,
authService: MockGoogleSignInService(),
);
void listener1() {}
buildState.addListener(listener1);
await tester.pumpWidget(
TaskBox(
cellSize: _cellSize,
child: MaterialApp(
theme: ThemeData(useMaterial3: false),
home: ValueProvider<BuildState>(
value: buildState,
child: const Material(
child: TaskGridContainer(),
),
),
),
),
);
await tester.pump();
final int commitCount = tester.elementList(find.byType(CommitBox)).length;
expect(commitCount, 16); // based on screen size this is how many show up
final double xPosition = tester.getTopLeft(find.byType(CommitBox).first).dx;
for (int index = 0; index < commitCount; index += 1) {
// All the x positions should match the first instance if they're all in the same column
expect(tester.getTopLeft(find.byType(CommitBox).at(index)).dx, xPosition);
}
await expectGoldenMatches(find.byType(TaskGrid), 'task_grid_test.dev.origin.png');
// Check if the LOADING... indicator appears.
service.paused = true;
TestGesture gesture = await tester.startGesture(
tester.getCenter(find.byType(TaskGrid)),
kind: PointerDeviceKind.mouse,
);
for (int i = 0; i < 100; i += 1) {
await gesture.moveBy(const Offset(0.0, -50.0));
}
await gesture.up();
await tester.pumpAndSettle();
await expectGoldenMatches(find.byType(TaskGrid), 'task_grid_test.dev.mouse_scroll_y.png');
await gesture.removePointer();
service.paused = false;
await tester.pumpAndSettle();
// Check the right edge after the data comes in.
service.paused = true;
gesture = await tester.startGesture(
tester.getCenter(find.byType(TaskGrid)),
kind: PointerDeviceKind.mouse,
);
for (int i = 0; i < 100; i += 1) {
await gesture.moveBy(const Offset(-50.0, 0));
}
//await gesture.moveBy(const Offset(-5000, 0));
await gesture.up();
await tester.pumpAndSettle();
await expectGoldenMatches(find.byType(TaskGrid), 'task_grid_test.dev.mouse_scroll_x.png');
service.paused = false;
await gesture.removePointer();
await tester.pumpWidget(Container());
buildState.dispose();
});
testWidgets('TaskGridContainer with DevelopmentCocoonService - dark', (WidgetTester tester) async {
await precacheTaskIcons(tester);
final DevelopmentCocoonService service = DevelopmentCocoonService(DateTime.utc(2020));
final BuildState buildState = BuildState(
cocoonService: service,
authService: MockGoogleSignInService(),
);
void listener1() {}
buildState.addListener(listener1);
await tester.pumpWidget(
TaskBox(
cellSize: _cellSize,
child: MaterialApp(
theme: ThemeData.dark(useMaterial3: false),
home: ValueProvider<BuildState>(
value: buildState,
child: const Material(
child: TaskGridContainer(),
),
),
),
),
);
await tester.pump();
final int commitCount = tester.elementList(find.byType(CommitBox)).length;
expect(commitCount, 16); // based on screen size this is how many show up
final double xPosition = tester.getTopLeft(find.byType(CommitBox).first).dx;
for (int index = 0; index < commitCount; index += 1) {
// All the x positions should match the first instance if they're all in the same column
expect(tester.getTopLeft(find.byType(CommitBox).at(index)).dx, xPosition);
}
await expectGoldenMatches(find.byType(TaskGrid), 'task_grid_test.dev.origin.dark.png');
// Check if the LOADING... indicator appears.
service.paused = true;
await tester.drag(find.byType(TaskGrid), const Offset(0.0, -5000.0));
await tester.pumpAndSettle();
await expectGoldenMatches(find.byType(TaskGrid), 'task_grid_test.dev.scroll_y.dark.png');
service.paused = false;
await tester.pumpAndSettle();
// Check the right edge after the data comes in.
service.paused = true;
await tester.drag(find.byType(TaskGrid), const Offset(-5000.0, 0.0));
await tester.pumpAndSettle();
await expectGoldenMatches(find.byType(TaskGrid), 'task_grid_test.dev.scroll_x.dark.png');
service.paused = false;
await tester.pumpAndSettle();
await tester.pumpWidget(Container());
buildState.dispose();
});
Future<void> testGrid(WidgetTester tester, TaskGridFilter? filter, int rows, int cols) async {
final BuildState buildState = BuildState(
cocoonService: DevelopmentCocoonService(DateTime.utc(2020)),
authService: MockGoogleSignInService(),
);
void listener1() {}
buildState.addListener(listener1);
await tester.pumpWidget(
TaskBox(
cellSize: _cellSize,
child: MaterialApp(
theme: ThemeData.dark(),
home: ValueProvider<BuildState>(
value: buildState,
child: Material(
child: TaskGridContainer(filter: filter),
),
),
),
),
);
await tester.pump();
expect(find.byType(LatticeScrollView), findsOneWidget);
final LatticeScrollView lattice = find.byType(LatticeScrollView).evaluate().first.widget as LatticeScrollView;
expect(lattice.cells.length, rows);
for (final List<LatticeCell> row in lattice.cells) {
expect(row.length, cols);
}
await tester.pumpWidget(Container());
buildState.dispose();
}
testWidgets('Task name filter affects grid', (WidgetTester tester) async {
// Default filters
await testGrid(tester, null, 27, 101);
await testGrid(tester, TaskGridFilter(), 27, 101);
await testGrid(tester, TaskGridFilter.fromMap(null), 27, 101);
// QualifiedTask (column) filters
await testGrid(tester, TaskGridFilter()..taskFilter = RegExp('Linux_android 2'), 27, 12);
// CommitStatus (row) filters
await testGrid(tester, TaskGridFilter()..authorFilter = RegExp('bob'), 8, 101);
await testGrid(tester, TaskGridFilter()..messageFilter = RegExp('developer'), 18, 101);
await testGrid(tester, TaskGridFilter()..hashFilter = RegExp('2d22b5e85f986f3fa2cf1bfaf085905c2182c270'), 4, 101);
});
testWidgets('Skipped tasks do not break the grid', (WidgetTester tester) async {
await precacheTaskIcons(tester);
// Matrix Diagram:
//
// ✓☐☐
// ☐✓☐
// ☐☐✓
//
// To construct the matrix from this diagram, each [CommitStatus] must have a unique [Task]
// that does not share its name with any other [Task]. This will make that [CommitStatus] have
// its task on its own unique row and column.
final List<CommitStatus> statusesWithSkips = <CommitStatus>[
CommitStatus()
..commit = (Commit()..author = 'Author')
..tasks.addAll(
<Task>[
Task()
..stageName = 'A'
..name = '1'
..builderName = '1'
..status = TaskBox.statusSucceeded,
],
),
CommitStatus()
..commit = (Commit()..author = 'Author')
..tasks.addAll(
<Task>[
Task()
..stageName = 'A'
..name = '2'
..builderName = '2'
..status = TaskBox.statusSucceeded,
],
),
CommitStatus()
..commit = (Commit()..author = 'Author')
..tasks.addAll(
<Task>[
Task()
..stageName = 'A'
..name = '3'
..builderName = '3'
..status = TaskBox.statusSucceeded,
],
),
];
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(useMaterial3: false),
home: Material(
child: TaskGrid(
buildState: FakeBuildState(),
commitStatuses: statusesWithSkips,
),
),
),
);
await expectGoldenMatches(find.byType(TaskGrid), 'task_grid_test.withSkips.png');
});
testWidgets('Cocoon and LUCI tasks share the same column', (WidgetTester tester) async {
await precacheTaskIcons(tester);
// Matrix Diagram:
//
// ✓
// ✓
//
// To construct the matrix from this diagram, each [CommitStatus] will have a [Task]
// that shares its name, but will have a different stage name.
final List<CommitStatus> statuses = <CommitStatus>[
CommitStatus()
..commit = (Commit()..author = 'Author')
..tasks.addAll(
<Task>[
Task()
..stageName = StageName.cocoon
..name = '1'
..builderName = '1'
..status = TaskBox.statusSucceeded,
],
),
CommitStatus()
..commit = (Commit()..author = 'Author')
..tasks.addAll(
<Task>[
Task()
..stageName = StageName.luci
..name = '1'
..builderName = '1'
..status = TaskBox.statusSucceeded,
],
),
];
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TaskGrid(
buildState: FakeBuildState(),
commitStatuses: statuses,
),
),
),
);
expect(find.byType(LatticeScrollView), findsOneWidget);
final LatticeScrollView lattice = find.byType(LatticeScrollView).evaluate().first.widget as LatticeScrollView;
// Rows (task icon, two commits, load more row)
expect(lattice.cells.length, 4);
// Columns (commit box, task)
expect(lattice.cells.first.length, 2);
expect(lattice.cells[1].length, 2);
});
testWidgets('TaskGrid creates a task icon row and they line up', (WidgetTester tester) async {
final List<CommitStatus> commitStatuses = <CommitStatus>[
CommitStatus()
..commit = (Commit()..author = 'Author')
..tasks.addAll(
<Task>[
Task()
..name = 'Task Name'
..builderName = 'Task Name'
..stageName = 'Stage Nome 1'
..status = TaskBox.statusSucceeded,
Task()
..name = 'Task Name'
..builderName = 'Task Name'
..stageName = 'Stage Nome 2'
..status = TaskBox.statusFailed,
],
),
];
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TaskGrid(
buildState: FakeBuildState(),
commitStatuses: commitStatuses,
),
),
),
);
expect(find.byType(TaskIcon), findsNWidgets(2));
expect(tester.getTopLeft(find.byType(TaskIcon).at(0)).dy, tester.getTopLeft(find.byType(TaskIcon).at(1)).dy);
});
testWidgets('TaskGrid honors moreStatusesExist', (WidgetTester tester) async {
await precacheTaskIcons(tester);
final List<CommitStatus> commitStatuses = <CommitStatus>[
CommitStatus()
..commit = (Commit()..author = 'Author')
..tasks.addAll(
<Task>[
Task()
..name = 'Task Name'
..name = 'Task Name'
..stageName = 'Stage Nome'
..status = TaskBox.statusSucceeded,
],
),
];
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(useMaterial3: false),
home: Material(
child: TaskGrid(
buildState: FakeBuildState(moreStatusesExist: false),
commitStatuses: commitStatuses,
),
),
),
);
await expectGoldenMatches(find.byType(TaskGrid), 'task_grid_test.withoutL.png');
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(useMaterial3: false),
home: Material(
child: TaskGrid(
buildState: FakeBuildState(moreStatusesExist: true),
commitStatuses: commitStatuses,
),
),
),
);
await expectGoldenMatches(find.byType(TaskGrid), 'task_grid_test.withL.png');
});
testWidgets('TaskGrid shows icon for rerun tasks', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TaskGrid(
buildState: FakeBuildState(
authService: MockGoogleSignInService(),
cocoonService: MockCocoonService(),
),
commitStatuses: <CommitStatus>[
CommitStatus()
..commit = (Commit()..author = 'Cast')
..tasks.addAll(
<Task>[
Task()
..stageName = 'A'
..status = 'Succeeded'
..attempts = 2,
],
),
],
),
),
),
);
expect(find.byIcon(Icons.priority_high), findsOneWidget);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TaskGrid(
buildState: FakeBuildState(
authService: MockGoogleSignInService(),
cocoonService: MockCocoonService(),
),
commitStatuses: <CommitStatus>[
CommitStatus()
..commit = (Commit()..author = 'Cast')
..tasks.addAll(
<Task>[
Task()
..stageName = 'A'
..status = 'Succeeded'
..attempts = 1,
],
),
],
),
),
),
);
expect(find.byIcon(Icons.priority_high), findsNothing);
});
testWidgets('TaskGrid shows icon for isTestFlaky tasks', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TaskGrid(
buildState: FakeBuildState(
authService: MockGoogleSignInService(),
cocoonService: MockCocoonService(),
),
commitStatuses: <CommitStatus>[
CommitStatus()
..commit = (Commit()..author = 'Cast')
..tasks.addAll(
<Task>[
Task()
..stageName = 'A'
..status = 'Succeeded'
..attempts = 1
..isTestFlaky = true,
],
),
],
),
),
),
);
expect(find.byIcon(Icons.priority_high), findsOneWidget);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TaskGrid(
buildState: FakeBuildState(
authService: MockGoogleSignInService(),
cocoonService: MockCocoonService(),
),
commitStatuses: <CommitStatus>[
CommitStatus()
..commit = (Commit()..author = 'Cast')
..tasks.addAll(
<Task>[
Task()
..stageName = 'A'
..status = 'Succeeded'
..attempts = 1
..isTestFlaky = false,
],
),
],
),
),
),
);
expect(find.byIcon(Icons.priority_high), findsNothing);
});
testWidgets('TaskGrid shows icon for isTestFlaky tasks with multiple attempts', (WidgetTester tester) async {
final Task taskA3 = Task()
..stageName = 'A'
..builderName = '1'
..name = 'A'
..status = TaskBox.statusSucceeded
..attempts = 3
..isTestFlaky = true;
final Task taskB1 = Task()
..stageName = 'B'
..builderName = '2'
..name = 'B'
..status = TaskBox.statusSucceeded
..attempts = 1
..isTestFlaky = false;
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TaskGrid(
buildState: FakeBuildState(
authService: MockGoogleSignInService(),
cocoonService: MockCocoonService(),
),
commitStatuses: <CommitStatus>[
CommitStatus()
..commit = (Commit()..author = 'Cast')
..tasks.addAll(
<Task>[
taskA3,
],
),
CommitStatus()
..commit = (Commit()..author = 'Cast')
..tasks.addAll(
<Task>[taskB1],
),
],
),
),
),
);
expect(find.byIcon(Icons.priority_high), findsNWidgets(1));
// check the order of the items. The flaky should be to the left and first.
expect(find.byType(TaskGrid).first, findsAtLeastNWidgets(1));
final LatticeScrollView latticeScrollView = tester.firstWidget(find.byType(LatticeScrollView));
final List<List<LatticeCell>> cells = latticeScrollView.cells;
final List<LatticeCell> myCells = cells.first;
expect(myCells.length, 3);
myCells.removeAt(0); // the first element is the github author box.
expect(myCells[0].taskName, 'A');
expect(myCells[1].taskName, 'B');
});
testWidgets('TaskGrid can handle all the various different statuses', (WidgetTester tester) async {
await precacheTaskIcons(tester);
final List<CommitStatus> statuses = <CommitStatus>[
CommitStatus()
..commit = (Commit()..author = 'Author')
..tasks.addAll(
<Task>[
Task()
..stageName = 'A'
..name = '1'
..builderName = '1'
..status = TaskBox.statusFailed,
Task()
..stageName = 'A'
..name = '2'
..builderName = '2'
..status = TaskBox.statusNew,
Task()
..stageName = 'A'
..name = '3'
..builderName = '3'
..status = TaskBox.statusSkipped,
Task()
..stageName = 'A'
..name = '4'
..builderName = '4'
..status = TaskBox.statusSucceeded,
Task()
..stageName = 'A'
..name = '5'
..builderName = '5'
..status = TaskBox.statusInProgress,
Task()..status = 'Invalid value',
],
),
CommitStatus()
..commit = (Commit()..author = 'Author')
..tasks.addAll(
<Task>[
Task()
..stageName = 'A'
..name = '1'
..builderName = '1'
..attempts = 2
..status = TaskBox.statusFailed,
Task()
..stageName = 'A'
..name = '2'
..builderName = '2'
..attempts = 2
..status = TaskBox.statusNew,
Task()
..stageName = 'A'
..name = '3'
..builderName = '3'
..attempts = 2
..status = TaskBox.statusSkipped,
Task()
..stageName = 'A'
..name = '4'
..builderName = '4'
..attempts = 2
..status = TaskBox.statusSucceeded,
Task()
..stageName = 'A'
..name = '5'
..builderName = '5'
..attempts = 2
..status = TaskBox.statusInProgress,
Task()..status = 'Invalid value',
],
),
CommitStatus()
..commit = (Commit()..author = 'Author')
..tasks.addAll(
<Task>[
Task()
..stageName = 'A'
..name = '1'
..builderName = '1'
..isFlaky = true
..status = TaskBox.statusFailed,
Task()
..stageName = 'A'
..name = '2'
..builderName = '2'
..isFlaky = true
..status = TaskBox.statusNew,
Task()
..stageName = 'A'
..name = '3'
..builderName = '3'
..isFlaky = true
..status = TaskBox.statusSkipped,
Task()
..stageName = 'A'
..name = '4'
..builderName = '4'
..isFlaky = true
..status = TaskBox.statusSucceeded,
Task()
..stageName = 'A'
..name = '5'
..builderName = '5'
..isFlaky = true
..status = TaskBox.statusInProgress,
Task()..status = 'Invalid value',
],
),
CommitStatus()
..commit = (Commit()..author = 'Author')
..tasks.addAll(
<Task>[
Task()
..stageName = 'A'
..name = '1'
..builderName = '1'
..attempts = 2
..isFlaky = true
..status = TaskBox.statusFailed,
Task()
..stageName = 'A'
..name = '2'
..builderName = '2'
..attempts = 2
..isFlaky = true
..status = TaskBox.statusNew,
Task()
..stageName = 'A'
..name = '3'
..builderName = '3'
..attempts = 2
..isFlaky = true
..status = TaskBox.statusSkipped,
Task()
..stageName = 'A'
..name = '4'
..builderName = '4'
..attempts = 2
..isFlaky = true
..status = TaskBox.statusSucceeded,
Task()
..stageName = 'A'
..name = '5'
..builderName = '5'
..attempts = 2
..isFlaky = true
..status = TaskBox.statusInProgress,
Task()..status = 'Invalid value',
],
),
];
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(useMaterial3: false),
home: Material(
child: TaskGrid(
buildState: FakeBuildState(),
commitStatuses: statuses,
),
),
),
);
await expectGoldenMatches(find.byType(TaskGrid), 'task_grid_test.differentTypes.png');
});
// Table Driven Approach to ensure every message does show the corresponding color
TaskBox.statusColor.forEach((String message, Color color) {
testWidgets('Is the color $color when given the message $message', (WidgetTester tester) async {
await expectTaskBoxColorWithMessage(tester, message, color);
});
});
}
Future<void> expectTaskBoxColorWithMessage(WidgetTester tester, String message, Color expectedColor) async {
const double cellSize = 18;
const double cellPixelSize = cellSize * 3.0;
const double cellPixelArea = cellPixelSize * cellPixelSize;
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: SizedBox(
height: cellPixelSize,
width: cellPixelSize,
child: RepaintBoundary(
child: TaskGrid(
buildState: FakeBuildState(
authService: MockGoogleSignInService(),
cocoonService: MockCocoonService(),
),
commitStatuses: <CommitStatus>[
CommitStatus()
..commit = (Commit()..author = 'Mathilda')
..tasks.addAll(
<Task>[Task()..status = message],
),
],
),
),
),
),
),
),
);
final RenderRepaintBoundary? renderObject =
tester.renderObject(find.byType(TaskGrid)).parent as RenderRepaintBoundary?;
final ByteData? pixels = await tester.runAsync<ByteData?>(() async {
return (await renderObject!.toImage()).toByteData();
});
expect(pixels!.lengthInBytes, (cellPixelArea * 4).round());
const double padding = 4.0;
final int rgba = pixels.getUint32((((cellPixelSize * (cellSize + padding)) + cellSize + padding).ceil()) * 4);
expect((rgba >> 8) | (rgba << 24) & 0xFFFFFFFF, expectedColor.value);
}