feat(19.6.1-05): persist picked relation labels across FormView tab remount

- FORM_REMEMBER_LABEL inject so hosts keep picked option names
- RelationField choose() writes the label before emit
- Vitest backstops remount and tab-switch labeling

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jakub Zych
2026-10-22 14:40:35 +02:00
parent ba0501c274
commit 14baafb27c
6 changed files with 122 additions and 4 deletions

View File

@@ -13,6 +13,7 @@ import {
langFixture,
listFixture,
listSchemaFixture,
optionsFixture,
recordFixture,
rosterFormSchemaFixture,
rosterRecordFixture,
@@ -931,3 +932,31 @@ describe('field triggers on the form (D-21, UI-SPEC C6 backstops)', () => {
expect(banner.text()).toContain('Overrides are refused.')
})
})
describe('picked relation labels across tab remount', () => {
type Mounted = Awaited<ReturnType<typeof mountApp>>['wrapper']
async function tab(wrapper: Mounted, key: string): Promise<void> {
await wrapper.find(`[role="tab"][data-tab="${key}"]`).trigger('click')
await flushPromises()
}
it('keeps a picked relation label after switching tabs', async () => {
const { wrapper } = await mountApp(
'/acme/demo/widgets/1',
routes({ [`GET ${BASE}/fields/maker/options`]: { body: optionsFixture.makerField } }),
)
const maker = () => wrapper.find('[data-field="maker"] [role="combobox"]')
expect((maker().element as HTMLInputElement).value).toBe('Bolt & Co')
await maker().trigger('click')
await flushPromises()
await wrapper.find('[data-field="maker"] [role="option"][data-value="1"]').trigger('mousedown')
await flushPromises()
await tab(wrapper, 'tab:Details')
await tab(wrapper, 'default')
expect((maker().element as HTMLInputElement).value).toBe('Acme Works')
expect((maker().element as HTMLInputElement).value).not.toBe('1')
})
})

View File

@@ -1,7 +1,9 @@
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { enableAutoUnmount, flushPromises, mount, type VueWrapper } from '@vue/test-utils'
import { setBundle } from '../../src/app/i18n'
import type { RelationOption } from '../../src/api/types'
import RelationField from '../../src/components/form/fields/RelationField.vue'
import { FORM_REMEMBER_LABEL } from '../../src/components/form/formContext'
import { formField, langFixture, optionsFixture, recordFixture } from '../fixtures/typed'
import { API, keydown, mockApi, queryOf, requestsTo, resetState, wait, type Route } from '../helpers'
@@ -243,6 +245,48 @@ describe('multiple relation (D-18)', () => {
})
})
describe('remembered relation labels across remount', () => {
it('shows a picked label after remount when the form remembered it', async () => {
mockApi(routes())
const previous: RelationOption[] = [{ value: 2, label: 'Bolt & Co' }]
const bag: RelationOption[] = []
const wrapper = mount(RelationField, {
props: {
field: formField('maker'),
modelValue: 2,
controlId: 'r-maker',
labels: previous,
source,
},
global: {
provide: {
[FORM_REMEMBER_LABEL as symbol]: (_field: string, option: RelationOption) => {
bag.push(option)
},
},
},
attachTo: document.body,
})
await openList(wrapper)
await wrapper.find('[role="option"][data-value="1"]').trigger('mousedown')
expect(bag).toEqual([expect.objectContaining({ value: 1, label: 'Acme Works' })])
wrapper.unmount()
const remounted = mount(RelationField, {
props: {
field: formField('maker'),
modelValue: 1,
controlId: 'r-maker',
labels: [...previous, ...bag],
source,
},
attachTo: document.body,
})
expect((combobox(remounted).element as HTMLInputElement).value).toBe('Acme Works')
expect((combobox(remounted).element as HTMLInputElement).value).not.toBe('1')
})
})
describe('locked relation options (UI-SPEC S6, D-07)', () => {
const NOTE = 'Items marked with a lock need an additional permission to change.'
const strings = {