Pārlūkot izejas kodu

test: 实现Worklist和History进入检查功能的E2E测试套件

- 在study.ts中新增mockGetStudyDetailsUnexposed和mockGetStudyDetailsExposed函数
- 在HistoryPage.ts中添加findTableAndDoubleClickFirstRow方法
- 创建worklist-enter-exam-unexposed测试文件,验证未曝光场景进入检查界面
- 创建worklist-enter-process-exposed测试文件,验证已曝光场景进入处理界面
- 创建history-enter-exam-unexposed测试文件,验证History未曝光场景
- 创建history-enter-process-exposed测试文件,验证History已曝光场景
- 编写详细的测试方案文档,包含4个测试场景和验证策略

改动文件:
- cypress/support/mock/handlers/study.ts
- cypress/support/pageObjects/HistoryPage.ts
- cypress/e2e/patient/worklist-enter-exam-unexposed.cy.ts (新建)
- cypress/e2e/patient/worklist-enter-process-exposed.cy.ts (新建)
- cypress/e2e/patient/history-enter-exam-unexposed.cy.ts (新建)
- cypress/e2e/patient/history-enter-process-exposed.cy.ts (新建)
- docs/测试/进入检查功能测试方案.md (新建)
dengdx 3 dienas atpakaļ
vecāks
revīzija
ce15268f85

+ 219 - 0
cypress/e2e/patient/history-enter-exam-unexposed.cy.ts

@@ -0,0 +1,219 @@
+/**
+ * 测试文件: 进入检查 - History双击进入检查(未曝光)
+ * 场景: 从History双击未曝光任务,系统应进入检查界面
+ * 
+ * 路径分析:
+ * - POM: LoginPage, MainPage, HistoryPage, ExamPage
+ * - Mock: 
+ *   - mockLoginSuccess() - 登录成功
+ *   - mockFetchHistoryDataWithUnlocked() - 获取History列表
+ *   - mockGetStudyDetailsUnexposed() - 获取未曝光检查详情
+ * - 规格: 
+ *   ✓ 登录成功
+ *   ✓ 导航到History列表
+ *   ✓ History列表显示至少2条记录
+ *   ✓ 双击第一条未曝光记录
+ *   ✓ 系统加载任务详细信息(未曝光状态)
+ *   ✓ 进入检查界面(businessFlow: 'exam')
+ *   ✓ 验证Redux状态正确
+ *   ✓ 验证界面元素可见
+ * 
+ * @see docs/测试/进入检查功能测试方案.md - 场景3
+ */
+
+import { mockLoginSuccess } from '../../support/mock/handlers/user';
+import { mockFetchHistoryDataWithUnlocked } from '../../support/mock/handlers/worklist';
+import { mockGetStudyDetailsUnexposed } from '../../support/mock/handlers/study';
+import LoginPage from '../../support/pageObjects/LoginPage';
+import MainPage from '../../support/pageObjects/MainPage';
+import HistoryPage from '../../support/pageObjects/HistoryPage';
+import ExamPage from '../../support/pageObjects/ExamPage';
+
+describe('进入检查:History双击进入检查(未曝光)', () => {
+  const loginPage = new LoginPage();
+  const mainPage = new MainPage();
+  const historyPage = new HistoryPage();
+  const examPage = new ExamPage();
+
+  beforeEach(() => {
+    // 设置所有必要的Mock
+    mockLoginSuccess();
+    mockFetchHistoryDataWithUnlocked();
+    // 使用第一条History记录的StudyID
+    mockGetStudyDetailsUnexposed('HIST20250912001');
+  });
+
+  it('应该通过双击History未曝光条目进入检查界面', () => {
+    /**
+     * Given: 用户已登录
+     * 步骤1(前置): 打开登录界面,完成登录
+     */
+    loginPage.visit();
+    loginPage.getUsernameInput().should('be.visible');
+    loginPage.login('admin', '123456');
+    
+    // 等待登录成功
+    cy.wait('@loginSuccess');
+    cy.contains('登录成功').should('be.visible', { timeout: 10000 });
+
+    /**
+     * When: 导航到History
+     * 步骤1: 打开 History
+     */
+    mainPage.clickPatientManagementButton();
+    mainPage.clickHistorylistButton();
+
+    /**
+     * Then: 系统加载History任务信息
+     * 步骤2(前置): 验证History列表加载
+     */
+    cy.wait('@getHistoryUnlocked').then((interception) => {
+      expect(interception.response?.statusCode).to.eq(200);
+    });
+
+    // 验证History列表显示(至少2条记录)
+    historyPage.getTable().should('be.visible');
+    historyPage.getTable().find('tr').should('have.length.at.least', 2);
+
+    /**
+     * When: 用户双击第一条未曝光任务
+     * 步骤2: 双击任务条目
+     */
+    historyPage.findTableAndDoubleClickFirstRow();
+
+    /**
+     * Then: 系统加载任务详细信息(未曝光状态)
+     * 步骤3: 验证API调用和返回数据
+     */
+    cy.wait('@getStudyDetailsUnexposed').then((interception) => {
+      expect(interception.response?.statusCode).to.eq(200);
+      
+      // 验证返回的数据包含未曝光的检查信息
+      const studyData = interception.response?.body.data;
+      expect(studyData).to.have.property('study_id');
+      expect(studyData).to.have.property('series');
+      expect(studyData.series).to.be.an('array');
+      
+      // 验证所有images的expose_status为'Unexposed'
+      studyData.series.forEach((series: any) => {
+        series.images.forEach((image: any) => {
+          expect(image.expose_status).to.eq('Unexposed');
+        });
+      });
+    });
+
+    /**
+     * Then: 验证Redux状态 - examWorksCache
+     * 步骤4: 验证任务数据已缓存
+     */
+    cy.window()
+      .its('store')
+      .invoke('getState')
+      .its('examWorksCache')
+      .its('works')
+      .should('have.length', 1)
+      .its(0)
+      .should('have.property', 'StudyID', 'HIST20250912001');
+
+    /**
+     * Then: 验证Redux状态 - businessFlow
+     * 步骤5: 验证进入检查流程(exam模式)
+     */
+    cy.window()
+      .its('store')
+      .invoke('getState')
+      .its('businessFlow')
+      .its('currentFlow')
+      .should('eq', 'exam');
+
+    /**
+     * Then: 验证检查界面元素可见
+     * 步骤6: 验证目标界面加载
+     */
+    examPage.verifyExamPageLoaded();
+    examPage.getToolbar().should('be.visible');
+    examPage.getContentArea().should('be.visible');
+  });
+
+  it('应该在Redux中正确保存任务的Views数据', () => {
+    /**
+     * 这个测试用例专注于验证数据转换的正确性
+     */
+    loginPage.visit();
+    loginPage.login('admin', '123456');
+    cy.wait('@loginSuccess');
+
+    mainPage.clickPatientManagementButton();
+    mainPage.clickHistorylistButton();
+    cy.wait('@getHistoryUnlocked');
+
+    historyPage.findTableAndDoubleClickFirstRow();
+    cy.wait('@getStudyDetailsUnexposed');
+
+    /**
+     * 验证Views数组被正确构建
+     */
+    cy.window()
+      .its('store')
+      .invoke('getState')
+      .its('examWorksCache')
+      .its('works')
+      .its(0)
+      .its('Views')
+      .should('be.an', 'array')
+      .should('have.length.greaterThan', 0)
+      .then((views: any[]) => {
+        // 验证每个View包含必要的属性
+        views.forEach((view) => {
+          expect(view).to.have.property('view_id');
+          expect(view).to.have.property('view_description');
+          expect(view).to.have.property('expose_status', 'Unexposed');
+          expect(view).to.have.property('study_id');
+          expect(view).to.have.property('series_instance_uid');
+        });
+      });
+  });
+
+  it('应该正确从History进入检查流程', () => {
+    /**
+     * 验证从History进入检查的完整流程
+     */
+    loginPage.visit();
+    loginPage.login('admin', '123456');
+    cy.wait('@loginSuccess');
+
+    mainPage.clickPatientManagementButton();
+    mainPage.clickHistorylistButton();
+    cy.wait('@getHistoryUnlocked');
+
+    // 验证History记录的状态
+    cy.window()
+      .its('store')
+      .invoke('getState')
+      .its('historyEntities')
+      .its('data')
+      .should('have.length.greaterThan', 0)
+      .its(0)
+      .should('have.property', 'study_status', 'Completed');
+
+    historyPage.findTableAndDoubleClickFirstRow();
+    cy.wait('@getStudyDetailsUnexposed');
+
+    // 验证进入检查流程后,businessFlow正确
+    cy.window()
+      .its('store')
+      .invoke('getState')
+      .its('businessFlow')
+      .its('currentFlow')
+      .should('eq', 'exam');
+
+    // 验证examWorksCache包含从History加载的数据
+    cy.window()
+      .its('store')
+      .invoke('getState')
+      .its('examWorksCache')
+      .its('works')
+      .its(0)
+      .should('have.property', 'StudyID', 'HIST20250912001');
+  });
+});

