From b96cad9683fb04de6e1b4aa59bba29bf944ece9f Mon Sep 17 00:00:00 2001 From: cat-shark <1716967236@qq.com> Date: Thu, 27 Aug 2026 22:25:59 +0800 Subject: [PATCH] =?UTF-8?q?test:=20=E8=A1=A5=E9=BD=90=E5=85=B3=E9=94=AE?= =?UTF-8?q?=E8=B7=AF=E5=BE=84=E4=BA=A4=E4=BA=92=E5=BC=8F=E9=9B=86=E6=88=90?= =?UTF-8?q?=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/__tests__/integration/login-flow.spec.ts | 71 +++++++++ .../integration/review-recall-flow.spec.ts | 127 +++++++++++++++ .../integration/start-task-flow.spec.ts | 150 ++++++++++++++++++ .../integration/task-form-flow.spec.ts | 98 ++++++++++++ 4 files changed, 446 insertions(+) create mode 100644 src/__tests__/integration/login-flow.spec.ts create mode 100644 src/__tests__/integration/review-recall-flow.spec.ts create mode 100644 src/__tests__/integration/start-task-flow.spec.ts create mode 100644 src/__tests__/integration/task-form-flow.spec.ts diff --git a/src/__tests__/integration/login-flow.spec.ts b/src/__tests__/integration/login-flow.spec.ts new file mode 100644 index 0000000..cae8c9b --- /dev/null +++ b/src/__tests__/integration/login-flow.spec.ts @@ -0,0 +1,71 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { flushPromises, mount } from '@vue/test-utils' +import ElementPlus, { ElMessage } from 'element-plus' +import Login from '@/components/Login.vue' +import router from '@/router' +import { login } from '@/api/login' + +vi.mock('@/api/login', () => ({ + login: vi.fn(), + logout: vi.fn(), +})) + +/** + * 交互式集成测试:真实挂载 Login,通过 setValue / trigger('click') 模拟用户行为, + * 断言提交链路(凭证传递、成功/失败分支、localStorage 与路由结果)。 + * + * 注意:jsdom 下 Element Plus 的 callback 式表单校验不可靠(空表单也可能判有效), + * “空账号被校验拦截”这类行为由 e2e/login.spec 在真实浏览器中覆盖,此处不重复断言。 + */ +describe('Login 交互流程(集成)', () => { + beforeEach(async () => { + vi.clearAllMocks() + localStorage.clear() + await router.push('/login') + await router.isReady() + }) + + const mountLogin = async () => { + const wrapper = mount(Login, { + global: { plugins: [ElementPlus, router] }, + }) + await flushPromises() + return wrapper + } + + const findSubmitButton = (wrapper: ReturnType extends Promise ? T : never) => + wrapper.findAll('button').find((b) => b.text().includes('进入系统'))! + + it('填写账号密码后点击“进入系统”,提交凭证并跳转 /welcome', async () => { + vi.mocked(login).mockResolvedValue({ code: 200, message: '登录成功' } as any) + const wrapper = await mountLogin() + + await wrapper.find('input[placeholder="请输入账号"]').setValue('admin') + await wrapper.find('input[placeholder="请输入密码"]').setValue('123456') + await findSubmitButton(wrapper).trigger('click') + // /welcome 是懒加载路由,动态 import 需要宏任务,用 waitFor 轮询等待到达 + await vi.waitFor(() => expect(router.currentRoute.value.path).toBe('/welcome')) + + expect(login).toHaveBeenCalledWith('admin', '123456') + expect(localStorage.getItem('isLoggedIn')).toBe('true') + expect(ElMessage.success).toHaveBeenCalledWith('登录成功') + wrapper.unmount() + }) + + it('后端返回失败时提示错误、不写登录标记、不跳转', async () => { + vi.mocked(login).mockResolvedValue({ code: 500, message: '账号或密码错误' } as any) + const wrapper = await mountLogin() + + await wrapper.find('input[placeholder="请输入账号"]').setValue('admin') + await wrapper.find('input[placeholder="请输入密码"]').setValue('wrong-pass') + await findSubmitButton(wrapper).trigger('click') + await flushPromises() + await flushPromises() + + expect(login).toHaveBeenCalledWith('admin', 'wrong-pass') + expect(ElMessage.error).toHaveBeenCalledWith('账号或密码错误') + expect(localStorage.getItem('isLoggedIn')).toBeNull() + expect(router.currentRoute.value.path).toBe('/login') + wrapper.unmount() + }) +}) diff --git a/src/__tests__/integration/review-recall-flow.spec.ts b/src/__tests__/integration/review-recall-flow.spec.ts new file mode 100644 index 0000000..24b3066 --- /dev/null +++ b/src/__tests__/integration/review-recall-flow.spec.ts @@ -0,0 +1,127 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { flushPromises, mount } from '@vue/test-utils' +import ElementPlus, { ElMessage } from 'element-plus' +import { createMemoryHistory, createRouter } from 'vue-router' +import ReviewRecall from '@/components/ReviewRecall.vue' +import { + getStandardMindMap, + listRecallRecords, + recallCompare, +} from '@/api/standardMindMap' + +vi.mock('@/api/standardMindMap', () => ({ + findNode: vi.fn(), + getStandardMindMap: vi.fn(), + listRecallRecords: vi.fn(), + recallCompare: vi.fn(), + regenerateStandardMindMap: vi.fn(), + updateStandardMindMap: vi.fn(), +})) + +const ok = (data: any = null) => ({ code: 200, message: '请求成功', data }) + +const standardMap = { + id: 1, + taskNum: 'T001', + title: '测试任务', + content: JSON.stringify({ title: '根主题', children: [{ title: '子节点' }] }), + outline: '根主题\n- 子节点', + summary: '', + generator: 'AI', + sourceReportCount: 1, +} + +/** MindMapViewer 依赖 mind-elixir(无法在 jsdom 运行),用可编程 stub 模拟导出的大纲 */ +const toOutline = vi.fn((): string => '根主题\n- 子节点') +const MindMapViewerStub = { + name: 'MindMapViewerStub', + props: ['modelValue', 'nodeSelectable', 'colorByCompare', 'readonly'], + template: '
', + methods: { toOutline }, +} + +/** + * 交互式集成测试:真实挂载 ReviewRecall, + * 点击“提交对比”模拟回忆对比流,断言 API 参数与对比结果渲染。 + */ +describe('ReviewRecall 交互流程(集成)', () => { + beforeEach(() => { + vi.clearAllMocks() + localStorage.clear() + localStorage.setItem('isLoggedIn', 'true') + + vi.mocked(getStandardMindMap).mockResolvedValue(ok(standardMap)) + vi.mocked(listRecallRecords).mockResolvedValue( + ok([ + { + id: 11, + taskNum: 'T001', + standardMapId: 1, + recallContent: '根主题', + compareResult: JSON.stringify({ + matchedTree: { title: '根主题', children: [] }, + recallRatio: 0.6, + matchedCount: 3, + missedCount: 2, + extraCount: 0, + }), + recallRatio: 0.6, + matchedCount: 3, + missedCount: 2, + extraCount: 0, + createdTime: '2026-08-27 10:00:00', + }, + ]), + ) + vi.mocked(recallCompare).mockResolvedValue(ok(standardMap)) + }) + + const mountPage = async () => { + const router = createRouter({ + history: createMemoryHistory(), + routes: [{ path: '/review/recall/:taskNum', component: ReviewRecall, meta: { wide: true } }], + }) + await router.push('/review/recall/T001') + await router.isReady() + + const wrapper = mount(ReviewRecall, { + global: { + plugins: [ElementPlus, router], + stubs: { ElTag: true, MindMapViewer: MindMapViewerStub }, + }, + }) + await flushPromises() + return { wrapper } + } + + it('加载后展示标准导图标题,点击“提交对比”调用对比接口并渲染结果', async () => { + const { wrapper } = await mountPage() + // 默认折叠标准导图(防剧透),回忆面板可见 + expect(wrapper.text()).toContain('你的回忆') + const submit = wrapper.findAll('button').find((b) => b.text().trim() === '提交对比')! + expect(submit).toBeTruthy() + await submit.trigger('click') + await flushPromises() + await flushPromises() + + expect(recallCompare).toHaveBeenCalledWith('T001', '根主题\n- 子节点', undefined) + expect(ElMessage.success).toHaveBeenCalledWith('回忆对比完成') + // 对比完成后出现“清空重写”入口(v-if=hasCompared) + expect(wrapper.findAll('button').some((b) => b.text().trim() === '清空重写')).toBe(true) + wrapper.unmount() + }) + + it('回忆导图内容为单行时点击“提交对比”,提示先添加节点且不调接口', async () => { + const { wrapper } = await mountPage() + toOutline.mockReturnValue('只有一个根') + + const submit = wrapper.findAll('button').find((b) => b.text().trim() === '提交对比')! + await submit.trigger('click') + await flushPromises() + + const { ElMessage: Msg } = await import('element-plus') + expect(Msg.warning).toHaveBeenCalledWith('请先在回忆导图中添加节点(选中节点后按 Tab 加子节点)') + expect(recallCompare).not.toHaveBeenCalled() + wrapper.unmount() + }) +}) diff --git a/src/__tests__/integration/start-task-flow.spec.ts b/src/__tests__/integration/start-task-flow.spec.ts new file mode 100644 index 0000000..1103b54 --- /dev/null +++ b/src/__tests__/integration/start-task-flow.spec.ts @@ -0,0 +1,150 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { flushPromises, mount } from '@vue/test-utils' +import ElementPlus from 'element-plus' +import { createMemoryHistory, createRouter } from 'vue-router' +import StartTask from '@/components/StartTask.vue' +import { + continueSession, + endSession, + getActiveSession, + getExpectation, + getReportDraft, + getSessionDetail, + pauseSession, + startOrContinueStudySession, +} from '@/api/studySessions' +import { getFragmentsBySession } from '@/api/reportFragments' + +vi.mock('@/api/studySessions', () => ({ + abortSession: vi.fn(), + continueSession: vi.fn(), + endSession: vi.fn(), + getActiveSession: vi.fn(), + getExpectation: vi.fn(), + getReportDraft: vi.fn(), + getSessionDetail: vi.fn(), + getTaskFragments: vi.fn(), + getTaskReports: vi.fn(), + pauseSession: vi.fn(), + startOrContinueStudySession: vi.fn(), + upsertExpectation: vi.fn(), +})) + +vi.mock('@/api/reportFragments', () => ({ + createFragments: vi.fn(), + getFragmentsBySession: vi.fn(), + updateFragments: vi.fn(), +})) + +const sessionOngoing = { + sessionNum: 'SESSION_001', + sessionState: 'ONGOING', + taskName: '测试任务', + taskNum: 'T001', + taskId: 1, + startTime: '2026-08-27 10:00:00', + endTime: '', + lastStartTime: '2026-08-27 10:00:00', + actualTime: 0, + effectiveTime: 0, + effectivenessRatio: '--', + pointerPosition: 1_500_000, + systemMessage: '', +} + +const ok = (data: any = null) => ({ code: 200, message: '请求成功', data }) + +/** + * 交互式集成测试:真实挂载 StartTask(不 shallow), + * 通过 trigger('click') 模拟“暂停 → 开始”完整用户流,断言 DOM 状态与 API 调用序列。 + */ +describe('StartTask 交互流程(集成)', () => { + beforeEach(() => { + vi.clearAllMocks() + localStorage.clear() + localStorage.setItem('isLoggedIn', 'true') + + vi.mocked(getActiveSession).mockResolvedValue(ok(null)) + vi.mocked(startOrContinueStudySession).mockResolvedValue(ok(sessionOngoing)) + vi.mocked(getExpectation).mockResolvedValue(ok({ description: '本次学习预期' })) + vi.mocked(getFragmentsBySession).mockResolvedValue(ok([])) + vi.mocked(pauseSession).mockResolvedValue(ok()) + vi.mocked(continueSession).mockResolvedValue(ok()) + }) + + const mountPage = async () => { + const router = createRouter({ + history: createMemoryHistory(), + routes: [ + { path: '/start-task/:taskNum', component: StartTask }, + { path: '/study', component: { template: '
' } }, + ], + }) + await router.push('/start-task/T001') + await router.isReady() + + const wrapper = mount(StartTask, { + global: { + plugins: [ElementPlus, router], + // el-tag 在 jsdom + VTU 下 vnode mounted 钩子崩溃(EP 2.8 已知问题),纯视觉组件,stub 掉 + stubs: { ElTag: true }, + }, + }) + await flushPromises() + return { wrapper, router } + } + + const findButton = (wrapper: any, text: string) => + wrapper.findAll('button').find((b: any) => b.text().trim() === text) + + it('进行中的会话:点击“暂停”同步后端状态,再点“开始”触发 continue 并恢复进行中', async () => { + // 暂停后从详情接口同步回 PAUSED 状态 + vi.mocked(getSessionDetail).mockResolvedValue( + ok({ ...sessionOngoing, sessionState: 'PAUSED', actualTime: 10, effectiveTime: 20 }), + ) + + const { wrapper } = await mountPage() + expect(wrapper.find('.status-text').text()).toContain('进行中') + + // 点击暂停:调用 pause API,页面状态变为已暂停 + const pauseBtn = findButton(wrapper, '暂停') + expect(pauseBtn).toBeTruthy() + await pauseBtn.trigger('click') + await flushPromises() + + expect(pauseSession).toHaveBeenCalledWith('SESSION_001') + expect(wrapper.find('.status-text').text()).toContain('已暂停') + + // 点击开始:会话为 PAUSED,应触发 continue API 并恢复进行中 + const startBtn = findButton(wrapper, '开始') + expect(startBtn).toBeTruthy() + await startBtn.trigger('click') + await flushPromises() + + expect(continueSession).toHaveBeenCalledWith('SESSION_001') + expect(wrapper.find('.status-text').text()).toContain('进行中') + wrapper.unmount() + }) + + it('点击“结束会话”打开总结弹窗,无内容时确认结束被拦截', async () => { + const { wrapper } = await mountPage() + + const endBtn = findButton(wrapper, '结束会话') + await endBtn.trigger('click') + await flushPromises() + + // 总结弹窗出现(无残片 → 不触发 AI 草稿等待) + expect(wrapper.text()).toContain('结束会话总结') + expect(getReportDraft).not.toHaveBeenCalled() + + // 总结内容为空时点击确认结束:弹警示、不调接口 + const confirmEnd = findButton(wrapper, '确认结束') + await confirmEnd.trigger('click') + await flushPromises() + + const { ElMessage } = await import('element-plus') + expect(ElMessage.warning).toHaveBeenCalledWith('请输入学习总结内容') + expect(endSession).not.toHaveBeenCalled() + wrapper.unmount() + }) +}) diff --git a/src/__tests__/integration/task-form-flow.spec.ts b/src/__tests__/integration/task-form-flow.spec.ts new file mode 100644 index 0000000..4b6a9dd --- /dev/null +++ b/src/__tests__/integration/task-form-flow.spec.ts @@ -0,0 +1,98 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { flushPromises, mount } from '@vue/test-utils' +import ElementPlus, { ElMessage } from 'element-plus' +import { createMemoryHistory, createRouter } from 'vue-router' +import TaskForm from '@/components/TaskForm.vue' +import request from '@/utils/request' + +vi.mock('@/utils/request', () => ({ + default: { + get: vi.fn().mockResolvedValue({ code: 200, data: null }), + post: vi.fn(), + put: vi.fn().mockResolvedValue({ code: 200, data: null }), + del: vi.fn().mockResolvedValue({ code: 200, data: null }), + }, +})) + +const mockedPost = vi.mocked(request.post) + +/** + * 交互式集成测试:真实挂载 TaskForm(add 模式), + * 填写任务名称 → 点击“添加”提交 → 断言请求载荷、成功提示与路由跳转。 + */ +describe('TaskForm 交互流程(集成)', () => { + beforeEach(() => { + vi.clearAllMocks() + localStorage.clear() + localStorage.setItem('isLoggedIn', 'true') + }) + + const mountAddForm = async () => { + const router = createRouter({ + history: createMemoryHistory(), + routes: [ + { + path: '/add-task', + name: 'add-task', + component: TaskForm, + meta: { requiresAuth: true, title: '创建学习任务', action: 'add', buttonText: '添加' }, + }, + { path: '/study', name: 'study', component: { template: '
' } }, + ], + }) + await router.push('/add-task') + await router.isReady() + + const wrapper = mount(TaskForm, { + global: { + plugins: [ElementPlus, router], + // el-tag 在 jsdom + VTU 下 vnode mounted 钩子崩溃(EP 2.8 已知问题),纯视觉组件,stub 掉 + stubs: { ElTag: true }, + }, + }) + await flushPromises() + return { wrapper, router } + } + + it('填写任务名称后点击“添加”,提交 POST /tasks 并跳转学习任务页', async () => { + mockedPost.mockResolvedValue({ code: 200, data: null } as any) + const { wrapper, router } = await mountAddForm() + + const nameInput = wrapper.find('input[placeholder="例如:Vue3 组件通信实践"]') + expect(nameInput.exists()).toBe(true) + await nameInput.setValue('集成测试任务') + + const submit = wrapper.findAll('button').find((b) => b.text().trim() === '添加')! + expect(submit).toBeTruthy() + await submit.trigger('click') + await flushPromises() + await flushPromises() + + expect(mockedPost).toHaveBeenCalledWith( + '/tasks', + expect.objectContaining({ + taskName: '集成测试任务', + taskDescription: '', + materialUrl: '', + }), + ) + expect(ElMessage.success).toHaveBeenCalledWith('创建任务成功') + expect(router.currentRoute.value.path).toBe('/study') + wrapper.unmount() + }) + + it('后端返回失败时提示错误、不跳转', async () => { + mockedPost.mockResolvedValue({ code: 500, message: '任务名称重复' } as any) + const { wrapper, router } = await mountAddForm() + + await wrapper.find('input[placeholder="例如:Vue3 组件通信实践"]').setValue('重复任务') + const submit = wrapper.findAll('button').find((b) => b.text().trim() === '添加')! + await submit.trigger('click') + await flushPromises() + await flushPromises() + + expect(ElMessage.error).toHaveBeenCalledWith('任务创建失败:任务名称重复') + expect(router.currentRoute.value.path).toBe('/add-task') + wrapper.unmount() + }) +})