소스 검색

Merge pull request #1229 from enzoftware/main

test: Improve Code Coverage for extensions files
Lucas.Xu 2 년 전
부모
커밋
06f1d52cc0

+ 8 - 6
frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/attributes_extension.dart

@@ -17,9 +17,9 @@ extension NodeAttributesExtensions on Attributes {
     return containsKey(BuiltInAttributeKey.quote);
   }
 
-  int? get number {
+  num? get number {
     if (containsKey(BuiltInAttributeKey.number) &&
-        this[BuiltInAttributeKey.number] is int) {
+        this[BuiltInAttributeKey.number] is num) {
       return this[BuiltInAttributeKey.number];
     }
     return null;
@@ -27,7 +27,7 @@ extension NodeAttributesExtensions on Attributes {
 
   bool get code {
     if (containsKey(BuiltInAttributeKey.code) &&
-        this[BuiltInAttributeKey.code] == true) {
+        this[BuiltInAttributeKey.code] is bool) {
       return this[BuiltInAttributeKey.code];
     }
     return false;
@@ -63,11 +63,14 @@ extension DeltaAttributesExtensions on Attributes {
         this[BuiltInAttributeKey.strikethrough] == true);
   }
 
+  static const whiteInt = 0XFFFFFFFF;
+
   Color? get color {
     if (containsKey(BuiltInAttributeKey.color) &&
         this[BuiltInAttributeKey.color] is String) {
       return Color(
-        int.parse(this[BuiltInAttributeKey.color]),
+        // If the parse fails returns white by default
+        int.tryParse(this[BuiltInAttributeKey.color]) ?? whiteInt,
       );
     }
     return null;
@@ -77,8 +80,7 @@ extension DeltaAttributesExtensions on Attributes {
     if (containsKey(BuiltInAttributeKey.backgroundColor) &&
         this[BuiltInAttributeKey.backgroundColor] is String) {
       return Color(
-        int.parse(this[BuiltInAttributeKey.backgroundColor]),
-      );
+          int.tryParse(this[BuiltInAttributeKey.backgroundColor]) ?? whiteInt);
     }
     return null;
   }

+ 4 - 3
frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart

@@ -6,7 +6,7 @@ import 'package:appflowy_editor/src/document/text_delta.dart';
 import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
 
 extension TextNodeExtension on TextNode {
-  dynamic getAttributeInSelection(Selection selection, String styleKey) {
+  T? getAttributeInSelection<T>(Selection selection, String styleKey) {
     final ops = delta.whereType<TextInsert>();
     final startOffset =
         selection.isBackward ? selection.start.offset : selection.end.offset;
@@ -19,8 +19,9 @@ extension TextNodeExtension on TextNode {
       }
       final length = op.length;
       if (start < endOffset && start + length > startOffset) {
-        if (op.attributes?.containsKey(styleKey) == true) {
-          return op.attributes![styleKey];
+        final attributes = op.attributes;
+        if (attributes != null && attributes[styleKey] is T?) {
+          return attributes[styleKey];
         }
       }
       start += length;

+ 4 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_all.dart

@@ -26,6 +26,7 @@ import 'messages_hu-HU.dart' as messages_hu_hu;
 import 'messages_id-ID.dart' as messages_id_id;
 import 'messages_it-IT.dart' as messages_it_it;
 import 'messages_ja-JP.dart' as messages_ja_jp;
+import 'messages_ml_IN.dart' as messages_ml_in;
 import 'messages_nl-NL.dart' as messages_nl_nl;
 import 'messages_pl-PL.dart' as messages_pl_pl;
 import 'messages_pt-BR.dart' as messages_pt_br;
@@ -48,6 +49,7 @@ Map<String, LibraryLoader> _deferredLibraries = {
   'id_ID': () => new Future.value(null),
   'it_IT': () => new Future.value(null),
   'ja_JP': () => new Future.value(null),
+  'ml_IN': () => new Future.value(null),
   'nl_NL': () => new Future.value(null),
   'pl_PL': () => new Future.value(null),
   'pt_BR': () => new Future.value(null),
@@ -82,6 +84,8 @@ MessageLookupByLibrary? _findExact(String localeName) {
       return messages_it_it.messages;
     case 'ja_JP':
       return messages_ja_jp.messages;
+    case 'ml_IN':
+      return messages_ml_in.messages;
     case 'nl_NL':
       return messages_nl_nl.messages;
     case 'pl_PL':

+ 45 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_ml_IN.dart

@@ -0,0 +1,45 @@
+// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
+// This is a library that provides messages for a ml_IN locale. All the
+// messages from the main program should be duplicated here with the same
+// function name.
+
+// Ignore issues from commonly used lints in this file.
+// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
+// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
+// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
+// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes
+// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes
+
+import 'package:intl/intl.dart';
+import 'package:intl/message_lookup_by_library.dart';
+
+final messages = new MessageLookup();
+
+typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
+
+class MessageLookup extends MessageLookupByLibrary {
+  String get localeName => 'ml_IN';
+
+  final messages = _notInlinedMessages(_notInlinedMessages);
+  static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
+        "bold": MessageLookupByLibrary.simpleMessage("ബോൾഡ്"),
+        "bulletedList":
+            MessageLookupByLibrary.simpleMessage("ബുള്ളറ്റഡ് പട്ടിക"),
+        "checkbox": MessageLookupByLibrary.simpleMessage("ചെക്ക്ബോക്സ്"),
+        "embedCode": MessageLookupByLibrary.simpleMessage("എംബെഡഡ് കോഡ്"),
+        "heading1": MessageLookupByLibrary.simpleMessage("തലക്കെട്ട് 1"),
+        "heading2": MessageLookupByLibrary.simpleMessage("തലക്കെട്ട് 2"),
+        "heading3": MessageLookupByLibrary.simpleMessage("തലക്കെട്ട് 3"),
+        "highlight":
+            MessageLookupByLibrary.simpleMessage("പ്രമുഖമാക്കിക്കാട്ടുക"),
+        "image": MessageLookupByLibrary.simpleMessage("ചിത്രം"),
+        "italic": MessageLookupByLibrary.simpleMessage("ഇറ്റാലിക്"),
+        "link": MessageLookupByLibrary.simpleMessage("ലിങ്ക്"),
+        "numberedList":
+            MessageLookupByLibrary.simpleMessage("അക്കമിട്ട പട്ടിക"),
+        "quote": MessageLookupByLibrary.simpleMessage("ഉദ്ധരണി"),
+        "strikethrough": MessageLookupByLibrary.simpleMessage("സ്ട്രൈക്ക്ത്രൂ"),
+        "text": MessageLookupByLibrary.simpleMessage("വചനം"),
+        "underline": MessageLookupByLibrary.simpleMessage("അടിവരയിടുക")
+      };
+}

+ 1 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/l10n.dart

@@ -229,6 +229,7 @@ class AppLocalizationDelegate
       Locale.fromSubtags(languageCode: 'id', countryCode: 'ID'),
       Locale.fromSubtags(languageCode: 'it', countryCode: 'IT'),
       Locale.fromSubtags(languageCode: 'ja', countryCode: 'JP'),
+      Locale.fromSubtags(languageCode: 'ml', countryCode: 'IN'),
       Locale.fromSubtags(languageCode: 'nl', countryCode: 'NL'),
       Locale.fromSubtags(languageCode: 'pl', countryCode: 'PL'),
       Locale.fromSubtags(languageCode: 'pt', countryCode: 'BR'),

+ 4 - 2
frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart

@@ -333,8 +333,10 @@ void showLinkMenu(
   final textNode = node.first as TextNode;
   String? linkText;
   if (textNode.allSatisfyLinkInSelection(selection)) {
-    linkText =
-        textNode.getAttributeInSelection(selection, BuiltInAttributeKey.href);
+    linkText = textNode.getAttributeInSelection<String>(
+      selection,
+      BuiltInAttributeKey.href,
+    );
   }
   _linkMenuOverlay = OverlayEntry(builder: (context) {
     return Positioned(

+ 1 - 0
frontend/app_flowy/packages/appflowy_editor/pubspec.yaml

@@ -33,6 +33,7 @@ dev_dependencies:
     sdk: flutter
   flutter_lints: ^2.0.1
   network_image_mock: ^2.1.1
+  mockito: ^5.3.2
 
 # For information on the generic Dart part of this file, see the
 # following page: https://dart.dev/tools/pub/pubspec

+ 201 - 0
frontend/app_flowy/packages/appflowy_editor/test/extensions/attributes_extension_test.dart

@@ -0,0 +1,201 @@
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+  group('NodeAttributesExtensions::', () {
+    test('heading', () {
+      final Attributes attribute = {
+        'subtype': 'heading',
+        'heading': 'AppFlowy',
+      };
+      expect(attribute.heading, 'AppFlowy');
+    });
+
+    test('heading - text is not String return null', () {
+      final Attributes attribute = {
+        'subtype': 'heading',
+        'heading': 123,
+      };
+      expect(attribute.heading, null);
+    });
+
+    test('heading - subtype is not "heading" return null', () {
+      final Attributes attribute = {
+        'subtype': 'code',
+        'heading': 'Hello World!',
+      };
+      expect(attribute.heading, null);
+    });
+
+    test('quote', () {
+      final Attributes attribute = {
+        'quote': 'quote text',
+      };
+      expect(attribute.quote, true);
+    });
+
+    test('number - int', () {
+      final Attributes attribute = {
+        'number': 99,
+      };
+      expect(attribute.number, 99);
+    });
+
+    test('number - double', () {
+      final Attributes attribute = {
+        'number': 12.34,
+      };
+      expect(attribute.number, 12.34);
+    });
+
+    test('number - return null', () {
+      final Attributes attribute = {
+        'code': 12.34,
+      };
+      expect(attribute.number, null);
+    });
+
+    test('code', () {
+      final Attributes attribute = {
+        'code': true,
+      };
+      expect(attribute.code, true);
+    });
+
+    test('code - return false', () {
+      final Attributes attribute = {
+        'quote': true,
+      };
+      expect(attribute.code, false);
+    });
+
+    test('check', () {
+      final Attributes attribute = {
+        'checkbox': true,
+      };
+      expect(attribute.check, true);
+    });
+
+    test('check - return false', () {
+      final Attributes attribute = {
+        'quote': true,
+      };
+      expect(attribute.check, false);
+    });
+  });
+
+  group('DeltaAttributesExtensions::', () {
+    test('bold', () {
+      final Attributes attribute = {
+        'bold': true,
+      };
+      expect(attribute.bold, true);
+    });
+
+    test('bold - return false', () {
+      final Attributes attribute = {
+        'bold': 123,
+      };
+      expect(attribute.bold, false);
+    });
+
+    test('italic', () {
+      final Attributes attribute = {
+        'italic': true,
+      };
+      expect(attribute.italic, true);
+    });
+
+    test('italic - return false', () {
+      final Attributes attribute = {
+        'italic': 123,
+      };
+      expect(attribute.italic, false);
+    });
+
+    test('underline', () {
+      final Attributes attribute = {
+        'underline': true,
+      };
+      expect(attribute.underline, true);
+    });
+
+    test('underline - return false', () {
+      final Attributes attribute = {
+        'underline': 123,
+      };
+      expect(attribute.underline, false);
+    });
+
+    test('strikethrough', () {
+      final Attributes attribute = {
+        'strikethrough': true,
+      };
+      expect(attribute.strikethrough, true);
+    });
+
+    test('strikethrough - return false', () {
+      final Attributes attribute = {
+        'strikethrough': 123,
+      };
+      expect(attribute.strikethrough, false);
+    });
+
+    test('color', () {
+      final Attributes attribute = {
+        'color': '0xff212fff',
+      };
+      expect(attribute.color, const Color(0XFF212FFF));
+    });
+
+    test('color - return null', () {
+      final Attributes attribute = {
+        'color': 123,
+      };
+      expect(attribute.color, null);
+    });
+
+    test('color - parse failure return white', () {
+      final Attributes attribute = {
+        'color': 'hello123',
+      };
+      expect(attribute.color, const Color(0XFFFFFFFF));
+    });
+
+    test('backgroundColor', () {
+      final Attributes attribute = {
+        'backgroundColor': '0xff678fff',
+      };
+      expect(attribute.backgroundColor, const Color(0XFF678FFF));
+    });
+
+    test('backgroundColor - return null', () {
+      final Attributes attribute = {
+        'backgroundColor': 123,
+      };
+      expect(attribute.backgroundColor, null);
+    });
+
+    test('backgroundColor - parse failure return white', () {
+      final Attributes attribute = {
+        'backgroundColor': 'hello123',
+      };
+      expect(attribute.backgroundColor, const Color(0XFFFFFFFF));
+    });
+
+    test('href', () {
+      final Attributes attribute = {
+        'href': '/app/flowy',
+      };
+      expect(attribute.href, '/app/flowy');
+    });
+
+    test('href - return null', () {
+      final Attributes attribute = {
+        'href': 123,
+      };
+      expect(attribute.href, null);
+    });
+  });
+}

+ 40 - 0
frontend/app_flowy/packages/appflowy_editor/test/extensions/color_extension_test.dart

@@ -0,0 +1,40 @@
+import 'package:appflowy_editor/src/extensions/color_extension.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+  group('ColorExtension::', () {
+    const white = Color(0XFFFFFFFF);
+    const black = Color(0XFF000000);
+    const blue = Color(0XFF000FFF);
+    const blueRgba = 'rgba(0, 15, 255, 255)';
+    test('ToRgbaString', () {
+      expect(blue.toRgbaString(), 'rgba(0, 15, 255, 255)');
+      expect(white.toRgbaString(), 'rgba(255, 255, 255, 255)');
+      expect(black.toRgbaString(), 'rgba(0, 0, 0, 255)');
+    });
+
+    test('tryFromRgbaString', () {
+      final color = ColorExtension.tryFromRgbaString(blueRgba);
+      expect(color, const Color.fromARGB(255, 0, 15, 255));
+    });
+
+    test('tryFromRgbaString - wrong rgba format return null', () {
+      const wrongRgba = 'abc(1,2,3,4)';
+      final color = ColorExtension.tryFromRgbaString(wrongRgba);
+      expect(color, null);
+    });
+
+    test('tryFromRgbaString - wrong length return null', () {
+      const wrongRgba = 'rgba(0, 15, 255)';
+      final color = ColorExtension.tryFromRgbaString(wrongRgba);
+      expect(color, null);
+    });
+
+    test('tryFromRgbaString - wrong values return null', () {
+      const wrongRgba = 'rgba(-12, 999, 1234, 619)';
+      final color = ColorExtension.tryFromRgbaString(wrongRgba);
+      expect(color, null);
+    });
+  });
+}

+ 57 - 0
frontend/app_flowy/packages/appflowy_editor/test/extensions/node_extension_test.dart

@@ -0,0 +1,57 @@
+import 'dart:collection';
+import 'dart:ui';
+
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:mockito/mockito.dart';
+import 'package:appflowy_editor/src/extensions/node_extensions.dart';
+
+class MockNode extends Mock implements Node {}
+
+void main() {
+  final mockNode = MockNode();
+
+  group('NodeExtensions::', () {
+    final selection = Selection(
+      start: Position(path: [0]),
+      end: Position(path: [1]),
+    );
+
+    test('rect - renderBox is null', () {
+      when(mockNode.renderBox).thenReturn(null);
+      final result = mockNode.rect;
+      expect(result, Rect.zero);
+    });
+
+    test('inSelection', () {
+      // I use an empty implementation instead of mock, because the mocked
+      // version throws error trying to access the path.
+
+      final subLinkedList = LinkedList<Node>()
+        ..addAll([
+          Node(type: 'type', children: LinkedList(), attributes: {}),
+          Node(type: 'type', children: LinkedList(), attributes: {}),
+          Node(type: 'type', children: LinkedList(), attributes: {}),
+          Node(type: 'type', children: LinkedList(), attributes: {}),
+          Node(type: 'type', children: LinkedList(), attributes: {}),
+        ]);
+
+      final linkedList = LinkedList<Node>()
+        ..addAll([
+          Node(
+            type: 'type',
+            children: subLinkedList,
+            attributes: {},
+          ),
+        ]);
+
+      final node = Node(
+        type: 'type',
+        children: linkedList,
+        attributes: {},
+      );
+      final result = node.inSelection(selection);
+      expect(result, false);
+    });
+  });
+}

+ 20 - 0
frontend/app_flowy/packages/appflowy_editor/test/extensions/object_extension_test.dart

@@ -0,0 +1,20 @@
+import 'dart:io';
+
+import 'package:flutter/gestures.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:appflowy_editor/src/extensions/object_extensions.dart';
+
+void main() {
+  group('FlowyObjectExtensions::', () {
+    test('unwrapOrNull', () {
+      final result = const TextSpan().unwrapOrNull<HitTestTarget>();
+      assert(result is TextSpan);
+    });
+
+    test('unwrapOrNull - return null', () {
+      final result = const TextSpan().unwrapOrNull<ServerSocket>();
+      expect(result, null);
+    });
+  });
+}

+ 7 - 0
frontend/app_flowy/packages/appflowy_editor/test/extensions/text_node_extensions_test.dart

@@ -0,0 +1,7 @@
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+  group('TextNodeExtension::', () {
+    test('description', () {});
+  });
+}

+ 43 - 0
frontend/app_flowy/packages/appflowy_editor/test/extensions/text_style_extension_test.dart

@@ -0,0 +1,43 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:appflowy_editor/src/extensions/text_style_extension.dart';
+
+void main() {
+  group('TextStyleExtensions::', () {
+    const style = TextStyle(
+      color: Colors.blue,
+      backgroundColor: Colors.white,
+      fontSize: 14,
+      height: 100,
+      wordSpacing: 2,
+      fontWeight: FontWeight.w700,
+    );
+
+    const otherStyle = TextStyle(
+      color: Colors.red,
+      backgroundColor: Colors.black,
+      fontSize: 12,
+      height: 10,
+      wordSpacing: 1,
+    );
+    test('combine', () {
+      final result = style.combine(otherStyle);
+      expect(result.color, Colors.red);
+      expect(result.backgroundColor, Colors.black);
+      expect(result.fontSize, 12);
+      expect(result.height, 10);
+      expect(result.wordSpacing, 1);
+    });
+
+    test('combine - return this', () {
+      final result = style.combine(null);
+      expect(result, style);
+    });
+
+    test('combine - return null with inherit', () {
+      final styleCopy = otherStyle.copyWith(inherit: false);
+      final result = style.combine(styleCopy);
+      expect(result, styleCopy);
+    });
+  });
+}

+ 10 - 0
frontend/app_flowy/packages/appflowy_editor/test/extensions/url_launcher_extension_test.dart

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