/* eslint-disable vue/one-component-per-file */ import { mount, shallowMount } from '@vue/test-utils' import { describe, expect, it } from 'vitest' import { defineComponent } from 'vue' import { VCard } from 'vuetify/components' import DialogBox from '../DialogBox.vue' const defaultProps = { modelValue: true, title: 'Test title', width: '600px', cancelBtnText: 'Cancel', confirmBtnText: 'Confirm', hideActions: false, persistent: false, } describe('DialogBox', () => { describe('rendering and props', () => { it('renders correctly with props', () => { const wrapper = mount(DialogBox, { props: { ...defaultProps, headingLevel: 2, }, global: { stubs: { VDialog: { template: '
Test
', }, }, }, attachTo: document.body, }) expect(wrapper.html()).toContain('Test') }) it('is closed when model value is false', async () => { const wrapper = mount(DialogBox, { props: { ...defaultProps, modelValue: false, headingLevel: 2, }, attachTo: document.body, }) expect(wrapper.html()).toBe('') wrapper.unmount() }) it('becomes visible when the model value is updated', async () => { const wrapper = mount(DialogBox, { props: { ...defaultProps, headingLevel: 2, }, }) // Initially visible with modelValue: true const card = wrapper.findComponent(VCard) expect(card.exists()).toBe(true) // Set modelValue to false - dialog should be hidden await wrapper.setProps({ modelValue: false }) await wrapper.vm.$nextTick() // Check if the dialog is actually closed by looking for the dialog wrapper const dialogWrapper = wrapper.find('.v-dialog') if (dialogWrapper.exists()) { // If dialog wrapper exists, check if it's hidden via CSS const dialogElement = dialogWrapper.element as HTMLElement const isHidden = dialogElement.style.display === 'none' || !dialogElement.offsetParent || dialogElement.style.visibility === 'hidden' expect(isHidden).toBe(true) } else { // Dialog wrapper doesn't exist, which means it's properly hidden expect(dialogWrapper.exists()).toBe(false) } // Set modelValue back to true - dialog should be visible again await wrapper.setProps({ modelValue: true }) await wrapper.vm.$nextTick() // Dialog should be visible again const cardAfterReopen = wrapper.findComponent(VCard) expect(cardAfterReopen.exists()).toBe(true) wrapper.unmount() }) it('renders the title slot', async () => { const wrapper = mount(DialogBox, { slots: { title: '

Test title

', }, props: { ...defaultProps, headingLevel: 2, }, attachTo: document.body, }) const modal = wrapper.getComponent(VCard) const title = modal.find('h2').text() await modal.vm.$nextTick() expect(title).toBe('Test title') wrapper.unmount() }) }) describe('focusable elements and tab navigation', () => { it('gets the correct focusable elements', async () => { const wrapper = mount(DialogBox, { slots: { default: ` ameli.fr `, }, props: { ...defaultProps, hideActions: true, persistent: true, headingLevel: 2, }, attachTo: document.body, }) const modal = wrapper.getComponent(VCard) const firstBtn = modal.find('#first') const thirdBtn = modal.find('#third') const theLink = modal.find('#link') await modal.vm.$nextTick() // @ts-expect-error - Testing private method expect(await wrapper.vm.getSelectableElements()).toEqual([ firstBtn.element, thirdBtn.element, theLink.element, ]) wrapper.unmount() }) it('handles the internal tab navigation', async () => { const wrapper = mount(DialogBox, { slots: { default: ` ameli.fr `, }, props: { ...defaultProps, hideActions: true, persistent: true, headingLevel: 2, }, attachTo: document.body, }) async function triggerTab() { await modal.find(':focus').trigger('keydown', { keyCode: 9, key: 'Tab', code: 'Tab', }) } async function triggerShiftTab() { await modal.find(':focus').trigger('keydown', { keyCode: 9, key: 'Tab', code: 'Tab', shiftKey: true, }) } const modal = wrapper.getComponent(VCard) const firstBtn = modal.find('#first') const link = modal.find('#link') await modal.vm.$nextTick() firstBtn.element.focus() await modal.vm.$nextTick() await triggerShiftTab() expect(link.element).toBe(document.activeElement) await triggerTab() expect(firstBtn.element).toBe(document.activeElement) wrapper.unmount() }) it('return to the first focusable element', async () => { const testComponent = defineComponent({ components: { DialogBox }, setup() { return { dialog: true, } }, template: ` ameli.fr `, }) const wrapper = mount(testComponent, { attachTo: document.body, }) const externalBtn = wrapper.find('#external') externalBtn.element.focus() await wrapper.vm.$nextTick() await externalBtn.trigger('keydown', { keyCode: 9, key: 'Tab', code: 'Tab', }) const modal = wrapper.getComponent(VCard) const firstBtn = modal.find('#first') firstBtn.element.focus() await modal.vm.$nextTick() expect(firstBtn.element).toEqual(document.activeElement) wrapper.unmount() }) it('return the last focusable element', async () => { const testComponent = defineComponent({ components: { DialogBox }, setup() { return { dialog: true, } }, template: ` ameli.fr `, }) const wrapper = mount(testComponent, { attachTo: document.body, }) const external = wrapper.find('#external') const modal = wrapper.getComponent(VCard) external.element.focus() await wrapper.vm.$nextTick() await modal.trigger('keydown', { keyCode: 9, key: 'Tab', code: 'Tab', shiftKey: true, }) const link = modal.find('#link') expect(link.element).toBe(document.activeElement) wrapper.unmount() }) }) describe('event emissions', () => { it('emits an event when close button is clicked', async () => { const wrapper = mount(DialogBox, { props: { modelValue: true, headingLevel: 2, }, attachTo: document.body, }) const modal = wrapper.getComponent(VCard) expect(modal.isVisible()).toBe(true) const closeBtn = modal.find('button') await closeBtn.trigger('click') expect(wrapper.emitted('update:modelValue')).toBeTruthy() wrapper.unmount() }) it('emits a cancel event when cancel button is clicked', async () => { const wrapper = mount(DialogBox, { props: { ...defaultProps, headingLevel: 2, }, attachTo: document.body, }) const modal = wrapper.getComponent(VCard) const cancelBtn = modal.find('.sy-dialog-box-actions-ctn button') await cancelBtn.trigger('click') expect(wrapper.emitted('cancel')).toBeTruthy() wrapper.unmount() }) it('emits a confirm event when confirm button is clicked', async () => { const wrapper = mount(DialogBox, { props: { ...defaultProps, headingLevel: 2, }, attachTo: document.body, }) const modal = wrapper.getComponent(VCard) const confirmBtn = modal.find('[data-test-id="confirm-btn"]') await confirmBtn.trigger('click') expect(wrapper.emitted('confirm')).toBeTruthy() wrapper.unmount() }) }) describe('Test methods', () => { it('getSelectableElements if this.$refs.dialogContent.$el is undefined', async () => { const wrapper = shallowMount(DialogBox, { props: { ...defaultProps, headingLevel: 2, }, attachTo: document.body, }) // @ts-expect-error - Testing private method const result = await wrapper.vm.getSelectableElements() expect(result).toEqual([]) wrapper.unmount() }) }) it('focus the confirm button on open if autofocusValidateBtn is true', async () => { const wrapper = mount(DialogBox, { props: { ...defaultProps, autofocusValidateBtn: true, headingLevel: 2, }, attachTo: document.body, }) const modal = wrapper.getComponent(VCard) const confirmBtn = modal.find('[data-test-id="confirm-btn"]') await wrapper.vm.$nextTick() expect(confirmBtn.element).toEqual(document.activeElement) wrapper.unmount() }) describe('draggable dialog', () => { it('renders the dialog as draggable when the draggable prop is true', async () => { const wrapper = mount(DialogBox, { props: { ...defaultProps, draggable: true, headingLevel: 2, }, attachTo: document.body, }) const card = wrapper.getComponent(VCard) const titleBar = card.find('.sy-dialog-box-title') expect(card.classes()).toContain('sy-dialog-box-draggable') titleBar.trigger('mousedown', { clientX: 100, clientY: 100 }) await wrapper.vm.$nextTick() expect(card.classes()).toContain('sy-dialog-box-draggable--active') await wrapper.trigger('mousemove', { clientX: 200, clientY: 200 }) await wrapper.vm.$nextTick() const overlayElement = card.element.closest('.v-overlay__content') as HTMLElement expect(overlayElement.style.left).toBe('100px') // Check that left style has been set expect(overlayElement.style.top).toBe('100px') // Check that top style has been set await wrapper.trigger('mouseup') expect(card.classes()).not.toContain('sy-dialog-box-draggable--active') wrapper.unmount() }) it('do not allow the dialog to be dragged outside the viewport', async () => { const wrapper = mount(DialogBox, { props: { ...defaultProps, draggable: true, headingLevel: 2, }, attachTo: document.body, }) const card = wrapper.getComponent(VCard) const titleBar = card.find('.sy-dialog-box-title') titleBar.trigger('mousedown', { clientX: 100, clientY: 100 }) await wrapper.vm.$nextTick() await wrapper.trigger('mousemove', { clientX: -1000, clientY: -1000 }) await wrapper.vm.$nextTick() const overlayElement = card.element.closest('.v-overlay__content') as HTMLElement expect(parseInt(overlayElement.style.left, 10)).toBe(0) expect(parseInt(overlayElement.style.top, 10)).toBe(0) await wrapper.trigger('mousemove', { clientX: 10000, clientY: 10000 }) await wrapper.vm.$nextTick() const windowWidth = window.innerWidth const windowHeight = window.innerHeight const overlayWidth = overlayElement.offsetWidth const overlayHeight = overlayElement.offsetHeight expect(parseInt(overlayElement.style.left, 10)).toBe(windowWidth - overlayWidth) expect(parseInt(overlayElement.style.top, 10)).toBe(windowHeight - overlayHeight) await wrapper.trigger('mouseup') wrapper.unmount() }) it('move the dialog to the left when the left arrow is pressed', async () => { const wrapper = mount(DialogBox, { props: { ...defaultProps, draggable: true, headingLevel: 2, }, attachTo: document.body, }) const card = wrapper.getComponent(VCard) const titleBar = card.find('.sy-dialog-box-title') await titleBar.trigger('keydown', { key: 'ArrowLeft' }) await wrapper.vm.$nextTick() const overlayElement = card.element.closest('.v-overlay__content') as HTMLElement expect(overlayElement.style.position).toBe('absolute') expect(overlayElement.style.left).toBe(`0px`) wrapper.unmount() }) it('move the dialog to the right when the right arrow is pressed', async () => { const wrapper = mount(DialogBox, { props: { ...defaultProps, draggable: true, headingLevel: 2, }, attachTo: document.body, }) const card = wrapper.getComponent(VCard) const titleBar = card.find('.sy-dialog-box-title') await titleBar.trigger('keydown', { key: 'ArrowRight' }) await wrapper.vm.$nextTick() const overlayElement = card.element.closest('.v-overlay__content') as HTMLElement const computedStyle = getComputedStyle(overlayElement) const marginLeft = parseFloat(computedStyle.marginLeft) || 0 const positionToLeft = window.innerWidth - overlayElement.offsetWidth - marginLeft * 2 expect(overlayElement.style.position).toBe('absolute') expect(overlayElement.style.left).toBe(`${positionToLeft}px`) wrapper.unmount() }) }) it('move the dialog to the top when the up arrow is pressed', async () => { const wrapper = mount(DialogBox, { props: { ...defaultProps, draggable: true, headingLevel: 2, }, attachTo: document.body, }) const card = wrapper.getComponent(VCard) const titleBar = card.find('.sy-dialog-box-title') await titleBar.trigger('keydown', { key: 'ArrowUp' }) await wrapper.vm.$nextTick() const overlayElement = card.element.closest('.v-overlay__content') as HTMLElement expect(overlayElement.style.position).toBe('absolute') expect(overlayElement.style.top).toBe(`0px`) wrapper.unmount() }) it('move the dialog to the bottom when the down arrow is pressed', async () => { const wrapper = mount(DialogBox, { props: { ...defaultProps, draggable: true, headingLevel: 2, }, attachTo: document.body, }) const card = wrapper.getComponent(VCard) const titleBar = card.find('.sy-dialog-box-title') await titleBar.trigger('keydown', { key: 'ArrowDown' }) await wrapper.vm.$nextTick() const overlayElement = card.element.closest('.v-overlay__content') as HTMLElement const computedStyle = getComputedStyle(overlayElement) const marginTop = parseFloat(computedStyle.marginTop) || 0 const positionToTop = window.innerHeight - overlayElement.offsetHeight - marginTop * 2 expect(overlayElement.style.position).toBe('absolute') expect(overlayElement.style.top).toBe(`${positionToTop}px`) wrapper.unmount() }) it('move the dialog to the left when the window is resized and the dialog is out of viewport', async () => { const wrapper = mount(DialogBox, { props: { ...defaultProps, draggable: true, headingLevel: 2, }, attachTo: document.body, }) const card = wrapper.getComponent(VCard) const titleBar = card.find('.sy-dialog-box-title') titleBar.trigger('mousedown', { clientX: 100, clientY: 100 }) await wrapper.vm.$nextTick() await wrapper.trigger('mousemove', { clientX: 800, clientY: 100 }) await wrapper.vm.$nextTick() const overlayElement = card.element.closest('.v-overlay__content') as HTMLElement const left = parseFloat(overlayElement.style.left) || 0 expect(left).toBe(700) Object.defineProperty(window, 'innerWidth', { writable: true, configurable: true, value: 300, }) window.dispatchEvent(new Event('resize')) await wrapper.vm.$nextTick() const newLeft = parseFloat(overlayElement.style.left) || 0 expect(newLeft).toBe(300 - overlayElement.offsetWidth) wrapper.unmount() }) it('move the dialog to the top when the window is resized and the dialog is out of viewport', async () => { const wrapper = mount(DialogBox, { props: { ...defaultProps, draggable: true, headingLevel: 2, }, attachTo: document.body, }) const card = wrapper.getComponent(VCard) const titleBar = card.find('.sy-dialog-box-title') titleBar.trigger('mousedown', { clientX: 100, clientY: 100 }) await wrapper.vm.$nextTick() await wrapper.trigger('mousemove', { clientX: 100, clientY: 800 }) await wrapper.vm.$nextTick() const overlayElement = card.element.closest('.v-overlay__content') as HTMLElement const top = parseFloat(overlayElement.style.top) || 0 expect(top).toBe(700) Object.defineProperty(window, 'innerHeight', { writable: true, configurable: true, value: 300, }) window.dispatchEvent(new Event('resize')) await wrapper.vm.$nextTick() const newTop = parseFloat(overlayElement.style.top) || 0 expect(newTop).toBe(300 - overlayElement.offsetHeight) wrapper.unmount() }) })