279 lines
7.0 KiB
TypeScript
279 lines
7.0 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { EMPTY_ARTICLE_DOCUMENT, parseArticleDocument } from './document';
|
|
|
|
describe('article documents', () => {
|
|
it('accepts formatted prose, multiple Builds, and article images', () => {
|
|
const parsed = parseArticleDocument({
|
|
version: 1,
|
|
document: {
|
|
type: 'doc',
|
|
content: [
|
|
{
|
|
type: 'heading',
|
|
attrs: { level: 2 },
|
|
content: [
|
|
{ type: 'text', text: 'Opening', marks: [{ type: 'bold' }] },
|
|
],
|
|
},
|
|
{
|
|
type: 'paragraph',
|
|
content: [
|
|
{
|
|
type: 'text',
|
|
text: 'Use this rotation.',
|
|
marks: [
|
|
{ type: 'link', attrs: { href: 'https://shd.build/guides' } },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
type: 'buildEmbed',
|
|
attrs: {
|
|
buildId: '11111111-1111-4111-8111-111111111111',
|
|
embedKey: 'build-11111111-1111-4111-8111-111111111111',
|
|
},
|
|
},
|
|
{
|
|
type: 'buildEmbed',
|
|
attrs: {
|
|
buildId: '22222222-2222-4222-8222-222222222222',
|
|
embedKey: 'build-22222222-2222-4222-8222-222222222222',
|
|
},
|
|
},
|
|
{
|
|
type: 'articleImage',
|
|
attrs: {
|
|
imageId: '33333333-3333-4333-8333-333333333333',
|
|
alt: 'A loadout screen',
|
|
caption: 'Final configuration',
|
|
},
|
|
},
|
|
],
|
|
},
|
|
});
|
|
expect(parsed?.buildEmbeds).toHaveLength(2);
|
|
expect(parsed?.imageIds).toEqual(['33333333-3333-4333-8333-333333333333']);
|
|
expect(parsed?.plainText).toContain('Opening Use this rotation.');
|
|
});
|
|
|
|
it('rejects unsafe links and unknown nodes', () => {
|
|
expect(
|
|
parseArticleDocument({
|
|
version: 1,
|
|
document: {
|
|
type: 'doc',
|
|
content: [
|
|
{
|
|
type: 'paragraph',
|
|
content: [
|
|
{
|
|
type: 'text',
|
|
text: 'unsafe',
|
|
marks: [
|
|
{ type: 'link', attrs: { href: 'javascript:alert(1)' } },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
}),
|
|
).toBeNull();
|
|
expect(
|
|
parseArticleDocument({
|
|
version: 1,
|
|
document: {
|
|
type: 'doc',
|
|
content: [{ type: 'iframe', attrs: { src: 'https://example.com' } }],
|
|
},
|
|
}),
|
|
).toBeNull();
|
|
});
|
|
|
|
it('accepts and canonicalizes metadata emitted by the Tiptap editor', () => {
|
|
const parsed = parseArticleDocument({
|
|
version: 1,
|
|
document: {
|
|
type: 'doc',
|
|
content: [
|
|
{
|
|
type: 'paragraph',
|
|
content: [
|
|
{
|
|
type: 'text',
|
|
text: 'Read more',
|
|
marks: [
|
|
{
|
|
type: 'link',
|
|
attrs: {
|
|
href: 'https://shd.build/news',
|
|
target: '_blank',
|
|
rel: 'noopener noreferrer nofollow',
|
|
class: null,
|
|
title: 'News archive',
|
|
},
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
type: 'orderedList',
|
|
attrs: { start: 1, type: null },
|
|
content: [
|
|
{
|
|
type: 'listItem',
|
|
content: [
|
|
{
|
|
type: 'paragraph',
|
|
content: [{ type: 'text', text: 'First item' }],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
expect(parsed?.document.document.content).toEqual([
|
|
{
|
|
type: 'paragraph',
|
|
content: [
|
|
{
|
|
type: 'text',
|
|
text: 'Read more',
|
|
marks: [
|
|
{
|
|
type: 'link',
|
|
attrs: {
|
|
href: 'https://shd.build/news',
|
|
title: 'News archive',
|
|
},
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
type: 'orderedList',
|
|
content: [
|
|
{
|
|
type: 'listItem',
|
|
content: [
|
|
{
|
|
type: 'paragraph',
|
|
content: [{ type: 'text', text: 'First item' }],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
]);
|
|
expect(parsed?.plainText).toContain('News archive');
|
|
});
|
|
|
|
it('preserves supported ordered-list numbering', () => {
|
|
const parsed = parseArticleDocument({
|
|
version: 1,
|
|
document: {
|
|
type: 'doc',
|
|
content: [
|
|
{
|
|
type: 'orderedList',
|
|
attrs: { start: 2, type: 'A' },
|
|
content: [
|
|
{
|
|
type: 'listItem',
|
|
content: [{ type: 'paragraph' }],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
expect(parsed?.document.document.content?.[0]).toMatchObject({
|
|
type: 'orderedList',
|
|
attrs: { start: 2, type: 'A' },
|
|
});
|
|
expect(parseArticleDocument(parsed?.document)?.document).toEqual(
|
|
parsed?.document,
|
|
);
|
|
});
|
|
|
|
it('rejects unknown link metadata and non-default ordered lists', () => {
|
|
const documentWith = (content: unknown[]) => ({
|
|
version: 1,
|
|
document: { type: 'doc', content },
|
|
});
|
|
|
|
expect(
|
|
parseArticleDocument(
|
|
documentWith([
|
|
{
|
|
type: 'paragraph',
|
|
content: [
|
|
{
|
|
type: 'text',
|
|
text: 'unsafe metadata',
|
|
marks: [
|
|
{
|
|
type: 'link',
|
|
attrs: {
|
|
href: 'https://shd.build',
|
|
onclick: 'alert(1)',
|
|
},
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
]),
|
|
),
|
|
).toBeNull();
|
|
expect(
|
|
parseArticleDocument(
|
|
documentWith([
|
|
{
|
|
type: 'orderedList',
|
|
attrs: { start: 0, type: 'decimal' },
|
|
content: [],
|
|
},
|
|
]),
|
|
),
|
|
).toBeNull();
|
|
expect(
|
|
parseArticleDocument(
|
|
documentWith([
|
|
{
|
|
type: 'orderedList',
|
|
attrs: { start: 1, type: 1 },
|
|
content: [],
|
|
},
|
|
]),
|
|
),
|
|
).toBeNull();
|
|
});
|
|
|
|
it('rejects duplicate embed keys and unknown envelope fields', () => {
|
|
const embed = {
|
|
type: 'buildEmbed',
|
|
attrs: {
|
|
buildId: '11111111-1111-4111-8111-111111111111',
|
|
embedKey: 'build-11111111-1111-4111-8111-111111111111',
|
|
},
|
|
};
|
|
expect(
|
|
parseArticleDocument({
|
|
version: 1,
|
|
document: { type: 'doc', content: [embed, embed] },
|
|
}),
|
|
).toBeNull();
|
|
expect(
|
|
parseArticleDocument({ ...EMPTY_ARTICLE_DOCUMENT, legacy: true }),
|
|
).toBeNull();
|
|
});
|
|
});
|