flutter_flutter/packages/flutter/test/widgets/editable_text_scribble_test.dart
Tirth 586a59211d
Rename 'SelectionChangedCause.scribble' to 'SelectionChangedCause.stylusHandwriting' (#161518)
Rename 'SelectionChangedCause.scribble' to
'SelectionChangedCause.stylusHandwriting'

Fixes #159223 

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
2025-01-25 05:50:26 +00:00

864 lines
30 KiB
Dart
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'editable_text_utils.dart';
void main() {
const TextStyle textStyle = TextStyle();
const Color cursorColor = Color.fromARGB(0xFF, 0xFF, 0x00, 0x00);
late TextEditingController controller;
late FocusNode focusNode;
setUp(() async {
controller = TextEditingController();
focusNode = FocusNode(debugLabel: 'EditableText Node');
});
tearDown(() {
controller.dispose();
focusNode.dispose();
});
testWidgets(
'selection rects re-sent when refocused',
(WidgetTester tester) async {
final List<List<SelectionRect>> log = <List<SelectionRect>>[];
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.textInput, (
MethodCall methodCall,
) async {
if (methodCall.method == 'TextInput.setSelectionRects') {
final List<dynamic> args = methodCall.arguments as List<dynamic>;
final List<SelectionRect> selectionRects = <SelectionRect>[];
for (final dynamic rect in args) {
selectionRects.add(
SelectionRect(
position: (rect as List<dynamic>)[4] as int,
bounds: Rect.fromLTWH(
rect[0] as double,
rect[1] as double,
rect[2] as double,
rect[3] as double,
),
),
);
}
log.add(selectionRects);
}
return null;
});
final ScrollController scrollController = ScrollController();
addTearDown(scrollController.dispose);
controller.text = 'Text1';
Future<void> pumpEditableText({
double? width,
double? height,
TextAlign textAlign = TextAlign.start,
}) async {
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(),
child: Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: SizedBox(
width: width,
height: height,
child: EditableText(
controller: controller,
textAlign: textAlign,
scrollController: scrollController,
maxLines: null,
focusNode: focusNode,
cursorWidth: 0,
style: Typography.material2018().black.titleMedium!,
cursorColor: Colors.blue,
backgroundCursorColor: Colors.grey,
),
),
),
),
),
);
}
const List<SelectionRect> expectedRects = <SelectionRect>[
SelectionRect(position: 0, bounds: Rect.fromLTRB(0.0, 0.0, 14.0, 14.0)),
SelectionRect(position: 1, bounds: Rect.fromLTRB(14.0, 0.0, 28.0, 14.0)),
SelectionRect(position: 2, bounds: Rect.fromLTRB(28.0, 0.0, 42.0, 14.0)),
SelectionRect(position: 3, bounds: Rect.fromLTRB(42.0, 0.0, 56.0, 14.0)),
SelectionRect(position: 4, bounds: Rect.fromLTRB(56.0, 0.0, 70.0, 14.0)),
];
await pumpEditableText();
expect(log, isEmpty);
await tester.showKeyboard(find.byType(EditableText));
// First update.
expect(log.single, expectedRects);
log.clear();
await tester.pumpAndSettle();
expect(log, isEmpty);
focusNode.unfocus();
await tester.pumpAndSettle();
expect(log, isEmpty);
focusNode.requestFocus();
//await tester.showKeyboard(find.byType(EditableText));
await tester.pumpAndSettle();
// Should re-receive the same rects.
expect(log.single, expectedRects);
log.clear();
// On web, we should rely on the browser's implementation of Scribble, so we will not send selection rects.
},
skip: kIsWeb, // [intended]
variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.iOS}),
);
testWidgets(
'Selection changes during Scribble interaction should have the scribble cause',
(WidgetTester tester) async {
controller.text = 'Lorem ipsum dolor sit amet';
late SelectionChangedCause selectionCause;
await tester.pumpWidget(
MaterialApp(
home: EditableText(
controller: controller,
backgroundCursorColor: Colors.grey,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
selectionControls: materialTextSelectionControls,
onSelectionChanged: (TextSelection selection, SelectionChangedCause? cause) {
if (cause != null) {
selectionCause = cause;
}
},
),
),
);
await tester.showKeyboard(find.byType(EditableText));
// A normal selection update from the framework has 'keyboard' as the cause.
tester.testTextInput.updateEditingValue(
TextEditingValue(
text: controller.text,
selection: const TextSelection(baseOffset: 2, extentOffset: 3),
),
);
await tester.pumpAndSettle();
expect(selectionCause, SelectionChangedCause.keyboard);
// A selection update during a scribble interaction has 'scribble' as the cause.
await tester.testTextInput.startScribbleInteraction();
tester.testTextInput.updateEditingValue(
TextEditingValue(
text: controller.text,
selection: const TextSelection(baseOffset: 3, extentOffset: 4),
),
);
await tester.pumpAndSettle();
expect(selectionCause, SelectionChangedCause.stylusHandwriting);
await tester.testTextInput.finishScribbleInteraction();
},
variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.iOS}),
);
testWidgets(
'Requests focus and changes the selection when onScribbleFocus is called',
(WidgetTester tester) async {
controller.text = 'Lorem ipsum dolor sit amet';
late SelectionChangedCause selectionCause;
await tester.pumpWidget(
MaterialApp(
home: EditableText(
controller: controller,
backgroundCursorColor: Colors.grey,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
selectionControls: materialTextSelectionControls,
onSelectionChanged: (TextSelection selection, SelectionChangedCause? cause) {
if (cause != null) {
selectionCause = cause;
}
},
),
),
);
await tester.testTextInput.scribbleFocusElement(
TextInput.scribbleClients.keys.first,
Offset.zero,
);
expect(focusNode.hasFocus, true);
expect(selectionCause, SelectionChangedCause.stylusHandwriting);
// On web, we should rely on the browser's implementation of Scribble, so the selection changed cause
// will never be SelectionChangedCause.stylusHandwriting.
},
skip: kIsWeb, // [intended]
variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.iOS}),
);
testWidgets(
'Declares itself for Scribble interaction if the bounds overlap the scribble rect and the widget is touchable',
(WidgetTester tester) async {
controller.text = 'Lorem ipsum dolor sit amet';
await tester.pumpWidget(
MaterialApp(
home: EditableText(
controller: controller,
backgroundCursorColor: Colors.grey,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
selectionControls: materialTextSelectionControls,
),
),
);
final List<dynamic> elementEntry = <dynamic>[
TextInput.scribbleClients.keys.first,
0.0,
0.0,
800.0,
600.0,
];
List<List<dynamic>> elements = await tester.testTextInput.scribbleRequestElementsInRect(
const Rect.fromLTWH(0, 0, 1, 1),
);
expect(elements.first, containsAll(elementEntry));
// Touch is outside the bounds of the widget.
elements = await tester.testTextInput.scribbleRequestElementsInRect(
const Rect.fromLTWH(-1, -1, 1, 1),
);
expect(elements.length, 0);
// Widget is read only.
await tester.pumpWidget(
MaterialApp(
home: EditableText(
readOnly: true,
controller: controller,
backgroundCursorColor: Colors.grey,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
selectionControls: materialTextSelectionControls,
),
),
);
elements = await tester.testTextInput.scribbleRequestElementsInRect(
const Rect.fromLTWH(0, 0, 1, 1),
);
expect(elements.length, 0);
// Widget is not touchable.
await tester.pumpWidget(
MaterialApp(
home: Stack(
children: <Widget>[
EditableText(
controller: controller,
backgroundCursorColor: Colors.grey,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
selectionControls: materialTextSelectionControls,
),
Positioned(
left: 0,
top: 0,
right: 0,
bottom: 0,
child: Container(color: Colors.black),
),
],
),
),
);
elements = await tester.testTextInput.scribbleRequestElementsInRect(
const Rect.fromLTWH(0, 0, 1, 1),
);
expect(elements.length, 0);
// Widget has scribble disabled.
await tester.pumpWidget(
MaterialApp(
home: EditableText(
controller: controller,
backgroundCursorColor: Colors.grey,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
selectionControls: materialTextSelectionControls,
stylusHandwritingEnabled: false,
),
),
);
elements = await tester.testTextInput.scribbleRequestElementsInRect(
const Rect.fromLTWH(0, 0, 1, 1),
);
expect(elements.length, 0);
// On web, we should rely on the browser's implementation of Scribble, so the engine will
// never request the scribble elements.
},
skip: kIsWeb, // [intended]
variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.iOS}),
);
testWidgets(
'single line Scribble fields can show a horizontal placeholder',
(WidgetTester tester) async {
controller.text = 'Lorem ipsum dolor sit amet';
await tester.pumpWidget(
MaterialApp(
home: EditableText(
controller: controller,
backgroundCursorColor: Colors.grey,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
selectionControls: materialTextSelectionControls,
),
),
);
await tester.showKeyboard(find.byType(EditableText));
tester.testTextInput.updateEditingValue(
TextEditingValue(
text: controller.text,
selection: const TextSelection(baseOffset: 5, extentOffset: 5),
),
);
await tester.pumpAndSettle();
await tester.testTextInput.scribbleInsertPlaceholder();
await tester.pumpAndSettle();
TextSpan textSpan = findRenderEditable(tester).text! as TextSpan;
expect(textSpan.children!.length, 3);
expect((textSpan.children![0] as TextSpan).text, 'Lorem');
expect(textSpan.children![1] is WidgetSpan, true);
expect((textSpan.children![2] as TextSpan).text, ' ipsum dolor sit amet');
await tester.testTextInput.scribbleRemovePlaceholder();
await tester.pumpAndSettle();
textSpan = findRenderEditable(tester).text! as TextSpan;
expect(textSpan.children, null);
expect(textSpan.text, 'Lorem ipsum dolor sit amet');
// Widget has scribble disabled.
await tester.pumpWidget(
MaterialApp(
home: EditableText(
controller: controller,
backgroundCursorColor: Colors.grey,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
selectionControls: materialTextSelectionControls,
stylusHandwritingEnabled: false,
),
),
);
await tester.showKeyboard(find.byType(EditableText));
tester.testTextInput.updateEditingValue(
TextEditingValue(
text: controller.text,
selection: const TextSelection(baseOffset: 5, extentOffset: 5),
),
);
await tester.pumpAndSettle();
await tester.testTextInput.scribbleInsertPlaceholder();
await tester.pumpAndSettle();
textSpan = findRenderEditable(tester).text! as TextSpan;
expect(textSpan.children, null);
expect(textSpan.text, 'Lorem ipsum dolor sit amet');
// On web, we should rely on the browser's implementation of Scribble, so the framework
// will not handle placeholders.
},
skip: kIsWeb, // [intended]
variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.iOS}),
);
testWidgets(
'multiline Scribble fields can show a vertical placeholder',
(WidgetTester tester) async {
controller.text = 'Lorem ipsum dolor sit amet';
await tester.pumpWidget(
MaterialApp(
home: EditableText(
controller: controller,
backgroundCursorColor: Colors.grey,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
selectionControls: materialTextSelectionControls,
maxLines: 2,
),
),
);
await tester.showKeyboard(find.byType(EditableText));
tester.testTextInput.updateEditingValue(
TextEditingValue(
text: controller.text,
selection: const TextSelection(baseOffset: 5, extentOffset: 5),
),
);
await tester.pumpAndSettle();
await tester.testTextInput.scribbleInsertPlaceholder();
await tester.pumpAndSettle();
TextSpan textSpan = findRenderEditable(tester).text! as TextSpan;
expect(textSpan.children!.length, 4);
expect((textSpan.children![0] as TextSpan).text, 'Lorem');
expect(textSpan.children![1] is WidgetSpan, true);
expect(textSpan.children![2] is WidgetSpan, true);
expect((textSpan.children![3] as TextSpan).text, ' ipsum dolor sit amet');
await tester.testTextInput.scribbleRemovePlaceholder();
await tester.pumpAndSettle();
textSpan = findRenderEditable(tester).text! as TextSpan;
expect(textSpan.children, null);
expect(textSpan.text, 'Lorem ipsum dolor sit amet');
// Widget has scribble disabled.
await tester.pumpWidget(
MaterialApp(
home: EditableText(
controller: controller,
backgroundCursorColor: Colors.grey,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
selectionControls: materialTextSelectionControls,
maxLines: 2,
stylusHandwritingEnabled: false,
),
),
);
await tester.showKeyboard(find.byType(EditableText));
tester.testTextInput.updateEditingValue(
TextEditingValue(
text: controller.text,
selection: const TextSelection(baseOffset: 5, extentOffset: 5),
),
);
await tester.pumpAndSettle();
await tester.testTextInput.scribbleInsertPlaceholder();
await tester.pumpAndSettle();
textSpan = findRenderEditable(tester).text! as TextSpan;
expect(textSpan.children, null);
expect(textSpan.text, 'Lorem ipsum dolor sit amet');
// On web, we should rely on the browser's implementation of Scribble, so the framework
// will not handle placeholders.
},
skip: kIsWeb, // [intended]
variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.iOS}),
);
testWidgets(
'selection rects are sent when they change',
(WidgetTester tester) async {
addTearDown(tester.view.reset);
// Ensure selection rects are sent on iPhone (using SE 3rd gen size)
tester.view.physicalSize = const Size(750.0, 1334.0);
final List<List<SelectionRect>> log = <List<SelectionRect>>[];
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.textInput, (
MethodCall methodCall,
) {
if (methodCall.method == 'TextInput.setSelectionRects') {
final List<dynamic> args = methodCall.arguments as List<dynamic>;
final List<SelectionRect> selectionRects = <SelectionRect>[];
for (final dynamic rect in args) {
selectionRects.add(
SelectionRect(
position: (rect as List<dynamic>)[4] as int,
bounds: Rect.fromLTWH(
rect[0] as double,
rect[1] as double,
rect[2] as double,
rect[3] as double,
),
),
);
}
log.add(selectionRects);
}
return null;
});
final ScrollController scrollController = ScrollController();
addTearDown(scrollController.dispose);
controller.text = 'Text1';
Future<void> pumpEditableText({
double? width,
double? height,
TextAlign textAlign = TextAlign.start,
}) async {
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(),
child: Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: SizedBox(
width: width,
height: height,
child: EditableText(
controller: controller,
textAlign: textAlign,
scrollController: scrollController,
maxLines: null,
focusNode: focusNode,
cursorWidth: 0,
style: Typography.material2018().black.titleMedium!,
cursorColor: Colors.blue,
backgroundCursorColor: Colors.grey,
),
),
),
),
),
);
}
await pumpEditableText();
expect(log, isEmpty);
await tester.showKeyboard(find.byType(EditableText));
// First update.
expect(log.single, const <SelectionRect>[
SelectionRect(position: 0, bounds: Rect.fromLTRB(0.0, 0.0, 14.0, 14.0)),
SelectionRect(position: 1, bounds: Rect.fromLTRB(14.0, 0.0, 28.0, 14.0)),
SelectionRect(position: 2, bounds: Rect.fromLTRB(28.0, 0.0, 42.0, 14.0)),
SelectionRect(position: 3, bounds: Rect.fromLTRB(42.0, 0.0, 56.0, 14.0)),
SelectionRect(position: 4, bounds: Rect.fromLTRB(56.0, 0.0, 70.0, 14.0)),
]);
log.clear();
await tester.pumpAndSettle();
expect(log, isEmpty);
await pumpEditableText();
expect(log, isEmpty);
// Change the width such that each character occupies a line.
await pumpEditableText(width: 20);
expect(log.single, const <SelectionRect>[
SelectionRect(position: 0, bounds: Rect.fromLTRB(0.0, 0.0, 14.0, 14.0)),
SelectionRect(position: 1, bounds: Rect.fromLTRB(0.0, 14.0, 14.0, 28.0)),
SelectionRect(position: 2, bounds: Rect.fromLTRB(0.0, 28.0, 14.0, 42.0)),
SelectionRect(position: 3, bounds: Rect.fromLTRB(0.0, 42.0, 14.0, 56.0)),
SelectionRect(position: 4, bounds: Rect.fromLTRB(0.0, 56.0, 14.0, 70.0)),
]);
log.clear();
await tester.enterText(find.byType(EditableText), 'Text1👨👩👦');
await tester.pump();
expect(log.single, const <SelectionRect>[
SelectionRect(position: 0, bounds: Rect.fromLTRB(0.0, 0.0, 14.0, 14.0)),
SelectionRect(position: 1, bounds: Rect.fromLTRB(0.0, 14.0, 14.0, 28.0)),
SelectionRect(position: 2, bounds: Rect.fromLTRB(0.0, 28.0, 14.0, 42.0)),
SelectionRect(position: 3, bounds: Rect.fromLTRB(0.0, 42.0, 14.0, 56.0)),
SelectionRect(position: 4, bounds: Rect.fromLTRB(0.0, 56.0, 14.0, 70.0)),
SelectionRect(position: 5, bounds: Rect.fromLTRB(0.0, 70.0, 42.0, 84.0)),
]);
log.clear();
// The 4th line will be partially visible.
await pumpEditableText(width: 20, height: 45);
expect(log.single, const <SelectionRect>[
SelectionRect(position: 0, bounds: Rect.fromLTRB(0.0, 0.0, 14.0, 14.0)),
SelectionRect(position: 1, bounds: Rect.fromLTRB(0.0, 14.0, 14.0, 28.0)),
SelectionRect(position: 2, bounds: Rect.fromLTRB(0.0, 28.0, 14.0, 42.0)),
SelectionRect(position: 3, bounds: Rect.fromLTRB(0.0, 42.0, 14.0, 56.0)),
]);
log.clear();
await pumpEditableText(width: 20, height: 45, textAlign: TextAlign.right);
// This is 1px off from being completely right-aligned. The 1px width is
// reserved for caret.
expect(log.single, const <SelectionRect>[
SelectionRect(position: 0, bounds: Rect.fromLTRB(5.0, 0.0, 19.0, 14.0)),
SelectionRect(position: 1, bounds: Rect.fromLTRB(5.0, 14.0, 19.0, 28.0)),
SelectionRect(position: 2, bounds: Rect.fromLTRB(5.0, 28.0, 19.0, 42.0)),
SelectionRect(position: 3, bounds: Rect.fromLTRB(5.0, 42.0, 19.0, 56.0)),
// These 2 lines will be out of bounds.
// SelectionRect(position: 4, bounds: Rect.fromLTRB(5.0, 56.0, 19.0, 70.0)),
// SelectionRect(position: 5, bounds: Rect.fromLTRB(-23.0, 70.0, 19.0, 84.0)),
]);
log.clear();
expect(scrollController.offset, 0);
// Scrolling also triggers update.
scrollController.jumpTo(14);
await tester.pumpAndSettle();
expect(log.single, const <SelectionRect>[
SelectionRect(position: 0, bounds: Rect.fromLTRB(5.0, -14.0, 19.0, 0.0)),
SelectionRect(position: 1, bounds: Rect.fromLTRB(5.0, 0.0, 19.0, 14.0)),
SelectionRect(position: 2, bounds: Rect.fromLTRB(5.0, 14.0, 19.0, 28.0)),
SelectionRect(position: 3, bounds: Rect.fromLTRB(5.0, 28.0, 19.0, 42.0)),
SelectionRect(position: 4, bounds: Rect.fromLTRB(5.0, 42.0, 19.0, 56.0)),
// This line is skipped because it's below the bottom edge of the render
// object.
// SelectionRect(position: 5, bounds: Rect.fromLTRB(5.0, 56.0, 47.0, 70.0)),
]);
log.clear();
// On web, we should rely on the browser's implementation of Scribble, so we will not send selection rects.
},
skip: kIsWeb, // [intended]
variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.iOS}),
);
testWidgets(
'selection rects are not sent if stylusHandwritingEnabled is false',
(WidgetTester tester) async {
final List<MethodCall> log = <MethodCall>[];
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.textInput, (
MethodCall methodCall,
) async {
log.add(methodCall);
return null;
});
controller.text = 'Text1';
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(),
child: Directionality(
textDirection: TextDirection.ltr,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
EditableText(
key: ValueKey<String>(controller.text),
controller: controller,
focusNode: focusNode,
style: Typography.material2018().black.titleMedium!,
cursorColor: Colors.blue,
backgroundCursorColor: Colors.grey,
stylusHandwritingEnabled: false,
),
],
),
),
),
);
await tester.showKeyboard(find.byKey(ValueKey<String>(controller.text)));
// There should be a new platform message updating the selection rects.
expect(log.where((MethodCall m) => m.method == 'TextInput.setSelectionRects').length, 0);
// On web, we should rely on the browser's implementation of Scribble, so we will not send selection rects.
},
skip: kIsWeb, // [intended]
variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.iOS}),
);
testWidgets(
'selection rects sent even when character corners are outside of paintBounds',
(WidgetTester tester) async {
final List<List<SelectionRect>> log = <List<SelectionRect>>[];
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.textInput, (
MethodCall methodCall,
) {
if (methodCall.method == 'TextInput.setSelectionRects') {
final List<dynamic> args = methodCall.arguments as List<dynamic>;
final List<SelectionRect> selectionRects = <SelectionRect>[];
for (final dynamic rect in args) {
selectionRects.add(
SelectionRect(
position: (rect as List<dynamic>)[4] as int,
bounds: Rect.fromLTWH(
rect[0] as double,
rect[1] as double,
rect[2] as double,
rect[3] as double,
),
),
);
}
log.add(selectionRects);
}
return null;
});
final ScrollController scrollController = ScrollController();
addTearDown(scrollController.dispose);
controller.text = 'Text1';
final GlobalKey<EditableTextState> editableTextKey = GlobalKey();
Future<void> pumpEditableText({
double? width,
double? height,
TextAlign textAlign = TextAlign.start,
}) async {
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(),
child: Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: SizedBox(
width: width,
height: height,
child: EditableText(
controller: controller,
textAlign: textAlign,
scrollController: scrollController,
maxLines: null,
focusNode: focusNode,
cursorWidth: 0,
key: editableTextKey,
style: Typography.material2018().black.titleMedium!,
cursorColor: Colors.blue,
backgroundCursorColor: Colors.grey,
),
),
),
),
),
);
}
// Set height to 1 pixel less than full height.
await pumpEditableText(height: 13);
expect(log, isEmpty);
// Scroll so that the top of each character is above the top of the renderEditable
// and the bottom of each character is below the bottom of the renderEditable.
final ViewportOffset offset = ViewportOffset.fixed(0.5);
addTearDown(offset.dispose);
editableTextKey.currentState!.renderEditable.offset = offset;
await tester.showKeyboard(find.byType(EditableText));
// We should get all the rects.
expect(log.single, const <SelectionRect>[
SelectionRect(position: 0, bounds: Rect.fromLTRB(0.0, -0.5, 14.0, 13.5)),
SelectionRect(position: 1, bounds: Rect.fromLTRB(14.0, -0.5, 28.0, 13.5)),
SelectionRect(position: 2, bounds: Rect.fromLTRB(28.0, -0.5, 42.0, 13.5)),
SelectionRect(position: 3, bounds: Rect.fromLTRB(42.0, -0.5, 56.0, 13.5)),
SelectionRect(position: 4, bounds: Rect.fromLTRB(56.0, -0.5, 70.0, 13.5)),
]);
log.clear();
// On web, we should rely on the browser's implementation of Scribble, so we will not send selection rects.
},
skip: kIsWeb, // [intended]
variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.iOS}),
);
// Regression test for https://github.com/flutter/flutter/issues/159259.
testWidgets(
'showToolbar does nothing and returns false when already shown during Scribble selection',
(WidgetTester tester) async {
controller.text = 'Lorem ipsum dolor sit amet';
final GlobalKey<EditableTextState> editableTextKey = GlobalKey();
await tester.pumpWidget(
MaterialApp(
home: EditableText(
key: editableTextKey,
controller: controller,
backgroundCursorColor: Colors.grey,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
selectionControls: materialTextSelectionHandleControls,
contextMenuBuilder: (BuildContext context, EditableTextState editableTextState) {
return AdaptiveTextSelectionToolbar.editableText(
editableTextState: editableTextState,
);
},
),
),
);
expect(find.byType(AdaptiveTextSelectionToolbar), findsNothing);
await tester.showKeyboard(find.byType(EditableText));
await tester.testTextInput.startScribbleInteraction();
tester.testTextInput.updateEditingValue(
TextEditingValue(
text: controller.text,
selection: const TextSelection(baseOffset: 3, extentOffset: 4),
),
);
await tester.pumpAndSettle();
expect(find.byType(AdaptiveTextSelectionToolbar), findsNothing);
expect(editableTextKey.currentState!.showToolbar(), isTrue);
await tester.pumpAndSettle();
expect(find.byType(AdaptiveTextSelectionToolbar), findsOneWidget);
expect(editableTextKey.currentState!.showToolbar(), isFalse);
await tester.pump();
expect(find.byType(AdaptiveTextSelectionToolbar), findsOneWidget);
await tester.pumpAndSettle();
expect(find.byType(AdaptiveTextSelectionToolbar), findsOneWidget);
await tester.testTextInput.finishScribbleInteraction();
},
variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.iOS}),
skip: kIsWeb, // [intended]
);
}