Files
payloadcms/test/query-presets/e2e.spec.ts
Jacob Fletcher 8a7124a15e fix(next): resolve filterOptions by path (#13779)
Follow up to #11375.

When setting `filterOptions` on relationship or upload fields _that are
nested within a named field_, those options won't be applied to the
`Filter` component in the list view.

This is because of how we key the results when resolving `filterOptions`
on the server. Instead of using the field path as expected, we were
using the field name, causing a failed lookup on the front-end. This
also solves an issue where two fields with the same name would override
each other's `filterOptions`, since field names alone are not unique.

Unrelated: this PR also does some general housekeeping to e2e test
helpers.

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211332845301583
2025-09-11 13:24:16 -07:00

412 lines
13 KiB
TypeScript

import type { BrowserContext, Page } from '@playwright/test'
import { expect, test } from '@playwright/test'
import { devUser } from 'credentials.js'
import { openListColumns, toggleColumn } from 'helpers/e2e/columns/index.js'
import { openNav } from 'helpers/e2e/toggleNav.js'
import * as path from 'path'
import { fileURLToPath } from 'url'
import type { PayloadTestSDK } from '../helpers/sdk/index.js'
import type { Config, PayloadQueryPreset } from './payload-types.js'
import {
ensureCompilationIsDone,
exactText,
initPageConsoleErrorCatch,
saveDocAndAssert,
// throttleTest,
} from '../helpers.js'
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js'
import { TEST_TIMEOUT_LONG } from '../playwright.config.js'
import { assertURLParams } from './helpers/assertURLParams.js'
import { openQueryPresetDrawer } from './helpers/openQueryPresetDrawer.js'
import { clearSelectedPreset, selectPreset } from './helpers/togglePreset.js'
import { seedData } from './seed.js'
import { pagesSlug } from './slugs.js'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
const { beforeAll, describe, beforeEach } = test
let page: Page
let pagesUrl: AdminUrlUtil
let payload: PayloadTestSDK<Config>
let serverURL: string
let everyoneID: string | undefined
let context: BrowserContext
let user: any
let ownerUser: any
let seededData: {
everyone: PayloadQueryPreset
onlyMe: PayloadQueryPreset
specificUsers: PayloadQueryPreset
}
describe('Query Presets', () => {
beforeAll(async ({ browser }, testInfo) => {
testInfo.setTimeout(TEST_TIMEOUT_LONG)
;({ payload, serverURL } = await initPayloadE2ENoConfig<Config>({ dirname }))
pagesUrl = new AdminUrlUtil(serverURL, pagesSlug)
context = await browser.newContext()
page = await context.newPage()
initPageConsoleErrorCatch(page)
await ensureCompilationIsDone({ page, serverURL })
user = await payload
.login({
collection: 'users',
data: {
email: devUser.email,
password: devUser.password,
},
})
?.then((res) => res.user) // TODO: this type is wrong
ownerUser = await payload
.find({
collection: 'users',
where: {
name: {
equals: 'Owner',
},
},
limit: 1,
depth: 0,
})
?.then((res) => res.docs[0])
})
beforeEach(async () => {
// await throttleTest({
// page,
// context,
// delay: 'Fast 4G',
// })
// clear and reseed everything
try {
await payload.delete({
collection: 'payload-query-presets',
where: {
id: {
exists: true,
},
},
})
const [, everyone, onlyMe, specificUsers] = await Promise.all([
payload.delete({
collection: 'payload-preferences',
where: {
and: [
{
key: { equals: 'pages-list' },
},
{
'user.relationTo': {
equals: 'users',
},
},
{
'user.value': {
equals: user.id,
},
},
],
},
}),
payload.create({
collection: 'payload-query-presets',
data: seedData.everyone({ ownerUserID: ownerUser?.id || '' }),
}),
payload.create({
collection: 'payload-query-presets',
data: seedData.onlyMe({ ownerUserID: ownerUser?.id || '' }),
}),
payload.create({
collection: 'payload-query-presets',
data: seedData.specificUsers({ ownerUserID: ownerUser?.id || '', adminUserID: user.id }),
}),
])
seededData = {
everyone,
onlyMe,
specificUsers,
}
everyoneID = everyone.id
} catch (error) {
console.error('Error in beforeEach:', error)
}
})
test('should select preset and apply filters', async () => {
await page.goto(pagesUrl.list)
await selectPreset({ page, presetTitle: seededData.everyone.title })
await assertURLParams({
page,
columns: seededData.everyone.columns,
preset: everyoneID,
})
})
test('should clear selected preset and reset filters', async () => {
await page.goto(pagesUrl.list)
await selectPreset({ page, presetTitle: seededData.everyone.title })
await clearSelectedPreset({ page })
// ensure that the preset was cleared from preferences by navigating without the `?preset=` param
// e.g. do not do `page.reload()`
await page.goto(pagesUrl.list)
// poll url to ensure that `?preset=` param is not present
// this is first set to an empty string to clear from the user's preferences
// it is then removed entirely after it is processed on the server
const regex = /preset=/
await page.waitForURL((url) => !regex.test(url.search), { timeout: TEST_TIMEOUT_LONG })
await expect(
page.locator('button#select-preset', {
hasText: exactText('Select Preset'),
}),
).toBeVisible()
})
test('should delete a preset, clear selection, and reset changes', async () => {
await page.goto(pagesUrl.list)
await selectPreset({ page, presetTitle: seededData.everyone.title })
await page.locator('#delete-preset').click()
await page.locator('#confirm-delete-preset #confirm-action').click()
// columns can either be omitted or an empty string after being cleared
const regex = /columns=(?:\[\]|$)/
await page.waitForURL((url) => !regex.test(url.search), {
timeout: TEST_TIMEOUT_LONG,
})
await expect(
page.locator('button#select-preset', {
hasText: exactText('Select Preset'),
}),
).toBeVisible()
await openQueryPresetDrawer({ page })
const modal = page.locator('[id^=list-drawer_0_]')
await expect(modal).toBeVisible()
await expect(
modal.locator('tbody tr td button', {
hasText: exactText(seededData.everyone.title),
}),
).toBeHidden()
})
test('should save last used preset to preferences and load on initial render', async () => {
await page.goto(pagesUrl.list)
await selectPreset({ page, presetTitle: seededData.everyone.title })
await page.goto(pagesUrl.list)
await assertURLParams({
page,
columns: seededData.everyone.columns,
where: seededData.everyone.where,
preset: everyoneID,
})
// for good measure, also soft navigate away and back
await page.goto(pagesUrl.admin)
await openNav(page)
await page.click(`a[href="/admin/collections/${pagesSlug}"]`)
await assertURLParams({
page,
columns: seededData.everyone.columns,
where: seededData.everyone.where,
preset: everyoneID,
})
})
test('should only show "edit" and "delete" controls when there is an active preset', async () => {
await page.goto(pagesUrl.list)
await expect(page.locator('#edit-preset')).toBeHidden()
await expect(page.locator('#delete-preset')).toBeHidden()
await selectPreset({ page, presetTitle: seededData.everyone.title })
await expect(page.locator('#edit-preset')).toBeVisible()
await expect(page.locator('#delete-preset')).toBeVisible()
})
test('should only show "reset" and "save" controls when there is an active preset and changes have been made', async () => {
await page.goto(pagesUrl.list)
await expect(page.locator('#reset-preset')).toBeHidden()
await expect(page.locator('#save-preset')).toBeHidden()
await selectPreset({ page, presetTitle: seededData.onlyMe.title })
await toggleColumn(page, { columnLabel: 'ID' })
await expect(page.locator('#reset-preset')).toBeVisible()
await expect(
page.locator('#save-preset', {
hasText: exactText('Save changes'),
}),
).toBeVisible()
})
test('should conditionally render "update for everyone" label based on if preset is shared', async () => {
await page.goto(pagesUrl.list)
await selectPreset({ page, presetTitle: seededData.onlyMe.title })
await toggleColumn(page, { columnLabel: 'ID' })
// When not shared, the label is "Save"
await expect(page.locator('#save-preset')).toBeVisible()
await expect(
page.locator('#save-preset', {
hasText: exactText('Save changes'),
}),
).toBeVisible()
await selectPreset({ page, presetTitle: seededData.everyone.title })
await toggleColumn(page, { columnLabel: 'ID' })
// When shared, the label is "Update for everyone"
await expect(
page.locator('#save-preset', {
hasText: exactText('Update for everyone'),
}),
).toBeVisible()
})
test('should reset active changes', async () => {
await page.goto(pagesUrl.list)
await selectPreset({ page, presetTitle: seededData.everyone.title })
const { columnContainer } = await toggleColumn(page, { columnLabel: 'ID' })
const column = columnContainer.locator(`.pill-selector .pill-selector__pill`, {
hasText: exactText('ID'),
})
await page.locator('#reset-preset').click()
await openListColumns(page, {})
await expect(column).toHaveClass(/pill-selector__pill--selected/)
})
test.skip('should only enter modified state when changes are made to an active preset', async () => {
await page.goto(pagesUrl.list)
await expect(page.locator('.list-controls__modified')).toBeHidden()
await selectPreset({ page, presetTitle: seededData.everyone.title })
await expect(page.locator('.list-controls__modified')).toBeHidden()
await toggleColumn(page, { columnLabel: 'ID' })
await expect(page.locator('.list-controls__modified')).toBeVisible()
await page.locator('#save-preset').click()
await expect(page.locator('.list-controls__modified')).toBeHidden()
await toggleColumn(page, { columnLabel: 'ID' })
await expect(page.locator('.list-controls__modified')).toBeVisible()
await page.locator('#reset-preset').click()
await expect(page.locator('.list-controls__modified')).toBeHidden()
})
test('can edit a preset through the document drawer', async () => {
const presetTitle = 'New Preset'
await page.goto(pagesUrl.list)
await selectPreset({ page, presetTitle: seededData.everyone.title })
await page.locator('#edit-preset').click()
const drawer = page.locator('[id^=doc-drawer_payload-query-presets_0_]')
const titleValue = drawer.locator('input[name="title"]')
await expect(titleValue).toHaveValue(seededData.everyone.title)
const newTitle = `${seededData.everyone.title} (Updated)`
await drawer.locator('input[name="title"]').fill(newTitle)
await saveDocAndAssert(page)
await drawer.locator('button.doc-drawer__header-close').click()
await expect(drawer).toBeHidden()
await expect(page.locator('button#select-preset')).toHaveText(newTitle)
})
test('should not display query presets when admin.enableQueryPresets is not true', async () => {
// go to users list view and ensure the query presets select is not visible
const usersURL = new AdminUrlUtil(serverURL, 'users')
await page.goto(usersURL.list)
await expect(page.locator('#select-preset')).toBeHidden()
})
// eslint-disable-next-line playwright/no-skipped-test, playwright/expect-expect
test.skip('can save a preset', () => {
// select a preset, make a change to the presets, click "save for everyone" or "save", and ensure the changes persist
})
test('can create new preset', async () => {
await page.goto(pagesUrl.list)
const presetTitle = 'New Preset'
await page.locator('#create-new-preset').click()
const modal = page.locator('[id^=doc-drawer_payload-query-presets_0_]')
await expect(modal).toBeVisible()
await modal.locator('input[name="title"]').fill(presetTitle)
const currentURL = page.url()
await saveDocAndAssert(page)
await expect(modal).toBeHidden()
await page.waitForURL(() => page.url() !== currentURL)
await expect(
page.locator('button#select-preset', {
hasText: exactText(presetTitle),
}),
).toBeVisible()
})
test('only shows query presets related to the underlying collection', async () => {
// no results on `posts` collection
const postsURL = new AdminUrlUtil(serverURL, 'posts')
await page.goto(postsURL.list)
const drawer = await openQueryPresetDrawer({ page })
await expect(drawer.locator('.table table > tbody > tr')).toHaveCount(0)
await expect(drawer.locator('.collection-list__no-results')).toBeVisible()
// results on `pages` collection
await page.goto(pagesUrl.list)
await openQueryPresetDrawer({ page })
await expect(drawer.locator('.table table > tbody > tr')).toHaveCount(3)
await drawer.locator('.collection-list__no-results').isHidden()
})
})