From 9175bed21946a63740fb9ad4c83344dabf07ee11 Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Mon, 15 Sep 2025 15:28:44 -0400 Subject: [PATCH 1/3] chore: ensure teleport container --- unraid-ui/src/composables/useTeleport.ts | 46 ++++++++++++------------ 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/unraid-ui/src/composables/useTeleport.ts b/unraid-ui/src/composables/useTeleport.ts index 75739c8c48..7d8811944e 100644 --- a/unraid-ui/src/composables/useTeleport.ts +++ b/unraid-ui/src/composables/useTeleport.ts @@ -1,31 +1,29 @@ -import { computed } from 'vue'; +import { onMounted, ref } from 'vue'; + +let virtualModalContainer: HTMLDivElement | null = null; + +const ensureVirtualContainer = () => { + if (!virtualModalContainer) { + virtualModalContainer = document.createElement('div'); + virtualModalContainer.id = 'unraid-api-modals-virtual'; + virtualModalContainer.className = 'unapi'; + virtualModalContainer.style.position = 'relative'; + virtualModalContainer.style.zIndex = '999999'; + document.body.appendChild(virtualModalContainer); + } + return virtualModalContainer; +}; const useTeleport = () => { - // Computed property that finds the correct teleport target - const teleportTarget = computed(() => { - // #modals should be unique (id), but let's be defensive - const modalsElement = document.getElementById('modals'); - if (modalsElement) return `#modals`; + const teleportTarget = ref('#unraid-api-modals-virtual'); - // Find only mounted unraid-modals components (data-vue-mounted="true") - // This ensures we don't target unmounted or duplicate elements - const mountedModals = document.querySelector('unraid-modals[data-vue-mounted="true"]'); - if (mountedModals) { - // Check if it has the inner #modals div - const innerModals = mountedModals.querySelector('#modals'); - if (innerModals && innerModals.id) { - return `#${innerModals.id}`; - } - // Use the mounted component itself as fallback - // Add a unique identifier if it doesn't have one - if (!mountedModals.id) { - mountedModals.id = 'unraid-modals-teleport-target'; - } - return `#${mountedModals.id}`; + onMounted(() => { + const existingModals = document.getElementById('modals'); + if (existingModals) { + teleportTarget.value = '#modals'; + } else { + ensureVirtualContainer(); } - - // Final fallback to body - modals component not mounted yet - return 'body'; }); return { From 4f0e0fae11d0d2fbe6c91e4edc200ea6bc04bf07 Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Mon, 15 Sep 2025 15:31:56 -0400 Subject: [PATCH 2/3] refactor(useTeleport): simplify modal container initialization by always ensuring the virtual container --- unraid-ui/src/composables/useTeleport.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/unraid-ui/src/composables/useTeleport.ts b/unraid-ui/src/composables/useTeleport.ts index 7d8811944e..d0ec36663e 100644 --- a/unraid-ui/src/composables/useTeleport.ts +++ b/unraid-ui/src/composables/useTeleport.ts @@ -18,12 +18,7 @@ const useTeleport = () => { const teleportTarget = ref('#unraid-api-modals-virtual'); onMounted(() => { - const existingModals = document.getElementById('modals'); - if (existingModals) { - teleportTarget.value = '#modals'; - } else { - ensureVirtualContainer(); - } + ensureVirtualContainer(); }); return { From dd2f2f941e96ec4552ee91e465d67ef489d41ffd Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Mon, 15 Sep 2025 15:54:59 -0400 Subject: [PATCH 3/3] test(useTeleport): enhance tests for teleportTarget and virtual container management --- unraid-ui/src/composables/useTeleport.test.ts | 206 +++++------------- 1 file changed, 57 insertions(+), 149 deletions(-) diff --git a/unraid-ui/src/composables/useTeleport.test.ts b/unraid-ui/src/composables/useTeleport.test.ts index c7754c1245..67308d3c4e 100644 --- a/unraid-ui/src/composables/useTeleport.test.ts +++ b/unraid-ui/src/composables/useTeleport.test.ts @@ -1,169 +1,77 @@ import useTeleport from '@/composables/useTeleport'; +import { mount } from '@vue/test-utils'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; - -// Mock Vue's computed while preserving other exports -vi.mock('vue', async () => ({ - ...(await vi.importActual('vue')), - computed: vi.fn((fn) => { - const result = { value: fn() }; - return result; - }), -})); +import { defineComponent } from 'vue'; describe('useTeleport', () => { beforeEach(() => { // Clear the DOM before each test document.body.innerHTML = ''; - document.head.innerHTML = ''; vi.clearAllMocks(); }); afterEach(() => { - vi.restoreAllMocks(); - }); - - it('should return teleportTarget computed property', () => { - const { teleportTarget } = useTeleport(); - expect(teleportTarget).toBeDefined(); - expect(teleportTarget).toHaveProperty('value'); - }); - - it('should return #modals when element with id="modals" exists', () => { - // Create element with id="modals" - const modalsDiv = document.createElement('div'); - modalsDiv.id = 'modals'; - document.body.appendChild(modalsDiv); - - const { teleportTarget } = useTeleport(); - expect(teleportTarget.value).toBe('#modals'); - }); - - it('should prioritize #modals id over mounted unraid-modals', () => { - // Create both elements - const modalsDiv = document.createElement('div'); - modalsDiv.id = 'modals'; - document.body.appendChild(modalsDiv); - - const unraidModals = document.createElement('unraid-modals'); - unraidModals.setAttribute('data-vue-mounted', 'true'); - document.body.appendChild(unraidModals); - - const { teleportTarget } = useTeleport(); - expect(teleportTarget.value).toBe('#modals'); - }); - - it('should return mounted unraid-modals with inner #modals div', () => { - // Create mounted unraid-modals with inner modals div - const unraidModals = document.createElement('unraid-modals'); - unraidModals.setAttribute('data-vue-mounted', 'true'); - - const innerModals = document.createElement('div'); - innerModals.id = 'modals'; - unraidModals.appendChild(innerModals); - - document.body.appendChild(unraidModals); - - const { teleportTarget } = useTeleport(); - expect(teleportTarget.value).toBe('#modals'); - }); - - it('should add id to mounted unraid-modals when no inner modals div exists', () => { - // Create mounted unraid-modals without inner div - const unraidModals = document.createElement('unraid-modals'); - unraidModals.setAttribute('data-vue-mounted', 'true'); - document.body.appendChild(unraidModals); - - const { teleportTarget } = useTeleport(); - expect(unraidModals.id).toBe('unraid-modals-teleport-target'); - expect(teleportTarget.value).toBe('#unraid-modals-teleport-target'); - }); - - it('should use existing id of mounted unraid-modals if present', () => { - // Create mounted unraid-modals with existing id - const unraidModals = document.createElement('unraid-modals'); - unraidModals.setAttribute('data-vue-mounted', 'true'); - unraidModals.id = 'custom-modals-id'; - document.body.appendChild(unraidModals); - - const { teleportTarget } = useTeleport(); - expect(teleportTarget.value).toBe('#custom-modals-id'); + // Clean up virtual container if it exists + const virtualContainer = document.getElementById('unraid-api-modals-virtual'); + if (virtualContainer) { + virtualContainer.remove(); + } + // Reset the module to clear the virtualModalContainer variable + vi.resetModules(); }); - it('should ignore unmounted unraid-modals elements', () => { - // Create unmounted unraid-modals (without data-vue-mounted attribute) - const unraidModals = document.createElement('unraid-modals'); - document.body.appendChild(unraidModals); - + it('should return teleportTarget ref with correct value', () => { const { teleportTarget } = useTeleport(); - expect(teleportTarget.value).toBe('body'); + expect(teleportTarget.value).toBe('#unraid-api-modals-virtual'); }); - it('should ignore unraid-modals with data-vue-mounted="false"', () => { - // Create unraid-modals with data-vue-mounted="false" - const unraidModals = document.createElement('unraid-modals'); - unraidModals.setAttribute('data-vue-mounted', 'false'); - document.body.appendChild(unraidModals); - - const { teleportTarget } = useTeleport(); - expect(teleportTarget.value).toBe('body'); + it('should create virtual container element on mount with correct properties', () => { + const TestComponent = defineComponent({ + setup() { + const { teleportTarget } = useTeleport(); + return { teleportTarget }; + }, + template: '
{{ teleportTarget }}
', + }); + + // Initially, virtual container should not exist + expect(document.getElementById('unraid-api-modals-virtual')).toBeNull(); + + // Mount the component + mount(TestComponent); + + // After mount, virtual container should be created with correct properties + const virtualContainer = document.getElementById('unraid-api-modals-virtual'); + expect(virtualContainer).toBeTruthy(); + expect(virtualContainer?.className).toBe('unapi'); + expect(virtualContainer?.style.position).toBe('relative'); + expect(virtualContainer?.style.zIndex).toBe('999999'); + expect(virtualContainer?.parentElement).toBe(document.body); }); - it('should return body as fallback when no suitable target exists', () => { - // No elements in DOM - const { teleportTarget } = useTeleport(); - expect(teleportTarget.value).toBe('body'); - }); - - it('should handle multiple unraid-modals elements correctly', () => { - // Create multiple unraid-modals, only one mounted - const unmountedModals1 = document.createElement('unraid-modals'); - document.body.appendChild(unmountedModals1); - - const mountedModals = document.createElement('unraid-modals'); - mountedModals.setAttribute('data-vue-mounted', 'true'); - mountedModals.id = 'mounted-modals'; - document.body.appendChild(mountedModals); - - const unmountedModals2 = document.createElement('unraid-modals'); - document.body.appendChild(unmountedModals2); - - const { teleportTarget } = useTeleport(); - expect(teleportTarget.value).toBe('#mounted-modals'); - }); - - it('should handle nested modal elements correctly', () => { - // Create nested structure - const container = document.createElement('div'); - - const unraidModals = document.createElement('unraid-modals'); - unraidModals.setAttribute('data-vue-mounted', 'true'); - - const innerDiv = document.createElement('div'); - const innerModals = document.createElement('div'); - innerModals.id = 'modals'; - - innerDiv.appendChild(innerModals); - unraidModals.appendChild(innerDiv); - container.appendChild(unraidModals); - document.body.appendChild(container); - - const { teleportTarget } = useTeleport(); - expect(teleportTarget.value).toBe('#modals'); - }); - - it('should be reactive to DOM changes', () => { - const { teleportTarget } = useTeleport(); - - // Initially should be body - expect(teleportTarget.value).toBe('body'); - - // Add modals element - const modalsDiv = document.createElement('div'); - modalsDiv.id = 'modals'; - document.body.appendChild(modalsDiv); - - // Recreate the composable to test updated DOM state - const { teleportTarget: newTeleportTarget } = useTeleport(); - expect(newTeleportTarget.value).toBe('#modals'); + it('should reuse existing virtual container within same test', () => { + // Manually create the container first + const manualContainer = document.createElement('div'); + manualContainer.id = 'unraid-api-modals-virtual'; + manualContainer.className = 'unapi'; + manualContainer.style.position = 'relative'; + manualContainer.style.zIndex = '999999'; + document.body.appendChild(manualContainer); + + const TestComponent = defineComponent({ + setup() { + const { teleportTarget } = useTeleport(); + return { teleportTarget }; + }, + template: '
{{ teleportTarget }}
', + }); + + // Mount component - should not create a new container + mount(TestComponent); + + // Should still have only one container + const containers = document.querySelectorAll('#unraid-api-modals-virtual'); + expect(containers.length).toBe(1); + expect(containers[0]).toBe(manualContainer); }); });