// 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:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:path/path.dart' as path; import 'package:test_api/scaffolding.dart' as test_package; import 'binding.dart'; /// Ensure the appropriate test binding is initialized. TestWidgetsFlutterBinding ensureInitialized([@visibleForTesting Map? environment]) { environment ??= Platform.environment; if (environment.containsKey('FLUTTER_TEST') && environment['FLUTTER_TEST'] != 'false') { return AutomatedTestWidgetsFlutterBinding.ensureInitialized(); } return LiveTestWidgetsFlutterBinding.ensureInitialized(); } /// 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']!; assert(Platform.environment['APP_NAME'] != null); final String prefix = 'packages/${Platform.environment['APP_NAME']!}/'; /// Navigation related actions (pop, push, replace) broadcasts these actions via /// platform messages. TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.navigation, (MethodCall methodCall) async { return null; }); TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMessageHandler('flutter/assets', (ByteData? message) { assert(message != null); 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 SynchronousFuture(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 { bool warningPrinted = false; @override HttpClient createHttpClient(SecurityContext? context) { if (!warningPrinted) { test_package.printOnFailure( 'Warning: At least one test in this suite creates an HttpClient. When ' 'running a test suite that uses TestWidgetsFlutterBinding, all HTTP ' 'requests will return status code 400, and no network request will ' 'actually be made. Any test expecting a real network connection and ' 'status code will fail.\n' 'To test code that needs an HttpClient, provide your own HttpClient ' 'implementation to the code under test, so that your test can ' 'consistently provide a testable response to the code under test.' .split('\n') .expand((String line) => debugWordWrap(line, FlutterError.wrapWidth)) .join('\n'), ); warningPrinted = true; } return _MockHttpClient(); } } /// A mocked [HttpClient] which always returns a [_MockHttpRequest]. class _MockHttpClient implements HttpClient { @override bool autoUncompress = true; @override Duration? connectionTimeout; @override Duration idleTimeout = const Duration(seconds: 15); @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 Future> Function(Uri url, String? proxyHost, int? proxyPort)? connectionFactory; @override Future Function(Uri url, String scheme, String realm)? authenticate; @override Future Function(String host, int port, String scheme, String realm)? authenticateProxy; @override bool Function(X509Certificate cert, String host, int port)? badCertificateCallback; @override Function(String line)? keyLog; @override void close({ bool force = false }) { } @override Future delete(String host, int port, String path) { return Future.value(_MockHttpRequest()); } @override Future deleteUrl(Uri url) { return Future.value(_MockHttpRequest()); } @override String Function(Uri url)? findProxy; @override Future get(String host, int port, String path) { return Future.value(_MockHttpRequest()); } @override Future getUrl(Uri url) { return Future.value(_MockHttpRequest()); } @override Future head(String host, int port, String path) { return Future.value(_MockHttpRequest()); } @override Future headUrl(Uri url) { return Future.value(_MockHttpRequest()); } @override Future open(String method, String host, int port, String path) { return Future.value(_MockHttpRequest()); } @override Future openUrl(String method, Uri url) { return Future.value(_MockHttpRequest()); } @override Future patch(String host, int port, String path) { return Future.value(_MockHttpRequest()); } @override Future patchUrl(Uri url) { return Future.value(_MockHttpRequest()); } @override Future post(String host, int port, String path) { return Future.value(_MockHttpRequest()); } @override Future postUrl(Uri url) { return Future.value(_MockHttpRequest()); } @override Future put(String host, int port, String path) { return Future.value(_MockHttpRequest()); } @override Future putUrl(Uri url) { return Future.value(_MockHttpRequest()); } } /// A mocked [HttpClientRequest] which always returns a [_MockHttpClientResponse]. class _MockHttpRequest implements HttpClientRequest { @override bool bufferOutput = true; @override int contentLength = -1; @override late Encoding encoding; @override bool followRedirects = true; @override final HttpHeaders headers = _MockHttpHeaders(); @override void add(List data) { } @override void addError(Object error, [ StackTrace? stackTrace ]) { } @override Future addStream(Stream> stream) { return Future.value(); } @override Future close() { return Future.value(_MockHttpResponse()); } @override void abort([Object? exception, StackTrace? stackTrace]) {} @override HttpConnectionInfo? get connectionInfo => null; @override List get cookies => []; @override Future get done async => _MockHttpResponse(); @override Future flush() { return Future.value(); } @override int maxRedirects = 5; @override String get method => ''; @override bool persistentConnection = true; @override Uri get uri => Uri(); @override void write(Object? obj) { } @override void writeAll(Iterable 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` once // https://dart-review.googlesource.com/c/sdk/+/104525 is rolled into the framework. class _MockHttpResponse implements HttpClientResponse { final Stream _delegate = Stream.fromIterable(const Iterable.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 get cookies => []; @override Future detachSocket() { return Future.error(UnsupportedError('Mocked response')); } @override bool get isRedirect => false; @override StreamSubscription listen(void Function(Uint8List event)? onData, { Function? onError, void Function()? onDone, bool? cancelOnError }) { return const Stream.empty().listen(onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError); } @override bool get persistentConnection => false; @override String get reasonPhrase => ''; @override Future redirect([ String? method, Uri? url, bool? followLoops ]) { return Future.error(UnsupportedError('Mocked response')); } @override List get redirects => []; @override int get statusCode => 400; @override Future any(bool Function(Uint8List element) test) { return _delegate.any(test); } @override Stream asBroadcastStream({ void Function(StreamSubscription subscription)? onListen, void Function(StreamSubscription subscription)? onCancel, }) { return _delegate.asBroadcastStream(onListen: onListen, onCancel: onCancel); } @override Stream asyncExpand(Stream? Function(Uint8List event) convert) { return _delegate.asyncExpand(convert); } @override Stream asyncMap(FutureOr Function(Uint8List event) convert) { return _delegate.asyncMap(convert); } @override Stream cast() { return _delegate.cast(); } @override Future contains(Object? needle) { return _delegate.contains(needle); } @override Stream distinct([bool Function(Uint8List previous, Uint8List next)? equals]) { return _delegate.distinct(equals); } @override Future drain([E? futureValue]) { return _delegate.drain(futureValue); } @override Future elementAt(int index) { return _delegate.elementAt(index); } @override Future every(bool Function(Uint8List element) test) { return _delegate.every(test); } @override Stream expand(Iterable Function(Uint8List element) convert) { return _delegate.expand(convert); } @override Future get first => _delegate.first; @override Future firstWhere( bool Function(Uint8List element) test, { List Function()? orElse, }) { return _delegate.firstWhere(test, orElse: orElse == null ? null : () { return Uint8List.fromList(orElse()); }); } @override Future fold(S initialValue, S Function(S previous, Uint8List element) combine) { return _delegate.fold(initialValue, combine); } @override Future forEach(void Function(Uint8List element) action) { return _delegate.forEach(action); } @override Stream handleError( Function onError, { bool Function(dynamic error)? test, }) { return _delegate.handleError(onError, test: test); } @override bool get isBroadcast => _delegate.isBroadcast; @override Future get isEmpty => _delegate.isEmpty; @override Future join([String separator = '']) { return _delegate.join(separator); } @override Future get last => _delegate.last; @override Future lastWhere( bool Function(Uint8List element) test, { List Function()? orElse, }) { return _delegate.lastWhere(test, orElse: orElse == null ? null : () { return Uint8List.fromList(orElse()); }); } @override Future get length => _delegate.length; @override Stream map(S Function(Uint8List event) convert) { return _delegate.map(convert); } @override Future pipe(StreamConsumer> streamConsumer) { return _delegate.cast>().pipe(streamConsumer); } @override Future reduce(List Function(Uint8List previous, Uint8List element) combine) { return _delegate.reduce((Uint8List previous, Uint8List element) { return Uint8List.fromList(combine(previous, element)); }); } @override Future get single => _delegate.single; @override Future singleWhere(bool Function(Uint8List element) test, {List Function()? orElse}) { return _delegate.singleWhere(test, orElse: orElse == null ? null : () { return Uint8List.fromList(orElse()); }); } @override Stream skip(int count) { return _delegate.skip(count); } @override Stream skipWhile(bool Function(Uint8List element) test) { return _delegate.skipWhile(test); } @override Stream take(int count) { return _delegate.take(count); } @override Stream takeWhile(bool Function(Uint8List element) test) { return _delegate.takeWhile(test); } @override Stream timeout( Duration timeLimit, { void Function(EventSink sink)? onTimeout, }) { return _delegate.timeout(timeLimit, onTimeout: onTimeout); } @override Future> toList() { return _delegate.toList(); } @override Future> toSet() { return _delegate.toSet(); } @override Stream transform(StreamTransformer, S> streamTransformer) { return _delegate.cast>().transform(streamTransformer); } @override Stream where(bool Function(Uint8List event) test) { return _delegate.where(test); } } /// A mocked [HttpHeaders] that ignores all writes. class _MockHttpHeaders implements HttpHeaders { @override List? operator [](String name) => []; @override void add(String name, Object value, {bool preserveHeaderCase = false}) { } @override late bool chunkedTransferEncoding; @override void clear() { } @override int contentLength = -1; @override ContentType? contentType; @override DateTime? date; @override DateTime? expires; @override void forEach(void Function(String name, List values) f) { } @override String? host; @override DateTime? ifModifiedSince; @override void noFolding(String name) { } @override late bool persistentConnection; @override int? port; @override void remove(String name, Object value) { } @override void removeAll(String name) { } @override void set(String name, Object value, {bool preserveHeaderCase = false}) { } @override String? value(String name) => null; }