Ctrl K
ComunidadGuías…

shared/utils/ProsemirrorHelper.test.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { Schema } from "prosemirror-model";
import type { ProsemirrorData } from "../types";
import type { CommentMark } from "./ProsemirrorHelper";
import { ProsemirrorHelper } from "./ProsemirrorHelper";

const schema = new Schema({
  nodes: {
    doc: { content: "block+" },
    paragraph: { group: "block", content: "inline*" },
    heading: {
      group: "block",
      content: "inline*",
      attrs: { level: { default: 1 } },
    },
    table: { group: "block", content: "tr+" },
    tr: { content: "td+" },
    td: { content: "block+" },
    image: {
      group: "inline",
      inline: true,
      attrs: {
        src: { default: "" },
        alt: { default: null },
        marks: { default: undefined },
      },
    },
    video: {
      group: "block",
      atom: true,
      attrs: { title: { default: null } },
      leafText: (node) => node.attrs.title,
    },
    text: { group: "inline" },
  },
});

const doc = (...children: object[]) =>
  schema.nodeFromJSON({ type: "doc", content: children });

const paragraph = (...content: object[]) => ({
  type: "paragraph",
  content,
});

const image = (attrs: object) => ({ type: "image", attrs });

const heading = (title: string, level: number) => ({
  type: "heading",
  attrs: { level },
  content: [{ type: "text", text: title }],
});

const table = (...content: object[]) => ({
  type: "table",
  content: [{ type: "tr", content: [{ type: "td", content }] }],
});

