| // 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:io'; |
| import 'dart:ui' as ui; |
| |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/gestures.dart'; |
| import 'package:flutter/rendering.dart'; |
| import 'package:flutter/scheduler.dart'; |
| import 'package:flutter/services.dart'; |
| import 'package:flutter/widgets.dart'; |
| import 'package:http/http.dart' as http; |
| import 'package:http/testing.dart' as http; |
| import 'package:quiver/testing/async.dart'; |
| import 'package:quiver/time.dart'; |
| import 'package:test/test.dart' as test_package; |
| import 'package:stack_trace/stack_trace.dart' as stack_trace; |
| import 'package:vector_math/vector_math_64.dart'; |
| |
| import 'stack_manipulation.dart'; |
| import 'test_async_utils.dart'; |
| import 'test_text_input.dart'; |
| |
| /// Phases that can be reached by [WidgetTester.pumpWidget] and |
| /// [TestWidgetsFlutterBinding.pump]. |
| /// |
| /// See [WidgetsBinding.drawFrame] for a more detailed description of some of |
| /// these phases. |
| enum EnginePhase { |
| /// The build phase in the widgets library. See [BuildOwner.buildScope]. |
| build, |
| |
| /// The layout phase in the rendering library. See [PipelineOwner.flushLayout]. |
| layout, |
| |
| /// The compositing bits update phase in the rendering library. See |
| /// [PipelineOwner.flushCompositingBits]. |
| compositingBits, |
| |
| /// The paint phase in the rendering library. See [PipelineOwner.flushPaint]. |
| paint, |
| |
| /// The compositing phase in the rendering library. See |
| /// [RenderView.compositeFrame]. This is the phase in which data is sent to |
| /// the GPU. If semantics are not enabled, then this is the last phase. |
| composite, |
| |
| /// The semantics building phase in the rendering library. See |
| /// [PipelineOwner.flushSemantics]. |
| flushSemantics, |
| |
| /// The final phase in the rendering library, wherein semantics information is |
| /// sent to the embedder. See [SemanticsOwner.sendSemanticsUpdate]. |
| sendSemanticsUpdate, |
| } |
| |
| /// Parts of the system that can generate pointer events that reach the test |
| /// binding. |
| /// |
| /// This is used to identify how to handle events in the |
| /// [LiveTestWidgetsFlutterBinding]. See |
| /// [TestWidgetsFlutterBinding.dispatchEvent]. |
| enum TestBindingEventSource { |
| /// The pointer event came from the test framework itself, e.g. from a |
| /// [TestGesture] created by [WidgetTester.startGesture]. |
| test, |
| |
| /// The pointer event came from the system, presumably as a result of the user |
| /// interactive directly with the device while the test was running. |
| device, |
| } |
| |
| const Size _kDefaultTestViewportSize = const Size(800.0, 600.0); |
| |
| /// Base class for bindings used by widgets library tests. |
| /// |
| /// The [ensureInitialized] method creates (if necessary) and returns |
| /// an instance of the appropriate subclass. |
| /// |
| /// When using these bindings, certain features are disabled. For |
| /// example, [timeDilation] is reset to 1.0 on initialization. |
| abstract class TestWidgetsFlutterBinding extends BindingBase |
| with SchedulerBinding, |
| GestureBinding, |
| RendererBinding, |
| // Services binding omitted to avoid dragging in the licenses code. |
| WidgetsBinding { |
| |
| /// Constructor for [TestWidgetsFlutterBinding]. |
| /// |
| /// This constructor overrides the [debugPrint] global hook to point to |
| /// [debugPrintOverride], which can be overridden by subclasses. |
| TestWidgetsFlutterBinding() { |
| debugPrint = debugPrintOverride; |
| } |
| |
| @protected |
| DebugPrintCallback get debugPrintOverride => debugPrint; |
| |
| /// Creates and initializes the binding. This function is |
| /// idempotent; calling it a second time will just return the |
| /// previously-created instance. |
| /// |
| /// This function will use [AutomatedTestWidgetsFlutterBinding] if |
| /// the test was run using `flutter test`, and |
| /// [LiveTestWidgetsFlutterBinding] otherwise (e.g. if it was run |
| /// using `flutter run`). (This is determined by looking at the |
| /// environment variables for a variable called `FLUTTER_TEST`.) |
| static WidgetsBinding ensureInitialized() { |
| if (WidgetsBinding.instance == null) { |
| if (Platform.environment.containsKey('FLUTTER_TEST')) { |
| new AutomatedTestWidgetsFlutterBinding(); |
| } else { |
| new LiveTestWidgetsFlutterBinding(); |
| } |
| } |
| assert(WidgetsBinding.instance is TestWidgetsFlutterBinding); |
| return WidgetsBinding.instance; |
| } |
| |
| @override |
| void initInstances() { |
| timeDilation = 1.0; // just in case the developer has artificially changed it for development |
| createHttpClient = () { |
| return new http.MockClient((http.BaseRequest request) { |
| return new Future<http.Response>.value( |
| new http.Response("Mocked: Unavailable.", 404, request: request) |
| ); |
| }); |
| }; |
| _testTextInput = new TestTextInput()..register(); |
| super.initInstances(); |
| } |
| |
| /// Whether there is currently a test executing. |
| bool get inTest; |
| |
| /// The number of outstanding microtasks in the queue. |
| int get microtaskCount; |
| |
| /// The default test timeout for tests when using this binding. |
| test_package.Timeout get defaultTestTimeout; |
| |
| /// The current time. |
| /// |
| /// In the automated test environment (`flutter test`), this is a fake clock |
| /// that begins in January 2015 at the start of the test and advances each |
| /// time [pump] is called with a non-zero duration. |
| /// |
| /// In the live testing environment (`flutter run`), this object shows the |
| /// actual current wall-clock time. |
| Clock get clock; |
| |
| /// Triggers a frame sequence (build/layout/paint/etc), |
| /// then flushes microtasks. |
| /// |
| /// If duration is set, then advances the clock by that much first. |
| /// Doing this flushes microtasks. |
| /// |
| /// The supplied EnginePhase is the final phase reached during the pump pass; |
| /// if not supplied, the whole pass is executed. |
| /// |
| /// See also [LiveTestWidgetsFlutterBindingFramePolicy], which affects how |
| /// this method works when the test is run with `flutter run`. |
| Future<Null> pump([ Duration duration, EnginePhase newPhase = EnginePhase.sendSemanticsUpdate ]); |
| |
| /// Artificially calls dispatchLocaleChanged on the Widget binding, |
| /// then flushes microtasks. |
| Future<Null> setLocale(String languageCode, String countryCode) { |
| return TestAsyncUtils.guard(() async { |
| assert(inTest); |
| final Locale locale = new Locale(languageCode, countryCode); |
| dispatchLocaleChanged(locale); |
| return null; |
| }); |
| } |
| |
| /// Acts as if the application went idle. |
| /// |
| /// Runs all remaining microtasks, including those scheduled as a result of |
| /// running them, until there are no more microtasks scheduled. Then, runs any |
| /// previously scheduled timers with zero time, and completes the returned future. |
| /// |
| /// May result in an infinite loop or run out of memory if microtasks continue |
| /// to recursively schedule new microtasks. Will not run any timers scheduled |
| /// after this method was invoked, even if they are zero-time timers. |
| Future<Null> idle() { |
| return TestAsyncUtils.guard(() { |
| final Completer<Null> completer = new Completer<Null>(); |
| Timer.run(() { |
| completer.complete(null); |
| }); |
| return completer.future; |
| }); |
| } |
| |
| /// Convert the given point from the global coodinate system (as used by |
| /// pointer events from the device) to the coordinate system used by the |
| /// tests (an 800 by 600 window). |
| Offset globalToLocal(Offset point) => point; |
| |
| /// Convert the given point from the coordinate system used by the tests (an |
| /// 800 by 600 window) to the global coodinate system (as used by pointer |
| /// events from the device). |
| Offset localToGlobal(Offset point) => point; |
| |
| @override |
| void dispatchEvent(PointerEvent event, HitTestResult result, { |
| TestBindingEventSource source: TestBindingEventSource.device |
| }) { |
| assert(source == TestBindingEventSource.test); |
| super.dispatchEvent(event, result); |
| } |
| |
| /// A stub for the system's onscreen keyboard. Callers must set the |
| /// [focusedEditable] before using this value. |
| TestTextInput get testTextInput => _testTextInput; |
| TestTextInput _testTextInput; |
| |
| /// The current client of the onscreen keyboard. Callers must pump |
| /// an additional frame after setting this property to complete the |
| /// the focus change. |
| EditableTextState get focusedEditable => _focusedEditable; |
| EditableTextState _focusedEditable; |
| set focusedEditable(EditableTextState value) { |
| _focusedEditable = value..requestKeyboard(); |
| } |
| |
| /// Returns the exception most recently caught by the Flutter framework. |
| /// |
| /// Call this if you expect an exception during a test. If an exception is |
| /// thrown and this is not called, then the exception is rethrown when |
| /// the [testWidgets] call completes. |
| /// |
| /// If two exceptions are thrown in a row without the first one being |
| /// acknowledged with a call to this method, then when the second exception is |
| /// thrown, they are both dumped to the console and then the second is |
| /// rethrown from the exception handler. This will likely result in the |
| /// framework entering a highly unstable state and everything collapsing. |
| /// |
| /// It's safe to call this when there's no pending exception; it will return |
| /// null in that case. |
| dynamic takeException() { |
| assert(inTest); |
| final dynamic result = _pendingExceptionDetails?.exception; |
| _pendingExceptionDetails = null; |
| return result; |
| } |
| FlutterExceptionHandler _oldExceptionHandler; |
| FlutterErrorDetails _pendingExceptionDetails; |
| |
| static const TextStyle _kMessageStyle = const TextStyle( |
| color: const Color(0xFF917FFF), |
| fontSize: 40.0 |
| ); |
| |
| static final Widget _kPreTestMessage = const Center( |
| child: const Text( |
| 'Test starting...', |
| style: _kMessageStyle |
| ) |
| ); |
| |
| static final Widget _kPostTestMessage = const Center( |
| child: const Text( |
| 'Test finished.', |
| style: _kMessageStyle |
| ) |
| ); |
| |
| /// Whether to include the output of debugDumpApp() when reporting |
| /// test failures. |
| bool showAppDumpInErrors = false; |
| |
| /// Call the testBody inside a [FakeAsync] scope on which [pump] can |
| /// advance time. |
| /// |
| /// Returns a future which completes when the test has run. |
| /// |
| /// Called by the [testWidgets] and [benchmarkWidgets] functions to |
| /// run a test. |
| /// |
| /// The `invariantTester` argument is called after the `testBody`'s [Future] |
| /// completes. If it throws, then the test is marked as failed. |
| /// |
| /// The `description` is used by the [LiveTestWidgetsFlutterBinding] to |
| /// show a label on the screen during the test. The description comes from |
| /// the value passed to [testWidgets]. It must not be null. |
| Future<Null> runTest(Future<Null> testBody(), VoidCallback invariantTester, { String description: '' }); |
| |
| /// This is called during test execution before and after the body has been |
| /// executed. |
| /// |
| /// It's used by [AutomatedTestWidgetsFlutterBinding] to drain the microtasks |
| /// before the final [pump] that happens during test cleanup. |
| void asyncBarrier() { |
| TestAsyncUtils.verifyAllScopesClosed(); |
| } |
| |
| Zone _parentZone; |
| Completer<Null> _currentTestCompleter; |
| |
| void _testCompletionHandler() { |
| // This can get called twice, in the case of a Future without listeners failing, and then |
| // our main future completing. |
| assert(Zone.current == _parentZone); |
| assert(_currentTestCompleter != null); |
| if (_pendingExceptionDetails != null) { |
| FlutterError.dumpErrorToConsole(_pendingExceptionDetails, forceReport: true); |
| // test_package.registerException actually just calls the current zone's error handler (that |
| // is to say, _parentZone's handleUncaughtError function). FakeAsync doesn't add one of those, |
| // but the test package does, that's how the test package tracks errors. So really we could |
| // get the same effect here by calling that error handler directly or indeed just throwing. |
| // However, we call registerException because that's the semantically correct thing... |
| test_package.registerException('Test failed. See exception logs above.', _EmptyStack.instance); |
| _pendingExceptionDetails = null; |
| } |
| if (!_currentTestCompleter.isCompleted) |
| _currentTestCompleter.complete(null); |
| } |
| |
| Future<Null> _runTest(Future<Null> testBody(), VoidCallback invariantTester, String description) { |
| assert(description != null); |
| assert(inTest); |
| _oldExceptionHandler = FlutterError.onError; |
| int _exceptionCount = 0; // number of un-taken exceptions |
| FlutterError.onError = (FlutterErrorDetails details) { |
| if (_pendingExceptionDetails != null) { |
| if (_exceptionCount == 0) { |
| _exceptionCount = 2; |
| FlutterError.dumpErrorToConsole(_pendingExceptionDetails, forceReport: true); |
| } else { |
| _exceptionCount += 1; |
| } |
| FlutterError.dumpErrorToConsole(details, forceReport: true); |
| _pendingExceptionDetails = new FlutterErrorDetails( |
| exception: 'Multiple exceptions ($_exceptionCount) were detected during the running of the current test, and at least one was unexpected.', |
| library: 'Flutter test framework' |
| ); |
| } else { |
| _pendingExceptionDetails = details; |
| } |
| }; |
| _currentTestCompleter = new Completer<Null>(); |
| final ZoneSpecification errorHandlingZoneSpecification = new ZoneSpecification( |
| handleUncaughtError: (Zone self, ZoneDelegate parent, Zone zone, dynamic exception, StackTrace stack) { |
| if (_currentTestCompleter.isCompleted) { |
| // Well this is not a good sign. |
| // Ideally, once the test has failed we would stop getting errors from the test. |
| // However, if someone tries hard enough they could get in a state where this happens. |
| // If we silently dropped these errors on the ground, nobody would ever know. So instead |
| // we report them to the console. They don't cause test failures, but hopefully someone |
| // will see them in the logs at some point. |
| FlutterError.dumpErrorToConsole(new FlutterErrorDetails( |
| exception: exception, |
| stack: _unmangle(stack), |
| context: 'running a test (but after the test had completed)', |
| library: 'Flutter test framework' |
| ), forceReport: true); |
| return; |
| } |
| // This is where test failures, e.g. those in expect(), will end up. |
| // Specifically, runUnaryGuarded() will call this synchronously and |
| // return our return value if _runTestBody fails synchronously (which it |
| // won't, so this never happens), and Future will call this when the |
| // Future completes with an error and it would otherwise call listeners |
| // if the listener is in a different zone (which it would be for the |
| // `whenComplete` handler below), or if the Future completes with an |
| // error and the future has no listeners at all. |
| // This handler further calls the onError handler above, which sets |
| // _pendingExceptionDetails. Nothing gets printed as a result of that |
| // call unless we already had an exception pending, because in general |
| // we want people to be able to cause the framework to report exceptions |
| // and then use takeException to verify that they were really caught. |
| // Now, if we actually get here, this isn't going to be one of those |
| // cases. We only get here if the test has actually failed. So, once |
| // we've carefully reported it, we then immediately end the test by |
| // calling the _testCompletionHandler in the _parentZone. |
| // We have to manually call _testCompletionHandler because if the Future |
| // library calls us, it is maybe _instead_ of calling a registered |
| // listener from a different zone. In our case, that would be instead of |
| // calling the whenComplete() listener below. |
| // We have to call it in the parent zone because if we called it in |
| // _this_ zone, the test framework would find this zone was the current |
| // zone and helpfully throw the error in this zone, causing us to be |
| // directly called again. |
| String treeDump; |
| try { |
| treeDump = renderViewElement?.toStringDeep() ?? '<no tree>'; |
| } catch (exception) { |
| treeDump = '<additional error caught while dumping tree: $exception>'; |
| } |
| final StringBuffer expectLine = new StringBuffer(); |
| final int stackLinesToOmit = reportExpectCall(stack, expectLine); |
| FlutterError.reportError(new FlutterErrorDetails( |
| exception: exception, |
| stack: _unmangle(stack), |
| context: 'running a test', |
| library: 'Flutter test framework', |
| stackFilter: (Iterable<String> frames) { |
| return FlutterError.defaultStackFilter(frames.skip(stackLinesToOmit)); |
| }, |
| informationCollector: (StringBuffer information) { |
| if (stackLinesToOmit > 0) |
| information.writeln(expectLine.toString()); |
| if (showAppDumpInErrors) { |
| information.writeln('At the time of the failure, the widget tree looked as follows:'); |
| information.writeln('# ${treeDump.split("\n").takeWhile((String s) => s != "").join("\n# ")}'); |
| } |
| if (description.isNotEmpty) |
| information.writeln('The test description was:\n$description'); |
| } |
| )); |
| assert(_parentZone != null); |
| assert(_pendingExceptionDetails != null); |
| _parentZone.run<Null>(_testCompletionHandler); |
| } |
| ); |
| _parentZone = Zone.current; |
| final Zone testZone = _parentZone.fork(specification: errorHandlingZoneSpecification); |
| testZone.runBinaryGuarded(_runTestBody, testBody, invariantTester) |
| .whenComplete(_testCompletionHandler); |
| asyncBarrier(); // When using AutomatedTestWidgetsFlutterBinding, this flushes the microtasks. |
| return _currentTestCompleter.future; |
| } |
| |
| Future<Null> _runTestBody(Future<Null> testBody(), VoidCallback invariantTester) async { |
| assert(inTest); |
| |
| runApp(new Container(key: new UniqueKey(), child: _kPreTestMessage)); // Reset the tree to a known state. |
| await pump(); |
| |
| // run the test |
| await testBody(); |
| asyncBarrier(); // drains the microtasks in `flutter test` mode (when using AutomatedTestWidgetsFlutterBinding) |
| |
| if (_pendingExceptionDetails == null) { |
| // We only try to clean up and verify invariants if we didn't already |
| // fail. If we got an exception already, then we instead leave everything |
| // alone so that we don't cause more spurious errors. |
| runApp(new Container(key: new UniqueKey(), child: _kPostTestMessage)); // Unmount any remaining widgets. |
| await pump(); |
| invariantTester(); |
| _verifyInvariants(); |
| } |
| |
| assert(inTest); |
| return null; |
| } |
| |
| void _verifyInvariants() { |
| assert(debugAssertNoTransientCallbacks( |
| 'An animation is still running even after the widget tree was disposed.' |
| )); |
| assert(debugAssertAllFoundationVarsUnset( |
| 'The value of a foundation debug variable was changed by the test.', |
| debugPrintOverride: debugPrintOverride, |
| )); |
| assert(debugAssertAllRenderVarsUnset( |
| 'The value of a rendering debug variable was changed by the test.' |
| )); |
| assert(debugAssertAllWidgetVarsUnset( |
| 'The value of a widget debug variable was changed by the test.' |
| )); |
| assert(debugAssertAllSchedulerVarsUnset( |
| 'The value of a scheduler debug variable was changed by the test.' |
| )); |
| } |
| |
| /// Called by the [testWidgets] function after a test is executed. |
| void postTest() { |
| assert(inTest); |
| FlutterError.onError = _oldExceptionHandler; |
| _pendingExceptionDetails = null; |
| _currentTestCompleter = null; |
| _parentZone = null; |
| } |
| } |
| |
| /// A variant of [TestWidgetsFlutterBinding] for executing tests in |
| /// the `flutter test` environment. |
| /// |
| /// This binding controls time, allowing tests to verify long |
| /// animation sequences without having to execute them in real time. |
| /// |
| /// This class assumes it is always run in checked mode (since tests are always |
| /// run in checked mode). |
| class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { |
| @override |
| void initInstances() { |
| super.initInstances(); |
| ui.window.onBeginFrame = null; |
| ui.window.onDrawFrame = null; |
| } |
| |
| FakeAsync _fakeAsync; |
| |
| @override |
| Clock get clock => _clock; |
| Clock _clock; |
| |
| @override |
| DebugPrintCallback get debugPrintOverride => debugPrintSynchronously; |
| |
| @override |
| test_package.Timeout get defaultTestTimeout => const test_package.Timeout(const Duration(seconds: 5)); |
| |
| @override |
| bool get inTest => _fakeAsync != null; |
| |
| @override |
| int get microtaskCount => _fakeAsync.microtaskCount; |
| |
| @override |
| Future<Null> pump([ Duration duration, EnginePhase newPhase = EnginePhase.sendSemanticsUpdate ]) { |
| return TestAsyncUtils.guard(() { |
| assert(inTest); |
| assert(_clock != null); |
| if (duration != null) |
| _fakeAsync.elapse(duration); |
| _phase = newPhase; |
| if (hasScheduledFrame) { |
| _fakeAsync.flushMicrotasks(); |
| handleBeginFrame(new Duration( |
| milliseconds: _clock.now().millisecondsSinceEpoch, |
| )); |
| _fakeAsync.flushMicrotasks(); |
| handleDrawFrame(); |
| } |
| _fakeAsync.flushMicrotasks(); |
| return new Future<Null>.value(); |
| }); |
| } |
| |
| @override |
| void scheduleWarmUpFrame() { |
| // We override the default version of this so that the application-startup warm-up frame |
| // does not schedule timers which we might never get around to running. |
| handleBeginFrame(null); |
| _fakeAsync.flushMicrotasks(); |
| handleDrawFrame(); |
| } |
| |
| @override |
| Future<Null> idle() { |
| final Future<Null> result = super.idle(); |
| _fakeAsync.elapse(const Duration()); |
| return result; |
| } |
| |
| EnginePhase _phase = EnginePhase.sendSemanticsUpdate; |
| |
| // Cloned from RendererBinding.drawFrame() but with early-exit semantics. |
| @override |
| void drawFrame() { |
| assert(inTest); |
| try { |
| debugBuildingDirtyElements = true; |
| buildOwner.buildScope(renderViewElement); |
| if (_phase != EnginePhase.build) { |
| assert(renderView != null); |
| pipelineOwner.flushLayout(); |
| if (_phase != EnginePhase.layout) { |
| pipelineOwner.flushCompositingBits(); |
| if (_phase != EnginePhase.compositingBits) { |
| pipelineOwner.flushPaint(); |
| if (_phase != EnginePhase.paint) { |
| renderView.compositeFrame(); // this sends the bits to the GPU |
| if (_phase != EnginePhase.composite) { |
| pipelineOwner.flushSemantics(); |
| assert(_phase == EnginePhase.flushSemantics || |
| _phase == EnginePhase.sendSemanticsUpdate); |
| } |
| } |
| } |
| } |
| } |
| buildOwner.finalizeTree(); |
| } finally { |
| debugBuildingDirtyElements = false; |
| } |
| } |
| |
| @override |
| Future<Null> runTest(Future<Null> testBody(), VoidCallback invariantTester, { String description: '' }) { |
| assert(description != null); |
| assert(!inTest); |
| assert(_fakeAsync == null); |
| assert(_clock == null); |
| _fakeAsync = new FakeAsync(); |
| _clock = _fakeAsync.getClock(new DateTime.utc(2015, 1, 1)); |
| Future<Null> testBodyResult; |
| _fakeAsync.run((FakeAsync fakeAsync) { |
| assert(fakeAsync == _fakeAsync); |
| testBodyResult = _runTest(testBody, invariantTester, description); |
| assert(inTest); |
| }); |
| // testBodyResult is a Future that was created in the Zone of the fakeAsync. |
| // This means that if we call .then() on it (as the test framework is about to), |
| // it will register a microtask to handle the future _in the fake async zone_. |
| // To avoid this, we wrap it in a Future that we've created _outside_ the fake |
| // async zone. |
| return new Future<Null>.value(testBodyResult); |
| } |
| |
| @override |
| void asyncBarrier() { |
| assert(_fakeAsync != null); |
| _fakeAsync.flushMicrotasks(); |
| super.asyncBarrier(); |
| } |
| |
| @override |
| void _verifyInvariants() { |
| super._verifyInvariants(); |
| assert( |
| _fakeAsync.periodicTimerCount == 0, |
| 'A periodic Timer is still running even after the widget tree was disposed.' |
| ); |
| assert( |
| _fakeAsync.nonPeriodicTimerCount == 0, |
| 'A Timer is still pending even after the widget tree was disposed.' |
| ); |
| assert(_fakeAsync.microtaskCount == 0); // Shouldn't be possible. |
| } |
| |
| @override |
| void postTest() { |
| super.postTest(); |
| assert(_fakeAsync != null); |
| assert(_clock != null); |
| _clock = null; |
| _fakeAsync = null; |
| } |
| |
| } |
| |
| /// Available policies for how a [LiveTestWidgetsFlutterBinding] should paint |
| /// frames. |
| /// |
| /// These values are set on the binding's |
| /// [LiveTestWidgetsFlutterBinding.framePolicy] property. The default is |
| /// [fadePointers]. |
| enum LiveTestWidgetsFlutterBindingFramePolicy { |
| /// Strictly show only frames that are explicitly pumped. This most closely |
| /// matches the behavior of tests when run under `flutter test`. |
| onlyPumps, |
| |
| /// Show pumped frames, and additionally schedule and run frames to fade |
| /// out the pointer crosshairs and other debugging information shown by |
| /// the binding. |
| /// |
| /// This can result in additional frames being pumped beyond those that |
| /// the test itself requests, which can cause differences in behavior. |
| fadePointers, |
| |
| /// Show every frame that the framework requests, even if the frames are not |
| /// explicitly pumped. |
| /// |
| /// This can help with orienting the developer when looking at |
| /// heavily-animated situations, and will almost certainly result in |
| /// additional frames being pumped beyond those that the test itself requests, |
| /// which can cause differences in behavior. |
| fullyLive, |
| } |
| |
| /// A variant of [TestWidgetsFlutterBinding] for executing tests in |
| /// the `flutter run` environment, on a device. This is intended to |
| /// allow interactive test development. |
| /// |
| /// This is not the way to run a remote-control test. To run a test on |
| /// a device from a development computer, see the [flutter_driver] |
| /// package and the `flutter drive` command. |
| /// |
| /// When running tests using `flutter run`, consider adding the |
| /// `--use-test-fonts` argument so that the fonts used match those used under |
| /// `flutter test`. (This forces all text to use the "Ahem" font, which is a |
| /// font that covers ASCII characters and gives them all the appearance of a |
| /// square whose size equals the font size.) |
| /// |
| /// This binding overrides the default [SchedulerBinding] behavior to ensure |
| /// that tests work in the same way in this environment as they would under the |
| /// [AutomatedTestWidgetsFlutterBinding]. To override this (and see intermediate |
| /// frames that the test does not explicitly trigger), set [framePolicy] to |
| /// [LiveTestWidgetsFlutterBindingFramePolicy.fullyLive]. (This is likely to |
| /// make tests fail, though, especially if e.g. they test how many times a |
| /// particular widget was built.) The default behavior is to show pumped frames |
| /// and a few additional frames when pointers are triggered (to animate the |
| /// pointer crosshairs). |
| /// |
| /// This binding does not support the [EnginePhase] argument to |
| /// [pump]. (There would be no point setting it to a value that |
| /// doesn't trigger a paint, since then you could not see anything |
| /// anyway.) |
| class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { |
| @override |
| bool get inTest => _inTest; |
| bool _inTest = false; |
| |
| @override |
| Clock get clock => const Clock(); |
| |
| @override |
| int get microtaskCount { |
| // Unsupported until we have a wrapper around the real async API |
| // https://github.com/flutter/flutter/issues/4637 |
| assert(false); |
| return -1; |
| } |
| |
| @override |
| test_package.Timeout get defaultTestTimeout => test_package.Timeout.none; |
| |
| Completer<Null> _pendingFrame; |
| bool _expectingFrame = false; |
| bool _viewNeedsPaint = false; |
| |
| /// Whether to have [pump] with a duration only pump a single frame |
| /// (as would happen in a normal test environment using |
| /// [AutomatedTestWidgetsFlutterBinding]), or whether to instead |
| /// pump every frame that the system requests during any |
| /// asynchronous pause in the test (as would normally happen when |
| /// running an application with [WidgetsFlutterBinding]). |
| /// |
| /// * [LiveTestWidgetsFlutterBindingFramePolicy.fadePointers] is the default |
| /// behavior, which is to only pump once, except when there has been some |
| /// activity with [TestPointer]s, in which case those are shown and may pump |
| /// additional frames. |
| /// |
| /// * [LiveTestWidgetsFlutterBindingFramePolicy.onlyPumps] is the strictest |
| /// behavior, which is to only pump once. This most closely matches the |
| /// [AutomatedTestWidgetsFlutterBinding] (`flutter test`) behavior. |
| /// |
| /// * [LiveTestWidgetsFlutterBindingFramePolicy.fullyLive] allows all frame |
| /// requests from the engine to be serviced, even those the test did not |
| /// explicitly pump. |
| /// |
| /// Setting this to anything other than |
| /// [LiveTestWidgetsFlutterBindingFramePolicy.onlyPumps] means pumping extra |
| /// frames, which might involve calling builders more, or calling paint |
| /// callbacks more, etc, which might interfere with the test. If you know your |
| /// test file wouldn't be affected by this, you can set it to |
| /// [LiveTestWidgetsFlutterBindingFramePolicy.fullyLive] persistently in that |
| /// particular test file. To set this to |
| /// [LiveTestWidgetsFlutterBindingFramePolicy.fullyLive] while still allowing |
| /// the test file to work as a normal test, add the following code to your |
| /// test file at the top of your `void main() { }` function, before calls to |
| /// [testWidgets]: |
| /// |
| /// ```dart |
| /// TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized(); |
| /// if (binding is LiveTestWidgetsFlutterBinding) |
| /// binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive; |
| /// ``` |
| LiveTestWidgetsFlutterBindingFramePolicy framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fadePointers; |
| |
| bool _doDrawThisFrame; |
| |
| @override |
| void handleBeginFrame(Duration rawTimeStamp) { |
| assert(_doDrawThisFrame == null); |
| if (_expectingFrame || |
| (framePolicy == LiveTestWidgetsFlutterBindingFramePolicy.fullyLive) || |
| (framePolicy == LiveTestWidgetsFlutterBindingFramePolicy.fadePointers && _viewNeedsPaint)) { |
| _doDrawThisFrame = true; |
| super.handleBeginFrame(rawTimeStamp); |
| } else { |
| _doDrawThisFrame = false; |
| } |
| } |
| |
| @override |
| void handleDrawFrame() { |
| assert(_doDrawThisFrame != null); |
| if (_doDrawThisFrame) |
| super.handleDrawFrame(); |
| _doDrawThisFrame = null; |
| _viewNeedsPaint = false; |
| if (_expectingFrame) { // set during pump |
| assert(_pendingFrame != null); |
| _pendingFrame.complete(); // unlocks the test API |
| _pendingFrame = null; |
| _expectingFrame = false; |
| } else { |
| ui.window.scheduleFrame(); |
| } |
| } |
| |
| @override |
| void initRenderView() { |
| assert(renderView == null); |
| renderView = new _LiveTestRenderView( |
| configuration: createViewConfiguration(), |
| onNeedPaint: _handleViewNeedsPaint, |
| ); |
| renderView.scheduleInitialFrame(); |
| } |
| |
| @override |
| _LiveTestRenderView get renderView => super.renderView; |
| |
| void _handleViewNeedsPaint() { |
| _viewNeedsPaint = true; |
| renderView.markNeedsPaint(); |
| } |
| |
| /// An object to which real device events should be routed. |
| /// |
| /// Normally, device events are silently dropped. However, if this property is |
| /// set to a non-null value, then the events will be routed to its |
| /// [HitTestDispatcher.dispatchEvent] method instead. |
| /// |
| /// Events dispatched by [TestGesture] are not affected by this. |
| HitTestDispatcher deviceEventDispatcher; |
| |
| @override |
| void dispatchEvent(PointerEvent event, HitTestResult result, { |
| TestBindingEventSource source: TestBindingEventSource.device |
| }) { |
| switch (source) { |
| case TestBindingEventSource.test: |
| if (!renderView._pointers.containsKey(event.pointer)) { |
| assert(event.down); |
| renderView._pointers[event.pointer] = new _LiveTestPointerRecord(event.pointer, event.position); |
| } else { |
| renderView._pointers[event.pointer].position = event.position; |
| if (!event.down) |
| renderView._pointers[event.pointer].decay = _kPointerDecay; |
| } |
| _handleViewNeedsPaint(); |
| super.dispatchEvent(event, result, source: source); |
| break; |
| case TestBindingEventSource.device: |
| if (deviceEventDispatcher != null) |
| deviceEventDispatcher.dispatchEvent(event, result); |
| break; |
| } |
| } |
| |
| @override |
| Future<Null> pump([ Duration duration, EnginePhase newPhase = EnginePhase.sendSemanticsUpdate ]) { |
| assert(newPhase == EnginePhase.sendSemanticsUpdate); |
| assert(inTest); |
| assert(!_expectingFrame); |
| assert(_pendingFrame == null); |
| return TestAsyncUtils.guard(() { |
| if (duration != null) { |
| new Timer(duration, () { |
| _expectingFrame = true; |
| scheduleFrame(); |
| }); |
| } else { |
| _expectingFrame = true; |
| scheduleFrame(); |
| } |
| _pendingFrame = new Completer<Null>(); |
| return _pendingFrame.future; |
| }); |
| } |
| |
| @override |
| Future<Null> runTest(Future<Null> testBody(), VoidCallback invariantTester, { String description: '' }) async { |
| assert(description != null); |
| assert(!inTest); |
| _inTest = true; |
| renderView._setDescription(description); |
| return _runTest(testBody, invariantTester, description); |
| } |
| |
| @override |
| void postTest() { |
| super.postTest(); |
| assert(!_expectingFrame); |
| assert(_pendingFrame == null); |
| _inTest = false; |
| } |
| |
| @override |
| ViewConfiguration createViewConfiguration() { |
| return new TestViewConfiguration(); |
| } |
| |
| @override |
| Offset globalToLocal(Offset point) { |
| final Matrix4 transform = renderView.configuration.toHitTestMatrix(); |
| final double det = transform.invert(); |
| assert(det != 0.0); |
| final Offset result = MatrixUtils.transformPoint(transform, point); |
| return result; |
| } |
| |
| @override |
| Offset localToGlobal(Offset point) { |
| final Matrix4 transform = renderView.configuration.toHitTestMatrix(); |
| return MatrixUtils.transformPoint(transform, point); |
| } |
| } |
| |
| /// A [ViewConfiguration] that pretends the display is of a particular size. The |
| /// size is in logical pixels. The resulting ViewConfiguration maps the given |
| /// 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), |
| super(size: size); |
| |
| static Matrix4 _getMatrix(Size size, double devicePixelRatio) { |
| final double actualWidth = ui.window.physicalSize.width; |
| final double actualHeight = ui.window.physicalSize.height; |
| final double desiredWidth = size.width; |
| final double desiredHeight = size.height; |
| double scale, shiftX, shiftY; |
| if ((actualWidth / actualHeight) > (desiredWidth / desiredHeight)) { |
| scale = actualHeight / desiredHeight; |
| shiftX = (actualWidth - desiredWidth * scale) / 2.0; |
| shiftY = 0.0; |
| } else { |
| scale = actualWidth / desiredWidth; |
| shiftX = 0.0; |
| shiftY = (actualHeight - desiredHeight * scale) / 2.0; |
| } |
| final Matrix4 matrix = new Matrix4.compose( |
| new Vector3(shiftX, shiftY, 0.0), // translation |
| new Quaternion.identity(), // rotation |
| new Vector3(scale, scale, 1.0) // scale |
| ); |
| return matrix; |
| } |
| |
| final Matrix4 _paintMatrix; |
| final Matrix4 _hitTestMatrix; |
| |
| @override |
| Matrix4 toMatrix() => _paintMatrix.clone(); |
| |
| /// Provides the transformation matrix that converts coordinates in the test |
| /// coordinate space to coordinates in logical pixels on the real display. |
| /// |
| /// This is essenitally the same as [toMatrix] but ignoring the device pixel |
| /// ratio. |
| /// |
| /// This is useful because pointers are described in logical pixels, as |
| /// opposed to graphics which are expressed in physical pixels. |
| // TODO(ianh): We should make graphics and pointers use the same coordinate space. |
| // See: https://github.com/flutter/flutter/issues/1360 |
| Matrix4 toHitTestMatrix() => _hitTestMatrix.clone(); |
| |
| @override |
| String toString() => 'TestViewConfiguration'; |
| } |
| |
| const int _kPointerDecay = -2; |
| |
| class _LiveTestPointerRecord { |
| _LiveTestPointerRecord( |
| this.pointer, |
| this.position |
| ) : color = new HSVColor.fromAHSV(0.8, (35.0 * pointer) % 360.0, 1.0, 1.0).toColor(), |
| decay = 1; |
| final int pointer; |
| final Color color; |
| Offset position; |
| int decay; // >0 means down, <0 means up, increases by one each time, removed at 0 |
| } |
| |
| class _LiveTestRenderView extends RenderView { |
| _LiveTestRenderView({ |
| ViewConfiguration configuration, |
| this.onNeedPaint, |
| }) : super(configuration: configuration); |
| |
| @override |
| TestViewConfiguration get configuration => super.configuration; |
| @override |
| set configuration(covariant TestViewConfiguration value) { super.configuration = value; } |
| |
| final VoidCallback onNeedPaint; |
| |
| final Map<int, _LiveTestPointerRecord> _pointers = <int, _LiveTestPointerRecord>{}; |
| |
| TextPainter _label; |
| static const TextStyle _labelStyle = const TextStyle( |
| fontFamily: 'sans-serif', |
| fontSize: 10.0, |
| ); |
| void _setDescription(String value) { |
| assert(value != null); |
| if (value.isEmpty) { |
| _label = null; |
| return; |
| } |
| _label ??= new TextPainter(textAlign: TextAlign.left); |
| _label.text = new TextSpan(text: value, style: _labelStyle); |
| _label.layout(); |
| if (onNeedPaint != null) |
| onNeedPaint(); |
| } |
| |
| @override |
| bool hitTest(HitTestResult result, { Offset position }) { |
| final Matrix4 transform = configuration.toHitTestMatrix(); |
| final double det = transform.invert(); |
| assert(det != 0.0); |
| position = MatrixUtils.transformPoint(transform, position); |
| return super.hitTest(result, position: position); |
| } |
| |
| @override |
| void paint(PaintingContext context, Offset offset) { |
| assert(offset == Offset.zero); |
| super.paint(context, offset); |
| if (_pointers.isNotEmpty) { |
| final double radius = configuration.size.shortestSide * 0.05; |
| final Path path = new Path() |
| ..addOval(new Rect.fromCircle(center: Offset.zero, radius: radius)) |
| ..moveTo(0.0, -radius * 2.0) |
| ..lineTo(0.0, radius * 2.0) |
| ..moveTo(-radius * 2.0, 0.0) |
| ..lineTo(radius * 2.0, 0.0); |
| final Canvas canvas = context.canvas; |
| final Paint paint = new Paint() |
| ..strokeWidth = radius / 10.0 |
| ..style = PaintingStyle.stroke; |
| bool dirty = false; |
| for (int pointer in _pointers.keys) { |
| final _LiveTestPointerRecord record = _pointers[pointer]; |
| paint.color = record.color.withOpacity(record.decay < 0 ? (record.decay / (_kPointerDecay - 1)) : 1.0); |
| canvas.drawPath(path.shift(record.position), paint); |
| if (record.decay < 0) |
| dirty = true; |
| record.decay += 1; |
| } |
| _pointers |
| .keys |
| .where((int pointer) => _pointers[pointer].decay == 0) |
| .toList() |
| .forEach(_pointers.remove); |
| if (dirty && onNeedPaint != null) |
| scheduleMicrotask(onNeedPaint); |
| } |
| _label?.paint(context.canvas, offset - const Offset(0.0, 10.0)); |
| } |
| } |
| |
| class _EmptyStack implements StackTrace { |
| const _EmptyStack._(); |
| static const _EmptyStack instance = const _EmptyStack._(); |
| @override |
| String toString() => ''; |
| } |
| |
| StackTrace _unmangle(StackTrace stack) { |
| if (stack is stack_trace.Trace) |
| return stack.vmTrace; |
| if (stack is stack_trace.Chain) |
| return stack.toTrace().vmTrace; |
| return stack; |
| } |