fix(ui): incorrect error states (#11574)
Fixes https://github.com/payloadcms/payload/issues/11568 ### What? Out of sync errors states - Collaspibles & Tabs were not reporting accurate child error counts - Arrays could get into a state where they would not update their error states - Slight issue with toasts ### Tabs & Collapsibles The logic for determining matching field paths was not functioning as intended. Fields were attempting to match with paths such as `_index-0` which will not work. ### Arrays The form state was not updating when the server sent back errorPaths. This PR adds `errorPaths` to `serverPropsToAccept`. ### Toasts Some toasts could report errors in the form of `my > > error`. This ensures they will be `my > error` ### Misc Removes 2 files that were not in use: - `getFieldStateFromPaths.ts` - `getNestedFieldState.ts`
This commit is contained in:
@@ -62,6 +62,12 @@ export const testEslintConfig = [
|
||||
'payload/no-wait-function': 'warn',
|
||||
// Enable the no-non-retryable-assertions rule ONLY for hunting for flakes
|
||||
// 'payload/no-non-retryable-assertions': 'error',
|
||||
'playwright/expect-expect': [
|
||||
'error',
|
||||
{
|
||||
assertFunctionNames: ['assertToastErrors', 'saveDocAndAssert', 'runFilterOptionsTest'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
|
||||
import { expect, test } from '@playwright/test'
|
||||
import { assertToastErrors } from 'helpers/assertToastErrors.js'
|
||||
import { addListFilter } from 'helpers/e2e/addListFilter.js'
|
||||
import { openDocControls } from 'helpers/e2e/openDocControls.js'
|
||||
import { openCreateDocDrawer, openDocDrawer } from 'helpers/e2e/toggleDocDrawer.js'
|
||||
@@ -288,9 +289,10 @@ describe('Relationship Field', () => {
|
||||
await expect(field).toContainText(anotherRelationOneDoc.id)
|
||||
await wait(2000) // Need to wait form state to come back before clicking save
|
||||
await page.locator('#action-save').click()
|
||||
await expect(page.locator('.payload-toast-container')).toContainText(
|
||||
`is invalid: ${fieldLabel}`,
|
||||
)
|
||||
await assertToastErrors({
|
||||
page,
|
||||
errors: [fieldLabel],
|
||||
})
|
||||
filteredField = page.locator(`#field-${fieldName} .react-select`)
|
||||
await filteredField.click({ delay: 100 })
|
||||
filteredOptions = filteredField.locator('.rs__option')
|
||||
@@ -303,7 +305,7 @@ describe('Relationship Field', () => {
|
||||
describe('filterOptions', () => {
|
||||
// TODO: Flaky test. Fix this! (This is an actual issue not just an e2e flake)
|
||||
test('should allow dynamic filterOptions', async () => {
|
||||
await runFilterOptionsTest('relationshipFilteredByID', 'Relationship Filtered')
|
||||
await runFilterOptionsTest('relationshipFilteredByID', 'Relationship Filtered By ID')
|
||||
})
|
||||
|
||||
// TODO: Flaky test. Fix this! (This is an actual issue not just an e2e flake)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
|
||||
import { expect, test } from '@playwright/test'
|
||||
import { assertToastErrors } from 'helpers/assertToastErrors.js'
|
||||
import path from 'path'
|
||||
import { wait } from 'payload/shared'
|
||||
import { fileURLToPath } from 'url'
|
||||
@@ -124,9 +125,10 @@ describe('Array', () => {
|
||||
await page.locator('#field-arrayWithMinRows >> .array-field__add-row').click()
|
||||
|
||||
await page.click('#action-save', { delay: 100 })
|
||||
await expect(page.locator('.payload-toast-container')).toContainText(
|
||||
'The following field is invalid: Array With Min Rows',
|
||||
)
|
||||
await assertToastErrors({
|
||||
page,
|
||||
errors: ['Array With Min Rows'],
|
||||
})
|
||||
})
|
||||
|
||||
test('should show singular label for array rows', async () => {
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
saveDocAndAssert,
|
||||
} from '../../../helpers.js'
|
||||
import { AdminUrlUtil } from '../../../helpers/adminUrlUtil.js'
|
||||
import { assertToastErrors } from '../../../helpers/assertToastErrors.js'
|
||||
import { initPayloadE2ENoConfig } from '../../../helpers/initPayloadE2ENoConfig.js'
|
||||
import { reInitializeDB } from '../../../helpers/reInitializeDB.js'
|
||||
import { RESTClient } from '../../../helpers/rest.js'
|
||||
@@ -274,9 +275,10 @@ describe('Block fields', () => {
|
||||
await expect(firstRow).toHaveValue('first row')
|
||||
|
||||
await page.click('#action-save', { delay: 100 })
|
||||
await expect(page.locator('.payload-toast-container')).toContainText(
|
||||
'The following field is invalid: Blocks With Min Rows',
|
||||
)
|
||||
await assertToastErrors({
|
||||
page,
|
||||
errors: ['Blocks With Min Rows'],
|
||||
})
|
||||
})
|
||||
|
||||
test('ensure functions passed to blocks field labels property are respected', async () => {
|
||||
|
||||
@@ -10,6 +10,7 @@ import type { Config } from '../../payload-types.js'
|
||||
|
||||
import { ensureCompilationIsDone, initPageConsoleErrorCatch } from '../../../helpers.js'
|
||||
import { AdminUrlUtil } from '../../../helpers/adminUrlUtil.js'
|
||||
import { assertToastErrors } from '../../../helpers/assertToastErrors.js'
|
||||
import { initPayloadE2ENoConfig } from '../../../helpers/initPayloadE2ENoConfig.js'
|
||||
import { reInitializeDB } from '../../../helpers/reInitializeDB.js'
|
||||
import { RESTClient } from '../../../helpers/rest.js'
|
||||
@@ -96,9 +97,10 @@ describe('Radio', () => {
|
||||
await page.click('#action-save', { delay: 200 })
|
||||
|
||||
// toast error
|
||||
await expect(page.locator('.payload-toast-container')).toContainText(
|
||||
'The following field is invalid: uniqueText',
|
||||
)
|
||||
await assertToastErrors({
|
||||
page,
|
||||
errors: ['uniqueText'],
|
||||
})
|
||||
|
||||
await expect.poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT }).toContain('create')
|
||||
|
||||
@@ -117,9 +119,10 @@ describe('Radio', () => {
|
||||
await page.locator('#action-save').click()
|
||||
|
||||
// toast error
|
||||
await expect(page.locator('.payload-toast-container')).toContainText(
|
||||
'The following field is invalid: group.unique',
|
||||
)
|
||||
await assertToastErrors({
|
||||
page,
|
||||
errors: ['group.unique'],
|
||||
})
|
||||
|
||||
await expect.poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT }).toContain('create')
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
saveDocAndAssert,
|
||||
} from '../../../../../helpers.js'
|
||||
import { AdminUrlUtil } from '../../../../../helpers/adminUrlUtil.js'
|
||||
import { assertToastErrors } from '../../../../../helpers/assertToastErrors.js'
|
||||
import { trackNetworkRequests } from '../../../../../helpers/e2e/trackNetworkRequests.js'
|
||||
import { initPayloadE2ENoConfig } from '../../../../../helpers/initPayloadE2ENoConfig.js'
|
||||
import { reInitializeDB } from '../../../../../helpers/reInitializeDB.js'
|
||||
@@ -570,17 +571,10 @@ describe('lexicalBlocks', () => {
|
||||
await topLevelDocTextField.fill('invalid')
|
||||
|
||||
await saveDocAndAssert(page, '#action-save', 'error')
|
||||
await expect(
|
||||
page
|
||||
.locator('.payload-toast-container li')
|
||||
.filter({ hasText: 'The following fields are invalid (2):' }),
|
||||
).toBeVisible()
|
||||
await expect(
|
||||
page.locator('.payload-toast-container [data-testid="field-errors"] li').nth(0),
|
||||
).toHaveText('Lexical With Blocks')
|
||||
await expect(
|
||||
page.locator('.payload-toast-container [data-testid="field-errors"] li').nth(1),
|
||||
).toHaveText('Lexical With Blocks → Group → Text Depends On Doc Data')
|
||||
await assertToastErrors({
|
||||
page,
|
||||
errors: ['Lexical With Blocks', 'Lexical With Blocks → Group → Text Depends On Doc Data'],
|
||||
})
|
||||
await expect(page.locator('.payload-toast-container .payload-toast-item')).toBeHidden()
|
||||
|
||||
await trackNetworkRequests(
|
||||
@@ -601,18 +595,13 @@ describe('lexicalBlocks', () => {
|
||||
await blockGroupTextField.fill('invalid')
|
||||
|
||||
await saveDocAndAssert(page, '#action-save', 'error')
|
||||
await expect(
|
||||
page
|
||||
.locator('.payload-toast-container li')
|
||||
.filter({ hasText: 'The following fields are invalid (2):' }),
|
||||
).toBeVisible()
|
||||
await expect(
|
||||
page.locator('.payload-toast-container [data-testid="field-errors"] li').nth(0),
|
||||
).toHaveText('Lexical With Blocks')
|
||||
await expect(
|
||||
page.locator('.payload-toast-container [data-testid="field-errors"] li').nth(1),
|
||||
).toHaveText('Lexical With Blocks → Group → Text Depends On Sibling Data')
|
||||
|
||||
await assertToastErrors({
|
||||
page,
|
||||
errors: [
|
||||
'Lexical With Blocks',
|
||||
'Lexical With Blocks → Group → Text Depends On Sibling Data',
|
||||
],
|
||||
})
|
||||
await expect(page.locator('.payload-toast-container .payload-toast-item')).toBeHidden()
|
||||
|
||||
await trackNetworkRequests(
|
||||
@@ -633,18 +622,10 @@ describe('lexicalBlocks', () => {
|
||||
await blockTextField.fill('invalid')
|
||||
|
||||
await saveDocAndAssert(page, '#action-save', 'error')
|
||||
await expect(
|
||||
page
|
||||
.locator('.payload-toast-container li')
|
||||
.filter({ hasText: 'The following fields are invalid (2):' }),
|
||||
).toBeVisible()
|
||||
await expect(
|
||||
page.locator('.payload-toast-container [data-testid="field-errors"] li').nth(0),
|
||||
).toHaveText('Lexical With Blocks')
|
||||
await expect(
|
||||
page.locator('.payload-toast-container [data-testid="field-errors"] li').nth(1),
|
||||
).toHaveText('Lexical With Blocks → Group → Text Depends On Block Data')
|
||||
|
||||
await assertToastErrors({
|
||||
page,
|
||||
errors: ['Lexical With Blocks', 'Lexical With Blocks → Group → Text Depends On Block Data'],
|
||||
})
|
||||
await expect(page.locator('.payload-toast-container .payload-toast-item')).toBeHidden()
|
||||
|
||||
await trackNetworkRequests(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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'
|
||||
@@ -15,12 +16,12 @@ import {
|
||||
saveDocAndAssert,
|
||||
} from '../../../helpers.js'
|
||||
import { AdminUrlUtil } from '../../../helpers/adminUrlUtil.js'
|
||||
import { assertToastErrors } from '../../../helpers/assertToastErrors.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 { numberDoc } from './shared.js'
|
||||
import { addListFilter } from 'helpers/e2e/addListFilter.js'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const currentFolder = path.dirname(filename)
|
||||
@@ -120,9 +121,10 @@ describe('Number', () => {
|
||||
await page.keyboard.type(String(input))
|
||||
await page.keyboard.press('Enter')
|
||||
await page.click('#action-save', { delay: 100 })
|
||||
await expect(page.locator('.payload-toast-container')).toContainText(
|
||||
'The following field is invalid: With Min Rows',
|
||||
)
|
||||
await assertToastErrors({
|
||||
page,
|
||||
errors: ['With Min Rows'],
|
||||
})
|
||||
})
|
||||
|
||||
test('should keep data removed on save if deleted', async () => {
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
saveDocHotkeyAndAssert,
|
||||
} from '../../../helpers.js'
|
||||
import { AdminUrlUtil } from '../../../helpers/adminUrlUtil.js'
|
||||
import { assertToastErrors } from '../../../helpers/assertToastErrors.js'
|
||||
import { initPayloadE2ENoConfig } from '../../../helpers/initPayloadE2ENoConfig.js'
|
||||
import { reInitializeDB } from '../../../helpers/reInitializeDB.js'
|
||||
import { RESTClient } from '../../../helpers/rest.js'
|
||||
@@ -448,8 +449,6 @@ describe('relationship', () => {
|
||||
}),
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test.skip('has many', async () => {})
|
||||
})
|
||||
|
||||
describe('should duplicate document within document drawer', () => {
|
||||
@@ -509,8 +508,6 @@ describe('relationship', () => {
|
||||
}),
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test.skip('has many', async () => {})
|
||||
})
|
||||
|
||||
describe('should delete document within document drawer', () => {
|
||||
@@ -569,8 +566,6 @@ describe('relationship', () => {
|
||||
}),
|
||||
).toBeHidden()
|
||||
})
|
||||
|
||||
test.skip('has many', async () => {})
|
||||
})
|
||||
|
||||
// TODO: Fix this. This test flakes due to react select
|
||||
@@ -603,9 +598,10 @@ describe('relationship', () => {
|
||||
.click()
|
||||
|
||||
await page.click('#action-save', { delay: 100 })
|
||||
await expect(page.locator('.payload-toast-container')).toContainText(
|
||||
'The following field is invalid: Relationship With Min Rows',
|
||||
)
|
||||
await assertToastErrors({
|
||||
page,
|
||||
errors: ['Relationship With Min Rows'],
|
||||
})
|
||||
})
|
||||
|
||||
test('should sort relationship options by sortOptions property (ID in ascending order)', async () => {
|
||||
|
||||
27
test/helpers/assertToastErrors.ts
Normal file
27
test/helpers/assertToastErrors.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
export async function assertToastErrors({
|
||||
page,
|
||||
errors,
|
||||
}: {
|
||||
errors: string[]
|
||||
page: Page
|
||||
}): Promise<void> {
|
||||
const message =
|
||||
errors.length === 1
|
||||
? 'The following field is invalid:'
|
||||
: `The following fields are invalid (${errors.length}):`
|
||||
await expect(
|
||||
page.locator('.payload-toast-container li').filter({ hasText: message }),
|
||||
).toBeVisible()
|
||||
for (let i = 0; i < errors.length; i++) {
|
||||
const error = errors[i]
|
||||
if (error) {
|
||||
await expect(
|
||||
page.locator('.payload-toast-container [data-testid="field-errors"] li').nth(i),
|
||||
).toHaveText(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
saveDocAndAssert,
|
||||
} from '../helpers.js'
|
||||
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
|
||||
import { assertToastErrors } from '../helpers/assertToastErrors.js'
|
||||
import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js'
|
||||
import { reInitializeDB } from '../helpers/reInitializeDB.js'
|
||||
import { RESTClient } from '../helpers/rest.js'
|
||||
@@ -440,9 +441,10 @@ describe('Uploads', () => {
|
||||
|
||||
// save the document and expect an error
|
||||
await page.locator('button#action-save').click()
|
||||
await expect(page.locator('.payload-toast-container .toast-error')).toContainText(
|
||||
'The following field is invalid: Audio',
|
||||
)
|
||||
await assertToastErrors({
|
||||
page,
|
||||
errors: ['Audio'],
|
||||
})
|
||||
})
|
||||
|
||||
test('should restrict uploads in drawer based on filterOptions', async () => {
|
||||
|
||||
Reference in New Issue
Block a user