independent_widget_layout_test.dart 8.45 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' show FlutterView;

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

11
const Size _kTestViewSize = Size(800.0, 600.0);
12

13 14
class ScheduledFrameTrackingPlatformDispatcher extends TestPlatformDispatcher {
  ScheduledFrameTrackingPlatformDispatcher({ required super.platformDispatcher });
15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30

  int _scheduledFrameCount = 0;
  int get scheduledFrameCount => _scheduledFrameCount;

  void resetScheduledFrameCount() {
    _scheduledFrameCount = 0;
  }

  @override
  void scheduleFrame() {
    _scheduledFrameCount++;
    super.scheduleFrame();
  }
}

class ScheduledFrameTrackingBindings extends AutomatedTestWidgetsFlutterBinding {
31
  late final ScheduledFrameTrackingPlatformDispatcher _platformDispatcher = ScheduledFrameTrackingPlatformDispatcher(platformDispatcher: super.platformDispatcher);
32 33

  @override
34
  ScheduledFrameTrackingPlatformDispatcher get platformDispatcher => _platformDispatcher;
35 36
}

37
class OffscreenRenderView extends RenderView {
38
  OffscreenRenderView({required super.view}) : super(
39 40
    configuration: const ViewConfiguration(size: _kTestViewSize),
  );
41 42 43 44 45 46 47 48

  @override
  void compositeFrame() {
    // Don't draw to ui.window
  }
}

class OffscreenWidgetTree {
49
  OffscreenWidgetTree(this.view) {
50
    renderView.attach(pipelineOwner);
51 52
    renderView.prepareInitialFrame();
    pipelineOwner.requestVisualUpdate();
53 54
  }

55 56
  final FlutterView view;
  late final RenderView renderView = OffscreenRenderView(view: view);
57
  final BuildOwner buildOwner = BuildOwner(focusManager: FocusManager());
58
  final PipelineOwner pipelineOwner = PipelineOwner();
59
  RenderObjectToWidgetElement<RenderBox>? root;
60

61
  void pumpWidget(Widget? app) {
62
    root = RenderObjectToWidgetAdapter<RenderBox>(
63 64
      container: renderView,
      debugShortDescription: '[root]',
65
      child: app,
66 67 68 69 70
    ).attachToRenderTree(buildOwner, root);
    pumpFrame();
  }

  void pumpFrame() {
71
    buildOwner.buildScope(root!);
72 73 74 75
    pipelineOwner.flushLayout();
    pipelineOwner.flushCompositingBits();
    pipelineOwner.flushPaint();
    renderView.compositeFrame();
76
    pipelineOwner.flushSemantics();
77 78 79 80 81 82 83 84 85 86
    buildOwner.finalizeTree();
  }

}

class Counter {
  int count = 0;
}

class Trigger {
87
  VoidCallback? callback;
88
  void fire() {
89
    callback?.call();
90 91 92 93
  }
}

class TriggerableWidget extends StatefulWidget {
94
  const TriggerableWidget({
95
    super.key,
96 97
    required this.trigger,
    required this.counter,
98
  });
99

100 101
  final Trigger trigger;
  final Counter counter;
102

103
  @override
104
  TriggerableState createState() => TriggerableState();
105 106 107 108 109 110
}

class TriggerableState extends State<TriggerableWidget> {
  @override
  void initState() {
    super.initState();
111
    widget.trigger.callback = fire;
112 113 114
  }

  @override
115
  void didUpdateWidget(TriggerableWidget oldWidget) {
116
    super.didUpdateWidget(oldWidget);
117
    widget.trigger.callback = fire;
118 119 120 121 122 123 124 125 126 127 128
  }

  int _count = 0;
  void fire() {
    setState(() {
      _count++;
    });
  }

  @override
  Widget build(BuildContext context) {
129
    widget.counter.count++;
130
    return Text('Bang $_count!', textDirection: TextDirection.ltr);
131 132 133
  }
}

134 135
class TestFocusable extends StatefulWidget {
  const TestFocusable({
136
    super.key,
137
    required this.focusNode,
138
    this.autofocus = true,
139
  });
140 141 142 143 144

  final bool autofocus;
  final FocusNode focusNode;

  @override
145
  TestFocusableState createState() => TestFocusableState();
146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165
}

class TestFocusableState extends State<TestFocusable> {
  bool _didAutofocus = false;

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    if (!_didAutofocus && widget.autofocus) {
      _didAutofocus = true;
      FocusScope.of(context).autofocus(widget.focusNode);
    }
  }

  @override
  Widget build(BuildContext context) {
    return const Text('Test focus node', textDirection: TextDirection.ltr);
  }
}

