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) }) })