import type { SerializedBlockNode, SerializedLinkNode, SerializedRelationshipNode, SerializedUploadNode, } from '@payloadcms/richtext-lexical' import type { SerializedEditorState, SerializedParagraphNode } from 'lexical' import type { Payload } from 'payload' import type { PaginatedDocs } from 'payload/database' import { getPayload } from 'payload' import type { LexicalField, LexicalMigrateField, RichTextField } from './payload-types.js' import { devUser } from '../credentials.js' import { NextRESTClient } from '../helpers/NextRESTClient.js' import { startMemoryDB } from '../startMemoryDB.js' import { arrayDoc } from './collections/Array/shared.js' import { lexicalDocData } from './collections/Lexical/data.js' import { lexicalMigrateDocData } from './collections/LexicalMigrate/data.js' import { richTextDocData } from './collections/RichText/data.js' import { generateLexicalRichText } from './collections/RichText/generateLexicalRichText.js' import { textDoc } from './collections/Text/shared.js' import { uploadsDoc } from './collections/Upload/shared.js' import configPromise from './config.js' import { clearAndSeedEverything } from './seed.js' import { arrayFieldsSlug, lexicalFieldsSlug, lexicalMigrateFieldsSlug, richTextFieldsSlug, textFieldsSlug, uploadsSlug, } from './slugs.js' let payload: Payload let restClient: NextRESTClient let token: string let createdArrayDocID: number | string = null let createdJPGDocID: number | string = null let createdTextDocID: number | string = null let createdRichTextDocID: number | string = null describe('Lexical', () => { beforeAll(async () => { const config = await startMemoryDB(configPromise) payload = await getPayload({ config }) restClient = new NextRESTClient(payload.config) }) beforeEach(async () => { await clearAndSeedEverything(payload) await restClient.login({ slug: 'users', credentials: devUser, }) createdArrayDocID = ( await payload.find({ collection: arrayFieldsSlug, where: { title: { equals: 'array doc 1', }, }, }) ).docs[0].id createdJPGDocID = ( await payload.find({ collection: uploadsSlug, where: { filename: { equals: 'payload.jpg', }, }, }) ).docs[0].id createdTextDocID = ( await payload.find({ collection: textFieldsSlug, where: { text: { equals: 'Seeded text document', }, }, }) ).docs[0].id createdRichTextDocID = ( await payload.find({ collection: richTextFieldsSlug, where: { title: { equals: 'Rich Text', }, }, }) ).docs[0].id }) describe('basic', () => { it('should allow querying on lexical content', async () => { const richTextDoc: RichTextField = ( await payload.find({ collection: richTextFieldsSlug, depth: 0, where: { title: { equals: richTextDocData.title, }, }, }) ).docs[0] as never expect(richTextDoc?.lexicalCustomFields).toStrictEqual( JSON.parse( JSON.stringify(generateLexicalRichText()) .replace( /"\{\{ARRAY_DOC_ID\}\}"/g, payload.db.defaultIDType === 'number' ? `${createdArrayDocID}` : `"${createdArrayDocID}"`, ) .replace( /"\{\{UPLOAD_DOC_ID\}\}"/g, payload.db.defaultIDType === 'number' ? `${createdJPGDocID}` : `"${createdJPGDocID}"`, ) .replace( /"\{\{TEXT_DOC_ID\}\}"/g, payload.db.defaultIDType === 'number' ? `${createdTextDocID}` : `"${createdTextDocID}"`, ), ), ) }) it('should populate respect depth parameter and populate link node relationship', async () => { const richTextDoc: RichTextField = ( await payload.find({ collection: richTextFieldsSlug, depth: 1, where: { title: { equals: richTextDocData.title, }, }, }) ).docs[0] as never const seededDocument = JSON.parse( JSON.stringify(generateLexicalRichText()) .replace( /"\{\{ARRAY_DOC_ID\}\}"/g, payload.db.defaultIDType === 'number' ? `${createdArrayDocID}` : `"${createdArrayDocID}"`, ) .replace( /"\{\{UPLOAD_DOC_ID\}\}"/g, payload.db.defaultIDType === 'number' ? `${createdJPGDocID}` : `"${createdJPGDocID}"`, ) .replace( /"\{\{TEXT_DOC_ID\}\}"/g, payload.db.defaultIDType === 'number' ? `${createdTextDocID}` : `"${createdTextDocID}"`, ), ) expect(richTextDoc?.lexicalCustomFields).not.toStrictEqual(seededDocument) // The whole seededDocument should not match, as richTextDoc should now contain populated documents not present in the seeded document const lexical: SerializedEditorState = richTextDoc?.lexicalCustomFields const linkNode: SerializedLinkNode = (lexical.root.children[1] as SerializedParagraphNode) .children[3] as SerializedLinkNode expect(linkNode.fields.doc.value.items[1].text).toStrictEqual(arrayDoc.items[1].text) }) it('should populate relationship node', async () => { const richTextDoc: RichTextField = ( await payload.find({ collection: richTextFieldsSlug, depth: 1, where: { title: { equals: richTextDocData.title, }, }, }) ).docs[0] as never const relationshipNode: SerializedRelationshipNode = richTextDoc.lexicalCustomFields.root.children.find( (node) => node.type === 'relationship', ) as SerializedRelationshipNode expect(relationshipNode.value.text).toStrictEqual(textDoc.text) }) it('should respect GraphQL rich text depth parameter and populate upload node', async () => { const query = `query { RichTextFields { docs { lexicalCustomFields(depth: 2) } } }` const response: { data: { RichTextFields: PaginatedDocs } } = await restClient .GRAPHQL_POST({ body: JSON.stringify({ query }), }) .then((res) => res.json()) const { docs } = response.data.RichTextFields const uploadNode: SerializedUploadNode = docs[0].lexicalCustomFields.root.children.find( (node) => node.type === 'upload', ) as SerializedUploadNode expect((uploadNode.value.media as any).filename).toStrictEqual('payload.png') }) }) describe('converters and migrations', () => { it('htmlConverter: should output correct HTML for top-level lexical field', async () => { const lexicalDoc: LexicalMigrateField = ( await payload.find({ collection: lexicalMigrateFieldsSlug, depth: 0, where: { title: { equals: lexicalMigrateDocData.title, }, }, }) ).docs[0] as never const htmlField: string = lexicalDoc?.lexicalSimple_html expect(htmlField).toStrictEqual('

simple

') }) it('htmlConverter: should output correct HTML for lexical field nested in group', async () => { const lexicalDoc: LexicalMigrateField = ( await payload.find({ collection: lexicalMigrateFieldsSlug, depth: 0, where: { title: { equals: lexicalMigrateDocData.title, }, }, }) ).docs[0] as never const htmlField: string = lexicalDoc?.groupWithLexicalField?.lexicalInGroupField_html expect(htmlField).toStrictEqual('

group

') }) it('htmlConverter: should output correct HTML for lexical field nested in array', async () => { const lexicalDoc: LexicalMigrateField = ( await payload.find({ collection: lexicalMigrateFieldsSlug, depth: 0, where: { title: { equals: lexicalMigrateDocData.title, }, }, }) ).docs[0] as never const htmlField1: string = lexicalDoc?.arrayWithLexicalField[0].lexicalInArrayField_html const htmlField2: string = lexicalDoc?.arrayWithLexicalField[1].lexicalInArrayField_html expect(htmlField1).toStrictEqual('

array 1

') expect(htmlField2).toStrictEqual('

array 2

') }) }) describe('advanced - blocks', () => { it('should not populate relationships in blocks if depth is 0', async () => { const lexicalDoc: LexicalField = ( await payload.find({ collection: lexicalFieldsSlug, depth: 0, where: { title: { equals: lexicalDocData.title, }, }, }) ).docs[0] as never const lexicalField: SerializedEditorState = lexicalDoc?.lexicalWithBlocks const relationshipBlockNode: SerializedBlockNode = lexicalField.root .children[2] as SerializedBlockNode /** * Depth 1 population: */ expect(relationshipBlockNode.fields.rel).toStrictEqual(createdJPGDocID) }) it('should populate relationships in blocks with depth=1', async () => { const lexicalDoc: LexicalField = ( await payload.find({ collection: lexicalFieldsSlug, depth: 1, where: { title: { equals: lexicalDocData.title, }, }, }) ).docs[0] as never const lexicalField: SerializedEditorState = lexicalDoc?.lexicalWithBlocks const relationshipBlockNode: SerializedBlockNode = lexicalField.root .children[2] as SerializedBlockNode /** * Depth 1 population: */ expect(relationshipBlockNode.fields.rel.filename).toStrictEqual('payload.jpg') }) it('should correctly populate polymorphic hasMany relationships in blocks with depth=0', async () => { const lexicalDoc: LexicalField = ( await payload.find({ collection: lexicalFieldsSlug, depth: 0, where: { title: { equals: lexicalDocData.title, }, }, }) ).docs[0] as never const lexicalField: SerializedEditorState = lexicalDoc?.lexicalWithBlocks const relationshipBlockNode: SerializedBlockNode = lexicalField.root .children[3] as SerializedBlockNode /** * Depth 0 population: */ expect(Object.keys(relationshipBlockNode.fields.rel[0])).toHaveLength(2) expect(relationshipBlockNode.fields.rel[0].relationTo).toStrictEqual('text-fields') expect(relationshipBlockNode.fields.rel[0].value).toStrictEqual(createdTextDocID) expect(Object.keys(relationshipBlockNode.fields.rel[1])).toHaveLength(2) expect(relationshipBlockNode.fields.rel[1].relationTo).toStrictEqual('uploads') expect(relationshipBlockNode.fields.rel[1].value).toStrictEqual(createdJPGDocID) }) it('should correctly populate polymorphic hasMany relationships in blocks with depth=1', async () => { // Related issue: https://github.com/payloadcms/payload/issues/4277 const lexicalDoc: LexicalField = ( await payload.find({ collection: lexicalFieldsSlug, depth: 1, where: { title: { equals: lexicalDocData.title, }, }, }) ).docs[0] as never const lexicalField: SerializedEditorState = lexicalDoc?.lexicalWithBlocks const relationshipBlockNode: SerializedBlockNode = lexicalField.root .children[3] as SerializedBlockNode /** * Depth 1 population: */ expect(Object.keys(relationshipBlockNode.fields.rel[0])).toHaveLength(2) expect(relationshipBlockNode.fields.rel[0].relationTo).toStrictEqual('text-fields') expect(relationshipBlockNode.fields.rel[0].value.id).toStrictEqual(createdTextDocID) expect(relationshipBlockNode.fields.rel[0].value.text).toStrictEqual(textDoc.text) expect(relationshipBlockNode.fields.rel[0].value.localizedText).toStrictEqual( textDoc.localizedText, ) expect(Object.keys(relationshipBlockNode.fields.rel[1])).toHaveLength(2) expect(relationshipBlockNode.fields.rel[1].relationTo).toStrictEqual('uploads') expect(relationshipBlockNode.fields.rel[1].value.id).toStrictEqual(createdJPGDocID) expect(relationshipBlockNode.fields.rel[1].value.text).toStrictEqual(uploadsDoc.text) expect(relationshipBlockNode.fields.rel[1].value.filename).toStrictEqual('payload.jpg') }) it('should not populate relationship nodes inside of a sub-editor from a blocks node with 0 depth', async () => { const lexicalDoc: LexicalField = ( await payload.find({ collection: lexicalFieldsSlug, depth: 0, where: { title: { equals: lexicalDocData.title, }, }, }) ).docs[0] as never const lexicalField: SerializedEditorState = lexicalDoc?.lexicalWithBlocks const subEditorBlockNode: SerializedBlockNode = lexicalField.root .children[4] as SerializedBlockNode const subEditor: SerializedEditorState = subEditorBlockNode.fields.richText const subEditorRelationshipNode: SerializedRelationshipNode = subEditor.root .children[0] as SerializedRelationshipNode /** * Depth 1 population: */ expect(subEditorRelationshipNode.value.id).toStrictEqual(createdRichTextDocID) // But the value should not be populated and only have the id field: expect(Object.keys(subEditorRelationshipNode.value)).toHaveLength(1) }) it('should populate relationship nodes inside of a sub-editor from a blocks node with 1 depth', async () => { const lexicalDoc: LexicalField = ( await payload.find({ collection: lexicalFieldsSlug, depth: 1, where: { title: { equals: lexicalDocData.title, }, }, }) ).docs[0] as never const lexicalField: SerializedEditorState = lexicalDoc?.lexicalWithBlocks const subEditorBlockNode: SerializedBlockNode = lexicalField.root .children[4] as SerializedBlockNode const subEditor: SerializedEditorState = subEditorBlockNode.fields.richText const subEditorRelationshipNode: SerializedRelationshipNode = subEditor.root .children[0] as SerializedRelationshipNode /** * Depth 1 population: */ expect(subEditorRelationshipNode.value.id).toStrictEqual(createdRichTextDocID) expect(subEditorRelationshipNode.value.title).toStrictEqual(richTextDocData.title) // Make sure that the referenced, popular document is NOT populated (that would require depth > 2): const populatedDocEditorState: SerializedEditorState = subEditorRelationshipNode.value .lexicalCustomFields as SerializedEditorState const populatedDocEditorRelationshipNode: SerializedRelationshipNode = populatedDocEditorState .root.children[2] as SerializedRelationshipNode //console.log('populatedDocEditorRelatonshipNode:', populatedDocEditorRelationshipNode) /** * Depth 2 population: */ expect(populatedDocEditorRelationshipNode.value.id).toStrictEqual(createdTextDocID) // But the value should not be populated and only have the id field - that's because it would require a depth of 2 expect(Object.keys(populatedDocEditorRelationshipNode.value)).toHaveLength(1) }) it('should populate relationship nodes inside of a sub-editor from a blocks node with depth 2', async () => { const lexicalDoc: LexicalField = ( await payload.find({ collection: lexicalFieldsSlug, depth: 2, where: { title: { equals: lexicalDocData.title, }, }, }) ).docs[0] as never const lexicalField: SerializedEditorState = lexicalDoc?.lexicalWithBlocks const subEditorBlockNode: SerializedBlockNode = lexicalField.root .children[4] as SerializedBlockNode const subEditor: SerializedEditorState = subEditorBlockNode.fields.richText const subEditorRelationshipNode: SerializedRelationshipNode = subEditor.root .children[0] as SerializedRelationshipNode /** * Depth 1 population: */ expect(subEditorRelationshipNode.value.id).toStrictEqual(createdRichTextDocID) expect(subEditorRelationshipNode.value.title).toStrictEqual(richTextDocData.title) // Make sure that the referenced, popular document is NOT populated (that would require depth > 2): const populatedDocEditorState: SerializedEditorState = subEditorRelationshipNode.value .lexicalCustomFields as SerializedEditorState const populatedDocEditorRelationshipNode: SerializedRelationshipNode = populatedDocEditorState .root.children[2] as SerializedRelationshipNode /** * Depth 2 population: */ expect(populatedDocEditorRelationshipNode.value.id).toStrictEqual(createdTextDocID) // Should now be populated (length 12) expect(populatedDocEditorRelationshipNode.value.text).toStrictEqual(textDoc.text) }) }) })