Implement screenshot test for flutter web. (#45530)
This commit is contained in:
parent
9233b53255
commit
c2eb0681ce
24
.cirrus.yml
24
.cirrus.yml
@ -153,7 +153,10 @@ task:
|
||||
# As of October 2019, the Web shards needed more than 6G of RAM.
|
||||
CPU: 2
|
||||
MEMORY: 8G
|
||||
GOLDCTL: "$CIRRUS_WORKING_DIR/depot_tools/goldctl"
|
||||
GOLD_SERVICE_ACCOUNT: ENCRYPTED[3afeea5ac7201151c3d0dc9648862f0462b5e4f55dc600ca8b692319622f7c3eda3d577b1b16cc2ef0311b7314c1c095]
|
||||
script:
|
||||
- ./dev/bots/download_goldctl.sh
|
||||
- dart --enable-asserts ./dev/bots/test.dart
|
||||
|
||||
- name: web_tests-1-linux
|
||||
@ -162,7 +165,10 @@ task:
|
||||
# As of October 2019, the Web shards needed more than 6G of RAM.
|
||||
CPU: 2
|
||||
MEMORY: 8G
|
||||
GOLDCTL: "$CIRRUS_WORKING_DIR/depot_tools/goldctl"
|
||||
GOLD_SERVICE_ACCOUNT: ENCRYPTED[3afeea5ac7201151c3d0dc9648862f0462b5e4f55dc600ca8b692319622f7c3eda3d577b1b16cc2ef0311b7314c1c095]
|
||||
script:
|
||||
- ./dev/bots/download_goldctl.sh
|
||||
- dart --enable-asserts ./dev/bots/test.dart
|
||||
|
||||
- name: web_tests-2-linux
|
||||
@ -171,7 +177,10 @@ task:
|
||||
# As of October 2019, the Web shards needed more than 6G of RAM.
|
||||
CPU: 2
|
||||
MEMORY: 8G
|
||||
GOLDCTL: "$CIRRUS_WORKING_DIR/depot_tools/goldctl"
|
||||
GOLD_SERVICE_ACCOUNT: ENCRYPTED[3afeea5ac7201151c3d0dc9648862f0462b5e4f55dc600ca8b692319622f7c3eda3d577b1b16cc2ef0311b7314c1c095]
|
||||
script:
|
||||
- ./dev/bots/download_goldctl.sh
|
||||
- dart --enable-asserts ./dev/bots/test.dart
|
||||
|
||||
- name: web_tests-3-linux
|
||||
@ -180,7 +189,10 @@ task:
|
||||
# As of October 2019, the Web shards needed more than 6G of RAM.
|
||||
CPU: 2
|
||||
MEMORY: 8G
|
||||
GOLDCTL: "$CIRRUS_WORKING_DIR/depot_tools/goldctl"
|
||||
GOLD_SERVICE_ACCOUNT: ENCRYPTED[3afeea5ac7201151c3d0dc9648862f0462b5e4f55dc600ca8b692319622f7c3eda3d577b1b16cc2ef0311b7314c1c095]
|
||||
script:
|
||||
- ./dev/bots/download_goldctl.sh
|
||||
- dart --enable-asserts ./dev/bots/test.dart
|
||||
|
||||
- name: web_tests-4-linux
|
||||
@ -189,7 +201,10 @@ task:
|
||||
# As of October 2019, the Web shards needed more than 6G of RAM.
|
||||
CPU: 2
|
||||
MEMORY: 8G
|
||||
GOLDCTL: "$CIRRUS_WORKING_DIR/depot_tools/goldctl"
|
||||
GOLD_SERVICE_ACCOUNT: ENCRYPTED[3afeea5ac7201151c3d0dc9648862f0462b5e4f55dc600ca8b692319622f7c3eda3d577b1b16cc2ef0311b7314c1c095]
|
||||
script:
|
||||
- ./dev/bots/download_goldctl.sh
|
||||
- dart --enable-asserts ./dev/bots/test.dart
|
||||
|
||||
- name: web_tests-5-linux
|
||||
@ -198,7 +213,10 @@ task:
|
||||
# As of October 2019, the Web shards needed more than 6G of RAM.
|
||||
CPU: 2
|
||||
MEMORY: 8G
|
||||
GOLDCTL: "$CIRRUS_WORKING_DIR/depot_tools/goldctl"
|
||||
GOLD_SERVICE_ACCOUNT: ENCRYPTED[3afeea5ac7201151c3d0dc9648862f0462b5e4f55dc600ca8b692319622f7c3eda3d577b1b16cc2ef0311b7314c1c095]
|
||||
script:
|
||||
- ./dev/bots/download_goldctl.sh
|
||||
- dart --enable-asserts ./dev/bots/test.dart
|
||||
|
||||
- name: web_tests-6-linux
|
||||
@ -207,7 +225,10 @@ task:
|
||||
# As of October 2019, the Web shards needed more than 6G of RAM.
|
||||
CPU: 2
|
||||
MEMORY: 8G
|
||||
GOLDCTL: "$CIRRUS_WORKING_DIR/depot_tools/goldctl"
|
||||
GOLD_SERVICE_ACCOUNT: ENCRYPTED[3afeea5ac7201151c3d0dc9648862f0462b5e4f55dc600ca8b692319622f7c3eda3d577b1b16cc2ef0311b7314c1c095]
|
||||
script:
|
||||
- ./dev/bots/download_goldctl.sh
|
||||
- dart --enable-asserts ./dev/bots/test.dart
|
||||
|
||||
- name: web_tests-7_last-linux # last Web shard must end with _last
|
||||
@ -216,7 +237,10 @@ task:
|
||||
# As of October 2019, the Web shards needed more than 6G of RAM.
|
||||
CPU: 2
|
||||
MEMORY: 8G
|
||||
GOLDCTL: "$CIRRUS_WORKING_DIR/depot_tools/goldctl"
|
||||
GOLD_SERVICE_ACCOUNT: ENCRYPTED[3afeea5ac7201151c3d0dc9648862f0462b5e4f55dc600ca8b692319622f7c3eda3d577b1b16cc2ef0311b7314c1c095]
|
||||
script:
|
||||
- ./dev/bots/download_goldctl.sh
|
||||
- dart --enable-asserts ./dev/bots/test.dart
|
||||
|
||||
- name: build_tests-linux
|
||||
|
@ -68,7 +68,6 @@ const List<String> kWebTestFileBlacklist = <String>[
|
||||
'test/widgets/selectable_text_test.dart',
|
||||
'test/widgets/color_filter_test.dart',
|
||||
'test/widgets/editable_text_cursor_test.dart',
|
||||
'test/widgets/shadow_test.dart',
|
||||
'test/widgets/raw_keyboard_listener_test.dart',
|
||||
'test/widgets/editable_text_test.dart',
|
||||
'test/widgets/widget_inspector_test.dart',
|
||||
|
@ -33,7 +33,7 @@ void main() {
|
||||
matchesGoldenFile('shadow.BoxDecoration.enabled.png'),
|
||||
);
|
||||
debugDisableShadows = true;
|
||||
}, skip: isBrowser);
|
||||
});
|
||||
|
||||
testWidgets('Shadows on ShapeDecoration', (WidgetTester tester) async {
|
||||
debugDisableShadows = false;
|
||||
@ -93,7 +93,7 @@ void main() {
|
||||
matchesGoldenFile('shadow.PhysicalModel.enabled.png'),
|
||||
);
|
||||
debugDisableShadows = true;
|
||||
}, skip: isBrowser);
|
||||
});
|
||||
|
||||
testWidgets('Shadows with PhysicalShape', (WidgetTester tester) async {
|
||||
debugDisableShadows = false;
|
||||
|
@ -19,6 +19,7 @@ import 'package:process/process.dart';
|
||||
const String _kFlutterRootKey = 'FLUTTER_ROOT';
|
||||
const String _kGoldctlKey = 'GOLDCTL';
|
||||
const String _kServiceAccountKey = 'GOLD_SERVICE_ACCOUNT';
|
||||
const String _kTestBrowserKey = 'FLUTTER_TEST_BROWSER';
|
||||
|
||||
/// A client for uploading image tests and making baseline requests to the
|
||||
/// Flutter Gold Dashboard.
|
||||
@ -408,14 +409,16 @@ class SkiaGoldClient {
|
||||
/// Returns a JSON String with keys value pairs used to uniquely identify the
|
||||
/// configuration that generated the given golden file.
|
||||
///
|
||||
/// Currently, the only key value pair being tracked is the platform the image
|
||||
/// was rendered on.
|
||||
/// Currently, the only key value pairs being tracked is the platform the
|
||||
/// image was rendered on, and for web tests, the browser the image was
|
||||
/// rendered on.
|
||||
String _getKeysJSON() {
|
||||
return json.encode(
|
||||
<String, dynamic>{
|
||||
'Platform' : platform.operatingSystem,
|
||||
}
|
||||
);
|
||||
final Map<String, dynamic> keys = <String, dynamic>{
|
||||
'Platform' : platform.operatingSystem,
|
||||
};
|
||||
if (platform.environment[_kTestBrowserKey] != null)
|
||||
keys['Browser'] = platform.environment[_kTestBrowserKey];
|
||||
return json.encode(keys);
|
||||
}
|
||||
|
||||
/// Removes the file extension from the [fileName] to represent the test name
|
||||
@ -455,7 +458,7 @@ class SkiaGoldDigest {
|
||||
return SkiaGoldDigest(
|
||||
imageHash: json['digest'] as String,
|
||||
paramSet: Map<String, dynamic>.from(json['paramset'] as Map<String, dynamic> ??
|
||||
<String, String>{'Platform': 'none'}),
|
||||
<String, List<String>>{'Platform': <String>[]}),
|
||||
testName: json['test'] as String,
|
||||
status: json['status'] as String,
|
||||
);
|
||||
@ -477,6 +480,8 @@ class SkiaGoldDigest {
|
||||
bool isValid(Platform platform, String name, String expectation) {
|
||||
return imageHash == expectation
|
||||
&& (paramSet['Platform'] as List<dynamic>).contains(platform.operatingSystem)
|
||||
&& (platform.environment[_kTestBrowserKey] == null
|
||||
|| paramSet['Browser'] == platform.environment[_kTestBrowserKey])
|
||||
&& testName == name
|
||||
&& status == 'positive';
|
||||
}
|
||||
|
@ -6,7 +6,9 @@ import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math' as math;
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/widgets.dart' show Element;
|
||||
import 'package:image/image.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
// ignore: deprecated_member_use
|
||||
@ -240,3 +242,16 @@ ComparisonResult compareLists(List<int> test, List<int> master) {
|
||||
}
|
||||
return ComparisonResult(passed: true);
|
||||
}
|
||||
|
||||
/// An unsupported [WebGoldenComparator] that exists for API compatibility.
|
||||
class DefaultWebGoldenComparator extends WebGoldenComparator {
|
||||
@override
|
||||
Future<bool> compare(Element element, Size size, Uri golden) {
|
||||
throw UnsupportedError('DefaultWebGoldenComparator is only supported on the web.');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> update(Uri golden, Element element, Size size) {
|
||||
throw UnsupportedError('DefaultWebGoldenComparator is only supported on the web.');
|
||||
}
|
||||
}
|
||||
|
@ -2,11 +2,19 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:typed_data';
|
||||
import 'dart:convert';
|
||||
import 'dart:html' as html;
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'goldens.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
// ignore: deprecated_member_use
|
||||
import 'package:test_api/test_api.dart' as test_package show TestFailure;
|
||||
|
||||
/// An unsupported [GoldenFileComparator] that exists for API compatibility.
|
||||
import 'goldens.dart';
|
||||
|
||||
/// An unsupported [GoldenFileComparator] that exists for API compatibility.
|
||||
class LocalFileComparator extends GoldenFileComparator {
|
||||
@override
|
||||
Future<bool> compare(Uint8List imageBytes, Uri golden) {
|
||||
@ -19,10 +27,63 @@ class LocalFileComparator extends GoldenFileComparator {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether [test] and [master] are pixel by pixel identical.
|
||||
/// Returns whether [test] and [master] are pixel by pixel identical.
|
||||
///
|
||||
/// This method is not supported on the web and throws an [UnsupportedError]
|
||||
/// when called.
|
||||
ComparisonResult compareLists(List<int> test, List<int> master) {
|
||||
throw UnsupportedError('Golden testing is not supported on the web.');
|
||||
}
|
||||
|
||||
/// The default [WebGoldenComparator] implementation for `flutter test`.
|
||||
///
|
||||
/// This comparator will send a request to the test server for golden comparison
|
||||
/// which will then defer the comparison to [goldenFileComparator].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [matchesGoldenFile], the function from [flutter_test] that invokes the
|
||||
/// comparator.
|
||||
class DefaultWebGoldenComparator extends WebGoldenComparator {
|
||||
/// Creates a new [DefaultWebGoldenComparator] for the specified [testFile].
|
||||
///
|
||||
/// Golden file keys will be interpreted as file paths relative to the
|
||||
/// directory in which [testFile] resides.
|
||||
///
|
||||
/// The [testFile] URL must represent a file.
|
||||
DefaultWebGoldenComparator(this.testUri);
|
||||
|
||||
/// The test file currently being executed.
|
||||
///
|
||||
/// Golden file keys will be interpreted as file paths relative to the
|
||||
/// directory in which this file resides.
|
||||
Uri testUri;
|
||||
|
||||
@override
|
||||
Future<bool> compare(Element element, Size size, Uri golden) async {
|
||||
final String key = golden.toString();
|
||||
|
||||
final html.HttpRequest request = await html.HttpRequest.request(
|
||||
'flutter_goldens',
|
||||
method: 'POST',
|
||||
sendData: json.encode(<String, Object>{
|
||||
'testUri': testUri.toString(),
|
||||
'key': key.toString(),
|
||||
'width': size.width.round(),
|
||||
'height': size.height.round(),
|
||||
}),
|
||||
);
|
||||
final String response = request.response as String;
|
||||
if (response == 'true') {
|
||||
return true;
|
||||
} else {
|
||||
throw test_package.TestFailure(response);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> update(Uri golden, Element element, Size size) async {
|
||||
// Update is handled on the server side, just use the same logic here
|
||||
await compare(element, size, golden);
|
||||
}
|
||||
}
|
||||
|
95
packages/flutter_test/lib/src/_matchers_io.dart
Normal file
95
packages/flutter_test/lib/src/_matchers_io.dart
Normal file
@ -0,0 +1,95 @@
|
||||
// Copyright 2014 The Flutter 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:async';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:test_api/src/frontend/async_matcher.dart'; // ignore: implementation_imports
|
||||
// ignore: deprecated_member_use
|
||||
import 'package:test_api/test_api.dart' hide TypeMatcher, isInstanceOf;
|
||||
|
||||
import 'binding.dart';
|
||||
import 'finders.dart';
|
||||
import 'goldens.dart';
|
||||
|
||||
/// Render the closest [RepaintBoundary] of the [element] into an image.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [OffsetLayer.toImage] which is the actual method being called.
|
||||
Future<ui.Image> captureImage(Element element) {
|
||||
RenderObject renderObject = element.renderObject;
|
||||
while (!renderObject.isRepaintBoundary) {
|
||||
renderObject = renderObject.parent as RenderObject;
|
||||
assert(renderObject != null);
|
||||
}
|
||||
assert(!renderObject.debugNeedsPaint);
|
||||
final OffsetLayer layer = renderObject.debugLayer as OffsetLayer;
|
||||
return layer.toImage(renderObject.paintBounds);
|
||||
}
|
||||
|
||||
/// The matcher created by [matchesGoldenFile]. This class is enabled when the
|
||||
/// test is running on a VM using conditional import.
|
||||
class MatchesGoldenFile extends AsyncMatcher {
|
||||
/// Creates an instance of [MatchesGoldenFile]. Called by [matchesGoldenFile].
|
||||
const MatchesGoldenFile(this.key, this.version);
|
||||
|
||||
/// Creates an instance of [MatchesGoldenFile]. Called by [matchesGoldenFile].
|
||||
MatchesGoldenFile.forStringPath(String path, this.version) : key = Uri.parse(path);
|
||||
|
||||
/// The [key] to the golden image.
|
||||
final Uri key;
|
||||
|
||||
/// The [version] of the golden image.
|
||||
final int version;
|
||||
|
||||
@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 as Finder;
|
||||
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 Uri testNameUri = goldenFileComparator.getTestUri(key, version);
|
||||
|
||||
final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized() as TestWidgetsFlutterBinding;
|
||||
return binding.runAsync<String>(() async {
|
||||
final ui.Image image = await imageFuture;
|
||||
final ByteData bytes = await image.toByteData(format: ui.ImageByteFormat.png);
|
||||
if (bytes == null)
|
||||
return 'could not encode screenshot.';
|
||||
if (autoUpdateGoldenFiles) {
|
||||
await goldenFileComparator.update(testNameUri, bytes.buffer.asUint8List());
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
final bool success = await goldenFileComparator.compare(bytes.buffer.asUint8List(), testNameUri);
|
||||
return success ? null : 'does not match';
|
||||
} on TestFailure catch (ex) {
|
||||
return ex.message;
|
||||
}
|
||||
}, additionalTime: const Duration(minutes: 1));
|
||||
}
|
||||
|
||||
@override
|
||||
Description describe(Description description) {
|
||||
final Uri testNameUri = goldenFileComparator.getTestUri(key, version);
|
||||
return description.add('one widget whose rasterized image matches golden image "$testNameUri"');
|
||||
}
|
||||
}
|
105
packages/flutter_test/lib/src/_matchers_web.dart
Normal file
105
packages/flutter_test/lib/src/_matchers_web.dart
Normal file
@ -0,0 +1,105 @@
|
||||
// Copyright 2014 The Flutter 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:async';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:test_api/src/frontend/async_matcher.dart'; // ignore: implementation_imports
|
||||
// ignore: deprecated_member_use
|
||||
import 'package:test_api/test_api.dart' hide TypeMatcher, isInstanceOf;
|
||||
|
||||
import 'binding.dart';
|
||||
import 'finders.dart';
|
||||
import 'goldens.dart';
|
||||
|
||||
/// An unsupported method that exists for API compatibility.
|
||||
Future<ui.Image> captureImage(Element element) {
|
||||
throw UnsupportedError('captureImage is not supported on the web.');
|
||||
}
|
||||
|
||||
/// The matcher created by [matchesGoldenFile]. This class is enabled when the
|
||||
/// test is running in a web browser using conditional import.
|
||||
class MatchesGoldenFile extends AsyncMatcher {
|
||||
/// Creates an instance of [MatchesGoldenFile]. Called by [matchesGoldenFile].
|
||||
const MatchesGoldenFile(this.key, this.version);
|
||||
|
||||
/// Creates an instance of [MatchesGoldenFile]. Called by [matchesGoldenFile].
|
||||
MatchesGoldenFile.forStringPath(String path, this.version) : key = Uri.parse(path);
|
||||
|
||||
/// The [key] to the golden image.
|
||||
final Uri key;
|
||||
|
||||
/// The [version] of the golden image.
|
||||
final int version;
|
||||
|
||||
@override
|
||||
Future<String> matchAsync(dynamic item) async {
|
||||
if (item is! Finder) {
|
||||
return 'web goldens only supports matching finders.';
|
||||
}
|
||||
final Finder finder = item as Finder;
|
||||
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';
|
||||
}
|
||||
final Element element = elements.single;
|
||||
final RenderObject renderObject = _findRepaintBoundary(element);
|
||||
final Size size = renderObject.paintBounds.size;
|
||||
final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized() as TestWidgetsFlutterBinding;
|
||||
final Element e = binding.renderViewElement;
|
||||
|
||||
// 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(binding.window, renderObject);
|
||||
final String result = await binding.runAsync<String>(() async {
|
||||
if (autoUpdateGoldenFiles) {
|
||||
await webGoldenComparator.update(key, element, size);
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
final bool success = await webGoldenComparator.compare(element, size, key);
|
||||
return success ? null : 'does not match';
|
||||
} on TestFailure catch (ex) {
|
||||
return ex.message;
|
||||
}
|
||||
}, additionalTime: const Duration(seconds: 11));
|
||||
_renderElement(binding.window, _findRepaintBoundary(e));
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
Description describe(Description description) {
|
||||
final Uri testNameUri = webGoldenComparator.getTestUri(key, version);
|
||||
return description.add('one widget whose rasterized image matches golden image "$testNameUri"');
|
||||
}
|
||||
}
|
||||
|
||||
RenderObject _findRepaintBoundary(Element element) {
|
||||
RenderObject renderObject = element.renderObject;
|
||||
while (!renderObject.isRepaintBoundary) {
|
||||
renderObject = renderObject.parent as RenderObject;
|
||||
assert(renderObject != null);
|
||||
}
|
||||
return renderObject;
|
||||
}
|
||||
|
||||
void _renderElement(ui.Window window, RenderObject renderObject) {
|
||||
final Layer layer = renderObject.debugLayer;
|
||||
final ui.SceneBuilder sceneBuilder = ui.SceneBuilder();
|
||||
if (layer is OffsetLayer) {
|
||||
sceneBuilder.pushOffset(-layer.offset.dx, -layer.offset.dy);
|
||||
}
|
||||
// ignore: invalid_use_of_visible_for_testing_member, invalid_use_of_protected_member
|
||||
layer.updateSubtreeNeedsAddToScene();
|
||||
// ignore: invalid_use_of_protected_member
|
||||
layer.addToScene(sceneBuilder);
|
||||
sceneBuilder.pop();
|
||||
window.render(sceneBuilder.build());
|
||||
}
|
@ -6,6 +6,7 @@ import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import '_goldens_io.dart' if (dart.library.html) '_goldens_web.dart' as _goldens;
|
||||
|
||||
@ -134,6 +135,115 @@ set goldenFileComparator(GoldenFileComparator value) {
|
||||
_goldenFileComparator = value;
|
||||
}
|
||||
|
||||
/// Compares image pixels against a golden image file.
|
||||
///
|
||||
/// Instances of this comparator will be used as the backend for
|
||||
/// [matchesGoldenFile] when tests are running on Flutter Web, and will usually
|
||||
/// implemented by deferring the screenshot taking and image comparison to a
|
||||
/// test server.
|
||||
///
|
||||
/// Instances of this comparator will be invoked by the test framework in the
|
||||
/// [TestWidgetsFlutterBinding.runAsync] zone and are thus not subject to the
|
||||
/// fake async constraints that are normally imposed on widget tests (i.e. the
|
||||
/// need or the ability to call [WidgetTester.pump] to advance the microtask
|
||||
/// queue). Prior to the invocation, the test framework will render only the
|
||||
/// [Element] to be compared on the screen.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [GoldenFileComparator] for the comparator to be used when the test is
|
||||
/// not running in a web browser.
|
||||
/// * [DefaultWebGoldenComparator] for the default [WebGoldenComparator]
|
||||
/// implementation for `flutter test`.
|
||||
/// * [matchesGoldenFile], the function from [flutter_test] that invokes the
|
||||
/// comparator.
|
||||
abstract class WebGoldenComparator {
|
||||
/// Compares the rendered pixels of [element] of size [size] that is being
|
||||
/// rendered on the top left of the screen 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> compare(Element element, Size size, Uri golden);
|
||||
|
||||
/// Updates the golden file identified by [golden] with rendered pixels of
|
||||
/// [element].
|
||||
///
|
||||
/// This will be invoked in lieu of [compare] 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> update(Uri golden, Element element, Size size);
|
||||
|
||||
/// Returns a new golden file [Uri] to incorporate any [version] number with
|
||||
/// the [key].
|
||||
///
|
||||
/// The [version] is an optional int that can be used to differentiate
|
||||
/// historical golden files.
|
||||
///
|
||||
/// Version numbers are used in golden file tests for package:flutter. You can
|
||||
/// learn more about these tests [here](https://github.com/flutter/flutter/wiki/Writing-a-golden-file-test-for-package:flutter).
|
||||
Uri getTestUri(Uri key, int version) {
|
||||
if (version == null)
|
||||
return key;
|
||||
final String keyString = key.toString();
|
||||
final String extension = path.extension(keyString);
|
||||
return Uri.parse(
|
||||
keyString
|
||||
.split(extension)
|
||||
.join() + '.' + version.toString() + extension
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Compares pixels against those of a golden image file.
|
||||
///
|
||||
/// This comparator is used as the backend for [matchesGoldenFile] when tests
|
||||
/// are running in a web browser.
|
||||
///
|
||||
/// When using `flutter test --platform=chrome`, a comparator implemented by
|
||||
/// [DefaultWebGoldenComparator] is used if no other comparator is specified. It
|
||||
/// will send a request to the test server, which uses [goldenFileComparator]
|
||||
/// for golden file compatison.
|
||||
///
|
||||
/// When using `flutter test --update-goldens`, the [DefaultWebGoldenComparator]
|
||||
/// updates the files on disk to match the rendering.
|
||||
///
|
||||
/// When using `flutter run`, the default comparator
|
||||
/// ([_TrivialWebGoldenComparator]) is used. It prints a message to the console
|
||||
/// but otherwise does nothing. This allows tests to be developed visually on a
|
||||
/// web browser.
|
||||
///
|
||||
/// Callers may choose to override the default comparator by setting this to a
|
||||
/// custom comparator during test set-up (or using directory-level test
|
||||
/// configuration). For example, some projects may wish to install a comparator
|
||||
/// with tolerance levels for allowable differences.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [flutter_test] for more information about how to configure tests at the
|
||||
/// directory-level.
|
||||
/// * [goldenFileComparator], the comparator used when tests are not running on
|
||||
/// a web browser.
|
||||
WebGoldenComparator get webGoldenComparator => _webGoldenComparator;
|
||||
WebGoldenComparator _webGoldenComparator = const _TrivialWebGoldenComparator._();
|
||||
set webGoldenComparator(WebGoldenComparator value) {
|
||||
assert(value != null);
|
||||
_webGoldenComparator = value;
|
||||
}
|
||||
|
||||
/// Whether golden files should be automatically updated during tests rather
|
||||
/// than compared to the image bytes recorded by the tests.
|
||||
///
|
||||
@ -185,6 +295,26 @@ class TrivialComparator implements GoldenFileComparator {
|
||||
}
|
||||
}
|
||||
|
||||
class _TrivialWebGoldenComparator implements WebGoldenComparator {
|
||||
const _TrivialWebGoldenComparator._();
|
||||
|
||||
@override
|
||||
Future<bool> compare(Element element, Size size, Uri golden) {
|
||||
debugPrint('Golden comparison requested for "$golden"; skipping...');
|
||||
return Future<bool>.value(true);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> update(Uri golden, Element element, Size size) {
|
||||
throw StateError('webGoldenComparator has not been initialized');
|
||||
}
|
||||
|
||||
@override
|
||||
Uri getTestUri(Uri key, int version) {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of a pixel comparison test.
|
||||
///
|
||||
/// The [ComparisonResult] will always indicate if a test has [passed]. The
|
||||
|
@ -20,6 +20,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '_matchers_io.dart' if (dart.library.html) '_matchers_web.dart' show MatchesGoldenFile, captureImage;
|
||||
import 'accessibility.dart';
|
||||
import 'binding.dart';
|
||||
import 'finders.dart';
|
||||
@ -366,9 +367,9 @@ Matcher coversSameAreaAs(Path expectedPath, { @required Rect areaToCompare, int
|
||||
/// may swap out the backend for this matcher.
|
||||
AsyncMatcher matchesGoldenFile(dynamic key, {int version}) {
|
||||
if (key is Uri) {
|
||||
return _MatchesGoldenFile(key, version);
|
||||
return MatchesGoldenFile(key, version);
|
||||
} else if (key is String) {
|
||||
return _MatchesGoldenFile.forStringPath(key, version);
|
||||
return MatchesGoldenFile.forStringPath(key, version);
|
||||
}
|
||||
throw ArgumentError('Unexpected type for golden file: ${key.runtimeType}');
|
||||
}
|
||||
@ -1636,17 +1637,6 @@ class _ColorMatcher extends Matcher {
|
||||
Description describe(Description description) => description.add('matches color $targetColor');
|
||||
}
|
||||
|
||||
Future<ui.Image> _captureImage(Element element) {
|
||||
RenderObject renderObject = element.renderObject;
|
||||
while (!renderObject.isRepaintBoundary) {
|
||||
renderObject = renderObject.parent as RenderObject;
|
||||
assert(renderObject != null);
|
||||
}
|
||||
assert(!renderObject.debugNeedsPaint);
|
||||
final OffsetLayer layer = renderObject.debugLayer as OffsetLayer;
|
||||
return layer.toImage(renderObject.paintBounds);
|
||||
}
|
||||
|
||||
int _countDifferentPixels(Uint8List imageA, Uint8List imageB) {
|
||||
assert(imageA.length == imageB.length);
|
||||
int delta = 0;
|
||||
@ -1681,7 +1671,7 @@ class _MatchesReferenceImage extends AsyncMatcher {
|
||||
} else if (elements.length > 1) {
|
||||
return 'matched too many widgets';
|
||||
}
|
||||
imageFuture = _captureImage(elements.single);
|
||||
imageFuture = captureImage(elements.single);
|
||||
}
|
||||
|
||||
final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized() as TestWidgetsFlutterBinding;
|
||||
@ -1712,60 +1702,6 @@ class _MatchesReferenceImage extends AsyncMatcher {
|
||||
}
|
||||
}
|
||||
|
||||
class _MatchesGoldenFile extends AsyncMatcher {
|
||||
const _MatchesGoldenFile(this.key, this.version);
|
||||
|
||||
_MatchesGoldenFile.forStringPath(String path, this.version) : key = Uri.parse(path);
|
||||
|
||||
final Uri key;
|
||||
final int version;
|
||||
|
||||
@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 as Finder;
|
||||
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 Uri testNameUri = goldenFileComparator.getTestUri(key, version);
|
||||
|
||||
final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized() as TestWidgetsFlutterBinding;
|
||||
return binding.runAsync<String>(() async {
|
||||
final ui.Image image = await imageFuture;
|
||||
final ByteData bytes = await image.toByteData(format: ui.ImageByteFormat.png);
|
||||
if (bytes == null)
|
||||
return 'could not encode screenshot.';
|
||||
if (autoUpdateGoldenFiles) {
|
||||
await goldenFileComparator.update(testNameUri, bytes.buffer.asUint8List());
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
final bool success = await goldenFileComparator.compare(bytes.buffer.asUint8List(), testNameUri);
|
||||
return success ? null : 'does not match';
|
||||
} on TestFailure catch (ex) {
|
||||
return ex.message;
|
||||
}
|
||||
}, additionalTime: const Duration(minutes: 1));
|
||||
}
|
||||
|
||||
@override
|
||||
Description describe(Description description) {
|
||||
final Uri testNameUri = goldenFileComparator.getTestUri(key, version);
|
||||
return description.add('one widget whose rasterized image matches golden image "$testNameUri"');
|
||||
}
|
||||
}
|
||||
|
||||
class _MatchesSemanticsData extends Matcher {
|
||||
_MatchesSemanticsData({
|
||||
this.label,
|
||||
|
@ -255,6 +255,7 @@ class FlutterWebTestBootstrapBuilder implements Builder {
|
||||
final String assetPath = id.pathSegments.first == 'lib'
|
||||
? path.url.join('packages', id.package, id.path)
|
||||
: id.path;
|
||||
final Uri testUrl = path.toUri(path.absolute(assetPath));
|
||||
final Metadata metadata = parseMetadata(
|
||||
assetPath, contents, Runtime.builtIn.map((Runtime runtime) => runtime.name).toSet());
|
||||
|
||||
@ -265,6 +266,7 @@ import 'dart:html';
|
||||
import 'dart:js';
|
||||
|
||||
import 'package:stream_channel/stream_channel.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:test_api/src/backend/stack_trace_formatter.dart'; // ignore: implementation_imports
|
||||
import 'package:test_api/src/util/stack_trace_mapper.dart'; // ignore: implementation_imports
|
||||
import 'package:test_api/src/remote_listener.dart'; // ignore: implementation_imports
|
||||
@ -279,6 +281,7 @@ Future<void> main() async {
|
||||
// this stuff in.
|
||||
ui.debugEmulateFlutterTesterEnvironment = true;
|
||||
await ui.webOnlyInitializePlatform();
|
||||
webGoldenComparator = DefaultWebGoldenComparator(Uri.parse('$testUrl'));
|
||||
// TODO(flutterweb): remove need for dynamic cast.
|
||||
(ui.window as dynamic).debugOverrideDevicePixelRatio(3.0);
|
||||
(ui.window as dynamic).webOnlyDebugPhysicalSizeOverride = const ui.Size(2400, 1800);
|
||||
|
@ -29,6 +29,7 @@ import '../globals.dart';
|
||||
import '../project.dart';
|
||||
import '../vmservice.dart';
|
||||
import 'test_compiler.dart';
|
||||
import 'test_config.dart';
|
||||
import 'watcher.dart';
|
||||
|
||||
/// The timeout we give the test process to connect to the test harness
|
||||
@ -55,14 +56,6 @@ const Duration _kTestProcessTimeout = Duration(minutes: 5);
|
||||
/// hold that against the test.
|
||||
const String _kStartTimeoutTimerMessage = 'sky_shell test process has entered main method';
|
||||
|
||||
/// The name of the test configuration file that will be discovered by the
|
||||
/// test harness if it exists in the project directory hierarchy.
|
||||
const String _kTestConfigFileName = 'flutter_test_config.dart';
|
||||
|
||||
/// The name of the file that signals the root of the project and that will
|
||||
/// cause the test harness to stop scanning for configuration files.
|
||||
const String _kProjectRootSentinel = 'pubspec.yaml';
|
||||
|
||||
/// The address at which our WebSocket server resides and at which the sky_shell
|
||||
/// processes will host the Observatory server.
|
||||
final Map<InternetAddressType, InternetAddress> _kHosts = <InternetAddressType, InternetAddress>{
|
||||
@ -743,25 +736,9 @@ class FlutterPlatform extends PlatformPlugin {
|
||||
Uri testUrl,
|
||||
}) {
|
||||
assert(testUrl.scheme == 'file');
|
||||
File testConfigFile;
|
||||
Directory directory = fs.file(testUrl).parent;
|
||||
while (directory.path != directory.parent.path) {
|
||||
final File configFile = directory.childFile(_kTestConfigFileName);
|
||||
if (configFile.existsSync()) {
|
||||
printTrace('Discovered $_kTestConfigFileName in ${directory.path}');
|
||||
testConfigFile = configFile;
|
||||
break;
|
||||
}
|
||||
if (directory.childFile(_kProjectRootSentinel).existsSync()) {
|
||||
printTrace('Stopping scan for $_kTestConfigFileName; '
|
||||
'found project root at ${directory.path}');
|
||||
break;
|
||||
}
|
||||
directory = directory.parent;
|
||||
}
|
||||
return generateTestBootstrap(
|
||||
testUrl: testUrl,
|
||||
testConfigFile: testConfigFile,
|
||||
testConfigFile: findTestConfigFile(fs.file(testUrl)),
|
||||
host: host,
|
||||
updateGoldens: updateGoldens,
|
||||
);
|
||||
|
@ -5,6 +5,7 @@
|
||||
// ignore_for_file: implementation_imports
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:async/async.dart';
|
||||
import 'package:http_multi_server/http_multi_server.dart';
|
||||
@ -27,18 +28,30 @@ import 'package:test_core/src/runner/plugin/platform_helpers.dart';
|
||||
import 'package:test_core/src/runner/runner_suite.dart';
|
||||
import 'package:test_core/src/runner/suite.dart';
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart' hide StackTrace;
|
||||
|
||||
import '../artifacts.dart';
|
||||
import '../base/common.dart';
|
||||
import '../base/file_system.dart';
|
||||
import '../base/io.dart';
|
||||
import '../base/process_manager.dart';
|
||||
import '../build_info.dart';
|
||||
import '../cache.dart';
|
||||
import '../convert.dart';
|
||||
import '../dart/package_map.dart';
|
||||
import '../globals.dart';
|
||||
import '../project.dart';
|
||||
import '../web/chrome.dart';
|
||||
|
||||
import 'test_compiler.dart';
|
||||
import 'test_config.dart';
|
||||
|
||||
class FlutterWebPlatform extends PlatformPlugin {
|
||||
FlutterWebPlatform._(this._server, this._config, this._root) {
|
||||
FlutterWebPlatform._(this._server, this._config, this._root, {
|
||||
FlutterProject flutterProject,
|
||||
String shellPath,
|
||||
this.updateGoldens,
|
||||
}) {
|
||||
// Look up the location of the testing resources.
|
||||
final Map<String, Uri> packageMap = PackageMap(fs.path.join(
|
||||
Cache.flutterRoot,
|
||||
@ -58,17 +71,30 @@ class FlutterWebPlatform extends PlatformPlugin {
|
||||
.add(createStaticHandler(_config.suiteDefaults.precompiledPath,
|
||||
serveFilesOutsidePath: true))
|
||||
.add(_handleStaticArtifact)
|
||||
.add(_goldenFileHandler)
|
||||
.add(_wrapperHandler);
|
||||
_server.mount(cascade.handler);
|
||||
|
||||
_testGoldenComparator = TestGoldenComparator(
|
||||
shellPath,
|
||||
() => TestCompiler(BuildMode.debug, false, flutterProject),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<FlutterWebPlatform> start(String root) async {
|
||||
static Future<FlutterWebPlatform> start(String root, {
|
||||
FlutterProject flutterProject,
|
||||
String shellPath,
|
||||
bool updateGoldens = false,
|
||||
}) async {
|
||||
final shelf_io.IOServer server =
|
||||
shelf_io.IOServer(await HttpMultiServer.loopback(0));
|
||||
return FlutterWebPlatform._(
|
||||
server,
|
||||
Configuration.current,
|
||||
root,
|
||||
flutterProject: flutterProject,
|
||||
shellPath: shellPath,
|
||||
updateGoldens: updateGoldens,
|
||||
);
|
||||
}
|
||||
|
||||
@ -167,6 +193,59 @@ class FlutterWebPlatform extends PlatformPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
final bool updateGoldens;
|
||||
TestGoldenComparator _testGoldenComparator;
|
||||
|
||||
Future<shelf.Response> _goldenFileHandler(shelf.Request request) async {
|
||||
if (request.url.path.contains('flutter_goldens')) {
|
||||
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;
|
||||
Uint8List bytes;
|
||||
|
||||
try {
|
||||
final Runtime browser = Runtime.chrome;
|
||||
final BrowserManager browserManager = await _browserManagerFor(browser);
|
||||
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) {
|
||||
printError('Caught WIPError: $ex');
|
||||
return shelf.Response.ok('WIP error: $ex');
|
||||
} on FormatException catch (ex) {
|
||||
printError('Caught FormatException: $ex');
|
||||
return shelf.Response.ok('Caught exception: $ex');
|
||||
}
|
||||
|
||||
if (bytes == null) {
|
||||
return shelf.Response.ok('Unknown error, bytes is null');
|
||||
}
|
||||
|
||||
final String errorMessage = await _testGoldenComparator.compareGoldens(testUri, bytes, goldenKey, updateGoldens);
|
||||
return shelf.Response.ok(errorMessage ?? 'true');
|
||||
} else {
|
||||
return shelf.Response.notFound('Not Found');
|
||||
}
|
||||
}
|
||||
|
||||
final OneOffHandler _webSocketHandler = OneOffHandler();
|
||||
final PathHandler _jsHandler = PathHandler();
|
||||
final AsyncMemoizer<void> _closeMemo = AsyncMemoizer<void>();
|
||||
@ -296,6 +375,7 @@ class FlutterWebPlatform extends PlatformPlugin {
|
||||
})
|
||||
.toList();
|
||||
futures.add(_server.close());
|
||||
futures.add(_testGoldenComparator.close());
|
||||
await Future.wait<void>(futures);
|
||||
});
|
||||
}
|
||||
@ -702,3 +782,182 @@ class _BrowserEnvironment implements Environment {
|
||||
@override
|
||||
CancelableOperation<dynamic> displayPause() => _manager._displayPause();
|
||||
}
|
||||
|
||||
/// Helper class to start golden file comparison in a separate process.
|
||||
///
|
||||
/// Golden file comparator is configured using flutter_test_config.dart and that
|
||||
/// file can contain arbitrary Dart code that depends on dart:ui. Thus it has to
|
||||
/// be executed in a `flutter_tester` environment. This helper class generates a
|
||||
/// Dart file configured with flutter_test_config.dart to perform the comparison
|
||||
/// of golden files.
|
||||
class TestGoldenComparator {
|
||||
/// Creates a [TestGoldenComparator] instance.
|
||||
TestGoldenComparator(this.shellPath, this.compilerFactory)
|
||||
: tempDir = fs.systemTempDirectory.createTempSync('flutter_web_platform.');
|
||||
|
||||
final String shellPath;
|
||||
final Directory tempDir;
|
||||
final TestCompiler Function() compilerFactory;
|
||||
|
||||
TestCompiler _compiler;
|
||||
TestGoldenComparatorProcess _previousComparator;
|
||||
Uri _previousTestUri;
|
||||
|
||||
Future<void> close() async {
|
||||
tempDir.deleteSync(recursive: true);
|
||||
await _compiler?.dispose();
|
||||
await _previousComparator?.close();
|
||||
}
|
||||
|
||||
/// Start golden comparator in a separate process. Start one file per test file
|
||||
/// to reduce the overhead of starting `flutter_tester`.
|
||||
Future<TestGoldenComparatorProcess> _processForTestFile(Uri testUri) async {
|
||||
if (testUri == _previousTestUri) {
|
||||
return _previousComparator;
|
||||
}
|
||||
|
||||
final String bootstrap = TestGoldenComparatorProcess.generateBootstrap(testUri);
|
||||
final Process process = await _startProcess(bootstrap);
|
||||
unawaited(_previousComparator?.close());
|
||||
_previousComparator = TestGoldenComparatorProcess(process);
|
||||
_previousTestUri = testUri;
|
||||
|
||||
return _previousComparator;
|
||||
}
|
||||
|
||||
Future<Process> _startProcess(String testBootstrap) async {
|
||||
// Prepare the Dart file that will talk to us and start the test.
|
||||
final File listenerFile = (await tempDir.createTemp('listener')).childFile('listener.dart');
|
||||
await listenerFile.writeAsString(testBootstrap);
|
||||
|
||||
// Lazily create the compiler
|
||||
_compiler = _compiler ?? compilerFactory();
|
||||
final String output = await _compiler.compile(listenerFile.path);
|
||||
final List<String> command = <String>[
|
||||
shellPath,
|
||||
'--disable-observatory',
|
||||
'--non-interactive',
|
||||
'--packages=${PackageMap.globalPackagesPath}',
|
||||
output,
|
||||
];
|
||||
|
||||
final Map<String, String> environment = <String, String>{
|
||||
// Chrome is the only supported browser currently.
|
||||
'FLUTTER_TEST_BROWSER': 'chrome',
|
||||
};
|
||||
return processManager.start(command, environment: environment);
|
||||
}
|
||||
|
||||
Future<String> compareGoldens(Uri testUri, Uint8List bytes, Uri goldenKey, bool updateGoldens) async {
|
||||
final File imageFile = await (await tempDir.createTemp('image')).childFile('image').writeAsBytes(bytes);
|
||||
|
||||
final TestGoldenComparatorProcess process = await _processForTestFile(testUri);
|
||||
process.sendCommand(imageFile, goldenKey, updateGoldens);
|
||||
|
||||
final Map<String, dynamic> result = await process.getResponse().timeout(const Duration(seconds: 10));
|
||||
|
||||
if (result == null) {
|
||||
return 'unknown error';
|
||||
} else {
|
||||
return (result['success'] as bool) ? null : ((result['message'] as String) ?? 'does not match');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a `flutter_tester` process started for golden comparison. Also
|
||||
/// handles communication with the child process.
|
||||
class TestGoldenComparatorProcess {
|
||||
/// Creates a [TestGoldenComparatorProcess] backed by [process].
|
||||
TestGoldenComparatorProcess(this.process) {
|
||||
// Pipe stdout and stderr to printTrace and printError.
|
||||
// Also parse stdout as a stream of JSON objects.
|
||||
streamIterator = StreamIterator<Map<String, dynamic>>(
|
||||
process.stdout
|
||||
.transform<String>(utf8.decoder)
|
||||
.transform<String>(const LineSplitter())
|
||||
.where((String line) {
|
||||
printTrace('<<< $line');
|
||||
return line.isNotEmpty && line[0] == '{';
|
||||
})
|
||||
.map<dynamic>(jsonDecode)
|
||||
.cast<Map<String, dynamic>>());
|
||||
|
||||
process.stderr
|
||||
.transform<String>(utf8.decoder)
|
||||
.transform<String>(const LineSplitter())
|
||||
.forEach((String line) {
|
||||
printError('<<< $line');
|
||||
});
|
||||
}
|
||||
|
||||
final Process process;
|
||||
StreamIterator<Map<String, dynamic>> streamIterator;
|
||||
|
||||
Future<void> close() async {
|
||||
await process.stdin.close();
|
||||
process.kill();
|
||||
}
|
||||
|
||||
void sendCommand(File imageFile, Uri goldenKey, bool updateGoldens) {
|
||||
final Object command = jsonEncode(<String, dynamic>{
|
||||
'imageFile': imageFile.path,
|
||||
'key': goldenKey.toString(),
|
||||
'update': updateGoldens,
|
||||
});
|
||||
printTrace('Preparing to send command: $command');
|
||||
process.stdin.writeln(command);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> getResponse() async {
|
||||
final bool available = await streamIterator.moveNext();
|
||||
assert(available);
|
||||
return streamIterator.current;
|
||||
}
|
||||
|
||||
static String generateBootstrap(Uri testUri) {
|
||||
final File testConfigFile = findTestConfigFile(fs.file(testUri));
|
||||
// Generate comparator process for the file.
|
||||
return '''
|
||||
import 'dart:convert'; // ignore: dart_convert_import
|
||||
import 'dart:io'; // ignore: dart_io_import
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
${testConfigFile != null ? "import '${Uri.file(testConfigFile.path)}' as test_config;" : ""}
|
||||
|
||||
void main() async {
|
||||
LocalFileComparator comparator = LocalFileComparator(Uri.parse('$testUri'));
|
||||
goldenFileComparator = comparator;
|
||||
|
||||
${testConfigFile != null ? 'test_config.main(() async {' : ''}
|
||||
final commands = stdin
|
||||
.transform<String>(utf8.decoder)
|
||||
.transform<String>(const LineSplitter())
|
||||
.map<Object>(jsonDecode);
|
||||
await for (Object command in commands) {
|
||||
if (command is Map<String, dynamic>) {
|
||||
File imageFile = File(command['imageFile']);
|
||||
Uri goldenKey = Uri.parse(command['key']);
|
||||
bool update = command['update'];
|
||||
|
||||
final bytes = await File(imageFile.path).readAsBytes();
|
||||
if (update) {
|
||||
await goldenFileComparator.update(goldenKey, bytes);
|
||||
print(jsonEncode({'success': true}));
|
||||
} else {
|
||||
try {
|
||||
bool success = await goldenFileComparator.compare(bytes, goldenKey);
|
||||
print(jsonEncode({'success': success}));
|
||||
} catch (ex) {
|
||||
print(jsonEncode({'success': false, 'message': '\$ex'}));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
print('object type is not right');
|
||||
}
|
||||
}
|
||||
${testConfigFile != null ? '});' : ''}
|
||||
}
|
||||
''';
|
||||
}
|
||||
}
|
||||
|
@ -48,6 +48,12 @@ Future<int> runTests(
|
||||
Directory coverageDirectory,
|
||||
bool web = false,
|
||||
}) async {
|
||||
// Configure package:test to use the Flutter engine for child processes.
|
||||
final String shellPath = artifacts.getArtifactPath(Artifact.flutterTester);
|
||||
if (!processManager.canRun(shellPath)) {
|
||||
throwToolExit('Cannot execute Flutter tester at $shellPath');
|
||||
}
|
||||
|
||||
// Compute the command-line arguments for package:test.
|
||||
final List<String> testArgs = <String>[
|
||||
if (!terminal.supportsColor)
|
||||
@ -86,7 +92,12 @@ Future<int> runTests(
|
||||
hack.registerPlatformPlugin(
|
||||
<Runtime>[Runtime.chrome],
|
||||
() {
|
||||
return FlutterWebPlatform.start(flutterProject.directory.path);
|
||||
return FlutterWebPlatform.start(
|
||||
flutterProject.directory.path,
|
||||
updateGoldens: updateGoldens,
|
||||
shellPath: shellPath,
|
||||
flutterProject: flutterProject,
|
||||
);
|
||||
},
|
||||
);
|
||||
await test.main(testArgs);
|
||||
@ -97,12 +108,6 @@ Future<int> runTests(
|
||||
..add('--')
|
||||
..addAll(testFiles);
|
||||
|
||||
// Configure package:test to use the Flutter engine for child processes.
|
||||
final String shellPath = artifacts.getArtifactPath(Artifact.flutterTester);
|
||||
if (!processManager.canRun(shellPath)) {
|
||||
throwToolExit('Cannot find Flutter shell at $shellPath');
|
||||
}
|
||||
|
||||
final InternetAddressType serverType =
|
||||
ipv6 ? InternetAddressType.IPv6 : InternetAddressType.IPv4;
|
||||
|
||||
|
35
packages/flutter_tools/lib/src/test/test_config.dart
Normal file
35
packages/flutter_tools/lib/src/test/test_config.dart
Normal file
@ -0,0 +1,35 @@
|
||||
// Copyright 2014 The Flutter 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 '../base/file_system.dart';
|
||||
import '../globals.dart';
|
||||
|
||||
/// The name of the test configuration file that will be discovered by the
|
||||
/// test harness if it exists in the project directory hierarchy.
|
||||
const String _kTestConfigFileName = 'flutter_test_config.dart';
|
||||
|
||||
/// The name of the file that signals the root of the project and that will
|
||||
/// cause the test harness to stop scanning for configuration files.
|
||||
const String _kProjectRootSentinel = 'pubspec.yaml';
|
||||
|
||||
/// Find the `flutter_test_config.dart` file for a specific test file.
|
||||
File findTestConfigFile(File testFile) {
|
||||
File testConfigFile;
|
||||
Directory directory = testFile.parent;
|
||||
while (directory.path != directory.parent.path) {
|
||||
final File configFile = directory.childFile(_kTestConfigFileName);
|
||||
if (configFile.existsSync()) {
|
||||
printTrace('Discovered $_kTestConfigFileName in ${directory.path}');
|
||||
testConfigFile = configFile;
|
||||
break;
|
||||
}
|
||||
if (directory.childFile(_kProjectRootSentinel).existsSync()) {
|
||||
printTrace('Stopping scan for $_kTestConfigFileName; '
|
||||
'found project root at ${directory.path}');
|
||||
break;
|
||||
}
|
||||
directory = directory.parent;
|
||||
}
|
||||
return testConfigFile;
|
||||
}
|
@ -137,6 +137,7 @@ class ChromeLauncher {
|
||||
'--no-default-browser-check',
|
||||
'--disable-default-apps',
|
||||
'--disable-translate',
|
||||
'--window-size=2400,1800',
|
||||
if (headless)
|
||||
...<String>['--headless', '--disable-gpu', '--no-sandbox'],
|
||||
url,
|
||||
@ -174,6 +175,7 @@ class ChromeLauncher {
|
||||
return _connect(Chrome._(
|
||||
port,
|
||||
ChromeConnection('localhost', port),
|
||||
url: url,
|
||||
process: process,
|
||||
remoteDebuggerUri: remoteDebuggerUri,
|
||||
), skipCheck);
|
||||
@ -225,10 +227,12 @@ class Chrome {
|
||||
Chrome._(
|
||||
this.debugPort,
|
||||
this.chromeConnection, {
|
||||
this.url,
|
||||
Process process,
|
||||
this.remoteDebuggerUri,
|
||||
}) : _process = process;
|
||||
|
||||
final String url;
|
||||
final int debugPort;
|
||||
final Process _process;
|
||||
final ChromeConnection chromeConnection;
|
||||
|
@ -4,23 +4,31 @@ Use of this source code is governed by a BSD-style license that can be
|
||||
found in the LICENSE file. -->
|
||||
<html>
|
||||
<head>
|
||||
<title>test Browser Host</title>
|
||||
<title>Flutter Test Browser Host</title>
|
||||
<style>
|
||||
/* Configure so that the test application takes up the whole screen */
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
iframe {
|
||||
border: none;
|
||||
width: 2400px;
|
||||
height: 1800px;
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<svg id="dart" version="1.1" x="0px" y="0px" width="400px" height="400px" viewBox="0 0 400 400">
|
||||
<path id="right-flank" fill="#0083C9" d="M249.379,226.486l-6.676,15.572L166.174,166h58.82c0,0,2.807-0.409,3.645,1.966L249.379,226.486z"/>
|
||||
<path id="right-ear" fill="#00D2B8" d="M201.84,141.906L166.174,166h58.82c0,0,2.168-0.25,2.645,0.566l-2.694-8.848l-15.024-14.68C207.555,140.329,203.578,140.744,201.84,141.906z"/>
|
||||
<path id="left-flank" fill="#00D2B8" d="M242.616,241.856l-15.022,6.799l-60.493-21.429c-1.035-0.395-1.101-3.696-1.101-3.696v-57.932L242.616,241.856z"/>
|
||||
<path id="left-paw" fill="#55DECA" d="M167.003,227.098l60.636,21.558l15.064-6.799L237.224,259h-43.856c0,0-14.077-13.929-18.141-17.993C171.162,236.943,169.162,233.989,167.003,227.098z"/>
|
||||
<path id="right-paw" fill="#00A4E4" d="M227.676,166.365c0.963,1.401,1.361,2.473,1.361,2.473l20.352,57.648l-6.711,15.37L259,236.463v-44.854c0,0-13.678-13.965-17.741-17.882C237.193,169.811,231.466,166.319,227.676,166.365z"/>
|
||||
<path id="left-ear" fill="#0083C9" d="M166.769,227.098c0,0-0.769-1.104-0.769-4.355v-57.144l-23.115,34.877c-1.626,1.774-1.567,6.538,1.595,9.755l13.636,13.892L166.769,227.098z"/>
|
||||
</svg>
|
||||
<div id="dark"></div>
|
||||
<svg id="play" version="1.1" x="0px" y="0px" width="80px" height="80px" viewBox="0 0 25 25">
|
||||
<defs><filter id="blur"><feGaussianBlur stdDeviation="0.3" id="feGaussianBlur5097" /></filter></defs>
|
||||
<path d="M 3.777014,1.3715789 A 1.1838119,1.1838119 0 0 0 2.693923,2.5488509 V 22.444746 a 1.1838119,1.1838119 0 0 0 1.765908,1.035999 l 17.235259,-9.95972 a 1.1838119,1.1838119 0 0 0 0,-2.071998 L 4.459831,1.5128519 A 1.1838119,1.1838119 0 0 0 3.777014,1.3715789 z" style="opacity:0.5;stroke:#000000;stroke-width:1;filter:url(#blur)" />
|
||||
<path style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.32722104" d="M 3.4770491,1.0714664 A 1.1838119,1.1838119 0 0 0 2.3939589,2.2487382 V 22.144633 a 1.1838119,1.1838119 0 0 0 1.7659079,1.035999 l 17.2352602,-9.95972 a 1.1838119,1.1838119 0 0 0 0,-2.071998 L 4.1598668,1.2127389 A 1.1838119,1.1838119 0 0 0 3.4770491,1.0714664 z" />
|
||||
</svg>
|
||||
<script src="host.dart.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -0,0 +1,113 @@
|
||||
// Copyright 2014 The Flutter 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:convert';
|
||||
|
||||
import 'package:flutter_tools/src/base/file_system.dart';
|
||||
import 'package:flutter_tools/src/test/flutter_web_platform.dart';
|
||||
|
||||
import '../../src/common.dart';
|
||||
import '../../src/mocks.dart';
|
||||
import '../../src/testbed.dart';
|
||||
|
||||
void main() {
|
||||
final Testbed testbed = Testbed();
|
||||
|
||||
group('Test that TestGoldenComparatorProcess', () {
|
||||
File imageFile;
|
||||
Uri goldenKey;
|
||||
File imageFile2;
|
||||
Uri goldenKey2;
|
||||
MockProcess Function(String) createMockProcess;
|
||||
|
||||
setUpAll(() {
|
||||
imageFile = fs.file('test_image_file');
|
||||
goldenKey = Uri.parse('file://golden_key');
|
||||
imageFile2 = fs.file('second_test_image_file');
|
||||
goldenKey2 = Uri.parse('file://second_golden_key');
|
||||
createMockProcess = (String stdout) => MockProcess(
|
||||
exitCode: Future<int>.value(0),
|
||||
stdout: stdoutFromString(stdout),
|
||||
);
|
||||
});
|
||||
|
||||
test('can pass data', () => testbed.run(() async {
|
||||
final Map<String, dynamic> expectedResponse = <String, dynamic>{
|
||||
'success': true,
|
||||
'message': 'some message',
|
||||
};
|
||||
|
||||
final MockProcess mockProcess = createMockProcess(jsonEncode(expectedResponse) + '\n');
|
||||
final MemoryIOSink ioSink = mockProcess.stdin as MemoryIOSink;
|
||||
|
||||
final TestGoldenComparatorProcess process = TestGoldenComparatorProcess(mockProcess);
|
||||
process.sendCommand(imageFile, goldenKey, false);
|
||||
|
||||
final Map<String, dynamic> response = await process.getResponse();
|
||||
final String stringToStdin = stringFromMemoryIOSink(ioSink);
|
||||
|
||||
expect(response, expectedResponse);
|
||||
expect(stringToStdin, '{"imageFile":"test_image_file","key":"file://golden_key/","update":false}\n');
|
||||
}));
|
||||
|
||||
test('can handle multiple requests', () => testbed.run(() async {
|
||||
final Map<String, dynamic> expectedResponse1 = <String, dynamic>{
|
||||
'success': true,
|
||||
'message': 'some message',
|
||||
};
|
||||
final Map<String, dynamic> expectedResponse2 = <String, dynamic>{
|
||||
'success': false,
|
||||
'message': 'some other message',
|
||||
};
|
||||
|
||||
final MockProcess mockProcess = createMockProcess(jsonEncode(expectedResponse1) + '\n' + jsonEncode(expectedResponse2) + '\n');
|
||||
final MemoryIOSink ioSink = mockProcess.stdin as MemoryIOSink;
|
||||
|
||||
final TestGoldenComparatorProcess process = TestGoldenComparatorProcess(mockProcess);
|
||||
process.sendCommand(imageFile, goldenKey, false);
|
||||
|
||||
final Map<String, dynamic> response1 = await process.getResponse();
|
||||
|
||||
process.sendCommand(imageFile2, goldenKey2, true);
|
||||
|
||||
final Map<String, dynamic> response2 = await process.getResponse();
|
||||
final String stringToStdin = stringFromMemoryIOSink(ioSink);
|
||||
|
||||
expect(response1, expectedResponse1);
|
||||
expect(response2, expectedResponse2);
|
||||
expect(stringToStdin, '{"imageFile":"test_image_file","key":"file://golden_key/","update":false}\n{"imageFile":"second_test_image_file","key":"file://second_golden_key/","update":true}\n');
|
||||
}));
|
||||
|
||||
test('ignores anything that does not look like JSON', () => testbed.run(() async {
|
||||
final Map<String, dynamic> expectedResponse = <String, dynamic>{
|
||||
'success': true,
|
||||
'message': 'some message',
|
||||
};
|
||||
|
||||
final MockProcess mockProcess = createMockProcess('''
|
||||
Some random data including {} curly bracket
|
||||
{} curly bracket that is not on the beginning of the line
|
||||
${jsonEncode(expectedResponse)}
|
||||
{"success": false}
|
||||
Other JSON data after the initial data
|
||||
''');
|
||||
final MemoryIOSink ioSink = mockProcess.stdin as MemoryIOSink;
|
||||
|
||||
final TestGoldenComparatorProcess process = TestGoldenComparatorProcess(mockProcess);
|
||||
process.sendCommand(imageFile, goldenKey, false);
|
||||
|
||||
final Map<String, dynamic> response = await process.getResponse();
|
||||
final String stringToStdin = stringFromMemoryIOSink(ioSink);
|
||||
|
||||
expect(response, expectedResponse);
|
||||
expect(stringToStdin, '{"imageFile":"test_image_file","key":"file://golden_key/","update":false}\n');
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
Stream<List<int>> stdoutFromString(String string) => Stream<List<int>>.fromIterable(<List<int>>[
|
||||
utf8.encode(string),
|
||||
]);
|
||||
|
||||
String stringFromMemoryIOSink(MemoryIOSink ioSink) => utf8.decode(ioSink.writes.expand((List<int> l) => l).toList());
|
@ -0,0 +1,188 @@
|
||||
// Copyright 2014 The Flutter 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:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_tools/src/base/file_system.dart';
|
||||
import 'package:flutter_tools/src/test/flutter_web_platform.dart';
|
||||
import 'package:flutter_tools/src/test/test_compiler.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
import 'package:process/process.dart';
|
||||
|
||||
import '../../src/common.dart';
|
||||
import '../../src/mocks.dart';
|
||||
import '../../src/testbed.dart';
|
||||
|
||||
void main() {
|
||||
|
||||
group('Test that TestGoldenComparator', () {
|
||||
Testbed testbed;
|
||||
Uri goldenKey;
|
||||
Uri goldenKey2;
|
||||
Uri testUri;
|
||||
Uri testUri2;
|
||||
Uint8List imageBytes;
|
||||
MockProcessManager mockProcessManager;
|
||||
MockTestCompiler mockCompiler;
|
||||
|
||||
setUp(() {
|
||||
goldenKey = Uri.parse('file://golden_key');
|
||||
goldenKey2 = Uri.parse('file://second_golden_key');
|
||||
testUri = Uri.parse('file://test_uri');
|
||||
testUri2 = Uri.parse('file://second_test_uri');
|
||||
imageBytes = Uint8List.fromList(<int>[1,2,3,4,5]);
|
||||
mockProcessManager = MockProcessManager();
|
||||
mockCompiler = MockTestCompiler();
|
||||
when(mockCompiler.compile(any)).thenAnswer((_) => Future<String>.value('compiler_output'));
|
||||
|
||||
testbed = Testbed(overrides: <Type, Generator>{
|
||||
ProcessManager: () {
|
||||
print('in get process manager');
|
||||
return mockProcessManager;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('succeed when golden comparison succeed', () => testbed.run(() async {
|
||||
final Map<String, dynamic> expectedResponse = <String, dynamic>{
|
||||
'success': true,
|
||||
'message': 'some message',
|
||||
};
|
||||
|
||||
when(mockProcessManager.start(any, environment: anyNamed('environment')))
|
||||
.thenAnswer((Invocation invocation) async {
|
||||
return FakeProcess(
|
||||
exitCode: Future<int>.value(0),
|
||||
stdout: stdoutFromString(jsonEncode(expectedResponse) + '\n'),
|
||||
);
|
||||
});
|
||||
|
||||
final TestGoldenComparator comparator = TestGoldenComparator(
|
||||
'shell',
|
||||
() => mockCompiler,
|
||||
);
|
||||
|
||||
final String result = await comparator.compareGoldens(testUri, imageBytes, goldenKey, false);
|
||||
expect(result, null);
|
||||
}));
|
||||
|
||||
test('fail with error message when golden comparison failed', () => testbed.run(() async {
|
||||
final Map<String, dynamic> expectedResponse = <String, dynamic>{
|
||||
'success': false,
|
||||
'message': 'some message',
|
||||
};
|
||||
|
||||
when(mockProcessManager.start(any, environment: anyNamed('environment')))
|
||||
.thenAnswer((Invocation invocation) async {
|
||||
return FakeProcess(
|
||||
exitCode: Future<int>.value(0),
|
||||
stdout: stdoutFromString(jsonEncode(expectedResponse) + '\n'),
|
||||
);
|
||||
});
|
||||
|
||||
final TestGoldenComparator comparator = TestGoldenComparator(
|
||||
'shell',
|
||||
() => mockCompiler,
|
||||
);
|
||||
|
||||
final String result = await comparator.compareGoldens(testUri, imageBytes, goldenKey, false);
|
||||
expect(result, 'some message');
|
||||
}));
|
||||
|
||||
test('reuse the process for the same test file', () => testbed.run(() async {
|
||||
final Map<String, dynamic> expectedResponse1 = <String, dynamic>{
|
||||
'success': false,
|
||||
'message': 'some message',
|
||||
};
|
||||
final Map<String, dynamic> expectedResponse2 = <String, dynamic>{
|
||||
'success': false,
|
||||
'message': 'some other message',
|
||||
};
|
||||
|
||||
when(mockProcessManager.start(any, environment: anyNamed('environment')))
|
||||
.thenAnswer((Invocation invocation) async {
|
||||
return FakeProcess(
|
||||
exitCode: Future<int>.value(0),
|
||||
stdout: stdoutFromString(jsonEncode(expectedResponse1) + '\n' + jsonEncode(expectedResponse2) + '\n'),
|
||||
);
|
||||
});
|
||||
|
||||
final TestGoldenComparator comparator = TestGoldenComparator(
|
||||
'shell',
|
||||
() => mockCompiler,
|
||||
);
|
||||
|
||||
final String result1 = await comparator.compareGoldens(testUri, imageBytes, goldenKey, false);
|
||||
expect(result1, 'some message');
|
||||
final String result2 = await comparator.compareGoldens(testUri, imageBytes, goldenKey2, false);
|
||||
expect(result2, 'some other message');
|
||||
verify(mockProcessManager.start(any, environment: anyNamed('environment'))).called(1);
|
||||
}));
|
||||
|
||||
test('does not reuse the process for different test file', () => testbed.run(() async {
|
||||
final Map<String, dynamic> expectedResponse1 = <String, dynamic>{
|
||||
'success': false,
|
||||
'message': 'some message',
|
||||
};
|
||||
final Map<String, dynamic> expectedResponse2 = <String, dynamic>{
|
||||
'success': false,
|
||||
'message': 'some other message',
|
||||
};
|
||||
|
||||
when(mockProcessManager.start(any, environment: anyNamed('environment')))
|
||||
.thenAnswer((Invocation invocation) async {
|
||||
return FakeProcess(
|
||||
exitCode: Future<int>.value(0),
|
||||
stdout: stdoutFromString(jsonEncode(expectedResponse1) + '\n' + jsonEncode(expectedResponse2) + '\n'),
|
||||
);
|
||||
});
|
||||
|
||||
final TestGoldenComparator comparator = TestGoldenComparator(
|
||||
'shell',
|
||||
() => mockCompiler,
|
||||
);
|
||||
|
||||
final String result1 = await comparator.compareGoldens(testUri, imageBytes, goldenKey, false);
|
||||
expect(result1, 'some message');
|
||||
final String result2 = await comparator.compareGoldens(testUri2, imageBytes, goldenKey2, false);
|
||||
expect(result2, 'some message');
|
||||
verify(mockProcessManager.start(any, environment: anyNamed('environment'))).called(2);
|
||||
}));
|
||||
|
||||
test('removes all temporary files when closed', () => testbed.run(() async {
|
||||
final Map<String, dynamic> expectedResponse = <String, dynamic>{
|
||||
'success': true,
|
||||
'message': 'some message',
|
||||
};
|
||||
|
||||
when(mockProcessManager.start(any, environment: anyNamed('environment')))
|
||||
.thenAnswer((Invocation invocation) async {
|
||||
return FakeProcess(
|
||||
exitCode: Future<int>.value(0),
|
||||
stdout: stdoutFromString(jsonEncode(expectedResponse) + '\n'),
|
||||
);
|
||||
});
|
||||
|
||||
final TestGoldenComparator comparator = TestGoldenComparator(
|
||||
'shell',
|
||||
() => mockCompiler,
|
||||
);
|
||||
|
||||
final String result = await comparator.compareGoldens(testUri, imageBytes, goldenKey, false);
|
||||
expect(result, null);
|
||||
|
||||
await comparator.close();
|
||||
expect(fs.systemTempDirectory.listSync(recursive: true), isEmpty);
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
Stream<List<int>> stdoutFromString(String string) => Stream<List<int>>.fromIterable(<List<int>>[
|
||||
utf8.encode(string),
|
||||
]);
|
||||
|
||||
class MockProcessManager extends Mock implements ProcessManager {}
|
||||
class MockTestCompiler extends Mock implements TestCompiler {}
|
Loading…
x
Reference in New Issue
Block a user