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 authored and GitHub committed 2023-02-17 15:04:07 -05:00
1 parent ae4183852f
commit 926290f69e
83 files changed
+6286 -6860

No files matched your search

+8 -8
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,
},
@@ -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,
+40
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,
},
},
});
-15
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
}
}
+6
View File
@@ -0,0 +1,6 @@
module.exports = {
plugins: ['cypress'],
env: {
'cypress/globals': true,
},
};
@@ -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);
});
});
@@ -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);
@@ -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
@@ -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', () => {
@@ -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
@@ -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');
});
*/
});
@@ -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');
});*/
});
@@ -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);
});
});
@@ -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');
});*/
});
@@ -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];
@@ -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
);
});
});
});
+11 -3
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
+47 -7
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);
};
+6 -4
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",
+2
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: [
+2
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,
+2
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: [
{
@@ -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: [
@@ -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: [
@@ -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: [
{
@@ -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
@@ -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: [
{
+6 -4
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',
},
},
+2
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: [
+2
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
@@ -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: [
{
@@ -6,6 +6,8 @@ window.config = {
showStudyList: true,
maxNumberOfWebWorkers: 3,
showLoadingIndicator: true,
showWarningMessageForCrossOrigin: true,
showCPUFallbackMessage: true,
// filterQueryParam: false,
dataSources: [
{
@@ -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: [
@@ -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: [
+2
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: [
@@ -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: [
+5
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,
+2 -1
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 };
@@ -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;
}
+16 -15
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'));
});
});
+35 -13
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);
}
@@ -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