ci: split up test suites (#6415)

This commit is contained in:
Alessio Gravili
2024-05-21 17:11:55 -04:00
committed by GitHub
parent 6c952875e8
commit 5c7647f45b
13 changed files with 2323 additions and 1737 deletions

View File

@@ -289,7 +289,8 @@ jobs:
suite:
- _community
- access-control
- admin
- admin__e2e__1
- admin__e2e__2
- auth
- field-error-states
- fields-relationship
@@ -298,7 +299,14 @@ jobs:
- fields__collections__Array
- fields__collections__Relationship
- fields__collections__RichText
- fields__collections__Lexical
- fields__collections__Lexical__e2e__main
- fields__collections__Lexical__e2e__blocks
- fields__collections__Date
- fields__collections__Number
- fields__collections__Point
- fields__collections__Tabs
- fields__collections__Text
- fields__collections__Upload
- live-preview
- localization
- i18n

View File

@@ -2,11 +2,9 @@ import type { Page } from '@playwright/test'
import { expect, test } from '@playwright/test'
import { wait } from 'payload/utilities'
import { mapAsync } from 'payload/utilities'
import qs from 'qs'
import type { Geo, Post } from './payload-types.js'
import type { Config } from './payload-types.js'
import type { Geo, Post } from '../../payload-types.js'
import type { Config } from '../../payload-types.js'
import {
checkBreadcrumb,
@@ -16,13 +14,12 @@ import {
getAdminRoutes,
initPageConsoleErrorCatch,
openDocControls,
openDocDrawer,
openNav,
saveDocAndAssert,
saveDocHotkeyAndAssert,
} from '../helpers.js'
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js'
} from '../../../helpers.js'
import { AdminUrlUtil } from '../../../helpers/adminUrlUtil.js'
import { initPayloadE2ENoConfig } from '../../../helpers/initPayloadE2ENoConfig.js'
import {
customAdminRoutes,
customEditLabel,
@@ -36,7 +33,7 @@ import {
customViewPath,
customViewTitle,
slugPluralLabel,
} from './shared.js'
} from '../../shared.js'
import {
customIdCollectionId,
customViews2CollectionSlug,
@@ -48,7 +45,7 @@ import {
noApiViewCollectionSlug,
noApiViewGlobalSlug,
postsCollectionSlug,
} from './slugs.js'
} from '../../slugs.js'
const { beforeAll, beforeEach, describe } = test
@@ -60,14 +57,15 @@ let payload: PayloadTestSDK<Config>
import path from 'path'
import { fileURLToPath } from 'url'
import type { PayloadTestSDK } from '../helpers/sdk/index.js'
import type { PayloadTestSDK } from '../../../helpers/sdk/index.js'
import { reInitializeDB } from '../helpers/reInitializeDB.js'
import { POLL_TOPASS_TIMEOUT, TEST_TIMEOUT_LONG } from '../playwright.config.js'
import { reInitializeDB } from '../../../helpers/reInitializeDB.js'
import { POLL_TOPASS_TIMEOUT, TEST_TIMEOUT_LONG } from '../../../playwright.config.js'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
const currentFolder = path.dirname(filename)
const dirname = path.resolve(currentFolder, '../../')
describe('admin', () => {
describe('admin1', () => {
let page: Page
let geoUrl: AdminUrlUtil
let postsUrl: AdminUrlUtil
@@ -97,7 +95,7 @@ describe('admin', () => {
initPageConsoleErrorCatch(page)
await reInitializeDB({
serverURL,
snapshotKey: 'adminTests',
snapshotKey: 'adminTests1',
})
await ensureAutoLoginAndCompilationIsDone({ page, serverURL, customAdminRoutes })
@@ -109,7 +107,7 @@ describe('admin', () => {
beforeEach(async () => {
await reInitializeDB({
serverURL,
snapshotKey: 'adminTests',
snapshotKey: 'adminTests1',
})
await ensureAutoLoginAndCompilationIsDone({ page, serverURL, customAdminRoutes })
@@ -834,701 +832,6 @@ describe('admin', () => {
await expect(localeListItem1).toContainText('Spanish (es)')
})
})
describe('custom CSS', () => {
test('should see custom css in admin UI', async () => {
await page.goto(postsUrl.admin)
await page.waitForURL(postsUrl.admin)
await openNav(page)
const navControls = page.locator('#custom-css')
await expect(navControls).toHaveCSS('font-family', 'monospace')
})
})
describe('list view', () => {
const tableRowLocator = 'table > tbody > tr'
beforeEach(async () => {
// delete all posts created by the seed
await deleteAllPosts()
await page.goto(postsUrl.list)
await page.waitForURL((url) => url.toString().startsWith(postsUrl.list))
await expect(page.locator(tableRowLocator)).toBeHidden()
await createPost({ title: 'post1' })
await createPost({ title: 'post2' })
await page.reload()
await expect(page.locator(tableRowLocator)).toHaveCount(2)
})
describe('filtering', () => {
test('should prefill search input from query param', async () => {
await createPost({ title: 'dennis' })
await createPost({ title: 'charlie' })
// prefill search with "a" from the query param
await page.goto(`${postsUrl.list}?search=dennis`)
await page.waitForURL(`${postsUrl.list}?search=dennis`)
// input should be filled out, list should filter
await expect(page.locator('.search-filter__input')).toHaveValue('dennis')
await expect(page.locator(tableRowLocator)).toHaveCount(1)
})
test('should search by id with listSearchableFields', async () => {
const { id } = await createPost()
const url = `${postsUrl.list}?limit=10&page=1&search=${id}`
await page.goto(url)
await page.waitForURL(url)
const tableItems = page.locator(tableRowLocator)
await expect(tableItems).toHaveCount(1)
})
test('should search by id without listSearchableFields', async () => {
const { id } = await createGeo()
const url = `${geoUrl.list}?limit=10&page=1&search=${id}`
await page.goto(url)
await page.waitForURL(url)
const tableItems = page.locator(tableRowLocator)
await expect(tableItems).toHaveCount(1)
})
test('should search by title or description', async () => {
await createPost({
description: 'this is fun',
title: 'find me',
})
await page.locator('.search-filter__input').fill('find me')
await expect(page.locator(tableRowLocator)).toHaveCount(1)
await page.locator('.search-filter__input').fill('this is fun')
await expect(page.locator(tableRowLocator)).toHaveCount(1)
})
test('should toggle columns', async () => {
const columnCountLocator = 'table > thead > tr > th'
await createPost()
await page.locator('.list-controls__toggle-columns').click()
// track the number of columns before manipulating toggling any
const numberOfColumns = await page.locator(columnCountLocator).count()
// wait until the column toggle UI is visible and fully expanded
await expect(page.locator('.column-selector')).toBeVisible()
await expect(page.locator('table > thead > tr > th:nth-child(2)')).toHaveText('ID')
const idButton = page.locator(`.column-selector .column-selector__column`, {
hasText: exactText('ID'),
})
// Remove ID column
await idButton.click()
// wait until .cell-id is not present on the page:
await page.locator('.cell-id').waitFor({ state: 'detached' })
await expect(page.locator(columnCountLocator)).toHaveCount(numberOfColumns - 1)
await expect(page.locator('table > thead > tr > th:nth-child(2)')).toHaveText('Number')
// Add back ID column
await idButton.click()
await expect(page.locator('.cell-id').first()).toBeVisible()
await expect(page.locator(columnCountLocator)).toHaveCount(numberOfColumns)
await expect(page.locator('table > thead > tr > th:nth-child(2)')).toHaveText('ID')
})
test('should link second cell', async () => {
const { id } = await createPost()
await page.reload()
const linkCell = page.locator(`${tableRowLocator} td`).nth(1).locator('a')
await expect(linkCell).toHaveAttribute(
'href',
`${adminRoutes.routes.admin}/collections/posts/${id}`,
)
// open the column controls
await page.locator('.list-controls__toggle-columns').click()
// wait until the column toggle UI is visible and fully expanded
await expect(page.locator('.list-controls__columns.rah-static--height-auto')).toBeVisible()
// toggle off the ID column
await page
.locator('.column-selector .column-selector__column', {
hasText: exactText('ID'),
})
.click()
// wait until .cell-id is not present on the page:
await page.locator('.cell-id').waitFor({ state: 'detached' })
// recheck that the 2nd cell is still a link
await expect(linkCell).toHaveAttribute(
'href',
`${adminRoutes.routes.admin}/collections/posts/${id}`,
)
})
test('should filter rows', async () => {
// open the column controls
await page.locator('.list-controls__toggle-columns').click()
// wait until the column toggle UI is visible and fully expanded
await expect(page.locator('.column-selector')).toBeVisible()
await expect(page.locator('table > thead > tr > th:nth-child(2)')).toHaveText('ID')
// ensure the ID column is active
const idButton = page.locator('.column-selector .column-selector__column', {
hasText: exactText('ID'),
})
const id = (await page.locator('.cell-id').first().innerText()).replace('ID: ', '')
const buttonClasses = await idButton.getAttribute('class')
if (buttonClasses && !buttonClasses.includes('column-selector__column--active')) {
await idButton.click()
await expect(page.locator(tableRowLocator).first().locator('.cell-id')).toBeVisible()
}
await expect(page.locator(tableRowLocator)).toHaveCount(2)
await page.locator('.list-controls__toggle-where').click()
// wait until the filter UI is visible and fully expanded
await expect(page.locator('.list-controls__where.rah-static--height-auto')).toBeVisible()
await page.locator('.where-builder__add-first-filter').click()
const operatorField = page.locator('.condition__operator')
const valueField = page.locator('.condition__value input')
await operatorField.click()
const dropdownOptions = operatorField.locator('.rs__option')
await dropdownOptions.locator('text=equals').click()
await valueField.fill(id)
await expect(page.locator(tableRowLocator)).toHaveCount(1)
const firstId = await page.locator(tableRowLocator).first().locator('.cell-id').innerText()
expect(firstId).toEqual(`ID: ${id}`)
// Remove filter
await page.locator('.condition__actions-remove').click()
await expect(page.locator(tableRowLocator)).toHaveCount(2)
})
test('should reset filter value and operator on field update', async () => {
const id = (await page.locator('.cell-id').first().innerText()).replace('ID: ', '')
// open the column controls
await page.locator('.list-controls__toggle-columns').click()
await page.locator('.list-controls__toggle-where').click()
await page.waitForSelector('.list-controls__where.rah-static--height-auto')
await page.locator('.where-builder__add-first-filter').click()
const operatorField = page.locator('.condition__operator')
await operatorField.click()
const dropdownOperatorOptions = operatorField.locator('.rs__option')
await dropdownOperatorOptions.locator('text=equals').click()
// execute filter (where ID equals id value)
const valueField = page.locator('.condition__value > input')
await valueField.fill(id)
const filterField = page.locator('.condition__field')
await filterField.click()
// select new filter field of Number
const dropdownFieldOptions = filterField.locator('.rs__option')
await dropdownFieldOptions.locator('text=Number').click()
// expect operator & value field to reset (be empty)
await expect(operatorField.locator('.rs__placeholder')).toContainText('Select a value')
await expect(valueField).toHaveValue('')
})
test('should accept where query from valid URL where parameter', async () => {
// delete all posts created by the seed
await deleteAllPosts()
await page.goto(postsUrl.list)
await expect(page.locator(tableRowLocator)).toBeHidden()
await createPost({ title: 'post1' })
await createPost({ title: 'post2' })
await page.goto(postsUrl.list)
await expect(page.locator(tableRowLocator)).toHaveCount(2)
await page.goto(
`${postsUrl.list}?limit=10&page=1&where[or][0][and][0][title][equals]=post1`,
)
await expect(page.locator('.react-select--single-value').first()).toContainText('Title')
await expect(page.locator(tableRowLocator)).toHaveCount(1)
})
test('should accept transformed where query from invalid URL where parameter', async () => {
// delete all posts created by the seed
await deleteAllPosts()
await page.goto(postsUrl.list)
await expect(page.locator(tableRowLocator)).toBeHidden()
await createPost({ title: 'post1' })
await createPost({ title: 'post2' })
await page.goto(postsUrl.list)
await expect(page.locator(tableRowLocator)).toHaveCount(2)
// [title][equals]=post1 should be getting transformed into a valid where[or][0][and][0][title][equals]=post1
await page.goto(`${postsUrl.list}?limit=10&page=1&where[title][equals]=post1`)
await expect(page.locator('.react-select--single-value').first()).toContainText('Title')
await expect(page.locator(tableRowLocator)).toHaveCount(1)
})
test('should accept where query from complex, valid URL where parameter using the near operator', async () => {
// We have one point collection with the point [5,-5] and one with [7,-7]. This where query should kick out the [5,-5] point
await page.goto(
`${
new AdminUrlUtil(serverURL, 'geo').list
}?limit=10&page=1&where[or][0][and][0][point][near]=6,-7,200000`,
)
await expect(page.getByPlaceholder('Enter a value')).toHaveValue('6,-7,200000')
await expect(page.locator(tableRowLocator)).toHaveCount(1)
})
test('should accept transformed where query from complex, invalid URL where parameter using the near operator', async () => {
// We have one point collection with the point [5,-5] and one with [7,-7]. This where query should kick out the [5,-5] point
await page.goto(
`${
new AdminUrlUtil(serverURL, 'geo').list
}?limit=10&page=1&where[point][near]=6,-7,200000`,
)
await expect(page.getByPlaceholder('Enter a value')).toHaveValue('6,-7,200000')
await expect(page.locator(tableRowLocator)).toHaveCount(1)
})
test('should accept where query from complex, valid URL where parameter using the within operator', async () => {
type Point = [number, number]
const polygon: Point[] = [
[3.5, -3.5], // bottom-left
[3.5, -6.5], // top-left
[6.5, -6.5], // top-right
[6.5, -3.5], // bottom-right
[3.5, -3.5], // back to starting point to close the polygon
]
const whereQueryJSON = {
point: {
within: {
type: 'Polygon',
coordinates: [polygon],
},
},
}
const whereQuery = qs.stringify(
{
...{ where: whereQueryJSON },
},
{
addQueryPrefix: false,
},
)
// We have one point collection with the point [5,-5] and one with [7,-7]. This where query should kick out the [7,-7] point, as it's not within the polygon
await page.goto(`${new AdminUrlUtil(serverURL, 'geo').list}?limit=10&page=1&${whereQuery}`)
await expect(page.getByPlaceholder('Enter a value')).toHaveValue('[object Object]')
await expect(page.locator(tableRowLocator)).toHaveCount(1)
})
})
describe('table columns', () => {
const reorderColumns = async () => {
// open the column controls
await page.locator('.list-controls__toggle-columns').click()
// wait until the column toggle UI is visible and fully expanded
await expect(page.locator('.list-controls__columns.rah-static--height-auto')).toBeVisible()
const numberBoundingBox = await page
.locator(`.column-selector .column-selector__column`, {
hasText: exactText('Number'),
})
.boundingBox()
const idBoundingBox = await page
.locator(`.column-selector .column-selector__column`, {
hasText: exactText('ID'),
})
.boundingBox()
if (!numberBoundingBox || !idBoundingBox) return
// drag the "number" column to the left of the "ID" column
await page.mouse.move(numberBoundingBox.x + 2, numberBoundingBox.y + 2, { steps: 10 })
await page.mouse.down()
await wait(300)
await page.mouse.move(idBoundingBox.x - 2, idBoundingBox.y - 2, { steps: 10 })
await page.mouse.up()
// ensure the "number" column is now first
await expect(
page.locator('.list-controls .column-selector .column-selector__column').first(),
).toHaveText('Number')
await expect(page.locator('table thead tr th').nth(1)).toHaveText('Number')
// 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)
}
test('should drag to reorder columns and save to preferences', async () => {
await createPost()
await reorderColumns()
// reload to ensure the preferred order was stored in the database
await page.reload()
await expect(
page.locator('.list-controls .column-selector .column-selector__column').first(),
).toHaveText('Number')
await expect(page.locator('table thead tr th').nth(1)).toHaveText('Number')
})
test('should render drawer columns in order', async () => {
// Re-order columns like done in the previous test
await createPost()
await reorderColumns()
await page.reload()
await createPost()
await page.goto(postsUrl.create)
await openDocDrawer(page, '.rich-text .list-drawer__toggler')
const listDrawer = page.locator('[id^=list-drawer_1_]')
await expect(listDrawer).toBeVisible()
const collectionSelector = page.locator(
'[id^=list-drawer_1_] .list-drawer__select-collection.react-select',
)
// select the "Post" collection
await collectionSelector.click()
await page
.locator(
'[id^=list-drawer_1_] .list-drawer__select-collection.react-select .rs__option',
{
hasText: exactText('Post'),
},
)
.click()
// open the column controls
const columnSelector = page.locator('[id^=list-drawer_1_] .list-controls__toggle-columns')
await columnSelector.click()
// wait until the column toggle UI is visible and fully expanded
await expect(page.locator('.list-controls__columns.rah-static--height-auto')).toBeVisible()
// ensure that the columns are in the correct order
await expect(
page
.locator(
'[id^=list-drawer_1_] .list-controls .column-selector .column-selector__column',
)
.first(),
).toHaveText('Number')
})
test('should retain preferences when changing drawer collections', async () => {
await page.goto(postsUrl.create)
// Open the drawer
await openDocDrawer(page, '.rich-text .list-drawer__toggler')
const listDrawer = page.locator('[id^=list-drawer_1_]')
await expect(listDrawer).toBeVisible()
const collectionSelector = page.locator(
'[id^=list-drawer_1_] .list-drawer__select-collection.react-select',
)
const columnSelector = page.locator('[id^=list-drawer_1_] .list-controls__toggle-columns')
// open the column controls
await columnSelector.click()
// wait until the column toggle UI is visible and fully expanded
await expect(page.locator('.list-controls__columns.rah-static--height-auto')).toBeVisible()
// deselect the "id" column
await page
.locator(
'[id^=list-drawer_1_] .list-controls .column-selector .column-selector__column',
{
hasText: exactText('ID'),
},
)
.click()
// select the "Post" collection
await collectionSelector.click()
await page
.locator(
'[id^=list-drawer_1_] .list-drawer__select-collection.react-select .rs__option',
{
hasText: exactText('Post'),
},
)
.click()
// deselect the "number" column
await page
.locator(
'[id^=list-drawer_1_] .list-controls .column-selector .column-selector__column',
{
hasText: exactText('Number'),
},
)
.click()
// select the "User" collection again
await collectionSelector.click()
await page
.locator(
'[id^=list-drawer_1_] .list-drawer__select-collection.react-select .rs__option',
{
hasText: exactText('User'),
},
)
.click()
// ensure that the "id" column is still deselected
await expect(
page
.locator(
'[id^=list-drawer_1_] .list-controls .column-selector .column-selector__column',
)
.first(),
).not.toHaveClass('column-selector__column--active')
// select the "Post" collection again
await collectionSelector.click()
await page
.locator(
'[id^=list-drawer_1_] .list-drawer__select-collection.react-select .rs__option',
{
hasText: exactText('Post'),
},
)
.click()
// ensure that the "number" column is still deselected
await expect(
page
.locator(
'[id^=list-drawer_1_] .list-controls .column-selector .column-selector__column',
)
.first(),
).not.toHaveClass('column-selector__column--active')
})
test('should render custom table cell component', async () => {
await createPost()
await page.goto(postsUrl.list)
await expect(
page.locator('table > thead > tr > th', {
hasText: exactText('Demo UI Field'),
}),
).toBeVisible()
})
})
describe('multi-select', () => {
beforeEach(async () => {
// delete all posts created by the seed
await deleteAllPosts()
await createPost()
await createPost()
await createPost()
})
test('should select multiple rows', async () => {
await page.reload()
const selectAll = page.locator('.checkbox-input:has(#select-all)')
await page.locator('.row-1 .cell-_select input').check()
const indeterminateSelectAll = selectAll.locator('.checkbox-input__icon.partial')
expect(indeterminateSelectAll).toBeDefined()
await selectAll.locator('input').click()
const emptySelectAll = selectAll.locator('.checkbox-input__icon:not(.check):not(.partial)')
await expect(emptySelectAll).toHaveCount(0)
await selectAll.locator('input').click()
const checkSelectAll = selectAll.locator('.checkbox-input__icon.check')
expect(checkSelectAll).toBeDefined()
})
test('should delete many', async () => {
await page.goto(postsUrl.list)
await page.waitForURL(postsUrl.list)
// delete should not appear without selection
await expect(page.locator('#confirm-delete')).toHaveCount(0)
// select one row
await page.locator('.row-1 .cell-_select input').check()
// delete button should be present
await expect(page.locator('#confirm-delete')).toHaveCount(1)
await page.locator('.row-2 .cell-_select input').check()
await page.locator('.delete-documents__toggle').click()
await page.locator('#confirm-delete').click()
await expect(page.locator('.cell-_select')).toHaveCount(1)
})
})
describe('pagination', () => {
test('should paginate', async () => {
await deleteAllPosts()
await mapAsync([...Array(11)], async () => {
await createPost()
})
await page.reload()
const pageInfo = page.locator('.collection-list__page-info')
const perPage = page.locator('.per-page')
const paginator = page.locator('.paginator')
const tableItems = page.locator(tableRowLocator)
await expect(tableItems).toHaveCount(10)
await expect(pageInfo).toHaveText('1-10 of 11')
await expect(perPage).toContainText('Per Page: 10')
// Forward one page and back using numbers
await paginator.locator('button').nth(1).click()
await expect.poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT }).toContain('page=2')
await expect(tableItems).toHaveCount(1)
await paginator.locator('button').nth(0).click()
await expect.poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT }).toContain('page=1')
await expect(tableItems).toHaveCount(10)
})
})
// TODO: Troubleshoot flaky suite
describe('sorting', () => {
beforeEach(async () => {
// delete all posts created by the seed
await deleteAllPosts()
await createPost({ number: 1 })
await createPost({ number: 2 })
})
test('should sort', async () => {
await page.reload()
const upChevron = page.locator('#heading-number .sort-column__asc')
const downChevron = page.locator('#heading-number .sort-column__desc')
await upChevron.click()
await page.waitForURL(/sort=number/)
await expect(page.locator('.row-1 .cell-number')).toHaveText('1')
await expect(page.locator('.row-2 .cell-number')).toHaveText('2')
await downChevron.click()
await page.waitForURL(/sort=-number/)
await expect(page.locator('.row-1 .cell-number')).toHaveText('2')
await expect(page.locator('.row-2 .cell-number')).toHaveText('1')
})
})
describe('i18n', () => {
test('should display translated collections and globals config options', async () => {
await page.goto(postsUrl.list)
// collection label
await expect(page.locator('#nav-posts')).toContainText('Posts')
// global label
await expect(page.locator('#nav-global-global')).toContainText('Global')
// view description
await expect(page.locator('.view-description')).toContainText('Description')
})
test('should display translated field titles', async () => {
await createPost()
// column controls
await page.locator('.list-controls__toggle-columns').click()
await expect(
page.locator('.column-selector__column', {
hasText: exactText('Title'),
}),
).toHaveText('Title')
// filters
await page.locator('.list-controls__toggle-where').click()
await page.locator('.where-builder__add-first-filter').click()
await page.locator('.condition__field .rs__control').click()
const options = page.locator('.rs__option')
await expect(options.locator('text=Title')).toHaveText('Title')
// list columns
await expect(page.locator('#heading-title .sort-column__label')).toHaveText('Title')
await expect(page.locator('.search-filter input')).toHaveAttribute('placeholder', /(Title)/)
})
test('should use fallback language on field titles', async () => {
// change language German
await page.goto(postsUrl.account)
await page.locator('.payload-settings__language .react-select').click()
const languageSelect = page.locator('.rs__option')
// text field does not have a 'de' label
await languageSelect.locator('text=Deutsch').click()
await page.goto(postsUrl.list)
await page.locator('.list-controls__toggle-columns').click()
// expecting the label to fall back to english as default fallbackLng
await expect(
page.locator('.column-selector__column', {
hasText: exactText('Title'),
}),
).toHaveText('Title')
})
})
})
describe('field descriptions', () => {
test('should render static field description', async () => {
await page.goto(postsUrl.create)
await expect(page.locator('.field-description-descriptionAsString')).toContainText(
'Static field description.',
)
})
test('should render functional field description', async () => {
await page.goto(postsUrl.create)
await page.locator('#field-descriptionAsFunction').fill('functional')
await expect(page.locator('.field-description-descriptionAsFunction')).toContainText(
'Function description',
)
})
test('should render component field description', async () => {
await page.goto(postsUrl.create)
await page.locator('#field-descriptionAsComponent').fill('component')
await expect(page.locator('.field-description-descriptionAsComponent')).toContainText(
'Component description: descriptionAsComponent - component',
)
})
})
})
async function createPost(overrides?: Partial<Post>): Promise<Post> {

View File

@@ -0,0 +1,803 @@
import type { Page } from '@playwright/test'
import { expect, test } from '@playwright/test'
import { wait } from 'payload/utilities'
import { mapAsync } from 'payload/utilities'
import qs from 'qs'
import type { Geo, Post } from '../../payload-types.js'
import type { Config } from '../../payload-types.js'
import {
ensureAutoLoginAndCompilationIsDone,
exactText,
getAdminRoutes,
initPageConsoleErrorCatch,
openDocDrawer,
openNav,
} from '../../../helpers.js'
import { AdminUrlUtil } from '../../../helpers/adminUrlUtil.js'
import { initPayloadE2ENoConfig } from '../../../helpers/initPayloadE2ENoConfig.js'
import { customAdminRoutes } from '../../shared.js'
import { geoCollectionSlug, postsCollectionSlug } from '../../slugs.js'
const { beforeAll, beforeEach, describe } = test
const title = 'Title'
const description = 'Description'
let payload: PayloadTestSDK<Config>
import path from 'path'
import { fileURLToPath } from 'url'
import type { PayloadTestSDK } from '../../../helpers/sdk/index.js'
import { reInitializeDB } from '../../../helpers/reInitializeDB.js'
import { POLL_TOPASS_TIMEOUT, TEST_TIMEOUT_LONG } from '../../../playwright.config.js'
const filename = fileURLToPath(import.meta.url)
const currentFolder = path.dirname(filename)
const dirname = path.resolve(currentFolder, '../../')
describe('admin2', () => {
let page: Page
let geoUrl: AdminUrlUtil
let postsUrl: AdminUrlUtil
let serverURL: string
let adminRoutes: ReturnType<typeof getAdminRoutes>
beforeAll(async ({ browser }, testInfo) => {
const prebuild = Boolean(process.env.CI)
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<Config>({
dirname,
prebuild,
}))
geoUrl = new AdminUrlUtil(serverURL, geoCollectionSlug)
postsUrl = new AdminUrlUtil(serverURL, postsCollectionSlug)
const context = await browser.newContext()
page = await context.newPage()
initPageConsoleErrorCatch(page)
await reInitializeDB({
serverURL,
snapshotKey: 'adminTests2',
})
await ensureAutoLoginAndCompilationIsDone({ page, serverURL, customAdminRoutes })
adminRoutes = getAdminRoutes({ customAdminRoutes })
})
beforeEach(async () => {
await reInitializeDB({
serverURL,
snapshotKey: 'adminTests2',
})
await ensureAutoLoginAndCompilationIsDone({ page, serverURL, customAdminRoutes })
})
describe('custom CSS', () => {
test('should see custom css in admin UI', async () => {
await page.goto(postsUrl.admin)
await page.waitForURL(postsUrl.admin)
await openNav(page)
const navControls = page.locator('#custom-css')
await expect(navControls).toHaveCSS('font-family', 'monospace')
})
})
describe('list view', () => {
const tableRowLocator = 'table > tbody > tr'
beforeEach(async () => {
// delete all posts created by the seed
await deleteAllPosts()
await page.goto(postsUrl.list)
await page.waitForURL((url) => url.toString().startsWith(postsUrl.list))
await expect(page.locator(tableRowLocator)).toBeHidden()
await createPost({ title: 'post1' })
await createPost({ title: 'post2' })
await page.reload()
await expect(page.locator(tableRowLocator)).toHaveCount(2)
})
describe('filtering', () => {
test('should prefill search input from query param', async () => {
await createPost({ title: 'dennis' })
await createPost({ title: 'charlie' })
// prefill search with "a" from the query param
await page.goto(`${postsUrl.list}?search=dennis`)
await page.waitForURL(`${postsUrl.list}?search=dennis`)
// input should be filled out, list should filter
await expect(page.locator('.search-filter__input')).toHaveValue('dennis')
await expect(page.locator(tableRowLocator)).toHaveCount(1)
})
test('should search by id with listSearchableFields', async () => {
const { id } = await createPost()
const url = `${postsUrl.list}?limit=10&page=1&search=${id}`
await page.goto(url)
await page.waitForURL(url)
const tableItems = page.locator(tableRowLocator)
await expect(tableItems).toHaveCount(1)
})
test('should search by id without listSearchableFields', async () => {
const { id } = await createGeo()
const url = `${geoUrl.list}?limit=10&page=1&search=${id}`
await page.goto(url)
await page.waitForURL(url)
const tableItems = page.locator(tableRowLocator)
await expect(tableItems).toHaveCount(1)
})
test('should search by title or description', async () => {
await createPost({
description: 'this is fun',
title: 'find me',
})
await page.locator('.search-filter__input').fill('find me')
await expect(page.locator(tableRowLocator)).toHaveCount(1)
await page.locator('.search-filter__input').fill('this is fun')
await expect(page.locator(tableRowLocator)).toHaveCount(1)
})
test('should toggle columns', async () => {
const columnCountLocator = 'table > thead > tr > th'
await createPost()
await page.locator('.list-controls__toggle-columns').click()
// track the number of columns before manipulating toggling any
const numberOfColumns = await page.locator(columnCountLocator).count()
// wait until the column toggle UI is visible and fully expanded
await expect(page.locator('.column-selector')).toBeVisible()
await expect(page.locator('table > thead > tr > th:nth-child(2)')).toHaveText('ID')
const idButton = page.locator(`.column-selector .column-selector__column`, {
hasText: exactText('ID'),
})
// Remove ID column
await idButton.click()
// wait until .cell-id is not present on the page:
await page.locator('.cell-id').waitFor({ state: 'detached' })
await expect(page.locator(columnCountLocator)).toHaveCount(numberOfColumns - 1)
await expect(page.locator('table > thead > tr > th:nth-child(2)')).toHaveText('Number')
// Add back ID column
await idButton.click()
await expect(page.locator('.cell-id').first()).toBeVisible()
await expect(page.locator(columnCountLocator)).toHaveCount(numberOfColumns)
await expect(page.locator('table > thead > tr > th:nth-child(2)')).toHaveText('ID')
})
test('should link second cell', async () => {
const { id } = await createPost()
await page.reload()
const linkCell = page.locator(`${tableRowLocator} td`).nth(1).locator('a')
await expect(linkCell).toHaveAttribute(
'href',
`${adminRoutes.routes.admin}/collections/posts/${id}`,
)
// open the column controls
await page.locator('.list-controls__toggle-columns').click()
// wait until the column toggle UI is visible and fully expanded
await expect(page.locator('.list-controls__columns.rah-static--height-auto')).toBeVisible()
// toggle off the ID column
await page
.locator('.column-selector .column-selector__column', {
hasText: exactText('ID'),
})
.click()
// wait until .cell-id is not present on the page:
await page.locator('.cell-id').waitFor({ state: 'detached' })
// recheck that the 2nd cell is still a link
await expect(linkCell).toHaveAttribute(
'href',
`${adminRoutes.routes.admin}/collections/posts/${id}`,
)
})
test('should filter rows', async () => {
// open the column controls
await page.locator('.list-controls__toggle-columns').click()
// wait until the column toggle UI is visible and fully expanded
await expect(page.locator('.column-selector')).toBeVisible()
await expect(page.locator('table > thead > tr > th:nth-child(2)')).toHaveText('ID')
// ensure the ID column is active
const idButton = page.locator('.column-selector .column-selector__column', {
hasText: exactText('ID'),
})
const id = (await page.locator('.cell-id').first().innerText()).replace('ID: ', '')
const buttonClasses = await idButton.getAttribute('class')
if (buttonClasses && !buttonClasses.includes('column-selector__column--active')) {
await idButton.click()
await expect(page.locator(tableRowLocator).first().locator('.cell-id')).toBeVisible()
}
await expect(page.locator(tableRowLocator)).toHaveCount(2)
await page.locator('.list-controls__toggle-where').click()
// wait until the filter UI is visible and fully expanded
await expect(page.locator('.list-controls__where.rah-static--height-auto')).toBeVisible()
await page.locator('.where-builder__add-first-filter').click()
const operatorField = page.locator('.condition__operator')
const valueField = page.locator('.condition__value input')
await operatorField.click()
const dropdownOptions = operatorField.locator('.rs__option')
await dropdownOptions.locator('text=equals').click()
await valueField.fill(id)
await expect(page.locator(tableRowLocator)).toHaveCount(1)
const firstId = await page.locator(tableRowLocator).first().locator('.cell-id').innerText()
expect(firstId).toEqual(`ID: ${id}`)
// Remove filter
await page.locator('.condition__actions-remove').click()
await expect(page.locator(tableRowLocator)).toHaveCount(2)
})
test('should reset filter value and operator on field update', async () => {
const id = (await page.locator('.cell-id').first().innerText()).replace('ID: ', '')
// open the column controls
await page.locator('.list-controls__toggle-columns').click()
await page.locator('.list-controls__toggle-where').click()
await page.waitForSelector('.list-controls__where.rah-static--height-auto')
await page.locator('.where-builder__add-first-filter').click()
const operatorField = page.locator('.condition__operator')
await operatorField.click()
const dropdownOperatorOptions = operatorField.locator('.rs__option')
await dropdownOperatorOptions.locator('text=equals').click()
// execute filter (where ID equals id value)
const valueField = page.locator('.condition__value > input')
await valueField.fill(id)
const filterField = page.locator('.condition__field')
await filterField.click()
// select new filter field of Number
const dropdownFieldOptions = filterField.locator('.rs__option')
await dropdownFieldOptions.locator('text=Number').click()
// expect operator & value field to reset (be empty)
await expect(operatorField.locator('.rs__placeholder')).toContainText('Select a value')
await expect(valueField).toHaveValue('')
})
test('should accept where query from valid URL where parameter', async () => {
// delete all posts created by the seed
await deleteAllPosts()
await page.goto(postsUrl.list)
await expect(page.locator(tableRowLocator)).toBeHidden()
await createPost({ title: 'post1' })
await createPost({ title: 'post2' })
await page.goto(postsUrl.list)
await expect(page.locator(tableRowLocator)).toHaveCount(2)
await page.goto(
`${postsUrl.list}?limit=10&page=1&where[or][0][and][0][title][equals]=post1`,
)
await expect(page.locator('.react-select--single-value').first()).toContainText('Title')
await expect(page.locator(tableRowLocator)).toHaveCount(1)
})
test('should accept transformed where query from invalid URL where parameter', async () => {
// delete all posts created by the seed
await deleteAllPosts()
await page.goto(postsUrl.list)
await expect(page.locator(tableRowLocator)).toBeHidden()
await createPost({ title: 'post1' })
await createPost({ title: 'post2' })
await page.goto(postsUrl.list)
await expect(page.locator(tableRowLocator)).toHaveCount(2)
// [title][equals]=post1 should be getting transformed into a valid where[or][0][and][0][title][equals]=post1
await page.goto(`${postsUrl.list}?limit=10&page=1&where[title][equals]=post1`)
await expect(page.locator('.react-select--single-value').first()).toContainText('Title')
await expect(page.locator(tableRowLocator)).toHaveCount(1)
})
test('should accept where query from complex, valid URL where parameter using the near operator', async () => {
// We have one point collection with the point [5,-5] and one with [7,-7]. This where query should kick out the [5,-5] point
await page.goto(
`${
new AdminUrlUtil(serverURL, 'geo').list
}?limit=10&page=1&where[or][0][and][0][point][near]=6,-7,200000`,
)
await expect(page.getByPlaceholder('Enter a value')).toHaveValue('6,-7,200000')
await expect(page.locator(tableRowLocator)).toHaveCount(1)
})
test('should accept transformed where query from complex, invalid URL where parameter using the near operator', async () => {
// We have one point collection with the point [5,-5] and one with [7,-7]. This where query should kick out the [5,-5] point
await page.goto(
`${
new AdminUrlUtil(serverURL, 'geo').list
}?limit=10&page=1&where[point][near]=6,-7,200000`,
)
await expect(page.getByPlaceholder('Enter a value')).toHaveValue('6,-7,200000')
await expect(page.locator(tableRowLocator)).toHaveCount(1)
})
test('should accept where query from complex, valid URL where parameter using the within operator', async () => {
type Point = [number, number]
const polygon: Point[] = [
[3.5, -3.5], // bottom-left
[3.5, -6.5], // top-left
[6.5, -6.5], // top-right
[6.5, -3.5], // bottom-right
[3.5, -3.5], // back to starting point to close the polygon
]
const whereQueryJSON = {
point: {
within: {
type: 'Polygon',
coordinates: [polygon],
},
},
}
const whereQuery = qs.stringify(
{
...{ where: whereQueryJSON },
},
{
addQueryPrefix: false,
},
)
// We have one point collection with the point [5,-5] and one with [7,-7]. This where query should kick out the [7,-7] point, as it's not within the polygon
await page.goto(`${new AdminUrlUtil(serverURL, 'geo').list}?limit=10&page=1&${whereQuery}`)
await expect(page.getByPlaceholder('Enter a value')).toHaveValue('[object Object]')
await expect(page.locator(tableRowLocator)).toHaveCount(1)
})
})
describe('table columns', () => {
const reorderColumns = async () => {
// open the column controls
await page.locator('.list-controls__toggle-columns').click()
// wait until the column toggle UI is visible and fully expanded
await expect(page.locator('.list-controls__columns.rah-static--height-auto')).toBeVisible()
const numberBoundingBox = await page
.locator(`.column-selector .column-selector__column`, {
hasText: exactText('Number'),
})
.boundingBox()
const idBoundingBox = await page
.locator(`.column-selector .column-selector__column`, {
hasText: exactText('ID'),
})
.boundingBox()
if (!numberBoundingBox || !idBoundingBox) return
// drag the "number" column to the left of the "ID" column
await page.mouse.move(numberBoundingBox.x + 2, numberBoundingBox.y + 2, { steps: 10 })
await page.mouse.down()
await wait(300)
await page.mouse.move(idBoundingBox.x - 2, idBoundingBox.y - 2, { steps: 10 })
await page.mouse.up()
// ensure the "number" column is now first
await expect(
page.locator('.list-controls .column-selector .column-selector__column').first(),
).toHaveText('Number')
await expect(page.locator('table thead tr th').nth(1)).toHaveText('Number')
// 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)
}
test('should drag to reorder columns and save to preferences', async () => {
await createPost()
await reorderColumns()
// reload to ensure the preferred order was stored in the database
await page.reload()
await expect(
page.locator('.list-controls .column-selector .column-selector__column').first(),
).toHaveText('Number')
await expect(page.locator('table thead tr th').nth(1)).toHaveText('Number')
})
test('should render drawer columns in order', async () => {
// Re-order columns like done in the previous test
await createPost()
await reorderColumns()
await page.reload()
await createPost()
await page.goto(postsUrl.create)
await openDocDrawer(page, '.rich-text .list-drawer__toggler')
const listDrawer = page.locator('[id^=list-drawer_1_]')
await expect(listDrawer).toBeVisible()
const collectionSelector = page.locator(
'[id^=list-drawer_1_] .list-drawer__select-collection.react-select',
)
// select the "Post" collection
await collectionSelector.click()
await page
.locator(
'[id^=list-drawer_1_] .list-drawer__select-collection.react-select .rs__option',
{
hasText: exactText('Post'),
},
)
.click()
// open the column controls
const columnSelector = page.locator('[id^=list-drawer_1_] .list-controls__toggle-columns')
await columnSelector.click()
// wait until the column toggle UI is visible and fully expanded
await expect(page.locator('.list-controls__columns.rah-static--height-auto')).toBeVisible()
// ensure that the columns are in the correct order
await expect(
page
.locator(
'[id^=list-drawer_1_] .list-controls .column-selector .column-selector__column',
)
.first(),
).toHaveText('Number')
})
test('should retain preferences when changing drawer collections', async () => {
await page.goto(postsUrl.create)
// Open the drawer
await openDocDrawer(page, '.rich-text .list-drawer__toggler')
const listDrawer = page.locator('[id^=list-drawer_1_]')
await expect(listDrawer).toBeVisible()
const collectionSelector = page.locator(
'[id^=list-drawer_1_] .list-drawer__select-collection.react-select',
)
const columnSelector = page.locator('[id^=list-drawer_1_] .list-controls__toggle-columns')
// open the column controls
await columnSelector.click()
// wait until the column toggle UI is visible and fully expanded
await expect(page.locator('.list-controls__columns.rah-static--height-auto')).toBeVisible()
// deselect the "id" column
await page
.locator(
'[id^=list-drawer_1_] .list-controls .column-selector .column-selector__column',
{
hasText: exactText('ID'),
},
)
.click()
// select the "Post" collection
await collectionSelector.click()
await page
.locator(
'[id^=list-drawer_1_] .list-drawer__select-collection.react-select .rs__option',
{
hasText: exactText('Post'),
},
)
.click()
// deselect the "number" column
await page
.locator(
'[id^=list-drawer_1_] .list-controls .column-selector .column-selector__column',
{
hasText: exactText('Number'),
},
)
.click()
// select the "User" collection again
await collectionSelector.click()
await page
.locator(
'[id^=list-drawer_1_] .list-drawer__select-collection.react-select .rs__option',
{
hasText: exactText('User'),
},
)
.click()
// ensure that the "id" column is still deselected
await expect(
page
.locator(
'[id^=list-drawer_1_] .list-controls .column-selector .column-selector__column',
)
.first(),
).not.toHaveClass('column-selector__column--active')
// select the "Post" collection again
await collectionSelector.click()
await page
.locator(
'[id^=list-drawer_1_] .list-drawer__select-collection.react-select .rs__option',
{
hasText: exactText('Post'),
},
)
.click()
// ensure that the "number" column is still deselected
await expect(
page
.locator(
'[id^=list-drawer_1_] .list-controls .column-selector .column-selector__column',
)
.first(),
).not.toHaveClass('column-selector__column--active')
})
test('should render custom table cell component', async () => {
await createPost()
await page.goto(postsUrl.list)
await expect(
page.locator('table > thead > tr > th', {
hasText: exactText('Demo UI Field'),
}),
).toBeVisible()
})
})
describe('multi-select', () => {
beforeEach(async () => {
// delete all posts created by the seed
await deleteAllPosts()
await createPost()
await createPost()
await createPost()
})
test('should select multiple rows', async () => {
await page.reload()
const selectAll = page.locator('.checkbox-input:has(#select-all)')
await page.locator('.row-1 .cell-_select input').check()
const indeterminateSelectAll = selectAll.locator('.checkbox-input__icon.partial')
expect(indeterminateSelectAll).toBeDefined()
await selectAll.locator('input').click()
const emptySelectAll = selectAll.locator('.checkbox-input__icon:not(.check):not(.partial)')
await expect(emptySelectAll).toHaveCount(0)
await selectAll.locator('input').click()
const checkSelectAll = selectAll.locator('.checkbox-input__icon.check')
expect(checkSelectAll).toBeDefined()
})
test('should delete many', async () => {
await page.goto(postsUrl.list)
await page.waitForURL(postsUrl.list)
// delete should not appear without selection
await expect(page.locator('#confirm-delete')).toHaveCount(0)
// select one row
await page.locator('.row-1 .cell-_select input').check()
// delete button should be present
await expect(page.locator('#confirm-delete')).toHaveCount(1)
await page.locator('.row-2 .cell-_select input').check()
await page.locator('.delete-documents__toggle').click()
await page.locator('#confirm-delete').click()
await expect(page.locator('.cell-_select')).toHaveCount(1)
})
})
describe('pagination', () => {
test('should paginate', async () => {
await deleteAllPosts()
await mapAsync([...Array(11)], async () => {
await createPost()
})
await page.reload()
const pageInfo = page.locator('.collection-list__page-info')
const perPage = page.locator('.per-page')
const paginator = page.locator('.paginator')
const tableItems = page.locator(tableRowLocator)
await expect(tableItems).toHaveCount(10)
await expect(pageInfo).toHaveText('1-10 of 11')
await expect(perPage).toContainText('Per Page: 10')
// Forward one page and back using numbers
await paginator.locator('button').nth(1).click()
await expect.poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT }).toContain('page=2')
await expect(tableItems).toHaveCount(1)
await paginator.locator('button').nth(0).click()
await expect.poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT }).toContain('page=1')
await expect(tableItems).toHaveCount(10)
})
})
// TODO: Troubleshoot flaky suite
describe('sorting', () => {
beforeEach(async () => {
// delete all posts created by the seed
await deleteAllPosts()
await createPost({ number: 1 })
await createPost({ number: 2 })
})
test('should sort', async () => {
await page.reload()
const upChevron = page.locator('#heading-number .sort-column__asc')
const downChevron = page.locator('#heading-number .sort-column__desc')
await upChevron.click()
await page.waitForURL(/sort=number/)
await expect(page.locator('.row-1 .cell-number')).toHaveText('1')
await expect(page.locator('.row-2 .cell-number')).toHaveText('2')
await downChevron.click()
await page.waitForURL(/sort=-number/)
await expect(page.locator('.row-1 .cell-number')).toHaveText('2')
await expect(page.locator('.row-2 .cell-number')).toHaveText('1')
})
})
describe('i18n', () => {
test('should display translated collections and globals config options', async () => {
await page.goto(postsUrl.list)
// collection label
await expect(page.locator('#nav-posts')).toContainText('Posts')
// global label
await expect(page.locator('#nav-global-global')).toContainText('Global')
// view description
await expect(page.locator('.view-description')).toContainText('Description')
})
test('should display translated field titles', async () => {
await createPost()
// column controls
await page.locator('.list-controls__toggle-columns').click()
await expect(
page.locator('.column-selector__column', {
hasText: exactText('Title'),
}),
).toHaveText('Title')
// filters
await page.locator('.list-controls__toggle-where').click()
await page.locator('.where-builder__add-first-filter').click()
await page.locator('.condition__field .rs__control').click()
const options = page.locator('.rs__option')
await expect(options.locator('text=Title')).toHaveText('Title')
// list columns
await expect(page.locator('#heading-title .sort-column__label')).toHaveText('Title')
await expect(page.locator('.search-filter input')).toHaveAttribute('placeholder', /(Title)/)
})
test('should use fallback language on field titles', async () => {
// change language German
await page.goto(postsUrl.account)
await page.locator('.payload-settings__language .react-select').click()
const languageSelect = page.locator('.rs__option')
// text field does not have a 'de' label
await languageSelect.locator('text=Deutsch').click()
await page.goto(postsUrl.list)
await page.locator('.list-controls__toggle-columns').click()
// expecting the label to fall back to english as default fallbackLng
await expect(
page.locator('.column-selector__column', {
hasText: exactText('Title'),
}),
).toHaveText('Title')
})
})
})
describe('field descriptions', () => {
test('should render static field description', async () => {
await page.goto(postsUrl.create)
await expect(page.locator('.field-description-descriptionAsString')).toContainText(
'Static field description.',
)
})
test('should render functional field description', async () => {
await page.goto(postsUrl.create)
await page.locator('#field-descriptionAsFunction').fill('functional')
await expect(page.locator('.field-description-descriptionAsFunction')).toContainText(
'Function description',
)
})
test('should render component field description', async () => {
await page.goto(postsUrl.create)
await page.locator('#field-descriptionAsComponent').fill('component')
await expect(page.locator('.field-description-descriptionAsComponent')).toContainText(
'Component description: descriptionAsComponent - component',
)
})
})
})
async function createPost(overrides?: Partial<Post>): Promise<Post> {
return payload.create({
collection: postsCollectionSlug,
data: {
description,
title,
...overrides,
},
}) as unknown as Promise<Post>
}
async function deleteAllPosts() {
await payload.delete({ collection: postsCollectionSlug, where: { id: { exists: true } } })
}
async function createGeo(overrides?: Partial<Geo>): Promise<Geo> {
return payload.create({
collection: geoCollectionSlug,
data: {
point: [4, -4],
...overrides,
},
}) as unknown as Promise<Geo>
}

