mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
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.
This commit is contained in:
parent
b01d3a2abc
commit
ccd070a5b4
@ -13,6 +13,7 @@ import 'dart:ui' as ui
|
||||
window,
|
||||
ClipOp,
|
||||
Image,
|
||||
ImageByteFormat,
|
||||
Paragraph,
|
||||
Picture,
|
||||
PictureRecorder,
|
||||
@ -1026,6 +1027,36 @@ class WidgetInspectorService {
|
||||
name: '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.
|
||||
|
||||
@ -512,7 +512,7 @@ void main() {
|
||||
|
||||
// If you add a service extension... TEST IT! :-)
|
||||
// ...then increment this number.
|
||||
expect(binding.extensions.length, 37);
|
||||
expect(binding.extensions.length, 38);
|
||||
|
||||
expect(console, isEmpty);
|
||||
debugPrint = debugPrintThrottled;
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
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/rendering.dart';
|
||||
@ -1545,18 +1545,48 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
|
||||
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.
|
||||
// This golden image is platform dependent due to the clip icon.
|
||||
await expectLater(
|
||||
service.screenshot(
|
||||
find.byType(ClipRRect).evaluate().single,
|
||||
width: 100.0,
|
||||
height: 100.0,
|
||||
margin: 20.0,
|
||||
debugPaint: true,
|
||||
),
|
||||
clipRectScreenshot,
|
||||
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
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui' as ui;
|
||||
import 'dart:ui';
|
||||
|
||||
@ -275,9 +276,11 @@ Matcher coversSameAreaAs(Path expectedPath, {@required Rect areaToCompare, int s
|
||||
/// See also:
|
||||
///
|
||||
/// * [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
|
||||
/// may swap out the backend for this matcher.
|
||||
Matcher matchesGoldenFile(dynamic key) {
|
||||
AsyncMatcher matchesGoldenFile(dynamic key) {
|
||||
if (key is Uri) {
|
||||
return _MatchesGoldenFile(key);
|
||||
} else if (key is String) {
|
||||
@ -286,6 +289,42 @@ Matcher matchesGoldenFile(dynamic key) {
|
||||
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.
|
||||
///
|
||||
/// If either the label, hint, value, textDirection, or rect fields are not
|
||||
@ -1513,6 +1552,76 @@ Future<ui.Image> _captureImage(Element element) {
|
||||
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 {
|
||||
const _MatchesGoldenFile(this.key);
|
||||
|
||||
|
||||
68
packages/flutter_test/test/reference_image_test.dart
Normal file
68
packages/flutter_test/test/reference_image_test.dart
Normal file
@ -0,0 +1,68 @@
|
||||
// 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'),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user