oficina / docs Public

Clone
git clone https://amadan.net/oficina/docs

Plain git — no account needed to clone.

Download

Download this file

1use docs_native_crdt::Editor;
2use serde_json::{json, Value};
3
4fn paragraph(editor: &mut Editor, text: &str) -> (String, String) {
5 editor.command(&json!({"op":"insert_element", "at":0, "type":"paragraph"})).unwrap();
6 let block = editor.projection()["children"][0]["id"].as_str().unwrap().to_owned();
7 editor.command(&json!({"op":"insert_text", "node":block, "at":0, "text":text})).unwrap();
8 let node = editor.projection()["children"][0]["children"][0]["id"].as_str().unwrap().to_owned();
9 (block, node)
10}
11fn plain(projection: &Value) -> String {
12 if let Some(runs) = projection["runs"].as_array() {
13 return runs.iter().map(|r| r["insert"].as_str().unwrap()).collect();
14 }
15 projection["children"].as_array().map(|items| items.iter().map(plain).collect()).unwrap_or_default()
16}
17#[test]
18fn utf16_edits_preserve_identity_and_marks() {
19 let mut editor = Editor::new("fixture", 1);
20 let (block, node) = paragraph(&mut editor, "A๐Ÿ˜€B");
21 editor.command(&json!({"op":"format", "node":node, "at":1, "length":2, "marks":{"strong":{}}})).unwrap();
22 editor.command(&json!({"op":"replace_text", "node":node, "at":3, "length":1, "text":"C"})).unwrap();
23 let p = editor.projection();
24 assert_eq!(plain(&p), "A๐Ÿ˜€C");
25 assert_eq!(p["children"][0]["id"], block);
26 assert_eq!(p["children"][0]["children"][0]["id"], node);
27 assert_eq!(p["children"][0]["children"][0]["runs"][1]["marks"]["strong"], json!({}));
28 assert!(editor.command(&json!({"op":"replace_text", "node":node, "at":2, "length":0, "text":"broken"})).is_err());
29 assert_eq!(plain(&editor.projection()), "A๐Ÿ˜€C");
30}
31#[test]
32fn concurrent_insertions_and_own_undo_preserve_peer() {
33 let mut a = Editor::new("fixture", 2);
34 let (_, node) = paragraph(&mut a, "Start");
35 let mut b = Editor::new("fixture", 3);
36 b.apply(&a.snapshot()).unwrap();
37 let local = a.command(&json!({"op":"replace_text", "node":node, "at":5, "length":0, "text":" alice"})).unwrap();
38 let peer = b.command(&json!({"op":"replace_text", "node":node, "at":0, "length":0, "text":"bob "})).unwrap();
39 a.apply(&peer).unwrap(); b.apply(&local).unwrap();
40 assert_eq!(plain(&a.projection()), plain(&b.projection()));
41 let undo = a.command(&json!({"op":"undo"})).unwrap(); b.apply(&undo).unwrap();
42 assert_eq!(plain(&a.projection()), "bob Start");
43 assert_eq!(plain(&a.projection()), plain(&b.projection()));
44}
45#[test]
46fn concatenated_log_applies_every_update() {
47 let mut a = Editor::new("fixture", 4);
48 let (_, node) = paragraph(&mut a, "one");
49 let mut log = a.snapshot();
50 log.extend(a.command(&json!({"op":"replace_text", "node":node, "at":3, "length":0, "text":" two"})).unwrap());
51 let b = Editor::new("fixture", 5);
52 b.apply(&log).unwrap();
53 assert_eq!(plain(&b.projection()), "one two");
54}
55#[test]
56fn reads_existing_yjs_fragment_without_rewriting() {
57 let editor = Editor::new("fixture", 6);
58 editor.apply(include_bytes!("../../../fixtures/vectors/fragment/formatted.update.bin")).unwrap();
59 let before = editor.projection();
60 let other = Editor::new("fixture", 7);
61 other.apply(&editor.snapshot()).unwrap();
62 assert_eq!(before["children"], other.projection()["children"]);
63 assert!(!plain(&before).is_empty());
64}
65#[test]
66fn paragraph_split_keeps_prefix_identity_and_suffix_marks() {
67 let mut editor = Editor::new("fixture", 9);
68 let (block, node) = paragraph(&mut editor, "A๐Ÿ˜€B");
69 editor.command(&json!({"op":"format", "node":node, "at":1, "length":3, "marks":{"em":{}}})).unwrap();
70 editor.command(&json!({"op":"split", "node":node, "at":1})).unwrap();
71 let p = editor.projection();
72 assert_eq!(p["children"][0]["id"], block);
73 assert_eq!(plain(&p["children"][0]), "A");
74 assert_eq!(plain(&p["children"][1]), "๐Ÿ˜€B");
75 assert_eq!(p["children"][1]["children"][0]["runs"][0]["marks"]["em"], json!({}));
76}
77#[test]
78fn changing_block_preserves_existing_inline_formatting() {
79 let mut editor = Editor::new("fixture", 10);
80 let (block, node) = paragraph(&mut editor, "Heading");
81 editor.command(&json!({"op":"format", "node":node, "at":0, "length":7, "marks":{"strong":{},"font_serif":{}}})).unwrap();
82 editor.command(&json!({"op":"block", "node":block, "type":"heading", "level":2})).unwrap();
83 let p = editor.projection();
84 assert_eq!(p["children"][0]["type"], "heading");
85 assert_eq!(p["children"][0]["attrs"]["level"], "2");
86 assert_eq!(p["children"][0]["children"][0]["runs"][0]["marks"]["font_serif"], json!({}));
87}
88
89#[test]
90fn distinct_display_generations_preserve_peer_and_authoritative_undo() {
91 let mut authority = Editor::new("generation", 100);
92 let (_, node) = paragraph(&mut authority, "abc");
93 let mut displayed = Editor::new("generation", 101);
94 displayed.apply(&authority.snapshot()).unwrap();
95 let mut peer = Editor::new("generation", 102);
96 peer.apply(&authority.snapshot()).unwrap();
97 let remote = peer.command(&json!({"op":"replace_text","node":node,"at":0,"length":0,"text":"P"})).unwrap();
98 authority.apply(&remote).unwrap();
99 // A callback still refers to the displayed pre-peer range, deleting b.
100 let local = displayed.command(&json!({"op":"replace_text","node":node,"at":1,"length":1,"text":"X"})).unwrap();
101 authority.apply_local(&local).unwrap();
102 assert_eq!(plain(&authority.projection()), "PaXc");
103 let undo = authority.command(&json!({"op":"undo"})).unwrap();
104 assert_eq!(plain(&authority.projection()), "Pabc");
105 let mut next = Editor::new("generation", 103);
106 next.apply(&authority.snapshot()).unwrap();
107 let typed = next.command(&json!({"op":"replace_text","node":node,"at":4,"length":0,"text":"!"})).unwrap();
108 authority.apply_local(&typed).unwrap();
109 peer.apply(&local).unwrap(); peer.apply(&undo).unwrap(); peer.apply(&typed).unwrap();
110 assert_eq!(plain(&authority.projection()), "Pabc!");
111 assert_eq!(plain(&peer.projection()), "Pabc!");
112}
113
114#[test]
115fn optimistic_newline_typeahead_resolves_new_paragraph_in_source() {
116 let mut source = Editor::new("typeahead", 201);
117 paragraph(&mut source, "A๐Ÿ˜€B");
118 source.command(&json!({"op":"display_edit","at":3,"length":0,"text":"\n"})).unwrap();
119 source.command(&json!({"op":"display_edit","at":4,"length":0,"text":"typed"})).unwrap();
120 let projection = source.projection();
121 assert_eq!(plain(&projection["children"][0]), "A๐Ÿ˜€");
122 assert_eq!(plain(&projection["children"][1]), "typedB");
123 source.command(&json!({"op":"display_edit","at":9,"length":0,"text":"\nsecond\nthird"})).unwrap();
124 let projection = source.projection();
125 assert_eq!(projection["children"].as_array().unwrap().len(), 4);
126 assert_eq!(plain(&projection["children"][2]), "second");
127 assert_eq!(plain(&projection["children"][3]), "thirdB");
128}
129
130#[test]
131fn cross_paragraph_replacement_keeps_prefix_id_and_formatted_suffix() {
132 let mut editor = Editor::new("join", 301);
133 let (prefix, _) = paragraph(&mut editor, "first");
134 editor.command(&json!({"op":"display_edit","at":5,"length":0,"text":"\nsecond\nthird"})).unwrap();
135 let tail = editor.projection()["children"][2]["children"][0]["id"].as_str().unwrap().to_owned();
136 editor.command(&json!({"op":"format","node":tail,"at":0,"length":5,"marks":{"em":{}}})).unwrap();
137 editor.command(&json!({"op":"display_edit","at":2,"length":13,"text":"X"})).unwrap();
138 let projected = editor.projection();
139 assert_eq!(projected["children"].as_array().unwrap().len(), 1);
140 assert_eq!(projected["children"][0]["id"], prefix);
141 assert_eq!(plain(&projected), "fiXird");
142 assert_eq!(projected["children"][0]["children"][1]["runs"][0]["marks"]["em"], json!({}));
143}
144
145#[test]
146fn selection_anchor_tracks_peer_insertion_in_utf16() {
147 let mut displayed = Editor::new("selection", 401);
148 let (_, node) = paragraph(&mut displayed, "A๐Ÿ˜€B");
149 let anchor = displayed.anchor(3).unwrap();
150 let mut authority = Editor::new("selection", 402);
151 authority.apply(&displayed.snapshot()).unwrap();
152 authority.command(&json!({"op":"replace_text","node":node,"at":0,"length":0,"text":"peer "})).unwrap();
153 assert_eq!(authority.resolve(&anchor).unwrap(), 8);
154}
155
156#[test]
157fn rich_paste_preserves_native_marks_and_existing_suffix() {
158 let mut editor = Editor::new("paste", 501);
159 let (prefix, _) = paragraph(&mut editor,"AB");
160 editor.command(&json!({"op":"display_paste","at":1,"length":0,"blocks":[
161 {"type":"paragraph","children":[{"type":"text","runs":[{"insert":"bold","marks":{"strong":{}}}]}]},
162 {"type":"paragraph","children":[{"type":"text","runs":[{"insert":"link","marks":{"link":{"href":"https://example.com"}}}]}]}
163 ]})).unwrap();
164 let p = editor.projection();
165 assert_eq!(p["children"][0]["id"],prefix);
166 assert_eq!(plain(&p["children"][0]),"Abold");
167 assert_eq!(plain(&p["children"][1]),"linkB");
168 assert_eq!(p["children"][0]["children"][1]["runs"][0]["marks"]["strong"],json!({}));
169 assert_eq!(p["children"][1]["children"][0]["runs"][0]["marks"]["link"]["href"],"https://example.com");
170}
171#[test]
172fn list_item_sink_and_lift_keep_text_and_marks() {
173 let mut editor = Editor::new("list", 601);
174 let (block, _) = paragraph(&mut editor,"one");
175 editor.command(&json!({"op":"block","node":block,"type":"bullet_list"})).unwrap();
176 editor.command(&json!({"op":"display_edit","at":3,"length":0,"text":"\ntwo"})).unwrap();
177 let second = editor.projection()["children"][0]["children"][1]["children"][0]["id"].as_str().unwrap().to_owned();
178 editor.command(&json!({"op":"list_indent","node":second,"delta":1})).unwrap();
179 let p = editor.projection();
180 assert_eq!(p["children"][0]["children"].as_array().unwrap().len(),1);
181 assert_eq!(plain(&p["children"][0]["children"][0]["children"][1]),"two");
182 let nested = p["children"][0]["children"][0]["children"][1]["children"][0]["children"][0]["id"].as_str().unwrap();
183 editor.command(&json!({"op":"list_indent","node":nested,"delta":-1})).unwrap();
184 assert_eq!(editor.projection()["children"][0]["children"].as_array().unwrap().len(),2);
185 assert_eq!(plain(&editor.projection()),"onetwo");
186}
187
188#[test]
189fn select_all_replaces_multiple_native_text_nodes_and_terminal_separator() {
190 let mut editor = Editor::new("select-all", 701);
191 let (block, _) = paragraph(&mut editor,"one");
192 editor.command(&json!({"op":"insert_text","node":block,"at":1,"text":"two"})).unwrap();
193 editor.command(&json!({"op":"display_edit","at":0,"length":7,"text":"replaced"})).unwrap();
194 assert_eq!(plain(&editor.projection()),"replaced");
195 assert_eq!(editor.projection()["children"][0]["id"],block);
196}
197
198#[test]
199fn hard_break_is_inline_and_code_enter_does_not_split_block() {
200 let mut editor = Editor::new("break", 801);
201 let (block, _) = paragraph(&mut editor,"AB");
202 editor.command(&json!({"op":"display_edit","at":1,"length":0,"text":"\u{2028}next"})).unwrap();
203 let p = editor.projection();
204 assert_eq!(p["children"].as_array().unwrap().len(),1);
205 assert_eq!(p["children"][0]["children"][1]["type"],"hard_break");
206 assert_eq!(plain(&p),"AnextB");
207 editor.command(&json!({"op":"block","node":block,"type":"code_block"})).unwrap();
208 editor.command(&json!({"op":"display_edit","at":0,"length":0,"text":"code\n"})).unwrap();
209 assert_eq!(editor.projection()["children"].as_array().unwrap().len(),1);
210 assert!(plain(&editor.projection()).starts_with("code\nA"));
211}
212#[test]
213fn toggling_existing_list_changes_type_and_lifts_without_nesting() {
214 let mut editor = Editor::new("toggle", 901);
215 let (block,_) = paragraph(&mut editor,"item");
216 editor.command(&json!({"op":"block","node":block,"type":"bullet_list"})).unwrap();
217 let block = editor.projection()["children"][0]["children"][0]["children"][0]["id"].as_str().unwrap().to_owned();
218 editor.command(&json!({"op":"block","node":block,"type":"ordered_list"})).unwrap();
219 assert_eq!(editor.projection()["children"][0]["type"],"ordered_list");
220 let block = editor.projection()["children"][0]["children"][0]["children"][0]["id"].as_str().unwrap().to_owned();
221 editor.command(&json!({"op":"block","node":block,"type":"ordered_list"})).unwrap();
222 assert_eq!(editor.projection()["children"][0]["type"],"paragraph");
223 assert_eq!(plain(&editor.projection()),"item");
224}
225
226#[test]
227fn deleting_and_replacing_inline_images_preserves_surrounding_text() {
228 let mut editor = Editor::new("image", 1001);
229 let (block,_) = paragraph(&mut editor,"");
230 editor.command(&json!({"op":"insert_element","node":block,"at":0,"type":"image","attrs":{"src":"https://example.com/a.png","alt":"A"}})).unwrap();
231 editor.command(&json!({"op":"insert_text","node":block,"at":1,"text":"after"})).unwrap();
232 editor.command(&json!({"op":"display_edit","at":0,"length":1,"text":"before "})).unwrap();
233 assert_eq!(plain(&editor.projection()),"before after");
234 assert!(!editor.projection().to_string().contains("https://example.com/a.png"));
235}
236
237#[test]
238fn rich_paste_at_empty_block_keeps_heading_and_list_structure() {
239 let mut editor = Editor::new("paste-structure", 1101);
240 paragraph(&mut editor, "");
241 editor.command(&json!({"op":"display_paste","at":0,"length":0,"blocks":[
242 {"type":"heading","attrs":{"level":"4"},"children":[{"type":"text","runs":[{"insert":"Heading","marks":{}}]}]},
243 {"type":"bullet_list","children":[{"type":"list_item","children":[{"type":"paragraph","children":[{"type":"text","runs":[{"insert":"Item","marks":{}}]}]}]}]}
244 ]})).unwrap();
245 let projected = editor.projection();
246 assert_eq!(projected["children"][0]["type"], "heading");
247 assert_eq!(projected["children"][0]["attrs"]["level"], "4");
248 assert_eq!(projected["children"][1]["type"], "bullet_list");
249 assert_eq!(projected["children"].as_array().unwrap().len(), 2);
250 editor.command(&json!({"op":"display_edit","at":12,"length":0,"text":"!"})).unwrap();
251 assert_eq!(plain(&editor.projection()), "HeadingItem!");
252}
253
254#[test]
255fn rich_paste_rule_retains_suffix_as_separate_paragraph() {
256 let mut editor = Editor::new("paste-rule", 1102);
257 paragraph(&mut editor, "AB");
258 editor.command(&json!({"op":"display_paste","at":1,"length":0,"blocks":[{"type":"horizontal_rule"}]})).unwrap();
259 let p = editor.projection();
260 assert_eq!(p["children"].as_array().unwrap().len(),3);
261 assert_eq!(plain(&p["children"][0]), "A");
262 assert_eq!(p["children"][1]["type"], "horizontal_rule");
263 assert_eq!(plain(&p["children"][2]), "B");
264 editor.command(&json!({"op":"display_edit","at":4,"length":0,"text":"!"})).unwrap();
265 assert_eq!(plain(&editor.projection()), "A!B");
266}
267
268#[test]
269fn input_rules_transform_and_backspace_restore_typed_prefix() {
270 for (prefix, kind, level) in [("-", "bullet_list", 0), ("7.", "ordered_list", 0), ("###", "heading", 3), (">", "blockquote", 0)] {
271 let mut editor = Editor::new("input-rule", 1201);
272 paragraph(&mut editor, prefix);
273 editor.command(&json!({"op":"display_autoformat","at":0,"length":prefix.len(),"kind":kind,"level":level})).unwrap();
274 assert_eq!(editor.projection()["children"][0]["type"], kind);
275 assert_eq!(plain(&editor.projection()), "");
276 editor.command(&json!({"op":"display_revert_rule","at":0,"kind":kind,"prior_type":"paragraph","prior_attrs":{},"text":format!("{prefix} ")})).unwrap();
277 assert_eq!(editor.projection()["children"][0]["type"], "paragraph");
278 assert_eq!(plain(&editor.projection()), format!("{prefix} "));
279 }
280}
281
282#[test]
283fn merged_pending_updates_preserve_dependencies_deletes_and_duplicate_delivery() {
284 use yrs::updates::decoder::{Decode, Decoder, DecoderV1};
285 use yrs::Update;
286 let mut writer = Editor::new("batch", 201);
287 let (_, node) = paragraph(&mut writer, "original");
288 let initial = writer.snapshot();
289 let mut bytes = vec![253];
290 bytes.extend(writer.command(&json!({"op":"replace_text","node":node,"at":8,"length":0,"text":" added"})).unwrap());
291 bytes.extend(writer.command(&json!({"op":"replace_text","node":node,"at":0,"length":3,"text":"new"})).unwrap());
292 bytes.extend(writer.command(&json!({"op":"format","node":node,"at":0,"length":3,"marks":{"strong":{}}})).unwrap());
293 let merged = docs_native_crdt::dispatch(&bytes).unwrap();
294 // A POST must contain exactly one Yjs v1 update, not concatenated messages.
295 let mut decoder = DecoderV1::from(merged.as_slice());
296 Update::decode(&mut decoder).unwrap();
297 assert!(decoder.read_to_end().unwrap().is_empty());
298 let receiver = Editor::new("batch", 202);
299 receiver.apply(&initial).unwrap();
300 receiver.apply(&merged).unwrap();
301 receiver.apply(&merged).unwrap();
302 assert_eq!(receiver.projection()["children"], writer.projection()["children"]);
303 assert!(docs_native_crdt::dispatch(&[253, 255]).is_err());
304}
305