Files
upstream-affine/blocksuite/affine/all/src/__tests__/adapters/pdf.unit.spec.ts
upstream-mirror 50f788e799
Some checks failed
Build & Test / Analyze (javascript, blocksuite) (push) Failing after 3s
Build & Test / Rust test filter (push) Failing after 3s
Build & Test / E2E BlockSuite Test (2) (push) Failing after 3s
Build & Test / E2E BlockSuite Test (3) (push) Failing after 4s
Build & Test / E2E BlockSuite Test (4) (push) Failing after 3s
Build & Test / E2E BlockSuite Cross Browser Test (push) Failing after 3s
Build & Test / E2E Mobile Test (1) (push) Failing after 3s
Build & Test / E2E Mobile Test (2) (push) Failing after 3s
Build & Test / Build AFFiNE native (x86_64-unknown-linux-gnu) (push) Failing after 4s
Build & Test / Unit Test (1) (push) Has been skipped
Build & Test / Unit Test (2) (push) Has been skipped
Build & Test / Unit Test (3) (push) Has been skipped
Build & Test / Build Server native (push) Failing after 3s
Build & Test / Check Git Status (push) Has been skipped
Build & Test / Server Test (0, 4) (push) Has been skipped
Build & Test / Server Test (1, 4) (push) Has been skipped
Build & Test / Analyze (javascript, affine) (push) Failing after 3s
Build & Test / Analyze (typescript, affine) (push) Failing after 3s
Build & Test / Analyze (typescript, blocksuite) (push) Failing after 3s
Build & Test / Mobile native build filter (push) Failing after 3s
Build & Test / Check yarn binary (push) Failing after 3s
Build & Test / E2E BlockSuite Test (1) (push) Failing after 4s
Build & Test / E2E BlockSuite Test (5) (push) Failing after 3s
Build & Test / Unit Test (4) (push) Has been skipped
Build & Test / Unit Test (5) (push) Has been skipped
Build & Test / Native Unit Test (push) Has been skipped
Build & Test / Server Test (2, 4) (push) Has been skipped
Build & Test / Server Test (3, 4) (push) Has been skipped
Build & Test / Server Test with Elasticsearch (push) Has been skipped
Build & Test / Server Test with Embedded Search (push) Has been skipped
Build & Test / Server Test with Manticore Search (push) Has been skipped
Build & Test / Server E2E Test (push) Has been skipped
Build & Test / Cloud Desktop E2E Test (push) Has been skipped
Build & Test / Cloud E2E Test 1/2 (push) Has been skipped
Build & Test / Cloud E2E Test 2/2 (push) Has been skipped
Build & Test / Build @affine/electron renderer (push) Failing after 3s
Build & Test / Copilot test filter (push) Failing after 4s
Build & Test / Lint Rust (push) Has been skipped
Build & Test / Build Android app (push) Has been skipped
Build & Test / Test Android app (push) Has been skipped
Build & Test / Run native tests (push) Has been skipped
Build & Test / Server Copilot Api Test (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (3, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (4, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (1, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (2, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (5, 5) (push) Has been skipped
Build & Test / Desktop Test (ubuntu-latest, linux, x64, x86_64-unknown-linux-gnu, true) (push) Has been cancelled
Build & Test / Desktop Test (windows-latest, windows, x64, x86_64-pc-windows-msvc, true) (push) Has been cancelled
Build & Test / 3, 2, 1 Launch (push) Has been cancelled
Build & Test / Lint (push) Has been cancelled
Build & Test / Typecheck (push) Has been cancelled
Build & Test / Build iOS app (push) Has been cancelled
Build & Test / Build AFFiNE native (aarch64-apple-darwin) (push) Has been cancelled
Build & Test / E2E Test (1) (push) Has been cancelled
Build & Test / E2E Test (2) (push) Has been cancelled
Build & Test / E2E Test (3) (push) Has been cancelled
Build & Test / E2E Test (4) (push) Has been cancelled
Build & Test / E2E Test (5) (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-apple-darwin) (push) Has been cancelled
Build & Test / Build AFFiNE native (aarch64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, arm64, aarch64-apple-darwin, true) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, x64, x86_64-apple-darwin, false) (push) Has been cancelled
snapshot of https://github.com/toeverything/AFFiNE.git (canary, 2026-09-16; blobs > 131072B omitted)
2026-09-16 00:31:07 +05:00

1619 lines
44 KiB
TypeScript

import { DefaultTheme, NoteDisplayMode } from '@blocksuite/affine-model';
import { PdfAdapter } from '@blocksuite/affine-shared/adapters';
import type { BlockSnapshot, DocSnapshot } from '@blocksuite/store';
import { AssetsManager, MemoryBlobCRUD } from '@blocksuite/store';
import { describe, expect, test } from 'vitest';
import { createJob } from '../utils/create-job.js';
import { getProvider } from '../utils/get-provider.js';
const provider = getProvider();
// Helper function to create a base snapshot structure
function createBaseSnapshot(
children: BlockSnapshot['children']
): BlockSnapshot {
return {
type: 'block',
id: 'block:test',
flavour: 'affine:page',
props: {
title: {
'$blocksuite:internal:text$': true,
delta: [],
},
},
children: [
{
type: 'block',
id: 'block:surface',
flavour: 'affine:surface',
props: {
elements: {},
},
children: [],
},
{
type: 'block',
id: 'block:note',
flavour: 'affine:note',
props: {
xywh: '[0,0,800,95]',
background: DefaultTheme.noteBackgrounColor,
index: 'a0',
hidden: false,
displayMode: NoteDisplayMode.DocAndEdgeless,
},
children,
},
],
};
}
describe('snapshot to pdf', () => {
test('paragraph', async () => {
const blockSnapshot: BlockSnapshot = {
type: 'block',
id: 'block:test',
flavour: 'affine:page',
props: {
title: {
'$blocksuite:internal:text$': true,
delta: [],
},
},
children: [
{
type: 'block',
id: 'block:surface',
flavour: 'affine:surface',
props: {
elements: {},
},
children: [],
},
{
type: 'block',
id: 'block:note',
flavour: 'affine:note',
props: {
xywh: '[0,0,800,95]',
background: DefaultTheme.noteBackgrounColor,
index: 'a0',
hidden: false,
displayMode: NoteDisplayMode.DocAndEdgeless,
},
children: [
{
type: 'block',
id: 'block:paragraph',
flavour: 'affine:paragraph',
props: {
type: 'text',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: 'Hello World',
},
],
},
},
children: [],
},
],
},
],
};
const pdfAdapter = new PdfAdapter(createJob(), provider);
const definition = await pdfAdapter.getDocDefinition(
[blockSnapshot],
undefined
);
expect(definition.content).toBeDefined();
expect(Array.isArray(definition.content)).toBe(true);
const content = definition.content as any[];
expect(content.length).toBeGreaterThan(0);
// Find the paragraph content
const paragraphContent = content.find(
(item: any) =>
item.text === 'Hello World' ||
(Array.isArray(item.text) && item.text.includes('Hello World'))
);
expect(paragraphContent).toBeDefined();
});
test('code block', async () => {
const blockSnapshot: BlockSnapshot = {
type: 'block',
id: 'block:test',
flavour: 'affine:page',
props: {
title: {
'$blocksuite:internal:text$': true,
delta: [],
},
},
children: [
{
type: 'block',
id: 'block:surface',
flavour: 'affine:surface',
props: {
elements: {},
},
children: [],
},
{
type: 'block',
id: 'block:note',
flavour: 'affine:note',
props: {
xywh: '[0,0,800,95]',
background: DefaultTheme.noteBackgrounColor,
index: 'a0',
hidden: false,
displayMode: NoteDisplayMode.DocAndEdgeless,
},
children: [
{
type: 'block',
id: 'block:code',
flavour: 'affine:code',
props: {
language: 'python',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: 'print("Hello")',
},
],
},
},
children: [],
},
],
},
],
};
const pdfAdapter = new PdfAdapter(createJob(), provider);
const definition = await pdfAdapter.getDocDefinition(
[blockSnapshot],
undefined
);
expect(definition.content).toBeDefined();
const content = definition.content as any[];
// Find code block table
const codeBlock = content.find(
(item: any) => item.table && item.table.body
);
expect(codeBlock).toBeDefined();
expect(codeBlock.table.body).toBeDefined();
});
test('list items', async () => {
const blockSnapshot: BlockSnapshot = {
type: 'block',
id: 'block:test',
flavour: 'affine:page',
props: {
title: {
'$blocksuite:internal:text$': true,
delta: [],
},
},
children: [
{
type: 'block',
id: 'block:surface',
flavour: 'affine:surface',
props: {
elements: {},
},
children: [],
},
{
type: 'block',
id: 'block:note',
flavour: 'affine:note',
props: {
xywh: '[0,0,800,95]',
background: DefaultTheme.noteBackgrounColor,
index: 'a0',
hidden: false,
displayMode: NoteDisplayMode.DocAndEdgeless,
},
children: [
{
type: 'block',
id: 'block:list1',
flavour: 'affine:list',
props: {
type: 'bulleted',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: 'Item 1',
},
],
},
},
children: [],
},
{
type: 'block',
id: 'block:list2',
flavour: 'affine:list',
props: {
type: 'bulleted',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: 'Item 2',
},
],
},
},
children: [],
},
],
},
],
};
const pdfAdapter = new PdfAdapter(createJob(), provider);
const definition = await pdfAdapter.getDocDefinition(
[blockSnapshot],
undefined
);
expect(definition.content).toBeDefined();
const content = definition.content as any[];
// Find list items (they should be tables with 2 columns)
const listItems = content.filter(
(item: any) =>
item.table && item.table.widths && item.table.widths.length === 2
);
expect(listItems.length).toBeGreaterThanOrEqual(2);
});
test('header', async () => {
const blockSnapshot: BlockSnapshot = {
type: 'block',
id: 'block:test',
flavour: 'affine:page',
props: {
title: {
'$blocksuite:internal:text$': true,
delta: [],
},
},
children: [
{
type: 'block',
id: 'block:surface',
flavour: 'affine:surface',
props: {
elements: {},
},
children: [],
},
{
type: 'block',
id: 'block:note',
flavour: 'affine:note',
props: {
xywh: '[0,0,800,95]',
background: DefaultTheme.noteBackgrounColor,
index: 'a0',
hidden: false,
displayMode: NoteDisplayMode.DocAndEdgeless,
},
children: [
{
type: 'block',
id: 'block:header',
flavour: 'affine:paragraph',
props: {
type: 'h1',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: 'Heading 1',
},
],
},
},
children: [],
},
],
},
],
};
const pdfAdapter = new PdfAdapter(createJob(), provider);
const definition = await pdfAdapter.getDocDefinition(
[blockSnapshot],
undefined
);
expect(definition.content).toBeDefined();
const content = definition.content as any[];
// Find header content
const header = content.find(
(item: any) =>
item.style === 'header1' ||
(item.text &&
(item.text === 'Heading 1' ||
(Array.isArray(item.text) && item.text.includes('Heading 1'))))
);
expect(header).toBeDefined();
if (header.style) {
expect(header.style).toBe('header1');
}
});
test('document with title', async () => {
const docSnapshot: DocSnapshot = {
type: 'page',
meta: {
id: 'testDocument',
title: 'Test Document',
createDate: 1718225423102,
tags: [],
},
blocks: {
type: 'block',
id: 'block:test',
flavour: 'affine:page',
props: {
title: {
'$blocksuite:internal:text$': true,
delta: [],
},
},
children: [
{
type: 'block',
id: 'block:surface',
flavour: 'affine:surface',
props: {
elements: {},
},
children: [],
},
{
type: 'block',
id: 'block:note',
flavour: 'affine:note',
props: {
xywh: '[0,0,800,95]',
background: DefaultTheme.noteBackgrounColor,
index: 'a0',
hidden: false,
displayMode: NoteDisplayMode.DocAndEdgeless,
},
children: [],
},
],
},
};
const pdfAdapter = new PdfAdapter(createJob(), provider);
const definition = await pdfAdapter.getDocDefinition(
[docSnapshot.blocks],
docSnapshot.meta?.title
);
expect(definition.content).toBeDefined();
const content = definition.content as any[];
// First item should be the title
expect(content[0].text).toBe('Test Document');
expect(content[0].style).toBe('title');
});
test('styles definition', async () => {
const pdfAdapter = new PdfAdapter(createJob(), provider);
const definition = await pdfAdapter.getDocDefinition([], undefined);
expect(definition.styles).toBeDefined();
expect(definition.styles?.title).toBeDefined();
expect(definition.styles?.header1).toBeDefined();
expect(definition.styles?.header2).toBeDefined();
expect(definition.styles?.header3).toBeDefined();
expect(definition.styles?.header4).toBeDefined();
expect(definition.styles?.code).toBeDefined();
expect(definition.defaultStyle).toBeDefined();
expect(definition.defaultStyle?.font).toBe('SarasaGothicCL');
});
describe('inline text styling', () => {
test('bold text', async () => {
const blockSnapshot = createBaseSnapshot([
{
type: 'block',
id: 'block:paragraph',
flavour: 'affine:paragraph',
props: {
type: 'text',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: 'Bold text',
attributes: { bold: true },
},
],
},
},
children: [],
},
]);
const pdfAdapter = new PdfAdapter(createJob(), provider);
const definition = await pdfAdapter.getDocDefinition(
[blockSnapshot],
undefined
);
const content = definition.content as any[];
const textContent = content.find((item: any) => item.text);
expect(textContent).toBeDefined();
if (Array.isArray(textContent.text)) {
const styledText = textContent.text.find(
(t: any) => typeof t === 'object' && t.bold === true
);
expect(styledText).toBeDefined();
expect(styledText.text).toBe('Bold text');
}
});
test('italic text', async () => {
const blockSnapshot = createBaseSnapshot([
{
type: 'block',
id: 'block:paragraph',
flavour: 'affine:paragraph',
props: {
type: 'text',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: 'Italic text',
attributes: { italic: true },
},
],
},
},
children: [],
},
]);
const pdfAdapter = new PdfAdapter(createJob(), provider);
const definition = await pdfAdapter.getDocDefinition(
[blockSnapshot],
undefined
);
const content = definition.content as any[];
const textContent = content.find((item: any) => item.text);
expect(textContent).toBeDefined();
if (Array.isArray(textContent.text)) {
const styledText = textContent.text.find(
(t: any) => typeof t === 'object' && t.italics === true
);
expect(styledText).toBeDefined();
expect(styledText.text).toBe('Italic text');
}
});
test('underline text', async () => {
const blockSnapshot = createBaseSnapshot([
{
type: 'block',
id: 'block:paragraph',
flavour: 'affine:paragraph',
props: {
type: 'text',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: 'Underlined text',
attributes: { underline: true },
},
],
},
},
children: [],
},
]);
const pdfAdapter = new PdfAdapter(createJob(), provider);
const definition = await pdfAdapter.getDocDefinition(
[blockSnapshot],
undefined
);
const content = definition.content as any[];
const textContent = content.find((item: any) => item.text);
expect(textContent).toBeDefined();
if (Array.isArray(textContent.text)) {
const styledText = textContent.text.find(
(t: any) =>
typeof t === 'object' &&
(t.decoration === 'underline' ||
(Array.isArray(t.decoration) &&
t.decoration.includes('underline')))
);
expect(styledText).toBeDefined();
expect(styledText.text).toBe('Underlined text');
}
});
test('strikethrough text', async () => {
const blockSnapshot = createBaseSnapshot([
{
type: 'block',
id: 'block:paragraph',
flavour: 'affine:paragraph',
props: {
type: 'text',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: 'Strikethrough text',
attributes: { strike: true },
},
],
},
},
children: [],
},
]);
const pdfAdapter = new PdfAdapter(createJob(), provider);
const definition = await pdfAdapter.getDocDefinition(
[blockSnapshot],
undefined
);
const content = definition.content as any[];
const textContent = content.find((item: any) => item.text);
expect(textContent).toBeDefined();
if (Array.isArray(textContent.text)) {
const styledText = textContent.text.find(
(t: any) =>
typeof t === 'object' &&
(t.decoration === 'lineThrough' ||
(Array.isArray(t.decoration) &&
t.decoration.includes('lineThrough')))
);
expect(styledText).toBeDefined();
expect(styledText.text).toBe('Strikethrough text');
}
});
test('inline code', async () => {
const blockSnapshot = createBaseSnapshot([
{
type: 'block',
id: 'block:paragraph',
flavour: 'affine:paragraph',
props: {
type: 'text',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: 'code',
attributes: { code: true },
},
],
},
},
children: [],
},
]);
const pdfAdapter = new PdfAdapter(createJob(), provider);
const definition = await pdfAdapter.getDocDefinition(
[blockSnapshot],
undefined
);
const content = definition.content as any[];
const textContent = content.find((item: any) => item.text);
expect(textContent).toBeDefined();
if (Array.isArray(textContent.text)) {
const codeText = textContent.text.find(
(t: any) =>
typeof t === 'object' &&
t.font === 'Inter' &&
t.background === '#f5f5f5'
);
expect(codeText).toBeDefined();
expect(codeText.text).toContain('code');
}
});
test('combined styles', async () => {
const blockSnapshot = createBaseSnapshot([
{
type: 'block',
id: 'block:paragraph',
flavour: 'affine:paragraph',
props: {
type: 'text',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: 'Bold and italic',
attributes: { bold: true, italic: true },
},
],
},
},
children: [],
},
]);
const pdfAdapter = new PdfAdapter(createJob(), provider);
const definition = await pdfAdapter.getDocDefinition(
[blockSnapshot],
undefined
);
const content = definition.content as any[];
const textContent = content.find((item: any) => item.text);
expect(textContent).toBeDefined();
if (Array.isArray(textContent.text)) {
const styledText = textContent.text.find(
(t: any) =>
typeof t === 'object' && t.bold === true && t.italics === true
);
expect(styledText).toBeDefined();
}
});
});
describe('links and references', () => {
test('link attribute', async () => {
const blockSnapshot = createBaseSnapshot([
{
type: 'block',
id: 'block:paragraph',
flavour: 'affine:paragraph',
props: {
type: 'text',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: 'Click here',
attributes: { link: 'https://example.com' },
},
],
},
},
children: [],
},
]);
const pdfAdapter = new PdfAdapter(createJob(), provider);
const definition = await pdfAdapter.getDocDefinition(
[blockSnapshot],
undefined
);
const content = definition.content as any[];
const textContent = content.find((item: any) => item.text);
expect(textContent).toBeDefined();
if (Array.isArray(textContent.text)) {
const linkText = textContent.text.find(
(t: any) => typeof t === 'object' && t.link === 'https://example.com'
);
expect(linkText).toBeDefined();
expect(linkText.color).toBe('#0066cc');
}
});
test('linked page reference - found', async () => {
const job = createJob();
const pdfAdapter = new PdfAdapter(job, provider);
pdfAdapter.configs.set('title:page123', 'Referenced Page');
pdfAdapter.configs.set('docLinkBaseUrl', 'https://example.com/doc');
const blockSnapshot = createBaseSnapshot([
{
type: 'block',
id: 'block:paragraph',
flavour: 'affine:paragraph',
props: {
type: 'text',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: ' ',
attributes: {
reference: {
type: 'LinkedPage',
pageId: 'page123',
},
},
},
],
},
},
children: [],
},
]);
const definition = await pdfAdapter.getDocDefinition(
[blockSnapshot],
undefined
);
const content = definition.content as any[];
const textContent = content.find((item: any) => item.text);
expect(textContent).toBeDefined();
if (Array.isArray(textContent.text)) {
const refText = textContent.text.find(
(t: any) =>
typeof t === 'object' &&
t.text === 'Referenced Page' &&
t.link === 'https://example.com/doc/page123'
);
expect(refText).toBeDefined();
expect(refText.color).toBe('#0066cc');
}
});
test('linked page reference - not found', async () => {
const job = createJob();
const pdfAdapter = new PdfAdapter(job, provider);
pdfAdapter.configs.set('docLinkBaseUrl', 'https://example.com/doc');
const blockSnapshot = createBaseSnapshot([
{
type: 'block',
id: 'block:paragraph',
flavour: 'affine:paragraph',
props: {
type: 'text',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: ' ',
attributes: {
reference: {
type: 'LinkedPage',
pageId: 'nonexistent',
},
},
},
],
},
},
children: [],
},
]);
const definition = await pdfAdapter.getDocDefinition(
[blockSnapshot],
undefined
);
const content = definition.content as any[];
const textContent = content.find((item: any) => item.text);
expect(textContent).toBeDefined();
if (Array.isArray(textContent.text)) {
const refText = textContent.text.find(
(t: any) => typeof t === 'object' && t.text === 'Page not found'
);
expect(refText).toBeDefined();
}
});
});
describe('quote blocks', () => {
test('quote block with text', async () => {
const blockSnapshot = createBaseSnapshot([
{
type: 'block',
id: 'block:quote',
flavour: 'affine:paragraph',
props: {
type: 'quote',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: 'This is a quote',
},
],
},
},
children: [],
},
]);
const pdfAdapter = new PdfAdapter(createJob(), provider);
const definition = await pdfAdapter.getDocDefinition(
[blockSnapshot],
undefined
);
const content = definition.content as any[];
const quoteBlock = content.find(
(item: any) =>
item.table &&
item.table.widths &&
item.table.widths.length === 2 &&
item.table.widths[0] === 2
);
expect(quoteBlock).toBeDefined();
expect(quoteBlock.table.body[0][0].fillColor).toBe('#cccccc');
});
test('quote block with children', async () => {
const blockSnapshot = createBaseSnapshot([
{
type: 'block',
id: 'block:quote',
flavour: 'affine:paragraph',
props: {
type: 'quote',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: 'Quote text',
},
],
},
},
children: [
{
type: 'block',
id: 'block:child',
flavour: 'affine:paragraph',
props: {
type: 'text',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: 'Child paragraph',
},
],
},
},
children: [],
},
],
},
]);
const pdfAdapter = new PdfAdapter(createJob(), provider);
const definition = await pdfAdapter.getDocDefinition(
[blockSnapshot],
undefined
);
const content = definition.content as any[];
const quoteBlock = content.find(
(item: any) =>
item.table && item.table.widths && item.table.widths.length === 2
);
expect(quoteBlock).toBeDefined();
expect(quoteBlock.table.body[0][1].stack).toBeDefined();
});
});
describe('callout blocks', () => {
test('callout with default background', async () => {
const blockSnapshot = createBaseSnapshot([
{
type: 'block',
id: 'block:callout',
flavour: 'affine:callout',
props: {
backgroundColorName: 'grey',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: 'Callout content',
},
],
},
},
children: [],
},
]);
const pdfAdapter = new PdfAdapter(createJob(), provider);
const definition = await pdfAdapter.getDocDefinition(
[blockSnapshot],
undefined
);
const content = definition.content as any[];
const calloutBlock = content.find(
(item: any) =>
item.table && item.table.widths && item.table.widths.length === 1
);
expect(calloutBlock).toBeDefined();
expect(calloutBlock.table.body[0][0].fillColor).toBeDefined();
});
test('callout with custom background color', async () => {
const blockSnapshot = createBaseSnapshot([
{
type: 'block',
id: 'block:callout',
flavour: 'affine:callout',
props: {
backgroundColorName: 'blue',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: 'Blue callout',
},
],
},
},
children: [],
},
]);
const pdfAdapter = new PdfAdapter(createJob(), provider);
const definition = await pdfAdapter.getDocDefinition(
[blockSnapshot],
undefined
);
const content = definition.content as any[];
const calloutBlock = content.find(
(item: any) =>
item.table && item.table.widths && item.table.widths.length === 1
);
expect(calloutBlock).toBeDefined();
expect(calloutBlock.table.body[0][0].fillColor).toBeDefined();
});
test('callout with children', async () => {
const blockSnapshot = createBaseSnapshot([
{
type: 'block',
id: 'block:callout',
flavour: 'affine:callout',
props: {
backgroundColorName: 'grey',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: 'Callout',
},
],
},
},
children: [
{
type: 'block',
id: 'block:child',
flavour: 'affine:paragraph',
props: {
type: 'text',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: 'Child content',
},
],
},
},
children: [],
},
],
},
]);
const pdfAdapter = new PdfAdapter(createJob(), provider);
const definition = await pdfAdapter.getDocDefinition(
[blockSnapshot],
undefined
);
const content = definition.content as any[];
const calloutBlock = content.find(
(item: any) =>
item.table && item.table.widths && item.table.widths.length === 1
);
expect(calloutBlock).toBeDefined();
expect(calloutBlock.table.body[0][0].stack).toBeDefined();
});
});
describe('image handling', () => {
test('image without sourceId', async () => {
const blockSnapshot = createBaseSnapshot([
{
type: 'block',
id: 'block:image',
flavour: 'affine:image',
props: {
sourceId: undefined,
caption: 'Test caption',
},
children: [],
},
]);
const pdfAdapter = new PdfAdapter(createJob(), provider);
const definition = await pdfAdapter.getDocDefinition(
[blockSnapshot],
undefined
);
const content = definition.content as any[];
const imageContent = content.find(
(item: any) =>
item.text &&
(item.text === '[Image: Test caption]' || item.text.includes('Image'))
);
expect(imageContent).toBeDefined();
expect(imageContent.italics).toBe(true);
});
test('SVG image', async () => {
const blobCRUD = new MemoryBlobCRUD();
const svgBlob = new Blob(
[
'<svg width="100" height="100"><rect width="100" height="100"/></svg>',
],
{
type: 'image/svg+xml',
}
);
const blobId = await blobCRUD.set(svgBlob);
const assets = new AssetsManager({ blob: blobCRUD });
await assets.readFromBlob(blobId);
assets.getAssets().set(blobId, svgBlob);
const blockSnapshot = createBaseSnapshot([
{
type: 'block',
id: 'block:image',
flavour: 'affine:image',
props: {
sourceId: blobId,
caption: 'SVG Image',
},
children: [],
},
]);
const pdfAdapter = new PdfAdapter(createJob(), provider);
const definition = await pdfAdapter.getDocDefinition(
[blockSnapshot],
undefined,
assets
);
const content = definition.content as any[];
const svgContent = content.find((item: any) => item.svg);
expect(svgContent).toBeDefined();
expect(svgContent.svg).toContain('<svg');
});
test('image with missing asset', async () => {
const blockSnapshot = createBaseSnapshot([
{
type: 'block',
id: 'block:image',
flavour: 'affine:image',
props: {
sourceId: 'nonexistent-blob-id',
caption: 'Missing image',
},
children: [],
},
]);
const pdfAdapter = new PdfAdapter(createJob(), provider);
const definition = await pdfAdapter.getDocDefinition(
[blockSnapshot],
undefined
);
const content = definition.content as any[];
const fallbackContent = content.find(
(item: any) =>
item.text &&
(item.text === '[Image: Missing image]' ||
item.text.includes('Image'))
);
expect(fallbackContent).toBeDefined();
});
test('image with width and height', async () => {
const blobCRUD = new MemoryBlobCRUD();
const svgBlob = new Blob(
[
'<svg width="200" height="150"><rect width="200" height="150"/></svg>',
],
{
type: 'image/svg+xml',
}
);
const blobId = await blobCRUD.set(svgBlob);
const assets = new AssetsManager({ blob: blobCRUD });
await assets.readFromBlob(blobId);
assets.getAssets().set(blobId, svgBlob);
const blockSnapshot = createBaseSnapshot([
{
type: 'block',
id: 'block:image',
flavour: 'affine:image',
props: {
sourceId: blobId,
width: 300,
height: 200,
caption: 'Sized image',
},
children: [],
},
]);
const pdfAdapter = new PdfAdapter(createJob(), provider);
const definition = await pdfAdapter.getDocDefinition(
[blockSnapshot],
undefined,
assets
);
const content = definition.content as any[];
const svgContent = content.find((item: any) => item.svg);
expect(svgContent).toBeDefined();
expect(svgContent.width).toBeDefined();
expect(svgContent.height).toBeDefined();
});
test('image text alignment', async () => {
const blobCRUD = new MemoryBlobCRUD();
const svgBlob = new Blob(['<svg width="100" height="100"></svg>'], {
type: 'image/svg+xml',
});
const blobId = await blobCRUD.set(svgBlob);
const assets = new AssetsManager({ blob: blobCRUD });
await assets.readFromBlob(blobId);
assets.getAssets().set(blobId, svgBlob);
const blockSnapshot = createBaseSnapshot([
{
type: 'block',
id: 'block:image',
flavour: 'affine:image',
props: {
sourceId: blobId,
textAlign: 'right',
},
children: [],
},
]);
const pdfAdapter = new PdfAdapter(createJob(), provider);
const definition = await pdfAdapter.getDocDefinition(
[blockSnapshot],
undefined,
assets
);
const content = definition.content as any[];
const svgContent = content.find((item: any) => item.svg);
expect(svgContent).toBeDefined();
expect(svgContent.alignment).toBe('right');
});
});
describe('table rendering', () => {
test('table with cells', async () => {
const blockSnapshot = createBaseSnapshot([
{
type: 'block',
id: 'block:table',
flavour: 'affine:table',
props: {
columns: {
col1: {
columnId: 'col1',
order: 'a0',
},
col2: {
columnId: 'col2',
order: 'a1',
},
},
rows: {
row1: {
rowId: 'row1',
order: 'a0',
},
row2: {
rowId: 'row2',
order: 'a1',
},
},
cells: {
'row1:col1': {
text: {
delta: [{ insert: 'Cell 1-1' }],
},
},
'row1:col2': {
text: {
delta: [{ insert: 'Cell 1-2' }],
},
},
'row2:col1': {
text: {
delta: [{ insert: 'Cell 2-1' }],
},
},
'row2:col2': {
text: {
delta: [{ insert: 'Cell 2-2' }],
},
},
},
},
children: [],
},
]);
const pdfAdapter = new PdfAdapter(createJob(), provider);
const definition = await pdfAdapter.getDocDefinition(
[blockSnapshot],
undefined
);
const content = definition.content as any[];
const table = content.find(
(item: any) =>
item.table && item.table.body && Array.isArray(item.table.body)
);
expect(table).toBeDefined();
expect(table.table.body.length).toBe(2);
expect(table.table.body[0].length).toBe(2);
});
test('empty table', async () => {
const blockSnapshot = createBaseSnapshot([
{
type: 'block',
id: 'block:table',
flavour: 'affine:table',
props: {
columns: {},
rows: {},
cells: {},
},
children: [],
},
]);
const pdfAdapter = new PdfAdapter(createJob(), provider);
const definition = await pdfAdapter.getDocDefinition(
[blockSnapshot],
undefined
);
const content = definition.content as any[];
const table = content.find((item: any) => item.table && item.table.body);
expect(table).toBeUndefined();
});
});
describe('edge cases', () => {
test('empty paragraph', async () => {
const blockSnapshot = createBaseSnapshot([
{
type: 'block',
id: 'block:paragraph',
flavour: 'affine:paragraph',
props: {
type: 'text',
text: {
'$blocksuite:internal:text$': true,
delta: [],
},
},
children: [],
},
]);
const pdfAdapter = new PdfAdapter(createJob(), provider);
const definition = await pdfAdapter.getDocDefinition(
[blockSnapshot],
undefined
);
expect(definition.content).toBeDefined();
const content = definition.content as any[];
expect(content.length).toBeGreaterThan(0);
});
test('paragraph with empty delta', async () => {
const blockSnapshot = createBaseSnapshot([
{
type: 'block',
id: 'block:paragraph',
flavour: 'affine:paragraph',
props: {
type: 'text',
text: {
'$blocksuite:internal:text$': true,
delta: [{ insert: '' }],
},
},
children: [],
},
]);
const pdfAdapter = new PdfAdapter(createJob(), provider);
const definition = await pdfAdapter.getDocDefinition(
[blockSnapshot],
undefined
);
expect(definition.content).toBeDefined();
});
test('malformed delta operations', async () => {
const blockSnapshot = createBaseSnapshot([
{
type: 'block',
id: 'block:paragraph',
flavour: 'affine:paragraph',
props: {
type: 'text',
text: {
'$blocksuite:internal:text$': true,
delta: [
{ insert: 123 as any }, // Invalid type
{ insert: 'Valid text' },
],
},
},
children: [],
},
]);
const pdfAdapter = new PdfAdapter(createJob(), provider);
const definition = await pdfAdapter.getDocDefinition(
[blockSnapshot],
undefined
);
expect(definition.content).toBeDefined();
const content = definition.content as any[];
const textContent = content.find((item: any) => item.text);
expect(textContent).toBeDefined();
});
test('text alignment', async () => {
const blockSnapshot = createBaseSnapshot([
{
type: 'block',
id: 'block:paragraph',
flavour: 'affine:paragraph',
props: {
type: 'text',
textAlign: 'center',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: 'Centered text',
},
],
},
},
children: [],
},
]);
const pdfAdapter = new PdfAdapter(createJob(), provider);
const definition = await pdfAdapter.getDocDefinition(
[blockSnapshot],
undefined
);
const content = definition.content as any[];
const paragraph = content.find(
(item: any) =>
item.text &&
(item.text === 'Centered text' ||
(Array.isArray(item.text) &&
item.text.some((t: any) =>
typeof t === 'string'
? t === 'Centered text'
: t.text === 'Centered text'
)))
);
expect(paragraph).toBeDefined();
expect(paragraph.alignment).toBe('center');
});
test('nested list items', async () => {
const blockSnapshot = createBaseSnapshot([
{
type: 'block',
id: 'block:list1',
flavour: 'affine:list',
props: {
type: 'bulleted',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: 'Parent item',
},
],
},
},
children: [
{
type: 'block',
id: 'block:list2',
flavour: 'affine:list',
props: {
type: 'bulleted',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: 'Child item',
},
],
},
},
children: [],
},
],
},
]);
const pdfAdapter = new PdfAdapter(createJob(), provider);
const definition = await pdfAdapter.getDocDefinition(
[blockSnapshot],
undefined
);
expect(definition.content).toBeDefined();
const content = definition.content as any[];
const listItems = content.filter(
(item: any) =>
item.table && item.table.widths && item.table.widths.length === 2
);
expect(listItems.length).toBeGreaterThanOrEqual(2);
});
test('numbered list with order', async () => {
const blockSnapshot = createBaseSnapshot([
{
type: 'block',
id: 'block:list1',
flavour: 'affine:list',
props: {
type: 'numbered',
order: 5,
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: 'Item 5',
},
],
},
},
children: [],
},
]);
const pdfAdapter = new PdfAdapter(createJob(), provider);
const definition = await pdfAdapter.getDocDefinition(
[blockSnapshot],
undefined
);
const content = definition.content as any[];
const listItem = content.find(
(item: any) =>
item.table &&
item.table.body &&
item.table.body[0] &&
item.table.body[0][0] &&
item.table.body[0][0].text
);
expect(listItem).toBeDefined();
expect(listItem.table.body[0][0].text).toContain('5.');
});
test('todo list checked', async () => {
const blockSnapshot = createBaseSnapshot([
{
type: 'block',
id: 'block:todo',
flavour: 'affine:list',
props: {
type: 'todo',
checked: true,
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: 'Completed task',
},
],
},
},
children: [],
},
]);
const pdfAdapter = new PdfAdapter(createJob(), provider);
const definition = await pdfAdapter.getDocDefinition(
[blockSnapshot],
undefined
);
const content = definition.content as any[];
const todoItem = content.find(
(item: any) =>
item.table &&
item.table.body &&
item.table.body[0] &&
item.table.body[0][0] &&
item.table.body[0][0].svg
);
expect(todoItem).toBeDefined();
expect(todoItem.table.body[0][0].svg).toContain('svg');
});
});
});