Feat/v2 e2e static tests (#2684)

Use the static data for hte e2e tests
This commit is contained in:
Bill Wallace 2022-01-28 20:02:30 -05:00 committed by GitHub
parent c3ce909434
commit 33307d3cd2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1377 additions and 245 deletions

View File

@ -25,11 +25,9 @@ executors:
docker:
# Primary container image where all steps run.
- image: 'cypress/browsers:node14.15.0-chrome86-ff82'
- image: 'ohif/viewer-testdata:0.1-test'
defaults: &defaults
docker:
- image: circleci/node:14.15.0
- image: cimg/node:14.18
environment:
TERM: xterm # Enable colors in term
QUICK_BUILD: true
@ -349,7 +347,8 @@ workflows:
store_artifacts: true
working_directory: platform/viewer
build:
npx cross-env QUICK_BUILD=true APP_CONFIG=config/dicomweb-server.js
git submodule update --init &&
npx cross-env QUICK_BUILD=true APP_CONFIG=config/e2e.js
yarn run build
start: yarn run test:e2e:serve
spec: 'cypress/integration/common/**/*,cypress/integration/pwa/**/*'
@ -377,7 +376,8 @@ workflows:
store_artifacts: true
working_directory: platform/viewer
build:
npx cross-env QUICK_BUILD=true APP_CONFIG=config/dicomweb-server.js
git submodule update --init &&
npx cross-env QUICK_BUILD=true APP_CONFIG=config/e2e.js
yarn run build:package
start: yarn run test:e2e:serve
spec: 'cypress/integration/common/**/*,cypress/integration/script-tag/**/*'
@ -410,7 +410,8 @@ workflows:
store_artifacts: false
working_directory: platform/viewer
build:
npx cross-env QUICK_BUILD=true APP_CONFIG=config/dicomweb-server.js
git submodule update --init &&
npx cross-env QUICK_BUILD=true APP_CONFIG=config/e2e.js
yarn run build
# start server --> verify running --> percy + chrome + cypress
command: yarn run test:e2e:dist
@ -489,7 +490,8 @@ workflows:
store_artifacts: false
working_directory: platform/viewer
build:
npx cross-env QUICK_BUILD=true APP_CONFIG=config/dicomweb-server.js
git submodule update --init &&
npx cross-env QUICK_BUILD=true APP_CONFIG=config/e2e.js
yarn run build
# start server --> verify running --> percy + chrome + cypress
command: yarn run test:e2e:dist

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "testdata"]
path = testdata
url = https://github.com/OHIF/viewer-testdata-dicomweb.git

1
.node-version Normal file
View File

@ -0,0 +1 @@
14.18.0

View File

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

View File

