Skip to content

Commit 747128f

Browse files
Use Layer.toImage for golden tests on CanvasKit (#135249)
Changes golden tests on CanvasKit to use Layer.toImage instead of browser APIs for screenshots. This brings it more in line with other platforms and should also fix some async timing bugs with tests. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/wiki/Tree-hygiene#overview [Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene [test-exempt]: https://github.com/flutter/flutter/wiki/Tree-hygiene#tests [Flutter Style Guide]: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo [Features we expect every widget to implement]: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/wiki/Chat
1 parent 9391a88 commit 747128f

File tree

5 files changed

+172
-54
lines changed

5 files changed

+172
-54
lines changed

packages/flutter_test/lib/src/_goldens_io.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,16 @@ class DefaultWebGoldenComparator extends WebGoldenComparator {
299299
Future<void> update(double width, double height, Uri golden) {
300300
throw UnsupportedError('DefaultWebGoldenComparator is only supported on the web.');
301301
}
302+
303+
@override
304+
Future<bool> compareBytes(Uint8List bytes, Uri golden) {
305+
throw UnsupportedError('DefaultWebGoldenComparator is only supported on the web.');
306+
}
307+
308+
@override
309+
Future<void> updateBytes(Uint8List bytes, Uri golden) {
310+
throw UnsupportedError('DefaultWebGoldenComparator is only supported on the web.');
311+
}
302312
}
303313

304314
/// Reads the red value out of a 32 bit rgba pixel.

packages/flutter_test/lib/src/_goldens_web.dart

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,30 @@ class DefaultWebGoldenComparator extends WebGoldenComparator {
8080
// Update is handled on the server side, just use the same logic here
8181
await compare(width, height, golden);
8282
}
83+
84+
@override
85+
Future<bool> compareBytes(Uint8List bytes, Uri golden) async {
86+
final String key = golden.toString();
87+
final String bytesEncoded = base64.encode(bytes);
88+
final html.HttpRequest request = await html.HttpRequest.request(
89+
'flutter_goldens',
90+
method: 'POST',
91+
sendData: json.encode(<String, Object>{
92+
'testUri': testUri.toString(),
93+
'key': key,
94+
'bytes': bytesEncoded,
95+
}),
96+
);
97+
final String response = request.response as String;
98+
if (response == 'true') {
99+
return true;
100+
}
101+
fail(response);
102+
}
103+
104+
@override
105+
Future<void> updateBytes(Uint8List bytes, Uri golden) async {
106+
// Update is handled on the server side, just use the same logic here
107+
await compareBytes(bytes, golden);
108+
}
83109
}

packages/flutter_test/lib/src/_matchers_web.dart

Lines changed: 53 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import 'dart:ui' as ui;
66

