scrollable_of_test.dart 7.84 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

class ScrollPositionListener extends StatefulWidget {
9
  const ScrollPositionListener({ super.key, required this.child, required this.log});
10 11 12 13 14

  final Widget child;
  final ValueChanged<String> log;

  @override
15
  State<ScrollPositionListener> createState() => _ScrollPositionListenerState();
16 17 18
}

class _ScrollPositionListenerState extends State<ScrollPositionListener> {
19
  ScrollPosition? _position;
20 21 22 23 24

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    _position?.removeListener(listener);
25
    _position = Scrollable.maybeOf(context)?.position;
26
    _position?.addListener(listener);
27
    widget.log('didChangeDependencies ${_position?.pixels.toStringAsFixed(1)}');
28 29 30 31 32 33 34 35 36 37 38 39
  }

  @override
  void dispose() {
    _position?.removeListener(listener);
    super.dispose();
  }

  @override
  Widget build(BuildContext context) => widget.child;

  void listener() {
40
    widget.log('listener ${_position?.pixels.toStringAsFixed(1)}');
41
  }
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
}

class TestScrollController extends ScrollController {
  TestScrollController({ required this.deferLoading });

  final bool deferLoading;

  @override
  ScrollPosition createScrollPosition(ScrollPhysics physics, ScrollContext context, ScrollPosition? oldPosition) {
    return TestScrollPosition(
      physics: physics,
      context: context,
      oldPosition: oldPosition,
      deferLoading: deferLoading,
    );
  }
}

class TestScrollPosition extends ScrollPositionWithSingleContext {
  TestScrollPosition({
    required super.physics,
    required super.context,
    super.oldPosition,
    required this.deferLoading,
  });

  final bool deferLoading;

  @override
  bool recommendDeferredLoading(BuildContext context) => deferLoading;
}

class TestScrollable extends StatefulWidget {
  const TestScrollable({ super.key, required this.child });

  final Widget child;
78

79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122
  @override
  State<StatefulWidget> createState() => TestScrollableState();
}

class TestScrollableState extends State<TestScrollable> {
  int dependenciesChanged = 0;

  @override
  void didChangeDependencies() {
    dependenciesChanged += 1;
    super.didChangeDependencies();
  }

  @override
  Widget build(BuildContext context) {
    return widget.child;
  }
}

class TestChild extends StatefulWidget {
  const TestChild({ super.key });

  @override
  State<TestChild> createState() => TestChildState();
}

class TestChildState extends State<TestChild> {
  int dependenciesChanged = 0;
  late ScrollableState scrollable;

  @override
  void didChangeDependencies() {
    dependenciesChanged += 1;
    scrollable = Scrollable.of(context, axis: Axis.horizontal);
    super.didChangeDependencies();
  }

  @override
  Widget build(BuildContext context) {
    return SizedBox.square(
      dimension: 1000,
      child: Text(scrollable.axisDirection.toString()),
    );
  }
123 124 125
}

