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();
+}