387 lines
14 KiB
Dart
387 lines
14 KiB
Dart
// 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 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
|
|
|
|
import 'restoration.dart';
|
|
|
|
void main() {
|
|
testWidgets('$RestorationManager dispatches memory events', (WidgetTester tester) async {
|
|
await expectLater(
|
|
await memoryEvents(() => RestorationManager().dispose(), RestorationManager),
|
|
areCreateAndDispose,
|
|
);
|
|
});
|
|
|
|
group('RestorationManager', () {
|
|
testWidgets('root bucket retrieval', (WidgetTester tester) async {
|
|
final callsToEngine = <MethodCall>[];
|
|
final result = Completer<Map<dynamic, dynamic>>();
|
|
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.restoration, (
|
|
MethodCall call,
|
|
) {
|
|
callsToEngine.add(call);
|
|
return result.future;
|
|
});
|
|
|
|
final manager = RestorationManager();
|
|
addTearDown(manager.dispose);
|
|
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);
|
|
addTearDown(child.dispose);
|
|
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 callsToEngine = <MethodCall>[];
|
|
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.restoration, (
|
|
MethodCall call,
|
|
) async {
|
|
callsToEngine.add(call);
|
|
return null;
|
|
});
|
|
final manager = RestorationManager();
|
|
addTearDown(manager.dispose);
|
|
|
|
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 callsToEngine = <MethodCall>[];
|
|
final result = Completer<Map<dynamic, dynamic>>();
|
|
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.restoration, (
|
|
MethodCall call,
|
|
) {
|
|
callsToEngine.add(call);
|
|
return result.future;
|
|
});
|
|
final manager = RestorationManager();
|
|
addTearDown(manager.dispose);
|
|
|
|
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 manager = RestorationManager();
|
|
addTearDown(manager.dispose);
|
|
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);
|
|
|
|
var 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);
|
|
addTearDown(newChild.dispose);
|
|
expect(newChild.read<String>('bar'), 'Hello');
|
|
});
|
|
|
|
testWidgets('returns null as root bucket when restoration is disabled', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final callsToEngine = <MethodCall>[];
|
|
final result = Completer<Map<dynamic, dynamic>>();
|
|
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.restoration, (
|
|
MethodCall call,
|
|
) {
|
|
callsToEngine.add(call);
|
|
return result.future;
|
|
});
|
|
var listenerCount = 0;
|
|
final manager = RestorationManager()
|
|
..addListener(() {
|
|
listenerCount++;
|
|
});
|
|
addTearDown(manager.dispose);
|
|
RestorationBucket? rootBucket;
|
|
var 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 callsToEngine = <MethodCall>[];
|
|
final result = Completer<Map<dynamic, dynamic>>();
|
|
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.restoration, (
|
|
MethodCall call,
|
|
) {
|
|
callsToEngine.add(call);
|
|
return result.future;
|
|
});
|
|
|
|
final manager = RestorationManager();
|
|
addTearDown(manager.dispose);
|
|
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 result = Completer<Map<dynamic, dynamic>>();
|
|
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.restoration, (
|
|
MethodCall call,
|
|
) {
|
|
return result.future;
|
|
});
|
|
|
|
final manager = TestRestorationManager();
|
|
addTearDown(manager.dispose);
|
|
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 TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
|
|
'flutter/restoration',
|
|
const StandardMethodCodec().encodeMethodCall(MethodCall('push', data)),
|
|
(_) {},
|
|
);
|
|
}
|
|
|
|
Map<dynamic, dynamic> _createEncodedRestorationData1() {
|
|
final 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 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);
|
|
}
|
|
}
|