blob: cc132308c3ac6bc0736a3b1a76dd3541ae243d10 [file] [log] [blame]
// 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 'dart:typed_data';
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);
});
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);
}
}