test: improve test coverage (#23)

This commit is contained in:
Mathias Mogensen 2023-03-24 13:59:03 +01:00 • committed by GitHub
commit 226d39cc78
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 472 additions and 21 deletions

View file

@ -12,6 +12,7 @@ class Position {
factory Position.fromJson(Map<String, dynamic> json) { factory Position.fromJson(Map<String, dynamic> json) {
final path = Path.from(json['path'] as List); final path = Path.from(json['path'] as List);
final offset = json['offset']; final offset = json['offset'];
return Position( return Position(
path: path, path: path,
offset: offset ?? 0, offset: offset ?? 0,

View file

@ -3,14 +3,14 @@ import 'package:flutter_svg/svg.dart';
class FlowySvg extends StatelessWidget { class FlowySvg extends StatelessWidget {
const FlowySvg({ const FlowySvg({
Key? key, super.key,
this.name, this.name,
this.width, this.width,
this.height, this.height,
this.color, this.color,
this.number, this.number,
this.padding, this.padding,
}) : super(key: key); });
final String? name; final String? name;
final double? width; final double? width;

View file

@ -46,14 +46,12 @@ class ActionMenuItem {
Widget Function(Widget item)? itemWrapper, Widget Function(Widget item)? itemWrapper,
}) { }) {
return ActionMenuItem( return ActionMenuItem(
iconBuilder: ({size, color}) { iconBuilder: ({color, size}) => FlowySvg(
return FlowySvg( name: name,
name: name, color: color,
color: color, width: size,
width: size, height: size,
height: size, ),
);
},
onPressed: onPressed, onPressed: onPressed,
selected: selected, selected: selected,
itemWrapper: itemWrapper, itemWrapper: itemWrapper,
@ -62,13 +60,11 @@ class ActionMenuItem {
factory ActionMenuItem.separator() { factory ActionMenuItem.separator() {
return ActionMenuItem( return ActionMenuItem(
iconBuilder: ({size, color}) { iconBuilder: ({color, size}) => FlowySvg(
return FlowySvg( name: 'image_toolbar/divider',
name: 'image_toolbar/divider', color: color,
color: color, height: size,
height: size, ),
);
},
onPressed: null, onPressed: null,
); );
} }

View file

@ -130,9 +130,7 @@ class _LinkMenuState extends State<LinkMenu> {
width: 24, width: 24,
height: 24, height: 24,
), ),
onPressed: () { onPressed: _textEditingController.clear,
_textEditingController.clear();
},
), ),
border: const OutlineInputBorder( border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12.0)), borderRadius: BorderRadius.all(Radius.circular(12.0)),

View file

@ -1,3 +1,5 @@
import 'dart:convert';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
@ -22,5 +24,17 @@ void main() async {
'offset': 4, '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);
});
}); });
} }

View file

@ -1,3 +1,5 @@
import 'dart:convert';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter_test/flutter_test.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);
});
}); });
} }

View file

@ -75,5 +75,9 @@ void main() async {
expect(op.invert().invert(), op); expect(op.invert().invert(), op);
expect(op.copyWith(), op); expect(op.copyWith(), op);
}); });
test('Operation.fromJson throws', () {
expect(() => Operation.fromJson(), throwsA(isA<UnimplementedError>()));
});
}); });
} }

View file

@ -45,6 +45,48 @@ void main() {
expect(result, false); 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', testWidgets('insert a new checkbox after an exsiting checkbox',
(tester) async { (tester) async {
const text = 'Welcome to Appflowy 😁'; const text = 'Welcome to Appflowy 😁';

View file

@ -2,7 +2,7 @@ import 'package:appflowy_editor/src/extensions/url_launcher_extension.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
void main() { void main() {
test('safeLaunchUrl without scheme', () async { test('safeLaunchUrl without href and scheme', () async {
const href = null; const href = null;
final result = await safeLaunchUrl(href); final result = await safeLaunchUrl(href);
expect(result, false); expect(result, false);

View 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);
});
});
}

View 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);
});
});
}

View 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>""";

View file

@ -52,6 +52,17 @@ void main() async {
assert(transaction.operations[2] is UpdateOperation); assert(transaction.operations[2] is UpdateOperation);
assert(isInsertAndPathEqual(transaction.operations[3], [0], '0')); 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]) { bool isInsertAndPathEqual(Operation operation, Path path, [String? content]) {

View 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
""";

View file

@ -10,8 +10,14 @@ void main() async {
'image_src': 'https://appflowy.io', 'image_src': 'https://appflowy.io',
}, },
); );
final result = const ImageNodeParser().transform(node); final result = const ImageNodeParser().transform(node);
expect(result, '![](https://appflowy.io)'); expect(result, '![](https://appflowy.io)');
}); });
test('ImageNodeParser id getter', () {
const imageNodeParser = ImageNodeParser();
expect(imageNodeParser.id, 'image');
});
}); });
} }

View file

@ -27,9 +27,34 @@ void main() async {
BuiltInAttributeKey.heading: BuiltInAttributeKey.h3, 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(h1), '# $text');
expect(const TextNodeParser().transform(h2), '## $text'); expect(const TextNodeParser().transform(h2), '## $text');
expect(const TextNodeParser().transform(h3), '### $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', () { test('bulleted list style', () {
@ -91,5 +116,19 @@ void main() async {
); );
expect(const TextNodeParser().transform(node), '```\n$text\n```'); 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');
});
}); });
} }

View 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
View 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();
}
}