|
|
@@ -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++}`
|
|
|
+}
|