+ 280 - 0
cypress/e2e/patient/history-enter-process-exposed.cy.ts

@@ -0,0 +1,280 @@
+/**
+ * 测试文件: 进入检查 - History双击进入处理(已曝光)
+ * 场景: 从History双击已曝光任务,系统应进入处理界面
+ * 
+ * 路径分析:
+ * - POM: LoginPage, MainPage, HistoryPage
+ * - Mock: 
+ *   - mockLoginSuccess() - 登录成功
+ *   - mockFetchHistoryDataWithUnlocked() - 获取History列表
+ *   - mockGetStudyDetailsExposed() - 获取已曝光检查详情
+ * - 规格: 
+ *   ✓ 登录成功
+ *   ✓ 导航到History列表
+ *   ✓ History列表显示至少2条记录
+ *   ✓ 双击第一条已曝光记录
+ *   ✓ 系统加载任务详细信息(已曝光状态)
+ *   ✓ 进入处理界面(businessFlow: 'process')
+ *   ✓ 验证Redux状态正确
+ *   ✓ 验证bodyPositionList已转换
+ * 
+ * @see docs/测试/进入检查功能测试方案.md - 场景4
+ */
+
+import { mockLoginSuccess } from '../../support/mock/handlers/user';
+import { mockFetchHistoryDataWithUnlocked } from '../../support/mock/handlers/worklist';
+import { mockGetStudyDetailsExposed } from '../../support/mock/handlers/study';
+import LoginPage from '../../support/pageObjects/LoginPage';
+import MainPage from '../../support/pageObjects/MainPage';
+import HistoryPage from '../../support/pageObjects/HistoryPage';
+
+describe('进入检查:History双击进入处理(已曝光)', () => {
+  const loginPage = new LoginPage();
+  const mainPage = new MainPage();
+  const historyPage = new HistoryPage();
+
+  beforeEach(() => {
+    // 设置所有必要的Mock
+    mockLoginSuccess();
+    mockFetchHistoryDataWithUnlocked();
+    // 使用第一条History记录的StudyID,返回已曝光数据
+    mockGetStudyDetailsExposed('HIST20250912001');
+  });
+
+  it('应该通过双击History已曝光条目进入处理界面', () => {
+    /**
+     * Given: 用户已登录
+     * 步骤1(前置): 打开登录界面,完成登录
+     */
+    loginPage.visit();
+    loginPage.getUsernameInput().should('be.visible');
+    loginPage.login('admin', '123456');
+    
+    // 等待登录成功
+    cy.wait('@loginSuccess');
+    cy.contains('登录成功').should('be.visible', { timeout: 10000 });
+
+    /**
+     * When: 导航到History
+     * 步骤1: 打开 History
+     */
+    mainPage.clickPatientManagementButton();
+    mainPage.clickHistorylistButton();
+
+    /**
+     * Then: 系统加载History任务信息
+     * 步骤2(前置): 验证History列表加载
+     */
+    cy.wait('@getHistoryUnlocked').then((interception) => {
+      expect(interception.response?.statusCode).to.eq(200);
+    });
+
+    // 验证History列表显示(至少2条记录)
+    historyPage.getTable().should('be.visible');
+    historyPage.getTable().find('tr').should('have.length.at.least', 2);
+
+    /**
+     * When: 用户双击第一条已曝光任务
+     * 步骤2: 双击任务条目
+     */
+    historyPage.findTableAndDoubleClickFirstRow();
+
+    /**
+     * Then: 系统加载任务详细信息(已曝光状态)
+     * 步骤3: 验证API调用和返回数据
+     */
+    cy.wait('@getStudyDetailsExposed').then((interception) => {
+      expect(interception.response?.statusCode).to.eq(200);
+      
+      // 验证返回的数据包含已曝光的检查信息
+      const studyData = interception.response?.body.data;
+      expect(studyData).to.have.property('study_id');
+      expect(studyData).to.have.property('series');
+      expect(studyData.series).to.be.an('array');
+      
+      // 验证所有images的expose_status为'Exposed'
+      studyData.series.forEach((series: any) => {
+        series.images.forEach((image: any) => {
+          expect(image.expose_status).to.eq('Exposed');
+          // 已曝光的图像应该有文件路径
+          expect(image.image_file_path).to.not.be.empty;
+        });
+      });
+    });
+
+    /**
+     * Then: 验证Redux状态 - examWorksCache
+     * 步骤4: 验证任务数据已缓存
+     */
+    cy.window()
+      .its('store')
+      .invoke('getState')
+      .its('examWorksCache')
+      .its('works')
+      .should('have.length', 1)
+      .its(0)
+      .should('have.property', 'StudyID', 'HIST20250912001');
+
+    /**
+     * Then: 验证Redux状态 - businessFlow
+     * 步骤5: 验证进入处理流程(process模式)
+     */
+    cy.window()
+      .its('store')
+      .invoke('getState')
+      .its('businessFlow')
+      .its('currentFlow')
+      .should('eq', 'process');
+
+    /**
+     * Then: 验证Redux状态 - bodyPositionList
+     * 步骤6: 验证体位列表已正确转换
+     */
+    cy.window()
+      .its('store')
+      .invoke('getState')
+      .its('bodyPositionList')
+      .its('positions')
+      .should('have.length.greaterThan', 0);
+  });
+
+  it('应该在Redux中正确保存已曝光任务的Views数据', () => {
+    /**
+     * 这个测试用例专注于验证已曝光数据的正确性
+     */
+    loginPage.visit();
+    loginPage.login('admin', '123456');
+    cy.wait('@loginSuccess');
+
+    mainPage.clickPatientManagementButton();
+    mainPage.clickHistorylistButton();
+    cy.wait('@getHistoryUnlocked');
+
+    historyPage.findTableAndDoubleClickFirstRow();
+    cy.wait('@getStudyDetailsExposed');
+
+    /**
+     * 验证Views数组包含已曝光数据
+     */
+    cy.window()
+      .its('store')
+      .invoke('getState')
+      .its('examWorksCache')
+      .its('works')
+      .its(0)
+      .its('Views')
+      .should('be.an', 'array')
+      .should('have.length.greaterThan', 0)
+      .then((views: any[]) => {
+        // 验证每个View都是已曝光状态
+        views.forEach((view) => {
+          expect(view).to.have.property('view_id');
+          expect(view).to.have.property('view_description');
+          expect(view).to.have.property('expose_status', 'Exposed');
+          expect(view).to.have.property('study_id');
+          expect(view).to.have.property('series_instance_uid');
+          // 已曝光的View应该有图像文件路径
+          expect(view).to.have.property('image_file_path');
+          expect(view.image_file_path).to.not.be.empty;
+        });
+      });
+  });
+
+  it('应该正确判断所有体位已曝光并进入处理流程', () => {
+    /**
+     * 这个测试用例验证曝光状态判断逻辑
+     */
+    loginPage.visit();
+    loginPage.login('admin', '123456');
+    cy.wait('@loginSuccess');
+
+    mainPage.clickPatientManagementButton();
+    mainPage.clickHistorylistButton();
+    cy.wait('@getHistoryUnlocked');
+
+    historyPage.findTableAndDoubleClickFirstRow();
+    cy.wait('@getStudyDetailsExposed');
+
+    /**
+     * 验证所有体位都是已曝光状态
+     */
+    cy.window()
+      .its('store')
+      .invoke('getState')
+      .its('examWorksCache')
+      .its('works')
+      .its(0)
+      .its('Views')
+      .then((views: any[]) => {
+        // 确认Views不为空
+        expect(views.length).to.be.greaterThan(0);
+        
+        // 确认所有Views都是已曝光
+        const allExposed = views.every(
+          (view) => view.expose_status === 'Exposed'
+        );
+        expect(allExposed).to.be.true;
+      });
+
+    /**
+     * 验证系统正确进入处理流程
+     */
+    cy.window()
+      .its('store')
+      .invoke('getState')
+      .its('businessFlow')
+      .its('currentFlow')
+      .should('eq', 'process');
+  });
+
+  it('应该正确从History进入处理流程', () => {
+    /**
+     * 验证从History进入处理流程的完整场景
+     */
+    loginPage.visit();
+    loginPage.login('admin', '123456');
+    cy.wait('@loginSuccess');
+
+    mainPage.clickPatientManagementButton();
+    mainPage.clickHistorylistButton();
+    cy.wait('@getHistoryUnlocked');
+
+    // 验证History记录的状态(已完成)
+    cy.window()
+      .its('store')
+      .invoke('getState')
+      .its('historyEntities')
+      .its('data')
+      .should('have.length.greaterThan', 0)
+      .its(0)
+      .should('have.property', 'study_status', 'Completed');
+
+    historyPage.findTableAndDoubleClickFirstRow();
+    cy.wait('@getStudyDetailsExposed');
+
+    // 验证进入处理流程
+    cy.window()
+      .its('store')
+      .invoke('getState')
+      .its('businessFlow')
+      .its('currentFlow')
+      .should('eq', 'process');
+
+    // 验证examWorksCache包含从History加载的数据
+    cy.window()
+      .its('store')
+      .invoke('getState')
+      .its('examWorksCache')
+      .its('works')
+      .its(0)
+      .should('have.property', 'StudyID', 'HIST20250912001');
+
+    // 验证bodyPositionList已转换
+    cy.window()
+      .its('store')
+      .invoke('getState')
+      .its('bodyPositionList')
+      .its('positions')
+      .should('have.length.greaterThan', 0);
+  });
+});

