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:
5
test/helpers/e2e/columns/index.ts
Normal file
5
test/helpers/e2e/columns/index.ts
Normal 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'
|
||||
@@ -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,
|
||||
@@ -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)
|
||||
}
|
||||
22
test/helpers/e2e/columns/waitForColumnsInURL.ts
Normal file
22
test/helpers/e2e/columns/waitForColumnsInURL.ts
Normal 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)
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
2
test/helpers/e2e/filters/index.ts
Normal file
2
test/helpers/e2e/filters/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { addListFilter } from './addListFilter.js'
|
||||
export { openListFilters } from './openListFilters.js'
|
||||
@@ -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 }
|
||||
}
|
||||
22
test/helpers/e2e/groupBy/addGroupBy.ts
Normal file
22
test/helpers/e2e/groupBy/addGroupBy.ts
Normal 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 }
|
||||
}
|
||||
21
test/helpers/e2e/groupBy/clearGroupBy.ts
Normal file
21
test/helpers/e2e/groupBy/clearGroupBy.ts
Normal 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 }
|
||||
}
|
||||
15
test/helpers/e2e/groupBy/closeGroupBy.ts
Normal file
15
test/helpers/e2e/groupBy/closeGroupBy.ts
Normal 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' })
|
||||
5
test/helpers/e2e/groupBy/index.js
Normal file
5
test/helpers/e2e/groupBy/index.js
Normal 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'
|
||||
15
test/helpers/e2e/groupBy/openGroupBy.ts
Normal file
15
test/helpers/e2e/groupBy/openGroupBy.ts
Normal 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' })
|
||||
37
test/helpers/e2e/groupBy/toggleGroupBy.ts
Normal file
37
test/helpers/e2e/groupBy/toggleGroupBy.ts
Normal 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 }
|
||||
}
|
||||
Reference in New Issue
Block a user