View File

@@ -0,0 +1,187 @@
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 {
ensureAutoLoginAndCompilationIsDone,
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 { dateFieldsSlug } 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<Config>
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('Date', () => {
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, dateFieldsSlug)
const context = await browser.newContext()
page = await context.newPage()
initPageConsoleErrorCatch(page)
await reInitializeDB({
serverURL,
snapshotKey: 'fieldsDateTest',
uploadsDir: path.resolve(dirname, './collections/Upload/uploads'),
})
await ensureAutoLoginAndCompilationIsDone({ page, serverURL })
})
beforeEach(async () => {
await reInitializeDB({
serverURL,
snapshotKey: 'fieldsDateTest',
uploadsDir: path.resolve(dirname, './collections/Upload/uploads'),
})
if (client) {
await client.logout()
}
client = new RESTClient(null, { defaultSlug: 'users', serverURL })
await client.login()
await ensureAutoLoginAndCompilationIsDone({ page, serverURL })
})
test('should display formatted date in list view table cell', async () => {
await page.goto(url.list)
const formattedDateCell = page.locator('.row-1 .cell-timeOnly')
await expect(formattedDateCell).toContainText(' Aug ')
const notFormattedDateCell = page.locator('.row-1 .cell-default')
await expect(notFormattedDateCell).toContainText('August')
})
test('should display formatted date in useAsTitle', async () => {
await page.goto(url.list)
await page.locator('.row-1 .cell-default a').click()
await expect(page.locator('.doc-header__title.render-title')).toContainText('August')
})
test('should clear date', async () => {
await page.goto(url.create)
const dateField = page.locator('#field-default input')
await expect(dateField).toBeVisible()
await dateField.fill('02/07/2023')
await expect(dateField).toHaveValue('02/07/2023')
await saveDocAndAssert(page)
const clearButton = page.locator('#field-default .date-time-picker__clear-button')
await expect(clearButton).toBeVisible()
await clearButton.click()
await expect(dateField).toHaveValue('')
})
describe('localized dates', () => {
describe('EST', () => {
test.use({
geolocation: {
latitude: 42.3314,
longitude: -83.0458,
},
timezoneId: 'America/Detroit',
})
test('create EST day only date', async () => {
await page.goto(url.create)
await page.waitForURL(`**/${url.create}`)
const dateField = page.locator('#field-default input')
// enter date in default date field
await dateField.fill('02/07/2023')
await saveDocAndAssert(page)
// get the ID of the doc
const routeSegments = page.url().split('/')
const id = routeSegments.pop()
// fetch the doc (need the date string from the DB)
const { doc } = await client.findByID({ id, auth: true, slug: 'date-fields' })
expect(doc.default).toEqual('2023-02-07T12:00:00.000Z')
})
})
describe('PST', () => {
test.use({
geolocation: {
latitude: 37.774929,
longitude: -122.419416,
},
timezoneId: 'America/Los_Angeles',
})
test('create PDT day only date', async () => {
await page.goto(url.create)
await page.waitForURL(`**/${url.create}`)
const dateField = page.locator('#field-default input')
// enter date in default date field
await dateField.fill('02/07/2023')
await saveDocAndAssert(page)
// get the ID of the doc
const routeSegments = page.url().split('/')
const id = routeSegments.pop()
// fetch the doc (need the date string from the DB)
const { doc } = await client.findByID({ id, auth: true, slug: 'date-fields' })
expect(doc.default).toEqual('2023-02-07T12:00:00.000Z')
})
})
describe('ST', () => {
test.use({
geolocation: {
latitude: -14.5994,
longitude: -171.857,
},
timezoneId: 'Pacific/Apia',
})
test('create ST day only date', async () => {
await page.goto(url.create)
await page.waitForURL(`**/${url.create}`)
const dateField = page.locator('#field-default input')
// enter date in default date field
await dateField.fill('02/07/2023')
await saveDocAndAssert(page)
// get the ID of the doc
const routeSegments = page.url().split('/')
const id = routeSegments.pop()
// fetch the doc (need the date string from the DB)
const { doc } = await client.findByID({ id, auth: true, slug: 'date-fields' })
expect(doc.default).toEqual('2023-02-07T12:00:00.000Z')
})
})
})
})

