| // Copyright 2014 The Flutter 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 'dart:ui' as ui; |
| |
| import 'package:flutter/material.dart'; |
| import 'package:flutter/rendering.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| |
| const Size _kTestViewSize = Size(800.0, 600.0); |
| |
| class ScheduledFrameTrackingWindow extends TestWindow { |
| ScheduledFrameTrackingWindow() : super(window: ui.window); |
| |
| int _scheduledFrameCount = 0; |
| int get scheduledFrameCount => _scheduledFrameCount; |
| |
| void resetScheduledFrameCount() { |
| _scheduledFrameCount = 0; |
| } |
| |
| @override |
| void scheduleFrame() { |
| _scheduledFrameCount++; |
| super.scheduleFrame(); |
| } |
| } |
| |
| class ScheduledFrameTrackingBindings extends AutomatedTestWidgetsFlutterBinding { |
| final ScheduledFrameTrackingWindow _window = ScheduledFrameTrackingWindow(); |
| |
| @override |
| ScheduledFrameTrackingWindow get window => _window; |
| } |
| |
| class OffscreenRenderView extends RenderView { |
| OffscreenRenderView() : super( |
| configuration: const ViewConfiguration(size: _kTestViewSize), |
| window: WidgetsBinding.instance!.window, |
| ); |
| |
| @override |
| void compositeFrame() { |
| // Don't draw to ui.window |
| } |
| } |
| |
| class OffscreenWidgetTree { |
| OffscreenWidgetTree() { |
| renderView.attach(pipelineOwner); |
| renderView.prepareInitialFrame(); |
| pipelineOwner.requestVisualUpdate(); |
| } |
| |
| final RenderView renderView = OffscreenRenderView(); |
| final BuildOwner buildOwner = BuildOwner(focusManager: FocusManager()); |
| final PipelineOwner pipelineOwner = PipelineOwner(); |
| RenderObjectToWidgetElement<RenderBox>? root; |
| |
| void pumpWidget(Widget? app) { |
| root = RenderObjectToWidgetAdapter<RenderBox>( |
| container: renderView, |
| debugShortDescription: '[root]', |
| child: app, |
| ).attachToRenderTree(buildOwner, root); |
| pumpFrame(); |
| } |
| |
| void pumpFrame() { |
| buildOwner.buildScope(root!); |
| pipelineOwner.flushLayout(); |
| pipelineOwner.flushCompositingBits(); |
| pipelineOwner.flushPaint(); |
| renderView.compositeFrame(); |
| pipelineOwner.flushSemantics(); |
| buildOwner.finalizeTree(); |
| } |
| |
| } |
| |
| class Counter { |
| int count = 0; |
| } |
| |
| class Trigger { |
| VoidCallback? callback; |
| void fire() { |
| callback?.call(); |
| } |
| } |
| |
| class TriggerableWidget extends StatefulWidget { |
| const TriggerableWidget({ |
| Key? key, |
| required this.trigger, |
| required this.counter, |
| }) : super(key: key); |
| |
| final Trigger trigger; |
| final Counter counter; |
| |
| @override |
| TriggerableState createState() => TriggerableState(); |
| } |
| |
| class TriggerableState extends State<TriggerableWidget> { |
| @override |
| void initState() { |
| super.initState(); |
| widget.trigger.callback = fire; |
| } |
| |
| @override |
| void didUpdateWidget(TriggerableWidget oldWidget) { |
| super.didUpdateWidget(oldWidget); |
| widget.trigger.callback = fire; |
| } |
| |
| int _count = 0; |
| void fire() { |
| setState(() { |
| _count++; |
| }); |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| widget.counter.count++; |
| return Text('Bang $_count!', textDirection: TextDirection.ltr); |
| } |
| } |
| |
| class TestFocusable extends StatefulWidget { |
| const TestFocusable({ |
| Key? key, |
| required this.focusNode, |
| this.autofocus = true, |
| }) : super(key: key); |
| |
| final bool autofocus; |
| final FocusNode focusNode; |
| |
| @override |
| TestFocusableState createState() => TestFocusableState(); |
| } |
| |
| 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); |
| } |
| } |
| |
| void main() { |
| // 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>()); |
| final ScheduledFrameTrackingWindow window = WidgetsBinding.instance!.window as ScheduledFrameTrackingWindow; |
| window.resetScheduledFrameCount(); |
| expect(window.scheduledFrameCount, isZero); |
| final OffscreenWidgetTree tree = OffscreenWidgetTree(); |
| tree.pumpWidget(const SizedBox.shrink()); |
| expect(window.scheduledFrameCount, isZero); |
| }); |
| |
| testWidgets('no crosstalk between widget build owners', (WidgetTester tester) async { |
| final Trigger trigger1 = Trigger(); |
| final Counter counter1 = Counter(); |
| final Trigger trigger2 = Trigger(); |
| final Counter counter2 = Counter(); |
| final OffscreenWidgetTree tree = OffscreenWidgetTree(); |
| // 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 |
| await tester.pumpWidget(TriggerableWidget(trigger: trigger1, counter: counter1)); |
| // 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 |
| tree.pumpWidget(TriggerableWidget(trigger: trigger2, counter: counter2)); |
| // 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 |
| await tester.pump(); |
| // 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 |
| await tester.pump(); |
| // Now both widgets should have rebuilt |
| expect(counter1.count, equals(3)); |
| expect(counter2.count, equals(3)); |
| }); |
| |
| testWidgets('no crosstalk between focus nodes', (WidgetTester tester) async { |
| final OffscreenWidgetTree tree = OffscreenWidgetTree(); |
| final FocusNode onscreenFocus = FocusNode(); |
| final FocusNode offscreenFocus = FocusNode(); |
| await tester.pumpWidget( |
| TestFocusable( |
| focusNode: onscreenFocus, |
| ), |
| ); |
| tree.pumpWidget( |
| TestFocusable( |
| focusNode: offscreenFocus, |
| ), |
| ); |
| |
| // Autofocus is delayed one frame. |
| await tester.pump(); |
| tree.pumpFrame(); |
| |
| expect(onscreenFocus.hasFocus, isTrue); |
| expect(offscreenFocus.hasFocus, isTrue); |
| }); |
| |
| testWidgets('able to tear down offscreen tree', (WidgetTester tester) async { |
| final OffscreenWidgetTree tree = OffscreenWidgetTree(); |
| 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 { |
| const TestStates({Key? key, required this.states}) : super(key: key); |
| |
| 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(); |
| } |