- 组织级 store 统一提供 resetForOrganization,清空缓存、查询参数、分页与 loading - 加载方法提交状态前校验捕获的公司 ID 与上下文版本,旧公司延迟响应不再覆盖新公司 - 页面重挂载清理局部状态(物料选择、单据 ID、申请详情、审批弹窗) - 新增切换守卫,脏表单时取消切换不清空数据、保存期间禁止切换 - 接上库存事件消费与写请求的保存互斥标记 - 仓库配置页在已有仓库/库位时隐藏新增入口,仅保留编辑与启停
453 lines
17 KiB
JavaScript
453 lines
17 KiB
JavaScript
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)
|
||
})
|
||
})
|