restoration_mixin_test.dart 26 KB
Newer Older
1 2 3 4 5 6
// 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';
7
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
8 9 10 11

import 'restoration.dart';

void main() {
12
  testWidgetsWithLeakTracking('claims bucket', (WidgetTester tester) async {
13 14
    const String id = 'hello world 1234';
    final MockRestorationManager manager = MockRestorationManager();
15
    addTearDown(manager.dispose);
16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
    final Map<String, dynamic> rawData = <String, dynamic>{};
    final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);
    expect(rawData, isEmpty);

    await tester.pumpWidget(
      UnmanagedRestorationScope(
        bucket: root,
        child: const _TestRestorableWidget(
          restorationId: id,
        ),
      ),
    );
    manager.doSerialization();

    final _TestRestorableWidgetState state = tester.state(find.byType(_TestRestorableWidget));
31
    expect(state.bucket?.restorationId, id);
32
    expect((rawData[childrenMapKey] as Map<Object?, Object?>).containsKey(id), isTrue);
33
    expect(state.property.value, 10);
34
    expect((((rawData[childrenMapKey] as Map<Object?, Object?>)[id]! as Map<String, dynamic>)[valuesMapKey] as Map<Object?, Object?>)['foo'], 10);
35
    expect(state.property.log, <String>['createDefaultValue', 'initWithValue', 'toPrimitives']);
36
    expect(state.toggleBucketLog, isEmpty);
37 38 39
    expect(state.restoreStateLog.single, isNull);
  });

40
  testWidgetsWithLeakTracking('claimed bucket with data', (WidgetTester tester) async {
41
    final MockRestorationManager manager = MockRestorationManager();
42
    addTearDown(manager.dispose);
43 44 45 46 47 48 49 50 51 52 53 54 55
    final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: _createRawDataSet());

    await tester.pumpWidget(
      UnmanagedRestorationScope(
        bucket: root,
        child: const _TestRestorableWidget(
          restorationId: 'child1',
        ),
      ),
    );
    manager.doSerialization();

    final _TestRestorableWidgetState state = tester.state(find.byType(_TestRestorableWidget));
56
    expect(state.bucket!.restorationId, 'child1');
57 58
    expect(state.property.value, 22);
    expect(state.property.log, <String>['fromPrimitives', 'initWithValue']);
59
    expect(state.toggleBucketLog, isEmpty);
60 61 62
    expect(state.restoreStateLog.single, isNull);
  });

63
  testWidgetsWithLeakTracking('renames existing bucket when new ID is provided via widget', (WidgetTester tester) async {
64
    final MockRestorationManager manager = MockRestorationManager();
65
    addTearDown(manager.dispose);
66 67 68 69 70 71 72 73 74 75 76 77 78 79
    final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: _createRawDataSet());

    await tester.pumpWidget(
      UnmanagedRestorationScope(
        bucket: root,
        child: const _TestRestorableWidget(
          restorationId: 'child1',
        ),
      ),
    );
    manager.doSerialization();

    // Claimed existing bucket with data.
    final _TestRestorableWidgetState state = tester.state(find.byType(_TestRestorableWidget));
80 81 82
    expect(state.bucket!.restorationId, 'child1');
    expect(state.bucket!.read<int>('foo'), 22);
    final RestorationBucket bucket = state.bucket!;
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97

    state.property.log.clear();
    state.restoreStateLog.clear();

    // Rename the existing bucket.
    await tester.pumpWidget(
      UnmanagedRestorationScope(
        bucket: root,
        child: const _TestRestorableWidget(
          restorationId: 'something else',
        ),
      ),
    );
    manager.doSerialization();

98 99
    expect(state.bucket!.restorationId, 'something else');
    expect(state.bucket!.read<int>('foo'), 22);
100 101 102
    expect(state.bucket, same(bucket));
    expect(state.property.log, isEmpty);
    expect(state.restoreStateLog, isEmpty);
103
    expect(state.toggleBucketLog, isEmpty);
104 105
  });

