Adds a new `addListFilter` e2e helper. This will help to standardize this common functionality across all tests that require filtering list tables and help reduce the overall lines of code within each test file.
1184 lines
42 KiB
TypeScript
1184 lines
42 KiB
TypeScript
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,
|
|
with300DocumentsSlug,
|
|
} 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'
|
|
import { addListFilter } from 'helpers/e2e/addListFilter.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 with300DocumentsUrl: 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)
|
|
with300DocumentsUrl = new AdminUrlUtil(serverURL, with300DocumentsSlug)
|
|
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('list view table', () => {
|
|
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}`,
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('list view custom components', () => {
|
|
test('should render custom beforeList component', async () => {
|
|
await page.goto(postsUrl.list)
|
|
await expect(
|
|
page.locator('.collection-list--posts').locator('div', {
|
|
hasText: exactText('BeforeList custom component'),
|
|
}),
|
|
).toBeVisible()
|
|
})
|
|
|
|
test('should render custom beforeListTable component', async () => {
|
|
await page.goto(postsUrl.list)
|
|
await expect(
|
|
page.locator('.collection-list__wrap').locator('div', {
|
|
hasText: exactText('BeforeListTable custom component'),
|
|
}),
|
|
).toBeVisible()
|
|
})
|
|
|
|
test('should render custom Cell component in table', async () => {
|
|
await page.goto(postsUrl.list)
|
|
await expect(
|
|
page
|
|
.locator(`${tableRowLocator} td.cell-demoUIField`)
|
|
.first()
|
|
.locator('p', {
|
|
hasText: exactText('Demo UI Field Cell'),
|
|
}),
|
|
).toBeVisible()
|
|
})
|
|
|
|
test('should render custom afterList component', async () => {
|
|
await page.goto(postsUrl.list)
|
|
await expect(
|
|
page.locator('.collection-list__wrap').locator('div', {
|
|
hasText: exactText('AfterListTable custom component'),
|
|
}),
|
|
).toBeVisible()
|
|
})
|
|
|
|
test('should render custom afterListTable component', async () => {
|
|
await page.goto(postsUrl.list)
|
|
await expect(
|
|
page.locator('.collection-list--posts').locator('div', {
|
|
hasText: exactText('AfterList custom component'),
|
|
}),
|
|
).toBeVisible()
|
|
})
|
|
})
|
|
|
|
describe('search', () => {
|
|
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('')
|
|
})
|
|
})
|
|
|
|
describe('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)
|
|
})
|
|
|
|
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 addListFilter({
|
|
page,
|
|
fieldLabel: 'ID',
|
|
operatorLabel: 'equals',
|
|
value: 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: ', '')
|
|
|
|
await addListFilter({
|
|
page,
|
|
fieldLabel: 'ID',
|
|
operatorLabel: 'equals',
|
|
value: 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)
|
|
const operatorField = page.locator('.condition__operator')
|
|
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: 6 }, async (_, i) => {
|
|
if (i < 3) {
|
|
await createPost()
|
|
} else {
|
|
await createPost({ title: 'test' })
|
|
}
|
|
}),
|
|
)
|
|
|
|
await page.reload()
|
|
|
|
const tableItems = page.locator(tableRowLocator)
|
|
|
|
await expect(tableItems).toHaveCount(5)
|
|
await expect(page.locator('.collection-list__page-info')).toHaveText('1-5 of 6')
|
|
await expect(page.locator('.per-page')).toContainText('Per Page: 5')
|
|
await page.goto(`${postsUrl.list}?limit=5&page=2`)
|
|
|
|
await addListFilter({
|
|
page,
|
|
fieldLabel: 'Tab 1 > Title',
|
|
operatorLabel: 'equals',
|
|
value: 'test',
|
|
})
|
|
|
|
await page.waitForURL(new RegExp(`${postsUrl.list}\\?limit=5&page=1`))
|
|
await expect(page.locator('.collection-list__page-info')).toHaveText('1-3 of 3')
|
|
})
|
|
|
|
test('should reset filter values for every additional filters', async () => {
|
|
await page.goto(postsUrl.list)
|
|
await openListFilters(page, {})
|
|
|
|
await page.locator('.where-builder__add-first-filter').click()
|
|
const firstConditionField = page.locator('.condition__field')
|
|
const firstOperatorField = page.locator('.condition__operator')
|
|
const firstValueField = page.locator('.condition__value >> input')
|
|
|
|
await firstConditionField.click()
|
|
await firstConditionField
|
|
.locator('.rs__option', {
|
|
hasText: exactText('Tab 1 > Title'),
|
|
})
|
|
.click()
|
|
|
|
await expect(firstConditionField.locator('.rs__single-value')).toContainText('Tab 1 > Title')
|
|
await firstOperatorField.click()
|
|
await firstOperatorField.locator('.rs__option').locator('text=equals').click()
|
|
await firstValueField.fill('Test')
|
|
await expect(firstValueField).toHaveValue('Test')
|
|
await page.locator('.condition__actions-add').click()
|
|
const secondLi = page.locator('.where-builder__and-filters li:nth-child(2)')
|
|
await expect(secondLi).toBeVisible()
|
|
|
|
await expect(
|
|
secondLi.locator('.condition__field').locator('.rs__single-value'),
|
|
).toContainText('Tab 1 > Title')
|
|
|
|
await expect(secondLi.locator('.condition__operator >> input')).toHaveValue('')
|
|
await expect(secondLi.locator('.condition__value >> input')).toHaveValue('')
|
|
})
|
|
|
|
test('should not re-render page upon typing in a value in the filter value field', async () => {
|
|
await page.goto(postsUrl.list)
|
|
|
|
await addListFilter({
|
|
page,
|
|
fieldLabel: 'Tab 1 > Title',
|
|
operatorLabel: 'equals',
|
|
skipValueInput: true,
|
|
})
|
|
|
|
const valueInput = page.locator('.condition__value >> input')
|
|
|
|
// Type into the input field instead of filling it
|
|
await valueInput.click()
|
|
await valueInput.type('Test', { delay: 100 }) // Add a delay to simulate typing speed
|
|
|
|
// Wait for a short period to see if the input loses focus
|
|
await page.waitForTimeout(500)
|
|
|
|
// Check if the input still has the correct value
|
|
await expect(valueInput).toHaveValue('Test')
|
|
})
|
|
|
|
test('should still show second filter if two filters exist and first filter is removed', async () => {
|
|
await page.goto(postsUrl.list)
|
|
|
|
await addListFilter({
|
|
page,
|
|
fieldLabel: 'Tab 1 > Title',
|
|
operatorLabel: 'equals',
|
|
value: 'Test 1',
|
|
})
|
|
|
|
await wait(500)
|
|
|
|
await page.locator('.condition__actions-add').click()
|
|
|
|
const secondLi = page.locator('.where-builder__and-filters li:nth-child(2)')
|
|
await expect(secondLi).toBeVisible()
|
|
const secondConditionField = secondLi.locator('.condition__field')
|
|
const secondOperatorField = secondLi.locator('.condition__operator')
|
|
const secondValueField = secondLi.locator('.condition__value >> input')
|
|
await secondConditionField.click()
|
|
|
|
await secondConditionField
|
|
.locator('.rs__option', { hasText: exactText('Tab 1 > Title') })
|
|
.click()
|
|
|
|
await expect(secondConditionField.locator('.rs__single-value')).toContainText('Tab 1 > Title')
|
|
await secondOperatorField.click()
|
|
await secondOperatorField.locator('.rs__option').locator('text=equals').click()
|
|
await secondValueField.fill('Test 2')
|
|
await expect(secondValueField).toHaveValue('Test 2')
|
|
|
|
const firstLi = page.locator('.where-builder__and-filters li:nth-child(1)')
|
|
const removeButton = firstLi.locator('.condition__actions-remove')
|
|
|
|
await wait(500)
|
|
|
|
// remove first filter
|
|
await removeButton.click()
|
|
const filterListItems = page.locator('.where-builder__and-filters li')
|
|
await expect(filterListItems).toHaveCount(1)
|
|
const firstValueField = page.locator('.condition__value >> input')
|
|
await expect(firstValueField).toHaveValue('Test 2')
|
|
})
|
|
|
|
test('should hide field filter when admin.disableListFilter is true', async () => {
|
|
await page.goto(postsUrl.list)
|
|
await openListFilters(page, {})
|
|
await page.locator('.where-builder__add-first-filter').click()
|
|
|
|
const initialField = page.locator('.condition__field')
|
|
await initialField.click()
|
|
|
|
await expect(
|
|
initialField.locator(`.rs__option :has-text("Disable List Filter Text")`),
|
|
).toBeHidden()
|
|
})
|
|
|
|
test('should simply disable field filter when admin.disableListFilter is true but still exists in the query', async () => {
|
|
await page.goto(
|
|
`${postsUrl.list}${qs.stringify(
|
|
{
|
|
where: {
|
|
or: [
|
|
{
|
|
and: [
|
|
{
|
|
disableListFilterText: {
|
|
equals: 'Disable List Filter Text',
|
|
},
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
},
|
|
{ addQueryPrefix: true },
|
|
)}`,
|
|
)
|
|
|
|
await openListFilters(page, {})
|
|
|
|
const condition = page.locator('.condition__field')
|
|
await expect(condition.locator('input.rs__input')).toBeDisabled()
|
|
await expect(page.locator('.condition__operator input.rs__input')).toBeDisabled()
|
|
await expect(page.locator('.condition__value input.condition-value-text')).toBeDisabled()
|
|
await expect(condition.locator('.rs__single-value')).toHaveText('Disable List Filter Text')
|
|
await page.locator('button.condition__actions-add').click()
|
|
const condition2 = page.locator('.condition__field').nth(1)
|
|
await condition2.click()
|
|
await expect(
|
|
condition2?.locator('.rs__menu-list:has-text("Disable List Filter Text")'),
|
|
).toBeHidden()
|
|
})
|
|
})
|
|
|
|
describe('WhereBuilder', () => {
|
|
test('should render where builder', async () => {
|
|
await page.goto(
|
|
`${with300DocumentsUrl.list}?limit=10&page=1&where%5Bor%5D%5B0%5D%5Band%5D%5B0%5D%5BselfRelation%5D%5Bequals%5D=null`,
|
|
)
|
|
const valueField = page.locator('.condition__value')
|
|
await valueField.click()
|
|
await page.keyboard.type('4')
|
|
const options = page.getByRole('option')
|
|
expect(options).toHaveCount(10)
|
|
for (const option of await options.all()) {
|
|
expect(option).toHaveText('4')
|
|
}
|
|
await page.keyboard.press('Backspace')
|
|
await page.keyboard.type('5')
|
|
expect(options).toHaveCount(10)
|
|
for (const option of await options.all()) {
|
|
expect(option).toHaveText('5')
|
|
}
|
|
// await options.last().scrollIntoViewIfNeeded()
|
|
await options.first().hover()
|
|
// three times because react-select is not very reliable
|
|
await page.mouse.wheel(0, 50)
|
|
await page.mouse.wheel(0, 50)
|
|
await page.mouse.wheel(0, 50)
|
|
expect(options).toHaveCount(20)
|
|
})
|
|
})
|
|
|
|
describe('table columns', () => {
|
|
test('should hide field column when field.hidden is true', async () => {
|
|
await page.goto(postsUrl.list)
|
|
await page.locator('.list-controls__toggle-columns').click()
|
|
|
|
await expect(page.locator('.column-selector')).toBeVisible()
|
|
|
|
await expect(
|
|
page.locator(`.column-selector .column-selector__column`, {
|
|
hasText: exactText('Hidden Field'),
|
|
}),
|
|
).toBeHidden()
|
|
})
|
|
|
|
test('should show field column despite admin.hidden being true', async () => {
|
|
await page.goto(postsUrl.list)
|
|
await page.locator('.list-controls__toggle-columns').click()
|
|
|
|
await expect(page.locator('.column-selector')).toBeVisible()
|
|
|
|
await expect(
|
|
page.locator(`.column-selector .column-selector__column`, {
|
|
hasText: exactText('Admin Hidden Field'),
|
|
}),
|
|
).toBeVisible()
|
|
})
|
|
|
|
test('should hide field in column selector when admin.disableListColumn is true', async () => {
|
|
await page.goto(postsUrl.list)
|
|
await page.locator('.list-controls__toggle-columns').click()
|
|
|
|
await expect(page.locator('.column-selector')).toBeVisible()
|
|
|
|
// Check if "Disable List Column Text" is not present in the column options
|
|
await expect(
|
|
page.locator(`.column-selector .column-selector__column`, {
|
|
hasText: exactText('Disable List Column Text'),
|
|
}),
|
|
).toBeHidden()
|
|
})
|
|
|
|
test('should display field in column selector despite admin.disableListFilter', async () => {
|
|
await page.goto(postsUrl.list)
|
|
await page.locator('.list-controls__toggle-columns').click()
|
|
|
|
await expect(page.locator('.column-selector')).toBeVisible()
|
|
|
|
// Check if "Disable List Filter Text" is present in the column options
|
|
await expect(
|
|
page.locator(`.column-selector .column-selector__column`, {
|
|
hasText: exactText('Disable List Filter Text'),
|
|
}),
|
|
).toBeVisible()
|
|
})
|
|
|
|
test('should still show field in filter when admin.disableListColumn is true', async () => {
|
|
await page.goto(postsUrl.list)
|
|
await openListFilters(page, {})
|
|
await page.locator('.where-builder__add-first-filter').click()
|
|
|
|
const initialField = page.locator('.condition__field')
|
|
await initialField.click()
|
|
|
|
await expect(
|
|
initialField.locator(`.rs__menu-list:has-text("Disable List Column Text")`),
|
|
).toBeVisible()
|
|
})
|
|
|
|
test('should 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 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 use custom admin.pagination.defaultLimit', async () => {
|
|
await deleteAllPosts()
|
|
|
|
await mapAsync([...Array(6)], async () => {
|
|
await createPost()
|
|
})
|
|
|
|
await page.goto(postsUrl.list)
|
|
await expect(page.locator('.per-page .per-page__base-button')).toContainText('Per Page: 5')
|
|
await expect(page.locator(tableRowLocator)).toHaveCount(5)
|
|
})
|
|
|
|
test('should use custom admin.pagination.limits', async () => {
|
|
await deleteAllPosts()
|
|
|
|
await mapAsync([...Array(6)], async () => {
|
|
await createPost()
|
|
})
|
|
|
|
await page.goto(postsUrl.list)
|
|
await page.locator('.per-page .popup-button').click()
|
|
await page.locator('.per-page .popup-button').click()
|
|
const options = await page.locator('.per-page button.per-page__button')
|
|
await expect(options).toHaveCount(3)
|
|
await expect(options.nth(0)).toContainText('5')
|
|
await expect(options.nth(1)).toContainText('10')
|
|
await expect(options.nth(2)).toContainText('15')
|
|
})
|
|
|
|
test('should paginate', async () => {
|
|
await deleteAllPosts()
|
|
|
|
await mapAsync([...Array(6)], async () => {
|
|
await createPost()
|
|
})
|
|
|
|
await page.reload()
|
|
await expect(page.locator(tableRowLocator)).toHaveCount(5)
|
|
await expect(page.locator('.collection-list__page-info')).toHaveText('1-5 of 6')
|
|
await expect(page.locator('.per-page')).toContainText('Per Page: 5')
|
|
await page.locator('.paginator button').nth(1).click()
|
|
await expect.poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT }).toContain('page=2')
|
|
await expect(page.locator(tableRowLocator)).toHaveCount(1)
|
|
await page.locator('.paginator button').nth(0).click()
|
|
await expect.poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT }).toContain('page=1')
|
|
await expect(page.locator(tableRowLocator)).toHaveCount(5)
|
|
})
|
|
|
|
test('should paginate without resetting selected limit', async () => {
|
|
await deleteAllPosts()
|
|
|
|
await mapAsync([...Array(16)], async () => {
|
|
await createPost()
|
|
})
|
|
|
|
await page.reload()
|
|
const tableItems = page.locator(tableRowLocator)
|
|
await expect(tableItems).toHaveCount(5)
|
|
await expect(page.locator('.collection-list__page-info')).toHaveText('1-5 of 16')
|
|
await expect(page.locator('.per-page')).toContainText('Per Page: 5')
|
|
await page.locator('.per-page .popup-button').click()
|
|
|
|
await page
|
|
.locator('.per-page button.per-page__button', {
|
|
hasText: '15',
|
|
})
|
|
.click()
|
|
|
|
await expect(tableItems).toHaveCount(15)
|
|
await expect(page.locator('.per-page .per-page__base-button')).toContainText('Per Page: 15')
|
|
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: 15') // ensure this hasn't changed
|
|
await expect(page.locator('.collection-list__page-info')).toHaveText('16-16 of 16')
|
|
})
|
|
})
|
|
|
|
// 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)
|
|
})
|
|
|
|
test('should sort without resetting column preferences', async () => {
|
|
await payload.delete({
|
|
collection: 'payload-preferences',
|
|
where: {
|
|
key: {
|
|
equals: `${postsCollectionSlug}.list`,
|
|
},
|
|
},
|
|
})
|
|
|
|
await page.goto(postsUrl.list)
|
|
|
|
// sort by title
|
|
await page.locator('#heading-title button.sort-column__asc').click()
|
|
await page.waitForURL(/sort=title/)
|
|
|
|
// enable a column that is _not_ part of this collection's default columns
|
|
await toggleColumn(page, { columnLabel: 'Status', targetState: 'on' })
|
|
await page.locator('#heading-_status').waitFor({ state: 'visible' })
|
|
|
|
const columnAfterSort = page.locator(
|
|
`.list-controls__columns .column-selector .column-selector__column`,
|
|
{
|
|
hasText: exactText('Status'),
|
|
},
|
|
)
|
|
|
|
await expect(columnAfterSort).toHaveClass(/column-selector__column--active/)
|
|
await expect(page.locator('#heading-_status')).toBeVisible()
|
|
await expect(page.locator('.cell-_status').first()).toBeVisible()
|
|
|
|
// sort by title again in descending order
|
|
await page.locator('#heading-title button.sort-column__desc').click()
|
|
await page.waitForURL(/sort=-title/)
|
|
|
|
// allow time for components to re-render
|
|
await wait(100)
|
|
|
|
// ensure the column is still visible
|
|
const columnAfterSecondSort = page.locator(
|
|
`.list-controls__columns .column-selector .column-selector__column`,
|
|
{
|
|
hasText: exactText('Status'),
|
|
},
|
|
)
|
|
|
|
await expect(columnAfterSecondSort).toHaveClass(/column-selector__column--active/)
|
|
await expect(page.locator('#heading-_status')).toBeVisible()
|
|
await expect(page.locator('.cell-_status').first()).toBeVisible()
|
|
})
|
|
})
|
|
|
|
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()
|
|
|
|
await page.locator('.list-controls__toggle-columns').click()
|
|
|
|
await expect(
|
|
page.locator('.column-selector__column', {
|
|
hasText: exactText('Title'),
|
|
}),
|
|
).toHaveText('Title')
|
|
|
|
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')
|
|
|
|
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')
|
|
})
|
|
})
|
|
})
|
|
|
|
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>
|
|
}
|