// 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/material.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { testWidgets('widget moves scopes during restore', (WidgetTester tester) async { await tester.pumpWidget(const RootRestorationScope( restorationId: 'root', child: Directionality( textDirection: TextDirection.ltr, child: TestWidgetWithCounterChild(), ), )); expect(tester.state<TestWidgetWithCounterChildState>(find.byType(TestWidgetWithCounterChild)).restoreChild, true); expect(find.text('Counter: 0'), findsOneWidget); await tester.tap(find.text('Counter: 0')); await tester.pump(); expect(find.text('Counter: 1'), findsOneWidget); final TestRestorationData dataWithChild = await tester.getRestorationData(); tester.state<TestWidgetWithCounterChildState>(find.byType(TestWidgetWithCounterChild)).restoreChild = false; await tester.pump(); expect(tester.state<TestWidgetWithCounterChildState>(find.byType(TestWidgetWithCounterChild)).restoreChild, false); await tester.tap(find.text('Counter: 1')); await tester.pump(); expect(find.text('Counter: 2'), findsOneWidget); final TestRestorationData dataWithoutChild = await tester.getRestorationData(); // Child moves from outside to inside scope. await tester.restoreFrom(dataWithChild); expect(find.text('Counter: 1'), findsOneWidget); await tester.tap(find.text('Counter: 1')); await tester.pump(); expect(find.text('Counter: 2'), findsOneWidget); // Child stays inside scope. await tester.restoreFrom(dataWithChild); expect(find.text('Counter: 1'), findsOneWidget); await tester.tap(find.text('Counter: 1')); await tester.tap(find.text('Counter: 1')); await tester.tap(find.text('Counter: 1')); await tester.tap(find.text('Counter: 1')); await tester.tap(find.text('Counter: 1')); await tester.pump(); expect(find.text('Counter: 6'), findsOneWidget); // Child moves from inside to outside scope. await tester.restoreFrom(dataWithoutChild); expect(find.text('Counter: 6'), findsOneWidget); await tester.tap(find.text('Counter: 6')); await tester.pump(); expect(find.text('Counter: 7'), findsOneWidget); // Child stays outside scope. await tester.restoreFrom(dataWithoutChild); expect(find.text('Counter: 7'), findsOneWidget); expect(tester.state<TestWidgetWithCounterChildState>(find.byType(TestWidgetWithCounterChild)).toggleCount, 0); }); testWidgets('restoration is turned on later', (WidgetTester tester) async { tester.binding.restorationManager.disableRestoration(); await tester.pumpWidget(const RootRestorationScope( restorationId: 'root-child', child: Directionality( textDirection: TextDirection.ltr, child: TestWidget( restorationId: 'foo', ), ), )); final TestWidgetState state = tester.state<TestWidgetState>(find.byType(TestWidget)); expect(find.text('hello'), findsOneWidget); expect(state.buckets.single, isNull); expect(state.flags.single, isTrue); expect(state.bucket, isNull); state.buckets.clear(); state.flags.clear(); await tester.restoreFrom(TestRestorationData.empty); await tester.pumpWidget(const RootRestorationScope( restorationId: 'root-child', child: Directionality( textDirection: TextDirection.ltr, child: TestWidget( restorationId: 'foo', ), ), )); expect(find.text('hello'), findsOneWidget); expect(state.buckets.single, isNull); expect(state.flags.single, isFalse); expect(state.bucket, isNotNull); expect(state.toggleCount, 0); }); } class TestWidgetWithCounterChild extends StatefulWidget { const TestWidgetWithCounterChild({super.key}); @override State<TestWidgetWithCounterChild> createState() => TestWidgetWithCounterChildState(); } class TestWidgetWithCounterChildState extends State<TestWidgetWithCounterChild> with RestorationMixin { final RestorableBool childRestorationEnabled = RestorableBool(true); int toggleCount = 0; @override void didToggleBucket(RestorationBucket? oldBucket) { super.didToggleBucket(oldBucket); toggleCount++; } @override void restoreState(RestorationBucket? oldBucket, bool initialRestore) { registerForRestoration(childRestorationEnabled, 'childRestorationEnabled'); } bool get restoreChild => childRestorationEnabled.value; set restoreChild(bool value) { if (value == childRestorationEnabled.value) { return; } setState(() { childRestorationEnabled.value = value; }); } @override void dispose() { childRestorationEnabled.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Counter( restorationId: restoreChild ? 'counter' : null, ); } @override String get restorationId => 'foo'; } class Counter extends StatefulWidget { const Counter({super.key, this.restorationId}); final String? restorationId; @override State<Counter> createState() => CounterState(); } class CounterState extends State<Counter> with RestorationMixin { final RestorableInt count = RestorableInt(0); @override String? get restorationId => widget.restorationId; @override void restoreState(RestorationBucket? oldBucket, bool initialRestore) { registerForRestoration(count, 'counter'); } @override void dispose() { count.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return OutlinedButton( onPressed: () { setState(() { count.value++; }); }, child: Text( 'Counter: ${count.value}', ), ); } } class TestWidget extends StatefulWidget { const TestWidget({super.key, required this.restorationId}); final String? restorationId; @override State<TestWidget> createState() => TestWidgetState(); } class TestWidgetState extends State<TestWidget> with RestorationMixin { List<RestorationBucket?> buckets = <RestorationBucket?>[]; List<bool> flags = <bool>[]; @override void restoreState(RestorationBucket? oldBucket, bool initialRestore) { buckets.add(oldBucket); flags.add(initialRestore); } int toggleCount = 0; @override void didToggleBucket(RestorationBucket? oldBucket) { super.didToggleBucket(oldBucket); toggleCount++; } @override String? get restorationId => widget.restorationId; @override Widget build(BuildContext context) { return const Text('hello'); } }