@ -33,6 +33,7 @@ class DicomPDFViewport extends Component {
static propTypes = {
byteArray: TypedArrayProp.uint8,
rawPdf: PropTypes.bool,
useNative: PropTypes.bool,
viewportData: PropTypes.object,
activeViewportIndex: PropTypes.number,
@ -45,7 +46,8 @@ class DicomPDFViewport extends Component {
};
async componentDidMount() {
const dataSet = this.parseByteArray(this.props.byteArray);
const { rawPdf } = this.props
const dataSet = !rawPdf && this.parseByteArray(this.props.byteArray);
const fileURL = this.getPDFFileUrl(dataSet, this.props.byteArray);
this.setState(state => ({ ...state, fileURL }));

View File

@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import OHIF from '@ohif/core';
import OHIFComponentPlugin from './OHIFComponentPlugin.js';
import DicomPDFViewport from './DicomPDFViewport';
import { str2ab } from '@ohif/core';
const { DicomLoaderService } = OHIF.utils;
@ -18,6 +19,7 @@ class OHIFDicomPDFViewport extends Component {
state = {
byteArray: null,
rawPdf: false,
error: null,
};
@ -34,6 +36,15 @@ class OHIFDicomPDFViewport extends Component {
componentDidMount() {
const { displaySet, studies } = this.props.viewportData;
if (displaySet.metadata && displaySet.metadata.EncapsulatedDocument) {
const { InlineBinary, BulkDataURI } = displaySet.metadata.EncapsulatedDocument;
if (InlineBinary) {
const inlineBinaryData = atob(InlineBinary);
const byteArray = str2ab(inlineBinaryData);
this.setState({ byteArray, rawPdf: true });
return;
}
}
DicomLoaderService.findDicomDataPromise(displaySet, studies).then(
data => this.setState({ byteArray: new Uint8Array(data) }),
error => {
@ -49,15 +60,16 @@ class OHIFDicomPDFViewport extends Component {
viewportIndex,
activeViewportIndex,
} = this.props;
const { byteArray, error } = this.state;
const { byteArray, error, rawPdf } = this.state;
const { id, init, destroy } = OHIFDicomPDFViewport;
const pluginProps = { id, init, destroy };
return (
<OHIFComponentPlugin {...pluginProps}>
{byteArray && (
{(byteArray) && (
<DicomPDFViewport
byteArray={byteArray}
rawPdf={rawPdf}
setViewportActive={setViewportActive}
viewportIndex={viewportIndex}
activeViewportIndex={activeViewportIndex}

View File

@ -56,6 +56,7 @@
"@babel/preset-env": "^7.5.0",
"@babel/preset-react": "^7.0.0",
"@percy/cypress": "^2.2.0",
"@webpack-cli/serve": "^1.6.1",
"babel-eslint": "9.x",
"babel-loader": "^8.0.6",
"babel-plugin-inline-react-svg": "1.1.0",
@ -104,8 +105,8 @@
"stylus-loader": "^3.0.2",
"terser-webpack-plugin": "^2.1.0",
"webpack": "^4.35.2",
"webpack-cli": "^3.3.5",
"webpack-dev-server": "^3.7.2",
"webpack-cli": "^4.9.2",
"webpack-dev-server": "^4.7.3",
"webpack-hot-middleware": "^2.25.0",
"webpack-merge": "^4.2.1",
"workbox-webpack-plugin": "^5.0.0-beta.1",
@ -123,6 +124,7 @@
]
},
"resolutions": {
"**/node-gyp": "8.4.1",
"**/@babel/runtime": "7.5.5"
}
}

View File

@ -20,6 +20,7 @@ import ui from './ui';
import user from './user.js';
import errorHandler from './errorHandler.js';
import utils, { hotkeys } from './utils/';
import str2ab from './utils/str2ab';
import {
UINotificationService,
@ -96,6 +97,8 @@ export {
UIDialogService,
MeasurementService,
LoggerService,
str2ab,
};
export { OHIF };

View File

@ -26,6 +26,7 @@ describe('Top level exports', () => {
'cornerstone',
'default', //
'errorHandler',
'str2ab',
'string',
'ui',
'user',

View File

@ -9,38 +9,27 @@ import { api } from 'dicomweb-client';
*/
export default class StaticWadoClient extends api.DICOMwebClient {
static filterKeys = {
StudyInstanceUID: '0020000D',
PatientName: '00100010',
PatientID: '00100020',
StudyDescription: '00081030',
ModalitiesInStudy: '00080061',
"StudyInstanceUID": "0020000D",
"PatientName": "00100010",
"00100020": "mrn",
"PatientID": "00100020",
"StudyDescription": "00081030",
"StudyDate": "00080020",
"ModalitiesInStudy": "00080061",
AccessionNumber: "00080050",
};
constructor(qidoConfig) {
super(qidoConfig);
this.staticWado = qidoConfig.staticWado;
this.extendMetadataWithInstances = qidoConfig.extendMetadataWithInstances;
}
async retrieveSeriesMetadata(options) {
if (!this.extendMetadataWithInstances)
return super.retrieveSeriesMetadata(options);
const results = await Promise.all([
super.retrieveSeriesMetadata(options),
this.searchForInstances(options),
]);
const metadata = results[0];
const instances = results[1];
return metadata.map(item => {
const sopUID = item['00080018'].Value[0];
const instance = instances.find(
instance => instance['00080018'].Value[0] == sopUID
);
Object.assign(item, instance);
return item;
});
}
/**
* Replace the search for studies remote query with a local version which
* retrieves a complete query list and then sub-selects from it locally.
* @param {*} options
* @returns
*/
async searchForStudies(options) {
if (!this.staticWado) return super.searchForStudies(options);
@ -56,6 +45,62 @@ export default class StaticWadoClient extends api.DICOMwebClient {
return filtered;
}
/**
* Compares values, matching any instance of desired to any instance of
* actual by recursively go through the paired set of values. That is,
* this is O(m*n) where m is how many items in desired and n is the length of actual
* Then, at the individual item node, compares the Alphabetic name if present,
* and does a sub-string matching on string values, and otherwise does an
* exact match comparison.
*
* @param {*} desired
* @param {*} actual
* @returns true if the values match
*/
compareValues(desired, actual) {
if (Array.isArray(desired)) {
return desired.find(item => this.compareValues(item, actual));
}
if (Array.isArray(actual)) {
return actual.find(actualItem => this.compareValues(desired, actualItem));
}
if (actual && actual.Alphabetic) {
actual = actual.Alphabetic;
}
if (typeof (actual) == 'string') {
if (actual.length === 0) return true;
if (desired.length === 0 || desired === '*') return true;
if (desired[0] === '*' && desired[desired.length - 1] === '*') {
console.log(`Comparing ${actual} to ${desired.substring(1, desired.length - 1)}`)
return actual.indexOf(desired.substring(1, desired.length - 1)) != -1;
} else if (desired[desired.length - 1] === '*') {
return actual.indexOf(desired.substring(0, desired.length - 1)) != -1;
} else if (desired[0] === '*') {
return actual.indexOf(desired.substring(1)) === actual.length - desired.length + 1;
}
}
return desired === actual;
}
/** Compares a pair of dates to see if the value is within the range */
compareDateRange(range, value) {
if (!value) return true;
const dash = range.indexOf('-');
if (dash === -1) return this.compareValues(range, value);
const start = range.substring(0, dash);
const end = range.substring(dash + 1);
return (!start || value >= start) &&
(!end || value <= end);
}
/**
* Filters the return list by the query parameters.
*
* @param {*} key
* @param {*} queryParams
* @param {*} study
* @returns
*/
filterItem(key, queryParams, study) {
const altKey = StaticWadoClient.filterKeys[key] || key;
if (!queryParams) return true;
@ -63,23 +108,8 @@ export default class StaticWadoClient extends api.DICOMwebClient {
if (!testValue) return true;
const valueElem = study[key] || study[altKey];
if (!valueElem) return false;
if (valueElem.vr == 'DA') return this.compareDateRange(testValue, valueElem.Value[0]);
const value = valueElem.Value;
return this.eqItem(testValue, value);
}
eqItem(testValue, value) {
if (testValue.filter) {
return (
testValue.filter(testItem => this.eqItem(testItem, value)).length > 0
);
}
if (value === testValue) return true;
if (typeof value == 'string') {
return value.indexOf(testValue) != -1;
}
if (value.Alphabetic) return this.eqItem(testValue, value.Alphabetic);
if (value.filter)
return value.filter(item => this.eqItem(testValue, item)).length > 0;
return false;
return this.compareValues(testValue, value) && true;
}
}

View File

@ -26,6 +26,16 @@ const PROXY_TARGET = process.env.PROXY_TARGET;
const PROXY_DOMAIN = process.env.PROXY_DOMAIN;
const ENTRY_TARGET = process.env.ENTRY_TARGET || `${SRC_DIR}/index.js`;
const setHeaders = (res, path) => {
res.setHeader('Content-Type', 'text/plain')
if (path.indexOf('.gz') !== -1) {
res.setHeader('Content-Encoding', 'gzip')
} else if (path.indexOf('.br') !== -1) {
res.setHeader('Content-Encoding', 'br')
}
}
module.exports = (env, argv) => {
const baseConfig = webpackBase(env, argv, { SRC_DIR, DIST_DIR });
const isProdBuild = process.env.NODE_ENV === 'production';
@ -134,11 +144,40 @@ module.exports = (env, argv) => {
hot: true,
open: true,
port: 3000,
host: '0.0.0.0',
public: 'http://localhost:' + 3000,
client: {
overlay: { errors: true, warnings: false },
},
'static': [
{
directory: path.join(require('os').homedir(), 'dicomweb'),
staticOptions: {
extensions: ['gz', 'br'],
index: "index.json.gz",
redirect: true,
setHeaders,
},
publicPath: '/dicomweb',
},
{
directory: '../../testdata',
staticOptions: {
extensions: ['gz', 'br'],
index: "index.json.gz",
redirect: true,
setHeaders,
},
publicPath: '/testdata',
},
],
//public: 'http://localhost:' + 3000,
//writeToDisk: true,
historyApiFallback: {
disableDotRule: true,
},
headers: {
'Cross-Origin-Embedder-Policy': 'require-corp',
'Cross-Origin-Opener-Policy': 'same-origin',
},
},
});

View File

@ -1,4 +1,5 @@
import '@percy/cypress';
import { enable } from 'cornerstone-core';
import 'cypress-file-upload';
import { DragSimulator } from '../helpers/DragSimulator.js';
import {
@ -199,6 +200,11 @@ Cypress.Commands.add('waitDicomImage', (timeout = 50000) => {
.its('cornerstone')
.then({ timeout }, $cornerstone => {
return new Cypress.Promise(resolve => {
const onEnabled = enabledEvt => {
const element = enabledEvt.detail.element;
element.addEventListener('cornerstoneimagerendered', onEvent);
};
const onEvent = renderedEvt => {
const element = renderedEvt.detail.element;
@ -209,15 +215,17 @@ Cypress.Commands.add('waitDicomImage', (timeout = 50000) => {
);
resolve();
};
const onEnabled = enabledEvt => {
const element = enabledEvt.detail.element;
element.addEventListener('cornerstoneimagerendered', onEvent);
};
$cornerstone.events.addEventListener(
'cornerstoneelementenabled',
onEnabled
);
const enabledElements = $cornerstone.getEnabledElements();
if (enabledElements && enabledElements.length && !enabledElements[0].invalid) {
// Sometimes the page finishes rendering before this gets run,
// if so, just resolve immediately.
resolve();
} else {
$cornerstone.events.addEventListener(
'cornerstoneelementenabled',
onEnabled
);
}
});
});
}

View File

@ -22,8 +22,8 @@
"build:viewer:ci": "cross-env NODE_ENV=production PUBLIC_URL=/pwa/ APP_CONFIG=config/netlify.js QUICK_BUILD=true node --max_old_space_size=4096 ./../../node_modules/webpack/bin/webpack.js --config .webpack/webpack.pwa.js",
"build:viewer:demo": "cross-env NODE_ENV=production APP_CONFIG=config/demo.js HTML_TEMPLATE=rollbar.html QUICK_BUILD=true node --max_old_space_size=4096 ./../../node_modules/webpack/bin/webpack.js --progress --config .webpack/webpack.pwa.js",
"build:viewer:package": "yarn run build:package",
"dev": "cross-env NODE_ENV=development webpack-dev-server --config .webpack/webpack.pwa.js --watch",
"dev:orthanc": "cross-env NODE_ENV=development PROXY_TARGET=/dicom-web PROXY_DOMAIN=http://localhost:8042 APP_CONFIG=config/docker_nginx-orthanc.js webpack-dev-server --config .webpack/webpack.pwa.js --watch",
"dev": "cross-env NODE_ENV=development webpack-dev-server --config .webpack/webpack.pwa.js",
"dev:orthanc": "cross-env NODE_ENV=development PROXY_TARGET=/dicom-web PROXY_DOMAIN=http://localhost:8042 APP_CONFIG=config/docker_nginx-orthanc.js webpack-dev-server --config .webpack/webpack.pwa.js",
"dev:viewer": "yarn run dev",
"start": "yarn run dev",
"test:e2e": "cypress open",
@ -31,7 +31,7 @@
"test:e2e:script-tag": "cypress run --config video=false --browser chrome --spec 'cypress/integration/common/**/*,cypress/integration/script-tag/**/*'",
"test:e2e:local": "cypress run --config video=false --browser chrome --spec 'cypress/integration/common/**/*,cypress/integration/pwa/**/*'",
"test:e2e:dist": "start-server-and-test test:e2e:serve http://localhost:3000 test:e2e:ci",
"test:e2e:serve": "serve -n -l 3000 -s dist",
"test:e2e:serve": "cross-env APP_CONFIG=config/e2e.js yarn start",
"test:unit": "jest --watchAll",
"test:unit:ci": "jest --ci --runInBand --collectCoverage",
"prepublishOnly": "yarn run build:package",

View File

@ -0,0 +1,116 @@
window.config = {
routerBasename: '/',
showStudyList: true,
servers: {
dicomWeb: [
{
name: 'LocalStatic',
wadoUriRoot: '/testdata',
qidoRoot: '/testdata',
wadoRoot: '/testdata',
qidoSupportsIncludeField: false,
imageRendering: 'wadors',
enableStudyLazyLoad: true,
supportsFuzzyMatching: false,
staticWado: true,
},
],
},
// Extensions should be able to suggest default values for these?
// Or we can require that these be explicitly set
hotkeys: [
// ~ Global
{
commandName: 'incrementActiveViewport',
label: 'Next Viewport',
keys: ['right'],
},
{
commandName: 'decrementActiveViewport',
label: 'Previous Viewport',
keys: ['left'],
},
// Supported Keys: https://craig.is/killing/mice
// ~ Cornerstone Extension
{ commandName: 'rotateViewportCW', label: 'Rotate Right', keys: ['r'] },
{ commandName: 'rotateViewportCCW', label: 'Rotate Left', keys: ['l'] },
{ commandName: 'invertViewport', label: 'Invert', keys: ['i'] },
{
commandName: 'flipViewportVertical',
label: 'Flip Horizontally',
keys: ['h'],
},
{
commandName: 'flipViewportHorizontal',
label: 'Flip Vertically',
keys: ['v'],
},
{ commandName: 'scaleUpViewport', label: 'Zoom In', keys: ['+'] },
{ commandName: 'scaleDownViewport', label: 'Zoom Out', keys: ['-'] },
{ commandName: 'fitViewportToWindow', label: 'Zoom to Fit', keys: ['='] },
{ commandName: 'resetViewport', label: 'Reset', keys: ['space'] },
// clearAnnotations
{ commandName: 'nextImage', label: 'Next Image', keys: ['down'] },
{ commandName: 'previousImage', label: 'Previous Image', keys: ['up'] },
// firstImage
// lastImage
{
commandName: 'previousViewportDisplaySet',
label: 'Previous Series',
keys: ['pagedown'],
},
{
commandName: 'nextViewportDisplaySet',
label: 'Next Series',
keys: ['pageup'],
},
// ~ Cornerstone Tools
{ commandName: 'setZoomTool', label: 'Zoom', keys: ['z'] },
// ~ Window level presets
{
commandName: 'windowLevelPreset1',
label: 'W/L Preset 1',
keys: ['1'],
},
{
commandName: 'windowLevelPreset2',
label: 'W/L Preset 2',
keys: ['2'],
},
{
commandName: 'windowLevelPreset3',
label: 'W/L Preset 3',
keys: ['3'],
},
{
commandName: 'windowLevelPreset4',
label: 'W/L Preset 4',
keys: ['4'],
},
{
commandName: 'windowLevelPreset5',
label: 'W/L Preset 5',
keys: ['5'],
},
{
commandName: 'windowLevelPreset6',
label: 'W/L Preset 6',
keys: ['6'],
},
{
commandName: 'windowLevelPreset7',
label: 'W/L Preset 7',
keys: ['7'],
},
{
commandName: 'windowLevelPreset8',
label: 'W/L Preset 8',
keys: ['8'],
},
{
commandName: 'windowLevelPreset9',
label: 'W/L Preset 9',
keys: ['9'],
},
],
};

View File

@ -5,9 +5,9 @@ window.config = {
dicomWeb: [
{
name: 'LocalStatic',
wadoUriRoot: 'http://localhost:5000/',
qidoRoot: 'http://localhost:5000',
wadoRoot: 'http://localhost:5000/',
wadoUriRoot: '/dicomweb',
qidoRoot: '/dicomweb',
wadoRoot: '/dicomweb',
qidoSupportsIncludeField: false,
imageRendering: 'wadors',
thumbnailRendering: 'thumbnail',

1
runtime.txt Normal file
View File

@ -0,0 +1 @@
3.7

1
testdata Submodule

@ -0,0 +1 @@
Subproject commit 47d51e15a0d41c02f806570d8cf78ab39e782387

1257
yarn.lock

File diff suppressed because it is too large Load Diff