import { computed, defineComponent } from 'vue' import { readFileSync } from 'node:fs' import { resolve } from 'node:path' import { shallowMount } from '@vue/test-utils' import { beforeEach, describe, expect, it } from 'vitest' import QueryToolbar, { type QueryField } from './QueryToolbar.vue' import LayoutPreferenceList from './LayoutPreferenceList.vue' import { UI_PREFERENCE_SCOPE, preferenceStorageKey } from '@/shared/utils/uiLayoutPreference' const FieldStub = defineComponent({ inheritAttrs: false, props: { placeholder: String }, emits: ['keyup'], template: '', }) const ButtonStub = defineComponent({ props: { disabled: Boolean, loading: Boolean }, template: '', }) const PopoverStub = defineComponent({ template: '
' }) const fields: QueryField[] = [ { key: 'keyword', type: 'input', label: '关键词', placeholder: '关键词' }, { key: 'status', type: 'select', label: '状态', placeholder: '状态' }, { key: 'createdRange', type: 'daterange', label: '创建日期', placeholder: '创建日期' }, ] function mountToolbar(props: Record = {}) { return shallowMount(QueryToolbar, { props: { modelValue: { keyword: '', status: '', createdRange: [] }, 'onUpdate:modelValue': () => undefined, ariaLabel: '订单筛选', fields, preferenceKey: 'orders-query', ...props, }, global: { provide: { [UI_PREFERENCE_SCOPE as symbol]: computed(() => 'user-1') }, stubs: { ElInput: FieldStub, ElSelect: FieldStub, ElDatePicker: FieldStub, ElButton: ButtonStub, ElPopover: PopoverStub, }, }, }) } describe('QueryToolbar layout preferences', () => { beforeEach(() => localStorage.clear()) it('restores and updates the user field order', async () => { localStorage.setItem(preferenceStorageKey('query', 'user-1', 'orders-query'), JSON.stringify({ version: 1, order: ['status', 'keyword', 'createdRange'], hidden: [], })) const wrapper = mountToolbar() expect(wrapper.findAll('input').map((input) => input.attributes('data-placeholder'))) .toEqual(['状态', '关键词', '创建日期']) wrapper.findComponent(LayoutPreferenceList).vm.$emit('move', 'createdRange', 0) await wrapper.vm.$nextTick() expect(wrapper.findAll('input').map((input) => input.attributes('data-placeholder'))) .toEqual(['创建日期', '状态', '关键词']) }) it('keeps desktop actions grouped with the final filter when the toolbar wraps', () => { const wrapper = mountToolbar() expect(wrapper.find('.query-toolbar__tail input').attributes('data-placeholder')).toBe('创建日期') expect(wrapper.find('.query-toolbar__tail .query-toolbar__actions').exists()).toBe(true) const source = readFileSync(resolve(process.cwd(), 'src/shared/components/QueryToolbar.vue'), 'utf8') expect(source).toContain('.query-toolbar__tail { display: flex; width: auto; max-width: 100%; min-width: 0; flex: 0 1 auto; align-items: flex-end; flex-wrap: wrap; gap: 10px; }') expect(source).not.toContain('.query-toolbar__filters { display: contents; }') }) it('derives compact widths by field type while explicit business widths win', () => { const compactFields: QueryField[] = [ { key: 'status', type: 'select', label: '短状态' }, { key: 'date', type: 'date', label: '日期' }, { key: 'keyword', type: 'input', label: '长关键词' }, { key: 'range', type: 'daterange', label: '日期范围' }, { key: 'warehouse', type: 'select', label: '业务显式宽度', width: 236 }, ] const wrapper = mountToolbar({ modelValue: { status: '', date: '', keyword: '', range: [], warehouse: '' }, fields: compactFields, availableFields: compactFields, }) const styles = wrapper.findAll('.query-toolbar__field').map((field) => field.attributes('style')) expect(styles).toEqual([ '--query-field-width: 180px;', '--query-field-width: 180px;', '--query-field-width: 260px;', '--query-field-width: 300px;', '--query-field-width: 236px;', ]) }) it('keeps a short status and long keyword left aligned without stretching or desktop overflow', () => { const mixedFields: QueryField[] = [ { key: 'status', type: 'select', label: '状态' }, { key: 'keyword', type: 'input', label: '订单号/入仓码/客户名称' }, ] const wrapper = mountToolbar({ modelValue: { status: '', keyword: '' }, fields: mixedFields, availableFields: mixedFields, }) const renderedFields = wrapper.findAll('.query-toolbar__field') expect(renderedFields[0].attributes('style')).toBe('--query-field-width: 180px;') expect(renderedFields[1].attributes('style')).toBe('--query-field-width: 260px;') expect(renderedFields[1].classes()).toContain('query-toolbar__tail') expect(renderedFields[1].find('.query-toolbar__actions').exists()).toBe(true) const source = readFileSync(resolve(process.cwd(), 'src/shared/components/QueryToolbar.vue'), 'utf8') expect(source).toContain('justify-content: flex-start; flex-wrap: wrap;') expect(source).toContain('flex: 0 0 var(--query-field-width)') expect(source).toContain('overflow-x: clip') expect(source).toContain('@media (max-width: 720px)') expect(source).toContain('.query-toolbar__field { width: 100%; flex: 0 0 auto; }') expect(source).toContain('.query-toolbar__control { width: 100%; flex: 0 0 auto; }') }) it('submits the current filters once when Enter is released in a text input', async () => { const wrapper = mountToolbar({ modelValue: { keyword: 'AKR1', status: 'ACTIVE', createdRange: [] } }) const textInput = wrapper.findAll('input')[0] await textInput.trigger('keyup', { key: 'Enter' }) expect(wrapper.emitted('search')).toEqual([[ { keyword: 'AKR1', status: 'ACTIVE', createdRange: [] }, ]]) }) it('does not submit Enter from select or date controls, composition, or repeated key events', async () => { const wrapper = mountToolbar() const controls = wrapper.findAll('input') await controls[1].trigger('keyup', { key: 'Enter' }) await controls[2].trigger('keyup', { key: 'Enter' }) await controls[0].trigger('keyup', { key: 'Enter', isComposing: true }) await controls[0].trigger('keyup', { key: 'Enter', repeat: true }) expect(wrapper.emitted('search')).toBeUndefined() }) it('resets all filters once and blocks search and reset while loading', async () => { const wrapper = mountToolbar({ modelValue: { keyword: 'AKR1', status: 'ACTIVE', createdRange: ['2026-08-01', '2026-08-14'] }, }) ;(wrapper.vm as unknown as { reset: () => void }).reset() expect(wrapper.emitted('reset')).toEqual([[ { keyword: '', status: '', createdRange: [] }, ]]) expect(wrapper.emitted('search')).toEqual([[ { keyword: '', status: '', createdRange: [] }, ]]) await wrapper.setProps({ loading: true }) ;(wrapper.vm as unknown as { search: () => void; reset: () => void }).search() ;(wrapper.vm as unknown as { search: () => void; reset: () => void }).reset() expect(wrapper.emitted('search')).toHaveLength(1) expect(wrapper.emitted('reset')).toHaveLength(1) expect(wrapper.findAllComponents(ButtonStub)[0].props('loading')).toBe(true) expect(wrapper.findAllComponents(ButtonStub)[1].props('disabled')).toBe(true) }) })