blob: f7dfd001645eff0cf706e94bf835f994c55b4773 [file] [log] [blame]
// 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/widgets.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({ bool extraPadding, String text, ScrollController controller }) {
return MaterialApp(
home: Scaffold(
body: Column(
children: <Widget>[
Expanded(
child: Container(),
),
Container(
height: 500.0,
child: ListView(
controller: controller,
children: List<Widget>.generate(10, (int i) {
return Container(
color: i % 2 == 0 ? 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, this.extraPadding, 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;
}