106
  testWidgetsWithLeakTracking('renames existing bucket when didUpdateRestorationId is called', (WidgetTester tester) async {
107
    final MockRestorationManager manager = MockRestorationManager();
108
    addTearDown(manager.dispose);
109 110 111 112 113 114 115 116 117 118 119 120 121 122
    final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: _createRawDataSet());

    await tester.pumpWidget(
      UnmanagedRestorationScope(
        bucket: root,
        child: const _TestRestorableWidget(
          restorationId: 'child1',
        ),
      ),
    );
    manager.doSerialization();

    // Claimed existing bucket with data.
    final _TestRestorableWidgetState state = tester.state(find.byType(_TestRestorableWidget));
123 124 125
    expect(state.bucket!.restorationId, 'child1');
    expect(state.bucket!.read<int>('foo'), 22);
    final RestorationBucket bucket = state.bucket!;
126 127 128 129 130 131 132 133

    state.property.log.clear();
    state.restoreStateLog.clear();

    // Rename the existing bucket.
    state.injectId('newnewnew');
    manager.doSerialization();

134 135
    expect(state.bucket!.restorationId, 'newnewnew');
    expect(state.bucket!.read<int>('foo'), 22);
136 137 138
    expect(state.bucket, same(bucket));
    expect(state.property.log, isEmpty);
    expect(state.restoreStateLog, isEmpty);
139
    expect(state.toggleBucketLog, isEmpty);
140 141
  });

142
  testWidgetsWithLeakTracking('Disposing widget removes its data', (WidgetTester tester) async {
143
    final MockRestorationManager manager = MockRestorationManager();
144
    addTearDown(manager.dispose);
145 146 147
    final Map<String, dynamic> rawData = _createRawDataSet();
    final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);

148
    expect((rawData[childrenMapKey] as Map<String, dynamic>).containsKey('child1'), isTrue);
149 150 151 152 153 154 155 156 157
    await tester.pumpWidget(
      UnmanagedRestorationScope(
        bucket: root,
        child: const _TestRestorableWidget(
          restorationId: 'child1',
        ),
      ),
    );
    manager.doSerialization();
158
    expect((rawData[childrenMapKey] as Map<String, dynamic>).containsKey('child1'), isTrue);
159 160 161 162 163 164 165 166 167

    await tester.pumpWidget(
      UnmanagedRestorationScope(
        bucket: root,
        child: Container(),
      ),
    );
    manager.doSerialization();

168
    expect((rawData[childrenMapKey] as Map<String, dynamic>).containsKey('child1'), isFalse);
169 170
  });

171
  testWidgetsWithLeakTracking('toggling id between null and non-null', (WidgetTester tester) async {
172
    final MockRestorationManager manager = MockRestorationManager();
173
    addTearDown(manager.dispose);
174 175 176 177 178 179
    final Map<String, dynamic> rawData = _createRawDataSet();
    final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);

    await tester.pumpWidget(
      UnmanagedRestorationScope(
        bucket: root,
180
        child: const _TestRestorableWidget(),
181 182 183 184 185
      ),
    );
    final _TestRestorableWidgetState state = tester.state(find.byType(_TestRestorableWidget));
    expect(state.bucket, isNull);
    expect(state.property.value, 10); // Initialized to default.
186
    expect((((rawData[childrenMapKey] as Map<String, dynamic>)['child1'] as Map<String, dynamic>)[valuesMapKey] as Map<String, dynamic>)['foo'], 22);
187 188 189
    expect(state.property.log, <String>['createDefaultValue', 'initWithValue']);
    state.property.log.clear();
    expect(state.restoreStateLog.single, isNull);
190
    expect(state.toggleBucketLog, isEmpty);
191
    state.restoreStateLog.clear();
192
    state.toggleBucketLog.clear();
193 194 195 196 197 198 199 200 201 202 203 204

    // Change id to non-null.
    await tester.pumpWidget(
      UnmanagedRestorationScope(
        bucket: root,
        child: const _TestRestorableWidget(
          restorationId: 'child1',
        ),
      ),
    );
    manager.doSerialization();
    expect(state.bucket, isNotNull);
205
    expect(state.bucket!.restorationId, 'child1');
206
    expect(state.property.value, 10);
207
    expect((((rawData[childrenMapKey] as Map<String, dynamic>)['child1'] as Map<String, dynamic>)[valuesMapKey] as Map<String, dynamic>)['foo'], 10);
208 209 210
    expect(state.property.log, <String>['toPrimitives']);
    state.property.log.clear();
    expect(state.restoreStateLog, isEmpty);
211
    expect(state.toggleBucketLog.single, isNull);
212
    state.restoreStateLog.clear();
213
    state.toggleBucketLog.clear();
214

215
    final RestorationBucket bucket = state.bucket!;
216 217 218 219 220

    // Change id back to null.
    await tester.pumpWidget(
      UnmanagedRestorationScope(
        bucket: root,
221
        child: const _TestRestorableWidget(),
222 223 224 225
      ),
    );
    manager.doSerialization();
    expect(state.bucket, isNull);
226
    expect((rawData[childrenMapKey] as Map<String, dynamic>).containsKey('child1'), isFalse);
227 228
    expect(state.property.log, isEmpty);
    expect(state.restoreStateLog, isEmpty);
229
    expect(state.toggleBucketLog.single, same(bucket));
230 231
  });

