Feat #1130 [WIP] Adding support for downloading and zipping files (#1501)

* Feat #1130 Adding support for downloading and zipping files

@ This is a combination of 12 commits.
 - Feat #1130 Initial Commit
 - Feat #1130 Adding support for hierachical lists
 - Feat #1130 [WIP] Adding support for downloading and zipping files
 - Feat #1130 Adding support for hierarchical folder structure study, series, instances on generated zip file
 - Feat #1130 Adding unit tests and addressing code review feedbacks
 - Feat #1130 POC (adding module to support on progress handling)
 - Feat #1130 Improving logic from progress tracking utils
 - Feat #1130 Renaming module with progress tracking utils
 - Feat #1130 Beta version of Progress Tracking utils
 - Feat #1130 Adding unit tests for Progress Tracking Utils
 - Feat #1130 Documenting public methods from progress tracking utils module
 - Feat #1130 Adding support for download and zip progress reporting

* Feat #1130 Addressing code review feedback

* Feat #1130 Preventing multiple downloads from executing at once

* Feat #1130 Improving error message for concurrent downloads

* Remove downloadAndZipSeriesOnViewports

Co-authored-by: James Petts <jamesapetts@gmail.com>
This commit is contained in:
Emanuel Fiuza de OliveiraandJames Petts authored and GitHub committed 2020-04-15 18:57:57 +02:00
1 parent 2412c08595
commit 29d4955980
26 files changed
+1665

No files matched your search

