Files
payloadcms/test/fields/collections/Lexical/e2e/blocks/e2e.spec.ts

939 lines
37 KiB
TypeScript

import type { SerializedBlockNode, SerializedLinkNode } from '@payloadcms/richtext-lexical'
import type { BrowserContext, Page } from '@playwright/test'
import type { SerializedEditorState, SerializedParagraphNode, SerializedTextNode } from 'lexical'
import { expect, test } from '@playwright/test'
import path from 'path'
import { wait } from 'payload/utilities'
import { fileURLToPath } from 'url'
import type { PayloadTestSDK } from '../../../../../helpers/sdk/index.js'
import type { Config, LexicalField, Upload } from '../../../../payload-types.js'
import {
ensureAutoLoginAndCompilationIsDone,
initPageConsoleErrorCatch,
saveDocAndAssert,
} from '../../../../../helpers.js'
import { AdminUrlUtil } from '../../../../../helpers/adminUrlUtil.js'
import { initPayloadE2ENoConfig } from '../../../../../helpers/initPayloadE2ENoConfig.js'
import { reInitializeDB } from '../../../../../helpers/reInitializeDB.js'
import { RESTClient } from '../../../../../helpers/rest.js'
import { POLL_TOPASS_TIMEOUT, TEST_TIMEOUT_LONG } from '../../../../../playwright.config.js'
import { lexicalFieldsSlug } from '../../../../slugs.js'
import { lexicalDocData } from '../../data.js'
const filename = fileURLToPath(import.meta.url)
const currentFolder = path.dirname(filename)
const dirname = path.resolve(currentFolder, '../../../../')
const { beforeAll, beforeEach, describe } = test
let payload: PayloadTestSDK<Config>
let client: RESTClient
let page: Page
let context: BrowserContext
let serverURL: string
/**
* Client-side navigation to the lexical editor from list view
*/
async function navigateToLexicalFields(
navigateToListView: boolean = true,
localized: boolean = false,
) {
if (navigateToListView) {
const url: AdminUrlUtil = new AdminUrlUtil(
serverURL,
localized ? 'lexical-localized-fields' : 'lexical-fields',
)
await page.goto(url.list)
}
const linkToDoc = page.locator('tbody tr:first-child .cell-title a').first()
await expect(() => expect(linkToDoc).toBeTruthy()).toPass({ timeout: POLL_TOPASS_TIMEOUT })
const linkDocHref = await linkToDoc.getAttribute('href')
await linkToDoc.click()
await page.waitForURL(`**${linkDocHref}`)
}
describe('lexicalBlocks', () => {
beforeAll(async ({ browser }, testInfo) => {
testInfo.setTimeout(TEST_TIMEOUT_LONG)
process.env.SEED_IN_CONFIG_ONINIT = 'false' // Makes it so the payload config onInit seed is not run. Otherwise, the seed would be run unnecessarily twice for the initial test run - once for beforeEach and once for onInit
;({ payload, serverURL } = await initPayloadE2ENoConfig({ dirname }))
context = await browser.newContext()
page = await context.newPage()
initPageConsoleErrorCatch(page)
await reInitializeDB({
serverURL,
snapshotKey: 'fieldsLexicalBlocksTest',
uploadsDir: path.resolve(dirname, './collections/Upload/uploads'),
})
await ensureAutoLoginAndCompilationIsDone({ page, serverURL })
})
beforeEach(async () => {
/*await throttleTest({
page,
context,
delay: 'Slow 4G',
})*/
await reInitializeDB({
serverURL,
snapshotKey: 'fieldsLexicalBlocksTest',
uploadsDir: [
path.resolve(dirname, './collections/Upload/uploads'),
path.resolve(dirname, './collections/Upload2/uploads2'),
],
})
if (client) {
await client.logout()
}
client = new RESTClient(null, { defaultSlug: 'rich-text-fields', serverURL })
await client.login()
})
describe('nested lexical editor in block', () => {
test('should type and save typed text', async () => {
await navigateToLexicalFields()
const richTextField = page.locator('.rich-text-lexical').nth(1) // second
await richTextField.scrollIntoViewIfNeeded()
await expect(richTextField).toBeVisible()
const lexicalBlock = richTextField.locator('.lexical-block').nth(2) // third: "Block Node, with RichText Field, with Relationship Node"
await lexicalBlock.scrollIntoViewIfNeeded()
await expect(lexicalBlock).toBeVisible()
// Find span in contentEditable with text "Some text below relationship node"
const spanInSubEditor = lexicalBlock
.locator('span')
.getByText('Some text below relationship node 1')
.first()
await expect(spanInSubEditor).toBeVisible()
await spanInSubEditor.click() // Use click, because focus does not work
// Now go to the END of the span
for (let i = 0; i < 18; i++) {
await page.keyboard.press('ArrowRight')
}
await page.keyboard.type(' inserted text')
await expect(spanInSubEditor).toHaveText('Some text below relationship node 1 inserted text')
await saveDocAndAssert(page)
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 lexicalField: SerializedEditorState = lexicalDoc.lexicalWithBlocks
const blockNode: SerializedBlockNode = lexicalField.root.children[4] as SerializedBlockNode
const textNodeInBlockNodeRichText =
blockNode.fields.richTextField.root.children[1].children[0]
expect(textNodeInBlockNodeRichText.text).toBe(
'Some text below relationship node 1 inserted text',
)
}).toPass({
timeout: POLL_TOPASS_TIMEOUT,
})
})
test('should be able to bold text using floating select toolbar', async () => {
// Reproduces https://github.com/payloadcms/payload/issues/4025
await navigateToLexicalFields()
const richTextField = page.locator('.rich-text-lexical').nth(1) // second
await richTextField.scrollIntoViewIfNeeded()
await expect(richTextField).toBeVisible()
const lexicalBlock = richTextField.locator('.lexical-block').nth(2) // third: "Block Node, with RichText Field, with Relationship Node"
await lexicalBlock.scrollIntoViewIfNeeded()
await expect(lexicalBlock).toBeVisible()
// Find span in contentEditable with text "Some text below relationship node"
const spanInSubEditor = lexicalBlock
.locator('span')
.getByText('Some text below relationship node 1')
.first()
await expect(spanInSubEditor).toBeVisible()
await spanInSubEditor.click() // Use click, because focus does not work
// Now go to the END of the span while selecting the text
for (let i = 0; i < 18; i++) {
await page.keyboard.press('Shift+ArrowRight')
}
// The following text should now be selectedelationship node 1
const floatingToolbar_formatSection = page.locator('.inline-toolbar-popup__group-format')
await expect(floatingToolbar_formatSection).toBeVisible()
await expect(page.locator('.toolbar-popup__button').first()).toBeVisible()
const boldButton = floatingToolbar_formatSection.locator('.toolbar-popup__button').first()
await expect(boldButton).toBeVisible()
await boldButton.click()
/**
* Next test section: check if it worked correctly
*/
const boldText = lexicalBlock
.locator('.LexicalEditorTheme__paragraph')
.first()
.locator('strong')
await expect(boldText).toBeVisible()
await expect(boldText).toHaveText('elationship node 1')
await saveDocAndAssert(page)
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 lexicalField: SerializedEditorState = lexicalDoc.lexicalWithBlocks
const blockNode: SerializedBlockNode = lexicalField.root.children[4] as SerializedBlockNode
const paragraphNodeInBlockNodeRichText = blockNode.fields.richTextField.root.children[1]
expect(paragraphNodeInBlockNodeRichText.children).toHaveLength(2)
const textNode1: SerializedTextNode = paragraphNodeInBlockNodeRichText.children[0]
const boldNode: SerializedTextNode = paragraphNodeInBlockNodeRichText.children[1]
expect(textNode1.text).toBe('Some text below r')
expect(textNode1.format).toBe(0)
expect(boldNode.text).toBe('elationship node 1')
expect(boldNode.format).toBe(1)
}).toPass({
timeout: POLL_TOPASS_TIMEOUT,
})
})
test('should be able to select text, make it an external link and receive the updated link value', async () => {
// Reproduces https://github.com/payloadcms/payload/issues/4025
await navigateToLexicalFields()
const richTextField = page.locator('.rich-text-lexical').nth(1) // second
await richTextField.scrollIntoViewIfNeeded()
await expect(richTextField).toBeVisible()
// Find span in contentEditable with text "Some text below relationship node"
const spanInEditor = richTextField.locator('span').getByText('Upload Node:').first()
await expect(spanInEditor).toBeVisible()
await spanInEditor.click() // Use click, because focus does not work
await page.keyboard.press('ArrowRight')
// Now select some text
for (let i = 0; i < 4; i++) {
await page.keyboard.press('Shift+ArrowRight')
}
// The following text should now be "Node"
const floatingToolbar = page.locator('.inline-toolbar-popup')
await expect(floatingToolbar).toBeVisible()
const linkButton = floatingToolbar.locator('.toolbar-popup__button-link').first()
await expect(linkButton).toBeVisible()
await linkButton.click()
/**
* In drawer
*/
const drawerContent = page.locator('.drawer__content').first()
await expect(drawerContent).toBeVisible()
const urlField = drawerContent.locator('input#field-url').first()
await expect(urlField).toBeVisible()
// Fill with https://www.payloadcms.com
await urlField.fill('https://www.payloadcms.com')
await expect(urlField).toHaveValue('https://www.payloadcms.com')
await drawerContent.locator('.form-submit button').click({ delay: 100 })
await expect(drawerContent).toBeHidden()
/**
* check if it worked correctly
*/
const linkInEditor = richTextField.locator('a.LexicalEditorTheme__link').first()
await expect(linkInEditor).toBeVisible()
await expect(linkInEditor).toHaveAttribute('href', 'https://www.payloadcms.com')
await saveDocAndAssert(page)
// Check if it persists after saving
await expect(linkInEditor).toBeVisible()
await expect(linkInEditor).toHaveAttribute('href', 'https://www.payloadcms.com')
// Make sure it's being returned from the API as well
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 lexicalField: SerializedEditorState = lexicalDoc.lexicalWithBlocks
expect(
(
(lexicalField.root.children[0] as SerializedParagraphNode)
.children[1] as SerializedLinkNode
).fields.url,
).toBe('https://www.payloadcms.com')
}).toPass({
timeout: POLL_TOPASS_TIMEOUT,
})
})
test('ensure slash menu is not hidden behind other blocks', async () => {
// This test makes sure there are no z-index issues here
await navigateToLexicalFields()
const richTextField = page.locator('.rich-text-lexical').nth(1) // second
await richTextField.scrollIntoViewIfNeeded()
await expect(richTextField).toBeVisible()
const lexicalBlock = richTextField.locator('.lexical-block').nth(2) // third: "Block Node, with RichText Field, with Relationship Node"
await lexicalBlock.scrollIntoViewIfNeeded()
await expect(lexicalBlock).toBeVisible()
// Find span in contentEditable with text "Some text below relationship node"
const spanInSubEditor = lexicalBlock
.locator('span')
.getByText('Some text below relationship node 1')
.first()
await expect(spanInSubEditor).toBeVisible()
await spanInSubEditor.click() // Use click, because focus does not work
// Now go to the END of the span
for (let i = 0; i < 18; i++) {
await page.keyboard.press('ArrowRight')
}
// Now scroll down, so that the following slash menu is positioned below the cursor and not above it
await page.mouse.wheel(0, 600)
await page.keyboard.press('Enter')
await page.keyboard.press('/')
const popover = page.locator('#slash-menu .slash-menu-popup')
await expect(popover).toBeVisible()
const popoverBasicGroup = popover
.locator('.slash-menu-popup__group.slash-menu-popup__group-basic')
.first() // Second group ("Basic") in popover
await expect(popoverBasicGroup).toBeVisible()
// Heading 2 should be the last, most bottom popover button element which should be initially visible, if not hidden by something (e.g. another block)
const popoverHeading2Button = popoverBasicGroup
.locator('button.slash-menu-popup__item-heading-2')
.first()
await expect(popoverHeading2Button).toBeVisible()
await expect(async () => {
// Make sure that, even though it's "visible", it's not actually covered by something else due to z-index issues
const popoverHeading2ButtonBoundingBox = await popoverHeading2Button.boundingBox()
expect(popoverHeading2ButtonBoundingBox).not.toBeNull()
expect(popoverHeading2ButtonBoundingBox).not.toBeUndefined()
expect(popoverHeading2ButtonBoundingBox.height).toBeGreaterThan(0)
expect(popoverHeading2ButtonBoundingBox.width).toBeGreaterThan(0)
// Now click the button to see if it actually works. Simulate an actual mouse click instead of using .click()
// by using page.mouse and the correct coordinates
// .isVisible() and .click() might work fine EVEN if the slash menu is not actually visible by humans
// see: https://github.com/microsoft/playwright/issues/9923
// This is why we use page.mouse.click() here. It's the most effective way of detecting such a z-index issue
// and usually the only method which works.
const x = popoverHeading2ButtonBoundingBox.x
const y = popoverHeading2ButtonBoundingBox.y
await page.mouse.click(x, y, { button: 'left' })
await page.keyboard.type('A Heading')
const newHeadingInSubEditor = lexicalBlock.locator('p ~ h2').getByText('A Heading').first()
await expect(newHeadingInSubEditor).toBeVisible()
await expect(newHeadingInSubEditor).toHaveText('A Heading')
}).toPass({
timeout: POLL_TOPASS_TIMEOUT,
})
})
test('should allow adding new blocks to a sub-blocks field, part of a parent lexical blocks field', async () => {
await navigateToLexicalFields()
const richTextField = page.locator('.rich-text-lexical').nth(1) // second
await richTextField.scrollIntoViewIfNeeded()
await expect(richTextField).toBeVisible()
const lexicalBlock = richTextField.locator('.lexical-block').nth(3) // third: "Block Node, with Blocks Field, With RichText Field, With Relationship Node"
await lexicalBlock.scrollIntoViewIfNeeded()
await expect(lexicalBlock).toBeVisible()
/**
* Create new textarea sub-block
*/
await lexicalBlock.locator('button').getByText('Add Sub Block').click()
const drawerContent = page.locator('.drawer__content').first()
await expect(drawerContent).toBeVisible()
const textAreaAddBlockButton = drawerContent.locator('button').getByText('Text Area').first()
await expect(textAreaAddBlockButton).toBeVisible()
await textAreaAddBlockButton.click()
/**
* Check if it was created successfully and
* fill newly created textarea sub-block with text
*/
const newSubBlock = lexicalBlock.locator('.blocks-field__rows > div').nth(1)
await expect(newSubBlock).toBeVisible()
const newContentTextArea = newSubBlock.locator('textarea').first()
await expect(newContentTextArea).toBeVisible()
// Type 'Some text in new sub block content textArea'
await newContentTextArea.click()
// Even though we could use newContentTextArea.fill, it's still nice to use .type here,
// as this also tests that this text area still receives keyboard input events properly. It's more realistic.
await page.keyboard.type('text123')
await expect(newContentTextArea).toHaveText('text123')
await saveDocAndAssert(page)
await expect(async () => {
/**
* Using the local API, check if the data was saved correctly and
* can be retrieved correctly
*/
const lexicalDoc: LexicalField = (
await payload.find({
collection: lexicalFieldsSlug,
depth: 0,
overrideAccess: true,
where: {
title: {
equals: lexicalDocData.title,
},
},
})
).docs[0] as never
const lexicalField: SerializedEditorState = lexicalDoc.lexicalWithBlocks
const blockNode: SerializedBlockNode = lexicalField.root.children[5] as SerializedBlockNode
const subBlocks = blockNode.fields.subBlocks
expect(subBlocks).toHaveLength(2)
const createdTextAreaBlock = subBlocks[1]
expect(createdTextAreaBlock.content).toBe('text123')
}).toPass({
timeout: POLL_TOPASS_TIMEOUT,
})
})
// 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
await expect(async () => {
const chooseExistingUploadButton = newSubLexicalAndUploadBlock
.locator('.upload__toggler.list-drawer__toggler')
.first()
await wait(300)
await expect(chooseExistingUploadButton).toBeVisible()
await wait(300)
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()
await wait(300)
// 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 wait(300)
await uploadButton.click()
await wait(300)
await expect(uploadListDrawer).toBeHidden()
// Check if the upload is there
await expect(
newSubLexicalAndUploadBlock.locator('.field-type.upload .file-meta__url a'),
).toHaveText('payload.jpg')
}).toPass({
timeout: POLL_TOPASS_TIMEOUT,
})
await wait(300)
// save document and assert
await saveDocAndAssert(page)
await wait(300)
await expect(
newSubLexicalAndUploadBlock.locator('.field-type.upload .file-meta__url a'),
).toHaveText('payload.jpg')
await expect(paragraphInSubEditor).toHaveText('Some subText')
await wait(300)
// reload page and assert again
await page.reload()
await wait(300)
await newSubLexicalAndUploadBlock.scrollIntoViewIfNeeded()
await expect(newSubLexicalAndUploadBlock).toBeVisible()
await newSubLexicalAndUploadBlock
.locator('.field-type.upload .file-meta__url a')
.scrollIntoViewIfNeeded()
await expect(
newSubLexicalAndUploadBlock.locator('.field-type.upload .file-meta__url a'),
).toBeVisible()
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[13] 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[13] 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
await navigateToLexicalFields()
const richTextField = page.locator('.rich-text-lexical').nth(1) // second
await richTextField.scrollIntoViewIfNeeded()
await expect(richTextField).toBeVisible()
const radioButtonBlock1 = richTextField.locator('.lexical-block').nth(5)
const radioButtonBlock2 = richTextField.locator('.lexical-block').nth(6)
await radioButtonBlock2.scrollIntoViewIfNeeded()
await expect(radioButtonBlock1).toBeVisible()
await expect(radioButtonBlock2).toBeVisible()
// Click radio button option2 of radioButtonBlock1
await radioButtonBlock1
.locator('.radio-input:has-text("Option 2")')
.first() // This already is an input for some reason
.click()
// Ensure radio button option1 of radioButtonBlock2 (the default option) is still selected
await expect(
radioButtonBlock2.locator('.radio-input:has-text("Option 1")').first(),
).toBeChecked()
// Click radio button option3 of radioButtonBlock2
await radioButtonBlock2
.locator('.radio-input:has-text("Option 3")')
.first() // This already is an input for some reason
.click()
// Ensure previously clicked option2 of radioButtonBlock1 is still selected
await expect(
radioButtonBlock1.locator('.radio-input:has-text("Option 2")').first(),
).toBeChecked()
/**
* Now save and check the actual data. radio button block 1 should have option2 selected and radio button block 2 should have option3 selected
*/
await saveDocAndAssert(page)
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 lexicalField: SerializedEditorState = lexicalDoc.lexicalWithBlocks
const radio1: SerializedBlockNode = lexicalField.root.children[8] as SerializedBlockNode
const radio2: SerializedBlockNode = lexicalField.root.children[9] as SerializedBlockNode
expect(radio1.fields.radioButtons).toBe('option2')
expect(radio2.fields.radioButtons).toBe('option3')
}).toPass({
timeout: POLL_TOPASS_TIMEOUT,
})
})
test('should not lose focus when writing in nested editor', async () => {
// https://github.com/payloadcms/payload/issues/4108
// Steps:
// 1. Focus parent editor
// 2. Focus nested editor and write something
// 3. In the issue, after writing one character, the cursor focuses back into the parent editor
await navigateToLexicalFields()
const richTextField = page.locator('.rich-text-lexical').nth(1) // second
await richTextField.scrollIntoViewIfNeeded()
await expect(richTextField).toBeVisible()
/**
* 1. Focus parent editor
*/
const parentEditorParagraph = richTextField.locator('span').getByText('Upload Node:').first()
await expect(parentEditorParagraph).toBeVisible()
await parentEditorParagraph.click() // Click works better than focus
const blockWithRichTextEditor = richTextField.locator('.lexical-block').nth(2) // third: "Block Node, with Blocks Field, With RichText Field, With Relationship Node"
await blockWithRichTextEditor.scrollIntoViewIfNeeded()
await expect(blockWithRichTextEditor).toBeVisible()
/**
* 2. Focus nested editor and write something
*/
const nestedEditorParagraph = blockWithRichTextEditor
.locator('span')
.getByText('Some text below relationship node 1')
.first()
await expect(nestedEditorParagraph).toBeVisible()
await nestedEditorParagraph.click() // Click works better than focus
// Now go to the END of the paragraph
for (let i = 0; i < 18; i++) {
await page.keyboard.press('ArrowRight')
}
await page.keyboard.type('2345')
/**
* 3. In the issue, after writing one character, the cursor focuses back into the parent editor and writes the text there.
* This checks that this does not happen, and that it writes the text in the correct position (so, in nestedEditorParagraph, NOT in parentEditorParagraph)
*/
await expect(nestedEditorParagraph).toHaveText('Some text below relationship node 12345')
})
const shouldRespectRowRemovalTest = async () => {
const richTextField = page.locator('.rich-text-lexical').nth(1) // second
await richTextField.scrollIntoViewIfNeeded()
await expect(richTextField).toBeVisible()
const conditionalArrayBlock = richTextField.locator('.lexical-block').nth(7)
await conditionalArrayBlock.scrollIntoViewIfNeeded()
await expect(conditionalArrayBlock).toBeVisible()
const selectField = conditionalArrayBlock.locator('.react-select').first()
await selectField.click()
const selectFieldMenu = selectField.locator('.rs__menu').first()
await selectFieldMenu.locator('.rs__option').nth(1).click() // Select "2" (2 columns / array fields)
// Make sure the OTHER arrays aren't visible, as their conditions are not fulfilled. Catches a bug where they might not be hidden fully
await expect(
conditionalArrayBlock.locator('.btn__label:has-text("Add Columns1")'),
).toBeHidden()
await expect(conditionalArrayBlock.locator('.row-label:has-text("Column 01")')).toBeHidden()
await expect(
conditionalArrayBlock.locator('.btn__label:has-text("Add Columns3")'),
).toBeHidden()
await expect(conditionalArrayBlock.locator('.row-label:has-text("Column 03")')).toBeHidden()
await conditionalArrayBlock.locator('.btn__label:has-text("Add Columns2")').first().click()
await expect(
conditionalArrayBlock.locator('.array-field__draggable-rows #columns2-row-0'),
).toBeVisible()
await conditionalArrayBlock.locator('.btn__label:has-text("Add Columns2")').first().click()
await expect(
conditionalArrayBlock.locator('.array-field__draggable-rows #columns2-row-1'),
).toBeVisible()
await conditionalArrayBlock
.locator('.array-field__draggable-rows > div:nth-child(2) .field-type.text input')
.fill('second input')
await saveDocAndAssert(page)
await expect(page.locator('.Toastify')).not.toContainText('Please correct invalid fields.')
}
// eslint-disable-next-line playwright/expect-expect
test('should respect row removal in nested array field', async () => {
await navigateToLexicalFields()
await shouldRespectRowRemovalTest()
})
test('should respect row removal in nested array field after navigating away from lexical document, then navigating back', async () => {
// This test verifies an issue where a lexical editor with blocks disappears when navigating away from the lexical document, then navigating back, without a hard refresh
await navigateToLexicalFields()
// Wait for lexical to be loaded up fully
const richTextField = page.locator('.rich-text-lexical').nth(1) // second
await richTextField.scrollIntoViewIfNeeded()
await expect(richTextField).toBeVisible()
const conditionalArrayBlock = richTextField.locator('.lexical-block').nth(7)
await conditionalArrayBlock.scrollIntoViewIfNeeded()
await expect(conditionalArrayBlock).toBeVisible()
// navigate to list view
await page.locator('.step-nav a').nth(1).click()
await page.waitForURL('**/lexical-fields?limit=10')
// Click on lexical document in list view (navigateToLexicalFields is client-side navigation which is what we need to reproduce the issue here)
await navigateToLexicalFields(false)
await shouldRespectRowRemovalTest()
})
test('ensure pre-seeded uploads node is visible', async () => {
// Due to issues with the relationships condition, we had issues with that not being visible. Checking for visibility ensures there is no breakage there again
await navigateToLexicalFields()
const richTextField = page.locator('.rich-text-lexical').nth(1) // second
await richTextField.scrollIntoViewIfNeeded()
await expect(richTextField).toBeVisible()
const uploadBlock = richTextField.locator('.ContentEditable__root > div').first() // Check for the first div, as we wanna make sure it's the first div in the editor (1. node is a paragraph, second node is a div which is the upload node)
await uploadBlock.scrollIntoViewIfNeeded()
await expect(uploadBlock).toBeVisible()
await expect(uploadBlock.locator('.lexical-upload__doc-drawer-toggler strong')).toHaveText(
'payload.jpg',
)
})
test('should respect required error state in deeply nested text field', async () => {
await navigateToLexicalFields()
await wait(300)
const richTextField = page.locator('.rich-text-lexical').nth(1) // second
await wait(300)
await richTextField.scrollIntoViewIfNeeded()
await wait(300)
await expect(richTextField).toBeVisible()
await wait(300)
const conditionalArrayBlock = richTextField.locator('.lexical-block').nth(7)
await wait(300)
await conditionalArrayBlock.scrollIntoViewIfNeeded()
await wait(300)
await expect(conditionalArrayBlock).toBeVisible()
await wait(300)
const addSubArrayButton = conditionalArrayBlock.locator(
'.btn__label:has-text("Add Sub Array")',
)
await addSubArrayButton.scrollIntoViewIfNeeded()
await wait(300)
await expect(addSubArrayButton).toBeVisible()
await wait(300)
await addSubArrayButton.first().click()
await wait(300)
await page.click('#action-save', { delay: 100 })
await wait(300)
await expect(page.locator('.Toastify')).toContainText('The following field is invalid')
await wait(300)
const requiredTooltip = conditionalArrayBlock
.locator('.tooltip-content:has-text("This field is required.")')
.first()
await wait(300)
await requiredTooltip.scrollIntoViewIfNeeded()
// Check if error is shown next to field
await wait(300)
await expect(requiredTooltip).toBeInViewport() // toBeVisible() doesn't work for some reason
})
})
})