exit-exam-logic.cy.ts 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697
  1. /**
  2. * 测试文件: 检查页面 - 退出检查逻辑
  3. * 功能: 验证从检查页面退出时的各种场景和逻辑处理
  4. *
  5. * 测试场景:
  6. * 1. 未曝光状态直接退出
  7. * 2. 半曝光状态 - 继续检查
  8. * 3. 半曝光状态 - 保存并完成
  9. * 4. 半曝光状态 - 直接中止
  10. * 5. 全曝光状态退出
  11. * 6. 发生器采集中阻止退出到process
  12. * 7. 从检查正常退出到process(非采集)
  13. * 8. 边界场景:快速连续点击退出
  14. *
  15. * 相关需求文档: docs/实现/退出检查的逻辑.md
  16. */
  17. import LoginPage from '../../support/pageObjects/LoginPage';
  18. import MainPage from '../../support/pageObjects/MainPage';
  19. import WorklistPage from '../../support/pageObjects/WorklistPage';
  20. describe('检查页面:退出检查逻辑', () => {
  21. const loginPage = new LoginPage();
  22. const mainPage = new MainPage();
  23. const worklistPage = new WorklistPage();
  24. /**
  25. * 通用的登录和进入检查页面的前置步骤
  26. */
  27. beforeEach(() => {
  28. // 登录系统
  29. loginPage.visit();
  30. loginPage.login('admin', '123456');
  31. cy.contains('登录成功').should('be.visible', { timeout: 10000 });
  32. // 导航到工作列表
  33. mainPage.clickPatientManagementButton();
  34. mainPage.clickWorklistButton();
  35. // 双击第一行进入检查页面
  36. worklistPage.findTableAndDoubleClickFirstRow();
  37. cy.wait(1000);
  38. // 验证已进入检查页面
  39. cy.get('[data-testid="exam-page"]').should('exist');
  40. });
  41. /**
  42. * 测试场景 1:未曝光状态直接退出
  43. *
  44. * Given: 用户已登录系统并从工作列表进入检查页面
  45. * 未进行任何曝光操作
  46. * When: 用户点击导航按钮切换到工作列表
  47. * Then: 应该直接退出检查页面,不显示退出反馈弹窗
  48. */
  49. it('场景1:未曝光状态应该直接退出检查', () => {
  50. /**
  51. * Given: 验证当前在检查页面
  52. */
  53. cy.get('[data-testid="exam-page"]').should('exist');
  54. /**
  55. * Given: 验证曝光状态为 "Not Exposed"
  56. */
  57. cy.window().its('store').invoke('getState')
  58. .its('bodyPositionList')
  59. .its('exposureStatus')
  60. .should('equal', 'Not Exposed');
  61. /**
  62. * When: 点击导航按钮切换到工作列表
  63. */
  64. mainPage.clickPatientManagementButton();
  65. mainPage.clickWorklistButton();
  66. /**
  67. * Then: 验证成功退出到工作列表页面
  68. */
  69. cy.url().should('include', '/worklist');
  70. /**
  71. * Then: 验证未显示退出反馈弹窗
  72. */
  73. cy.get('[role="dialog"]').should('not.exist');
  74. /**
  75. * Then: 验证Redux store中业务流程已切换
  76. */
  77. cy.window().its('store').invoke('getState')
  78. .its('BusinessFlow')
  79. .its('currentKey')
  80. .should('equal', 'worklist');
  81. });
  82. /**
  83. * 测试场景 2:半曝光状态 - 继续检查
  84. *
  85. * Given: 用户已登录系统并进入检查页面
  86. * 已完成部分体位的曝光
  87. * When: 用户点击退出,显示反馈弹窗
  88. * 用户点击"继续检查"按钮
  89. * Then: 弹窗关闭,停留在检查页面
  90. */
  91. it('场景2:半曝光状态点击"继续检查"应该停留在检查页面', () => {
  92. /**
  93. * Given: 模拟半曝光状态
  94. * 通过Redux store直接设置状态
  95. */
  96. cy.window().its('store').then((store) => {
  97. store.dispatch({
  98. type: 'bodyPositionList/setExposureStatus',
  99. payload: 'Half Exposed'
  100. });
  101. });
  102. /**
  103. * Given: 验证曝光状态为 "Half Exposed"
  104. */
  105. cy.window().its('store').invoke('getState')
  106. .its('bodyPositionList')
  107. .its('exposureStatus')
  108. .should('equal', 'Half Exposed');
  109. /**
  110. * When: 点击导航按钮尝试退出
  111. */
  112. mainPage.clickPatientManagementButton();
  113. mainPage.clickWorklistButton();
  114. /**
  115. * Then: 验证显示退出反馈弹窗
  116. */
  117. cy.get('[role="dialog"]').should('be.visible');
  118. /**
  119. * Then: 验证弹窗标题
  120. */
  121. cy.get('[role="dialog"]').should('contain', '检查未完成');
  122. /**
  123. * Then: 验证弹窗提示信息
  124. */
  125. cy.get('[role="dialog"]').should('contain', '当前检查的曝光步骤尚未完成');
  126. /**
  127. * Then: 验证Redux store中弹窗状态
  128. */
  129. cy.window().its('store').invoke('getState')
  130. .its('largeScreen')
  131. .its('isFeedbackOpen')
  132. .should('be.true');
  133. /**
  134. * When: 点击"继续检查"按钮
  135. */
  136. cy.get('[role="dialog"]').contains('button', '继续检查').click();
  137. /**
  138. * Then: 验证弹窗关闭
  139. */
  140. cy.get('[role="dialog"]').should('not.exist');
  141. /**
  142. * Then: 验证仍停留在检查页面
  143. */
  144. cy.get('[data-testid="exam-page"]').should('exist');
  145. cy.url().should('include', '/exam');
  146. /**
  147. * Then: 验证Redux store中弹窗已关闭
  148. */
  149. cy.window().its('store').invoke('getState')
  150. .its('largeScreen')
  151. .its('isFeedbackOpen')
  152. .should('be.false');
  153. /**
  154. * Then: 验证业务流程仍为exam
  155. */
  156. cy.window().its('store').invoke('getState')
  157. .its('BusinessFlow')
  158. .its('currentKey')
  159. .should('equal', 'exam');
  160. });
  161. /**
  162. * 测试场景 3:半曝光状态 - 保存并完成
  163. *
  164. * Given: 用户已登录系统并进入检查页面
  165. * 已完成部分体位的曝光
  166. * When: 用户点击退出,显示反馈弹窗
  167. * 用户点击"保存并完成"按钮
  168. * Then: 调用API通知后端(study_status='Completed')
  169. * 成功跳转到工作列表页面
  170. */
  171. it('场景3:半曝光状态点击"保存并完成"应该调用API并退出', () => {
  172. /**
  173. * Given: Mock API接口
  174. */
  175. cy.intercept('POST', '/auth/task/inspection/leave', {
  176. statusCode: 200,
  177. body: {
  178. code: '0x000000',
  179. description: 'Success',
  180. solution: '',
  181. data: {}
  182. }
  183. }).as('suspendOrCompleteStudy');
  184. /**
  185. * Given: 模拟半曝光状态并设置StudyID
  186. */
  187. cy.window().its('store').then((store) => {
  188. const state = store.getState();
  189. // 设置曝光状态
  190. store.dispatch({
  191. type: 'bodyPositionList/setExposureStatus',
  192. payload: 'Half Exposed'
  193. });
  194. // 确保有StudyID
  195. if (state.bodyPositionList.selectedBodyPosition?.work?.StudyID) {
  196. cy.wrap(state.bodyPositionList.selectedBodyPosition.work.StudyID).as('currentStudyId');
  197. }
  198. });
  199. /**
  200. * When: 点击导航按钮尝试退出
  201. */
  202. mainPage.clickPatientManagementButton();
  203. mainPage.clickWorklistButton();
  204. /**
  205. * Then: 验证显示退出反馈弹窗
  206. */
  207. cy.get('[role="dialog"]').should('be.visible');
  208. /**
  209. * When: 点击"保存并完成"按钮
  210. */
  211. cy.get('[role="dialog"]').contains('button', '保存并完成').click();
  212. /**
  213. * Then: 验证调用了API
  214. */
  215. cy.wait('@suspendOrCompleteStudy').then((interception) => {
  216. // 验证请求参数
  217. expect(interception.request.body).to.have.property('study_status', 'Completed');
  218. expect(interception.request.body).to.have.property('study_id');
  219. });
  220. /**
  221. * Then: 验证弹窗关闭
  222. */
  223. cy.get('[role="dialog"]').should('not.exist');
  224. /**
  225. * Then: 验证成功跳转到工作列表页面
  226. */
  227. cy.url().should('include', '/worklist');
  228. /**
  229. * Then: 验证业务流程已切换
  230. */
  231. cy.window().its('store').invoke('getState')
  232. .its('BusinessFlow')
  233. .its('currentKey')
  234. .should('equal', 'worklist');
  235. });
  236. /**
  237. * 测试场景 4:半曝光状态 - 直接中止
  238. *
  239. * Given: 用户已登录系统并进入检查页面
  240. * 已完成部分体位的曝光
  241. * When: 用户点击退出,显示反馈弹窗
  242. * 用户点击"直接中止"按钮(危险按钮)
  243. * Then: 调用API通知后端(study_status='InProgress')
  244. * 成功跳转到工作列表页面
  245. */
  246. it('场景4:半曝光状态点击"直接中止"应该调用API并退出', () => {
  247. /**
  248. * Given: Mock API接口
  249. */
  250. cy.intercept('POST', '/auth/task/inspection/leave', {
  251. statusCode: 200,
  252. body: {
  253. code: '0x000000',
  254. description: 'Success',
  255. solution: '',
  256. data: {}
  257. }
  258. }).as('suspendOrCompleteStudy');
  259. /**
  260. * Given: 模拟半曝光状态
  261. */
  262. cy.window().its('store').then((store) => {
  263. store.dispatch({
  264. type: 'bodyPositionList/setExposureStatus',
  265. payload: 'Half Exposed'
  266. });
  267. });
  268. /**
  269. * When: 点击导航按钮尝试退出
  270. */
  271. mainPage.clickPatientManagementButton();
  272. mainPage.clickWorklistButton();
  273. /**
  274. * Then: 验证显示退出反馈弹窗
  275. */
  276. cy.get('[role="dialog"]').should('be.visible');
  277. /**
  278. * Then: 验证"直接中止"按钮为危险样式
  279. */
  280. cy.get('[role="dialog"]')
  281. .contains('button', '直接中止')
  282. .should('have.class', 'ant-btn-dangerous');
  283. /**
  284. * When: 点击"直接中止"按钮
  285. */
  286. cy.get('[role="dialog"]').contains('button', '直接中止').click();
  287. /**
  288. * Then: 验证调用了API
  289. */
  290. cy.wait('@suspendOrCompleteStudy').then((interception) => {
  291. // 验证请求参数
  292. expect(interception.request.body).to.have.property('study_status', 'InProgress');
  293. expect(interception.request.body).to.have.property('study_id');
  294. });
  295. /**
  296. * Then: 验证弹窗关闭
  297. */
  298. cy.get('[role="dialog"]').should('not.exist');
  299. /**
  300. * Then: 验证成功跳转到工作列表页面
  301. */
  302. cy.url().should('include', '/worklist');
  303. });
  304. /**
  305. * 测试场景 5:全曝光状态退出
  306. *
  307. * Given: 用户已登录系统并进入检查页面
  308. * 已完成所有体位的曝光
  309. * When: 用户点击导航按钮切换到工作列表
  310. * Then: 应该直接退出,不显示退出反馈弹窗
  311. */
  312. it('场景5:全曝光状态应该直接退出检查', () => {
  313. /**
  314. * Given: 模拟全曝光状态
  315. */
  316. cy.window().its('store').then((store) => {
  317. store.dispatch({
  318. type: 'bodyPositionList/setExposureStatus',
  319. payload: 'Fully Exposed'
  320. });
  321. });
  322. /**
  323. * Given: 验证曝光状态为 "Fully Exposed"
  324. */
  325. cy.window().its('store').invoke('getState')
  326. .its('bodyPositionList')
  327. .its('exposureStatus')
  328. .should('equal', 'Fully Exposed');
  329. /**
  330. * When: 点击导航按钮切换到工作列表
  331. */
  332. mainPage.clickPatientManagementButton();
  333. mainPage.clickWorklistButton();
  334. /**
  335. * Then: 验证未显示退出反馈弹窗
  336. */
  337. cy.get('[role="dialog"]').should('not.exist');
  338. /**
  339. * Then: 验证成功退出到工作列表页面
  340. */
  341. cy.url().should('include', '/worklist');
  342. /**
  343. * Then: 验证业务流程已切换
  344. */
  345. cy.window().its('store').invoke('getState')
  346. .its('BusinessFlow')
  347. .its('currentKey')
  348. .should('equal', 'worklist');
  349. });
  350. /**
  351. * 测试场景 6:发生器采集中阻止退出到process
  352. *
  353. * Given: 用户在检查页面
  354. * 发生器正在采集(acquisitionState = 1)
  355. * When: 用户尝试跳转到图像处理页面(process)
  356. * Then: 跳转被阻止,停留在检查页面
  357. */
  358. it('场景6:发生器采集中应该阻止退出到图像处理', () => {
  359. /**
  360. * Given: 模拟发生器正在采集
  361. */
  362. cy.window().its('store').then((store) => {
  363. store.dispatch({
  364. type: 'generatorMonitor/setAcquisitionState',
  365. payload: 1
  366. });
  367. });
  368. /**
  369. * Given: 验证发生器状态
  370. */
  371. cy.window().its('store').invoke('getState')
  372. .its('generatorMonitor')
  373. .its('acquisitionState')
  374. .should('equal', 1);
  375. /**
  376. * When: 尝试跳转到图像处理页面
  377. * 注意:这里需要根据实际的导航方式调整
  378. */
  379. cy.window().its('store').then((store) => {
  380. store.dispatch({
  381. type: 'BusinessFlow/setBusinessFlow',
  382. payload: 'process'
  383. });
  384. });
  385. /**
  386. * Then: 验证仍停留在检查页面
  387. */
  388. cy.wait(500); // 等待可能的状态变化
  389. cy.get('[data-testid="exam-page"]').should('exist');
  390. /**
  391. * Then: 验证业务流程仍为exam
  392. */
  393. cy.window().its('store').invoke('getState')
  394. .its('BusinessFlow')
  395. .its('currentKey')
  396. .should('equal', 'exam');
  397. });
  398. /**
  399. * 测试场景 7:从检查正常退出到process(非采集)
  400. *
  401. * Given: 用户在检查页面
  402. * 发生器空闲(acquisitionState != 1)
  403. * 有已曝光的图像
  404. * When: 用户点击进入图像处理页面
  405. * Then: 成功跳转到process页面
  406. * 执行了清理操作(unprepare)
  407. */
  408. it('场景7:发生器空闲时应该能正常退出到图像处理', () => {
  409. /**
  410. * Given: 确保发生器不在采集状态
  411. */
  412. cy.window().its('store').then((store) => {
  413. store.dispatch({
  414. type: 'generatorMonitor/setAcquisitionState',
  415. payload: 0
  416. });
  417. });
  418. /**
  419. * Given: 验证发生器状态
  420. */
  421. cy.window().its('store').invoke('getState')
  422. .its('generatorMonitor')
  423. .its('acquisitionState')
  424. .should('not.equal', 1);
  425. /**
  426. * Given: 监听控制台日志(验证unprepare调用)
  427. */
  428. cy.window().then((win) => {
  429. cy.spy(win.console, 'log').as('consoleLog');
  430. });
  431. /**
  432. * When: 点击进入图像处理页面
  433. * 注意:这里需要根据实际的导航方式调整
  434. */
  435. cy.window().its('store').then((store) => {
  436. store.dispatch({
  437. type: 'BusinessFlow/setBusinessFlow',
  438. payload: 'process'
  439. });
  440. });
  441. /**
  442. * Then: 验证成功跳转到process页面
  443. */
  444. cy.wait(1000);
  445. cy.url().should('include', '/process');
  446. /**
  447. * Then: 验证业务流程已切换
  448. */
  449. cy.window().its('store').invoke('getState')
  450. .its('BusinessFlow')
  451. .its('currentKey')
  452. .should('equal', 'process');
  453. /**
  454. * Then: 验证控制台输出了相应日志
  455. */
  456. cy.get('@consoleLog').should('be.calledWith', '从检查退出到处理');
  457. });
  458. /**
  459. * 测试场景 8:边界场景 - 快速连续点击退出
  460. *
  461. * Given: 用户在检查页面(半曝光状态)
  462. * When: 快速连续点击退出按钮(3次)
  463. * Then: 只显示一个退出反馈弹窗
  464. * 弹窗功能正常
  465. */
  466. it('场景8:快速连续点击退出应该只显示一个弹窗', () => {
  467. /**
  468. * Given: 模拟半曝光状态
  469. */
  470. cy.window().its('store').then((store) => {
  471. store.dispatch({
  472. type: 'bodyPositionList/setExposureStatus',
  473. payload: 'Half Exposed'
  474. });
  475. });
  476. /**
  477. * When: 快速连续点击退出按钮3次
  478. */
  479. for (let i = 0; i < 3; i++) {
  480. mainPage.clickPatientManagementButton();
  481. mainPage.clickWorklistButton();
  482. cy.wait(50); // 很短的等待时间,模拟快速点击
  483. }
  484. /**
  485. * Then: 验证只显示一个弹窗
  486. */
  487. cy.get('[role="dialog"]').should('have.length', 1);
  488. /**
  489. * Then: 验证弹窗可见
  490. */
  491. cy.get('[role="dialog"]').should('be.visible');
  492. /**
  493. * Then: 验证弹窗功能正常(点击"继续检查")
  494. */
  495. cy.get('[role="dialog"]').contains('button', '继续检查').click();
  496. cy.get('[role="dialog"]').should('not.exist');
  497. /**
  498. * Then: 验证仍在检查页面
  499. */
  500. cy.get('[data-testid="exam-page"]').should('exist');
  501. });
  502. /**
  503. * 测试场景 9:边界场景 - StudyID为空的错误处理
  504. *
  505. * Given: 用户在检查页面但StudyID为空
  506. * When: 用户尝试退出并选择保存
  507. * Then: 应该有适当的错误处理
  508. */
  509. it('场景9:StudyID为空时应该有错误处理', () => {
  510. /**
  511. * Given: 清空StudyID
  512. */
  513. cy.window().its('store').then((store) => {
  514. store.dispatch({
  515. type: 'bodyPositionList/setExposureStatus',
  516. payload: 'Half Exposed'
  517. });
  518. // 模拟StudyID为空的情况
  519. store.dispatch({
  520. type: 'bodyPositionList/clearSelectedBodyPosition'
  521. });
  522. });
  523. /**
  524. * Given: 监听控制台错误日志
  525. */
  526. cy.window().then((win) => {
  527. cy.spy(win.console, 'error').as('consoleError');
  528. });
  529. /**
  530. * When: 尝试退出
  531. */
  532. mainPage.clickPatientManagementButton();
  533. mainPage.clickWorklistButton();
  534. /**
  535. * Then: 可能显示弹窗或直接显示错误
  536. * 根据实际实现调整验证逻辑
  537. */
  538. cy.get('[role="dialog"]').then(($dialog) => {
  539. if ($dialog.length > 0) {
  540. // 如果显示了弹窗,尝试点击保存
  541. cy.get('[role="dialog"]').contains('button', '保存并完成').click();
  542. // 验证控制台输出了错误日志
  543. cy.get('@consoleError').should('be.called');
  544. }
  545. });
  546. });
  547. /**
  548. * 测试场景 10:验证退出反馈弹窗UI元素
  549. *
  550. * Given: 用户在检查页面(半曝光状态)
  551. * When: 触发退出反馈弹窗
  552. * Then: 验证弹窗包含所有必要的UI元素
  553. */
  554. it('场景10:退出反馈弹窗应该包含所有必要的UI元素', () => {
  555. /**
  556. * Given: 模拟半曝光状态
  557. */
  558. cy.window().its('store').then((store) => {
  559. store.dispatch({
  560. type: 'bodyPositionList/setExposureStatus',
  561. payload: 'Half Exposed'
  562. });
  563. });
  564. /**
  565. * When: 触发退出
  566. */
  567. mainPage.clickPatientManagementButton();
  568. mainPage.clickWorklistButton();
  569. /**
  570. * Then: 验证弹窗存在
  571. */
  572. cy.get('[role="dialog"]').should('be.visible');
  573. /**
  574. * Then: 验证弹窗标题
  575. */
  576. cy.get('[role="dialog"]').should('contain', '检查未完成');
  577. /**
  578. * Then: 验证警告图标
  579. */
  580. cy.get('[role="dialog"]').find('.anticon-exclamation-circle').should('exist');
  581. /**
  582. * Then: 验证提示消息
  583. */
  584. cy.get('[role="dialog"]').should('contain', '当前检查的曝光步骤尚未完成');
  585. cy.get('[role="dialog"]').should('contain', '中止后,本次检查的进度将不会保存');
  586. /**
  587. * Then: 验证包含三个按钮
  588. */
  589. cy.get('[role="dialog"]').find('button').should('have.length', 3);
  590. /**
  591. * Then: 验证"继续检查"按钮
  592. */
  593. cy.get('[role="dialog"]').contains('button', '继续检查').should('exist');
  594. /**
  595. * Then: 验证"保存并完成"按钮(主按钮)
  596. */
  597. cy.get('[role="dialog"]')
  598. .contains('button', '保存并完成')
  599. .should('have.class', 'ant-btn-primary');
  600. /**
  601. * Then: 验证"直接中止"按钮(危险按钮)
  602. */
  603. cy.get('[role="dialog"]')
  604. .contains('button', '直接中止')
  605. .should('have.class', 'ant-btn-primary')
  606. .should('have.class', 'ant-btn-dangerous');
  607. /**
  608. * 清理:关闭弹窗
  609. */
  610. cy.get('[role="dialog"]').contains('button', '继续检查').click();
  611. });
  612. });