+ 176 - 0
cypress/e2e/patient/worklist-enter-exam-unexposed.cy.ts

@@ -0,0 +1,176 @@
+/**
+ * 测试文件: 进入检查 - Worklist双击进入检查(未曝光)
+ * 场景: 从Worklist双击未曝光任务,系统应进入检查界面
+ * 
+ * 路径分析:
+ * - POM: LoginPage, MainPage, WorklistPage, ExamPage
+ * - Mock: 
+ *   - mockLoginSuccess() - 登录成功
+ *   - mockFetchTwoWorks() - 获取工作列表
+ *   - mockGetStudyDetailsUnexposed() - 获取未曝光检查详情
+ * - 规格: 
+ *   ✓ 登录成功
+ *   ✓ 导航到工作列表
+ *   ✓ 工作列表显示至少2条记录
+ *   ✓ 双击第一条未曝光记录
+ *   ✓ 系统加载任务详细信息(未曝光状态)
+ *   ✓ 进入检查界面(businessFlow: 'exam')
+ *   ✓ 验证Redux状态正确
+ *   ✓ 验证界面元素可见
+ * 
+ * @see docs/测试/进入检查功能测试方案.md - 场景1
+ */
+
+import { mockLoginSuccess } from '../../support/mock/handlers/user';
+import { mockFetchTwoWorks } from '../../support/mock/handlers/worklist';
+import { mockGetStudyDetailsUnexposed } from '../../support/mock/handlers/study';
+import LoginPage from '../../support/pageObjects/LoginPage';
+import MainPage from '../../support/pageObjects/MainPage';
+import WorklistPage from '../../support/pageObjects/WorklistPage';
+import ExamPage from '../../support/pageObjects/ExamPage';
+
+describe('进入检查:Worklist双击进入检查(未曝光)', () => {
+  const loginPage = new LoginPage();
+  const mainPage = new MainPage();
+  const worklistPage = new WorklistPage();
+  const examPage = new ExamPage();
+
+  beforeEach(() => {
+    // 设置所有必要的Mock
+    mockLoginSuccess();
+    mockFetchTwoWorks();
+    // 使用第一条记录的StudyID
+    mockGetStudyDetailsUnexposed('250929163817805');
+  });
+
+  it('应该通过双击Worklist未曝光条目进入检查界面', () => {
+    /**
+     * Given: 用户已登录
+     * 步骤1(前置): 打开登录界面,完成登录
+     */
+    loginPage.visit();
+    loginPage.getUsernameInput().should('be.visible');
+    loginPage.login('admin', '123456');
+    
+    // 等待登录成功
+    cy.wait('@loginSuccess');
+    cy.contains('登录成功').should('be.visible', { timeout: 10000 });
+
+    /**
+     * When: 导航到Worklist
+     * 步骤1: 打开 Worklist
+     */
+    mainPage.clickPatientManagementButton();
+    mainPage.clickWorklistButton();
+
+    /**
+     * Then: 系统加载Worklist任务信息
+     * 步骤2(前置): 验证工作列表加载
+     */
+    cy.wait('@fetchTwoWorks').then((interception) => {
+      expect(interception.response?.statusCode).to.eq(200);
+    });
+
+    // 验证工作列表显示(至少2条记录)
+    worklistPage.getTable().should('be.visible');
+    worklistPage.getTable().find('tr').should('have.length.at.least', 2);
+
+    /**
+     * When: 用户双击第一条未曝光任务
+     * 步骤2: 双击任务条目
+     */
+    worklistPage.findTableAndDoubleClickFirstRow();
+
+    /**
+     * Then: 系统加载任务详细信息(未曝光状态)
+     * 步骤3: 验证API调用和返回数据
+     */
+    cy.wait('@getStudyDetailsUnexposed').then((interception) => {
+      expect(interception.response?.statusCode).to.eq(200);
+      
+      // 验证返回的数据包含未曝光的检查信息
+      const studyData = interception.response?.body.data;
+      expect(studyData).to.have.property('StudyID');
+      expect(studyData).to.have.property('series');
+      expect(studyData.series).to.be.an('array');
+      
+      // 验证所有images的expose_status为'Unexposed'
+      studyData.series.forEach((series: any) => {
+        series.images.forEach((image: any) => {
+          expect(image.expose_status).to.eq('Unexposed');
+        });
+      });
+    });
+
+    /**
+     * Then: 验证Redux状态 - examWorksCache
+     * 步骤4: 验证任务数据已缓存
+     */
+    cy.window()
+      .its('store')
+      .invoke('getState')
+      .its('examWorksCache')
+      .its('works')
+      .should('have.length', 1)
+      .its(0)
+      .should('have.property', 'StudyID', '250929163817805');
+
+    /**
+     * Then: 验证Redux状态 - businessFlow
+     * 步骤5: 验证进入检查流程(exam模式)
+     */
+    cy.window()
+      .its('store')
+      .invoke('getState')
+      .its('businessFlow')
+      .its('currentFlow')
+      .should('eq', 'exam');
+
+    /**
+     * Then: 验证检查界面元素可见
+     * 步骤6: 验证目标界面加载
+     */
+    examPage.verifyExamPageLoaded();
+    examPage.getToolbar().should('be.visible');
+    examPage.getContentArea().should('be.visible');
+  });
+
+  it('应该在Redux中正确保存任务的Views数据', () => {
+    /**
+     * 这个测试用例专注于验证数据转换的正确性
+     */
+    loginPage.visit();
+    loginPage.login('admin', '123456');
+    cy.wait('@loginSuccess');
+
+    mainPage.clickPatientManagementButton();
+    mainPage.clickWorklistButton();
+    cy.wait('@fetchTwoWorks');
+
+    worklistPage.findTableAndDoubleClickFirstRow();
+    cy.wait('@getStudyDetailsUnexposed');
+
+    /**
+     * 验证Views数组被正确构建
+     */
+    cy.window()
+      .its('store')
+      .invoke('getState')
+      .its('examWorksCache')
+      .its('works')
+      .its(0)
+      .its('Views')
+      .should('be.an', 'array')
+      .should('have.length.greaterThan', 0)
+      .then((views: any[]) => {
+        // 验证每个View包含必要的属性
+        views.forEach((view) => {
+          expect(view).to.have.property('view_id');
+          expect(view).to.have.property('view_description');
+          expect(view).to.have.property('expose_status', 'Unexposed');
+          expect(view).to.have.property('study_id');
+          expect(view).to.have.property('series_instance_uid');
+        });
+      });
+  });
+});

