independent_widget_layout_test.dart 5.56 KB
Newer Older
1 2 3 4 5 6 7 8
// Copyright 2016 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';

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

class OffscreenRenderView extends RenderView {
12
  OffscreenRenderView() : super(configuration: const ViewConfiguration(size: _kTestViewSize));
13 14 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);
    renderView.scheduleInitialFrame();
  }

26 27 28
  final RenderView renderView = OffscreenRenderView();
  final BuildOwner buildOwner = BuildOwner();
  final PipelineOwner pipelineOwner = PipelineOwner();
29 30 31
  RenderObjectToWidgetElement<RenderBox> root;

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

  void pumpFrame() {
41
    buildOwner.buildScope(root);
42 43 44 45
    pipelineOwner.flushLayout();
    pipelineOwner.flushCompositingBits();
    pipelineOwner.flushPaint();
    renderView.compositeFrame();
46
    pipelineOwner.flushSemantics();
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
    buildOwner.finalizeTree();
  }

}

class Counter {
  int count = 0;
}

class Trigger {
  VoidCallback callback;
  void fire() {
    if (callback != null)
      callback();
  }
}

class TriggerableWidget extends StatefulWidget {
65
  const TriggerableWidget({ this.trigger, this.counter });
66 67 68
  final Trigger trigger;
  final Counter counter;
  @override
69
  TriggerableState createState() => TriggerableState();
70 71 72 73 74 75
}

class TriggerableState extends State<TriggerableWidget> {
  @override
  void initState() {
    super.initState();
76
    widget.trigger.callback = fire;
77 78 79
  }

  @override
80
  void didUpdateWidget(TriggerableWidget oldWidget) {
81
    super.didUpdateWidget(oldWidget);
82
    widget.trigger.callback = fire;
83 84 85 86 87 88 89 90 91 92 93
  }

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

  @override
  Widget build(BuildContext context) {
94
    widget.counter.count++;
95
    return Text('Bang $_count!', textDirection: TextDirection.ltr);
96 97 98
  }
}

99 100 101 102
class TestFocusable extends StatefulWidget {
  const TestFocusable({
    Key key,
    this.focusNode,
103
    this.autofocus = true,
104 105 106 107 108 109
  }) : super(key: key);

  final bool autofocus;
  final FocusNode focusNode;

  @override
110
  TestFocusableState createState() => TestFocusableState();
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130
}

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

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

  testWidgets('no crosstalk between focus nodes', (WidgetTester tester) async {
183 184 185
    final OffscreenWidgetTree tree = OffscreenWidgetTree();
    final FocusNode onscreenFocus = FocusNode();
    final FocusNode offscreenFocus = FocusNode();
186
    await tester.pumpWidget(
187
      TestFocusable(
188 189 190 191
        focusNode: onscreenFocus,
      ),
    );
    tree.pumpWidget(
192
      TestFocusable(
193 194 195 196 197 198 199 200 201 202 203 204
        focusNode: offscreenFocus,
      ),
    );

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

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

205
}