scrollable_semantics_test.dart 19.1 KB
Newer Older
1 2 3 4
// Copyright 2017 The Chromium Authors. All rights reserved.
// 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 25 26

    final List<Widget> textWidgets = <Widget>[];
    for (int i = 0; i < 80; i++)
27
      textWidgets.add(Text('$i'));
28
    await tester.pumpWidget(
29
      Directionality(
30
        textDirection: TextDirection.ltr,
31
        child: ListView(children: textWidgets),
32 33
      ),
    );
34 35 36 37 38 39 40 41 42 43 44 45 46 47

    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]));
48 49

    semantics.dispose();
50 51 52
  });

  testWidgets('showOnScreen works in scrollable', (WidgetTester tester) async {
53
    semantics = SemanticsTester(tester); // enables semantics tree generation
54 55 56 57 58

    const double kItemHeight = 40.0;

    final List<Widget> containers = <Widget>[];
    for (int i = 0; i < 80; i++)
59
      containers.add(MergeSemantics(child: Container(
60
        height: kItemHeight,
61
        child: Text('container $i', textDirection: TextDirection.ltr),
62 63
      )));

64
    final ScrollController scrollController = ScrollController(
65 66 67
      initialScrollOffset: kItemHeight / 2,
    );

68
    await tester.pumpWidget(
69
      Directionality(
70
        textDirection: TextDirection.ltr,
71
        child: ListView(
72 73 74 75 76
          controller: scrollController,
          children: containers,
        ),
      ),
    );
77 78 79 80 81 82 83

    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));
84

85
    expect(scrollController.offset, 0.0);
86 87

    semantics.dispose();
88
  });
89 90

  testWidgets('showOnScreen works with pinned app bar and sliver list', (WidgetTester tester) async {
91
    semantics = SemanticsTester(tester); // enables semantics tree generation
92 93 94 95 96 97

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

    final List<Widget> containers = <Widget>[];
    for (int i = 0; i < 80; i++)
98
      containers.add(MergeSemantics(child: Container(
99
        height: kItemHeight,
100
        child: Text('container $i'),
101 102
      )));

103
    final ScrollController scrollController = ScrollController(
104 105 106
      initialScrollOffset: kItemHeight / 2,
    );

107
    await tester.pumpWidget(Directionality(
108
      textDirection: TextDirection.ltr,
109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136
      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),
                  )
                ],
              );
            }),
          ),
137 138 139 140 141 142 143 144 145 146 147
      ),
    ));

    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);

148
    semantics.dispose();
149 150 151
  });

  testWidgets('showOnScreen works with pinned app bar and individual slivers', (WidgetTester tester) async {
152
    semantics = SemanticsTester(tester); // enables semantics tree generation
153 154 155 156 157

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


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

172
    final ScrollController scrollController = ScrollController(
173
      initialScrollOffset: 2.5 * kItemHeight,
174 175
    );

176
    await tester.pumpWidget(Directionality(
177
      textDirection: TextDirection.ltr,
178
      child: MediaQuery(
179
        data: const MediaQueryData(),
180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197
        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'),
                    ),
198
                  ),
199 200 201 202
                ]..addAll(slivers),
              );
            },
          ),
203 204 205 206
        ),
      ),
    ));

207
    expect(scrollController.offset, 2.5 * kItemHeight);
208

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

215
    semantics.dispose();
216
  });
217

218
  testWidgets('correct scrollProgress', (WidgetTester tester) async {
219
    semantics = SemanticsTester(tester);
220 221 222

    final List<Widget> textWidgets = <Widget>[];
    for (int i = 0; i < 80; i++)
223 224
      textWidgets.add(Text('$i'));
    await tester.pumpWidget(Directionality(
225
      textDirection: TextDirection.ltr,
226
      child: ListView(children: textWidgets),
227 228
    ));

229 230 231 232 233 234 235 236
    expect(semantics, includesNodeWith(
      scrollExtentMin: 0.0,
      scrollPosition: 0.0,
      scrollExtentMax: 520.0,
      actions: <SemanticsAction>[
        SemanticsAction.scrollUp,
      ],
    ));
237

238
    await flingUp(tester);
239

240 241 242 243 244 245 246 247 248
    expect(semantics, includesNodeWith(
      scrollExtentMin: 0.0,
      scrollPosition: 380.2,
      scrollExtentMax: 520.0,
      actions: <SemanticsAction>[
        SemanticsAction.scrollUp,
        SemanticsAction.scrollDown,
      ],
    ));
249

250
    await flingUp(tester);
251

252 253 254 255 256 257 258 259
    expect(semantics, includesNodeWith(
      scrollExtentMin: 0.0,
      scrollPosition: 520.0,
      scrollExtentMax: 520.0,
      actions: <SemanticsAction>[
        SemanticsAction.scrollDown,
      ],
    ));
260 261

    semantics.dispose();
262 263
  });