+ 229 - 0
cypress/e2e/patient/worklist-enter-process-exposed.cy.ts

@@ -0,0 +1,229 @@
+/**
+ * 测试文件: 进入检查 - Worklist双击进入处理(已曝光)
+ * 场景: 从Worklist双击已曝光任务,系统应进入处理界面
+ * 
+ * 路径分析:
+ * - POM: LoginPage, MainPage, WorklistPage
+ * - Mock: 
+ *   - mockLoginSuccess() - 登录成功
+ *   - mockFetchTwoWorks() - 获取工作列表
+ *   - mockGetStudyDetailsExposed() - 获取已曝光检查详情
+ * - 规格: 
+ *   ✓ 登录成功
+ *   ✓ 导航到工作列表
+ *   ✓ 工作列表显示至少2条记录
+ *   ✓ 双击第一条已曝光记录
+ *   ✓ 系统加载任务详细信息(已曝光状态)
+ *   ✓ 进入处理界面(businessFlow: 'process')
+ *   ✓ 验证Redux状态正确
+ *   ✓ 验证bodyPositionList已转换
+ * 
+ * @see docs/测试/进入检查功能测试方案.md - 场景2
+ */
+
+import { mockLoginSuccess } from '../../support/mock/handlers/user';
+import { mockFetchTwoWorks } from '../../support/mock/handlers/worklist';
+import { mockGetStudyDetailsExposed } from '../../support/mock/handlers/study';
+import LoginPage from '../../support/pageObjects/LoginPage';
+import MainPage from '../../support/pageObjects/MainPage';
+import WorklistPage from '../../support/pageObjects/WorklistPage';
+
+describe('进入检查:Worklist双击进入处理(已曝光)', () => {
+  const loginPage = new LoginPage();
+  const mainPage = new MainPage();
+  const worklistPage = new WorklistPage();
+
+  beforeEach(() => {
+    // 设置所有必要的Mock
+    mockLoginSuccess();
+    mockFetchTwoWorks();
+    // 使用第一条记录的StudyID,返回已曝光数据
+    mockGetStudyDetailsExposed('250929163817805');
+  });
+
+  it('应该通过双击Worklist已曝光条目进入处理界面', () => {
+    /**
+     * Given: 用户已登录
+     * 步骤1(前置): 打开登录界面,完成登录
+     */
+    loginPage.visit();
+    loginPage.getUsernameInput().should('be.visible');
+    loginPage.login('admin', '123456');
+    
+    // 等待登录成功
+    cy.wait('@loginSuccess');
+    cy.contains('登录成功').should('be.visible', { timeout: 10000 });
+
+    /**
+     * When: 导航到Worklist
+     * 步骤1: 打开 Worklist
+     */
+    mainPage.clickPatientManagementButton();
+    mainPage.clickWorklistButton();
+
+    /**
+     * Then: 系统加载Worklist任务信息
+     * 步骤2(前置): 验证工作列表加载
+     */
+    cy.wait('@fetchTwoWorks').then((interception) => {
+      expect(interception.response?.statusCode).to.eq(200);
+    });
+
+    // 验证工作列表显示(至少2条记录)
+    worklistPage.getTable().should('be.visible');
+    worklistPage.getTable().find('tr').should('have.length.at.least', 2);
+
+    /**
+     * When: 用户双击第一条已曝光任务
+     * 步骤2: 双击任务条目
+     */
+    worklistPage.findTableAndDoubleClickFirstRow();
+
+    /**
+     * Then: 系统加载任务详细信息(已曝光状态)
+     * 步骤3: 验证API调用和返回数据
+     */
+    cy.wait('@getStudyDetailsExposed').then((interception) => {
+      expect(interception.response?.statusCode).to.eq(200);
+      
+      // 验证返回的数据包含已曝光的检查信息
+      const studyData = interception.response?.body.data;
+      expect(studyData).to.have.property('study_id');
+      expect(studyData).to.have.property('series');
+      expect(studyData.series).to.be.an('array');
+      
+      // 验证所有images的expose_status为'Exposed'
+      studyData.series.forEach((series: any) => {
+        series.images.forEach((image: any) => {
+          expect(image.expose_status).to.eq('Exposed');
+          // 已曝光的图像应该有文件路径
+          expect(image.image_file_path).to.not.be.empty;
+        });
+      });
+    });
+
+    /**
+     * Then: 验证Redux状态 - examWorksCache
+     * 步骤4: 验证任务数据已缓存
+     */
+    cy.window()
+      .its('store')
+      .invoke('getState')
+      .its('examWorksCache')
+      .its('works')
+      .should('have.length', 1)
+      .its(0)
+      .should('have.property', 'StudyID', '250929163817805');
+
+    /**
+     * Then: 验证Redux状态 - businessFlow
+     * 步骤5: 验证进入处理流程(process模式)
+     */
+    cy.window()
+      .its('store')
+      .invoke('getState')
+      .its('businessFlow')
+      .its('currentFlow')
+      .should('eq', 'process');
+
+    /**
+     * Then: 验证Redux状态 - bodyPositionList
+     * 步骤6: 验证体位列表已正确转换
+     */
+    cy.window()
+      .its('store')
+      .invoke('getState')
+      .its('bodyPositionList')
+      .its('positions')
+      .should('have.length.greaterThan', 0);
+  });
+
+  it('应该在Redux中正确保存已曝光任务的Views数据', () => {
+    /**
+     * 这个测试用例专注于验证已曝光数据的正确性
+     */
+    loginPage.visit();
+    loginPage.login('admin', '123456');
+    cy.wait('@loginSuccess');
+
+    mainPage.clickPatientManagementButton();
+    mainPage.clickWorklistButton();
+    cy.wait('@fetchTwoWorks');
+
+    worklistPage.findTableAndDoubleClickFirstRow();
+    cy.wait('@getStudyDetailsExposed');
+
+    /**
+     * 验证Views数组包含已曝光数据
+     */
+    cy.window()
+      .its('store')
+      .invoke('getState')
+      .its('examWorksCache')
+      .its('works')
+      .its(0)
+      .its('Views')
+      .should('be.an', 'array')
+      .should('have.length.greaterThan', 0)
+      .then((views: any[]) => {
+        // 验证每个View都是已曝光状态
+        views.forEach((view) => {
+          expect(view).to.have.property('view_id');
+          expect(view).to.have.property('view_description');
+          expect(view).to.have.property('expose_status', 'Exposed');
+          expect(view).to.have.property('study_id');
+          expect(view).to.have.property('series_instance_uid');
+          // 已曝光的View应该有图像文件路径
+          expect(view).to.have.property('image_file_path');
+          expect(view.image_file_path).to.not.be.empty;
+        });
+      });
+  });
+
+  it('应该正确判断所有体位已曝光并进入处理流程', () => {
+    /**
+     * 这个测试用例验证曝光状态判断逻辑
+     */
+    loginPage.visit();
+    loginPage.login('admin', '123456');
+    cy.wait('@loginSuccess');
+
+    mainPage.clickPatientManagementButton();
+    mainPage.clickWorklistButton();
+    cy.wait('@fetchTwoWorks');
+
+    worklistPage.findTableAndDoubleClickFirstRow();
+    cy.wait('@getStudyDetailsExposed');
+
+    /**
+     * 验证所有体位都是已曝光状态
+     */
+    cy.window()
+      .its('store')
+      .invoke('getState')
+      .its('examWorksCache')
+      .its('works')
+      .its(0)
+      .its('Views')
+      .then((views: any[]) => {
+        // 确认Views不为空
+        expect(views.length).to.be.greaterThan(0);
+        
+        // 确认所有Views都是已曝光
+        const allExposed = views.every(
+          (view) => view.expose_status === 'Exposed'
+        );
+        expect(allExposed).to.be.true;
+      });
+
+    /**
+     * 验证系统正确进入处理流程
+     */
+    cy.window()
+      .its('store')
+      .invoke('getState')
+      .its('businessFlow')
+      .its('currentFlow')
+      .should('eq', 'process');
+  });
+});

