Unverified Commit 747128ff authored by Harry Terkelsen's avatar Harry Terkelsen Committed by GitHub

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
parent 9391a88b
......@@ -299,6 +299,16 @@ class DefaultWebGoldenComparator extends WebGoldenComparator {
Future<void> update(double width, double height, Uri golden) {
throw UnsupportedError('DefaultWebGoldenComparator is only supported on the web.');
}
@override
Future<bool> compareBytes(Uint8List bytes, Uri golden) {
throw UnsupportedError('DefaultWebGoldenComparator is only supported on the web.');
}
@override
Future<void> updateBytes(Uint8List bytes, Uri golden) {
throw UnsupportedError('DefaultWebGoldenComparator is only supported on the web.');
}
}
/// Reads the red value out of a 32 bit rgba pixel.
......
......@@ -80,4 +80,30 @@ class DefaultWebGoldenComparator extends WebGoldenComparator {
// Update is handled on the server side, just use the same logic here
await compare(width, height, golden);
}
@override
Future<bool> compareBytes(Uint8List bytes, Uri golden) async {
final String key = golden.toString();
final String bytesEncoded = base64.encode(bytes);
final html.HttpRequest request = await html.HttpRequest.request(
'flutter_goldens',
method: 'POST',
sendData: json.encode(<String, Object>{
'testUri': testUri.toString(),
'key': key,
'bytes': bytesEncoded,
}),
);
final String response = request.response as String;
if (response == 'true') {
return true;
}
fail(response);
}
@override
Future<void> updateBytes(Uint8List bytes, Uri golden) async {
// Update is handled on the server side, just use the same logic here
await compareBytes(bytes, golden);
}
}
......@@ -4,6 +4,7 @@
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:matcher/expect.dart';
......@@ -61,25 +62,58 @@ class MatchesGoldenFile extends AsyncMatcher {
final ui.FlutterView view = binding.platformDispatcher.implicitView!;
final RenderView renderView = binding.renderViews.firstWhere((RenderView r) => r.flutterView == view);
// Unlike `flutter_tester`, we don't have the ability to render an element
// to an image directly. Instead, we will use `window.render()` to render
// only the element being requested, and send a request to the test server
// requesting it to take a screenshot through the browser's debug interface.
_renderElement(view, renderObject);
final String? result = await binding.runAsync<String?>(() async {
if (autoUpdateGoldenFiles) {
await webGoldenComparator.update(size.width, size.height, key);
return null;
}
try {
final bool success = await webGoldenComparator.compare(size.width, size.height, key);
return success ? null : 'does not match';
} on TestFailure catch (ex) {
return ex.message;
}
});
_renderElement(view, renderView);
return result;
if (isCanvasKit) {
// In CanvasKit, use Layer.toImage to generate the screenshot.
final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.instance;
return binding.runAsync<String?>(() async {
assert(element.renderObject != null);
RenderObject renderObject = element.renderObject!;
while (!renderObject.isRepaintBoundary) {
renderObject = renderObject.parent!;
}
assert(!renderObject.debugNeedsPaint);
final OffsetLayer layer = renderObject.debugLayer! as OffsetLayer;
final ui.Image image = await layer.toImage(renderObject.paintBounds);
try {
final ByteData? bytes = await image.toByteData(format: ui.ImageByteFormat.png);
if (bytes == null) {
return 'could not encode screenshot.';
}
if (autoUpdateGoldenFiles) {
await webGoldenComparator.updateBytes(bytes.buffer.asUint8List(), key);
return null;
}
try {
final bool success = await webGoldenComparator.compareBytes(bytes.buffer.asUint8List(), key);
return success ? null : 'does not match';
} on TestFailure catch (ex) {
return ex.message;
}
} finally {
image.dispose();
}
});
} else {
// In the HTML renderer, we don't have the ability to render an element
// to an image directly. Instead, we will use `window.render()` to render
// only the element being requested, and send a request to the test server
// requesting it to take a screenshot through the browser's debug interface.
_renderElement(view, renderObject);
final String? result = await binding.runAsync<String?>(() async {
if (autoUpdateGoldenFiles) {
await webGoldenComparator.update(size.width, size.height, key);
return null;
}
try {
final bool success = await webGoldenComparator.compare(size.width, size.height, key);
return success ? null : 'does not match';
} on TestFailure catch (ex) {
return ex.message;
}
});
_renderElement(view, renderView);
return result;
}
}
@override
......
......@@ -185,6 +185,34 @@ abstract class WebGoldenComparator {
/// is left up to the implementation class.
Future<void> update(double width, double height, Uri golden);
/// Compares the pixels of decoded png [bytes] against the golden file
/// identified by [golden].
///
/// The returned future completes with a boolean value that indicates whether
/// the pixels rendered on screen match the golden file's pixels.
///
/// In the case of comparison mismatch, the comparator may choose to throw a
/// [TestFailure] if it wants to control the failure message, often in the
/// form of a [ComparisonResult] that provides detailed information about the
/// mismatch.
///
/// The method by which [golden] is located and by which its bytes are loaded
/// is left up to the implementation class. For instance, some implementations
/// may load files from the local file system, whereas others may load files
/// over the network or from a remote repository.
Future<bool> compareBytes(Uint8List bytes, Uri golden);
/// Compares the pixels of decoded png [bytes] against the golden file
/// identified by [golden].
///
/// This will be invoked in lieu of [compareBytes] when [autoUpdateGoldenFiles]
/// is `true` (which gets set automatically by the test framework when the
/// user runs `flutter test --update-goldens --platform=chrome`).
///
/// The method by which [golden] is located and by which its bytes are written
/// is left up to the implementation class.
Future<void> updateBytes(Uint8List bytes, Uri golden);
/// Returns a new golden file [Uri] to incorporate any [version] number with
/// the [key].
///
......@@ -298,12 +326,7 @@ class _TrivialWebGoldenComparator implements WebGoldenComparator {
@override
Future<bool> compare(double width, double height, Uri golden) {
// Ideally we would use markTestSkipped here but in some situations,
// comparators are called outside of tests.
// See also: https://github.com/flutter/flutter/issues/91285
// ignore: avoid_print
print('Golden comparison requested for "$golden"; skipping...');
return Future<bool>.value(true);
return _warnAboutSkipping(golden);
}
@override
......@@ -315,6 +338,25 @@ class _TrivialWebGoldenComparator implements WebGoldenComparator {
Uri getTestUri(Uri key, int? version) {
return key;
}
@override
Future<bool> compareBytes(Uint8List bytes, Uri golden) {
return _warnAboutSkipping(golden);
}
@override
Future<void> updateBytes(Uint8List bytes, Uri golden) {
throw StateError('webGoldenComparator has not been initialized');
}
Future<bool> _warnAboutSkipping(Uri golden) {
// Ideally we would use markTestSkipped here but in some situations,
// comparators are called outside of tests.
// See also: https://github.com/flutter/flutter/issues/91285
// ignore: avoid_print
print('Golden comparison requested for "$golden"; skipping...');
return Future<bool>.value(true);
}
}
/// The result of a pixel comparison test.
......
......@@ -336,38 +336,44 @@ class FlutterWebPlatform extends PlatformPlugin {
final Map<String, Object?> body = json.decode(await request.readAsString()) as Map<String, Object?>;
final Uri goldenKey = Uri.parse(body['key']! as String);
final Uri testUri = Uri.parse(body['testUri']! as String);
final num width = body['width']! as num;
final num height = body['height']! as num;
final num? width = body['width'] as num?;
final num? height = body['height'] as num?;
Uint8List bytes;
try {
final ChromeTab chromeTab = (await _browserManager!._browser.chromeConnection.getTab((ChromeTab tab) {
return tab.url.contains(_browserManager!._browser.url!);
}))!;
final WipConnection connection = await chromeTab.connect();
final WipResponse response = await connection.sendCommand('Page.captureScreenshot', <String, Object>{
// Clip the screenshot to include only the element.
// Prior to taking a screenshot, we are calling `window.render()` in
// `_matchers_web.dart` to only render the element on screen. That
// will make sure that the element will always be displayed on the
// origin of the screen.
'clip': <String, Object>{
'x': 0.0,
'y': 0.0,
'width': width.toDouble(),
'height': height.toDouble(),
'scale': 1.0,
},
});
bytes = base64.decode(response.result!['data'] as String);
} on WipError catch (ex) {
_logger.printError('Caught WIPError: $ex');
return shelf.Response.ok('WIP error: $ex');
} on FormatException catch (ex) {
_logger.printError('Caught FormatException: $ex');
return shelf.Response.ok('Caught exception: $ex');
if (body.containsKey('bytes')) {
bytes = base64.decode(body['bytes']! as String);
} else {
// TODO(hterkelsen): Do not use browser screenshots for testing on the
// web once we transition off the HTML renderer. See:
// https://github.com/flutter/flutter/issues/135700
try {
final ChromeTab chromeTab = (await _browserManager!._browser.chromeConnection.getTab((ChromeTab tab) {
return tab.url.contains(_browserManager!._browser.url!);
}))!;
final WipConnection connection = await chromeTab.connect();
final WipResponse response = await connection.sendCommand('Page.captureScreenshot', <String, Object>{
// Clip the screenshot to include only the element.
// Prior to taking a screenshot, we are calling `window.render()` in
// `_matchers_web.dart` to only render the element on screen. That
// will make sure that the element will always be displayed on the
// origin of the screen.
'clip': <String, Object>{
'x': 0.0,
'y': 0.0,
'width': width!.toDouble(),
'height': height!.toDouble(),
'scale': 1.0,
},
});
bytes = base64.decode(response.result!['data'] as String);
} on WipError catch (ex) {
_logger.printError('Caught WIPError: $ex');
return shelf.Response.ok('WIP error: $ex');
} on FormatException catch (ex) {
_logger.printError('Caught FormatException: $ex');
return shelf.Response.ok('Caught exception: $ex');
}
}
final String? errorMessage = await _testGoldenComparator.compareGoldens(testUri, bytes, goldenKey, updateGoldens);
return shelf.Response.ok(errorMessage ?? 'true');
} else {
......
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