Compare commits

...

1 Commits

Author SHA1 Message Date
Jacob Fletcher
7d1c4d5dc4 scaffolds test 2025-02-17 17:26:27 -05:00
7 changed files with 417 additions and 124 deletions

View File

@@ -1,6 +1,10 @@
import type { Page } from '@playwright/test'
import type { BrowserContext, Page } from '@playwright/test'
import { expect, test } from '@playwright/test'
import { addBlock } from 'helpers/e2e/addBlock.js'
import { openBlockDrawer } from 'helpers/e2e/openBlockDrawer.js'
import { removeAllBlocks } from 'helpers/e2e/removeAllBlocks.js'
import { reorderBlocks } from 'helpers/e2e/reorderBlocks.js'
import path from 'path'
import { fileURLToPath } from 'url'
@@ -8,6 +12,7 @@ import {
ensureCompilationIsDone,
initPageConsoleErrorCatch,
saveDocAndAssert,
throttleTest,
} from '../../../helpers.js'
import { AdminUrlUtil } from '../../../helpers/adminUrlUtil.js'
import { initPayloadE2ENoConfig } from '../../../helpers/initPayloadE2ENoConfig.js'
@@ -24,6 +29,8 @@ const { beforeAll, beforeEach, describe } = test
let client: RESTClient
let page: Page
let serverURL: string
let context: BrowserContext
// If we want to make this run in parallel: test.describe.configure({ mode: 'parallel' })
describe('Block fields', () => {
@@ -35,7 +42,7 @@ describe('Block fields', () => {
dirname,
}))
const context = await browser.newContext()
context = await browser.newContext()
page = await context.newPage()
initPageConsoleErrorCatch(page)
@@ -64,19 +71,8 @@ describe('Block fields', () => {
test('should open blocks drawer and select first block', async () => {
await page.goto(url.create)
const addButton = page.locator('#field-blocks > .blocks-field__drawer-toggler')
await expect(addButton).toContainText('Add Block')
await addButton.click()
const blocksDrawer = page.locator('[id^=drawer_1_blocks-drawer-]')
await expect(blocksDrawer).toBeVisible()
// select the first block in the drawer
const firstBlockSelector = blocksDrawer
.locator('.blocks-drawer__blocks .blocks-drawer__block')
.first()
await expect(firstBlockSelector).toContainText('Content')
await firstBlockSelector.click()
await addBlock({ page, fieldName: 'blocks', fieldLabel: 'Blocks', blockLabel: 'Content' })
// ensure the block was appended to the rows
const addedRow = page.locator('#field-blocks .blocks-field__row').last()
@@ -88,17 +84,14 @@ describe('Block fields', () => {
test('should reset search state in blocks drawer on re-open', async () => {
await page.goto(url.create)
const addButton = page.locator('#field-blocks > .blocks-field__drawer-toggler')
await expect(addButton).toContainText('Add Block')
await addButton.click()
const blocksDrawer = page.locator('[id^=drawer_1_blocks-drawer-]')
await expect(blocksDrawer).toBeVisible()
const blocksDrawer = await openBlockDrawer({ page, fieldName: 'blocks', fieldLabel: 'Blocks' })
const searchInput = page.locator('.block-search__input')
await searchInput.fill('Number')
// select the first block in the drawer
const firstBlockSelector = blocksDrawer
.locator('.blocks-drawer__blocks .blocks-drawer__block')
.first()
@@ -106,6 +99,7 @@ describe('Block fields', () => {
await expect(firstBlockSelector).toContainText('Number')
await page.locator('.drawer__header__close').click()
const addButton = page.locator('#field-blocks > .blocks-field__drawer-toggler')
await addButton.click()
await expect(blocksDrawer).toBeVisible()
@@ -131,6 +125,7 @@ describe('Block fields', () => {
const firstBlockSelector = blocksDrawer
.locator('.blocks-drawer__blocks .blocks-drawer__block')
.first()
await expect(firstBlockSelector).toContainText('Content')
await firstBlockSelector.click()
@@ -179,19 +174,12 @@ describe('Block fields', () => {
await page.goto(url.create)
await expect(page.locator('#field-i18nBlocks .blocks-field__header')).toContainText('Block en')
const addButton = page.locator('#field-i18nBlocks > .blocks-field__drawer-toggler')
await expect(addButton).toContainText('Add Block en')
await addButton.click()
const blocksDrawer = page.locator('[id^=drawer_1_blocks-drawer-]')
await expect(blocksDrawer).toBeVisible()
// select the first block in the drawer
const firstBlockSelector = blocksDrawer
.locator('.blocks-drawer__blocks .blocks-drawer__block')
.first()
await expect(firstBlockSelector).toContainText('Text en')
await firstBlockSelector.click()
await addBlock({
page,
fieldName: 'i18nBlocks',
fieldLabel: 'I18n Blocks',
blockLabel: 'Text en',
})
// ensure the block was appended to the rows
const firstRow = page.locator('#field-i18nBlocks .blocks-field__row').first()
@@ -201,15 +189,8 @@ describe('Block fields', () => {
test('should render custom block row label', async () => {
await page.goto(url.create)
const addButton = page.locator('#field-blocks > .blocks-field__drawer-toggler')
await addButton.click()
const blocksDrawer = page.locator('[id^=drawer_1_blocks-drawer-]')
await blocksDrawer
.locator('.blocks-drawer__block .thumbnail-card__label', {
hasText: 'Content',
})
.click()
await addBlock({ page, fieldName: 'blocks', fieldLabel: 'Blocks', blockLabel: 'Content' })
await expect(
page.locator('#field-blocks .blocks-field__row .blocks-field__block-header', {
@@ -221,20 +202,18 @@ describe('Block fields', () => {
test('should add different blocks with similar field configs', async () => {
await page.goto(url.create)
async function addBlock(name: 'Block A' | 'Block B') {
await page
.locator('#field-blocksWithSimilarConfigs')
.getByRole('button', { name: 'Add Blocks With Similar Config' })
.click()
await page.getByRole('button', { name }).click()
}
await addBlock('Block A')
await addBlock({
page,
fieldName: 'blocksWithSimilarConfigs',
fieldLabel: 'Blocks With Similar Config',
blockLabel: 'Block A',
})
await page
.locator('#blocksWithSimilarConfigs-row-0')
.getByRole('button', { name: 'Add Item' })
.click()
await page
.locator('input[name="blocksWithSimilarConfigs.0.items.0.title"]')
.fill('items>0>title')
@@ -243,12 +222,18 @@ describe('Block fields', () => {
page.locator('input[name="blocksWithSimilarConfigs.0.items.0.title"]'),
).toHaveValue('items>0>title')
await addBlock('Block B')
await addBlock({
page,
fieldName: 'blocksWithSimilarConfigs',
fieldLabel: 'Blocks With Similar Config',
blockLabel: 'Block B',
})
await page
.locator('#blocksWithSimilarConfigs-row-1')
.getByRole('button', { name: 'Add Item' })
.click()
await page
.locator('input[name="blocksWithSimilarConfigs.1.items.0.title2"]')
.fill('items>1>title')
@@ -267,19 +252,12 @@ describe('Block fields', () => {
test('should fail min rows validation when rows are present', async () => {
await page.goto(url.create)
await page
.locator('#field-blocksWithMinRows')
.getByRole('button', { name: 'Add Blocks With Min Row' })
.click()
const blocksDrawer = page.locator('[id^=drawer_1_blocks-drawer-]')
await expect(blocksDrawer).toBeVisible()
const firstBlockSelector = blocksDrawer
.locator('.blocks-drawer__blocks .blocks-drawer__block')
.first()
await firstBlockSelector.click()
await addBlock({
page,
fieldName: 'blocksWithMinRows',
fieldLabel: 'Blocks With Min Rows',
blockLabel: 'Block',
})
const firstRow = page.locator('input[name="blocksWithMinRows.0.blockTitle"]')
await expect(firstRow).toBeVisible()
@@ -326,6 +304,7 @@ describe('Block fields', () => {
.locator('.custom-blocks-field-management')
.getByRole('button', { name: 'Add Block 2' })
.click()
await expect(
page.locator('#field-customBlocks input[name="customBlocks.1.block2Title"]'),
).toHaveValue('Block 2: Prefilled Title')
@@ -334,6 +313,7 @@ describe('Block fields', () => {
.locator('.custom-blocks-field-management')
.getByRole('button', { name: 'Replace Block 2' })
.click()
await expect(
page.locator('#field-customBlocks input[name="customBlocks.1.block1Title"]'),
).toHaveValue('REPLACED BLOCK')
@@ -342,18 +322,68 @@ describe('Block fields', () => {
})
describe('sortable blocks', () => {
test('should have disabled admin sorting', async () => {
test('should not render sort controls when sorting is disabled', 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 () => {
test('should not render drag handle when sorting is disabled', 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)
})
test('should quickly sort blocks on slow networks without losing data', async () => {
await page.goto(url.create)
await removeAllBlocks({ page, fieldName: 'blocks' })
await addBlock({
page,
fieldName: 'blocks',
blockLabelSingular: 'Number',
fieldLabelSingular: 'Block',
})
await addBlock({
page,
fieldName: 'blocks',
blockLabelSingular: 'Content',
fieldLabelSingular: 'Block',
})
// fill in the first block with a value
await page.locator('input[name="blocks.0.number"]').fill('1')
await page.locator('input[name="blocks.1.text"]').fill('Content')
// only throttle test after initial load to avoid timeouts
const cdpSession = await throttleTest({
page,
context,
delay: 'Slow 3G',
})
await reorderBlocks({
page,
fieldName: 'blocks',
fromBlockIndex: 1,
toBlockIndex: 0,
})
await expect(page.locator('input[name="blocks.0.text"]')).toHaveValue('Content')
await expect(page.locator('input[name="blocks.1.number"]')).toHaveValue('1')
await cdpSession.send('Network.emulateNetworkConditions', {
offline: false,
latency: 0,
downloadThroughput: -1,
uploadThroughput: -1,
})
await cdpSession.detach()
})
})
})

View File

@@ -6,60 +6,6 @@
* and re-run `payload generate:types` to regenerate this file.
*/
/**
* Supported timezones in IANA format.
*
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "supportedTimezones".
*/
export type SupportedTimezones =
| 'Pacific/Midway'
| 'Pacific/Niue'
| 'Pacific/Honolulu'
| 'Pacific/Rarotonga'
| 'America/Anchorage'
| 'Pacific/Gambier'
| 'America/Los_Angeles'
| 'America/Tijuana'
| 'America/Denver'
| 'America/Phoenix'
| 'America/Chicago'
| 'America/Guatemala'
| 'America/New_York'
| 'America/Bogota'
| 'America/Caracas'
| 'America/Santiago'
| 'America/Buenos_Aires'
| 'America/Sao_Paulo'
| 'Atlantic/South_Georgia'
| 'Atlantic/Azores'
| 'Atlantic/Cape_Verde'
| 'Europe/London'
| 'Europe/Berlin'
| 'Africa/Lagos'
| 'Europe/Athens'
| 'Africa/Cairo'
| 'Europe/Moscow'
| 'Asia/Riyadh'
| 'Asia/Dubai'
| 'Asia/Baku'
| 'Asia/Karachi'
| 'Asia/Tashkent'
| 'Asia/Calcutta'
| 'Asia/Dhaka'
| 'Asia/Almaty'
| 'Asia/Jakarta'
| 'Asia/Bangkok'
| 'Asia/Shanghai'
| 'Asia/Singapore'
| 'Asia/Tokyo'
| 'Asia/Seoul'
| 'Australia/Sydney'
| 'Pacific/Guam'
| 'Pacific/Noumea'
| 'Pacific/Auckland'
| 'Pacific/Fiji'
| 'America/Monterrey';
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "BlockColumns".
@@ -1136,16 +1082,163 @@ export interface DateField {
dayAndTime?: string | null;
monthOnly?: string | null;
defaultWithTimezone?: string | null;
defaultWithTimezone_tz?: SupportedTimezones;
defaultWithTimezone_tz?:
| (
| 'Pacific/Midway'
| 'Pacific/Niue'
| 'Pacific/Honolulu'
| 'Pacific/Rarotonga'
| 'America/Anchorage'
| 'Pacific/Gambier'
| 'America/Los_Angeles'
| 'America/Tijuana'
| 'America/Denver'
| 'America/Phoenix'
| 'America/Chicago'
| 'America/Guatemala'
| 'America/New_York'
| 'America/Bogota'
| 'America/Caracas'
| 'America/Santiago'
| 'America/Buenos_Aires'
| 'America/Sao_Paulo'
| 'Atlantic/South_Georgia'
| 'Atlantic/Azores'
| 'Atlantic/Cape_Verde'
| 'Europe/London'
| 'Europe/Berlin'
| 'Africa/Lagos'
| 'Europe/Athens'
| 'Africa/Cairo'
| 'Europe/Moscow'
| 'Asia/Riyadh'
| 'Asia/Dubai'
| 'Asia/Baku'
| 'Asia/Karachi'
| 'Asia/Tashkent'
| 'Asia/Calcutta'
| 'Asia/Dhaka'
| 'Asia/Almaty'
| 'Asia/Jakarta'
| 'Asia/Bangkok'
| 'Asia/Shanghai'
| 'Asia/Singapore'
| 'Asia/Tokyo'
| 'Asia/Seoul'
| 'Australia/Sydney'
| 'Pacific/Guam'
| 'Pacific/Noumea'
| 'Pacific/Auckland'
| 'Pacific/Fiji'
| 'America/Monterrey'
)
| null;
/**
* This date here should be required.
*/
dayAndTimeWithTimezone: string;
dayAndTimeWithTimezone_tz: SupportedTimezones;
dayAndTimeWithTimezone_tz:
| 'Pacific/Midway'
| 'Pacific/Niue'
| 'Pacific/Honolulu'
| 'Pacific/Rarotonga'
| 'America/Anchorage'
| 'Pacific/Gambier'
| 'America/Los_Angeles'
| 'America/Tijuana'
| 'America/Denver'
| 'America/Phoenix'
| 'America/Chicago'
| 'America/Guatemala'
| 'America/New_York'
| 'America/Bogota'
| 'America/Caracas'
| 'America/Santiago'
| 'America/Buenos_Aires'
| 'America/Sao_Paulo'
| 'Atlantic/South_Georgia'
| 'Atlantic/Azores'
| 'Atlantic/Cape_Verde'
| 'Europe/London'
| 'Europe/Berlin'
| 'Africa/Lagos'
| 'Europe/Athens'
| 'Africa/Cairo'
| 'Europe/Moscow'
| 'Asia/Riyadh'
| 'Asia/Dubai'
| 'Asia/Baku'
| 'Asia/Karachi'
| 'Asia/Tashkent'
| 'Asia/Calcutta'
| 'Asia/Dhaka'
| 'Asia/Almaty'
| 'Asia/Jakarta'
| 'Asia/Bangkok'
| 'Asia/Shanghai'
| 'Asia/Singapore'
| 'Asia/Tokyo'
| 'Asia/Seoul'
| 'Australia/Sydney'
| 'Pacific/Guam'
| 'Pacific/Noumea'
| 'Pacific/Auckland'
| 'Pacific/Fiji'
| 'America/Monterrey';
timezoneBlocks?:
| {
dayAndTime?: string | null;
dayAndTime_tz?: SupportedTimezones;
dayAndTime_tz?:
| (
| 'Pacific/Midway'
| 'Pacific/Niue'
| 'Pacific/Honolulu'
| 'Pacific/Rarotonga'
| 'America/Anchorage'
| 'Pacific/Gambier'
| 'America/Los_Angeles'
| 'America/Tijuana'
| 'America/Denver'
| 'America/Phoenix'
| 'America/Chicago'
| 'America/Guatemala'
| 'America/New_York'
| 'America/Bogota'
| 'America/Caracas'
| 'America/Santiago'
| 'America/Buenos_Aires'
| 'America/Sao_Paulo'
| 'Atlantic/South_Georgia'
| 'Atlantic/Azores'
| 'Atlantic/Cape_Verde'
| 'Europe/London'
| 'Europe/Berlin'
| 'Africa/Lagos'
| 'Europe/Athens'
| 'Africa/Cairo'
| 'Europe/Moscow'
| 'Asia/Riyadh'
| 'Asia/Dubai'
| 'Asia/Baku'
| 'Asia/Karachi'
| 'Asia/Tashkent'
| 'Asia/Calcutta'
| 'Asia/Dhaka'
| 'Asia/Almaty'
| 'Asia/Jakarta'
| 'Asia/Bangkok'
| 'Asia/Shanghai'
| 'Asia/Singapore'
| 'Asia/Tokyo'
| 'Asia/Seoul'
| 'Australia/Sydney'
| 'Pacific/Guam'
| 'Pacific/Noumea'
| 'Pacific/Auckland'
| 'Pacific/Fiji'
| 'America/Monterrey'
)
| null;
id?: string | null;
blockName?: string | null;
blockType: 'dateBlock';
@@ -1154,7 +1247,57 @@ export interface DateField {
timezoneArray?:
| {
dayAndTime?: string | null;
dayAndTime_tz?: SupportedTimezones;
dayAndTime_tz?:
| (
| 'Pacific/Midway'
| 'Pacific/Niue'
| 'Pacific/Honolulu'
| 'Pacific/Rarotonga'
| 'America/Anchorage'
| 'Pacific/Gambier'
| 'America/Los_Angeles'
| 'America/Tijuana'
| 'America/Denver'
| 'America/Phoenix'
| 'America/Chicago'
| 'America/Guatemala'
| 'America/New_York'
| 'America/Bogota'
| 'America/Caracas'
| 'America/Santiago'
| 'America/Buenos_Aires'
| 'America/Sao_Paulo'
| 'Atlantic/South_Georgia'
| 'Atlantic/Azores'
| 'Atlantic/Cape_Verde'
| 'Europe/London'
| 'Europe/Berlin'
| 'Africa/Lagos'
| 'Europe/Athens'
| 'Africa/Cairo'
| 'Europe/Moscow'
| 'Asia/Riyadh'
| 'Asia/Dubai'
| 'Asia/Baku'
| 'Asia/Karachi'
| 'Asia/Tashkent'
| 'Asia/Calcutta'
| 'Asia/Dhaka'
| 'Asia/Almaty'
| 'Asia/Jakarta'
| 'Asia/Bangkok'
| 'Asia/Shanghai'
| 'Asia/Singapore'
| 'Asia/Tokyo'
| 'Asia/Seoul'
| 'Australia/Sydney'
| 'Pacific/Guam'
| 'Pacific/Noumea'
| 'Pacific/Auckland'
| 'Pacific/Fiji'
| 'America/Monterrey'
)
| null;
id?: string | null;
}[]
| null;

View File

@@ -0,0 +1,29 @@
import type { Page } from '@playwright/test'
import { expect } from '@playwright/test'
import { openBlockDrawer } from './openBlockDrawer.js'
export const addBlock = async ({
page,
fieldName = 'blocks',
blockLabelSingular = 'Block',
fieldLabelSingular = 'Blocks',
}: {
blockLabelSingular: string
fieldLabelSingular: string
fieldName: string
page: Page
}) => {
const blocksDrawer = await openBlockDrawer({ page, fieldName, fieldLabelSingular })
const blockCard = blocksDrawer.locator('.blocks-drawer__block .thumbnail-card__label', {
hasText: blockLabelSingular,
})
await expect(blockCard).toBeVisible()
await page.getByRole('button', { name: blockLabelSingular }).click()
// expect to see the block on the page
}

View File

@@ -0,0 +1,25 @@
import type { Locator, Page } from '@playwright/test'
import { expect } from '@playwright/test'
export const openBlockDrawer = async ({
page,
fieldName = 'blocks',
fieldLabelSingular = 'Block',
}: {
fieldLabelSingular: string
fieldName: string
page: Page
}): Promise<Locator> => {
const blocksDrawer = page.locator('[id^=drawer_1_blocks-drawer-]')
if (!(await blocksDrawer.isVisible())) {
const addButton = page.locator(`#field-${fieldName} > .blocks-field__drawer-toggler`)
await expect(addButton).toContainText(`Add ${fieldLabelSingular}`)
await addButton.click()
}
await expect(blocksDrawer).toBeVisible()
return blocksDrawer
}

View File

@@ -0,0 +1,25 @@
import type { Page } from '@playwright/test'
import { expect } from '@playwright/test'
export const removeAllBlocks = async ({
page,
fieldName = 'blocks',
}: {
fieldName: string
page: Page
}) => {
const blocksField = page.locator(`#field-${fieldName}`)
const blocks = blocksField.locator(`[id^="${fieldName}-row-"]`)
const count = await blocks.count()
expect(count).toBeGreaterThan(0)
for (let i = 0; i < count; i++) {
// delete in reverse order to avoid index issues
const block = blocksField.locator(`[id^="${fieldName}-row-${count - i - 1}"]`)
await block.locator('.array-actions__button').first().click()
await block.locator('.array-actions__action.array-actions__remove').first().click()
}
}

View File

@@ -0,0 +1,41 @@
import type { Page } from '@playwright/test'
import { wait } from 'payload/shared'
import { exactText } from '../../helpers.js'
export const reorderBlocks = async ({
page,
fromBlockIndex = 1,
toBlockIndex = 2,
fieldName = 'blocks',
}: {
fieldName?: string
fromBlockIndex: number
page: Page
toBlockIndex: number
}) => {
const blocksField = page.locator(`#field-${fieldName}`).first()
const fromField = blocksField.locator(`[id^="${fieldName}-row-${fromBlockIndex}"]`)
const fromBoundingBox = await fromField.locator(`.collapsible__drag`).boundingBox()
const toField = blocksField.locator(`[id^="${fieldName}-row-${toBlockIndex}"]`)
const toBoundingBox = await toField.locator(`.collapsible__drag`).boundingBox()
if (!fromBoundingBox || !toBoundingBox) {
return
}
// drag the "from" column to the left of the "to" column
await page.mouse.move(fromBoundingBox.x + 2, fromBoundingBox.y + 2, { steps: 10 })
await page.mouse.down()
await wait(300)
await page.mouse.move(toBoundingBox.x - 2, toBoundingBox.y - 2, { steps: 10 })
await page.mouse.up()
// TODO: This wait makes sure the preferences are actually saved. Just waiting for the UI to update is not enough. We should replace this wait
await wait(1000)
}

View File

@@ -31,7 +31,7 @@
}
],
"paths": {
"@payload-config": ["./test/_community/config.ts"],
"@payload-config": ["./test/fields/config.ts"],
"@payloadcms/live-preview": ["./packages/live-preview/src"],
"@payloadcms/live-preview-react": ["./packages/live-preview-react/src/index.ts"],
"@payloadcms/live-preview-vue": ["./packages/live-preview-vue/src/index.ts"],