diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b8b0c4e581..0df5f6a808 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -285,18 +285,28 @@ jobs: - auth-basic - field-error-states - fields-relationship - - fields - - fields__collections__Blocks - fields__collections__Array - - fields__collections__Relationship - - fields__collections__RichText + - fields__collections__Blocks + - fields__collections__Collapsible + - fields__collections__ConditionalLogic + - fields__collections__CustomID + - fields__collections__Date + - fields__collections__Email + - fields__collections__Indexed + - fields__collections__JSON - fields__collections__Lexical__e2e__main - fields__collections__Lexical__e2e__blocks - - fields__collections__Date - fields__collections__Number - fields__collections__Point + - fields__collections__Radio + - fields__collections__Relationship + - fields__collections__RichText + - fields__collections__Row + - fields__collections__Select - fields__collections__Tabs + - fields__collections__Tabs2 - fields__collections__Text + - fields__collections__UI - fields__collections__Upload - live-preview - localization diff --git a/test/fields/collections/Array/e2e.spec.ts b/test/fields/collections/Array/e2e.spec.ts index 0a3cded140..e9b5945a9b 100644 --- a/test/fields/collections/Array/e2e.spec.ts +++ b/test/fields/collections/Array/e2e.spec.ts @@ -309,4 +309,20 @@ describe('Array', () => { const collapsedArrayRow = page.locator('#collapsedArray-row-0 .collapsible--collapsed') await expect(collapsedArrayRow).toBeHidden() }) + + describe('sortable arrays', () => { + test('should have disabled admin sorting', async () => { + await page.goto(url.create) + const field = page.locator('#field-disableSort > div > div > .array-actions__action-chevron') + expect(await field.count()).toEqual(0) + }) + + test('the drag handle should be hidden', async () => { + await page.goto(url.create) + const field = page.locator( + '#field-disableSort > .blocks-field__rows > div > div > .collapsible__drag', + ) + expect(await field.count()).toEqual(0) + }) + }) }) diff --git a/test/fields/collections/Blocks/e2e.spec.ts b/test/fields/collections/Blocks/e2e.spec.ts index 5850b56bc2..40e062365c 100644 --- a/test/fields/collections/Blocks/e2e.spec.ts +++ b/test/fields/collections/Blocks/e2e.spec.ts @@ -280,4 +280,20 @@ describe('Block fields', () => { }) }) }) + + describe('sortable blocks', () => { + test('should have disabled admin sorting', async () => { + await page.goto(url.create) + const field = page.locator('#field-disableSort > div > div > .array-actions__action-chevron') + expect(await field.count()).toEqual(0) + }) + + test('the drag handle should be hidden', async () => { + await page.goto(url.create) + const field = page.locator( + '#field-disableSort > .blocks-field__rows > div > div > .collapsible__drag', + ) + expect(await field.count()).toEqual(0) + }) + }) }) diff --git a/test/fields/collections/Collapsible/e2e.spec.ts b/test/fields/collections/Collapsible/e2e.spec.ts new file mode 100644 index 0000000000..f0be6ef78d --- /dev/null +++ b/test/fields/collections/Collapsible/e2e.spec.ts @@ -0,0 +1,105 @@ +import type { Page } from '@playwright/test' + +import { expect, test } from '@playwright/test' +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 { collapsibleFieldsSlug } from '../../slugs.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 +let client: RESTClient +let page: Page +let serverURL: string +let url: AdminUrlUtil + +describe('Collapsibles', () => { + 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({ + dirname, + // prebuild, + })) + + url = new AdminUrlUtil(serverURL, collapsibleFieldsSlug) + + 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(null, { defaultSlug: 'users', serverURL }) + await client.login() + await ensureCompilationIsDone({ page, serverURL }) + }) + + test('should render collapsible as collapsed if initCollapsed is true', async () => { + await page.goto(url.create) + const collapsedCollapsible = page.locator( + '#field-collapsible-_index-1 .collapsible__toggle--collapsed', + ) + await expect(collapsedCollapsible).toBeVisible() + }) + + test('should render CollapsibleLabel using a function', async () => { + const label = 'custom row label' + await page.goto(url.create) + await page.locator('#field-collapsible-_index-3-1 #field-nestedTitle').fill(label) + await wait(100) + const customCollapsibleLabel = page.locator( + `#field-collapsible-_index-3-1 .collapsible-field__row-label-wrap :text("${label}")`, + ) + await expect(customCollapsibleLabel).toContainText(label) + }) + + test('should render CollapsibleLabel using a component', async () => { + const label = 'custom row label as component' + await page.goto(url.create) + await page.locator('#field-arrayWithCollapsibles').scrollIntoViewIfNeeded() + + const arrayWithCollapsibles = page.locator('#field-arrayWithCollapsibles') + await expect(arrayWithCollapsibles).toBeVisible() + + await page.locator('#field-arrayWithCollapsibles >> .array-field__add-row').click() + + await page + .locator( + '#arrayWithCollapsibles-row-0 #field-collapsible-arrayWithCollapsibles__0___index-0 #field-arrayWithCollapsibles__0__innerCollapsible', + ) + .fill(label) + + await wait(100) + + const customCollapsibleLabel = page.locator( + `#field-arrayWithCollapsibles >> #arrayWithCollapsibles-row-0 >> .collapsible-field__row-label-wrap :text("${label}")`, + ) + await expect(customCollapsibleLabel).toHaveCSS('text-transform', 'uppercase') + }) +}) diff --git a/test/fields/collections/ConditionalLogic/e2e.spec.ts b/test/fields/collections/ConditionalLogic/e2e.spec.ts new file mode 100644 index 0000000000..9f1f16c833 --- /dev/null +++ b/test/fields/collections/ConditionalLogic/e2e.spec.ts @@ -0,0 +1,137 @@ +import type { Page } from '@playwright/test' + +import { expect, test } from '@playwright/test' +import path from 'path' +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 { conditionalLogicSlug } from '../../slugs.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 +let client: RESTClient +let page: Page +let serverURL: string +let url: AdminUrlUtil + +const toggleConditionAndCheckField = async (toggleLocator: string, fieldLocator: string) => { + const toggle = page.locator(toggleLocator) + + if (!(await toggle.isChecked())) { + await expect(page.locator(fieldLocator)).toBeHidden() + await toggle.click() + await expect(page.locator(fieldLocator)).toBeVisible() + } else { + await expect(page.locator(fieldLocator)).toBeVisible() + await toggle.click() + await expect(page.locator(fieldLocator)).toBeHidden() + } +} + +describe('Conditional Logic', () => { + 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({ + dirname, + // prebuild, + })) + + url = new AdminUrlUtil(serverURL, conditionalLogicSlug) + + 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(null, { defaultSlug: 'users', serverURL }) + await client.login() + await ensureCompilationIsDone({ page, serverURL }) + }) + + test("should conditionally render field based on another field's data", async () => { + await page.goto(url.create) + + await toggleConditionAndCheckField( + 'label[for=field-toggleField]', + 'label[for=field-fieldWithCondition]', + ) + + expect(true).toBe(true) + }) + + test('should show conditional field based on user data', async () => { + await page.goto(url.create) + const userConditional = page.locator('input#field-userConditional') + await expect(userConditional).toBeVisible() + }) + + test('should show conditional field based on nested field data', async () => { + await page.goto(url.create) + + const parentGroupFields = page.locator( + 'div#field-parentGroup > .group-field__wrap > .render-fields', + ) + await expect(parentGroupFields).toHaveCount(1) + + const toggle = page.locator('label[for=field-parentGroup__enableParentGroupFields]') + await toggle.click() + + const toggledField = page.locator('input#field-parentGroup__siblingField') + + await expect(toggledField).toBeVisible() + }) + + test('should show conditional field based on siblingData', async () => { + await page.goto(url.create) + + const toggle = page.locator('label[for=field-parentGroup__enableParentGroupFields]') + await toggle.click() + + const fieldRelyingOnSiblingData = page.locator('input#field-reliesOnParentGroup') + await expect(fieldRelyingOnSiblingData).toBeVisible() + }) + + test('should not render fields when adding array or blocks rows until form state returns', async () => { + await page.goto(url.create) + const addRowButton = page.locator('.array-field__add-row') + const fieldWithConditionSelector = 'input#field-arrayWithConditionalField__0__textWithCondition' + await addRowButton.click() + + const wasFieldAttached = await page + .waitForSelector(fieldWithConditionSelector, { + state: 'attached', + timeout: 100, // A small timeout to catch any transient rendering + }) + .catch(() => false) // If it doesn't appear, this resolves to `false` + + expect(wasFieldAttached).toBeFalsy() + const fieldToToggle = page.locator('input#field-enableConditionalFields') + await fieldToToggle.click() + await expect(page.locator(fieldWithConditionSelector)).toBeVisible() + }) +}) diff --git a/test/fields/collections/ConditionalLogic/index.ts b/test/fields/collections/ConditionalLogic/index.ts index 0a412c9d48..90f79a2f21 100644 --- a/test/fields/collections/ConditionalLogic/index.ts +++ b/test/fields/collections/ConditionalLogic/index.ts @@ -18,7 +18,7 @@ const ConditionalLogic: CollectionConfig = { type: 'checkbox', }, { - name: 'fieldToToggle', + name: 'fieldWithCondition', type: 'text', required: true, admin: { diff --git a/test/fields/collections/ConditionalLogic/shared.ts b/test/fields/collections/ConditionalLogic/shared.ts index 6d8456d2dc..3d86f9031c 100644 --- a/test/fields/collections/ConditionalLogic/shared.ts +++ b/test/fields/collections/ConditionalLogic/shared.ts @@ -1,9 +1,9 @@ -import type { RequiredDataFromCollection } from 'payload/types' +import type { RequiredDataFromCollection } from 'payload' import type { ConditionalLogic } from '../../payload-types.js' export const conditionalLogicDoc: RequiredDataFromCollection = { text: 'Seeded conditional logic document', toggleField: true, - fieldToToggle: 'spiderman', + fieldWithCondition: 'spiderman', } diff --git a/test/fields/collections/CustomID/e2e.spec.ts b/test/fields/collections/CustomID/e2e.spec.ts new file mode 100644 index 0000000000..9330630608 --- /dev/null +++ b/test/fields/collections/CustomID/e2e.spec.ts @@ -0,0 +1,82 @@ +import type { Page } from '@playwright/test' + +import { expect, test } from '@playwright/test' +import { navigateToDoc } from 'helpers/e2e/navigateToDoc.js' +import path from 'path' +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 { customIdSlug } from '../../slugs.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 +let client: RESTClient +let page: Page +let serverURL: string +let url: AdminUrlUtil + +describe('Radio', () => { + 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({ + dirname, + // prebuild, + })) + + url = new AdminUrlUtil(serverURL, customIdSlug) + + 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(null, { defaultSlug: 'users', serverURL }) + await client.login() + await ensureCompilationIsDone({ page, serverURL }) + }) + + function createCustomIDDoc(id: string) { + return payload.create({ + collection: customIdSlug, + data: { + id, + }, + }) + } + + test('allow create of non standard ID', async () => { + await createCustomIDDoc('id 1') + await page.goto(url.list) + + await navigateToDoc(page, url) + + // Page should load and ID should be correct + await expect(page.locator('#field-id')).toHaveValue('id 1') + await expect(page.locator('.id-label')).toContainText('id 1') + }) +}) diff --git a/test/fields/collections/Indexed/e2e.spec.ts b/test/fields/collections/Indexed/e2e.spec.ts new file mode 100644 index 0000000000..71de2e4e2d --- /dev/null +++ b/test/fields/collections/Indexed/e2e.spec.ts @@ -0,0 +1,130 @@ +import type { Page } from '@playwright/test' + +import { expect, test } from '@playwright/test' +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 { POLL_TOPASS_TIMEOUT, TEST_TIMEOUT_LONG } from '../../../playwright.config.js' +import { indexedFieldsSlug } from '../../slugs.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 +let client: RESTClient +let page: Page +let serverURL: string +let url: AdminUrlUtil + +describe('Radio', () => { + 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({ + dirname, + // prebuild, + })) + + url = new AdminUrlUtil(serverURL, indexedFieldsSlug) + + 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(null, { defaultSlug: 'users', serverURL }) + await client.login() + await ensureCompilationIsDone({ page, serverURL }) + }) + + test('should display unique constraint error in ui', async () => { + const uniqueText = 'uniqueText' + const doc = await payload.create({ + collection: 'indexed-fields', + data: { + group: { + unique: uniqueText, + }, + localizedUniqueRequiredText: 'text', + text: 'text', + uniqueRequiredText: 'text', + uniqueText, + }, + }) + + await payload.update({ + id: doc.id, + collection: 'indexed-fields', + data: { + localizedUniqueRequiredText: 'es text', + }, + locale: 'es', + }) + + await page.goto(url.create) + await page.waitForURL(url.create) + + await page.locator('#field-text').fill('test') + await page.locator('#field-uniqueText').fill(uniqueText) + await page.locator('#field-localizedUniqueRequiredText').fill('localizedUniqueRequired2') + + await wait(500) + + // attempt to save + await page.click('#action-save', { delay: 200 }) + + // toast error + await expect(page.locator('.payload-toast-container')).toContainText( + 'The following field is invalid: uniqueText', + ) + + await expect.poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT }).toContain('create') + + // field specific error + await expect(page.locator('.field-type.text.error #field-uniqueText')).toBeVisible() + + // reset first unique field + await page.locator('#field-uniqueText').clear() + + // nested in a group error + await page.locator('#field-group__unique').fill(uniqueText) + + await wait(1000) + + // attempt to save + await page.locator('#action-save').click() + + // toast error + await expect(page.locator('.payload-toast-container')).toContainText( + 'The following field is invalid: group.unique', + ) + + await expect.poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT }).toContain('create') + + // field specific error inside group + await expect(page.locator('.field-type.text.error #field-group__unique')).toBeVisible() + }) +}) diff --git a/test/fields/collections/JSON/e2e.spec.ts b/test/fields/collections/JSON/e2e.spec.ts new file mode 100644 index 0000000000..e7f4a8f2ad --- /dev/null +++ b/test/fields/collections/JSON/e2e.spec.ts @@ -0,0 +1,106 @@ +import type { Page } from '@playwright/test' + +import { expect, test } from '@playwright/test' +import path from 'path' +import { fileURLToPath } from 'url' + +import type { PayloadTestSDK } from '../../../helpers/sdk/index.js' +import type { Config } from '../../payload-types.js' + +import { + ensureCompilationIsDone, + initPageConsoleErrorCatch, + saveDocAndAssert, +} 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 { POLL_TOPASS_TIMEOUT, TEST_TIMEOUT_LONG } from '../../../playwright.config.js' +import { jsonFieldsSlug } from '../../slugs.js' +import { jsonDoc } 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 +let client: RESTClient +let page: Page +let serverURL: string +let url: AdminUrlUtil + +describe('JSON', () => { + 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({ + dirname, + // prebuild, + })) + + url = new AdminUrlUtil(serverURL, jsonFieldsSlug) + + 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(null, { defaultSlug: 'users', serverURL }) + await client.login() + await ensureCompilationIsDone({ page, serverURL }) + }) + + test('should display field in list view', async () => { + await page.goto(url.list) + const jsonCell = page.locator('.row-1 .cell-json') + await expect(jsonCell).toHaveText(JSON.stringify(jsonDoc.json)) + }) + + test('should create', async () => { + const input = '{"foo": "bar"}' + await page.goto(url.create) + await page.waitForURL(url.create) + const jsonCodeEditor = page.locator('.json-field .code-editor').first() + await expect(() => expect(jsonCodeEditor).toBeVisible()).toPass({ + timeout: POLL_TOPASS_TIMEOUT, + }) + const jsonFieldInputArea = page.locator('.json-field .inputarea').first() + await jsonFieldInputArea.fill(input) + + await saveDocAndAssert(page) + const jsonField = page.locator('.json-field').first() + await expect(jsonField).toContainText('"foo": "bar"') + }) + + test('should not unflatten json field containing keys with dots', async () => { + const input = '{"foo.with.periods": "bar"}' + + await page.goto(url.create) + await page.waitForURL(url.create) + const jsonCodeEditor = page.locator('.group-field .json-field .code-editor').first() + await expect(() => expect(jsonCodeEditor).toBeVisible()).toPass({ + timeout: POLL_TOPASS_TIMEOUT, + }) + const json = page.locator('.group-field .json-field .inputarea') + await json.fill(input) + + await saveDocAndAssert(page, '.form-submit button') + await expect(page.locator('.group-field .json-field')).toContainText( + '"foo.with.periods": "bar"', + ) + }) +}) diff --git a/test/fields/collections/Radio/e2e.spec.ts b/test/fields/collections/Radio/e2e.spec.ts new file mode 100644 index 0000000000..6ac99c452a --- /dev/null +++ b/test/fields/collections/Radio/e2e.spec.ts @@ -0,0 +1,78 @@ +import type { Page } from '@playwright/test' + +import { expect, test } from '@playwright/test' +import path from 'path' +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 { radioFieldsSlug } from '../../slugs.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 +let client: RESTClient +let page: Page +let serverURL: string +let url: AdminUrlUtil + +describe('Radio', () => { + 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({ + dirname, + // prebuild, + })) + + url = new AdminUrlUtil(serverURL, radioFieldsSlug) + + 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(null, { defaultSlug: 'users', serverURL }) + await client.login() + await ensureCompilationIsDone({ page, serverURL }) + }) + + test('should show i18n label in list', async () => { + await page.goto(url.list) + await expect(page.locator('.cell-radio')).toHaveText('Value One') + }) + + test('should show i18n label while editing', async () => { + await page.goto(url.create) + await expect(page.locator('label[for="field-radio"]')).toHaveText('Radio en') + }) + + test('should show i18n radio labels', async () => { + await page.goto(url.create) + await expect(page.locator('label[for="field-radio-one"] .radio-input__label')).toHaveText( + 'Value One', + ) + }) +}) diff --git a/test/fields/collections/Row/e2e.spec.ts b/test/fields/collections/Row/e2e.spec.ts new file mode 100644 index 0000000000..57cf05b0d2 --- /dev/null +++ b/test/fields/collections/Row/e2e.spec.ts @@ -0,0 +1,208 @@ +import type { Page } from '@playwright/test' + +import { expect, test } from '@playwright/test' +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 { rowFieldsSlug } from '../../slugs.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 +let client: RESTClient +let page: Page +let serverURL: string +let url: AdminUrlUtil + +describe('Row', () => { + 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({ + dirname, + // prebuild, + })) + + url = new AdminUrlUtil(serverURL, rowFieldsSlug) + + 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(null, { defaultSlug: 'users', serverURL }) + await client.login() + await ensureCompilationIsDone({ page, serverURL }) + }) + + test('should show row fields as table columns', async () => { + await page.goto(url.create) + + // fill the required fields, including the row field + const idInput = page.locator('input#field-id') + await idInput.fill('123') + const titleInput = page.locator('input#field-title') + await titleInput.fill('Row 123') + await page.locator('#action-save').click() + await wait(200) + await expect(page.locator('.payload-toast-container')).toContainText('successfully') + + // ensure the 'title' field is visible in the table header + await page.goto(url.list) + const titleHeading = page.locator('th#heading-title') + await expect(titleHeading).toBeVisible() + + // ensure the 'title' field shows the correct value in the table cell + const titleCell = page.locator('.row-1 td.cell-title') + await expect(titleCell).toBeVisible() + await expect(titleCell).toContainText('Row 123') + }) + + test('should not show duplicative ID field', async () => { + await page.goto(url.create) + // fill the required fields, including the custom ID field + const idInput = page.locator('input#field-id') + await idInput.fill('456') + const titleInput = page.locator('input#field-title') + await titleInput.fill('Row 456') + await page.locator('#action-save').click() + await wait(200) + await expect(page.locator('.payload-toast-container')).toContainText('successfully') + + // ensure there are not two ID fields in the table header + await page.goto(url.list) + const idHeadings = page.locator('th#heading-id') + await expect(idHeadings).toBeVisible() + await expect(idHeadings).toHaveCount(1) + }) + + test('should render row fields inline and with explicit widths', async () => { + await page.goto(url.create) + const fieldA = page.locator('input#field-field_with_width_a') + const fieldB = page.locator('input#field-field_with_width_b') + + await expect(fieldA).toBeVisible() + await expect(fieldB).toBeVisible() + + const fieldABox = await fieldA.boundingBox() + const fieldBBox = await fieldB.boundingBox() + + await expect(() => { + expect(fieldABox.y).toEqual(fieldBBox.y) + expect(fieldABox.width).toEqual(fieldBBox.width) + }).toPass() + + const field_30_percent = page.locator( + '.field-type.text:has(input#field-field_with_width_30_percent)', + ) + const field_60_percent = page.locator( + '.field-type.text:has(input#field-field_with_width_60_percent)', + ) + const field_20_percent = page.locator( + '.field-type.text:has(input#field-field_with_width_20_percent)', + ) + const collapsible_30_percent = page.locator( + '.collapsible-field:has(#field-field_within_collapsible_a)', + ) + + const field_20_percent_width_within_row_a = page.locator( + '.field-type.text:has(input#field-field_20_percent_width_within_row_a)', + ) + const field_no_set_width_within_row_b = page.locator( + '.field-type.text:has(input#field-no_set_width_within_row_b)', + ) + const field_no_set_width_within_row_c = page.locator( + '.field-type.text:has(input#field-no_set_width_within_row_c)', + ) + const field_20_percent_width_within_row_d = page.locator( + '.field-type.text:has(input#field-field_20_percent_width_within_row_d)', + ) + + await expect(field_30_percent).toBeVisible() + await expect(field_60_percent).toBeVisible() + await expect(field_20_percent).toBeVisible() + await expect(collapsible_30_percent).toBeVisible() + await expect(field_20_percent_width_within_row_a).toBeVisible() + await expect(field_no_set_width_within_row_b).toBeVisible() + await expect(field_no_set_width_within_row_c).toBeVisible() + await expect(field_20_percent_width_within_row_d).toBeVisible() + + const field_30_boundingBox = await field_30_percent.boundingBox() + const field_60_boundingBox = await field_60_percent.boundingBox() + const field_20_boundingBox = await field_20_percent.boundingBox() + const collapsible_30_boundingBox = await collapsible_30_percent.boundingBox() + const field_20_percent_width_within_row_a_box = + await field_20_percent_width_within_row_a.boundingBox() + const field_no_set_width_within_row_b_box = await field_no_set_width_within_row_b.boundingBox() + const field_no_set_width_within_row_c_box = await field_no_set_width_within_row_c.boundingBox() + const field_20_percent_width_within_row_d_box = + await field_20_percent_width_within_row_d.boundingBox() + + await expect(() => { + expect(field_30_boundingBox.y).toEqual(field_60_boundingBox.y) + expect(field_30_boundingBox.x).toEqual(field_20_boundingBox.x) + expect(field_30_boundingBox.y).not.toEqual(field_20_boundingBox.y) + expect(field_30_boundingBox.height).toEqual(field_60_boundingBox.height) + expect(collapsible_30_boundingBox.width).toEqual(field_30_boundingBox.width) + + expect(field_20_percent_width_within_row_a_box.y).toEqual( + field_no_set_width_within_row_b_box.y, + ) + expect(field_no_set_width_within_row_b_box.y).toEqual(field_no_set_width_within_row_c_box.y) + expect(field_no_set_width_within_row_c_box.y).toEqual( + field_20_percent_width_within_row_d_box.y, + ) + + expect(field_20_percent_width_within_row_a_box.width).toEqual( + field_20_percent_width_within_row_d_box.width, + ) + expect(field_no_set_width_within_row_b_box.width).toEqual( + field_no_set_width_within_row_c_box.width, + ) + }).toPass() + }) + + test('should render nested row fields in the correct position', async () => { + await page.goto(url.create) + + // These fields are not given explicit `width` values + await page.goto(url.create) + const fieldA = page.locator('input#field-field_within_collapsible_a') + await expect(fieldA).toBeVisible() + const fieldB = page.locator('input#field-field_within_collapsible_b') + await expect(fieldB).toBeVisible() + const fieldABox = await fieldA.boundingBox() + const fieldBBox = await fieldB.boundingBox() + + await expect(() => { + // Check that the top value of the fields are the same + expect(fieldABox.y).toEqual(fieldBBox.y) + expect(fieldABox.height).toEqual(fieldBBox.height) + }).toPass() + }) +}) diff --git a/test/fields/collections/Select/e2e.spec.ts b/test/fields/collections/Select/e2e.spec.ts new file mode 100644 index 0000000000..e98304cde6 --- /dev/null +++ b/test/fields/collections/Select/e2e.spec.ts @@ -0,0 +1,78 @@ +import type { Page } from '@playwright/test' + +import { expect, test } from '@playwright/test' +import path from 'path' +import { fileURLToPath } from 'url' + +import type { PayloadTestSDK } from '../../../helpers/sdk/index.js' +import type { Config } from '../../payload-types.js' + +import { + ensureCompilationIsDone, + initPageConsoleErrorCatch, + saveDocAndAssert, +} 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 { selectFieldsSlug } from '../../slugs.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 +let client: RESTClient +let page: Page +let serverURL: string +let url: AdminUrlUtil + +describe('Radio', () => { + 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({ + dirname, + // prebuild, + })) + + url = new AdminUrlUtil(serverURL, selectFieldsSlug) + + 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(null, { defaultSlug: 'users', serverURL }) + await client.login() + await ensureCompilationIsDone({ page, serverURL }) + }) + + test('should use i18n option labels', async () => { + await page.goto(url.create) + + const field = page.locator('#field-selectI18n') + await field.click({ delay: 100 }) + const options = page.locator('.rs__option') + // Select an option + await options.locator('text=One').click() + + await saveDocAndAssert(page) + await expect(field.locator('.rs__value-container')).toContainText('One') + }) +}) diff --git a/test/fields/collections/Tabs/e2e.spec.ts b/test/fields/collections/Tabs/e2e.spec.ts index 9670d29727..5a009f8dbb 100644 --- a/test/fields/collections/Tabs/e2e.spec.ts +++ b/test/fields/collections/Tabs/e2e.spec.ts @@ -19,7 +19,7 @@ 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 { POLL_TOPASS_TIMEOUT, TEST_TIMEOUT_LONG } from '../../../playwright.config.js' import { tabsFieldsSlug } from '../../slugs.js' const filename = fileURLToPath(import.meta.url) @@ -132,4 +132,26 @@ describe('Tabs', () => { "Hello, I'm the first row, in a named tab", ) }) + + test('should save preferences for tab order', async () => { + await page.goto(url.list) + + const firstItem = page.locator('.cell-id a').nth(0) + const href = await firstItem.getAttribute('href') + await firstItem.click() + + const regex = new RegExp(href.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) + + await page.waitForURL(regex) + + await page.locator('.tabs-field__tabs button:nth-child(2)').nth(0).click() + + await page.reload() + + const tab2 = page.locator('.tabs-field__tabs button:nth-child(2)').nth(0) + + await expect(async () => await expect(tab2).toHaveClass(/--active/)).toPass({ + timeout: POLL_TOPASS_TIMEOUT, + }) + }) }) diff --git a/test/fields/collections/Tabs2/e2e.spec.ts b/test/fields/collections/Tabs2/e2e.spec.ts new file mode 100644 index 0000000000..913d20b5ed --- /dev/null +++ b/test/fields/collections/Tabs2/e2e.spec.ts @@ -0,0 +1,81 @@ +import type { Page } from '@playwright/test' + +import { expect, test } from '@playwright/test' +import path from 'path' +import { fileURLToPath } from 'url' + +import type { PayloadTestSDK } from '../../../helpers/sdk/index.js' +import type { Config } from '../../payload-types.js' + +import { + ensureCompilationIsDone, + initPageConsoleErrorCatch, + saveDocAndAssert, +} 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 { tabsFields2Slug } from '../../slugs.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 +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('Tabs', () => { + 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({ + dirname, + // prebuild, + })) + url = new AdminUrlUtil(serverURL, tabsFields2Slug) + + 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(null, { defaultSlug: 'users', serverURL }) + await client.login() + + await ensureCompilationIsDone({ page, serverURL }) + }) + + test('should correctly save nested unnamed and named tabs', async () => { + await page.goto(url.create) + + await page.locator('#field-tabsInArray .array-field__add-row').click() + await page.locator('#field-tabsInArray__0__text').fill('tab 1 text') + await page.locator('.tabs-field__tabs button:nth-child(2)').click() + await page.locator('#field-tabsInArray__0__tab2__text2').fill('tab 2 text') + + await saveDocAndAssert(page) + + await expect(page.locator('#field-tabsInArray__0__text')).toHaveValue('tab 1 text') + await page.locator('.tabs-field__tabs button:nth-child(2)').click() + await expect(page.locator('#field-tabsInArray__0__tab2__text2')).toHaveValue('tab 2 text') + }) +}) diff --git a/test/fields/collections/UI/e2e.spec.ts b/test/fields/collections/UI/e2e.spec.ts new file mode 100644 index 0000000000..54d499f099 --- /dev/null +++ b/test/fields/collections/UI/e2e.spec.ts @@ -0,0 +1,70 @@ +import type { Page } from '@playwright/test' + +import { expect, test } from '@playwright/test' +import path from 'path' +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 { uiSlug } from '../../slugs.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 +let client: RESTClient +let page: Page +let serverURL: string +let url: AdminUrlUtil + +describe('Radio', () => { + 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({ + dirname, + // prebuild, + })) + + url = new AdminUrlUtil(serverURL, uiSlug) + + 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(null, { defaultSlug: 'users', serverURL }) + await client.login() + await ensureCompilationIsDone({ page, serverURL }) + }) + + test('should show custom: client configuration', async () => { + await page.goto(url.create) + + const uiField = page.locator('#uiCustomClient') + + await expect(uiField).toBeVisible() + await expect(uiField).toContainText('client-side-configuration') + }) +}) diff --git a/test/fields/e2e.spec.ts b/test/fields/e2e.spec.ts deleted file mode 100644 index c6ec5303a0..0000000000 --- a/test/fields/e2e.spec.ts +++ /dev/null @@ -1,641 +0,0 @@ -import type { Page } from '@playwright/test' - -import { expect, test } from '@playwright/test' -import { navigateToDoc } from 'helpers/e2e/navigateToDoc.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, saveDocAndAssert } 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 { POLL_TOPASS_TIMEOUT, TEST_TIMEOUT_LONG } from '../playwright.config.js' -import { jsonDoc } from './collections/JSON/shared.js' -import { - arrayFieldsSlug, - blockFieldsSlug, - collapsibleFieldsSlug, - customIdSlug, - tabsFields2Slug, - tabsFieldsSlug, -} from './slugs.js' -const filename = fileURLToPath(import.meta.url) -const dirname = path.dirname(filename) - -const { beforeAll, beforeEach, describe } = test - -let payload: PayloadTestSDK -let client: RESTClient -let page: Page -let serverURL: string -// If we want to make this run in parallel: test.describe.configure({ mode: 'parallel' }) - -describe('fields', () => { - 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({ - dirname, - // prebuild, - })) - - 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(null, { defaultSlug: 'users', serverURL }) - await client.login() - - await ensureCompilationIsDone({ page, serverURL }) - }) - - describe('indexed', () => { - let url: AdminUrlUtil - beforeEach(() => { - url = new AdminUrlUtil(serverURL, 'indexed-fields') - }) - - // TODO - This test is flaky. Rarely, but sometimes it randomly fails. - test('should display unique constraint error in ui', async () => { - const uniqueText = 'uniqueText' - const doc = await payload.create({ - collection: 'indexed-fields', - data: { - group: { - unique: uniqueText, - }, - localizedUniqueRequiredText: 'text', - text: 'text', - uniqueRequiredText: 'text', - uniqueText, - }, - }) - await payload.update({ - id: doc.id, - collection: 'indexed-fields', - data: { - localizedUniqueRequiredText: 'es text', - }, - locale: 'es', - }) - - await page.goto(url.create) - await page.waitForURL(url.create) - - await page.locator('#field-text').fill('test') - await page.locator('#field-uniqueText').fill(uniqueText) - await page.locator('#field-localizedUniqueRequiredText').fill('localizedUniqueRequired2') - - await wait(500) - - // attempt to save - await page.click('#action-save', { delay: 200 }) - - // toast error - await expect(page.locator('.payload-toast-container')).toContainText( - 'The following field is invalid: uniqueText', - ) - - await expect.poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT }).toContain('create') - - // field specific error - await expect(page.locator('.field-type.text.error #field-uniqueText')).toBeVisible() - - // reset first unique field - await page.locator('#field-uniqueText').clear() - - // nested in a group error - await page.locator('#field-group__unique').fill(uniqueText) - - await wait(1000) - - // attempt to save - await page.locator('#action-save').click() - - // toast error - await expect(page.locator('.payload-toast-container')).toContainText( - 'The following field is invalid: group.unique', - ) - - await expect.poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT }).toContain('create') - - // field specific error inside group - await expect(page.locator('.field-type.text.error #field-group__unique')).toBeVisible() - }) - }) - - describe('json', () => { - let url: AdminUrlUtil - beforeAll(() => { - url = new AdminUrlUtil(serverURL, 'json-fields') - }) - - test('should display field in list view', async () => { - await page.goto(url.list) - const jsonCell = page.locator('.row-1 .cell-json') - await expect(jsonCell).toHaveText(JSON.stringify(jsonDoc.json)) - }) - - test('should create', async () => { - const input = '{"foo": "bar"}' - await page.goto(url.create) - await page.waitForURL(url.create) - const jsonCodeEditor = page.locator('.json-field .code-editor').first() - await expect(() => expect(jsonCodeEditor).toBeVisible()).toPass({ - timeout: POLL_TOPASS_TIMEOUT, - }) - const jsonFieldInputArea = page.locator('.json-field .inputarea').first() - await jsonFieldInputArea.fill(input) - - await saveDocAndAssert(page) - const jsonField = page.locator('.json-field').first() - await expect(jsonField).toContainText('"foo": "bar"') - }) - - test('should not unflatten json field containing keys with dots', async () => { - const input = '{"foo.with.periods": "bar"}' - - await page.goto(url.create) - await page.waitForURL(url.create) - const jsonCodeEditor = page.locator('.group-field .json-field .code-editor').first() - await expect(() => expect(jsonCodeEditor).toBeVisible()).toPass({ - timeout: POLL_TOPASS_TIMEOUT, - }) - const json = page.locator('.group-field .json-field .inputarea') - await json.fill(input) - - await saveDocAndAssert(page, '.form-submit button') - await expect(page.locator('.group-field .json-field')).toContainText( - '"foo.with.periods": "bar"', - ) - }) - }) - - describe('radio', () => { - let url: AdminUrlUtil - beforeAll(() => { - url = new AdminUrlUtil(serverURL, 'radio-fields') - }) - - test('should show i18n label in list', async () => { - await page.goto(url.list) - await expect(page.locator('.cell-radio')).toHaveText('Value One') - }) - - test('should show i18n label while editing', async () => { - await page.goto(url.create) - await expect(page.locator('label[for="field-radio"]')).toHaveText('Radio en') - }) - - test('should show i18n radio labels', async () => { - await page.goto(url.create) - await expect(page.locator('label[for="field-radio-one"] .radio-input__label')).toHaveText( - 'Value One', - ) - }) - }) - - describe('select', () => { - let url: AdminUrlUtil - beforeAll(() => { - url = new AdminUrlUtil(serverURL, 'select-fields') - }) - - test('should use i18n option labels', async () => { - await page.goto(url.create) - - const field = page.locator('#field-selectI18n') - await field.click({ delay: 100 }) - const options = page.locator('.rs__option') - // Select an option - await options.locator('text=One').click() - - await saveDocAndAssert(page) - await expect(field.locator('.rs__value-container')).toContainText('One') - }) - }) - - describe('collapsible', () => { - let url: AdminUrlUtil - beforeAll(() => { - url = new AdminUrlUtil(serverURL, collapsibleFieldsSlug) - }) - - test('should render collapsible as collapsed if initCollapsed is true', async () => { - await page.goto(url.create) - const collapsedCollapsible = page.locator( - '#field-collapsible-_index-1 .collapsible__toggle--collapsed', - ) - await expect(collapsedCollapsible).toBeVisible() - }) - - test('should render CollapsibleLabel using a function', async () => { - const label = 'custom row label' - await page.goto(url.create) - await page.locator('#field-collapsible-_index-3-1 #field-nestedTitle').fill(label) - await wait(100) - const customCollapsibleLabel = page.locator( - `#field-collapsible-_index-3-1 .collapsible-field__row-label-wrap :text("${label}")`, - ) - await expect(customCollapsibleLabel).toContainText(label) - }) - - test('should render CollapsibleLabel using a component', async () => { - const label = 'custom row label as component' - await page.goto(url.create) - await page.locator('#field-arrayWithCollapsibles').scrollIntoViewIfNeeded() - - const arrayWithCollapsibles = page.locator('#field-arrayWithCollapsibles') - await expect(arrayWithCollapsibles).toBeVisible() - - await page.locator('#field-arrayWithCollapsibles >> .array-field__add-row').click() - - await page - .locator( - '#arrayWithCollapsibles-row-0 #field-collapsible-arrayWithCollapsibles__0___index-0 #field-arrayWithCollapsibles__0__innerCollapsible', - ) - .fill(label) - await wait(100) - const customCollapsibleLabel = page.locator( - `#field-arrayWithCollapsibles >> #arrayWithCollapsibles-row-0 >> .collapsible-field__row-label-wrap :text("${label}")`, - ) - await expect(customCollapsibleLabel).toHaveCSS('text-transform', 'uppercase') - }) - }) - - describe('sortable arrays', () => { - let url: AdminUrlUtil - beforeAll(() => { - url = new AdminUrlUtil(serverURL, arrayFieldsSlug) - }) - - test('should have disabled admin sorting', async () => { - await page.goto(url.create) - const field = page.locator('#field-disableSort > div > div > .array-actions__action-chevron') - expect(await field.count()).toEqual(0) - }) - - test('the drag handle should be hidden', async () => { - await page.goto(url.create) - const field = page.locator( - '#field-disableSort > .blocks-field__rows > div > div > .collapsible__drag', - ) - expect(await field.count()).toEqual(0) - }) - }) - - describe('sortable blocks', () => { - let url: AdminUrlUtil - beforeAll(() => { - url = new AdminUrlUtil(serverURL, blockFieldsSlug) - }) - - test('should have disabled admin sorting', async () => { - await page.goto(url.create) - const field = page.locator('#field-disableSort > div > div > .array-actions__action-chevron') - expect(await field.count()).toEqual(0) - }) - - test('the drag handle should be hidden', async () => { - await page.goto(url.create) - const field = page.locator( - '#field-disableSort > .blocks-field__rows > div > div > .collapsible__drag', - ) - expect(await field.count()).toEqual(0) - }) - }) - - describe('row', () => { - let url: AdminUrlUtil - beforeAll(() => { - url = new AdminUrlUtil(serverURL, 'row-fields') - }) - - test('should show row fields as table columns', async () => { - await page.goto(url.create) - - // fill the required fields, including the row field - const idInput = page.locator('input#field-id') - await idInput.fill('123') - const titleInput = page.locator('input#field-title') - await titleInput.fill('Row 123') - await page.locator('#action-save').click() - await wait(200) - await expect(page.locator('.payload-toast-container')).toContainText('successfully') - - // ensure the 'title' field is visible in the table header - await page.goto(url.list) - const titleHeading = page.locator('th#heading-title') - await expect(titleHeading).toBeVisible() - - // ensure the 'title' field shows the correct value in the table cell - const titleCell = page.locator('.row-1 td.cell-title') - await expect(titleCell).toBeVisible() - await expect(titleCell).toContainText('Row 123') - }) - - test('should not show duplicative ID field', async () => { - await page.goto(url.create) - // fill the required fields, including the custom ID field - const idInput = page.locator('input#field-id') - await idInput.fill('456') - const titleInput = page.locator('input#field-title') - await titleInput.fill('Row 456') - await page.locator('#action-save').click() - await wait(200) - await expect(page.locator('.payload-toast-container')).toContainText('successfully') - - // ensure there are not two ID fields in the table header - await page.goto(url.list) - const idHeadings = page.locator('th#heading-id') - await expect(idHeadings).toBeVisible() - await expect(idHeadings).toHaveCount(1) - }) - - test('should render row fields inline and with explicit widths', async () => { - await page.goto(url.create) - const fieldA = page.locator('input#field-field_with_width_a') - const fieldB = page.locator('input#field-field_with_width_b') - - await expect(fieldA).toBeVisible() - await expect(fieldB).toBeVisible() - - const fieldABox = await fieldA.boundingBox() - const fieldBBox = await fieldB.boundingBox() - - await expect(() => { - expect(fieldABox.y).toEqual(fieldBBox.y) - expect(fieldABox.width).toEqual(fieldBBox.width) - }).toPass() - - const field_30_percent = page.locator( - '.field-type.text:has(input#field-field_with_width_30_percent)', - ) - const field_60_percent = page.locator( - '.field-type.text:has(input#field-field_with_width_60_percent)', - ) - const field_20_percent = page.locator( - '.field-type.text:has(input#field-field_with_width_20_percent)', - ) - const collapsible_30_percent = page.locator( - '.collapsible-field:has(#field-field_within_collapsible_a)', - ) - - const field_20_percent_width_within_row_a = page.locator( - '.field-type.text:has(input#field-field_20_percent_width_within_row_a)', - ) - const field_no_set_width_within_row_b = page.locator( - '.field-type.text:has(input#field-no_set_width_within_row_b)', - ) - const field_no_set_width_within_row_c = page.locator( - '.field-type.text:has(input#field-no_set_width_within_row_c)', - ) - const field_20_percent_width_within_row_d = page.locator( - '.field-type.text:has(input#field-field_20_percent_width_within_row_d)', - ) - - await expect(field_30_percent).toBeVisible() - await expect(field_60_percent).toBeVisible() - await expect(field_20_percent).toBeVisible() - await expect(collapsible_30_percent).toBeVisible() - await expect(field_20_percent_width_within_row_a).toBeVisible() - await expect(field_no_set_width_within_row_b).toBeVisible() - await expect(field_no_set_width_within_row_c).toBeVisible() - await expect(field_20_percent_width_within_row_d).toBeVisible() - - const field_30_boundingBox = await field_30_percent.boundingBox() - const field_60_boundingBox = await field_60_percent.boundingBox() - const field_20_boundingBox = await field_20_percent.boundingBox() - const collapsible_30_boundingBox = await collapsible_30_percent.boundingBox() - const field_20_percent_width_within_row_a_box = - await field_20_percent_width_within_row_a.boundingBox() - const field_no_set_width_within_row_b_box = - await field_no_set_width_within_row_b.boundingBox() - const field_no_set_width_within_row_c_box = - await field_no_set_width_within_row_c.boundingBox() - const field_20_percent_width_within_row_d_box = - await field_20_percent_width_within_row_d.boundingBox() - - await expect(() => { - expect(field_30_boundingBox.y).toEqual(field_60_boundingBox.y) - expect(field_30_boundingBox.x).toEqual(field_20_boundingBox.x) - expect(field_30_boundingBox.y).not.toEqual(field_20_boundingBox.y) - expect(field_30_boundingBox.height).toEqual(field_60_boundingBox.height) - expect(collapsible_30_boundingBox.width).toEqual(field_30_boundingBox.width) - - expect(field_20_percent_width_within_row_a_box.y).toEqual( - field_no_set_width_within_row_b_box.y, - ) - expect(field_no_set_width_within_row_b_box.y).toEqual(field_no_set_width_within_row_c_box.y) - expect(field_no_set_width_within_row_c_box.y).toEqual( - field_20_percent_width_within_row_d_box.y, - ) - - expect(field_20_percent_width_within_row_a_box.width).toEqual( - field_20_percent_width_within_row_d_box.width, - ) - expect(field_no_set_width_within_row_b_box.width).toEqual( - field_no_set_width_within_row_c_box.width, - ) - }).toPass() - }) - - test('should render nested row fields in the correct position', async () => { - await page.goto(url.create) - - // These fields are not given explicit `width` values - await page.goto(url.create) - const fieldA = page.locator('input#field-field_within_collapsible_a') - await expect(fieldA).toBeVisible() - const fieldB = page.locator('input#field-field_within_collapsible_b') - await expect(fieldB).toBeVisible() - const fieldABox = await fieldA.boundingBox() - const fieldBBox = await fieldB.boundingBox() - - await expect(() => { - // Check that the top value of the fields are the same - expect(fieldABox.y).toEqual(fieldBBox.y) - expect(fieldABox.height).toEqual(fieldBBox.height) - }).toPass() - }) - }) - - describe('ui', () => { - let url: AdminUrlUtil - beforeAll(() => { - url = new AdminUrlUtil(serverURL, 'ui-fields') - }) - - test('should show custom: client configuration', async () => { - await page.goto(url.create) - - const uiField = page.locator('#uiCustomClient') - - await expect(uiField).toBeVisible() - await expect(uiField).toContainText('client-side-configuration') - }) - }) - - describe('conditional logic', () => { - let url: AdminUrlUtil - beforeAll(() => { - url = new AdminUrlUtil(serverURL, 'conditional-logic') - }) - - test('should toggle conditional field when data changes', async () => { - await page.goto(url.create) - const toggleField = page.locator('label[for=field-toggleField]') - await toggleField.click() - - const fieldToToggle = page.locator('input#field-fieldToToggle') - - await expect(fieldToToggle).toBeVisible() - }) - - test('should show conditional field based on user data', async () => { - await page.goto(url.create) - const userConditional = page.locator('input#field-userConditional') - await expect(userConditional).toBeVisible() - }) - - test('should show conditional field based on fields nested within data', async () => { - await page.goto(url.create) - - const parentGroupFields = page.locator( - 'div#field-parentGroup > .group-field__wrap > .render-fields', - ) - await expect(parentGroupFields).toHaveCount(1) - - const toggle = page.locator('label[for=field-parentGroup__enableParentGroupFields]') - await toggle.click() - - const toggledField = page.locator('input#field-parentGroup__siblingField') - - await expect(toggledField).toBeVisible() - }) - - test('should show conditional field based on fields nested within siblingData', async () => { - await page.goto(url.create) - - const toggle = page.locator('label[for=field-parentGroup__enableParentGroupFields]') - await toggle.click() - - const fieldRelyingOnSiblingData = page.locator('input#field-reliesOnParentGroup') - await expect(fieldRelyingOnSiblingData).toBeVisible() - }) - - test('should not render conditional fields when adding array rows', async () => { - await page.goto(url.create) - const addRowButton = page.locator('.array-field__add-row') - const fieldWithConditionSelector = - 'input#field-arrayWithConditionalField__0__textWithCondition' - await addRowButton.click() - - const wasFieldAttached = await page - .waitForSelector(fieldWithConditionSelector, { - state: 'attached', - timeout: 100, // A small timeout to catch any transient rendering - }) - .catch(() => false) // If it doesn't appear, this resolves to `false` - - expect(wasFieldAttached).toBeFalsy() - const fieldToToggle = page.locator('input#field-enableConditionalFields') - await fieldToToggle.click() - await expect(page.locator(fieldWithConditionSelector)).toBeVisible() - }) - }) - - describe('tabs', () => { - // tabsFieldsSlug is used for testing tabs - let tabsFieldsUrl: AdminUrlUtil - // tabsFields2Slug is used for testing nested tabs - let tabsFieldsUrl2: AdminUrlUtil - - beforeAll(() => { - tabsFieldsUrl = new AdminUrlUtil(serverURL, tabsFieldsSlug) - tabsFieldsUrl2 = new AdminUrlUtil(serverURL, tabsFields2Slug) - }) - - test('should correctly save nested unnamed and named tabs', async () => { - await page.goto(tabsFieldsUrl2.create) - - await page.locator('#field-tabsInArray .array-field__add-row').click() - await page.locator('#field-tabsInArray__0__text').fill('tab 1 text') - await page.locator('.tabs-field__tabs button:nth-child(2)').click() - await page.locator('#field-tabsInArray__0__tab2__text2').fill('tab 2 text') - - await saveDocAndAssert(page) - - await expect(page.locator('#field-tabsInArray__0__text')).toHaveValue('tab 1 text') - await page.locator('.tabs-field__tabs button:nth-child(2)').click() - await expect(page.locator('#field-tabsInArray__0__tab2__text2')).toHaveValue('tab 2 text') - }) - - test('should save preferences for tab order', async () => { - await page.goto(tabsFieldsUrl.list) - - const firstItem = page.locator('.cell-id a').nth(0) - const href = await firstItem.getAttribute('href') - await firstItem.click() - - const regex = new RegExp(href.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) - - await page.waitForURL(regex) - - await page.locator('.tabs-field__tabs button:nth-child(2)').nth(0).click() - - await page.reload() - - const tab2 = page.locator('.tabs-field__tabs button:nth-child(2)').nth(0) - - await expect(async () => await expect(tab2).toHaveClass(/--active/)).toPass({ - timeout: POLL_TOPASS_TIMEOUT, - }) - }) - }) - - describe('id', () => { - let url: AdminUrlUtil - beforeAll(() => { - url = new AdminUrlUtil(serverURL, customIdSlug) - }) - - function createCustomIDDoc(id: string) { - return payload.create({ - collection: customIdSlug, - data: { - id, - }, - }) - } - - test('allow create of non standard ID', async () => { - await createCustomIDDoc('id 1') - await page.goto(url.list) - - await navigateToDoc(page, url) - - // Page should load and ID should be correct - await expect(page.locator('#field-id')).toHaveValue('id 1') - await expect(page.locator('.id-label')).toContainText('id 1') - }) - }) -}) diff --git a/test/fields/payload-types.ts b/test/fields/payload-types.ts index 8c19391aef..500a4a2ebd 100644 --- a/test/fields/payload-types.ts +++ b/test/fields/payload-types.ts @@ -969,7 +969,7 @@ export interface ConditionalLogic { id: string; text: string; toggleField?: boolean | null; - fieldToToggle?: string | null; + fieldWithCondition?: string | null; userConditional?: string | null; parentGroup?: { enableParentGroupFields?: boolean | null; @@ -983,6 +983,23 @@ export interface ConditionalLogic { group2?: { group2Field?: string | null; }; + enableConditionalFields?: boolean | null; + arrayWithConditionalField?: + | { + text?: string | null; + textWithCondition?: string | null; + id?: string | null; + }[] + | null; + blocksWithConditionalField?: + | { + text?: string | null; + textWithCondition?: string | null; + id?: string | null; + blockName?: string | null; + blockType: 'blockWithConditionalField'; + }[] + | null; updatedAt: string; createdAt: string; } @@ -2639,7 +2656,7 @@ export interface CollapsibleFieldsSelect { export interface ConditionalLogicSelect { text?: T; toggleField?: T; - fieldToToggle?: T; + fieldWithCondition?: T; userConditional?: T; parentGroup?: | T @@ -2659,6 +2676,26 @@ export interface ConditionalLogicSelect { | { group2Field?: T; }; + enableConditionalFields?: T; + arrayWithConditionalField?: + | T + | { + text?: T; + textWithCondition?: T; + id?: T; + }; + blocksWithConditionalField?: + | T + | { + blockWithConditionalField?: + | T + | { + text?: T; + textWithCondition?: T; + id?: T; + blockName?: T; + }; + }; updatedAt?: T; createdAt?: T; }