blob: 0a51570db80623a1f628066461dacbc8deab4963 [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 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'restoration.dart';
void main() {
group('UnmanagedRestorationScope', () {
testWidgets('makes bucket available to descendants', (WidgetTester tester) async {
final RestorationBucket bucket1 = RestorationBucket.empty(
restorationId: 'foo',
debugOwner: 'owner',
);
addTearDown(bucket1.dispose);
await tester.pumpWidget(UnmanagedRestorationScope(bucket: bucket1, child: const BucketSpy()));
final BucketSpyState state = tester.state(find.byType(BucketSpy));
expect(state.bucket, bucket1);
// Notifies when bucket changes.
final RestorationBucket bucket2 = RestorationBucket.empty(
restorationId: 'foo2',
debugOwner: 'owner',
);
addTearDown(bucket2.dispose);
await tester.pumpWidget(UnmanagedRestorationScope(bucket: bucket2, child: const BucketSpy()));
expect(state.bucket, bucket2);
});
testWidgets('null bucket disables restoration', (WidgetTester tester) async {
await tester.pumpWidget(const UnmanagedRestorationScope(child: BucketSpy()));
final BucketSpyState state = tester.state(find.byType(BucketSpy));
expect(state.bucket, isNull);
});
});
group('RestorationScope', () {
testWidgets('asserts when none is found', (WidgetTester tester) async {
late BuildContext capturedContext;
await tester.pumpWidget(
WidgetsApp(
color: const Color(0xD0FF0000),
builder: (_, _) {
return RestorationScope(
restorationId: 'test',
child: Builder(
builder: (BuildContext context) {
capturedContext = context;
return Container();
},
),
);
},
),
);
expect(
() {
RestorationScope.of(capturedContext);
},
throwsA(
isA<FlutterError>().having(
(FlutterError error) => error.message,
'message',
contains('State restoration must be enabled for a RestorationScope'),
),
),
);
await tester.pumpWidget(
WidgetsApp(
restorationScopeId: 'test scope',
color: const Color(0xD0FF0000),
builder: (_, _) {
return RestorationScope(
restorationId: 'test',
child: Builder(
builder: (BuildContext context) {
capturedContext = context;
return Container();
},
),
);
},
),
);
final UnmanagedRestorationScope scope = tester.widget(
find.byType(UnmanagedRestorationScope).last,
);
expect(RestorationScope.of(capturedContext), scope.bucket);
});
testWidgets('makes bucket available to descendants', (WidgetTester tester) async {
const String id = 'hello world 1234';
final MockRestorationManager manager = MockRestorationManager();
addTearDown(manager.dispose);
final Map<String, dynamic> rawData = <String, dynamic>{};
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);
addTearDown(root.dispose);
expect(rawData, isEmpty);
await tester.pumpWidget(
UnmanagedRestorationScope(
bucket: root,
child: const RestorationScope(restorationId: id, child: BucketSpy()),
),
);
manager.doSerialization();
final BucketSpyState state = tester.state(find.byType(BucketSpy));
expect(state.bucket!.restorationId, id);
expect((rawData[childrenMapKey] as Map<Object?, Object?>).containsKey(id), isTrue);
});
testWidgets('bucket for descendants contains data claimed from parent', (
WidgetTester tester,
) async {
final MockRestorationManager manager = MockRestorationManager();
addTearDown(manager.dispose);
final RestorationBucket root = RestorationBucket.root(
manager: manager,
rawData: _createRawDataSet(),
);
addTearDown(root.dispose);
await tester.pumpWidget(
UnmanagedRestorationScope(
bucket: root,
child: const RestorationScope(restorationId: 'child1', child: BucketSpy()),
),
);
manager.doSerialization();
final BucketSpyState state = tester.state(find.byType(BucketSpy));
expect(state.bucket!.restorationId, 'child1');
expect(state.bucket!.read<int>('foo'), 22);
});
testWidgets('renames existing bucket when new ID is provided', (WidgetTester tester) async {
final MockRestorationManager manager = MockRestorationManager();
addTearDown(manager.dispose);
final RestorationBucket root = RestorationBucket.root(
manager: manager,
rawData: _createRawDataSet(),
);
addTearDown(root.dispose);
await tester.pumpWidget(
UnmanagedRestorationScope(
bucket: root,
child: const RestorationScope(restorationId: 'child1', child: BucketSpy()),
),
);
manager.doSerialization();
// Claimed existing bucket with data.
final BucketSpyState state = tester.state(find.byType(BucketSpy));
expect(state.bucket!.restorationId, 'child1');
expect(state.bucket!.read<int>('foo'), 22);
final RestorationBucket bucket = state.bucket!;
// Rename the existing bucket.
await tester.pumpWidget(
UnmanagedRestorationScope(
bucket: root,
child: const RestorationScope(restorationId: 'something else', child: BucketSpy()),
),
);
manager.doSerialization();
expect(state.bucket!.restorationId, 'something else');
expect(state.bucket!.read<int>('foo'), 22);
expect(state.bucket, same(bucket));
});
testWidgets('Disposing a scope removes its data', (WidgetTester tester) async {
final MockRestorationManager manager = MockRestorationManager();
addTearDown(manager.dispose);
final Map<String, dynamic> rawData = _createRawDataSet();
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);
addTearDown(root.dispose);
expect((rawData[childrenMapKey] as Map<String, dynamic>).containsKey('child1'), isTrue);
await tester.pumpWidget(
UnmanagedRestorationScope(
bucket: root,
child: const RestorationScope(restorationId: 'child1', child: BucketSpy()),
),
);
manager.doSerialization();
expect((rawData[childrenMapKey] as Map<String, dynamic>).containsKey('child1'), isTrue);
await tester.pumpWidget(UnmanagedRestorationScope(bucket: root, child: Container()));
manager.doSerialization();
expect((rawData[childrenMapKey] as Map<String, dynamic>).containsKey('child1'), isFalse);
});
testWidgets('no bucket for descendants when id is null', (WidgetTester tester) async {
final MockRestorationManager manager = MockRestorationManager();
addTearDown(manager.dispose);
final RestorationBucket root = RestorationBucket.root(
manager: manager,
rawData: <String, dynamic>{},
);
addTearDown(root.dispose);
await tester.pumpWidget(
UnmanagedRestorationScope(
bucket: root,
child: const RestorationScope(restorationId: null, child: BucketSpy()),
),
);
final BucketSpyState state = tester.state(find.byType(BucketSpy));
expect(state.bucket, isNull);
// Change id to non-null.
await tester.pumpWidget(
UnmanagedRestorationScope(
bucket: root,
child: const RestorationScope(restorationId: 'foo', child: BucketSpy()),
),
);
manager.doSerialization();
expect(state.bucket, isNotNull);
expect(state.bucket!.restorationId, 'foo');
// Change id back to null.
await tester.pumpWidget(
UnmanagedRestorationScope(
bucket: root,
child: const RestorationScope(restorationId: null, child: BucketSpy()),
),
);
manager.doSerialization();
expect(state.bucket, isNull);
});
testWidgets('no bucket for descendants when scope is null', (WidgetTester tester) async {
final Key scopeKey = GlobalKey();
await tester.pumpWidget(
RestorationScope(key: scopeKey, restorationId: 'foo', child: const BucketSpy()),
);
final BucketSpyState state = tester.state(find.byType(BucketSpy));
expect(state.bucket, isNull);
// Move it under a valid scope.
final MockRestorationManager manager = MockRestorationManager();
addTearDown(manager.dispose);
final RestorationBucket root = RestorationBucket.root(
manager: manager,
rawData: <String, dynamic>{},
);
addTearDown(root.dispose);
await tester.pumpWidget(
UnmanagedRestorationScope(
bucket: root,
child: RestorationScope(key: scopeKey, restorationId: 'foo', child: const BucketSpy()),
),
);
manager.doSerialization();
expect(state.bucket, isNotNull);
expect(state.bucket!.restorationId, 'foo');
// Move out of scope again.
await tester.pumpWidget(
RestorationScope(key: scopeKey, restorationId: 'foo', child: const BucketSpy()),
);
manager.doSerialization();
expect(state.bucket, isNull);
});
testWidgets('no bucket for descendants when scope and id are null', (
WidgetTester tester,
) async {
await tester.pumpWidget(const RestorationScope(restorationId: null, child: BucketSpy()));
final BucketSpyState state = tester.state(find.byType(BucketSpy));
expect(state.bucket, isNull);
});
testWidgets('moving scope moves its data', (WidgetTester tester) async {
final MockRestorationManager manager = MockRestorationManager();
addTearDown(manager.dispose);
final Map<String, dynamic> rawData = <String, dynamic>{};
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);
addTearDown(root.dispose);
final Key scopeKey = GlobalKey();
await tester.pumpWidget(
UnmanagedRestorationScope(
bucket: root,
child: Row(
textDirection: TextDirection.ltr,
children: <Widget>[
RestorationScope(
restorationId: 'fixed',
child: RestorationScope(
key: scopeKey,
restorationId: 'moving-child',
child: const BucketSpy(),
),
),
],
),
),
);
manager.doSerialization();
final BucketSpyState state = tester.state(find.byType(BucketSpy));
expect(state.bucket!.restorationId, 'moving-child');
expect(
(((rawData[childrenMapKey] as Map<Object?, Object?>)['fixed']!
as Map<String, dynamic>)[childrenMapKey]
as Map<Object?, Object?>)
.containsKey('moving-child'),
isTrue,
);
final RestorationBucket bucket = state.bucket!;
state.bucket!.write('value', 11);
manager.doSerialization();
// Move scope.
await tester.pumpWidget(
UnmanagedRestorationScope(
bucket: root,
child: Row(
textDirection: TextDirection.ltr,
children: <Widget>[
RestorationScope(restorationId: 'fixed', child: Container()),
RestorationScope(
key: scopeKey,
restorationId: 'moving-child',
child: const BucketSpy(),
),
],
),
),
);
manager.doSerialization();
expect(state.bucket!.restorationId, 'moving-child');
expect(state.bucket, same(bucket));
expect(state.bucket!.read<int>('value'), 11);
expect((rawData[childrenMapKey] as Map<Object?, Object?>)['fixed'], isEmpty);
expect(
(rawData[childrenMapKey] as Map<Object?, Object?>).containsKey('moving-child'),
isTrue,
);
});
});
}
Map<String, dynamic> _createRawDataSet() {
return <String, dynamic>{
valuesMapKey: <String, dynamic>{'value1': 10, 'value2': 'Hello'},
childrenMapKey: <String, dynamic>{
'child1': <String, dynamic>{
valuesMapKey: <String, dynamic>{'foo': 22},
},
'child2': <String, dynamic>{
valuesMapKey: <String, dynamic>{'bar': 33},
},
},
};
}