| // 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' show ValueListenable, defaultTargetPlatform; |
| 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 'editable_text_utils.dart'; |
| |
| const int kSingleTapUpTimeout = 500; |
| |
| void main() { |
| late int tapCount; |
| late int singleTapUpCount; |
| late int singleTapCancelCount; |
| late int singleLongTapStartCount; |
| late int doubleTapDownCount; |
| late int tripleTapDownCount; |
| late int forcePressStartCount; |
| late int forcePressEndCount; |
| late int dragStartCount; |
| late int dragUpdateCount; |
| late int dragEndCount; |
| const Offset forcePressOffset = Offset(400.0, 50.0); |
| |
| void handleTapDown(TapDragDownDetails details) { tapCount++; } |
| void handleSingleTapUp(TapDragUpDetails details) { singleTapUpCount++; } |
| void handleSingleTapCancel() { singleTapCancelCount++; } |
| void handleSingleLongTapStart(LongPressStartDetails details) { singleLongTapStartCount++; } |
| void handleDoubleTapDown(TapDragDownDetails details) { doubleTapDownCount++; } |
| void handleTripleTapDown(TapDragDownDetails details) { tripleTapDownCount++; } |
| void handleForcePressStart(ForcePressDetails details) { forcePressStartCount++; } |
| void handleForcePressEnd(ForcePressDetails details) { forcePressEndCount++; } |
| void handleDragSelectionStart(TapDragStartDetails details) { dragStartCount++; } |
| void handleDragSelectionUpdate(TapDragUpdateDetails details) { dragUpdateCount++; } |
| void handleDragSelectionEnd(TapDragEndDetails details) { dragEndCount++; } |
| |
| setUp(() { |
| tapCount = 0; |
| singleTapUpCount = 0; |
| singleTapCancelCount = 0; |
| singleLongTapStartCount = 0; |
| doubleTapDownCount = 0; |
| tripleTapDownCount = 0; |
| forcePressStartCount = 0; |
| forcePressEndCount = 0; |
| dragStartCount = 0; |
| dragUpdateCount = 0; |
| dragEndCount = 0; |
| }); |
| |
| Future<void> pumpGestureDetector(WidgetTester tester) async { |
| await tester.pumpWidget( |
| TextSelectionGestureDetector( |
| behavior: HitTestBehavior.opaque, |
| onTapDown: handleTapDown, |
| onSingleTapUp: handleSingleTapUp, |
| onSingleTapCancel: handleSingleTapCancel, |
| onSingleLongTapStart: handleSingleLongTapStart, |
| onDoubleTapDown: handleDoubleTapDown, |
| onTripleTapDown: handleTripleTapDown, |
| onForcePressStart: handleForcePressStart, |
| onForcePressEnd: handleForcePressEnd, |
| onDragSelectionStart: handleDragSelectionStart, |
| onDragSelectionUpdate: handleDragSelectionUpdate, |
| onDragSelectionEnd: handleDragSelectionEnd, |
| child: Container(), |
| ), |
| ); |
| } |
| |
| Future<void> pumpTextSelectionGestureDetectorBuilder( |
| WidgetTester tester, { |
| bool forcePressEnabled = true, |
| bool selectionEnabled = true, |
| }) async { |
| final GlobalKey<EditableTextState> editableTextKey = GlobalKey<EditableTextState>(); |
| final FakeTextSelectionGestureDetectorBuilderDelegate delegate = FakeTextSelectionGestureDetectorBuilderDelegate( |
| editableTextKey: editableTextKey, |
| forcePressEnabled: forcePressEnabled, |
| selectionEnabled: selectionEnabled, |
| ); |
| |
| final TextSelectionGestureDetectorBuilder provider = |
| TextSelectionGestureDetectorBuilder(delegate: delegate); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: provider.buildGestureDetector( |
| behavior: HitTestBehavior.translucent, |
| child: FakeEditableText(key: editableTextKey), |
| ), |
| ), |
| ); |
| } |
| |
| test('TextSelectionOverlay.fadeDuration exist', () async { |
| expect(TextSelectionOverlay.fadeDuration, SelectionOverlay.fadeDuration); |
| }); |
| |
| testWidgets('a series of taps all call onTaps', (WidgetTester tester) async { |
| await pumpGestureDetector(tester); |
| await tester.tapAt(const Offset(200, 200)); |
| await tester.pump(const Duration(milliseconds: 150)); |
| await tester.tapAt(const Offset(200, 200)); |
| await tester.pump(const Duration(milliseconds: 150)); |
| await tester.tapAt(const Offset(200, 200)); |
| await tester.pump(const Duration(milliseconds: 150)); |
| await tester.tapAt(const Offset(200, 200)); |
| await tester.pump(const Duration(milliseconds: 150)); |
| await tester.tapAt(const Offset(200, 200)); |
| await tester.pump(const Duration(milliseconds: 150)); |
| await tester.tapAt(const Offset(200, 200)); |
| expect(tapCount, 6); |
| }); |
| |
| testWidgets('in a series of rapid taps, onTapDown, onDoubleTapDown, and onTripleTapDown alternate', (WidgetTester tester) async { |
| await pumpGestureDetector(tester); |
| await tester.tapAt(const Offset(200, 200)); |
| await tester.pump(const Duration(milliseconds: 50)); |
| expect(singleTapUpCount, 1); |
| await tester.tapAt(const Offset(200, 200)); |
| await tester.pump(const Duration(milliseconds: 50)); |
| expect(singleTapUpCount, 1); |
| expect(doubleTapDownCount, 1); |
| await tester.tapAt(const Offset(200, 200)); |
| await tester.pump(const Duration(milliseconds: 50)); |
| expect(singleTapUpCount, 1); |
| expect(doubleTapDownCount, 1); |
| expect(tripleTapDownCount, 1); |
| await tester.tapAt(const Offset(200, 200)); |
| await tester.pump(const Duration(milliseconds: 50)); |
| expect(singleTapUpCount, 2); |
| expect(doubleTapDownCount, 1); |
| expect(tripleTapDownCount, 1); |
| await tester.tapAt(const Offset(200, 200)); |
| await tester.pump(const Duration(milliseconds: 50)); |
| expect(singleTapUpCount, 2); |
| expect(doubleTapDownCount, 2); |
| expect(tripleTapDownCount, 1); |
| await tester.tapAt(const Offset(200, 200)); |
| await tester.pump(const Duration(milliseconds: 50)); |
| expect(singleTapUpCount, 2); |
| expect(doubleTapDownCount, 2); |
| expect(tripleTapDownCount, 2); |
| await tester.tapAt(const Offset(200, 200)); |
| expect(singleTapUpCount, 3); |
| expect(doubleTapDownCount, 2); |
| expect(tripleTapDownCount, 2); |
| expect(tapCount, 7); |
| }); |
| |
| testWidgets('quick tap-tap-hold is a double tap down', (WidgetTester tester) async { |
| await pumpGestureDetector(tester); |
| await tester.tapAt(const Offset(200, 200)); |
| await tester.pump(const Duration(milliseconds: 50)); |
| expect(singleTapUpCount, 1); |
| final TestGesture gesture = await tester.startGesture(const Offset(200, 200)); |
| await tester.pump(const Duration(milliseconds: 200)); |
| expect(singleTapUpCount, 1); |
| // Every down is counted. |
| expect(tapCount, 2); |
| // No cancels because the second tap of the double tap is a second successful |
| // single tap behind the scene. |
| expect(singleTapCancelCount, 0); |
| expect(doubleTapDownCount, 1); |
| // The double tap down hold supersedes the single tap down. |
| expect(singleLongTapStartCount, 0); |
| |
| await gesture.up(); |
| // Nothing else happens on up. |
| expect(singleTapUpCount, 1); |
| expect(tapCount, 2); |
| expect(singleTapCancelCount, 0); |
| expect(doubleTapDownCount, 1); |
| expect(singleLongTapStartCount, 0); |
| }); |
| |
| testWidgets('a very quick swipe is ignored', (WidgetTester tester) async { |
| await pumpGestureDetector(tester); |
| final TestGesture gesture = await tester.startGesture(const Offset(200, 200)); |
| await tester.pump(const Duration(milliseconds: 20)); |
| await gesture.moveBy(const Offset(100, 100)); |
| await tester.pump(); |
| expect(singleTapUpCount, 0); |
| // Before the move to TapAndDragGestureRecognizer the tapCount was 0 because the |
| // TapGestureRecognizer rejected itself when the initial pointer moved past a certain |
| // threshold. With TapAndDragGestureRecognizer, we have two thresholds, a normal tap |
| // threshold, and a drag threshold, so it is possible for the tap count to increase |
| // even though the original pointer has moved beyond the tap threshold. |
| expect(tapCount, 1); |
| expect(singleTapCancelCount, 0); |
| expect(doubleTapDownCount, 0); |
| expect(singleLongTapStartCount, 0); |
| |
| await gesture.up(); |
| // Nothing else happens on up. |
| expect(singleTapUpCount, 0); |
| expect(tapCount, 1); |
| expect(singleTapCancelCount, 0); |
| expect(doubleTapDownCount, 0); |
| expect(singleLongTapStartCount, 0); |
| }); |
| |
| testWidgets('a slower swipe has a tap down and a canceled tap', (WidgetTester tester) async { |
| await pumpGestureDetector(tester); |
| final TestGesture gesture = await tester.startGesture(const Offset(200, 200)); |
| await tester.pump(const Duration(milliseconds: 120)); |
| await gesture.moveBy(const Offset(100, 100)); |
| await tester.pump(); |
| expect(singleTapUpCount, 0); |
| expect(tapCount, 1); |
| expect(singleTapCancelCount, 0); |
| expect(doubleTapDownCount, 0); |
| expect(singleLongTapStartCount, 0); |
| }); |
| |
| testWidgets('a force press initiates a force press', (WidgetTester tester) async { |
| await pumpGestureDetector(tester); |
| |
| final int pointerValue = tester.nextPointer; |
| |
| final TestGesture gesture = await tester.createGesture(); |
| |
| await gesture.downWithCustomEvent( |
| forcePressOffset, |
| PointerDownEvent( |
| pointer: pointerValue, |
| position: forcePressOffset, |
| pressure: 0.0, |
| pressureMax: 6.0, |
| pressureMin: 0.0, |
| ), |
| ); |
| |
| await gesture.updateWithCustomEvent(PointerMoveEvent( |
| pointer: pointerValue, |
| pressure: 0.5, |
| pressureMin: 0, |
| )); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| await gesture.downWithCustomEvent( |
| forcePressOffset, |
| PointerDownEvent( |
| pointer: pointerValue, |
| position: forcePressOffset, |
| pressure: 0.0, |
| pressureMax: 6.0, |
| pressureMin: 0.0, |
| ), |
| ); |
| await gesture.updateWithCustomEvent(PointerMoveEvent( |
| pointer: pointerValue, |
| pressure: 0.5, |
| pressureMin: 0, |
| )); |
| await gesture.up(); |
| await tester.pump(const Duration(milliseconds: 20)); |
| |
| await gesture.downWithCustomEvent( |
| forcePressOffset, |
| PointerDownEvent( |
| pointer: pointerValue, |
| position: forcePressOffset, |
| pressure: 0.0, |
| pressureMax: 6.0, |
| pressureMin: 0.0, |
| ), |
| ); |
| await gesture.updateWithCustomEvent(PointerMoveEvent( |
| pointer: pointerValue, |
| pressure: 0.5, |
| pressureMin: 0, |
| )); |
| await gesture.up(); |
| await tester.pump(const Duration(milliseconds: 20)); |
| |
| await gesture.downWithCustomEvent( |
| forcePressOffset, |
| PointerDownEvent( |
| pointer: pointerValue, |
| position: forcePressOffset, |
| pressure: 0.0, |
| pressureMax: 6.0, |
| pressureMin: 0.0, |
| ), |
| ); |
| await gesture.updateWithCustomEvent(PointerMoveEvent( |
| pointer: pointerValue, |
| pressure: 0.5, |
| pressureMin: 0, |
| )); |
| await gesture.up(); |
| |
| expect(forcePressStartCount, 4); |
| }); |
| |
| testWidgets('a tap and then force press initiates a force press and not a double tap', (WidgetTester tester) async { |
| await pumpGestureDetector(tester); |
| |
| final int pointerValue = tester.nextPointer; |
| final TestGesture gesture = await tester.createGesture(); |
| await gesture.downWithCustomEvent( |
| forcePressOffset, |
| PointerDownEvent( |
| pointer: pointerValue, |
| position: forcePressOffset, |
| pressure: 0.0, |
| pressureMax: 6.0, |
| pressureMin: 0.0, |
| ), |
| |
| ); |
| // Initiate a quick tap. |
| await gesture.updateWithCustomEvent( |
| PointerMoveEvent( |
| pointer: pointerValue, |
| pressure: 0.0, |
| pressureMin: 0, |
| ), |
| ); |
| await tester.pump(const Duration(milliseconds: 50)); |
| await gesture.up(); |
| |
| // Initiate a force tap. |
| await gesture.downWithCustomEvent( |
| forcePressOffset, |
| PointerDownEvent( |
| pointer: pointerValue, |
| position: forcePressOffset, |
| pressure: 0.0, |
| pressureMax: 6.0, |
| pressureMin: 0.0, |
| ), |
| ); |
| await gesture.updateWithCustomEvent(PointerMoveEvent( |
| pointer: pointerValue, |
| pressure: 0.5, |
| pressureMin: 0, |
| )); |
| expect(forcePressStartCount, 1); |
| |
| await tester.pump(const Duration(milliseconds: 50)); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| expect(forcePressEndCount, 1); |
| expect(doubleTapDownCount, 0); |
| }); |
| |
| testWidgets('a long press from a touch device is recognized as a long single tap', (WidgetTester tester) async { |
| await pumpGestureDetector(tester); |
| |
| final int pointerValue = tester.nextPointer; |
| final TestGesture gesture = await tester.startGesture( |
| const Offset(200.0, 200.0), |
| pointer: pointerValue, |
| ); |
| await tester.pump(const Duration(seconds: 2)); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| expect(tapCount, 1); |
| expect(singleTapUpCount, 0); |
| expect(singleLongTapStartCount, 1); |
| }); |
| |
| testWidgets('a long press from a mouse is just a tap', (WidgetTester tester) async { |
| await pumpGestureDetector(tester); |
| |
| final int pointerValue = tester.nextPointer; |
| final TestGesture gesture = await tester.startGesture( |
| const Offset(200.0, 200.0), |
| pointer: pointerValue, |
| kind: PointerDeviceKind.mouse, |
| ); |
| await tester.pump(const Duration(seconds: 2)); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| expect(tapCount, 1); |
| expect(singleTapUpCount, 1); |
| expect(singleLongTapStartCount, 0); |
| }); |
| |
| testWidgets('a touch drag is recognized for text selection', (WidgetTester tester) async { |
| await pumpGestureDetector(tester); |
| |
| final int pointerValue = tester.nextPointer; |
| final TestGesture gesture = await tester.startGesture( |
| const Offset(200.0, 200.0), |
| pointer: pointerValue, |
| ); |
| await tester.pump(); |
| await gesture.moveBy(const Offset(210.0, 200.0)); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| expect(tapCount, 1); |
| expect(singleTapUpCount, 0); |
| expect(singleTapCancelCount, 0); |
| expect(dragStartCount, 1); |
| expect(dragUpdateCount, 1); |
| expect(dragEndCount, 1); |
| }); |
| |
| testWidgets('a mouse drag is recognized for text selection', (WidgetTester tester) async { |
| await pumpGestureDetector(tester); |
| |
| final int pointerValue = tester.nextPointer; |
| final TestGesture gesture = await tester.startGesture( |
| const Offset(200.0, 200.0), |
| pointer: pointerValue, |
| kind: PointerDeviceKind.mouse, |
| ); |
| await tester.pump(); |
| await gesture.moveBy(const Offset(210.0, 200.0)); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| // The tap and drag gesture recognizer will detect the tap down, but not the tap up. |
| expect(tapCount, 1); |
| expect(singleTapCancelCount, 0); |
| expect(singleTapUpCount, 0); |
| |
| expect(dragStartCount, 1); |
| expect(dragUpdateCount, 1); |
| expect(dragEndCount, 1); |
| }); |
| |
| testWidgets('a slow mouse drag is still recognized for text selection', (WidgetTester tester) async { |
| await pumpGestureDetector(tester); |
| |
| final int pointerValue = tester.nextPointer; |
| final TestGesture gesture = await tester.startGesture( |
| const Offset(200.0, 200.0), |
| pointer: pointerValue, |
| kind: PointerDeviceKind.mouse, |
| ); |
| await tester.pump(const Duration(seconds: 2)); |
| await gesture.moveBy(const Offset(210.0, 200.0)); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| // The tap and drag gesture recognizer will detect the tap down, but not the tap up. |
| expect(tapCount, 1); |
| expect(singleTapCancelCount, 0); |
| expect(singleTapUpCount, 0); |
| |
| expect(dragStartCount, 1); |
| expect(dragUpdateCount, 1); |
| expect(dragEndCount, 1); |
| }); |
| |
| testWidgets('test TextSelectionGestureDetectorBuilder long press on Apple Platforms - focused renderEditable', (WidgetTester tester) async { |
| await pumpTextSelectionGestureDetectorBuilder(tester); |
| final FakeEditableTextState state = tester.state(find.byType(FakeEditableText)); |
| final FakeRenderEditable renderEditable = tester.renderObject(find.byType(FakeEditable)); |
| renderEditable.hasFocus = true; |
| |
| final TestGesture gesture = await tester.startGesture( |
| const Offset(200.0, 200.0), |
| pointer: 0, |
| ); |
| await tester.pump(const Duration(seconds: 2)); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| expect(state.showToolbarCalled, isTrue); |
| expect(renderEditable.selectPositionAtCalled, isTrue); |
| expect(renderEditable.lastCause, SelectionChangedCause.longPress); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); |
| |
| testWidgets('test TextSelectionGestureDetectorBuilder long press on iOS - renderEditable not focused', (WidgetTester tester) async { |
| await pumpTextSelectionGestureDetectorBuilder(tester); |
| final FakeEditableTextState state = tester.state(find.byType(FakeEditableText)); |
| final FakeRenderEditable renderEditable = tester.renderObject(find.byType(FakeEditable)); |
| |
| final TestGesture gesture = await tester.startGesture( |
| const Offset(200.0, 200.0), |
| pointer: 0, |
| ); |
| await tester.pump(const Duration(seconds: 2)); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| expect(state.showToolbarCalled, isTrue); |
| expect(renderEditable.selectWordCalled, isTrue); |
| expect(renderEditable.lastCause, SelectionChangedCause.longPress); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS })); |
| |
| testWidgets('test TextSelectionGestureDetectorBuilder long press on non-Apple Platforms', (WidgetTester tester) async { |
| await pumpTextSelectionGestureDetectorBuilder(tester); |
| final TestGesture gesture = await tester.startGesture( |
| const Offset(200.0, 200.0), |
| pointer: 0, |
| ); |
| await tester.pump(const Duration(seconds: 2)); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| final FakeEditableTextState state = tester.state(find.byType(FakeEditableText)); |
| final FakeRenderEditable renderEditable = tester.renderObject(find.byType(FakeEditable)); |
| expect(state.showToolbarCalled, isTrue); |
| expect(renderEditable.selectWordCalled, isTrue); |
| expect(renderEditable.lastCause, SelectionChangedCause.longPress); |
| }, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); |
| |
| testWidgets('TextSelectionGestureDetectorBuilder right click Apple platforms', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/80119 |
| await pumpTextSelectionGestureDetectorBuilder(tester); |
| |
| final FakeRenderEditable renderEditable = tester.renderObject(find.byType(FakeEditable)); |
| renderEditable.text = const TextSpan(text: 'one two three four five six seven'); |
| await tester.pump(); |
| |
| final TestGesture gesture = await tester.createGesture( |
| pointer: 0, |
| kind: PointerDeviceKind.mouse, |
| buttons: kSecondaryButton, |
| ); |
| |
| // Get the location of the 10th character |
| final Offset charLocation = renderEditable |
| .getLocalRectForCaret(const TextPosition(offset: 10)).center; |
| final Offset globalCharLocation = charLocation + tester.getTopLeft(find.byType(FakeEditable)); |
| |
| // Right clicking on a word should select it |
| await gesture.down(globalCharLocation); |
| await gesture.up(); |
| await tester.pump(); |
| expect(renderEditable.selectWordCalled, isTrue); |
| expect(renderEditable.lastCause, SelectionChangedCause.tap); |
| |
| // Right clicking on a word within a selection shouldn't change the selection |
| renderEditable.selectWordCalled = false; |
| renderEditable.selection = const TextSelection(baseOffset: 3, extentOffset: 20); |
| await gesture.down(globalCharLocation); |
| await gesture.up(); |
| await tester.pump(); |
| expect(renderEditable.selectWordCalled, isFalse); |
| |
| // Right clicking on a word within a reverse (right-to-left) selection shouldn't change the selection |
| renderEditable.selectWordCalled = false; |
| renderEditable.selection = const TextSelection(baseOffset: 20, extentOffset: 3); |
| await gesture.down(globalCharLocation); |
| await gesture.up(); |
| await tester.pump(); |
| expect(renderEditable.selectWordCalled, isFalse); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), |
| ); |
| |
| testWidgets('TextSelectionGestureDetectorBuilder right click non-Apple platforms', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/80119 |
| await pumpTextSelectionGestureDetectorBuilder(tester); |
| |
| final FakeRenderEditable renderEditable = tester.renderObject(find.byType(FakeEditable)); |
| renderEditable.text = const TextSpan(text: 'one two three four five six seven'); |
| await tester.pump(); |
| |
| final TestGesture gesture = await tester.createGesture( |
| pointer: 0, |
| kind: PointerDeviceKind.mouse, |
| buttons: kSecondaryButton, |
| ); |
| |
| // Get the location of the 10th character |
| final Offset charLocation = renderEditable |
| .getLocalRectForCaret(const TextPosition(offset: 10)).center; |
| final Offset globalCharLocation = charLocation + tester.getTopLeft(find.byType(FakeEditable)); |
| |
| // Right clicking on an unfocused field should place the cursor, not select |
| // the word. |
| await gesture.down(globalCharLocation); |
| await gesture.up(); |
| await tester.pump(); |
| expect(renderEditable.selectWordCalled, isFalse); |
| expect(renderEditable.selectPositionCalled, isTrue); |
| |
| // Right clicking on a focused field with selection shouldn't change the |
| // selection. |
| renderEditable.selectPositionCalled = false; |
| renderEditable.selection = const TextSelection(baseOffset: 3, extentOffset: 20); |
| renderEditable.hasFocus = true; |
| await gesture.down(globalCharLocation); |
| await gesture.up(); |
| await tester.pump(); |
| expect(renderEditable.selectWordCalled, isFalse); |
| expect(renderEditable.selectPositionCalled, isFalse); |
| |
| // Right clicking on a focused field with a reverse (right to left) |
| // selection shouldn't change the selection. |
| renderEditable.selection = const TextSelection(baseOffset: 20, extentOffset: 3); |
| await gesture.down(globalCharLocation); |
| await gesture.up(); |
| await tester.pump(); |
| expect(renderEditable.selectWordCalled, isFalse); |
| expect(renderEditable.selectPositionCalled, isFalse); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows }), |
| ); |
| |
| testWidgets('test TextSelectionGestureDetectorBuilder tap', (WidgetTester tester) async { |
| await pumpTextSelectionGestureDetectorBuilder(tester); |
| final TestGesture gesture = await tester.startGesture( |
| const Offset(200.0, 200.0), |
| pointer: 0, |
| ); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| final FakeEditableTextState state = tester.state(find.byType(FakeEditableText)); |
| final FakeRenderEditable renderEditable = tester.renderObject(find.byType(FakeEditable)); |
| expect(state.showToolbarCalled, isFalse); |
| |
| switch (defaultTargetPlatform) { |
| case TargetPlatform.iOS: |
| expect(renderEditable.selectWordEdgeCalled, isTrue); |
| expect(renderEditable.lastCause, SelectionChangedCause.tap); |
| case TargetPlatform.macOS: |
| case TargetPlatform.android: |
| case TargetPlatform.fuchsia: |
| case TargetPlatform.linux: |
| case TargetPlatform.windows: |
| expect(renderEditable.selectPositionAtCalled, isTrue); |
| expect(renderEditable.lastCause, SelectionChangedCause.tap); |
| } |
| }, variant: TargetPlatformVariant.all()); |
| |
| testWidgets('test TextSelectionGestureDetectorBuilder toggles toolbar on single tap on previous selection iOS', (WidgetTester tester) async { |
| await pumpTextSelectionGestureDetectorBuilder(tester); |
| |
| final FakeEditableTextState state = tester.state(find.byType(FakeEditableText)); |
| final FakeRenderEditable renderEditable = tester.renderObject(find.byType(FakeEditable)); |
| expect(state.showToolbarCalled, isFalse); |
| expect(state.toggleToolbarCalled, isFalse); |
| renderEditable.selection = const TextSelection(baseOffset: 2, extentOffset: 6); |
| renderEditable.hasFocus = true; |
| |
| final TestGesture gesture = await tester.startGesture( |
| const Offset(25.0, 200.0), |
| pointer: 0, |
| ); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| switch (defaultTargetPlatform) { |
| case TargetPlatform.iOS: |
| expect(renderEditable.selectWordEdgeCalled, isFalse); |
| expect(state.toggleToolbarCalled, isTrue); |
| case TargetPlatform.macOS: |
| case TargetPlatform.android: |
| case TargetPlatform.fuchsia: |
| case TargetPlatform.linux: |
| case TargetPlatform.windows: |
| expect(renderEditable.selectPositionAtCalled, isTrue); |
| expect(renderEditable.lastCause, SelectionChangedCause.tap); |
| } |
| }, variant: TargetPlatformVariant.all()); |
| |
| |
| testWidgets('test TextSelectionGestureDetectorBuilder shows spell check toolbar on single tap on Android', (WidgetTester tester) async { |
| await pumpTextSelectionGestureDetectorBuilder(tester); |
| |
| final FakeEditableTextState state = tester.state(find.byType(FakeEditableText)); |
| final FakeRenderEditable renderEditable = tester.renderObject(find.byType(FakeEditable)); |
| expect(state.showSpellCheckSuggestionsToolbarCalled, isFalse); |
| renderEditable.selection = const TextSelection(baseOffset: 2, extentOffset: 6); |
| renderEditable.hasFocus = true; |
| |
| final TestGesture gesture = await tester.startGesture( |
| const Offset(25.0, 200.0), |
| pointer: 0, |
| ); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| expect(state.showSpellCheckSuggestionsToolbarCalled, isTrue); |
| |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android })); |
| |
| testWidgets('test TextSelectionGestureDetectorBuilder shows spell check toolbar on single tap on iOS if word misspelled and text selection toolbar on additonal taps', (WidgetTester tester) async { |
| await pumpTextSelectionGestureDetectorBuilder(tester); |
| final FakeEditableTextState state = tester.state(find.byType(FakeEditableText)); |
| final FakeRenderEditable renderEditable = tester.renderObject(find.byType(FakeEditable)); |
| const TextSelection selection = TextSelection.collapsed(offset: 1); |
| state.updateEditingValue(const TextEditingValue(text: 'something misspelled', selection: selection)); |
| |
| // Mark word to be tapped as misspelled for testing. |
| state.markCurrentSelectionAsMisspelled = true; |
| await tester.pump(); |
| |
| // Test spell check suggestions toolbar is shown on first tap of misspelled word. |
| const Offset position = Offset(25.0, 200.0); |
| await tester.tapAt(position); |
| await tester.pumpAndSettle(); |
| |
| expect(state.showSpellCheckSuggestionsToolbarCalled, isTrue); |
| |
| // Reset and test text selection toolbar is toggled for additional taps. |
| state.showSpellCheckSuggestionsToolbarCalled = false; |
| renderEditable.selection = selection; |
| await tester.pump(const Duration(milliseconds: kSingleTapUpTimeout)); |
| |
| // Test first tap. |
| await tester.tapAt(position); |
| await tester.pumpAndSettle(); |
| |
| expect(state.showSpellCheckSuggestionsToolbarCalled, isFalse); |
| expect(state.toggleToolbarCalled, isTrue); |
| |
| // Reset and test second tap. |
| state.toggleToolbarCalled = false; |
| await tester.pump(const Duration(milliseconds: kSingleTapUpTimeout)); |
| |
| await tester.tapAt(position); |
| await tester.pumpAndSettle(); |
| |
| expect(state.showSpellCheckSuggestionsToolbarCalled, isFalse); |
| expect(state.toggleToolbarCalled, isTrue); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS })); |
| |
| testWidgets('test TextSelectionGestureDetectorBuilder double tap', (WidgetTester tester) async { |
| await pumpTextSelectionGestureDetectorBuilder(tester); |
| final TestGesture gesture = await tester.startGesture( |
| const Offset(200.0, 200.0), |
| pointer: 0, |
| ); |
| await tester.pump(const Duration(milliseconds: 50)); |
| await gesture.up(); |
| await gesture.down(const Offset(200.0, 200.0)); |
| await tester.pump(const Duration(milliseconds: 50)); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| final FakeEditableTextState state = tester.state(find.byType(FakeEditableText)); |
| final FakeRenderEditable renderEditable = tester.renderObject(find.byType(FakeEditable)); |
| expect(state.showToolbarCalled, isTrue); |
| expect(renderEditable.selectWordCalled, isTrue); |
| expect(renderEditable.lastCause, SelectionChangedCause.doubleTap); |
| }); |
| |
| testWidgets('test TextSelectionGestureDetectorBuilder forcePress enabled', (WidgetTester tester) async { |
| await pumpTextSelectionGestureDetectorBuilder(tester); |
| final TestGesture gesture = await tester.createGesture(); |
| await gesture.downWithCustomEvent( |
| const Offset(200.0, 200.0), |
| const PointerDownEvent( |
| position: Offset(200.0, 200.0), |
| pressure: 3.0, |
| pressureMax: 6.0, |
| pressureMin: 0.0, |
| ), |
| ); |
| await gesture.updateWithCustomEvent( |
| const PointerUpEvent( |
| position: Offset(200.0, 200.0), |
| pressureMax: 6.0, |
| pressureMin: 0.0, |
| ), |
| ); |
| await tester.pump(); |
| |
| final FakeEditableTextState state = tester.state(find.byType(FakeEditableText)); |
| final FakeRenderEditable renderEditable = tester.renderObject(find.byType(FakeEditable)); |
| expect(state.showToolbarCalled, isTrue); |
| expect(renderEditable.selectWordsInRangeCalled, isTrue); |
| }); |
| |
| testWidgets('Mouse drag does not show handles nor toolbar', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/69001 |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Scaffold( |
| body: SelectableText('I love Flutter!'), |
| ), |
| ), |
| ); |
| |
| final Offset textFieldStart = tester.getTopLeft(find.byType(SelectableText)); |
| |
| final TestGesture gesture = await tester.startGesture(textFieldStart, kind: PointerDeviceKind.mouse); |
| await tester.pump(); |
| await gesture.moveTo(textFieldStart + const Offset(50.0, 0)); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| final EditableTextState editableText = tester.state(find.byType(EditableText)); |
| expect(editableText.selectionOverlay!.handlesAreVisible, isFalse); |
| expect(editableText.selectionOverlay!.toolbarIsVisible, isFalse); |
| }); |
| |
| testWidgets('Mouse drag selects and cannot drag cursor', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/102928 |
| final TextEditingController controller = TextEditingController( |
| text: 'I love flutter!', |
| ); |
| final GlobalKey<EditableTextState> editableTextKey = GlobalKey<EditableTextState>(); |
| final FakeTextSelectionGestureDetectorBuilderDelegate delegate = FakeTextSelectionGestureDetectorBuilderDelegate( |
| editableTextKey: editableTextKey, |
| forcePressEnabled: false, |
| selectionEnabled: true, |
| ); |
| final TextSelectionGestureDetectorBuilder provider = |
| TextSelectionGestureDetectorBuilder(delegate: delegate); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: provider.buildGestureDetector( |
| behavior: HitTestBehavior.translucent, |
| child: EditableText( |
| key: editableTextKey, |
| controller: controller, |
| focusNode: FocusNode(), |
| backgroundCursorColor: Colors.white, |
| cursorColor: Colors.white, |
| style: const TextStyle(), |
| selectionControls: materialTextSelectionControls, |
| ), |
| ), |
| ), |
| ); |
| |
| expect(controller.selection.isCollapsed, isTrue); |
| expect(controller.selection.baseOffset, -1); |
| |
| final Offset position = textOffsetToPosition(tester, 4); |
| |
| await tester.tapAt(position); |
| // Don't do a double tap drag. |
| await tester.pump(const Duration(milliseconds: 300)); |
| |
| expect(controller.selection.isCollapsed, isTrue); |
| expect(controller.selection.baseOffset, 4); |
| |
| final TestGesture gesture = await tester.startGesture(position, kind: PointerDeviceKind.mouse); |
| |
| // Checking that double-tap was not registered. |
| expect(controller.selection.isCollapsed, isTrue); |
| expect(controller.selection.baseOffset, 4); |
| |
| addTearDown(gesture.removePointer); |
| await tester.pump(); |
| await gesture.moveTo(textOffsetToPosition(tester, 7)); |
| await tester.pump(); |
| await gesture.moveTo(textOffsetToPosition(tester, 10)); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.isCollapsed, isFalse); |
| expect(controller.selection.baseOffset, 4); |
| expect(controller.selection.extentOffset, 10); |
| }); |
| |
| testWidgets('Touch drag moves the cursor', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/102928 |
| final TextEditingController controller = TextEditingController( |
| text: 'I love flutter!', |
| ); |
| final GlobalKey<EditableTextState> editableTextKey = GlobalKey<EditableTextState>(); |
| final FakeTextSelectionGestureDetectorBuilderDelegate delegate = FakeTextSelectionGestureDetectorBuilderDelegate( |
| editableTextKey: editableTextKey, |
| forcePressEnabled: false, |
| selectionEnabled: true, |
| ); |
| final TextSelectionGestureDetectorBuilder provider = |
| TextSelectionGestureDetectorBuilder(delegate: delegate); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: provider.buildGestureDetector( |
| behavior: HitTestBehavior.translucent, |
| child: EditableText( |
| key: editableTextKey, |
| controller: controller, |
| focusNode: FocusNode(), |
| backgroundCursorColor: Colors.white, |
| cursorColor: Colors.white, |
| style: const TextStyle(), |
| selectionControls: materialTextSelectionControls, |
| ), |
| ), |
| ), |
| ); |
| |
| expect(controller.selection.isCollapsed, isTrue); |
| expect(controller.selection.baseOffset, -1); |
| |
| final Offset position = textOffsetToPosition(tester, 4); |
| |
| await tester.tapAt(position); |
| await tester.pump(); |
| |
| expect(controller.selection.isCollapsed, isTrue); |
| expect(controller.selection.baseOffset, 4); |
| |
| final TestGesture gesture = await tester.startGesture(position); |
| addTearDown(gesture.removePointer); |
| await tester.pump(); |
| await gesture.moveTo(textOffsetToPosition(tester, 7)); |
| await tester.pump(); |
| await gesture.moveTo(textOffsetToPosition(tester, 10)); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.isCollapsed, isTrue); |
| expect(controller.selection.baseOffset, 10); |
| }); |
| |
| testWidgets('Stylus drag moves the cursor', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/102928 |
| final TextEditingController controller = TextEditingController( |
| text: 'I love flutter!', |
| ); |
| final GlobalKey<EditableTextState> editableTextKey = GlobalKey<EditableTextState>(); |
| final FakeTextSelectionGestureDetectorBuilderDelegate delegate = FakeTextSelectionGestureDetectorBuilderDelegate( |
| editableTextKey: editableTextKey, |
| forcePressEnabled: false, |
| selectionEnabled: true, |
| ); |
| final TextSelectionGestureDetectorBuilder provider = |
| TextSelectionGestureDetectorBuilder(delegate: delegate); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: provider.buildGestureDetector( |
| behavior: HitTestBehavior.translucent, |
| child: EditableText( |
| key: editableTextKey, |
| controller: controller, |
| focusNode: FocusNode(), |
| backgroundCursorColor: Colors.white, |
| cursorColor: Colors.white, |
| style: const TextStyle(), |
| selectionControls: materialTextSelectionControls, |
| ), |
| ), |
| ), |
| ); |
| |
| expect(controller.selection.isCollapsed, isTrue); |
| expect(controller.selection.baseOffset, -1); |
| |
| final Offset position = textOffsetToPosition(tester, 4); |
| |
| await tester.tapAt(position); |
| await tester.pump(); |
| |
| expect(controller.selection.isCollapsed, isTrue); |
| expect(controller.selection.baseOffset, 4); |
| |
| final TestGesture gesture = await tester.startGesture(position, kind: PointerDeviceKind.stylus); |
| addTearDown(gesture.removePointer); |
| await tester.pump(); |
| await gesture.moveTo(textOffsetToPosition(tester, 7)); |
| await tester.pump(); |
| await gesture.moveTo(textOffsetToPosition(tester, 10)); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.isCollapsed, isTrue); |
| expect(controller.selection.baseOffset, 10); |
| }); |
| |
| testWidgets('Drag of unknown type moves the cursor', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/102928 |
| final TextEditingController controller = TextEditingController( |
| text: 'I love flutter!', |
| ); |
| final GlobalKey<EditableTextState> editableTextKey = GlobalKey<EditableTextState>(); |
| final FakeTextSelectionGestureDetectorBuilderDelegate delegate = FakeTextSelectionGestureDetectorBuilderDelegate( |
| editableTextKey: editableTextKey, |
| forcePressEnabled: false, |
| selectionEnabled: true, |
| ); |
| final TextSelectionGestureDetectorBuilder provider = |
| TextSelectionGestureDetectorBuilder(delegate: delegate); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: provider.buildGestureDetector( |
| behavior: HitTestBehavior.translucent, |
| child: EditableText( |
| key: editableTextKey, |
| controller: controller, |
| focusNode: FocusNode(), |
| backgroundCursorColor: Colors.white, |
| cursorColor: Colors.white, |
| style: const TextStyle(), |
| selectionControls: materialTextSelectionControls, |
| ), |
| ), |
| ), |
| ); |
| |
| expect(controller.selection.isCollapsed, isTrue); |
| expect(controller.selection.baseOffset, -1); |
| |
| final Offset position = textOffsetToPosition(tester, 4); |
| |
| await tester.tapAt(position); |
| await tester.pump(); |
| |
| expect(controller.selection.isCollapsed, isTrue); |
| expect(controller.selection.baseOffset, 4); |
| |
| final TestGesture gesture = await tester.startGesture(position, kind: PointerDeviceKind.unknown); |
| addTearDown(gesture.removePointer); |
| await tester.pump(); |
| await gesture.moveTo(textOffsetToPosition(tester, 7)); |
| await tester.pump(); |
| await gesture.moveTo(textOffsetToPosition(tester, 10)); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.isCollapsed, isTrue); |
| expect(controller.selection.baseOffset, 10); |
| }); |
| |
| testWidgets('test TextSelectionGestureDetectorBuilder drag with RenderEditable viewport offset change', (WidgetTester tester) async { |
| await pumpTextSelectionGestureDetectorBuilder(tester); |
| final FakeRenderEditable renderEditable = tester.renderObject(find.byType(FakeEditable)); |
| |
| // Reconfigure the RenderEditable for multi-line. |
| renderEditable.maxLines = null; |
| renderEditable.offset = ViewportOffset.fixed(20.0); |
| renderEditable.layout(const BoxConstraints.tightFor(width: 400, height: 300.0)); |
| await tester.pumpAndSettle(); |
| |
| final TestGesture gesture = await tester.startGesture( |
| const Offset(200.0, 200.0), |
| kind: PointerDeviceKind.mouse, |
| ); |
| await tester.pumpAndSettle(); |
| expect(renderEditable.selectPositionAtCalled, isFalse); |
| |
| await gesture.moveTo(const Offset(300.0, 200.0)); |
| await tester.pumpAndSettle(); |
| expect(renderEditable.selectPositionAtCalled, isTrue); |
| expect(renderEditable.selectPositionAtFrom, const Offset(200.0, 200.0)); |
| expect(renderEditable.selectPositionAtTo, const Offset(300.0, 200.0)); |
| |
| // Move the viewport offset (scroll). |
| renderEditable.offset = ViewportOffset.fixed(150.0); |
| renderEditable.layout(const BoxConstraints.tightFor(width: 400, height: 300.0)); |
| await tester.pumpAndSettle(); |
| |
| await gesture.moveTo(const Offset(300.0, 400.0)); |
| await tester.pumpAndSettle(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| expect(renderEditable.selectPositionAtCalled, isTrue); |
| expect(renderEditable.selectPositionAtFrom, const Offset(200.0, 70.0)); |
| expect(renderEditable.selectPositionAtTo, const Offset(300.0, 400.0)); |
| }); |
| |
| testWidgets('test TextSelectionGestureDetectorBuilder selection disabled', (WidgetTester tester) async { |
| await pumpTextSelectionGestureDetectorBuilder(tester, selectionEnabled: false); |
| final TestGesture gesture = await tester.startGesture( |
| const Offset(200.0, 200.0), |
| pointer: 0, |
| ); |
| await tester.pump(const Duration(seconds: 2)); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| final FakeEditableTextState state = tester.state(find.byType(FakeEditableText)); |
| final FakeRenderEditable renderEditable = tester.renderObject(find.byType(FakeEditable)); |
| expect(state.showToolbarCalled, isTrue); |
| expect(renderEditable.selectWordsInRangeCalled, isFalse); |
| }); |
| |
| testWidgets('test TextSelectionGestureDetectorBuilder mouse drag disabled', (WidgetTester tester) async { |
| await pumpTextSelectionGestureDetectorBuilder(tester, selectionEnabled: false); |
| final TestGesture gesture = await tester.startGesture( |
| Offset.zero, |
| kind: PointerDeviceKind.mouse, |
| ); |
| await tester.pump(); |
| await gesture.moveTo(const Offset(50.0, 0)); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| final FakeRenderEditable renderEditable = tester.renderObject(find.byType(FakeEditable)); |
| expect(renderEditable.selectPositionAtCalled, isFalse); |
| }); |
| |
| testWidgets('test TextSelectionGestureDetectorBuilder forcePress disabled', (WidgetTester tester) async { |
| await pumpTextSelectionGestureDetectorBuilder(tester, forcePressEnabled: false); |
| final TestGesture gesture = await tester.createGesture(); |
| await gesture.downWithCustomEvent( |
| const Offset(200.0, 200.0), |
| const PointerDownEvent( |
| position: Offset(200.0, 200.0), |
| pressure: 3.0, |
| pressureMax: 6.0, |
| pressureMin: 0.0, |
| ), |
| ); |
| await gesture.up(); |
| await tester.pump(); |
| |
| final FakeEditableTextState state = tester.state(find.byType(FakeEditableText)); |
| final FakeRenderEditable renderEditable = tester.renderObject(find.byType(FakeEditable)); |
| expect(state.showToolbarCalled, isFalse); |
| expect(renderEditable.selectWordsInRangeCalled, isFalse); |
| }); |
| |
| // Regression test for https://github.com/flutter/flutter/issues/37032. |
| testWidgets("selection handle's GestureDetector should not cover the entire screen", (WidgetTester tester) async { |
| final TextEditingController controller = TextEditingController(text: 'a'); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Scaffold( |
| body: TextField( |
| autofocus: true, |
| controller: controller, |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.pumpAndSettle(); |
| |
| final Finder gestureDetector = find.descendant( |
| of: find.byType(CompositedTransformFollower), |
| matching: find.descendant( |
| of: find.byType(FadeTransition), |
| matching: find.byType(RawGestureDetector), |
| ), |
| ); |
| |
| expect(gestureDetector, findsOneWidget); |
| // The GestureDetector's size should not exceed that of the TextField. |
| final Rect hitRect = tester.getRect(gestureDetector); |
| final Rect textFieldRect = tester.getRect(find.byType(TextField)); |
| |
| expect(hitRect.size.width, lessThan(textFieldRect.size.width)); |
| expect(hitRect.size.height, lessThan(textFieldRect.size.height)); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS })); |
| |
| group('SelectionOverlay', () { |
| Future<SelectionOverlay> pumpApp(WidgetTester tester, { |
| ValueChanged<DragStartDetails>? onStartDragStart, |
| ValueChanged<DragUpdateDetails>? onStartDragUpdate, |
| ValueChanged<DragEndDetails>? onStartDragEnd, |
| ValueChanged<DragStartDetails>? onEndDragStart, |
| ValueChanged<DragUpdateDetails>? onEndDragUpdate, |
| ValueChanged<DragEndDetails>? onEndDragEnd, |
| VoidCallback? onSelectionHandleTapped, |
| TextSelectionControls? selectionControls, |
| TextMagnifierConfiguration? magnifierConfiguration, |
| }) async { |
| final UniqueKey column = UniqueKey(); |
| final LayerLink startHandleLayerLink = LayerLink(); |
| final LayerLink endHandleLayerLink = LayerLink(); |
| final LayerLink toolbarLayerLink = LayerLink(); |
| await tester.pumpWidget(MaterialApp( |
| home: Column( |
| key: column, |
| children: <Widget>[ |
| CompositedTransformTarget( |
| link: startHandleLayerLink, |
| child: const Text('start handle'), |
| ), |
| CompositedTransformTarget( |
| link: endHandleLayerLink, |
| child: const Text('end handle'), |
| ), |
| CompositedTransformTarget( |
| link: toolbarLayerLink, |
| child: const Text('toolbar'), |
| ), |
| ], |
| ), |
| )); |
| |
| return SelectionOverlay( |
| context: tester.element(find.byKey(column)), |
| onSelectionHandleTapped: onSelectionHandleTapped, |
| startHandleType: TextSelectionHandleType.collapsed, |
| startHandleLayerLink: startHandleLayerLink, |
| lineHeightAtStart: 0.0, |
| onStartHandleDragStart: onStartDragStart, |
| onStartHandleDragUpdate: onStartDragUpdate, |
| onStartHandleDragEnd: onStartDragEnd, |
| endHandleType: TextSelectionHandleType.collapsed, |
| endHandleLayerLink: endHandleLayerLink, |
| lineHeightAtEnd: 0.0, |
| onEndHandleDragStart: onEndDragStart, |
| onEndHandleDragUpdate: onEndDragUpdate, |
| onEndHandleDragEnd: onEndDragEnd, |
| clipboardStatus: FakeClipboardStatusNotifier(), |
| selectionDelegate: FakeTextSelectionDelegate(), |
| selectionControls: selectionControls, |
| selectionEndpoints: const <TextSelectionPoint>[], |
| toolbarLayerLink: toolbarLayerLink, |
| magnifierConfiguration: magnifierConfiguration ?? TextMagnifierConfiguration.disabled, |
| ); |
| } |
| |
| testWidgets('can show and hide handles', (WidgetTester tester) async { |
| final TextSelectionControlsSpy spy = TextSelectionControlsSpy(); |
| final SelectionOverlay selectionOverlay = await pumpApp( |
| tester, |
| selectionControls: spy, |
| ); |
| selectionOverlay |
| ..startHandleType = TextSelectionHandleType.left |
| ..endHandleType = TextSelectionHandleType.right |
| ..selectionEndpoints = const <TextSelectionPoint>[ |
| TextSelectionPoint(Offset(10, 10), TextDirection.ltr), |
| TextSelectionPoint(Offset(20, 20), TextDirection.ltr), |
| ]; |
| selectionOverlay.showHandles(); |
| await tester.pump(); |
| expect(find.byKey(spy.leftHandleKey), findsOneWidget); |
| expect(find.byKey(spy.rightHandleKey), findsOneWidget); |
| |
| selectionOverlay.hideHandles(); |
| await tester.pump(); |
| expect(find.byKey(spy.leftHandleKey), findsNothing); |
| expect(find.byKey(spy.rightHandleKey), findsNothing); |
| |
| selectionOverlay.showToolbar(); |
| await tester.pump(); |
| expect(find.byKey(spy.toolBarKey), findsOneWidget); |
| |
| selectionOverlay.hideToolbar(); |
| await tester.pump(); |
| expect(find.byKey(spy.toolBarKey), findsNothing); |
| |
| selectionOverlay.showHandles(); |
| selectionOverlay.showToolbar(); |
| await tester.pump(); |
| expect(find.byKey(spy.leftHandleKey), findsOneWidget); |
| expect(find.byKey(spy.rightHandleKey), findsOneWidget); |
| expect(find.byKey(spy.toolBarKey), findsOneWidget); |
| |
| selectionOverlay.hide(); |
| await tester.pump(); |
| expect(find.byKey(spy.leftHandleKey), findsNothing); |
| expect(find.byKey(spy.rightHandleKey), findsNothing); |
| expect(find.byKey(spy.toolBarKey), findsNothing); |
| }); |
| |
| testWidgets('only paints one collapsed handle', (WidgetTester tester) async { |
| final TextSelectionControlsSpy spy = TextSelectionControlsSpy(); |
| final SelectionOverlay selectionOverlay = await pumpApp( |
| tester, |
| selectionControls: spy, |
| ); |
| selectionOverlay |
| ..startHandleType = TextSelectionHandleType.collapsed |
| ..endHandleType = TextSelectionHandleType.collapsed |
| ..selectionEndpoints = const <TextSelectionPoint>[ |
| TextSelectionPoint(Offset(10, 10), TextDirection.ltr), |
| TextSelectionPoint(Offset(20, 20), TextDirection.ltr), |
| ]; |
| selectionOverlay.showHandles(); |
| await tester.pump(); |
| expect(find.byKey(spy.leftHandleKey), findsNothing); |
| expect(find.byKey(spy.rightHandleKey), findsNothing); |
| expect(find.byKey(spy.collapsedHandleKey), findsOneWidget); |
| }); |
| |
| testWidgets('can change handle parameter', (WidgetTester tester) async { |
| final TextSelectionControlsSpy spy = TextSelectionControlsSpy(); |
| final SelectionOverlay selectionOverlay = await pumpApp( |
| tester, |
| selectionControls: spy, |
| ); |
| selectionOverlay |
| ..startHandleType = TextSelectionHandleType.left |
| ..lineHeightAtStart = 10.0 |
| ..endHandleType = TextSelectionHandleType.right |
| ..lineHeightAtEnd = 11.0 |
| ..selectionEndpoints = const <TextSelectionPoint>[ |
| TextSelectionPoint(Offset(10, 10), TextDirection.ltr), |
| TextSelectionPoint(Offset(20, 20), TextDirection.ltr), |
| ]; |
| selectionOverlay.showHandles(); |
| await tester.pump(); |
| Text leftHandle = tester.widget(find.byKey(spy.leftHandleKey)) as Text; |
| Text rightHandle = tester.widget(find.byKey(spy.rightHandleKey)) as Text; |
| expect(leftHandle.data, 'height 10'); |
| expect(rightHandle.data, 'height 11'); |
| |
| selectionOverlay |
| ..startHandleType = TextSelectionHandleType.right |
| ..lineHeightAtStart = 12.0 |
| ..endHandleType = TextSelectionHandleType.left |
| ..lineHeightAtEnd = 13.0; |
| await tester.pump(); |
| leftHandle = tester.widget(find.byKey(spy.leftHandleKey)) as Text; |
| rightHandle = tester.widget(find.byKey(spy.rightHandleKey)) as Text; |
| expect(leftHandle.data, 'height 13'); |
| expect(rightHandle.data, 'height 12'); |
| }); |
| |
| testWidgets('can trigger selection handle onTap', (WidgetTester tester) async { |
| bool selectionHandleTapped = false; |
| void handleTapped() => selectionHandleTapped = true; |
| final TextSelectionControlsSpy spy = TextSelectionControlsSpy(); |
| final SelectionOverlay selectionOverlay = await pumpApp( |
| tester, |
| onSelectionHandleTapped: handleTapped, |
| selectionControls: spy, |
| ); |
| selectionOverlay |
| ..startHandleType = TextSelectionHandleType.left |
| ..lineHeightAtStart = 10.0 |
| ..endHandleType = TextSelectionHandleType.right |
| ..lineHeightAtEnd = 11.0 |
| ..selectionEndpoints = const <TextSelectionPoint>[ |
| TextSelectionPoint(Offset(10, 10), TextDirection.ltr), |
| TextSelectionPoint(Offset(20, 20), TextDirection.ltr), |
| ]; |
| selectionOverlay.showHandles(); |
| await tester.pump(); |
| expect(find.byKey(spy.leftHandleKey), findsOneWidget); |
| expect(find.byKey(spy.rightHandleKey), findsOneWidget); |
| expect(selectionHandleTapped, isFalse); |
| |
| await tester.tap(find.byKey(spy.leftHandleKey)); |
| expect(selectionHandleTapped, isTrue); |
| |
| selectionHandleTapped = false; |
| await tester.tap(find.byKey(spy.rightHandleKey)); |
| expect(selectionHandleTapped, isTrue); |
| }); |
| |
| testWidgets('can trigger selection handle drag', (WidgetTester tester) async { |
| DragStartDetails? startDragStartDetails; |
| DragUpdateDetails? startDragUpdateDetails; |
| DragEndDetails? startDragEndDetails; |
| DragStartDetails? endDragStartDetails; |
| DragUpdateDetails? endDragUpdateDetails; |
| DragEndDetails? endDragEndDetails; |
| void startDragStart(DragStartDetails details) => startDragStartDetails = details; |
| void startDragUpdate(DragUpdateDetails details) => startDragUpdateDetails = details; |
| void startDragEnd(DragEndDetails details) => startDragEndDetails = details; |
| void endDragStart(DragStartDetails details) => endDragStartDetails = details; |
| void endDragUpdate(DragUpdateDetails details) => endDragUpdateDetails = details; |
| void endDragEnd(DragEndDetails details) => endDragEndDetails = details; |
| final TextSelectionControlsSpy spy = TextSelectionControlsSpy(); |
| final SelectionOverlay selectionOverlay = await pumpApp( |
| tester, |
| onStartDragStart: startDragStart, |
| onStartDragUpdate: startDragUpdate, |
| onStartDragEnd: startDragEnd, |
| onEndDragStart: endDragStart, |
| onEndDragUpdate: endDragUpdate, |
| onEndDragEnd: endDragEnd, |
| selectionControls: spy, |
| ); |
| selectionOverlay |
| ..startHandleType = TextSelectionHandleType.left |
| ..lineHeightAtStart = 10.0 |
| ..endHandleType = TextSelectionHandleType.right |
| ..lineHeightAtEnd = 11.0 |
| ..selectionEndpoints = const <TextSelectionPoint>[ |
| TextSelectionPoint(Offset(10, 10), TextDirection.ltr), |
| TextSelectionPoint(Offset(20, 20), TextDirection.ltr), |
| ]; |
| selectionOverlay.showHandles(); |
| await tester.pump(); |
| expect(find.byKey(spy.leftHandleKey), findsOneWidget); |
| expect(find.byKey(spy.rightHandleKey), findsOneWidget); |
| expect(startDragStartDetails, isNull); |
| expect(startDragUpdateDetails, isNull); |
| expect(startDragEndDetails, isNull); |
| expect(endDragStartDetails, isNull); |
| expect(endDragUpdateDetails, isNull); |
| expect(endDragEndDetails, isNull); |
| |
| final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byKey(spy.leftHandleKey))); |
| await tester.pump(const Duration(milliseconds: 200)); |
| expect(startDragStartDetails!.globalPosition, tester.getCenter(find.byKey(spy.leftHandleKey))); |
| |
| const Offset newLocation = Offset(20, 20); |
| await gesture.moveTo(newLocation); |
| await tester.pump(const Duration(milliseconds: 20)); |
| expect(startDragUpdateDetails!.globalPosition, newLocation); |
| |
| await gesture.up(); |
| await tester.pump(const Duration(milliseconds: 20)); |
| expect(startDragEndDetails, isNotNull); |
| |
| final TestGesture gesture2 = await tester.startGesture(tester.getCenter(find.byKey(spy.rightHandleKey))); |
| addTearDown(gesture2.removePointer); |
| await tester.pump(const Duration(milliseconds: 200)); |
| expect(endDragStartDetails!.globalPosition, tester.getCenter(find.byKey(spy.rightHandleKey))); |
| |
| await gesture2.moveTo(newLocation); |
| await tester.pump(const Duration(milliseconds: 20)); |
| expect(endDragUpdateDetails!.globalPosition, newLocation); |
| |
| await gesture2.up(); |
| await tester.pump(const Duration(milliseconds: 20)); |
| expect(endDragEndDetails, isNotNull); |
| }); |
| |
| testWidgets('can show magnifier when no handles exist', (WidgetTester tester) async { |
| final GlobalKey magnifierKey = GlobalKey(); |
| final SelectionOverlay selectionOverlay = await pumpApp( |
| tester, |
| magnifierConfiguration: TextMagnifierConfiguration( |
| shouldDisplayHandlesInMagnifier: false, |
| magnifierBuilder: (BuildContext context, MagnifierController controller, ValueNotifier<MagnifierInfo>? notifier) { |
| return SizedBox.shrink( |
| key: magnifierKey, |
| ); |
| }, |
| ), |
| ); |
| |
| expect(find.byKey(magnifierKey), findsNothing); |
| |
| final MagnifierInfo info = MagnifierInfo( |
| globalGesturePosition: Offset.zero, |
| caretRect: Offset.zero & const Size(5.0, 20.0), |
| fieldBounds: Offset.zero & const Size(200.0, 50.0), |
| currentLineBoundaries: Offset.zero & const Size(200.0, 50.0), |
| ); |
| selectionOverlay.showMagnifier(info); |
| await tester.pump(); |
| |
| expect(tester.takeException(), isNull); |
| expect(find.byKey(magnifierKey), findsOneWidget); |
| }); |
| }); |
| |
| group('ClipboardStatusNotifier', () { |
| group('when Clipboard fails', () { |
| setUp(() { |
| final MockClipboard mockClipboard = MockClipboard(hasStringsThrows: true); |
| TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, mockClipboard.handleMethodCall); |
| }); |
| |
| tearDown(() { |
| TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, null); |
| }); |
| |
| test('Clipboard API failure is gracefully recovered from', () async { |
| final ClipboardStatusNotifier notifier = ClipboardStatusNotifier(); |
| expect(notifier.value, ClipboardStatus.unknown); |
| |
| await expectLater(notifier.update(), completes); |
| expect(notifier.value, ClipboardStatus.unknown); |
| }); |
| }); |
| |
| group('when Clipboard succeeds', () { |
| final MockClipboard mockClipboard = MockClipboard(); |
| |
| setUp(() { |
| TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, mockClipboard.handleMethodCall); |
| }); |
| |
| tearDown(() { |
| TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, null); |
| }); |
| |
| test('update sets value based on clipboard contents', () async { |
| final ClipboardStatusNotifier notifier = ClipboardStatusNotifier(); |
| expect(notifier.value, ClipboardStatus.unknown); |
| |
| await expectLater(notifier.update(), completes); |
| expect(notifier.value, ClipboardStatus.notPasteable); |
| |
| mockClipboard.handleMethodCall(const MethodCall( |
| 'Clipboard.setData', |
| <String, dynamic>{ |
| 'text': 'pasteablestring', |
| }, |
| )); |
| await expectLater(notifier.update(), completes); |
| expect(notifier.value, ClipboardStatus.pasteable); |
| }); |
| }); |
| }); |
| |
| testWidgets('Mouse edge scrolling works in an outer scrollable', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/102484 |
| final TextEditingController controller = TextEditingController( |
| text: 'I love flutter!\n' * 8, |
| ); |
| final GlobalKey<EditableTextState> editableTextKey = GlobalKey<EditableTextState>(); |
| final FakeTextSelectionGestureDetectorBuilderDelegate delegate = FakeTextSelectionGestureDetectorBuilderDelegate( |
| editableTextKey: editableTextKey, |
| forcePressEnabled: false, |
| selectionEnabled: true, |
| ); |
| |
| final ScrollController scrollController = ScrollController(); |
| const double kLineHeight = 16.0; |
| final TextSelectionGestureDetectorBuilder provider = |
| TextSelectionGestureDetectorBuilder( |
| delegate: delegate, |
| ); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Scaffold( |
| body: SizedBox( |
| // Only 4 lines visible of 8 given. |
| height: kLineHeight * 4, |
| child: SingleChildScrollView( |
| controller: scrollController, |
| child: provider.buildGestureDetector( |
| behavior: HitTestBehavior.translucent, |
| child: EditableText( |
| key: editableTextKey, |
| controller: controller, |
| focusNode: FocusNode(), |
| backgroundCursorColor: Colors.white, |
| cursorColor: Colors.white, |
| style: const TextStyle(), |
| selectionControls: materialTextSelectionControls, |
| // EditableText will expand to the full 8 line height and will |
| // not scroll itself. |
| maxLines: null, |
| ), |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(controller.selection.isCollapsed, isTrue); |
| expect(controller.selection.baseOffset, -1); |
| expect(scrollController.position.pixels, 0.0); |
| |
| final Offset position = textOffsetToPosition(tester, 4); |
| |
| await tester.tapAt(position); |
| await tester.pump(); |
| |
| expect(controller.selection.isCollapsed, isTrue); |
| expect(controller.selection.baseOffset, 4); |
| |
| // Select all text with the mouse. |
| final TestGesture gesture = await tester.startGesture(position, kind: PointerDeviceKind.mouse); |
| addTearDown(gesture.removePointer); |
| await tester.pump(); |
| await gesture.moveTo(textOffsetToPosition(tester, (controller.text.length / 2).floor())); |
| await tester.pump(); |
| await gesture.moveTo(textOffsetToPosition(tester, controller.text.length)); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.isCollapsed, isFalse); |
| expect(controller.selection.baseOffset, 4); |
| expect(controller.selection.extentOffset, controller.text.length); |
| expect(scrollController.position.pixels, scrollController.position.maxScrollExtent); |
| }); |
| |
| testWidgets('Mouse edge scrolling works with both an outer scrollable and scrolling in the EditableText', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/102484 |
| final TextEditingController controller = TextEditingController( |
| text: 'I love flutter!\n' * 8, |
| ); |
| final GlobalKey<EditableTextState> editableTextKey = GlobalKey<EditableTextState>(); |
| final FakeTextSelectionGestureDetectorBuilderDelegate delegate = FakeTextSelectionGestureDetectorBuilderDelegate( |
| editableTextKey: editableTextKey, |
| forcePressEnabled: false, |
| selectionEnabled: true, |
| ); |
| |
| final ScrollController scrollController = ScrollController(); |
| const double kLineHeight = 16.0; |
| final TextSelectionGestureDetectorBuilder provider = |
| TextSelectionGestureDetectorBuilder( |
| delegate: delegate, |
| ); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Scaffold( |
| body: SizedBox( |
| // Only 4 lines visible of 8 given. |
| height: kLineHeight * 4, |
| child: SingleChildScrollView( |
| controller: scrollController, |
| child: provider.buildGestureDetector( |
| behavior: HitTestBehavior.translucent, |
| child: EditableText( |
| key: editableTextKey, |
| controller: controller, |
| focusNode: FocusNode(), |
| backgroundCursorColor: Colors.white, |
| cursorColor: Colors.white, |
| style: const TextStyle(), |
| selectionControls: materialTextSelectionControls, |
| // EditableText is taller than the SizedBox but not taller |
| // than the text. |
| maxLines: 6, |
| ), |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(controller.selection.isCollapsed, isTrue); |
| expect(controller.selection.baseOffset, -1); |
| expect(scrollController.position.pixels, 0.0); |
| |
| final Offset position = textOffsetToPosition(tester, 4); |
| |
| await tester.tapAt(position); |
| await tester.pump(); |
| |
| expect(controller.selection.isCollapsed, isTrue); |
| expect(controller.selection.baseOffset, 4); |
| |
| // Select all text with the mouse. |
| final TestGesture gesture = await tester.startGesture(position, kind: PointerDeviceKind.mouse); |
| addTearDown(gesture.removePointer); |
| await tester.pump(); |
| await gesture.moveTo(textOffsetToPosition(tester, (controller.text.length / 2).floor())); |
| await tester.pump(); |
| await gesture.moveTo(textOffsetToPosition(tester, controller.text.length)); |
| await tester.pump(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.selection.isCollapsed, isFalse); |
| expect(controller.selection.baseOffset, 4); |
| expect(controller.selection.extentOffset, controller.text.length); |
| expect(scrollController.position.pixels, scrollController.position.maxScrollExtent); |
| }); |
| } |
| |
| class FakeTextSelectionGestureDetectorBuilderDelegate implements TextSelectionGestureDetectorBuilderDelegate { |
| FakeTextSelectionGestureDetectorBuilderDelegate({ |
| required this.editableTextKey, |
| required this.forcePressEnabled, |
| required this.selectionEnabled, |
| }); |
| |
| @override |
| final GlobalKey<EditableTextState> editableTextKey; |
| |
| @override |
| final bool forcePressEnabled; |
| |
| @override |
| final bool selectionEnabled; |
| } |
| |
| class FakeEditableText extends EditableText { |
| FakeEditableText({super.key}): super( |
| controller: TextEditingController(), |
| focusNode: FocusNode(), |
| backgroundCursorColor: Colors.white, |
| cursorColor: Colors.white, |
| style: const TextStyle(), |
| ); |
| |
| @override |
| FakeEditableTextState createState() => FakeEditableTextState(); |
| } |
| |
| class FakeEditableTextState extends EditableTextState { |
| final GlobalKey _editableKey = GlobalKey(); |
| bool showToolbarCalled = false; |
| bool toggleToolbarCalled = false; |
| bool showSpellCheckSuggestionsToolbarCalled = false; |
| bool markCurrentSelectionAsMisspelled = false; |
| |
| @override |
| RenderEditable get renderEditable => _editableKey.currentContext!.findRenderObject()! as RenderEditable; |
| |
| @override |
| bool showToolbar() { |
| showToolbarCalled = true; |
| return true; |
| } |
| |
| @override |
| void toggleToolbar([bool hideHandles = true]) { |
| toggleToolbarCalled = true; |
| return; |
| } |
| |
| @override |
| bool showSpellCheckSuggestionsToolbar() { |
| showSpellCheckSuggestionsToolbarCalled = true; |
| return true; |
| } |
| |
| @override |
| SuggestionSpan? findSuggestionSpanAtCursorIndex(int cursorIndex) { |
| return markCurrentSelectionAsMisspelled |
| ? const SuggestionSpan( |
| TextRange(start: 7, end: 12), |
| <String>['word', 'world', 'old'], |
| ) : null; |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| super.build(context); |
| return FakeEditable(this, key: _editableKey); |
| } |
| } |
| |
| class FakeEditable extends LeafRenderObjectWidget { |
| const FakeEditable( |
| this.delegate, { |
| super.key, |
| }); |
| final EditableTextState delegate; |
| |
| @override |
| RenderEditable createRenderObject(BuildContext context) { |
| return FakeRenderEditable(delegate); |
| } |
| } |
| |
| class FakeRenderEditable extends RenderEditable { |
| FakeRenderEditable(EditableTextState delegate) : super( |
| text: const TextSpan( |
| style: TextStyle(height: 1.0, fontSize: 10.0), |
| text: 'placeholder', |
| ), |
| startHandleLayerLink: LayerLink(), |
| endHandleLayerLink: LayerLink(), |
| ignorePointer: true, |
| textAlign: TextAlign.start, |
| textDirection: TextDirection.ltr, |
| locale: const Locale('en', 'US'), |
| offset: ViewportOffset.fixed(10.0), |
| textSelectionDelegate: delegate, |
| selection: const TextSelection.collapsed( |
| offset: 0, |
| ), |
| ); |
| |
| SelectionChangedCause? lastCause; |
| |
| bool selectWordsInRangeCalled = false; |
| @override |
| void selectWordsInRange({ required Offset from, Offset? to, required SelectionChangedCause cause }) { |
| selectWordsInRangeCalled = true; |
| hasFocus = true; |
| lastCause = cause; |
| } |
| |
| bool selectWordEdgeCalled = false; |
| @override |
| void selectWordEdge({ required SelectionChangedCause cause }) { |
| selectWordEdgeCalled = true; |
| hasFocus = true; |
| lastCause = cause; |
| } |
| |
| bool selectPositionAtCalled = false; |
| Offset? selectPositionAtFrom; |
| Offset? selectPositionAtTo; |
| @override |
| void selectPositionAt({ required Offset from, Offset? to, required SelectionChangedCause cause }) { |
| selectPositionAtCalled = true; |
| selectPositionAtFrom = from; |
| selectPositionAtTo = to; |
| hasFocus = true; |
| lastCause = cause; |
| } |
| |
| bool selectPositionCalled = false; |
| @override |
| void selectPosition({ required SelectionChangedCause cause }) { |
| selectPositionCalled = true; |
| lastCause = cause; |
| return super.selectPosition(cause: cause); |
| } |
| |
| bool selectWordCalled = false; |
| @override |
| void selectWord({ required SelectionChangedCause cause }) { |
| selectWordCalled = true; |
| hasFocus = true; |
| lastCause = cause; |
| } |
| |
| @override |
| bool hasFocus = false; |
| } |
| |
| class TextSelectionControlsSpy extends TextSelectionControls { |
| UniqueKey leftHandleKey = UniqueKey(); |
| UniqueKey rightHandleKey = UniqueKey(); |
| UniqueKey collapsedHandleKey = UniqueKey(); |
| UniqueKey toolBarKey = UniqueKey(); |
| |
| @override |
| Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight, [VoidCallback? onTap]) { |
| switch (type) { |
| case TextSelectionHandleType.left: |
| return ElevatedButton(onPressed: onTap, child: Text('height ${textLineHeight.toInt()}', key: leftHandleKey)); |
| case TextSelectionHandleType.right: |
| return ElevatedButton(onPressed: onTap, child: Text('height ${textLineHeight.toInt()}', key: rightHandleKey)); |
| case TextSelectionHandleType.collapsed: |
| return ElevatedButton(onPressed: onTap, child: Text('height ${textLineHeight.toInt()}', key: collapsedHandleKey)); |
| } |
| } |
| |
| @override |
| Widget buildToolbar( |
| BuildContext context, |
| Rect globalEditableRegion, |
| double textLineHeight, |
| Offset position, |
| List<TextSelectionPoint> endpoints, |
| TextSelectionDelegate delegate, |
| ValueListenable<ClipboardStatus>? clipboardStatus, |
| Offset? lastSecondaryTapDownPosition, |
| ) { |
| return Text('dummy', key: toolBarKey); |
| } |
| |
| @override |
| Offset getHandleAnchor(TextSelectionHandleType type, double textLineHeight) { |
| return Offset.zero; |
| } |
| |
| @override |
| Size getHandleSize(double textLineHeight) { |
| return Size(textLineHeight, textLineHeight); |
| } |
| } |
| |
| class FakeClipboardStatusNotifier extends ClipboardStatusNotifier { |
| FakeClipboardStatusNotifier() : super(value: ClipboardStatus.unknown); |
| |
| bool updateCalled = false; |
| @override |
| Future<void> update() async { |
| updateCalled = true; |
| } |
| } |
| |
| class FakeTextSelectionDelegate extends Fake implements TextSelectionDelegate { |
| @override |
| void cutSelection(SelectionChangedCause cause) { } |
| |
| @override |
| void copySelection(SelectionChangedCause cause) { } |
| } |