232
  testWidgetsWithLeakTracking('move in and out of scope', (WidgetTester tester) async {
233 234
    final Key key = GlobalKey();
    final MockRestorationManager manager = MockRestorationManager();
235
    addTearDown(manager.dispose);
236 237 238 239 240 241 242 243 244 245 246 247
    final Map<String, dynamic> rawData = _createRawDataSet();
    final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);

    await tester.pumpWidget(
      _TestRestorableWidget(
        key: key,
        restorationId: 'child1',
      ),
    );
    final _TestRestorableWidgetState state = tester.state(find.byType(_TestRestorableWidget));
    expect(state.bucket, isNull);
    expect(state.property.value, 10); // Initialized to default.
248
    expect((((rawData[childrenMapKey] as Map<String, dynamic>)['child1'] as Map<String, dynamic>)[valuesMapKey] as Map<String, dynamic>)['foo'], 22);
249 250 251
    expect(state.property.log, <String>['createDefaultValue', 'initWithValue']);
    state.property.log.clear();
    expect(state.restoreStateLog.single, isNull);
252
    expect(state.toggleBucketLog, isEmpty);
253
    state.restoreStateLog.clear();
254
    state.toggleBucketLog.clear();
255 256 257 258 259 260 261 262 263 264 265 266 267

    // Move it under a valid scope.
    await tester.pumpWidget(
      UnmanagedRestorationScope(
        bucket: root,
        child: _TestRestorableWidget(
          key: key,
          restorationId: 'child1',
        ),
      ),
    );
    manager.doSerialization();
    expect(state.bucket, isNotNull);
268
    expect(state.bucket!.restorationId, 'child1');
269
    expect(state.property.value, 10);
270
    expect((((rawData[childrenMapKey] as Map<String, dynamic>)['child1'] as Map<String, dynamic>)[valuesMapKey] as Map<String, dynamic>)['foo'], 10);
271 272 273
    expect(state.property.log, <String>['toPrimitives']);
    state.property.log.clear();
    expect(state.restoreStateLog, isEmpty);
274
    expect(state.toggleBucketLog.single, isNull);
275
    state.restoreStateLog.clear();
276
    state.toggleBucketLog.clear();
277

278
    final RestorationBucket bucket = state.bucket!;
279 280 281 282 283 284 285 286 287 288

    // Move out of scope again.
    await tester.pumpWidget(
      _TestRestorableWidget(
        key: key,
        restorationId: 'child1',
      ),
    );
    manager.doSerialization();
    expect(state.bucket, isNull);
289
    expect((rawData[childrenMapKey] as Map<String, dynamic>).containsKey('child1'), isFalse);
290 291
    expect(state.property.log, isEmpty);
    expect(state.restoreStateLog, isEmpty);
292
    expect(state.toggleBucketLog.single, same(bucket));
293 294
  });

