independent_widget_layout_test.dart 5.72 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6 7 8
// 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';

9
const Size _kTestViewSize = Size(800.0, 600.0);
10 11

class OffscreenRenderView extends RenderView {
12 13
  OffscreenRenderView() : super(
    configuration: const ViewConfiguration(size: _kTestViewSize),
14
    window: WidgetsBinding.instance!.window,
15
  );
16 17 18 19 20 21 22 23 24 25

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

class OffscreenWidgetTree {
  OffscreenWidgetTree() {
    renderView.attach(pipelineOwner);
26 27
    renderView.prepareInitialFrame();
    pipelineOwner.requestVisualUpdate();
28 29
  }

30 31 32
  final RenderView renderView = OffscreenRenderView();
  final BuildOwner buildOwner = BuildOwner();
  final PipelineOwner pipelineOwner = PipelineOwner();
33
  RenderObjectToWidgetElement<RenderBox>? root;
34 35

  void pumpWidget(Widget app) {
36
    root = RenderObjectToWidgetAdapter<RenderBox>(
37 38
      container: renderView,
      debugShortDescription: '[root]',
39
      child: app,
40 41 42 43 44
    ).attachToRenderTree(buildOwner, root);
    pumpFrame();
  }

  void pumpFrame() {
45
    buildOwner.buildScope(root!);
46 47 48 49
    pipelineOwner.flushLayout();
    pipelineOwner.flushCompositingBits();
    pipelineOwner.flushPaint();
    renderView.compositeFrame();
50
    pipelineOwner.flushSemantics();
51 52 53 54 55 56 57 58 59 60
    buildOwner.finalizeTree();
  }

}

class Counter {
  int count = 0;
}

class Trigger {
61
  VoidCallback? callback;
62 63
  void fire() {
    if (callback != null)
64
      callback!();
65 66 67 68
  }
}

class TriggerableWidget extends StatefulWidget {
69
  const TriggerableWidget({
70 71 72
    Key? key,
    required this.trigger,
    required this.counter,
73 74
  }) : super(key: key);

75 76
  final Trigger trigger;
  final Counter counter;
77

78
  @override
79
  TriggerableState createState() => TriggerableState();
80 81 82 83 84 85
}

class TriggerableState extends State<TriggerableWidget> {
  @override
  void initState() {
    super.initState();
86
    widget.trigger.callback = fire;
87 88 89
  }

  @override
90
  void didUpdateWidget(TriggerableWidget oldWidget) {
91
    super.didUpdateWidget(oldWidget);
92
    widget.trigger.callback = fire;
93 94 95 96 97 98 99 100 101 102 103
  }

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

  @override
  Widget build(BuildContext context) {
104
    widget.counter.count++;
105
    return Text('Bang $_count!', textDirection: TextDirection.ltr);
106 107 108
  }
}

109 110
class TestFocusable extends StatefulWidget {
  const TestFocusable({
111 112
    Key? key,
    required this.focusNode,
113
    this.autofocus = true,
114 115 116 117 118 119
  }) : super(key: key);

  final bool autofocus;
  final FocusNode focusNode;

  @override
120
  TestFocusableState createState() => TestFocusableState();
121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
}

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);
  }
}

141
void main() {
142
  testWidgets('no crosstalk between widget build owners', (WidgetTester tester) async {
143 144 145 146 147
    final Trigger trigger1 = Trigger();
    final Counter counter1 = Counter();
    final Trigger trigger2 = Trigger();
    final Counter counter2 = Counter();
    final OffscreenWidgetTree tree = OffscreenWidgetTree();
148 149 150 151
    // 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
152
    await tester.pumpWidget(TriggerableWidget(trigger: trigger1, counter: counter1));
153 154 155 156
    // 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
157
    tree.pumpWidget(TriggerableWidget(trigger: trigger2, counter: counter2));
158 159 160 161 162 163 164 165 166 167
    // 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
168
    await tester.pump();
169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185
    // 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
186
    await tester.pump();
187 188 189
    // Now both widgets should have rebuilt
    expect(counter1.count, equals(3));
    expect(counter2.count, equals(3));
190
  });
191 192

  testWidgets('no crosstalk between focus nodes', (WidgetTester tester) async {
193 194 195
    final OffscreenWidgetTree tree = OffscreenWidgetTree();
    final FocusNode onscreenFocus = FocusNode();
    final FocusNode offscreenFocus = FocusNode();
196
    await tester.pumpWidget(
197
      TestFocusable(
198 199 200 201
        focusNode: onscreenFocus,
      ),
    );
    tree.pumpWidget(
202
      TestFocusable(
203 204 205 206 207 208 209 210 211 212 213 214
        focusNode: offscreenFocus,
      ),
    );

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

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

215
}