@zeix/ui-element
Version:
UIElement - a HTML-first library for reactive Web Components
694 lines (590 loc) • 19.8 kB
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>