295
  testWidgetsWithLeakTracking('moving scope moves its data', (WidgetTester tester) async {
296
    final MockRestorationManager manager = MockRestorationManager();
297
    addTearDown(manager.dispose);
298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320
    final Map<String, dynamic> rawData = <String, dynamic>{};
    final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);
    final Key key = GlobalKey();

    await tester.pumpWidget(
      UnmanagedRestorationScope(
        bucket: root,
        child: Row(
          textDirection: TextDirection.ltr,
          children: <Widget>[
            RestorationScope(
              restorationId: 'fixed',
              child: _TestRestorableWidget(
                key: key,
                restorationId: 'moving-child',
              ),
            ),
          ],
        ),
      ),
    );
    manager.doSerialization();
    final _TestRestorableWidgetState state = tester.state(find.byType(_TestRestorableWidget));
321
    expect(state.bucket!.restorationId, 'moving-child');
322
    expect((((rawData[childrenMapKey] as Map<Object?, Object?>)['fixed']! as Map<String, dynamic>)[childrenMapKey] as Map<Object?, Object?>).containsKey('moving-child'), isTrue);
323
    final RestorationBucket bucket = state.bucket!;
324 325 326
    state.property.log.clear();
    state.restoreStateLog.clear();

327
    state.bucket!.write('value', 11);
328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349
    manager.doSerialization();

    // Move widget.
    await tester.pumpWidget(
      UnmanagedRestorationScope(
        bucket: root,
        child: Row(
          textDirection: TextDirection.ltr,
          children: <Widget>[
            RestorationScope(
              restorationId: 'fixed',
              child: Container(),
            ),
            _TestRestorableWidget(
              key: key,
              restorationId: 'moving-child',
            ),
          ],
        ),
      ),
    );
    manager.doSerialization();
350
    expect(state.bucket!.restorationId, 'moving-child');
351
    expect(state.bucket, same(bucket));
352
    expect(state.bucket!.read<int>('value'), 11);
353
    expect(state.property.log, isEmpty);
354
    expect(state.toggleBucketLog, isEmpty);
355 356
    expect(state.restoreStateLog, isEmpty);

357 358
    expect((rawData[childrenMapKey] as Map<Object?, Object?>)['fixed'], isEmpty);
    expect((rawData[childrenMapKey] as Map<Object?, Object?>).containsKey('moving-child'), isTrue);
359 360
  });

361
  testWidgetsWithLeakTracking('restartAndRestore', (WidgetTester tester) async {
362 363 364 365 366 367
    await tester.pumpWidget(
      const RootRestorationScope(
        restorationId: 'root-child',
        child: _TestRestorableWidget(
          restorationId: 'widget',
        ),
368
      ),
369 370 371 372 373 374 375
    );

    _TestRestorableWidgetState state = tester.state(find.byType(_TestRestorableWidget));
    expect(state.bucket, isNotNull);
    expect(state.property.value, 10); // default
    expect(state.property.log, <String>['createDefaultValue', 'initWithValue', 'toPrimitives']);
    expect(state.restoreStateLog.single, isNull);
376
    expect(state.toggleBucketLog, isEmpty);
377 378 379 380 381 382 383 384 385
    _clearLogs(state);

    state.setProperties(() {
      state.property.value = 20;
    });
    await tester.pump();
    expect(state.property.value, 20);
    expect(state.property.log, <String>['toPrimitives']);
    expect(state.restoreStateLog, isEmpty);
386
    expect(state.toggleBucketLog, isEmpty);
387 388 389 390 391 392 393 394 395 396
    _clearLogs(state);

    final _TestRestorableWidgetState oldState = state;
    await tester.restartAndRestore();
    state = tester.state(find.byType(_TestRestorableWidget));

    expect(state, isNot(same(oldState)));
    expect(state.property.value, 20);
    expect(state.property.log, <String>['fromPrimitives', 'initWithValue']);
    expect(state.restoreStateLog.single, isNull);
397
    expect(state.toggleBucketLog, isEmpty);
398 399
  });

