diff --git a/packages/next/src/views/List/resolveAllFilterOptions.ts b/packages/next/src/views/List/resolveAllFilterOptions.ts index d5fb33d8c..c0582386d 100644 --- a/packages/next/src/views/List/resolveAllFilterOptions.ts +++ b/packages/next/src/views/List/resolveAllFilterOptions.ts @@ -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 }): Promise> => { @@ -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, - }), - ), + }) + }), ) } }), diff --git a/packages/ui/src/elements/WhereBuilder/Condition/index.tsx b/packages/ui/src/elements/WhereBuilder/Condition/index.tsx index 23c849bf2..de9c69f2c 100644 --- a/packages/ui/src/elements/WhereBuilder/Condition/index.tsx +++ b/packages/ui/src/elements/WhereBuilder/Condition/index.tsx @@ -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) => { const { addCondition, andIndex, - fieldName, + fieldPath, filterOptions, operator, orIndex, @@ -55,7 +55,7 @@ export const Condition: React.FC = (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) diff --git a/packages/ui/src/elements/WhereBuilder/index.tsx b/packages/ui/src/elements/WhereBuilder/index.tsx index d155456c0..a1db810c1 100644 --- a/packages/ui/src/elements/WhereBuilder/index.tsx +++ b/packages/ui/src/elements/WhereBuilder/index.tsx @@ -143,12 +143,12 @@ export const WhereBuilder: React.FC = (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 (
  • @@ -158,13 +158,13 @@ export const WhereBuilder: React.FC = (props) => { diff --git a/packages/ui/src/utilities/reduceFieldsToOptions.tsx b/packages/ui/src/utilities/reduceFieldsToOptions.tsx index a5064c8a7..32e1ee40d 100644 --- a/packages/ui/src/utilities/reduceFieldsToOptions.tsx +++ b/packages/ui/src/utilities/reduceFieldsToOptions.tsx @@ -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, diff --git a/test/admin/e2e/list-view/e2e.spec.ts b/test/admin/e2e/list-view/e2e.spec.ts index cb38d5b20..aa6148724 100644 --- a/test/admin/e2e/list-view/e2e.spec.ts +++ b/test/admin/e2e/list-view/e2e.spec.ts @@ -36,15 +36,17 @@ let payload: PayloadTestSDK 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') diff --git a/test/bulk-edit/e2e.spec.ts b/test/bulk-edit/e2e.spec.ts index 01ec5e8b4..4f09d51ad 100644 --- a/test/bulk-edit/e2e.spec.ts +++ b/test/bulk-edit/e2e.spec.ts @@ -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 diff --git a/test/fields-relationship/collections/Relationship/index.ts b/test/fields-relationship/collections/Relationship/index.ts index c07374b9a..532c1d0eb 100644 --- a/test/fields-relationship/collections/Relationship/index.ts +++ b/test/fields-relationship/collections/Relationship/index.ts @@ -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: { diff --git a/test/fields-relationship/e2e.spec.ts b/test/fields-relationship/e2e.spec.ts index ececdc35c..9d28fe780 100644 --- a/test/fields-relationship/e2e.spec.ts +++ b/test/fields-relationship/e2e.spec.ts @@ -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', + }) + + 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 () => { + 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: { diff --git a/test/fields/collections/Checkbox/e2e.spec.ts b/test/fields/collections/Checkbox/e2e.spec.ts index 7d4076b48..1a4ac1c32 100644 --- a/test/fields/collections/Checkbox/e2e.spec.ts +++ b/test/fields/collections/Checkbox/e2e.spec.ts @@ -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' diff --git a/test/fields/collections/Email/e2e.spec.ts b/test/fields/collections/Email/e2e.spec.ts index 4b2a5c0e5..39267211f 100644 --- a/test/fields/collections/Email/e2e.spec.ts +++ b/test/fields/collections/Email/e2e.spec.ts @@ -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' diff --git a/test/fields/collections/Group/e2e.spec.ts b/test/fields/collections/Group/e2e.spec.ts index 8bd439645..d4662e24b 100644 --- a/test/fields/collections/Group/e2e.spec.ts +++ b/test/fields/collections/Group/e2e.spec.ts @@ -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' diff --git a/test/fields/collections/Number/e2e.spec.ts b/test/fields/collections/Number/e2e.spec.ts index 321e5d527..c137f72bf 100644 --- a/test/fields/collections/Number/e2e.spec.ts +++ b/test/fields/collections/Number/e2e.spec.ts @@ -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' diff --git a/test/fields/collections/Relationship/e2e.spec.ts b/test/fields/collections/Relationship/e2e.spec.ts index 3bf2232ff..a424a4c91 100644 --- a/test/fields/collections/Relationship/e2e.spec.ts +++ b/test/fields/collections/Relationship/e2e.spec.ts @@ -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' diff --git a/test/fields/collections/Text/e2e.spec.ts b/test/fields/collections/Text/e2e.spec.ts index f8ca7926d..af5720e4a 100644 --- a/test/fields/collections/Text/e2e.spec.ts +++ b/test/fields/collections/Text/e2e.spec.ts @@ -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' diff --git a/test/group-by/e2e.spec.ts b/test/group-by/e2e.spec.ts index 29b13b928..724211b5e 100644 --- a/test/group-by/e2e.spec.ts +++ b/test/group-by/e2e.spec.ts @@ -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' diff --git a/test/helpers/e2e/columns/index.ts b/test/helpers/e2e/columns/index.ts new file mode 100644 index 000000000..91309e3ce --- /dev/null +++ b/test/helpers/e2e/columns/index.ts @@ -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' diff --git a/test/helpers/e2e/openListColumns.ts b/test/helpers/e2e/columns/openListColumns.ts similarity index 100% rename from test/helpers/e2e/openListColumns.ts rename to test/helpers/e2e/columns/openListColumns.ts diff --git a/test/helpers/e2e/reorderColumns.ts b/test/helpers/e2e/columns/reorderColumns.ts similarity index 97% rename from test/helpers/e2e/reorderColumns.ts rename to test/helpers/e2e/columns/reorderColumns.ts index d3b3931a4..77214c070 100644 --- a/test/helpers/e2e/reorderColumns.ts +++ b/test/helpers/e2e/columns/reorderColumns.ts @@ -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, diff --git a/test/helpers/e2e/sortColumn.ts b/test/helpers/e2e/columns/sortColumn.ts similarity index 100% rename from test/helpers/e2e/sortColumn.ts rename to test/helpers/e2e/columns/sortColumn.ts diff --git a/test/helpers/e2e/toggleColumn.ts b/test/helpers/e2e/columns/toggleColumn.ts similarity index 70% rename from test/helpers/e2e/toggleColumn.ts rename to test/helpers/e2e/columns/toggleColumn.ts index 7250d1daa..b5537595a 100644 --- a/test/helpers/e2e/toggleColumn.ts +++ b/test/helpers/e2e/columns/toggleColumn.ts @@ -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 => { - 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) -} diff --git a/test/helpers/e2e/columns/waitForColumnsInURL.ts b/test/helpers/e2e/columns/waitForColumnsInURL.ts new file mode 100644 index 000000000..3f50dd53a --- /dev/null +++ b/test/helpers/e2e/columns/waitForColumnsInURL.ts @@ -0,0 +1,22 @@ +import type { Page } from 'playwright' + +export const waitForColumnInURL = async ({ + page, + columnName, + state, +}: { + columnName: string + page: Page + state: 'off' | 'on' +}): Promise => { + 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) +} diff --git a/test/helpers/e2e/addListFilter.ts b/test/helpers/e2e/filters/addListFilter.ts similarity index 53% rename from test/helpers/e2e/addListFilter.ts rename to test/helpers/e2e/filters/addListFilter.ts index a6320ea3c..fdb30c826 100644 --- a/test/helpers/e2e/addListFilter.ts +++ b/test/helpers/e2e/filters/addListFilter.ts @@ -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 } } diff --git a/test/helpers/e2e/filters/index.ts b/test/helpers/e2e/filters/index.ts new file mode 100644 index 000000000..412e19a94 --- /dev/null +++ b/test/helpers/e2e/filters/index.ts @@ -0,0 +1,2 @@ +export { addListFilter } from './addListFilter.js' +export { openListFilters } from './openListFilters.js' diff --git a/test/helpers/e2e/openListFilters.ts b/test/helpers/e2e/filters/openListFilters.ts similarity index 100% rename from test/helpers/e2e/openListFilters.ts rename to test/helpers/e2e/filters/openListFilters.ts diff --git a/test/helpers/e2e/groupBy.ts b/test/helpers/e2e/groupBy.ts deleted file mode 100644 index 6e15b18ee..000000000 --- a/test/helpers/e2e/groupBy.ts +++ /dev/null @@ -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, -): 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, -): 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 } -} diff --git a/test/helpers/e2e/groupBy/addGroupBy.ts b/test/helpers/e2e/groupBy/addGroupBy.ts new file mode 100644 index 000000000..aab5dc1b8 --- /dev/null +++ b/test/helpers/e2e/groupBy/addGroupBy.ts @@ -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 } +} diff --git a/test/helpers/e2e/groupBy/clearGroupBy.ts b/test/helpers/e2e/groupBy/clearGroupBy.ts new file mode 100644 index 000000000..ec7e5933b --- /dev/null +++ b/test/helpers/e2e/groupBy/clearGroupBy.ts @@ -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 } +} diff --git a/test/helpers/e2e/groupBy/closeGroupBy.ts b/test/helpers/e2e/groupBy/closeGroupBy.ts new file mode 100644 index 000000000..3e924b2f7 --- /dev/null +++ b/test/helpers/e2e/groupBy/closeGroupBy.ts @@ -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, +): Promise<{ + groupByContainer: Locator +}> => toggleGroupBy(page, { ...(options || ({} as ToggleOptions)), targetState: 'closed' }) diff --git a/test/helpers/e2e/groupBy/index.js b/test/helpers/e2e/groupBy/index.js new file mode 100644 index 000000000..ab232471a --- /dev/null +++ b/test/helpers/e2e/groupBy/index.js @@ -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' diff --git a/test/helpers/e2e/groupBy/openGroupBy.ts b/test/helpers/e2e/groupBy/openGroupBy.ts new file mode 100644 index 000000000..d66f54613 --- /dev/null +++ b/test/helpers/e2e/groupBy/openGroupBy.ts @@ -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, +): Promise<{ + groupByContainer: Locator +}> => toggleGroupBy(page, { ...(options || ({} as ToggleOptions)), targetState: 'open' }) diff --git a/test/helpers/e2e/groupBy/toggleGroupBy.ts b/test/helpers/e2e/groupBy/toggleGroupBy.ts new file mode 100644 index 000000000..00cbfe292 --- /dev/null +++ b/test/helpers/e2e/groupBy/toggleGroupBy.ts @@ -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 } +} diff --git a/test/i18n/e2e.spec.ts b/test/i18n/e2e.spec.ts index a57bd9f12..e2cbe08a9 100644 --- a/test/i18n/e2e.spec.ts +++ b/test/i18n/e2e.spec.ts @@ -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' diff --git a/test/joins/e2e.spec.ts b/test/joins/e2e.spec.ts index 379be2238..ac3905d6a 100644 --- a/test/joins/e2e.spec.ts +++ b/test/joins/e2e.spec.ts @@ -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' diff --git a/test/query-presets/e2e.spec.ts b/test/query-presets/e2e.spec.ts index 0e1660401..3bad40e27 100644 --- a/test/query-presets/e2e.spec.ts +++ b/test/query-presets/e2e.spec.ts @@ -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' diff --git a/test/trash/e2e.spec.ts b/test/trash/e2e.spec.ts index fecb29d45..1d24f6a16 100644 --- a/test/trash/e2e.spec.ts +++ b/test/trash/e2e.spec.ts @@ -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' diff --git a/test/uploads/e2e.spec.ts b/test/uploads/e2e.spec.ts index bdc355e11..5b8f13baa 100644 --- a/test/uploads/e2e.spec.ts +++ b/test/uploads/e2e.spec.ts @@ -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'