Преглед на файлове

test: add more test cases for image

Lucas.Xu преди 2 години
родител
ревизия
b52f618b1a

+ 3 - 2
frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart

@@ -45,6 +45,7 @@ class _MyHomePageState extends State<MyHomePage> {
   @override
   Widget build(BuildContext context) {
     return Scaffold(
+      extendBodyBehindAppBar: true,
       body: Container(
         alignment: Alignment.topCenter,
         child: _buildEditor(context),
@@ -92,8 +93,8 @@ class _MyHomePageState extends State<MyHomePage> {
             ..handler = (message) {
               debugPrint(message);
             };
-          return Container(
-            padding: const EdgeInsets.all(20),
+          return SizedBox(
+            width: MediaQuery.of(context).size.width,
             child: AppFlowyEditor(
               editorState: _editorState,
             ),

+ 1 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_builder.dart

@@ -17,6 +17,7 @@ class ImageNodeBuilder extends NodeWidgetBuilder<Node> {
     }
     return ImageNodeWidget(
       key: context.node.key,
+      node: context.node,
       src: src,
       width: width,
       alignment: _textToAlignment(align),

+ 48 - 2
frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart

@@ -1,10 +1,17 @@
+import 'dart:math';
+
+import 'package:appflowy_editor/src/document/node.dart';
+import 'package:appflowy_editor/src/document/position.dart';
+import 'package:appflowy_editor/src/document/selection.dart';
 import 'package:appflowy_editor/src/infra/flowy_svg.dart';
 import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
+import 'package:appflowy_editor/src/render/selection/selectable.dart';
 import 'package:flutter/material.dart';
 
 class ImageNodeWidget extends StatefulWidget {
   const ImageNodeWidget({
     Key? key,
+    required this.node,
     required this.src,
     this.width,
     required this.alignment,
@@ -14,6 +21,7 @@ class ImageNodeWidget extends StatefulWidget {
     required this.onResize,
   }) : super(key: key);
 
+  final Node node;
   final String src;
   final double? width;
   final Alignment alignment;
@@ -26,7 +34,7 @@ class ImageNodeWidget extends StatefulWidget {
   State<ImageNodeWidget> createState() => _ImageNodeWidgetState();
 }
 
-class _ImageNodeWidgetState extends State<ImageNodeWidget> {
+class _ImageNodeWidgetState extends State<ImageNodeWidget> with Selectable {
   double? _imageWidth;
   double _initial = 0;
   double _distance = 0;
@@ -42,7 +50,8 @@ class _ImageNodeWidgetState extends State<ImageNodeWidget> {
     _imageWidth = widget.width;
     _imageStreamListener = ImageStreamListener(
       (image, _) {
-        _imageWidth = image.image.width.toDouble();
+        _imageWidth =
+            min(defaultMaxTextNodeWidth, image.image.width.toDouble());
       },
     );
   }
@@ -64,6 +73,43 @@ class _ImageNodeWidgetState extends State<ImageNodeWidget> {
     );
   }
 
+  @override
+  Position start() {
+    return Position(path: widget.node.path, offset: 0);
+  }
+
+  @override
+  Position end() {
+    return Position(path: widget.node.path, offset: 1);
+  }
+
+  @override
+  Position getPositionInOffset(Offset start) {
+    return end();
+  }
+
+  @override
+  Rect? getCursorRectInPosition(Position position) {
+    return null;
+  }
+
+  @override
+  List<Rect> getRectsInSelection(Selection selection) {
+    final renderBox = context.findRenderObject() as RenderBox;
+    return [Offset.zero & renderBox.size];
+  }
+
+  @override
+  Selection getSelectionInRange(Offset start, Offset end) {
+    return Selection(start: this.start(), end: this.end());
+  }
+
+  @override
+  Offset localToGlobal(Offset offset) {
+    final renderBox = context.findRenderObject() as RenderBox;
+    return renderBox.localToGlobal(offset);
+  }
+
   Widget _buildNetworkImage(BuildContext context) {
     return Align(
       alignment: widget.alignment,

+ 12 - 4
frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart → frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart

@@ -11,10 +11,16 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) {
   var nodes = editorState.service.selectionService.currentSelectedNodes;
   nodes = selection.isBackward ? nodes : nodes.reversed.toList(growable: false);
   selection = selection.isBackward ? selection : selection.reversed;
-  // make sure all nodes is [TextNode].
   final textNodes = nodes.whereType<TextNode>().toList();
+  final nonTextNodes =
+      nodes.where((node) => node is! TextNode).toList(growable: false);
 
   final transactionBuilder = TransactionBuilder(editorState);
+
+  if (nonTextNodes.isNotEmpty) {
+    transactionBuilder.deleteNodes(nonTextNodes);
+  }
+
   if (textNodes.length == 1) {
     final textNode = textNodes.first;
     final index = textNode.delta.prevRunePosition(selection.start.offset);
@@ -68,7 +74,9 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) {
       }
     }
   } else {
-    _deleteNodes(transactionBuilder, textNodes, selection);
+    if (textNodes.isNotEmpty) {
+      _deleteTextNodes(transactionBuilder, textNodes, selection);
+    }
   }
 
   if (transactionBuilder.operations.isNotEmpty) {
@@ -121,7 +129,7 @@ KeyEventResult _handleDelete(EditorState editorState, RawKeyEvent event) {
       }
     }
   } else {
-    _deleteNodes(transactionBuilder, textNodes, selection);
+    _deleteTextNodes(transactionBuilder, textNodes, selection);
   }
 
   transactionBuilder.commit();
@@ -129,7 +137,7 @@ KeyEventResult _handleDelete(EditorState editorState, RawKeyEvent event) {
   return KeyEventResult.handled;
 }
 
-void _deleteNodes(TransactionBuilder transactionBuilder,
+void _deleteTextNodes(TransactionBuilder transactionBuilder,
     List<TextNode> textNodes, Selection selection) {
   final first = textNodes.first;
   final last = textNodes.last;

+ 1 - 1
frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/default_key_event_handlers.dart

@@ -1,6 +1,6 @@
 import 'package:appflowy_editor/src/service/internal_key_event_handlers/arrow_keys_handler.dart';
 import 'package:appflowy_editor/src/service/internal_key_event_handlers/copy_paste_handler.dart';
-import 'package:appflowy_editor/src/service/internal_key_event_handlers/delete_text_handler.dart';
+import 'package:appflowy_editor/src/service/internal_key_event_handlers/backspace_handler.dart';
 import 'package:appflowy_editor/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart';
 import 'package:appflowy_editor/src/service/internal_key_event_handlers/redo_undo_handler.dart';
 import 'package:appflowy_editor/src/service/internal_key_event_handlers/slash_handler.dart';

+ 1 - 1
frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart

@@ -90,7 +90,7 @@ class _FlowyToolbarState extends State<FlowyToolbar>
         .where((item) => item.validator(widget.editorState))
         .toList(growable: false)
       ..sort((a, b) => a.type.compareTo(b.type));
-    if (items.isEmpty) {
+    if (filterItems.isEmpty) {
       return [];
     }
     final List<ToolbarItem> dividedItems = [filterItems.first];

+ 1 - 1
frontend/app_flowy/packages/appflowy_editor/test/infra/test_editor.dart

@@ -80,7 +80,7 @@ class EditorWidgetTester {
     } else {
       _editorState.service.selectionService.updateSelection(selection);
     }
-    await tester.pumpAndSettle();
+    await tester.pump(const Duration(milliseconds: 200));
 
     expect(_editorState.service.selectionService.currentSelection.value,
         selection);

+ 11 - 0
frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_widget_test.dart

@@ -1,3 +1,6 @@
+import 'dart:collection';
+
+import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:appflowy_editor/src/render/image/image_node_widget.dart';
 import 'package:flutter/gestures.dart';
 import 'package:flutter/material.dart';
@@ -20,6 +23,14 @@ void main() async {
 
         final widget = ImageNodeWidget(
           src: src,
+          node: Node(
+            type: 'image',
+            children: LinkedList(),
+            attributes: {
+              'image_src': src,
+              'align': 'center',
+            },
+          ),
           alignment: Alignment.center,
           onCopy: () {
             onCopyHit = true;

+ 74 - 1
frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/delete_text_handler_test.dart → frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/backspace_handler_test.dart

@@ -1,7 +1,9 @@
 import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:appflowy_editor/src/render/image/image_node_widget.dart';
 import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter_test/flutter_test.dart';
+import 'package:network_image_mock/network_image_mock.dart';
 import '../../infra/test_editor.dart';
 
 void main() async {
@@ -9,7 +11,7 @@ void main() async {
     TestWidgetsFlutterBinding.ensureInitialized();
   });
 
-  group('delete_text_handler.dart', () {
+  group('backspace_handler.dart', () {
     testWidgets('Presses backspace key in empty document', (tester) async {
       // Before
       //
@@ -167,6 +169,77 @@ void main() async {
   testWidgets('Presses delete key in styled text (quote)', (tester) async {
     await _deleteStyledTextByDelete(tester, StyleKey.quote);
   });
+
+  // Before
+  //
+  // Welcome to Appflowy 😁
+  // Welcome to Appflowy 😁
+  // [Image]
+  // Welcome to Appflowy 😁
+  // Welcome to Appflowy 😁
+  //
+  // After
+  //
+  // Welcome to Appflowy 😁
+  // Welcome to Appflowy 😁
+  //
+  testWidgets('Deletes the image surrounded by text', (tester) async {
+    mockNetworkImagesFor(() async {
+      const text = 'Welcome to Appflowy 😁';
+      const src = 'https://s1.ax1x.com/2022/08/26/v2sSbR.jpg';
+      final editor = tester.editor
+        ..insertTextNode(text)
+        ..insertTextNode(text)
+        ..insertImageNode(src)
+        ..insertTextNode(text)
+        ..insertTextNode(text);
+      await editor.startTesting();
+
+      expect(editor.documentLength, 5);
+      expect(find.byType(ImageNodeWidget), findsOneWidget);
+
+      await editor.updateSelection(
+        Selection(
+          start: Position(path: [1], offset: 0),
+          end: Position(path: [3], offset: text.length),
+        ),
+      );
+
+      await editor.pressLogicKey(LogicalKeyboardKey.backspace);
+      expect(editor.documentLength, 3);
+      expect(find.byType(ImageNodeWidget), findsNothing);
+      expect(
+        editor.documentSelection,
+        Selection.single(path: [1], startOffset: 0),
+      );
+    });
+  });
+
+  testWidgets('Deletes the first image', (tester) async {
+    mockNetworkImagesFor(() async {
+      const text = 'Welcome to Appflowy 😁';
+      const src = 'https://s1.ax1x.com/2022/08/26/v2sSbR.jpg';
+      final editor = tester.editor
+        ..insertImageNode(src)
+        ..insertTextNode(text)
+        ..insertTextNode(text);
+      await editor.startTesting();
+
+      expect(editor.documentLength, 3);
+      expect(find.byType(ImageNodeWidget), findsOneWidget);
+
+      await editor.updateSelection(
+        Selection(
+          start: Position(path: [0], offset: 0),
+          end: Position(path: [0], offset: 1),
+        ),
+      );
+
+      await editor.pressLogicKey(LogicalKeyboardKey.backspace);
+      expect(editor.documentLength, 2);
+      expect(find.byType(ImageNodeWidget), findsNothing);
+    });
+  });
 }
 
 Future<void> _deleteStyledTextByBackspace(