Merge pull request #5436 from payloadcms/temp11

chore: improvements to eslint, and access-control + lexical test suites
This commit is contained in:
Alessio Gravili
2024-03-25 10:48:27 -04:00
committed by GitHub
10 changed files with 275 additions and 221 deletions

View File

@@ -43,40 +43,74 @@ module.exports = {
'stringMatching',
]
function isNonRetryableAssertion(node) {
return (
node.type === 'MemberExpression' &&
node.property.type === 'Identifier' &&
nonRetryableAssertions.includes(node.property.name)
)
}
function isExpectPollOrToPass(node) {
if (
node.type === 'MemberExpression' &&
(node?.property?.name === 'poll' || node?.property?.name === 'toPass')
) {
return true
}
return (
node.type === 'CallExpression' &&
node.callee.type === 'MemberExpression' &&
((node.callee.object.type === 'CallExpression' &&
node.callee.object.callee.type === 'MemberExpression' &&
node.callee.object.callee.property.name === 'poll') ||
node.callee.property.name === 'toPass')
)
}
function hasExpectPollOrToPassInChain(node) {
let ancestor = node
while (ancestor) {
if (isExpectPollOrToPass(ancestor)) {
return true
}
ancestor = 'object' in ancestor ? ancestor.object : ancestor.callee
}
return false
}
function hasExpectPollOrToPassInParentChain(node) {
let ancestor = node
while (ancestor) {
if (isExpectPollOrToPass(ancestor)) {
return true
}
ancestor = ancestor.parent
}
return false
}
return {
CallExpression(node) {
if (
node.callee.type === 'MemberExpression' &&
//node.callee.object.name === 'expect' &&
node.callee.property.type === 'Identifier' &&
nonRetryableAssertions.includes(node.callee.property.name)
) {
let ancestor = node
let hasExpectPollOrToPass = false
while (ancestor) {
if (
ancestor.type === 'CallExpression' &&
ancestor.callee.type === 'MemberExpression' &&
((ancestor.callee.object.type === 'CallExpression' &&
ancestor.callee.object.callee.type === 'MemberExpression' &&
ancestor.callee.object.callee.property.name === 'poll') ||
ancestor.callee.property.name === 'toPass')
) {
hasExpectPollOrToPass = true
break
}
ancestor = ancestor.parent
// node.callee is MemberExpressiom
if (isNonRetryableAssertion(node.callee)) {
if (hasExpectPollOrToPassInChain(node.callee)) {
return
}
if (hasExpectPollOrToPass) {
if (hasExpectPollOrToPassInParentChain(node)) {
return
}
context.report({
node: node.callee.property,
message:
'Non-retryable, flaky assertion used in Playwright test: "{{ assertion }}". Those need to be wrapped in expect.poll() or expect().toPass.',
'Non-retryable, flaky assertion used in Playwright test: "{{ assertion }}". Those need to be wrapped in expect.poll() or expect().toPass().',
data: {
assertion: node.callee.property.name,
},

View File

@@ -1,6 +1,5 @@
import type { Field, SanitizedConfig } from 'payload/types'
import { sanitizeFields } from 'payload/config'
import { tabHasName } from 'payload/types'
import type { FieldSchemaMap } from './types.js'
@@ -21,16 +20,10 @@ export const traverseFields = ({
validRelationships,
}: Args) => {
fields.map((field) => {
let fieldsToSet
switch (field.type) {
case 'group':
case 'array':
fieldsToSet = sanitizeFields({
config,
fields: field.fields,
validRelationships,
})
schemaMap.set(`${schemaPath}.${field.name}`, fieldsToSet)
schemaMap.set(`${schemaPath}.${field.name}`, field.fields)
traverseFields({
config,
@@ -55,12 +48,8 @@ export const traverseFields = ({
case 'blocks':
field.blocks.map((block) => {
const blockSchemaPath = `${schemaPath}.${field.name}.${block.slug}`
fieldsToSet = sanitizeFields({
config,
fields: [...block.fields, { name: 'blockName', type: 'text' }],
validRelationships,
})
schemaMap.set(blockSchemaPath, fieldsToSet)
schemaMap.set(blockSchemaPath, block.fields)
traverseFields({
config,
@@ -88,12 +77,7 @@ export const traverseFields = ({
const tabSchemaPath = tabHasName(tab) ? `${schemaPath}.${tab.name}` : schemaPath
if (tabHasName(tab)) {
fieldsToSet = sanitizeFields({
config,
fields: tab.fields,
validRelationships,
})
schemaMap.set(tabSchemaPath, fieldsToSet)
schemaMap.set(tabSchemaPath, tab.fields)
}
traverseFields({

View File

@@ -37,7 +37,9 @@ type Props = {
formData: BlockFields
formSchema: FieldMap
nodeKey: string
path: string
reducedBlock: ReducedBlock
schemaPath: string
}
/**
@@ -52,7 +54,9 @@ export const BlockContent: React.FC<Props> = (props) => {
formData,
formSchema,
nodeKey,
path,
reducedBlock: { labels },
schemaPath,
} = props
const { i18n } = useTranslation()
@@ -236,9 +240,9 @@ export const BlockContent: React.FC<Props> = (props) => {
fieldMap={Array.isArray(formSchema) ? formSchema : []}
forceRender
margins="small"
path=""
path={path}
readOnly={false}
schemaPath=""
schemaPath={schemaPath}
/>
</Collapsible>

View File

@@ -42,7 +42,7 @@ export const BlockComponent: React.FC<Props> = (props) => {
const config = useConfig()
const submitted = useFormSubmitted()
const { id } = useDocumentInfo()
const { schemaPath } = useFieldProps()
const { path, schemaPath } = useFieldProps()
const { editorConfig, field: parentLexicalRichTextField } = useEditorConfigContext()
const [initialState, setInitialState] = useState<FormState | false>(false)
@@ -137,7 +137,9 @@ export const BlockComponent: React.FC<Props> = (props) => {
formData={formData}
formSchema={Array.isArray(fieldMap) ? fieldMap : []}
nodeKey={nodeKey}
path={`${path}.feature.blocks.${formData.blockType}`}
reducedBlock={reducedBlock}
schemaPath={schemaFieldsPath}
/>
</Form>
)

View File

@@ -1,9 +1,8 @@
import type { Page } from '@playwright/test'
import type { Payload } from 'payload/types'
import type { Payload, TypeWithID } from 'payload/types'
import { expect, test } from '@playwright/test'
import path from 'path'
import { wait } from 'payload/utilities'
import { fileURLToPath } from 'url'
import type { ReadOnlyCollection, RestrictedVersion } from './payload-types.js'
@@ -134,7 +133,6 @@ describe('access control', () => {
describe('restricted fields', () => {
test('should not show field without permission', async () => {
await page.goto(url.account)
await wait(500)
await expect(page.locator('#field-roles')).toBeHidden()
})
})
@@ -163,7 +161,7 @@ describe('access control', () => {
test('should have collection url', async () => {
await page.goto(readOnlyUrl.list)
await expect(page).toHaveURL(readOnlyUrl.list) // no redirect
await expect(page).toHaveURL(new RegExp(`${readOnlyUrl.list}.*`)) // will redirect to ?limit=10 at the end, so we have to use a wildcard at the end
})
test('should not have "Create New" button', async () => {
@@ -188,7 +186,6 @@ describe('access control', () => {
test('should not render dot menu popup when `create` and `delete` access control is set to false', async () => {
await page.goto(readOnlyUrl.edit(existingDoc.id))
await wait(1000)
await expect(page.locator('.collection-edit .doc-controls .doc-controls__popup')).toBeHidden()
})
})
@@ -213,7 +210,7 @@ describe('access control', () => {
describe('doc level access', () => {
let existingDoc: ReadOnlyCollection
let docLevelAccessURL
let docLevelAccessURL: AdminUrlUtil
beforeAll(async () => {
docLevelAccessURL = new AdminUrlUtil(serverURL, docLevelAccessSlug)
@@ -265,28 +262,26 @@ describe('access control', () => {
const unrestrictedURL = new AdminUrlUtil(serverURL, unrestrictedSlug)
await page.goto(unrestrictedURL.edit(unrestrictedDoc.id))
const button = page.locator(
const addDocButton = page.locator(
'#userRestrictedDocs-add-new button.relationship-add-new__add-button.doc-drawer__toggler',
)
await button.click()
await addDocButton.click()
const documentDrawer = page.locator('[id^=doc-drawer_user-restricted_1_]')
await expect(documentDrawer).toBeVisible()
await documentDrawer.locator('#field-name').fill('anonymous@email.com')
await documentDrawer.locator('#action-save').click()
await wait(200)
await expect(page.locator('.Toastify')).toContainText('successfully')
// ensure user is not allowed to edit this document
await expect(documentDrawer.locator('#field-name')).toBeDisabled()
await documentDrawer.locator('button.doc-drawer__header-close').click()
await wait(200)
await expect(documentDrawer).toBeHidden()
await button.click()
await addDocButton.click()
const documentDrawer2 = page.locator('[id^=doc-drawer_user-restricted_1_]')
await expect(documentDrawer2).toBeVisible()
await documentDrawer2.locator('#field-name').fill('dev@payloadcms.com')
await documentDrawer2.locator('#action-save').click()
await wait(200)
await expect(page.locator('.Toastify')).toContainText('successfully')
// ensure user is allowed to edit this document
@@ -294,7 +289,7 @@ describe('access control', () => {
})
})
async function createDoc(data: any): Promise<{ id: string }> {
async function createDoc(data: any): Promise<TypeWithID & Record<string, unknown>> {
return payload.create({
collection: slug,
data,

View File

@@ -8,6 +8,7 @@ import { fileURLToPath } from 'url'
import { delayNetwork, initPageConsoleErrorCatch, login, saveDocAndAssert } from '../helpers.js'
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
import { initPayloadE2E } from '../helpers/initPayloadE2E.js'
import { POLL_TOPASS_TIMEOUT } from '../playwright.config.js'
import config from './config.js'
import { apiKeysSlug, slug } from './shared.js'
const filename = fileURLToPath(import.meta.url)
@@ -105,7 +106,7 @@ describe('auth', () => {
// assert that the value is set
const apiKeyLocator = page.locator('#apiKey')
await expect
.poll(async () => await apiKeyLocator.inputValue(), { timeout: 45000 })
.poll(async () => await apiKeyLocator.inputValue(), { timeout: POLL_TOPASS_TIMEOUT })
.toBeDefined()
await saveDocAndAssert(page)
@@ -114,7 +115,7 @@ describe('auth', () => {
const apiKey = await apiKeyLocator.inputValue()
expect(await page.locator('#apiKey').inputValue()).toStrictEqual(apiKey)
}).toPass({
timeout: 45000,
timeout: POLL_TOPASS_TIMEOUT,
})
})
@@ -140,7 +141,7 @@ describe('auth', () => {
expect(response.user).toBeNull()
}).toPass({
timeout: 45000,
timeout: POLL_TOPASS_TIMEOUT,
})
})
})

View File

@@ -13,6 +13,7 @@ import { initPageConsoleErrorCatch, saveDocAndAssert } from '../helpers.js'
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
import { initPayloadE2E } from '../helpers/initPayloadE2E.js'
import { RESTClient } from '../helpers/rest.js'
import { POLL_TOPASS_TIMEOUT } from '../playwright.config.js'
import { lexicalDocData } from './collections/Lexical/data.js'
import config from './config.js'
import { clearAndSeedEverything } from './seed.js'
@@ -27,12 +28,6 @@ let client: RESTClient
let page: Page
let serverURL: string
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async function navigateToRichTextFields() {
const url: AdminUrlUtil = new AdminUrlUtil(serverURL, 'rich-text-fields')
await page.goto(url.list)
await page.locator('.row-1 .cell-title a').click()
}
async function navigateToLexicalFields() {
const url: AdminUrlUtil = new AdminUrlUtil(serverURL, 'lexical-fields')
await page.goto(url.list)
@@ -70,7 +65,7 @@ describe('lexical', () => {
await page.locator('.app-header__step-nav').first().locator('a').first().click()
// Make sure .leave-without-saving__content (the "Leave without saving") is not visible
await expect(page.locator('.leave-without-saving__content').first()).not.toBeVisible()
await expect(page.locator('.leave-without-saving__content').first()).toBeHidden()
})
test('should not warn about unsaved changes when navigating to lexical editor with blocks node and then leaving the page after making a change and saving', async () => {
@@ -105,7 +100,7 @@ describe('lexical', () => {
await page.locator('.app-header__step-nav').first().locator('a').first().click()
// Make sure .leave-without-saving__content (the "Leave without saving") is not visible
await expect(page.locator('.leave-without-saving__content').first()).not.toBeVisible()
await expect(page.locator('.leave-without-saving__content').first()).toBeHidden()
})
test('should type and save typed text', async () => {
@@ -128,24 +123,28 @@ describe('lexical', () => {
await saveDocAndAssert(page)
const lexicalDoc: LexicalField = (
await payload.find({
collection: lexicalFieldsSlug,
depth: 0,
where: {
title: {
equals: lexicalDocData.title,
await expect(async () => {
const lexicalDoc: LexicalField = (
await payload.find({
collection: lexicalFieldsSlug,
depth: 0,
where: {
title: {
equals: lexicalDocData.title,
},
},
},
})
).docs[0] as never
})
).docs[0] as never
const lexicalField: SerializedEditorState = lexicalDoc.lexicalWithBlocks
const firstParagraphTextNode: SerializedTextNode = (
lexicalField.root.children[0] as SerializedParagraphNode
).children[0] as SerializedTextNode
const lexicalField: SerializedEditorState = lexicalDoc.lexicalWithBlocks
const firstParagraphTextNode: SerializedTextNode = (
lexicalField.root.children[0] as SerializedParagraphNode
).children[0] as SerializedTextNode
expect(firstParagraphTextNode.text).toBe('Upload Node:moretext')
expect(firstParagraphTextNode.text).toBe('Upload Node:moretext')
}).toPass({
timeout: POLL_TOPASS_TIMEOUT,
})
})
test('should be able to bold text using floating select toolbar', async () => {
await navigateToLexicalFields()
@@ -193,35 +192,39 @@ describe('lexical', () => {
await saveDocAndAssert(page)
const lexicalDoc: LexicalField = (
await payload.find({
collection: lexicalFieldsSlug,
depth: 0,
where: {
title: {
equals: lexicalDocData.title,
await expect(async () => {
const lexicalDoc: LexicalField = (
await payload.find({
collection: lexicalFieldsSlug,
depth: 0,
where: {
title: {
equals: lexicalDocData.title,
},
},
},
})
).docs[0] as never
})
).docs[0] as never
const lexicalField: SerializedEditorState = lexicalDoc.lexicalWithBlocks
const firstParagraph: SerializedParagraphNode = lexicalField.root
.children[0] as SerializedParagraphNode
expect(firstParagraph.children).toHaveLength(3)
const lexicalField: SerializedEditorState = lexicalDoc.lexicalWithBlocks
const firstParagraph: SerializedParagraphNode = lexicalField.root
.children[0] as SerializedParagraphNode
expect(firstParagraph.children).toHaveLength(3)
const textNode1: SerializedTextNode = firstParagraph.children[0] as SerializedTextNode
const boldNode: SerializedTextNode = firstParagraph.children[1] as SerializedTextNode
const textNode2: SerializedTextNode = firstParagraph.children[2] as SerializedTextNode
const textNode1: SerializedTextNode = firstParagraph.children[0] as SerializedTextNode
const boldNode: SerializedTextNode = firstParagraph.children[1] as SerializedTextNode
const textNode2: SerializedTextNode = firstParagraph.children[2] as SerializedTextNode
expect(textNode1.text).toBe('Upload ')
expect(textNode1.format).toBe(0)
expect(textNode1.text).toBe('Upload ')
expect(textNode1.format).toBe(0)
expect(boldNode.text).toBe('Node')
expect(boldNode.format).toBe(1)
expect(boldNode.text).toBe('Node')
expect(boldNode.format).toBe(1)
expect(textNode2.text).toBe(':')
expect(textNode2.format).toBe(0)
expect(textNode2.text).toBe(':')
expect(textNode2.format).toBe(0)
}).toPass({
timeout: POLL_TOPASS_TIMEOUT,
})
})
test('Make sure highly specific issue does not occur when two richText fields share the same editor prop', async () => {
@@ -239,10 +242,13 @@ describe('lexical', () => {
await expect(richTextField).toBeVisible()
const contentEditable = richTextField.locator('.ContentEditable__root').first()
const textContent = await contentEditable.textContent()
expect(textContent).not.toBe('some text')
expect(textContent).toBe('')
await expect
.poll(async () => await contentEditable.textContent(), { timeout: POLL_TOPASS_TIMEOUT })
.not.toBe('some text')
await expect
.poll(async () => await contentEditable.textContent(), { timeout: POLL_TOPASS_TIMEOUT })
.toBe('')
})
test('ensure blocks content is not hidden behind components outside of the editor', async () => {
@@ -283,23 +289,27 @@ describe('lexical', () => {
const popover = page.locator('.rs__menu').first()
const popoverOption3 = popover.locator('.rs__option').nth(2)
const popoverOption3BoundingBox = await popoverOption3.boundingBox()
expect(popoverOption3BoundingBox).not.toBeNull()
expect(popoverOption3BoundingBox).not.toBeUndefined()
expect(popoverOption3BoundingBox.height).toBeGreaterThan(0)
expect(popoverOption3BoundingBox.width).toBeGreaterThan(0)
await expect(async () => {
const popoverOption3BoundingBox = await popoverOption3.boundingBox()
expect(popoverOption3BoundingBox).not.toBeNull()
expect(popoverOption3BoundingBox).not.toBeUndefined()
expect(popoverOption3BoundingBox.height).toBeGreaterThan(0)
expect(popoverOption3BoundingBox.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.
// 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 = popoverOption3BoundingBox.x
const y = popoverOption3BoundingBox.y
const x = popoverOption3BoundingBox.x
const y = popoverOption3BoundingBox.y
await page.mouse.click(x, y, { button: 'left' })
await page.mouse.click(x, y, { button: 'left' })
}).toPass({
timeout: POLL_TOPASS_TIMEOUT,
})
await expect(reactSelect.locator('.rs__value-container').first()).toHaveText('Option 3')
})
@@ -332,25 +342,29 @@ describe('lexical', () => {
await expect(spanInSubEditor).toHaveText('Some text below relationship node 1 inserted text')
await saveDocAndAssert(page)
const lexicalDoc: LexicalField = (
await payload.find({
collection: lexicalFieldsSlug,
depth: 0,
where: {
title: {
equals: lexicalDocData.title,
await expect(async () => {
const lexicalDoc: LexicalField = (
await payload.find({
collection: lexicalFieldsSlug,
depth: 0,
where: {
title: {
equals: lexicalDocData.title,
},
},
},
})
).docs[0] as never
})
).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 lexicalField: SerializedEditorState = lexicalDoc.lexicalWithBlocks
const blockNode: SerializedBlockNode = lexicalField.root.children[4] as SerializedBlockNode
const textNodeInBlockNodeRichText = blockNode.fields.richText.root.children[1].children[0]
expect(textNodeInBlockNodeRichText.text).toBe(
'Some text below relationship node 1 inserted text',
)
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
@@ -405,32 +419,36 @@ describe('lexical', () => {
await saveDocAndAssert(page)
const lexicalDoc: LexicalField = (
await payload.find({
collection: lexicalFieldsSlug,
depth: 0,
where: {
title: {
equals: lexicalDocData.title,
await expect(async () => {
const lexicalDoc: LexicalField = (
await payload.find({
collection: lexicalFieldsSlug,
depth: 0,
where: {
title: {
equals: lexicalDocData.title,
},
},
},
})
).docs[0] as never
})
).docs[0] as never
const lexicalField: SerializedEditorState = lexicalDoc.lexicalWithBlocks
const blockNode: SerializedBlockNode = lexicalField.root.children[4] as SerializedBlockNode
const paragraphNodeInBlockNodeRichText = blockNode.fields.richText.root.children[1]
const lexicalField: SerializedEditorState = lexicalDoc.lexicalWithBlocks
const blockNode: SerializedBlockNode = lexicalField.root.children[4] as SerializedBlockNode
const paragraphNodeInBlockNodeRichText = blockNode.fields.richText.root.children[1]
expect(paragraphNodeInBlockNodeRichText.children).toHaveLength(2)
expect(paragraphNodeInBlockNodeRichText.children).toHaveLength(2)
const textNode1: SerializedTextNode = paragraphNodeInBlockNodeRichText.children[0]
const boldNode: SerializedTextNode = paragraphNodeInBlockNodeRichText.children[1]
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(textNode1.text).toBe('Some text below r')
expect(textNode1.format).toBe(0)
expect(boldNode.text).toBe('elationship node 1')
expect(boldNode.format).toBe(1)
expect(boldNode.text).toBe('elationship node 1')
expect(boldNode.format).toBe(1)
}).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
@@ -476,31 +494,35 @@ describe('lexical', () => {
.first()
await expect(popoverHeading2Button).toBeVisible()
// 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)
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.
// 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
const x = popoverHeading2ButtonBoundingBox.x
const y = popoverHeading2ButtonBoundingBox.y
await page.mouse.click(x, y, { button: 'left' })
await page.mouse.click(x, y, { button: 'left' })
await page.keyboard.type('A Heading')
await page.keyboard.type('A Heading')
const newHeadingInSubEditor = lexicalBlock.locator('p ~ h2').getByText('A Heading').first()
const newHeadingInSubEditor = lexicalBlock.locator('p ~ h2').getByText('A Heading').first()
await expect(newHeadingInSubEditor).toBeVisible()
await expect(newHeadingInSubEditor).toHaveText('A Heading')
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()
@@ -543,32 +565,36 @@ describe('lexical', () => {
await saveDocAndAssert(page)
/**
* Using the local API, check if the data was saved correctly and
* can be retrieved correctly
*/
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,
where: {
title: {
equals: lexicalDocData.title,
const lexicalDoc: LexicalField = (
await payload.find({
collection: lexicalFieldsSlug,
depth: 0,
where: {
title: {
equals: lexicalDocData.title,
},
},
},
})
).docs[0] as never
})
).docs[0] as never
const lexicalField: SerializedEditorState = lexicalDoc.lexicalWithBlocks
const blockNode: SerializedBlockNode = lexicalField.root.children[5] as SerializedBlockNode
const subBlocks = blockNode.fields.subBlocks
const lexicalField: SerializedEditorState = lexicalDoc.lexicalWithBlocks
const blockNode: SerializedBlockNode = lexicalField.root.children[5] as SerializedBlockNode
const subBlocks = blockNode.fields.subBlocks
expect(subBlocks).toHaveLength(2)
expect(subBlocks).toHaveLength(2)
const createdTextAreaBlock = subBlocks[1]
const createdTextAreaBlock = subBlocks[1]
expect(createdTextAreaBlock.content).toBe('text123')
expect(createdTextAreaBlock.content).toBe('text123')
}).toPass({
timeout: POLL_TOPASS_TIMEOUT,
})
})
test('should allow changing values of two different radio button blocks independently', async () => {
@@ -614,24 +640,28 @@ describe('lexical', () => {
await saveDocAndAssert(page)
const lexicalDoc: LexicalField = (
await payload.find({
collection: lexicalFieldsSlug,
depth: 0,
where: {
title: {
equals: lexicalDocData.title,
await expect(async () => {
const lexicalDoc: LexicalField = (
await payload.find({
collection: lexicalFieldsSlug,
depth: 0,
where: {
title: {
equals: lexicalDocData.title,
},
},
},
})
).docs[0] as never
})
).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
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')
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 () => {

View File

@@ -5,6 +5,7 @@ import { wait } from 'payload/utilities'
import shelljs from 'shelljs'
import { devUser } from './credentials.js'
import { POLL_TOPASS_TIMEOUT } from './playwright.config.js'
type FirstRegisterArgs = {
page: Page
@@ -91,7 +92,7 @@ export async function saveDocHotkeyAndAssert(page: Page): Promise<void> {
export async function saveDocAndAssert(page: Page, selector = '#action-save'): Promise<void> {
await page.click(selector, { delay: 100 })
await expect(page.locator('.Toastify')).toContainText('successfully')
await expect.poll(() => page.url(), { timeout: 45000 }).not.toContain('create')
await expect.poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT }).not.toContain('create')
}
export async function openNav(page: Page): Promise<void> {
@@ -137,7 +138,7 @@ export function exactText(text: string) {
export const checkPageTitle = async (page: Page, title: string) => {
await expect
.poll(async () => await page.locator('.doc-header__title.render-title')?.first()?.innerText(), {
timeout: 45000,
timeout: POLL_TOPASS_TIMEOUT,
})
.toBe(title)
}
@@ -147,7 +148,7 @@ export const checkBreadcrumb = async (page: Page, text: string) => {
.poll(
async () => await page.locator('.step-nav.app-header__step-nav .step-nav__last')?.innerText(),
{
timeout: 45000,
timeout: POLL_TOPASS_TIMEOUT,
},
)
.toBe(text)

View File

@@ -18,7 +18,7 @@ export class AdminUrlUtil {
return `${this.admin}/collections/${slug}`
}
edit(id: string): string {
edit(id: number | string): string {
return `${this.list}/${id}`
}

View File

@@ -1,5 +1,8 @@
import { defineConfig } from '@playwright/test'
export const EXPECT_TIMEOUT = 45000
export const POLL_TOPASS_TIMEOUT = EXPECT_TIMEOUT * 4 // That way expect.poll() or expect().toPass can retry 4 times
export default defineConfig({
// Look for test files in the "test" directory, relative to this configuration file
testDir: '',
@@ -11,7 +14,7 @@ export default defineConfig({
video: 'retain-on-failure',
},
expect: {
timeout: 60000,
timeout: EXPECT_TIMEOUT,
},
workers: 16,
})