UNPKG

@mozaic-ds/vue

Version:

Mozaic-Vue is the Vue.js implementation of ADEO Design system

281 lines (242 loc) 9.05 kB
import { mount } from '@vue/test-utils'; import { describe, it, expect, vi, beforeAll, afterAll } from 'vitest'; import { nextTick } from 'vue'; import MCarousel from './MCarousel.vue'; import MIconButton from '../iconbutton/MIconButton.vue'; import { ChevronLeft20, ChevronRight20 } from '@mozaic-ds/icons-vue'; /* eslint-disable @typescript-eslint/no-explicit-any */ class MockResizeObserver { callback: any; constructor(callback: any) { this.callback = callback; } observe = vi.fn(); unobserve = vi.fn(); disconnect = vi.fn(); } describe('MCarousel component', () => { let originalResizeObserver: any; beforeAll(() => { originalResizeObserver = global.ResizeObserver; global.ResizeObserver = MockResizeObserver as any; Object.defineProperty(window.HTMLElement.prototype, 'scrollIntoView', { value: vi.fn(), writable: true, }); // Mock getBoundingClientRect for visibility tests window.HTMLElement.prototype.getBoundingClientRect = vi.fn(() => ({ left: 0, top: 0, right: 100, bottom: 100, width: 100, height: 100, x: 0, y: 0, toJSON: () => {}, })); }); afterAll(() => { global.ResizeObserver = originalResizeObserver; }); const mockChildren = [ '<div class="slide">Slide 1</div>', '<div class="slide">Slide 2</div>', '<div class="slide">Slide 3</div>', ]; const mountCarousel = (options = {}) => mount(MCarousel, { attachTo: document.body, slots: { default: mockChildren.join(''), header: '<h2 id="mc-carousel__title">Carousel Header</h2>', }, ...options, }); it('renders correctly with header and default slot', () => { const wrapper = mountCarousel(); expect(wrapper.find('.mc-carousel__headings').text()).toContain( 'Carousel Header', ); expect(wrapper.findAll('.slide')).toHaveLength(3); }); it('renders navigation buttons with correct aria labels', () => { const wrapper = mountCarousel({ props: { previousButtonAriaLabel: 'Go back', nextButtonAriaLabel: 'Go forward', }, }); const buttons = wrapper.findAllComponents(MIconButton); expect(buttons).toHaveLength(2); expect(buttons[0].attributes('aria-label')).toBe('Go back'); expect(buttons[1].attributes('aria-label')).toBe('Go forward'); }); it('renders default aria labels when not provided', () => { const wrapper = mountCarousel(); const buttons = wrapper.findAllComponents(MIconButton); expect(buttons[0].attributes('aria-label')).toBe('previous'); expect(buttons[1].attributes('aria-label')).toBe('next'); }); it('renders icon components inside navigation buttons', () => { const wrapper = mountCarousel(); expect(wrapper.findComponent(ChevronLeft20).exists()).toBe(true); expect(wrapper.findComponent(ChevronRight20).exists()).toBe(true); }); it('disables the previous button when on the first slide', async () => { const wrapper = mountCarousel(); // Mock container at start position const contentContainer = wrapper.find('.mc-carousel__content'); Object.defineProperty(contentContainer.element, 'scrollLeft', { value: 0, writable: true, configurable: true, }); Object.defineProperty(contentContainer.element, 'scrollWidth', { value: 300, writable: true, configurable: true, }); Object.defineProperty(contentContainer.element, 'clientWidth', { value: 100, writable: true, configurable: true, }); // Trigger scroll to update state await contentContainer.trigger('scroll'); await nextTick(); const [prevButton] = wrapper.findAllComponents(MIconButton); expect(prevButton.props('disabled')).toBe(true); }); it('enables next button when not on last slide', async () => { const wrapper = mountCarousel({ slots: { default: '<div class="slide">Slide 1</div><div class="slide">Slide 2</div><div class="slide">Slide 3</div>', header: '<h2 id="mc-carousel__title">Carousel Header</h2>', }, }); // Mock container with scrollable content (on first slide) const contentContainer = wrapper.find('.mc-carousel__content'); Object.defineProperty(contentContainer.element, 'scrollLeft', { value: 0, writable: true, configurable: true, }); Object.defineProperty(contentContainer.element, 'scrollWidth', { value: 300, writable: true, configurable: true, }); Object.defineProperty(contentContainer.element, 'clientWidth', { value: 100, writable: true, configurable: true, }); // Trigger scroll to update state await contentContainer.trigger('scroll'); await nextTick(); await new Promise((resolve) => setTimeout(resolve, 200)); const buttons = wrapper.findAllComponents(MIconButton); const nextButton = buttons[1]; expect(nextButton.props('disabled')).toBe(false); }); it('sets correct ARIA attributes on main container', () => { const wrapper = mountCarousel(); const container = wrapper.find('.mc-carousel'); expect(container.attributes('role')).toBe('group'); expect(container.attributes('aria-roledescription')).toBe('carousel'); // aria-labelledby should point to the headings wrapper (dynamic id) const labelledby = container.attributes('aria-labelledby'); expect(labelledby).toBeDefined(); expect(wrapper.find(`#${labelledby}`).exists()).toBe(true); }); it('slide container has aria-live="polite"', () => { const wrapper = mountCarousel(); const content = wrapper.find('.mc-carousel__content'); expect(content.attributes('aria-live')).toBe('polite'); }); it('disables next button when there is only one child', async () => { const wrapper = mountCarousel({ slots: { default: '<div class="slide">Slide 1</div>', header: '<h2 id="mc-carousel__title">Carousel Header</h2>', }, }); // Wait for component to initialize await nextTick(); await new Promise((resolve) => setTimeout(resolve, 100)); const buttons = wrapper.findAllComponents(MIconButton); const nextButton = buttons[1]; // Next button should be disabled when there's only one child (which is the last) expect(nextButton.props('disabled')).toBe(true); }); it('enables next button when there are multiple children and not on last', async () => { const wrapper = mountCarousel({ slots: { default: '<div class="slide">Slide 1</div><div class="slide">Slide 2</div>', header: '<h2 id="mc-carousel__title">Carousel Header</h2>', }, }); // Mock container to simulate being on first slide with more content to scroll const contentContainer = wrapper.find('.mc-carousel__content'); Object.defineProperty(contentContainer.element, 'scrollLeft', { value: 0, writable: true, configurable: true, }); Object.defineProperty(contentContainer.element, 'scrollWidth', { value: 200, writable: true, configurable: true, }); Object.defineProperty(contentContainer.element, 'clientWidth', { value: 100, writable: true, configurable: true, }); // Trigger scroll to update state await contentContainer.trigger('scroll'); await nextTick(); await new Promise((resolve) => setTimeout(resolve, 200)); const buttons = wrapper.findAllComponents(MIconButton); const nextButton = buttons[1]; // Next button should be enabled when on first slide of two expect(nextButton.props('disabled')).toBe(false); }); it('disables next button when on last child even with remaining scroll space', async () => { const wrapper = mountCarousel({ slots: { default: '<div class="slide" style="width: 200px;">Wide Slide</div>', header: '<h2 id="mc-carousel__title">Carousel Header</h2>', }, }); // Mock container with a wide child that can still scroll but is the only/last child const contentContainer = wrapper.find('.mc-carousel__content'); Object.defineProperty(contentContainer.element, 'scrollLeft', { value: 50, writable: true, configurable: true, }); Object.defineProperty(contentContainer.element, 'scrollWidth', { value: 200, writable: true, configurable: true, }); Object.defineProperty(contentContainer.element, 'clientWidth', { value: 100, writable: true, configurable: true, }); // Trigger scroll to update state await contentContainer.trigger('scroll'); await nextTick(); await new Promise((resolve) => setTimeout(resolve, 200)); const buttons = wrapper.findAllComponents(MIconButton); const nextButton = buttons[1]; // Should be disabled because we're on the last (only) child // even though scrollLeft (50) < scrollWidth - clientWidth (100) expect(nextButton.props('disabled')).toBe(true); }); });