@@ -11,6 +11,7 @@ vi.mock('../src/env', async (importOriginal) => {
1111 } ;
1212} ) ;
1313
14+ import { docsBlockNoteSchema } from '@/blockSpecs' ;
1415import { initApp } from '@/servers' ;
1516
1617import {
@@ -300,6 +301,314 @@ describe('Conversion Testing', () => {
300301 expect ( response . body ) . toStrictEqual ( expectedBlocks ) ;
301302 } ) ;
302303
304+ test ( 'POST /api/convert Yjs to HTML with callout block' , async ( ) => {
305+ const app = initApp ( ) ;
306+ const editor = ServerBlockNoteEditor . create ( {
307+ schema : docsBlockNoteSchema ,
308+ } ) ;
309+ const blocks = [
310+ {
311+ type : 'callout' as const ,
312+ props : { emoji : '⚠️' , backgroundColor : 'yellow' } ,
313+ content : [ { type : 'text' as const , text : 'Be careful' , styles : { } } ] ,
314+ } ,
315+ ] ;
316+ const yDocument = editor . blocksToYDoc ( blocks , 'document-store' ) ;
317+ const yjsUpdate = Y . encodeStateAsUpdate ( yDocument ) ;
318+ const response = await request ( app )
319+ . post ( '/api/convert' )
320+ . set ( 'origin' , origin )
321+ . set ( 'authorization' , `Bearer ${ apiKey } ` )
322+ . set ( 'content-type' , 'application/vnd.yjs.doc' )
323+ . set ( 'accept' , 'text/html' )
324+ . send ( Buffer . from ( yjsUpdate ) ) ;
325+
326+ expect ( response . status ) . toBe ( 200 ) ;
327+ expect ( response . text ) . toContain ( '<aside' ) ;
328+ expect ( response . text ) . toContain ( 'role="note"' ) ;
329+ expect ( response . text ) . toContain ( 'data-emoji="⚠️"' ) ;
330+ expect ( response . text ) . toContain ( 'data-background-color="yellow"' ) ;
331+ expect ( response . text ) . toContain ( 'Be careful' ) ;
332+ // The inner emoji span is marked so downstream parsers can drop it
333+ // (the canonical emoji is on the <aside>).
334+ expect ( response . text ) . toContain (
335+ '<span aria-hidden="true" data-emoji="⚠️">' ,
336+ ) ;
337+ } ) ;
338+
339+ test ( 'POST /api/convert Yjs to Markdown preserves callout content' , async ( ) => {
340+ const app = initApp ( ) ;
341+ const editor = ServerBlockNoteEditor . create ( {
342+ schema : docsBlockNoteSchema ,
343+ } ) ;
344+ const blocks = [
345+ {
346+ type : 'callout' as const ,
347+ props : { emoji : '⚠️' , backgroundColor : 'yellow' } ,
348+ content : [ { type : 'text' as const , text : 'Be careful' , styles : { } } ] ,
349+ } ,
350+ ] ;
351+ const yDocument = editor . blocksToYDoc ( blocks , 'document-store' ) ;
352+ const yjsUpdate = Y . encodeStateAsUpdate ( yDocument ) ;
353+ const response = await request ( app )
354+ . post ( '/api/convert' )
355+ . set ( 'origin' , origin )
356+ . set ( 'authorization' , `Bearer ${ apiKey } ` )
357+ . set ( 'content-type' , 'application/vnd.yjs.doc' )
358+ . set ( 'accept' , 'text/markdown' )
359+ . send ( Buffer . from ( yjsUpdate ) ) ;
360+
361+ expect ( response . status ) . toBe ( 200 ) ;
362+ expect ( response . text ) . toContain ( '⚠️' ) ;
363+ expect ( response . text ) . toContain ( 'Be careful' ) ;
364+ } ) ;
365+
366+ test ( 'POST /api/convert Yjs to Markdown preserves interlinking link' , async ( ) => {
367+ const app = initApp ( ) ;
368+ const editor = ServerBlockNoteEditor . create ( {
369+ schema : docsBlockNoteSchema ,
370+ } ) ;
371+ const blocks = [
372+ {
373+ type : 'paragraph' as const ,
374+ content : [
375+ {
376+ type : 'interlinkingLinkInline' as const ,
377+ props : {
378+ docId : '00000000-0000-0000-0000-000000000123' ,
379+ title : 'Other doc' ,
380+ disabled : false ,
381+ trigger : '/' as const ,
382+ } ,
383+ } ,
384+ ] ,
385+ } ,
386+ ] ;
387+ const yDocument = editor . blocksToYDoc ( blocks , 'document-store' ) ;
388+ const yjsUpdate = Y . encodeStateAsUpdate ( yDocument ) ;
389+ const response = await request ( app )
390+ . post ( '/api/convert' )
391+ . set ( 'origin' , origin )
392+ . set ( 'authorization' , `Bearer ${ apiKey } ` )
393+ . set ( 'content-type' , 'application/vnd.yjs.doc' )
394+ . set ( 'accept' , 'text/markdown' )
395+ . send ( Buffer . from ( yjsUpdate ) ) ;
396+
397+ expect ( response . status ) . toBe ( 200 ) ;
398+ expect ( response . text ) . toContain (
399+ '[Other doc](/docs/00000000-0000-0000-0000-000000000123/ "Other doc")' ,
400+ ) ;
401+ } ) ;
402+
403+ test ( 'POST /api/convert Yjs to HTML with PDF block' , async ( ) => {
404+ const app = initApp ( ) ;
405+ const editor = ServerBlockNoteEditor . create ( {
406+ schema : docsBlockNoteSchema ,
407+ } ) ;
408+ const blocks = [
409+ {
410+ type : 'pdf' as const ,
411+ props : {
412+ url : 'https://example.com/file.pdf' ,
413+ name : 'Annual report' ,
414+ showPreview : true ,
415+ } ,
416+ } ,
417+ ] ;
418+ const yDocument = editor . blocksToYDoc ( blocks , 'document-store' ) ;
419+ const yjsUpdate = Y . encodeStateAsUpdate ( yDocument ) ;
420+ const response = await request ( app )
421+ . post ( '/api/convert' )
422+ . set ( 'origin' , origin )
423+ . set ( 'authorization' , `Bearer ${ apiKey } ` )
424+ . set ( 'content-type' , 'application/vnd.yjs.doc' )
425+ . set ( 'accept' , 'text/html' )
426+ . send ( Buffer . from ( yjsUpdate ) ) ;
427+
428+ expect ( response . status ) . toBe ( 200 ) ;
429+ expect ( response . text ) . toContain ( '<iframe' ) ;
430+ expect ( response . text ) . toContain ( 'src="https://example.com/file.pdf"' ) ;
431+ expect ( response . text ) . toContain ( 'title="Annual report"' ) ;
432+ } ) ;
433+
434+ test ( 'POST /api/convert Yjs to HTML strips unsafe PDF URL schemes' , async ( ) => {
435+ const app = initApp ( ) ;
436+ const editor = ServerBlockNoteEditor . create ( {
437+ schema : docsBlockNoteSchema ,
438+ } ) ;
439+ const blocks = [
440+ {
441+ type : 'pdf' as const ,
442+ props : {
443+ url : 'javascript:alert(1)' ,
444+ name : 'Malicious' ,
445+ showPreview : true ,
446+ } ,
447+ } ,
448+ ] ;
449+ const yDocument = editor . blocksToYDoc ( blocks , 'document-store' ) ;
450+ const yjsUpdate = Y . encodeStateAsUpdate ( yDocument ) ;
451+ const response = await request ( app )
452+ . post ( '/api/convert' )
453+ . set ( 'origin' , origin )
454+ . set ( 'authorization' , `Bearer ${ apiKey } ` )
455+ . set ( 'content-type' , 'application/vnd.yjs.doc' )
456+ . set ( 'accept' , 'text/html' )
457+ . send ( Buffer . from ( yjsUpdate ) ) ;
458+
459+ expect ( response . status ) . toBe ( 200 ) ;
460+ expect ( response . text ) . not . toContain ( '<iframe' ) ;
461+ expect ( response . text ) . not . toMatch ( / (?: s r c | h r e f ) = " j a v a s c r i p t : / ) ;
462+ } ) ;
463+
464+ test ( 'POST /api/convert Yjs to HTML with interlinking inline content' , async ( ) => {
465+ const app = initApp ( ) ;
466+ const editor = ServerBlockNoteEditor . create ( {
467+ schema : docsBlockNoteSchema ,
468+ } ) ;
469+ const blocks = [
470+ {
471+ type : 'paragraph' as const ,
472+ content : [
473+ {
474+ type : 'interlinkingLinkInline' as const ,
475+ props : {
476+ docId : '00000000-0000-0000-0000-000000000123' ,
477+ title : 'Other doc' ,
478+ disabled : false ,
479+ trigger : '/' as const ,
480+ } ,
481+ } ,
482+ ] ,
483+ } ,
484+ ] ;
485+ const yDocument = editor . blocksToYDoc ( blocks , 'document-store' ) ;
486+ const yjsUpdate = Y . encodeStateAsUpdate ( yDocument ) ;
487+ const response = await request ( app )
488+ . post ( '/api/convert' )
489+ . set ( 'origin' , origin )
490+ . set ( 'authorization' , `Bearer ${ apiKey } ` )
491+ . set ( 'content-type' , 'application/vnd.yjs.doc' )
492+ . set ( 'accept' , 'text/html' )
493+ . send ( Buffer . from ( yjsUpdate ) ) ;
494+
495+ expect ( response . status ) . toBe ( 200 ) ;
496+ expect ( response . text ) . toContain (
497+ 'href="/docs/00000000-0000-0000-0000-000000000123/"' ,
498+ ) ;
499+ expect ( response . text ) . toContain (
500+ 'data-doc-id="00000000-0000-0000-0000-000000000123"' ,
501+ ) ;
502+ expect ( response . text ) . toContain ( 'title="Other doc"' ) ;
503+ expect ( response . text ) . toContain ( 'Other doc' ) ;
504+ expect ( response . text ) . not . toContain ( 'data-inline-content-type' ) ;
505+ } ) ;
506+
507+ test ( 'POST /api/convert Yjs to HTML with disabled interlinking renders no link' , async ( ) => {
508+ const app = initApp ( ) ;
509+ const editor = ServerBlockNoteEditor . create ( {
510+ schema : docsBlockNoteSchema ,
511+ } ) ;
512+ const blocks = [
513+ {
514+ type : 'paragraph' as const ,
515+ content : [
516+ {
517+ type : 'interlinkingLinkInline' as const ,
518+ props : {
519+ docId : '00000000-0000-0000-0000-000000000123' ,
520+ title : 'Hidden' ,
521+ disabled : true ,
522+ trigger : '/' as const ,
523+ } ,
524+ } ,
525+ ] ,
526+ } ,
527+ ] ;
528+ const yDocument = editor . blocksToYDoc ( blocks , 'document-store' ) ;
529+ const yjsUpdate = Y . encodeStateAsUpdate ( yDocument ) ;
530+ const response = await request ( app )
531+ . post ( '/api/convert' )
532+ . set ( 'origin' , origin )
533+ . set ( 'authorization' , `Bearer ${ apiKey } ` )
534+ . set ( 'content-type' , 'application/vnd.yjs.doc' )
535+ . set ( 'accept' , 'text/html' )
536+ . send ( Buffer . from ( yjsUpdate ) ) ;
537+
538+ expect ( response . status ) . toBe ( 200 ) ;
539+ expect ( response . text ) . not . toContain ( 'href=' ) ;
540+ expect ( response . text ) . not . toContain ( 'data-doc-id' ) ;
541+ expect ( response . text ) . not . toContain ( 'Hidden' ) ;
542+ } ) ;
543+
544+ test ( 'POST /api/convert Yjs to BlockNote JSON preserves pageBreak block' , async ( ) => {
545+ const app = initApp ( ) ;
546+ const editor = ServerBlockNoteEditor . create ( {
547+ schema : docsBlockNoteSchema ,
548+ } ) ;
549+ const blocks = [
550+ {
551+ type : 'paragraph' as const ,
552+ content : [ { type : 'text' as const , text : 'before' , styles : { } } ] ,
553+ } ,
554+ { type : 'pageBreak' as const } ,
555+ {
556+ type : 'paragraph' as const ,
557+ content : [ { type : 'text' as const , text : 'after' , styles : { } } ] ,
558+ } ,
559+ ] ;
560+ const yDocument = editor . blocksToYDoc ( blocks , 'document-store' ) ;
561+ const yjsUpdate = Y . encodeStateAsUpdate ( yDocument ) ;
562+ const response = await request ( app )
563+ . post ( '/api/convert' )
564+ . set ( 'origin' , origin )
565+ . set ( 'authorization' , `Bearer ${ apiKey } ` )
566+ . set ( 'content-type' , 'application/vnd.yjs.doc' )
567+ . set ( 'accept' , 'application/json' )
568+ . send ( Buffer . from ( yjsUpdate ) ) ;
569+
570+ expect ( response . status ) . toBe ( 200 ) ;
571+ const types = ( response . body as { type : string } [ ] ) . map ( ( b ) => b . type ) ;
572+ expect ( types ) . toContain ( 'pageBreak' ) ;
573+ } ) ;
574+
575+ test ( 'POST /api/convert Yjs to BlockNote JSON preserves uploadLoader block' , async ( ) => {
576+ const app = initApp ( ) ;
577+ const editor = ServerBlockNoteEditor . create ( {
578+ schema : docsBlockNoteSchema ,
579+ } ) ;
580+ const blocks = [
581+ {
582+ type : 'uploadLoader' as const ,
583+ props : {
584+ information : 'uploading' ,
585+ type : 'loading' as const ,
586+ blockUploadName : 'doc.pdf' ,
587+ } ,
588+ } ,
589+ ] ;
590+ const yDocument = editor . blocksToYDoc ( blocks , 'document-store' ) ;
591+ const yjsUpdate = Y . encodeStateAsUpdate ( yDocument ) ;
592+ const response = await request ( app )
593+ . post ( '/api/convert' )
594+ . set ( 'origin' , origin )
595+ . set ( 'authorization' , `Bearer ${ apiKey } ` )
596+ . set ( 'content-type' , 'application/vnd.yjs.doc' )
597+ . set ( 'accept' , 'application/json' )
598+ . send ( Buffer . from ( yjsUpdate ) ) ;
599+
600+ expect ( response . status ) . toBe ( 200 ) ;
601+ const uploadLoader = (
602+ response . body as { type : string ; props : Record < string , unknown > } [ ]
603+ ) . find ( ( b ) => b . type === 'uploadLoader' ) ;
604+ expect ( uploadLoader ) . toBeDefined ( ) ;
605+ expect ( uploadLoader ?. props ) . toMatchObject ( {
606+ information : 'uploading' ,
607+ type : 'loading' ,
608+ blockUploadName : 'doc.pdf' ,
609+ } ) ;
610+ } ) ;
611+
303612 test ( 'POST /api/convert with invalid Yjs content returns 400' , async ( ) => {
304613 const destroySpy = vi . spyOn ( Y . Doc . prototype , 'destroy' ) ;
305614 const app = initApp ( ) ;
0 commit comments