+66
View File
@@ -0,0 +1,66 @@
export default class Queue {
constructor(limit) {
this.limit = limit;
this.size = 0;
this.awaiting = null;
}
/**
* Creates a new "proxy" function associated with the current execution queue
* instance. When the returned function is invoked, the queue limit is checked
* to make sure the limit of scheduled tasks is repected (throwing an
* exception when the limit has been reached and before calling the original
* function). The original function is only invoked after all the previously
* scheduled tasks have finished executing (their returned promises have
* resolved/rejected);
*
* @param {function} task The function whose execution will be associated
* with the current Queue instance;
* @returns {function} The "proxy" function bound to the current Queue
* instance;
*/
bind(task) {
return bind(this, task);
}
bindSafe(task, onError) {
const boundTask = bind(this, task);
return async function safeTask(...args) {
try {
return await boundTask(...args);
} catch (e) {
onError(e);
}
};
}
}
/**
* Utils
*/
function bind(queue, task) {
const cleaner = clean.bind(null, queue);
return async function boundTask(...args) {
if (queue.size >= queue.limit) {
throw new Error('Queue limit reached');
}
const promise = chain(queue.awaiting, task, args);
queue.awaiting = promise.then(cleaner, cleaner);
queue.size++;
return promise;
};
}
function clean(queue) {
if (queue.size > 0 && --queue.size === 0) {
queue.awaiting = null;
}
}
async function chain(prev, task, args) {
await prev;
return task(...args);
}
+66
View File
@@ -0,0 +1,66 @@
import makeDeferred from './makeDeferred';
import Queue from './Queue';
/**
* Utils
*/
function timeout(delay) {
const { resolve, promise } = makeDeferred();
setTimeout(() => void resolve(Date.now()), delay);
return promise;
}
/**
* Tests
*/
describe('Queue', () => {
it('should bind functions to the queue', async () => {
const queue = new Queue(2);
const mockedTimeout = jest.fn(timeout);
const timer = queue.bind(mockedTimeout);
const start = Date.now();
timer(120).then(now => {
const elapsed = now - start;
expect(elapsed >= 120 && elapsed < 240).toBe(true);
});
const end = await timer(120);
expect(end - start > 240).toBe(true);
expect(mockedTimeout).toBeCalledTimes(2);
});
it('should prevent task execution when queue limit is reached', async () => {
const queue = new Queue(1);
const mockedTimeout = jest.fn(timeout);
const timer = queue.bind(mockedTimeout);
const start = Date.now();
const promise = timer(120).then(time => time - start);
try {
await timer(120);
} catch (e) {
expect(Date.now() - start < 120).toBe(true);
expect(e.message).toBe('Queue limit reached');
}
const elapsed = await promise;
expect(elapsed >= 120 && elapsed < 240).toBe(true);
expect(mockedTimeout).toBeCalledTimes(1);
});
it('should safely bind tasks to the queue', async () => {
const queue = new Queue(1);
const mockedErrorHandler = jest.fn();
const mockedTimeout = jest.fn(timeout);
const timer = queue.bindSafe(mockedTimeout, mockedErrorHandler);
const start = Date.now();
const promise = timer(120).then(time => time - start);
await timer(120);
expect(Date.now() - start < 120).toBe(true);
expect(mockedErrorHandler).toBeCalledTimes(1);
expect(mockedErrorHandler).nthCalledWith(
1,
expect.objectContaining({ message: 'Queue limit reached' })
);
const elapsed = await promise;
expect(elapsed >= 120 && elapsed < 240).toBe(true);
expect(mockedTimeout).toBeCalledTimes(1);
});
});
@@ -0,0 +1,193 @@
/**
* Constants
*/
const SEPARATOR = '/';
/**
* API
*/
/**
* Add values to a list hierarchically.
* @ For example:
* addToList([], 'a', 'b', 'c');
* will add the following hierarchy to the list:
* a > b > c
* resulting in the following array:
* [['a', [['b', ['c']]]]]
* @param {Array} list The target list;
* @param {...string} values The values to be hierarchically added to the list;
* @returns {Array} Returns the provided list possibly updated with the given
* values or null when a bad list (not an actual array) is provided
*/
function addToList(list, ...values) {
if (Array.isArray(list)) {
if (values.length > 0) {
addValuesToList(list, values);
}
return list;
}
return null;
}
/**
* Iterates through the provided hierarchical list executing the callback
* once for each leaf-node of the tree. The ancestors of the leaf-node being
* visited are passed to the callback function along with the leaf-node in
* the exact same order they appear on the tree (from root to leaf);
* @ For example, if the hierachy `a > b > c` appears on the tree ("a" being
* the root and "c" being the leaf) the callback function will be called as:
* callback('a', 'b', 'c');
* @param {Array} list The hierarchical list to be iterated
* @param {function} callback The callback which will be exected once for
* each leaf-node of the hierarchical list;
* @returns {Array} Returns the provided list or null for bad arguments;
*/
function forEach(list, callback) {
if (Array.isArray(list)) {
if (typeof callback === 'function') {
forEachValue(list, callback);
}
return list;
}
return null;
}
/**
* Retrieves an item from the given hierarchical list based on an index (number)
* or a path (string).
* @ For example:
* getItem(list, '1/0/4')
* will retrieve the fourth grandchild, from the first child of the second
* element of the list;
* @param {Array} list The source list;
* @param {string|number} indexOrPath The index of the element inside list
* (number) or the path to reach the desired element (string). The slash "/"
* character is cosidered the path separator;
*/
function getItem(list, indexOrPath) {
if (Array.isArray(list)) {
let subpath = null;
let index = typeof indexOrPath === 'number' ? indexOrPath : -1;
if (typeof indexOrPath === 'string') {
const separator = indexOrPath.indexOf(SEPARATOR);
if (separator > 0) {
index = parseInt(indexOrPath.slice(0, separator), 10);
if (separator + 1 < indexOrPath.length) {
subpath = indexOrPath.slice(separator + 1, indexOrPath.length);
}
} else {
index = parseInt(indexOrPath, 10);
}
}
if (index >= 0 && index < list.length) {
const item = list[index];
if (isSublist(item)) {
if (subpath !== null) {
return getItem(item[1], subpath);
}
return item[0];
}
return item;
}
}
}
/**
* Pretty-print the provided hierarchical list;
* @param {Array} list The source list;
* @returns {string} The textual representation of the hierarchical list;
*/
function print(list) {
let text = '';
if (Array.isArray(list)) {
let prev = [];
forEachValue(list, function(...args) {
let prevLen = prev.length;
for (let i = 0, l = args.length; i < l; ++i) {
if (i < prevLen && args[i] === prev[i]) {
continue;
}
text += ' '.repeat(i) + args[i] + '\n';
}
prev = args;
});
}
return text;
}
/**
* Utils
*/
function forEachValue(list, callback) {
for (let i = 0, l = list.length; i < l; ++i) {
let item = list[i];
if (isSublist(item)) {
if (item[1].length > 0) {
forEachValue(item[1], callback.bind(null, item[0]));
continue;
}
item = item[0];
}
callback(item);
}
}
function addValuesToList(list, values) {
let value = values.shift();
let index = add(list, value);
if (index >= 0) {
if (values.length > 0) {
let sublist = list[index];
if (!isSublist(sublist)) {
sublist = toSublist(value);
list[index] = sublist;
}
return addValuesToList(sublist[1], values);
}
return true;
}
return false;
}
function add(list, value) {
let index = find(list, value);
if (index === -2) {
index = list.push(value) - 1;
}
return index;
}
function find(list, value) {
if (typeof value === 'string') {
for (let i = 0, l = list.length; i < l; ++i) {
let item = list[i];
if (item === value || (isSublist(item) && item[0] === value)) {
return i;
}
}
return -2;
}
return -1;
}
function isSublist(subject) {
return (
Array.isArray(subject) &&
subject.length === 2 &&
typeof subject[0] === 'string' &&
Array.isArray(subject[1])
);
}
function toSublist(value) {
return [value + '', []];
}
/**
* Exports
*/
export { addToList, getItem, forEach, print };
@@ -0,0 +1,96 @@
import { addToList, forEach, getItem, print } from './hierarchicalListUtils';
describe('hierarchicalListUtils', function() {
let sharedList;
beforeEach(function() {
sharedList = [
['1.2.3.1', ['1.2.3.1.1', '1.2.3.1.2']],
'1.2.3.2',
['1.2.3.3', ['1.2.3.3.1', ['1.2.3.3.2', ['1.2.3.3.2.1', '1.2.3.3.2.2']]]],
];
});
describe('getItem', function() {
it('should retrieve elements from a list by index', function() {
expect(getItem(sharedList, 0)).toBe('1.2.3.1');
expect(getItem(sharedList, 1)).toBe('1.2.3.2');
expect(getItem(sharedList, 2)).toBe('1.2.3.3');
expect(getItem(sharedList, 3)).toBeUndefined();
});
it('should retrieve elements from a list by path', function() {
expect(getItem(sharedList, '0')).toBe('1.2.3.1');
expect(getItem(sharedList, '0/0')).toBe('1.2.3.1.1');
expect(getItem(sharedList, '0/1')).toBe('1.2.3.1.2');
expect(getItem(sharedList, '0/2')).toBeUndefined();
expect(getItem(sharedList, '1')).toBe('1.2.3.2');
expect(getItem(sharedList, '2')).toBe('1.2.3.3');
expect(getItem(sharedList, '2/0')).toBe('1.2.3.3.1');
expect(getItem(sharedList, '2/1')).toBe('1.2.3.3.2');
expect(getItem(sharedList, '2/2')).toBeUndefined();
expect(getItem(sharedList, '2/1/0')).toBe('1.2.3.3.2.1');
expect(getItem(sharedList, '2/1/1')).toBe('1.2.3.3.2.2');
expect(getItem(sharedList, '2/1/2')).toBeUndefined();
expect(getItem(sharedList, '3')).toBeUndefined();
});
});
describe('addToList', function() {
it('should support adding elements to a list hierarchically', function() {
const list = [];
addToList(list, '1.2.3.1', '1.2.3.1.1');
addToList(list, '1.2.3.1', '1.2.3.1.2');
addToList(list, '1.2.3.2');
addToList(list, '1.2.3.3', '1.2.3.3.1');
addToList(list, '1.2.3.3', '1.2.3.3.2', '1.2.3.3.2.1');
addToList(list, '1.2.3.3', '1.2.3.3.2', '1.2.3.3.2.2');
expect(list).toStrictEqual(sharedList);
});
it('should change leaf nodes into non-leaf nodes', function() {
const listw = [];
const listx = [['x.1', ['x.1.1', 'x.1.2']], 'x.2'];
const listy = [
['x.1', [['x.1.1', ['x.1.1.1']], 'x.1.2']],
['x.2', ['x.2.1']],
];
addToList(listw, 'x.1');
addToList(listw, 'x.1', 'x.1.1');
addToList(listw, 'x.1', 'x.1.2');
addToList(listw, 'x.2');
expect(listw).toStrictEqual(listx);
addToList(listw, 'x.2', 'x.2.1');
addToList(listw, 'x.1', 'x.1.1', 'x.1.1.1');
expect(listw).toStrictEqual(listy);
});
});
describe('forEach', function() {
it('should iterate through all leaf nodes of the tree', function() {
const fn = jest.fn();
forEach(sharedList, fn);
expect(fn).toHaveBeenCalledTimes(6);
expect(fn).nthCalledWith(1, '1.2.3.1', '1.2.3.1.1');
expect(fn).nthCalledWith(2, '1.2.3.1', '1.2.3.1.2');
expect(fn).nthCalledWith(3, '1.2.3.2');
expect(fn).nthCalledWith(4, '1.2.3.3', '1.2.3.3.1');
expect(fn).nthCalledWith(5, '1.2.3.3', '1.2.3.3.2', '1.2.3.3.2.1');
expect(fn).nthCalledWith(6, '1.2.3.3', '1.2.3.3.2', '1.2.3.3.2.2');
});
});
describe('print', function() {
it('should pretty-print the hierarchical list', function() {
expect(print(sharedList)).toBe(
'1.2.3.1\n' +
' 1.2.3.1.1\n' +
' 1.2.3.1.2\n' +
'1.2.3.2\n' +
'1.2.3.3\n' +
' 1.2.3.3.1\n' +
' 1.2.3.3.2\n' +
' 1.2.3.3.2.1\n' +
' 1.2.3.3.2.2\n'
);
});
});
});
+18
View File
@@ -10,8 +10,14 @@ import DicomLoaderService from './dicomLoaderService.js';
import b64toBlob from './b64toBlob.js';
import loadAndCacheDerivedDisplaySets from './loadAndCacheDerivedDisplaySets.js';
import * as urlUtil from './urlUtil';
import makeDeferred from './makeDeferred';
import makeCancelable from './makeCancelable';
import hotkeys from './hotkeys';
import Queue from './Queue';
import isDicomUid from './isDicomUid';
import resolveObjectPath from './resolveObjectPath';
import * as hierarchicalListUtils from './hierarchicalListUtils';
import * as progressTrackingUtils from './progressTrackingUtils';
const utils = {
guid,
@@ -26,8 +32,14 @@ const utils = {
DicomLoaderService,
urlUtil,
loadAndCacheDerivedDisplaySets,
makeDeferred,
makeCancelable,
hotkeys,
Queue,
isDicomUid,
resolveObjectPath,
hierarchicalListUtils,
progressTrackingUtils,
};
export {
@@ -43,8 +55,14 @@ export {
DicomLoaderService,
urlUtil,
loadAndCacheDerivedDisplaySets,
makeDeferred,
makeCancelable,
hotkeys,
Queue,
isDicomUid,
resolveObjectPath,
hierarchicalListUtils,
progressTrackingUtils,
};
export default utils;
+6
View File
@@ -15,8 +15,14 @@ describe('Top level exports', () => {
'loadAndCacheDerivedDisplaySets',
'DicomLoaderService',
'urlUtil',
'makeDeferred',
'makeCancelable',
'hotkeys',
'Queue',
'isDicomUid',
'resolveObjectPath',
'hierarchicalListUtils',
'progressTrackingUtils',
].sort();
const exports = Object.keys(utils.default).sort();
+4
View File
@@ -0,0 +1,4 @@
export default function isDicomUid(subject) {
const regex = /^\d+(?:\.\d+)*$/;
return typeof subject === 'string' && regex.test(subject.trim());
}
@@ -0,0 +1,16 @@
import isDicomUid from './isDicomUid';
describe('isDicomUid', function() {
it('should return true for valid DICOM UIDs', function() {
expect(isDicomUid('1')).toBe(true);
expect(isDicomUid('1.2')).toBe(true);
expect(isDicomUid('1.2.3')).toBe(true);
expect(isDicomUid('1.2.3.4')).toBe(true);
});
it('should return false for invalid DICOM UIDs', function() {
expect(isDicomUid('x')).toBe(false);
expect(isDicomUid('1.')).toBe(false);
expect(isDicomUid('1. 2')).toBe(false);
expect(isDicomUid('1.2.n.4')).toBe(false);
});
});
+7
View File
@@ -0,0 +1,7 @@
export default function makeDeferred() {
let reject, resolve, promise = new Promise(function (res, rej) {
resolve = res;
reject = rej;
});
return Object.freeze({ promise, resolve, reject });
}
@@ -0,0 +1,14 @@
import makeDeferred from './makeDeferred';
describe('makeDeferred', () => {
it('should provide a promise to be resolved externally', () => {
const deferred = makeDeferred();
setTimeout(() => void deferred.resolve('Yay!'));
return deferred.promise.then(result => void expect(result).toBe('Yay!'));
});
it('should provide a promise to be rejected externally', () => {
const deferred = makeDeferred();
setTimeout(() => void deferred.reject('Oops...'));
return deferred.promise.catch(error => void expect(error).toBe('Oops...'));
});
});
@@ -0,0 +1,336 @@
import makeDeferred from './makeDeferred';
/**
* Constants
*/
const TYPE = Symbol('Type');
const TASK = Symbol('Task');
const LIST = Symbol('List');
/**
* Public Methods
*/
/**
* Creates an instance of a task list
* @returns {Object} A task list object
*/
function createList() {
return objectWithType(LIST, {
head: null,
named: Object.create(null),
observers: [],
});
}
/**
* Checks if the given argument is a List instance
* @param {any} subject The value to be tested
* @returns {boolean} true if a valid List instance is given, false otherwise
*/
function isList(subject) {
return isOfType(LIST, subject);
}
/**
* Creates an instance of a task
* @param {Object} list The List instance related to this task
* @param {Object} next The next Task instance to link to
* @returns {Object} A task object
*/
function createTask(list, next) {
return objectWithType(TASK, {
list: isList(list) ? list : null,
next: isTask(next) ? next : null,
failed: false,
awaiting: null,
progress: 0.0,
});
}
/**
* Checks if the given argument is a Task instance
* @param {any} subject The value to be tested
* @returns {boolean} true if a valid Task instance is given, false otherwise
*/
function isTask(subject) {
return isOfType(TASK, subject);
}
/**
* Appends a new Task to the given List instance and notifies the list observers
* @param {Object} list A List instance
* @returns {Object} The new Task instance appended to the List or null if the
* given List instanc is not valid
*/
function increaseList(list) {
if (isList(list)) {
const task = createTask(list, list.head);
list.head = task;
notify(list, getOverallProgress(list));
return task;
}
return null;
}
/**
* Updates the internal progress value of the given Task instance and notifies
* the observers of the associated list.
* @param {Object} task The Task instance to be updated
* @param {number} value A number between 0 (inclusive) and 1 (exclusive)
* indicating the progress of the task;
* @returns {void} Nothing is returned
*/
function update(task, value) {
if (isTask(task) && isValidProgress(value) && value < 1.0) {
if (task.progress !== value) {
task.progress = value;
if (isList(task.list)) {
notify(task.list, getOverallProgress(task.list));
}
}
}
}
/**
* Sets a Task instance as finished (progress = 1.0), freezes it in order to
* prevent further modifications and notifies the observers of the associated
* list.
* @param {Object} task The Task instance to be finalized
* @returns {void} Nothing is returned
*/
function finish(task) {
if (isTask(task)) {
task.progress = 1.0;
task.awaiting = null;
Object.freeze(task);
if (isList(task.list)) {
notify(task.list, getOverallProgress(task.list));
}
}
}
/**
* Generate a summarized snapshot of the current status of the given task List
* @param {Object} list The List instance to be scanned
* @returns {Object} An obeject representing the summarized status of the list
*/
function getOverallProgress(list) {
const status = createStatus();
if (isList(list)) {
let task = list.head;
while (isTask(task)) {
status.total++;
if (isValidProgress(task.progress)) {
status.partial += task.progress;
if (task.progress === 1.0 && task.failed) status.failures++;
}
task = task.next;
}
}
if (status.total > 0) {
status.progress = status.partial / status.total;
}
return Object.freeze(status);
}
/**
* Adds a Task instance to the given list that waits on a given "thenable". When
* the thenable resolves the "finish" method is called on the newly created
* instance thus notifying the observers of the list.
* @param {Object} list The List instance to which the new task will be added
* @param {Object|Promise} thenable The thenable to be waited on
* @returns {Object} A reference to the newly created Task;
*/
function waitOn(list, thenable) {
const task = increaseList(list);
if (isTask(task)) {
task.awaiting = Promise.resolve(thenable).then(
function() {
finish(task);
},
function() {
task.failed = true;
finish(task);
}
);
return task;
}
return null;
}
/**
* Adds a Task instance to the given list using a deferred (a Promise that can
* be externally resolved) notifying the observers of the list.
* @param {Object} list The List instance to which the new task will be added
* @returns {Object} An object with references to the created deferred and task
*/
function addDeferred(list) {
const deferred = makeDeferred();
const task = waitOn(list, deferred.promise);
return Object.freeze({
deferred,
task,
});
}
/**
* Assigns a name to a specific task of the list
* @param {Object} list The List instance whose task will be named
* @param {Object} task The specified Task instance
* @param {string} name The name of the task
* @returns {boolean} Returns true on success, false otherwise
*/
function setTaskName(list, task, name) {
if (
contains(list, task) &&
list.named !== null &&
typeof list.named === 'object' &&
typeof name === 'string'
) {
list.named[name] = task;
return true;
}
return false;
}
/**
* Retrieves a task by name
* @param {Object} list The List instance whose task will be retrieved
* @param {string} name The name of the task to be retrieved
* @returns {Object} The Task instance or null if not found
*/
function getTaskByName(list, name) {
if (
isList(list) &&
list.named !== null &&
typeof list.named === 'object' &&
typeof name === 'string'
) {
const task = list.named[name];
if (isTask(task)) {
return task;
}
}
return null;
}
/**
* Adds an observer (callback function) to a given List instance
* @param {Object} list The List instance to which the observer will be appended
* @param {Function} observer The observer (function) that will be executed
* every time a change happens within the list
* @returns {boolean} Returns true on success and false otherewise
*/
function addObserver(list, observer) {
if (
isList(list) &&
Array.isArray(list.observers) &&
typeof observer === 'function'
) {
list.observers.push(observer);
return true;
}
return false;
}
/**
* Removes an observer (callback function) from a given List instance
* @param {Object} list The instance List from which the observer will removed
* @param {Function} observer The observer function to be removed
* @returns {boolean} Returns true on success and false otherewise
*/
function removeObserver(list, observer) {
if (
isList(list) &&
Array.isArray(list.observers) &&
list.observers.length > 0
) {
const index = list.observers.indexOf(observer);
if (index >= 0) {
list.observers.splice(index, 1);
return true;
}
}
return false;
}
/**
* Utils
*/
function createStatus() {
return Object.seal({
total: 0,
partial: 0.0,
progress: 0.0,
failures: 0,
});
}
function objectWithType(type, object) {
return Object.seal(Object.defineProperty(object, TYPE, { value: type }));
}
function isOfType(type, subject) {
return (
subject !== null && typeof subject === 'object' && subject[TYPE] === type
);
}
function isValidProgress(value) {
return typeof value === 'number' && value >= 0.0 && value <= 1.0;
}
function contains(list, task) {
if (isList(list) && isTask(task)) {
let item = list.head;
while (isTask(item)) {
if (item === task) {
return true;
}
item = item.next;
}
}
return false;
}
function notify(list, data) {
if (
isList(list) &&
Array.isArray(list.observers) &&
list.observers.length > 0
) {
list.observers.slice().forEach(function(observer) {
if (typeof observer === 'function') {
try {
observer(data, list);
} catch (e) {
/* Oops! */
}
}
});
}
}
/**
* Exports
*/
export {
createList,
isList,
createTask,
isTask,
increaseList,
update,
finish,
getOverallProgress,
waitOn,
addDeferred,
setTaskName,
getTaskByName,
addObserver,
removeObserver,
};
@@ -0,0 +1,175 @@
import * as utils from './progressTrackingUtils';
describe('progressTrackingUtils', () => {
describe('Creation of lists of tasks to be tracked', () => {
it('should support creation of task lists', () => {
expect(utils.createList()).toBeInstanceOf(Object);
});
it('should support validation of task lists', () => {
const list = utils.createList();
expect(utils.isList(list)).toBe(true);
expect(utils.isList(JSON.parse(JSON.stringify(list)))).toBe(false);
});
});
describe('Usage of lists of tasks to be tracked', () => {
let context;
// Mock for download
function fakeRequest(callback) {
return new Promise(resolve => {
let progress = 0.0;
setTimeout(function step() {
if (progress < 1.0) {
progress += 1 / 4;
callback(progress);
setTimeout(step);
return;
}
resolve(true);
});
});
}
beforeEach(() => {
const list = utils.createList();
const observer = jest.fn();
utils.addObserver(list, observer);
context = { list, observer };
});
it('should call observer twice for each task', () => {
const { list, observer } = context;
const promises = [
Promise.resolve('A'),
Promise.resolve('B'),
Promise.resolve('C'),
];
promises.forEach(promise => void utils.waitOn(list, promise));
return Promise.all(promises).then(() => {
expect(observer).toBeCalledTimes(6);
[
{
failures: 0,
partial: 0,
progress: 0,
total: 1,
},
{
failures: 0,
partial: 0,
progress: 0,
total: 2,
},
{
failures: 0,
partial: 0,
progress: 0,
total: 3,
},
{
failures: 0,
partial: 1.0,
progress: 1 / 3,
total: 3,
},
{
failures: 0,
partial: 2.0,
progress: 2 / 3,
total: 3,
},
{
failures: 0,
partial: 3.0,
progress: 1.0,
total: 3,
},
].forEach((item, i) => {
const result = expect.objectContaining(item);
expect(observer).nthCalledWith(i + 1, result, list);
});
expect(utils.getOverallProgress(list)).toStrictEqual({
failures: 0,
partial: 3.0,
progress: 1.0,
total: 3,
});
});
});
it('should support tasks with internal progress updates', () => {
const { list, observer } = context;
const download = utils.addDeferred(list);
const processing = download.deferred.promise.then(result => result);
const update = jest.fn(p => void utils.update(download.task, p));
download.deferred.resolve(fakeRequest(update));
utils.waitOn(list, processing);
return processing.then(() => {
expect(update).toBeCalledTimes(4);
[0.25, 0.5, 0.75, 1.0].forEach(
(value, i) => void expect(update).nthCalledWith(i + 1, value)
);
expect(observer).toBeCalledTimes(7);
[
{
failures: 0,
partial: 0,
progress: 0,
total: 1,
},
{
failures: 0,
partial: 0,
progress: 0,
total: 2,
},
{
failures: 0,
partial: 0.25,
progress: 0.125,
total: 2,
},
{
failures: 0,
partial: 0.5,
progress: 0.25,
total: 2,
},
{
failures: 0,
partial: 0.75,
progress: 0.375,
total: 2,
},
{
failures: 0,
partial: 1.0,
progress: 0.5,
total: 2,
},
{
failures: 0,
partial: 2.0,
progress: 1.0,
total: 2,
},
].forEach((item, i) => {
const result = expect.objectContaining(item);
expect(observer).nthCalledWith(i + 1, result, list);
});
});
});
});
describe('Naming of specific tasks', () => {
it('should support naming specific tasks', () => {
const list = utils.createList();
const tasks = [utils.increaseList(list), utils.increaseList(list)];
expect(utils.setTaskName(list, tasks[0], 'firstTask')).toBe(true);
expect(utils.setTaskName(list, tasks[1], 'secondTask')).toBe(true);
expect(utils.getTaskByName(list, 'secondTask')).toBe(tasks[1]);
expect(utils.getTaskByName(list, 'firstTask')).toBe(tasks[0]);
});
});
});
@@ -0,0 +1,17 @@
export default function resolveObjectPath(root, path, defaultValue) {
if (root !== null && typeof root === 'object' && typeof path === 'string') {
let value,
separator = path.indexOf('.');
if (separator >= 0) {
return resolveObjectPath(
root[path.slice(0, separator)],
path.slice(separator + 1, path.length),
defaultValue
);
}
value = root[path];
return value === undefined && defaultValue !== undefined
? defaultValue
: value;
}
}
@@ -0,0 +1,35 @@
import resolveObjectPath from './resolveObjectPath';
describe('resolveObjectPath', function() {
let config;
beforeEach(function() {
config = {
active: {
user: {
name: {
first: 'John',
last: 'Doe',
},
},
servers: [
{
ipv4: '10.0.0.1',
},
],
},
};
});
it('should safely return deeply nested values from an object', function() {
expect(resolveObjectPath(config, 'active.user.name.first')).toBe('John');
expect(resolveObjectPath(config, 'active.user.name.last')).toBe('Doe');
expect(resolveObjectPath(config, 'active.servers.0.ipv4')).toBe('10.0.0.1');
});
it('should silently return undefined when intermediate values are not valid objects', function() {
expect(resolveObjectPath(config, 'active.usr.name.first')).toBeUndefined();
expect(resolveObjectPath(config, 'active.name.last')).toBeUndefined();
expect(resolveObjectPath(config, 'active.servers.7.ipv4')).toBeUndefined();
});
});
+1
View File
@@ -56,6 +56,7 @@
"@ohif/extension-dicom-rt": "^0.0.2",
"@ohif/extension-dicom-segmentation": "^0.1.2",
"@ohif/extension-lesion-tracker": "^0.2.0",
"@ohif/extension-dicom-p10-downloader": "^0.0.1",
"@ohif/extension-vtk": "^1.3.11",
"@ohif/i18n": "^0.52.8",
"@ohif/ui": "^1.4.3",
+2
View File
@@ -29,6 +29,7 @@ import OHIFDicomSegmentationExtension from '@ohif/extension-dicom-segmentation';
import OHIFDicomRtExtension from '@ohif/extension-dicom-rt';
import OHIFDicomMicroscopyExtension from '@ohif/extension-dicom-microscopy';
import OHIFDicomPDFExtension from '@ohif/extension-dicom-pdf';
import OHIFDicomP10DownloaderExtension from '@ohif/extension-dicom-p10-downloader';
/*
* Default Settings
@@ -48,6 +49,7 @@ const appProps = {
OHIFDicomPDFExtension,
OHIFDicomSegmentationExtension,
OHIFDicomRtExtension,
OHIFDicomP10DownloaderExtension,
],
};