166
void main() {
167 168 169 170 171 172
  // Override the bindings for this test suite so that we can track the number
  // of times a frame has been scheduled.
  ScheduledFrameTrackingBindings();

  testWidgets('RenderObjectToWidgetAdapter.attachToRenderTree does not schedule frame', (WidgetTester tester) async {
    expect(WidgetsBinding.instance, isA<ScheduledFrameTrackingBindings>());
173 174 175 176
    final ScheduledFrameTrackingPlatformDispatcher platformDispatcher = tester.platformDispatcher as ScheduledFrameTrackingPlatformDispatcher;
    platformDispatcher.resetScheduledFrameCount();
    expect(platformDispatcher.scheduledFrameCount, isZero);
    final OffscreenWidgetTree tree = OffscreenWidgetTree(tester.view);
177
    tree.pumpWidget(const SizedBox.shrink());
178
    expect(platformDispatcher.scheduledFrameCount, isZero);
179 180
  });

181
  testWidgets('no crosstalk between widget build owners', (WidgetTester tester) async {
182 183 184 185
    final Trigger trigger1 = Trigger();
    final Counter counter1 = Counter();
    final Trigger trigger2 = Trigger();
    final Counter counter2 = Counter();
186
    final OffscreenWidgetTree tree = OffscreenWidgetTree(tester.view);
187 188 189 190
    // Both counts should start at zero
    expect(counter1.count, equals(0));
    expect(counter2.count, equals(0));
    // Lay out the "onscreen" in the default test binding
191
    await tester.pumpWidget(TriggerableWidget(trigger: trigger1, counter: counter1));
192 193 194 195
    // Only the "onscreen" widget should have built
    expect(counter1.count, equals(1));
    expect(counter2.count, equals(0));
    // Lay out the "offscreen" in a separate tree
196
    tree.pumpWidget(TriggerableWidget(trigger: trigger2, counter: counter2));
197 198 199 200 201 202 203 204 205 206
    // Now both widgets should have built
    expect(counter1.count, equals(1));
    expect(counter2.count, equals(1));
    // Mark both as needing layout
    trigger1.fire();
    trigger2.fire();
    // Marking as needing layout shouldn't immediately build anything
    expect(counter1.count, equals(1));
    expect(counter2.count, equals(1));
    // Pump the "onscreen" layout
207
    await tester.pump();
208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224
    // Only the "onscreen" widget should have rebuilt
    expect(counter1.count, equals(2));
    expect(counter2.count, equals(1));
    // Pump the "offscreen" layout
    tree.pumpFrame();
    // Now both widgets should have rebuilt
    expect(counter1.count, equals(2));
    expect(counter2.count, equals(2));
    // Mark both as needing layout, again
    trigger1.fire();
    trigger2.fire();
    // Now pump the "offscreen" layout first
    tree.pumpFrame();
    // Only the "offscreen" widget should have rebuilt
    expect(counter1.count, equals(2));
    expect(counter2.count, equals(3));
    // Pump the "onscreen" layout
225
    await tester.pump();
226 227 228
    // Now both widgets should have rebuilt
    expect(counter1.count, equals(3));
    expect(counter2.count, equals(3));
229
  });
230 231

  testWidgets('no crosstalk between focus nodes', (WidgetTester tester) async {
232
    final OffscreenWidgetTree tree = OffscreenWidgetTree(tester.view);
233 234
    final FocusNode onscreenFocus = FocusNode();
    final FocusNode offscreenFocus = FocusNode();
235
    await tester.pumpWidget(
236
      TestFocusable(
237 238 239 240
        focusNode: onscreenFocus,
      ),
    );
    tree.pumpWidget(
241
      TestFocusable(
242 243 244 245 246 247 248 249 250 251 252 253
        focusNode: offscreenFocus,
      ),
    );

    // Autofocus is delayed one frame.
    await tester.pump();
    tree.pumpFrame();

    expect(onscreenFocus.hasFocus, isTrue);
    expect(offscreenFocus.hasFocus, isTrue);
  });

254
  testWidgets('able to tear down offscreen tree', (WidgetTester tester) async {
255
    final OffscreenWidgetTree tree = OffscreenWidgetTree(tester.view);
256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271
    final List<WidgetState> states = <WidgetState>[];
    tree.pumpWidget(SizedBox(child: TestStates(states: states)));
    expect(states, <WidgetState>[WidgetState.initialized]);
    expect(tree.renderView.child, isNotNull);
    tree.pumpWidget(null); // The root node should be allowed to have no child.
    expect(states, <WidgetState>[WidgetState.initialized, WidgetState.disposed]);
    expect(tree.renderView.child, isNull);
  });
}

enum WidgetState {
  initialized,
  disposed,
}

class TestStates extends StatefulWidget {
272
  const TestStates({super.key, required this.states});
273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294

  final List<WidgetState> states;

  @override
  TestStatesState createState() => TestStatesState();
}

class TestStatesState extends State<TestStates> {
  @override
  void initState() {
    super.initState();
    widget.states.add(WidgetState.initialized);
  }

  @override
  void dispose() {
    widget.states.add(WidgetState.disposed);
    super.dispose();
  }

  @override
  Widget build(BuildContext context) => Container();
295
}