* Initial pnpm change
* Install/update in pnpm sort of works
* Attempt to fix install
* fix pnpm
* Refactor build process to use RSPack instead of Webpack across multiple extensions and modes. Update package.json scripts for development and production builds, and adjust webpack configuration files to accommodate new plugin imports and settings.
* Update RSPack dependencies to version 2.0.0 across the project, enhancing compatibility and performance. Refactor webpack configuration in multiple extensions and modes to utilize the new library structure for UMD output. Adjust package.json scripts and settings for improved build processes.
* Implement migration guide for OHIF 3.13, detailing the transition from Webpack to Rspack v2, the shift to pnpm workspaces, and the increase in minimum Node.js version to 24. Include new build commands, plugin replacements, and updates to package configurations across the monorepo.
* Fix some dev:fast bugs
* Move netlify to top level and remove webpack builds
* Update version script to deal with lerna missing
* chore(tests): Update multiple screenshot test images for various specs
* feat(screenshot-reviewer): Add screenshot review tool and update package.json scripts
* fix(DICOMSRDisplayTool): Improve actor presence check in viewport
* chore(tests): Update multiple screenshot assets for various specs
* chore(tests): Integrate waitForPaintToSettle and waitForViewportsRendered in multiple specs for improved rendering stability
* chore(tests): Update screenshot assets for SEGHydration and SEGNoHydration specs
* test: update progressive loading screenshots
* jest 30 test fixes for compatibility with pnpm cs3d
* Use correct setDisplaySets instead of setDataId
* fix: Naming change for LegacyVolumeViewport3D
* Update to allow tolerance for contour tests
* update
* fix
* refactor: Replace instanceof checks with utility functions for viewport type validation
* fix: Update createSegmentationForViewport to handle undefined displaySetInstanceUID gracefully
* bun lock
* chore(pnpm): align workspace setup to cornerstone3D and address PR review
- .npmrc / pnpm-workspace.yaml: mirror cs3d (node-linker hoisted,
strict-peer-dependencies=false, link/prefer workspace packages,
minimumReleaseAge, frozenLockfile); sync axios 1.17.0 + tmp override
- root: packageManager pnpm@11.4.0, engines pnpm >=11, wire preinstall.js
- restore preinstall.js (token/private-repo + CLAUDE->AGENTS symlink)
- playwright.yml: keep base CS3D-integration workflow, switch only the
package manager (bun/yarn -> pnpm) and node 20 -> 24
- webpack.base.js: revert prod devtool to source-map (drop hidden-source-map)
- remove @percy/cypress; .netlify + cli templates engines pnpm >=11
- Dockerfile pin pnpm@11; document tests/globalSetup.ts warmup
- drop dicom-sr -> measurement-tracking edge (breaks pnpm cyclic dep)
* chore(pnpm): regenerate lockfile and restore cs3d:* dev scripts
- Regenerate pnpm-lock.yaml against the resolved workspace (cycle removed,
cs3d 4.22.10, workspace:* internal deps).
- Restore cs3d:checkout/check/build/watch/install/link/unlink helper scripts
(dropped when taking the PR's root scripts), converted yarn -> pnpm since
the local cornerstone3D checkout is now pnpm too.
* Merge base branch issues
* Link ohif app
* ci: pin pnpm to 11.4.0 in action-setup (was version: latest)
version: latest floats across runs and breaks reproducibility. Pin to the repo's packageManager version (pnpm@11.4.0) in both playwright and build-docs workflows.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: Accidental commit of .npmrc config
* Fix build issues
* build fix
* Dependency fixes
* fix: Dependency bug in app on extensions/modes
* Fix output configuration
* Replace percy screenshots with native cypress
* Remove percy screenshot entirely
* fix: Build issues
* force click to prevent canvas cover issues
* Enable swiftshader
* Update launch of electron
* fix: Broken size calculation
* Empty change to force a re-build
* fix(cypress): generate ui-next tailwind classes; drop unsupported electron arg
Tailwind only scanned @ohif/ui-next via ../../node_modules/@ohif/ui-next, which no longer resolves under the pnpm layout, so ui-next-unique classes (e.g. toolbar split-button sizing) were never generated. Those buttons' hit-boxes collapsed, producing Cypress 'covered by element' failures on toolbar interactions. Scan ui-next by direct filesystem path like ui/extensions/modes.
Also stop pushing --enable-unsafe-swiftshader into Electron's launchOptions.args (Electron ignores it and warns); it is supplied via ELECTRON_EXTRA_LAUNCH_ARGS in CI. The arg is still pushed for non-electron chromium (local chrome).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(e2e): disable dev-server overlay during coverage runs
The rspack dev-server client overlay injects an iframe (id=rspack-dev-server-client-overlay) that intercepts pointer events, causing Playwright/Cypress clicks on toolbar buttons to time out (e.g. MicroscopyPanel). Disable the overlay when COVERAGE=true (e2e/Playwright webServer) while keeping it for normal local dev.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(microscopy): provide dicom-microscopy-viewer runtime assets
The microscopy viewer is loaded at runtime via peerImport('dicom-microscopy-viewer'), which fetches the copied asset served at /dicom-microscopy-viewer/. Two issues prevented those assets from reaching dist, so the viewer never initialized and drawing produced no measurement row:
1) dicom-microscopy-viewer was declared in no workspace package.json (only under libs/@cornerstonejs), so it was not installed at root node_modules and the pluginConfig copy source did not exist. Declare it (0.48.6, matching libs) in the microscopy extension.
2) createCopyPluginToDist appended the public/dist folder name to entries that specify an explicit directory, breaking the dicom-microscopy-viewer public entry (looked for .../dynamic-import/public). Use an explicit directory as-is; only append the folder name for package-derived entries.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Update to pnpm 11.5.2 and cs3d 5.0.2
* Lock file
* fix: Update to newer versions of actions to try to fix hang
* Remove unnecessary version
* fix: Tests that are flaky
* PR comments
* Incorrect run name
* docs: Explain pluginConfig tooling
* Updated notes on pluginConfig and PR comments
* fix: resolve plugins in rsbuild build and fix directory-based asset copy
rsbuild.config.ts: merge getPluginResolveAliases() and an @ohif/app$ alias into resolve.alias, and add resolve.modules (root, platform/app, platform/ui node_modules) so extensions resolve their shared @ohif/* imports. Brings the rsbuild (dev:fast) path to parity with webpack.pwa.js / webpack.base.js.
writePluginImportsFile.js: treat a 'directory' on an extension/mode entry as the package root and copy its public/ and dist/ subdirs instead of the whole directory; public-section entries keep copying their directory verbatim via a new literalDirectory flag.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor: share resolve alias/modules between webpack and rsbuild configs
Extract the resolve.alias (@ohif/app, @, @components, ...) and the node_modules search paths into .webpack/resolveConfig.js, the single source of truth consumed by both webpack.base.js (and every per-package webpack.prod/.dev.js that merges it) and rsbuild.config.ts. getModules(srcDir) appends the building package's own source root, preserving the previous per-package behavior. Ends the drift where the rsbuild path kept missing aliases/module paths that webpack.base.js already had.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Add a timeout on git fetch and avoid all the unnecessary mirrros
* PR comments and tests
* Remove cache to prevent corrupting the cache key/setup.
* Fix stale/corrupted pnpm installer
---------
Co-authored-by: Alireza <ar.sedghi@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
619 lines
19 KiB
JavaScript
619 lines
19 KiB
JavaScript
import 'cypress-file-upload';
|
|
import { DragSimulator } from './DragSimulator.js';
|
|
import {
|
|
initCornerstoneToolsAliases,
|
|
initCommonElementsAliases,
|
|
initRouteAliases,
|
|
initStudyListAliasesOnDesktop,
|
|
initPreferencesModalAliases,
|
|
initPreferencesModalFooterBtnAliases,
|
|
} from './aliases.js';
|
|
|
|
/**
|
|
* Command to select a layout preset.
|
|
* The layout preset is selected by clicking on the Layout button and then clicking on the desired preset.
|
|
* The preset name is the text that is displayed on the button.
|
|
* @param {string} presetName - The name of the layout preset that we would like to select
|
|
* @param {boolean} screenshot - If true, a screenshot will be taken when the layout tool is opened
|
|
*/
|
|
Cypress.Commands.add('selectLayoutPreset', (presetName, screenshot) => {
|
|
cy.get('[data-cy="Layout"]').click();
|
|
if (screenshot) {
|
|
cy.screenshot('Layout tool opened');
|
|
}
|
|
cy.get('div').contains(presetName).should('be.visible').click();
|
|
// fixed wait time for layout changes and rendering
|
|
cy.wait(3000);
|
|
});
|
|
|
|
/**
|
|
* Command to search for a patient name and open his/her study.
|
|
*
|
|
* @param {string} PatientName - Patient name that we would like to search for
|
|
*/
|
|
Cypress.Commands.add('openStudy', PatientName => {
|
|
cy.openStudyList();
|
|
cy.get('#filter-patientNameOrId').type(PatientName);
|
|
// cy.get('@getStudies').then(() => {
|
|
// cy.waitQueryList();
|
|
|
|
cy.get('[data-cy="study-list-results"]', { timeout: 15000 })
|
|
.contains(PatientName)
|
|
.first()
|
|
.click({ force: true });
|
|
});
|
|
|
|
Cypress.Commands.add(
|
|
'checkStudyRouteInViewer',
|
|
(StudyInstanceUID, otherParams = '', mode = '/basic-test') => {
|
|
Cypress.on('uncaught:exception', () => false);
|
|
cy.location('pathname').then($url => {
|
|
cy.log($url);
|
|
if ($url === 'blank' || !$url.includes(`${mode}/${StudyInstanceUID}${otherParams}`)) {
|
|
cy.openStudyInViewer(StudyInstanceUID, otherParams, mode);
|
|
cy.waitDicomImage();
|
|
// Very short wait to ensure pending updates are handled
|
|
cy.wait(25);
|
|
}
|
|
});
|
|
}
|
|
);
|
|
|
|
Cypress.Commands.add('initViewer', (StudyInstanceUID, other = {}) => {
|
|
const { mode = '/basic-test', minimumThumbnails = 1, params = '' } = other;
|
|
cy.openStudyInViewer(StudyInstanceUID, params, mode);
|
|
cy.waitDicomImage();
|
|
// Very short wait to ensure pending updates are handled
|
|
cy.wait(25);
|
|
|
|
cy.expectMinimumThumbnails(minimumThumbnails);
|
|
cy.initCommonElementsAliases();
|
|
cy.initCornerstoneToolsAliases();
|
|
});
|
|
|
|
Cypress.Commands.add(
|
|
'openStudyInViewer',
|
|
(StudyInstanceUID, otherParams = '', mode = '/basic-test') => {
|
|
cy.visit(`${mode}?StudyInstanceUIDs=${StudyInstanceUID}${otherParams}`);
|
|
}
|
|
);
|
|
|
|
Cypress.Commands.add('waitQueryList', () => {
|
|
cy.get('[data-querying="false"]', { timeout: 15000 });
|
|
});
|
|
|
|
/**
|
|
* Command to search for a Modality and open the study.
|
|
*
|
|
* @param {string} Modality - Modality type that we would like to search for
|
|
*/
|
|
Cypress.Commands.add('openStudyModality', Modality => {
|
|
cy.initRouteAliases();
|
|
cy.visit('/');
|
|
|
|
cy.get('#filter-accessionOrModalityOrDescription').type(Modality).waitQueryList();
|
|
|
|
cy.get('[data-cy="study-list-results"]').contains(Modality).first().click();
|
|
});
|
|
|
|
/**
|
|
* Command to wait and check if a new page was loaded
|
|
*
|
|
* @param {string} url - part of the expected url. Default value is /basic-test
|
|
*/
|
|
Cypress.Commands.add('isPageLoaded', (url = '/basic-test') => {
|
|
return cy.location('pathname', { timeout: 60000 }).should('include', url);
|
|
});
|
|
|
|
Cypress.Commands.add('openStudyList', () => {
|
|
cy.initRouteAliases();
|
|
cy.visit('/', { timeout: 300000 });
|
|
|
|
// For some reason cypress 12.x does not like to stub the network request
|
|
// so we just wait here for querying to be done.
|
|
// cy.wait('@getStudies');
|
|
cy.waitQueryList();
|
|
});
|
|
|
|
Cypress.Commands.add('waitStudyList', () => {
|
|
// wait 1 second for the studies to get updated
|
|
cy.wait(1000);
|
|
cy.get('@searchResult').should($list => {
|
|
expect($list).to.not.have.class('no-hover');
|
|
});
|
|
});
|
|
|
|
Cypress.Commands.add('waitViewportImageLoading', () => {
|
|
// Wait for finish loading
|
|
cy.get('[data-cy="viewport-grid"]', { timeout: 30000 }).should($grid => {
|
|
expect($grid).not.to.contain.text('Load');
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Command to perform a drag and drop action. Before using this command, we must get the element that should be dragged first.
|
|
* Example of usage: cy.get(element-to-be-dragged).drag(dropzone-element)
|
|
*
|
|
* @param {*} element - Selector for element that we want to use as dropzone
|
|
*/
|
|
Cypress.Commands.add('drag', { prevSubject: 'element' }, (...args) =>
|
|
DragSimulator.simulate(...args)
|
|
);
|
|
|
|
/**
|
|
* Command to perform three clicks into three different positions. Each position must be [x, y].
|
|
* The positions are considering the element as reference, therefore, top-left of the element will be (0, 0).
|
|
*
|
|
* @param {*} viewport - Selector for viewport we would like to interact with
|
|
* @param {number[]} firstClick - Click position [x, y]
|
|
* @param {number[]} secondClick - Click position [x, y]
|
|
* @param {number[]} thirdClick - Click position [x, y]
|
|
*/
|
|
Cypress.Commands.add('addAngle', (viewport, firstClick, secondClick, thirdClick) => {
|
|
cy.get(viewport).then($viewport => {
|
|
const [x1, y1] = firstClick;
|
|
const [x2, y2] = secondClick;
|
|
const [x3, y3] = thirdClick;
|
|
|
|
cy.wrap($viewport)
|
|
.click(x1, y1, { force: true })
|
|
.trigger('mousemove', { clientX: x2, clientY: y2 })
|
|
.click(x2, y2, { force: true })
|
|
.trigger('mousemove', { clientX: x3, clientY: y3 })
|
|
.click(x3, y3, { force: true });
|
|
});
|
|
});
|
|
|
|
Cypress.Commands.add('expectMinimumThumbnails', (seriesToWait = 1) => {
|
|
cy.get('[data-cy="study-browser-thumbnail"]', { timeout: 50000 }).should(
|
|
'have.length.gte',
|
|
seriesToWait
|
|
);
|
|
});
|
|
|
|
//Command to wait DICOM image to load into the viewport
|
|
Cypress.Commands.add('waitDicomImage', (mode = '/basic-test', timeout = 50000) => {
|
|
cy.window()
|
|
.its('cornerstone', { timeout: 30000 })
|
|
.should($cornerstone => {
|
|
const enabled = $cornerstone.getEnabledElements();
|
|
if (enabled?.length) {
|
|
enabled.forEach((item, i) => {
|
|
if (item.viewport.viewportStatus !== $cornerstone.Enums.ViewportStatus.RENDERED) {
|
|
throw new Error(`Viewport ${i} in state ${item.viewport.viewportStatus}`);
|
|
}
|
|
});
|
|
} else {
|
|
throw new Error('No enabled elements');
|
|
}
|
|
});
|
|
// This shouldn't be necessary, but seems to be.
|
|
cy.wait(250);
|
|
cy.log('DICOM image loaded');
|
|
});
|
|
|
|
//Command to reset and clear all the changes made to the viewport
|
|
Cypress.Commands.add('resetViewport', () => {
|
|
// Assign an alias to the More button
|
|
cy.get('[data-cy="MoreTools-split-button-primary"]')
|
|
.should('have.attr', 'data-tool', 'Reset')
|
|
.as('moreBtn');
|
|
|
|
// Use the alias to click on the More button
|
|
cy.get('@moreBtn').click();
|
|
});
|
|
|
|
Cypress.Commands.add('imageZoomIn', () => {
|
|
cy.initCornerstoneToolsAliases();
|
|
cy.get('@zoomBtn').click();
|
|
cy.wait(25);
|
|
|
|
//drags the mouse inside the viewport to be able to interact with series
|
|
cy.get('@viewport')
|
|
.trigger('mousedown', 'top', { buttons: 1 })
|
|
.trigger('mousemove', 'center', { buttons: 1 })
|
|
.trigger('mouseup');
|
|
});
|
|
|
|
Cypress.Commands.add('imageContrast', () => {
|
|
cy.initCornerstoneToolsAliases();
|
|
cy.get('@wwwcBtnPrimary').click();
|
|
cy.wait(25);
|
|
|
|
//drags the mouse inside the viewport to be able to interact with series
|
|
cy.get('@viewport')
|
|
.trigger('mousedown', 'center', { buttons: 1 })
|
|
.trigger('mousemove', 'top', { buttons: 1 })
|
|
.trigger('mouseup');
|
|
});
|
|
|
|
//Initialize aliases for Cornerstone tools buttons
|
|
Cypress.Commands.add('initCornerstoneToolsAliases', () => {
|
|
initCornerstoneToolsAliases();
|
|
});
|
|
|
|
//Initialize aliases for Common page elements
|
|
Cypress.Commands.add('initCommonElementsAliases', skipMarkers => {
|
|
initCommonElementsAliases(skipMarkers);
|
|
});
|
|
|
|
//Initialize aliases for Routes
|
|
Cypress.Commands.add('initRouteAliases', () => {
|
|
initRouteAliases();
|
|
});
|
|
|
|
//Initialize aliases for Study List page elements
|
|
Cypress.Commands.add('initStudyListAliasesOnDesktop', () => {
|
|
initStudyListAliasesOnDesktop();
|
|
});
|
|
|
|
//Add measurements in the viewport
|
|
Cypress.Commands.add(
|
|
'addLengthMeasurement',
|
|
(firstClick = [150, 100], secondClick = [130, 170]) => {
|
|
// Assign an alias to the button element
|
|
cy.get('@measurementToolsBtnPrimary').as('lengthButton');
|
|
|
|
cy.get('@lengthButton').should('have.attr', 'data-tool', 'Length');
|
|
|
|
cy.get('@lengthButton').then(button => {
|
|
// Only click the length tool if it is not active, in case the length tool is set up to
|
|
// toggle to inactive.
|
|
if (!button.is('.active')) {
|
|
cy.wrap(button).click();
|
|
}
|
|
});
|
|
|
|
cy.get('@lengthButton').should('have.attr', 'data-active', 'true');
|
|
|
|
cy.get('@viewport').then($viewport => {
|
|
const [x1, y1] = firstClick;
|
|
const [x2, y2] = secondClick;
|
|
|
|
cy.wrap($viewport)
|
|
.click(x1, y1, { force: true })
|
|
.wait(1000)
|
|
.click(x2, y2, { force: true })
|
|
.wait(1000);
|
|
});
|
|
}
|
|
);
|
|
|
|
// Add brush stroke in the viewport
|
|
Cypress.Commands.add('addBrush', (viewport, firstClick = [85, 100], secondClick = [85, 300]) => {
|
|
cy.get(viewport)
|
|
.first()
|
|
.then(viewportElement => {
|
|
const [x1, y1] = firstClick;
|
|
const [x2, y2] = secondClick;
|
|
|
|
const steps = 10;
|
|
const xStep = (x2 - x1) / steps;
|
|
const yStep = (y2 - y1) / steps;
|
|
|
|
cy.wrap(viewportElement)
|
|
.trigger('mousedown', x1, y1, { buttons: 1 })
|
|
.then(() => {
|
|
for (let i = 1; i <= steps; i++) {
|
|
let x = x1 + xStep * i;
|
|
let y = y1 + yStep * i;
|
|
cy.wrap(viewportElement).trigger('mousemove', x, y, { buttons: 1 });
|
|
}
|
|
})
|
|
.trigger('mouseup');
|
|
});
|
|
});
|
|
|
|
// Add erase stroke in the viewport
|
|
Cypress.Commands.add('addEraser', (viewport, firstClick = [85, 100], secondClick = [85, 300]) => {
|
|
cy.get(viewport)
|
|
.first()
|
|
.then(viewportElement => {
|
|
const [x1, y1] = firstClick;
|
|
const [x2, y2] = secondClick;
|
|
|
|
const steps = 10;
|
|
const xStep = (x2 - x1) / steps;
|
|
const yStep = (y2 - y1) / steps;
|
|
|
|
cy.wrap(viewportElement)
|
|
.trigger('mousedown', x1, y1, { buttons: 1 })
|
|
.then(() => {
|
|
for (let i = 1; i <= steps; i++) {
|
|
let x = x1 + xStep * i;
|
|
let y = y1 + yStep * i;
|
|
cy.wrap(viewportElement).trigger('mousemove', x, y, { buttons: 1 });
|
|
}
|
|
})
|
|
.trigger('mouseup');
|
|
});
|
|
});
|
|
|
|
//Add measurements in the viewport
|
|
Cypress.Commands.add(
|
|
'addAngleMeasurement',
|
|
(initPos = [180, 390], midPos = [300, 410], finalPos = [180, 450]) => {
|
|
cy.get('[data-cy="MeasurementTools-split-button-secondary"]').click();
|
|
cy.get('[data-cy="Angle"]').click();
|
|
|
|
cy.addAngle('.cornerstone-canvas', initPos, midPos, finalPos);
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Tests if element is NOT in viewport, or does not exist in DOM
|
|
*
|
|
* @param {string} element - element selector string or alias
|
|
* @returns
|
|
*/
|
|
Cypress.Commands.add('isNotInViewport', element => {
|
|
cy.get(element, { timeout: 3000 }).should($el => {
|
|
const bottom = Cypress.$(cy.state('window')).height() - 50;
|
|
const right = Cypress.$(cy.state('window')).width() - 50;
|
|
|
|
// If it's not visible, it's not in the viewport
|
|
if ($el) {
|
|
const rect = $el[0].getBoundingClientRect();
|
|
|
|
// TODO: support leftOf, above
|
|
const isBeneath = rect.top >= bottom && rect.bottom >= bottom;
|
|
const isRightOf = rect.left >= right && rect.right >= right;
|
|
const isNotInViewport = isBeneath && isRightOf;
|
|
|
|
expect(isNotInViewport).to.be.true;
|
|
}
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Tests if element is in viewport, or it does exist in DOM
|
|
*
|
|
* @param {string} element - element selector string or alias
|
|
* @returns
|
|
*/
|
|
Cypress.Commands.add('isInViewport', element => {
|
|
cy.get(element, { timeout: 3000 }).should($el => {
|
|
const bottom = Cypress.$(cy.state('window')).height();
|
|
const right = Cypress.$(cy.state('window')).width();
|
|
|
|
// If it's not visible, it's not in the viewport
|
|
if ($el) {
|
|
const rect = $el[0].getBoundingClientRect();
|
|
|
|
// TODO: support leftOf, above
|
|
const isBeneath = rect.top < bottom && rect.bottom < bottom;
|
|
const isRightOf = rect.left < right && rect.right < right;
|
|
const isInViewport = isBeneath && isRightOf;
|
|
|
|
expect(isInViewport).to.be.true;
|
|
}
|
|
});
|
|
});
|
|
|
|
Cypress.Commands.add('setLayout', (columns = 1, rows = 1) => {
|
|
cy.get('[data-cy="Layout"]').click();
|
|
|
|
cy.get(`[data-cy="Layout-${columns - 1}-${rows - 1}"]`).click();
|
|
|
|
cy.wait(10);
|
|
cy.waitDicomImage();
|
|
});
|
|
|
|
//Initialize aliases for User Preferences modal
|
|
Cypress.Commands.add('initPreferencesModalAliases', () => {
|
|
initPreferencesModalAliases();
|
|
});
|
|
|
|
Cypress.Commands.add('openPreferences', () => {
|
|
cy.log('Open User Preferences Modal');
|
|
// Open User Preferences modal
|
|
cy.get('body').then(body => {
|
|
if (body.find('.OHIFModal').length === 0) {
|
|
cy.get('[data-cy="options-chevron-down-icon"]')
|
|
.scrollIntoView()
|
|
.click()
|
|
.then(() => {
|
|
cy.get('[data-cy="options-dropdown"]').last().click().wait(200);
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
Cypress.Commands.add('scrollToIndex', index => {
|
|
cy.get('.cornerstone-viewport-element:visible')
|
|
.first()
|
|
.then($viewportEl => {
|
|
cy.window().then(win => {
|
|
const cornerstone = win.cornerstone;
|
|
const element = $viewportEl[0];
|
|
const enabledElement =
|
|
cornerstone?.getEnabledElement?.(element) ||
|
|
cornerstone?.getEnabledElements?.().find(item => item.element === element);
|
|
|
|
if (!enabledElement?.viewport) {
|
|
throw new Error('scrollToIndex: no enabled cornerstone viewport found');
|
|
}
|
|
|
|
const viewport = enabledElement.viewport;
|
|
const numberOfSlices = viewport.getNumberOfSlices?.() ?? 0;
|
|
|
|
if (!numberOfSlices) {
|
|
throw new Error('scrollToIndex: viewport has no slices');
|
|
}
|
|
|
|
const targetIndex = index < 0 ? numberOfSlices + index : index;
|
|
|
|
if (targetIndex < 0 || targetIndex >= numberOfSlices) {
|
|
throw new Error(
|
|
`scrollToIndex: index ${index} resolves to ${targetIndex}, but range is 0-${
|
|
numberOfSlices - 1
|
|
}`
|
|
);
|
|
}
|
|
|
|
return Cypress.Promise.resolve(
|
|
cornerstone.utilities.jumpToSlice(element, {
|
|
imageIndex: targetIndex,
|
|
debounceLoading: false,
|
|
})
|
|
).then(() => {
|
|
const currentIndex = viewport.getCurrentImageIdIndex?.();
|
|
|
|
if (currentIndex !== targetIndex) {
|
|
throw new Error(
|
|
`scrollToIndex: expected slice ${targetIndex}, but viewport is at ${currentIndex}`
|
|
);
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
// Give the viewport a brief settle time after programmatic jump.
|
|
cy.wait(50);
|
|
});
|
|
|
|
Cypress.Commands.add('closePreferences', () => {
|
|
cy.log('Close User Preferences Modal');
|
|
|
|
cy.get('body').then(body => {
|
|
// Close notification if displayed
|
|
if (body.find('.sb-closeIcon').length > 0) {
|
|
cy.get('.sb-closeIcon').first().click({ force: true });
|
|
}
|
|
|
|
// Close User Preferences Modal (if displayed)
|
|
if (body.find('.OHIFModal__header').length > 0) {
|
|
cy.get('[data-cy="close-button"]').click({ force: true });
|
|
}
|
|
});
|
|
});
|
|
|
|
Cypress.Commands.add('selectPreferencesTab', tabAlias => {
|
|
cy.initPreferencesModalAliases();
|
|
|
|
cy.get(tabAlias).as('selectedTab');
|
|
cy.get('@selectedTab').click();
|
|
cy.get('@selectedTab').should('have.class', 'active');
|
|
|
|
initPreferencesModalFooterBtnAliases();
|
|
});
|
|
|
|
Cypress.Commands.add('resetUserHotkeyPreferences', () => {
|
|
// Open User Preferences modal
|
|
cy.openPreferences();
|
|
|
|
cy.selectPreferencesTab('@userPreferencesHotkeysTab').then(() => {
|
|
cy.log('Reset Hotkeys to Default Preferences');
|
|
cy.get('@restoreBtn').click();
|
|
});
|
|
|
|
// Close Success Message overlay (if displayed)
|
|
cy.get('body').then(body => {
|
|
if (body.find('.sb-closeIcon').length > 0) {
|
|
cy.get('.sb-closeIcon').first().click({ force: true });
|
|
}
|
|
// Click on Save Button
|
|
cy.get('@saveBtn').click();
|
|
});
|
|
});
|
|
|
|
Cypress.Commands.add('resetUserGeneralPreferences', () => {
|
|
// Open User Preferences modal
|
|
cy.openPreferences();
|
|
|
|
cy.selectPreferencesTab('@userPreferencesGeneralTab').then(() => {
|
|
cy.log('Reset Language to Default Preferences');
|
|
cy.get('@restoreBtn').click();
|
|
});
|
|
|
|
// Close Success Message overlay (if displayed)
|
|
cy.get('body').then(body => {
|
|
if (body.find('.sb-closeIcon').length > 0) {
|
|
cy.get('.sb-closeIcon').first().click({ force: true });
|
|
}
|
|
// Click on Save Button
|
|
cy.get('@saveBtn').click();
|
|
});
|
|
});
|
|
|
|
Cypress.Commands.add('setNewHotkeyShortcutOnUserPreferencesModal', (function_label, shortcut) => {
|
|
// Within scopes all `.get` and `.contains` to within the matched elements
|
|
// dom instead of checking from document
|
|
cy.get('.HotkeysPreferences').within(() => {
|
|
cy.contains(function_label) // label we're looking for
|
|
.parent()
|
|
.find('input') // closest input to that label
|
|
.type(shortcut, { force: true }); // Set new shortcut for that function
|
|
});
|
|
});
|
|
|
|
Cypress.Commands.add(
|
|
'setWindowLevelPreset',
|
|
(preset_index, description_value, window_value, level_value) => {
|
|
let index = parseInt(preset_index) + 1;
|
|
|
|
// Set new Description value
|
|
cy.get(':nth-child(' + index + ') > .description > .preferencesInput')
|
|
.clear()
|
|
.type(description_value, {
|
|
force: true,
|
|
})
|
|
.blur();
|
|
|
|
// Set new Window value
|
|
cy.get(':nth-child(' + index + ') > .window > .preferencesInput')
|
|
.clear()
|
|
.type(window_value, {
|
|
force: true,
|
|
})
|
|
.blur();
|
|
|
|
// Set new Level value
|
|
cy.get(':nth-child(' + index + ') > .level > .preferencesInput')
|
|
.clear()
|
|
.type(level_value, {
|
|
force: true,
|
|
})
|
|
.blur();
|
|
}
|
|
);
|
|
|
|
Cypress.Commands.add('openDownloadImageModal', () => {
|
|
// Click on More button
|
|
cy.get('[data-cy="Capture"]').as('captureBtn').click();
|
|
});
|
|
|
|
Cypress.Commands.add('setLanguage', (language, save = true) => {
|
|
cy.openPreferences();
|
|
cy.initPreferencesModalAliases();
|
|
cy.selectPreferencesTab('@userPreferencesGeneralTab');
|
|
|
|
// Language dropdown should be displayed
|
|
cy.get('#language-select').should('be.visible');
|
|
|
|
// Select Language and Save/Cancel
|
|
cy.get('#language-select').select(language);
|
|
|
|
// Close Success Message overlay (if displayed)
|
|
cy.get('body').then(body => {
|
|
if (body.find('.sb-closeIcon').length > 0) {
|
|
cy.get('.sb-closeIcon').first().click({ force: true });
|
|
}
|
|
|
|
//Click on Save/Cancel button
|
|
const toClick = save ? '@saveBtn' : '@cancelBtn';
|
|
cy.get(toClick).scrollIntoView().click();
|
|
});
|
|
});
|
|
|
|
// hide noisy logs
|
|
// https://github.com/cypress-io/cypress/issues/7362
|
|
// uncomment this if you really need the network logs
|
|
const origLog = Cypress.log;
|
|
Cypress.log = function (opts, ...other) {
|
|
if (opts.displayName === 'script' || opts.name === 'request') {
|
|
return;
|
|
}
|
|
return origLog(opts, ...other);
|
|
};
|