main function
void
main()
Implementation
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('RichTextController Tests', () {
late BuildContext mockContext;
setUp(() {
// Create a mock BuildContext
mockContext = MaterialApp(home: Container()).createElement();
});
test('Basic text matching and styling', () {
final matches = <String>[];
final matchIndices = <Map<String, List<int>>>[];
final controller = RichTextController(
targetMatches: [
MatchTargetItem(
text: 'test',
style: const TextStyle(color: Colors.red),
),
],
onMatch: (m) => matches.addAll(m),
onMatchIndex: (m) => matchIndices.addAll(m),
);
controller.text = 'This is a test message';
final textSpan = controller.buildTextSpan(
context: mockContext,
style: const TextStyle(),
withComposing: false,
);
expect(matches, contains('test'));
expect(matchIndices.length, 1);
expect(textSpan.children!.length, 3); // Before match, match, after match
});
test('Multiple pattern matching', () {
final controller = RichTextController(
targetMatches: [
MatchTargetItem(
text: 'hello',
style: const TextStyle(color: Colors.blue),
),
MatchTargetItem(
text: 'world',
style: const TextStyle(color: Colors.red),
),
],
onMatch: (_) {},
);
controller.text = 'hello world';
final textSpan = controller.buildTextSpan(
context: mockContext,
style: const TextStyle(),
withComposing: false,
);
expect(textSpan.children!.length, 3); // 'hello', space, 'world'
});
test('Regex pattern matching', () {
final matches = <String>[];
final controller = RichTextController(
targetMatches: [
MatchTargetItem(
regex: RegExp(r'\d+'),
style: const TextStyle(color: Colors.green),
),
],
onMatch: (m) => matches.addAll(m),
);
controller.text = 'Number 123 and 456';
controller.buildTextSpan(
context: mockContext,
style: const TextStyle(),
withComposing: false,
);
expect(matches, contains('123'));
expect(matches, contains('456'));
});
test('Tap callback functionality', () {
String? tappedText;
final controller = RichTextController(
targetMatches: [
MatchTargetItem(
text: 'clickme',
style: const TextStyle(color: Colors.blue),
onTap: (text) => tappedText = text,
),
],
onMatch: (_) {},
);
controller.text = 'Please clickme now';
final textSpan = controller.buildTextSpan(
context: mockContext,
style: const TextStyle(),
withComposing: false,
);
// Find the clickable span
final clickableSpan = textSpan.children!
.whereType<TextSpan>()
.firstWhere((span) => span.recognizer != null);
// Simulate tap
(clickableSpan.recognizer as TapGestureRecognizer).onTap!();
expect(tappedText, equals('clickme'));
});
test('Dynamic target matches update', () {
final controller = RichTextController(
targetMatches: [
MatchTargetItem(
text: 'initial',
style: const TextStyle(color: Colors.red),
),
],
onMatch: (_) {},
);
controller.updateTargetMatches([
MatchTargetItem(
text: 'updated',
style: const TextStyle(color: Colors.blue),
),
]);
controller.text = 'This is updated text';
final textSpan = controller.buildTextSpan(
context: mockContext,
style: const TextStyle(),
withComposing: false,
);
// Verify the new pattern is matched
final matchedSpan = textSpan.children!
.whereType<TextSpan>()
.firstWhere((span) => span.style?.color == Colors.blue);
expect(matchedSpan.text, equals('updated'));
});
test('RegExp properties update', () {
final controller = RichTextController(
targetMatches: [
MatchTargetItem(
regex: RegExp(r'test$'),
style: const TextStyle(color: Colors.red),
),
],
onMatch: (_) {},
regExpMultiLine: false,
);
controller.text = 'test\ntest';
var initialMatches = controller
.buildTextSpan(
context: mockContext,
style: const TextStyle(),
withComposing: false,
)
.children!
.length;
controller.updateRegExpProperties(multiLine: true);
var updatedMatches = controller
.buildTextSpan(
context: mockContext,
style: const TextStyle(),
withComposing: false,
)
.children!
.length;
expect(initialMatches != updatedMatches, true);
});
test('IME composing region handling', () {
final controller = RichTextController(
targetMatches: [
MatchTargetItem(
text: 'test',
style: const TextStyle(color: Colors.red),
),
],
onMatch: (_) {},
);
controller.value = const TextEditingValue(
text: 'This is a test',
composing: TextRange(start: 10, end: 14),
);
final textSpan = controller.buildTextSpan(
context: mockContext,
style: const TextStyle(),
withComposing: true,
);
// Verify composing region has underline decoration
final composingSpan = textSpan.children!.whereType<TextSpan>().firstWhere(
(span) => span.style?.decoration == TextDecoration.underline);
expect(composingSpan, isNotNull);
});
test('Backspace deletion behavior', () {
final controller = RichTextController(
targetMatches: [
MatchTargetItem(
text: 'delete',
style: const TextStyle(color: Colors.red),
deleteOnBack: true,
),
],
onMatch: (_) {},
);
controller.text = 'Please delete me';
controller.buildTextSpan(
context: mockContext,
style: const TextStyle(),
withComposing: false,
);
controller.selection =
const TextSelection.collapsed(offset: 12); // End of 'delete'
// Simulate backspace
'Please delete m';
controller.buildTextSpan(
context: mockContext,
style: const TextStyle(),
withComposing: false,
);
// Wait for post-frame callback
WidgetsBinding.instance.addPostFrameCallback((_) {
expect(controller.text, equals('Please me'));
});
});
test('Cache invalidation', () {
final controller = RichTextController(
targetMatches: [
MatchTargetItem(
text: 'test',
style: const TextStyle(color: Colors.red),
),
],
onMatch: (_) {},
);
controller.text = 'This is a test';
controller.buildTextSpan(
context: mockContext,
style: const TextStyle(),
withComposing: false,
);
// Access private fields for testing cache
final initialRegex = controller.toString();
controller.updateTargetMatches([
MatchTargetItem(
text: 'different',
style: const TextStyle(color: Colors.blue),
),
]);
controller.buildTextSpan(
context: mockContext,
style: const TextStyle(),
withComposing: false,
);
final updatedRegex = controller.toString();
expect(initialRegex != updatedRegex, true);
});
});
}