scrollable_test.dart 13.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' as ui;

7 8 9 10
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

11 12 13 14 15 16
Future<void> pumpTest(
  WidgetTester tester,
  TargetPlatform platform, {
  bool scrollable = true,
  bool reverse = false,
}) async {
17 18
  await tester.pumpWidget(MaterialApp(
    theme: ThemeData(
19 20
      platform: platform,
    ),
21
    home: CustomScrollView(
22
      reverse: reverse,
23 24
      physics: scrollable ? null : const NeverScrollableScrollPhysics(),
      slivers: const <Widget>[
25
        SliverToBoxAdapter(child: SizedBox(height: 2000.0)),
26 27 28 29 30 31 32 33 34
      ],
    ),
  ));
  await tester.pump(const Duration(seconds: 5)); // to let the theme animate
}

const double dragOffset = 200.0;

double getScrollOffset(WidgetTester tester) {
35
  final RenderViewport viewport = tester.renderObject(find.byType(Viewport));
36 37 38
  return viewport.offset.pixels;
}

39 40 41
double getScrollVelocity(WidgetTester tester) {
  final RenderViewport viewport = tester.renderObject(find.byType(Viewport));
  final ScrollPosition position = viewport.offset;
42
  return position.activity.velocity;
43 44
}

45
void resetScrollOffset(WidgetTester tester) {
46 47
  final RenderViewport viewport = tester.renderObject(find.byType(Viewport));
  final ScrollPosition position = viewport.offset;
48 49 50 51 52 53
  position.jumpTo(0.0);
}

void main() {
  testWidgets('Flings on different platforms', (WidgetTester tester) async {
    await pumpTest(tester, TargetPlatform.android);
Adam Barth's avatar
Adam Barth committed
54
    await tester.fling(find.byType(Viewport), const Offset(0.0, -dragOffset), 1000.0);
55 56 57 58 59 60 61 62 63
    expect(getScrollOffset(tester), dragOffset);
    await tester.pump(); // trigger fling
    expect(getScrollOffset(tester), dragOffset);
    await tester.pump(const Duration(seconds: 5));
    final double result1 = getScrollOffset(tester);

    resetScrollOffset(tester);

    await pumpTest(tester, TargetPlatform.iOS);
Adam Barth's avatar
Adam Barth committed
64
    await tester.fling(find.byType(Viewport), const Offset(0.0, -dragOffset), 1000.0);
65 66
    // Scroll starts ease into the scroll on iOS.
    expect(getScrollOffset(tester), moreOrLessEquals(197.16666666666669));
67
    await tester.pump(); // trigger fling
68
    expect(getScrollOffset(tester), moreOrLessEquals(197.16666666666669));
69 70 71 72 73 74
    await tester.pump(const Duration(seconds: 5));
    final double result2 = getScrollOffset(tester);

    expect(result1, lessThan(result2)); // iOS (result2) is slipperier than Android (result1)
  });

75 76
  testWidgets('Holding scroll', (WidgetTester tester) async {
    await pumpTest(tester, TargetPlatform.iOS);
77
    await tester.drag(find.byType(Viewport), const Offset(0.0, 200.0), touchSlopY: 0.0);
78 79 80 81 82
    expect(getScrollOffset(tester), -200.0);
    await tester.pump(); // trigger ballistic
    await tester.pump(const Duration(milliseconds: 10));
    expect(getScrollOffset(tester), greaterThan(-200.0));
    expect(getScrollOffset(tester), lessThan(0.0));
83 84
    final double heldPosition = getScrollOffset(tester);
    // Hold and let go while in overscroll.
85 86
    final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(Viewport)));
    expect(await tester.pumpAndSettle(), 1);
87
    expect(getScrollOffset(tester), heldPosition);
88
    await gesture.up();
89
    // Once the hold is let go, it should still snap back to origin.
90 91 92 93 94
    expect(await tester.pumpAndSettle(const Duration(minutes: 1)), 2);
    expect(getScrollOffset(tester), 0.0);
  });

  testWidgets('Repeated flings builds momentum on iOS', (WidgetTester tester) async {
95
    await pumpTest(tester, TargetPlatform.iOS);
Adam Barth's avatar
Adam Barth committed
96
    await tester.fling(find.byType(Viewport), const Offset(0.0, -dragOffset), 1000.0);
97
    await tester.pump(); // trigger fling
98 99 100 101 102 103 104
    await tester.pump(const Duration(milliseconds: 10));
    // Repeat the exact same motion.
    await tester.fling(find.byType(Viewport), const Offset(0.0, -dragOffset), 1000.0);
    await tester.pump();
    // On iOS, the velocity will be larger than the velocity of the last fling by a
    // non-trivial amount.
    expect(getScrollVelocity(tester), greaterThan(1100.0));
105 106 107 108

    resetScrollOffset(tester);

    await pumpTest(tester, TargetPlatform.android);
Adam Barth's avatar
Adam Barth committed
109
    await tester.fling(find.byType(Viewport), const Offset(0.0, -dragOffset), 1000.0);
110
    await tester.pump(); // trigger fling
111 112 113 114 115 116 117 118
    await tester.pump(const Duration(milliseconds: 10));
    // Repeat the exact same motion.
    await tester.fling(find.byType(Viewport), const Offset(0.0, -dragOffset), 1000.0);
    await tester.pump();
    // On Android, there is no momentum build. The final velocity is the same as the
    // velocity of the last fling.
    expect(getScrollVelocity(tester), moreOrLessEquals(1000.0));
  });
