161 lines
5.9 KiB
Dart
161 lines
5.9 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:muzzle_velocity/markdown_editing.dart';
|
|
|
|
final _formatter = MarkdownInputFormatter();
|
|
|
|
TextEditingValue _value(String text, int start, [int? end]) => TextEditingValue(
|
|
text: text,
|
|
selection: TextSelection(baseOffset: start, extentOffset: end ?? start),
|
|
);
|
|
|
|
/// Simulates typing [typed] over the current selection of [old].
|
|
TextEditingValue _type(TextEditingValue old, String typed) {
|
|
final s = old.selection.start;
|
|
final raw = _value(old.text.replaceRange(s, old.selection.end, typed),
|
|
s + typed.length);
|
|
return _formatter.formatEditUpdate(old, raw);
|
|
}
|
|
|
|
/// Text with `|` marking the caret.
|
|
TextEditingValue _caret(String marked) =>
|
|
_value(marked.replaceFirst('|', ''), marked.indexOf('|'));
|
|
|
|
String _marked(TextEditingValue v) =>
|
|
v.text.replaceRange(v.selection.start, v.selection.start, '|');
|
|
|
|
Map<String, String> _classes(String text) => {
|
|
for (final s in highlightMarkdown(text))
|
|
text.substring(s.start, s.end): s.className,
|
|
};
|
|
|
|
void main() {
|
|
group('wrap selection', () {
|
|
test('asterisk wraps and keeps the word selected', () {
|
|
final once = _type(_value('a word here', 2, 6), '*');
|
|
expect(once.text, 'a *word* here');
|
|
expect(once.selection, const TextSelection(baseOffset: 3, extentOffset: 7));
|
|
expect(_type(once, '*').text, 'a **word** here');
|
|
});
|
|
|
|
test('bracket uses its closing pair', () {
|
|
expect(_type(_value('see link', 4, 8), '[').text, 'see [link]');
|
|
});
|
|
|
|
test('a letter still replaces the selection', () {
|
|
final result = _type(_value('a word', 2, 6), 'x');
|
|
expect(result.text, 'a x');
|
|
});
|
|
});
|
|
|
|
group('new line', () {
|
|
test('keeps indentation', () {
|
|
expect(_marked(_type(_caret(' code|'), '\n')), ' code\n |');
|
|
});
|
|
|
|
test('continues bullets, numbers, tasks and quotes', () {
|
|
expect(_marked(_type(_caret('- one|'), '\n')), '- one\n- |');
|
|
expect(_marked(_type(_caret(' * one|'), '\n')), ' * one\n * |');
|
|
expect(_marked(_type(_caret('9. nine|'), '\n')), '9. nine\n10. |');
|
|
expect(_marked(_type(_caret('1) a|'), '\n')), '1) a\n2) |');
|
|
expect(_marked(_type(_caret('- [x] done|'), '\n')), '- [x] done\n- [ ] |');
|
|
expect(_marked(_type(_caret('> quote|'), '\n')), '> quote\n> |');
|
|
});
|
|
|
|
test('splits an item when the caret is mid-line', () {
|
|
expect(_marked(_type(_caret('- one|two'), '\n')), '- one\n- |two');
|
|
});
|
|
|
|
test('an empty item ends the list', () {
|
|
expect(_marked(_type(_caret('- one\n- |'), '\n')), '- one\n|');
|
|
expect(_marked(_type(_caret('1. a\n2. |'), '\n')), '1. a\n|');
|
|
});
|
|
|
|
test('inside a code fence only indentation carries over', () {
|
|
expect(_marked(_type(_caret('```\n - x|'), '\n')), '```\n - x\n |');
|
|
});
|
|
|
|
test('emphasis at line start is not a bullet', () {
|
|
expect(_marked(_type(_caret('*bold* text|'), '\n')), '*bold* text\n|');
|
|
});
|
|
});
|
|
|
|
group('indentLines', () {
|
|
test('indents and outdents all selected lines', () {
|
|
final value = _value('a\nb\nc', 0, 3);
|
|
final indented = indentLines(value);
|
|
expect(indented.text, ' a\n b\nc');
|
|
expect(indentLines(indented, outdent: true).text, 'a\nb\nc');
|
|
});
|
|
});
|
|
|
|
group('toggleEmphasis', () {
|
|
test('wraps, unwraps and inserts empty pairs', () {
|
|
final bold = toggleEmphasis(_value('a word', 2, 6), '**');
|
|
expect(bold.text, 'a **word**');
|
|
expect(bold.selection, const TextSelection(baseOffset: 4, extentOffset: 8));
|
|
expect(toggleEmphasis(bold, '**').text, 'a word');
|
|
|
|
final boldItalic = toggleEmphasis(bold, '*');
|
|
expect(boldItalic.text, 'a ***word***');
|
|
expect(toggleEmphasis(boldItalic, '*').text, 'a **word**');
|
|
expect(toggleEmphasis(boldItalic, '**').text, 'a *word*');
|
|
|
|
expect(_marked(toggleEmphasis(_caret('x|'), '*')), 'x*|*');
|
|
});
|
|
});
|
|
|
|
group('highlightMarkdown', () {
|
|
test('line and inline syntax', () {
|
|
final classes = _classes(
|
|
'## Title\n- [ ] task **bold** and *it* `code` ~~gone~~\n[text](url) #tag +proj @ctx');
|
|
expect(classes['## Title'], 'header-h2');
|
|
expect(classes['- [ ] '], 'task-list');
|
|
expect(classes['**bold**'], 'bold');
|
|
expect(classes['*it*'], 'italic');
|
|
expect(classes['`code`'], 'code');
|
|
expect(classes['~~gone~~'], 'strikethrough');
|
|
expect(classes['[text]'], 'link-text');
|
|
expect(classes['(url)'], 'link-url');
|
|
expect(classes['#tag'], 'tag-hash');
|
|
expect(classes['+proj'], 'tag-plus');
|
|
expect(classes['@ctx'], 'tag-at');
|
|
});
|
|
|
|
test('fenced code spans lines without inline highlighting', () {
|
|
final classes = _classes('```\n**not bold**\n```\n**bold**');
|
|
expect(classes['**not bold**'], 'code');
|
|
expect(classes['**bold**'], 'bold');
|
|
});
|
|
|
|
test('segments are sorted and do not overlap', () {
|
|
final segments = highlightMarkdown('- a **b** #c\n> q\n---');
|
|
for (var i = 1; i < segments.length; i++) {
|
|
expect(segments[i].start, greaterThanOrEqualTo(segments[i - 1].end));
|
|
}
|
|
});
|
|
});
|
|
|
|
group('MarkdownController search', () {
|
|
test('finds terms case-insensitively and follows edits', () {
|
|
final controller =
|
|
MarkdownController(text: 'Foo bar foo #tag', theme: const {});
|
|
controller.search(['foo', '#tag']);
|
|
expect(controller.matches.map((m) => m.start), [0, 8, 12]);
|
|
|
|
controller.text = 'bar foo';
|
|
expect(controller.matches.map((m) => m.start), [4]);
|
|
});
|
|
|
|
test('builds spans covering the whole text', () {
|
|
final controller =
|
|
MarkdownController(text: '# Hi **there**', theme: const {});
|
|
controller.search(['there']);
|
|
final span = controller.buildTextSpan(
|
|
context: _FakeContext(), withComposing: false);
|
|
expect(span.toPlainText(), '# Hi **there**');
|
|
});
|
|
});
|
|
}
|
|
|
|
class _FakeContext extends Fake implements BuildContext {}
|