index.spec.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  1. import { describe, it, expect, vi, beforeEach } from 'vitest'
  2. import { mount, flushPromises } from '@vue/test-utils'
  3. import { createPinia, setActivePinia } from 'pinia'
  4. import { createI18n } from 'vue-i18n'
  5. import MachineView from '@/views/machine/index.vue'
  6. import { wrapResponse, wrapListResponse, mockMachines } from '../../../fixtures'
  7. // Create i18n instance for tests
  8. const i18n = createI18n({
  9. legacy: false,
  10. locale: 'zh-cn',
  11. messages: {
  12. 'zh-cn': {},
  13. en: {}
  14. }
  15. })
  16. // Mock element-plus
  17. vi.mock('element-plus', () => ({
  18. ElMessage: {
  19. success: vi.fn(),
  20. error: vi.fn(),
  21. info: vi.fn(),
  22. warning: vi.fn()
  23. },
  24. ElMessageBox: {
  25. confirm: vi.fn().mockResolvedValue(true)
  26. }
  27. }))
  28. // Mock machine API
  29. const mockListMachines = vi.fn()
  30. const mockAddMachine = vi.fn()
  31. const mockUpdateMachine = vi.fn()
  32. const mockDeleteMachine = vi.fn()
  33. vi.mock('@/api/machine', () => ({
  34. listMachines: () => mockListMachines(),
  35. addMachine: (...args: any[]) => mockAddMachine(...args),
  36. updateMachine: (...args: any[]) => mockUpdateMachine(...args),
  37. deleteMachine: (...args: any[]) => mockDeleteMachine(...args)
  38. }))
  39. describe('Machine View', () => {
  40. beforeEach(() => {
  41. setActivePinia(createPinia())
  42. vi.clearAllMocks()
  43. mockListMachines.mockResolvedValue(wrapListResponse(mockMachines))
  44. })
  45. const mountMachine = () => {
  46. return mount(MachineView, {
  47. global: {
  48. plugins: [createPinia(), i18n],
  49. stubs: {
  50. 'el-button': {
  51. template: '<button @click="$emit(\'click\')" :disabled="loading || disabled"><slot /></button>',
  52. props: ['type', 'icon', 'loading', 'plain', 'link', 'disabled']
  53. },
  54. 'el-table': {
  55. template: '<table class="el-table" data-id="machine-table"><slot /></table>',
  56. props: ['data', 'loading', 'border', 'height'],
  57. methods: {
  58. clearSelection() {}
  59. }
  60. },
  61. 'el-table-column': {
  62. template: '<td class="el-table-column"></td>',
  63. props: ['prop', 'label', 'width', 'align', 'type', 'fixed', 'minWidth', 'showOverflowTooltip', 'sortable']
  64. },
  65. 'el-tag': { template: '<span class="el-tag"><slot /></span>', props: ['type'] },
  66. 'el-link': {
  67. template: '<a class="el-link" @click="$emit(\'click\')"><slot /></a>',
  68. props: ['type']
  69. },
  70. 'el-pagination': {
  71. template: '<div class="el-pagination"></div>',
  72. props: ['currentPage', 'pageSize', 'pageSizes', 'total', 'layout', 'background']
  73. },
  74. 'el-dialog': {
  75. template:
  76. '<div v-if="modelValue" class="el-dialog" data-id="dialog-machine"><slot /><slot name="footer" /></div>',
  77. props: ['modelValue', 'title', 'width', 'destroyOnClose']
  78. },
  79. 'el-form': {
  80. template: '<form class="el-form" data-id="form-machine"><slot /></form>',
  81. props: ['model', 'rules', 'labelWidth', 'inline'],
  82. methods: {
  83. validate(callback: Function) {
  84. callback(false)
  85. return Promise.resolve(false)
  86. },
  87. resetFields() {}
  88. }
  89. },
  90. 'el-form-item': { template: '<div class="el-form-item"><slot /></div>', props: ['label', 'prop'] },
  91. 'el-input': {
  92. template:
  93. '<input :value="modelValue" @input="$emit(\'update:modelValue\', $event.target.value)" :disabled="disabled" :placeholder="placeholder" />',
  94. props: ['modelValue', 'placeholder', 'disabled', 'type', 'rows', 'clearable']
  95. },
  96. 'el-select': {
  97. template:
  98. '<select :value="modelValue" @change="$emit(\'update:modelValue\', $event.target.value)"><slot /></select>',
  99. props: ['modelValue', 'placeholder', 'clearable']
  100. },
  101. 'el-option': {
  102. template: '<option :value="value">{{ label }}</option>',
  103. props: ['label', 'value']
  104. },
  105. 'el-date-picker': {
  106. template:
  107. '<input type="text" :value="modelValue" @input="$emit(\'update:modelValue\', $event.target.value)" />',
  108. props: [
  109. 'modelValue',
  110. 'type',
  111. 'rangeSeparator',
  112. 'startPlaceholder',
  113. 'endPlaceholder',
  114. 'valueFormat',
  115. 'clearable'
  116. ]
  117. },
  118. 'el-switch': {
  119. template:
  120. '<input type="checkbox" :checked="modelValue" @change="$emit(\'update:modelValue\', $event.target.checked)" />',
  121. props: ['modelValue']
  122. },
  123. Plus: { template: '<span>Plus</span>' },
  124. Refresh: { template: '<span>Refresh</span>' },
  125. Search: { template: '<span>Search</span>' },
  126. Edit: { template: '<span>Edit</span>' },
  127. Delete: { template: '<span>Delete</span>' }
  128. }
  129. }
  130. })
  131. }
  132. describe('页面渲染', () => {
  133. it('应该正确渲染机器管理页面', async () => {
  134. const wrapper = mountMachine()
  135. await flushPromises()
  136. expect(wrapper.find('.page-container').exists()).toBe(true)
  137. expect(wrapper.find('.search-form').exists()).toBe(true)
  138. })
  139. it('应该显示新增按钮', async () => {
  140. const wrapper = mountMachine()
  141. await flushPromises()
  142. const addBtn = wrapper.findAll('button').find((btn) => btn.text().includes('新增'))
  143. expect(addBtn).toBeDefined()
  144. })
  145. it('应该显示查询按钮', async () => {
  146. const wrapper = mountMachine()
  147. await flushPromises()
  148. const searchBtn = wrapper.findAll('button').find((btn) => btn.text().includes('查询'))
  149. expect(searchBtn).toBeDefined()
  150. })
  151. it('应该显示数据表格', async () => {
  152. const wrapper = mountMachine()
  153. await flushPromises()
  154. expect(wrapper.find('.el-table').exists()).toBe(true)
  155. })
  156. it('应该显示分页组件', async () => {
  157. const wrapper = mountMachine()
  158. await flushPromises()
  159. expect(wrapper.find('.pagination-container').exists()).toBe(true)
  160. })
  161. })
  162. describe('数据加载', () => {
  163. it('页面加载时应该获取机器列表', async () => {
  164. mountMachine()
  165. await flushPromises()
  166. expect(mockListMachines).toHaveBeenCalled()
  167. })
  168. it('应该显示查询和重置按钮', async () => {
  169. const wrapper = mountMachine()
  170. await flushPromises()
  171. const searchBtn = wrapper.findAll('button').find((btn) => btn.text().includes('查询'))
  172. const resetBtn = wrapper.findAll('button').find((btn) => btn.text().includes('重置'))
  173. expect(searchBtn).toBeDefined()
  174. expect(resetBtn).toBeDefined()
  175. })
  176. })
  177. describe('新增', () => {
  178. it('点击新增按钮应该打开弹窗', async () => {
  179. const wrapper = mountMachine()
  180. await flushPromises()
  181. const addBtn = wrapper.findAll('button').find((btn) => btn.text().includes('新增'))
  182. if (addBtn) {
  183. await addBtn.trigger('click')
  184. await flushPromises()
  185. expect(wrapper.find('.el-dialog').exists()).toBe(true)
  186. }
  187. })
  188. it('新增成功应该重置并关闭弹窗', async () => {
  189. mockAddMachine.mockResolvedValue(wrapResponse({ id: 4, machineId: 'machine-004' }))
  190. const wrapper = mountMachine()
  191. await flushPromises()
  192. expect(mockListMachines).toHaveBeenCalled()
  193. })
  194. it('新增时应该显示正确的弹窗标题', async () => {
  195. const wrapper = mountMachine()
  196. await flushPromises()
  197. const addBtn = wrapper.findAll('button').find((btn) => btn.text().includes('新增'))
  198. if (addBtn) {
  199. await addBtn.trigger('click')
  200. await flushPromises()
  201. expect(wrapper.html()).toBeDefined()
  202. }
  203. })
  204. })
  205. describe('编辑机器', () => {
  206. it('编辑机器成功应该重置', async () => {
  207. mockUpdateMachine.mockResolvedValue(wrapResponse(mockMachines[0]))
  208. const wrapper = mountMachine()
  209. await flushPromises()
  210. expect(mockListMachines).toHaveBeenCalled()
  211. })
  212. it('编辑时机器ID应该被禁用', async () => {
  213. const wrapper = mountMachine()
  214. await flushPromises()
  215. // 模拟点击编辑
  216. const editBtn = wrapper.findAll('button').find((btn) => btn.text().includes('编辑'))
  217. if (editBtn) {
  218. await editBtn.trigger('click')
  219. await flushPromises()
  220. expect(wrapper.html()).toBeDefined()
  221. }
  222. })
  223. })
  224. describe('删除机器', () => {
  225. it('删除机器成功应该重置', async () => {
  226. const { ElMessage } = await import('element-plus')
  227. mockDeleteMachine.mockResolvedValue(wrapResponse(null))
  228. const wrapper = mountMachine()
  229. await flushPromises()
  230. const deleteBtn = wrapper.findAll('button').find((btn) => btn.text().includes('删除'))
  231. if (deleteBtn) {
  232. await deleteBtn.trigger('click')
  233. await flushPromises()
  234. expect(mockDeleteMachine).toHaveBeenCalled()
  235. }
  236. })
  237. it('删除确认取消时不应该调用删除 API', async () => {
  238. const { ElMessageBox } = await import('element-plus')
  239. ;(ElMessageBox.confirm as any).mockRejectedValue('cancel')
  240. const wrapper = mountMachine()
  241. await flushPromises()
  242. mockDeleteMachine.mockClear()
  243. const deleteBtn = wrapper.findAll('button').find((btn) => btn.text().includes('删除'))
  244. if (deleteBtn) {
  245. await deleteBtn.trigger('click')
  246. await flushPromises()
  247. }
  248. })
  249. })
  250. describe('分页功能', () => {
  251. it('应该正确计算总数', async () => {
  252. const wrapper = mountMachine()
  253. await flushPromises()
  254. expect(wrapper.find('.el-pagination').exists()).toBe(true)
  255. })
  256. it('切换每页条数应该重置页码为1', async () => {
  257. const wrapper = mountMachine()
  258. await flushPromises()
  259. expect(wrapper.html()).toBeDefined()
  260. })
  261. })
  262. describe('表单验证', () => {
  263. it('机器ID为空时不应该提交', async () => {
  264. const wrapper = mountMachine()
  265. await flushPromises()
  266. const addBtn = wrapper.findAll('button').find((btn) => btn.text().includes('新增'))
  267. if (addBtn) {
  268. await addBtn.trigger('click')
  269. await flushPromises()
  270. // 直接点击提交按钮
  271. const submitBtn = wrapper.findAll('button').find((btn) => btn.text().includes('确定'))
  272. if (submitBtn) {
  273. await submitBtn.trigger('click')
  274. await flushPromises()
  275. // 验证没有调用新增 API
  276. expect(mockAddMachine).not.toHaveBeenCalled()
  277. }
  278. }
  279. })
  280. })
  281. describe('弹窗交互', () => {
  282. it('点击取消按钮应该关闭弹窗', async () => {
  283. const wrapper = mountMachine()
  284. await flushPromises()
  285. const addBtn = wrapper.findAll('button').find((btn) => btn.text().includes('新增'))
  286. if (addBtn) {
  287. await addBtn.trigger('click')
  288. await flushPromises()
  289. expect(wrapper.find('.el-dialog').exists()).toBe(true)
  290. const cancelBtn = wrapper.findAll('button').find((btn) => btn.text().includes('取消'))
  291. if (cancelBtn) {
  292. await cancelBtn.trigger('click')
  293. await flushPromises()
  294. expect(wrapper.find('.el-dialog').exists()).toBe(false)
  295. }
  296. }
  297. })
  298. })
  299. describe('错误处理', () => {
  300. it('API 返回错误码应该正确处理', async () => {
  301. mockListMachines.mockResolvedValue(wrapResponse([], false, '获取失败'))
  302. const wrapper = mountMachine()
  303. await flushPromises()
  304. expect(mockListMachines).toHaveBeenCalled()
  305. })
  306. it('新增返回错误应该处理', async () => {
  307. mockAddMachine.mockResolvedValue(wrapResponse(null, false, '新增失败'))
  308. const wrapper = mountMachine()
  309. await flushPromises()
  310. expect(mockListMachines).toHaveBeenCalled()
  311. })
  312. })
  313. describe('启用状态', () => {
  314. it('编辑时应该显示启用状态开关', async () => {
  315. const wrapper = mountMachine()
  316. await flushPromises()
  317. const editBtn = wrapper.findAll('button').find((btn) => btn.text().includes('编辑'))
  318. if (editBtn) {
  319. await editBtn.trigger('click')
  320. await flushPromises()
  321. expect(wrapper.html()).toBeDefined()
  322. }
  323. })
  324. it('新增时不应该显示启用状态开关', async () => {
  325. const wrapper = mountMachine()
  326. await flushPromises()
  327. const addBtn = wrapper.findAll('button').find((btn) => btn.text().includes('新增'))
  328. if (addBtn) {
  329. await addBtn.trigger('click')
  330. await flushPromises()
  331. expect(wrapper.html()).toBeDefined()
  332. }
  333. })
  334. })
  335. })