119

120 121 122 123 124 125 126 127 128 129 130 131
  testWidgets('No iOS momentum build with flings in opposite directions', (WidgetTester tester) async {
    await pumpTest(tester, TargetPlatform.iOS);
    await tester.fling(find.byType(Viewport), const Offset(0.0, -dragOffset), 1000.0);
    await tester.pump(); // trigger fling
    await tester.pump(const Duration(milliseconds: 10));
    // Repeat the exact same motion in the opposite direction.
    await tester.fling(find.byType(Viewport), const Offset(0.0, dragOffset), 1000.0);
    await tester.pump();
    // The only applied velocity to the scrollable is the second fling that was in the
    // opposite direction.
    expect(getScrollVelocity(tester), greaterThan(-1000.0));
    expect(getScrollVelocity(tester), lessThan(0.0));
132
  });
133

134
  testWidgets('No iOS momentum kept on hold gestures', (WidgetTester tester) async {
135
    await pumpTest(tester, TargetPlatform.iOS);
136 137
    await tester.fling(find.byType(Viewport), const Offset(0.0, -dragOffset), 1000.0);
    await tester.pump(); // trigger fling
138
    await tester.pump(const Duration(milliseconds: 10));
139
    expect(getScrollVelocity(tester), greaterThan(0.0));
140
    final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(Viewport)));
141
    await tester.pump(const Duration(milliseconds: 40));
142
    await gesture.up();
143 144
    // After a hold longer than 2 frames, previous velocity is lost.
    expect(getScrollVelocity(tester), 0.0);
145
  });
146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180

  testWidgets('Drags creeping unaffected on Android', (WidgetTester tester) async {
    await pumpTest(tester, TargetPlatform.android);
    final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(Viewport)));
    await gesture.moveBy(const Offset(0.0, -0.5));
    expect(getScrollOffset(tester), 0.5);
    await gesture.moveBy(const Offset(0.0, -0.5), timeStamp: const Duration(milliseconds: 10));
    expect(getScrollOffset(tester), 1.0);
    await gesture.moveBy(const Offset(0.0, -0.5), timeStamp: const Duration(milliseconds: 20));
    expect(getScrollOffset(tester), 1.5);
  });

  testWidgets('Drags creeping must break threshold on iOS', (WidgetTester tester) async {
    await pumpTest(tester, TargetPlatform.iOS);
    final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(Viewport)));
    await gesture.moveBy(const Offset(0.0, -0.5));
    expect(getScrollOffset(tester), 0.0);
    await gesture.moveBy(const Offset(0.0, -0.5), timeStamp: const Duration(milliseconds: 10));
    expect(getScrollOffset(tester), 0.0);
    await gesture.moveBy(const Offset(0.0, -0.5), timeStamp: const Duration(milliseconds: 20));
    expect(getScrollOffset(tester), 0.0);
    await gesture.moveBy(const Offset(0.0, -1.0), timeStamp: const Duration(milliseconds: 30));
    // Now -2.5 in total.
    expect(getScrollOffset(tester), 0.0);
    await gesture.moveBy(const Offset(0.0, -1.0), timeStamp: const Duration(milliseconds: 40));
    // Now -3.5, just reached threshold.
    expect(getScrollOffset(tester), 0.0);
    await gesture.moveBy(const Offset(0.0, -0.5), timeStamp: const Duration(milliseconds: 50));
    // -0.5 over threshold transferred.
    expect(getScrollOffset(tester), 0.5);
  });

  testWidgets('Big drag over threshold magnitude preserved on iOS', (WidgetTester tester) async {
    await pumpTest(tester, TargetPlatform.iOS);
    final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(Viewport)));
181
    await gesture.moveBy(const Offset(0.0, -30.0));
182
    // No offset lost from threshold.
