test: improve test coverage (#23)
This commit is contained in:
parent
6ea90facd1
commit
226d39cc78
18 changed files with 472 additions and 21 deletions
|
|
@ -12,6 +12,7 @@ class Position {
|
|||
factory Position.fromJson(Map<String, dynamic> json) {
|
||||
final path = Path.from(json['path'] as List);
|
||||
final offset = json['offset'];
|
||||
|
||||
return Position(
|
||||
path: path,
|
||||
offset: offset ?? 0,
|
||||
|
|
|
|||
|
|
@ -3,14 +3,14 @@ import 'package:flutter_svg/svg.dart';
|
|||
|
||||
class FlowySvg extends StatelessWidget {
|
||||
const FlowySvg({
|
||||
Key? key,
|
||||
super.key,
|
||||
this.name,
|
||||
this.width,
|
||||
this.height,
|
||||
this.color,
|
||||
this.number,
|
||||
this.padding,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
final String? name;
|
||||
final double? width;
|
||||
|
|
|
|||
|
|
@ -46,14 +46,12 @@ class ActionMenuItem {
|
|||
Widget Function(Widget item)? itemWrapper,
|
||||
}) {
|
||||
return ActionMenuItem(
|
||||
iconBuilder: ({size, color}) {
|
||||
return FlowySvg(
|
||||
name: name,
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
},
|
||||
iconBuilder: ({color, size}) => FlowySvg(
|
||||
name: name,
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
),
|
||||
onPressed: onPressed,
|
||||
selected: selected,
|
||||
itemWrapper: itemWrapper,
|
||||
|
|
@ -62,13 +60,11 @@ class ActionMenuItem {
|
|||
|
||||
factory ActionMenuItem.separator() {
|
||||
return ActionMenuItem(
|
||||
iconBuilder: ({size, color}) {
|
||||
return FlowySvg(
|
||||
name: 'image_toolbar/divider',
|
||||
color: color,
|
||||
height: size,
|
||||
);
|
||||
},
|
||||
iconBuilder: ({color, size}) => FlowySvg(
|
||||
name: 'image_toolbar/divider',
|
||||
color: color,
|
||||
height: size,
|
||||
),
|
||||
onPressed: null,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -130,9 +130,7 @@ class _LinkMenuState extends State<LinkMenu> {
|
|||
width: 24,
|
||||
height: 24,
|
||||
),
|
||||
onPressed: () {
|
||||
_textEditingController.clear();
|
||||
},
|
||||
onPressed: _textEditingController.clear,
|
||||
),
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12.0)),
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
|
|
@ -22,5 +24,17 @@ void main() async {
|
|||
'offset': 4,
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('Position.fromJson', (tester) async {
|
||||
final positionJson = jsonDecode("""{
|
||||
"path": [4],
|
||||
"offset": 5
|
||||
}""");
|
||||
|
||||
final position = Position.fromJson(positionJson);
|
||||
|
||||
expect(position.path.first, 4);
|
||||
expect(position.offset, 5);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
|
|
@ -73,5 +75,26 @@ void main() async {
|
|||
}
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('Selection.fromJson', (tester) async {
|
||||
final selectionJson = jsonDecode("""{
|
||||
"start": {
|
||||
"path": [0],
|
||||
"offset": 0
|
||||
},
|
||||
"end": {
|
||||
"path": [2],
|
||||
"offset": 25
|
||||
}
|
||||
}""");
|
||||
|
||||
final position = Selection.fromJson(selectionJson);
|
||||
|
||||
expect(position.start.path.first, 0);
|
||||
expect(position.start.offset, 0);
|
||||
|
||||
expect(position.end.path.first, 2);
|
||||
expect(position.end.offset, 25);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,5 +75,9 @@ void main() async {
|
|||
expect(op.invert().invert(), op);
|
||||
expect(op.copyWith(), op);
|
||||
});
|
||||
|
||||
test('Operation.fromJson throws', () {
|
||||
expect(() => Operation.fromJson(), throwsA(isA<UnimplementedError>()));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,48 @@ void main() {
|
|||
expect(result, false);
|
||||
});
|
||||
|
||||
test('inSelection w/ Reverse selection', () {
|
||||
final linkedList = LinkedList<Node>()
|
||||
..addAll([
|
||||
Node(
|
||||
type: 'type',
|
||||
attributes: {},
|
||||
),
|
||||
]);
|
||||
|
||||
final node = Node(
|
||||
type: 'type',
|
||||
children: linkedList,
|
||||
attributes: {},
|
||||
);
|
||||
|
||||
final reverseSelection = Selection(
|
||||
start: Position(path: [1]),
|
||||
end: Position(path: [0]),
|
||||
);
|
||||
|
||||
final result = node.inSelection(reverseSelection);
|
||||
expect(result, false);
|
||||
});
|
||||
|
||||
testWidgets('isSelected', (tester) async {
|
||||
final editor = tester.editor
|
||||
..insertTextNode('Hello')
|
||||
..insertTextNode('World');
|
||||
await editor.startTesting();
|
||||
|
||||
final selection = Selection(
|
||||
start: Position(path: [0]),
|
||||
end: Position(path: [0], offset: 1),
|
||||
);
|
||||
|
||||
await editor.updateSelection(selection);
|
||||
|
||||
final node = editor.editorState.getTextNode(path: [0]);
|
||||
|
||||
expect(node.isSelected(editor.editorState), true);
|
||||
});
|
||||
|
||||
testWidgets('insert a new checkbox after an exsiting checkbox',
|
||||
(tester) async {
|
||||
const text = 'Welcome to Appflowy 😁';
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import 'package:appflowy_editor/src/extensions/url_launcher_extension.dart';
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
test('safeLaunchUrl without scheme', () async {
|
||||
test('safeLaunchUrl without href and scheme', () async {
|
||||
const href = null;
|
||||
final result = await safeLaunchUrl(href);
|
||||
expect(result, false);
|
||||
|
|
|
|||
88
test/infra/clipboard_test.dart
Normal file
88
test/infra/clipboard_test.dart
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import 'package:appflowy_editor/src/infra/clipboard.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
class MockClipboard {
|
||||
final String? text;
|
||||
final String? html;
|
||||
|
||||
const MockClipboard({required this.text, required this.html});
|
||||
|
||||
MockClipboard copyWith({
|
||||
String? text,
|
||||
String? html,
|
||||
}) =>
|
||||
MockClipboard(
|
||||
text: text,
|
||||
html: html,
|
||||
);
|
||||
|
||||
Map<String, String?> get getData => {"html": html, "text": text};
|
||||
}
|
||||
|
||||
void main() {
|
||||
late MockClipboard mockClipboard;
|
||||
|
||||
setUp(() {
|
||||
mockClipboard = const MockClipboard(html: null, text: null);
|
||||
|
||||
SystemChannels.platform
|
||||
.setMockMethodCallHandler((MethodCall methodCall) async {
|
||||
switch (methodCall.method) {
|
||||
case "Clipboard.getData":
|
||||
return mockClipboard.getData;
|
||||
case "Clipboard.setData":
|
||||
final args = methodCall.arguments as Map<String, dynamic>;
|
||||
mockClipboard = mockClipboard.copyWith(
|
||||
text: args['text'],
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
group('Clipboard tests', () {
|
||||
test('AppFlowyClipboardData constructor', () {
|
||||
const data = AppFlowyClipboardData(text: null, html: null);
|
||||
|
||||
expect(data.text, null);
|
||||
expect(data.html, null);
|
||||
});
|
||||
|
||||
testWidgets('AppFlowyClipboard setData and getData', (tester) async {
|
||||
const rawText = "Hello World";
|
||||
|
||||
AppFlowyClipboardData? clipboardData;
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: ThemeData(platform: TargetPlatform.windows),
|
||||
home: Column(
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () async =>
|
||||
await AppFlowyClipboard.setData(text: rawText),
|
||||
child: const Text('setData'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async =>
|
||||
clipboardData = await AppFlowyClipboard.getData(),
|
||||
child: const Text('getData'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('setData'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('getData'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(clipboardData?.text, rawText);
|
||||
});
|
||||
});
|
||||
}
|
||||
43
test/infra/flowy_svg_test.dart
Normal file
43
test/infra/flowy_svg_test.dart
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import 'package:appflowy_editor/src/infra/flowy_svg.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../test_helper.dart';
|
||||
|
||||
void main() {
|
||||
group('FlowySvg tests', () {
|
||||
testWidgets('FlowySvg w/ Name', (tester) async {
|
||||
await tester.buildAndPump(
|
||||
const FlowySvg(name: 'checkmark', color: Colors.blue),
|
||||
);
|
||||
|
||||
final svgFinder = find.byType(SvgPicture);
|
||||
expect(svgFinder, findsOneWidget);
|
||||
|
||||
final svgPicture = tester.widget<SvgPicture>(svgFinder);
|
||||
expect(svgPicture.fit, BoxFit.fill);
|
||||
expect(
|
||||
svgPicture.colorFilter,
|
||||
const ColorFilter.mode(Colors.blue, BlendMode.srcIn),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('FlowySvg w/ Number', (tester) async {
|
||||
await tester.buildAndPump(
|
||||
const FlowySvg(number: 1),
|
||||
);
|
||||
|
||||
expect(find.byType(SvgPicture), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('FlowySvg null', (tester) async {
|
||||
await tester.buildAndPump(
|
||||
const FlowySvg(),
|
||||
);
|
||||
|
||||
expect(find.byType(Container), findsOneWidget);
|
||||
expect(find.byType(SvgPicture), findsNothing);
|
||||
});
|
||||
});
|
||||
}
|
||||
53
test/infra/html_converter_test.dart
Normal file
53
test/infra/html_converter_test.dart
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import 'package:appflowy_editor/src/infra/html_converter.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('HTMLConverter tests', () {
|
||||
test('HTMLToNodesConverter', () {
|
||||
final converter = HTMLToNodesConverter(rawHTML);
|
||||
final nodes = converter.toNodes();
|
||||
|
||||
expect(nodes.isNotEmpty, true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const rawHTML = """<h1>AppFlowyEditor</h1>
|
||||
<h2>👋 <strong>Welcome to</strong> <strong><em><a href="appflowy.io">AppFlowy Editor</a></em></strong></h2>
|
||||
<p>AppFlowy Editor is a <strong>highly customizable</strong> <em>rich-text editor</em></p>
|
||||
|
||||
<p>Here is an example you can give a try</p>
|
||||
|
||||
<span style="font-weight: bold;">Span element</span>
|
||||
|
||||
<span style="font-weight: medium;">Span element two</span>
|
||||
|
||||
<ul>
|
||||
<li>[x] Customizable</li>
|
||||
<li>[x] Test-covered</li>
|
||||
<li>[ ] more to come!</li>
|
||||
</ul>
|
||||
|
||||
<ol>
|
||||
<li>First item</li>
|
||||
<li>Second item</li>
|
||||
</ol>
|
||||
|
||||
<li>List element</li>
|
||||
|
||||
<blockquote>
|
||||
<p>This is a quote!</p>
|
||||
</blockquote>
|
||||
|
||||
<pre>
|
||||
<code>
|
||||
Code block
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
<b>Bold tag</b>
|
||||
<img src="http://appflowy.io" alt="AppFlowy">
|
||||
<p>You can also use <strong><em>AppFlowy Editor</em></strong> as a component to build your own app.</p>
|
||||
<h3>Awesome features</h3>
|
||||
<p>If you have questions or feedback, please submit an issue on Github or join the community along with 1000+ builders!</p>
|
||||
<hr>""";
|
||||
|
|
@ -52,6 +52,17 @@ void main() async {
|
|||
assert(transaction.operations[2] is UpdateOperation);
|
||||
assert(isInsertAndPathEqual(transaction.operations[3], [0], '0'));
|
||||
});
|
||||
|
||||
test("HistoryItem sealed", () {
|
||||
final historyItem = HistoryItem();
|
||||
|
||||
historyItem.add(
|
||||
DeleteOperation([0], [TextNode(delta: Delta()..insert('0'))]),
|
||||
);
|
||||
|
||||
historyItem.seal();
|
||||
expect(historyItem.sealed, true);
|
||||
});
|
||||
}
|
||||
|
||||
bool isInsertAndPathEqual(Operation operation, Path path, [String? content]) {
|
||||
|
|
|
|||
60
test/plugins/markdown/document_markdown_test.dart
Normal file
60
test/plugins/markdown/document_markdown_test.dart
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('document_markdown.dart tests', () {
|
||||
test('markdownToDocument()', () {
|
||||
final document = markdownToDocument(markdownDocument);
|
||||
final data = Map<String, Object>.from(json.decode(testDocument));
|
||||
|
||||
expect(document.toJson(), data);
|
||||
});
|
||||
|
||||
test('documentToMarkdown()', () {
|
||||
final document = markdownToDocument(markdownDocument);
|
||||
final markdown = documentToMarkdown(document);
|
||||
|
||||
expect(markdown, markdownDocumentEncoded);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const testDocument = '''{
|
||||
"document": {
|
||||
"type": "editor",
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": {"subtype": "heading", "heading": "h1"},
|
||||
"delta": [{"insert": "Heading 1"}]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": {"subtype": "heading", "heading": "h2"},
|
||||
"delta": [{"insert": "Heading 2"}]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": {"subtype": "heading", "heading": "h3"},
|
||||
"delta": [{"insert": "Heading 3"}]
|
||||
},
|
||||
{"type": "text", "delta": []},
|
||||
{"type": "divider"},
|
||||
{"type": "text", "delta": [{"insert": ""}]}
|
||||
]
|
||||
}
|
||||
}''';
|
||||
|
||||
const markdownDocument = """# Heading 1
|
||||
## Heading 2
|
||||
### Heading 3
|
||||
|
||||
---""";
|
||||
|
||||
const markdownDocumentEncoded = """# Heading 1
|
||||
## Heading 2
|
||||
### Heading 3
|
||||
|
||||
""";
|
||||
|
|
@ -10,8 +10,14 @@ void main() async {
|
|||
'image_src': 'https://appflowy.io',
|
||||
},
|
||||
);
|
||||
|
||||
final result = const ImageNodeParser().transform(node);
|
||||
expect(result, '');
|
||||
});
|
||||
|
||||
test('ImageNodeParser id getter', () {
|
||||
const imageNodeParser = ImageNodeParser();
|
||||
expect(imageNodeParser.id, 'image');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,9 +27,34 @@ void main() async {
|
|||
BuiltInAttributeKey.heading: BuiltInAttributeKey.h3,
|
||||
},
|
||||
);
|
||||
final h4 = TextNode(
|
||||
delta: Delta(operations: [TextInsert(text)]),
|
||||
attributes: {
|
||||
BuiltInAttributeKey.subtype: BuiltInAttributeKey.heading,
|
||||
BuiltInAttributeKey.heading: BuiltInAttributeKey.h4,
|
||||
},
|
||||
);
|
||||
final h5 = TextNode(
|
||||
delta: Delta(operations: [TextInsert(text)]),
|
||||
attributes: {
|
||||
BuiltInAttributeKey.subtype: BuiltInAttributeKey.heading,
|
||||
BuiltInAttributeKey.heading: BuiltInAttributeKey.h5,
|
||||
},
|
||||
);
|
||||
final h6 = TextNode(
|
||||
delta: Delta(operations: [TextInsert(text)]),
|
||||
attributes: {
|
||||
BuiltInAttributeKey.subtype: BuiltInAttributeKey.heading,
|
||||
BuiltInAttributeKey.heading: BuiltInAttributeKey.h6,
|
||||
},
|
||||
);
|
||||
|
||||
expect(const TextNodeParser().transform(h1), '# $text');
|
||||
expect(const TextNodeParser().transform(h2), '## $text');
|
||||
expect(const TextNodeParser().transform(h3), '### $text');
|
||||
expect(const TextNodeParser().transform(h4), '#### $text');
|
||||
expect(const TextNodeParser().transform(h5), '##### $text');
|
||||
expect(const TextNodeParser().transform(h6), '###### $text');
|
||||
});
|
||||
|
||||
test('bulleted list style', () {
|
||||
|
|
@ -91,5 +116,19 @@ void main() async {
|
|||
);
|
||||
expect(const TextNodeParser().transform(node), '```\n$text\n```');
|
||||
});
|
||||
|
||||
test('fallback', () {
|
||||
final node = TextNode(
|
||||
delta: Delta(operations: [TextInsert(text)]),
|
||||
attributes: {
|
||||
BuiltInAttributeKey.bold: true,
|
||||
},
|
||||
);
|
||||
expect(const TextNodeParser().transform(node), text);
|
||||
});
|
||||
|
||||
test('TextNodeParser.id', () {
|
||||
expect(const TextNodeParser().id, 'text');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
48
test/render/action_menu/action_menu_item_test.dart
Normal file
48
test/render/action_menu/action_menu_item_test.dart
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor/src/infra/flowy_svg.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../../test_helper.dart';
|
||||
|
||||
void main() {
|
||||
group('ActionMenuItem tests', () {
|
||||
testWidgets('ActionMenuItem.icon()', (tester) async {
|
||||
int count = 0;
|
||||
|
||||
final item = ActionMenuItem.icon(
|
||||
iconData: Icons.abc,
|
||||
onPressed: () => count++,
|
||||
);
|
||||
|
||||
await tester.buildAndPump(item.iconBuilder());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
item.onPressed!.call();
|
||||
expect(count, 1);
|
||||
|
||||
expect(find.byType(Icon), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('ActionMenuItem.svg()', (tester) async {
|
||||
final item = ActionMenuItem.svg(name: 'check', onPressed: () {});
|
||||
|
||||
await tester.buildAndPump(item.iconBuilder());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(FlowySvg), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('ActionMenuItem.separator()', (tester) async {
|
||||
final item = ActionMenuItem.separator();
|
||||
|
||||
await tester.buildAndPump(item.iconBuilder());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(FlowySvg), findsOneWidget);
|
||||
|
||||
final svg = tester.widget<FlowySvg>(find.byType(FlowySvg));
|
||||
expect(svg.name, 'image_toolbar/divider');
|
||||
});
|
||||
});
|
||||
}
|
||||
25
test/test_helper.dart
Normal file
25
test/test_helper.dart
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
extension BuildAndPump on WidgetTester {
|
||||
Future<void> buildAndPump(Widget child) async {
|
||||
await pumpWidget(
|
||||
MaterialApp(
|
||||
localizationsDelegates: const [
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
AppFlowyEditorLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: AppFlowyEditorLocalizations.delegate.supportedLocales,
|
||||
locale: const Locale('en'),
|
||||
home: Scaffold(
|
||||
body: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
await pump();
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue