import type { Page } from '@playwright/test'; import { expect, test } from '@playwright/test'; import { AdminUrlUtil } from '../helpers/adminUrlUtil'; import { initPayloadE2E } from '../helpers/configHelpers'; import { login, saveDocAndAssert } from '../helpers'; import { textDoc } from './collections/Text'; import { arrayFieldsSlug } from './collections/Array'; import { pointFieldsSlug } from './collections/Point'; import { tabsSlug } from './collections/Tabs'; import { collapsibleFieldsSlug } from './collections/Collapsible'; import wait from '../../src/utilities/wait'; import { relationshipFieldsSlug } from './collections/Relationship'; const { beforeAll, describe } = test; let page: Page; let serverURL; describe('fields', () => { beforeAll(async ({ browser }) => { const config = await initPayloadE2E(__dirname); serverURL = config.serverURL; const context = await browser.newContext(); page = await context.newPage(); await login({ page, serverURL }); }); describe('text', () => { let url: AdminUrlUtil; beforeAll(() => { url = new AdminUrlUtil(serverURL, 'text-fields'); }); test('should display field in list view', async () => { await page.goto(url.list); const textCell = page.locator('.row-1 .cell-text'); await expect(textCell) .toHaveText(textDoc.text); }); test('should display i18n label in cells when missing field data', async () => { await page.goto(url.list); const textCell = page.locator('.row-1 .cell-i18nText'); await expect(textCell) .toHaveText(''); }); test('should show i18n label', async () => { await page.goto(url.create); await expect(page.locator('label[for="field-i18nText"]')).toHaveText('Text en'); }); test('should show i18n placeholder', async () => { await page.goto(url.create); await expect(await page.locator('#field-i18nText')).toHaveAttribute('placeholder', 'en placeholder'); }); test('should show i18n descriptions', async () => { await page.goto(url.create); const description = page.locator('.field-description-i18nText'); await expect(description).toHaveText('en description'); }); }); 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(await page.locator('label[for="field-radio-one"] .radio-input__label')) .toHaveText('Value One'); }); }); describe('point', () => { let url: AdminUrlUtil; beforeAll(() => { url = new AdminUrlUtil(serverURL, pointFieldsSlug); }); test('should save point', async () => { await page.goto(url.create); const longField = page.locator('#field-longitude-point'); await longField.fill('9'); const latField = page.locator('#field-latitude-point'); await latField.fill('-2'); const localizedLongField = page.locator('#field-longitude-localized'); await localizedLongField.fill('1'); const localizedLatField = page.locator('#field-latitude-localized'); await localizedLatField.fill('-1'); const groupLongitude = page.locator('#field-longitude-group__point'); await groupLongitude.fill('3'); const groupLatField = page.locator('#field-latitude-group__point'); await groupLatField.fill('-8'); await saveDocAndAssert(page); }); }); describe('fields - collapsible', () => { let url: AdminUrlUtil; beforeAll(() => { url = new AdminUrlUtil(serverURL, collapsibleFieldsSlug); }); test('should render CollapsibleLabel using a function', async () => { const label = 'custom row label'; await page.goto(url.create); await page.locator('#field-collapsible-3__1 >> #field-nestedTitle').fill(label); await wait(100); const customCollapsibleLabel = await page.locator('#field-collapsible-3__1 >> .row-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 >> .array-field__add-button-wrap >> button').click(); await page.locator('#field-collapsible-4__0-arrayWithCollapsibles__0 >> #field-arrayWithCollapsibles__0__innerCollapsible').fill(label); await wait(100); const customCollapsibleLabel = await page.locator(`#field-collapsible-4__0-arrayWithCollapsibles__0 >> .row-label :text("${label}")`); await expect(customCollapsibleLabel).toHaveCSS('text-transform', 'uppercase'); }); }); describe('blocks', () => { let url: AdminUrlUtil; beforeAll(() => { url = new AdminUrlUtil(serverURL, 'block-fields'); }); test('should use i18n block labels', async () => { await page.goto(url.create); await expect(page.locator('#field-i18nBlocks .blocks-field__header')).toContainText('Block en'); const addButton = page.locator('#field-i18nBlocks .btn__label'); await expect(addButton).toContainText('Add Block en'); await addButton.click(); const blockSelector = page.locator('#field-i18nBlocks .block-selector .block-selection').first(); await expect(blockSelector).toContainText('Text en'); await blockSelector.click(); await expect(page.locator('#i18nBlocks-row-0 .blocks-field__block-pill-text')).toContainText('Text en'); }); }); describe('fields - array', () => { let url: AdminUrlUtil; beforeAll(() => { url = new AdminUrlUtil(serverURL, arrayFieldsSlug); }); test('should be readOnly', async () => { await page.goto(url.create); const field = page.locator('#field-readOnly__0__text'); await expect(field) .toBeDisabled(); }); test('should have defaultValue', async () => { await page.goto(url.create); const field = page.locator('#field-readOnly__0__text'); await expect(field) .toHaveValue('defaultValue'); }); test('should render RowLabel using a function', async () => { const label = 'custom row label as function'; await page.goto(url.create); await page.locator('#field-rowLabelAsFunction >> .array-field__add-button-wrap >> button').click(); await page.locator('#field-rowLabelAsFunction__0__title').fill(label); await wait(100); const customRowLabel = await page.locator('#rowLabelAsFunction-row-0 >> .row-label'); await expect(customRowLabel).toContainText(label); }); test('should render RowLabel using a component', async () => { const label = 'custom row label as component'; await page.goto(url.create); await page.locator('#field-rowLabelAsComponent >> .array-field__add-button-wrap >> button').click(); await page.locator('#field-rowLabelAsComponent__0__title').fill(label); await wait(100); const customRowLabel = await page.locator('#rowLabelAsComponent-row-0 >> .row-label :text("custom row label")'); await expect(customRowLabel).toHaveCSS('text-transform', 'uppercase'); }); }); describe('fields - tabs', () => { let url: AdminUrlUtil; beforeAll(() => { url = new AdminUrlUtil(serverURL, tabsSlug); }); test('should fill and retain a new value within a tab while switching tabs', async () => { const textInRowValue = 'hello'; const numberInRowValue = '23'; await page.goto(url.create); await page.locator('.tabs-field__tab-button:has-text("Tab with Row")').click(); await page.locator('#field-textInRow').fill(textInRowValue); await page.locator('#field-numberInRow').fill(numberInRowValue); await wait(300); await page.locator('.tabs-field__tab-button:has-text("Tab with Array")').click(); await page.locator('.tabs-field__tab-button:has-text("Tab with Row")').click(); await wait(100); await expect(page.locator('#field-textInRow')).toHaveValue(textInRowValue); await expect(page.locator('#field-numberInRow')).toHaveValue(numberInRowValue); }); test('should retain updated values within tabs while switching between tabs', async () => { const textInRowValue = 'new value'; await page.goto(url.list); await page.locator('.cell-id a').click(); // Go to Row tab, update the value await page.locator('.tabs-field__tab-button:has-text("Tab with Row")').click(); await page.locator('#field-textInRow').fill(textInRowValue); await wait(250); // Go to Array tab, then back to Row. Make sure new value is still there await page.locator('.tabs-field__tab-button:has-text("Tab with Array")').click(); await page.locator('.tabs-field__tab-button:has-text("Tab with Row")').click(); await expect(page.locator('#field-textInRow')).toHaveValue(textInRowValue); // Go to array tab, save the doc await page.locator('.tabs-field__tab-button:has-text("Tab with Array")').click(); await page.click('#action-save', { delay: 100 }); await wait(250); // Go back to row tab, make sure the new value is still present await page.locator('.tabs-field__tab-button:has-text("Tab with Row")').click(); await expect(page.locator('#field-textInRow')).toHaveValue(textInRowValue); }); }); describe('fields - richText', () => { async function navigateToRichTextFields() { const url: AdminUrlUtil = new AdminUrlUtil(serverURL, 'rich-text-fields'); await page.goto(url.list); await page.locator('.row-1 .cell-id').click(); } describe('toolbar', () => { test('should create new url link', async () => { await navigateToRichTextFields(); // Open link popup await page.locator('.rich-text__toolbar button:not([disabled]) .link').click(); const editLinkModal = page.locator('.rich-text-link-edit-modal__template'); await expect(editLinkModal).toBeVisible(); // Fill values and click Confirm await editLinkModal.locator('#field-text').fill('link text'); await editLinkModal.locator('label[for="field-linkType-custom"]').click(); await editLinkModal.locator('#field-url').fill('https://payloadcms.com'); await wait(200); await editLinkModal.locator('button[type="submit"]').click(); // Remove link from editor body await page.locator('span >> text="link text"').click(); const popup = page.locator('.popup--active .rich-text-link__popup'); await expect(popup.locator('.rich-text-link__link-label')).toBeVisible(); await popup.locator('.rich-text-link__link-close').click(); await expect(page.locator('span >> text="link text"')).toHaveCount(0); }); test('should not create new url link when read only', async () => { await navigateToRichTextFields(); // Attempt to open link popup const modalTrigger = page.locator('.rich-text--read-only .rich-text__toolbar button .link'); await expect(modalTrigger).toBeDisabled(); }); }); describe('editor', () => { test('should populate url link', async () => { navigateToRichTextFields(); // Open link popup await page.locator('#field-richText span >> text="render links"').click(); const popup = page.locator('.popup--active .rich-text-link__popup'); await expect(popup).toBeVisible(); await expect(popup.locator('a')).toHaveAttribute('href', 'https://payloadcms.com'); // Open link edit modal await popup.locator('.rich-text-link__link-edit').click(); const editLinkModal = page.locator('.rich-text-link-edit-modal__template'); await expect(editLinkModal).toBeVisible(); // Close link edit modal await editLinkModal.locator('button[type="submit"]').click(); await expect(editLinkModal).not.toBeVisible(); }); test('should populate relationship link', async () => { navigateToRichTextFields(); // Open link popup await page.locator('#field-richText span >> text="link to relationships"').click(); const popup = page.locator('.popup--active .rich-text-link__popup'); await expect(popup).toBeVisible(); await expect(popup.locator('a')).toHaveAttribute('href', /\/admin\/collections\/array-fields\/.*/); // Open link edit modal await popup.locator('.rich-text-link__link-edit').click(); const editLinkModal = page.locator('.rich-text-link-edit-modal__template'); await expect(editLinkModal).toBeVisible(); // Close link edit modal await editLinkModal.locator('button[type="submit"]').click(); await expect(editLinkModal).not.toBeVisible(); }); }); describe('relationship', () => { let url: AdminUrlUtil; beforeAll(() => { url = new AdminUrlUtil(serverURL, relationshipFieldsSlug); }); test('should create inline relationship within field with many relations', async () => { await page.goto(url.create); const button = page.locator('#relationship-add-new .relationship-add-new__add-button'); await button.click(); await page.locator('.relationship-add-new__relation-button--text-fields').click(); const textField = page.locator('#field-text'); const textValue = 'hello'; await textField.fill(textValue); await page.locator('#relationship-add-modal-depth-1 #action-save').click(); await expect(page.locator('.Toastify')).toContainText('successfully'); await expect(page.locator('#field-relationship .rs__single-value')).toContainText(textValue); await page.locator('#action-save').click(); await expect(page.locator('.Toastify')).toContainText('successfully'); }); test('should create nested inline relationships', async () => { await page.goto(url.create); // Open first modal await page.locator('#relationToSelf-add-new .relationship-add-new__add-button').click(); // Fill first modal's required relationship field await page.locator('#relationToSelf-add-modal-depth-1 #field-relationship').click(); await page.locator('#relationToSelf-add-modal-depth-1 .rs__option:has-text("Seeded text document")').click(); // Open second modal await page.locator('#relationToSelf-add-modal-depth-1 #relationToSelf-add-new button').click(); // Fill second modal's required relationship field await page.locator('#relationToSelf-add-modal-depth-2 #field-relationship').click(); await page.locator('#relationToSelf-add-modal-depth-2 .rs__option:has-text("Seeded text document")').click(); // Save second modal await page.locator('#relationToSelf-add-modal-depth-2 #action-save').click(); // Assert that the first modal is still open // and that the Relation to Self field now has a value in its input await expect(page.locator('#relationToSelf-add-modal-depth-1 #field-relationToSelf .rs__single-value')).toBeVisible(); // Save first modal await page.locator('#relationToSelf-add-modal-depth-1 #action-save').click(); await wait(200); // Expect the original field to have a value filled await expect(page.locator('#field-relationToSelf .rs__single-value')).toBeVisible(); // Fill the required field await page.locator('#field-relationship').click(); await page.locator('.rs__option:has-text("Seeded text document")').click(); await page.locator('#action-save').click(); await expect(page.locator('.Toastify')).toContainText('successfully'); }); }); }); });