material_manage_system/frontend/src/stores/organization.test.js
文睿 张 1a4b48cf9a fix: 切换公司时重置组织级页面状态
- 组织级 store 统一提供 resetForOrganization,清空缓存、查询参数、分页与 loading
- 加载方法提交状态前校验捕获的公司 ID 与上下文版本,旧公司延迟响应不再覆盖新公司
- 页面重挂载清理局部状态(物料选择、单据 ID、申请详情、审批弹窗)
- 新增切换守卫,脏表单时取消切换不清空数据、保存期间禁止切换
- 接上库存事件消费与写请求的保存互斥标记
- 仓库配置页在已有仓库/库位时隐藏新增入口,仅保留编辑与启停
2026-10-09 14:19:17 +08:00

453 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, test, vi } from 'vitest'
import { authApi } from '../api/auth'
import { useOrganizationStore } from './organization'
import { currentReadSignal, resetOrganizationContext } from '../utils/organizationContext'
vi.mock('../api/auth', () => ({
authApi: {
listOrganizations: vi.fn(),
switchOrganizationContext: vi.fn(),
createWebSocketTicket: vi.fn()
}
}))
// 公司级实时连接在 store 内接线:这里用桩替换,断言切换触发 connect、登出/空状态触发 close。
const socketMock = vi.hoisted(() => ({ connect: vi.fn(), close: vi.fn() }))
vi.mock('../utils/companySocket', () => ({
createCompanySocket: () => socketMock
}))
// 模拟某个账号已登录:公司选择按用户 id 分开存储,这里写入对应账号。
function loginAs(id) {
localStorage.setItem('wmms_user', JSON.stringify({ id, username: `user${id}` }))
}
function orgList(organizations, defaultOrganizationId) {
return { data: { organizations, default_organization_id: defaultOrganizationId } }
}
function switchResult(organizationId, warehouse = null) {
return { data: { organization_id: organizationId, organization: { id: organizationId }, warehouse } }
}
const ACTIVE_WAREHOUSE = {
id: 10,
name: '武汉仓',
status: 'active',
location: { id: 100, name: 'A库位', status: 'active' }
}
describe('organization store', () => {
beforeEach(() => {
localStorage.clear()
sessionStorage.clear()
resetOrganizationContext()
setActivePinia(createPinia())
authApi.listOrganizations.mockReset()
authApi.switchOrganizationContext.mockReset()
socketMock.connect.mockReset()
socketMock.close.mockReset()
})
test('默认公司进入并自动选中唯一启用的仓库与库位', async () => {
loginAs(1)
authApi.listOrganizations.mockResolvedValue(orgList([{ id: 1, name: '北京' }, { id: 2, name: '武汉' }], 2))
authApi.switchOrganizationContext.mockResolvedValue(switchResult(2, ACTIVE_WAREHOUSE))
const store = useOrganizationStore()
await store.initialize()
expect(store.organizations.map(o => o.id)).toEqual([1, 2])
expect(store.currentOrganizationId).toBe(2)
expect(store.currentWarehouseId).toBe(10)
expect(store.currentLocationId).toBe(100)
expect(store.initialized).toBe(true)
expect(authApi.switchOrganizationContext).toHaveBeenCalledWith(2, 0)
})
test('只展示接口返回的授权公司', async () => {
loginAs(1)
authApi.listOrganizations.mockResolvedValue(orgList([{ id: 3, name: '上海' }], 3))
authApi.switchOrganizationContext.mockResolvedValue(switchResult(3))
const store = useOrganizationStore()
await store.initialize()
expect(store.organizations.map(o => o.id)).toEqual([3])
expect(store.currentOrganizationId).toBe(3)
})
test('默认公司失效时保留列表并要求重新选择', async () => {
loginAs(1)
authApi.listOrganizations.mockResolvedValue(orgList([{ id: 1, name: '北京' }], 99))
// 候选公司确实无授权时服务端返回 403,才判定为“失效”。
authApi.switchOrganizationContext.mockRejectedValue({ response: { status: 403 } })
const store = useOrganizationStore()
await store.initialize()
expect(store.organizations.map(o => o.id)).toEqual([1])
expect(store.currentOrganizationId).toBeNull()
expect(store.initialized).toBe(true)
})
test('候选校验遇到非 403 的临时故障时向上抛出,不误报空状态', async () => {
loginAs(1)
authApi.listOrganizations.mockResolvedValue(orgList([{ id: 1, name: '北京' }], 1))
// 网络异常/500 没有 response.status===403,不能被当成候选失效。
authApi.switchOrganizationContext.mockRejectedValue(new Error('网络异常'))
const store = useOrganizationStore()
await expect(store.initialize()).rejects.toThrow('网络异常')
// 不进入空状态:标记加载失败、保留未初始化,供 App 区分“加载失败”并允许重试。
expect(store.currentOrganizationId).toBeNull()
expect(store.initialized).toBe(false)
expect(store.loadError).toBe(true)
// 故障恢复后重试成功,加载失败标记清除。
authApi.switchOrganizationContext.mockResolvedValueOnce(switchResult(1))
await store.initialize()
expect(store.initialized).toBe(true)
expect(store.loadError).toBe(false)
expect(store.currentOrganizationId).toBe(1)
})
test('没有授权公司时进入空状态且不发起校验', async () => {
loginAs(1)
authApi.listOrganizations.mockResolvedValue(orgList([], null))
const store = useOrganizationStore()
await store.initialize()
expect(store.organizations).toEqual([])
expect(store.currentOrganizationId).toBeNull()
expect(store.currentWarehouseId).toBeNull()
expect(store.currentLocationId).toBeNull()
expect(authApi.switchOrganizationContext).not.toHaveBeenCalled()
})
test('切换失败时保留旧公司', async () => {
loginAs(1)
authApi.listOrganizations.mockResolvedValue(orgList([{ id: 1 }, { id: 2 }], 1))
authApi.switchOrganizationContext.mockResolvedValueOnce(
switchResult(1, { id: 9, name: '北京仓', status: 'active', location: { id: 90, name: '库位', status: 'active' } })
)
const store = useOrganizationStore()
await store.initialize()
expect(store.currentOrganizationId).toBe(1)
authApi.switchOrganizationContext.mockRejectedValueOnce(new Error('公司不可访问'))
await expect(store.switchOrganization(2)).rejects.toThrow()
expect(store.currentOrganizationId).toBe(1)
expect(store.currentWarehouseId).toBe(9)
expect(store.currentLocationId).toBe(90)
expect(store.switching).toBe(false)
})
test('切换成功后提交新公司并递增上下文版本', async () => {
loginAs(1)
authApi.listOrganizations.mockResolvedValue(orgList([{ id: 1 }, { id: 2 }], 1))
authApi.switchOrganizationContext.mockResolvedValueOnce(switchResult(1))
const store = useOrganizationStore()
await store.initialize()
const versionBefore = store.contextVersion
authApi.switchOrganizationContext.mockResolvedValueOnce(switchResult(2, ACTIVE_WAREHOUSE))
await store.switchOrganization(2)
expect(store.currentOrganizationId).toBe(2)
expect(store.contextVersion).toBeGreaterThan(versionBefore)
expect(store.currentWarehouseId).toBe(10)
expect(store.currentLocationId).toBe(100)
expect(authApi.switchOrganizationContext).toHaveBeenLastCalledWith(2, 1)
})
test('切换到当前公司不重复发起校验', async () => {
loginAs(1)
authApi.listOrganizations.mockResolvedValue(orgList([{ id: 1 }], 1))
authApi.switchOrganizationContext.mockResolvedValueOnce(switchResult(1))
const store = useOrganizationStore()
await store.initialize()
authApi.switchOrganizationContext.mockClear()
await store.switchOrganization(1)
expect(authApi.switchOrganizationContext).not.toHaveBeenCalled()
})
test('仓库停用时仓库与库位都为空', async () => {
loginAs(1)
authApi.listOrganizations.mockResolvedValue(orgList([{ id: 1 }], 1))
authApi.switchOrganizationContext.mockResolvedValue(switchResult(1, {
id: 5, name: '停用仓', status: 'inactive', location: { id: 50, name: '库位', status: 'active' }
}))
const store = useOrganizationStore()
await store.initialize()
expect(store.currentWarehouseId).toBeNull()
expect(store.currentLocationId).toBeNull()
})
test('库位缺失或停用时仅库位为空', async () => {
loginAs(1)
authApi.listOrganizations.mockResolvedValue(orgList([{ id: 1 }], 1))
authApi.switchOrganizationContext.mockResolvedValue(switchResult(1, {
id: 5, name: '启用仓', status: 'active', location: null
}))
const store = useOrganizationStore()
await store.initialize()
expect(store.currentWarehouseId).toBe(5)
expect(store.currentLocationId).toBeNull()
})
test('目标公司没有仓库时清空旧公司的仓库与库位', async () => {
loginAs(1)
authApi.listOrganizations.mockResolvedValue(orgList([{ id: 1 }, { id: 2 }], 1))
authApi.switchOrganizationContext.mockResolvedValueOnce(switchResult(1, ACTIVE_WAREHOUSE))
const store = useOrganizationStore()
await store.initialize()
expect(store.currentWarehouseId).toBe(10)
authApi.switchOrganizationContext.mockResolvedValueOnce(switchResult(2, null))
await store.switchOrganization(2)
expect(store.currentOrganizationId).toBe(2)
expect(store.currentWarehouseId).toBeNull()
expect(store.currentLocationId).toBeNull()
})
test('不同账号不继承上一账号的公司选择', async () => {
// 账号 1 上次在本标签页选择了公司 2
loginAs(1)
sessionStorage.setItem('wmms_organization_id:1', '2')
// 换成账号 2 登录:读到的应是账号 2 的存储(为空),回落到默认公司 1
loginAs(2)
authApi.listOrganizations.mockResolvedValue(orgList([{ id: 1 }, { id: 2 }], 1))
authApi.switchOrganizationContext.mockResolvedValue(switchResult(1))
const store = useOrganizationStore()
await store.initialize()
expect(store.currentOrganizationId).toBe(1)
expect(authApi.switchOrganizationContext).toHaveBeenCalledWith(1, 0)
})
test('切换进行中禁止再次切换', async () => {
loginAs(1)
authApi.listOrganizations.mockResolvedValue(orgList([{ id: 1 }], 1))
authApi.switchOrganizationContext.mockResolvedValueOnce(switchResult(1))
const store = useOrganizationStore()
await store.initialize()
authApi.switchOrganizationContext.mockClear()
store.switching = true
await store.switchOrganization(2)
expect(authApi.switchOrganizationContext).not.toHaveBeenCalled()
expect(store.currentOrganizationId).toBe(1)
})
test('写请求未完成时切换被拒绝', async () => {
loginAs(1)
authApi.listOrganizations.mockResolvedValue(orgList([{ id: 1 }, { id: 2 }], 1))
authApi.switchOrganizationContext.mockResolvedValueOnce(switchResult(1))
const store = useOrganizationStore()
await store.initialize()
authApi.switchOrganizationContext.mockClear()
// 模拟某个业务 store 的保存请求仍在飞行中。
store.beginSaving()
expect(store.busy).toBe(true)
await store.switchOrganization(2)
// 保存期间切换会让保存响应被版本判定作废并静默丢弃,必须直接拒绝且不发起校验。
expect(authApi.switchOrganizationContext).not.toHaveBeenCalled()
expect(store.currentOrganizationId).toBe(1)
// 保存结束后重新允许切换,避免忙标志被永久卡住。
store.endSaving()
expect(store.busy).toBe(false)
authApi.switchOrganizationContext.mockResolvedValueOnce(switchResult(2))
await store.switchOrganization(2)
expect(store.currentOrganizationId).toBe(2)
})
test('切换公司会中止旧公司尚未返回的读请求', async () => {
loginAs(1)
authApi.listOrganizations.mockResolvedValue(orgList([{ id: 1 }, { id: 2 }], 1))
authApi.switchOrganizationContext.mockResolvedValueOnce(switchResult(1))
const store = useOrganizationStore()
await store.initialize()
// 切换前取到的读请求信号,代表旧公司尚未返回的读请求。
const oldSignal = currentReadSignal()
expect(oldSignal.aborted).toBe(false)
authApi.switchOrganizationContext.mockResolvedValueOnce(switchResult(2))
await store.switchOrganization(2)
expect(oldSignal.aborted).toBe(true)
expect(store.currentOrganizationId).toBe(2)
})
test('reset 清空公司状态与上下文', async () => {
loginAs(1)
authApi.listOrganizations.mockResolvedValue(orgList([{ id: 1 }], 1))
authApi.switchOrganizationContext.mockResolvedValue(switchResult(1, ACTIVE_WAREHOUSE))
const store = useOrganizationStore()
await store.initialize()
expect(store.currentOrganizationId).toBe(1)
store.reset()
expect(store.organizations).toEqual([])
expect(store.currentOrganizationId).toBeNull()
expect(store.currentWarehouseId).toBeNull()
expect(store.currentLocationId).toBeNull()
expect(store.initialized).toBe(false)
})
})
// 公司级实时连接的运行时接线:计划要求“切换公司关旧连并为新公司重连、登出关闭、空状态不连接”。
// 这些用例钉住 store 在上述生命周期节点确实调用了 connect/close。
describe('公司级实时连接接线', () => {
beforeEach(() => {
localStorage.clear()
sessionStorage.clear()
resetOrganizationContext()
setActivePinia(createPinia())
authApi.listOrganizations.mockReset()
authApi.switchOrganizationContext.mockReset()
socketMock.connect.mockReset()
socketMock.close.mockReset()
})
test('切换公司时为当前公司重连(connect 内部先关旧连)', async () => {
loginAs(1)
authApi.listOrganizations.mockResolvedValue(orgList([{ id: 1 }, { id: 2 }], 1))
authApi.switchOrganizationContext.mockResolvedValueOnce(switchResult(1))
const store = useOrganizationStore()
await store.initialize()
const afterInit = socketMock.connect.mock.calls.length
expect(afterInit).toBeGreaterThanOrEqual(1)
authApi.switchOrganizationContext.mockResolvedValueOnce(switchResult(2))
await store.switchOrganization(2)
expect(socketMock.connect.mock.calls.length).toBeGreaterThan(afterInit)
})
test('登出(reset)关闭连接且不重连', async () => {
loginAs(1)
authApi.listOrganizations.mockResolvedValue(orgList([{ id: 1 }], 1))
authApi.switchOrganizationContext.mockResolvedValue(switchResult(1))
const store = useOrganizationStore()
await store.initialize()
const connectBefore = socketMock.connect.mock.calls.length
store.reset()
expect(socketMock.close).toHaveBeenCalled()
// reset 之后不再建立新连接。
expect(socketMock.connect.mock.calls.length).toBe(connectBefore)
})
test('无可用公司时不建立连接,只关闭', async () => {
loginAs(1)
authApi.listOrganizations.mockResolvedValue(orgList([], null))
const store = useOrganizationStore()
await store.initialize()
expect(socketMock.connect).not.toHaveBeenCalled()
expect(socketMock.close).toHaveBeenCalled()
})
})
// 组织范围变更时的业务 store 清理注册表,以及切换前的“未保存内容”确认守卫。
describe('公司范围变更的清理注册与切换守卫', () => {
beforeEach(() => {
localStorage.clear()
sessionStorage.clear()
resetOrganizationContext()
setActivePinia(createPinia())
authApi.listOrganizations.mockReset()
authApi.switchOrganizationContext.mockReset()
socketMock.connect.mockReset()
socketMock.close.mockReset()
})
async function enterCompany1(store) {
loginAs(1)
authApi.listOrganizations.mockResolvedValue(orgList([{ id: 1 }, { id: 2 }], 1))
authApi.switchOrganizationContext.mockResolvedValueOnce(switchResult(1))
await store.initialize()
}
test('切换公司时调用注册的重置回调', async () => {
const store = useOrganizationStore()
await enterCompany1(store)
const reset = vi.fn()
store.registerOrganizationReset(reset)
authApi.switchOrganizationContext.mockResolvedValueOnce(switchResult(2))
await store.switchOrganization(2)
expect(reset).toHaveBeenCalledTimes(1)
expect(store.currentOrganizationId).toBe(2)
})
test('登出(reset)时也调用注册的重置回调', async () => {
const store = useOrganizationStore()
await enterCompany1(store)
const reset = vi.fn()
store.registerOrganizationReset(reset)
store.reset()
expect(reset).toHaveBeenCalledTimes(1)
})
test('守卫返回 false 时取消切换,保持当前公司且不发起校验', async () => {
const store = useOrganizationStore()
await enterCompany1(store)
authApi.switchOrganizationContext.mockClear()
store.setOrganizationSwitchGuard(async () => false)
await store.switchOrganization(2)
expect(store.currentOrganizationId).toBe(1)
expect(authApi.switchOrganizationContext).not.toHaveBeenCalled()
// 取消后不能把“切换中”标志卡住
expect(store.switching).toBe(false)
})
test('守卫返回 true 时按原流程切换', async () => {
const store = useOrganizationStore()
await enterCompany1(store)
store.setOrganizationSwitchGuard(async () => true)
authApi.switchOrganizationContext.mockResolvedValueOnce(switchResult(2))
await store.switchOrganization(2)
expect(store.currentOrganizationId).toBe(2)
})
test('没有注册守卫时行为不变,直接切换', async () => {
const store = useOrganizationStore()
await enterCompany1(store)
authApi.switchOrganizationContext.mockResolvedValueOnce(switchResult(2))
await store.switchOrganization(2)
expect(store.currentOrganizationId).toBe(2)
})
})