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 loading = ref(false)
|
||||||
const myPage = ref({ currentPage: 1, pageSize: 20, total: 0, totalPages: 0 })
|
const myPage = ref({ currentPage: 1, pageSize: 20, total: 0, totalPages: 0 })
|
||||||
|
|
||||||
|
// 列表当前生效的筛选条件。取消申请后会在原地重新拉取列表,那次拉取必须沿用
|
||||||
|
// 用户选中的状态,否则列表会悄悄退回全量,而下拉框里还显示着选中的筛选值。
|
||||||
|
const filters = ref({ status: '' })
|
||||||
|
|
||||||
async function fetchMyApplications(status = '', pageNum = 1) {
|
async function fetchMyApplications(status = '', pageNum = 1) {
|
||||||
|
filters.value = { status }
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const res = await applicationApi.getMy(status, pageNum, myPage.value.pageSize)
|
const res = await applicationApi.getMy(status, pageNum, myPage.value.pageSize)
|
||||||
|
|
@ -22,13 +27,16 @@ export const useApplicationStore = defineStore('application', () => {
|
||||||
|
|
||||||
async function createApplication(data) {
|
async function createApplication(data) {
|
||||||
const res = await applicationApi.create(data)
|
const res = await applicationApi.create(data)
|
||||||
|
// 这处刻意清空筛选并回到第 1 页,和上面「沿用筛选」的规矩相反:刚提交的是
|
||||||
|
// 待审批单,若沿用「已通过」之类的筛选就看不见它,用户会以为没提交成功。
|
||||||
|
// 清空的是 store 这份状态,页面那份(下拉框)由 Application.vue 同步重置。
|
||||||
await fetchMyApplications('', 1)
|
await fetchMyApplications('', 1)
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cancelApplication(id) {
|
async function cancelApplication(id) {
|
||||||
const res = await applicationApi.cancel(id)
|
const res = await applicationApi.cancel(id)
|
||||||
await fetchMyApplications('', myPage.value.currentPage)
|
await fetchMyApplications(filters.value.status, myPage.value.currentPage)
|
||||||
return res
|
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 loading = ref(false)
|
||||||
const page = ref({ currentPage: 1, pageSize: 20, total: 0, totalPages: 0 })
|
const page = ref({ currentPage: 1, pageSize: 20, total: 0, totalPages: 0 })
|
||||||
|
|
||||||
|
// 列表当前生效的筛选条件。审批通过/拒绝后会在原地重新拉取列表,那次拉取必须
|
||||||
|
// 沿用用户选中的状态,否则列表会悄悄退回全量,而下拉框里还显示着选中的筛选值。
|
||||||
|
const filters = ref({ status: '' })
|
||||||
|
|
||||||
async function fetchPendingApplications(status = '', pageNum = 1) {
|
async function fetchPendingApplications(status = '', pageNum = 1) {
|
||||||
|
filters.value = { status }
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const res = await applicationApi.getAll(status, pageNum, page.value.pageSize)
|
const res = await applicationApi.getAll(status, pageNum, page.value.pageSize)
|
||||||
|
|
@ -22,13 +27,13 @@ export const useApprovalStore = defineStore('approval', () => {
|
||||||
|
|
||||||
async function approveApplication(id, data = {}) {
|
async function approveApplication(id, data = {}) {
|
||||||
const res = await applicationApi.approve(id, data)
|
const res = await applicationApi.approve(id, data)
|
||||||
await fetchPendingApplications('', page.value.currentPage)
|
await fetchPendingApplications(filters.value.status, page.value.currentPage)
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
async function rejectApplication(id, data = {}) {
|
async function rejectApplication(id, data = {}) {
|
||||||
const res = await applicationApi.reject(id, data)
|
const res = await applicationApi.reject(id, data)
|
||||||
await fetchPendingApplications('', page.value.currentPage)
|
await fetchPendingApplications(filters.value.status, page.value.currentPage)
|
||||||
return res
|
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.purpose = ''
|
||||||
applyForm.items = [{ material_id: null, materialSearchText: '', quantity: 1 }]
|
applyForm.items = [{ material_id: null, materialSearchText: '', quantity: 1 }]
|
||||||
activeTab.value = 'my'
|
activeTab.value = 'my'
|
||||||
|
// store 里提交后的那次刷新会清空筛选(好让刚提交的待审批单出现在列表里),
|
||||||
|
// 下拉框得跟着一起回到「全部」,否则会出现下拉写着「已通过」、列表却是全量。
|
||||||
|
statusFilter.value = ''
|
||||||
} finally {
|
} finally {
|
||||||
submitLoading.value = false
|
submitLoading.value = false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user