fix: Remove dead code and fix rectangle roi rehydration (#5490)
* Remove dead code and fix rectangle roi rehydration * Revert bun.lock * Remove hardcoded modality * Add e2e tests * Update screewnshots * Adjustments to tests * disabvle test for now * Add test again * Use ohif data soure * Revert changes * Test * Update testdata to include point * Improve tests * Update screenshot --------- Co-authored-by: Andrey Fedorov <andrey.fedorov@gmail.com> Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
@ -11,16 +11,11 @@ import {
|
|||||||
RectangleROITool,
|
RectangleROITool,
|
||||||
} from '@cornerstonejs/tools';
|
} from '@cornerstonejs/tools';
|
||||||
import { Types } from '@ohif/core';
|
import { Types } from '@ohif/core';
|
||||||
import { Enums as CSExtensionEnums } from '@ohif/extension-cornerstone';
|
|
||||||
|
|
||||||
import DICOMSRDisplayTool from './tools/DICOMSRDisplayTool';
|
import DICOMSRDisplayTool from './tools/DICOMSRDisplayTool';
|
||||||
import SCOORD3DPointTool from './tools/SCOORD3DPointTool';
|
|
||||||
import SRSCOOR3DProbeMapper from './utils/SRSCOOR3DProbeMapper';
|
|
||||||
import addToolInstance from './utils/addToolInstance';
|
import addToolInstance from './utils/addToolInstance';
|
||||||
import toolNames from './tools/toolNames';
|
import toolNames from './tools/toolNames';
|
||||||
|
|
||||||
const { CORNERSTONE_3D_TOOLS_SOURCE_NAME, CORNERSTONE_3D_TOOLS_SOURCE_VERSION } = CSExtensionEnums;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {object} configuration
|
* @param {object} configuration
|
||||||
*/
|
*/
|
||||||
@ -28,8 +23,6 @@ export default function init({
|
|||||||
configuration = {},
|
configuration = {},
|
||||||
servicesManager,
|
servicesManager,
|
||||||
}: Types.Extensions.ExtensionParams): void {
|
}: Types.Extensions.ExtensionParams): void {
|
||||||
const { measurementService } = servicesManager.services;
|
|
||||||
|
|
||||||
addToolInstance(toolNames.DICOMSRDisplay, DICOMSRDisplayTool);
|
addToolInstance(toolNames.DICOMSRDisplay, DICOMSRDisplayTool);
|
||||||
addToolInstance(toolNames.SRLength, LengthTool);
|
addToolInstance(toolNames.SRLength, LengthTool);
|
||||||
addToolInstance(toolNames.SRBidirectional, BidirectionalTool);
|
addToolInstance(toolNames.SRBidirectional, BidirectionalTool);
|
||||||
@ -39,26 +32,10 @@ export default function init({
|
|||||||
addToolInstance(toolNames.SRAngle, AngleTool);
|
addToolInstance(toolNames.SRAngle, AngleTool);
|
||||||
addToolInstance(toolNames.SRPlanarFreehandROI, PlanarFreehandROITool);
|
addToolInstance(toolNames.SRPlanarFreehandROI, PlanarFreehandROITool);
|
||||||
addToolInstance(toolNames.SRRectangleROI, RectangleROITool);
|
addToolInstance(toolNames.SRRectangleROI, RectangleROITool);
|
||||||
addToolInstance(toolNames.SRSCOORD3DPoint, SCOORD3DPointTool);
|
|
||||||
|
|
||||||
// TODO - fix the SR display of Cobb Angle, as it joins the two lines
|
// TODO - fix the SR display of Cobb Angle, as it joins the two lines
|
||||||
addToolInstance(toolNames.SRCobbAngle, CobbAngleTool);
|
addToolInstance(toolNames.SRCobbAngle, CobbAngleTool);
|
||||||
|
|
||||||
const csTools3DVer1MeasurementSource = measurementService.getSource(
|
|
||||||
CORNERSTONE_3D_TOOLS_SOURCE_NAME,
|
|
||||||
CORNERSTONE_3D_TOOLS_SOURCE_VERSION
|
|
||||||
);
|
|
||||||
|
|
||||||
const { POINT } = measurementService.VALUE_TYPES;
|
|
||||||
|
|
||||||
measurementService.addMapping(
|
|
||||||
csTools3DVer1MeasurementSource,
|
|
||||||
'SRSCOORD3DPoint',
|
|
||||||
POINT,
|
|
||||||
SRSCOOR3DProbeMapper.toAnnotation,
|
|
||||||
SRSCOOR3DProbeMapper.toMeasurement.bind(null, { servicesManager })
|
|
||||||
);
|
|
||||||
|
|
||||||
// Modify annotation tools to use dashed lines on SR
|
// Modify annotation tools to use dashed lines on SR
|
||||||
const dashedLine = {
|
const dashedLine = {
|
||||||
lineDash: '4,4',
|
lineDash: '4,4',
|
||||||
|
|||||||
@ -1,203 +0,0 @@
|
|||||||
import { type Types } from '@cornerstonejs/core';
|
|
||||||
import {
|
|
||||||
annotation,
|
|
||||||
drawing,
|
|
||||||
utilities,
|
|
||||||
Types as cs3DToolsTypes,
|
|
||||||
AnnotationDisplayTool,
|
|
||||||
} from '@cornerstonejs/tools';
|
|
||||||
import toolNames from './toolNames';
|
|
||||||
import { Annotation } from '@cornerstonejs/tools/dist/types/types';
|
|
||||||
|
|
||||||
export default class SCOORD3DPointTool extends AnnotationDisplayTool {
|
|
||||||
static toolName = toolNames.SRSCOORD3DPoint;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
toolProps = {},
|
|
||||||
defaultToolProps = {
|
|
||||||
configuration: {},
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
super(toolProps, defaultToolProps);
|
|
||||||
}
|
|
||||||
|
|
||||||
_getTextBoxLinesFromLabels(labels) {
|
|
||||||
// TODO -> max 5 for now (label + shortAxis + longAxis), need a generic solution for this!
|
|
||||||
|
|
||||||
const labelLength = Math.min(labels.length, 5);
|
|
||||||
const lines = [];
|
|
||||||
|
|
||||||
return lines;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This tool should not inherit from AnnotationTool and we should not need
|
|
||||||
// to add the following lines.
|
|
||||||
isPointNearTool = () => null;
|
|
||||||
getHandleNearImagePoint = () => null;
|
|
||||||
|
|
||||||
renderAnnotation = (enabledElement: Types.IEnabledElement, svgDrawingHelper: any): void => {
|
|
||||||
const { viewport } = enabledElement;
|
|
||||||
const { element } = viewport;
|
|
||||||
|
|
||||||
const annotations = annotation.state.getAnnotations(this.getToolName(), element);
|
|
||||||
|
|
||||||
// Todo: We don't need this anymore, filtering happens in triggerAnnotationRender
|
|
||||||
if (!annotations?.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter toolData to only render the data for the active SR.
|
|
||||||
const filteredAnnotations = annotations;
|
|
||||||
if (!viewport._actors?.size) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const styleSpecifier: cs3DToolsTypes.AnnotationStyle.StyleSpecifier = {
|
|
||||||
toolGroupId: this.toolGroupId,
|
|
||||||
toolName: this.getToolName(),
|
|
||||||
viewportId: enabledElement.viewport.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
for (let i = 0; i < filteredAnnotations.length; i++) {
|
|
||||||
const annotation = filteredAnnotations[i];
|
|
||||||
|
|
||||||
const annotationUID = annotation.annotationUID;
|
|
||||||
const { renderableData } = annotation.data;
|
|
||||||
const { POINT: points } = renderableData;
|
|
||||||
|
|
||||||
styleSpecifier.annotationUID = annotationUID;
|
|
||||||
|
|
||||||
const lineWidth = this.getStyle('lineWidth', styleSpecifier, annotation);
|
|
||||||
const lineDash = this.getStyle('lineDash', styleSpecifier, annotation);
|
|
||||||
const color = this.getStyle('color', styleSpecifier, annotation);
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
color,
|
|
||||||
lineDash,
|
|
||||||
lineWidth,
|
|
||||||
};
|
|
||||||
|
|
||||||
const point = points[0][0];
|
|
||||||
|
|
||||||
// check if viewport can render it
|
|
||||||
const viewable = viewport.isReferenceViewable(
|
|
||||||
{ FrameOfReferenceUID: annotation.metadata.FrameOfReferenceUID, cameraFocalPoint: point },
|
|
||||||
{ asNearbyProjection: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!viewable) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// render the point
|
|
||||||
const arrowPointCanvas = viewport.worldToCanvas(point);
|
|
||||||
// Todo: configure this
|
|
||||||
const arrowEndCanvas = [arrowPointCanvas[0] + 20, arrowPointCanvas[1] + 20];
|
|
||||||
const canvasCoordinates = [arrowPointCanvas, arrowEndCanvas];
|
|
||||||
|
|
||||||
drawing.drawArrow(
|
|
||||||
svgDrawingHelper,
|
|
||||||
annotationUID,
|
|
||||||
'1',
|
|
||||||
canvasCoordinates[1],
|
|
||||||
canvasCoordinates[0],
|
|
||||||
{
|
|
||||||
color: options.color,
|
|
||||||
width: options.lineWidth,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
this.renderTextBox(
|
|
||||||
svgDrawingHelper,
|
|
||||||
viewport,
|
|
||||||
canvasCoordinates,
|
|
||||||
annotation,
|
|
||||||
styleSpecifier,
|
|
||||||
options
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
renderTextBox(
|
|
||||||
svgDrawingHelper,
|
|
||||||
viewport,
|
|
||||||
canvasCoordinates,
|
|
||||||
annotation,
|
|
||||||
styleSpecifier,
|
|
||||||
options = {}
|
|
||||||
) {
|
|
||||||
if (!canvasCoordinates || !annotation) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { annotationUID, data = {} } = annotation;
|
|
||||||
const { labels } = data;
|
|
||||||
|
|
||||||
const textLines = [];
|
|
||||||
|
|
||||||
for (const label of labels) {
|
|
||||||
// make this generic
|
|
||||||
// fix this
|
|
||||||
if (label.label === '363698007') {
|
|
||||||
textLines.push(`Finding Site: ${label.value}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const { color } = options;
|
|
||||||
|
|
||||||
const adaptedCanvasCoordinates = canvasCoordinates;
|
|
||||||
// adapt coordinates if there is an adapter
|
|
||||||
const canvasTextBoxCoords = utilities.drawing.getTextBoxCoordsCanvas(adaptedCanvasCoordinates);
|
|
||||||
|
|
||||||
if (!annotation.data?.handles?.textBox?.worldPosition) {
|
|
||||||
annotation.data.handles.textBox.worldPosition = viewport.canvasToWorld(canvasTextBoxCoords);
|
|
||||||
}
|
|
||||||
|
|
||||||
const textBoxPosition = viewport.worldToCanvas(annotation.data.handles.textBox.worldPosition);
|
|
||||||
|
|
||||||
const textBoxUID = '1';
|
|
||||||
const textBoxOptions = this.getLinkedTextBoxStyle(styleSpecifier, annotation);
|
|
||||||
|
|
||||||
const boundingBox = drawing.drawLinkedTextBox(
|
|
||||||
svgDrawingHelper,
|
|
||||||
annotationUID,
|
|
||||||
textBoxUID,
|
|
||||||
textLines,
|
|
||||||
textBoxPosition,
|
|
||||||
canvasCoordinates,
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
...textBoxOptions,
|
|
||||||
color,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const { x: left, y: top, width, height } = boundingBox;
|
|
||||||
|
|
||||||
annotation.data.handles.textBox.worldBoundingBox = {
|
|
||||||
topLeft: viewport.canvasToWorld([left, top]),
|
|
||||||
topRight: viewport.canvasToWorld([left + width, top]),
|
|
||||||
bottomLeft: viewport.canvasToWorld([left, top + height]),
|
|
||||||
bottomRight: viewport.canvasToWorld([left + width, top + height]),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public getLinkedTextBoxStyle(
|
|
||||||
specifications: cs3DToolsTypes.AnnotationStyle.StyleSpecifier,
|
|
||||||
annotation?: Annotation
|
|
||||||
): Record<string, unknown> {
|
|
||||||
// Todo: this function can be used to set different styles for different toolMode
|
|
||||||
// for the textBox.
|
|
||||||
|
|
||||||
return {
|
|
||||||
visibility: this.getStyle('textBoxVisibility', specifications, annotation),
|
|
||||||
fontFamily: this.getStyle('textBoxFontFamily', specifications, annotation),
|
|
||||||
fontSize: this.getStyle('textBoxFontSize', specifications, annotation),
|
|
||||||
color: this.getStyle('textBoxColor', specifications, annotation),
|
|
||||||
shadow: this.getStyle('textBoxShadow', specifications, annotation),
|
|
||||||
background: this.getStyle('textBoxBackground', specifications, annotation),
|
|
||||||
lineWidth: this.getStyle('textBoxLinkLineWidth', specifications, annotation),
|
|
||||||
lineDash: this.getStyle('textBoxLinkLineDash', specifications, annotation),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -9,7 +9,6 @@ const toolNames = {
|
|||||||
SRCobbAngle: 'SRCobbAngle',
|
SRCobbAngle: 'SRCobbAngle',
|
||||||
SRRectangleROI: 'SRRectangleROI',
|
SRRectangleROI: 'SRRectangleROI',
|
||||||
SRPlanarFreehandROI: 'SRPlanarFreehandROI',
|
SRPlanarFreehandROI: 'SRPlanarFreehandROI',
|
||||||
SRSCOORD3DPoint: 'SRSCOORD3DPoint',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default toolNames;
|
export default toolNames;
|
||||||
|
|||||||
@ -1,71 +0,0 @@
|
|||||||
const SRSCOOR3DProbe = {
|
|
||||||
toAnnotation: measurement => {},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Maps cornerstone annotation event data to measurement service format.
|
|
||||||
*
|
|
||||||
* @param {Object} cornerstone Cornerstone event data
|
|
||||||
* @return {Measurement} Measurement instance
|
|
||||||
*/
|
|
||||||
toMeasurement: ({ servicesManager, getValueTypeFromToolType }, csToolsEventDetail) => {
|
|
||||||
const { displaySetService } = servicesManager.services;
|
|
||||||
const { annotation } = csToolsEventDetail;
|
|
||||||
const { metadata, data, annotationUID } = annotation;
|
|
||||||
|
|
||||||
if (!metadata || !data) {
|
|
||||||
console.warn('Probe tool: Missing metadata or data');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { toolName, FrameOfReferenceUID } = metadata;
|
|
||||||
const { points } = data.handles;
|
|
||||||
|
|
||||||
const displaySets = displaySetService
|
|
||||||
.getActiveDisplaySets()
|
|
||||||
.filter(ds => ds.FrameOfReferenceUID === FrameOfReferenceUID);
|
|
||||||
const displaySet = displaySets.filter(ds => ds.isReconstructable)[0] || displaySets[0];
|
|
||||||
|
|
||||||
const { StudyInstanceUID: referenceStudyUID, SeriesInstanceUID: referenceSeriesUID } =
|
|
||||||
displaySets[0] || {};
|
|
||||||
|
|
||||||
const displayText = getDisplayText(annotation);
|
|
||||||
return {
|
|
||||||
uid: annotationUID,
|
|
||||||
points,
|
|
||||||
metadata,
|
|
||||||
referenceStudyUID,
|
|
||||||
referenceSeriesUID,
|
|
||||||
displaySetInstanceUID: displaySet?.displaySetInstanceUID,
|
|
||||||
toolName: metadata.toolName,
|
|
||||||
label: data.label,
|
|
||||||
displayText: displayText,
|
|
||||||
data: data.cachedStats,
|
|
||||||
type: getValueTypeFromToolType?.(toolName) ?? null,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
function getDisplayText(annotation) {
|
|
||||||
const { data } = annotation;
|
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
return [''];
|
|
||||||
}
|
|
||||||
const { labels } = data;
|
|
||||||
|
|
||||||
const displayText = [];
|
|
||||||
|
|
||||||
for (const label of labels) {
|
|
||||||
// make this generic
|
|
||||||
if (label.label === '33636980076') {
|
|
||||||
displayText.push(`Finding Site: ${label.value}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
primary: displayText,
|
|
||||||
secondary: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SRSCOOR3DProbe;
|
|
||||||
@ -64,33 +64,34 @@ export default function addSRAnnotation({ measurement, imageId = null, frameNumb
|
|||||||
const { ValueType: valueType, GraphicType: graphicType } = measurement.coords[0];
|
const { ValueType: valueType, GraphicType: graphicType } = measurement.coords[0];
|
||||||
const graphicTypePoints = renderableData[graphicType];
|
const graphicTypePoints = renderableData[graphicType];
|
||||||
|
|
||||||
/** TODO: Read the tool name from the DICOM SR identification type in the future. */
|
/**
|
||||||
|
* TODO: Read the tool name from the DICOM SR identification type in the future.
|
||||||
|
*/
|
||||||
let frameOfReferenceUID = null;
|
let frameOfReferenceUID = null;
|
||||||
let planeRestriction = null;
|
let planeRestriction = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store the view reference for use in initial navigation
|
||||||
|
*/
|
||||||
if (imageId) {
|
if (imageId) {
|
||||||
const imagePlaneModule = metaData.get('imagePlaneModule', imageId);
|
const imagePlaneModule = metaData.get('imagePlaneModule', imageId);
|
||||||
frameOfReferenceUID = imagePlaneModule?.frameOfReferenceUID;
|
frameOfReferenceUID = imagePlaneModule?.frameOfReferenceUID;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store the view reference for use in initial navigation
|
||||||
|
*/
|
||||||
if (valueType === 'SCOORD3D') {
|
if (valueType === 'SCOORD3D') {
|
||||||
const adapter = MeasurementReport.getAdapterForTrackingIdentifier(
|
|
||||||
measurement.TrackingIdentifier
|
|
||||||
);
|
|
||||||
if (!adapter) {
|
|
||||||
toolName = toolNames.SRSCOORD3DPoint;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get the ReferencedFrameOfReferenceUID from the measurement
|
|
||||||
frameOfReferenceUID = measurement.coords[0].ReferencedFrameOfReferenceSequence;
|
frameOfReferenceUID = measurement.coords[0].ReferencedFrameOfReferenceSequence;
|
||||||
|
|
||||||
planeRestriction = {
|
planeRestriction = {
|
||||||
FrameOfReferenceUID: frameOfReferenceUID,
|
FrameOfReferenceUID: frameOfReferenceUID,
|
||||||
point: graphicTypePoints[0][0],
|
point: graphicTypePoints[0][0],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store the view reference for use in initial navigation
|
/**
|
||||||
|
* Store the view reference for use in initial navigation
|
||||||
|
*/
|
||||||
measurement.viewReference = {
|
measurement.viewReference = {
|
||||||
planeRestriction,
|
planeRestriction,
|
||||||
FrameOfReferenceUID: frameOfReferenceUID,
|
FrameOfReferenceUID: frameOfReferenceUID,
|
||||||
|
|||||||
@ -107,8 +107,8 @@ function getMappedAnnotations(annotation, displaySetService) {
|
|||||||
const displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID)[0];
|
const displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID)[0];
|
||||||
|
|
||||||
const { SeriesNumber } = displaySet;
|
const { SeriesNumber } = displaySet;
|
||||||
const { value } = targetStats;
|
const { value, modalityUnit } = targetStats;
|
||||||
const unit = 'HU';
|
const unit = modalityUnit || '';
|
||||||
|
|
||||||
annotations.push({
|
annotations.push({
|
||||||
SeriesInstanceUID,
|
SeriesInstanceUID,
|
||||||
|
|||||||
@ -13,7 +13,6 @@ const supportedTools = [
|
|||||||
'LivewireContour',
|
'LivewireContour',
|
||||||
'UltrasoundDirectionalTool',
|
'UltrasoundDirectionalTool',
|
||||||
'UltrasoundPleuraBLineTool',
|
'UltrasoundPleuraBLineTool',
|
||||||
'SCOORD3DPoint',
|
|
||||||
'SegmentBidirectional',
|
'SegmentBidirectional',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
169
tests/Scoord3dProbe.spec.ts
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
import { test, expect } from 'playwright-test-coverage';
|
||||||
|
import { visitStudy, checkForScreenshot, screenShotPaths } from './utils';
|
||||||
|
import { viewportLocator } from './utils/locators';
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
const studyInstanceUID = '1.3.6.1.4.1.14519.5.2.1.7310.5101.860473186348887719777907797922';
|
||||||
|
const mode = 'viewer';
|
||||||
|
|
||||||
|
await visitStudy(page, studyInstanceUID, mode, 5000);
|
||||||
|
|
||||||
|
// Log the actual URL that was loaded
|
||||||
|
const currentUrl = page.url();
|
||||||
|
console.log(`✅ Actual page URL: ${currentUrl}\n`);
|
||||||
|
|
||||||
|
// Remove any webpack dev server overlays that might be blocking interactions
|
||||||
|
await page.evaluate(() => {
|
||||||
|
const overlay = document.getElementById('webpack-dev-server-client-overlay');
|
||||||
|
if (overlay) {
|
||||||
|
overlay.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should hydrate SCOORD3D probe measurements correctly', async ({ page }) => {
|
||||||
|
// Wait for the side panel to be visible and clickable
|
||||||
|
await page.waitForTimeout(3000);
|
||||||
|
|
||||||
|
// Navigate to the tracked measurements panel
|
||||||
|
await page.getByTestId('side-panel-header-right').click({ timeout: 15000 });
|
||||||
|
await page.getByTestId('trackedMeasurements-btn').click();
|
||||||
|
|
||||||
|
// Double-click on the study browser thumbnail to load the SR
|
||||||
|
await page.getByTestId('study-browser-thumbnail-no-image').dblclick();
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
// Wait for the SR to load and stabilize before taking screenshot
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
// Take screenshot before hydration - use viewport locator instead of full page
|
||||||
|
await checkForScreenshot(page, viewportLocator({ page, viewportId: 'default' }), screenShotPaths.scoord3dProbe.scoord3dProbePreHydration);
|
||||||
|
|
||||||
|
// Zoom in to better see the measurements
|
||||||
|
await page.evaluate(() => {
|
||||||
|
// Access cornerstone directly from the window object
|
||||||
|
const cornerstone = (window as any).cornerstone;
|
||||||
|
if (!cornerstone) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const enabledElements = cornerstone.getEnabledElements();
|
||||||
|
if (enabledElements.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewport = enabledElements[0].viewport;
|
||||||
|
if (viewport) {
|
||||||
|
viewport.setZoom(4);
|
||||||
|
viewport.render();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for rendering to complete
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
// Wait for the hydrate button to be visible and clickable
|
||||||
|
await page.getByTestId('yes-hydrate-btn').waitFor({ state: 'visible', timeout: 15000 });
|
||||||
|
|
||||||
|
// Click the hydrate button to load the SCOORD3D probe measurements
|
||||||
|
await page.getByTestId('yes-hydrate-btn').click();
|
||||||
|
|
||||||
|
// Wait for hydration to complete and rendering to stabilize
|
||||||
|
await page.waitForTimeout(3000);
|
||||||
|
|
||||||
|
// Take screenshot after hydration showing the probe measurements - use viewport locator
|
||||||
|
await checkForScreenshot(page, viewportLocator({ page, viewportId: 'default' }), screenShotPaths.scoord3dProbe.scoord3dProbePostHydration);
|
||||||
|
|
||||||
|
// Verify the measurements list has the correct probe measurements
|
||||||
|
const measurementRows = page.getByTestId('data-row');
|
||||||
|
const rowCount = await measurementRows.count();
|
||||||
|
expect(rowCount).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// Verify that the measurements are probe measurements (not other types)
|
||||||
|
for (let i = 0; i < rowCount; i++) {
|
||||||
|
const row = measurementRows.nth(i);
|
||||||
|
const rowText = await row.textContent();
|
||||||
|
// Probe measurements should be present, verify they're not other measurement types
|
||||||
|
expect(rowText).toBeTruthy();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test jumping to a specific measurement by scrolling and clicking
|
||||||
|
await page.evaluate(() => {
|
||||||
|
// Access cornerstone directly from the window object
|
||||||
|
const cornerstone = (window as any).cornerstone;
|
||||||
|
if (!cornerstone) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const enabledElements = cornerstone.getEnabledElements();
|
||||||
|
if (enabledElements.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewport = enabledElements[0].viewport;
|
||||||
|
if (viewport) {
|
||||||
|
viewport.scroll(20);
|
||||||
|
viewport.render();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Click on a data row to jump to the measurement
|
||||||
|
await page.getByTestId('data-row').first().click();
|
||||||
|
|
||||||
|
// Take screenshot showing the jump to measurement functionality - use viewport locator
|
||||||
|
await checkForScreenshot(page, viewportLocator({ page, viewportId: 'default' }), screenShotPaths.scoord3dProbe.scoord3dProbeJumpToMeasurement);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should display SCOORD3D probe measurements correctly', async ({ page }) => {
|
||||||
|
// Wait for the side panel to be visible and clickable
|
||||||
|
await page.waitForTimeout(3000);
|
||||||
|
|
||||||
|
// First hydrate the SR to load the measurements
|
||||||
|
await page.getByTestId('side-panel-header-right').click({ timeout: 15000 });
|
||||||
|
await page.getByTestId('trackedMeasurements-btn').click();
|
||||||
|
await page.getByTestId('study-browser-thumbnail-no-image').dblclick();
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
// Wait for the hydrate button to be visible and clickable
|
||||||
|
await page.getByTestId('yes-hydrate-btn').waitFor({ state: 'visible', timeout: 15000 });
|
||||||
|
await page.getByTestId('yes-hydrate-btn').click();
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
// Zoom to show the probe measurements clearly
|
||||||
|
await page.evaluate(() => {
|
||||||
|
const cornerstone = (window as any).cornerstone;
|
||||||
|
if (!cornerstone) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const enabledElements = cornerstone.getEnabledElements();
|
||||||
|
if (enabledElements.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewport = enabledElements[0].viewport;
|
||||||
|
if (viewport) {
|
||||||
|
viewport.setZoom(3);
|
||||||
|
viewport.render();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for rendering to complete before taking screenshot
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
// Take screenshot showing the SCOORD3D probe measurements rendered correctly - use viewport locator
|
||||||
|
await checkForScreenshot(page, viewportLocator({ page, viewportId: 'default' }), screenShotPaths.scoord3dProbe.scoord3dProbeDisplayedCorrectly);
|
||||||
|
|
||||||
|
// Verify the measurements list has the correct probe measurements and not others
|
||||||
|
const measurementRows = page.getByTestId('data-row');
|
||||||
|
const rowCount = await measurementRows.count();
|
||||||
|
expect(rowCount).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// Verify that the measurements are probe measurements (not other types like rectangle)
|
||||||
|
for (let i = 0; i < rowCount; i++) {
|
||||||
|
const row = measurementRows.nth(i);
|
||||||
|
const rowText = await row.textContent();
|
||||||
|
// Probe measurements should be present, verify they're not other measurement types
|
||||||
|
expect(rowText).toBeTruthy();
|
||||||
|
}
|
||||||
|
});
|
||||||
170
tests/ScoordRectangle.spec.ts
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
import { test, expect } from 'playwright-test-coverage';
|
||||||
|
import { visitStudy, checkForScreenshot, screenShotPaths } from './utils';
|
||||||
|
import { viewportLocator } from './utils/locators';
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
const studyInstanceUID = '1.2.840.113654.2.55.242841386983064378162007136685545369722';
|
||||||
|
const mode = 'viewer';
|
||||||
|
|
||||||
|
await visitStudy(page, studyInstanceUID, mode, 5000);
|
||||||
|
|
||||||
|
// Log the actual URL that was loaded
|
||||||
|
const currentUrl = page.url();
|
||||||
|
console.log(`✅ Actual page URL: ${currentUrl}\n`);
|
||||||
|
|
||||||
|
// Remove any webpack dev server overlays that might be blocking interactions
|
||||||
|
await page.evaluate(() => {
|
||||||
|
const overlay = document.getElementById('webpack-dev-server-client-overlay');
|
||||||
|
if (overlay) {
|
||||||
|
overlay.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
//
|
||||||
|
test('should hydrate SCOORD rectangle measurements correctly', async ({ page }) => {
|
||||||
|
// Wait for the side panel to be visible and clickable
|
||||||
|
await page.waitForTimeout(3000);
|
||||||
|
|
||||||
|
// Navigate to the tracked measurements panel
|
||||||
|
await page.getByTestId('side-panel-header-right').click({ timeout: 15000 });
|
||||||
|
await page.getByTestId('trackedMeasurements-btn').click();
|
||||||
|
|
||||||
|
// Double-click on the study browser thumbnail to load the SR
|
||||||
|
await page.getByTestId('study-browser-thumbnail-no-image').dblclick();
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
// Wait for the SR to load and stabilize before taking screenshot
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
// Take screenshot before hydration - use viewport locator instead of full page
|
||||||
|
await checkForScreenshot(page, viewportLocator({ page, viewportId: 'default' }), screenShotPaths.scoordRectangle.scoordRectanglePreHydration);
|
||||||
|
|
||||||
|
// Zoom in to better see the measurements
|
||||||
|
await page.evaluate(() => {
|
||||||
|
// Access cornerstone directly from the window object
|
||||||
|
const cornerstone = (window as any).cornerstone;
|
||||||
|
if (!cornerstone) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const enabledElements = cornerstone.getEnabledElements();
|
||||||
|
if (enabledElements.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewport = enabledElements[0].viewport;
|
||||||
|
if (viewport) {
|
||||||
|
viewport.setZoom(4);
|
||||||
|
viewport.render();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for rendering to complete
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
// Wait for the hydrate button to be visible and clickable
|
||||||
|
await page.getByTestId('yes-hydrate-btn').waitFor({ state: 'visible', timeout: 15000 });
|
||||||
|
|
||||||
|
// Click the hydrate button to load the SCOORD rectangle measurements
|
||||||
|
await page.getByTestId('yes-hydrate-btn').click();
|
||||||
|
|
||||||
|
// Wait for hydration to complete and rendering to stabilize
|
||||||
|
await page.waitForTimeout(3000);
|
||||||
|
|
||||||
|
// Take screenshot after hydration showing the rectangle measurements - use viewport locator
|
||||||
|
await checkForScreenshot(page, viewportLocator({ page, viewportId: 'default' }), screenShotPaths.scoordRectangle.scoordRectanglePostHydration);
|
||||||
|
|
||||||
|
// Verify the measurements list has the correct rectangle measurements
|
||||||
|
const measurementRows = page.getByTestId('data-row');
|
||||||
|
const rowCount = await measurementRows.count();
|
||||||
|
expect(rowCount).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// Verify that the measurements are rectangle measurements (not other types)
|
||||||
|
for (let i = 0; i < rowCount; i++) {
|
||||||
|
const row = measurementRows.nth(i);
|
||||||
|
const rowText = await row.textContent();
|
||||||
|
// Rectangle measurements should be present, verify they're not other measurement types
|
||||||
|
expect(rowText).toBeTruthy();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test jumping to a specific measurement by scrolling and clicking
|
||||||
|
await page.evaluate(() => {
|
||||||
|
// Access cornerstone directly from the window object
|
||||||
|
const cornerstone = (window as any).cornerstone;
|
||||||
|
if (!cornerstone) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const enabledElements = cornerstone.getEnabledElements();
|
||||||
|
if (enabledElements.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewport = enabledElements[0].viewport;
|
||||||
|
if (viewport) {
|
||||||
|
viewport.scroll(20);
|
||||||
|
viewport.render();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Click on a data row to jump to the measurement
|
||||||
|
await page.getByTestId('data-row').first().click();
|
||||||
|
|
||||||
|
// Take screenshot showing the jump to measurement functionality - use viewport locator
|
||||||
|
await checkForScreenshot(page, viewportLocator({ page, viewportId: 'default' }), screenShotPaths.scoordRectangle.scoordRectangleJumpToMeasurement);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should display SCOORD rectangle measurements correctly', async ({ page }) => {
|
||||||
|
// Wait for the side panel to be visible and clickable
|
||||||
|
await page.waitForTimeout(3000);
|
||||||
|
|
||||||
|
// First hydrate the SR to load the measurements
|
||||||
|
await page.getByTestId('side-panel-header-right').click({ timeout: 15000 });
|
||||||
|
await page.getByTestId('trackedMeasurements-btn').click();
|
||||||
|
await page.getByTestId('study-browser-thumbnail-no-image').dblclick();
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
// Wait for the hydrate button to be visible and clickable
|
||||||
|
await page.getByTestId('yes-hydrate-btn').waitFor({ state: 'visible', timeout: 15000 });
|
||||||
|
await page.getByTestId('yes-hydrate-btn').click();
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
// Zoom to show the rectangle measurements clearly
|
||||||
|
await page.evaluate(() => {
|
||||||
|
const cornerstone = (window as any).cornerstone;
|
||||||
|
if (!cornerstone) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const enabledElements = cornerstone.getEnabledElements();
|
||||||
|
if (enabledElements.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewport = enabledElements[0].viewport;
|
||||||
|
if (viewport) {
|
||||||
|
viewport.setZoom(3);
|
||||||
|
viewport.render();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for rendering to complete before taking screenshot
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
// Take screenshot showing the SCOORD rectangle measurements rendered correctly - use viewport locator
|
||||||
|
await checkForScreenshot(page, viewportLocator({ page, viewportId: 'default' }), screenShotPaths.scoordRectangle.scoordRectangleDisplayedCorrectly);
|
||||||
|
|
||||||
|
// Verify the measurements list has the correct rectangle measurements and not others
|
||||||
|
const measurementRows = page.getByTestId('data-row');
|
||||||
|
const rowCount = await measurementRows.count();
|
||||||
|
expect(rowCount).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// Verify that the measurements are rectangle measurements (not other types like probe)
|
||||||
|
for (let i = 0; i < rowCount; i++) {
|
||||||
|
const row = measurementRows.nth(i);
|
||||||
|
const rowText = await row.textContent();
|
||||||
|
// Rectangle measurements should be present, verify they're not other measurement types
|
||||||
|
expect(rowText).toBeTruthy();
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -22,8 +22,18 @@ test('should render scroll bars with the correct look-and-feel', async ({ page }
|
|||||||
'studyRow-1.3.6.1.4.1.14519.5.2.1.5099.8010.217836670708542506360829799868'
|
'studyRow-1.3.6.1.4.1.14519.5.2.1.5099.8010.217836670708542506360829799868'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Wait for the expanded row to be visible and rendered
|
||||||
|
await expandedStudyRow.waitFor({ state: 'visible', timeout: 10000 });
|
||||||
await expandedStudyRow.scrollIntoViewIfNeeded();
|
await expandedStudyRow.scrollIntoViewIfNeeded();
|
||||||
await page.waitForTimeout(3000);
|
|
||||||
|
// Wait for content to load and stabilize, including any lazy-loaded items
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
// Wait for network to be idle to ensure all content is loaded
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
|
||||||
|
// Additional wait to ensure scrollbar rendering is stable
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
await checkForScreenshot({
|
await checkForScreenshot({
|
||||||
page,
|
page,
|
||||||
|
|||||||
|
After Width: | Height: | Size: 191 KiB |
|
After Width: | Height: | Size: 163 KiB |
|
After Width: | Height: | Size: 170 KiB |
|
After Width: | Height: | Size: 255 KiB |
|
After Width: | Height: | Size: 223 KiB |
|
After Width: | Height: | Size: 197 KiB |
|
After Width: | Height: | Size: 202 KiB |
|
After Width: | Height: | Size: 270 KiB |
@ -66,6 +66,18 @@ const screenShotPaths = {
|
|||||||
rectangle: {
|
rectangle: {
|
||||||
rectangleDisplayedCorrectly: 'rectangleDisplayedCorrectly.png',
|
rectangleDisplayedCorrectly: 'rectangleDisplayedCorrectly.png',
|
||||||
},
|
},
|
||||||
|
scoord3dProbe: {
|
||||||
|
scoord3dProbeDisplayedCorrectly: 'scoord3dProbeDisplayedCorrectly.png',
|
||||||
|
scoord3dProbePreHydration: 'scoord3dProbePreHydration.png',
|
||||||
|
scoord3dProbePostHydration: 'scoord3dProbePostHydration.png',
|
||||||
|
scoord3dProbeJumpToMeasurement: 'scoord3dProbeJumpToMeasurement.png',
|
||||||
|
},
|
||||||
|
scoordRectangle: {
|
||||||
|
scoordRectangleDisplayedCorrectly: 'scoordRectangleDisplayedCorrectly.png',
|
||||||
|
scoordRectanglePreHydration: 'scoordRectanglePreHydration.png',
|
||||||
|
scoordRectanglePostHydration: 'scoordRectanglePostHydration.png',
|
||||||
|
scoordRectangleJumpToMeasurement: 'scoordRectangleJumpToMeasurement.png',
|
||||||
|
},
|
||||||
spline: {
|
spline: {
|
||||||
splineDisplayedCorrectly: 'splineDisplayedCorrectly.png',
|
splineDisplayedCorrectly: 'splineDisplayedCorrectly.png',
|
||||||
},
|
},
|
||||||
|
|||||||