diff --git a/src/components/LayerProperties.vue b/src/components/LayerProperties.vue index ad9495aa1..7ecde1f2c 100644 --- a/src/components/LayerProperties.vue +++ b/src/components/LayerProperties.vue @@ -83,6 +83,7 @@ export default defineComponent({ thumb-label :model-value="blendConfig.opacity" @update:model-value="setBlendConfig('opacity', $event)" + data-testid="layer-opacity-slider" /> </div> </template> diff --git a/src/components/ModulePanel.vue b/src/components/ModulePanel.vue index 690bcb6c8..2e3d4ee6f 100644 --- a/src/components/ModulePanel.vue +++ b/src/components/ModulePanel.vue @@ -7,7 +7,11 @@ icons-and-text show-arrows > - <v-tab v-for="item in Modules" :key="item.name"> + <v-tab + v-for="item in Modules" + :key="item.name" + :data-testid="`module-tab-${item.name}`" + > <div class="tab-content"> <span class="mb-0 mt-1 module-text">{{ item.name }}</span> <v-icon>mdi-{{ item.icon }}</v-icon> diff --git a/src/components/PatientStudyVolumeBrowser.vue b/src/components/PatientStudyVolumeBrowser.vue index e4be12c1c..7e21769fb 100644 --- a/src/components/PatientStudyVolumeBrowser.vue +++ b/src/components/PatientStudyVolumeBrowser.vue @@ -291,12 +291,14 @@ export default defineComponent({ size="x-small" class="dataset-menu" @click.stop + data-testid="dataset-menu-button" > <v-menu activator="parent"> <v-list> <v-list-item v-if="volume.layerable" @click.stop="volume.layerHandler()" + data-testid="dataset-menu-layer-item" > <template v-if="volume.layerLoading"> <div style="margin: 0 auto"> diff --git a/src/io/manifest.ts b/src/io/manifest.ts index d132a80aa..eb69f17c3 100644 --- a/src/io/manifest.ts +++ b/src/io/manifest.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; -const RemoteResource = z.object({ +export const RemoteResource = z.object({ url: z.string(), name: z.optional(z.string()), }); diff --git a/tests/pageobjects/volview.page.ts b/tests/pageobjects/volview.page.ts index ce04b4d23..1d1df733c 100644 --- a/tests/pageobjects/volview.page.ts +++ b/tests/pageobjects/volview.page.ts @@ -149,6 +149,18 @@ class VolViewPage extends Page { get editLabelModalDoneButton() { return $('button[data-testid="edit-label-done-button"]'); } + + get datasetMenuButtons() { + return $$('button[data-testid="dataset-menu-button"]'); + } + + get renderingModuleTab() { + return $('button[data-testid="module-tab-Rendering"]'); + } + + get layerOpacitySliders() { + return $$('div[data-testid="layer-opacity-slider"] input'); + } } export const volViewPage = new VolViewPage(); diff --git a/tests/specs/layers.e2e.ts b/tests/specs/layers.e2e.ts new file mode 100644 index 000000000..f1ecfc010 --- /dev/null +++ b/tests/specs/layers.e2e.ts @@ -0,0 +1,60 @@ +import { volViewPage } from '../pageobjects/volview.page'; +import { openUrls } from './utils'; + +describe('Add Layer button', () => { + it('should create overlay with 2 DICOM images', async () => { + await openUrls([ + { + 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', + }, + ]); + + // Wait for both volumes to appear in list + await browser.waitUntil( + async () => { + const menus = await volViewPage.datasetMenuButtons; + return menus.length >= 2; + }, + { + timeout: 30000, + timeoutMsg: `Expected 2 volumes to appear in list`, + } + ); + + const menus = await volViewPage.datasetMenuButtons; + await menus[0].click(); + await browser.waitUntil( + async () => { + const addLayerButton = await $( + 'div[data-testid="dataset-menu-layer-item"]' + ); + return addLayerButton.isClickable(); + }, + { timeoutMsg: 'Expected clickable Add Layer button' } + ); + + const addLayerButton = await $( + 'div[data-testid="dataset-menu-layer-item"]' + ); + await addLayerButton.click(); + + const renderTab = await volViewPage.renderingModuleTab; + await renderTab.click(); + + // need to wait a little for layer section to render + await browser.waitUntil( + async function slidersExist() { + const layerOpacitySliders = await volViewPage.layerOpacitySliders; + return layerOpacitySliders.length > 0; + }, + { + timeoutMsg: `Expected at least one layer opacity slider`, + } + ); + }); +}); diff --git a/tests/specs/remote-manifest.e2e.ts b/tests/specs/remote-manifest.e2e.ts index e76f282ee..fe0583f8c 100644 --- a/tests/specs/remote-manifest.e2e.ts +++ b/tests/specs/remote-manifest.e2e.ts @@ -1,23 +1,5 @@ -import * as path from 'path'; -import * as fs from 'fs'; -import { cleanuptotal } from 'wdio-cleanuptotal-service'; -import { TEMP_DIR } from '../../wdio.shared.conf'; import { volViewPage } from '../pageobjects/volview.page'; -import { downloadFile } from './utils'; - -async function writeManifestToFile(manifest: any, fileName: string) { - const filePath = path.join(TEMP_DIR, fileName); - await fs.promises.writeFile(filePath, JSON.stringify(manifest)); - cleanuptotal.addCleanup(async () => { - fs.unlinkSync(filePath); - }); - return filePath; -} - -async function openVolViewPage(fileName: string) { - const urlParams = `?urls=[tmp/${fileName}]`; - await volViewPage.open(urlParams); -} +import { downloadFile, writeManifestToFile, openVolViewPage } from './utils'; describe('VolView loading of remoteManifest.json', () => { it('should show error when there is no name and URL is malformed', async () => { diff --git a/tests/specs/utils.ts b/tests/specs/utils.ts index f48424353..dead29e7b 100644 --- a/tests/specs/utils.ts +++ b/tests/specs/utils.ts @@ -1,6 +1,10 @@ import * as path from 'path'; import * as fs from 'fs'; +import { z } from 'zod'; +import { cleanuptotal } from 'wdio-cleanuptotal-service'; import { TEMP_DIR } from '../../wdio.shared.conf'; +import { volViewPage } from '../pageobjects/volview.page'; +import { RemoteResource } from '../../src/io/manifest'; // File is not automatically deleted export const downloadFile = async (url: string, fileName: string) => { @@ -13,3 +17,34 @@ export const downloadFile = async (url: string, fileName: string) => { } return savePath; }; + +export async function writeManifestToFile(manifest: any, fileName: string) { + const filePath = path.join(TEMP_DIR, fileName); + await fs.promises.writeFile(filePath, JSON.stringify(manifest)); + cleanuptotal.addCleanup(async () => { + fs.unlinkSync(filePath); + }); + return filePath; +} + +export async function openVolViewPage(fileName: string) { + const urlParams = `?urls=[tmp/${fileName}]`; + await volViewPage.open(urlParams); +} + +type RemoteResourceType = z.infer<typeof RemoteResource> & { name: string }; + +export async function openUrls(urlsAndNames: Array<RemoteResourceType>) { + await Promise.all( + urlsAndNames.map((resource) => downloadFile(resource.url, resource.name)) + ); + + const resources = urlsAndNames.map(({ name }) => ({ url: `/tmp/${name}` })); + const manifest = { + resources, + }; + const fileName = 'openUrlsManifest.json'; + await writeManifestToFile(manifest, fileName); + await openVolViewPage(fileName); + await volViewPage.waitForViews(); +}