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
256 lines
8.2 KiB
JavaScript
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 })
|
|
})
|
|
})
|