Files
payload/test/helpers/e2e/toggleColumn.ts
Jacob Fletcher 3709950d50 feat: maintains column state in url (#11387)
Maintains column state in the URL. This makes it possible to share
direct links to the list view in a specific column order or active
column state, similar to the behavior of filters. This also makes it
possible to change both the filters and columns in the same rendering
cycle, a requirement of the "list presets" feature being worked on here:
#11330.

For example:

```
?columns=%5B"title"%2C"content"%2C"-updatedAt"%2C"createdAt"%2C"id"%5D
```

The `-` prefix denotes that the column is inactive.

This strategy performs a single round trip to the server, ultimately
simplifying the table columns provider as it no longer needs to request
a newly rendered table for itself. Without this change, column state
would need to be replaced first, followed by a change to the filters.
This would make an unnecessary number of requests to the server and
briefly render the UI in a stale state.

This all happens behind an optimistic update, where the state of the
columns is immediately reflected in the UI while the request takes place
in the background.

Technically speaking, an additional database query in performed compared
to the old strategy, whereas before we'd send the data through the
request to avoid this. But this is a necessary tradeoff and doesn't have
huge performance implications. One could argue that this is actually a
good thing, as the data might have changed in the background which would
not have been reflected in the result otherwise.
2025-02-27 20:00:40 -05:00

83 lines
2.3 KiB
TypeScript

import type { Page } from '@playwright/test'
import { expect } from '@playwright/test'
import { exactText } from '../../helpers.js'
import { openListColumns } from './openListColumns.js'
export const toggleColumn = async (
page: Page,
{
togglerSelector,
columnContainerSelector,
columnLabel,
targetState: targetStateFromArgs,
columnName,
expectURLChange = true,
}: {
columnContainerSelector?: string
columnLabel: string
columnName?: string
expectURLChange?: boolean
targetState?: 'off' | 'on'
togglerSelector?: string
},
): Promise<any> => {
const columnContainer = await openListColumns(page, { togglerSelector, columnContainerSelector })
const column = columnContainer.locator(`.column-selector .column-selector__column`, {
hasText: exactText(columnLabel),
})
const isActiveBeforeClick = await column.evaluate((el) =>
el.classList.contains('column-selector__column--active'),
)
const targetState =
targetStateFromArgs !== undefined ? targetStateFromArgs : isActiveBeforeClick ? 'off' : 'on'
await expect(column).toBeVisible()
const requiresToggle =
(isActiveBeforeClick && targetState === 'off') || (!isActiveBeforeClick && targetState === 'on')
if (requiresToggle) {
await column.click()
}
if (targetState === 'off') {
// no class
await expect(column).not.toHaveClass(/column-selector__column--active/)
} else {
// has class
await expect(column).toHaveClass(/column-selector__column--active/)
}
if (expectURLChange && columnName && requiresToggle) {
await waitForColumnInURL({ page, columnName, state: targetState })
}
return column
}
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)
}