Fixes #9873. The relationship filter in the "where" builder renders stale values when switching between fields or adding additional "and" conditions. This was because the `RelationshipFilter` component was not responding to changes in the `relationTo` prop and failing to reset internal state when these events took place. While it sounds like a simple fix, it was actually quite extensive. The `RelationshipFilter` component was previously relying on a `useEffect` that had a callback in its dependencies. This was causing the effect to run uncontrollably using old references. To avoid this, we use the new `useEffectEvent` approach which allows the underlying effect to run much more precisely. Same with the `Condition` component that wraps it. We now run callbacks directly within event handlers as much as possible, and rely on `useEffectEvent` _only_ for debounced value changes. This component was also unnecessarily complex...and still is to some degree. Previously, it was maintaining two separate refs, one to track the relationships that have yet to fully load, and another to track the next pages of each relationship that need to load on the next run. These have been combined into a single ref that tracks both simultaneously, as this data is interrelated. This change also does some much needed housekeeping to the `WhereBuilder` by improving types, defaulting the operator field, etc. Related: #11023 and #11032 Unrelated: finds a few more instances where the new `addListFilter` helper from #11026 could be used. Also removes a few duplicative tests.
130 lines
4.7 KiB
TypeScript
130 lines
4.7 KiB
TypeScript
import type { Page } from '@playwright/test'
|
|
|
|
import { expect, test } from '@playwright/test'
|
|
import { addListFilter } from 'helpers/e2e/addListFilter.js'
|
|
import { openListFilters } from 'helpers/e2e/openListFilters.js'
|
|
import path from 'path'
|
|
import { wait } from 'payload/shared'
|
|
import { fileURLToPath } from 'url'
|
|
|
|
import type { PayloadTestSDK } from '../../../helpers/sdk/index.js'
|
|
import type { Config } from '../../payload-types.js'
|
|
|
|
import { ensureCompilationIsDone, initPageConsoleErrorCatch } from '../../../helpers.js'
|
|
import { AdminUrlUtil } from '../../../helpers/adminUrlUtil.js'
|
|
import { initPayloadE2ENoConfig } from '../../../helpers/initPayloadE2ENoConfig.js'
|
|
import { reInitializeDB } from '../../../helpers/reInitializeDB.js'
|
|
import { RESTClient } from '../../../helpers/rest.js'
|
|
import { TEST_TIMEOUT_LONG } from '../../../playwright.config.js'
|
|
import { emailFieldsSlug } from '../../slugs.js'
|
|
import { emailDoc } from './shared.js'
|
|
|
|
const filename = fileURLToPath(import.meta.url)
|
|
const currentFolder = path.dirname(filename)
|
|
const dirname = path.resolve(currentFolder, '../../')
|
|
|
|
const { beforeAll, beforeEach, describe } = test
|
|
|
|
let payload: PayloadTestSDK<Config>
|
|
let client: RESTClient
|
|
let page: Page
|
|
let serverURL: string
|
|
// If we want to make this run in parallel: test.describe.configure({ mode: 'parallel' })
|
|
let url: AdminUrlUtil
|
|
|
|
describe('Email', () => {
|
|
beforeAll(async ({ browser }, testInfo) => {
|
|
testInfo.setTimeout(TEST_TIMEOUT_LONG)
|
|
process.env.SEED_IN_CONFIG_ONINIT = 'false' // Makes it so the payload config onInit seed is not run. Otherwise, the seed would be run unnecessarily twice for the initial test run - once for beforeEach and once for onInit
|
|
;({ payload, serverURL } = await initPayloadE2ENoConfig<Config>({
|
|
dirname,
|
|
// prebuild,
|
|
}))
|
|
url = new AdminUrlUtil(serverURL, emailFieldsSlug)
|
|
|
|
const context = await browser.newContext()
|
|
page = await context.newPage()
|
|
initPageConsoleErrorCatch(page)
|
|
|
|
await ensureCompilationIsDone({ page, serverURL })
|
|
})
|
|
beforeEach(async () => {
|
|
await reInitializeDB({
|
|
serverURL,
|
|
snapshotKey: 'fieldsTest',
|
|
uploadsDir: path.resolve(dirname, './collections/Upload/uploads'),
|
|
})
|
|
|
|
if (client) {
|
|
await client.logout()
|
|
}
|
|
client = new RESTClient({ defaultSlug: 'users', serverURL })
|
|
await client.login()
|
|
|
|
await ensureCompilationIsDone({ page, serverURL })
|
|
})
|
|
|
|
test('should display field in list view', async () => {
|
|
await page.goto(url.list)
|
|
const emailCell = page.locator('.row-1 .cell-email')
|
|
await expect(emailCell).toHaveText(emailDoc.email)
|
|
})
|
|
|
|
test('should have autocomplete', async () => {
|
|
await page.goto(url.create)
|
|
const autoCompleteEmail = page.locator('#field-emailWithAutocomplete')
|
|
await expect(autoCompleteEmail).toHaveAttribute('autocomplete')
|
|
})
|
|
|
|
test('should show i18n label', async () => {
|
|
await page.goto(url.create)
|
|
|
|
await expect(page.locator('label[for="field-i18nEmail"]')).toHaveText('Text en')
|
|
})
|
|
|
|
test('should show i18n placeholder', async () => {
|
|
await page.goto(url.create)
|
|
await expect(page.locator('#field-i18nEmail')).toHaveAttribute('placeholder', 'en placeholder')
|
|
})
|
|
|
|
test('should show i18n descriptions', async () => {
|
|
await page.goto(url.create)
|
|
const description = page.locator('.field-description-i18nEmail')
|
|
await expect(description).toHaveText('en description')
|
|
})
|
|
|
|
test('should render custom label', async () => {
|
|
await page.goto(url.create)
|
|
const label = page.locator('label.custom-label[for="field-customLabel"]')
|
|
await expect(label).toHaveText('#label')
|
|
})
|
|
|
|
test('should render custom error', async () => {
|
|
await page.goto(url.create)
|
|
const input = page.locator('input[id="field-customError"]')
|
|
await input.fill('ab')
|
|
await expect(input).toHaveValue('ab')
|
|
const error = page.locator('.custom-error:near(input[id="field-customError"])')
|
|
const submit = page.locator('button[type="button"][id="action-save"]')
|
|
await submit.click()
|
|
await expect(error).toHaveText('#custom-error')
|
|
})
|
|
|
|
test('should render beforeInput and afterInput', async () => {
|
|
await page.goto(url.create)
|
|
const input = page.locator('input[id="field-beforeAndAfterInput"]')
|
|
|
|
const prevSibling = await input.evaluateHandle((el) => {
|
|
return el.previousElementSibling
|
|
})
|
|
const prevSiblingText = await page.evaluate((el) => el.textContent, prevSibling)
|
|
expect(prevSiblingText).toEqual('#before-input')
|
|
|
|
const nextSibling = await input.evaluateHandle((el) => {
|
|
return el.nextElementSibling
|
|
})
|
|
const nextSiblingText = await page.evaluate((el) => el.textContent, nextSibling)
|
|
expect(nextSiblingText).toEqual('#after-input')
|
|
})
|
|
})
|