400
  testWidgetsWithLeakTracking('restore while running', (WidgetTester tester) async {
401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427
    await tester.pumpWidget(
      const RootRestorationScope(
        restorationId: 'root-child',
        child: _TestRestorableWidget(
          restorationId: 'widget',
        ),
      ),
    );

    _TestRestorableWidgetState state = tester.state(find.byType(_TestRestorableWidget));

    state.setProperties(() {
      state.property.value = 20;
    });
    await tester.pump();
    expect(state.property.value, 20);

    final TestRestorationData data = await tester.getRestorationData();

    state.setProperties(() {
      state.property.value = 30;
    });
    await tester.pump();
    expect(state.property.value, 30);
    _clearLogs(state);

    final _TestRestorableWidgetState oldState = state;
428
    final RestorationBucket oldBucket = oldState.bucket!;
429 430 431 432 433 434 435
    await tester.restoreFrom(data);
    state = tester.state(find.byType(_TestRestorableWidget));

    expect(state, same(oldState));
    expect(state.property.value, 20);
    expect(state.property.log, <String>['fromPrimitives', 'initWithValue']);
    expect(state.restoreStateLog.single, oldBucket);
436
    expect(state.toggleBucketLog, isEmpty);
437 438
  });

439
  testWidgetsWithLeakTracking('can register additional property outside of restoreState', (WidgetTester tester) async {
440 441 442 443 444 445 446 447 448 449 450
    await tester.pumpWidget(
      const RootRestorationScope(
        restorationId: 'root-child',
        child: _TestRestorableWidget(
          restorationId: 'widget',
        ),
      ),
    );

    final _TestRestorableWidgetState state = tester.state(find.byType(_TestRestorableWidget));
    state.registerAdditionalProperty();
451 452
    expect(state.additionalProperty!.value, 11);
    expect(state.additionalProperty!.log, <String>['createDefaultValue', 'initWithValue', 'toPrimitives']);
453 454

    state.setProperties(() {
455
      state.additionalProperty!.value = 33;
456 457
    });
    await tester.pump();
458
    expect(state.additionalProperty!.value, 33);
459 460 461 462

    final TestRestorationData data = await tester.getRestorationData();

    state.setProperties(() {
463
      state.additionalProperty!.value = 44;
464 465
    });
    await tester.pump();
466
    expect(state.additionalProperty!.value, 44);
467 468 469 470 471
    _clearLogs(state);

    await tester.restoreFrom(data);

    expect(state, same(tester.state(find.byType(_TestRestorableWidget))));
472
    expect(state.additionalProperty!.value, 33);
473 474 475
    expect(state.property.log, <String>['fromPrimitives', 'initWithValue']);
  });

476
  testWidgetsWithLeakTracking('cannot register same property twice', (WidgetTester tester) async {
477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505
    await tester.pumpWidget(
      const RootRestorationScope(
        restorationId: 'root-child',
        child: _TestRestorableWidget(
          restorationId: 'widget',
        ),
      ),
    );

    final _TestRestorableWidgetState state = tester.state(find.byType(_TestRestorableWidget));
    state.registerAdditionalProperty();
    await tester.pump();
    expect(() => state.registerAdditionalProperty(), throwsAssertionError);
  });

  testWidgets('cannot register under ID that is already in use', (WidgetTester tester) async {
    await tester.pumpWidget(
      const RootRestorationScope(
        restorationId: 'root-child',
        child: _TestRestorableWidget(
          restorationId: 'widget',
        ),
      ),
    );

    final _TestRestorableWidgetState state = tester.state(find.byType(_TestRestorableWidget));
    expect(() => state.registerPropertyUnderSameId(), throwsAssertionError);
  });

506
  testWidgetsWithLeakTracking('data of disabled property is not stored', (WidgetTester tester) async {
507 508 509 510 511 512 513 514 515 516 517 518 519 520 521
    await tester.pumpWidget(
      const RootRestorationScope(
        restorationId: 'root-child',
        child: _TestRestorableWidget(
          restorationId: 'widget',
        ),
      ),
    );
    _TestRestorableWidgetState state = tester.state(find.byType(_TestRestorableWidget));

    state.setProperties(() {
      state.property.value = 30;
    });
    await tester.pump();
    expect(state.property.value, 30);
522
    expect(state.bucket!.read<int>('foo'), 30);
523 524 525 526 527 528 529
    _clearLogs(state);

    state.setProperties(() {
      state.property.enabled = false;
    });
    await tester.pump();
    expect(state.property.value, 30);
530
    expect(state.bucket!.contains('foo'), isFalse);
531 532 533 534 535 536
    expect(state.property.log, isEmpty);

    state.setProperties(() {
      state.property.value = 40;
    });
    await tester.pump();
537
    expect(state.bucket!.contains('foo'), isFalse);
538 539 540 541 542 543 544 545
    expect(state.property.log, isEmpty);

    await tester.restartAndRestore();
    state = tester.state(find.byType(_TestRestorableWidget));
    expect(state.property.log, <String>['createDefaultValue', 'initWithValue', 'toPrimitives']);
    expect(state.property.value, 10); // Initialized to default value.
  });

