فهرست منبع

add undo test

appflowy 3 سال پیش
والد
کامیت
bfb80d1184

+ 2 - 1
app_flowy/packages/flowy_editor/lib/src/service/controller.dart

@@ -93,7 +93,8 @@ class EditorController extends ChangeNotifier {
       toggledStyle = toggledStyle.put(attribute);
     }
 
-    final change = document.format(index, length, attribute);
+    final change =
+        document.format(index, length, LinkAttribute("www.baidu.com"));
     final adjustedSelection = selection.copyWith(
       baseOffset: change.transformPosition(selection.baseOffset),
       extentOffset: change.transformPosition(selection.extentOffset),

+ 1 - 1
rust-lib/flowy-ot/src/client/extensions/delete/preserve_line_format_merge.rs

@@ -1,6 +1,6 @@
 use crate::{
     client::{extensions::DeleteExt, util::is_newline},
-    core::{Attributes, CharMetric, Delta, DeltaBuilder, DeltaIter, Interval, Operation, NEW_LINE},
+    core::{Attributes, CharMetric, Delta, DeltaBuilder, DeltaIter, Interval, NEW_LINE},
 };
 
 pub struct PreserveLineFormatOnMerge {}

+ 48 - 47
rust-lib/flowy-ot/src/client/extensions/format/format_at_position.rs

@@ -1,47 +1,48 @@
-use crate::{
-    client::extensions::FormatExt,
-    core::{Attribute, AttributeKey, Delta, DeltaBuilder, DeltaIter, Interval},
-};
-
-pub struct FormatLinkAtCaretPositionExt {}
-
-impl FormatExt for FormatLinkAtCaretPositionExt {
-    fn ext_name(&self) -> &str { std::any::type_name::<FormatLinkAtCaretPositionExt>() }
-
-    fn apply(&self, delta: &Delta, interval: Interval, attribute: &Attribute) -> Option<Delta> {
-        if attribute.key != AttributeKey::Link || interval.size() != 0 {
-            return None;
-        }
-
-        let mut iter = DeltaIter::from_offset(delta, interval.start);
-        let (before, after) = (iter.next_op_with_len(interval.size()), iter.next_op());
-        let mut start = interval.end;
-        let mut retain = 0;
-
-        if let Some(before) = before {
-            if before.contain_attribute(attribute) {
-                start -= before.len();
-                retain += before.len();
-            }
-        }
-
-        if let Some(after) = after {
-            if after.contain_attribute(attribute) {
-                if retain != 0 {
-                    retain += after.len();
-                }
-            }
-        }
-
-        if retain == 0 {
-            return None;
-        }
-
-        Some(
-            DeltaBuilder::new()
-                .retain(start)
-                .retain_with_attributes(retain, (attribute.clone()).into())
-                .build(),
-        )
-    }
-}
+// use crate::{
+//     client::extensions::FormatExt,
+//     core::{Attribute, AttributeKey, Delta, DeltaBuilder, DeltaIter,
+// Interval}, };
+//
+// pub struct FormatLinkAtCaretPositionExt {}
+//
+// impl FormatExt for FormatLinkAtCaretPositionExt {
+//     fn ext_name(&self) -> &str {
+// std::any::type_name::<FormatLinkAtCaretPositionExt>() }
+//
+//     fn apply(&self, delta: &Delta, interval: Interval, attribute: &Attribute)
+// -> Option<Delta> {         if attribute.key != AttributeKey::Link ||
+// interval.size() != 0 {             return None;
+//         }
+//
+//         let mut iter = DeltaIter::from_offset(delta, interval.start);
+//         let (before, after) = (iter.next_op_with_len(interval.size()),
+// iter.next_op());         let mut start = interval.end;
+//         let mut retain = 0;
+//
+//         if let Some(before) = before {
+//             if before.contain_attribute(attribute) {
+//                 start -= before.len();
+//                 retain += before.len();
+//             }
+//         }
+//
+//         if let Some(after) = after {
+//             if after.contain_attribute(attribute) {
+//                 if retain != 0 {
+//                     retain += after.len();
+//                 }
+//             }
+//         }
+//
+//         if retain == 0 {
+//             return None;
+//         }
+//
+//         Some(
+//             DeltaBuilder::new()
+//                 .retain(start)
+//                 .retain_with_attributes(retain, (attribute.clone()).into())
+//                 .build(),
+//         )
+//     }
+// }

+ 1 - 1
rust-lib/flowy-ot/src/client/extensions/insert/auto_exit_block.rs

@@ -1,6 +1,6 @@
 use crate::{
     client::{extensions::InsertExt, util::is_newline},
-    core::{AttributeKey, Delta, DeltaBuilder, DeltaIter, Operation},
+    core::{AttributeKey, Delta, DeltaBuilder, DeltaIter},
 };
 
 use crate::core::{attributes_except_header, is_empty_line_at_index};

+ 1 - 1
rust-lib/flowy-ot/src/client/extensions/insert/auto_format.rs

@@ -50,7 +50,7 @@ impl InsertExt for AutoFormatExt {
     }
 }
 
