blob: caa02c660e72f6548a6145af31e09f0a1ecb65fd [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/material.dart';
import 'package:flutter/rendering.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/golden.dart';
import '../utils/mocks.dart';
import '../utils/task_icons.dart';
void main() {
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(
MaterialApp(
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 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(
MaterialApp(
theme: ThemeData.dark(),
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(
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;
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, 111);
await testGrid(tester, TaskGridFilter(), 27, 111);
await testGrid(tester, TaskGridFilter.fromMap(null), 27, 111);
// QualifiedTask (column) filters
await testGrid(tester, TaskGridFilter()..taskFilter = RegExp('task 2'), 27, 30);
await testGrid(tester, TaskGridFilter()..showCirrus = false, 27, 109);
await testGrid(tester, TaskGridFilter()..showLuci = false, 27, 111);
// CommitStatus (row) filters
await testGrid(tester, TaskGridFilter()..authorFilter = RegExp('bob'), 8, 111);
await testGrid(tester, TaskGridFilter()..messageFilter = RegExp('developer'), 18, 111);
await testGrid(tester, TaskGridFilter()..hashFilter = RegExp('c'), 20, 111);
});
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(
home: Material(
child: TaskGrid(
buildState: FakeBuildState(),
commitStatuses: statusesWithSkips,
),
),
),
);
await expectGoldenMatches(find.byType(TaskGrid), 'task_grid_test.withSkips.png');
});
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(
home: Material(
child: TaskGrid(
buildState: FakeBuildState(moreStatusesExist: false),
commitStatuses: commitStatuses,
),
),
),
);
await expectGoldenMatches(find.byType(TaskGrid), 'task_grid_test.withoutL.png');
await tester.pumpWidget(
MaterialApp(
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 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(
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 {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: SizedBox(
height: TaskBox.cellSize * 3.0,
width: TaskBox.cellSize * 3.0,
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 (await renderObject.toImage()).toByteData();
});
assert(pixels.lengthInBytes == ((TaskBox.cellSize * 3.0) * (TaskBox.cellSize * 3.0) * 4).round());
const double padding = 4.0;
final int rgba = pixels
.getUint32(((((TaskBox.cellSize * 3.0) * (TaskBox.cellSize + padding)) + TaskBox.cellSize + padding).ceil()) * 4);
expect((rgba >> 8) | (rgba << 24) & 0xFFFFFFFF, expectedColor.value);
}