View File

@@ -1,32 +1,31 @@
import type { SerializedBlockNode, SerializedLinkNode } from '@payloadcms/richtext-lexical'
import type { BrowserContext, Page } from '@playwright/test'
import type { PayloadTestSDK } from 'helpers/sdk/index.js'
import type { SerializedEditorState, SerializedParagraphNode, SerializedTextNode } from 'lexical'
import { expect, test } from '@playwright/test'
import { initPayloadE2ENoConfig } from 'helpers/initPayloadE2ENoConfig.js'
import { reInitializeDB } from 'helpers/reInitializeDB.js'
import path from 'path'
import { wait } from 'payload/utilities'
import { fileURLToPath } from 'url'
import type { Config, LexicalField, Upload } from '../../payload-types.js'
import type { PayloadTestSDK } from '../../../../../helpers/sdk/index.js'
import type { Config, LexicalField, Upload } from '../../../../payload-types.js'
import {
ensureAutoLoginAndCompilationIsDone,
initPageConsoleErrorCatch,
saveDocAndAssert,
throttleTest,
} from '../../../helpers.js'
import { AdminUrlUtil } from '../../../helpers/adminUrlUtil.js'
import { RESTClient } from '../../../helpers/rest.js'
import { POLL_TOPASS_TIMEOUT, TEST_TIMEOUT_LONG } from '../../../playwright.config.js'
import { lexicalFieldsSlug } from '../../slugs.js'
import { lexicalDocData } from './data.js'
} 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 { lexicalFieldsSlug } from '../../../../slugs.js'
import { lexicalDocData } from '../../data.js'
const filename = fileURLToPath(import.meta.url)
const currentFolder = path.dirname(filename)
const dirname = path.resolve(currentFolder, '../../')
const dirname = path.resolve(currentFolder, '../../../../')
const { beforeAll, beforeEach, describe } = test
@@ -60,7 +59,7 @@ async function navigateToLexicalFields(
await page.waitForURL(`**${linkDocHref}`)
}
describe('lexical', () => {
describe('lexicalBlocks', () => {
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
@@ -72,7 +71,7 @@ describe('lexical', () => {
initPageConsoleErrorCatch(page)
await reInitializeDB({
serverURL,
snapshotKey: 'fieldsLexicalTest',
snapshotKey: 'fieldsLexicalBlocksTest',
uploadsDir: path.resolve(dirname, './collections/Upload/uploads'),
})
await ensureAutoLoginAndCompilationIsDone({ page, serverURL })
@@ -85,7 +84,7 @@ describe('lexical', () => {
})*/
await reInitializeDB({
serverURL,
snapshotKey: 'fieldsLexicalTest',
snapshotKey: 'fieldsLexicalBlocksTest',
uploadsDir: [
path.resolve(dirname, './collections/Upload/uploads'),
path.resolve(dirname, './collections/Upload2/uploads2'),
@@ -99,345 +98,6 @@ describe('lexical', () => {
await client.login()
})
test('should not warn about unsaved changes when navigating to lexical editor with blocks node and then leaving the page without actually changing anything', async () => {
// This used to be an issue in the past, due to the node.setFields function in the blocks node being called unnecessarily when it's initialized after opening the document
// Other than the annoying unsaved changed prompt, this can also cause unnecessary auto-saves, when drafts & autosave is enabled
await navigateToLexicalFields()
await expect(
page.locator('.rich-text-lexical').nth(1).locator('.lexical-block').first(),
).toBeVisible()
// Navigate to some different page, away from the current document
await page.locator('.app-header__step-nav').first().locator('a').first().click()
// Make sure .leave-without-saving__content (the "Leave without saving") is not visible
await expect(page.locator('.leave-without-saving__content').first()).toBeHidden()
})
test('should not warn about unsaved changes when navigating to lexical editor with blocks node and then leaving the page after making a change and saving', async () => {
// Relevant issue: https://github.com/payloadcms/payload/issues/4115
await navigateToLexicalFields()
const thirdBlock = page.locator('.rich-text-lexical').nth(1).locator('.lexical-block').nth(2)
await thirdBlock.scrollIntoViewIfNeeded()
await expect(thirdBlock).toBeVisible()
const spanInBlock = thirdBlock
.locator('span')
.getByText('Some text below relationship node 1')
.first()
await spanInBlock.scrollIntoViewIfNeeded()
await expect(spanInBlock).toBeVisible()
await spanInBlock.click() // Click works better than focus
await page.keyboard.type('moretext')
const newSpanInBlock = thirdBlock
.locator('span')
.getByText('Some text below rmoretextelationship node 1')
.first()
await expect(newSpanInBlock).toBeVisible()
await expect(newSpanInBlock).toHaveText('Some text below rmoretextelationship node 1')
// Save
await saveDocAndAssert(page)
await expect(newSpanInBlock).toHaveText('Some text below rmoretextelationship node 1')
// Navigate to some different page, away from the current document
await page.locator('.app-header__step-nav').first().locator('a').first().click()
// Make sure .leave-without-saving__content (the "Leave without saving") is not visible
await expect(page.locator('.leave-without-saving__content').first()).toBeHidden()
})
test('should type and save typed text', async () => {
await navigateToLexicalFields()
const richTextField = page.locator('.rich-text-lexical').nth(1) // second
await richTextField.scrollIntoViewIfNeeded()
await expect(richTextField).toBeVisible()
const spanInEditor = richTextField.locator('span').getByText('Upload Node:').first()
await expect(spanInEditor).toBeVisible()
await spanInEditor.click() // Click works better than focus
// Now go to the END of the span
for (let i = 0; i < 6; i++) {
await page.keyboard.press('ArrowRight')
}
await page.keyboard.type('moretext')
await expect(spanInEditor).toHaveText('Upload Node:moretext')
await saveDocAndAssert(page)
await expect(async () => {
const lexicalDoc: LexicalField = (
await payload.find({
collection: lexicalFieldsSlug,
depth: 0,
overrideAccess: true,
where: {
title: {
equals: lexicalDocData.title,
},
},
})
).docs[0] as never
const lexicalField: SerializedEditorState = lexicalDoc.lexicalWithBlocks
const firstParagraphTextNode: SerializedTextNode = (
lexicalField.root.children[0] as SerializedParagraphNode
).children[0] as SerializedTextNode
expect(firstParagraphTextNode.text).toBe('Upload Node:moretext')
}).toPass({
timeout: POLL_TOPASS_TIMEOUT,
})
})
test('should be able to bold text using floating select toolbar', async () => {
await navigateToLexicalFields()
const richTextField = page.locator('.rich-text-lexical').nth(1) // second
await richTextField.scrollIntoViewIfNeeded()
await expect(richTextField).toBeVisible()
const spanInEditor = richTextField.locator('span').getByText('Upload Node:').first()
await expect(spanInEditor).toBeVisible()
await spanInEditor.click() // Click works better than focus
await page.keyboard.press('ArrowRight')
// Now select the text 'Node' (the .click() makes it click in the middle of the span)
for (let i = 0; i < 4; i++) {
await page.keyboard.press('Shift+ArrowRight')
}
// The following text should now be selected: Node
const floatingToolbar_formatSection = page.locator('.inline-toolbar-popup__group-format')
await expect(floatingToolbar_formatSection).toBeVisible()
await expect(page.locator('.toolbar-popup__button').first()).toBeVisible()
const boldButton = floatingToolbar_formatSection.locator('.toolbar-popup__button').first()
await expect(boldButton).toBeVisible()
await boldButton.click()
/**
* Next test section: check if it worked correctly
*/
const boldText = richTextField
.locator('.LexicalEditorTheme__paragraph')
.first()
.locator('strong')
await expect(boldText).toBeVisible()
await expect(boldText).toHaveText('Node')
await saveDocAndAssert(page)
await expect(async () => {
const lexicalDoc: LexicalField = (
await payload.find({
collection: lexicalFieldsSlug,
depth: 0,
overrideAccess: true,
where: {
title: {
equals: lexicalDocData.title,
},
},
})
).docs[0] as never
const lexicalField: SerializedEditorState = lexicalDoc.lexicalWithBlocks
const firstParagraph: SerializedParagraphNode = lexicalField.root
.children[0] as SerializedParagraphNode
expect(firstParagraph.children).toHaveLength(3)
const textNode1: SerializedTextNode = firstParagraph.children[0] as SerializedTextNode
const boldNode: SerializedTextNode = firstParagraph.children[1] as SerializedTextNode
const textNode2: SerializedTextNode = firstParagraph.children[2] as SerializedTextNode
expect(textNode1.text).toBe('Upload ')
expect(textNode1.format).toBe(0)
expect(boldNode.text).toBe('Node')
expect(boldNode.format).toBe(1)
expect(textNode2.text).toBe(':')
expect(textNode2.format).toBe(0)
}).toPass({
timeout: POLL_TOPASS_TIMEOUT,
})
})
test('Make sure highly specific issue does not occur when two richText fields share the same editor prop', async () => {
// Reproduces https://github.com/payloadcms/payload/issues/4282
const url: AdminUrlUtil = new AdminUrlUtil(serverURL, 'tabsWithRichText')
await page.goto(url.global('tabsWithRichText'))
const richTextField = page.locator('.rich-text-lexical').first()
await richTextField.scrollIntoViewIfNeeded()
await expect(richTextField).toBeVisible()
await richTextField.click() // Use click, because focus does not work
await page.keyboard.type('some text')
await page.locator('.tabs-field__tabs').first().getByText('en tab2').first().click()
await richTextField.scrollIntoViewIfNeeded()
await expect(richTextField).toBeVisible()
const contentEditable = richTextField.locator('.ContentEditable__root').first()
await expect
.poll(async () => await contentEditable.textContent(), { timeout: POLL_TOPASS_TIMEOUT })
.not.toBe('some text')
await expect
.poll(async () => await contentEditable.textContent(), { timeout: POLL_TOPASS_TIMEOUT })
.toBe('')
})
test('ensure blocks content is not hidden behind components outside of the editor', async () => {
// This test expects there to be a TreeView below the editor
// This test makes sure there are no z-index issues here
await navigateToLexicalFields()
const richTextField = page.locator('.rich-text-lexical').first()
await richTextField.scrollIntoViewIfNeeded()
await expect(richTextField).toBeVisible()
// Find span in contentEditable with text "Some text below relationship node"
const contentEditable = richTextField.locator('.ContentEditable__root').first()
await expect(contentEditable).toBeVisible()
await contentEditable.click() // Use click, because focus does not work
await page.keyboard.press('/')
const slashMenuPopover = page.locator('#slash-menu .slash-menu-popup')
await expect(slashMenuPopover).toBeVisible()
// Heading 2 should be the last, most bottom popover button element which should be initially visible, if not hidden by something (e.g. another block)
const popoverSelectButton = slashMenuPopover
.locator('button.slash-menu-popup__item-block-select')
.first()
await expect(popoverSelectButton).toBeVisible()
await popoverSelectButton.click()
const newSelectBlock = richTextField.locator('.lexical-block').first()
await newSelectBlock.scrollIntoViewIfNeeded()
await expect(newSelectBlock).toBeVisible()
await page.mouse.wheel(0, 300) // Scroll down so that the future react-select menu popover is displayed below and not above
const reactSelect = newSelectBlock.locator('.rs__control').first()
await reactSelect.click()
const popover = page.locator('.rs__menu').first()
const popoverOption3 = popover.locator('.rs__option').nth(2)
await expect(async () => {
const popoverOption3BoundingBox = await popoverOption3.boundingBox()
expect(popoverOption3BoundingBox).not.toBeNull()
expect(popoverOption3BoundingBox).not.toBeUndefined()
expect(popoverOption3BoundingBox.height).toBeGreaterThan(0)
expect(popoverOption3BoundingBox.width).toBeGreaterThan(0)
// Now click the button to see if it actually works. Simulate an actual mouse click instead of using .click()
// by using page.mouse and the correct coordinates
// .isVisible() and .click() might work fine EVEN if the slash menu is not actually visible by humans
// see: https://github.com/microsoft/playwright/issues/9923
// This is why we use page.mouse.click() here. It's the most effective way of detecting such a z-index issue
// and usually the only method which works.
const x = popoverOption3BoundingBox.x
const y = popoverOption3BoundingBox.y
await page.mouse.click(x, y, { button: 'left' })
}).toPass({
timeout: POLL_TOPASS_TIMEOUT,
})
await expect(reactSelect.locator('.rs__value-container').first()).toHaveText('Option 3')
})
// This reproduces an issue where if you create an upload node, the document drawer opens, you select a collection other than the default one, create a NEW upload document and save, it throws a lexical error
test('ensure creation of new upload document within upload node works', async () => {
await navigateToLexicalFields()
const richTextField = page.locator('.rich-text-lexical').nth(1) // second
await richTextField.scrollIntoViewIfNeeded()
await expect(richTextField).toBeVisible()
const lastParagraph = richTextField.locator('p').last()
await lastParagraph.scrollIntoViewIfNeeded()
await expect(lastParagraph).toBeVisible()
/**
* Create new upload node
*/
// type / to open the slash menu
await lastParagraph.click()
await page.keyboard.press('/')
await page.keyboard.type('Upload')
// Create Upload node
const slashMenuPopover = page.locator('#slash-menu .slash-menu-popup')
await expect(slashMenuPopover).toBeVisible()
const uploadSelectButton = slashMenuPopover.locator('button').nth(1)
await expect(uploadSelectButton).toBeVisible()
await expect(uploadSelectButton).toContainText('Upload')
await uploadSelectButton.click()
await expect(slashMenuPopover).toBeHidden()
await wait(500) // wait for drawer form state to initialize (it's a flake)
const uploadListDrawer = page.locator('dialog[id^=list-drawer_1_]').first() // IDs starting with list-drawer_1_ (there's some other symbol after the underscore)
await expect(uploadListDrawer).toBeVisible()
await wait(500)
await uploadListDrawer.locator('.rs__control .value-container').first().click()
await wait(500)
await expect(uploadListDrawer.locator('.rs__option').nth(1)).toBeVisible()
await expect(uploadListDrawer.locator('.rs__option').nth(1)).toContainText('Upload 2')
await uploadListDrawer.locator('.rs__option').nth(1).click()
// wait till the text appears in uploadListDrawer: "No Uploads 2 found. Either no Uploads 2 exist yet or none match the filters you've specified above."
await expect(
uploadListDrawer.getByText(
"No Uploads 2 found. Either no Uploads 2 exist yet or none match the filters you've specified above.",
),
).toBeVisible()
await uploadListDrawer.getByText('Create New').first().click()
const createUploadDrawer = page.locator('dialog[id^=doc-drawer_uploads2_]').first() // IDs starting with list-drawer_1_ (there's some other symbol after the underscore)
await expect(createUploadDrawer).toBeVisible()
await wait(500)
const input = createUploadDrawer.locator('.file-field__upload input[type="file"]').first()
await expect(input).toBeAttached()
await input.setInputFiles(path.resolve(dirname, './collections/Upload/payload.jpg'))
await expect(createUploadDrawer.locator('.file-field .file-field__filename')).toHaveValue(
'payload.jpg',
)
await wait(500)
await createUploadDrawer.getByText('Save').first().click()
await expect(createUploadDrawer).toBeHidden()
await expect(uploadListDrawer).toBeHidden()
await wait(500)
await saveDocAndAssert(page)
// second one should be the newly created one
const secondUploadNode = richTextField.locator('.lexical-upload').nth(1)
await secondUploadNode.scrollIntoViewIfNeeded()
await expect(secondUploadNode).toBeVisible()
await expect(secondUploadNode.locator('.lexical-upload__bottomRow')).toContainText(
'payload.jpg',
)
await expect(secondUploadNode.locator('.lexical-upload__collectionLabel')).toContainText(
'Upload 2',
)
})
describe('nested lexical editor in block', () => {
test('should type and save typed text', async () => {
await navigateToLexicalFields()
@@ -1248,10 +908,4 @@ describe('lexical', () => {
await expect(requiredTooltip).toBeInViewport() // toBeVisible() doesn't work for some reason
})
})
describe('localization', () => {
test.skip('ensure simple localized lexical field works', async () => {
await navigateToLexicalFields(true, true)
})
})
})

View File

@@ -0,0 +1,444 @@
import type { BrowserContext, Page } from '@playwright/test'
import type { SerializedEditorState, SerializedParagraphNode, SerializedTextNode } from 'lexical'
import { expect, test } from '@playwright/test'
import path from 'path'
import { wait } from 'payload/utilities'
import { fileURLToPath } from 'url'
import type { PayloadTestSDK } from '../../../../../helpers/sdk/index.js'
import type { Config, LexicalField } from '../../../../payload-types.js'
import {
ensureAutoLoginAndCompilationIsDone,
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 { lexicalFieldsSlug } from '../../../../slugs.js'
import { lexicalDocData } from '../../data.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<Config>
let client: RESTClient
let page: Page
let context: BrowserContext
let serverURL: string
/**
* Client-side navigation to the lexical editor from list view
*/
async function navigateToLexicalFields(
navigateToListView: boolean = true,
localized: boolean = false,
) {
if (navigateToListView) {
const url: AdminUrlUtil = new AdminUrlUtil(
serverURL,
localized ? 'lexical-localized-fields' : 'lexical-fields',
)
await page.goto(url.list)
}
const linkToDoc = page.locator('tbody tr:first-child .cell-title a').first()
await expect(() => expect(linkToDoc).toBeTruthy()).toPass({ timeout: POLL_TOPASS_TIMEOUT })
const linkDocHref = await linkToDoc.getAttribute('href')
await linkToDoc.click()
await page.waitForURL(`**${linkDocHref}`)
}
describe('lexicalMain', () => {
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 }))
context = await browser.newContext()
page = await context.newPage()
initPageConsoleErrorCatch(page)
await reInitializeDB({
serverURL,
snapshotKey: 'fieldsLexicalMainTest',
uploadsDir: path.resolve(dirname, './collections/Upload/uploads'),
})
await ensureAutoLoginAndCompilationIsDone({ page, serverURL })
})
beforeEach(async () => {
/*await throttleTest({
page,
context,
delay: 'Slow 4G',
})*/
await reInitializeDB({
serverURL,
snapshotKey: 'fieldsLexicalMainTest',
uploadsDir: [
path.resolve(dirname, './collections/Upload/uploads'),
path.resolve(dirname, './collections/Upload2/uploads2'),
],
})
if (client) {
await client.logout()
}
client = new RESTClient(null, { defaultSlug: 'rich-text-fields', serverURL })
await client.login()
})
test('should not warn about unsaved changes when navigating to lexical editor with blocks node and then leaving the page without actually changing anything', async () => {
// This used to be an issue in the past, due to the node.setFields function in the blocks node being called unnecessarily when it's initialized after opening the document
// Other than the annoying unsaved changed prompt, this can also cause unnecessary auto-saves, when drafts & autosave is enabled
await navigateToLexicalFields()
await expect(
page.locator('.rich-text-lexical').nth(1).locator('.lexical-block').first(),
).toBeVisible()
// Navigate to some different page, away from the current document
await page.locator('.app-header__step-nav').first().locator('a').first().click()
// Make sure .leave-without-saving__content (the "Leave without saving") is not visible
await expect(page.locator('.leave-without-saving__content').first()).toBeHidden()
})
test('should not warn about unsaved changes when navigating to lexical editor with blocks node and then leaving the page after making a change and saving', async () => {
// Relevant issue: https://github.com/payloadcms/payload/issues/4115
await navigateToLexicalFields()
const thirdBlock = page.locator('.rich-text-lexical').nth(1).locator('.lexical-block').nth(2)
await thirdBlock.scrollIntoViewIfNeeded()
await expect(thirdBlock).toBeVisible()
const spanInBlock = thirdBlock
.locator('span')
.getByText('Some text below relationship node 1')
.first()
await spanInBlock.scrollIntoViewIfNeeded()
await expect(spanInBlock).toBeVisible()
await spanInBlock.click() // Click works better than focus
await page.keyboard.type('moretext')
const newSpanInBlock = thirdBlock
.locator('span')
.getByText('Some text below rmoretextelationship node 1')
.first()
await expect(newSpanInBlock).toBeVisible()
await expect(newSpanInBlock).toHaveText('Some text below rmoretextelationship node 1')
// Save
await saveDocAndAssert(page)
await expect(newSpanInBlock).toHaveText('Some text below rmoretextelationship node 1')
// Navigate to some different page, away from the current document
await page.locator('.app-header__step-nav').first().locator('a').first().click()
// Make sure .leave-without-saving__content (the "Leave without saving") is not visible
await expect(page.locator('.leave-without-saving__content').first()).toBeHidden()
})
test('should type and save typed text', async () => {
await navigateToLexicalFields()
const richTextField = page.locator('.rich-text-lexical').nth(1) // second
await richTextField.scrollIntoViewIfNeeded()
await expect(richTextField).toBeVisible()
const spanInEditor = richTextField.locator('span').getByText('Upload Node:').first()
await expect(spanInEditor).toBeVisible()
await spanInEditor.click() // Click works better than focus
// Now go to the END of the span
for (let i = 0; i < 6; i++) {
await page.keyboard.press('ArrowRight')
}
await page.keyboard.type('moretext')
await expect(spanInEditor).toHaveText('Upload Node:moretext')
await saveDocAndAssert(page)
await expect(async () => {
const lexicalDoc: LexicalField = (
await payload.find({
collection: lexicalFieldsSlug,
depth: 0,
overrideAccess: true,
where: {
title: {
equals: lexicalDocData.title,
},
},
})
).docs[0] as never
const lexicalField: SerializedEditorState = lexicalDoc.lexicalWithBlocks
const firstParagraphTextNode: SerializedTextNode = (
lexicalField.root.children[0] as SerializedParagraphNode
).children[0] as SerializedTextNode
expect(firstParagraphTextNode.text).toBe('Upload Node:moretext')
}).toPass({
timeout: POLL_TOPASS_TIMEOUT,
})
})
test('should be able to bold text using floating select toolbar', async () => {
await navigateToLexicalFields()
const richTextField = page.locator('.rich-text-lexical').nth(1) // second
await richTextField.scrollIntoViewIfNeeded()
await expect(richTextField).toBeVisible()
const spanInEditor = richTextField.locator('span').getByText('Upload Node:').first()
await expect(spanInEditor).toBeVisible()
await spanInEditor.click() // Click works better than focus
await page.keyboard.press('ArrowRight')
// Now select the text 'Node' (the .click() makes it click in the middle of the span)
for (let i = 0; i < 4; i++) {
await page.keyboard.press('Shift+ArrowRight')
}
// The following text should now be selected: Node
const floatingToolbar_formatSection = page.locator('.inline-toolbar-popup__group-format')
await expect(floatingToolbar_formatSection).toBeVisible()
await expect(page.locator('.toolbar-popup__button').first()).toBeVisible()
const boldButton = floatingToolbar_formatSection.locator('.toolbar-popup__button').first()
await expect(boldButton).toBeVisible()
await boldButton.click()
/**
* Next test section: check if it worked correctly
*/
const boldText = richTextField
.locator('.LexicalEditorTheme__paragraph')
.first()
.locator('strong')
await expect(boldText).toBeVisible()
await expect(boldText).toHaveText('Node')
await saveDocAndAssert(page)
await expect(async () => {
const lexicalDoc: LexicalField = (
await payload.find({
collection: lexicalFieldsSlug,
depth: 0,
overrideAccess: true,
where: {
title: {
equals: lexicalDocData.title,
},
},
})
).docs[0] as never
const lexicalField: SerializedEditorState = lexicalDoc.lexicalWithBlocks
const firstParagraph: SerializedParagraphNode = lexicalField.root
.children[0] as SerializedParagraphNode
expect(firstParagraph.children).toHaveLength(3)
const textNode1: SerializedTextNode = firstParagraph.children[0] as SerializedTextNode
const boldNode: SerializedTextNode = firstParagraph.children[1] as SerializedTextNode
const textNode2: SerializedTextNode = firstParagraph.children[2] as SerializedTextNode
expect(textNode1.text).toBe('Upload ')
expect(textNode1.format).toBe(0)
expect(boldNode.text).toBe('Node')
expect(boldNode.format).toBe(1)
expect(textNode2.text).toBe(':')
expect(textNode2.format).toBe(0)
}).toPass({
timeout: POLL_TOPASS_TIMEOUT,
})
})
test('Make sure highly specific issue does not occur when two richText fields share the same editor prop', async () => {
// Reproduces https://github.com/payloadcms/payload/issues/4282
const url: AdminUrlUtil = new AdminUrlUtil(serverURL, 'tabsWithRichText')
await page.goto(url.global('tabsWithRichText'))
const richTextField = page.locator('.rich-text-lexical').first()
await richTextField.scrollIntoViewIfNeeded()
await expect(richTextField).toBeVisible()
await richTextField.click() // Use click, because focus does not work
await page.keyboard.type('some text')
await page.locator('.tabs-field__tabs').first().getByText('en tab2').first().click()
await richTextField.scrollIntoViewIfNeeded()
await expect(richTextField).toBeVisible()
const contentEditable = richTextField.locator('.ContentEditable__root').first()
await expect
.poll(async () => await contentEditable.textContent(), { timeout: POLL_TOPASS_TIMEOUT })
.not.toBe('some text')
await expect
.poll(async () => await contentEditable.textContent(), { timeout: POLL_TOPASS_TIMEOUT })
.toBe('')
})
test('ensure blocks content is not hidden behind components outside of the editor', async () => {
// This test expects there to be a TreeView below the editor
// This test makes sure there are no z-index issues here
await navigateToLexicalFields()
const richTextField = page.locator('.rich-text-lexical').first()
await richTextField.scrollIntoViewIfNeeded()
await expect(richTextField).toBeVisible()
// Find span in contentEditable with text "Some text below relationship node"
const contentEditable = richTextField.locator('.ContentEditable__root').first()
await expect(contentEditable).toBeVisible()
await contentEditable.click() // Use click, because focus does not work
await page.keyboard.press('/')
const slashMenuPopover = page.locator('#slash-menu .slash-menu-popup')
await expect(slashMenuPopover).toBeVisible()
// Heading 2 should be the last, most bottom popover button element which should be initially visible, if not hidden by something (e.g. another block)
const popoverSelectButton = slashMenuPopover
.locator('button.slash-menu-popup__item-block-select')
.first()
await expect(popoverSelectButton).toBeVisible()
await popoverSelectButton.click()
const newSelectBlock = richTextField.locator('.lexical-block').first()
await newSelectBlock.scrollIntoViewIfNeeded()
await expect(newSelectBlock).toBeVisible()
await page.mouse.wheel(0, 300) // Scroll down so that the future react-select menu popover is displayed below and not above
const reactSelect = newSelectBlock.locator('.rs__control').first()
await reactSelect.click()
const popover = page.locator('.rs__menu').first()
const popoverOption3 = popover.locator('.rs__option').nth(2)
await expect(async () => {
const popoverOption3BoundingBox = await popoverOption3.boundingBox()
expect(popoverOption3BoundingBox).not.toBeNull()
expect(popoverOption3BoundingBox).not.toBeUndefined()
expect(popoverOption3BoundingBox.height).toBeGreaterThan(0)
expect(popoverOption3BoundingBox.width).toBeGreaterThan(0)
// Now click the button to see if it actually works. Simulate an actual mouse click instead of using .click()
// by using page.mouse and the correct coordinates
// .isVisible() and .click() might work fine EVEN if the slash menu is not actually visible by humans
// see: https://github.com/microsoft/playwright/issues/9923
// This is why we use page.mouse.click() here. It's the most effective way of detecting such a z-index issue
// and usually the only method which works.
const x = popoverOption3BoundingBox.x
const y = popoverOption3BoundingBox.y
await page.mouse.click(x, y, { button: 'left' })
}).toPass({
timeout: POLL_TOPASS_TIMEOUT,
})
await expect(reactSelect.locator('.rs__value-container').first()).toHaveText('Option 3')
})
// This reproduces an issue where if you create an upload node, the document drawer opens, you select a collection other than the default one, create a NEW upload document and save, it throws a lexical error
test('ensure creation of new upload document within upload node works', async () => {
await navigateToLexicalFields()
const richTextField = page.locator('.rich-text-lexical').nth(1) // second
await richTextField.scrollIntoViewIfNeeded()
await expect(richTextField).toBeVisible()
const lastParagraph = richTextField.locator('p').last()
await lastParagraph.scrollIntoViewIfNeeded()
await expect(lastParagraph).toBeVisible()
/**
* Create new upload node
*/
// type / to open the slash menu
await lastParagraph.click()
await page.keyboard.press('/')
await page.keyboard.type('Upload')
// Create Upload node
const slashMenuPopover = page.locator('#slash-menu .slash-menu-popup')
await expect(slashMenuPopover).toBeVisible()
const uploadSelectButton = slashMenuPopover.locator('button').nth(1)
await expect(uploadSelectButton).toBeVisible()
await expect(uploadSelectButton).toContainText('Upload')
await uploadSelectButton.click()
await expect(slashMenuPopover).toBeHidden()
await wait(500) // wait for drawer form state to initialize (it's a flake)
const uploadListDrawer = page.locator('dialog[id^=list-drawer_1_]').first() // IDs starting with list-drawer_1_ (there's some other symbol after the underscore)
await expect(uploadListDrawer).toBeVisible()
await wait(500)
await uploadListDrawer.locator('.rs__control .value-container').first().click()
await wait(500)
await expect(uploadListDrawer.locator('.rs__option').nth(1)).toBeVisible()
await expect(uploadListDrawer.locator('.rs__option').nth(1)).toContainText('Upload 2')
await uploadListDrawer.locator('.rs__option').nth(1).click()
// wait till the text appears in uploadListDrawer: "No Uploads 2 found. Either no Uploads 2 exist yet or none match the filters you've specified above."
await expect(
uploadListDrawer.getByText(
"No Uploads 2 found. Either no Uploads 2 exist yet or none match the filters you've specified above.",
),
).toBeVisible()
await uploadListDrawer.getByText('Create New').first().click()
const createUploadDrawer = page.locator('dialog[id^=doc-drawer_uploads2_]').first() // IDs starting with list-drawer_1_ (there's some other symbol after the underscore)
await expect(createUploadDrawer).toBeVisible()
await wait(500)
const input = createUploadDrawer.locator('.file-field__upload input[type="file"]').first()
await expect(input).toBeAttached()
await input.setInputFiles(path.resolve(dirname, './collections/Upload/payload.jpg'))
await expect(createUploadDrawer.locator('.file-field .file-field__filename')).toHaveValue(
'payload.jpg',
)
await wait(500)
await createUploadDrawer.getByText('Save').first().click()
await expect(createUploadDrawer).toBeHidden()
await expect(uploadListDrawer).toBeHidden()
await wait(500)
await saveDocAndAssert(page)
// second one should be the newly created one
const secondUploadNode = richTextField.locator('.lexical-upload').nth(1)
await secondUploadNode.scrollIntoViewIfNeeded()
await expect(secondUploadNode).toBeVisible()
await expect(secondUploadNode.locator('.lexical-upload__bottomRow')).toContainText(
'payload.jpg',
)
await expect(secondUploadNode.locator('.lexical-upload__collectionLabel')).toContainText(
'Upload 2',
)
})
describe('localization', () => {
test.skip('ensure simple localized lexical field works', async () => {
await navigateToLexicalFields(true, true)
})
})
})

View File

@@ -0,0 +1,158 @@
import type { Page } from '@playwright/test'
import { expect, test } from '@playwright/test'
import path from 'path'
import { wait } from 'payload/utilities'
import { fileURLToPath } from 'url'
import type { PayloadTestSDK } from '../../../helpers/sdk/index.js'
import type { Config } from '../../payload-types.js'
import {
ensureAutoLoginAndCompilationIsDone,
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 { numberDoc } 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<Config>
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('Number', () => {
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, 'number-fields')
const context = await browser.newContext()
page = await context.newPage()
initPageConsoleErrorCatch(page)
await reInitializeDB({
serverURL,
snapshotKey: 'fieldsNumberTest',
uploadsDir: path.resolve(dirname, './collections/Upload/uploads'),
})
await ensureAutoLoginAndCompilationIsDone({ page, serverURL })
})
beforeEach(async () => {
await reInitializeDB({
serverURL,
snapshotKey: 'fieldsNumberTest',
uploadsDir: path.resolve(dirname, './collections/Upload/uploads'),
})
if (client) {
await client.logout()
}
client = new RESTClient(null, { defaultSlug: 'users', serverURL })
await client.login()
await ensureAutoLoginAndCompilationIsDone({ page, serverURL })
})
test('should display field in list view', async () => {
await page.goto(url.list)
const textCell = page.locator('.row-1 .cell-number')
await expect(textCell).toHaveText(String(numberDoc.number))
})
test('should filter Number fields in the collection view - greaterThanOrEqual', async () => {
await page.goto(url.list)
// should have 3 entries
await expect(page.locator('table >> tbody >> tr')).toHaveCount(3)
// open the filter options
await page.locator('.list-controls__toggle-where').click()
await expect(page.locator('.list-controls__where.rah-static--height-auto')).toBeVisible()
await page.locator('.where-builder__add-first-filter').click()
const initialField = page.locator('.condition__field')
const operatorField = page.locator('.condition__operator')
const valueField = page.locator('.condition__value >> input')
// select Number field to filter on
await initialField.click()
const initialFieldOptions = initialField.locator('.rs__option')
await initialFieldOptions.locator('text=number').first().click()
await expect(initialField.locator('.rs__single-value')).toContainText('Number')
// select >= operator
await operatorField.click()
const operatorOptions = operatorField.locator('.rs__option')
await operatorOptions.last().click()
await expect(operatorField.locator('.rs__single-value')).toContainText(
'is greater than or equal to',
)
// enter value of 3
await valueField.fill('3')
await expect(valueField).toHaveValue('3')
await wait(300)
// should have 2 entries after filtering
await expect(page.locator('table >> tbody >> tr')).toHaveCount(2)
})
test('should create', async () => {
const input = 5
await page.goto(url.create)
const field = page.locator('#field-number')
await field.fill(String(input))
await saveDocAndAssert(page)
await expect(field).toHaveValue(String(input))
})
test('should create hasMany', async () => {
const input = 5
await page.goto(url.create)
const field = page.locator('.field-hasMany')
await field.click()
await page.keyboard.type(String(input))
await page.keyboard.press('Enter')
await saveDocAndAssert(page)
await expect(field.locator('.rs__value-container')).toContainText(String(input))
})
test('should bypass min rows validation when no rows present and field is not required', async () => {
await page.goto(url.create)
await saveDocAndAssert(page)
await expect(page.locator('.Toastify')).toContainText('successfully')
})
test('should fail min rows validation when rows are present', async () => {
const input = 5
await page.goto(url.create)
await page.locator('.field-withMinRows').click()
await page.keyboard.type(String(input))
await page.keyboard.press('Enter')
await page.click('#action-save', { delay: 100 })
await expect(page.locator('.Toastify')).toContainText(
'The following field is invalid: withMinRows',
)
})
})

View File

@@ -0,0 +1,164 @@
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 {
ensureAutoLoginAndCompilationIsDone,
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 { pointFieldsSlug } 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<Config>
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
let filledGroupPoint
let emptyGroupPoint
describe('Point', () => {
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, pointFieldsSlug)
const context = await browser.newContext()
page = await context.newPage()
initPageConsoleErrorCatch(page)
await reInitializeDB({
serverURL,
snapshotKey: 'fieldsPointTest',
uploadsDir: path.resolve(dirname, './collections/Upload/uploads'),
})
await ensureAutoLoginAndCompilationIsDone({ page, serverURL })
})
beforeEach(async () => {
await reInitializeDB({
serverURL,
snapshotKey: 'fieldsPointTest',
uploadsDir: path.resolve(dirname, './collections/Upload/uploads'),
})
if (client) {
await client.logout()
}
client = new RESTClient(null, { defaultSlug: 'users', serverURL })
await client.login()
await ensureAutoLoginAndCompilationIsDone({ page, serverURL })
filledGroupPoint = await payload.create({
collection: pointFieldsSlug,
data: {
group: { point: [4, 2] },
localized: [4, 2],
point: [5, 5],
},
})
emptyGroupPoint = await payload.create({
collection: pointFieldsSlug,
data: {
group: {},
localized: [3, -2],
point: [5, 5],
},
})
})
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)
await expect(longField).toHaveAttribute('value', '9')
await expect(latField).toHaveAttribute('value', '-2')
await expect(localizedLongField).toHaveAttribute('value', '1')
await expect(localizedLatField).toHaveAttribute('value', '-1')
await expect(groupLongitude).toHaveAttribute('value', '3')
await expect(groupLatField).toHaveAttribute('value', '-8')
})
test('should update point', async () => {
await page.goto(url.edit(emptyGroupPoint.id))
await page.waitForURL(`**/${emptyGroupPoint.id}`)
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('2')
const localizedLatField = page.locator('#field-latitude-localized')
await localizedLatField.fill('-2')
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)
await expect(longField).toHaveAttribute('value', '9')
await expect(latField).toHaveAttribute('value', '-2')
await expect(localizedLongField).toHaveAttribute('value', '2')
await expect(localizedLatField).toHaveAttribute('value', '-2')
await expect(groupLongitude).toHaveAttribute('value', '3')
await expect(groupLatField).toHaveAttribute('value', '-8')
})
test('should be able to clear a value point', async () => {
await page.goto(url.edit(filledGroupPoint.id))
await page.waitForURL(`**/${filledGroupPoint.id}`)
const groupLongitude = page.locator('#field-longitude-group__point')
await groupLongitude.fill('')
const groupLatField = page.locator('#field-latitude-group__point')
await groupLatField.fill('')
await saveDocAndAssert(page)
await expect(groupLongitude).toHaveAttribute('value', '')
await expect(groupLatField).toHaveAttribute('value', '')
})
})

