binding_test.dart 13.9 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
  List<AppLifecycleState> accumulatedStates = <AppLifecycleState>[];
21 22 23

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
24
    accumulatedStates.add(state);
25 26 27
  }
}

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 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
    await setAppLifeCycleState(AppLifecycleState.paused);
    expect(observer.accumulatedStates, <AppLifecycleState>[AppLifecycleState.paused]);

    observer.accumulatedStates.clear();
    await setAppLifeCycleState(AppLifecycleState.resumed);
    expect(observer.accumulatedStates, <AppLifecycleState>[
      AppLifecycleState.hidden,
      AppLifecycleState.inactive,
      AppLifecycleState.resumed,
    ]);

    observer.accumulatedStates.clear();
    await setAppLifeCycleState(AppLifecycleState.paused);
    expect(observer.accumulatedStates, <AppLifecycleState>[
      AppLifecycleState.inactive,
      AppLifecycleState.hidden,
      AppLifecycleState.paused,
    ]);

    observer.accumulatedStates.clear();
    await setAppLifeCycleState(AppLifecycleState.inactive);
    expect(observer.accumulatedStates, <AppLifecycleState>[
      AppLifecycleState.hidden,
      AppLifecycleState.inactive,
    ]);

    observer.accumulatedStates.clear();
    await setAppLifeCycleState(AppLifecycleState.hidden);
    expect(observer.accumulatedStates, <AppLifecycleState>[
      AppLifecycleState.hidden,
    ]);

    observer.accumulatedStates.clear();
    await setAppLifeCycleState(AppLifecycleState.paused);
    expect(observer.accumulatedStates, <AppLifecycleState>[
      AppLifecycleState.paused,
    ]);

    observer.accumulatedStates.clear();
    await setAppLifeCycleState(AppLifecycleState.detached);
    expect(observer.accumulatedStates, <AppLifecycleState>[
      AppLifecycleState.detached,
    ]);

    observer.accumulatedStates.clear();
    await setAppLifeCycleState(AppLifecycleState.resumed);
    expect(observer.accumulatedStates, <AppLifecycleState>[
      AppLifecycleState.resumed,
    ]);

    observer.accumulatedStates.clear();
    await expectLater(() async => setAppLifeCycleState(AppLifecycleState.detached), throwsAssertionError);
121
  });
122 123

  testWidgets('didPushRoute callback', (WidgetTester tester) async {
124
    final PushRouteObserver observer = PushRouteObserver();
125
    WidgetsBinding.instance.addObserver(observer);
126

127
    const String testRouteName = 'testRouteName';
128
    final ByteData message = const JSONMethodCodec().encodeMethodCall(const MethodCall('pushRoute', testRouteName));
129
    await tester.binding.defaultBinaryMessenger.handlePlatformMessage('flutter/navigation', message, (_) {});
130 131
    expect(observer.pushedRoute, testRouteName);

132
    WidgetsBinding.instance.removeObserver(observer);
133
  });
134

135 136
  testWidgets('didPushRouteInformation calls didPushRoute by default', (WidgetTester tester) async {
    final PushRouteObserver observer = PushRouteObserver();
137
    WidgetsBinding.instance.addObserver(observer);
138 139 140 141

    const Map<String, dynamic> testRouteInformation = <String, dynamic>{
      'location': 'testRouteName',
      'state': 'state',
142
      'restorationData': <dynamic, dynamic>{'test': 'config'},
143 144
    };
    final ByteData message = const JSONMethodCodec().encodeMethodCall(
145 146
      const MethodCall('pushRouteInformation', testRouteInformation),
    );
147
    await tester.binding.defaultBinaryMessenger
148
        .handlePlatformMessage('flutter/navigation', message, (_) {});
149
    expect(observer.pushedRoute, 'testRouteName');
150
    WidgetsBinding.instance.removeObserver(observer);
151 152
  });

153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184
  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);
  });

185 186
  testWidgets('didPushRouteInformation callback', (WidgetTester tester) async {
    final PushRouteInformationObserver observer = PushRouteInformationObserver();
187
    WidgetsBinding.instance.addObserver(observer);
188 189 190 191 192 193

    const Map<String, dynamic> testRouteInformation = <String, dynamic>{
      'location': 'testRouteName',
      'state': 'state',
    };
    final ByteData message = const JSONMethodCodec().encodeMethodCall(
194 195
      const MethodCall('pushRouteInformation', testRouteInformation),
    );
196
    await tester.binding.defaultBinaryMessenger.handlePlatformMessage('flutter/navigation', message, (_) { });
197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215
    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');
216
    expect(observer.pushedRouteInformation.state, 'state');
217
    WidgetsBinding.instance.removeObserver(observer);
218 219
  });

