binding_test.dart 12.4 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
void main() {
49 50 51
  Future<void> setAppLifeCycleState(AppLifecycleState state) async {
    final ByteData? message =
        const StringCodec().encodeMessage(state.toString());
52 53
    await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
        .handlePlatformMessage('flutter/lifecycle', message, (_) { });
54 55
  }

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

  testWidgets('handleLifecycleStateChanged callback', (WidgetTester tester) async {
66
    final AppLifecycleStateObserver observer = AppLifecycleStateObserver();
67
    WidgetsBinding.instance.addObserver(observer);
68

69
    setAppLifeCycleState(AppLifecycleState.paused);
70 71
    expect(observer.lifecycleState, AppLifecycleState.paused);

72
    setAppLifeCycleState(AppLifecycleState.resumed);
73 74
    expect(observer.lifecycleState, AppLifecycleState.resumed);

75
    setAppLifeCycleState(AppLifecycleState.inactive);
76
    expect(observer.lifecycleState, AppLifecycleState.inactive);
77

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

    setAppLifeCycleState(AppLifecycleState.resumed);
82
  });
83 84

  testWidgets('didPushRoute callback', (WidgetTester tester) async {
85
    final PushRouteObserver observer = PushRouteObserver();
86
    WidgetsBinding.instance.addObserver(observer);
87

88
    const String testRouteName = 'testRouteName';
89
    final ByteData message = const JSONMethodCodec().encodeMethodCall(const MethodCall('pushRoute', testRouteName));
90
    await tester.binding.defaultBinaryMessenger.handlePlatformMessage('flutter/navigation', message, (_) { });
91 92
    expect(observer.pushedRoute, testRouteName);

93
    WidgetsBinding.instance.removeObserver(observer);
94
  });
95

96 97
  testWidgets('didPushRouteInformation calls didPushRoute by default', (WidgetTester tester) async {
    final PushRouteObserver observer = PushRouteObserver();
98
    WidgetsBinding.instance.addObserver(observer);
99 100 101 102

    const Map<String, dynamic> testRouteInformation = <String, dynamic>{
      'location': 'testRouteName',
      'state': 'state',
103
      'restorationData': <dynamic, dynamic>{'test': 'config'},
104 105
    };
    final ByteData message = const JSONMethodCodec().encodeMethodCall(
106 107
      const MethodCall('pushRouteInformation', testRouteInformation),
    );
108
    await tester.binding.defaultBinaryMessenger
109
        .handlePlatformMessage('flutter/navigation', message, (_) {});
110
    expect(observer.pushedRoute, 'testRouteName');
111
    WidgetsBinding.instance.removeObserver(observer);
112 113
  });

114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145
  testWidgets('didPushRouteInformation calls didPushRoute correctly when handling url', (WidgetTester tester) async {
    final PushRouteObserver observer = PushRouteObserver();
    WidgetsBinding.instance.addObserver(observer);

    // A url without any path.
    Map<String, dynamic> testRouteInformation = const <String, dynamic>{
      'location': 'http://hostname',
      'state': 'state',
      'restorationData': <dynamic, dynamic>{'test': 'config'},
    };
    ByteData message = const JSONMethodCodec().encodeMethodCall(
      MethodCall('pushRouteInformation', testRouteInformation),
    );
    await ServicesBinding.instance.defaultBinaryMessenger
        .handlePlatformMessage('flutter/navigation', message, (_) {});
    expect(observer.pushedRoute, '/');

    // A complex url.
    testRouteInformation = const <String, dynamic>{
      'location': 'http://hostname/abc?def=123&def=456#789',
      'state': 'state',
      'restorationData': <dynamic, dynamic>{'test': 'config'},
    };
    message = const JSONMethodCodec().encodeMethodCall(
      MethodCall('pushRouteInformation', testRouteInformation),
    );
    await ServicesBinding.instance.defaultBinaryMessenger
        .handlePlatformMessage('flutter/navigation', message, (_) {});
    expect(observer.pushedRoute, '/abc?def=123&def=456#789');
    WidgetsBinding.instance.removeObserver(observer);
  });

146 147
  testWidgets('didPushRouteInformation callback', (WidgetTester tester) async {
    final PushRouteInformationObserver observer = PushRouteInformationObserver();
148
    WidgetsBinding.instance.addObserver(observer);
149 150 151 152 153 154

    const Map<String, dynamic> testRouteInformation = <String, dynamic>{
      'location': 'testRouteName',
      'state': 'state',
    };
    final ByteData message = const JSONMethodCodec().encodeMethodCall(
155 156
      const MethodCall('pushRouteInformation', testRouteInformation),
    );
157
    await tester.binding.defaultBinaryMessenger.handlePlatformMessage('flutter/navigation', message, (_) { });
158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176
    expect(observer.pushedRouteInformation.uri.toString(), 'testRouteName');
    expect(observer.pushedRouteInformation.state, 'state');
    WidgetsBinding.instance.removeObserver(observer);
  });

  testWidgets('didPushRouteInformation callback can handle url', (WidgetTester tester) async {
    final PushRouteInformationObserver observer = PushRouteInformationObserver();
    WidgetsBinding.instance.addObserver(observer);

    const Map<String, dynamic> testRouteInformation = <String, dynamic>{
      'location': 'http://hostname/abc?def=123&def=456#789',
      'state': 'state',
    };
    final ByteData message = const JSONMethodCodec().encodeMethodCall(
      const MethodCall('pushRouteInformation', testRouteInformation),
    );
    await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage('flutter/navigation', message, (_) { });
    expect(observer.pushedRouteInformation.location, '/abc?def=123&def=456#789');
    expect(observer.pushedRouteInformation.uri.toString(), 'http://hostname/abc?def=123&def=456#789');
177
    expect(observer.pushedRouteInformation.state, 'state');
178
    WidgetsBinding.instance.removeObserver(observer);
179 180
  });

181 182
  testWidgets('didPushRouteInformation callback with null state', (WidgetTester tester) async {
    final PushRouteInformationObserver observer = PushRouteInformationObserver();
183
    WidgetsBinding.instance.addObserver(observer);
184 185 186 187 188 189

    const Map<String, dynamic> testRouteInformation = <String, dynamic>{
      'location': 'testRouteName',
      'state': null,
    };
    final ByteData message = const JSONMethodCodec().encodeMethodCall(
190 191
      const MethodCall('pushRouteInformation', testRouteInformation),
    );
192

193
    await tester.binding.defaultBinaryMessenger.handlePlatformMessage('flutter/navigation', message, (_) { });
194
    expect(observer.pushedRouteInformation.uri.toString(), 'testRouteName');
195
    expect(observer.pushedRouteInformation.state, null);
196
    WidgetsBinding.instance.removeObserver(observer);
197 198
  });

199 200 201
  testWidgets('Application lifecycle affects frame scheduling', (WidgetTester tester) async {
    expect(tester.binding.hasScheduledFrame, isFalse);

202
    setAppLifeCycleState(AppLifecycleState.paused);
203 204
    expect(tester.binding.hasScheduledFrame, isFalse);

205
    setAppLifeCycleState(AppLifecycleState.resumed);
206 207 208 209
    expect(tester.binding.hasScheduledFrame, isTrue);
    await tester.pump();
    expect(tester.binding.hasScheduledFrame, isFalse);

210
    setAppLifeCycleState(AppLifecycleState.inactive);
211 212
    expect(tester.binding.hasScheduledFrame, isFalse);

213
    setAppLifeCycleState(AppLifecycleState.detached);
214 215
    expect(tester.binding.hasScheduledFrame, isFalse);

216
    setAppLifeCycleState(AppLifecycleState.inactive);
217 218 219 220
    expect(tester.binding.hasScheduledFrame, isTrue);
    await tester.pump();
    expect(tester.binding.hasScheduledFrame, isFalse);

221
    setAppLifeCycleState(AppLifecycleState.paused);
222 223 224 225 226 227
    expect(tester.binding.hasScheduledFrame, isFalse);

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

    tester.binding.scheduleForcedFrame();
228 229
    expect(tester.binding.hasScheduledFrame, isTrue);
    await tester.pump();
230 231

    int frameCount = 0;
232 233 234
    tester.binding.addPostFrameCallback((Duration duration) {
      frameCount += 1;
    });
235 236 237 238 239 240 241 242
    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);
243 244

    // Get the tester back to a resumed state for subsequent tests.
245
    setAppLifeCycleState(AppLifecycleState.resumed);
246 247
    expect(tester.binding.hasScheduledFrame, isTrue);
    await tester.pump();
248
  });
