blob: 86a0b187cffd38ecbb904838d67405c2b44071e7 [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/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/services.dart';
class _TestSliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate {
_TestSliverPersistentHeaderDelegate({
required this.minExtent,
required this.maxExtent,
required this.child,
this.vsync = const TestVSync(),
this.showOnScreenConfiguration = const PersistentHeaderShowOnScreenConfiguration(),
});
final Widget child;
@override
final double maxExtent;
@override
final double minExtent;
@override
final TickerProvider? vsync;
@override
final PersistentHeaderShowOnScreenConfiguration showOnScreenConfiguration;
@override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) => child;
@override
bool shouldRebuild(_TestSliverPersistentHeaderDelegate oldDelegate) => true;
}
void main() {
const TextStyle textStyle = TextStyle();
const Color cursorColor = Color.fromARGB(0xFF, 0xFF, 0x00, 0x00);
testWidgets('tapping on a partly visible editable brings it fully on screen', (WidgetTester tester) async {
final ScrollController scrollController = ScrollController();
final TextEditingController controller = TextEditingController();
final FocusNode focusNode = FocusNode();
await tester.pumpWidget(MaterialApp(
home: Center(
child: Container(
height: 300.0,
child: ListView(
controller: scrollController,
children: <Widget>[
EditableText(
backgroundCursorColor: Colors.grey,
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
),
Container(
height: 350.0,
),
],
),
),
),
));
// Scroll the EditableText half off screen.
final RenderBox render = tester.renderObject(find.byType(EditableText));
scrollController.jumpTo(render.size.height / 2);
await tester.pumpAndSettle();
expect(scrollController.offset, render.size.height / 2);
await tester.showKeyboard(find.byType(EditableText));
await tester.pumpAndSettle();
expect(scrollController.offset, 0.0);
});
testWidgets('tapping on a partly visible editable brings it fully on screen with scrollInsets', (WidgetTester tester) async {
final ScrollController scrollController = ScrollController();
final TextEditingController controller = TextEditingController();
final FocusNode focusNode = FocusNode();
await tester.pumpWidget(MaterialApp(
home: Center(
child: Container(
height: 300.0,
child: ListView(
controller: scrollController,
children: <Widget>[
Container(
height: 200.0,
),
EditableText(
backgroundCursorColor: Colors.grey,
scrollPadding: const EdgeInsets.all(50.0),
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
),
Container(
height: 850.0,
),
],
),
),
),
));
// Scroll the EditableText half off screen.
final RenderBox render = tester.renderObject(find.byType(EditableText));
scrollController.jumpTo(200 + render.size.height / 2);
await tester.pumpAndSettle();
expect(scrollController.offset, 200 + render.size.height / 2);
await tester.showKeyboard(find.byType(EditableText));
await tester.pumpAndSettle();
// Container above the text is 200 in height, the scrollInsets are 50
// Tolerance of 5 units (The actual value was 152.0 in the current tests instead of 150.0)
expect(scrollController.offset, lessThan(200.0 - 50.0 + 5.0));
expect(scrollController.offset, greaterThan(200.0 - 50.0 - 5.0));
});
testWidgets('editable comes back on screen when entering text while it is off-screen', (WidgetTester tester) async {
final ScrollController scrollController = ScrollController(initialScrollOffset: 100.0);
final TextEditingController controller = TextEditingController();
final FocusNode focusNode = FocusNode();
await tester.pumpWidget(MaterialApp(
home: Center(
child: Container(
height: 300.0,
child: ListView(
controller: scrollController,
children: <Widget>[
Container(
height: 350.0,
),
EditableText(
backgroundCursorColor: Colors.grey,
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
),
Container(
height: 350.0,
),
],
),
),
),
));
// Focus the EditableText and scroll it off screen.
await tester.showKeyboard(find.byType(EditableText));
await tester.pumpAndSettle();
expect(focusNode.hasFocus, isTrue);
scrollController.jumpTo(0.0);
await tester.pumpAndSettle();
expect(scrollController.offset, 0.0);
expect(find.byType(EditableText), findsNothing);
// Entering text brings it back on screen.
tester.testTextInput.enterText('Hello');
await tester.pumpAndSettle();
expect(scrollController.offset, greaterThan(0.0));
expect(find.byType(EditableText), findsOneWidget);
});
testWidgets('entering text does not scroll when scrollPhysics.allowImplicitScrolling = false', (WidgetTester tester) async {
// regression test for https://github.com/flutter/flutter/issues/19523
final ScrollController scrollController = ScrollController(initialScrollOffset: 100.0);
final TextEditingController controller = TextEditingController();
final FocusNode focusNode = FocusNode();
await tester.pumpWidget(MaterialApp(
home: Center(
child: Container(
height: 300.0,
child: ListView(
physics: const NoImplicitScrollPhysics(),
controller: scrollController,
children: <Widget>[
Container(
height: 350.0,
),
EditableText(
backgroundCursorColor: Colors.grey,
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
),
Container(
height: 350.0,
),
],
),
),
),
));
// Focus the EditableText and scroll it off screen.
await tester.showKeyboard(find.byType(EditableText));
await tester.pumpAndSettle();
expect(focusNode.hasFocus, isTrue);
scrollController.jumpTo(0.0);
await tester.pumpAndSettle();
expect(scrollController.offset, 0.0);
expect(find.byType(EditableText), findsNothing);
// Entering text brings it not back on screen.
tester.testTextInput.enterText('Hello');
await tester.pumpAndSettle();
expect(scrollController.offset, 0.0);
expect(find.byType(EditableText), findsNothing);
});
testWidgets('entering text does not scroll a surrounding PageView', (WidgetTester tester) async {
// regression test for https://github.com/flutter/flutter/issues/19523
final TextEditingController textController = TextEditingController();
final PageController pageController = PageController(initialPage: 1);
await tester.pumpWidget(
MaterialApp(
home: MediaQuery(
data: const MediaQueryData(devicePixelRatio: 1.0),
child: Directionality(
textDirection: TextDirection.ltr,
child: Material(
child: PageView(
controller: pageController,
children: <Widget>[
Container(
color: Colors.red,
),
Container(
child: TextField(
controller: textController,
),
color: Colors.green,
),
Container(
color: Colors.red,
),
],
),
),
),
),
),
);
await tester.showKeyboard(find.byType(EditableText));
await tester.pumpAndSettle();
expect(textController.text, '');
tester.testTextInput.enterText('H');
final int frames = await tester.pumpAndSettle();
// The text input should not trigger any animations, which would indicate
// that the surrounding PageView is incorrectly scrolling back-and-forth.
expect(frames, 1);
expect(textController.text, 'H');
});
testWidgets('focused multi-line editable scrolls caret back into view when typing', (WidgetTester tester) async {
final ScrollController scrollController = ScrollController();
final TextEditingController controller = TextEditingController();
final FocusNode focusNode = FocusNode();
controller.text = 'Start\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nEnd';
await tester.pumpWidget(MaterialApp(
home: Center(
child: Container(
height: 300.0,
child: ListView(
controller: scrollController,
children: <Widget>[
EditableText(
backgroundCursorColor: Colors.grey,
maxLines: null, // multiline
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
),
],
),
),
),
));
// Bring keyboard up and verify that end of EditableText is not on screen.
await tester.showKeyboard(find.byType(EditableText));
await tester.pumpAndSettle();
scrollController.jumpTo(0.0);
await tester.pumpAndSettle();
final RenderBox render = tester.renderObject(find.byType(EditableText));
expect(render.size.height, greaterThan(500.0));
expect(scrollController.offset, 0.0);
// Enter text at end, which is off-screen.
final String textToEnter = '${controller.text} HELLO';
tester.testTextInput.updateEditingValue(TextEditingValue(
text: textToEnter,
selection: TextSelection.collapsed(offset: textToEnter.length),
));
await tester.pumpAndSettle();
// Caret scrolls into view.
expect(find.byType(EditableText), findsOneWidget);
expect(render.size.height, greaterThan(500.0));
expect(scrollController.offset, greaterThan(0.0));
});
testWidgets('scrolls into view with scrollInserts after the keyboard pops up', (WidgetTester tester) async {
final ScrollController scrollController = ScrollController();
final TextEditingController controller = TextEditingController();
final FocusNode focusNode = FocusNode();
const Key container = Key('container');
await tester.pumpWidget(MaterialApp(
home: Align(
alignment: Alignment.bottomCenter,
child: Container(
height: 300.0,
child: ListView(
controller: scrollController,
children: <Widget>[
Container(
key: container,
height: 200.0,
),
EditableText(
backgroundCursorColor: Colors.grey,
scrollPadding: const EdgeInsets.only(bottom: 300.0),
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
),
Container(
height: 400.0,
),
],
),
),
),
));
expect(scrollController.offset, 0.0);
await tester.showKeyboard(find.byType(EditableText));
await tester.pumpAndSettle();
expect(scrollController.offset, greaterThan(0.0));
expect(find.byKey(container), findsNothing);
});
testWidgets(
'A pinned persistent header should not scroll when its descendant EditableText gains focus',
(WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/25507.
ScrollController controller;
final TextEditingController textEditingController = TextEditingController();
final FocusNode focusNode = FocusNode();
const Key headerKey = Key('header');
await tester.pumpWidget(
MaterialApp(
home: Center(
child: SizedBox(
height: 600.0,
width: 600.0,
child: CustomScrollView(
controller: controller = ScrollController(initialScrollOffset: 0),
slivers: List<Widget>.generate(50, (int i) {
return i == 10
? SliverPersistentHeader(
pinned: true,
floating: false,
delegate: _TestSliverPersistentHeaderDelegate(
minExtent: 50,
maxExtent: 50,
child: Container(
alignment: Alignment.topCenter,
child: EditableText(
key: headerKey,
backgroundCursorColor: Colors.grey,
controller: textEditingController,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
),
),
),
)
: SliverToBoxAdapter(
child: Container(
height: 100.0,
child: Text('Tile $i'),
),
);
}),
),
),
),
),
);
// The persistent header should now be pinned at the top.
controller.jumpTo(100.0 * 15);
await tester.pumpAndSettle();
expect(controller.offset, 100.0 * 15);
focusNode.requestFocus();
await tester.pumpAndSettle();
// The scroll offset should remain the same.
expect(controller.offset, 100.0 * 15);
});
testWidgets(
'A pinned persistent header should not scroll when its descendant EditableText gains focus (no animation)',
(WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/25507.
ScrollController controller;
final TextEditingController textEditingController = TextEditingController();
final FocusNode focusNode = FocusNode();
const Key headerKey = Key('header');
await tester.pumpWidget(
MaterialApp(
home: Center(
child: SizedBox(
height: 600.0,
width: 600.0,
child: CustomScrollView(
controller: controller = ScrollController(initialScrollOffset: 0),
slivers: List<Widget>.generate(50, (int i) {
return i == 10
? SliverPersistentHeader(
pinned: true,
floating: false,
delegate: _TestSliverPersistentHeaderDelegate(
minExtent: 50,
maxExtent: 50,
vsync: null,
child: Container(
alignment: Alignment.topCenter,
child: EditableText(
key: headerKey,
backgroundCursorColor: Colors.grey,
controller: textEditingController,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
),
),
),
)
: SliverToBoxAdapter(
child: Container(
height: 100.0,
child: Text('Tile $i'),
),
);
}),
),
),
),
),
);
// The persistent header should now be pinned at the top.
controller.jumpTo(100.0 * 15);
await tester.pumpAndSettle();
expect(controller.offset, 100.0 * 15);
focusNode.requestFocus();
await tester.pumpAndSettle();
// The scroll offset should remain the same.
expect(controller.offset, 100.0 * 15);
});
}
class NoImplicitScrollPhysics extends AlwaysScrollableScrollPhysics {
const NoImplicitScrollPhysics({ ScrollPhysics? parent }) : super(parent: parent);
@override
bool get allowImplicitScrolling => false;
@override
NoImplicitScrollPhysics applyTo(ScrollPhysics? ancestor) {
return NoImplicitScrollPhysics(parent: buildParent(ancestor));
}
}