Merge pull request #5436 from payloadcms/temp11
chore: improvements to eslint, and access-control + lexical test suites
This commit is contained in:
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}`
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user