// @vitest-environment jsdom import { describe, expect, it } from "vitest"; import { Schema } from "prosemirror-model"; import { schema as basicSchema } from "prosemirror-schema-basic"; import { addListNodes } from "prosemirror-schema-list"; import { DOMParser as ProseMirrorDOMParser } from "prosemirror-model"; import { recreateTransform } from "../lib/recreateTransform"; const schema = new Schema({ nodes: addListNodes(basicSchema.spec.nodes, "paragraph block*", "block"), marks: basicSchema.spec.marks, }); function htmlToDoc(html: string) { const dom = new DOMParser().parseFromString(html, "text/html"); return ProseMirrorDOMParser.fromSchema(schema).parse(dom.body); } function applyAndCompare(fromHtml: string, toHtml: string) { const fromDoc = htmlToDoc(fromHtml); const toDoc = htmlToDoc(toHtml); const tr = recreateTransform(fromDoc, toDoc); expect(tr.doc.eq(toDoc)).toBe(true); return tr; } describe("recreateTransform", () => { it("returns no steps for identical documents", () => { const doc = htmlToDoc("
Hello
"); const tr = recreateTransform(doc, doc); expect(tr.steps).toHaveLength(0); expect(tr.doc.eq(doc)).toBe(true); }); it("handles appending text", () => { const tr = applyAndCompare("Hello
", "Hello World
"); expect(tr.steps).toHaveLength(1); }); it("handles prepending text", () => { const tr = applyAndCompare("World
", "Hello World
"); expect(tr.steps).toHaveLength(1); }); it("handles text deletion", () => { const tr = applyAndCompare("Hello World
", "Hello
"); expect(tr.steps).toHaveLength(1); }); it("handles replacing text in the middle", () => { applyAndCompare("Hello World
", "Hello Jazz
"); }); it("handles complete text replacement", () => { applyAndCompare("Hello
", "Goodbye
"); }); it("handles empty to non-empty", () => { applyAndCompare("", "Hello
"); }); it("handles non-empty to empty", () => { applyAndCompare("Hello
", ""); }); it("handles adding a paragraph", () => { applyAndCompare("First
", "First
Second
"); }); it("handles removing a paragraph", () => { applyAndCompare("First
Second
", "First
"); }); it("handles changes in a middle paragraph", () => { applyAndCompare( "One
Two
Three
", "One
Changed
Three
", ); }); it("handles paragraph to list structural change", () => { applyAndCompare("Item one
", "Item one
Item
Item
"); }); it("handles adding bold mark", () => { applyAndCompare("Hello
", "Hello
"); }); it("handles removing bold mark", () => { applyAndCompare("Hello
", "Hello
"); }); it("handles changing marks from bold to italic", () => { applyAndCompare("Hello
", "Hello
"); }); it("handles nested mark changes", () => { applyAndCompare( "A bold word
", "A bold word
", ); }); it("handles emoji content", () => { applyAndCompare("Hello
", "Hello 🌍
"); }); it("handles multi-byte unicode", () => { applyAndCompare("café
", "naïve café
"); }); it("produces a valid transform that can be applied", () => { const fromDoc = htmlToDoc("Before
"); const toDoc = htmlToDoc("After
"); const tr = recreateTransform(fromDoc, toDoc); for (const step of tr.steps) { const result = step.apply(fromDoc); expect(result.failed).toBeNull(); } }); // Tests adapted from @manuscripts/prosemirror-recreate-steps test suite. // Our implementation produces a single ReplaceStep rather than granular // addMark/removeMark/replaceAround steps, so we verify the resulting // document matches rather than asserting exact step shapes. describe("mark diffs (adapted from original library)", () => { it("adds em to inline text", () => { applyAndCompare( "Before textitalicAfter text
", "Before textitalicAfter text
", ); }); it("removes strong from inline text", () => { applyAndCompare( "Before textboldAfter text
", "Before textboldAfter text
", ); }); it("adds em and strong simultaneously", () => { applyAndCompare( "Before textitalic/boldAfter text
", "Before textitalic/boldAfter text
", ); }); it("replaces em with strong", () => { applyAndCompare( "Before textstyledAfter text
", "Before textstyledAfter text
", ); }); it("replaces em with strong in different regions", () => { applyAndCompare( "Before textstyledAfter text
", "Before textstyledAfter text
", ); }); }); describe("structural diffs (adapted from original library)", () => { it("wraps paragraph in blockquote", () => { applyAndCompare( "A quoted sentence
", "", ); }); it("unwraps paragraph from blockquote", () => { applyAndCompare( "A quoted sentence
", "A quoted sentence
A quoted sentence
", ); }); it("changes heading level", () => { applyAndCompare("", "The start text
", ); }); it("replaces text across multiple nodes", () => { applyAndCompare( "The end text
", "The start text
The second text
", ); }); it("replaces multiple words in a single text node", () => { applyAndCompare( "The end text
The second sentence
", "The cat is barking at the house
", ); }); }); describe("combined content and structure changes (adapted from original library)", () => { it("changes both heading type and paragraph content", () => { applyAndCompare( "The dog is meauwing in the ship
The fish are great!
", "A different sentence.
", ); }); it("restructures from heading+paragraph to paragraphs", () => { applyAndCompare( "The fish are great!
", "Yet another first line.
With a second line that is not styled.
", ); }); }); });