Rich text editor for Flutter
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

419 lines
15 KiB

import 'package:flutter/material.dart';
import 'package:flutter_quill/flutter_quill.dart';
import 'package:flutter_quill/quill_delta.dart';
import 'package:test/test.dart';
void main() {
const testDocumentContents = 'data';
late QuillController controller;
WidgetsFlutterBinding.ensureInitialized();
setUp(() {
controller = QuillController.basic()
..compose(Delta()..insert(testDocumentContents),
const TextSelection.collapsed(offset: 0), ChangeSource.local);
});
group('controller', () {
test('set document', () {
const replacementContents = 'replacement\n';
final newDocument =
Document.fromDelta(Delta()..insert(replacementContents));
var listenerCalled = false;
controller
..addListener(() {
listenerCalled = true;
})
..document = newDocument;
expect(listenerCalled, isTrue);
expect(controller.document.toPlainText(), replacementContents);
});
test('getSelectionStyle', () {
controller
..formatText(0, 5, Attribute.h1)
..updateSelection(const TextSelection(baseOffset: 0, extentOffset: 4),
ChangeSource.local);
expect(controller.getSelectionStyle().values, [Attribute.h1]);
});
test('indentSelection with single line document', () {
var listenerCalled = false;
// With selection range
controller
..updateSelection(const TextSelection(baseOffset: 0, extentOffset: 4),
ChangeSource.local)
..addListener(() {
listenerCalled = true;
})
..indentSelection(true);
expect(listenerCalled, isTrue);
expect(controller.getSelectionStyle().values, [Attribute.indentL1]);
controller.indentSelection(true);
expect(controller.getSelectionStyle().values, [Attribute.indentL2]);
controller.indentSelection(false);
expect(controller.getSelectionStyle().values, [Attribute.indentL1]);
controller.indentSelection(false);
expect(controller.getSelectionStyle().values, []);
// With collapsed selection
controller
..updateSelection(
const TextSelection.collapsed(offset: 0), ChangeSource.local)
..indentSelection(true);
expect(controller.getSelectionStyle().values, [Attribute.indentL1]);
controller
..updateSelection(
const TextSelection.collapsed(offset: 0), ChangeSource.local)
..indentSelection(true);
expect(controller.getSelectionStyle().values, [Attribute.indentL2]);
controller.indentSelection(false);
expect(controller.getSelectionStyle().values, [Attribute.indentL1]);
controller.indentSelection(false);
expect(controller.getSelectionStyle().values, []);
});
test('indentSelection with multiline document', () {
controller
..compose(Delta()..insert('line1\nline2\nline3\n'),
const TextSelection.collapsed(offset: 0), ChangeSource.local)
// Indent first line
..updateSelection(
const TextSelection.collapsed(offset: 0), ChangeSource.local)
..indentSelection(true);
expect(controller.getSelectionStyle().values, [Attribute.indentL1]);
// Indent first two lines
controller
..updateSelection(const TextSelection(baseOffset: 0, extentOffset: 11),
ChangeSource.local)
..indentSelection(true);
// Should have both L1 and L2 indent attributes in selection.
expect(
controller.getAllSelectionStyles(),
contains(
const Style().put(Attribute.indentL1).put(Attribute.indentL2),
),
);
// Remaining lines should have no attributes.
controller.updateSelection(
TextSelection(
baseOffset: 12,
extentOffset: controller.document.toPlainText().length - 1),
ChangeSource.local);
expect(controller.getAllSelectionStyles(), everyElement(const Style()));
});
test('getAllIndividualSelectionStylesAndEmbed', () {
controller
..formatText(0, 2, Attribute.bold)
..replaceText(2, 2, BlockEmbed.image('/test'), null)
..updateSelection(const TextSelection(baseOffset: 0, extentOffset: 4),
ChangeSource.remote);
final result = controller.getAllIndividualSelectionStylesAndEmbed();
expect(result.length, 2);
expect(result[0].offset, 0);
expect(result[0].value, const Style().put(Attribute.bold));
expect((result[1].value as Embeddable).type, BlockEmbed.imageType);
});
test('getAllIndividualSelectionStylesAndEmbed mixed', () {
controller
..replaceText(0, 4, 'bold plain italic', null)
..formatText(0, 4, Attribute.bold)
..formatText(11, 17, Attribute.italic)
..updateSelection(const TextSelection(baseOffset: 2, extentOffset: 14),
ChangeSource.local);
expect(controller.getPlainText(), 'ld plain ita',
reason: 'Selection spans 3 styles');
//
final result = controller.getAllIndividualSelectionStylesAndEmbed();
expect(result.length, 2);
expect(result[0].offset, 0);
expect(result[0].length, 2, reason: 'First style is 2 characters bold');
expect(result[0].value, const Style().put(Attribute.bold));
expect(result[1].offset, 9);
expect(result[1].length, 3, reason: 'Last style is 3 characters italic');
expect(result[1].value, const Style().put(Attribute.italic));
});
test('getPlainText', () {
controller.updateSelection(
const TextSelection(baseOffset: 0, extentOffset: 4),
ChangeSource.local);
expect(controller.getPlainText(), testDocumentContents);
});
test('getAllSelectionStyles', () {
controller.formatText(0, 2, Attribute.bold);
expect(controller.getAllSelectionStyles(),
contains(const Style().put(Attribute.bold)));
});
test('undo', () {
var listenerCalled = false;
controller.updateSelection(
const TextSelection.collapsed(offset: 4), ChangeSource.local);
expect(
controller.document.toDelta(),
Delta()..insert('data\n'),
);
controller
..addListener(() {
listenerCalled = true;
})
..undo();
expect(listenerCalled, isTrue);
expect(controller.document.toDelta(), Delta()..insert('\n'));
});
test('redo', () {
var listenerCalled = false;
controller.updateSelection(
const TextSelection.collapsed(offset: 4), ChangeSource.local);
expect(controller.document.toDelta(), Delta()..insert('data\n'));
controller.undo();
expect(controller.document.toDelta(), Delta()..insert('\n'));
controller
..addListener(() {
listenerCalled = true;
})
..redo();
expect(listenerCalled, isTrue);
expect(controller.document.toDelta(), Delta()..insert('data\n'));
});
test('clear', () {
var listenerCalled = false;
controller
..addListener(() {
listenerCalled = true;
})
..clear();
expect(listenerCalled, isTrue);
expect(controller.document.toDelta(), Delta()..insert('\n'));
});
test('replaceText', () {
var listenerCalled = false;
controller
..addListener(() {
listenerCalled = true;
})
..replaceText(1, 2, '11', const TextSelection.collapsed(offset: 0));
expect(listenerCalled, isTrue);
expect(controller.document.toDelta(), Delta()..insert('d11a\n'));
});
test('formatTextStyle', () {
var listenerCalled = false;
final style = const Style().put(Attribute.bold).put(Attribute.italic);
controller
..addListener(() {
listenerCalled = true;
})
..formatTextStyle(0, 2, style);
expect(listenerCalled, isTrue);
expect(controller.document.collectAllStyles(0, 2), contains(style));
expect(controller.document.collectAllStyles(2, 4),
everyElement(const Style()));
});
test('formatText', () {
var listenerCalled = false;
controller
..addListener(() {
listenerCalled = true;
})
..formatText(0, 2, Attribute.bold);
expect(listenerCalled, isTrue);
expect(controller.document.collectAllStyles(0, 2),
contains(const Style().put(Attribute.bold)));
expect(controller.document.collectAllStyles(2, 4),
everyElement(const Style()));
});
test('formatSelection', () {
var listenerCalled = false;
controller
..updateSelection(const TextSelection(baseOffset: 0, extentOffset: 2),
ChangeSource.local)
..addListener(() {
listenerCalled = true;
})
..formatSelection(Attribute.bold);
expect(listenerCalled, isTrue);
expect(controller.document.collectAllStyles(0, 2),
contains(const Style().put(Attribute.bold)));
expect(controller.document.collectAllStyles(2, 4),
everyElement(const Style()));
});
test('moveCursorToStart', () {
var listenerCalled = false;
controller
..updateSelection(
const TextSelection.collapsed(offset: 4), ChangeSource.local)
..addListener(() {
listenerCalled = true;
});
expect(controller.selection, const TextSelection.collapsed(offset: 4));
controller.moveCursorToStart();
expect(listenerCalled, isTrue);
expect(controller.selection, const TextSelection.collapsed(offset: 0));
});
test('moveCursorToPosition', () {
var listenerCalled = false;
controller.addListener(() {
listenerCalled = true;
});
expect(controller.selection, const TextSelection.collapsed(offset: 0));
controller.moveCursorToPosition(2);
expect(listenerCalled, isTrue);
expect(controller.selection, const TextSelection.collapsed(offset: 2));
});
test('moveCursorToEnd', () {
var listenerCalled = false;
controller.addListener(() {
listenerCalled = true;
});
expect(controller.selection, const TextSelection.collapsed(offset: 0));
controller.moveCursorToEnd();
expect(listenerCalled, isTrue);
expect(controller.selection,
TextSelection.collapsed(offset: controller.document.length - 1));
});
test('updateSelection', () {
var listenerCalled = false;
const selection = TextSelection.collapsed(offset: 0);
controller
..addListener(() {
listenerCalled = true;
})
..updateSelection(selection, ChangeSource.local);
expect(listenerCalled, isTrue);
expect(controller.selection, selection);
});
test('compose', () {
var listenerCalled = false;
final originalContents = controller.document.toPlainText();
controller
..addListener(() {
listenerCalled = true;
})
..compose(Delta()..insert('test '),
const TextSelection.collapsed(offset: 0), ChangeSource.local);
expect(listenerCalled, isTrue);
expect(controller.document.toDelta(),
Delta()..insert('test $originalContents'));
});
test('clipboardSelection empty', () {
expect(controller.clipboardSelection(true), false,
reason: 'No effect when no selection');
expect(controller.clipboardSelection(false), false);
});
test('clipboardSelection', () {
controller
..replaceText(0, 4, 'bold plain italic', null)
..formatText(0, 4, Attribute.bold)
..formatText(11, 17, Attribute.italic)
..updateSelection(const TextSelection(baseOffset: 2, extentOffset: 14),
ChangeSource.local);
//
expect(controller.clipboardSelection(true), true);
expect(controller.document.length, 18,
reason: 'Copy does not change the document');
expect(controller.clipboardSelection(false), true);
expect(controller.document.length, 6, reason: 'Cut changes the document');
//
controller
..readOnly = true
..updateSelection(const TextSelection(baseOffset: 2, extentOffset: 4),
ChangeSource.local);
expect(controller.selection.isCollapsed, false);
expect(controller.clipboardSelection(true), true);
expect(controller.document.length, 6);
expect(controller.clipboardSelection(false), false);
expect(controller.document.length, 6,
reason: 'Cut not permitted on readOnly document');
});
test('blockSelectionStyles', () {
Style select(int start, int end) {
controller.updateSelection(
TextSelection(baseOffset: start, extentOffset: end),
ChangeSource.local);
return controller.getSelectionStyle();
}
Attribute fromKey(String key) => switch (key) {
'header' => Attribute.h1,
'list' => Attribute.ol,
'align' => Attribute.centerAlignment,
'code-block' => Attribute.codeBlock,
'blockquote' => Attribute.blockQuote,
'indent' => Attribute.indentL2,
'direction' => Attribute.rtl,
'line-height' => LineHeightAttribute.lineHeightNormal,
String() => throw UnimplementedError(key)
};
for (final blockKey in Attribute.blockKeys) {
final blockAttribute = fromKey(blockKey);
controller
..clear()
..replaceText(0, 0, 'line 1\nLine 2\nLine 3', null)
..formatText(0, 0, blockAttribute) // first 2 lines
..formatText(
4, 6, Attribute.bold) // spans end of line 1 and start of line 2
..formatText(7, 0, blockAttribute);
expect(select(2, 5), const Style().put(blockAttribute),
reason: 'line 1 block, plain and bold');
expect(
select(5, 6), const Style().put(Attribute.bold).put(blockAttribute),
reason: 'line 1 block, bold');
expect(
select(4, 8), const Style().put(Attribute.bold).put(blockAttribute),
reason: 'spans line1 and 2, selection is all bold');
expect(select(4, 11), const Style().put(blockAttribute),
reason: 'selection expands into non-bold text');
expect(select(2, 11), const Style().put(blockAttribute),
reason:
'selection starts in non-bold text extends into plain on next line');
expect(select(2, 8), const Style().put(blockAttribute),
reason:
'selection starts in non-bold text, extends into bold on next line');
expect(
select(7, 8), const Style().put(Attribute.bold).put(blockAttribute),
reason: 'line 2 block, bold');
expect(select(7, 11), const Style().put(blockAttribute),
reason: 'line 2 block, selection extends into plain text');
expect(select(4, 16), const Style(),
reason: 'line 1 extends into line3 which is not block');
}
});
// TODO: Implement this, check of kIsWeb is not testable in QuillController
test('the paste event should be not null on web', () {});
});
}