Unverified Commit ccd070a5 authored by Jacob Richman's avatar Jacob Richman Committed by GitHub

Add back screenshot extension that was temporarily disabled. (#21828)

Add back screenshot extension that was temporarily disabled.

Add matchesReferenceImage matcher to test that the screenshot extension
generates equivalent images to InspectorService.instance.screenshot.
parent b01d3a2a
...@@ -13,6 +13,7 @@ import 'dart:ui' as ui ...@@ -13,6 +13,7 @@ import 'dart:ui' as ui
window, window,
ClipOp, ClipOp,
Image, Image,
ImageByteFormat,
Paragraph, Paragraph,
Picture, Picture,
PictureRecorder, PictureRecorder,
...@@ -1026,6 +1027,36 @@ class WidgetInspectorService { ...@@ -1026,6 +1027,36 @@ class WidgetInspectorService {
name: 'isWidgetCreationTracked', name: 'isWidgetCreationTracked',
callback: isWidgetCreationTracked, callback: isWidgetCreationTracked,
); );
assert(() {
registerServiceExtension(
name: 'screenshot',
callback: (Map<String, String> parameters) async {
assert(parameters.containsKey('id'));
assert(parameters.containsKey('width'));
assert(parameters.containsKey('height'));
final ui.Image image = await screenshot(
toObject(parameters['id']),
width: double.parse(parameters['width']),
height: double.parse(parameters['height']),
margin: parameters.containsKey('margin') ?
double.parse(parameters['margin']) : 0.0,
maxPixelRatio: parameters.containsKey('maxPixelRatio') ?
double.parse(parameters['maxPixelRatio']) : 1.0,
debugPaint: parameters['debugPaint'] == 'true',
);
if (image == null) {
return <String, Object>{'result': null};
}
final ByteData byteData = await image.toByteData(format:ui.ImageByteFormat.png);
return <String, Object>{
'result': base64.encoder.convert(Uint8List.view(byteData.buffer)),
};
},
);
return true;
}());
} }
/// Clear all InspectorService object references. /// Clear all InspectorService object references.
......
...@@ -512,7 +512,7 @@ void main() { ...@@ -512,7 +512,7 @@ void main() {
// If you add a service extension... TEST IT! :-) // If you add a service extension... TEST IT! :-)
// ...then increment this number. // ...then increment this number.
expect(binding.extensions.length, 37); expect(binding.extensions.length, 38);
expect(console, isEmpty); expect(console, isEmpty);
debugPrint = debugPrintThrottled; debugPrint = debugPrintThrottled;
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io' show Platform; import 'dart:io' show Platform;
import 'dart:ui' as ui show PictureRecorder; import 'dart:ui' as ui;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
...@@ -1545,18 +1545,48 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { ...@@ -1545,18 +1545,48 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
matchesGoldenFile('inspector.clipRect_debugPaint.png'), matchesGoldenFile('inspector.clipRect_debugPaint.png'),
); );
final Element clipRect = find.byType(ClipRRect).evaluate().single;
final Future<ui.Image> clipRectScreenshot = service.screenshot(
clipRect,
width: 100.0,
height: 100.0,
margin: 20.0,
debugPaint: true,
);
// Add a margin so that the clip icon shows up in the screenshot. // Add a margin so that the clip icon shows up in the screenshot.
// This golden image is platform dependent due to the clip icon. // This golden image is platform dependent due to the clip icon.
await expectLater( await expectLater(
service.screenshot( clipRectScreenshot,
find.byType(ClipRRect).evaluate().single,
width: 100.0,
height: 100.0,
margin: 20.0,
debugPaint: true,
),
matchesGoldenFile('inspector.clipRect_debugPaint_margin.png'), matchesGoldenFile('inspector.clipRect_debugPaint_margin.png'),
skip: !Platform.isLinux skip: !Platform.isLinux,
);
// Verify we get the same image if we go through the service extension
// instead of invoking the screenshot method directly.
final Future<Object> base64ScreenshotFuture = service.testExtension(
'screenshot',
<String, String>{
'id': service.toId(clipRect, 'group'),
'width': '100.0',
'height': '100.0',
'margin': '20.0',
'debugPaint': 'true',
},
);
final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding
.ensureInitialized();
final ui.Image screenshotImage = await binding.runAsync<ui.Image>(() async {
final String base64Screenshot = await base64ScreenshotFuture;
final ui.Codec codec = await ui.instantiateImageCodec(base64.decode(base64Screenshot));
final ui.FrameInfo frame = await codec.getNextFrame();
return frame.image;
}, additionalTime: const Duration(seconds: 11));
await expectLater(
screenshotImage,
matchesReferenceImage(await clipRectScreenshot),
); );
// Test with a very visible debug paint // Test with a very visible debug paint
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:math' as math; import 'dart:math' as math;
import 'dart:typed_data';
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'dart:ui'; import 'dart:ui';
...@@ -275,9 +276,11 @@ Matcher coversSameAreaAs(Path expectedPath, {@required Rect areaToCompare, int s ...@@ -275,9 +276,11 @@ Matcher coversSameAreaAs(Path expectedPath, {@required Rect areaToCompare, int s
/// See also: /// See also:
/// ///
/// * [goldenFileComparator], which acts as the backend for this matcher. /// * [goldenFileComparator], which acts as the backend for this matcher.
/// * [matchesReferenceImage], which should be used instead if you want to
/// verify that two different code paths create identical images.
/// * [flutter_test] for a discussion of test configurations, whereby callers /// * [flutter_test] for a discussion of test configurations, whereby callers
/// may swap out the backend for this matcher. /// may swap out the backend for this matcher.
Matcher matchesGoldenFile(dynamic key) { AsyncMatcher matchesGoldenFile(dynamic key) {
if (key is Uri) { if (key is Uri) {
return _MatchesGoldenFile(key); return _MatchesGoldenFile(key);
} else if (key is String) { } else if (key is String) {
...@@ -286,6 +289,42 @@ Matcher matchesGoldenFile(dynamic key) { ...@@ -286,6 +289,42 @@ Matcher matchesGoldenFile(dynamic key) {
throw ArgumentError('Unexpected type for golden file: ${key.runtimeType}'); throw ArgumentError('Unexpected type for golden file: ${key.runtimeType}');
} }
/// Asserts that a [Finder], [Future<ui.Image>], or [ui.Image] matches a
/// reference image identified by [image].
///
/// For the case of a [Finder], the [Finder] must match exactly one widget and
/// the rendered image of the first [RepaintBoundary] ancestor of the widget is
/// treated as the image for the widget.
///
/// This is an asynchronous matcher, meaning that callers should use
/// [expectLater] when using this matcher and await the future returned by
/// [expectLater].
///
/// ## Sample code
///
/// ```dart
/// final ui.Paint paint = ui.Paint()
/// ..style = ui.PaintingStyle.stroke
/// ..strokeWidth = 1.0;
/// final ui.PictureRecorder recorder = ui.PictureRecorder();
/// final ui.Canvas pictureCanvas = ui.Canvas(recorder);
/// pictureCanvas.drawCircle(Offset.zero, 20.0, paint);
/// final ui.Picture picture = recorder.endRecording();
/// ui.Image referenceImage = picture.toImage(50, 50);
///
/// await expectLater(find.text('Save'), matchesReferenceImage(referenceImage));
/// await expectLater(image, matchesReferenceImage(referenceImage);
/// await expectLater(imageFuture, matchesReferenceImage(referenceImage));
/// ```
///
/// See also:
///
/// * [matchesGoldenFile], which should be used instead if you need to verify
/// that a [Finder] or [ui.Image] matches a golden image.
AsyncMatcher matchesReferenceImage(ui.Image image) {
return _MatchesReferenceImage(image);
}
/// Asserts that a [SemanticsData] contains the specified information. /// Asserts that a [SemanticsData] contains the specified information.
/// ///
/// If either the label, hint, value, textDirection, or rect fields are not /// If either the label, hint, value, textDirection, or rect fields are not
...@@ -1513,6 +1552,76 @@ Future<ui.Image> _captureImage(Element element) { ...@@ -1513,6 +1552,76 @@ Future<ui.Image> _captureImage(Element element) {
return layer.toImage(renderObject.paintBounds); return layer.toImage(renderObject.paintBounds);
} }
int _countDifferentPixels(Uint8List imageA, Uint8List imageB) {
assert(imageA.length == imageB.length);
int delta = 0;
for (int i = 0; i < imageA.length; i+=4) {
if (imageA[i] != imageB[i] ||
imageA[i+1] != imageB[i+1] ||
imageA[i+2] != imageB[i+2] ||
imageA[i+3] != imageB[i+3]) {
delta++;
}
}
return delta;
}
class _MatchesReferenceImage extends AsyncMatcher {
const _MatchesReferenceImage(this.referenceImage);
final ui.Image referenceImage;
@override
Future<String> matchAsync(dynamic item) async {
Future<ui.Image> imageFuture;
if (item is Future<ui.Image>) {
imageFuture = item;
} else if (item is ui.Image) {
imageFuture = Future<ui.Image>.value(item);
} else {
final Finder finder = item;
final Iterable<Element> elements = finder.evaluate();
if (elements.isEmpty) {
return 'could not be rendered because no widget was found';
} else if (elements.length > 1) {
return 'matched too many widgets';
}
imageFuture = _captureImage(elements.single);
}
final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized();
return binding.runAsync<String>(() async {
final ui.Image image = await imageFuture;
final ByteData bytes = await image.toByteData()
.timeout(const Duration(seconds: 10), onTimeout: () => null);
if (bytes == null) {
return 'Failed to generate an image from engine within the 10,000ms timeout.';
}
final ByteData referenceBytes = await referenceImage.toByteData()
.timeout(const Duration(seconds: 10), onTimeout: () => null);
if (referenceBytes == null) {
return 'Failed to generate an image from engine within the 10,000ms timeout.';
}
if (referenceImage.height != image.height || referenceImage.width != image.width) {
return 'does not match as width or height do not match. $image != $referenceImage';
}
final int countDifferentPixels = _countDifferentPixels(
Uint8List.view(bytes.buffer),
Uint8List.view(referenceBytes.buffer),
);
return countDifferentPixels == 0 ? null : 'does not match on $countDifferentPixels pixels';
}, additionalTime: const Duration(seconds: 21));
}
@override
Description describe(Description description) {
return description.add('rasterized image matches that of a $referenceImage reference image');
}
}
class _MatchesGoldenFile extends AsyncMatcher { class _MatchesGoldenFile extends AsyncMatcher {
const _MatchesGoldenFile(this.key); const _MatchesGoldenFile(this.key);
......
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui' as ui;
import 'package:flutter_test/flutter_test.dart';
ui.Image createTestImage(int width, int height, ui.Color color) {
final ui.Paint paint = ui.Paint()
..style = ui.PaintingStyle.stroke
..strokeWidth = 1.0
..color = color;
final ui.PictureRecorder recorder = ui.PictureRecorder();
final ui.Canvas pictureCanvas = ui.Canvas(recorder);
pictureCanvas.drawCircle(Offset.zero, 20.0, paint);
final ui.Picture picture = recorder.endRecording();
return picture.toImage(width, height);
}
void main() {
const ui.Color red = ui.Color.fromARGB(255, 255, 0, 0);
const ui.Color green = ui.Color.fromARGB(255, 0, 255, 0);
const ui.Color transparentRed = ui.Color.fromARGB(128, 255, 0, 0);
group('succeeds', () {
testWidgets('when images have the same content', (WidgetTester tester) async {
await expectLater(
createTestImage(100, 100, red),
matchesReferenceImage(createTestImage(100, 100, red)),
);
await expectLater(
createTestImage(100, 100, green),
matchesReferenceImage(createTestImage(100, 100, green)),
);
await expectLater(
createTestImage(100, 100, transparentRed),
matchesReferenceImage(createTestImage(100, 100, transparentRed)),
);
});
testWidgets('when images are identical', (WidgetTester tester) async {
final ui.Image image = createTestImage(100, 100, red);
await expectLater(image, matchesReferenceImage(image));
});
});
group('fails', () {
testWidgets('when image sizes do not match', (WidgetTester tester) async {
expect(
await matchesReferenceImage(createTestImage(50, 50, red)).matchAsync(createTestImage(100, 100, red)),
equals('does not match as width or height do not match. [100×100] != [50×50]'),
);
});
testWidgets('when image pixels do not match', (WidgetTester tester) async {
expect(
await matchesReferenceImage(createTestImage(100, 100, red)).matchAsync(createTestImage(100, 100, transparentRed)),
equals('does not match on 53 pixels'),
);
expect(
await matchesReferenceImage(createTestImage(100, 100, red)).matchAsync(createTestImage(100, 100, green)),
equals('does not match on 57 pixels'),
);
});
});
}
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