View File

@@ -0,0 +1,142 @@
import type { Page } from '@playwright/test'
import { expect, test } from '@playwright/test'
import path from 'path'
import { wait } from 'payload/utilities'
import { fileURLToPath } from 'url'
import type { PayloadTestSDK } from '../../../helpers/sdk/index.js'
import type { Config } from '../../payload-types.js'
import {
ensureAutoLoginAndCompilationIsDone,
initPageConsoleErrorCatch,
navigateToListCellLink,
saveDocAndAssert,
switchTab,
} 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 { tabsFieldsSlug } 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<Config>
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, tabsFieldsSlug)
const context = await browser.newContext()
page = await context.newPage()
initPageConsoleErrorCatch(page)
await reInitializeDB({
serverURL,
snapshotKey: 'fieldsTabsTest',
uploadsDir: path.resolve(dirname, './collections/Upload/uploads'),
})
await ensureAutoLoginAndCompilationIsDone({ page, serverURL })
})
beforeEach(async () => {
await reInitializeDB({
serverURL,
snapshotKey: 'fieldsTabsTest',
uploadsDir: path.resolve(dirname, './collections/Upload/uploads'),
})
if (client) {
await client.logout()
}
client = new RESTClient(null, { defaultSlug: 'users', serverURL })
await client.login()
await ensureAutoLoginAndCompilationIsDone({ page, serverURL })
})
test('should fill and retain a new value within a tab while switching tabs', async () => {
const textInRowValue = 'hello'
const numberInRowValue = '23'
const jsonValue = '{ "foo": "bar"}'
await page.goto(url.create)
await page.waitForURL(url.create)
await switchTab(page, '.tabs-field__tab-button:has-text("Tab with Row")')
await page.locator('#field-textInRow').fill(textInRowValue)
await page.locator('#field-numberInRow').fill(numberInRowValue)
await page.locator('.json-field .inputarea').fill(jsonValue)
await wait(300)
await switchTab(page, '.tabs-field__tab-button:has-text("Tab with Array")')
await switchTab(page, '.tabs-field__tab-button:has-text("Tab with Row")')
await expect(page.locator('#field-textInRow')).toHaveValue(textInRowValue)
await expect(page.locator('#field-numberInRow')).toHaveValue(numberInRowValue)
await expect(page.locator('.json-field .lines-content')).toContainText(jsonValue)
})
test('should retain updated values within tabs while switching between tabs', async () => {
const textInRowValue = 'new value'
const jsonValue = '{ "new": "value"}'
await page.goto(url.list)
await navigateToListCellLink(page)
// Go to Row tab, update the value
await switchTab(page, '.tabs-field__tab-button:has-text("Tab with Row")')
await page.locator('#field-textInRow').fill(textInRowValue)
await page.locator('.json-field .inputarea').fill(jsonValue)
await wait(500)
// Go to Array tab, then back to Row. Make sure new value is still there
await switchTab(page, '.tabs-field__tab-button:has-text("Tab with Array")')
await switchTab(page, '.tabs-field__tab-button:has-text("Tab with Row")')
await expect(page.locator('#field-textInRow')).toHaveValue(textInRowValue)
await expect(page.locator('.json-field .lines-content')).toContainText(jsonValue)
// Go to array tab, save the doc
await switchTab(page, '.tabs-field__tab-button:has-text("Tab with Array")')
await saveDocAndAssert(page)
// Go back to row tab, make sure the new value is still present
await switchTab(page, '.tabs-field__tab-button:has-text("Tab with Row")')
await expect(page.locator('#field-textInRow')).toHaveValue(textInRowValue)
})
test('should render array data within unnamed tabs', async () => {
await page.goto(url.list)
await navigateToListCellLink(page)
await switchTab(page, '.tabs-field__tab-button:has-text("Tab with Array")')
await expect(page.locator('#field-array__0__text')).toHaveValue("Hello, I'm the first row")
})
test('should render array data within named tabs', async () => {
await page.goto(url.list)
await navigateToListCellLink(page)
await switchTab(page, '.tabs-field__tab-button:nth-child(5)')
await expect(page.locator('#field-tab__array__0__text')).toHaveValue(
"Hello, I'm the first row, in a named tab",
)
})
})

