From 3f2df643e7fc49246edfae554b8b99d39390cae5 Mon Sep 17 00:00:00 2001 From: Alessio Gravili Date: Sun, 14 Apr 2024 02:18:16 -0400 Subject: [PATCH] chore(richtext-lexical): add failing e2e test which ensures sub-richtext blocks work as intended --- test/fields/collections/Lexical/blocks.ts | 30 ++- test/fields/collections/Lexical/e2e.spec.ts | 200 +++++++++++++++++- .../Lexical/generateLexicalRichText.ts | 4 +- test/helpers.ts | 2 +- 4 files changed, 227 insertions(+), 9 deletions(-) diff --git a/test/fields/collections/Lexical/blocks.ts b/test/fields/collections/Lexical/blocks.ts index 15ae911f01..593e12c54b 100644 --- a/test/fields/collections/Lexical/blocks.ts +++ b/test/fields/collections/Lexical/blocks.ts @@ -1,6 +1,7 @@ import type { LexicalBlock } from '@payloadcms/richtext-lexical' import type { ArrayField } from 'payload/types' +import { BlocksFeature } from '@payloadcms/richtext-lexical' import { lexicalEditor } from '@payloadcms/richtext-lexical' import { textFieldsSlug } from '../Text/shared.js' @@ -113,12 +114,35 @@ export const RadioButtonsBlock: LexicalBlock = { export const RichTextBlock: LexicalBlock = { fields: [ { - name: 'richText', + name: 'richTextField', type: 'richText', - editor: lexicalEditor(), + editor: lexicalEditor({ + features: ({ defaultFeatures }) => [ + ...defaultFeatures, + BlocksFeature({ + blocks: [ + { + fields: [ + { + name: 'subRichTextField', + type: 'richText', + editor: lexicalEditor({}), + }, + { + name: 'subUploadField', + type: 'upload', + relationTo: 'uploads', + }, + ], + slug: 'lexicalAndUploadBlock', + }, + ], + }), + ], + }), }, ], - slug: 'richText', + slug: 'richTextBlock', } export const UploadAndRichTextBlock: LexicalBlock = { diff --git a/test/fields/collections/Lexical/e2e.spec.ts b/test/fields/collections/Lexical/e2e.spec.ts index ff08b17e6b..197413321b 100644 --- a/test/fields/collections/Lexical/e2e.spec.ts +++ b/test/fields/collections/Lexical/e2e.spec.ts @@ -7,9 +7,10 @@ import { expect, test } from '@playwright/test' import { initPayloadE2ENoConfig } from 'helpers/initPayloadE2ENoConfig.js' import { reInitializeDB } from 'helpers/reInit.js' import path from 'path' +import { wait } from 'payload/utilities' import { fileURLToPath } from 'url' -import type { Config, LexicalField } from '../../payload-types.js' +import type { Config, LexicalField, Upload } from '../../payload-types.js' import { initPageConsoleErrorCatch, saveDocAndAssert } from '../../../helpers.js' import { AdminUrlUtil } from '../../../helpers/adminUrlUtil.js' @@ -147,6 +148,7 @@ describe('lexical', () => { await payload.find({ collection: lexicalFieldsSlug, depth: 0, + overrideAccess: true, where: { title: { equals: lexicalDocData.title, @@ -216,6 +218,7 @@ describe('lexical', () => { await payload.find({ collection: lexicalFieldsSlug, depth: 0, + overrideAccess: true, where: { title: { equals: lexicalDocData.title, @@ -366,6 +369,7 @@ describe('lexical', () => { await payload.find({ collection: lexicalFieldsSlug, depth: 0, + overrideAccess: true, where: { title: { equals: lexicalDocData.title, @@ -375,8 +379,11 @@ describe('lexical', () => { ).docs[0] as never const lexicalField: SerializedEditorState = lexicalDoc.lexicalWithBlocks + const blockNode: SerializedBlockNode = lexicalField.root.children[4] as SerializedBlockNode - const textNodeInBlockNodeRichText = blockNode.fields.richText.root.children[1].children[0] + + const textNodeInBlockNodeRichText = + blockNode.fields.richTextField.root.children[1].children[0] expect(textNodeInBlockNodeRichText.text).toBe( 'Some text below relationship node 1 inserted text', @@ -443,6 +450,7 @@ describe('lexical', () => { await payload.find({ collection: lexicalFieldsSlug, depth: 0, + overrideAccess: true, where: { title: { equals: lexicalDocData.title, @@ -453,7 +461,7 @@ describe('lexical', () => { const lexicalField: SerializedEditorState = lexicalDoc.lexicalWithBlocks const blockNode: SerializedBlockNode = lexicalField.root.children[4] as SerializedBlockNode - const paragraphNodeInBlockNodeRichText = blockNode.fields.richText.root.children[1] + const paragraphNodeInBlockNodeRichText = blockNode.fields.richTextField.root.children[1] expect(paragraphNodeInBlockNodeRichText.children).toHaveLength(2) @@ -534,6 +542,7 @@ describe('lexical', () => { await payload.find({ collection: lexicalFieldsSlug, depth: 0, + overrideAccess: true, where: { title: { equals: lexicalDocData.title, @@ -680,6 +689,7 @@ describe('lexical', () => { await payload.find({ collection: lexicalFieldsSlug, depth: 0, + overrideAccess: true, where: { title: { equals: lexicalDocData.title, @@ -702,6 +712,189 @@ describe('lexical', () => { }) }) + // Big test which tests a bunch of things: Creation of blocks via slash commands, creation of deeply nested sub-lexical-block fields via slash commands, properly populated deeply nested fields within those + test('ensure creation of a lexical, lexical-field-block, which contains another lexical, lexical-and-upload-field-block, works and that the sub-upload field is properly populated', async () => { + await navigateToLexicalFields() + const richTextField = page.locator('.rich-text-lexical').nth(1) // second + await richTextField.scrollIntoViewIfNeeded() + await expect(richTextField).toBeVisible() + + const lastParagraph = richTextField.locator('p').last() + await lastParagraph.scrollIntoViewIfNeeded() + await expect(lastParagraph).toBeVisible() + + /** + * Create new sub-block + */ + // type / to open the slash menu + await lastParagraph.click() + await page.keyboard.press('/') + await page.keyboard.type('Rich') + + // Create Rich Text Block + const slashMenuPopover = page.locator('#slash-menu .slash-menu-popup') + await expect(slashMenuPopover).toBeVisible() + + // Click 1. Button and ensure it's the Rich Text block creation button (it should be! Otherwise, sorting wouldn't work) + const richTextBlockSelectButton = slashMenuPopover.locator('button').first() + await expect(richTextBlockSelectButton).toBeVisible() + await expect(richTextBlockSelectButton).toContainText('Rich Text') + await richTextBlockSelectButton.click() + await expect(slashMenuPopover).toBeHidden() + + const newRichTextBlock = richTextField + .locator('.lexical-block:not(.lexical-block .lexical-block)') + .last() // The :not(.lexical-block .lexical-block) makes sure this does not select sub-blocks + await newRichTextBlock.scrollIntoViewIfNeeded() + await expect(newRichTextBlock).toBeVisible() + + // Ensure that sub-editor is empty + const newRichTextEditorParagraph = newRichTextBlock.locator('p').first() + await expect(newRichTextEditorParagraph).toBeVisible() + await expect(newRichTextEditorParagraph).toHaveText('') + + await newRichTextEditorParagraph.click() + await page.keyboard.press('/') + await page.keyboard.type('Lexical') + await expect(slashMenuPopover).toBeVisible() + // Click 1. Button and ensure it's the Lexical And Upload block creation button (it should be! Otherwise, sorting wouldn't work) + const lexicalAndUploadBlockSelectButton = slashMenuPopover.locator('button').first() + await expect(lexicalAndUploadBlockSelectButton).toBeVisible() + await expect(lexicalAndUploadBlockSelectButton).toContainText('Lexical And Upload') + await lexicalAndUploadBlockSelectButton.click() + await expect(slashMenuPopover).toBeHidden() + + // Ensure that sub-editor is created + const newSubLexicalAndUploadBlock = newRichTextBlock.locator('.lexical-block').first() + await newSubLexicalAndUploadBlock.scrollIntoViewIfNeeded() + await expect(newSubLexicalAndUploadBlock).toBeVisible() + + // Type in newSubLexicalAndUploadBlock + const paragraphInSubEditor = newSubLexicalAndUploadBlock.locator('p').first() + await expect(paragraphInSubEditor).toBeVisible() + await paragraphInSubEditor.click() + await page.keyboard.type('Some subText') + // Upload something + const chooseExistingUploadButton = newSubLexicalAndUploadBlock + .locator('.upload__toggler.list-drawer__toggler') + .first() + await expect(chooseExistingUploadButton).toBeVisible() + await chooseExistingUploadButton.click() + await wait(500) // wait for drawer form state to initialize (it's a flake) + const uploadListDrawer = page.locator('dialog[id^=list-drawer_1_]').first() // IDs starting with list-drawer_1_ (there's some other symbol after the underscore) + await expect(uploadListDrawer).toBeVisible() + // find button which has a span with text "payload.jpg" and click it in playwright + const uploadButton = uploadListDrawer.locator('button').getByText('payload.jpg').first() + await expect(uploadButton).toBeVisible() + await uploadButton.click() + await expect(uploadListDrawer).toBeHidden() + // Check if the upload is there + await expect( + newSubLexicalAndUploadBlock.locator('.field-type.upload .file-meta__url a'), + ).toHaveText('payload.jpg') + // save document and assert + await saveDocAndAssert(page) + await expect( + newSubLexicalAndUploadBlock.locator('.field-type.upload .file-meta__url a'), + ).toHaveText('payload.jpg') + await expect(paragraphInSubEditor).toHaveText('Some subText') + + // reload page and assert again + await page.reload() + await expect( + newSubLexicalAndUploadBlock.locator('.field-type.upload .file-meta__url a'), + ).toHaveText('payload.jpg') + await expect(paragraphInSubEditor).toHaveText('Some subText') + + // Check if the API result is populated correctly - Depth 0 + await expect(async () => { + const lexicalDoc: LexicalField = ( + await payload.find({ + collection: lexicalFieldsSlug, + depth: 0, + overrideAccess: true, + where: { + title: { + equals: lexicalDocData.title, + }, + }, + }) + ).docs[0] as never + + const uploadDoc: Upload = ( + await payload.find({ + collection: 'uploads', + depth: 0, + overrideAccess: true, + where: { + filename: { + equals: 'payload.jpg', + }, + }, + }) + ).docs[0] as never + + const lexicalField: SerializedEditorState = lexicalDoc.lexicalWithBlocks + const richTextBlock: SerializedBlockNode = lexicalField.root + .children[12] as SerializedBlockNode + const subRichTextBlock: SerializedBlockNode = richTextBlock.fields.richTextField.root + .children[1] as SerializedBlockNode // index 0 and 2 are paragraphs created by default around the block node when a new block is added via slash command + + const subSubRichTextField = subRichTextBlock.fields.subRichTextField + const subSubUploadField = subRichTextBlock.fields.subUploadField + + expect(subSubRichTextField.root.children[0].children[0].text).toBe('Some subText') + expect(subSubUploadField).toBe(uploadDoc.id) + }).toPass({ + timeout: POLL_TOPASS_TIMEOUT, + }) + + // Check if the API result is populated correctly - Depth 1 + await expect(async () => { + // Now with depth 1 + const lexicalDocDepth1: LexicalField = ( + await payload.find({ + collection: lexicalFieldsSlug, + depth: 1, + overrideAccess: true, + where: { + title: { + equals: lexicalDocData.title, + }, + }, + }) + ).docs[0] as never + + const uploadDoc: Upload = ( + await payload.find({ + collection: 'uploads', + depth: 0, + overrideAccess: true, + where: { + filename: { + equals: 'payload.jpg', + }, + }, + }) + ).docs[0] as never + + const lexicalField2: SerializedEditorState = lexicalDocDepth1.lexicalWithBlocks + const richTextBlock2: SerializedBlockNode = lexicalField2.root + .children[12] as SerializedBlockNode + const subRichTextBlock2: SerializedBlockNode = richTextBlock2.fields.richTextField.root + .children[1] as SerializedBlockNode // index 0 and 2 are paragraphs created by default around the block node when a new block is added via slash command + + const subSubRichTextField2 = subRichTextBlock2.fields.subRichTextField + const subSubUploadField2 = subRichTextBlock2.fields.subUploadField + + expect(subSubRichTextField2.root.children[0].children[0].text).toBe('Some subText') + expect(subSubUploadField2.id).toBe(uploadDoc.id) + expect(subSubUploadField2.filename).toBe(uploadDoc.filename) + }).toPass({ + timeout: POLL_TOPASS_TIMEOUT, + }) + }) + test('should allow changing values of two different radio button blocks independently', async () => { // This test ensures that https://github.com/payloadcms/payload/issues/3911 does not happen again @@ -750,6 +943,7 @@ describe('lexical', () => { await payload.find({ collection: lexicalFieldsSlug, depth: 0, + overrideAccess: true, where: { title: { equals: lexicalDocData.title, diff --git a/test/fields/collections/Lexical/generateLexicalRichText.ts b/test/fields/collections/Lexical/generateLexicalRichText.ts index b38d2fd61b..08bedd6bb4 100644 --- a/test/fields/collections/Lexical/generateLexicalRichText.ts +++ b/test/fields/collections/Lexical/generateLexicalRichText.ts @@ -110,7 +110,7 @@ export function generateLexicalRichText() { version: 2, fields: { id: '65298b1ddb4ef8c744a7faab', - richText: { + richTextField: { root: { type: 'root', format: '', @@ -149,7 +149,7 @@ export function generateLexicalRichText() { }, }, blockName: 'Block Node, with RichText Field, with Relationship Node', - blockType: 'richText', + blockType: 'richTextBlock', }, }, { diff --git a/test/helpers.ts b/test/helpers.ts index 8211346fd9..7497a65d35 100644 --- a/test/helpers.ts +++ b/test/helpers.ts @@ -133,7 +133,7 @@ export async function openNav(page: Page): Promise { export async function openDocDrawer(page: Page, selector: string): Promise { await wait(300) // wait for parent form state to initialize await page.locator(selector).click({ delay: 100 }) - await wait(500) // wait for drawer form state to initializ + await wait(500) // wait for drawer form state to initialize } export async function closeNav(page: Page): Promise {