Unverified Commit 7bc02037 authored by Kate Lovett's avatar Kate Lovett Committed by GitHub

Re-land Local & Pre-Submit Support for Skia Gold (#43371)

parent 4bd5eb59
65bd07204149c4f7612bbf179cf088a2d69ca549
...@@ -45,7 +45,7 @@ void main() { ...@@ -45,7 +45,7 @@ void main() {
await expectLater( await expectLater(
find.byKey(key), find.byKey(key),
matchesGoldenFile('activityIndicator.paused.light.png', version: 0), matchesGoldenFile('activityIndicator.paused.light.png'),
); );
await tester.pumpWidget( await tester.pumpWidget(
...@@ -65,7 +65,7 @@ void main() { ...@@ -65,7 +65,7 @@ void main() {
await expectLater( await expectLater(
find.byKey(key), find.byKey(key),
matchesGoldenFile('activityIndicator.paused.dark.png', version: 0), matchesGoldenFile('activityIndicator.paused.dark.png'),
); );
}); });
......
...@@ -922,10 +922,7 @@ void main() { ...@@ -922,10 +922,7 @@ void main() {
await expectLater( await expectLater(
find.byType(CupertinoDatePicker), find.byType(CupertinoDatePicker),
matchesGoldenFile( matchesGoldenFile('date_picker_test.datetime.initial.png'),
'date_picker_test.datetime.initial.png',
version: 2,
),
); );
// Slightly drag the hour component to make the current hour off-center. // Slightly drag the hour component to make the current hour off-center.
...@@ -934,10 +931,7 @@ void main() { ...@@ -934,10 +931,7 @@ void main() {
await expectLater( await expectLater(
find.byType(CupertinoDatePicker), find.byType(CupertinoDatePicker),
matchesGoldenFile( matchesGoldenFile('date_picker_test.datetime.drag.png'),
'date_picker_test.datetime.drag.png',
version: 2,
),
); );
}); });
}); });
...@@ -971,10 +965,7 @@ void main() { ...@@ -971,10 +965,7 @@ void main() {
await expectLater( await expectLater(
find.byType(CupertinoTimerPicker), find.byType(CupertinoTimerPicker),
matchesGoldenFile( matchesGoldenFile('timer_picker_test.datetime.initial.png'),
'timer_picker_test.datetime.initial.png',
version: 1,
),
); );
// Slightly drag the minute component to make the current minute off-center. // Slightly drag the minute component to make the current minute off-center.
...@@ -983,10 +974,7 @@ void main() { ...@@ -983,10 +974,7 @@ void main() {
await expectLater( await expectLater(
find.byType(CupertinoTimerPicker), find.byType(CupertinoTimerPicker),
matchesGoldenFile( matchesGoldenFile('timer_picker_test.datetime.drag.png'),
'timer_picker_test.datetime.drag.png',
version: 1,
),
); );
}); });
......
...@@ -820,10 +820,7 @@ void main() { ...@@ -820,10 +820,7 @@ void main() {
await expectLater( await expectLater(
find.byType(RepaintBoundary).last, find.byType(RepaintBoundary).last,
matchesGoldenFile( matchesGoldenFile('nav_bar_test.standard_title.png'),
'nav_bar_test.standard_title.png',
version: 2,
),
); );
}, },
); );
...@@ -854,10 +851,7 @@ void main() { ...@@ -854,10 +851,7 @@ void main() {
await expectLater( await expectLater(
find.byType(RepaintBoundary).last, find.byType(RepaintBoundary).last,
matchesGoldenFile( matchesGoldenFile('nav_bar_test.large_title.png'),
'nav_bar_test.large_title.png',
version: 2,
),
); );
}, },
); );
......
...@@ -1414,10 +1414,7 @@ void main() { ...@@ -1414,10 +1414,7 @@ void main() {
await expectLater( await expectLater(
find.byType(RepaintBoundary), find.byType(RepaintBoundary),
matchesGoldenFile( matchesGoldenFile('segmented_control_test.0.png'),
'segmented_control_test.0.png',
version: 0,
),
); );
}); });
...@@ -1455,10 +1452,7 @@ void main() { ...@@ -1455,10 +1452,7 @@ void main() {
await expectLater( await expectLater(
find.byType(RepaintBoundary), find.byType(RepaintBoundary),
matchesGoldenFile( matchesGoldenFile('segmented_control_test.1.png'),
'segmented_control_test.1.png',
version: 0,
),
); );
}); });
} }
...@@ -542,10 +542,7 @@ void main() { ...@@ -542,10 +542,7 @@ void main() {
await expectLater( await expectLater(
find.byKey(switchKey), find.byKey(switchKey),
matchesGoldenFile( matchesGoldenFile('switch.tap.off.png'),
'switch.tap.off.png',
version: 1,
),
); );
await tester.tap(find.byKey(switchKey)); await tester.tap(find.byKey(switchKey));
...@@ -556,19 +553,13 @@ void main() { ...@@ -556,19 +553,13 @@ void main() {
await tester.pump(const Duration(milliseconds: 60)); await tester.pump(const Duration(milliseconds: 60));
await expectLater( await expectLater(
find.byKey(switchKey), find.byKey(switchKey),
matchesGoldenFile( matchesGoldenFile('switch.tap.turningOn.png'),
'switch.tap.turningOn.png',
version: 1,
),
); );
await tester.pumpAndSettle(); await tester.pumpAndSettle();
await expectLater( await expectLater(
find.byKey(switchKey), find.byKey(switchKey),
matchesGoldenFile( matchesGoldenFile('switch.tap.on.png'),
'switch.tap.on.png',
version: 1,
),
); );
}); });
...@@ -604,10 +595,7 @@ void main() { ...@@ -604,10 +595,7 @@ void main() {
await expectLater( await expectLater(
find.byKey(switchKey), find.byKey(switchKey),
matchesGoldenFile( matchesGoldenFile('switch.tap.off.dark.png'),
'switch.tap.off.dark.png',
version: 0,
),
); );
await tester.tap(find.byKey(switchKey)); await tester.tap(find.byKey(switchKey));
...@@ -616,10 +604,7 @@ void main() { ...@@ -616,10 +604,7 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
await expectLater( await expectLater(
find.byKey(switchKey), find.byKey(switchKey),
matchesGoldenFile( matchesGoldenFile('switch.tap.on.dark.png'),
'switch.tap.on.dark.png',
version: 0,
),
); );
}); });
} }
...@@ -503,10 +503,7 @@ void main() { ...@@ -503,10 +503,7 @@ void main() {
await expectLater( await expectLater(
find.byKey(const ValueKey<int>(1)), find.byKey(const ValueKey<int>(1)),
matchesGoldenFile( matchesGoldenFile('text_field_cursor_test.cupertino.0.png'),
'text_field_cursor_test.cupertino.0.png',
version: 3,
),
); );
}); });
...@@ -536,10 +533,7 @@ void main() { ...@@ -536,10 +533,7 @@ void main() {
debugDefaultTargetPlatformOverride = null; debugDefaultTargetPlatformOverride = null;
await expectLater( await expectLater(
find.byKey(const ValueKey<int>(1)), find.byKey(const ValueKey<int>(1)),
matchesGoldenFile( matchesGoldenFile('text_field_cursor_test.cupertino.1.png'),
'text_field_cursor_test.cupertino.1.png',
version: 3,
),
); );
}); });
...@@ -3042,10 +3036,7 @@ void main() { ...@@ -3042,10 +3036,7 @@ void main() {
await expectLater( await expectLater(
find.byKey(const ValueKey<int>(1)), find.byKey(const ValueKey<int>(1)),
matchesGoldenFile( matchesGoldenFile('text_field_test.disabled.png'),
'text_field_test.disabled.png',
version: 1,
),
); );
}); });
......
...@@ -71,19 +71,13 @@ void main() { ...@@ -71,19 +71,13 @@ void main() {
await pump(FloatingActionButtonLocation.endDocked); await pump(FloatingActionButtonLocation.endDocked);
await expectLater( await expectLater(
find.byKey(key), find.byKey(key),
matchesGoldenFile( matchesGoldenFile('bottom_app_bar.custom_shape.1.png'),
'bottom_app_bar.custom_shape.1.png',
version: null,
),
); );
await pump(FloatingActionButtonLocation.centerDocked); await pump(FloatingActionButtonLocation.centerDocked);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
await expectLater( await expectLater(
find.byKey(key), find.byKey(key),
matchesGoldenFile( matchesGoldenFile('bottom_app_bar.custom_shape.2.png'),
'bottom_app_bar.custom_shape.2.png',
version: null,
),
); );
}, skip: isBrowser); }, skip: isBrowser);
......
...@@ -80,10 +80,7 @@ void main() { ...@@ -80,10 +80,7 @@ void main() {
await expectLater( await expectLater(
find.byKey(_painterKey), find.byKey(_painterKey),
matchesGoldenFile( matchesGoldenFile('bottom_app_bar_theme.custom_shape.png'),
'bottom_app_bar_theme.custom_shape.png',
version: null,
),
); );
}, skip: isBrowser); }, skip: isBrowser);
......
...@@ -1426,10 +1426,7 @@ void main() { ...@@ -1426,10 +1426,7 @@ void main() {
await tester.pump(const Duration(milliseconds: 30)); await tester.pump(const Duration(milliseconds: 30));
await expectLater( await expectLater(
find.byType(BottomNavigationBar), find.byType(BottomNavigationBar),
matchesGoldenFile( matchesGoldenFile('bottom_navigation_bar.shifting_transition.$pump.png'),
'bottom_navigation_bar.shifting_transition.$pump.png',
version: 2,
),
); );
} }
}, skip: isBrowser); }, skip: isBrowser);
......
...@@ -137,10 +137,7 @@ void main() { ...@@ -137,10 +137,7 @@ void main() {
await expectLater( await expectLater(
find.byKey(painterKey), find.byKey(painterKey),
matchesGoldenFile( matchesGoldenFile('card_theme.custom_shape.png'),
'card_theme.custom_shape.png',
version: null,
),
); );
}, skip: isBrowser); }, skip: isBrowser);
} }
......
...@@ -130,10 +130,7 @@ void main() { ...@@ -130,10 +130,7 @@ void main() {
await expectLater( await expectLater(
find.byKey(_painterKey), find.byKey(_painterKey),
matchesGoldenFile( matchesGoldenFile('dialog_theme.dialog_with_custom_border.png'),
'dialog_theme.dialog_with_custom_border.png',
version: null,
),
); );
}, skip: isBrowser); }, skip: isBrowser);
......
...@@ -227,10 +227,7 @@ void main() { ...@@ -227,10 +227,7 @@ void main() {
assert(tester.renderObject(buttonFinder).attached); assert(tester.renderObject(buttonFinder).attached);
await expectLater( await expectLater(
find.ancestor(of: buttonFinder, matching: find.byType(RepaintBoundary)).first, find.ancestor(of: buttonFinder, matching: find.byType(RepaintBoundary)).first,
matchesGoldenFile( matchesGoldenFile('dropdown_test.default.png'),
'dropdown_test.default.png',
version: 0,
),
); );
}, skip: isBrowser); }, skip: isBrowser);
...@@ -242,10 +239,7 @@ void main() { ...@@ -242,10 +239,7 @@ void main() {
assert(tester.renderObject(buttonFinder).attached); assert(tester.renderObject(buttonFinder).attached);
await expectLater( await expectLater(
find.ancestor(of: buttonFinder, matching: find.byType(RepaintBoundary)).first, find.ancestor(of: buttonFinder, matching: find.byType(RepaintBoundary)).first,
matchesGoldenFile( matchesGoldenFile('dropdown_test.expanded.png'),
'dropdown_test.expanded.png',
version: 0,
),
); );
}, skip: isBrowser); }, skip: isBrowser);
......
...@@ -740,10 +740,7 @@ void main() { ...@@ -740,10 +740,7 @@ void main() {
await tester.pump(const Duration(milliseconds: 1000)); await tester.pump(const Duration(milliseconds: 1000));
await expectLater( await expectLater(
find.byKey(key), find.byKey(key),
matchesGoldenFile( matchesGoldenFile('floating_action_button_test.clip.png'),
'floating_action_button_test.clip.png',
version: 2,
),
); );
}); });
......
...@@ -2960,19 +2960,13 @@ void main() { ...@@ -2960,19 +2960,13 @@ void main() {
await tester.pumpWidget(buildFrame(TextDirection.ltr)); await tester.pumpWidget(buildFrame(TextDirection.ltr));
await expectLater( await expectLater(
find.byType(InputDecorator), find.byType(InputDecorator),
matchesGoldenFile( matchesGoldenFile('input_decorator.outline_icon_label.ltr.png'),
'input_decorator.outline_icon_label.ltr.png',
version: null,
),
); );
await tester.pumpWidget(buildFrame(TextDirection.rtl)); await tester.pumpWidget(buildFrame(TextDirection.rtl));
await expectLater( await expectLater(
find.byType(InputDecorator), find.byType(InputDecorator),
matchesGoldenFile( matchesGoldenFile('input_decorator.outline_icon_label.rtl.png'),
'input_decorator.outline_icon_label.rtl.png',
version: null,
),
); );
}, },
); );
......
...@@ -715,10 +715,7 @@ void main() { ...@@ -715,10 +715,7 @@ void main() {
await expectLater( await expectLater(
find.byKey(painterKey), find.byKey(painterKey),
matchesGoldenFile( matchesGoldenFile('material.border_paint_above.png'),
'material.border_paint_above.png',
version: null,
),
); );
}, skip: isBrowser); }, skip: isBrowser);
...@@ -758,10 +755,7 @@ void main() { ...@@ -758,10 +755,7 @@ void main() {
await expectLater( await expectLater(
find.byKey(painterKey), find.byKey(painterKey),
matchesGoldenFile( matchesGoldenFile('material.border_paint_below.png'),
'material.border_paint_below.png',
version: null,
),
); );
}, skip: isBrowser); }, skip: isBrowser);
}); });
......
...@@ -276,10 +276,7 @@ void main() { ...@@ -276,10 +276,7 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
await expectLater( await expectLater(
find.byKey(painterKey), find.byKey(painterKey),
matchesGoldenFile( matchesGoldenFile('radio.ink_ripple.png'),
'radio.ink_ripple.png',
version: null,
),
); );
}, skip: isBrowser); }, skip: isBrowser);
} }
......
...@@ -267,10 +267,7 @@ void main() { ...@@ -267,10 +267,7 @@ void main() {
await expectLater( await expectLater(
find.byKey(_painterKey), find.byKey(_painterKey),
matchesGoldenFile( matchesGoldenFile('tab_bar_theme.tab_indicator_size_tab.png'),
'tab_bar_theme.tab_indicator_size_tab.png',
version: null,
),
); );
}, skip: isBrowser); }, skip: isBrowser);
...@@ -281,10 +278,7 @@ void main() { ...@@ -281,10 +278,7 @@ void main() {
await expectLater( await expectLater(
find.byKey(_painterKey), find.byKey(_painterKey),
matchesGoldenFile( matchesGoldenFile('tab_bar_theme.tab_indicator_size_label.png'),
'tab_bar_theme.tab_indicator_size_label.png',
version: null,
),
); );
}, skip: isBrowser); }, skip: isBrowser);
...@@ -300,10 +294,7 @@ void main() { ...@@ -300,10 +294,7 @@ void main() {
await expectLater( await expectLater(
find.byKey(_painterKey), find.byKey(_painterKey),
matchesGoldenFile( matchesGoldenFile('tab_bar_theme.custom_tab_indicator.png'),
'tab_bar_theme.custom_tab_indicator.png',
version: null,
),
); );
}, skip: isBrowser); }, skip: isBrowser);
...@@ -319,10 +310,7 @@ void main() { ...@@ -319,10 +310,7 @@ void main() {
await expectLater( await expectLater(
find.byKey(_painterKey), find.byKey(_painterKey),
matchesGoldenFile( matchesGoldenFile('tab_bar_theme.beveled_rect_indicator.png'),
'tab_bar_theme.beveled_rect_indicator.png',
version: null,
),
); );
}, skip: isBrowser); }, skip: isBrowser);
} }
...@@ -412,10 +412,7 @@ void main() { ...@@ -412,10 +412,7 @@ void main() {
await expectLater( await expectLater(
find.byKey(const ValueKey<int>(1)), find.byKey(const ValueKey<int>(1)),
matchesGoldenFile( matchesGoldenFile('text_field_cursor_test.material.0.png'),
'text_field_cursor_test.material.0.png',
version: 0,
),
); );
}); });
...@@ -444,10 +441,7 @@ void main() { ...@@ -444,10 +441,7 @@ void main() {
debugDefaultTargetPlatformOverride = null; debugDefaultTargetPlatformOverride = null;
await expectLater( await expectLater(
find.byKey(const ValueKey<int>(1)), find.byKey(const ValueKey<int>(1)),
matchesGoldenFile( matchesGoldenFile('text_field_cursor_test.material.1.png'),
'text_field_cursor_test.material.1.png',
version: 0,
),
); );
}); });
...@@ -498,10 +492,7 @@ void main() { ...@@ -498,10 +492,7 @@ void main() {
await expectLater( await expectLater(
// The toolbar exists in the Overlay above the MaterialApp. // The toolbar exists in the Overlay above the MaterialApp.
find.byType(Overlay), find.byType(Overlay),
matchesGoldenFile( matchesGoldenFile('text_field_opacity_test.0.png'),
'text_field_opacity_test.0.png',
version: 3,
),
); );
}, skip: isBrowser); }, skip: isBrowser);
......
...@@ -71,10 +71,7 @@ void main() { ...@@ -71,10 +71,7 @@ void main() {
await expectLater( await expectLater(
find.byType(RepaintBoundary), find.byType(RepaintBoundary),
matchesGoldenFile( matchesGoldenFile('continuous_rectangle_border.golden_test_even_radii.png'),
'continuous_rectangle_border.golden_test_even_radii.png',
version: null,
),
); );
}, skip: isBrowser); }, skip: isBrowser);
...@@ -95,10 +92,7 @@ void main() { ...@@ -95,10 +92,7 @@ void main() {
await expectLater( await expectLater(
find.byType(RepaintBoundary), find.byType(RepaintBoundary),
matchesGoldenFile( matchesGoldenFile('continuous_rectangle_border.golden_test_varying_radii.png'),
'continuous_rectangle_border.golden_test_varying_radii.png',
version: null,
),
); );
}, skip: isBrowser); }, skip: isBrowser);
...@@ -116,10 +110,7 @@ void main() { ...@@ -116,10 +110,7 @@ void main() {
await expectLater( await expectLater(
find.byType(RepaintBoundary), find.byType(RepaintBoundary),
matchesGoldenFile( matchesGoldenFile('continuous_rectangle_border.golden_test_large_radii.png'),
'continuous_rectangle_border.golden_test_large_radii.png',
version: null,
),
); );
}, skip: isBrowser); }, skip: isBrowser);
......
...@@ -806,7 +806,10 @@ void main() { ...@@ -806,7 +806,10 @@ void main() {
), ),
), ),
)); ));
await expectLater(find.byKey(painterKey), matchesGoldenFile(goldenName)); await expectLater(
find.byKey(painterKey),
matchesGoldenFile(goldenName),
);
} }
testWidgets('Gradients - 45 degrees', (WidgetTester tester) async { testWidgets('Gradients - 45 degrees', (WidgetTester tester) async {
......
...@@ -49,10 +49,7 @@ void main() { ...@@ -49,10 +49,7 @@ void main() {
await expectLater( await expectLater(
find.byType(RichText), find.byType(RichText),
matchesGoldenFile( matchesGoldenFile('localized_fonts.rich_text.styled_text_span.png'),
'localized_fonts.rich_text.styled_text_span.png',
version: null,
),
); );
}, },
skip: isBrowser, // TODO(yjbanov): implement goldens on the Web: https://github.com/flutter/flutter/issues/40297 skip: isBrowser, // TODO(yjbanov): implement goldens on the Web: https://github.com/flutter/flutter/issues/40297
...@@ -104,10 +101,7 @@ void main() { ...@@ -104,10 +101,7 @@ void main() {
await expectLater( await expectLater(
find.byType(Row), find.byType(Row),
matchesGoldenFile( matchesGoldenFile('localized_fonts.text_ambient_locale.chars.png'),
'localized_fonts.text_ambient_locale.chars.png',
version: null,
),
); );
}, },
skip: isBrowser, // TODO(yjbanov): implement goldens on the Web: https://github.com/flutter/flutter/issues/40297 skip: isBrowser, // TODO(yjbanov): implement goldens on the Web: https://github.com/flutter/flutter/issues/40297
...@@ -151,10 +145,7 @@ void main() { ...@@ -151,10 +145,7 @@ void main() {
await expectLater( await expectLater(
find.byType(Row), find.byType(Row),
matchesGoldenFile( matchesGoldenFile('localized_fonts.text_explicit_locale.chars.png'),
'localized_fonts.text_explicit_locale.chars.png',
version: null,
),
); );
}, },
skip: isBrowser, // TODO(yjbanov): implement goldens on the Web: https://github.com/flutter/flutter/issues/40297 skip: isBrowser, // TODO(yjbanov): implement goldens on the Web: https://github.com/flutter/flutter/issues/40297
......
...@@ -43,10 +43,7 @@ void main() { ...@@ -43,10 +43,7 @@ void main() {
); );
await expectLater( await expectLater(
find.byType(RepaintBoundary).first, find.byType(RepaintBoundary).first,
matchesGoldenFile( matchesGoldenFile('backdrop_filter_test.cull_rect.png'),
'backdrop_filter_test.cull_rect.png',
version: 1,
),
); );
}, skip: isBrowser); }, skip: isBrowser);
} }
...@@ -381,10 +381,7 @@ void main() { ...@@ -381,10 +381,7 @@ void main() {
); );
await expectLater( await expectLater(
find.byType(RepaintBoundary).first, find.byType(RepaintBoundary).first,
matchesGoldenFile( matchesGoldenFile('clip.ClipRect.png'),
'clip.ClipRect.png',
version: 1,
),
); );
}, skip: isBrowser); }, skip: isBrowser);
...@@ -424,10 +421,7 @@ void main() { ...@@ -424,10 +421,7 @@ void main() {
); );
await expectLater( await expectLater(
find.byType(RepaintBoundary).first, find.byType(RepaintBoundary).first,
matchesGoldenFile( matchesGoldenFile('clip.ClipRectOverlay.png'),
'clip.ClipRectOverlay.png',
version: 1,
),
); );
}, skip: isBrowser); }, skip: isBrowser);
...@@ -476,10 +470,7 @@ void main() { ...@@ -476,10 +470,7 @@ void main() {
); );
await expectLater( await expectLater(
find.byType(RepaintBoundary).first, find.byType(RepaintBoundary).first,
matchesGoldenFile( matchesGoldenFile('clip.ClipRRect.png'),
'clip.ClipRRect.png',
version: 1,
),
); );
}, skip: isBrowser); }, skip: isBrowser);
...@@ -522,10 +513,7 @@ void main() { ...@@ -522,10 +513,7 @@ void main() {
); );
await expectLater( await expectLater(
find.byType(RepaintBoundary).first, find.byType(RepaintBoundary).first,
matchesGoldenFile( matchesGoldenFile('clip.ClipOval.png'),
'clip.ClipOval.png',
version: 1,
),
); );
}, skip: isBrowser); }, skip: isBrowser);
...@@ -573,10 +561,7 @@ void main() { ...@@ -573,10 +561,7 @@ void main() {
); );
await expectLater( await expectLater(
find.byType(RepaintBoundary).first, find.byType(RepaintBoundary).first,
matchesGoldenFile( matchesGoldenFile('clip.ClipPath.png'),
'clip.ClipPath.png',
version: 1,
),
); );
}, skip: isBrowser); }, skip: isBrowser);
...@@ -621,10 +606,7 @@ void main() { ...@@ -621,10 +606,7 @@ void main() {
await tester.pumpWidget(genPhysicalModel(Clip.antiAlias)); await tester.pumpWidget(genPhysicalModel(Clip.antiAlias));
await expectLater( await expectLater(
find.byType(RepaintBoundary).first, find.byType(RepaintBoundary).first,
matchesGoldenFile( matchesGoldenFile('clip.PhysicalModel.antiAlias.png'),
'clip.PhysicalModel.antiAlias.png',
version: 1,
),
); );
}, skip: isBrowser); }, skip: isBrowser);
...@@ -632,10 +614,7 @@ void main() { ...@@ -632,10 +614,7 @@ void main() {
await tester.pumpWidget(genPhysicalModel(Clip.hardEdge)); await tester.pumpWidget(genPhysicalModel(Clip.hardEdge));
await expectLater( await expectLater(
find.byType(RepaintBoundary).first, find.byType(RepaintBoundary).first,
matchesGoldenFile( matchesGoldenFile('clip.PhysicalModel.hardEdge.png'),
'clip.PhysicalModel.hardEdge.png',
version: 1,
),
); );
}, skip: isBrowser); }, skip: isBrowser);
...@@ -645,10 +624,7 @@ void main() { ...@@ -645,10 +624,7 @@ void main() {
await tester.pumpWidget(genPhysicalModel(Clip.antiAliasWithSaveLayer)); await tester.pumpWidget(genPhysicalModel(Clip.antiAliasWithSaveLayer));
await expectLater( await expectLater(
find.byType(RepaintBoundary).first, find.byType(RepaintBoundary).first,
matchesGoldenFile( matchesGoldenFile('clip.PhysicalModel.antiAliasWithSaveLayer.png'),
'clip.PhysicalModel.antiAliasWithSaveLayer.png',
version: null,
),
); );
}, skip: isBrowser); }, skip: isBrowser);
...@@ -690,10 +666,7 @@ void main() { ...@@ -690,10 +666,7 @@ void main() {
); );
await expectLater( await expectLater(
find.byType(RepaintBoundary).first, find.byType(RepaintBoundary).first,
matchesGoldenFile( matchesGoldenFile('clip.PhysicalModel.default.png'),
'clip.PhysicalModel.default.png',
version: 1,
),
); );
}, skip: isBrowser); }, skip: isBrowser);
...@@ -742,10 +715,7 @@ void main() { ...@@ -742,10 +715,7 @@ void main() {
await tester.pumpWidget(genPhysicalShape(Clip.antiAlias)); await tester.pumpWidget(genPhysicalShape(Clip.antiAlias));
await expectLater( await expectLater(
find.byType(RepaintBoundary).first, find.byType(RepaintBoundary).first,
matchesGoldenFile( matchesGoldenFile('clip.PhysicalShape.antiAlias.png'),
'clip.PhysicalShape.antiAlias.png',
version: 1,
),
); );
}, skip: isBrowser); }, skip: isBrowser);
...@@ -753,10 +723,7 @@ void main() { ...@@ -753,10 +723,7 @@ void main() {
await tester.pumpWidget(genPhysicalShape(Clip.hardEdge)); await tester.pumpWidget(genPhysicalShape(Clip.hardEdge));
await expectLater( await expectLater(
find.byType(RepaintBoundary).first, find.byType(RepaintBoundary).first,
matchesGoldenFile( matchesGoldenFile('clip.PhysicalShape.hardEdge.png'),
'clip.PhysicalShape.hardEdge.png',
version: 1,
),
); );
}, skip: isBrowser); }, skip: isBrowser);
...@@ -764,10 +731,7 @@ void main() { ...@@ -764,10 +731,7 @@ void main() {
await tester.pumpWidget(genPhysicalShape(Clip.antiAliasWithSaveLayer)); await tester.pumpWidget(genPhysicalShape(Clip.antiAliasWithSaveLayer));
await expectLater( await expectLater(
find.byType(RepaintBoundary).first, find.byType(RepaintBoundary).first,
matchesGoldenFile( matchesGoldenFile('clip.PhysicalShape.antiAliasWithSaveLayer.png'),
'clip.PhysicalShape.antiAliasWithSaveLayer.png',
version: null,
),
); );
}, skip: isBrowser); }, skip: isBrowser);
...@@ -813,10 +777,7 @@ void main() { ...@@ -813,10 +777,7 @@ void main() {
); );
await expectLater( await expectLater(
find.byType(RepaintBoundary).first, find.byType(RepaintBoundary).first,
matchesGoldenFile( matchesGoldenFile('clip.PhysicalShape.default.png'),
'clip.PhysicalShape.default.png',
version: 1,
),
); );
}, skip: isBrowser); }, skip: isBrowser);
......
...@@ -18,10 +18,7 @@ void main() { ...@@ -18,10 +18,7 @@ void main() {
); );
await expectLater( await expectLater(
find.byType(ColorFiltered), find.byType(ColorFiltered),
matchesGoldenFile( matchesGoldenFile('color_filter_red.png'),
'color_filter_red.png',
version: 1,
),
); );
}); });
...@@ -58,10 +55,7 @@ void main() { ...@@ -58,10 +55,7 @@ void main() {
); );
await expectLater( await expectLater(
find.byType(ColorFiltered), find.byType(ColorFiltered),
matchesGoldenFile( matchesGoldenFile('color_filter_sepia.png'),
'color_filter_sepia.png',
version: 1,
),
); );
}); });
......
...@@ -90,10 +90,7 @@ void main() { ...@@ -90,10 +90,7 @@ void main() {
await expectLater( await expectLater(
find.byKey(const ValueKey<int>(1)), find.byKey(const ValueKey<int>(1)),
matchesGoldenFile( matchesGoldenFile('editable_text_test.0.png'),
'editable_text_test.0.png',
version: 3,
),
); );
}); });
...@@ -144,10 +141,7 @@ void main() { ...@@ -144,10 +141,7 @@ void main() {
await expectLater( await expectLater(
find.byKey(const ValueKey<int>(1)), find.byKey(const ValueKey<int>(1)),
matchesGoldenFile( matchesGoldenFile('editable_text_test.1.png'),
'editable_text_test.1.png',
version: 3,
),
); );
}); });
...@@ -797,10 +791,7 @@ void main() { ...@@ -797,10 +791,7 @@ void main() {
await expectLater( await expectLater(
find.byKey(const ValueKey<int>(1)), find.byKey(const ValueKey<int>(1)),
matchesGoldenFile( matchesGoldenFile('editable_text_test.2.png'),
'editable_text_test.2.png',
version: 0,
),
); );
debugDefaultTargetPlatformOverride = null; debugDefaultTargetPlatformOverride = null;
}); });
......
...@@ -20,10 +20,7 @@ void main() { ...@@ -20,10 +20,7 @@ void main() {
await expectLater( await expectLater(
find.byType(RepaintBoundary), find.byType(RepaintBoundary),
matchesGoldenFile( matchesGoldenFile('invert_colors_test.0.png'),
'invert_colors_test.0.png',
version: null,
),
); );
}, skip: isBrowser); }, skip: isBrowser);
...@@ -41,10 +38,7 @@ void main() { ...@@ -41,10 +38,7 @@ void main() {
await expectLater( await expectLater(
find.byType(RepaintBoundary), find.byType(RepaintBoundary),
matchesGoldenFile( matchesGoldenFile('invert_colors_test.1.png'),
'invert_colors_test.1.png',
version: null,
),
); );
}, skip: isBrowser); }, skip: isBrowser);
} }
......
...@@ -535,10 +535,7 @@ void main() { ...@@ -535,10 +535,7 @@ void main() {
await expectLater( await expectLater(
find.byKey(const Key('list_wheel_scroll_view')), find.byKey(const Key('list_wheel_scroll_view')),
matchesGoldenFile( matchesGoldenFile('list_wheel_scroll_view.center_child.magnified.png'),
'list_wheel_scroll_view.center_child.magnified.png',
version: null,
),
); );
}, skip: isBrowser); }, skip: isBrowser);
...@@ -592,10 +589,7 @@ void main() { ...@@ -592,10 +589,7 @@ void main() {
await expectLater( await expectLater(
find.byKey(const Key('list_wheel_scroll_view')), find.byKey(const Key('list_wheel_scroll_view')),
matchesGoldenFile( matchesGoldenFile('list_wheel_scroll_view.curved_wheel.left.png'),
'list_wheel_scroll_view.curved_wheel.left.png',
version: null,
),
); );
}, skip: isBrowser); }, skip: isBrowser);
......
...@@ -178,10 +178,7 @@ void main() { ...@@ -178,10 +178,7 @@ void main() {
); );
await expectLater( await expectLater(
find.byType(RepaintBoundary).first, find.byType(RepaintBoundary).first,
matchesGoldenFile( matchesGoldenFile('opacity_test.offset.png'),
'opacity_test.offset.png',
version: 1,
),
); );
}, skip: isBrowser); }, skip: isBrowser);
......
...@@ -110,10 +110,7 @@ void main() { ...@@ -110,10 +110,7 @@ void main() {
expect(exception.diagnostics.first.toString(), startsWith('A RenderFlex overflowed by ')); expect(exception.diagnostics.first.toString(), startsWith('A RenderFlex overflowed by '));
await expectLater( await expectLater(
find.byKey(key), find.byKey(key),
matchesGoldenFile( matchesGoldenFile('physical_model_overflow.png'),
'physical_model_overflow.png',
version: null,
),
); );
}, skip: isBrowser); }, skip: isBrowser);
......
...@@ -23,20 +23,14 @@ void main() { ...@@ -23,20 +23,14 @@ void main() {
); );
await expectLater( await expectLater(
find.byType(Container), find.byType(Container),
matchesGoldenFile( matchesGoldenFile('shadow.BoxDecoration.disabled.png'),
'shadow.BoxDecoration.disabled.png',
version: null,
),
); );
debugDisableShadows = false; debugDisableShadows = false;
tester.binding.reassembleApplication(); tester.binding.reassembleApplication();
await tester.pump(); await tester.pump();
await expectLater( await expectLater(
find.byType(Container), find.byType(Container),
matchesGoldenFile( matchesGoldenFile('shadow.BoxDecoration.enabled.png'),
'shadow.BoxDecoration.enabled.png',
version: null,
),
); );
debugDisableShadows = true; debugDisableShadows = true;
}, skip: isBrowser); }, skip: isBrowser);
...@@ -62,10 +56,7 @@ void main() { ...@@ -62,10 +56,7 @@ void main() {
await tester.pumpWidget(build(elevation)); await tester.pumpWidget(build(elevation));
await expectLater( await expectLater(
find.byType(Container), find.byType(Container),
matchesGoldenFile( matchesGoldenFile('shadow.ShapeDecoration.$elevation.png'),
'shadow.ShapeDecoration.$elevation.png',
version: null,
),
); );
} }
debugDisableShadows = true; debugDisableShadows = true;
...@@ -92,20 +83,14 @@ void main() { ...@@ -92,20 +83,14 @@ void main() {
); );
await expectLater( await expectLater(
find.byType(Container), find.byType(Container),
matchesGoldenFile( matchesGoldenFile('shadow.PhysicalModel.disabled.png'),
'shadow.PhysicalModel.disabled.png',
version: null,
),
); );
debugDisableShadows = false; debugDisableShadows = false;
tester.binding.reassembleApplication(); tester.binding.reassembleApplication();
await tester.pump(); await tester.pump();
await expectLater( await expectLater(
find.byType(Container), find.byType(Container),
matchesGoldenFile( matchesGoldenFile('shadow.PhysicalModel.enabled.png'),
'shadow.PhysicalModel.enabled.png',
version: null,
),
); );
debugDisableShadows = true; debugDisableShadows = true;
}, skip: isBrowser); }, skip: isBrowser);
...@@ -135,10 +120,7 @@ void main() { ...@@ -135,10 +120,7 @@ void main() {
await tester.pumpWidget(build(elevation.toDouble())); await tester.pumpWidget(build(elevation.toDouble()));
await expectLater( await expectLater(
find.byType(Container), find.byType(Container),
matchesGoldenFile( matchesGoldenFile('shadow.PhysicalShape.$elevation.png'),
'shadow.PhysicalShape.$elevation.png',
version: 1,
),
); );
} }
debugDisableShadows = true; debugDisableShadows = true;
......
...@@ -30,10 +30,7 @@ void main() { ...@@ -30,10 +30,7 @@ void main() {
await expectLater( await expectLater(
find.byType(Container), find.byType(Container),
matchesGoldenFile( matchesGoldenFile('text_golden.Centered.png'),
'text_golden.Centered.png',
version: null,
),
); );
await tester.pumpWidget( await tester.pumpWidget(
...@@ -57,10 +54,7 @@ void main() { ...@@ -57,10 +54,7 @@ void main() {
await expectLater( await expectLater(
find.byType(Container), find.byType(Container),
matchesGoldenFile( matchesGoldenFile('text_golden.Centered.wrap.png'),
'text_golden.Centered.wrap.png',
version: null,
),
); );
}); });
...@@ -91,10 +85,7 @@ void main() { ...@@ -91,10 +85,7 @@ void main() {
await expectLater( await expectLater(
find.byType(RepaintBoundary), find.byType(RepaintBoundary),
matchesGoldenFile( matchesGoldenFile('text_golden.Foreground.gradient.png'),
'text_golden.Foreground.gradient.png',
version: null,
),
); );
await tester.pumpWidget( await tester.pumpWidget(
...@@ -116,10 +107,7 @@ void main() { ...@@ -116,10 +107,7 @@ void main() {
await expectLater( await expectLater(
find.byType(RepaintBoundary), find.byType(RepaintBoundary),
matchesGoldenFile( matchesGoldenFile('text_golden.Foreground.stroke.png'),
'text_golden.Foreground.stroke.png',
version: null,
),
); );
await tester.pumpWidget( await tester.pumpWidget(
...@@ -142,10 +130,7 @@ void main() { ...@@ -142,10 +130,7 @@ void main() {
await expectLater( await expectLater(
find.byType(RepaintBoundary), find.byType(RepaintBoundary),
matchesGoldenFile( matchesGoldenFile('text_golden.Foreground.stroke_and_gradient.png'),
'text_golden.Foreground.stroke_and_gradient.png',
version: null,
),
); );
}); });
...@@ -195,10 +180,7 @@ void main() { ...@@ -195,10 +180,7 @@ void main() {
await expectLater( await expectLater(
find.byType(RepaintBoundary), find.byType(RepaintBoundary),
matchesGoldenFile( matchesGoldenFile('text_golden.Background.png'),
'text_golden.Background.png',
version: null,
),
); );
}); });
...@@ -234,10 +216,7 @@ void main() { ...@@ -234,10 +216,7 @@ void main() {
await expectLater( await expectLater(
find.byType(RepaintBoundary).first, find.byType(RepaintBoundary).first,
matchesGoldenFile( matchesGoldenFile('text_golden.Fade.png'),
'text_golden.Fade.png',
version: 1,
),
); );
}); });
...@@ -262,10 +241,7 @@ void main() { ...@@ -262,10 +241,7 @@ void main() {
); );
await expectLater( await expectLater(
find.byType(Container), find.byType(Container),
matchesGoldenFile( matchesGoldenFile('text_golden.StrutDefault.png'),
'text_golden.StrutDefault.png',
version: null,
),
); );
}); });
...@@ -292,10 +268,7 @@ void main() { ...@@ -292,10 +268,7 @@ void main() {
); );
await expectLater( await expectLater(
find.byType(Container), find.byType(Container),
matchesGoldenFile( matchesGoldenFile('text_golden.Strut.1.png'),
'text_golden.Strut.1.png',
version: 1,
),
); );
}); });
...@@ -323,10 +296,7 @@ void main() { ...@@ -323,10 +296,7 @@ void main() {
); );
await expectLater( await expectLater(
find.byType(Container), find.byType(Container),
matchesGoldenFile( matchesGoldenFile('text_golden.Strut.2.png'),
'text_golden.Strut.2.png',
version: 1,
),
); );
}); });
...@@ -377,10 +347,7 @@ void main() { ...@@ -377,10 +347,7 @@ void main() {
); );
await expectLater( await expectLater(
find.byType(Container), find.byType(Container),
matchesGoldenFile( matchesGoldenFile('text_golden.Strut.3.png'),
'text_golden.Strut.3.png',
version: 1,
),
); );
}); });
...@@ -415,10 +382,7 @@ void main() { ...@@ -415,10 +382,7 @@ void main() {
); );
await expectLater( await expectLater(
find.byType(Container), find.byType(Container),
matchesGoldenFile( matchesGoldenFile('text_golden.Strut.4.png'),
'text_golden.Strut.4.png',
version: 1,
),
); );
}); });
...@@ -469,10 +433,7 @@ void main() { ...@@ -469,10 +433,7 @@ void main() {
); );
await expectLater( await expectLater(
find.byType(Container), find.byType(Container),
matchesGoldenFile( matchesGoldenFile('text_golden.StrutForce.1.png'),
'text_golden.StrutForce.1.png',
version: 1,
),
); );
}); });
...@@ -510,10 +471,7 @@ void main() { ...@@ -510,10 +471,7 @@ void main() {
); );
await expectLater( await expectLater(
find.byType(Container), find.byType(Container),
matchesGoldenFile( matchesGoldenFile('text_golden.Decoration.1.png'),
'text_golden.Decoration.1.png',
version: 0,
),
); );
}); });
...@@ -552,10 +510,7 @@ void main() { ...@@ -552,10 +510,7 @@ void main() {
); );
await expectLater( await expectLater(
find.byType(Container), find.byType(Container),
matchesGoldenFile( matchesGoldenFile('text_golden.DecorationThickness.1.png'),
'text_golden.DecorationThickness.1.png',
version: 1,
),
); );
}); });
...@@ -649,10 +604,7 @@ void main() { ...@@ -649,10 +604,7 @@ void main() {
); );
await expectLater( await expectLater(
find.byType(Container), find.byType(Container),
matchesGoldenFile( matchesGoldenFile('text_golden.TextInlineWidget.1.png'),
'text_golden.TextInlineWidget.1.png',
version: 1,
),
); );
}); });
...@@ -697,10 +649,7 @@ void main() { ...@@ -697,10 +649,7 @@ void main() {
); );
await expectLater( await expectLater(
find.byType(Container), find.byType(Container),
matchesGoldenFile( matchesGoldenFile('text_golden.TextInlineWidget.2.png'),
'text_golden.TextInlineWidget.2.png',
version: 2,
),
); );
}); });
...@@ -829,10 +778,7 @@ void main() { ...@@ -829,10 +778,7 @@ void main() {
); );
await expectLater( await expectLater(
find.byType(Container), find.byType(Container),
matchesGoldenFile( matchesGoldenFile('text_golden.TextInlineWidgetNest.1.png'),
'text_golden.TextInlineWidgetNest.1.png',
version: 3,
),
); );
}); });
...@@ -939,10 +885,7 @@ void main() { ...@@ -939,10 +885,7 @@ void main() {
); );
await expectLater( await expectLater(
find.byType(Container), find.byType(Container),
matchesGoldenFile( matchesGoldenFile('text_golden.TextInlineWidgetBaseline.1.png'),
'text_golden.TextInlineWidgetBaseline.1.png',
version: 1,
),
); );
}); });
...@@ -1049,10 +992,7 @@ void main() { ...@@ -1049,10 +992,7 @@ void main() {
); );
await expectLater( await expectLater(
find.byType(Container), find.byType(Container),
matchesGoldenFile( matchesGoldenFile('text_golden.TextInlineWidgetAboveBaseline.1.png'),
'text_golden.TextInlineWidgetAboveBaseline.1.png',
version: 1,
),
); );
}); });
...@@ -1159,10 +1099,7 @@ void main() { ...@@ -1159,10 +1099,7 @@ void main() {
); );
await expectLater( await expectLater(
find.byType(Container), find.byType(Container),
matchesGoldenFile( matchesGoldenFile('text_golden.TextInlineWidgetBelowBaseline.1.png'),
'text_golden.TextInlineWidgetBelowBaseline.1.png',
version: 1,
),
); );
}); });
...@@ -1269,10 +1206,7 @@ void main() { ...@@ -1269,10 +1206,7 @@ void main() {
); );
await expectLater( await expectLater(
find.byType(Container), find.byType(Container),
matchesGoldenFile( matchesGoldenFile('text_golden.TextInlineWidgetTop.1.png'),
'text_golden.TextInlineWidgetTop.1.png',
version: 1,
),
); );
}); });
...@@ -1379,10 +1313,7 @@ void main() { ...@@ -1379,10 +1313,7 @@ void main() {
); );
await expectLater( await expectLater(
find.byType(Container), find.byType(Container),
matchesGoldenFile( matchesGoldenFile('text_golden.TextInlineWidgetMiddle.1.png'),
'text_golden.TextInlineWidgetMiddle.1.png',
version: 1,
),
); );
}); });
} }
...@@ -2039,10 +2039,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { ...@@ -2039,10 +2039,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
expect(expectedChildLayerCount, equals(2)); expect(expectedChildLayerCount, equals(2));
await expectLater( await expectLater(
layer.toImage(renderObject.semanticBounds.inflate(50.0)), layer.toImage(renderObject.semanticBounds.inflate(50.0)),
matchesGoldenFile( matchesGoldenFile('inspector.repaint_boundary_margin.png'),
'inspector.repaint_boundary_margin.png',
version: null,
),
); );
// Regression test for how rendering with a pixel scale other than 1.0 // Regression test for how rendering with a pixel scale other than 1.0
...@@ -2052,10 +2049,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { ...@@ -2052,10 +2049,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
renderObject.semanticBounds.inflate(50.0), renderObject.semanticBounds.inflate(50.0),
pixelRatio: 0.5, pixelRatio: 0.5,
), ),
matchesGoldenFile( matchesGoldenFile('inspector.repaint_boundary_margin_small.png'),
'inspector.repaint_boundary_margin_small.png',
version: null,
),
); );
await expectLater( await expectLater(
...@@ -2063,10 +2057,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { ...@@ -2063,10 +2057,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
renderObject.semanticBounds.inflate(50.0), renderObject.semanticBounds.inflate(50.0),
pixelRatio: 2.0, pixelRatio: 2.0,
), ),
matchesGoldenFile( matchesGoldenFile('inspector.repaint_boundary_margin_large.png'),
'inspector.repaint_boundary_margin_large.png',
version: null,
),
); );
final Layer layerParent = layer.parent; final Layer layerParent = layer.parent;
...@@ -2081,10 +2072,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { ...@@ -2081,10 +2072,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
width: 300.0, width: 300.0,
height: 300.0, height: 300.0,
), ),
matchesGoldenFile( matchesGoldenFile('inspector.repaint_boundary.png'),
'inspector.repaint_boundary.png',
version: null,
),
); );
// Verify that taking a screenshot didn't change the layers associated with // Verify that taking a screenshot didn't change the layers associated with
...@@ -2101,10 +2089,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { ...@@ -2101,10 +2089,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
height: 500.0, height: 500.0,
margin: 50.0, margin: 50.0,
), ),
matchesGoldenFile( matchesGoldenFile('inspector.repaint_boundary_margin.png'),
'inspector.repaint_boundary_margin.png',
version: null,
),
); );
// Verify that taking a screenshot didn't change the layers associated with // Verify that taking a screenshot didn't change the layers associated with
...@@ -2124,10 +2109,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { ...@@ -2124,10 +2109,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
height: 300.0, height: 300.0,
debugPaint: true, debugPaint: true,
), ),
matchesGoldenFile( matchesGoldenFile('inspector.repaint_boundary_debugPaint.png'),
'inspector.repaint_boundary_debugPaint.png',
version: null,
),
); );
// Verify that taking a screenshot with debug paint on did not change // Verify that taking a screenshot with debug paint on did not change
// the number of children the layer has. // the number of children the layer has.
...@@ -2137,10 +2119,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { ...@@ -2137,10 +2119,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
// hasn't changed the regular render of the widget. // hasn't changed the regular render of the widget.
await expectLater( await expectLater(
find.byType(RepaintBoundaryWithDebugPaint), find.byType(RepaintBoundaryWithDebugPaint),
matchesGoldenFile( matchesGoldenFile('inspector.repaint_boundary.png'),
'inspector.repaint_boundary.png',
version: null,
),
); );
expect(renderObject.debugLayer, equals(layer)); expect(renderObject.debugLayer, equals(layer));
...@@ -2153,10 +2132,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { ...@@ -2153,10 +2132,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
width: 100.0, width: 100.0,
height: 100.0, height: 100.0,
), ),
matchesGoldenFile( matchesGoldenFile('inspector.container.png'),
'inspector.container.png',
version: null,
),
); );
await expectLater( await expectLater(
...@@ -2166,10 +2142,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { ...@@ -2166,10 +2142,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
height: 100.0, height: 100.0,
debugPaint: true, debugPaint: true,
), ),
matchesGoldenFile( matchesGoldenFile('inspector.container_debugPaint.png'),
'inspector.container_debugPaint.png',
version: null,
),
); );
{ {
...@@ -2189,10 +2162,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { ...@@ -2189,10 +2162,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
height: 100.0, height: 100.0,
debugPaint: true, debugPaint: true,
), ),
matchesGoldenFile( matchesGoldenFile('inspector.container_debugPaint.png'),
'inspector.container_debugPaint.png',
version: null,
),
); );
expect(container.debugNeedsLayout, isFalse); expect(container.debugNeedsLayout, isFalse);
} }
...@@ -2204,10 +2174,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { ...@@ -2204,10 +2174,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
width: 50.0, width: 50.0,
height: 100.0, height: 100.0,
), ),
matchesGoldenFile( matchesGoldenFile('inspector.container_small.png'),
'inspector.container_small.png',
version: null,
),
); );
await expectLater( await expectLater(
...@@ -2217,10 +2184,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { ...@@ -2217,10 +2184,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
height: 400.0, height: 400.0,
maxPixelRatio: 3.0, maxPixelRatio: 3.0,
), ),
matchesGoldenFile( matchesGoldenFile('inspector.container_large.png'),
'inspector.container_large.png',
version: null,
),
); );
// This screenshot will show the clip rect debug paint but no other // This screenshot will show the clip rect debug paint but no other
...@@ -2232,10 +2196,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { ...@@ -2232,10 +2196,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
height: 100.0, height: 100.0,
debugPaint: true, debugPaint: true,
), ),
matchesGoldenFile( matchesGoldenFile('inspector.clipRect_debugPaint.png'),
'inspector.clipRect_debugPaint.png',
version: null,
),
); );
final Element clipRect = find.byType(ClipRRect).evaluate().single; final Element clipRect = find.byType(ClipRRect).evaluate().single;
...@@ -2251,10 +2212,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { ...@@ -2251,10 +2212,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
// This golden image is platform dependent due to the clip icon. // This golden image is platform dependent due to the clip icon.
await expectLater( await expectLater(
clipRectScreenshot, clipRectScreenshot,
matchesGoldenFile( matchesGoldenFile('inspector.clipRect_debugPaint_margin.png'),
'inspector.clipRect_debugPaint_margin.png',
version: null,
),
); );
// Verify we get the same image if we go through the service extension // Verify we get the same image if we go through the service extension
...@@ -2293,10 +2251,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { ...@@ -2293,10 +2251,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
height: 300.0, height: 300.0,
debugPaint: true, debugPaint: true,
), ),
matchesGoldenFile( matchesGoldenFile('inspector.padding_debugPaint.png'),
'inspector.padding_debugPaint.png',
version: null,
),
); );
// The bounds for this box crop its rendered content. // The bounds for this box crop its rendered content.
...@@ -2307,10 +2262,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { ...@@ -2307,10 +2262,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
height: 300.0, height: 300.0,
debugPaint: true, debugPaint: true,
), ),
matchesGoldenFile( matchesGoldenFile('inspector.sizedBox_debugPaint.png'),
'inspector.sizedBox_debugPaint.png',
version: 1,
),
); );
// Verify that setting a margin includes the previously cropped content. // Verify that setting a margin includes the previously cropped content.
...@@ -2322,10 +2274,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { ...@@ -2322,10 +2274,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
margin: 50.0, margin: 50.0,
debugPaint: true, debugPaint: true,
), ),
matchesGoldenFile( matchesGoldenFile('inspector.sizedBox_debugPaint_margin.png'),
'inspector.sizedBox_debugPaint_margin.png',
version: null,
),
); );
}, skip: isBrowser); }, skip: isBrowser);
...@@ -2461,10 +2410,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { ...@@ -2461,10 +2410,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
await expectLater( await expectLater(
find.byKey(mainStackKey), find.byKey(mainStackKey),
matchesGoldenFile( matchesGoldenFile('inspector.composited_transform.only_offsets.png'),
'inspector.composited_transform.only_offsets.png',
version: null,
),
); );
await expectLater( await expectLater(
...@@ -2473,18 +2419,12 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { ...@@ -2473,18 +2419,12 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
width: 5000.0, width: 5000.0,
height: 500.0, height: 500.0,
), ),
matchesGoldenFile( matchesGoldenFile('inspector.composited_transform.only_offsets_follower.png'),
'inspector.composited_transform.only_offsets_follower.png',
version: null,
),
); );
await expectLater( await expectLater(
WidgetInspectorService.instance.screenshot(find.byType(Stack).evaluate().first, width: 300.0, height: 300.0), WidgetInspectorService.instance.screenshot(find.byType(Stack).evaluate().first, width: 300.0, height: 300.0),
matchesGoldenFile( matchesGoldenFile('inspector.composited_transform.only_offsets_small.png'),
'inspector.composited_transform.only_offsets_small.png',
version: 1,
),
); );
await expectLater( await expectLater(
...@@ -2493,10 +2433,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { ...@@ -2493,10 +2433,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
width: 500.0, width: 500.0,
height: 500.0, height: 500.0,
), ),
matchesGoldenFile( matchesGoldenFile('inspector.composited_transform.only_offsets_target.png'),
'inspector.composited_transform.only_offsets_target.png',
version: null,
),
); );
}, skip: isBrowser); }, skip: isBrowser);
...@@ -2568,10 +2505,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { ...@@ -2568,10 +2505,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
// screenshots of specific subtrees are reasonable. // screenshots of specific subtrees are reasonable.
await expectLater( await expectLater(
find.byKey(mainStackKey), find.byKey(mainStackKey),
matchesGoldenFile( matchesGoldenFile('inspector.composited_transform.with_rotations.png'),
'inspector.composited_transform.with_rotations.png',
version: null,
),
); );
await expectLater( await expectLater(
...@@ -2580,10 +2514,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { ...@@ -2580,10 +2514,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
width: 500.0, width: 500.0,
height: 500.0, height: 500.0,
), ),
matchesGoldenFile( matchesGoldenFile('inspector.composited_transform.with_rotations_small.png'),
'inspector.composited_transform.with_rotations_small.png',
version: null,
),
); );
await expectLater( await expectLater(
...@@ -2592,10 +2523,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { ...@@ -2592,10 +2523,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
width: 500.0, width: 500.0,
height: 500.0, height: 500.0,
), ),
matchesGoldenFile( matchesGoldenFile('inspector.composited_transform.with_rotations_target.png'),
'inspector.composited_transform.with_rotations_target.png',
version: null,
),
); );
await expectLater( await expectLater(
...@@ -2604,10 +2532,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { ...@@ -2604,10 +2532,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
width: 500.0, width: 500.0,
height: 500.0, height: 500.0,
), ),
matchesGoldenFile( matchesGoldenFile('inspector.composited_transform.with_rotations_follower.png'),
'inspector.composited_transform.with_rotations_follower.png',
version: null,
),
); );
// Make sure taking screenshots hasn't modified the positions of the // Make sure taking screenshots hasn't modified the positions of the
......
...@@ -11,49 +11,99 @@ import 'package:flutter_test/flutter_test.dart'; ...@@ -11,49 +11,99 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:platform/platform.dart'; import 'package:platform/platform.dart';
import 'package:flutter_goldens_client/client.dart';
import 'package:flutter_goldens_client/skia_client.dart'; import 'package:flutter_goldens_client/skia_client.dart';
export 'package:flutter_goldens_client/client.dart';
export 'package:flutter_goldens_client/skia_client.dart'; export 'package:flutter_goldens_client/skia_client.dart';
// If you are here trying to figure out how to use golden files in the Flutter
// repo itself, consider reading this wiki page:
// https://github.com/flutter/flutter/wiki/Writing-a-golden-file-test-for-package%3Aflutter
const String _kFlutterRootKey = 'FLUTTER_ROOT';
/// Main method that can be used in a `flutter_test_config.dart` file to set /// Main method that can be used in a `flutter_test_config.dart` file to set
/// [goldenFileComparator] to an instance of [FlutterGoldenFileComparator] that /// [goldenFileComparator] to an instance of [FlutterGoldenFileComparator] that
/// works for the current test. _Which_ FlutterGoldenFileComparator is /// works for the current test. _Which_ FlutterGoldenFileComparator is
/// instantiated is based on the current testing environment. /// instantiated is based on the current testing environment.
Future<void> main(FutureOr<void> testMain()) async { Future<void> main(FutureOr<void> testMain()) async {
const Platform platform = LocalPlatform(); const Platform platform = LocalPlatform();
if (FlutterSkiaGoldFileComparator.isAvailableOnPlatform(platform)) { if (FlutterSkiaGoldFileComparator.isAvailableForEnvironment(platform)) {
goldenFileComparator = await FlutterSkiaGoldFileComparator.fromDefaultComparator(); goldenFileComparator = await FlutterSkiaGoldFileComparator.fromDefaultComparator(platform);
} else if (FlutterGoldensRepositoryFileComparator.isAvailableOnPlatform(platform)) { } else if (FlutterPreSubmitFileComparator.isAvailableForEnvironment(platform)) {
goldenFileComparator = await FlutterGoldensRepositoryFileComparator.fromDefaultComparator(); goldenFileComparator = await FlutterPreSubmitFileComparator.fromDefaultComparator(platform);
} else { } else if (FlutterSkippingGoldenFileComparator.isAvailableForEnvironment(platform)){
goldenFileComparator = FlutterSkippingGoldenFileComparator.fromDefaultComparator(); goldenFileComparator = FlutterSkippingGoldenFileComparator.fromDefaultComparator();
} else {
goldenFileComparator = await FlutterLocalFileComparator.fromDefaultComparator(platform);
} }
await testMain(); await testMain();
} }
/// Abstract base class golden file comparator specific to the `flutter/flutter` /// Abstract base class golden file comparator specific to the `flutter/flutter`
/// repository. /// repository.
///
/// Golden file testing for the `flutter/flutter` repository is handled by three
/// different [FlutterGoldenFileComparator]s, depending on the current testing
/// environment.
///
/// * The [FlutterSkiaGoldFileComparator] is utilized during post-submit
/// testing, after a pull request has landed on the master branch. This
/// comparator uses the [SkiaGoldClient] and the `goldctl` tool to upload
/// tests to the [Flutter Gold dashboard](https://flutter-gold.skia.org).
/// Flutter Gold manages the master golden files for the `flutter/flutter`
/// repository.
///
/// * The [FlutterPreSubmitFileComparator] is utilized in pre-submit testing,
/// before a pull request can land on the master branch. This comparator
/// uses the [SkiaGoldClient] to request the baseline images kept by the
/// [Flutter Gold dashboard](https://flutter-gold.skia.org). It then
/// compares the current test image to the baseline images using the
/// standard [GoldenFileComparator.compareLists] to detect any pixel
/// difference. The [SkiaGoldClient] is also used here to check the active
/// ignores from the dashboard, in order to allow intended changes to pass
/// tests.
///
/// * The [FlutterLocalFileComparator] is used for any other tests run outside
/// of the above conditions. Similar to the
/// [FlutterPreSubmitFileComparator], this comparator will use the
/// [SkiaGoldClient] to request baseline images from
/// [Flutter Gold](https://flutter-gold.skia.org) and compares for the
/// current test image. If a difference is detected, this comparator will
/// generate failure output illustrating the found difference. If a baseline
/// is not found for a given test image, it will consider it a new test and
/// output the new image for verification.
/// The [FlutterSkippingGoldenFileComparator] is utilized to skip tests outside
/// of the appropriate environments. Currently, tests executing in post-submit
/// on the LUCI build environment are skipped, as post-submit checks are done
/// on Cirrus.
abstract class FlutterGoldenFileComparator extends GoldenFileComparator { abstract class FlutterGoldenFileComparator extends GoldenFileComparator {
/// Creates a [FlutterGoldenFileComparator] that will resolve golden file /// Creates a [FlutterGoldenFileComparator] that will resolve golden file
/// URIs relative to the specified [basedir]. /// URIs relative to the specified [basedir], and retrieve golden baselines
/// using the [skiaClient]. The [basedir] is used for writing and accessing
/// information and files for interacting with the [skiaClient]. When testing
/// locally, the [basedir] will also contain any diffs from failed tests, or
/// goldens generated from newly introduced tests.
/// ///
/// The [fs] and [platform] parameters useful in tests, where the default file /// The [fs] and [platform] parameters are useful in tests, where the default
/// system and platform can be replaced by mock instances. /// file system and platform can be replaced by mock instances.
@visibleForTesting @visibleForTesting
FlutterGoldenFileComparator( FlutterGoldenFileComparator(
this.basedir, { this.basedir,
this.skiaClient, {
this.fs = const LocalFileSystem(), this.fs = const LocalFileSystem(),
this.platform = const LocalPlatform(), this.platform = const LocalPlatform(),
}) : assert(basedir != null), }) : assert(basedir != null),
assert(skiaClient != null),
assert(fs != null), assert(fs != null),
assert(platform != null); assert(platform != null);
/// The directory to which golden file URIs will be resolved in [compare] and /// The directory to which golden file URIs will be resolved in [compare] and
/// [update]. /// [update], cannot be null.
final Uri basedir; final Uri basedir;
/// A client for uploading image tests and making baseline requests to the
/// Flutter Gold Dashboard, cannot be null.
final SkiaGoldClient skiaClient;
/// The file system used to perform file access. /// The file system used to perform file access.
@visibleForTesting @visibleForTesting
final FileSystem fs; final FileSystem fs;
...@@ -69,191 +119,340 @@ abstract class FlutterGoldenFileComparator extends GoldenFileComparator { ...@@ -69,191 +119,340 @@ abstract class FlutterGoldenFileComparator extends GoldenFileComparator {
await goldenFile.writeAsBytes(imageBytes, flush: true); await goldenFile.writeAsBytes(imageBytes, flush: true);
} }
@override
Uri getTestUri(Uri key, int version) => key;
/// Calculate the appropriate basedir for the current test context. /// Calculate the appropriate basedir for the current test context.
@protected @protected
@visibleForTesting @visibleForTesting
static Directory getBaseDirectory(GoldensClient goldens, LocalFileComparator defaultComparator) { static Directory getBaseDirectory(LocalFileComparator defaultComparator, Platform platform) {
final FileSystem fs = goldens.fs; const FileSystem fs = LocalFileSystem();
final Directory flutterRoot = fs.directory(platform.environment[_kFlutterRootKey]);
final Directory comparisonRoot = flutterRoot.childDirectory(
fs.path.join(
'bin',
'cache',
'pkg',
'skia_goldens',
)
);
final Directory testDirectory = fs.directory(defaultComparator.basedir); final Directory testDirectory = fs.directory(defaultComparator.basedir);
final String testDirectoryRelativePath = fs.path.relative(testDirectory.path, from: goldens.flutterRoot.path); final String testDirectoryRelativePath = fs.path.relative(
return goldens.comparisonRoot.childDirectory(testDirectoryRelativePath); testDirectory.path,
from: flutterRoot.path,
);
return comparisonRoot.childDirectory(testDirectoryRelativePath);
} }
/// Returns the golden [File] identified by the given [Uri]. /// Returns the golden [File] identified by the given [Uri].
@protected @protected
File getGoldenFile(Uri uri) { File getGoldenFile(Uri uri) {
assert(basedir.scheme == 'file');
final File goldenFile = fs.directory(basedir).childFile(fs.file(uri).path); final File goldenFile = fs.directory(basedir).childFile(fs.file(uri).path);
assert(goldenFile.uri.scheme == 'file');
return goldenFile; return goldenFile;
} }
/// Prepends the golden Uri with the library name that encloses the current
/// test.
Uri _addPrefix(Uri golden) {
final String prefix = basedir.pathSegments[basedir.pathSegments.length - 2];
return Uri.parse(prefix + '.' + golden.toString());
}
} }
/// A [FlutterGoldenFileComparator] for testing golden images against the /// A [FlutterGoldenFileComparator] for testing golden images with Skia Gold.
/// `flutter/goldens` repository.
///
/// Within the https://github.com/flutter/flutter repository, it's important
/// not to check-in binaries in order to keep the size of the repository to a
/// minimum. To satisfy this requirement, this comparator retrieves the golden
/// files from a sibling repository, `flutter/goldens`.
/// ///
/// This comparator will locally clone the `flutter/goldens` repository into /// For testing across all platforms, the [SkiaGoldClient] is used to upload
/// the `$FLUTTER_ROOT/bin/cache/pkg/goldens` folder using the /// images for framework-related golden tests and process results. Currently
/// [GoldensRepositoryClient], then perform the comparison against the files /// these tests are designed to be run post-submit on Cirrus CI, informed by the
/// therein. /// environment.
/// ///
/// See also: /// See also:
/// ///
/// * [GoldenFileComparator], the abstract class that /// * [GoldenFileComparator], the abstract class that
/// [FlutterGoldenFileComparator] implements. /// [FlutterGoldenFileComparator] implements.
/// * [FlutterSkiaGoldFileComparator], another [FlutterGoldenFileComparator] /// * [FlutterPreSubmitFileComparator], another
/// that tests golden images through Skia Gold. /// [FlutterGoldenFileComparator] that tests golden images before changes are
class FlutterGoldensRepositoryFileComparator extends FlutterGoldenFileComparator { /// merged into the master branch.
/// Creates a [FlutterGoldensRepositoryFileComparator] that will test golden /// * [FlutterLocalFileComparator], another
/// file images against the `flutter/goldens` repository. /// [FlutterGoldenFileComparator] that tests golden images locally on your
/// current machine.
class FlutterSkiaGoldFileComparator extends FlutterGoldenFileComparator {
/// Creates a [FlutterSkiaGoldFileComparator] that will test golden file
/// images against Skia Gold.
/// ///
/// The [fs] and [platform] parameters useful in tests, where the default file /// The [fs] and [platform] parameters are useful in tests, where the default
/// system and platform can be replaced by mock instances. /// file system and platform can be replaced by mock instances.
FlutterGoldensRepositoryFileComparator( FlutterSkiaGoldFileComparator(
Uri basedir, { final Uri basedir,
FileSystem fs = const LocalFileSystem(), final SkiaGoldClient skiaClient, {
Platform platform = const LocalPlatform(), final FileSystem fs = const LocalFileSystem(),
final Platform platform = const LocalPlatform(),
}) : super( }) : super(
basedir, basedir,
skiaClient,
fs: fs, fs: fs,
platform: platform, platform: platform,
); );
/// Creates a new [FlutterGoldensRespositoryFileComparator] that mirrors the /// Creates a new [FlutterSkiaGoldFileComparator] that mirrors the relative
/// relative path resolution of the default [goldenFileComparator]. /// path resolution of the default [goldenFileComparator].
///
/// By the time the future completes, the clone of the `flutter/goldens`
/// repository is guaranteed to be ready to use.
/// ///
/// The [goldens] and [defaultComparator] parameters are visible for testing /// The [goldens] and [defaultComparator] parameters are visible for testing
/// purposes only. /// purposes only.
static Future<FlutterGoldensRepositoryFileComparator> fromDefaultComparator({ static Future<FlutterSkiaGoldFileComparator> fromDefaultComparator(
GoldensRepositoryClient goldens, final Platform platform, {
SkiaGoldClient goldens,
LocalFileComparator defaultComparator, LocalFileComparator defaultComparator,
}) async { }) async {
defaultComparator ??= goldenFileComparator; defaultComparator ??= goldenFileComparator;
final Directory baseDirectory = FlutterGoldenFileComparator.getBaseDirectory(
defaultComparator,
platform,
);
// Prepare the goldens repo. if(!baseDirectory.existsSync()) {
goldens ??= GoldensRepositoryClient(); baseDirectory.createSync(recursive: true);
await goldens.prepare(); }
final Directory baseDirectory = FlutterGoldenFileComparator.getBaseDirectory(goldens, defaultComparator); goldens ??= SkiaGoldClient(baseDirectory);
return FlutterGoldensRepositoryFileComparator(baseDirectory.uri); await goldens.auth();
await goldens.imgtestInit();
return FlutterSkiaGoldFileComparator(baseDirectory.uri, goldens);
} }
@override @override
Future<bool> compare(Uint8List imageBytes, Uri golden) async { Future<bool> compare(Uint8List imageBytes, Uri golden) async {
golden = _addPrefix(golden);
await update(golden, imageBytes);
final File goldenFile = getGoldenFile(golden); final File goldenFile = getGoldenFile(golden);
if (!goldenFile.existsSync()) {
throw TestFailure('Could not be compared against non-existent file: "$golden"'); return skiaClient.imgtestAdd(golden.path, goldenFile);
}
final List<int> goldenBytes = await goldenFile.readAsBytes();
final ComparisonResult result = GoldenFileComparator.compareLists(imageBytes, goldenBytes);
return result.passed;
} }
/// Decides based on the current platform whether goldens tests should be /// Decides based on the current environment whether goldens tests should be
/// performed against the flutter/goldens repository. /// performed against Skia Gold.
static bool isAvailableOnPlatform(Platform platform) => platform.isLinux; static bool isAvailableForEnvironment(Platform platform) {
final String cirrusCI = platform.environment['CIRRUS_CI'] ?? '';
final String cirrusPR = platform.environment['CIRRUS_PR'] ?? '';
final String cirrusBranch = platform.environment['CIRRUS_BRANCH'] ?? '';
final String goldServiceAccount = platform.environment['GOLD_SERVICE_ACCOUNT'] ?? '';
return cirrusCI.isNotEmpty
&& cirrusPR.isEmpty
&& cirrusBranch == 'master'
&& goldServiceAccount.isNotEmpty;
}
} }
/// A [FlutterGoldenFileComparator] for testing golden images with Skia Gold. /// A [FlutterGoldenFileComparator] for testing golden images before changes are
/// merged into the master branch.
/// ///
/// For testing across all platforms, the [SkiaGoldClient] is used to upload /// This comparator utilizes the [SkiaGoldClient] to request baseline images for
/// images for framework-related golden tests and process results. Currently /// the given device under test for comparison. This comparator is only
/// these tests are designed to be run post-submit on Cirrus CI, informed by the /// initialized during pre-submit testing on Cirrus CI.
/// environment.
/// ///
/// See also: /// See also:
/// ///
/// * [GoldenFileComparator], the abstract class that /// * [GoldenFileComparator], the abstract class that
/// [FlutterGoldenFileComparator] implements. /// [FlutterGoldenFileComparator] implements.
/// * [FlutterGoldensRepositoryFileComparator], another /// * [FlutterSkiaGoldFileComparator], another
/// [FlutterGoldenFileComparator] that tests golden images using the /// [FlutterGoldenFileComparator] that uploads tests to the Skia Gold
/// flutter/goldens repository. /// dashboard.
class FlutterSkiaGoldFileComparator extends FlutterGoldenFileComparator { /// * [FlutterLocalFileComparator], another
/// Creates a [FlutterSkiaGoldFileComparator] that will test golden file /// [FlutterGoldenFileComparator] that tests golden images locally on your
/// images against Skia Gold. /// current machine.
class FlutterPreSubmitFileComparator extends FlutterGoldenFileComparator {
/// Creates a [FlutterPreSubmitFileComparator] that will test golden file
/// images against baselines requested from Flutter Gold.
/// ///
/// The [fs] and [platform] parameters useful in tests, where the default file /// The [fs] and [platform] parameters are useful in tests, where the default
/// system and platform can be replaced by mock instances. /// file system and platform can be replaced by mock instances.
FlutterSkiaGoldFileComparator( FlutterPreSubmitFileComparator(
final Uri basedir, final Uri basedir,
this.skiaClient, { final SkiaGoldClient skiaClient, {
FileSystem fs = const LocalFileSystem(), final FileSystem fs = const LocalFileSystem(),
Platform platform = const LocalPlatform(), final Platform platform = const LocalPlatform(),
}) : super( }) : super(
basedir, basedir,
skiaClient,
fs: fs, fs: fs,
platform: platform, platform: platform,
); );
final SkiaGoldClient skiaClient; /// Creates a new [FlutterPreSubmitFileComparator] that mirrors the
/// relative path resolution of the default [goldenFileComparator].
/// Creates a new [FlutterSkiaGoldFileComparator] that mirrors the relative
/// path resolution of the default [goldenFileComparator].
/// ///
/// The [goldens] and [defaultComparator] parameters are visible for testing /// The [goldens] and [defaultComparator] parameters are visible for testing
/// purposes only. /// purposes only.
static Future<FlutterSkiaGoldFileComparator> fromDefaultComparator({ static Future<FlutterGoldenFileComparator> fromDefaultComparator(
final Platform platform, {
SkiaGoldClient goldens, SkiaGoldClient goldens,
LocalFileComparator defaultComparator, LocalFileComparator defaultComparator,
}) async { }) async {
defaultComparator ??= goldenFileComparator; defaultComparator ??= goldenFileComparator;
goldens ??= SkiaGoldClient(); final Directory baseDirectory = FlutterGoldenFileComparator.getBaseDirectory(
defaultComparator,
platform,
);
final Directory baseDirectory = FlutterGoldenFileComparator.getBaseDirectory(goldens, defaultComparator); if(!baseDirectory.existsSync()) {
if (!baseDirectory.existsSync())
baseDirectory.createSync(recursive: true); baseDirectory.createSync(recursive: true);
await goldens.auth(baseDirectory); }
await goldens.imgtestInit();
return FlutterSkiaGoldFileComparator(baseDirectory.uri, goldens); goldens ??= SkiaGoldClient(baseDirectory);
await goldens.getExpectations();
return FlutterPreSubmitFileComparator(baseDirectory.uri, goldens);
} }
@override @override
Future<bool> compare(Uint8List imageBytes, Uri golden) async { Future<bool> compare(Uint8List imageBytes, Uri golden) async {
golden = _addPrefix(golden); golden = _addPrefix(golden);
await update(golden, imageBytes); final String testName = skiaClient.cleanTestName(golden.path);
final List<String> testExpectations = skiaClient.expectations[testName];
final File goldenFile = getGoldenFile(golden); if (testExpectations == null) {
if (!goldenFile.existsSync()) { // There is no baseline for this test
throw TestFailure('Could not be compared against non-existent file: "$golden"'); return true;
}
ComparisonResult result;
for (String expectation in testExpectations) {
final List<int> goldenBytes = await skiaClient.getImageBytes(expectation);
result = GoldenFileComparator.compareLists(
imageBytes,
goldenBytes,
);
if (result.passed) {
return true;
} }
return await skiaClient.imgtestAdd(golden.path, goldenFile);
} }
@override return skiaClient.testIsIgnoredForPullRequest(
Uri getTestUri(Uri key, int version) => key; platform.environment['CIRRUS_PR'] ?? '',
golden.path,
);
}
/// Decides based on the current environment whether goldens tests should be /// Decides based on the current environment whether goldens tests should be
/// performed against Skia Gold. /// performed as pre-submit tests with Skia Gold.
static bool isAvailableOnPlatform(Platform platform) { static bool isAvailableForEnvironment(Platform platform) {
final String cirrusCI = platform.environment['CIRRUS_CI'] ?? ''; final String cirrusCI = platform.environment['CIRRUS_CI'] ?? '';
final String cirrusPR = platform.environment['CIRRUS_PR'] ?? ''; final String cirrusPR = platform.environment['CIRRUS_PR'] ?? '';
final String cirrusBranch = platform.environment['CIRRUS_BRANCH'] ?? ''; return cirrusCI.isNotEmpty && cirrusPR.isNotEmpty;
final String goldServiceAccount = platform.environment['GOLD_SERVICE_ACCOUNT'] ?? '';
return cirrusCI.isNotEmpty
&& cirrusPR.isEmpty
&& cirrusBranch == 'master'
&& goldServiceAccount.isNotEmpty;
} }
}
/// Prepends the golden Uri with the library name that encloses the current /// A [FlutterGoldenFileComparator] for testing golden images locally on your
/// test. /// current machine.
Uri _addPrefix(Uri golden) { ///
final String prefix = basedir.pathSegments[basedir.pathSegments.length - 2]; /// This comparator utilizes the [SkiaGoldClient] to request baseline images for
return Uri.parse(prefix + '.' + golden.toString()); /// the given device under test for comparison. This comparator is only
/// initialized when running tests locally, and is intended to serve as a smoke
/// test during development. As such, it will not be able to detect unintended
/// changes on other machines until it they are tested using the
/// [FlutterPreSubmitFileComparator].
///
/// See also:
///
/// * [GoldenFileComparator], the abstract class that
/// [FlutterGoldenFileComparator] implements.
/// * [FlutterSkiaGoldFileComparator], another
/// [FlutterGoldenFileComparator] that uploads tests to the Skia Gold
/// dashboard.
/// * [FlutterPreSubmitFileComparator], another
/// [FlutterGoldenFileComparator] that tests golden images before changes are
/// merged into the master branch.
class FlutterLocalFileComparator extends FlutterGoldenFileComparator with LocalComparisonOutput {
/// Creates a [FlutterLocalFileComparator] that will test golden file
/// images against baselines requested from Flutter Gold.
///
/// The [fs] and [platform] parameters are useful in tests, where the default
/// file system and platform can be replaced by mock instances.
FlutterLocalFileComparator(
final Uri basedir,
final SkiaGoldClient skiaClient, {
final FileSystem fs = const LocalFileSystem(),
final Platform platform = const LocalPlatform(),
}) : super(
basedir,
skiaClient,
fs: fs,
platform: platform,
);
/// Creates a new [FlutterLocalFileComparator] that mirrors the
/// relative path resolution of the default [goldenFileComparator].
///
/// The [goldens] and [defaultComparator] parameters are visible for testing
/// purposes only.
static Future<FlutterGoldenFileComparator> fromDefaultComparator(
final Platform platform, {
SkiaGoldClient goldens,
LocalFileComparator defaultComparator,
}) async {
defaultComparator ??= goldenFileComparator;
final Directory baseDirectory = FlutterGoldenFileComparator.getBaseDirectory(
defaultComparator,
platform,
);
if(!baseDirectory.existsSync()) {
baseDirectory.createSync(recursive: true);
}
goldens ??= SkiaGoldClient(baseDirectory);
await goldens.getExpectations();
return FlutterLocalFileComparator(baseDirectory.uri, goldens);
}
@override
Future<bool> compare(Uint8List imageBytes, Uri golden) async {
golden = _addPrefix(golden);
final String testName = skiaClient.cleanTestName(golden.path);
final List<String> testExpectations = skiaClient.expectations[testName];
if (testExpectations == null) {
// There is no baseline for this test
print('No expectations provided by Skia Gold for test: $golden. '
'This may be a new test. If this is an unexpected result, check '
'https://flutter-gold.skia.org.\n'
'Validate image output found at $basedir'
);
update(golden, imageBytes);
return true;
}
ComparisonResult result;
final Map<String, ComparisonResult> failureDiffs = <String, ComparisonResult>{};
for (String expectation in testExpectations) {
final List<int> goldenBytes = await skiaClient.getImageBytes(expectation);
result = GoldenFileComparator.compareLists(
imageBytes,
goldenBytes,
);
if (result.passed) {
return true;
}
failureDiffs[expectation] = result;
}
failureDiffs.forEach((String expectation, ComparisonResult result) async {
if (await skiaClient.isValidDigestForExpectation(expectation, golden.path))
generateFailureOutput(result, golden, basedir, key: expectation);
});
return false;
} }
} }
/// A [FlutterGoldenFileComparator] for skipping golden image tests when Skia /// A [FlutterGoldenFileComparator] for skipping golden image tests when the
/// Gold is unavailable or the current platform that is executing tests is not /// current environment is not supported. This comparator is used in post-submit
/// Linux. /// checks on LUCI.
/// ///
/// See also: /// See also:
/// ///
...@@ -262,29 +461,48 @@ class FlutterSkiaGoldFileComparator extends FlutterGoldenFileComparator { ...@@ -262,29 +461,48 @@ class FlutterSkiaGoldFileComparator extends FlutterGoldenFileComparator {
/// flutter/goldens repository. /// flutter/goldens repository.
/// * [FlutterSkiaGoldFileComparator], another [FlutterGoldenFileComparator] /// * [FlutterSkiaGoldFileComparator], another [FlutterGoldenFileComparator]
/// that tests golden images through Skia Gold. /// that tests golden images through Skia Gold.
/// * [FlutterPreSubmitFileComparator], another
/// [FlutterGoldenFileComparator] that tests golden images before changes are
/// merged into the master branch.
/// * [FlutterLocalFileComparator], another
/// [FlutterGoldenFileComparator] that tests golden images locally on your
/// current machine.
class FlutterSkippingGoldenFileComparator extends FlutterGoldenFileComparator { class FlutterSkippingGoldenFileComparator extends FlutterGoldenFileComparator {
/// Creates a [FlutterSkippingGoldenFileComparator] that will skip tests that /// Creates a [FlutterSkippingGoldenFileComparator] that will skip tests that
/// are not in the right environment for golden file testing. /// are not in the right environment for golden file testing.
FlutterSkippingGoldenFileComparator(Uri basedir) : super(basedir); FlutterSkippingGoldenFileComparator(
final Uri basedir,
final SkiaGoldClient skiaClient,
) : super(basedir, skiaClient);
/// Creates a new [FlutterSkippingGoldenFileComparator] that mirrors the relative /// Creates a new [FlutterSkippingGoldenFileComparator] that mirrors the
/// path resolution of the default [goldenFileComparator]. /// relative path resolution of the default [goldenFileComparator].
static FlutterSkippingGoldenFileComparator fromDefaultComparator({ static FlutterSkippingGoldenFileComparator fromDefaultComparator({
LocalFileComparator defaultComparator, LocalFileComparator defaultComparator,
}) { }) {
defaultComparator ??= goldenFileComparator; defaultComparator ??= goldenFileComparator;
return FlutterSkippingGoldenFileComparator(defaultComparator.basedir); const FileSystem fs = LocalFileSystem();
final Uri basedir = defaultComparator.basedir;
final SkiaGoldClient skiaClient = SkiaGoldClient(fs.directory(basedir));
return FlutterSkippingGoldenFileComparator(basedir, skiaClient);
} }
@override @override
Future<bool> compare(Uint8List imageBytes, Uri golden) async { Future<bool> compare(Uint8List imageBytes, Uri golden) async {
print('Skipping "$golden" test : Skia Gold is not available in this testing ' print(
'environment and flutter/goldens repository comparison is only available ' 'Skipping "$golden" test : Golden file testing is unavailble in LUCI'
'on Linux machines.' 'environment.'
); );
return true; return true;
} }
@override @override
Future<void> update(Uri golden, Uint8List imageBytes) => null; Future<void> update(Uri golden, Uint8List imageBytes) => null;
/// Decides based on the current environment whether goldens tests should be
/// skipped.
static bool isAvailableForEnvironment(Platform platform) {
final String luci = platform.environment['SWARMING_TASK_ID'] ?? '';
return luci.isNotEmpty;
}
} }
...@@ -2,7 +2,10 @@ ...@@ -2,7 +2,10 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:io' as io; import 'dart:async';
import 'dart:convert';
import 'dart:core';
import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:file/file.dart'; import 'package:file/file.dart';
...@@ -13,196 +16,560 @@ import 'package:mockito/mockito.dart'; ...@@ -13,196 +16,560 @@ import 'package:mockito/mockito.dart';
import 'package:platform/platform.dart'; import 'package:platform/platform.dart';
import 'package:process/process.dart'; import 'package:process/process.dart';
import 'json_templates.dart';
const String _kFlutterRoot = '/flutter'; const String _kFlutterRoot = '/flutter';
const String _kRepositoryRoot = '$_kFlutterRoot/bin/cache/pkg/goldens';
const String _kVersionFile = '$_kFlutterRoot/bin/internal/goldens.version'; // 1x1 transparent pixel
const String _kGoldensVersion = '123456abcdef'; const List<int> _kTestPngBytes =
<int>[137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0,
1, 0, 0, 0, 1, 8, 6, 0, 0, 0, 31, 21, 196, 137, 0, 0, 0, 11, 73, 68, 65, 84,
120, 1, 99, 97, 0, 2, 0, 0, 25, 0, 5, 144, 240, 54, 245, 0, 0, 0, 0, 73, 69,
78, 68, 174, 66, 96, 130];
// 1x1 colored pixel
const List<int> _kFailPngBytes =
<int>[137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0,
1, 0, 0, 0, 1, 8, 6, 0, 0, 0, 31, 21, 196, 137, 0, 0, 0, 13, 73, 68, 65, 84,
120, 1, 99, 249, 207, 240, 255, 63, 0, 7, 18, 3, 2, 164, 147, 160, 197, 0,
0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130];
void main() { void main() {
MemoryFileSystem fs; MemoryFileSystem fs;
FakePlatform platform; FakePlatform platform;
MockProcessManager process; MockProcessManager process;
MockHttpClient mockHttpClient;
setUp(() { setUp(() {
fs = MemoryFileSystem(); fs = MemoryFileSystem();
platform = FakePlatform(environment: <String, String>{'FLUTTER_ROOT': _kFlutterRoot}); platform = FakePlatform(
environment: <String, String>{'FLUTTER_ROOT': _kFlutterRoot},
operatingSystem: 'macos'
);
process = MockProcessManager(); process = MockProcessManager();
mockHttpClient = MockHttpClient();
fs.directory(_kFlutterRoot).createSync(recursive: true); fs.directory(_kFlutterRoot).createSync(recursive: true);
fs.directory(_kRepositoryRoot).createSync(recursive: true);
fs.file(_kVersionFile).createSync(recursive: true);
fs.file(_kVersionFile).writeAsStringSync(_kGoldensVersion);
}); });
group('GoldensClient', () { group('SkiaGoldClient', () {
GoldensRepositoryClient goldens; SkiaGoldClient skiaClient;
setUp(() { setUp(() {
goldens = GoldensRepositoryClient( final Directory workDirectory = fs.directory('/workDirectory')
..createSync(recursive: true);
skiaClient = SkiaGoldClient(
workDirectory,
fs: fs, fs: fs,
process: process, process: process,
platform: platform, platform: platform,
httpClient: mockHttpClient,
); );
}); });
group('prepare', () { group('auth', () {
test('performs minimal work if versions match', () async { test('performs minimal work if already authorized', () async {
when(process.run(any, workingDirectory: anyNamed('workingDirectory'))) fs.file('/workDirectory/temp/auth_opt.json')
.thenAnswer((_) => Future<io.ProcessResult>.value(io.ProcessResult(123, 0, _kGoldensVersion, ''))); ..createSync(recursive: true);
await goldens.prepare(); when(process.run(any))
.thenAnswer((_) => Future<ProcessResult>
.value(ProcessResult(123, 0, '', '')));
await skiaClient.auth();
// Verify that we only spawned `git rev-parse HEAD` verifyNever(process.run(
final VerificationResult verifyProcessRun = captureAny,
verify(process.run(captureAny, workingDirectory: captureAnyNamed('workingDirectory'))); workingDirectory: captureAnyNamed('workingDirectory'),
verifyProcessRun.called(1); ));
expect(verifyProcessRun.captured.first, <String>['git', 'rev-parse', 'HEAD']);
expect(verifyProcessRun.captured.last, _kRepositoryRoot);
});
}); });
}); });
group('SkiaGoldClient', () { group('Request Handling', () {
SkiaGoldClient goldens; String testName;
String pullRequestNumber;
String expectation;
Uri url;
MockHttpClientRequest mockHttpRequest;
setUp(() { setUp(() {
goldens = SkiaGoldClient( testName = 'flutter.golden_test.1.png';
fs: fs, pullRequestNumber = '1234';
process: process, expectation = '55109a4bed52acc780530f7a9aeff6c0';
platform: platform, mockHttpRequest = MockHttpClientRequest();
);
}); });
group('auth', () { test('validates SkiaDigest', () {
test('performs minimal work if already authorized', () async { final Map<String, dynamic> skiaJson = json.decode(digestResponseTemplate());
final Directory workDirectory = fs.directory('/workDirectory')..createSync(recursive: true); final SkiaGoldDigest digest = SkiaGoldDigest.fromJson(skiaJson['digest']);
fs.file('/workDirectory/temp/auth_opt.json')..createSync(recursive: true); expect(
when(process.run(any)).thenAnswer((_) => Future<io.ProcessResult>.value(io.ProcessResult(123, 0, '', ''))); digest.isValid(
await goldens.auth(workDirectory); platform,
'flutter.golden_test.1',
expectation,
),
isTrue,
);
});
// Verify that we spawned no process calls test('invalidates bad SkiaDigest - platform', () {
final VerificationResult verifyProcessRun = final Map<String, dynamic> skiaJson = json.decode(
verifyNever(process.run(captureAny, workingDirectory: captureAnyNamed('workingDirectory'))); digestResponseTemplate(platform: 'linux')
expect(verifyProcessRun.callCount, 0); );
final SkiaGoldDigest digest = SkiaGoldDigest.fromJson(skiaJson['digest']);
expect(
digest.isValid(
platform,
'flutter.golden_test.1',
expectation,
),
isFalse,
);
}); });
test('invalidates bad SkiaDigest - test name', () {
final Map<String, dynamic> skiaJson = json.decode(
digestResponseTemplate(testName: 'flutter.golden_test.2')
);
final SkiaGoldDigest digest = SkiaGoldDigest.fromJson(skiaJson['digest']);
expect(
digest.isValid(
platform,
'flutter.golden_test.1',
expectation,
),
isFalse,
);
}); });
test('invalidates bad SkiaDigest - expectation', () {
final Map<String, dynamic> skiaJson = json.decode(
digestResponseTemplate(expectation: '1deg543sf645erg44awqcc78')
);
final SkiaGoldDigest digest = SkiaGoldDigest.fromJson(skiaJson['digest']);
expect(
digest.isValid(
platform,
'flutter.golden_test.1',
expectation,
),
isFalse,
);
}); });
group('FlutterGoldenFileComparator', () { test('invalidates bad SkiaDigest - status', () {
test('calculates the basedir correctly', () async { final Map<String, dynamic> skiaJson = json.decode(
final MockSkiaGoldClient goldens = MockSkiaGoldClient(); digestResponseTemplate(status: 'negative')
final MockLocalFileComparator defaultComparator = MockLocalFileComparator(); );
final Directory flutterRoot = fs.directory('/foo')..createSync(recursive: true); final SkiaGoldDigest digest = SkiaGoldDigest.fromJson(skiaJson['digest']);
final Directory goldensRoot = flutterRoot.childDirectory('bar')..createSync(recursive: true); expect(
when(goldens.fs).thenReturn(fs); digest.isValid(
when(goldens.flutterRoot).thenReturn(flutterRoot); platform,
when(goldens.comparisonRoot).thenReturn(goldensRoot); 'flutter.golden_test.1',
when(defaultComparator.basedir).thenReturn(flutterRoot.childDirectory('baz').uri); expectation,
final Directory basedir = FlutterGoldenFileComparator.getBaseDirectory(goldens, defaultComparator); ),
expect(basedir.uri, fs.directory('/foo/bar/baz').uri); isFalse,
);
}); });
test('sets up expectations', () async {
url = Uri.parse('https://flutter-gold.skia.org/json/expectations/commit/HEAD');
final MockHttpClientResponse mockHttpResponse = MockHttpClientResponse(
utf8.encode(rawExpectationsTemplate())
);
when(mockHttpClient.getUrl(url))
.thenAnswer((_) => Future<MockHttpClientRequest>.value(mockHttpRequest));
when(mockHttpRequest.close())
.thenAnswer((_) => Future<MockHttpClientResponse>.value(mockHttpResponse));
await skiaClient.getExpectations();
expect(skiaClient.expectations, isNotNull);
expect(
skiaClient.expectations['flutter.golden_test.1'],
contains(expectation),
);
}); });
group('FlutterGoldensRepositoryFileComparator', () { test('detects invalid digests SkiaDigest', () {
MemoryFileSystem fs; const String testName = 'flutter.golden_test.2';
FlutterGoldensRepositoryFileComparator comparator; final Map<String, dynamic> skiaJson = json.decode(digestResponseTemplate());
final SkiaGoldDigest digest = SkiaGoldDigest.fromJson(skiaJson['digest']);
expect(digest.isValid(platform, testName, expectation), isFalse);
});
setUp(() { test('image bytes are processed properly', () async {
fs = MemoryFileSystem(); final Uri imageUrl = Uri.parse(
platform = FakePlatform( 'https://flutter-gold.skia.org/img/images/$expectation.png'
operatingSystem: 'linux',
environment: <String, String>{'FLUTTER_ROOT': _kFlutterRoot},
); );
final Directory flutterRoot = fs.directory('/path/to/flutter')..createSync(recursive: true); final MockHttpClientRequest mockImageRequest = MockHttpClientRequest();
final Directory goldensRoot = flutterRoot.childDirectory('bin/cache/goldens')..createSync(recursive: true); final MockHttpImageResponse mockImageResponse = MockHttpImageResponse(
final Directory testDirectory = goldensRoot.childDirectory('test/foo/bar')..createSync(recursive: true); imageResponseTemplate()
comparator = FlutterGoldensRepositoryFileComparator(
testDirectory.uri,
fs: fs,
platform: platform,
); );
}); when(mockHttpClient.getUrl(imageUrl))
.thenAnswer((_) => Future<MockHttpClientRequest>.value(mockImageRequest));
when(mockImageRequest.close())
.thenAnswer((_) => Future<MockHttpImageResponse>.value(mockImageResponse));
group('compare', () { final List<int> masterBytes = await skiaClient.getImageBytes(expectation);
test('throws if golden file is not found', () async {
try { expect(masterBytes, equals(_kTestPngBytes));
await comparator.compare(Uint8List.fromList(<int>[1, 2, 3]), Uri.parse('test.png'));
fail('TestFailure expected but not thrown');
} on TestFailure catch (error) {
expect(error.message, contains('Could not be compared against non-existent file'));
}
}); });
test('returns false if golden bytes do not match', () async { group('ignores', () {
final File goldenFile = fs.file('/path/to/flutter/bin/cache/goldens/test/foo/bar/test.png') Uri url;
..createSync(recursive: true); MockHttpClientRequest mockHttpRequest;
goldenFile.writeAsBytesSync(<int>[4, 5, 6], flush: true); MockHttpClientResponse mockHttpResponse;
final bool result = await comparator.compare(Uint8List.fromList(<int>[1, 2, 3]), Uri.parse('test.png'));
expect(result, isFalse); setUp(() {
url = Uri.parse('https://flutter-gold.skia.org/json/ignores');
mockHttpRequest = MockHttpClientRequest();
mockHttpResponse = MockHttpClientResponse(utf8.encode(
ignoreResponseTemplate(pullRequestNumber: pullRequestNumber)
));
when(mockHttpClient.getUrl(url))
.thenAnswer((_) => Future<MockHttpClientRequest>.value(mockHttpRequest));
when(mockHttpRequest.close())
.thenAnswer((_) => Future<MockHttpClientResponse>.value(mockHttpResponse));
}); });
test('returns true if golden bytes match', () async { test('returns true for ignored test and ignored pull request number', () async {
final File goldenFile = fs.file('/path/to/flutter/bin/cache/goldens/test/foo/bar/test.png') expect(
..createSync(recursive: true); await skiaClient.testIsIgnoredForPullRequest(
goldenFile.writeAsBytesSync(<int>[1, 2, 3], flush: true); pullRequestNumber,
final bool result = await comparator.compare(Uint8List.fromList(<int>[1, 2, 3]), Uri.parse('test.png')); testName,
expect(result, isTrue); ),
isTrue,
);
}); });
test('returns false for not ignored test and ignored pull request number', () async {
expect(
await skiaClient.testIsIgnoredForPullRequest(
'5678',
testName,
),
isFalse,
);
}); });
group('update', () { test('returns false for ignored test and not ignored pull request number', () async {
test('creates golden file if it does not already exist', () async { expect(
final File goldenFile = fs.file('/path/to/flutter/bin/cache/goldens/test/foo/bar/test.png'); await skiaClient.testIsIgnoredForPullRequest(
expect(goldenFile.existsSync(), isFalse); pullRequestNumber,
await comparator.update(Uri.parse('test.png'), Uint8List.fromList(<int>[1, 2, 3])); 'failure.png',
expect(goldenFile.existsSync(), isTrue); ),
expect(goldenFile.readAsBytesSync(), <int>[1, 2, 3]); isFalse,
);
});
}); });
test('overwrites golden bytes if golden file already exist', () async { group('digest parsing', () {
final File goldenFile = fs.file('/path/to/flutter/bin/cache/goldens/test/foo/bar/test.png') Uri url;
..createSync(recursive: true); MockHttpClientRequest mockHttpRequest;
goldenFile.writeAsBytesSync(<int>[4, 5, 6], flush: true); MockHttpClientResponse mockHttpResponse;
await comparator.update(Uri.parse('test.png'), Uint8List.fromList(<int>[1, 2, 3]));
expect(goldenFile.readAsBytesSync(), <int>[1, 2, 3]); setUp(() {
url = Uri.parse(
'https://flutter-gold.skia.org/json/details?'
'test=flutter.golden_test.1&digest=$expectation'
);
mockHttpRequest = MockHttpClientRequest();
when(mockHttpClient.getUrl(url))
.thenAnswer((_) => Future<MockHttpClientRequest>.value(mockHttpRequest));
}); });
test('succeeds when valid', () async {
mockHttpResponse = MockHttpClientResponse(utf8.encode(digestResponseTemplate()));
when(mockHttpRequest.close())
.thenAnswer((_) => Future<MockHttpClientResponse>.value(mockHttpResponse));
expect(
await skiaClient.isValidDigestForExpectation(
expectation,
testName,
),
isTrue,
);
}); });
group('getTestUri', () { test('fails when invalid', () async {
test('incorporates version number', () { mockHttpResponse = MockHttpClientResponse(utf8.encode(
final Uri key = comparator.getTestUri(Uri.parse('foo.png'), 1); digestResponseTemplate(platform: 'linux')
expect(key, Uri.parse('foo.1.png')); ));
when(mockHttpRequest.close())
.thenAnswer((_) => Future<MockHttpClientResponse>.value(mockHttpResponse));
expect(
await skiaClient.isValidDigestForExpectation(
expectation,
testName,
),
isFalse,
);
}); });
test('ignores null version number', () {
final Uri key = comparator.getTestUri(Uri.parse('foo.png'), null);
expect(key, Uri.parse('foo.png'));
}); });
}); });
}); });
group('FlutterSkiaGoldFileComparator', () { group('FlutterGoldenFileComparator', () {
FlutterSkiaGoldFileComparator comparator; FlutterSkiaGoldFileComparator comparator;
setUp(() { setUp(() {
final Directory flutterRoot = fs.directory('/path/to/flutter')..createSync(recursive: true); final Directory basedir = fs.directory('flutter/test/library/')
final Directory goldensRoot = flutterRoot.childDirectory('bin/cache/goldens')..createSync(recursive: true); ..createSync(recursive: true);
final Directory testDirectory = goldensRoot.childDirectory('test/foo/bar')..createSync(recursive: true);
comparator = FlutterSkiaGoldFileComparator( comparator = FlutterSkiaGoldFileComparator(
testDirectory.uri, basedir.uri,
MockSkiaGoldClient(), MockSkiaGoldClient(),
fs: fs, fs: fs,
platform: platform, platform: platform,
); );
}); });
group('getTestUri', () { test('calculates the basedir correctly from defaultComparator', () async {
final MockLocalFileComparator defaultComparator = MockLocalFileComparator();
final Directory flutterRoot = fs.directory(platform.environment['FLUTTER_ROOT'])
..createSync(recursive: true);
when(defaultComparator.basedir).thenReturn(flutterRoot.childDirectory('baz').uri);
final Directory basedir = FlutterGoldenFileComparator.getBaseDirectory(
defaultComparator,
platform,
);
expect(
basedir.uri,
fs.directory('/flutter/bin/cache/pkg/skia_goldens/baz').uri,
);
});
test('ignores version number', () { test('ignores version number', () {
final Uri key = comparator.getTestUri(Uri.parse('foo.png'), 1); final Uri key = comparator.getTestUri(Uri.parse('foo.png'), 1);
expect(key, Uri.parse('foo.png')); expect(key, Uri.parse('foo.png'));
}); });
group('Post-Submit', () {
final MockSkiaGoldClient mockSkiaClient = MockSkiaGoldClient();
setUp(() {
final Directory basedir = fs.directory('flutter/test/library/')
..createSync(recursive: true);
comparator = FlutterSkiaGoldFileComparator(
basedir.uri,
mockSkiaClient,
fs: fs,
platform: platform,
);
});
test('correctly determines testing environment', () {
platform = FakePlatform(
environment: <String, String>{
'FLUTTER_ROOT': _kFlutterRoot,
'CIRRUS_CI' : 'true',
'CIRRUS_PR' : '',
'CIRRUS_BRANCH' : 'master',
'GOLD_SERVICE_ACCOUNT' : 'service account...',
},
operatingSystem: 'macos'
);
expect(
FlutterSkiaGoldFileComparator.isAvailableForEnvironment(platform),
isTrue,
);
});
});
group('Pre-Submit', () {
FlutterPreSubmitFileComparator comparator;
final MockSkiaGoldClient mockSkiaClient = MockSkiaGoldClient();
setUp(() {
final Directory basedir = fs.directory('flutter/test/library/')
..createSync(recursive: true);
comparator = FlutterPreSubmitFileComparator(
basedir.uri,
mockSkiaClient,
fs: fs,
platform: FakePlatform(
environment: <String, String>{
'FLUTTER_ROOT': _kFlutterRoot,
'CIRRUS_CI' : 'true',
'CIRRUS_PR' : '1234',
},
operatingSystem: 'macos'
),
);
when(mockSkiaClient.getImageBytes('55109a4bed52acc780530f7a9aeff6c0'))
.thenAnswer((_) => Future<List<int>>.value(_kTestPngBytes));
when(mockSkiaClient.expectations)
.thenReturn(expectationsTemplate());
when(mockSkiaClient.cleanTestName('library.flutter.golden_test.1.png'))
.thenReturn('flutter.golden_test.1');
when(mockSkiaClient.isValidDigestForExpectation(
'55109a4bed52acc780530f7a9aeff6c0',
'library.flutter.golden_test.1.png',
))
.thenAnswer((_) => Future<bool>.value(false));
});
test('correctly determines testing environment', () {
platform = FakePlatform(
environment: <String, String>{
'FLUTTER_ROOT': _kFlutterRoot,
'CIRRUS_CI' : 'true',
'CIRRUS_PR' : '1234',
},
operatingSystem: 'macos'
);
expect(
FlutterPreSubmitFileComparator.isAvailableForEnvironment(platform),
isTrue,
);
});
test('comparison passes test that is ignored for this PR', () async {
when(mockSkiaClient.getImageBytes('55109a4bed52acc780530f7a9aeff6c0'))
.thenAnswer((_) => Future<List<int>>.value(_kTestPngBytes));
when(mockSkiaClient.testIsIgnoredForPullRequest(
'1234',
'library.flutter.golden_test.1.png',
))
.thenAnswer((_) => Future<bool>.value(true));
expect(
await comparator.compare(
Uint8List.fromList(_kFailPngBytes),
Uri.parse('flutter.golden_test.1.png'),
),
isTrue,
);
});
test('fails test that is not ignored for this PR', () async {
when(mockSkiaClient.getImageBytes('55109a4bed52acc780530f7a9aeff6c0'))
.thenAnswer((_) => Future<List<int>>.value(_kTestPngBytes));
when(mockSkiaClient.testIsIgnoredForPullRequest(
'1234',
'library.flutter.golden_test.1.png',
))
.thenAnswer((_) => Future<bool>.value(false));
expect(
await comparator.compare(
Uint8List.fromList(_kFailPngBytes),
Uri.parse('flutter.golden_test.1.png'),
),
isFalse,
);
});
test('passes non-existent baseline for new test', () async {
expect(
await comparator.compare(
Uint8List.fromList(_kFailPngBytes),
Uri.parse('flutter.new_golden_test.1.png'),
),
isTrue,
);
});
});
group('Local', () {
FlutterLocalFileComparator comparator;
final MockSkiaGoldClient mockSkiaClient = MockSkiaGoldClient();
setUp(() async {
final Directory basedir = fs.directory('flutter/test/library/')
..createSync(recursive: true);
comparator = FlutterLocalFileComparator(
basedir.uri,
mockSkiaClient,
fs: fs,
platform: FakePlatform(
environment: <String, String>{'FLUTTER_ROOT': _kFlutterRoot},
operatingSystem: 'macos'
),
);
when(mockSkiaClient.getImageBytes('55109a4bed52acc780530f7a9aeff6c0'))
.thenAnswer((_) => Future<List<int>>.value(_kTestPngBytes));
when(mockSkiaClient.expectations)
.thenReturn(expectationsTemplate());
when(mockSkiaClient.cleanTestName('library.flutter.golden_test.1.png'))
.thenReturn('flutter.golden_test.1');
when(mockSkiaClient.isValidDigestForExpectation(
'55109a4bed52acc780530f7a9aeff6c0',
'library.flutter.golden_test.1.png',
))
.thenAnswer((_) => Future<bool>.value(false));
});
test('passes when bytes match', () async {
expect(
await comparator.compare(
Uint8List.fromList(_kTestPngBytes),
Uri.parse('flutter.golden_test.1.png'),
),
isTrue,
);
});
test('passes non-existent baseline for new test', () async {
expect(
await comparator.compare(
Uint8List.fromList(_kFailPngBytes),
Uri.parse('flutter.new_golden_test.1'),
),
isTrue,
);
});
});
group('Skipping', () {
test('correctly determines testing environment', () {
platform = FakePlatform(
environment: <String, String>{
'FLUTTER_ROOT': _kFlutterRoot,
'SWARMING_TASK_ID' : '1234567890',
},
operatingSystem: 'macos'
);
expect(
FlutterSkippingGoldenFileComparator.isAvailableForEnvironment(platform),
isTrue,
);
});
}); });
}); });
} }
class MockProcessManager extends Mock implements ProcessManager {} class MockProcessManager extends Mock implements ProcessManager {}
class MockGoldensRepositoryClient extends Mock implements GoldensRepositoryClient {}
class MockSkiaGoldClient extends Mock implements SkiaGoldClient {} class MockSkiaGoldClient extends Mock implements SkiaGoldClient {}
class MockLocalFileComparator extends Mock implements LocalFileComparator {} class MockLocalFileComparator extends Mock implements LocalFileComparator {}
class MockHttpClient extends Mock implements HttpClient {}
class MockHttpClientRequest extends Mock implements HttpClientRequest {}
class MockHttpClientResponse extends Mock implements HttpClientResponse {
MockHttpClientResponse(this.response);
final Uint8List response;
@override
StreamSubscription<Uint8List> listen(
void onData(Uint8List event), {
Function onError,
void onDone(),
bool cancelOnError,
}) {
return Stream<Uint8List>.fromFuture(Future<Uint8List>.value(response))
.listen(onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError);
}
}
class MockHttpImageResponse extends Mock implements HttpClientResponse {
MockHttpImageResponse(this.response);
final List<List<int>> response;
@override
Future<void> forEach(void action(List<int> element)) async {
response.forEach(action);
}
}
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/// Json response template for Skia Gold expectations request:
/// https://flutter-gold.skia.org/json/expectations/commit/HEAD
String rawExpectationsTemplate() {
return '''
{
"md5": "a7489b00e03a1846e43500b7c14dd7b0",
"master": {
"flutter.golden_test.1": {
"55109a4bed52acc780530f7a9aeff6c0": 1
},
"flutter.golden_test.3": {
"87cb35131e6ad4b57d4d09d59ae743c3": 1,
"dc94eb2c39c0c8ae11a4efd090b72f94": 1,
"f2583c9003978a06b7888878bdc089e2": 1
},
"flutter.golden_test.2": {
"eb03a5e3114c9ecad5e4f1178f285a49": 1,
"f14631979de24fca6e14ad247d5f2bd6": 1
}
}
}
''';
}
/// Decoded json response template for Skia Gold expectations request:
/// https://flutter-gold.skia.org/json/expectations/commit/HEAD
Map<String, List<String>> expectationsTemplate() {
return <String, List<String>>{
'flutter.golden_test.1': <String>[
'55109a4bed52acc780530f7a9aeff6c0'
],
'flutter.golden_test.3': <String>[
'87cb35131e6ad4b57d4d09d59ae743c3',
'dc94eb2c39c0c8ae11a4efd090b72f94',
'f2583c9003978a06b7888878bdc089e2',
],
'flutter.golden_test.2': <String>[
'eb03a5e3114c9ecad5e4f1178f285a49',
'f14631979de24fca6e14ad247d5f2bd6',
],
};
}
/// Json response template for Skia Gold digest request:
/// https://flutter-gold.skia.org/json/details?test=[testName]&digest=[expectation]
String digestResponseTemplate({
String testName = 'flutter.golden_test.1',
String expectation = '55109a4bed52acc780530f7a9aeff6c0',
String platform = 'macos',
String status = 'positive',
}) {
return '''
{
"digest": {
"test": "$testName",
"digest": "$expectation",
"status": "$status",
"paramset": {
"Platform": [
"$platform"
],
"ext": [
"png"
],
"name": [
"$testName"
],
"source_type": [
"flutter"
]
},
"traces": {
"tileSize": 200,
"traces": [
{
"data": [
{
"x": 0,
"y": 0,
"s": 0
},
{
"x": 1,
"y": 0,
"s": 0
},
{
"x": 199,
"y": 0,
"s": 0
}
],
"label": ",Platform=$platform,name=$testName,source_type=flutter,",
"params": {
"Platform": "$platform",
"ext": "png",
"name": "$testName",
"source_type": "flutter"
}
}
],
"digests": [
{
"digest": "$expectation",
"status": "$status"
}
]
},
"closestRef": "pos",
"refDiffs": {
"neg": null,
"pos": {
"numDiffPixels": 999,
"pixelDiffPercent": 0.4995,
"maxRGBADiffs": [
86,
86,
86,
0
],
"dimDiffer": false,
"diffs": {
"combined": 0.381955,
"percent": 0.4995,
"pixel": 999
},
"digest": "aa748136c70cefdda646df5be0ae189d",
"status": "positive",
"paramset": {
"Platform": [
"macos"
],
"ext": [
"png"
],
"name": [
"$testName"
],
"source_type": [
"flutter"
]
},
"n": 197
}
}
},
"commits": [
{
"commit_time": 1568069344,
"hash": "399bb04e2de41665320d3c888f40af6d8bc734a2",
"author": "Contributor A (contributorA@getMail.com)"
},
{
"commit_time": 1568078053,
"hash": "0f365d3add253a65e5e5af1024f56c6169bf9739",
"author": "Contributor B (contributorB@getMail.com)"
},
{
"commit_time": 1569353925,
"hash": "81e693a7fe3b808cc9ae2bb3a2cbe404e67ec773",
"author": "Contributor C (contributorC@getMail.com)"
}
]
}
''';
}
/// Json response template for Skia Gold ignore request:
/// https://flutter-gold.skia.org/json/ignores
String ignoreResponseTemplate({
String pullRequestNumber = '0000',
String testName = 'flutter.golden_test.1',
}) {
return '''
[
{
"id": "7579425228619212078",
"name": "contributor@getMail.com",
"updatedBy": "contributor@getMail.com",
"expires": "2019-09-06T21:28:18.815336Z",
"query": "ext=png&name=$testName",
"note": "https://github.com/flutter/flutter/pull/$pullRequestNumber"
}
]
''';
}
/// Json response template for Skia Gold image request:
/// https://flutter-gold.skia.org/img/images/[imageHash].png
List<List<int>> imageResponseTemplate() {
return <List<int>>[
<int>[137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73,
72, 68, 82, 0, 0, 0, 1, 0, 0, 0, 1, 8, 6, 0, 0, 0, 31, 21, 196, 137, 0],
<int>[0, 0, 11, 73, 68, 65, 84, 120, 1, 99, 97, 0, 2, 0,
0, 25, 0, 5, 144, 240, 54, 245, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96,
130],
];
}
// Copyright 2018 The Chromium 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:async';
import 'dart:io' as io;
import 'package:file/file.dart';
import 'package:file/local.dart';
import 'package:platform/platform.dart';
import 'package:process/process.dart';
// If you are here trying to figure out how to use golden files in the Flutter
// repo itself, consider reading this wiki page:
// https://github.com/flutter/flutter/wiki/Writing-a-golden-file-test-for-package%3Aflutter
const String _kFlutterRootKey = 'FLUTTER_ROOT';
/// A base class that provides shared information to the
/// [FlutterGoldenFileComparator] as well as the [SkiaGoldClient] and
/// [GoldensRepositoryClient].
abstract class GoldensClient {
/// Creates a handle to the local environment of golden file images.
GoldensClient({
this.fs = const LocalFileSystem(),
this.platform = const LocalPlatform(),
this.process = const LocalProcessManager(),
});
/// The file system to use for storing the local clone of the repository.
///
/// This is useful in tests, where a local file system (the default) can
/// be replaced by a memory file system.
final FileSystem fs;
/// A wrapper for the [dart:io.Platform] API.
///
/// This is useful in tests, where the system platform (the default) can
/// be replaced by a mock platform instance.
final Platform platform;
/// A controller for launching subprocesses.
///
/// This is useful in tests, where the real process manager (the default)
/// can be replaced by a mock process manager that doesn't really create
/// subprocesses.
final ProcessManager process;
/// The local [Directory] where the Flutter repository is hosted.
///
/// Uses the [fs] file system.
Directory get flutterRoot => fs.directory(platform.environment[_kFlutterRootKey]);
/// The local [Directory] where the goldens files are located.
///
/// Uses the [fs] file system.
Directory get comparisonRoot => flutterRoot.childDirectory(fs.path.join('bin', 'cache', 'pkg', 'goldens'));
}
/// A class that represents a clone of the https://github.com/flutter/goldens
/// repository, nested within the `bin/cache` directory of the caller's Flutter
/// repository.
class GoldensRepositoryClient extends GoldensClient {
GoldensRepositoryClient({
FileSystem fs = const LocalFileSystem(),
ProcessManager process = const LocalProcessManager(),
Platform platform = const LocalPlatform(),
}) : super(
fs: fs,
process: process,
platform: platform,
);
RandomAccessFile _lock;
/// Prepares the local clone of the `flutter/goldens` repository for golden
/// file testing.
///
/// This ensures that the goldens repository has been cloned into its
/// expected location within `bin/cache` and that it is synced to the Git
/// revision specified in `bin/internal/goldens.version`.
///
/// While this is preparing the repository, it obtains a file lock such that
/// [GoldensClient] instances in other processes or isolates will not
/// duplicate the work that this is doing.
Future<void> prepare() async {
final String goldensCommit = await _getGoldensCommit();
String currentCommit = await _getCurrentCommit();
if (currentCommit != goldensCommit) {
await _obtainLock();
try {
// Check the current commit again now that we have the lock.
currentCommit = await _getCurrentCommit();
if (currentCommit != goldensCommit) {
if (currentCommit == null) {
await _initRepository();
}
await _checkCanSync();
await _syncTo(goldensCommit);
}
} finally {
await _releaseLock();
}
}
}
Future<String> _getCurrentCommit() async {
if (!comparisonRoot.existsSync()) {
return null;
} else {
final io.ProcessResult revParse = await process.run(
<String>['git', 'rev-parse', 'HEAD'],
workingDirectory: comparisonRoot.path,
);
return revParse.exitCode == 0 ? revParse.stdout.trim() : null;
}
}
Future<String> _getGoldensCommit() async {
final File versionFile = flutterRoot.childFile(fs.path.join('bin', 'internal', 'goldens.version'));
return (await versionFile.readAsString()).trim();
}
Future<void> _initRepository() async {
await comparisonRoot.create(recursive: true);
await _runCommands(
<String>[
'git init',
'git remote add upstream https://github.com/flutter/goldens.git',
'git remote set-url --push upstream git@github.com:flutter/goldens.git',
],
workingDirectory: comparisonRoot,
);
}
Future<void> _checkCanSync() async {
final io.ProcessResult result = await process.run(
<String>['git', 'status', '--porcelain'],
workingDirectory: comparisonRoot.path,
);
if (result.stdout.trim().isNotEmpty) {
final StringBuffer buf = StringBuffer()
..writeln('flutter_goldens git checkout at ${comparisonRoot.path} has local changes and cannot be synced.')
..writeln('To reset your client to a clean state, and lose any local golden test changes:')
..writeln('cd ${comparisonRoot.path}')
..writeln('git reset --hard HEAD')
..writeln('git clean -x -d -f -f');
throw NonZeroExitCode(1, buf.toString());
}
}
Future<void> _syncTo(String commit) async {
await _runCommands(
<String>[
'git pull upstream master',
'git fetch upstream $commit',
'git reset --hard FETCH_HEAD',
],
workingDirectory: comparisonRoot,
);
}
Future<void> _runCommands(
List<String> commands, {
Directory workingDirectory,
}) async {
for (String command in commands) {
final List<String> parts = command.split(' ');
final io.ProcessResult result = await process.run(
parts,
workingDirectory: workingDirectory?.path,
);
if (result.exitCode != 0) {
throw NonZeroExitCode(result.exitCode, result.stderr);
}
}
}
Future<void> _obtainLock() async {
final File lockFile = flutterRoot.childFile(fs.path.join('bin', 'cache', 'goldens.lockfile'));
await lockFile.create(recursive: true);
_lock = await lockFile.open(mode: io.FileMode.write);
await _lock.lock(io.FileLock.blockingExclusive);
}
Future<void> _releaseLock() async {
await _lock.close();
_lock = null;
}
}
/// Exception that signals a process' exit with a non-zero exit code.
class NonZeroExitCode implements Exception {
/// Create an exception that represents a non-zero exit code.
///
/// The first argument must be non-zero.
const NonZeroExitCode(this.exitCode, this.stderr) : assert(exitCode != 0);
/// The code that the process will signal to the operating system.
///
/// By definition, this is not zero.
final int exitCode;
/// The message to show on standard error.
final String stderr;
@override
String toString() {
return 'Exit code $exitCode: $stderr';
}
}
...@@ -12,30 +12,50 @@ import 'package:path/path.dart' as path; ...@@ -12,30 +12,50 @@ import 'package:path/path.dart' as path;
import 'package:platform/platform.dart'; import 'package:platform/platform.dart';
import 'package:process/process.dart'; import 'package:process/process.dart';
import 'package:flutter_goldens_client/client.dart';
// If you are here trying to figure out how to use golden files in the Flutter // If you are here trying to figure out how to use golden files in the Flutter
// repo itself, consider reading this wiki page: // repo itself, consider reading this wiki page:
// https://github.com/flutter/flutter/wiki/Writing-a-golden-file-test-for-package%3Aflutter // https://github.com/flutter/flutter/wiki/Writing-a-golden-file-test-for-package%3Aflutter
// TODO(Piinks): This file will replace ./client.dart when transition to Skia const String _kFlutterRootKey = 'FLUTTER_ROOT';
// Gold testing is complete
const String _kGoldctlKey = 'GOLDCTL'; const String _kGoldctlKey = 'GOLDCTL';
const String _kServiceAccountKey = 'GOLD_SERVICE_ACCOUNT'; const String _kServiceAccountKey = 'GOLD_SERVICE_ACCOUNT';
/// An extension of the [GoldensClient] class that interfaces with Skia Gold /// A client for uploading image tests and making baseline requests to the
/// for golden file testing. /// Flutter Gold Dashboard.
class SkiaGoldClient extends GoldensClient { class SkiaGoldClient {
SkiaGoldClient({ SkiaGoldClient(
FileSystem fs = const LocalFileSystem(), this.workDirectory, {
ProcessManager process = const LocalProcessManager(), this.fs = const LocalFileSystem(),
Platform platform = const LocalPlatform(), this.process = const LocalProcessManager(),
}) : super( this.platform = const LocalPlatform(),
fs: fs, io.HttpClient httpClient,
process: process, }) : assert(workDirectory != null),
platform: platform, assert(fs != null),
); assert(process != null),
assert(platform != null),
httpClient = httpClient ?? io.HttpClient();
/// The file system to use for storing the local clone of the repository.
///
/// This is useful in tests, where a local file system (the default) can
/// be replaced by a memory file system.
final FileSystem fs;
/// A wrapper for the [dart:io.Platform] API.
///
/// This is useful in tests, where the system platform (the default) can
/// be replaced by a mock platform instance.
final Platform platform;
/// A controller for launching sub-processes.
///
/// This is useful in tests, where the real process manager (the default)
/// can be replaced by a mock process manager that doesn't really create
/// sub-processes.
final ProcessManager process;
/// A client for making Http requests to the Flutter Gold dashboard.
final io.HttpClient httpClient;
/// The local [Directory] within the [comparisonRoot] for the current test /// The local [Directory] within the [comparisonRoot] for the current test
/// context. In this directory, the client will create image and json files /// context. In this directory, the client will create image and json files
...@@ -43,7 +63,21 @@ class SkiaGoldClient extends GoldensClient { ...@@ -43,7 +63,21 @@ class SkiaGoldClient extends GoldensClient {
/// ///
/// This is informed by the [FlutterGoldenFileComparator] [basedir]. It cannot /// This is informed by the [FlutterGoldenFileComparator] [basedir]. It cannot
/// be null. /// be null.
Directory _workDirectory; final Directory workDirectory;
/// A map of known golden file tests and their associated positive image
/// hashes.
///
/// This is set and used by the [FlutterLocalFileComparator] and
/// [FlutterPreSubmitFileComparator] to test against golden masters maintained
/// in the Flutter Gold dashboard.
Map<String, List<String>> get expectations => _expectations;
Map<String, List<String>> _expectations;
/// The local [Directory] where the Flutter repository is hosted.
///
/// Uses the [fs] file system.
Directory get _flutterRoot => fs.directory(platform.environment[_kFlutterRootKey]);
/// The path to the local [Directory] where the goldctl tool is hosted. /// The path to the local [Directory] where the goldctl tool is hosted.
/// ///
...@@ -56,9 +90,6 @@ class SkiaGoldClient extends GoldensClient { ...@@ -56,9 +90,6 @@ class SkiaGoldClient extends GoldensClient {
/// Uses the [platform] environment in this implementation. /// Uses the [platform] environment in this implementation.
String get _serviceAccount => platform.environment[_kServiceAccountKey]; String get _serviceAccount => platform.environment[_kServiceAccountKey];
@override
Directory get comparisonRoot => flutterRoot.childDirectory(fs.path.join('bin', 'cache', 'pkg', 'skia_goldens'));
/// Prepares the local work space for golden file testing and calls the /// Prepares the local work space for golden file testing and calls the
/// goldctl `auth` command. /// goldctl `auth` command.
/// ///
...@@ -69,39 +100,31 @@ class SkiaGoldClient extends GoldensClient { ...@@ -69,39 +100,31 @@ class SkiaGoldClient extends GoldensClient {
/// The [workDirectory] parameter specifies the current directory that golden /// The [workDirectory] parameter specifies the current directory that golden
/// tests are executing in, relative to the library of the given test. It is /// tests are executing in, relative to the library of the given test. It is
/// informed by the basedir of the [FlutterSkiaGoldFileComparator]. /// informed by the basedir of the [FlutterSkiaGoldFileComparator].
Future<void> auth(Directory workDirectory) async { Future<void> auth() async {
assert(workDirectory != null);
_workDirectory = workDirectory;
if (_clientIsAuthorized()) if (_clientIsAuthorized())
return; return;
if (_serviceAccount.isEmpty) { if (_serviceAccount.isEmpty) {
final StringBuffer buf = StringBuffer()..writeln('Gold service account is unavailable.'); final StringBuffer buf = StringBuffer()
..writeln('Gold service account is unavailable.');
throw NonZeroExitCode(1, buf.toString()); throw NonZeroExitCode(1, buf.toString());
} }
final File authorization = _workDirectory.childFile('serviceAccount.json'); final File authorization = workDirectory.childFile('serviceAccount.json');
await authorization.writeAsString(_serviceAccount); await authorization.writeAsString(_serviceAccount);
final List<String> authArguments = <String>[ final List<String> authArguments = <String>[
'auth', 'auth',
'--service-account', authorization.path, '--service-account', authorization.path,
'--work-dir', _workDirectory.childDirectory('temp').path, '--work-dir', workDirectory
.childDirectory('temp')
.path,
]; ];
// final io.ProcessResult authResults =
await io.Process.run( await io.Process.run(
_goldctl, _goldctl,
authArguments, authArguments,
); );
// TODO(Piinks): Re-enable after Gold flakes are resolved, https://github.com/flutter/flutter/pull/36103
// if (authResults.exitCode != 0) {
// final StringBuffer buf = StringBuffer()
// ..writeln('Flutter + Skia Gold auth failed.')
// ..writeln('stdout: ${authResults.stdout}')
// ..writeln('stderr: ${authResults.stderr}');
// throw NonZeroExitCode(authResults.exitCode, buf.toString());
// }
} }
/// Executes the `imgtest init` command in the goldctl tool. /// Executes the `imgtest init` command in the goldctl tool.
...@@ -109,8 +132,8 @@ class SkiaGoldClient extends GoldensClient { ...@@ -109,8 +132,8 @@ class SkiaGoldClient extends GoldensClient {
/// The `imgtest` command collects and uploads test results to the Skia Gold /// The `imgtest` command collects and uploads test results to the Skia Gold
/// backend, the `init` argument initializes the current test. /// backend, the `init` argument initializes the current test.
Future<void> imgtestInit() async { Future<void> imgtestInit() async {
final File keys = _workDirectory.childFile('keys.json'); final File keys = workDirectory.childFile('keys.json');
final File failures = _workDirectory.childFile('failures.json'); final File failures = workDirectory.childFile('failures.json');
await keys.writeAsString(_getKeysJSON()); await keys.writeAsString(_getKeysJSON());
await failures.create(); await failures.create();
...@@ -119,7 +142,9 @@ class SkiaGoldClient extends GoldensClient { ...@@ -119,7 +142,9 @@ class SkiaGoldClient extends GoldensClient {
final List<String> imgtestInitArguments = <String>[ final List<String> imgtestInitArguments = <String>[
'imgtest', 'init', 'imgtest', 'init',
'--instance', 'flutter', '--instance', 'flutter',
'--work-dir', _workDirectory.childDirectory('temp').path, '--work-dir', workDirectory
.childDirectory('temp')
.path,
'--commit', commitHash, '--commit', commitHash,
'--keys-file', keys.path, '--keys-file', keys.path,
'--failure-file', failures.path, '--failure-file', failures.path,
...@@ -127,26 +152,16 @@ class SkiaGoldClient extends GoldensClient { ...@@ -127,26 +152,16 @@ class SkiaGoldClient extends GoldensClient {
]; ];
if (imgtestInitArguments.contains(null)) { if (imgtestInitArguments.contains(null)) {
final StringBuffer buf = StringBuffer(); final StringBuffer buf = StringBuffer()
buf.writeln('Null argument for Skia Gold imgtest init:'); ..writeln('Null argument for Skia Gold imgtest init:');
imgtestInitArguments.forEach(buf.writeln); imgtestInitArguments.forEach(buf.writeln);
throw NonZeroExitCode(1, buf.toString()); throw NonZeroExitCode(1, buf.toString());
} }
// final io.ProcessResult imgtestInitResult =
await io.Process.run( await io.Process.run(
_goldctl, _goldctl,
imgtestInitArguments, imgtestInitArguments,
); );
// TODO(Piinks): Re-enable after Gold flakes are resolved, https://github.com/flutter/flutter/pull/36103
// if (imgtestInitResult.exitCode != 0) {
// final StringBuffer buf = StringBuffer()
// ..writeln('Flutter + Skia Gold imgtest init failed.')
// ..writeln('stdout: ${imgtestInitResult.stdout}')
// ..writeln('stderr: ${imgtestInitResult.stderr}');
// throw NonZeroExitCode(imgtestInitResult.exitCode, buf.toString());
// }
} }
/// Executes the `imgtest add` command in the goldctl tool. /// Executes the `imgtest add` command in the goldctl tool.
...@@ -164,8 +179,10 @@ class SkiaGoldClient extends GoldensClient { ...@@ -164,8 +179,10 @@ class SkiaGoldClient extends GoldensClient {
final List<String> imgtestArguments = <String>[ final List<String> imgtestArguments = <String>[
'imgtest', 'add', 'imgtest', 'add',
'--work-dir', _workDirectory.childDirectory('temp').path, '--work-dir', workDirectory
'--test-name', testName.split(path.extension(testName.toString()))[0], .childDirectory('temp')
.path,
'--test-name', cleanTestName(testName),
'--png-file', goldenFile.path, '--png-file', goldenFile.path,
]; ];
...@@ -173,25 +190,145 @@ class SkiaGoldClient extends GoldensClient { ...@@ -173,25 +190,145 @@ class SkiaGoldClient extends GoldensClient {
_goldctl, _goldctl,
imgtestArguments, imgtestArguments,
); );
// TODO(Piinks): Comment on PR if triage is needed, https://github.com/flutter/flutter/issues/34673
// So as not to turn the tree red in this initial implementation, this will
// return true for now.
// The ProcessResult that returns from line 157 contains the pass/fail
// result of the test & links to the dashboard and diffs.
return true; return true;
} }
/// Requests and sets the [_expectations] known to Flutter Gold at head.
Future<void> getExpectations() async {
_expectations = <String, List<String>>{};
await io.HttpOverrides.runWithHttpOverrides<Future<void>>(() async {
final Uri requestForExpectations = Uri.parse(
'https://flutter-gold.skia.org/json/expectations/commit/HEAD'
);
String rawResponse;
try {
final io.HttpClientRequest request = await httpClient.getUrl(requestForExpectations);
final io.HttpClientResponse response = await request.close();
rawResponse = await utf8.decodeStream(response);
final Map<String, dynamic> skiaJson = json.decode(rawResponse)['master'];
skiaJson.forEach((String key, dynamic value) {
final Map<String, dynamic> hashesMap = value;
_expectations[key] = hashesMap.keys.toList();
});
} on FormatException catch(_) {
print('Formatting error detected requesting expectations from Flutter Gold.\n'
'rawResponse: $rawResponse');
rethrow;
}
},
SkiaGoldHttpOverrides(),
);
}
/// Returns a list of bytes representing the golden image retrieved from the
/// Flutter Gold dashboard.
///
/// The provided image hash represents an expectation from Flutter Gold.
Future<List<int>>getImageBytes(String imageHash) async {
final List<int> imageBytes = <int>[];
await io.HttpOverrides.runWithHttpOverrides<Future<void>>(() async {
final Uri requestForImage = Uri.parse(
'https://flutter-gold.skia.org/img/images/$imageHash.png',
);
try {
final io.HttpClientRequest request = await httpClient.getUrl(requestForImage);
final io.HttpClientResponse response = await request.close();
await response.forEach((List<int> bytes) => imageBytes.addAll(bytes));
} catch(e) {
rethrow;
}
},
SkiaGoldHttpOverrides(),
);
return imageBytes;
}
/// Returns a boolean value for whether or not the given test and current pull
/// request are ignored on Flutter Gold.
///
/// This is only relevant when used by the [FlutterPreSubmitFileComparator].
/// In order to land a change to an exiting golden file, an ignore must be set
/// up in Flutter Gold. This will serve as a flag to permit the change to
/// land, and protect against any unwanted changes.
Future<bool> testIsIgnoredForPullRequest(String pullRequest, String testName) async {
bool ignoreIsActive = false;
testName = cleanTestName(testName);
String rawResponse;
await io.HttpOverrides.runWithHttpOverrides<Future<void>>(() async {
final Uri requestForIgnores = Uri.parse(
'https://flutter-gold.skia.org/json/ignores'
);
try {
final io.HttpClientRequest request = await httpClient.getUrl(requestForIgnores);
final io.HttpClientResponse response = await request.close();
rawResponse = await utf8.decodeStream(response);
final List<dynamic> ignores = json.decode(rawResponse);
for(Map<String, dynamic> ignore in ignores) {
final List<String> ignoredQueries = ignore['query'].split('&');
final String ignoredPullRequest = ignore['note'].split('/').last;
if (ignoredQueries.contains('name=$testName') &&
ignoredPullRequest == pullRequest) {
ignoreIsActive = true;
break;
}
}
} on FormatException catch(_) {
print('Formatting error detected requesting ignores from Flutter Gold.\n'
'rawResponse: $rawResponse');
rethrow;
}
},
SkiaGoldHttpOverrides(),
);
return ignoreIsActive;
}
/// The [_expectations] retrieved from Flutter Gold do not include the
/// parameters of the given test. This function queries the Flutter Gold
/// details api to determine if the given expectation for a test matches the
/// configuration of the executing machine.
Future<bool> isValidDigestForExpectation(String expectation, String testName) async {
bool isValid = false;
testName = cleanTestName(testName);
String rawResponse;
await io.HttpOverrides.runWithHttpOverrides<Future<void>>(() async {
final Uri requestForDigest = Uri.parse(
'https://flutter-gold.skia.org/json/details?test=$testName&digest=$expectation'
);
try {
final io.HttpClientRequest request = await httpClient.getUrl(requestForDigest);
final io.HttpClientResponse response = await request.close();
rawResponse = await utf8.decodeStream(response);
final Map<String, dynamic> skiaJson = json.decode(rawResponse);
final SkiaGoldDigest digest = SkiaGoldDigest.fromJson(skiaJson['digest']);
isValid = digest.isValid(platform, testName, expectation);
} on FormatException catch(_) {
print('Formatting error detected requesting digest from Flutter Gold.\n'
'rawResponse: $rawResponse');
rethrow;
}
},
SkiaGoldHttpOverrides(),
);
return isValid;
}
/// Returns the current commit hash of the Flutter repository. /// Returns the current commit hash of the Flutter repository.
Future<String> _getCurrentCommit() async { Future<String> _getCurrentCommit() async {
if (!flutterRoot.existsSync()) { if (!_flutterRoot.existsSync()) {
final StringBuffer buf = StringBuffer() final StringBuffer buf = StringBuffer()
..writeln('Flutter root could not be found: $flutterRoot'); ..writeln('Flutter root could not be found: $_flutterRoot');
throw NonZeroExitCode(1, buf.toString()); throw NonZeroExitCode(1, buf.toString());
} else { } else {
final io.ProcessResult revParse = await process.run( final io.ProcessResult revParse = await process.run(
<String>['git', 'rev-parse', 'HEAD'], <String>['git', 'rev-parse', 'HEAD'],
workingDirectory: flutterRoot.path, workingDirectory: _flutterRoot.path,
); );
return revParse.exitCode == 0 ? revParse.stdout.trim() : null; return revParse.exitCode == 0 ? revParse.stdout.trim() : null;
} }
...@@ -210,13 +347,85 @@ class SkiaGoldClient extends GoldensClient { ...@@ -210,13 +347,85 @@ class SkiaGoldClient extends GoldensClient {
); );
} }
/// Removes the file extension from the [fileName] to represent the test name
/// properly.
String cleanTestName(String fileName) {
return fileName.split(path.extension(fileName.toString()))[0];
}
/// Returns a boolean value to prevent the client from re-authorizing itself /// Returns a boolean value to prevent the client from re-authorizing itself
/// for multiple tests. /// for multiple tests.
bool _clientIsAuthorized() { bool _clientIsAuthorized() {
final File authFile = _workDirectory?.childFile(super.fs.path.join( final File authFile = workDirectory?.childFile(fs.path.join(
'temp', 'temp',
'auth_opt.json', 'auth_opt.json',
)); ));
return authFile.existsSync(); return authFile.existsSync();
} }
} }
/// Used to make HttpRequests during testing.
class SkiaGoldHttpOverrides extends io.HttpOverrides {}
/// A digest returned from a request to the Flutter Gold dashboard.
class SkiaGoldDigest {
const SkiaGoldDigest({
this.imageHash,
this.paramSet,
this.testName,
this.status,
});
/// Create a digest from requested json.
factory SkiaGoldDigest.fromJson(Map<String, dynamic> json) {
if (json == null)
return null;
return SkiaGoldDigest(
imageHash: json['digest'],
paramSet: Map<String, dynamic>.from(json['paramset'] ??
<String, String>{'Platform': 'none'}),
testName: json['test'],
status: json['status'],
);
}
/// Unique identifier for the image associated with the digest.
final String imageHash;
/// Parameter set for the given test, e.g. Platform : Windows.
final Map<String, dynamic> paramSet;
/// Test name associated with the digest, e.g. positive or untriaged.
final String testName;
/// Status of the given digest, e.g. positive or untriaged.
final String status;
/// Validates a given digest against the current testing conditions.
bool isValid(Platform platform, String name, String expectation) {
return imageHash == expectation
&& paramSet['Platform'].contains(platform.operatingSystem)
&& testName == name
&& status == 'positive';
}
}
/// Exception that signals a process' exit with a non-zero exit code.
class NonZeroExitCode implements Exception {
/// Create an exception that represents a non-zero exit code.
///
/// The first argument must be non-zero.
const NonZeroExitCode(this.exitCode, this.stderr) : assert(exitCode != 0);
/// The code that the process will signal to the operating system.
///
/// By definition, this is not zero.
final int exitCode;
/// The message to show on standard error.
final String stderr;
@override
String toString() => 'Exit code $exitCode: $stderr';
}
...@@ -15,8 +15,8 @@ import 'goldens.dart'; ...@@ -15,8 +15,8 @@ import 'goldens.dart';
/// The default [GoldenFileComparator] implementation for `flutter test`. /// The default [GoldenFileComparator] implementation for `flutter test`.
/// ///
/// The term __golden file__ refers to a master image that is considered the true /// The term __golden file__ refers to a master image that is considered the
/// rendering of a given widget, state, application, or other visual /// true rendering of a given widget, state, application, or other visual
/// representation you have chosen to capture. This comparator loads golden /// representation you have chosen to capture. This comparator loads golden
/// files from the local file system, treating the golden key as a relative /// files from the local file system, treating the golden key as a relative
/// path from the test file's directory. /// path from the test file's directory.
...@@ -53,7 +53,7 @@ import 'goldens.dart'; ...@@ -53,7 +53,7 @@ import 'goldens.dart';
/// implements. /// implements.
/// * [matchesGoldenFile], the function from [flutter_test] that invokes the /// * [matchesGoldenFile], the function from [flutter_test] that invokes the
/// comparator. /// comparator.
class LocalFileComparator extends GoldenFileComparator { class LocalFileComparator extends GoldenFileComparator with LocalComparisonOutput {
/// Creates a new [LocalFileComparator] for the specified [testFile]. /// Creates a new [LocalFileComparator] for the specified [testFile].
/// ///
/// Golden file keys will be interpreted as file paths relative to the /// Golden file keys will be interpreted as file paths relative to the
...@@ -90,24 +90,18 @@ class LocalFileComparator extends GoldenFileComparator { ...@@ -90,24 +90,18 @@ class LocalFileComparator extends GoldenFileComparator {
Future<bool> compare(Uint8List imageBytes, Uri golden) async { Future<bool> compare(Uint8List imageBytes, Uri golden) async {
final File goldenFile = _getGoldenFile(golden); final File goldenFile = _getGoldenFile(golden);
if (!goldenFile.existsSync()) { if (!goldenFile.existsSync()) {
throw test_package.TestFailure('Could not be compared against non-existent file: "$golden"'); throw test_package.TestFailure(
'Could not be compared against non-existent file: "$golden"'
);
} }
final List<int> goldenBytes = await goldenFile.readAsBytes(); final List<int> goldenBytes = await goldenFile.readAsBytes();
final ComparisonResult result = GoldenFileComparator.compareLists(imageBytes, goldenBytes); final ComparisonResult result = GoldenFileComparator.compareLists(
imageBytes,
goldenBytes,
);
if (!result.passed) { if (!result.passed) {
String additionalFeedback = ''; generateFailureOutput(result, golden, basedir);
if (result.diffs != null) {
additionalFeedback = '\nFailure feedback can be found at ${path.join(basedir.path, 'failures')}';
final Map<String, Object> diffs = result.diffs;
diffs.forEach((String name, Object untypedImage) {
final Image image = untypedImage;
final File output = _getFailureFile(name, golden);
output.parent.createSync(recursive: true);
output.writeAsBytesSync(encodePng(image));
});
}
throw test_package.TestFailure('Golden "$golden": ${result.error}$additionalFeedback');
} }
return result.passed; return result.passed;
} }
...@@ -122,16 +116,50 @@ class LocalFileComparator extends GoldenFileComparator { ...@@ -122,16 +116,50 @@ class LocalFileComparator extends GoldenFileComparator {
File _getGoldenFile(Uri golden) { File _getGoldenFile(Uri golden) {
return File(_path.join(_path.fromUri(basedir), _path.fromUri(golden.path))); return File(_path.join(_path.fromUri(basedir), _path.fromUri(golden.path)));
} }
}
/// A class for use in golden file comparators that run locally and provide
/// output.
class LocalComparisonOutput {
/// Writes out diffs from the [ComparisonResult] of a golden file test.
///
/// Will throw an error if a null result is provided.
void generateFailureOutput(
ComparisonResult result,
Uri golden,
Uri basedir, {
String key = '',
}) {
String additionalFeedback = '';
if (result.diffs != null) {
additionalFeedback = '\nFailure feedback can be found at '
'${path.join(basedir.path, 'failures')}';
final Map<String, Image> diffs = result.diffs;
diffs.forEach((String name, Image image) {
final File output = getFailureFile(
key.isEmpty ? name : name + '_' + key,
golden,
basedir,
);
output.parent.createSync(recursive: true);
output.writeAsBytesSync(encodePng(image));
});
}
throw test_package.TestFailure(
'Golden "$golden": ${result.error}$additionalFeedback'
);
}
File _getFailureFile(String failure, Uri golden) { /// Returns the appropriate file for a given diff from a [ComparisonResult].
File getFailureFile(String failure, Uri golden, Uri basedir) {
final String fileName = golden.pathSegments[0]; final String fileName = golden.pathSegments[0];
final String testName = fileName.split(path.extension(fileName))[0] final String testName = fileName.split(path.extension(fileName))[0]
+ '_' + '_'
+ failure + failure
+ '.png'; + '.png';
return File(_path.join( return File(path.join(
_path.fromUri(basedir), path.fromUri(basedir),
_path.fromUri(Uri.parse('failures/$testName')), path.fromUri(Uri.parse('failures/$testName')),
)); ));
} }
} }
...@@ -203,7 +231,9 @@ ComparisonResult compareLists(List<int> test, List<int> master) { ...@@ -203,7 +231,9 @@ ComparisonResult compareLists(List<int> test, List<int> master) {
if (pixelDiffCount > 0) { if (pixelDiffCount > 0) {
return ComparisonResult( return ComparisonResult(
passed: false, passed: false,
error: 'Pixel test failed, ${((pixelDiffCount/totalPixels) * 100).toStringAsFixed(2)}% diff detected.', error: 'Pixel test failed, '
'${((pixelDiffCount/totalPixels) * 100).toStringAsFixed(2)}% '
'diff detected.',
diffs: diffs, diffs: diffs,
); );
} }
......
...@@ -303,9 +303,7 @@ Matcher coversSameAreaAs(Path expectedPath, { @required Rect areaToCompare, int ...@@ -303,9 +303,7 @@ Matcher coversSameAreaAs(Path expectedPath, { @required Rect areaToCompare, int
/// The [key] may be either a [Uri] or a [String] representation of a URI. /// The [key] may be either a [Uri] or a [String] representation of a URI.
/// ///
/// The [version] is a number that can be used to differentiate historical /// The [version] is a number that can be used to differentiate historical
/// golden files. This parameter is optional. Version numbers are used in golden /// golden files. This parameter is optional.
/// file tests for package:flutter. You can learn more about these tests
/// [here](https://github.com/flutter/flutter/wiki/Writing-a-golden-file-test-for-package:flutter).
/// ///
/// This is an asynchronous matcher, meaning that callers should use /// This is an asynchronous matcher, meaning that callers should use
/// [expectLater] when using this matcher and await the future returned by /// [expectLater] when using this matcher and await the future returned by
...@@ -336,7 +334,10 @@ Matcher coversSameAreaAs(Path expectedPath, { @required Rect areaToCompare, int ...@@ -336,7 +334,10 @@ Matcher coversSameAreaAs(Path expectedPath, { @required Rect areaToCompare, int
/// ///
/// await expectLater( /// await expectLater(
/// imageFuture, /// imageFuture,
/// matchesGoldenFile('save.png'), /// matchesGoldenFile(
/// 'save.png',
/// version: 2,
/// ),
/// ); /// );
/// ///
/// await expectLater( /// await expectLater(
......
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