View File

@@ -0,0 +1,208 @@
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 {
ensureAutoLoginAndCompilationIsDone,
exactText,
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 { textFieldsSlug } from '../../slugs.js'
import { textDoc } 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<Config>
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('Text', () => {
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, textFieldsSlug)
const context = await browser.newContext()
page = await context.newPage()
initPageConsoleErrorCatch(page)
await reInitializeDB({
serverURL,
snapshotKey: 'fieldsTextTest',
uploadsDir: path.resolve(dirname, './collections/Upload/uploads'),
})
await ensureAutoLoginAndCompilationIsDone({ page, serverURL })
})
beforeEach(async () => {
await reInitializeDB({
serverURL,
snapshotKey: 'fieldsTextTest',
uploadsDir: path.resolve(dirname, './collections/Upload/uploads'),
})
if (client) {
await client.logout()
}
client = new RESTClient(null, { defaultSlug: 'users', serverURL })
await client.login()
await ensureAutoLoginAndCompilationIsDone({ page, serverURL })
})
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 hide field in column selector when admin.disableListColumn', async () => {
await page.goto(url.list)
await page.locator('.list-controls__toggle-columns').click()
await expect(page.locator('.column-selector')).toBeVisible()
// Check if "Disable List Column Text" is not present in the column options
await expect(
page.locator(`.column-selector .column-selector__column`, {
hasText: exactText('Disable List Column Text'),
}),
).toBeHidden()
})
test('should show field in filter when admin.disableListColumn is true', async () => {
await page.goto(url.list)
await page.locator('.list-controls__toggle-where').click()
await page.locator('.where-builder__add-first-filter').click()
const initialField = page.locator('.condition__field')
await initialField.click()
await expect(
initialField.locator(`.rs__menu-list:has-text("Disable List Column Text")`),
).toBeVisible()
})
test('should display field in list view column selector if admin.disableListColumn is false and admin.disableListFilter is true', async () => {
await page.goto(url.list)
await page.locator('.list-controls__toggle-columns').click()
await expect(page.locator('.column-selector')).toBeVisible()
// Check if "Disable List Filter Text" is present in the column options
await expect(
page.locator(`.column-selector .column-selector__column`, {
hasText: exactText('Disable List Filter Text'),
}),
).toBeVisible()
})
test('should hide field in filter when admin.disableListFilter is true', async () => {
await page.goto(url.list)
await page.locator('.list-controls__toggle-where').click()
await page.locator('.where-builder__add-first-filter').click()
const initialField = page.locator('.condition__field')
await initialField.click()
await expect(
initialField.locator(`.rs__option :has-text("Disable List Filter Text")`),
).toBeHidden()
})
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('<No Text en>')
})
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(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')
})
test('should render custom label', async () => {
await page.goto(url.create)
const label = page.locator('label.custom-label[for="field-customLabel"]')
await expect(label).toHaveText('#label')
})
test('should render custom error', async () => {
await page.goto(url.create)
const input = page.locator('input[id="field-customError"]')
await input.fill('ab')
await expect(input).toHaveValue('ab')
const error = page.locator('.custom-error:near(input[id="field-customError"])')
const submit = page.locator('button[type="button"][id="action-save"]')
await submit.click()
await expect(error).toHaveText('#custom-error')
})
test('should render beforeInput and afterInput', async () => {
await page.goto(url.create)
const input = page.locator('input[id="field-beforeAndAfterInput"]')
const prevSibling = await input.evaluateHandle((el) => {
return el.previousElementSibling
})
const prevSiblingText = await page.evaluate((el) => el.textContent, prevSibling)
expect(prevSiblingText).toEqual('#before-input')
const nextSibling = await input.evaluateHandle((el) => {
return el.nextElementSibling
})
const nextSiblingText = await page.evaluate((el) => el.textContent, nextSibling)
expect(nextSiblingText).toEqual('#after-input')
})
test('should create hasMany with multiple texts', async () => {
const input = 'five'
const furtherInput = 'six'
await page.goto(url.create)
const requiredField = page.locator('#field-text')
const field = page.locator('.field-hasMany')
await requiredField.fill(String(input))
await field.click()
await page.keyboard.type(input)
await page.keyboard.press('Enter')
await page.keyboard.type(furtherInput)
await page.keyboard.press('Enter')
await saveDocAndAssert(page)
await expect(field.locator('.rs__value-container')).toContainText(input)
await expect(field.locator('.rs__value-container')).toContainText(furtherInput)
})
})

