Skip to content

Commit c5e9676

Browse files
committed
test(versioning): add tests for type changes and moves in version diffs
1 parent 23143cb commit c5e9676

2 files changed

Lines changed: 248 additions & 5 deletions

File tree

‎packages/core/src/y/extensions/nestingChanges.test.ts‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ describe("version diff of a nesting change", () => {
211211
);
212212
if (node.type.name === "blockContainer" && mark) {
213213
changed.push(
214-
`${mark.type.name === "y-attributed-insert" ? "+" : "-"}${node.firstChild!.textContent}`,
214+
`${mark.attrs["moved"] ? ">" : mark.type.name === "y-attributed-insert" ? "+" : "-"}${node.firstChild!.textContent}`,
215215
);
216216
}
217217
return true;
@@ -220,7 +220,7 @@ describe("version diff of a nesting change", () => {
220220
return changed;
221221
}
222222

223-
// To be fixed by #3168.
223+
// To be fixed by #3172.
224224
it.fails("shows an indent as a moved block, leaving the new parent unchanged", () => {
225225
expect(
226226
diffOf(
@@ -233,10 +233,10 @@ describe("version diff of a nesting change", () => {
233233
{ id: "x", type: "paragraph", content: "X" },
234234
],
235235
),
236-
).toEqual(["+X", "-X"]);
236+
).toEqual([">X"]);
237237
});
238238

239-
// To be fixed by #3168.
239+
// To be fixed by #3172.
240240
it.fails("shows an unindent as a moved block, leaving the old parent unchanged", () => {
241241
expect(
242242
diffOf(
@@ -253,6 +253,6 @@ describe("version diff of a nesting change", () => {
253253
},
254254
],
255255
),
256-
).toEqual(["-C", "+C"]);
256+
).toEqual([">C"]);
257257
});
258258
});

‎packages/core/src/y/extensions/versionDiffAttribution.test.ts‎

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,23 @@ describe("version diff of a moved block", () => {
316316
).toEqual(["[block moved]: bob", "Moved: bob"]);
317317
});
318318

