ci: split up test suites (#6415)
This commit is contained in:
12
.github/workflows/main.yml
vendored
12
.github/workflows/main.yml
vendored
@@ -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
|
||||
|
||||
@@ -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> {
|
||||
803
test/admin/e2e/2/e2e.spec.ts
Normal file
803
test/admin/e2e/2/e2e.spec.ts
Normal 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>
|
||||
}
|
||||
187
test/fields/collections/Date/e2e.spec.ts
Normal file
187
test/fields/collections/Date/e2e.spec.ts
Normal 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')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
444
test/fields/collections/Lexical/e2e/main/e2e.spec.ts
Normal file
444
test/fields/collections/Lexical/e2e/main/e2e.spec.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
158
test/fields/collections/Number/e2e.spec.ts
Normal file
158
test/fields/collections/Number/e2e.spec.ts
Normal 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',
|
||||
)
|
||||
})
|
||||
})
|
||||
164
test/fields/collections/Point/e2e.spec.ts
Normal file
164
test/fields/collections/Point/e2e.spec.ts
Normal 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', '')
|
||||
})
|
||||
})
|
||||
142
test/fields/collections/Tabs/e2e.spec.ts
Normal file
142
test/fields/collections/Tabs/e2e.spec.ts
Normal 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",
|
||||
)
|
||||
})
|
||||
})
|
||||
208
test/fields/collections/Text/e2e.spec.ts
Normal file
208
test/fields/collections/Text/e2e.spec.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
176
test/fields/collections/Upload/e2e.spec.ts
Normal file
176
test/fields/collections/Upload/e2e.spec.ts
Normal 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')
|
||||
})
|
||||
})
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
],
|
||||
"paths": {
|
||||
"@payload-config": [
|
||||
"./test/access-control/config.ts"
|
||||
"./test/_community/config.ts"
|
||||
],
|
||||
"@payloadcms/live-preview": [
|
||||
"./packages/live-preview/src"
|
||||
|
||||
Reference in New Issue
Block a user