View File

@@ -0,0 +1,176 @@
import type { Page } from '@playwright/test'
import { expect, test } from '@playwright/test'
import path from 'path'
import { wait } from 'payload/utilities'
import { fileURLToPath } from 'url'
import type { PayloadTestSDK } from '../../../helpers/sdk/index.js'
import type { Config } from '../../payload-types.js'
import {
ensureAutoLoginAndCompilationIsDone,
initPageConsoleErrorCatch,
openDocDrawer,
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 { uploadsSlug } 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<Config>
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('Upload', () => {
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, uploadsSlug)
const context = await browser.newContext()
page = await context.newPage()
initPageConsoleErrorCatch(page)
await reInitializeDB({
serverURL,
snapshotKey: 'fieldsUploadTest',
uploadsDir: path.resolve(dirname, './collections/Upload/uploads'),
})
await ensureAutoLoginAndCompilationIsDone({ page, serverURL })
})
beforeEach(async () => {
await reInitializeDB({
serverURL,
snapshotKey: 'fieldsUploadTest',
uploadsDir: path.resolve(dirname, './collections/Upload/uploads'),
})
if (client) {
await client.logout()
}
client = new RESTClient(null, { defaultSlug: 'users', serverURL })
await client.login()
await ensureAutoLoginAndCompilationIsDone({ page, serverURL })
})
async function uploadImage() {
await page.goto(url.create)
// create a jpg upload
await page
.locator('.file-field__upload input[type="file"]')
.setInputFiles(path.resolve(dirname, './collections/Upload/payload.jpg'))
await expect(page.locator('.file-field .file-field__filename')).toHaveValue('payload.jpg')
await saveDocAndAssert(page)
}
// eslint-disable-next-line playwright/expect-expect
test('should upload files', async () => {
await uploadImage()
})
// test that the image renders
test('should render uploaded image', async () => {
await uploadImage()
await expect(page.locator('.file-field .file-details img')).toHaveAttribute(
'src',
'/api/uploads/file/payload-1.jpg',
)
})
test('should upload using the document drawer', async () => {
await uploadImage()
await wait(1000)
// Open the media drawer and create a png upload
await openDocDrawer(page, '.field-type.upload .upload__toggler.doc-drawer__toggler')
await page
.locator('[id^=doc-drawer_uploads_1_] .file-field__upload input[type="file"]')
.setInputFiles(path.resolve(dirname, './uploads/payload.png'))
await expect(
page.locator('[id^=doc-drawer_uploads_1_] .file-field__upload .file-field__filename'),
).toHaveValue('payload.png')
await page.locator('[id^=doc-drawer_uploads_1_] #action-save').click()
await expect(page.locator('.Toastify')).toContainText('successfully')
// Assert that the media field has the png upload
await expect(
page.locator('.field-type.upload .file-details .file-meta__url a'),
).toHaveAttribute('href', '/api/uploads/file/payload-1.png')
await expect(page.locator('.field-type.upload .file-details .file-meta__url a')).toContainText(
'payload-1.png',
)
await expect(page.locator('.field-type.upload .file-details img')).toHaveAttribute(
'src',
'/api/uploads/file/payload-1.png',
)
await saveDocAndAssert(page)
})
test('should clear selected upload', async () => {
await uploadImage()
await wait(1000) // TODO: Fix this. Need to wait a bit until the form in the drawer mounted, otherwise values sometimes disappear. This is an issue for all drawers
await openDocDrawer(page, '.field-type.upload .upload__toggler.doc-drawer__toggler')
await wait(1000)
await page
.locator('[id^=doc-drawer_uploads_1_] .file-field__upload input[type="file"]')
.setInputFiles(path.resolve(dirname, './uploads/payload.png'))
await expect(
page.locator('[id^=doc-drawer_uploads_1_] .file-field__upload .file-field__filename'),
).toHaveValue('payload.png')
await page.locator('[id^=doc-drawer_uploads_1_] #action-save').click()
await expect(page.locator('.Toastify')).toContainText('successfully')
await page.locator('.field-type.upload .file-details__remove').click()
})
test('should select using the list drawer and restrict mimetype based on filterOptions', async () => {
await uploadImage()
await openDocDrawer(page, '.field-type.upload .upload__toggler.list-drawer__toggler')
const jpgImages = page.locator('[id^=list-drawer_1_] .upload-gallery img[src$=".jpg"]')
await expect
.poll(async () => await jpgImages.count(), { timeout: POLL_TOPASS_TIMEOUT })
.toEqual(0)
})
test.skip('should show drawer for input field when enableRichText is false', async () => {
const uploads3URL = new AdminUrlUtil(serverURL, 'uploads3')
await page.goto(uploads3URL.create)
// create file in uploads 3 collection
await page
.locator('.file-field__upload input[type="file"]')
.setInputFiles(path.resolve(dirname, './collections/Upload/payload.jpg'))
await expect(page.locator('.file-field .file-field__filename')).toContainText('payload.jpg')
await page.locator('#action-save').click()
await wait(200)
// open drawer
await openDocDrawer(page, '.field-type.upload .list-drawer__toggler')
// check title
await expect(page.locator('.list-drawer__header-text')).toContainText('Uploads 3')
})
})