void main() {
126
  testWidgets('Scrollable.of() dependent rebuilds when Scrollable position changes', (WidgetTester tester) async {
127
    late String logValue;
128
    final ScrollController controller = ScrollController();
129
    addTearDown(controller.dispose);
130 131 132 133

    // Changing the SingleChildScrollView's physics causes the
    // ScrollController's ScrollPosition to be rebuilt.

134
    Widget buildFrame(ScrollPhysics? physics) {
135
      return SingleChildScrollView(
136 137
        controller: controller,
        physics: physics,
138
        child: ScrollPositionListener(
139 140 141 142 143 144 145
          log: (String s) { logValue = s; },
          child: const SizedBox(height: 400.0),
        ),
      );
    }

    await tester.pumpWidget(buildFrame(null));
Ian Hickson's avatar
Ian Hickson committed
146
    expect(logValue, 'didChangeDependencies 0.0');
147 148

    controller.jumpTo(100.0);
Ian Hickson's avatar
Ian Hickson committed
149
    expect(logValue, 'listener 100.0');
150 151

    await tester.pumpWidget(buildFrame(const ClampingScrollPhysics()));
Ian Hickson's avatar
Ian Hickson committed
152
    expect(logValue, 'didChangeDependencies 100.0');
153 154

    controller.jumpTo(200.0);
Ian Hickson's avatar
Ian Hickson committed
155
    expect(logValue, 'listener 200.0');
156 157

    controller.jumpTo(300.0);
Ian Hickson's avatar
Ian Hickson committed
158
    expect(logValue, 'listener 300.0');
159 160

    await tester.pumpWidget(buildFrame(const BouncingScrollPhysics()));
Ian Hickson's avatar
Ian Hickson committed
161
    expect(logValue, 'didChangeDependencies 300.0');
162 163

    controller.jumpTo(400.0);
Ian Hickson's avatar
Ian Hickson committed
164
    expect(logValue, 'listener 400.0');
165
  });
166

167
  testWidgets('Scrollable.of() is possible using ScrollNotification context', (WidgetTester tester) async {
168
    late ScrollNotification notification;
169 170 171 172 173 174

    await tester.pumpWidget(NotificationListener<ScrollNotification>(
      onNotification: (ScrollNotification value) {
        notification = value;
        return false;
      },
175
      child: const SingleChildScrollView(
176 177
        child: SizedBox(height: 1200.0),
      ),
178 179
    ));

180
    final TestGesture gesture = await tester.startGesture(const Offset(100.0, 100.0));
181 182
    await tester.pump(const Duration(seconds: 1));

183
    final StatefulElement scrollableElement = find.byType(Scrollable).evaluate().first as StatefulElement;
184
    expect(Scrollable.of(notification.context!), equals(scrollableElement.state));
185 186 187 188

    // Finish gesture to release resources.
    await gesture.up();
    await tester.pumpAndSettle();
189
  });
190

191
  testWidgets('Static Scrollable methods can target a specific axis', (WidgetTester tester) async {
192
    final TestScrollController horizontalController = TestScrollController(deferLoading: true);
193
    addTearDown(horizontalController.dispose);
194
    final TestScrollController verticalController = TestScrollController(deferLoading: false);
195
    addTearDown(verticalController.dispose);
196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227
    late final AxisDirection foundAxisDirection;
    late final bool foundRecommendation;

    await tester.pumpWidget(Directionality(
      textDirection: TextDirection.ltr,
      child: SingleChildScrollView(
        scrollDirection: Axis.horizontal,
        controller: horizontalController,
        child: SingleChildScrollView(
          controller: verticalController,
          child: Builder(
            builder: (BuildContext context) {
              foundAxisDirection = Scrollable.of(
                context,
                axis: Axis.horizontal,
              ).axisDirection;
              foundRecommendation = Scrollable.recommendDeferredLoadingForContext(
                context,
                axis: Axis.horizontal,
              );
              return const SizedBox(height: 1200.0, width: 1200.0);
            }
          ),
        ),
      ),
    ));
    await tester.pumpAndSettle();

    expect(foundAxisDirection, AxisDirection.right);
    expect(foundRecommendation, isTrue);
  });

228
  testWidgets('Axis targeting scrollables establishes the correct dependencies', (WidgetTester tester) async {
229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246
    final GlobalKey<TestScrollableState> verticalKey = GlobalKey<TestScrollableState>();
    final GlobalKey<TestChildState> childKey = GlobalKey<TestChildState>();

    await tester.pumpWidget(Directionality(
      textDirection: TextDirection.ltr,
      child: SingleChildScrollView(
        scrollDirection: Axis.horizontal,
        child: TestScrollable(
          key: verticalKey,
          child: TestChild(key: childKey),
        ),
      ),
    ));
    await tester.pumpAndSettle();

    expect(verticalKey.currentState!.dependenciesChanged, 1);
    expect(childKey.currentState!.dependenciesChanged, 1);

247 248 249
    final ScrollController controller = ScrollController();
    addTearDown(controller.dispose);

250 251 252 253 254
    // Change the horizontal ScrollView, adding a controller
    await tester.pumpWidget(Directionality(
      textDirection: TextDirection.ltr,
      child: SingleChildScrollView(
        scrollDirection: Axis.horizontal,
255
        controller: controller,
256 257 258 259 260 261 262 263 264 265
        child: TestScrollable(
          key: verticalKey,
          child: TestChild(key: childKey),
        ),
      ),
    ));
    await tester.pumpAndSettle();
    expect(verticalKey.currentState!.dependenciesChanged, 1);
    expect(childKey.currentState!.dependenciesChanged, 2);
  });
266
}