写操作后原地重新拉取列表时,search 参数被写死成缺省的空串:搜索框里还显示着 用户输入的关键字,列表却退回全量。用户会把全量列表当成筛选结果,接下来的作废/ 删除操作就打在被误认的那一条上。store 层一直有这个缺陷却没被拦住,因为页面 那层没有测试。 修法照 stock.js / approval.js 已有的约定(store 存一份 filters,写操作原地刷新 时取出来用): - materials.js:编辑/作废/恢复沿用 search 和 status - users.js:编辑/删除沿用 search - 新增走反方向,清空筛选并回第 1 页:新建的项不一定匹配当前关键字,沿用搜索 就看不见它,用户会以为没建成功。物料按 id 倒序,回第 1 页正好能看见 - Materials.vue / Users.vue:新增成功后把搜索框一起清空,否则 store 清了筛选而 搜索框没清,同样是表里不一 测试补 14 条(两个 store + 两个页面)。全部做过变异验证:把实现改回原样后, 6 条 store 断言和 2 条页面断言确实变红,且只有它们变红。
198 lines
6.0 KiB
JavaScript
198 lines
6.0 KiB
JavaScript
import { createPinia, setActivePinia } from 'pinia'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { materialsApi } from '../api/materials'
|
|
import { useMaterialsStore } from './materials'
|
|
|
|
vi.mock('../api/materials', () => ({
|
|
materialsApi: {
|
|
getAll: vi.fn(),
|
|
create: vi.fn(),
|
|
update: vi.fn(),
|
|
obsolete: vi.fn(),
|
|
restore: vi.fn()
|
|
}
|
|
}))
|
|
|
|
describe('materials store selection options', () => {
|
|
beforeEach(() => {
|
|
setActivePinia(createPinia())
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
it('loads every page so a selector can find materials outside the first page', async () => {
|
|
const firstPage = Array.from({ length: 100 }, (_, index) => ({
|
|
id: 120 - index,
|
|
material_code: String(120 - index).padStart(3, '0'),
|
|
name: `物料 ${120 - index}`
|
|
}))
|
|
materialsApi.getAll
|
|
.mockResolvedValueOnce({
|
|
data: {
|
|
items: firstPage,
|
|
limit: 100,
|
|
offset: 0,
|
|
total: 101,
|
|
has_more: true
|
|
}
|
|
})
|
|
.mockResolvedValueOnce({
|
|
data: {
|
|
items: [{ id: 1, material_code: '001', name: '早期物料' }],
|
|
limit: 100,
|
|
offset: 100,
|
|
total: 101,
|
|
has_more: false
|
|
}
|
|
})
|
|
|
|
const store = useMaterialsStore()
|
|
await store.fetchMaterialOptions()
|
|
|
|
expect(store.materialOptions).toHaveLength(101)
|
|
expect(store.materialOptions.at(-1)).toEqual({
|
|
id: 1,
|
|
material_code: '001',
|
|
name: '早期物料'
|
|
})
|
|
})
|
|
|
|
it('loads obsolete materials for historical record selectors only', async () => {
|
|
materialsApi.getAll.mockResolvedValue({
|
|
data: {
|
|
items: [
|
|
{ id: 1, material_code: 'A-001', name: '有效物料', status: 'active' },
|
|
{ id: 2, material_code: 'B-002', name: '历史物料', status: 'obsolete' }
|
|
],
|
|
total: 2
|
|
}
|
|
})
|
|
|
|
const store = useMaterialsStore()
|
|
await store.fetchHistoricalMaterialOptions()
|
|
|
|
expect(materialsApi.getAll).toHaveBeenCalledWith({ status: 'all', limit: 100, offset: 0 })
|
|
expect(store.historicalMaterialOptions).toEqual([
|
|
{ id: 1, material_code: 'A-001', name: '有效物料', status: 'active' },
|
|
{ id: 2, material_code: 'B-002', name: '历史物料', status: 'obsolete' }
|
|
])
|
|
expect(store.materialOptions).toEqual([])
|
|
})
|
|
})
|
|
|
|
describe('materials store 的筛选保持', () => {
|
|
beforeEach(() => {
|
|
setActivePinia(createPinia())
|
|
vi.clearAllMocks()
|
|
materialsApi.getAll.mockResolvedValue({ data: { items: [], total: 0 } })
|
|
materialsApi.create.mockResolvedValue({ data: {} })
|
|
materialsApi.update.mockResolvedValue({ data: {} })
|
|
materialsApi.obsolete.mockResolvedValue({ data: {} })
|
|
materialsApi.restore.mockResolvedValue({ data: {} })
|
|
})
|
|
|
|
// 编辑/作废/恢复都在原地重新拉取列表,那次拉取必须沿用搜索框里的关键字:否则列表
|
|
// 会悄悄退回全量,而搜索框里还显示着用户输入的内容——用户会把全量列表当成筛选结果,
|
|
// 接着的作废操作就打在误认的那一条上。
|
|
it('编辑物料后刷新列表要沿用当前的搜索关键字', async () => {
|
|
const store = useMaterialsStore()
|
|
|
|
await store.fetchMaterials(2, '螺丝')
|
|
materialsApi.getAll.mockClear()
|
|
|
|
await store.updateMaterial(7, { name: '内六角螺丝' })
|
|
|
|
expect(materialsApi.getAll).toHaveBeenCalledWith({
|
|
search: '螺丝',
|
|
status: 'all',
|
|
limit: 20,
|
|
offset: 20
|
|
})
|
|
})
|
|
|
|
// 作废和恢复除了列表还要刷两份选项,所以 getAll 会被调用多次,只看第一次(列表那次)。
|
|
it('作废物料后刷新列表要沿用当前的搜索关键字', async () => {
|
|
const store = useMaterialsStore()
|
|
|
|
await store.fetchMaterials(1, '螺丝')
|
|
materialsApi.getAll.mockClear()
|
|
|
|
await store.obsoleteMaterial(7)
|
|
|
|
expect(materialsApi.getAll).toHaveBeenNthCalledWith(1, {
|
|
search: '螺丝',
|
|
status: 'all',
|
|
limit: 20,
|
|
offset: 0
|
|
})
|
|
})
|
|
|
|
it('恢复物料后刷新列表要沿用当前的搜索关键字', async () => {
|
|
const store = useMaterialsStore()
|
|
|
|
await store.fetchMaterials(1, '螺丝')
|
|
materialsApi.getAll.mockClear()
|
|
|
|
await store.restoreMaterial(7)
|
|
|
|
expect(materialsApi.getAll).toHaveBeenNthCalledWith(1, {
|
|
search: '螺丝',
|
|
status: 'all',
|
|
limit: 20,
|
|
offset: 0
|
|
})
|
|
})
|
|
|
|
// 状态筛选目前还没有页面在用,但 store 的接口本来就有这一维,原地刷新时漏掉它会
|
|
// 埋下同一个坑:以后页面加上状态下拉,列表又会悄悄退回全量。
|
|
it('刷新时一并沿用当前的状态筛选', async () => {
|
|
const store = useMaterialsStore()
|
|
|
|
await store.fetchMaterials(1, '', 'obsolete')
|
|
materialsApi.getAll.mockClear()
|
|
|
|
await store.updateMaterial(7, { name: '内六角螺丝' })
|
|
|
|
expect(materialsApi.getAll).toHaveBeenCalledWith({
|
|
search: '',
|
|
status: 'obsolete',
|
|
limit: 20,
|
|
offset: 0
|
|
})
|
|
})
|
|
|
|
// 这处刻意与上面相反:新建的物料不一定匹配当前关键字,沿用搜索就看不见它,用户会
|
|
// 以为没建成功。列表按 id 倒序,回到第 1 页正好能看见它。
|
|
it('新建物料后要清空筛选并回到第 1 页', async () => {
|
|
const store = useMaterialsStore()
|
|
|
|
await store.fetchMaterials(3, '螺丝', 'active')
|
|
materialsApi.getAll.mockClear()
|
|
|
|
await store.createMaterial({ name: '新物料' })
|
|
|
|
expect(materialsApi.getAll).toHaveBeenCalledWith({
|
|
search: '',
|
|
status: 'all',
|
|
limit: 20,
|
|
offset: 0
|
|
})
|
|
})
|
|
|
|
// 首次加载没有筛选条件,别把上一次的筛选状态带到新页面上。
|
|
it('不带筛选条件地重新查询会清掉旧的筛选状态', async () => {
|
|
const store = useMaterialsStore()
|
|
|
|
await store.fetchMaterials(2, '螺丝', 'obsolete')
|
|
materialsApi.getAll.mockClear()
|
|
|
|
await store.fetchMaterials()
|
|
|
|
expect(materialsApi.getAll).toHaveBeenCalledWith({
|
|
search: '',
|
|
status: 'all',
|
|
limit: 20,
|
|
offset: 0
|
|
})
|
|
})
|
|
})
|