scroll_controller_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
// 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' as ui;

7
import 'package:flutter/widgets.dart';
8
import 'package:flutter_test/flutter_test.dart';
9
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
10 11 12 13 14

import 'states.dart';

void main() {
  testWidgets('ScrollController control test', (WidgetTester tester) async {
15
    final ScrollController controller = ScrollController();
16

17
    await tester.pumpWidget(
18
      Directionality(
19
        textDirection: TextDirection.ltr,
20
        child: ListView(
21 22
          controller: controller,
          children: kStates.map<Widget>((String state) {
23
            return SizedBox(
24
              height: 200.0,
25
              child: Text(state),
26 27 28 29 30
            );
          }).toList(),
        ),
      ),
    );
31 32

    double realOffset() {
Adam Barth's avatar
Adam Barth committed
33
      return tester.state<ScrollableState>(find.byType(Scrollable)).position.pixels;
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
    }

    expect(controller.offset, equals(0.0));
    expect(realOffset(), equals(controller.offset));

    controller.jumpTo(653.0);

    expect(controller.offset, equals(653.0));
    expect(realOffset(), equals(controller.offset));

    await tester.pump();

    expect(controller.offset, equals(653.0));
    expect(realOffset(), equals(controller.offset));

    controller.animateTo(326.0, duration: const Duration(milliseconds: 300), curve: Curves.ease);
50
    await tester.pumpAndSettle();
51 52 53 54

    expect(controller.offset, equals(326.0));
    expect(realOffset(), equals(controller.offset));

55
    await tester.pumpWidget(
56
      Directionality(
57
        textDirection: TextDirection.ltr,
58
        child: ListView(
59 60 61
          key: const Key('second'),
          controller: controller,
          children: kStates.map<Widget>((String state) {
62
            return SizedBox(
63
              height: 200.0,
64
              child: Text(state),
65 66 67 68 69
            );
          }).toList(),
        ),
      ),
    );
70 71 72 73 74 75 76 77 78

    expect(controller.offset, equals(0.0));
    expect(realOffset(), equals(controller.offset));

    controller.jumpTo(653.0);

    expect(controller.offset, equals(653.0));
    expect(realOffset(), equals(controller.offset));

79
    final ScrollController controller2 = ScrollController();
80

81
    await tester.pumpWidget(
82
      Directionality(
83
        textDirection: TextDirection.ltr,
84
        child: ListView(
85 86 87
          key: const Key('second'),
          controller: controller2,
          children: kStates.map<Widget>((String state) {
88
            return SizedBox(
89
              height: 200.0,
90
              child: Text(state),
91 92 93 94 95
            );
          }).toList(),
        ),
      ),
    );
96

97
    expect(() => controller.offset, throwsAssertionError);
98 99 100
    expect(controller2.offset, equals(653.0));
    expect(realOffset(), equals(controller2.offset));

101 102
    expect(() => controller.jumpTo(120.0), throwsAssertionError);
    expect(() => controller.animateTo(132.0, duration: const Duration(milliseconds: 300), curve: Curves.ease), throwsAssertionError);
103

104
    await tester.pumpWidget(
105
      Directionality(
106
        textDirection: TextDirection.ltr,
107
        child: ListView(
108 109 110 111
          key: const Key('second'),
          controller: controller2,
          physics: const BouncingScrollPhysics(),
          children: kStates.map<Widget>((String state) {
112
            return SizedBox(
113
              height: 200.0,
114
              child: Text(state),
115 116 117 118 119
            );
          }).toList(),
        ),
      ),
    );
120 121 122 123 124 125 126 127 128 129 130 131 132 133

    expect(controller2.offset, equals(653.0));
    expect(realOffset(), equals(controller2.offset));

    controller2.jumpTo(432.0);

    expect(controller2.offset, equals(432.0));
    expect(realOffset(), equals(controller2.offset));

    await tester.pump();

    expect(controller2.offset, equals(432.0));
    expect(realOffset(), equals(controller2.offset));
  });
134 135

  testWidgets('ScrollController control test', (WidgetTester tester) async {
136
    final ScrollController controller = ScrollController(
137 138 139
      initialScrollOffset: 209.0,
    );

140
    await tester.pumpWidget(
141
      Directionality(
142
        textDirection: TextDirection.ltr,
143
        child: GridView.count(
144 145
          crossAxisCount: 4,
          controller: controller,
146
          children: kStates.map<Widget>((String state) => Text(state)).toList(),
147 148 149
        ),
      ),
    );
150 151

    double realOffset() {
Adam Barth's avatar
Adam Barth committed
152
      return tester.state<ScrollableState>(find.byType(Scrollable)).position.pixels;
153 154 155 156 157 158 159 160 161 162 163 164
    }

    expect(controller.offset, equals(209.0));
    expect(realOffset(), equals(controller.offset));

    controller.jumpTo(105.0);

    await tester.pump();

    expect(controller.offset, equals(105.0));
    expect(realOffset(), equals(controller.offset));

165
    await tester.pumpWidget(
166
      Directionality(
167
        textDirection: TextDirection.ltr,
168
        child: GridView.count(
169 170
          crossAxisCount: 2,
          controller: controller,
171
          children: kStates.map<Widget>((String state) => Text(state)).toList(),
172 173 174
        ),
      ),
    );
175 176 177 178

    expect(controller.offset, equals(105.0));
    expect(realOffset(), equals(controller.offset));
  });
179 180

  testWidgets('DrivenScrollActivity ending after dispose', (WidgetTester tester) async {
181
    final ScrollController controller = ScrollController();
182

183
    await tester.pumpWidget(
184
      Directionality(
185
        textDirection: TextDirection.ltr,
186
        child: ListView(
187
          controller: controller,
188
          children: <Widget>[ Container(height: 200000.0) ],
189 190 191
        ),
      ),
    );
192 193 194 195 196 197

    controller.animateTo(1000.0, duration: const Duration(seconds: 1), curve: Curves.linear);

    await tester.pump(); // Start the animation.

    // We will now change the tree on the same frame as the animation ends.
198
    await tester.pumpWidget(Container(), const Duration(seconds: 2));
199 200
  });

201
  testWidgets('Read operations on ScrollControllers with no positions fail', (WidgetTester tester) async {
202
    final ScrollController controller = ScrollController();
203 204 205 206 207
    expect(() => controller.offset, throwsAssertionError);
    expect(() => controller.position, throwsAssertionError);
  });

  testWidgets('Read operations on ScrollControllers with more than one position fail', (WidgetTester tester) async {
208
    final ScrollController controller = ScrollController();
209
    await tester.pumpWidget(
210
      Directionality(
211
        textDirection: TextDirection.ltr,
212
        child: ListView(
213
          children: <Widget>[
214
            Container(
215
              constraints: const BoxConstraints(maxHeight: 500.0),
216
              child: ListView(
217 218
                controller: controller,
                children: kStates.map<Widget>((String state) {
219
                  return SizedBox(height: 200.0, child: Text(state));
220 221 222
                }).toList(),
              ),
            ),
223
            Container(
224
              constraints: const BoxConstraints(maxHeight: 500.0),
225
              child: ListView(
226 227
                controller: controller,
                children: kStates.map<Widget>((String state) {
228
                  return SizedBox(height: 200.0, child: Text(state));
229 230 231 232
                }).toList(),
              ),
            ),
          ],
233
        ),
234 235
      ),
    );
236 237 238 239 240 241

    expect(() => controller.offset, throwsAssertionError);
    expect(() => controller.position, throwsAssertionError);
  });

  testWidgets('Write operations on ScrollControllers with no positions fail', (WidgetTester tester) async {
242
    final ScrollController controller = ScrollController();
243 244 245 246
    expect(() => controller.animateTo(1.0, duration: const Duration(seconds: 1), curve: Curves.linear), throwsAssertionError);
    expect(() => controller.jumpTo(1.0), throwsAssertionError);
  });

247
  testWidgets('Write operations on ScrollControllers with more than one position do not throw', (WidgetTester tester) async {
248
    final ScrollController controller = ScrollController();
249
    await tester.pumpWidget(
250
      Directionality(
251
        textDirection: TextDirection.ltr,
252
        child: ListView(
253
          children: <Widget>[
254
            Container(
255
              constraints: const BoxConstraints(maxHeight: 500.0),
256
              child: ListView(
257 258
                controller: controller,
                children: kStates.map<Widget>((String state) {
259
                  return SizedBox(height: 200.0, child: Text(state));
260 261 262
                }).toList(),
              ),
            ),
263
            Container(
264
              constraints: const BoxConstraints(maxHeight: 500.0),
265
              child: ListView(
266 267
                controller: controller,
                children: kStates.map<Widget>((String state) {
268
                  return SizedBox(height: 200.0, child: Text(state));
269 270 271 272
                }).toList(),
              ),
            ),
          ],
273
        ),
274 275
      ),
    );
276

277 278
    controller.jumpTo(1.0);
    controller.animateTo(1.0, duration: const Duration(seconds: 1), curve: Curves.linear);
279
    await tester.pumpAndSettle();
280
  });
281 282

  testWidgets('Scroll controllers notify when the position changes', (WidgetTester tester) async {
283
    final ScrollController controller = ScrollController();
284 285 286 287 288 289 290

    final List<double> log = <double>[];

    controller.addListener(() {
      log.add(controller.offset);
    });

291
    await tester.pumpWidget(
292
      Directionality(
293
        textDirection: TextDirection.ltr,
294
        child: ListView(
295 296
          controller: controller,
          children: kStates.map<Widget>((String state) {
297
            return SizedBox(height: 200.0, child: Text(state));
298 299 300 301
          }).toList(),
        ),
      ),
    );
302 303 304 305 306

    expect(log, isEmpty);

    await tester.drag(find.byType(ListView), const Offset(0.0, -250.0));

307
    expect(log, equals(<double>[ 20.0, 250.0 ]));
308 309 310 311 312 313 314
    log.clear();

    controller.dispose();

    await tester.drag(find.byType(ListView), const Offset(0.0, -130.0));
    expect(log, isEmpty);
  });
315 316

  testWidgets('keepScrollOffset', (WidgetTester tester) async {
317
    final PageStorageBucket bucket = PageStorageBucket();
318 319

    Widget buildFrame(ScrollController controller) {
320
      return Directionality(
321
        textDirection: TextDirection.ltr,
322
        child: PageStorage(
323
          bucket: bucket,
324
          child: KeyedSubtree(
325
            key: const PageStorageKey<String>('ListView'),
326 327
            child: ListView(
              key: UniqueKey(), // it's a different ListView every time
328
              controller: controller,
329
              children: List<Widget>.generate(50, (int index) {
330
                return SizedBox(height: 100.0, child: Text('Item $index'));
331 332
              }).toList(),
            ),
333
          ),
334 335 336 337 338 339 340 341 342
        ),
      );
    }

    // keepScrollOffset: true (the default). The scroll offset is restored
    // when the ListView is recreated with a new ScrollController.

    // The initialScrollOffset is used in this case, because there's no saved
    // scroll offset.
343
    ScrollController controller = ScrollController(initialScrollOffset: 200.0);
344
    await tester.pumpWidget(buildFrame(controller));
345
    expect(tester.getTopLeft(find.widgetWithText(SizedBox, 'Item 2')), Offset.zero);
346 347 348

    controller.jumpTo(2000.0);
    await tester.pump();
349
    expect(tester.getTopLeft(find.widgetWithText(SizedBox, 'Item 20')), Offset.zero);
350 351 352

    // The initialScrollOffset isn't used in this case, because the scrolloffset
    // can be restored.
353
    controller = ScrollController(initialScrollOffset: 25.0);
354 355
    await tester.pumpWidget(buildFrame(controller));
    expect(controller.offset, 2000.0);
356
    expect(tester.getTopLeft(find.widgetWithText(SizedBox, 'Item 20')), Offset.zero);
357 358 359 360 361

    // keepScrollOffset: false. The scroll offset is -not- restored
    // when the ListView is recreated with a new ScrollController and
    // the initialScrollOffset is used.

362
    controller = ScrollController(keepScrollOffset: false, initialScrollOffset: 100.0);
363 364
    await tester.pumpWidget(buildFrame(controller));
    expect(controller.offset, 100.0);
365
    expect(tester.getTopLeft(find.widgetWithText(SizedBox, 'Item 1')), Offset.zero);
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

  testWidgets('isScrollingNotifier works with pointer scroll', (WidgetTester tester) async {
    Widget buildFrame(ScrollController controller) {
      return Directionality(
        textDirection: TextDirection.ltr,
        child: ListView(
          controller: controller,
          children: List<Widget>.generate(50, (int index) {
            return SizedBox(height: 100.0, child: Text('Item $index'));
          }).toList(),
        ),
      );
    }

    bool isScrolling = false;
    final ScrollController controller = ScrollController();
    controller.addListener((){
      isScrolling = controller.position.isScrollingNotifier.value;
    });
    await tester.pumpWidget(buildFrame(controller));
    final Offset scrollEventLocation = tester.getCenter(find.byType(ListView));
    final TestPointer testPointer = TestPointer(1, ui.PointerDeviceKind.mouse);
    // Create a hover event so that |testPointer| has a location when generating the scroll.
    testPointer.hover(scrollEventLocation);
    await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 300.0)));
    // When the listener was notified, the value of the isScrollingNotifier
    // should have been true
    expect(isScrolling, isTrue);
  });
397 398

  test('$ScrollController dispatches object creation in constructor', () {
399
    expect(()=> ScrollController().dispose(), dispatchesMemoryEvents(ScrollController));
400
  });
401
}