验证图像注释的创建、保存、加载、重现、编辑和删除功能,确保所有类型的注释工具都能正常工作,并且注释数据能够正确持久化和恢复。
基于实际代码 src/pages/view/components/MeasurementPanel.tsx,系统支持以下测量工具:
| 工具名称 | 工具标识(action) | 图标名称 | 描述 | 优先级 |
|---|---|---|---|---|
| 线段测量 | 线段测量 | btn_LineMeasurement | 测量两点之间的直线距离 | P0 |
| 角度测量 | 角度测量 | btn_AngleMeasurement | 测量三点形成的角度 | P0 |
| 清除测量 | 清除测量 | btn_ClearMeasurement | 清除所有测量标记 | P0 |
| 测量校正 | 测量校正 | MeasurementCalibration | 校正测量比例 | P1 |
| 工具名称 | 工具标识(action) | 图标名称 | 描述 | 优先级 |
|---|---|---|---|---|
| Cobb角 | Cobb角 | btn_Cobbangle | 脊柱侧弯角度测量 | P1 |
| 工具名称 | 工具标识(action) | 图标名称 | 描述 | 优先级 |
|---|---|---|---|---|
| 髋臼水平角 | 髋臼水平角 | btn_DAR | 髋关节发育评估 | P1 |
| 胫骨平台夹角 | 胫骨平台夹角 | btn_TPA | 膝关节角度测量 | P1 |
| 髋关节牵引指数 | 髋关节牵引指数 | btn_HDI | 髋关节松弛度评估 | P1 |
| 髋关节水平角 | 髋关节水平角 | btn_NHA | 髋关节角度测量 | P1 |
| 心锥比 | 心锥比 | btn_VHS | 心脏大小评估 | P1 |
| 胫骨平台骨切开术 | 胫骨平台骨切开术 | btn_TPLO | TPLO手术规划 | P2 |
| 胫骨结节前移术 | 胫骨结节前移术 | btn_TTA | TTA手术规划 | P2 |
| 胫骨结节前移术5点测量法 | 胫骨结节前移术5点测量法 | btn_TTA2 | TTA手术规划(5点法) | P2 |
| 水平截骨术 | 水平截骨术 | btn_CBLO | CBLO手术规划 | P2 |
| 股骨头覆盖率 | 股骨头覆盖率 | btn_DLS | 髋关节覆盖评估 | P2 |
| 髋臼背覆盖 | 髋臼背覆盖 | btn_DAC | 髋关节覆盖评估 | P2 |
| 拆线长度测量 | 拆线长度测量 | btn_FoldLine | 折线长度测量 | P1 |
| 多边形长度测量 | 多边形长度测量 | btn_Polygon | 多边形周长测量 | P1 |
| 找圆心 | 找圆心 | btn_CenterCircle | 定位圆形中心点 | P1 |
| 找中线 | 找中线 | btn_CenterLine | 计算中线位置 | P1 |
| 找中点 | 找中点 | btn_Midpoint | 计算线段中点 | P1 |
| 直线垂直倾斜度 | 直线垂直倾斜度 | btn_VerticalInclination | 垂直倾斜角度 | P1 |
| 直线水平倾斜度 | 直线水平倾斜度 | btn_HorizontalInclination | 水平倾斜角度 | P1 |
| 矩形区域灰度 | 矩形区域灰度 | btn_RectangularGrayscale | 矩形区域灰度分析 | P1 |
| 直线灰度 | 直线灰度 | btn_LineGrayscale | 直线灰度分布分析 | P1 |
工具总数: 24个 (4个基础工具 + 1个专业工具 + 19个宠物专用工具)
注意: 这些测量工具主要用于宠物医学影像分析,包括骨科手术规划、心脏评估、髋关节评估等专业应用场景。
文件路径: cypress/e2e/process/annotation-creation.cy.ts
测试目标: 验证LengthTool创建长度测量注释
前置条件:
测试步骤:
验证点:
POST /dr/api/v1/auth/image/{id}/annotationMock配置:
cy.intercept('POST', '/dr/api/v1/auth/image/*/annotation', {
statusCode: 200,
body: {
code: '0x000000',
description: 'Success',
data: {}
}
}).as('saveAnnotation');
优先级: P0 (高)
测试目标: 验证AngleTool创建角度测量注释
前置条件: 同TC-ANN-CREATE-01
测试步骤:
验证点:
优先级: P0 (高)
测试目标: 验证LabelTool创建文本标注
前置条件: 同TC-ANN-CREATE-01
测试步骤:
验证点:
优先级: P0 (高)
测试目标: 验证Cobb角测量工具创建专业测量注释
前置条件: 同TC-ANN-CREATE-01
测试步骤:
验证点:
优先级: P1 (中)
测试目标: 验证宠物专用测量工具(以髋关节水平角为例)创建专业测量
前置条件: 同TC-ANN-CREATE-01
测试步骤:
验证点:
优先级: P1 (中)
注: 其他宠物专用测量工具(如胫骨平台夹角、心锥比、矩形区域灰度等)的测试方法类似,主要验证:
文件路径: cypress/e2e/process/annotation-save.cy.ts
测试目标: 验证注释创建后自动保存
前置条件: 同TC-ANN-CREATE-01
测试步骤:
验证点:
/dr/api/v1/auth/image/{sopInstanceUid}/annotation优先级: P0 (高)
测试目标: 验证编辑注释时的防抖保存
前置条件: 已创建一个注释
测试步骤:
验证点:
优先级: P0 (高)
测试目标: 验证多个注释的批量保存
前置条件: 同TC-ANN-CREATE-01
测试步骤:
验证点:
优先级: P1 (中)
测试目标: 验证保存失败时的错误处理
前置条件: Mock保存API返回错误
测试步骤:
验证点:
Mock配置:
cy.intercept('POST', '/dr/api/v1/auth/image/*/annotation', {
statusCode: 500,
body: {
code: '0x000001',
description: 'Internal Server Error'
}
}).as('saveAnnotationFail');
优先级: P0 (高)
文件路径: cypress/e2e/process/annotation-load.cy.ts
测试目标: 验证打开图像时自动加载注释
前置条件:
测试步骤:
验证点:
GET /dr/api/v1/auth/image/{sopInstanceUid}/annotationMock数据示例:
{
"code": "0x000000",
"description": "Success",
"data": [
{
"id": "ann-001",
"toolName": "LengthTool",
"sopInstanceUid": "1.2.3.4.5",
"handles": {
"points": [
{"x": 100, "y": 100, "z": 0},
{"x": 200, "y": 200, "z": 0}
]
},
"metadata": {
"viewPlaneNormal": [0, 0, 1],
"viewUp": [0, -1, 0],
"FrameOfReferenceUID": "1.2.3",
"referencedImageId": "image-1"
},
"cachedStats": {
"length": 141.42
},
"createdAt": "2025-12-16T10:00:00Z",
"updatedAt": "2025-12-16T10:00:00Z"
}
]
}
优先级: P0 (高)
测试目标: 验证同时加载多个不同类型的注释
前置条件: Mock包含3个不同类型注释的数据
测试步骤:
验证点:
优先级: P0 (高)
测试目标: 验证图像无注释时的处理
前置条件: Mock返回空注释列表
测试步骤:
验证点:
Mock配置:
cy.intercept('GET', '/dr/api/v1/auth/image/*/annotation', {
statusCode: 200,
body: {
code: '0x000000',
description: 'Success',
data: []
}
}).as('getEmptyAnnotations');
优先级: P1 (中)
测试目标: 验证加载注释失败时的处理
前置条件: Mock API返回错误
测试步骤:
验证点:
优先级: P1 (中)
测试目标: 验证加载的注释数据格式验证
前置条件: Mock包含格式错误的注释数据
测试步骤:
验证点:
优先级: P1 (中)
文件路径: cypress/e2e/process/annotation-edit.cy.ts
测试目标: 验证通过拖拽手柄修改注释
前置条件: 已加载包含注释的图像
测试步骤:
验证点:
优先级: P0 (高)
测试目标: 验证修改文本标注的内容
前置条件: 已加载包含文本标注的图像
测试步骤:
验证点:
优先级: P0 (高)
测试目标: 验证多用户编辑冲突的处理
前置条件: Mock服务器返回版本冲突
测试步骤:
验证点:
优先级: P2 (低)
文件路径: cypress/e2e/process/annotation-delete.cy.ts
测试目标: 验证删除单个注释
前置条件: 已加载包含注释的图像
测试步骤:
验证点:
优先级: P0 (高)
测试目标: 验证批量删除多个注释
前置条件: 已加载包含多个注释的图像
测试步骤:
验证点:
优先级: P1 (中)
测试目标: 验证删除确认对话框
前置条件: 配置启用删除确认
测试步骤:
验证点:
优先级: P1 (中)
文件路径: cypress/e2e/process/annotation-api.cy.ts
测试目标: 验证保存注释的API请求格式
前置条件: 已创建注释
测试步骤:
验证点:
优先级: P0 (高)
测试目标: 验证获取注释的API响应处理
前置条件: Mock正确格式的响应
测试步骤:
验证点:
优先级: P0 (高)
测试目标: 验证各种API错误码的处理
前置条件: Mock不同错误响应
测试步骤:
验证点:
优先级: P1 (中)
文件路径: cypress/e2e/process/annotation-performance.cy.ts
测试目标: 验证加载大量注释时的性能
前置条件: Mock包含100个注释的数据
测试步骤:
验证点:
优先级: P1 (中)
测试目标: 验证注释数据大小限制
前置条件: 尝试创建超大注释
测试步骤:
验证点:
优先级: P2 (低)
测试目标: 验证快速连续操作的性能
前置条件: 已加载图像
测试步骤:
验证点:
优先级: P1 (中)
{
"id": "length-001",
"toolName": "LengthTool",
"sopInstanceUid": "1.2.840.10008.5.1.4.1.1.2.1",
"handles": {
"points": [
{"x": 100, "y": 100, "z": 0},
{"x": 200, "y": 200, "z": 0}
]
},
"metadata": {
"viewPlaneNormal": [0, 0, 1],
"viewUp": [0, -1, 0],
"FrameOfReferenceUID": "1.2.3.4.5",
"referencedImageId": "wadouri:http://example.com/image.dcm"
},
"cachedStats": {
"length": 141.42
},
"label": "Length Measurement",
"createdAt": "2025-12-16T10:00:00.000Z",
"updatedAt": "2025-12-16T10:00:00.000Z",
"userId": "user-001"
}
{
"id": "angle-001",
"toolName": "AngleTool",
"sopInstanceUid": "1.2.840.10008.5.1.4.1.1.2.1",
"handles": {
"points": [
{"x": 100, "y": 100, "z": 0},
{"x": 150, "y": 150, "z": 0},
{"x": 200, "y": 100, "z": 0}
]
},
"metadata": {
"viewPlaneNormal": [0, 0, 1],
"viewUp": [0, -1, 0],
"FrameOfReferenceUID": "1.2.3.4.5",
"referencedImageId": "wadouri:http://example.com/image.dcm"
},
"cachedStats": {
"angle": 90.0
},
"label": "Angle Measurement",
"createdAt": "2025-12-16T10:01:00.000Z",
"updatedAt": "2025-12-16T10:01:00.000Z",
"userId": "user-001"
}
{
"id": "label-001",
"toolName": "LabelTool",
"sopInstanceUid": "1.2.840.10008.5.1.4.1.1.2.1",
"handles": {
"points": [
{"x": 150, "y": 150, "z": 0}
],
"textBox": {"x": 160, "y": 150}
},
"metadata": {
"viewPlaneNormal": [0, 0, 1],
"viewUp": [0, -1, 0],
"FrameOfReferenceUID": "1.2.3.4.5",
"referencedImageId": "wadouri:http://example.com/image.dcm"
},
"label": "疑似结节",
"createdAt": "2025-12-16T10:02:00.000Z",
"updatedAt": "2025-12-16T10:02:00.000Z",
"userId": "user-001"
}
cypress/support/mock/handlers/annotation.ts/**
* Mock获取注释成功
*/
export const mockGetAnnotationsSuccess = (annotations: any[]) => {
cy.intercept('GET', '/dr/api/v1/auth/image/*/annotation', {
statusCode: 200,
body: {
code: '0x000000',
description: 'Success',
data: annotations
}
}).as('getAnnotations');
};
/**
* Mock保存注释成功
*/
export const mockSaveAnnotationSuccess = () => {
cy.intercept('POST', '/dr/api/v1/auth/image/*/annotation', {
statusCode: 200,
body: {
code: '0x000000',
description: 'Success',
data: {}
}
}).as('saveAnnotation');
};
/**
* Mock删除注释成功
*/
export const mockDeleteAnnotationSuccess = () => {
cy.intercept('DELETE', '/dr/api/v1/auth/image/*/annotation/*', {
statusCode: 200,
body: {
code: '0x000000',
description: 'Success',
data: {}
}
}).as('deleteAnnotation');
};
/**
* Mock API失败
*/
export const mockAnnotationAPIFail = (statusCode: number = 500) => {
cy.intercept('**/annotation**', {
statusCode: statusCode,
body: {
code: '0x000001',
description: 'Internal Server Error'
}
}).as('annotationAPIFail');
};
文件: cypress/support/pageObjects/ProcessPage.ts
class ProcessPage {
// 工具选择
selectLengthTool() {
cy.get('[data-testid="length-tool-btn"]').click();
}
selectAngleTool() {
cy.get('[data-testid="angle-tool-btn"]').click();
}
selectLabelTool() {
cy.get('[data-testid="label-tool-btn"]').click();
}
selectAreaTool() {
cy.get('[data-testid="area-tool-btn"]').click();
}
// 注释操作
createLengthAnnotation(point1: {x: number, y: number}, point2: {x: number, y: number}) {
this.selectLengthTool();
cy.get('.viewport-canvas').click(point1.x, point1.y);
cy.get('.viewport-canvas').click(point2.x, point2.y);
}
createAngleAnnotation(
point1: {x: number, y: number},
point2: {x: number, y: number},
point3: {x: number, y: number}
) {
this.selectAngleTool();
cy.get('.viewport-canvas').click(point1.x, point1.y);
cy.get('.viewport-canvas').click(point2.x, point2.y);
cy.get('.viewport-canvas').click(point3.x, point3.y);
}
createLabelAnnotation(position: {x: number, y: number}, text: string) {
this.selectLabelTool();
cy.get('.viewport-canvas').click(position.x, position.y);
cy.get('input[type="text"]').type(text);
cy.get('input[type="text"]').type('{enter}');
}
// 验证注释显示
verifyAnnotationVisible(toolName: string) {
cy.get(`.annotation-${toolName.toLowerCase()}`).should('be.visible');
}
verifyMeasurementValue(value: string) {
cy.get('.measurement-value').should('contain', value);
}
// 注释编辑
selectAnnotation(annotationId: string) {
cy.get(`[data-annotation-id="${annotationId}"]`).click();
}
deleteSelectedAnnotation() {
cy.get('body').type('{del}');
}
// Redux状态验证
verifyAnnotationInStore(annotationId: string) {
cy.window().its('store').invoke('getState')
.its('annotations').its('list')
.should('contain', annotationId);
}
// API调用验证
waitForSaveAnnotation() {
cy.wait('@saveAnnotation');
}
waitForLoadAnnotations() {
cy.wait('@getAnnotations');
}
verifyAnnotationCount(count: number) {
cy.get('.annotation-item').should('have.length', count);
}
}
export default ProcessPage;
时间: 3-4天
时间: 2-3天
时间: 1-2天
| 风险项 | 影响 | 概率 | 缓解措施 |
|---|---|---|---|
| Cornerstone Tools集成问题 | 高 | 中 | 深入学习Cornerstone API,准备降级方案 |
| 坐标系转换错误 | 高 | 中 | 详细测试各种图像类型,验证坐标精度 |
| 大数据量性能问题 | 中 | 中 | 实现虚拟化渲染,分批加载 |
| 浏览器兼容性问题 | 中 | 低 | 多浏览器测试,使用polyfill |
| 风险项 | 影响 | 概率 | 缓解措施 |
|---|---|---|---|
| 需求变更 | 中 | 中 | 模块化设计,易于扩展 |
| 用户接受度低 | 中 | 低 | 用户测试,收集反馈 |
| 数据丢失 | 高 | 低 | 实现本地缓存,离线支持 |
// cypress.config.ts
export default {
e2e: {
baseUrl: 'http://localhost:3000',
viewportWidth: 1920,
viewportHeight: 1080,
video: false,
defaultCommandTimeout: 10000
}
};
npx cypress run --spec "cypress/e2e/process/annotation-*.cy.ts"
# 创建功能测试
npx cypress run --spec "cypress/e2e/process/annotation-creation.cy.ts"
# 保存功能测试
npx cypress run --spec "cypress/e2e/process/annotation-save.cy.ts"
# 加载功能测试
npx cypress run --spec "cypress/e2e/process/annotation-load.cy.ts"
npx cypress open
然后选择相应的测试文件。
| 测试套件 | 用例总数 | 通过 | 失败 | 跳过 | 通过率 | 执行时间 |
|---|---|---|---|---|---|---|
| 注释创建 | 5 | - | - | - | - | - |
| 注释保存 | 4 | - | - | - | - | - |
| 注释加载 | 5 | - | - | - | - | - |
| 注释编辑 | 3 | - | - | - | - | - |
| 注释删除 | 3 | - | - | - | - | - |
| API测试 | 3 | - | - | - | - | - |
| 性能测试 | 3 | - | - | - | - | - |
| 总计 | 26 | - | - | - | - | - |
缺陷ID: BUG-ANN-XXX
测试用例: TC-ANN-XXX
严重程度: Critical / Major / Minor
优先级: P0 / P1 / P2
复现步骤:
1. ...
2. ...
3. ...
实际结果: ...
预期结果: ...
附件: (截图/视频)
interface AnnotationData {
// 基础信息
id: string;
toolName: string;
sopInstanceUid: string;
// 几何数据
handles: {
points: Point3[];
activeHandleIndex?: number;
textBox?: Point2;
};
// 元数据
metadata: {
viewPlaneNormal: Point3;
viewUp: Point3;
FrameOfReferenceUID: string;
referencedImageId: string;
};
// 计算结果
cachedStats?: {
length?: number;
angle?: number;
area?: number;
};
// 显示属性
label?: string;
highlighted?: boolean;
isSelected?: boolean;
// 时间戳
createdAt: string;
updatedAt: string;
userId?: string;
}
请求:
GET /dr/api/v1/auth/image/{sopInstanceUid}/annotation
响应:
{
"code": "0x000000",
"description": "Success",
"data": [AnnotationData]
}
请求:
POST /dr/api/v1/auth/image/{sopInstanceUid}/annotation
Content-Type: application/json
AnnotationData
响应:
{
"code": "0x000000",
"description": "Success",
"data": {}
}
请求:
DELETE /dr/api/v1/auth/image/{sopInstanceUid}/annotation/{annotationId}
响应:
{
"code": "0x000000",
"description": "Success",
"data": {}
}
测试方案状态: ✅ 已完成