Files
workspace/code/fms/fms-vue/tests/unit/fms-module-list-page.spec.js
T
2026-09-23 23:05:01 +08:00

373 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* FmsModuleListPage 冒烟测试(V2:元数据来自 s_field + s_module_schema JSON)。
* 验证:按模块编码加载字段与 view/query 界面配置后,能正常渲染数据列、无配置错误。
*/
import { flushPromises, mount } from '@vue/test-utils'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createPinia, setActivePinia } from 'pinia'
import { nextTick } from 'vue'
import { registerFeature, useAreaSelection } from 'stk-table-vue'
registerFeature(useAreaSelection)
vi.mock('vue-router', () => ({
useRoute: () => ({ path: '/module/i18n-type', meta: {} }),
}))
// 查询界面配置 JSON(s_module_schema.query,schemaVersion 3):按模块管理查询配置面板的真实产物组织
const querySchema = {
schemaVersion: 3,
views: [
{ code: 'all', label: '全部', order: 10, isDefault: true },
{
code: 'named',
label: '有名称',
order: 20,
showCount: true,
conditionSql: 'b_name IS NOT NULL',
},
],
keywordSearch: { fields: ['b_name'] },
quickFilters: [{ field: 'b_name', order: 10 }],
queryFields: { b_name: { component: 'input', operator: 'like' } },
}
let withSummary = false
vi.mock('@/services/exportService', () => ({
exportListApi: vi.fn(async () => 'data.xlsx'),
loadTotalsApi: vi.fn(async () => ({ data: { b_amount: 120.5 } })),
countExportApi: vi.fn(async () => ({ data: { total: 5 } })),
}))
vi.mock('@/services/api', () => ({
loadDataApi: vi.fn(async (viewName) => {
if (viewName === 's_module') {
return {
data: [
{
b_id: 's_i18n_type',
b_module_type: 'data',
b_canuse: 1,
b_view_table: 's_i18n_type',
b_save_table: 's_i18n_type',
b_key_field: 'b_id',
},
],
}
}
if (viewName === 's_field') {
return {
data: [
{ b_field: 'b_id', b_name: '语言编码', b_type: 'input', b_canuse: 1, b_i18n: null },
{ b_field: 'b_name', b_name: '语言名称', b_type: 'input', b_canuse: 1, b_i18n: null },
{ b_field: 'b_amount', b_name: '金额', b_type: 'money', b_canuse: 1 },
],
}
}
if (viewName === 's_module_schema') {
return {
data: [
{
b_module_id: 's_i18n_type',
b_schema_type: 'view',
b_schema_json: JSON.stringify({
schemaVersion: 1,
columns: [
{ field: 'b_id', width: 120 },
{ field: 'b_name', width: 180 },
...(withSummary ? [{ field: 'b_amount', width: 120, summary: 'sum' }] : []),
],
}),
},
{
b_module_id: 's_i18n_type',
b_schema_type: 'query',
b_schema_json: JSON.stringify(querySchema),
},
],
}
}
return { data: [] }
}),
pageDataApi: vi.fn(async () => ({
data: { rows: [{ b_id: 'zh-CN', b_name: '简体中文' }], total: 1 },
})),
saveObjectApi: vi.fn(async () => ({})),
nextIdApi: vi.fn(async () => ({ data: '1' })),
loadDataBySqlApi: vi.fn(async () => ({ data: [] })),
}))
import FmsModuleListPage from '@/components/fms-module-list/FmsModuleListPage.vue'
import { pageDataApi } from '@/services/api'
import { exportListApi, loadTotalsApi } from '@/services/exportService'
import { Pagination } from '@/components/ui'
beforeEach(() => {
setActivePinia(createPinia())
pageDataApi.mockClear()
exportListApi.mockClear()
loadTotalsApi.mockClear()
withSummary = false
Object.defineProperty(HTMLElement.prototype, 'clientWidth', {
configurable: true,
get() {
return 5000
},
})
})
/** 面板内按钮:文案已精简(条件 / 组 / 清空 / 应用 / 取消),按面板作用域 + 精确文案定位 */
function panelButtonOf(wrapper, text) {
return wrapper.findAll('.filter-drawer button').find((button) => button.text().trim() === text)
}
/** 最近一次分页请求的条件 SQL */
const lastCondition = () =>
pageDataApi.mock.calls[pageDataApi.mock.calls.length - 1][0].searchCondition
describe('FmsModuleListPage 冒烟', () => {
it('配置合计后底层表格显示当页及总合计,翻页导出使用当前区间', async () => {
withSummary = true
pageDataApi.mockResolvedValueOnce({
data: {
rows: [
{ b_id: '1', b_amount: 10.2 },
{ b_id: '2', b_amount: 20.3 },
],
total: 5,
},
})
const wrapper = mount(FmsModuleListPage, {
props: { dataCode: 's_i18n_type', pageSize: 2, fixedSearchCondition: 'b_canuse = 1' },
attachTo: document.body,
})
await flushPromises()
expect(wrapper.find('.stk-footer').text()).toContain('该页合计')
expect(wrapper.find('.stk-footer').text()).toContain('30.50')
expect(wrapper.find('.stk-footer').text()).toContain('总合计')
expect(wrapper.find('.stk-footer').text()).toContain('120.50')
expect(loadTotalsApi).toHaveBeenCalledWith(
expect.objectContaining({ searchCondition: '(b_canuse = 1)' }),
)
pageDataApi.mockResolvedValueOnce({
data: {
rows: [
{ b_id: '3', b_amount: 40 },
{ b_id: '4', b_amount: 25 },
],
total: 5,
},
})
wrapper.findComponent(Pagination).vm.$emit('change', 2, 2)
await flushPromises()
expect(wrapper.find('.stk-footer').text()).toContain('65.00')
await wrapper.find('button[title="导出当页数据;勾选行则只导出勾选"]').trigger('click')
await flushPromises()
expect(exportListApi).toHaveBeenLastCalledWith(
expect.objectContaining({
rangeStart: 3,
rangeEnd: 4,
columns: ['b_id', 'b_name', 'b_amount'],
searchCondition: '(b_canuse = 1)',
orderBy: 'b_id DESC',
}),
)
wrapper.unmount()
})
it('勾选导出不叠加当前页范围,空页不发送非法导出区间', async () => {
const wrapper = mount(FmsModuleListPage, {
props: { dataCode: 's_i18n_type', rowSelection: { selectedRowKeys: ['a', 'b'] } },
attachTo: document.body,
})
await flushPromises()
const button = () => wrapper.find('button[title="导出当页数据;勾选行则只导出勾选"]')
await button().trigger('click')
await flushPromises()
expect(exportListApi).toHaveBeenLastCalledWith(expect.objectContaining({ rowKeys: ['a', 'b'] }))
expect(exportListApi.mock.calls.at(-1)[0]).not.toHaveProperty('rangeStart')
await wrapper.setProps({ rowSelection: null })
pageDataApi.mockResolvedValueOnce({ data: { rows: [], total: 0 } })
await wrapper.vm.reload()
await flushPromises()
exportListApi.mockClear()
await button().trigger('click')
await flushPromises()
expect(exportListApi).not.toHaveBeenCalled()
expect(loadTotalsApi).not.toHaveBeenCalled()
wrapper.unmount()
})
it('按 data-code 加载字段与界面配置后渲染列,无配置错误', async () => {
const wrapper = mount(FmsModuleListPage, {
props: { dataCode: 's_i18n_type' },
attachTo: document.body,
})
await new Promise((r) => setTimeout(r, 60))
await nextTick()
expect(wrapper.find('.module-config-error').exists()).toBe(false)
const text = wrapper.text()
expect(text).toContain('语言编码')
expect(text).toContain('语言名称')
// 与 dashboard 同布局:上查询卡 + 下表格卡两张白卡,工具条在表格卡内,分页条在卡片底部
expect(wrapper.findAll('.fms-module-card')).toHaveLength(2)
expect(wrapper.find('.table-card .list-toolbar').exists()).toBe(true)
expect(wrapper.find('.module-main-column > .fms-module-card').exists()).toBe(true)
expect(wrapper.find('.list-footer .footer-total').text()).toContain('共')
wrapper.unmount()
})
it('高级查询面板配置条件后,应用查询把生成的条件带到分页查询', async () => {
const wrapper = mount(FmsModuleListPage, {
props: { dataCode: 's_i18n_type' },
attachTo: document.body,
})
await new Promise((r) => setTimeout(r, 60))
await nextTick()
const buttonOf = (text) =>
wrapper.findAll('button').find((button) => button.text().includes(text))
await buttonOf('高级查询').trigger('click')
await nextTick()
expect(wrapper.find('.filter-drawer').exists()).toBe(true)
await panelButtonOf(wrapper, '条件').trigger('click')
await nextTick()
// 条件行结构:字段 / 操作符 / 值 三个独立控件,移除按钮在行尾
expect(wrapper.find('.filter-row__field').exists()).toBe(true)
expect(wrapper.find('.filter-row__operator').exists()).toBe(true)
expect(wrapper.find('.filter-row__value').exists()).toBe(true)
expect(wrapper.find('.filter-row__remove').exists()).toBe(true)
// 新行字段未选:操作符与值为禁用态(禁用占位 Input),不触发「值换行」的区间修饰类
expect(wrapper.find('.filter-row__operator').classes('select-disabled')).toBe(true)
expect(wrapper.find('.filter-row__value input').attributes('disabled')).toBeDefined()
expect(wrapper.find('.filter-drawer__head').exists()).toBe(true)
expect(wrapper.find('.module-main-column > .filter-drawer').exists()).toBe(false)
expect(wrapper.find('.filter-row--wide-value').exists()).toBe(false)
// 选中字段后操作符与值激活,再填搜索值
await wrapper.findComponent('.filter-row__field').vm.$emit('update:modelValue', 'b_name')
await nextTick()
expect(wrapper.find('.filter-row__operator').classes('select-disabled')).toBe(false)
await wrapper.find('.filter-row__value input').setValue('abc')
await panelButtonOf(wrapper, '应用').trigger('click')
await new Promise((r) => setTimeout(r, 20))
const lastCall = pageDataApi.mock.calls[pageDataApi.mock.calls.length - 1][0]
expect(lastCall.searchCondition).toContain("[b_name] LIKE N'%abc%'")
wrapper.unmount()
})
it('查询配置未写 quick 列表时,配置的条件仍显示在常用区', async () => {
const original = querySchema.quick
querySchema.quick = []
const wrapper = mount(FmsModuleListPage, {
props: { dataCode: 's_i18n_type' },
attachTo: document.body,
})
await new Promise((r) => setTimeout(r, 60))
await nextTick()
// 模块只配置了 b_name 一个查询条件,未写 quick 列表时它必须出现在常用区
expect(wrapper.findAll('.query-field')).toHaveLength(1)
// 条件字段已改用 FormItem 渲染:label 由组件库输出
expect(wrapper.find('.query-field .fms-form-item-label').text()).toBe('语言名称')
querySchema.quick = original
wrapper.unmount()
})
it('条件组是灰底卡片分支:药丸在卡片内、整组删除在右上角,组内条件是它的子层', async () => {
const wrapper = mount(FmsModuleListPage, {
props: { dataCode: 's_i18n_type' },
attachTo: document.body,
})
await new Promise((r) => setTimeout(r, 60))
await nextTick()
const buttons = () => wrapper.findAll('button')
await buttons()
.find((button) => button.text().includes('高级查询'))
.trigger('click')
await nextTick()
// 面板作用域内找「组」按钮,避免与页面其他按钮文案撞车
const panel = wrapper.find('.filter-drawer')
await panel
.findAll('button')
.find((button) => button.text().includes('组'))
.trigger('click')
await nextTick()
const group = wrapper.find('.filter-group')
expect(group.exists()).toBe(true)
// 组是浅底圆角块:底栏两端是「添加条件」与「删除组」文字按钮,体内是条件行
expect(group.find('.filter-group__remove').text()).toBe('删除组')
expect(group.find('.filter-group__add').exists()).toBe(true)
expect(group.findAll('.filter-row')).toHaveLength(1)
wrapper.unmount()
})
it('常用查询与高级查询互斥:切换模式只保留当前模式的条件', async () => {
const wrapper = mount(FmsModuleListPage, {
props: { dataCode: 's_i18n_type' },
attachTo: document.body,
})
await new Promise((r) => setTimeout(r, 60))
await nextTick()
const buttons = () => wrapper.findAll('button')
const buttonOf = (text) => buttons().find((button) => button.text().trim() === text)
// 1) 常用查询生效
await wrapper.find('.query-field input').setValue('abc')
await buttonOf('查询').trigger('click')
await new Promise((r) => setTimeout(r, 20))
expect(lastCondition()).toContain("[b_name] LIKE N'%abc%'")
// 2) 切到高级查询并应用:常用条件不再生效
await buttons()
.find((button) => button.text().includes('高级查询'))
.trigger('click')
await nextTick()
await panelButtonOf(wrapper, '条件').trigger('click')
await nextTick()
await wrapper.findComponent('.filter-row__field').vm.$emit('update:modelValue', 'b_name')
await nextTick()
await wrapper.find('.filter-row__value input').setValue('xyz')
await panelButtonOf(wrapper, '应用').trigger('click')
await new Promise((r) => setTimeout(r, 20))
expect(lastCondition()).toContain("[b_name] LIKE N'%xyz%'")
expect(lastCondition()).not.toContain('abc')
// 3) 高级生效时普通表单隐藏、改为条件预览
expect(wrapper.find('.advanced-active-bar').exists()).toBe(true)
expect(wrapper.find('.fms-query-toolbar').exists()).toBe(false)
// 3.1) 重新打开抽屉(编辑)再「取消」:只关抽屉,高级查询仍生效(不能借取消退出)
await buttons()
.find((button) => button.text().trim() === '编辑')
.trigger('click')
await nextTick()
expect(wrapper.find('.filter-drawer').exists()).toBe(true)
await panelButtonOf(wrapper, '取消').trigger('click')
await new Promise((r) => setTimeout(r, 20))
expect(wrapper.find('.filter-drawer').exists()).toBe(false)
expect(wrapper.find('.advanced-active-bar').exists()).toBe(true)
expect(lastCondition()).toContain("[b_name] LIKE N'%xyz%'")
expect(lastCondition()).not.toContain('abc')
// 3.2) 点「回到普通查询」:清空高级条件并恢复常用条件
await buttons()
.find((button) => button.text().includes('回到普通查询'))
.trigger('click')
await new Promise((r) => setTimeout(r, 20))
expect(lastCondition()).toContain("[b_name] LIKE N'%abc%'")
expect(lastCondition()).not.toContain('xyz')
// 高级草稿已清空、普通表单重新显示
expect(wrapper.find('.fms-query-toolbar').exists()).toBe(true)
wrapper.unmount()
})
})