This clarifies that jobs.autoRun only *runs* already-queued jobs. It does not queue the jobs for you. Also adds an e2e test as this functionality had no e2e coverage
567 lines
13 KiB
TypeScript
567 lines
13 KiB
TypeScript
import type { FormState, Payload, User } from 'payload'
|
|
|
|
import { buildFormState } from '@payloadcms/ui/utilities/buildFormState'
|
|
import path from 'path'
|
|
import { createLocalReq } from 'payload'
|
|
import { fileURLToPath } from 'url'
|
|
|
|
import type { NextRESTClient } from '../helpers/NextRESTClient.js'
|
|
|
|
import { devUser } from '../credentials.js'
|
|
import { initPayloadInt } from '../helpers/initPayloadInt.js'
|
|
import { postsSlug } from './collections/Posts/index.js'
|
|
// eslint-disable-next-line payload/no-relative-monorepo-imports
|
|
import { mergeServerFormState } from '../../packages/ui/src/forms/Form/mergeServerFormState.js'
|
|
|
|
let payload: Payload
|
|
let restClient: NextRESTClient
|
|
let user: User
|
|
|
|
const { email, password } = devUser
|
|
const filename = fileURLToPath(import.meta.url)
|
|
const dirname = path.dirname(filename)
|
|
|
|
describe('Form State', () => {
|
|
beforeAll(async () => {
|
|
;({ payload, restClient } = await initPayloadInt(dirname, undefined, true))
|
|
|
|
const data = await restClient
|
|
.POST('/users/login', {
|
|
body: JSON.stringify({
|
|
email,
|
|
password,
|
|
}),
|
|
})
|
|
.then((res) => res.json())
|
|
|
|
user = data.user
|
|
})
|
|
|
|
afterAll(async () => {
|
|
await payload.destroy()
|
|
})
|
|
|
|
it('should build entire form state', async () => {
|
|
const req = await createLocalReq({ user }, payload)
|
|
|
|
const postData = await payload.create({
|
|
collection: postsSlug,
|
|
data: {
|
|
title: 'Test Post',
|
|
},
|
|
})
|
|
|
|
const { state } = await buildFormState({
|
|
mockRSCs: true,
|
|
id: postData.id,
|
|
collectionSlug: postsSlug,
|
|
data: postData,
|
|
docPermissions: {
|
|
create: true,
|
|
delete: true,
|
|
fields: true,
|
|
read: true,
|
|
readVersions: true,
|
|
update: true,
|
|
},
|
|
docPreferences: {
|
|
fields: {},
|
|
},
|
|
documentFormState: undefined,
|
|
operation: 'update',
|
|
renderAllFields: false,
|
|
req,
|
|
schemaPath: postsSlug,
|
|
})
|
|
|
|
expect(state).toMatchObject({
|
|
title: {
|
|
value: postData.title,
|
|
initialValue: postData.title,
|
|
},
|
|
updatedAt: {
|
|
value: postData.updatedAt,
|
|
initialValue: postData.updatedAt,
|
|
},
|
|
createdAt: {
|
|
value: postData.createdAt,
|
|
initialValue: postData.createdAt,
|
|
},
|
|
renderTracker: {},
|
|
validateUsingEvent: {},
|
|
blocks: {
|
|
initialValue: 0,
|
|
rows: [],
|
|
value: 0,
|
|
},
|
|
})
|
|
})
|
|
|
|
it('should use `select` to build partial form state with only specified fields', async () => {
|
|
const req = await createLocalReq({ user }, payload)
|
|
|
|
const postData = await payload.create({
|
|
collection: postsSlug,
|
|
data: {
|
|
title: 'Test Post',
|
|
},
|
|
})
|
|
|
|
const { state } = await buildFormState({
|
|
mockRSCs: true,
|
|
id: postData.id,
|
|
collectionSlug: postsSlug,
|
|
data: postData,
|
|
docPermissions: undefined,
|
|
docPreferences: {
|
|
fields: {},
|
|
},
|
|
documentFormState: undefined,
|
|
operation: 'update',
|
|
renderAllFields: false,
|
|
req,
|
|
schemaPath: postsSlug,
|
|
select: {
|
|
title: true,
|
|
},
|
|
})
|
|
|
|
expect(state).toStrictEqual({
|
|
title: {
|
|
value: postData.title,
|
|
initialValue: postData.title,
|
|
lastRenderedPath: 'title',
|
|
addedByServer: true,
|
|
},
|
|
})
|
|
})
|
|
|
|
it('should not render custom components when `lastRenderedPath` exists', async () => {
|
|
const req = await createLocalReq({ user }, payload)
|
|
|
|
const { state: stateWithRow } = await buildFormState({
|
|
mockRSCs: true,
|
|
collectionSlug: postsSlug,
|
|
formState: {
|
|
array: {
|
|
rows: [
|
|
{
|
|
id: '123',
|
|
},
|
|
],
|
|
},
|
|
'array.0.id': {
|
|
value: '123',
|
|
initialValue: '123',
|
|
},
|
|
},
|
|
docPermissions: undefined,
|
|
docPreferences: {
|
|
fields: {},
|
|
},
|
|
documentFormState: undefined,
|
|
operation: 'update',
|
|
renderAllFields: false,
|
|
req,
|
|
schemaPath: postsSlug,
|
|
})
|
|
|
|
// Ensure that row 1 _DOES_ return with rendered components
|
|
expect(stateWithRow?.['array.0.customTextField']?.lastRenderedPath).toStrictEqual(
|
|
'array.0.customTextField',
|
|
)
|
|
expect(stateWithRow?.['array.0.customTextField']?.customComponents?.Field).toBeDefined()
|
|
|
|
const { state: stateWithTitle } = await buildFormState({
|
|
mockRSCs: true,
|
|
collectionSlug: postsSlug,
|
|
formState: {
|
|
array: {
|
|
rows: [
|
|
{
|
|
id: '123',
|
|
},
|
|
{
|
|
id: '456',
|
|
},
|
|
],
|
|
},
|
|
'array.0.id': {
|
|
value: '123',
|
|
initialValue: '123',
|
|
},
|
|
'array.0.customTextField': {
|
|
lastRenderedPath: 'array.0.customTextField',
|
|
},
|
|
'array.1.id': {
|
|
value: '456',
|
|
initialValue: '456',
|
|
},
|
|
},
|
|
docPermissions: undefined,
|
|
docPreferences: {
|
|
fields: {},
|
|
},
|
|
documentFormState: undefined,
|
|
operation: 'update',
|
|
renderAllFields: false,
|
|
schemaPath: postsSlug,
|
|
req,
|
|
})
|
|
|
|
// Ensure that row 1 _DOES NOT_ return with rendered components
|
|
expect(stateWithTitle?.['array.0.customTextField']).toHaveProperty('lastRenderedPath')
|
|
expect(stateWithTitle?.['array.0.customTextField']).not.toHaveProperty('customComponents')
|
|
|
|
// Ensure that row 2 _DOES_ return with rendered components
|
|
expect(stateWithTitle?.['array.1.customTextField']).toHaveProperty('lastRenderedPath')
|
|
expect(stateWithTitle?.['array.1.customTextField']).toHaveProperty('customComponents')
|
|
expect(stateWithTitle?.['array.1.customTextField']?.customComponents?.Field).toBeDefined()
|
|
})
|
|
|
|
it('should add `addedByServer` flag to fields that originate on the server', async () => {
|
|
const req = await createLocalReq({ user }, payload)
|
|
|
|
const postData = await payload.create({
|
|
collection: postsSlug,
|
|
data: {
|
|
title: 'Test Post',
|
|
blocks: [
|
|
{
|
|
blockType: 'text',
|
|
text: 'Test block',
|
|
},
|
|
],
|
|
},
|
|
})
|
|
|
|
const { state } = await buildFormState({
|
|
mockRSCs: true,
|
|
id: postData.id,
|
|
collectionSlug: postsSlug,
|
|
data: postData,
|
|
docPermissions: undefined,
|
|
docPreferences: {
|
|
fields: {},
|
|
},
|
|
documentFormState: undefined,
|
|
operation: 'update',
|
|
renderAllFields: false,
|
|
req,
|
|
schemaPath: postsSlug,
|
|
})
|
|
|
|
expect(state.title?.addedByServer).toBe(true)
|
|
expect(state['blocks.0.blockType']?.addedByServer).toBe(true)
|
|
|
|
// Ensure that `addedByServer` is removed after being received by the client
|
|
const newState = mergeServerFormState({
|
|
currentState: state,
|
|
incomingState: state,
|
|
})
|
|
|
|
expect(newState.title?.addedByServer).toBeUndefined()
|
|
})
|
|
|
|
it('should not omit value and initialValue from fields added by the server', () => {
|
|
const currentState: FormState = {
|
|
array: {
|
|
rows: [
|
|
{
|
|
id: '1',
|
|
},
|
|
],
|
|
},
|
|
}
|
|
|
|
const serverState: FormState = {
|
|
array: {
|
|
rows: [
|
|
{
|
|
id: '1',
|
|
},
|
|
],
|
|
},
|
|
'array.0.id': {
|
|
value: '1',
|
|
initialValue: '1',
|
|
},
|
|
'array.0.customTextField': {
|
|
value: 'Test',
|
|
initialValue: 'Test',
|
|
addedByServer: true,
|
|
},
|
|
}
|
|
|
|
const newState = mergeServerFormState({
|
|
currentState,
|
|
incomingState: serverState,
|
|
})
|
|
|
|
expect(newState['array.0.customTextField']).toStrictEqual({
|
|
passesCondition: true,
|
|
valid: true,
|
|
value: 'Test',
|
|
initialValue: 'Test',
|
|
})
|
|
})
|
|
|
|
it('should merge array rows without losing rows added to local state', () => {
|
|
const currentState: FormState = {
|
|
array: {
|
|
rows: [
|
|
{
|
|
id: '1',
|
|
lastRenderedPath: 'array.0.customTextField',
|
|
},
|
|
{
|
|
id: '2',
|
|
isLoading: true,
|
|
},
|
|
],
|
|
},
|
|
'array.0.id': {
|
|
value: '1',
|
|
initialValue: '1',
|
|
},
|
|
'array.1.id': {
|
|
value: '2',
|
|
initialValue: '2',
|
|
},
|
|
}
|
|
|
|
const serverState: FormState = {
|
|
array: {
|
|
rows: [
|
|
{
|
|
id: '1',
|
|
lastRenderedPath: 'array.0.customTextField',
|
|
},
|
|
],
|
|
},
|
|
'array.0.id': {
|
|
value: '1',
|
|
initialValue: '1',
|
|
},
|
|
'array.0.customTextField': {
|
|
value: 'Test',
|
|
initialValue: 'Test',
|
|
addedByServer: true,
|
|
},
|
|
}
|
|
|
|
const newState = mergeServerFormState({
|
|
currentState,
|
|
incomingState: serverState,
|
|
})
|
|
|
|
// Row 2 should still exist
|
|
expect(newState).toStrictEqual({
|
|
array: {
|
|
passesCondition: true,
|
|
valid: true,
|
|
rows: [
|
|
{
|
|
id: '1',
|
|
lastRenderedPath: 'array.0.customTextField',
|
|
},
|
|
{
|
|
id: '2',
|
|
isLoading: true,
|
|
},
|
|
],
|
|
},
|
|
'array.0.id': {
|
|
value: '1',
|
|
initialValue: '1',
|
|
passesCondition: true,
|
|
valid: true,
|
|
},
|
|
'array.0.customTextField': {
|
|
value: 'Test',
|
|
initialValue: 'Test',
|
|
passesCondition: true,
|
|
valid: true,
|
|
},
|
|
'array.1.id': {
|
|
value: '2',
|
|
initialValue: '2',
|
|
},
|
|
})
|
|
})
|
|
|
|
it('should merge array rows without bringing back rows deleted from local state', () => {
|
|
const currentState: FormState = {
|
|
array: {
|
|
rows: [
|
|
{
|
|
id: '1',
|
|
lastRenderedPath: 'array.0.customTextField',
|
|
},
|
|
],
|
|
},
|
|
'array.0.id': {
|
|
value: '1',
|
|
initialValue: '1',
|
|
},
|
|
}
|
|
|
|
const serverState: FormState = {
|
|
array: {
|
|
rows: [
|
|
{
|
|
id: '1',
|
|
lastRenderedPath: 'array.0.customTextField',
|
|
},
|
|
{
|
|
id: '2',
|
|
lastRenderedPath: 'array.1.customTextField',
|
|
},
|
|
],
|
|
},
|
|
'array.0.id': {
|
|
value: '1',
|
|
initialValue: '1',
|
|
},
|
|
'array.0.customTextField': {
|
|
value: 'Test',
|
|
initialValue: 'Test',
|
|
addedByServer: true,
|
|
},
|
|
'array.1.id': {
|
|
value: '2',
|
|
initialValue: '2',
|
|
},
|
|
'array.1.customTextField': {
|
|
value: 'Test',
|
|
initialValue: 'Test',
|
|
},
|
|
}
|
|
|
|
const newState = mergeServerFormState({
|
|
currentState,
|
|
incomingState: serverState,
|
|
})
|
|
|
|
// Row 2 should not exist
|
|
expect(newState).toStrictEqual({
|
|
array: {
|
|
passesCondition: true,
|
|
valid: true,
|
|
rows: [
|
|
{
|
|
id: '1',
|
|
lastRenderedPath: 'array.0.customTextField',
|
|
},
|
|
],
|
|
},
|
|
'array.0.id': {
|
|
value: '1',
|
|
initialValue: '1',
|
|
passesCondition: true,
|
|
valid: true,
|
|
},
|
|
'array.0.customTextField': {
|
|
value: 'Test',
|
|
initialValue: 'Test',
|
|
passesCondition: true,
|
|
valid: true,
|
|
},
|
|
})
|
|
})
|
|
|
|
it('should merge new fields returned from the server that do not yet exist in local state', () => {
|
|
const currentState: FormState = {
|
|
array: {
|
|
rows: [
|
|
{
|
|
id: '1',
|
|
isLoading: true,
|
|
},
|
|
],
|
|
},
|
|
'array.0.id': {
|
|
value: '1',
|
|
initialValue: '1',
|
|
},
|
|
}
|
|
|
|
const serverState: FormState = {
|
|
array: {
|
|
rows: [
|
|
{
|
|
id: '1',
|
|
lastRenderedPath: 'array.0.customTextField',
|
|
isLoading: false,
|
|
},
|
|
],
|
|
},
|
|
'array.0.id': {
|
|
value: '1',
|
|
initialValue: '1',
|
|
},
|
|
'array.0.customTextField': {
|
|
value: 'Test',
|
|
initialValue: 'Test',
|
|
addedByServer: true,
|
|
},
|
|
}
|
|
|
|
const newState = mergeServerFormState({
|
|
currentState,
|
|
incomingState: serverState,
|
|
})
|
|
|
|
expect(newState).toStrictEqual({
|
|
array: {
|
|
passesCondition: true,
|
|
valid: true,
|
|
rows: [
|
|
{
|
|
id: '1',
|
|
lastRenderedPath: 'array.0.customTextField',
|
|
isLoading: false,
|
|
},
|
|
],
|
|
},
|
|
'array.0.id': {
|
|
passesCondition: true,
|
|
valid: true,
|
|
value: '1',
|
|
initialValue: '1',
|
|
},
|
|
'array.0.customTextField': {
|
|
passesCondition: true,
|
|
valid: true,
|
|
value: 'Test',
|
|
initialValue: 'Test',
|
|
},
|
|
})
|
|
})
|
|
|
|
it('should return the same object reference when only modifying a value', () => {
|
|
const currentState = {
|
|
title: {
|
|
value: 'Test Post',
|
|
initialValue: 'Test Post',
|
|
valid: true,
|
|
passesCondition: true,
|
|
},
|
|
}
|
|
|
|
const newState = mergeServerFormState({
|
|
currentState,
|
|
incomingState: {
|
|
title: {
|
|
value: 'Test Post (modified)',
|
|
initialValue: 'Test Post',
|
|
valid: true,
|
|
passesCondition: true,
|
|
},
|
|
},
|
|
})
|
|
|
|
expect(newState === currentState).toBe(true)
|
|
})
|
|
})
|