Unverified Commit 464e751a authored by Harry Terkelsen's avatar Harry Terkelsen Committed by GitHub

Reland "Use Layer.toImage for golden tests on CanvasKit" (#136918)

Relands https://github.com/flutter/flutter/pull/135249

A golden test was failing in post submit in the previous PR
parent 9beb98aa
......@@ -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