Files
summercms/admin/tests/form/PartialField.test.ts
Jakub Zych 11c4e5466b test(10.1-04): bring the SPA extension point under Vitest
- WidgetField: skeleton and aria-busy, the 5000 ms whenDefined timeout,
  script failure, attributes only, fill-values and locale sync, one POST
  while busy, fill-key-only patching, danger toasts, unmount
- PartialHost and partialNodes: exhaustive tag, attribute and URL
  allowlist, depth and node caps, skeleton sizes, empty, failure,
  busy refetch and ?id= only with a record
- PartialField, ListToolbar, ListView, registry, formState, FormField and
  FormView: group labels, registered toolbar actions, header refetch
  rules, form context provision and asset loading
2026-09-29 02:59:13 +02:00

94 lines
4.3 KiB
TypeScript

// PartialField (D-09; UI-SPEC S2): a `type: partial` form row renders its
// controller partial on create (no id) and update (?id=) inside a group named
// by the row's label span; without a label there is no label row and no
// aria-labelledby. Mounted through FormField, as FormGrid renders it.
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { flushPromises, mount } from '@vue/test-utils'
import FormField from '../../src/components/form/FormField.vue'
import PartialField from '../../src/components/form/fields/PartialField.vue'
import PartialHost from '../../src/components/partial/PartialHost.vue'
import type { FormField as Field } from '../../src/api/types'
import { clone, extensionFormSchemaFixture } from '../fixtures/typed'
import { API, mockApi, queryOf, requestsTo, resetState } from '../helpers'
const SOURCE = { vendor: 'acme', plugin: 'demo', controller: 'widgets' }
const SUMMARY = `${API}/acme/demo/widgets/partials/summary`
const reply = { body: { data: { nodes: [{ tag: 'p', children: [{ text: 'Summary of the widget' }] }] }, meta: {} } }
function summaryField(change: Partial<Field> = {}): Field {
const field = clone(extensionFormSchemaFixture).data.fields.find((item) => item.name === 'summary')
if (!field) {
throw new Error('fixture summary field missing')
}
return { ...field, ...change }
}
function mountRow(field: Field, recordId: number | null, source: typeof SOURCE | null = SOURCE) {
return mount(FormField, { props: { field, modelValue: undefined, source, recordId, idPrefix: 'f' } })
}
beforeEach(() => {
resetState()
})
afterEach(() => {
document.body.innerHTML = ''
})
describe('partial form row', () => {
it('fetches without an id on create and with the record id on update', async () => {
const calls = mockApi({ [`GET ${SUMMARY}`]: reply })
mountRow(summaryField(), null)
mountRow(summaryField(), 3)
await flushPromises()
const [create, update] = requestsTo(calls, 'GET', SUMMARY)
expect(queryOf(create!).has('id')).toBe(false)
expect(queryOf(update!).get('id')).toBe('3')
})
it('is a group named by the row label span, not a <label>', async () => {
mockApi({ [`GET ${SUMMARY}`]: reply })
const wrapper = mountRow(summaryField({ comment: 'Read only', required: true }), 3)
await flushPromises()
const group = wrapper.find('[data-partial-field]')
expect(group.attributes('role')).toBe('group')
expect(group.attributes('id')).toBe('f-summary')
expect(group.attributes('aria-labelledby')).toBe('f-summary-label')
expect(group.attributes('aria-describedby')).toBe('f-summary-comment')
const label = wrapper.find('#f-summary-label')
expect(label.element.tagName).toBe('SPAN')
expect(label.text()).toBe('Summary *')
expect(label.find('[aria-hidden="true"]').text()).toBe('*')
expect(wrapper.find('label').exists()).toBe(false)
expect(group.find('[data-partial="field"]').text()).toBe('Summary of the widget')
expect(wrapper.findComponent(PartialHost).props()).toMatchObject({ variant: 'field', name: 'summary', recordId: 3 })
})
it('has no label row and no aria-labelledby without a label', async () => {
mockApi({ [`GET ${SUMMARY}`]: reply })
const wrapper = mountRow(summaryField({ label: undefined }), null)
await flushPromises()
expect(wrapper.find('#f-summary-label').exists()).toBe(false)
expect(wrapper.find('label').exists()).toBe(false)
expect(wrapper.find('[data-partial-field]').attributes('aria-labelledby')).toBeUndefined()
})
it('falls back to the field name when the path is empty', async () => {
const calls = mockApi({ [`GET ${API}/acme/demo/widgets/partials/notes`]: reply })
mountRow(summaryField({ name: 'notes', path: '' }), null)
await flushPromises()
expect(requestsTo(calls, 'GET', `${API}/acme/demo/widgets/partials/notes`)).toHaveLength(1)
})
it('shows the failure box and calls nothing without a controller (settings forms)', async () => {
const calls = mockApi({})
const wrapper = mount(PartialField, {
props: { field: summaryField(), modelValue: undefined, controlId: 'f-summary', source: null, recordId: null },
})
await flushPromises()
expect(wrapper.find('[data-partial-failed]').attributes('role')).toBe('alert')
expect(wrapper.findComponent(PartialHost).exists()).toBe(false)
expect(calls).toHaveLength(0)
})
})