20260919211702

This commit is contained in:
oneao committed 2026-09-19 21:17:02 +08:00
1 parent f56e70b050
commit f4bfc1ddfc
97 files changed
+6059 -1712

No files matched your search

@@ -0,0 +1,394 @@
/**
* 栏位设置(列宽/顺序)冒烟测试:模拟真实操作链路。
*
* 覆盖:
* 1. 未改动 → 操作条不出现(不占内容区);
* 2. 拖列宽 → 操作条出现,撤销后消失且不落库;
* 3. 保存为个人 → 只写 s_user_module_pref(b_schema_type='view'),且只存相对通用的增量;
* 4. 保存为通用 → 写 s_module_schema(且只对管理员可见按钮);
* 5. 个人层加载 → 合并进列配置,且不得引入通用没有的字段。
*/
import { flushPromises, mount } from '@vue/test-utils'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createPinia, setActivePinia } from 'pinia'
import { registerFeature, useAreaSelection } from 'stk-table-vue'
registerFeature(useAreaSelection)
// vi.mock 工厂被提升到 import 之上,工厂内引用的变量必须同样提升
const EMPTY = vi.hoisted(() => Object.freeze({}))
vi.mock('vue-router', () => ({
useRoute: () => ({ path: '/module/i18n-type', meta: EMPTY }),
}))
vi.mock('@/services/exportService', () => ({
exportListApi: vi.fn(async () => 'data.xlsx'),
loadTotalsApi: vi.fn(async () => ({ data: EMPTY })),
countExportApi: vi.fn(async () => ({ data: EMPTY })),
}))
/** 通用栏位:b_id 120 / b_name 180。测试按需注入个人层偏好。 */
const GLOBAL_VIEW = {
schemaVersion: 1,
columns: [
{ field: 'b_id', width: 120 },
{ field: 'b_name', width: 180 },
],
}
let personalColumnPref = null
let currentAccount = 'u001'
const 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 },
],
}
}
if (viewName === 's_module_schema') {
return {
data: [
{
b_module_id: 's_i18n_type',
b_schema_type: 'view',
b_schema_json: JSON.stringify(GLOBAL_VIEW),
},
],
}
}
if (viewName === 's_user_module_pref') {
if (!personalColumnPref) return { data: [] }
return {
data: [
{
b_user_id: currentAccount,
b_module_id: 's_i18n_type',
b_schema_type: 'view',
b_schema_json: JSON.stringify(personalColumnPref),
},
],
}
}
return { data: [] }
})
const saveObjectApi = vi.fn(async () => ({}))
vi.mock('@/services/api', () => ({
loadDataApi: (...args) => loadDataApi(...args),
saveObjectApi: (...args) => saveObjectApi(...args),
pageDataApi: vi.fn(async () => ({ data: { rows: [{ b_id: 'zh-CN' }], total: 1 } })),
nextIdApi: vi.fn(async () => ({ data: '1' })),
loadDataBySqlApi: vi.fn(async () => ({ data: [] })),
}))
vi.mock('@/stores/auth', async () => {
const actual = await vi.importActual('@/stores/auth')
return {
...actual,
useAuthStore: () => ({
userInfo: { account: currentAccount },
loginInfo: { user: { account: currentAccount, b_id: currentAccount } },
}),
}
})
// 权限 store 决定「保存为通用」按钮是否渲染:默认按非管理员(普通用户)跑
let isG3soft = false
vi.mock('@/stores/permissions', () => ({
usePermissionStore: () => ({
get isG3soft() {
return isG3soft
},
}),
}))
import FmsModuleListPage from '@/components/fms-module-list/FmsModuleListPage.vue'
import FmsModuleTable from '@/components/fms-module-list/FmsModuleTable.vue'
import FmsTable from '@/components/fms-table/FmsTable.vue'
beforeEach(() => {
setActivePinia(createPinia())
loadDataApi.mockClear()
saveObjectApi.mockClear()
personalColumnPref = null
currentAccount = 'u001'
isG3soft = false
Object.defineProperty(HTMLElement.prototype, 'clientWidth', {
configurable: true,
get: () => 5000,
})
})
async function mountPage() {
const wrapper = mount(FmsModuleListPage, {
props: { dataCode: 's_i18n_type' },
attachTo: document.body,
})
await flushPromises()
return wrapper
}
/**
* 模拟用户拖拽列宽:FmsTable 收到后会 emit('update:columns', 渲染列数组),
* 其中每项保留 dataIndex/key,只有 width 变化——按这个真实形态构造,别只传 b_field/b_width,
* 否则 FmsTable.onColumnsUpdate 按 key/dataIndex 匹配不到原始列,回传结果会变成空数组。
*/
async function resizeColumn(wrapper, dataIndex, width) {
const table = wrapper.findComponent(FmsModuleTable)
const fmsTable = table.findComponent(FmsTable)
const rendered = fmsTable
.props('columns')
.map((column) => (column.dataIndex === dataIndex ? { ...column, width } : { ...column }))
fmsTable.vm.$emit('update:columns', rendered)
await flushPromises()
}
/** 操作条内的按钮 */
const barButton = (wrapper, text) =>
wrapper.findAll('.list-column-bar button').find((button) => button.text().trim() === text)
describe('栏位设置冒烟', () => {
it('未改动时不出现操作条,也不渲染栏位相关按钮', async () => {
const wrapper = await mountPage()
expect(wrapper.find('.list-column-bar').exists()).toBe(false)
wrapper.unmount()
})
it('下拉菜单不再包含栏位相关项(已迁到操作条)', async () => {
const wrapper = await mountPage()
const menuText = wrapper.find('.list-toolbar__group--tools').text()
expect(menuText).not.toContain('恢复栏位设置')
expect(menuText).not.toContain('保存栏位设置')
wrapper.unmount()
})
it('拖列宽后操作条出现;撤销后消失且不落库', async () => {
const wrapper = await mountPage()
await resizeColumn(wrapper, 'b_name', 320)
expect(wrapper.find('.list-column-bar').exists()).toBe(true)
expect(wrapper.find('.list-column-bar').text()).toContain('列宽或顺序已调整')
await barButton(wrapper, '撤销').trigger('click')
await flushPromises()
expect(wrapper.find('.list-column-bar').exists()).toBe(false)
expect(saveObjectApi).not.toHaveBeenCalled()
wrapper.unmount()
})
it('普通用户看不到「保存为通用」,只看到个人与撤销', async () => {
const wrapper = await mountPage()
await resizeColumn(wrapper, 'b_name', 320)
const bar = wrapper.find('.list-column-bar')
expect(bar.text()).toContain('撤销')
expect(bar.text()).toContain('保存为个人')
expect(bar.text()).not.toContain('保存为通用')
wrapper.unmount()
})
it('管理员能看到「保存为通用」', async () => {
isG3soft = true
const wrapper = await mountPage()
await resizeColumn(wrapper, 'b_name', 320)
expect(wrapper.find('.list-column-bar').text()).toContain('保存为通用')
wrapper.unmount()
})
it('保存为个人:只写 s_user_module_pref,且只存改过的字段宽度', async () => {
const wrapper = await mountPage()
await resizeColumn(wrapper, 'b_name', 320)
await barButton(wrapper, '保存为个人').trigger('click')
await flushPromises()
expect(saveObjectApi).toHaveBeenCalledTimes(1)
const requests = saveObjectApi.mock.calls[0][0]
expect(requests).toHaveLength(1)
const req = requests[0]
expect(req.table).toBe('s_user_module_pref')
expect(req.key_field).toBe('b_user_id,b_module_id,b_schema_type')
const row = req.inserts[0]
expect(row.b_user_id).toBe('u001')
expect(row.b_module_id).toBe('s_i18n_type')
expect(row.b_schema_type).toBe('view')
const delta = JSON.parse(row.b_schema_json)
// 只有 b_name 改了宽度 → 增量里只应出现它,通用改 b_id 仍能透传
expect(delta.columns).toEqual({ b_name: { width: 320 } })
// 顺序未变 → 不应写 order(否则会把通用顺序固化)
expect(delta.order).toBeUndefined()
// 保存成功后操作条消失(已成本新基线)
expect(wrapper.find('.list-column-bar').exists()).toBe(false)
wrapper.unmount()
})
it('保存为个人不碰 s_module_schema(不污染通用)', async () => {
const wrapper = await mountPage()
await resizeColumn(wrapper, 'b_name', 320)
await barButton(wrapper, '保存为个人').trigger('click')
await flushPromises()
const tables = saveObjectApi.mock.calls.flatMap((call) => call[0].map((r) => r.table))
expect(tables).not.toContain('s_module_schema')
wrapper.unmount()
})
it('保存为通用:写 s_module_schema 并回写宽度', async () => {
isG3soft = true
const wrapper = await mountPage()
await resizeColumn(wrapper, 'b_name', 320)
await barButton(wrapper, '保存为通用').trigger('click')
await flushPromises()
const requests = saveObjectApi.mock.calls.at(-1)[0]
const req = requests.find((r) => r.table === 's_module_schema')
expect(req).toBeTruthy()
expect(req.key_field).toBe('b_module_id,b_schema_type')
const patch = req.updates ? req.updates[0] : req.inserts[0]
expect(patch.b_schema_type).toBe('view')
const schema = JSON.parse(patch.b_schema_json)
const nameNode = schema.columns.find((node) => node.field === 'b_name')
expect(nameNode.width).toBe(320)
expect(wrapper.find('.list-column-bar').exists()).toBe(false)
wrapper.unmount()
})
it('个人层加载:合并进列配置(宽度生效)', async () => {
personalColumnPref = { schemaVersion: 1, columns: { b_name: { width: 260 } } }
const wrapper = await mountPage()
const table = wrapper.findComponent(FmsModuleTable)
const nameConfig = table.props('listConfig').find((item) => item.b_field === 'b_name')
expect(nameConfig.b_width).toBe(260)
// 已按个人层加载 → 与基线一致 → 不应立即显示「已调整」
expect(wrapper.find('.list-column-bar').exists()).toBe(false)
wrapper.unmount()
})
it('个人层不得引入通用没有的字段(越权字段被忽略)', async () => {
personalColumnPref = {
schemaVersion: 1,
columns: { b_hack: { width: 999 } },
order: ['b_hack', 'b_id'],
}
const wrapper = await mountPage()
const table = wrapper.findComponent(FmsModuleTable)
const fields = table.props('listConfig').map((item) => item.b_field)
expect(fields).not.toContain('b_hack')
expect(fields).toEqual(['b_id', 'b_name'])
wrapper.unmount()
})
it('个人层顺序生效,且不误报为「已调整」', async () => {
personalColumnPref = { schemaVersion: 1, order: ['b_name', 'b_id'] }
const wrapper = await mountPage()
const table = wrapper.findComponent(FmsModuleTable)
expect(table.props('listConfig').map((item) => item.b_field)).toEqual(['b_name', 'b_id'])
expect(wrapper.find('.list-column-bar').exists()).toBe(false)
wrapper.unmount()
})
it('保留个人设置再次保存时走 updates 而非 inserts(避免主键冲突)', async () => {
personalColumnPref = { schemaVersion: 1, columns: { b_name: { width: 260 } } }
const wrapper = await mountPage()
await resizeColumn(wrapper, 'b_name', 300)
await barButton(wrapper, '保存为个人').trigger('click')
await flushPromises()
const req = saveObjectApi.mock.calls.at(-1)[0].find((r) => r.table === 's_user_module_pref')
expect(req.updates).toBeTruthy()
expect(req.inserts).toBeUndefined()
// 增量是相对通用(180),不是相对个人上一次(260)
expect(JSON.parse(req.updates[0].b_schema_json).columns).toEqual({ b_name: { width: 300 } })
wrapper.unmount()
})
it('连续改动只累积一次落库,保存后操作条消失', async () => {
const wrapper = await mountPage()
await resizeColumn(wrapper, 'b_id', 140)
await resizeColumn(wrapper, 'b_id', 150)
await resizeColumn(wrapper, 'b_name', 260)
expect(wrapper.find('.list-column-bar').exists()).toBe(true)
await barButton(wrapper, '保存为个人').trigger('click')
await flushPromises()
expect(saveObjectApi).toHaveBeenCalledTimes(1)
const delta = JSON.parse(saveObjectApi.mock.calls[0][0][0].inserts[0].b_schema_json)
// 只记最终值(150 / 260),中间态不入增量
expect(delta.columns).toEqual({ b_id: { width: 150 }, b_name: { width: 260 } })
expect(wrapper.find('.list-column-bar').exists()).toBe(false)
wrapper.unmount()
})
it('保存后再次改动会重新出现操作条(基线已前移)', async () => {
const wrapper = await mountPage()
await resizeColumn(wrapper, 'b_name', 320)
await barButton(wrapper, '保存为个人').trigger('click')
await flushPromises()
expect(wrapper.find('.list-column-bar').exists()).toBe(false)
// 保存后把宽度改回通用值 → 与「新基线 320」不同,仍应提示
await resizeColumn(wrapper, 'b_name', 180)
expect(wrapper.find('.list-column-bar').exists()).toBe(true)
wrapper.unmount()
})
it('改回通用值后保存:清空个人层而非留下空壳覆盖', async () => {
personalColumnPref = { schemaVersion: 1, columns: { b_name: { width: 260 } } }
const wrapper = await mountPage()
// 改回与通用一致(b_name 通用宽度 180)
await resizeColumn(wrapper, 'b_name', 180)
await barButton(wrapper, '保存为个人').trigger('click')
await flushPromises()
const req = saveObjectApi.mock.calls.at(-1)[0].find((r) => r.table === 's_user_module_pref')
// 无增量可存 → 走删除,不写空 columns
expect(req.deletes).toBeTruthy()
expect(req.inserts).toBeUndefined()
expect(req.updates).toBeUndefined()
expect(req.deletes[0]).toMatchObject({
b_user_id: 'u001',
b_module_id: 's_i18n_type',
b_schema_type: 'view',
})
expect(wrapper.find('.list-column-bar').exists()).toBe(false)
wrapper.unmount()
})
it('已有个人设置但本次改动恰好等于通用时,也走删除清理', async () => {
// 个人层只改了顺序,本次把顺序也拖回通用顺序
personalColumnPref = { schemaVersion: 1, order: ['b_name', 'b_id'] }
const wrapper = await mountPage()
const table = wrapper.findComponent(FmsModuleTable)
const fmsTable = table.findComponent(FmsTable)
// 拖回通用顺序 b_id, b_name
const reordered = fmsTable.props('columns').toReversed()
fmsTable.vm.$emit('update:columns', reordered)
await flushPromises()
expect(wrapper.find('.list-column-bar').exists()).toBe(true)
await barButton(wrapper, '保存为个人').trigger('click')
await flushPromises()
const req = saveObjectApi.mock.calls.at(-1)[0].find((r) => r.table === 's_user_module_pref')
expect(req.deletes).toBeTruthy()
wrapper.unmount()
})
})