546
  testWidgetsWithLeakTracking('Enabling property stores its data again', (WidgetTester tester) async {
547 548 549 550 551 552 553 554 555 556 557 558 559 560 561
    await tester.pumpWidget(
      const RootRestorationScope(
        restorationId: 'root-child',
        child: _TestRestorableWidget(
          restorationId: 'widget',
        ),
      ),
    );
    _TestRestorableWidgetState state = tester.state(find.byType(_TestRestorableWidget));
    _clearLogs(state);

    state.setProperties(() {
      state.property.enabled = false;
    });
    await tester.pump();
562
    expect(state.bucket!.contains('foo'), isFalse);
563 564 565 566 567
    state.setProperties(() {
      state.property.value = 40;
    });
    await tester.pump();
    expect(state.property.value, 40);
568
    expect(state.bucket!.contains('foo'), isFalse);
569 570 571 572 573 574
    expect(state.property.log, isEmpty);

    state.setProperties(() {
      state.property.enabled = true;
    });
    await tester.pump();
575
    expect(state.bucket!.read<int>('foo'), 40);
576 577 578 579 580 581 582 583
    expect(state.property.log, <String>['toPrimitives']);

    await tester.restartAndRestore();
    state = tester.state(find.byType(_TestRestorableWidget));
    expect(state.property.log, <String>['fromPrimitives', 'initWithValue']);
    expect(state.property.value, 40);
  });

584
  testWidgetsWithLeakTracking('Unregistering a property removes its data', (WidgetTester tester) async {
585 586 587 588 589 590 591 592 593 594 595
    await tester.pumpWidget(
      const RootRestorationScope(
        restorationId: 'root-child',
        child: _TestRestorableWidget(
          restorationId: 'widget',
        ),
      ),
    );
    final _TestRestorableWidgetState state = tester.state(find.byType(_TestRestorableWidget));
    state.registerAdditionalProperty();
    await tester.pump();
596 597
    expect(state.additionalProperty!.value, 11);
    expect(state.bucket!.read<int>('additional'), 11);
598 599
    state.unregisterAdditionalProperty();
    await tester.pump();
600 601
    expect(state.bucket!.contains('additional'), isFalse);
    expect(() => state.additionalProperty!.value, throwsAssertionError); // No longer registered.
602 603 604 605

    // Can register the same property again.
    state.registerAdditionalProperty();
    await tester.pump();
606 607
    expect(state.additionalProperty!.value, 11);
    expect(state.bucket!.read<int>('additional'), 11);
608 609
  });

610
  testWidgetsWithLeakTracking('Disposing a property unregisters it, but keeps data', (WidgetTester tester) async {
611 612 613 614 615 616 617 618 619 620 621
    await tester.pumpWidget(
      const RootRestorationScope(
        restorationId: 'root-child',
        child: _TestRestorableWidget(
          restorationId: 'widget',
        ),
      ),
    );
    final _TestRestorableWidgetState state = tester.state(find.byType(_TestRestorableWidget));
    state.registerAdditionalProperty();
    await tester.pump();
622 623
    expect(state.additionalProperty!.value, 11);
    expect(state.bucket!.read<int>('additional'), 11);
624

625
    state.additionalProperty!.dispose();
626
    await tester.pump();
627
    expect(state.bucket!.read<int>('additional'), 11);
628 629 630 631 632 633

    // Can register property under same id again.
    state.additionalProperty = _TestRestorableProperty(22);
    state.registerAdditionalProperty();
    await tester.pump();

634 635
    expect(state.additionalProperty!.value, 11); // Old value restored.
    expect(state.bucket!.read<int>('additional'), 11);
636 637 638
  });

  test('RestorableProperty throws after disposed', () {
639
    final RestorableProperty<Object?> property = _TestRestorableProperty(10);
640 641 642 643 644 645 646
    property.dispose();
    expect(() => property.dispose(), throwsFlutterError);
  });
}