319+
// To be fixed by #3172.
320+
it.fails("does not show a moved block as deleted", () => {
321+
const base = blocks();
322+
const server = history(base);
323+
// Moving "next" above "moved" reorders them (not an indent).
324+
const after = server.apply(
325+
editOf(base, 2, (editor) => {
326+
editor.setTextCursorPosition("next");
327+
editor.moveBlocksUp();
328+
}),
329+
"bob",
330+
);
331+
expect(
332+
deletions(Y.encodeStateAsUpdateV2(base), after, server.attributions),
333+
).toEqual([]);
334+
});
335+
319336
// To be fixed by #3166.
320337
it.fails("names no author for a block moved into a concurrently deleted one", () => {
321338
const base = blocks();
@@ -405,3 +422,229 @@ describe("version diff of a moved block", () => {
405422
},
406423
);
407424
});
425+
426+
describe("version diff of a type change", () => {
427+
const blocks = () =>
428+
baseDocument([
429+
{
430+
id: "parent",
431+
type: "paragraph",
432+
content: "Parent text",
433+
children: [{ id: "child", type: "paragraph", content: "Child text" }],
434+
},
435+
{ id: "next", type: "paragraph", content: "Next" },
436+
]);
437+
function toHeading(editor: BlockNoteEditor<any, any, any>) {
438+
editor.updateBlock("parent", { type: "heading" });
439+
}
440+
/** Changes as `kind text: users`, attribute changes as `attrs <node>: users`. */
441+
function diff(before: Uint8Array, after: Uint8Array, map: Y.ContentMap) {
442+
const { editor, view } = showDiff(before, after, map);
443+
const out: string[] = [];
444+
editor.prosemirrorState.doc.descendants((node) => {
445+
const replaced = node.marks.some((mark) =>
446+
["y-attributed-insert", "y-attributed-delete"].includes(mark.type.name),
447+
);
448+
for (const mark of node.marks) {
449+
if (mark.type.name === "y-attributed-attrs" && !replaced) {
450+
const users = new Set(
451+
Object.values(
452+
mark.attrs["changes"] as Record<string, { userIds: string[] }>,
453+
).flatMap((change) => change.userIds),
454+
);
455+
out.push(`attrs <${node.type.name}>: ${[...users].join(", ")}`);
456+
} else if (
457+
mark.type.name === "y-attributed-insert" ||
458+
mark.type.name === "y-attributed-delete"
459+
) {
460+
const what = node.isText ? node.text : `<${node.type.name}>`;
461+
const kind = mark.attrs["moved"] ? "moved" : mark.type.name.slice(13);
462+
out.push(`${kind} ${what}: ${mark.attrs["userIds"].join(", ")}`);
463+
}
464+
}
465+
return true;
466+
});
467+
view.close();
468+
return out;
469+
}
470+
471+
// To be fixed by #3172.
472+
it.fails("shows a type change as a formatting change, not as replaced text", () => {
473+
const base = blocks();
474+
const server = history(base);
475+
const after = server.apply(editOf(base, 2, toHeading), "bob");
476+
expect(
477+
diff(Y.encodeStateAsUpdateV2(base), after, server.attributions),
478+
).toEqual(["attrs <heading>: bob"]);
479+
});
480+
481+
// To be fixed by #3172.
482+
it.fails("credits a type-changed block's text to its writer, from before it existed", () => {
483+
const base = baseDocument([
484+
{ id: "next", type: "paragraph", content: "Next" },
485+
]);
486+
const server = history(base);
487+
server.apply(
488+
editOf(base, 1, (editor) =>
489+
editor.insertBlocks(
490+
[{ id: "parent", type: "paragraph", content: "Parent text" }],
491+
"next",
492+
"before",
493+
),
494+
),
495+
"alice",
496+
);
497+
const after = server.apply(editOf(server.server, 2, toHeading), "bob");
498+
expect(
499+
diff(Y.encodeStateAsUpdateV2(base), after, server.attributions),
500+
).toEqual([
501+
"insert <blockContainer>: alice",
502+
"insert <heading>: alice",
503+
"insert Parent text: alice",
504+
]);
505+
});
506+
507+
// To be fixed by #3172.
508+
it.fails("keeps later edits to a type-changed block as their author's", () => {
509+
const base = blocks();
510+
const server = history(base);
511+
server.apply(editOf(base, 2, toHeading), "bob");
512+
const after = server.apply(
513+
editOf(server.server, 3, (editor) => {
514+
editor.setTextCursorPosition("parent", "end");
515+
editor.insertInlineContent(" by Carol");
516+
}),
517+
"carol",
518+
);
519+
expect(
520+
diff(Y.encodeStateAsUpdateV2(base), after, server.attributions),
521+
).toEqual(["attrs <heading>: bob", "insert by Carol: carol"]);
522+
});
523+
524+
it("shows both copies when two users change the type concurrently", () => {
525+
const base = blocks();
526+
const bob = editOf(base, 2, toHeading);
527+
const carol = editOf(base, 3, (editor) =>
528+
editor.updateBlock("parent", { type: "bulletListItem" }),
529+
);
530+
const server = history(base);
531+
server.apply(bob, "bob");
532+
const after = server.apply(carol, "carol");
533+
const changes = diff(
534+
Y.encodeStateAsUpdateV2(base),
535+
after,
536+
server.attributions,
537+
);
538+
expect(changes.filter((change) => change.startsWith("insert <"))).toEqual([
539+
"insert <blockContainer>: bob",
540+
"insert <heading>: bob",
541+
"insert <blockGroup>: bob",
542+
"insert <blockContainer>: bob",
543+
"insert <paragraph>: bob",
544+
"insert <blockContainer>: carol",
545+
"insert <bulletListItem>: carol",
546+
"insert <blockGroup>: carol",
547+
"insert <blockContainer>: carol",
548+
"insert <paragraph>: carol",
549+
]);
550+
});
551+
552+
// To be fixed by #3172.
553+
it.fails("credits an indented block's text to its writer, from before it existed", () => {
554+
const base = blocks();
555+
const server = history(base);
556+
server.apply(
557+
editOf(base, 1, (editor) =>
558+
editor.insertBlocks(
559+
[{ id: "new", type: "paragraph", content: "New text" }],
560+
"next",
561+
"after",
562+
),
563+
),
564+
"alice",
565+
);
566+
const after = server.apply(
567+
editOf(server.server, 2, (editor) => {
568+
editor.setTextCursorPosition("new");
569+
editor.nestBlock();
570+
}),
571+
"bob",
572+
);
573+
expect(
574+
diff(Y.encodeStateAsUpdateV2(base), after, server.attributions),
575+
).toEqual([
576+
// Bob's indent created the parent's child group.
577+
"insert <blockGroup>: bob",
578+
"insert <blockContainer>: alice",
579+
"insert <paragraph>: alice",
580+
"insert New text: alice",
581+
]);
582+
});
583+
584+
// To be fixed by #3172.
585+
it.fails("shows a block moved among its siblings as a move", () => {
586+
const base = baseDocument([
587+
{ id: "first", type: "paragraph", content: "First" },
588+
{ id: "second", type: "paragraph", content: "Second" },
589+
]);
590+
const server = history(base);
591+
const after = server.apply(
592+
editOf(base, 2, (editor) => {
593+
editor.setTextCursorPosition("second");
594+
editor.moveBlocksUp();
595+
}),
596+
"bob",
597+
);
598+
expect(
599+
diff(Y.encodeStateAsUpdateV2(base), after, server.attributions),
600+
).toEqual(["moved <blockContainer>: bob"]);
601+
});
602+
603+
// To be fixed by #3172.
604+
it.fails("shows a type change as a formatting change after the text was rewritten", () => {
605+
const base = blocks();
606+
const server = history(base);
607+
// The rewrite reuses some characters, so the block's stored text mixes
608+
// kept characters with characters deleted before the type change.
609+
const rewritten = server.apply(
610+
editOf(base, 1, (editor) =>
611+
editor.updateBlock("parent", { content: "Parts were rewritten" }),
612+
),
613+
"alice",
614+
);
615+
const after = server.apply(editOf(server.server, 2, toHeading), "bob");
616+
expect(diff(rewritten, after, server.attributions)).toEqual([
617+
"attrs <heading>: bob",
618+
]);
619+
});
620+
621+
it.each([
622+
[
623+
"a paragraph into an image",
624+
{ type: "paragraph", content: "Some text" },
625+
{ type: "image", props: { url: "https://example.com/a.png" } },
626+
],
627+
[
628+
"an image into a paragraph",
629+
{ type: "image", props: { url: "https://example.com/a.png" } },
630+
{ type: "paragraph", content: "New text" },
631+
],
632+
])("shows turning %s as a deletion and an insertion", (_, from, to) => {
633+
const base = baseDocument([
634+
{ id: "block", ...from },
635+
{ id: "next", type: "paragraph", content: "Next" },
636+
]);
637+
const server = history(base);
638+
const after = server.apply(
639+
editOf(base, 2, (editor) => editor.updateBlock("block", to as any)),
640+
"bob",
641+
);
642+
const changes = diff(
643+
Y.encodeStateAsUpdateV2(base),
644+
after,
645+
server.attributions,
646+
);
647+
expect(changes).toContain("delete <blockContainer>: bob");
648+
expect(changes).toContain("insert <blockContainer>: bob");
649+
});
650+
});

0 commit comments

Comments
 (0)