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({ 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() }) test('should respect admin.width for Blocks fields inside a row', async () => { await page.goto(url.create) // Target the Blocks field wrappers const left = page.locator('#field-leftColumn') const right = page.locator('#field-rightColumn') await expect(left).toBeVisible() await expect(right).toBeVisible() // 1) CSS variable is applied (via mergeFieldStyles) const leftVar = await left.evaluate((el) => getComputedStyle(el).getPropertyValue('--field-width').trim(), ) const rightVar = await right.evaluate((el) => getComputedStyle(el).getPropertyValue('--field-width').trim(), ) await expect(() => { expect(leftVar).toBe('50%') expect(rightVar).toBe('50%') }).toPass() // Also assert inline style contains the var (robust to other inline styles) await expect(left).toHaveAttribute('style', /--field-width:\s*50%/) await expect(right).toHaveAttribute('style', /--field-width:\s*50%/) // 2) Layout reflects the widths (same row, equal widths) const leftBox = await left.boundingBox() const rightBox = await right.boundingBox() await expect(() => { // Same row expect(Math.round(leftBox.y)).toEqual(Math.round(rightBox.y)) // Equal width (tolerate sub-pixel differences) expect(Math.round(leftBox.width)).toEqual(Math.round(rightBox.width)) }).toPass() }) test('should respect admin.width for array fields inside a row', async () => { await page.goto(url.create) // Target the Array field wrappers const left = page.locator('#field-arrayLeftColumn') const right = page.locator('#field-arrayRightColumn') await expect(left).toBeVisible() await expect(right).toBeVisible() // 1) CSS variable is applied (via mergeFieldStyles) const leftVar = await left.evaluate((el) => getComputedStyle(el).getPropertyValue('--field-width').trim(), ) const rightVar = await right.evaluate((el) => getComputedStyle(el).getPropertyValue('--field-width').trim(), ) await expect(() => { expect(leftVar).toBe('50%') expect(rightVar).toBe('50%') }).toPass() // Also assert inline style contains the var (robust to other inline styles) await expect(left).toHaveAttribute('style', /--field-width:\s*50%/) await expect(right).toHaveAttribute('style', /--field-width:\s*50%/) // 2) Layout reflects the widths (same row, equal widths) const leftBox = await left.boundingBox() const rightBox = await right.boundingBox() await expect(() => { // Same row expect(Math.round(leftBox.y)).toEqual(Math.round(rightBox.y)) // Equal width (tolerate sub-pixel differences) expect(Math.round(leftBox.width)).toEqual(Math.round(rightBox.width)) }).toPass() }) })