264
  testWidgets('correct scrollProgress for unbound', (WidgetTester tester) async {
265
    semantics = SemanticsTester(tester);
266

267
    await tester.pumpWidget(Directionality(
268
      textDirection: TextDirection.ltr,
269
      child: ListView.builder(
270
        dragStartBehavior: DragStartBehavior.down,
271 272
        itemExtent: 20.0,
        itemBuilder: (BuildContext context, int index) {
273
          return Text('entry $index');
274
        },
275 276 277
      ),
    ));

278 279 280 281 282 283 284 285
    expect(semantics, includesNodeWith(
      scrollExtentMin: 0.0,
      scrollPosition: 0.0,
      scrollExtentMax: double.infinity,
      actions: <SemanticsAction>[
        SemanticsAction.scrollUp,
      ],
    ));
286

287
    await flingUp(tester);
288

289 290 291 292 293 294 295 296 297
    expect(semantics, includesNodeWith(
      scrollExtentMin: 0.0,
      scrollPosition: 380.2,
      scrollExtentMax: double.infinity,
      actions: <SemanticsAction>[
        SemanticsAction.scrollUp,
        SemanticsAction.scrollDown,
      ],
    ));
298

299
    await flingUp(tester);
300

301 302 303 304 305 306 307 308 309
    expect(semantics, includesNodeWith(
      scrollExtentMin: 0.0,
      scrollPosition: 760.4,
      scrollExtentMax: double.infinity,
      actions: <SemanticsAction>[
        SemanticsAction.scrollUp,
        SemanticsAction.scrollDown,
      ],
    ));
310 311

    semantics.dispose();
312
  });
313 314

  testWidgets('Semantics tree is populated mid-scroll', (WidgetTester tester) async {
315
    semantics = SemanticsTester(tester);
316 317 318

    final List<Widget> children = <Widget>[];
    for (int i = 0; i < 80; i++)
319 320
      children.add(Container(
        child: Text('Item $i'),
321 322 323
        height: 40.0,
      ));
    await tester.pumpWidget(
324
      Directionality(
325
        textDirection: TextDirection.ltr,
326
        child: ListView(children: children),
327 328 329 330 331 332 333 334 335 336
      ),
    );

    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'));
337 338

    semantics.dispose();
339
  });
340

341
  testWidgets('Can toggle semantics on, off, on without crash', (WidgetTester tester) async {
342
    await tester.pumpWidget(
343
      Directionality(
344
        textDirection: TextDirection.ltr,
345 346 347 348
        child: ListView(
          children: List<Widget>.generate(40, (int i) {
            return Container(
              child: Text('item $i'),
349
              height: 400.0,
350 351 352 353 354 355
            );
          }),
        ),
      ),
    );

356
    final TestSemantics expectedSemantics = TestSemantics.root(
357
      children: <TestSemantics>[
358
        TestSemantics.rootChild(
359
          children: <TestSemantics>[
360
            TestSemantics(
361 362 363
              flags: <SemanticsFlag>[
                SemanticsFlag.hasImplicitScrolling,
              ],
364 365
              actions: <SemanticsAction>[SemanticsAction.scrollUp],
              children: <TestSemantics>[
366
                TestSemantics(
367 368 369
                  label: r'item 0',
                  textDirection: TextDirection.ltr,
                ),
370
                TestSemantics(
371 372 373
                  label: r'item 1',
                  textDirection: TextDirection.ltr,
                ),
374
                TestSemantics(
375 376 377 378 379
                  flags: <SemanticsFlag>[
                    SemanticsFlag.isHidden,
                  ],
                  label: r'item 2',
                ),
380 381 382 383 384 385 386
              ],
            ),
          ],
        ),
      ],
    );

387 388 389 390
    // Start with semantics off.
    expect(tester.binding.pipelineOwner.semanticsOwner, isNull);

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

    // Semantics off
397
    semantics.dispose();
398 399 400 401
    await tester.pumpAndSettle();
    expect(tester.binding.pipelineOwner.semanticsOwner, isNull);

    // Semantics on
402
    semantics = SemanticsTester(tester);
403 404
    await tester.pumpAndSettle();
    expect(tester.binding.pipelineOwner.semanticsOwner, isNotNull);
405
    expect(semantics, hasSemantics(expectedSemantics, ignoreId: true, ignoreRect: true, ignoreTransform: true));
406 407

    semantics.dispose();
408
  });
