test: dedicated bulk edit test suite (#11756)

Consolidates all bulk edit related tests into a single, dedicated suite.

Currently, bulk edit tests are dispersed throughout the Admin > General
and the Versions test suites, which are considerably bloated for their
own purposes. This made them very hard to locate, mentally digest, and
add on new tests. Going forward, many more tests specifically for bulk
edit will need to be written. This gives us a simple, isolated place for
that.

With this change are also a few improvements to the tests themselves to
make them more predictable and efficient.
This commit is contained in:
Jacob Fletcher
2025-03-18 13:31:51 -04:00
committed by GitHub
parent 3f23160a96
commit a44a252f31
19 changed files with 2847 additions and 459 deletions

View File

@@ -137,64 +137,6 @@ export const Posts: CollectionConfig = {
},
],
},
{
name: 'arrayOfFields',
type: 'array',
admin: {
initCollapsed: true,
},
fields: [
{
name: 'optional',
type: 'text',
},
{
name: 'innerArrayOfFields',
type: 'array',
fields: [
{
name: 'innerOptional',
type: 'text',
},
],
},
],
},
{
name: 'group',
type: 'group',
fields: [
{
name: 'defaultValueField',
type: 'text',
defaultValue: 'testing',
},
{
name: 'title',
type: 'text',
},
],
},
{
name: 'someBlock',
type: 'blocks',
blocks: [
{
slug: 'textBlock',
fields: [
{
name: 'textFieldForBlock',
type: 'text',
},
],
},
],
},
{
name: 'defaultValueField',
type: 'text',
defaultValue: 'testing',
},
{
name: 'relationship',
type: 'relationship',

View File

@@ -6,7 +6,6 @@ import type { Config, Geo, Post } from '../../payload-types.js'
import {
ensureCompilationIsDone,
exactText,
getRoutes,
initPageConsoleErrorCatch,
saveDocAndAssert,
@@ -781,205 +780,6 @@ describe('General', () => {
expect(page.url()).toContain(postsUrl.list)
})
test('should bulk delete all on page', async () => {
await deleteAllPosts()
await Promise.all([createPost(), createPost(), createPost()])
await page.goto(postsUrl.list)
await page.locator('input#select-all').check()
await page.locator('.delete-documents__toggle').click()
await page.locator('#delete-posts #confirm-action').click()
await expect(page.locator('.payload-toast-container .toast-success')).toHaveText(
'Deleted 3 Posts successfully.',
)
// Poll until router has refreshed
await expect.poll(() => page.locator('.collection-list__no-results').isVisible()).toBeTruthy()
})
test('should bulk delete with filters and across pages', async () => {
await deleteAllPosts()
Array.from({ length: 6 }).forEach(async (_, i) => {
await createPost({ title: `Post ${i + 1}` })
})
await page.goto(postsUrl.list)
await page.locator('#search-filter-input').fill('Post')
await page.waitForURL(/search=Post/)
await expect(page.locator('.table table > tbody > tr')).toHaveCount(5)
await page.locator('input#select-all').check()
await page.locator('button#select-all-across-pages').click()
await page.locator('.delete-documents__toggle').click()
await page.locator('#delete-posts #confirm-action').click()
await expect(page.locator('.payload-toast-container .toast-success')).toHaveText(
'Deleted 6 Posts successfully.',
)
// Poll until router has refreshed
await expect.poll(() => page.locator('.table table > tbody > tr').count()).toBe(0)
})
test('should bulk update', async () => {
// First, delete all posts created by the seed
await deleteAllPosts()
const post1Title = 'Post'
const updatedPostTitle = `${post1Title} (Updated)`
await Promise.all([createPost({ title: post1Title }), createPost(), createPost()])
await page.goto(postsUrl.list)
await page.locator('input#select-all').check()
await page.locator('.edit-many__toggle').click()
await page.locator('.field-select .rs__control').click()
const titleOption = page.locator('.field-select .rs__option', {
hasText: exactText('Title'),
})
await expect(titleOption).toBeVisible()
await titleOption.click()
const titleInput = page.locator('#field-title')
await expect(titleInput).toBeVisible()
await titleInput.fill(updatedPostTitle)
await page.locator('.form-submit button[type="submit"].edit-many__publish').click()
await expect(page.locator('.payload-toast-container .toast-success')).toContainText(
'Updated 3 Posts successfully.',
)
await expect(page.locator('.row-1 .cell-title')).toContainText(updatedPostTitle)
await expect(page.locator('.row-2 .cell-title')).toContainText(updatedPostTitle)
await expect(page.locator('.row-3 .cell-title')).toContainText(updatedPostTitle)
})
test('should not override un-edited values in bulk edit if it has a defaultValue', async () => {
await deleteAllPosts()
const post1Title = 'Post'
const postData = {
title: 'Post',
arrayOfFields: [
{
optional: 'some optional array field',
innerArrayOfFields: [
{
innerOptional: 'some inner optional array field',
},
],
},
],
group: {
defaultValueField: 'not the group default value',
title: 'some title',
},
someBlock: [
{
textFieldForBlock: 'some text for block text',
blockType: 'textBlock',
},
],
defaultValueField: 'not the default value',
}
const updatedPostTitle = `${post1Title} (Updated)`
await createPost(postData)
await page.goto(postsUrl.list)
await page.locator('input#select-all').check()
await page.locator('.edit-many__toggle').click()
await page.locator('.field-select .rs__control').click()
const titleOption = page.locator('.field-select .rs__option', {
hasText: exactText('Title'),
})
await titleOption.click()
const titleInput = page.locator('#field-title')
await titleInput.fill(updatedPostTitle)
await page.locator('.form-submit button[type="submit"].edit-many__publish').click()
await expect(page.locator('.payload-toast-container .toast-success')).toContainText(
'Updated 1 Post successfully.',
)
const updatedPost = await payload.find({
collection: 'posts',
limit: 1,
})
expect(updatedPost.docs[0].title).toBe(updatedPostTitle)
expect(updatedPost.docs[0].arrayOfFields.length).toBe(1)
expect(updatedPost.docs[0].arrayOfFields[0].optional).toBe('some optional array field')
expect(updatedPost.docs[0].arrayOfFields[0].innerArrayOfFields.length).toBe(1)
expect(updatedPost.docs[0].someBlock[0].textFieldForBlock).toBe('some text for block text')
expect(updatedPost.docs[0].defaultValueField).toBe('not the default value')
})
test('should not show "select all across pages" button if already selected all', async () => {
await deleteAllPosts()
await createPost({ title: `Post 1` })
await page.goto(postsUrl.list)
await page.locator('input#select-all').check()
await expect(page.locator('button#select-all-across-pages')).toBeHidden()
})
test('should bulk update with filters and across pages', async () => {
// First, delete all posts created by the seed
await deleteAllPosts()
Array.from({ length: 6 }).forEach(async (_, i) => {
await createPost({ title: `Post ${i + 1}` })
})
await page.goto(postsUrl.list)
await page.locator('#search-filter-input').fill('Post')
await page.waitForURL(/search=Post/)
await expect(page.locator('.table table > tbody > tr')).toHaveCount(5)
await page.locator('input#select-all').check()
await page.locator('button#select-all-across-pages').click()
await page.locator('.edit-many__toggle').click()
await page.locator('.field-select .rs__control').click()
const titleOption = page.locator('.field-select .rs__option', {
hasText: exactText('Title'),
})
await expect(titleOption).toBeVisible()
await titleOption.click()
const titleInput = page.locator('#field-title')
await expect(titleInput).toBeVisible()
const updatedTitle = `Post (Updated)`
await titleInput.fill(updatedTitle)
await page.locator('.form-submit button[type="submit"].edit-many__publish').click()
await expect(page.locator('.payload-toast-container .toast-success')).toContainText(
'Updated 6 Posts successfully.',
)
// Poll until router has refreshed
await expect.poll(() => page.locator('.table table > tbody > tr').count()).toBe(5)
await expect(page.locator('.row-1 .cell-title')).toContainText(updatedTitle)
})
test('should update selection state after deselecting item following select all', async () => {
await deleteAllPosts()
Array.from({ length: 6 }).forEach(async (_, i) => {
await createPost({ title: `Post ${i + 1}` })
})
await page.goto(postsUrl.list)
await page.locator('input#select-all').check()
await page.locator('button#select-all-across-pages').click()
// Deselect the first row
await page.locator('.row-1 input').click()
// eslint-disable-next-line jest-dom/prefer-checked
await expect(page.locator('input#select-all')).not.toHaveAttribute('checked', '')
})
test('should save globals', async () => {
await page.goto(postsUrl.global(globalSlug))
@@ -1079,10 +879,6 @@ describe('General', () => {
})
})
async function deleteAllPosts() {
await payload.delete({ collection: postsCollectionSlug, where: { id: { exists: true } } })
}
async function createPost(overrides?: Partial<Post>): Promise<Post> {
return payload.create({
collection: postsCollectionSlug,

View File

@@ -54,6 +54,7 @@ export type SupportedTimezones =
| 'Asia/Singapore'
| 'Asia/Tokyo'
| 'Asia/Seoul'
| 'Australia/Brisbane'
| 'Australia/Sydney'
| 'Pacific/Guam'
| 'Pacific/Noumea'
@@ -232,31 +233,6 @@ export interface Post {
[k: string]: unknown;
}[]
| null;
arrayOfFields?:
| {
optional?: string | null;
innerArrayOfFields?:
| {
innerOptional?: string | null;
id?: string | null;
}[]
| null;
id?: string | null;
}[]
| null;
group?: {
defaultValueField?: string | null;
title?: string | null;
};
someBlock?:
| {
textFieldForBlock?: string | null;
id?: string | null;
blockName?: string | null;
blockType: 'textBlock';
}[]
| null;
defaultValueField?: string | null;
relationship?: (string | null) | Post;
users?: (string | null) | User;
customCell?: string | null;
@@ -676,36 +652,6 @@ export interface PostsSelect<T extends boolean = true> {
description?: T;
number?: T;
richText?: T;
arrayOfFields?:
| T
| {
optional?: T;
innerArrayOfFields?:
| T
| {
innerOptional?: T;
id?: T;
};
id?: T;
};
group?:
| T
| {
defaultValueField?: T;
title?: T;
};
someBlock?:
| T
| {
textBlock?:
| T
| {
textFieldForBlock?: T;
id?: T;
blockName?: T;
};
};
defaultValueField?: T;
relationship?: T;
users?: T;
customCell?: T;