diff --git a/packages/flutter_test/lib/src/_goldens_io.dart b/packages/flutter_test/lib/src/_goldens_io.dart index 371458d0535..9a8481c92ae 100644 --- a/packages/flutter_test/lib/src/_goldens_io.dart +++ b/packages/flutter_test/lib/src/_goldens_io.dart @@ -299,6 +299,16 @@ class DefaultWebGoldenComparator extends WebGoldenComparator { Future update(double width, double height, Uri golden) { throw UnsupportedError('DefaultWebGoldenComparator is only supported on the web.'); } + + @override + Future compareBytes(Uint8List bytes, Uri golden) { + throw UnsupportedError('DefaultWebGoldenComparator is only supported on the web.'); + } + + @override + Future 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. diff --git a/packages/flutter_test/lib/src/_goldens_web.dart b/packages/flutter_test/lib/src/_goldens_web.dart index 4053e4d22d2..b29f9ccdca1 100644 --- a/packages/flutter_test/lib/src/_goldens_web.dart +++ b/packages/flutter_test/lib/src/_goldens_web.dart @@ -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 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({ + 'testUri': testUri.toString(), + 'key': key, + 'bytes': bytesEncoded, + }), + ); + final String response = request.response as String; + if (response == 'true') { + return true; + } + fail(response); + } + + @override + Future updateBytes(Uint8List bytes, Uri golden) async { + // Update is handled on the server side, just use the same logic here + await compareBytes(bytes, golden); + } } diff --git a/packages/flutter_test/lib/src/_matchers_web.dart b/packages/flutter_test/lib/src/_matchers_web.dart index 7f54720eb23..bb40a8ebd0f 100644 --- a/packages/flutter_test/lib/src/_matchers_web.dart +++ b/packages/flutter_test/lib/src/_matchers_web.dart @@ -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(() 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(() 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(() 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 diff --git a/packages/flutter_test/lib/src/goldens.dart b/packages/flutter_test/lib/src/goldens.dart index 721952bf4c2..0f83ac832ed 100644 --- a/packages/flutter_test/lib/src/goldens.dart +++ b/packages/flutter_test/lib/src/goldens.dart @@ -185,6 +185,34 @@ abstract class WebGoldenComparator { /// is left up to the implementation class. Future 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 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 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 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.value(true); + return _warnAboutSkipping(golden); } @override @@ -315,6 +338,25 @@ class _TrivialWebGoldenComparator implements WebGoldenComparator { Uri getTestUri(Uri key, int? version) { return key; } + + @override + Future compareBytes(Uint8List bytes, Uri golden) { + return _warnAboutSkipping(golden); + } + + @override + Future updateBytes(Uint8List bytes, Uri golden) { + throw StateError('webGoldenComparator has not been initialized'); + } + + Future _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.value(true); + } } /// The result of a pixel comparison test. diff --git a/packages/flutter_tools/lib/src/test/flutter_web_platform.dart b/packages/flutter_tools/lib/src/test/flutter_web_platform.dart index 6400b7a0593..5a2fc29ab4e 100644 --- a/packages/flutter_tools/lib/src/test/flutter_web_platform.dart +++ b/packages/flutter_tools/lib/src/test/flutter_web_platform.dart @@ -336,38 +336,44 @@ class FlutterWebPlatform extends PlatformPlugin { final Map body = json.decode(await request.readAsString()) as Map; 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', { - // 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': { - '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', { + // 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': { + '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 {