feat: add more e2e tests for MPR and add test mode and extension (#3180)

* tests: add various e2e tests for MPR and measurements

wip

add cypress config

feat: add mode and extension for testing

add hp applied through search params

add MPR tests

apply review comments

add more e2e tests

update yarn lock

* fix unit tests failing
This commit is contained in:
Alireza 2023-02-17 15:04:07 -05:00 committed by GitHub
parent ae4183852f
commit 926290f69e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
83 changed files with 6286 additions and 6860 deletions

View File

@ -39,7 +39,7 @@ module.exports = (env, argv, { SRC_DIR, DIST_DIR }) => {
const config = {
mode: isProdBuild ? 'production' : 'development',
devtool: isProdBuild ? 'source-map' : 'eval-source-map',
devtool: isProdBuild ? 'source-map' : 'cheap-module-source-map',
entry: {
app: `${SRC_DIR}/index.js`,
},

View File

@ -36,13 +36,13 @@
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-i18next": "^10.11.0",
"react-router": "^6.3.0",
"react-router-dom": "^6.3.0",
"react-router": "^6.8.1",
"react-router-dom": "^6.8.1",
"webpack": "^5.50.0",
"webpack-merge": "^5.7.3"
},
"dependencies": {
"@babel/runtime": "7.7.6",
"@babel/runtime": "^7.20.13",
"react-color": "^2.19.3"
},
"devDependencies": {

View File

@ -43,7 +43,7 @@
"react": "^17.0.2"
},
"dependencies": {
"@babel/runtime": "7.16.3",
"@babel/runtime": "^7.20.13",
"classnames": "^2.3.2",
"@cornerstonejs/core": "0.30.1",
"@cornerstonejs/tools": "0.39.0"

View File

@ -42,7 +42,7 @@
"lodash": "^4.17.21"
},
"dependencies": {
"@babel/runtime": "7.17.9",
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "0.3.0",
"@cornerstonejs/core": "0.30.1",
"@cornerstonejs/streaming-image-volume-loader": "^0.11.2",

View File

@ -77,7 +77,10 @@ export default async function init({
window.services = servicesManager.services;
if (!window.crossOriginIsolated) {
if (
appConfig.showWarningMessageForCrossOrigin &&
!window.crossOriginIsolated
) {
uiNotificationService.show({
title: 'Cross Origin Isolation',
message:
@ -86,7 +89,10 @@ export default async function init({
});
}
if (cornerstone.getShouldUseCPURendering()) {
if (
appConfig.showCPUFallbackMessage &&
cornerstone.getShouldUseCPURendering()
) {
_showCPURenderingModal(uiModalService, hangingProtocolService);
}

View File

@ -59,6 +59,7 @@ function callInputDialog(
autoFocus
className="mt-2 bg-black border-primary-main"
type="text"
id="annotation"
containerClassName="mr-2"
value={value.label}
onChange={event => {

View File

@ -58,26 +58,20 @@ export default function toggleMPRHangingProtocol({
const viewportDisplaySetInstanceUIDs =
viewports[activeViewportIndex].displaySetInstanceUIDs;
const errorCallback = error => {
// Unable to create MPR, so be sure to return to the cached/original protocol.
hangingProtocolService.setProtocol(
cachedState.protocol.id,
viewportMatchDetails,
restoreErrorCallback
);
// What is the current active protocol and stage number to restore later
const { protocol, stage } = hangingProtocolService.getActiveProtocol();
const restoreErrorCallback = error => {
console.error(error);
uiNotificationService.show({
title: 'Multiplanar reconstruction (MPR) ',
message:
'Cannot create MPR for this DisplaySet since it is not reconstructable.',
'Something went wrong while trying to restore the previous layout.',
type: 'info',
duration: 3000,
});
};
// What is the current active protocol and stage number to restore later
const { protocol, stage } = hangingProtocolService.getActiveProtocol();
if (toggledState) {
resetCachedState();
@ -109,6 +103,23 @@ export default function toggleMPRHangingProtocol({
getToolGroup
);
const errorCallback = error => {
// Unable to create MPR, so be sure to return to the cached/original protocol.
hangingProtocolService.setProtocol(
cachedState.protocol.id,
viewportMatchDetails,
restoreErrorCallback
);
uiNotificationService.show({
title: 'Multiplanar reconstruction (MPR) ',
message:
'Cannot create MPR for this DisplaySet since it is not reconstructable.',
type: 'info',
duration: 3000,
});
};
hangingProtocolService.setProtocol(
MPR_TOOLGROUP_ID,
matchDetails,
@ -117,16 +128,6 @@ export default function toggleMPRHangingProtocol({
return;
}
const restoreErrorCallback = error => {
uiNotificationService.show({
title: 'Multiplanar reconstruction (MPR) ',
message:
'Something went wrong while trying to restore the previous layout.',
type: 'info',
duration: 3000,
});
};
_disableCrosshairs([MPR_TOOLGROUP_ID], getToolGroup);
const { layoutType, properties } = cachedState.viewportStructure;

View File

@ -42,7 +42,7 @@
"webpack-merge": "^5.7.3"
},
"dependencies": {
"@babel/runtime": "7.16.3",
"@babel/runtime": "^7.20.13",
"@cornerstonejs/calculate-suv": "^1.0.3"
}
}

View File

@ -176,6 +176,7 @@ export default function PanelMeasurementTable({
<div className="p-4 bg-primary-dark">
<Input
autoFocus
id="annotation"
className="mt-2 bg-black border-primary-main"
type="text"
containerClassName="mr-2"

View File

@ -37,7 +37,7 @@
"react": "^17.0.2"
},
"dependencies": {
"@babel/runtime": "7.7.6",
"@babel/runtime": "^7.20.13",
"classnames": "^2.3.2"
}
}

View File

@ -37,7 +37,7 @@
"react": "^17.0.2"
},
"dependencies": {
"@babel/runtime": "7.7.6",
"@babel/runtime": "^7.20.13",
"classnames": "^2.3.2"
}
}

View File

@ -44,7 +44,7 @@
"@ohif/ui": "^2.0.0"
},
"dependencies": {
"@babel/runtime": "7.16.3",
"@babel/runtime": "^7.20.13",
"@ohif/ui": "^2.0.0",
"@xstate/react": "^0.8.1",
"xstate": "^4.10.0"

View File

@ -198,6 +198,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
<div className="p-4 bg-primary-dark">
<Input
autoFocus
id="annotation"
className="mt-2 bg-black border-primary-main"
type="text"
containerClassName="mr-2"

View File

@ -0,0 +1,8 @@
const path = require('path');
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
const SRC_DIR = path.join(__dirname, '../src');
const DIST_DIR = path.join(__dirname, '../dist');
module.exports = (env, argv) => {
return webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
};

View File

@ -0,0 +1,43 @@
const webpack = require('webpack');
const merge = require('webpack-merge');
const path = require('path');
const webpackCommon = require('./../../../.webpack/webpack.base.js');
const pkg = require('./../package.json');
const ROOT_DIR = path.join(__dirname, './..');
const SRC_DIR = path.join(__dirname, '../src');
const DIST_DIR = path.join(__dirname, '../dist');
module.exports = (env, argv) => {
const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
return merge(commonConfig, {
stats: {
colors: true,
hash: true,
timings: true,
assets: true,
chunks: false,
chunkModules: false,
modules: false,
children: false,
warnings: true,
},
optimization: {
minimize: true,
sideEffects: true,
},
output: {
path: ROOT_DIR,
library: 'OHIFExtDICOMSR',
libraryTarget: 'umd',
libraryExport: 'default',
filename: pkg.main,
},
plugins: [
new webpack.optimize.LimitChunkCountPlugin({
maxChunks: 1,
}),
],
});
};

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Open Health Imaging Foundation
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,4 @@
# Test Extension
This extension will provide uitlities and module to help with testing. It is not intended to be used in production.
For instance, we can use the hanging protocol module to inject a hanging protocol into the core and later
use it to test the hanging protocol detection mechanism.

View File

@ -0,0 +1 @@
module.exports = require('../../babel.config.js');

View File

@ -0,0 +1,43 @@
{
"name": "@ohif/extension-test",
"version": "0.0.1",
"description": "OHIF extension used inside e2e testing",
"author": "OHIF",
"license": "MIT",
"repository": "OHIF/Viewers",
"main": "dist/index.umd.js",
"module": "src/index.tsx",
"engines": {
"node": ">=14",
"npm": ">=6",
"yarn": ">=1.16.0"
},
"files": [
"dist",
"README.md"
],
"publishConfig": {
"access": "public"
},
"scripts": {
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --debug --output-pathinfo",
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
"build:package": "yarn run build",
"start": "yarn run dev",
"test:unit": "jest --watchAll",
"test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests"
},
"peerDependencies": {
"@ohif/core": "^3.0.0",
"@ohif/ui": "^2.0.0",
"dcmjs": "0.29.4",
"dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8",
"prop-types": "^15.6.2",
"react": "^17.0.2"
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"classnames": "^2.3.2"
}
}

View File

@ -0,0 +1,5 @@
import packageJson from '../package.json';
const id = packageJson.name;
export { id };

View File

@ -0,0 +1,17 @@
import { id } from './id';
import { Types } from '@ohif/core';
/**
*
*/
const testExtension: Types.Extensions.Extension = {
/**
* Only required property. Should be a unique value across all extensions.
*/
id,
preRegistration() {
console.debug('hello from test-extension init.js');
},
};
export default testExtension;

View File

@ -37,7 +37,7 @@
"react": "^17.0.2"
},
"dependencies": {
"@babel/runtime": "7.7.6",
"@babel/runtime": "^7.20.13",
"classnames": "^2.3.2"
}
}

View File

@ -32,13 +32,12 @@
"@ohif/core": "^3.0.0",
"@ohif/extension-default": "^3.0.0",
"@ohif/extension-cornerstone": "^3.0.0",
"@ohif/extension-cornerstone": "^3.0.0",
"@ohif/extension-cornerstone-dicom-sr": "^3.0.0",
"@ohif/extension-dicom-pdf": "^3.0.1",
"@ohif/extension-dicom-video": "^3.0.1"
},
"dependencies": {
"@babel/runtime": "7.13.10"
"@babel/runtime": "^7.20.13"
},
"devDependencies": {
"webpack": "^5.50.0",

View File

@ -1,5 +1,5 @@
{
"name": "mode-basic-test-mode",
"name": "@ohif/mode-test",
"version": "0.0.1",
"description": "Basic mode for testing",
"author": "OHIF",
@ -38,10 +38,11 @@
"@ohif/extension-cornerstone-dicom-sr": "^3.0.0",
"@ohif/extension-dicom-pdf": "^3.0.1",
"@ohif/extension-dicom-video": "^3.0.1",
"@ohif/extension-measurement-tracking": "^3.0.0"
"@ohif/extension-measurement-tracking": "^3.0.0",
"@ohif/extension-test": "0.0.1"
},
"dependencies": {
"@babel/runtime": "7.16.3"
"@babel/runtime": "^7.20.13"
},
"devDependencies": {
"webpack": "^5.50.0",

View File

@ -54,6 +54,7 @@ const extensionDependencies = {
'@ohif/extension-cornerstone-dicom-seg': '^3.0.0',
'@ohif/extension-dicom-pdf': '^3.0.1',
'@ohif/extension-dicom-video': '^3.0.1',
'@ohif/extension-test': '^0.0.1',
};
function modeFactory() {

View File

@ -41,7 +41,7 @@
"@ohif/extension-measurement-tracking": "^3.0.0"
},
"dependencies": {
"@babel/runtime": "7.16.3"
"@babel/runtime": "^7.20.13"
},
"devDependencies": {
"webpack": "^5.50.0",

View File

@ -41,7 +41,7 @@
"@ohif/extension-measurement-tracking": "^3.0.0"
},
"dependencies": {
"@babel/runtime": "7.16.3"
"@babel/runtime": "^7.20.13"
},
"devDependencies": {
"webpack": "^5.50.0",

View File

@ -55,7 +55,7 @@
"link-list": "npm ls --depth=0 --link=true"
},
"dependencies": {
"@babel/runtime": "7.16.3",
"@babel/runtime": "^7.20.13",
"@kitware/vtk.js": "26.4.0",
"core-js": "^3.2.1"
},
@ -124,7 +124,7 @@
"start-server-and-test": "^1.10.0",
"style-loader": "^1.0.0",
"terser-webpack-plugin": "^5.1.4",
"typescript": "^4.2.3",
"typescript": "4.6.4",
"unused-webpack-plugin": "2.4.0",
"webpack": "^5.50.0",
"webpack-cli": "^4.7.2",
@ -146,6 +146,6 @@
]
},
"resolutions": {
"**/@babel/runtime": "7.16.3"
"**/@babel/runtime": "^7.20.13"
}
}

View File

@ -23,26 +23,29 @@
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-i18next": "^10.11.0",
"react-router": "^6.3.0",
"react-router-dom": "^6.3.0",
"react-router": "^6.8.1",
"react-router-dom": "^6.8.1",
"webpack": "^5.50.0",
"webpack-merge": "^5.7.3"
},
"dependencies": {
"@babel/runtime": "7.7.6"
"@babel/runtime": "^7.20.13"
},
"devDependencies": {
"@babel/core": "^7.5.0",
"@babel/plugin-proposal-class-properties": "^7.5.0",
"@babel/plugin-proposal-object-rest-spread": "^7.5.5",
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/plugin-transform-arrow-functions": "^7.2.0",
"@babel/plugin-transform-regenerator": "^7.4.5",
"@babel/plugin-transform-runtime": "^7.5.0",
"@babel/core": "^7.17.8",
"@babel/plugin-proposal-class-properties": "^7.16.7",
"@babel/plugin-proposal-object-rest-spread": "^7.17.3",
"@babel/plugin-proposal-private-methods": "^7.18.6",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-arrow-functions": "^7.16.7",
"@babel/plugin-transform-regenerator": "^7.16.7",
"@babel/plugin-transform-runtime": "^7.17.0",
"babel-plugin-inline-react-svg": "^2.0.1",
"@babel/preset-env": "^7.5.0",
"@babel/preset-react": "^7.0.0",
"@babel/preset-typescript": "^7.18.6",
"@babel/plugin-transform-typescript": "^7.13.0",
"@babel/preset-env": "^7.16.11",
"@babel/preset-react": "^7.16.7",
"@babel/preset-typescript": "^7.13.0",
"babel-eslint": "^8.0.3",
"babel-loader": "^8.0.0-beta.4",
"clean-webpack-plugin": "^4.0.0",

View File

@ -20,19 +20,21 @@
"@ohif/core": "^3.0.0"
},
"dependencies": {
"@babel/runtime": "7.7.6"
"@babel/runtime": "^7.20.13"
},
"devDependencies": {
"@babel/core": "^7.5.0",
"@babel/plugin-proposal-class-properties": "^7.5.0",
"@babel/plugin-proposal-object-rest-spread": "^7.5.5",
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/plugin-transform-arrow-functions": "^7.2.0",
"@babel/plugin-transform-regenerator": "^7.4.5",
"@babel/plugin-transform-runtime": "^7.5.0",
"@babel/preset-env": "^7.5.0",
"@babel/preset-react": "^7.0.0",
"@babel/preset-typescript": "^7.17.12",
"@babel/core": "^7.17.8",
"@babel/plugin-proposal-class-properties": "^7.16.7",
"@babel/plugin-proposal-object-rest-spread": "^7.17.3",
"@babel/plugin-proposal-private-methods": "^7.18.6",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-arrow-functions": "^7.16.7",
"@babel/plugin-transform-regenerator": "^7.16.7",
"@babel/plugin-transform-runtime": "^7.17.0",
"@babel/plugin-transform-typescript": "^7.13.0",
"@babel/preset-env": "^7.16.11",
"@babel/preset-react": "^7.16.7",
"@babel/preset-typescript": "^7.13.0",
"babel-eslint": "^8.0.3",
"babel-loader": "^8.0.0-beta.4",
"babel-plugin-inline-react-svg": "^2.0.1",

View File

@ -36,7 +36,7 @@
"@ohif/ui": "^2.0.0"
},
"dependencies": {
"@babel/runtime": "7.16.3",
"@babel/runtime": "^7.20.13",
"dcmjs": "0.29.4",
"dicomweb-client": "^0.6.0",
"isomorphic-base64": "^1.0.2",

View File

@ -35,6 +35,7 @@ export interface ExtensionParams extends ExtensionConstructor {
* have more values than this.
*/
export interface Extension {
id: string;
preRegistration?: (p: ExtensionParams) => void;
}
@ -160,11 +161,18 @@ export default class ExtensionManager {
// but currently since some extensions need to be registered before
// others, we need to run them sequentially. We need a postInit hook
// to avoid this sequential async registration
for (const extension of extensions) {
for (let i = 0; i < extensions.length; i++) {
const extension = extensions[i];
const hasConfiguration = Array.isArray(extension);
try {
if (hasConfiguration) {
const [ohifExtension, configuration] = extension;
// Important: for some reason in the line below the type
// of extension is not recognized as [ExtensionRegister,
// ExtensionConfiguration] by babel DON"T CHANGE IT
// Same for the for loop above don't use
// for (const extension of extensions)
const ohifExtension = extension[0];
const configuration = extension[1];
await this.registerExtension(
ohifExtension,
configuration,

View File

@ -185,7 +185,11 @@ class HangingProtocolService {
* @param protocolId - the id of the protocol
* @returns protocol - the protocol with the given id
*/
public getProtocolById(id: string): HangingProtocol.Protocol {
public getProtocolById(id: string): HangingProtocol.Protocol | undefined {
if (!id) {
return;
}
const protocol = this.protocols.get(id);
if (protocol instanceof Function) {
@ -233,7 +237,7 @@ class HangingProtocolService {
* If active protocols ids is null right now, then the specified
* protocol will become the only active protocol.
*/
public addActiveProtocol(id: string): void {
public addActiveProtocolId(id: string): void {
if (!id) {
return;
}
@ -247,17 +251,17 @@ class HangingProtocolService {
* Sets the active hanging protocols to use, by name. If the value is empty,
* then resets the active protocols to all the named items.
*/
public setActiveProtocols(hangingProtocol?: string[] | string): void {
if (!hangingProtocol || !hangingProtocol.length) {
public setActiveProtocolIds(protocolId?: string[] | string): void {
if (!protocolId || !protocolId.length) {
this.activeProtocolIds = null;
console.log('No active protocols, setting all to active');
return;
}
if (typeof hangingProtocol === 'string') {
this.setActiveProtocols([hangingProtocol]);
if (typeof protocolId === 'string') {
this.setActiveProtocolIds([protocolId]);
return;
}
this.activeProtocolIds = [...hangingProtocol];
this.activeProtocolIds = [...protocolId];
}
/**
@ -272,7 +276,6 @@ class HangingProtocolService {
* @param params.displaySets is the list of display sets associated with
* the studies to display in viewports.
* @param protocol is a specific protocol to apply.
* @returns
*/
public run({ studies, displaySets, activeStudy }, protocolId) {
this.studies = [...studies];

View File

@ -36,7 +36,7 @@
"react-i18next": "^10.11.0"
},
"dependencies": {
"@babel/runtime": "7.16.3",
"@babel/runtime": "^7.20.13",
"i18next-locize-backend": "^2.0.0",
"locize-editor": "^2.0.0",
"locize-lastused": "^1.1.0"

View File

@ -57,6 +57,7 @@ const Input = ({
};
Input.propTypes = {
id: PropTypes.string,
label: PropTypes.string,
containerClassName: PropTypes.string,
labelClassName: PropTypes.string,
@ -70,7 +71,7 @@ Input.propTypes = {
readOnly: PropTypes.bool,
onKeyPress: PropTypes.func,
onKeyDown: PropTypes.func,
disabled: PropTypes.bool
disabled: PropTypes.bool,
};
export default Input;

View File

@ -32,6 +32,7 @@ const Modal = ({
<Typography
variant="h4"
className="flex grow text-primary-light font-light"
data-cy="modal-header"
>
{title}
</Typography>

View File

@ -79,6 +79,7 @@ const Select = ({
className,
'flex flex-col flex-1 customSelect__wrapper'
)}
data-cy={`input-${id}`}
classNamePrefix="customSelect"
isDisabled={isDisabled}
isClearable={isClearable}

View File

@ -95,7 +95,11 @@ const ThumbnailTracked = ({
>
<Icon name={trackedIcon} className="w-4 mb-2 text-primary-light" />
</Tooltip>
<div className="text-xl leading-tight text-white text-center">
<div
className="text-xl leading-tight text-white text-center"
data-cy={'thumbnail-viewport-labels'}
>
{renderViewportLabels()}
</div>
</div>

View File

@ -29,18 +29,18 @@ const copyPluginFromExtensions = writePluginImportFile(SRC_DIR, DIST_DIR);
const setHeaders = (res, path) => {
if (path.indexOf('.gz') !== -1) {
res.setHeader('Content-Encoding', 'gzip')
res.setHeader('Content-Encoding', 'gzip');
} else if (path.indexOf('.br') !== -1) {
res.setHeader('Content-Encoding', 'br')
res.setHeader('Content-Encoding', 'br');
}
if (path.indexOf('.pdf') !== -1) {
res.setHeader('Content-Type', 'application/pdf');
} else if (path.indexOf('/frames') !== -1) {
res.setHeader('Content-Type', 'multipart/related')
res.setHeader('Content-Type', 'multipart/related');
} else {
res.setHeader('Content-Type', 'application/json')
res.setHeader('Content-Type', 'application/json');
}
}
};
module.exports = (env, argv) => {
const baseConfig = webpackBase(env, argv, { SRC_DIR, DIST_DIR });
@ -55,7 +55,7 @@ module.exports = (env, argv) => {
path: DIST_DIR,
filename: isProdBuild ? '[name].bundle.[chunkhash].js' : '[name].js',
publicPath: PUBLIC_URL, // Used by HtmlWebPackPlugin for asset prefix
devtoolModuleFilenameTemplate: function (info) {
devtoolModuleFilenameTemplate: function(info) {
if (isProdBuild) {
return `webpack:///${info.resourcePath}`;
} else {
@ -146,12 +146,12 @@ module.exports = (env, argv) => {
proxy: {
'/dicomweb': 'http://localhost:5000',
},
'static': [
static: [
{
directory: '../../testdata',
staticOptions: {
extensions: ['gz', 'br'],
index: "index.json.gz",
index: ['index.json.gz', 'index.mht.gz'],
redirect: true,
setHeaders,
},

View File

@ -17,8 +17,9 @@ function constructLines(input, categoryName) {
if (!input) return lines;
input.forEach(entry => {
const packageName = entry.packageName;
if (entry.default === false) return;
const packageName = entry.packageName;
const defaultImportName = `${categoryName}${pluginCount}`;
lines.importLines.push(
@ -66,7 +67,23 @@ function getRuntimeLoadModesExtensions() {
' for(const modeFactory of modesFactory) {\n' +
' const newModes = await modeFactory(modes,extensions);\n' +
' newModes.forEach(newMode => modes.push(newMode));\n' +
'}\n}\n'
'}\n}\n\n\n' +
'async function loadRuntimeImports(config) {\n' +
' if (config && config.modes && config.modes.length) {\n' +
' for (const modeName of config.modes) {\n' +
' const existingMode = modes.find(mode => mode.id === modeName);\n' +
' if (!existingMode) {\n' +
' if (modeName === `@ohif/mode-test`) {\n' +
' const mode = await import(`@ohif/mode-test`);\n' +
' window.modes.push(mode.default);\n' +
' const extension = await import(`@ohif/extension-test`);\n' +
' window.extensions.push(extension.default);\n' +
' }\n' +
' }\n' +
' };\n' +
' }\n' +
'}\n' +
'export { loadRuntimeImports };\n\n'
);
}
@ -201,11 +218,12 @@ function writePluginImportsFile(SRC_DIR, DIST_DIR) {
'dist'
);
console.log('copy plugins', [...copyPluginPublicToDistBuild,
...copyPluginPublicToDistLink,
...copyPluginDistToDistBuild,
...copyPluginDistToDistLink,
])
console.log('copy plugins', [
...copyPluginPublicToDistBuild,
...copyPluginPublicToDistLink,
...copyPluginDistToDistBuild,
...copyPluginDistToDistLink,
]);
return [
...copyPluginPublicToDistBuild,
...copyPluginPublicToDistLink,

View File

@ -0,0 +1,40 @@
import { defineConfig } from 'cypress';
export default defineConfig({
chromeWebSecurity: false,
e2e: {
experimentalRunAllSpecs: true,
supportFile: 'cypress/support/index.js',
setupNodeEvents(on, config) {
on('before:browser:launch', (browser, launchOptions) => {
// `args` is an array of all the arguments that will
// be passed to browsers when it launches
console.log(launchOptions.args); // print all current args
console.log('***', browser.family, browser.name, '***');
if (browser.family === 'chromium' && browser.name !== 'electron') {
// auto open devtools
launchOptions.args.push('--enable-features=SharedArrayBuffer');
}
// whatever you return here becomes the launchOptions
return launchOptions;
});
},
baseUrl: 'http://localhost:3000',
waitForAnimations: true,
chromeWebSecurity: false,
defaultCommandTimeout: 10000,
requestTimeout: 10000,
responseTimeout: 10000,
specPattern: 'cypress/integration/**/*.spec.[jt]s',
projectId: '4oe38f',
video: false,
reporter: 'junit',
reporterOptions: {
mochaFile: 'cypress/results/test-output.xml',
toConsole: true,
},
},
});

View File

@ -1,15 +0,0 @@
{
"baseUrl": "http://localhost:3000",
"chromeWebSecurity": false,
"waitForAnimations": true,
"defaultCommandTimeout": 10000,
"requestTimeout": 10000,
"responseTimeout": 10000,
"projectId": "4oe38f",
"video": false,
"reporter": "junit",
"reporterOptions": {
"mochaFile": "cypress/results/test-output.xml",
"toConsole": true
}
}

View File

@ -0,0 +1,6 @@
module.exports = {
plugins: ['cypress'],
env: {
'cypress/globals': true,
},
};

View File

@ -1,18 +1,13 @@
describe('OHIF PDF Display', function () {
before(() => {
cy.openStudyInViewer(
'2.25.317377619501274872606137091638706705333'
);
});
describe('OHIF PDF Display', function() {
beforeEach(function() {
cy.openStudyInViewer('2.25.317377619501274872606137091638706705333');
beforeEach(function () {
cy.resetViewport().wait(50);
});
it('checks if series thumbnails are being displayed', function () {
it('checks if series thumbnails are being displayed', function() {
cy.get('[data-cy="study-browser-thumbnail"]')
.its('length')
.should('be.gt', 0);
});
});

View File

@ -1,15 +1,10 @@
describe('OHIF Video Display', function () {
before(() => {
cy.openStudyInViewer(
'2.25.96975534054447904995905761963464388233'
);
});
beforeEach(function () {
describe('OHIF Video Display', function() {
beforeEach(function() {
cy.openStudyInViewer('2.25.96975534054447904995905761963464388233');
cy.resetViewport().wait(50);
});
it('checks if series thumbnails are being displayed', function () {
it('checks if series thumbnails are being displayed', function() {
cy.get('[data-cy="study-browser-thumbnail"]')
.its('length')
.should('be.gt', 1);

View File

@ -1,5 +1,5 @@
describe('OHIF Cornerstone Hotkeys', () => {
before(() => {
beforeEach(() => {
cy.checkStudyRouteInViewer(
'1.2.840.113619.2.5.1762583153.215519.978957063.78'
);
@ -15,12 +15,8 @@ describe('OHIF Cornerstone Hotkeys', () => {
});
cy.expectMinimumThumbnails(3);
});
beforeEach(() => {
cy.initCornerstoneToolsAliases();
cy.initCommonElementsAliases();
cy.resetViewport();
});
it('checks if hotkeys "R" and "L" can rotate the image', () => {
@ -45,11 +41,11 @@ describe('OHIF Cornerstone Hotkeys', () => {
it('checks if hotkeys "V" and "H" can flip the image', () => {
// Hotkey H
cy.get('body').type('H');
cy.get('body').type('h');
cy.get('@viewportInfoMidLeft').should('contains.text', 'L');
cy.get('@viewportInfoMidTop').should('contains.text', 'A');
// Hotkey V
cy.get('body').type('V');
cy.get('body').type('v');
cy.get('@viewportInfoMidLeft').should('contains.text', 'L');
cy.get('@viewportInfoMidTop').should('contains.text', 'P');
});
@ -73,26 +69,17 @@ describe('OHIF Cornerstone Hotkeys', () => {
// cy.get('@viewportInfoTopLeft').should('contains.text', 'Zoom:1.67x');
// });
// it('checks if hotkey "SPACEBAR" can reset the image', () => {
// //Click on button and verify if icon is active on toolbar
// cy.get('@zoomBtn')
// .click()
// .then($zoomBtn => {
// cy.wrap($zoomBtn).should('have.class', 'active');
// });
it('checks if hotkey "SPACEBAR" can reset the image', () => {
// Press multiples hotkeys
cy.get('body').type('v+++i');
cy.get('@viewportInfoMidLeft').should('contains.text', 'R');
cy.get('@viewportInfoMidTop').should('contains.text', 'P');
// // Press multiples hotkeys
// cy.get('body').type('V+++I');
// cy.get('@viewportInfoMidLeft').should('contains.text', 'L');
// cy.get('@viewportInfoMidTop').should('contains.text', 'A');
// cy.get('@viewportInfoTopLeft').should('contains.text', 'Zoom:2.30x');
// // Hotkey SPACEBAR
// cy.get('body').type(' ');
// cy.get('@viewportInfoMidLeft').should('contains.text', 'R');
// cy.get('@viewportInfoMidTop').should('contains.text', 'A');
// cy.get('@viewportInfoTopLeft').should('contains.text', 'Zoom:1.67x');
// });
// Hotkey SPACEBAR
cy.get('body').type(' ');
cy.get('@viewportInfoMidLeft').should('contains.text', 'R');
cy.get('@viewportInfoMidTop').should('contains.text', 'A');
});
/*
// TODO: Pretty sure this is not implemented yet

View File

@ -1,12 +1,9 @@
describe('OHIF Cornerstone Toolbar', () => {
before(() => {
beforeEach(() => {
cy.checkStudyRouteInViewer(
'1.2.840.113619.2.5.1762583153.215519.978957063.78'
);
cy.expectMinimumThumbnails(3);
});
beforeEach(() => {
cy.initCornerstoneToolsAliases();
cy.initCommonElementsAliases();
@ -16,8 +13,6 @@ describe('OHIF Cornerstone Toolbar', () => {
//const expectedText = 'Ser: 1';
//cy.get('@viewportInfoBottomLeft').should('contains.text', expectedText);
cy.resetViewport();
});
it('checks if all primary buttons are being displayed', () => {

View File

@ -1,32 +1,17 @@
describe('OHIF Download Snapshot File', () => {
before(() => {
beforeEach(() => {
cy.checkStudyRouteInViewer(
'1.2.840.113619.2.5.1762583153.215519.978957063.78'
);
cy.expectMinimumThumbnails(3);
});
beforeEach(() => {
cy.openDownloadImageModal();
});
/**
afterEach(() => {
// Close modal
// TODO: Modal.tsx currently uses Icon instead of IconButton, so
// it's not as easy to give it the cypress data attribute.
// We should fix that first
cy.get('[data-cy="close-button"]')
.scrollIntoView()
.click();
});*/
/*it('checks displayed information for Desktop experience', function() {
it('checks displayed information for Desktop experience', function() {
// Set Desktop resolution
cy.viewport(1750, 720);
// cy.viewport(1750, 720);
// Visual comparison
cy.screenshot('Download Image Modal - Desktop experience');
// cy.screenshot('Download Image Modal - Desktop experience');
//Check if all elements are displayed
// TODO: need to add this attribute to the modal
@ -36,19 +21,19 @@ describe('OHIF Download Snapshot File', () => {
// Check input fields
// TODO: select2
cy.get('[data-cy="file-type"]')
.select('png')
.should('have.value', 'png')
.select('jpg')
.should('have.value', 'jpg');
// cy.get('[data-cy="file-type"]')
// .select('png')
// .should('have.value', 'png')
// .select('jpg')
// .should('have.value', 'jpg');
// Check image preview
cy.get('[data-cy="image-preview"]').should('contain.text', 'Image preview');
TODO: This is a canvas now, not an img with src
cy.get('[data-cy="viewport-preview-img"]')
.should('have.attr', 'src')
.and('include', 'data:image');
//TODO: This is a canvas now, not an img with src
// cy.get('[data-cy="viewport-preview-img"]')
// .should('have.attr', 'src')
// .and('include', 'data:image');
// Check buttons
cy.get('[data-cy="cancel-btn"]')
@ -58,7 +43,6 @@ describe('OHIF Download Snapshot File', () => {
.scrollIntoView()
.should('be.visible');
});
*/
/*it('cancel changes on download modal', function() {
//Change Image Width, Filename and File Type
@ -88,8 +72,7 @@ describe('OHIF Download Snapshot File', () => {
// TO-DO once issue is fixed: https://github.com/OHIF/Viewers/issues/1217
// it('checks error messages for empty fields', function() {
// //Clear fields Image Width and Filename
// cy.get('[data-cy="image-width"]')
// .clear();
// cy.get('[data-cy="image-width"]').clear();
// cy.get('[data-cy="file-name"]').clear();
// //Click on Download button

View File

@ -0,0 +1,98 @@
describe('OHIF Study Viewer Page', function() {
beforeEach(function() {
cy.checkStudyRouteInViewer(
'1.2.840.113619.2.5.1762583153.215519.978957063.78'
);
cy.expectMinimumThumbnails(3);
cy.initCommonElementsAliases();
cy.initCornerstoneToolsAliases();
});
it('scrolls series stack using scrollbar', function() {
cy.scrollToIndex(13);
cy.get('@viewportInfoTopRight').should('contains.text', '14');
});
it('performs right click to zoom', function() {
// This is not used to activate the tool, it is used to ensure the
// top left viewport info shows the zoom values (it only shows up
// when the zoom tool is active)
cy.get('@zoomBtn')
.click()
.then($zoomBtn => {
cy.wrap($zoomBtn).should('have.class', 'active');
});
const zoomLevelInitial = cy
.get('@viewportInfoTopLeft')
.then($viewportInfo => {
return $viewportInfo.text().substring(6, 9);
});
//Right click on viewport
cy.get('@viewport')
.trigger('mousedown', 'top', { buttons: 2 })
.trigger('mousemove', 'center', { buttons: 2 })
.trigger('mouseup');
// make sure the new zoom level is less than the initial
cy.get('@viewportInfoTopLeft').then($viewportInfo => {
const zoomLevelFinal = $viewportInfo.text().substring(6, 9);
expect(zoomLevelFinal < zoomLevelInitial).to.eq(true);
});
});
/*it('performs middle click to pan', function() {
//Get image position from cornerstone and check if y axis was modified
let cornerstone;
let currentPan;
// TO DO: Replace the cornerstone pan check by Percy snapshot comparison
cy.window()
.its('cornerstone')
.then(c => {
cornerstone = c;
currentPan = () =>
cornerstone.getEnabledElements()[0].viewport.translation;
});
//pan image with middle click
cy.get('@viewport')
.trigger('mousedown', 'center', { buttons: 3 })
.trigger('mousemove', 'bottom', { buttons: 3 })
.trigger('mouseup', 'bottom')
.then(() => {
expect(currentPan().y > 0).to.eq(true);
});
});*/
/*it('opens About modal and verify the displayed information', function() {
cy.get('[data-cy="options-dropdown"]')
.first()
.click();
cy.get('[data-cy="about-modal"]')
.as('aboutOverlay')
.should('be.visible');
//check buttons and links
cy.get('[data-cy="about-modal"]')
.should('contains.text', 'Visit the forum')
.and('contains.text', 'Report an issue')
.and('contains.text', 'https://github.com/OHIF/Viewers/');
//check version number
cy.get('[data-cy="about-modal"]').then($modal => {
cy.get('[data-cy="header-version-info"]').should($headerVersionNumber => {
$headerVersionNumber = $headerVersionNumber.text().substring(1);
expect($modal).to.contain($headerVersionNumber);
});
});
//close modal
cy.get('[data-cy="close-button"]').click();
cy.get('@aboutOverlay').should('not.exist');
});
*/
});

View File

@ -0,0 +1,213 @@
describe('OHIF Measurement Panel', function() {
beforeEach(function() {
cy.checkStudyRouteInViewer(
'1.2.840.113619.2.5.1762583153.215519.978957063.78'
);
cy.expectMinimumThumbnails(3);
cy.initCommonElementsAliases();
cy.initCornerstoneToolsAliases();
cy.resetViewport().wait(50);
});
it('checks if Measurements right panel can be hidden/displayed', function() {
cy.get('@measurementsPanel').should('exist');
cy.get('@measurementsPanel').should('be.visible');
cy.get('@RightCollapseBtn').click();
cy.get('@measurementsPanel').should('not.exist');
cy.get('@RightCollapseBtn').click();
cy.get('@measurementsPanel').should('exist');
cy.get('@measurementsPanel').should('be.visible');
});
it('checks if measurement item can be Relabeled under Measurements panel', function() {
// Add length measurement
cy.addLengthMeasurement();
cy.get('[data-cy="measurement-tracking-prompt-begin-tracking"]').should(
'exist'
);
cy.get('[data-cy="measurement-tracking-prompt-begin-tracking"]').should(
'be.visible'
);
cy.get('[data-cy="prompt-begin-tracking-yes"]').click();
cy.get('[data-cy="measurement-item"]').click();
cy.get('[data-cy="measurement-item"]')
.find('svg')
.click();
// enter Bone label
cy.get('[data-cy="input-annotation"]').should('exist');
cy.get('[data-cy="input-annotation"]').should('be.visible');
cy.get('[data-cy="input-annotation"]').type('Bone{enter}');
// Verify if 'Bone' label was added
cy.get('[data-cy="measurement-item"]').should('contain.text', 'Bone');
});
it('checks if image would jump when clicked on a measurement item', function() {
// Add length measurement
cy.addLengthMeasurement();
cy.get('[data-cy="prompt-begin-tracking-yes"]').click();
cy.scrollToIndex(13);
cy.addLengthMeasurement([100, 100], [200, 200]); //Adding measurement in the viewport
cy.get('@viewportInfoTopRight').should('contains.text', '(14/');
// Click on first measurement item
cy.get('[data-cy="measurement-item"]').click();
cy.get('@viewportInfoTopRight').should('contains.text', '(1/');
cy.get('@viewportInfoTopRight').should('not.contains.text', '(14/');
});
/*
TODO: Not sure why this is failing
it('checks if Description can be added to measurement item under Measurements panel', () => {
cy.addLengthMeasurement(); //Adding measurement in the viewport
cy.get('@measurementsBtn').click();
cy.get('.measurementItem').click();
// Click "Description"
cy.get('.btnAction')
.contains('Description')
.click();
// Enter description text
const descriptionText = 'Adding text for description test';
cy.get('#description').type(descriptionText);
// Confirm
cy.get('.btn-confirm').click();
//Verify if descriptionText was added
cy.get('.measurementLocation').should('contain.text', descriptionText);
// Remove the measurement we just added
cy.get('.btnAction')
.last()
.contains('Delete')
.click()
// Close panel
cy.get('@measurementsBtn').click();
cy.get('@measurementsPanel').should('not.be.enabled');
});
*/
/*it('checks if measurement item can be deleted through the context menu on the viewport', function() {
cy.addLengthMeasurement([100, 100], [200, 100]); //Adding measurement in the viewport
//Right click on measurement annotation
const [x1, y1] = [150, 100];
cy.get('@viewport')
.trigger('mousedown', x1, y1, {
which: 3,
})
.trigger('mouseup', x1, y1, {
which: 3,
})
.wait(300)
.then(() => {
//Contextmenu is visible
cy.get('.ToolContextMenu').should('be.visible');
});
//Click "Delete measurement"
cy.get('.form-action')
.contains('Delete measurement')
.click();
//Open measurements menu
cy.get('@measurementsBtn').click();
//Verify measurements was removed from panel
cy.get('.measurementItem')
.should('not.exist')
.log('Annotation successfully removed');
//Close panel
cy.get('@measurementsBtn').click();
cy.get('@measurementsPanel').should('not.exist');
});*/
/*it('adds relabel and description to measurement item through the context menu on the viewport', function() {
cy.addLengthMeasurement([100, 100], [200, 100]); //Adding measurement in the viewport
// Relabel
// Right click on measurement annotation
const [x1, y1] = [150, 100];
cy.get('@viewport')
.trigger('mousedown', x1, y1, {
which: 3,
})
.trigger('mouseup', x1, y1, {
which: 3,
});
// Contextmenu is visible
cy.get('.ToolContextMenu').should('be.visible');
// Click "Relabel"
cy.get('.form-action')
.contains('Relabel')
.click();
// Search for "Brain"
cy.get('.searchInput').type('Brain');
// Select "Brain" Result
cy.get('.treeInputs > .wrapperLabel')
.contains('Brain')
.click();
// Confirm Selection
cy.get('.checkIconWrapper').click();
// Description
// Right click on measurement annotation
cy.get('@viewport')
.trigger('mousedown', x1, y1, {
which: 3,
})
.trigger('mouseup', x1, y1, {
which: 3,
});
// Contextmenu is visible
cy.get('.ToolContextMenu').should('be.visible');
// Click "Description"
cy.get('.form-action')
.contains('Add Description')
.click();
// Enter description text
const descriptionText = 'Adding text for description test';
cy.get('#description').type(descriptionText);
// Confirm
cy.get('.btn-confirm').click();
//Open measurements menu
cy.get('@measurementsBtn').click();
// Verify if label was added
cy.get('.measurementLocation')
.should('contain.text', 'Brain')
.log('Relabel added with success');
//Verify if descriptionText was added
cy.get('.measurementLocation')
.should('contain.text', descriptionText)
.log('Description added with success');
// Close panel
cy.get('@measurementsBtn').click();
cy.get('@measurementsPanel').should('not.exist');
});*/
});

View File

@ -0,0 +1,45 @@
describe('OHIF Study Viewer Page', function() {
beforeEach(function() {
cy.checkStudyRouteInViewer(
'1.2.840.113619.2.5.1762583153.215519.978957063.78'
);
cy.expectMinimumThumbnails(3);
cy.initCommonElementsAliases();
cy.initCornerstoneToolsAliases();
cy.resetViewport().wait(50);
});
it('checks if series thumbnails are being displayed', function() {
cy.get('[data-cy="study-browser-thumbnail"]')
.its('length')
.should('be.gt', 1);
});
it('drags and drop a series thumbnail into viewport', function() {
cy.get('[data-cy="study-browser-thumbnail"]:nth-child(2)') //element to be dragged
.drag('.cornerstone-canvas'); //dropzone element
//const expectedText =
// 'Ser: 2Img: 1 1/13512 x 512Loc: -17.60 mm Thick: 3.00 mm';
//cy.get('@viewportInfoBottomLeft').should('contain.text', expectedText);
});
it('checks if Series left panel can be hidden/displayed', function() {
cy.get('@seriesPanel').should('exist');
cy.get('@seriesPanel').should('be.visible');
cy.get('@seriesBtn').click();
cy.get('@seriesPanel').should('not.exist');
cy.get('@seriesBtn').click();
cy.get('@seriesPanel').should('exist');
cy.get('@seriesPanel').should('be.visible');
});
it('performs double-click to load thumbnail in active viewport', () => {
cy.get('[data-cy="study-browser-thumbnail"]:nth-child(2)').dblclick();
//cy.get('@viewportInfoBottomLeft').should('contains.text', expectedText);
});
});

View File

@ -1,339 +0,0 @@
describe('OHIF Study Viewer Page', function() {
before(() => {
cy.checkStudyRouteInViewer(
'1.2.840.113619.2.5.1762583153.215519.978957063.78'
);
cy.expectMinimumThumbnails(3);
});
beforeEach(function() {
cy.initCommonElementsAliases();
cy.initCornerstoneToolsAliases();
cy.resetViewport().wait(50);
});
it('checks if series thumbnails are being displayed', function() {
cy.get('[data-cy="study-browser-thumbnail"]')
.its('length')
.should('be.gt', 1);
});
it('drags and drop a series thumbnail into viewport', function() {
cy.get('[data-cy="study-browser-thumbnail"]:nth-child(2)') //element to be dragged
.drag('.cornerstone-canvas'); //dropzone element
//const expectedText =
// 'Ser: 2Img: 1 1/13512 x 512Loc: -17.60 mm Thick: 3.00 mm';
//cy.get('@viewportInfoBottomLeft').should('contain.text', expectedText);
});
it('checks if Series left panel can be hidden/displayed', function() {
cy.get('@seriesPanel').should('exist');
cy.get('@seriesPanel').should('be.visible');
cy.get('@seriesBtn').click();
cy.get('@seriesPanel').should('not.exist');
cy.get('@seriesBtn').click();
cy.get('@seriesPanel').should('exist');
cy.get('@seriesPanel').should('be.visible');
});
it('checks if Measurements right panel can be hidden/displayed', function() {
cy.get('@measurementsPanel').should('exist');
cy.get('@measurementsPanel').should('be.visible');
cy.get('@RightCollapseBtn').click();
cy.get('@measurementsPanel').should('not.exist');
cy.get('@RightCollapseBtn').click();
cy.get('@measurementsPanel').should('exist');
cy.get('@measurementsPanel').should('be.visible');
});
/*it('checks if measurement item can be Relabeled under Measurements panel', function() {
// Add length measurement
cy.addLengthMeasurement();
cy.get('[data-cy="measurement-tracking-prompt-begin-tracking"]').should('exist')
cy.get('[data-cy="measurement-tracking-prompt-begin-tracking"]').should('be.visible')
cy.get('[data-cy="prompt-begin-tracking-yes"]').click();
cy.get('.measurementItem').click();
// Click "Relabel"
cy.get('.btnAction', { timeout: 10000 })
.first()
.contains('Relabel')
.click().should('be.visible');
// Search for "Bone"
cy.get('.searchInput').type('Bone');
// Select "Bone" Result
cy.get('.treeInputs > .wrapperLabel')
.contains('Bone')
.click();
// Confirm Selection
cy.get('.checkIconWrapper').click();
// Verify if 'Bone' label was added
cy.get('.measurementLocation').should('contain.text', 'Bone');
// Remove the measurement we just added
cy.get('.btnAction')
.last()
.contains('Delete')
.click()
// Close panel
cy.get('@measurementsBtn').click();
cy.get('@measurementsPanel').should('not.exist');
});*/
/*
TODO: Not sure why this is failing
it('checks if Description can be added to measurement item under Measurements panel', () => {
cy.addLengthMeasurement(); //Adding measurement in the viewport
cy.get('@measurementsBtn').click();
cy.get('.measurementItem').click();
// Click "Description"
cy.get('.btnAction')
.contains('Description')
.click();
// Enter description text
const descriptionText = 'Adding text for description test';
cy.get('#description').type(descriptionText);
// Confirm
cy.get('.btn-confirm').click();
//Verify if descriptionText was added
cy.get('.measurementLocation').should('contain.text', descriptionText);
// Remove the measurement we just added
cy.get('.btnAction')
.last()
.contains('Delete')
.click()
// Close panel
cy.get('@measurementsBtn').click();
cy.get('@measurementsPanel').should('not.be.enabled');
});
*/
/*it('checks if measurement item can be deleted through the context menu on the viewport', function() {
cy.addLengthMeasurement([100, 100], [200, 100]); //Adding measurement in the viewport
//Right click on measurement annotation
const [x1, y1] = [150, 100];
cy.get('@viewport')
.trigger('mousedown', x1, y1, {
which: 3,
})
.trigger('mouseup', x1, y1, {
which: 3,
})
.wait(300)
.then(() => {
//Contextmenu is visible
cy.get('.ToolContextMenu').should('be.visible');
});
//Click "Delete measurement"
cy.get('.form-action')
.contains('Delete measurement')
.click();
//Open measurements menu
cy.get('@measurementsBtn').click();
//Verify measurements was removed from panel
cy.get('.measurementItem')
.should('not.exist')
.log('Annotation successfully removed');
//Close panel
cy.get('@measurementsBtn').click();
cy.get('@measurementsPanel').should('not.exist');
});*/
/*it('adds relabel and description to measurement item through the context menu on the viewport', function() {
cy.addLengthMeasurement([100, 100], [200, 100]); //Adding measurement in the viewport
// Relabel
// Right click on measurement annotation
const [x1, y1] = [150, 100];
cy.get('@viewport')
.trigger('mousedown', x1, y1, {
which: 3,
})
.trigger('mouseup', x1, y1, {
which: 3,
});
// Contextmenu is visible
cy.get('.ToolContextMenu').should('be.visible');
// Click "Relabel"
cy.get('.form-action')
.contains('Relabel')
.click();
// Search for "Brain"
cy.get('.searchInput').type('Brain');
// Select "Brain" Result
cy.get('.treeInputs > .wrapperLabel')
.contains('Brain')
.click();
// Confirm Selection
cy.get('.checkIconWrapper').click();
// Description
// Right click on measurement annotation
cy.get('@viewport')
.trigger('mousedown', x1, y1, {
which: 3,
})
.trigger('mouseup', x1, y1, {
which: 3,
});
// Contextmenu is visible
cy.get('.ToolContextMenu').should('be.visible');
// Click "Description"
cy.get('.form-action')
.contains('Add Description')
.click();
// Enter description text
const descriptionText = 'Adding text for description test';
cy.get('#description').type(descriptionText);
// Confirm
cy.get('.btn-confirm').click();
//Open measurements menu
cy.get('@measurementsBtn').click();
// Verify if label was added
cy.get('.measurementLocation')
.should('contain.text', 'Brain')
.log('Relabel added with success');
//Verify if descriptionText was added
cy.get('.measurementLocation')
.should('contain.text', descriptionText)
.log('Description added with success');
// Close panel
cy.get('@measurementsBtn').click();
cy.get('@measurementsPanel').should('not.exist');
});*/
it('scrolls series stack using scrollbar', function() {
// Workaround implemented based on Cypress issue:
// https://github.com/cypress-io/cypress/issues/1570#issuecomment-450966053
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
'value'
).set;
cy.get('input.imageSlider[type=range]').then($range => {
// get the DOM node
const range = $range[0];
// set the value manually
nativeInputValueSetter.call(range, 13);
// now dispatch the event
range.dispatchEvent(new Event('change', { value: 13, bubbles: true }));
});
/*const expectedText =
'Ser: 2Img: 13 13/13512 x 512Loc: 18.40 mm Thick: 3.00 mm'; //'Img: 13 13/13';
cy.get('@viewportInfoBottomLeft').should('contains.text', expectedText);*/
});
it('performs double-click to load thumbnail in active viewport', () => {
cy.get('[data-cy="study-browser-thumbnail"]:nth-child(2)').dblclick();
//const expectedText = 'Ser: 3';
//cy.get('@viewportInfoBottomLeft').should('contains.text', expectedText);
});
// it('performs right click to zoom', function() {
// // This is not used to activate the tool, it is used to ensure the
// // top left viewport info shows the zoom values (it only shows up
// // when the zoom tool is active)
// cy.get('@zoomBtn')
// .click()
// .then($zoomBtn => {
// cy.wrap($zoomBtn).should('have.class', 'active');
// });
// //Right click on viewport
// cy.get('@viewport')
// .trigger('mousedown', 'top', { buttons: 2 })
// .trigger('mousemove', 'center', { buttons: 2 })
// .trigger('mouseup');
// const expectedText = 'Zoom:6.65x';
// cy.get('@viewportInfoTopLeft').should('contains.text', expectedText);
// });
/*it('performs middle click to pan', function() {
//Get image position from cornerstone and check if y axis was modified
let cornerstone;
let currentPan;
// TO DO: Replace the cornerstone pan check by Percy snapshot comparison
cy.window()
.its('cornerstone')
.then(c => {
cornerstone = c;
currentPan = () =>
cornerstone.getEnabledElements()[0].viewport.translation;
});
//pan image with middle click
cy.get('@viewport')
.trigger('mousedown', 'center', { buttons: 3 })
.trigger('mousemove', 'bottom', { buttons: 3 })
.trigger('mouseup', 'bottom')
.then(() => {
expect(currentPan().y > 0).to.eq(true);
});
});*/
/*it('opens About modal and verify the displayed information', function() {
cy.get('[data-cy="options-dropdown"]')
.first()
.click();
cy.get('[data-cy="about-modal"]')
.as('aboutOverlay')
.should('be.visible');
//check buttons and links
cy.get('[data-cy="about-modal"]')
.should('contains.text', 'Visit the forum')
.and('contains.text', 'Report an issue')
.and('contains.text', 'https://github.com/OHIF/Viewers/');
//check version number
cy.get('[data-cy="about-modal"]').then($modal => {
cy.get('[data-cy="header-version-info"]').should($headerVersionNumber => {
$headerVersionNumber = $headerVersionNumber.text().substring(1);
expect($modal).to.contain($headerVersionNumber);
});
});
//close modal
cy.get('[data-cy="close-button"]').click();
cy.get('@aboutOverlay').should('not.exist');
});*/
});

View File

@ -1,12 +1,10 @@
//We are keeping the hardcoded results values for the study list tests
//this is intended to be running in a controled docker environment with test data.
describe('OHIF Study List', function () {
context('Desktop resolution', function () {
before(function () {
describe('OHIF Study List', function() {
context('Desktop resolution', function() {
beforeEach(function() {
cy.openStudyList();
});
beforeEach(function () {
cy.viewport(1750, 720);
cy.initStudyListAliasesOnDesktop();
//Clear all text fields
@ -14,14 +12,9 @@ describe('OHIF Study List', function () {
cy.get('@MRN').clear();
cy.get('@AccessionNumber').clear();
cy.get('@StudyDescription').clear();
// TODO: Find out how to clear this input
// select-2 / react-select has some specific ways
// to trigger things
//cy.get('@modalities')
});
it('Displays several studies initially', function () {
it('Displays several studies initially', function() {
cy.waitStudyList();
cy.get('@searchResult2').should($list => {
expect($list.length).to.be.greaterThan(1);
@ -30,7 +23,7 @@ describe('OHIF Study List', function () {
});
});
it('searches Patient Name with exact string', function () {
it('searches Patient Name with exact string', function() {
cy.get('@PatientName').type('Juno');
//Wait result list to be displayed
cy.waitStudyList();
@ -40,8 +33,7 @@ describe('OHIF Study List', function () {
});
});
/* TODO - re-enable this once the test server is fixed to allow searching by mrn
it('searches MRN with exact string', function () {
it('searches MRN with exact string', function() {
cy.get('@MRN').type('0000003');
//Wait result list to be displayed
cy.waitStudyList();
@ -50,31 +42,17 @@ describe('OHIF Study List', function () {
expect($list).to.contain('0000003');
});
});
*/
it('searches Accession with exact string', function () {
cy.get('@AccessionNumber').type('0000155811');
it('searches Accession with exact string', function() {
cy.get('@AccessionNumber').type('321');
//Wait result list to be displayed
cy.waitStudyList();
cy.get('@searchResult2').should($list => {
expect($list.length).to.be.eq(1);
expect($list).to.contain('0000155811');
expect($list).to.contain('321');
});
});
// TODO: need to be able to programmatically change react-select
/*it('searches Modality with camel case', function() {
cy.get('@modalities').type('Ct');
//Wait result list to be displayed
cy.waitStudyList();
cy.get('@searchResult2').should($list => {
expect($list.length).to.be.greaterThan(1);
expect($list).to.contain('CT');
});
});*/
/*
TODO: Currently broken in dicomweb-server
it('searches Description with exact string', function() {
cy.get('@StudyDescription').type('PETCT');
//Wait result list to be displayed
@ -84,10 +62,19 @@ describe('OHIF Study List', function () {
expect($list).to.contain('PETCT');
});
});
*/
// TODO: need to be able to programmatically change react-select
/*it('changes Rows per page and checks the study count', function() {
/* Todo: fix react select
it('searches Modality with camel case', function() {
cy.get('@modalities').type('Ct');
// Wait result list to be displayed
cy.waitStudyList();
cy.get('@searchResult2').should($list => {
expect($list.length).to.be.greaterThan(1);
expect($list).to.contain('CT');
});
});
it('changes Rows per page and checks the study count', function() {
//Show Rows per page options
const pageRows = [25, 50, 100];

View File

@ -0,0 +1,139 @@
describe('OHIF MPR', () => {
beforeEach(() => {
cy.checkStudyRouteInViewer(
'1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1'
);
cy.expectMinimumThumbnails(3);
cy.initCornerstoneToolsAliases();
cy.initCommonElementsAliases();
});
it('should not go MPR for non reconstructible displaySets', () => {
cy.get('[data-cy="MPR"]').click();
cy.get('.cornerstone-canvas').should('have.length', 1);
});
it('should go MPR for reconstructible displaySets and come back', () => {
cy.wait(1000);
cy.get(':nth-child(3) > [data-cy="study-browser-thumbnail"]').dblclick();
cy.get('[data-cy="MPR"]').click();
cy.get('.cornerstone-canvas').should('have.length', 3);
cy.get('[data-cy="MPR"]').click();
cy.get('.cornerstone-canvas').should('have.length', 1);
});
it('should render correctly the MPR', () => {
cy.wait(1000);
cy.get(':nth-child(3) > [data-cy="study-browser-thumbnail"]').dblclick();
cy.get('[data-cy="MPR"]').click();
cy.get('[data-cy="thumbnail-viewport-labels"]').should('have.length', 3);
cy.get('.cornerstone-canvas').should('have.length', 3);
cy.get('[data-cy="thumbnail-viewport-labels"]')
.eq(2)
.find('div')
.should('have.length', 3)
.each(($div, index) => {
const text = $div.text();
switch (index) {
case 0:
expect(text).to.equal('A');
break;
case 1:
expect(text).to.equal('B');
break;
case 2:
expect(text).to.equal('C');
break;
default:
throw new Error(`Unexpected div found with text: ${text}`);
}
});
// check cornerstone to see if each has images
// we can later do visual testing to match the images with a baseline
cy.window()
.its('cornerstone')
.then(cornerstone => {
const viewports = cornerstone.getRenderingEngines()[0].getViewports();
const imageData1 = viewports[0].getImageData();
const imageData2 = viewports[1].getImageData();
const imageData3 = viewports[2].getImageData();
// for some reason map doesn't work here
cy.wrap(imageData1).should('not.be', undefined);
cy.wrap(imageData2).should('not.be', undefined);
cy.wrap(imageData3).should('not.be', undefined);
cy.wrap(imageData1.dimensions).should(
'deep.equal',
imageData2.dimensions
);
cy.wrap(imageData1.origin).should('deep.equal', imageData2.origin);
});
cy.get('[data-cy="MPR"]').click();
cy.get('.cornerstone-canvas').should('have.length', 1);
// should not have any div under it
cy.get('[data-cy="thumbnail-viewport-labels"]')
.eq(2)
.find('div')
.should('have.length', 0);
});
it('should correctly render Crosshairs for MPR', () => {
cy.wait(1000);
cy.get('[data-cy="Crosshairs"]').click();
cy.window()
.its('cornerstoneTools')
.then(cornerstoneTools => {
const state = cornerstoneTools.annotation.state.getDefaultAnnotationManager();
const fORMap = state.annotations;
// it should not have crosshairs yet
expect(Object.keys(fORMap)).to.have.length(0);
});
cy.get(':nth-child(3) > [data-cy="study-browser-thumbnail"]').dblclick();
cy.get('[data-cy="MPR"]').click();
cy.get('[data-cy="Crosshairs"]').click();
cy.wait(1000);
// check cornerstone to see if each has crosshairs
// we can later do visual testing to match the images with a baseline
cy.window()
.its('cornerstoneTools')
.then(cornerstoneTools => {
const state = cornerstoneTools.annotation.state.getDefaultAnnotationManager();
const fORMap = state.annotations;
const fOR = Object.keys(fORMap)[0];
const fORAnnotation = fORMap[fOR];
// it should have crosshairs as the only key
expect(Object.keys(fORAnnotation)).to.have.length(1);
const crosshairs = fORAnnotation.Crosshairs;
// it should have three
expect(crosshairs).to.have.length(3);
expect(crosshairs[0].data.handles.toolCenter).to.deep.equal(
crosshairs[1].data.handles.toolCenter
);
});
});
});

View File

@ -18,6 +18,7 @@ export function initCornerstoneToolsAliases() {
cy.get('[data-cy="MoreTools-split-button-secondary"]').as('moreBtnSecondary');
cy.get('[data-cy="Layout"]').as('layoutBtn');
cy.get('.cornerstone-viewport-element').as('viewport');
cy.get('[data-cy="MPR"]').as('mprBtn');
}
//Creating aliases for Common page elements
@ -49,9 +50,16 @@ export function initCommonElementsAliases() {
//Creating aliases for Routes
export function initRouteAliases() {
cy.server();
cy.route('GET', '**/series**').as('getStudySeries');
cy.route('GET', '**/studies**').as('getStudies');
cy.intercept('GET', '**/series**', { statusCode: 200, body: [] }).as(
'getStudySeries'
);
// Todo: for some reason cypress does not redirect to the correct url
// so we intercept the request and redirect it to the correct url
cy.intercept('/studies?limit*', req => {
const url = req.url.replace(/\/studies\?/, '/studies/?limit');
req.url = url;
});
}
//Creating aliases for Study List page elements on Desktop experience

View File

@ -5,7 +5,6 @@ import {
initCornerstoneToolsAliases,
initCommonElementsAliases,
initRouteAliases,
initVTKToolsAliases,
initStudyListAliasesOnDesktop,
initStudyListAliasesOnTablet,
initPreferencesModalAliases,
@ -46,7 +45,9 @@ import {
Cypress.Commands.add('openStudy', PatientName => {
cy.openStudyList();
cy.get('#filter-patientNameOrId').type(PatientName);
cy.wait('@getStudies');
// cy.get('@getStudies').then(() => {
cy.wait(1000);
cy.get('[data-cy="study-list-results"]', { timeout: 5000 })
.contains(PatientName)
.first()
@ -56,15 +57,16 @@ Cypress.Commands.add('openStudy', PatientName => {
Cypress.Commands.add('checkStudyRouteInViewer', StudyInstanceUID => {
cy.location('pathname').then($url => {
cy.log($url);
if ($url == 'blank' || !$url.includes(`/viewer/${StudyInstanceUID}`)) {
if ($url == 'blank' || !$url.includes(`/basic-test/${StudyInstanceUID}`)) {
cy.openStudyInViewer(StudyInstanceUID);
cy.waitDicomImage();
cy.wait(2000);
}
});
});
Cypress.Commands.add('openStudyInViewer', StudyInstanceUID => {
cy.visit(`/viewer?StudyInstanceUIDs=${StudyInstanceUID}`);
cy.visit(`/basic-test?StudyInstanceUIDs=${StudyInstanceUID}`);
});
/**
@ -89,16 +91,20 @@ Cypress.Commands.add('openStudyModality', Modality => {
/**
* Command to wait and check if a new page was loaded
*
* @param {string} url - part of the expected url. Default value is /viewer
* @param {string} url - part of the expected url. Default value is /basic-test
*/
Cypress.Commands.add('isPageLoaded', (url = '/viewer') => {
Cypress.Commands.add('isPageLoaded', (url = '/basic-test') => {
return cy.location('pathname', { timeout: 60000 }).should('include', url);
});
Cypress.Commands.add('openStudyList', () => {
cy.initRouteAliases();
cy.visit('/');
cy.wait('@getStudies');
// For some reason cypress 12.x does not like to stub the network request
// so we just wait herer for 1 second
// cy.wait('@getStudies');
cy.wait(1000);
});
Cypress.Commands.add('waitStudyList', () => {
@ -450,6 +456,29 @@ Cypress.Commands.add('openPreferences', () => {
});
});
Cypress.Commands.add('scrollToIndex', index => {
// Workaround implemented based on Cypress issue:
// https://github.com/cypress-io/cypress/issues/1570#issuecomment-450966053
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
'value'
).set;
cy.get('input.imageSlider[type=range]').then($range => {
// get the DOM node
const range = $range[0];
// set the value manually
nativeInputValueSetter.call(range, index);
// now dispatch the event
range.dispatchEvent(
new Event('change', {
value: index,
bubbles: true,
})
);
});
});
Cypress.Commands.add('closePreferences', () => {
cy.log('Close User Preferences Modal');
@ -596,3 +625,14 @@ Cypress.Commands.add('setLanguage', (language, save = true) => {
.click();
});
});
// hide noisy logs
// https://github.com/cypress-io/cypress/issues/7362
// uncomment this if you really need the network logs
const origLog = Cypress.log;
Cypress.log = function(opts, ...other) {
if (opts.displayName === 'script' || opts.name === 'request') {
return;
}
return origLog(opts, ...other);
};

View File

@ -46,7 +46,7 @@
"README.md"
],
"dependencies": {
"@babel/runtime": "7.16.3",
"@babel/runtime": "^7.20.13",
"@ohif/core": "^3.0.0",
"@ohif/extension-cornerstone": "^3.0.0",
"@ohif/extension-cornerstone-dicom-seg": "^3.0.0",
@ -54,8 +54,10 @@
"@ohif/extension-default": "^3.0.0",
"@ohif/extension-dicom-pdf": "^3.0.1",
"@ohif/extension-dicom-video": "^3.0.1",
"@ohif/extension-test": "0.0.1",
"@ohif/i18n": "^1.0.0",
"@ohif/mode-basic-dev-mode": "^3.0.0",
"@ohif/mode-test": "^0.0.1",
"@ohif/mode-longitudinal": "^3.0.0",
"@ohif/ui": "^2.0.0",
"@types/react": "^17.0.38",
@ -82,13 +84,13 @@
"react-dropzone": "^10.1.7",
"react-i18next": "^10.11.0",
"react-resize-detector": "^6.7.6",
"react-router": "^6.3.0",
"react-router-dom": "^6.3.0"
"react-router": "^6.8.1",
"react-router-dom": "^6.8.1"
},
"devDependencies": {
"@babel/plugin-proposal-private-methods": "^7.18.6",
"@percy/cypress": "^3.1.1",
"cypress": "^9.5.4",
"cypress": "^12.6.0",
"cypress-file-upload": "^3.5.3",
"glob": "^8.0.3",
"identity-obj-proxy": "3.0.x",

View File

@ -6,6 +6,8 @@ window.config = {
showStudyList: true,
// below flag is for performance reasons, but it might not work for all servers
omitQuotationForMultipartRequest: true,
showWarningMessageForCrossOrigin: true,
showCPUFallbackMessage: true,
showLoadingIndicator: true,
// filterQueryParam: false,
dataSources: [

View File

@ -12,6 +12,8 @@ window.config = {
maxNumberOfWebWorkers: 3,
// below flag is for performance reasons, but it might not work for all servers
omitQuotationForMultipartRequest: true,
showWarningMessageForCrossOrigin: true,
showCPUFallbackMessage: true,
showLoadingIndicator: true,
maxNumRequests: {
interaction: 100,

View File

@ -4,6 +4,8 @@ window.config = {
showStudyList: true,
// below flag is for performance reasons, but it might not work for all servers
omitQuotationForMultipartRequest: true,
showWarningMessageForCrossOrigin: true,
showCPUFallbackMessage: true,
servers: {
dicomWeb: [
{

View File

@ -6,6 +6,8 @@ window.config = {
showStudyList: true,
// below flag is for performance reasons, but it might not work for all servers
omitQuotationForMultipartRequest: true,
showWarningMessageForCrossOrigin: true,
showCPUFallbackMessage: true,
showLoadingIndicator: true,
// filterQueryParam: false,
dataSources: [

View File

@ -7,6 +7,8 @@ window.config = {
maxNumberOfWebWorkers: 3,
// below flag is for performance reasons, but it might not work for all servers
omitQuotationForMultipartRequest: true,
showWarningMessageForCrossOrigin: true,
showCPUFallbackMessage: true,
showLoadingIndicator: true,
// filterQueryParam: false,
dataSources: [

View File

@ -5,6 +5,8 @@ window.config = {
modes: [],
// below flag is for performance reasons, but it might not work for all servers
omitQuotationForMultipartRequest: true,
showWarningMessageForCrossOrigin: true,
showCPUFallbackMessage: true,
showLoadingIndicator: true,
dataSources: [
{

View File

@ -3,6 +3,8 @@ window.config = {
showStudyList: true,
// below flag is for performance reasons, but it might not work for all servers
omitQuotationForMultipartRequest: true,
showWarningMessageForCrossOrigin: true,
showCPUFallbackMessage: true,
showLoadingIndicator: true,
servers: {
// This is an array, but we'll only use the first entry for now

View File

@ -5,6 +5,8 @@ window.config = {
modes: [],
// below flag is for performance reasons, but it might not work for all servers
omitQuotationForMultipartRequest: true,
showWarningMessageForCrossOrigin: true,
showCPUFallbackMessage: true,
showLoadingIndicator: true,
dataSources: [
{

View File

@ -1,11 +1,12 @@
window.config = {
routerBasename: '/',
// whiteLabelling: {},
extensions: [],
modes: [],
modes: ['@ohif/mode-test'],
showStudyList: true,
// below flag is for performance reasons, but it might not work for all servers
omitQuotationForMultipartRequest: true,
showWarningMessageForCrossOrigin: false,
showCPUFallbackMessage: false,
// filterQueryParam: false,
dataSources: [
{
@ -13,7 +14,9 @@ window.config = {
namespace: '@ohif/extension-default.dataSourcesModule.dicomweb',
sourceName: 'dicomweb',
configuration: {
name: 'DCM4CHEE',
// The most important field to set for static WADO
staticWado: true,
name: 'StaticWADO',
wadoUriRoot: '/viewer-testdata',
qidoRoot: '/viewer-testdata',
wadoRoot: '/viewer-testdata',
@ -24,7 +27,6 @@ window.config = {
enableStudyLazyLoad: true,
supportsFuzzyMatching: false,
supportsWildcard: true,
staticWado: true,
singlepart: 'video,thumbnail,pdf',
},
},

View File

@ -3,6 +3,8 @@ window.config = {
enableGoogleCloudAdapter: false,
// below flag is for performance reasons, but it might not work for all servers
omitQuotationForMultipartRequest: true,
showWarningMessageForCrossOrigin: true,
showCPUFallbackMessage: true,
showLoadingIndicator: true,
// This is an array, but we'll only use the first entry for now
oidc: [

View File

@ -3,6 +3,8 @@ window.config = {
enableGoogleCloudAdapter: true,
// below flag is for performance reasons, but it might not work for all servers
omitQuotationForMultipartRequest: true,
showWarningMessageForCrossOrigin: true,
showCPUFallbackMessage: true,
showLoadingIndicator: true,
servers: {
// This is an array, but we'll only use the first entry for now

View File

@ -5,6 +5,8 @@ window.config = {
modes: [],
// below flag is for performance reasons, but it might not work for all servers
omitQuotationForMultipartRequest: true,
showWarningMessageForCrossOrigin: true,
showCPUFallbackMessage: true,
showLoadingIndicator: true,
dataSources: [
{

View File

@ -6,6 +6,8 @@ window.config = {
showStudyList: true,
maxNumberOfWebWorkers: 3,
showLoadingIndicator: true,
showWarningMessageForCrossOrigin: true,
showCPUFallbackMessage: true,
// filterQueryParam: false,
dataSources: [
{

View File

@ -9,6 +9,8 @@ window.config = {
maxNumberOfWebWorkers: 4,
// below flag is for performance reasons, but it might not work for all servers
omitQuotationForMultipartRequest: true,
showWarningMessageForCrossOrigin: true,
showCPUFallbackMessage: true,
showLoadingIndicator: true,
// filterQueryParam: false,
dataSources: [

View File

@ -14,6 +14,8 @@ window.config = {
maxNumberOfWebWorkers: 4,
// below flag is for performance reasons, but it might not work for all servers
omitQuotationForMultipartRequest: true,
showWarningMessageForCrossOrigin: true,
showCPUFallbackMessage: true,
showLoadingIndicator: true,
// filterQueryParam: false,
dataSources: [

View File

@ -6,6 +6,8 @@ window.config = {
showStudyList: true,
// below flag is for performance reasons, but it might not work for all servers
omitQuotationForMultipartRequest: true,
showWarningMessageForCrossOrigin: true,
showCPUFallbackMessage: true,
showLoadingIndicator: true,
// filterQueryParam: false,
dataSources: [

View File

@ -3,6 +3,8 @@ window.config = {
showStudyList: true,
// below flag is for performance reasons, but it might not work for all servers
omitQuotationForMultipartRequest: true,
showWarningMessageForCrossOrigin: true,
showCPUFallbackMessage: true,
showLoadingIndicator: true,
servers: {
dicomWeb: [

View File

@ -96,6 +96,11 @@ async function appInit(appConfigOrFunc, defaultExtensions, defaultModes) {
appConfig.modes.push(mode);
}
// remove modes that are not objects, or have no id
appConfig.modes = appConfig.modes.filter(
mode => typeof mode === 'object' && mode.id
);
return {
appConfig,
commandsManager,

View File

@ -1,4 +1,5 @@
import useDebounce from './useDebounce.js';
import useQuery from './useQuery.js';
import useSearchParams from './useSearchParams.js';
export { useDebounce, useQuery };
export { useDebounce, useQuery, useSearchParams };

View File

@ -0,0 +1,17 @@
import useQuery from './useQuery';
/**
* It returns a Map of the query parameters in the URL, where the keys are
* lowercase
* @returns A function that returns a Map of the query parameters.
*/
export default function useSearchParams() {
const query = useQuery();
// make query params case-insensitive
const searchParams = new Map();
for (const [key, value] of query) {
searchParams.set(key.toLowerCase(), value);
}
return searchParams;
}

View File

@ -5,7 +5,6 @@ import 'regenerator-runtime/runtime';
import App from './App';
import React from 'react';
import ReactDOM from 'react-dom';
/**
* EXTENSIONS AND MODES
* =================
@ -15,21 +14,23 @@ import ReactDOM from 'react-dom';
* pluginImports.js imports all of the modes and extensions and adds them
* to the window for processing.
*/
import loadDynamicImports from './pluginImports.js';
import loadDynamicImports, { loadRuntimeImports } from './pluginImports.js';
loadDynamicImports().then(() => {
/**
* Combine our appConfiguration with installed extensions and modes.
* In the future appConfiguration may contain modes added at runtime.
* */
const appProps = {
config: window ? window.config : {},
defaultExtensions: window.extensions,
defaultModes: window.modes,
};
loadRuntimeImports(window.config).then(() => {
/**
* Combine our appConfiguration with installed extensions and modes.
* In the future appConfiguration may contain modes added at runtime.
* */
const appProps = {
config: window ? window.config : {},
defaultExtensions: window.extensions,
defaultModes: window.modes,
};
/** Create App */
const app = React.createElement(App, appProps, null);
/** Render */
ReactDOM.render(app, document.getElementById('root'));
/** Create App */
const app = React.createElement(App, appProps, null);
/** Render */
ReactDOM.render(app, document.getElementById('root'));
});
});

View File

@ -3,9 +3,9 @@ import { useParams, useLocation } from 'react-router';
import PropTypes from 'prop-types';
// TODO: DicomMetadataStore should be injected?
import { DicomMetadataStore } from '@ohif/core';
import { DicomMetadataStore, ServicesManager } from '@ohif/core';
import { DragAndDropProvider, ImageViewerProvider } from '@ohif/ui';
import { useQuery } from '@hooks';
import { useQuery, useSearchParams } from '@hooks';
import ViewportGrid from '@components/ViewportGrid';
import Compose from './Compose';
@ -20,7 +20,7 @@ import Compose from './Compose';
*/
function defaultRouteInit(
{ servicesManager, studyInstanceUIDs, dataSource, filters },
hangingProtocol
hangingProtocolId
) {
const {
displaySetService,
@ -86,7 +86,7 @@ function defaultRouteInit(
// hanging protocol in the mode configuration
hangingProtocolService.run(
{ studies, activeStudy, displaySets },
hangingProtocol
hangingProtocolId
);
});
@ -105,7 +105,9 @@ export default function ModeRoute({
const location = useLocation();
const query = useQuery();
const params = useParams();
const searchParams = useSearchParams();
const runTimeHangingProtocolId = searchParams.get('hangingprotocolid');
const [studyInstanceUIDs, setStudyInstanceUIDs] = useState();
const [refresh, setRefresh] = useState(false);
@ -121,7 +123,7 @@ export default function ModeRoute({
const {
displaySetService,
hangingProtocolService,
} = servicesManager.services;
} = (servicesManager as ServicesManager).services;
const { extensions, sopClassHandlers, hotkeys, hangingProtocol } = mode;
@ -249,22 +251,36 @@ export default function ModeRoute({
extensionManager,
commandsManager,
});
// use the URL hangingProtocolId if it exists, otherwise use the one
// defined in the mode configuration
const hangingProtocolIdToUse = hangingProtocolService.getProtocolById(
runTimeHangingProtocolId
)
? runTimeHangingProtocolId
: hangingProtocol;
// Sets the active hanging protocols - if hangingProtocol is undefined,
// resets to default. Done before the onModeEnter to allow the onModeEnter
// to perform custom hanging protocol actions
hangingProtocolService.setActiveProtocols(hangingProtocol);
mode?.onModeEnter({ servicesManager, extensionManager, commandsManager });
hangingProtocolService.setActiveProtocolIds(hangingProtocolIdToUse);
mode?.onModeEnter({
servicesManager,
extensionManager,
commandsManager,
});
const setupRouteInit = async () => {
/**
* The next line should get all the query parameters provided by the URL
* - except the StudyInstaceUIDs - and create an object called filters
* - except the StudyInstanceUIDs - and create an object called filters
* used to filtering the study as the user wants otherwise it will return
* a empty object.
*
* Example:
* const filters = {
* seriesInstaceUID: 1.2.276.0.7230010.3.1.3.1791068887.5412.1620253993.114611
* seriesInstanceUID: 1.2.276.0.7230010.3.1.3.1791068887.5412.1620253993.114611
* }
*/
const filters =
@ -272,7 +288,10 @@ export default function ModeRoute({
(acc: Record<string, string>, val: string) => {
if (val !== 'StudyInstanceUIDs') {
if (['seriesInstanceUID', 'SeriesInstanceUID'].includes(val)) {
return { ...acc, seriesInstanceUID: query.get(val) };
return {
...acc,
seriesInstanceUID: query.get(val),
};
}
return { ...acc, [val]: query.get(val) };
@ -291,7 +310,7 @@ export default function ModeRoute({
dataSource,
filters,
},
hangingProtocol
hangingProtocolIdToUse
);
}
@ -302,7 +321,7 @@ export default function ModeRoute({
dataSource,
filters,
},
hangingProtocol
hangingProtocolIdToUse
);
};
@ -316,7 +335,10 @@ export default function ModeRoute({
// information, and must be in a try/catch to ensure subscriptions
// are unsubscribed.
try {
mode?.onModeExit?.({ servicesManager, extensionManager });
mode?.onModeExit?.({
servicesManager,
extensionManager,
});
} catch (e) {
console.warn('mode exit failure', e);
}

View File

@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next';
//
import filtersMeta from './filtersMeta.js';
import { useAppConfig } from '@state';
import { useDebounce, useQuery } from '@hooks';
import { useDebounce, useSearchParams } from '@hooks';
import { utils, hotkeys } from '@ohif/core';
import {
@ -54,10 +54,10 @@ function WorkList({
// ~ Modes
const [appConfig] = useAppConfig();
// ~ Filters
const query = useQuery();
const searchParams = useSearchParams();
const navigate = useNavigate();
const STUDIES_LIMIT = 101;
const queryFilterValues = _getQueryFilterValues(query);
const queryFilterValues = _getQueryFilterValues(searchParams);
const [filterValues, _setFilterValues] = useState({
...defaultFilterValues,
...queryFilterValues,
@ -503,7 +503,7 @@ const defaultFilterValues = {
function _tryParseInt(str, defaultValue) {
let retValue = defaultValue;
if (str !== null) {
if (str != null) {
if (str.length > 0) {
if (!isNaN(str)) {
retValue = parseInt(str);
@ -513,24 +513,24 @@ function _tryParseInt(str, defaultValue) {
return retValue;
}
function _getQueryFilterValues(query) {
function _getQueryFilterValues(params) {
const queryFilterValues = {
patientName: query.get('patientName'),
mrn: query.get('mrn'),
patientName: params.get('patientname'),
mrn: params.get('mrn'),
studyDate: {
startDate: query.get('startDate'),
endDate: query.get('endDate'),
startDate: params.get('startdate') || null,
endDate: params.get('enddate') || null,
},
description: query.get('description'),
modalities: query.get('modalities')
? query.get('modalities').split(',')
description: params.get('description'),
modalities: params.get('modalities')
? params.get('modalities').split(',')
: [],
accession: query.get('accession'),
sortBy: query.get('sortBy'),
sortDirection: query.get('sortDirection'),
pageNumber: _tryParseInt(query.get('pageNumber'), undefined),
resultsPerPage: _tryParseInt(query.get('resultsPerPage'), undefined),
datasources: query.get('datasources'),
accession: params.get('accession'),
sortBy: params.get('sortby'),
sortDirection: params.get('sortdirection'),
pageNumber: _tryParseInt(params.get('pagenumber'), undefined),
resultsPerPage: _tryParseInt(params.get('resultsperpage'), undefined),
datasources: params.get('datasources'),
};
// Delete null/undefined keys

@ -1 +1 @@
Subproject commit 1bca96aa2aeae7d90b3c8db73845c85dd27b34d5
Subproject commit 4d59660c2883ed749a680e5fb6d4624ab54c9422

View File

@ -3,7 +3,8 @@
"baseUrl": "./",
"checkJs": true,
// Ensure that Babel can safely transpile files in the TypeScript project
"isolatedModules": false,
"isolatedModules": true,
"emitDeclarationOnly": true,
"jsx": "react",
"resolveJsonModule": true,
"esModuleInterop": true,
@ -12,14 +13,16 @@
"moduleResolution": "node",
"outDir": "./dist",
"paths": {
"@ohif/core": [
"platform/core/src"
],
"@ohif/core": ["platform/core/src"],
"@ohif/ui": ["platform/ui/src"],
"@ohif/i18n": ["platform/i18n/src"],
"@ohif/viewer": ["platform/viewer/src"]
}
},
"include": ["platform/**/src/**/*", "extensions/**/src/**/*", "modes/**/src/**/*"],
"include": [
"platform/**/src/**/*",
"extensions/**/src/**/*",
"modes/**/src/**/*"
],
"exclude": ["node_modules", "dist"]
}

11416
yarn.lock

File diff suppressed because it is too large Load Diff