fix: 审批/取消后列表的筛选条件被丢弃
写操作后原地重新拉取列表时,status 参数被写死成 '':列表悄悄退回全量(已通过、 已拒绝的单子全冒出来),而页面下拉框仍显示着用户选中的筛选值。用户以为看到的 是筛选结果,实际不是。store 层一直有这个缺陷却没被拦住,因为页面那层没有测试。 修法照 stock.js 已有的约定(stockFilters 配合 stock.test.js 的断言):store 存一份 filters,写操作原地刷新时取出来用。 - approval.js:approve / reject 沿用筛选 - application.js:cancel 沿用筛选 - application.js:create 保持清空筛选并回第 1 页——刚提交的是待审批单,若沿用 「已通过」之类的筛选就看不见它,用户会以为没提交成功 - Application.vue:提交成功后同步把下拉框重置为「全部」,否则 store 清了筛选而 下拉没清,同样是表里不一 测试补 9 条(approval/application 两个 store + Application 页面层)。全部做过变异 验证:把实现改回原样后对应断言确实变红,且只有该变红的那几条变红。
This commit is contained in:
parent
bf7f7129dc
commit
fbd0bc1341
|
|
@ -7,7 +7,12 @@ export const useApplicationStore = defineStore('application', () => {
|
|||
const loading = ref(false)
|
||||
const myPage = ref({ currentPage: 1, pageSize: 20, total: 0, totalPages: 0 })
|
||||
|
||||
// 列表当前生效的筛选条件。取消申请后会在原地重新拉取列表,那次拉取必须沿用
|
||||
// 用户选中的状态,否则列表会悄悄退回全量,而下拉框里还显示着选中的筛选值。
|
||||
const filters = ref({ status: '' })
|
||||
|
||||
async function fetchMyApplications(status = '', pageNum = 1) {
|
||||
filters.value = { status }
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await applicationApi.getMy(status, pageNum, myPage.value.pageSize)
|
||||
|
|
@ -22,13 +27,16 @@ export const useApplicationStore = defineStore('application', () => {
|
|||
|
||||
async function createApplication(data) {
|
||||
const res = await applicationApi.create(data)
|
||||
// 这处刻意清空筛选并回到第 1 页,和上面「沿用筛选」的规矩相反:刚提交的是
|
||||
// 待审批单,若沿用「已通过」之类的筛选就看不见它,用户会以为没提交成功。
|
||||
// 清空的是 store 这份状态,页面那份(下拉框)由 Application.vue 同步重置。
|
||||
await fetchMyApplications('', 1)
|
||||
return res
|
||||
}
|
||||
|
||||
async function cancelApplication(id) {
|
||||
const res = await applicationApi.cancel(id)
|
||||
await fetchMyApplications('', myPage.value.currentPage)
|
||||
await fetchMyApplications(filters.value.status, myPage.value.currentPage)
|
||||
return res
|
||||
}
|
||||
|
||||
|
|
|
|||
63
frontend/src/stores/application.test.js
Normal file
63
frontend/src/stores/application.test.js
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { applicationApi } from '../api/application'
|
||||
import { useApplicationStore } from './application'
|
||||
|
||||
vi.mock('../api/application', () => ({
|
||||
applicationApi: {
|
||||
getAll: vi.fn(),
|
||||
getMy: vi.fn(),
|
||||
create: vi.fn(),
|
||||
cancel: vi.fn(),
|
||||
approve: vi.fn(),
|
||||
reject: vi.fn()
|
||||
}
|
||||
}))
|
||||
|
||||
describe('application store 的筛选保持', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
vi.clearAllMocks()
|
||||
applicationApi.getMy.mockResolvedValue({ data: { items: [], total: 0 } })
|
||||
applicationApi.create.mockResolvedValue({ data: {} })
|
||||
applicationApi.cancel.mockResolvedValue({ data: {} })
|
||||
})
|
||||
|
||||
// 取消申请会在原地重新拉取列表,那次拉取必须沿用用户选中的状态:否则列表会悄悄
|
||||
// 退回全量(已通过/已拒绝的单子全冒出来),而下拉框里还显示着「待审批」。
|
||||
it('取消申请后刷新列表要沿用当前的筛选条件', async () => {
|
||||
const store = useApplicationStore()
|
||||
|
||||
await store.fetchMyApplications('pending', 2)
|
||||
applicationApi.getMy.mockClear()
|
||||
|
||||
await store.cancelApplication(3)
|
||||
|
||||
expect(applicationApi.getMy).toHaveBeenCalledWith('pending', 2, 20)
|
||||
})
|
||||
|
||||
// 这处刻意与上面相反:刚提交的是待审批单,若沿用「已通过」之类的筛选就看不见它,
|
||||
// 用户会以为没提交成功。所以清空筛选并回到第 1 页,让新单排在列表最前面。
|
||||
it('提交新申请后要清空筛选并回到第 1 页', async () => {
|
||||
const store = useApplicationStore()
|
||||
|
||||
await store.fetchMyApplications('approved', 3)
|
||||
applicationApi.getMy.mockClear()
|
||||
|
||||
await store.createApplication({ purpose: '测试' })
|
||||
|
||||
expect(applicationApi.getMy).toHaveBeenCalledWith('', 1, 20)
|
||||
})
|
||||
|
||||
// 首次加载没有筛选条件,别把上一次的筛选状态带到新页面上。
|
||||
it('不带筛选条件地重新查询会清掉旧的筛选状态', async () => {
|
||||
const store = useApplicationStore()
|
||||
|
||||
await store.fetchMyApplications('rejected', 1)
|
||||
applicationApi.getMy.mockClear()
|
||||
|
||||
await store.fetchMyApplications()
|
||||
|
||||
expect(applicationApi.getMy).toHaveBeenCalledWith('', 1, 20)
|
||||
})
|
||||
})
|
||||
|
|
@ -7,7 +7,12 @@ export const useApprovalStore = defineStore('approval', () => {
|
|||
const loading = ref(false)
|
||||
const page = ref({ currentPage: 1, pageSize: 20, total: 0, totalPages: 0 })
|
||||
|
||||
// 列表当前生效的筛选条件。审批通过/拒绝后会在原地重新拉取列表,那次拉取必须
|
||||
// 沿用用户选中的状态,否则列表会悄悄退回全量,而下拉框里还显示着选中的筛选值。
|
||||
const filters = ref({ status: '' })
|
||||
|
||||
async function fetchPendingApplications(status = '', pageNum = 1) {
|
||||
filters.value = { status }
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await applicationApi.getAll(status, pageNum, page.value.pageSize)
|
||||
|
|
@ -22,13 +27,13 @@ export const useApprovalStore = defineStore('approval', () => {
|
|||
|
||||
async function approveApplication(id, data = {}) {
|
||||
const res = await applicationApi.approve(id, data)
|
||||
await fetchPendingApplications('', page.value.currentPage)
|
||||
await fetchPendingApplications(filters.value.status, page.value.currentPage)
|
||||
return res
|
||||
}
|
||||
|
||||
async function rejectApplication(id, data = {}) {
|
||||
const res = await applicationApi.reject(id, data)
|
||||
await fetchPendingApplications('', page.value.currentPage)
|
||||
await fetchPendingApplications(filters.value.status, page.value.currentPage)
|
||||
return res
|
||||
}
|
||||
|
||||
|
|
|
|||
70
frontend/src/stores/approval.test.js
Normal file
70
frontend/src/stores/approval.test.js
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { applicationApi } from '../api/application'
|
||||
import { useApprovalStore } from './approval'
|
||||
|
||||
vi.mock('../api/application', () => ({
|
||||
applicationApi: {
|
||||
getAll: vi.fn(),
|
||||
getMy: vi.fn(),
|
||||
create: vi.fn(),
|
||||
cancel: vi.fn(),
|
||||
approve: vi.fn(),
|
||||
reject: vi.fn()
|
||||
}
|
||||
}))
|
||||
|
||||
describe('approval store 的筛选保持', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
vi.clearAllMocks()
|
||||
applicationApi.getAll.mockResolvedValue({ data: { items: [], total: 0 } })
|
||||
applicationApi.approve.mockResolvedValue({ data: {} })
|
||||
applicationApi.reject.mockResolvedValue({ data: {} })
|
||||
})
|
||||
|
||||
// 审批会在原地重新拉取列表,那次拉取必须沿用用户选中的状态:否则列表会悄悄退回
|
||||
// 全量(已通过/已拒绝的单子全冒出来),而下拉框里还显示着「待处理」。
|
||||
it('审批通过后刷新列表要沿用当前的筛选条件', async () => {
|
||||
const store = useApprovalStore()
|
||||
|
||||
await store.fetchPendingApplications('pending', 2)
|
||||
applicationApi.getAll.mockClear()
|
||||
|
||||
await store.approveApplication(9, { auto_outbound: true })
|
||||
|
||||
expect(applicationApi.getAll).toHaveBeenCalledWith('pending', 2, 20)
|
||||
})
|
||||
|
||||
it('审批拒绝后刷新列表要沿用当前的筛选条件', async () => {
|
||||
const store = useApprovalStore()
|
||||
|
||||
await store.fetchPendingApplications('pending', 1)
|
||||
applicationApi.getAll.mockClear()
|
||||
|
||||
await store.rejectApplication(9, { reason: '库存不足' })
|
||||
|
||||
expect(applicationApi.getAll).toHaveBeenCalledWith('pending', 1, 20)
|
||||
})
|
||||
|
||||
// 首次加载没有筛选条件,别把上一次的筛选状态带到新页面上。
|
||||
it('不带筛选条件地重新查询会清掉旧的筛选状态', async () => {
|
||||
const store = useApprovalStore()
|
||||
|
||||
await store.fetchPendingApplications('approved', 1)
|
||||
applicationApi.getAll.mockClear()
|
||||
|
||||
await store.fetchPendingApplications()
|
||||
|
||||
expect(applicationApi.getAll).toHaveBeenCalledWith('', 1, 20)
|
||||
})
|
||||
|
||||
// 翻页本身也要带上筛选:漏掉的话第 2 页会突然冒出全量数据。
|
||||
it('翻页时带上当前的筛选条件', async () => {
|
||||
const store = useApprovalStore()
|
||||
|
||||
await store.fetchPendingApplications('approved', 3)
|
||||
|
||||
expect(applicationApi.getAll).toHaveBeenCalledWith('approved', 3, 20)
|
||||
})
|
||||
})
|
||||
108
frontend/src/views/Application.test.js
Normal file
108
frontend/src/views/Application.test.js
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import ElementPlus, { ElMessage } from 'element-plus'
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest'
|
||||
import Application from './Application.vue'
|
||||
|
||||
const applicationStore = vi.hoisted(() => ({
|
||||
myApplications: [],
|
||||
loading: false,
|
||||
myPage: { currentPage: 1, pageSize: 20, total: 0, totalPages: 0 },
|
||||
fetchMyApplications: vi.fn(),
|
||||
createApplication: vi.fn(),
|
||||
cancelApplication: vi.fn()
|
||||
}))
|
||||
|
||||
const materialsStore = vi.hoisted(() => ({
|
||||
materialOptions: [],
|
||||
fetchMaterialOptions: vi.fn()
|
||||
}))
|
||||
|
||||
const stockStore = vi.hoisted(() => ({
|
||||
fetchStock: vi.fn(),
|
||||
fetchStockByMaterialId: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('../stores/application', () => ({ useApplicationStore: () => applicationStore }))
|
||||
vi.mock('../stores/materials', () => ({ useMaterialsStore: () => materialsStore }))
|
||||
vi.mock('../stores/stock', () => ({ useStockStore: () => stockStore }))
|
||||
|
||||
// 真实的 ElSelect 在 jsdom 下会因状态双向绑定反复触发自身更新而报
|
||||
// "Maximum recursive updates exceeded"。这里用替身直接派发 change,
|
||||
// 模拟用户在状态下拉里选中某一项。
|
||||
const SelectStub = {
|
||||
props: ['modelValue', 'placeholder', 'filterable', 'filterMethod', 'noDataText', 'disabled', 'clearable'],
|
||||
emits: ['update:modelValue', 'change'],
|
||||
methods: {
|
||||
pick(value) {
|
||||
this.$emit('update:modelValue', value)
|
||||
this.$emit('change', value)
|
||||
}
|
||||
},
|
||||
template: '<div class="select-stub"></div>'
|
||||
}
|
||||
|
||||
const OptionStub = { props: ['label', 'value'], template: '<span />' }
|
||||
|
||||
function mountApplication() {
|
||||
return mount(Application, {
|
||||
global: {
|
||||
plugins: [ElementPlus],
|
||||
stubs: { teleport: true, ElSelect: SelectStub, ElOption: OptionStub }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 页面里有两处 ElSelect:申请表单里的物料选择,和「我的申请」里的状态筛选。
|
||||
function statusSelect(wrapper) {
|
||||
return wrapper.findAllComponents(SelectStub)
|
||||
.find(select => select.props('placeholder') === '全部状态')
|
||||
}
|
||||
|
||||
function setupState(wrapper) {
|
||||
return wrapper.vm.$.setupState
|
||||
}
|
||||
|
||||
describe('提交申请后筛选条件与列表保持一致', () => {
|
||||
let successSpy
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
applicationStore.createApplication.mockResolvedValue({ data: {} })
|
||||
successSpy = vi.spyOn(ElMessage, 'success').mockImplementation(() => {})
|
||||
})
|
||||
|
||||
test('状态下拉选中的值会传给列表查询', async () => {
|
||||
const wrapper = mountApplication()
|
||||
await flushPromises()
|
||||
|
||||
statusSelect(wrapper).vm.pick('approved')
|
||||
await flushPromises()
|
||||
|
||||
expect(setupState(wrapper).statusFilter).toBe('approved')
|
||||
expect(applicationStore.fetchMyApplications).toHaveBeenLastCalledWith('approved', 1)
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
// store 里提交后的那次刷新是刻意清空筛选的(好让刚提交的待审批单出现在列表里),
|
||||
// 页面这份状态(下拉框)必须跟着一起回到「全部」:否则会出现下拉写着「已通过」、
|
||||
// 列表却是全量的表里不一,这次修复就是补的这一处。
|
||||
test('提交成功后下拉框的筛选要一起重置为全部', async () => {
|
||||
const wrapper = mountApplication()
|
||||
await flushPromises()
|
||||
|
||||
statusSelect(wrapper).vm.pick('approved')
|
||||
await flushPromises()
|
||||
expect(setupState(wrapper).statusFilter).toBe('approved')
|
||||
|
||||
setupState(wrapper).applyForm.purpose = '测试用途'
|
||||
setupState(wrapper).applyForm.items = [
|
||||
{ material_id: 7, materialSearchText: '', quantity: 1 }
|
||||
]
|
||||
await setupState(wrapper).handleSubmit()
|
||||
await flushPromises()
|
||||
|
||||
expect(applicationStore.createApplication).toHaveBeenCalled()
|
||||
expect(setupState(wrapper).statusFilter).toBe('')
|
||||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
|
|
@ -212,6 +212,9 @@ async function handleSubmit() {
|
|||
applyForm.purpose = ''
|
||||
applyForm.items = [{ material_id: null, materialSearchText: '', quantity: 1 }]
|
||||
activeTab.value = 'my'
|
||||
// store 里提交后的那次刷新会清空筛选(好让刚提交的待审批单出现在列表里),
|
||||
// 下拉框得跟着一起回到「全部」,否则会出现下拉写着「已通过」、列表却是全量。
|
||||
statusFilter.value = ''
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user