describe("ProsemirrorHelper", () => {
  describe("getNodeHash", () => {
    it("returns the same hash regardless of attribute order", () => {
      const a = schema.nodeFromJSON(image({ src: "a.png", alt: "label" }));
      const b = schema.nodeFromJSON(image({ alt: "label", src: "a.png" }));

      expect(ProsemirrorHelper.getNodeHash(a)).toEqual(
        ProsemirrorHelper.getNodeHash(b)
      );
    });

    it("excludes the marks attribute from the hash", () => {
      const plain = schema.nodeFromJSON(image({ src: "a.png" }));
      const marked = schema.nodeFromJSON(
        image({
          src: "a.png",
          marks: [{ type: "comment", attrs: { id: "comment-1" } }],
        })
      );

      expect(ProsemirrorHelper.getNodeHash(plain)).toEqual(
        ProsemirrorHelper.getNodeHash(marked)
      );
    });

    it("returns different hashes for different attributes", () => {
      const a = schema.nodeFromJSON(image({ src: "a.png" }));
      const b = schema.nodeFromJSON(image({ src: "b.png" }));

      expect(ProsemirrorHelper.getNodeHash(a)).not.toEqual(
        ProsemirrorHelper.getNodeHash(b)
      );
    });
  });

  describe("findNodeByHash", () => {
    it("finds a node by its hash", () => {
      const document = doc(
        paragraph({ type: "text", text: "hello" }),
        paragraph(image({ src: "a.png" }))
      );
      const hash = ProsemirrorHelper.getNodeHash(
        schema.nodeFromJSON(image({ src: "a.png" }))
      );

      const match = ProsemirrorHelper.findNodeByHash(document, hash);
      expect(match?.node.attrs.src).toEqual("a.png");
    });

    it("returns the first occurrence in document order for identical nodes", () => {
      const document = doc(
        paragraph({ type: "text", text: "hello" }),
        paragraph(image({ src: "a.png" })),
        paragraph(image({ src: "a.png" }))
      );
      const hash = ProsemirrorHelper.getNodeHash(
        schema.nodeFromJSON(image({ src: "a.png" }))
      );

      const match = ProsemirrorHelper.findNodeByHash(document, hash);
      expect(match?.node.type.name).toEqual("image");
      expect(match?.pos).toEqual(8);
    });

    it("returns null when no node matches", () => {
      const document = doc(paragraph(image({ src: "a.png" })));

      expect(ProsemirrorHelper.findNodeByHash(document, "missing")).toBeNull();
    });
  });

  describe("getAnchorTextForComment", () => {
    it("should return the anchor text for the comment", async () => {
      const commentId = "test-comment-id";
      const anchorText = "anchor text";
      const commentMarks: CommentMark[] = [
        {
          id: commentId,
          userId: "test-user-id",
          text: anchorText,
        },
        {
          id: "random-comment-id",
          userId: "test-user-id",
          text: "some random text",
        },
      ];

      const returnedAnchorText = ProsemirrorHelper.getAnchorTextForComment(
        commentMarks,
        commentId
      );

      expect(returnedAnchorText).toEqual(anchorText);
    });

    it("should return the consolidated anchor text when multiple marks are present for the comment", async () => {
      const commentId = "test-comment-id";
      const anchorTextOne = "anchor text 1";
      const anchorTextTwo = "anchor text 2";
      const commentMarks: CommentMark[] = [
        {
          id: commentId,
          userId: "test-user-id",
          text: anchorTextOne,
        },
        {
          id: commentId,
          userId: "test-user-id",
          text: anchorTextTwo,
        },
        {
          id: "random-comment-id",
          userId: "test-user-id",
          text: "some random text",
        },
      ];

      const returnedAnchorText = ProsemirrorHelper.getAnchorTextForComment(
        commentMarks,
        commentId
      );

      expect(returnedAnchorText).toEqual(`${anchorTextOne}${anchorTextTwo}`);
    });

    it("should return undefined when no comment mark matches the provided comment", async () => {
      const commentId = "test-comment-id";
      const commentMarks: CommentMark[] = [
        {
          id: "random-comment-id-1",
          userId: "test-user-id",
          text: "some random text",
        },
        {
          id: "random-comment-id-2",
          userId: "test-user-id",
          text: "some random text",
        },
      ];

      const returnedAnchorText = ProsemirrorHelper.getAnchorTextForComment(
        commentMarks,
        commentId
      );

      expect(returnedAnchorText).toBeUndefined();
    });

    it("should return undefined when comment marks are empty", async () => {
      const returnedAnchorText = ProsemirrorHelper.getAnchorTextForComment(
        [],
        "test-comment-id"
      );
      expect(returnedAnchorText).toBeUndefined();
    });
  });

  describe("everyNode", () => {
    const data = {
      type: "doc",
      content: [
        {
          type: "paragraph",
          content: [
            { type: "text", text: "plain" },
            { type: "text", text: "bold", marks: [{ type: "bold" }] },
          ],
        },
        {
          type: "bullet_list",
          content: [
            {
              type: "list_item",
              content: [{ type: "paragraph" }],
            },
          ],
        },
      ],
    } as ProsemirrorData;

    it("should return true when every node passes the predicate", () => {
      expect(ProsemirrorHelper.everyNode(data, () => true)).toBe(true);
    });

    it("should visit the root node", () => {
      expect(
        ProsemirrorHelper.everyNode(data, (node) => node.type !== "doc")
      ).toBe(false);
    });

    it("should visit nested nodes", () => {
      expect(
        ProsemirrorHelper.everyNode(data, (node) => node.type !== "list_item")
      ).toBe(false);
    });

    it("should expose marks on text nodes", () => {
      expect(
        ProsemirrorHelper.everyNode(data, (node) => !node.marks?.length)
      ).toBe(false);
    });

    it("should handle nodes without content", () => {
      expect(
        ProsemirrorHelper.everyNode({ type: "paragraph" }, () => true)
      ).toBe(true);
    });
  });

  describe("removeMarks", () => {
    it("should remove specified mark types from text nodes", () => {
      const doc: ProsemirrorData = {
        type: "doc",
        content: [
          {
            type: "paragraph",
            content: [
              {
                type: "text",
                text: "hello",
                marks: [
                  { type: "comment", attrs: { id: "c1" } },
                  { type: "bold" },
                ],
              },
            ],
          },
        ],
      };

      const result = ProsemirrorHelper.removeMarks(doc, ["comment"]);
      expect(result.content![0].content![0].marks).toEqual([{ type: "bold" }]);
    });

    it("should remove marks from nested content", () => {
      const doc: ProsemirrorData = {
        type: "doc",
        content: [
          {
            type: "blockquote",
            content: [
              {
                type: "paragraph",
                content: [
                  {
                    type: "text",
                    text: "nested",
                    marks: [{ type: "comment", attrs: { id: "c1" } }],
                  },
                ],
              },
            ],
          },
        ],
      };

      const result = ProsemirrorHelper.removeMarks(doc, ["comment"]);
      expect(result.content![0].content![0].content![0].marks).toBeUndefined();
    });

    it("should remove marks from node attrs.marks", () => {
      const doc: ProsemirrorData = {
        type: "doc",
        content: [
          {
            type: "image",
            attrs: {
              src: "test.png",
              marks: [
                { type: "comment", attrs: { id: "c1" } },
                { type: "link", attrs: { href: "url" } },
              ],
            },
          },
        ],
      };

      const result = ProsemirrorHelper.removeMarks(doc, ["comment"]);
      expect(result.content![0].attrs!.marks).toEqual([
        { type: "link", attrs: { href: "url" } },
      ]);
    });

    it("should remove multiple mark types at once", () => {
      const doc: ProsemirrorData = {
        type: "doc",
        content: [
          {
            type: "paragraph",
            content: [
              {
                type: "text",
                text: "hello",
                marks: [
                  { type: "comment", attrs: { id: "c1" } },
                  { type: "bold" },
                  { type: "highlight" },
                ],
              },
            ],
          },
        ],
      };

      const result = ProsemirrorHelper.removeMarks(doc, [
        "comment",
        "highlight",
      ]);
      expect(result.content![0].content![0].marks).toEqual([{ type: "bold" }]);
    });

    it("should leave nodes unchanged when no marks match", () => {
      const doc: ProsemirrorData = {
        type: "doc",
        content: [
          {
            type: "paragraph",
            content: [
              {
                type: "text",
                text: "hello",
                marks: [{ type: "bold" }],
              },
            ],
          },
        ],
      };

      const result = ProsemirrorHelper.removeMarks(doc, ["comment"]);
      expect(result.content![0].content![0].marks).toEqual([{ type: "bold" }]);
    });

    it("should handle nodes with no marks", () => {
      const doc: ProsemirrorData = {
        type: "doc",
        content: [
          {
            type: "paragraph",
            content: [
              {
                type: "text",
                text: "plain",
              },
            ],
          },
        ],
      };

      const result = ProsemirrorHelper.removeMarks(doc, ["comment"]);
      expect(result.content![0].content![0].marks).toBeUndefined();
    });
  });

  describe("trim", () => {
    it("removes empty paragraphs from the start and end", () => {
      const result = ProsemirrorHelper.trim(
        doc(
          paragraph(),
          paragraph({ type: "text", text: "Hello" }),
          paragraph()
        )
      );

      expect(result.childCount).toEqual(1);
      expect(result.textContent).toEqual("Hello");
    });

    it("keeps a boundary block containing an untitled video", () => {
      const result = ProsemirrorHelper.trim(
        doc(
          paragraph({ type: "text", text: "Hello" }),
          table({ type: "video" })
        )
      );

      expect(result.childCount).toEqual(2);
    });
  });

  describe("getHeadings", () => {
    it("gives each heading a unique id", () => {
      const node = doc(
        heading("Setup", 2),
        heading("Setup", 2),
        heading("Setup 1", 2)
      );

      expect(ProsemirrorHelper.getHeadings(node).map((h) => h.id)).toEqual([
        "h-setup",
        "h-setup-1",
        "h-setup-1-1",
      ]);
    });

    it("marks headings that are nested inside tables", () => {
      const node = doc(
        heading("One", 1),
        table(heading("Cell", 2)),
        heading("Two", 2)
      );

      expect(ProsemirrorHelper.getHeadings(node)).toEqual([
        expect.objectContaining({ title: "One", level: 1, inTable: false }),
        expect.objectContaining({ title: "Cell", level: 2, inTable: true }),
        expect.objectContaining({ title: "Two", level: 2, inTable: false }),
      ]);
    });

    it("marks headings after a nested table as still inside the outer table", () => {
      const node = doc(
        table(
          heading("Before", 2),
          table(heading("Inner", 3)),
          heading("After", 2)
        ),
        heading("Outside", 1)
      );

      expect(ProsemirrorHelper.getHeadings(node)).toEqual([
        expect.objectContaining({ title: "Before", inTable: true }),
        expect.objectContaining({ title: "Inner", inTable: true }),
        expect.objectContaining({ title: "After", inTable: true }),
        expect.objectContaining({ title: "Outside", inTable: false }),
      ]);
    });
  });
});