mirror of
https://github.com/oxynote/oxynote.git
synced 2026-10-17 19:09:19 +02:00
110 lines
2.9 KiB
TypeScript
110 lines
2.9 KiB
TypeScript
import type { JSONContent } from "@tiptap/core"
|
|
import { describe, expect, it } from "vitest"
|
|
import { computeTokenDiff } from "./diff-ops"
|
|
import type { DiffToken } from "./diff-tokens"
|
|
|
|
// builds one token per code point, all sharing the same marks array —
|
|
// exactly how tokens coming from a single text node look
|
|
function tokens(text: string, marks: JSONContent[] = []): DiffToken[] {
|
|
// eslint-disable-next-line @typescript-eslint/no-misused-spread -- code point granularity is exactly what DiffToken uses
|
|
return [...text].map((ch) => ({ text: ch, marks }))
|
|
}
|
|
|
|
// compresses ops into [type, text] pairs for readable assertions
|
|
function shape(oldText: string, newText: string): [string, string][]
|
|
function shape(
|
|
oldTokens: DiffToken[],
|
|
newTokens: DiffToken[],
|
|
): [string, string][]
|
|
function shape(
|
|
oldInput: string | DiffToken[],
|
|
newInput: string | DiffToken[],
|
|
): [string, string][] {
|
|
const oldTokens = typeof oldInput === "string" ? tokens(oldInput) : oldInput
|
|
const newTokens = typeof newInput === "string" ? tokens(newInput) : newInput
|
|
|
|
return computeTokenDiff(oldTokens, newTokens).map((op) => [
|
|
op.type,
|
|
op.tokens.map((t) => t.text).join(""),
|
|
])
|
|
}
|
|
|
|
describe("computeTokenDiff", () => {
|
|
it.for([
|
|
{
|
|
name: "returns one equal op for identical text",
|
|
oldText: "hello world",
|
|
newText: "hello world",
|
|
expected: [["equal", "hello world"]],
|
|
},
|
|
{
|
|
name: "returns no ops for empty inputs",
|
|
oldText: "",
|
|
newText: "",
|
|
expected: [],
|
|
},
|
|
{
|
|
name: "emits an insert for appended text",
|
|
oldText: "hello",
|
|
newText: "hello world",
|
|
expected: [
|
|
["equal", "hello"],
|
|
["insert", " world"],
|
|
],
|
|
},
|
|
{
|
|
name: "emits a delete for removed text",
|
|
oldText: "hello world",
|
|
newText: "hello",
|
|
expected: [
|
|
["equal", "hello"],
|
|
["delete", " world"],
|
|
],
|
|
},
|
|
{
|
|
name: "emits a delete and insert pair for a replaced word",
|
|
oldText: "hello world",
|
|
newText: "goodbye world",
|
|
expected: [
|
|
["delete", "hello"],
|
|
["insert", "goodbye"],
|
|
["equal", " world"],
|
|
],
|
|
},
|
|
{
|
|
name: "treats an emoji as a single unit",
|
|
oldText: "👍",
|
|
newText: "👍!",
|
|
expected: [
|
|
["equal", "👍"],
|
|
["insert", "!"],
|
|
],
|
|
},
|
|
])("$name", ({ oldText, newText, expected }, { expect }) => {
|
|
expect(shape(oldText, newText)).toEqual(expected)
|
|
})
|
|
|
|
it("emits a delete and insert pair when only marks change", () => {
|
|
const bold = [{ type: "bold" }]
|
|
const oldTokens = [...tokens("plain "), ...tokens("word")]
|
|
const newTokens = [...tokens("plain "), ...tokens("word", bold)]
|
|
|
|
expect(shape(oldTokens, newTokens)).toEqual([
|
|
["equal", "plain "],
|
|
["delete", "word"],
|
|
["insert", "word"],
|
|
])
|
|
})
|
|
|
|
it("expands a single-character mark change to the whole word", () => {
|
|
const bold = [{ type: "bold" }]
|
|
const oldTokens = tokens("abc")
|
|
const newTokens = [...tokens("a"), ...tokens("b", bold), ...tokens("c")]
|
|
|
|
expect(shape(oldTokens, newTokens)).toEqual([
|
|
["delete", "abc"],
|
|
["insert", "abc"],
|
|
])
|
|
})
|
|
})
|