Sfoglia il codice sorgente

test(editor): add paste handling unit tests for zone and block paste paths

- Test multi-line zone paste creates one block per non-empty line
- Test empty lines are skipped during zone paste
- Test single-line paste in block does not trigger split
- Test multi-line paste with mixed block types calls onSplit
- Test classifyBlock correctly identifies callout, blockquote, task types
Zander Hawke 3 giorni fa
parent
commit
9d99703fff

+ 117 - 0
packages/editor/src/components/__tests__/editor-integration.test.ts

@@ -1,5 +1,7 @@
 import { describe, expect, it, vi } from "vitest"
 import { ref } from "vue"
+import { EditorState } from "prosemirror-state"
+import { EditorView } from "prosemirror-view"
 import type { PatternOpenPayload } from "@/composables/usePatternPlugin"
 import {
   useMentionGroups,
@@ -10,6 +12,11 @@ import {
   createOperationHistory,
   type SetBlockContentOp,
 } from "@/lib/operation-history"
+import { createPasteHandler } from "@/composables/usePasteHandler"
+import { insertBlock } from "@/lib/editor-operations"
+import { classifyBlock } from "@/lib/markdown-rules/block-classifier"
+import { contentToDoc } from "@/lib/content-model"
+import { schema } from "@/lib/schema"
 
 function createMockSession(
   kind: string = "command",
@@ -247,3 +254,113 @@ describe("content-change-op coalescing", () => {
     expect(history.undo()).not.toBeNull()
   })
 })
+
+describe("paste handling", () => {
+  it("inserts multiple blocks when pasting into zone", () => {
+    const ids = stableId("zone")
+    const blocks: Array<{ id: string; depth: number; content: string }> = [
+      { id: "a", depth: 0, content: "first" },
+    ]
+
+    // Simulate onZoneActivate with 3 non-empty lines
+    const lines = ["line1", "line2", "line3"]
+    let insertIndex = 1
+    for (const line of lines) {
+      const result = insertBlock(blocks, insertIndex, line, 0, ids)
+      blocks.length = 0
+      blocks.push(...result.blocks)
+      insertIndex++
+    }
+
+    expect(blocks).toHaveLength(4) // original 1 + 3 new
+    expect(blocks[1].content).toBe("line1")
+    expect(blocks[2].content).toBe("line2")
+    expect(blocks[3].content).toBe("line3")
+  })
+
+  it("skips empty lines during zone paste", () => {
+    const ids = stableId("zone")
+    const blocks: Array<{ id: string; depth: number; content: string }> = [
+      { id: "a", depth: 0, content: "first" },
+    ]
+
+    // Lines with empty trailing line (like onZoneActivate skips them)
+    const lines = ["line1", "", "line2"]
+    let insertIndex = 1
+    for (let i = 0; i < lines.length; i++) {
+      const line = lines[i]
+      if (line === undefined) continue
+      if (line === "" && i < lines.length - 1) continue
+      const result = insertBlock(blocks, insertIndex, line, 0, ids)
+      blocks.length = 0
+      blocks.push(...result.blocks)
+      insertIndex++
+    }
+
+    expect(blocks).toHaveLength(3)
+    expect(blocks[1].content).toBe("line1")
+    expect(blocks[2].content).toBe("line2")
+  })
+
+  it("does not split on single-line paste", () => {
+    const onSplit = vi.fn()
+    const handlePaste = createPasteHandler(onSplit)
+
+    // Single line — paste handler returns false
+    const text = "single line"
+    const doc = contentToDoc(text)
+    const state = EditorState.create({ doc, schema })
+    const dom = document.createElement("div")
+    const view = new EditorView(dom, { state })
+
+    const event = {
+      clipboardData: {
+        getData: () => "single line",
+      },
+      preventDefault: vi.fn(),
+    } as unknown as ClipboardEvent
+
+    const result = handlePaste(view, event)
+    expect(result).toBe(false)
+    expect(onSplit).not.toHaveBeenCalled()
+
+    view.destroy()
+  })
+
+  it("calls onSplit on multi-line paste with mixed types", () => {
+    const onSplit = vi.fn()
+    const handlePaste = createPasteHandler(onSplit)
+
+    const doc = contentToDoc("existing content")
+    const state = EditorState.create({ doc, schema })
+    const dom = document.createElement("div")
+    const view = new EditorView(dom, { state })
+
+    const event = {
+      clipboardData: {
+        getData: () => "para1\npara2\n> blockquote",
+      },
+      preventDefault: vi.fn(),
+    } as unknown as ClipboardEvent
+
+    const result = handlePaste(view, event)
+    expect(result).toBe(true)
+    expect(onSplit).toHaveBeenCalled()
+    expect(event.preventDefault).toHaveBeenCalled()
+
+    view.destroy()
+  })
+
+  it("classifies blockquote and callout blocks correctly", () => {
+    expect(classifyBlock("> [!NOTE] note text").kind).toBe("callout")
+    expect(classifyBlock("> blockquote text").kind).toBe("blockquote")
+    expect(classifyBlock("TODO task text").kind).toBe("task")
+    expect(classifyBlock("normal text").kind).toBe("paragraph")
+    expect(classifyBlock("").kind).toBe("paragraph")
+  })
+})
+
+function stableId(prefix: string): () => string {
+  let n = 0
+  return () => `${prefix}-${n++}`
+}