snapshot of https://github.com/frappe/crm.git (develop, 2026-09-18; blobs > 131072B omitted)
Some checks failed
Frontend / Unit Tests & Coverage (push) Has been skipped
Regenerate POT file (translatable strings) / Regenerate POT file (develop) (push) Failing after 1m13s
Regenerate POT file (translatable strings) / Regenerate POT file (main-hotfix) (push) Failing after 6s
Server / Tests (frappe ${{ matrix.frappe-branch }}) (push) Has been skipped
Server / Resolve frappe matrix (push) Failing after 1s
Frontend / Lint (push) Failing after 1s
Frontend / Production Build (push) Failing after 1s
Server / Coverage Wrap Up (push) Has been skipped
Frontend / Coverage Wrap Up (push) Has been skipped
Some checks failed
Frontend / Unit Tests & Coverage (push) Has been skipped
Regenerate POT file (translatable strings) / Regenerate POT file (develop) (push) Failing after 1m13s
Regenerate POT file (translatable strings) / Regenerate POT file (main-hotfix) (push) Failing after 6s
Server / Tests (frappe ${{ matrix.frappe-branch }}) (push) Has been skipped
Server / Resolve frappe matrix (push) Failing after 1s
Frontend / Lint (push) Failing after 1s
Frontend / Production Build (push) Failing after 1s
Server / Coverage Wrap Up (push) Has been skipped
Frontend / Coverage Wrap Up (push) Has been skipped
This commit is contained in:
255
frontend/tests/unit/renderFieldLayoutDialog.test.js
Normal file
255
frontend/tests/unit/renderFieldLayoutDialog.test.js
Normal file
@@ -0,0 +1,255 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import {
|
||||
fieldLayoutDialogs,
|
||||
renderFieldLayoutDialog,
|
||||
} from '@/utils/renderFieldLayoutDialog'
|
||||
|
||||
describe('renderFieldLayoutDialog', () => {
|
||||
beforeEach(() => {
|
||||
fieldLayoutDialogs.value = []
|
||||
})
|
||||
|
||||
// ── Core promise behavior ──
|
||||
|
||||
it('returns a Promise', () => {
|
||||
const result = renderFieldLayoutDialog({ title: 'Test' })
|
||||
expect(result).toBeInstanceOf(Promise)
|
||||
})
|
||||
|
||||
it('pushes a dialog entry to fieldLayoutDialogs', () => {
|
||||
renderFieldLayoutDialog({ title: 'Test Dialog' })
|
||||
expect(fieldLayoutDialogs.value).toHaveLength(1)
|
||||
expect(fieldLayoutDialogs.value[0].props.title).toBe('Test Dialog')
|
||||
expect(fieldLayoutDialogs.value[0].key).toBeTruthy()
|
||||
})
|
||||
|
||||
it('resolves with data when onResolve is called', async () => {
|
||||
const promise = renderFieldLayoutDialog({ title: 'Test' })
|
||||
fieldLayoutDialogs.value[0].props.onResolve({ name: 'Alice' })
|
||||
expect(await promise).toEqual({ name: 'Alice' })
|
||||
})
|
||||
|
||||
it('resolves with null on cancel', async () => {
|
||||
const promise = renderFieldLayoutDialog({ title: 'Test' })
|
||||
fieldLayoutDialogs.value[0].props.onResolve(null)
|
||||
expect(await promise).toBeNull()
|
||||
})
|
||||
|
||||
it('only resolves once even if onResolve is called multiple times', async () => {
|
||||
const promise = renderFieldLayoutDialog({ title: 'Test' })
|
||||
const entry = fieldLayoutDialogs.value[0]
|
||||
entry.props.onResolve({ first: true })
|
||||
entry.props.onResolve({ second: true })
|
||||
expect(await promise).toEqual({ first: true })
|
||||
})
|
||||
|
||||
it('removes dialog entry after resolve (with delay)', async () => {
|
||||
vi.useFakeTimers()
|
||||
const promise = renderFieldLayoutDialog({ title: 'Test' })
|
||||
expect(fieldLayoutDialogs.value).toHaveLength(1)
|
||||
fieldLayoutDialogs.value[0].props.onResolve({ done: true })
|
||||
expect(fieldLayoutDialogs.value).toHaveLength(1)
|
||||
vi.advanceTimersByTime(300)
|
||||
expect(fieldLayoutDialogs.value).toHaveLength(0)
|
||||
vi.useRealTimers()
|
||||
await promise
|
||||
})
|
||||
|
||||
// ── Options passthrough ──
|
||||
|
||||
it('passes all core options through to dialog props', () => {
|
||||
renderFieldLayoutDialog({
|
||||
title: 'My Dialog',
|
||||
doctype: 'CRM Lost Reason',
|
||||
size: 'lg',
|
||||
defaults: { lead: 'LEAD-001' },
|
||||
required: ['lost_reason'],
|
||||
actions: [{ label: 'Save', variant: 'solid' }],
|
||||
})
|
||||
|
||||
const props = fieldLayoutDialogs.value[0].props
|
||||
expect(props.title).toBe('My Dialog')
|
||||
expect(props.doctype).toBe('CRM Lost Reason')
|
||||
expect(props.size).toBe('lg')
|
||||
expect(props.defaults).toEqual({ lead: 'LEAD-001' })
|
||||
expect(props.required).toEqual(['lost_reason'])
|
||||
expect(props.actions).toEqual([{ label: 'Save', variant: 'solid' }])
|
||||
})
|
||||
|
||||
it('passes fieldnames option', () => {
|
||||
renderFieldLayoutDialog({
|
||||
title: 'Pick Fields',
|
||||
doctype: 'CRM Lead',
|
||||
fieldnames: ['first_name', 'email', 'phone'],
|
||||
})
|
||||
const props = fieldLayoutDialogs.value[0].props
|
||||
expect(props.fieldnames).toEqual(['first_name', 'email', 'phone'])
|
||||
})
|
||||
|
||||
it('passes inline fields', () => {
|
||||
renderFieldLayoutDialog({
|
||||
title: 'Inline',
|
||||
fields: [
|
||||
{ fieldname: 'territory', fieldtype: 'Data', label: 'Territory' },
|
||||
],
|
||||
})
|
||||
expect(fieldLayoutDialogs.value[0].props.fields).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('passes full tabs layout', () => {
|
||||
const tabs = [
|
||||
{
|
||||
name: 'tab1',
|
||||
label: 'Details',
|
||||
sections: [
|
||||
{
|
||||
name: 'sec1',
|
||||
label: 'Basic',
|
||||
columns: [
|
||||
{
|
||||
name: 'col1',
|
||||
fields: [
|
||||
{ fieldname: 'name', fieldtype: 'Data', label: 'Name' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'col2',
|
||||
fields: [
|
||||
{ fieldname: 'email', fieldtype: 'Data', label: 'Email' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
renderFieldLayoutDialog({ title: 'Full Layout', tabs })
|
||||
const props = fieldLayoutDialogs.value[0].props
|
||||
expect(props.tabs).toHaveLength(1)
|
||||
expect(props.tabs[0].sections[0].columns).toHaveLength(2)
|
||||
})
|
||||
|
||||
// ── onSubmit / onCancel passthrough ──
|
||||
|
||||
it('passes onSubmit function through to dialog props', () => {
|
||||
const handler = vi.fn()
|
||||
renderFieldLayoutDialog({
|
||||
title: 'Test',
|
||||
fields: [{ fieldname: 'x', fieldtype: 'Data', label: 'X' }],
|
||||
onSubmit: handler,
|
||||
})
|
||||
expect(fieldLayoutDialogs.value[0].props.onSubmit).toBe(handler)
|
||||
})
|
||||
|
||||
it('passes onCancel function through to dialog props', () => {
|
||||
const handler = vi.fn()
|
||||
renderFieldLayoutDialog({
|
||||
title: 'Test',
|
||||
fields: [{ fieldname: 'x', fieldtype: 'Data', label: 'X' }],
|
||||
onCancel: handler,
|
||||
})
|
||||
expect(fieldLayoutDialogs.value[0].props.onCancel).toBe(handler)
|
||||
})
|
||||
|
||||
it('promise resolves with null when onCancel is provided and dialog is cancelled', async () => {
|
||||
const cancelHandler = vi.fn()
|
||||
const promise = renderFieldLayoutDialog({
|
||||
title: 'Test',
|
||||
onCancel: cancelHandler,
|
||||
})
|
||||
// Simulate cancel — onResolve called with null
|
||||
fieldLayoutDialogs.value[0].props.onResolve(null)
|
||||
expect(await promise).toBeNull()
|
||||
})
|
||||
|
||||
it('promise still resolves with data even when onSubmit is provided', async () => {
|
||||
const submitHandler = vi.fn()
|
||||
const promise = renderFieldLayoutDialog({
|
||||
title: 'Test',
|
||||
onSubmit: submitHandler,
|
||||
})
|
||||
// Simulate submit — onResolve called with data
|
||||
fieldLayoutDialogs.value[0].props.onResolve({ foo: 'bar' })
|
||||
expect(await promise).toEqual({ foo: 'bar' })
|
||||
})
|
||||
|
||||
// ── Concurrent dialogs ──
|
||||
|
||||
it('supports multiple concurrent dialogs', () => {
|
||||
renderFieldLayoutDialog({ title: 'Dialog 1' })
|
||||
renderFieldLayoutDialog({ title: 'Dialog 2' })
|
||||
expect(fieldLayoutDialogs.value).toHaveLength(2)
|
||||
expect(fieldLayoutDialogs.value[0].key).not.toBe(
|
||||
fieldLayoutDialogs.value[1].key,
|
||||
)
|
||||
})
|
||||
|
||||
it('resolves concurrent dialogs independently', async () => {
|
||||
const p1 = renderFieldLayoutDialog({ title: 'D1' })
|
||||
const p2 = renderFieldLayoutDialog({ title: 'D2' })
|
||||
fieldLayoutDialogs.value[1].props.onResolve({ from: 'D2' })
|
||||
fieldLayoutDialogs.value[0].props.onResolve({ from: 'D1' })
|
||||
expect(await p1).toEqual({ from: 'D1' })
|
||||
expect(await p2).toEqual({ from: 'D2' })
|
||||
})
|
||||
|
||||
// ── Actions passthrough ──
|
||||
|
||||
it('passes custom actions array', () => {
|
||||
const actions = [
|
||||
{ label: 'Approve', variant: 'solid' },
|
||||
{ label: 'Reject', variant: 'subtle', theme: 'red' },
|
||||
{ label: 'Cancel' },
|
||||
]
|
||||
renderFieldLayoutDialog({ title: 'Review', actions })
|
||||
expect(fieldLayoutDialogs.value[0].props.actions).toEqual(actions)
|
||||
})
|
||||
|
||||
it('works without actions or onSubmit (default Submit via promise)', () => {
|
||||
renderFieldLayoutDialog({ title: 'Simple' })
|
||||
const props = fieldLayoutDialogs.value[0].props
|
||||
expect(props.actions).toBeUndefined()
|
||||
expect(props.onSubmit).toBeUndefined()
|
||||
})
|
||||
|
||||
// ── submitLabel / cancelLabel passthrough ──
|
||||
|
||||
it('passes submitLabel through to dialog props', () => {
|
||||
renderFieldLayoutDialog({
|
||||
title: 'Test',
|
||||
fields: [{ fieldname: 'x', fieldtype: 'Data', label: 'X' }],
|
||||
submitLabel: 'Save',
|
||||
})
|
||||
expect(fieldLayoutDialogs.value[0].props.submitLabel).toBe('Save')
|
||||
})
|
||||
|
||||
it('passes cancelLabel through to dialog props', () => {
|
||||
renderFieldLayoutDialog({
|
||||
title: 'Test',
|
||||
fields: [{ fieldname: 'x', fieldtype: 'Data', label: 'X' }],
|
||||
cancelLabel: 'Discard',
|
||||
})
|
||||
expect(fieldLayoutDialogs.value[0].props.cancelLabel).toBe('Discard')
|
||||
})
|
||||
|
||||
it('does not pass cancelLabel when not provided', () => {
|
||||
renderFieldLayoutDialog({ title: 'Test' })
|
||||
expect(fieldLayoutDialogs.value[0].props.cancelLabel).toBeUndefined()
|
||||
})
|
||||
|
||||
// ── onResolve is separate from user options ──
|
||||
|
||||
it('onResolve does not collide with user onSubmit', async () => {
|
||||
const submitFn = vi.fn()
|
||||
const promise = renderFieldLayoutDialog({
|
||||
title: 'Test',
|
||||
onSubmit: submitFn,
|
||||
})
|
||||
const entry = fieldLayoutDialogs.value[0]
|
||||
// onResolve is our internal hook, onSubmit is the user's
|
||||
expect(entry.props.onResolve).toBeTypeOf('function')
|
||||
expect(entry.props.onSubmit).toBe(submitFn)
|
||||
entry.props.onResolve({ data: 1 })
|
||||
expect(await promise).toEqual({ data: 1 })
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user