V13-FE-006: consolidate approved UI and contract hardening
deploy / deploy (push) Successful in 1m52s
deploy / notify (push) Successful in 1s

This commit is contained in:
2026-08-13 02:41:00 +09:00
parent d79edae546
commit 3f293d8aa8
1278 changed files with 14384 additions and 1664 deletions
@@ -0,0 +1,62 @@
import { mount } from '@vue/test-utils'
import { describe, expect, it } from 'vitest'
import KsButton from '../KsButton.vue'
import KsTextField from '../KsTextField.vue'
import { nativeUiAdapter } from '../../adapter/native'
import { uiAdapterKey } from '../../adapter/contracts'
const primeVueHarness = {
stubs: {
Button: {
props: ['label', 'type', 'loading', 'disabled'],
template: '<button :type="type" :disabled="disabled || loading" @click="$emit(\'click\', $event)">{{ label }}<span v-if="loading">…</span><slot /></button>',
},
InputText: {
props: ['modelValue', 'inputId', 'type', 'disabled', 'invalid', 'placeholder'],
template: '<input :id="inputId" :value="modelValue" :type="type" :disabled="disabled" :aria-invalid="invalid ? \'true\' : undefined" @input="$emit(\'update:modelValue\', $event.target.value)" />',
},
},
}
describe('core shared control contracts', () => {
it('keeps a loading button disabled and forwards its semantic type', () => {
const wrapper = mount(KsButton, {
props: { label: 'Save', type: 'submit', loading: true },
global: primeVueHarness,
})
const button = wrapper.get('button')
expect(button.attributes()).toMatchObject({ type: 'submit', disabled: '' })
expect(button.text()).toContain('…')
})
it('forwards activation through the vendor-neutral click event', async () => {
const wrapper = mount(KsButton, {
props: { label: 'Retry' },
global: primeVueHarness,
})
await wrapper.get('button').trigger('click')
expect(wrapper.emitted('click')).toBeTruthy()
})
it('connects text-field label, invalid state, and model updates', async () => {
const wrapper = mount(KsTextField, {
props: { modelValue: 'old', label: 'Name', inputId: 'name', error: 'Required', type: 'email' },
global: { ...primeVueHarness, provide: { [uiAdapterKey as unknown as string]: nativeUiAdapter } },
})
const input = wrapper.get('input')
expect(wrapper.get('label').attributes('for')).toBe('name')
expect(input.attributes()).toMatchObject({
id: 'name',
type: 'email',
'aria-describedby': 'name-message',
'aria-invalid': 'true',
})
expect(input.attributes('aria-required')).toBeUndefined()
await input.setValue('new@example.com')
expect(wrapper.emitted('update:modelValue')).toEqual([['new@example.com']])
})
})
@@ -0,0 +1,37 @@
import { mount } from '@vue/test-utils'
import { describe, expect, it } from 'vitest'
import KsDataGrid from '../KsDataGrid.vue'
const gridHarness = {
stubs: {
AgGridVue: {
props: ['rowData', 'columnDefs', 'rowSelection', 'loading'],
template: '<button data-test="grid" @click="$emit(\'row-clicked\', { data: rowData[0] })">select</button>',
},
},
}
describe('KsDataGrid contract', () => {
it('preserves grid defaults and native grid configuration', () => {
const rows = [{ id: 'row-1' }]
const columns = [{ field: 'id', header: 'ID' }]
const wrapper = mount(KsDataGrid, {
props: { rows, columns },
global: gridHarness,
})
expect(wrapper.find('[data-test="grid"]').exists()).toBe(true)
expect(wrapper.html()).toContain('grid')
})
it('forwards the selected row without changing its identity', async () => {
const row = { id: 'row-1', status: 'READY' }
const wrapper = mount(KsDataGrid, {
props: { rows: [row], columns: [] },
global: gridHarness,
})
await wrapper.get('[data-test="grid"]').trigger('click')
expect(wrapper.emitted('rowSelected')).toEqual([[row]])
})
})
@@ -5,7 +5,17 @@ import { nativeUiAdapter } from '../../adapter/native'
import KsMoneyField from '../KsMoneyField.vue'
import KsQuantityField from '../KsQuantityField.vue'
const globalProvide = { global: { provide: { [uiAdapterKey as unknown as string]: nativeUiAdapter } } }
const globalProvide = {
global: {
provide: { [uiAdapterKey as unknown as string]: nativeUiAdapter },
stubs: {
InputNumber: {
props: ['modelValue', 'inputId'],
template: '<input :id="inputId" :value="modelValue" @input="$emit(\'update:modelValue\', Number($event.target.value))" />',
},
},
},
}
describe('KsMoneyField', () => {
it('connects the label, shows the currency code, and emits numeric updates', async () => {
@@ -0,0 +1,15 @@
import { mount } from '@vue/test-utils'
import { describe, expect, it } from 'vitest'
import KsStatusTag from '../KsStatusTag.vue'
describe('KsStatusTag semantic and unknown contract', () => {
it('exposes text and non-colour cues for an unknown status', () => {
const wrapper = mount(KsStatusTag, {
props: { value: '상태 미등록 · VENDOR_NEW', semantic: 'warning', unknown: true },
})
const tag = wrapper.find('.ks-status-tag')
expect(tag.attributes('data-semantic')).toBe('warning')
expect(tag.attributes('aria-label')).toContain('정의되지 않음')
expect(tag.text()).toContain('?')
})
})