scrollable_semantics_test.dart 19 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
import 'dart:ui';

7
import 'package:flutter/material.dart';
8 9 10
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
11
import 'package:flutter/gestures.dart' show DragStartBehavior;
12 13 14 15

import 'semantics_tester.dart';

void main() {
16 17
  SemanticsTester semantics;

18 19 20 21
  setUp(() {
    debugResetSemanticsIdCounter();
  });

22
  testWidgets('scrollable exposes the correct semantic actions', (WidgetTester tester) async {
23
    semantics = SemanticsTester(tester);
24
    await tester.pumpWidget(
25
      Directionality(
26
        textDirection: TextDirection.ltr,
27
        child: ListView(children: List<Widget>.generate(80, (int i) => Text('$i'))),
28 29
      ),
    );
30 31 32 33 34 35 36 37 38 39 40 41 42 43

    expect(semantics,includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollUp]));

    await flingUp(tester);
    expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollUp, SemanticsAction.scrollDown]));

    await flingDown(tester, repetitions: 2);
    expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollUp]));

    await flingUp(tester, repetitions: 5);
    expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollDown]));

    await flingDown(tester);
    expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollUp, SemanticsAction.scrollDown]));
44 45

    semantics.dispose();
46 47 48
  });

  testWidgets('showOnScreen works in scrollable', (WidgetTester tester) async {
49
    semantics = SemanticsTester(tester); // enables semantics tree generation
50 51 52

    const double kItemHeight = 40.0;

53 54
    final List<Widget> containers = List<Widget>.generate(80, (int i) => MergeSemantics(
      child: Container(
55
        height: kItemHeight,
56
        child: Text('container $i', textDirection: TextDirection.ltr),
57 58
      ),
    ));
59

60
    final ScrollController scrollController = ScrollController(
61 62 63
      initialScrollOffset: kItemHeight / 2,
    );

64
    await tester.pumpWidget(
65
      Directionality(
66
        textDirection: TextDirection.ltr,
67
        child: ListView(
68 69 70 71 72
          controller: scrollController,
          children: containers,
        ),
      ),
    );
73 74 75 76 77 78 79

    expect(scrollController.offset, kItemHeight / 2);

    final int firstContainerId = tester.renderObject(find.byWidget(containers.first)).debugSemantics.id;
    tester.binding.pipelineOwner.semanticsOwner.performAction(firstContainerId, SemanticsAction.showOnScreen);
    await tester.pump();
    await tester.pump(const Duration(seconds: 5));
80

81
    expect(scrollController.offset, 0.0);
82 83

    semantics.dispose();
84
  });
