395 lines
15 KiB
JavaScript
395 lines
15 KiB
JavaScript
/**
|
||
* 栏位设置(列宽/顺序)冒烟测试:模拟真实操作链路。
|
||
*
|
||
* 覆盖:
|
||
* 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()
|
||
})
|
||
})
|