Unverified Commit 7b4c195f authored by Yegor's avatar Yegor Committed by GitHub

Fix web test flakiness; enable web golden tests (#52789)

* Fix web test flakiness; enable web golden tests

The fix is three-part:

- Only allow one test to load _and_ test at any point in time.
- Use a fresh Chrome instance for each test file.
- Increase Cirrus resources.

The first two changes only fix the "Unknown error loading" error, but not hanging tests. The resource increase also prevents hanging tests.

Other minor changes:

- Remove test batching (it's no longer necessary)
- Fix the Chrome class, which was using the wrong Completer.
parent f9b2d42b
...@@ -4,9 +4,9 @@ ...@@ -4,9 +4,9 @@
web_shard_template: &WEB_SHARD_TEMPLATE web_shard_template: &WEB_SHARD_TEMPLATE
only_if: "changesInclude('.cirrus.yml', 'dev/**', 'packages/flutter/**', 'packages/flutter_test/**', 'packages/flutter_tools/lib/src/test/**', 'packages/flutter_web_plugins/**', 'bin/internal/**') || $CIRRUS_PR == ''" only_if: "changesInclude('.cirrus.yml', 'dev/**', 'packages/flutter/**', 'packages/flutter_test/**', 'packages/flutter_tools/lib/src/test/**', 'packages/flutter_web_plugins/**', 'bin/internal/**') || $CIRRUS_PR == ''"
environment: environment:
# As of October 2019, the Web shards needed more than 6G of RAM. # As of March 2020, the Web shards needed 16G of RAM and 4 CPUs to run all framework tests with goldens without flaking.
CPU: 2 CPU: 4
MEMORY: 8G MEMORY: 16G
CHROME_NO_SANDBOX: true CHROME_NO_SANDBOX: true
GOLD_SERVICE_ACCOUNT: ENCRYPTED[3afeea5ac7201151c3d0dc9648862f0462b5e4f55dc600ca8b692319622f7c3eda3d577b1b16cc2ef0311b7314c1c095] GOLD_SERVICE_ACCOUNT: ENCRYPTED[3afeea5ac7201151c3d0dc9648862f0462b5e4f55dc600ca8b692319622f7c3eda3d577b1b16cc2ef0311b7314c1c095]
script: script:
......
...@@ -53,11 +53,6 @@ const int kDeviceLabShardCount = 4; ...@@ -53,11 +53,6 @@ const int kDeviceLabShardCount = 4;
/// The last shard also runs the Web plugin tests. /// The last shard also runs the Web plugin tests.
const int kWebShardCount = 8; const int kWebShardCount = 8;
/// Maximum number of Web tests to run in a single `flutter test`. We found that
/// large batches can get flaky, possibly because we reuse a single instance of
/// the browser, and after many tests the browser's state gets corrupted.
const int kWebBatchSize = 20;
/// Tests that we don't run on Web for various reasons. /// Tests that we don't run on Web for various reasons.
// //
// TODO(yjbanov): we're getting rid of this blacklist as part of https://github.com/flutter/flutter/projects/60 // TODO(yjbanov): we're getting rid of this blacklist as part of https://github.com/flutter/flutter/projects/60
...@@ -685,31 +680,23 @@ Future<void> _runWebDebugTest(String target) async { ...@@ -685,31 +680,23 @@ Future<void> _runWebDebugTest(String target) async {
} }
Future<void> _runFlutterWebTest(String workingDirectory, List<String> tests) async { Future<void> _runFlutterWebTest(String workingDirectory, List<String> tests) async {
final List<String> batch = <String>[]; await runCommand(
for (int i = 0; i < tests.length; i += 1) { flutter,
final String testFilePath = tests[i]; <String>[
batch.add(testFilePath); 'test',
if (batch.length == kWebBatchSize || i == tests.length - 1) { if (ciProvider == CiProviders.cirrus)
await runCommand( '--concurrency=1', // do not parallelize on Cirrus, to reduce flakiness
flutter, '-v',
<String>[ '--platform=chrome',
'test', ...?flutterTestArgs,
if (ciProvider == CiProviders.cirrus) ...tests,
'--concurrency=1', // do not parallelize on Cirrus, to reduce flakiness ],
'-v', workingDirectory: workingDirectory,
'--platform=chrome', environment: <String, String>{
...?flutterTestArgs, 'FLUTTER_WEB': 'true',
...batch, 'FLUTTER_LOW_RESOURCE_MODE': 'true',
], },
workingDirectory: workingDirectory, );
environment: <String, String>{
'FLUTTER_WEB': 'true',
'FLUTTER_LOW_RESOURCE_MODE': 'true',
},
);
batch.clear();
}
}
} }
Future<void> _pubRunTest(String workingDirectory, { Future<void> _pubRunTest(String workingDirectory, {
......
...@@ -82,7 +82,7 @@ void main() { ...@@ -82,7 +82,7 @@ void main() {
find.byKey(_painterKey), find.byKey(_painterKey),
matchesGoldenFile('bottom_app_bar_theme.custom_shape.png'), matchesGoldenFile('bottom_app_bar_theme.custom_shape.png'),
); );
}, skip: isBrowser); });
testWidgets('BAB theme does not affect defaults', (WidgetTester tester) async { testWidgets('BAB theme does not affect defaults', (WidgetTester tester) async {
await tester.pumpWidget(const MaterialApp( await tester.pumpWidget(const MaterialApp(
......
...@@ -913,7 +913,7 @@ void main() { ...@@ -913,7 +913,7 @@ void main() {
await tester.tap(find.text('Alarm')); await tester.tap(find.text('Alarm'));
await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1));
expect(Theme.of(tester.element(find.text('Alarm'))).brightness, equals(Brightness.dark)); expect(Theme.of(tester.element(find.text('Alarm'))).brightness, equals(Brightness.dark));
}, skip: isBrowser); });
testWidgets('BottomNavigationBar iconSize test', (WidgetTester tester) async { testWidgets('BottomNavigationBar iconSize test', (WidgetTester tester) async {
double builderIconSize; double builderIconSize;
...@@ -1023,7 +1023,7 @@ void main() { ...@@ -1023,7 +1023,7 @@ void main() {
final RenderBox box = tester.renderObject(find.byType(BottomNavigationBar)); final RenderBox box = tester.renderObject(find.byType(BottomNavigationBar));
expect(box.size.height, equals(66.0)); expect(box.size.height, equals(66.0));
}, skip: isBrowser); });
testWidgets('BottomNavigationBar limits width of tiles with long titles', (WidgetTester tester) async { testWidgets('BottomNavigationBar limits width of tiles with long titles', (WidgetTester tester) async {
final Text longTextA = Text(''.padLeft(100, 'A')); final Text longTextA = Text(''.padLeft(100, 'A'));
...@@ -1055,7 +1055,7 @@ void main() { ...@@ -1055,7 +1055,7 @@ void main() {
expect(itemBoxA.size, equals(const Size(400.0, 14.0))); expect(itemBoxA.size, equals(const Size(400.0, 14.0)));
final RenderBox itemBoxB = tester.renderObject(find.text(longTextB.data)); final RenderBox itemBoxB = tester.renderObject(find.text(longTextB.data));
expect(itemBoxB.size, equals(const Size(400.0, 14.0))); expect(itemBoxB.size, equals(const Size(400.0, 14.0)));
}, skip: isBrowser); });
testWidgets('BottomNavigationBar paints circles', (WidgetTester tester) async { testWidgets('BottomNavigationBar paints circles', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
...@@ -1125,7 +1125,7 @@ void main() { ...@@ -1125,7 +1125,7 @@ void main() {
..translate(x: 400.0) ..translate(x: 400.0)
..circle(x: 200.0), ..circle(x: 200.0),
); );
}, skip: isBrowser); });
testWidgets('BottomNavigationBar inactiveIcon shown', (WidgetTester tester) async { testWidgets('BottomNavigationBar inactiveIcon shown', (WidgetTester tester) async {
const Key filled = Key('filled'); const Key filled = Key('filled');
...@@ -1452,7 +1452,7 @@ void main() { ...@@ -1452,7 +1452,7 @@ void main() {
find.byType(BottomNavigationBar), find.byType(BottomNavigationBar),
matchesGoldenFile('bottom_navigation_bar.shifting_transition.${pump - 1}.png'), matchesGoldenFile('bottom_navigation_bar.shifting_transition.${pump - 1}.png'),
); );
}, skip: isBrowser); // TODO(yjbanov): web does not support golden tests yet: https://github.com/flutter/flutter/issues/40297 });
} }
}); });
......
...@@ -132,7 +132,7 @@ void main() { ...@@ -132,7 +132,7 @@ void main() {
find.byKey(_painterKey), find.byKey(_painterKey),
matchesGoldenFile('dialog_theme.dialog_with_custom_border.png'), matchesGoldenFile('dialog_theme.dialog_with_custom_border.png'),
); );
}, skip: isBrowser); });
testWidgets('Custom Title Text Style - Constructor Param', (WidgetTester tester) async { testWidgets('Custom Title Text Style - Constructor Param', (WidgetTester tester) async {
const String titleText = 'Title'; const String titleText = 'Title';
......
...@@ -189,7 +189,7 @@ void main() { ...@@ -189,7 +189,7 @@ void main() {
find.ancestor(of: buttonFinder, matching: find.byType(RepaintBoundary)).first, find.ancestor(of: buttonFinder, matching: find.byType(RepaintBoundary)).first,
matchesGoldenFile('dropdown_test.default.png'), matchesGoldenFile('dropdown_test.default.png'),
); );
}, skip: isBrowser); });
testWidgets('Expanded dropdown golden', (WidgetTester tester) async { testWidgets('Expanded dropdown golden', (WidgetTester tester) async {
final Key buttonKey = UniqueKey(); final Key buttonKey = UniqueKey();
...@@ -201,7 +201,7 @@ void main() { ...@@ -201,7 +201,7 @@ void main() {
find.ancestor(of: buttonFinder, matching: find.byType(RepaintBoundary)).first, find.ancestor(of: buttonFinder, matching: find.byType(RepaintBoundary)).first,
matchesGoldenFile('dropdown_test.expanded.png'), matchesGoldenFile('dropdown_test.expanded.png'),
); );
}, skip: isBrowser); });
testWidgets('Dropdown button control test', (WidgetTester tester) async { testWidgets('Dropdown button control test', (WidgetTester tester) async {
String value = 'one'; String value = 'one';
......
...@@ -85,7 +85,7 @@ void main() { ...@@ -85,7 +85,7 @@ void main() {
find.byType(FlexibleSpaceBar), find.byType(FlexibleSpaceBar),
matchesGoldenFile('flexible_space_bar_stretch_mode.blur_background.png'), matchesGoldenFile('flexible_space_bar_stretch_mode.blur_background.png'),
); );
}, skip: isBrowser); });
testWidgets('FlexibleSpaceBar stretch mode fadeTitle', (WidgetTester tester) async { testWidgets('FlexibleSpaceBar stretch mode fadeTitle', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
......
...@@ -282,7 +282,7 @@ void main() { ...@@ -282,7 +282,7 @@ void main() {
find.byKey(painterKey), find.byKey(painterKey),
matchesGoldenFile('radio.ink_ripple.png'), matchesGoldenFile('radio.ink_ripple.png'),
); );
}, skip: isBrowser); });
testWidgets('Radio is focusable and has correct focus color', (WidgetTester tester) async { testWidgets('Radio is focusable and has correct focus color', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(debugLabel: 'Radio'); final FocusNode focusNode = FocusNode(debugLabel: 'Radio');
......
...@@ -269,7 +269,7 @@ void main() { ...@@ -269,7 +269,7 @@ void main() {
find.byKey(_painterKey), find.byKey(_painterKey),
matchesGoldenFile('tab_bar_theme.tab_indicator_size_tab.png'), matchesGoldenFile('tab_bar_theme.tab_indicator_size_tab.png'),
); );
}, skip: isBrowser); });
testWidgets('Tab bar theme overrides tab indicator size (label)', (WidgetTester tester) async { testWidgets('Tab bar theme overrides tab indicator size (label)', (WidgetTester tester) async {
const TabBarTheme tabBarTheme = TabBarTheme(indicatorSize: TabBarIndicatorSize.label); const TabBarTheme tabBarTheme = TabBarTheme(indicatorSize: TabBarIndicatorSize.label);
...@@ -280,7 +280,7 @@ void main() { ...@@ -280,7 +280,7 @@ void main() {
find.byKey(_painterKey), find.byKey(_painterKey),
matchesGoldenFile('tab_bar_theme.tab_indicator_size_label.png'), matchesGoldenFile('tab_bar_theme.tab_indicator_size_label.png'),
); );
}, skip: isBrowser); });
testWidgets('Tab bar theme - custom tab indicator', (WidgetTester tester) async { testWidgets('Tab bar theme - custom tab indicator', (WidgetTester tester) async {
final TabBarTheme tabBarTheme = TabBarTheme( final TabBarTheme tabBarTheme = TabBarTheme(
...@@ -296,7 +296,7 @@ void main() { ...@@ -296,7 +296,7 @@ void main() {
find.byKey(_painterKey), find.byKey(_painterKey),
matchesGoldenFile('tab_bar_theme.custom_tab_indicator.png'), matchesGoldenFile('tab_bar_theme.custom_tab_indicator.png'),
); );
}, skip: isBrowser); });
testWidgets('Tab bar theme - beveled rect indicator', (WidgetTester tester) async { testWidgets('Tab bar theme - beveled rect indicator', (WidgetTester tester) async {
final TabBarTheme tabBarTheme = TabBarTheme( final TabBarTheme tabBarTheme = TabBarTheme(
...@@ -312,5 +312,5 @@ void main() { ...@@ -312,5 +312,5 @@ void main() {
find.byKey(_painterKey), find.byKey(_painterKey),
matchesGoldenFile('tab_bar_theme.beveled_rect_indicator.png'), matchesGoldenFile('tab_bar_theme.beveled_rect_indicator.png'),
); );
}, skip: isBrowser); });
} }
...@@ -52,7 +52,6 @@ void main() { ...@@ -52,7 +52,6 @@ void main() {
matchesGoldenFile('localized_fonts.rich_text.styled_text_span.png'), matchesGoldenFile('localized_fonts.rich_text.styled_text_span.png'),
); );
}, },
skip: isBrowser, // TODO(yjbanov): implement goldens on the Web: https://github.com/flutter/flutter/issues/40297
); );
testWidgets( testWidgets(
......
...@@ -45,5 +45,5 @@ void main() { ...@@ -45,5 +45,5 @@ void main() {
find.byType(RepaintBoundary).first, find.byType(RepaintBoundary).first,
matchesGoldenFile('backdrop_filter_test.cull_rect.png'), matchesGoldenFile('backdrop_filter_test.cull_rect.png'),
); );
}, skip: isBrowser); });
} }
...@@ -383,7 +383,7 @@ void main() { ...@@ -383,7 +383,7 @@ void main() {
find.byType(RepaintBoundary).first, find.byType(RepaintBoundary).first,
matchesGoldenFile('clip.ClipRect.png'), matchesGoldenFile('clip.ClipRect.png'),
); );
}, skip: isBrowser); });
testWidgets('ClipRect save, overlay, and antialiasing', (WidgetTester tester) async { testWidgets('ClipRect save, overlay, and antialiasing', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
...@@ -423,7 +423,7 @@ void main() { ...@@ -423,7 +423,7 @@ void main() {
find.byType(RepaintBoundary).first, find.byType(RepaintBoundary).first,
matchesGoldenFile('clip.ClipRectOverlay.png'), matchesGoldenFile('clip.ClipRectOverlay.png'),
); );
}, skip: isBrowser); });
testWidgets('ClipRRect painting', (WidgetTester tester) async { testWidgets('ClipRRect painting', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
...@@ -472,7 +472,7 @@ void main() { ...@@ -472,7 +472,7 @@ void main() {
find.byType(RepaintBoundary).first, find.byType(RepaintBoundary).first,
matchesGoldenFile('clip.ClipRRect.png'), matchesGoldenFile('clip.ClipRRect.png'),
); );
}, skip: isBrowser); });
testWidgets('ClipOval painting', (WidgetTester tester) async { testWidgets('ClipOval painting', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
...@@ -515,7 +515,7 @@ void main() { ...@@ -515,7 +515,7 @@ void main() {
find.byType(RepaintBoundary).first, find.byType(RepaintBoundary).first,
matchesGoldenFile('clip.ClipOval.png'), matchesGoldenFile('clip.ClipOval.png'),
); );
}, skip: isBrowser); });
testWidgets('ClipPath painting', (WidgetTester tester) async { testWidgets('ClipPath painting', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
...@@ -563,7 +563,7 @@ void main() { ...@@ -563,7 +563,7 @@ void main() {
find.byType(RepaintBoundary).first, find.byType(RepaintBoundary).first,
matchesGoldenFile('clip.ClipPath.png'), matchesGoldenFile('clip.ClipPath.png'),
); );
}, skip: isBrowser); });
Center genPhysicalModel(Clip clipBehavior) { Center genPhysicalModel(Clip clipBehavior) {
return Center( return Center(
...@@ -608,7 +608,7 @@ void main() { ...@@ -608,7 +608,7 @@ void main() {
find.byType(RepaintBoundary).first, find.byType(RepaintBoundary).first,
matchesGoldenFile('clip.PhysicalModel.antiAlias.png'), matchesGoldenFile('clip.PhysicalModel.antiAlias.png'),
); );
}, skip: isBrowser); });
testWidgets('PhysicalModel painting with Clip.hardEdge', (WidgetTester tester) async { testWidgets('PhysicalModel painting with Clip.hardEdge', (WidgetTester tester) async {
await tester.pumpWidget(genPhysicalModel(Clip.hardEdge)); await tester.pumpWidget(genPhysicalModel(Clip.hardEdge));
...@@ -616,7 +616,7 @@ void main() { ...@@ -616,7 +616,7 @@ void main() {
find.byType(RepaintBoundary).first, find.byType(RepaintBoundary).first,
matchesGoldenFile('clip.PhysicalModel.hardEdge.png'), matchesGoldenFile('clip.PhysicalModel.hardEdge.png'),
); );
}, skip: isBrowser); });
// There will be bleeding edges on the rect edges, but there shouldn't be any bleeding edges on the // There will be bleeding edges on the rect edges, but there shouldn't be any bleeding edges on the
// round corners. // round corners.
...@@ -626,7 +626,7 @@ void main() { ...@@ -626,7 +626,7 @@ void main() {
find.byType(RepaintBoundary).first, find.byType(RepaintBoundary).first,
matchesGoldenFile('clip.PhysicalModel.antiAliasWithSaveLayer.png'), matchesGoldenFile('clip.PhysicalModel.antiAliasWithSaveLayer.png'),
); );
}, skip: isBrowser); });
testWidgets('Default PhysicalModel painting', (WidgetTester tester) async { testWidgets('Default PhysicalModel painting', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
...@@ -668,7 +668,7 @@ void main() { ...@@ -668,7 +668,7 @@ void main() {
find.byType(RepaintBoundary).first, find.byType(RepaintBoundary).first,
matchesGoldenFile('clip.PhysicalModel.default.png'), matchesGoldenFile('clip.PhysicalModel.default.png'),
); );
}, skip: isBrowser); });
Center genPhysicalShape(Clip clipBehavior) { Center genPhysicalShape(Clip clipBehavior) {
return Center( return Center(
...@@ -717,7 +717,7 @@ void main() { ...@@ -717,7 +717,7 @@ void main() {
find.byType(RepaintBoundary).first, find.byType(RepaintBoundary).first,
matchesGoldenFile('clip.PhysicalShape.antiAlias.png'), matchesGoldenFile('clip.PhysicalShape.antiAlias.png'),
); );
}, skip: isBrowser); });
testWidgets('PhysicalShape painting with Clip.hardEdge', (WidgetTester tester) async { testWidgets('PhysicalShape painting with Clip.hardEdge', (WidgetTester tester) async {
await tester.pumpWidget(genPhysicalShape(Clip.hardEdge)); await tester.pumpWidget(genPhysicalShape(Clip.hardEdge));
...@@ -725,7 +725,7 @@ void main() { ...@@ -725,7 +725,7 @@ void main() {
find.byType(RepaintBoundary).first, find.byType(RepaintBoundary).first,
matchesGoldenFile('clip.PhysicalShape.hardEdge.png'), matchesGoldenFile('clip.PhysicalShape.hardEdge.png'),
); );
}, skip: isBrowser); });
testWidgets('PhysicalShape painting with Clip.antiAliasWithSaveLayer', (WidgetTester tester) async { testWidgets('PhysicalShape painting with Clip.antiAliasWithSaveLayer', (WidgetTester tester) async {
await tester.pumpWidget(genPhysicalShape(Clip.antiAliasWithSaveLayer)); await tester.pumpWidget(genPhysicalShape(Clip.antiAliasWithSaveLayer));
...@@ -733,7 +733,7 @@ void main() { ...@@ -733,7 +733,7 @@ void main() {
find.byType(RepaintBoundary).first, find.byType(RepaintBoundary).first,
matchesGoldenFile('clip.PhysicalShape.antiAliasWithSaveLayer.png'), matchesGoldenFile('clip.PhysicalShape.antiAliasWithSaveLayer.png'),
); );
}, skip: isBrowser); });
testWidgets('PhysicalShape painting', (WidgetTester tester) async { testWidgets('PhysicalShape painting', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
...@@ -779,7 +779,7 @@ void main() { ...@@ -779,7 +779,7 @@ void main() {
find.byType(RepaintBoundary).first, find.byType(RepaintBoundary).first,
matchesGoldenFile('clip.PhysicalShape.default.png'), matchesGoldenFile('clip.PhysicalShape.default.png'),
); );
}, skip: isBrowser); });
testWidgets('ClipPath.shape', (WidgetTester tester) async { testWidgets('ClipPath.shape', (WidgetTester tester) async {
final List<String> logs = <String>[]; final List<String> logs = <String>[];
......
...@@ -22,7 +22,7 @@ void main() { ...@@ -22,7 +22,7 @@ void main() {
find.byType(RepaintBoundary), find.byType(RepaintBoundary),
matchesGoldenFile('invert_colors_test.0.png'), matchesGoldenFile('invert_colors_test.0.png'),
); );
}, skip: isBrowser); });
testWidgets('InvertColors and ColorFilter', (WidgetTester tester) async { testWidgets('InvertColors and ColorFilter', (WidgetTester tester) async {
await tester.pumpWidget(const RepaintBoundary( await tester.pumpWidget(const RepaintBoundary(
...@@ -40,7 +40,7 @@ void main() { ...@@ -40,7 +40,7 @@ void main() {
find.byType(RepaintBoundary), find.byType(RepaintBoundary),
matchesGoldenFile('invert_colors_test.1.png'), matchesGoldenFile('invert_colors_test.1.png'),
); );
}, skip: isBrowser); });
} }
// Draws a rectangle sized by the parent widget with [color], [colorFilter], // Draws a rectangle sized by the parent widget with [color], [colorFilter],
......
...@@ -594,7 +594,7 @@ void main() { ...@@ -594,7 +594,7 @@ void main() {
find.byKey(const Key('list_wheel_scroll_view')), find.byKey(const Key('list_wheel_scroll_view')),
matchesGoldenFile('list_wheel_scroll_view.center_child.magnified.png'), matchesGoldenFile('list_wheel_scroll_view.center_child.magnified.png'),
); );
}, skip: isBrowser); });
testWidgets('Default middle transform', (WidgetTester tester) async { testWidgets('Default middle transform', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
...@@ -648,7 +648,7 @@ void main() { ...@@ -648,7 +648,7 @@ void main() {
find.byKey(const Key('list_wheel_scroll_view')), find.byKey(const Key('list_wheel_scroll_view')),
matchesGoldenFile('list_wheel_scroll_view.curved_wheel.left.png'), matchesGoldenFile('list_wheel_scroll_view.curved_wheel.left.png'),
); );
}, skip: isBrowser); });
testWidgets('Scrolling, diameterRatio, perspective all changes matrix', (WidgetTester tester) async { testWidgets('Scrolling, diameterRatio, perspective all changes matrix', (WidgetTester tester) async {
final ScrollController controller = ScrollController(initialScrollOffset: 200.0); final ScrollController controller = ScrollController(initialScrollOffset: 200.0);
......
...@@ -180,7 +180,7 @@ void main() { ...@@ -180,7 +180,7 @@ void main() {
find.byType(RepaintBoundary).first, find.byType(RepaintBoundary).first,
matchesGoldenFile('opacity_test.offset.png'), matchesGoldenFile('opacity_test.offset.png'),
); );
}, skip: isBrowser); });
testWidgets('empty opacity does not crash', (WidgetTester tester) async { testWidgets('empty opacity does not crash', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
......
...@@ -112,7 +112,7 @@ void main() { ...@@ -112,7 +112,7 @@ void main() {
find.byKey(key), find.byKey(key),
matchesGoldenFile('physical_model_overflow.png'), matchesGoldenFile('physical_model_overflow.png'),
); );
}, skip: isBrowser); });
group('PhysicalModelLayer checks elevation', () { group('PhysicalModelLayer checks elevation', () {
Future<void> _testStackChildren( Future<void> _testStackChildren(
......
...@@ -256,10 +256,8 @@ class FlutterWebPlatform extends PlatformPlugin { ...@@ -256,10 +256,8 @@ class FlutterWebPlatform extends PlatformPlugin {
Uint8List bytes; Uint8List bytes;
try { try {
final Runtime browser = Runtime.chrome; final ChromeTab chromeTab = await _browserManager._browser.chromeConnection.getTab((ChromeTab tab) {
final BrowserManager browserManager = await _browserManagerFor(browser); return tab.url.contains(_browserManager._browser.url);
final ChromeTab chromeTab = await browserManager._browser.chromeConnection.getTab((ChromeTab tab) {
return tab.url.contains(browserManager._browser.url);
}); });
final WipConnection connection = await chromeTab.connect(); final WipConnection connection = await chromeTab.connect();
final WipResponse response = await connection.sendCommand('Page.captureScreenshot', <String, Object>{ final WipResponse response = await connection.sendCommand('Page.captureScreenshot', <String, Object>{
...@@ -303,10 +301,7 @@ class FlutterWebPlatform extends PlatformPlugin { ...@@ -303,10 +301,7 @@ class FlutterWebPlatform extends PlatformPlugin {
bool get _closed => _closeMemo.hasRun; bool get _closed => _closeMemo.hasRun;
// A map from browser identifiers to futures that will complete to the BrowserManager _browserManager;
// [BrowserManager]s for those browsers, or `null` if they failed to load.
final Map<Runtime, Future<BrowserManager>> _browserManagers =
<Runtime, Future<BrowserManager>>{};
// A handler that serves wrapper files used to bootstrap tests. // A handler that serves wrapper files used to bootstrap tests.
shelf.Response _wrapperHandler(shelf.Request request) { shelf.Response _wrapperHandler(shelf.Request request) {
...@@ -330,6 +325,11 @@ class FlutterWebPlatform extends PlatformPlugin { ...@@ -330,6 +325,11 @@ class FlutterWebPlatform extends PlatformPlugin {
return shelf.Response.notFound('Not found.'); return shelf.Response.notFound('Not found.');
} }
/// Allows only one test suite (typically one test file) to be loaded and run
/// at any given point in time. Loading more than one file at a time is known
/// to lead to flaky tests.
final Pool _suiteLock = Pool(1);
@override @override
Future<RunnerSuite> load( Future<RunnerSuite> load(
String path, String path,
...@@ -340,17 +340,28 @@ class FlutterWebPlatform extends PlatformPlugin { ...@@ -340,17 +340,28 @@ class FlutterWebPlatform extends PlatformPlugin {
if (_closed) { if (_closed) {
return null; return null;
} }
final PoolResource lockResource = await _suiteLock.request();
final Runtime browser = platform.runtime; final Runtime browser = platform.runtime;
final BrowserManager browserManager = await _browserManagerFor(browser); try {
if (_closed || browserManager == null) { _browserManager = await _launchBrowser(browser);
} on Error catch (_) {
await _suiteLock.close();
rethrow;
}
if (_closed) {
return null; return null;
} }
final Uri suiteUrl = url.resolveUri(globals.fs.path.toUri(globals.fs.path.withoutExtension( final Uri suiteUrl = url.resolveUri(globals.fs.path.toUri(globals.fs.path.withoutExtension(
globals.fs.path.relative(path, from: globals.fs.path.join(_root, 'test'))) + globals.fs.path.relative(path, from: globals.fs.path.join(_root, 'test'))) +
'.html')); '.html'));
final RunnerSuite suite = await browserManager final RunnerSuite suite = await _browserManager.load(path, suiteUrl, suiteConfig, message, onDone: () async {
.load(path, suiteUrl, suiteConfig, message); await _browserManager.close();
_browserManager = null;
lockResource.release();
});
if (_closed) { if (_closed) {
return null; return null;
} }
...@@ -364,11 +375,11 @@ class FlutterWebPlatform extends PlatformPlugin { ...@@ -364,11 +375,11 @@ class FlutterWebPlatform extends PlatformPlugin {
/// Returns the [BrowserManager] for [runtime], which should be a browser. /// Returns the [BrowserManager] for [runtime], which should be a browser.
/// ///
/// If no browser manager is running yet, starts one. /// If no browser manager is running yet, starts one.
Future<BrowserManager> _browserManagerFor(Runtime browser) { Future<BrowserManager> _launchBrowser(Runtime browser) {
final Future<BrowserManager> managerFuture = _browserManagers[browser]; if (_browserManager != null) {
if (managerFuture != null) { throw StateError('Another browser is currently running.');
return managerFuture;
} }
final Completer<WebSocketChannel> completer = final Completer<WebSocketChannel> completer =
Completer<WebSocketChannel>.sync(); Completer<WebSocketChannel>.sync();
final String path = final String path =
...@@ -383,48 +394,29 @@ class FlutterWebPlatform extends PlatformPlugin { ...@@ -383,48 +394,29 @@ class FlutterWebPlatform extends PlatformPlugin {
globals.printTrace('Serving tests at $hostUrl'); globals.printTrace('Serving tests at $hostUrl');
final Future<BrowserManager> future = BrowserManager.start( return BrowserManager.start(
browser, browser,
hostUrl, hostUrl,
completer.future, completer.future,
headless: !_config.pauseAfterLoad, headless: !_config.pauseAfterLoad,
); );
// Store null values for browsers that error out so we know not to load them
// again.
_browserManagers[browser] = future.catchError((dynamic _) => null);
return future;
} }
@override @override
Future<void> closeEphemeral() { Future<void> closeEphemeral() async {
final List<Future<BrowserManager>> managers = if (_browserManager != null) {
_browserManagers.values.toList(); await _browserManager.close();
_browserManagers.clear(); }
return Future.wait(managers.map((Future<BrowserManager> manager) async {
final BrowserManager result = await manager;
if (result == null) {
return;
}
await result.close();
}));
} }
@override @override
Future<void> close() => _closeMemo.runOnce(() async { Future<void> close() => _closeMemo.runOnce(() async {
final List<Future<dynamic>> futures = _browserManagers.values await Future.wait<void>(<Future<dynamic>>[
.map<Future<dynamic>>((Future<BrowserManager> future) async { if (_browserManager != null)
final BrowserManager result = await future; _browserManager.close(),
if (result == null) { _server.close(),
return; _testGoldenComparator.close(),
} ]);
await result.close();
})
.toList();
futures.add(_server.close());
futures.add(_testGoldenComparator.close());
await Future.wait<void>(futures);
}); });
} }
...@@ -578,21 +570,6 @@ class BrowserManager { ...@@ -578,21 +570,6 @@ class BrowserManager {
/// This is connected to a page running `static/host.dart`. /// This is connected to a page running `static/host.dart`.
MultiChannel<dynamic> _channel; MultiChannel<dynamic> _channel;
/// A pool that ensures that limits the number of initial connections the
/// manager will wait for at once.
///
/// This isn't the *total* number of connections; any number of iframes may be
/// loaded in the same browser. However, the browser can only load so many at
/// once, and we want a timeout in case they fail so we only wait for so many
/// at once.
// The number 1 is chosen to disallow multiple iframes in the same browser. This
// is because in some environments, such as Cirrus CI, tests end up stuck and
// time out eventually. The exact reason for timeouts is unknown, but the
// hypothesis is that we were the first ones to attempt to run DDK-compiled
// tests concurrently in the browser. DDK is known to produce an order of
// magnitude bigger and somewhat slower code, which may overload the browser.
final Pool _pool = Pool(1);
/// The ID of the next suite to be loaded. /// The ID of the next suite to be loaded.
/// ///
/// This is used to ensure that the suites can be referred to consistently /// This is used to ensure that the suites can be referred to consistently
...@@ -654,8 +631,8 @@ class BrowserManager { ...@@ -654,8 +631,8 @@ class BrowserManager {
final Completer<BrowserManager> completer = Completer<BrowserManager>(); final Completer<BrowserManager> completer = Completer<BrowserManager>();
unawaited(chrome.onExit.then((void _) { unawaited(chrome.onExit.then((int browserExitCode) {
throwToolExit('${runtime.name} exited before connecting.'); throwToolExit('${runtime.name} exited with code $browserExitCode before connecting.');
}).catchError((dynamic error, StackTrace stackTrace) { }).catchError((dynamic error, StackTrace stackTrace) {
if (completer.isCompleted) { if (completer.isCompleted) {
return; return;
...@@ -700,7 +677,9 @@ class BrowserManager { ...@@ -700,7 +677,9 @@ class BrowserManager {
String path, String path,
Uri url, Uri url,
SuiteConfiguration suiteConfig, SuiteConfiguration suiteConfig,
Object message, Object message, {
Future<void> Function() onDone,
}
) async { ) async {
url = url.replace(fragment: Uri.encodeFull(jsonEncode(<String, Object>{ url = url.replace(fragment: Uri.encodeFull(jsonEncode(<String, Object>{
'metadata': suiteConfig.metadata.serialize(), 'metadata': suiteConfig.metadata.serialize(),
...@@ -726,29 +705,28 @@ class BrowserManager { ...@@ -726,29 +705,28 @@ class BrowserManager {
StreamTransformer<dynamic, dynamic>.fromHandlers(handleDone: (EventSink<dynamic> sink) { StreamTransformer<dynamic, dynamic>.fromHandlers(handleDone: (EventSink<dynamic> sink) {
closeIframe(); closeIframe();
sink.close(); sink.close();
onDone();
}), }),
); );
return await _pool.withResource<RunnerSuite>(() async { _channel.sink.add(<String, Object>{
_channel.sink.add(<String, Object>{ 'command': 'loadSuite',
'command': 'loadSuite', 'url': url.toString(),
'url': url.toString(), 'id': suiteID,
'id': suiteID, 'channel': suiteChannelID,
'channel': suiteChannelID, });
});
try { try {
controller = deserializeSuite(path, SuitePlatform(Runtime.chrome), controller = deserializeSuite(path, SuitePlatform(Runtime.chrome),
suiteConfig, await _environment, suiteChannel, message); suiteConfig, await _environment, suiteChannel, message);
_controllers.add(controller); _controllers.add(controller);
return await controller.suite; return await controller.suite;
// Not limiting to catching Exception because the exception is rethrown. // Not limiting to catching Exception because the exception is rethrown.
} catch (_) { // ignore: avoid_catches_without_on_clauses } catch (_) { // ignore: avoid_catches_without_on_clauses
closeIframe(); closeIframe();
rethrow; rethrow;
} }
});
} }
/// An implementation of [Environment.displayPause]. /// An implementation of [Environment.displayPause].
......
...@@ -116,6 +116,10 @@ class ChromeLauncher { ...@@ -116,6 +116,10 @@ class ChromeLauncher {
/// ///
/// `skipCheck` does not attempt to make a devtools connection before returning. /// `skipCheck` does not attempt to make a devtools connection before returning.
Future<Chrome> launch(String url, { bool headless = false, int debugPort, bool skipCheck = false, Directory dataDir }) async { Future<Chrome> launch(String url, { bool headless = false, int debugPort, bool skipCheck = false, Directory dataDir }) async {
if (_currentCompleter.isCompleted) {
throwToolExit('Only one instance of chrome can be started.');
}
// This is a JSON file which contains configuration from the // This is a JSON file which contains configuration from the
// browser session, such as window position. It is located // browser session, such as window position. It is located
// under the Chrome data-dir folder. // under the Chrome data-dir folder.
...@@ -203,9 +207,6 @@ class ChromeLauncher { ...@@ -203,9 +207,6 @@ class ChromeLauncher {
} }
static Future<Chrome> _connect(Chrome chrome, bool skipCheck) async { static Future<Chrome> _connect(Chrome chrome, bool skipCheck) async {
if (_currentCompleter.isCompleted) {
throwToolExit('Only one instance of chrome can be started.');
}
// The connection is lazy. Try a simple call to make sure the provided // The connection is lazy. Try a simple call to make sure the provided
// connection is valid. // connection is valid.
if (!skipCheck) { if (!skipCheck) {
...@@ -262,13 +263,11 @@ class Chrome { ...@@ -262,13 +263,11 @@ class Chrome {
final ChromeConnection chromeConnection; final ChromeConnection chromeConnection;
final Uri remoteDebuggerUri; final Uri remoteDebuggerUri;
static Completer<Chrome> _currentCompleter = Completer<Chrome>(); Future<int> get onExit => _process.exitCode;
Future<void> get onExit => _currentCompleter.future;
Future<void> close() async { Future<void> close() async {
if (_currentCompleter.isCompleted) { if (ChromeLauncher.hasChromeInstance) {
_currentCompleter = Completer<Chrome>(); ChromeLauncher._currentCompleter = Completer<Chrome>();
} }
chromeConnection.close(); chromeConnection.close();
_process?.kill(); _process?.kill();
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import 'dart:async'; import 'dart:async';
import 'package:file/memory.dart'; import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/common.dart';
import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/os.dart'; import 'package:flutter_tools/src/base/os.dart';
...@@ -62,28 +63,31 @@ void main() { ...@@ -62,28 +63,31 @@ void main() {
}); });
test('can launch chrome and connect to the devtools', () async { test('can launch chrome and connect to the devtools', () async {
processManager.addCommand(const FakeCommand( await testLaunchChrome('/.tmp_rand0/flutter_tool.rand0', processManager, chromeLauncher);
command: <String>[ });
'example_chrome',
'--user-data-dir=/.tmp_rand0/flutter_tool.rand0',
'--remote-debugging-port=1234',
..._kChromeArgs,
'example_url',
],
stderr: kDevtoolsStderr,
));
await chromeLauncher.launch( test('cannot have two concurrent instances of chrome', () async {
'example_url', await testLaunchChrome('/.tmp_rand0/flutter_tool.rand0', processManager, chromeLauncher);
skipCheck: true, bool pass = false;
); try {
await testLaunchChrome('/.tmp_rand0/flutter_tool.rand1', processManager, chromeLauncher);
} on ToolExit catch (_) {
pass = true;
}
expect(pass, isTrue);
});
test('can launch new chrome after stopping a previous chrome', () async {
final Chrome chrome = await testLaunchChrome('/.tmp_rand0/flutter_tool.rand0', processManager, chromeLauncher);
await chrome.close();
await testLaunchChrome('/.tmp_rand0/flutter_tool.rand1', processManager, chromeLauncher);
}); });
test('can launch chrome with a custom debug port', () async { test('can launch chrome with a custom debug port', () async {
processManager.addCommand(const FakeCommand( processManager.addCommand(const FakeCommand(
command: <String>[ command: <String>[
'example_chrome', 'example_chrome',
'--user-data-dir=/.tmp_rand0/flutter_tool.rand0', '--user-data-dir=/.tmp_rand1/flutter_tool.rand1',
'--remote-debugging-port=10000', '--remote-debugging-port=10000',
..._kChromeArgs, ..._kChromeArgs,
'example_url', 'example_url',
...@@ -102,7 +106,7 @@ void main() { ...@@ -102,7 +106,7 @@ void main() {
processManager.addCommand(const FakeCommand( processManager.addCommand(const FakeCommand(
command: <String>[ command: <String>[
'example_chrome', 'example_chrome',
'--user-data-dir=/.tmp_rand0/flutter_tool.rand0', '--user-data-dir=/.tmp_rand1/flutter_tool.rand1',
'--remote-debugging-port=1234', '--remote-debugging-port=1234',
..._kChromeArgs, ..._kChromeArgs,
'--headless', '--headless',
...@@ -133,7 +137,7 @@ void main() { ...@@ -133,7 +137,7 @@ void main() {
processManager.addCommand(FakeCommand(command: const <String>[ processManager.addCommand(FakeCommand(command: const <String>[
'example_chrome', 'example_chrome',
'--user-data-dir=/.tmp_rand0/flutter_tool.rand0', '--user-data-dir=/.tmp_rand1/flutter_tool.rand1',
'--remote-debugging-port=1234', '--remote-debugging-port=1234',
..._kChromeArgs, ..._kChromeArgs,
'example_url', 'example_url',
...@@ -146,7 +150,7 @@ void main() { ...@@ -146,7 +150,7 @@ void main() {
); );
final File tempFile = fileSystem final File tempFile = fileSystem
.directory('.tmp_rand0/flutter_tool.rand0') .directory('.tmp_rand1/flutter_tool.rand1')
.childDirectory('Default') .childDirectory('Default')
.childFile('preferences'); .childFile('preferences');
...@@ -163,3 +167,21 @@ void main() { ...@@ -163,3 +167,21 @@ void main() {
} }
class MockOperatingSystemUtils extends Mock implements OperatingSystemUtils {} class MockOperatingSystemUtils extends Mock implements OperatingSystemUtils {}
Future<Chrome> testLaunchChrome(String userDataDir, FakeProcessManager processManager, ChromeLauncher chromeLauncher) {
processManager.addCommand(FakeCommand(
command: <String>[
'example_chrome',
'--user-data-dir=$userDataDir',
'--remote-debugging-port=1234',
..._kChromeArgs,
'example_url',
],
stderr: kDevtoolsStderr,
));
return chromeLauncher.launch(
'example_url',
skipCheck: true,
);
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment