/** * 测试文件: 检查页面 - 退出检查逻辑 * 功能: 验证从检查页面退出时的各种场景和逻辑处理 * * 测试场景: * 1. 未曝光状态直接退出 * 2. 半曝光状态 - 继续检查 * 3. 半曝光状态 - 保存并完成 * 4. 半曝光状态 - 直接中止 * 5. 全曝光状态退出 * 6. 发生器采集中阻止退出到process * 7. 从检查正常退出到process(非采集) * 8. 边界场景:快速连续点击退出 * * 相关需求文档: docs/实现/退出检查的逻辑.md */ import LoginPage from '../../support/pageObjects/LoginPage'; import MainPage from '../../support/pageObjects/MainPage'; import WorklistPage from '../../support/pageObjects/WorklistPage'; describe('检查页面:退出检查逻辑', () => { const loginPage = new LoginPage(); const mainPage = new MainPage(); const worklistPage = new WorklistPage(); /** * 通用的登录和进入检查页面的前置步骤 */ beforeEach(() => { // 登录系统 loginPage.visit(); loginPage.login('admin', '123456'); cy.contains('登录成功').should('be.visible', { timeout: 10000 }); // 导航到工作列表 mainPage.clickPatientManagementButton(); mainPage.clickWorklistButton(); // 双击第一行进入检查页面 worklistPage.findTableAndDoubleClickFirstRow(); cy.wait(1000); // 验证已进入检查页面 cy.get('[data-testid="exam-page"]').should('exist'); }); /** * 测试场景 1:未曝光状态直接退出 * * Given: 用户已登录系统并从工作列表进入检查页面 * 未进行任何曝光操作 * When: 用户点击导航按钮切换到工作列表 * Then: 应该直接退出检查页面,不显示退出反馈弹窗 */ it('场景1:未曝光状态应该直接退出检查', () => { /** * Given: 验证当前在检查页面 */ cy.get('[data-testid="exam-page"]').should('exist'); /** * Given: 验证曝光状态为 "Not Exposed" */ cy.window().its('store').invoke('getState') .its('bodyPositionList') .its('exposureStatus') .should('equal', 'Not Exposed'); /** * When: 点击导航按钮切换到工作列表 */ mainPage.clickPatientManagementButton(); mainPage.clickWorklistButton(); /** * Then: 验证成功退出到工作列表页面 */ cy.url().should('include', '/worklist'); /** * Then: 验证未显示退出反馈弹窗 */ cy.get('[role="dialog"]').should('not.exist'); /** * Then: 验证Redux store中业务流程已切换 */ cy.window().its('store').invoke('getState') .its('BusinessFlow') .its('currentKey') .should('equal', 'worklist'); }); /** * 测试场景 2:半曝光状态 - 继续检查 * * Given: 用户已登录系统并进入检查页面 * 已完成部分体位的曝光 * When: 用户点击退出,显示反馈弹窗 * 用户点击"继续检查"按钮 * Then: 弹窗关闭,停留在检查页面 */ it('场景2:半曝光状态点击"继续检查"应该停留在检查页面', () => { /** * Given: 模拟半曝光状态 * 通过Redux store直接设置状态 */ cy.window().its('store').then((store) => { store.dispatch({ type: 'bodyPositionList/setExposureStatus', payload: 'Half Exposed' }); }); /** * Given: 验证曝光状态为 "Half Exposed" */ cy.window().its('store').invoke('getState') .its('bodyPositionList') .its('exposureStatus') .should('equal', 'Half Exposed'); /** * When: 点击导航按钮尝试退出 */ mainPage.clickPatientManagementButton(); mainPage.clickWorklistButton(); /** * Then: 验证显示退出反馈弹窗 */ cy.get('[role="dialog"]').should('be.visible'); /** * Then: 验证弹窗标题 */ cy.get('[role="dialog"]').should('contain', '检查未完成'); /** * Then: 验证弹窗提示信息 */ cy.get('[role="dialog"]').should('contain', '当前检查的曝光步骤尚未完成'); /** * Then: 验证Redux store中弹窗状态 */ cy.window().its('store').invoke('getState') .its('largeScreen') .its('isFeedbackOpen') .should('be.true'); /** * When: 点击"继续检查"按钮 */ cy.get('[role="dialog"]').contains('button', '继续检查').click(); /** * Then: 验证弹窗关闭 */ cy.get('[role="dialog"]').should('not.exist'); /** * Then: 验证仍停留在检查页面 */ cy.get('[data-testid="exam-page"]').should('exist'); cy.url().should('include', '/exam'); /** * Then: 验证Redux store中弹窗已关闭 */ cy.window().its('store').invoke('getState') .its('largeScreen') .its('isFeedbackOpen') .should('be.false'); /** * Then: 验证业务流程仍为exam */ cy.window().its('store').invoke('getState') .its('BusinessFlow') .its('currentKey') .should('equal', 'exam'); }); /** * 测试场景 3:半曝光状态 - 保存并完成 * * Given: 用户已登录系统并进入检查页面 * 已完成部分体位的曝光 * When: 用户点击退出,显示反馈弹窗 * 用户点击"保存并完成"按钮 * Then: 调用API通知后端(study_status='Completed') * 成功跳转到工作列表页面 */ it('场景3:半曝光状态点击"保存并完成"应该调用API并退出', () => { /** * Given: Mock API接口 */ cy.intercept('POST', '/auth/task/inspection/leave', { statusCode: 200, body: { code: '0x000000', description: 'Success', solution: '', data: {} } }).as('suspendOrCompleteStudy'); /** * Given: 模拟半曝光状态并设置StudyID */ cy.window().its('store').then((store) => { const state = store.getState(); // 设置曝光状态 store.dispatch({ type: 'bodyPositionList/setExposureStatus', payload: 'Half Exposed' }); // 确保有StudyID if (state.bodyPositionList.selectedBodyPosition?.work?.StudyID) { cy.wrap(state.bodyPositionList.selectedBodyPosition.work.StudyID).as('currentStudyId'); } }); /** * When: 点击导航按钮尝试退出 */ mainPage.clickPatientManagementButton(); mainPage.clickWorklistButton(); /** * Then: 验证显示退出反馈弹窗 */ cy.get('[role="dialog"]').should('be.visible'); /** * When: 点击"保存并完成"按钮 */ cy.get('[role="dialog"]').contains('button', '保存并完成').click(); /** * Then: 验证调用了API */ cy.wait('@suspendOrCompleteStudy').then((interception) => { // 验证请求参数 expect(interception.request.body).to.have.property('study_status', 'Completed'); expect(interception.request.body).to.have.property('study_id'); }); /** * Then: 验证弹窗关闭 */ cy.get('[role="dialog"]').should('not.exist'); /** * Then: 验证成功跳转到工作列表页面 */ cy.url().should('include', '/worklist'); /** * Then: 验证业务流程已切换 */ cy.window().its('store').invoke('getState') .its('BusinessFlow') .its('currentKey') .should('equal', 'worklist'); }); /** * 测试场景 4:半曝光状态 - 直接中止 * * Given: 用户已登录系统并进入检查页面 * 已完成部分体位的曝光 * When: 用户点击退出,显示反馈弹窗 * 用户点击"直接中止"按钮(危险按钮) * Then: 调用API通知后端(study_status='InProgress') * 成功跳转到工作列表页面 */ it('场景4:半曝光状态点击"直接中止"应该调用API并退出', () => { /** * Given: Mock API接口 */ cy.intercept('POST', '/auth/task/inspection/leave', { statusCode: 200, body: { code: '0x000000', description: 'Success', solution: '', data: {} } }).as('suspendOrCompleteStudy'); /** * Given: 模拟半曝光状态 */ cy.window().its('store').then((store) => { store.dispatch({ type: 'bodyPositionList/setExposureStatus', payload: 'Half Exposed' }); }); /** * When: 点击导航按钮尝试退出 */ mainPage.clickPatientManagementButton(); mainPage.clickWorklistButton(); /** * Then: 验证显示退出反馈弹窗 */ cy.get('[role="dialog"]').should('be.visible'); /** * Then: 验证"直接中止"按钮为危险样式 */ cy.get('[role="dialog"]') .contains('button', '直接中止') .should('have.class', 'ant-btn-dangerous'); /** * When: 点击"直接中止"按钮 */ cy.get('[role="dialog"]').contains('button', '直接中止').click(); /** * Then: 验证调用了API */ cy.wait('@suspendOrCompleteStudy').then((interception) => { // 验证请求参数 expect(interception.request.body).to.have.property('study_status', 'InProgress'); expect(interception.request.body).to.have.property('study_id'); }); /** * Then: 验证弹窗关闭 */ cy.get('[role="dialog"]').should('not.exist'); /** * Then: 验证成功跳转到工作列表页面 */ cy.url().should('include', '/worklist'); }); /** * 测试场景 5:全曝光状态退出 * * Given: 用户已登录系统并进入检查页面 * 已完成所有体位的曝光 * When: 用户点击导航按钮切换到工作列表 * Then: 应该直接退出,不显示退出反馈弹窗 */ it('场景5:全曝光状态应该直接退出检查', () => { /** * Given: 模拟全曝光状态 */ cy.window().its('store').then((store) => { store.dispatch({ type: 'bodyPositionList/setExposureStatus', payload: 'Fully Exposed' }); }); /** * Given: 验证曝光状态为 "Fully Exposed" */ cy.window().its('store').invoke('getState') .its('bodyPositionList') .its('exposureStatus') .should('equal', 'Fully Exposed'); /** * When: 点击导航按钮切换到工作列表 */ mainPage.clickPatientManagementButton(); mainPage.clickWorklistButton(); /** * Then: 验证未显示退出反馈弹窗 */ cy.get('[role="dialog"]').should('not.exist'); /** * Then: 验证成功退出到工作列表页面 */ cy.url().should('include', '/worklist'); /** * Then: 验证业务流程已切换 */ cy.window().its('store').invoke('getState') .its('BusinessFlow') .its('currentKey') .should('equal', 'worklist'); }); /** * 测试场景 6:发生器采集中阻止退出到process * * Given: 用户在检查页面 * 发生器正在采集(acquisitionState = 1) * When: 用户尝试跳转到图像处理页面(process) * Then: 跳转被阻止,停留在检查页面 */ it('场景6:发生器采集中应该阻止退出到图像处理', () => { /** * Given: 模拟发生器正在采集 */ cy.window().its('store').then((store) => { store.dispatch({ type: 'generatorMonitor/setAcquisitionState', payload: 1 }); }); /** * Given: 验证发生器状态 */ cy.window().its('store').invoke('getState') .its('generatorMonitor') .its('acquisitionState') .should('equal', 1); /** * When: 尝试跳转到图像处理页面 * 注意:这里需要根据实际的导航方式调整 */ cy.window().its('store').then((store) => { store.dispatch({ type: 'BusinessFlow/setBusinessFlow', payload: 'process' }); }); /** * Then: 验证仍停留在检查页面 */ cy.wait(500); // 等待可能的状态变化 cy.get('[data-testid="exam-page"]').should('exist'); /** * Then: 验证业务流程仍为exam */ cy.window().its('store').invoke('getState') .its('BusinessFlow') .its('currentKey') .should('equal', 'exam'); }); /** * 测试场景 7:从检查正常退出到process(非采集) * * Given: 用户在检查页面 * 发生器空闲(acquisitionState != 1) * 有已曝光的图像 * When: 用户点击进入图像处理页面 * Then: 成功跳转到process页面 * 执行了清理操作(unprepare) */ it('场景7:发生器空闲时应该能正常退出到图像处理', () => { /** * Given: 确保发生器不在采集状态 */ cy.window().its('store').then((store) => { store.dispatch({ type: 'generatorMonitor/setAcquisitionState', payload: 0 }); }); /** * Given: 验证发生器状态 */ cy.window().its('store').invoke('getState') .its('generatorMonitor') .its('acquisitionState') .should('not.equal', 1); /** * Given: 监听控制台日志(验证unprepare调用) */ cy.window().then((win) => { cy.spy(win.console, 'log').as('consoleLog'); }); /** * When: 点击进入图像处理页面 * 注意:这里需要根据实际的导航方式调整 */ cy.window().its('store').then((store) => { store.dispatch({ type: 'BusinessFlow/setBusinessFlow', payload: 'process' }); }); /** * Then: 验证成功跳转到process页面 */ cy.wait(1000); cy.url().should('include', '/process'); /** * Then: 验证业务流程已切换 */ cy.window().its('store').invoke('getState') .its('BusinessFlow') .its('currentKey') .should('equal', 'process'); /** * Then: 验证控制台输出了相应日志 */ cy.get('@consoleLog').should('be.calledWith', '从检查退出到处理'); }); /** * 测试场景 8:边界场景 - 快速连续点击退出 * * Given: 用户在检查页面(半曝光状态) * When: 快速连续点击退出按钮(3次) * Then: 只显示一个退出反馈弹窗 * 弹窗功能正常 */ it('场景8:快速连续点击退出应该只显示一个弹窗', () => { /** * Given: 模拟半曝光状态 */ cy.window().its('store').then((store) => { store.dispatch({ type: 'bodyPositionList/setExposureStatus', payload: 'Half Exposed' }); }); /** * When: 快速连续点击退出按钮3次 */ for (let i = 0; i < 3; i++) { mainPage.clickPatientManagementButton(); mainPage.clickWorklistButton(); cy.wait(50); // 很短的等待时间,模拟快速点击 } /** * Then: 验证只显示一个弹窗 */ cy.get('[role="dialog"]').should('have.length', 1); /** * Then: 验证弹窗可见 */ cy.get('[role="dialog"]').should('be.visible'); /** * Then: 验证弹窗功能正常(点击"继续检查") */ cy.get('[role="dialog"]').contains('button', '继续检查').click(); cy.get('[role="dialog"]').should('not.exist'); /** * Then: 验证仍在检查页面 */ cy.get('[data-testid="exam-page"]').should('exist'); }); /** * 测试场景 9:边界场景 - StudyID为空的错误处理 * * Given: 用户在检查页面但StudyID为空 * When: 用户尝试退出并选择保存 * Then: 应该有适当的错误处理 */ it('场景9:StudyID为空时应该有错误处理', () => { /** * Given: 清空StudyID */ cy.window().its('store').then((store) => { store.dispatch({ type: 'bodyPositionList/setExposureStatus', payload: 'Half Exposed' }); // 模拟StudyID为空的情况 store.dispatch({ type: 'bodyPositionList/clearSelectedBodyPosition' }); }); /** * Given: 监听控制台错误日志 */ cy.window().then((win) => { cy.spy(win.console, 'error').as('consoleError'); }); /** * When: 尝试退出 */ mainPage.clickPatientManagementButton(); mainPage.clickWorklistButton(); /** * Then: 可能显示弹窗或直接显示错误 * 根据实际实现调整验证逻辑 */ cy.get('[role="dialog"]').then(($dialog) => { if ($dialog.length > 0) { // 如果显示了弹窗,尝试点击保存 cy.get('[role="dialog"]').contains('button', '保存并完成').click(); // 验证控制台输出了错误日志 cy.get('@consoleError').should('be.called'); } }); }); /** * 测试场景 10:验证退出反馈弹窗UI元素 * * Given: 用户在检查页面(半曝光状态) * When: 触发退出反馈弹窗 * Then: 验证弹窗包含所有必要的UI元素 */ it('场景10:退出反馈弹窗应该包含所有必要的UI元素', () => { /** * Given: 模拟半曝光状态 */ cy.window().its('store').then((store) => { store.dispatch({ type: 'bodyPositionList/setExposureStatus', payload: 'Half Exposed' }); }); /** * When: 触发退出 */ mainPage.clickPatientManagementButton(); mainPage.clickWorklistButton(); /** * Then: 验证弹窗存在 */ cy.get('[role="dialog"]').should('be.visible'); /** * Then: 验证弹窗标题 */ cy.get('[role="dialog"]').should('contain', '检查未完成'); /** * Then: 验证警告图标 */ cy.get('[role="dialog"]').find('.anticon-exclamation-circle').should('exist'); /** * Then: 验证提示消息 */ cy.get('[role="dialog"]').should('contain', '当前检查的曝光步骤尚未完成'); cy.get('[role="dialog"]').should('contain', '中止后,本次检查的进度将不会保存'); /** * Then: 验证包含三个按钮 */ cy.get('[role="dialog"]').find('button').should('have.length', 3); /** * Then: 验证"继续检查"按钮 */ cy.get('[role="dialog"]').contains('button', '继续检查').should('exist'); /** * Then: 验证"保存并完成"按钮(主按钮) */ cy.get('[role="dialog"]') .contains('button', '保存并完成') .should('have.class', 'ant-btn-primary'); /** * Then: 验证"直接中止"按钮(危险按钮) */ cy.get('[role="dialog"]') .contains('button', '直接中止') .should('have.class', 'ant-btn-primary') .should('have.class', 'ant-btn-dangerous'); /** * 清理:关闭弹窗 */ cy.get('[role="dialog"]').contains('button', '继续检查').click(); }); });