Files
datahub/frontend/src/components/__tests__/DataScopeModal.spec.ts
T

236 lines
6.7 KiB
TypeScript

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('保存出错')
})
})
})