// @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

", "
  1. Item one

"); }); it("handles list to paragraph structural change", () => { applyAndCompare("", "

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

", "

A quoted sentence

", ); }); it("unwraps paragraph from blockquote", () => { applyAndCompare( "

A quoted sentence

", "

A quoted sentence

", ); }); it("changes heading level", () => { applyAndCompare("

A title

", "

A title

"); }); }); describe("text diffs (adapted from original library)", () => { it("replaces text in a single node", () => { applyAndCompare( "

The start text

", "

The end text

", ); }); it("replaces text across multiple nodes", () => { applyAndCompare( "

The start text

The second text

", "

The end text

The second sentence

", ); }); it("replaces multiple words in a single text node", () => { applyAndCompare( "

The cat is barking at the house

", "

The dog is meauwing in the ship

", ); }); }); describe("combined content and structure changes (adapted from original library)", () => { it("changes both heading type and paragraph content", () => { applyAndCompare( "

The title

The fish are great!

", "

A different title

A different sentence.

", ); }); it("restructures from heading+paragraph to paragraphs", () => { applyAndCompare( "

The title

The fish are great!

", "

Yet another first line.

With a second line that is not styled.

", ); }); }); });