Skip to content

Commit 0d37850

Browse files
[Feature] - Added PixelSnapCenter, PixelSnapAlign, PixelSnapRow, PixelSnapColumn, PixelSnapFlex, and used them throughout the codebase (Resolves #65) (#66)
1 parent bc9daea commit 0d37850

26 files changed

+924
-60
lines changed

doc/marketing_goldens/platform_adaptive_gallery/platform_adaptive_gallery_test.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,12 +97,12 @@ Widget _itemDecorator(
9797
) {
9898
return Padding(
9999
padding: const EdgeInsets.all(24),
100-
child: Column(
100+
child: PixelSnapColumn(
101101
mainAxisSize: MainAxisSize.min,
102102
children: [
103103
content,
104104
Divider(),
105-
Row(
105+
PixelSnapRow(
106106
mainAxisSize: MainAxisSize.min,
107107
spacing: 16,
108108
children: [

doc/marketing_goldens/shadcn_test_tools.dart

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ class ShadcnSingleShotSceneLayout implements SceneLayout {
8787
margin: const EdgeInsets.all(48),
8888
padding: const EdgeInsets.all(48),
8989
color: ShadBlueColorScheme.dark().background,
90-
child: Column(
90+
child: PixelSnapColumn(
9191
mainAxisSize: MainAxisSize.min,
9292
crossAxisAlignment: CrossAxisAlignment.center,
9393
spacing: 24,
@@ -148,7 +148,7 @@ class ShadcnGalleryLayout implements SceneLayout {
148148
),
149149
child: Padding(
150150
padding: const EdgeInsets.all(48),
151-
child: Column(
151+
child: PixelSnapColumn(
152152
mainAxisSize: MainAxisSize.min,
153153
crossAxisAlignment: CrossAxisAlignment.center,
154154
spacing: 24,
@@ -187,7 +187,7 @@ class ShadcnGalleryLayout implements SceneLayout {
187187
child: Container(
188188
padding: const EdgeInsets.all(48),
189189
color: ShadBlueColorScheme.dark().background,
190-
child: Column(
190+
child: PixelSnapColumn(
191191
mainAxisSize: MainAxisSize.min,
192192
spacing: 24,
193193
children: [
@@ -251,7 +251,7 @@ Widget shadcnItemDecorator(
251251
) {
252252
return ColoredBox(
253253
color: ShadBlueColorScheme.dark().background,
254-
child: Column(
254+
child: PixelSnapColumn(
255255
mainAxisSize: MainAxisSize.min,
256256
crossAxisAlignment: CrossAxisAlignment.stretch,
257257
children: [

doc/website/bin/main.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Future<void> main(List<String> arguments) async {
88
// Here, you can directly hook into the StaticShock pipeline. For example,
99
// you can copy an "images" directory from the source set to build set:
1010
..pick(DirectoryPicker.parse("images"))
11+
..pick(ExtensionPicker("png"))
1112
// All 3rd party behavior is added through plugins, even the behavior
1213
// shipped with Static Shock.
1314
..plugin(const MarkdownPlugin())

doc/website/source/_data.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ navigation:
5353
tag: failure-scenes
5454
sortBy: navOrder
5555

56+
- title: Reduce Flakiness
57+
tag: reduce-flakiness
58+
sortBy: navOrder
59+
5660
- title: Flutter's Implementation
5761
tag: flutter-implementation
5862
sortBy: navOrder
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
tags: reduce-flakiness
11.1 KB
Loading
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
---
2+
title: Position and Size
3+
description: How to avoid partial-pixel offsets and sizes in golden tests.
4+
navOrder: 10
5+
---
6+
Partial pixel offsets and sizes guarantee that your golden tests will have flaky false
7+
failures when running on different platforms, or even running on the same platform
8+
through Docker containers.
9+
10+
![Flaky Fractional Position &amp; Size](/reduce-flakiness/failure_centered-square.png)
11+
12+
Most of these situations are outside your control. Your UI is what it is, and that UI
13+
probably positions some things at partial-pixel offsets, e.g. `(45.7, 203.83)`, and/or
14+
partial-pixel sizes, e.g. `103.2 x 46.7`.
15+
16+
That said, some steps can be taken to reduce the likelihood and severity of these
17+
partial-pixel offsets and sizes.
18+
19+
## Golden Test Device Pixel Ratio
20+
It's important to understand the concept of device pixel ratios and how they relate to
21+
widget tests and golden tests.
22+
23+
### Background of Logical Pixels
24+
Over time, screens have evolved to include higher and higher pixel densities. This posed
25+
a problem for app layouts in earlier years, where developers hard-coded various dimensions,
26+
which then looked comically tiny on newer device screens.
27+
28+
To help port existing layouts to new screens with higher pixel densities, devices started
29+
reporting dimensions in terms of ["points" (Apple)](https://developer.apple.com/library/archive/documentation/GraphicsAnimation/Conceptual/HighResolutionOSX/Explained/Explained.html)
30+
and ["density-independent pixels (DIPs)" (Android)](https://developer.android.com/training/multiscreen/screendensities).
31+
Both of these concepts are similar - they define dimensions in terms of physical distances,
32+
rather than pixel count. This way, a screen layout that's made for a 400px x 800px
33+
mobile device will work equally well on a screen with a size of 800px x 1,600px.
34+
35+
Of course, Flutter honors measurements in terms of these physical distances rather than
36+
pixel distances. In fact, every width, height, x, and y value that you define in Flutter
37+
is actually a density-independent value, NOT a pixel value. Flutter calls these "logical pixels".
38+
Flutter tracks and reports the number of "physical pixels" per "logical pixel" in a property
39+
called [`devicePixelRatio`](https://api.flutter.dev/flutter/dart-ui/FlutterView/devicePixelRatio.html),
40+
which is published as part of Flutter's `MediaQuery`.
41+
42+
### Device Pixel Ratios in Golden Tests
43+
With all of that background information, why do pixels, points, and DIPs matter for golden tests? They matter
44+
because **Flutter attempts to simulate real `devicePixelRatio`s in widget tests** (including golden tests).
45+
Speaking of which, another detail about widget tests you might know is that **every widget test,
46+
by default, is configured as if its running on an Android device**. These two facts, combined,
47+
means that every widget test, by default, simulates a `devicePixelRatio` greater than `1.0`.
48+
Historically, tests have used a `devicePixelRatio` of `3.0`. This number is likely subject to
49+
change based on the industry standard at any given time.
50+
51+
Golden Scenes are rendered to physical pixels. Therefore, rendering Golden Scenes requires
52+
mapping from Flutter's logical pixels to the bitmap's physical pixels. This mapping can change
53+
whole-number pixel values into partial-pixel values depending on the `devicePixelRatio`. For
54+
example, imagine a `devicePixelRatio` of `1.75`, and a logical pixel value of `30`. Where would
55+
that `30` end up in the final bitmap? `30 * 1.75 = 52.5`. Thus, a whole value has become a
56+
partial-value, and it will now undergo anti-aliasing effects when rendering to the bitmap.
57+
58+
### What To Do About It
59+
The answer is to change Flutter's test configuration to use a `devicePixelRatio` of `1.0`
60+
instead of the default.
61+
62+
```dart
63+
testWidgets("my test", (tester) async {
64+
// Change the configuration for this test, and reset it after.
65+
tester.view.devicePixelRatio = 1.0;
66+
addTearDown(() => tester.view.reset());
67+
68+
// Now do your real test work...
69+
});
70+
```
71+
72+
When you use `flutter_test_goldens` test runners, you don't need to worry about this. It's done
73+
automatically, on your behalf.
74+
75+
```dart
76+
testGoldenScene("my golden test", (tester) async {
77+
// Just worry about your test, we've already changed the devicePixelRatio to 1.0...
78+
});
79+
```
80+
81+
So I guess what we're saying is...if you use `flutter_test_goldens` you don't need to
82+
worry about anything you just read :)
83+
84+
## Positioning and Sizing Widgets
85+
You may not think about it, but it's very common for widgets to be sized or positioned
86+
at partial pixel boundaries.
87+
88+
For example: Imagine a `25x25px` square that's centered within a `100x100` area. The top/left
89+
offset of the centered square would be `(87.5, 87.5)`.
90+
91+
For example: Imagine a `Row` that's `100px` wide, with 3 squares (`25x25px`) spaced evenly across it.
92+
Those squares would sit at x-values of `6.25px`, `37.6px`, and `68.75px`, respectively.
93+
94+
You may not be able to control these details within your app UI, but there's one place
95+
where it's very important to control these values - and that's within Golden Scene layouts.
96+
97+
Typical Golden Scene layouts include rows, columns, grids, and any other layout strategy
98+
that you choose to employ. A Golden Scene layout is responsible for placing every golden image
99+
in the scene. These golden images are later extracted for comparison within tests. Therefore,
100+
it's absolutely critical that a Golden Scene places golden images precisely on whole-pixel
101+
boundaries. If a Golden Scene positions an individual golden image on a partial boundary, the
102+
scene will interpolate the color of the pixels on the edge of the golden, which will change
103+
at least one pixel of detail all around the perimeter of the image. This will cause the golden
104+
to fail, even when run on the exact same machine that generated it.
105+
106+
To help our team, and your team, to create expressive Golden Scenes without breaking golden
107+
comparison, we've published variations of a few of your favorite widgets, which now snap
108+
their children to whole-pixel locations and sizes.
109+
110+
* `PixelSnapCenter`: Like `Center` but with offset and size snapping.
111+
* `PixelSnapAlign`: Like `Align` but with offset and size snapping.
112+
* `PixelSnapRow`: Like `Row` but with offset and size snapping.
113+
* `PixelSnapColumn`: Like `Column` but with offset and size snapping.
114+
* `PixelSnapFlex`: Like `Flex` but with offset and size snapping.
115+
116+
Take the earlier example of a square centered in a larger area. We can fix that
117+
situation with a `PixelSnapCenter`.
118+
119+
First, the bad version.
120+
121+
```dart
122+
SizedBox(
123+
width: 50,
124+
height: 50,
125+
child: Center(
126+
child: Container(
127+
width: 24.5,
128+
height: 24.5,
129+
color: Colors.red,
130+
),
131+
),
132+
);
133+
```
134+
135+
The bad version includes a square with a partial-pixel size of `24.5x24.5px`, and that
136+
square is located at `(12.75, 12.75)`.
137+
138+
Let's snap the offset and the size with `PixelSnapCenter`.
139+
140+
```dart
141+
SizedBox(
142+
width: 50,
143+
height: 50,
144+
child: PixelSnapCenter( // <-- the change
145+
child: Container(
146+
width: 24.5,
147+
height: 24.5,
148+
color: Colors.red,
149+
),
150+
),
151+
);
152+
```
153+
154+
The version with `PixelSnapCenter` positions the square at `(12, 12)` and forces the
155+
square to become `(24, 24)`. No more partial pixels.
156+
157+
These snapping widgets MUST be used when building Golden Scenes, to ensure that new/updated
158+
goldens are consistent with extracted goldens. However, if desired, these widgets can also
159+
be used in your regular widget trees.

doc/website/source/styles/docs_page_layout.scss

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ main.page-content {
258258
}
259259

260260
h2, h3, h4, h5, h6 {
261-
margin-top: 1em;
261+
margin-top: 2em;
262262
}
263263

264264
p {
@@ -280,6 +280,11 @@ main.page-content {
280280
margin-bottom: 1.5em;
281281
}
282282

283+
img {
284+
display: block;
285+
margin: auto;
286+
}
287+
283288
pre code {
284289
margin-top: 1em;
285290
margin-bottom: 1em;

golden_tester.Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ RUN apt install -y git curl unzip
1313
RUN cat /etc/lsb-release
1414

1515
# Invalidate the cache when flutter pushes a new commit.
16-
ADD https://api.github.com/repos/flutter/flutter/git/refs/heads/master ./flutter-latest-master
16+
ADD https://api.github.com/repos/flutter/flutter/git/refs/heads/stable ./flutter-latest-stable
1717

1818
RUN git clone https://github.com/flutter/flutter.git ${FLUTTER_HOME}
1919

lib/flutter_test_goldens.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export 'src/flutter/flutter_camera.dart';
22
export 'src/flutter/flutter_golden_matcher.dart';
3+
export 'src/flutter/flutter_pixel_alignment.dart';
34
export 'src/flutter/flutter_test_extensions.dart';
45
export 'src/fonts/fonts.dart';
56
export 'src/fonts/icons.dart';

0 commit comments

Comments
 (0)