85 86

  testWidgets('showOnScreen works with pinned app bar and sliver list', (WidgetTester tester) async {
87
    semantics = SemanticsTester(tester); // enables semantics tree generation
88 89 90 91

    const double kItemHeight = 100.0;
    const double kExpandedAppBarHeight = 56.0;

92 93
    final List<Widget> containers = List<Widget>.generate(80, (int i) => MergeSemantics(
      child: Container(
94
        height: kItemHeight,
95
        child: Text('container $i'),
96 97
      ),
    ));
98

99
    final ScrollController scrollController = ScrollController(
100 101 102
      initialScrollOffset: kItemHeight / 2,
    );

103
    await tester.pumpWidget(Directionality(
104
      textDirection: TextDirection.ltr,
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127
      child: Localizations(
        locale: const Locale('en', 'us'),
        delegates: const <LocalizationsDelegate<dynamic>>[
          DefaultWidgetsLocalizations.delegate,
          DefaultMaterialLocalizations.delegate,
        ],
        child: MediaQuery(
          data: const MediaQueryData(),
            child: Scrollable(
            controller: scrollController,
            viewportBuilder: (BuildContext context, ViewportOffset offset) {
              return Viewport(
                offset: offset,
                slivers: <Widget>[
                  const SliverAppBar(
                    pinned: true,
                    expandedHeight: kExpandedAppBarHeight,
                    flexibleSpace: FlexibleSpaceBar(
                      title: Text('App Bar'),
                    ),
                  ),
                  SliverList(
                    delegate: SliverChildListDelegate(containers),
128
                  ),
129 130 131 132
                ],
              );
            }),
          ),
133 134 135 136 137 138 139 140 141 142 143
      ),
    ));

    expect(scrollController.offset, kItemHeight / 2);

    final int firstContainerId = tester.renderObject(find.byWidget(containers.first)).debugSemantics.id;
    tester.binding.pipelineOwner.semanticsOwner.performAction(firstContainerId, SemanticsAction.showOnScreen);
    await tester.pump();
    await tester.pump(const Duration(seconds: 5));
    expect(tester.getTopLeft(find.byWidget(containers.first)).dy, kExpandedAppBarHeight);

144
    semantics.dispose();
145 146 147
  });

  testWidgets('showOnScreen works with pinned app bar and individual slivers', (WidgetTester tester) async {
148
    semantics = SemanticsTester(tester); // enables semantics tree generation
149 150 151 152 153

    const double kItemHeight = 100.0;
    const double kExpandedAppBarHeight = 256.0;


154
    final List<Widget> children = <Widget>[];
155 156 157 158
    final List<Widget> slivers = List<Widget>.generate(30, (int i) {
      final Widget child = MergeSemantics(
        child: Container(
          child: Text('Item $i'),
159 160 161
          height: 72.0,
        ),
      );
162
      children.add(child);
163
      return SliverToBoxAdapter(
164 165 166 167
        child: child,
      );
    });

168
    final ScrollController scrollController = ScrollController(
169
      initialScrollOffset: 2.5 * kItemHeight,
170 171
    );

172
    await tester.pumpWidget(Directionality(
173
      textDirection: TextDirection.ltr,
174
      child: MediaQuery(
175
        data: const MediaQueryData(),
176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193
        child: Localizations(
          locale: const Locale('en', 'us'),
          delegates: const <LocalizationsDelegate<dynamic>>[
            DefaultWidgetsLocalizations.delegate,
            DefaultMaterialLocalizations.delegate,
          ],
          child: Scrollable(
            controller: scrollController,
            viewportBuilder: (BuildContext context, ViewportOffset offset) {
              return Viewport(
                offset: offset,
                slivers: <Widget>[
                  const SliverAppBar(
                    pinned: true,
                    expandedHeight: kExpandedAppBarHeight,
                    flexibleSpace: FlexibleSpaceBar(
                      title: Text('App Bar'),
                    ),
194
                  ),
195 196
                  ...slivers,
                ],
197 198 199
              );
            },
          ),
200 201 202 203
        ),
      ),
    ));

204
    expect(scrollController.offset, 2.5 * kItemHeight);
205

206
    final int id0 = tester.renderObject(find.byWidget(children[0])).debugSemantics.id;
207 208 209
    tester.binding.pipelineOwner.semanticsOwner.performAction(id0, SemanticsAction.showOnScreen);
    await tester.pump();
    await tester.pump(const Duration(seconds: 5));
210
    expect(tester.getTopLeft(find.byWidget(children[0])).dy, kToolbarHeight);
211

212
    semantics.dispose();
213
  });
214

215
  testWidgets('correct scrollProgress', (WidgetTester tester) async {
216
    semantics = SemanticsTester(tester);
217

218
    await tester.pumpWidget(Directionality(
219
      textDirection: TextDirection.ltr,
220
      child: ListView(children: List<Widget>.generate(80, (int i) => Text('$i'))),
221 222
    ));

223 224 225 226 227 228 229 230
    expect(semantics, includesNodeWith(
      scrollExtentMin: 0.0,
      scrollPosition: 0.0,
      scrollExtentMax: 520.0,
      actions: <SemanticsAction>[
        SemanticsAction.scrollUp,
      ],
    ));
231

232
    await flingUp(tester);
233

234 235 236 237 238 239 240 241 242
    expect(semantics, includesNodeWith(
      scrollExtentMin: 0.0,
      scrollPosition: 380.2,
      scrollExtentMax: 520.0,
      actions: <SemanticsAction>[
        SemanticsAction.scrollUp,
        SemanticsAction.scrollDown,
      ],
    ));
243

244
    await flingUp(tester);
245

246 247 248 249 250 251 252 253
    expect(semantics, includesNodeWith(
      scrollExtentMin: 0.0,
      scrollPosition: 520.0,
      scrollExtentMax: 520.0,
      actions: <SemanticsAction>[
        SemanticsAction.scrollDown,
      ],
    ));
254 255

    semantics.dispose();
256
  });
