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',
|
'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 {
|
return {
|
||||||
CallExpression(node) {
|
CallExpression(node) {
|
||||||
if (
|
// node.callee is MemberExpressiom
|
||||||
node.callee.type === 'MemberExpression' &&
|
if (isNonRetryableAssertion(node.callee)) {
|
||||||
//node.callee.object.name === 'expect' &&
|
if (hasExpectPollOrToPassInChain(node.callee)) {
|
||||||
node.callee.property.type === 'Identifier' &&
|
return
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasExpectPollOrToPass) {
|
if (hasExpectPollOrToPassInParentChain(node)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
context.report({
|
context.report({
|
||||||
node: node.callee.property,
|
node: node.callee.property,
|
||||||
message:
|
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: {
|
data: {
|
||||||
assertion: node.callee.property.name,
|
assertion: node.callee.property.name,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { Field, SanitizedConfig } from 'payload/types'
|
import type { Field, SanitizedConfig } from 'payload/types'
|
||||||
|
|
||||||
import { sanitizeFields } from 'payload/config'
|
|
||||||
import { tabHasName } from 'payload/types'
|
import { tabHasName } from 'payload/types'
|
||||||
|
|
||||||
import type { FieldSchemaMap } from './types.js'
|
import type { FieldSchemaMap } from './types.js'
|
||||||
@@ -21,16 +20,10 @@ export const traverseFields = ({
|
|||||||
validRelationships,
|
validRelationships,
|
||||||
}: Args) => {
|
}: Args) => {
|
||||||
fields.map((field) => {
|
fields.map((field) => {
|
||||||
let fieldsToSet
|
|
||||||
switch (field.type) {
|
switch (field.type) {
|
||||||
case 'group':
|
case 'group':
|
||||||
case 'array':
|
case 'array':
|
||||||
fieldsToSet = sanitizeFields({
|
schemaMap.set(`${schemaPath}.${field.name}`, field.fields)
|
||||||
config,
|
|
||||||
fields: field.fields,
|
|
||||||
validRelationships,
|
|
||||||
})
|
|
||||||
schemaMap.set(`${schemaPath}.${field.name}`, fieldsToSet)
|
|
||||||
|
|
||||||
traverseFields({
|
traverseFields({
|
||||||
config,
|
config,
|
||||||
@@ -55,12 +48,8 @@ export const traverseFields = ({
|
|||||||
case 'blocks':
|
case 'blocks':
|
||||||
field.blocks.map((block) => {
|
field.blocks.map((block) => {
|
||||||
const blockSchemaPath = `${schemaPath}.${field.name}.${block.slug}`
|
const blockSchemaPath = `${schemaPath}.${field.name}.${block.slug}`
|
||||||
fieldsToSet = sanitizeFields({
|
|
||||||
config,
|
schemaMap.set(blockSchemaPath, block.fields)
|
||||||
fields: [...block.fields, { name: 'blockName', type: 'text' }],
|
|
||||||
validRelationships,
|
|
||||||
})
|
|
||||||
schemaMap.set(blockSchemaPath, fieldsToSet)
|
|
||||||
|
|
||||||
traverseFields({
|
traverseFields({
|
||||||
config,
|
config,
|
||||||
@@ -88,12 +77,7 @@ export const traverseFields = ({
|
|||||||
const tabSchemaPath = tabHasName(tab) ? `${schemaPath}.${tab.name}` : schemaPath
|
const tabSchemaPath = tabHasName(tab) ? `${schemaPath}.${tab.name}` : schemaPath
|
||||||
|
|
||||||
if (tabHasName(tab)) {
|
if (tabHasName(tab)) {
|
||||||
fieldsToSet = sanitizeFields({
|
schemaMap.set(tabSchemaPath, tab.fields)
|
||||||
config,
|
|
||||||
fields: tab.fields,
|
|
||||||
validRelationships,
|
|
||||||
})
|
|
||||||
schemaMap.set(tabSchemaPath, fieldsToSet)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
traverseFields({
|
traverseFields({
|
||||||
|
|||||||
@@ -37,7 +37,9 @@ type Props = {
|
|||||||
formData: BlockFields
|
formData: BlockFields
|
||||||
formSchema: FieldMap
|
formSchema: FieldMap
|
||||||
nodeKey: string
|
nodeKey: string
|
||||||
|
path: string
|
||||||
reducedBlock: ReducedBlock
|
reducedBlock: ReducedBlock
|
||||||
|
schemaPath: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -52,7 +54,9 @@ export const BlockContent: React.FC<Props> = (props) => {
|
|||||||
formData,
|
formData,
|
||||||
formSchema,
|
formSchema,
|
||||||
nodeKey,
|
nodeKey,
|
||||||
|
path,
|
||||||
reducedBlock: { labels },
|
reducedBlock: { labels },
|
||||||
|
schemaPath,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const { i18n } = useTranslation()
|
const { i18n } = useTranslation()
|
||||||
@@ -236,9 +240,9 @@ export const BlockContent: React.FC<Props> = (props) => {
|
|||||||
fieldMap={Array.isArray(formSchema) ? formSchema : []}
|
fieldMap={Array.isArray(formSchema) ? formSchema : []}
|
||||||
forceRender
|
forceRender
|
||||||
margins="small"
|
margins="small"
|
||||||
path=""
|
path={path}
|
||||||
readOnly={false}
|
readOnly={false}
|
||||||
schemaPath=""
|
schemaPath={schemaPath}
|
||||||
/>
|
/>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
|||||||
const config = useConfig()
|
const config = useConfig()
|
||||||
const submitted = useFormSubmitted()
|
const submitted = useFormSubmitted()
|
||||||
const { id } = useDocumentInfo()
|
const { id } = useDocumentInfo()
|
||||||
const { schemaPath } = useFieldProps()
|
const { path, schemaPath } = useFieldProps()
|
||||||
const { editorConfig, field: parentLexicalRichTextField } = useEditorConfigContext()
|
const { editorConfig, field: parentLexicalRichTextField } = useEditorConfigContext()
|
||||||
|
|
||||||
const [initialState, setInitialState] = useState<FormState | false>(false)
|
const [initialState, setInitialState] = useState<FormState | false>(false)
|
||||||
@@ -137,7 +137,9 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
|||||||
formData={formData}
|
formData={formData}
|
||||||
formSchema={Array.isArray(fieldMap) ? fieldMap : []}
|
formSchema={Array.isArray(fieldMap) ? fieldMap : []}
|
||||||
nodeKey={nodeKey}
|
nodeKey={nodeKey}
|
||||||
|
path={`${path}.feature.blocks.${formData.blockType}`}
|
||||||
reducedBlock={reducedBlock}
|
reducedBlock={reducedBlock}
|
||||||
|
schemaPath={schemaFieldsPath}
|
||||||
/>
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import type { Page } from '@playwright/test'
|
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 { expect, test } from '@playwright/test'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { wait } from 'payload/utilities'
|
|
||||||
import { fileURLToPath } from 'url'
|
import { fileURLToPath } from 'url'
|
||||||
|
|
||||||
import type { ReadOnlyCollection, RestrictedVersion } from './payload-types.js'
|
import type { ReadOnlyCollection, RestrictedVersion } from './payload-types.js'
|
||||||
@@ -134,7 +133,6 @@ describe('access control', () => {
|
|||||||
describe('restricted fields', () => {
|
describe('restricted fields', () => {
|
||||||
test('should not show field without permission', async () => {
|
test('should not show field without permission', async () => {
|
||||||
await page.goto(url.account)
|
await page.goto(url.account)
|
||||||
await wait(500)
|
|
||||||
await expect(page.locator('#field-roles')).toBeHidden()
|
await expect(page.locator('#field-roles')).toBeHidden()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -163,7 +161,7 @@ describe('access control', () => {
|
|||||||
|
|
||||||
test('should have collection url', async () => {
|
test('should have collection url', async () => {
|
||||||
await page.goto(readOnlyUrl.list)
|
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 () => {
|
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 () => {
|
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 page.goto(readOnlyUrl.edit(existingDoc.id))
|
||||||
await wait(1000)
|
|
||||||
await expect(page.locator('.collection-edit .doc-controls .doc-controls__popup')).toBeHidden()
|
await expect(page.locator('.collection-edit .doc-controls .doc-controls__popup')).toBeHidden()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -213,7 +210,7 @@ describe('access control', () => {
|
|||||||
|
|
||||||
describe('doc level access', () => {
|
describe('doc level access', () => {
|
||||||
let existingDoc: ReadOnlyCollection
|
let existingDoc: ReadOnlyCollection
|
||||||
let docLevelAccessURL
|
let docLevelAccessURL: AdminUrlUtil
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
docLevelAccessURL = new AdminUrlUtil(serverURL, docLevelAccessSlug)
|
docLevelAccessURL = new AdminUrlUtil(serverURL, docLevelAccessSlug)
|
||||||
@@ -265,28 +262,26 @@ describe('access control', () => {
|
|||||||
const unrestrictedURL = new AdminUrlUtil(serverURL, unrestrictedSlug)
|
const unrestrictedURL = new AdminUrlUtil(serverURL, unrestrictedSlug)
|
||||||
await page.goto(unrestrictedURL.edit(unrestrictedDoc.id))
|
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',
|
'#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_]')
|
const documentDrawer = page.locator('[id^=doc-drawer_user-restricted_1_]')
|
||||||
await expect(documentDrawer).toBeVisible()
|
await expect(documentDrawer).toBeVisible()
|
||||||
await documentDrawer.locator('#field-name').fill('anonymous@email.com')
|
await documentDrawer.locator('#field-name').fill('anonymous@email.com')
|
||||||
await documentDrawer.locator('#action-save').click()
|
await documentDrawer.locator('#action-save').click()
|
||||||
await wait(200)
|
|
||||||
await expect(page.locator('.Toastify')).toContainText('successfully')
|
await expect(page.locator('.Toastify')).toContainText('successfully')
|
||||||
|
|
||||||
// ensure user is not allowed to edit this document
|
// ensure user is not allowed to edit this document
|
||||||
await expect(documentDrawer.locator('#field-name')).toBeDisabled()
|
await expect(documentDrawer.locator('#field-name')).toBeDisabled()
|
||||||
await documentDrawer.locator('button.doc-drawer__header-close').click()
|
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_]')
|
const documentDrawer2 = page.locator('[id^=doc-drawer_user-restricted_1_]')
|
||||||
await expect(documentDrawer2).toBeVisible()
|
await expect(documentDrawer2).toBeVisible()
|
||||||
await documentDrawer2.locator('#field-name').fill('dev@payloadcms.com')
|
await documentDrawer2.locator('#field-name').fill('dev@payloadcms.com')
|
||||||
await documentDrawer2.locator('#action-save').click()
|
await documentDrawer2.locator('#action-save').click()
|
||||||
await wait(200)
|
|
||||||
await expect(page.locator('.Toastify')).toContainText('successfully')
|
await expect(page.locator('.Toastify')).toContainText('successfully')
|
||||||
|
|
||||||
// ensure user is allowed to edit this document
|
// 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({
|
return payload.create({
|
||||||
collection: slug,
|
collection: slug,
|
||||||
data,
|
data,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { fileURLToPath } from 'url'
|
|||||||
import { delayNetwork, initPageConsoleErrorCatch, login, saveDocAndAssert } from '../helpers.js'
|
import { delayNetwork, initPageConsoleErrorCatch, login, saveDocAndAssert } from '../helpers.js'
|
||||||
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
|
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
|
||||||
import { initPayloadE2E } from '../helpers/initPayloadE2E.js'
|
import { initPayloadE2E } from '../helpers/initPayloadE2E.js'
|
||||||
|
import { POLL_TOPASS_TIMEOUT } from '../playwright.config.js'
|
||||||
import config from './config.js'
|
import config from './config.js'
|
||||||
import { apiKeysSlug, slug } from './shared.js'
|
import { apiKeysSlug, slug } from './shared.js'
|
||||||
const filename = fileURLToPath(import.meta.url)
|
const filename = fileURLToPath(import.meta.url)
|
||||||
@@ -105,7 +106,7 @@ describe('auth', () => {
|
|||||||
// assert that the value is set
|
// assert that the value is set
|
||||||
const apiKeyLocator = page.locator('#apiKey')
|
const apiKeyLocator = page.locator('#apiKey')
|
||||||
await expect
|
await expect
|
||||||
.poll(async () => await apiKeyLocator.inputValue(), { timeout: 45000 })
|
.poll(async () => await apiKeyLocator.inputValue(), { timeout: POLL_TOPASS_TIMEOUT })
|
||||||
.toBeDefined()
|
.toBeDefined()
|
||||||
|
|
||||||
await saveDocAndAssert(page)
|
await saveDocAndAssert(page)
|
||||||
@@ -114,7 +115,7 @@ describe('auth', () => {
|
|||||||
const apiKey = await apiKeyLocator.inputValue()
|
const apiKey = await apiKeyLocator.inputValue()
|
||||||
expect(await page.locator('#apiKey').inputValue()).toStrictEqual(apiKey)
|
expect(await page.locator('#apiKey').inputValue()).toStrictEqual(apiKey)
|
||||||
}).toPass({
|
}).toPass({
|
||||||
timeout: 45000,
|
timeout: POLL_TOPASS_TIMEOUT,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -140,7 +141,7 @@ describe('auth', () => {
|
|||||||
|
|
||||||
expect(response.user).toBeNull()
|
expect(response.user).toBeNull()
|
||||||
}).toPass({
|
}).toPass({
|
||||||
timeout: 45000,
|
timeout: POLL_TOPASS_TIMEOUT,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { initPageConsoleErrorCatch, saveDocAndAssert } from '../helpers.js'
|
|||||||
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
|
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
|
||||||
import { initPayloadE2E } from '../helpers/initPayloadE2E.js'
|
import { initPayloadE2E } from '../helpers/initPayloadE2E.js'
|
||||||
import { RESTClient } from '../helpers/rest.js'
|
import { RESTClient } from '../helpers/rest.js'
|
||||||
|
import { POLL_TOPASS_TIMEOUT } from '../playwright.config.js'
|
||||||
import { lexicalDocData } from './collections/Lexical/data.js'
|
import { lexicalDocData } from './collections/Lexical/data.js'
|
||||||
import config from './config.js'
|
import config from './config.js'
|
||||||
import { clearAndSeedEverything } from './seed.js'
|
import { clearAndSeedEverything } from './seed.js'
|
||||||
@@ -27,12 +28,6 @@ let client: RESTClient
|
|||||||
let page: Page
|
let page: Page
|
||||||
let serverURL: string
|
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() {
|
async function navigateToLexicalFields() {
|
||||||
const url: AdminUrlUtil = new AdminUrlUtil(serverURL, 'lexical-fields')
|
const url: AdminUrlUtil = new AdminUrlUtil(serverURL, 'lexical-fields')
|
||||||
await page.goto(url.list)
|
await page.goto(url.list)
|
||||||
@@ -70,7 +65,7 @@ describe('lexical', () => {
|
|||||||
await page.locator('.app-header__step-nav').first().locator('a').first().click()
|
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
|
// 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 () => {
|
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()
|
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
|
// 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 () => {
|
test('should type and save typed text', async () => {
|
||||||
@@ -128,24 +123,28 @@ describe('lexical', () => {
|
|||||||
|
|
||||||
await saveDocAndAssert(page)
|
await saveDocAndAssert(page)
|
||||||
|
|
||||||
const lexicalDoc: LexicalField = (
|
await expect(async () => {
|
||||||
await payload.find({
|
const lexicalDoc: LexicalField = (
|
||||||
collection: lexicalFieldsSlug,
|
await payload.find({
|
||||||
depth: 0,
|
collection: lexicalFieldsSlug,
|
||||||
where: {
|
depth: 0,
|
||||||
title: {
|
where: {
|
||||||
equals: lexicalDocData.title,
|
title: {
|
||||||
|
equals: lexicalDocData.title,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
})
|
).docs[0] as never
|
||||||
).docs[0] as never
|
|
||||||
|
|
||||||
const lexicalField: SerializedEditorState = lexicalDoc.lexicalWithBlocks
|
const lexicalField: SerializedEditorState = lexicalDoc.lexicalWithBlocks
|
||||||
const firstParagraphTextNode: SerializedTextNode = (
|
const firstParagraphTextNode: SerializedTextNode = (
|
||||||
lexicalField.root.children[0] as SerializedParagraphNode
|
lexicalField.root.children[0] as SerializedParagraphNode
|
||||||
).children[0] as SerializedTextNode
|
).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 () => {
|
test('should be able to bold text using floating select toolbar', async () => {
|
||||||
await navigateToLexicalFields()
|
await navigateToLexicalFields()
|
||||||
@@ -193,35 +192,39 @@ describe('lexical', () => {
|
|||||||
|
|
||||||
await saveDocAndAssert(page)
|
await saveDocAndAssert(page)
|
||||||
|
|
||||||
const lexicalDoc: LexicalField = (
|
await expect(async () => {
|
||||||
await payload.find({
|
const lexicalDoc: LexicalField = (
|
||||||
collection: lexicalFieldsSlug,
|
await payload.find({
|
||||||
depth: 0,
|
collection: lexicalFieldsSlug,
|
||||||
where: {
|
depth: 0,
|
||||||
title: {
|
where: {
|
||||||
equals: lexicalDocData.title,
|
title: {
|
||||||
|
equals: lexicalDocData.title,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
})
|
).docs[0] as never
|
||||||
).docs[0] as never
|
|
||||||
|
|
||||||
const lexicalField: SerializedEditorState = lexicalDoc.lexicalWithBlocks
|
const lexicalField: SerializedEditorState = lexicalDoc.lexicalWithBlocks
|
||||||
const firstParagraph: SerializedParagraphNode = lexicalField.root
|
const firstParagraph: SerializedParagraphNode = lexicalField.root
|
||||||
.children[0] as SerializedParagraphNode
|
.children[0] as SerializedParagraphNode
|
||||||
expect(firstParagraph.children).toHaveLength(3)
|
expect(firstParagraph.children).toHaveLength(3)
|
||||||
|
|
||||||
const textNode1: SerializedTextNode = firstParagraph.children[0] as SerializedTextNode
|
const textNode1: SerializedTextNode = firstParagraph.children[0] as SerializedTextNode
|
||||||
const boldNode: SerializedTextNode = firstParagraph.children[1] as SerializedTextNode
|
const boldNode: SerializedTextNode = firstParagraph.children[1] as SerializedTextNode
|
||||||
const textNode2: SerializedTextNode = firstParagraph.children[2] as SerializedTextNode
|
const textNode2: SerializedTextNode = firstParagraph.children[2] as SerializedTextNode
|
||||||
|
|
||||||
expect(textNode1.text).toBe('Upload ')
|
expect(textNode1.text).toBe('Upload ')
|
||||||
expect(textNode1.format).toBe(0)
|
expect(textNode1.format).toBe(0)
|
||||||
|
|
||||||
expect(boldNode.text).toBe('Node')
|
expect(boldNode.text).toBe('Node')
|
||||||
expect(boldNode.format).toBe(1)
|
expect(boldNode.format).toBe(1)
|
||||||
|
|
||||||
expect(textNode2.text).toBe(':')
|
expect(textNode2.text).toBe(':')
|
||||||
expect(textNode2.format).toBe(0)
|
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 () => {
|
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()
|
await expect(richTextField).toBeVisible()
|
||||||
|
|
||||||
const contentEditable = richTextField.locator('.ContentEditable__root').first()
|
const contentEditable = richTextField.locator('.ContentEditable__root').first()
|
||||||
const textContent = await contentEditable.textContent()
|
|
||||||
|
|
||||||
expect(textContent).not.toBe('some text')
|
await expect
|
||||||
expect(textContent).toBe('')
|
.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 () => {
|
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 popover = page.locator('.rs__menu').first()
|
||||||
const popoverOption3 = popover.locator('.rs__option').nth(2)
|
const popoverOption3 = popover.locator('.rs__option').nth(2)
|
||||||
|
|
||||||
const popoverOption3BoundingBox = await popoverOption3.boundingBox()
|
await expect(async () => {
|
||||||
expect(popoverOption3BoundingBox).not.toBeNull()
|
const popoverOption3BoundingBox = await popoverOption3.boundingBox()
|
||||||
expect(popoverOption3BoundingBox).not.toBeUndefined()
|
expect(popoverOption3BoundingBox).not.toBeNull()
|
||||||
expect(popoverOption3BoundingBox.height).toBeGreaterThan(0)
|
expect(popoverOption3BoundingBox).not.toBeUndefined()
|
||||||
expect(popoverOption3BoundingBox.width).toBeGreaterThan(0)
|
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()
|
// 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
|
// 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
|
// .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
|
// 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
|
// 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.
|
// and usually the only method which works.
|
||||||
|
|
||||||
const x = popoverOption3BoundingBox.x
|
const x = popoverOption3BoundingBox.x
|
||||||
const y = popoverOption3BoundingBox.y
|
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')
|
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 expect(spanInSubEditor).toHaveText('Some text below relationship node 1 inserted text')
|
||||||
await saveDocAndAssert(page)
|
await saveDocAndAssert(page)
|
||||||
|
|
||||||
const lexicalDoc: LexicalField = (
|
await expect(async () => {
|
||||||
await payload.find({
|
const lexicalDoc: LexicalField = (
|
||||||
collection: lexicalFieldsSlug,
|
await payload.find({
|
||||||
depth: 0,
|
collection: lexicalFieldsSlug,
|
||||||
where: {
|
depth: 0,
|
||||||
title: {
|
where: {
|
||||||
equals: lexicalDocData.title,
|
title: {
|
||||||
|
equals: lexicalDocData.title,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
})
|
).docs[0] as never
|
||||||
).docs[0] as never
|
|
||||||
|
|
||||||
const lexicalField: SerializedEditorState = lexicalDoc.lexicalWithBlocks
|
const lexicalField: SerializedEditorState = lexicalDoc.lexicalWithBlocks
|
||||||
const blockNode: SerializedBlockNode = lexicalField.root.children[4] as SerializedBlockNode
|
const blockNode: SerializedBlockNode = lexicalField.root.children[4] as SerializedBlockNode
|
||||||
const textNodeInBlockNodeRichText = blockNode.fields.richText.root.children[1].children[0]
|
const textNodeInBlockNodeRichText = blockNode.fields.richText.root.children[1].children[0]
|
||||||
|
|
||||||
expect(textNodeInBlockNodeRichText.text).toBe(
|
expect(textNodeInBlockNodeRichText.text).toBe(
|
||||||
'Some text below relationship node 1 inserted text',
|
'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 () => {
|
test('should be able to bold text using floating select toolbar', async () => {
|
||||||
// Reproduces https://github.com/payloadcms/payload/issues/4025
|
// Reproduces https://github.com/payloadcms/payload/issues/4025
|
||||||
@@ -405,32 +419,36 @@ describe('lexical', () => {
|
|||||||
|
|
||||||
await saveDocAndAssert(page)
|
await saveDocAndAssert(page)
|
||||||
|
|
||||||
const lexicalDoc: LexicalField = (
|
await expect(async () => {
|
||||||
await payload.find({
|
const lexicalDoc: LexicalField = (
|
||||||
collection: lexicalFieldsSlug,
|
await payload.find({
|
||||||
depth: 0,
|
collection: lexicalFieldsSlug,
|
||||||
where: {
|
depth: 0,
|
||||||
title: {
|
where: {
|
||||||
equals: lexicalDocData.title,
|
title: {
|
||||||
|
equals: lexicalDocData.title,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
})
|
).docs[0] as never
|
||||||
).docs[0] as never
|
|
||||||
|
|
||||||
const lexicalField: SerializedEditorState = lexicalDoc.lexicalWithBlocks
|
const lexicalField: SerializedEditorState = lexicalDoc.lexicalWithBlocks
|
||||||
const blockNode: SerializedBlockNode = lexicalField.root.children[4] as SerializedBlockNode
|
const blockNode: SerializedBlockNode = lexicalField.root.children[4] as SerializedBlockNode
|
||||||
const paragraphNodeInBlockNodeRichText = blockNode.fields.richText.root.children[1]
|
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 textNode1: SerializedTextNode = paragraphNodeInBlockNodeRichText.children[0]
|
||||||
const boldNode: SerializedTextNode = paragraphNodeInBlockNodeRichText.children[1]
|
const boldNode: SerializedTextNode = paragraphNodeInBlockNodeRichText.children[1]
|
||||||
|
|
||||||
expect(textNode1.text).toBe('Some text below r')
|
expect(textNode1.text).toBe('Some text below r')
|
||||||
expect(textNode1.format).toBe(0)
|
expect(textNode1.format).toBe(0)
|
||||||
|
|
||||||
expect(boldNode.text).toBe('elationship node 1')
|
expect(boldNode.text).toBe('elationship node 1')
|
||||||
expect(boldNode.format).toBe(1)
|
expect(boldNode.format).toBe(1)
|
||||||
|
}).toPass({
|
||||||
|
timeout: POLL_TOPASS_TIMEOUT,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
test('ensure slash menu is not hidden behind other blocks', async () => {
|
test('ensure slash menu is not hidden behind other blocks', async () => {
|
||||||
// This test makes sure there are no z-index issues here
|
// This test makes sure there are no z-index issues here
|
||||||
@@ -476,31 +494,35 @@ describe('lexical', () => {
|
|||||||
.first()
|
.first()
|
||||||
await expect(popoverHeading2Button).toBeVisible()
|
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
|
await expect(async () => {
|
||||||
const popoverHeading2ButtonBoundingBox = await popoverHeading2Button.boundingBox()
|
// Make sure that, even though it's "visible", it's not actually covered by something else due to z-index issues
|
||||||
expect(popoverHeading2ButtonBoundingBox).not.toBeNull()
|
const popoverHeading2ButtonBoundingBox = await popoverHeading2Button.boundingBox()
|
||||||
expect(popoverHeading2ButtonBoundingBox).not.toBeUndefined()
|
expect(popoverHeading2ButtonBoundingBox).not.toBeNull()
|
||||||
expect(popoverHeading2ButtonBoundingBox.height).toBeGreaterThan(0)
|
expect(popoverHeading2ButtonBoundingBox).not.toBeUndefined()
|
||||||
expect(popoverHeading2ButtonBoundingBox.width).toBeGreaterThan(0)
|
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()
|
// 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
|
// 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
|
// .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
|
// 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
|
// 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.
|
// and usually the only method which works.
|
||||||
|
|
||||||
const x = popoverHeading2ButtonBoundingBox.x
|
const x = popoverHeading2ButtonBoundingBox.x
|
||||||
const y = popoverHeading2ButtonBoundingBox.y
|
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).toBeVisible()
|
||||||
await expect(newHeadingInSubEditor).toHaveText('A Heading')
|
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 () => {
|
test('should allow adding new blocks to a sub-blocks field, part of a parent lexical blocks field', async () => {
|
||||||
await navigateToLexicalFields()
|
await navigateToLexicalFields()
|
||||||
@@ -543,32 +565,36 @@ describe('lexical', () => {
|
|||||||
|
|
||||||
await saveDocAndAssert(page)
|
await saveDocAndAssert(page)
|
||||||
|
|
||||||
/**
|
await expect(async () => {
|
||||||
* Using the local API, check if the data was saved correctly and
|
/**
|
||||||
* can be retrieved correctly
|
* Using the local API, check if the data was saved correctly and
|
||||||
*/
|
* can be retrieved correctly
|
||||||
|
*/
|
||||||
|
|
||||||
const lexicalDoc: LexicalField = (
|
const lexicalDoc: LexicalField = (
|
||||||
await payload.find({
|
await payload.find({
|
||||||
collection: lexicalFieldsSlug,
|
collection: lexicalFieldsSlug,
|
||||||
depth: 0,
|
depth: 0,
|
||||||
where: {
|
where: {
|
||||||
title: {
|
title: {
|
||||||
equals: lexicalDocData.title,
|
equals: lexicalDocData.title,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
})
|
).docs[0] as never
|
||||||
).docs[0] as never
|
|
||||||
|
|
||||||
const lexicalField: SerializedEditorState = lexicalDoc.lexicalWithBlocks
|
const lexicalField: SerializedEditorState = lexicalDoc.lexicalWithBlocks
|
||||||
const blockNode: SerializedBlockNode = lexicalField.root.children[5] as SerializedBlockNode
|
const blockNode: SerializedBlockNode = lexicalField.root.children[5] as SerializedBlockNode
|
||||||
const subBlocks = blockNode.fields.subBlocks
|
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 () => {
|
test('should allow changing values of two different radio button blocks independently', async () => {
|
||||||
@@ -614,24 +640,28 @@ describe('lexical', () => {
|
|||||||
|
|
||||||
await saveDocAndAssert(page)
|
await saveDocAndAssert(page)
|
||||||
|
|
||||||
const lexicalDoc: LexicalField = (
|
await expect(async () => {
|
||||||
await payload.find({
|
const lexicalDoc: LexicalField = (
|
||||||
collection: lexicalFieldsSlug,
|
await payload.find({
|
||||||
depth: 0,
|
collection: lexicalFieldsSlug,
|
||||||
where: {
|
depth: 0,
|
||||||
title: {
|
where: {
|
||||||
equals: lexicalDocData.title,
|
title: {
|
||||||
|
equals: lexicalDocData.title,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
})
|
).docs[0] as never
|
||||||
).docs[0] as never
|
|
||||||
|
|
||||||
const lexicalField: SerializedEditorState = lexicalDoc.lexicalWithBlocks
|
const lexicalField: SerializedEditorState = lexicalDoc.lexicalWithBlocks
|
||||||
const radio1: SerializedBlockNode = lexicalField.root.children[8] as SerializedBlockNode
|
const radio1: SerializedBlockNode = lexicalField.root.children[8] as SerializedBlockNode
|
||||||
const radio2: SerializedBlockNode = lexicalField.root.children[9] as SerializedBlockNode
|
const radio2: SerializedBlockNode = lexicalField.root.children[9] as SerializedBlockNode
|
||||||
|
|
||||||
expect(radio1.fields.radioButtons).toBe('option2')
|
expect(radio1.fields.radioButtons).toBe('option2')
|
||||||
expect(radio2.fields.radioButtons).toBe('option3')
|
expect(radio2.fields.radioButtons).toBe('option3')
|
||||||
|
}).toPass({
|
||||||
|
timeout: POLL_TOPASS_TIMEOUT,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should not lose focus when writing in nested editor', async () => {
|
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 shelljs from 'shelljs'
|
||||||
|
|
||||||
import { devUser } from './credentials.js'
|
import { devUser } from './credentials.js'
|
||||||
|
import { POLL_TOPASS_TIMEOUT } from './playwright.config.js'
|
||||||
|
|
||||||
type FirstRegisterArgs = {
|
type FirstRegisterArgs = {
|
||||||
page: Page
|
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> {
|
export async function saveDocAndAssert(page: Page, selector = '#action-save'): Promise<void> {
|
||||||
await page.click(selector, { delay: 100 })
|
await page.click(selector, { delay: 100 })
|
||||||
await expect(page.locator('.Toastify')).toContainText('successfully')
|
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> {
|
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) => {
|
export const checkPageTitle = async (page: Page, title: string) => {
|
||||||
await expect
|
await expect
|
||||||
.poll(async () => await page.locator('.doc-header__title.render-title')?.first()?.innerText(), {
|
.poll(async () => await page.locator('.doc-header__title.render-title')?.first()?.innerText(), {
|
||||||
timeout: 45000,
|
timeout: POLL_TOPASS_TIMEOUT,
|
||||||
})
|
})
|
||||||
.toBe(title)
|
.toBe(title)
|
||||||
}
|
}
|
||||||
@@ -147,7 +148,7 @@ export const checkBreadcrumb = async (page: Page, text: string) => {
|
|||||||
.poll(
|
.poll(
|
||||||
async () => await page.locator('.step-nav.app-header__step-nav .step-nav__last')?.innerText(),
|
async () => await page.locator('.step-nav.app-header__step-nav .step-nav__last')?.innerText(),
|
||||||
{
|
{
|
||||||
timeout: 45000,
|
timeout: POLL_TOPASS_TIMEOUT,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.toBe(text)
|
.toBe(text)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export class AdminUrlUtil {
|
|||||||
return `${this.admin}/collections/${slug}`
|
return `${this.admin}/collections/${slug}`
|
||||||
}
|
}
|
||||||
|
|
||||||
edit(id: string): string {
|
edit(id: number | string): string {
|
||||||
return `${this.list}/${id}`
|
return `${this.list}/${id}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { defineConfig } from '@playwright/test'
|
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({
|
export default defineConfig({
|
||||||
// Look for test files in the "test" directory, relative to this configuration file
|
// Look for test files in the "test" directory, relative to this configuration file
|
||||||
testDir: '',
|
testDir: '',
|
||||||
@@ -11,7 +14,7 @@ export default defineConfig({
|
|||||||
video: 'retain-on-failure',
|
video: 'retain-on-failure',
|
||||||
},
|
},
|
||||||
expect: {
|
expect: {
|
||||||
timeout: 60000,
|
timeout: EXPECT_TIMEOUT,
|
||||||
},
|
},
|
||||||
workers: 16,
|
workers: 16,
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user