View File

@@ -10,12 +10,8 @@ import type { Config } from './payload-types.js'
import {
ensureAutoLoginAndCompilationIsDone,
exactText,
initPageConsoleErrorCatch,
navigateToListCellLink,
openDocDrawer,
saveDocAndAssert,
switchTab,
} from '../helpers.js'
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js'
@@ -23,16 +19,7 @@ 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 { numberDoc } from './collections/Number/shared.js'
import { textDoc } from './collections/Text/shared.js'
import {
arrayFieldsSlug,
blockFieldsSlug,
collapsibleFieldsSlug,
pointFieldsSlug,
tabsFieldsSlug,
textFieldsSlug,
} from './slugs.js'
import { arrayFieldsSlug, blockFieldsSlug, collapsibleFieldsSlug } from './slugs.js'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
@@ -78,242 +65,6 @@ describe('fields', () => {
await ensureAutoLoginAndCompilationIsDone({ page, serverURL })
})
describe('text', () => {
let url: AdminUrlUtil
beforeAll(() => {
url = new AdminUrlUtil(serverURL, textFieldsSlug)
})
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 hide field in column selector when admin.disableListColumn', async () => {
await page.goto(url.list)
await page.locator('.list-controls__toggle-columns').click()
await expect(page.locator('.column-selector')).toBeVisible()
// Check if "Disable List Column Text" is not present in the column options
await expect(
page.locator(`.column-selector .column-selector__column`, {
hasText: exactText('Disable List Column Text'),
}),
).toBeHidden()
})
test('should show field in filter when admin.disableListColumn is true', async () => {
await page.goto(url.list)
await page.locator('.list-controls__toggle-where').click()
await page.locator('.where-builder__add-first-filter').click()
const initialField = page.locator('.condition__field')
await initialField.click()
await expect(
initialField.locator(`.rs__menu-list:has-text("Disable List Column Text")`),
).toBeVisible()
})
test('should display field in list view column selector if admin.disableListColumn is false and admin.disableListFilter is true', async () => {
await page.goto(url.list)
await page.locator('.list-controls__toggle-columns').click()
await expect(page.locator('.column-selector')).toBeVisible()
// Check if "Disable List Filter Text" is present in the column options
await expect(
page.locator(`.column-selector .column-selector__column`, {
hasText: exactText('Disable List Filter Text'),
}),
).toBeVisible()
})
test('should hide field in filter when admin.disableListFilter is true', async () => {
await page.goto(url.list)
await page.locator('.list-controls__toggle-where').click()
await page.locator('.where-builder__add-first-filter').click()
const initialField = page.locator('.condition__field')
await initialField.click()
await expect(
initialField.locator(`.rs__option :has-text("Disable List Filter Text")`),
).toBeHidden()
})
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('<No Text en>')
})
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(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')
})
test('should render custom label', async () => {
await page.goto(url.create)
const label = page.locator('label.custom-label[for="field-customLabel"]')
await expect(label).toHaveText('#label')
})
test('should render custom error', async () => {
await page.goto(url.create)
const input = page.locator('input[id="field-customError"]')
await input.fill('ab')
await expect(input).toHaveValue('ab')
const error = page.locator('.custom-error:near(input[id="field-customError"])')
const submit = page.locator('button[type="button"][id="action-save"]')
await submit.click()
await expect(error).toHaveText('#custom-error')
})
test('should render beforeInput and afterInput', async () => {
await page.goto(url.create)
const input = page.locator('input[id="field-beforeAndAfterInput"]')
const prevSibling = await input.evaluateHandle((el) => {
return el.previousElementSibling
})
const prevSiblingText = await page.evaluate((el) => el.textContent, prevSibling)
expect(prevSiblingText).toEqual('#before-input')
const nextSibling = await input.evaluateHandle((el) => {
return el.nextElementSibling
})
const nextSiblingText = await page.evaluate((el) => el.textContent, nextSibling)
expect(nextSiblingText).toEqual('#after-input')
})
test('should create hasMany with multiple texts', async () => {
const input = 'five'
const furtherInput = 'six'
await page.goto(url.create)
const requiredField = page.locator('#field-text')
const field = page.locator('.field-hasMany')
await requiredField.fill(String(input))
await field.click()
await page.keyboard.type(input)
await page.keyboard.press('Enter')
await page.keyboard.type(furtherInput)
await page.keyboard.press('Enter')
await saveDocAndAssert(page)
await expect(field.locator('.rs__value-container')).toContainText(input)
await expect(field.locator('.rs__value-container')).toContainText(furtherInput)
})
})
describe('number', () => {
let url: AdminUrlUtil
beforeAll(() => {
url = new AdminUrlUtil(serverURL, 'number-fields')
})
test('should display field in list view', async () => {
await page.goto(url.list)
const textCell = page.locator('.row-1 .cell-number')
await expect(textCell).toHaveText(String(numberDoc.number))
})
test('should filter Number fields in the collection view - greaterThanOrEqual', async () => {
await page.goto(url.list)
// should have 3 entries
await expect(page.locator('table >> tbody >> tr')).toHaveCount(3)
// open the filter options
await page.locator('.list-controls__toggle-where').click()
await expect(page.locator('.list-controls__where.rah-static--height-auto')).toBeVisible()
await page.locator('.where-builder__add-first-filter').click()
const initialField = page.locator('.condition__field')
const operatorField = page.locator('.condition__operator')
const valueField = page.locator('.condition__value >> input')
// select Number field to filter on
await initialField.click()
const initialFieldOptions = initialField.locator('.rs__option')
await initialFieldOptions.locator('text=number').first().click()
await expect(initialField.locator('.rs__single-value')).toContainText('Number')
// select >= operator
await operatorField.click()
const operatorOptions = operatorField.locator('.rs__option')
await operatorOptions.last().click()
await expect(operatorField.locator('.rs__single-value')).toContainText(
'is greater than or equal to',
)
// enter value of 3
await valueField.fill('3')
await expect(valueField).toHaveValue('3')
await wait(300)
// should have 2 entries after filtering
await expect(page.locator('table >> tbody >> tr')).toHaveCount(2)
})
test('should create', async () => {
const input = 5
await page.goto(url.create)
const field = page.locator('#field-number')
await field.fill(String(input))
await saveDocAndAssert(page)
await expect(field).toHaveValue(String(input))
})
test('should create hasMany', async () => {
const input = 5
await page.goto(url.create)
const field = page.locator('.field-hasMany')
await field.click()
await page.keyboard.type(String(input))
await page.keyboard.press('Enter')
await saveDocAndAssert(page)
await expect(field.locator('.rs__value-container')).toContainText(String(input))
})
test('should bypass min rows validation when no rows present and field is not required', async () => {
await page.goto(url.create)
await saveDocAndAssert(page)
await expect(page.locator('.Toastify')).toContainText('successfully')
})
test('should fail min rows validation when rows are present', async () => {
const input = 5
await page.goto(url.create)
await page.locator('.field-withMinRows').click()
await page.keyboard.type(String(input))
await page.keyboard.press('Enter')
await page.click('#action-save', { delay: 100 })
await expect(page.locator('.Toastify')).toContainText(
'The following field is invalid: withMinRows',
)
})
})
describe('indexed', () => {
let url: AdminUrlUtil
@@ -449,107 +200,6 @@ describe('fields', () => {
})
})
describe('point', () => {
let url: AdminUrlUtil
let filledGroupPoint
let emptyGroupPoint
beforeEach(async () => {
url = new AdminUrlUtil(serverURL, pointFieldsSlug)
filledGroupPoint = await payload.create({
collection: pointFieldsSlug,
data: {
group: { point: [4, 2] },
localized: [4, 2],
point: [5, 5],
},
})
emptyGroupPoint = await payload.create({
collection: pointFieldsSlug,
data: {
group: {},
localized: [3, -2],
point: [5, 5],
},
})
})
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)
await expect(longField).toHaveAttribute('value', '9')
await expect(latField).toHaveAttribute('value', '-2')
await expect(localizedLongField).toHaveAttribute('value', '1')
await expect(localizedLatField).toHaveAttribute('value', '-1')
await expect(groupLongitude).toHaveAttribute('value', '3')
await expect(groupLatField).toHaveAttribute('value', '-8')
})
test('should update point', async () => {
await page.goto(url.edit(emptyGroupPoint.id))
await page.waitForURL(`**/${emptyGroupPoint.id}`)
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('2')
const localizedLatField = page.locator('#field-latitude-localized')
await localizedLatField.fill('-2')
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)
await expect(longField).toHaveAttribute('value', '9')
await expect(latField).toHaveAttribute('value', '-2')
await expect(localizedLongField).toHaveAttribute('value', '2')
await expect(localizedLatField).toHaveAttribute('value', '-2')
await expect(groupLongitude).toHaveAttribute('value', '3')
await expect(groupLatField).toHaveAttribute('value', '-8')
})
test('should be able to clear a value point', async () => {
await page.goto(url.edit(filledGroupPoint.id))
await page.waitForURL(`**/${filledGroupPoint.id}`)
const groupLongitude = page.locator('#field-longitude-group__point')
await groupLongitude.fill('')
const groupLatField = page.locator('#field-latitude-group__point')
await groupLatField.fill('')
await saveDocAndAssert(page)
await expect(groupLongitude).toHaveAttribute('value', '')
await expect(groupLatField).toHaveAttribute('value', '')
})
})
describe('collapsible', () => {
let url: AdminUrlUtil
beforeAll(() => {
@@ -636,317 +286,6 @@ describe('fields', () => {
})
})
describe('tabs', () => {
let url: AdminUrlUtil
beforeAll(() => {
url = new AdminUrlUtil(serverURL, tabsFieldsSlug)
})
test('should fill and retain a new value within a tab while switching tabs', async () => {
const textInRowValue = 'hello'
const numberInRowValue = '23'
const jsonValue = '{ "foo": "bar"}'
await page.goto(url.create)
await page.waitForURL(url.create)
await switchTab(page, '.tabs-field__tab-button:has-text("Tab with Row")')
await page.locator('#field-textInRow').fill(textInRowValue)
await page.locator('#field-numberInRow').fill(numberInRowValue)
await page.locator('.json-field .inputarea').fill(jsonValue)
await wait(300)
await switchTab(page, '.tabs-field__tab-button:has-text("Tab with Array")')
await switchTab(page, '.tabs-field__tab-button:has-text("Tab with Row")')
await expect(page.locator('#field-textInRow')).toHaveValue(textInRowValue)
await expect(page.locator('#field-numberInRow')).toHaveValue(numberInRowValue)
await expect(page.locator('.json-field .lines-content')).toContainText(jsonValue)
})
test('should retain updated values within tabs while switching between tabs', async () => {
const textInRowValue = 'new value'
const jsonValue = '{ "new": "value"}'
await page.goto(url.list)
await navigateToListCellLink(page)
// Go to Row tab, update the value
await switchTab(page, '.tabs-field__tab-button:has-text("Tab with Row")')
await page.locator('#field-textInRow').fill(textInRowValue)
await page.locator('.json-field .inputarea').fill(jsonValue)
await wait(500)
// Go to Array tab, then back to Row. Make sure new value is still there
await switchTab(page, '.tabs-field__tab-button:has-text("Tab with Array")')
await switchTab(page, '.tabs-field__tab-button:has-text("Tab with Row")')
await expect(page.locator('#field-textInRow')).toHaveValue(textInRowValue)
await expect(page.locator('.json-field .lines-content')).toContainText(jsonValue)
// Go to array tab, save the doc
await switchTab(page, '.tabs-field__tab-button:has-text("Tab with Array")')
await saveDocAndAssert(page)
// Go back to row tab, make sure the new value is still present
await switchTab(page, '.tabs-field__tab-button:has-text("Tab with Row")')
await expect(page.locator('#field-textInRow')).toHaveValue(textInRowValue)
})
test('should render array data within unnamed tabs', async () => {
await page.goto(url.list)
await navigateToListCellLink(page)
await switchTab(page, '.tabs-field__tab-button:has-text("Tab with Array")')
await expect(page.locator('#field-array__0__text')).toHaveValue("Hello, I'm the first row")
})
test('should render array data within named tabs', async () => {
await page.goto(url.list)
await navigateToListCellLink(page)
await switchTab(page, '.tabs-field__tab-button:nth-child(5)')
await expect(page.locator('#field-tab__array__0__text')).toHaveValue(
"Hello, I'm the first row, in a named tab",
)
})
})
describe('date', () => {
let url: AdminUrlUtil
beforeAll(() => {
url = new AdminUrlUtil(serverURL, 'date-fields')
})
test('should display formatted date in list view table cell', async () => {
await page.goto(url.list)
const formattedDateCell = page.locator('.row-1 .cell-timeOnly')
await expect(formattedDateCell).toContainText(' Aug ')
const notFormattedDateCell = page.locator('.row-1 .cell-default')
await expect(notFormattedDateCell).toContainText('August')
})
test('should display formatted date in useAsTitle', async () => {
await page.goto(url.list)
await page.locator('.row-1 .cell-default a').click()
await expect(page.locator('.doc-header__title.render-title')).toContainText('August')
})
test('should clear date', async () => {
await page.goto(url.create)
const dateField = page.locator('#field-default input')
await expect(dateField).toBeVisible()
await dateField.fill('02/07/2023')
await expect(dateField).toHaveValue('02/07/2023')
await saveDocAndAssert(page)
const clearButton = page.locator('#field-default .date-time-picker__clear-button')
await expect(clearButton).toBeVisible()
await clearButton.click()
await expect(dateField).toHaveValue('')
})
describe('localized dates', () => {
describe('EST', () => {
test.use({
geolocation: {
latitude: 42.3314,
longitude: -83.0458,
},
timezoneId: 'America/Detroit',
})
test('create EST day only date', async () => {
await page.goto(url.create)
await page.waitForURL(`**/${url.create}`)
const dateField = page.locator('#field-default input')
// enter date in default date field
await dateField.fill('02/07/2023')
await saveDocAndAssert(page)
// get the ID of the doc
const routeSegments = page.url().split('/')
const id = routeSegments.pop()
// fetch the doc (need the date string from the DB)
const { doc } = await client.findByID({ id, auth: true, slug: 'date-fields' })
expect(doc.default).toEqual('2023-02-07T12:00:00.000Z')
})
})
describe('PST', () => {
test.use({
geolocation: {
latitude: 37.774929,
longitude: -122.419416,
},
timezoneId: 'America/Los_Angeles',
})
test('create PDT day only date', async () => {
await page.goto(url.create)
await page.waitForURL(`**/${url.create}`)
const dateField = page.locator('#field-default input')
// enter date in default date field
await dateField.fill('02/07/2023')
await saveDocAndAssert(page)
// get the ID of the doc
const routeSegments = page.url().split('/')
const id = routeSegments.pop()
// fetch the doc (need the date string from the DB)
const { doc } = await client.findByID({ id, auth: true, slug: 'date-fields' })
expect(doc.default).toEqual('2023-02-07T12:00:00.000Z')
})
})
describe('ST', () => {
test.use({
geolocation: {
latitude: -14.5994,
longitude: -171.857,
},
timezoneId: 'Pacific/Apia',
})
test('create ST day only date', async () => {
await page.goto(url.create)
await page.waitForURL(`**/${url.create}`)
const dateField = page.locator('#field-default input')
// enter date in default date field
await dateField.fill('02/07/2023')
await saveDocAndAssert(page)
// get the ID of the doc
const routeSegments = page.url().split('/')
const id = routeSegments.pop()
// fetch the doc (need the date string from the DB)
const { doc } = await client.findByID({ id, auth: true, slug: 'date-fields' })
expect(doc.default).toEqual('2023-02-07T12:00:00.000Z')
})
})
})
})
describe('upload', () => {
let url: AdminUrlUtil
beforeAll(() => {
url = new AdminUrlUtil(serverURL, 'uploads')
})
async function uploadImage() {
await page.goto(url.create)
// create a jpg upload
await page
.locator('.file-field__upload input[type="file"]')
.setInputFiles(path.resolve(dirname, './collections/Upload/payload.jpg'))
await expect(page.locator('.file-field .file-field__filename')).toHaveValue('payload.jpg')
await saveDocAndAssert(page)
}
// eslint-disable-next-line playwright/expect-expect
test('should upload files', async () => {
await uploadImage()
})
// test that the image renders
test('should render uploaded image', async () => {
await uploadImage()
await expect(page.locator('.file-field .file-details img')).toHaveAttribute(
'src',
'/api/uploads/file/payload-1.jpg',
)
})
test('should upload using the document drawer', async () => {
await uploadImage()
await wait(1000)
// Open the media drawer and create a png upload
await openDocDrawer(page, '.field-type.upload .upload__toggler.doc-drawer__toggler')
await page
.locator('[id^=doc-drawer_uploads_1_] .file-field__upload input[type="file"]')
.setInputFiles(path.resolve(dirname, './uploads/payload.png'))
await expect(
page.locator('[id^=doc-drawer_uploads_1_] .file-field__upload .file-field__filename'),
).toHaveValue('payload.png')
await page.locator('[id^=doc-drawer_uploads_1_] #action-save').click()
await expect(page.locator('.Toastify')).toContainText('successfully')
// Assert that the media field has the png upload
await expect(
page.locator('.field-type.upload .file-details .file-meta__url a'),
).toHaveAttribute('href', '/api/uploads/file/payload-1.png')
await expect(
page.locator('.field-type.upload .file-details .file-meta__url a'),
).toContainText('payload-1.png')
await expect(page.locator('.field-type.upload .file-details img')).toHaveAttribute(
'src',
'/api/uploads/file/payload-1.png',
)
await saveDocAndAssert(page)
})
test('should clear selected upload', async () => {
await uploadImage()
await wait(1000) // TODO: Fix this. Need to wait a bit until the form in the drawer mounted, otherwise values sometimes disappear. This is an issue for all drawers
await openDocDrawer(page, '.field-type.upload .upload__toggler.doc-drawer__toggler')
await wait(1000)
await page
.locator('[id^=doc-drawer_uploads_1_] .file-field__upload input[type="file"]')
.setInputFiles(path.resolve(dirname, './uploads/payload.png'))
await expect(
page.locator('[id^=doc-drawer_uploads_1_] .file-field__upload .file-field__filename'),
).toHaveValue('payload.png')
await page.locator('[id^=doc-drawer_uploads_1_] #action-save').click()
await expect(page.locator('.Toastify')).toContainText('successfully')
await page.locator('.field-type.upload .file-details__remove').click()
})
test('should select using the list drawer and restrict mimetype based on filterOptions', async () => {
await uploadImage()
await openDocDrawer(page, '.field-type.upload .upload__toggler.list-drawer__toggler')
const jpgImages = page.locator('[id^=list-drawer_1_] .upload-gallery img[src$=".jpg"]')
await expect
.poll(async () => await jpgImages.count(), { timeout: POLL_TOPASS_TIMEOUT })
.toEqual(0)
})
test.skip('should show drawer for input field when enableRichText is false', async () => {
const uploads3URL = new AdminUrlUtil(serverURL, 'uploads3')
await page.goto(uploads3URL.create)
// create file in uploads 3 collection
await page
.locator('.file-field__upload input[type="file"]')
.setInputFiles(path.resolve(dirname, './collections/Upload/payload.jpg'))
await expect(page.locator('.file-field .file-field__filename')).toContainText('payload.jpg')
await page.locator('#action-save').click()
await wait(200)
// open drawer
await openDocDrawer(page, '.field-type.upload .list-drawer__toggler')
// check title
await expect(page.locator('.list-drawer__header-text')).toContainText('Uploads 3')
})
})
describe('row', () => {
let url: AdminUrlUtil
beforeAll(() => {

View File

@@ -37,7 +37,7 @@
],
"paths": {
"@payload-config": [
"./test/access-control/config.ts"
"./test/_community/config.ts"
],
"@payloadcms/live-preview": [
"./packages/live-preview/src"