blob: 85104e39da4779641608e2f7d71169d6dac34ed7 [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 'dart:ui' show PointerDeviceKind;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized();
const textStyle = TextStyle();
const cursorColor = Color.fromARGB(0xFF, 0xFF, 0x00, 0x00);
final calls = <MethodCall>[];
var isFeatureAvailableReturnValue = true;
late TextEditingController controller;
late FocusNode focusNode;
setUp(() async {
calls.clear();
isFeatureAvailableReturnValue = true;
binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.scribe, (
MethodCall methodCall,
) {
calls.add(methodCall);
return switch (methodCall.method) {
'Scribe.isFeatureAvailable' => Future<bool>.value(isFeatureAvailableReturnValue),
'Scribe.startStylusHandwriting' => Future<void>.value(),
_ => throw FlutterError('Unexpected method call: ${methodCall.method}'),
};
});
controller = TextEditingController(text: 'Lorem ipsum dolor sit amet');
focusNode = FocusNode(debugLabel: 'EditableText Node');
});
tearDown(() {
controller.dispose();
focusNode.dispose();
});
Future<void> pumpTextSelectionGestureDetectorBuilder(
WidgetTester tester, {
bool forcePressEnabled = true,
bool selectionEnabled = true,
}) async {
final editableTextKey = GlobalKey<EditableTextState>();
final delegate = FakeTextSelectionGestureDetectorBuilderDelegate(
editableTextKey: editableTextKey,
forcePressEnabled: forcePressEnabled,
selectionEnabled: selectionEnabled,
);
final provider = TextSelectionGestureDetectorBuilder(delegate: delegate);
await tester.pumpWidget(
MaterialApp(
home: provider.buildGestureDetector(
behavior: HitTestBehavior.translucent,
child: EditableText(
key: editableTextKey,
controller: controller,
backgroundCursorColor: Colors.grey,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
selectionControls: materialTextSelectionControls,
showSelectionHandles: true,
),
),
),
);
}
testWidgets(
'when Scribe is available, starts handwriting on tap down',
(WidgetTester tester) async {
isFeatureAvailableReturnValue = true;
await pumpTextSelectionGestureDetectorBuilder(tester);
expect(focusNode.hasFocus, isFalse);
final TestGesture gesture = await tester.createGesture(
kind: PointerDeviceKind.stylus,
pointer: 1,
);
await gesture.down(tester.getCenter(find.byType(EditableText)));
// Wait for the gesture arena.
await tester.pumpAndSettle();
expect(calls, hasLength(2));
expect(calls.first.method, 'Scribe.isFeatureAvailable');
expect(calls[1].method, 'Scribe.startStylusHandwriting');
await gesture.up();
expect(focusNode.hasFocus, isTrue);
// On web, let the browser handle handwriting input.
},
skip: kIsWeb, // [intended]
variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.android}),
);
testWidgets(
'when Scribe is unavailable, does not start handwriting on tap down',
(WidgetTester tester) async {
isFeatureAvailableReturnValue = false;
await pumpTextSelectionGestureDetectorBuilder(tester);
expect(focusNode.hasFocus, isFalse);
final TestGesture gesture = await tester.createGesture(
kind: PointerDeviceKind.stylus,
pointer: 1,
);
await gesture.down(tester.getCenter(find.byType(EditableText)));
// Wait for the gesture arena.
await tester.pumpAndSettle();
expect(calls, hasLength(1));
expect(calls.first.method, 'Scribe.isFeatureAvailable');
await gesture.up();
},
skip: kIsWeb, // [intended]
variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.android}),
);
testWidgets(
'tap down event must be from a stylus in order to start handwriting',
(WidgetTester tester) async {
isFeatureAvailableReturnValue = true;
await pumpTextSelectionGestureDetectorBuilder(tester);
expect(focusNode.hasFocus, isFalse);
final TestGesture gesture = await tester.createGesture(
kind: PointerDeviceKind.mouse,
pointer: 1,
);
await gesture.down(tester.getCenter(find.byType(EditableText)));
expect(calls, isEmpty);
await gesture.up();
},
skip: kIsWeb, // [intended]
variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.android}),
);
testWidgets(
'tap down event on a collapsed selection handle is handled by the handle and does not start handwriting',
(WidgetTester tester) async {
isFeatureAvailableReturnValue = true;
await pumpTextSelectionGestureDetectorBuilder(tester);
expect(focusNode.hasFocus, isFalse);
expect(find.byType(CompositedTransformFollower), findsNothing);
// Tap to show the collapsed selection handle.
final Offset fieldOffset = tester.getTopLeft(find.byType(EditableText));
await tester.tapAt(fieldOffset + const Offset(20.0, 10.0));
await tester.pump();
expect(find.byType(CompositedTransformFollower), findsOneWidget);
final TestGesture gesture = await tester.createGesture(
kind: PointerDeviceKind.stylus,
pointer: 1,
);
final Finder handleFinder = find.descendant(
of: find.byType(CompositedTransformFollower),
matching: find.byType(CustomPaint),
);
await gesture.down(tester.getCenter(handleFinder));
// Wait for the gesture arena.
await tester.pumpAndSettle();
expect(calls, hasLength(0));
expect(controller.selection.isCollapsed, isTrue);
final int cursorStart = controller.selection.start;
// Dragging on top of the handle moves it like normal.
await gesture.moveBy(const Offset(20.0, 0.0));
expect(controller.selection.isCollapsed, isTrue);
expect(controller.selection.start, greaterThan(cursorStart));
expect(calls, hasLength(0));
await gesture.up();
},
skip: kIsWeb, // [intended]
variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.android}),
);
testWidgets(
'tap down event on the end selection handle is handled by the handle and does not start handwriting',
(WidgetTester tester) async {
isFeatureAvailableReturnValue = true;
await pumpTextSelectionGestureDetectorBuilder(tester);
expect(focusNode.hasFocus, isFalse);
expect(find.byType(CompositedTransformFollower), findsNothing);
// Long press to select the first word and show both handles.
final Offset fieldOffset = tester.getTopLeft(find.byType(EditableText));
await tester.longPressAt(fieldOffset + const Offset(20.0, 10.0));
await tester.pump();
expect(find.byType(CompositedTransformFollower), findsNWidgets(2));
final TestGesture gesture = await tester.createGesture(
kind: PointerDeviceKind.stylus,
pointer: 1,
);
final Finder endHandleFinder = find.descendant(
of: find.byType(CompositedTransformFollower).at(1),
matching: find.byType(CustomPaint),
);
await gesture.down(tester.getCenter(endHandleFinder));
// Wait for the gesture arena.
await tester.pumpAndSettle();
expect(calls, isEmpty);
expect(controller.selection.isCollapsed, isFalse);
final TextSelection selectionStart = controller.selection;
// Dragging on top of the handle extends selection like normal.
await gesture.moveBy(const Offset(20.0, 0.0));
expect(controller.selection.isCollapsed, isFalse);
expect(controller.selection.start, equals(selectionStart.start));
expect(controller.selection.end, greaterThan(selectionStart.end));
expect(calls, isEmpty);
await gesture.up();
},
skip: kIsWeb, // [intended]
variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.android}),
);
}
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;
}