restoration_scope_test.dart 12.9 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11
// 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', () {
12
    testWidgets('makes bucket available to descendants', (WidgetTester tester) async {
13 14 15 16
      final RestorationBucket bucket1 = RestorationBucket.empty(
        restorationId: 'foo',
        debugOwner: 'owner',
      );
17
      addTearDown(bucket1.dispose);
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33

      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',
      );
34 35
      addTearDown(bucket2.dispose);

36 37 38 39 40 41 42 43 44
      await tester.pumpWidget(
        UnmanagedRestorationScope(
          bucket: bucket2,
          child: const BucketSpy(),
        ),
      );
      expect(state.bucket, bucket2);
    });

45
    testWidgets('null bucket disables restoration', (WidgetTester tester) async {
46 47 48 49 50 51 52 53 54 55 56
      await tester.pumpWidget(
        const UnmanagedRestorationScope(
          child: BucketSpy(),
        ),
      );
      final BucketSpyState state = tester.state(find.byType(BucketSpy));
      expect(state.bucket, isNull);
    });
  });

  group('RestorationScope', () {
57
    testWidgets('asserts when none is found', (WidgetTester tester) async {
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
      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);
    });

103
    testWidgets('makes bucket available to descendants', (WidgetTester tester) async {
104 105
      const String id = 'hello world 1234';
      final MockRestorationManager manager = MockRestorationManager();
106
      addTearDown(manager.dispose);
107 108
      final Map<String, dynamic> rawData = <String, dynamic>{};
      final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);
109
      addTearDown(root.dispose);
110 111 112 113 114 115 116 117 118 119 120 121 122 123
      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));
124
      expect(state.bucket!.restorationId, id);
125
      expect((rawData[childrenMapKey] as Map<Object?, Object?>).containsKey(id), isTrue);
126 127
    });

128
    testWidgets('bucket for descendants contains data claimed from parent', (WidgetTester tester) async {
129
      final MockRestorationManager manager = MockRestorationManager();
130
      addTearDown(manager.dispose);
131
      final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: _createRawDataSet());
132
      addTearDown(root.dispose);
133 134 135 136 137 138 139 140 141 142 143 144 145

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

      final BucketSpyState state = tester.state(find.byType(BucketSpy));
146 147
      expect(state.bucket!.restorationId, 'child1');
      expect(state.bucket!.read<int>('foo'), 22);
148 149
    });

150
    testWidgets('renames existing bucket when new ID is provided', (WidgetTester tester) async {
151
      final MockRestorationManager manager = MockRestorationManager();
152
      addTearDown(manager.dispose);
153
      final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: _createRawDataSet());
154
      addTearDown(root.dispose);
155 156 157 158 159 160 161 162 163 164 165 166 167 168

      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));
169 170 171
      expect(state.bucket!.restorationId, 'child1');
      expect(state.bucket!.read<int>('foo'), 22);
      final RestorationBucket bucket = state.bucket!;
172 173 174 175 176 177 178 179 180 181 182 183 184

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

185 186
      expect(state.bucket!.restorationId, 'something else');
      expect(state.bucket!.read<int>('foo'), 22);
187 188 189
      expect(state.bucket, same(bucket));
    });

190
    testWidgets('Disposing a scope removes its data', (WidgetTester tester) async {
191
      final MockRestorationManager manager = MockRestorationManager();
192
      addTearDown(manager.dispose);
193 194
      final Map<String, dynamic> rawData = _createRawDataSet();
      final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);
195
      addTearDown(root.dispose);
196

197
      expect((rawData[childrenMapKey] as Map<String, dynamic>).containsKey('child1'), isTrue);
198 199 200 201 202 203 204 205 206 207
      await tester.pumpWidget(
        UnmanagedRestorationScope(
          bucket: root,
          child: const RestorationScope(
            restorationId: 'child1',
            child: BucketSpy(),
          ),
        ),
      );
      manager.doSerialization();
208
      expect((rawData[childrenMapKey] as Map<String, dynamic>).containsKey('child1'), isTrue);
209 210 211 212 213 214 215 216 217

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

218
      expect((rawData[childrenMapKey] as Map<String, dynamic>).containsKey('child1'), isFalse);
219 220
    });