257

258
  testWidgets('correct scrollProgress for unbound', (WidgetTester tester) async {
259
    semantics = SemanticsTester(tester);
260

261
    await tester.pumpWidget(Directionality(
262
      textDirection: TextDirection.ltr,
263
      child: ListView.builder(
264
        dragStartBehavior: DragStartBehavior.down,
265 266
        itemExtent: 20.0,
        itemBuilder: (BuildContext context, int index) {
267
          return Text('entry $index');
268
        },
269 270 271
      ),
    ));

272 273 274 275 276 277 278 279
    expect(semantics, includesNodeWith(
      scrollExtentMin: 0.0,
      scrollPosition: 0.0,
      scrollExtentMax: double.infinity,
      actions: <SemanticsAction>[
        SemanticsAction.scrollUp,
      ],
    ));
280

281
    await flingUp(tester);
282

283 284 285 286 287 288 289 290 291
    expect(semantics, includesNodeWith(
      scrollExtentMin: 0.0,
      scrollPosition: 380.2,
      scrollExtentMax: double.infinity,
      actions: <SemanticsAction>[
        SemanticsAction.scrollUp,
        SemanticsAction.scrollDown,
      ],
    ));
292

293
    await flingUp(tester);
294

295 296 297 298 299 300 301 302 303
    expect(semantics, includesNodeWith(
      scrollExtentMin: 0.0,
      scrollPosition: 760.4,
      scrollExtentMax: double.infinity,
      actions: <SemanticsAction>[
        SemanticsAction.scrollUp,
        SemanticsAction.scrollDown,
      ],
    ));
304 305

    semantics.dispose();
306
  });
307 308

  testWidgets('Semantics tree is populated mid-scroll', (WidgetTester tester) async {
309
    semantics = SemanticsTester(tester);
310

311 312 313 314
    final List<Widget> children = List<Widget>.generate(80, (int i) => Container(
      child: Text('Item $i'),
      height: 40.0,
    ));
315
    await tester.pumpWidget(
316
      Directionality(
317
        textDirection: TextDirection.ltr,
318
        child: ListView(children: children),
319 320 321 322 323 324 325 326 327 328
      ),
    );

    final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(ListView)));
    await gesture.moveBy(const Offset(0.0, -40.0));
    await tester.pump();

    expect(semantics, includesNodeWith(label: 'Item 1'));
    expect(semantics, includesNodeWith(label: 'Item 2'));
    expect(semantics, includesNodeWith(label: 'Item 3'));
329 330

    semantics.dispose();
331
  });
332

333
  testWidgets('Can toggle semantics on, off, on without crash', (WidgetTester tester) async {
334
    await tester.pumpWidget(
335
      Directionality(
336
        textDirection: TextDirection.ltr,
337 338 339 340
        child: ListView(
          children: List<Widget>.generate(40, (int i) {
            return Container(
              child: Text('item $i'),
341
              height: 400.0,
342 343 344 345 346 347
            );
          }),
        ),
      ),
    );

348
    final TestSemantics expectedSemantics = TestSemantics.root(
349
      children: <TestSemantics>[
350
        TestSemantics.rootChild(
351
          children: <TestSemantics>[
352
            TestSemantics(
353 354 355
              flags: <SemanticsFlag>[
                SemanticsFlag.hasImplicitScrolling,
              ],
356 357
              actions: <SemanticsAction>[SemanticsAction.scrollUp],
              children: <TestSemantics>[
358
                TestSemantics(
359 360 361
                  label: r'item 0',
                  textDirection: TextDirection.ltr,
                ),
362
                TestSemantics(
363 364 365
                  label: r'item 1',
                  textDirection: TextDirection.ltr,
                ),
366
                TestSemantics(
367 368 369 370 371
                  flags: <SemanticsFlag>[
                    SemanticsFlag.isHidden,
                  ],
                  label: r'item 2',
                ),
372 373 374 375 376 377 378
              ],
            ),
          ],
        ),
      ],
    );

379 380 381 382
    // Start with semantics off.
    expect(tester.binding.pipelineOwner.semanticsOwner, isNull);

    // Semantics on
383
    semantics = SemanticsTester(tester);
384 385
    await tester.pumpAndSettle();
    expect(tester.binding.pipelineOwner.semanticsOwner, isNotNull);
386
    expect(semantics, hasSemantics(expectedSemantics, ignoreId: true, ignoreRect: true, ignoreTransform: true));
387 388

    // Semantics off
389
    semantics.dispose();
390 391 392 393
    await tester.pumpAndSettle();
    expect(tester.binding.pipelineOwner.semanticsOwner, isNull);

    // Semantics on
394
    semantics = SemanticsTester(tester);
395 396
    await tester.pumpAndSettle();
    expect(tester.binding.pipelineOwner.semanticsOwner, isNotNull);
397
    expect(semantics, hasSemantics(expectedSemantics, ignoreId: true, ignoreRect: true, ignoreTransform: true));
398 399

    semantics.dispose();
400
  }, semanticsEnabled: false);
