// 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 = []; final result = Completer>(); tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.restoration, ( MethodCall call, ) { callsToEngine.add(call); return result.future; }); final manager = RestorationManager(); addTearDown(manager.dispose); final Future 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('value1'), 10); expect(rootBucket!.read('value2'), 'Hello'); final RestorationBucket child = rootBucket!.claimChild('child1', debugOwner: null); addTearDown(child.dispose); expect(child.read('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 = []; 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 = []; final result = Completer>(); 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('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('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('value1'), 10); final RestorationBucket child = rootBucket!.claimChild('child1', debugOwner: null); expect(child.read('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('foo'), 33); expect(newRoot!.read('value1'), null); final RestorationBucket newChild = newRoot!.claimChild('childFoo', debugOwner: null); addTearDown(newChild.dispose); expect(newChild.read('bar'), 'Hello'); }); testWidgets('returns null as root bucket when restoration is disabled', ( WidgetTester tester, ) async { final callsToEngine = []; final result = Completer>(); 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 = []; final result = Completer>(); tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.restoration, ( MethodCall call, ) { callsToEngine.add(call); return result.future; }); final manager = RestorationManager(); addTearDown(manager.dispose); final Future 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>(); 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([12, 13, 14]), isTrue); expect(debugIsSerializableForRestoration({'v1': 10, 'v2': 23}), isTrue); expect( debugIsSerializableForRestoration({ 'hello': [12, 12, 12], 'world': {1: true, 2: false, 4: true}, }), isTrue, ); }); } Future _pushDataFromEngine(Map data) async { await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage( 'flutter/restoration', const StandardMethodCodec().encodeMethodCall(MethodCall('push', data)), (_) {}, ); } Map _createEncodedRestorationData1() { final data = { valuesMapKey: {'value1': 10, 'value2': 'Hello'}, childrenMapKey: { 'child1': { valuesMapKey: {'another value': 22}, }, }, }; return _packageRestorationData(data: data); } Map _createEncodedRestorationData2() { final data = { valuesMapKey: {'foo': 33}, childrenMapKey: { 'childFoo': { valuesMapKey: {'bar': 'Hello'}, }, }, }; return _packageRestorationData(data: data); } Map _packageRestorationData({bool enabled = true, Map? data}) { final ByteData? encoded = const StandardMessageCodec().encodeMessage(data); return { '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); } }