Files
payloadcms/test/form-state/e2e.spec.ts
Jacob Fletcher 0b1a1b585b fix(ui): processing and initializing form does not disable standalone fields (#11714)
The form component's `initializing` and `processing` states do not
disable fields that are rendered outside of `DocumentFields`. Fields
currently rely on the `readOnly` prop provided by `DocumentFields` and
do not subscribe to these states for themselves. This means that fields
that are rendered outright, such as within the bulk edit drawer, they do
not receive a `readOnly` prop and are therefore never disabled.

The fix is add a `disabled` property to the `useField` hook. This
subscribes to the `initializing` and `processing` states in the same way
as `DocumentFields`, however, now each field can determine its own
disabled state instead of relying solely on the `readOnly` prop. Adding
this new prop has no overhead as `processing` and `initializing` is
already being subscribed to within `useField`.
2025-03-17 10:27:21 -04:00

193 lines
5.5 KiB
TypeScript

import type { BrowserContext, Page } from '@playwright/test'
import type { PayloadTestSDK } from 'helpers/sdk/index.js'
import { expect, test } from '@playwright/test'
import { addBlock } from 'helpers/e2e/addBlock.js'
import { assertNetworkRequests } from 'helpers/e2e/assertNetworkRequests.js'
import * as path from 'path'
import { fileURLToPath } from 'url'
import type { Config, Post } from './payload-types.js'
import {
ensureCompilationIsDone,
initPageConsoleErrorCatch,
saveDocAndAssert,
throttleTest,
} from '../helpers.js'
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js'
import { TEST_TIMEOUT_LONG } from '../playwright.config.js'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
const title = 'Title'
let context: BrowserContext
let payload: PayloadTestSDK<Config>
let serverURL: string
test.describe('Form State', () => {
let page: Page
let postsUrl: AdminUrlUtil
test.beforeAll(async ({ browser }, testInfo) => {
testInfo.setTimeout(TEST_TIMEOUT_LONG)
;({ payload, serverURL } = await initPayloadE2ENoConfig({ dirname }))
postsUrl = new AdminUrlUtil(serverURL, 'posts')
context = await browser.newContext()
page = await context.newPage()
initPageConsoleErrorCatch(page)
await ensureCompilationIsDone({ page, serverURL })
})
test.beforeEach(async () => {
// await throttleTest({ page, context, delay: 'Fast 3G' })
})
test('should disable fields during initialization', async () => {
await page.goto(postsUrl.create, { waitUntil: 'commit' })
await expect(page.locator('#field-title')).toBeDisabled()
})
test('should disable fields while processing', async () => {
const doc = await createPost()
await page.goto(postsUrl.edit(doc.id))
await page.locator('#field-title').fill(title)
await page.click('#action-save', { delay: 100 })
await expect(page.locator('#field-title')).toBeDisabled()
})
test('should re-enable fields after save', async () => {
await page.goto(postsUrl.create)
await page.locator('#field-title').fill(title)
await saveDocAndAssert(page)
await expect(page.locator('#field-title')).toBeEnabled()
})
test('should only validate on submit via the `event` argument', async () => {
await page.goto(postsUrl.create)
await page.locator('#field-title').fill(title)
await page.locator('#field-validateUsingEvent').fill('Not allowed')
await saveDocAndAssert(page, '#action-save', 'error')
})
test('should fire a single network request for onChange events when manipulating blocks', async () => {
await page.goto(postsUrl.create)
await assertNetworkRequests(
page,
postsUrl.create,
async () => {
await addBlock({
page,
blockLabel: 'Text',
fieldName: 'blocks',
})
},
{
allowedNumberOfRequests: 1,
},
)
})
test('should not throw fields into an infinite rendering loop', async () => {
await page.goto(postsUrl.create)
await page.locator('#field-title').fill(title)
let numberOfRenders = 0
page.on('console', (msg) => {
if (msg.type() === 'count' && msg.text().includes('Renders')) {
numberOfRenders++
}
})
const allowedNumberOfRenders = 25
const pollInterval = 200
const maxTime = 5000
let elapsedTime = 0
const intervalId = setInterval(() => {
if (numberOfRenders > allowedNumberOfRenders) {
clearInterval(intervalId)
throw new Error(`Render count exceeded the threshold of ${allowedNumberOfRenders}`)
}
elapsedTime += pollInterval
if (elapsedTime >= maxTime) {
clearInterval(intervalId)
}
}, pollInterval)
await page.waitForTimeout(maxTime)
expect(numberOfRenders).toBeLessThanOrEqual(allowedNumberOfRenders)
})
test('should debounce onChange events', async () => {
await page.goto(postsUrl.create)
const field = page.locator('#field-title')
await assertNetworkRequests(
page,
postsUrl.create,
async () => {
// Need to type _faster_ than the debounce rate (250ms)
await field.pressSequentially('Some text to type', { delay: 50 })
},
{
allowedNumberOfRequests: 1,
},
)
})
test('should queue onChange functions', async () => {
await page.goto(postsUrl.create)
const field = page.locator('#field-title')
await field.fill('Test')
// only throttle test after initial load to avoid timeouts
const cdpSession = await throttleTest({
page,
context,
delay: 'Slow 3G',
})
await assertNetworkRequests(
page,
postsUrl.create,
async () => {
await field.fill('')
// Need to type into a _slower_ than the debounce rate (250ms), but _faster_ than the network request
await field.pressSequentially('Some text to type', { delay: 275 })
},
{
allowedNumberOfRequests: 2,
timeout: 10000, // watch network for 10 seconds to allow requests to build up
},
)
await cdpSession.send('Network.emulateNetworkConditions', {
offline: false,
latency: 0,
downloadThroughput: -1,
uploadThroughput: -1,
})
await cdpSession.detach()
})
})
async function createPost(overrides?: Partial<Post>): Promise<Post> {
return payload.create({
collection: 'posts',
data: {
title: 'Post Title',
...overrides,
},
}) as unknown as Promise<Post>
}