// @vitest-environment node // No DOM needed — the helpers are pure (vitest.config.ts defaults to jsdom, // which this workspace does not install; node-env tests are bannered, matching // the other web unit tests). /** * TASK-0136 — the empty-text-block predicates that back BOTH halves of the * blank-area fix (click-reuse + delete-on-blur): * * - `isAutoCleanableType` — only the inline-text types (a toggle is * deliberately EXCLUDED: it can own children even with an empty title). * - `isContentEmpty` — the two content shapes in the wild (Tiptap HTML * string / `{ text }`|`{ title }` envelope) plus the `{}` fresh-create * default; anything unrecognized is NON-empty (a false negative leaves a * block around; a false positive deletes user data). * - `isEmptyTextBlock` — the combined guard, with the page-TITLE block * protected via `properties.isTitle`. */ import { describe, expect, it } from "vitest"; import { isAutoCleanableType, isContentEmpty, isEmptyTextBlock, type BlockLikeForEmptiness, } from "../block-content"; const block = (over: Partial & { type: string; content: unknown }): BlockLikeForEmptiness => ({ type: over.type, content: over.content, properties: over.properties ?? null, }); describe("isAutoCleanableType", () => { it("accepts the inline-text types", () => { for (const t of [ "paragraph", "heading_1", "heading_2", "heading_3", "bulleted_list", "numbered_list", "quote", "callout", "todo", ]) { expect(isAutoCleanableType(t), t).toBe(true); } }); it("rejects structural / media types", () => { for (const t of [ "image", "video", "audio", "file", "code", "divider", "spacer", "toggle", "column_list", "column", "table", "chart", "form", "database", "linked_database", "child_database", "synced_block", "child_page", "bookmark", "embed", "equation", ]) { expect(isAutoCleanableType(t), t).toBe(false); } }); it("rejects unknown / missing types", () => { expect(isAutoCleanableType("nope")).toBe(false); expect(isAutoCleanableType(undefined)).toBe(false); expect(isAutoCleanableType(null)).toBe(false); }); }); describe("isContentEmpty", () => { it("treats null / undefined as empty (nothing stored)", () => { expect(isContentEmpty(null)).toBe(true); expect(isContentEmpty(undefined)).toBe(true); }); it("treats empty Tiptap HTML shapes as empty", () => { expect(isContentEmpty("")).toBe(true); expect(isContentEmpty("

")).toBe(true); expect(isContentEmpty("

")).toBe(true); expect(isContentEmpty("


")).toBe(true); expect(isContentEmpty(" \n\t ")).toBe(true); }); it("treats non-empty Tiptap HTML as non-empty", () => { expect(isContentEmpty("

hi

")).toBe(false); expect(isContentEmpty("

a b c

")).toBe(false); expect(isContentEmpty('

t

')).toBe(false); }); it("treats { text } / { title } envelopes by their text", () => { expect(isContentEmpty({ text: "" })).toBe(true); expect(isContentEmpty({ text: " " })).toBe(true); expect(isContentEmpty({ title: "" })).toBe(true); expect(isContentEmpty({ title: " " })).toBe(true); expect(isContentEmpty({ text: "x" })).toBe(false); expect(isContentEmpty({ title: "x" })).toBe(false); }); it("treats the freshly-created default {} as empty (no keys → nothing to lose)", () => { expect(isContentEmpty({})).toBe(true); }); it("treats unrecognized object shapes as NON-empty (never guess)", () => { expect(isContentEmpty({ text: 42 })).toBe(false); expect(isContentEmpty({ checked: true })).toBe(false); expect(isContentEmpty({ text: { nested: true } })).toBe(false); expect(isContentEmpty({ a: 1, b: 2 })).toBe(false); expect(isContentEmpty(["x"])).toBe(false); expect(isContentEmpty([])).toBe(false); }); it("treats numbers / booleans as NON-empty", () => { expect(isContentEmpty(42)).toBe(false); expect(isContentEmpty(true)).toBe(false); }); }); describe("isEmptyTextBlock", () => { it("is true for an empty text block", () => { expect(isEmptyTextBlock(block({ type: "paragraph", content: "" }))).toBe(true); expect(isEmptyTextBlock(block({ type: "paragraph", content: "

" }))).toBe(true); expect(isEmptyTextBlock(block({ type: "paragraph", content: {} }))).toBe(true); expect(isEmptyTextBlock(block({ type: "paragraph", content: { text: "" } }))).toBe(true); expect(isEmptyTextBlock(block({ type: "heading_2", content: null }))).toBe(true); expect(isEmptyTextBlock(block({ type: "todo", content: "

" }))).toBe(true); }); it("is false when the block has content", () => { expect(isEmptyTextBlock(block({ type: "paragraph", content: "

hi

" }))).toBe(false); expect(isEmptyTextBlock(block({ type: "paragraph", content: { text: "x" } }))).toBe(false); }); it("is false for non-text types even when their content is empty", () => { expect(isEmptyTextBlock(block({ type: "image", content: "" }))).toBe(false); expect(isEmptyTextBlock(block({ type: "divider", content: "" }))).toBe(false); expect(isEmptyTextBlock(block({ type: "code", content: "" }))).toBe(false); expect(isEmptyTextBlock(block({ type: "toggle", content: "" }))).toBe(false); }); it("protects the page-title block (properties.isTitle) even when empty", () => { expect( isEmptyTextBlock( block({ type: "paragraph", content: "", properties: { isTitle: true } }), ), ).toBe(false); expect( isEmptyTextBlock( block({ type: "paragraph", content: { text: "" }, properties: { isTitle: true } }), ), ).toBe(false); }); it("ignores isTitle: false / missing (only === true protects)", () => { expect(isEmptyTextBlock(block({ type: "paragraph", content: "", properties: { isTitle: false } }))).toBe(true); expect(isEmptyTextBlock(block({ type: "paragraph", content: "", properties: null }))).toBe(true); }); });