Skip to content

Commit c28a13b

Browse files
CatHood0CatHood0EchoEllet
authored
Chore: general improvements in flutter_quill_tests API (singerdmx#2512)
* feat: added support for common text operations in flutter_quill_tests * feat: toolbar and selection nodes interactions * chore: updated example code in readme to be useful --------- Co-authored-by: CatHood0 <santiagowmar@gmail.com> Co-authored-by: Ellet <echo.ellet@gmail.com>
1 parent 48f5b57 commit c28a13b

10 files changed

+688
-34
lines changed

flutter_quill_test/CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
1111
## [Unreleased]
1212

13+
### Added
14+
15+
- Add more APIs for testing [#2512](https://github.com/singerdmx/flutter-quill/pull/2512).
16+
- Replace text with `quillReplaceText` and `quillReplaceTextWithSelection`.
17+
- Delete text with `quillRemoveText` and `quillRemoveTextInSelection`.
18+
- Insert text at the specified position with `quillEnterTextAtPosition`.
19+
- Update the edit text value with a `TextSelection` using `quillUpdateEditingValueWithSelection`.
20+
- Get the current `TextEditingValue` using `getTextEditingValue`.
21+
- Move the cursor with `quillMoveCursorTo`, `quillUpdateSelection`, and `quillExpandSelectionTo`.
22+
- Simulate hiding the keyboard using `quillHideKeyboard`.
23+
- Find the `QuillEditor` or `QuillRawEditorState` using `findRawEditor` and `findEditor`.
24+
1325
## [11.0.0] - 2025-02-17
1426

1527
### Changed

flutter_quill_test/README.md

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ which include methods to simplify interacting with the editor in test cases.
77

88
- [💾 Installation](#-installation)
99
- [🧪 Testing](#-testing)
10+
- [🛠️ Utilities](#-utilities)
1011
- [🤝 Contributing](#-contributing)
1112

1213
## 💾 Installation
@@ -23,18 +24,93 @@ flutter pub add dev:flutter_quill_test
2324
To aid in testing applications using the editor an extension to the flutter `WidgetTester` is provided which includes
2425
methods to simplify interacting with the editor in test cases.
2526

26-
Import the test utilities in your test file:
27+
## 🛠️ Utilities
28+
29+
This package provides a set of utilities to simplify testing with the `QuillEditor`.
30+
31+
First, import the test utilities in your test file:
2732

2833
```dart
2934
import 'package:flutter_quill_test/flutter_quill_test.dart';
3035
```
3136

32-
and then enter text using `quillEnterText`:
37+
### Usage
38+
39+
#### Entering Text
40+
41+
To enter text into the `QuillEditor`, use the `quillEnterText` method:
3342

3443
```dart
3544
await tester.quillEnterText(find.byType(QuillEditor), 'test\n');
3645
```
3746

47+
#### Replacing Text
48+
49+
You can replace text in the `QuillEditor` using the `quillReplaceText` method:
50+
51+
```dart
52+
await tester.quillReplaceText(find.byType(QuillEditor), 'text to be used for replace');
53+
```
54+
55+
#### Removing Text
56+
57+
To remove text from the `QuillEditor`, you can use the `quillRemoveText` method:
58+
59+
```dart
60+
await tester.quillRemoveText(
61+
find.byType(QuillEditor),
62+
TextSelection(baseOffset: 2, extentOffset: 3),
63+
);
64+
```
65+
66+
#### Moving the Cursor
67+
68+
To change the selection values into the `QuillEditor` without use the `QuillController`, use the `quillUpdateSelection` method:
69+
70+
```dart
71+
await tester.quillUpdateSelection(find.byType(QuillEditor), 0, 10);
72+
```
73+
74+
#### Full Example
75+
76+
Here’s a complete example of how you might use these utilities in a test:
77+
78+
```dart
79+
import 'package:flutter_test/flutter_test.dart';
80+
import 'package:flutter_quill/flutter_quill.dart';
81+
import 'package:flutter_quill_test/flutter_quill_test.dart';
82+
83+
void main() {
84+
testWidgets('Example QuillEditor test', (WidgetTester tester) async {
85+
final QuillController controller = QuillController.basic();
86+
await tester.pumpWidget(
87+
MaterialApp(
88+
home: QuillEditor.basic(
89+
controller: controller,
90+
config: const QuillEditorConfig(),
91+
),
92+
),
93+
);
94+
95+
await tester.tap(find.byType(QuillEditor));
96+
await tester.quillEnterText(find.byType(QuillEditor), 'Hello, World!\n');
97+
expect(controller.document.toPlainText(), 'Hello, World!\n');
98+
99+
await tester.quillMoveCursorTo(find.byType(QuillEditor), 12);
100+
await tester.quillExpandSelectionTo(find.byType(QuillEditor), 13);
101+
102+
await tester.quillReplaceText(find.byType(QuillEditor), ' and hi, World!');
103+
expect(controller.document.toPlainText(), 'Hello, World and hi, World!\n');
104+
105+
await tester.quillMoveCursorTo(find.byType(QuillEditor), 0);
106+
await tester.quillExpandSelectionTo(find.byType(QuillEditor), 7);
107+
108+
await tester.quillRemoveTextInSelection(find.byType(QuillEditor));
109+
expect(controller.document.toPlainText(), 'World and hi, World!\n');
110+
});
111+
}
112+
```
113+
38114
## 🤝 Contributing
39115

40116
We greatly appreciate your time and effort.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
11
library;
22

3+
export 'src/test/selection_interactions/widget_tester_cursor_movement_extension.dart';
4+
export 'src/test/selection_interactions/widget_tester_nodes_selection_extension.dart';
5+
export 'src/test/text_interactions/widget_tester_insert_extension.dart';
6+
export 'src/test/text_interactions/widget_tester_remove_extension.dart';
7+
export 'src/test/text_interactions/widget_tester_replace_extension.dart';
38
export 'src/test/widget_tester_extension.dart';
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter_test/flutter_test.dart';
3+
import '../widget_tester_extension.dart';
4+
5+
extension QuillWidgetTesterCursorMovementExtension on WidgetTester {
6+
/// Updates the text editing value to move the cursor to the specified [index],
7+
/// as if it had been provided by the onscreen keyboard.
8+
///
9+
/// Example:
10+
/// ```dart
11+
/// await tester.quillGiveFocus(find.byType(QuillEditor));
12+
/// await tester.quillMoveCursorTo(find.byType(QuillEditor), 5); // Move cursor to index 5
13+
/// ```
14+
///
15+
/// The widget specified by [finder] must already have focus and be a
16+
/// [QuillEditor] or have a [QuillEditor] descendant. For example:
17+
/// ```dart
18+
/// find.byType(QuillEditor)
19+
/// ```
20+
Future<void> quillMoveCursorTo(Finder finder, int index) async {
21+
final editor = findRawEditor(finder);
22+
return TestAsyncUtils.guard(() async {
23+
testTextInput.updateEditingValue(
24+
TextEditingValue(
25+
text: editor.textEditingValue.text,
26+
selection: TextSelection.collapsed(
27+
offset: index,
28+
),
29+
),
30+
);
31+
await idle();
32+
});
33+
}
34+
35+
/// Updates the text editing value to expand the current selection to the specified
36+
/// [index], as if it had been provided by the onscreen keyboard.
37+
///
38+
/// Example:
39+
/// ```dart
40+
/// await tester.quillGiveFocus(find.byType(QuillEditor));
41+
/// await quillExpandSelectionTo(find.byType(QuillEditor), 10); // Expand selection to index 10
42+
/// ```
43+
///
44+
/// The widget specified by [finder] must already have focus and be a
45+
/// [QuillEditor] or have a [QuillEditor] descendant. For example:
46+
/// ```dart
47+
/// find.byType(QuillEditor)
48+
/// ```
49+
Future<void> quillExpandSelectionTo(Finder finder, int index) async {
50+
final editor = findRawEditor(finder);
51+
return TestAsyncUtils.guard(() async {
52+
testTextInput.updateEditingValue(
53+
TextEditingValue(
54+
text: editor.textEditingValue.text,
55+
selection: editor.textEditingValue.selection.expandTo(
56+
TextPosition(offset: index),
57+
),
58+
),
59+
);
60+
await idle();
61+
});
62+
}
63+
64+
/// Updates the [selection] of the current text editing value to the range
65+
/// specified by [from] and [to], as if it had been provided by the onscreen
66+
/// keyboard.
67+
///
68+
/// The widget specified by [finder] must already have focus and be a
69+
/// [QuillEditor] or have a [QuillEditor] descendant. For example:
70+
/// ```dart
71+
/// find.byType(QuillEditor)
72+
/// ```
73+
///
74+
/// Example:
75+
/// ```dart
76+
/// await tester.quillGiveFocus(find.byType(QuillEditor));
77+
/// await quillUpdateSelection(
78+
/// find.byType(QuillEditor),
79+
/// 5, // Start of selection
80+
/// 10, // End of selection
81+
/// );
82+
/// ```
83+
Future<void> quillUpdateSelection(Finder finder, int from, int to) async {
84+
final editor = findRawEditor(finder);
85+
expect(from, isNonNegative);
86+
expect(to, isNonNegative);
87+
return TestAsyncUtils.guard(() async {
88+
testTextInput.updateEditingValue(
89+
TextEditingValue(
90+
text: editor.textEditingValue.text,
91+
selection: TextSelection(baseOffset: from, extentOffset: to),
92+
),
93+
);
94+
await idle();
95+
});
96+
}
97+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import 'package:flutter_quill/flutter_quill.dart';
2+
import 'package:flutter_test/flutter_test.dart';
3+
4+
import '../widget_tester_extension.dart';
5+
6+
extension QuillWidgetTesterNodesSelectionExtension on WidgetTester {
7+
/// Returns all the nodes that are currently selected in the [QuillEditor] widget
8+
/// specified by [editorFinder].
9+
///
10+
/// Example:
11+
/// ```dart
12+
/// final nodes = await tester.getNodesInSelection(find.byType(QuillEditor));
13+
/// ```
14+
///
15+
/// The widget specified by [editorFinder] must already have focus and be a
16+
/// [QuillEditor] or have a [QuillEditor] descendant. For example:
17+
/// ```dart
18+
/// find.byType(QuillEditor)
19+
/// ```
20+
Future<Iterable<Node>> getNodesInSelection(Finder editorFinder) async {
21+
final hasFocus = await quillHasFocusEditor(editorFinder);
22+
expect(hasFocus, isTrue, reason: 'The editor must already have focus');
23+
final editor = findRawEditor(editorFinder);
24+
final selection = editor.textEditingValue.selection;
25+
if (!selection.isValid || selection.isCollapsed) return [];
26+
final start =
27+
editor.controller.document.queryChild(selection.baseOffset).node;
28+
final end =
29+
editor.controller.document.queryChild(selection.extentOffset).node;
30+
expect(start, isNotNull,
31+
reason: 'The node at offset: ${selection.start} was not found');
32+
expect(end, isNotNull,
33+
reason: 'The node at offset: ${selection.end} was not found');
34+
if (start == end) {
35+
return [start!];
36+
}
37+
var nextChild = start?.next;
38+
expect(nextChild, isNotNull);
39+
40+
final nodesInSelection = <Node>[start!, nextChild!];
41+
while (nextChild != end) {
42+
nextChild = nextChild?.next;
43+
nodesInSelection.add(nextChild!);
44+
}
45+
return [...nodesInSelection];
46+
}
47+
48+
/// Returns the node that is currently selected in the [QuillEditor] widget
49+
/// specified by [editorFinder].
50+
///
51+
/// Example:
52+
/// ```dart
53+
/// final node = await tester.getNodeInSelection(find.byType(QuillEditor));
54+
/// if (node != null) {
55+
/// print('Selected node: $node');
56+
/// }
57+
/// ```
58+
///
59+
/// The widget specified by [editorFinder] must already have focus and be a
60+
/// [QuillEditor] or have a [QuillEditor] descendant. For example:
61+
/// ```dart
62+
/// find.byType(QuillEditor)
63+
/// ```
64+
Future<Node?> getNodeInSelection(Finder editorFinder) async {
65+
final hasFocus = await quillHasFocusEditor(editorFinder);
66+
expect(hasFocus, isTrue, reason: 'The editor must already have focus');
67+
final editor = findRawEditor(editorFinder);
68+
final selection = editor.textEditingValue.selection;
69+
if (!selection.isValid) return null;
70+
final start =
71+
editor.controller.document.queryChild(selection.baseOffset).node;
72+
final end =
73+
editor.controller.document.queryChild(selection.extentOffset).node;
74+
expect(start, isNotNull,
75+
reason: 'The node at offset: ${start?.documentOffset} was not found');
76+
final isSelectionIntoSameNode = start == end;
77+
if (isSelectionIntoSameNode) {
78+
return start!;
79+
}
80+
return null;
81+
}
82+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import 'package:flutter/cupertino.dart';
2+
import 'package:flutter_test/flutter_test.dart';
3+
import '../widget_tester_extension.dart';
4+
5+
extension QuillWidgetTesterInsertionExt on WidgetTester {
6+
/// Updates its editing value with the provided [text], as if it had been
7+
/// entered via the onscreen keyboard.
8+
///
9+
/// Example:
10+
/// ```dart
11+
/// await tester.quillEnterText(find.byType(QuillEditor), 'Hello, world!');
12+
/// ```
13+
///
14+
/// The widget specified by [finder] must be a [QuillEditor] or have a
15+
/// [QuillEditor] descendant. For example:
16+
/// ```dart
17+
/// find.byType(QuillEditor)
18+
/// ```
19+
Future<void> quillEnterText(Finder finder, String text) async {
20+
return TestAsyncUtils.guard(() async {
21+
await quillGiveFocus(finder);
22+
await quillUpdateEditingValue(finder, text);
23+
await idle();
24+
});
25+
}
26+
27+
/// Inserts [textInsert] at the specified [index], updating its editing value
28+
/// as if it had been provided by the onscreen keyboard.
29+
///
30+
/// Example:
31+
/// ```dart
32+
/// await tester.quillEnterTextAtPosition(
33+
/// find.byType(QuillEditor),
34+
/// 'inserted text',
35+
/// 5, // Insert at index 5
36+
/// );
37+
/// ```
38+
///
39+
/// The widget specified by [finder] must be a [QuillEditor] or have a
40+
/// [QuillEditor] descendant. For example:
41+
/// ```dart
42+
/// find.byType(QuillEditor)
43+
/// ```
44+
Future<void> quillEnterTextAtPosition(
45+
Finder finder, String textInsert, int index) async {
46+
expect(index, isNonNegative,
47+
reason: 'Index passed cannot be less than zero');
48+
final editor = findRawEditor(finder);
49+
final plainText = editor.controller.document
50+
.toPlainText()
51+
.replaceRange(index, index, textInsert);
52+
return TestAsyncUtils.guard(() async {
53+
await quillGiveFocus(finder);
54+
await quillUpdateEditingValueWithSelection(
55+
finder,
56+
plainText,
57+
TextSelection.collapsed(offset: index + textInsert.length),
58+
);
59+
await idle();
60+
});
61+
}
62+
}

0 commit comments

Comments
 (0)