401 402 403 404 405 406 407 408 409 410

  group('showOnScreen', () {

    const double kItemHeight = 100.0;

    List<Widget> children;
    ScrollController scrollController;
    Widget widgetUnderTest;

    setUp(() {
411 412 413
      children = List<Widget>.generate(10, (int i) {
        return MergeSemantics(
          child: Container(
414
            height: kItemHeight,
415
            child: Text('container $i'),
416 417 418 419
          ),
        );
      });

420
      scrollController = ScrollController(
421 422 423
        initialScrollOffset: kItemHeight / 2,
      );

424
      widgetUnderTest = Directionality(
425
        textDirection: TextDirection.ltr,
426 427
        child: Center(
          child: Container(
428
            height: 2 * kItemHeight,
429
            child: ListView(
430 431 432 433 434 435 436 437 438 439
              controller: scrollController,
              children: children,
            ),
          ),
        ),
      );

    });

    testWidgets('brings item above leading edge to leading edge', (WidgetTester tester) async {
440
      semantics = SemanticsTester(tester); // enables semantics tree generation
441 442 443 444 445 446 447 448 449 450 451 452 453 454 455

      await tester.pumpWidget(widgetUnderTest);

      expect(scrollController.offset, kItemHeight / 2);

      final int firstContainerId = tester.renderObject(find.byWidget(children.first)).debugSemantics.id;
      tester.binding.pipelineOwner.semanticsOwner.performAction(firstContainerId, SemanticsAction.showOnScreen);
      await tester.pumpAndSettle();

      expect(scrollController.offset, 0.0);

      semantics.dispose();
    });

    testWidgets('brings item below trailing edge to trailing edge', (WidgetTester tester) async {
456
      semantics = SemanticsTester(tester); // enables semantics tree generation
457 458 459 460 461 462 463 464 465 466 467 468 469 470 471

      await tester.pumpWidget(widgetUnderTest);

      expect(scrollController.offset, kItemHeight / 2);

      final int firstContainerId = tester.renderObject(find.byWidget(children[2])).debugSemantics.id;
      tester.binding.pipelineOwner.semanticsOwner.performAction(firstContainerId, SemanticsAction.showOnScreen);
      await tester.pumpAndSettle();

      expect(scrollController.offset, kItemHeight);

      semantics.dispose();
    });

    testWidgets('does not change position of items already fully on-screen', (WidgetTester tester) async {
472
      semantics = SemanticsTester(tester); // enables semantics tree generation
473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495

      await tester.pumpWidget(widgetUnderTest);

      expect(scrollController.offset, kItemHeight / 2);

      final int firstContainerId = tester.renderObject(find.byWidget(children[1])).debugSemantics.id;
      tester.binding.pipelineOwner.semanticsOwner.performAction(firstContainerId, SemanticsAction.showOnScreen);
      await tester.pumpAndSettle();

      expect(scrollController.offset, kItemHeight / 2);

      semantics.dispose();
    });
  });

  group('showOnScreen with negative children', () {
    const double kItemHeight = 100.0;

    List<Widget> children;
    ScrollController scrollController;
    Widget widgetUnderTest;

    setUp(() {
496
      final Key center = GlobalKey();
497

498 499
      children = List<Widget>.generate(10, (int i) {
        return SliverToBoxAdapter(
500
          key: i == 5 ? center : null,
501 502 503
          child: MergeSemantics(
            key: ValueKey<int>(i),
            child: Container(
504
              height: kItemHeight,
505
              child: Text('container $i'),
506 507 508 509 510
            ),
          ),
        );
      });

511
      scrollController = ScrollController(
512 513 514 515 516 517 518 519 520 521
        initialScrollOffset: -2.5 * kItemHeight,
      );

      // 'container 0' is at offset -500
      // 'container 1' is at offset -400
      // 'container 2' is at offset -300
      // 'container 3' is at offset -200
      // 'container 4' is at offset -100
      // 'container 5' is at offset 0

522
      widgetUnderTest = Directionality(
523
        textDirection: TextDirection.ltr,
524 525
        child: Center(
          child: Container(
526
            height: 2 * kItemHeight,
527
            child: Scrollable(
528 529
              controller: scrollController,
              viewportBuilder: (BuildContext context, ViewportOffset offset) {
530
                return Viewport(
531 532 533
                  cacheExtent: 0.0,
                  offset: offset,
                  center: center,
534
                  slivers: children,
535 536 537 538 539 540 541 542 543 544
                );
              },
            ),
          ),
        ),
      );

    });

    testWidgets('brings item above leading edge to leading edge', (WidgetTester tester) async {
545
      semantics = SemanticsTester(tester); // enables semantics tree generation
546 547 548 549 550 551 552 553 554 555 556 557 558 559 560

      await tester.pumpWidget(widgetUnderTest);

      expect(scrollController.offset, -250.0);

      final int firstContainerId = tester.renderObject(find.byKey(const ValueKey<int>(2))).debugSemantics.id;
      tester.binding.pipelineOwner.semanticsOwner.performAction(firstContainerId, SemanticsAction.showOnScreen);
      await tester.pumpAndSettle();

      expect(scrollController.offset, -300.0);

      semantics.dispose();
    });

    testWidgets('brings item below trailing edge to trailing edge', (WidgetTester tester) async {
561
      semantics = SemanticsTester(tester); // enables semantics tree generation
562 563 564 565 566 567 568 569 570 571 572 573 574 575 576

      await tester.pumpWidget(widgetUnderTest);

      expect(scrollController.offset, -250.0);

      final int firstContainerId = tester.renderObject(find.byKey(const ValueKey<int>(4))).debugSemantics.id;
      tester.binding.pipelineOwner.semanticsOwner.performAction(firstContainerId, SemanticsAction.showOnScreen);
      await tester.pumpAndSettle();

      expect(scrollController.offset, -200.0);

      semantics.dispose();
    });

    testWidgets('does not change position of items already fully on-screen', (WidgetTester tester) async {
577
      semantics = SemanticsTester(tester); // enables semantics tree generation
578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594

      await tester.pumpWidget(widgetUnderTest);

      expect(scrollController.offset, -250.0);

      final int firstContainerId = tester.renderObject(find.byKey(const ValueKey<int>(3))).debugSemantics.id;
      tester.binding.pipelineOwner.semanticsOwner.performAction(firstContainerId, SemanticsAction.showOnScreen);
      await tester.pumpAndSettle();

      expect(scrollController.offset, -250.0);

      semantics.dispose();
    });

  });


595 596
}

597
Future<void> flingUp(WidgetTester tester, { int repetitions = 1 }) => fling(tester, const Offset(0.0, -200.0), repetitions);
598

599
Future<void> flingDown(WidgetTester tester, { int repetitions = 1 }) => fling(tester, const Offset(0.0, 200.0), repetitions);
600

601
Future<void> flingRight(WidgetTester tester, { int repetitions = 1 }) => fling(tester, const Offset(200.0, 0.0), repetitions);
602

603
Future<void> flingLeft(WidgetTester tester, { int repetitions = 1 }) => fling(tester, const Offset(-200.0, 0.0), repetitions);
604

605
Future<void> fling(WidgetTester tester, Offset offset, int repetitions) async {
606
  while (repetitions-- > 0) {
607
    await tester.fling(find.byType(ListView), offset, 1000.0);
608 609 610
    await tester.pump();
    await tester.pump(const Duration(seconds: 5));
  }
611
}