From 9dd9e2e6b38df18ae9d2e1ad267730b9ccd07baf Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Mon, 20 Nov 2023 19:13:22 -0500 Subject: [PATCH 1/5] fix(session-zip.e2e): select child input avoiding webdriverio error --- tests/pageobjects/volview.page.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/pageobjects/volview.page.ts b/tests/pageobjects/volview.page.ts index b1e06adec..d37b4be58 100644 --- a/tests/pageobjects/volview.page.ts +++ b/tests/pageobjects/volview.page.ts @@ -68,13 +68,17 @@ class VolViewPage extends Page { return $('#notifications'); } + async getNotificationsCount() { + const badge = await this.notifications.$('span[aria-label="Badge"]'); + const innerText = await badge.getText(); + return parseInt(innerText, 10); + } + async waitForNotification() { const this_ = this; await browser.waitUntil( async () => { - const badge = await this_.notifications.$('span[aria-label="Badge"]'); - const innerText = await badge.getText(); - const notificationCount = parseInt(innerText, 10); + const notificationCount = await this_.getNotificationsCount(); return notificationCount >= 1; }, { @@ -138,7 +142,7 @@ class VolViewPage extends Page { get labelStrokeWidthInput() { // there should only be one on the screen at any given time - return $('.label-stroke-width-input'); + return $('.label-stroke-width-input').$('input'); } get editLabelModalDoneButton() { From 49dd65bd7bf3e1e2d1bd9614825b24b835e19b5e Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Mon, 20 Nov 2023 19:42:34 -0500 Subject: [PATCH 2/5] chore(state-manifest.e2e): add test loading version 3.0.0 state manifest --- tests/fixtures/toolsProstate.volview.json | 407 ++++++++++++++++++++++ tests/pageobjects/volview.page.ts | 1 + tests/specs/state-manifest.e2e.ts | 35 ++ wdio.shared.conf.ts | 2 + 4 files changed, 445 insertions(+) create mode 100644 tests/fixtures/toolsProstate.volview.json create mode 100644 tests/specs/state-manifest.e2e.ts diff --git a/tests/fixtures/toolsProstate.volview.json b/tests/fixtures/toolsProstate.volview.json new file mode 100644 index 000000000..40ae51e2b --- /dev/null +++ b/tests/fixtures/toolsProstate.volview.json @@ -0,0 +1,407 @@ +{ + "version": "3.0.0", + "datasets": [ + { + "id": "1.3.6.1.4.1.14519.5.2.1.7311.5101.206828891270520544417996275680.5tse2d1254.538438420111018.1D000000SN0D000000S0D000000S0D000000S0D970296SN0D241922", + "path": "data/1.3.6.1.4.1.14519.5.2.1.7311.5101.206828891270520544417996275680.5tse2d1254.538438420111018.1D000000SN0D000000S0D000000S0D000000S0D970296SN0D241922/", + "type": "dicom" + } + ], + "remoteFiles": { + "1.3.6.1.4.1.14519.5.2.1.7311.5101.206828891270520544417996275680.5tse2d1254.538438420111018.1D000000SN0D000000S0D000000S0D000000S0D970296SN0D241922": [ + { + "archiveSrc": { "path": "MRI-PROSTATEx-0004/1-19.dcm" }, + "parent": { + "uriSrc": { + "uri": "https://data.kitware.com/api/v1/item/63527c7311dab8142820a338/download", + "name": "MRI-PROSTATEx-0004.zip" + } + } + }, + { + "archiveSrc": { "path": "MRI-PROSTATEx-0004/1-18.dcm" }, + "parent": { + "uriSrc": { + "uri": "https://data.kitware.com/api/v1/item/63527c7311dab8142820a338/download", + "name": "MRI-PROSTATEx-0004.zip" + } + } + }, + { + "archiveSrc": { "path": "MRI-PROSTATEx-0004/1-17.dcm" }, + "parent": { + "uriSrc": { + "uri": "https://data.kitware.com/api/v1/item/63527c7311dab8142820a338/download", + "name": "MRI-PROSTATEx-0004.zip" + } + } + }, + { + "archiveSrc": { "path": "MRI-PROSTATEx-0004/1-16.dcm" }, + "parent": { + "uriSrc": { + "uri": "https://data.kitware.com/api/v1/item/63527c7311dab8142820a338/download", + "name": "MRI-PROSTATEx-0004.zip" + } + } + }, + { + "archiveSrc": { "path": "MRI-PROSTATEx-0004/1-15.dcm" }, + "parent": { + "uriSrc": { + "uri": "https://data.kitware.com/api/v1/item/63527c7311dab8142820a338/download", + "name": "MRI-PROSTATEx-0004.zip" + } + } + }, + { + "archiveSrc": { "path": "MRI-PROSTATEx-0004/1-14.dcm" }, + "parent": { + "uriSrc": { + "uri": "https://data.kitware.com/api/v1/item/63527c7311dab8142820a338/download", + "name": "MRI-PROSTATEx-0004.zip" + } + } + }, + { + "archiveSrc": { "path": "MRI-PROSTATEx-0004/1-13.dcm" }, + "parent": { + "uriSrc": { + "uri": "https://data.kitware.com/api/v1/item/63527c7311dab8142820a338/download", + "name": "MRI-PROSTATEx-0004.zip" + } + } + }, + { + "archiveSrc": { "path": "MRI-PROSTATEx-0004/1-12.dcm" }, + "parent": { + "uriSrc": { + "uri": "https://data.kitware.com/api/v1/item/63527c7311dab8142820a338/download", + "name": "MRI-PROSTATEx-0004.zip" + } + } + }, + { + "archiveSrc": { "path": "MRI-PROSTATEx-0004/1-11.dcm" }, + "parent": { + "uriSrc": { + "uri": "https://data.kitware.com/api/v1/item/63527c7311dab8142820a338/download", + "name": "MRI-PROSTATEx-0004.zip" + } + } + }, + { + "archiveSrc": { "path": "MRI-PROSTATEx-0004/1-10.dcm" }, + "parent": { + "uriSrc": { + "uri": "https://data.kitware.com/api/v1/item/63527c7311dab8142820a338/download", + "name": "MRI-PROSTATEx-0004.zip" + } + } + }, + { + "archiveSrc": { "path": "MRI-PROSTATEx-0004/1-09.dcm" }, + "parent": { + "uriSrc": { + "uri": "https://data.kitware.com/api/v1/item/63527c7311dab8142820a338/download", + "name": "MRI-PROSTATEx-0004.zip" + } + } + }, + { + "archiveSrc": { "path": "MRI-PROSTATEx-0004/1-08.dcm" }, + "parent": { + "uriSrc": { + "uri": "https://data.kitware.com/api/v1/item/63527c7311dab8142820a338/download", + "name": "MRI-PROSTATEx-0004.zip" + } + } + }, + { + "archiveSrc": { "path": "MRI-PROSTATEx-0004/1-07.dcm" }, + "parent": { + "uriSrc": { + "uri": "https://data.kitware.com/api/v1/item/63527c7311dab8142820a338/download", + "name": "MRI-PROSTATEx-0004.zip" + } + } + }, + { + "archiveSrc": { "path": "MRI-PROSTATEx-0004/1-06.dcm" }, + "parent": { + "uriSrc": { + "uri": "https://data.kitware.com/api/v1/item/63527c7311dab8142820a338/download", + "name": "MRI-PROSTATEx-0004.zip" + } + } + }, + { + "archiveSrc": { "path": "MRI-PROSTATEx-0004/1-05.dcm" }, + "parent": { + "uriSrc": { + "uri": "https://data.kitware.com/api/v1/item/63527c7311dab8142820a338/download", + "name": "MRI-PROSTATEx-0004.zip" + } + } + }, + { + "archiveSrc": { "path": "MRI-PROSTATEx-0004/1-04.dcm" }, + "parent": { + "uriSrc": { + "uri": "https://data.kitware.com/api/v1/item/63527c7311dab8142820a338/download", + "name": "MRI-PROSTATEx-0004.zip" + } + } + }, + { + "archiveSrc": { "path": "MRI-PROSTATEx-0004/1-03.dcm" }, + "parent": { + "uriSrc": { + "uri": "https://data.kitware.com/api/v1/item/63527c7311dab8142820a338/download", + "name": "MRI-PROSTATEx-0004.zip" + } + } + }, + { + "archiveSrc": { "path": "MRI-PROSTATEx-0004/1-02.dcm" }, + "parent": { + "uriSrc": { + "uri": "https://data.kitware.com/api/v1/item/63527c7311dab8142820a338/download", + "name": "MRI-PROSTATEx-0004.zip" + } + } + }, + { + "archiveSrc": { "path": "MRI-PROSTATEx-0004/1-01.dcm" }, + "parent": { + "uriSrc": { + "uri": "https://data.kitware.com/api/v1/item/63527c7311dab8142820a338/download", + "name": "MRI-PROSTATEx-0004.zip" + } + } + } + ] + }, + "labelMaps": [], + "tools": { + "crosshairs": { "position": [0, 0, 0] }, + "paint": { + "activeSegmentGroupID": null, + "activeSegment": null, + "brushSize": 4, + "labelmapOpacity": 1 + }, + "crop": { + "10": { + "Sagittal": [-0.5, 383.5], + "Coronal": [-0.5, 383.5], + "Axial": [-0.5, 18.5] + } + }, + "current": "Ruler", + "polygons": { + "tools": [ + { + "imageID": "1.3.6.1.4.1.14519.5.2.1.7311.5101.206828891270520544417996275680.5tse2d1254.538438420111018.1D000000SN0D000000S0D000000S0D000000S0D970296SN0D241922", + "frameOfReference": { + "planeNormal": [ + 1.408073370182161e-17, 0.24192189256080032, 0.9702957270336713 + ], + "planeOrigin": [ + -117.91325380387, -75.35208161269456, 52.13696960699617 + ] + }, + "slice": 9, + "placing": false, + "color": "#ffffff", + "strokeWidth": 1, + "name": "Polygon", + "points": [ + [-18.053922796672243, -55.928583890596556, 47.29414777633043], + [11.43659320468982, -15.117696849450404, 37.11885095186395], + [-31.590564517872114, -5.0322464081816385, 34.6042657691187] + ], + "id": "15", + "label": "9", + "labelName": "white", + "movePoint": null + } + ], + "labels": { + "7": { "labelName": "red", "color": "red", "strokeWidth": 1 }, + "8": { "labelName": "green", "color": "#00ff00", "strokeWidth": 1 }, + "9": { "labelName": "white", "color": "#ffffff", "strokeWidth": 1 } + } + }, + "rectangles": { + "tools": [ + { + "imageID": "1.3.6.1.4.1.14519.5.2.1.7311.5101.206828891270520544417996275680.5tse2d1254.538438420111018.1D000000SN0D000000S0D000000S0D000000S0D970296SN0D241922", + "frameOfReference": { + "planeNormal": [ + 1.408073370182161e-17, 0.24192189256080032, 0.9702957270336713 + ], + "planeOrigin": [ + -117.91325380387, -75.35208161269456, 52.13696960699617 + ] + }, + "slice": 9, + "placing": false, + "color": "red", + "strokeWidth": 1, + "name": "Rectangle", + "firstPoint": [ + -56.7300165298934, -65.31039649940394, 49.633296345923895 + ], + "secondPoint": [ + -34.007813285594224, -51.23767954755222, 46.12457398055551 + ], + "id": "11", + "fillColor": "transparent", + "label": "6", + "labelName": "lesion" + } + ], + "labels": { + "4": { + "labelName": "artifact", + "color": "#888888", + "strokeWidth": 1, + "fillColor": "transparent" + }, + "5": { + "labelName": "innocuous", + "color": "#00ff00", + "strokeWidth": 1, + "fillColor": "transparent" + }, + "6": { + "labelName": "lesion", + "color": "red", + "strokeWidth": 1, + "fillColor": "transparent" + } + } + }, + "rulers": { + "tools": [ + { + "imageID": "1.3.6.1.4.1.14519.5.2.1.7311.5101.206828891270520544417996275680.5tse2d1254.538438420111018.1D000000SN0D000000S0D000000S0D000000S0D970296SN0D241922", + "frameOfReference": { + "planeNormal": [ + 1.408073370182161e-17, 0.24192189256080032, 0.9702957270336713 + ], + "planeOrigin": [ + -117.91325380387, -75.35208161269456, 52.13696960699617 + ] + }, + "slice": 9, + "placing": false, + "color": "#ffffff", + "strokeWidth": 1, + "name": "Ruler", + "firstPoint": [ + -49.23652102879336, -28.72132454079226, 40.510616230314696 + ], + "secondPoint": [ + 14.579040930216323, -31.7704125712456, 41.270839249297595 + ], + "id": "28", + "label": "3", + "labelName": "white" + }, + { + "imageID": "1.3.6.1.4.1.14519.5.2.1.7311.5101.206828891270520544417996275680.5tse2d1254.538438420111018.1D000000SN0D000000S0D000000S0D000000S0D970296SN0D241922", + "frameOfReference": { + "planeNormal": [ + 1.408073370182161e-17, 0.24192189256080032, 0.9702957270336713 + ], + "planeOrigin": [ + -117.91325380387, -75.35208161269456, 52.13696960699617 + ] + }, + "slice": 9, + "placing": false, + "color": "#58f24c", + "strokeWidth": 3, + "name": "Ruler", + "firstPoint": [ + -49.7199707777567, -16.29041996967262, 37.411243661415796 + ], + "secondPoint": [ + 16.75455670028652, -20.043149753622366, 38.346904271174324 + ], + "id": "31", + "label": "32", + "labelName": "New Label" + } + ], + "labels": { + "1": { "labelName": "red", "color": "red", "strokeWidth": 1 }, + "2": { "labelName": "green", "color": "#00ff00", "strokeWidth": 1 }, + "3": { "labelName": "white", "color": "#ffffff", "strokeWidth": 1 }, + "32": { "labelName": "New Label", "color": "#58f24c", "strokeWidth": 3 } + } + } + }, + "layout": { + "name": "Axial Primary", + "direction": "V", + "items": [ + "Axial", + { "direction": "H", "items": ["3D", "Coronal", "Sagittal"] } + ] + }, + "views": [ + { + "id": "Coronal", + "type": "2D", + "props": { "viewDirection": "Posterior", "viewUp": "Superior" }, + "config": {} + }, + { + "id": "Sagittal", + "type": "2D", + "props": { "viewDirection": "Right", "viewUp": "Superior" }, + "config": {} + }, + { + "id": "Axial", + "type": "2D", + "props": { "viewDirection": "Superior", "viewUp": "Anterior" }, + "config": {} + }, + { + "id": "ObliqueCoronal", + "type": "Oblique", + "props": { "viewDirection": "Posterior", "viewUp": "Superior" }, + "config": {} + }, + { + "id": "ObliqueSagittal", + "type": "Oblique", + "props": { "viewDirection": "Right", "viewUp": "Superior" }, + "config": {} + }, + { + "id": "ObliqueAxial", + "type": "Oblique", + "props": { "viewDirection": "Superior", "viewUp": "Anterior" }, + "config": {} + }, + { + "id": "3D", + "type": "3D", + "props": { "viewDirection": "Posterior", "viewUp": "Superior" }, + "config": {} + }, + { + "id": "Oblique3D", + "type": "Oblique3D", + "props": { "viewDirection": "Posterior", "viewUp": "Superior" }, + "config": {} + } + ], + "parentToLayers": [], + "primarySelection": "1.3.6.1.4.1.14519.5.2.1.7311.5101.206828891270520544417996275680.5tse2d1254.538438420111018.1D000000SN0D000000S0D000000S0D000000S0D970296SN0D241922" +} diff --git a/tests/pageobjects/volview.page.ts b/tests/pageobjects/volview.page.ts index d37b4be58..f740e0f2f 100644 --- a/tests/pageobjects/volview.page.ts +++ b/tests/pageobjects/volview.page.ts @@ -71,6 +71,7 @@ class VolViewPage extends Page { async getNotificationsCount() { const badge = await this.notifications.$('span[aria-label="Badge"]'); const innerText = await badge.getText(); + if (innerText === '') return 0; return parseInt(innerText, 10); } diff --git a/tests/specs/state-manifest.e2e.ts b/tests/specs/state-manifest.e2e.ts new file mode 100644 index 000000000..9242018ef --- /dev/null +++ b/tests/specs/state-manifest.e2e.ts @@ -0,0 +1,35 @@ +import * as path from 'path'; +import * as fs from 'fs'; +import { cleanuptotal } from 'wdio-cleanuptotal-service'; +import JSZip from 'jszip'; +import { FIXTURES, TEMP_DIR } from '../../wdio.shared.conf'; +import { volViewPage } from '../pageobjects/volview.page'; +// import toolsProstateJSON from '/fixtures/toolsProstate.volview.json'; + +describe('State file manifest.json code', () => { + it('has no errors loading version 3.0.0 manifest.json file ', async () => { + // write json to zip file in temp dir + const fileName = 'asdf.zip'; + const filePath = path.join(TEMP_DIR, fileName); + + const manifest = fs.readFileSync( + path.join(FIXTURES, 'toolsProstate.volview.json') + ); + + const zip = new JSZip(); + zip.file('manifest.json', manifest); + const data = await zip.generateAsync({ type: 'nodebuffer' }); + + await fs.promises.writeFile(filePath, data); + cleanuptotal.addCleanup(async () => { + fs.unlinkSync(filePath); + }); + + const urlParams = `?urls=[tmp/${fileName}]`; + await volViewPage.open(urlParams); + await volViewPage.waitForViews(); + + const notifications = await volViewPage.getNotificationsCount(); + expect(notifications).toEqual(0); + }); +}); diff --git a/wdio.shared.conf.ts b/wdio.shared.conf.ts index c7b8389fb..aa9e06b52 100644 --- a/wdio.shared.conf.ts +++ b/wdio.shared.conf.ts @@ -13,6 +13,8 @@ const ROOT = projectRoot(); // TEMP_DIR is also downloads directory const TMP = '.tmp/'; export const TEMP_DIR = path.resolve(ROOT, TMP); +const FIXTURES_DIR = 'tests/fixtures/'; +export const FIXTURES = path.resolve(ROOT, FIXTURES_DIR); export const config: Options.Testrunner = { baseUrl: `http://localhost:${TEST_PORT}`, From b94b14276786ea8a59ec8cad71636ec955720dc2 Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Mon, 27 Nov 2023 15:52:18 -0500 Subject: [PATCH 3/5] fix(restoreStateFile): support 2.1.0 version of state file Fixes #521 --- src/io/import/processors/restoreStateFile.ts | 125 +++++++++++++++++- ...json => tools-prostate.3-0-0.volview.json} | 0 tests/specs/remote-manifest.e2e.ts | 14 +- tests/specs/state-manifest.e2e.ts | 62 ++++++--- tests/specs/utils.ts | 15 +++ wdio.shared.conf.ts | 2 +- 6 files changed, 184 insertions(+), 34 deletions(-) rename tests/fixtures/{toolsProstate.volview.json => tools-prostate.3-0-0.volview.json} (100%) create mode 100644 tests/specs/utils.ts diff --git a/src/io/import/processors/restoreStateFile.ts b/src/io/import/processors/restoreStateFile.ts index 840b3120c..f7f12c287 100644 --- a/src/io/import/processors/restoreStateFile.ts +++ b/src/io/import/processors/restoreStateFile.ts @@ -26,7 +26,10 @@ import { makeImageSelection, useDatasetStore, } from '@/src/store/datasets'; -import { useSegmentGroupStore } from '@/src/store/segmentGroups'; +import { + makeDefaultSegmentGroupName, + useSegmentGroupStore, +} from '@/src/store/segmentGroups'; import { useToolStore } from '@/src/store/tools'; import { useLayersStore } from '@/src/store/datasets-layers'; import { extractFilesFromZip } from '@/src/io/zip'; @@ -34,6 +37,120 @@ import downloadUrl from '@/src/io/import/processors/downloadUrl'; import updateFileMimeType from '@/src/io/import/processors/updateFileMimeType'; import extractArchiveTarget from '@/src/io/import/processors/extractArchiveTarget'; +const LABELMAP_PALETTE_2_1_0 = { + '0': { + value: 0, + name: 'Segment 0', + color: [0, 0, 0, 0], + }, + '1': { + value: 1, + name: 'Segment 1', + color: [153, 153, 0, 255], + }, + '2': { + value: 2, + name: 'Segment 2', + color: [76, 76, 0, 255], + }, + '3': { + value: 3, + name: 'Segment 3', + color: [255, 255, 0, 255], + }, + '4': { + value: 4, + name: 'Segment 4', + color: [0, 76, 0, 255], + }, + '5': { + value: 5, + name: 'Segment 5', + color: [0, 153, 0, 255], + }, + '6': { + value: 6, + name: 'Segment 6', + color: [0, 255, 0, 255], + }, + '7': { + value: 7, + name: 'Segment 7', + color: [76, 0, 0, 255], + }, + '8': { + value: 8, + name: 'Segment 8', + color: [153, 0, 0, 255], + }, + '9': { + value: 9, + name: 'Segment 9', + color: [255, 0, 0, 255], + }, + '10': { + value: 10, + name: 'Segment 10', + color: [0, 76, 76, 255], + }, + '11': { + value: 11, + name: 'Segment 11', + color: [0, 153, 153, 255], + }, + '12': { + value: 12, + name: 'Segment 12', + color: [0, 255, 255, 255], + }, + '13': { + value: 13, + name: 'Segment 13', + color: [0, 0, 76, 255], + }, + '14': { + value: 14, + name: 'Segment 14', + color: [0, 0, 153, 255], + }, +}; + +const OLD_MANIFEST_VERSIONS = ['2.1.0', '1.1.0', '1.0.0', '0.5.0']; + +const migrateManifest = (manifestString: string) => { + const inputManifest = JSON.parse(manifestString); + + if ( + OLD_MANIFEST_VERSIONS.some( + (oldVersion) => oldVersion === inputManifest.version + ) + ) { + inputManifest.tools.paint.activeSegmentGroupID = + inputManifest.tools.paint.activeLabelmapID; + delete inputManifest.tools.paint.activeLabelmapID; + + const order = Object.keys(LABELMAP_PALETTE_2_1_0).map((key) => Number(key)); + inputManifest.labelMaps = inputManifest.labelMaps.map( + (labelMap: any, index: number) => ({ + id: labelMap.id, + path: labelMap.path, + metadata: { + parentImage: labelMap.parent, + name: makeDefaultSegmentGroupName('My Image', index), + segments: { + order, + byValue: LABELMAP_PALETTE_2_1_0, + }, + }, + }) + ); + + inputManifest.version = '3.0.0'; + } + + return inputManifest; +}; + const resolveUriSource: ImportHandler = async (dataSource, { extra, done }) => { const { uriSrc } = dataSource; @@ -234,9 +351,9 @@ const restoreStateFile: ImportHandler = async ( throw new Error('State file does not have exactly 1 manifest'); } - const manifest = ManifestSchema.parse( - JSON.parse(await manifests[0].file.text()) - ); + const manifestString = await manifests[0].file.text(); + const migrated = migrateManifest(manifestString); + const manifest = ManifestSchema.parse(migrated); // We restore the view first, so that the appropriate watchers are triggered // in the views as the data is loaded diff --git a/tests/fixtures/toolsProstate.volview.json b/tests/fixtures/tools-prostate.3-0-0.volview.json similarity index 100% rename from tests/fixtures/toolsProstate.volview.json rename to tests/fixtures/tools-prostate.3-0-0.volview.json diff --git a/tests/specs/remote-manifest.e2e.ts b/tests/specs/remote-manifest.e2e.ts index c167cec0f..e76f282ee 100644 --- a/tests/specs/remote-manifest.e2e.ts +++ b/tests/specs/remote-manifest.e2e.ts @@ -3,6 +3,7 @@ import * as fs from 'fs'; import { cleanuptotal } from 'wdio-cleanuptotal-service'; import { TEMP_DIR } from '../../wdio.shared.conf'; import { volViewPage } from '../pageobjects/volview.page'; +import { downloadFile } from './utils'; async function writeManifestToFile(manifest: any, fileName: string) { const filePath = path.join(TEMP_DIR, fileName); @@ -32,15 +33,10 @@ describe('VolView loading of remoteManifest.json', () => { it('should load relative URI with no name property', async () => { const dicom = '1-001.dcm'; - const dicomPath = path.join(TEMP_DIR, dicom); - if (!fs.existsSync(dicomPath)) { - const response = await fetch( - 'https://data.kitware.com/api/v1/file/655d42a694ef39bf0a4a8bb3/download' - ); - const data = await response.arrayBuffer(); - const buffer = Buffer.from(data); - fs.writeFileSync(dicomPath, buffer); - } + await downloadFile( + 'https://data.kitware.com/api/v1/file/655d42a694ef39bf0a4a8bb3/download', + dicom + ); const manifest = { resources: [{ url: `/tmp/${dicom}` }], diff --git a/tests/specs/state-manifest.e2e.ts b/tests/specs/state-manifest.e2e.ts index 9242018ef..da6e19825 100644 --- a/tests/specs/state-manifest.e2e.ts +++ b/tests/specs/state-manifest.e2e.ts @@ -4,32 +4,54 @@ import { cleanuptotal } from 'wdio-cleanuptotal-service'; import JSZip from 'jszip'; import { FIXTURES, TEMP_DIR } from '../../wdio.shared.conf'; import { volViewPage } from '../pageobjects/volview.page'; -// import toolsProstateJSON from '/fixtures/toolsProstate.volview.json'; +import { downloadFile } from './utils'; + +async function writeManifestToZip(manifestPath: string, fileName: string) { + const filePath = path.join(TEMP_DIR, fileName); + const manifest = fs.readFileSync(manifestPath); + + const zip = new JSZip(); + zip.file('manifest.json', manifest); + const data = await zip.generateAsync({ type: 'nodebuffer' }); + + await fs.promises.writeFile(filePath, data); + cleanuptotal.addCleanup(async () => { + fs.unlinkSync(filePath); + }); + + return filePath; +} + +async function openVolViewPage(fileName: string) { + const urlParams = `?urls=[tmp/${fileName}]`; + await volViewPage.open(urlParams); + await volViewPage.waitForViews(); + + const notifications = await volViewPage.getNotificationsCount(); + expect(notifications).toEqual(0); +} describe('State file manifest.json code', () => { it('has no errors loading version 3.0.0 manifest.json file ', async () => { - // write json to zip file in temp dir - const fileName = 'asdf.zip'; - const filePath = path.join(TEMP_DIR, fileName); - - const manifest = fs.readFileSync( - path.join(FIXTURES, 'toolsProstate.volview.json') + const manifestPath = path.join( + FIXTURES, + 'tools-prostate.3-0-0.volview.json' ); + const fileName = 'temp-session.volview.zip'; + await writeManifestToZip(manifestPath, fileName); + await openVolViewPage(fileName); + }); - const zip = new JSZip(); - zip.file('manifest.json', manifest); - const data = await zip.generateAsync({ type: 'nodebuffer' }); - - await fs.promises.writeFile(filePath, data); - cleanuptotal.addCleanup(async () => { - fs.unlinkSync(filePath); - }); + // Dev test + // http://localhost:8080/?&urls=[http://localhost:9999/session.volview-2-1-0-labelmap-tools.zip] + it('has no errors loading version 2.1.0 manifest.json file ', async () => { + const FILE_NAME = 'session.volview-2-1-0-labelmap-tools.zip'; - const urlParams = `?urls=[tmp/${fileName}]`; - await volViewPage.open(urlParams); - await volViewPage.waitForViews(); + await downloadFile( + 'https://data.kitware.com/api/v1/file/6566acb6c5a2b36857ad1786/download', + FILE_NAME + ); - const notifications = await volViewPage.getNotificationsCount(); - expect(notifications).toEqual(0); + await openVolViewPage(FILE_NAME); }); }); diff --git a/tests/specs/utils.ts b/tests/specs/utils.ts new file mode 100644 index 000000000..f48424353 --- /dev/null +++ b/tests/specs/utils.ts @@ -0,0 +1,15 @@ +import * as path from 'path'; +import * as fs from 'fs'; +import { TEMP_DIR } from '../../wdio.shared.conf'; + +// File is not automatically deleted +export const downloadFile = async (url: string, fileName: string) => { + const savePath = path.join(TEMP_DIR, fileName); + if (!fs.existsSync(savePath)) { + const response = await fetch(url); + const data = await response.arrayBuffer(); + const buffer = Buffer.from(data); + fs.writeFileSync(savePath, buffer); + } + return savePath; +}; diff --git a/wdio.shared.conf.ts b/wdio.shared.conf.ts index aa9e06b52..376bf91b3 100644 --- a/wdio.shared.conf.ts +++ b/wdio.shared.conf.ts @@ -10,8 +10,8 @@ export const TEST_PORT = 4567; export const DOWNLOAD_TIMEOUT = Number(process.env.DOWNLOAD_TIMEOUT ?? 5000); const ROOT = projectRoot(); -// TEMP_DIR is also downloads directory const TMP = '.tmp/'; +// TEMP_DIR is also browser downloads directory export const TEMP_DIR = path.resolve(ROOT, TMP); const FIXTURES_DIR = 'tests/fixtures/'; export const FIXTURES = path.resolve(ROOT, FIXTURES_DIR); From f5ae4e9a8343932a9c3041584043a49c722d6dbe Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Wed, 29 Nov 2023 16:16:08 -0500 Subject: [PATCH 4/5] refactor(restoreStateFile): pipe manifest migration funcs --- src/io/import/processors/restoreStateFile.ts | 79 +++++++++++--------- src/utils/index.ts | 37 +++++++++ 2 files changed, 81 insertions(+), 35 deletions(-) diff --git a/src/io/import/processors/restoreStateFile.ts b/src/io/import/processors/restoreStateFile.ts index f7f12c287..093540b5c 100644 --- a/src/io/import/processors/restoreStateFile.ts +++ b/src/io/import/processors/restoreStateFile.ts @@ -14,7 +14,7 @@ import { fileToDataSource, } from '@/src/io/import/dataSource'; import { MANIFEST, isStateFile } from '@/src/io/state-file'; -import { ensureError, partition } from '@/src/utils'; +import { ensureError, partition, pipe } from '@/src/utils'; import Pipeline, { PipelineContext } from '@/src/core/pipeline'; import { Awaitable } from '@vueuse/core'; import doneWithDataSource from '@/src/io/import/processors/doneWithDataSource'; @@ -38,11 +38,6 @@ import updateFileMimeType from '@/src/io/import/processors/updateFileMimeType'; import extractArchiveTarget from '@/src/io/import/processors/extractArchiveTarget'; const LABELMAP_PALETTE_2_1_0 = { - '0': { - value: 0, - name: 'Segment 0', - color: [0, 0, 0, 0], - }, '1': { value: 1, name: 'Segment 1', @@ -115,40 +110,54 @@ const LABELMAP_PALETTE_2_1_0 = { }, }; -const OLD_MANIFEST_VERSIONS = ['2.1.0', '1.1.0', '1.0.0', '0.5.0']; +const migrateOrPass = + (versions: Array, migrationFunc: (manifest: any) => any) => + (inputManifest: any) => { + if (versions.includes(inputManifest.version)) { + return migrationFunc(inputManifest); + } + return inputManifest; + }; -const migrateManifest = (manifestString: string) => { - const inputManifest = JSON.parse(manifestString); +const migrateBefore210 = (inputManifest: any) => { + const manifest = JSON.parse(JSON.stringify(inputManifest)); + manifest.version = '2.1.0'; + return manifest; +}; - if ( - OLD_MANIFEST_VERSIONS.some( - (oldVersion) => oldVersion === inputManifest.version - ) - ) { - inputManifest.tools.paint.activeSegmentGroupID = - inputManifest.tools.paint.activeLabelmapID; - delete inputManifest.tools.paint.activeLabelmapID; - - const order = Object.keys(LABELMAP_PALETTE_2_1_0).map((key) => Number(key)); - inputManifest.labelMaps = inputManifest.labelMaps.map( - (labelMap: any, index: number) => ({ - id: labelMap.id, - path: labelMap.path, - metadata: { - parentImage: labelMap.parent, - name: makeDefaultSegmentGroupName('My Image', index), - segments: { - order, - byValue: LABELMAP_PALETTE_2_1_0, - }, +const migrate210To300 = (inputManifest: any) => { + const manifest = JSON.parse(JSON.stringify(inputManifest)); + manifest.tools.paint.activeSegmentGroupID = + inputManifest.tools.paint.activeLabelmapID; + delete manifest.tools.paint.activeLabelmapID; + + const order = Object.keys(LABELMAP_PALETTE_2_1_0).map((key) => Number(key)); + manifest.labelMaps = inputManifest.labelMaps.map( + (labelMap: any, index: number) => ({ + id: labelMap.id, + path: labelMap.path, + metadata: { + parentImage: labelMap.parent, + name: makeDefaultSegmentGroupName('My Image', index), + segments: { + order, + byValue: LABELMAP_PALETTE_2_1_0, }, - }) - ); + }, + }) + ); - inputManifest.version = '3.0.0'; - } + manifest.version = '3.0.0'; + return manifest; +}; - return inputManifest; +const migrateManifest = (manifestString: string) => { + const inputManifest = JSON.parse(manifestString); + return pipe( + inputManifest, + migrateOrPass(['1.1.0', '1.0.0', '0.5.0'], migrateBefore210), + migrateOrPass(['2.1.0'], migrate210To300) + ); }; const resolveUriSource: ImportHandler = async (dataSource, { extra, done }) => { diff --git a/src/utils/index.ts b/src/utils/index.ts index 28a83ac51..b4722bde0 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -326,3 +326,40 @@ export function normalizeForStore(objects: T[], key: K) { return { order, byKey }; } + +// Pipe code from +// https://dev.to/ecyrbe/how-to-use-advanced-typescript-to-define-a-pipe-function-381h +type AnyFunc = (...arg: any) => any; + +type LastFnReturnType, Else = never> = F extends [ + ...any[], + (...arg: any) => infer R +] + ? R + : Else; + +type PipeArgs = F extends [ + (...args: infer A) => infer B +] + ? [...Acc, (...args: A) => B] + : F extends [(...args: infer A) => any, ...infer Tail] + ? Tail extends [(arg: infer B) => any, ...any[]] + ? PipeArgs B]> + : Acc + : Acc; + +// Example: +// const valid = pipe( +// "1", +// (a: string) => Number(a), +// (c: number) => c + 1, +// (d: number) => `${d}`, +// (e: string) => Number(e) +// ); +export function pipe( + arg: Parameters[0], + firstFn: FirstFn, + ...fns: PipeArgs extends F ? F : PipeArgs +): LastFnReturnType> { + return (fns as AnyFunc[]).reduce((acc, fn) => fn(acc), firstFn(arg)); +} From f58d5f20a570392398e99a5778dcf35028d77d4d Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Thu, 30 Nov 2023 13:43:49 -0500 Subject: [PATCH 5/5] refactor(functional): move pipe function, rename old pipe to flow --- src/core/remote/client.ts | 11 ++--- src/io/import/processors/restoreStateFile.ts | 3 +- src/utils/functional.ts | 42 ++++++++++++++++++++ src/utils/index.ts | 37 ----------------- src/utils/pipe.ts | 5 --- 5 files changed, 47 insertions(+), 51 deletions(-) create mode 100644 src/utils/functional.ts delete mode 100644 src/utils/pipe.ts diff --git a/src/core/remote/client.ts b/src/core/remote/client.ts index f13babdae..2ee649751 100644 --- a/src/core/remote/client.ts +++ b/src/core/remote/client.ts @@ -7,7 +7,7 @@ import { import { Maybe } from '@/src/types'; import { Deferred, defer } from '@/src/utils'; import { debug } from '@/src/utils/loggers'; -import pipe from '@/src/utils/pipe'; +import { flow } from '@/src/utils/functional'; import { nanoid } from 'nanoid'; import { Socket, io } from 'socket.io-client'; import { z } from 'zod'; @@ -160,13 +160,8 @@ export default class RpcClient { this.socket.on(STREAM_RESULT_EVENT, this.onStreamResultEvent); } - protected serialize = (obj: any) => { - return pipe(...this.serializers)(obj); - }; - - protected deserialize = (obj: any) => { - return pipe(...this.deserializers)(obj); - }; + protected serialize = flow(...this.serializers); + protected deserialize = flow(...this.deserializers); async connect(uri: string) { await this.disconnect(); diff --git a/src/io/import/processors/restoreStateFile.ts b/src/io/import/processors/restoreStateFile.ts index 093540b5c..ad6157c1c 100644 --- a/src/io/import/processors/restoreStateFile.ts +++ b/src/io/import/processors/restoreStateFile.ts @@ -14,7 +14,8 @@ import { fileToDataSource, } from '@/src/io/import/dataSource'; import { MANIFEST, isStateFile } from '@/src/io/state-file'; -import { ensureError, partition, pipe } from '@/src/utils'; +import { ensureError, partition } from '@/src/utils'; +import { pipe } from '@/src/utils/functional'; import Pipeline, { PipelineContext } from '@/src/core/pipeline'; import { Awaitable } from '@vueuse/core'; import doneWithDataSource from '@/src/io/import/processors/doneWithDataSource'; diff --git a/src/utils/functional.ts b/src/utils/functional.ts new file mode 100644 index 000000000..3f27df22b --- /dev/null +++ b/src/utils/functional.ts @@ -0,0 +1,42 @@ +type FlowFunction = (o: T) => T; + +export function flow(...fns: Array>) { + return (input: T) => fns.reduce((result, fn) => fn(result), input); +} + +// Pipe code from +// https://dev.to/ecyrbe/how-to-use-advanced-typescript-to-define-a-pipe-function-381h +// Changed second parameter to rest/spread argument. +type AnyFunc = (...arg: any) => any; + +type LastFnReturnType, Else = never> = F extends [ + ...any[], + (...arg: any) => infer R +] + ? R + : Else; + +type PipeArgs = F extends [ + (...args: infer A) => infer B +] + ? [...Acc, (...args: A) => B] + : F extends [(...args: infer A) => any, ...infer Tail] + ? Tail extends [(arg: infer B) => any, ...any[]] + ? PipeArgs B]> + : Acc + : Acc; + +// Example: +// const myNumber = pipe( +// "1", +// (a: string) => Number(a), +// (c: number) => c + 1, +// (d: number) => `${d}`, +// (e: string) => Number(e) +// ); +export function pipe( + arg: Parameters[0], + ...fns: PipeArgs extends F ? F : PipeArgs +): LastFnReturnType> { + return (fns.slice(1) as AnyFunc[]).reduce((acc, fn) => fn(acc), fns[0](arg)); +} diff --git a/src/utils/index.ts b/src/utils/index.ts index b4722bde0..28a83ac51 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -326,40 +326,3 @@ export function normalizeForStore(objects: T[], key: K) { return { order, byKey }; } - -// Pipe code from -// https://dev.to/ecyrbe/how-to-use-advanced-typescript-to-define-a-pipe-function-381h -type AnyFunc = (...arg: any) => any; - -type LastFnReturnType, Else = never> = F extends [ - ...any[], - (...arg: any) => infer R -] - ? R - : Else; - -type PipeArgs = F extends [ - (...args: infer A) => infer B -] - ? [...Acc, (...args: A) => B] - : F extends [(...args: infer A) => any, ...infer Tail] - ? Tail extends [(arg: infer B) => any, ...any[]] - ? PipeArgs B]> - : Acc - : Acc; - -// Example: -// const valid = pipe( -// "1", -// (a: string) => Number(a), -// (c: number) => c + 1, -// (d: number) => `${d}`, -// (e: string) => Number(e) -// ); -export function pipe( - arg: Parameters[0], - firstFn: FirstFn, - ...fns: PipeArgs extends F ? F : PipeArgs -): LastFnReturnType> { - return (fns as AnyFunc[]).reduce((acc, fn) => fn(acc), firstFn(arg)); -} diff --git a/src/utils/pipe.ts b/src/utils/pipe.ts deleted file mode 100644 index a41065412..000000000 --- a/src/utils/pipe.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type PipeFunction = (o: T) => T; - -export default function pipe(...fns: Array>) { - return (input: T) => fns.reduce((result, fn) => fn(result), input); -}