mirror of
https://github.com/flutter/flutter
synced 2024-10-12 19:23:02 +00:00
Update the supported library set for Flutter for web (#39983)
This commit is contained in:
parent
f098de1fde
commit
8504f3ae13
|
@ -153,7 +153,7 @@ class FlutterGoldensRepositoryFileComparator extends FlutterGoldenFileComparator
|
|||
throw TestFailure('Could not be compared against non-existent file: "$golden"');
|
||||
}
|
||||
final List<int> goldenBytes = await goldenFile.readAsBytes();
|
||||
final ComparisonResult result = GoldenFileComparator.compareLists<Uint8List>(imageBytes, goldenBytes);
|
||||
final ComparisonResult result = GoldenFileComparator.compareLists(imageBytes, goldenBytes);
|
||||
return result.passed;
|
||||
}
|
||||
|
||||
|
|
|
@ -46,6 +46,8 @@ library flutter_test;
|
|||
|
||||
export 'dart:async' show Future;
|
||||
|
||||
export 'src/_goldens_io.dart'
|
||||
if (dart.library.html) 'src/_goldens_web.dart';
|
||||
export 'src/accessibility.dart';
|
||||
export 'src/all_elements.dart';
|
||||
export 'src/binding.dart';
|
||||
|
|
524
packages/flutter_test/lib/src/_binding_io.dart
Normal file
524
packages/flutter_test/lib/src/_binding_io.dart
Normal file
|
@ -0,0 +1,524 @@
|
|||
// Copyright 2016 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:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import 'binding.dart';
|
||||
|
||||
/// Ensure the [WidgetsBinding] is initialized.
|
||||
WidgetsBinding ensureInitialized([@visibleForTesting Map<String, String> environment]) {
|
||||
environment ??= Platform.environment;
|
||||
if (WidgetsBinding.instance == null) {
|
||||
if (environment.containsKey('FLUTTER_TEST') && environment['FLUTTER_TEST'] != 'false') {
|
||||
AutomatedTestWidgetsFlutterBinding();
|
||||
} else {
|
||||
LiveTestWidgetsFlutterBinding();
|
||||
}
|
||||
}
|
||||
assert(WidgetsBinding.instance is TestWidgetsFlutterBinding);
|
||||
return WidgetsBinding.instance;
|
||||
}
|
||||
|
||||
/// Setup mocking of the global [HttpClient].
|
||||
void setupHttpOverrides() {
|
||||
HttpOverrides.global = _MockHttpOverrides();
|
||||
}
|
||||
|
||||
/// Setup mocking of platform assets if `UNIT_TEST_ASSETS` is defined.
|
||||
void mockFlutterAssets() {
|
||||
if (!Platform.environment.containsKey('UNIT_TEST_ASSETS')) {
|
||||
return;
|
||||
}
|
||||
final String assetFolderPath = Platform.environment['UNIT_TEST_ASSETS'];
|
||||
final String prefix = 'packages/${Platform.environment['APP_NAME']}/';
|
||||
|
||||
/// Navigation related actions (pop, push, replace) broadcasts these actions via
|
||||
/// platform messages.
|
||||
SystemChannels.navigation.setMockMethodCallHandler((MethodCall methodCall) async {});
|
||||
|
||||
ServicesBinding.instance.defaultBinaryMessenger.setMockMessageHandler('flutter/assets', (ByteData message) {
|
||||
String key = utf8.decode(message.buffer.asUint8List());
|
||||
File asset = File(path.join(assetFolderPath, key));
|
||||
|
||||
if (!asset.existsSync()) {
|
||||
// For tests in package, it will load assets with its own package prefix.
|
||||
// In this case, we do a best-effort look up.
|
||||
if (!key.startsWith(prefix)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
key = key.replaceFirst(prefix, '');
|
||||
asset = File(path.join(assetFolderPath, key));
|
||||
if (!asset.existsSync()) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
final Uint8List encoded = Uint8List.fromList(asset.readAsBytesSync());
|
||||
return Future<ByteData>.value(encoded.buffer.asByteData());
|
||||
});
|
||||
}
|
||||
|
||||
/// Provides a default [HttpClient] which always returns empty 400 responses.
|
||||
///
|
||||
/// If another [HttpClient] is provided using [HttpOverrides.runZoned], that will
|
||||
/// take precedence over this provider.
|
||||
class _MockHttpOverrides extends HttpOverrides {
|
||||
@override
|
||||
HttpClient createHttpClient(SecurityContext _) {
|
||||
return _MockHttpClient();
|
||||
}
|
||||
}
|
||||
|
||||
/// A mocked [HttpClient] which always returns a [_MockHttpRequest].
|
||||
class _MockHttpClient implements HttpClient {
|
||||
@override
|
||||
bool autoUncompress;
|
||||
|
||||
@override
|
||||
Duration connectionTimeout;
|
||||
|
||||
@override
|
||||
Duration idleTimeout;
|
||||
|
||||
@override
|
||||
int maxConnectionsPerHost;
|
||||
|
||||
@override
|
||||
String userAgent;
|
||||
|
||||
@override
|
||||
void addCredentials(Uri url, String realm, HttpClientCredentials credentials) { }
|
||||
|
||||
@override
|
||||
void addProxyCredentials(String host, int port, String realm, HttpClientCredentials credentials) { }
|
||||
|
||||
@override
|
||||
set authenticate(Future<bool> Function(Uri url, String scheme, String realm) f) { }
|
||||
|
||||
@override
|
||||
set authenticateProxy(Future<bool> Function(String host, int port, String scheme, String realm) f) { }
|
||||
|
||||
@override
|
||||
set badCertificateCallback(bool Function(X509Certificate cert, String host, int port) callback) { }
|
||||
|
||||
@override
|
||||
void close({ bool force = false }) { }
|
||||
|
||||
@override
|
||||
Future<HttpClientRequest> delete(String host, int port, String path) {
|
||||
return Future<HttpClientRequest>.value(_MockHttpRequest());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<HttpClientRequest> deleteUrl(Uri url) {
|
||||
return Future<HttpClientRequest>.value(_MockHttpRequest());
|
||||
}
|
||||
|
||||
@override
|
||||
set findProxy(String Function(Uri url) f) { }
|
||||
|
||||
@override
|
||||
Future<HttpClientRequest> get(String host, int port, String path) {
|
||||
return Future<HttpClientRequest>.value(_MockHttpRequest());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<HttpClientRequest> getUrl(Uri url) {
|
||||
return Future<HttpClientRequest>.value(_MockHttpRequest());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<HttpClientRequest> head(String host, int port, String path) {
|
||||
return Future<HttpClientRequest>.value(_MockHttpRequest());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<HttpClientRequest> headUrl(Uri url) {
|
||||
return Future<HttpClientRequest>.value(_MockHttpRequest());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<HttpClientRequest> open(String method, String host, int port, String path) {
|
||||
return Future<HttpClientRequest>.value(_MockHttpRequest());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<HttpClientRequest> openUrl(String method, Uri url) {
|
||||
return Future<HttpClientRequest>.value(_MockHttpRequest());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<HttpClientRequest> patch(String host, int port, String path) {
|
||||
return Future<HttpClientRequest>.value(_MockHttpRequest());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<HttpClientRequest> patchUrl(Uri url) {
|
||||
return Future<HttpClientRequest>.value(_MockHttpRequest());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<HttpClientRequest> post(String host, int port, String path) {
|
||||
return Future<HttpClientRequest>.value(_MockHttpRequest());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<HttpClientRequest> postUrl(Uri url) {
|
||||
return Future<HttpClientRequest>.value(_MockHttpRequest());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<HttpClientRequest> put(String host, int port, String path) {
|
||||
return Future<HttpClientRequest>.value(_MockHttpRequest());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<HttpClientRequest> putUrl(Uri url) {
|
||||
return Future<HttpClientRequest>.value(_MockHttpRequest());
|
||||
}
|
||||
}
|
||||
|
||||
/// A mocked [HttpClientRequest] which always returns a [_MockHttpClientResponse].
|
||||
class _MockHttpRequest extends HttpClientRequest {
|
||||
@override
|
||||
Encoding encoding;
|
||||
|
||||
@override
|
||||
final HttpHeaders headers = _MockHttpHeaders();
|
||||
|
||||
@override
|
||||
void add(List<int> data) { }
|
||||
|
||||
@override
|
||||
void addError(Object error, [ StackTrace stackTrace ]) { }
|
||||
|
||||
@override
|
||||
Future<void> addStream(Stream<List<int>> stream) {
|
||||
return Future<void>.value();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<HttpClientResponse> close() {
|
||||
return Future<HttpClientResponse>.value(_MockHttpResponse());
|
||||
}
|
||||
|
||||
@override
|
||||
HttpConnectionInfo get connectionInfo => null;
|
||||
|
||||
@override
|
||||
List<Cookie> get cookies => null;
|
||||
|
||||
@override
|
||||
Future<HttpClientResponse> get done async => null;
|
||||
|
||||
@override
|
||||
Future<void> flush() {
|
||||
return Future<void>.value();
|
||||
}
|
||||
|
||||
@override
|
||||
String get method => null;
|
||||
|
||||
@override
|
||||
Uri get uri => null;
|
||||
|
||||
@override
|
||||
void write(Object obj) { }
|
||||
|
||||
@override
|
||||
void writeAll(Iterable<Object> objects, [ String separator = '' ]) { }
|
||||
|
||||
@override
|
||||
void writeCharCode(int charCode) { }
|
||||
|
||||
@override
|
||||
void writeln([ Object obj = '' ]) { }
|
||||
}
|
||||
|
||||
/// A mocked [HttpClientResponse] which is empty and has a [statusCode] of 400.
|
||||
// TODO(tvolkert): Change to `extends Stream<Uint8List>` once
|
||||
// https://dart-review.googlesource.com/c/sdk/+/104525 is rolled into the framework.
|
||||
class _MockHttpResponse implements HttpClientResponse {
|
||||
final Stream<Uint8List> _delegate = Stream<Uint8List>.fromIterable(const Iterable<Uint8List>.empty());
|
||||
|
||||
@override
|
||||
final HttpHeaders headers = _MockHttpHeaders();
|
||||
|
||||
@override
|
||||
X509Certificate get certificate => null;
|
||||
|
||||
@override
|
||||
HttpConnectionInfo get connectionInfo => null;
|
||||
|
||||
@override
|
||||
int get contentLength => -1;
|
||||
|
||||
@override
|
||||
HttpClientResponseCompressionState get compressionState {
|
||||
return HttpClientResponseCompressionState.decompressed;
|
||||
}
|
||||
|
||||
@override
|
||||
List<Cookie> get cookies => null;
|
||||
|
||||
@override
|
||||
Future<Socket> detachSocket() {
|
||||
return Future<Socket>.error(UnsupportedError('Mocked response'));
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isRedirect => false;
|
||||
|
||||
@override
|
||||
StreamSubscription<Uint8List> listen(void Function(Uint8List event) onData, { Function onError, void Function() onDone, bool cancelOnError }) {
|
||||
return const Stream<Uint8List>.empty().listen(onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get persistentConnection => null;
|
||||
|
||||
@override
|
||||
String get reasonPhrase => null;
|
||||
|
||||
@override
|
||||
Future<HttpClientResponse> redirect([ String method, Uri url, bool followLoops ]) {
|
||||
return Future<HttpClientResponse>.error(UnsupportedError('Mocked response'));
|
||||
}
|
||||
|
||||
@override
|
||||
List<RedirectInfo> get redirects => <RedirectInfo>[];
|
||||
|
||||
@override
|
||||
int get statusCode => 400;
|
||||
|
||||
@override
|
||||
Future<bool> any(bool Function(Uint8List element) test) {
|
||||
return _delegate.any(test);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<Uint8List> asBroadcastStream({
|
||||
void Function(StreamSubscription<Uint8List> subscription) onListen,
|
||||
void Function(StreamSubscription<Uint8List> subscription) onCancel,
|
||||
}) {
|
||||
return _delegate.asBroadcastStream(onListen: onListen, onCancel: onCancel);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<E> asyncExpand<E>(Stream<E> Function(Uint8List event) convert) {
|
||||
return _delegate.asyncExpand<E>(convert);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<E> asyncMap<E>(FutureOr<E> Function(Uint8List event) convert) {
|
||||
return _delegate.asyncMap<E>(convert);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<R> cast<R>() {
|
||||
return _delegate.cast<R>();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> contains(Object needle) {
|
||||
return _delegate.contains(needle);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<Uint8List> distinct([bool Function(Uint8List previous, Uint8List next) equals]) {
|
||||
return _delegate.distinct(equals);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<E> drain<E>([E futureValue]) {
|
||||
return _delegate.drain<E>(futureValue);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Uint8List> elementAt(int index) {
|
||||
return _delegate.elementAt(index);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> every(bool Function(Uint8List element) test) {
|
||||
return _delegate.every(test);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<S> expand<S>(Iterable<S> Function(Uint8List element) convert) {
|
||||
return _delegate.expand(convert);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Uint8List> get first => _delegate.first;
|
||||
|
||||
@override
|
||||
Future<Uint8List> firstWhere(
|
||||
bool Function(Uint8List element) test, {
|
||||
List<int> Function() orElse,
|
||||
}) {
|
||||
return _delegate.firstWhere(test, orElse: () {
|
||||
return Uint8List.fromList(orElse());
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<S> fold<S>(S initialValue, S Function(S previous, Uint8List element) combine) {
|
||||
return _delegate.fold<S>(initialValue, combine);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<dynamic> forEach(void Function(Uint8List element) action) {
|
||||
return _delegate.forEach(action);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<Uint8List> handleError(
|
||||
Function onError, {
|
||||
bool Function(dynamic error) test,
|
||||
}) {
|
||||
return _delegate.handleError(onError, test: test);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isBroadcast => _delegate.isBroadcast;
|
||||
|
||||
@override
|
||||
Future<bool> get isEmpty => _delegate.isEmpty;
|
||||
|
||||
@override
|
||||
Future<String> join([String separator = '']) {
|
||||
return _delegate.join(separator);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Uint8List> get last => _delegate.last;
|
||||
|
||||
@override
|
||||
Future<Uint8List> lastWhere(
|
||||
bool Function(Uint8List element) test, {
|
||||
List<int> Function() orElse,
|
||||
}) {
|
||||
return _delegate.lastWhere(test, orElse: () {
|
||||
return Uint8List.fromList(orElse());
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> get length => _delegate.length;
|
||||
|
||||
@override
|
||||
Stream<S> map<S>(S Function(Uint8List event) convert) {
|
||||
return _delegate.map<S>(convert);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<dynamic> pipe(StreamConsumer<List<int>> streamConsumer) {
|
||||
return _delegate.cast<List<int>>().pipe(streamConsumer);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Uint8List> reduce(List<int> Function(Uint8List previous, Uint8List element) combine) {
|
||||
return _delegate.reduce((Uint8List previous, Uint8List element) {
|
||||
return Uint8List.fromList(combine(previous, element));
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Uint8List> get single => _delegate.single;
|
||||
|
||||
@override
|
||||
Future<Uint8List> singleWhere(bool Function(Uint8List element) test, {List<int> Function() orElse}) {
|
||||
return _delegate.singleWhere(test, orElse: () {
|
||||
return Uint8List.fromList(orElse());
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<Uint8List> skip(int count) {
|
||||
return _delegate.skip(count);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<Uint8List> skipWhile(bool Function(Uint8List element) test) {
|
||||
return _delegate.skipWhile(test);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<Uint8List> take(int count) {
|
||||
return _delegate.take(count);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<Uint8List> takeWhile(bool Function(Uint8List element) test) {
|
||||
return _delegate.takeWhile(test);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<Uint8List> timeout(
|
||||
Duration timeLimit, {
|
||||
void Function(EventSink<Uint8List> sink) onTimeout,
|
||||
}) {
|
||||
return _delegate.timeout(timeLimit, onTimeout: onTimeout);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Uint8List>> toList() {
|
||||
return _delegate.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Set<Uint8List>> toSet() {
|
||||
return _delegate.toSet();
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<S> transform<S>(StreamTransformer<List<int>, S> streamTransformer) {
|
||||
return _delegate.cast<List<int>>().transform<S>(streamTransformer);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<Uint8List> where(bool Function(Uint8List event) test) {
|
||||
return _delegate.where(test);
|
||||
}
|
||||
}
|
||||
|
||||
/// A mocked [HttpHeaders] that ignores all writes.
|
||||
class _MockHttpHeaders extends HttpHeaders {
|
||||
@override
|
||||
List<String> operator [](String name) => <String>[];
|
||||
|
||||
@override
|
||||
void add(String name, Object value) { }
|
||||
|
||||
@override
|
||||
void clear() { }
|
||||
|
||||
@override
|
||||
void forEach(void Function(String name, List<String> values) f) { }
|
||||
|
||||
@override
|
||||
void noFolding(String name) { }
|
||||
|
||||
@override
|
||||
void remove(String name, Object value) { }
|
||||
|
||||
@override
|
||||
void removeAll(String name) { }
|
||||
|
||||
@override
|
||||
void set(String name, Object value) { }
|
||||
|
||||
@override
|
||||
String value(String name) => null;
|
||||
}
|
22
packages/flutter_test/lib/src/_binding_web.dart
Normal file
22
packages/flutter_test/lib/src/_binding_web.dart
Normal file
|
@ -0,0 +1,22 @@
|
|||
// Copyright 2019 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 'package:flutter/widgets.dart';
|
||||
|
||||
import 'binding.dart';
|
||||
|
||||
/// Ensure the [WidgetsBinding] is initialized.
|
||||
WidgetsBinding ensureInitialized([@visibleForTesting Map<String, String> environment]) {
|
||||
if (WidgetsBinding.instance == null) {
|
||||
AutomatedTestWidgetsFlutterBinding();
|
||||
}
|
||||
assert(WidgetsBinding.instance is TestWidgetsFlutterBinding);
|
||||
return WidgetsBinding.instance;
|
||||
}
|
||||
|
||||
/// This method is a noop on the web.
|
||||
void setupHttpOverrides() { }
|
||||
|
||||
/// This method is a noop on the web.
|
||||
void mockFlutterAssets() { }
|
208
packages/flutter_test/lib/src/_goldens_io.dart
Normal file
208
packages/flutter_test/lib/src/_goldens_io.dart
Normal file
|
@ -0,0 +1,208 @@
|
|||
// 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:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math' as math;
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:image/image.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:test_api/test_api.dart' as test_package show TestFailure;
|
||||
|
||||
import 'goldens.dart';
|
||||
|
||||
/// The default [GoldenFileComparator] implementation for `flutter test`.
|
||||
///
|
||||
/// The term __golden file__ refers to a master image that is considered the true
|
||||
/// rendering of a given widget, state, application, or other visual
|
||||
/// representation you have chosen to capture. This comparator loads golden
|
||||
/// files from the local file system, treating the golden key as a relative
|
||||
/// path from the test file's directory.
|
||||
///
|
||||
/// This comparator performs a pixel-for-pixel comparison of the decoded PNGs,
|
||||
/// returning true only if there's an exact match. In cases where the captured
|
||||
/// test image does not match the golden file, this comparator will provide
|
||||
/// output to illustrate the difference, described in further detail below.
|
||||
///
|
||||
/// When using `flutter test --update-goldens`, [LocalFileComparator]
|
||||
/// updates the golden files on disk to match the rendering.
|
||||
///
|
||||
/// ## Local Output from Golden File Testing
|
||||
///
|
||||
/// The [LocalFileComparator] will output test feedback when a golden file test
|
||||
/// fails. This output takes the form of differential images contained within a
|
||||
/// `failures` directory that will be generated in the same location specified
|
||||
/// by the golden key. The differential images include the master and test
|
||||
/// images that were compared, as well as an isolated diff of detected pixels,
|
||||
/// and a masked diff that overlays these detected pixels over the master image.
|
||||
///
|
||||
/// The following images are examples of a test failure output:
|
||||
///
|
||||
/// | File Name | Image Output |
|
||||
/// |----------------------------|---------------|
|
||||
/// | testName_masterImage.png | ![A golden master image](https://flutter.github.io/assets-for-api-docs/assets/flutter-test/goldens/widget_masterImage.png) |
|
||||
/// | testName_testImage.png | ![Test image](https://flutter.github.io/assets-for-api-docs/assets/flutter-test/goldens/widget_testImage.png) |
|
||||
/// | testName_isolatedDiff.png | ![An isolated pixel difference.](https://flutter.github.io/assets-for-api-docs/assets/flutter-test/goldens/widget_isolatedDiff.png) |
|
||||
/// | testName_maskedDiff.png | ![A masked pixel difference](https://flutter.github.io/assets-for-api-docs/assets/flutter-test/goldens/widget_maskedDiff.png) |
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [GoldenFileComparator], the abstract class that [LocalFileComparator]
|
||||
/// implements.
|
||||
/// * [matchesGoldenFile], the function from [flutter_test] that invokes the
|
||||
/// comparator.
|
||||
class LocalFileComparator extends GoldenFileComparator {
|
||||
/// Creates a new [LocalFileComparator] 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.
|
||||
LocalFileComparator(Uri testFile, {path.Style pathStyle})
|
||||
: basedir = _getBasedir(testFile, pathStyle),
|
||||
_path = _getPath(pathStyle);
|
||||
|
||||
static path.Context _getPath(path.Style style) {
|
||||
return path.Context(style: style ?? path.Style.platform);
|
||||
}
|
||||
|
||||
static Uri _getBasedir(Uri testFile, path.Style pathStyle) {
|
||||
final path.Context context = _getPath(pathStyle);
|
||||
final String testFilePath = context.fromUri(testFile);
|
||||
final String testDirectoryPath = context.dirname(testFilePath);
|
||||
return context.toUri(testDirectoryPath + context.separator);
|
||||
}
|
||||
|
||||
/// The directory in which the test was loaded.
|
||||
///
|
||||
/// Golden file keys will be interpreted as file paths relative to this
|
||||
/// directory.
|
||||
final Uri basedir;
|
||||
|
||||
/// Path context exists as an instance variable rather than just using the
|
||||
/// system path context in order to support testing, where we can spoof the
|
||||
/// platform to test behaviors with arbitrary path styles.
|
||||
final path.Context _path;
|
||||
|
||||
@override
|
||||
Future<bool> compare(Uint8List imageBytes, Uri golden) async {
|
||||
final File goldenFile = _getGoldenFile(golden);
|
||||
if (!goldenFile.existsSync()) {
|
||||
throw test_package.TestFailure('Could not be compared against non-existent file: "$golden"');
|
||||
}
|
||||
final List<int> goldenBytes = await goldenFile.readAsBytes();
|
||||
final ComparisonResult result = GoldenFileComparator.compareLists(imageBytes, goldenBytes);
|
||||
|
||||
if (!result.passed) {
|
||||
String additionalFeedback = '';
|
||||
if (result.diffs != null) {
|
||||
additionalFeedback = '\nFailure feedback can be found at ${path.join(basedir.path, 'failures')}';
|
||||
final Map<String, Object> diffs = result.diffs;
|
||||
diffs.forEach((String name, Object untypedImage) {
|
||||
final Image image = untypedImage;
|
||||
final File output = _getFailureFile(name, golden);
|
||||
output.parent.createSync(recursive: true);
|
||||
output.writeAsBytesSync(encodePng(image));
|
||||
});
|
||||
}
|
||||
throw test_package.TestFailure('Golden "$golden": ${result.error}$additionalFeedback');
|
||||
}
|
||||
return result.passed;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> update(Uri golden, Uint8List imageBytes) async {
|
||||
final File goldenFile = _getGoldenFile(golden);
|
||||
await goldenFile.parent.create(recursive: true);
|
||||
await goldenFile.writeAsBytes(imageBytes, flush: true);
|
||||
}
|
||||
|
||||
File _getGoldenFile(Uri golden) {
|
||||
return File(_path.join(_path.fromUri(basedir), _path.fromUri(golden.path)));
|
||||
}
|
||||
|
||||
File _getFailureFile(String failure, Uri golden) {
|
||||
final String fileName = golden.pathSegments[0];
|
||||
final String testName = fileName.split(path.extension(fileName))[0]
|
||||
+ '_'
|
||||
+ failure
|
||||
+ '.png';
|
||||
return File(_path.join('failures', testName));
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a [ComparisonResult] to describe the pixel differential of the
|
||||
/// [test] and [master] image bytes provided.
|
||||
ComparisonResult compareLists(List<int> test, List<int> master) {
|
||||
if (identical(test, master))
|
||||
return ComparisonResult(passed: true);
|
||||
|
||||
if (test == null || master == null || test.isEmpty || master.isEmpty) {
|
||||
return ComparisonResult(
|
||||
passed: false,
|
||||
error: 'Pixel test failed, null image provided.',
|
||||
);
|
||||
}
|
||||
|
||||
final Image testImage = decodePng(test);
|
||||
final Image masterImage = decodePng(master);
|
||||
|
||||
assert(testImage != null);
|
||||
assert(masterImage != null);
|
||||
|
||||
final int width = testImage.width;
|
||||
final int height = testImage.height;
|
||||
|
||||
if (width != masterImage.width || height != masterImage.height) {
|
||||
return ComparisonResult(
|
||||
passed: false,
|
||||
error: 'Pixel test failed, image sizes do not match.\n'
|
||||
'Master Image: ${masterImage.width} X ${masterImage.height}\n'
|
||||
'Test Image: ${testImage.width} X ${testImage.height}',
|
||||
);
|
||||
}
|
||||
|
||||
int pixelDiffCount = 0;
|
||||
final int totalPixels = width * height;
|
||||
final Image invertedMaster = invert(Image.from(masterImage));
|
||||
final Image invertedTest = invert(Image.from(testImage));
|
||||
|
||||
final Map<String, Image> diffs = <String, Image>{
|
||||
'masterImage' : masterImage,
|
||||
'testImage' : testImage,
|
||||
'maskedDiff' : Image.from(testImage),
|
||||
'isolatedDiff' : Image(width, height),
|
||||
};
|
||||
|
||||
for (int x = 0; x < width; x++) {
|
||||
for (int y =0; y < height; y++) {
|
||||
final int testPixel = testImage.getPixel(x, y);
|
||||
final int masterPixel = masterImage.getPixel(x, y);
|
||||
|
||||
final int diffPixel = (getRed(testPixel) - getRed(masterPixel)).abs()
|
||||
+ (getGreen(testPixel) - getGreen(masterPixel)).abs()
|
||||
+ (getBlue(testPixel) - getBlue(masterPixel)).abs()
|
||||
+ (getAlpha(testPixel) - getAlpha(masterPixel)).abs();
|
||||
|
||||
if (diffPixel != 0 ) {
|
||||
final int invertedMasterPixel = invertedMaster.getPixel(x, y);
|
||||
final int invertedTestPixel = invertedTest.getPixel(x, y);
|
||||
final int maskPixel = math.max(invertedMasterPixel, invertedTestPixel);
|
||||
diffs['maskedDiff'].setPixel(x, y, maskPixel);
|
||||
diffs['isolatedDiff'].setPixel(x, y, maskPixel);
|
||||
pixelDiffCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pixelDiffCount > 0) {
|
||||
return ComparisonResult(
|
||||
passed: false,
|
||||
error: 'Pixel test failed, ${((pixelDiffCount/totalPixels) * 100).toStringAsFixed(2)}% diff detected.',
|
||||
diffs: diffs,
|
||||
);
|
||||
}
|
||||
return ComparisonResult(passed: true);
|
||||
}
|
28
packages/flutter_test/lib/src/_goldens_web.dart
Normal file
28
packages/flutter_test/lib/src/_goldens_web.dart
Normal file
|
@ -0,0 +1,28 @@
|
|||
// Copyright 2019 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:typed_data';
|
||||
|
||||
import 'goldens.dart';
|
||||
|
||||
/// An unsupported [GoldenFileComparator] that exists for API compatibility.
|
||||
class LocalFileComparator extends GoldenFileComparator {
|
||||
@override
|
||||
Future<bool> compare(Uint8List imageBytes, Uri golden) {
|
||||
throw UnsupportedError('LocalFileComparator is not supported on the web.');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> update(Uri golden, Uint8List imageBytes) {
|
||||
throw UnsupportedError('LocalFileComparator is not supported on the web.');
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.');
|
||||
}
|
|
@ -3,8 +3,6 @@
|
|||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
|
@ -17,11 +15,12 @@ import 'package:flutter/widgets.dart';
|
|||
import 'package:flutter_test/flutter_test.dart' show TestWindow;
|
||||
import 'package:quiver/testing/async.dart';
|
||||
import 'package:quiver/time.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:test_api/test_api.dart' as test_package;
|
||||
import 'package:stack_trace/stack_trace.dart' as stack_trace;
|
||||
import 'package:vector_math/vector_math_64.dart';
|
||||
|
||||
import '_binding_io.dart'
|
||||
if (dart.library.html) '_binding_web.dart' as binding;
|
||||
import 'goldens.dart';
|
||||
import 'platform.dart';
|
||||
import 'stack_manipulation.dart';
|
||||
|
@ -247,30 +246,13 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
|
|||
///
|
||||
/// The parameter `environment` is exposed to test different environment
|
||||
/// variable values, and should not be used.
|
||||
static WidgetsBinding ensureInitialized([@visibleForTesting Map<String, String> environment]) {
|
||||
if (!isBrowser) {
|
||||
// Accessing Platform may throw from a browser, so we guard this.
|
||||
environment ??= Platform.environment;
|
||||
}
|
||||
if (WidgetsBinding.instance == null) {
|
||||
if (isBrowser) {
|
||||
// Browser environments do not support the LiveTestWidgetsFlutterBinding.
|
||||
AutomatedTestWidgetsFlutterBinding();
|
||||
} else if (environment.containsKey('FLUTTER_TEST') && environment['FLUTTER_TEST'] != 'false') {
|
||||
AutomatedTestWidgetsFlutterBinding();
|
||||
} else {
|
||||
LiveTestWidgetsFlutterBinding();
|
||||
}
|
||||
}
|
||||
assert(WidgetsBinding.instance is TestWidgetsFlutterBinding);
|
||||
return WidgetsBinding.instance;
|
||||
}
|
||||
static WidgetsBinding ensureInitialized([@visibleForTesting Map<String, String> environment]) => binding.ensureInitialized(environment);
|
||||
|
||||
@override
|
||||
void initInstances() {
|
||||
super.initInstances();
|
||||
timeDilation = 1.0; // just in case the developer has artificially changed it for development
|
||||
HttpOverrides.global = _MockHttpOverrides();
|
||||
binding.setupHttpOverrides();
|
||||
_testTextInput = TestTextInput(onCleared: _resetFocusedEditable)..register();
|
||||
}
|
||||
|
||||
|
@ -834,7 +816,7 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
|
|||
@override
|
||||
void initInstances() {
|
||||
super.initInstances();
|
||||
_mockFlutterAssets();
|
||||
binding.mockFlutterAssets();
|
||||
}
|
||||
|
||||
FakeAsync _currentFakeAsync; // set in runTest; cleared in postTest
|
||||
|
@ -862,43 +844,6 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
|
|||
@override
|
||||
int get microtaskCount => _currentFakeAsync.microtaskCount;
|
||||
|
||||
void _mockFlutterAssets() {
|
||||
if (isBrowser) {
|
||||
return;
|
||||
}
|
||||
if (!Platform.environment.containsKey('UNIT_TEST_ASSETS')) {
|
||||
return;
|
||||
}
|
||||
final String assetFolderPath = Platform.environment['UNIT_TEST_ASSETS'];
|
||||
final String prefix = 'packages/${Platform.environment['APP_NAME']}/';
|
||||
|
||||
/// Navigation related actions (pop, push, replace) broadcasts these actions via
|
||||
/// platform messages.
|
||||
SystemChannels.navigation.setMockMethodCallHandler((MethodCall methodCall) async {});
|
||||
|
||||
ServicesBinding.instance.defaultBinaryMessenger.setMockMessageHandler('flutter/assets', (ByteData message) {
|
||||
String key = utf8.decode(message.buffer.asUint8List());
|
||||
File asset = File(path.join(assetFolderPath, key));
|
||||
|
||||
if (!asset.existsSync()) {
|
||||
// For tests in package, it will load assets with its own package prefix.
|
||||
// In this case, we do a best-effort look up.
|
||||
if (!key.startsWith(prefix)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
key = key.replaceFirst(prefix, '');
|
||||
asset = File(path.join(assetFolderPath, key));
|
||||
if (!asset.existsSync()) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
final Uint8List encoded = Uint8List.fromList(asset.readAsBytesSync());
|
||||
return Future<ByteData>.value(encoded.buffer.asByteData());
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> pump([ Duration duration, EnginePhase newPhase = EnginePhase.sendSemanticsUpdate ]) {
|
||||
return TestAsyncUtils.guard<void>(() {
|
||||
|
@ -1700,458 +1645,3 @@ StackTrace _unmangle(StackTrace stack) {
|
|||
return stack.toTrace().vmTrace;
|
||||
return stack;
|
||||
}
|
||||
|
||||
/// Provides a default [HttpClient] which always returns empty 400 responses.
|
||||
///
|
||||
/// If another [HttpClient] is provided using [HttpOverrides.runZoned], that will
|
||||
/// take precedence over this provider.
|
||||
class _MockHttpOverrides extends HttpOverrides {
|
||||
@override
|
||||
HttpClient createHttpClient(SecurityContext _) {
|
||||
return _MockHttpClient();
|
||||
}
|
||||
}
|
||||
|
||||
/// A mocked [HttpClient] which always returns a [_MockHttpRequest].
|
||||
class _MockHttpClient implements HttpClient {
|
||||
@override
|
||||
bool autoUncompress;
|
||||
|
||||
@override
|
||||
Duration connectionTimeout;
|
||||
|
||||
@override
|
||||
Duration idleTimeout;
|
||||
|
||||
@override
|
||||
int maxConnectionsPerHost;
|
||||
|
||||
@override
|
||||
String userAgent;
|
||||
|
||||
@override
|
||||
void addCredentials(Uri url, String realm, HttpClientCredentials credentials) { }
|
||||
|
||||
@override
|
||||
void addProxyCredentials(String host, int port, String realm, HttpClientCredentials credentials) { }
|
||||
|
||||
@override
|
||||
set authenticate(Future<bool> Function(Uri url, String scheme, String realm) f) { }
|
||||
|
||||
@override
|
||||
set authenticateProxy(Future<bool> Function(String host, int port, String scheme, String realm) f) { }
|
||||
|
||||
@override
|
||||
set badCertificateCallback(bool Function(X509Certificate cert, String host, int port) callback) { }
|
||||
|
||||
@override
|
||||
void close({ bool force = false }) { }
|
||||
|
||||
@override
|
||||
Future<HttpClientRequest> delete(String host, int port, String path) {
|
||||
return Future<HttpClientRequest>.value(_MockHttpRequest());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<HttpClientRequest> deleteUrl(Uri url) {
|
||||
return Future<HttpClientRequest>.value(_MockHttpRequest());
|
||||
}
|
||||
|
||||
@override
|
||||
set findProxy(String Function(Uri url) f) { }
|
||||
|
||||
@override
|
||||
Future<HttpClientRequest> get(String host, int port, String path) {
|
||||
return Future<HttpClientRequest>.value(_MockHttpRequest());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<HttpClientRequest> getUrl(Uri url) {
|
||||
return Future<HttpClientRequest>.value(_MockHttpRequest());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<HttpClientRequest> head(String host, int port, String path) {
|
||||
return Future<HttpClientRequest>.value(_MockHttpRequest());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<HttpClientRequest> headUrl(Uri url) {
|
||||
return Future<HttpClientRequest>.value(_MockHttpRequest());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<HttpClientRequest> open(String method, String host, int port, String path) {
|
||||
return Future<HttpClientRequest>.value(_MockHttpRequest());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<HttpClientRequest> openUrl(String method, Uri url) {
|
||||
return Future<HttpClientRequest>.value(_MockHttpRequest());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<HttpClientRequest> patch(String host, int port, String path) {
|
||||
return Future<HttpClientRequest>.value(_MockHttpRequest());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<HttpClientRequest> patchUrl(Uri url) {
|
||||
return Future<HttpClientRequest>.value(_MockHttpRequest());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<HttpClientRequest> post(String host, int port, String path) {
|
||||
return Future<HttpClientRequest>.value(_MockHttpRequest());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<HttpClientRequest> postUrl(Uri url) {
|
||||
return Future<HttpClientRequest>.value(_MockHttpRequest());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<HttpClientRequest> put(String host, int port, String path) {
|
||||
return Future<HttpClientRequest>.value(_MockHttpRequest());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<HttpClientRequest> putUrl(Uri url) {
|
||||
return Future<HttpClientRequest>.value(_MockHttpRequest());
|
||||
}
|
||||
}
|
||||
|
||||
/// A mocked [HttpClientRequest] which always returns a [_MockHttpClientResponse].
|
||||
class _MockHttpRequest extends HttpClientRequest {
|
||||
@override
|
||||
Encoding encoding;
|
||||
|
||||
@override
|
||||
final HttpHeaders headers = _MockHttpHeaders();
|
||||
|
||||
@override
|
||||
void add(List<int> data) { }
|
||||
|
||||
@override
|
||||
void addError(Object error, [ StackTrace stackTrace ]) { }
|
||||
|
||||
@override
|
||||
Future<void> addStream(Stream<List<int>> stream) {
|
||||
return Future<void>.value();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<HttpClientResponse> close() {
|
||||
return Future<HttpClientResponse>.value(_MockHttpResponse());
|
||||
}
|
||||
|
||||
@override
|
||||
HttpConnectionInfo get connectionInfo => null;
|
||||
|
||||
@override
|
||||
List<Cookie> get cookies => null;
|
||||
|
||||
@override
|
||||
Future<HttpClientResponse> get done async => null;
|
||||
|
||||
@override
|
||||
Future<void> flush() {
|
||||
return Future<void>.value();
|
||||
}
|
||||
|
||||
@override
|
||||
String get method => null;
|
||||
|
||||
@override
|
||||
Uri get uri => null;
|
||||
|
||||
@override
|
||||
void write(Object obj) { }
|
||||
|
||||
@override
|
||||
void writeAll(Iterable<Object> objects, [ String separator = '' ]) { }
|
||||
|
||||
@override
|
||||
void writeCharCode(int charCode) { }
|
||||
|
||||
@override
|
||||
void writeln([ Object obj = '' ]) { }
|
||||
}
|
||||
|
||||
/// A mocked [HttpClientResponse] which is empty and has a [statusCode] of 400.
|
||||
// TODO(tvolkert): Change to `extends Stream<Uint8List>` once
|
||||
// https://dart-review.googlesource.com/c/sdk/+/104525 is rolled into the framework.
|
||||
class _MockHttpResponse implements HttpClientResponse {
|
||||
final Stream<Uint8List> _delegate = Stream<Uint8List>.fromIterable(const Iterable<Uint8List>.empty());
|
||||
|
||||
@override
|
||||
final HttpHeaders headers = _MockHttpHeaders();
|
||||
|
||||
@override
|
||||
X509Certificate get certificate => null;
|
||||
|
||||
@override
|
||||
HttpConnectionInfo get connectionInfo => null;
|
||||
|
||||
@override
|
||||
int get contentLength => -1;
|
||||
|
||||
@override
|
||||
HttpClientResponseCompressionState get compressionState {
|
||||
return HttpClientResponseCompressionState.decompressed;
|
||||
}
|
||||
|
||||
@override
|
||||
List<Cookie> get cookies => null;
|
||||
|
||||
@override
|
||||
Future<Socket> detachSocket() {
|
||||
return Future<Socket>.error(UnsupportedError('Mocked response'));
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isRedirect => false;
|
||||
|
||||
@override
|
||||
StreamSubscription<Uint8List> listen(void Function(Uint8List event) onData, { Function onError, void Function() onDone, bool cancelOnError }) {
|
||||
return const Stream<Uint8List>.empty().listen(onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get persistentConnection => null;
|
||||
|
||||
@override
|
||||
String get reasonPhrase => null;
|
||||
|
||||
@override
|
||||
Future<HttpClientResponse> redirect([ String method, Uri url, bool followLoops ]) {
|
||||
return Future<HttpClientResponse>.error(UnsupportedError('Mocked response'));
|
||||
}
|
||||
|
||||
@override
|
||||
List<RedirectInfo> get redirects => <RedirectInfo>[];
|
||||
|
||||
@override
|
||||
int get statusCode => 400;
|
||||
|
||||
@override
|
||||
Future<bool> any(bool Function(Uint8List element) test) {
|
||||
return _delegate.any(test);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<Uint8List> asBroadcastStream({
|
||||
void Function(StreamSubscription<Uint8List> subscription) onListen,
|
||||
void Function(StreamSubscription<Uint8List> subscription) onCancel,
|
||||
}) {
|
||||
return _delegate.asBroadcastStream(onListen: onListen, onCancel: onCancel);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<E> asyncExpand<E>(Stream<E> Function(Uint8List event) convert) {
|
||||
return _delegate.asyncExpand<E>(convert);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<E> asyncMap<E>(FutureOr<E> Function(Uint8List event) convert) {
|
||||
return _delegate.asyncMap<E>(convert);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<R> cast<R>() {
|
||||
return _delegate.cast<R>();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> contains(Object needle) {
|
||||
return _delegate.contains(needle);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<Uint8List> distinct([bool Function(Uint8List previous, Uint8List next) equals]) {
|
||||
return _delegate.distinct(equals);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<E> drain<E>([E futureValue]) {
|
||||
return _delegate.drain<E>(futureValue);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Uint8List> elementAt(int index) {
|
||||
return _delegate.elementAt(index);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> every(bool Function(Uint8List element) test) {
|
||||
return _delegate.every(test);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<S> expand<S>(Iterable<S> Function(Uint8List element) convert) {
|
||||
return _delegate.expand(convert);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Uint8List> get first => _delegate.first;
|
||||
|
||||
@override
|
||||
Future<Uint8List> firstWhere(
|
||||
bool Function(Uint8List element) test, {
|
||||
List<int> Function() orElse,
|
||||
}) {
|
||||
return _delegate.firstWhere(test, orElse: () {
|
||||
return Uint8List.fromList(orElse());
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<S> fold<S>(S initialValue, S Function(S previous, Uint8List element) combine) {
|
||||
return _delegate.fold<S>(initialValue, combine);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<dynamic> forEach(void Function(Uint8List element) action) {
|
||||
return _delegate.forEach(action);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<Uint8List> handleError(
|
||||
Function onError, {
|
||||
bool Function(dynamic error) test,
|
||||
}) {
|
||||
return _delegate.handleError(onError, test: test);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isBroadcast => _delegate.isBroadcast;
|
||||
|
||||
@override
|
||||
Future<bool> get isEmpty => _delegate.isEmpty;
|
||||
|
||||
@override
|
||||
Future<String> join([String separator = '']) {
|
||||
return _delegate.join(separator);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Uint8List> get last => _delegate.last;
|
||||
|
||||
@override
|
||||
Future<Uint8List> lastWhere(
|
||||
bool Function(Uint8List element) test, {
|
||||
List<int> Function() orElse,
|
||||
}) {
|
||||
return _delegate.lastWhere(test, orElse: () {
|
||||
return Uint8List.fromList(orElse());
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> get length => _delegate.length;
|
||||
|
||||
@override
|
||||
Stream<S> map<S>(S Function(Uint8List event) convert) {
|
||||
return _delegate.map<S>(convert);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<dynamic> pipe(StreamConsumer<List<int>> streamConsumer) {
|
||||
return _delegate.cast<List<int>>().pipe(streamConsumer);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Uint8List> reduce(List<int> Function(Uint8List previous, Uint8List element) combine) {
|
||||
return _delegate.reduce((Uint8List previous, Uint8List element) {
|
||||
return Uint8List.fromList(combine(previous, element));
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Uint8List> get single => _delegate.single;
|
||||
|
||||
@override
|
||||
Future<Uint8List> singleWhere(bool Function(Uint8List element) test, {List<int> Function() orElse}) {
|
||||
return _delegate.singleWhere(test, orElse: () {
|
||||
return Uint8List.fromList(orElse());
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<Uint8List> skip(int count) {
|
||||
return _delegate.skip(count);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<Uint8List> skipWhile(bool Function(Uint8List element) test) {
|
||||
return _delegate.skipWhile(test);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<Uint8List> take(int count) {
|
||||
return _delegate.take(count);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<Uint8List> takeWhile(bool Function(Uint8List element) test) {
|
||||
return _delegate.takeWhile(test);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<Uint8List> timeout(
|
||||
Duration timeLimit, {
|
||||
void Function(EventSink<Uint8List> sink) onTimeout,
|
||||
}) {
|
||||
return _delegate.timeout(timeLimit, onTimeout: onTimeout);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Uint8List>> toList() {
|
||||
return _delegate.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Set<Uint8List>> toSet() {
|
||||
return _delegate.toSet();
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<S> transform<S>(StreamTransformer<List<int>, S> streamTransformer) {
|
||||
return _delegate.cast<List<int>>().transform<S>(streamTransformer);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<Uint8List> where(bool Function(Uint8List event) test) {
|
||||
return _delegate.where(test);
|
||||
}
|
||||
}
|
||||
|
||||
/// A mocked [HttpHeaders] that ignores all writes.
|
||||
class _MockHttpHeaders extends HttpHeaders {
|
||||
@override
|
||||
List<String> operator [](String name) => <String>[];
|
||||
|
||||
@override
|
||||
void add(String name, Object value) { }
|
||||
|
||||
@override
|
||||
void clear() { }
|
||||
|
||||
@override
|
||||
void forEach(void Function(String name, List<String> values) f) { }
|
||||
|
||||
@override
|
||||
void noFolding(String name) { }
|
||||
|
||||
@override
|
||||
void remove(String name, Object value) { }
|
||||
|
||||
@override
|
||||
void removeAll(String name) { }
|
||||
|
||||
@override
|
||||
void set(String name, Object value) { }
|
||||
|
||||
@override
|
||||
String value(String name) => null;
|
||||
}
|
||||
|
|
|
@ -3,15 +3,12 @@
|
|||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math' as math;
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:image/image.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:test_api/test_api.dart' as test_package show TestFailure;
|
||||
import '_goldens_io.dart'
|
||||
if (dart.library.html) '_goldens_web.dart' as _goldens;
|
||||
|
||||
/// Compares image pixels against a golden image file.
|
||||
///
|
||||
|
@ -100,76 +97,8 @@ abstract class GoldenFileComparator {
|
|||
|
||||
/// Returns a [ComparisonResult] to describe the pixel differential of the
|
||||
/// [test] and [master] image bytes provided.
|
||||
static ComparisonResult compareLists<T>(List<int> test, List<int> master) {
|
||||
if (identical(test, master))
|
||||
return ComparisonResult(passed: true);
|
||||
|
||||
if (test == null || master == null || test.isEmpty || master.isEmpty) {
|
||||
return ComparisonResult(
|
||||
passed: false,
|
||||
error: 'Pixel test failed, null image provided.',
|
||||
);
|
||||
}
|
||||
|
||||
final Image testImage = decodePng(test);
|
||||
final Image masterImage = decodePng(master);
|
||||
|
||||
assert(testImage != null);
|
||||
assert(masterImage != null);
|
||||
|
||||
final int width = testImage.width;
|
||||
final int height = testImage.height;
|
||||
|
||||
if (width != masterImage.width || height != masterImage.height) {
|
||||
return ComparisonResult(
|
||||
passed: false,
|
||||
error: 'Pixel test failed, image sizes do not match.\n'
|
||||
'Master Image: ${masterImage.width} X ${masterImage.height}\n'
|
||||
'Test Image: ${testImage.width} X ${testImage.height}',
|
||||
);
|
||||
}
|
||||
|
||||
int pixelDiffCount = 0;
|
||||
final int totalPixels = width * height;
|
||||
final Image invertedMaster = invert(Image.from(masterImage));
|
||||
final Image invertedTest = invert(Image.from(testImage));
|
||||
|
||||
final Map<String, Image> diffs = <String, Image>{
|
||||
'masterImage' : masterImage,
|
||||
'testImage' : testImage,
|
||||
'maskedDiff' : Image.from(testImage),
|
||||
'isolatedDiff' : Image(width, height),
|
||||
};
|
||||
|
||||
for (int x = 0; x < width; x++) {
|
||||
for (int y =0; y < height; y++) {
|
||||
final int testPixel = testImage.getPixel(x, y);
|
||||
final int masterPixel = masterImage.getPixel(x, y);
|
||||
|
||||
final int diffPixel = (getRed(testPixel) - getRed(masterPixel)).abs()
|
||||
+ (getGreen(testPixel) - getGreen(masterPixel)).abs()
|
||||
+ (getBlue(testPixel) - getBlue(masterPixel)).abs()
|
||||
+ (getAlpha(testPixel) - getAlpha(masterPixel)).abs();
|
||||
|
||||
if (diffPixel != 0 ) {
|
||||
final int invertedMasterPixel = invertedMaster.getPixel(x, y);
|
||||
final int invertedTestPixel = invertedTest.getPixel(x, y);
|
||||
final int maskPixel = math.max(invertedMasterPixel, invertedTestPixel);
|
||||
diffs['maskedDiff'].setPixel(x, y, maskPixel);
|
||||
diffs['isolatedDiff'].setPixel(x, y, maskPixel);
|
||||
pixelDiffCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pixelDiffCount > 0) {
|
||||
return ComparisonResult(
|
||||
passed: false,
|
||||
error: 'Pixel test failed, ${((pixelDiffCount/totalPixels) * 100).toStringAsFixed(2)}% diff detected.',
|
||||
diffs: diffs,
|
||||
);
|
||||
}
|
||||
return ComparisonResult(passed: true);
|
||||
static ComparisonResult compareLists(List<int> test, List<int> master) {
|
||||
return _goldens.compareLists(test, master);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -221,32 +150,6 @@ set goldenFileComparator(GoldenFileComparator value) {
|
|||
/// * [goldenFileComparator]
|
||||
bool autoUpdateGoldenFiles = false;
|
||||
|
||||
/// The result of a pixel comparison test.
|
||||
///
|
||||
/// The [ComparisonResult] will always indicate if a test has [passed]. The
|
||||
/// optional [error] and [diffs] parameters provide further information about
|
||||
/// the result of a failing test.
|
||||
class ComparisonResult {
|
||||
/// Creates a new [ComparisonResult] for the current test.
|
||||
ComparisonResult({
|
||||
@required this.passed,
|
||||
this.error,
|
||||
this.diffs,
|
||||
}) : assert(passed != null);
|
||||
|
||||
/// Indicates whether or not a pixel comparison test has failed.
|
||||
///
|
||||
/// This value cannot be null.
|
||||
final bool passed;
|
||||
|
||||
/// Error message used to describe the cause of the pixel comparison failure.
|
||||
final String error;
|
||||
|
||||
/// Map containing differential images to illustrate found variants in pixel
|
||||
/// values in the execution of the pixel test.
|
||||
final Map<String, Image> diffs;
|
||||
}
|
||||
|
||||
/// Placeholder comparator that is set as the value of [goldenFileComparator]
|
||||
/// when the initialization that happens in the test bootstrap either has not
|
||||
/// yet happened or has been bypassed.
|
||||
|
@ -283,121 +186,29 @@ class TrivialComparator implements GoldenFileComparator {
|
|||
}
|
||||
}
|
||||
|
||||
/// The default [GoldenFileComparator] implementation for `flutter test`.
|
||||
/// The result of a pixel comparison test.
|
||||
///
|
||||
/// The term __golden file__ refers to a master image that is considered the true
|
||||
/// rendering of a given widget, state, application, or other visual
|
||||
/// representation you have chosen to capture. This comparator loads golden
|
||||
/// files from the local file system, treating the golden key as a relative
|
||||
/// path from the test file's directory.
|
||||
///
|
||||
/// This comparator performs a pixel-for-pixel comparison of the decoded PNGs,
|
||||
/// returning true only if there's an exact match. In cases where the captured
|
||||
/// test image does not match the golden file, this comparator will provide
|
||||
/// output to illustrate the difference, described in further detail below.
|
||||
///
|
||||
/// When using `flutter test --update-goldens`, [LocalFileComparator]
|
||||
/// updates the golden files on disk to match the rendering.
|
||||
///
|
||||
/// ## Local Output from Golden File Testing
|
||||
///
|
||||
/// The [LocalFileComparator] will output test feedback when a golden file test
|
||||
/// fails. This output takes the form of differential images contained within a
|
||||
/// `failures` directory that will be generated in the same location specified
|
||||
/// by the golden key. The differential images include the master and test
|
||||
/// images that were compared, as well as an isolated diff of detected pixels,
|
||||
/// and a masked diff that overlays these detected pixels over the master image.
|
||||
///
|
||||
/// The following images are examples of a test failure output:
|
||||
///
|
||||
/// | File Name | Image Output |
|
||||
/// |----------------------------|---------------|
|
||||
/// | testName_masterImage.png | ![A golden master image](https://flutter.github.io/assets-for-api-docs/assets/flutter-test/goldens/widget_masterImage.png) |
|
||||
/// | testName_testImage.png | ![Test image](https://flutter.github.io/assets-for-api-docs/assets/flutter-test/goldens/widget_testImage.png) |
|
||||
/// | testName_isolatedDiff.png | ![An isolated pixel difference.](https://flutter.github.io/assets-for-api-docs/assets/flutter-test/goldens/widget_isolatedDiff.png) |
|
||||
/// | testName_maskedDiff.png | ![A masked pixel difference](https://flutter.github.io/assets-for-api-docs/assets/flutter-test/goldens/widget_maskedDiff.png) |
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [GoldenFileComparator], the abstract class that [LocalFileComparator]
|
||||
/// implements.
|
||||
/// * [matchesGoldenFile], the function from [flutter_test] that invokes the
|
||||
/// comparator.
|
||||
class LocalFileComparator extends GoldenFileComparator {
|
||||
/// Creates a new [LocalFileComparator] for the specified [testFile].
|
||||
/// The [ComparisonResult] will always indicate if a test has [passed]. The
|
||||
/// optional [error] and [diffs] parameters provide further information about
|
||||
/// the result of a failing test.
|
||||
class ComparisonResult {
|
||||
/// Creates a new [ComparisonResult] for the current test.
|
||||
ComparisonResult({
|
||||
@required this.passed,
|
||||
this.error,
|
||||
this.diffs,
|
||||
}) : assert(passed != null);
|
||||
|
||||
/// Indicates whether or not a pixel comparison test has failed.
|
||||
///
|
||||
/// Golden file keys will be interpreted as file paths relative to the
|
||||
/// directory in which [testFile] resides.
|
||||
///
|
||||
/// The [testFile] URL must represent a file.
|
||||
LocalFileComparator(Uri testFile, {path.Style pathStyle})
|
||||
: basedir = _getBasedir(testFile, pathStyle),
|
||||
_path = _getPath(pathStyle);
|
||||
/// This value cannot be null.
|
||||
final bool passed;
|
||||
|
||||
static path.Context _getPath(path.Style style) {
|
||||
return path.Context(style: style ?? path.Style.platform);
|
||||
}
|
||||
/// Error message used to describe the cause of the pixel comparison failure.
|
||||
final String error;
|
||||
|
||||
static Uri _getBasedir(Uri testFile, path.Style pathStyle) {
|
||||
final path.Context context = _getPath(pathStyle);
|
||||
final String testFilePath = context.fromUri(testFile);
|
||||
final String testDirectoryPath = context.dirname(testFilePath);
|
||||
return context.toUri(testDirectoryPath + context.separator);
|
||||
}
|
||||
|
||||
/// The directory in which the test was loaded.
|
||||
///
|
||||
/// Golden file keys will be interpreted as file paths relative to this
|
||||
/// directory.
|
||||
final Uri basedir;
|
||||
|
||||
/// Path context exists as an instance variable rather than just using the
|
||||
/// system path context in order to support testing, where we can spoof the
|
||||
/// platform to test behaviors with arbitrary path styles.
|
||||
final path.Context _path;
|
||||
|
||||
@override
|
||||
Future<bool> compare(Uint8List imageBytes, Uri golden) async {
|
||||
final File goldenFile = _getGoldenFile(golden);
|
||||
if (!goldenFile.existsSync()) {
|
||||
throw test_package.TestFailure('Could not be compared against non-existent file: "$golden"');
|
||||
}
|
||||
final List<int> goldenBytes = await goldenFile.readAsBytes();
|
||||
final ComparisonResult result = GoldenFileComparator.compareLists<Uint8List>(imageBytes, goldenBytes);
|
||||
|
||||
if (!result.passed) {
|
||||
String additionalFeedback = '';
|
||||
if (result.diffs != null) {
|
||||
additionalFeedback = '\nFailure feedback can be found at ${path.join(basedir.path, 'failures')}';
|
||||
final Map<String, Image> diffs = result.diffs;
|
||||
diffs.forEach((String name, Image image) {
|
||||
final File output = _getFailureFile(name, golden);
|
||||
output.parent.createSync(recursive: true);
|
||||
output.writeAsBytesSync(encodePng(image));
|
||||
});
|
||||
}
|
||||
throw test_package.TestFailure('Golden "$golden": ${result.error}$additionalFeedback');
|
||||
}
|
||||
return result.passed;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> update(Uri golden, Uint8List imageBytes) async {
|
||||
final File goldenFile = _getGoldenFile(golden);
|
||||
await goldenFile.parent.create(recursive: true);
|
||||
await goldenFile.writeAsBytes(imageBytes, flush: true);
|
||||
}
|
||||
|
||||
File _getGoldenFile(Uri golden) {
|
||||
return File(_path.join(_path.fromUri(basedir), _path.fromUri(golden.path)));
|
||||
}
|
||||
|
||||
File _getFailureFile(String failure, Uri golden) {
|
||||
final String fileName = golden.pathSegments[0];
|
||||
final String testName = fileName.split(path.extension(fileName))[0]
|
||||
+ '_'
|
||||
+ failure
|
||||
+ '.png';
|
||||
return File(_path.join('failures', testName));
|
||||
}
|
||||
/// Map containing differential images to illustrate found variants in pixel
|
||||
/// values in the execution of the pixel test.
|
||||
// TODO(jonahwilliams): fix type signature when image is updated to support web.
|
||||
final Map<String, Object> diffs;
|
||||
}
|
||||
|
|
|
@ -41,6 +41,21 @@ const String jsSourceMapExtension = '.ddc.js.map';
|
|||
const String kReleaseFlag = 'release';
|
||||
const String kProfileFlag = 'profile';
|
||||
|
||||
// A minimum set of libraries to skip checks for to keep the examples compiling
|
||||
// until we make a decision on whether to support dart:io on the web.
|
||||
// See https://github.com/dart-lang/sdk/issues/35969
|
||||
// See https://github.com/flutter/flutter/issues/39998
|
||||
const Set<String> skipPlatformCheckPackages = <String>{
|
||||
'flutter',
|
||||
'flutter_test',
|
||||
'flutter_driver',
|
||||
'flutter_goldens',
|
||||
'flutter_goldens_client',
|
||||
'flutter_gallery',
|
||||
'connectivity',
|
||||
'video_player',
|
||||
};
|
||||
|
||||
final DartPlatform flutterWebPlatform =
|
||||
DartPlatform.register('flutter_web', <String>[
|
||||
'async',
|
||||
|
@ -127,7 +142,7 @@ final List<core.BuilderApplication> builders = <core.BuilderApplication>[
|
|||
sdkKernelPath: path.join('kernel', 'flutter_ddc_sdk.dill'),
|
||||
outputExtension: ddcKernelExtension,
|
||||
platform: flutterWebPlatform,
|
||||
librariesPath: 'libraries.json',
|
||||
librariesPath: path.absolute(path.join(builderOptions.config['flutterWebSdk'], 'libraries.json')),
|
||||
kernelTargetName: 'ddc',
|
||||
),
|
||||
(BuilderOptions builderOptions) => DevCompilerBuilder(
|
||||
|
@ -135,7 +150,7 @@ final List<core.BuilderApplication> builders = <core.BuilderApplication>[
|
|||
platform: flutterWebPlatform,
|
||||
platformSdk: builderOptions.config['flutterWebSdk'],
|
||||
sdkKernelPath: path.url.join('kernel', 'flutter_ddc_sdk.dill'),
|
||||
librariesPath: 'libraries.json',
|
||||
librariesPath: path.absolute(path.join(builderOptions.config['flutterWebSdk'], 'libraries.json')),
|
||||
),
|
||||
],
|
||||
core.toAllPackages(),
|
||||
|
@ -201,7 +216,8 @@ class FlutterWebTestEntrypointBuilder implements Builder {
|
|||
@override
|
||||
Future<void> build(BuildStep buildStep) async {
|
||||
log.info('building for target ${buildStep.inputId.path}');
|
||||
await bootstrapDdc(buildStep, platform: flutterWebPlatform);
|
||||
await bootstrapDdc(buildStep, platform: flutterWebPlatform,
|
||||
skipPlatformCheckPackages: skipPlatformCheckPackages);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -229,7 +245,8 @@ class FlutterWebEntrypointBuilder implements Builder {
|
|||
if (release || profile) {
|
||||
await bootstrapDart2Js(buildStep, flutterWebSdk, profile);
|
||||
} else {
|
||||
await bootstrapDdc(buildStep, platform: flutterWebPlatform);
|
||||
await bootstrapDdc(buildStep, platform: flutterWebPlatform,
|
||||
skipPlatformCheckPackages: skipPlatformCheckPackages);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -422,7 +422,11 @@ class BuildDaemonCreator {
|
|||
}
|
||||
break;
|
||||
default:
|
||||
printTrace(serverLog.message);
|
||||
if (serverLog.message.contains('Skipping compiling')) {
|
||||
printError(serverLog.message);
|
||||
} else {
|
||||
printTrace(serverLog.message);
|
||||
}
|
||||
}
|
||||
},
|
||||
buildMode: daemon.BuildMode.Manual,
|
||||
|
|
Loading…
Reference in a new issue