Skip to content

Commit 386d611

Browse files
authored
Merge pull request #59 from eBay/image-loading
Switch to better implementation of priming images for goldens
2 parents 3ebdd83 + f85292f commit 386d611

20 files changed

+323
-13
lines changed

packages/golden_toolkit/CHANGELOG.md

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,25 @@
22

33
## 0.5.0
44

5+
### More intelligent behavior for loading assets
6+
7+
A new mechanism has been added for ensuring that images have been decoded before capturing goldens. The old implementation worked most of the time, but was non-deterministic and hacky. The new implementation inspects the widget tree to identify images that need to be loaded. It should display images more consistently in goldens.
8+
9+
This may be a breaking change for some consumers. If you run into issues, you can revert to the old behavior, by applying the following configuration:
10+
11+
```GoldenToolkitConfiguration(primeAssets: legacyPrimeAssets);```
12+
13+
Additionally, you can provide your own implementation that extends the new default behavior:
14+
15+
```dart
16+
GoldenToolkitConfiguration(primeAssets: (tester) async {
17+
await defaultPrimeAssets(tester);
18+
/* do anything custom */
19+
});
20+
```
21+
22+
If you run into issues, please submit issues so we can expand on the default behavior. We expect that it is likely missing some cases.
23+
524
### New API for configuring the toolkit
625

726
Reworked the configuration API introduced in 0.4.0. The prior method relied on global state and could be error prone. The old implementation still functions, but has been marked as deprecated and will be removed in a future release.
@@ -37,16 +56,20 @@ Added the following extensions. These can be used with any vanilla golden assert
3756

3857
```dart
3958
// configures the simulated device to mirror the supplied device configuration (dimensions, pixel density, safe area, etc)
40-
tester.binding.applyDeviceOverrides(device);
59+
await tester.binding.applyDeviceOverrides(device);
4160
4261
// resets any configuration applied by applyDeviceOverrides
43-
tester.binding.resetDeviceOverrides();
62+
await tester.binding.resetDeviceOverrides();
4463
4564
// runs a block of code with the simulated device settings and automatically clears upon completion
46-
tester.binding.runWithDeviceOverrides(device, body: (){});
65+
await tester.binding.runWithDeviceOverrides(device, body: (){});
4766
4867
// convenience helper for configurating the safe area... the built-in paddingTestValue is difficult to work with
4968
tester.binding.window.safeAreaTestValue = EdgeInsets.all(8);
69+
70+
// a stand-alone version of the image loading mechanism described at the top of these release notes. Will wait for all images to be decoded
71+
// so that they will for sure appear in the golden.
72+
await tester.waitForAssets();
5073
```
5174

5275
### Misc Changes

packages/golden_toolkit/example/test/example_test.dart

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,24 @@ void main() {
8585
overrideGoldenHeight: 1200,
8686
);
8787
});
88+
89+
group('GoldenBuilder examples of accessibility testing', () {
90+
// With those test we want to make sure our widgets look right when user changes system font size
91+
testGoldens('Card should look right when user bumps system font size', (tester) async {
92+
const widget = WeatherCard(temp: 56, weather: Weather.cloudy);
93+
94+
final gb = GoldenBuilder.column(bgColor: Colors.white, wrap: _simpleFrame)
95+
..addScenario('Regular font size', widget)
96+
..addTextScaleScenario('Large font size', widget, textScaleFactor: 2.0)
97+
..addTextScaleScenario('Largest font size', widget, textScaleFactor: 3.2);
98+
99+
await tester.pumpWidgetBuilder(
100+
gb.build(),
101+
surfaceSize: const Size(200, 1000),
102+
);
103+
await screenMatchesGolden(tester, 'weather_accessibility');
104+
});
105+
});
88106
});
89107

