impl frontend permission management
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import { mount, flushPromises } from '@vue/test-utils'
|
||||
import { nextTick } from 'vue'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { message } from 'ant-design-vue'
|
||||
import DataScopeModal from '@/components/DataScopeModal.vue'
|
||||
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: (query: string) => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: () => {},
|
||||
removeListener: () => {},
|
||||
addEventListener: () => {},
|
||||
removeEventListener: () => {},
|
||||
dispatchEvent: () => false,
|
||||
}),
|
||||
})
|
||||
|
||||
vi.mock('@/utils/request', () => ({
|
||||
api: {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
put: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
import { api } from '@/utils/request'
|
||||
|
||||
const mockScopes = {
|
||||
user_id: 1,
|
||||
role: 'accessor',
|
||||
scopes: [
|
||||
{ scope_type: 'company', scope_id: 1, name: '阿克米公司' },
|
||||
{ scope_type: 'store', scope_id: 5, name: '我的店铺' },
|
||||
],
|
||||
resolved_store_ids: [5],
|
||||
}
|
||||
|
||||
const mockCompanies = [
|
||||
{ id: 1, name: 'acme', label: '阿克米公司', enabled: true },
|
||||
{ id: 2, name: 'beta', label: '贝塔公司', enabled: true },
|
||||
]
|
||||
|
||||
const mockPlatforms = [
|
||||
{ id: 1, developer_id: 1 },
|
||||
{ id: 2, developer_id: 1 },
|
||||
]
|
||||
|
||||
const mockStores = [
|
||||
{ id: 5, company_id: 1, platform_id: 1, name: 'my-store', label: '我的店铺' },
|
||||
{ id: 6, company_id: 2, platform_id: 2, name: 'other-store', label: '其他店铺' },
|
||||
]
|
||||
|
||||
function setupMocks(overrides: { scopes?: typeof mockScopes } = {}) {
|
||||
vi.mocked(api.get).mockImplementation((url: string) => {
|
||||
if (url.match(/\/users\/\d+\/data-scope/)) return Promise.resolve(overrides.scopes ?? mockScopes) as never
|
||||
if (url === '/api/v1/companies') return Promise.resolve(mockCompanies) as never
|
||||
if (url === '/api/v1/platforms') return Promise.resolve(mockPlatforms) as never
|
||||
if (url === '/api/v1/stores') return Promise.resolve(mockStores) as never
|
||||
return Promise.resolve([]) as never
|
||||
})
|
||||
vi.mocked(api.put).mockResolvedValue(undefined as never)
|
||||
}
|
||||
|
||||
describe('DataScopeModal', () => {
|
||||
let wrapper: ReturnType<typeof mount>
|
||||
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
document.body.innerHTML = ''
|
||||
setActivePinia(createPinia())
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
wrapper?.unmount()
|
||||
document.body.innerHTML = ''
|
||||
})
|
||||
|
||||
async function mountModal(props = {}) {
|
||||
wrapper = mount(DataScopeModal, {
|
||||
props: {
|
||||
open: true,
|
||||
userId: 1,
|
||||
username: 'testuser',
|
||||
...props,
|
||||
},
|
||||
attachTo: document.body,
|
||||
})
|
||||
await flushPromises()
|
||||
await nextTick()
|
||||
return wrapper
|
||||
}
|
||||
|
||||
function queryBody(selector: string) {
|
||||
return document.body.querySelectorAll(selector)
|
||||
}
|
||||
|
||||
describe('数据加载', () => {
|
||||
it('打开时加载用户 scope 数据', async () => {
|
||||
setupMocks()
|
||||
await mountModal()
|
||||
|
||||
expect(vi.mocked(api.get)).toHaveBeenCalled()
|
||||
const calls = vi.mocked(api.get).mock.calls.map((c) => c[0])
|
||||
expect(calls.some((url) => url.includes('/data-scope'))).toBe(true)
|
||||
expect(calls.some((url) => url.includes('/companies'))).toBe(true)
|
||||
})
|
||||
|
||||
it('标题包含用户名', async () => {
|
||||
setupMocks()
|
||||
await mountModal()
|
||||
const title = document.body.querySelector('.ant-modal-title')
|
||||
expect(title?.textContent).toContain('testuser')
|
||||
})
|
||||
|
||||
it('open=false 不渲染内容', async () => {
|
||||
setupMocks()
|
||||
await mountModal({ open: false })
|
||||
const modal = document.body.querySelector('.ant-modal')
|
||||
expect(modal).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Scope 行操作', () => {
|
||||
it('加载后显示 scope 行', async () => {
|
||||
setupMocks()
|
||||
await mountModal()
|
||||
|
||||
// 2 scope rows rendered as flex rows
|
||||
const selects = queryBody('.ant-select')
|
||||
// Each row has 2 selects (type + entity), so expect >= 4
|
||||
expect(selects.length).toBeGreaterThanOrEqual(4)
|
||||
})
|
||||
|
||||
it('点击添加按钮增加一行', async () => {
|
||||
setupMocks({ scopes: { ...mockScopes, scopes: [] } })
|
||||
await mountModal()
|
||||
|
||||
const addBtn = Array.from(queryBody('.ant-btn-dashed')).find(
|
||||
(btn) => btn.textContent?.includes('添加数据范围'),
|
||||
) as HTMLElement
|
||||
expect(addBtn).toBeTruthy()
|
||||
addBtn.click()
|
||||
await flushPromises()
|
||||
await nextTick()
|
||||
|
||||
// Should have at least one row with selects now
|
||||
const selects = queryBody('.ant-select')
|
||||
expect(selects.length).toBeGreaterThanOrEqual(2)
|
||||
})
|
||||
|
||||
it('点击删除按钮移除行', async () => {
|
||||
setupMocks()
|
||||
await mountModal()
|
||||
|
||||
const deleteBtns = queryBody('.anticon-delete')
|
||||
const initialCount = deleteBtns.length
|
||||
expect(initialCount).toBe(2)
|
||||
|
||||
// Click first delete
|
||||
const firstDeleteBtn = deleteBtns[0]?.closest('button') as HTMLElement
|
||||
if (firstDeleteBtn) {
|
||||
firstDeleteBtn.click()
|
||||
await flushPromises()
|
||||
await nextTick()
|
||||
}
|
||||
|
||||
const afterDeleteBtns = queryBody('.anticon-delete')
|
||||
expect(afterDeleteBtns.length).toBe(initialCount - 1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('保存', () => {
|
||||
it('保存调用正确 API', async () => {
|
||||
setupMocks()
|
||||
await mountModal()
|
||||
|
||||
// Click OK button
|
||||
const okBtn = Array.from(queryBody('.ant-btn-primary')).find(
|
||||
(btn) => btn.textContent?.includes('确') || btn.textContent?.includes('OK'),
|
||||
) as HTMLElement
|
||||
if (okBtn) {
|
||||
okBtn.click()
|
||||
await flushPromises()
|
||||
}
|
||||
|
||||
expect(vi.mocked(api.put)).toHaveBeenCalledWith(
|
||||
'/api/v1/users/1/data-scope',
|
||||
{
|
||||
scopes: [
|
||||
{ scope_type: 'company', scope_id: 1 },
|
||||
{ scope_type: 'store', scope_id: 5 },
|
||||
],
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
it('保存成功显示 success message', async () => {
|
||||
setupMocks()
|
||||
const spy = vi.spyOn(message, 'success')
|
||||
await mountModal()
|
||||
|
||||
const okBtn = Array.from(queryBody('.ant-btn-primary')).find(
|
||||
(btn) => btn.textContent?.includes('确') || btn.textContent?.includes('OK'),
|
||||
) as HTMLElement
|
||||
if (okBtn) {
|
||||
okBtn.click()
|
||||
await flushPromises()
|
||||
}
|
||||
|
||||
expect(spy).toHaveBeenCalledWith('数据范围保存成功')
|
||||
})
|
||||
|
||||
it('API 失败显示 error message', async () => {
|
||||
setupMocks()
|
||||
vi.mocked(api.put).mockRejectedValueOnce(new Error('保存出错'))
|
||||
const spy = vi.spyOn(message, 'error')
|
||||
await mountModal()
|
||||
|
||||
const okBtn = Array.from(queryBody('.ant-btn-primary')).find(
|
||||
(btn) => btn.textContent?.includes('确') || btn.textContent?.includes('OK'),
|
||||
) as HTMLElement
|
||||
if (okBtn) {
|
||||
okBtn.click()
|
||||
await flushPromises()
|
||||
}
|
||||
|
||||
expect(spy).toHaveBeenCalledWith('保存出错')
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user