UNPKG

@zeix/ui-element

Version:

UIElement - a HTML-first library for reactive Web Components

694 lines (590 loc) 19.8 kB
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>My Carousel Component Tests</title> </head> <body> <!-- Test fixtures --> <module-carousel id="test1"> <h2 class="visually-hidden">Test Slides 1</h2> <div class="slides"> <div id="slide1" role="tabpanel" aria-current="true"> <h3>Slide 1</h3> <p>Content 1</p> </div> <div id="slide2" role="tabpanel" aria-current="false"> <h3>Slide 2</h3> <p>Content 2</p> </div> <div id="slide3" role="tabpanel" aria-current="false"> <h3>Slide 3</h3> <p>Content 3</p> </div> </div> <nav aria-label="Carousel Navigation"> <button type="button" class="prev" aria-label="Previous"></button> <button type="button" class="next" aria-label="Next"></button> <div role="tablist"> <button role="tab" aria-selected="true" aria-controls="slide1" aria-label="Slide 1" data-index="0" tabindex="0" ></button> <button role="tab" aria-selected="false" aria-controls="slide2" aria-label="Slide 2" data-index="1" tabindex="-1" ></button> <button role="tab" aria-selected="false" aria-controls="slide3" aria-label="Slide 3" data-index="2" tabindex="-1" ></button> </div> </nav> </module-carousel> <module-carousel id="test2"> <h2 class="visually-hidden">Test Slides 2</h2> <div class="slides"> <div id="slideA" role="tabpanel" aria-current="true"> <h3>Slide A</h3> <p>Content A</p> </div> <div id="slideB" role="tabpanel" aria-current="false"> <h3>Slide B</h3> <p>Content B</p> </div> </div> <nav aria-label="Carousel Navigation"> <button type="button" class="prev" aria-label="Previous"></button> <button type="button" class="next" aria-label="Next"></button> <div role="tablist"> <button role="tab" aria-selected="true" aria-controls="slideA" aria-label="Slide A" data-index="0" tabindex="0" ></button> <button role="tab" aria-selected="false" aria-controls="slideB" aria-label="Slide B" data-index="1" tabindex="-1" ></button> </div> </nav> </module-carousel> <module-carousel id="test3"> <h2 class="visually-hidden">Single Slide</h2> <div class="slides"> <div id="singleSlide" role="tabpanel" aria-current="true"> <h3>Only Slide</h3> <p>Single content</p> </div> </div> <nav aria-label="Carousel Navigation"> <button type="button" class="prev" aria-label="Previous"></button> <button type="button" class="next" aria-label="Next"></button> <div role="tablist"> <button role="tab" aria-selected="true" aria-controls="singleSlide" aria-label="Only Slide" data-index="0" tabindex="0" ></button> </div> </nav> </module-carousel> <script type="module"> import { runTests } from '@web/test-runner-mocha' import { assert } from '@esm-bundle/chai' import '../../../docs/assets/main.js' // Built components bundle const wait = ms => new Promise(resolve => setTimeout(resolve, ms)) const animationFrame = () => new Promise(requestAnimationFrame) // Helper function to simulate keyboard events const simulateKeyup = (element, key) => { const event = new KeyboardEvent('keyup', { key: key, bubbles: true, cancelable: true, }) element.dispatchEvent(event) return event } // Helper to get current slide index from aria-current const getCurrentSlideIndex = carousel => { const slides = Array.from( carousel.querySelectorAll('[role="tabpanel"]'), ) return slides.findIndex( slide => slide.getAttribute('aria-current') === 'true', ) } // Helper to get selected tab index from aria-selected const getSelectedTabIndex = carousel => { const tabs = Array.from( carousel.querySelectorAll('[role="tab"]'), ) return tabs.findIndex( tab => tab.getAttribute('aria-selected') === 'true', ) } // Helper to reset carousel to first slide const resetCarousel = async carousel => { // Simulate clicking first tab to reset state const firstTab = carousel.querySelector('[data-index="0"]') if (firstTab) { firstTab.click() await animationFrame() } } runTests(() => { describe('My Carousel Component', () => { beforeEach(async () => { // Reset all carousels before each test const carousels = ['test1', 'test2', 'test3'] for (const id of carousels) { const el = document.getElementById(id) if (el) await resetCarousel(el) } }) it('should verify component exists and has expected structure', () => { const el = document.getElementById('test1') assert.isNotNull(el) assert.equal( el.tagName.toLowerCase(), 'module-carousel', ) // Check for required elements const nav = el.querySelector('nav') const prevBtn = el.querySelector('.prev') const nextBtn = el.querySelector('.next') const tablist = el.querySelector('[role="tablist"]') const slides = el.querySelectorAll('[role="tabpanel"]') const tabs = el.querySelectorAll('[role="tab"]') assert.isNotNull(nav) assert.isNotNull(prevBtn) assert.isNotNull(nextBtn) assert.isNotNull(tablist) assert.equal(slides.length, 3) assert.equal(tabs.length, 3) }) it('should initialize with correct default state', async () => { const el = document.getElementById('test1') await animationFrame() // First slide should be current assert.equal(getCurrentSlideIndex(el), 0) assert.equal(getSelectedTabIndex(el), 0) // Check ARIA attributes const firstSlide = el.querySelector('#slide1') const firstTab = el.querySelector('[data-index="0"]') assert.equal( firstSlide.getAttribute('aria-current'), 'true', ) assert.equal( firstTab.getAttribute('aria-selected'), 'true', ) assert.equal(firstTab.getAttribute('tabindex'), '0') // Other tabs should not be selected const secondTab = el.querySelector('[data-index="1"]') const thirdTab = el.querySelector('[data-index="2"]') assert.equal( secondTab.getAttribute('aria-selected'), 'false', ) assert.equal(secondTab.getAttribute('tabindex'), '-1') assert.equal( thirdTab.getAttribute('aria-selected'), 'false', ) assert.equal(thirdTab.getAttribute('tabindex'), '-1') }) it('should navigate forward with next button', async () => { const el = document.getElementById('test1') const nextBtn = el.querySelector('.next') // Ensure we start at first slide await animationFrame() assert.equal(getCurrentSlideIndex(el), 0) // Click next button nextBtn.click() await animationFrame() // Should be on second slide assert.equal(getCurrentSlideIndex(el), 1) assert.equal(getSelectedTabIndex(el), 1) // Check ARIA attributes updated const secondSlide = el.querySelector('#slide2') const secondTab = el.querySelector('[data-index="1"]') assert.equal( secondSlide.getAttribute('aria-current'), 'true', ) assert.equal( secondTab.getAttribute('aria-selected'), 'true', ) assert.equal(secondTab.getAttribute('tabindex'), '0') }) it('should navigate backward with prev button', async () => { const el = document.getElementById('test1') const prevBtn = el.querySelector('.prev') const nextBtn = el.querySelector('.next') // Ensure we start at first slide await resetCarousel(el) assert.equal(getCurrentSlideIndex(el), 0) // Go to second slide first nextBtn.click() await animationFrame() assert.equal(getCurrentSlideIndex(el), 1) // Click prev button prevBtn.click() await animationFrame() // Should be back to first slide assert.equal(getCurrentSlideIndex(el), 0) assert.equal(getSelectedTabIndex(el), 0) // Check ARIA attributes updated const firstSlide = el.querySelector('#slide1') const firstTab = el.querySelector('[data-index="0"]') assert.equal( firstSlide.getAttribute('aria-current'), 'true', ) assert.equal( firstTab.getAttribute('aria-selected'), 'true', ) assert.equal(firstTab.getAttribute('tabindex'), '0') }) it('should wrap around from last to first slide with next', async () => { const el = document.getElementById('test1') const nextBtn = el.querySelector('.next') // Ensure we start at first slide await resetCarousel(el) assert.equal(getCurrentSlideIndex(el), 0) // Go to last slide (index 2) nextBtn.click() await animationFrame() nextBtn.click() await animationFrame() assert.equal(getCurrentSlideIndex(el), 2) // Click next again - should wrap to first nextBtn.click() await animationFrame() assert.equal(getCurrentSlideIndex(el), 0) assert.equal(getSelectedTabIndex(el), 0) }) it('should wrap around from first to last slide with prev', async () => { const el = document.getElementById('test1') const prevBtn = el.querySelector('.prev') // Ensure we start at first slide await resetCarousel(el) assert.equal(getCurrentSlideIndex(el), 0) // Go prev - should wrap to last prevBtn.click() await animationFrame() assert.equal(getCurrentSlideIndex(el), 2) assert.equal(getSelectedTabIndex(el), 2) }) it('should navigate by clicking tab dots', async () => { const el = document.getElementById('test1') const thirdTab = el.querySelector('[data-index="2"]') // Ensure we start at first slide await resetCarousel(el) assert.equal(getCurrentSlideIndex(el), 0) // Click third tab thirdTab.click() await animationFrame() // Should be on third slide assert.equal(getCurrentSlideIndex(el), 2) assert.equal(getSelectedTabIndex(el), 2) // Check ARIA attributes const thirdSlide = el.querySelector('#slide3') assert.equal( thirdSlide.getAttribute('aria-current'), 'true', ) assert.equal( thirdTab.getAttribute('aria-selected'), 'true', ) assert.equal(thirdTab.getAttribute('tabindex'), '0') }) it('should handle keyboard navigation with arrow keys', async () => { const el = document.getElementById('test1') const nav = el.querySelector('nav') // Ensure we start at first slide await resetCarousel(el) assert.equal(getCurrentSlideIndex(el), 0) // Simulate ArrowRight simulateKeyup(nav, 'ArrowRight') await animationFrame() assert.equal(getCurrentSlideIndex(el), 1) assert.equal(getSelectedTabIndex(el), 1) // Simulate ArrowLeft simulateKeyup(nav, 'ArrowLeft') await animationFrame() assert.equal(getCurrentSlideIndex(el), 0) assert.equal(getSelectedTabIndex(el), 0) }) it('should handle Home key to go to first slide', async () => { const el = document.getElementById('test1') const nav = el.querySelector('nav') const nextBtn = el.querySelector('.next') // Ensure we start at first slide, then go to middle slide await resetCarousel(el) nextBtn.click() await animationFrame() assert.equal(getCurrentSlideIndex(el), 1) // Press Home key simulateKeyup(nav, 'Home') await animationFrame() assert.equal(getCurrentSlideIndex(el), 0) assert.equal(getSelectedTabIndex(el), 0) }) it('should handle End key to go to last slide', async () => { const el = document.getElementById('test1') const nav = el.querySelector('nav') // Ensure we start at first slide await resetCarousel(el) assert.equal(getCurrentSlideIndex(el), 0) // Press End key simulateKeyup(nav, 'End') await animationFrame() assert.equal(getCurrentSlideIndex(el), 2) assert.equal(getSelectedTabIndex(el), 2) }) it('should focus correct tab after keyboard navigation', async () => { const el = document.getElementById('test1') const nav = el.querySelector('nav') const firstTab = el.querySelector('[data-index="0"]') const secondTab = el.querySelector('[data-index="1"]') // Ensure we start at first slide and focus first tab await resetCarousel(el) firstTab.focus() // Mock the focus method to track calls let focusedElement = null secondTab.focus = () => { focusedElement = secondTab } // Simulate ArrowRight - should focus second tab simulateKeyup(nav, 'ArrowRight') await animationFrame() // Component should have called focus on the second tab assert.equal(getCurrentSlideIndex(el), 1) }) it('should ignore non-navigation keys', async () => { const el = document.getElementById('test1') const nav = el.querySelector('nav') // Ensure we start at first slide await resetCarousel(el) const initialIndex = getCurrentSlideIndex(el) // Simulate various non-navigation keys simulateKeyup(nav, 'Enter') simulateKeyup(nav, 'Space') simulateKeyup(nav, 'Tab') simulateKeyup(nav, 'Escape') await animationFrame() // Should remain at same slide assert.equal(getCurrentSlideIndex(el), initialIndex) }) it('should handle rapid multiple clicks', async () => { const el = document.getElementById('test1') const nextBtn = el.querySelector('.next') // Ensure we start at first slide await resetCarousel(el) assert.equal(getCurrentSlideIndex(el), 0) // Rapid clicks nextBtn.click() nextBtn.click() nextBtn.click() nextBtn.click() await animationFrame() // Should wrap around correctly (4 clicks = back to slide 1, index 1) assert.equal(getCurrentSlideIndex(el), 1) }) it('should work with two-slide carousel', async () => { const el = document.getElementById('test2') const nextBtn = el.querySelector('.next') const prevBtn = el.querySelector('.prev') // Ensure we start at first slide await resetCarousel(el) assert.equal(getCurrentSlideIndex(el), 0) // Next should go to slide 2 nextBtn.click() await animationFrame() assert.equal(getCurrentSlideIndex(el), 1) // Next again should wrap to slide 1 nextBtn.click() await animationFrame() assert.equal(getCurrentSlideIndex(el), 0) // Prev should go to slide 2 prevBtn.click() await animationFrame() assert.equal(getCurrentSlideIndex(el), 1) }) it('should work with single-slide carousel', async () => { const el = document.getElementById('test3') const nextBtn = el.querySelector('.next') const prevBtn = el.querySelector('.prev') // Ensure we start at first slide await resetCarousel(el) assert.equal(getCurrentSlideIndex(el), 0) // Next and prev should stay on same slide nextBtn.click() await animationFrame() assert.equal(getCurrentSlideIndex(el), 0) prevBtn.click() await animationFrame() assert.equal(getCurrentSlideIndex(el), 0) }) it('should handle tab clicks with invalid data-index', async () => { const el = document.getElementById('test1') const firstTab = el.querySelector('[data-index="0"]') // Remove data-index to test invalid scenario const originalIndex = firstTab.dataset.index delete firstTab.dataset.index // Ensure we start at first slide, then go to slide 2 await resetCarousel(el) const nextBtn = el.querySelector('.next') nextBtn.click() await animationFrame() assert.equal(getCurrentSlideIndex(el), 1) // Click tab with missing data-index - should default to 0 firstTab.click() await animationFrame() assert.equal(getCurrentSlideIndex(el), 0) // Restore data-index firstTab.dataset.index = originalIndex }) it('should prevent default on navigation key events', async () => { const el = document.getElementById('test1') const nav = el.querySelector('nav') // Create spy to track preventDefault calls let preventDefaultCalled = false const originalPreventDefault = KeyboardEvent.prototype.preventDefault KeyboardEvent.prototype.preventDefault = function () { preventDefaultCalled = true originalPreventDefault.call(this) } // Simulate navigation key simulateKeyup(nav, 'ArrowRight') await animationFrame() // Should have called preventDefault assert.isTrue(preventDefaultCalled) // Restore original method KeyboardEvent.prototype.preventDefault = originalPreventDefault }) it('should maintain ARIA relationships', async () => { const el = document.getElementById('test1') const tabs = el.querySelectorAll('[role="tab"]') const slides = el.querySelectorAll('[role="tabpanel"]') // Ensure we start at first slide await resetCarousel(el) // Check initial ARIA relationships tabs.forEach((tab, index) => { const controls = tab.getAttribute('aria-controls') const correspondingSlide = slides[index] assert.equal(controls, correspondingSlide.id) }) // Navigate and check relationships are maintained const nextBtn = el.querySelector('.next') nextBtn.click() await animationFrame() tabs.forEach((tab, index) => { const controls = tab.getAttribute('aria-controls') const correspondingSlide = slides[index] assert.equal(controls, correspondingSlide.id) }) }) it('should update all slides aria-current correctly', async () => { const el = document.getElementById('test1') const slides = el.querySelectorAll('[role="tabpanel"]') const nextBtn = el.querySelector('.next') // Ensure we start at first slide await resetCarousel(el) // Initially first slide should be current assert.equal( slides[0].getAttribute('aria-current'), 'true', ) assert.equal( slides[1].getAttribute('aria-current'), 'false', ) assert.equal( slides[2].getAttribute('aria-current'), 'false', ) // Navigate to second slide nextBtn.click() await animationFrame() assert.equal( slides[0].getAttribute('aria-current'), 'false', ) assert.equal( slides[1].getAttribute('aria-current'), 'true', ) assert.equal( slides[2].getAttribute('aria-current'), 'false', ) // Navigate to third slide nextBtn.click() await animationFrame() assert.equal( slides[0].getAttribute('aria-current'), 'false', ) assert.equal( slides[1].getAttribute('aria-current'), 'false', ) assert.equal( slides[2].getAttribute('aria-current'), 'true', ) }) }) }) </script> </body> </html>