Files
upstream-frappe-crm/frontend/tests/unit/renderFieldLayoutDialog.test.js
upstream-mirror db69de1c11
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
snapshot of https://github.com/frappe/crm.git (develop, 2026-09-18; blobs > 131072B omitted)
2026-09-18 01:36:01 +05:00

256 lines
8.2 KiB
JavaScript

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 })
})
})