183 184 185 186 187 188 189 190 191 192 193 194
    expect(getScrollOffset(tester), 30.0);
  });

  testWidgets('Slow threshold breaks are attenuated on iOS', (WidgetTester tester) async {
    await pumpTest(tester, TargetPlatform.iOS);
    final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(Viewport)));
    // This is a typical 'hesitant' iOS scroll start.
    await gesture.moveBy(const Offset(0.0, -10.0));
    expect(getScrollOffset(tester), moreOrLessEquals(1.1666666666666667));
    await gesture.moveBy(const Offset(0.0, -10.0), timeStamp: const Duration(milliseconds: 20));
    // Subsequent motions unaffected.
    expect(getScrollOffset(tester), moreOrLessEquals(11.16666666666666673));
195 196 197 198 199
  });

  testWidgets('Small continuing motion preserved on iOS', (WidgetTester tester) async {
    await pumpTest(tester, TargetPlatform.iOS);
    final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(Viewport)));
200 201
    await gesture.moveBy(const Offset(0.0, -30.0)); // Break threshold.
    expect(getScrollOffset(tester), 30.0);
202
    await gesture.moveBy(const Offset(0.0, -0.5), timeStamp: const Duration(milliseconds: 20));
203
    expect(getScrollOffset(tester), 30.5);
204
    await gesture.moveBy(const Offset(0.0, -0.5), timeStamp: const Duration(milliseconds: 40));
205
    expect(getScrollOffset(tester), 31.0);
206
    await gesture.moveBy(const Offset(0.0, -0.5), timeStamp: const Duration(milliseconds: 60));
207
    expect(getScrollOffset(tester), 31.5);
208 209 210 211 212
  });

  testWidgets('Motion stop resets threshold on iOS', (WidgetTester tester) async {
    await pumpTest(tester, TargetPlatform.iOS);
    final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(Viewport)));
213 214
    await gesture.moveBy(const Offset(0.0, -30.0)); // Break threshold.
    expect(getScrollOffset(tester), 30.0);
215
    await gesture.moveBy(const Offset(0.0, -0.5), timeStamp: const Duration(milliseconds: 20));
216
    expect(getScrollOffset(tester), 30.5);
217 218 219 220
    await gesture.moveBy(Offset.zero);
    // Stationary too long, threshold reset.
    await gesture.moveBy(Offset.zero, timeStamp: const Duration(milliseconds: 120));
    await gesture.moveBy(const Offset(0.0, -1.0), timeStamp: const Duration(milliseconds: 140));
221
    expect(getScrollOffset(tester), 30.5);
222
    await gesture.moveBy(const Offset(0.0, -1.0), timeStamp: const Duration(milliseconds: 150));
223
    expect(getScrollOffset(tester), 30.5);
224
    await gesture.moveBy(const Offset(0.0, -1.0), timeStamp: const Duration(milliseconds: 160));
225
    expect(getScrollOffset(tester), 30.5);
226 227
    await gesture.moveBy(const Offset(0.0, -1.0), timeStamp: const Duration(milliseconds: 170));
    // New threshold broken.
228
    expect(getScrollOffset(tester), 31.5);
229
    await gesture.moveBy(const Offset(0.0, -1.0), timeStamp: const Duration(milliseconds: 180));
230
    expect(getScrollOffset(tester), 32.5);
231
  });
232 233 234 235 236 237 238 239 240 241 242 243 244 245

  testWidgets('Scroll pointer signals are handled', (WidgetTester tester) async {
    await pumpTest(tester, TargetPlatform.fuchsia);
    final Offset scrollEventLocation = tester.getCenter(find.byType(Viewport));
    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);
    final HitTestResult result = tester.hitTestOnBinding(scrollEventLocation);
    await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 20.0)), result);
    expect(getScrollOffset(tester), 20.0);
    // Pointer signals should not cause overscroll.
    await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, -30.0)), result);
    expect(getScrollOffset(tester), 0.0);
  });
246 247 248 249 250 251 252 253 254 255 256

  testWidgets('Scroll pointer signals are ignored when scrolling is disabled', (WidgetTester tester) async {
    await pumpTest(tester, TargetPlatform.fuchsia, scrollable: false);
    final Offset scrollEventLocation = tester.getCenter(find.byType(Viewport));
    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);
    final HitTestResult result = tester.hitTestOnBinding(scrollEventLocation);
    await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 20.0)), result);
    expect(getScrollOffset(tester), 0.0);
  });
257 258 259 260 261 262 263 264 265 266 267 268 269

  testWidgets('Scrolls in correct direction when scroll axis is reversed', (WidgetTester tester) async {
    await pumpTest(tester, TargetPlatform.fuchsia, reverse: true);

    final Offset scrollEventLocation = tester.getCenter(find.byType(Viewport));
    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);
    final HitTestResult result = tester.hitTestOnBinding(scrollEventLocation);
    await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, -20.0)), result);

    expect(getScrollOffset(tester), 20.0);
  });
270
}