From b5e9bf6ee30207083b14921fa2150495dc03982d Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 10 Oct 2022 23:33:58 +0800 Subject: [PATCH] test: operation.dart --- .../lib/src/core/document/node.dart | 25 +++++- .../lib/src/core/transform/operation.dart | 67 ++++++++++++++-- .../test/core/transform/operation_test.dart | 79 +++++++++++++++++++ 3 files changed, 165 insertions(+), 6 deletions(-) create mode 100644 frontend/app_flowy/packages/appflowy_editor/test/core/transform/operation_test.dart diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/core/document/node.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/core/document/node.dart index bb52629054..975ef00df3 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/core/document/node.dart +++ b/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({ @@ -276,3 +276,26 @@ class TextNode extends Node { String toPlainText() => _delta.toPlainText(); } + +extension NodeEquality on Iterable { + bool equals(Iterable 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 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); + } +} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/core/transform/operation.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/core/transform/operation.dart index 7966b8864a..31662a61ca 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/core/transform/operation.dart +++ b/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 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 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 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 diff --git a/frontend/app_flowy/packages/appflowy_editor/test/core/transform/operation_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/core/transform/operation_test.dart new file mode 100644 index 0000000000..d52ba43221 --- /dev/null +++ b/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); + }); + }); +}