binding_test.dart 10.2 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'package:flutter/foundation.dart';
6 7
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
8
import 'package:flutter_test/flutter_test.dart';
9

10
class MemoryPressureObserver with WidgetsBindingObserver {
11 12 13 14 15 16 17 18
  bool sawMemoryPressure = false;

  @override
  void didHaveMemoryPressure() {
    sawMemoryPressure = true;
  }
}

19
class AppLifecycleStateObserver with WidgetsBindingObserver {
20
  late AppLifecycleState lifecycleState;
21 22 23 24 25 26 27

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    lifecycleState = state;
  }
}

28
class PushRouteObserver with WidgetsBindingObserver {
29
  late String pushedRoute;
30 31 32 33 34 35 36 37

  @override
  Future<bool> didPushRoute(String route) async {
    pushedRoute = route;
    return true;
  }
}

38
class PushRouteInformationObserver with WidgetsBindingObserver {
39
  late RouteInformation pushedRouteInformation;
40 41 42 43 44 45 46 47

  @override
  Future<bool> didPushRouteInformation(RouteInformation routeInformation) async {
    pushedRouteInformation = routeInformation;
    return true;
  }
}

48 49 50 51 52
void main() {
  setUp(() {
    WidgetsFlutterBinding.ensureInitialized();
  });

53 54 55
  Future<void> setAppLifeCycleState(AppLifecycleState state) async {
    final ByteData? message =
        const StringCodec().encodeMessage(state.toString());
56
    await ServicesBinding.instance.defaultBinaryMessenger
57 58 59
        .handlePlatformMessage('flutter/lifecycle', message, (_) {});
  }

60
  testWidgets('didHaveMemoryPressure callback', (WidgetTester tester) async {
61
    final MemoryPressureObserver observer = MemoryPressureObserver();
62
    WidgetsBinding.instance.addObserver(observer);
63
    final ByteData message = const JSONMessageCodec().encodeMessage(<String, dynamic>{'type': 'memoryPressure'})!;
64
    await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage('flutter/system', message, (_) { });
65
    expect(observer.sawMemoryPressure, true);
66
    WidgetsBinding.instance.removeObserver(observer);
67
  });
68 69

  testWidgets('handleLifecycleStateChanged callback', (WidgetTester tester) async {
70
    final AppLifecycleStateObserver observer = AppLifecycleStateObserver();
71
    WidgetsBinding.instance.addObserver(observer);
72

73
    setAppLifeCycleState(AppLifecycleState.paused);
74 75
    expect(observer.lifecycleState, AppLifecycleState.paused);

76
    setAppLifeCycleState(AppLifecycleState.resumed);
77 78
    expect(observer.lifecycleState, AppLifecycleState.resumed);

79
    setAppLifeCycleState(AppLifecycleState.inactive);
80
    expect(observer.lifecycleState, AppLifecycleState.inactive);
81

82
    setAppLifeCycleState(AppLifecycleState.detached);
83
    expect(observer.lifecycleState, AppLifecycleState.detached);
84 85

    setAppLifeCycleState(AppLifecycleState.resumed);
86
  });
87 88

  testWidgets('didPushRoute callback', (WidgetTester tester) async {
89
    final PushRouteObserver observer = PushRouteObserver();
90
    WidgetsBinding.instance.addObserver(observer);
91

92
    const String testRouteName = 'testRouteName';
93
    final ByteData message = const JSONMethodCodec().encodeMethodCall(const MethodCall('pushRoute', testRouteName));
94
    await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage('flutter/navigation', message, (_) { });
95 96
    expect(observer.pushedRoute, testRouteName);

97
    WidgetsBinding.instance.removeObserver(observer);
98
  });
99

100 101
  testWidgets('didPushRouteInformation calls didPushRoute by default', (WidgetTester tester) async {
    final PushRouteObserver observer = PushRouteObserver();
102
    WidgetsBinding.instance.addObserver(observer);
103 104 105 106

    const Map<String, dynamic> testRouteInformation = <String, dynamic>{
      'location': 'testRouteName',
      'state': 'state',
107
      'restorationData': <dynamic, dynamic>{'test': 'config'},
108 109
    };
    final ByteData message = const JSONMethodCodec().encodeMethodCall(
110 111
      const MethodCall('pushRouteInformation', testRouteInformation),
    );
112
    await ServicesBinding.instance.defaultBinaryMessenger
113
        .handlePlatformMessage('flutter/navigation', message, (_) {});
114
    expect(observer.pushedRoute, 'testRouteName');
115
    WidgetsBinding.instance.removeObserver(observer);
116 117 118 119
  });

  testWidgets('didPushRouteInformation callback', (WidgetTester tester) async {
    final PushRouteInformationObserver observer = PushRouteInformationObserver();
120
    WidgetsBinding.instance.addObserver(observer);
121 122 123 124 125 126

    const Map<String, dynamic> testRouteInformation = <String, dynamic>{
      'location': 'testRouteName',
      'state': 'state',
    };
    final ByteData message = const JSONMethodCodec().encodeMethodCall(
127 128
      const MethodCall('pushRouteInformation', testRouteInformation),
    );
129
    await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage('flutter/navigation', message, (_) { });
130 131
    expect(observer.pushedRouteInformation.location, 'testRouteName');
    expect(observer.pushedRouteInformation.state, 'state');
132
    WidgetsBinding.instance.removeObserver(observer);
133 134
  });

135 136
  testWidgets('didPushRouteInformation callback with null state', (WidgetTester tester) async {
    final PushRouteInformationObserver observer = PushRouteInformationObserver();
137
    WidgetsBinding.instance.addObserver(observer);
138 139 140 141 142 143

    const Map<String, dynamic> testRouteInformation = <String, dynamic>{
      'location': 'testRouteName',
      'state': null,
    };
    final ByteData message = const JSONMethodCodec().encodeMethodCall(
144 145
      const MethodCall('pushRouteInformation', testRouteInformation),
    );
146
    await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage('flutter/navigation', message, (_) { });
147 148
    expect(observer.pushedRouteInformation.location, 'testRouteName');
    expect(observer.pushedRouteInformation.state, null);
149
    WidgetsBinding.instance.removeObserver(observer);
150 151
  });

152 153 154
  testWidgets('Application lifecycle affects frame scheduling', (WidgetTester tester) async {
    expect(tester.binding.hasScheduledFrame, isFalse);

155
    setAppLifeCycleState(AppLifecycleState.paused);
156 157
    expect(tester.binding.hasScheduledFrame, isFalse);

158
    setAppLifeCycleState(AppLifecycleState.resumed);
159 160 161 162
    expect(tester.binding.hasScheduledFrame, isTrue);
    await tester.pump();
    expect(tester.binding.hasScheduledFrame, isFalse);

163
    setAppLifeCycleState(AppLifecycleState.inactive);
164 165
    expect(tester.binding.hasScheduledFrame, isFalse);

166
    setAppLifeCycleState(AppLifecycleState.detached);
167 168
    expect(tester.binding.hasScheduledFrame, isFalse);

169
    setAppLifeCycleState(AppLifecycleState.inactive);
170 171 172 173
    expect(tester.binding.hasScheduledFrame, isTrue);
    await tester.pump();
    expect(tester.binding.hasScheduledFrame, isFalse);

174
    setAppLifeCycleState(AppLifecycleState.paused);
175 176 177 178 179 180
    expect(tester.binding.hasScheduledFrame, isFalse);

    tester.binding.scheduleFrame();
    expect(tester.binding.hasScheduledFrame, isFalse);

    tester.binding.scheduleForcedFrame();
181 182
    expect(tester.binding.hasScheduledFrame, isTrue);
    await tester.pump();
183 184

    int frameCount = 0;
185 186 187
    tester.binding.addPostFrameCallback((Duration duration) {
      frameCount += 1;
    });
188 189 190 191 192 193 194 195
    expect(tester.binding.hasScheduledFrame, isFalse);
    await tester.pump(const Duration(milliseconds: 1));
    expect(tester.binding.hasScheduledFrame, isFalse);
    expect(frameCount, 0);

    tester.binding.scheduleWarmUpFrame(); // this actually tests flutter_test's implementation
    expect(tester.binding.hasScheduledFrame, isFalse);
    expect(frameCount, 1);
196 197

    // Get the tester back to a resumed state for subsequent tests.
198
    setAppLifeCycleState(AppLifecycleState.resumed);
199 200
    expect(tester.binding.hasScheduledFrame, isTrue);
    await tester.pump();
201
  });
202 203

  testWidgets('scheduleFrameCallback error control test', (WidgetTester tester) async {
204
    late FlutterError error;
205
    try {
206
      tester.binding.scheduleFrameCallback((Duration _) { }, rescheduling: true);
207 208 209 210 211 212 213 214 215 216 217
    } on FlutterError catch (e) {
      error = e;
    }
    expect(error, isNotNull);
    expect(error.diagnostics.length, 3);
    expect(error.diagnostics.last.level, DiagnosticLevel.hint);
    expect(
      error.diagnostics.last.toStringDeep(),
      equalsIgnoringHashCodes(
        'If this is the initial registration of the callback, or if the\n'
        'callback is asynchronous, then do not use the "rescheduling"\n'
218
        'argument.\n',
219 220 221 222 223 224 225 226 227 228 229 230
      ),
    );
    expect(
      error.toStringDeep(),
      'FlutterError\n'
      '   scheduleFrameCallback called with rescheduling true, but no\n'
      '   callback is in scope.\n'
      '   The "rescheduling" argument should only be set to true if the\n'
      '   callback is being reregistered from within the callback itself,\n'
      '   and only then if the callback itself is entirely synchronous.\n'
      '   If this is the initial registration of the callback, or if the\n'
      '   callback is asynchronous, then do not use the "rescheduling"\n'
231
      '   argument.\n',
232 233
    );
  });
234 235

  testWidgets('defaultStackFilter elides framework Element mounting stacks', (WidgetTester tester) async {
236 237
    final FlutterExceptionHandler? oldHandler = FlutterError.onError;
    late FlutterErrorDetails errorDetails;
238
    FlutterError.onError = (FlutterErrorDetails details) {
239
      errorDetails = details;
240 241 242 243 244 245 246 247 248
    };
    await tester.pumpWidget(Directionality(
      textDirection: TextDirection.ltr,
      child: TestStatefulWidget(
        child: Builder(
          builder: (BuildContext context) {
            return Opacity(
              opacity: .5,
              child: Builder(
249 250 251 252
                builder: (BuildContext context) {
                  assert(false);
                  return const Text('');
                },
253 254 255 256 257 258 259
              ),
            );
          },
        ),
      ),
    ));
    FlutterError.onError = oldHandler;
260
    expect(errorDetails.exception, isAssertionError);
261
    const String toMatch = '...     Normal element mounting (';
262
    expect(toMatch.allMatches(errorDetails.toString()).length, 1);
263
  }, skip: kIsWeb); // https://github.com/flutter/flutter/issues/87875
264 265 266
}

class TestStatefulWidget extends StatefulWidget {
267
  const TestStatefulWidget({required this.child, super.key});
268 269 270 271 272 273 274 275 276 277 278 279

  final Widget child;

  @override
  State<StatefulWidget> createState() => TestStatefulWidgetState();
}

class TestStatefulWidgetState extends State<TestStatefulWidget> {
  @override
  Widget build(BuildContext context) {
    return widget.child;
  }
280
}