blob: 859026432d79e2a50171bf2ca4eed18761ad99d4 [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/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'clipboard_utils.dart';
import 'keyboard_utils.dart';
import 'process_text_utils.dart';
import 'semantics_tester.dart';
Offset textOffsetToPosition(RenderParagraph paragraph, int offset) {
const Rect caret = Rect.fromLTWH(0.0, 0.0, 2.0, 20.0);
final Offset localOffset = paragraph.getOffsetForCaret(TextPosition(offset: offset), caret);
return paragraph.localToGlobal(localOffset);
}
Offset globalize(Offset point, RenderBox box) {
return box.localToGlobal(point);
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final MockClipboard mockClipboard = MockClipboard();
setUp(() async {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, mockClipboard.handleMethodCall);
await Clipboard.setData(const ClipboardData(text: 'empty'));
});
tearDown(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, null);
});
group('SelectableRegion', () {
testWidgets('mouse selection single click sends correct events', (WidgetTester tester) async {
final UniqueKey spy = UniqueKey();
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: SelectionSpy(key: spy),
),
),
);
await tester.pumpAndSettle();
final RenderSelectionSpy renderSelectionSpy = tester.renderObject<RenderSelectionSpy>(find.byKey(spy));
final TestGesture gesture = await tester.startGesture(const Offset(200.0, 200.0), kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await tester.pumpAndSettle();
renderSelectionSpy.events.clear();
await gesture.moveTo(const Offset(200.0, 100.0));
expect(renderSelectionSpy.events.length, 2);
expect(renderSelectionSpy.events[0].type, SelectionEventType.startEdgeUpdate);
final SelectionEdgeUpdateEvent startEdge = renderSelectionSpy.events[0] as SelectionEdgeUpdateEvent;
expect(startEdge.globalPosition, const Offset(200.0, 200.0));
expect(renderSelectionSpy.events[1].type, SelectionEventType.endEdgeUpdate);
SelectionEdgeUpdateEvent endEdge = renderSelectionSpy.events[1] as SelectionEdgeUpdateEvent;
expect(endEdge.globalPosition, const Offset(200.0, 100.0));
renderSelectionSpy.events.clear();
await gesture.moveTo(const Offset(100.0, 100.0));
expect(renderSelectionSpy.events.length, 1);
expect(renderSelectionSpy.events[0].type, SelectionEventType.endEdgeUpdate);
endEdge = renderSelectionSpy.events[0] as SelectionEdgeUpdateEvent;
expect(endEdge.globalPosition, const Offset(100.0, 100.0));
await gesture.up();
}, skip: kIsWeb); // https://github.com/flutter/flutter/issues/102410.
testWidgets('mouse double click sends select-word event', (WidgetTester tester) async {
final UniqueKey spy = UniqueKey();
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: SelectionSpy(key: spy),
),
)
);
final RenderSelectionSpy renderSelectionSpy = tester.renderObject<RenderSelectionSpy>(find.byKey(spy));
final TestGesture gesture = await tester.startGesture(const Offset(200.0, 200.0), kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await tester.pump();
await gesture.up();
await tester.pump();
renderSelectionSpy.events.clear();
await gesture.down(const Offset(200.0, 200.0));
await tester.pump();
await gesture.up();
expect(renderSelectionSpy.events.length, 1);
expect(renderSelectionSpy.events[0], isA<SelectWordSelectionEvent>());
final SelectWordSelectionEvent selectionEvent = renderSelectionSpy.events[0] as SelectWordSelectionEvent;
expect(selectionEvent.globalPosition, const Offset(200.0, 200.0));
});
testWidgets('Does not crash when using Navigator pages', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/119776
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: Navigator(
pages: <Page<void>> [
MaterialPage<void>(
child: Column(
children: <Widget>[
const Text('How are you?'),
SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: const SelectAllWidget(child: SizedBox(width: 100, height: 100)),
),
const Text('Fine, thank you.'),
],
),
),
const MaterialPage<void>(
child: Scaffold(body: Text('Foreground Page')),
),
],
onPopPage: (_, __) => false,
),
),
);
expect(tester.takeException(), isNull);
});
testWidgets('can draw handles when they are at rect boundaries', (WidgetTester tester) async {
final UniqueKey spy = UniqueKey();
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: Column(
children: <Widget>[
const Text('How are you?'),
SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: SelectAllWidget(key: spy, child: const SizedBox(width: 100, height: 100)),
),
const Text('Fine, thank you.'),
],
),
),
);
await tester.pumpAndSettle();
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byKey(spy)));
addTearDown(gesture.removePointer);
await tester.pump(const Duration(milliseconds: 500));
await gesture.up();
await tester.pump();
final RenderSelectAll renderSpy = tester.renderObject<RenderSelectAll>(find.byKey(spy));
expect(renderSpy.startHandle, isNotNull);
expect(renderSpy.endHandle, isNotNull);
});
testWidgets('touch does not accept drag', (WidgetTester tester) async {
final UniqueKey spy = UniqueKey();
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: SelectionSpy(key: spy),
),
)
);
final RenderSelectionSpy renderSelectionSpy = tester.renderObject<RenderSelectionSpy>(find.byKey(spy));
final TestGesture gesture = await tester.startGesture(const Offset(200.0, 200.0));
addTearDown(gesture.removePointer);
await gesture.moveTo(const Offset(200.0, 100.0));
await gesture.up();
expect(
renderSelectionSpy.events.every((SelectionEvent element) => element is ClearSelectionEvent),
isTrue
);
});
testWidgets('does not merge semantics node of the children', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: Scaffold(
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const Text('Line one'),
const Text('Line two'),
ElevatedButton(
onPressed: () {},
child: const Text('Button'),
)
],
),
),
),
),
),
);
expect(
semantics,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(
textDirection: TextDirection.ltr,
children: <TestSemantics>[
TestSemantics(
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
TestSemantics(
label: 'Line one',
textDirection: TextDirection.ltr,
),
TestSemantics(
label: 'Line two',
textDirection: TextDirection.ltr,
),
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.isButton,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable
],
actions: <SemanticsAction>[SemanticsAction.tap],
label: 'Button',
textDirection: TextDirection.ltr,
),
],
),
],
),
],
),
],
),
ignoreRect: true,
ignoreTransform: true,
ignoreId: true,
),
);
semantics.dispose();
});
testWidgets('mouse single-click selection collapses the selection', (WidgetTester tester) async {
final UniqueKey spy = UniqueKey();
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: SelectionSpy(key: spy),
),
)
);
await tester.pumpAndSettle();
final RenderSelectionSpy renderSelectionSpy = tester.renderObject<RenderSelectionSpy>(find.byKey(spy));
final TestGesture gesture = await tester.startGesture(const Offset(200.0, 200.0), kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(renderSelectionSpy.events.length, 2);
expect(renderSelectionSpy.events[0], isA<SelectionEdgeUpdateEvent>());
expect((renderSelectionSpy.events[0] as SelectionEdgeUpdateEvent).type, SelectionEventType.startEdgeUpdate);
expect(renderSelectionSpy.events[1], isA<SelectionEdgeUpdateEvent>());
expect((renderSelectionSpy.events[1] as SelectionEdgeUpdateEvent).type, SelectionEventType.endEdgeUpdate);
}, skip: kIsWeb); // https://github.com/flutter/flutter/issues/102410.
testWidgets('touch long press sends select-word event', (WidgetTester tester) async {
final UniqueKey spy = UniqueKey();
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: SelectionSpy(key: spy),
),
)
);
await tester.pumpAndSettle();
final RenderSelectionSpy renderSelectionSpy = tester.renderObject<RenderSelectionSpy>(find.byKey(spy));
renderSelectionSpy.events.clear();
final TestGesture gesture = await tester.startGesture(const Offset(200.0, 200.0));
addTearDown(gesture.removePointer);
await tester.pump(const Duration(milliseconds: 500));
await gesture.up();
expect(renderSelectionSpy.events.length, 1);
expect(renderSelectionSpy.events[0], isA<SelectWordSelectionEvent>());
final SelectWordSelectionEvent selectionEvent = renderSelectionSpy.events[0] as SelectWordSelectionEvent;
expect(selectionEvent.globalPosition, const Offset(200.0, 200.0));
});
testWidgets('touch long press and drag sends correct events', (WidgetTester tester) async {
final UniqueKey spy = UniqueKey();
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: SelectionSpy(key: spy),
),
)
);
await tester.pumpAndSettle();
final RenderSelectionSpy renderSelectionSpy = tester.renderObject<RenderSelectionSpy>(find.byKey(spy));
renderSelectionSpy.events.clear();
final TestGesture gesture = await tester.startGesture(const Offset(200.0, 200.0));
addTearDown(gesture.removePointer);
await tester.pump(const Duration(milliseconds: 500));
expect(renderSelectionSpy.events.length, 1);
expect(renderSelectionSpy.events[0], isA<SelectWordSelectionEvent>());
final SelectWordSelectionEvent selectionEvent = renderSelectionSpy.events[0] as SelectWordSelectionEvent;
expect(selectionEvent.globalPosition, const Offset(200.0, 200.0));
renderSelectionSpy.events.clear();
await gesture.moveTo(const Offset(200.0, 50.0));
await gesture.up();
expect(renderSelectionSpy.events.length, 1);
expect(renderSelectionSpy.events[0].type, SelectionEventType.endEdgeUpdate);
final SelectionEdgeUpdateEvent edgeEvent = renderSelectionSpy.events[0] as SelectionEdgeUpdateEvent;
expect(edgeEvent.globalPosition, const Offset(200.0, 50.0));
expect(edgeEvent.granularity, TextGranularity.word);
});
testWidgets(
'touch long press cancel does not send ClearSelectionEvent',
(WidgetTester tester) async {
final UniqueKey spy = UniqueKey();
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: SelectionSpy(key: spy),
),
),
);
await tester.pumpAndSettle();
final RenderSelectionSpy renderSelectionSpy =
tester.renderObject<RenderSelectionSpy>(find.byKey(spy));
renderSelectionSpy.events.clear();
final TestGesture gesture =
await tester.startGesture(const Offset(200.0, 200.0));
addTearDown(gesture.removePointer);
await tester.pump(const Duration(milliseconds: 500));
await gesture.cancel();
expect(
renderSelectionSpy.events.any((SelectionEvent element) => element is ClearSelectionEvent),
isFalse,
);
},
);
testWidgets(
'scrolling after the selection does not send ClearSelectionEvent',
(WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/128765
final UniqueKey spy = UniqueKey();
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SizedBox(
height: 750,
child: SingleChildScrollView(
child: SizedBox(
height: 2000,
child: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: SelectionSpy(key: spy),
),
),
),
),
),
);
await tester.pumpAndSettle();
final RenderSelectionSpy renderSelectionSpy = tester.renderObject<RenderSelectionSpy>(find.byKey(spy));
renderSelectionSpy.events.clear();
final TestGesture selectGesture = await tester.startGesture(const Offset(200.0, 200.0));
addTearDown(selectGesture.removePointer);
await tester.pump(const Duration(milliseconds: 500));
await selectGesture.up();
expect(renderSelectionSpy.events.length, 1);
expect(renderSelectionSpy.events[0], isA<SelectWordSelectionEvent>());
renderSelectionSpy.events.clear();
final TestGesture scrollGesture =
await tester.startGesture(const Offset(250.0, 850.0));
await tester.pump(const Duration(milliseconds: 500));
await scrollGesture.moveTo(Offset.zero);
await scrollGesture.up();
await tester.pumpAndSettle();
expect(renderSelectionSpy.events.length, 0);
},
);
testWidgets('mouse long press does not send select-word event', (WidgetTester tester) async {
final UniqueKey spy = UniqueKey();
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: SelectionSpy(key: spy),
),
),
);
await tester.pumpAndSettle();
final RenderSelectionSpy renderSelectionSpy = tester.renderObject<RenderSelectionSpy>(find.byKey(spy));
renderSelectionSpy.events.clear();
final TestGesture gesture = await tester.startGesture(const Offset(200.0, 200.0), kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await tester.pump(const Duration(milliseconds: 500));
await gesture.up();
expect(
renderSelectionSpy.events.every((SelectionEvent element) => element is SelectionEdgeUpdateEvent),
isTrue,
);
});
});
testWidgets('dragging handle or selecting word triggers haptic feedback on Android', (WidgetTester tester) async {
final List<MethodCall> log = <MethodCall>[];
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async {
log.add(methodCall);
return null;
});
addTearDown(() {
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, mockClipboard.handleMethodCall);
});
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: const Text('How are you?'),
),
),
);
await tester.pumpAndSettle();
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)));
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph, 6)); // at the 'r'
addTearDown(gesture.removePointer);
await tester.pump(const Duration(milliseconds: 500));
await gesture.up();
await tester.pump(const Duration(milliseconds: 500));
// `are` is selected.
expect(paragraph.selections[0], const TextSelection(baseOffset: 4, extentOffset: 7));
expect(
log.last,
isMethodCall('HapticFeedback.vibrate', arguments: 'HapticFeedbackType.selectionClick'),
);
log.clear();
final List<TextBox> boxes = paragraph.getBoxesForSelection(paragraph.selections[0]);
expect(boxes.length, 1);
final Offset handlePos = globalize(boxes[0].toRect().bottomRight, paragraph);
await gesture.down(handlePos);
final Offset endPos = Offset(textOffsetToPosition(paragraph, 8).dx, handlePos.dy);
// Select 1 more character by dragging end handle to trigger feedback.
await gesture.moveTo(endPos);
expect(paragraph.selections[0], const TextSelection(baseOffset: 4, extentOffset: 8));
// Only Android vibrate when dragging the handle.
switch (defaultTargetPlatform) {
case TargetPlatform.android:
expect(
log.last,
isMethodCall('HapticFeedback.vibrate', arguments: 'HapticFeedbackType.selectionClick'),
);
case TargetPlatform.fuchsia:
case TargetPlatform.iOS:
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
expect(log, isEmpty);
}
await gesture.up();
}, variant: TargetPlatformVariant.all());
group('SelectionArea integration', () {
testWidgets('mouse can select single text on desktop platforms', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: const Center(
child: Text('How are you'),
),
),
),
);
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you'), matching: find.byType(RichText)));
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph, 2), kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await tester.pump();
await gesture.moveTo(textOffsetToPosition(paragraph, 4));
await tester.pump();
expect(paragraph.selections[0], const TextSelection(baseOffset: 2, extentOffset: 4));
await gesture.moveTo(textOffsetToPosition(paragraph, 6));
await tester.pump();
expect(paragraph.selections[0], const TextSelection(baseOffset: 2, extentOffset: 6));
// Check backward selection.
await gesture.moveTo(textOffsetToPosition(paragraph, 1));
await tester.pump();
expect(paragraph.selections.isEmpty, isFalse);
expect(paragraph.selections[0], const TextSelection(baseOffset: 2, extentOffset: 1));
// Start a new drag.
await gesture.up();
await tester.pumpAndSettle();
await gesture.down(textOffsetToPosition(paragraph, 5));
await tester.pumpAndSettle();
expect(paragraph.selections.isEmpty, isFalse);
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 5));
// Selecting across line should select to the end.
await gesture.moveTo(textOffsetToPosition(paragraph, 5) + const Offset(0.0, 200.0));
await tester.pump();
expect(paragraph.selections[0], const TextSelection(baseOffset: 5, extentOffset: 11));
await gesture.up();
}, variant: TargetPlatformVariant.desktop());
testWidgets('mouse can select single text on mobile platforms', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: const Center(
child: Text('How are you'),
),
),
),
);
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you'), matching: find.byType(RichText)));
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph, 2), kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await tester.pump();
await gesture.moveTo(textOffsetToPosition(paragraph, 4));
await tester.pump();
expect(paragraph.selections[0], const TextSelection(baseOffset: 2, extentOffset: 4));
await gesture.moveTo(textOffsetToPosition(paragraph, 6));
await tester.pump();
expect(paragraph.selections[0], const TextSelection(baseOffset: 2, extentOffset: 6));
// Check backward selection.
await gesture.moveTo(textOffsetToPosition(paragraph, 1));
await tester.pump();
expect(paragraph.selections.isEmpty, isFalse);
expect(paragraph.selections[0], const TextSelection(baseOffset: 2, extentOffset: 1));
// Start a new drag.
await gesture.up();
await tester.pumpAndSettle();
await gesture.down(textOffsetToPosition(paragraph, 5));
await tester.pumpAndSettle();
await gesture.moveTo(textOffsetToPosition(paragraph, 6));
await tester.pump();
expect(paragraph.selections.isEmpty, isFalse);
expect(paragraph.selections[0], const TextSelection(baseOffset: 5, extentOffset: 6));
// Selecting across line should select to the end.
await gesture.moveTo(textOffsetToPosition(paragraph, 5) + const Offset(0.0, 200.0));
await tester.pump();
expect(paragraph.selections[0], const TextSelection(baseOffset: 5, extentOffset: 11));
await gesture.up();
}, variant: TargetPlatformVariant.mobile());
testWidgets('mouse can select word-by-word on double click drag', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: const Center(
child: Text('How are you'),
),
),
),
);
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you'), matching: find.byType(RichText)));
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph, 2), kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await tester.pump();
await gesture.up();
await tester.pump();
await gesture.down(textOffsetToPosition(paragraph, 2));
await tester.pumpAndSettle();
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
await gesture.moveTo(textOffsetToPosition(paragraph, 3));
await tester.pumpAndSettle();
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 4));
await gesture.moveTo(textOffsetToPosition(paragraph, 4));
await tester.pump();
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 7));
await gesture.moveTo(textOffsetToPosition(paragraph, 7));
await tester.pump();
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 8));
await gesture.moveTo(textOffsetToPosition(paragraph, 8));
await tester.pump();
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 11));
// Check backward selection.
await gesture.moveTo(textOffsetToPosition(paragraph, 1));
await tester.pump();
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
// Start a new double-click drag.
await gesture.up();
await tester.pump();
await gesture.down(textOffsetToPosition(paragraph, 5));
await tester.pump();
await gesture.up();
expect(paragraph.selections.isEmpty, isFalse);
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 5));
await tester.pump(kDoubleTapTimeout);
// Double-click.
await gesture.down(textOffsetToPosition(paragraph, 5));
await tester.pump();
await gesture.up();
await tester.pump();
await gesture.down(textOffsetToPosition(paragraph, 5));
await tester.pumpAndSettle();
expect(paragraph.selections[0], const TextSelection(baseOffset: 4, extentOffset: 7));
// Selecting across line should select to the end.
await gesture.moveTo(textOffsetToPosition(paragraph, 5) + const Offset(0.0, 200.0));
await tester.pump();
expect(paragraph.selections[0], const TextSelection(baseOffset: 4, extentOffset: 11));
await gesture.up();
}, skip: kIsWeb); // https://github.com/flutter/flutter/issues/125582.
testWidgets('mouse can select multiple widgets on double click drag', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: const Column(
children: <Widget>[
Text('How are you?'),
Text('Good, and you?'),
Text('Fine, thank you.'),
],
),
),
),
);
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)));
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph1, 2), kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await tester.pump();
await gesture.up();
await tester.pump();
await gesture.down(textOffsetToPosition(paragraph1, 2));
await tester.pumpAndSettle();
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
await gesture.moveTo(textOffsetToPosition(paragraph1, 4));
await tester.pump();
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 7));
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)));
await gesture.moveTo(textOffsetToPosition(paragraph2, 5));
// Should select the rest of paragraph 1.
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 12));
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 6));
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText)));
await gesture.moveTo(textOffsetToPosition(paragraph3, 6));
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 12));
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 14));
expect(paragraph3.selections[0], const TextSelection(baseOffset: 0, extentOffset: 11));
await gesture.up();
}, skip: kIsWeb); // https://github.com/flutter/flutter/issues/125582.
testWidgets('mouse can select multiple widgets on double click drag and return to origin word', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: const Column(
children: <Widget>[
Text('How are you?'),
Text('Good, and you?'),
Text('Fine, thank you.'),
],
),
),
),
);
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)));
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph1, 2), kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await tester.pump();
await gesture.up();
await tester.pump();
await gesture.down(textOffsetToPosition(paragraph1, 2));
await tester.pumpAndSettle();
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
await gesture.moveTo(textOffsetToPosition(paragraph1, 4));
await tester.pump();
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 7));
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)));
await gesture.moveTo(textOffsetToPosition(paragraph2, 5));
// Should select the rest of paragraph 1.
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 12));
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 6));
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText)));
await gesture.moveTo(textOffsetToPosition(paragraph3, 6));
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 12));
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 14));
expect(paragraph3.selections[0], const TextSelection(baseOffset: 0, extentOffset: 11));
await gesture.moveTo(textOffsetToPosition(paragraph2, 5));
// Should clear the selection on paragraph 3.
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 12));
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 6));
expect(paragraph3.selections.isEmpty, isTrue);
await gesture.moveTo(textOffsetToPosition(paragraph1, 4));
// Should clear the selection on paragraph 2.
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 7));
expect(paragraph2.selections.isEmpty, isTrue);
expect(paragraph3.selections.isEmpty, isTrue);
await gesture.up();
}, skip: kIsWeb); // https://github.com/flutter/flutter/issues/125582.
testWidgets('mouse can reverse selection across multiple widgets on double click drag', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: const Column(
children: <Widget>[
Text('How are you?'),
Text('Good, and you?'),
Text('Fine, thank you.'),
],
),
),
),
);
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText)));
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph3, 10), kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await tester.pump();
await gesture.up();
await tester.pump();
await gesture.down(textOffsetToPosition(paragraph3, 10));
await tester.pumpAndSettle();
expect(paragraph3.selections[0], const TextSelection(baseOffset: 6, extentOffset: 11));
await gesture.moveTo(textOffsetToPosition(paragraph3, 4));
await tester.pump();
expect(paragraph3.selections[0], const TextSelection(baseOffset: 11, extentOffset: 4));
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)));
await gesture.moveTo(textOffsetToPosition(paragraph2, 5));
expect(paragraph3.selections[0], const TextSelection(baseOffset: 11, extentOffset: 0));
expect(paragraph2.selections[0], const TextSelection(baseOffset: 14, extentOffset: 5));
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)));
await gesture.moveTo(textOffsetToPosition(paragraph1, 6));
expect(paragraph3.selections[0], const TextSelection(baseOffset: 11, extentOffset: 0));
expect(paragraph2.selections[0], const TextSelection(baseOffset: 14, extentOffset: 0));
expect(paragraph1.selections[0], const TextSelection(baseOffset: 12, extentOffset: 4));
await gesture.up();
}, skip: kIsWeb); // https://github.com/flutter/flutter/issues/125582.
testWidgets('mouse can select paragraph-by-paragraph on triple click drag', (WidgetTester tester) async {
const String longText = 'Hello world this is some long piece of text '
'that will represent a long paragraph, when triple clicking this block '
'of text all of it will be selected.\n'
'This will be the start of a new line. When triple clicking this block '
'of text all of it should be selected.';
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: const Center(
child: Text(longText),
),
),
),
);
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(find.descendant(of: find.text(longText), matching: find.byType(RichText)));
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph, 2), kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await tester.pump();
await gesture.up();
await tester.pump();
await gesture.down(textOffsetToPosition(paragraph, 2));
await tester.pump();
await gesture.up();
await tester.pump();
await gesture.down(textOffsetToPosition(paragraph, 2));
await tester.pumpAndSettle();
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 150));
await gesture.moveTo(textOffsetToPosition(paragraph, 155));
await tester.pumpAndSettle();
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 257));
await gesture.moveTo(textOffsetToPosition(paragraph, 170));
await tester.pump();
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 257));
// Check backward selection.
await gesture.moveTo(textOffsetToPosition(paragraph, 1));
await tester.pump();
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 150));
// Start a new triple-click drag.
await gesture.up();
await tester.pumpAndSettle(kDoubleTapTimeout);
await gesture.down(textOffsetToPosition(paragraph, 151));
await tester.pumpAndSettle();
await gesture.up();
expect(paragraph.selections.isNotEmpty, isTrue);
expect(paragraph.selections.length, 1);
expect(paragraph.selections.first, const TextSelection.collapsed(offset: 151));
await tester.pump(kDoubleTapTimeout);
// Triple-click.
await gesture.down(textOffsetToPosition(paragraph, 151));
await tester.pump();
await gesture.up();
await tester.pump();
await gesture.down(textOffsetToPosition(paragraph, 151));
await tester.pump();
await gesture.up();
await tester.pump();
await gesture.down(textOffsetToPosition(paragraph, 151));
await tester.pumpAndSettle();
expect(paragraph.selections[0], const TextSelection(baseOffset: 150, extentOffset: 257));
// Selecting across line should select to the end.
await gesture.moveTo(textOffsetToPosition(paragraph, 5) + const Offset(0.0, -200.0));
await tester.pump();
expect(paragraph.selections[0], const TextSelection(baseOffset: 257, extentOffset: 0));
await gesture.up();
}, skip: kIsWeb); // https://github.com/flutter/flutter/issues/125582.
testWidgets('mouse can select multiple widgets on triple click drag when selecting inside a WidgetSpan', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: const Text.rich(
WidgetSpan(
child: Column(
children: <Widget>[
Text('Text widget A.'),
Text('Text widget B.'),
Text('Text widget C.'),
Text('Text widget D.'),
Text('Text widget E.'),
],
),
),
),
),
),
);
final RenderParagraph paragraphC = tester.renderObject<RenderParagraph>(find.descendant(of: find.textContaining('Text widget C.'), matching: find.byType(RichText)));
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraphC, 2), kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await tester.pump();
await gesture.up();
await tester.pump();
await gesture.down(textOffsetToPosition(paragraphC, 2));
await tester.pump();
await gesture.up();
await tester.pump();
await gesture.down(textOffsetToPosition(paragraphC, 2));
await tester.pumpAndSettle();
expect(paragraphC.selections[0], const TextSelection(baseOffset: 0, extentOffset: 14));
await gesture.moveTo(textOffsetToPosition(paragraphC, 7));
await tester.pump();
expect(paragraphC.selections[0], const TextSelection(baseOffset: 0, extentOffset: 14));
final RenderParagraph paragraphE = tester.renderObject<RenderParagraph>(find.descendant(of: find.textContaining('Text widget E.'), matching: find.byType(RichText)));
final RenderParagraph paragraphD = tester.renderObject<RenderParagraph>(find.descendant(of: find.textContaining('Text widget D.'), matching: find.byType(RichText)));
await gesture.moveTo(textOffsetToPosition(paragraphE, 5));
// Should select line C-E.
expect(paragraphC.selections[0], const TextSelection(baseOffset: 0, extentOffset: 14));
expect(paragraphD.selections[0], const TextSelection(baseOffset: 0, extentOffset: 14));
expect(paragraphE.selections[0], const TextSelection(baseOffset: 0, extentOffset: 14));
await gesture.up();
}, skip: kIsWeb); // https://github.com/flutter/flutter/issues/125582.
testWidgets('mouse can select multiple widgets on triple click drag', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: const Column(
children: <Widget>[
Text('How are you?\nThis is the first text widget.'),
Text('Good, and you?\nThis is the second text widget.'),
Text('Fine, thank you.\nThis is the third text widget.'),
],
),
),
),
);
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(find.descendant(of: find.textContaining('first text widget'), matching: find.byType(RichText)));
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph1, 2), kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await tester.pump();
await gesture.up();
await tester.pump();
await gesture.down(textOffsetToPosition(paragraph1, 2));
await tester.pump();
await gesture.up();
await tester.pump();
await gesture.down(textOffsetToPosition(paragraph1, 2));
await tester.pumpAndSettle();
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 13));
await gesture.moveTo(textOffsetToPosition(paragraph1, 14));
await tester.pump();
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 43));
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(find.descendant(of: find.textContaining('second text widget'), matching: find.byType(RichText)));
await gesture.moveTo(textOffsetToPosition(paragraph2, 5));
// Should select line 1 of text widget 2.
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 43));
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 15));
await gesture.moveTo(textOffsetToPosition(paragraph2, 16));
// Should select the rest of text widget 2.
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 43));
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 46));
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(find.descendant(of: find.textContaining('third text widget'), matching: find.byType(RichText)));
await gesture.moveTo(textOffsetToPosition(paragraph3, 6));
// Should select line 1 of text widget 3.
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 43));
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 46));
expect(paragraph3.selections[0], const TextSelection(baseOffset: 0, extentOffset: 17));
await gesture.moveTo(textOffsetToPosition(paragraph3, 18));
// Should select the rest of text widget 3.
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 43));
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 46));
expect(paragraph3.selections[0], const TextSelection(baseOffset: 0, extentOffset: 47));
await gesture.up();
}, skip: kIsWeb); // https://github.com/flutter/flutter/issues/125582.
testWidgets('mouse can select multiple widgets on triple click drag and return to origin paragraph', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: const Column(
children: <Widget>[
Text('How are you?\nThis is the first text widget.'),
Text('Good, and you?\nThis is the second text widget.'),
Text('Fine, thank you.\nThis is the third text widget.'),
],
),
),
),
);
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(find.descendant(of: find.textContaining('second text widget'), matching: find.byType(RichText)));
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph2, 2), kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await tester.pump();
await gesture.up();
await tester.pump();
await gesture.down(textOffsetToPosition(paragraph2, 2));
await tester.pump();
await gesture.up();
await tester.pump();
await gesture.down(textOffsetToPosition(paragraph2, 2));
await tester.pumpAndSettle();
// Should select line 1 of text widget 2.
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 15));
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(find.descendant(of: find.textContaining('first text widget'), matching: find.byType(RichText)));
// Should select line 2 of text widget 1.
await gesture.moveTo(textOffsetToPosition(paragraph1, 14));
await tester.pump();
expect(paragraph1.selections[0], const TextSelection(baseOffset: 43, extentOffset: 13));
expect(paragraph2.selections[0], const TextSelection(baseOffset: 15, extentOffset: 0));
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(find.descendant(of: find.textContaining('third text widget'), matching: find.byType(RichText)));
await gesture.moveTo(textOffsetToPosition(paragraph1, 5));
// Should select rest of text widget 1.
expect(paragraph1.selections[0], const TextSelection(baseOffset: 43, extentOffset: 0));
expect(paragraph2.selections[0], const TextSelection(baseOffset: 15, extentOffset: 0));
await gesture.moveTo(textOffsetToPosition(paragraph2, 2));
// Should clear the selection on paragraph 1 and return to the origin paragraph.
expect(paragraph1.selections.isEmpty, true);
expect(paragraph2.selections[0], const TextSelection(baseOffset: 15, extentOffset: 0));
await gesture.moveTo(textOffsetToPosition(paragraph3, 6));
// Should select line 1 of text widget 3.
expect(paragraph1.selections.isEmpty, true);
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 46));
expect(paragraph3.selections[0], const TextSelection(baseOffset: 0, extentOffset: 17));
await gesture.moveTo(textOffsetToPosition(paragraph3, 18));
// Should select line 2 of text widget 3.
expect(paragraph1.selections.isEmpty, true);
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 46));
expect(paragraph3.selections[0], const TextSelection(baseOffset: 0, extentOffset: 47));
await gesture.moveTo(textOffsetToPosition(paragraph2, 5));
// Should clear the selection on paragraph 3 and return to the origin paragraph.
expect(paragraph1.selections.isEmpty, true);
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 15));
expect(paragraph3.selections.isEmpty, true);
await gesture.up();
}, skip: kIsWeb); // https://github.com/flutter/flutter/issues/125582.
testWidgets('mouse can reverse selection across multiple widgets on triple click drag', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: const Column(
children: <Widget>[
Text('How are you?\nThis is the first text widget.'),
Text('Good, and you?\nThis is the second text widget.'),
Text('Fine, thank you.\nThis is the third text widget.'),
],
),
),
),
);
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(find.descendant(of: find.textContaining('Fine, thank you.'), matching: find.byType(RichText)));
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph3, 18), kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await tester.pump();
await gesture.up();
await tester.pump();
await gesture.down(textOffsetToPosition(paragraph3, 18));
await tester.pump();
await gesture.up();
await tester.pump();
await gesture.down(textOffsetToPosition(paragraph3, 18));
await tester.pumpAndSettle();
expect(paragraph3.selections[0], const TextSelection(baseOffset: 17, extentOffset: 47));
await gesture.moveTo(textOffsetToPosition(paragraph3, 4));
await tester.pump();
expect(paragraph3.selections[0], const TextSelection(baseOffset: 47, extentOffset: 0));
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(find.descendant(of: find.textContaining('Good, and you?'), matching: find.byType(RichText)));
await gesture.moveTo(textOffsetToPosition(paragraph2, 5));
expect(paragraph3.selections[0], const TextSelection(baseOffset: 47, extentOffset: 0));
expect(paragraph2.selections[0], const TextSelection(baseOffset: 46, extentOffset: 0));
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(find.descendant(of: find.textContaining('How are you?'), matching: find.byType(RichText)));
await gesture.moveTo(textOffsetToPosition(paragraph1, 6));
expect(paragraph3.selections[0], const TextSelection(baseOffset: 47, extentOffset: 0));
expect(paragraph2.selections[0], const TextSelection(baseOffset: 46, extentOffset: 0));
expect(paragraph1.selections[0], const TextSelection(baseOffset: 43, extentOffset: 0));
await gesture.up();
}, skip: kIsWeb); // https://github.com/flutter/flutter/issues/125582.
testWidgets('mouse can select multiple widgets', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: const Column(
children: <Widget>[
Text('How are you?'),
Text('Good, and you?'),
Text('Fine, thank you.'),
],
),
),
),
);
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)));
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph1, 2), kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await tester.pump();
await gesture.moveTo(textOffsetToPosition(paragraph1, 4));
await tester.pump();
expect(paragraph1.selections[0], const TextSelection(baseOffset: 2, extentOffset: 4));
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)));
await gesture.moveTo(textOffsetToPosition(paragraph2, 5));
// Should select the rest of paragraph 1.
expect(paragraph1.selections[0], const TextSelection(baseOffset: 2, extentOffset: 12));
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 5));
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText)));
await gesture.moveTo(textOffsetToPosition(paragraph3, 6));
expect(paragraph1.selections[0], const TextSelection(baseOffset: 2, extentOffset: 12));
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 14));
expect(paragraph3.selections[0], const TextSelection(baseOffset: 0, extentOffset: 6));
await gesture.up();
});
testWidgets('collapsing selection should clear selection of all other selectables', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: const Column(
children: <Widget>[
Text('How are you?'),
Text('Good, and you?'),
Text('Fine, thank you.'),
],
),
),
),
);
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)));
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph1, 2), kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(paragraph1.selections[0], const TextSelection.collapsed(offset: 2));
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)));
await gesture.down(textOffsetToPosition(paragraph2, 5));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(paragraph1.selections.isEmpty, isTrue);
expect(paragraph2.selections[0], const TextSelection.collapsed(offset: 5));
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText)));
await gesture.down(textOffsetToPosition(paragraph3, 13));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(paragraph1.selections.isEmpty, isTrue);
expect(paragraph2.selections.isEmpty, isTrue);
expect(paragraph3.selections[0], const TextSelection.collapsed(offset: 13));
});
testWidgets('mouse can work with disabled container', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: const Column(
children: <Widget>[
Text('How are you?'),
SelectionContainer.disabled(child: Text('Good, and you?')),
Text('Fine, thank you.'),
],
),
),
),
);
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)));
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph1, 2), kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await tester.pump();
await gesture.moveTo(textOffsetToPosition(paragraph1, 4));
await tester.pump();
expect(paragraph1.selections[0], const TextSelection(baseOffset: 2, extentOffset: 4));
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)));
await gesture.moveTo(textOffsetToPosition(paragraph2, 5));
// Should select the rest of paragraph 1.
expect(paragraph1.selections[0], const TextSelection(baseOffset: 2, extentOffset: 12));
// paragraph2 is in a disabled container.
expect(paragraph2.selections.isEmpty, isTrue);
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText)));
await gesture.moveTo(textOffsetToPosition(paragraph3, 6));
expect(paragraph1.selections[0], const TextSelection(baseOffset: 2, extentOffset: 12));
expect(paragraph2.selections.isEmpty, isTrue);
expect(paragraph3.selections[0], const TextSelection(baseOffset: 0, extentOffset: 6));
await gesture.up();
});
testWidgets('mouse can reverse selection', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: const Column(
children: <Widget>[
Text('How are you?'),
Text('Good, and you?'),
Text('Fine, thank you.'),
],
),
),
),
);
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText)));
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph3, 10), kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await tester.pump();
await gesture.moveTo(textOffsetToPosition(paragraph3, 4));
await tester.pump();
expect(paragraph3.selections[0], const TextSelection(baseOffset: 10, extentOffset: 4));
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)));
await gesture.moveTo(textOffsetToPosition(paragraph2, 5));
expect(paragraph3.selections[0], const TextSelection(baseOffset: 10, extentOffset: 0));
expect(paragraph2.selections[0], const TextSelection(baseOffset: 14, extentOffset: 5));
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)));
await gesture.moveTo(textOffsetToPosition(paragraph1, 6));
expect(paragraph3.selections[0], const TextSelection(baseOffset: 10, extentOffset: 0));
expect(paragraph2.selections[0], const TextSelection(baseOffset: 14, extentOffset: 0));
expect(paragraph1.selections[0], const TextSelection(baseOffset: 12, extentOffset: 6));
await gesture.up();
});
testWidgets(
'long press selection overlay behavior on iOS and Android',
(WidgetTester tester) async {
// This test verifies that all platforms wait until long press end to
// show the context menu, and only Android waits until long press end to
// show the selection handles.
final bool isPlatformAndroid = defaultTargetPlatform == TargetPlatform.android;
Set<ContextMenuButtonType> buttonTypes = <ContextMenuButtonType>{};
final UniqueKey toolbarKey = UniqueKey();
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionHandleControls,
contextMenuBuilder: (
BuildContext context,
SelectableRegionState selectableRegionState,
) {
buttonTypes = selectableRegionState.contextMenuButtonItems
.map((ContextMenuButtonItem buttonItem) => buttonItem.type)
.toSet();
return SizedBox.shrink(key: toolbarKey);
},
child: const Text('How are you?'),
),
),
);
expect(buttonTypes.isEmpty, true);
expect(find.byKey(toolbarKey), findsNothing);
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)));
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph, 2));
addTearDown(gesture.removePointer);
await tester.pump(const Duration(milliseconds: 500));
await tester.pumpAndSettle();
// All platform except Android should show the selection handles when the
// long press starts.
List<FadeTransition> transitions = find.descendant(
of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_SelectionHandleOverlay'),
matching: find.byType(FadeTransition),
).evaluate().map((Element e) => e.widget).cast<FadeTransition>().toList();
expect(transitions.length, isPlatformAndroid ? 0 : 2);
FadeTransition? left;
FadeTransition? right;
if (!isPlatformAndroid) {
left = transitions[0];
right = transitions[1];
expect(left.opacity.value, equals(1.0));
expect(right.opacity.value, equals(1.0));
}
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
expect(find.byKey(toolbarKey), findsNothing);
await gesture.moveTo(textOffsetToPosition(paragraph, 8));
await tester.pumpAndSettle();
transitions = find.descendant(
of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_SelectionHandleOverlay'),
matching: find.byType(FadeTransition),
).evaluate().map((Element e) => e.widget).cast<FadeTransition>().toList();
// All platform except Android should show the selection handles while doing
// a long press drag.
expect(transitions.length, isPlatformAndroid ? 0 : 2);
if (!isPlatformAndroid) {
left = transitions[0];
right = transitions[1];
expect(left.opacity.value, equals(1.0));
expect(right.opacity.value, equals(1.0));
}
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 11));
expect(find.byKey(toolbarKey), findsNothing);
await gesture.up();
await tester.pumpAndSettle();
transitions = find.descendant(
of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_SelectionHandleOverlay'),
matching: find.byType(FadeTransition),
).evaluate().map((Element e) => e.widget).cast<FadeTransition>().toList();
expect(transitions.length, 2);
left = transitions[0];
right = transitions[1];
// All platforms should show the selection handles and context menu when
// the long press ends.
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 11));
expect(left.opacity.value, equals(1.0));
expect(right.opacity.value, equals(1.0));
expect(find.byKey(toolbarKey), findsOneWidget);
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.iOS }),
skip: kIsWeb, // [intended] Web uses its native context menu.
);
testWidgets(
'single tap on the previous selection toggles the toolbar on iOS',
(WidgetTester tester) async {
Set<ContextMenuButtonType> buttonTypes = <ContextMenuButtonType>{};
final UniqueKey toolbarKey = UniqueKey();
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionHandleControls,
contextMenuBuilder: (
BuildContext context,
SelectableRegionState selectableRegionState,
) {
buttonTypes = selectableRegionState.contextMenuButtonItems
.map((ContextMenuButtonItem buttonItem) => buttonItem.type)
.toSet();
return SizedBox.shrink(key: toolbarKey);
},
child: const Column(
children: <Widget>[
Text('How are you?'),
Text('Good, and you?'),
Text('Fine, thank you.'),
],
),
),
),
);
expect(buttonTypes.isEmpty, true);
expect(find.byKey(toolbarKey), findsNothing);
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)));
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph, 2));
addTearDown(gesture.removePointer);
await tester.pump(const Duration(milliseconds: 500));
await gesture.up();
await tester.pumpAndSettle();
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
expect(buttonTypes, contains(ContextMenuButtonType.copy));
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
expect(find.byKey(toolbarKey), findsOneWidget);
await gesture.down(textOffsetToPosition(paragraph, 2));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
expect(buttonTypes, contains(ContextMenuButtonType.copy));
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
expect(find.byKey(toolbarKey), findsNothing);
await gesture.down(textOffsetToPosition(paragraph, 2));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
expect(buttonTypes, contains(ContextMenuButtonType.copy));
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
expect(find.byKey(toolbarKey), findsOneWidget);
// Collapse selection.
await tester.tapAt(textOffsetToPosition(paragraph, 9));
await tester.pump();
expect(paragraph.selections.isEmpty, isFalse);
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 9));
expect(find.byKey(toolbarKey), findsNothing);
},
variant: TargetPlatformVariant.only(TargetPlatform.iOS),
skip: kIsWeb, // [intended] Web uses its native context menu.
);
testWidgets(
'right-click mouse can select word at position on Apple platforms',
(WidgetTester tester) async {
Set<ContextMenuButtonType> buttonTypes = <ContextMenuButtonType>{};
final UniqueKey toolbarKey = UniqueKey();
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionHandleControls,
contextMenuBuilder: (
BuildContext context,
SelectableRegionState selectableRegionState,
) {
buttonTypes = selectableRegionState.contextMenuButtonItems
.map((ContextMenuButtonItem buttonItem) => buttonItem.type)
.toSet();
return SizedBox.shrink(key: toolbarKey);
},
child: const Center(
child: Text('How are you'),
),
),
),
);
expect(buttonTypes.isEmpty, true);
expect(find.byKey(toolbarKey), findsNothing);
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you'), matching: find.byType(RichText)));
final TestGesture primaryMouseButtonGesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph, 2), kind: PointerDeviceKind.mouse, buttons: kSecondaryMouseButton);
addTearDown(primaryMouseButtonGesture.removePointer);
addTearDown(gesture.removePointer);
await tester.pump();
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
await gesture.up();
await tester.pump();
expect(buttonTypes, contains(ContextMenuButtonType.copy));
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
expect(find.byKey(toolbarKey), findsOneWidget);
await gesture.down(textOffsetToPosition(paragraph, 6));
await tester.pump();
expect(paragraph.selections[0], const TextSelection(baseOffset: 4, extentOffset: 7));
await gesture.up();
await tester.pump();
expect(buttonTypes, contains(ContextMenuButtonType.copy));
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
expect(find.byKey(toolbarKey), findsOneWidget);
await gesture.down(textOffsetToPosition(paragraph, 9));
await tester.pump();
expect(paragraph.selections[0], const TextSelection(baseOffset: 8, extentOffset: 11));
await gesture.up();
await tester.pump();
expect(buttonTypes, contains(ContextMenuButtonType.copy));
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
expect(find.byKey(toolbarKey), findsOneWidget);
// Collapse selection.
await primaryMouseButtonGesture.down(textOffsetToPosition(paragraph, 1));
await tester.pump();
await primaryMouseButtonGesture.up();
await tester.pumpAndSettle();
// Selection is collapsed.
expect(paragraph.selections.isEmpty, false);
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 1));
expect(find.byKey(toolbarKey), findsNothing);
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
skip: kIsWeb, // [intended] Web uses its native context menu.
);
testWidgets(
'right-click mouse at the same position as previous right-click toggles the context menu on macOS',
(WidgetTester tester) async {
Set<ContextMenuButtonType> buttonTypes = <ContextMenuButtonType>{};
final UniqueKey toolbarKey = UniqueKey();
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionHandleControls,
contextMenuBuilder: (
BuildContext context,
SelectableRegionState selectableRegionState,
) {
buttonTypes = selectableRegionState.contextMenuButtonItems
.map((ContextMenuButtonItem buttonItem) => buttonItem.type)
.toSet();
return SizedBox.shrink(key: toolbarKey);
},
child: const Center(
child: Text('How are you'),
),
),
),
);
expect(buttonTypes.isEmpty, true);
expect(find.byKey(toolbarKey), findsNothing);
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you'), matching: find.byType(RichText)));
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph, 2), kind: PointerDeviceKind.mouse, buttons: kSecondaryMouseButton);
final TestGesture primaryMouseButtonGesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
addTearDown(primaryMouseButtonGesture.removePointer);
addTearDown(gesture.removePointer);
await tester.pump();
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
await gesture.up();
await tester.pump();
expect(buttonTypes, contains(ContextMenuButtonType.copy));
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
expect(find.byKey(toolbarKey), findsOneWidget);
await gesture.down(textOffsetToPosition(paragraph, 2));
await tester.pump();
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
await gesture.up();
await tester.pump();
// Right-click at same position will toggle the context menu off.
expect(buttonTypes, contains(ContextMenuButtonType.copy));
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
expect(find.byKey(toolbarKey), findsNothing);
await gesture.down(textOffsetToPosition(paragraph, 9));
await tester.pump();
expect(paragraph.selections[0], const TextSelection(baseOffset: 8, extentOffset: 11));
await gesture.up();
await tester.pump();
expect(buttonTypes, contains(ContextMenuButtonType.copy));
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
expect(find.byKey(toolbarKey), findsOneWidget);
await gesture.down(textOffsetToPosition(paragraph, 9));
await tester.pump();
expect(paragraph.selections[0], const TextSelection(baseOffset: 8, extentOffset: 11));
await gesture.up();
await tester.pump();
// Right-click at same position will toggle the context menu off.
expect(buttonTypes, contains(ContextMenuButtonType.copy));
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
expect(find.byKey(toolbarKey), findsNothing);
await gesture.down(textOffsetToPosition(paragraph, 6));
await tester.pump();
expect(paragraph.selections[0], const TextSelection(baseOffset: 4, extentOffset: 7));
await gesture.up();
await tester.pump();
expect(buttonTypes, contains(ContextMenuButtonType.copy));
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
expect(find.byKey(toolbarKey), findsOneWidget);
// Collapse selection.
await primaryMouseButtonGesture.down(textOffsetToPosition(paragraph, 1));
await tester.pump();
await primaryMouseButtonGesture.up();
await tester.pumpAndSettle();
// Selection is collapsed.
expect(paragraph.selections.isEmpty, false);
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 1));
expect(find.byKey(toolbarKey), findsNothing);
},
variant: TargetPlatformVariant.only(TargetPlatform.macOS),
skip: kIsWeb, // [intended] Web uses its native context menu.
);
testWidgets(
'right-click mouse shows the context menu at position on Android, Fuchsia, and Windows',
(WidgetTester tester) async {
Set<ContextMenuButtonType> buttonTypes = <ContextMenuButtonType>{};
final UniqueKey toolbarKey = UniqueKey();
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionHandleControls,
contextMenuBuilder: (
BuildContext context,
SelectableRegionState selectableRegionState,
) {
buttonTypes = selectableRegionState.contextMenuButtonItems
.map((ContextMenuButtonItem buttonItem) => buttonItem.type)
.toSet();
return SizedBox.shrink(key: toolbarKey);
},
child: const Center(
child: Text('How are you'),
),
),
),
);
expect(buttonTypes.isEmpty, true);
expect(find.byKey(toolbarKey), findsNothing);
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you'), matching: find.byType(RichText)));
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph, 2), kind: PointerDeviceKind.mouse, buttons: kSecondaryMouseButton);
final TestGesture primaryMouseButtonGesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
addTearDown(primaryMouseButtonGesture.removePointer);
addTearDown(gesture.removePointer);
await tester.pump();
await gesture.up();
await tester.pump();
// Selection is collapsed.
expect(paragraph.selections.isEmpty, false);
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 2));
expect(buttonTypes.length, 1);
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
expect(find.byKey(toolbarKey), findsOneWidget);
await gesture.down(textOffsetToPosition(paragraph, 6));
await tester.pump();
expect(paragraph.selections.isEmpty, false);
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 6));
await gesture.up();
await tester.pump();
expect(buttonTypes.length, 1);
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
expect(find.byKey(toolbarKey), findsOneWidget);
await gesture.down(textOffsetToPosition(paragraph, 9));
await tester.pump();
expect(paragraph.selections.isEmpty, false);
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 9));
await gesture.up();
await tester.pump();
expect(buttonTypes.length, 1);
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
expect(find.byKey(toolbarKey), findsOneWidget);
// Collapse selection.
await primaryMouseButtonGesture.down(textOffsetToPosition(paragraph, 1));
await tester.pump();
await primaryMouseButtonGesture.up();
await tester.pumpAndSettle(kDoubleTapTimeout);
// Selection is collapsed.
expect(paragraph.selections.isEmpty, false);
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 1));
expect(find.byKey(toolbarKey), findsNothing);
// Create an uncollapsed selection by dragging.
await primaryMouseButtonGesture.down(textOffsetToPosition(paragraph, 0));
await tester.pump();
await primaryMouseButtonGesture.moveTo(textOffsetToPosition(paragraph, 5));
await tester.pump();
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 5));
await primaryMouseButtonGesture.up();
await tester.pump();
// Right click on previous selection should not collapse the selection.
await gesture.down(textOffsetToPosition(paragraph, 2));
await tester.pump();
await gesture.up();
await tester.pump();
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 5));
expect(find.byKey(toolbarKey), findsOneWidget);
// Right click anywhere outside previous selection should collapse the
// selection.
await gesture.down(textOffsetToPosition(paragraph, 7));
await tester.pump();
await gesture.up();
await tester.pump();
expect(paragraph.selections.isEmpty, false);
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 7));
expect(find.byKey(toolbarKey), findsOneWidget);
// Collapse selection.
await primaryMouseButtonGesture.down(textOffsetToPosition(paragraph, 1));
await tester.pump();
await primaryMouseButtonGesture.up();
await tester.pumpAndSettle();
// Selection is collapsed.
expect(paragraph.selections.isEmpty, false);
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 1));
expect(find.byKey(toolbarKey), findsNothing);
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.windows }),
skip: kIsWeb, // [intended] Web uses its native context menu.
);
testWidgets(
'right-click mouse toggles the context menu on Linux',
(WidgetTester tester) async {
Set<ContextMenuButtonType> buttonTypes = <ContextMenuButtonType>{};
final UniqueKey toolbarKey = UniqueKey();
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionHandleControls,
contextMenuBuilder: (
BuildContext context,
SelectableRegionState selectableRegionState,
) {
buttonTypes = selectableRegionState.contextMenuButtonItems
.map((ContextMenuButtonItem buttonItem) => buttonItem.type)
.toSet();
return SizedBox.shrink(key: toolbarKey);
},
child: const Center(
child: Text('How are you'),
),
),
),
);
expect(buttonTypes.isEmpty, true);
expect(find.byKey(toolbarKey), findsNothing);
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you'), matching: find.byType(RichText)));
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph, 2), kind: PointerDeviceKind.mouse, buttons: kSecondaryMouseButton);
final TestGesture primaryMouseButtonGesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
addTearDown(primaryMouseButtonGesture.removePointer);
addTearDown(gesture.removePointer);
await tester.pump();
await gesture.up();
await tester.pump();
// Selection is collapsed.
expect(paragraph.selections.isEmpty, false);
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 2));
// Context menu toggled on.
expect(buttonTypes.length, 1);
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
expect(find.byKey(toolbarKey), findsOneWidget);
await gesture.down(textOffsetToPosition(paragraph, 6));
await tester.pump();
await gesture.up();
await tester.pump();
expect(paragraph.selections.isEmpty, false);
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 2));
// Context menu toggled off. Selection remains the same.
expect(find.byKey(toolbarKey), findsNothing);
await gesture.down(textOffsetToPosition(paragraph, 9));
await tester.pump();
expect(paragraph.selections.isEmpty, false);
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 9));
await gesture.up();
await tester.pump();
// Context menu toggled on.
expect(buttonTypes.length, 1);
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
expect(find.byKey(toolbarKey), findsOneWidget);
// Collapse selection.
await primaryMouseButtonGesture.down(textOffsetToPosition(paragraph, 1));
await tester.pump();
await primaryMouseButtonGesture.up();
await tester.pumpAndSettle(kDoubleTapTimeout);
// Selection is collapsed.
expect(paragraph.selections.isEmpty, false);
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 1));
expect(find.byKey(toolbarKey), findsNothing);
await primaryMouseButtonGesture.down(textOffsetToPosition(paragraph, 0));
await tester.pump();
await primaryMouseButtonGesture.moveTo(textOffsetToPosition(paragraph, 5));
await tester.pump();
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 5));
await primaryMouseButtonGesture.up();
await tester.pump();
// Right click on previous selection should not collapse the selection.
await gesture.down(textOffsetToPosition(paragraph, 2));
await tester.pump();
await gesture.up();
await tester.pump();
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 5));
expect(find.byKey(toolbarKey), findsOneWidget);
// Right click anywhere outside previous selection should first toggle the context
// menu off.
await gesture.down(textOffsetToPosition(paragraph, 7));
await tester.pump();
await gesture.up();
await tester.pump();
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 5));
expect(find.byKey(toolbarKey), findsNothing);
// Right click again should collapse the selection and toggle the context
// menu on.
await gesture.down(textOffsetToPosition(paragraph, 7));
await tester.pump();
await gesture.up();
await tester.pump();
expect(paragraph.selections.isEmpty, false);
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 7));
expect(find.byKey(toolbarKey), findsOneWidget);
// Collapse selection.
await primaryMouseButtonGesture.down(textOffsetToPosition(paragraph, 1));
await tester.pump();
await primaryMouseButtonGesture.up();
await tester.pumpAndSettle();
// Selection is collapsed.
expect(paragraph.selections.isEmpty, false);
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 1));
expect(find.byKey(toolbarKey), findsNothing);
},
variant: TargetPlatformVariant.only(TargetPlatform.linux),
skip: kIsWeb, // [intended] Web uses its native context menu.
);
testWidgets('can copy a selection made with the mouse', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: const Column(
children: <Widget>[
Text('How are you?'),
Text('Good, and you?'),
Text('Fine, thank you.'),
],
),
),
),
);
// Select from offset 2 of paragraph 1 to offset 6 of paragraph3.
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)));
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph1, 2), kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await tester.pump();
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText)));
await gesture.moveTo(textOffsetToPosition(paragraph3, 6));
await gesture.up();
// keyboard copy.
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.keyC, control: true));
final Map<String, dynamic> clipboardData = mockClipboard.clipboardData as Map<String, dynamic>;
expect(clipboardData['text'], 'w are you?Good, and you?Fine, ');
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.windows, TargetPlatform.linux, TargetPlatform.fuchsia }));
testWidgets(
'does not override TextField keyboard shortcuts if the TextField is focused - non apple',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(text: 'I am fine, thank you.');
addTearDown(controller.dispose);
final FocusNode selectableRegionFocus = FocusNode();
addTearDown(selectableRegionFocus.dispose);
final FocusNode textFieldFocus = FocusNode();
addTearDown(textFieldFocus.dispose);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: SelectableRegion(
focusNode: selectableRegionFocus,
selectionControls: materialTextSelectionControls,
child: Column(
children: <Widget>[
const Text('How are you?'),
const Text('Good, and you?'),
TextField(controller: controller, focusNode: textFieldFocus),
],
),
),
),
),
);
textFieldFocus.requestFocus();
await tester.pump();
// Make sure keyboard select all works on TextField.
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.keyA, control: true));
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 21));
// Make sure no selection in SelectableRegion.
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)));
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)));
expect(paragraph1.selections.isEmpty, isTrue);
expect(paragraph2.selections.isEmpty, isTrue);
// Focus selectable region.
selectableRegionFocus.requestFocus();
await tester.pump();
// Reset controller selection once the TextField is unfocused.
controller.selection = const TextSelection.collapsed(offset: -1);
// Make sure keyboard select all will be handled by selectable region now.
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.keyA, control: true));
expect(controller.selection, const TextSelection.collapsed(offset: -1));
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 14));
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 12));
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.windows, TargetPlatform.linux, TargetPlatform.fuchsia }),
skip: kIsWeb, // [intended] the web handles this on its own.
);
testWidgets(
'does not override TextField keyboard shortcuts if the TextField is focused - apple',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(text: 'I am fine, thank you.');
addTearDown(controller.dispose);
final FocusNode selectableRegionFocus = FocusNode();
addTearDown(selectableRegionFocus.dispose);
final FocusNode textFieldFocus = FocusNode();
addTearDown(textFieldFocus.dispose);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: SelectableRegion(
focusNode: selectableRegionFocus,
selectionControls: materialTextSelectionControls,
child: Column(
children: <Widget>[
const Text('How are you?'),
const Text('Good, and you?'),
TextField(controller: controller, focusNode: textFieldFocus),
],
),
),
),
),
);
textFieldFocus.requestFocus();
await tester.pump();
// Make sure keyboard select all works on TextField.
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.keyA, meta: true));
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 21));
// Make sure no selection in SelectableRegion.
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)));
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)));
expect(paragraph1.selections.isEmpty, isTrue);
expect(paragraph2.selections.isEmpty, isTrue);
// Focus selectable region.
selectableRegionFocus.requestFocus();
await tester.pump();
// Reset controller selection once the TextField is unfocused.
controller.selection = const TextSelection.collapsed(offset: -1);
// Make sure keyboard select all will be handled by selectable region now.
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.keyA, meta: true));
expect(controller.selection, const TextSelection.collapsed(offset: -1));
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 14));
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 12));
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
skip: kIsWeb, // [intended] the web handles this on its own.
);
testWidgets('select all', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: const Column(
children: <Widget>[
Text('How are you?'),
Text('Good, and you?'),
Text('Fine, thank you.'),
],
),
),
),
);
await tester.pumpAndSettle();
focusNode.requestFocus();
// keyboard select all.
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.keyA, control: true));
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText)));
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)));
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)));
expect(paragraph3.selections[0], const TextSelection(baseOffset: 0, extentOffset: 16));
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 14));
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 12));
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.windows, TargetPlatform.linux, TargetPlatform.fuchsia }));
testWidgets(
'mouse selection can handle widget span', (WidgetTester tester) async {
final UniqueKey outerText = UniqueKey();
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: Center(
child: Text.rich(
const TextSpan(
children: <InlineSpan>[
TextSpan(text: 'How are you?'),
WidgetSpan(child: Text('Good, and you?')),
TextSpan(text: 'Fine, thank you.'),
],
),
key: outerText,
),
),
),
),
);
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(find.descendant(of: find.byKey(outerText), matching: find.byType(RichText)).first);
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph, 2), kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await tester.pump();
await gesture.moveTo(textOffsetToPosition(paragraph, 17)); // right after `Fine`.
await gesture.up();
// keyboard copy.
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.keyC, control: true));
final Map<String, dynamic> clipboardData = mockClipboard.clipboardData as Map<String, dynamic>;
expect(clipboardData['text'], 'w are you?Good, and you?Fine');
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.windows, TargetPlatform.linux, TargetPlatform.fuchsia }),
skip: isBrowser, // https://github.com/flutter/flutter/issues/61020
);
testWidgets(
'double click + drag mouse selection can handle widget span', (WidgetTester tester) async {
final UniqueKey outerText = UniqueKey();
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: Center(
child: Text.rich(
const TextSpan(
children: <InlineSpan>[
TextSpan(text: 'How are you?'),
WidgetSpan(child: Text('Good, and you?')),
TextSpan(text: 'Fine, thank you.'),
],
),
key: outerText,
),
),
),
),
);
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(find.descendant(of: find.byKey(outerText), matching: find.byType(RichText)).first);
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph, 0), kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await tester.pump();
await gesture.up();
await tester.pump();
await gesture.down(textOffsetToPosition(paragraph, 0));
await tester.pump();
await gesture.moveTo(textOffsetToPosition(paragraph, 17)); // right after `Fine`.
await gesture.up();
// keyboard copy.
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.keyC, control: true));
final Map<String, dynamic> clipboardData = mockClipboard.clipboardData as Map<String, dynamic>;
expect(clipboardData['text'], 'How are you?Good, and you?Fine,');
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.windows, TargetPlatform.linux, TargetPlatform.fuchsia }),
skip: isBrowser, // https://github.com/flutter/flutter/issues/61020
);
testWidgets(
'double click + drag mouse selection can handle widget span - multiline', (WidgetTester tester) async {
final UniqueKey outerText = UniqueKey();
final UniqueKey innerText = UniqueKey();
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: Center(
child: Text.rich(
TextSpan(
children: <InlineSpan>[
const TextSpan(text: 'How are you\n?'),
WidgetSpan(
child: Text(
'Good, and you?',
key: innerText,
),
),
const TextSpan(text: 'Fine, thank you.'),
],
),
key: outerText,
),
),
),
),
);
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(find.descendant(of: find.byKey(outerText), matching: find.byType(RichText)).first);
final RenderParagraph innerParagraph = tester.renderObject<RenderParagraph>(find.descendant(of: find.byKey(innerText), matching: find.byType(RichText)).first);
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph, 0), kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await tester.pump();
await gesture.up();
await tester.pump();
await gesture.down(textOffsetToPosition(paragraph, 0));
await tester.pump();
await gesture.moveTo(textOffsetToPosition(innerParagraph, 2)); // on `Good`.
// Should not crash.
expect(tester.takeException(), isNull);
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.windows, TargetPlatform.linux, TargetPlatform.fuchsia }),
skip: isBrowser, // https://github.com/flutter/flutter/issues/61020
);
testWidgets(
'select word event can select inline widget', (WidgetTester tester) async {
final UniqueKey outerText = UniqueKey();
final UniqueKey innerText = UniqueKey();
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: Center(
child: Text.rich(
TextSpan(
children: <InlineSpan>[
const TextSpan(text: 'How are\n you?'),
WidgetSpan(
child: Text(
'Good, and you?',
key: innerText,
),
),
const TextSpan(text: 'Fine, thank you.'),
],
),
key: outerText,
),
),
),
),
);
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(find.descendant(of: find.byKey(outerText), matching: find.byType(RichText)).first);
final RenderParagraph innerParagraph = tester.renderObject<RenderParagraph>(find.descendant(of: find.byKey(innerText), matching: find.byType(RichText)).first);
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byKey(innerText)), kind: PointerDeviceKind.mouse, buttons: kSecondaryMouseButton);
addTearDown(gesture.removePointer);
await tester.pump();
await gesture.up();
await tester.pump();
// Should select "and".
expect(paragraph.selections.isEmpty, isTrue);
expect(innerParagraph.selections[0], const TextSelection(baseOffset: 6, extentOffset: 9));
},
variant: TargetPlatformVariant.only(TargetPlatform.macOS),
skip: isBrowser, // https://github.com/flutter/flutter/issues/61020
);
testWidgets(
'select word event should not crash when its position is at an unselectable inline element', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
final UniqueKey flutterLogo = UniqueKey();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: Scaffold(
body: Center(
child: Text.rich(
TextSpan(
children: <InlineSpan>[
const TextSpan(
text:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
),
WidgetSpan(child: FlutterLogo(key: flutterLogo)),
const TextSpan(text: 'Hello, world.'),
],
),
),
),
),
),
),
);
final Offset gestureOffset = tester.getCenter(find.byKey(flutterLogo).first);
// Right click on unselectable element.
final TestGesture gesture = await tester.startGesture(gestureOffset, kind: PointerDeviceKind.mouse, buttons: kSecondaryMouseButton);
addTearDown(gesture.removePointer);
await tester.pump();
await gesture.up();
await tester.pump();
// Should not crash.
expect(tester.takeException(), isNull);
},
variant: TargetPlatformVariant.only(TargetPlatform.macOS),
skip: isBrowser, // https://github.com/flutter/flutter/issues/61020
);
testWidgets(
'can select word when a selectables rect is completely inside of another selectables rect', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/127076.
final UniqueKey outerText = UniqueKey();
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: Scaffold(
body: Center(
child: Text.rich(
const TextSpan(
children: <InlineSpan>[
TextSpan(
text:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
),
WidgetSpan(child: Text('Some text in a WidgetSpan. ')),
TextSpan(text: 'Hello, world.'),
],
),
key: outerText,
),
),
),
),
),
);
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(find.descendant(of: find.byKey(outerText), matching: find.byType(RichText)).first);
// Adjust `textOffsetToPosition` result because it returns the wrong vertical position (wrong line).
// TODO(bleroux): Remove when https://github.com/flutter/flutter/issues/133637 is fixed.
final Offset gestureOffset = textOffsetToPosition(paragraph, 125).translate(0, 10);
// Right click to select word at position.
final TestGesture gesture = await tester.startGesture(gestureOffset, kind: PointerDeviceKind.mouse, buttons: kSecondaryMouseButton);
addTearDown(gesture.removePointer);
await tester.pump();
await gesture.up();
await tester.pump();
// Should select "Hello".
expect(paragraph.selections[0], const TextSelection(baseOffset: 124, extentOffset: 129));
},
variant: TargetPlatformVariant.only(TargetPlatform.macOS),
skip: isBrowser, // https://github.com/flutter/flutter/issues/61020
);
testWidgets(
'can select word when selectable is broken up by an unselectable WidgetSpan', (WidgetTester tester) async {
final UniqueKey outerText = UniqueKey();
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: Scaffold(
body: Center(
child: Text.rich(
const TextSpan(
children: <InlineSpan>[
TextSpan(
text:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
),
WidgetSpan(child: SizedBox.shrink()),
TextSpan(text: 'Hello, world.'),
],
),
key: outerText,
),
),
),
),
),
);
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(find.descendant(of: find.byKey(outerText), matching: find.byType(RichText)).first);
// Adjust `textOffsetToPosition` result because it returns the wrong vertical position (wrong line).
// TODO(bleroux): Remove when https://github.com/flutter/flutter/issues/133637 is fixed.
final Offset gestureOffset = textOffsetToPosition(paragraph, 125).translate(0, 10);
// Right click to select word at position.
final TestGesture gesture = await tester.startGesture(gestureOffset, kind: PointerDeviceKind.mouse, buttons: kSecondaryMouseButton);
addTearDown(gesture.removePointer);
await tester.pump();
await gesture.up();
await tester.pump();
// Should select "Hello".
expect(paragraph.selections[0], const TextSelection(baseOffset: 124, extentOffset: 129));
},
variant: TargetPlatformVariant.only(TargetPlatform.macOS),
skip: isBrowser, // https://github.com/flutter/flutter/issues/61020
);
testWidgets(
'widget span is ignored if it does not contain text - non Apple',
(WidgetTester tester) async {
final UniqueKey outerText = UniqueKey();
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: Center(
child: Text.rich(
const TextSpan(
children: <InlineSpan>[
TextSpan(text: 'How are you?'),
WidgetSpan(child: Placeholder()),
TextSpan(text: 'Fine, thank you.'),
],
),
key: outerText,
),
),
),
),
);
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(find.descendant(of: find.byKey(outerText), matching: find.byType(RichText)).first);
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph, 2), kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await tester.pump();
await gesture.moveTo(textOffsetToPosition(paragraph, 17)); // right after `Fine`.
await gesture.up();
// keyboard copy.
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.keyC, control: true));
final Map<String, dynamic> clipboardData = mockClipboard.clipboardData as Map<String, dynamic>;
expect(clipboardData['text'], 'w are you?Fine');
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.windows, TargetPlatform.linux, TargetPlatform.fuchsia }),
skip: isBrowser, // https://github.com/flutter/flutter/issues/61020
);
testWidgets(
'widget span is ignored if it does not contain text - Apple',
(WidgetTester tester) async {
final UniqueKey outerText = UniqueKey();
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: Center(
child: Text.rich(
const TextSpan(
children: <InlineSpan>[
TextSpan(text: 'How are you?'),
WidgetSpan(child: Placeholder()),
TextSpan(text: 'Fine, thank you.'),
],
),
key: outerText,
),
),
),
),
);
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(find.descendant(of: find.byKey(outerText), matching: find.byType(RichText)).first);
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph, 2), kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await tester.pump();
await gesture.moveTo(textOffsetToPosition(paragraph, 17)); // right after `Fine`.
await gesture.up();
// keyboard copy.
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.keyC, meta: true));
final Map<String, dynamic> clipboardData = mockClipboard.clipboardData as Map<String, dynamic>;
expect(clipboardData['text'], 'w are you?Fine');
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
skip: isBrowser, // https://github.com/flutter/flutter/issues/61020
);
testWidgets('mouse can select across bidi text', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: const Column(
children: <Widget>[
Text('How are you?'),
Text('جيد وانت؟', textDirection: TextDirection.rtl),
Text('Fine, thank you.'),
],
),
),
),
);
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)));
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph1, 2), kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await tester.pump();
await gesture.moveTo(textOffsetToPosition(paragraph1, 4));
await tester.pump();
expect(paragraph1.selections[0], const TextSelection(baseOffset: 2, extentOffset: 4));
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('جيد وانت؟'), matching: find.byType(RichText)));
await gesture.moveTo(textOffsetToPosition(paragraph2, 5));
// Should select the rest of paragraph 1.
expect(paragraph1.selections[0], const TextSelection(baseOffset: 2, extentOffset: 12));
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 5));
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText)));
// Add a little offset to cross the boundary between paragraph 2 and 3.
await gesture.moveTo(textOffsetToPosition(paragraph3, 6) + const Offset(0, 1));
expect(paragraph1.selections[0], const TextSelection(baseOffset: 2, extentOffset: 12));
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 9));
expect(paragraph3.selections[0], const TextSelection(baseOffset: 0, extentOffset: 6));
await gesture.up();
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61020
testWidgets('long press and drag touch moves selection word by word', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: const Column(
children: <Widget>[
Text('How are you?'),
Text('Good, and you?'),
Text('Fine, thank you.'),
],
),
),
),
);
await tester.pumpAndSettle();
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)));
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph1, 6)); // at the 'r'
addTearDown(gesture.removePointer);
await tester.pump(const Duration(milliseconds: 500));
// `are` is selected.
expect(paragraph1.selections[0], const TextSelection(baseOffset: 4, extentOffset: 7));
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)));
await gesture.moveTo(textOffsetToPosition(paragraph2, 7));
expect(paragraph1.selections[0], const TextSelection(baseOffset: 4, extentOffset: 12));
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 9));
await gesture.up();
});
testWidgets('can drag end handle when not covering entire screen', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/104620.
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: Column(
children: <Widget>[
const Text('How are you?'),
SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: const Text('Good, and you?'),
),
const Text('Fine, thank you.'),
],
),
),
);
await tester.pumpAndSettle();
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)));
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph2, 7)); // at the 'a'
addTearDown(gesture.removePointer);
await tester.pump(const Duration(milliseconds: 500));
await gesture.up();
await tester.pump(const Duration(milliseconds: 500));
expect(paragraph2.selections[0], const TextSelection(baseOffset: 6, extentOffset: 9));
final List<TextBox> boxes = paragraph2.getBoxesForSelection(paragraph2.selections[0]);
expect(boxes.length, 1);
final Offset handlePos = globalize(boxes[0].toRect().bottomRight, paragraph2);
await gesture.down(handlePos);
await gesture.moveTo(textOffsetToPosition(paragraph2, 11) + Offset(0, paragraph2.size.height / 2));
expect(paragraph2.selections[0], const TextSelection(baseOffset: 6, extentOffset: 11));
await gesture.up();
});
testWidgets('can drag start handle when not covering entire screen', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/104620.
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: Column(
children: <Widget>[
const Text('How are you?'),
SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: const Text('Good, and you?'),
),
const Text('Fine, thank you.'),
],
),
),
);
await tester.pumpAndSettle();
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)));
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph2, 7)); // at the 'a'
addTearDown(gesture.removePointer);
await tester.pump(const Duration(milliseconds: 500));
await gesture.up();
await tester.pump(const Duration(milliseconds: 500));
expect(paragraph2.selections[0], const TextSelection(baseOffset: 6, extentOffset: 9));
final List<TextBox> boxes = paragraph2.getBoxesForSelection(paragraph2.selections[0]);
expect(boxes.length, 1);
final Offset handlePos = globalize(boxes[0].toRect().bottomLeft, paragraph2);
await gesture.down(handlePos);
await gesture.moveTo(textOffsetToPosition(paragraph2, 11) + Offset(0, paragraph2.size.height / 2));
expect(paragraph2.selections[0], const TextSelection(baseOffset: 11, extentOffset: 9));
await gesture.up();
});
testWidgets('can drag start selection handle', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: const Column(
children: <Widget>[
Text('How are you?'),
Text('Good, and you?'),
Text('Fine, thank you.'),
],
),
),
),
);
await tester.pumpAndSettle();
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText)));
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph3, 7)); // at the 'h'
addTearDown(gesture.removePointer);
await tester.pump(const Duration(milliseconds: 500));
await gesture.up();
await tester.pump(const Duration(milliseconds: 500));
expect(paragraph3.selections[0], const TextSelection(baseOffset: 6, extentOffset: 11));
final List<TextBox> boxes = paragraph3.getBoxesForSelection(paragraph3.selections[0]);
expect(boxes.length, 1);
final Offset handlePos = globalize(boxes[0].toRect().bottomLeft, paragraph3);
await gesture.down(handlePos);
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)));
await gesture.moveTo(textOffsetToPosition(paragraph2, 5) + Offset(0, paragraph2.size.height / 2));
expect(paragraph3.selections[0], const TextSelection(baseOffset: 0, extentOffset: 11));
expect(paragraph2.selections[0], const TextSelection(baseOffset: 5, extentOffset: 14));
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)));
await gesture.moveTo(textOffsetToPosition(paragraph1, 6) + Offset(0, paragraph1.size.height / 2));
expect(paragraph3.selections[0], const TextSelection(baseOffset: 0, extentOffset: 11));
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 14));
expect(paragraph1.selections[0], const TextSelection(baseOffset: 6, extentOffset: 12));
await gesture.up();
});
testWidgets('can drag start selection handle across end selection handle', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: const Column(
children: <Widget>[
Text('How are you?'),
Text('Good, and you?'),
Text('Fine, thank you.'),
],
),
),
),
);
await tester.pumpAndSettle();
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText)));
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph3, 7)); // at the 'h'
addTearDown(gesture.removePointer);
await tester.pump(const Duration(milliseconds: 500));
await gesture.up();
await tester.pump(const Duration(milliseconds: 500));
expect(paragraph3.selections[0], const TextSelection(baseOffset: 6, extentOffset: 11));
final List<TextBox> boxes = paragraph3.getBoxesForSelection(paragraph3.selections[0]);
expect(boxes.length, 1);
final Offset handlePos = globalize(boxes[0].toRect().bottomLeft, paragraph3);
await gesture.down(handlePos);
await gesture.moveTo(textOffsetToPosition(paragraph3, 14) + Offset(0, paragraph3.size.height / 2));
expect(paragraph3.selections[0], const TextSelection(baseOffset: 14, extentOffset: 11));
await gesture.moveTo(textOffsetToPosition(paragraph3, 4) + Offset(0, paragraph3.size.height / 2));
expect(paragraph3.selections[0], const TextSelection(baseOffset: 4, extentOffset: 11));
await gesture.up();
});
testWidgets('can drag end selection handle across start selection handle', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: const Column(
children: <Widget>[
Text('How are you?'),
Text('Good, and you?'),
Text('Fine, thank you.'),
],
),
),
),
);
await tester.pumpAndSettle();
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText)));
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph3, 7)); // at the 'h'
addTearDown(gesture.removePointer);
await tester.pump(const Duration(milliseconds: 500));
await gesture.up();
await tester.pump(const Duration(milliseconds: 500));
expect(paragraph3.selections[0], const TextSelection(baseOffset: 6, extentOffset: 11));
final List<TextBox> boxes = paragraph3.getBoxesForSelection(paragraph3.selections[0]);
expect(boxes.length, 1);
final Offset handlePos = globalize(boxes[0].toRect().bottomRight, paragraph3);
await gesture.down(handlePos);
await gesture.moveTo(textOffsetToPosition(paragraph3, 4) + Offset(0, paragraph3.size.height / 2));
expect(paragraph3.selections[0], const TextSelection(baseOffset: 6, extentOffset: 4));
await gesture.moveTo(textOffsetToPosition(paragraph3, 12) + Offset(0, paragraph3.size.height / 2));
expect(paragraph3.selections[0], const TextSelection(baseOffset: 6, extentOffset: 12));
await gesture.up();
});
testWidgets('can select all from toolbar', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: const Column(
children: <Widget>[
Text('How are you?'),
Text('Good, and you?'),
Text('Fine, thank you.'),
],
),
),
),
);
await tester.pumpAndSettle();
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText)));
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph3, 7)); // at the 'h'
addTearDown(gesture.removePointer);
await tester.pump(const Duration(milliseconds: 500));
await gesture.up();
await tester.pump(const Duration(milliseconds: 500));
expect(paragraph3.selections[0], const TextSelection(baseOffset: 6, extentOffset: 11));
expect(find.text('Select all'), findsOneWidget);
await tester.tap(find.text('Select all'));
await tester.pump();
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)));
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)));
expect(paragraph3.selections[0], const TextSelection(baseOffset: 0, extentOffset: 16));
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 14));
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 12));
}, skip: kIsWeb); // [intended] Web uses its native context menu.
testWidgets('can copy from toolbar', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: const Column(
children: <Widget>[
Text('How are you?'),
Text('Good, and you?'),
Text('Fine, thank you.'),
],
),
),
),
);
await tester.pumpAndSettle();
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText)));
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph3, 7)); // at the 'h'
addTearDown(gesture.removePointer);
await tester.pump(const Duration(milliseconds: 500));
await gesture.up();
await tester.pump(const Duration(milliseconds: 500));
expect(paragraph3.selections[0], const TextSelection(baseOffset: 6, extentOffset: 11));
expect(find.text('Copy'), findsOneWidget);
await tester.tap(find.text('Copy'));
await tester.pump();
// Selection should be cleared.
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)));
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)));
expect(paragraph3.selections.isEmpty, isTrue);
expect(paragraph2.selections.isEmpty, isTrue);
expect(paragraph1.selections.isEmpty, isTrue);
final Map<String, dynamic> clipboardData = mockClipboard.clipboardData as Map<String, dynamic>;
expect(clipboardData['text'], 'thank');
}, skip: kIsWeb); // [intended] Web uses its native context menu.
testWidgets('can use keyboard to granularly extend selection - character', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: const Column(
children: <Widget>[
Text('How are you?'),
Text('Good, and you?'),
Text('Fine, thank you.'),
],
),
),
),
);
// Select from offset 2 of paragraph1 to offset 6 of paragraph1.
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)));
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph1, 2), kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await tester.pump();
await gesture.moveTo(textOffsetToPosition(paragraph1, 6));
await gesture.up();
await tester.pump();
// Ho[w ar]e you?
// Good, and you?
// Fine, thank you.
expect(paragraph1.selections.length, 1);
expect(paragraph1.selections[0].start, 2);
expect(paragraph1.selections[0].end, 6);
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowRight, shift: true));
await tester.pump();
// Ho[w are] you?
// Good, and you?
// Fine, thank you.
expect(paragraph1.selections.length, 1);
expect(paragraph1.selections[0].start, 2);
expect(paragraph1.selections[0].end, 7);
for (int i = 0; i < 5; i += 1) {
await sendKeyCombination(tester,
const SingleActivator(LogicalKeyboardKey.arrowRight, shift: true));
await tester.pump();
expect(paragraph1.selections.length, 1);
expect(paragraph1.selections[0].start, 2);
expect(paragraph1.selections[0].end, 8 + i);
}
for (int i = 0; i < 5; i += 1) {
await sendKeyCombination(tester,
const SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true));
await tester.pump();
expect(paragraph1.selections.length, 1);
expect(paragraph1.selections[0].start, 2);
expect(paragraph1.selections[0].end, 11 - i);
}
}, variant: TargetPlatformVariant.all());
testWidgets('can use keyboard to granularly extend selection - word', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: const Column(
children: <Widget>[
Text('How are you?'),
Text('Good, and you?'),
Text('Fine, thank you.'),
],
),
),
),
);
// Select from offset 2 of paragraph1 to offset 6 of paragraph1.
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)));
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph1, 2), kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await tester.pump();
await gesture.moveTo(textOffsetToPosition(paragraph1, 6));
await gesture.up();
await tester.pump();
final bool alt;
final bool control;
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
alt = false;
control = true;
case TargetPlatform.iOS:
case TargetPlatform.macOS:
alt = true;
control = false;
}
// Ho[w ar]e you?
// Good, and you?
// Fine, thank you.
expect(paragraph1.selections.length, 1);
expect(paragraph1.selections[0].start, 2);
expect(paragraph1.selections[0].end, 6);
await sendKeyCombination(tester, SingleActivator(LogicalKeyboardKey.arrowRight, shift: true, alt: alt, control: control));
await tester.pump();
// Ho[w are] you?
// Good, and you?
// Fine, thank you.
expect(paragraph1.selections.length, 1);
expect(paragraph1.selections[0].start, 2);
expect(paragraph1.selections[0].end, 7);
await sendKeyCombination(tester, SingleActivator(LogicalKeyboardKey.arrowRight, shift: true, alt: alt, control: control));
await tester.pump();
// Ho[w are you]?
// Good, and you?
// Fine, thank you.
expect(paragraph1.selections.length, 1);
expect(paragraph1.selections[0].start, 2);
expect(paragraph1.selections[0].end, 11);
await sendKeyCombination(tester, SingleActivator(LogicalKeyboardKey.arrowRight, shift: true, alt: alt, control: control));
await tester.pump();
// Ho[w are you?]
// Good, and you?
// Fine, thank you.
expect(paragraph1.selections.length, 1);
expect(paragraph1.selections[0].start, 2);
expect(paragraph1.selections[0].end, 12);
await sendKeyCombination(tester, SingleActivator(LogicalKeyboardKey.arrowRight, shift: true, alt: alt, control: control));
await tester.pump();
// Ho[w are you?
// Good], and you?
// Fine, thank you.
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)));
expect(paragraph1.selections.length, 1);
expect(paragraph1.selections[0].start, 2);
expect(paragraph1.selections[0].end, 12);
expect(paragraph2.selections.length, 1);
expect(paragraph2.selections[0].start, 0);
expect(paragraph2.selections[0].end, 4);
await sendKeyCombination(tester, SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, alt: alt, control: control));
await tester.pump();
// Ho[w are you?
// ]Good, and you?
// Fine, thank you.
expect(paragraph1.selections.length, 1);
expect(paragraph1.selections[0].start, 2);
expect(paragraph1.selections[0].end, 12);
expect(paragraph2.selections.length, 1);
expect(paragraph2.selections[0].start, 0);
expect(paragraph2.selections[0].end, 0);
await sendKeyCombination(tester, SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, alt: alt, control: control));
await tester.pump();
// Ho[w are ]you?
// Good, and you?
// Fine, thank you.
expect(paragraph1.selections.length, 1);
expect(paragraph1.selections[0].start, 2);
expect(paragraph1.selections[0].end, 8);
expect(paragraph2.selections.length, 1);
expect(paragraph2.selections[0].start, 0);
expect(paragraph2.selections[0].end, 0);
}, variant: TargetPlatformVariant.all());
testWidgets('can use keyboard to granularly extend selection - line', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: const Column(
children: <Widget>[
Text('How are you?'),
Text('Good, and you?'),
Text('Fine, thank you.'),
],
),
),
),
);
// Select from offset 2 of paragraph1 to offset 6 of paragraph1.
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)));
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph1, 2), kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await tester.pump();
await gesture.moveTo(textOffsetToPosition(paragraph1, 6));
await gesture.up();
await tester.pump();
final bool alt;
final bool meta;
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
meta = false;
alt = true;
case TargetPlatform.iOS:
case TargetPlatform.macOS:
meta = true;
alt = false;
}
// Ho[w ar]e you?
// Good, and you?
// Fine, thank you.
expect(paragraph1.selections.length, 1);
expect(paragraph1.selections[0].start, 2);
expect(paragraph1.selections[0].end, 6);
await sendKeyCombination(tester, SingleActivator(LogicalKeyboardKey.arrowRight, shift: true, alt: alt, meta: meta));
await tester.pump();
// Ho[w are you?]
// Good, and you?
// Fine, thank you.
expect(paragraph1.selections.length, 1);
expect(paragraph1.selections[0].start, 2);
expect(paragraph1.selections[0].end, 12);
await sendKeyCombination(tester, SingleActivator(LogicalKeyboardKey.arrowRight, shift: true, alt: alt, meta: meta));
await tester.pump();
// Ho[w are you?
// Good, and you?]
// Fine, thank you.
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)));
expect(paragraph1.selections.length, 1);
expect(paragraph1.selections[0].start, 2);
expect(paragraph1.selections[0].end, 12);
expect(paragraph2.selections.length, 1);
expect(paragraph2.selections[0].start, 0);
expect(paragraph2.selections[0].end, 14);
await sendKeyCombination(tester, SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, alt: alt, meta: meta));
await tester.pump();
// Ho[w are you?]
// Good, and you?
// Fine, thank you.
expect(paragraph1.selections.length, 1);
expect(paragraph1.selections[0].start, 2);
expect(paragraph1.selections[0].end, 12);
expect(paragraph2.selections.length, 1);
expect(paragraph2.selections[0].start, 0);
expect(paragraph2.selections[0].end, 0);
await sendKeyCombination(tester, SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, alt: alt, meta: meta));
await tester.pump();
// [Ho]w are you?
// Good, and you?
// Fine, thank you.
expect(paragraph1.selections.length, 1);
expect(paragraph1.selections[0].start, 0);
expect(paragraph1.selections[0].end, 2);
}, variant: TargetPlatformVariant.all());
testWidgets('can use keyboard to granularly extend selection - document', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: const Column(
children: <Widget>[
Text('How are you?'),
Text('Good, and you?'),
Text('Fine, thank you.'),
],
),
),
),
);
// Select from offset 2 of paragraph1 to offset 6 of paragraph1.
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)));
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph1, 2), kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await tester.pump();
await gesture.moveTo(textOffsetToPosition(paragraph1, 6));
await gesture.up();
await tester.pump();
final bool alt;
final bool meta;
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
meta = false;
alt = true;
case TargetPlatform.iOS:
case TargetPlatform.macOS:
meta = true;
alt = false;
}
// Ho[w ar]e you?
// Good, and you?
// Fine, thank you.
expect(paragraph1.selections.length, 1);
expect(paragraph1.selections[0].start, 2);
expect(paragraph1.selections[0].end, 6);
await sendKeyCombination(tester, SingleActivator(LogicalKeyboardKey.arrowDown, shift: true, meta: meta, alt: alt));
await tester.pump();
// Ho[w are you?
// Good, and you?
// Fine, thank you.]
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)));
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText)));
expect(paragraph1.selections.length, 1);
expect(paragraph1.selections[0].start, 2);
expect(paragraph1.selections[0].end, 12);
expect(paragraph2.selections.length, 1);
expect(paragraph2.selections[0].start, 0);
expect(paragraph2.selections[0].end, 14);
expect(paragraph3.selections.length, 1);
expect(paragraph3.selections[0].start, 0);
expect(paragraph3.selections[0].end, 16);
await sendKeyCombination(tester, SingleActivator(LogicalKeyboardKey.arrowUp, shift: true, meta: meta, alt: alt));
await tester.pump();
// [Ho]w are you?
// Good, and you?
// Fine, thank you.
expect(paragraph1.selections.length, 1);
expect(paragraph1.selections[0].start, 0);
expect(paragraph1.selections[0].end, 2);
expect(paragraph2.selections.length, 1);
expect(paragraph2.selections[0].start, 0);
expect(paragraph2.selections[0].end, 0);
expect(paragraph3.selections.length, 1);
expect(paragraph3.selections[0].start, 0);
expect(paragraph3.selections[0].end, 0);
}, variant: TargetPlatformVariant.all());
testWidgets('can use keyboard to directionally extend selection', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: const Column(
children: <Widget>[
Text('How are you?'),
Text('Good, and you?'),
Text('Fine, thank you.'),
],
),
),
),
);
// Select from offset 2 of paragraph2 to offset 6 of paragraph2.
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)));
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph2, 2), kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await tester.pump();
await gesture.moveTo(textOffsetToPosition(paragraph2, 6));
await gesture.up();
await tester.pump();
// How are you?
// Go[od, ]and you?
// Fine, thank you.
expect(paragraph2.selections.length, 1);
expect(paragraph2.selections[0].start, 2);
expect(paragraph2.selections[0].end, 6);
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true));
await tester.pump();
// How are you?
// Go[od, and you?
// Fine, t]hank you.
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText)));
expect(paragraph2.selections.length, 1);
expect(paragraph2.selections[0].start, 2);
expect(paragraph2.selections[0].end, 14);
expect(paragraph3.selections.length, 1);
expect(paragraph3.selections[0].start, 0);
expect(paragraph3.selections[0].end, 7);
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true));
await tester.pump();
// How are you?
// Go[od, and you?
// Fine, thank you.]
expect(paragraph2.selections.length, 1);
expect(paragraph2.selections[0].start, 2);
expect(paragraph2.selections[0].end, 14);
expect(paragraph3.selections.length, 1);
expect(paragraph3.selections[0].start, 0);
expect(paragraph3.selections[0].end, 16);
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true));
await tester.pump();
// How are you?
// Go[od, ]and you?
// Fine, thank you.
expect(paragraph2.selections.length, 1);
expect(paragraph2.selections[0].start, 2);
expect(paragraph2.selections[0].end, 6);
expect(paragraph3.selections.length, 1);
expect(paragraph3.selections[0].start, 0);
expect(paragraph3.selections[0].end, 0);
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true));
await tester.pump();
// How a[re you?
// Go]od, and you?
// Fine, thank you.
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)));
expect(paragraph1.selections.length, 1);
expect(paragraph1.selections[0].start, 5);
expect(paragraph1.selections[0].end, 12);
expect(paragraph2.selections.length, 1);
expect(paragraph2.selections[0].start, 0);
expect(paragraph2.selections[0].end, 2);
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true));
await tester.pump();
// [How are you?
// Go]od, and you?
// Fine, thank you.
expect(paragraph1.selections.length, 1);
expect(paragraph1.selections[0].start, 0);
expect(paragraph1.selections[0].end, 12);
expect(paragraph2.selections.length, 1);
expect(paragraph2.selections[0].start, 0);
expect(paragraph2.selections[0].end, 2);
}, variant: TargetPlatformVariant.all());
group('magnifier', () {
late ValueNotifier<MagnifierInfo> magnifierInfo;
final Widget fakeMagnifier = Container(key: UniqueKey());
testWidgets('Can drag handles to show, unshow, and update magnifier',
(WidgetTester tester) async {
const String text = 'Monkeys and rabbits in my soup';
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
magnifierConfiguration: TextMagnifierConfiguration(
magnifierBuilder: (_,
MagnifierController controller,
ValueNotifier<MagnifierInfo>
localMagnifierInfo) {
magnifierInfo = localMagnifierInfo;
return fakeMagnifier;
},
),
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: const Text(text),
),
),
);
await tester.pumpAndSettle();
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(
find.descendant(
of: find.text(text), matching: find.byType(RichText)));
// Show the selection handles.
final TestGesture activateSelectionGesture = await tester
.startGesture(textOffsetToPosition(paragraph, text.length ~/ 2));
addTearDown(activateSelectionGesture.removePointer);
await tester.pump(const Duration(milliseconds: 500));
await activateSelectionGesture.up();
await tester.pump(const Duration(milliseconds: 500));
// Drag the handle around so that the magnifier shows.
final TextBox selectionBox =
paragraph.getBoxesForSelection(paragraph.selections.first).first;
final Offset leftHandlePos =
globalize(selectionBox.toRect().bottomLeft, paragraph);
final TestGesture gesture = await tester.startGesture(leftHandlePos);
await gesture.moveTo(textOffsetToPosition(paragraph, text.length - 2));
await tester.pump();
// Expect the magnifier to show and then store it's position.
expect(find.byKey(fakeMagnifier.key!), findsOneWidget);
final Offset firstDragGesturePosition =
magnifierInfo.value.globalGesturePosition;
await gesture.moveTo(textOffsetToPosition(paragraph, text.length));
await tester.pump();
// Expect the position the magnifier gets to have moved.
expect(firstDragGesturePosition,
isNot(magnifierInfo.value.globalGesturePosition));
// Lift the pointer and expect the magnifier to disappear.
await gesture.up();
await tester.pump();
expect(find.byKey(fakeMagnifier.key!), findsNothing);
});
});
});
testWidgets('toolbar is hidden on Android and iOS when orientation changes', (WidgetTester tester) async {
addTearDown(tester.view.reset);
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: const Text('How are you?'),
),
),
);
await tester.pumpAndSettle();
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)));
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph1, 6)); // at the 'r'
addTearDown(gesture.removePointer);
await tester.pump(const Duration(milliseconds: 500));
// `are` is selected.
expect(paragraph1.selections[0], const TextSelection(baseOffset: 4, extentOffset: 7));
await tester.pumpAndSettle();
await gesture.up();
await tester.pumpAndSettle();
// Text selection toolbar has appeared.
expect(find.text('Copy'), findsOneWidget);
// Hide the toolbar by changing orientation.
tester.view.physicalSize = const Size(1800.0, 2400.0);
await tester.pumpAndSettle();
expect(find.text('Copy'), findsNothing);
// Handles should be hidden as well on Android
expect(
find.descendant(
of: find.byType(CompositedTransformFollower),
matching: find.byType(Padding),
),
defaultTargetPlatform == TargetPlatform.android ? findsNothing : findsNWidgets(2),
);
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.android }),
skip: kIsWeb, // [intended] Web uses its native context menu.
);
testWidgets('the selection behavior when clicking `Copy` item in mobile platforms', (WidgetTester tester) async {
List<ContextMenuButtonItem> buttonItems = <ContextMenuButtonItem>[];
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionHandleControls,
contextMenuBuilder: (
BuildContext context,
SelectableRegionState selectableRegionState,
) {
buttonItems = selectableRegionState.contextMenuButtonItems;
return const SizedBox.shrink();
},
child: const Text('How are you?'),
),
),
);
await tester.pumpAndSettle();
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)));
await tester.longPressAt(textOffsetToPosition(paragraph1, 6)); // at the 'r'
await tester.pump(kLongPressTimeout);
// `are` is selected.
expect(paragraph1.selections[0], const TextSelection(baseOffset: 4, extentOffset: 7));
// Press `Copy` item.
expect(buttonItems[0].type, ContextMenuButtonType.copy);
buttonItems[0].onPressed?.call();
final SelectableRegionState regionState = tester.state<SelectableRegionState>(find.byType(SelectableRegion));
// In Android copy should clear the selection.
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
expect(regionState.selectionOverlay, isNull);
expect(regionState.selectionOverlay?.startHandleLayerLink, isNull);
expect(regionState.selectionOverlay?.endHandleLayerLink, isNull);
case TargetPlatform.iOS:
expect(regionState.selectionOverlay, isNotNull);
expect(regionState.selectionOverlay?.startHandleLayerLink, isNotNull);
expect(regionState.selectionOverlay?.endHandleLayerLink, isNotNull);
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
// Test doesn't run these platforms.
break; }
},
variant: TargetPlatformVariant.mobile(),
skip: kIsWeb, // [intended] Web uses its native context menu.
);
testWidgets('the handles do not disappear when clicking `Select all` item in mobile platforms', (WidgetTester tester) async {
List<ContextMenuButtonItem> buttonItems = <ContextMenuButtonItem>[];
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionHandleControls,
contextMenuBuilder: (
BuildContext context,
SelectableRegionState selectableRegionState,
) {
buttonItems = selectableRegionState.contextMenuButtonItems;
return const SizedBox.shrink();
},
child: const Text('How are you?'),
),
),
);
await tester.pumpAndSettle();
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)));
await tester.longPressAt(textOffsetToPosition(paragraph1, 6)); // at the 'r'
await tester.pump(kLongPressTimeout);
// `are` is selected.
expect(paragraph1.selections[0], const TextSelection(baseOffset: 4, extentOffset: 7));
late ContextMenuButtonItem selectAllButton;
switch (defaultTargetPlatform) {
case TargetPlatform.android:
// On Android, the select all button is after the share button.
expect(buttonItems[2].type, ContextMenuButtonType.selectAll);
selectAllButton = buttonItems[2];
case TargetPlatform.iOS:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
expect(buttonItems[1].type, ContextMenuButtonType.selectAll);
selectAllButton = buttonItems[1];
}
// Press `Select All` item.
selectAllButton.onPressed?.call();
final SelectableRegionState regionState = tester.state<SelectableRegionState>(find.byType(SelectableRegion));
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.iOS:
case TargetPlatform.fuchsia:
expect(regionState.selectionOverlay, isNotNull);
expect(regionState.selectionOverlay?.startHandleLayerLink, isNotNull);
expect(regionState.selectionOverlay?.endHandleLayerLink, isNotNull);
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
// Test doesn't run these platforms.
break;
}
},
variant: TargetPlatformVariant.mobile(),
skip: kIsWeb, // [intended] Web uses its native context menu.
);
testWidgets('Selection behavior when clicking the `Share` button on Android', (WidgetTester tester) async {
List<ContextMenuButtonItem> buttonItems = <ContextMenuButtonItem>[];
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionHandleControls,
contextMenuBuilder: (
BuildContext context,
SelectableRegionState selectableRegionState,
) {
buttonItems = selectableRegionState.contextMenuButtonItems;
return const SizedBox.shrink();
},
child: const Text('How are you?'),
),
),
);
await tester.pumpAndSettle();
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(
find.descendant(
of: find.text('How are you?'),
matching: find.byType(RichText),
),
);
await tester.longPressAt(textOffsetToPosition(paragraph, 6)); // at the 'r'
await tester.pump(kLongPressTimeout);
// `are` is selected.
expect(paragraph.selections[0], const TextSelection(baseOffset: 4, extentOffset: 7));
String? lastShare;
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async {
if (methodCall.method == 'Share.invoke') {
expect(methodCall.arguments, isA<String>());
lastShare = methodCall.arguments as String;
}
return null;
});
addTearDown(() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, null));
final SelectableRegionState regionState = tester.state<SelectableRegionState>(find.byType(SelectableRegion));
// Press the `Share` button.
expect(buttonItems[1].type, ContextMenuButtonType.share);
buttonItems[1].onPressed?.call();
expect(lastShare, 'are');
// On Android, share should clear the selection.
expect(regionState.selectionOverlay, isNull);
expect(regionState.selectionOverlay?.startHandleLayerLink, isNull);
expect(regionState.selectionOverlay?.endHandleLayerLink, isNull);
},
skip: kIsWeb, // [intended] Web uses its native context menu.
);
testWidgets('builds the correct button items', (WidgetTester tester) async {
List<ContextMenuButtonItem> buttonItems = <ContextMenuButtonItem>[];
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionHandleControls,
contextMenuBuilder: (
BuildContext context,
SelectableRegionState selectableRegionState,
) {
buttonItems = selectableRegionState.contextMenuButtonItems;
return const SizedBox.shrink();
},
child: const Text('How are you?'),
),
),
);
await tester.pumpAndSettle();
expect(find.byType(AdaptiveTextSelectionToolbar), findsNothing);
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(
find.descendant(
of: find.text('How are you?'),
matching: find.byType(RichText),
),
);
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph, 6)); // at the 'r'
addTearDown(gesture.removePointer);
await tester.pump(const Duration(milliseconds: 500));
// `are` is selected.
expect(paragraph.selections[0], const TextSelection(baseOffset: 4, extentOffset: 7));
await gesture.up();
await tester.pumpAndSettle();
switch (defaultTargetPlatform) {
case TargetPlatform.android:
// On Android, the share button is before the select all button.
expect(buttonItems.length, 3);
expect(buttonItems[0].type, ContextMenuButtonType.copy);
expect(buttonItems[1].type, ContextMenuButtonType.share);
expect(buttonItems[2].type, ContextMenuButtonType.selectAll);
case TargetPlatform.iOS:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
expect(buttonItems.length, 2);
expect(buttonItems[0].type, ContextMenuButtonType.copy);
expect(buttonItems[1].type, ContextMenuButtonType.selectAll);
}
},
variant: TargetPlatformVariant.all(),
skip: kIsWeb, // [intended] Web uses its native context menu.
);
testWidgets('Text processing actions are added to the toolbar', (WidgetTester tester) async {
final MockProcessTextHandler mockProcessTextHandler = MockProcessTextHandler();
TestWidgetsFlutterBinding.ensureInitialized().defaultBinaryMessenger
.setMockMethodCallHandler(SystemChannels.processText, mockProcessTextHandler.handleMethodCall);
addTearDown(() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.processText, null));
Set<String?> buttonLabels = <String?>{};
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionHandleControls,
contextMenuBuilder: (
BuildContext context,
SelectableRegionState selectableRegionState,
) {
buttonLabels = selectableRegionState.contextMenuButtonItems
.map((ContextMenuButtonItem buttonItem) => buttonItem.label)
.toSet();
return const SizedBox.shrink();
},
child: const Text('How are you?'),
),
),
);
await tester.pumpAndSettle();
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(
find.descendant(
of: find.text('How are you?'),
matching: find.byType(RichText),
),
);
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph, 6)); // at the 'r'
addTearDown(gesture.removePointer);
await tester.pump(const Duration(milliseconds: 500));
// `are` is selected.
expect(paragraph.selections[0], const TextSelection(baseOffset: 4, extentOffset: 7));
await gesture.up();
await tester.pumpAndSettle();
// The text processing actions are available on Android only.
final bool areTextActionsSupported = defaultTargetPlatform == TargetPlatform.android;
expect(buttonLabels.contains(fakeAction1Label), areTextActionsSupported);
expect(buttonLabels.contains(fakeAction2Label), areTextActionsSupported);
},
variant: TargetPlatformVariant.all(),
skip: kIsWeb, // [intended] Web uses its native context menu.
);
testWidgets('onSelectionChange is called when the selection changes through gestures', (WidgetTester tester) async {
SelectedContent? content;
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
onSelectionChanged: (SelectedContent? selectedContent) => content = selectedContent,
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: const Center(
child: Text('How are you'),
),
),
),
);
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you'), matching: find.byType(RichText)));
final TestGesture mouseGesture = await tester.startGesture(textOffsetToPosition(paragraph, 4), kind: PointerDeviceKind.mouse);
final TestGesture touchGesture = await tester.createGesture();
expect(content, isNull);
addTearDown(mouseGesture.removePointer);
addTearDown(touchGesture.removePointer);
await tester.pump();
// Called on drag.
await mouseGesture.moveTo(textOffsetToPosition(paragraph, 7));
await tester.pumpAndSettle();
expect(content, isNotNull);
expect(content!.plainText, 'are');
// Updates on drag.
await mouseGesture.moveTo(textOffsetToPosition(paragraph, 10));
await tester.pumpAndSettle();
expect(content, isNotNull);
expect(content!.plainText, 'are yo');
// Called on drag end.
await mouseGesture.up();
await tester.pump();
expect(content, isNotNull);
expect(content!.plainText, 'are yo');
// Backwards selection.
await mouseGesture.down(textOffsetToPosition(paragraph, 3));
await tester.pump();
await mouseGesture.up();
await tester.pumpAndSettle(kDoubleTapTimeout);
expect(content, isNotNull);
expect(content!.plainText, '');
await mouseGesture.down(textOffsetToPosition(paragraph, 3));
await tester.pump();
await mouseGesture.moveTo(textOffsetToPosition(paragraph, 0));
await tester.pumpAndSettle();
expect(content, isNotNull);
expect(content!.plainText, 'How');
await mouseGesture.up();
await tester.pump();
expect(content, isNotNull);
expect(content!.plainText, 'How');
// Called on double tap.
await mouseGesture.down(textOffsetToPosition(paragraph, 6));
await tester.pump();
await mouseGesture.up();
await tester.pump();
await mouseGesture.down(textOffsetToPosition(paragraph, 6));
await tester.pumpAndSettle();
expect(content, isNotNull);
expect(content!.plainText, 'are');
await mouseGesture.up();
await tester.pumpAndSettle();
// Called on tap.
await mouseGesture.down(textOffsetToPosition(paragraph, 0));
await tester.pumpAndSettle();
await mouseGesture.up();
await tester.pumpAndSettle(kDoubleTapTimeout);
expect(content, isNotNull);
expect(content!.plainText, '');
// With touch gestures.
// Called on long press start.
await touchGesture.down(textOffsetToPosition(paragraph, 0));
await tester.pumpAndSettle(kLongPressTimeout);
expect(content, isNotNull);
expect(content!.plainText, 'How');
// Called on long press update.
await touchGesture.moveTo(textOffsetToPosition(paragraph, 5));
await tester.pumpAndSettle();
expect(content, isNotNull);
expect(content!.plainText, 'How are');
// Called on long press end.
await touchGesture.up();
await tester.pumpAndSettle();
expect(content, isNotNull);
expect(content!.plainText, 'How are');
// Long press to select 'you'.
await touchGesture.down(textOffsetToPosition(paragraph, 9));
await tester.pumpAndSettle(kLongPressTimeout);
expect(content, isNotNull);
expect(content!.plainText, 'you');
await touchGesture.up();
await tester.pumpAndSettle();
// Called while moving selection handles.
final List<TextBox> boxes = paragraph.getBoxesForSelection(paragraph.selections[0]);
expect(boxes.length, 1);
final Offset startHandlePos = globalize(boxes[0].toRect().bottomLeft, paragraph);
final Offset endHandlePos = globalize(boxes[0].toRect().bottomRight, paragraph);
final Offset startPos = Offset(textOffsetToPosition(paragraph, 4).dx, startHandlePos.dy);
final Offset endPos = Offset(textOffsetToPosition(paragraph, 6).dx, endHandlePos.dy);
// Start handle.
await touchGesture.down(startHandlePos);
await touchGesture.moveTo(startPos);
await tester.pumpAndSettle();
expect(content, isNotNull);
expect(content!.plainText, 'are you');
await touchGesture.up();
await tester.pumpAndSettle();
// End handle.
await touchGesture.down(endHandlePos);
await touchGesture.moveTo(endPos);
await tester.pumpAndSettle();
expect(content, isNotNull);
expect(content!.plainText, 'ar');
await touchGesture.up();
await tester.pumpAndSettle();
});
testWidgets('onSelectionChange is called when the selection changes through keyboard actions', (WidgetTester tester) async {
SelectedContent? content;
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
onSelectionChanged: (SelectedContent? selectedContent) => content = selectedContent,
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: const Column(
children: <Widget>[
Text('How are you?'),
Text('Good, and you?'),
Text('Fine, thank you.'),
],
),
),
),
);
expect(content, isNull);
await tester.pump();
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)));
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)));
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText)));
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph1, 2), kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await tester.pump();
await gesture.moveTo(textOffsetToPosition(paragraph1, 6));
await gesture.up();
await tester.pump();
expect(paragraph1.selections.length, 1);
expect(paragraph1.selections[0].start, 2);
expect(paragraph1.selections[0].end, 6);
expect(content, isNotNull);
expect(content!.plainText, 'w ar');
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowRight, shift: true));
await tester.pump();
expect(paragraph1.selections.length, 1);
expect(paragraph1.selections[0].start, 2);
expect(paragraph1.selections[0].end, 7);
expect(content, isNotNull);
expect(content!.plainText, 'w are');
for (int i = 0; i < 5; i += 1) {
await sendKeyCombination(tester,
const SingleActivator(LogicalKeyboardKey.arrowRight, shift: true));
await tester.pump();
expect(paragraph1.selections.length, 1);
expect(paragraph1.selections[0].start, 2);
expect(paragraph1.selections[0].end, 8 + i);
expect(content, isNotNull);
}
expect(content, isNotNull);
expect(content!.plainText, 'w are you?');
for (int i = 0; i < 5; i += 1) {
await sendKeyCombination(tester,
const SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true));
await tester.pump();
expect(paragraph1.selections.length, 1);
expect(paragraph1.selections[0].start, 2);
expect(paragraph1.selections[0].end, 11 - i);
expect(content, isNotNull);
}
expect(content, isNotNull);
expect(content!.plainText, 'w are');
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true));
await tester.pump();
expect(paragraph1.selections.length, 1);
expect(paragraph1.selections[0].start, 2);
expect(paragraph1.selections[0].end, 12);
expect(paragraph2.selections.length, 1);
expect(paragraph2.selections[0].start, 0);
expect(paragraph2.selections[0].end, 8);
expect(content, isNotNull);
expect(content!.plainText, 'w are you?Good, an');
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true));
await tester.pump();
expect(paragraph1.selections.length, 1);
expect(paragraph1.selections[0].start, 2);
expect(paragraph1.selections[0].end, 12);
expect(paragraph2.selections.length, 1);
expect(paragraph2.selections[0].start, 0);
expect(paragraph2.selections[0].end, 14);
expect(paragraph3.selections.length, 1);
expect(paragraph3.selections[0].start, 0);
expect(paragraph3.selections[0].end, 9);
expect(content, isNotNull);
expect(content!.plainText, 'w are you?Good, and you?Fine, tha');
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true));
await tester.pump();
expect(paragraph1.selections.length, 1);
expect(paragraph1.selections[0].start, 2);
expect(paragraph1.selections[0].end, 12);
expect(paragraph2.selections.length, 1);
expect(paragraph2.selections[0].start, 0);
expect(paragraph2.selections[0].end, 14);
expect(paragraph3.selections.length, 1);
expect(paragraph3.selections[0].start, 0);
expect(paragraph3.selections[0].end, 16);
expect(content, isNotNull);
expect(content!.plainText, 'w are you?Good, and you?Fine, thank you.');
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true));
await tester.pump();
expect(paragraph1.selections.length, 1);
expect(paragraph1.selections[0].start, 2);
expect(paragraph1.selections[0].end, 12);
expect(paragraph2.selections.length, 1);
expect(paragraph2.selections[0].start, 0);
expect(paragraph2.selections[0].end, 8);
expect(paragraph3.selections.length, 1);
expect(content, isNotNull);
expect(content!.plainText, 'w are you?Good, an');
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true));
await tester.pump();
expect(paragraph1.selections.length, 1);
expect(paragraph1.selections[0].start, 2);
expect(paragraph1.selections[0].end, 7);
expect(paragraph2.selections.length, 1);
expect(paragraph3.selections.length, 1);
expect(content, isNotNull);
expect(content!.plainText, 'w are');
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true));
await tester.pump();
expect(paragraph1.selections.length, 1);
expect(paragraph1.selections[0].start, 0);
expect(paragraph1.selections[0].end, 2);
expect(paragraph2.selections.length, 1);
expect(paragraph3.selections.length, 1);
expect(content, isNotNull);
expect(content!.plainText, 'Ho');
});
group('BrowserContextMenu', () {
setUp(() async {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.contextMenu, (MethodCall call) {
// Just complete successfully, so that BrowserContextMenu thinks that
// the engine successfully received its call.
return Future<void>.value();
});
await BrowserContextMenu.disableContextMenu();
});
tearDown(() async {
await BrowserContextMenu.enableContextMenu();
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.contextMenu, null);
});
testWidgets('web can show flutter context menu when the browser context menu is disabled', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
onSelectionChanged: (SelectedContent? selectedContent) {},
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: const Center(
child: Text('How are you'),
),
),
),
);
await tester.pumpAndSettle();
final SelectableRegionState state =
tester.state<SelectableRegionState>(find.byType(SelectableRegion));
expect(find.text('Copy'), findsNothing);
state.selectAll(SelectionChangedCause.toolbar);
await tester.pumpAndSettle();
expect(find.text('Copy'), findsOneWidget);
state.hideToolbar();
await tester.pumpAndSettle();
expect(find.text('Copy'), findsNothing);
},
skip: !kIsWeb, // [intended]
);
});
testWidgets('Multiple selectables on a single line should be in screen order', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/127942.
final UniqueKey outerText = UniqueKey();
const TextStyle textStyle = TextStyle(fontSize: 10);
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: Scaffold(
body: Center(
child: Text.rich(
const TextSpan(
children: <InlineSpan>[
TextSpan(text: 'Hello my name is ', style: textStyle),
WidgetSpan(
child: Text('Dash', style: textStyle),
alignment: PlaceholderAlignment.middle,
),
TextSpan(text: '.', style: textStyle),
],
),
key: outerText,
),
),
),
),
),
);
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(find.descendant(of: find.byKey(outerText), matching: find.byType(RichText)).first);
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph1, 0), kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await tester.pump();
await gesture.up();
// Select all.
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.keyA, control: true));
// keyboard copy.
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.keyC, control: true));
final Map<String, dynamic> clipboardData = mockClipboard.clipboardData as Map<String, dynamic>;
expect(clipboardData['text'], 'Hello my name is Dash.');
});
}
class SelectionSpy extends LeafRenderObjectWidget {
const SelectionSpy({
super.key,
});
@override
RenderObject createRenderObject(BuildContext context) {
return RenderSelectionSpy(
SelectionContainer.maybeOf(context),
);
}
@override
void updateRenderObject(BuildContext context, covariant RenderObject renderObject) { }
}
class RenderSelectionSpy extends RenderProxyBox
with Selectable, SelectionRegistrant {
RenderSelectionSpy(
SelectionRegistrar? registrar,
) {
this.registrar = registrar;
}
final Set<VoidCallback> listeners = <VoidCallback>{};
List<SelectionEvent> events = <SelectionEvent>[];
@override
Size get size => _size;
Size _size = Size.zero;
@override
List<Rect> get boundingBoxes => <Rect>[paintBounds];
@override
Size computeDryLayout(BoxConstraints constraints) {
_size = Size(constraints.maxWidth, constraints.maxHeight);
return _size;
}
@override
void addListener(VoidCallback listener) => listeners.add(listener);
@override
void removeListener(VoidCallback listener) => listeners.remove(listener);
@override
SelectionResult dispatchSelectionEvent(SelectionEvent event) {
events.add(event);
return SelectionResult.end;
}
@override
SelectedContent? getSelectedContent() {
return const SelectedContent(plainText: 'content');
}
@override
final SelectionGeometry value = const SelectionGeometry(
hasContent: true,
status: SelectionStatus.uncollapsed,
startSelectionPoint: SelectionPoint(
localPosition: Offset.zero,
lineHeight: 0.0,
handleType: TextSelectionHandleType.left,
),
endSelectionPoint: SelectionPoint(
localPosition: Offset.zero,
lineHeight: 0.0,
handleType: TextSelectionHandleType.left,
),
);
@override
void pushHandleLayers(LayerLink? startHandle, LayerLink? endHandle) { }
}
class SelectAllWidget extends SingleChildRenderObjectWidget {
const SelectAllWidget({
super.key,
super.child,
});
@override
RenderObject createRenderObject(BuildContext context) {
return RenderSelectAll(
SelectionContainer.maybeOf(context),
);
}
@override
void updateRenderObject(BuildContext context, covariant RenderObject renderObject) { }
}
class RenderSelectAll extends RenderProxyBox
with Selectable, SelectionRegistrant {
RenderSelectAll(
SelectionRegistrar? registrar,
) {
this.registrar = registrar;
}
@override
List<Rect> get boundingBoxes => <Rect>[paintBounds];
final Set<VoidCallback> listeners = <VoidCallback>{};
LayerLink? startHandle;
LayerLink? endHandle;
@override
void addListener(VoidCallback listener) => listeners.add(listener);
@override
void removeListener(VoidCallback listener) => listeners.remove(listener);
@override
SelectionResult dispatchSelectionEvent(SelectionEvent event) {
value = SelectionGeometry(
hasContent: true,
status: SelectionStatus.uncollapsed,
startSelectionPoint: SelectionPoint(
localPosition: Offset(0, size.height),
lineHeight: 0.0,
handleType: TextSelectionHandleType.left,
),
endSelectionPoint: SelectionPoint(
localPosition: Offset(size.width, size.height),
lineHeight: 0.0,
handleType: TextSelectionHandleType.left,
),
);
return SelectionResult.end;
}
@override
SelectedContent? getSelectedContent() {
return const SelectedContent(plainText: 'content');
}
@override
SelectionGeometry get value => _value;
SelectionGeometry _value = const SelectionGeometry(
hasContent: true,
status: SelectionStatus.uncollapsed,
startSelectionPoint: SelectionPoint(
localPosition: Offset.zero,
lineHeight: 0.0,
handleType: TextSelectionHandleType.left,
),
endSelectionPoint: SelectionPoint(
localPosition: Offset.zero,
lineHeight: 0.0,
handleType: TextSelectionHandleType.left,
),
);
set value(SelectionGeometry other) {
if (other == _value) {
return;
}
_value = other;
for (final VoidCallback callback in listeners) {
callback();
}
}
@override
void pushHandleLayers(LayerLink? startHandle, LayerLink? endHandle) {
this.startHandle = startHandle;
this.endHandle = endHandle;
}
}