From 61c39cfa2f874cd9983efe745546652cb440a613 Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Mon, 21 Sep 2026 10:27:20 -0400 Subject: [PATCH 1/9] test(e2e): download every dataset before the specs start Specs and the two 5.0.1 session fixtures fetched from data.kitware.com while a test timer was running, so a slow host failed whichever spec happened to be waiting. All datasets now live in tests/datasets.ts, are downloaded once in onPrepare, and are read from the local server. A failed download aborts the run and names the dataset. CI caches the downloads. --- .github/workflows/e2e.yml | 5 ++ tests/datasets.ts | 81 +++++++++++++++++ tests/fixtures/layer-axial.5-0-1.volview.json | 2 +- .../fixtures/pre-multi-4up.5-0-1.volview.json | 2 +- tests/server/content-disposition-server.ts | 5 +- tests/specs/annotationTestUtils.ts | 2 +- tests/specs/automatic-layering.e2e.ts | 4 +- tests/specs/cine-drag-onto-slot.e2e.ts | 5 +- tests/specs/cine-rendering.e2e.ts | 8 +- tests/specs/cine-ruler-mid-placement.e2e.ts | 2 +- tests/specs/cine-ruler-session.e2e.ts | 2 +- tests/specs/configTestUtils.ts | 74 ++------------- tests/specs/content-disposition.e2e.ts | 7 -- tests/specs/crosshairs-multi-image.e2e.ts | 2 +- .../specs/different-direction-labelmap.e2e.ts | 11 +-- tests/specs/layers-and-rendering.e2e.ts | 2 +- tests/specs/layout-config.e2e.ts | 3 +- tests/specs/reference-lines.e2e.ts | 2 +- tests/specs/remote-manifest.e2e.ts | 6 +- tests/specs/reveal-slice.e2e.ts | 5 +- tests/specs/seg-nrrd-export.e2e.ts | 3 +- tests/specs/segment-group-download.e2e.ts | 2 +- tests/specs/segment-group-list-scroll.e2e.ts | 2 +- tests/specs/session-state-lifecycle.e2e.ts | 10 +-- .../sparse-manifest-prostate-rectangle.e2e.ts | 6 +- tests/specs/sparse-manifest.e2e.ts | 18 +--- tests/specs/ultrasound-spacing.e2e.ts | 2 +- tests/specs/utils.ts | 50 +---------- tests/specs/vtk-interactor-lifecycle.e2e.ts | 2 +- tests/specs/windowing-config.e2e.ts | 7 +- wdio.shared.conf.ts | 89 +++++-------------- 31 files changed, 160 insertions(+), 261 deletions(-) create mode 100644 tests/datasets.ts diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 4993780ad..854b8ef23 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -29,6 +29,11 @@ jobs: run: npm ci env: DETECT_CHROMEDRIVER_VERSION: true + - name: Cache test datasets + uses: actions/cache@v4 + with: + path: .tmp/datasets + key: e2e-datasets-${{ hashFiles('tests/datasets.ts') }} - name: Install xvfb (linux) if: matrix.os == 'ubuntu-latest' run: sudo apt-get install xvfb diff --git a/tests/datasets.ts b/tests/datasets.ts new file mode 100644 index 000000000..67d006d9b --- /dev/null +++ b/tests/datasets.ts @@ -0,0 +1,81 @@ +// Every file the suite fetches from the internet. The runner downloads them +// into the dataset cache before any spec starts, and specs read them from the +// local server at `/tmp/`. CI keys its dataset cache on this file. + +export const ONE_CT_SLICE_DICOM = { + url: 'https://data.kitware.com/api/v1/file/6566aa81c5a2b36857ad1783/download', + name: 'CT000085.dcm', +} as const; + +export const MINIMAL_DICOM = { + url: 'https://data.kitware.com/api/v1/file/68e9807dbf0f869935e36481/download', + name: 'minimal.dcm', +} as const; + +export const MINIMAL_501_SESSION = { + url: 'https://data.kitware.com/api/v1/file/693077d880eaefe49a4abb88/download', + name: 'minimal-501-session.volview.zip', +} as const; + +export const PROSTATEX_DATASET = { + url: 'https://data.kitware.com/api/v1/item/63527c7311dab8142820a338/download', + name: 'prostate.zip', +} as const; + +export const PROSTATE_SEGMENT_GROUP = { + url: 'https://data.kitware.com/api/v1/file/692f13ed80eaefe49a4abb72/download', + name: 'prostate-total.seg.nii.gz', +} as const; + +export const MRA_HEAD_NECK_DATASET = { + url: 'https://data.kitware.com/api/v1/item/6352a2b311dab8142820a33b/download', + name: 'MRA-Head_and_Neck.zip', +} as const; + +export const FETUS_DATASET = { + url: 'https://data.kitware.com/api/v1/item/635679c311dab8142820a4f4/download', + name: 'fetus.zip', +} as const; + +// Multiframe ultrasound DICOM from pydicom public test data. +// SequenceOfUltrasoundRegions: PhysicalDeltaX/Y = 0.05104970559 cm/pixel +// (unit code 3 = cm), so with US spacing fix the VTK spacing is ~0.5105 mm. +export const US_MULTIFRAME_DICOM = { + url: 'https://data.kitware.com/api/v1/file/69e1630646ef98a20f563020/download', + name: 'US_multiframe_30frames.dcm', +} as const; + +// 8-frame echocardiogram from the BSD-licensed GDCM test corpus. Native +// Explicit VR LE, MONOCHROME2, retired Ultrasound Multi-frame SOP UID. +export const CINE_US_DATASET = { + url: 'https://sourceforge.net/p/gdcm/gdcmdata/ci/master/tree/US-MONO2-8-8x-execho.dcm?format=raw', + name: 'US-MONO2-8-8x-echo.dcm', +} as const; + +// 120-frame RGB ultrasound cine encoded as JPEG Baseline. Exercises the async +// browser JPEG decode path used by common compressed ultrasound clips. +export const COLOR3D_JPEG_BASELINE_DICOM = { + url: 'https://raw.githubusercontent.com/pydicom/pydicom-data/master/data_store/data/color3d_jpeg_baseline.dcm', + name: 'color3d_jpeg_baseline.dcm', +} as const; + +export const CT_ELECTRODES = { + url: 'https://raw.githubusercontent.com/neurolabusc/niivue-images/main/CT_Electrodes.nii.gz', + name: 'CT_Electrodes.nii.gz', +} as const; + +export const TEST_DATASETS = [ + ONE_CT_SLICE_DICOM, + MINIMAL_DICOM, + MINIMAL_501_SESSION, + PROSTATEX_DATASET, + PROSTATE_SEGMENT_GROUP, + MRA_HEAD_NECK_DATASET, + FETUS_DATASET, + US_MULTIFRAME_DICOM, + CINE_US_DATASET, + COLOR3D_JPEG_BASELINE_DICOM, + CT_ELECTRODES, +] as const; + +export type TestDataset = (typeof TEST_DATASETS)[number]; diff --git a/tests/fixtures/layer-axial.5-0-1.volview.json b/tests/fixtures/layer-axial.5-0-1.volview.json index 2a83b40ce..da7ca7a0d 100644 --- a/tests/fixtures/layer-axial.5-0-1.volview.json +++ b/tests/fixtures/layer-axial.5-0-1.volview.json @@ -1 +1 @@ -{"version":"5.0.1","datasets":[{"id":"1.3.6.1.4.1.14519.5.2.1.7311.5101.206828891270520544417996275680.5tse2d1254.538438420111018.1D000000SN0D000000S0D000000S0D000000S0D970296SN0D241922","dataSourceId":1},{"id":"1.3.6.1.4.1.5962.1.1.0.0.0.1196533885.18148.0.21.31.200000e0051251220030505.1D000000S0D001152SN0D001332SN0D001144S1D000000S0D006144","dataSourceId":22}],"dataSources":[{"id":2,"type":"uri","name":"MRI-PROSTATEx-0004.zip","uri":"https://data.kitware.com/api/v1/item/63527c7311dab8142820a338/download"},{"id":3,"type":"archive","path":"MRI-PROSTATEx-0004/1-19.dcm","parent":2},{"id":4,"type":"archive","path":"MRI-PROSTATEx-0004/1-18.dcm","parent":2},{"id":5,"type":"archive","path":"MRI-PROSTATEx-0004/1-17.dcm","parent":2},{"id":6,"type":"archive","path":"MRI-PROSTATEx-0004/1-16.dcm","parent":2},{"id":7,"type":"archive","path":"MRI-PROSTATEx-0004/1-15.dcm","parent":2},{"id":8,"type":"archive","path":"MRI-PROSTATEx-0004/1-14.dcm","parent":2},{"id":9,"type":"archive","path":"MRI-PROSTATEx-0004/1-13.dcm","parent":2},{"id":10,"type":"archive","path":"MRI-PROSTATEx-0004/1-12.dcm","parent":2},{"id":11,"type":"archive","path":"MRI-PROSTATEx-0004/1-11.dcm","parent":2},{"id":12,"type":"archive","path":"MRI-PROSTATEx-0004/1-10.dcm","parent":2},{"id":13,"type":"archive","path":"MRI-PROSTATEx-0004/1-09.dcm","parent":2},{"id":14,"type":"archive","path":"MRI-PROSTATEx-0004/1-08.dcm","parent":2},{"id":15,"type":"archive","path":"MRI-PROSTATEx-0004/1-07.dcm","parent":2},{"id":16,"type":"archive","path":"MRI-PROSTATEx-0004/1-06.dcm","parent":2},{"id":17,"type":"archive","path":"MRI-PROSTATEx-0004/1-05.dcm","parent":2},{"id":18,"type":"archive","path":"MRI-PROSTATEx-0004/1-04.dcm","parent":2},{"id":19,"type":"archive","path":"MRI-PROSTATEx-0004/1-03.dcm","parent":2},{"id":20,"type":"archive","path":"MRI-PROSTATEx-0004/1-02.dcm","parent":2},{"id":21,"type":"archive","path":"MRI-PROSTATEx-0004/1-01.dcm","parent":2},{"id":1,"type":"collection","sources":[3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21]},{"id":23,"type":"uri","name":"MRA-Head_and_Neck.zip","uri":"https://data.kitware.com/api/v1/item/6352a2b311dab8142820a33b/download"},{"id":24,"type":"archive","path":"MRA-Head_and_Neck/4016","parent":23},{"id":25,"type":"archive","path":"MRA-Head_and_Neck/4046","parent":23},{"id":26,"type":"archive","path":"MRA-Head_and_Neck/3956","parent":23},{"id":27,"type":"archive","path":"MRA-Head_and_Neck/3986","parent":23},{"id":28,"type":"archive","path":"MRA-Head_and_Neck/3925","parent":23},{"id":29,"type":"archive","path":"MRA-Head_and_Neck/25492","parent":23},{"id":30,"type":"archive","path":"MRA-Head_and_Neck/25432","parent":23},{"id":31,"type":"archive","path":"MRA-Head_and_Neck/25402","parent":23},{"id":32,"type":"archive","path":"MRA-Head_and_Neck/25462","parent":23},{"id":33,"type":"archive","path":"MRA-Head_and_Neck/25372","parent":23},{"id":34,"type":"archive","path":"MRA-Head_and_Neck/25312","parent":23},{"id":35,"type":"archive","path":"MRA-Head_and_Neck/25342","parent":23},{"id":36,"type":"archive","path":"MRA-Head_and_Neck/25282","parent":23},{"id":37,"type":"archive","path":"MRA-Head_and_Neck/25252","parent":23},{"id":38,"type":"archive","path":"MRA-Head_and_Neck/25162","parent":23},{"id":39,"type":"archive","path":"MRA-Head_and_Neck/25222","parent":23},{"id":40,"type":"archive","path":"MRA-Head_and_Neck/25132","parent":23},{"id":41,"type":"archive","path":"MRA-Head_and_Neck/25102","parent":23},{"id":42,"type":"archive","path":"MRA-Head_and_Neck/25072","parent":23},{"id":43,"type":"archive","path":"MRA-Head_and_Neck/25042","parent":23},{"id":44,"type":"archive","path":"MRA-Head_and_Neck/24892","parent":23},{"id":45,"type":"archive","path":"MRA-Head_and_Neck/24952","parent":23},{"id":46,"type":"archive","path":"MRA-Head_and_Neck/25012","parent":23},{"id":47,"type":"archive","path":"MRA-Head_and_Neck/24922","parent":23},{"id":48,"type":"archive","path":"MRA-Head_and_Neck/24982","parent":23},{"id":49,"type":"archive","path":"MRA-Head_and_Neck/24742","parent":23},{"id":50,"type":"archive","path":"MRA-Head_and_Neck/24802","parent":23},{"id":51,"type":"archive","path":"MRA-Head_and_Neck/24832","parent":23},{"id":52,"type":"archive","path":"MRA-Head_and_Neck/24772","parent":23},{"id":53,"type":"archive","path":"MRA-Head_and_Neck/24201","parent":23},{"id":54,"type":"archive","path":"MRA-Head_and_Neck/23570","parent":23},{"id":55,"type":"archive","path":"MRA-Head_and_Neck/24531","parent":23},{"id":56,"type":"archive","path":"MRA-Head_and_Neck/23871","parent":23},{"id":57,"type":"archive","path":"MRA-Head_and_Neck/22549","parent":23},{"id":58,"type":"archive","path":"MRA-Head_and_Neck/23240","parent":23},{"id":59,"type":"archive","path":"MRA-Head_and_Neck/22910","parent":23},{"id":60,"type":"archive","path":"MRA-Head_and_Neck/22008","parent":23},{"id":61,"type":"archive","path":"MRA-Head_and_Neck/22338","parent":23},{"id":62,"type":"archive","path":"MRA-Head_and_Neck/21348","parent":23},{"id":63,"type":"archive","path":"MRA-Head_and_Neck/21678","parent":23},{"id":64,"type":"archive","path":"MRA-Head_and_Neck/21048","parent":23},{"id":65,"type":"archive","path":"MRA-Head_and_Neck/20718","parent":23},{"id":66,"type":"archive","path":"MRA-Head_and_Neck/20388","parent":23},{"id":67,"type":"archive","path":"MRA-Head_and_Neck/20117","parent":23},{"id":68,"type":"archive","path":"MRA-Head_and_Neck/19817","parent":23},{"id":69,"type":"archive","path":"MRA-Head_and_Neck/19127","parent":23},{"id":70,"type":"archive","path":"MRA-Head_and_Neck/17860","parent":23},{"id":71,"type":"archive","path":"MRA-Head_and_Neck/19457","parent":23},{"id":72,"type":"archive","path":"MRA-Head_and_Neck/18797","parent":23},{"id":73,"type":"archive","path":"MRA-Head_and_Neck/18196","parent":23},{"id":74,"type":"archive","path":"MRA-Head_and_Neck/18466","parent":23},{"id":75,"type":"archive","path":"MRA-Head_and_Neck/15910","parent":23},{"id":76,"type":"archive","path":"MRA-Head_and_Neck/16870","parent":23},{"id":77,"type":"archive","path":"MRA-Head_and_Neck/16180","parent":23},{"id":78,"type":"archive","path":"MRA-Head_and_Neck/17530","parent":23},{"id":79,"type":"archive","path":"MRA-Head_and_Neck/16540","parent":23},{"id":80,"type":"archive","path":"MRA-Head_and_Neck/17200","parent":23},{"id":81,"type":"archive","path":"MRA-Head_and_Neck/15640","parent":23},{"id":82,"type":"archive","path":"MRA-Head_and_Neck/15310","parent":23},{"id":83,"type":"archive","path":"MRA-Head_and_Neck/14980","parent":23},{"id":84,"type":"archive","path":"MRA-Head_and_Neck/14320","parent":23},{"id":85,"type":"archive","path":"MRA-Head_and_Neck/14080","parent":23},{"id":86,"type":"archive","path":"MRA-Head_and_Neck/14650","parent":23},{"id":87,"type":"archive","path":"MRA-Head_and_Neck/13419","parent":23},{"id":88,"type":"archive","path":"MRA-Head_and_Neck/13750","parent":23},{"id":89,"type":"archive","path":"MRA-Head_and_Neck/12759","parent":23},{"id":90,"type":"archive","path":"MRA-Head_and_Neck/13089","parent":23},{"id":91,"type":"archive","path":"MRA-Head_and_Neck/12128","parent":23},{"id":92,"type":"archive","path":"MRA-Head_and_Neck/12458","parent":23},{"id":93,"type":"archive","path":"MRA-Head_and_Neck/11827","parent":23},{"id":94,"type":"archive","path":"MRA-Head_and_Neck/11497","parent":23},{"id":95,"type":"archive","path":"MRA-Head_and_Neck/11167","parent":23},{"id":96,"type":"archive","path":"MRA-Head_and_Neck/10866","parent":23},{"id":97,"type":"archive","path":"MRA-Head_and_Neck/9215","parent":23},{"id":98,"type":"archive","path":"MRA-Head_and_Neck/9875","parent":23},{"id":99,"type":"archive","path":"MRA-Head_and_Neck/10536","parent":23},{"id":100,"type":"archive","path":"MRA-Head_and_Neck/10206","parent":23},{"id":101,"type":"archive","path":"MRA-Head_and_Neck/9545","parent":23},{"id":102,"type":"archive","path":"MRA-Head_and_Neck/8885","parent":23},{"id":103,"type":"archive","path":"MRA-Head_and_Neck/7895","parent":23},{"id":104,"type":"archive","path":"MRA-Head_and_Neck/8555","parent":23},{"id":105,"type":"archive","path":"MRA-Head_and_Neck/8225","parent":23},{"id":106,"type":"archive","path":"MRA-Head_and_Neck/7565","parent":23},{"id":22,"type":"collection","sources":[24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106]}],"datasetFilePath":{},"labelMaps":[],"tools":{"crosshairs":{"position":[0,0,0]},"paint":{"activeSegmentGroupID":null,"activeSegment":null,"brushSize":4,"crossPlaneSync":false},"crop":{"1.3.6.1.4.1.14519.5.2.1.7311.5101.206828891270520544417996275680.5tse2d1254.538438420111018.1D000000SN0D000000S0D000000S0D000000S0D970296SN0D241922":{"Sagittal":[-0.5,383.5],"Coronal":[-0.5,383.5],"Axial":[-0.5,18.5]},"1.3.6.1.4.1.5962.1.1.0.0.0.1196533885.18148.0.21.31.200000e0051251220030505.1D000000S0D001152SN0D001332SN0D001144S1D000000S0D006144":{"Sagittal":[-0.5,511.5],"Coronal":[-0.5,511.5],"Axial":[-0.5,82.5]}},"current":"WindowLevel","polygons":{"tools":[],"labels":{"1":{"labelName":"Label 1","color":"red","strokeWidth":1}}},"rectangles":{"tools":[],"labels":{"3":{"labelName":"Label 1","color":"red","strokeWidth":1,"fillColor":"transparent"}}},"rulers":{"tools":[],"labels":{"2":{"labelName":"Label 1","color":"red","strokeWidth":1}}}},"layout":{"name":"Axial Only","direction":"H","items":["Axial"]},"views":[{"id":"Coronal","type":"2D","props":{"viewDirection":"Posterior","viewUp":"Superior"},"config":{"1.3.6.1.4.1.14519.5.2.1.7311.5101.206828891270520544417996275680.5tse2d1254.538438420111018.1D000000SN0D000000S0D000000S0D000000S0D970296SN0D241922":{"slice":{"slice":192,"min":0,"max":383,"axisDirection":"Posterior","syncState":false},"window":{"auto":"FullRange","useAuto":false,"width":1120,"level":549},"camera":{"position":[-22.163253907287167,-564.594223248674,174.1187338921456],"focalPoint":[-22.163253784231358,17.55373397057385,28.972948296001235],"viewUp":[1.4080733262381892e-17,0.24192188680171967,0.9702957272529602],"parallelScale":101.92947988832593,"directionOfProjection":[2.0510340073149801e-10,0.9702957270336714,-0.2419218925608004]}},"1.3.6.1.4.1.5962.1.1.0.0.0.1196533885.18148.0.21.31.200000e0051251220030505.1D000000S0D001152SN0D001332SN0D001144S1D000000S0D006144":{"slice":{"slice":256,"min":0,"max":511,"axisDirection":"Posterior","syncState":false},"window":{"auto":"FullRange","useAuto":false,"userTriggered":true,"width":81,"level":46},"camera":{"position":[-0.7825957138099202,-582.9333614467896,-5.548833820787268],"focalPoint":[-1.4482100259604564,-1.1179264052817643,-1.9742063331270465],"viewUp":[0.0013390391832217574,-0.00614239601418376,1.000001311302185],"parallelScale":106.17654155033951,"directionOfProjection":[-0.0011440076597175992,0.9999804722932084,0.0061438000233316895]}}}},{"id":"Sagittal","type":"2D","props":{"viewDirection":"Right","viewUp":"Superior"},"config":{"1.3.6.1.4.1.14519.5.2.1.7311.5101.206828891270520544417996275680.5tse2d1254.538438420111018.1D000000SN0D000000S0D000000S0D000000S0D970296SN0D241922":{"slice":{"slice":192,"min":0,"max":383,"axisDirection":"Right","syncState":false},"window":{"auto":"FullRange","useAuto":false,"width":1120,"level":549},"camera":{"position":[577.8063647556294,17.5537338511733,28.972948325771128],"focalPoint":[-22.163253784231358,17.55373397057385,28.972948296001235],"viewUp":[1.4080733262381892e-17,0.24192188680171967,0.9702957272529602],"parallelScale":101.92947988832593,"directionOfProjection":[-1,1.99010999481902e-10,-4.961900119157013e-11]}},"1.3.6.1.4.1.5962.1.1.0.0.0.1196533885.18148.0.21.31.200000e0051251220030505.1D000000S0D001152SN0D001332SN0D001144S1D000000S0D006144":{"slice":{"slice":256,"min":0,"max":511,"axisDirection":"Right","syncState":false},"window":{"auto":"FullRange","useAuto":false,"userTriggered":true,"width":81,"level":46},"camera":{"position":[580.3672250155474,-0.44754120656388774,-2.7491612199849333],"focalPoint":[-1.4482100259604564,-1.1179264052817643,-1.9742063331270465],"viewUp":[0.0013390391832217574,-0.00614239601418376,1.000001311302185],"parallelScale":106.17654155033951,"directionOfProjection":[-0.9999984491279005,-0.001152228213038641,0.0013319579343003985]}}}},{"id":"Axial","type":"2D","props":{"viewDirection":"Superior","viewUp":"Anterior"},"config":{"1.3.6.1.4.1.14519.5.2.1.7311.5101.206828891270520544417996275680.5tse2d1254.538438420111018.1D000000SN0D000000S0D000000S0D000000S0D970296SN0D241922":{"slice":{"slice":9,"min":0,"max":18,"axisDirection":"Superior","syncState":false},"window":{"auto":"FullRange","useAuto":false,"width":1120,"level":549},"camera":{"position":[-22.163253784231365,-127.59204817029726,-553.175009054814],"focalPoint":[-22.163253784231358,17.55373397057385,28.972948296001235],"viewUp":[-2.051034e-10,-0.97029572703367,0.2419218925608],"parallelScale":101.92947988832593,"directionOfProjection":[1.1842978621619476e-17,0.24192188708730178,0.9702957283983675]}},"1.3.6.1.4.1.5962.1.1.0.0.0.1196533885.18148.0.21.31.200000e0051251220030505.1D000000S0D001152SN0D001332SN0D001144S1D000000S0D006144":{"slice":{"slice":41,"min":0,"max":82,"axisDirection":"Superior","syncState":false},"window":{"auto":"FullRange","useAuto":false,"userTriggered":true,"width":81,"level":46},"camera":{"position":[-2.2272836908842484,2.4558144039077834,-583.7904043104862],"focalPoint":[-1.4482100259604564,-1.1179264052817643,-1.9742063331270465],"viewUp":[0.00114403,-1,-0.00614392],"parallelScale":106.17654155033951,"directionOfProjection":[0.0013390109674375123,-0.006142266583676531,0.9999802396101856]}}}},{"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":{"1.3.6.1.4.1.14519.5.2.1.7311.5101.206828891270520544417996275680.5tse2d1254.538438420111018.1D000000SN0D000000S0D000000S0D000000S0D970296SN0D241922":{"camera":{"position":[-22.163253907287167,-564.594223248674,174.1187338921456],"focalPoint":[-22.163253784231358,17.55373397057385,28.972948296001235],"viewUp":[1.4080733262381892e-17,0.24192188680171967,0.9702957272529602],"parallelScale":155.28356376101038,"directionOfProjection":[2.0510340073149801e-10,0.9702957270336714,-0.2419218925608004]},"volumeColorConfig":{"colorBy":{"arrayName":"vtkDataArray838","location":"pointData"},"transferFunction":{"preset":"CT-Coronary-Arteries-2","mappingRange":[-2048,3661]},"opacityFunction":{"mode":1,"gaussians":[],"mappingRange":[0,1432],"preset":"CT-Coronary-Arteries-2","shift":0,"shiftAlpha":0},"cvr":{"enabled":true,"lightFollowsCamera":true,"volumeQuality":2,"useVolumetricScatteringBlending":false,"volumetricScatteringBlending":0.5,"useLocalAmbientOcclusion":true,"laoKernelRadius":5,"laoKernelSize":15,"ambient":0.2,"diffuse":0.7,"specular":0.3}}},"1.3.6.1.4.1.5962.1.1.0.0.0.1196533885.18148.0.21.31.200000e0051251220030505.1D000000S0D001152SN0D001332SN0D001144S1D000000S0D006144":{"camera":{"position":[-0.7825957138099202,-582.9333614467896,-5.548833820787268],"focalPoint":[-1.4482100259604564,-1.1179264052817643,-1.9742063331270465],"viewUp":[0.0013390391832217574,-0.00614239601418376,1.000001311302185],"parallelScale":150.58491532335074,"directionOfProjection":[-0.0011440076597175992,0.9999804722932084,0.0061438000233316895]},"volumeColorConfig":{"colorBy":{"arrayName":"vtkDataArray1797","location":"pointData"},"transferFunction":{"preset":"CT-Coronary-Arteries-2","mappingRange":[-2048,3661]},"opacityFunction":{"mode":1,"gaussians":[],"mappingRange":[0,496],"preset":"CT-Coronary-Arteries-2","shift":0,"shiftAlpha":0},"cvr":{"enabled":true,"lightFollowsCamera":true,"volumeQuality":2,"useVolumetricScatteringBlending":false,"volumetricScatteringBlending":0.5,"useLocalAmbientOcclusion":true,"laoKernelRadius":5,"laoKernelSize":15,"ambient":0.2,"diffuse":0.7,"specular":0.3}}}}},{"id":"Oblique3D","type":"Oblique3D","props":{"viewDirection":"Posterior","viewUp":"Superior","slices":[{"viewID":"ObliqueSagittal","axis":"Sagittal"},{"viewID":"ObliqueCoronal","axis":"Coronal"},{"viewID":"ObliqueAxial","axis":"Axial"}]},"config":{}}],"parentToLayers":[{"selectionKey":"1.3.6.1.4.1.5962.1.1.0.0.0.1196533885.18148.0.21.31.200000e0051251220030505.1D000000S0D001152SN0D001332SN0D001144S1D000000S0D006144","sourceSelectionKeys":["1.3.6.1.4.1.14519.5.2.1.7311.5101.206828891270520544417996275680.5tse2d1254.538438420111018.1D000000SN0D000000S0D000000S0D000000S0D970296SN0D241922"]}],"primarySelection":"1.3.6.1.4.1.5962.1.1.0.0.0.1196533885.18148.0.21.31.200000e0051251220030505.1D000000S0D001152SN0D001332SN0D001144S1D000000S0D006144"} \ No newline at end of file +{"version":"5.0.1","datasets":[{"id":"1.3.6.1.4.1.14519.5.2.1.7311.5101.206828891270520544417996275680.5tse2d1254.538438420111018.1D000000SN0D000000S0D000000S0D000000S0D970296SN0D241922","dataSourceId":1},{"id":"1.3.6.1.4.1.5962.1.1.0.0.0.1196533885.18148.0.21.31.200000e0051251220030505.1D000000S0D001152SN0D001332SN0D001144S1D000000S0D006144","dataSourceId":22}],"dataSources":[{"id":2,"type":"uri","name":"MRI-PROSTATEx-0004.zip","uri":"/tmp/prostate.zip"},{"id":3,"type":"archive","path":"MRI-PROSTATEx-0004/1-19.dcm","parent":2},{"id":4,"type":"archive","path":"MRI-PROSTATEx-0004/1-18.dcm","parent":2},{"id":5,"type":"archive","path":"MRI-PROSTATEx-0004/1-17.dcm","parent":2},{"id":6,"type":"archive","path":"MRI-PROSTATEx-0004/1-16.dcm","parent":2},{"id":7,"type":"archive","path":"MRI-PROSTATEx-0004/1-15.dcm","parent":2},{"id":8,"type":"archive","path":"MRI-PROSTATEx-0004/1-14.dcm","parent":2},{"id":9,"type":"archive","path":"MRI-PROSTATEx-0004/1-13.dcm","parent":2},{"id":10,"type":"archive","path":"MRI-PROSTATEx-0004/1-12.dcm","parent":2},{"id":11,"type":"archive","path":"MRI-PROSTATEx-0004/1-11.dcm","parent":2},{"id":12,"type":"archive","path":"MRI-PROSTATEx-0004/1-10.dcm","parent":2},{"id":13,"type":"archive","path":"MRI-PROSTATEx-0004/1-09.dcm","parent":2},{"id":14,"type":"archive","path":"MRI-PROSTATEx-0004/1-08.dcm","parent":2},{"id":15,"type":"archive","path":"MRI-PROSTATEx-0004/1-07.dcm","parent":2},{"id":16,"type":"archive","path":"MRI-PROSTATEx-0004/1-06.dcm","parent":2},{"id":17,"type":"archive","path":"MRI-PROSTATEx-0004/1-05.dcm","parent":2},{"id":18,"type":"archive","path":"MRI-PROSTATEx-0004/1-04.dcm","parent":2},{"id":19,"type":"archive","path":"MRI-PROSTATEx-0004/1-03.dcm","parent":2},{"id":20,"type":"archive","path":"MRI-PROSTATEx-0004/1-02.dcm","parent":2},{"id":21,"type":"archive","path":"MRI-PROSTATEx-0004/1-01.dcm","parent":2},{"id":1,"type":"collection","sources":[3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21]},{"id":23,"type":"uri","name":"MRA-Head_and_Neck.zip","uri":"/tmp/MRA-Head_and_Neck.zip"},{"id":24,"type":"archive","path":"MRA-Head_and_Neck/4016","parent":23},{"id":25,"type":"archive","path":"MRA-Head_and_Neck/4046","parent":23},{"id":26,"type":"archive","path":"MRA-Head_and_Neck/3956","parent":23},{"id":27,"type":"archive","path":"MRA-Head_and_Neck/3986","parent":23},{"id":28,"type":"archive","path":"MRA-Head_and_Neck/3925","parent":23},{"id":29,"type":"archive","path":"MRA-Head_and_Neck/25492","parent":23},{"id":30,"type":"archive","path":"MRA-Head_and_Neck/25432","parent":23},{"id":31,"type":"archive","path":"MRA-Head_and_Neck/25402","parent":23},{"id":32,"type":"archive","path":"MRA-Head_and_Neck/25462","parent":23},{"id":33,"type":"archive","path":"MRA-Head_and_Neck/25372","parent":23},{"id":34,"type":"archive","path":"MRA-Head_and_Neck/25312","parent":23},{"id":35,"type":"archive","path":"MRA-Head_and_Neck/25342","parent":23},{"id":36,"type":"archive","path":"MRA-Head_and_Neck/25282","parent":23},{"id":37,"type":"archive","path":"MRA-Head_and_Neck/25252","parent":23},{"id":38,"type":"archive","path":"MRA-Head_and_Neck/25162","parent":23},{"id":39,"type":"archive","path":"MRA-Head_and_Neck/25222","parent":23},{"id":40,"type":"archive","path":"MRA-Head_and_Neck/25132","parent":23},{"id":41,"type":"archive","path":"MRA-Head_and_Neck/25102","parent":23},{"id":42,"type":"archive","path":"MRA-Head_and_Neck/25072","parent":23},{"id":43,"type":"archive","path":"MRA-Head_and_Neck/25042","parent":23},{"id":44,"type":"archive","path":"MRA-Head_and_Neck/24892","parent":23},{"id":45,"type":"archive","path":"MRA-Head_and_Neck/24952","parent":23},{"id":46,"type":"archive","path":"MRA-Head_and_Neck/25012","parent":23},{"id":47,"type":"archive","path":"MRA-Head_and_Neck/24922","parent":23},{"id":48,"type":"archive","path":"MRA-Head_and_Neck/24982","parent":23},{"id":49,"type":"archive","path":"MRA-Head_and_Neck/24742","parent":23},{"id":50,"type":"archive","path":"MRA-Head_and_Neck/24802","parent":23},{"id":51,"type":"archive","path":"MRA-Head_and_Neck/24832","parent":23},{"id":52,"type":"archive","path":"MRA-Head_and_Neck/24772","parent":23},{"id":53,"type":"archive","path":"MRA-Head_and_Neck/24201","parent":23},{"id":54,"type":"archive","path":"MRA-Head_and_Neck/23570","parent":23},{"id":55,"type":"archive","path":"MRA-Head_and_Neck/24531","parent":23},{"id":56,"type":"archive","path":"MRA-Head_and_Neck/23871","parent":23},{"id":57,"type":"archive","path":"MRA-Head_and_Neck/22549","parent":23},{"id":58,"type":"archive","path":"MRA-Head_and_Neck/23240","parent":23},{"id":59,"type":"archive","path":"MRA-Head_and_Neck/22910","parent":23},{"id":60,"type":"archive","path":"MRA-Head_and_Neck/22008","parent":23},{"id":61,"type":"archive","path":"MRA-Head_and_Neck/22338","parent":23},{"id":62,"type":"archive","path":"MRA-Head_and_Neck/21348","parent":23},{"id":63,"type":"archive","path":"MRA-Head_and_Neck/21678","parent":23},{"id":64,"type":"archive","path":"MRA-Head_and_Neck/21048","parent":23},{"id":65,"type":"archive","path":"MRA-Head_and_Neck/20718","parent":23},{"id":66,"type":"archive","path":"MRA-Head_and_Neck/20388","parent":23},{"id":67,"type":"archive","path":"MRA-Head_and_Neck/20117","parent":23},{"id":68,"type":"archive","path":"MRA-Head_and_Neck/19817","parent":23},{"id":69,"type":"archive","path":"MRA-Head_and_Neck/19127","parent":23},{"id":70,"type":"archive","path":"MRA-Head_and_Neck/17860","parent":23},{"id":71,"type":"archive","path":"MRA-Head_and_Neck/19457","parent":23},{"id":72,"type":"archive","path":"MRA-Head_and_Neck/18797","parent":23},{"id":73,"type":"archive","path":"MRA-Head_and_Neck/18196","parent":23},{"id":74,"type":"archive","path":"MRA-Head_and_Neck/18466","parent":23},{"id":75,"type":"archive","path":"MRA-Head_and_Neck/15910","parent":23},{"id":76,"type":"archive","path":"MRA-Head_and_Neck/16870","parent":23},{"id":77,"type":"archive","path":"MRA-Head_and_Neck/16180","parent":23},{"id":78,"type":"archive","path":"MRA-Head_and_Neck/17530","parent":23},{"id":79,"type":"archive","path":"MRA-Head_and_Neck/16540","parent":23},{"id":80,"type":"archive","path":"MRA-Head_and_Neck/17200","parent":23},{"id":81,"type":"archive","path":"MRA-Head_and_Neck/15640","parent":23},{"id":82,"type":"archive","path":"MRA-Head_and_Neck/15310","parent":23},{"id":83,"type":"archive","path":"MRA-Head_and_Neck/14980","parent":23},{"id":84,"type":"archive","path":"MRA-Head_and_Neck/14320","parent":23},{"id":85,"type":"archive","path":"MRA-Head_and_Neck/14080","parent":23},{"id":86,"type":"archive","path":"MRA-Head_and_Neck/14650","parent":23},{"id":87,"type":"archive","path":"MRA-Head_and_Neck/13419","parent":23},{"id":88,"type":"archive","path":"MRA-Head_and_Neck/13750","parent":23},{"id":89,"type":"archive","path":"MRA-Head_and_Neck/12759","parent":23},{"id":90,"type":"archive","path":"MRA-Head_and_Neck/13089","parent":23},{"id":91,"type":"archive","path":"MRA-Head_and_Neck/12128","parent":23},{"id":92,"type":"archive","path":"MRA-Head_and_Neck/12458","parent":23},{"id":93,"type":"archive","path":"MRA-Head_and_Neck/11827","parent":23},{"id":94,"type":"archive","path":"MRA-Head_and_Neck/11497","parent":23},{"id":95,"type":"archive","path":"MRA-Head_and_Neck/11167","parent":23},{"id":96,"type":"archive","path":"MRA-Head_and_Neck/10866","parent":23},{"id":97,"type":"archive","path":"MRA-Head_and_Neck/9215","parent":23},{"id":98,"type":"archive","path":"MRA-Head_and_Neck/9875","parent":23},{"id":99,"type":"archive","path":"MRA-Head_and_Neck/10536","parent":23},{"id":100,"type":"archive","path":"MRA-Head_and_Neck/10206","parent":23},{"id":101,"type":"archive","path":"MRA-Head_and_Neck/9545","parent":23},{"id":102,"type":"archive","path":"MRA-Head_and_Neck/8885","parent":23},{"id":103,"type":"archive","path":"MRA-Head_and_Neck/7895","parent":23},{"id":104,"type":"archive","path":"MRA-Head_and_Neck/8555","parent":23},{"id":105,"type":"archive","path":"MRA-Head_and_Neck/8225","parent":23},{"id":106,"type":"archive","path":"MRA-Head_and_Neck/7565","parent":23},{"id":22,"type":"collection","sources":[24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106]}],"datasetFilePath":{},"labelMaps":[],"tools":{"crosshairs":{"position":[0,0,0]},"paint":{"activeSegmentGroupID":null,"activeSegment":null,"brushSize":4,"crossPlaneSync":false},"crop":{"1.3.6.1.4.1.14519.5.2.1.7311.5101.206828891270520544417996275680.5tse2d1254.538438420111018.1D000000SN0D000000S0D000000S0D000000S0D970296SN0D241922":{"Sagittal":[-0.5,383.5],"Coronal":[-0.5,383.5],"Axial":[-0.5,18.5]},"1.3.6.1.4.1.5962.1.1.0.0.0.1196533885.18148.0.21.31.200000e0051251220030505.1D000000S0D001152SN0D001332SN0D001144S1D000000S0D006144":{"Sagittal":[-0.5,511.5],"Coronal":[-0.5,511.5],"Axial":[-0.5,82.5]}},"current":"WindowLevel","polygons":{"tools":[],"labels":{"1":{"labelName":"Label 1","color":"red","strokeWidth":1}}},"rectangles":{"tools":[],"labels":{"3":{"labelName":"Label 1","color":"red","strokeWidth":1,"fillColor":"transparent"}}},"rulers":{"tools":[],"labels":{"2":{"labelName":"Label 1","color":"red","strokeWidth":1}}}},"layout":{"name":"Axial Only","direction":"H","items":["Axial"]},"views":[{"id":"Coronal","type":"2D","props":{"viewDirection":"Posterior","viewUp":"Superior"},"config":{"1.3.6.1.4.1.14519.5.2.1.7311.5101.206828891270520544417996275680.5tse2d1254.538438420111018.1D000000SN0D000000S0D000000S0D000000S0D970296SN0D241922":{"slice":{"slice":192,"min":0,"max":383,"axisDirection":"Posterior","syncState":false},"window":{"auto":"FullRange","useAuto":false,"width":1120,"level":549},"camera":{"position":[-22.163253907287167,-564.594223248674,174.1187338921456],"focalPoint":[-22.163253784231358,17.55373397057385,28.972948296001235],"viewUp":[1.4080733262381892e-17,0.24192188680171967,0.9702957272529602],"parallelScale":101.92947988832593,"directionOfProjection":[2.0510340073149801e-10,0.9702957270336714,-0.2419218925608004]}},"1.3.6.1.4.1.5962.1.1.0.0.0.1196533885.18148.0.21.31.200000e0051251220030505.1D000000S0D001152SN0D001332SN0D001144S1D000000S0D006144":{"slice":{"slice":256,"min":0,"max":511,"axisDirection":"Posterior","syncState":false},"window":{"auto":"FullRange","useAuto":false,"userTriggered":true,"width":81,"level":46},"camera":{"position":[-0.7825957138099202,-582.9333614467896,-5.548833820787268],"focalPoint":[-1.4482100259604564,-1.1179264052817643,-1.9742063331270465],"viewUp":[0.0013390391832217574,-0.00614239601418376,1.000001311302185],"parallelScale":106.17654155033951,"directionOfProjection":[-0.0011440076597175992,0.9999804722932084,0.0061438000233316895]}}}},{"id":"Sagittal","type":"2D","props":{"viewDirection":"Right","viewUp":"Superior"},"config":{"1.3.6.1.4.1.14519.5.2.1.7311.5101.206828891270520544417996275680.5tse2d1254.538438420111018.1D000000SN0D000000S0D000000S0D000000S0D970296SN0D241922":{"slice":{"slice":192,"min":0,"max":383,"axisDirection":"Right","syncState":false},"window":{"auto":"FullRange","useAuto":false,"width":1120,"level":549},"camera":{"position":[577.8063647556294,17.5537338511733,28.972948325771128],"focalPoint":[-22.163253784231358,17.55373397057385,28.972948296001235],"viewUp":[1.4080733262381892e-17,0.24192188680171967,0.9702957272529602],"parallelScale":101.92947988832593,"directionOfProjection":[-1,1.99010999481902e-10,-4.961900119157013e-11]}},"1.3.6.1.4.1.5962.1.1.0.0.0.1196533885.18148.0.21.31.200000e0051251220030505.1D000000S0D001152SN0D001332SN0D001144S1D000000S0D006144":{"slice":{"slice":256,"min":0,"max":511,"axisDirection":"Right","syncState":false},"window":{"auto":"FullRange","useAuto":false,"userTriggered":true,"width":81,"level":46},"camera":{"position":[580.3672250155474,-0.44754120656388774,-2.7491612199849333],"focalPoint":[-1.4482100259604564,-1.1179264052817643,-1.9742063331270465],"viewUp":[0.0013390391832217574,-0.00614239601418376,1.000001311302185],"parallelScale":106.17654155033951,"directionOfProjection":[-0.9999984491279005,-0.001152228213038641,0.0013319579343003985]}}}},{"id":"Axial","type":"2D","props":{"viewDirection":"Superior","viewUp":"Anterior"},"config":{"1.3.6.1.4.1.14519.5.2.1.7311.5101.206828891270520544417996275680.5tse2d1254.538438420111018.1D000000SN0D000000S0D000000S0D000000S0D970296SN0D241922":{"slice":{"slice":9,"min":0,"max":18,"axisDirection":"Superior","syncState":false},"window":{"auto":"FullRange","useAuto":false,"width":1120,"level":549},"camera":{"position":[-22.163253784231365,-127.59204817029726,-553.175009054814],"focalPoint":[-22.163253784231358,17.55373397057385,28.972948296001235],"viewUp":[-2.051034e-10,-0.97029572703367,0.2419218925608],"parallelScale":101.92947988832593,"directionOfProjection":[1.1842978621619476e-17,0.24192188708730178,0.9702957283983675]}},"1.3.6.1.4.1.5962.1.1.0.0.0.1196533885.18148.0.21.31.200000e0051251220030505.1D000000S0D001152SN0D001332SN0D001144S1D000000S0D006144":{"slice":{"slice":41,"min":0,"max":82,"axisDirection":"Superior","syncState":false},"window":{"auto":"FullRange","useAuto":false,"userTriggered":true,"width":81,"level":46},"camera":{"position":[-2.2272836908842484,2.4558144039077834,-583.7904043104862],"focalPoint":[-1.4482100259604564,-1.1179264052817643,-1.9742063331270465],"viewUp":[0.00114403,-1,-0.00614392],"parallelScale":106.17654155033951,"directionOfProjection":[0.0013390109674375123,-0.006142266583676531,0.9999802396101856]}}}},{"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":{"1.3.6.1.4.1.14519.5.2.1.7311.5101.206828891270520544417996275680.5tse2d1254.538438420111018.1D000000SN0D000000S0D000000S0D000000S0D970296SN0D241922":{"camera":{"position":[-22.163253907287167,-564.594223248674,174.1187338921456],"focalPoint":[-22.163253784231358,17.55373397057385,28.972948296001235],"viewUp":[1.4080733262381892e-17,0.24192188680171967,0.9702957272529602],"parallelScale":155.28356376101038,"directionOfProjection":[2.0510340073149801e-10,0.9702957270336714,-0.2419218925608004]},"volumeColorConfig":{"colorBy":{"arrayName":"vtkDataArray838","location":"pointData"},"transferFunction":{"preset":"CT-Coronary-Arteries-2","mappingRange":[-2048,3661]},"opacityFunction":{"mode":1,"gaussians":[],"mappingRange":[0,1432],"preset":"CT-Coronary-Arteries-2","shift":0,"shiftAlpha":0},"cvr":{"enabled":true,"lightFollowsCamera":true,"volumeQuality":2,"useVolumetricScatteringBlending":false,"volumetricScatteringBlending":0.5,"useLocalAmbientOcclusion":true,"laoKernelRadius":5,"laoKernelSize":15,"ambient":0.2,"diffuse":0.7,"specular":0.3}}},"1.3.6.1.4.1.5962.1.1.0.0.0.1196533885.18148.0.21.31.200000e0051251220030505.1D000000S0D001152SN0D001332SN0D001144S1D000000S0D006144":{"camera":{"position":[-0.7825957138099202,-582.9333614467896,-5.548833820787268],"focalPoint":[-1.4482100259604564,-1.1179264052817643,-1.9742063331270465],"viewUp":[0.0013390391832217574,-0.00614239601418376,1.000001311302185],"parallelScale":150.58491532335074,"directionOfProjection":[-0.0011440076597175992,0.9999804722932084,0.0061438000233316895]},"volumeColorConfig":{"colorBy":{"arrayName":"vtkDataArray1797","location":"pointData"},"transferFunction":{"preset":"CT-Coronary-Arteries-2","mappingRange":[-2048,3661]},"opacityFunction":{"mode":1,"gaussians":[],"mappingRange":[0,496],"preset":"CT-Coronary-Arteries-2","shift":0,"shiftAlpha":0},"cvr":{"enabled":true,"lightFollowsCamera":true,"volumeQuality":2,"useVolumetricScatteringBlending":false,"volumetricScatteringBlending":0.5,"useLocalAmbientOcclusion":true,"laoKernelRadius":5,"laoKernelSize":15,"ambient":0.2,"diffuse":0.7,"specular":0.3}}}}},{"id":"Oblique3D","type":"Oblique3D","props":{"viewDirection":"Posterior","viewUp":"Superior","slices":[{"viewID":"ObliqueSagittal","axis":"Sagittal"},{"viewID":"ObliqueCoronal","axis":"Coronal"},{"viewID":"ObliqueAxial","axis":"Axial"}]},"config":{}}],"parentToLayers":[{"selectionKey":"1.3.6.1.4.1.5962.1.1.0.0.0.1196533885.18148.0.21.31.200000e0051251220030505.1D000000S0D001152SN0D001332SN0D001144S1D000000S0D006144","sourceSelectionKeys":["1.3.6.1.4.1.14519.5.2.1.7311.5101.206828891270520544417996275680.5tse2d1254.538438420111018.1D000000SN0D000000S0D000000S0D000000S0D970296SN0D241922"]}],"primarySelection":"1.3.6.1.4.1.5962.1.1.0.0.0.1196533885.18148.0.21.31.200000e0051251220030505.1D000000S0D001152SN0D001332SN0D001144S1D000000S0D006144"} \ No newline at end of file diff --git a/tests/fixtures/pre-multi-4up.5-0-1.volview.json b/tests/fixtures/pre-multi-4up.5-0-1.volview.json index d5fada989..d03f4af0f 100644 --- a/tests/fixtures/pre-multi-4up.5-0-1.volview.json +++ b/tests/fixtures/pre-multi-4up.5-0-1.volview.json @@ -11,7 +11,7 @@ "id": 2, "type": "uri", "name": "MRI-PROSTATEx-0004.zip", - "uri": "https://data.kitware.com/api/v1/item/63527c7311dab8142820a338/download" + "uri": "/tmp/prostate.zip" }, { "id": 3, diff --git a/tests/server/content-disposition-server.ts b/tests/server/content-disposition-server.ts index 3f0c72087..6662deed4 100644 --- a/tests/server/content-disposition-server.ts +++ b/tests/server/content-disposition-server.ts @@ -4,6 +4,7 @@ import { readFileSync } from 'fs'; import { join } from 'path'; import { TEMP_DIR } from '../../wdio.shared.conf'; import { AUX_PORT } from '../e2ePorts'; +import { CT_ELECTRODES } from '../datasets'; export function createContentDispositionServer() { const app = express(); @@ -16,13 +17,13 @@ export function createContentDispositionServer() { ); app.get('/scan', (req, res) => { - const filePath = join(TEMP_DIR, 'CT_Electrodes.nii.gz'); + const filePath = join(TEMP_DIR, CT_ELECTRODES.name); const fileBuffer = readFileSync(filePath); res.setHeader('Content-Type', 'application/octet-stream'); res.setHeader( 'Content-Disposition', - 'attachment; filename="CT_Electrodes.nii.gz"' + `attachment; filename="${CT_ELECTRODES.name}"` ); res.send(fileBuffer); }); diff --git a/tests/specs/annotationTestUtils.ts b/tests/specs/annotationTestUtils.ts index 457f7a100..66c3fc3d5 100644 --- a/tests/specs/annotationTestUtils.ts +++ b/tests/specs/annotationTestUtils.ts @@ -1,6 +1,6 @@ import { type ChainablePromiseElement } from 'webdriverio'; import AppPage from '../pageobjects/volview.page'; -import { MINIMAL_DICOM } from './configTestUtils'; +import { MINIMAL_DICOM } from '../datasets'; import { openUrls } from './utils'; const pointerAt = (x: number, y: number) => diff --git a/tests/specs/automatic-layering.e2e.ts b/tests/specs/automatic-layering.e2e.ts index 8010fcfbf..27fb5200a 100644 --- a/tests/specs/automatic-layering.e2e.ts +++ b/tests/specs/automatic-layering.e2e.ts @@ -1,6 +1,6 @@ import { DOWNLOAD_TIMEOUT } from '@/wdio.shared.conf'; import { volViewPage } from '../pageobjects/volview.page'; -import { FETUS_DATASET } from './configTestUtils'; +import { FETUS_DATASET } from '../datasets'; import { writeManifestToFile } from './utils'; describe('Automatic Layering by File Name', () => { @@ -15,7 +15,7 @@ describe('Automatic Layering by File Name', () => { await writeManifestToFile(config, configFileName); await volViewPage.open( - `?urls=[${FETUS_DATASET.url},${FETUS_DATASET.url},tmp/${configFileName}]&names=[base-image.mha,base-image.layer.mha,${configFileName}]` + `?urls=[tmp/${FETUS_DATASET.name},tmp/${FETUS_DATASET.name},tmp/${configFileName}]&names=[base-image.mha,base-image.layer.mha,${configFileName}]` ); await volViewPage.waitForViews(); diff --git a/tests/specs/cine-drag-onto-slot.e2e.ts b/tests/specs/cine-drag-onto-slot.e2e.ts index 66e57f192..dbf07e0ad 100644 --- a/tests/specs/cine-drag-onto-slot.e2e.ts +++ b/tests/specs/cine-drag-onto-slot.e2e.ts @@ -2,8 +2,8 @@ // stores a 3D viewInfo. When cine binds to it, the slot renders as // CineViewer (data wins over slot type). Dragging a volume onto that same // slot reverts it to VolumeViewer because viewInfo was never mutated. -import { CINE_US_DATASET, PROSTATEX_DATASET } from './configTestUtils'; -import { downloadFile, openUrls } from './utils'; +import { CINE_US_DATASET, PROSTATEX_DATASET } from '../datasets'; +import { openUrls } from './utils'; import { volViewPage } from '../pageobjects/volview.page'; const IMAGE_DRAG_MEDIA_TYPE = 'application/x-volview-image-id'; @@ -60,7 +60,6 @@ async function gridItemKind(index: number) { describe('Drag cine onto a non-2D slot', () => { it('cine renders in the 3D slot; dropping a volume reverts the slot to 3D', async () => { - await downloadFile(PROSTATEX_DATASET.url, PROSTATEX_DATASET.name); await openUrls([PROSTATEX_DATASET, CINE_US_DATASET]); await volViewPage.waitForViews(); await browser.waitUntil( diff --git a/tests/specs/cine-rendering.e2e.ts b/tests/specs/cine-rendering.e2e.ts index a27364521..26791084b 100644 --- a/tests/specs/cine-rendering.e2e.ts +++ b/tests/specs/cine-rendering.e2e.ts @@ -1,10 +1,7 @@ import { Key } from 'webdriverio'; import { volViewPage } from '../pageobjects/volview.page'; -import { - CINE_US_DATASET, - COLOR3D_JPEG_BASELINE_DICOM, -} from './configTestUtils'; -import { downloadFile, openUrls, writeManifestToFile } from './utils'; +import { CINE_US_DATASET, COLOR3D_JPEG_BASELINE_DICOM } from '../datasets'; +import { openUrls, writeManifestToFile } from './utils'; const PLAY_CONTROLS = '.play-controls'; const FRAME_LABEL = '.view-annotations .frame-label'; @@ -57,7 +54,6 @@ async function waitForCineFrameChange(index: number, previousText: string) { } async function openCineDatasetWithConfig(config: unknown, configName: string) { - await downloadFile(CINE_US_DATASET.url, CINE_US_DATASET.name); const configFileName = `${configName}-${Date.now()}.json`; await writeManifestToFile(config, configFileName); await volViewPage.open( diff --git a/tests/specs/cine-ruler-mid-placement.e2e.ts b/tests/specs/cine-ruler-mid-placement.e2e.ts index 2be86f8c7..4aa602714 100644 --- a/tests/specs/cine-ruler-mid-placement.e2e.ts +++ b/tests/specs/cine-ruler-mid-placement.e2e.ts @@ -7,7 +7,7 @@ // click time is fragile (both render an svg ), so we scrub off the // click frame and back. A committed (buggy) ruler persists; an unfinished // placing tool is wiped by the reset on the second scroll. -import { CINE_US_DATASET } from './configTestUtils'; +import { CINE_US_DATASET } from '../datasets'; import { openUrls } from './utils'; import { volViewPage } from '../pageobjects/volview.page'; import { diff --git a/tests/specs/cine-ruler-session.e2e.ts b/tests/specs/cine-ruler-session.e2e.ts index 8bba5ac3b..046806117 100644 --- a/tests/specs/cine-ruler-session.e2e.ts +++ b/tests/specs/cine-ruler-session.e2e.ts @@ -3,7 +3,7 @@ // frame N. A regression in either the zod schema or addTool defaults would // silently lose the per-frame association. import * as path from 'path'; -import { CINE_US_DATASET } from './configTestUtils'; +import { CINE_US_DATASET } from '../datasets'; import { openUrls, SESSION_SAVE_TIMEOUT, waitForFileExists } from './utils'; import { volViewPage } from '../pageobjects/volview.page'; import { TEMP_DIR } from '../../wdio.shared.conf'; diff --git a/tests/specs/configTestUtils.ts b/tests/specs/configTestUtils.ts index 74724384d..5e2310de0 100644 --- a/tests/specs/configTestUtils.ts +++ b/tests/specs/configTestUtils.ts @@ -1,30 +1,11 @@ import { volViewPage } from '../pageobjects/volview.page'; import { writeManifestToFile } from './utils'; - -export const ONE_CT_SLICE_DICOM = { - url: 'https://data.kitware.com/api/v1/file/6566aa81c5a2b36857ad1783/download', - name: 'CT000085.dcm', -} as const; - -export const MINIMAL_DICOM = { - url: 'https://data.kitware.com/api/v1/file/68e9807dbf0f869935e36481/download', - name: 'minimal.dcm', -} as const; - -export const MINIMAL_501_SESSION = { - url: 'https://data.kitware.com/api/v1/file/693077d880eaefe49a4abb88/download', - name: 'minimal-501-session.volview.zip', -} as const; - -export const PROSTATEX_DATASET = { - url: 'https://data.kitware.com/api/v1/item/63527c7311dab8142820a338/download', - name: 'prostate.zip', -} as const; - -export const PROSTATE_SEGMENT_GROUP = { - url: 'https://data.kitware.com/api/v1/file/692f13ed80eaefe49a4abb72/download', - name: 'prostate-total.seg.nii.gz', -} as const; +import { + ONE_CT_SLICE_DICOM, + PROSTATEX_DATASET, + PROSTATE_SEGMENT_GROUP, + type TestDataset, +} from '../datasets'; export const PROSTATE_610_LABELMAP_MANIFEST = { version: '6.1.0', @@ -63,55 +44,16 @@ export const PROSTATE_610_LABELMAP_MANIFEST = { ], } as const; -export const MRA_HEAD_NECK_DATASET = { - url: 'https://data.kitware.com/api/v1/item/6352a2b311dab8142820a33b/download', - name: 'MRA-Head_and_Neck.zip', -} as const; - -export const FETUS_DATASET = { - url: 'https://data.kitware.com/api/v1/item/635679c311dab8142820a4f4/download', - name: 'fetus.zip', -} as const; - -// Multiframe ultrasound DICOM from pydicom public test data. -// SequenceOfUltrasoundRegions: PhysicalDeltaX/Y = 0.05104970559 cm/pixel -// (unit code 3 = cm), so with US spacing fix the VTK spacing is ~0.5105 mm. -export const US_MULTIFRAME_DICOM = { - url: 'https://data.kitware.com/api/v1/file/69e1630646ef98a20f563020/download', - name: 'US_multiframe_30frames.dcm', -} as const; - -// 8-frame echocardiogram from the BSD-licensed GDCM test corpus. Native -// Explicit VR LE, MONOCHROME2, retired Ultrasound Multi-frame SOP UID. -export const CINE_US_DATASET = { - url: 'https://sourceforge.net/p/gdcm/gdcmdata/ci/master/tree/US-MONO2-8-8x-execho.dcm?format=raw', - name: 'US-MONO2-8-8x-echo.dcm', -} as const; - -// 120-frame RGB ultrasound cine encoded as JPEG Baseline. Exercises the async -// browser JPEG decode path used by common compressed ultrasound clips. -export const COLOR3D_JPEG_BASELINE_DICOM = { - url: 'https://raw.githubusercontent.com/pydicom/pydicom-data/master/data_store/data/color3d_jpeg_baseline.dcm', - name: 'color3d_jpeg_baseline.dcm', -} as const; - -export type DatasetResource = { - url: string; - name?: string; -}; - export const openConfigAndDataset = async ( config: unknown, name: string, - dataset: DatasetResource = ONE_CT_SLICE_DICOM + dataset: TestDataset = ONE_CT_SLICE_DICOM ) => { const configFileName = `${name}-config.json`; await writeManifestToFile(config, configFileName); await volViewPage.open( - `?urls=[${dataset.url},tmp/${configFileName}]&names=[${ - dataset.name ?? '' - },${configFileName}]` + `?urls=[tmp/${dataset.name},tmp/${configFileName}]&names=[${dataset.name},${configFileName}]` ); await volViewPage.waitForViews(); }; diff --git a/tests/specs/content-disposition.e2e.ts b/tests/specs/content-disposition.e2e.ts index f8b550d1c..659f3d4a6 100644 --- a/tests/specs/content-disposition.e2e.ts +++ b/tests/specs/content-disposition.e2e.ts @@ -1,18 +1,11 @@ import { volViewPage } from '../pageobjects/volview.page'; -import { downloadFile } from './utils'; import { startServer, stopServer } from '../server/content-disposition-server'; import { AUX_PORT as SERVER_PORT } from '../e2ePorts'; -const CT_ELECTRODES = { - url: 'https://raw.githubusercontent.com/neurolabusc/niivue-images/main/CT_Electrodes.nii.gz', - name: 'CT_Electrodes.nii.gz', -}; - describe('Content-Disposition header handling', () => { let server: ReturnType; before(async () => { - await downloadFile(CT_ELECTRODES.url, CT_ELECTRODES.name); server = startServer(); }); diff --git a/tests/specs/crosshairs-multi-image.e2e.ts b/tests/specs/crosshairs-multi-image.e2e.ts index 76cfa9230..b7371720f 100644 --- a/tests/specs/crosshairs-multi-image.e2e.ts +++ b/tests/specs/crosshairs-multi-image.e2e.ts @@ -1,4 +1,4 @@ -import { MRA_HEAD_NECK_DATASET, PROSTATEX_DATASET } from './configTestUtils'; +import { MRA_HEAD_NECK_DATASET, PROSTATEX_DATASET } from '../datasets'; import { openUrls } from './utils'; import { volViewPage } from '../pageobjects/volview.page'; diff --git a/tests/specs/different-direction-labelmap.e2e.ts b/tests/specs/different-direction-labelmap.e2e.ts index 04c0f685a..f6ed0845b 100644 --- a/tests/specs/different-direction-labelmap.e2e.ts +++ b/tests/specs/different-direction-labelmap.e2e.ts @@ -1,9 +1,5 @@ -import { - PROSTATEX_DATASET, - PROSTATE_SEGMENT_GROUP, - PROSTATE_610_LABELMAP_MANIFEST, -} from './configTestUtils'; -import { downloadFile, writeManifestToFile } from './utils'; +import { PROSTATE_610_LABELMAP_MANIFEST } from './configTestUtils'; +import { writeManifestToFile } from './utils'; import { volViewPage } from '../pageobjects/volview.page'; import { DOWNLOAD_TIMEOUT, TEMP_DIR } from '../../wdio.shared.conf'; import * as path from 'path'; @@ -22,9 +18,6 @@ import { cleanuptotal } from 'wdio-cleanuptotal-service'; */ describe('Labelmap with different direction matrix', () => { it('paint tool works on coronal view', async () => { - await downloadFile(PROSTATEX_DATASET.url, PROSTATEX_DATASET.name); - await downloadFile(PROSTATE_SEGMENT_GROUP.url, PROSTATE_SEGMENT_GROUP.name); - const config = { layouts: { 'Coronal Only': [['coronal']], diff --git a/tests/specs/layers-and-rendering.e2e.ts b/tests/specs/layers-and-rendering.e2e.ts index a67be5fd0..50d53a4f8 100644 --- a/tests/specs/layers-and-rendering.e2e.ts +++ b/tests/specs/layers-and-rendering.e2e.ts @@ -4,7 +4,7 @@ import { cleanuptotal } from 'wdio-cleanuptotal-service'; import { DOWNLOAD_TIMEOUT, FIXTURES, TEMP_DIR } from '@/wdio.shared.conf'; import { volViewPage } from '../pageobjects/volview.page'; import { openUrls, openVolViewPage, writeManifestToFile } from './utils'; -import { PROSTATEX_DATASET } from './configTestUtils'; +import { PROSTATEX_DATASET } from '../datasets'; const openLayeringFixtures = async () => { const fixtureNames = ['prostate-3-slices.zip', 'mra-head-neck-3-slices.zip']; diff --git a/tests/specs/layout-config.e2e.ts b/tests/specs/layout-config.e2e.ts index a158183db..b89892a49 100644 --- a/tests/specs/layout-config.e2e.ts +++ b/tests/specs/layout-config.e2e.ts @@ -1,5 +1,6 @@ import { volViewPage } from '../pageobjects/volview.page'; -import { PROSTATEX_DATASET, openConfigAndDataset } from './configTestUtils'; +import { openConfigAndDataset } from './configTestUtils'; +import { PROSTATEX_DATASET } from '../datasets'; describe('VolView Layout Configuration', () => { it('should create a 2x2 grid layout from simple string array', async () => { diff --git a/tests/specs/reference-lines.e2e.ts b/tests/specs/reference-lines.e2e.ts index 63eb3fd35..757962135 100644 --- a/tests/specs/reference-lines.e2e.ts +++ b/tests/specs/reference-lines.e2e.ts @@ -1,4 +1,4 @@ -import { PROSTATEX_DATASET } from './configTestUtils'; +import { PROSTATEX_DATASET } from '../datasets'; import { openUrls } from './utils'; import { volViewPage } from '../pageobjects/volview.page'; diff --git a/tests/specs/remote-manifest.e2e.ts b/tests/specs/remote-manifest.e2e.ts index 64b509c82..ece688aec 100644 --- a/tests/specs/remote-manifest.e2e.ts +++ b/tests/specs/remote-manifest.e2e.ts @@ -1,6 +1,6 @@ import { volViewPage } from '../pageobjects/volview.page'; -import { MINIMAL_DICOM } from './configTestUtils'; -import { downloadFile, writeManifestToFile, openVolViewPage } from './utils'; +import { MINIMAL_DICOM } from '../datasets'; +import { writeManifestToFile, openVolViewPage } from './utils'; describe('VolView loading of remoteManifest.json', () => { it('should show error when there is no name and URL is malformed', async () => { @@ -17,8 +17,6 @@ describe('VolView loading of remoteManifest.json', () => { }); it('should load relative URI with no name property', async () => { - await downloadFile(MINIMAL_DICOM.url, MINIMAL_DICOM.name); - const manifest = { resources: [{ url: `/tmp/${MINIMAL_DICOM.name}` }], }; diff --git a/tests/specs/reveal-slice.e2e.ts b/tests/specs/reveal-slice.e2e.ts index 5134dbb91..ff123e77d 100644 --- a/tests/specs/reveal-slice.e2e.ts +++ b/tests/specs/reveal-slice.e2e.ts @@ -1,5 +1,5 @@ -import { CINE_US_DATASET, PROSTATEX_DATASET } from './configTestUtils'; -import { downloadFile, openUrls } from './utils'; +import { CINE_US_DATASET, PROSTATEX_DATASET } from '../datasets'; +import { openUrls } from './utils'; import { volViewPage } from '../pageobjects/volview.page'; import { advanceCineFrame, @@ -88,7 +88,6 @@ const waitForSlice = async (expectedSlice1Indexed: number) => { describe('Reveal Slice on a volume image', () => { it('ruler Reveal Slice jumps the view back to the placed slice', async () => { - await downloadFile(PROSTATEX_DATASET.url, PROSTATEX_DATASET.name); await openUrls([PROSTATEX_DATASET]); await volViewPage.focusFirst2DView(); diff --git a/tests/specs/seg-nrrd-export.e2e.ts b/tests/specs/seg-nrrd-export.e2e.ts index 684b7d4ab..582849d81 100644 --- a/tests/specs/seg-nrrd-export.e2e.ts +++ b/tests/specs/seg-nrrd-export.e2e.ts @@ -5,7 +5,8 @@ import JSZip from 'jszip'; import { volViewPage } from '../pageobjects/volview.page'; import { TEMP_DIR } from '../../wdio.shared.conf'; import { waitForFileExists } from './utils'; -import { ONE_CT_SLICE_DICOM, openConfigAndDataset } from './configTestUtils'; +import { openConfigAndDataset } from './configTestUtils'; +import { ONE_CT_SLICE_DICOM } from '../datasets'; /** * Parse NRRD header key-value pairs from a buffer (handles gzip). diff --git a/tests/specs/segment-group-download.e2e.ts b/tests/specs/segment-group-download.e2e.ts index 33f6b094e..52c97be0c 100644 --- a/tests/specs/segment-group-download.e2e.ts +++ b/tests/specs/segment-group-download.e2e.ts @@ -4,7 +4,7 @@ import { cleanuptotal } from 'wdio-cleanuptotal-service'; import { openUrls, waitForFileExists } from './utils'; import { volViewPage } from '../pageobjects/volview.page'; import { TEMP_DIR } from '../../wdio.shared.conf'; -import { PROSTATEX_DATASET } from './configTestUtils'; +import { PROSTATEX_DATASET } from '../datasets'; const SAVE_TIMEOUT = 40000; diff --git a/tests/specs/segment-group-list-scroll.e2e.ts b/tests/specs/segment-group-list-scroll.e2e.ts index f642e93b8..94d984b18 100644 --- a/tests/specs/segment-group-list-scroll.e2e.ts +++ b/tests/specs/segment-group-list-scroll.e2e.ts @@ -1,6 +1,6 @@ import { volViewPage } from '../pageobjects/volview.page'; import { openUrls } from './utils'; -import { PROSTATEX_DATASET } from './configTestUtils'; +import { PROSTATEX_DATASET } from '../datasets'; // Six 48px rows overflow the list's 240px cap. const GROUP_COUNT = 6; diff --git a/tests/specs/session-state-lifecycle.e2e.ts b/tests/specs/session-state-lifecycle.e2e.ts index c41faea8b..2acc180e8 100644 --- a/tests/specs/session-state-lifecycle.e2e.ts +++ b/tests/specs/session-state-lifecycle.e2e.ts @@ -1,13 +1,8 @@ import * as path from 'path'; import * as fs from 'fs'; import JSZip from 'jszip'; -import { MINIMAL_501_SESSION, PROSTATEX_DATASET } from './configTestUtils'; -import { - downloadFile, - openUrls, - SESSION_SAVE_TIMEOUT, - waitForFileExists, -} from './utils'; +import { MINIMAL_501_SESSION, PROSTATEX_DATASET } from '../datasets'; +import { openUrls, SESSION_SAVE_TIMEOUT, waitForFileExists } from './utils'; import { setValueVueInput, volViewPage } from '../pageobjects/volview.page'; import { TEMP_DIR } from '../../wdio.shared.conf'; @@ -56,7 +51,6 @@ const saveAndParseManifest = async () => { }; const loadSession = async () => { - await downloadFile(MINIMAL_501_SESSION.url, MINIMAL_501_SESSION.name); const urlParams = `?urls=[tmp/${MINIMAL_501_SESSION.name}]`; await volViewPage.open(urlParams); await volViewPage.waitForViews(); diff --git a/tests/specs/sparse-manifest-prostate-rectangle.e2e.ts b/tests/specs/sparse-manifest-prostate-rectangle.e2e.ts index 5d712f6b1..31f400847 100644 --- a/tests/specs/sparse-manifest-prostate-rectangle.e2e.ts +++ b/tests/specs/sparse-manifest-prostate-rectangle.e2e.ts @@ -1,10 +1,8 @@ -import { PROSTATEX_DATASET } from './configTestUtils'; -import { downloadFile, openVolViewPage, writeManifestToZip } from './utils'; +import { PROSTATEX_DATASET } from '../datasets'; +import { openVolViewPage, writeManifestToZip } from './utils'; describe('Sparse manifest with prostate rectangle', () => { it('loads prostate dataset with lesion rectangle annotation', async () => { - await downloadFile(PROSTATEX_DATASET.url, PROSTATEX_DATASET.name); - const sparseManifest = { version: '6.1.0', dataSources: [ diff --git a/tests/specs/sparse-manifest.e2e.ts b/tests/specs/sparse-manifest.e2e.ts index e160cae5e..fcbb27b37 100644 --- a/tests/specs/sparse-manifest.e2e.ts +++ b/tests/specs/sparse-manifest.e2e.ts @@ -1,11 +1,6 @@ +import { PROSTATE_610_LABELMAP_MANIFEST } from './configTestUtils'; +import { MINIMAL_DICOM } from '../datasets'; import { - MINIMAL_DICOM, - PROSTATEX_DATASET, - PROSTATE_SEGMENT_GROUP, - PROSTATE_610_LABELMAP_MANIFEST, -} from './configTestUtils'; -import { - downloadFile, openVolViewPage, writeManifestToFile, writeManifestToZip, @@ -14,8 +9,6 @@ import { DOWNLOAD_TIMEOUT } from '../../wdio.shared.conf'; describe('Sparse manifest.json', () => { it('loads manifest with only URL data source', async () => { - await downloadFile(MINIMAL_DICOM.url, MINIMAL_DICOM.name); - const sparseManifest = { version: '6.1.0', dataSources: [ @@ -33,8 +26,6 @@ describe('Sparse manifest.json', () => { }); it('loads sparse manifest with tools section (rectangle)', async () => { - await downloadFile(MINIMAL_DICOM.url, MINIMAL_DICOM.name); - const sparseManifest = { version: '6.1.0', dataSources: [ @@ -98,8 +89,6 @@ describe('Sparse manifest.json', () => { }); it('loads standalone JSON state file (not zipped)', async () => { - await downloadFile(MINIMAL_DICOM.url, MINIMAL_DICOM.name); - const sparseManifest = { version: '6.1.0', dataSources: [ @@ -117,9 +106,6 @@ describe('Sparse manifest.json', () => { }); it('loads remote segment group from URI', async () => { - await downloadFile(PROSTATEX_DATASET.url, PROSTATEX_DATASET.name); - await downloadFile(PROSTATE_SEGMENT_GROUP.url, PROSTATE_SEGMENT_GROUP.name); - const fileName = 'remote-segment-group.volview.json'; await writeManifestToFile(PROSTATE_610_LABELMAP_MANIFEST, fileName); await openVolViewPage(fileName); diff --git a/tests/specs/ultrasound-spacing.e2e.ts b/tests/specs/ultrasound-spacing.e2e.ts index 51d7a8677..a50b214c7 100644 --- a/tests/specs/ultrasound-spacing.e2e.ts +++ b/tests/specs/ultrasound-spacing.e2e.ts @@ -1,4 +1,4 @@ -import { US_MULTIFRAME_DICOM } from './configTestUtils'; +import { US_MULTIFRAME_DICOM } from '../datasets'; import { openUrls } from './utils'; import { volViewPage } from '../pageobjects/volview.page'; diff --git a/tests/specs/utils.ts b/tests/specs/utils.ts index 756c1085a..184b74614 100644 --- a/tests/specs/utils.ts +++ b/tests/specs/utils.ts @@ -1,45 +1,10 @@ import * as path from 'path'; import * as fs from 'fs'; -import { z } from 'zod'; import { cleanuptotal } from 'wdio-cleanuptotal-service'; import JSZip from 'jszip'; -import { - DATASET_CACHE, - TEMP_DIR, - linkCachedDataset, -} from '../../wdio.shared.conf'; +import { TEMP_DIR } from '../../wdio.shared.conf'; import { volViewPage } from '../pageobjects/volview.page'; -import { RemoteResource } from '../../src/io/manifest'; - -// Cached across runs, and not automatically deleted -export const downloadFile = async (url: string, fileName: string) => { - const savePath = path.join(DATASET_CACHE, fileName); - if (!fs.existsSync(savePath)) { - // Download to a temporary file first to avoid race conditions - const tempPath = `${savePath}.${process.pid}.${Date.now()}.tmp`; - const response = await fetch(url); - const data = await response.arrayBuffer(); - const buffer = Buffer.from(data); - fs.writeFileSync(tempPath, buffer); - - // Atomic rename - if another process already created the file, this will fail safely - try { - fs.renameSync(tempPath, savePath); - } catch (err) { - // Another process beat us to it, clean up our temp file - try { - fs.unlinkSync(tempPath); - } catch { - // ignore - } - // Check if the final file exists and is complete - if (!fs.existsSync(savePath)) { - throw new Error(`Failed to download ${fileName}: ${err}`); - } - } - } - return linkCachedDataset(fileName); -}; +import type { TestDataset } from '../datasets'; export function writeMetaImage( fileName: string, @@ -107,8 +72,6 @@ export async function openVolViewPage(fileName: string) { expect(notifications).toEqual(0); } -type RemoteResourceType = z.infer & { name: string }; - export const SESSION_SAVE_TIMEOUT = 40_000; export const waitForFileExists = (filePath: string, timeout: number) => @@ -140,14 +103,9 @@ export const waitForFileExists = (filePath: string, timeout: number) => }); }); -export async function openUrls(urlsAndNames: Array) { - await Promise.all( - urlsAndNames.map((resource) => downloadFile(resource.url, resource.name)) - ); - - const resources = urlsAndNames.map(({ name }) => ({ url: `/tmp/${name}` })); +export async function openUrls(datasets: ReadonlyArray) { const manifest = { - resources, + resources: datasets.map(({ name }) => ({ url: `/tmp/${name}` })), }; const fileName = `openUrlsManifest_${Date.now()}_${Math.random() .toString(36) diff --git a/tests/specs/vtk-interactor-lifecycle.e2e.ts b/tests/specs/vtk-interactor-lifecycle.e2e.ts index 24fd741cb..dbb29febc 100644 --- a/tests/specs/vtk-interactor-lifecycle.e2e.ts +++ b/tests/specs/vtk-interactor-lifecycle.e2e.ts @@ -1,4 +1,4 @@ -import { COLOR3D_JPEG_BASELINE_DICOM } from './configTestUtils'; +import { COLOR3D_JPEG_BASELINE_DICOM } from '../datasets'; import { openUrls } from './utils'; const VIEW_SELECTOR = 'div[data-testid="vtk-view vtk-cine-view"]'; diff --git a/tests/specs/windowing-config.e2e.ts b/tests/specs/windowing-config.e2e.ts index 382e36361..a7393e168 100644 --- a/tests/specs/windowing-config.e2e.ts +++ b/tests/specs/windowing-config.e2e.ts @@ -1,10 +1,7 @@ import { volViewPage } from '../pageobjects/volview.page'; import { openUrls } from './utils'; -import { - openConfigAndDataset, - ONE_CT_SLICE_DICOM, - MINIMAL_DICOM, -} from './configTestUtils'; +import { openConfigAndDataset } from './configTestUtils'; +import { ONE_CT_SLICE_DICOM, MINIMAL_DICOM } from '../datasets'; describe('VolView windowing configuration', () => { it('should use runtime config window level over DICOM window level', async () => { diff --git a/wdio.shared.conf.ts b/wdio.shared.conf.ts index 1f6495cf4..c287be5ec 100644 --- a/wdio.shared.conf.ts +++ b/wdio.shared.conf.ts @@ -5,33 +5,7 @@ import type { Options, Capabilities } from '@wdio/types'; import { SevereServiceError } from 'webdriverio'; import { projectRoot } from './tests/e2eTestUtils'; import { AUX_PORT, BASE_URL, TEST_PORT } from './tests/e2ePorts'; - -const TEST_DATASETS = [ - { - url: 'https://data.kitware.com/api/v1/file/6566aa81c5a2b36857ad1783/download', - name: 'CT000085.dcm', - }, - { - url: 'https://data.kitware.com/api/v1/file/68e9807dbf0f869935e36481/download', - name: 'minimal.dcm', - }, - { - url: 'https://data.kitware.com/api/v1/item/63527c7311dab8142820a338/download', - name: 'prostate.zip', - }, - { - url: 'https://data.kitware.com/api/v1/item/6352a2b311dab8142820a33b/download', - name: 'MRA-Head_and_Neck.zip', - }, - { - url: 'https://data.kitware.com/api/v1/item/635679c311dab8142820a4f4/download', - name: 'fetus.zip', - }, - { - url: 'https://sourceforge.net/p/gdcm/gdcmdata/ci/master/tree/US-MONO2-8-8x-execho.dcm?format=raw', - name: 'US-MONO2-8-8x-echo.dcm', - }, -]; +import { TEST_DATASETS } from './tests/datasets'; // Fixed capture viewport (Playwright's default). export const CONTENT_VIEWPORT = { width: 1280, height: 720 } as const; @@ -50,7 +24,7 @@ const IS_CI = !!(process.env.CI || process.env.GITHUB_ACTIONS); const ROOT = projectRoot(); const TMP = '.tmp/'; // Fixtures are downloaded once and shared by every run. -export const DATASET_CACHE = path.resolve(ROOT, TMP, 'datasets'); +const DATASET_CACHE = path.resolve(ROOT, TMP, 'datasets'); // Everything a run generates or downloads through the browser, including the // fixture links it serves. Also the browser downloads directory. Keyed by port // so a checkout, or an overridden port, gets scratch space of its own. @@ -73,7 +47,7 @@ const removeDir = (dir: string) => * Exposes a cached fixture under this run's directory, so specs can reach it at * `/tmp/` without every run holding its own copy. */ -export function linkCachedDataset(name: string) { +function linkCachedDataset(name: string) { const runPath = path.join(TEMP_DIR, name); if (fs.existsSync(runPath)) return runPath; @@ -197,47 +171,30 @@ export const config: Options.Testrunner = { removeDir(TEMP_DIR); fs.mkdirSync(TEMP_DIR, { recursive: true }); - const RETRIES = 3; - const RETRY_DELAY_MS = 500; - const delay = (ms: number) => - new Promise((resolve) => { - setTimeout(resolve, ms); - }); - const downloadOnce = async (url: string, savePath: string) => { + // The only place the suite reaches the internet, so a slow or missing + // host stops the run here and never shows up as a failing spec. + const download = async (url: string, savePath: string) => { const response = await fetch(url); - if (!response.ok) { - throw new Error(`HTTP ${response.status} for ${url}`); - } - const data = await response.arrayBuffer(); - // Write to a temp path first so a failed/partial download never leaves a - // corrupt file that the existsSync check would treat as already cached. - const tmpPath = `${savePath}.part`; - fs.writeFileSync(tmpPath, Buffer.from(data)); - fs.renameSync(tmpPath, savePath); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + // A partial file must never look cached to the existsSync check. + const partPath = `${savePath}.part`; + fs.writeFileSync(partPath, Buffer.from(await response.arrayBuffer())); + fs.renameSync(partPath, savePath); }; - const downloads = TEST_DATASETS.map(async ({ url, name }) => { - const savePath = path.join(DATASET_CACHE, name); - if (!fs.existsSync(savePath)) { - for (let attempt = 1; attempt <= RETRIES; attempt += 1) { - try { - await downloadOnce(url, savePath); - break; - } catch (err) { - if (attempt === RETRIES) { - throw new Error( - `Failed to download ${name} after ${RETRIES} attempts: ${ - (err as Error).message - }` - ); - } - await delay(RETRY_DELAY_MS); - } + await Promise.all( + TEST_DATASETS.map(async ({ url, name }) => { + const savePath = path.join(DATASET_CACHE, name); + if (!fs.existsSync(savePath)) { + await download(url, savePath).catch((err: Error) => { + throw new SevereServiceError( + `Could not download test dataset ${name} from ${url}: ${err.message}` + ); + }); } - } - linkCachedDataset(name); - }); - await Promise.all(downloads); + linkCachedDataset(name); + }) + ); }, async onComplete(exitCode, completedConfig) { From ed7c2a3a72dfd93eb8366e99aad11f100d43708d Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Mon, 21 Sep 2026 10:47:30 -0400 Subject: [PATCH 2/9] fix(fonts): serve Roboto and the icon font from the bundle Every page load fetched Roboto from Google Fonts and an unpinned @mdi/font@latest from jsdelivr, both render blocking. An offline or firewalled deployment lost its icons, and the e2e screenshots depended on two CDNs. --- index.html | 8 -------- package-lock.json | 19 +++++++++++++++++++ package.json | 2 ++ src/main.js | 7 +++++++ vite.config.ts | 26 ++++++++++++++++++++++++++ 5 files changed, 54 insertions(+), 8 deletions(-) diff --git a/index.html b/index.html index eda9801e8..d460585dc 100644 --- a/index.html +++ b/index.html @@ -6,14 +6,6 @@ VolView - -