From 791555001262f8b27e9fa15a980036cffe3cbca4 Mon Sep 17 00:00:00 2001 From: Matt Carroll Date: Tue, 8 Jul 2025 18:21:31 -0700 Subject: [PATCH] [Feature] - Added PixelSnapCenter, PixelSnapAlign, PixelSnapRow, PixelSnapColumn, PixelSnapFlex, and used them throughout the codebase (Resolves #65) --- .../platform_adaptive_gallery_test.dart | 4 +- doc/marketing_goldens/shadcn_test_tools.dart | 8 +- doc/website/bin/main.dart | 1 + doc/website/source/_data.yaml | 4 + .../source/reduce-flakiness/_data.yaml | 1 + .../failure_centered-square.png | Bin 0 -> 11382 bytes .../reduce-flakiness/position-and-size.md | 159 +++++++++ .../source/styles/docs_page_layout.scss | 7 +- golden_tester.Dockerfile | 2 +- lib/flutter_test_goldens.dart | 1 + lib/src/flutter/flutter_pixel_alignment.dart | 306 ++++++++++++++++++ lib/src/flutter/flutter_test_extensions.dart | 6 + lib/src/scenes/failure_scene.dart | 6 +- lib/src/scenes/golden_scene.dart | 32 +- .../layouts/animation_timeline_layout.dart | 15 +- lib/src/scenes/layouts/grid_layout.dart | 14 +- lib/src/scenes/layouts/magazine_layout.dart | 5 +- .../scenes/layouts/row_and_column_layout.dart | 3 +- lib/src/scenes/single_shot.dart | 9 + test/flutter/pixel_snapping_test.dart | 298 +++++++++++++++++ .../intentional_failures/failing_test.dart | 5 +- .../gallery/gallery_grid_layout.png | Bin 0 -> 10306 bytes .../gallery/gallery_grid_test.dart | 64 ++++ test_goldens/theming/scoped_theme_test.dart | 4 +- ...ilure_text_layout_with_partial_pixel_1.png | Bin 1882 -> 0 bytes .../known_failure_cases.dart | 30 +- 26 files changed, 924 insertions(+), 60 deletions(-) create mode 100644 doc/website/source/reduce-flakiness/_data.yaml create mode 100644 doc/website/source/reduce-flakiness/failure_centered-square.png create mode 100644 doc/website/source/reduce-flakiness/position-and-size.md create mode 100644 lib/src/flutter/flutter_pixel_alignment.dart create mode 100644 test/flutter/pixel_snapping_test.dart create mode 100644 test_goldens/scene_types/gallery/gallery_grid_layout.png create mode 100644 test_goldens/scene_types/gallery/gallery_grid_test.dart delete mode 100644 test_goldens_known_failure_cases/failures/failure_text_layout_with_partial_pixel_1.png diff --git a/doc/marketing_goldens/platform_adaptive_gallery/platform_adaptive_gallery_test.dart b/doc/marketing_goldens/platform_adaptive_gallery/platform_adaptive_gallery_test.dart index 839309b..b18607e 100644 --- a/doc/marketing_goldens/platform_adaptive_gallery/platform_adaptive_gallery_test.dart +++ b/doc/marketing_goldens/platform_adaptive_gallery/platform_adaptive_gallery_test.dart @@ -97,12 +97,12 @@ Widget _itemDecorator( ) { return Padding( padding: const EdgeInsets.all(24), - child: Column( + child: PixelSnapColumn( mainAxisSize: MainAxisSize.min, children: [ content, Divider(), - Row( + PixelSnapRow( mainAxisSize: MainAxisSize.min, spacing: 16, children: [ diff --git a/doc/marketing_goldens/shadcn_test_tools.dart b/doc/marketing_goldens/shadcn_test_tools.dart index 3b0226c..0970155 100644 --- a/doc/marketing_goldens/shadcn_test_tools.dart +++ b/doc/marketing_goldens/shadcn_test_tools.dart @@ -87,7 +87,7 @@ class ShadcnSingleShotSceneLayout implements SceneLayout { margin: const EdgeInsets.all(48), padding: const EdgeInsets.all(48), color: ShadBlueColorScheme.dark().background, - child: Column( + child: PixelSnapColumn( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, spacing: 24, @@ -148,7 +148,7 @@ class ShadcnGalleryLayout implements SceneLayout { ), child: Padding( padding: const EdgeInsets.all(48), - child: Column( + child: PixelSnapColumn( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, spacing: 24, @@ -187,7 +187,7 @@ class ShadcnGalleryLayout implements SceneLayout { child: Container( padding: const EdgeInsets.all(48), color: ShadBlueColorScheme.dark().background, - child: Column( + child: PixelSnapColumn( mainAxisSize: MainAxisSize.min, spacing: 24, children: [ @@ -251,7 +251,7 @@ Widget shadcnItemDecorator( ) { return ColoredBox( color: ShadBlueColorScheme.dark().background, - child: Column( + child: PixelSnapColumn( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ diff --git a/doc/website/bin/main.dart b/doc/website/bin/main.dart index 97f730e..b0512fb 100644 --- a/doc/website/bin/main.dart +++ b/doc/website/bin/main.dart @@ -8,6 +8,7 @@ Future main(List arguments) async { // Here, you can directly hook into the StaticShock pipeline. For example, // you can copy an "images" directory from the source set to build set: ..pick(DirectoryPicker.parse("images")) + ..pick(ExtensionPicker("png")) // All 3rd party behavior is added through plugins, even the behavior // shipped with Static Shock. ..plugin(const MarkdownPlugin()) diff --git a/doc/website/source/_data.yaml b/doc/website/source/_data.yaml index 4e42059..ef55212 100644 --- a/doc/website/source/_data.yaml +++ b/doc/website/source/_data.yaml @@ -53,6 +53,10 @@ navigation: tag: failure-scenes sortBy: navOrder + - title: Reduce Flakiness + tag: reduce-flakiness + sortBy: navOrder + - title: Flutter's Implementation tag: flutter-implementation sortBy: navOrder \ No newline at end of file diff --git a/doc/website/source/reduce-flakiness/_data.yaml b/doc/website/source/reduce-flakiness/_data.yaml new file mode 100644 index 0000000..0529c7f --- /dev/null +++ b/doc/website/source/reduce-flakiness/_data.yaml @@ -0,0 +1 @@ +tags: reduce-flakiness diff --git a/doc/website/source/reduce-flakiness/failure_centered-square.png b/doc/website/source/reduce-flakiness/failure_centered-square.png new file mode 100644 index 0000000000000000000000000000000000000000..719dadbb61be294272fb59af8523e28d1240e84d GIT binary patch literal 11382 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 tester.view.reset()); + + // Now do your real test work... +}); +``` + +When you use `flutter_test_goldens` test runners, you don't need to worry about this. It's done +automatically, on your behalf. + +```dart +testGoldenScene("my golden test", (tester) async { + // Just worry about your test, we've already changed the devicePixelRatio to 1.0... +}); +``` + +So I guess what we're saying is...if you use `flutter_test_goldens` you don't need to +worry about anything you just read :) + +## Positioning and Sizing Widgets +You may not think about it, but it's very common for widgets to be sized or positioned +at partial pixel boundaries. + +For example: Imagine a `25x25px` square that's centered within a `100x100` area. The top/left +offset of the centered square would be `(87.5, 87.5)`. + +For example: Imagine a `Row` that's `100px` wide, with 3 squares (`25x25px`) spaced evenly across it. +Those squares would sit at x-values of `6.25px`, `37.6px`, and `68.75px`, respectively. + +You may not be able to control these details within your app UI, but there's one place +where it's very important to control these values - and that's within Golden Scene layouts. + +Typical Golden Scene layouts include rows, columns, grids, and any other layout strategy +that you choose to employ. A Golden Scene layout is responsible for placing every golden image +in the scene. These golden images are later extracted for comparison within tests. Therefore, +it's absolutely critical that a Golden Scene places golden images precisely on whole-pixel +boundaries. If a Golden Scene positions an individual golden image on a partial boundary, the +scene will interpolate the color of the pixels on the edge of the golden, which will change +at least one pixel of detail all around the perimeter of the image. This will cause the golden +to fail, even when run on the exact same machine that generated it. + +To help our team, and your team, to create expressive Golden Scenes without breaking golden +comparison, we've published variations of a few of your favorite widgets, which now snap +their children to whole-pixel locations and sizes. + + * `PixelSnapCenter`: Like `Center` but with offset and size snapping. + * `PixelSnapAlign`: Like `Align` but with offset and size snapping. + * `PixelSnapRow`: Like `Row` but with offset and size snapping. + * `PixelSnapColumn`: Like `Column` but with offset and size snapping. + * `PixelSnapFlex`: Like `Flex` but with offset and size snapping. + +Take the earlier example of a square centered in a larger area. We can fix that +situation with a `PixelSnapCenter`. + +First, the bad version. + +```dart +SizedBox( + width: 50, + height: 50, + child: Center( + child: Container( + width: 24.5, + height: 24.5, + color: Colors.red, + ), + ), +); +``` + +The bad version includes a square with a partial-pixel size of `24.5x24.5px`, and that +square is located at `(12.75, 12.75)`. + +Let's snap the offset and the size with `PixelSnapCenter`. + +```dart +SizedBox( + width: 50, + height: 50, + child: PixelSnapCenter( // <-- the change + child: Container( + width: 24.5, + height: 24.5, + color: Colors.red, + ), + ), +); +``` + +The version with `PixelSnapCenter` positions the square at `(12, 12)` and forces the +square to become `(24, 24)`. No more partial pixels. + +These snapping widgets MUST be used when building Golden Scenes, to ensure that new/updated +goldens are consistent with extracted goldens. However, if desired, these widgets can also +be used in your regular widget trees. diff --git a/doc/website/source/styles/docs_page_layout.scss b/doc/website/source/styles/docs_page_layout.scss index 78036ef..92d926a 100644 --- a/doc/website/source/styles/docs_page_layout.scss +++ b/doc/website/source/styles/docs_page_layout.scss @@ -258,7 +258,7 @@ main.page-content { } h2, h3, h4, h5, h6 { - margin-top: 1em; + margin-top: 2em; } p { @@ -280,6 +280,11 @@ main.page-content { margin-bottom: 1.5em; } + img { + display: block; + margin: auto; + } + pre code { margin-top: 1em; margin-bottom: 1em; diff --git a/golden_tester.Dockerfile b/golden_tester.Dockerfile index b61f350..10b3100 100644 --- a/golden_tester.Dockerfile +++ b/golden_tester.Dockerfile @@ -13,7 +13,7 @@ RUN apt install -y git curl unzip RUN cat /etc/lsb-release # Invalidate the cache when flutter pushes a new commit. -ADD https://api.github.com/repos/flutter/flutter/git/refs/heads/master ./flutter-latest-master +ADD https://api.github.com/repos/flutter/flutter/git/refs/heads/stable ./flutter-latest-stable RUN git clone https://github.com/flutter/flutter.git ${FLUTTER_HOME} diff --git a/lib/flutter_test_goldens.dart b/lib/flutter_test_goldens.dart index 04ff04f..786b49c 100644 --- a/lib/flutter_test_goldens.dart +++ b/lib/flutter_test_goldens.dart @@ -1,5 +1,6 @@ export 'src/flutter/flutter_camera.dart'; export 'src/flutter/flutter_golden_matcher.dart'; +export 'src/flutter/flutter_pixel_alignment.dart'; export 'src/flutter/flutter_test_extensions.dart'; export 'src/fonts/fonts.dart'; export 'src/fonts/icons.dart'; diff --git a/lib/src/flutter/flutter_pixel_alignment.dart b/lib/src/flutter/flutter_pixel_alignment.dart new file mode 100644 index 0000000..3da7c68 --- /dev/null +++ b/lib/src/flutter/flutter_pixel_alignment.dart @@ -0,0 +1,306 @@ +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; + +/// A widget that works like [Center], except the child's final offset is +/// forced to an integer value, e.g., `(50.4, 30.2)` -> `(50.0, 30.0)`, also +/// the size of the child can optionally be forced to an integer value, too. +/// +/// {@template integer_pixel_flakiness} +/// Integer pixel positioning is a strategy to reduce flakiness in golden +/// tests, though it still isn't perfect. +/// {@endtemplate} +class PixelSnapCenter extends SingleChildRenderObjectWidget { + const PixelSnapCenter({ + super.key, + this.snapSize = true, + super.child, + }); + + final bool snapSize; + + @override + RenderPositionedBoxAtPixel createRenderObject(BuildContext context) { + return RenderPositionedBoxAtPixel() // + ..alignment = Alignment.center + ..snapSize = snapSize; + } + + @override + void updateRenderObject(BuildContext context, RenderPositionedBoxAtPixel renderObject) { + renderObject.snapSize = snapSize; + } +} + +/// A widget that works like [Align], except the child's final offset is +/// forced to an integer value, e.g., `(50.4, 30.2)` -> `(50.0, 30.0)`. +/// +/// {@macro integer_pixel_flakiness} +class PixelSnapAlign extends SingleChildRenderObjectWidget { + const PixelSnapAlign({ + super.key, + required this.alignment, + this.snapSize = true, + super.child, + }); + + final Alignment alignment; + final bool snapSize; + + @override + RenderObject createRenderObject(BuildContext context) { + return RenderPositionedBoxAtPixel() // + ..alignment = alignment + ..snapSize = snapSize; + } + + @override + void updateRenderObject(BuildContext context, RenderPositionedBoxAtPixel renderObject) { + renderObject // + ..alignment = alignment + ..snapSize = snapSize; + } +} + +/// A [RenderPositionedBox] subclass that ensures each child sits at a whole-pixel (x, y) offset +/// and (optionally) forces children to be sized at integer values. +/// +/// {@template render_pixel_snap_performance} +/// This render object works by running the standard layout and then making adjustments +/// after the fact. This isn't great for performance, but this render object is intended +/// for tests where such performance isn't critical. +/// {@endtemplate} +class RenderPositionedBoxAtPixel extends RenderPositionedBox { + bool _snapSize = true; + set snapSize(bool newValue) { + if (newValue == _snapSize) { + return; + } + + _snapSize = newValue; + markNeedsLayout(); + } + + @override + void performLayout() { + super.performLayout(); + + // Re-position (and maybe resize) child so that it sits on a whole-pixel. + final child = this.child; + if (child != null) { + final parentData = child.parentData as BoxParentData; + final offset = parentData.offset; + if (offset.dx != offset.dx.floorToDouble() || offset.dy != offset.dy.floorToDouble()) { + parentData.offset = Offset( + offset.dx.floorToDouble(), + offset.dy.floorToDouble(), + ); + } + + if (_snapSize && + (child.size.width != child.size.width.floorToDouble() || + child.size.height != child.size.height.floorToDouble())) { + // This child doesn't have an integer width/height - run layout again, + // forcing the nearest smaller size. + child.layout( + BoxConstraints.tightFor( + width: child.size.width.floorToDouble(), + height: child.size.height.floorToDouble(), + ), + ); + } + } + } +} + +/// A [Row] whose children are positioned at whole-pixel offsets, and whose +/// children are forced to layout at whole-pixel widths and heights. +/// +/// {@macro integer_pixel_flakiness} +class PixelSnapRow extends Row { + const PixelSnapRow({ + super.key, + super.mainAxisAlignment, + super.mainAxisSize, + super.crossAxisAlignment, + super.textDirection, + super.verticalDirection, + super.textBaseline, // NO DEFAULT: we don't know what the text's baseline should be + super.spacing, + super.children, + }); + + @override + RenderPixelSnapFlex createRenderObject(BuildContext context) { + return RenderPixelSnapFlex( + direction: direction, + mainAxisAlignment: mainAxisAlignment, + mainAxisSize: mainAxisSize, + crossAxisAlignment: crossAxisAlignment, + textDirection: getEffectiveTextDirection(context), + verticalDirection: verticalDirection, + textBaseline: textBaseline, + clipBehavior: clipBehavior, + spacing: spacing, + ); + } + + @override + void updateRenderObject(BuildContext context, RenderPixelSnapFlex renderObject) { + renderObject + ..direction = direction + ..mainAxisAlignment = mainAxisAlignment + ..mainAxisSize = mainAxisSize + ..crossAxisAlignment = crossAxisAlignment + ..textDirection = getEffectiveTextDirection(context) + ..verticalDirection = verticalDirection + ..textBaseline = textBaseline + ..clipBehavior = clipBehavior + ..spacing = spacing; + } +} + +/// A [Column] whose children are positioned at whole-pixel offsets, and whose +/// children are forced to layout at whole-pixel widths and heights. +/// +/// {@macro integer_pixel_flakiness} +class PixelSnapColumn extends Column { + const PixelSnapColumn({ + super.key, + super.mainAxisAlignment, + super.mainAxisSize, + super.crossAxisAlignment, + super.textDirection, + super.verticalDirection, + super.textBaseline, + super.spacing, + super.children, + }); + + @override + RenderPixelSnapFlex createRenderObject(BuildContext context) { + return RenderPixelSnapFlex( + direction: direction, + mainAxisAlignment: mainAxisAlignment, + mainAxisSize: mainAxisSize, + crossAxisAlignment: crossAxisAlignment, + textDirection: getEffectiveTextDirection(context), + verticalDirection: verticalDirection, + textBaseline: textBaseline, + clipBehavior: clipBehavior, + spacing: spacing, + ); + } + + @override + void updateRenderObject(BuildContext context, RenderPixelSnapFlex renderObject) { + renderObject + ..direction = direction + ..mainAxisAlignment = mainAxisAlignment + ..mainAxisSize = mainAxisSize + ..crossAxisAlignment = crossAxisAlignment + ..textDirection = getEffectiveTextDirection(context) + ..verticalDirection = verticalDirection + ..textBaseline = textBaseline + ..clipBehavior = clipBehavior + ..spacing = spacing; + } +} + +/// A [Flex] whose children are positioned at whole-pixel offsets, and whose +/// children are forced to layout at whole-pixel widths and heights. +/// +/// {@macro integer_pixel_flakiness} +class PixelSnapFlex extends Flex { + const PixelSnapFlex({ + super.key, + required super.direction, + super.mainAxisAlignment, + super.mainAxisSize, + super.crossAxisAlignment, + super.textDirection, + super.verticalDirection, + super.textBaseline, + super.spacing, + super.children, + }); + + @override + RenderPixelSnapFlex createRenderObject(BuildContext context) { + return RenderPixelSnapFlex( + direction: direction, + mainAxisAlignment: mainAxisAlignment, + mainAxisSize: mainAxisSize, + crossAxisAlignment: crossAxisAlignment, + textDirection: getEffectiveTextDirection(context), + verticalDirection: verticalDirection, + textBaseline: textBaseline, + clipBehavior: clipBehavior, + spacing: spacing, + ); + } + + @override + void updateRenderObject(BuildContext context, RenderPixelSnapFlex renderObject) { + renderObject + ..direction = direction + ..mainAxisAlignment = mainAxisAlignment + ..mainAxisSize = mainAxisSize + ..crossAxisAlignment = crossAxisAlignment + ..textDirection = getEffectiveTextDirection(context) + ..verticalDirection = verticalDirection + ..textBaseline = textBaseline + ..clipBehavior = clipBehavior + ..spacing = spacing; + } +} + +/// A [RenderFlex] subclass that ensures each child sits at a whole-pixel (x, y) offset +/// and (optionally) forces children to be sized at integer values. +/// +/// {@macro render_pixel_snap_performance} +class RenderPixelSnapFlex extends RenderFlex { + RenderPixelSnapFlex({ + required super.direction, + super.mainAxisSize = MainAxisSize.max, + super.mainAxisAlignment = MainAxisAlignment.start, + super.crossAxisAlignment = CrossAxisAlignment.center, + super.textDirection, + super.verticalDirection = VerticalDirection.down, + super.textBaseline, + super.clipBehavior = Clip.none, + super.spacing = 0.0, + super.children, + }); + + @override + void performLayout() { + super.performLayout(); + + // Re-position children so that they sit on a whole-pixel. + final children = getChildrenAsList(); + for (final child in children) { + final parentData = child.parentData as BoxParentData; + final offset = parentData.offset; + if (offset.dx != offset.dx.floorToDouble() || offset.dy != offset.dy.floorToDouble()) { + // This child doesn't have an integer x/y offset - change the offset to + // be the nearest lesser integer offset. + parentData.offset = Offset( + offset.dx.floorToDouble(), + offset.dy.floorToDouble(), + ); + } + + if (child.size.width != child.size.width.floorToDouble() || + child.size.height != child.size.height.floorToDouble()) { + // This child doesn't have an integer width/height - run layout again, + // forcing the nearest smaller size. + child.layout( + BoxConstraints.tightFor( + width: child.size.width.floorToDouble(), + height: child.size.height.floorToDouble(), + ), + ); + } + } + } +} diff --git a/lib/src/flutter/flutter_test_extensions.dart b/lib/src/flutter/flutter_test_extensions.dart index fb2b56e..d62fe56 100644 --- a/lib/src/flutter/flutter_test_extensions.dart +++ b/lib/src/flutter/flutter_test_extensions.dart @@ -102,3 +102,9 @@ extension FlutterTestGoldens on WidgetTester { return Future.wait(futures); } } + +extension Snapshot on WidgetTester { + Future takePhoto(String name, {Finder? finder}) async { + expectLater(finder ?? find.byType(WidgetsApp), matchesGoldenFile(name)); + } +} diff --git a/lib/src/scenes/failure_scene.dart b/lib/src/scenes/failure_scene.dart index 41ec9fc..e5e2b07 100644 --- a/lib/src/scenes/failure_scene.dart +++ b/lib/src/scenes/failure_scene.dart @@ -543,19 +543,19 @@ Widget _itemDecorator( return Padding( padding: const EdgeInsets.all(24), child: IntrinsicWidth( - child: Column( + child: PixelSnapColumn( mainAxisSize: MainAxisSize.min, spacing: 4, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Row( + PixelSnapRow( children: [ Expanded(child: Text('Golden')), Expanded(child: Text('Candidate')), ], ), content, - Row( + PixelSnapRow( children: [ Expanded(child: Text('Absolute Diff')), Expanded(child: Text('Relative Diff')), diff --git a/lib/src/scenes/golden_scene.dart b/lib/src/scenes/golden_scene.dart index 885a8f6..ca2010b 100644 --- a/lib/src/scenes/golden_scene.dart +++ b/lib/src/scenes/golden_scene.dart @@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart' show Colors, MaterialApp, Scaffold, ThemeData; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_test_goldens/src/flutter/flutter_pixel_alignment.dart'; import 'package:flutter_test_goldens/src/fonts/fonts.dart'; import 'package:flutter_test_goldens/src/goldens/golden_collections.dart'; import 'package:flutter_test_goldens/src/goldens/golden_comparisons.dart'; @@ -213,22 +214,23 @@ Widget defaultGoldenSceneItemDecorator( return ColoredBox( // TODO: need this to be configurable, e.g., light vs dark color: Colors.white, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Align( - alignment: Alignment.centerLeft, - child: content, - ), - Padding( - padding: const EdgeInsets.all(24), - child: Text( - metadata.description, - style: TextStyle(fontFamily: TestFonts.openSans), + child: IntrinsicWidth( + child: PixelSnapColumn( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + PixelSnapCenter( + child: content, + ), + Padding( + padding: const EdgeInsets.all(24), + child: Text( + metadata.description, + style: TextStyle(fontFamily: TestFonts.openSans), + ), ), - ), - ], + ], + ), ), ); } diff --git a/lib/src/scenes/layouts/animation_timeline_layout.dart b/lib/src/scenes/layouts/animation_timeline_layout.dart index 6b7c3e1..5c94843 100644 --- a/lib/src/scenes/layouts/animation_timeline_layout.dart +++ b/lib/src/scenes/layouts/animation_timeline_layout.dart @@ -2,6 +2,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_test_goldens/src/flutter/flutter_pixel_alignment.dart'; import 'package:flutter_test_goldens/src/fonts/fonts.dart'; import 'package:flutter_test_goldens/src/goldens/golden_collections.dart'; import 'package:flutter_test_goldens/src/goldens/golden_rendering.dart'; @@ -129,7 +130,7 @@ class AnimationTimelineGoldenScene extends StatelessWidget { Widget _buildGoldens() { return Padding( padding: spacing.around, - child: Column( + child: PixelSnapColumn( mainAxisSize: MainAxisSize.min, children: [ Text( @@ -162,7 +163,7 @@ class AnimationTimelineGoldenScene extends StatelessWidget { Widget _buildRows() { final itemRows = _breakDownRows(); - return Column( + return PixelSnapColumn( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, spacing: spacing.between, @@ -241,17 +242,17 @@ class AnimationTimelineGoldenScene extends StatelessWidget { } Widget _buildRow(List> items) { - return Column( + return PixelSnapColumn( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( + PixelSnapRow( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.start, spacing: spacing.between, children: [ for (final entry in items) // - Column( + PixelSnapColumn( mainAxisSize: MainAxisSize.min, children: [ IntrinsicWidth( @@ -283,7 +284,7 @@ class AnimationTimelineGoldenScene extends StatelessWidget { ), Divider(height: 2, thickness: 2, color: _accentColor), const SizedBox(height: 16), - Row( + PixelSnapRow( children: [ Text( "Start >", @@ -320,7 +321,7 @@ Widget _itemDecorator( ) { return ColoredBox( color: const Color(0xff020817), - child: Column( + child: PixelSnapColumn( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ diff --git a/lib/src/scenes/layouts/grid_layout.dart b/lib/src/scenes/layouts/grid_layout.dart index 8d88cef..d16568c 100644 --- a/lib/src/scenes/layouts/grid_layout.dart +++ b/lib/src/scenes/layouts/grid_layout.dart @@ -78,13 +78,12 @@ class GridGoldenScene extends StatelessWidget { Widget _buildGoldens() { final entries = goldens.entries.toList(); - final rows = []; + final rows = []; for (int row = 0; row < goldens.length / 3; row += 1) { final items = []; for (int col = 0; col < 3; col += 1) { final index = row * 3 + col; if (index >= entries.length) { - items.add(const SizedBox()); continue; } @@ -92,7 +91,6 @@ class GridGoldenScene extends StatelessWidget { Padding( padding: EdgeInsets.only( top: row > 0 ? defaultGridSpacing.between : 0, - left: col > 0 ? defaultGridSpacing.between : 0, ), child: Builder(builder: (context) { return _decorator( @@ -111,14 +109,18 @@ class GridGoldenScene extends StatelessWidget { } rows.add( - TableRow( + PixelSnapRow( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + spacing: spacing.between, children: items, ), ); } - return Table( - defaultColumnWidth: IntrinsicColumnWidth(), + return PixelSnapColumn( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, children: rows, ); } diff --git a/lib/src/scenes/layouts/magazine_layout.dart b/lib/src/scenes/layouts/magazine_layout.dart index 6442e0a..cdc15a1 100644 --- a/lib/src/scenes/layouts/magazine_layout.dart +++ b/lib/src/scenes/layouts/magazine_layout.dart @@ -4,6 +4,7 @@ import 'dart:ui'; import 'package:flutter/material.dart' show Colors; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_test_goldens/src/flutter/flutter_pixel_alignment.dart'; import 'package:flutter_test_goldens/src/fonts/fonts.dart'; import 'package:flutter_test_goldens/src/goldens/golden_collections.dart'; import 'package:flutter_test_goldens/src/goldens/golden_rendering.dart'; @@ -101,7 +102,7 @@ class MagazineGoldenScene extends StatelessWidget { Widget build(BuildContext context) { return GoldenSceneBounds( child: IntrinsicHeight( - child: Row( + child: PixelSnapRow( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Stack( @@ -193,7 +194,7 @@ class MagazineGoldenScene extends StatelessWidget { IntrinsicWidth( child: Padding( padding: const EdgeInsets.all(12), - child: Column( + child: PixelSnapColumn( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ diff --git a/lib/src/scenes/layouts/row_and_column_layout.dart b/lib/src/scenes/layouts/row_and_column_layout.dart index 8332f75..d80a385 100644 --- a/lib/src/scenes/layouts/row_and_column_layout.dart +++ b/lib/src/scenes/layouts/row_and_column_layout.dart @@ -1,5 +1,6 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_test_goldens/src/flutter/flutter_pixel_alignment.dart'; import 'package:flutter_test_goldens/src/goldens/golden_collections.dart'; import 'package:flutter_test_goldens/src/goldens/golden_rendering.dart'; import 'package:flutter_test_goldens/src/scenes/golden_scene.dart'; @@ -118,7 +119,7 @@ class FlexGoldenScene extends StatelessWidget { Widget _buildGoldens() { return Padding( padding: spacing.around, - child: Flex( + child: PixelSnapFlex( direction: direction, mainAxisSize: MainAxisSize.min, spacing: spacing.between, diff --git a/lib/src/scenes/single_shot.dart b/lib/src/scenes/single_shot.dart index 0c51373..8c9972b 100644 --- a/lib/src/scenes/single_shot.dart +++ b/lib/src/scenes/single_shot.dart @@ -51,6 +51,15 @@ class SingleShotConfigurator { ); } + SingleShotConfigurator withConstraints(BoxConstraints constraints) { + _ensureStepNotComplete("constraints"); + + return SingleShotConfigurator( + _config.copyWith(constraints: constraints), + {..._stepsCompleted, "constraints"}, + ); + } + SingleShotConfigurator withSetup(GoldenSetup setup) { _ensureStepNotComplete("setup"); diff --git a/test/flutter/pixel_snapping_test.dart b/test/flutter/pixel_snapping_test.dart new file mode 100644 index 0000000..3a0843e --- /dev/null +++ b/test/flutter/pixel_snapping_test.dart @@ -0,0 +1,298 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_test_goldens/flutter_test_goldens.dart'; + +void main() { + group("Pixel snapping >", () { + testWidgets("PixelSnapCenter", (tester) async { + _configureWindow(tester); + + final contentKey = GlobalKey(); + + // Show regular Center behavior. + await _pumpScaffold(tester, _CenteredSquareAtPartialPixel(contentKey)); + expect( + tester.getTopLeft(find.byKey(contentKey)), + Offset(25.35, 25.35), + ); + expect(tester.getSize(find.byKey(contentKey)), Size(49.3, 49.3)); + + // Show PixelSnapCenter behavior (no size snapping). + await _pumpScaffold(tester, _CenteredSquareAtPartialPixel(contentKey, snapOffset: true)); + + // Ensure a whole-pixel offset, but fractional size. + expect( + tester.getTopLeft(find.byKey(contentKey)), + Offset(25, 25), + ); + expect(tester.getSize(find.byKey(contentKey)), Size(49.3, 49.3)); + + // Show PixelSnapCenter behavior (with size snapping). + await _pumpScaffold(tester, _CenteredSquareAtPartialPixel(contentKey, snapOffset: true, snapSize: true)); + + // Ensure a whole-pixel offset, AND whole-pixel size. + expect( + tester.getTopLeft(find.byKey(contentKey)), + Offset(25, 25), + ); + expect(tester.getSize(find.byKey(contentKey)), Size(49, 49)); + }); + + testWidgets("PixelSnapAlign", (tester) async { + _configureWindow(tester); + + final contentKey = GlobalKey(); + + // Show regular Align behavior. + await _pumpScaffold(tester, _AlignedSquareAtPartialPixel(contentKey)); + expect( + tester.getTopLeft(find.byKey(contentKey)), + Offset(13.435500000000001, 13.435500000000001), + ); + expect(tester.getSize(find.byKey(contentKey)), Size(49.3, 49.3)); + + // Show PixelSnapCenter behavior (no size snapping). + await _pumpScaffold(tester, _AlignedSquareAtPartialPixel(contentKey, snapOffset: true)); + + // Ensure a whole-pixel offset. + expect( + tester.getTopLeft(find.byKey(contentKey)), + Offset(13, 13), + ); + expect(tester.getSize(find.byKey(contentKey)), Size(49.3, 49.3)); + + // Show PixelSnapCenter behavior (with size snapping). + await _pumpScaffold(tester, _AlignedSquareAtPartialPixel(contentKey, snapOffset: true, snapSize: true)); + + // Ensure a whole-pixel offset, AND size snapping. + expect( + tester.getTopLeft(find.byKey(contentKey)), + Offset(13, 13), + ); + expect(tester.getSize(find.byKey(contentKey)), Size(49, 49)); + }); + + testWidgets("PixelSnapRow", (tester) async { + _configureWindow(tester); + + final item1Key = GlobalKey(); + final item2Key = GlobalKey(); + final item3Key = GlobalKey(); + + // Show regular Align behavior. + await _pumpScaffold( + tester, + _RowWithPartialPixels(item1Key: item1Key, item2Key: item2Key, item3Key: item3Key), + ); + expect(tester.getTopLeft(find.byKey(item1Key)), Offset(7.524999999999999, 38.35)); + expect(tester.getSize(find.byKey(item1Key)), Size(23.3, 23.3)); + expect(tester.getTopLeft(find.byKey(item2Key)), Offset(38.349999999999994, 38.35)); + expect(tester.getSize(find.byKey(item2Key)), Size(23.3, 23.3)); + expect(tester.getTopLeft(find.byKey(item3Key)), Offset(69.175, 38.35)); + expect(tester.getSize(find.byKey(item3Key)), Size(23.3, 23.3)); + + // Show PixelSnapAlign behavior. + await _pumpScaffold( + tester, + _RowWithPartialPixels(snap: true, item1Key: item1Key, item2Key: item2Key, item3Key: item3Key), + ); + + // Ensure a whole-pixel offset. + expect(tester.getTopLeft(find.byKey(item1Key)), Offset(7, 38)); + expect(tester.getSize(find.byKey(item1Key)), Size(23, 23)); + expect(tester.getTopLeft(find.byKey(item2Key)), Offset(38, 38)); + expect(tester.getSize(find.byKey(item2Key)), Size(23, 23)); + expect(tester.getTopLeft(find.byKey(item3Key)), Offset(69, 38)); + expect(tester.getSize(find.byKey(item3Key)), Size(23, 23)); + }); + + testWidgets("PixelSnapColumn", (tester) async { + _configureWindow(tester); + + final item1Key = GlobalKey(); + final item2Key = GlobalKey(); + final item3Key = GlobalKey(); + + // Show regular Align behavior. + await _pumpScaffold( + tester, + _ColumnWithPartialPixels(item1Key: item1Key, item2Key: item2Key, item3Key: item3Key), + ); + expect(tester.getTopLeft(find.byKey(item1Key)), Offset(38.35, 7.524999999999999)); + expect(tester.getSize(find.byKey(item1Key)), Size(23.3, 23.3)); + expect(tester.getTopLeft(find.byKey(item2Key)), Offset(38.35, 38.349999999999994)); + expect(tester.getSize(find.byKey(item2Key)), Size(23.3, 23.3)); + expect(tester.getTopLeft(find.byKey(item3Key)), Offset(38.35, 69.175)); + expect(tester.getSize(find.byKey(item3Key)), Size(23.3, 23.3)); + + // Show PixelSnapAlign behavior. + await _pumpScaffold( + tester, + _ColumnWithPartialPixels(snap: true, item1Key: item1Key, item2Key: item2Key, item3Key: item3Key), + ); + + // Ensure a whole-pixel offset. + expect(tester.getTopLeft(find.byKey(item1Key)), Offset(38, 7)); + expect(tester.getSize(find.byKey(item1Key)), Size(23, 23)); + expect(tester.getTopLeft(find.byKey(item2Key)), Offset(38, 38)); + expect(tester.getSize(find.byKey(item2Key)), Size(23, 23)); + expect(tester.getTopLeft(find.byKey(item3Key)), Offset(38, 69)); + expect(tester.getSize(find.byKey(item3Key)), Size(23, 23)); + }); + }); +} + +Future _pumpScaffold(WidgetTester tester, Widget content) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: content, + ), + debugShowCheckedModeBanner: false, + ), + ); +} + +void _configureWindow(WidgetTester tester) { + tester.view + ..devicePixelRatio = 1 + ..physicalSize = Size(100, 100); +} + +class _CenteredSquareAtPartialPixel extends StatelessWidget { + const _CenteredSquareAtPartialPixel(this.contentKey, {this.snapOffset = false, this.snapSize = false}); + + final Key contentKey; + final bool snapOffset; + final bool snapSize; + + @override + Widget build(BuildContext context) { + final square = Container( + key: contentKey, + width: 49.3, + height: 49.3, + color: Colors.red, + ); + + return snapOffset // + ? PixelSnapCenter( + snapSize: snapSize, + child: square, + ) + : Center(child: square); + } +} + +class _AlignedSquareAtPartialPixel extends StatelessWidget { + const _AlignedSquareAtPartialPixel( + this.contentKey, { + this.snapOffset = false, + this.snapSize = false, + }); + + final Key contentKey; + final bool snapOffset; + final bool snapSize; + + @override + Widget build(BuildContext context) { + final square = Container( + key: contentKey, + width: 49.3, + height: 49.3, + color: Colors.red, + ); + + return snapOffset + ? PixelSnapAlign( + alignment: Alignment(-0.47, -0.47), + snapSize: snapSize, + child: square, + ) + : Align( + alignment: Alignment(-0.47, -0.47), + child: square, + ); + } +} + +class _RowWithPartialPixels extends StatelessWidget { + const _RowWithPartialPixels({ + this.snap = false, + required this.item1Key, + required this.item2Key, + required this.item3Key, + }); + + final bool snap; + final Key item1Key; + final Key item2Key; + final Key item3Key; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: double.infinity, + child: snap + ? PixelSnapRow( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.center, + children: _buildItems(), + ) + : Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.center, + children: _buildItems(), + ), + ); + } + + List _buildItems() { + return [ + Container(key: item1Key, width: 23.3, height: 23.3, color: Colors.red), + Container(key: item2Key, width: 23.3, height: 23.3, color: Colors.red), + Container(key: item3Key, width: 23.3, height: 23.3, color: Colors.red), + ]; + } +} + +class _ColumnWithPartialPixels extends StatelessWidget { + const _ColumnWithPartialPixels({ + this.snap = false, + required this.item1Key, + required this.item2Key, + required this.item3Key, + }); + + final bool snap; + final Key item1Key; + final Key item2Key; + final Key item3Key; + + @override + Widget build(BuildContext context) { + return SizedBox( + width: double.infinity, + child: snap + ? PixelSnapColumn( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.center, + children: _buildItems(), + ) + : Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.center, + children: _buildItems(), + ), + ); + } + + List _buildItems() { + return [ + Container(key: item1Key, width: 23.3, height: 23.3, color: Colors.red), + Container(key: item2Key, width: 23.3, height: 23.3, color: Colors.red), + Container(key: item3Key, width: 23.3, height: 23.3, color: Colors.red), + ]; + } +} diff --git a/test_golden_references/intentional_failures/failing_test.dart b/test_golden_references/intentional_failures/failing_test.dart index 47fa4d6..2fa6f9f 100644 --- a/test_golden_references/intentional_failures/failing_test.dart +++ b/test_golden_references/intentional_failures/failing_test.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_test_goldens/src/flutter/flutter_pixel_alignment.dart'; void main() { testWidgets("failing test", (tester) async { @@ -22,7 +23,7 @@ void main() { // ignore: unused_element Widget _buildVersion1() { return Center( - child: Column( + child: PixelSnapColumn( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Expanded( @@ -52,7 +53,7 @@ Widget _buildVersion1() { Widget _buildVersion2() { return Center( - child: Column( + child: PixelSnapColumn( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Expanded( diff --git a/test_goldens/scene_types/gallery/gallery_grid_layout.png b/test_goldens/scene_types/gallery/gallery_grid_layout.png new file mode 100644 index 0000000000000000000000000000000000000000..0bb40ac68404f327acad326182eae4b121fcf9ee GIT binary patch literal 10306 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 S4aZOwIi#bnowfhW#s2|=RAI^h literal 0 HcmV?d00001 diff --git a/test_goldens/scene_types/gallery/gallery_grid_test.dart b/test_goldens/scene_types/gallery/gallery_grid_test.dart new file mode 100644 index 0000000..0b64fb0 --- /dev/null +++ b/test_goldens/scene_types/gallery/gallery_grid_test.dart @@ -0,0 +1,64 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_test_goldens/flutter_test_goldens.dart'; + +void main() { + group("Scene types > gallery >", () { + testGoldenScene( + "grid", + (tester) async { + await Gallery( + "Grid Layout", + directory: Directory("."), + fileName: "gallery_grid_layout", + layout: GridGoldenSceneLayout(), + ) + .itemFromWidget( + description: "Red", + widget: _buildItem(Colors.red), + ) + .itemFromWidget( + description: "Orange", + widget: _buildItem(Colors.orange), + ) + .itemFromWidget( + description: "Yellow", + widget: _buildItem(Colors.yellow), + ) + .itemFromWidget( + description: "Green", + widget: _buildItem(Colors.green), + ) + .itemFromWidget( + description: "Blue", + widget: _buildItem(Colors.blue), + ) + .itemFromWidget( + description: "Indigo", + widget: _buildItem(Colors.indigo), + ) + .itemFromWidget( + description: "Violet", + widget: _buildItem(Colors.purple), + ) + .run(tester); + }, + ); + }); +} + +Widget _buildItem(Color color) { + return SizedBox( + width: 100, + height: 100, + child: PixelSnapCenter( + child: Container( + width: 50, + height: 50, + color: color, + ), + ), + ); +} diff --git a/test_goldens/theming/scoped_theme_test.dart b/test_goldens/theming/scoped_theme_test.dart index dd80c26..f4806c6 100644 --- a/test_goldens/theming/scoped_theme_test.dart +++ b/test_goldens/theming/scoped_theme_test.dart @@ -60,7 +60,7 @@ Widget yellowItemScaffold(WidgetTester tester, Widget content) { } Widget yellowItemDecorator(BuildContext context, GoldenScreenshotMetadata metadata, Widget content) { - return Column( + return PixelSnapColumn( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Container( @@ -105,7 +105,7 @@ Widget redItemScaffold(WidgetTester tester, Widget content) { } Widget redItemDecorator(BuildContext context, GoldenScreenshotMetadata metadata, Widget content) { - return Column( + return PixelSnapColumn( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Container( diff --git a/test_goldens_known_failure_cases/failures/failure_text_layout_with_partial_pixel_1.png b/test_goldens_known_failure_cases/failures/failure_text_layout_with_partial_pixel_1.png deleted file mode 100644 index 9efc618e38b540fd23529ca9c52a7da52446e34b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1882 zcmb_ddpHvc8((rsVwCd5j!XKKLzHWqO=22t=+HOEhMi{nr+;hPQ!`U^sx3vRB= zH)yThqxZ+ri}89OCp`1fL3^IOXxN}`W}Sx5>7Y>uhi6M=f$sl+fQ)o~a$O`l0h#7y zdRL8McW%sG2f=~wttr-e;+^0e%bwF8eT@DeXdi2i3nk+l#wk<^d4BiJL&4=GCv2k^ z?A0{2K7>7yK)9t?r6}S+AW&lpHajb;Ju$56d{#+TW#!Ra=Z7Lb&caIbKrp86Ia?80 zJ_`5uU;Uo?&g}}O4!yy$>AVsdhv3OuArQ!krGw5PWN+|zY{TlRy0ob1{qR9AF9^7x zlvsMD_`8bGK=9NXsOF!^O9dbcvzoE_V{{B2PuQb-BP1zLKlo!x+tXRN(riC&`L#?Y zn>>mM?@-(R#=D1_4LP;>8J?`{SzP2)PrMo;$=BFVo+z&+Xr^gcG&=UesH$khG}pN+ z#zmw#-TQdRLBfbXJ~x>ruN!F`VS9PoJ4x%Wg#Gpr>^HTsYQg76$%P!Q=yO~7B!&D3 ze~kalDKxEmsc5~dm*-ksoJLa0i}eNWtlGX3?y;G4!V1ZfJX(FX%|V3u8CM^x{HyvY zHT8bjo`l47Rv;sdM5=6RTAjT;DKQ(pl>DwCSLI+d+d|j;t0MoDke=KU>Evn7?Ze(N z>j3~Qq0nnS0^kW=3y67DctWEpUNAFu1YK%~xxR zVVT2*G?q`ndUtE+g9(v0;4Sc%$GF_YL$HLxJai(UG+)r=zB(MyO~H zX4Y?2%uI7`vSi&I@{_8GcZSoJhws_JC?zP%DU;fX*R8D^BSof~GG=e)ff5vvmf;7+ z1wC*+5|iO_i6E+2$XtRvF6;(2WEc`Dn)waUN)xoG;s=$fKep@pcPekA?83k0_3qCj0(}cZ!9fpir3a+$ z-N8R%>#B|+M78j&|AG%J5gEp*CdM!yqRm&6=NG*r6hnefT5y+0#aDKKFDH(i6nlXN zxtz&7`)2R8J6_z5^TU0G>Nm4s#%5SM)HDSQMpeaxKMshpq6XLz7*-9pEykyxFDzfM z8obApr{a-FVzY2aMqK2&(7k`@EAj0*bA}}nQbx1EPdo2=KdBmxS4CcLDK8(djd&(* zI>Y6wK;rNwie`J%r&p?a99{YYG2haG<$GHu$7uJ{jBB1O@j!!re%KtK}V;y>PHDa z*a(RvAQ-xCK3I#sff*cUj30L(f2CmN)!(PdzJTxzd!!v9B4TTfKqxuup5R|`RSI1! zi|Z$QnLPOQaBS1E+04~qR==*Ezg&OE|F~~%`W%_t=+onfeQD_(qfiXeJMW)8WKCHF zcI!|wGqljS4_hShDNJ1_&vUm<+qI$fkl?*SC$gU(ino%qC!{Mkdck~QzIDo@3n->R zo@LHU8EbADLxP7FoqUxRN?voMtNRaLAILUZ3ddkrqHi5^JeYR_%@=x1rjw;ObJYsy zq&4(%a?`J_XR{q(IrqWC>q#+(B}v*?ZMen_cahgzc1S+-gPOtQA++H@