20260821230656
This commit is contained in:
1 parent
2e5d532f23
commit
c43b4c8364
144 files changed
+7665
-242
No files matched your search
@@ -0,0 +1,127 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { h, nextTick, ref } from 'vue'
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import Form from '@/components/ui/form/form.vue'
|
||||
import FormItem from '@/components/ui/form/form-item.vue'
|
||||
|
||||
const model = () =>
|
||||
ref({
|
||||
code: 'a',
|
||||
name: 'b',
|
||||
note: 'c',
|
||||
})
|
||||
|
||||
function mountForm(columns = 1, extraProps = {}) {
|
||||
return mount(Form, {
|
||||
props: { model: model().value, columns, ...extraProps },
|
||||
slots: {
|
||||
default: () => [
|
||||
h(FormItem, { name: 'code', label: '编码' }, { default: () => h('input') }),
|
||||
h(FormItem, { name: 'name', label: '名称' }, { default: () => h('input') }),
|
||||
h(FormItem, { name: 'note', label: '备注', span: 2 }, { default: () => h('input') }),
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
describe('Form 多列网格布局', () => {
|
||||
it('columns=1 时保持原布局(无网格 class)', () => {
|
||||
const wrapper = mountForm()
|
||||
expect(wrapper.classes()).not.toContain('fms-form-grid')
|
||||
expect(wrapper.attributes('style')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('columns>1 时启用网格并生成列模板', () => {
|
||||
const wrapper = mountForm(2)
|
||||
expect(wrapper.classes()).toContain('fms-form-grid')
|
||||
expect(wrapper.element.style.gridTemplateColumns).toBe('repeat(2, minmax(0, 1fr))')
|
||||
})
|
||||
|
||||
it('span>1 的 FormItem 跨列', () => {
|
||||
const wrapper = mountForm(2)
|
||||
const items = wrapper.findAllComponents(FormItem)
|
||||
const noteItem = items.find((i) => i.props('name') === 'note')
|
||||
expect(noteItem.element.style.gridColumn).toBe('span 2')
|
||||
const codeItem = items.find((i) => i.props('name') === 'code')
|
||||
expect(codeItem.element.style.gridColumn).toBe('')
|
||||
})
|
||||
|
||||
it('span 超过列数时取整行', () => {
|
||||
const wrapper = mount(Form, {
|
||||
props: { model: model().value, columns: 3 },
|
||||
slots: {
|
||||
default: () => [
|
||||
h(FormItem, { name: 'code', label: '编码', span: 5 }, { default: () => h('input') }),
|
||||
],
|
||||
},
|
||||
})
|
||||
const item = wrapper.findComponent(FormItem)
|
||||
expect(item.element.style.gridColumn).toBe('span 3')
|
||||
})
|
||||
|
||||
it('maxWidth 数字按 px 处理', () => {
|
||||
const wrapper = mountForm(2, { maxWidth: 720 })
|
||||
expect(wrapper.element.style.maxWidth).toBe('720px')
|
||||
})
|
||||
|
||||
it('vertical 布局下 label 在上', () => {
|
||||
const wrapper = mountForm(2, { layout: 'vertical' })
|
||||
const item = wrapper.findComponent(FormItem)
|
||||
expect(item.classes()).toContain('fms-form-item-vertical')
|
||||
expect(item.find('.fms-form-label-vertical').exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Form 必填校验与 FormItem 错误展示', () => {
|
||||
function mountRequiredForm(rules, modelValue = { code: '' }) {
|
||||
const model = ref(modelValue)
|
||||
const wrapper = mount(Form, {
|
||||
props: { model: model.value, rules },
|
||||
slots: {
|
||||
default: () => [h(FormItem, { name: 'code', label: '编码' }, { default: () => h('input') })],
|
||||
},
|
||||
})
|
||||
return { wrapper, model }
|
||||
}
|
||||
|
||||
it('必填为空时提交:FormItem 下方显示错误并标红', async () => {
|
||||
const { wrapper } = mountRequiredForm({ code: [{ required: true, message: '请填写编码' }] })
|
||||
await wrapper.find('form').trigger('submit')
|
||||
await flushPromises()
|
||||
const item = wrapper.findComponent(FormItem)
|
||||
expect(item.classes()).toContain('fms-form-item-error')
|
||||
const help = item.find('.fms-form-item-help-error')
|
||||
expect(help.exists()).toBe(true)
|
||||
expect(help.text()).toBe('请填写编码')
|
||||
expect(wrapper.emitted('submitFailed')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('message 为函数时按校验时求值(动态文案)', async () => {
|
||||
const { wrapper } = mountRequiredForm({ code: [{ required: true, message: () => '请填写分类编码' }] })
|
||||
await wrapper.find('form').trigger('submit')
|
||||
await flushPromises()
|
||||
const help = wrapper.findComponent(FormItem).find('.fms-form-item-help-error')
|
||||
expect(help.text()).toBe('请填写分类编码')
|
||||
})
|
||||
|
||||
it('validateAll 暴露并返回带错误的字段列表', async () => {
|
||||
const { wrapper } = mountRequiredForm({ code: [{ required: true, message: '请填写编码' }] })
|
||||
const errors = await wrapper.vm.validateAll()
|
||||
expect(errors).toHaveLength(1)
|
||||
expect(errors[0].name).toBe('code')
|
||||
expect(errors[0].errors).toEqual(['请填写编码'])
|
||||
})
|
||||
|
||||
it('修正必填值后错误清除', async () => {
|
||||
const { wrapper, model } = mountRequiredForm({ code: [{ required: true, message: '请填写编码' }] })
|
||||
await wrapper.find('form').trigger('submit')
|
||||
await flushPromises()
|
||||
expect(wrapper.findComponent(FormItem).classes()).toContain('fms-form-item-error')
|
||||
model.value.code = 'abc'
|
||||
await nextTick()
|
||||
await flushPromises()
|
||||
const item = wrapper.findComponent(FormItem)
|
||||
expect(item.classes()).not.toContain('fms-form-item-error')
|
||||
expect(item.find('.fms-form-item-help-error').exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,90 @@
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import Tree from '@/components/ui/tree/tree.vue'
|
||||
|
||||
const treeData = [{ b_id: '1', b_name: '根节点', children: [{ b_id: '2', b_name: '子节点' }] }]
|
||||
const fieldNames = { key: 'b_id', title: 'b_name', children: 'children' }
|
||||
|
||||
describe('Tree 右键菜单', () => {
|
||||
it('节点右键发出 contextmenu 事件(携带 node 与 key)', async () => {
|
||||
const wrapper = mount(Tree, { props: { treeData, fieldNames } })
|
||||
await wrapper.find('.tree-node-content').trigger('contextmenu')
|
||||
const emitted = wrapper.emitted('contextmenu')
|
||||
expect(emitted).toHaveLength(1)
|
||||
expect(emitted[0][0].node.b_id).toBe('1')
|
||||
expect(emitted[0][0].key).toBe('1')
|
||||
expect(emitted[0][0].event).toBeTruthy()
|
||||
})
|
||||
|
||||
it('节点右键不会重复触发树空白处右键逻辑', async () => {
|
||||
const wrapper = mount(Tree, { props: { treeData, fieldNames } })
|
||||
await wrapper.find('.tree-node-content').trigger('contextmenu')
|
||||
expect(wrapper.emitted('contextmenu')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('空白区域右键发出 contextmenu 事件(node 为 null)', async () => {
|
||||
const wrapper = mount(Tree, { props: { treeData, fieldNames } })
|
||||
await wrapper.find('.tree').trigger('contextmenu')
|
||||
const emitted = wrapper.emitted('contextmenu')
|
||||
expect(emitted).toHaveLength(1)
|
||||
expect(emitted[0][0].node).toBeNull()
|
||||
expect(emitted[0][0].key).toBeNull()
|
||||
})
|
||||
|
||||
it('传入 contextMenu 时按节点渲染菜单(Teleport 到 body)并发出 contextMenuClick', async () => {
|
||||
const contextMenu = vi.fn((node) =>
|
||||
node
|
||||
? [
|
||||
{ key: 'add', label: '新增子项' },
|
||||
{ divider: true },
|
||||
{ key: 'delete', label: '删除', danger: true },
|
||||
]
|
||||
: [{ key: 'add-root', label: '新增顶级' }],
|
||||
)
|
||||
const wrapper = mount(Tree, { props: { treeData, fieldNames, contextMenu } })
|
||||
|
||||
// 节点右键:渲染节点菜单
|
||||
await wrapper.find('.tree-node-content').trigger('contextmenu')
|
||||
expect(contextMenu).toHaveBeenCalled()
|
||||
expect(contextMenu.mock.calls[0][0].b_id).toBe('1')
|
||||
|
||||
let menu = document.body.querySelector('.tree-context-menu')
|
||||
expect(menu).toBeTruthy()
|
||||
expect(menu.textContent).toContain('新增子项')
|
||||
const divider = menu.querySelector('.tree-context-menu__divider')
|
||||
expect(divider).toBeTruthy()
|
||||
|
||||
menu.querySelector('.tree-context-menu__item').click()
|
||||
expect(wrapper.emitted('contextMenuClick')).toHaveLength(1)
|
||||
expect(wrapper.emitted('contextMenuClick')[0][0].item.key).toBe('add')
|
||||
expect(wrapper.emitted('contextMenuClick')[0][0].node.b_id).toBe('1')
|
||||
// 点击后菜单关闭(等待离开动画结束)
|
||||
await new Promise((r) => setTimeout(r, 150))
|
||||
expect(document.body.querySelector('.tree-context-menu')).toBeNull()
|
||||
|
||||
// 空白右键:渲染顶级菜单
|
||||
await wrapper.find('.tree').trigger('contextmenu')
|
||||
menu = document.body.querySelector('.tree-context-menu')
|
||||
expect(menu.textContent).toContain('新增顶级')
|
||||
})
|
||||
|
||||
it('disabled 菜单项点击不发出 contextMenuClick', async () => {
|
||||
const contextMenu = () => [{ key: 'x', label: '禁用项', disabled: true }]
|
||||
const wrapper = mount(Tree, { props: { treeData, fieldNames, contextMenu } })
|
||||
await wrapper.find('.tree-node-content').trigger('contextmenu')
|
||||
const menu = document.body.querySelector('.tree-context-menu')
|
||||
menu.querySelector('.tree-context-menu__item').click()
|
||||
expect(wrapper.emitted('contextMenuClick')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('点击菜单外部关闭菜单', async () => {
|
||||
const contextMenu = () => [{ key: 'x', label: '项目' }]
|
||||
const wrapper = mount(Tree, { props: { treeData, fieldNames, contextMenu } })
|
||||
await wrapper.find('.tree-node-content').trigger('contextmenu')
|
||||
expect(document.body.querySelector('.tree-context-menu')).toBeTruthy()
|
||||
document.body.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }))
|
||||
await new Promise((r) => setTimeout(r, 0))
|
||||
expect(document.body.querySelector('.tree-context-menu')).toBeNull()
|
||||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,155 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { nextTick, ref } from 'vue'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia } from 'pinia'
|
||||
import MenuDetailPanel from '@/views/system/module-management/components/MenuDetailPanel.vue'
|
||||
|
||||
const moduleRow = {
|
||||
b_id: '1',
|
||||
b_code: 'test',
|
||||
b_name: '测试模块',
|
||||
b_module_type: 'data',
|
||||
b_parent_id: null,
|
||||
b_canuse: 1,
|
||||
b_xh: 10,
|
||||
}
|
||||
|
||||
vi.mock('@/services/api', () => ({
|
||||
loadDataApi: vi.fn(async (table) => {
|
||||
if (table === 's_module') return { data: [moduleRow] }
|
||||
return { data: [] }
|
||||
}),
|
||||
saveObjectApi: vi.fn(async () => ({})),
|
||||
nextIdApi: vi.fn(async () => ({ data: '999' })),
|
||||
describeApi: vi.fn(async () => ({ data: [] })),
|
||||
}))
|
||||
|
||||
import ModuleManagement from '@/views/system/module-management/index.vue'
|
||||
|
||||
describe('模块管理 tab 切换复现', () => {
|
||||
let wrapper
|
||||
|
||||
beforeEach(async () => {
|
||||
wrapper = mount(ModuleManagement, {
|
||||
global: {
|
||||
plugins: [createPinia()],
|
||||
stubs: {
|
||||
ModuleBasicPanel: { template: '<div class="stub-basic" />' },
|
||||
ModuleFieldsPanel: { template: '<div class="stub-fields" />' },
|
||||
ModuleListConfigPanel: { template: '<div class="stub-list" />' },
|
||||
ModuleEditConfigPanel: { template: '<div class="stub-edit" />' },
|
||||
ModuleQueryConfigPanel: { template: '<div class="stub-query" />' },
|
||||
ModuleAutoCodePanel: { template: '<div class="stub-autocode" />' },
|
||||
ModulePowersPanel: { template: '<div class="stub-powers" />' },
|
||||
ModuleI18nPanel: { template: '<div class="stub-i18n" />' },
|
||||
MenuDetailPanel: { template: '<div class="stub-menu" />' },
|
||||
Modal: { template: '<div class="stub-modal" />' },
|
||||
Transition: { template: '<slot />' },
|
||||
},
|
||||
},
|
||||
})
|
||||
// 等待树加载
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
})
|
||||
|
||||
it('选中模块后 section Tabs 仅一个 tab 激活', async () => {
|
||||
// 点击树节点选中模块
|
||||
const node = wrapper.find('.tree-node-content')
|
||||
expect(node.exists()).toBe(true)
|
||||
await node.trigger('click')
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
// 等待 loadModuleConfig 完成
|
||||
await new Promise((r) => setTimeout(r, 50))
|
||||
await nextTick()
|
||||
|
||||
const sectionTabs = wrapper.find('.mm-section-tabs')
|
||||
expect(sectionTabs.exists()).toBe(true)
|
||||
const activeTabs = sectionTabs.findAll('.tabs-tab-active')
|
||||
expect(activeTabs).toHaveLength(1)
|
||||
|
||||
// 点击"字段定义"
|
||||
const fieldsTab = sectionTabs
|
||||
.findAll('.tabs-tab')
|
||||
.find((t) => t.text().includes('字段定义'))
|
||||
expect(fieldsTab).toBeTruthy()
|
||||
await fieldsTab.trigger('click')
|
||||
await nextTick()
|
||||
|
||||
const activeTabs2 = sectionTabs.findAll('.tabs-tab-active')
|
||||
expect(activeTabs2).toHaveLength(1)
|
||||
expect(activeTabs2[0].text()).toContain('字段定义')
|
||||
})
|
||||
})
|
||||
|
||||
describe('MenuDetailPanel 暴露的保存校验 API', () => {
|
||||
it('validate / saveI18n / markI18nSaved 同时暴露(defineExpose 未被覆盖)', async () => {
|
||||
const menu = ref({
|
||||
b_id: '1',
|
||||
b_code: '',
|
||||
b_name: '',
|
||||
b_menu_type: 'directory',
|
||||
b_canuse: 1,
|
||||
})
|
||||
const wrapper = mount(MenuDetailPanel, {
|
||||
props: {
|
||||
menu: menu.value,
|
||||
parentName: '',
|
||||
moduleOptions: [],
|
||||
powers: [],
|
||||
loading: false,
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
IconSelect: true,
|
||||
ModulePowersPanel: true,
|
||||
ModuleI18nPanel: true,
|
||||
I18nQuickSetModal: true,
|
||||
Tabs: { template: '<div><slot /></div>' },
|
||||
TabPane: { template: '<div><slot /></div>' },
|
||||
},
|
||||
},
|
||||
})
|
||||
await nextTick()
|
||||
expect(typeof wrapper.vm.validate).toBe('function')
|
||||
expect(typeof wrapper.vm.saveI18n).toBe('function')
|
||||
expect(typeof wrapper.vm.markI18nSaved).toBe('function')
|
||||
})
|
||||
|
||||
it('validate 对空必填返回错误字段,且校验时切回基本信息 tab', async () => {
|
||||
const menu = ref({
|
||||
b_id: '1',
|
||||
b_code: '',
|
||||
b_name: '',
|
||||
b_menu_type: 'page',
|
||||
b_route: '',
|
||||
b_canuse: 1,
|
||||
})
|
||||
const wrapper = mount(MenuDetailPanel, {
|
||||
props: {
|
||||
menu: menu.value,
|
||||
parentName: '',
|
||||
moduleOptions: [],
|
||||
powers: [],
|
||||
loading: false,
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
IconSelect: true,
|
||||
ModulePowersPanel: true,
|
||||
ModuleI18nPanel: true,
|
||||
I18nQuickSetModal: true,
|
||||
Tabs: { template: '<div><slot /></div>' },
|
||||
TabPane: { template: '<div><slot /></div>' },
|
||||
},
|
||||
},
|
||||
})
|
||||
await nextTick()
|
||||
const errors = await wrapper.vm.validate()
|
||||
expect(errors.length).toBeGreaterThan(0)
|
||||
const names = errors.map((e) => e.name)
|
||||
expect(names).toContain('b_code')
|
||||
expect(names).toContain('b_name')
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user