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
This commit is contained in:
307
packages/core/tests/unit/api/content-handlers.test.ts
Normal file
307
packages/core/tests/unit/api/content-handlers.test.ts
Normal file
@@ -0,0 +1,307 @@
|
||||
import type { Kysely } from "kysely";
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
|
||||
import {
|
||||
handleContentCreate,
|
||||
handleContentDuplicate,
|
||||
handleContentGet,
|
||||
handleContentList,
|
||||
handleContentUpdate,
|
||||
} from "../../../src/api/index.js";
|
||||
import { BylineRepository } from "../../../src/database/repositories/byline.js";
|
||||
import type { Database } from "../../../src/database/types.js";
|
||||
import { SchemaRegistry } from "../../../src/schema/registry.js";
|
||||
import { setupTestDatabaseWithCollections, teardownTestDatabase } from "../../utils/test-db.js";
|
||||
|
||||
describe("Content Handlers — auto-slug generation", () => {
|
||||
let db: Kysely<Database>;
|
||||
|
||||
beforeEach(async () => {
|
||||
db = await setupTestDatabaseWithCollections();
|
||||
// Add a "name" field to the page collection so we can test name-based slug generation
|
||||
const registry = new SchemaRegistry(db);
|
||||
await registry.createField("page", { slug: "name", label: "Name", type: "string" });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await teardownTestDatabase(db);
|
||||
});
|
||||
|
||||
describe("handleContentCreate", () => {
|
||||
it("should auto-generate slug from title when slug is omitted", async () => {
|
||||
const result = await handleContentCreate(db, "post", {
|
||||
data: { title: "Hello World" },
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.item.slug).toBe("hello-world");
|
||||
});
|
||||
|
||||
it("should auto-generate slug from name when title is absent", async () => {
|
||||
const result = await handleContentCreate(db, "page", {
|
||||
data: { name: "My Widget" },
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.item.slug).toBe("my-widget");
|
||||
});
|
||||
|
||||
it("should prefer title over name for slug generation", async () => {
|
||||
const result = await handleContentCreate(db, "page", {
|
||||
data: { title: "From Title", name: "From Name" },
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.item.slug).toBe("from-title");
|
||||
});
|
||||
|
||||
it("should respect explicit slug and not auto-generate", async () => {
|
||||
const result = await handleContentCreate(db, "post", {
|
||||
data: { title: "Hello World" },
|
||||
slug: "custom-slug",
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.item.slug).toBe("custom-slug");
|
||||
});
|
||||
|
||||
it("should handle slug collisions by appending numeric suffix", async () => {
|
||||
// Create first item with the slug
|
||||
await handleContentCreate(db, "post", {
|
||||
data: { title: "Hello World" },
|
||||
});
|
||||
|
||||
// Create second item with same title — should get unique slug
|
||||
const result = await handleContentCreate(db, "post", {
|
||||
data: { title: "Hello World" },
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.item.slug).toBe("hello-world-1");
|
||||
});
|
||||
|
||||
it("should increment suffix on repeated collisions", async () => {
|
||||
await handleContentCreate(db, "post", {
|
||||
data: { title: "Hello World" },
|
||||
});
|
||||
await handleContentCreate(db, "post", {
|
||||
data: { title: "Hello World" },
|
||||
});
|
||||
|
||||
const result = await handleContentCreate(db, "post", {
|
||||
data: { title: "Hello World" },
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.item.slug).toBe("hello-world-2");
|
||||
});
|
||||
|
||||
it("should leave slug null when no title or name is present", async () => {
|
||||
// `data: {}` — no title, no name. Slug source isn't there, so the
|
||||
// auto-generator has nothing to work with.
|
||||
const result = await handleContentCreate(db, "post", {
|
||||
data: {},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.item.slug).toBeNull();
|
||||
});
|
||||
|
||||
it("should leave slug null when title is empty string", async () => {
|
||||
const result = await handleContentCreate(db, "post", {
|
||||
data: { title: "" },
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.item.slug).toBeNull();
|
||||
});
|
||||
|
||||
it("should handle unicode titles", async () => {
|
||||
const result = await handleContentCreate(db, "post", {
|
||||
data: { title: "Café Naïve" },
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.item.slug).toBe("cafe-naive");
|
||||
});
|
||||
|
||||
it("should allow same auto-slug in different collections", async () => {
|
||||
const postResult = await handleContentCreate(db, "post", {
|
||||
data: { title: "About" },
|
||||
});
|
||||
const pageResult = await handleContentCreate(db, "page", {
|
||||
data: { title: "About" },
|
||||
});
|
||||
|
||||
expect(postResult.success).toBe(true);
|
||||
expect(pageResult.success).toBe(true);
|
||||
expect(postResult.data?.item.slug).toBe("about");
|
||||
expect(pageResult.data?.item.slug).toBe("about");
|
||||
});
|
||||
|
||||
it("preserves publishedAt and createdAt when provided — content migration use case", async () => {
|
||||
const originalCreated = "2019-03-15T10:30:00.000Z";
|
||||
const originalPublished = "2019-03-16T09:00:00.000Z";
|
||||
|
||||
const result = await handleContentCreate(db, "post", {
|
||||
data: { title: "Migrated Post" },
|
||||
createdAt: originalCreated,
|
||||
publishedAt: originalPublished,
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.item.createdAt).toBe(originalCreated);
|
||||
expect(result.data?.item.publishedAt).toBe(originalPublished);
|
||||
});
|
||||
});
|
||||
|
||||
describe("handleContentDuplicate", () => {
|
||||
it("should generate slug from duplicated title", async () => {
|
||||
const original = await handleContentCreate(db, "post", {
|
||||
data: { title: "My Post" },
|
||||
slug: "my-post",
|
||||
});
|
||||
|
||||
const result = await handleContentDuplicate(db, "post", original.data!.item.id);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
// Title becomes "My Post (Copy)", slug should be generated from it
|
||||
expect(result.data?.item.slug).toBe("my-post-copy");
|
||||
});
|
||||
|
||||
it("should handle duplicate slug collision from copy", async () => {
|
||||
const original = await handleContentCreate(db, "post", {
|
||||
data: { title: "My Post" },
|
||||
slug: "my-post",
|
||||
});
|
||||
|
||||
// First duplicate
|
||||
const dup1 = await handleContentDuplicate(db, "post", original.data!.item.id);
|
||||
expect(dup1.data?.item.slug).toBe("my-post-copy");
|
||||
|
||||
// Second duplicate — "My Post (Copy)" title slugifies to "my-post-copy"
|
||||
// which now collides with the first duplicate
|
||||
const dup2 = await handleContentDuplicate(db, "post", original.data!.item.id);
|
||||
expect(dup2.success).toBe(true);
|
||||
expect(dup2.data?.item.slug).toBe("my-post-copy-1");
|
||||
});
|
||||
});
|
||||
|
||||
describe("byline hydration and assignment", () => {
|
||||
it("should assign and return bylines on create", async () => {
|
||||
const bylineRepo = new BylineRepository(db);
|
||||
const byline = await bylineRepo.create({
|
||||
slug: "author-one",
|
||||
displayName: "Author One",
|
||||
});
|
||||
|
||||
const created = await handleContentCreate(db, "post", {
|
||||
data: { title: "Bylined" },
|
||||
bylines: [{ bylineId: byline.id, roleLabel: "Writer" }],
|
||||
});
|
||||
|
||||
expect(created.success).toBe(true);
|
||||
expect(created.data?.item.primaryBylineId).toBe(byline.id);
|
||||
expect(created.data?.item.byline?.id).toBe(byline.id);
|
||||
expect(created.data?.item.bylines).toHaveLength(1);
|
||||
expect(created.data?.item.bylines?.[0]?.roleLabel).toBe("Writer");
|
||||
});
|
||||
|
||||
it("should return bylines on get and list", async () => {
|
||||
const bylineRepo = new BylineRepository(db);
|
||||
const first = await bylineRepo.create({ slug: "first", displayName: "First" });
|
||||
const second = await bylineRepo.create({ slug: "second", displayName: "Second" });
|
||||
|
||||
const created = await handleContentCreate(db, "post", {
|
||||
data: { title: "Order Test" },
|
||||
bylines: [{ bylineId: second.id }, { bylineId: first.id }],
|
||||
});
|
||||
expect(created.success).toBe(true);
|
||||
const contentId = created.data!.item.id;
|
||||
|
||||
const fetched = await handleContentGet(db, "post", contentId);
|
||||
expect(fetched.success).toBe(true);
|
||||
expect(fetched.data?.item.bylines?.[0]?.byline.id).toBe(second.id);
|
||||
expect(fetched.data?.item.bylines?.[1]?.byline.id).toBe(first.id);
|
||||
expect(fetched.data?.item.byline?.id).toBe(second.id);
|
||||
|
||||
const listed = await handleContentList(db, "post", {});
|
||||
expect(listed.success).toBe(true);
|
||||
const listedItem = listed.data?.items.find((item) => item.id === contentId);
|
||||
expect(listedItem?.byline?.id).toBe(second.id);
|
||||
expect(listedItem?.bylines?.[0]?.byline.id).toBe(second.id);
|
||||
});
|
||||
|
||||
it("should update byline ordering on update", async () => {
|
||||
const bylineRepo = new BylineRepository(db);
|
||||
const first = await bylineRepo.create({ slug: "first-upd", displayName: "First" });
|
||||
const second = await bylineRepo.create({ slug: "second-upd", displayName: "Second" });
|
||||
|
||||
const created = await handleContentCreate(db, "post", {
|
||||
data: { title: "Update Bylines" },
|
||||
bylines: [{ bylineId: first.id }, { bylineId: second.id }],
|
||||
});
|
||||
expect(created.success).toBe(true);
|
||||
|
||||
const updated = await handleContentUpdate(db, "post", created.data!.item.id, {
|
||||
bylines: [{ bylineId: second.id }, { bylineId: first.id }],
|
||||
});
|
||||
|
||||
expect(updated.success).toBe(true);
|
||||
expect(updated.data?.item.primaryBylineId).toBe(second.id);
|
||||
expect(updated.data?.item.bylines?.[0]?.byline.id).toBe(second.id);
|
||||
expect(updated.data?.item.bylines?.[1]?.byline.id).toBe(first.id);
|
||||
});
|
||||
|
||||
it("should copy bylines when duplicating", async () => {
|
||||
const bylineRepo = new BylineRepository(db);
|
||||
const byline = await bylineRepo.create({
|
||||
slug: "dup-author",
|
||||
displayName: "Dup Author",
|
||||
});
|
||||
|
||||
const original = await handleContentCreate(db, "post", {
|
||||
data: { title: "Duplicate With Bylines" },
|
||||
bylines: [{ bylineId: byline.id }],
|
||||
});
|
||||
expect(original.success).toBe(true);
|
||||
|
||||
const duplicated = await handleContentDuplicate(db, "post", original.data!.item.id);
|
||||
expect(duplicated.success).toBe(true);
|
||||
expect(duplicated.data?.item.byline?.id).toBe(byline.id);
|
||||
expect(duplicated.data?.item.bylines).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("handleContentUpdate — publishedAt override", () => {
|
||||
it("persists publishedAt when provided", async () => {
|
||||
const created = await handleContentCreate(db, "post", { data: { title: "Hi" } });
|
||||
expect(created.success).toBe(true);
|
||||
|
||||
const newPublishedAt = "2019-03-16T09:00:00.000Z";
|
||||
const updated = await handleContentUpdate(db, "post", created.data!.item.id, {
|
||||
publishedAt: newPublishedAt,
|
||||
});
|
||||
|
||||
expect(updated.success).toBe(true);
|
||||
expect(updated.data?.item.publishedAt).toBe(newPublishedAt);
|
||||
});
|
||||
|
||||
it("leaves createdAt untouched on update", async () => {
|
||||
const originalCreated = "2019-03-15T10:30:00.000Z";
|
||||
const created = await handleContentCreate(db, "post", {
|
||||
data: { title: "Hi" },
|
||||
createdAt: originalCreated,
|
||||
});
|
||||
expect(created.success).toBe(true);
|
||||
|
||||
const updated = await handleContentUpdate(db, "post", created.data!.item.id, {
|
||||
data: { title: "Edited" },
|
||||
publishedAt: "2020-01-01T00:00:00.000Z",
|
||||
});
|
||||
|
||||
expect(updated.success).toBe(true);
|
||||
expect(updated.data?.item.createdAt).toBe(originalCreated);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user