import { describe, expect, it } from "vitest"; import { createElement } from "react"; import { renderToStaticMarkup } from "react-dom/server"; import { Composer, isPublishShortcut } from "@/components/Composer"; import { attachmentMeta, BULLET, COMPOSER_EMOJI, formatBytes, insertBullets } from "@/components/composer-text"; describe("insertBullets", () => { it("adds a bullet at the start of the caret's line", () => { expect(insertBullets("hello", 3, 3)).toEqual({ text: "• hello", selectionStart: 5, selectionEnd: 5 }); expect(insertBullets("one\ntwo\nthree", 5, 5).text).toBe("one\n• two\nthree"); }); it("adds a bullet to an empty text and to an empty last line", () => { expect(insertBullets("", 0, 0)).toEqual({ text: BULLET, selectionStart: 2, selectionEnd: 2 }); expect(insertBullets("a\n", 2, 2).text).toBe("a\n• "); }); it("adds a bullet to every selected line and keeps the selection on the same characters", () => { const r = insertBullets("one\ntwo\nthree", 1, 9); expect(r.text).toBe("• one\n• two\n• three"); expect(r.text.slice(r.selectionStart, r.selectionEnd)).toBe("ne\n• two\n• t"); }); it("does not bullet the next line when the selection ends right after a line break", () => { expect(insertBullets("one\ntwo", 0, 4).text).toBe("• one\ntwo"); }); it("leaves lines that already start with a bullet unchanged", () => { expect(insertBullets("• one\ntwo", 0, 9).text).toBe("• one\n• two"); expect(insertBullets("• one", 3, 3).text).toBe("• one"); }); it("changes nothing but the inserted characters", () => { const text = "**bold** _it_\n indented"; const r = insertBullets(text, 0, text.length); expect(r.text.replaceAll(BULLET, "")).toBe(text); }); }); describe("composer helpers", () => { it("offers 32 distinct emoji", () => { expect(COMPOSER_EMOJI).toHaveLength(32); expect(new Set(COMPOSER_EMOJI).size).toBe(32); }); it("describes an attachment by type and size", () => { expect(formatBytes(512)).toBe("512 B"); expect(formatBytes(2048)).toBe("2 KB"); expect(attachmentMeta({ type: "image/png", size: 2.4 * 1024 * 1024 })).toBe("Image, 2.4 MB"); expect(attachmentMeta({ type: "video/mp4", size: 18.2 * 1024 * 1024 })).toBe("Video, 18.2 MB"); }); it("publishes on Ctrl + Enter and Cmd + Enter only", () => { expect(isPublishShortcut({ key: "Enter", ctrlKey: true, metaKey: false })).toBe(true); expect(isPublishShortcut({ key: "Enter", ctrlKey: false, metaKey: true })).toBe(true); expect(isPublishShortcut({ key: "Enter", ctrlKey: false, metaKey: false })).toBe(false); expect(isPublishShortcut({ key: "a", ctrlKey: true, metaKey: false })).toBe(false); }); }); describe("Composer markup", () => { const base = { value: "", busy: false, error: false, onChange: () => {}, onSubmit: () => {} }; const html = (p: Partial[0]>) => renderToStaticMarkup(createElement(Composer, { ...base, ...p })); it("is collapsed when empty and open when holding text or a file", () => { expect(html({})).toMatch(/class="composer"/); expect(html({ value: "hi" })).toContain("composer composer-open"); const f = new File(["x"], "a.png", { type: "image/png" }); expect(html({ file: f, onFile: () => {} })).toContain("composer composer-open"); }); it("orders the toolbar: photo, video, emoji, divider, bulleted list, hint, Post", () => { const out = html({ onFile: () => {} }); const order = ["Add image", "Add video", "Add emoji", "composer-sep", "Bulleted list", "composer-kbd", "btn-flip"]; const at = order.map((s) => out.indexOf(s)); expect(at.every((i) => i >= 0)).toBe(true); expect([...at].sort((a, b) => a - b)).toEqual(at); expect(out).not.toMatch(/aria-label="(Bold|Italic)"/); }); it("shows the three Post states", () => { expect(html({})).toMatch(/btn-flip-front">Post<.*btn-flip-back[^>]*>Write first/); expect(html({ value: "hi" })).toMatch(/btn-flip-back[^>]*>PublishPosting<"); }); it("shows the attachment row with name, type, size, progress and remove", () => { const f = new File([new Uint8Array(3 * 1024)], "clip.mp4", { type: "video/mp4" }); const out = html({ file: f, onFile: () => {}, person: { name: "Jordan Rivera" } }); expect(out).toContain("clip.mp4"); expect(out).toContain("Video, 3 KB"); expect(out).toContain('role="progressbar"'); expect(out).toContain('aria-label="Remove attachment"'); expect(out).toContain("composer-att-indent"); expect(out).toContain("hex-avatar"); }); });