diff --git a/apps/client/src/features/editor/gitmost/gitmost-recording.test.ts b/apps/client/src/features/editor/gitmost/gitmost-recording.test.ts index 29149822..bcd31451 100644 --- a/apps/client/src/features/editor/gitmost/gitmost-recording.test.ts +++ b/apps/client/src/features/editor/gitmost/gitmost-recording.test.ts @@ -4,19 +4,31 @@ import { Document } from "@tiptap/extension-document"; import { Paragraph } from "@tiptap/extension-paragraph"; import { Text } from "@tiptap/extension-text"; import { Heading } from "@tiptap/extension-heading"; +import { Bold } from "@tiptap/extension-bold"; +import { Italic } from "@tiptap/extension-italic"; +import { Link } from "@tiptap/extension-link"; import { gitmostInsertTranscriptIntoEditor } from "./gitmost-recording.ts"; +const ZWSP = "​"; // U+200B, the helper's block-trigger neutralizer + /** * #377 — the web-side bridge must append the native host's transcript below the * recording. These exercise the pure insert helper through a REAL Tiptap editor - * (Document/Paragraph/Text/Heading), asserting the resulting document rather - * than mocking the editor: transcript present -> "Transcript" heading + one - * paragraph per non-empty line (verbatim); absent/empty/non-string -> no-op. + * (Document/Paragraph/Text/Heading + Bold/Italic/Link marks so an HTML-parsing + * regression would be caught), asserting the resulting document rather than + * mocking the editor: transcript present -> "Transcript" heading + one paragraph + * per non-empty line; content is inserted as LITERAL TEXT (no HTML/markdown + * parsing); col-0 markdown block triggers are neutralized so git-sync keeps them + * paragraphs; absent/empty/non-string -> no-op. */ describe("gitmostInsertTranscriptIntoEditor", () => { const makeEditor = () => new Editor({ - extensions: [Document, Paragraph, Text, Heading], + // Bold/Italic/Link are registered specifically so that IF the helper ever + // regressed to inserting an HTML/markdown string (instead of a text node), + // TipTap would parse ``/`*..*`/`[..](..)` into marks and the literal- + // text assertions below would fail. + extensions: [Document, Paragraph, Text, Heading, Bold, Italic, Link], // Start from a single empty paragraph (a fresh page's baseline). The // helper appends at the end of the doc, i.e. below existing content. content: { type: "doc", content: [{ type: "paragraph" }] }, @@ -49,6 +61,73 @@ describe("gitmostInsertTranscriptIntoEditor", () => { editor.destroy(); }); + it("inserts HTML + markdown metacharacters as LITERAL text (no injection / no mark parsing)", () => { + const editor = makeEditor(); + const line = + "You: bold and *stars* and [link](x)"; + + const inserted = gitmostInsertTranscriptIntoEditor(editor, line); + expect(inserted).toBe(true); + + const paras = (editor.getJSON().content ?? []).filter( + (n: any) => n.type === "paragraph", + ) as any[]; + // The transcript line is exactly ONE paragraph holding a SINGLE text node + // whose text is the verbatim string — not split into bold/link/other nodes, + // not carrying any marks, not raw HTML. This FAILS if the helper switched to + // insertContent(htmlString): TipTap would then parse /[link](x)/*stars*. + const content = paras[paras.length - 1].content; + expect(content).toHaveLength(1); + expect(content[0].type).toBe("text"); + expect(content[0].marks ?? []).toEqual([]); + expect(content[0].text).toBe(line); + // And no bold/italic/link mark exists anywhere in the document. + const html = editor.getHTML(); + expect(html).not.toMatch(/<(strong|b|em|i|a)\b/); + // The angle brackets survived as escaped entities (literal text), not a live + //