220 221
  testWidgets('didPushRouteInformation callback with null state', (WidgetTester tester) async {
    final PushRouteInformationObserver observer = PushRouteInformationObserver();
222
    WidgetsBinding.instance.addObserver(observer);
223 224 225 226 227 228

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

232
    await tester.binding.defaultBinaryMessenger.handlePlatformMessage('flutter/navigation', message, (_) { });
233
    expect(observer.pushedRouteInformation.uri.toString(), 'testRouteName');
234
    expect(observer.pushedRouteInformation.state, null);
235
    WidgetsBinding.instance.removeObserver(observer);
236 237
  });

238 239 240
  testWidgets('Application lifecycle affects frame scheduling', (WidgetTester tester) async {
    expect(tester.binding.hasScheduledFrame, isFalse);

241
    await setAppLifeCycleState(AppLifecycleState.paused);
242 243
    expect(tester.binding.hasScheduledFrame, isFalse);

244
    await setAppLifeCycleState(AppLifecycleState.resumed);
245 246 247 248
    expect(tester.binding.hasScheduledFrame, isTrue);
    await tester.pump();
    expect(tester.binding.hasScheduledFrame, isFalse);

249 250 251 252
    await setAppLifeCycleState(AppLifecycleState.inactive);
    expect(tester.binding.hasScheduledFrame, isFalse);

    await setAppLifeCycleState(AppLifecycleState.paused);
253 254
    expect(tester.binding.hasScheduledFrame, isFalse);

255
    await setAppLifeCycleState(AppLifecycleState.detached);
256 257
    expect(tester.binding.hasScheduledFrame, isFalse);

258
    await setAppLifeCycleState(AppLifecycleState.inactive);
259 260 261 262
    expect(tester.binding.hasScheduledFrame, isTrue);
    await tester.pump();
    expect(tester.binding.hasScheduledFrame, isFalse);

263
    await setAppLifeCycleState(AppLifecycleState.paused);
264 265 266 267 268 269
    expect(tester.binding.hasScheduledFrame, isFalse);

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

    tester.binding.scheduleForcedFrame();
270 271
    expect(tester.binding.hasScheduledFrame, isTrue);
    await tester.pump();
272 273

    int frameCount = 0;
274 275 276
    tester.binding.addPostFrameCallback((Duration duration) {
      frameCount += 1;
    });
277 278 279 280 281 282 283 284
    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);
285 286

    // Get the tester back to a resumed state for subsequent tests.
287
    await setAppLifeCycleState(AppLifecycleState.resumed);
288 289
    expect(tester.binding.hasScheduledFrame, isTrue);
    await tester.pump();
290
  });
291 292

  testWidgets('scheduleFrameCallback error control test', (WidgetTester tester) async {
293
    late FlutterError error;
294
    try {
295
      tester.binding.scheduleFrameCallback((Duration _) { }, rescheduling: true);
296 297 298 299 300 301 302 303 304 305 306
    } 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'
307
        'argument.\n',
308 309 310 311 312 313 314 315 316 317 318 319
      ),
    );
    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'
320
      '   argument.\n',
321 322
    );
  });
323 324

  testWidgets('defaultStackFilter elides framework Element mounting stacks', (WidgetTester tester) async {
325 326
    final FlutterExceptionHandler? oldHandler = FlutterError.onError;
    late FlutterErrorDetails errorDetails;
327
    FlutterError.onError = (FlutterErrorDetails details) {
328
      errorDetails = details;
329 330 331 332 333 334 335 336 337
    };
    await tester.pumpWidget(Directionality(
      textDirection: TextDirection.ltr,
      child: TestStatefulWidget(
        child: Builder(
          builder: (BuildContext context) {
            return Opacity(
              opacity: .5,
              child: Builder(
338 339 340 341
                builder: (BuildContext context) {
                  assert(false);
                  return const Text('');
                },
342 343 344 345 346 347 348
              ),
            );
          },
        ),
      ),
    ));
    FlutterError.onError = oldHandler;
349
    expect(errorDetails.exception, isAssertionError);
350
    const String toMatch = '...     Normal element mounting (';
351
    expect(toMatch.allMatches(errorDetails.toString()).length, 1);
352
  }, skip: kIsWeb); // https://github.com/flutter/flutter/issues/87875
353 354 355
}

class TestStatefulWidget extends StatefulWidget {
356
  const TestStatefulWidget({required this.child, super.key});
357 358 359 360 361 362 363 364 365 366 367 368

  final Widget child;

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

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