90108
group('Multi-Screen Golden', () {

packages/golden_toolkit/lib/src/configuration.dart

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ import 'package:flutter/foundation.dart';
1010
/// https://opensource.org/licenses/BSD-3-Clause
1111
/// ***************************************************
1212
13-
import 'package:meta/meta.dart';
1413
import 'package:flutter_test/flutter_test.dart';
14+
import 'package:meta/meta.dart';
15+
import '../golden_toolkit.dart';
1516
import 'device.dart';
1617

1718
/// Manages global state & behavior for the Golden Toolkit
@@ -49,6 +50,7 @@ class GoldenToolkit {
4950
}
5051
}
5152

53+
/// A func that will be evaluated at runtime to determine if the golden assertion should be skipped
5254
typedef SkipGoldenAssertion = bool Function();
5355

5456
/// A factory to determine an actual file name/path from a given name.
@@ -65,17 +67,31 @@ typedef FileNameFactory = String Function(String name);
6567
/// * [GoldenToolkitConfiguration] to configure a global device file name factory.
6668
typedef DeviceFileNameFactory = String Function(String name, Device device);
6769

70+
/// A function that primes all needed assets for the given [tester].
71+
///
72+
/// For ready to use implementations see:
73+
/// * [legacyPrimeAssets], which is the default [PrimeAssets] used by the global configuration by default.
74+
/// * [defaultPrimeAssets], which just waits for all [Image] widgets in the widget tree to finish decoding.
75+
typedef PrimeAssets = Future<void> Function(WidgetTester tester);
76+
6877
/// Represents configuration options for the GoldenToolkit. These are akin to environmental flags.
6978
@immutable
7079
class GoldenToolkitConfiguration {
7180
/// GoldenToolkitConfiguration constructor
7281
///
7382
/// [skipGoldenAssertion] a func that returns a bool as to whether the golden assertion should be skipped.
7483
/// A typical example may be to skip when the assertion is invoked on certain platforms. For example: () => !Platform.isMacOS
84+
///
85+
/// [fileNameFactory] a func used to decide the final filename for screenMatchesGolden() invocations
86+
///
87+
/// [deviceFileNameFactory] a func used to decide the final filename for multiScreenGolden() invocations
88+
///
89+
/// [primeAssets] a func that is used to ensure that all images have been decoded before trying to render
7590
const GoldenToolkitConfiguration({
7691
this.skipGoldenAssertion = _doNotSkip,
7792
this.fileNameFactory = defaultFileNameFactory,
7893
this.deviceFileNameFactory = defaultDeviceFileNameFactory,
94+
this.primeAssets = defaultPrimeAssets,
7995
});
8096

8197
/// a function indicating whether a golden assertion should be skipped
@@ -87,16 +103,21 @@ class GoldenToolkitConfiguration {
87103
/// A function to determine the file name/path [multiScreenGolden] uses to call [matchesGoldenFile].
88104
final DeviceFileNameFactory deviceFileNameFactory;
89105

106+
/// A function that primes all needed assets for the given [tester]. Defaults to [defaultPrimeAssets].
107+
final PrimeAssets primeAssets;
108+
90109
/// Copies the configuration with the given values overridden.
91110
GoldenToolkitConfiguration copyWith({
92111
SkipGoldenAssertion skipGoldenAssertion,
93112
FileNameFactory fileNameFactory,
94113
DeviceFileNameFactory deviceFileNameFactory,
114+
PrimeAssets primeAssets,
95115
}) {
96116
return GoldenToolkitConfiguration(
97117
skipGoldenAssertion: skipGoldenAssertion ?? this.skipGoldenAssertion,
98118
fileNameFactory: fileNameFactory ?? this.fileNameFactory,
99119
deviceFileNameFactory: deviceFileNameFactory ?? this.deviceFileNameFactory,
120+
primeAssets: primeAssets ?? this.primeAssets,
100121
);
101122
}
102123

@@ -107,11 +128,13 @@ class GoldenToolkitConfiguration {
107128
runtimeType == other.runtimeType &&
108129
skipGoldenAssertion == other.skipGoldenAssertion &&
109130
fileNameFactory == other.fileNameFactory &&
110-
deviceFileNameFactory == other.deviceFileNameFactory;
131+
deviceFileNameFactory == other.deviceFileNameFactory &&
132+
primeAssets == other.primeAssets;
111133
}
112134

113135
@override
114-
int get hashCode => skipGoldenAssertion.hashCode ^ fileNameFactory.hashCode ^ deviceFileNameFactory.hashCode;
136+
int get hashCode =>
137+
skipGoldenAssertion.hashCode ^ fileNameFactory.hashCode ^ deviceFileNameFactory.hashCode ^ primeAssets.hashCode;
115138
}
116139

117140
bool _doNotSkip() => false;

packages/golden_toolkit/lib/src/multi_screen_golden.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import 'package:flutter/material.dart';
1212
import 'package:flutter_test/flutter_test.dart';
1313

14-
import 'configuration.dart';
14+
import '../golden_toolkit.dart';
1515
import 'device.dart';
1616
import 'testing_tools.dart';
1717
import 'widget_tester_extensions.dart';
@@ -75,9 +75,9 @@ Future<void> multiScreenGolden(
7575
await compareWithGolden(
7676
tester,
7777
name,
78+
customPump: customPump,
7879
autoHeight: autoHeight,
7980
finder: finder,
80-
customPump: customPump,
8181
//ignore: deprecated_member_use_from_same_package
8282
skip: skip,
8383
device: device,

packages/golden_toolkit/lib/src/testing_tools.dart

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,17 @@
99
//ignore_for_file: deprecated_member_use_from_same_package
1010

1111
import 'dart:async';
12+
import 'dart:ui';
1213

1314
import 'package:flutter/material.dart';
15+
import 'package:flutter/rendering.dart';
1416
import 'package:flutter_test/flutter_test.dart';
1517
import 'package:meta/meta.dart';
1618

1719
import 'configuration.dart';
1820
import 'device.dart';
1921
import 'test_asset_bundle.dart';
22+
import 'widget_tester_extensions.dart';
2023

2124
const Size _defaultSize = Size(800, 600);
2225

@@ -201,10 +204,10 @@ Future<void> compareWithGolden(
201204
final fileName = fileNameFactory(name, device);
202205
final originalWindowSize = tester.binding.window.physicalSize;
203206

204-
// This is a minor optimization and works around an issue with the current hacky implementation of invoking the golden assertion method.
205207
if (!shouldSkipGoldenGeneration) {
206-
await _primeImages(fileName, actualFinder);
208+
await tester.waitForAssets();
207209
}
210+
208211
await pumpAfterPrime(tester);
209212

210213
if (autoHeight == true) {
@@ -249,7 +252,54 @@ Future<void> compareWithGolden(
249252
}
250253
}
251254

252-
// Matches Golden file is the easiest way for the images to be requested.
253-
Future<void> _primeImages(String fileName, Finder finder) => matchesGoldenFile(fileName).matchAsync(finder);
255+
/// A function that primes all assets by just wasting time and hoping that it is enough for all assets to
256+
/// finish loading. Doing so is not recommended and very flaky. Consider switching to [defaultPrimeAssets] or
257+
/// a custom implementation.
258+
///
259+
/// See also:
260+
/// * [GoldenToolkitConfiguration.primeAssets] to configure a global asset prime function.
261+
Future<void> legacyPrimeAssets(WidgetTester tester) async {
262+
final renderObject = tester.binding.renderView;
263+
assert(!renderObject.debugNeedsPaint);
264+
265+
final OffsetLayer layer = renderObject.debugLayer;
266+
267+
// This is a very flaky hack which should be avoided if possible.
268+
// We are just trying to waste some time that matches the time needed to call matchesGoldenFile.
269+
// This should be enough time for most images/assets to be ready.
270+
await tester.runAsync<void>(() async {
271+
final image = await layer.toImage(renderObject.paintBounds);
272+
await image.toByteData(format: ImageByteFormat.png);
273+
await image.toByteData(format: ImageByteFormat.png);
274+
});
275+
}
276+
277+
/// A function that waits for all [Image] widgets found in the widget tree to finish decoding.
278+
///
279+
/// Currently this supports images included via Image widgets, or as part of BoxDecorations.
280+
///
281+
/// See also:
282+
/// * [GoldenToolkitConfiguration.primeAssets] to configure a global asset prime function.
283+
Future<void> defaultPrimeAssets(WidgetTester tester) async {
284+
final imageElements = find.byType(Image).evaluate();
285+
final containerElements = find.byType(Container).evaluate();
286+
await tester.runAsync(() async {
287+
for (final imageElement in imageElements) {
288+
final widget = imageElement.widget;
289+
if (widget is Image) {
290+
await precacheImage(widget.image, imageElement);
291+
}
292+
}
293+
for (final container in containerElements) {
294+
final Container widget = container.widget;
295+
final decoration = widget.decoration;
296+
if (decoration is BoxDecoration) {
297+
if (decoration.image != null) {
298+
await precacheImage(decoration.image.image, container);
299+
}
300+
}
301+
}
302+
});
303+
}
254304

255305
Future<void> _onlyPumpAndSettle(WidgetTester tester) async => tester.pumpAndSettle();

packages/golden_toolkit/lib/src/widget_tester_extensions.dart

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,20 @@ import 'dart:ui';
33
import 'package:flutter/widgets.dart';
44
import 'package:flutter_test/flutter_test.dart';
55

6+
import 'configuration.dart';
67
import 'device.dart';
78

9+
/// Convenience extensions on WidgetTester
10+
extension WidgetTesterImageLoadingExtensions on WidgetTester {
11+
/// Waits for images to decode. Use this to ensure that images are properly displayed
12+
/// in Goldens. The implementation of this can be configured as part of GoldenToolkitConfiguration
13+
///
14+
/// If you have assets that are not loading with this implementation, please file an issue and we will explore solutions.
15+
Future<void> waitForAssets() => GoldenToolkit.configuration.primeAssets(this);
16+
}
17+
818
/// Convenience extensions for more easily configuring WidgetTester for pre-set configurations
9-
extension WidgetTesterExtensions on TestWidgetsFlutterBinding {
19+
extension WidgetFlutterBindingExtensions on TestWidgetsFlutterBinding {
1020
/// Configure the Test device for the duration of the supplied operation and revert
1121
///
1222
/// [device] the desired configuration to apply

packages/golden_toolkit/test/configuration_test.dart

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,40 @@ void main() {
7676
);
7777
});
7878

79+
testGoldens('screenMatchesGolden method uses primeAssets from global configuration', (tester) async {
80+
var globalPrimeCalledCount = 0;
81+
return GoldenToolkit.runWithConfiguration(
82+
() async {
83+
await tester.pumpWidgetBuilder(Image.asset('packages/sample_dependency/images/image.png'));
84+
await screenMatchesGolden(tester, 'screen_matches_golden_defers_primeAssets');
85+
expect(globalPrimeCalledCount, 1);
86+
},
87+
config: GoldenToolkitConfiguration(primeAssets: (WidgetTester tester) async {
88+
globalPrimeCalledCount += 1;
89+
await legacyPrimeAssets(tester);
90+
}),
91+
);
92+
});
93+
94+
testGoldens('multiScreenGolden method uses primeAssets from global configuration', (tester) async {
95+
var globalPrimeCalledCount = 0;
96+
return GoldenToolkit.runWithConfiguration(
97+
() async {
98+
await tester.pumpWidgetBuilder(Image.asset('packages/sample_dependency/images/image.png'));
99+
await multiScreenGolden(
100+
tester,
101+
'multi_screen_golden_defers_primeAssets',
102+
devices: [const Device(size: Size(200, 200), name: 'custom')],
103+
);
104+
expect(globalPrimeCalledCount, 1);
105+
},
106+
config: GoldenToolkitConfiguration(primeAssets: (WidgetTester tester) async {
107+
globalPrimeCalledCount += 1;
108+
await legacyPrimeAssets(tester);
109+
}),
110+
);
111+
});
112+
79113
test('Default Configuration', () {
80114
const config = GoldenToolkitConfiguration();
81115
expect(config.skipGoldenAssertion(), isFalse);
@@ -90,11 +124,13 @@ void main() {
90124
bool skipGoldenAssertion() => false;
91125
String fileNameFactory(String filename) => '';
92126
String deviceFileNameFactory(String filename, Device device) => '';
127+
Future<void> primeAssets(WidgetTester tester) async {}
93128

94129
final config = GoldenToolkitConfiguration(
95130
skipGoldenAssertion: skipGoldenAssertion,
96131
deviceFileNameFactory: deviceFileNameFactory,
97132
fileNameFactory: fileNameFactory,
133+
primeAssets: primeAssets,
98134
);
99135

100136
test('config with identical params should be equal', () {
@@ -115,6 +151,10 @@ void main() {
115151
expect(config, isNot(equals(config.copyWith(deviceFileNameFactory: (file, dev) => ''))));
116152
expect(config.hashCode, isNot(equals(config.copyWith(deviceFileNameFactory: (file, dev) => '').hashCode)));
117153
});
154+
test('primeImages', () {
155+
expect(config, isNot(equals(config.copyWith(primeAssets: (_) async {}))));
156+
expect(config.hashCode, isNot(equals(config.copyWith(primeAssets: (_) async {}).hashCode)));
157+
});
118158
});
119159
});
120160
});
2.85 KB
Loading
5.67 KB
Loading

0 commit comments

Comments
 (0)