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:
Jacob Fletcher
2024-12-28 00:28:37 -05:00
committed by GitHub
parent 466ea292b9
commit 0be1a1d880
18 changed files with 1720 additions and 1755 deletions

View File

@@ -278,9 +278,9 @@ jobs:
suite:
- _community
- access-control
- admin__e2e__1
- admin__e2e__2
- admin__e2e__3
- admin__e2e__general
- admin__e2e__list-view
- admin__e2e__document-view
- admin-root
- auth
- auth-basic

View File

@@ -12,7 +12,7 @@ import { TEST_TIMEOUT_LONG } from '../playwright.config.js'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
test.describe('Admin Panel', () => {
test.describe('Community', () => {
let page: Page
let url: AdminUrlUtil

View File

@@ -54,7 +54,7 @@ const dirname = path.dirname(filename)
const { beforeAll, describe } = test
let payload: PayloadTestSDK<Config>
describe('access control', () => {
describe('Access Control', () => {
let page: Page
let url: AdminUrlUtil
let restrictedUrl: AdminUrlUtil

View File

@@ -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>
}

View File

@@ -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>
}

View 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>
}

View File

@@ -2,7 +2,7 @@ import type { Page } 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 {
ensureCompilationIsDone,
@@ -11,6 +11,7 @@ import {
initPageConsoleErrorCatch,
openNav,
saveDocAndAssert,
saveDocHotkeyAndAssert,
} from '../../../helpers.js'
import { AdminUrlUtil } from '../../../helpers/adminUrlUtil.js'
import { initPayloadE2ENoConfig } from '../../../helpers/initPayloadE2ENoConfig.js'
@@ -18,15 +19,10 @@ import {
customAdminRoutes,
customCollectionMetaTitle,
customDefaultTabMetaTitle,
customEditLabel,
customNestedTabViewPath,
customNestedTabViewTitle,
customNestedViewPath,
customNestedViewTitle,
customRootViewMetaTitle,
customTabLabel,
customTabViewPath,
customTabViewTitle,
customVersionsTabMetaTitle,
customViewMetaTitle,
customViewPath,
@@ -36,9 +32,9 @@ import {
slugPluralLabel,
} from '../../shared.js'
import {
customFieldsSlug,
customGlobalViews2GlobalSlug,
customViews2CollectionSlug,
disableDuplicateSlug,
geoCollectionSlug,
globalSlug,
notInViewCollectionSlug,
postsCollectionSlug,
@@ -53,6 +49,7 @@ 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'
@@ -64,13 +61,14 @@ const filename = fileURLToPath(import.meta.url)
const currentFolder = path.dirname(filename)
const dirname = path.resolve(currentFolder, '../../')
describe('admin1', () => {
describe('General', () => {
let page: Page
let postsUrl: AdminUrlUtil
let geoUrl: AdminUrlUtil
let notInViewUrl: AdminUrlUtil
let globalURL: AdminUrlUtil
let customViewsURL: AdminUrlUtil
let customFieldsURL: AdminUrlUtil
let disableDuplicateURL: AdminUrlUtil
let serverURL: string
let adminRoutes: ReturnType<typeof getRoutes>
@@ -85,10 +83,11 @@ describe('admin1', () => {
prebuild,
}))
postsUrl = new AdminUrlUtil(serverURL, postsCollectionSlug)
geoUrl = new AdminUrlUtil(serverURL, geoCollectionSlug)
notInViewUrl = new AdminUrlUtil(serverURL, notInViewCollectionSlug)
globalURL = new AdminUrlUtil(serverURL, globalSlug)
customViewsURL = new AdminUrlUtil(serverURL, customViews2CollectionSlug)
customFieldsURL = new AdminUrlUtil(serverURL, customFieldsSlug)
disableDuplicateURL = new AdminUrlUtil(serverURL, disableDuplicateSlug)
const context = await browser.newContext()
page = await context.newPage()
@@ -98,6 +97,7 @@ describe('admin1', () => {
adminRoutes = getRoutes({ customAdminRoutes })
})
beforeEach(async () => {
await reInitializeDB({
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', () => {
test('should render custom providers', async () => {
await page.goto(`${serverURL}/admin`)
@@ -493,14 +503,14 @@ describe('admin1', () => {
})
})
describe('custom views', () => {
test('root — should render custom view', async () => {
describe('custom root views', () => {
test('should render custom view', async () => {
await page.goto(`${serverURL}${adminRoutes.routes.admin}${customViewPath}`)
await page.waitForURL(`**${adminRoutes.routes.admin}${customViewPath}`)
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}`)
const pageURL = page.url()
const pathname = new URL(pageURL).pathname
@@ -508,13 +518,13 @@ describe('admin1', () => {
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.waitForURL(`**${adminRoutes.routes.admin}${publicCustomViewPath}`)
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.waitForURL(`**${adminRoutes.routes.admin}/unauthorized`)
await expect(page.locator('.unauthorized')).toBeVisible()
@@ -531,196 +541,61 @@ describe('admin1', () => {
await page.waitForURL(`**${adminRoutes.routes.admin}${protectedCustomNestedViewPath}`)
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)
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('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('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('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('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('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('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('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')
})
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)
})
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('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('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('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('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('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('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)
})
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)
})
})
@@ -731,8 +606,328 @@ describe('admin1', () => {
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> {
return payload.create({
collection: postsCollectionSlug,
@@ -743,3 +938,13 @@ async function createPost(overrides?: Partial<Post>): 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>
}

View 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>
}

View File

@@ -68,16 +68,14 @@ const createFirstUser = async ({
.not.toContain('create-first-user')
}
describe('auth-basic', () => {
describe('Auth (Basic)', () => {
let page: Page
let url: AdminUrlUtil
let serverURL: string
let apiURL: string
beforeAll(async ({ browser }, testInfo) => {
testInfo.setTimeout(TEST_TIMEOUT_LONG)
;({ payload, serverURL } = await initPayloadE2ENoConfig<Config>({ dirname }))
apiURL = `${serverURL}/api`
url = new AdminUrlUtil(serverURL, 'users')
const context = await browser.newContext()

View File

@@ -4,7 +4,6 @@ import type { SanitizedConfig } from 'payload'
import { expect, test } from '@playwright/test'
import { devUser } from 'credentials.js'
import path from 'path'
import { wait } from 'payload/shared'
import { fileURLToPath } from 'url'
import { v4 as uuid } from 'uuid'
@@ -82,7 +81,7 @@ const createFirstUser = async ({
.not.toContain('create-first-user')
}
describe('auth', () => {
describe('Auth', () => {
let page: Page
let context: BrowserContext
let url: AdminUrlUtil

View File

@@ -15,7 +15,7 @@ const { beforeAll, describe } = test
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
describe('field error states', () => {
describe('Field Error States', () => {
let serverURL: string
let page: Page
let validateDraftsOff: AdminUrlUtil

View File

@@ -51,7 +51,7 @@ const { beforeAll, beforeEach, describe } = test
let payload: PayloadTestSDK<Config>
describe('fields - relationship', () => {
describe('Relationship Field', () => {
let url: AdminUrlUtil
let versionedRelationshipFieldURL: AdminUrlUtil
let page: Page

View File

@@ -21,7 +21,7 @@ const { beforeAll, beforeEach, describe } = test
let payload: PayloadTestSDK<Config>
describe('hooks', () => {
describe('Hooks', () => {
let url: AdminUrlUtil
let page: Page
let serverURL: string

View File

@@ -36,7 +36,7 @@ let testsUrl: AdminUrlUtil
let payload: PayloadTestSDK<Config>
let serverURL: string
describe('locked documents', () => {
describe('Locked Documents', () => {
beforeAll(async ({ browser }, testInfo) => {
testInfo.setTimeout(TEST_TIMEOUT_LONG)
;({ payload, serverURL } = await initPayloadE2ENoConfig({ dirname }))

View File

@@ -13,7 +13,7 @@ import { mediaSlug } from './shared.js'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
test.describe('Admin Panel', () => {
test.describe('Cloud Storage Plugin', () => {
let page: Page
let mediaURL: AdminUrlUtil

View File

@@ -15,7 +15,7 @@ import { POLL_TOPASS_TIMEOUT, TEST_TIMEOUT_LONG } from '../playwright.config.js'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
test.describe('Form Builder', () => {
test.describe('Form Builder Plugin', () => {
let page: Page
let formsUrl: AdminUrlUtil
let submissionsUrl: AdminUrlUtil

View File

@@ -6,7 +6,7 @@ import { wait } from 'payload/shared'
import { fileURLToPath } from 'url'
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 {
ensureCompilationIsDone,
@@ -55,7 +55,7 @@ let withOnlyJPEGMetadataURL: AdminUrlUtil
let relationPreviewURL: AdminUrlUtil
let customFileNameURL: AdminUrlUtil
describe('uploads', () => {
describe('Uploads', () => {
let page: Page
beforeAll(async ({ browser }, testInfo) => {

View File

@@ -25,7 +25,6 @@
import type { BrowserContext, Page } from '@playwright/test'
import { expect, test } from '@playwright/test'
import { navigateToDoc } from 'helpers/e2e/navigateToDoc.js'
import path from 'path'
import { wait } from 'payload/shared'
import { fileURLToPath } from 'url'
@@ -41,7 +40,6 @@ import {
initPageConsoleErrorCatch,
saveDocAndAssert,
selectTableRow,
throttleTest,
} from '../helpers.js'
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
import { trackNetworkRequests } from '../helpers/e2e/trackNetworkRequests.js'
@@ -71,12 +69,9 @@ const dirname = path.dirname(filename)
const { beforeAll, beforeEach, describe } = test
let payload: PayloadTestSDK<Config>
let global: AdminUrlUtil
let id: string
let context: BrowserContext
describe('versions', () => {
describe('Versions', () => {
let page: Page
let url: AdminUrlUtil
let serverURL: string
@@ -84,7 +79,6 @@ describe('versions', () => {
let disablePublishURL: AdminUrlUtil
let customIDURL: AdminUrlUtil
let postURL: AdminUrlUtil
let global: AdminUrlUtil
let id: string
beforeAll(async ({ browser }, testInfo) => {
@@ -641,8 +635,8 @@ describe('versions', () => {
describe('Collections - publish specific locale', () => {
beforeAll(() => {
url = new AdminUrlUtil(serverURL, localizedCollectionSlug)
global = new AdminUrlUtil(serverURL, localizedGlobalSlug)
})
test('should show publish individual locale dropdown', async () => {
await page.goto(url.create)
const publishOptions = page.locator('.doc-controls__controls .popup')