236 lines
6.7 KiB
TypeScript
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('保存出错')
|
||
|
|
})
|
||
|
|
})
|
||
|
|
})
|