Merge branch 'main' into fix/parent-labels-in-toast

This commit is contained in:
Jessica Chowdhury
2025-02-26 13:40:03 +00:00
216 changed files with 2854 additions and 1749 deletions

View File

@@ -12,6 +12,7 @@ import {
openNav,
saveDocAndAssert,
saveDocHotkeyAndAssert,
// throttleTest,
} from '../../../helpers.js'
import { AdminUrlUtil } from '../../../helpers/adminUrlUtil.js'
import { initPayloadE2ENoConfig } from '../../../helpers/initPayloadE2ENoConfig.js'
@@ -100,6 +101,12 @@ describe('General', () => {
})
beforeEach(async () => {
// await throttleTest({
// page,
// context,
// delay: 'Fast 4G',
// })
await reInitializeDB({
serverURL,
snapshotKey: 'adminTests',
@@ -721,25 +728,32 @@ describe('General', () => {
'Deleted 3 Posts successfully.',
)
await expect(page.locator('.collection-list__no-results')).toBeVisible()
// 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()
await Promise.all([createPost({ title: 'Post 1' }), createPost({ title: 'Post 2' })])
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 1')
await expect(page.locator('.table table > tbody > tr')).toHaveCount(1)
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.list-selection__button').click()
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 1 Post successfully.',
'Deleted 6 Posts successfully.',
)
await expect(page.locator('.table table > tbody > tr')).toHaveCount(1)
// Poll until router has refreshed
await expect.poll(() => page.locator('.table table > tbody > tr').count()).toBe(0)
})
test('should bulk update', async () => {
@@ -835,17 +849,30 @@ describe('General', () => {
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()
const post1Title = 'Post 1'
await Promise.all([createPost({ title: post1Title }), createPost({ title: 'Post 2' })])
const updatedPostTitle = `${post1Title} (Updated)`
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 1')
await expect(page.locator('.table table > tbody > tr')).toHaveCount(1)
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.list-selection__button').click()
await page.locator('button#select-all-across-pages').click()
await page.locator('.edit-many__toggle').click()
await page.locator('.field-select .rs__control').click()
@@ -857,23 +884,29 @@ describe('General', () => {
await titleOption.click()
const titleInput = page.locator('#field-title')
await expect(titleInput).toBeVisible()
await titleInput.fill(updatedPostTitle)
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 1 Post successfully.',
'Updated 6 Posts successfully.',
)
await expect(page.locator('.table table > tbody > tr')).toHaveCount(1)
await expect(page.locator('.row-1 .cell-title')).toContainText(updatedPostTitle)
// 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()
await createPost({ title: 'Post 1' })
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.list-selection__button').click()
await page.locator('button#select-all-across-pages').click()
// Deselect the first row
await page.locator('.row-1 input').click()

8
test/config/bin.ts Normal file
View File

@@ -0,0 +1,8 @@
import path from 'path'
import { fileURLToPath } from 'url'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
const { bin } = await import(path.resolve(dirname, '../../packages/payload/src/bin/index.js'))
await bin()

View File

@@ -80,6 +80,12 @@ export default buildConfigWithDefaults({
path: '/config',
},
],
bin: [
{
scriptPath: path.resolve(dirname, 'customScript.ts'),
key: 'start-server',
},
],
globals: [
{
slug: 'my-global',
@@ -107,13 +113,17 @@ export default buildConfigWithDefaults({
},
],
onInit: async (payload) => {
await payload.create({
collection: 'users',
data: {
email: devUser.email,
password: devUser.password,
},
})
const { totalDocs } = await payload.count({ collection: 'users' })
if (totalDocs === 0) {
await payload.create({
collection: 'users',
data: {
email: devUser.email,
password: devUser.password,
},
})
}
},
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),

View File

@@ -0,0 +1,13 @@
import type { SanitizedConfig } from 'payload'
import { writeFileSync } from 'fs'
import payload from 'payload'
import { testFilePath } from './testFilePath.js'
export const script = async (config: SanitizedConfig) => {
await payload.init({ config })
const data = await payload.find({ collection: 'users' })
writeFileSync(testFilePath, JSON.stringify(data), 'utf-8')
process.exit(0)
}

View File

@@ -1,11 +1,14 @@
import type { BlockField, Payload } from 'payload'
import { execSync } from 'child_process'
import { existsSync, readFileSync, rmSync } from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
import type { NextRESTClient } from '../helpers/NextRESTClient.js'
import { initPayloadInt } from '../helpers/initPayloadInt.js'
import { testFilePath } from './testFilePath.js'
let restClient: NextRESTClient
let payload: Payload
@@ -106,4 +109,31 @@ describe('Config', () => {
expect(response.headers.get('Access-Control-Allow-Headers')).toContain('x-custom-header')
})
})
describe('bin config', () => {
const executeCLI = (command: string) => {
execSync(`pnpm tsx "${path.resolve(dirname, 'bin.ts')}" ${command}`, {
env: {
...process.env,
PAYLOAD_CONFIG_PATH: path.resolve(dirname, 'config.ts'),
PAYLOAD_DROP_DATABASE: 'false',
},
stdio: 'inherit',
cwd: path.resolve(dirname, '../..'), // from root
})
}
const deleteTestFile = () => {
if (existsSync(testFilePath)) {
rmSync(testFilePath)
}
}
it('should execute a custom script', () => {
deleteTestFile()
executeCLI('start-server')
expect(JSON.parse(readFileSync(testFilePath, 'utf-8')).docs).toHaveLength(1)
deleteTestFile()
})
})
})

View File

@@ -0,0 +1,7 @@
import path from 'path'
import { fileURLToPath } from 'url'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
export const testFilePath = path.resolve(dirname, '_data.json')

View File

@@ -88,6 +88,28 @@ export const Relationship: CollectionConfig = {
relationTo: slug,
type: 'relationship',
},
{
type: 'collapsible',
label: 'Collapsible',
fields: [
{
name: 'nestedRelationshipFilteredByField',
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: 'relationshipFilteredAsync',
filterOptions: (args: FilterOptionsProps<FieldsRelationship>) => {

View File

@@ -351,6 +351,41 @@ describe('Relationship Field', () => {
await expect(valueOptions.locator(`text=${idToInclude}`)).toBeVisible()
})
test('should apply filter options of nested fields to list view filter controls', async () => {
const { id: idToInclude } = await payload.create({
collection: slug,
data: {
filter: 'Include me',
},
})
// first ensure that filter options are applied in the edit view
await page.goto(url.edit(idToInclude))
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
await page.goto(url.list)
const whereBuilder = await addListFilter({
page,
fieldLabel: 'Collapsible > Nested Relationship Filtered By Field',
operatorLabel: 'equals',
skipValueInput: true,
})
const valueInput = page.locator('.condition__value input')
await valueInput.click()
const valueOptions = whereBuilder.locator('.condition__value .rs__option')
await expect(valueOptions).toHaveCount(2)
await expect(valueOptions.locator(`text=None`)).toBeVisible()
await expect(valueOptions.locator(`text=${idToInclude}`)).toBeVisible()
})
test('should allow usage of relationTo in filterOptions', async () => {
const { id: include } = (await payload.create({
collection: relationOneSlug,

View File

@@ -177,6 +177,10 @@ export interface FieldsRelationship {
* This will filter the relationship options if the filter field in this document is set to "Include me"
*/
relationshipFilteredByField?: (string | null) | FieldsRelationship;
/**
* This will filter the relationship options if the filter field in this document is set to "Include me"
*/
nestedRelationshipFilteredByField?: (string | null) | FieldsRelationship;
relationshipFilteredAsync?: (string | null) | RelationOne;
relationshipManyFiltered?:
| (
@@ -506,6 +510,7 @@ export interface FieldsRelationshipSelect<T extends boolean = true> {
relationshipWithTitle?: T;
relationshipFilteredByID?: T;
relationshipFilteredByField?: T;
nestedRelationshipFilteredByField?: T;
relationshipFilteredAsync?: T;
relationshipManyFiltered?: T;
filter?: T;

View File

@@ -3,6 +3,7 @@ import type { BrowserContext, Page } from '@playwright/test'
import { expect, test } from '@playwright/test'
import { addBlock } from 'helpers/e2e/addBlock.js'
import { openBlocksDrawer } from 'helpers/e2e/openBlocksDrawer.js'
import { reorderBlocks } from 'helpers/e2e/reorderBlocks.js'
import path from 'path'
import { fileURLToPath } from 'url'
@@ -289,6 +290,39 @@ describe('Block fields', () => {
})
describe('row manipulation', () => {
test('moving rows should immediately move custom row labels', async () => {
await page.goto(url.create)
// first ensure that the first block has the custom header, and that the second block doesn't
await expect(
page.locator('#field-blocks #blocks-row-0 .blocks-field__block-header'),
).toHaveText('Custom Block Label: Content 01')
const secondBlockHeader = page.locator(
'#field-blocks #blocks-row-1 .blocks-field__block-header',
)
await expect(secondBlockHeader.locator('.blocks-field__block-pill')).toHaveText('Number')
await expect(secondBlockHeader.locator('input[id="blocks.1.blockName"]')).toHaveValue(
'Second block',
)
await reorderBlocks({
page,
fieldName: 'blocks',
fromBlockIndex: 0,
toBlockIndex: 1,
})
// Important: do _not_ poll here, use `textContent()` instead of `toHaveText()`
// This will prevent Playwright from polling for the change to the DOM
expect(
await page.locator('#field-blocks #blocks-row-1 .blocks-field__block-header').textContent(),
).toMatch(/^Custom Block Label: Content/)
})
describe('react hooks', () => {
test('should add 2 new block rows', async () => {
await page.goto(url.create)

View File

@@ -0,0 +1,38 @@
'use client'
import { useField } from '@payloadcms/ui'
export function AfterField() {
const { setValue } = useField({ path: 'customJSON' })
return (
<button
id="set-custom-json"
onClick={(e) => {
e.preventDefault()
setValue({
users: [
{
id: 1,
name: 'John Doe',
email: 'john.doe@example.com',
isActive: true,
roles: ['admin', 'editor'],
},
{
id: 2,
name: 'Jane Smith',
email: 'jane.smith@example.com',
isActive: false,
roles: ['viewer'],
},
],
})
}}
style={{ marginTop: '5px', padding: '5px 10px' }}
type="button"
>
Set Custom JSON
</button>
)
}

View File

@@ -103,4 +103,24 @@ describe('JSON', () => {
'"foo.with.periods": "bar"',
)
})
test('should update', async () => {
const createdDoc = await payload.create({
collection: 'json-fields',
data: {
customJSON: {
default: 'value',
},
},
})
await page.goto(url.edit(createdDoc.id))
const jsonField = page.locator('.json-field #field-customJSON')
await expect(jsonField).toContainText('"default": "value"')
const originalHeight = (await page.locator('#field-customJSON').boundingBox())?.height || 0
await page.locator('#set-custom-json').click()
const newHeight = (await page.locator('#field-customJSON').boundingBox())?.height || 0
expect(newHeight).toBeGreaterThan(originalHeight)
})
})

View File

@@ -67,6 +67,16 @@ const JSON: CollectionConfig = {
},
],
},
{
name: 'customJSON',
type: 'json',
admin: {
components: {
afterInput: ['./collections/JSON/AfterField#AfterField'],
},
},
label: 'Custom Json',
},
],
versions: {
maxPerDoc: 1,

View File

@@ -170,12 +170,7 @@ describe('Text', () => {
user: client.user,
key: 'text-fields-list',
value: {
columns: [
{
accessor: 'disableListColumnText',
active: true,
},
],
columns: [{ disableListColumnText: true }],
},
})

View File

@@ -165,6 +165,68 @@ describe('Fields', () => {
expect(missResult).toBeFalsy()
})
it('should query like on value', async () => {
const miss = await payload.create({
collection: 'text-fields',
data: {
text: 'dog',
},
})
const hit = await payload.create({
collection: 'text-fields',
data: {
text: 'cat',
},
})
const { docs } = await payload.find({
collection: 'text-fields',
where: {
text: {
like: 'cat',
},
},
})
const hitResult = docs.find(({ id: findID }) => hit.id === findID)
const missResult = docs.find(({ id: findID }) => miss.id === findID)
expect(hitResult).toBeDefined()
expect(missResult).toBeFalsy()
})
it('should query not_like on value', async () => {
const hit = await payload.create({
collection: 'text-fields',
data: {
text: 'dog',
},
})
const miss = await payload.create({
collection: 'text-fields',
data: {
text: 'cat',
},
})
const { docs } = await payload.find({
collection: 'text-fields',
where: {
text: {
not_like: 'cat',
},
},
})
const hitResult = docs.find(({ id: findID }) => hit.id === findID)
const missResult = docs.find(({ id: findID }) => miss.id === findID)
expect(hitResult).toBeDefined()
expect(missResult).toBeFalsy()
})
it('should query hasMany within an array', async () => {
const docFirst = await payload.create({
collection: 'text-fields',
@@ -2713,6 +2775,20 @@ describe('Fields', () => {
expect(docIDs).not.toContain(bazBar.id)
})
it('should query nested properties - not_like', async () => {
const { docs } = await payload.find({
collection: 'json-fields',
where: {
'json.baz': { not_like: 'bar' },
},
})
const docIDs = docs.map(({ id }) => id)
expect(docIDs).toContain(fooBar.id)
expect(docIDs).not.toContain(bazBar.id)
})
it('should query nested properties - equals', async () => {
const { docs } = await payload.find({
collection: 'json-fields',

View File

@@ -1474,6 +1474,15 @@ export interface JsonField {
| boolean
| null;
};
customJSON?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
updatedAt: string;
createdAt: string;
}
@@ -3165,6 +3174,7 @@ export interface JsonFieldsSelect<T extends boolean = true> {
| {
jsonWithinGroup?: T;
};
customJSON?: T;
updatedAt?: T;
createdAt?: T;
}

View File

@@ -391,9 +391,13 @@ export async function switchTab(page: Page, selector: string) {
*
* Useful to prevent the e2e test from passing when, for example, there are react missing key prop errors
* @param page
* @param options
*/
export function initPageConsoleErrorCatch(page: Page, options?: { ignoreCORS?: boolean }) {
const { ignoreCORS = false } = options || {} // Default to not ignoring CORS errors
const consoleErrors: string[] = []
let shouldCollectErrors = false
page.on('console', (msg) => {
if (
@@ -435,6 +439,21 @@ export function initPageConsoleErrorCatch(page: Page, options?: { ignoreCORS?: b
console.log(`Ignoring expected network error: ${msg.text()}`)
}
})
// Capture uncaught errors that do not appear in the console
page.on('pageerror', (error) => {
if (shouldCollectErrors) {
consoleErrors.push(`Page error: ${error.message}`)
} else {
throw new Error(`Page error: ${error.message}`)
}
})
return {
consoleErrors,
collectErrors: () => (shouldCollectErrors = true), // Enable collection of errors for specific tests
stopCollectingErrors: () => (shouldCollectErrors = false), // Disable collection of errors after the test
}
}
export function describeIfInCIOrHasLocalstack(): jest.Describe {

View File

@@ -175,7 +175,10 @@ describe('Joins Field', () => {
collection: categoriesSlug,
})
expect(Object.keys(categoryWithPosts)).toStrictEqual(['id', 'group'])
expect(categoryWithPosts).toStrictEqual({
id: categoryWithPosts.id,
group: categoryWithPosts.group,
})
expect(categoryWithPosts.group.relatedPosts.docs).toHaveLength(10)
expect(categoryWithPosts.group.relatedPosts.docs[0]).toHaveProperty('id')
@@ -1202,6 +1205,37 @@ describe('Joins Field', () => {
expect(parent.children.docs[1]?.value.id).toBe(child_1.id)
expect(parent.children.docs[1]?.relationTo).toBe('multiple-collections-1')
// Pagination across collections
parent = await payload.findByID({
collection: 'multiple-collections-parents',
id: parent.id,
depth: 1,
joins: {
children: {
limit: 1,
sort: 'title',
},
},
})
expect(parent.children.docs).toHaveLength(1)
expect(parent.children?.hasNextPage).toBe(true)
parent = await payload.findByID({
collection: 'multiple-collections-parents',
id: parent.id,
depth: 1,
joins: {
children: {
limit: 2,
sort: 'title',
},
},
})
expect(parent.children.docs).toHaveLength(2)
expect(parent.children?.hasNextPage).toBe(false)
// Sorting across collections
parent = await payload.findByID({
collection: 'multiple-collections-parents',

View File

@@ -64,8 +64,16 @@ export default buildConfigWithDefaults({
NestedArray,
NestedFields,
{
admin: {
listSearchableFields: 'name',
},
auth: true,
fields: [
{
name: 'name',
label: { en: 'Full name' },
type: 'text',
},
{
name: 'relation',
relationTo: localizedPostsSlug,
@@ -83,6 +91,7 @@ export default buildConfigWithDefaults({
fields: [
{
name: 'title',
label: { en: 'Full title' },
index: true,
localized: true,
type: 'text',

View File

@@ -1,7 +1,11 @@
import type { BrowserContext, Page } from '@playwright/test'
import type { GeneratedTypes } from 'helpers/sdk/types.js'
import { expect, test } from '@playwright/test'
import { navigateToDoc } from 'helpers/e2e/navigateToDoc.js'
import { openDocControls } from 'helpers/e2e/openDocControls.js'
import { upsertPrefs } from 'helpers/e2e/upsertPrefs.js'
import { RESTClient } from 'helpers/rest.js'
import path from 'path'
import { fileURLToPath } from 'url'
@@ -31,11 +35,6 @@ import {
spanishLocale,
withRequiredLocalizedFields,
} from './shared.js'
import { navigateToDoc } from 'helpers/e2e/navigateToDoc.js'
import { upsertPrefs } from 'helpers/e2e/upsertPrefs.js'
import { RESTClient } from 'helpers/rest.js'
import { GeneratedTypes } from 'helpers/sdk/types.js'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
@@ -119,16 +118,16 @@ describe('Localization', () => {
await expect(page.locator('.localizer .popup')).toHaveClass(/popup--active/)
const activeOption = await page.locator(
const activeOption = page.locator(
`.localizer .popup.popup--active .popup-button-list__button--selected`,
)
await expect(activeOption).toBeVisible()
const tagName = await activeOption.evaluate((node) => node.tagName)
await expect(tagName).not.toBe('A')
expect(tagName).not.toBe('A')
await expect(activeOption).not.toHaveAttribute('href')
await expect(tagName).not.toBe('BUTTON')
await expect(tagName).toBe('DIV')
expect(tagName).not.toBe('BUTTON')
expect(tagName).toBe('DIV')
})
})
@@ -140,7 +139,7 @@ describe('Localization', () => {
const createNewButtonLocator =
'.collection-list a[href="/admin/collections/cannot-create-default-locale/create"]'
await expect(page.locator(createNewButtonLocator)).not.toBeVisible()
await expect(page.locator(createNewButtonLocator)).toBeHidden()
await changeLocale(page, spanishLocale)
await expect(page.locator(createNewButtonLocator).first()).toBeVisible()
await page.goto(urlCannotCreateDefaultLocale.create)
@@ -330,11 +329,11 @@ describe('Localization', () => {
await page.goto(url.list)
const localeLabel = await page
const localeLabel = page
.locator('.localizer.app-header__localizer .localizer-button__current-label')
.innerText()
expect(localeLabel).not.toEqual('English')
await expect(localeLabel).not.toHaveText('English')
})
})
@@ -351,7 +350,7 @@ describe('Localization', () => {
await navigateToDoc(page, urlRelationshipLocalized)
const drawerToggler =
'#field-relationMultiRelationTo .relationship--single-value__drawer-toggler'
expect(page.locator(drawerToggler)).toBeEnabled()
await expect(page.locator(drawerToggler)).toBeEnabled()
await openDocDrawer(page, drawerToggler)
await expect(page.locator('.doc-drawer__header-text')).toContainText('spanish-relation2')
await page.locator('.doc-drawer__header-close').click()
@@ -518,7 +517,7 @@ describe('Localization', () => {
// only throttle test after initial load to avoid timeouts
const cdpSession = await throttleTest({
page: page,
page,
context,
delay: 'Fast 4G',
})
@@ -541,6 +540,13 @@ describe('Localization', () => {
await cdpSession.detach()
})
})
test('should use label in search filter when string or object', async () => {
await page.goto(url.list)
const searchInput = page.locator('.search-filter__input')
await expect(searchInput).toBeVisible()
await expect(searchInput).toHaveAttribute('placeholder', 'Search by Full title')
})
})
async function fillValues(data: Partial<LocalizedPost>) {

View File

@@ -64,7 +64,6 @@ export interface Config {
auth: {
users: UserAuthOperations;
};
blocks: {};
collections: {
richText: RichText;
'blocks-fields': BlocksField;
@@ -322,6 +321,7 @@ export interface NestedFieldTable {
*/
export interface User {
id: string;
name?: string | null;
relation?: (string | null) | LocalizedPost;
updatedAt: string;
createdAt: string;
@@ -928,6 +928,7 @@ export interface NestedFieldTablesSelect<T extends boolean = true> {
* via the `definition` "users_select".
*/
export interface UsersSelect<T extends boolean = true> {
name?: T;
relation?: T;
updatedAt?: T;
createdAt?: T;

View File

@@ -7,6 +7,7 @@ import type { NextRESTClient } from '../helpers/NextRESTClient.js'
import { devUser } from '../credentials.js'
import { initPayloadInt } from '../helpers/initPayloadInt.js'
import { tenantsSlug } from './shared.js'
let payload: Payload
let restClient: NextRESTClient
@@ -40,7 +41,7 @@ describe('@payloadcms/plugin-multi-tenant', () => {
describe('tenants', () => {
it('should create a tenant', async () => {
const tenant1 = await payload.create({
collection: 'tenants',
collection: tenantsSlug,
data: {
name: 'tenant1',
domain: 'tenant1.com',

View File

@@ -1,19 +1,19 @@
import type { Config } from 'payload'
import { devUser } from '../../credentials.js'
import { menuItemsSlug, menuSlug, usersSlug } from '../shared.js'
import { menuItemsSlug, menuSlug, tenantsSlug, usersSlug } from '../shared.js'
export const seed: Config['onInit'] = async (payload) => {
// create tenants
const blueDogTenant = await payload.create({
collection: 'tenants',
collection: tenantsSlug,
data: {
name: 'Blue Dog',
domain: 'bluedog.com',
},
})
const steelCatTenant = await payload.create({
collection: 'tenants',
collection: tenantsSlug,
data: {
name: 'Steel Cat',
domain: 'steelcat.com',

View File

@@ -76,7 +76,6 @@ describe('@payloadcms/plugin-nested-docs', () => {
},
})
}
// update parent doc
await payload.update({
collection: 'pages',
@@ -110,6 +109,91 @@ describe('@payloadcms/plugin-nested-docs', () => {
// @ts-ignore
expect(lastUpdatedChildBreadcrumbs[0].url).toStrictEqual('/11-children-updated')
})
it('should return breadcrumbs as an array of objects', async () => {
const parentDoc = await payload.create({
collection: 'pages',
data: {
title: 'parent doc',
slug: 'parent-doc',
_status: 'published',
},
})
const childDoc = await payload.create({
collection: 'pages',
data: {
title: 'child doc',
slug: 'child-doc',
parent: parentDoc.id,
_status: 'published',
},
})
// expect breadcrumbs to be an array
expect(childDoc.breadcrumbs).toBeInstanceOf(Array)
expect(childDoc.breadcrumbs).toBeDefined()
// expect each to be objects
childDoc.breadcrumbs?.map((breadcrumb) => {
expect(breadcrumb).toBeInstanceOf(Object)
})
})
it('should update child doc breadcrumb without affecting any other data', async () => {
const parentDoc = await payload.create({
collection: 'pages',
data: {
title: 'parent doc',
slug: 'parent',
},
})
const childDoc = await payload.create({
collection: 'pages',
data: {
title: 'child doc',
slug: 'child',
parent: parentDoc.id,
_status: 'published',
},
})
await payload.update({
collection: 'pages',
id: parentDoc.id,
data: {
title: 'parent updated',
slug: 'parent-updated',
_status: 'published',
},
})
const updatedChild = await payload
.find({
collection: 'pages',
where: {
id: {
equals: childDoc.id,
},
},
})
.then(({ docs }) => docs[0])
if (!updatedChild) {
return
}
// breadcrumbs should be updated
expect(updatedChild.breadcrumbs).toHaveLength(2)
expect(updatedChild.breadcrumbs?.[0]?.url).toStrictEqual('/parent-updated')
expect(updatedChild.breadcrumbs?.[1]?.url).toStrictEqual('/parent-updated/child')
// no other data should be affected
expect(updatedChild.title).toEqual('child doc')
expect(updatedChild.slug).toEqual('child')
})
})
describe('overrides', () => {

View File

@@ -1648,7 +1648,10 @@ describe('Select', () => {
},
})
expect(Object.keys(res)).toStrictEqual(['id', 'text'])
expect(res).toStrictEqual({
id: res.id,
text: res.text,
})
})
it('should apply select with updateByID', async () => {
@@ -1661,7 +1664,10 @@ describe('Select', () => {
select: { text: true },
})
expect(Object.keys(res)).toStrictEqual(['id', 'text'])
expect(res).toStrictEqual({
id: res.id,
text: res.text,
})
})
it('should apply select with updateBulk', async () => {
@@ -1680,7 +1686,10 @@ describe('Select', () => {
assert(res.docs[0])
expect(Object.keys(res.docs[0])).toStrictEqual(['id', 'text'])
expect(res.docs[0]).toStrictEqual({
id: res.docs[0].id,
text: res.docs[0].text,
})
})
it('should apply select with deleteByID', async () => {
@@ -1692,7 +1701,10 @@ describe('Select', () => {
select: { text: true },
})
expect(Object.keys(res)).toStrictEqual(['id', 'text'])
expect(res).toStrictEqual({
id: res.id,
text: res.text,
})
})
it('should apply select with deleteBulk', async () => {
@@ -1710,7 +1722,10 @@ describe('Select', () => {
assert(res.docs[0])
expect(Object.keys(res.docs[0])).toStrictEqual(['id', 'text'])
expect(res.docs[0]).toStrictEqual({
id: res.docs[0].id,
text: res.docs[0].text,
})
})
it('should apply select with duplicate', async () => {
@@ -1722,7 +1737,10 @@ describe('Select', () => {
select: { text: true },
})
expect(Object.keys(res)).toStrictEqual(['id', 'text'])
expect(res).toStrictEqual({
id: res.id,
text: res.text,
})
})
})

View File

@@ -26,7 +26,7 @@ export const Uploads1: CollectionConfig = {
relationTo: 'uploads-2',
filterOptions: {
mimeType: {
equals: 'image/png',
in: ['image/png', 'application/pdf'],
},
},
hasMany: true,

View File

@@ -65,6 +65,9 @@ let uploadsOne: AdminUrlUtil
let uploadsTwo: AdminUrlUtil
let customUploadFieldURL: AdminUrlUtil
let hideFileInputOnCreateURL: AdminUrlUtil
let consoleErrorsFromPage: string[] = []
let collectErrorsFromPage: () => boolean
let stopCollectingErrorsFromPage: () => boolean
describe('Uploads', () => {
let page: Page
@@ -99,7 +102,14 @@ describe('Uploads', () => {
const context = await browser.newContext()
page = await context.newPage()
initPageConsoleErrorCatch(page, { ignoreCORS: true })
const { consoleErrors, collectErrors, stopCollectingErrors } = initPageConsoleErrorCatch(page, {
ignoreCORS: true,
})
consoleErrorsFromPage = consoleErrors
collectErrorsFromPage = collectErrors
stopCollectingErrorsFromPage = stopCollectingErrors
await ensureCompilationIsDone({ page, serverURL })
})
@@ -744,6 +754,55 @@ describe('Uploads', () => {
await saveDocAndAssert(page)
})
test('should bulk upload non-image files without page errors', async () => {
// Enable collection ONLY for this test
collectErrorsFromPage()
// Navigate to the upload creation page
await page.goto(uploadsOne.create)
await page.waitForURL(uploadsOne.create)
// Upload single file
await page.setInputFiles(
'.file-field input[type="file"]',
path.resolve(dirname, './image.png'),
)
const filename = page.locator('.file-field__filename')
await expect(filename).toHaveValue('image.png')
const bulkUploadButton = page.locator('#field-hasManyUpload button', {
hasText: exactText('Create New'),
})
await bulkUploadButton.click()
const bulkUploadModal = page.locator('#bulk-upload-drawer-slug-1')
await expect(bulkUploadModal).toBeVisible()
await page.setInputFiles('#bulk-upload-drawer-slug-1 .dropzone input[type="file"]', [
path.resolve(dirname, './test-pdf.pdf'),
])
await page
.locator('.bulk-upload--file-manager .render-fields #field-prefix')
.fill('prefix-one')
const saveButton = page.locator('.bulk-upload--actions-bar__saveButtons button')
await saveButton.click()
await page.waitForSelector('#field-hasManyUpload .upload--has-many__dragItem')
const itemCount = await page
.locator('#field-hasManyUpload .upload--has-many__dragItem')
.count()
expect(itemCount).toEqual(1)
await saveDocAndAssert(page)
// Assert no console errors occurred for this test only
expect(consoleErrorsFromPage).toEqual([])
// Reset global behavior for other tests
stopCollectingErrorsFromPage()
})
test('should apply field value to all bulk upload files after edit many', async () => {
// Navigate to the upload creation page
await page.goto(uploadsOne.create)

View File

@@ -64,7 +64,6 @@ export interface Config {
auth: {
users: UserAuthOperations;
};
blocks: {};
collections: {
relation: Relation;
audio: Audio;

BIN
test/uploads/test-pdf.pdf Normal file

Binary file not shown.

View File

@@ -438,6 +438,47 @@ describe('Versions', () => {
await expect(drawer.locator('.id-label')).toBeVisible()
})
test('collection - autosave - should not create duplicates when clicking Create new', async () => {
// This test checks that when we click "Create new" in the list view, it only creates 1 extra document and not more
const { totalDocs: initialDocsCount } = await payload.find({
collection: autosaveCollectionSlug,
draft: true,
})
await page.goto(autosaveURL.create)
await page.locator('#field-title').fill('autosave title')
await waitForAutoSaveToRunAndComplete(page)
await expect(page.locator('#field-title')).toHaveValue('autosave title')
const { totalDocs: updatedDocsCount } = await payload.find({
collection: autosaveCollectionSlug,
draft: true,
})
await expect(() => {
expect(updatedDocsCount).toBe(initialDocsCount + 1)
}).toPass({ timeout: POLL_TOPASS_TIMEOUT, intervals: [100] })
await page.goto(autosaveURL.list)
const createNewButton = page.locator('.list-header .btn:has-text("Create New")')
await createNewButton.click()
await page.waitForURL(`**/${autosaveCollectionSlug}/**`)
await page.locator('#field-title').fill('autosave title')
await waitForAutoSaveToRunAndComplete(page)
await expect(page.locator('#field-title')).toHaveValue('autosave title')
const { totalDocs: latestDocsCount } = await payload.find({
collection: autosaveCollectionSlug,
draft: true,
})
await expect(() => {
expect(latestDocsCount).toBe(updatedDocsCount + 1)
}).toPass({ timeout: POLL_TOPASS_TIMEOUT, intervals: [100] })
})
test('collection - should update updatedAt', async () => {
await page.goto(url.create)
await page.waitForURL(`**/${url.create}`)
@@ -757,7 +798,7 @@ describe('Versions', () => {
// schedule publish should not be available before document has been saved
await page.locator('#action-save-popup').click()
await expect(page.locator('#schedule-publish')).not.toBeVisible()
await expect(page.locator('#schedule-publish')).toBeHidden()
// save draft then try to schedule publish
await saveDocAndAssert(page)