void _clearLogs(_TestRestorableWidgetState state) {
  state.property.log.clear();
647
  state.additionalProperty?.log.clear();
648
  state.restoreStateLog.clear();
649
  state.toggleBucketLog.clear();
650 651 652 653
}

class _TestRestorableWidget extends StatefulWidget {

654
  const _TestRestorableWidget({super.key, this.restorationId});
655

656
  final String? restorationId;
657 658 659 660 661 662 663

  @override
  State<_TestRestorableWidget> createState() => _TestRestorableWidgetState();
}

class _TestRestorableWidgetState extends State<_TestRestorableWidget> with RestorationMixin {
  final _TestRestorableProperty property = _TestRestorableProperty(10);
664
  _TestRestorableProperty? additionalProperty;
665 666
  bool _rerigisterAdditionalProperty = false;

667 668
  final List<RestorationBucket?> restoreStateLog = <RestorationBucket?>[];
  final List<RestorationBucket?> toggleBucketLog = <RestorationBucket?>[];
669 670

  @override
671
  void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
672 673 674
    restoreStateLog.add(oldBucket);
    registerForRestoration(property, 'foo');
    if (_rerigisterAdditionalProperty && additionalProperty != null) {
675
      registerForRestoration(additionalProperty!, 'additional');
676 677 678 679
    }
  }

  @override
680 681
  void didToggleBucket(RestorationBucket? oldBucket) {
    toggleBucketLog.add(oldBucket);
682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698
    super.didToggleBucket(oldBucket);
  }

  @override
  void dispose() {
    super.dispose();
    property.dispose();
    additionalProperty?.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Container();
  }

  void setProperties(VoidCallback fn) => setState(fn);

699
  String? _injectedId;
700 701 702 703 704 705 706
  void injectId(String id) {
    _injectedId = id;
    didUpdateRestorationId();
  }

  void registerAdditionalProperty({bool reregister = true}) {
    additionalProperty ??= _TestRestorableProperty(11);
707
    registerForRestoration(additionalProperty!, 'additional');
708 709 710 711
    _rerigisterAdditionalProperty = reregister;
  }

  void unregisterAdditionalProperty() {
712
    unregisterFromRestoration(additionalProperty!);
713 714 715 716 717 718 719
  }

  void registerPropertyUnderSameId() {
    registerForRestoration(_TestRestorableProperty(11), 'foo');
  }

  @override
720
  String? get restorationId => _injectedId ?? widget.restorationId;
721 722 723 724 725 726 727 728 729 730 731 732
}

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,
733
        },
734 735 736 737
      },
      'child2' : <String, dynamic>{
        valuesMapKey : <String, dynamic>{
          'bar': 33,
738
        },
739 740 741 742 743
      },
    },
  };
}

744
class _TestRestorableProperty extends RestorableProperty<Object?> {
745 746 747 748 749 750 751 752 753 754 755 756 757
  _TestRestorableProperty(this._value);

  List<String> log = <String>[];

  @override
  bool get enabled => _enabled;
  bool _enabled = true;
  set enabled(bool value) {
    _enabled = value;
    notifyListeners();
  }

  @override
758
  Object? createDefaultValue() {
759 760 761 762 763
    log.add('createDefaultValue');
    return _value;
  }

  @override
764
  Object? fromPrimitives(Object? data) {
765 766 767 768
    log.add('fromPrimitives');
    return data;
  }

769
  Object? get value {
770 771 772
    assert(isRegistered);
    return _value;
  }
773 774
  Object? _value;
  set value(Object? value) {
775 776 777 778 779
    _value = value;
    notifyListeners();
  }

  @override
780
  void initWithValue(Object? v) {
781 782 783 784 785
    log.add('initWithValue');
    _value = v;
  }

  @override
786
  Object? toPrimitives() {
787 788 789 790
    log.add('toPrimitives');
    return _value;
  }
}