+ 333 - 1
cypress/support/mock/handlers/study.ts

@@ -80,7 +80,339 @@ export function mockGetStudyArrived() {
 }
 
 /**
- * 获取检查信息 - 完整详情
+ * 获取检查信息 - 完整详情(未曝光)
+ * 
+ * @description 根据study_id获取完整的检查详细信息,所有images为未曝光状态
+ * @method GET
+ * @url /dr/api/v1/auth/study/{id}
+ * @access 需要认证
+ * 
+ * @param {string} studyId - 检查ID(study_id),路径参数
+ * 
+ * @returns {Object} data - 完整的检查详细信息,包含未曝光的series和images
+ * 
+ * @example
+ * mockGetStudyDetailsUnexposed('250929163817805');
+ * cy.wait('@getStudyDetailsUnexposed');
+ * 
+ * @see docs/测试/进入检查功能测试方案.md
+ */
+export function mockGetStudyDetailsUnexposed(studyId: string) {
+  cy.intercept('GET', `/dr/api/v1/auth/study/${studyId}`, {
+    statusCode: 200,
+    body: {
+      code: "0x000000",
+      description: "成功",
+      solution: "",
+      data: {
+        "@type": "type.googleapis.com/dr.study.Study",
+        study_instance_uid: "2.25.156.999999.0000.1.2.8323328.269954.1759135097.323784",
+        study_id: studyId,
+        public_study_id: "",
+        specific_character_set: "ISO_IR 192",
+        accession_number: "ACC0012345",
+        ref_physician: "Dr. Smith (Vet)",
+        patient_id: "PET007",
+        patient_name: "Buddy (Dog)",
+        patient_english_name: "Buddy en",
+        patient_former_name: "Buddy f",
+        patient_size: "Large",
+        other_patient_ids: "",
+        other_patient_names: "",
+        patient_age: "008Y",
+        patient_dob: "2025-06-10T03:12:36.181739Z",
+        patient_sex: "M",
+        sex_neutered: "",
+        pregnancy_status: "",
+        patient_state: "",
+        admitting_time: null,
+        priority: "",
+        reg_source: "",
+        study_description: "",
+        study_start_datetime: "2025-09-29T08:38:17.283651Z",
+        study_end_datetime: null,
+        scheduled_procedure_step_start_date: null,
+        performed_physician: "",
+        study_lock: "Unlocked",
+        folder_path: "",
+        operator_name: "OP987",
+        modality: "DX",
+        weight: 25,
+        thickness: 15,
+        length: 60,
+        patient_type: "Human",
+        study_type: "Normal",
+        owner_name: "owner1",
+        chip_number: "CHIP123456789",
+        variety: "Golden Retriever",
+        is_anaesthesia: true,
+        is_sedation: true,
+        mwl: "",
+        is_exported: false,
+        is_edited: false,
+        is_appended: false,
+        department: "",
+        mapped_status: false,
+        qc_result: false,
+        comment: "未曝光测试数据",
+        study_status: "Arrived",
+        sort: 0,
+        product: "DROS",
+        create_time: "2025-09-29T08:38:17.353598Z",
+        series: [
+          {
+            series_instance_uid: "2.25.156.999999.0000.1.3.8323328.269954.1759135097.323785",
+            study_instance_uid: "2.25.156.999999.0000.1.2.8323328.269954.1759135097.323784",
+            study_id: studyId,
+            procedure_id: "P0-0002",
+            patient_type: "Human",
+            body_part: "Human_SKULL",
+            performed_datetime: null,
+            performed_protocol_code_meaning: "颅骨前后位 + 侧位",
+            performed_protocol_code_value: "P0-0002",
+            sort: 1,
+            product: "DROS",
+            is_pre_install: true,
+            create_time: "2025-09-29T08:38:17.359308Z",
+            images: [
+              {
+                sop_instance_uid: "2.25.156.999999.0000.1.4.8323328.269954.1759135097.323786",
+                series_instance_uid: "2.25.156.999999.0000.1.3.8323328.269954.1759135097.323785",
+                study_instance_uid: "2.25.156.999999.0000.1.2.8323328.269954.1759135097.323784",
+                secondary_sop_uid: "",
+                study_id: studyId,
+                view_id: "View_DX_T_A_SK_AP_00",
+                view_description: "颅骨前后位",
+                patient_type: "Human",
+                body_part_id: "Human_SKULL",
+                anatomic_region: "Skull",
+                image_type: "expose",
+                image_file_path: "",
+                image_file: "",
+                thumbnail_file: "",
+                acquisition_mode: "RAD",
+                acquisition_context: null,
+                img_proc_context: null,
+                comment: "",
+                expose_status: "Unexposed",
+                expose_time: null,
+                judged_status: "NotJudged",
+                send_status: "Unsent",
+                export_status: "NotExported",
+                storage_status: "NotSaved",
+                ticket: "",
+                sort: 1,
+                product: "DROS",
+                is_pre_install: true,
+                create_time: "2025-09-29T08:38:17.361002Z"
+              },
+              {
+                sop_instance_uid: "2.25.156.999999.0000.1.4.8323328.269954.1759135097.323787",
+                series_instance_uid: "2.25.156.999999.0000.1.3.8323328.269954.1759135097.323785",
+                study_instance_uid: "2.25.156.999999.0000.1.2.8323328.269954.1759135097.323784",
+                secondary_sop_uid: "",
+                study_id: studyId,
+                view_id: "View_DX_T_A_SK_LAT_00",
+                view_description: "颅骨左侧位",
+                patient_type: "Human",
+                body_part_id: "Human_SKULL",
+                anatomic_region: "Skull",
+                image_type: "expose",
+                image_file_path: "",
+                image_file: "",
+                thumbnail_file: "",
+                acquisition_mode: "RAD",
+                acquisition_context: null,
+                img_proc_context: null,
+                comment: "",
+                expose_status: "Unexposed",
+                expose_time: null,
+                judged_status: "NotJudged",
+                send_status: "Unsent",
+                export_status: "NotExported",
+                storage_status: "NotSaved",
+                ticket: "",
+                sort: 2,
+                product: "DROS",
+                is_pre_install: true,
+                create_time: "2025-09-29T08:38:17.362195Z"
+              }
+            ]
+          }
+        ]
+      }
+    }
+  }).as('getStudyDetailsUnexposed');
+}
+
+/**
+ * 获取检查信息 - 完整详情(已曝光)
+ * 
+ * @description 根据study_id获取完整的检查详细信息,所有images为已曝光状态
+ * @method GET
+ * @url /dr/api/v1/auth/study/{id}
+ * @access 需要认证
+ * 
+ * @param {string} studyId - 检查ID(study_id),路径参数
+ * 
+ * @returns {Object} data - 完整的检查详细信息,包含已曝光的series和images
+ * 
+ * @example
+ * mockGetStudyDetailsExposed('250929163817805');
+ * cy.wait('@getStudyDetailsExposed');
+ * 
+ * @see docs/测试/进入检查功能测试方案.md
+ */
+export function mockGetStudyDetailsExposed(studyId: string) {
+  cy.intercept('GET', `/dr/api/v1/auth/study/${studyId}`, {
+    statusCode: 200,
+    body: {
+      code: "0x000000",
+      description: "成功",
+      solution: "",
+      data: {
+        "@type": "type.googleapis.com/dr.study.Study",
+        study_instance_uid: "2.25.156.999999.0000.1.2.8323328.269954.1759135097.323784",
+        study_id: studyId,
+        public_study_id: "",
+        specific_character_set: "ISO_IR 192",
+        accession_number: "ACC0012345",
+        ref_physician: "Dr. Smith (Vet)",
+        patient_id: "PET007",
+        patient_name: "Buddy (Dog)",
+        patient_english_name: "Buddy en",
+        patient_former_name: "Buddy f",
+        patient_size: "Large",
+        other_patient_ids: "",
+        other_patient_names: "",
+        patient_age: "008Y",
+        patient_dob: "2025-06-10T03:12:36.181739Z",
+        patient_sex: "M",
+        sex_neutered: "",
+        pregnancy_status: "",
+        patient_state: "",
+        admitting_time: null,
+        priority: "",
+        reg_source: "",
+        study_description: "",
+        study_start_datetime: "2025-09-29T08:38:17.283651Z",
+        study_end_datetime: null,
+        scheduled_procedure_step_start_date: null,
+        performed_physician: "",
+        study_lock: "Unlocked",
+        folder_path: "",
+        operator_name: "OP987",
+        modality: "DX",
+        weight: 25,
+        thickness: 15,
+        length: 60,
+        patient_type: "Human",
+        study_type: "Normal",
+        owner_name: "owner1",
+        chip_number: "CHIP123456789",
+        variety: "Golden Retriever",
+        is_anaesthesia: true,
+        is_sedation: true,
+        mwl: "",
+        is_exported: false,
+        is_edited: false,
+        is_appended: false,
+        department: "",
+        mapped_status: false,
+        qc_result: false,
+        comment: "已曝光测试数据",
+        study_status: "Completed",
+        sort: 0,
+        product: "DROS",
+        create_time: "2025-09-29T08:38:17.353598Z",
+        series: [
+          {
+            series_instance_uid: "2.25.156.999999.0000.1.3.8323328.269954.1759135097.323785",
+            study_instance_uid: "2.25.156.999999.0000.1.2.8323328.269954.1759135097.323784",
+            study_id: studyId,
+            procedure_id: "P0-0002",
+            patient_type: "Human",
+            body_part: "Human_SKULL",
+            performed_datetime: "2025-09-29T09:00:00.000000Z",
+            performed_protocol_code_meaning: "颅骨前后位 + 侧位",
+            performed_protocol_code_value: "P0-0002",
+            sort: 1,
+            product: "DROS",
+            is_pre_install: true,
+            create_time: "2025-09-29T08:38:17.359308Z",
+            images: [
+              {
+                sop_instance_uid: "2.25.156.999999.0000.1.4.8323328.269954.1759135097.323786",
+                series_instance_uid: "2.25.156.999999.0000.1.3.8323328.269954.1759135097.323785",
+                study_instance_uid: "2.25.156.999999.0000.1.2.8323328.269954.1759135097.323784",
+                secondary_sop_uid: "",
+                study_id: studyId,
+                view_id: "View_DX_T_A_SK_AP_00",
+                view_description: "颅骨前后位",
+                patient_type: "Human",
+                body_part_id: "Human_SKULL",
+                anatomic_region: "Skull",
+                image_type: "expose",
+                image_file_path: "/path/to/image1.dcm",
+                image_file: "/path/to/image1.dcm",
+                thumbnail_file: "/path/to/image1_thumb.jpg",
+                acquisition_mode: "RAD",
+                acquisition_context: null,
+                img_proc_context: null,
+                comment: "",
+                expose_status: "Exposed",
+                expose_time: "2025-09-29T09:00:00.000000Z",
+                judged_status: "NotJudged",
+                send_status: "Unsent",
+                export_status: "NotExported",
+                storage_status: "Saved",
+                ticket: "",
+                sort: 1,
+                product: "DROS",
+                is_pre_install: true,
+                create_time: "2025-09-29T08:38:17.361002Z"
+              },
+              {
+                sop_instance_uid: "2.25.156.999999.0000.1.4.8323328.269954.1759135097.323787",
+                series_instance_uid: "2.25.156.999999.0000.1.3.8323328.269954.1759135097.323785",
+                study_instance_uid: "2.25.156.999999.0000.1.2.8323328.269954.1759135097.323784",
+                secondary_sop_uid: "",
+                study_id: studyId,
+                view_id: "View_DX_T_A_SK_LAT_00",
+                view_description: "颅骨左侧位",
+                patient_type: "Human",
+                body_part_id: "Human_SKULL",
+                anatomic_region: "Skull",
+                image_type: "expose",
+                image_file_path: "/path/to/image2.dcm",
+                image_file: "/path/to/image2.dcm",
+                thumbnail_file: "/path/to/image2_thumb.jpg",
+                acquisition_mode: "RAD",
+                acquisition_context: null,
+                img_proc_context: null,
+                comment: "",
+                expose_status: "Exposed",
+                expose_time: "2025-09-29T09:00:30.000000Z",
+                judged_status: "NotJudged",
+                send_status: "Unsent",
+                export_status: "NotExported",
+                storage_status: "Saved",
+                ticket: "",
+                sort: 2,
+                product: "DROS",
+                is_pre_install: true,
+                create_time: "2025-09-29T08:38:17.362195Z"
+              }
+            ]
+          }
+        ]
+      }
+    }
+  }).as('getStudyDetailsExposed');
+}
+
+/**
+ * 获取检查信息 - 完整详情(通用,保持向后兼容)
  * 
  * @description 根据study_id获取完整的检查详细信息,包含series和images
  * @method GET

+ 12 - 0
cypress/support/pageObjects/HistoryPage.ts

@@ -10,6 +10,18 @@ class HistoryPage {
     return cy.get('table');
   }
 
+  /**
+   * 双击表格第一行
+   */
+  findTableAndDoubleClickFirstRow() {
+    cy.get('table').within(() => {
+      cy.get('tbody tr[data-testid="row-0"]')
+        .scrollIntoView()
+        .should('be.visible')
+        .dblclick({ force: true });
+    });
+  }
+
   /**
    * 点击指定索引的行
    * @param index 行索引(从0开始)

+ 242 - 0
docs/测试/进入检查功能测试方案.md

@@ -0,0 +1,242 @@
+# 进入检查功能测试方案
+
+## 功能概述
+
+本测试方案覆盖从Worklist和History列表双击进入检查的功能。根据任务的曝光状态,系统会自动导航到不同的界面:
+- **未曝光任务**:进入检查界面(Exam)
+- **已曝光任务**:进入处理界面(Process)
+
+## 核心实现逻辑
+
+### 业务流程
+
+```typescript
+worklistToExam(task) {
+  1. 调用 fetchTaskDetails(StudyID) 获取完整检查信息
+  2. 转换数据格式,包含所有Views(体位)
+  3. 判断曝光状态:
+     - 所有体位已曝光 → 进入"处理界面"(businessFlow: 'process')
+     - 有未曝光体位 → 进入"检查界面"(businessFlow: 'exam')
+  4. 更新Redux状态:
+     - clearWorks() - 清空缓存
+     - addWork(updatedTask) - 保存任务
+     - setBusinessFlow() - 设置流程
+}
+```
+
+### 关键文件
+- `src/domain/patient/worklistToExam.ts` - 核心业务逻辑
+- `src/pages/patient/worklist.tsx` - Worklist页面(双击触发)
+- `src/pages/patient/HistoryList.tsx` - History页面(双击触发)
+
+## 测试场景
+
+### 场景1:Worklist双击进入检查(未曝光)
+**测试文件**:`cypress/e2e/patient/worklist-enter-exam-unexposed.cy.ts`
+
+**前置条件**:
+- 用户已登录
+- Worklist中存在未曝光任务
+
+**测试步骤**:
+1. 导航到Worklist
+2. 双击第一条未曝光任务
+3. 系统调用API获取任务详情
+4. 系统判断为未曝光状态
+5. 进入检查界面
+
+**验证点**:
+- API调用验证:
+  - `GET /dr/api/v1/auth/study/{id}` 被正确调用
+  - 返回数据包含未曝光的series和images
+- Redux状态验证:
+  - `examWorksCache` 包含正确的任务数据
+  - `businessFlow` 状态为 'exam'
+- 界面验证:
+  - 检查界面的主要元素可见(工具栏、内容区域等)
+
+---
+
+### 场景2:Worklist双击进入处理(已曝光)
+**测试文件**:`cypress/e2e/patient/worklist-enter-process-exposed.cy.ts`
+
+**前置条件**:
+- 用户已登录
+- Worklist中存在已曝光任务
+
+**测试步骤**:
+1. 导航到Worklist
+2. 双击第一条已曝光任务
+3. 系统调用API获取任务详情
+4. 系统判断为已曝光状态
+5. 进入处理界面
+
+**验证点**:
+- API调用验证:
+  - `GET /dr/api/v1/auth/study/{id}` 被正确调用
+  - 返回数据的所有images状态为 'Exposed'
+- Redux状态验证:
+  - `examWorksCache` 包含正确的任务数据
+  - `businessFlow` 状态为 'process'
+  - `bodyPositionList` 已正确转换
+- 界面验证:
+  - 处理界面的主要元素可见
+
+---
+
+### 场景3:History双击进入检查(未曝光)
+**测试文件**:`cypress/e2e/patient/history-enter-exam-unexposed.cy.ts`
+
+**前置条件**:
+- 用户已登录
+- History中存在未曝光任务
+
+**测试步骤**:
+1. 导航到History
+2. 双击第一条未曝光任务
+3. 系统调用API获取任务详情
+4. 系统判断为未曝光状态
+5. 进入检查界面
+
+**验证点**:
+- API调用验证:同场景1
+- Redux状态验证:同场景1
+- 界面验证:同场景1
+
+---
+
+### 场景4:History双击进入处理(已曝光)
+**测试文件**:`cypress/e2e/patient/history-enter-process-exposed.cy.ts`
+
+**前置条件**:
+- 用户已登录
+- History中存在已曝光任务
+
+**测试步骤**:
+1. 导航到History
+2. 双击第一条已曝光任务
+3. 系统调用API获取任务详情
+4. 系统判断为已曝光状态
+5. 进入处理界面
+
+**验证点**:
+- API调用验证:同场景2
+- Redux状态验证:同场景2
+- 界面验证:同场景2
+
+## 测试数据准备
+
+### Mock数据需求
+
+#### 1. 未曝光任务详情
+```typescript
+mockGetStudyDetailsUnexposed(studyId: string)
+```
+- 返回包含未曝光images的完整study信息
+- `expose_status: 'Unexposed'`
+
+#### 2. 已曝光任务详情
+```typescript
+mockGetStudyDetailsExposed(studyId: string)
+```
+- 返回包含已曝光images的完整study信息
+- `expose_status: 'Exposed'`
+- 包含图片路径信息
+
+#### 3. Worklist数据
+```typescript
+mockFetchTwoWorks()
+```
+- 返回至少2条任务记录(已存在)
+
+#### 4. History数据
+```typescript
+mockFetchTwoHistoryWorks()
+```
+- 返回至少2条历史任务记录
+
+## Page Object增强
+
+### HistoryPage
+需要添加:
+```typescript
+findTableAndDoubleClickFirstRow() {
+  cy.get('table').within(() => {
+    cy.get('tbody tr[data-testid="row-0"]')
+      .scrollIntoView()
+      .should('be.visible')
+      .dblclick({ force: true });
+  });
+}
+```
+
+### ExamPage
+已有的验证方法:
+- `verifyExamPageLoaded()` - 验证检查界面加载
+- `getToolbar()` - 获取工具栏
+- `getContentArea()` - 获取内容区域
+
+### ProcessPage(如需要)
+需要创建新的Page Object来验证处理界面元素
+
+## Redux状态验证模式
+
+### 验证examWorksCache
+```typescript
+cy.window()
+  .its('store')
+  .invoke('getState')
+  .its('examWorksCache')
+  .its('works')
+  .should('have.length', 1)
+  .its(0)
+  .should('have.property', 'StudyID');
+```
+
+### 验证businessFlow
+```typescript
+cy.window()
+  .its('store')
+  .invoke('getState')
+  .its('businessFlow')
+  .its('currentFlow')
+  .should('eq', 'exam'); // 或 'process'
+```
+
+### 验证bodyPositionList(处理界面)
+```typescript
+cy.window()
+  .its('store')
+  .invoke('getState')
+  .its('bodyPositionList')
+  .its('positions')
+  .should('have.length.greaterThan', 0);
+```
+
+## 执行顺序
+
+1. ✅ 创建测试方案文档(本文档)
+2. 创建/增强Mock handlers
+3. 增强Page Objects
+4. 编写测试用例代码:
+   - `worklist-enter-exam-unexposed.cy.ts`
+   - `worklist-enter-process-exposed.cy.ts`
+   - `history-enter-exam-unexposed.cy.ts`
+   - `history-enter-process-exposed.cy.ts`
+
+## 注意事项
+
+1. **不验证URL变化**:根据用户反馈,去掉URL验证点
+2. **关注页面元素**:重点验证目标页面的关键元素可见性
+3. **Redux状态**:每个场景都应验证相应的Redux状态
+4. **API响应**:确保Mock返回正确的曝光状态数据
+5. **等待策略**:使用`cy.wait()`等待API调用完成后再验证状态
+
+## 成功标准
+
+所有4个测试场景都应:
+- ✅ 正确触发双击事件
+- ✅ 正确调用API
+- ✅ 正确更新Redux状态
+- ✅ 正确显示目标界面元素
+- ✅ 测试稳定可重复执行