From 2835fd3302c99d9b926696ff8f0ccd67f1149d09 Mon Sep 17 00:00:00 2001 From: Sergey Kozyrenko Date: Tue, 7 Jul 2026 20:45:32 +0700 Subject: [PATCH] test(markdown-editor): close four coverage gaps found in review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - history: assert resetUndoHistory actually empties the undo stack (editor.can().undo() === false), not just that it doesn't throw — a silent no-op regression would otherwise pass. - field: assert insertAtCursor is a no-op (no onChange) when the field is disabled — the documented "a mid-save variable click can't dirty the form" guard was untested. - content-integrity: the pipe-table generative test now also asserts each pipe-bearing atom survives as an escaped code span, not just the row sentinels. - code-fence: fix the pinned-limitation comment — an upstream fix turns this test RED (the canary), it does not "flip green". Co-Authored-By: Claude Opus 4.8 --- .../markdown-editor-code-fence.test.ts | 5 +++-- .../markdown-editor-content-integrity.test.ts | 10 ++++++++++ .../markdown-editor-field.test.tsx | 19 +++++++++++++++++++ .../markdown-editor-history.test.ts | 14 ++++++++++++++ 4 files changed, 46 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/shared/markdown-editor/markdown-editor-code-fence.test.ts b/frontend/src/components/shared/markdown-editor/markdown-editor-code-fence.test.ts index 7018ab29..d1067e83 100644 --- a/frontend/src/components/shared/markdown-editor/markdown-editor-code-fence.test.ts +++ b/frontend/src/components/shared/markdown-editor/markdown-editor-code-fence.test.ts @@ -33,8 +33,9 @@ describe('code-block fence lengthening — a block documenting a ``` fence stays // delimiter by rendering it around a fixed placeholder (getMarkOpening/getMarkClosing), so the `code` mark's // real content is never seen at delimiter time and it always emits a SINGLE backtick. A code span whose // content contains a backtick therefore collapses on save and never converges. A NODE (codeBlock, above) DOES -// receive its real content and is content-aware — hence the fence widening is fixable but this is not. Pinned so the gap stays -// visible and this test flips green if a future @tiptap/markdown makes mark delimiters content-aware. +// receive its real content and is content-aware — hence the fence widening is fixable but this is not. Pinned so +// the gap stays visible: this test turns RED (the canary to revisit it) if a future @tiptap/markdown makes mark +// delimiters content-aware and the span starts round-tripping. describe('inline code containing a backtick — known lossy on save (upstream mark-delimiter limitation)', () => { it('collapses a backtick-containing code span (does not round-trip)', () => { const out = roundTrip('x ``a `b` c`` y'); diff --git a/frontend/src/components/shared/markdown-editor/markdown-editor-content-integrity.test.ts b/frontend/src/components/shared/markdown-editor/markdown-editor-content-integrity.test.ts index 9cf63f4d..5ca90bd9 100644 --- a/frontend/src/components/shared/markdown-editor/markdown-editor-content-integrity.test.ts +++ b/frontend/src/components/shared/markdown-editor/markdown-editor-content-integrity.test.ts @@ -151,12 +151,14 @@ describe('generative content-integrity — atoms survive load↔serialize across const wrap = (cells: string) => (outerPipe ? `| ${cells} |` : cells); const rows: string[] = []; const sentinels: string[] = []; + const atoms: string[] = []; for (let row = 0; row < rowCount; row++) { const atom = pipeAtoms[Math.floor(rng() * pipeAtoms.length)] as string; const sentinel = `${WORDS[Math.floor(rng() * WORDS.length)]}${row}`; sentinels.push(sentinel); + atoms.push(atom); rows.push(wrap(`\`${atom}\` | ${sentinel}`)); } @@ -168,6 +170,14 @@ describe('generative content-integrity — atoms survive load↔serialize across for (const sentinel of sentinels) { expect(out.includes(sentinel), `cell "${sentinel}" dropped (i=${i}):\n${doc}\n-->\n${out}`).toBe(true); } + + // The atom itself must survive as a code span with its structural pipes escaped for the table — the + // sentinel guards the row's cell count, this guards the pipe-bearing content from silent corruption. + for (const atom of atoms) { + const span = `\`${atom.replace(/\|/g, '\\|')}\``; + + expect(out.includes(span), `atom ${span} corrupted (i=${i}):\n${doc}\n-->\n${out}`).toBe(true); + } } }); }); diff --git a/frontend/src/components/shared/markdown-editor/markdown-editor-field.test.tsx b/frontend/src/components/shared/markdown-editor/markdown-editor-field.test.tsx index 40c19f48..a4d4d295 100644 --- a/frontend/src/components/shared/markdown-editor/markdown-editor-field.test.tsx +++ b/frontend/src/components/shared/markdown-editor/markdown-editor-field.test.tsx @@ -54,6 +54,25 @@ describe('MarkdownEditorField raw-mode handle', () => { ref.current!.insertAtCursor('{{.Bar}}'); expect(onChange).toHaveBeenCalledWith(expect.stringContaining('{{.Bar}}')); }); + + it('insertAtCursor is a no-op when the field is disabled (a mid-save variable click cannot dirty it)', () => { + const onChange = vi.fn(); + const ref = createRef(); + const { container } = render( + , + ); + + container.querySelector('textarea')!.setSelectionRange(0, 0); + ref.current!.insertAtCursor('{{.Bar}}'); + + expect(onChange).not.toHaveBeenCalled(); + }); }); describe('MarkdownEditorField rich-mode handle', () => { diff --git a/frontend/src/components/shared/markdown-editor/markdown-editor-history.test.ts b/frontend/src/components/shared/markdown-editor/markdown-editor-history.test.ts index 01f3ec5e..7c06de69 100644 --- a/frontend/src/components/shared/markdown-editor/markdown-editor-history.test.ts +++ b/frontend/src/components/shared/markdown-editor/markdown-editor-history.test.ts @@ -64,3 +64,17 @@ it('resetUndoHistory survives view.state advancing during updateState', () => { editor.destroy(); }); + +it('resetUndoHistory actually clears the undo stack', () => { + const editor = new Editor({ content: 'seed', contentType: 'markdown', extensions: createMarkdownExtensions() }); + + editor.commands.insertContent(' more'); + + expect(editor.can().undo()).toBe(true); + + resetUndoHistory(editor); + + expect(editor.can().undo()).toBe(false); + + editor.destroy(); +});