|
|
@@ -1,8 +1,9 @@
|
|
|
-import { describe, expect, it, vi } from "vitest"
|
|
|
-import { ref } from "vue"
|
|
|
import { EditorState } from "prosemirror-state"
|
|
|
import { EditorView } from "prosemirror-view"
|
|
|
+import { describe, expect, it, vi } from "vitest"
|
|
|
+import { ref } from "vue"
|
|
|
import { stableId } from "@/__test-utils__"
|
|
|
+import { createPasteHandler } from "@/composables/usePasteHandler"
|
|
|
import type { PatternOpenPayload } from "@/composables/usePatternPlugin"
|
|
|
import {
|
|
|
buildMentionGroups,
|
|
|
@@ -10,13 +11,11 @@ import {
|
|
|
useMentionGroups,
|
|
|
useSlashGroups,
|
|
|
} from "@/composables/useSuggestionGroups"
|
|
|
+import { contentToDoc } from "@/lib/content-model"
|
|
|
+import { insertBlock, moveBlock } from "@/lib/editor-operations"
|
|
|
import type { FormattingHandlers } from "@/lib/formatting"
|
|
|
-import { createOperationHistory } from "@/lib/operation-history"
|
|
|
-import { createPasteHandler } from "@/composables/usePasteHandler"
|
|
|
-import { insertBlock } from "@/lib/editor-operations"
|
|
|
-import { moveBlock } from "@/lib/editor-operations"
|
|
|
import { classifyBlock } from "@/lib/markdown-rules/block-classifier"
|
|
|
-import { contentToDoc } from "@/lib/content-model"
|
|
|
+import { createOperationHistory } from "@/lib/operation-history"
|
|
|
import { schema } from "@/lib/schema"
|
|
|
|
|
|
function createMockSession(
|
|
|
@@ -69,7 +68,7 @@ describe("Editor integration — menu routing", () => {
|
|
|
expect(activeSession.value).toBeNull()
|
|
|
onPatternOpen(createMockSession("command"))
|
|
|
expect(activeSession.value).not.toBeNull()
|
|
|
- expect(activeSession.value!.kind).toBe("command")
|
|
|
+ expect(activeSession.value?.kind).toBe("command")
|
|
|
onPatternClose()
|
|
|
expect(activeSession.value).toBeNull()
|
|
|
})
|
|
|
@@ -112,9 +111,9 @@ describe("Editor integration — menu routing", () => {
|
|
|
position: { x: 50, y: 100 },
|
|
|
range: { from: 0, to: 9 },
|
|
|
})
|
|
|
- expect(activeSession.value!.query).toBe("/newquery")
|
|
|
- expect(activeSession.value!.position).toEqual({ x: 50, y: 100 })
|
|
|
- expect(activeSession.value!.range).toEqual({ from: 0, to: 9 })
|
|
|
+ expect(activeSession.value?.query).toBe("/newquery")
|
|
|
+ expect(activeSession.value?.position).toEqual({ x: 50, y: 100 })
|
|
|
+ expect(activeSession.value?.range).toEqual({ from: 0, to: 9 })
|
|
|
})
|
|
|
})
|
|
|
|
|
|
@@ -123,7 +122,7 @@ describe("content-change-op coalescing", () => {
|
|
|
const history = createOperationHistory()
|
|
|
const COALESCE_MS = 500
|
|
|
let canUndo = false
|
|
|
- let canRedo = false
|
|
|
+ let _canRedo = false
|
|
|
|
|
|
function handleOp(op: {
|
|
|
blockId: string
|
|
|
@@ -154,15 +153,40 @@ describe("content-change-op coalescing", () => {
|
|
|
})
|
|
|
}
|
|
|
canUndo = history.canUndo
|
|
|
- canRedo = history.canRedo
|
|
|
+ _canRedo = history.canRedo
|
|
|
}
|
|
|
|
|
|
// Rapid typing — should coalesce into 1 entry
|
|
|
- handleOp({ blockId: "a", previousContent: "", newContent: "h", timestamp: 1000 })
|
|
|
- handleOp({ blockId: "a", previousContent: "h", newContent: "he", timestamp: 1050 })
|
|
|
- handleOp({ blockId: "a", previousContent: "he", newContent: "hel", timestamp: 1120 })
|
|
|
- handleOp({ blockId: "a", previousContent: "hel", newContent: "hell", timestamp: 1180 })
|
|
|
- handleOp({ blockId: "a", previousContent: "hell", newContent: "hello", timestamp: 1250 })
|
|
|
+ handleOp({
|
|
|
+ blockId: "a",
|
|
|
+ previousContent: "",
|
|
|
+ newContent: "h",
|
|
|
+ timestamp: 1000,
|
|
|
+ })
|
|
|
+ handleOp({
|
|
|
+ blockId: "a",
|
|
|
+ previousContent: "h",
|
|
|
+ newContent: "he",
|
|
|
+ timestamp: 1050,
|
|
|
+ })
|
|
|
+ handleOp({
|
|
|
+ blockId: "a",
|
|
|
+ previousContent: "he",
|
|
|
+ newContent: "hel",
|
|
|
+ timestamp: 1120,
|
|
|
+ })
|
|
|
+ handleOp({
|
|
|
+ blockId: "a",
|
|
|
+ previousContent: "hel",
|
|
|
+ newContent: "hell",
|
|
|
+ timestamp: 1180,
|
|
|
+ })
|
|
|
+ handleOp({
|
|
|
+ blockId: "a",
|
|
|
+ previousContent: "hell",
|
|
|
+ newContent: "hello",
|
|
|
+ timestamp: 1250,
|
|
|
+ })
|
|
|
|
|
|
expect(canUndo).toBe(true)
|
|
|
expect(history.peek()?.type).toBe("set-block-content")
|
|
|
@@ -211,9 +235,19 @@ describe("content-change-op coalescing", () => {
|
|
|
}
|
|
|
|
|
|
// First burst
|
|
|
- handleOp({ blockId: "a", previousContent: "", newContent: "hi", timestamp: 1000 })
|
|
|
+ handleOp({
|
|
|
+ blockId: "a",
|
|
|
+ previousContent: "",
|
|
|
+ newContent: "hi",
|
|
|
+ timestamp: 1000,
|
|
|
+ })
|
|
|
// Pause > 500ms
|
|
|
- handleOp({ blockId: "a", previousContent: "hi", newContent: "hi there", timestamp: 1600 })
|
|
|
+ handleOp({
|
|
|
+ blockId: "a",
|
|
|
+ previousContent: "hi",
|
|
|
+ newContent: "hi there",
|
|
|
+ timestamp: 1600,
|
|
|
+ })
|
|
|
|
|
|
// Two separate entries
|
|
|
const first = history.undo()
|
|
|
@@ -258,8 +292,18 @@ describe("content-change-op coalescing", () => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- handleOp({ blockId: "a", previousContent: "", newContent: "hello", timestamp: 1000 })
|
|
|
- handleOp({ blockId: "b", previousContent: "", newContent: "world", timestamp: 1100 })
|
|
|
+ handleOp({
|
|
|
+ blockId: "a",
|
|
|
+ previousContent: "",
|
|
|
+ newContent: "hello",
|
|
|
+ timestamp: 1000,
|
|
|
+ })
|
|
|
+ handleOp({
|
|
|
+ blockId: "b",
|
|
|
+ previousContent: "",
|
|
|
+ newContent: "world",
|
|
|
+ timestamp: 1100,
|
|
|
+ })
|
|
|
|
|
|
// Two separate entries (different block)
|
|
|
const first = history.undo()
|
|
|
@@ -304,7 +348,12 @@ describe("content-change-op coalescing", () => {
|
|
|
}
|
|
|
|
|
|
// 1. Type "hello" in block A
|
|
|
- handleContentOp({ blockId: "a", previousContent: "", newContent: "hello", timestamp: 1000 })
|
|
|
+ handleContentOp({
|
|
|
+ blockId: "a",
|
|
|
+ previousContent: "",
|
|
|
+ newContent: "hello",
|
|
|
+ timestamp: 1000,
|
|
|
+ })
|
|
|
|
|
|
// 2. Split at index 0 (creates block B with "world")
|
|
|
history.execute({
|
|
|
@@ -318,12 +367,17 @@ describe("content-change-op coalescing", () => {
|
|
|
})
|
|
|
|
|
|
// 3. Type " test" in block B
|
|
|
- handleContentOp({ blockId: "b", previousContent: "", newContent: " test", timestamp: 2100 })
|
|
|
+ handleContentOp({
|
|
|
+ blockId: "b",
|
|
|
+ previousContent: "",
|
|
|
+ newContent: " test",
|
|
|
+ timestamp: 2100,
|
|
|
+ })
|
|
|
|
|
|
// Step 3 undo — revert block B typing
|
|
|
const undo1 = history.undo()
|
|
|
expect(undo1).not.toBeNull()
|
|
|
- expect(undo1!.type).toBe("set-block-content")
|
|
|
+ expect(undo1?.type).toBe("set-block-content")
|
|
|
expect(undo1).toHaveProperty("blockId", "b")
|
|
|
expect(undo1).toHaveProperty("newContent", "")
|
|
|
expect(history.canUndo).toBe(true)
|
|
|
@@ -331,13 +385,13 @@ describe("content-change-op coalescing", () => {
|
|
|
// Step 2 undo — revert the split (merge block B back into block A)
|
|
|
const undo2 = history.undo()
|
|
|
expect(undo2).not.toBeNull()
|
|
|
- expect(undo2!.type).toBe("merge-block")
|
|
|
+ expect(undo2?.type).toBe("merge-block")
|
|
|
expect(history.canUndo).toBe(true)
|
|
|
|
|
|
// Step 1 undo — revert block A typing
|
|
|
const undo3 = history.undo()
|
|
|
expect(undo3).not.toBeNull()
|
|
|
- expect(undo3!.type).toBe("set-block-content")
|
|
|
+ expect(undo3?.type).toBe("set-block-content")
|
|
|
expect(undo3).toHaveProperty("blockId", "a")
|
|
|
expect(undo3).toHaveProperty("newContent", "")
|
|
|
expect(history.canUndo).toBe(false)
|
|
|
@@ -494,7 +548,7 @@ describe("suggestion menu selection", () => {
|
|
|
.find((item) => item.label === "Heading 1")
|
|
|
|
|
|
expect(heading1).toBeDefined()
|
|
|
- heading1!.onSelect()
|
|
|
+ heading1?.onSelect()
|
|
|
|
|
|
expect(respond).toHaveBeenCalledWith({ text: "# ", mode: "replace" })
|
|
|
expect(closeSession).toHaveBeenCalledWith("completed")
|
|
|
@@ -523,7 +577,7 @@ describe("suggestion menu selection", () => {
|
|
|
|
|
|
expect(firstPage).toBeDefined()
|
|
|
expect(groups[0].id).toBe("pages")
|
|
|
- firstPage!.onSelect()
|
|
|
+ firstPage?.onSelect()
|
|
|
|
|
|
expect(respond).toHaveBeenCalledWith({
|
|
|
text: "[[Getting Started]]",
|