scrollable_test.dart 10.8 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13
// 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.

import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

Future<Null> pumpTest(WidgetTester tester, TargetPlatform platform) async {
  await tester.pumpWidget(new MaterialApp(
    theme: new ThemeData(
      platform: platform,
    ),
14
    home: new CustomScrollView(
15
      slivers: const <Widget>[
16
        const SliverToBoxAdapter(child: const SizedBox(height: 2000.0)),
17 18 19 20 21 22 23 24 25 26
      ],
    ),
  ));
  await tester.pump(const Duration(seconds: 5)); // to let the theme animate
  return null;
}

const double dragOffset = 200.0;

double getScrollOffset(WidgetTester tester) {
27
  final RenderViewport viewport = tester.renderObject(find.byType(Viewport));
28 29 30
  return viewport.offset.pixels;
}

31 32 33 34 35 36 37
double getScrollVelocity(WidgetTester tester) {
  final RenderViewport viewport = tester.renderObject(find.byType(Viewport));
  final ScrollPosition position = viewport.offset;
  // Access for test only.
  return position.activity.velocity; // ignore: INVALID_USE_OF_PROTECTED_MEMBER
}

38
void resetScrollOffset(WidgetTester tester) {
39 40
  final RenderViewport viewport = tester.renderObject(find.byType(Viewport));
  final ScrollPosition position = viewport.offset;
41 42 43 44 45 46
  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
47
    await tester.fling(find.byType(Viewport), const Offset(0.0, -dragOffset), 1000.0);
48 49 50 51 52 53 54 55 56
    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
57
    await tester.fling(find.byType(Viewport), const Offset(0.0, -dragOffset), 1000.0);
58 59
    // Scroll starts ease into the scroll on iOS.
    expect(getScrollOffset(tester), moreOrLessEquals(197.16666666666669));
60
    await tester.pump(); // trigger fling
61
    expect(getScrollOffset(tester), moreOrLessEquals(197.16666666666669));
62 63 64 65 66 67
    await tester.pump(const Duration(seconds: 5));
    final double result2 = getScrollOffset(tester);

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

68 69 70 71 72 73 74 75
  testWidgets('Holding scroll', (WidgetTester tester) async {
    await pumpTest(tester, TargetPlatform.iOS);
    await tester.drag(find.byType(Viewport), const Offset(0.0, 200.0));
    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));
76 77
    final double heldPosition = getScrollOffset(tester);
    // Hold and let go while in overscroll.
78 79
    final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(Viewport)));
    expect(await tester.pumpAndSettle(), 1);
80
    expect(getScrollOffset(tester), heldPosition);
81
    await gesture.up();
82
    // Once the hold is let go, it should still snap back to origin.
83 84 85 86 87
    expect(await tester.pumpAndSettle(const Duration(minutes: 1)), 2);
    expect(getScrollOffset(tester), 0.0);
  });

  testWidgets('Repeated flings builds momentum on iOS', (WidgetTester tester) async {
88
    await pumpTest(tester, TargetPlatform.iOS);
Adam Barth's avatar
Adam Barth committed
89
    await tester.fling(find.byType(Viewport), const Offset(0.0, -dragOffset), 1000.0);
90
    await tester.pump(); // trigger fling
91 92 93 94 95 96 97
    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));
98 99 100 101

    resetScrollOffset(tester);

    await pumpTest(tester, TargetPlatform.android);
Adam Barth's avatar
Adam Barth committed
102
    await tester.fling(find.byType(Viewport), const Offset(0.0, -dragOffset), 1000.0);
103
    await tester.pump(); // trigger fling
104 105 106 107 108 109 110 111
    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));
  });
112

113 114 115 116 117 118 119 120 121 122 123 124
  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));
125
  });
126

127
  testWidgets('No iOS momentum kept on hold gestures', (WidgetTester tester) async {
128
    await pumpTest(tester, TargetPlatform.iOS);
129 130
    await tester.fling(find.byType(Viewport), const Offset(0.0, -dragOffset), 1000.0);
    await tester.pump(); // trigger fling
131
    await tester.pump(const Duration(milliseconds: 10));
132
    expect(getScrollVelocity(tester), greaterThan(0.0));
133
    final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(Viewport)));
134
    await tester.pump(const Duration(milliseconds: 40));
135
    await gesture.up();
136 137
    // After a hold longer than 2 frames, previous velocity is lost.
    expect(getScrollVelocity(tester), 0.0);
138
  });
139 140 141 142 143 144 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

  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)));
174
    await gesture.moveBy(const Offset(0.0, -30.0));
175
    // No offset lost from threshold.
176 177 178 179 180 181 182 183 184 185 186 187
    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));
188 189 190 191 192
  });

  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)));
193 194
    await gesture.moveBy(const Offset(0.0, -30.0)); // Break threshold.
    expect(getScrollOffset(tester), 30.0);
195
    await gesture.moveBy(const Offset(0.0, -0.5), timeStamp: const Duration(milliseconds: 20));
196
    expect(getScrollOffset(tester), 30.5);
197
    await gesture.moveBy(const Offset(0.0, -0.5), timeStamp: const Duration(milliseconds: 40));
198
    expect(getScrollOffset(tester), 31.0);
199
    await gesture.moveBy(const Offset(0.0, -0.5), timeStamp: const Duration(milliseconds: 60));
200
    expect(getScrollOffset(tester), 31.5);
201 202 203 204 205
  });

  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)));
206 207
    await gesture.moveBy(const Offset(0.0, -30.0)); // Break threshold.
    expect(getScrollOffset(tester), 30.0);
208
    await gesture.moveBy(const Offset(0.0, -0.5), timeStamp: const Duration(milliseconds: 20));
209
    expect(getScrollOffset(tester), 30.5);
210 211 212 213
    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));
214
    expect(getScrollOffset(tester), 30.5);
215
    await gesture.moveBy(const Offset(0.0, -1.0), timeStamp: const Duration(milliseconds: 150));
216
    expect(getScrollOffset(tester), 30.5);
217
    await gesture.moveBy(const Offset(0.0, -1.0), timeStamp: const Duration(milliseconds: 160));
218
    expect(getScrollOffset(tester), 30.5);
219 220
    await gesture.moveBy(const Offset(0.0, -1.0), timeStamp: const Duration(milliseconds: 170));
    // New threshold broken.
221
    expect(getScrollOffset(tester), 31.5);
222
    await gesture.moveBy(const Offset(0.0, -1.0), timeStamp: const Duration(milliseconds: 180));
223
    expect(getScrollOffset(tester), 32.5);
224
  });
225
}