3599 lines
138 KiB
Dart
3599 lines
138 KiB
Dart
// 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:io' show Platform;
|
||
|
||
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 '../rendering/mock_canvas.dart';
|
||
import '../rendering/recording_canvas.dart';
|
||
import 'rendering_tester.dart';
|
||
|
||
class FakeEditableTextState with TextSelectionDelegate {
|
||
@override
|
||
TextEditingValue textEditingValue = TextEditingValue.empty;
|
||
|
||
@override
|
||
void hideToolbar([bool hideHandles = true]) { }
|
||
|
||
@override
|
||
void userUpdateTextEditingValue(TextEditingValue value, SelectionChangedCause cause) { }
|
||
|
||
@override
|
||
void bringIntoView(TextPosition position) { }
|
||
}
|
||
|
||
void main() {
|
||
test('RenderEditable respects clipBehavior', () {
|
||
const BoxConstraints viewport = BoxConstraints(maxHeight: 100.0, maxWidth: 100.0);
|
||
final TestClipPaintingContext context = TestClipPaintingContext();
|
||
|
||
final String longString = 'a' * 10000;
|
||
|
||
// By default, clipBehavior should be Clip.none
|
||
final RenderEditable defaultEditable = RenderEditable(
|
||
text: TextSpan(text: longString),
|
||
textDirection: TextDirection.ltr,
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
offset: ViewportOffset.zero(),
|
||
textSelectionDelegate: FakeEditableTextState(),
|
||
selection: const TextSelection(baseOffset: 0, extentOffset: 0),
|
||
);
|
||
layout(defaultEditable, constraints: viewport, phase: EnginePhase.composite, onErrors: expectOverflowedErrors);
|
||
defaultEditable.paint(context, Offset.zero);
|
||
expect(context.clipBehavior, equals(Clip.hardEdge));
|
||
|
||
context.clipBehavior = Clip.none; // Reset as Clip.none won't write into clipBehavior.
|
||
for (final Clip clip in Clip.values) {
|
||
final RenderEditable editable = RenderEditable(
|
||
text: TextSpan(text: longString),
|
||
textDirection: TextDirection.ltr,
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
offset: ViewportOffset.zero(),
|
||
textSelectionDelegate: FakeEditableTextState(),
|
||
selection: const TextSelection(baseOffset: 0, extentOffset: 0),
|
||
clipBehavior: clip,
|
||
);
|
||
layout(editable, constraints: viewport, phase: EnginePhase.composite, onErrors: expectOverflowedErrors);
|
||
editable.paint(context, Offset.zero);
|
||
expect(context.clipBehavior, equals(clip));
|
||
}
|
||
});
|
||
|
||
test('editable intrinsics', () {
|
||
final TextSelectionDelegate delegate = FakeEditableTextState();
|
||
final RenderEditable editable = RenderEditable(
|
||
text: const TextSpan(
|
||
style: TextStyle(height: 1.0, fontSize: 10.0, fontFamily: 'Ahem'),
|
||
text: '12345',
|
||
),
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
textAlign: TextAlign.start,
|
||
textDirection: TextDirection.ltr,
|
||
locale: const Locale('ja', 'JP'),
|
||
offset: ViewportOffset.zero(),
|
||
textSelectionDelegate: delegate,
|
||
);
|
||
expect(editable.getMinIntrinsicWidth(double.infinity), 50.0);
|
||
// The width includes the width of the cursor (1.0).
|
||
expect(editable.getMaxIntrinsicWidth(double.infinity), 52.0);
|
||
expect(editable.getMinIntrinsicHeight(double.infinity), 10.0);
|
||
expect(editable.getMaxIntrinsicHeight(double.infinity), 10.0);
|
||
|
||
expect(
|
||
editable.toStringDeep(minLevel: DiagnosticLevel.info),
|
||
equalsIgnoringHashCodes(
|
||
'RenderEditable#00000 NEEDS-LAYOUT NEEDS-PAINT NEEDS-COMPOSITING-BITS-UPDATE DETACHED\n'
|
||
' │ parentData: MISSING\n'
|
||
' │ constraints: MISSING\n'
|
||
' │ size: MISSING\n'
|
||
' │ cursorColor: null\n'
|
||
' │ showCursor: ValueNotifier<bool>#00000(false)\n'
|
||
' │ maxLines: 1\n'
|
||
' │ minLines: null\n'
|
||
' │ selectionColor: null\n'
|
||
' │ textScaleFactor: 1.0\n'
|
||
' │ locale: ja_JP\n'
|
||
' │ selection: null\n'
|
||
' │ offset: _FixedViewportOffset#00000(offset: 0.0)\n'
|
||
' ╘═╦══ text ═══\n'
|
||
' ║ TextSpan:\n'
|
||
' ║ inherit: true\n'
|
||
' ║ family: Ahem\n'
|
||
' ║ size: 10.0\n'
|
||
' ║ height: 1.0x\n'
|
||
' ║ "12345"\n'
|
||
' ╚═══════════\n',
|
||
),
|
||
);
|
||
});
|
||
|
||
// Test that clipping will be used even when the text fits within the visible
|
||
// region if the start position of the text is offset (e.g. during scrolling
|
||
// animation).
|
||
test('correct clipping', () {
|
||
final TextSelectionDelegate delegate = FakeEditableTextState();
|
||
final RenderEditable editable = RenderEditable(
|
||
text: const TextSpan(
|
||
style: TextStyle(height: 1.0, fontSize: 10.0, fontFamily: 'Ahem'),
|
||
text: 'A',
|
||
),
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
textAlign: TextAlign.start,
|
||
textDirection: TextDirection.ltr,
|
||
locale: const Locale('en', 'US'),
|
||
offset: ViewportOffset.fixed(10.0),
|
||
textSelectionDelegate: delegate,
|
||
selection: const TextSelection.collapsed(
|
||
offset: 0,
|
||
),
|
||
);
|
||
layout(editable, constraints: BoxConstraints.loose(const Size(500.0, 500.0)));
|
||
// Prepare for painting after layout.
|
||
pumpFrame(phase: EnginePhase.compositingBits);
|
||
expect(
|
||
(Canvas canvas) => editable.paint(TestRecordingPaintingContext(canvas), Offset.zero),
|
||
paints..clipRect(rect: const Rect.fromLTRB(0.0, 0.0, 500.0, 10.0)),
|
||
);
|
||
});
|
||
|
||
test('Can change cursor color, radius, visibility', () {
|
||
final TextSelectionDelegate delegate = FakeEditableTextState();
|
||
final ValueNotifier<bool> showCursor = ValueNotifier<bool>(true);
|
||
EditableText.debugDeterministicCursor = true;
|
||
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: const Color.fromARGB(0xFF, 0xFF, 0x00, 0x00),
|
||
offset: ViewportOffset.zero(),
|
||
textSelectionDelegate: delegate,
|
||
text: const TextSpan(
|
||
text: 'test',
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
selection: const TextSelection.collapsed(
|
||
offset: 4,
|
||
affinity: TextAffinity.upstream,
|
||
),
|
||
);
|
||
|
||
layout(editable);
|
||
|
||
editable.layout(BoxConstraints.loose(const Size(100, 100)));
|
||
// Prepare for painting after layout.
|
||
pumpFrame(phase: EnginePhase.compositingBits);
|
||
|
||
expect(
|
||
editable,
|
||
// Draw no cursor by default.
|
||
paintsExactlyCountTimes(#drawRect, 0),
|
||
);
|
||
|
||
editable.showCursor = showCursor;
|
||
pumpFrame(phase: EnginePhase.compositingBits);
|
||
|
||
expect(editable, paints..rect(
|
||
color: const Color.fromARGB(0xFF, 0xFF, 0x00, 0x00),
|
||
rect: const Rect.fromLTWH(40, 0, 1, 10),
|
||
));
|
||
|
||
// Now change to a rounded caret.
|
||
editable.cursorColor = const Color.fromARGB(0xFF, 0x00, 0x00, 0xFF);
|
||
editable.cursorWidth = 4;
|
||
editable.cursorRadius = const Radius.circular(3);
|
||
pumpFrame(phase: EnginePhase.compositingBits);
|
||
|
||
expect(editable, paints..rrect(
|
||
color: const Color.fromARGB(0xFF, 0x00, 0x00, 0xFF),
|
||
rrect: RRect.fromRectAndRadius(
|
||
const Rect.fromLTWH(40, 0, 4, 10),
|
||
const Radius.circular(3),
|
||
),
|
||
));
|
||
|
||
editable.textScaleFactor = 2;
|
||
pumpFrame(phase: EnginePhase.compositingBits);
|
||
|
||
// Now the caret height is much bigger due to the bigger font scale.
|
||
expect(editable, paints..rrect(
|
||
color: const Color.fromARGB(0xFF, 0x00, 0x00, 0xFF),
|
||
rrect: RRect.fromRectAndRadius(
|
||
const Rect.fromLTWH(80, 0, 4, 20),
|
||
const Radius.circular(3),
|
||
),
|
||
));
|
||
|
||
// Can turn off caret.
|
||
showCursor.value = false;
|
||
pumpFrame(phase: EnginePhase.compositingBits);
|
||
|
||
expect(editable, paintsExactlyCountTimes(#drawRRect, 0));
|
||
});
|
||
|
||
test('Can change textAlign', () {
|
||
final TextSelectionDelegate delegate = FakeEditableTextState();
|
||
|
||
final RenderEditable editable = RenderEditable(
|
||
textAlign: TextAlign.start,
|
||
textDirection: TextDirection.ltr,
|
||
offset: ViewportOffset.zero(),
|
||
textSelectionDelegate: delegate,
|
||
text: const TextSpan(text: 'test'),
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
);
|
||
|
||
layout(editable);
|
||
|
||
editable.layout(BoxConstraints.loose(const Size(100, 100)));
|
||
expect(editable.textAlign, TextAlign.start);
|
||
expect(editable.debugNeedsLayout, isFalse);
|
||
|
||
editable.textAlign = TextAlign.center;
|
||
expect(editable.textAlign, TextAlign.center);
|
||
expect(editable.debugNeedsLayout, isTrue);
|
||
});
|
||
|
||
test('Cursor with ideographic script', () {
|
||
final TextSelectionDelegate delegate = FakeEditableTextState();
|
||
final ValueNotifier<bool> showCursor = ValueNotifier<bool>(true);
|
||
EditableText.debugDeterministicCursor = true;
|
||
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: const Color.fromARGB(0xFF, 0xFF, 0x00, 0x00),
|
||
offset: ViewportOffset.zero(),
|
||
textSelectionDelegate: delegate,
|
||
text: const TextSpan(
|
||
text: '中文测试文本是否正确',
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
selection: const TextSelection.collapsed(
|
||
offset: 4,
|
||
affinity: TextAffinity.upstream,
|
||
),
|
||
);
|
||
|
||
layout(editable, constraints: BoxConstraints.loose(const Size(100, 100)));
|
||
pumpFrame(phase: EnginePhase.compositingBits);
|
||
expect(
|
||
editable,
|
||
// Draw no cursor by default.
|
||
paintsExactlyCountTimes(#drawRect, 0),
|
||
);
|
||
|
||
editable.showCursor = showCursor;
|
||
pumpFrame(phase: EnginePhase.compositingBits);
|
||
|
||
expect(editable, paints..rect(
|
||
color: const Color.fromARGB(0xFF, 0xFF, 0x00, 0x00),
|
||
rect: const Rect.fromLTWH(40, 0, 1, 10),
|
||
));
|
||
|
||
// Now change to a rounded caret.
|
||
editable.cursorColor = const Color.fromARGB(0xFF, 0x00, 0x00, 0xFF);
|
||
editable.cursorWidth = 4;
|
||
editable.cursorRadius = const Radius.circular(3);
|
||
pumpFrame(phase: EnginePhase.compositingBits);
|
||
|
||
expect(editable, paints..rrect(
|
||
color: const Color.fromARGB(0xFF, 0x00, 0x00, 0xFF),
|
||
rrect: RRect.fromRectAndRadius(
|
||
const Rect.fromLTWH(40, 0, 4, 10),
|
||
const Radius.circular(3),
|
||
),
|
||
));
|
||
|
||
editable.textScaleFactor = 2;
|
||
pumpFrame(phase: EnginePhase.compositingBits);
|
||
|
||
// Now the caret height is much bigger due to the bigger font scale.
|
||
expect(editable, paints..rrect(
|
||
color: const Color.fromARGB(0xFF, 0x00, 0x00, 0xFF),
|
||
rrect: RRect.fromRectAndRadius(
|
||
const Rect.fromLTWH(80, 0, 4, 20),
|
||
const Radius.circular(3),
|
||
),
|
||
));
|
||
|
||
// Can turn off caret.
|
||
showCursor.value = false;
|
||
pumpFrame(phase: EnginePhase.compositingBits);
|
||
|
||
expect(editable, paintsExactlyCountTimes(#drawRRect, 0));
|
||
|
||
// TODO(yjbanov): ahem.ttf doesn't have Chinese glyphs, making this test
|
||
// sensitive to browser/OS when running in web mode:
|
||
// https://github.com/flutter/flutter/issues/83129
|
||
}, skip: kIsWeb);
|
||
|
||
test('text is painted above selection', () {
|
||
final TextSelectionDelegate delegate = FakeEditableTextState();
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: Colors.red,
|
||
offset: ViewportOffset.zero(),
|
||
textSelectionDelegate: delegate,
|
||
text: const TextSpan(
|
||
text: 'test',
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
selection: const TextSelection(
|
||
baseOffset: 0,
|
||
extentOffset: 3,
|
||
affinity: TextAffinity.upstream,
|
||
),
|
||
);
|
||
|
||
layout(editable);
|
||
|
||
expect(
|
||
editable,
|
||
paints
|
||
// Check that it's the black selection box, not the red cursor.
|
||
..rect(color: Colors.black)
|
||
..paragraph(),
|
||
);
|
||
|
||
// There is exactly one rect paint (1 selection, 0 cursor).
|
||
expect(editable, paintsExactlyCountTimes(#drawRect, 1));
|
||
});
|
||
|
||
test('cursor can paint above or below the text', () {
|
||
final TextSelectionDelegate delegate = FakeEditableTextState();
|
||
final ValueNotifier<bool> showCursor = ValueNotifier<bool>(true);
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
paintCursorAboveText: true,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: Colors.red,
|
||
showCursor: showCursor,
|
||
offset: ViewportOffset.zero(),
|
||
textSelectionDelegate: delegate,
|
||
text: const TextSpan(
|
||
text: 'test',
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
selection: const TextSelection.collapsed(
|
||
offset: 2,
|
||
affinity: TextAffinity.upstream,
|
||
),
|
||
);
|
||
|
||
layout(editable);
|
||
|
||
expect(
|
||
editable,
|
||
paints
|
||
..paragraph()
|
||
// Red collapsed cursor is painted, not a selection box.
|
||
..rect(color: Colors.red[500]),
|
||
);
|
||
|
||
// There is exactly one rect paint (0 selection, 1 cursor).
|
||
expect(editable, paintsExactlyCountTimes(#drawRect, 1));
|
||
|
||
editable.paintCursorAboveText = false;
|
||
pumpFrame(phase: EnginePhase.compositingBits);
|
||
|
||
expect(
|
||
editable,
|
||
// The paint order is now flipped.
|
||
paints
|
||
..rect(color: Colors.red[500])
|
||
..paragraph(),
|
||
);
|
||
expect(editable, paintsExactlyCountTimes(#drawRect, 1));
|
||
});
|
||
|
||
test('selects correct place with offsets', () {
|
||
const String text = 'test\ntest';
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(text: text);
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
late TextSelection currentSelection;
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: Colors.red,
|
||
offset: viewportOffset,
|
||
// This makes the scroll axis vertical.
|
||
maxLines: 2,
|
||
textSelectionDelegate: delegate,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {
|
||
currentSelection = selection;
|
||
},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: const TextSpan(
|
||
text: text,
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
selection: const TextSelection.collapsed(
|
||
offset: 4,
|
||
),
|
||
);
|
||
|
||
layout(editable);
|
||
|
||
expect(
|
||
editable,
|
||
paints..paragraph(offset: Offset.zero),
|
||
);
|
||
|
||
editable.selectPositionAt(from: const Offset(0, 2), cause: SelectionChangedCause.tap);
|
||
pumpFrame();
|
||
|
||
expect(currentSelection.isCollapsed, true);
|
||
expect(currentSelection.baseOffset, 0);
|
||
|
||
viewportOffset.correctBy(10);
|
||
|
||
pumpFrame(phase: EnginePhase.compositingBits);
|
||
|
||
expect(
|
||
editable,
|
||
paints..paragraph(offset: const Offset(0, -10)),
|
||
);
|
||
|
||
// Tap the same place. But because the offset is scrolled up, the second line
|
||
// gets tapped instead.
|
||
editable.selectPositionAt(from: const Offset(0, 2), cause: SelectionChangedCause.tap);
|
||
pumpFrame();
|
||
|
||
expect(currentSelection.isCollapsed, true);
|
||
expect(currentSelection.baseOffset, 5);
|
||
|
||
// Test the other selection methods.
|
||
// Move over by one character.
|
||
editable.handleTapDown(TapDownDetails(globalPosition: const Offset(10, 2)));
|
||
pumpFrame();
|
||
editable.selectPosition(cause:SelectionChangedCause.tap);
|
||
pumpFrame();
|
||
expect(currentSelection.isCollapsed, true);
|
||
expect(currentSelection.baseOffset, 6);
|
||
|
||
editable.handleTapDown(TapDownDetails(globalPosition: const Offset(20, 2)));
|
||
pumpFrame();
|
||
editable.selectWord(cause:SelectionChangedCause.longPress);
|
||
pumpFrame();
|
||
expect(currentSelection.isCollapsed, false);
|
||
expect(currentSelection.baseOffset, 5);
|
||
expect(currentSelection.extentOffset, 9);
|
||
|
||
// Select one more character down but since it's still part of the same
|
||
// word, the same word is selected.
|
||
editable.selectWordsInRange(from: const Offset(30, 2), cause:SelectionChangedCause.longPress);
|
||
pumpFrame();
|
||
expect(currentSelection.isCollapsed, false);
|
||
expect(currentSelection.baseOffset, 5);
|
||
expect(currentSelection.extentOffset, 9);
|
||
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61026
|
||
|
||
test('selects readonly renderEditable matches native behavior for android', () {
|
||
// Regression test for https://github.com/flutter/flutter/issues/79166.
|
||
final TargetPlatform? previousPlatform = debugDefaultTargetPlatformOverride;
|
||
debugDefaultTargetPlatformOverride = TargetPlatform.android;
|
||
const String text = ' test';
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(text: text);
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
late TextSelection currentSelection;
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: Colors.red,
|
||
readOnly: true,
|
||
offset: viewportOffset,
|
||
textSelectionDelegate: delegate,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {
|
||
currentSelection = selection;
|
||
},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: const TextSpan(
|
||
text: text,
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
selection: const TextSelection.collapsed(
|
||
offset: 4,
|
||
),
|
||
);
|
||
|
||
layout(editable);
|
||
|
||
// Select the second white space, where the text position = 1.
|
||
editable.selectWordsInRange(from: const Offset(10, 2), cause:SelectionChangedCause.longPress);
|
||
pumpFrame();
|
||
expect(currentSelection.isCollapsed, false);
|
||
expect(currentSelection.baseOffset, 1);
|
||
expect(currentSelection.extentOffset, 2);
|
||
debugDefaultTargetPlatformOverride = previousPlatform;
|
||
});
|
||
|
||
test('selects renderEditable matches native behavior for iOS case 1', () {
|
||
// Regression test for https://github.com/flutter/flutter/issues/79166.
|
||
final TargetPlatform? previousPlatform = debugDefaultTargetPlatformOverride;
|
||
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
|
||
const String text = ' test';
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(text: text);
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
late TextSelection currentSelection;
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: Colors.red,
|
||
offset: viewportOffset,
|
||
textSelectionDelegate: delegate,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {
|
||
currentSelection = selection;
|
||
},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: const TextSpan(
|
||
text: text,
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
selection: const TextSelection.collapsed(
|
||
offset: 4,
|
||
),
|
||
);
|
||
|
||
layout(editable);
|
||
|
||
// Select the second white space, where the text position = 1.
|
||
editable.selectWordsInRange(from: const Offset(10, 2), cause:SelectionChangedCause.longPress);
|
||
pumpFrame();
|
||
expect(currentSelection.isCollapsed, false);
|
||
expect(currentSelection.baseOffset, 1);
|
||
expect(currentSelection.extentOffset, 6);
|
||
debugDefaultTargetPlatformOverride = previousPlatform;
|
||
});
|
||
|
||
test('selects renderEditable matches native behavior for iOS case 2', () {
|
||
// Regression test for https://github.com/flutter/flutter/issues/79166.
|
||
final TargetPlatform? previousPlatform = debugDefaultTargetPlatformOverride;
|
||
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
|
||
const String text = ' ';
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(text: text);
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
late TextSelection currentSelection;
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: Colors.red,
|
||
offset: viewportOffset,
|
||
textSelectionDelegate: delegate,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {
|
||
currentSelection = selection;
|
||
},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: const TextSpan(
|
||
text: text,
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
selection: const TextSelection.collapsed(
|
||
offset: 4,
|
||
),
|
||
);
|
||
|
||
layout(editable);
|
||
|
||
// Select the second white space, where the text position = 1.
|
||
editable.selectWordsInRange(from: const Offset(10, 2), cause:SelectionChangedCause.longPress);
|
||
pumpFrame();
|
||
expect(currentSelection.isCollapsed, true);
|
||
expect(currentSelection.baseOffset, 1);
|
||
expect(currentSelection.extentOffset, 1);
|
||
debugDefaultTargetPlatformOverride = previousPlatform;
|
||
});
|
||
|
||
test('selects correct place when offsets are flipped', () {
|
||
const String text = 'abc def ghi';
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(text: text);
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
late TextSelection currentSelection;
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: Colors.red,
|
||
offset: viewportOffset,
|
||
textSelectionDelegate: delegate,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {
|
||
currentSelection = selection;
|
||
},
|
||
text: const TextSpan(
|
||
text: text,
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
);
|
||
|
||
layout(editable);
|
||
|
||
editable.selectPositionAt(from: const Offset(30, 2), to: const Offset(10, 2), cause: SelectionChangedCause.drag);
|
||
pumpFrame();
|
||
|
||
expect(currentSelection.isCollapsed, isFalse);
|
||
expect(currentSelection.baseOffset, 3);
|
||
expect(currentSelection.extentOffset, 1);
|
||
});
|
||
|
||
test('selection does not flicker as user is dragging', () {
|
||
int selectionChangedCount = 0;
|
||
TextSelection? updatedSelection;
|
||
const String text = 'abc def ghi';
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(text: text);
|
||
const TextSpan span = TextSpan(
|
||
text: text,
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
);
|
||
|
||
final RenderEditable editable1 = RenderEditable(
|
||
textSelectionDelegate: delegate,
|
||
textDirection: TextDirection.ltr,
|
||
offset: ViewportOffset.zero(),
|
||
selection: const TextSelection(baseOffset: 3, extentOffset: 4),
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {
|
||
selectionChangedCount++;
|
||
updatedSelection = selection;
|
||
},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: span,
|
||
);
|
||
|
||
layout(editable1);
|
||
|
||
// Shouldn't cause a selection change.
|
||
editable1.selectPositionAt(from: const Offset(30, 2), to: const Offset(42, 2), cause: SelectionChangedCause.drag);
|
||
pumpFrame();
|
||
|
||
expect(updatedSelection, isNull);
|
||
expect(selectionChangedCount, 0);
|
||
|
||
final RenderEditable editable2 = RenderEditable(
|
||
textSelectionDelegate: delegate,
|
||
textDirection: TextDirection.ltr,
|
||
offset: ViewportOffset.zero(),
|
||
selection: const TextSelection(baseOffset: 3, extentOffset: 4),
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {
|
||
selectionChangedCount++;
|
||
updatedSelection = selection;
|
||
},
|
||
text: span,
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
);
|
||
|
||
layout(editable2);
|
||
|
||
// Now this should cause a selection change.
|
||
editable2.selectPositionAt(from: const Offset(30, 2), to: const Offset(48, 2), cause: SelectionChangedCause.drag);
|
||
pumpFrame();
|
||
|
||
expect(updatedSelection!.baseOffset, 3);
|
||
expect(updatedSelection!.extentOffset, 5);
|
||
expect(selectionChangedCount, 1);
|
||
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61028
|
||
|
||
test('promptRect disappears when promptRectColor is set to null', () {
|
||
const Color promptRectColor = Color(0x12345678);
|
||
final TextSelectionDelegate delegate = FakeEditableTextState();
|
||
final RenderEditable editable = RenderEditable(
|
||
text: const TextSpan(
|
||
style: TextStyle(height: 1.0, fontSize: 10.0, fontFamily: 'Ahem'),
|
||
text: 'ABCDEFG',
|
||
),
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
textAlign: TextAlign.start,
|
||
textDirection: TextDirection.ltr,
|
||
locale: const Locale('en', 'US'),
|
||
offset: ViewportOffset.fixed(10.0),
|
||
textSelectionDelegate: delegate,
|
||
selection: const TextSelection.collapsed(offset: 0),
|
||
promptRectColor: promptRectColor,
|
||
promptRectRange: const TextRange(start: 0, end: 1),
|
||
);
|
||
|
||
layout(editable, constraints: BoxConstraints.loose(const Size(1000.0, 1000.0)));
|
||
pumpFrame(phase: EnginePhase.compositingBits);
|
||
|
||
expect(
|
||
(Canvas canvas) => editable.paint(TestRecordingPaintingContext(canvas), Offset.zero),
|
||
paints..rect(color: promptRectColor),
|
||
);
|
||
|
||
editable.promptRectColor = null;
|
||
|
||
editable.layout(BoxConstraints.loose(const Size(1000.0, 1000.0)));
|
||
pumpFrame(phase: EnginePhase.compositingBits);
|
||
|
||
expect(editable.promptRectColor, null);
|
||
expect(
|
||
(Canvas canvas) => editable.paint(TestRecordingPaintingContext(canvas), Offset.zero),
|
||
isNot(paints..rect(color: promptRectColor)),
|
||
);
|
||
});
|
||
|
||
test('editable hasFocus correctly initialized', () {
|
||
// Regression test for https://github.com/flutter/flutter/issues/21640
|
||
final TextSelectionDelegate delegate = FakeEditableTextState();
|
||
final RenderEditable editable = RenderEditable(
|
||
text: const TextSpan(
|
||
style: TextStyle(height: 1.0, fontSize: 10.0, fontFamily: 'Ahem'),
|
||
text: '12345',
|
||
),
|
||
textAlign: TextAlign.start,
|
||
textDirection: TextDirection.ltr,
|
||
locale: const Locale('en', 'US'),
|
||
offset: ViewportOffset.zero(),
|
||
textSelectionDelegate: delegate,
|
||
hasFocus: true,
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
);
|
||
|
||
expect(editable.hasFocus, true);
|
||
editable.hasFocus = false;
|
||
expect(editable.hasFocus, false);
|
||
});
|
||
|
||
test('has correct maxScrollExtent', () {
|
||
final TextSelectionDelegate delegate = FakeEditableTextState();
|
||
EditableText.debugDeterministicCursor = true;
|
||
|
||
final RenderEditable editable = RenderEditable(
|
||
maxLines: 2,
|
||
backgroundCursorColor: Colors.grey,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: const Color.fromARGB(0xFF, 0xFF, 0x00, 0x00),
|
||
offset: ViewportOffset.zero(),
|
||
textSelectionDelegate: delegate,
|
||
text: const TextSpan(
|
||
text: '撒地方加咖啡哈金凤凰卡号方式剪坏算法发挥福建垃\nasfjafjajfjaslfjaskjflasjfksajf撒分开建安路口附近拉设\n计费可使肌肤撒附近埃里克圾房卡设计费"',
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Roboto',
|
||
),
|
||
),
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
selection: const TextSelection.collapsed(
|
||
offset: 4,
|
||
affinity: TextAffinity.upstream,
|
||
),
|
||
);
|
||
|
||
editable.layout(BoxConstraints.loose(const Size(100.0, 1000.0)));
|
||
expect(editable.size, equals(const Size(100, 20)));
|
||
expect(editable.maxLines, equals(2));
|
||
expect(editable.maxScrollExtent, equals(90));
|
||
|
||
editable.layout(BoxConstraints.loose(const Size(150.0, 1000.0)));
|
||
expect(editable.maxScrollExtent, equals(50));
|
||
|
||
editable.layout(BoxConstraints.loose(const Size(200.0, 1000.0)));
|
||
expect(editable.maxScrollExtent, equals(40));
|
||
|
||
editable.layout(BoxConstraints.loose(const Size(500.0, 1000.0)));
|
||
expect(editable.maxScrollExtent, equals(10));
|
||
|
||
editable.layout(BoxConstraints.loose(const Size(1000.0, 1000.0)));
|
||
expect(editable.maxScrollExtent, equals(10));
|
||
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/42772
|
||
|
||
test('moveSelectionLeft/RightByLine stays on the current line', () async {
|
||
const String text = 'one two three\n\nfour five six';
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(
|
||
text: text,
|
||
selection: TextSelection.collapsed(offset: 0),
|
||
);
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
late TextSelection currentSelection;
|
||
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: Colors.red,
|
||
offset: viewportOffset,
|
||
textSelectionDelegate: delegate,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {
|
||
renderObject.selection = selection;
|
||
currentSelection = selection;
|
||
},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: const TextSpan(
|
||
text: text,
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
selection: const TextSelection.collapsed(
|
||
offset: 0,
|
||
),
|
||
);
|
||
|
||
layout(editable);
|
||
editable.hasFocus = true;
|
||
|
||
editable.selectPositionAt(from: Offset.zero, cause: SelectionChangedCause.tap);
|
||
editable.selection = const TextSelection.collapsed(offset: 0);
|
||
pumpFrame();
|
||
|
||
// Move to the end of the first line.
|
||
editable.moveSelectionRightByLine(SelectionChangedCause.keyboard);
|
||
expect(currentSelection.isCollapsed, true);
|
||
expect(currentSelection.baseOffset, 13);
|
||
// RenderEditable relies on its parent that passes onSelectionChanged to set
|
||
// the selection.
|
||
|
||
// Try moveSelectionRightByLine again and nothing happens because we're
|
||
// already at the end of a line.
|
||
editable.moveSelectionRightByLine(SelectionChangedCause.keyboard);
|
||
expect(currentSelection.isCollapsed, true);
|
||
expect(currentSelection.baseOffset, 13);
|
||
|
||
// Move back to the start of the line.
|
||
editable.moveSelectionLeftByLine(SelectionChangedCause.keyboard);
|
||
expect(currentSelection.isCollapsed, true);
|
||
expect(currentSelection.baseOffset, 0);
|
||
|
||
// Trying moveSelectionLeftByLine does nothing at the leftmost of the field.
|
||
editable.moveSelectionLeftByLine(SelectionChangedCause.keyboard);
|
||
expect(currentSelection.isCollapsed, true);
|
||
expect(currentSelection.baseOffset, 0);
|
||
|
||
// Move the selection to the empty line.
|
||
editable.moveSelectionRightByLine(SelectionChangedCause.keyboard);
|
||
expect(currentSelection.isCollapsed, true);
|
||
expect(currentSelection.baseOffset, 13);
|
||
editable.moveSelectionRight(SelectionChangedCause.keyboard);
|
||
expect(currentSelection.isCollapsed, true);
|
||
expect(currentSelection.baseOffset, 14);
|
||
|
||
// Neither moveSelectionLeftByLine nor moveSelectionRightByLine do anything
|
||
// here, because we're at both the beginning and end of the line.
|
||
editable.moveSelectionLeftByLine(SelectionChangedCause.keyboard);
|
||
expect(currentSelection.isCollapsed, true);
|
||
expect(currentSelection.baseOffset, 14);
|
||
editable.moveSelectionRightByLine(SelectionChangedCause.keyboard);
|
||
expect(currentSelection.isCollapsed, true);
|
||
expect(currentSelection.baseOffset, 14);
|
||
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021
|
||
|
||
test('arrow keys and delete handle simple text correctly', () async {
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(
|
||
text: 'test',
|
||
selection: TextSelection.collapsed(offset: 0),
|
||
);
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
late TextSelection currentSelection;
|
||
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: Colors.red,
|
||
offset: viewportOffset,
|
||
textSelectionDelegate: delegate,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {
|
||
renderObject.selection = selection;
|
||
currentSelection = selection;
|
||
},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: const TextSpan(
|
||
text: 'test',
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
selection: const TextSelection.collapsed(
|
||
offset: 0,
|
||
),
|
||
);
|
||
|
||
layout(editable);
|
||
editable.hasFocus = true;
|
||
|
||
editable.selectPositionAt(from: Offset.zero, cause: SelectionChangedCause.tap);
|
||
editable.selection = const TextSelection.collapsed(offset: 0);
|
||
pumpFrame();
|
||
|
||
editable.moveSelectionRight(SelectionChangedCause.keyboard);
|
||
expect(currentSelection.isCollapsed, true);
|
||
expect(currentSelection.baseOffset, 1);
|
||
|
||
editable.moveSelectionLeft(SelectionChangedCause.keyboard);
|
||
expect(currentSelection.isCollapsed, true);
|
||
expect(currentSelection.baseOffset, 0);
|
||
|
||
editable.deleteForward(SelectionChangedCause.keyboard);
|
||
expect(delegate.textEditingValue.text, 'est');
|
||
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021
|
||
|
||
test('arrow keys and delete handle surrogate pairs correctly', () async {
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(
|
||
text: '0123😆6789',
|
||
selection: TextSelection.collapsed(offset: 0),
|
||
);
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
late TextSelection currentSelection;
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: Colors.red,
|
||
offset: viewportOffset,
|
||
textSelectionDelegate: delegate,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {
|
||
renderObject.selection = selection;
|
||
currentSelection = selection;
|
||
},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: const TextSpan(
|
||
text: '0123😆6789',
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
selection: const TextSelection.collapsed(
|
||
offset: 0,
|
||
),
|
||
);
|
||
|
||
layout(editable);
|
||
editable.hasFocus = true;
|
||
|
||
editable.selection = const TextSelection.collapsed(offset: 4);
|
||
pumpFrame();
|
||
|
||
editable.moveSelectionRight(SelectionChangedCause.keyboard);
|
||
expect(currentSelection.isCollapsed, true);
|
||
expect(currentSelection.baseOffset, 6);
|
||
|
||
editable.moveSelectionLeft(SelectionChangedCause.keyboard);
|
||
expect(currentSelection.isCollapsed, true);
|
||
expect(currentSelection.baseOffset, 4);
|
||
|
||
editable.deleteForward(SelectionChangedCause.keyboard);
|
||
expect(delegate.textEditingValue.text, '01236789');
|
||
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021
|
||
|
||
test('arrow keys and delete handle grapheme clusters correctly', () async {
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(
|
||
text: '0123👨👩👦2345',
|
||
selection: TextSelection.collapsed(offset: 0),
|
||
);
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
late TextSelection currentSelection;
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: Colors.red,
|
||
offset: viewportOffset,
|
||
textSelectionDelegate: delegate,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {
|
||
renderObject.selection = selection;
|
||
currentSelection = selection;
|
||
},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: const TextSpan(
|
||
text: '0123👨👩👦2345',
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
selection: const TextSelection.collapsed(
|
||
offset: 0,
|
||
),
|
||
);
|
||
|
||
layout(editable);
|
||
editable.hasFocus = true;
|
||
|
||
editable.selection = const TextSelection.collapsed(offset: 4);
|
||
pumpFrame();
|
||
|
||
editable.moveSelectionRight(SelectionChangedCause.keyboard);
|
||
expect(currentSelection.isCollapsed, true);
|
||
expect(currentSelection.baseOffset, 12);
|
||
|
||
editable.moveSelectionLeft(SelectionChangedCause.keyboard);
|
||
expect(currentSelection.isCollapsed, true);
|
||
expect(currentSelection.baseOffset, 4);
|
||
|
||
editable.deleteForward(SelectionChangedCause.keyboard);
|
||
expect(delegate.textEditingValue.text, '01232345');
|
||
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021
|
||
|
||
test('arrow keys and delete handle surrogate pairs correctly case 2', () async {
|
||
const String text = '\u{1F44D}';
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(text: text);
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
late TextSelection currentSelection;
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: Colors.red,
|
||
offset: viewportOffset,
|
||
textSelectionDelegate: delegate,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {
|
||
renderObject.selection = selection;
|
||
currentSelection = selection;
|
||
},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: const TextSpan(
|
||
text: text, // Thumbs up
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
selection: const TextSelection.collapsed(
|
||
offset: 0,
|
||
),
|
||
);
|
||
|
||
layout(editable);
|
||
editable.hasFocus = true;
|
||
|
||
editable.selectPositionAt(from: Offset.zero, cause: SelectionChangedCause.tap);
|
||
editable.selection = const TextSelection.collapsed(offset: 0);
|
||
pumpFrame();
|
||
|
||
editable.moveSelectionRight(SelectionChangedCause.keyboard);
|
||
expect(currentSelection.isCollapsed, true);
|
||
expect(currentSelection.baseOffset, 2);
|
||
|
||
editable.moveSelectionLeft(SelectionChangedCause.keyboard);
|
||
expect(currentSelection.isCollapsed, true);
|
||
expect(currentSelection.baseOffset, 0);
|
||
|
||
editable.deleteForward(SelectionChangedCause.keyboard);
|
||
expect(delegate.textEditingValue.text, '');
|
||
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021
|
||
|
||
test('arrow keys work after detaching the widget and attaching it again', () async {
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(
|
||
text: 'W Szczebrzeszynie chrząszcz brzmi w trzcinie',
|
||
selection: TextSelection.collapsed(offset: 0),
|
||
);
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: Colors.red,
|
||
offset: viewportOffset,
|
||
textSelectionDelegate: delegate,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {
|
||
renderObject.selection = selection;
|
||
},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: const TextSpan(
|
||
text: 'W Szczebrzeszynie chrząszcz brzmi w trzcinie',
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
selection: const TextSelection.collapsed(
|
||
offset: 0,
|
||
),
|
||
);
|
||
|
||
final PipelineOwner pipelineOwner = PipelineOwner();
|
||
editable.attach(pipelineOwner);
|
||
editable.hasFocus = true;
|
||
editable.detach();
|
||
layout(editable);
|
||
editable.hasFocus = true;
|
||
editable.selectPositionAt(from: Offset.zero, cause: SelectionChangedCause.tap);
|
||
editable.selection = const TextSelection.collapsed(offset: 0);
|
||
pumpFrame();
|
||
|
||
editable.moveSelectionRight(SelectionChangedCause.keyboard);
|
||
editable.moveSelectionRight(SelectionChangedCause.keyboard);
|
||
editable.moveSelectionRight(SelectionChangedCause.keyboard);
|
||
editable.moveSelectionRight(SelectionChangedCause.keyboard);
|
||
expect(editable.selection?.isCollapsed, true);
|
||
expect(editable.selection?.baseOffset, 4);
|
||
|
||
editable.moveSelectionLeft(SelectionChangedCause.keyboard);
|
||
expect(editable.selection?.isCollapsed, true);
|
||
expect(editable.selection?.baseOffset, 3);
|
||
|
||
editable.deleteForward(SelectionChangedCause.keyboard);
|
||
expect(delegate.textEditingValue.text, 'W Sczebrzeszynie chrząszcz brzmi w trzcinie');
|
||
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021
|
||
|
||
test('RenderEditable registers and unregisters raw keyboard listener correctly', () async {
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(
|
||
text: 'how are you',
|
||
selection: TextSelection.collapsed(offset: 0),
|
||
);
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: Colors.red,
|
||
offset: viewportOffset,
|
||
hasFocus: true,
|
||
textSelectionDelegate: delegate,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {
|
||
renderObject.selection = selection;
|
||
},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: const TextSpan(
|
||
text: 'how are you',
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
selection: const TextSelection.collapsed(
|
||
offset: 0,
|
||
),
|
||
);
|
||
|
||
final PipelineOwner pipelineOwner = PipelineOwner();
|
||
editable.attach(pipelineOwner);
|
||
|
||
editable.deleteForward(SelectionChangedCause.keyboard);
|
||
expect(delegate.textEditingValue.text, 'ow are you');
|
||
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021
|
||
|
||
test('arrow keys with selection text', () async {
|
||
const String text = '012345';
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(text: text);
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
late TextSelection currentSelection;
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: Colors.red,
|
||
offset: viewportOffset,
|
||
textSelectionDelegate: delegate,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {
|
||
renderObject.selection = selection;
|
||
currentSelection = selection;
|
||
},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: const TextSpan(
|
||
text: text, // Thumbs up
|
||
style: TextStyle(height: 1.0, fontSize: 10.0, fontFamily: 'Ahem'),
|
||
),
|
||
selection: const TextSelection.collapsed(
|
||
offset: 0,
|
||
),
|
||
);
|
||
|
||
layout(editable);
|
||
editable.hasFocus = true;
|
||
|
||
editable.selection = const TextSelection(baseOffset: 2, extentOffset: 4);
|
||
pumpFrame();
|
||
|
||
editable.moveSelectionRight(SelectionChangedCause.keyboard);
|
||
expect(currentSelection.isCollapsed, true);
|
||
expect(currentSelection.baseOffset, 4);
|
||
|
||
editable.selection = const TextSelection(baseOffset: 4, extentOffset: 2);
|
||
pumpFrame();
|
||
|
||
editable.moveSelectionRight(SelectionChangedCause.keyboard);
|
||
expect(currentSelection.isCollapsed, true);
|
||
expect(currentSelection.baseOffset, 4);
|
||
|
||
editable.selection = const TextSelection(baseOffset: 2, extentOffset: 4);
|
||
pumpFrame();
|
||
|
||
editable.moveSelectionLeft(SelectionChangedCause.keyboard);
|
||
expect(currentSelection.isCollapsed, true);
|
||
expect(currentSelection.baseOffset, 2);
|
||
|
||
editable.selection = const TextSelection(baseOffset: 4, extentOffset: 2);
|
||
pumpFrame();
|
||
|
||
editable.moveSelectionLeft(SelectionChangedCause.keyboard);
|
||
expect(currentSelection.isCollapsed, true);
|
||
expect(currentSelection.baseOffset, 2);
|
||
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/58068
|
||
|
||
test('arrow keys with selection text and shift', () async {
|
||
const String text = '012345';
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(text: text);
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
late TextSelection currentSelection;
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: Colors.red,
|
||
offset: viewportOffset,
|
||
textSelectionDelegate: delegate,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {
|
||
renderObject.selection = selection;
|
||
currentSelection = selection;
|
||
},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: const TextSpan(
|
||
text: text, // Thumbs up
|
||
style: TextStyle(height: 1.0, fontSize: 10.0, fontFamily: 'Ahem'),
|
||
),
|
||
selection: const TextSelection.collapsed(
|
||
offset: 0,
|
||
),
|
||
);
|
||
|
||
layout(editable);
|
||
editable.hasFocus = true;
|
||
|
||
editable.selection = const TextSelection(baseOffset: 2, extentOffset: 4);
|
||
pumpFrame();
|
||
|
||
editable.extendSelectionRight(SelectionChangedCause.keyboard);
|
||
expect(currentSelection.isCollapsed, false);
|
||
expect(currentSelection.baseOffset, 2);
|
||
expect(currentSelection.extentOffset, 5);
|
||
|
||
editable.selection = const TextSelection(baseOffset: 4, extentOffset: 2);
|
||
pumpFrame();
|
||
|
||
editable.extendSelectionRight(SelectionChangedCause.keyboard);
|
||
expect(currentSelection.isCollapsed, false);
|
||
expect(currentSelection.baseOffset, 4);
|
||
expect(currentSelection.extentOffset, 3);
|
||
|
||
editable.selection = const TextSelection(baseOffset: 2, extentOffset: 4);
|
||
pumpFrame();
|
||
|
||
editable.extendSelectionLeft(SelectionChangedCause.keyboard);
|
||
expect(currentSelection.isCollapsed, false);
|
||
expect(currentSelection.baseOffset, 2);
|
||
expect(currentSelection.extentOffset, 3);
|
||
|
||
editable.selection = const TextSelection(baseOffset: 4, extentOffset: 2);
|
||
pumpFrame();
|
||
|
||
editable.extendSelectionLeft(SelectionChangedCause.keyboard);
|
||
expect(currentSelection.isCollapsed, false);
|
||
expect(currentSelection.baseOffset, 4);
|
||
expect(currentSelection.extentOffset, 1);
|
||
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/58068
|
||
|
||
test('respects enableInteractiveSelection', () async {
|
||
const String text = '012345';
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(text: text);
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
late TextSelection currentSelection;
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: Colors.red,
|
||
offset: viewportOffset,
|
||
textSelectionDelegate: delegate,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {
|
||
renderObject.selection = selection;
|
||
currentSelection = selection;
|
||
},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: const TextSpan(
|
||
text: text, // Thumbs up
|
||
style: TextStyle(height: 1.0, fontSize: 10.0, fontFamily: 'Ahem'),
|
||
),
|
||
selection: const TextSelection.collapsed(
|
||
offset: 0,
|
||
),
|
||
enableInteractiveSelection: false,
|
||
);
|
||
|
||
layout(editable);
|
||
editable.hasFocus = true;
|
||
|
||
editable.selection = const TextSelection.collapsed(offset: 2);
|
||
pumpFrame();
|
||
|
||
await simulateKeyDownEvent(LogicalKeyboardKey.shift);
|
||
|
||
editable.moveSelectionRight(SelectionChangedCause.keyboard);
|
||
expect(currentSelection.isCollapsed, true);
|
||
expect(currentSelection.baseOffset, 3);
|
||
|
||
editable.moveSelectionLeft(SelectionChangedCause.keyboard);
|
||
expect(currentSelection.isCollapsed, true);
|
||
expect(currentSelection.baseOffset, 2);
|
||
|
||
final LogicalKeyboardKey wordModifier =
|
||
Platform.isMacOS ? LogicalKeyboardKey.alt : LogicalKeyboardKey.control;
|
||
|
||
await simulateKeyDownEvent(wordModifier);
|
||
|
||
editable.moveSelectionRightByWord(SelectionChangedCause.keyboard);
|
||
expect(currentSelection.isCollapsed, true);
|
||
expect(currentSelection.baseOffset, 6);
|
||
|
||
editable.moveSelectionLeftByWord(SelectionChangedCause.keyboard);
|
||
expect(currentSelection.isCollapsed, true);
|
||
expect(currentSelection.baseOffset, 0);
|
||
|
||
await simulateKeyUpEvent(wordModifier);
|
||
await simulateKeyUpEvent(LogicalKeyboardKey.shift);
|
||
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/58068
|
||
|
||
group('delete', () {
|
||
test('when as a non-collapsed selection, it should delete a selection', () async {
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(
|
||
text: 'test',
|
||
selection: TextSelection(baseOffset: 1, extentOffset: 3),
|
||
);
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: Colors.red,
|
||
offset: viewportOffset,
|
||
textSelectionDelegate: delegate,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: const TextSpan(
|
||
text: 'test',
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
selection: const TextSelection(baseOffset: 1, extentOffset: 3),
|
||
);
|
||
|
||
layout(editable);
|
||
editable.hasFocus = true;
|
||
pumpFrame();
|
||
|
||
editable.delete(SelectionChangedCause.keyboard);
|
||
expect(delegate.textEditingValue.text, 'tt');
|
||
expect(delegate.textEditingValue.selection.isCollapsed, true);
|
||
expect(delegate.textEditingValue.selection.baseOffset, 1);
|
||
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021
|
||
|
||
test('when as simple text, it should delete the character to the left', () async {
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(
|
||
text: 'test',
|
||
selection: TextSelection.collapsed(offset: 3),
|
||
);
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: Colors.red,
|
||
offset: viewportOffset,
|
||
textSelectionDelegate: delegate,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: const TextSpan(
|
||
text: 'test',
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
selection: const TextSelection.collapsed(offset: 3),
|
||
);
|
||
|
||
layout(editable);
|
||
editable.hasFocus = true;
|
||
pumpFrame();
|
||
|
||
editable.delete(SelectionChangedCause.keyboard);
|
||
expect(delegate.textEditingValue.text, 'tet');
|
||
expect(delegate.textEditingValue.selection.isCollapsed, true);
|
||
expect(delegate.textEditingValue.selection.baseOffset, 2);
|
||
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021
|
||
|
||
test('when has surrogate pairs, it should delete the pair', () async {
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(
|
||
text: '\u{1F44D}',
|
||
selection: TextSelection.collapsed(offset: 2),
|
||
);
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: Colors.red,
|
||
offset: viewportOffset,
|
||
textSelectionDelegate: delegate,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: const TextSpan(
|
||
text: '\u{1F44D}', // Thumbs up
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
selection: const TextSelection.collapsed(offset: 2),
|
||
);
|
||
|
||
layout(editable);
|
||
editable.hasFocus = true;
|
||
pumpFrame();
|
||
|
||
editable.delete(SelectionChangedCause.keyboard);
|
||
expect(delegate.textEditingValue.text, '');
|
||
expect(delegate.textEditingValue.selection.isCollapsed, true);
|
||
expect(delegate.textEditingValue.selection.baseOffset, 0);
|
||
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021
|
||
|
||
test('when has grapheme clusters, it should delete the grapheme cluster', () async {
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(
|
||
text: '0123👨👩👦2345',
|
||
selection: TextSelection.collapsed(offset: 12),
|
||
);
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: Colors.red,
|
||
offset: viewportOffset,
|
||
textSelectionDelegate: delegate,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: const TextSpan(
|
||
text: '0123👨👩👦2345',
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
selection: const TextSelection.collapsed(offset: 12),
|
||
);
|
||
|
||
layout(editable);
|
||
editable.hasFocus = true;
|
||
pumpFrame();
|
||
|
||
editable.delete(SelectionChangedCause.keyboard);
|
||
expect(delegate.textEditingValue.text, '01232345');
|
||
expect(delegate.textEditingValue.selection.isCollapsed, true);
|
||
expect(delegate.textEditingValue.selection.baseOffset, 4);
|
||
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021
|
||
|
||
test('when is at the start of the text, it should be a no-op', () async {
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(
|
||
text: 'test',
|
||
selection: TextSelection.collapsed(offset: 0),
|
||
);
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: Colors.red,
|
||
offset: viewportOffset,
|
||
textSelectionDelegate: delegate,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: const TextSpan(
|
||
text: 'test',
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
selection: const TextSelection.collapsed(offset: 0),
|
||
);
|
||
|
||
layout(editable);
|
||
editable.hasFocus = true;
|
||
pumpFrame();
|
||
|
||
editable.delete(SelectionChangedCause.keyboard);
|
||
expect(delegate.textEditingValue.text, 'test');
|
||
expect(delegate.textEditingValue.selection.isCollapsed, true);
|
||
expect(delegate.textEditingValue.selection.baseOffset, 0);
|
||
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021
|
||
|
||
test('when input has obscured text, it should delete the character to the left', () async {
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(
|
||
text: 'test',
|
||
selection: TextSelection.collapsed(offset: 4),
|
||
);
|
||
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: Colors.red,
|
||
offset: viewportOffset,
|
||
textSelectionDelegate: delegate,
|
||
obscureText: true,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: const TextSpan(
|
||
text: '****',
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
selection: const TextSelection.collapsed(offset: 4),
|
||
);
|
||
|
||
layout(editable);
|
||
editable.hasFocus = true;
|
||
pumpFrame();
|
||
|
||
editable.delete(SelectionChangedCause.keyboard);
|
||
expect(delegate.textEditingValue.text, 'tes');
|
||
expect(delegate.textEditingValue.selection.isCollapsed, true);
|
||
expect(delegate.textEditingValue.selection.baseOffset, 3);
|
||
}, skip: isBrowser);
|
||
|
||
test('when using cjk characters', () async {
|
||
const String text = '用多個塊測試';
|
||
const int offset = 4;
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(
|
||
text: text,
|
||
selection: TextSelection.collapsed(offset: offset),
|
||
);
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: Colors.red,
|
||
offset: viewportOffset,
|
||
textSelectionDelegate: delegate,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: const TextSpan(
|
||
text: text,
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
selection: const TextSelection.collapsed(offset: offset),
|
||
);
|
||
|
||
layout(editable);
|
||
editable.hasFocus = true;
|
||
pumpFrame();
|
||
|
||
editable.delete(SelectionChangedCause.keyboard);
|
||
expect(delegate.textEditingValue.text, '用多個測試');
|
||
expect(delegate.textEditingValue.selection.isCollapsed, true);
|
||
expect(delegate.textEditingValue.selection.baseOffset, 3);
|
||
}, skip: isBrowser);
|
||
|
||
test('when using rtl', () async {
|
||
const String text = 'برنامج أهلا بالعالم';
|
||
const int offset = text.length;
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(
|
||
text: text,
|
||
selection: TextSelection.collapsed(offset: offset),
|
||
);
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.rtl,
|
||
cursorColor: Colors.red,
|
||
offset: viewportOffset,
|
||
textSelectionDelegate: delegate,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: const TextSpan(
|
||
text: text,
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
selection: const TextSelection.collapsed(offset: offset),
|
||
);
|
||
|
||
layout(editable);
|
||
editable.hasFocus = true;
|
||
pumpFrame();
|
||
|
||
editable.delete(SelectionChangedCause.keyboard);
|
||
expect(delegate.textEditingValue.text, 'برنامج أهلا بالعال');
|
||
expect(delegate.textEditingValue.selection.isCollapsed, true);
|
||
expect(delegate.textEditingValue.selection.baseOffset, text.length - 1);
|
||
}, skip: isBrowser);
|
||
});
|
||
|
||
group('deleteByWord', () {
|
||
test('when cursor is on the middle of a word, it should delete the left part of the word', () async {
|
||
const String text = 'test with multiple blocks';
|
||
const int offset = 8;
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(
|
||
text: text,
|
||
selection: TextSelection.collapsed(offset: offset),
|
||
);
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: Colors.red,
|
||
offset: viewportOffset,
|
||
textSelectionDelegate: delegate,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: const TextSpan(
|
||
text: text,
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
selection: const TextSelection.collapsed(offset: offset),
|
||
);
|
||
|
||
layout(editable);
|
||
editable.hasFocus = true;
|
||
pumpFrame();
|
||
|
||
editable.deleteByWord(SelectionChangedCause.keyboard, false);
|
||
expect(delegate.textEditingValue.text, 'test h multiple blocks');
|
||
expect(delegate.textEditingValue.selection.isCollapsed, true);
|
||
expect(delegate.textEditingValue.selection.baseOffset, 5);
|
||
}, skip: isBrowser);
|
||
|
||
test('when includeWhiteSpace is true, it should treat a whiteSpace as a single word', () async {
|
||
const String text = 'test with multiple blocks';
|
||
const int offset = 10;
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(
|
||
text: text,
|
||
selection: TextSelection.collapsed(offset: offset),
|
||
);
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: Colors.red,
|
||
offset: viewportOffset,
|
||
textSelectionDelegate: delegate,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: const TextSpan(
|
||
text: text,
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
selection: const TextSelection.collapsed(offset: offset),
|
||
);
|
||
|
||
layout(editable);
|
||
editable.hasFocus = true;
|
||
pumpFrame();
|
||
|
||
editable.deleteByWord(SelectionChangedCause.keyboard);
|
||
expect(delegate.textEditingValue.text, 'test withmultiple blocks');
|
||
expect(delegate.textEditingValue.selection.isCollapsed, true);
|
||
expect(delegate.textEditingValue.selection.baseOffset, 9);
|
||
}, skip: isBrowser);
|
||
|
||
test('when cursor is after a word, it should delete the whole word', () async {
|
||
const String text = 'test with multiple blocks';
|
||
const int offset = 9;
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(
|
||
text: text,
|
||
selection: TextSelection.collapsed(offset: offset),
|
||
);
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: Colors.red,
|
||
offset: viewportOffset,
|
||
textSelectionDelegate: delegate,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: const TextSpan(
|
||
text: text,
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
selection: const TextSelection.collapsed(offset: offset),
|
||
);
|
||
|
||
layout(editable);
|
||
editable.hasFocus = true;
|
||
pumpFrame();
|
||
|
||
editable.deleteByWord(SelectionChangedCause.keyboard, false);
|
||
expect(delegate.textEditingValue.text, 'test multiple blocks');
|
||
expect(delegate.textEditingValue.selection.isCollapsed, true);
|
||
expect(delegate.textEditingValue.selection.baseOffset, 5);
|
||
}, skip: isBrowser);
|
||
|
||
test('when cursor is preceeded by white spaces, it should delete the spaces and the next word to the left', () async {
|
||
const String text = 'test with multiple blocks';
|
||
const int offset = 12;
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(
|
||
text: text,
|
||
selection: TextSelection.collapsed(offset: offset),
|
||
);
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: Colors.red,
|
||
offset: viewportOffset,
|
||
textSelectionDelegate: delegate,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: const TextSpan(
|
||
text: text,
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
selection: const TextSelection.collapsed(offset: offset),
|
||
);
|
||
|
||
layout(editable);
|
||
editable.hasFocus = true;
|
||
pumpFrame();
|
||
|
||
editable.deleteByWord(SelectionChangedCause.keyboard, false);
|
||
expect(delegate.textEditingValue.text, 'test multiple blocks');
|
||
expect(delegate.textEditingValue.selection.isCollapsed, true);
|
||
expect(delegate.textEditingValue.selection.baseOffset, 5);
|
||
}, skip: isBrowser);
|
||
|
||
test('when cursor is preceeded by tabs spaces', () async {
|
||
const String text = 'test with\t\t\tmultiple blocks';
|
||
const int offset = 12;
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(
|
||
text: text,
|
||
selection: TextSelection.collapsed(offset: offset),
|
||
);
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: Colors.red,
|
||
offset: viewportOffset,
|
||
textSelectionDelegate: delegate,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: const TextSpan(
|
||
text: text,
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
selection: const TextSelection.collapsed(offset: offset),
|
||
);
|
||
|
||
layout(editable);
|
||
editable.hasFocus = true;
|
||
pumpFrame();
|
||
|
||
editable.deleteByWord(SelectionChangedCause.keyboard, false);
|
||
expect(delegate.textEditingValue.text, 'test multiple blocks');
|
||
expect(delegate.textEditingValue.selection.isCollapsed, true);
|
||
expect(delegate.textEditingValue.selection.baseOffset, 5);
|
||
}, skip: isBrowser);
|
||
|
||
test('when cursor is preceeded by break line, it should delete the breaking line and the word right before it', () async {
|
||
const String text = 'test with\nmultiple blocks';
|
||
const int offset = 10;
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(
|
||
text: text,
|
||
selection: TextSelection.collapsed(offset: offset),
|
||
);
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: Colors.red,
|
||
offset: viewportOffset,
|
||
textSelectionDelegate: delegate,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: const TextSpan(
|
||
text: text,
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
selection: const TextSelection.collapsed(offset: offset),
|
||
);
|
||
|
||
layout(editable);
|
||
editable.hasFocus = true;
|
||
pumpFrame();
|
||
|
||
editable.deleteByWord(SelectionChangedCause.keyboard, false);
|
||
expect(delegate.textEditingValue.text, 'test multiple blocks');
|
||
expect(delegate.textEditingValue.selection.isCollapsed, true);
|
||
expect(delegate.textEditingValue.selection.baseOffset, 5);
|
||
}, skip: isBrowser);
|
||
|
||
test('when using cjk characters', () async {
|
||
const String text = '用多個塊測試';
|
||
const int offset = 4;
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(
|
||
text: text,
|
||
selection: TextSelection.collapsed(offset: offset),
|
||
);
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: Colors.red,
|
||
offset: viewportOffset,
|
||
textSelectionDelegate: delegate,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: const TextSpan(
|
||
text: text,
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
selection: const TextSelection.collapsed(offset: offset),
|
||
);
|
||
|
||
layout(editable);
|
||
editable.hasFocus = true;
|
||
pumpFrame();
|
||
|
||
editable.deleteByWord(SelectionChangedCause.keyboard, false);
|
||
expect(delegate.textEditingValue.text, '用多個測試');
|
||
expect(delegate.textEditingValue.selection.isCollapsed, true);
|
||
expect(delegate.textEditingValue.selection.baseOffset, 3);
|
||
}, skip: isBrowser);
|
||
|
||
test('when using rtl', () async {
|
||
const String text = 'برنامج أهلا بالعالم';
|
||
const int offset = text.length;
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(
|
||
text: text,
|
||
selection: TextSelection.collapsed(offset: offset),
|
||
);
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.rtl,
|
||
cursorColor: Colors.red,
|
||
offset: viewportOffset,
|
||
textSelectionDelegate: delegate,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: const TextSpan(
|
||
text: text,
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
selection: const TextSelection.collapsed(offset: offset),
|
||
);
|
||
|
||
layout(editable);
|
||
editable.hasFocus = true;
|
||
pumpFrame();
|
||
|
||
editable.deleteByWord(SelectionChangedCause.keyboard, false);
|
||
expect(delegate.textEditingValue.text, 'برنامج أهلا ');
|
||
expect(delegate.textEditingValue.selection.isCollapsed, true);
|
||
expect(delegate.textEditingValue.selection.baseOffset, 12);
|
||
}, skip: isBrowser);
|
||
|
||
test('when input has obscured text, it should delete everything before the selection', () async {
|
||
const int offset = 21;
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(
|
||
text: 'test with multiple\n\n words',
|
||
selection: TextSelection.collapsed(offset: offset),
|
||
);
|
||
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: Colors.red,
|
||
offset: viewportOffset,
|
||
textSelectionDelegate: delegate,
|
||
obscureText: true,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: const TextSpan(
|
||
text: '****',
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
selection: const TextSelection.collapsed(offset: offset),
|
||
);
|
||
|
||
layout(editable);
|
||
editable.hasFocus = true;
|
||
pumpFrame();
|
||
|
||
editable.deleteByWord(SelectionChangedCause.keyboard, false);
|
||
expect(delegate.textEditingValue.text, 'words');
|
||
expect(delegate.textEditingValue.selection.isCollapsed, true);
|
||
expect(delegate.textEditingValue.selection.baseOffset, 0);
|
||
}, skip: isBrowser);
|
||
});
|
||
|
||
group('deleteByLine', () {
|
||
test('when cursor is on last character of a line, it should delete everything to the left', () async {
|
||
const String text = 'test with multiple blocks';
|
||
const int offset = text.length;
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(
|
||
text: text,
|
||
selection: TextSelection.collapsed(offset: offset),
|
||
);
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: Colors.red,
|
||
offset: viewportOffset,
|
||
textSelectionDelegate: delegate,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: const TextSpan(
|
||
text: text,
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
selection: const TextSelection.collapsed(offset: offset),
|
||
);
|
||
|
||
layout(editable);
|
||
editable.hasFocus = true;
|
||
pumpFrame();
|
||
|
||
editable.deleteByLine(SelectionChangedCause.keyboard);
|
||
expect(delegate.textEditingValue.text, '');
|
||
expect(delegate.textEditingValue.selection.isCollapsed, true);
|
||
expect(delegate.textEditingValue.selection.baseOffset, 0);
|
||
}, skip: isBrowser);
|
||
|
||
test('when cursor is on the middle of a word, it should delete delete everything to the left', () async {
|
||
const String text = 'test with multiple blocks';
|
||
const int offset = 8;
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(
|
||
text: text,
|
||
selection: TextSelection.collapsed(offset: offset),
|
||
);
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: Colors.red,
|
||
offset: viewportOffset,
|
||
textSelectionDelegate: delegate,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: const TextSpan(
|
||
text: text,
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
selection: const TextSelection.collapsed(offset: offset),
|
||
);
|
||
|
||
layout(editable);
|
||
editable.hasFocus = true;
|
||
pumpFrame();
|
||
|
||
editable.deleteByLine(SelectionChangedCause.keyboard);
|
||
expect(delegate.textEditingValue.text, 'h multiple blocks');
|
||
expect(delegate.textEditingValue.selection.isCollapsed, true);
|
||
expect(delegate.textEditingValue.selection.baseOffset, 0);
|
||
}, skip: isBrowser);
|
||
|
||
test('when previous character is a breakline, it should preserve it', () async {
|
||
const String text = 'test with\nmultiple blocks';
|
||
const int offset = 10;
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(
|
||
text: text,
|
||
selection: TextSelection.collapsed(offset: offset),
|
||
);
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: Colors.red,
|
||
offset: viewportOffset,
|
||
textSelectionDelegate: delegate,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: const TextSpan(
|
||
text: text,
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
selection: const TextSelection.collapsed(offset: offset),
|
||
);
|
||
|
||
layout(editable);
|
||
editable.hasFocus = true;
|
||
pumpFrame();
|
||
|
||
editable.deleteByLine(SelectionChangedCause.keyboard);
|
||
expect(delegate.textEditingValue.text, text);
|
||
expect(delegate.textEditingValue.selection.isCollapsed, true);
|
||
expect(delegate.textEditingValue.selection.baseOffset, offset);
|
||
}, skip: isBrowser);
|
||
|
||
test('when text is multiline, it should delete until the first line break it finds', () async {
|
||
const String text = 'test with\n\nMore stuff right here.\nmultiple blocks';
|
||
const int offset = 22;
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(
|
||
text: text,
|
||
selection: TextSelection.collapsed(offset: offset),
|
||
);
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: Colors.red,
|
||
offset: viewportOffset,
|
||
textSelectionDelegate: delegate,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: const TextSpan(
|
||
text: text,
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
selection: const TextSelection.collapsed(offset: offset),
|
||
);
|
||
|
||
layout(editable);
|
||
editable.hasFocus = true;
|
||
pumpFrame();
|
||
|
||
editable.deleteByLine(SelectionChangedCause.keyboard);
|
||
expect(delegate.textEditingValue.text, 'test with\n\nright here.\nmultiple blocks');
|
||
expect(delegate.textEditingValue.selection.isCollapsed, true);
|
||
expect(delegate.textEditingValue.selection.baseOffset, 11);
|
||
}, skip: isBrowser);
|
||
|
||
test('when input has obscured text, it should delete everything before the selection', () async {
|
||
const int offset = 21;
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(
|
||
text: 'test with multiple\n\n words',
|
||
selection: TextSelection.collapsed(offset: offset),
|
||
);
|
||
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: Colors.red,
|
||
offset: viewportOffset,
|
||
textSelectionDelegate: delegate,
|
||
obscureText: true,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: const TextSpan(
|
||
text: '****',
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
selection: const TextSelection.collapsed(offset: offset),
|
||
);
|
||
|
||
layout(editable);
|
||
editable.hasFocus = true;
|
||
pumpFrame();
|
||
|
||
editable.deleteByLine(SelectionChangedCause.keyboard);
|
||
expect(delegate.textEditingValue.text, 'words');
|
||
expect(delegate.textEditingValue.selection.isCollapsed, true);
|
||
expect(delegate.textEditingValue.selection.baseOffset, 0);
|
||
}, skip: isBrowser);
|
||
});
|
||
|
||
group('deleteForward', () {
|
||
test('when as a non-collapsed selection, it should delete a selection', () async {
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(
|
||
text: 'test',
|
||
selection: TextSelection(baseOffset: 1, extentOffset: 3),
|
||
);
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: Colors.red,
|
||
offset: viewportOffset,
|
||
textSelectionDelegate: delegate,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: const TextSpan(
|
||
text: 'test',
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
selection: const TextSelection(baseOffset: 1, extentOffset: 3),
|
||
);
|
||
|
||
layout(editable);
|
||
editable.hasFocus = true;
|
||
pumpFrame();
|
||
|
||
editable.deleteForward(SelectionChangedCause.keyboard);
|
||
expect(delegate.textEditingValue.text, 'tt');
|
||
expect(delegate.textEditingValue.selection.isCollapsed, true);
|
||
expect(delegate.textEditingValue.selection.baseOffset, 1);
|
||
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021
|
||
|
||
test('when includeWhiteSpace is true, it should treat a whiteSpace as a single word', () async {
|
||
const String text = 'test with multiple blocks';
|
||
const int offset = 9;
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(
|
||
text: text,
|
||
selection: TextSelection.collapsed(offset: offset),
|
||
);
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: Colors.red,
|
||
offset: viewportOffset,
|
||
textSelectionDelegate: delegate,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: const TextSpan(
|
||
text: text,
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
selection: const TextSelection.collapsed(offset: offset),
|
||
);
|
||
|
||
layout(editable);
|
||
editable.hasFocus = true;
|
||
pumpFrame();
|
||
|
||
editable.deleteForwardByWord(SelectionChangedCause.keyboard);
|
||
expect(delegate.textEditingValue.text, 'test withmultiple blocks');
|
||
expect(delegate.textEditingValue.selection.isCollapsed, true);
|
||
expect(delegate.textEditingValue.selection.baseOffset, 9);
|
||
}, skip: isBrowser);
|
||
|
||
test('when at the end of a text, it should be a no-op', () async {
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(
|
||
text: 'test',
|
||
selection: TextSelection.collapsed(offset: 4),
|
||
);
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: Colors.red,
|
||
offset: viewportOffset,
|
||
textSelectionDelegate: delegate,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: const TextSpan(
|
||
text: 'test',
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
selection: const TextSelection.collapsed(offset: 4),
|
||
);
|
||
|
||
layout(editable);
|
||
editable.hasFocus = true;
|
||
pumpFrame();
|
||
|
||
editable.deleteForward(SelectionChangedCause.keyboard);
|
||
expect(delegate.textEditingValue.text, 'test');
|
||
expect(delegate.textEditingValue.selection.isCollapsed, true);
|
||
expect(delegate.textEditingValue.selection.baseOffset, 4);
|
||
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021
|
||
|
||
test('when the input has obscured text, it should delete the forward character', () async {
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(
|
||
text: 'test',
|
||
selection: TextSelection.collapsed(offset: 0),
|
||
);
|
||
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: Colors.red,
|
||
offset: viewportOffset,
|
||
textSelectionDelegate: delegate,
|
||
obscureText: true,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: const TextSpan(
|
||
text: '****',
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
selection: const TextSelection.collapsed(offset: 0),
|
||
);
|
||
|
||
layout(editable);
|
||
editable.hasFocus = true;
|
||
pumpFrame();
|
||
|
||
editable.deleteForward(SelectionChangedCause.keyboard);
|
||
expect(delegate.textEditingValue.text, 'est');
|
||
expect(delegate.textEditingValue.selection.isCollapsed, true);
|
||
expect(delegate.textEditingValue.selection.baseOffset, 0);
|
||
}, skip: isBrowser);
|
||
|
||
test('when using cjk characters', () async {
|
||
const String text = '用多個塊測試';
|
||
const int offset = 0;
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(
|
||
text: text,
|
||
selection: TextSelection.collapsed(offset: offset),
|
||
);
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: Colors.red,
|
||
offset: viewportOffset,
|
||
textSelectionDelegate: delegate,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: const TextSpan(
|
||
text: text,
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
selection: const TextSelection.collapsed(offset: offset),
|
||
);
|
||
|
||
layout(editable);
|
||
editable.hasFocus = true;
|
||
pumpFrame();
|
||
|
||
editable.deleteForward(SelectionChangedCause.keyboard);
|
||
expect(delegate.textEditingValue.text, '多個塊測試');
|
||
expect(delegate.textEditingValue.selection.isCollapsed, true);
|
||
expect(delegate.textEditingValue.selection.baseOffset, 0);
|
||
}, skip: isBrowser);
|
||
|
||
test('when using rtl', () async {
|
||
const String text = 'برنامج أهلا بالعالم';
|
||
const int offset = 0;
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(
|
||
text: text,
|
||
selection: TextSelection.collapsed(offset: offset),
|
||
);
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.rtl,
|
||
cursorColor: Colors.red,
|
||
offset: viewportOffset,
|
||
textSelectionDelegate: delegate,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: const TextSpan(
|
||
text: text,
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
selection: const TextSelection.collapsed(offset: offset),
|
||
);
|
||
|
||
layout(editable);
|
||
editable.hasFocus = true;
|
||
pumpFrame();
|
||
|
||
editable.deleteForward(SelectionChangedCause.keyboard);
|
||
expect(delegate.textEditingValue.text, 'رنامج أهلا بالعالم');
|
||
expect(delegate.textEditingValue.selection.isCollapsed, true);
|
||
expect(delegate.textEditingValue.selection.baseOffset, 0);
|
||
}, skip: isBrowser);
|
||
|
||
});
|
||
|
||
group('deleteForwardByWord', () {
|
||
test('when cursor is on the middle of a word, it should delete the next part of the word', () async {
|
||
const String text = 'test with multiple blocks';
|
||
const int offset = 6;
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(
|
||
text: text,
|
||
selection: TextSelection.collapsed(offset: offset),
|
||
);
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: Colors.red,
|
||
offset: viewportOffset,
|
||
textSelectionDelegate: delegate,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: const TextSpan(
|
||
text: text,
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
selection: const TextSelection.collapsed(offset: offset),
|
||
);
|
||
|
||
layout(editable);
|
||
editable.hasFocus = true;
|
||
pumpFrame();
|
||
|
||
editable.deleteForwardByWord(SelectionChangedCause.keyboard, false);
|
||
expect(delegate.textEditingValue.text, 'test w multiple blocks');
|
||
expect(delegate.textEditingValue.selection.isCollapsed, true);
|
||
expect(delegate.textEditingValue.selection.baseOffset, offset);
|
||
}, skip: isBrowser);
|
||
|
||
test('when cursor is before a word, it should delete the whole word', () async {
|
||
const String text = 'test with multiple blocks';
|
||
const int offset = 10;
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(
|
||
text: text,
|
||
selection: TextSelection.collapsed(offset: offset),
|
||
);
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: Colors.red,
|
||
offset: viewportOffset,
|
||
textSelectionDelegate: delegate,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: const TextSpan(
|
||
text: text,
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
selection: const TextSelection.collapsed(offset: offset),
|
||
);
|
||
|
||
layout(editable);
|
||
editable.hasFocus = true;
|
||
pumpFrame();
|
||
|
||
editable.deleteForwardByWord(SelectionChangedCause.keyboard, false);
|
||
expect(delegate.textEditingValue.text, 'test with blocks');
|
||
expect(delegate.textEditingValue.selection.isCollapsed, true);
|
||
expect(delegate.textEditingValue.selection.baseOffset, offset);
|
||
}, skip: isBrowser);
|
||
|
||
test('when cursor is preceeded by white spaces, it should delete the spaces and the next word', () async {
|
||
const String text = 'test with multiple blocks';
|
||
const int offset = 9;
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(
|
||
text: text,
|
||
selection: TextSelection.collapsed(offset: offset),
|
||
);
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: Colors.red,
|
||
offset: viewportOffset,
|
||
textSelectionDelegate: delegate,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: const TextSpan(
|
||
text: text,
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
selection: const TextSelection.collapsed(offset: offset),
|
||
);
|
||
|
||
layout(editable);
|
||
editable.hasFocus = true;
|
||
pumpFrame();
|
||
|
||
editable.deleteForwardByWord(SelectionChangedCause.keyboard, false);
|
||
expect(delegate.textEditingValue.text, 'test with blocks');
|
||
expect(delegate.textEditingValue.selection.isCollapsed, true);
|
||
expect(delegate.textEditingValue.selection.baseOffset, offset);
|
||
}, skip: isBrowser);
|
||
|
||
test('when cursor is before tabs, it should delete the tabs and the next word', () async {
|
||
const String text = 'test with\t\t\tmultiple blocks';
|
||
const int offset = 9;
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(
|
||
text: text,
|
||
selection: TextSelection.collapsed(offset: offset),
|
||
);
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: Colors.red,
|
||
offset: viewportOffset,
|
||
textSelectionDelegate: delegate,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: const TextSpan(
|
||
text: text,
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
selection: const TextSelection.collapsed(offset: offset),
|
||
);
|
||
|
||
layout(editable);
|
||
editable.hasFocus = true;
|
||
pumpFrame();
|
||
|
||
editable.deleteForwardByWord(SelectionChangedCause.keyboard, false);
|
||
expect(delegate.textEditingValue.text, 'test with blocks');
|
||
expect(delegate.textEditingValue.selection.isCollapsed, true);
|
||
expect(delegate.textEditingValue.selection.baseOffset, offset);
|
||
}, skip: isBrowser);
|
||
|
||
test('when cursor is followed by break line, it should delete the next word', () async {
|
||
const String text = 'test with\n\n\nmultiple blocks';
|
||
const int offset = 9;
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(
|
||
text: text,
|
||
selection: TextSelection.collapsed(offset: offset),
|
||
);
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: Colors.red,
|
||
offset: viewportOffset,
|
||
textSelectionDelegate: delegate,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: const TextSpan(
|
||
text: text,
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
selection: const TextSelection.collapsed(offset: offset),
|
||
);
|
||
|
||
layout(editable);
|
||
editable.hasFocus = true;
|
||
pumpFrame();
|
||
|
||
editable.deleteForwardByWord(SelectionChangedCause.keyboard, false);
|
||
expect(delegate.textEditingValue.text, 'test with blocks');
|
||
expect(delegate.textEditingValue.selection.isCollapsed, true);
|
||
expect(delegate.textEditingValue.selection.baseOffset, offset);
|
||
}, skip: isBrowser);
|
||
|
||
test('when using cjk characters', () async {
|
||
const String text = '用多個塊測試';
|
||
const int offset = 0;
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(
|
||
text: text,
|
||
selection: TextSelection.collapsed(offset: offset),
|
||
);
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: Colors.red,
|
||
offset: viewportOffset,
|
||
textSelectionDelegate: delegate,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: const TextSpan(
|
||
text: text,
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
selection: const TextSelection.collapsed(offset: offset),
|
||
);
|
||
|
||
layout(editable);
|
||
editable.hasFocus = true;
|
||
pumpFrame();
|
||
|
||
editable.deleteForwardByWord(SelectionChangedCause.keyboard, false);
|
||
expect(delegate.textEditingValue.text, '多個塊測試');
|
||
expect(delegate.textEditingValue.selection.isCollapsed, true);
|
||
expect(delegate.textEditingValue.selection.baseOffset, offset);
|
||
}, skip: isBrowser);
|
||
|
||
test('when using rtl', () async {
|
||
const String text = 'برنامج أهلا بالعالم';
|
||
const int offset = 0;
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(
|
||
text: text,
|
||
selection: TextSelection.collapsed(offset: offset),
|
||
);
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.rtl,
|
||
cursorColor: Colors.red,
|
||
offset: viewportOffset,
|
||
textSelectionDelegate: delegate,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: const TextSpan(
|
||
text: text,
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
selection: const TextSelection.collapsed(offset: offset),
|
||
);
|
||
|
||
layout(editable);
|
||
editable.hasFocus = true;
|
||
pumpFrame();
|
||
|
||
editable.deleteForwardByWord(SelectionChangedCause.keyboard, false);
|
||
expect(delegate.textEditingValue.text, ' أهلا بالعالم');
|
||
expect(delegate.textEditingValue.selection.isCollapsed, true);
|
||
expect(delegate.textEditingValue.selection.baseOffset, offset);
|
||
}, skip: isBrowser);
|
||
|
||
test('when input has obscured text, it should delete everything after the selection', () async {
|
||
const int offset = 4;
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(
|
||
text: 'test with multiple\n\n words',
|
||
selection: TextSelection.collapsed(offset: offset),
|
||
);
|
||
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: Colors.red,
|
||
offset: viewportOffset,
|
||
textSelectionDelegate: delegate,
|
||
obscureText: true,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: const TextSpan(
|
||
text: '****',
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
selection: const TextSelection.collapsed(offset: offset),
|
||
);
|
||
|
||
layout(editable);
|
||
editable.hasFocus = true;
|
||
pumpFrame();
|
||
|
||
editable.deleteForwardByWord(SelectionChangedCause.keyboard, false);
|
||
expect(delegate.textEditingValue.text, 'test');
|
||
expect(delegate.textEditingValue.selection.isCollapsed, true);
|
||
expect(delegate.textEditingValue.selection.baseOffset, offset);
|
||
}, skip: isBrowser);
|
||
});
|
||
|
||
group('deleteForwardByLine', () {
|
||
test('when cursor is on first character of a line, it should delete everything that follows', () async {
|
||
const String text = 'test with multiple blocks';
|
||
const int offset = 4;
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(
|
||
text: text,
|
||
selection: TextSelection.collapsed(offset: offset),
|
||
);
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: Colors.red,
|
||
offset: viewportOffset,
|
||
textSelectionDelegate: delegate,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: const TextSpan(
|
||
text: text,
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
selection: const TextSelection.collapsed(offset: offset),
|
||
);
|
||
|
||
layout(editable);
|
||
editable.hasFocus = true;
|
||
pumpFrame();
|
||
|
||
editable.deleteForwardByLine(SelectionChangedCause.keyboard);
|
||
expect(delegate.textEditingValue.text, 'test');
|
||
expect(delegate.textEditingValue.selection.isCollapsed, true);
|
||
expect(delegate.textEditingValue.selection.baseOffset, offset);
|
||
}, skip: isBrowser);
|
||
|
||
test('when cursor is on the middle of a word, it should delete delete everything that follows', () async {
|
||
const String text = 'test with multiple blocks';
|
||
const int offset = 8;
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(
|
||
text: text,
|
||
selection: TextSelection.collapsed(offset: offset),
|
||
);
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: Colors.red,
|
||
offset: viewportOffset,
|
||
textSelectionDelegate: delegate,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: const TextSpan(
|
||
text: text,
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
selection: const TextSelection.collapsed(offset: offset),
|
||
);
|
||
|
||
layout(editable);
|
||
editable.hasFocus = true;
|
||
pumpFrame();
|
||
|
||
editable.deleteForwardByLine(SelectionChangedCause.keyboard);
|
||
expect(delegate.textEditingValue.text, 'test wit');
|
||
expect(delegate.textEditingValue.selection.isCollapsed, true);
|
||
expect(delegate.textEditingValue.selection.baseOffset, offset);
|
||
}, skip: isBrowser);
|
||
|
||
test('when next character is a breakline, it should preserve it', () async {
|
||
const String text = 'test with\n\n\nmultiple blocks';
|
||
const int offset = 9;
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(
|
||
text: text,
|
||
selection: TextSelection.collapsed(offset: offset),
|
||
);
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: Colors.red,
|
||
offset: viewportOffset,
|
||
textSelectionDelegate: delegate,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: const TextSpan(
|
||
text: text,
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
selection: const TextSelection.collapsed(offset: offset),
|
||
);
|
||
|
||
layout(editable);
|
||
editable.hasFocus = true;
|
||
pumpFrame();
|
||
|
||
editable.deleteForwardByLine(SelectionChangedCause.keyboard);
|
||
expect(delegate.textEditingValue.text, text);
|
||
expect(delegate.textEditingValue.selection.isCollapsed, true);
|
||
expect(delegate.textEditingValue.selection.baseOffset, offset);
|
||
}, skip: isBrowser);
|
||
|
||
test('when text is multiline, it should delete until the first line break it finds', () async {
|
||
const String text = 'test with\n\nMore stuff right here.\nmultiple blocks';
|
||
const int offset = 2;
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(
|
||
text: text,
|
||
selection: TextSelection.collapsed(offset: offset),
|
||
);
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: Colors.red,
|
||
offset: viewportOffset,
|
||
textSelectionDelegate: delegate,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: const TextSpan(
|
||
text: text,
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
selection: const TextSelection.collapsed(offset: offset),
|
||
);
|
||
|
||
layout(editable);
|
||
editable.hasFocus = true;
|
||
pumpFrame();
|
||
|
||
editable.deleteForwardByLine(SelectionChangedCause.keyboard);
|
||
expect(delegate.textEditingValue.text, 'te\n\nMore stuff right here.\nmultiple blocks');
|
||
expect(delegate.textEditingValue.selection.isCollapsed, true);
|
||
expect(delegate.textEditingValue.selection.baseOffset, offset);
|
||
}, skip: isBrowser);
|
||
|
||
test('when input has obscured text, it should delete everything after the selection', () async {
|
||
const int offset = 4;
|
||
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||
..textEditingValue = const TextEditingValue(
|
||
text: 'test with multiple\n\n words',
|
||
selection: TextSelection.collapsed(offset: offset),
|
||
);
|
||
|
||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||
final RenderEditable editable = RenderEditable(
|
||
backgroundCursorColor: Colors.grey,
|
||
selectionColor: Colors.black,
|
||
textDirection: TextDirection.ltr,
|
||
cursorColor: Colors.red,
|
||
offset: viewportOffset,
|
||
textSelectionDelegate: delegate,
|
||
obscureText: true,
|
||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
text: const TextSpan(
|
||
text: '****',
|
||
style: TextStyle(
|
||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
selection: const TextSelection.collapsed(offset: offset),
|
||
);
|
||
|
||
layout(editable);
|
||
editable.hasFocus = true;
|
||
pumpFrame();
|
||
|
||
editable.deleteForwardByLine(SelectionChangedCause.keyboard);
|
||
expect(delegate.textEditingValue.text, 'test');
|
||
expect(delegate.textEditingValue.selection.isCollapsed, true);
|
||
expect(delegate.textEditingValue.selection.baseOffset, offset);
|
||
}, skip: isBrowser);
|
||
});
|
||
|
||
test('getEndpointsForSelection handles empty characters', () {
|
||
final TextSelectionDelegate delegate = FakeEditableTextState();
|
||
final RenderEditable editable = RenderEditable(
|
||
// This is a Unicode left-to-right mark character that will not render
|
||
// any glyphs.
|
||
text: const TextSpan(text: '\u200e'),
|
||
textAlign: TextAlign.start,
|
||
textDirection: TextDirection.ltr,
|
||
offset: ViewportOffset.zero(),
|
||
textSelectionDelegate: delegate,
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
);
|
||
editable.layout(BoxConstraints.loose(const Size(100, 100)));
|
||
final List<TextSelectionPoint> endpoints = editable.getEndpointsForSelection(
|
||
const TextSelection(baseOffset: 0, extentOffset: 1),
|
||
);
|
||
expect(endpoints[0].point.dx, 0);
|
||
});
|
||
|
||
group('nextCharacter', () {
|
||
test('handles normal strings correctly', () {
|
||
expect(RenderEditable.nextCharacter(0, '01234567'), 1);
|
||
expect(RenderEditable.nextCharacter(3, '01234567'), 4);
|
||
expect(RenderEditable.nextCharacter(7, '01234567'), 8);
|
||
expect(RenderEditable.nextCharacter(8, '01234567'), 8);
|
||
});
|
||
|
||
test('throws for invalid indices', () {
|
||
expect(() => RenderEditable.nextCharacter(-1, '01234567'), throwsAssertionError);
|
||
expect(() => RenderEditable.nextCharacter(9, '01234567'), throwsAssertionError);
|
||
});
|
||
|
||
test('skips spaces in normal strings when includeWhitespace is false', () {
|
||
expect(RenderEditable.nextCharacter(3, '0123 5678', false), 5);
|
||
expect(RenderEditable.nextCharacter(4, '0123 5678', false), 5);
|
||
expect(RenderEditable.nextCharacter(3, '0123 0123', false), 10);
|
||
expect(RenderEditable.nextCharacter(2, '0123 0123', false), 3);
|
||
expect(RenderEditable.nextCharacter(4, '0123 0123', false), 10);
|
||
expect(RenderEditable.nextCharacter(9, '0123 0123', false), 10);
|
||
expect(RenderEditable.nextCharacter(10, '0123 0123', false), 11);
|
||
// If the subsequent characters are all whitespace, it returns the length
|
||
// of the string.
|
||
expect(RenderEditable.nextCharacter(5, '0123 ', false), 10);
|
||
});
|
||
|
||
test('handles surrogate pairs correctly', () {
|
||
expect(RenderEditable.nextCharacter(3, '0123👨👩👦0123'), 4);
|
||
expect(RenderEditable.nextCharacter(4, '0123👨👩👦0123'), 6);
|
||
expect(RenderEditable.nextCharacter(5, '0123👨👩👦0123'), 6);
|
||
expect(RenderEditable.nextCharacter(6, '0123👨👩👦0123'), 8);
|
||
expect(RenderEditable.nextCharacter(7, '0123👨👩👦0123'), 8);
|
||
expect(RenderEditable.nextCharacter(8, '0123👨👩👦0123'), 10);
|
||
expect(RenderEditable.nextCharacter(9, '0123👨👩👦0123'), 10);
|
||
expect(RenderEditable.nextCharacter(10, '0123👨👩👦0123'), 11);
|
||
});
|
||
|
||
test('handles extended grapheme clusters correctly', () {
|
||
expect(RenderEditable.nextCharacter(3, '0123👨👩👦2345'), 4);
|
||
expect(RenderEditable.nextCharacter(4, '0123👨👩👦2345'), 12);
|
||
// Even when extent falls within an extended grapheme cluster, it still
|
||
// identifies the whole grapheme cluster.
|
||
expect(RenderEditable.nextCharacter(5, '0123👨👩👦2345'), 12);
|
||
expect(RenderEditable.nextCharacter(12, '0123👨👩👦2345'), 13);
|
||
});
|
||
});
|
||
|
||
group('getRectForComposingRange', () {
|
||
const TextSpan emptyTextSpan = TextSpan(text: '\u200e');
|
||
final TextSelectionDelegate delegate = FakeEditableTextState();
|
||
final RenderEditable editable = RenderEditable(
|
||
maxLines: null,
|
||
textAlign: TextAlign.start,
|
||
textDirection: TextDirection.ltr,
|
||
offset: ViewportOffset.zero(),
|
||
textSelectionDelegate: delegate,
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
);
|
||
|
||
test('returns null when no composing range', () {
|
||
editable.text = const TextSpan(text: '123');
|
||
editable.layout(const BoxConstraints.tightFor(width: 200));
|
||
|
||
// Invalid range.
|
||
expect(editable.getRectForComposingRange(const TextRange(start: -1, end: 2)), isNull);
|
||
// Collapsed range.
|
||
expect(editable.getRectForComposingRange(const TextRange.collapsed(2)), isNull);
|
||
|
||
// Empty Editable.
|
||
editable.text = emptyTextSpan;
|
||
editable.layout(const BoxConstraints.tightFor(width: 200));
|
||
|
||
expect(
|
||
editable.getRectForComposingRange(const TextRange(start: 0, end: 1)),
|
||
// On web this evaluates to a zero-width Rect.
|
||
anyOf(isNull, (Rect rect) => rect.width == 0),
|
||
);
|
||
});
|
||
|
||
test('more than 1 run on the same line', () {
|
||
const TextStyle tinyText = TextStyle(fontSize: 1, fontFamily: 'Ahem');
|
||
const TextStyle normalText = TextStyle(fontSize: 10, fontFamily: 'Ahem');
|
||
editable.text = TextSpan(
|
||
children: <TextSpan>[
|
||
const TextSpan(text: 'A', style: tinyText),
|
||
TextSpan(text: 'A' * 20, style: normalText),
|
||
const TextSpan(text: 'A', style: tinyText),
|
||
],
|
||
);
|
||
// Give it a width that forces the editable to wrap.
|
||
editable.layout(const BoxConstraints.tightFor(width: 200));
|
||
|
||
final Rect composingRect = editable.getRectForComposingRange(const TextRange(start: 0, end: 20 + 2))!;
|
||
|
||
// Since the range covers an entire line, the Rect should also be almost
|
||
// as wide as the entire paragraph (give or take 1 character).
|
||
expect(composingRect.width, greaterThan(200 - 10));
|
||
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/66089
|
||
});
|
||
|
||
group('previousCharacter', () {
|
||
test('handles normal strings correctly', () {
|
||
expect(RenderEditable.previousCharacter(8, '01234567'), 7);
|
||
expect(RenderEditable.previousCharacter(0, '01234567'), 0);
|
||
expect(RenderEditable.previousCharacter(1, '01234567'), 0);
|
||
expect(RenderEditable.previousCharacter(5, '01234567'), 4);
|
||
expect(RenderEditable.previousCharacter(8, '01234567'), 7);
|
||
});
|
||
|
||
test('throws for invalid indices', () {
|
||
expect(() => RenderEditable.previousCharacter(-1, '01234567'), throwsAssertionError);
|
||
expect(() => RenderEditable.previousCharacter(9, '01234567'), throwsAssertionError);
|
||
});
|
||
|
||
test('skips spaces in normal strings when includeWhitespace is false', () {
|
||
expect(RenderEditable.previousCharacter(10, '0123 0123', false), 3);
|
||
expect(RenderEditable.previousCharacter(11, '0123 0123', false), 10);
|
||
expect(RenderEditable.previousCharacter(9, '0123 0123', false), 3);
|
||
expect(RenderEditable.previousCharacter(4, '0123 0123', false), 3);
|
||
expect(RenderEditable.previousCharacter(3, '0123 0123', false), 2);
|
||
// If the previous characters are all whitespace, it returns zero.
|
||
expect(RenderEditable.previousCharacter(3, ' 0123', false), 0);
|
||
});
|
||
|
||
test('handles surrogate pairs correctly', () {
|
||
expect(RenderEditable.previousCharacter(11, '0123👨👩👦0123'), 10);
|
||
expect(RenderEditable.previousCharacter(10, '0123👨👩👦0123'), 8);
|
||
expect(RenderEditable.previousCharacter(9, '0123👨👩👦0123'), 8);
|
||
expect(RenderEditable.previousCharacter(8, '0123👨👩👦0123'), 6);
|
||
expect(RenderEditable.previousCharacter(7, '0123👨👩👦0123'), 6);
|
||
expect(RenderEditable.previousCharacter(6, '0123👨👩👦0123'), 4);
|
||
expect(RenderEditable.previousCharacter(5, '0123👨👩👦0123'), 4);
|
||
expect(RenderEditable.previousCharacter(4, '0123👨👩👦0123'), 3);
|
||
expect(RenderEditable.previousCharacter(3, '0123👨👩👦0123'), 2);
|
||
});
|
||
|
||
test('handles extended grapheme clusters correctly', () {
|
||
expect(RenderEditable.previousCharacter(13, '0123👨👩👦2345'), 12);
|
||
// Even when extent falls within an extended grapheme cluster, it still
|
||
// identifies the whole grapheme cluster.
|
||
expect(RenderEditable.previousCharacter(12, '0123👨👩👦2345'), 4);
|
||
expect(RenderEditable.previousCharacter(11, '0123👨👩👦2345'), 4);
|
||
expect(RenderEditable.previousCharacter(5, '0123👨👩👦2345'), 4);
|
||
expect(RenderEditable.previousCharacter(4, '0123👨👩👦2345'), 3);
|
||
});
|
||
});
|
||
|
||
group('custom painters', () {
|
||
final TextSelectionDelegate delegate = FakeEditableTextState();
|
||
|
||
final _TestRenderEditable editable = _TestRenderEditable(
|
||
textDirection: TextDirection.ltr,
|
||
offset: ViewportOffset.zero(),
|
||
textSelectionDelegate: delegate,
|
||
text: const TextSpan(
|
||
text: 'test',
|
||
style: TextStyle(
|
||
height: 1.0,
|
||
fontSize: 10.0,
|
||
fontFamily: 'Ahem',
|
||
),
|
||
),
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
selection: const TextSelection.collapsed(
|
||
offset: 4,
|
||
affinity: TextAffinity.upstream,
|
||
),
|
||
);
|
||
|
||
setUp(() { EditableText.debugDeterministicCursor = true; });
|
||
tearDown(() {
|
||
EditableText.debugDeterministicCursor = false;
|
||
_TestRenderEditablePainter.paintHistory.clear();
|
||
editable.foregroundPainter = null;
|
||
editable.painter = null;
|
||
editable.paintCount = 0;
|
||
|
||
final AbstractNode? parent = editable.parent;
|
||
if (parent is RenderConstrainedBox)
|
||
parent.child = null;
|
||
});
|
||
|
||
test('paints in the correct order', () {
|
||
layout(editable, constraints: BoxConstraints.loose(const Size(100, 100)));
|
||
// Prepare for painting after layout.
|
||
|
||
// Foreground painter.
|
||
editable.foregroundPainter = _TestRenderEditablePainter();
|
||
pumpFrame(phase: EnginePhase.compositingBits);
|
||
|
||
expect(
|
||
(Canvas canvas) => editable.paint(TestRecordingPaintingContext(canvas), Offset.zero),
|
||
paints
|
||
..paragraph()
|
||
..rect(rect: const Rect.fromLTRB(1, 1, 1, 1), color: const Color(0x12345678)),
|
||
);
|
||
|
||
// Background painter.
|
||
editable.foregroundPainter = null;
|
||
editable.painter = _TestRenderEditablePainter();
|
||
|
||
expect(
|
||
(Canvas canvas) => editable.paint(TestRecordingPaintingContext(canvas), Offset.zero),
|
||
paints
|
||
..rect(rect: const Rect.fromLTRB(1, 1, 1, 1), color: const Color(0x12345678))
|
||
..paragraph(),
|
||
);
|
||
|
||
editable.foregroundPainter = _TestRenderEditablePainter();
|
||
editable.painter = _TestRenderEditablePainter();
|
||
|
||
expect(
|
||
(Canvas canvas) => editable.paint(TestRecordingPaintingContext(canvas), Offset.zero),
|
||
paints
|
||
..rect(rect: const Rect.fromLTRB(1, 1, 1, 1), color: const Color(0x12345678))
|
||
..paragraph()
|
||
..rect(rect: const Rect.fromLTRB(1, 1, 1, 1), color: const Color(0x12345678)),
|
||
);
|
||
});
|
||
|
||
test('changing foreground painter', () {
|
||
layout(editable, constraints: BoxConstraints.loose(const Size(100, 100)));
|
||
// Prepare for painting after layout.
|
||
|
||
_TestRenderEditablePainter currentPainter = _TestRenderEditablePainter();
|
||
// Foreground painter.
|
||
editable.foregroundPainter = currentPainter;
|
||
pumpFrame(phase: EnginePhase.paint);
|
||
expect(currentPainter.paintCount, 1);
|
||
|
||
editable.foregroundPainter = currentPainter = _TestRenderEditablePainter()..repaint = false;
|
||
pumpFrame(phase: EnginePhase.paint);
|
||
expect(currentPainter.paintCount, 0);
|
||
|
||
editable.foregroundPainter = currentPainter = _TestRenderEditablePainter()..repaint = true;
|
||
pumpFrame(phase: EnginePhase.paint);
|
||
expect(currentPainter.paintCount, 1);
|
||
});
|
||
|
||
test('changing background painter', () {
|
||
layout(editable, constraints: BoxConstraints.loose(const Size(100, 100)));
|
||
// Prepare for painting after layout.
|
||
|
||
_TestRenderEditablePainter currentPainter = _TestRenderEditablePainter();
|
||
// Foreground painter.
|
||
editable.painter = currentPainter;
|
||
pumpFrame(phase: EnginePhase.paint);
|
||
expect(currentPainter.paintCount, 1);
|
||
|
||
editable.painter = currentPainter = _TestRenderEditablePainter()..repaint = false;
|
||
pumpFrame(phase: EnginePhase.paint);
|
||
expect(currentPainter.paintCount, 0);
|
||
|
||
editable.painter = currentPainter = _TestRenderEditablePainter()..repaint = true;
|
||
pumpFrame(phase: EnginePhase.paint);
|
||
expect(currentPainter.paintCount, 1);
|
||
});
|
||
|
||
test('swapping painters', () {
|
||
layout(editable, constraints: BoxConstraints.loose(const Size(100, 100)));
|
||
|
||
final _TestRenderEditablePainter painter1 = _TestRenderEditablePainter();
|
||
final _TestRenderEditablePainter painter2 = _TestRenderEditablePainter();
|
||
|
||
editable.painter = painter1;
|
||
editable.foregroundPainter = painter2;
|
||
pumpFrame(phase: EnginePhase.paint);
|
||
expect(
|
||
_TestRenderEditablePainter.paintHistory,
|
||
<_TestRenderEditablePainter>[painter1, painter2],
|
||
);
|
||
|
||
_TestRenderEditablePainter.paintHistory.clear();
|
||
editable.painter = painter2;
|
||
editable.foregroundPainter = painter1;
|
||
pumpFrame(phase: EnginePhase.paint);
|
||
expect(
|
||
_TestRenderEditablePainter.paintHistory,
|
||
<_TestRenderEditablePainter>[painter2, painter1],
|
||
);
|
||
});
|
||
|
||
test('reusing the same painter', () {
|
||
layout(editable, constraints: BoxConstraints.loose(const Size(100, 100)));
|
||
|
||
final _TestRenderEditablePainter painter = _TestRenderEditablePainter();
|
||
FlutterErrorDetails? errorDetails;
|
||
editable.painter = painter;
|
||
editable.foregroundPainter = painter;
|
||
pumpFrame(phase: EnginePhase.paint, onErrors: () {
|
||
errorDetails = renderer.takeFlutterErrorDetails();
|
||
});
|
||
expect(errorDetails, isNull);
|
||
|
||
expect(
|
||
_TestRenderEditablePainter.paintHistory,
|
||
<_TestRenderEditablePainter>[painter, painter],
|
||
);
|
||
expect(
|
||
(Canvas canvas) => editable.paint(TestRecordingPaintingContext(canvas), Offset.zero),
|
||
paints
|
||
..rect(rect: const Rect.fromLTRB(1, 1, 1, 1), color: const Color(0x12345678))
|
||
..paragraph()
|
||
..rect(rect: const Rect.fromLTRB(1, 1, 1, 1), color: const Color(0x12345678)),
|
||
);
|
||
});
|
||
test('does not repaint the render editable when custom painters need repaint', () {
|
||
layout(editable, constraints: BoxConstraints.loose(const Size(100, 100)));
|
||
|
||
final _TestRenderEditablePainter painter = _TestRenderEditablePainter();
|
||
editable.painter = painter;
|
||
pumpFrame(phase: EnginePhase.paint);
|
||
editable.paintCount = 0;
|
||
painter.paintCount = 0;
|
||
|
||
painter.markNeedsPaint();
|
||
|
||
pumpFrame(phase: EnginePhase.paint);
|
||
expect(editable.paintCount, 0);
|
||
expect(painter.paintCount, 1);
|
||
});
|
||
|
||
test('repaints when its RenderEditable repaints', () {
|
||
layout(editable, constraints: BoxConstraints.loose(const Size(100, 100)));
|
||
|
||
final _TestRenderEditablePainter painter = _TestRenderEditablePainter();
|
||
editable.painter = painter;
|
||
pumpFrame(phase: EnginePhase.paint);
|
||
editable.paintCount = 0;
|
||
painter.paintCount = 0;
|
||
|
||
editable.markNeedsPaint();
|
||
|
||
pumpFrame(phase: EnginePhase.paint);
|
||
expect(editable.paintCount, 1);
|
||
expect(painter.paintCount, 1);
|
||
});
|
||
|
||
test('correct coordinate space', () {
|
||
layout(editable, constraints: BoxConstraints.loose(const Size(100, 100)));
|
||
|
||
final _TestRenderEditablePainter painter = _TestRenderEditablePainter();
|
||
editable.painter = painter;
|
||
editable.offset = ViewportOffset.fixed(1000);
|
||
|
||
pumpFrame(phase: EnginePhase.compositingBits);
|
||
expect(
|
||
(Canvas canvas) => editable.paint(TestRecordingPaintingContext(canvas), Offset.zero),
|
||
paints
|
||
..rect(rect: const Rect.fromLTRB(1, 1, 1, 1), color: const Color(0x12345678))
|
||
..paragraph(),
|
||
);
|
||
});
|
||
|
||
group('hit testing', () {
|
||
test('hits correct TextSpan when not scrolled', () {
|
||
final TextSelectionDelegate delegate = FakeEditableTextState();
|
||
final RenderEditable editable = RenderEditable(
|
||
text: const TextSpan(
|
||
style: TextStyle(height: 1.0, fontSize: 10.0, fontFamily: 'Ahem'),
|
||
children: <InlineSpan>[
|
||
TextSpan(text: 'A'),
|
||
TextSpan(text: 'B'),
|
||
],
|
||
),
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
textDirection: TextDirection.ltr,
|
||
offset: ViewportOffset.fixed(0.0),
|
||
textSelectionDelegate: delegate,
|
||
selection: const TextSelection.collapsed(
|
||
offset: 0,
|
||
),
|
||
);
|
||
layout(editable, constraints: BoxConstraints.loose(const Size(500.0, 500.0)));
|
||
// Prepare for painting after layout.
|
||
pumpFrame(phase: EnginePhase.compositingBits);
|
||
|
||
BoxHitTestResult result = BoxHitTestResult();
|
||
editable.hitTest(result, position: Offset.zero);
|
||
// We expect two hit test entries in the path because the RenderEditable
|
||
// will add itself as well.
|
||
expect(result.path, hasLength(2));
|
||
HitTestTarget target = result.path.first.target;
|
||
expect(target, isA<TextSpan>());
|
||
expect((target as TextSpan).text, 'A');
|
||
// Only testing the RenderEditable entry here once, not anymore below.
|
||
expect(result.path.last.target, isA<RenderEditable>());
|
||
|
||
result = BoxHitTestResult();
|
||
editable.hitTest(result, position: const Offset(15.0, 0.0));
|
||
expect(result.path, hasLength(2));
|
||
target = result.path.first.target;
|
||
expect(target, isA<TextSpan>());
|
||
expect((target as TextSpan).text, 'B');
|
||
});
|
||
|
||
test('hits correct TextSpan when scrolled vertically', () {
|
||
final TextSelectionDelegate delegate = FakeEditableTextState();
|
||
final RenderEditable editable = RenderEditable(
|
||
text: const TextSpan(
|
||
style: TextStyle(height: 1.0, fontSize: 10.0, fontFamily: 'Ahem'),
|
||
children: <InlineSpan>[
|
||
TextSpan(text: 'A'),
|
||
TextSpan(text: 'B\n'),
|
||
TextSpan(text: 'C'),
|
||
],
|
||
),
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
textDirection: TextDirection.ltr,
|
||
// Given maxLines of null and an offset of 5, the editable will be
|
||
// scrolled vertically by 5 pixels.
|
||
maxLines: null,
|
||
offset: ViewportOffset.fixed(5.0),
|
||
textSelectionDelegate: delegate,
|
||
selection: const TextSelection.collapsed(
|
||
offset: 0,
|
||
),
|
||
);
|
||
layout(editable, constraints: BoxConstraints.loose(const Size(500.0, 500.0)));
|
||
// Prepare for painting after layout.
|
||
pumpFrame(phase: EnginePhase.compositingBits);
|
||
|
||
BoxHitTestResult result = BoxHitTestResult();
|
||
editable.hitTest(result, position: Offset.zero);
|
||
expect(result.path, hasLength(2));
|
||
HitTestTarget target = result.path.first.target;
|
||
expect(target, isA<TextSpan>());
|
||
expect((target as TextSpan).text, 'A');
|
||
|
||
result = BoxHitTestResult();
|
||
editable.hitTest(result, position: const Offset(15.0, 0.0));
|
||
expect(result.path, hasLength(2));
|
||
target = result.path.first.target;
|
||
expect(target, isA<TextSpan>());
|
||
expect((target as TextSpan).text, 'B\n');
|
||
|
||
result = BoxHitTestResult();
|
||
// When we hit at y=6 and are scrolled by -5 vertically, we expect "C"
|
||
// to be hit because the font size is 10.
|
||
editable.hitTest(result, position: const Offset(0.0, 6.0));
|
||
expect(result.path, hasLength(2));
|
||
target = result.path.first.target;
|
||
expect(target, isA<TextSpan>());
|
||
expect((target as TextSpan).text, 'C');
|
||
});
|
||
|
||
test('hits correct TextSpan when scrolled horizontally', () {
|
||
final TextSelectionDelegate delegate = FakeEditableTextState();
|
||
final RenderEditable editable = RenderEditable(
|
||
text: const TextSpan(
|
||
style: TextStyle(height: 1.0, fontSize: 10.0, fontFamily: 'Ahem'),
|
||
children: <InlineSpan>[
|
||
TextSpan(text: 'A'),
|
||
TextSpan(text: 'B'),
|
||
],
|
||
),
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
textDirection: TextDirection.ltr,
|
||
// Given maxLines of 1 and an offset of 5, the editable will be
|
||
// scrolled by 5 pixels to the left.
|
||
maxLines: 1,
|
||
offset: ViewportOffset.fixed(5.0),
|
||
textSelectionDelegate: delegate,
|
||
selection: const TextSelection.collapsed(
|
||
offset: 0,
|
||
),
|
||
);
|
||
layout(editable, constraints: BoxConstraints.loose(const Size(500.0, 500.0)));
|
||
// Prepare for painting after layout.
|
||
pumpFrame(phase: EnginePhase.compositingBits);
|
||
|
||
final BoxHitTestResult result = BoxHitTestResult();
|
||
// At x=6, we should hit "B" as we are scrolled to the left by 6
|
||
// pixels.
|
||
editable.hitTest(result, position: const Offset(6.0, 0));
|
||
expect(result.path, hasLength(2));
|
||
final HitTestTarget target = result.path.first.target;
|
||
expect(target, isA<TextSpan>());
|
||
expect((target as TextSpan).text, 'B');
|
||
});
|
||
});
|
||
});
|
||
|
||
group('delete API implementations', () {
|
||
// Regression test for: https://github.com/flutter/flutter/issues/80226.
|
||
//
|
||
// This textSelectionDelegate has different text and selection from the
|
||
// render editable.
|
||
final FakeEditableTextState delegate = FakeEditableTextState();
|
||
|
||
late RenderEditable editable;
|
||
|
||
setUp(() {
|
||
editable = RenderEditable(
|
||
text: TextSpan(
|
||
text: 'A ' * 50,
|
||
),
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
textDirection: TextDirection.ltr,
|
||
offset: ViewportOffset.fixed(0),
|
||
textSelectionDelegate: delegate,
|
||
selection: const TextSelection(baseOffset: 0, extentOffset: 50),
|
||
);
|
||
|
||
delegate.textEditingValue = const TextEditingValue(
|
||
text: 'BBB',
|
||
selection: TextSelection.collapsed(offset: 0),
|
||
);
|
||
});
|
||
|
||
void verifyDoesNotCrashWithInconsistentTextEditingValue(void Function(SelectionChangedCause) method) {
|
||
editable = RenderEditable(
|
||
text: TextSpan(
|
||
text: 'A ' * 50,
|
||
),
|
||
startHandleLayerLink: LayerLink(),
|
||
endHandleLayerLink: LayerLink(),
|
||
textDirection: TextDirection.ltr,
|
||
offset: ViewportOffset.fixed(0),
|
||
textSelectionDelegate: delegate,
|
||
selection: const TextSelection(baseOffset: 0, extentOffset: 50),
|
||
);
|
||
|
||
layout(editable, constraints: BoxConstraints.loose(const Size(500.0, 500.0)));
|
||
dynamic error;
|
||
try {
|
||
method(SelectionChangedCause.tap);
|
||
} catch (e) {
|
||
error = e;
|
||
}
|
||
expect(error, isNull);
|
||
}
|
||
|
||
test('delete is not racy and handles composing region correctly', () {
|
||
delegate.textEditingValue = const TextEditingValue(
|
||
text: 'ABCDEF',
|
||
selection: TextSelection.collapsed(offset: 2),
|
||
composing: TextRange(start: 1, end: 6),
|
||
);
|
||
verifyDoesNotCrashWithInconsistentTextEditingValue(editable.delete);
|
||
final TextEditingValue textEditingValue = editable.textSelectionDelegate.textEditingValue;
|
||
expect(textEditingValue.text, 'ACDEF');
|
||
expect(textEditingValue.selection.isCollapsed, isTrue);
|
||
expect(textEditingValue.selection.baseOffset, 1);
|
||
expect(textEditingValue.composing, const TextRange(start: 1, end: 5));
|
||
});
|
||
|
||
test('deleteForward is not racy and handles composing region correctly', () {
|
||
delegate.textEditingValue = const TextEditingValue(
|
||
text: 'ABCDEF',
|
||
selection: TextSelection.collapsed(offset: 2),
|
||
composing: TextRange(start: 2, end: 6),
|
||
);
|
||
verifyDoesNotCrashWithInconsistentTextEditingValue(editable.deleteForward);
|
||
final TextEditingValue textEditingValue = editable.textSelectionDelegate.textEditingValue;
|
||
expect(textEditingValue.text, 'ABDEF');
|
||
expect(textEditingValue.selection.isCollapsed, isTrue);
|
||
expect(textEditingValue.selection.baseOffset, 2);
|
||
expect(textEditingValue.composing, const TextRange(start: 2, end: 5));
|
||
});
|
||
});
|
||
}
|
||
|
||
class _TestRenderEditable extends RenderEditable {
|
||
_TestRenderEditable({
|
||
required TextDirection textDirection,
|
||
required ViewportOffset offset,
|
||
required TextSelectionDelegate textSelectionDelegate,
|
||
TextSpan? text,
|
||
required LayerLink startHandleLayerLink,
|
||
required LayerLink endHandleLayerLink,
|
||
TextSelection? selection,
|
||
}) : super(
|
||
textDirection: textDirection,
|
||
offset: offset,
|
||
textSelectionDelegate: textSelectionDelegate,
|
||
text: text,
|
||
startHandleLayerLink: startHandleLayerLink,
|
||
endHandleLayerLink: endHandleLayerLink,
|
||
selection: selection,
|
||
);
|
||
|
||
int paintCount = 0;
|
||
|
||
@override
|
||
void paint(PaintingContext context, Offset offset) {
|
||
super.paint(context, offset);
|
||
paintCount += 1;
|
||
}
|
||
}
|
||
|
||
class _TestRenderEditablePainter extends RenderEditablePainter {
|
||
bool repaint = true;
|
||
int paintCount = 0;
|
||
static final List<_TestRenderEditablePainter> paintHistory = <_TestRenderEditablePainter>[];
|
||
|
||
@override
|
||
void paint(Canvas canvas, Size size, RenderEditable renderEditable) {
|
||
paintCount += 1;
|
||
canvas.drawRect(const Rect.fromLTRB(1, 1, 1, 1), Paint()..color = const Color(0x12345678));
|
||
paintHistory.add(this);
|
||
}
|
||
|
||
@override
|
||
bool shouldRepaint(RenderEditablePainter? oldDelegate) => repaint;
|
||
|
||
void markNeedsPaint() {
|
||
notifyListeners();
|
||
}
|
||
}
|