-use crate::core::{AttributeBuilder, Attributes, DeltaBuilder, Operation};
+use crate::core::{AttributeBuilder, Attributes, DeltaBuilder};
 use bytecount::num_chars;
 use std::cmp::min;
 use url::Url;

+ 0 - 1
rust-lib/flowy-ot/src/client/extensions/insert/preserve_block_format.rs

@@ -7,7 +7,6 @@ use crate::{
         Delta,
         DeltaBuilder,
         DeltaIter,
-        Operation,
         NEW_LINE,
     },
 };

+ 1 - 1
rust-lib/flowy-ot/src/client/view.rs

@@ -94,7 +94,7 @@ fn construct_insert_exts() -> Vec<InsertExtension> {
 
 fn construct_format_exts() -> Vec<FormatExtension> {
     vec![
-        Box::new(FormatLinkAtCaretPositionExt {}),
+        // Box::new(FormatLinkAtCaretPositionExt {}),
         Box::new(ResolveBlockFormat {}),
         Box::new(ResolveInlineFormat {}),
     ]

+ 1 - 1
rust-lib/flowy-ot/src/core/attributes/attributes.rs

@@ -47,7 +47,7 @@ impl Attributes {
             None => {
                 self.inner
                     .iter_mut()
-                    .for_each(|(k, v)| v.0 = REMOVE_FLAG.into());
+                    .for_each(|(_k, v)| v.0 = REMOVE_FLAG.into());
             },
             Some(attribute) => {
                 self.inner.iter_mut().for_each(|(k, v)| {

+ 5 - 2
rust-lib/flowy-ot/src/core/delta/cursor.rs

@@ -175,14 +175,17 @@ pub struct CharMetric {}
 
 impl Metric for CharMetric {
     fn seek(cursor: &mut OpCursor, index: usize) -> SeekResult {
-        let _ = check_bound(cursor.consume_count, index)?;
-        let _ = cursor.next_with_len(Some(index));
+        if index > 0 {
+            let _ = check_bound(cursor.consume_count, index)?;
+            let _ = cursor.next_with_len(Some(index));
+        }
 
         Ok(())
     }
 }
 
 fn check_bound(current: usize, target: usize) -> Result<(), OTError> {
+    debug_assert!(current <= target);
     if current > target {
         let msg = format!("{} should be greater than current: {}", target, current);
         return Err(ErrorBuilder::new(OTErrorCode::IncompatibleLength)

+ 61 - 73
rust-lib/flowy-ot/tests/attribute_test.rs

@@ -1,61 +1,10 @@
 pub mod helper;
 
 use crate::helper::{TestOp::*, *};
-use flowy_ot::core::Interval;
-
-use flowy_ot::core::{NEW_LINE, WHITESPACE};
+use flowy_ot::core::{Interval, NEW_LINE, WHITESPACE};
 
 #[test]
-fn attributes_insert_text() {
-    let ops = vec![
-        Insert(0, "123", 0),
-        Insert(0, "456", 3),
-        AssertOpsJson(0, r#"[{"insert":"123456"}]"#),
-    ];
-    OpTester::new().run_script(ops);
-}
-
-#[test]
-fn attributes_insert_text_at_head() {
-    let ops = vec![
-        Insert(0, "123", 0),
-        Insert(0, "456", 0),
-        AssertOpsJson(0, r#"[{"insert":"456123"}]"#),
-    ];
-    OpTester::new().run_script(ops);
-}
-
-#[test]
-fn attributes_insert_text_at_middle() {
-    let ops = vec![
-        Insert(0, "123", 0),
-        Insert(0, "456", 1),
-        AssertOpsJson(0, r#"[{"insert":"145623"}]"#),
-    ];
-    OpTester::new().run_script(ops);
-}
-
-#[test]
-fn attributes_insert_text_with_attr() {
-    let ops = vec![
-        Insert(0, "145", 0),
-        Insert(0, "23", 1),
-        Bold(0, Interval::new(0, 2), true),
-        AssertOpsJson(
-            0,
-            r#"[{"insert":"12","attributes":{"bold":"true"}},{"insert":"345"}]"#,
-        ),
-        Insert(0, "abc", 1),
-        AssertOpsJson(
-            0,
-            r#"[{"insert":"1abc2","attributes":{"bold":"true"}},{"insert":"345"}]"#,
-        ),
-    ];
-    OpTester::new().run_script(ops);
-}
-
-#[test]
-fn attributes_add_bold() {
+fn attributes_bold_added() {
     let ops = vec![
         Insert(0, "123456", 0),
         Bold(0, Interval::new(3, 5), true),
@@ -72,7 +21,7 @@ fn attributes_add_bold() {
 }
 
 #[test]
-fn attributes_add_bold_and_invert_all() {
+fn attributes_bold_added_and_invert_all() {
     let ops = vec![
         Insert(0, "123", 0),
         Bold(0, Interval::new(0, 3), true),
@@ -84,7 +33,7 @@ fn attributes_add_bold_and_invert_all() {
 }
 
 #[test]
-fn attributes_add_bold_and_invert_partial_suffix() {
+fn attributes_bold_added_and_invert_partial_suffix() {
     let ops = vec![
         Insert(0, "1234", 0),
         Bold(0, Interval::new(0, 4), true),
@@ -99,7 +48,7 @@ fn attributes_add_bold_and_invert_partial_suffix() {
 }
 
 #[test]
-fn attributes_add_bold_and_invert_partial_suffix2() {
+fn attributes_bold_added_and_invert_partial_suffix2() {
     let ops = vec![
         Insert(0, "1234", 0),
         Bold(0, Interval::new(0, 4), true),
@@ -116,7 +65,7 @@ fn attributes_add_bold_and_invert_partial_suffix2() {
 }
 
 #[test]
-fn attributes_add_bold_with_new_line() {
+fn attributes_bold_added_with_new_line() {
     let ops = vec![
         Insert(0, "123456", 0),
         Bold(0, Interval::new(0, 6), true),
@@ -144,7 +93,7 @@ fn attributes_add_bold_with_new_line() {
 }
 
 #[test]
-fn attributes_add_bold_and_invert_partial_prefix() {
+fn attributes_bold_added_and_invert_partial_prefix() {
     let ops = vec![
         Insert(0, "1234", 0),
         Bold(0, Interval::new(0, 4), true),
@@ -159,7 +108,7 @@ fn attributes_add_bold_and_invert_partial_prefix() {
 }
 
 #[test]
-fn attributes_add_bold_consecutive() {
+fn attributes_bold_added_consecutive() {
     let ops = vec![
         Insert(0, "1234", 0),
         Bold(0, Interval::new(0, 1), true),
@@ -177,7 +126,7 @@ fn attributes_add_bold_consecutive() {
 }
 
 #[test]
-fn attributes_add_bold_italic() {
+fn attributes_bold_added_italic() {
     let ops = vec![
         Insert(0, "1234", 0),
         Bold(0, Interval::new(0, 4), true),
@@ -196,7 +145,7 @@ fn attributes_add_bold_italic() {
 }
 
 #[test]
-fn attributes_add_bold_italic2() {
+fn attributes_bold_added_italic2() {
     let ops = vec![
         Insert(0, "123456", 0),
         Bold(0, Interval::new(0, 6), true),
@@ -224,7 +173,7 @@ fn attributes_add_bold_italic2() {
 }
 
 #[test]
-fn attributes_add_bold_italic3() {
+fn attributes_bold_added_italic3() {
     let ops = vec![
         Insert(0, "123456789", 0),
         Bold(0, Interval::new(0, 5), true),
@@ -261,7 +210,7 @@ fn attributes_add_bold_italic3() {
 }
 
 #[test]
-fn attributes_add_bold_italic_delete() {
+fn attributes_bold_added_italic_delete() {
     let ops = vec![
         Insert(0, "123456789", 0),
         Bold(0, Interval::new(0, 5), true),
@@ -467,7 +416,7 @@ fn attributes_header_insert_newline_at_middle() {
 }
 
 #[test]
-fn attributes_header_insert_newline_at_middle2() {
+fn attributes_header_insert_double_newline_at_middle() {
     let ops = vec![
         Insert(0, "123456", 0),
         Header(0, Interval::new(0, 6), 1, true),
@@ -523,7 +472,7 @@ fn attributes_header_insert_double_newline_at_trailing() {
 }
 
 #[test]
-fn attributes_add_link() {
+fn attributes_link_added() {
     let ops = vec![
         Insert(0, "123456", 0),
         Link(0, Interval::new(0, 6), "https://appflowy.io", true),
@@ -536,6 +485,25 @@ fn attributes_add_link() {
     OpTester::new().run_script_with_newline(ops);
 }
 
+#[test]
+fn attributes_link_format_with_bold() {
+    let ops = vec![
+        Insert(0, "123456", 0),
+        Link(0, Interval::new(0, 6), "https://appflowy.io", true),
+        Bold(0, Interval::new(0, 3), true),
+        AssertOpsJson(
+            0,
+            r#"[
+            {"insert":"123","attributes":{"bold":"true","link":"https://appflowy.io"}},
+            {"insert":"456","attributes":{"link":"https://appflowy.io"}},
+            {"insert":"\n"}]
+            "#,
+        ),
+    ];
+
+    OpTester::new().run_script_with_newline(ops);
+}
+
 #[test]
 fn attributes_link_insert_char_at_head() {
     let ops = vec![
@@ -605,7 +573,7 @@ fn attributes_link_insert_newline_at_middle() {
 }
 
 #[test]
-fn attributes_auto_format_link() {
+fn attributes_link_auto_format() {
     let site = "https://appflowy.io";
     let ops = vec![
         Insert(0, site, 0),
@@ -621,7 +589,7 @@ fn attributes_auto_format_link() {
 }
 
 #[test]
-fn attributes_auto_format_exist_link() {
+fn attributes_link_auto_format_exist() {
     let site = "https://appflowy.io";
     let ops = vec![
         Insert(0, site, 0),
@@ -637,7 +605,7 @@ fn attributes_auto_format_exist_link() {
 }
 
 #[test]
-fn attributes_auto_format_exist_link2() {
+fn attributes_link_auto_format_exist2() {
     let site = "https://appflowy.io";
     let ops = vec![
         Insert(0, site, 0),
@@ -653,7 +621,7 @@ fn attributes_auto_format_exist_link2() {
 }
 
 #[test]
-fn attributes_add_bullet() {
+fn attributes_bullet_added() {
     let ops = vec![
         Insert(0, "12", 0),
         Bullet(0, Interval::new(0, 1), true),
@@ -667,7 +635,7 @@ fn attributes_add_bullet() {
 }
 
 #[test]
-fn attributes_add_bullet2() {
+fn attributes_bullet_added_2() {
     let ops = vec![
         Insert(0, "1", 0),
         Bullet(0, Interval::new(0, 1), true),
@@ -691,7 +659,7 @@ fn attributes_add_bullet2() {
 }
 
 #[test]
-fn attributes_un_bullet_one() {
+fn attributes_bullet_remove_partial() {
     let ops = vec![
         Insert(0, "1", 0),
         Bullet(0, Interval::new(0, 1), true),
@@ -708,7 +676,7 @@ fn attributes_un_bullet_one() {
 }
 
 #[test]
-fn attributes_auto_exit_block() {
+fn attributes_bullet_auto_exit() {
     let ops = vec![
         Insert(0, "1", 0),
         Bullet(0, Interval::new(0, 1), true),
@@ -764,7 +732,7 @@ fn attributes_preserve_block_when_insert_newline_inside() {
 }
 
 #[test]
-fn attributes_preserve_line_format_on_merge() {
+fn attributes_preserve_header_format_on_merge() {
     let ops = vec![
         Insert(0, "123456", 0),
         Header(0, Interval::new(0, 6), 1, true),
@@ -782,3 +750,23 @@ fn attributes_preserve_line_format_on_merge() {
 
     OpTester::new().run_script_with_newline(ops);
 }
+
+#[test]
+fn attributes_preserve_list_format_on_merge() {
+    let ops = vec![
+        Insert(0, "123456", 0),
+        Bullet(0, Interval::new(0, 6), true),
+        Insert(0, NEW_LINE, 3),
+        AssertOpsJson(
+            0,
+            r#"[{"insert":"123"},{"insert":"\n","attributes":{"bullet":"true"}},{"insert":"456"},{"insert":"\n","attributes":{"bullet":"true"}}]"#,
+        ),
+        Delete(0, Interval::new(3, 4)),
+        AssertOpsJson(
+            0,
+            r#"[{"insert":"123456"},{"insert":"\n","attributes":{"bullet":"true"}}]"#,
+        ),
+    ];
+
+    OpTester::new().run_script_with_newline(ops);
+}

+ 1 - 1
rust-lib/flowy-ot/tests/helper/mod.rs

@@ -3,7 +3,7 @@ use flowy_ot::{client::Document, core::*};
 use rand::{prelude::*, Rng as WrappedRng};
 use std::{sync::Once, time::Duration};
 
-const LEVEL: &'static str = "debug";
+const LEVEL: &'static str = "info";
 
 #[derive(Clone, Debug, Display)]
 pub enum TestOp {

+ 30 - 0
rust-lib/flowy-ot/tests/op_test.rs

@@ -5,6 +5,36 @@ use bytecount::num_chars;
 use flowy_ot::core::*;
 use helper::*;
 
+#[test]
+fn attributes_insert_text() {
+    let ops = vec![
+        Insert(0, "123", 0),
+        Insert(0, "456", 3),
+        AssertOpsJson(0, r#"[{"insert":"123456"}]"#),
+    ];
+    OpTester::new().run_script(ops);
+}
+
+#[test]
+fn attributes_insert_text_at_head() {
+    let ops = vec![
+        Insert(0, "123", 0),
+        Insert(0, "456", 0),
+        AssertOpsJson(0, r#"[{"insert":"456123"}]"#),
+    ];
+    OpTester::new().run_script(ops);
+}
+
+#[test]
+fn attributes_insert_text_at_middle() {
+    let ops = vec![
+        Insert(0, "123", 0),
+        Insert(0, "456", 1),
+        AssertOpsJson(0, r#"[{"insert":"145623"}]"#),
+    ];
+    OpTester::new().run_script(ops);
+}
+
 #[test]
 fn delta_get_ops_in_interval_1() {
     let mut delta = Delta::default();

+ 122 - 18
rust-lib/flowy-ot/tests/undo_redo_test.rs

@@ -1,10 +1,13 @@
 pub mod helper;
 
 use crate::helper::{TestOp::*, *};
-use flowy_ot::{client::RECORD_THRESHOLD, core::Interval};
+use flowy_ot::{
+    client::RECORD_THRESHOLD,
+    core::{Interval, NEW_LINE, WHITESPACE},
+};
 
 #[test]
-fn history_undo_insert() {
+fn history_insert_undo() {
     let ops = vec![
         Insert(0, "123", 0),
         Undo(0),
@@ -14,7 +17,7 @@ fn history_undo_insert() {
 }
 
 #[test]
-fn history_undo_insert2() {
+fn history_insert_undo_with_lagging() {
     let ops = vec![
         Insert(0, "123", 0),
         Wait(RECORD_THRESHOLD),
@@ -28,7 +31,7 @@ fn history_undo_insert2() {
 }
 
 #[test]
-fn history_redo_insert() {
+fn history_insert_redo() {
     let ops = vec![
         Insert(0, "123", 0),
         AssertOpsJson(0, r#"[{"insert":"123\n"}]"#),
@@ -41,7 +44,7 @@ fn history_redo_insert() {
 }
 
 #[test]
-fn history_redo_insert_with_lagging() {
+fn history_insert_redo_with_lagging() {
     let ops = vec![
         Insert(0, "123", 0),
         Wait(RECORD_THRESHOLD),
@@ -60,7 +63,7 @@ fn history_redo_insert_with_lagging() {
 }
 
 #[test]
-fn history_undo_attributes() {
+fn history_bold_undo() {
     let ops = vec![
         Insert(0, "123", 0),
         Bold(0, Interval::new(0, 3), true),
@@ -71,7 +74,7 @@ fn history_undo_attributes() {
 }
 
 #[test]
-fn history_undo_attributes_with_lagging() {
+fn history_bold_undo_with_lagging() {
     let ops = vec![
         Insert(0, "123", 0),
         Wait(RECORD_THRESHOLD),
@@ -83,7 +86,7 @@ fn history_undo_attributes_with_lagging() {
 }
 
 #[test]
-fn history_redo_attributes() {
+fn history_bold_redo() {
     let ops = vec![
         Insert(0, "123", 0),
         Bold(0, Interval::new(0, 3), true),
@@ -99,7 +102,7 @@ fn history_redo_attributes() {
 }
 
 #[test]
-fn history_redo_attributes_with_lagging() {
+fn history_bold_redo_with_lagging() {
     let ops = vec![
         Insert(0, "123", 0),
         Wait(RECORD_THRESHOLD),
@@ -116,7 +119,7 @@ fn history_redo_attributes_with_lagging() {
 }
 
 #[test]
-fn history_undo_delete() {
+fn history_delete_undo() {
     let ops = vec![
         Insert(0, "123", 0),
         AssertOpsJson(0, r#"[{"insert":"123"}]"#),
@@ -129,7 +132,7 @@ fn history_undo_delete() {
 }
 
 #[test]
-fn history_undo_delete2() {
+fn history_delete_undo_2() {
     let ops = vec![
         Insert(0, "123", 0),
         Bold(0, Interval::new(0, 3), true),
@@ -148,7 +151,7 @@ fn history_undo_delete2() {
 }
 
 #[test]
-fn history_undo_delete2_with_lagging() {
+fn history_delete_undo_with_lagging() {
     let ops = vec![
         Insert(0, "123", 0),
         Wait(RECORD_THRESHOLD),
@@ -175,7 +178,7 @@ fn history_undo_delete2_with_lagging() {
 }
 
 #[test]
-fn history_redo_delete() {
+fn history_delete_redo() {
     let ops = vec![
         Insert(0, "123", 0),
         Wait(RECORD_THRESHOLD),
@@ -189,7 +192,7 @@ fn history_redo_delete() {
 }
 
 #[test]
-fn history_undo_replace() {
+fn history_replace_undo() {
     let ops = vec![
         Insert(0, "123", 0),
         Bold(0, Interval::new(0, 3), true),
@@ -208,7 +211,7 @@ fn history_undo_replace() {
 }
 
 #[test]
-fn history_undo_replace_with_lagging() {
+fn history_replace_undo_with_lagging() {
     let ops = vec![
         Insert(0, "123", 0),
         Wait(RECORD_THRESHOLD),
@@ -232,7 +235,7 @@ fn history_undo_replace_with_lagging() {
 }
 
 #[test]
-fn history_redo_replace() {
+fn history_replace_redo() {
     let ops = vec![
         Insert(0, "123", 0),
         Bold(0, Interval::new(0, 3), true),
@@ -251,13 +254,14 @@ fn history_redo_replace() {
 }
 
 #[test]
-fn history_undo_add_header() {
+fn history_header_added_undo() {
     let ops = vec![
         Insert(0, "123456", 0),
         Header(0, Interval::new(0, 6), 1, true),
         Insert(0, "\n", 3),
         Insert(0, "\n", 4),
         Undo(0),
+        AssertOpsJson(0, r#"[{"insert":"\n"}]"#),
         Redo(0),
         AssertOpsJson(
             0,
@@ -269,7 +273,7 @@ fn history_undo_add_header() {
 }
 
 #[test]
-fn history_undo_add_link() {
+fn history_link_added_undo() {
     let site = "https://appflowy.io";
     let ops = vec![
         Insert(0, site, 0),
@@ -286,3 +290,103 @@ fn history_undo_add_link() {
 
     OpTester::new().run_script_with_newline(ops);
 }
+
+#[test]
+fn history_link_auto_format_undo_with_lagging() {
+    let site = "https://appflowy.io";
+    let ops = vec![
+        Insert(0, site, 0),
+        AssertOpsJson(0, r#"[{"insert":"https://appflowy.io\n"}]"#),
+        Wait(RECORD_THRESHOLD),
+        Insert(0, WHITESPACE, site.len()),
+        AssertOpsJson(
+            0,
+            r#"[{"insert":"https://appflowy.io","attributes":{"link":"https://appflowy.io/"}},{"insert":" \n"}]"#,
+        ),
+        Undo(0),
+        AssertOpsJson(0, r#"[{"insert":"https://appflowy.io\n"}]"#),
+    ];
+
+    OpTester::new().run_script_with_newline(ops);
+}
+
+#[test]
+fn history_bullet_undo() {
+    let ops = vec![
+        Insert(0, "1", 0),
+        Bullet(0, Interval::new(0, 1), true),
+        Insert(0, NEW_LINE, 1),
+        Insert(0, "2", 2),
+        AssertOpsJson(
+            0,
+            r#"[{"insert":"1"},{"insert":"\n","attributes":{"bullet":"true"}},{"insert":"2"},{"insert":"\n","attributes":{"bullet":"true"}}]"#,
+        ),
+        Undo(0),
+        AssertOpsJson(0, r#"[{"insert":"\n"}]"#),
+        Redo(0),
+        AssertOpsJson(
+            0,
+            r#"[{"insert":"1"},{"insert":"\n","attributes":{"bullet":"true"}},{"insert":"2"},{"insert":"\n","attributes":{"bullet":"true"}}]"#,
+        ),
+    ];
+
+    OpTester::new().run_script_with_newline(ops);
+}
+
+#[test]
+fn history_bullet_undo_with_lagging() {
+    let ops = vec![
+        Insert(0, "1", 0),
+        Bullet(0, Interval::new(0, 1), true),
+        Wait(RECORD_THRESHOLD),
+        Insert(0, NEW_LINE, 1),
+        Insert(0, "2", 2),
+        Wait(RECORD_THRESHOLD),
+        AssertOpsJson(
+            0,
+            r#"[{"insert":"1"},{"insert":"\n","attributes":{"bullet":"true"}},{"insert":"2"},{"insert":"\n","attributes":{"bullet":"true"}}]"#,
+        ),
+        Undo(0),
+        AssertOpsJson(
+            0,
+            r#"[{"insert":"1"},{"insert":"\n","attributes":{"bullet":"true"}}]"#,
+        ),
+        Undo(0),
+        AssertOpsJson(0, r#"[{"insert":"\n"}]"#),
+        Redo(0),
+        Redo(0),
+        AssertOpsJson(
+            0,
+            r#"[{"insert":"1"},{"insert":"\n","attributes":{"bullet":"true"}},{"insert":"2"},{"insert":"\n","attributes":{"bullet":"true"}}]"#,
+        ),
+    ];
+
+    OpTester::new().run_script_with_newline(ops);
+}
+
+#[test]
+fn history_undo_attribute_on_merge_between_line() {
+    let ops = vec![
+        Insert(0, "123456", 0),
+        Bullet(0, Interval::new(0, 6), true),
+        Wait(RECORD_THRESHOLD),
+        Insert(0, NEW_LINE, 3),
+        Wait(RECORD_THRESHOLD),
+        AssertOpsJson(
+            0,
+            r#"[{"insert":"123"},{"insert":"\n","attributes":{"bullet":"true"}},{"insert":"456"},{"insert":"\n","attributes":{"bullet":"true"}}]"#,
+        ),
+        Delete(0, Interval::new(3, 4)), // delete the newline
+        AssertOpsJson(
+            0,
+            r#"[{"insert":"123456"},{"insert":"\n","attributes":{"bullet":"true"}}]"#,
+        ),
+        Undo(0),
+        AssertOpsJson(
+            0,
+            r#"[{"insert":"123"},{"insert":"\n","attributes":{"bullet":"true"}},{"insert":"456"},{"insert":"\n","attributes":{"bullet":"true"}}]"#,
+        ),
+    ];
+
+    OpTester::new().run_script_with_newline(ops);
+}