7+
import 'package:flutter/foundation.dart';
78
import 'package:flutter/rendering.dart';
89
import 'package:flutter/widgets.dart';
910
import 'package:matcher/expect.dart';
@@ -61,25 +62,58 @@ class MatchesGoldenFile extends AsyncMatcher {
6162
final ui.FlutterView view = binding.platformDispatcher.implicitView!;
6263
final RenderView renderView = binding.renderViews.firstWhere((RenderView r) => r.flutterView == view);
6364

64-
// Unlike `flutter_tester`, we don't have the ability to render an element
65-
// to an image directly. Instead, we will use `window.render()` to render
66-
// only the element being requested, and send a request to the test server
67-
// requesting it to take a screenshot through the browser's debug interface.
68-
_renderElement(view, renderObject);
69-
final String? result = await binding.runAsync<String?>(() async {
70-
if (autoUpdateGoldenFiles) {
71-
await webGoldenComparator.update(size.width, size.height, key);
72-
return null;
73-
}
74-
try {
75-
final bool success = await webGoldenComparator.compare(size.width, size.height, key);
76-
return success ? null : 'does not match';
77-
} on TestFailure catch (ex) {
78-
return ex.message;
79-
}
80-
});
81-
_renderElement(view, renderView);
82-
return result;
65+
if (isCanvasKit) {
66+
// In CanvasKit, use Layer.toImage to generate the screenshot.
67+
final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.instance;
68+
return binding.runAsync<String?>(() async {
69+
assert(element.renderObject != null);
70+
RenderObject renderObject = element.renderObject!;
71+
while (!renderObject.isRepaintBoundary) {
72+
renderObject = renderObject.parent!;
73+
}
74+
assert(!renderObject.debugNeedsPaint);
75+
final OffsetLayer layer = renderObject.debugLayer! as OffsetLayer;
76+
final ui.Image image = await layer.toImage(renderObject.paintBounds);
77+
try {
78+
final ByteData? bytes = await image.toByteData(format: ui.ImageByteFormat.png);
79+
if (bytes == null) {
80+
return 'could not encode screenshot.';
81+
}
82+
if (autoUpdateGoldenFiles) {
83+
await webGoldenComparator.updateBytes(bytes.buffer.asUint8List(), key);
84+
return null;
85+
}
86+
try {
87+
final bool success = await webGoldenComparator.compareBytes(bytes.buffer.asUint8List(), key);
88+
return success ? null : 'does not match';
89+
} on TestFailure catch (ex) {
90+
return ex.message;
91+
}
92+
} finally {
93+
image.dispose();
94+
}
95+
});
96+
} else {
97+
// In the HTML renderer, we don't have the ability to render an element
98+
// to an image directly. Instead, we will use `window.render()` to render
99+
// only the element being requested, and send a request to the test server
100+
// requesting it to take a screenshot through the browser's debug interface.
101+
_renderElement(view, renderObject);
102+
final String? result = await binding.runAsync<String?>(() async {
103+
if (autoUpdateGoldenFiles) {
104+
await webGoldenComparator.update(size.width, size.height, key);
105+
return null;
106+
}
107+
try {
108+
final bool success = await webGoldenComparator.compare(size.width, size.height, key);
109+
return success ? null : 'does not match';
110+
} on TestFailure catch (ex) {
111+
return ex.message;
112+
}
113+
});
114+
_renderElement(view, renderView);
115+
return result;
116+
}
83117
}
84118

85119
@override

