From 92125ed38fcd1834c3e12bf8db325e26e30645d2 Mon Sep 17 00:00:00 2001 From: Matt Carroll Date: Mon, 4 Feb 2019 16:46:04 -0800 Subject: [PATCH] Enable dependency injection of Window instead of using static property (#27389) --- .../lib/stocks/layout_bench.dart | 10 +- .../flutter/lib/src/foundation/binding.dart | 23 +- .../flutter/lib/src/gestures/binding.dart | 6 +- .../flutter/lib/src/rendering/binding.dart | 13 +- packages/flutter/lib/src/rendering/view.dart | 20 +- .../flutter/lib/src/scheduler/binding.dart | 9 +- .../flutter/lib/src/semantics/binding.dart | 6 +- .../flutter/lib/src/semantics/semantics.dart | 3 +- .../flutter/lib/src/services/binding.dart | 8 +- .../lib/src/services/platform_messages.dart | 3 +- packages/flutter/lib/src/widgets/app.dart | 21 +- packages/flutter/lib/src/widgets/binding.dart | 11 +- .../lib/src/widgets/editable_text.dart | 6 +- .../lib/src/widgets/scroll_physics.dart | 6 +- .../lib/src/widgets/semantics_debugger.dart | 5 +- .../lib/src/widgets/widget_inspector.dart | 3 +- .../rendering/independent_layout_test.dart | 4 +- .../flutter/test/rendering/object_test.dart | 6 +- .../test/services/platform_messages_test.dart | 5 +- .../independent_widget_layout_test.dart | 5 +- .../test/widgets/media_query_test.dart | 10 +- packages/flutter_test/lib/flutter_test.dart | 1 + .../flutter_test/lib/src/accessibility.dart | 6 +- packages/flutter_test/lib/src/binding.dart | 45 ++- packages/flutter_test/lib/src/window.dart | 303 ++++++++++++++++++ packages/flutter_test/test/window_test.dart | 256 +++++++++++++++ 26 files changed, 702 insertions(+), 92 deletions(-) create mode 100644 packages/flutter_test/lib/src/window.dart create mode 100644 packages/flutter_test/test/window_test.dart diff --git a/dev/benchmarks/microbenchmarks/lib/stocks/layout_bench.dart b/dev/benchmarks/microbenchmarks/lib/stocks/layout_bench.dart index 1c6f2e33e94..31392395dba 100644 --- a/dev/benchmarks/microbenchmarks/lib/stocks/layout_bench.dart +++ b/dev/benchmarks/microbenchmarks/lib/stocks/layout_bench.dart @@ -32,8 +32,14 @@ Future main() async { await tester.pump(); // Start drawer animation await tester.pump(const Duration(seconds: 1)); // Complete drawer animation - final TestViewConfiguration big = TestViewConfiguration(size: const Size(360.0, 640.0)); - final TestViewConfiguration small = TestViewConfiguration(size: const Size(355.0, 635.0)); + final TestViewConfiguration big = TestViewConfiguration( + size: const Size(360.0, 640.0), + window: RendererBinding.instance.window, + ); + final TestViewConfiguration small = TestViewConfiguration( + size: const Size(355.0, 635.0), + window: RendererBinding.instance.window, + ); final RenderView renderView = WidgetsBinding.instance.renderView; binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.benchmark; diff --git a/packages/flutter/lib/src/foundation/binding.dart b/packages/flutter/lib/src/foundation/binding.dart index b1648d554cf..07852d8f908 100644 --- a/packages/flutter/lib/src/foundation/binding.dart +++ b/packages/flutter/lib/src/foundation/binding.dart @@ -6,7 +6,8 @@ import 'dart:async'; import 'dart:convert' show json; import 'dart:developer' as developer; import 'dart:io' show exit; -import 'dart:ui' show saveCompilationTrace; +// Before adding any more dart:ui imports, pleaes read the README. +import 'dart:ui' as ui show saveCompilationTrace, Window, window; import 'package:meta/meta.dart'; @@ -66,6 +67,24 @@ abstract class BindingBase { static bool _debugInitialized = false; static bool _debugServiceExtensionsRegistered = false; + /// The window to which this binding is bound. + /// + /// A number of additional bindings are defined as extensions of [BindingBase], + /// e.g., [ServicesBinding], [RendererBinding], and [WidgetsBinding]. Each of + /// these bindings define behaviors that interact with a [ui.Window], e.g., + /// [ServicesBinding] registers a [ui.Window.onPlatformMessage] handler, and + /// [RendererBinding] registers [ui.Window.onMetricsChanged], + /// [ui.Window.onTextScaleFactorChanged], [ui.Window.onSemanticsEnabledChanged], + /// and [ui.Window.onSemanticsAction] handlers. + /// + /// Each of these other bindings could individually access a [Window] statically, + /// but that would preclude the ability to test these behaviors with a fake + /// window for verification purposes. Therefore, [BindingBase] exposes this + /// [Window] for use by other bindings. A subclass of [BindingBase], such as + /// [TestWidgetsFlutterBinding], can override this accessor to return a + /// different [Window] implementation, such as a [TestWindow]. + ui.Window get window => ui.window; + /// The initialization method. Subclasses override this method to hook into /// the platform and otherwise configure their services. Subclasses must call /// "super.initInstances()". @@ -122,7 +141,7 @@ abstract class BindingBase { name: 'saveCompilationTrace', callback: (Map parameters) async { return { - 'value': saveCompilationTrace(), + 'value': ui.saveCompilationTrace(), }; } ); diff --git a/packages/flutter/lib/src/gestures/binding.dart b/packages/flutter/lib/src/gestures/binding.dart index 63740b5cd39..56ea434e1ee 100644 --- a/packages/flutter/lib/src/gestures/binding.dart +++ b/packages/flutter/lib/src/gestures/binding.dart @@ -4,7 +4,7 @@ import 'dart:async'; import 'dart:collection'; -import 'dart:ui' as ui show window, PointerDataPacket; +import 'dart:ui' as ui show PointerDataPacket; import 'package:flutter/foundation.dart'; @@ -62,7 +62,7 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H void initInstances() { super.initInstances(); _instance = this; - ui.window.onPointerDataPacket = _handlePointerDataPacket; + window.onPointerDataPacket = _handlePointerDataPacket; } @override @@ -80,7 +80,7 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H void _handlePointerDataPacket(ui.PointerDataPacket packet) { // We convert pointer data to logical pixels so that e.g. the touch slop can be // defined in a device-independent manner. - _pendingPointerEvents.addAll(PointerEventConverter.expand(packet.data, ui.window.devicePixelRatio)); + _pendingPointerEvents.addAll(PointerEventConverter.expand(packet.data, window.devicePixelRatio)); if (!locked) _flushPointerEventQueue(); } diff --git a/packages/flutter/lib/src/rendering/binding.dart b/packages/flutter/lib/src/rendering/binding.dart index 47aa24b97de..3babe2e031a 100644 --- a/packages/flutter/lib/src/rendering/binding.dart +++ b/packages/flutter/lib/src/rendering/binding.dart @@ -5,7 +5,6 @@ import 'dart:async'; import 'dart:developer'; import 'dart:typed_data'; -import 'dart:ui' as ui show window; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; @@ -31,7 +30,7 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture onSemanticsOwnerCreated: _handleSemanticsOwnerCreated, onSemanticsOwnerDisposed: _handleSemanticsOwnerDisposed, ); - ui.window + window ..onMetricsChanged = handleMetricsChanged ..onTextScaleFactorChanged = handleTextScaleFactorChanged ..onSemanticsEnabledChanged = _handleSemanticsEnabledChanged @@ -131,7 +130,7 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture /// Called automatically when the binding is created. void initRenderView() { assert(renderView == null); - renderView = RenderView(configuration: createViewConfiguration()); + renderView = RenderView(configuration: createViewConfiguration(), window: window); renderView.scheduleInitialFrame(); } @@ -181,9 +180,9 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture /// this to force the display into 800x600 when a test is run on the device /// using `flutter run`. ViewConfiguration createViewConfiguration() { - final double devicePixelRatio = ui.window.devicePixelRatio; + final double devicePixelRatio = window.devicePixelRatio; return ViewConfiguration( - size: ui.window.physicalSize / devicePixelRatio, + size: window.physicalSize / devicePixelRatio, devicePixelRatio: devicePixelRatio, ); } @@ -198,12 +197,12 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture // the logical coordinates of the event location back to device pixels // here. return renderView.layer - .find(offset * ui.window.devicePixelRatio); + .find(offset * window.devicePixelRatio); }); } void _handleSemanticsEnabledChanged() { - setSemanticsEnabled(ui.window.semanticsEnabled); + setSemanticsEnabled(window.semanticsEnabled); } /// Whether the render tree associated with this binding should produce a tree diff --git a/packages/flutter/lib/src/rendering/view.dart b/packages/flutter/lib/src/rendering/view.dart index 673e96cc7e8..2d079911e8f 100644 --- a/packages/flutter/lib/src/rendering/view.dart +++ b/packages/flutter/lib/src/rendering/view.dart @@ -4,7 +4,7 @@ import 'dart:developer'; import 'dart:io' show Platform; -import 'dart:ui' as ui show Scene, SceneBuilder, window; +import 'dart:ui' as ui show Scene, SceneBuilder, Window; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; @@ -56,8 +56,10 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin RenderView({ RenderBox child, @required ViewConfiguration configuration, + @required ui.Window window, }) : assert(configuration != null), - _configuration = configuration { + _configuration = configuration, + _window = window { this.child = child; } @@ -82,6 +84,8 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin markNeedsLayout(); } + ui.Window _window; + /// Whether Flutter should automatically compute the desired system UI. /// /// When this setting is enabled, Flutter will hit-test the layer tree at the @@ -195,7 +199,7 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin final ui.Scene scene = layer.buildScene(builder); if (automaticSystemUiAdjustment) _updateSystemChrome(); - ui.window.render(scene); + _window.render(scene); scene.dispose(); assert(() { if (debugRepaintRainbowEnabled || debugRepaintTextRainbowEnabled) @@ -209,8 +213,8 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin void _updateSystemChrome() { final Rect bounds = paintBounds; - final Offset top = Offset(bounds.center.dx, ui.window.padding.top / ui.window.devicePixelRatio); - final Offset bottom = Offset(bounds.center.dx, bounds.center.dy - ui.window.padding.bottom / ui.window.devicePixelRatio); + final Offset top = Offset(bounds.center.dx, _window.padding.top / _window.devicePixelRatio); + final Offset bottom = Offset(bounds.center.dx, bounds.center.dy - _window.padding.bottom / _window.devicePixelRatio); final SystemUiOverlayStyle upperOverlayStyle = layer.find(top); // Only android has a customizable system navigation bar. SystemUiOverlayStyle lowerOverlayStyle; @@ -254,10 +258,10 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin properties.add(DiagnosticsNode.message('debug mode enabled - ${Platform.operatingSystem}')); return true; }()); - properties.add(DiagnosticsProperty('window size', ui.window.physicalSize, tooltip: 'in physical pixels')); - properties.add(DoubleProperty('device pixel ratio', ui.window.devicePixelRatio, tooltip: 'physical pixels per logical pixel')); + properties.add(DiagnosticsProperty('window size', _window.physicalSize, tooltip: 'in physical pixels')); + properties.add(DoubleProperty('device pixel ratio', _window.devicePixelRatio, tooltip: 'physical pixels per logical pixel')); properties.add(DiagnosticsProperty('configuration', configuration, tooltip: 'in logical pixels')); - if (ui.window.semanticsEnabled) + if (_window.semanticsEnabled) properties.add(DiagnosticsNode.message('semantics enabled')); } } diff --git a/packages/flutter/lib/src/scheduler/binding.dart b/packages/flutter/lib/src/scheduler/binding.dart index d5df6a25b5b..386c942c438 100644 --- a/packages/flutter/lib/src/scheduler/binding.dart +++ b/packages/flutter/lib/src/scheduler/binding.dart @@ -5,7 +5,6 @@ import 'dart:async'; import 'dart:collection'; import 'dart:developer'; -import 'dart:ui' as ui show window; import 'dart:ui' show AppLifecycleState; import 'package:collection/collection.dart' show PriorityQueue, HeapPriorityQueue; @@ -191,8 +190,8 @@ mixin SchedulerBinding on BindingBase, ServicesBinding { void initInstances() { super.initInstances(); _instance = this; - ui.window.onBeginFrame = _handleBeginFrame; - ui.window.onDrawFrame = _handleDrawFrame; + window.onBeginFrame = _handleBeginFrame; + window.onDrawFrame = _handleDrawFrame; SystemChannels.lifecycle.setMessageHandler(_handleLifecycleMessage); } @@ -682,7 +681,7 @@ mixin SchedulerBinding on BindingBase, ServicesBinding { debugPrintStack(label: 'scheduleFrame() called. Current phase is $schedulerPhase.'); return true; }()); - ui.window.scheduleFrame(); + window.scheduleFrame(); _hasScheduledFrame = true; } @@ -713,7 +712,7 @@ mixin SchedulerBinding on BindingBase, ServicesBinding { debugPrintStack(label: 'scheduleForcedFrame() called. Current phase is $schedulerPhase.'); return true; }()); - ui.window.scheduleFrame(); + window.scheduleFrame(); _hasScheduledFrame = true; } diff --git a/packages/flutter/lib/src/semantics/binding.dart b/packages/flutter/lib/src/semantics/binding.dart index 34e72c48c5d..b52ef78e785 100644 --- a/packages/flutter/lib/src/semantics/binding.dart +++ b/packages/flutter/lib/src/semantics/binding.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui' as ui show AccessibilityFeatures, window; +import 'dart:ui' as ui show AccessibilityFeatures; import 'package:flutter/foundation.dart'; @@ -21,7 +21,7 @@ mixin SemanticsBinding on BindingBase { void initInstances() { super.initInstances(); _instance = this; - _accessibilityFeatures = ui.window.accessibilityFeatures; + _accessibilityFeatures = window.accessibilityFeatures; } /// Called when the platform accessibility features change. @@ -29,7 +29,7 @@ mixin SemanticsBinding on BindingBase { /// See [Window.onAccessibilityFeaturesChanged]. @protected void handleAccessibilityFeaturesChanged() { - _accessibilityFeatures = ui.window.accessibilityFeatures; + _accessibilityFeatures = window.accessibilityFeatures; } /// The currently active set of [AccessibilityFeatures]. diff --git a/packages/flutter/lib/src/semantics/semantics.dart b/packages/flutter/lib/src/semantics/semantics.dart index e56eb323d87..3c757ffdf36 100644 --- a/packages/flutter/lib/src/semantics/semantics.dart +++ b/packages/flutter/lib/src/semantics/semantics.dart @@ -13,6 +13,7 @@ import 'package:flutter/painting.dart' show MatrixUtils, TransformProperty; import 'package:flutter/services.dart'; import 'package:vector_math/vector_math_64.dart'; +import 'binding.dart' show SemanticsBinding; import 'semantics_event.dart'; export 'dart:ui' show SemanticsAction; @@ -2473,7 +2474,7 @@ class SemanticsOwner extends ChangeNotifier { final CustomSemanticsAction action = CustomSemanticsAction.getAction(actionId); builder.updateCustomAction(id: actionId, label: action.label, hint: action.hint, overrideId: action.action?.index ?? -1); } - ui.window.updateSemantics(builder.build()); + SemanticsBinding.instance.window.updateSemantics(builder.build()); notifyListeners(); } diff --git a/packages/flutter/lib/src/services/binding.dart b/packages/flutter/lib/src/services/binding.dart index ccaa815661f..d9b011b0646 100644 --- a/packages/flutter/lib/src/services/binding.dart +++ b/packages/flutter/lib/src/services/binding.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:ui' as ui; import 'package:flutter/foundation.dart'; @@ -20,11 +19,16 @@ mixin ServicesBinding on BindingBase { @override void initInstances() { super.initInstances(); - ui.window + _instance = this; + window ..onPlatformMessage = BinaryMessages.handlePlatformMessage; initLicenses(); } + /// The current [ServicesBinding], if one has been created. + static ServicesBinding get instance => _instance; + static ServicesBinding _instance; + /// Adds relevant licenses to the [LicenseRegistry]. /// /// By default, the [ServicesBinding]'s implementation of [initLicenses] adds diff --git a/packages/flutter/lib/src/services/platform_messages.dart b/packages/flutter/lib/src/services/platform_messages.dart index e8e191cca18..b5b3a22db6e 100644 --- a/packages/flutter/lib/src/services/platform_messages.dart +++ b/packages/flutter/lib/src/services/platform_messages.dart @@ -8,6 +8,7 @@ import 'dart:ui' as ui; import 'package:flutter/foundation.dart'; +import 'binding.dart' show ServicesBinding; import 'platform_channel.dart'; typedef _MessageHandler = Future Function(ByteData message); @@ -36,7 +37,7 @@ class BinaryMessages { static Future _sendPlatformMessage(String channel, ByteData message) { final Completer completer = Completer(); - ui.window.sendPlatformMessage(channel, message, (ByteData reply) { + ServicesBinding.instance.window.sendPlatformMessage(channel, message, (ByteData reply) { try { completer.complete(reply); } catch (exception, stack) { diff --git a/packages/flutter/lib/src/widgets/app.dart b/packages/flutter/lib/src/widgets/app.dart index 22cc41e531d..7934569aa3b 100644 --- a/packages/flutter/lib/src/widgets/app.dart +++ b/packages/flutter/lib/src/widgets/app.dart @@ -4,7 +4,6 @@ import 'dart:async'; import 'dart:collection' show HashMap; -import 'dart:ui' as ui show window; import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; @@ -710,7 +709,7 @@ class _WidgetsAppState extends State implements WidgetsBindingObserv void initState() { super.initState(); _updateNavigator(); - _locale = _resolveLocales(ui.window.locales, widget.supportedLocales); + _locale = _resolveLocales(WidgetsBinding.instance.window.locales, widget.supportedLocales); WidgetsBinding.instance.addObserver(this); } @@ -996,7 +995,7 @@ class _WidgetsAppState extends State implements WidgetsBindingObserv @override void didChangeAccessibilityFeatures() { setState(() { - // The properties of ui.window have changed. We use them in our build + // The properties of window have changed. We use them in our build // function, so we need setState(), but we don't cache anything locally. }); } @@ -1007,7 +1006,7 @@ class _WidgetsAppState extends State implements WidgetsBindingObserv @override void didChangeMetrics() { setState(() { - // The properties of ui.window have changed. We use them in our build + // The properties of window have changed. We use them in our build // function, so we need setState(), but we don't cache anything locally. }); } @@ -1015,8 +1014,8 @@ class _WidgetsAppState extends State implements WidgetsBindingObserv @override void didChangeTextScaleFactor() { setState(() { - // The textScaleFactor property of ui.window has changed. We reference - // ui.window in our build function, so we need to call setState(), but + // The textScaleFactor property of window has changed. We reference + // window in our build function, so we need to call setState(), but // we don't need to cache anything locally. }); } @@ -1077,12 +1076,12 @@ class _WidgetsAppState extends State implements WidgetsBindingObserv if (_navigator != null) { navigator = Navigator( key: _navigator, - // If ui.window.defaultRouteName isn't '/', we should assume it was set + // If window.defaultRouteName isn't '/', we should assume it was set // intentionally via `setInitialRoute`, and should override whatever // is in [widget.initialRoute]. - initialRoute: ui.window.defaultRouteName != Navigator.defaultRouteName - ? ui.window.defaultRouteName - : widget.initialRoute ?? ui.window.defaultRouteName, + initialRoute: WidgetsBinding.instance.window.defaultRouteName != Navigator.defaultRouteName + ? WidgetsBinding.instance.window.defaultRouteName + : widget.initialRoute ?? WidgetsBinding.instance.window.defaultRouteName, onGenerateRoute: _onGenerateRoute, onUnknownRoute: _onUnknownRoute, observers: widget.navigatorObservers, @@ -1183,7 +1182,7 @@ class _WidgetsAppState extends State implements WidgetsBindingObserv assert(_debugCheckLocalizations(appLocale)); return MediaQuery( - data: MediaQueryData.fromWindow(ui.window), + data: MediaQueryData.fromWindow(WidgetsBinding.instance.window), child: Localizations( locale: appLocale, delegates: _localizationsDelegates.toList(), diff --git a/packages/flutter/lib/src/widgets/binding.dart b/packages/flutter/lib/src/widgets/binding.dart index 81fbdebdef8..76b166ee983 100644 --- a/packages/flutter/lib/src/widgets/binding.dart +++ b/packages/flutter/lib/src/widgets/binding.dart @@ -5,7 +5,6 @@ import 'dart:async'; import 'dart:developer' as developer; import 'dart:ui' show AppLifecycleState, Locale, AccessibilityFeatures; -import 'dart:ui' as ui show window; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; @@ -141,7 +140,7 @@ abstract class WidgetsBindingObserver { /// /// @override /// void didChangeMetrics() { - /// setState(() { _lastSize = ui.window.physicalSize; }); + /// setState(() { _lastSize = WidgetsBinding.instance.window.physicalSize; }); /// } /// /// @override @@ -197,7 +196,7 @@ abstract class WidgetsBindingObserver { /// /// @override /// void didChangeTextScaleFactor() { - /// setState(() { _lastTextScaleFactor = ui.window.textScaleFactor; }); + /// setState(() { _lastTextScaleFactor = WidgetsBinding.instance.window.textScaleFactor; }); /// } /// /// @override @@ -250,8 +249,8 @@ mixin WidgetsBinding on BindingBase, SchedulerBinding, GestureBinding, RendererB super.initInstances(); _instance = this; buildOwner.onBuildScheduled = _handleBuildScheduled; - ui.window.onLocaleChanged = handleLocaleChanged; - ui.window.onAccessibilityFeaturesChanged = handleAccessibilityFeaturesChanged; + window.onLocaleChanged = handleLocaleChanged; + window.onAccessibilityFeaturesChanged = handleAccessibilityFeaturesChanged; SystemChannels.navigation.setMethodCallHandler(_handleNavigationInvocation); SystemChannels.system.setMessageHandler(_handleSystemMessage); } @@ -424,7 +423,7 @@ mixin WidgetsBinding on BindingBase, SchedulerBinding, GestureBinding, RendererB @protected @mustCallSuper void handleLocaleChanged() { - dispatchLocalesChanged(ui.window.locales); + dispatchLocalesChanged(window.locales); } /// Notify all the observers that the locale has changed (using diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart index 4a986e1dbb1..a93c46d16c1 100644 --- a/packages/flutter/lib/src/widgets/editable_text.dart +++ b/packages/flutter/lib/src/widgets/editable_text.dart @@ -980,10 +980,10 @@ class EditableTextState extends State with AutomaticKeepAliveClien @override void didChangeMetrics() { - if (_lastBottomViewInset < ui.window.viewInsets.bottom) { + if (_lastBottomViewInset < WidgetsBinding.instance.window.viewInsets.bottom) { _showCaretOnScreen(); } - _lastBottomViewInset = ui.window.viewInsets.bottom; + _lastBottomViewInset = WidgetsBinding.instance.window.viewInsets.bottom; } void _formatAndSetValue(TextEditingValue value) { @@ -1102,7 +1102,7 @@ class EditableTextState extends State with AutomaticKeepAliveClien if (_hasFocus) { // Listen for changing viewInsets, which indicates keyboard showing up. WidgetsBinding.instance.addObserver(this); - _lastBottomViewInset = ui.window.viewInsets.bottom; + _lastBottomViewInset = WidgetsBinding.instance.window.viewInsets.bottom; _showCaretOnScreen(); if (!_value.selection.isValid) { // Place cursor at the end if the selection is invalid when we receive focus. diff --git a/packages/flutter/lib/src/widgets/scroll_physics.dart b/packages/flutter/lib/src/widgets/scroll_physics.dart index 5dff934dc24..c9eb76d8d92 100644 --- a/packages/flutter/lib/src/widgets/scroll_physics.dart +++ b/packages/flutter/lib/src/widgets/scroll_physics.dart @@ -3,12 +3,12 @@ // found in the LICENSE file. import 'dart:math' as math; -import 'dart:ui' as ui; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/physics.dart'; +import 'binding.dart' show WidgetsBinding; import 'overscroll_indicator.dart'; import 'scroll_metrics.dart'; import 'scroll_simulation.dart'; @@ -174,8 +174,8 @@ class ScrollPhysics { static final Tolerance _kDefaultTolerance = Tolerance( // TODO(ianh): Handle the case of the device pixel ratio changing. // TODO(ianh): Get this from the local MediaQuery not dart:ui's window object. - velocity: 1.0 / (0.050 * ui.window.devicePixelRatio), // logical pixels per second - distance: 1.0 / ui.window.devicePixelRatio // logical pixels + velocity: 1.0 / (0.050 * WidgetsBinding.instance.window.devicePixelRatio), // logical pixels per second + distance: 1.0 / WidgetsBinding.instance.window.devicePixelRatio // logical pixels ); /// The tolerance to use for ballistic simulations. diff --git a/packages/flutter/lib/src/widgets/semantics_debugger.dart b/packages/flutter/lib/src/widgets/semantics_debugger.dart index 2001c73bdfa..90be139e857 100644 --- a/packages/flutter/lib/src/widgets/semantics_debugger.dart +++ b/packages/flutter/lib/src/widgets/semantics_debugger.dart @@ -4,7 +4,6 @@ import 'dart:math' as math; import 'dart:ui' show SemanticsFlag; -import 'dart:ui' as ui show window; import 'package:flutter/foundation.dart'; import 'package:flutter/scheduler.dart'; @@ -89,7 +88,7 @@ class _SemanticsDebuggerState extends State with WidgetsBindi Offset _lastPointerDownLocation; void _handlePointerDown(PointerDownEvent event) { setState(() { - _lastPointerDownLocation = event.position * ui.window.devicePixelRatio; + _lastPointerDownLocation = event.position * WidgetsBinding.instance.window.devicePixelRatio; }); // TODO(ianh): Use a gesture recognizer so that we can reset the // _lastPointerDownLocation when none of the other gesture recognizers win. @@ -150,7 +149,7 @@ class _SemanticsDebuggerState extends State with WidgetsBindi _pipelineOwner, _client.generation, _lastPointerDownLocation, // in physical pixels - ui.window.devicePixelRatio, + WidgetsBinding.instance.window.devicePixelRatio, ), child: GestureDetector( behavior: HitTestBehavior.opaque, diff --git a/packages/flutter/lib/src/widgets/widget_inspector.dart b/packages/flutter/lib/src/widgets/widget_inspector.dart index 3d65db87e77..ffe9b3a966b 100644 --- a/packages/flutter/lib/src/widgets/widget_inspector.dart +++ b/packages/flutter/lib/src/widgets/widget_inspector.dart @@ -10,7 +10,6 @@ import 'dart:math' as math; import 'dart:typed_data'; import 'dart:ui' as ui show - window, ClipOp, EngineLayer, Image, @@ -2294,7 +2293,7 @@ class _WidgetInspectorState extends State // on the edge of the display. If the pointer is being dragged off the edge // of the display we do not want to select anything. A user can still select // a widget that is only at the exact screen margin by tapping. - final Rect bounds = (Offset.zero & (ui.window.physicalSize / ui.window.devicePixelRatio)).deflate(_kOffScreenMargin); + final Rect bounds = (Offset.zero & (WidgetsBinding.instance.window.physicalSize / WidgetsBinding.instance.window.devicePixelRatio)).deflate(_kOffScreenMargin); if (!bounds.contains(_lastPointerLocation)) { setState(() { selection.clear(); diff --git a/packages/flutter/test/rendering/independent_layout_test.dart b/packages/flutter/test/rendering/independent_layout_test.dart index dfd22e3a2e6..4a063f7e6cb 100644 --- a/packages/flutter/test/rendering/independent_layout_test.dart +++ b/packages/flutter/test/rendering/independent_layout_test.dart @@ -44,7 +44,7 @@ void main() { expect(offscreen.child.hasSize, isFalse); expect(offscreen.painted, isFalse); // Attach the offscreen to a custom render view and owner - final RenderView renderView = RenderView(configuration: testConfiguration); + final RenderView renderView = RenderView(configuration: testConfiguration, window: null); final PipelineOwner pipelineOwner = PipelineOwner(); renderView.attach(pipelineOwner); renderView.child = offscreen.root; @@ -73,7 +73,7 @@ void main() { expect(offscreen.child.hasSize, isFalse); expect(offscreen.painted, isFalse); // Attach the offscreen to a custom render view and owner - final RenderView renderView = RenderView(configuration: testConfiguration); + final RenderView renderView = RenderView(configuration: testConfiguration, window: null); final PipelineOwner pipelineOwner = PipelineOwner(); renderView.attach(pipelineOwner); renderView.child = offscreen.root; diff --git a/packages/flutter/test/rendering/object_test.dart b/packages/flutter/test/rendering/object_test.dart index 3d3ec89ac70..b8e0d568369 100644 --- a/packages/flutter/test/rendering/object_test.dart +++ b/packages/flutter/test/rendering/object_test.dart @@ -4,10 +4,14 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; -import '../flutter_test_alternative.dart'; +import 'package:flutter_test/src/binding.dart' show TestWidgetsFlutterBinding; +import 'package:flutter_test/flutter_test.dart'; void main() { test('ensure frame is scheduled for markNeedsSemanticsUpdate', () { + // Initialize all bindings because owner.flushSemantics() requires a window + TestWidgetsFlutterBinding.ensureInitialized(); + final TestRenderObject renderObject = TestRenderObject(); int onNeedVisualUpdateCallCount = 0; final PipelineOwner owner = PipelineOwner(onNeedVisualUpdate: () { diff --git a/packages/flutter/test/services/platform_messages_test.dart b/packages/flutter/test/services/platform_messages_test.dart index 70d20bd74ba..14b590553be 100644 --- a/packages/flutter/test/services/platform_messages_test.dart +++ b/packages/flutter/test/services/platform_messages_test.dart @@ -5,10 +5,13 @@ import 'dart:typed_data'; import 'package:flutter/services.dart'; -import '../flutter_test_alternative.dart'; +import 'package:flutter_test/flutter_test.dart'; void main() { test('Mock binary message handler control test', () async { + // Initialize all bindings because BinaryMessages.send() needs a window. + TestWidgetsFlutterBinding.ensureInitialized(); + final List log = []; BinaryMessages.setMockMessageHandler('test1', (ByteData message) async { diff --git a/packages/flutter/test/widgets/independent_widget_layout_test.dart b/packages/flutter/test/widgets/independent_widget_layout_test.dart index 2f2d8af13b5..b1861fd9937 100644 --- a/packages/flutter/test/widgets/independent_widget_layout_test.dart +++ b/packages/flutter/test/widgets/independent_widget_layout_test.dart @@ -9,7 +9,10 @@ import 'package:flutter/rendering.dart'; const Size _kTestViewSize = Size(800.0, 600.0); class OffscreenRenderView extends RenderView { - OffscreenRenderView() : super(configuration: const ViewConfiguration(size: _kTestViewSize)); + OffscreenRenderView() : super( + configuration: const ViewConfiguration(size: _kTestViewSize), + window: WidgetsBinding.instance.window, + ); @override void compositeFrame() { diff --git a/packages/flutter/test/widgets/media_query_test.dart b/packages/flutter/test/widgets/media_query_test.dart index ca9f98a78b5..9e0f97df34a 100644 --- a/packages/flutter/test/widgets/media_query_test.dart +++ b/packages/flutter/test/widgets/media_query_test.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui' as ui; - import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/widgets.dart'; @@ -39,10 +37,10 @@ void main() { }); testWidgets('MediaQueryData is sane', (WidgetTester tester) async { - final MediaQueryData data = MediaQueryData.fromWindow(ui.window); + final MediaQueryData data = MediaQueryData.fromWindow(WidgetsBinding.instance.window); expect(data, hasOneLineDescription); expect(data.hashCode, equals(data.copyWith().hashCode)); - expect(data.size, equals(ui.window.physicalSize / ui.window.devicePixelRatio)); + expect(data.size, equals(WidgetsBinding.instance.window.physicalSize / WidgetsBinding.instance.window.devicePixelRatio)); expect(data.accessibleNavigation, false); expect(data.invertColors, false); expect(data.disableAnimations, false); @@ -50,7 +48,7 @@ void main() { }); testWidgets('MediaQueryData.copyWith defaults to source', (WidgetTester tester) async { - final MediaQueryData data = MediaQueryData.fromWindow(ui.window); + final MediaQueryData data = MediaQueryData.fromWindow(WidgetsBinding.instance.window); final MediaQueryData copied = data.copyWith(); expect(copied.size, data.size); expect(copied.devicePixelRatio, data.devicePixelRatio); @@ -65,7 +63,7 @@ void main() { }); testWidgets('MediaQuery.copyWith copies specified values', (WidgetTester tester) async { - final MediaQueryData data = MediaQueryData.fromWindow(ui.window); + final MediaQueryData data = MediaQueryData.fromWindow(WidgetsBinding.instance.window); final MediaQueryData copied = data.copyWith( size: const Size(3.14, 2.72), devicePixelRatio: 1.41, diff --git a/packages/flutter_test/lib/flutter_test.dart b/packages/flutter_test/lib/flutter_test.dart index 61e439a1841..177a959fe6d 100644 --- a/packages/flutter_test/lib/flutter_test.dart +++ b/packages/flutter_test/lib/flutter_test.dart @@ -62,3 +62,4 @@ export 'src/test_pointer.dart'; export 'src/test_text_input.dart'; export 'src/test_vsync.dart'; export 'src/widget_tester.dart'; +export 'src/window.dart'; diff --git a/packages/flutter_test/lib/src/accessibility.dart b/packages/flutter_test/lib/src/accessibility.dart index 9676c585c6c..9d0d3db61cf 100644 --- a/packages/flutter_test/lib/src/accessibility.dart +++ b/packages/flutter_test/lib/src/accessibility.dart @@ -108,11 +108,11 @@ class MinimumTapTargetGuideline extends AccessibilityGuideline { const double delta = 0.001; if (paintBounds.left <= delta || paintBounds.top <= delta - || (paintBounds.bottom - ui.window.physicalSize.height).abs() <= delta - || (paintBounds.right - ui.window.physicalSize.width).abs() <= delta) + || (paintBounds.bottom - tester.binding.window.physicalSize.height).abs() <= delta + || (paintBounds.right - tester.binding.window.physicalSize.width).abs() <= delta) return result; // shrink by device pixel ratio. - final Size candidateSize = paintBounds.size / ui.window.devicePixelRatio; + final Size candidateSize = paintBounds.size / tester.binding.window.devicePixelRatio; if (candidateSize.width < size.width || candidateSize.height < size.height) result += Evaluation.fail( '$node: expected tap target size of at least $size, but found $candidateSize\n' diff --git a/packages/flutter_test/lib/src/binding.dart b/packages/flutter_test/lib/src/binding.dart index c11b41a0f0a..35595eb5af2 100644 --- a/packages/flutter_test/lib/src/binding.dart +++ b/packages/flutter_test/lib/src/binding.dart @@ -13,6 +13,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; 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:test_api/test_api.dart' as test_package; @@ -96,12 +97,16 @@ abstract class TestWidgetsFlutterBinding extends BindingBase /// /// This constructor overrides the [debugPrint] global hook to point to /// [debugPrintOverride], which can be overridden by subclasses. - TestWidgetsFlutterBinding() { + TestWidgetsFlutterBinding() : _window = TestWindow(window: ui.window) { debugPrint = debugPrintOverride; debugDisableShadows = disableShadows; debugCheckIntrinsicSizes = checkIntrinsicSizes; } + @override + TestWindow get window => _window; + final TestWindow _window; + /// The value to set [debugPrint] to while tests are running. /// /// This can be used to redirect console output from the framework, or to @@ -265,8 +270,8 @@ abstract class TestWidgetsFlutterBinding extends BindingBase @override ViewConfiguration createViewConfiguration() { - final double devicePixelRatio = ui.window.devicePixelRatio; - final Size size = _surfaceSize ?? ui.window.physicalSize / devicePixelRatio; + final double devicePixelRatio = window.devicePixelRatio; + final Size size = _surfaceSize ?? window.physicalSize / devicePixelRatio; return ViewConfiguration( size: size, devicePixelRatio: devicePixelRatio, @@ -665,8 +670,8 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { @override void initInstances() { super.initInstances(); - ui.window.onBeginFrame = null; - ui.window.onDrawFrame = null; + window.onBeginFrame = null; + window.onDrawFrame = null; } FakeAsync _currentFakeAsync; // set in runTest; cleared in postTest @@ -1145,7 +1150,7 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { _pendingFrame = null; _expectingFrame = false; } else if (framePolicy != LiveTestWidgetsFlutterBindingFramePolicy.benchmark) { - ui.window.scheduleFrame(); + window.scheduleFrame(); } } @@ -1155,6 +1160,7 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { renderView = _LiveTestRenderView( configuration: createViewConfiguration(), onNeedPaint: _handleViewNeedsPaint, + window: window, ); renderView.scheduleInitialFrame(); } @@ -1286,7 +1292,10 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { @override ViewConfiguration createViewConfiguration() { - return TestViewConfiguration(size: _surfaceSize ?? _kDefaultTestViewportSize); + return TestViewConfiguration( + size: _surfaceSize ?? _kDefaultTestViewportSize, + window: window, + ); } @override @@ -1310,15 +1319,18 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { /// size onto the actual display using the [BoxFit.contain] algorithm. class TestViewConfiguration extends ViewConfiguration { /// Creates a [TestViewConfiguration] with the given size. Defaults to 800x600. - TestViewConfiguration({ Size size = _kDefaultTestViewportSize }) - : _paintMatrix = _getMatrix(size, ui.window.devicePixelRatio), - _hitTestMatrix = _getMatrix(size, 1.0), + TestViewConfiguration({ + Size size = _kDefaultTestViewportSize, + ui.Window window, + }) + : _paintMatrix = _getMatrix(size, window.devicePixelRatio, window), + _hitTestMatrix = _getMatrix(size, 1.0, window), super(size: size); - static Matrix4 _getMatrix(Size size, double devicePixelRatio) { - final double inverseRatio = devicePixelRatio / ui.window.devicePixelRatio; - final double actualWidth = ui.window.physicalSize.width * inverseRatio; - final double actualHeight = ui.window.physicalSize.height * inverseRatio; + static Matrix4 _getMatrix(Size size, double devicePixelRatio, ui.Window window) { + final double inverseRatio = devicePixelRatio / window.devicePixelRatio; + final double actualWidth = window.physicalSize.width * inverseRatio; + final double actualHeight = window.physicalSize.height * inverseRatio; final double desiredWidth = size.width; final double desiredHeight = size.height; double scale, shiftX, shiftY; @@ -1377,7 +1389,8 @@ class _LiveTestRenderView extends RenderView { _LiveTestRenderView({ ViewConfiguration configuration, this.onNeedPaint, - }) : super(configuration: configuration); + @required ui.Window window, + }) : super(configuration: configuration, window: window); @override TestViewConfiguration get configuration => super.configuration; @@ -1714,4 +1727,4 @@ class _MockHttpHeaders extends HttpHeaders { @override String value(String name) => null; -} +} \ No newline at end of file diff --git a/packages/flutter_test/lib/src/window.dart b/packages/flutter_test/lib/src/window.dart new file mode 100644 index 00000000000..5f8beeef3e0 --- /dev/null +++ b/packages/flutter_test/lib/src/window.dart @@ -0,0 +1,303 @@ +// 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:typed_data' show ByteData; +import 'dart:ui' hide window; + +import 'package:meta/meta.dart'; + +/// [Window] that wraps another [Window] and allows faking of some properties +/// for testing purposes. +/// +/// Tests for certain widgets, e.g., [MaterialApp], might require faking certain +/// properties of a [Window]. [TestWindow] facilitates the faking of these properties +/// by overidding the properties of a real [Window] with desired fake values. The +/// binding used within tests, [TestWidgetsFlutterBinding], contains a [TestWindow] +/// that is used by all tests. +/// +/// ## Sample Code +/// +/// A test can utilize a [TestWindow] in the following way: +/// +/// ```dart +/// testWidgets('your test name here', (WidgetTester tester) async { +/// // Retrieve the TestWidgetsFlutterBinding. +/// final TestWidgetsFlutterBinding testBinding = tester.binding; +/// +/// // Fake the desired properties of the TestWindow. All code running +/// // within this test will perceive the following fake text scale +/// // factor as the real text scale factor of the window. +/// testBinding.window.textScaleFactorFakeValue = 2.5; +/// +/// // Test code that depends on text scale factor here. +/// }); +/// ``` +/// +/// The [TestWidgetsFlutterBinding] is recreated for each test and +/// therefore any fake values defined in one test will not persist +/// to the next. +/// +/// If a test needs to override a real [Window] property and then later +/// return to using the real [Window] property, [TestWindow] provides +/// methods to clear each individual test value, e.g., [clearLocaleTestValue()]. +/// +/// To clear all fake test values in a [TestWindow], consider using [clearAllTestValues()]. +class TestWindow implements Window { + /// Constructs a [TestWindow] that defers all behavior to the given [window] unless + /// explicitly overidden for test purposes. + TestWindow({ + @required Window window, + }) : _window = window; + + /// The [Window] that is wrapped by this [TestWindow]. + final Window _window; + + @override + double get devicePixelRatio => _devicePixelRatio ?? _window.devicePixelRatio; + double _devicePixelRatio; + /// Hides the real device pixel ratio and reports the given [devicePixelRatio] instead. + set devicePixelRatioTestValue(double devicePixelRatio) { + _devicePixelRatio = devicePixelRatio; + } + /// Deletes any existing test device pixel ratio and returns to using the real device pixel ratio. + void clearDevicePixelRatioTestValue() { + _devicePixelRatio = null; + } + + @override + Size get physicalSize => _physicalSizeTestValue ?? _window.physicalSize; + Size _physicalSizeTestValue; + /// Hides the real physical size and reports the given [physicalSizeTestValue] instead. + set physicalSizeTestValue (Size physicalSizeTestValue) { + _physicalSizeTestValue = physicalSizeTestValue; + } + /// Deletes any existing test physical size and returns to using the real physical size. + void clearPhysicalSizeTestValue() { + _physicalSizeTestValue = null; + } + + @override + WindowPadding get viewInsets => _viewInsetsTestValue ?? _window.viewInsets; + WindowPadding _viewInsetsTestValue; + /// Hides the real view insets and reports the given [viewInsetsTestValue] instead. + set viewInsetsTestValue(WindowPadding viewInsetsTestValue) { + _viewInsetsTestValue = viewInsetsTestValue; + } + /// Deletes any existing test view insets and returns to using the real view insets. + void clearViewInsetsTestValue() { + _viewInsetsTestValue = null; + } + + @override + WindowPadding get padding => _paddingTestValue ?? _window.padding; + WindowPadding _paddingTestValue; + /// Hides the real padding and reports the given [paddingTestValue] instead. + set paddingTestValue(WindowPadding paddingTestValue) { + _paddingTestValue = paddingTestValue; + } + /// Deletes any existing test padding and returns to using the real padding. + void clearPaddingTestValue() { + _paddingTestValue = null; + } + + @override + VoidCallback get onMetricsChanged => _window.onMetricsChanged; + @override + set onMetricsChanged(VoidCallback callback) { + _window.onMetricsChanged = callback; + } + + @override + Locale get locale => _localeTestValue ?? _window.locale; + Locale _localeTestValue; + /// Hides the real locale and reports the given [localeTestValue] instead. + set localeTestValue(Locale localeTestValue) { + _localeTestValue = localeTestValue; + } + /// Deletes any existing test locale and returns to using the real locale. + void clearLocaleTestValue() { + _localeTestValue = null; + } + + @override + List get locales => _localesTestValue ?? _window.locales; + List _localesTestValue; + /// Hides the real locales and reports the given [localesTestValue] instead. + set localesTestValue(List localesTestValue) { + _localesTestValue = localesTestValue; + } + /// Deletes any existing test locales and returns to using the real locales. + void clearLocalesTestValue() { + _localesTestValue = null; + } + + @override + VoidCallback get onLocaleChanged => _window.onLocaleChanged; + @override + set onLocaleChanged(VoidCallback callback) { + _window.onLocaleChanged = callback; + } + + @override + double get textScaleFactor => _textScaleFactorTestValue ?? _window.textScaleFactor; + double _textScaleFactorTestValue; + /// Hides the real text scale factor and reports the given [textScaleFactorTestValue] instead. + set textScaleFactorTestValue(double textScaleFactorTestValue) { + _textScaleFactorTestValue = textScaleFactorTestValue; + } + /// Deletes any existing test text scale factor and returns to using the real text scale factor. + void clearTextScaleFactorTestValue() { + _textScaleFactorTestValue = null; + } + + @override + bool get alwaysUse24HourFormat => _alwaysUse24HourFormatTestValue ?? _window.alwaysUse24HourFormat; + bool _alwaysUse24HourFormatTestValue; + /// Hides the real clock format and reports the given [alwaysUse24HourFormatTestValue] instead. + set alwaysUse24HourFormatTestValue(bool alwaysUse24HourFormatTestValue) { + _alwaysUse24HourFormatTestValue = alwaysUse24HourFormatTestValue; + } + /// Deletes any existing test clock format and returns to using the real clock format. + void clearAlwaysUse24HourTestValue() { + _alwaysUse24HourFormatTestValue = null; + } + + @override + VoidCallback get onTextScaleFactorChanged => _window.onTextScaleFactorChanged; + @override + set onTextScaleFactorChanged(VoidCallback callback) { + _window.onTextScaleFactorChanged = callback; + } + + @override + FrameCallback get onBeginFrame => _window.onBeginFrame; + @override + set onBeginFrame(FrameCallback callback) { + _window.onBeginFrame = callback; + } + + @override + VoidCallback get onDrawFrame => _window.onDrawFrame; + @override + set onDrawFrame(VoidCallback callback) { + _window.onDrawFrame = callback; + } + + @override + PointerDataPacketCallback get onPointerDataPacket => _window.onPointerDataPacket; + @override + set onPointerDataPacket(PointerDataPacketCallback callback) { + _window.onPointerDataPacket = callback; + } + + @override + String get defaultRouteName => _defaultRouteNameTestValue ?? _window.defaultRouteName; + String _defaultRouteNameTestValue; + /// Hides the real default route name and reports the given [defaultRouteNameTestValue] instead. + set defaultRouteNameTestValue(String defaultRouteNameTestValue) { + _defaultRouteNameTestValue = defaultRouteNameTestValue; + } + /// Deletes any existing test default route name and returns to using the real default route name. + void clearDefaultRouteNameTestValue() { + _defaultRouteNameTestValue = null; + } + + @override + void scheduleFrame() { + _window.scheduleFrame(); + } + + @override + void render(Scene scene) { + _window.render(scene); + } + + @override + bool get semanticsEnabled => _semanticsEnabledTestValue ?? _window.semanticsEnabled; + bool _semanticsEnabledTestValue; + /// Hides the real semantics enabled and reports the given [semanticsEnabledTestValue] instead. + set semanticsEnabledTestValue(bool semanticsEnabledTestValue) { + _semanticsEnabledTestValue = semanticsEnabledTestValue; + } + /// Deletes any existing test semantics enabled and returns to using the real semantics enabled. + void clearSemanticsEnabledTestValue() { + _semanticsEnabledTestValue = null; + } + + @override + VoidCallback get onSemanticsEnabledChanged => _window.onSemanticsEnabledChanged; + @override + set onSemanticsEnabledChanged(VoidCallback callback) { + _window.onSemanticsEnabledChanged = callback; + } + + @override + SemanticsActionCallback get onSemanticsAction => _window.onSemanticsAction; + @override + set onSemanticsAction(SemanticsActionCallback callback) { + _window.onSemanticsAction = callback; + } + + @override + AccessibilityFeatures get accessibilityFeatures => _accessibilityFeaturesTestValue ?? _window.accessibilityFeatures; + AccessibilityFeatures _accessibilityFeaturesTestValue; + /// Hides the real accessibility features and reports the given [accessibilityFeaturesTestValue] instead. + set accessibilityFeaturesTestValue(AccessibilityFeatures accessibilityFeaturesTestValue) { + _accessibilityFeaturesTestValue = accessibilityFeaturesTestValue; + } + /// Deletes any existing test accessibility features and returns to using the real accessibility features. + void clearAccessibilityFeaturesTestValue() { + _accessibilityFeaturesTestValue = null; + } + + @override + VoidCallback get onAccessibilityFeaturesChanged => _window.onAccessibilityFeaturesChanged; + @override + set onAccessibilityFeaturesChanged(VoidCallback callback) { + _window.onAccessibilityFeaturesChanged = callback; + } + + @override + void updateSemantics(SemanticsUpdate update) { + _window.updateSemantics(update); + } + + @override + void setIsolateDebugName(String name) { + _window.setIsolateDebugName(name); + } + + @override + void sendPlatformMessage(String name, + ByteData data, + PlatformMessageResponseCallback callback) { + _window.sendPlatformMessage(name, data, callback); + } + + @override + PlatformMessageCallback get onPlatformMessage => _window.onPlatformMessage; + @override + set onPlatformMessage(PlatformMessageCallback callback) { + _window.onPlatformMessage = callback; + } + + /// Delete any test value properties that have been set on this [TestWindow] + /// and return to reporting the real [Window] values for all [Window] properties. + /// + /// If desired, clearing of properties can be done on an individual basis, e.g., + /// [clearLocaleTestValue()]. + void clearAllTestValues() { + clearAccessibilityFeaturesTestValue(); + clearAlwaysUse24HourTestValue(); + clearDefaultRouteNameTestValue(); + clearDevicePixelRatioTestValue(); + clearLocaleTestValue(); + clearLocalesTestValue(); + clearPaddingTestValue(); + clearPhysicalSizeTestValue(); + clearSemanticsEnabledTestValue(); + clearTextScaleFactorTestValue(); + clearViewInsetsTestValue(); + } +} \ No newline at end of file diff --git a/packages/flutter_test/test/window_test.dart b/packages/flutter_test/test/window_test.dart new file mode 100644 index 00000000000..11b317b9fe3 --- /dev/null +++ b/packages/flutter_test/test/window_test.dart @@ -0,0 +1,256 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ui' as ui show window; +import 'dart:ui' show Size, Locale, WindowPadding, AccessibilityFeatures; + +import 'package:flutter/widgets.dart' show WidgetsBinding; +import 'package:flutter_test/flutter_test.dart'; +import 'package:meta/meta.dart'; + +void main() { + testWidgets('TestWindow can fake device pixel ratio', (WidgetTester tester) async { + verifyThatTestWindowCanFakeProperty( + tester: tester, + realValue: ui.window.devicePixelRatio, + fakeValue: 2.5, + propertyRetriever: () { + return WidgetsBinding.instance.window.devicePixelRatio; + }, + propertyFaker: (TestWidgetsFlutterBinding binding, double fakeValue) { + binding.window.devicePixelRatioTestValue = fakeValue; + } + ); + }); + + testWidgets('TestWindow can fake physical size', (WidgetTester tester) async { + verifyThatTestWindowCanFakeProperty( + tester: tester, + realValue: ui.window.physicalSize, + fakeValue: const Size(50, 50), + propertyRetriever: () { + return WidgetsBinding.instance.window.physicalSize; + }, + propertyFaker: (TestWidgetsFlutterBinding binding, Size fakeValue) { + binding.window.physicalSizeTestValue = fakeValue; + } + ); + }); + + testWidgets('TestWindow can fake view insets', (WidgetTester tester) async { + verifyThatTestWindowCanFakeProperty( + tester: tester, + realValue: ui.window.viewInsets, + fakeValue: const FakeWindowPadding(), + propertyRetriever: () { + return WidgetsBinding.instance.window.viewInsets; + }, + propertyFaker: (TestWidgetsFlutterBinding binding, WindowPadding fakeValue) { + binding.window.viewInsetsTestValue = fakeValue; + } + ); + }); + + testWidgets('TestWindow can fake padding', (WidgetTester tester) async { + verifyThatTestWindowCanFakeProperty( + tester: tester, + realValue: ui.window.padding, + fakeValue: const FakeWindowPadding(), + propertyRetriever: () { + return WidgetsBinding.instance.window.padding; + }, + propertyFaker: (TestWidgetsFlutterBinding binding, WindowPadding fakeValue) { + binding.window.paddingTestValue = fakeValue; + } + ); + }); + + testWidgets('TestWindow can fake locale', (WidgetTester tester) async { + verifyThatTestWindowCanFakeProperty( + tester: tester, + realValue: ui.window.locale, + fakeValue: const Locale('fake_language_code'), + propertyRetriever: () { + return WidgetsBinding.instance.window.locale; + }, + propertyFaker: (TestWidgetsFlutterBinding binding, Locale fakeValue) { + binding.window.localeTestValue = fakeValue; + } + ); + }); + + testWidgets('TestWindow can fake locales', (WidgetTester tester) async { + verifyThatTestWindowCanFakeProperty>( + tester: tester, + realValue: ui.window.locales, + fakeValue: [const Locale('fake_language_code')], + propertyRetriever: () { + return WidgetsBinding.instance.window.locales; + }, + propertyFaker: (TestWidgetsFlutterBinding binding, List fakeValue) { + binding.window.localesTestValue = fakeValue; + } + ); + }); + + testWidgets('TestWindow can fake text scale factor', (WidgetTester tester) async { + verifyThatTestWindowCanFakeProperty( + tester: tester, + realValue: ui.window.textScaleFactor, + fakeValue: 2.5, + propertyRetriever: () { + return WidgetsBinding.instance.window.textScaleFactor; + }, + propertyFaker: (TestWidgetsFlutterBinding binding, double fakeValue) { + binding.window.textScaleFactorTestValue = fakeValue; + } + ); + }); + + testWidgets('TestWindow can fake clock format', (WidgetTester tester) async { + verifyThatTestWindowCanFakeProperty( + tester: tester, + realValue: ui.window.alwaysUse24HourFormat, + fakeValue: !ui.window.alwaysUse24HourFormat, + propertyRetriever: () { + return WidgetsBinding.instance.window.alwaysUse24HourFormat; + }, + propertyFaker: (TestWidgetsFlutterBinding binding, bool fakeValue) { + binding.window.alwaysUse24HourFormatTestValue = fakeValue; + } + ); + }); + + testWidgets('TestWindow can fake default route name', (WidgetTester tester) async { + verifyThatTestWindowCanFakeProperty( + tester: tester, + realValue: ui.window.defaultRouteName, + fakeValue: 'fake_route', + propertyRetriever: () { + return WidgetsBinding.instance.window.defaultRouteName; + }, + propertyFaker: (TestWidgetsFlutterBinding binding, String fakeValue) { + binding.window.defaultRouteNameTestValue = fakeValue; + } + ); + }); + + testWidgets('TestWindow can fake semantics enabled', (WidgetTester tester) async { + verifyThatTestWindowCanFakeProperty( + tester: tester, + realValue: ui.window.semanticsEnabled, + fakeValue: !ui.window.semanticsEnabled, + propertyRetriever: () { + return WidgetsBinding.instance.window.semanticsEnabled; + }, + propertyFaker: (TestWidgetsFlutterBinding binding, bool fakeValue) { + binding.window.semanticsEnabledTestValue = fakeValue; + } + ); + }); + + testWidgets('TestWindow can fake accessibility features', (WidgetTester tester) async { + verifyThatTestWindowCanFakeProperty( + tester: tester, + realValue: ui.window.accessibilityFeatures, + fakeValue: const FakeAccessibilityFeatures(), + propertyRetriever: () { + return WidgetsBinding.instance.window.accessibilityFeatures; + }, + propertyFaker: (TestWidgetsFlutterBinding binding, AccessibilityFeatures fakeValue) { + binding.window.accessibilityFeaturesTestValue = fakeValue; + } + ); + }); + + testWidgets('TestWindow can clear out fake properties all at once', (WidgetTester tester) { + final double originalDevicePixelRatio = ui.window.devicePixelRatio; + final double originalTextScaleFactor = ui.window.textScaleFactor; + final TestWindow testWindow = retrieveTestBinding(tester).window; + + // Set fake values for window properties. + testWindow.devicePixelRatioTestValue = 2.5; + testWindow.textScaleFactorTestValue = 3.0; + + // Erase fake window property values. + testWindow.clearAllTestValues(); + + // Verify that the window once again reports real property values. + expect(WidgetsBinding.instance.window.devicePixelRatio, originalDevicePixelRatio); + expect(WidgetsBinding.instance.window.textScaleFactor, originalTextScaleFactor); + }); +} + +void verifyThatTestWindowCanFakeProperty({ + @required WidgetTester tester, + @required WindowPropertyType realValue, + @required WindowPropertyType fakeValue, + @required WindowPropertyType Function() propertyRetriever, + @required Function(TestWidgetsFlutterBinding, WindowPropertyType fakeValue) propertyFaker, +}) { + WindowPropertyType propertyBeforeFaking; + WindowPropertyType propertyAfterFaking; + + propertyBeforeFaking = propertyRetriever(); + + propertyFaker(retrieveTestBinding(tester), fakeValue); + + propertyAfterFaking = propertyRetriever(); + + expect(propertyBeforeFaking, realValue); + expect(propertyAfterFaking, fakeValue); +} + +TestWidgetsFlutterBinding retrieveTestBinding(WidgetTester tester) { + final WidgetsBinding binding = tester.binding; + assert(binding is TestWidgetsFlutterBinding); + final TestWidgetsFlutterBinding testBinding = binding; + return testBinding; +} + +class FakeWindowPadding implements WindowPadding { + const FakeWindowPadding({ + this.left = 0.0, + this.top = 0.0, + this.right = 0.0, + this.bottom = 0.0, + }); + + @override + final double left; + + @override + final double top; + + @override + final double right; + + @override + final double bottom; +} + +class FakeAccessibilityFeatures implements AccessibilityFeatures { + const FakeAccessibilityFeatures({ + this.accessibleNavigation = false, + this.invertColors = false, + this.disableAnimations = false, + this.boldText = false, + this.reduceMotion = false, + }); + + @override + final bool accessibleNavigation; + + @override + final bool invertColors; + + @override + final bool disableAnimations; + + @override + final bool boldText; + + @override + final bool reduceMotion; +} \ No newline at end of file