Browse Source

test: operation.dart

Lucas.Xu 2 năm trước cách đây
mục cha
commit
b5e9bf6ee3

+ 24 - 1
frontend/app_flowy/packages/appflowy_editor/lib/src/core/document/node.dart

@@ -4,8 +4,8 @@ import 'package:flutter/material.dart';
 
 import 'package:appflowy_editor/src/core/document/attributes.dart';
 import 'package:appflowy_editor/src/core/document/path.dart';
-import 'package:appflowy_editor/src/core/legacy/built_in_attribute_keys.dart';
 import 'package:appflowy_editor/src/core/document/text_delta.dart';
+import 'package:appflowy_editor/src/core/legacy/built_in_attribute_keys.dart';
 
 class Node extends ChangeNotifier with LinkedListEntry<Node> {
   Node({
@@ -276,3 +276,26 @@ class TextNode extends Node {
 
   String toPlainText() => _delta.toPlainText();
 }
+
+extension NodeEquality on Iterable<Node> {
+  bool equals(Iterable<Node> other) {
+    if (length != other.length) {
+      return false;
+    }
+    for (var i = 0; i < length; i++) {
+      if (!_nodeEquals(elementAt(i), other.elementAt(i))) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  bool _nodeEquals<T, U>(T base, U other) {
+    if (identical(this, other)) return true;
+
+    return base is Node &&
+        other is Node &&
+        other.type == base.type &&
+        other.children.equals(base.children);
+  }
+}

+ 62 - 5
frontend/app_flowy/packages/appflowy_editor/lib/src/core/transform/operation.dart

@@ -1,3 +1,5 @@
+import 'package:flutter/foundation.dart';
+
 import 'package:appflowy_editor/src/core/document/attributes.dart';
 import 'package:appflowy_editor/src/core/document/node.dart';
 import 'package:appflowy_editor/src/core/document/path.dart';
@@ -33,7 +35,9 @@ class InsertOperation extends Operation {
 
   factory InsertOperation.fromJson(Map<String, dynamic> json) {
     final path = json['path'] as Path;
-    final nodes = (json['nodes'] as List).map((n) => Node.fromJson(n));
+    final nodes = (json['nodes'] as List)
+        .map((n) => Node.fromJson(n))
+        .toList(growable: false);
     return InsertOperation(path, nodes);
   }
 
@@ -47,7 +51,7 @@ class InsertOperation extends Operation {
     return {
       'op': 'insert',
       'path': path,
-      'nodes': nodes.map((n) => n.toJson()),
+      'nodes': nodes.map((n) => n.toJson()).toList(growable: false),
     };
   }
 
@@ -55,6 +59,18 @@ class InsertOperation extends Operation {
   Operation copyWith({Path? path}) {
     return InsertOperation(path ?? this.path, nodes);
   }
+
+  @override
+  bool operator ==(Object other) {
+    if (identical(this, other)) return true;
+
+    return other is InsertOperation &&
+        other.path.equals(path) &&
+        other.nodes.equals(nodes);
+  }
+
+  @override
+  int get hashCode => path.hashCode ^ Object.hashAll(nodes);
 }
 
 /// [DeleteOperation] represents a delete operation.
@@ -66,7 +82,9 @@ class DeleteOperation extends Operation {
 
   factory DeleteOperation.fromJson(Map<String, dynamic> json) {
     final path = json['path'] as Path;
-    final nodes = (json['nodes'] as List).map((n) => Node.fromJson(n));
+    final nodes = (json['nodes'] as List)
+        .map((n) => Node.fromJson(n))
+        .toList(growable: false);
     return DeleteOperation(path, nodes);
   }
 
@@ -80,7 +98,7 @@ class DeleteOperation extends Operation {
     return {
       'op': 'delete',
       'path': path,
-      'nodes': nodes.map((n) => n.toJson()),
+      'nodes': nodes.map((n) => n.toJson()).toList(growable: false),
     };
   }
 
@@ -88,6 +106,18 @@ class DeleteOperation extends Operation {
   Operation copyWith({Path? path}) {
     return DeleteOperation(path ?? this.path, nodes);
   }
+
+  @override
+  bool operator ==(Object other) {
+    if (identical(this, other)) return true;
+
+    return other is DeleteOperation &&
+        other.path.equals(path) &&
+        other.nodes.equals(nodes);
+  }
+
+  @override
+  int get hashCode => path.hashCode ^ Object.hashAll(nodes);
 }
 
 /// [UpdateOperation] represents an attributes update operation.
@@ -137,6 +167,20 @@ class UpdateOperation extends Operation {
       {...oldAttributes},
     );
   }
+
+  @override
+  bool operator ==(Object other) {
+    if (identical(this, other)) return true;
+
+    return other is UpdateOperation &&
+        other.path.equals(path) &&
+        mapEquals(other.attributes, attributes) &&
+        mapEquals(other.oldAttributes, oldAttributes);
+  }
+
+  @override
+  int get hashCode =>
+      path.hashCode ^ attributes.hashCode ^ oldAttributes.hashCode;
 }
 
 /// [UpdateTextOperation] represents a text update operation.
@@ -150,7 +194,7 @@ class UpdateTextOperation extends Operation {
   factory UpdateTextOperation.fromJson(Map<String, dynamic> json) {
     final path = json['path'] as Path;
     final delta = Delta.fromJson(json['delta']);
-    final inverted = Delta.fromJson(json['invert']);
+    final inverted = Delta.fromJson(json['inverted']);
     return UpdateTextOperation(path, delta, inverted);
   }
 
@@ -174,6 +218,19 @@ class UpdateTextOperation extends Operation {
   Operation copyWith({Path? path}) {
     return UpdateTextOperation(path ?? this.path, delta, inverted);
   }
+
+  @override
+  bool operator ==(Object other) {
+    if (identical(this, other)) return true;
+
+    return other is UpdateTextOperation &&
+        other.path.equals(path) &&
+        other.delta == delta &&
+        other.inverted == inverted;
+  }
+
+  @override
+  int get hashCode => delta.hashCode ^ inverted.hashCode;
 }
 
 // TODO(Lucas.Xu): refactor this part

+ 79 - 0
frontend/app_flowy/packages/appflowy_editor/test/core/transform/operation_test.dart

@@ -0,0 +1,79 @@
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() async {
+  group('operation.dart', () {
+    test('test insert operation', () {
+      final node = Node(type: 'example');
+      final op = InsertOperation([0], [node]);
+      final json = op.toJson();
+      expect(json, {
+        'op': 'insert',
+        'path': [0],
+        'nodes': [
+          {
+            'type': 'example',
+          }
+        ]
+      });
+      expect(InsertOperation.fromJson(json), op);
+      expect(op.invert().invert(), op);
+      expect(op.copyWith(), op);
+    });
+
+    test('test update operation', () {
+      final op = UpdateOperation([0], {'a': 1}, {'a': 0});
+      final json = op.toJson();
+      expect(json, {
+        'op': 'update',
+        'path': [0],
+        'attributes': {'a': 1},
+        'oldAttributes': {'a': 0}
+      });
+      expect(UpdateOperation.fromJson(json), op);
+      expect(op.invert().invert(), op);
+      expect(op.copyWith(), op);
+    });
+
+    test('test delete operation', () {
+      final node = Node(type: 'example');
+      final op = DeleteOperation([0], [node]);
+      final json = op.toJson();
+      expect(json, {
+        'op': 'delete',
+        'path': [0],
+        'nodes': [
+          {
+            'type': 'example',
+          }
+        ]
+      });
+      expect(DeleteOperation.fromJson(json), op);
+      expect(op.invert().invert(), op);
+      expect(op.copyWith(), op);
+    });
+
+    test('test update text operation', () {
+      final app = Delta()..insert('App');
+      final appflowy = Delta()
+        ..retain(3)
+        ..insert('Flowy');
+      final op = UpdateTextOperation([0], app, appflowy.invert(app));
+      final json = op.toJson();
+      expect(json, {
+        'op': 'update_text',
+        'path': [0],
+        'delta': [
+          {'insert': 'App'}
+        ],
+        'inverted': [
+          {'retain': 3},
+          {'delete': 5}
+        ]
+      });
+      expect(UpdateTextOperation.fromJson(json), op);
+      expect(op.invert().invert(), op);
+      expect(op.copyWith(), op);
+    });
+  });
+}