1
2
3
4
5
18import { describe, expect, it } from "vitest";
19import {
20 isAutoCleanableType,
21 isContentEmpty,
22 isEmptyTextBlock,
23 type BlockLikeForEmptiness,
24} from "../block-content";
25
26const block = (over: Partial<BlockLikeForEmptiness> & { type: string; content: unknown }): BlockLikeForEmptiness => ({
27 type: over.type,
28 content: over.content,
29 properties: over.properties ?? null,
30});
31
32describe("isAutoCleanableType", () => {
33 it("accepts the inline-text types", () => {
34 for (const t of [
35 "paragraph",
36 "heading_1",
37 "heading_2",
38 "heading_3",
39 "bulleted_list",
40 "numbered_list",
41 "quote",
42 "callout",
43 "todo",
44 ]) {
45 expect(isAutoCleanableType(t), t).toBe(true);
46 }
47 });
48
49 it("rejects structural / media types", () => {
50 for (const t of [
51 "image",
52 "video",
53 "audio",
54 "file",
55 "code",
56 "divider",
57 "spacer",
58 "toggle",
59 "column_list",
60 "column",
61 "table",
62 "chart",
63 "form",
64 "database",
65 "linked_database",
66 "child_database",
67 "synced_block",
68 "child_page",
69 "bookmark",
70 "embed",
71 "equation",
72 ]) {
73 expect(isAutoCleanableType(t), t).toBe(false);
74 }
75 });
76
77 it("rejects unknown / missing types", () => {
78 expect(isAutoCleanableType("nope")).toBe(false);
79 expect(isAutoCleanableType(undefined)).toBe(false);
80 expect(isAutoCleanableType(null)).toBe(false);
81 });
82});
83
84describe("isContentEmpty", () => {
85 it("treats null / undefined as empty (nothing stored)", () => {
86 expect(isContentEmpty(null)).toBe(true);
87 expect(isContentEmpty(undefined)).toBe(true);
88 });
89
90 it("treats empty Tiptap HTML shapes as empty", () => {
91 expect(isContentEmpty("")).toBe(true);
92 expect(isContentEmpty("<p></p>")).toBe(true);
93 expect(isContentEmpty("<p> </p>")).toBe(true);
94 expect(isContentEmpty("<p><br></p>")).toBe(true);
95 expect(isContentEmpty(" \n\t ")).toBe(true);
96 });
97
98 it("treats non-empty Tiptap HTML as non-empty", () => {
99 expect(isContentEmpty("<p>hi</p>")).toBe(false);
100 expect(isContentEmpty("<p>a <b>b</b> c</p>")).toBe(false);
101 expect(isContentEmpty('<h1>t</h1>')).toBe(false);
102 });
103
104 it("treats { text } / { title } envelopes by their text", () => {
105 expect(isContentEmpty({ text: "" })).toBe(true);
106 expect(isContentEmpty({ text: " " })).toBe(true);
107 expect(isContentEmpty({ title: "" })).toBe(true);
108 expect(isContentEmpty({ title: " " })).toBe(true);
109 expect(isContentEmpty({ text: "x" })).toBe(false);
110 expect(isContentEmpty({ title: "x" })).toBe(false);
111 });
112
113 it("treats the freshly-created default {} as empty (no keys → nothing to lose)", () => {
114 expect(isContentEmpty({})).toBe(true);
115 });
116
117 it("treats unrecognized object shapes as NON-empty (never guess)", () => {
118 expect(isContentEmpty({ text: 42 })).toBe(false);
119 expect(isContentEmpty({ checked: true })).toBe(false);
120 expect(isContentEmpty({ text: { nested: true } })).toBe(false);
121 expect(isContentEmpty({ a: 1, b: 2 })).toBe(false);
122 expect(isContentEmpty(["x"])).toBe(false);
123 expect(isContentEmpty([])).toBe(false);
124 });
125
126 it("treats numbers / booleans as NON-empty", () => {
127 expect(isContentEmpty(42)).toBe(false);
128 expect(isContentEmpty(true)).toBe(false);
129 });
130});
131
132describe("isEmptyTextBlock", () => {
133 it("is true for an empty text block", () => {
134 expect(isEmptyTextBlock(block({ type: "paragraph", content: "" }))).toBe(true);
135 expect(isEmptyTextBlock(block({ type: "paragraph", content: "<p></p>" }))).toBe(true);
136 expect(isEmptyTextBlock(block({ type: "paragraph", content: {} }))).toBe(true);
137 expect(isEmptyTextBlock(block({ type: "paragraph", content: { text: "" } }))).toBe(true);
138 expect(isEmptyTextBlock(block({ type: "heading_2", content: null }))).toBe(true);
139 expect(isEmptyTextBlock(block({ type: "todo", content: "<p> </p>" }))).toBe(true);
140 });
141
142 it("is false when the block has content", () => {
143 expect(isEmptyTextBlock(block({ type: "paragraph", content: "<p>hi</p>" }))).toBe(false);
144 expect(isEmptyTextBlock(block({ type: "paragraph", content: { text: "x" } }))).toBe(false);
145 });
146
147 it("is false for non-text types even when their content is empty", () => {
148 expect(isEmptyTextBlock(block({ type: "image", content: "" }))).toBe(false);
149 expect(isEmptyTextBlock(block({ type: "divider", content: "" }))).toBe(false);
150 expect(isEmptyTextBlock(block({ type: "code", content: "" }))).toBe(false);
151 expect(isEmptyTextBlock(block({ type: "toggle", content: "" }))).toBe(false);
152 });
153
154 it("protects the page-title block (properties.isTitle) even when empty", () => {
155 expect(
156 isEmptyTextBlock(
157 block({ type: "paragraph", content: "", properties: { isTitle: true } }),
158 ),
159 ).toBe(false);
160 expect(
161 isEmptyTextBlock(
162 block({ type: "paragraph", content: { text: "" }, properties: { isTitle: true } }),
163 ),
164 ).toBe(false);
165 });
166
167 it("ignores isTitle: false / missing (only === true protects)", () => {
168 expect(isEmptyTextBlock(block({ type: "paragraph", content: "", properties: { isTitle: false } }))).toBe(true);
169 expect(isEmptyTextBlock(block({ type: "paragraph", content: "", properties: null }))).toBe(true);
170 });
171});
172