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:
311
packages/core/tests/unit/bylines/bylines-query.test.ts
Normal file
311
packages/core/tests/unit/bylines/bylines-query.test.ts
Normal file
@@ -0,0 +1,311 @@
|
||||
import type { Kysely } from "kysely";
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
|
||||
import { BylineRepository } from "../../../src/database/repositories/byline.js";
|
||||
import { ContentRepository } from "../../../src/database/repositories/content.js";
|
||||
import { UserRepository } from "../../../src/database/repositories/user.js";
|
||||
import type { Database } from "../../../src/database/types.js";
|
||||
import { SQL_BATCH_SIZE } from "../../../src/utils/chunks.js";
|
||||
import { setupTestDatabaseWithCollections, teardownTestDatabase } from "../../utils/test-db.js";
|
||||
|
||||
// Mock the loader's getDb to return our test database
|
||||
vi.mock("../../../src/loader.js", () => ({
|
||||
getDb: vi.fn(),
|
||||
}));
|
||||
|
||||
import {
|
||||
getByline,
|
||||
getBylineBySlug,
|
||||
getEntryBylines,
|
||||
getBylinesForEntries,
|
||||
} from "../../../src/bylines/index.js";
|
||||
import { getDb } from "../../../src/loader.js";
|
||||
|
||||
describe("Byline query functions", () => {
|
||||
let db: Kysely<Database>;
|
||||
let bylineRepo: BylineRepository;
|
||||
let contentRepo: ContentRepository;
|
||||
|
||||
beforeEach(async () => {
|
||||
db = await setupTestDatabaseWithCollections();
|
||||
bylineRepo = new BylineRepository(db);
|
||||
contentRepo = new ContentRepository(db);
|
||||
vi.mocked(getDb).mockResolvedValue(db);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await teardownTestDatabase(db);
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("getByline", () => {
|
||||
it("returns a byline by ID", async () => {
|
||||
const created = await bylineRepo.create({
|
||||
slug: "jane-doe",
|
||||
displayName: "Jane Doe",
|
||||
});
|
||||
|
||||
const result = await getByline(created.id);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.id).toBe(created.id);
|
||||
expect(result?.displayName).toBe("Jane Doe");
|
||||
expect(result?.slug).toBe("jane-doe");
|
||||
});
|
||||
|
||||
it("returns null for non-existent ID", async () => {
|
||||
const result = await getByline("non-existent");
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getBylineBySlug", () => {
|
||||
it("returns a byline by slug", async () => {
|
||||
await bylineRepo.create({
|
||||
slug: "john-smith",
|
||||
displayName: "John Smith",
|
||||
});
|
||||
|
||||
const result = await getBylineBySlug("john-smith");
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.displayName).toBe("John Smith");
|
||||
});
|
||||
|
||||
it("returns null for non-existent slug", async () => {
|
||||
const result = await getBylineBySlug("nobody");
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getEntryBylines", () => {
|
||||
it("returns explicit byline credits for an entry", async () => {
|
||||
const lead = await bylineRepo.create({
|
||||
slug: "lead-author",
|
||||
displayName: "Lead Author",
|
||||
});
|
||||
const editor = await bylineRepo.create({
|
||||
slug: "editor",
|
||||
displayName: "Editor",
|
||||
});
|
||||
|
||||
const post = await contentRepo.create({
|
||||
type: "post",
|
||||
slug: "my-post",
|
||||
data: { title: "My Post" },
|
||||
});
|
||||
|
||||
await bylineRepo.setContentBylines("post", post.id, [
|
||||
{ bylineId: lead.id },
|
||||
{ bylineId: editor.id, roleLabel: "Contributing Editor" },
|
||||
]);
|
||||
|
||||
const bylines = await getEntryBylines("post", post.id);
|
||||
|
||||
expect(bylines).toHaveLength(2);
|
||||
expect(bylines[0]?.byline.displayName).toBe("Lead Author");
|
||||
expect(bylines[0]?.sortOrder).toBe(0);
|
||||
expect(bylines[0]?.source).toBe("explicit");
|
||||
expect(bylines[1]?.byline.displayName).toBe("Editor");
|
||||
expect(bylines[1]?.roleLabel).toBe("Contributing Editor");
|
||||
expect(bylines[1]?.source).toBe("explicit");
|
||||
});
|
||||
|
||||
it("falls back to user-linked byline when no explicit credits", async () => {
|
||||
// Create a user
|
||||
const userRepo = new UserRepository(db);
|
||||
const user = await userRepo.create({
|
||||
email: "author@example.com",
|
||||
displayName: "Author User",
|
||||
role: "editor",
|
||||
});
|
||||
|
||||
// Create a byline linked to the user
|
||||
await bylineRepo.create({
|
||||
slug: "author-user",
|
||||
displayName: "Author User",
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
// Create a post with this user as author, no explicit bylines
|
||||
const post = await contentRepo.create({
|
||||
type: "post",
|
||||
slug: "authored-post",
|
||||
data: { title: "Authored Post" },
|
||||
authorId: user.id,
|
||||
});
|
||||
|
||||
const bylines = await getEntryBylines("post", post.id);
|
||||
|
||||
expect(bylines).toHaveLength(1);
|
||||
expect(bylines[0]?.byline.displayName).toBe("Author User");
|
||||
expect(bylines[0]?.source).toBe("inferred");
|
||||
expect(bylines[0]?.roleLabel).toBeNull();
|
||||
});
|
||||
|
||||
it("returns empty array when no bylines and no author fallback", async () => {
|
||||
const post = await contentRepo.create({
|
||||
type: "post",
|
||||
slug: "no-author-post",
|
||||
data: { title: "No Author" },
|
||||
});
|
||||
|
||||
const bylines = await getEntryBylines("post", post.id);
|
||||
expect(bylines).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getBylinesForEntries", () => {
|
||||
it("batch-fetches byline credits for multiple entries", async () => {
|
||||
const author1 = await bylineRepo.create({
|
||||
slug: "author-one",
|
||||
displayName: "Author One",
|
||||
});
|
||||
const author2 = await bylineRepo.create({
|
||||
slug: "author-two",
|
||||
displayName: "Author Two",
|
||||
});
|
||||
|
||||
const post1 = await contentRepo.create({
|
||||
type: "post",
|
||||
slug: "post-1",
|
||||
data: { title: "Post 1" },
|
||||
});
|
||||
const post2 = await contentRepo.create({
|
||||
type: "post",
|
||||
slug: "post-2",
|
||||
data: { title: "Post 2" },
|
||||
});
|
||||
const post3 = await contentRepo.create({
|
||||
type: "post",
|
||||
slug: "post-3",
|
||||
data: { title: "Post 3" },
|
||||
});
|
||||
|
||||
await bylineRepo.setContentBylines("post", post1.id, [{ bylineId: author1.id }]);
|
||||
await bylineRepo.setContentBylines("post", post2.id, [
|
||||
{ bylineId: author1.id },
|
||||
{ bylineId: author2.id, roleLabel: "Contributor" },
|
||||
]);
|
||||
// post3 has no bylines
|
||||
|
||||
const result = await getBylinesForEntries(
|
||||
"post",
|
||||
[post1, post2, post3].map((p) => ({ id: p.id, authorId: p.authorId })),
|
||||
);
|
||||
|
||||
expect(result.get(post1.id)).toHaveLength(1);
|
||||
expect(result.get(post1.id)?.[0]?.byline.displayName).toBe("Author One");
|
||||
expect(result.get(post1.id)?.[0]?.source).toBe("explicit");
|
||||
|
||||
expect(result.get(post2.id)).toHaveLength(2);
|
||||
expect(result.get(post2.id)?.[0]?.byline.displayName).toBe("Author One");
|
||||
expect(result.get(post2.id)?.[1]?.byline.displayName).toBe("Author Two");
|
||||
expect(result.get(post2.id)?.[1]?.roleLabel).toBe("Contributor");
|
||||
|
||||
expect(result.get(post3.id)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("returns inferred bylines for entries without explicit credits", async () => {
|
||||
const userRepo = new UserRepository(db);
|
||||
const user = await userRepo.create({
|
||||
email: "batch-author@example.com",
|
||||
displayName: "Batch Author",
|
||||
role: "editor",
|
||||
});
|
||||
|
||||
await bylineRepo.create({
|
||||
slug: "batch-author",
|
||||
displayName: "Batch Author",
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
const post = await contentRepo.create({
|
||||
type: "post",
|
||||
slug: "batch-post",
|
||||
data: { title: "Batch Post" },
|
||||
authorId: user.id,
|
||||
});
|
||||
|
||||
const result = await getBylinesForEntries("post", [{ id: post.id, authorId: post.authorId }]);
|
||||
|
||||
expect(result.get(post.id)).toHaveLength(1);
|
||||
expect(result.get(post.id)?.[0]?.source).toBe("inferred");
|
||||
expect(result.get(post.id)?.[0]?.byline.displayName).toBe("Batch Author");
|
||||
});
|
||||
|
||||
it("handles batches larger than SQL_BATCH_SIZE across explicit and inferred bylines", async () => {
|
||||
const userRepo = new UserRepository(db);
|
||||
const explicitByline = await bylineRepo.create({
|
||||
slug: "large-batch-explicit",
|
||||
displayName: "Large Batch Explicit",
|
||||
});
|
||||
|
||||
const explicitPost1 = await contentRepo.create({
|
||||
type: "post",
|
||||
slug: "large-batch-explicit-1",
|
||||
data: { title: "Large Batch Explicit 1" },
|
||||
});
|
||||
await bylineRepo.setContentBylines("post", explicitPost1.id, [
|
||||
{ bylineId: explicitByline.id },
|
||||
]);
|
||||
|
||||
const inferredPosts: { id: string; authorId: string | null }[] = [];
|
||||
for (let i = 0; i < SQL_BATCH_SIZE + 2; i++) {
|
||||
const user = await userRepo.create({
|
||||
email: `large-batch-${i}@example.com`,
|
||||
displayName: `Large Batch ${i}`,
|
||||
role: "editor",
|
||||
});
|
||||
|
||||
await bylineRepo.create({
|
||||
slug: `large-batch-${i}`,
|
||||
displayName: `Large Batch ${i}`,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
const post = await contentRepo.create({
|
||||
type: "post",
|
||||
slug: `large-batch-post-${i}`,
|
||||
data: { title: `Large Batch Post ${i}` },
|
||||
authorId: user.id,
|
||||
});
|
||||
inferredPosts.push({ id: post.id, authorId: post.authorId });
|
||||
}
|
||||
|
||||
const explicitPost2 = await contentRepo.create({
|
||||
type: "post",
|
||||
slug: "large-batch-explicit-2",
|
||||
data: { title: "Large Batch Explicit 2" },
|
||||
});
|
||||
await bylineRepo.setContentBylines("post", explicitPost2.id, [
|
||||
{ bylineId: explicitByline.id },
|
||||
]);
|
||||
|
||||
const inferredPostIds = inferredPosts.map((p) => p.id);
|
||||
const entries = [
|
||||
{ id: explicitPost1.id, authorId: explicitPost1.authorId },
|
||||
...inferredPosts,
|
||||
{ id: explicitPost2.id, authorId: explicitPost2.authorId },
|
||||
];
|
||||
const result = await getBylinesForEntries("post", entries);
|
||||
|
||||
expect(result.size).toBe(entries.length);
|
||||
expect(result.get(explicitPost1.id)?.[0]?.source).toBe("explicit");
|
||||
expect(result.get(explicitPost1.id)?.[0]?.byline.displayName).toBe("Large Batch Explicit");
|
||||
expect(result.get(explicitPost2.id)?.[0]?.source).toBe("explicit");
|
||||
expect(result.get(explicitPost2.id)?.[0]?.byline.displayName).toBe("Large Batch Explicit");
|
||||
expect(result.get(inferredPostIds[0]!)?.[0]?.source).toBe("inferred");
|
||||
expect(result.get(inferredPostIds[0]!)?.[0]?.byline.displayName).toBe("Large Batch 0");
|
||||
expect(result.get(inferredPostIds[SQL_BATCH_SIZE + 1]!)?.[0]?.source).toBe("inferred");
|
||||
expect(result.get(inferredPostIds[SQL_BATCH_SIZE + 1]!)?.[0]?.byline.displayName).toBe(
|
||||
`Large Batch ${SQL_BATCH_SIZE + 1}`,
|
||||
);
|
||||
});
|
||||
|
||||
it("returns empty map for empty input", async () => {
|
||||
const result = await getBylinesForEntries("post", []);
|
||||
expect(result.size).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user