mirror of
https://github.com/unraid/api.git
synced 2026-02-09 09:29:12 -06:00
fix: use virtual-modal-container (#1709)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - Bug Fixes - Ensured modals consistently render by using a dedicated container, reducing cases where dialogs failed to open or appeared in the wrong place. - Improved reliability of modal mounting during page load and navigation. - Refactor - Simplified the modal mounting mechanism to improve stability and reduce reliance on DOM structure assumptions. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -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();
|
||||
// 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 return teleportTarget computed property', () => {
|
||||
it('should return teleportTarget ref with correct value', () => {
|
||||
const { teleportTarget } = useTeleport();
|
||||
expect(teleportTarget).toBeDefined();
|
||||
expect(teleportTarget).toHaveProperty('value');
|
||||
expect(teleportTarget.value).toBe('#unraid-api-modals-virtual');
|
||||
});
|
||||
|
||||
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);
|
||||
it('should create virtual container element on mount with correct properties', () => {
|
||||
const TestComponent = defineComponent({
|
||||
setup() {
|
||||
const { teleportTarget } = useTeleport();
|
||||
return { teleportTarget };
|
||||
},
|
||||
template: '<div>{{ teleportTarget }}</div>',
|
||||
});
|
||||
|
||||
const { teleportTarget } = useTeleport();
|
||||
expect(teleportTarget.value).toBe('#modals');
|
||||
// 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 prioritize #modals id over mounted unraid-modals', () => {
|
||||
// Create both elements
|
||||
const modalsDiv = document.createElement('div');
|
||||
modalsDiv.id = 'modals';
|
||||
document.body.appendChild(modalsDiv);
|
||||
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 unraidModals = document.createElement('unraid-modals');
|
||||
unraidModals.setAttribute('data-vue-mounted', 'true');
|
||||
document.body.appendChild(unraidModals);
|
||||
const TestComponent = defineComponent({
|
||||
setup() {
|
||||
const { teleportTarget } = useTeleport();
|
||||
return { teleportTarget };
|
||||
},
|
||||
template: '<div>{{ teleportTarget }}</div>',
|
||||
});
|
||||
|
||||
const { teleportTarget } = useTeleport();
|
||||
expect(teleportTarget.value).toBe('#modals');
|
||||
});
|
||||
// Mount component - should not create a new container
|
||||
mount(TestComponent);
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
const { teleportTarget } = useTeleport();
|
||||
expect(teleportTarget.value).toBe('body');
|
||||
});
|
||||
|
||||
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 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');
|
||||
// Should still have only one container
|
||||
const containers = document.querySelectorAll('#unraid-api-modals-virtual');
|
||||
expect(containers.length).toBe(1);
|
||||
expect(containers[0]).toBe(manualContainer);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,31 +1,24 @@
|
||||
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<string>('#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}`;
|
||||
}
|
||||
|
||||
// Final fallback to body - modals component not mounted yet
|
||||
return 'body';
|
||||
onMounted(() => {
|
||||
ensureVirtualContainer();
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user