/** * 查询配置面板回写语义(module-management): * - 草稿与 query JSON 不是一一映射(缺省 component、老结构 conditions 转 SQL、已废弃的 * filterGroups / defaultViewCode 被清理),只加载(含父级重载换新对象)不得回写, * 否则草稿与库中基线不一致 → 切树节点误报「未保存」,并静默改写库中数据; * - 用户真实编辑(改分组 / 改页签条件 SQL)必须回写,且回写为 schemaVersion 3 形态。 */ import { beforeAll, beforeEach, describe, expect, it } from 'vitest' import { mount } from '@vue/test-utils' import { createPinia, setActivePinia } from 'pinia' import { nextTick } from 'vue' import ModuleQueryConfigPanel from '@/views/module/module-management/ModuleQueryConfigPanel.vue' import FmsTable from '@/components/fms-table/FmsTable.vue' // jsdom 无布局:StkTable 在 clientWidth 为 0 时启用虚拟列裁剪,补成宽容器 beforeAll(() => { Object.defineProperty(HTMLElement.prototype, 'clientWidth', { configurable: true, get() { return 5000 }, }) }) const fields = [ { b_field: 'b_status', b_name: '状态', b_type: 'select', b_canuse: 1 }, { b_field: 'b_amount', b_name: '金额', b_type: 'money', b_canuse: 1 }, ] /** 库中历史查询配置(schemaVersion 2:条件为结构化 conditions、默认页签在顶层、还有 filterGroups) */ const legacySchema = { schemaVersion: 2, defaultViewCode: 'pending', views: [ { code: 'pending', label: '待处理', order: 10, showCount: true, conditions: [{ field: 'b_status', operator: 'eq', value: '1' }], }, ], keywordSearch: { placeholder: '', trigger: 'enter_or_debounce', fields: [] }, quickFilters: [], filterGroups: [{ code: 'basic', label: '基础', order: 10, fields: ['b_status'] }], queryFields: { b_status: { component: 'select', operator: 'eq' } }, } async function mountPanel(modelValue) { const pinia = createPinia() pinia.install({ provide() {}, config: { globalProperties: {} } }) setActivePinia(pinia) const wrapper = mount(ModuleQueryConfigPanel, { props: { moduleId: 'demo', fields, modelValue, loading: false }, global: { plugins: [pinia] }, attachTo: document.body, }) await nextTick() return wrapper } const lastSchema = (wrapper) => { const events = wrapper.emitted('update:modelValue') || [] return events.length ? events[events.length - 1][0] : null } /** 「工作队列页签」分节内的页签表格(四个分节同页平铺,按分节类名定位) */ function viewsTable(wrapper) { return wrapper.find('.mm-qdesign__section--views').findComponent(FmsTable) } beforeEach(() => { document.body.innerHTML = '' }) describe('查询配置面板:加载不回写、编辑才回写', () => { it('四个分节同页平铺(不用页签分组),各自带一张定高表格', async () => { const wrapper = await mountPanel(legacySchema) // 页签栏与页签面板都不应存在 expect(wrapper.findAll('.tabs-tab').length, '不应再有页签').toBe(0) expect(wrapper.findAll('.tabs-pane').length, '不应再有页签面板').toBe(0) const sections = wrapper.findAll('.mm-qdesign__section') expect(sections.map((section) => section.find('.mm-qdesign__section-title strong').text())).toEqual( ['更多条件字段', '工作队列页签', '关键词字段', '快捷筛选字段'], ) // 三个带表格的分节:表格容器有按行数算出的显式高度(不再靠 flex 撑满整屏) for (const modifier of ['--fields', '--views', '--quick']) { const tableBox = wrapper.find(`.mm-qdesign__section${modifier} .mm-qdesign__table`) expect(tableBox.exists(), `${modifier} 分节应有表格容器`).toBe(true) expect(tableBox.attributes('style'), `${modifier} 分节表格应有显式高度`).toMatch( /height:\s*\d+px/, ) expect(tableBox.findComponent(FmsTable).exists(), `${modifier} 分节应渲染表格`).toBe(true) } wrapper.unmount() }) it('仅加载(含父级重载换新对象)不回写', async () => { const wrapper = await mountPanel(legacySchema) expect(lastSchema(wrapper)).toBe(null) // 模拟父级重载:同一份历史 JSON 以新引用下发 await wrapper.setProps({ modelValue: JSON.parse(JSON.stringify(legacySchema)) }) await nextTick() expect(lastSchema(wrapper)).toBe(null) wrapper.unmount() }) it('老结构条件在草稿里转成 SQL 展示,但加载本身不回写', async () => { const wrapper = await mountPanel(legacySchema) const rows = viewsTable(wrapper).props('dataSource') expect(rows[0].conditionSql).toBe("[b_status] = N'1'") expect(rows[0].isDefault).toBe(true) expect(lastSchema(wrapper)).toBe(null) wrapper.unmount() }) it('改分组后回写为 schemaVersion 3(已废弃键被清理)', async () => { const wrapper = await mountPanel(legacySchema) const table = viewsTable(wrapper) const rows = table.props('dataSource') rows[0].group = '状态' table.vm.$emit('change', rows[0], { dataIndex: 'group' }) await nextTick() const next = lastSchema(wrapper) expect(next).not.toBe(null) expect(next.schemaVersion).toBe(3) expect(next.views[0]).toMatchObject({ code: 'pending', group: '状态', isDefault: true, conditionSql: "[b_status] = N'1'", }) expect(next.filterGroups).toBeUndefined() expect(next.defaultViewCode).toBeUndefined() // 关键词只保留可搜索字段:旧的占位文案 / 触发方式随改写清理 expect(next.keywordSearch).toEqual({ fields: [] }) wrapper.unmount() }) it('页签条件改为 SQL 后回写', async () => { const wrapper = await mountPanel(legacySchema) viewsTable(wrapper) await wrapper.find('button.mm-qdesign__sql-cell').trigger('click') await nextTick() await wrapper.find('textarea').setValue("b_status = N'2'") const buttons = wrapper.findAll('.fms-modal-footer button') await buttons[buttons.length - 1].trigger('click') await nextTick() const next = lastSchema(wrapper) expect(next).not.toBe(null) expect(next.views[0].conditionSql).toBe("b_status = N'2'") wrapper.unmount() }) it('条件 SQL 含分号时不回写(拦截手误)', async () => { const wrapper = await mountPanel(legacySchema) viewsTable(wrapper) await wrapper.find('button.mm-qdesign__sql-cell').trigger('click') await nextTick() await wrapper.find('textarea').setValue("b_status = N'2'; DELETE FROM x") const buttons = wrapper.findAll('.fms-modal-footer button') await buttons[buttons.length - 1].trigger('click') await nextTick() expect(lastSchema(wrapper)).toBe(null) wrapper.unmount() }) })