// 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 'package:flutter_test/flutter_test.dart'; import 'package:flutter/widgets.dart'; import 'test_widgets.dart'; class ProbeWidget extends StatefulWidget { const ProbeWidget({ Key? key }) : super(key: key); @override ProbeWidgetState createState() => ProbeWidgetState(); } class ProbeWidgetState extends State<ProbeWidget> { static int buildCount = 0; @override void initState() { super.initState(); setState(() { }); } @override void didUpdateWidget(ProbeWidget oldWidget) { super.didUpdateWidget(oldWidget); setState(() { }); } @override Widget build(BuildContext context) { setState(() { }); buildCount++; return Container(); } } class BadWidget extends StatelessWidget { const BadWidget(this.parentState, { Key? key }) : super(key: key); final BadWidgetParentState parentState; @override Widget build(BuildContext context) { parentState._markNeedsBuild(); return Container(); } } class BadWidgetParent extends StatefulWidget { const BadWidgetParent({ Key? key }) : super(key: key); @override BadWidgetParentState createState() => BadWidgetParentState(); } class BadWidgetParentState extends State<BadWidgetParent> { void _markNeedsBuild() { setState(() { // Our state didn't really change, but we're doing something pathological // here to trigger an interesting scenario to test. }); } @override Widget build(BuildContext context) { return BadWidget(this); } } class BadDisposeWidget extends StatefulWidget { const BadDisposeWidget({ Key? key }) : super(key: key); @override BadDisposeWidgetState createState() => BadDisposeWidgetState(); } class BadDisposeWidgetState extends State<BadDisposeWidget> { @override Widget build(BuildContext context) { return Container(); } @override void dispose() { setState(() { /* This is invalid behavior. */ }); super.dispose(); } } class StatefulWrapper extends StatefulWidget { const StatefulWrapper({ Key? key, required this.child, }) : super(key: key); final Widget child; @override StatefulWrapperState createState() => StatefulWrapperState(); } class StatefulWrapperState extends State<StatefulWrapper> { void trigger() { setState(() { built = null; }); } int? built; late int oldBuilt; static int buildId = 0; @override Widget build(BuildContext context) { buildId += 1; built = buildId; return widget.child; } } class Wrapper extends StatelessWidget { const Wrapper({ Key? key, required this.child, }) : super(key: key); final Widget child; @override Widget build(BuildContext context) { return child; } } void main() { testWidgets('Legal times for setState', (WidgetTester tester) async { final GlobalKey flipKey = GlobalKey(); expect(ProbeWidgetState.buildCount, equals(0)); await tester.pumpWidget(const ProbeWidget(key: Key('a'))); expect(ProbeWidgetState.buildCount, equals(1)); await tester.pumpWidget(const ProbeWidget(key: Key('b'))); expect(ProbeWidgetState.buildCount, equals(2)); await tester.pumpWidget(FlipWidget( key: flipKey, left: Container(), right: const ProbeWidget(key: Key('c')), )); expect(ProbeWidgetState.buildCount, equals(2)); final FlipWidgetState flipState1 = flipKey.currentState! as FlipWidgetState; flipState1.flip(); await tester.pump(); expect(ProbeWidgetState.buildCount, equals(3)); final FlipWidgetState flipState2 = flipKey.currentState! as FlipWidgetState; flipState2.flip(); await tester.pump(); expect(ProbeWidgetState.buildCount, equals(3)); await tester.pumpWidget(Container()); expect(ProbeWidgetState.buildCount, equals(3)); }); testWidgets('Setting parent state during build is forbidden', (WidgetTester tester) async { await tester.pumpWidget(const BadWidgetParent()); expect(tester.takeException(), isFlutterError); await tester.pumpWidget(Container()); }); testWidgets('Setting state during dispose is forbidden', (WidgetTester tester) async { await tester.pumpWidget(const BadDisposeWidget()); expect(tester.takeException(), isNull); await tester.pumpWidget(Container()); expect(tester.takeException(), isNotNull); }); testWidgets('Dirty element list sort order', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(debugLabel: 'key1'); final GlobalKey key2 = GlobalKey(debugLabel: 'key2'); bool didMiddle = false; late Widget middle; final List<StateSetter> setStates = <StateSetter>[]; Widget builder(BuildContext context, StateSetter setState) { setStates.add(setState); final bool returnMiddle = !didMiddle; didMiddle = true; return Wrapper( child: Wrapper( child: StatefulWrapper( child: returnMiddle ? middle : Container(), ), ), ); } final Widget part1 = Wrapper( child: KeyedSubtree( key: key1, child: StatefulBuilder( builder: builder, ), ), ); final Widget part2 = Wrapper( child: KeyedSubtree( key: key2, child: StatefulBuilder( builder: builder, ), ), ); middle = part2; await tester.pumpWidget(part1); for (final StatefulWrapperState state in tester.stateList<StatefulWrapperState>(find.byType(StatefulWrapper))) { expect(state.built, isNotNull); state.oldBuilt = state.built!; state.trigger(); } for (final StateSetter setState in setStates) setState(() { }); StatefulWrapperState.buildId = 0; middle = part1; didMiddle = false; await tester.pumpWidget(part2); for (final StatefulWrapperState state in tester.stateList<StatefulWrapperState>(find.byType(StatefulWrapper))) { expect(state.built, isNotNull); expect(state.built, isNot(equals(state.oldBuilt))); } }); }