249 250

  testWidgets('scheduleFrameCallback error control test', (WidgetTester tester) async {
251
    late FlutterError error;
252
    try {
253
      tester.binding.scheduleFrameCallback((Duration _) { }, rescheduling: true);
254 255 256 257 258 259 260 261 262 263 264
    } 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'
265
        'argument.\n',
266 267 268 269 270 271 272 273 274 275 276 277
      ),
    );
    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'
278
      '   argument.\n',
279 280
    );
  });
281 282

  testWidgets('defaultStackFilter elides framework Element mounting stacks', (WidgetTester tester) async {
283 284
    final FlutterExceptionHandler? oldHandler = FlutterError.onError;
    late FlutterErrorDetails errorDetails;
285
    FlutterError.onError = (FlutterErrorDetails details) {
286
      errorDetails = details;
287 288 289 290 291 292 293 294 295
    };
    await tester.pumpWidget(Directionality(
      textDirection: TextDirection.ltr,
      child: TestStatefulWidget(
        child: Builder(
          builder: (BuildContext context) {
            return Opacity(
              opacity: .5,
              child: Builder(
296 297 298 299
                builder: (BuildContext context) {
                  assert(false);
                  return const Text('');
                },
300 301 302 303 304 305 306
              ),
            );
          },
        ),
      ),
    ));
    FlutterError.onError = oldHandler;
307
    expect(errorDetails.exception, isAssertionError);
308
    const String toMatch = '...     Normal element mounting (';
309
    expect(toMatch.allMatches(errorDetails.toString()).length, 1);
310
  }, skip: kIsWeb); // https://github.com/flutter/flutter/issues/87875
311 312 313
}

class TestStatefulWidget extends StatefulWidget {
314
  const TestStatefulWidget({required this.child, super.key});
315 316 317 318 319 320 321 322 323 324 325 326

  final Widget child;

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

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