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
This commit is contained in:
Jacob Fletcher
2025-09-11 16:24:16 -04:00
committed by GitHub
parent 82820312e8
commit 8a7124a15e
36 changed files with 313 additions and 207 deletions

View File

@@ -1,14 +1,21 @@
import type { Field, PayloadRequest, ResolvedFilterOptions } from 'payload'
import { resolveFilterOptions } from '@payloadcms/ui/rsc'
import { fieldHasSubFields, fieldIsHiddenOrDisabled } from 'payload/shared'
import {
fieldAffectsData,
fieldHasSubFields,
fieldIsHiddenOrDisabled,
tabHasName,
} from 'payload/shared'
export const resolveAllFilterOptions = async ({
fields,
pathPrefix,
req,
result,
}: {
fields: Field[]
pathPrefix?: string
req: PayloadRequest
result?: Map<string, ResolvedFilterOptions>
}): Promise<Map<string, ResolvedFilterOptions>> => {
@@ -20,6 +27,12 @@ export const resolveAllFilterOptions = async ({
return
}
const fieldPath = fieldAffectsData(field)
? pathPrefix
? `${pathPrefix}.${field.name}`
: field.name
: pathPrefix
if (
(field.type === 'relationship' || field.type === 'upload') &&
'filterOptions' in field &&
@@ -28,19 +41,20 @@ export const resolveAllFilterOptions = async ({
const options = await resolveFilterOptions(field.filterOptions, {
id: undefined,
blockData: undefined,
data: {}, // use empty object to prevent breaking queries when accessing properties of data
data: {}, // use empty object to prevent breaking queries when accessing properties of `data`
relationTo: field.relationTo,
req,
siblingData: {}, // use empty object to prevent breaking queries when accessing properties of data
siblingData: {}, // use empty object to prevent breaking queries when accessing properties of `siblingData`
user: req.user,
})
resolvedFilterOptions.set(field.name, options)
resolvedFilterOptions.set(fieldPath, options)
}
if (fieldHasSubFields(field)) {
await resolveAllFilterOptions({
fields: field.fields,
pathPrefix: fieldPath,
req,
result: resolvedFilterOptions,
})
@@ -48,13 +62,20 @@ export const resolveAllFilterOptions = async ({
if (field.type === 'tabs') {
await Promise.all(
field.tabs.map((tab) =>
resolveAllFilterOptions({
field.tabs.map(async (tab) => {
const tabPath = tabHasName(tab)
? fieldPath
? `${fieldPath}.${tab.name}`
: tab.name
: fieldPath
await resolveAllFilterOptions({
fields: tab.fields,
pathPrefix: tabPath,
req,
result: resolvedFilterOptions,
})
}),
),
)
}
}),

View File

@@ -12,7 +12,7 @@ import type {
export type Props = {
readonly addCondition: AddCondition
readonly andIndex: number
readonly fieldName: string
readonly fieldPath: string
readonly filterOptions: ResolvedFilterOptions
readonly operator: Operator
readonly orIndex: number
@@ -42,7 +42,7 @@ export const Condition: React.FC<Props> = (props) => {
const {
addCondition,
andIndex,
fieldName,
fieldPath,
filterOptions,
operator,
orIndex,
@@ -55,7 +55,7 @@ export const Condition: React.FC<Props> = (props) => {
const { t } = useTranslation()
const reducedField = reducedFields.find((field) => field.value === fieldName)
const reducedField = reducedFields.find((field) => field.value === fieldPath)
const [internalValue, setInternalValue] = useState<Value>(value)

View File

@@ -143,12 +143,12 @@ export const WhereBuilder: React.FC<WhereBuilderProps> = (props) => {
{Array.isArray(or?.and) &&
or.and.map((_, andIndex) => {
const condition = conditions[orIndex].and[andIndex]
const fieldName = Object.keys(condition)[0]
const fieldPath = Object.keys(condition)[0]
const operator =
(Object.keys(condition?.[fieldName] || {})?.[0] as Operator) || undefined
(Object.keys(condition?.[fieldPath] || {})?.[0] as Operator) || undefined
const value = condition?.[fieldName]?.[operator] || undefined
const value = condition?.[fieldPath]?.[operator] || undefined
return (
<li key={andIndex}>
@@ -158,13 +158,13 @@ export const WhereBuilder: React.FC<WhereBuilderProps> = (props) => {
<Condition
addCondition={addCondition}
andIndex={andIndex}
fieldName={fieldName}
filterOptions={resolvedFilterOptions?.get(fieldName)}
fieldPath={fieldPath}
filterOptions={resolvedFilterOptions?.get(fieldPath)}
operator={operator}
orIndex={orIndex}
reducedFields={reducedFields}
removeCondition={removeCondition}
RenderedFilter={renderedFilters?.get(fieldName)}
RenderedFilter={renderedFilters?.get(fieldPath)}
updateCondition={updateCondition}
value={value}
/>

View File

@@ -19,8 +19,8 @@ type ReduceFieldOptionsArgs = {
}
/**
* Reduces a field map to a flat array of fields with labels and values.
* Used in the WhereBuilder component to render the fields in the dropdown.
* Transforms a fields schema into a flattened array of fields with labels and values.
* Used in the `WhereBuilder` component to render the fields in the dropdown.
*/
export const reduceFieldsToOptions = ({
fields,

View File

@@ -36,15 +36,17 @@ let payload: PayloadTestSDK<Config>
import { listViewSelectAPISlug } from 'admin/collections/ListViewSelectAPI/index.js'
import { devUser } from 'credentials.js'
import { addListFilter } from 'helpers/e2e/addListFilter.js'
import { assertNetworkRequests } from 'helpers/e2e/assertNetworkRequests.js'
import {
openListColumns,
reorderColumns,
sortColumn,
toggleColumn,
waitForColumnInURL,
} from 'helpers/e2e/columns/index.js'
import { addListFilter, openListFilters } from 'helpers/e2e/filters/index.js'
import { goToNextPage, goToPreviousPage } from 'helpers/e2e/goToNextPage.js'
import { goToFirstCell } from 'helpers/e2e/navigateToDoc.js'
import { openListColumns } from 'helpers/e2e/openListColumns.js'
import { openListFilters } from 'helpers/e2e/openListFilters.js'
import { deletePreferences } from 'helpers/e2e/preferences.js'
import { sortColumn } from 'helpers/e2e/sortColumn.js'
import { toggleColumn, waitForColumnInURL } from 'helpers/e2e/toggleColumn.js'
import { openDocDrawer } from 'helpers/e2e/toggleDocDrawer.js'
import { closeListDrawer } from 'helpers/e2e/toggleListDrawer.js'
import path from 'path'
@@ -53,9 +55,8 @@ 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 { TEST_TIMEOUT_LONG } from '../../../playwright.config.js'
const filename = fileURLToPath(import.meta.url)
const currentFolder = path.dirname(filename)
@@ -719,7 +720,6 @@ describe('List View', () => {
page,
fieldLabel: 'Tab 1 > Title',
operatorLabel: 'equals',
skipValueInput: true,
})
const valueInput = whereBuilder.locator('.condition__value >> input')
@@ -857,7 +857,6 @@ describe('List View', () => {
page,
fieldLabel: 'Self Relation',
operatorLabel: 'equals',
skipValueInput: true,
})
const valueField = whereBuilder.locator('.condition__value')

View File

@@ -2,8 +2,8 @@ import type { BrowserContext, Locator, Page } from '@playwright/test'
import type { PayloadTestSDK } from 'helpers/sdk/index.js'
import { expect, test } from '@playwright/test'
import { addListFilter } from 'helpers/e2e/addListFilter.js'
import { addArrayRow } from 'helpers/e2e/fields/array/index.js'
import { addListFilter } from 'helpers/e2e/filters/index.js'
import { selectInput } from 'helpers/e2e/selectInput.js'
import { toggleBlockOrArrayRow } from 'helpers/e2e/toggleCollapsible.js'
import * as path from 'path'
@@ -642,7 +642,6 @@ test.describe('Bulk Edit', () => {
fieldLabel: 'ID',
operatorLabel: 'equals',
value: originalDoc.id,
skipValueInput: false,
})
// select first item

View File

@@ -93,7 +93,30 @@ export const Relationship: CollectionConfig = {
label: 'Collapsible',
fields: [
{
name: 'nestedRelationshipFilteredByField',
name: 'filteredByFieldInCollapsible',
filterOptions: () => {
return {
filter: {
equals: 'Include me',
},
}
},
admin: {
description:
'This will filter the relationship options if the filter field in this document is set to "Include me"',
},
relationTo: slug,
type: 'relationship',
},
],
},
{
name: 'array',
type: 'array',
label: 'Array',
fields: [
{
name: 'filteredByFieldInArray',
filterOptions: () => {
return {
filter: {

View File

@@ -3,8 +3,9 @@ import type { CollectionSlug } from 'payload'
import { expect, test } from '@playwright/test'
import { assertToastErrors } from 'helpers/assertToastErrors.js'
import { addListFilter } from 'helpers/e2e/addListFilter.js'
import { addArrayRow } from 'helpers/e2e/fields/array/addArrayRow.js'
import { openCreateDocDrawer } from 'helpers/e2e/fields/relationship/openCreateDocDrawer.js'
import { addListFilter } from 'helpers/e2e/filters/index.js'
import { goToNextPage } from 'helpers/e2e/goToNextPage.js'
import { openDocControls } from 'helpers/e2e/openDocControls.js'
import { openDocDrawer } from 'helpers/e2e/toggleDocDrawer.js'
@@ -28,12 +29,12 @@ import {
ensureCompilationIsDone,
initPageConsoleErrorCatch,
saveDocAndAssert,
throttleTest,
// throttleTest,
} from '../helpers.js'
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
import { assertNetworkRequests } from '../helpers/e2e/assertNetworkRequests.js'
import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js'
import { POLL_TOPASS_TIMEOUT, TEST_TIMEOUT_LONG } from '../playwright.config.js'
import { TEST_TIMEOUT_LONG } from '../playwright.config.js'
import {
collection1Slug,
mixedMediaCollectionSlug,
@@ -382,7 +383,6 @@ describe('Relationship Field', () => {
page,
fieldLabel: 'Relationship Filtered By Field',
operatorLabel: 'equals',
skipValueInput: true,
})
const valueInput = page.locator('.condition__value input')
@@ -394,7 +394,7 @@ describe('Relationship Field', () => {
await expect(valueOptions.locator(`text=${idToInclude}`)).toBeVisible()
})
test('should apply filter options of nested fields to list view filter controls', async () => {
test('should apply `filterOptions` of nested fields to list view filter controls', async () => {
const { id: idToInclude } = await payload.create({
collection: slug,
data: {
@@ -402,42 +402,65 @@ describe('Relationship Field', () => {
},
})
// first ensure that filter options are applied in the edit view
// First ensure that filter options are applied to the Edit View
await page.goto(url.edit(idToInclude))
await wait(300)
const field = page.locator('#field-nestedRelationshipFilteredByField')
await field.click({ delay: 100 })
const options = field.locator('.rs__option')
await expect(options).toHaveCount(1)
await expect(options).toContainText(idToInclude)
// now ensure that the same filter options are applied in the list view
const fieldInCollapsible = page.locator('#field-filteredByFieldInCollapsible')
await fieldInCollapsible.click({ delay: 100 })
const optionsInCollapsible = fieldInCollapsible.locator('.rs__option')
await expect(optionsInCollapsible).toHaveCount(1)
await expect(optionsInCollapsible).toContainText(idToInclude)
await addArrayRow(page, { fieldName: 'array' })
const fieldInArray = page.locator('#field-array__0__filteredByFieldInArray')
await fieldInArray.click({ delay: 100 })
const optionsInArray = fieldInArray.locator('.rs__option')
await expect(optionsInArray).toHaveCount(1)
await expect(optionsInArray).toContainText(idToInclude)
// Now ensure that the same filter options are applied in the list view
await page.goto(url.list)
await wait(300)
const { whereBuilder } = await addListFilter({
const { condition: condition1 } = await addListFilter({
page,
fieldLabel: 'Collapsible > Nested Relationship Filtered By Field',
fieldLabel: 'Collapsible > Filtered By Field In Collapsible',
operatorLabel: 'equals',
skipValueInput: true,
})
const valueInput = page.locator('.condition__value input')
const valueInput = condition1.locator('.condition__value input')
await valueInput.click()
const valueOptions = whereBuilder.locator('.condition__value .rs__option')
const valueOptions = condition1.locator('.condition__value .rs__option')
await expect(valueOptions).toHaveCount(2)
await expect(valueOptions.locator(`text=None`)).toBeVisible()
await expect(valueOptions.locator(`text=${idToInclude}`)).toBeVisible()
const { condition: condition2 } = await addListFilter({
page,
fieldLabel: 'Array > Filtered By Field In Array',
operatorLabel: 'equals',
})
test('should allow usage of relationTo in filterOptions', async () => {
const valueInput2 = condition2.locator('.condition__value input')
await valueInput2.click()
const valueOptions2 = condition2.locator('.condition__value .rs__option')
await expect(valueOptions2).toHaveCount(2)
await expect(valueOptions2.locator(`text=None`)).toBeVisible()
await expect(valueOptions2.locator(`text=${idToInclude}`)).toBeVisible()
})
test('should allow usage of relationTo in `filterOptions`', async () => {
const { id: include } = (await payload.create({
collection: relationOneSlug,
data: {
name: 'include',
},
})) as any
const { id: exclude } = (await payload.create({
collection: relationOneSlug,
data: {
@@ -455,7 +478,7 @@ describe('Relationship Field', () => {
await expect(options).not.toContainText(exclude)
})
test('should allow usage of siblingData in filterOptions', async () => {
test('should allow usage of siblingData in `filterOptions`', async () => {
await payload.create({
collection: relationWithTitleSlug,
data: {
@@ -476,7 +499,7 @@ describe('Relationship Field', () => {
})
// TODO: Flaky test in CI - fix. https://github.com/payloadcms/payload/actions/runs/8559547748/job/23456806365
test.skip('should not query for a relationship when filterOptions returns false', async () => {
test.skip('should not query for a relationship when `filterOptions` returns false', async () => {
await payload.create({
collection: relationFalseFilterOptionSlug,
data: {
@@ -495,7 +518,7 @@ describe('Relationship Field', () => {
})
// TODO: Flaky test in CI - fix.
test('should show a relationship when filterOptions returns true', async () => {
test('should show a relationship when `filterOptions` returns true', async () => {
await payload.create({
collection: relationTrueFilterOptionSlug,
data: {

View File

@@ -1,9 +1,8 @@
import type { Page } from '@playwright/test'
import { expect, test } from '@playwright/test'
import { addListFilter } from 'helpers/e2e/addListFilter.js'
import { addListFilter } from 'helpers/e2e/filters/index.js'
import path from 'path'
import { wait } from 'payload/shared'
import { fileURLToPath } from 'url'
import { ensureCompilationIsDone, initPageConsoleErrorCatch } from '../../../helpers.js'

View File

@@ -1,10 +1,7 @@
import type { Page } from '@playwright/test'
import { expect, test } from '@playwright/test'
import { addListFilter } from 'helpers/e2e/addListFilter.js'
import { openListFilters } from 'helpers/e2e/openListFilters.js'
import path from 'path'
import { wait } from 'payload/shared'
import { fileURLToPath } from 'url'
import type { PayloadTestSDK } from '../../../helpers/sdk/index.js'

View File

@@ -1,21 +1,14 @@
import type { Page } from '@playwright/test'
import { expect, test } from '@playwright/test'
import { addListFilter } from 'helpers/e2e/addListFilter.js'
import path from 'path'
import { wait } from 'payload/shared'
import { fileURLToPath } from 'url'
import type { PayloadTestSDK } from '../../../helpers/sdk/index.js'
import type { Config } from '../../payload-types.js'
import {
ensureCompilationIsDone,
initPageConsoleErrorCatch,
saveDocAndAssert,
} from '../../../helpers.js'
import { ensureCompilationIsDone, initPageConsoleErrorCatch } from '../../../helpers.js'
import { AdminUrlUtil } from '../../../helpers/adminUrlUtil.js'
import { assertToastErrors } from '../../../helpers/assertToastErrors.js'
import { initPayloadE2ENoConfig } from '../../../helpers/initPayloadE2ENoConfig.js'
import { reInitializeDB } from '../../../helpers/reInitializeDB.js'
import { RESTClient } from '../../../helpers/rest.js'

View File

@@ -1,7 +1,7 @@
import type { Page } from '@playwright/test'
import { expect, test } from '@playwright/test'
import { addListFilter } from 'helpers/e2e/addListFilter.js'
import { addListFilter } from 'helpers/e2e/filters/index.js'
import path from 'path'
import { wait } from 'payload/shared'
import { fileURLToPath } from 'url'

View File

@@ -1,8 +1,8 @@
import type { Page } from '@playwright/test'
import { expect, test } from '@playwright/test'
import { addListFilter } from 'helpers/e2e/addListFilter.js'
import { openCreateDocDrawer } from 'helpers/e2e/fields/relationship/openCreateDocDrawer.js'
import { addListFilter } from 'helpers/e2e/filters/index.js'
import { navigateToDoc } from 'helpers/e2e/navigateToDoc.js'
import { openDocControls } from 'helpers/e2e/openDocControls.js'
import { openDocDrawer } from 'helpers/e2e/toggleDocDrawer.js'

View File

@@ -2,10 +2,9 @@ import type { Page } from '@playwright/test'
import type { GeneratedTypes } from 'helpers/sdk/types.js'
import { expect, test } from '@playwright/test'
import { addListFilter } from 'helpers/e2e/addListFilter.js'
import { openListColumns } from 'helpers/e2e/openListColumns.js'
import { openListColumns, toggleColumn } from 'helpers/e2e/columns/index.js'
import { addListFilter } from 'helpers/e2e/filters/index.js'
import { upsertPreferences } from 'helpers/e2e/preferences.js'
import { toggleColumn } from 'helpers/e2e/toggleColumn.js'
import path from 'path'
import { wait } from 'payload/shared'
import { fileURLToPath } from 'url'

View File

@@ -3,12 +3,11 @@ import type { PayloadTestSDK } from 'helpers/sdk/index.js'
import { expect, test } from '@playwright/test'
import { devUser } from 'credentials.js'
import { addListFilter } from 'helpers/e2e/addListFilter.js'
import { sortColumn, toggleColumn } from 'helpers/e2e/columns/index.js'
import { addListFilter } from 'helpers/e2e/filters/index.js'
import { goToNextPage } from 'helpers/e2e/goToNextPage.js'
import { addGroupBy, clearGroupBy, closeGroupBy, openGroupBy } from 'helpers/e2e/groupBy.js'
import { addGroupBy, clearGroupBy, closeGroupBy, openGroupBy } from 'helpers/e2e/groupBy/index.js'
import { deletePreferences } from 'helpers/e2e/preferences.js'
import { sortColumn } from 'helpers/e2e/sortColumn.js'
import { toggleColumn } from 'helpers/e2e/toggleColumn.js'
import { openNav } from 'helpers/e2e/toggleNav.js'
import { reInitializeDB } from 'helpers/reInitializeDB.js'
import * as path from 'path'

View File

@@ -0,0 +1,5 @@
export { openListColumns } from './openListColumns.js'
export { reorderColumns } from './reorderColumns.js'
export { sortColumn } from './sortColumn.js'
export { toggleColumn } from './toggleColumn.js'
export { waitForColumnInURL } from './waitForColumnsInURL.js'

View File

@@ -3,7 +3,7 @@ import type { Page } from '@playwright/test'
import { expect } from '@playwright/test'
import { wait } from 'payload/shared'
import { exactText } from '../../helpers.js'
import { exactText } from '../../../helpers.js'
export const reorderColumns = async (
page: Page,

View File

@@ -2,8 +2,9 @@ import type { Locator, Page } from '@playwright/test'
import { expect } from '@playwright/test'
import { exactText } from '../../helpers.js'
import { exactText } from '../../../helpers.js'
import { openListColumns } from './openListColumns.js'
import { waitForColumnInURL } from './waitForColumnsInURL.js'
export const toggleColumn = async (
page: Page,
@@ -64,24 +65,3 @@ export const toggleColumn = async (
return { columnContainer }
}
export const waitForColumnInURL = async ({
page,
columnName,
state,
}: {
columnName: string
page: Page
state: 'off' | 'on'
}): Promise<void> => {
await page.waitForURL(/.*\?.*/)
const identifier = `${state === 'off' ? '-' : ''}${columnName}`
// Test that the identifier is in the URL
// It must appear in the `columns` query parameter, i.e. after `columns=...` and before the next `&`
// It must also appear in it entirety to prevent partially matching other values, i.e. between quotation marks
const regex = new RegExp(`columns=([^&]*${encodeURIComponent(`"${identifier}"`)}[^&]*)`)
await page.waitForURL(regex)
}

View File

@@ -0,0 +1,22 @@
import type { Page } from 'playwright'
export const waitForColumnInURL = async ({
page,
columnName,
state,
}: {
columnName: string
page: Page
state: 'off' | 'on'
}): Promise<void> => {
await page.waitForURL(/.*\?.*/)
const identifier = `${state === 'off' ? '-' : ''}${columnName}`
// Test that the identifier is in the URL
// It must appear in the `columns` query parameter, i.e. after `columns=...` and before the next `&`
// It must also appear in it entirety to prevent partially matching other values, i.e. between quotation marks
const regex = new RegExp(`columns=([^&]*${encodeURIComponent(`"${identifier}"`)}[^&]*)`)
await page.waitForURL(regex)
}

View File

@@ -2,55 +2,77 @@ import type { Locator, Page } from '@playwright/test'
import { expect } from '@playwright/test'
import { selectInput } from '../selectInput.js'
import { openListFilters } from './openListFilters.js'
import { selectInput } from './selectInput.js'
export const addListFilter = async ({
page,
fieldLabel = 'ID',
operatorLabel = 'equals',
value = '',
skipValueInput,
value,
}: {
fieldLabel: string
operatorLabel: string
page: Page
replaceExisting?: boolean
skipValueInput?: boolean
value?: string
}): Promise<{
/**
* A Locator pointing to the condition that was just added.
*/
condition: Locator
/**
* A Locator pointing to the WhereBuilder node.
*/
whereBuilder: Locator
}> => {
await openListFilters(page, {})
const whereBuilder = page.locator('.where-builder')
await whereBuilder.locator('.where-builder__add-first-filter').click()
const addFirst = whereBuilder.locator('.where-builder__add-first-filter')
const initializedEmpty = await addFirst.isVisible()
if (initializedEmpty) {
await addFirst.click()
}
const filters = whereBuilder.locator('.where-builder__or-filters > li')
expect(await filters.count()).toBeGreaterThan(0)
// If there were already filter(s), need to add another and manipulate _that_ instead of the existing one
if (!initializedEmpty) {
const addFilterButtons = whereBuilder.locator('.where-builder__add-or')
await addFilterButtons.last().click()
await expect(filters).toHaveCount(2)
}
const condition = filters.last()
await selectInput({
selectLocator: whereBuilder.locator('.condition__field'),
selectLocator: condition.locator('.condition__field'),
multiSelect: false,
option: fieldLabel,
})
await selectInput({
selectLocator: whereBuilder.locator('.condition__operator'),
selectLocator: condition.locator('.condition__operator'),
multiSelect: false,
option: operatorLabel,
})
if (!skipValueInput) {
if (value !== undefined) {
const networkPromise = page.waitForResponse(
(response) =>
response.url().includes(encodeURIComponent('where[or')) && response.status() === 200,
)
const valueLocator = whereBuilder.locator('.condition__value')
const valueLocator = condition.locator('.condition__value')
const valueInput = valueLocator.locator('input')
await valueInput.fill(value)
await expect(valueInput).toHaveValue(value)
if ((await valueLocator.locator('input.rs__input').count()) > 0) {
const valueOptions = whereBuilder.locator('.condition__value .rs__option')
const valueOptions = condition.locator('.condition__value .rs__option')
const createValue = valueOptions.locator(`text=Create "${value}"`)
if ((await createValue.count()) > 0) {
await createValue.click()
@@ -65,5 +87,5 @@ export const addListFilter = async ({
await networkPromise
}
return { whereBuilder }
return { whereBuilder, condition }
}

View File

@@ -0,0 +1,2 @@
export { addListFilter } from './addListFilter.js'
export { openListFilters } from './openListFilters.js'

View File

@@ -1,90 +0,0 @@
import type { Locator, Page } from '@playwright/test'
import { expect } from '@playwright/test'
import { exactText } from 'helpers.js'
type ToggleOptions = {
groupByContainerSelector: string
targetState: 'closed' | 'open'
togglerSelector: string
}
/**
* Toggles the group-by drawer in the list view based on the targetState option.
*/
export const toggleGroupBy = async (
page: Page,
{
targetState = 'open',
togglerSelector = '#toggle-group-by',
groupByContainerSelector = '#list-controls-group-by',
}: ToggleOptions,
) => {
const groupByContainer = page.locator(groupByContainerSelector).first()
const isAlreadyOpen = await groupByContainer.isVisible()
if (!isAlreadyOpen && targetState === 'open') {
await page.locator(togglerSelector).first().click()
await expect(page.locator(`${groupByContainerSelector}.rah-static--height-auto`)).toBeVisible()
}
if (isAlreadyOpen && targetState === 'closed') {
await page.locator(togglerSelector).first().click()
await expect(page.locator(`${groupByContainerSelector}.rah-static--height-auto`)).toBeHidden()
}
return { groupByContainer }
}
/**
* Closes the group-by drawer in the list view. If it's already closed, does nothing.
*/
export const closeGroupBy = async (
page: Page,
options?: Omit<ToggleOptions, 'targetState'>,
): Promise<{
groupByContainer: Locator
}> => toggleGroupBy(page, { ...(options || ({} as ToggleOptions)), targetState: 'closed' })
/**
* Opens the group-by drawer in the list view. If it's already open, does nothing.
*/
export const openGroupBy = async (
page: Page,
options?: Omit<ToggleOptions, 'targetState'>,
): Promise<{
groupByContainer: Locator
}> => toggleGroupBy(page, { ...(options || ({} as ToggleOptions)), targetState: 'open' })
export const addGroupBy = async (
page: Page,
{ fieldLabel, fieldPath }: { fieldLabel: string; fieldPath: string },
): Promise<{ field: Locator; groupByContainer: Locator }> => {
const { groupByContainer } = await openGroupBy(page)
const field = groupByContainer.locator('#group-by--field-select')
await field.click()
await field.locator('.rs__option', { hasText: exactText(fieldLabel) })?.click()
await expect(field.locator('.react-select--single-value')).toHaveText(fieldLabel)
await expect(page).toHaveURL(new RegExp(`&groupBy=${fieldPath}`))
return { groupByContainer, field }
}
export const clearGroupBy = async (page: Page): Promise<{ groupByContainer: Locator }> => {
const { groupByContainer } = await openGroupBy(page)
await groupByContainer.locator('#group-by--reset').click()
const field = groupByContainer.locator('#group-by--field-select')
await expect(field.locator('.react-select--single-value')).toHaveText('Select a value')
await expect(groupByContainer.locator('#group-by--reset')).toBeHidden()
await expect(page).not.toHaveURL(/&groupBy=/)
await expect(groupByContainer.locator('#field-direction input')).toBeDisabled()
await expect(page.locator('.table-wrap')).toHaveCount(1)
await expect(page.locator('.group-by-header')).toHaveCount(0)
return { groupByContainer }
}

View File

@@ -0,0 +1,22 @@
import type { Locator, Page } from '@playwright/test'
import { expect } from '@playwright/test'
import { exactText } from 'helpers.js'
import { openGroupBy } from './openGroupBy.js'
export const addGroupBy = async (
page: Page,
{ fieldLabel, fieldPath }: { fieldLabel: string; fieldPath: string },
): Promise<{ field: Locator; groupByContainer: Locator }> => {
const { groupByContainer } = await openGroupBy(page)
const field = groupByContainer.locator('#group-by--field-select')
await field.click()
await field.locator('.rs__option', { hasText: exactText(fieldLabel) })?.click()
await expect(field.locator('.react-select--single-value')).toHaveText(fieldLabel)
await expect(page).toHaveURL(new RegExp(`&groupBy=${fieldPath}`))
return { groupByContainer, field }
}

View File

@@ -0,0 +1,21 @@
import type { Locator, Page } from '@playwright/test'
import { expect } from '@playwright/test'
import { openGroupBy } from './openGroupBy.js'
export const clearGroupBy = async (page: Page): Promise<{ groupByContainer: Locator }> => {
const { groupByContainer } = await openGroupBy(page)
await groupByContainer.locator('#group-by--reset').click()
const field = groupByContainer.locator('#group-by--field-select')
await expect(field.locator('.react-select--single-value')).toHaveText('Select a value')
await expect(groupByContainer.locator('#group-by--reset')).toBeHidden()
await expect(page).not.toHaveURL(/&groupBy=/)
await expect(groupByContainer.locator('#field-direction input')).toBeDisabled()
await expect(page.locator('.table-wrap')).toHaveCount(1)
await expect(page.locator('.group-by-header')).toHaveCount(0)
return { groupByContainer }
}

View File

@@ -0,0 +1,15 @@
import type { Locator, Page } from '@playwright/test'
import type { ToggleOptions } from './toggleGroupBy.js';
import { toggleGroupBy } from './toggleGroupBy.js'
/**
* Closes the group-by drawer in the list view. If it's already closed, does nothing.
*/
export const closeGroupBy = async (
page: Page,
options?: Omit<ToggleOptions, 'targetState'>,
): Promise<{
groupByContainer: Locator
}> => toggleGroupBy(page, { ...(options || ({} as ToggleOptions)), targetState: 'closed' })

View File

@@ -0,0 +1,5 @@
export { addGroupBy } from './addGroupBy.js'
export { clearGroupBy } from './clearGroupBy.js'
export { closeGroupBy } from './closeGroupBy.js'
export { openGroupBy } from './openGroupBy.js'
export { toggleGroupBy } from './toggleGroupBy.js'

View File

@@ -0,0 +1,15 @@
import type { Locator, Page } from '@playwright/test'
import type { ToggleOptions } from './toggleGroupBy.js'
import { toggleGroupBy } from './toggleGroupBy.js'
/**
* Opens the group-by drawer in the list view. If it's already open, does nothing.
*/
export const openGroupBy = async (
page: Page,
options?: Omit<ToggleOptions, 'targetState'>,
): Promise<{
groupByContainer: Locator
}> => toggleGroupBy(page, { ...(options || ({} as ToggleOptions)), targetState: 'open' })

View File

@@ -0,0 +1,37 @@
import type { Page } from '@playwright/test'
import { expect } from '@playwright/test'
export type ToggleOptions = {
groupByContainerSelector: string
targetState: 'closed' | 'open'
togglerSelector: string
}
/**
* Toggles the group-by drawer in the list view based on the targetState option.
*/
export const toggleGroupBy = async (
page: Page,
{
targetState = 'open',
togglerSelector = '#toggle-group-by',
groupByContainerSelector = '#list-controls-group-by',
}: ToggleOptions,
) => {
const groupByContainer = page.locator(groupByContainerSelector).first()
const isAlreadyOpen = await groupByContainer.isVisible()
if (!isAlreadyOpen && targetState === 'open') {
await page.locator(togglerSelector).first().click()
await expect(page.locator(`${groupByContainerSelector}.rah-static--height-auto`)).toBeVisible()
}
if (isAlreadyOpen && targetState === 'closed') {
await page.locator(togglerSelector).first().click()
await expect(page.locator(`${groupByContainerSelector}.rah-static--height-auto`)).toBeHidden()
}
return { groupByContainer }
}

View File

@@ -13,7 +13,7 @@ import type { PayloadTestSDK } from '../helpers/sdk/index.js'
import { ensureCompilationIsDone, initPageConsoleErrorCatch } from '../helpers.js'
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
import { openListFilters } from '../helpers/e2e/openListFilters.js'
import { openListFilters } from '../helpers/e2e/filters/index.js'
import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js'
import { reInitializeDB } from '../helpers/reInitializeDB.js'
import { TEST_TIMEOUT_LONG } from '../playwright.config.js'

View File

@@ -17,8 +17,8 @@ import {
// throttleTest,
} from '../helpers.js'
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
import { reorderColumns } from '../helpers/e2e/columns/index.js'
import { navigateToDoc } from '../helpers/e2e/navigateToDoc.js'
import { reorderColumns } from '../helpers/e2e/reorderColumns.js'
import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js'
import { reInitializeDB } from '../helpers/reInitializeDB.js'
import { RESTClient } from '../helpers/rest.js'

View File

@@ -2,8 +2,7 @@ import type { BrowserContext, Page } from '@playwright/test'
import { expect, test } from '@playwright/test'
import { devUser } from 'credentials.js'
import { openListColumns } from 'helpers/e2e/openListColumns.js'
import { toggleColumn } from 'helpers/e2e/toggleColumn.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'

View File

@@ -1,7 +1,7 @@
import type { Page } from '@playwright/test'
import { expect, test } from '@playwright/test'
import { addListFilter } from 'helpers/e2e/addListFilter.js'
import { addListFilter } from 'helpers/e2e/filters/index.js'
import * as path from 'path'
import { mapAsync } from 'payload'
import { fileURLToPath } from 'url'

View File

@@ -2,9 +2,8 @@ import type { Page } from '@playwright/test'
import { expect, test } from '@playwright/test'
import { statSync } from 'fs'
import { openListColumns } from 'helpers/e2e/openListColumns.js'
import { openListFilters } from 'helpers/e2e/openListFilters.js'
import { toggleColumn } from 'helpers/e2e/toggleColumn.js'
import { openListColumns, toggleColumn } from 'helpers/e2e/columns/index.js'
import { openListFilters } from 'helpers/e2e/filters/index.js'
import { openDocDrawer } from 'helpers/e2e/toggleDocDrawer.js'
import path from 'path'
import { wait } from 'payload/shared'