| // Copyright 2014 The Flutter 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 'package:flutter/scheduler.dart'; |
| import 'package:flutter/services.dart'; |
| import 'package:flutter/widgets.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| |
| import 'restoration.dart'; |
| |
| void main() { |
| group('RestorationManager', () { |
| testWidgets('root bucket retrieval', (WidgetTester tester) async { |
| final List<MethodCall> callsToEngine = <MethodCall>[]; |
| final Completer<Map<dynamic, dynamic>> result = Completer<Map<dynamic, dynamic>>(); |
| tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.restoration, (MethodCall call) { |
| callsToEngine.add(call); |
| return result.future; |
| }); |
| |
| final RestorationManager manager = RestorationManager(); |
| final Future<RestorationBucket?> rootBucketFuture = manager.rootBucket; |
| RestorationBucket? rootBucket; |
| rootBucketFuture.then((RestorationBucket? bucket) { |
| rootBucket = bucket; |
| }); |
| expect(rootBucketFuture, isNotNull); |
| expect(rootBucket, isNull); |
| |
| // Accessing rootBucket again gives same future. |
| expect(manager.rootBucket, same(rootBucketFuture)); |
| |
| // Engine has only been contacted once. |
| expect(callsToEngine, hasLength(1)); |
| expect(callsToEngine.single.method, 'get'); |
| |
| // Complete the engine request. |
| result.complete(_createEncodedRestorationData1()); |
| await tester.pump(); |
| |
| // Root bucket future completed. |
| expect(rootBucket, isNotNull); |
| |
| // Root bucket contains the expected data. |
| expect(rootBucket!.read<int>('value1'), 10); |
| expect(rootBucket!.read<String>('value2'), 'Hello'); |
| final RestorationBucket child = rootBucket!.claimChild('child1', debugOwner: null); |
| expect(child.read<int>('another value'), 22); |
| |
| // Accessing the root bucket again completes synchronously with same bucket. |
| RestorationBucket? synchronousBucket; |
| manager.rootBucket.then((RestorationBucket? bucket) { |
| synchronousBucket = bucket; |
| }); |
| expect(synchronousBucket, isNotNull); |
| expect(synchronousBucket, same(rootBucket)); |
| }); |
| |
| testWidgets('root bucket received from engine before retrieval', (WidgetTester tester) async { |
| SystemChannels.restoration.setMethodCallHandler(null); |
| final List<MethodCall> callsToEngine = <MethodCall>[]; |
| tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.restoration, (MethodCall call) async { |
| callsToEngine.add(call); |
| return null; |
| }); |
| final RestorationManager manager = RestorationManager(); |
| |
| await _pushDataFromEngine(_createEncodedRestorationData1()); |
| |
| RestorationBucket? rootBucket; |
| manager.rootBucket.then((RestorationBucket? bucket) => rootBucket = bucket); |
| // Root bucket is available synchronously. |
| expect(rootBucket, isNotNull); |
| // Engine was never asked. |
| expect(callsToEngine, isEmpty); |
| }); |
| |
| testWidgets('root bucket received while engine retrieval is pending', (WidgetTester tester) async { |
| SystemChannels.restoration.setMethodCallHandler(null); |
| final List<MethodCall> callsToEngine = <MethodCall>[]; |
| final Completer<Map<dynamic, dynamic>> result = Completer<Map<dynamic, dynamic>>(); |
| tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.restoration, (MethodCall call) { |
| callsToEngine.add(call); |
| return result.future; |
| }); |
| final RestorationManager manager = RestorationManager(); |
| |
| RestorationBucket? rootBucket; |
| manager.rootBucket.then((RestorationBucket? bucket) => rootBucket = bucket); |
| expect(rootBucket, isNull); |
| expect(callsToEngine.single.method, 'get'); |
| |
| await _pushDataFromEngine(_createEncodedRestorationData1()); |
| expect(rootBucket, isNotNull); |
| expect(rootBucket!.read<int>('value1'), 10); |
| |
| result.complete(_createEncodedRestorationData2()); |
| await tester.pump(); |
| |
| RestorationBucket? rootBucket2; |
| manager.rootBucket.then((RestorationBucket? bucket) => rootBucket2 = bucket); |
| expect(rootBucket2, isNotNull); |
| expect(rootBucket2, same(rootBucket)); |
| expect(rootBucket2!.read<int>('value1'), 10); |
| expect(rootBucket2!.contains('foo'), isFalse); |
| }); |
| |
| testWidgets('root bucket is properly replaced when new data is available', (WidgetTester tester) async { |
| tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.restoration, (MethodCall call) async { |
| return _createEncodedRestorationData1(); |
| }); |
| final RestorationManager manager = RestorationManager(); |
| RestorationBucket? rootBucket; |
| manager.rootBucket.then((RestorationBucket? bucket) { |
| rootBucket = bucket; |
| }); |
| await tester.pump(); |
| expect(rootBucket, isNotNull); |
| expect(rootBucket!.read<int>('value1'), 10); |
| final RestorationBucket child = rootBucket!.claimChild('child1', debugOwner: null); |
| expect(child.read<int>('another value'), 22); |
| |
| bool rootReplaced = false; |
| RestorationBucket? newRoot; |
| manager.addListener(() { |
| rootReplaced = true; |
| manager.rootBucket.then((RestorationBucket? bucket) { |
| newRoot = bucket; |
| }); |
| // The new bucket is available synchronously. |
| expect(newRoot, isNotNull); |
| }); |
| |
| // Send new Data. |
| await _pushDataFromEngine(_createEncodedRestorationData2()); |
| |
| expect(rootReplaced, isTrue); |
| expect(newRoot, isNot(same(rootBucket))); |
| |
| child.dispose(); |
| |
| expect(newRoot!.read<int>('foo'), 33); |
| expect(newRoot!.read<int>('value1'), null); |
| final RestorationBucket newChild = newRoot!.claimChild('childFoo', debugOwner: null); |
| expect(newChild.read<String>('bar'), 'Hello'); |
| }); |
| |
| testWidgets('returns null as root bucket when restoration is disabled', (WidgetTester tester) async { |
| final List<MethodCall> callsToEngine = <MethodCall>[]; |
| final Completer<Map<dynamic, dynamic>> result = Completer<Map<dynamic, dynamic>>(); |
| tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.restoration, (MethodCall call) { |
| callsToEngine.add(call); |
| return result.future; |
| }); |
| int listenerCount = 0; |
| final RestorationManager manager = RestorationManager()..addListener(() { |
| listenerCount++; |
| }); |
| RestorationBucket? rootBucket; |
| bool rootBucketResolved = false; |
| manager.rootBucket.then((RestorationBucket? bucket) { |
| rootBucketResolved = true; |
| rootBucket = bucket; |
| }); |
| await tester.pump(); |
| expect(rootBucketResolved, isFalse); |
| expect(listenerCount, 0); |
| |
| result.complete(_packageRestorationData(enabled: false)); |
| await tester.pump(); |
| expect(rootBucketResolved, isTrue); |
| expect(rootBucket, isNull); |
| |
| // Switch to non-null. |
| await _pushDataFromEngine(_createEncodedRestorationData1()); |
| expect(listenerCount, 1); |
| manager.rootBucket.then((RestorationBucket? bucket) { |
| rootBucket = bucket; |
| }); |
| expect(rootBucket, isNotNull); |
| |
| // Switch to null again. |
| await _pushDataFromEngine(_packageRestorationData(enabled: false)); |
| expect(listenerCount, 2); |
| manager.rootBucket.then((RestorationBucket? bucket) { |
| rootBucket = bucket; |
| }); |
| expect(rootBucket, isNull); |
| }); |
| |
| testWidgets('flushData', (WidgetTester tester) async { |
| final List<MethodCall> callsToEngine = <MethodCall>[]; |
| final Completer<Map<dynamic, dynamic>> result = Completer<Map<dynamic, dynamic>>(); |
| tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.restoration, (MethodCall call) { |
| callsToEngine.add(call); |
| return result.future; |
| }); |
| |
| final RestorationManager manager = RestorationManager(); |
| final Future<RestorationBucket?> rootBucketFuture = manager.rootBucket; |
| RestorationBucket? rootBucket; |
| rootBucketFuture.then((RestorationBucket? bucket) { |
| rootBucket = bucket; |
| }); |
| result.complete(_createEncodedRestorationData1()); |
| await tester.pump(); |
| expect(rootBucket, isNotNull); |
| callsToEngine.clear(); |
| |
| // Schedule a frame. |
| SchedulerBinding.instance.ensureVisualUpdate(); |
| rootBucket!.write('foo', 1); |
| // flushData is no-op because frame is scheduled. |
| manager.flushData(); |
| expect(callsToEngine, isEmpty); |
| // Data is flushed at the end of the frame. |
| await tester.pump(); |
| expect(callsToEngine, hasLength(1)); |
| callsToEngine.clear(); |
| |
| // flushData without frame sends data directly. |
| rootBucket!.write('foo', 2); |
| manager.flushData(); |
| expect(callsToEngine, hasLength(1)); |
| }); |
| |
| testWidgets('isReplacing', (WidgetTester tester) async { |
| final Completer<Map<dynamic, dynamic>> result = Completer<Map<dynamic, dynamic>>(); |
| tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.restoration, (MethodCall call) { |
| return result.future; |
| }); |
| |
| final TestRestorationManager manager = TestRestorationManager(); |
| expect(manager.isReplacing, isFalse); |
| |
| RestorationBucket? rootBucket; |
| manager.rootBucket.then((RestorationBucket? bucket) { |
| rootBucket = bucket; |
| }); |
| result.complete(_createEncodedRestorationData1()); |
| await tester.idle(); |
| expect(rootBucket, isNotNull); |
| expect(rootBucket!.isReplacing, isFalse); |
| expect(manager.isReplacing, isFalse); |
| tester.binding.scheduleFrame(); |
| await tester.pump(); |
| expect(manager.isReplacing, isFalse); |
| expect(rootBucket!.isReplacing, isFalse); |
| |
| manager.receiveDataFromEngine(enabled: true, data: null); |
| RestorationBucket? rootBucket2; |
| manager.rootBucket.then((RestorationBucket? bucket) { |
| rootBucket2 = bucket; |
| }); |
| expect(rootBucket2, isNotNull); |
| expect(rootBucket2, isNot(same(rootBucket))); |
| expect(manager.isReplacing, isTrue); |
| expect(rootBucket2!.isReplacing, isTrue); |
| await tester.idle(); |
| expect(manager.isReplacing, isTrue); |
| expect(rootBucket2!.isReplacing, isTrue); |
| tester.binding.scheduleFrame(); |
| await tester.pump(); |
| expect(manager.isReplacing, isFalse); |
| expect(rootBucket2!.isReplacing, isFalse); |
| |
| manager.receiveDataFromEngine(enabled: false, data: null); |
| RestorationBucket? rootBucket3; |
| manager.rootBucket.then((RestorationBucket? bucket) { |
| rootBucket3 = bucket; |
| }); |
| expect(rootBucket3, isNull); |
| expect(manager.isReplacing, isFalse); |
| await tester.idle(); |
| expect(manager.isReplacing, isFalse); |
| tester.binding.scheduleFrame(); |
| await tester.pump(); |
| expect(manager.isReplacing, isFalse); |
| }); |
| }); |
| |
| test('debugIsSerializableForRestoration', () { |
| expect(debugIsSerializableForRestoration(Object()), isFalse); |
| expect(debugIsSerializableForRestoration(Container()), isFalse); |
| |
| expect(debugIsSerializableForRestoration(null), isTrue); |
| expect(debugIsSerializableForRestoration(147823), isTrue); |
| expect(debugIsSerializableForRestoration(12.43), isTrue); |
| expect(debugIsSerializableForRestoration('Hello World'), isTrue); |
| expect(debugIsSerializableForRestoration(<int>[12, 13, 14]), isTrue); |
| expect(debugIsSerializableForRestoration(<String, int>{'v1' : 10, 'v2' : 23}), isTrue); |
| expect(debugIsSerializableForRestoration(<String, dynamic>{ |
| 'hello': <int>[12, 12, 12], |
| 'world': <int, bool>{ |
| 1: true, |
| 2: false, |
| 4: true, |
| }, |
| }), isTrue); |
| }); |
| } |
| |
| Future<void> _pushDataFromEngine(Map<dynamic, dynamic> data) async { |
| await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage( |
| 'flutter/restoration', |
| const StandardMethodCodec().encodeMethodCall(MethodCall('push', data)), |
| (_) { }, |
| ); |
| } |
| |
| Map<dynamic, dynamic> _createEncodedRestorationData1() { |
| final Map<String, dynamic> data = <String, dynamic>{ |
| valuesMapKey: <String, dynamic>{ |
| 'value1' : 10, |
| 'value2' : 'Hello', |
| }, |
| childrenMapKey: <String, dynamic>{ |
| 'child1' : <String, dynamic>{ |
| valuesMapKey : <String, dynamic>{ |
| 'another value': 22, |
| }, |
| }, |
| }, |
| }; |
| return _packageRestorationData(data: data); |
| } |
| |
| Map<dynamic, dynamic> _createEncodedRestorationData2() { |
| final Map<String, dynamic> data = <String, dynamic>{ |
| valuesMapKey: <String, dynamic>{ |
| 'foo' : 33, |
| }, |
| childrenMapKey: <String, dynamic>{ |
| 'childFoo' : <String, dynamic>{ |
| valuesMapKey : <String, dynamic>{ |
| 'bar': 'Hello', |
| }, |
| }, |
| }, |
| }; |
| return _packageRestorationData(data: data); |
| } |
| |
| Map<dynamic, dynamic> _packageRestorationData({bool enabled = true, Map<dynamic, dynamic>? data}) { |
| final ByteData? encoded = const StandardMessageCodec().encodeMessage(data); |
| return <dynamic, dynamic>{ |
| 'enabled': enabled, |
| 'data': encoded?.buffer.asUint8List(encoded.offsetInBytes, encoded.lengthInBytes), |
| }; |
| } |
| |
| class TestRestorationManager extends RestorationManager { |
| void receiveDataFromEngine({required bool enabled, required Uint8List? data}) { |
| handleRestorationUpdateFromEngine(enabled: enabled, data: data); |
| } |
| } |