221
    testWidgets('no bucket for descendants when id is null', (WidgetTester tester) async {
222
      final MockRestorationManager manager = MockRestorationManager();
223
      addTearDown(manager.dispose);
224
      final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: <String, dynamic>{});
225
      addTearDown(root.dispose);
226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250

      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);
251
      expect(state.bucket!.restorationId, 'foo');
252 253 254 255 256 257 258 259 260 261 262 263 264 265 266

      // Change id back to null.
      await tester.pumpWidget(
        UnmanagedRestorationScope(
          bucket: root,
          child: const RestorationScope(
            restorationId: null,
            child: BucketSpy(),
          ),
        ),
      );
      manager.doSerialization();
      expect(state.bucket, isNull);
    });

267
    testWidgets('no bucket for descendants when scope is null', (WidgetTester tester) async {
268 269 270 271 272 273 274 275 276 277 278 279 280 281
      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();
282
      addTearDown(manager.dispose);
283
      final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: <String, dynamic>{});
284 285
      addTearDown(root.dispose);

286 287 288 289 290 291 292 293 294 295 296 297
      await tester.pumpWidget(
        UnmanagedRestorationScope(
          bucket: root,
          child: RestorationScope(
            key: scopeKey,
            restorationId: 'foo',
            child: const BucketSpy(),
          ),
        ),
      );
      manager.doSerialization();
      expect(state.bucket, isNotNull);
298
      expect(state.bucket!.restorationId, 'foo');
299 300 301 302 303 304 305 306 307 308 309 310 311

      // Move out of scope again.
      await tester.pumpWidget(
        RestorationScope(
          key: scopeKey,
          restorationId: 'foo',
          child: const BucketSpy(),
        ),
      );
      manager.doSerialization();
      expect(state.bucket, isNull);
    });

312
    testWidgets('no bucket for descendants when scope and id are null', (WidgetTester tester) async {
313 314 315 316 317 318 319 320 321 322
      await tester.pumpWidget(
        const RestorationScope(
          restorationId: null,
          child: BucketSpy(),
        ),
      );
      final BucketSpyState state = tester.state(find.byType(BucketSpy));
      expect(state.bucket, isNull);
    });

323
    testWidgets('moving scope moves its data', (WidgetTester tester) async {
324
      final MockRestorationManager manager = MockRestorationManager();
325
      addTearDown(manager.dispose);
326 327
      final Map<String, dynamic> rawData = <String, dynamic>{};
      final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);
328
      addTearDown(root.dispose);
329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350
      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));
351
      expect(state.bucket!.restorationId, 'moving-child');
352
      expect((((rawData[childrenMapKey] as Map<Object?, Object?>)['fixed']! as Map<String, dynamic>)[childrenMapKey] as Map<Object?, Object?>).containsKey('moving-child'), isTrue);
353
      final RestorationBucket bucket = state.bucket!;
354

355
      state.bucket!.write('value', 11);
356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378
      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();
379
      expect(state.bucket!.restorationId, 'moving-child');
380
      expect(state.bucket, same(bucket));
381
      expect(state.bucket!.read<int>('value'), 11);
382

383 384
      expect((rawData[childrenMapKey] as Map<Object?, Object?>)['fixed'], isEmpty);
      expect((rawData[childrenMapKey] as Map<Object?, Object?>).containsKey('moving-child'), isTrue);
385 386 387 388 389 390 391 392 393 394 395 396 397 398
    });
  });
}

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,
399
        },
400 401 402 403
      },
      'child2' : <String, dynamic>{
        valuesMapKey : <String, dynamic>{
          'bar': 33,
404
        },
405 406 407 408
      },
    },
  };
}