Files
edgeever/packages/shared/src/content.test.mjs
T

162 lines
5.6 KiB
JavaScript

import { describe, expect, test } from "bun:test";
import { countMemoCharacters, docToMarkdown, docToText, markdownToDoc, resolveMemoContentDoc, resolveMemoContentMarkdown, resolveMergedMemoTitle } from "./content.ts";
describe("merged memo title", () => {
test("prefers an explicit title, then the first custom source title", () => {
const sources = [{ title: "无标题笔记" }, { title: " 手动标题 " }, { title: "另一个标题" }];
expect(resolveMergedMemoTitle(" 指定标题 ", sources)).toBe("指定标题");
expect(resolveMergedMemoTitle(undefined, sources)).toBe("手动标题");
});
test("uses a dated merge title when every source is untitled", () => {
expect(resolveMergedMemoTitle(undefined, [{ title: null }, { title: "无标题笔记" }], new Date(2026, 7, 2)))
.toBe("合并笔记 2026/8/2");
});
});
describe("memo character count", () => {
test("counts punctuation while excluding whitespace and formatting", () => {
const doc = markdownToDoc("你好, **EdgeEver**!\n\n下一行");
expect(countMemoCharacters(doc)).toBe(15);
});
test("counts grapheme clusters and ignores image labels", () => {
const doc = {
type: "doc",
content: [
{ type: "paragraph", content: [{ type: "text", text: "e\u0301 👨‍👩‍👧‍👦" }] },
{ type: "image", attrs: { alt: "不计入" } },
],
};
expect(countMemoCharacters(doc)).toBe(2);
});
});
describe("Markdown table conversion", () => {
const markdown = [
"| Name | Status |",
"| --- | --- |",
"| Editor | Ready |",
"| Mobile | Planned |",
].join("\n");
test("parses a GFM table into TipTap nodes", () => {
const doc = markdownToDoc(markdown);
expect(doc.content).toHaveLength(1);
expect(doc.content[0]?.type).toBe("table");
expect(doc.content[0]?.content?.[0]?.type).toBe("tableRow");
expect(doc.content[0]?.content?.[0]?.content?.[0]?.type).toBe("tableHeader");
expect(doc.content[0]?.content?.[1]?.content?.[0]?.type).toBe("tableCell");
});
test("preserves table values through a Markdown round trip", () => {
const serialized = docToMarkdown(markdownToDoc(markdown));
const reparsed = markdownToDoc(serialized);
expect(serialized).toContain("| Name");
expect(serialized).toContain("| Editor");
expect(reparsed).toEqual(markdownToDoc(markdown));
});
test("recovers a table omitted by an older JSON schema", () => {
const legacyDoc = {
type: "doc",
content: [{ type: "paragraph", content: [{ type: "text", text: "Legacy note" }] }],
};
expect(resolveMemoContentDoc(legacyDoc, markdown).content[0]?.type).toBe("table");
expect(resolveMemoContentDoc(legacyDoc, "Legacy note")).toBe(legacyDoc);
});
});
describe("legacy Markdown body recovery", () => {
test("recovers a body when the stored JSON document is empty", () => {
const markdown = "权威和反中心化特征,即当一个人成为某种代表性的符号之后";
const resolved = resolveMemoContentDoc(markdownToDoc(""), markdown);
expect(docToText(resolved)).toContain(markdown);
});
test("recovers Markdown from rich content when the compatibility copy is empty", () => {
const richContent = markdownToDoc("这段正文只保存在富文本 JSON 中。");
expect(resolveMemoContentMarkdown(richContent, "")).toContain("这段正文只保存在富文本 JSON 中。");
});
});
describe("Nested list Markdown conversion", () => {
const markdown = [
"- Level 1",
" - Level 2",
" - Level 3",
" - Another level 2",
"- Another level 1",
].join("\n");
test("preserves nested list hierarchy through a Markdown round trip", () => {
const doc = markdownToDoc(markdown);
const topList = doc.content[0];
const firstItem = topList?.content?.[0];
const secondLevelList = firstItem?.content?.[1];
const secondLevelItem = secondLevelList?.content?.[0];
expect(topList?.type).toBe("bulletList");
expect(topList?.content).toHaveLength(2);
expect(firstItem?.type).toBe("listItem");
expect(secondLevelList?.type).toBe("bulletList");
expect(secondLevelList?.content).toHaveLength(2);
expect(secondLevelItem?.content?.[1]?.type).toBe("bulletList");
expect(secondLevelItem?.content?.[1]?.content).toHaveLength(1);
expect(docToMarkdown(doc)).toBe(markdown);
});
});
describe("Mermaid Markdown conversion", () => {
const markdown = "```mermaid\nflowchart LR\n A --> B\n```";
test("preserves Mermaid fenced code blocks through a Markdown round trip", () => {
const doc = markdownToDoc(markdown);
expect(doc.content[0]).toMatchObject({
type: "codeBlock",
attrs: { language: "mermaid" },
});
expect(docToMarkdown(doc)).toBe(markdown);
});
});
describe("Theme block compatibility", () => {
test("keeps themed blocks in the richer JSON document when Markdown is also present", () => {
const doc = {
type: "doc",
content: [{
type: "edgeeverThemeBlock",
attrs: { kind: "key-point" },
content: [{ type: "paragraph", content: [{ type: "text", text: "Important" }] }],
}],
};
expect(resolveMemoContentDoc(doc, "Important")).toBe(doc);
});
test("exports themed blocks as readable quoted Markdown instead of dropping their text", () => {
const doc = {
type: "doc",
content: [{
type: "edgeeverThemeBlock",
attrs: { kind: "intro" },
content: [{ type: "paragraph", content: [{ type: "text", text: "Read this first" }] }],
}],
};
const markdown = docToMarkdown(doc);
expect(markdown).toContain("\\[intro\\]");
expect(markdown).toContain("Read this first");
});
});