409 410 411 412 413 414 415 416 417 418

  group('showOnScreen', () {

    const double kItemHeight = 100.0;

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

    setUp(() {
419 420 421
      children = List<Widget>.generate(10, (int i) {
        return MergeSemantics(
          child: Container(
422
            height: kItemHeight,
423
            child: Text('container $i'),
424 425 426 427
          ),
        );
      });

428
      scrollController = ScrollController(
429 430 431
        initialScrollOffset: kItemHeight / 2,
      );

432
      widgetUnderTest = Directionality(
433
        textDirection: TextDirection.ltr,
434 435
        child: Center(
          child: Container(
436
            height: 2 * kItemHeight,
437
            child: ListView(
438 439 440 441 442 443 444 445 446 447
              controller: scrollController,
              children: children,
            ),
          ),
        ),
      );

    });

    testWidgets('brings item above leading edge to leading edge', (WidgetTester tester) async {
448
      semantics = SemanticsTester(tester); // enables semantics tree generation
449 450 451 452 453 454 455 456 457 458 459 460 461 462 463

      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 {
464
      semantics = SemanticsTester(tester); // enables semantics tree generation
465 466 467 468 469 470 471 472 473 474 475 476 477 478 479

      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 {
480
      semantics = SemanticsTester(tester); // enables semantics tree generation
481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503

      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(() {
504
      final Key center = GlobalKey();
505

506 507
      children = List<Widget>.generate(10, (int i) {
        return SliverToBoxAdapter(
508
          key: i == 5 ? center : null,
509 510 511
          child: MergeSemantics(
            key: ValueKey<int>(i),
            child: Container(
512
              height: kItemHeight,
513
              child: Text('container $i'),
514 515 516 517 518
            ),
          ),
        );
      });

519
      scrollController = ScrollController(
520 521 522 523 524 525 526 527 528 529
        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

530
      widgetUnderTest = Directionality(
531
        textDirection: TextDirection.ltr,
532 533
        child: Center(
          child: Container(
534
            height: 2 * kItemHeight,
535
            child: Scrollable(
536 537
              controller: scrollController,
              viewportBuilder: (BuildContext context, ViewportOffset offset) {
538
                return Viewport(
539 540 541 542 543 544 545 546 547 548 549 550 551 552
                  cacheExtent: 0.0,
                  offset: offset,
                  center: center,
                  slivers: children
                );
              },
            ),
          ),
        ),
      );

    });

    testWidgets('brings item above leading edge to leading edge', (WidgetTester tester) async {
553
      semantics = SemanticsTester(tester); // enables semantics tree generation
554 555 556 557 558 559 560 561 562 563 564 565 566 567 568

      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 {
569
      semantics = SemanticsTester(tester); // enables semantics tree generation
570 571 572 573 574 575 576 577 578 579 580 581 582 583 584

      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 {
585
      semantics = SemanticsTester(tester); // enables semantics tree generation
586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602

      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();
    });

  });


603 604
}

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

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

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

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

613
Future<void> fling(WidgetTester tester, Offset offset, int repetitions) async {
614
  while (repetitions-- > 0) {
615
    await tester.fling(find.byType(ListView), offset, 1000.0);
616 617 618
    await tester.pump();
    await tester.pump(const Duration(seconds: 5));
  }
619
}