packages/flutter_test/lib/src/goldens.dart

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,34 @@ abstract class WebGoldenComparator {
185185
/// is left up to the implementation class.
186186
Future<void> update(double width, double height, Uri golden);
187187

188+
/// Compares the pixels of decoded png [bytes] against the golden file
189+
/// identified by [golden].
190+
///
191+
/// The returned future completes with a boolean value that indicates whether
192+
/// the pixels rendered on screen match the golden file's pixels.
193+
///
194+
/// In the case of comparison mismatch, the comparator may choose to throw a
195+
/// [TestFailure] if it wants to control the failure message, often in the
196+
/// form of a [ComparisonResult] that provides detailed information about the
197+
/// mismatch.
198+
///
199+
/// The method by which [golden] is located and by which its bytes are loaded
200+
/// is left up to the implementation class. For instance, some implementations
201+
/// may load files from the local file system, whereas others may load files
202+
/// over the network or from a remote repository.
203+
Future<bool> compareBytes(Uint8List bytes, Uri golden);
204+
205+
/// Compares the pixels of decoded png [bytes] against the golden file
206+
/// identified by [golden].
207+
///
208+
/// This will be invoked in lieu of [compareBytes] when [autoUpdateGoldenFiles]
209+
/// is `true` (which gets set automatically by the test framework when the
210+
/// user runs `flutter test --update-goldens --platform=chrome`).
211+
///
212+
/// The method by which [golden] is located and by which its bytes are written
213+
/// is left up to the implementation class.
214+
Future<void> updateBytes(Uint8List bytes, Uri golden);
215+
188216
/// Returns a new golden file [Uri] to incorporate any [version] number with
189217
/// the [key].
190218
///
@@ -298,12 +326,7 @@ class _TrivialWebGoldenComparator implements WebGoldenComparator {
298326

299327
@override
300328
Future<bool> compare(double width, double height, Uri golden) {
301-
// Ideally we would use markTestSkipped here but in some situations,
302-
// comparators are called outside of tests.
303-
// See also: https://github.com/flutter/flutter/issues/91285
304-
// ignore: avoid_print
305-
print('Golden comparison requested for "$golden"; skipping...');
306-
return Future<bool>.value(true);
329+
return _warnAboutSkipping(golden);
307330
}
308331

309332
@override
@@ -315,6 +338,25 @@ class _TrivialWebGoldenComparator implements WebGoldenComparator {
315338
Uri getTestUri(Uri key, int? version) {
316339
return key;
317340
}
341+
342+
@override
343+
Future<bool> compareBytes(Uint8List bytes, Uri golden) {
344+
return _warnAboutSkipping(golden);
345+
}
346+
347+
@override
348+
Future<void> updateBytes(Uint8List bytes, Uri golden) {
349+
throw StateError('webGoldenComparator has not been initialized');
350+
}
351+
352+
Future<bool> _warnAboutSkipping(Uri golden) {
353+
// Ideally we would use markTestSkipped here but in some situations,
354+
// comparators are called outside of tests.
355+
// See also: https://github.com/flutter/flutter/issues/91285
356+
// ignore: avoid_print
357+
print('Golden comparison requested for "$golden"; skipping...');
358+
return Future<bool>.value(true);
359+
}
318360
}
319361

320362
/// The result of a pixel comparison test.

packages/flutter_tools/lib/src/test/flutter_web_platform.dart

Lines changed: 35 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -336,38 +336,44 @@ class FlutterWebPlatform extends PlatformPlugin {
336336
final Map<String, Object?> body = json.decode(await request.readAsString()) as Map<String, Object?>;
337337
final Uri goldenKey = Uri.parse(body['key']! as String);
338338
final Uri testUri = Uri.parse(body['testUri']! as String);
339-
final num width = body['width']! as num;
340-
final num height = body['height']! as num;
339+
final num? width = body['width'] as num?;
340+
final num? height = body['height'] as num?;
341341
Uint8List bytes;
342342

343-
try {
344-
final ChromeTab chromeTab = (await _browserManager!._browser.chromeConnection.getTab((ChromeTab tab) {
345-
return tab.url.contains(_browserManager!._browser.url!);
346-
}))!;
347-
final WipConnection connection = await chromeTab.connect();
348-
final WipResponse response = await connection.sendCommand('Page.captureScreenshot', <String, Object>{
349-
// Clip the screenshot to include only the element.
350-
// Prior to taking a screenshot, we are calling `window.render()` in
351-
// `_matchers_web.dart` to only render the element on screen. That
352-
// will make sure that the element will always be displayed on the
353-
// origin of the screen.
354-
'clip': <String, Object>{
355-
'x': 0.0,
356-
'y': 0.0,
357-
'width': width.toDouble(),
358-
'height': height.toDouble(),
359-
'scale': 1.0,
360-
},
361-
});
362-
bytes = base64.decode(response.result!['data'] as String);
363-
} on WipError catch (ex) {
364-
_logger.printError('Caught WIPError: $ex');
365-
return shelf.Response.ok('WIP error: $ex');
366-
} on FormatException catch (ex) {
367-
_logger.printError('Caught FormatException: $ex');
368-
return shelf.Response.ok('Caught exception: $ex');
343+
if (body.containsKey('bytes')) {
344+
bytes = base64.decode(body['bytes']! as String);
345+
} else {
346+
// TODO(hterkelsen): Do not use browser screenshots for testing on the
347+
// web once we transition off the HTML renderer. See:
348+
// https://github.com/flutter/flutter/issues/135700
349+
try {
350+
final ChromeTab chromeTab = (await _browserManager!._browser.chromeConnection.getTab((ChromeTab tab) {
351+
return tab.url.contains(_browserManager!._browser.url!);
352+
}))!;
353+
final WipConnection connection = await chromeTab.connect();
354+
final WipResponse response = await connection.sendCommand('Page.captureScreenshot', <String, Object>{
355+
// Clip the screenshot to include only the element.
356+
// Prior to taking a screenshot, we are calling `window.render()` in
357+
// `_matchers_web.dart` to only render the element on screen. That
358+
// will make sure that the element will always be displayed on the
359+
// origin of the screen.
360+
'clip': <String, Object>{
361+
'x': 0.0,
362+
'y': 0.0,
363+
'width': width!.toDouble(),
364+
'height': height!.toDouble(),
365+
'scale': 1.0,
366+
},
367+
});
368+
bytes = base64.decode(response.result!['data'] as String);
369+
} on WipError catch (ex) {
370+
_logger.printError('Caught WIPError: $ex');
371+
return shelf.Response.ok('WIP error: $ex');
372+
} on FormatException catch (ex) {
373+
_logger.printError('Caught FormatException: $ex');
374+
return shelf.Response.ok('Caught exception: $ex');
375+
}
369376
}
370-
371377
final String? errorMessage = await _testGoldenComparator.compareGoldens(testUri, bytes, goldenKey, updateGoldens);
372378
return shelf.Response.ok(errorMessage ?? 'true');
373379
} else {

0 commit comments

Comments
 (0)