test: semantically splits admin e2e (#10213)
Improves the admin e2e test splitting by grouping them by type with semantic names as opposed to numerically. This will provide much needed clarity to exactly _where_ new admin tests should be written and help to quickly distinguish the areas of failure within the CI overview.
This commit is contained in:
6
.github/workflows/main.yml
vendored
6
.github/workflows/main.yml
vendored
@@ -278,9 +278,9 @@ jobs:
|
|||||||
suite:
|
suite:
|
||||||
- _community
|
- _community
|
||||||
- access-control
|
- access-control
|
||||||
- admin__e2e__1
|
- admin__e2e__general
|
||||||
- admin__e2e__2
|
- admin__e2e__list-view
|
||||||
- admin__e2e__3
|
- admin__e2e__document-view
|
||||||
- admin-root
|
- admin-root
|
||||||
- auth
|
- auth
|
||||||
- auth-basic
|
- auth-basic
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { TEST_TIMEOUT_LONG } from '../playwright.config.js'
|
|||||||
const filename = fileURLToPath(import.meta.url)
|
const filename = fileURLToPath(import.meta.url)
|
||||||
const dirname = path.dirname(filename)
|
const dirname = path.dirname(filename)
|
||||||
|
|
||||||
test.describe('Admin Panel', () => {
|
test.describe('Community', () => {
|
||||||
let page: Page
|
let page: Page
|
||||||
let url: AdminUrlUtil
|
let url: AdminUrlUtil
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ const dirname = path.dirname(filename)
|
|||||||
|
|
||||||
const { beforeAll, describe } = test
|
const { beforeAll, describe } = test
|
||||||
let payload: PayloadTestSDK<Config>
|
let payload: PayloadTestSDK<Config>
|
||||||
describe('access control', () => {
|
describe('Access Control', () => {
|
||||||
let page: Page
|
let page: Page
|
||||||
let url: AdminUrlUtil
|
let url: AdminUrlUtil
|
||||||
let restrictedUrl: AdminUrlUtil
|
let restrictedUrl: AdminUrlUtil
|
||||||
|
|||||||
@@ -1,862 +0,0 @@
|
|||||||
import type { Page } from '@playwright/test'
|
|
||||||
|
|
||||||
import { expect, test } from '@playwright/test'
|
|
||||||
import { mapAsync } from 'payload'
|
|
||||||
import * as qs from 'qs-esm'
|
|
||||||
|
|
||||||
import type { Config, Geo, Post } from '../../payload-types.js'
|
|
||||||
|
|
||||||
import {
|
|
||||||
ensureCompilationIsDone,
|
|
||||||
exactText,
|
|
||||||
getRoutes,
|
|
||||||
initPageConsoleErrorCatch,
|
|
||||||
openDocDrawer,
|
|
||||||
openNav,
|
|
||||||
} from '../../../helpers.js'
|
|
||||||
import { AdminUrlUtil } from '../../../helpers/adminUrlUtil.js'
|
|
||||||
import { initPayloadE2ENoConfig } from '../../../helpers/initPayloadE2ENoConfig.js'
|
|
||||||
import { customAdminRoutes } from '../../shared.js'
|
|
||||||
import { customViews1CollectionSlug, geoCollectionSlug, postsCollectionSlug } from '../../slugs.js'
|
|
||||||
|
|
||||||
const { beforeAll, beforeEach, describe } = test
|
|
||||||
|
|
||||||
const title = 'Title'
|
|
||||||
const description = 'Description'
|
|
||||||
|
|
||||||
let payload: PayloadTestSDK<Config>
|
|
||||||
|
|
||||||
import { goToFirstCell } from 'helpers/e2e/navigateToDoc.js'
|
|
||||||
import { openListColumns } from 'helpers/e2e/openListColumns.js'
|
|
||||||
import { openListFilters } from 'helpers/e2e/openListFilters.js'
|
|
||||||
import { toggleColumn } from 'helpers/e2e/toggleColumn.js'
|
|
||||||
import path from 'path'
|
|
||||||
import { wait } from 'payload/shared'
|
|
||||||
import { fileURLToPath } from 'url'
|
|
||||||
|
|
||||||
import type { PayloadTestSDK } from '../../../helpers/sdk/index.js'
|
|
||||||
|
|
||||||
import { reorderColumns } from '../../../helpers/e2e/reorderColumns.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 baseListFiltersUrl: AdminUrlUtil
|
|
||||||
let customViewsUrl: AdminUrlUtil
|
|
||||||
|
|
||||||
let serverURL: string
|
|
||||||
let adminRoutes: ReturnType<typeof getRoutes>
|
|
||||||
|
|
||||||
beforeAll(async ({ browser }, testInfo) => {
|
|
||||||
const prebuild = false // 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)
|
|
||||||
baseListFiltersUrl = new AdminUrlUtil(serverURL, 'base-list-filters')
|
|
||||||
customViewsUrl = new AdminUrlUtil(serverURL, customViews1CollectionSlug)
|
|
||||||
|
|
||||||
const context = await browser.newContext()
|
|
||||||
page = await context.newPage()
|
|
||||||
initPageConsoleErrorCatch(page)
|
|
||||||
|
|
||||||
await ensureCompilationIsDone({ customAdminRoutes, page, serverURL })
|
|
||||||
|
|
||||||
adminRoutes = getRoutes({ customAdminRoutes })
|
|
||||||
})
|
|
||||||
beforeEach(async () => {
|
|
||||||
await reInitializeDB({
|
|
||||||
serverURL,
|
|
||||||
snapshotKey: 'adminTests',
|
|
||||||
})
|
|
||||||
|
|
||||||
await ensureCompilationIsDone({ customAdminRoutes, page, serverURL })
|
|
||||||
})
|
|
||||||
|
|
||||||
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('list view descriptions', () => {
|
|
||||||
test('should render static collection descriptions', async () => {
|
|
||||||
await page.goto(postsUrl.list)
|
|
||||||
await expect(
|
|
||||||
page.locator('.view-description', {
|
|
||||||
hasText: exactText('This is a custom collection description.'),
|
|
||||||
}),
|
|
||||||
).toBeVisible()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('should render dynamic collection description components', async () => {
|
|
||||||
await page.goto(customViewsUrl.list)
|
|
||||||
await expect(
|
|
||||||
page.locator('.view-description', {
|
|
||||||
hasText: exactText('This is a custom view description component.'),
|
|
||||||
}),
|
|
||||||
).toBeVisible()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
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(new RegExp(`${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('search should persist through browser back button', async () => {
|
|
||||||
const url = `${postsUrl.list}?limit=10&page=1&search=post1`
|
|
||||||
await page.goto(url)
|
|
||||||
await page.waitForURL(url)
|
|
||||||
await expect(page.locator('#search-filter-input')).toHaveValue('post1')
|
|
||||||
await goToFirstCell(page, postsUrl)
|
|
||||||
await page.goBack()
|
|
||||||
await wait(1000) // wait one second to ensure that the new view does not accidentally reset the search
|
|
||||||
await page.waitForURL(url)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('search should not persist between navigation', async () => {
|
|
||||||
const url = `${postsUrl.list}?limit=10&page=1&search=test`
|
|
||||||
await page.goto(url)
|
|
||||||
await page.waitForURL(url)
|
|
||||||
|
|
||||||
await expect(page.locator('#search-filter-input')).toHaveValue('test')
|
|
||||||
|
|
||||||
await page.locator('.nav-toggler.template-default__nav-toggler').click()
|
|
||||||
await expect(page.locator('#nav-uploads')).toContainText('Uploads')
|
|
||||||
|
|
||||||
const uploadsUrl = await page.locator('#nav-uploads').getAttribute('href')
|
|
||||||
await page.goto(serverURL + uploadsUrl)
|
|
||||||
await page.waitForURL(serverURL + uploadsUrl)
|
|
||||||
|
|
||||||
await expect(page.locator('#search-filter-input')).toHaveValue('')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('should toggle columns', async () => {
|
|
||||||
const columnCountLocator = 'table > thead > tr > th'
|
|
||||||
await createPost()
|
|
||||||
await openListColumns(page, {})
|
|
||||||
const numberOfColumns = await page.locator(columnCountLocator).count()
|
|
||||||
await expect(page.locator('.column-selector')).toBeVisible()
|
|
||||||
await expect(page.locator('table > thead > tr > th:nth-child(2)')).toHaveText('ID')
|
|
||||||
await toggleColumn(page, { columnLabel: 'ID', targetState: 'off' })
|
|
||||||
await page.locator('#heading-id').waitFor({ state: 'detached' })
|
|
||||||
await page.locator('.cell-id').first().waitFor({ state: 'detached' })
|
|
||||||
await expect(page.locator(columnCountLocator)).toHaveCount(numberOfColumns - 1)
|
|
||||||
await expect(page.locator('table > thead > tr > th:nth-child(2)')).toHaveText('Number')
|
|
||||||
await toggleColumn(page, { columnLabel: 'ID', targetState: 'on' })
|
|
||||||
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}`,
|
|
||||||
)
|
|
||||||
|
|
||||||
await page.locator('.list-controls__toggle-columns').click()
|
|
||||||
await expect(page.locator('.list-controls__columns.rah-static--height-auto')).toBeVisible()
|
|
||||||
|
|
||||||
await page
|
|
||||||
.locator('.column-selector .column-selector__column', {
|
|
||||||
hasText: exactText('ID'),
|
|
||||||
})
|
|
||||||
.click()
|
|
||||||
|
|
||||||
await page.locator('#heading-id').waitFor({ state: 'detached' })
|
|
||||||
await page.locator('.cell-id').first().waitFor({ state: 'detached' })
|
|
||||||
|
|
||||||
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 openListFilters(page, {})
|
|
||||||
|
|
||||||
await page.locator('.where-builder__add-first-filter').click()
|
|
||||||
|
|
||||||
const conditionField = page.locator('.condition__field')
|
|
||||||
await conditionField.click()
|
|
||||||
const dropdownFieldOption = conditionField.locator('.rs__option', {
|
|
||||||
hasText: exactText('ID'),
|
|
||||||
})
|
|
||||||
await dropdownFieldOption.click()
|
|
||||||
await expect(page.locator('.condition__field')).toContainText('ID')
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
const tableRows = page.locator(tableRowLocator)
|
|
||||||
|
|
||||||
await expect(tableRows).toHaveCount(1)
|
|
||||||
const firstId = page.locator(tableRowLocator).first().locator('.cell-id')
|
|
||||||
await expect(firstId).toHaveText(`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 openListFilters(page, {})
|
|
||||||
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 dropdownFieldOption = filterField.locator('.rs__option', {
|
|
||||||
hasText: exactText('Status'),
|
|
||||||
})
|
|
||||||
await dropdownFieldOption.click()
|
|
||||||
await expect(filterField).toContainText('Status')
|
|
||||||
|
|
||||||
// expect operator & value field to reset (be empty)
|
|
||||||
await expect(operatorField.locator('.rs__placeholder')).toContainText('Select a value')
|
|
||||||
await expect(page.locator('.condition__value input')).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)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('should reset page when filters are applied', async () => {
|
|
||||||
await deleteAllPosts()
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
Array.from({ length: 12 }, async (_, i) => {
|
|
||||||
if (i < 6) {
|
|
||||||
await createPost()
|
|
||||||
} else {
|
|
||||||
await createPost({ title: 'test' })
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
await page.reload()
|
|
||||||
|
|
||||||
const tableItems = page.locator(tableRowLocator)
|
|
||||||
|
|
||||||
await expect(tableItems).toHaveCount(10)
|
|
||||||
await expect(page.locator('.collection-list__page-info')).toHaveText('1-10 of 12')
|
|
||||||
await expect(page.locator('.per-page')).toContainText('Per Page: 10')
|
|
||||||
await page.goto(`${postsUrl.list}?limit=10&page=2`)
|
|
||||||
await openListFilters(page, {})
|
|
||||||
await page.locator('.where-builder__add-first-filter').click()
|
|
||||||
await page.locator('.condition__field .rs__control').click()
|
|
||||||
const options = page.locator('.rs__option')
|
|
||||||
await options.locator('text=Tab 1 > Title').click()
|
|
||||||
await page.locator('.condition__operator .rs__control').click()
|
|
||||||
await options.locator('text=equals').click()
|
|
||||||
await page.locator('.condition__value input').fill('test')
|
|
||||||
await expect(page.locator('.collection-list__page-info')).toHaveText('1-6 of 6')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('table columns', () => {
|
|
||||||
test('should drag to reorder columns and save to preferences', async () => {
|
|
||||||
await createPost()
|
|
||||||
|
|
||||||
await reorderColumns(page, { fromColumn: 'Number', toColumn: 'ID' })
|
|
||||||
|
|
||||||
// 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(page, { fromColumn: 'Number', toColumn: 'ID' })
|
|
||||||
|
|
||||||
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-header__select-collection.react-select',
|
|
||||||
)
|
|
||||||
|
|
||||||
// select the "Post" collection
|
|
||||||
await collectionSelector.click()
|
|
||||||
await page
|
|
||||||
.locator(
|
|
||||||
'[id^=list-drawer_1_] .list-header__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-header__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-header__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-header__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-header__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(new RegExp(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 tableItems = page.locator(tableRowLocator)
|
|
||||||
await expect(tableItems).toHaveCount(10)
|
|
||||||
await expect(page.locator('.collection-list__page-info')).toHaveText('1-10 of 11')
|
|
||||||
await expect(page.locator('.per-page')).toContainText('Per Page: 10')
|
|
||||||
await page.locator('.paginator button').nth(1).click()
|
|
||||||
await expect.poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT }).toContain('page=2')
|
|
||||||
await expect(tableItems).toHaveCount(1)
|
|
||||||
await page.locator('.paginator button').nth(0).click()
|
|
||||||
await expect.poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT }).toContain('page=1')
|
|
||||||
await expect(tableItems).toHaveCount(10)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('should paginate and maintain perPage', async () => {
|
|
||||||
await deleteAllPosts()
|
|
||||||
|
|
||||||
await mapAsync([...Array(26)], async () => {
|
|
||||||
await createPost()
|
|
||||||
})
|
|
||||||
|
|
||||||
await page.reload()
|
|
||||||
const tableItems = page.locator(tableRowLocator)
|
|
||||||
await expect(tableItems).toHaveCount(10)
|
|
||||||
await expect(page.locator('.collection-list__page-info')).toHaveText('1-10 of 26')
|
|
||||||
await expect(page.locator('.per-page')).toContainText('Per Page: 10')
|
|
||||||
await page.locator('.per-page .popup-button').click()
|
|
||||||
|
|
||||||
await page
|
|
||||||
.locator('.per-page button.per-page__button', {
|
|
||||||
hasText: '25',
|
|
||||||
})
|
|
||||||
.click()
|
|
||||||
|
|
||||||
await expect(tableItems).toHaveCount(25)
|
|
||||||
await expect(page.locator('.per-page .per-page__base-button')).toContainText('Per Page: 25')
|
|
||||||
await page.locator('.paginator button').nth(1).click()
|
|
||||||
await expect.poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT }).toContain('page=2')
|
|
||||||
await expect(tableItems).toHaveCount(1)
|
|
||||||
await expect(page.locator('.per-page')).toContainText('Per Page: 25')
|
|
||||||
await expect(page.locator('.collection-list__page-info')).toHaveText('26-26 of 26')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// 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')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('should sort with existing filters', async () => {
|
|
||||||
await page.goto(postsUrl.list)
|
|
||||||
await toggleColumn(page, { columnLabel: 'ID', targetState: 'off' })
|
|
||||||
await page.locator('#heading-id').waitFor({ state: 'detached' })
|
|
||||||
await page.locator('#heading-title button.sort-column__asc').click()
|
|
||||||
await page.waitForURL(/sort=title/)
|
|
||||||
|
|
||||||
const columnAfterSort = page.locator(
|
|
||||||
`.list-controls__columns .column-selector .column-selector__column`,
|
|
||||||
{
|
|
||||||
hasText: exactText('ID'),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
await expect(columnAfterSort).not.toHaveClass('column-selector__column--active')
|
|
||||||
await expect(page.locator('#heading-id')).toBeHidden()
|
|
||||||
await expect(page.locator('.cell-id')).toHaveCount(0)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('i18n', () => {
|
|
||||||
test('should display translated collections and globals config options', async () => {
|
|
||||||
await page.goto(postsUrl.list)
|
|
||||||
await expect(page.locator('#nav-posts')).toContainText('Posts')
|
|
||||||
await expect(page.locator('#nav-global-global')).toContainText('Global')
|
|
||||||
})
|
|
||||||
|
|
||||||
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 openListFilters(page, {})
|
|
||||||
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=Tab 1 > Title')).toHaveText('Tab 1 > 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('base list filters', () => {
|
|
||||||
test('should respect base list filters', async () => {
|
|
||||||
await page.goto(baseListFiltersUrl.list)
|
|
||||||
await page.waitForURL((url) => url.toString().startsWith(baseListFiltersUrl.list))
|
|
||||||
await expect(page.locator(tableRowLocator)).toHaveCount(1)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
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>
|
|
||||||
}
|
|
||||||
@@ -1,679 +0,0 @@
|
|||||||
import type { Page } from '@playwright/test'
|
|
||||||
|
|
||||||
import { expect, test } from '@playwright/test'
|
|
||||||
import { wait } from 'payload/shared'
|
|
||||||
|
|
||||||
import type { Config, Geo, Post } from '../../payload-types.js'
|
|
||||||
|
|
||||||
import {
|
|
||||||
checkBreadcrumb,
|
|
||||||
checkPageTitle,
|
|
||||||
ensureCompilationIsDone,
|
|
||||||
exactText,
|
|
||||||
getRoutes,
|
|
||||||
initPageConsoleErrorCatch,
|
|
||||||
openNav,
|
|
||||||
saveDocAndAssert,
|
|
||||||
saveDocHotkeyAndAssert,
|
|
||||||
} from '../../../helpers.js'
|
|
||||||
import { AdminUrlUtil } from '../../../helpers/adminUrlUtil.js'
|
|
||||||
import { initPayloadE2ENoConfig } from '../../../helpers/initPayloadE2ENoConfig.js'
|
|
||||||
import { customAdminRoutes } from '../../shared.js'
|
|
||||||
import {
|
|
||||||
disableDuplicateSlug,
|
|
||||||
geoCollectionSlug,
|
|
||||||
globalSlug,
|
|
||||||
group1Collection1Slug,
|
|
||||||
group1GlobalSlug,
|
|
||||||
noApiViewCollectionSlug,
|
|
||||||
noApiViewGlobalSlug,
|
|
||||||
postsCollectionSlug,
|
|
||||||
} from '../../slugs.js'
|
|
||||||
|
|
||||||
const { beforeAll, beforeEach, describe } = test
|
|
||||||
|
|
||||||
const title = 'Title'
|
|
||||||
const description = 'Description'
|
|
||||||
|
|
||||||
let payload: PayloadTestSDK<Config>
|
|
||||||
|
|
||||||
import { navigateToDoc } from 'helpers/e2e/navigateToDoc.js'
|
|
||||||
import { openDocControls } from 'helpers/e2e/openDocControls.js'
|
|
||||||
import path from 'path'
|
|
||||||
import { fileURLToPath } from 'url'
|
|
||||||
|
|
||||||
import type { PayloadTestSDK } from '../../../helpers/sdk/index.js'
|
|
||||||
|
|
||||||
import { reInitializeDB } from '../../../helpers/reInitializeDB.js'
|
|
||||||
import { TEST_TIMEOUT_LONG } from '../../../playwright.config.js'
|
|
||||||
const filename = fileURLToPath(import.meta.url)
|
|
||||||
const currentFolder = path.dirname(filename)
|
|
||||||
const dirname = path.resolve(currentFolder, '../../')
|
|
||||||
|
|
||||||
describe('admin3', () => {
|
|
||||||
let page: Page
|
|
||||||
let geoUrl: AdminUrlUtil
|
|
||||||
let postsUrl: AdminUrlUtil
|
|
||||||
let globalURL: AdminUrlUtil
|
|
||||||
let disableDuplicateURL: AdminUrlUtil
|
|
||||||
let serverURL: string
|
|
||||||
let adminRoutes: ReturnType<typeof getRoutes>
|
|
||||||
|
|
||||||
beforeAll(async ({ browser }, testInfo) => {
|
|
||||||
const prebuild = false // 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)
|
|
||||||
globalURL = new AdminUrlUtil(serverURL, globalSlug)
|
|
||||||
disableDuplicateURL = new AdminUrlUtil(serverURL, disableDuplicateSlug)
|
|
||||||
|
|
||||||
const context = await browser.newContext()
|
|
||||||
page = await context.newPage()
|
|
||||||
initPageConsoleErrorCatch(page)
|
|
||||||
|
|
||||||
await ensureCompilationIsDone({ customAdminRoutes, page, serverURL })
|
|
||||||
|
|
||||||
adminRoutes = getRoutes({ customAdminRoutes })
|
|
||||||
})
|
|
||||||
beforeEach(async () => {
|
|
||||||
await reInitializeDB({
|
|
||||||
serverURL,
|
|
||||||
snapshotKey: 'adminTests',
|
|
||||||
})
|
|
||||||
|
|
||||||
await ensureCompilationIsDone({ customAdminRoutes, page, serverURL })
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('API view', () => {
|
|
||||||
test('collection — should not show API tab when disabled in config', async () => {
|
|
||||||
await page.goto(postsUrl.collection(noApiViewCollectionSlug))
|
|
||||||
await page.locator('.collection-list .table a').click()
|
|
||||||
await expect(page.locator('.doc-tabs__tabs-container')).not.toContainText('API')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('collection — should not enable API route when disabled in config', async () => {
|
|
||||||
const collectionItems = await payload.find({
|
|
||||||
collection: noApiViewCollectionSlug,
|
|
||||||
limit: 1,
|
|
||||||
})
|
|
||||||
expect(collectionItems.docs.length).toBe(1)
|
|
||||||
await page.goto(
|
|
||||||
`${postsUrl.collection(noApiViewGlobalSlug)}/${collectionItems.docs[0].id}/api`,
|
|
||||||
)
|
|
||||||
await expect(page.locator('.not-found')).toHaveCount(1)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('collection — sidebar fields should respond to permission', async () => {
|
|
||||||
const { id } = await createPost()
|
|
||||||
await page.goto(postsUrl.edit(id))
|
|
||||||
|
|
||||||
await expect(page.locator('#field-sidebarField')).toBeDisabled()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('collection — depth field should have value 0 when empty', async () => {
|
|
||||||
const { id } = await createPost()
|
|
||||||
await page.goto(`${postsUrl.edit(id)}/api`)
|
|
||||||
|
|
||||||
const depthField = page.locator('#field-depth')
|
|
||||||
await depthField.fill('')
|
|
||||||
await expect(depthField).toHaveValue('0')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('global — should not show API tab when disabled in config', async () => {
|
|
||||||
await page.goto(postsUrl.global(noApiViewGlobalSlug))
|
|
||||||
await expect(page.locator('.doc-tabs__tabs-container')).not.toContainText('API')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('global — should not enable API route when disabled in config', async () => {
|
|
||||||
await page.goto(`${postsUrl.global(noApiViewGlobalSlug)}/api`)
|
|
||||||
await expect(page.locator('.not-found')).toHaveCount(1)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('header actions', () => {
|
|
||||||
test('should show admin level action in admin panel', async () => {
|
|
||||||
await page.goto(postsUrl.admin)
|
|
||||||
// Check if the element with the class .admin-button exists
|
|
||||||
await expect(page.locator('.app-header .admin-button')).toHaveCount(1)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('should show admin level action in collection list view', async () => {
|
|
||||||
await page.goto(`${new AdminUrlUtil(serverURL, 'geo').list}`)
|
|
||||||
await expect(page.locator('.app-header .admin-button')).toHaveCount(1)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('should show admin level action in collection edit view', async () => {
|
|
||||||
const { id } = await createGeo()
|
|
||||||
await page.goto(geoUrl.edit(id))
|
|
||||||
await expect(page.locator('.app-header .admin-button')).toHaveCount(1)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('should show collection list view level action in collection list view', async () => {
|
|
||||||
await page.goto(`${new AdminUrlUtil(serverURL, 'geo').list}`)
|
|
||||||
await expect(page.locator('.app-header .collection-list-button')).toHaveCount(1)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('should show collection edit view level action in collection edit view', async () => {
|
|
||||||
const { id } = await createGeo()
|
|
||||||
await page.goto(geoUrl.edit(id))
|
|
||||||
await expect(page.locator('.app-header .collection-edit-button')).toHaveCount(1)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('should show collection api view level action in collection api view', async () => {
|
|
||||||
const { id } = await createGeo()
|
|
||||||
await page.goto(`${geoUrl.edit(id)}/api`)
|
|
||||||
await expect(page.locator('.app-header .collection-api-button')).toHaveCount(1)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('should show global edit view level action in globals edit view', async () => {
|
|
||||||
const globalWithPreview = new AdminUrlUtil(serverURL, globalSlug)
|
|
||||||
await page.goto(globalWithPreview.global(globalSlug))
|
|
||||||
await expect(page.locator('.app-header .global-edit-button')).toHaveCount(1)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('should show global api view level action in globals api view', async () => {
|
|
||||||
const globalWithPreview = new AdminUrlUtil(serverURL, globalSlug)
|
|
||||||
await page.goto(`${globalWithPreview.global(globalSlug)}/api`)
|
|
||||||
await expect(page.locator('.app-header .global-api-button')).toHaveCount(1)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('should reset actions array when navigating from view with actions to view without actions', async () => {
|
|
||||||
await page.goto(geoUrl.list)
|
|
||||||
await expect(page.locator('.app-header .collection-list-button')).toHaveCount(1)
|
|
||||||
await page.locator('button.nav-toggler[aria-label="Open Menu"][tabindex="0"]').click()
|
|
||||||
await page.locator(`#nav-posts`).click()
|
|
||||||
await expect(page.locator('.app-header .collection-list-button')).toHaveCount(0)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('preview button', () => {
|
|
||||||
test('collection — should render preview button when `admin.preview` is set', async () => {
|
|
||||||
const collectionWithPreview = new AdminUrlUtil(serverURL, postsCollectionSlug)
|
|
||||||
await page.goto(collectionWithPreview.create)
|
|
||||||
await page.waitForURL(collectionWithPreview.create)
|
|
||||||
await page.locator('#field-title').fill(title)
|
|
||||||
await saveDocAndAssert(page)
|
|
||||||
await expect(page.locator('.btn.preview-btn')).toBeVisible()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('collection — should not render preview button when `admin.preview` is not set', async () => {
|
|
||||||
const collectionWithoutPreview = new AdminUrlUtil(serverURL, group1Collection1Slug)
|
|
||||||
await page.goto(collectionWithoutPreview.create)
|
|
||||||
await page.waitForURL(collectionWithoutPreview.create)
|
|
||||||
await page.locator('#field-title').fill(title)
|
|
||||||
await saveDocAndAssert(page)
|
|
||||||
await expect(page.locator('.btn.preview-btn')).toBeHidden()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('global — should render preview button when `admin.preview` is set', async () => {
|
|
||||||
const globalWithPreview = new AdminUrlUtil(serverURL, globalSlug)
|
|
||||||
await page.goto(globalWithPreview.global(globalSlug))
|
|
||||||
await expect(page.locator('.btn.preview-btn')).toBeVisible()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('global — should not render preview button when `admin.preview` is not set', async () => {
|
|
||||||
const globalWithoutPreview = new AdminUrlUtil(serverURL, group1GlobalSlug)
|
|
||||||
await page.goto(globalWithoutPreview.global(group1GlobalSlug))
|
|
||||||
await page.locator('#field-title').fill(title)
|
|
||||||
await saveDocAndAssert(page)
|
|
||||||
await expect(page.locator('.btn.preview-btn')).toBeHidden()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('form state', () => {
|
|
||||||
test('collection — should re-enable fields after save', async () => {
|
|
||||||
await page.goto(postsUrl.create)
|
|
||||||
await page.locator('#field-title').fill(title)
|
|
||||||
await saveDocAndAssert(page)
|
|
||||||
await expect(page.locator('#field-title')).toBeEnabled()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('global — should re-enable fields after save', async () => {
|
|
||||||
await page.goto(globalURL.global(globalSlug))
|
|
||||||
await page.locator('#field-title').fill(title)
|
|
||||||
await saveDocAndAssert(page)
|
|
||||||
await expect(page.locator('#field-title')).toBeEnabled()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('document titles', () => {
|
|
||||||
test('collection — should render fallback titles when creating new', async () => {
|
|
||||||
await page.goto(postsUrl.create)
|
|
||||||
await checkPageTitle(page, '[Untitled]')
|
|
||||||
await checkBreadcrumb(page, 'Create New')
|
|
||||||
await saveDocAndAssert(page)
|
|
||||||
expect(true).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('collection — should render `useAsTitle` field', async () => {
|
|
||||||
await page.goto(postsUrl.create)
|
|
||||||
await page.locator('#field-title')?.fill(title)
|
|
||||||
await saveDocAndAssert(page)
|
|
||||||
await wait(500)
|
|
||||||
await checkPageTitle(page, title)
|
|
||||||
await checkBreadcrumb(page, title)
|
|
||||||
expect(true).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('collection — should render `id` as `useAsTitle` fallback', async () => {
|
|
||||||
const { id } = await createPost()
|
|
||||||
const postURL = postsUrl.edit(id)
|
|
||||||
await page.goto(postURL)
|
|
||||||
await page.waitForURL(postURL)
|
|
||||||
await wait(500)
|
|
||||||
await page.locator('#field-title')?.fill('')
|
|
||||||
await expect(page.locator('.doc-header__title.render-title:has-text("ID:")')).toBeVisible()
|
|
||||||
await saveDocAndAssert(page)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('global — should render custom, localized label', async () => {
|
|
||||||
await page.goto(globalURL.global(globalSlug))
|
|
||||||
await page.waitForURL(globalURL.global(globalSlug))
|
|
||||||
await openNav(page)
|
|
||||||
const label = 'My Global Label'
|
|
||||||
const globalLabel = page.locator(`#nav-global-global`)
|
|
||||||
await expect(globalLabel).toContainText(label)
|
|
||||||
await globalLabel.click()
|
|
||||||
await checkPageTitle(page, label)
|
|
||||||
await checkBreadcrumb(page, label)
|
|
||||||
await page.locator('#field-title').fill(title)
|
|
||||||
await saveDocAndAssert(page)
|
|
||||||
await checkPageTitle(page, label)
|
|
||||||
await checkBreadcrumb(page, label)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('global — should render simple label strings', async () => {
|
|
||||||
await page.goto(postsUrl.admin)
|
|
||||||
await page.waitForURL(postsUrl.admin)
|
|
||||||
await openNav(page)
|
|
||||||
const label = 'Group Globals 1'
|
|
||||||
const globalLabel = page.locator(`#nav-global-group-globals-one`)
|
|
||||||
await expect(globalLabel).toContainText(label)
|
|
||||||
await globalLabel.click()
|
|
||||||
await checkPageTitle(page, label)
|
|
||||||
await checkBreadcrumb(page, label)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('global — should render slug in sentence case as fallback', async () => {
|
|
||||||
await page.goto(postsUrl.admin)
|
|
||||||
await page.waitForURL(postsUrl.admin)
|
|
||||||
await openNav(page)
|
|
||||||
const label = 'Group Globals Two'
|
|
||||||
const globalLabel = page.locator(`#nav-global-group-globals-two`)
|
|
||||||
await expect(globalLabel).toContainText(label)
|
|
||||||
await globalLabel.click()
|
|
||||||
await checkPageTitle(page, label)
|
|
||||||
await checkBreadcrumb(page, label)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('i18n', () => {
|
|
||||||
test('should allow changing language', async () => {
|
|
||||||
await page.goto(postsUrl.account)
|
|
||||||
|
|
||||||
const field = page.locator('.payload-settings__language .react-select')
|
|
||||||
|
|
||||||
await field.click()
|
|
||||||
const options = page.locator('.rs__option')
|
|
||||||
await options.locator('text=Español').click()
|
|
||||||
|
|
||||||
await expect(page.locator('.step-nav a').first().locator('span')).toHaveAttribute(
|
|
||||||
'title',
|
|
||||||
'Tablero',
|
|
||||||
)
|
|
||||||
|
|
||||||
await field.click()
|
|
||||||
await options.locator('text=English').click()
|
|
||||||
await field.click()
|
|
||||||
await expect(page.locator('.form-submit .btn')).toContainText('Save')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('should allow custom translation', async () => {
|
|
||||||
await page.goto(postsUrl.account)
|
|
||||||
await expect(page.locator('.step-nav a').first().locator('span')).toHaveAttribute(
|
|
||||||
'title',
|
|
||||||
'Home',
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('should allow custom translation of locale labels', async () => {
|
|
||||||
const selectOptionClass = '.localizer .popup-button-list__button'
|
|
||||||
const localizerButton = page.locator('.localizer .popup-button')
|
|
||||||
const localeListItem1 = page.locator(selectOptionClass).nth(0)
|
|
||||||
|
|
||||||
async function checkLocaleLabels(firstLabel: string, secondLabel: string) {
|
|
||||||
await localizerButton.click()
|
|
||||||
await expect(page.locator(selectOptionClass).first()).toContainText(firstLabel)
|
|
||||||
await expect(page.locator(selectOptionClass).nth(1)).toContainText(secondLabel)
|
|
||||||
}
|
|
||||||
|
|
||||||
await checkLocaleLabels('Spanish (es)', 'English (en)')
|
|
||||||
|
|
||||||
// Change locale to Spanish
|
|
||||||
await localizerButton.click()
|
|
||||||
await expect(localeListItem1).toContainText('Spanish (es)')
|
|
||||||
await localeListItem1.click()
|
|
||||||
|
|
||||||
// Go to account page
|
|
||||||
await page.goto(postsUrl.account)
|
|
||||||
|
|
||||||
const languageField = page.locator('.payload-settings__language .react-select')
|
|
||||||
const options = page.locator('.rs__option')
|
|
||||||
|
|
||||||
// Change language to Spanish
|
|
||||||
await languageField.click()
|
|
||||||
await options.locator('text=Español').click()
|
|
||||||
|
|
||||||
await checkLocaleLabels('Español (es)', 'Inglés (en)')
|
|
||||||
|
|
||||||
// Change locale and language back to English
|
|
||||||
await languageField.click()
|
|
||||||
await options.locator('text=English').click()
|
|
||||||
await localizerButton.click()
|
|
||||||
await expect(localeListItem1).toContainText('Spanish (es)')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('drawers', () => {
|
|
||||||
test('document drawers are visually stacking', async () => {
|
|
||||||
await navigateToDoc(page, postsUrl)
|
|
||||||
await page.locator('#field-title').fill(title)
|
|
||||||
await saveDocAndAssert(page)
|
|
||||||
await page
|
|
||||||
.locator('.field-type.relationship .relationship--single-value__drawer-toggler')
|
|
||||||
.click()
|
|
||||||
await wait(500)
|
|
||||||
const drawer1Content = page.locator('[id^=doc-drawer_posts_1_] .drawer__content')
|
|
||||||
await expect(drawer1Content).toBeVisible()
|
|
||||||
const drawerLeft = await drawer1Content.boundingBox().then((box) => box.x)
|
|
||||||
await drawer1Content
|
|
||||||
.locator('.field-type.relationship .relationship--single-value__drawer-toggler')
|
|
||||||
.click()
|
|
||||||
const drawer2Content = page.locator('[id^=doc-drawer_posts_2_] .drawer__content')
|
|
||||||
await expect(drawer2Content).toBeVisible()
|
|
||||||
const drawer2Left = await drawer2Content.boundingBox().then((box) => box.x)
|
|
||||||
expect(drawer2Left > drawerLeft).toBe(true)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('CRUD', () => {
|
|
||||||
test('should create', async () => {
|
|
||||||
await page.goto(postsUrl.create)
|
|
||||||
await page.locator('#field-title').fill(title)
|
|
||||||
await page.locator('#field-description').fill(description)
|
|
||||||
await saveDocAndAssert(page)
|
|
||||||
await expect(page.locator('#field-title')).toHaveValue(title)
|
|
||||||
await expect(page.locator('#field-description')).toHaveValue(description)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('should read existing', async () => {
|
|
||||||
const { id } = await createPost()
|
|
||||||
await page.goto(postsUrl.edit(id))
|
|
||||||
await expect(page.locator('#field-title')).toHaveValue(title)
|
|
||||||
await expect(page.locator('#field-description')).toHaveValue(description)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('should update existing', async () => {
|
|
||||||
const { id } = await createPost()
|
|
||||||
await page.goto(postsUrl.edit(id))
|
|
||||||
const newTitle = 'new title'
|
|
||||||
const newDesc = 'new description'
|
|
||||||
await page.locator('#field-title').fill(newTitle)
|
|
||||||
await page.locator('#field-description').fill(newDesc)
|
|
||||||
await saveDocAndAssert(page)
|
|
||||||
await expect(page.locator('#field-title')).toHaveValue(newTitle)
|
|
||||||
await expect(page.locator('#field-description')).toHaveValue(newDesc)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('should save using hotkey', async () => {
|
|
||||||
const { id } = await createPost()
|
|
||||||
await page.goto(postsUrl.edit(id))
|
|
||||||
const newTitle = 'new title'
|
|
||||||
await page.locator('#field-title').fill(newTitle)
|
|
||||||
await saveDocHotkeyAndAssert(page)
|
|
||||||
await expect(page.locator('#field-title')).toHaveValue(newTitle)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('should delete existing', async () => {
|
|
||||||
const { id, title } = await createPost()
|
|
||||||
await page.goto(postsUrl.edit(id))
|
|
||||||
await openDocControls(page)
|
|
||||||
await page.locator('#action-delete').click()
|
|
||||||
await page.locator('#confirm-delete').click()
|
|
||||||
await expect(page.locator(`text=Post "${title}" successfully deleted.`)).toBeVisible()
|
|
||||||
expect(page.url()).toContain(postsUrl.list)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('should bulk delete all on page', async () => {
|
|
||||||
await deleteAllPosts()
|
|
||||||
await Promise.all([createPost(), createPost(), createPost()])
|
|
||||||
await page.goto(postsUrl.list)
|
|
||||||
await page.locator('input#select-all').check()
|
|
||||||
await page.locator('.delete-documents__toggle').click()
|
|
||||||
await page.locator('#confirm-delete').click()
|
|
||||||
|
|
||||||
await expect(page.locator('.payload-toast-container .toast-success')).toHaveText(
|
|
||||||
'Deleted 3 Posts successfully.',
|
|
||||||
)
|
|
||||||
|
|
||||||
await expect(page.locator('.collection-list__no-results')).toBeVisible()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('should bulk delete with filters and across pages', async () => {
|
|
||||||
await deleteAllPosts()
|
|
||||||
await Promise.all([createPost({ title: 'Post 1' }), createPost({ title: 'Post 2' })])
|
|
||||||
await page.goto(postsUrl.list)
|
|
||||||
await page.locator('#search-filter-input').fill('Post 1')
|
|
||||||
await expect(page.locator('.table table > tbody > tr')).toHaveCount(1)
|
|
||||||
await page.locator('input#select-all').check()
|
|
||||||
await page.locator('button.list-selection__button').click()
|
|
||||||
await page.locator('.delete-documents__toggle').click()
|
|
||||||
await page.locator('#confirm-delete').click()
|
|
||||||
|
|
||||||
await expect(page.locator('.payload-toast-container .toast-success')).toHaveText(
|
|
||||||
'Deleted 1 Post successfully.',
|
|
||||||
)
|
|
||||||
|
|
||||||
await expect(page.locator('.table table > tbody > tr')).toHaveCount(1)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('should bulk update', async () => {
|
|
||||||
// First, delete all posts created by the seed
|
|
||||||
await deleteAllPosts()
|
|
||||||
const post1Title = 'Post'
|
|
||||||
const updatedPostTitle = `${post1Title} (Updated)`
|
|
||||||
await Promise.all([createPost({ title: post1Title }), createPost(), createPost()])
|
|
||||||
await page.goto(postsUrl.list)
|
|
||||||
await page.locator('input#select-all').check()
|
|
||||||
await page.locator('.edit-many__toggle').click()
|
|
||||||
await page.locator('.field-select .rs__control').click()
|
|
||||||
|
|
||||||
const titleOption = page.locator('.field-select .rs__option', {
|
|
||||||
hasText: exactText('Title'),
|
|
||||||
})
|
|
||||||
|
|
||||||
await expect(titleOption).toBeVisible()
|
|
||||||
await titleOption.click()
|
|
||||||
const titleInput = page.locator('#field-title')
|
|
||||||
await expect(titleInput).toBeVisible()
|
|
||||||
await titleInput.fill(updatedPostTitle)
|
|
||||||
await page.locator('.form-submit button[type="submit"].edit-many__publish').click()
|
|
||||||
|
|
||||||
await expect(page.locator('.payload-toast-container .toast-success')).toContainText(
|
|
||||||
'Updated 3 Posts successfully.',
|
|
||||||
)
|
|
||||||
|
|
||||||
await expect(page.locator('.row-1 .cell-title')).toContainText(updatedPostTitle)
|
|
||||||
await expect(page.locator('.row-2 .cell-title')).toContainText(updatedPostTitle)
|
|
||||||
await expect(page.locator('.row-3 .cell-title')).toContainText(updatedPostTitle)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('should not override un-edited values in bulk edit if it has a defaultValue', async () => {
|
|
||||||
await deleteAllPosts()
|
|
||||||
const post1Title = 'Post'
|
|
||||||
const postData = {
|
|
||||||
title: 'Post',
|
|
||||||
arrayOfFields: [
|
|
||||||
{
|
|
||||||
optional: 'some optional array field',
|
|
||||||
innerArrayOfFields: [
|
|
||||||
{
|
|
||||||
innerOptional: 'some inner optional array field',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
group: {
|
|
||||||
defaultValueField: 'not the group default value',
|
|
||||||
title: 'some title',
|
|
||||||
},
|
|
||||||
someBlock: [
|
|
||||||
{
|
|
||||||
textFieldForBlock: 'some text for block text',
|
|
||||||
blockType: 'textBlock',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
defaultValueField: 'not the default value',
|
|
||||||
}
|
|
||||||
const updatedPostTitle = `${post1Title} (Updated)`
|
|
||||||
await createPost(postData)
|
|
||||||
await page.goto(postsUrl.list)
|
|
||||||
await page.locator('input#select-all').check()
|
|
||||||
await page.locator('.edit-many__toggle').click()
|
|
||||||
await page.locator('.field-select .rs__control').click()
|
|
||||||
|
|
||||||
const titleOption = page.locator('.field-select .rs__option', {
|
|
||||||
hasText: exactText('Title'),
|
|
||||||
})
|
|
||||||
|
|
||||||
await expect(titleOption).toBeVisible()
|
|
||||||
await titleOption.click()
|
|
||||||
const titleInput = page.locator('#field-title')
|
|
||||||
await expect(titleInput).toBeVisible()
|
|
||||||
await titleInput.fill(updatedPostTitle)
|
|
||||||
await page.locator('.form-submit button[type="submit"].edit-many__publish').click()
|
|
||||||
|
|
||||||
await expect(page.locator('.payload-toast-container .toast-success')).toContainText(
|
|
||||||
'Updated 1 Post successfully.',
|
|
||||||
)
|
|
||||||
|
|
||||||
const updatedPost = await payload.find({
|
|
||||||
collection: 'posts',
|
|
||||||
limit: 1,
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(updatedPost.docs[0].title).toBe(updatedPostTitle)
|
|
||||||
expect(updatedPost.docs[0].arrayOfFields.length).toBe(1)
|
|
||||||
expect(updatedPost.docs[0].arrayOfFields[0].optional).toBe('some optional array field')
|
|
||||||
expect(updatedPost.docs[0].arrayOfFields[0].innerArrayOfFields.length).toBe(1)
|
|
||||||
expect(updatedPost.docs[0].someBlock[0].textFieldForBlock).toBe('some text for block text')
|
|
||||||
expect(updatedPost.docs[0].defaultValueField).toBe('not the default value')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('should bulk update with filters and across pages', async () => {
|
|
||||||
// First, delete all posts created by the seed
|
|
||||||
await deleteAllPosts()
|
|
||||||
const post1Title = 'Post 1'
|
|
||||||
await Promise.all([createPost({ title: post1Title }), createPost({ title: 'Post 2' })])
|
|
||||||
const updatedPostTitle = `${post1Title} (Updated)`
|
|
||||||
await page.goto(postsUrl.list)
|
|
||||||
await page.locator('#search-filter-input').fill('Post 1')
|
|
||||||
await expect(page.locator('.table table > tbody > tr')).toHaveCount(1)
|
|
||||||
await page.locator('input#select-all').check()
|
|
||||||
await page.locator('button.list-selection__button').click()
|
|
||||||
await page.locator('.edit-many__toggle').click()
|
|
||||||
await page.locator('.field-select .rs__control').click()
|
|
||||||
|
|
||||||
const titleOption = page.locator('.field-select .rs__option', {
|
|
||||||
hasText: exactText('Title'),
|
|
||||||
})
|
|
||||||
|
|
||||||
await expect(titleOption).toBeVisible()
|
|
||||||
await titleOption.click()
|
|
||||||
const titleInput = page.locator('#field-title')
|
|
||||||
await expect(titleInput).toBeVisible()
|
|
||||||
await titleInput.fill(updatedPostTitle)
|
|
||||||
|
|
||||||
await page.locator('.form-submit button[type="submit"].edit-many__publish').click()
|
|
||||||
await expect(page.locator('.payload-toast-container .toast-success')).toContainText(
|
|
||||||
'Updated 1 Post successfully.',
|
|
||||||
)
|
|
||||||
|
|
||||||
await expect(page.locator('.table table > tbody > tr')).toHaveCount(1)
|
|
||||||
await expect(page.locator('.row-1 .cell-title')).toContainText(updatedPostTitle)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('should save globals', async () => {
|
|
||||||
await page.goto(postsUrl.global(globalSlug))
|
|
||||||
|
|
||||||
await page.locator('#field-title').fill(title)
|
|
||||||
await saveDocAndAssert(page)
|
|
||||||
|
|
||||||
await expect(page.locator('#field-title')).toHaveValue(title)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('should hide duplicate when disableDuplicate: true', async () => {
|
|
||||||
await page.goto(disableDuplicateURL.create)
|
|
||||||
await page.locator('#field-title').fill(title)
|
|
||||||
await saveDocAndAssert(page)
|
|
||||||
await page.locator('.doc-controls__popup >> .popup-button').click()
|
|
||||||
await expect(page.locator('#action-duplicate')).toBeHidden()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('should properly close leave-without-saving modal after clicking leave-anyway button', async () => {
|
|
||||||
const { id } = await createPost()
|
|
||||||
await page.goto(postsUrl.edit(id))
|
|
||||||
const title = 'title'
|
|
||||||
await page.locator('#field-title').fill(title)
|
|
||||||
await saveDocHotkeyAndAssert(page)
|
|
||||||
await expect(page.locator('#field-title')).toHaveValue(title)
|
|
||||||
|
|
||||||
const newTitle = 'new title'
|
|
||||||
await page.locator('#field-title').fill(newTitle)
|
|
||||||
|
|
||||||
await page.locator('header.app-header a[href="/admin/collections/posts"]').click()
|
|
||||||
|
|
||||||
// Locate the modal container
|
|
||||||
const modalContainer = page.locator('.payload__modal-container')
|
|
||||||
await expect(modalContainer).toBeVisible()
|
|
||||||
|
|
||||||
// Click the "Leave anyway" button
|
|
||||||
await page.locator('.leave-without-saving__controls .btn--style-primary').click()
|
|
||||||
|
|
||||||
// Assert that the class on the modal container changes to 'payload__modal-container--exitDone'
|
|
||||||
await expect(modalContainer).toHaveClass(/payload__modal-container--exitDone/)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
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>
|
|
||||||
}
|
|
||||||
490
test/admin/e2e/document-view/e2e.spec.ts
Normal file
490
test/admin/e2e/document-view/e2e.spec.ts
Normal file
@@ -0,0 +1,490 @@
|
|||||||
|
import type { Page } from '@playwright/test'
|
||||||
|
|
||||||
|
import { expect, test } from '@playwright/test'
|
||||||
|
import { wait } from 'payload/shared'
|
||||||
|
|
||||||
|
import type { Config, Post } from '../../payload-types.js'
|
||||||
|
|
||||||
|
import {
|
||||||
|
checkBreadcrumb,
|
||||||
|
checkPageTitle,
|
||||||
|
ensureCompilationIsDone,
|
||||||
|
exactText,
|
||||||
|
initPageConsoleErrorCatch,
|
||||||
|
openNav,
|
||||||
|
saveDocAndAssert,
|
||||||
|
} from '../../../helpers.js'
|
||||||
|
import { AdminUrlUtil } from '../../../helpers/adminUrlUtil.js'
|
||||||
|
import { initPayloadE2ENoConfig } from '../../../helpers/initPayloadE2ENoConfig.js'
|
||||||
|
import {
|
||||||
|
customAdminRoutes,
|
||||||
|
customEditLabel,
|
||||||
|
customNestedTabViewPath,
|
||||||
|
customNestedTabViewTitle,
|
||||||
|
customTabLabel,
|
||||||
|
customTabViewPath,
|
||||||
|
customTabViewTitle,
|
||||||
|
} from '../../shared.js'
|
||||||
|
import {
|
||||||
|
customFieldsSlug,
|
||||||
|
customGlobalViews2GlobalSlug,
|
||||||
|
customViews2CollectionSlug,
|
||||||
|
globalSlug,
|
||||||
|
group1Collection1Slug,
|
||||||
|
group1GlobalSlug,
|
||||||
|
noApiViewCollectionSlug,
|
||||||
|
noApiViewGlobalSlug,
|
||||||
|
postsCollectionSlug,
|
||||||
|
} from '../../slugs.js'
|
||||||
|
|
||||||
|
const { beforeAll, beforeEach, describe } = test
|
||||||
|
|
||||||
|
const title = 'Title'
|
||||||
|
const description = 'Description'
|
||||||
|
|
||||||
|
let payload: PayloadTestSDK<Config>
|
||||||
|
|
||||||
|
import { navigateToDoc } from 'helpers/e2e/navigateToDoc.js'
|
||||||
|
import path from 'path'
|
||||||
|
import { fileURLToPath } from 'url'
|
||||||
|
|
||||||
|
import type { PayloadTestSDK } from '../../../helpers/sdk/index.js'
|
||||||
|
|
||||||
|
import { reInitializeDB } from '../../../helpers/reInitializeDB.js'
|
||||||
|
import { TEST_TIMEOUT_LONG } from '../../../playwright.config.js'
|
||||||
|
const filename = fileURLToPath(import.meta.url)
|
||||||
|
const currentFolder = path.dirname(filename)
|
||||||
|
const dirname = path.resolve(currentFolder, '../../')
|
||||||
|
|
||||||
|
describe('Document View', () => {
|
||||||
|
let page: Page
|
||||||
|
let postsUrl: AdminUrlUtil
|
||||||
|
let globalURL: AdminUrlUtil
|
||||||
|
let serverURL: string
|
||||||
|
let customViewsURL: AdminUrlUtil
|
||||||
|
let customFieldsURL: AdminUrlUtil
|
||||||
|
|
||||||
|
beforeAll(async ({ browser }, testInfo) => {
|
||||||
|
const prebuild = false // 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,
|
||||||
|
}))
|
||||||
|
postsUrl = new AdminUrlUtil(serverURL, postsCollectionSlug)
|
||||||
|
globalURL = new AdminUrlUtil(serverURL, globalSlug)
|
||||||
|
customViewsURL = new AdminUrlUtil(serverURL, customViews2CollectionSlug)
|
||||||
|
customFieldsURL = new AdminUrlUtil(serverURL, customFieldsSlug)
|
||||||
|
|
||||||
|
const context = await browser.newContext()
|
||||||
|
page = await context.newPage()
|
||||||
|
initPageConsoleErrorCatch(page)
|
||||||
|
|
||||||
|
await ensureCompilationIsDone({ customAdminRoutes, page, serverURL })
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await reInitializeDB({
|
||||||
|
serverURL,
|
||||||
|
snapshotKey: 'adminTests',
|
||||||
|
})
|
||||||
|
|
||||||
|
await ensureCompilationIsDone({ customAdminRoutes, page, serverURL })
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('API view', () => {
|
||||||
|
test('collection — should not show API tab when disabled in config', async () => {
|
||||||
|
await page.goto(postsUrl.collection(noApiViewCollectionSlug))
|
||||||
|
await page.locator('.collection-list .table a').click()
|
||||||
|
await expect(page.locator('.doc-tabs__tabs-container')).not.toContainText('API')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('collection — should not enable API route when disabled in config', async () => {
|
||||||
|
const collectionItems = await payload.find({
|
||||||
|
collection: noApiViewCollectionSlug,
|
||||||
|
limit: 1,
|
||||||
|
})
|
||||||
|
expect(collectionItems.docs.length).toBe(1)
|
||||||
|
await page.goto(
|
||||||
|
`${postsUrl.collection(noApiViewGlobalSlug)}/${collectionItems.docs[0].id}/api`,
|
||||||
|
)
|
||||||
|
await expect(page.locator('.not-found')).toHaveCount(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('collection — sidebar fields should respond to permission', async () => {
|
||||||
|
const { id } = await createPost()
|
||||||
|
await page.goto(postsUrl.edit(id))
|
||||||
|
|
||||||
|
await expect(page.locator('#field-sidebarField')).toBeDisabled()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('collection — depth field should have value 0 when empty', async () => {
|
||||||
|
const { id } = await createPost()
|
||||||
|
await page.goto(`${postsUrl.edit(id)}/api`)
|
||||||
|
|
||||||
|
const depthField = page.locator('#field-depth')
|
||||||
|
await depthField.fill('')
|
||||||
|
await expect(depthField).toHaveValue('0')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('global — should not show API tab when disabled in config', async () => {
|
||||||
|
await page.goto(postsUrl.global(noApiViewGlobalSlug))
|
||||||
|
await expect(page.locator('.doc-tabs__tabs-container')).not.toContainText('API')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('global — should not enable API route when disabled in config', async () => {
|
||||||
|
await page.goto(`${postsUrl.global(noApiViewGlobalSlug)}/api`)
|
||||||
|
await expect(page.locator('.not-found')).toHaveCount(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('preview button', () => {
|
||||||
|
test('collection — should render preview button when `admin.preview` is set', async () => {
|
||||||
|
const collectionWithPreview = new AdminUrlUtil(serverURL, postsCollectionSlug)
|
||||||
|
await page.goto(collectionWithPreview.create)
|
||||||
|
await page.waitForURL(collectionWithPreview.create)
|
||||||
|
await page.locator('#field-title').fill(title)
|
||||||
|
await saveDocAndAssert(page)
|
||||||
|
await expect(page.locator('.btn.preview-btn')).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('collection — should not render preview button when `admin.preview` is not set', async () => {
|
||||||
|
const collectionWithoutPreview = new AdminUrlUtil(serverURL, group1Collection1Slug)
|
||||||
|
await page.goto(collectionWithoutPreview.create)
|
||||||
|
await page.waitForURL(collectionWithoutPreview.create)
|
||||||
|
await page.locator('#field-title').fill(title)
|
||||||
|
await saveDocAndAssert(page)
|
||||||
|
await expect(page.locator('.btn.preview-btn')).toBeHidden()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('global — should render preview button when `admin.preview` is set', async () => {
|
||||||
|
const globalWithPreview = new AdminUrlUtil(serverURL, globalSlug)
|
||||||
|
await page.goto(globalWithPreview.global(globalSlug))
|
||||||
|
await expect(page.locator('.btn.preview-btn')).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('global — should not render preview button when `admin.preview` is not set', async () => {
|
||||||
|
const globalWithoutPreview = new AdminUrlUtil(serverURL, group1GlobalSlug)
|
||||||
|
await page.goto(globalWithoutPreview.global(group1GlobalSlug))
|
||||||
|
await page.locator('#field-title').fill(title)
|
||||||
|
await saveDocAndAssert(page)
|
||||||
|
await expect(page.locator('.btn.preview-btn')).toBeHidden()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('form state', () => {
|
||||||
|
test('collection — should re-enable fields after save', async () => {
|
||||||
|
await page.goto(postsUrl.create)
|
||||||
|
await page.locator('#field-title').fill(title)
|
||||||
|
await saveDocAndAssert(page)
|
||||||
|
await expect(page.locator('#field-title')).toBeEnabled()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('global — should re-enable fields after save', async () => {
|
||||||
|
await page.goto(globalURL.global(globalSlug))
|
||||||
|
await page.locator('#field-title').fill(title)
|
||||||
|
await saveDocAndAssert(page)
|
||||||
|
await expect(page.locator('#field-title')).toBeEnabled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('document titles', () => {
|
||||||
|
test('collection — should render fallback titles when creating new', async () => {
|
||||||
|
await page.goto(postsUrl.create)
|
||||||
|
await checkPageTitle(page, '[Untitled]')
|
||||||
|
await checkBreadcrumb(page, 'Create New')
|
||||||
|
await saveDocAndAssert(page)
|
||||||
|
expect(true).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('collection — should render `useAsTitle` field', async () => {
|
||||||
|
await page.goto(postsUrl.create)
|
||||||
|
await page.locator('#field-title')?.fill(title)
|
||||||
|
await saveDocAndAssert(page)
|
||||||
|
await wait(500)
|
||||||
|
await checkPageTitle(page, title)
|
||||||
|
await checkBreadcrumb(page, title)
|
||||||
|
expect(true).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('collection — should render `id` as `useAsTitle` fallback', async () => {
|
||||||
|
const { id } = await createPost()
|
||||||
|
const postURL = postsUrl.edit(id)
|
||||||
|
await page.goto(postURL)
|
||||||
|
await page.waitForURL(postURL)
|
||||||
|
await wait(500)
|
||||||
|
await page.locator('#field-title')?.fill('')
|
||||||
|
await expect(page.locator('.doc-header__title.render-title:has-text("ID:")')).toBeVisible()
|
||||||
|
await saveDocAndAssert(page)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('global — should render custom, localized label', async () => {
|
||||||
|
await page.goto(globalURL.global(globalSlug))
|
||||||
|
await page.waitForURL(globalURL.global(globalSlug))
|
||||||
|
await openNav(page)
|
||||||
|
const label = 'My Global Label'
|
||||||
|
const globalLabel = page.locator(`#nav-global-global`)
|
||||||
|
await expect(globalLabel).toContainText(label)
|
||||||
|
await globalLabel.click()
|
||||||
|
await checkPageTitle(page, label)
|
||||||
|
await checkBreadcrumb(page, label)
|
||||||
|
await page.locator('#field-title').fill(title)
|
||||||
|
await saveDocAndAssert(page)
|
||||||
|
await checkPageTitle(page, label)
|
||||||
|
await checkBreadcrumb(page, label)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('global — should render simple label strings', async () => {
|
||||||
|
await page.goto(postsUrl.admin)
|
||||||
|
await page.waitForURL(postsUrl.admin)
|
||||||
|
await openNav(page)
|
||||||
|
const label = 'Group Globals 1'
|
||||||
|
const globalLabel = page.locator(`#nav-global-group-globals-one`)
|
||||||
|
await expect(globalLabel).toContainText(label)
|
||||||
|
await globalLabel.click()
|
||||||
|
await checkPageTitle(page, label)
|
||||||
|
await checkBreadcrumb(page, label)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('global — should render slug in sentence case as fallback', async () => {
|
||||||
|
await page.goto(postsUrl.admin)
|
||||||
|
await page.waitForURL(postsUrl.admin)
|
||||||
|
await openNav(page)
|
||||||
|
const label = 'Group Globals Two'
|
||||||
|
const globalLabel = page.locator(`#nav-global-group-globals-two`)
|
||||||
|
await expect(globalLabel).toContainText(label)
|
||||||
|
await globalLabel.click()
|
||||||
|
await checkPageTitle(page, label)
|
||||||
|
await checkBreadcrumb(page, label)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('custom document views', () => {
|
||||||
|
test('collection — should render custom tab view', async () => {
|
||||||
|
await page.goto(customViewsURL.create)
|
||||||
|
await page.locator('#field-title').fill('Test')
|
||||||
|
await saveDocAndAssert(page)
|
||||||
|
const pageURL = page.url()
|
||||||
|
const customViewURL = `${pageURL}${customTabViewPath}`
|
||||||
|
await page.goto(customViewURL)
|
||||||
|
expect(page.url()).toEqual(customViewURL)
|
||||||
|
await expect(page.locator('h1#custom-view-title')).toContainText(customTabViewTitle)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('collection — should render custom nested tab view', async () => {
|
||||||
|
await page.goto(customViewsURL.create)
|
||||||
|
await page.locator('#field-title').fill('Test')
|
||||||
|
await saveDocAndAssert(page)
|
||||||
|
|
||||||
|
// wait for the update view to load
|
||||||
|
await page.waitForURL(/\/(?!create$)[\w-]+$/)
|
||||||
|
const pageURL = page.url()
|
||||||
|
|
||||||
|
const customNestedTabViewURL = `${pageURL}${customNestedTabViewPath}`
|
||||||
|
await page.goto(customNestedTabViewURL)
|
||||||
|
await page.waitForURL(customNestedTabViewURL)
|
||||||
|
await expect(page.locator('h1#custom-view-title')).toContainText(customNestedTabViewTitle)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('collection — should render custom tab label', async () => {
|
||||||
|
await page.goto(customViewsURL.create)
|
||||||
|
await page.locator('#field-title').fill('Test')
|
||||||
|
await saveDocAndAssert(page)
|
||||||
|
|
||||||
|
// wait for the update view to load
|
||||||
|
await page.waitForURL(/\/(?!create$)[\w-]+$/)
|
||||||
|
const editTab = page.locator('.doc-tab a[tabindex="-1"]')
|
||||||
|
|
||||||
|
await expect(editTab).toContainText(customEditLabel)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('collection — should render custom tab component', async () => {
|
||||||
|
await page.goto(customViewsURL.create)
|
||||||
|
await page.locator('#field-title').fill('Test')
|
||||||
|
await saveDocAndAssert(page)
|
||||||
|
|
||||||
|
const customTab = page.locator(`.doc-tab a:has-text("${customTabLabel}")`)
|
||||||
|
|
||||||
|
await expect(customTab).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('global — should render custom tab label', async () => {
|
||||||
|
await page.goto(globalURL.global(customGlobalViews2GlobalSlug) + '/custom-tab-view')
|
||||||
|
|
||||||
|
const title = page.locator('#custom-view-title')
|
||||||
|
|
||||||
|
const docTab = page.locator('.doc-tab__link:has-text("Custom")')
|
||||||
|
|
||||||
|
await expect(docTab).toBeVisible()
|
||||||
|
await expect(title).toContainText('Custom Tab Label View')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('global — should render custom tab component', async () => {
|
||||||
|
await page.goto(globalURL.global(customGlobalViews2GlobalSlug) + '/custom-tab-component')
|
||||||
|
const title = page.locator('#custom-view-title')
|
||||||
|
|
||||||
|
const docTab = page.locator('.custom-doc-tab').first()
|
||||||
|
|
||||||
|
await expect(docTab).toBeVisible()
|
||||||
|
await expect(docTab).toContainText('Custom Tab Component')
|
||||||
|
await expect(title).toContainText('Custom View With Tab Component')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('drawers', () => {
|
||||||
|
test('document drawers are visually stacking', async () => {
|
||||||
|
await navigateToDoc(page, postsUrl)
|
||||||
|
await page.locator('#field-title').fill(title)
|
||||||
|
await saveDocAndAssert(page)
|
||||||
|
await page
|
||||||
|
.locator('.field-type.relationship .relationship--single-value__drawer-toggler')
|
||||||
|
.click()
|
||||||
|
await wait(500)
|
||||||
|
const drawer1Content = page.locator('[id^=doc-drawer_posts_1_] .drawer__content')
|
||||||
|
await expect(drawer1Content).toBeVisible()
|
||||||
|
const drawerLeft = await drawer1Content.boundingBox().then((box) => box.x)
|
||||||
|
await drawer1Content
|
||||||
|
.locator('.field-type.relationship .relationship--single-value__drawer-toggler')
|
||||||
|
.click()
|
||||||
|
const drawer2Content = page.locator('[id^=doc-drawer_posts_2_] .drawer__content')
|
||||||
|
await expect(drawer2Content).toBeVisible()
|
||||||
|
const drawer2Left = await drawer2Content.boundingBox().then((box) => box.x)
|
||||||
|
expect(drawer2Left > drawerLeft).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('custom fields', () => {
|
||||||
|
test('should render custom field component', async () => {
|
||||||
|
await page.goto(customFieldsURL.create)
|
||||||
|
await page.waitForURL(customFieldsURL.create)
|
||||||
|
await expect(page.locator('#field-customTextClientField')).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('renders custom label component', async () => {
|
||||||
|
await page.goto(customFieldsURL.create)
|
||||||
|
await page.waitForURL(customFieldsURL.create)
|
||||||
|
await expect(page.locator('#custom-client-field-label')).toBeVisible()
|
||||||
|
await expect(page.locator('#custom-server-field-label')).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('renders custom field description text', async () => {
|
||||||
|
await page.goto(customFieldsURL.create)
|
||||||
|
await page.waitForURL(customFieldsURL.create)
|
||||||
|
await expect(page.locator('#custom-client-field-description')).toBeVisible()
|
||||||
|
await expect(page.locator('#custom-server-field-description')).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('custom server components should receive field props', async () => {
|
||||||
|
await page.goto(customFieldsURL.create)
|
||||||
|
await page.waitForURL(customFieldsURL.create)
|
||||||
|
await expect(
|
||||||
|
page.locator('#custom-server-field-label', {
|
||||||
|
hasText: exactText('Label: the max length of this field is: 100'),
|
||||||
|
}),
|
||||||
|
).toBeVisible()
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.locator('#custom-server-field-description', {
|
||||||
|
hasText: exactText('Description: the max length of this field is: 100'),
|
||||||
|
}),
|
||||||
|
).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('custom client components should receive field props', async () => {
|
||||||
|
await page.goto(customFieldsURL.create)
|
||||||
|
await page.waitForURL(customFieldsURL.create)
|
||||||
|
await expect(
|
||||||
|
page.locator('#custom-client-field-label', {
|
||||||
|
hasText: exactText('Label: the max length of this field is: 100'),
|
||||||
|
}),
|
||||||
|
).toBeVisible()
|
||||||
|
await expect(
|
||||||
|
page.locator('#custom-client-field-description', {
|
||||||
|
hasText: exactText('Description: the max length of this field is: 100'),
|
||||||
|
}),
|
||||||
|
).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('field descriptions', () => {
|
||||||
|
test('should render static field description', async () => {
|
||||||
|
await page.goto(customFieldsURL.create)
|
||||||
|
await page.waitForURL(customFieldsURL.create)
|
||||||
|
await expect(page.locator('.field-description-descriptionAsString')).toContainText(
|
||||||
|
'Static field description.',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should render functional field description', async () => {
|
||||||
|
await page.goto(customFieldsURL.create)
|
||||||
|
await page.waitForURL(customFieldsURL.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(customFieldsURL.create)
|
||||||
|
await page.waitForURL(customFieldsURL.create)
|
||||||
|
await page.locator('#field-descriptionAsComponent').fill('component')
|
||||||
|
await expect(page.locator('.field-description-descriptionAsComponent')).toContainText(
|
||||||
|
'Component description: descriptionAsComponent - component',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should render custom error component', async () => {
|
||||||
|
await page.goto(customFieldsURL.create)
|
||||||
|
await page.waitForURL(customFieldsURL.create)
|
||||||
|
const input = page.locator('input[id="field-customTextClientField"]')
|
||||||
|
await input.fill('ab')
|
||||||
|
await expect(input).toHaveValue('ab')
|
||||||
|
const error = page.locator('.custom-error:near(input[id="field-customTextClientField"])')
|
||||||
|
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(customFieldsURL.create)
|
||||||
|
const input = page.locator('input[id="field-customTextClientField"]')
|
||||||
|
|
||||||
|
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')
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('select field', () => {
|
||||||
|
test('should render custom select options', async () => {
|
||||||
|
await page.goto(customFieldsURL.create)
|
||||||
|
await page.waitForURL(customFieldsURL.create)
|
||||||
|
await page.locator('#field-customSelectField .rs__control').click()
|
||||||
|
await expect(page.locator('#field-customSelectField .rs__option')).toHaveCount(2)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
async function createPost(overrides?: Partial<Post>): Promise<Post> {
|
||||||
|
return payload.create({
|
||||||
|
collection: postsCollectionSlug,
|
||||||
|
data: {
|
||||||
|
description,
|
||||||
|
title,
|
||||||
|
...overrides,
|
||||||
|
},
|
||||||
|
}) as unknown as Promise<Post>
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ import type { Page } from '@playwright/test'
|
|||||||
|
|
||||||
import { expect, test } from '@playwright/test'
|
import { expect, test } from '@playwright/test'
|
||||||
|
|
||||||
import type { Config, Post } from '../../payload-types.js'
|
import type { Config, Geo, Post } from '../../payload-types.js'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ensureCompilationIsDone,
|
ensureCompilationIsDone,
|
||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
initPageConsoleErrorCatch,
|
initPageConsoleErrorCatch,
|
||||||
openNav,
|
openNav,
|
||||||
saveDocAndAssert,
|
saveDocAndAssert,
|
||||||
|
saveDocHotkeyAndAssert,
|
||||||
} from '../../../helpers.js'
|
} from '../../../helpers.js'
|
||||||
import { AdminUrlUtil } from '../../../helpers/adminUrlUtil.js'
|
import { AdminUrlUtil } from '../../../helpers/adminUrlUtil.js'
|
||||||
import { initPayloadE2ENoConfig } from '../../../helpers/initPayloadE2ENoConfig.js'
|
import { initPayloadE2ENoConfig } from '../../../helpers/initPayloadE2ENoConfig.js'
|
||||||
@@ -18,15 +19,10 @@ import {
|
|||||||
customAdminRoutes,
|
customAdminRoutes,
|
||||||
customCollectionMetaTitle,
|
customCollectionMetaTitle,
|
||||||
customDefaultTabMetaTitle,
|
customDefaultTabMetaTitle,
|
||||||
customEditLabel,
|
|
||||||
customNestedTabViewPath,
|
|
||||||
customNestedTabViewTitle,
|
|
||||||
customNestedViewPath,
|
customNestedViewPath,
|
||||||
customNestedViewTitle,
|
customNestedViewTitle,
|
||||||
customRootViewMetaTitle,
|
customRootViewMetaTitle,
|
||||||
customTabLabel,
|
|
||||||
customTabViewPath,
|
customTabViewPath,
|
||||||
customTabViewTitle,
|
|
||||||
customVersionsTabMetaTitle,
|
customVersionsTabMetaTitle,
|
||||||
customViewMetaTitle,
|
customViewMetaTitle,
|
||||||
customViewPath,
|
customViewPath,
|
||||||
@@ -36,9 +32,9 @@ import {
|
|||||||
slugPluralLabel,
|
slugPluralLabel,
|
||||||
} from '../../shared.js'
|
} from '../../shared.js'
|
||||||
import {
|
import {
|
||||||
customFieldsSlug,
|
|
||||||
customGlobalViews2GlobalSlug,
|
|
||||||
customViews2CollectionSlug,
|
customViews2CollectionSlug,
|
||||||
|
disableDuplicateSlug,
|
||||||
|
geoCollectionSlug,
|
||||||
globalSlug,
|
globalSlug,
|
||||||
notInViewCollectionSlug,
|
notInViewCollectionSlug,
|
||||||
postsCollectionSlug,
|
postsCollectionSlug,
|
||||||
@@ -53,6 +49,7 @@ const description = 'Description'
|
|||||||
let payload: PayloadTestSDK<Config>
|
let payload: PayloadTestSDK<Config>
|
||||||
|
|
||||||
import { navigateToDoc } from 'helpers/e2e/navigateToDoc.js'
|
import { navigateToDoc } from 'helpers/e2e/navigateToDoc.js'
|
||||||
|
import { openDocControls } from 'helpers/e2e/openDocControls.js'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { fileURLToPath } from 'url'
|
import { fileURLToPath } from 'url'
|
||||||
|
|
||||||
@@ -64,13 +61,14 @@ const filename = fileURLToPath(import.meta.url)
|
|||||||
const currentFolder = path.dirname(filename)
|
const currentFolder = path.dirname(filename)
|
||||||
const dirname = path.resolve(currentFolder, '../../')
|
const dirname = path.resolve(currentFolder, '../../')
|
||||||
|
|
||||||
describe('admin1', () => {
|
describe('General', () => {
|
||||||
let page: Page
|
let page: Page
|
||||||
let postsUrl: AdminUrlUtil
|
let postsUrl: AdminUrlUtil
|
||||||
|
let geoUrl: AdminUrlUtil
|
||||||
let notInViewUrl: AdminUrlUtil
|
let notInViewUrl: AdminUrlUtil
|
||||||
let globalURL: AdminUrlUtil
|
let globalURL: AdminUrlUtil
|
||||||
let customViewsURL: AdminUrlUtil
|
let customViewsURL: AdminUrlUtil
|
||||||
let customFieldsURL: AdminUrlUtil
|
let disableDuplicateURL: AdminUrlUtil
|
||||||
let serverURL: string
|
let serverURL: string
|
||||||
let adminRoutes: ReturnType<typeof getRoutes>
|
let adminRoutes: ReturnType<typeof getRoutes>
|
||||||
|
|
||||||
@@ -85,10 +83,11 @@ describe('admin1', () => {
|
|||||||
prebuild,
|
prebuild,
|
||||||
}))
|
}))
|
||||||
postsUrl = new AdminUrlUtil(serverURL, postsCollectionSlug)
|
postsUrl = new AdminUrlUtil(serverURL, postsCollectionSlug)
|
||||||
|
geoUrl = new AdminUrlUtil(serverURL, geoCollectionSlug)
|
||||||
notInViewUrl = new AdminUrlUtil(serverURL, notInViewCollectionSlug)
|
notInViewUrl = new AdminUrlUtil(serverURL, notInViewCollectionSlug)
|
||||||
globalURL = new AdminUrlUtil(serverURL, globalSlug)
|
globalURL = new AdminUrlUtil(serverURL, globalSlug)
|
||||||
customViewsURL = new AdminUrlUtil(serverURL, customViews2CollectionSlug)
|
customViewsURL = new AdminUrlUtil(serverURL, customViews2CollectionSlug)
|
||||||
customFieldsURL = new AdminUrlUtil(serverURL, customFieldsSlug)
|
disableDuplicateURL = new AdminUrlUtil(serverURL, disableDuplicateSlug)
|
||||||
|
|
||||||
const context = await browser.newContext()
|
const context = await browser.newContext()
|
||||||
page = await context.newPage()
|
page = await context.newPage()
|
||||||
@@ -98,6 +97,7 @@ describe('admin1', () => {
|
|||||||
|
|
||||||
adminRoutes = getRoutes({ customAdminRoutes })
|
adminRoutes = getRoutes({ customAdminRoutes })
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await reInitializeDB({
|
await reInitializeDB({
|
||||||
serverURL,
|
serverURL,
|
||||||
@@ -477,6 +477,16 @@ describe('admin1', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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('custom providers', () => {
|
describe('custom providers', () => {
|
||||||
test('should render custom providers', async () => {
|
test('should render custom providers', async () => {
|
||||||
await page.goto(`${serverURL}/admin`)
|
await page.goto(`${serverURL}/admin`)
|
||||||
@@ -493,14 +503,14 @@ describe('admin1', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('custom views', () => {
|
describe('custom root views', () => {
|
||||||
test('root — should render custom view', async () => {
|
test('should render custom view', async () => {
|
||||||
await page.goto(`${serverURL}${adminRoutes.routes.admin}${customViewPath}`)
|
await page.goto(`${serverURL}${adminRoutes.routes.admin}${customViewPath}`)
|
||||||
await page.waitForURL(`**${adminRoutes.routes.admin}${customViewPath}`)
|
await page.waitForURL(`**${adminRoutes.routes.admin}${customViewPath}`)
|
||||||
await expect(page.locator('h1#custom-view-title')).toContainText(customViewTitle)
|
await expect(page.locator('h1#custom-view-title')).toContainText(customViewTitle)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('root — should render custom nested view', async () => {
|
test('should render custom nested view', async () => {
|
||||||
await page.goto(`${serverURL}${adminRoutes.routes.admin}${customNestedViewPath}`)
|
await page.goto(`${serverURL}${adminRoutes.routes.admin}${customNestedViewPath}`)
|
||||||
const pageURL = page.url()
|
const pageURL = page.url()
|
||||||
const pathname = new URL(pageURL).pathname
|
const pathname = new URL(pageURL).pathname
|
||||||
@@ -508,13 +518,13 @@ describe('admin1', () => {
|
|||||||
await expect(page.locator('h1#custom-view-title')).toContainText(customNestedViewTitle)
|
await expect(page.locator('h1#custom-view-title')).toContainText(customNestedViewTitle)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('root — should render public custom view', async () => {
|
test('should render public custom view', async () => {
|
||||||
await page.goto(`${serverURL}${adminRoutes.routes.admin}${publicCustomViewPath}`)
|
await page.goto(`${serverURL}${adminRoutes.routes.admin}${publicCustomViewPath}`)
|
||||||
await page.waitForURL(`**${adminRoutes.routes.admin}${publicCustomViewPath}`)
|
await page.waitForURL(`**${adminRoutes.routes.admin}${publicCustomViewPath}`)
|
||||||
await expect(page.locator('h1#custom-view-title')).toContainText(customViewTitle)
|
await expect(page.locator('h1#custom-view-title')).toContainText(customViewTitle)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('root — should render protected nested custom view', async () => {
|
test('should render protected nested custom view', async () => {
|
||||||
await page.goto(`${serverURL}${adminRoutes.routes.admin}${protectedCustomNestedViewPath}`)
|
await page.goto(`${serverURL}${adminRoutes.routes.admin}${protectedCustomNestedViewPath}`)
|
||||||
await page.waitForURL(`**${adminRoutes.routes.admin}/unauthorized`)
|
await page.waitForURL(`**${adminRoutes.routes.admin}/unauthorized`)
|
||||||
await expect(page.locator('.unauthorized')).toBeVisible()
|
await expect(page.locator('.unauthorized')).toBeVisible()
|
||||||
@@ -531,196 +541,61 @@ describe('admin1', () => {
|
|||||||
await page.waitForURL(`**${adminRoutes.routes.admin}${protectedCustomNestedViewPath}`)
|
await page.waitForURL(`**${adminRoutes.routes.admin}${protectedCustomNestedViewPath}`)
|
||||||
await expect(page.locator('h1#custom-view-title')).toContainText(customNestedViewTitle)
|
await expect(page.locator('h1#custom-view-title')).toContainText(customNestedViewTitle)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('collection - should render custom tab view', async () => {
|
|
||||||
await page.goto(customViewsURL.create)
|
|
||||||
await page.locator('#field-title').fill('Test')
|
|
||||||
await saveDocAndAssert(page)
|
|
||||||
const pageURL = page.url()
|
|
||||||
const customViewURL = `${pageURL}${customTabViewPath}`
|
|
||||||
await page.goto(customViewURL)
|
|
||||||
expect(page.url()).toEqual(customViewURL)
|
|
||||||
await expect(page.locator('h1#custom-view-title')).toContainText(customTabViewTitle)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('collection — should render custom nested tab view', async () => {
|
|
||||||
await page.goto(customViewsURL.create)
|
|
||||||
await page.locator('#field-title').fill('Test')
|
|
||||||
await saveDocAndAssert(page)
|
|
||||||
|
|
||||||
// wait for the update view to load
|
|
||||||
await page.waitForURL(/\/(?!create$)[\w-]+$/)
|
|
||||||
const pageURL = page.url()
|
|
||||||
|
|
||||||
const customNestedTabViewURL = `${pageURL}${customNestedTabViewPath}`
|
|
||||||
await page.goto(customNestedTabViewURL)
|
|
||||||
await page.waitForURL(customNestedTabViewURL)
|
|
||||||
await expect(page.locator('h1#custom-view-title')).toContainText(customNestedTabViewTitle)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('collection — should render custom tab label', async () => {
|
|
||||||
await page.goto(customViewsURL.create)
|
|
||||||
await page.locator('#field-title').fill('Test')
|
|
||||||
await saveDocAndAssert(page)
|
|
||||||
|
|
||||||
// wait for the update view to load
|
|
||||||
await page.waitForURL(/\/(?!create$)[\w-]+$/)
|
|
||||||
const editTab = page.locator('.doc-tab a[tabindex="-1"]')
|
|
||||||
|
|
||||||
await expect(editTab).toContainText(customEditLabel)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('collection — should render custom tab component', async () => {
|
|
||||||
await page.goto(customViewsURL.create)
|
|
||||||
await page.locator('#field-title').fill('Test')
|
|
||||||
await saveDocAndAssert(page)
|
|
||||||
|
|
||||||
const customTab = page.locator(`.doc-tab a:has-text("${customTabLabel}")`)
|
|
||||||
|
|
||||||
await expect(customTab).toBeVisible()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('global — should render custom tab label', async () => {
|
|
||||||
await page.goto(globalURL.global(customGlobalViews2GlobalSlug) + '/custom-tab-view')
|
|
||||||
|
|
||||||
const title = page.locator('#custom-view-title')
|
|
||||||
|
|
||||||
const docTab = page.locator('.doc-tab__link:has-text("Custom")')
|
|
||||||
|
|
||||||
await expect(docTab).toBeVisible()
|
|
||||||
await expect(title).toContainText('Custom Tab Label View')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('global — should render custom tab component', async () => {
|
|
||||||
await page.goto(globalURL.global(customGlobalViews2GlobalSlug) + '/custom-tab-component')
|
|
||||||
const title = page.locator('#custom-view-title')
|
|
||||||
|
|
||||||
const docTab = page.locator('.custom-doc-tab').first()
|
|
||||||
|
|
||||||
await expect(docTab).toBeVisible()
|
|
||||||
await expect(docTab).toContainText('Custom Tab Component')
|
|
||||||
await expect(title).toContainText('Custom View With Tab Component')
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('custom fields', () => {
|
describe('header actions', () => {
|
||||||
test('should render custom field component', async () => {
|
test('should show admin level action in admin panel', async () => {
|
||||||
await page.goto(customFieldsURL.create)
|
await page.goto(postsUrl.admin)
|
||||||
await page.waitForURL(customFieldsURL.create)
|
// Check if the element with the class .admin-button exists
|
||||||
await expect(page.locator('#field-customTextClientField')).toBeVisible()
|
await expect(page.locator('.app-header .admin-button')).toHaveCount(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('renders custom label component', async () => {
|
test('should show admin level action in collection list view', async () => {
|
||||||
await page.goto(customFieldsURL.create)
|
await page.goto(`${new AdminUrlUtil(serverURL, 'geo').list}`)
|
||||||
await page.waitForURL(customFieldsURL.create)
|
await expect(page.locator('.app-header .admin-button')).toHaveCount(1)
|
||||||
await expect(page.locator('#custom-client-field-label')).toBeVisible()
|
|
||||||
await expect(page.locator('#custom-server-field-label')).toBeVisible()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('renders custom field description text', async () => {
|
test('should show admin level action in collection edit view', async () => {
|
||||||
await page.goto(customFieldsURL.create)
|
const { id } = await createGeo()
|
||||||
await page.waitForURL(customFieldsURL.create)
|
await page.goto(geoUrl.edit(id))
|
||||||
await expect(page.locator('#custom-client-field-description')).toBeVisible()
|
await expect(page.locator('.app-header .admin-button')).toHaveCount(1)
|
||||||
await expect(page.locator('#custom-server-field-description')).toBeVisible()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('custom server components should receive field props', async () => {
|
test('should show collection list view level action in collection list view', async () => {
|
||||||
await page.goto(customFieldsURL.create)
|
await page.goto(`${new AdminUrlUtil(serverURL, 'geo').list}`)
|
||||||
await page.waitForURL(customFieldsURL.create)
|
await expect(page.locator('.app-header .collection-list-button')).toHaveCount(1)
|
||||||
await expect(
|
|
||||||
page.locator('#custom-server-field-label', {
|
|
||||||
hasText: exactText('Label: the max length of this field is: 100'),
|
|
||||||
}),
|
|
||||||
).toBeVisible()
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
page.locator('#custom-server-field-description', {
|
|
||||||
hasText: exactText('Description: the max length of this field is: 100'),
|
|
||||||
}),
|
|
||||||
).toBeVisible()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('custom client components should receive field props', async () => {
|
test('should show collection edit view level action in collection edit view', async () => {
|
||||||
await page.goto(customFieldsURL.create)
|
const { id } = await createGeo()
|
||||||
await page.waitForURL(customFieldsURL.create)
|
await page.goto(geoUrl.edit(id))
|
||||||
await expect(
|
await expect(page.locator('.app-header .collection-edit-button')).toHaveCount(1)
|
||||||
page.locator('#custom-client-field-label', {
|
|
||||||
hasText: exactText('Label: the max length of this field is: 100'),
|
|
||||||
}),
|
|
||||||
).toBeVisible()
|
|
||||||
await expect(
|
|
||||||
page.locator('#custom-client-field-description', {
|
|
||||||
hasText: exactText('Description: the max length of this field is: 100'),
|
|
||||||
}),
|
|
||||||
).toBeVisible()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('field descriptions', () => {
|
test('should show collection api view level action in collection api view', async () => {
|
||||||
test('should render static field description', async () => {
|
const { id } = await createGeo()
|
||||||
await page.goto(customFieldsURL.create)
|
await page.goto(`${geoUrl.edit(id)}/api`)
|
||||||
await page.waitForURL(customFieldsURL.create)
|
await expect(page.locator('.app-header .collection-api-button')).toHaveCount(1)
|
||||||
await expect(page.locator('.field-description-descriptionAsString')).toContainText(
|
|
||||||
'Static field description.',
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('should render functional field description', async () => {
|
|
||||||
await page.goto(customFieldsURL.create)
|
|
||||||
await page.waitForURL(customFieldsURL.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 () => {
|
test('should show global edit view level action in globals edit view', async () => {
|
||||||
await page.goto(customFieldsURL.create)
|
const globalWithPreview = new AdminUrlUtil(serverURL, globalSlug)
|
||||||
await page.waitForURL(customFieldsURL.create)
|
await page.goto(globalWithPreview.global(globalSlug))
|
||||||
await page.locator('#field-descriptionAsComponent').fill('component')
|
await expect(page.locator('.app-header .global-edit-button')).toHaveCount(1)
|
||||||
await expect(page.locator('.field-description-descriptionAsComponent')).toContainText(
|
|
||||||
'Component description: descriptionAsComponent - component',
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should render custom error component', async () => {
|
test('should show global api view level action in globals api view', async () => {
|
||||||
await page.goto(customFieldsURL.create)
|
const globalWithPreview = new AdminUrlUtil(serverURL, globalSlug)
|
||||||
await page.waitForURL(customFieldsURL.create)
|
await page.goto(`${globalWithPreview.global(globalSlug)}/api`)
|
||||||
const input = page.locator('input[id="field-customTextClientField"]')
|
await expect(page.locator('.app-header .global-api-button')).toHaveCount(1)
|
||||||
await input.fill('ab')
|
|
||||||
await expect(input).toHaveValue('ab')
|
|
||||||
const error = page.locator('.custom-error:near(input[id="field-customTextClientField"])')
|
|
||||||
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 () => {
|
test('should reset actions array when navigating from view with actions to view without actions', async () => {
|
||||||
await page.goto(customFieldsURL.create)
|
await page.goto(geoUrl.list)
|
||||||
const input = page.locator('input[id="field-customTextClientField"]')
|
await expect(page.locator('.app-header .collection-list-button')).toHaveCount(1)
|
||||||
|
await page.locator('button.nav-toggler[aria-label="Open Menu"][tabindex="0"]').click()
|
||||||
const prevSibling = await input.evaluateHandle((el) => {
|
await page.locator(`#nav-posts`).click()
|
||||||
return el.previousElementSibling
|
await expect(page.locator('.app-header .collection-list-button')).toHaveCount(0)
|
||||||
})
|
|
||||||
|
|
||||||
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')
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('select field', () => {
|
|
||||||
test('should render custom select options', async () => {
|
|
||||||
await page.goto(customFieldsURL.create)
|
|
||||||
await page.waitForURL(customFieldsURL.create)
|
|
||||||
await page.locator('#field-customSelectField .rs__control').click()
|
|
||||||
await expect(page.locator('#field-customSelectField .rs__option')).toHaveCount(2)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -731,8 +606,328 @@ describe('admin1', () => {
|
|||||||
await expect(header).toContainText('Here is a custom header')
|
await expect(header).toContainText('Here is a custom header')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('i18n', () => {
|
||||||
|
test('should allow changing language', async () => {
|
||||||
|
await page.goto(postsUrl.account)
|
||||||
|
|
||||||
|
const field = page.locator('.payload-settings__language .react-select')
|
||||||
|
|
||||||
|
await field.click()
|
||||||
|
const options = page.locator('.rs__option')
|
||||||
|
await options.locator('text=Español').click()
|
||||||
|
|
||||||
|
await expect(page.locator('.step-nav a').first().locator('span')).toHaveAttribute(
|
||||||
|
'title',
|
||||||
|
'Tablero',
|
||||||
|
)
|
||||||
|
|
||||||
|
await field.click()
|
||||||
|
await options.locator('text=English').click()
|
||||||
|
await field.click()
|
||||||
|
await expect(page.locator('.form-submit .btn')).toContainText('Save')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should allow custom translation', async () => {
|
||||||
|
await page.goto(postsUrl.account)
|
||||||
|
await expect(page.locator('.step-nav a').first().locator('span')).toHaveAttribute(
|
||||||
|
'title',
|
||||||
|
'Home',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should allow custom translation of locale labels', async () => {
|
||||||
|
const selectOptionClass = '.localizer .popup-button-list__button'
|
||||||
|
const localizerButton = page.locator('.localizer .popup-button')
|
||||||
|
const localeListItem1 = page.locator(selectOptionClass).nth(0)
|
||||||
|
|
||||||
|
async function checkLocaleLabels(firstLabel: string, secondLabel: string) {
|
||||||
|
await localizerButton.click()
|
||||||
|
await expect(page.locator(selectOptionClass).first()).toContainText(firstLabel)
|
||||||
|
await expect(page.locator(selectOptionClass).nth(1)).toContainText(secondLabel)
|
||||||
|
}
|
||||||
|
|
||||||
|
await checkLocaleLabels('Spanish (es)', 'English (en)')
|
||||||
|
|
||||||
|
// Change locale to Spanish
|
||||||
|
await localizerButton.click()
|
||||||
|
await expect(localeListItem1).toContainText('Spanish (es)')
|
||||||
|
await localeListItem1.click()
|
||||||
|
|
||||||
|
// Go to account page
|
||||||
|
await page.goto(postsUrl.account)
|
||||||
|
|
||||||
|
const languageField = page.locator('.payload-settings__language .react-select')
|
||||||
|
const options = page.locator('.rs__option')
|
||||||
|
|
||||||
|
// Change language to Spanish
|
||||||
|
await languageField.click()
|
||||||
|
await options.locator('text=Español').click()
|
||||||
|
|
||||||
|
await checkLocaleLabels('Español (es)', 'Inglés (en)')
|
||||||
|
|
||||||
|
// Change locale and language back to English
|
||||||
|
await languageField.click()
|
||||||
|
await options.locator('text=English').click()
|
||||||
|
await localizerButton.click()
|
||||||
|
await expect(localeListItem1).toContainText('Spanish (es)')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('CRUD', () => {
|
||||||
|
test('should create', async () => {
|
||||||
|
await page.goto(postsUrl.create)
|
||||||
|
await page.locator('#field-title').fill(title)
|
||||||
|
await page.locator('#field-description').fill(description)
|
||||||
|
await saveDocAndAssert(page)
|
||||||
|
await expect(page.locator('#field-title')).toHaveValue(title)
|
||||||
|
await expect(page.locator('#field-description')).toHaveValue(description)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should read existing', async () => {
|
||||||
|
const { id } = await createPost()
|
||||||
|
await page.goto(postsUrl.edit(id))
|
||||||
|
await expect(page.locator('#field-title')).toHaveValue(title)
|
||||||
|
await expect(page.locator('#field-description')).toHaveValue(description)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should update existing', async () => {
|
||||||
|
const { id } = await createPost()
|
||||||
|
await page.goto(postsUrl.edit(id))
|
||||||
|
const newTitle = 'new title'
|
||||||
|
const newDesc = 'new description'
|
||||||
|
await page.locator('#field-title').fill(newTitle)
|
||||||
|
await page.locator('#field-description').fill(newDesc)
|
||||||
|
await saveDocAndAssert(page)
|
||||||
|
await expect(page.locator('#field-title')).toHaveValue(newTitle)
|
||||||
|
await expect(page.locator('#field-description')).toHaveValue(newDesc)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should save using hotkey', async () => {
|
||||||
|
const { id } = await createPost()
|
||||||
|
await page.goto(postsUrl.edit(id))
|
||||||
|
const newTitle = 'new title'
|
||||||
|
await page.locator('#field-title').fill(newTitle)
|
||||||
|
await saveDocHotkeyAndAssert(page)
|
||||||
|
await expect(page.locator('#field-title')).toHaveValue(newTitle)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should delete existing', async () => {
|
||||||
|
const { id, title } = await createPost()
|
||||||
|
await page.goto(postsUrl.edit(id))
|
||||||
|
await openDocControls(page)
|
||||||
|
await page.locator('#action-delete').click()
|
||||||
|
await page.locator('#confirm-delete').click()
|
||||||
|
await expect(page.locator(`text=Post "${title}" successfully deleted.`)).toBeVisible()
|
||||||
|
expect(page.url()).toContain(postsUrl.list)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should bulk delete all on page', async () => {
|
||||||
|
await deleteAllPosts()
|
||||||
|
await Promise.all([createPost(), createPost(), createPost()])
|
||||||
|
await page.goto(postsUrl.list)
|
||||||
|
await page.locator('input#select-all').check()
|
||||||
|
await page.locator('.delete-documents__toggle').click()
|
||||||
|
await page.locator('#confirm-delete').click()
|
||||||
|
|
||||||
|
await expect(page.locator('.payload-toast-container .toast-success')).toHaveText(
|
||||||
|
'Deleted 3 Posts successfully.',
|
||||||
|
)
|
||||||
|
|
||||||
|
await expect(page.locator('.collection-list__no-results')).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should bulk delete with filters and across pages', async () => {
|
||||||
|
await deleteAllPosts()
|
||||||
|
await Promise.all([createPost({ title: 'Post 1' }), createPost({ title: 'Post 2' })])
|
||||||
|
await page.goto(postsUrl.list)
|
||||||
|
await page.locator('#search-filter-input').fill('Post 1')
|
||||||
|
await expect(page.locator('.table table > tbody > tr')).toHaveCount(1)
|
||||||
|
await page.locator('input#select-all').check()
|
||||||
|
await page.locator('button.list-selection__button').click()
|
||||||
|
await page.locator('.delete-documents__toggle').click()
|
||||||
|
await page.locator('#confirm-delete').click()
|
||||||
|
|
||||||
|
await expect(page.locator('.payload-toast-container .toast-success')).toHaveText(
|
||||||
|
'Deleted 1 Post successfully.',
|
||||||
|
)
|
||||||
|
|
||||||
|
await expect(page.locator('.table table > tbody > tr')).toHaveCount(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should bulk update', async () => {
|
||||||
|
// First, delete all posts created by the seed
|
||||||
|
await deleteAllPosts()
|
||||||
|
const post1Title = 'Post'
|
||||||
|
const updatedPostTitle = `${post1Title} (Updated)`
|
||||||
|
await Promise.all([createPost({ title: post1Title }), createPost(), createPost()])
|
||||||
|
await page.goto(postsUrl.list)
|
||||||
|
await page.locator('input#select-all').check()
|
||||||
|
await page.locator('.edit-many__toggle').click()
|
||||||
|
await page.locator('.field-select .rs__control').click()
|
||||||
|
|
||||||
|
const titleOption = page.locator('.field-select .rs__option', {
|
||||||
|
hasText: exactText('Title'),
|
||||||
|
})
|
||||||
|
|
||||||
|
await expect(titleOption).toBeVisible()
|
||||||
|
await titleOption.click()
|
||||||
|
const titleInput = page.locator('#field-title')
|
||||||
|
await expect(titleInput).toBeVisible()
|
||||||
|
await titleInput.fill(updatedPostTitle)
|
||||||
|
await page.locator('.form-submit button[type="submit"].edit-many__publish').click()
|
||||||
|
|
||||||
|
await expect(page.locator('.payload-toast-container .toast-success')).toContainText(
|
||||||
|
'Updated 3 Posts successfully.',
|
||||||
|
)
|
||||||
|
|
||||||
|
await expect(page.locator('.row-1 .cell-title')).toContainText(updatedPostTitle)
|
||||||
|
await expect(page.locator('.row-2 .cell-title')).toContainText(updatedPostTitle)
|
||||||
|
await expect(page.locator('.row-3 .cell-title')).toContainText(updatedPostTitle)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should not override un-edited values in bulk edit if it has a defaultValue', async () => {
|
||||||
|
await deleteAllPosts()
|
||||||
|
const post1Title = 'Post'
|
||||||
|
const postData = {
|
||||||
|
title: 'Post',
|
||||||
|
arrayOfFields: [
|
||||||
|
{
|
||||||
|
optional: 'some optional array field',
|
||||||
|
innerArrayOfFields: [
|
||||||
|
{
|
||||||
|
innerOptional: 'some inner optional array field',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
group: {
|
||||||
|
defaultValueField: 'not the group default value',
|
||||||
|
title: 'some title',
|
||||||
|
},
|
||||||
|
someBlock: [
|
||||||
|
{
|
||||||
|
textFieldForBlock: 'some text for block text',
|
||||||
|
blockType: 'textBlock',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
defaultValueField: 'not the default value',
|
||||||
|
}
|
||||||
|
const updatedPostTitle = `${post1Title} (Updated)`
|
||||||
|
await createPost(postData)
|
||||||
|
await page.goto(postsUrl.list)
|
||||||
|
await page.locator('input#select-all').check()
|
||||||
|
await page.locator('.edit-many__toggle').click()
|
||||||
|
await page.locator('.field-select .rs__control').click()
|
||||||
|
|
||||||
|
const titleOption = page.locator('.field-select .rs__option', {
|
||||||
|
hasText: exactText('Title'),
|
||||||
|
})
|
||||||
|
|
||||||
|
await expect(titleOption).toBeVisible()
|
||||||
|
await titleOption.click()
|
||||||
|
const titleInput = page.locator('#field-title')
|
||||||
|
await expect(titleInput).toBeVisible()
|
||||||
|
await titleInput.fill(updatedPostTitle)
|
||||||
|
await page.locator('.form-submit button[type="submit"].edit-many__publish').click()
|
||||||
|
|
||||||
|
await expect(page.locator('.payload-toast-container .toast-success')).toContainText(
|
||||||
|
'Updated 1 Post successfully.',
|
||||||
|
)
|
||||||
|
|
||||||
|
const updatedPost = await payload.find({
|
||||||
|
collection: 'posts',
|
||||||
|
limit: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(updatedPost.docs[0].title).toBe(updatedPostTitle)
|
||||||
|
expect(updatedPost.docs[0].arrayOfFields.length).toBe(1)
|
||||||
|
expect(updatedPost.docs[0].arrayOfFields[0].optional).toBe('some optional array field')
|
||||||
|
expect(updatedPost.docs[0].arrayOfFields[0].innerArrayOfFields.length).toBe(1)
|
||||||
|
expect(updatedPost.docs[0].someBlock[0].textFieldForBlock).toBe('some text for block text')
|
||||||
|
expect(updatedPost.docs[0].defaultValueField).toBe('not the default value')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should bulk update with filters and across pages', async () => {
|
||||||
|
// First, delete all posts created by the seed
|
||||||
|
await deleteAllPosts()
|
||||||
|
const post1Title = 'Post 1'
|
||||||
|
await Promise.all([createPost({ title: post1Title }), createPost({ title: 'Post 2' })])
|
||||||
|
const updatedPostTitle = `${post1Title} (Updated)`
|
||||||
|
await page.goto(postsUrl.list)
|
||||||
|
await page.locator('#search-filter-input').fill('Post 1')
|
||||||
|
await expect(page.locator('.table table > tbody > tr')).toHaveCount(1)
|
||||||
|
await page.locator('input#select-all').check()
|
||||||
|
await page.locator('button.list-selection__button').click()
|
||||||
|
await page.locator('.edit-many__toggle').click()
|
||||||
|
await page.locator('.field-select .rs__control').click()
|
||||||
|
|
||||||
|
const titleOption = page.locator('.field-select .rs__option', {
|
||||||
|
hasText: exactText('Title'),
|
||||||
|
})
|
||||||
|
|
||||||
|
await expect(titleOption).toBeVisible()
|
||||||
|
await titleOption.click()
|
||||||
|
const titleInput = page.locator('#field-title')
|
||||||
|
await expect(titleInput).toBeVisible()
|
||||||
|
await titleInput.fill(updatedPostTitle)
|
||||||
|
|
||||||
|
await page.locator('.form-submit button[type="submit"].edit-many__publish').click()
|
||||||
|
await expect(page.locator('.payload-toast-container .toast-success')).toContainText(
|
||||||
|
'Updated 1 Post successfully.',
|
||||||
|
)
|
||||||
|
|
||||||
|
await expect(page.locator('.table table > tbody > tr')).toHaveCount(1)
|
||||||
|
await expect(page.locator('.row-1 .cell-title')).toContainText(updatedPostTitle)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should save globals', async () => {
|
||||||
|
await page.goto(postsUrl.global(globalSlug))
|
||||||
|
|
||||||
|
await page.locator('#field-title').fill(title)
|
||||||
|
await saveDocAndAssert(page)
|
||||||
|
|
||||||
|
await expect(page.locator('#field-title')).toHaveValue(title)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should hide duplicate when disableDuplicate: true', async () => {
|
||||||
|
await page.goto(disableDuplicateURL.create)
|
||||||
|
await page.locator('#field-title').fill(title)
|
||||||
|
await saveDocAndAssert(page)
|
||||||
|
await page.locator('.doc-controls__popup >> .popup-button').click()
|
||||||
|
await expect(page.locator('#action-duplicate')).toBeHidden()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should properly close leave-without-saving modal after clicking leave-anyway button', async () => {
|
||||||
|
const { id } = await createPost()
|
||||||
|
await page.goto(postsUrl.edit(id))
|
||||||
|
const title = 'title'
|
||||||
|
await page.locator('#field-title').fill(title)
|
||||||
|
await saveDocHotkeyAndAssert(page)
|
||||||
|
await expect(page.locator('#field-title')).toHaveValue(title)
|
||||||
|
|
||||||
|
const newTitle = 'new title'
|
||||||
|
await page.locator('#field-title').fill(newTitle)
|
||||||
|
|
||||||
|
await page.locator('header.app-header a[href="/admin/collections/posts"]').click()
|
||||||
|
|
||||||
|
// Locate the modal container
|
||||||
|
const modalContainer = page.locator('.payload__modal-container')
|
||||||
|
await expect(modalContainer).toBeVisible()
|
||||||
|
|
||||||
|
// Click the "Leave anyway" button
|
||||||
|
await page.locator('.leave-without-saving__controls .btn--style-primary').click()
|
||||||
|
|
||||||
|
// Assert that the class on the modal container changes to 'payload__modal-container--exitDone'
|
||||||
|
await expect(modalContainer).toHaveClass(/payload__modal-container--exitDone/)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
async function deleteAllPosts() {
|
||||||
|
await payload.delete({ collection: postsCollectionSlug, where: { id: { exists: true } } })
|
||||||
|
}
|
||||||
|
|
||||||
async function createPost(overrides?: Partial<Post>): Promise<Post> {
|
async function createPost(overrides?: Partial<Post>): Promise<Post> {
|
||||||
return payload.create({
|
return payload.create({
|
||||||
collection: postsCollectionSlug,
|
collection: postsCollectionSlug,
|
||||||
@@ -743,3 +938,13 @@ async function createPost(overrides?: Partial<Post>): Promise<Post> {
|
|||||||
},
|
},
|
||||||
}) as unknown as Promise<Post>
|
}) as unknown as Promise<Post>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function createGeo(overrides?: Partial<Geo>): Promise<Geo> {
|
||||||
|
return payload.create({
|
||||||
|
collection: geoCollectionSlug,
|
||||||
|
data: {
|
||||||
|
point: [4, -4],
|
||||||
|
...overrides,
|
||||||
|
},
|
||||||
|
}) as unknown as Promise<Geo>
|
||||||
|
}
|
||||||
820
test/admin/e2e/list-view/e2e.spec.ts
Normal file
820
test/admin/e2e/list-view/e2e.spec.ts
Normal file
@@ -0,0 +1,820 @@
|
|||||||
|
import type { Page } from '@playwright/test'
|
||||||
|
|
||||||
|
import { expect, test } from '@playwright/test'
|
||||||
|
import { mapAsync } from 'payload'
|
||||||
|
import * as qs from 'qs-esm'
|
||||||
|
|
||||||
|
import type { Config, Geo, Post } from '../../payload-types.js'
|
||||||
|
|
||||||
|
import {
|
||||||
|
ensureCompilationIsDone,
|
||||||
|
exactText,
|
||||||
|
getRoutes,
|
||||||
|
initPageConsoleErrorCatch,
|
||||||
|
openDocDrawer,
|
||||||
|
} from '../../../helpers.js'
|
||||||
|
import { AdminUrlUtil } from '../../../helpers/adminUrlUtil.js'
|
||||||
|
import { initPayloadE2ENoConfig } from '../../../helpers/initPayloadE2ENoConfig.js'
|
||||||
|
import { customAdminRoutes } from '../../shared.js'
|
||||||
|
import { customViews1CollectionSlug, geoCollectionSlug, postsCollectionSlug } from '../../slugs.js'
|
||||||
|
|
||||||
|
const { beforeAll, beforeEach, describe } = test
|
||||||
|
|
||||||
|
const title = 'Title'
|
||||||
|
const description = 'Description'
|
||||||
|
|
||||||
|
let payload: PayloadTestSDK<Config>
|
||||||
|
|
||||||
|
import { goToFirstCell } from 'helpers/e2e/navigateToDoc.js'
|
||||||
|
import { openListColumns } from 'helpers/e2e/openListColumns.js'
|
||||||
|
import { openListFilters } from 'helpers/e2e/openListFilters.js'
|
||||||
|
import { toggleColumn } from 'helpers/e2e/toggleColumn.js'
|
||||||
|
import path from 'path'
|
||||||
|
import { wait } from 'payload/shared'
|
||||||
|
import { fileURLToPath } from 'url'
|
||||||
|
|
||||||
|
import type { PayloadTestSDK } from '../../../helpers/sdk/index.js'
|
||||||
|
|
||||||
|
import { reorderColumns } from '../../../helpers/e2e/reorderColumns.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('List View', () => {
|
||||||
|
let page: Page
|
||||||
|
let geoUrl: AdminUrlUtil
|
||||||
|
let postsUrl: AdminUrlUtil
|
||||||
|
let baseListFiltersUrl: AdminUrlUtil
|
||||||
|
let customViewsUrl: AdminUrlUtil
|
||||||
|
|
||||||
|
let serverURL: string
|
||||||
|
let adminRoutes: ReturnType<typeof getRoutes>
|
||||||
|
|
||||||
|
beforeAll(async ({ browser }, testInfo) => {
|
||||||
|
const prebuild = false // 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)
|
||||||
|
baseListFiltersUrl = new AdminUrlUtil(serverURL, 'base-list-filters')
|
||||||
|
customViewsUrl = new AdminUrlUtil(serverURL, customViews1CollectionSlug)
|
||||||
|
|
||||||
|
const context = await browser.newContext()
|
||||||
|
page = await context.newPage()
|
||||||
|
initPageConsoleErrorCatch(page)
|
||||||
|
|
||||||
|
await ensureCompilationIsDone({ customAdminRoutes, page, serverURL })
|
||||||
|
|
||||||
|
adminRoutes = getRoutes({ customAdminRoutes })
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await reInitializeDB({
|
||||||
|
serverURL,
|
||||||
|
snapshotKey: 'adminTests',
|
||||||
|
})
|
||||||
|
|
||||||
|
await ensureCompilationIsDone({ customAdminRoutes, page, serverURL })
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
})
|
||||||
|
|
||||||
|
const tableRowLocator = 'table > tbody > tr'
|
||||||
|
|
||||||
|
describe('list view descriptions', () => {
|
||||||
|
test('should render static collection descriptions', async () => {
|
||||||
|
await page.goto(postsUrl.list)
|
||||||
|
await expect(
|
||||||
|
page.locator('.view-description', {
|
||||||
|
hasText: exactText('This is a custom collection description.'),
|
||||||
|
}),
|
||||||
|
).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should render dynamic collection description components', async () => {
|
||||||
|
await page.goto(customViewsUrl.list)
|
||||||
|
await expect(
|
||||||
|
page.locator('.view-description', {
|
||||||
|
hasText: exactText('This is a custom view description component.'),
|
||||||
|
}),
|
||||||
|
).toBeVisible()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
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(new RegExp(`${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('search should persist through browser back button', async () => {
|
||||||
|
const url = `${postsUrl.list}?limit=10&page=1&search=post1`
|
||||||
|
await page.goto(url)
|
||||||
|
await page.waitForURL(url)
|
||||||
|
await expect(page.locator('#search-filter-input')).toHaveValue('post1')
|
||||||
|
await goToFirstCell(page, postsUrl)
|
||||||
|
await page.goBack()
|
||||||
|
await wait(1000) // wait one second to ensure that the new view does not accidentally reset the search
|
||||||
|
await page.waitForURL(url)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('search should not persist between navigation', async () => {
|
||||||
|
const url = `${postsUrl.list}?limit=10&page=1&search=test`
|
||||||
|
await page.goto(url)
|
||||||
|
await page.waitForURL(url)
|
||||||
|
|
||||||
|
await expect(page.locator('#search-filter-input')).toHaveValue('test')
|
||||||
|
|
||||||
|
await page.locator('.nav-toggler.template-default__nav-toggler').click()
|
||||||
|
await expect(page.locator('#nav-uploads')).toContainText('Uploads')
|
||||||
|
|
||||||
|
const uploadsUrl = await page.locator('#nav-uploads').getAttribute('href')
|
||||||
|
await page.goto(serverURL + uploadsUrl)
|
||||||
|
await page.waitForURL(serverURL + uploadsUrl)
|
||||||
|
|
||||||
|
await expect(page.locator('#search-filter-input')).toHaveValue('')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should toggle columns', async () => {
|
||||||
|
const columnCountLocator = 'table > thead > tr > th'
|
||||||
|
await createPost()
|
||||||
|
await openListColumns(page, {})
|
||||||
|
const numberOfColumns = await page.locator(columnCountLocator).count()
|
||||||
|
await expect(page.locator('.column-selector')).toBeVisible()
|
||||||
|
await expect(page.locator('table > thead > tr > th:nth-child(2)')).toHaveText('ID')
|
||||||
|
await toggleColumn(page, { columnLabel: 'ID', targetState: 'off' })
|
||||||
|
await page.locator('#heading-id').waitFor({ state: 'detached' })
|
||||||
|
await page.locator('.cell-id').first().waitFor({ state: 'detached' })
|
||||||
|
await expect(page.locator(columnCountLocator)).toHaveCount(numberOfColumns - 1)
|
||||||
|
await expect(page.locator('table > thead > tr > th:nth-child(2)')).toHaveText('Number')
|
||||||
|
await toggleColumn(page, { columnLabel: 'ID', targetState: 'on' })
|
||||||
|
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}`,
|
||||||
|
)
|
||||||
|
|
||||||
|
await page.locator('.list-controls__toggle-columns').click()
|
||||||
|
await expect(page.locator('.list-controls__columns.rah-static--height-auto')).toBeVisible()
|
||||||
|
|
||||||
|
await page
|
||||||
|
.locator('.column-selector .column-selector__column', {
|
||||||
|
hasText: exactText('ID'),
|
||||||
|
})
|
||||||
|
.click()
|
||||||
|
|
||||||
|
await page.locator('#heading-id').waitFor({ state: 'detached' })
|
||||||
|
await page.locator('.cell-id').first().waitFor({ state: 'detached' })
|
||||||
|
|
||||||
|
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 openListFilters(page, {})
|
||||||
|
|
||||||
|
await page.locator('.where-builder__add-first-filter').click()
|
||||||
|
|
||||||
|
const conditionField = page.locator('.condition__field')
|
||||||
|
await conditionField.click()
|
||||||
|
const dropdownFieldOption = conditionField.locator('.rs__option', {
|
||||||
|
hasText: exactText('ID'),
|
||||||
|
})
|
||||||
|
await dropdownFieldOption.click()
|
||||||
|
await expect(page.locator('.condition__field')).toContainText('ID')
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
const tableRows = page.locator(tableRowLocator)
|
||||||
|
|
||||||
|
await expect(tableRows).toHaveCount(1)
|
||||||
|
const firstId = page.locator(tableRowLocator).first().locator('.cell-id')
|
||||||
|
await expect(firstId).toHaveText(`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 openListFilters(page, {})
|
||||||
|
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 dropdownFieldOption = filterField.locator('.rs__option', {
|
||||||
|
hasText: exactText('Status'),
|
||||||
|
})
|
||||||
|
await dropdownFieldOption.click()
|
||||||
|
await expect(filterField).toContainText('Status')
|
||||||
|
|
||||||
|
// expect operator & value field to reset (be empty)
|
||||||
|
await expect(operatorField.locator('.rs__placeholder')).toContainText('Select a value')
|
||||||
|
await expect(page.locator('.condition__value input')).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)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should reset page when filters are applied', async () => {
|
||||||
|
await deleteAllPosts()
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
Array.from({ length: 12 }, async (_, i) => {
|
||||||
|
if (i < 6) {
|
||||||
|
await createPost()
|
||||||
|
} else {
|
||||||
|
await createPost({ title: 'test' })
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
await page.reload()
|
||||||
|
|
||||||
|
const tableItems = page.locator(tableRowLocator)
|
||||||
|
|
||||||
|
await expect(tableItems).toHaveCount(10)
|
||||||
|
await expect(page.locator('.collection-list__page-info')).toHaveText('1-10 of 12')
|
||||||
|
await expect(page.locator('.per-page')).toContainText('Per Page: 10')
|
||||||
|
await page.goto(`${postsUrl.list}?limit=10&page=2`)
|
||||||
|
await openListFilters(page, {})
|
||||||
|
await page.locator('.where-builder__add-first-filter').click()
|
||||||
|
await page.locator('.condition__field .rs__control').click()
|
||||||
|
const options = page.locator('.rs__option')
|
||||||
|
await options.locator('text=Tab 1 > Title').click()
|
||||||
|
await page.locator('.condition__operator .rs__control').click()
|
||||||
|
await options.locator('text=equals').click()
|
||||||
|
await page.locator('.condition__value input').fill('test')
|
||||||
|
await expect(page.locator('.collection-list__page-info')).toHaveText('1-6 of 6')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('table columns', () => {
|
||||||
|
test('should drag to reorder columns and save to preferences', async () => {
|
||||||
|
await createPost()
|
||||||
|
|
||||||
|
await reorderColumns(page, { fromColumn: 'Number', toColumn: 'ID' })
|
||||||
|
|
||||||
|
// 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(page, { fromColumn: 'Number', toColumn: 'ID' })
|
||||||
|
|
||||||
|
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-header__select-collection.react-select',
|
||||||
|
)
|
||||||
|
|
||||||
|
// select the "Post" collection
|
||||||
|
await collectionSelector.click()
|
||||||
|
await page
|
||||||
|
.locator('[id^=list-drawer_1_] .list-header__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-header__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-header__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-header__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-header__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(new RegExp(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 tableItems = page.locator(tableRowLocator)
|
||||||
|
await expect(tableItems).toHaveCount(10)
|
||||||
|
await expect(page.locator('.collection-list__page-info')).toHaveText('1-10 of 11')
|
||||||
|
await expect(page.locator('.per-page')).toContainText('Per Page: 10')
|
||||||
|
await page.locator('.paginator button').nth(1).click()
|
||||||
|
await expect.poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT }).toContain('page=2')
|
||||||
|
await expect(tableItems).toHaveCount(1)
|
||||||
|
await page.locator('.paginator button').nth(0).click()
|
||||||
|
await expect.poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT }).toContain('page=1')
|
||||||
|
await expect(tableItems).toHaveCount(10)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should paginate and maintain perPage', async () => {
|
||||||
|
await deleteAllPosts()
|
||||||
|
|
||||||
|
await mapAsync([...Array(26)], async () => {
|
||||||
|
await createPost()
|
||||||
|
})
|
||||||
|
|
||||||
|
await page.reload()
|
||||||
|
const tableItems = page.locator(tableRowLocator)
|
||||||
|
await expect(tableItems).toHaveCount(10)
|
||||||
|
await expect(page.locator('.collection-list__page-info')).toHaveText('1-10 of 26')
|
||||||
|
await expect(page.locator('.per-page')).toContainText('Per Page: 10')
|
||||||
|
await page.locator('.per-page .popup-button').click()
|
||||||
|
|
||||||
|
await page
|
||||||
|
.locator('.per-page button.per-page__button', {
|
||||||
|
hasText: '25',
|
||||||
|
})
|
||||||
|
.click()
|
||||||
|
|
||||||
|
await expect(tableItems).toHaveCount(25)
|
||||||
|
await expect(page.locator('.per-page .per-page__base-button')).toContainText('Per Page: 25')
|
||||||
|
await page.locator('.paginator button').nth(1).click()
|
||||||
|
await expect.poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT }).toContain('page=2')
|
||||||
|
await expect(tableItems).toHaveCount(1)
|
||||||
|
await expect(page.locator('.per-page')).toContainText('Per Page: 25')
|
||||||
|
await expect(page.locator('.collection-list__page-info')).toHaveText('26-26 of 26')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 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')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should sort with existing filters', async () => {
|
||||||
|
await page.goto(postsUrl.list)
|
||||||
|
await toggleColumn(page, { columnLabel: 'ID', targetState: 'off' })
|
||||||
|
await page.locator('#heading-id').waitFor({ state: 'detached' })
|
||||||
|
await page.locator('#heading-title button.sort-column__asc').click()
|
||||||
|
await page.waitForURL(/sort=title/)
|
||||||
|
|
||||||
|
const columnAfterSort = page.locator(
|
||||||
|
`.list-controls__columns .column-selector .column-selector__column`,
|
||||||
|
{
|
||||||
|
hasText: exactText('ID'),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
await expect(columnAfterSort).not.toHaveClass('column-selector__column--active')
|
||||||
|
await expect(page.locator('#heading-id')).toBeHidden()
|
||||||
|
await expect(page.locator('.cell-id')).toHaveCount(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('i18n', () => {
|
||||||
|
test('should display translated collections and globals config options', async () => {
|
||||||
|
await page.goto(postsUrl.list)
|
||||||
|
await expect(page.locator('#nav-posts')).toContainText('Posts')
|
||||||
|
await expect(page.locator('#nav-global-global')).toContainText('Global')
|
||||||
|
})
|
||||||
|
|
||||||
|
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 openListFilters(page, {})
|
||||||
|
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=Tab 1 > Title')).toHaveText('Tab 1 > 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('base list filters', () => {
|
||||||
|
test('should respect base list filters', async () => {
|
||||||
|
await page.goto(baseListFiltersUrl.list)
|
||||||
|
await page.waitForURL((url) => url.toString().startsWith(baseListFiltersUrl.list))
|
||||||
|
await expect(page.locator(tableRowLocator)).toHaveCount(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
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>
|
||||||
|
}
|
||||||
@@ -68,16 +68,14 @@ const createFirstUser = async ({
|
|||||||
.not.toContain('create-first-user')
|
.not.toContain('create-first-user')
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('auth-basic', () => {
|
describe('Auth (Basic)', () => {
|
||||||
let page: Page
|
let page: Page
|
||||||
let url: AdminUrlUtil
|
let url: AdminUrlUtil
|
||||||
let serverURL: string
|
let serverURL: string
|
||||||
let apiURL: string
|
|
||||||
|
|
||||||
beforeAll(async ({ browser }, testInfo) => {
|
beforeAll(async ({ browser }, testInfo) => {
|
||||||
testInfo.setTimeout(TEST_TIMEOUT_LONG)
|
testInfo.setTimeout(TEST_TIMEOUT_LONG)
|
||||||
;({ payload, serverURL } = await initPayloadE2ENoConfig<Config>({ dirname }))
|
;({ payload, serverURL } = await initPayloadE2ENoConfig<Config>({ dirname }))
|
||||||
apiURL = `${serverURL}/api`
|
|
||||||
url = new AdminUrlUtil(serverURL, 'users')
|
url = new AdminUrlUtil(serverURL, 'users')
|
||||||
|
|
||||||
const context = await browser.newContext()
|
const context = await browser.newContext()
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import type { SanitizedConfig } from 'payload'
|
|||||||
import { expect, test } from '@playwright/test'
|
import { expect, test } from '@playwright/test'
|
||||||
import { devUser } from 'credentials.js'
|
import { devUser } from 'credentials.js'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { wait } from 'payload/shared'
|
|
||||||
import { fileURLToPath } from 'url'
|
import { fileURLToPath } from 'url'
|
||||||
import { v4 as uuid } from 'uuid'
|
import { v4 as uuid } from 'uuid'
|
||||||
|
|
||||||
@@ -82,7 +81,7 @@ const createFirstUser = async ({
|
|||||||
.not.toContain('create-first-user')
|
.not.toContain('create-first-user')
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('auth', () => {
|
describe('Auth', () => {
|
||||||
let page: Page
|
let page: Page
|
||||||
let context: BrowserContext
|
let context: BrowserContext
|
||||||
let url: AdminUrlUtil
|
let url: AdminUrlUtil
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const { beforeAll, describe } = test
|
|||||||
const filename = fileURLToPath(import.meta.url)
|
const filename = fileURLToPath(import.meta.url)
|
||||||
const dirname = path.dirname(filename)
|
const dirname = path.dirname(filename)
|
||||||
|
|
||||||
describe('field error states', () => {
|
describe('Field Error States', () => {
|
||||||
let serverURL: string
|
let serverURL: string
|
||||||
let page: Page
|
let page: Page
|
||||||
let validateDraftsOff: AdminUrlUtil
|
let validateDraftsOff: AdminUrlUtil
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ const { beforeAll, beforeEach, describe } = test
|
|||||||
|
|
||||||
let payload: PayloadTestSDK<Config>
|
let payload: PayloadTestSDK<Config>
|
||||||
|
|
||||||
describe('fields - relationship', () => {
|
describe('Relationship Field', () => {
|
||||||
let url: AdminUrlUtil
|
let url: AdminUrlUtil
|
||||||
let versionedRelationshipFieldURL: AdminUrlUtil
|
let versionedRelationshipFieldURL: AdminUrlUtil
|
||||||
let page: Page
|
let page: Page
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const { beforeAll, beforeEach, describe } = test
|
|||||||
|
|
||||||
let payload: PayloadTestSDK<Config>
|
let payload: PayloadTestSDK<Config>
|
||||||
|
|
||||||
describe('hooks', () => {
|
describe('Hooks', () => {
|
||||||
let url: AdminUrlUtil
|
let url: AdminUrlUtil
|
||||||
let page: Page
|
let page: Page
|
||||||
let serverURL: string
|
let serverURL: string
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ let testsUrl: AdminUrlUtil
|
|||||||
let payload: PayloadTestSDK<Config>
|
let payload: PayloadTestSDK<Config>
|
||||||
let serverURL: string
|
let serverURL: string
|
||||||
|
|
||||||
describe('locked documents', () => {
|
describe('Locked Documents', () => {
|
||||||
beforeAll(async ({ browser }, testInfo) => {
|
beforeAll(async ({ browser }, testInfo) => {
|
||||||
testInfo.setTimeout(TEST_TIMEOUT_LONG)
|
testInfo.setTimeout(TEST_TIMEOUT_LONG)
|
||||||
;({ payload, serverURL } = await initPayloadE2ENoConfig({ dirname }))
|
;({ payload, serverURL } = await initPayloadE2ENoConfig({ dirname }))
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { mediaSlug } from './shared.js'
|
|||||||
const filename = fileURLToPath(import.meta.url)
|
const filename = fileURLToPath(import.meta.url)
|
||||||
const dirname = path.dirname(filename)
|
const dirname = path.dirname(filename)
|
||||||
|
|
||||||
test.describe('Admin Panel', () => {
|
test.describe('Cloud Storage Plugin', () => {
|
||||||
let page: Page
|
let page: Page
|
||||||
let mediaURL: AdminUrlUtil
|
let mediaURL: AdminUrlUtil
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { POLL_TOPASS_TIMEOUT, TEST_TIMEOUT_LONG } from '../playwright.config.js'
|
|||||||
const filename = fileURLToPath(import.meta.url)
|
const filename = fileURLToPath(import.meta.url)
|
||||||
const dirname = path.dirname(filename)
|
const dirname = path.dirname(filename)
|
||||||
|
|
||||||
test.describe('Form Builder', () => {
|
test.describe('Form Builder Plugin', () => {
|
||||||
let page: Page
|
let page: Page
|
||||||
let formsUrl: AdminUrlUtil
|
let formsUrl: AdminUrlUtil
|
||||||
let submissionsUrl: AdminUrlUtil
|
let submissionsUrl: AdminUrlUtil
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { wait } from 'payload/shared'
|
|||||||
import { fileURLToPath } from 'url'
|
import { fileURLToPath } from 'url'
|
||||||
|
|
||||||
import type { PayloadTestSDK } from '../helpers/sdk/index.js'
|
import type { PayloadTestSDK } from '../helpers/sdk/index.js'
|
||||||
import type { Config, Media, Relation } from './payload-types.js'
|
import type { Config } from './payload-types.js'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ensureCompilationIsDone,
|
ensureCompilationIsDone,
|
||||||
@@ -55,7 +55,7 @@ let withOnlyJPEGMetadataURL: AdminUrlUtil
|
|||||||
let relationPreviewURL: AdminUrlUtil
|
let relationPreviewURL: AdminUrlUtil
|
||||||
let customFileNameURL: AdminUrlUtil
|
let customFileNameURL: AdminUrlUtil
|
||||||
|
|
||||||
describe('uploads', () => {
|
describe('Uploads', () => {
|
||||||
let page: Page
|
let page: Page
|
||||||
|
|
||||||
beforeAll(async ({ browser }, testInfo) => {
|
beforeAll(async ({ browser }, testInfo) => {
|
||||||
|
|||||||
@@ -25,7 +25,6 @@
|
|||||||
import type { BrowserContext, Page } from '@playwright/test'
|
import type { BrowserContext, Page } from '@playwright/test'
|
||||||
|
|
||||||
import { expect, test } from '@playwright/test'
|
import { expect, test } from '@playwright/test'
|
||||||
import { navigateToDoc } from 'helpers/e2e/navigateToDoc.js'
|
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { wait } from 'payload/shared'
|
import { wait } from 'payload/shared'
|
||||||
import { fileURLToPath } from 'url'
|
import { fileURLToPath } from 'url'
|
||||||
@@ -41,7 +40,6 @@ import {
|
|||||||
initPageConsoleErrorCatch,
|
initPageConsoleErrorCatch,
|
||||||
saveDocAndAssert,
|
saveDocAndAssert,
|
||||||
selectTableRow,
|
selectTableRow,
|
||||||
throttleTest,
|
|
||||||
} from '../helpers.js'
|
} from '../helpers.js'
|
||||||
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
|
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
|
||||||
import { trackNetworkRequests } from '../helpers/e2e/trackNetworkRequests.js'
|
import { trackNetworkRequests } from '../helpers/e2e/trackNetworkRequests.js'
|
||||||
@@ -71,12 +69,9 @@ const dirname = path.dirname(filename)
|
|||||||
const { beforeAll, beforeEach, describe } = test
|
const { beforeAll, beforeEach, describe } = test
|
||||||
|
|
||||||
let payload: PayloadTestSDK<Config>
|
let payload: PayloadTestSDK<Config>
|
||||||
let global: AdminUrlUtil
|
|
||||||
let id: string
|
|
||||||
|
|
||||||
let context: BrowserContext
|
let context: BrowserContext
|
||||||
|
|
||||||
describe('versions', () => {
|
describe('Versions', () => {
|
||||||
let page: Page
|
let page: Page
|
||||||
let url: AdminUrlUtil
|
let url: AdminUrlUtil
|
||||||
let serverURL: string
|
let serverURL: string
|
||||||
@@ -84,7 +79,6 @@ describe('versions', () => {
|
|||||||
let disablePublishURL: AdminUrlUtil
|
let disablePublishURL: AdminUrlUtil
|
||||||
let customIDURL: AdminUrlUtil
|
let customIDURL: AdminUrlUtil
|
||||||
let postURL: AdminUrlUtil
|
let postURL: AdminUrlUtil
|
||||||
let global: AdminUrlUtil
|
|
||||||
let id: string
|
let id: string
|
||||||
|
|
||||||
beforeAll(async ({ browser }, testInfo) => {
|
beforeAll(async ({ browser }, testInfo) => {
|
||||||
@@ -641,8 +635,8 @@ describe('versions', () => {
|
|||||||
describe('Collections - publish specific locale', () => {
|
describe('Collections - publish specific locale', () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
url = new AdminUrlUtil(serverURL, localizedCollectionSlug)
|
url = new AdminUrlUtil(serverURL, localizedCollectionSlug)
|
||||||
global = new AdminUrlUtil(serverURL, localizedGlobalSlug)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should show publish individual locale dropdown', async () => {
|
test('should show publish individual locale dropdown', async () => {
|
||||||
await page.goto(url.create)
|
await page.goto(url.create)
|
||||||
const publishOptions = page.locator('.doc-controls__controls .popup')
|
const publishOptions = page.locator('.doc-controls__controls .popup')
|
||||||
|
|||||||
Reference in New Issue
Block a user