2026-03-18 15:25:17 +08:00
|
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
|
|
|
import { mount, flushPromises } from '@vue/test-utils'
|
|
|
|
|
import { nextTick } from 'vue'
|
|
|
|
|
import UserFormModal from '@/components/UserFormModal.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: {
|
|
|
|
|
post: vi.fn(),
|
|
|
|
|
put: vi.fn(),
|
|
|
|
|
},
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
const mockUserData = {
|
|
|
|
|
id: 1,
|
|
|
|
|
username: 'testuser',
|
|
|
|
|
email: 'test@test.com',
|
|
|
|
|
status: 1,
|
|
|
|
|
role_id: 1,
|
|
|
|
|
ext: null,
|
|
|
|
|
created_at: '2026-01-01T00:00:00Z',
|
|
|
|
|
updated_at: '2026-01-01T00:00:00Z',
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
describe('UserFormModal', () => {
|
|
|
|
|
let wrapper: ReturnType<typeof mount>
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
vi.restoreAllMocks()
|
|
|
|
|
document.body.innerHTML = ''
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
wrapper?.unmount()
|
|
|
|
|
document.body.innerHTML = ''
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
async function mountModal(props = {}) {
|
|
|
|
|
wrapper = mount(UserFormModal, {
|
|
|
|
|
props: {
|
|
|
|
|
open: true,
|
|
|
|
|
mode: 'create' as const,
|
|
|
|
|
userData: null,
|
|
|
|
|
...props,
|
|
|
|
|
},
|
|
|
|
|
attachTo: document.body,
|
|
|
|
|
})
|
|
|
|
|
await flushPromises()
|
|
|
|
|
await nextTick()
|
|
|
|
|
return wrapper
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 查找 teleported 到 body 的模态框内容
|
|
|
|
|
function queryBody(selector: string) {
|
|
|
|
|
return document.body.querySelectorAll(selector)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
describe('create mode', () => {
|
|
|
|
|
it('renders create title', async () => {
|
|
|
|
|
await mountModal()
|
|
|
|
|
const title = document.body.querySelector('.ant-modal-title')
|
|
|
|
|
expect(title?.textContent).toBe('新建用户')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('shows password field in create mode', async () => {
|
|
|
|
|
await mountModal()
|
|
|
|
|
const labels = Array.from(queryBody('.ant-form-item label'))
|
|
|
|
|
.map((el) => el.textContent?.trim())
|
|
|
|
|
expect(labels).toContain('密码')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('shows all four form fields', async () => {
|
|
|
|
|
await mountModal()
|
|
|
|
|
const formItems = queryBody('.ant-form-item')
|
|
|
|
|
expect(formItems).toHaveLength(4) // username, password, email, status
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('emits update:open false on cancel', async () => {
|
|
|
|
|
const w = await mountModal()
|
|
|
|
|
// 找到取消按钮(模态框 footer 中非 primary 按钮)
|
|
|
|
|
const buttons = Array.from(queryBody('.ant-modal-footer .ant-btn'))
|
|
|
|
|
const cancelBtn = buttons.find((b) => !b.classList.contains('ant-btn-primary'))
|
|
|
|
|
cancelBtn?.dispatchEvent(new Event('click'))
|
|
|
|
|
await flushPromises()
|
|
|
|
|
expect(w.emitted('update:open')?.[0]).toEqual([false])
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('edit mode', () => {
|
|
|
|
|
it('renders edit title', async () => {
|
|
|
|
|
await mountModal({ mode: 'edit', userData: mockUserData })
|
|
|
|
|
const title = document.body.querySelector('.ant-modal-title')
|
|
|
|
|
expect(title?.textContent).toBe('编辑用户')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('hides password field in edit mode', async () => {
|
|
|
|
|
await mountModal({ mode: 'edit', userData: mockUserData })
|
|
|
|
|
const labels = Array.from(queryBody('.ant-form-item label'))
|
|
|
|
|
.map((el) => el.textContent?.trim())
|
|
|
|
|
expect(labels).not.toContain('密码')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('shows three form fields (no password)', async () => {
|
|
|
|
|
await mountModal({ mode: 'edit', userData: mockUserData })
|
|
|
|
|
const formItems = queryBody('.ant-form-item')
|
|
|
|
|
expect(formItems).toHaveLength(3) // username, email, status
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('prefills form with user data', async () => {
|
|
|
|
|
// 先以 open=false 挂载,再切换为 true 触发 watch 预填数据(与实际使用流程一致)
|
|
|
|
|
const w = await mountModal({ mode: 'edit', userData: mockUserData, open: false })
|
|
|
|
|
await w.setProps({ open: true })
|
|
|
|
|
await flushPromises()
|
|
|
|
|
await nextTick()
|
|
|
|
|
|
|
|
|
|
const inputs = Array.from(queryBody('input')) as HTMLInputElement[]
|
|
|
|
|
const values = inputs.map((i) => i.value)
|
|
|
|
|
expect(values).toContain('testuser')
|
|
|
|
|
expect(values).toContain('test@test.com')
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('form submission', () => {
|
|
|
|
|
it('calls api.post for create mode', async () => {
|
|
|
|
|
const { api } = await import('@/utils/request')
|
|
|
|
|
vi.mocked(api.post).mockResolvedValueOnce({})
|
|
|
|
|
|
|
|
|
|
await mountModal()
|
|
|
|
|
|
|
|
|
|
// 填充表单
|
|
|
|
|
const inputs = Array.from(queryBody('.ant-form-item input')) as HTMLInputElement[]
|
|
|
|
|
expect(inputs.length).toBeGreaterThanOrEqual(3)
|
|
|
|
|
inputs[0]!.value = 'newuser'
|
|
|
|
|
inputs[0]!.dispatchEvent(new Event('input'))
|
|
|
|
|
inputs[1]!.value = 'password123'
|
|
|
|
|
inputs[1]!.dispatchEvent(new Event('input'))
|
|
|
|
|
inputs[2]!.value = 'new@test.com'
|
|
|
|
|
inputs[2]!.dispatchEvent(new Event('input'))
|
|
|
|
|
await flushPromises()
|
|
|
|
|
|
|
|
|
|
// 点击确定按钮
|
|
|
|
|
const okBtn = document.body.querySelector('.ant-modal-footer .ant-btn-primary') as HTMLElement
|
|
|
|
|
okBtn?.click()
|
|
|
|
|
await flushPromises()
|
|
|
|
|
})
|
|
|
|
|
|
2026-03-18 15:51:34 +08:00
|
|
|
it('calls api.put for edit mode with status field', async () => {
|
2026-03-18 15:25:17 +08:00
|
|
|
const { api } = await import('@/utils/request')
|
|
|
|
|
vi.mocked(api.put).mockResolvedValueOnce({})
|
|
|
|
|
|
2026-03-18 15:51:34 +08:00
|
|
|
// 先以 open=false 挂载,再切换为 true 触发 watch 预填数据,使表单验证通过
|
|
|
|
|
const w = await mountModal({ mode: 'edit', userData: mockUserData, open: false })
|
|
|
|
|
await w.setProps({ open: true })
|
|
|
|
|
await flushPromises()
|
|
|
|
|
await nextTick()
|
2026-03-18 15:25:17 +08:00
|
|
|
|
|
|
|
|
// 点击确定按钮
|
|
|
|
|
const okBtn = document.body.querySelector('.ant-modal-footer .ant-btn-primary') as HTMLElement
|
|
|
|
|
expect(okBtn).toBeTruthy()
|
|
|
|
|
okBtn?.click()
|
|
|
|
|
await flushPromises()
|
2026-03-18 15:51:34 +08:00
|
|
|
|
|
|
|
|
// 验证 PUT payload 包含 status 且不包含 ext
|
|
|
|
|
expect(api.put).toHaveBeenCalledOnce()
|
2026-03-18 20:08:04 +08:00
|
|
|
const [url, payload] = vi.mocked(api.put).mock.calls[0]!
|
2026-03-18 15:51:34 +08:00
|
|
|
expect(url).toContain('/api/v1/users/')
|
|
|
|
|
expect(payload).toHaveProperty('status')
|
|
|
|
|
expect(payload).not.toHaveProperty('ext')
|
2026-03-18 15:25:17 +08:00
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
})
|