list_view_builder_test.dart 13.1 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5
// 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';
6
import 'package:flutter_test/flutter_test.dart';
7 8 9 10 11

import 'test_widgets.dart';

void main() {
  testWidgets('ListView.builder mount/dismount smoke test', (WidgetTester tester) async {
12
    final List<int> callbackTracker = <int>[];
13 14 15 16 17

    // the root view is 800x600 in the test environment
    // so if our widget is 100 pixels tall, it should fit exactly 6 times.

    Widget builder() {
18
      return Directionality(
19
        textDirection: TextDirection.ltr,
20 21
        child: FlipWidget(
          left: ListView.builder(
22 23 24
            itemExtent: 100.0,
            itemBuilder: (BuildContext context, int index) {
              callbackTracker.add(index);
25
              return SizedBox(
26
                key: ValueKey<int>(index),
27
                height: 100.0,
28
                child: Text('$index'),
29 30 31 32
              );
            },
          ),
          right: const Text('Not Today'),
33 34 35 36 37 38
        ),
      );
    }

    await tester.pumpWidget(builder());

39
    final FlipWidgetState testWidget = tester.state(find.byType(FlipWidget));
40

41 42 43 44 45
    expect(callbackTracker, equals(<int>[
      0, 1, 2, 3, 4, 5, // visible in viewport
      6, 7, 8, // in caching area
    ]));
    check(visible: <int>[0, 1, 2, 3, 4, 5], hidden: <int>[ 6, 7, 8]);
46 47 48 49 50 51 52 53 54 55 56

    callbackTracker.clear();
    testWidget.flip();
    await tester.pump();

    expect(callbackTracker, equals(<int>[]));

    callbackTracker.clear();
    testWidget.flip();
    await tester.pump();

57 58 59 60 61
    expect(callbackTracker, equals(<int>[
      0, 1, 2, 3, 4, 5,
      6, 7, 8, // in caching area
    ]));
    check(visible: <int>[0, 1, 2, 3, 4, 5], hidden: <int>[ 6, 7, 8]);
62 63 64
  });

  testWidgets('ListView.builder vertical', (WidgetTester tester) async {
65
    final List<int> callbackTracker = <int>[];
66 67 68 69 70

    // the root view is 800x600 in the test environment
    // so if our widget is 200 pixels tall, it should fit exactly 3 times.
    // but if we are offset by 300 pixels, there will be 4, numbered 1-4.

71
    Widget itemBuilder(BuildContext context, int index) {
72
      callbackTracker.add(index);
73
      return SizedBox(
74
        key: ValueKey<int>(index),
75 76
        width: 500.0, // this should be ignored
        height: 400.0, // should be overridden by itemExtent
77
        child: Text('$index', textDirection: TextDirection.ltr),
78
      );
79
    }
80

81
    Widget buildWidget() {
82
      return Directionality(
83
        textDirection: TextDirection.ltr,
84 85 86
        child: FlipWidget(
          left: ListView.builder(
            controller: ScrollController(initialScrollOffset: 300.0),
87 88 89
            itemExtent: 200.0,
            itemBuilder: itemBuilder,
          ),
Ian Hickson's avatar
Ian Hickson committed
90
          right: const Text('Not Today'),
91 92 93 94 95
        ),
      );
    }

    void jumpTo(double newScrollOffset) {
Adam Barth's avatar
Adam Barth committed
96
      final ScrollableState scrollable = tester.state(find.byType(Scrollable));
97 98 99 100 101
      scrollable.position.jumpTo(newScrollOffset);
    }

    await tester.pumpWidget(buildWidget());

102 103 104 105 106 107
    expect(callbackTracker, equals(<int>[
      0, // in caching area
      1, 2, 3, 4,
      5, // in caching area
    ]));
    check(visible: <int>[1, 2, 3, 4], hidden: <int>[0, 5]);
108 109 110 111 112 113 114
    callbackTracker.clear();

    jumpTo(400.0);
    // now only 3 should fit, numbered 2-4.

    await tester.pumpWidget(buildWidget());

115 116 117 118 119 120
    expect(callbackTracker, equals(<int>[
      0, 1, // in caching area
      2, 3, 4,
      5, 6, // in caching area
    ]));
    check(visible: <int>[2, 3, 4], hidden: <int>[0, 1, 5, 6]);
121 122 123 124 125 126 127
    callbackTracker.clear();

    jumpTo(500.0);
    // now 4 should fit, numbered 2-5.

    await tester.pumpWidget(buildWidget());

128 129 130 131 132 133
    expect(callbackTracker, equals(<int>[
      0, 1, // in caching area
      2, 3, 4, 5,
      6, // in caching area
    ]));
    check(visible: <int>[2, 3, 4, 5], hidden: <int>[0, 1, 6]);
134 135 136 137
    callbackTracker.clear();
  });

  testWidgets('ListView.builder horizontal', (WidgetTester tester) async {
138
    final List<int> callbackTracker = <int>[];
139 140 141 142 143

    // the root view is 800x600 in the test environment
    // so if our widget is 200 pixels wide, it should fit exactly 4 times.
    // but if we are offset by 300 pixels, there will be 5, numbered 1-5.

144
    Widget itemBuilder(BuildContext context, int index) {
145
      callbackTracker.add(index);
146
      return SizedBox(
147
        key: ValueKey<int>(index),
148 149
        width: 400.0, // this should be overridden by itemExtent
        height: 500.0, // this should be ignored
150
        child: Text('$index'),
151
      );
152
    }
153

154
    Widget buildWidget() {
155
      return Directionality(
156
        textDirection: TextDirection.ltr,
157 158 159
        child: FlipWidget(
          left: ListView.builder(
            controller: ScrollController(initialScrollOffset: 300.0),
160 161 162 163 164
            itemBuilder: itemBuilder,
            itemExtent: 200.0,
            scrollDirection: Axis.horizontal,
          ),
          right: const Text('Not Today'),
165 166 167 168 169
        ),
      );
    }

    void jumpTo(double newScrollOffset) {
Adam Barth's avatar
Adam Barth committed
170
      final ScrollableState scrollable = tester.state(find.byType(Scrollable));
171 172 173 174 175
      scrollable.position.jumpTo(newScrollOffset);
    }

    await tester.pumpWidget(buildWidget());

176 177 178 179 180 181
    expect(callbackTracker, equals(<int>[
      0, // in caching area
      1, 2, 3, 4, 5,
      6, // in caching area
    ]));
    check(visible: <int>[1, 2, 3, 4, 5], hidden: <int>[0, 6]);
182 183 184 185 186 187 188
    callbackTracker.clear();

    jumpTo(400.0);
    // now only 4 should fit, numbered 2-5.

    await tester.pumpWidget(buildWidget());

189 190 191 192 193 194
    expect(callbackTracker, equals(<int>[
      0, 1, // in caching area
      2, 3, 4, 5,
      6, 7, // in caching area
    ]));
    check(visible: <int>[2, 3, 4, 5], hidden: <int>[0, 1, 6, 7]);
195 196 197 198 199 200 201
    callbackTracker.clear();

    jumpTo(500.0);
    // now only 5 should fit, numbered 2-6.

    await tester.pumpWidget(buildWidget());

202 203 204 205 206 207
    expect(callbackTracker, equals(<int>[
      0, 1, // in caching area
      2, 3, 4, 5, 6,
      7, // in caching area
    ]));
    check(visible: <int>[2, 3, 4, 5, 6], hidden: <int>[0, 1, 7]);
208 209 210 211
    callbackTracker.clear();
  });

  testWidgets('ListView.builder 10 items, 2-3 items visible', (WidgetTester tester) async {
212
    final List<int> callbackTracker = <int>[];
213 214 215 216 217

    // The root view is 800x600 in the test environment and our list
    // items are 300 tall. Scrolling should cause two or three items
    // to be built.

218
    Widget itemBuilder(BuildContext context, int index) {
219
      callbackTracker.add(index);
220
      return Text('$index', key: ValueKey<int>(index), textDirection: TextDirection.ltr);
221
    }
222

223
    final Widget testWidget = Directionality(
224
      textDirection: TextDirection.ltr,
225
      child: ListView.builder(
226 227 228 229
        itemBuilder: itemBuilder,
        itemExtent: 300.0,
        itemCount: 10,
      ),
230 231 232
    );

    void jumpTo(double newScrollOffset) {
Adam Barth's avatar
Adam Barth committed
233
      final ScrollableState scrollable = tester.state(find.byType(Scrollable));
234 235 236 237
      scrollable.position.jumpTo(newScrollOffset);
    }

    await tester.pumpWidget(testWidget);
238 239
    expect(callbackTracker, equals(<int>[0, 1, 2]));
    check(visible: <int>[0, 1], hidden: <int>[2]);
240 241 242 243 244
    callbackTracker.clear();

    jumpTo(150.0);
    await tester.pump();

245 246
    expect(callbackTracker, equals(<int>[3]));
    check(visible: <int>[0, 1, 2], hidden: <int>[3]);
247 248 249 250 251
    callbackTracker.clear();

    jumpTo(600.0);
    await tester.pump();

252 253
    expect(callbackTracker, equals(<int>[4]));
    check(visible: <int>[2, 3], hidden: <int>[0, 1, 4]);
254 255 256 257 258
    callbackTracker.clear();

    jumpTo(750.0);
    await tester.pump();

259 260
    expect(callbackTracker, equals(<int>[5]));
    check(visible: <int>[2, 3, 4], hidden: <int>[0, 1, 5]);
261 262 263
    callbackTracker.clear();
  });

264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311
  testWidgets('ListView.builder 30 items with big jump, using prototypeItem', (WidgetTester tester) async {
    final List<int> callbackTracker = <int>[];

    // The root view is 800x600 in the test environment and our list
    // items are 300 tall. Scrolling should cause two or three items
    // to be built.

    Widget itemBuilder(BuildContext context, int index) {
      callbackTracker.add(index);
      return Text('$index', key: ValueKey<int>(index), textDirection: TextDirection.ltr);
    }

    final Widget testWidget = Directionality(
      textDirection: TextDirection.ltr,
      child: ListView.builder(
        itemBuilder: itemBuilder,
        prototypeItem: const SizedBox(
          width: 800,
          height: 300,
        ),
        itemCount: 30,
      ),
    );

    void jumpTo(double newScrollOffset) {
      final ScrollableState scrollable = tester.state(find.byType(Scrollable));
      scrollable.position.jumpTo(newScrollOffset);
    }

    await tester.pumpWidget(testWidget);

    // 2 is in the cache area, but not visible.
    expect(callbackTracker, equals(<int>[0, 1, 2]));
    final List<int> initialExpectedHidden = List<int>.generate(28, (int i) => i + 2);
    check(visible: <int>[0, 1], hidden: initialExpectedHidden);
    callbackTracker.clear();

    // Jump to the end of the ListView.
    jumpTo(8400);
    await tester.pump();

    // 27 is in the cache area, but not visible.
    expect(callbackTracker, equals(<int>[27, 28, 29]));
    final List<int> finalExpectedHidden = List<int>.generate(28, (int i) => i);
    check(visible: <int>[28, 29], hidden: finalExpectedHidden);
    callbackTracker.clear();
  });

312
  testWidgets('ListView.separated', (WidgetTester tester) async {
313
    Widget buildFrame({ required int itemCount }) {
314
      return Directionality(
315
        textDirection: TextDirection.ltr,
316
        child: ListView.separated(
317 318
          itemCount: itemCount,
          itemBuilder: (BuildContext context, int index) {
319
            return SizedBox(
320
              height: 100.0,
321
              child: Text('i$index'),
322 323 324
            );
          },
          separatorBuilder: (BuildContext context, int index) {
325
            return SizedBox(
326
              height: 10.0,
327
              child: Text('s$index'),
328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349
            );
          },
        ),
      );
    }

    await tester.pumpWidget(buildFrame(itemCount: 0));
    expect(find.text('i0'), findsNothing);
    expect(find.text('s0'), findsNothing);

    await tester.pumpWidget(buildFrame(itemCount: 1));
    expect(find.text('i0'), findsOneWidget);
    expect(find.text('s0'), findsNothing);

    await tester.pumpWidget(buildFrame(itemCount: 2));
    expect(find.text('i0'), findsOneWidget);
    expect(find.text('s0'), findsOneWidget);
    expect(find.text('i1'), findsOneWidget);
    expect(find.text('s1'), findsNothing);

    // ListView's height is 600, so items i0-i5 and s0-s4 fit.
    await tester.pumpWidget(buildFrame(itemCount: 25));
350
    for (final String s in <String>['i0', 's0', 'i1', 's1', 'i2', 's2', 'i3', 's3', 'i4', 's4', 'i5']) {
351
      expect(find.text(s), findsOneWidget);
352
    }
353 354 355
    expect(find.text('s5'), findsNothing);
    expect(find.text('i6'), findsNothing);
  });
356 357 358


  testWidgets('ListView.separated uses correct semanticChildCount', (WidgetTester tester) async {
359
    Widget buildFrame({ required int itemCount}) {
360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403
      return Directionality(
        textDirection: TextDirection.ltr,
        child: ListView.separated(
          itemCount: itemCount,
          itemBuilder: (BuildContext context, int index) {
            return SizedBox(
              height: 100.0,
              child: Text('i$index'),
            );
          },
          separatorBuilder: (BuildContext context, int index) {
            return SizedBox(
              height: 10.0,
              child: Text('s$index'),
            );
          },
        ),
      );
    }

    Scrollable scrollable() {
      return tester.widget<Scrollable>(
        find.descendant(
          of: find.byType(ListView),
          matching: find.byType(Scrollable),
        ),
      );
    }

    await tester.pumpWidget(buildFrame(itemCount: 0));
    expect(scrollable().semanticChildCount, 0);

    await tester.pumpWidget(buildFrame(itemCount: 1));
    expect(scrollable().semanticChildCount, 1);

    await tester.pumpWidget(buildFrame(itemCount: 2));
    expect(scrollable().semanticChildCount, 2);

    await tester.pumpWidget(buildFrame(itemCount: 3));
    expect(scrollable().semanticChildCount, 3);

    await tester.pumpWidget(buildFrame(itemCount: 4));
    expect(scrollable().semanticChildCount, 4);
  });
404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426

  // Regression test for https://github.com/flutter/flutter/issues/72292
  testWidgets('ListView.builder and SingleChildScrollView can work well together', (WidgetTester tester) async {
    Widget builder(int itemCount) {
      return Directionality(
        textDirection: TextDirection.ltr,
        child: SingleChildScrollView(
          child: ListView.builder(
            shrinkWrap: true,
            itemExtent: 35,
            itemCount: itemCount,
            itemBuilder: (BuildContext context, int index) {
              return const Text('I love Flutter.');
            },
          ),
        ),
      );
    }

    await tester.pumpWidget(builder(1));
    // Trigger relayout and garbage collect.
    await tester.pumpWidget(builder(2));
  });
427
}
428

429
void check({ List<int> visible = const <int>[], List<int> hidden = const <int>[] }) {
430
  for (final int i in visible) {
431 432
    expect(find.text('$i'), findsOneWidget);
  }
433
  for (final int i in hidden) {
434 435 436
    expect(find.text('$i'), findsNothing);
  }
}