Files
kunthawat 2d1be52177 Emdash source with visual editor image upload fix
Fixes:
1. media.ts: wrap placeholder generation in try-catch
2. toolbar.ts: check r.ok, display error message in popover
2026-05-03 10:44:54 +07:00

325 lines
9.2 KiB
TypeScript

/**
* Block Transform Tests
*
* Tests that block transformations work correctly:
* - Transform paragraph to headings (H1, H2, H3)
* - Transform to blockquote, code block
* - Transform to bullet and ordered lists
* - Duplicate block preserves content
* - Delete block removes content
*
* These transformations are used by the BlockMenu component.
*/
import { Editor } from "@tiptap/core";
import StarterKit from "@tiptap/starter-kit";
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { blockTransforms } from "../../src/components/editor/BlockMenu";
describe("Block Transforms", () => {
let editor: Editor;
beforeEach(() => {
editor = new Editor({
extensions: [
StarterKit.configure({
heading: { levels: [1, 2, 3] },
}),
],
content: "<p>Test content</p>",
});
});
afterEach(() => {
editor.destroy();
});
describe("Transform to Paragraph", () => {
it("transforms heading to paragraph", () => {
editor.commands.setHeading({ level: 1 });
expect(editor.isActive("heading", { level: 1 })).toBe(true);
const transform = blockTransforms.find((t) => t.id === "paragraph");
transform?.transform(editor);
expect(editor.isActive("heading")).toBe(false);
expect(editor.isActive("paragraph")).toBe(true);
});
it("preserves text content when transforming to paragraph", () => {
editor.commands.setHeading({ level: 2 });
const transform = blockTransforms.find((t) => t.id === "paragraph");
transform?.transform(editor);
expect(editor.getText().trim()).toBe("Test content");
});
});
describe("Transform to Heading", () => {
it("transforms paragraph to heading 1", () => {
const transform = blockTransforms.find((t) => t.id === "heading1");
transform?.transform(editor);
expect(editor.isActive("heading", { level: 1 })).toBe(true);
});
it("transforms paragraph to heading 2", () => {
const transform = blockTransforms.find((t) => t.id === "heading2");
transform?.transform(editor);
expect(editor.isActive("heading", { level: 2 })).toBe(true);
});
it("transforms paragraph to heading 3", () => {
const transform = blockTransforms.find((t) => t.id === "heading3");
transform?.transform(editor);
expect(editor.isActive("heading", { level: 3 })).toBe(true);
});
it("preserves text content when transforming to heading", () => {
const transform = blockTransforms.find((t) => t.id === "heading1");
transform?.transform(editor);
expect(editor.getText().trim()).toBe("Test content");
});
it("can change heading level", () => {
const h1Transform = blockTransforms.find((t) => t.id === "heading1");
h1Transform?.transform(editor);
expect(editor.isActive("heading", { level: 1 })).toBe(true);
const h2Transform = blockTransforms.find((t) => t.id === "heading2");
h2Transform?.transform(editor);
expect(editor.isActive("heading", { level: 2 })).toBe(true);
expect(editor.isActive("heading", { level: 1 })).toBe(false);
});
});
describe("Transform to Blockquote", () => {
it("transforms paragraph to blockquote", () => {
const transform = blockTransforms.find((t) => t.id === "blockquote");
transform?.transform(editor);
expect(editor.isActive("blockquote")).toBe(true);
});
it("preserves text content when transforming to blockquote", () => {
const transform = blockTransforms.find((t) => t.id === "blockquote");
transform?.transform(editor);
expect(editor.getText().trim()).toBe("Test content");
});
it("toggles blockquote off when already active", () => {
const transform = blockTransforms.find((t) => t.id === "blockquote");
transform?.transform(editor);
expect(editor.isActive("blockquote")).toBe(true);
transform?.transform(editor);
expect(editor.isActive("blockquote")).toBe(false);
});
});
describe("Transform to Code Block", () => {
it("transforms paragraph to code block", () => {
const transform = blockTransforms.find((t) => t.id === "codeBlock");
transform?.transform(editor);
expect(editor.isActive("codeBlock")).toBe(true);
});
it("preserves text content when transforming to code block", () => {
const transform = blockTransforms.find((t) => t.id === "codeBlock");
transform?.transform(editor);
expect(editor.getText().trim()).toBe("Test content");
});
it("toggles code block off when already active", () => {
const transform = blockTransforms.find((t) => t.id === "codeBlock");
transform?.transform(editor);
expect(editor.isActive("codeBlock")).toBe(true);
transform?.transform(editor);
expect(editor.isActive("codeBlock")).toBe(false);
});
});
describe("Transform to Bullet List", () => {
it("transforms paragraph to bullet list", () => {
const transform = blockTransforms.find((t) => t.id === "bulletList");
transform?.transform(editor);
expect(editor.isActive("bulletList")).toBe(true);
});
it("preserves text content when transforming to bullet list", () => {
const transform = blockTransforms.find((t) => t.id === "bulletList");
transform?.transform(editor);
expect(editor.getText().trim()).toBe("Test content");
});
it("toggles bullet list off when already active", () => {
const transform = blockTransforms.find((t) => t.id === "bulletList");
transform?.transform(editor);
expect(editor.isActive("bulletList")).toBe(true);
transform?.transform(editor);
expect(editor.isActive("bulletList")).toBe(false);
});
});
describe("Transform to Ordered List", () => {
it("transforms paragraph to ordered list", () => {
const transform = blockTransforms.find((t) => t.id === "orderedList");
transform?.transform(editor);
expect(editor.isActive("orderedList")).toBe(true);
});
it("preserves text content when transforming to ordered list", () => {
const transform = blockTransforms.find((t) => t.id === "orderedList");
transform?.transform(editor);
expect(editor.getText().trim()).toBe("Test content");
});
it("can switch between bullet and ordered list", () => {
const bulletTransform = blockTransforms.find((t) => t.id === "bulletList");
bulletTransform?.transform(editor);
expect(editor.isActive("bulletList")).toBe(true);
const orderedTransform = blockTransforms.find((t) => t.id === "orderedList");
orderedTransform?.transform(editor);
expect(editor.isActive("orderedList")).toBe(true);
expect(editor.isActive("bulletList")).toBe(false);
});
});
describe("Transform metadata", () => {
it("has all required transform definitions", () => {
const expectedIds = [
"paragraph",
"heading1",
"heading2",
"heading3",
"blockquote",
"codeBlock",
"bulletList",
"orderedList",
];
for (const id of expectedIds) {
const transform = blockTransforms.find((t) => t.id === id);
expect(transform, `Transform "${id}" should exist`).toBeDefined();
expect(transform?.label, `Transform "${id}" should have a label`).toBeTruthy();
expect(transform?.icon, `Transform "${id}" should have an icon`).toBeDefined();
expect(
typeof transform?.transform,
`Transform "${id}" should have a transform function`,
).toBe("function");
}
});
});
});
describe("Block Duplicate", () => {
let editor: Editor;
beforeEach(() => {
editor = new Editor({
extensions: [
StarterKit.configure({
heading: { levels: [1, 2, 3] },
}),
],
content: "<p>First paragraph</p><p>Second paragraph</p>",
});
});
afterEach(() => {
editor.destroy();
});
it("duplicates block and preserves content", () => {
// Position cursor in first paragraph
editor.commands.setTextSelection(1);
const { selection } = editor.state;
const { $from, $to } = selection;
// Get the block node at current position
const blockStart = $from.start($from.depth);
const blockEnd = $to.end($to.depth);
// Get the content to duplicate
const slice = editor.state.doc.slice(blockStart, blockEnd);
// Insert after current block
editor
.chain()
.focus()
.command(({ tr }) => {
tr.insert(blockEnd + 1, slice.content);
return true;
})
.run();
const json = editor.getJSON();
expect(json.content?.length).toBe(3); // Now 3 paragraphs
// Check content
const texts =
json.content?.map((block) => {
if (block.type === "paragraph" && block.content?.[0]) {
return (block.content[0] as { text?: string }).text;
}
return "";
}) ?? [];
expect(texts[0]).toBe("First paragraph");
expect(texts[1]).toBe("First paragraph"); // Duplicated
expect(texts[2]).toBe("Second paragraph");
});
});
describe("Block Delete", () => {
let editor: Editor;
beforeEach(() => {
editor = new Editor({
extensions: [
StarterKit.configure({
heading: { levels: [1, 2, 3] },
}),
],
content: "<p>First paragraph</p><p>Second paragraph</p>",
});
});
afterEach(() => {
editor.destroy();
});
it("deletes block at cursor position", () => {
// Position cursor in first paragraph
editor.commands.setTextSelection(1);
editor.commands.deleteNode("paragraph");
const json = editor.getJSON();
expect(json.content?.length).toBe(1); // Now 1 paragraph
// Check remaining content
const text =
json.content?.[0]?.type === "paragraph" && json.content[0].content?.[0]
? (json.content[0].content[0] as { text?: string }).text
: "";
expect(text).toBe("Second paragraph");
});
});