| // 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/material.dart'; |
| import 'package:flutter/rendering.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| |
| import 'semantics_tester.dart'; |
| |
| void main() { |
| testWidgets('Un-layouted RenderObject in keep alive offstage area do not crash semantics compiler', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/20313. |
| |
| final SemanticsTester semantics = SemanticsTester(tester); |
| |
| const String initialLabel = 'Foo'; |
| const double bottomScrollOffset = 3000.0; |
| |
| final ScrollController controller = ScrollController(initialScrollOffset: bottomScrollOffset); |
| |
| await tester.pumpWidget(_buildTestWidget( |
| extraPadding: false, |
| text: initialLabel, |
| controller: controller, |
| )); |
| await tester.pumpAndSettle(); |
| |
| // The ProblemWidget has been instantiated (it is on screen). |
| expect(tester.widgetList(find.widgetWithText(ProblemWidget, initialLabel)), hasLength(1)); |
| expect(semantics, includesNodeWith(label: initialLabel)); |
| |
| controller.jumpTo(0.0); |
| await tester.pumpAndSettle(); |
| |
| // The ProblemWidget is not on screen... |
| expect(tester.widgetList(find.widgetWithText(ProblemWidget, initialLabel)), hasLength(0)); |
| // ... but still in the tree as offstage. |
| expect(tester.widgetList(find.widgetWithText(ProblemWidget, initialLabel, skipOffstage: false)), hasLength(1)); |
| expect(semantics, isNot(includesNodeWith(label: initialLabel))); |
| |
| // Introduce a new Padding widget to offstage subtree that will not get its |
| // size calculated because it's offstage. |
| await tester.pumpWidget(_buildTestWidget( |
| extraPadding: true, |
| text: initialLabel, |
| controller: controller, |
| )); |
| final RenderPadding renderPadding = tester.renderObject(find.byKey(paddingWidget, skipOffstage: false)); |
| expect(renderPadding.hasSize, isFalse); |
| expect(semantics, isNot(includesNodeWith(label: initialLabel))); |
| |
| // Change the semantics of the offstage ProblemWidget without crashing. |
| const String newLabel = 'Bar'; |
| expect(newLabel, isNot(equals(initialLabel))); |
| await tester.pumpWidget(_buildTestWidget( |
| extraPadding: true, |
| text: newLabel, |
| controller: controller, |
| )); |
| |
| // The label has changed. |
| expect(tester.widgetList(find.widgetWithText(ProblemWidget, initialLabel, skipOffstage: false)), hasLength(0)); |
| expect(tester.widgetList(find.widgetWithText(ProblemWidget, newLabel, skipOffstage: false)), hasLength(1)); |
| expect(semantics, isNot(includesNodeWith(label: initialLabel))); |
| expect(semantics, isNot(includesNodeWith(label: newLabel))); |
| |
| // Bringing the offstage node back on the screen produces correct semantics tree. |
| controller.jumpTo(bottomScrollOffset); |
| await tester.pumpAndSettle(); |
| |
| expect(tester.widgetList(find.widgetWithText(ProblemWidget, initialLabel)), hasLength(0)); |
| expect(tester.widgetList(find.widgetWithText(ProblemWidget, newLabel)), hasLength(1)); |
| expect(semantics, isNot(includesNodeWith(label: initialLabel))); |
| expect(semantics, includesNodeWith(label: newLabel)); |
| |
| semantics.dispose(); |
| }); |
| } |
| |
| final Key paddingWidget = GlobalKey(); |
| |
| Widget _buildTestWidget({ |
| required bool extraPadding, |
| required String text, |
| required ScrollController controller, |
| }) { |
| return MaterialApp( |
| home: Scaffold( |
| body: Column( |
| children: <Widget>[ |
| Expanded( |
| child: Container(), |
| ), |
| SizedBox( |
| height: 500.0, |
| child: ListView( |
| controller: controller, |
| children: List<Widget>.generate(10, (int i) { |
| return Container( |
| color: i.isEven ? Colors.red : Colors.blue, |
| height: 250.0, |
| child: Text('Item $i'), |
| ); |
| })..add(ProblemWidget( |
| extraPadding: extraPadding, |
| text: text, |
| )), |
| ), |
| ), |
| Expanded( |
| child: Container(), |
| ), |
| ], |
| ), |
| ), |
| ); |
| } |
| |
| class ProblemWidget extends StatefulWidget { |
| const ProblemWidget({ |
| Key? key, |
| required this.extraPadding, |
| required this.text, |
| }) : super(key: key); |
| |
| final bool extraPadding; |
| final String text; |
| |
| @override |
| State<ProblemWidget> createState() => ProblemWidgetState(); |
| } |
| |
| class ProblemWidgetState extends State<ProblemWidget> with AutomaticKeepAliveClientMixin<ProblemWidget> { |
| @override |
| Widget build(BuildContext context) { |
| super.build(context); |
| Widget child = Semantics( |
| container: true, |
| child: Text(widget.text), |
| ); |
| if (widget.extraPadding) { |
| child = Semantics( |
| container: true, |
| child: Padding( |
| key: paddingWidget, |
| padding: const EdgeInsets.all(20.0), |
| child: child, |
| ), |
| ); |
| } |
| return child; |
| } |
| |
| @override |
| bool get wantKeepAlive => true; |
| } |