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 { ...@@ -299,6 +299,16 @@ class DefaultWebGoldenComparator extends WebGoldenComparator {
Future<void> update(double width, double height, Uri golden) { Future<void> update(double width, double height, Uri golden) {
throw UnsupportedError('DefaultWebGoldenComparator is only supported on the web.'); 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. /// Reads the red value out of a 32 bit rgba pixel.
......
...@@ -80,4 +80,30 @@ class DefaultWebGoldenComparator extends WebGoldenComparator { ...@@ -80,4 +80,30 @@ class DefaultWebGoldenComparator extends WebGoldenComparator {
// Update is handled on the server side, just use the same logic here // Update is handled on the server side, just use the same logic here
await compare(width, height, golden); 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 @@ ...@@ -4,6 +4,7 @@
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:matcher/expect.dart'; import 'package:matcher/expect.dart';
...@@ -61,25 +62,58 @@ class MatchesGoldenFile extends AsyncMatcher { ...@@ -61,25 +62,58 @@ class MatchesGoldenFile extends AsyncMatcher {
final ui.FlutterView view = binding.platformDispatcher.implicitView!; final ui.FlutterView view = binding.platformDispatcher.implicitView!;
final RenderView renderView = binding.renderViews.firstWhere((RenderView r) => r.flutterView == view); final RenderView renderView = binding.renderViews.firstWhere((RenderView r) => r.flutterView == view);
// Unlike `flutter_tester`, we don't have the ability to render an element if (isCanvasKit) {
// to an image directly. Instead, we will use `window.render()` to render // In CanvasKit, use Layer.toImage to generate the screenshot.
// only the element being requested, and send a request to the test server final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.instance;
// requesting it to take a screenshot through the browser's debug interface. return binding.runAsync<String?>(() async {
_renderElement(view, renderObject); assert(element.renderObject != null);
final String? result = await binding.runAsync<String?>(() async { RenderObject renderObject = element.renderObject!;
if (autoUpdateGoldenFiles) { while (!renderObject.isRepaintBoundary) {
await webGoldenComparator.update(size.width, size.height, key); renderObject = renderObject.parent!;
return null; }
} assert(!renderObject.debugNeedsPaint);
try { final OffsetLayer layer = renderObject.debugLayer! as OffsetLayer;
final bool success = await webGoldenComparator.compare(size.width, size.height, key); final ui.Image image = await layer.toImage(renderObject.paintBounds);
return success ? null : 'does not match'; try {
} on TestFailure catch (ex) { final ByteData? bytes = await image.toByteData(format: ui.ImageByteFormat.png);
return ex.message; if (bytes == null) {
} return 'could not encode screenshot.';
}); }
_renderElement(view, renderView); if (autoUpdateGoldenFiles) {
return result; 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 @override
......
...@@ -185,6 +185,34 @@ abstract class WebGoldenComparator { ...@@ -185,6 +185,34 @@ abstract class WebGoldenComparator {
/// is left up to the implementation class. /// is left up to the implementation class.
Future<void> update(double width, double height, Uri golden); 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 /// Returns a new golden file [Uri] to incorporate any [version] number with
/// the [key]. /// the [key].
/// ///
...@@ -298,12 +326,7 @@ class _TrivialWebGoldenComparator implements WebGoldenComparator { ...@@ -298,12 +326,7 @@ class _TrivialWebGoldenComparator implements WebGoldenComparator {
@override @override
Future<bool> compare(double width, double height, Uri golden) { Future<bool> compare(double width, double height, Uri golden) {
// Ideally we would use markTestSkipped here but in some situations, return _warnAboutSkipping(golden);
// 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);
} }
@override @override
...@@ -315,6 +338,25 @@ class _TrivialWebGoldenComparator implements WebGoldenComparator { ...@@ -315,6 +338,25 @@ class _TrivialWebGoldenComparator implements WebGoldenComparator {
Uri getTestUri(Uri key, int? version) { Uri getTestUri(Uri key, int? version) {
return key; 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. /// The result of a pixel comparison test.
......
...@@ -336,38 +336,44 @@ class FlutterWebPlatform extends PlatformPlugin { ...@@ -336,38 +336,44 @@ class FlutterWebPlatform extends PlatformPlugin {
final Map<String, Object?> body = json.decode(await request.readAsString()) as Map<String, Object?>; 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 goldenKey = Uri.parse(body['key']! as String);
final Uri testUri = Uri.parse(body['testUri']! as String); final Uri testUri = Uri.parse(body['testUri']! as String);
final num width = body['width']! as num; final num? width = body['width'] as num?;
final num height = body['height']! as num; final num? height = body['height'] as num?;
Uint8List bytes; Uint8List bytes;
try { if (body.containsKey('bytes')) {
final ChromeTab chromeTab = (await _browserManager!._browser.chromeConnection.getTab((ChromeTab tab) { bytes = base64.decode(body['bytes']! as String);
return tab.url.contains(_browserManager!._browser.url!); } else {
}))!; // TODO(hterkelsen): Do not use browser screenshots for testing on the
final WipConnection connection = await chromeTab.connect(); // web once we transition off the HTML renderer. See:
final WipResponse response = await connection.sendCommand('Page.captureScreenshot', <String, Object>{ // https://github.com/flutter/flutter/issues/135700
// Clip the screenshot to include only the element. try {
// Prior to taking a screenshot, we are calling `window.render()` in final ChromeTab chromeTab = (await _browserManager!._browser.chromeConnection.getTab((ChromeTab tab) {
// `_matchers_web.dart` to only render the element on screen. That return tab.url.contains(_browserManager!._browser.url!);
// will make sure that the element will always be displayed on the }))!;
// origin of the screen. final WipConnection connection = await chromeTab.connect();
'clip': <String, Object>{ final WipResponse response = await connection.sendCommand('Page.captureScreenshot', <String, Object>{
'x': 0.0, // Clip the screenshot to include only the element.
'y': 0.0, // Prior to taking a screenshot, we are calling `window.render()` in
'width': width.toDouble(), // `_matchers_web.dart` to only render the element on screen. That
'height': height.toDouble(), // will make sure that the element will always be displayed on the
'scale': 1.0, // origin of the screen.
}, 'clip': <String, Object>{
}); 'x': 0.0,
bytes = base64.decode(response.result!['data'] as String); 'y': 0.0,
} on WipError catch (ex) { 'width': width!.toDouble(),
_logger.printError('Caught WIPError: $ex'); 'height': height!.toDouble(),
return shelf.Response.ok('WIP error: $ex'); 'scale': 1.0,
} on FormatException catch (ex) { },
_logger.printError('Caught FormatException: $ex'); });
return shelf.Response.ok('Caught exception: $ex'); 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); final String? errorMessage = await _testGoldenComparator.compareGoldens(testUri, bytes, goldenKey, updateGoldens);
return shelf.Response.ok(errorMessage ?? 'true'); return shelf.Response.ok(errorMessage ?? 'true');
} else { } 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