blob: 9c6cbc8525995228f9f2a7a36dbaba77aee65f08 [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 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'restoration.dart';
void main() {
final TestAutomatedTestWidgetsFlutterBinding binding = TestAutomatedTestWidgetsFlutterBinding();
setUp(() {
binding._restorationManager = MockRestorationManager();
});
testWidgets('does not inject root bucket if inside scope', (WidgetTester tester) async {
final MockRestorationManager manager = MockRestorationManager();
final Map<String, dynamic> rawData = <String, dynamic>{};
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);
expect(rawData, isEmpty);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: UnmanagedRestorationScope(
bucket: root,
child: const RootRestorationScope(
restorationId: 'root-child',
child: BucketSpy(
child: Text('Hello'),
),
),
),
),
);
manager.doSerialization();
expect(binding.restorationManager.rootBucketAccessed, 0);
final BucketSpyState state = tester.state(find.byType(BucketSpy));
expect(state.bucket!.restorationId, 'root-child');
expect((rawData[childrenMapKey] as Map<Object?, Object?>).containsKey('root-child'), isTrue);
expect(find.text('Hello'), findsOneWidget);
});
testWidgets('waits for root bucket', (WidgetTester tester) async {
final Completer<RestorationBucket> bucketCompleter = Completer<RestorationBucket>();
binding.restorationManager.rootBucket = bucketCompleter.future;
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: RootRestorationScope(
restorationId: 'root-child',
child: BucketSpy(
child: Text('Hello'),
),
),
),
);
expect(binding.restorationManager.rootBucketAccessed, 1);
// Child rendering is delayed until root bucket is available.
expect(find.text('Hello'), findsNothing);
expect(binding.firstFrameIsDeferred, isTrue);
// Complete the future.
final Map<String, dynamic> rawData = <String, dynamic>{};
final RestorationBucket root = RestorationBucket.root(manager: binding.restorationManager, rawData: rawData);
bucketCompleter.complete(root);
await tester.pump(const Duration(milliseconds: 100));
expect(binding.restorationManager.rootBucketAccessed, 1);
expect(find.text('Hello'), findsOneWidget);
expect(binding.firstFrameIsDeferred, isFalse);
final BucketSpyState state = tester.state(find.byType(BucketSpy));
expect(state.bucket!.restorationId, 'root-child');
expect((rawData[childrenMapKey] as Map<Object?, Object?>).containsKey('root-child'), isTrue);
});
testWidgets('no delay when root is available synchronously', (WidgetTester tester) async {
final Map<String, dynamic> rawData = <String, dynamic>{};
final RestorationBucket root = RestorationBucket.root(manager: binding.restorationManager, rawData: rawData);
binding.restorationManager.rootBucket = SynchronousFuture<RestorationBucket>(root);
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: RootRestorationScope(
restorationId: 'root-child',
child: BucketSpy(
child: Text('Hello'),
),
),
),
);
expect(binding.restorationManager.rootBucketAccessed, 1);
expect(find.text('Hello'), findsOneWidget);
expect(binding.firstFrameIsDeferred, isFalse);
final BucketSpyState state = tester.state(find.byType(BucketSpy));
expect(state.bucket!.restorationId, 'root-child');
expect((rawData[childrenMapKey] as Map<Object?, Object?>).containsKey('root-child'), isTrue);
});
testWidgets('does not insert root when restoration id is null', (WidgetTester tester) async {
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: RootRestorationScope(
restorationId: null,
child: BucketSpy(
child: Text('Hello'),
),
),
),
);
expect(binding.restorationManager.rootBucketAccessed, 0);
expect(find.text('Hello'), findsOneWidget);
expect(binding.firstFrameIsDeferred, isFalse);
final BucketSpyState state = tester.state(find.byType(BucketSpy));
expect(state.bucket, isNull);
// Change restoration id to non-null.
final Completer<RestorationBucket> bucketCompleter = Completer<RestorationBucket>();
binding.restorationManager.rootBucket = bucketCompleter.future;
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: RootRestorationScope(
restorationId: 'root-child',
child: BucketSpy(
child: Text('Hello'),
),
),
),
);
expect(binding.restorationManager.rootBucketAccessed, 1);
expect(find.text('Hello'), findsOneWidget);
expect(state.bucket, isNull); // root bucket future has not completed yet.
// Complete the future.
final RestorationBucket root = RestorationBucket.root(manager: binding.restorationManager, rawData: <String, dynamic>{});
bucketCompleter.complete(root);
await tester.pump(const Duration(milliseconds: 100));
expect(binding.restorationManager.rootBucketAccessed, 1);
expect(find.text('Hello'), findsOneWidget);
expect(state.bucket!.restorationId, 'root-child');
// Change ID back to null.
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: RootRestorationScope(
restorationId: null,
child: BucketSpy(
child: Text('Hello'),
),
),
),
);
expect(binding.restorationManager.rootBucketAccessed, 1);
expect(find.text('Hello'), findsOneWidget);
expect(state.bucket, isNull);
});
testWidgets('injects root bucket when moved out of scope', (WidgetTester tester) async {
final Key rootScopeKey = GlobalKey();
final MockRestorationManager manager = MockRestorationManager();
final Map<String, dynamic> inScopeRawData = <String, dynamic>{};
final RestorationBucket inScopeRootBucket = RestorationBucket.root(manager: manager, rawData: inScopeRawData);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: UnmanagedRestorationScope(
bucket: inScopeRootBucket,
child: RootRestorationScope(
key: rootScopeKey,
restorationId: 'root-child',
child: const BucketSpy(
child: Text('Hello'),
),
),
),
),
);
expect(binding.restorationManager.rootBucketAccessed, 0);
expect(find.text('Hello'), findsOneWidget);
final BucketSpyState state = tester.state(find.byType(BucketSpy));
expect(state.bucket!.restorationId, 'root-child');
expect((inScopeRawData[childrenMapKey] as Map<Object?, Object?>).containsKey('root-child'), isTrue);
// Move out of scope.
final Completer<RestorationBucket> bucketCompleter = Completer<RestorationBucket>();
binding.restorationManager.rootBucket = bucketCompleter.future;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: RootRestorationScope(
key: rootScopeKey,
restorationId: 'root-child',
child: const BucketSpy(
child: Text('Hello'),
),
),
),
);
expect(binding.restorationManager.rootBucketAccessed, 1);
expect(find.text('Hello'), findsOneWidget);
final Map<String, dynamic> outOfScopeRawData = <String, dynamic>{};
final RestorationBucket outOfScopeRootBucket = RestorationBucket.root(manager: binding.restorationManager, rawData: outOfScopeRawData);
bucketCompleter.complete(outOfScopeRootBucket);
await tester.pump(const Duration(milliseconds: 100));
expect(binding.restorationManager.rootBucketAccessed, 1);
expect(find.text('Hello'), findsOneWidget);
expect(state.bucket!.restorationId, 'root-child');
expect((outOfScopeRawData[childrenMapKey] as Map<Object?, Object?>).containsKey('root-child'), isTrue);
expect(inScopeRawData, isEmpty);
// Move into scope.
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: UnmanagedRestorationScope(
bucket: inScopeRootBucket,
child: RootRestorationScope(
key: rootScopeKey,
restorationId: 'root-child',
child: const BucketSpy(
child: Text('Hello'),
),
),
),
),
);
expect(binding.restorationManager.rootBucketAccessed, 1);
expect(find.text('Hello'), findsOneWidget);
expect(state.bucket!.restorationId, 'root-child');
expect(outOfScopeRawData, isEmpty);
expect((inScopeRawData[childrenMapKey] as Map<Object?, Object?>).containsKey('root-child'), isTrue);
});
testWidgets('injects new root when old one is decommissioned', (WidgetTester tester) async {
final Map<String, dynamic> firstRawData = <String, dynamic>{};
final RestorationBucket firstRoot = RestorationBucket.root(manager: binding.restorationManager, rawData: firstRawData);
binding.restorationManager.rootBucket = SynchronousFuture<RestorationBucket>(firstRoot);
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: RootRestorationScope(
restorationId: 'root-child',
child: BucketSpy(
child: Text('Hello'),
),
),
),
);
expect(binding.restorationManager.rootBucketAccessed, 1);
expect(find.text('Hello'), findsOneWidget);
final BucketSpyState state = tester.state(find.byType(BucketSpy));
state.bucket!.write('foo', 42);
expect((((firstRawData[childrenMapKey] as Map<Object?, Object?>)['root-child']! as Map<String, dynamic>)[valuesMapKey] as Map<Object?, Object?>)['foo'], 42);
final RestorationBucket firstBucket = state.bucket!;
// Replace with new root.
final Map<String, dynamic> secondRawData = <String, dynamic>{
childrenMapKey: <String, dynamic>{
'root-child': <String, dynamic>{
valuesMapKey: <String, dynamic>{
'foo': 22,
},
},
},
};
final RestorationBucket secondRoot = RestorationBucket.root(manager: binding.restorationManager, rawData: secondRawData);
binding.restorationManager.rootBucket = SynchronousFuture<RestorationBucket>(secondRoot);
await tester.pump();
firstRoot.dispose();
expect(state.bucket, isNot(same(firstBucket)));
expect(state.bucket!.read<int>('foo'), 22);
});
testWidgets('injects null when rootBucket is null', (WidgetTester tester) async {
final Completer<RestorationBucket?> completer = Completer<RestorationBucket?>();
binding.restorationManager.rootBucket = completer.future;
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: RootRestorationScope(
restorationId: 'root-child',
child: BucketSpy(
child: Text('Hello'),
),
),
),
);
expect(binding.restorationManager.rootBucketAccessed, 1);
expect(find.text('Hello'), findsNothing);
completer.complete(null);
await tester.pump(const Duration(milliseconds: 100));
expect(binding.restorationManager.rootBucketAccessed, 1);
expect(find.text('Hello'), findsOneWidget);
final BucketSpyState state = tester.state(find.byType(BucketSpy));
expect(state.bucket, isNull);
final RestorationBucket root = RestorationBucket.root(manager: binding.restorationManager, rawData: null);
binding.restorationManager.rootBucket = SynchronousFuture<RestorationBucket>(root);
await tester.pump();
expect(binding.restorationManager.rootBucketAccessed, 2);
expect(find.text('Hello'), findsOneWidget);
expect(state.bucket, isNotNull);
});
testWidgets('can switch to null', (WidgetTester tester) async {
final RestorationBucket root = RestorationBucket.root(manager: binding.restorationManager, rawData: null);
binding.restorationManager.rootBucket = SynchronousFuture<RestorationBucket>(root);
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: RootRestorationScope(
restorationId: 'root-child',
child: BucketSpy(
child: Text('Hello'),
),
),
),
);
expect(binding.restorationManager.rootBucketAccessed, 1);
expect(find.text('Hello'), findsOneWidget);
final BucketSpyState state = tester.state(find.byType(BucketSpy));
expect(state.bucket, isNotNull);
binding.restorationManager.rootBucket = SynchronousFuture<RestorationBucket?>(null);
await tester.pump();
root.dispose();
expect(binding.restorationManager.rootBucketAccessed, 2);
expect(find.text('Hello'), findsOneWidget);
expect(state.bucket, isNull);
});
}
class TestAutomatedTestWidgetsFlutterBinding extends AutomatedTestWidgetsFlutterBinding {
late MockRestorationManager _restorationManager;
@override
MockRestorationManager get restorationManager => _restorationManager;
@override
TestRestorationManager createRestorationManager() {
return TestRestorationManager();
}
int _deferred = 0;
bool get firstFrameIsDeferred => _deferred > 0;
@override
void deferFirstFrame() {
_deferred++;
super.deferFirstFrame();
}
@override
void allowFirstFrame() {
_deferred--;
super.allowFirstFrame();
}
}