@zeix/ui-element
Version:
UIElement - a HTML-first library for reactive Web Components
401 lines (324 loc) • 11.6 kB
HTML
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Form Spinbutton Component Tests</title>
</head>
<body>
<!-- Test fixtures -->
<form-spinbutton id="test1" value="0" zero-label="Add to Cart">
<button
type="button"
class="decrement"
aria-label="Decrement"
hidden
>
−
</button>
<p class="value" hidden>0</p>
<button type="button" class="increment primary">Add to Cart</button>
</form-spinbutton>
<form-spinbutton id="test2" value="5">
<button type="button" class="decrement" aria-label="Decrement">
−
</button>
<p class="value">5</p>
<button type="button" class="increment primary">+</button>
</form-spinbutton>
<form-spinbutton id="test3" value="0" max="3">
<button
type="button"
class="decrement"
aria-label="Decrement"
hidden
>
−
</button>
<p class="value" hidden>0</p>
<button type="button" class="increment primary">Add to Cart</button>
</form-spinbutton>
<form-spinbutton id="test4" value="9" max="9">
<button type="button" class="decrement" aria-label="Decrement">
−
</button>
<p class="value">9</p>
<button type="button" class="increment primary" disabled>+</button>
</form-spinbutton>
<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)
const microtask = () => new Promise(queueMicrotask)
const tick = async () => {
await animationFrame() // Wait for effects to execute
await microtask() // Wait for DOM to reflect changes
}
// Helper to simulate keyboard events
const simulateKeyEvent = (
element,
eventType,
key,
options = {},
) => {
const event = new KeyboardEvent(eventType, {
key: key,
bubbles: true,
cancelable: true,
...options,
})
element.dispatchEvent(event)
return event
}
// Helper to reset component state
const resetComponent = async el => {
// Reset to initial state by finding the initial value from the DOM
const initialValue = parseInt(el.getAttribute('value') || '0')
// If component is not at initial value, click buttons to get back to it
while (el.value !== initialValue) {
if (el.value > initialValue) {
const decrementBtn = el.querySelector('.decrement')
if (decrementBtn && !decrementBtn.hidden) {
decrementBtn.click()
} else {
break
}
} else {
const incrementBtn = el.querySelector('.increment')
if (incrementBtn && !incrementBtn.disabled) {
incrementBtn.click()
} else {
break
}
}
}
}
runTests(() => {
describe('Form Spinbutton Component', () => {
beforeEach(async () => {
// Reset all test components before each test
const testComponents = [
'test1',
'test2',
'test3',
'test4',
]
for (const id of testComponents) {
const el = document.getElementById(id)
if (el) {
await resetComponent(el)
}
}
})
it('should verify component exists and has expected structure', () => {
const el = document.getElementById('test1')
assert.isNotNull(el)
assert.equal(
el.tagName.toLowerCase(),
'form-spinbutton',
)
// Check for required elements
const incrementBtn = el.querySelector('.increment')
const decrementBtn = el.querySelector('.decrement')
const valueDisplay = el.querySelector('.value')
assert.isNotNull(incrementBtn)
assert.isNotNull(decrementBtn)
assert.isNotNull(valueDisplay)
// Check initial properties exist
assert.isDefined(el.value)
assert.isNumber(el.value)
})
it('should initialize with correct default state', async () => {
const el = document.getElementById('test1')
const incrementBtn = el.querySelector('.increment')
const decrementBtn = el.querySelector('.decrement')
const valueDisplay = el.querySelector('.value')
// Should start at 0
assert.equal(el.value, 0)
// At zero state: increment shows label, decrement is hidden, value is hidden
assert.equal(incrementBtn.textContent, 'Add to Cart')
assert.isTrue(decrementBtn.hidden)
assert.isTrue(valueDisplay.hidden)
})
it('should initialize with non-zero value', async () => {
const el = document.getElementById('test2')
const incrementBtn = el.querySelector('.increment')
const decrementBtn = el.querySelector('.decrement')
const valueDisplay = el.querySelector('.value')
// Should start at 5
assert.equal(el.value, 5)
// At non-zero state: increment shows +, decrement is visible, value is visible
assert.equal(incrementBtn.textContent, '+')
assert.isFalse(decrementBtn.hidden)
assert.isFalse(valueDisplay.hidden)
assert.equal(valueDisplay.textContent, '5')
})
it('should increment value when increment button clicked', async () => {
const el = document.getElementById('test1')
const incrementBtn = el.querySelector('.increment')
const valueDisplay = el.querySelector('.value')
// Start at 0
assert.equal(el.value, 0)
incrementBtn.click()
assert.equal(el.value, 1)
assert.equal(valueDisplay.textContent, '1')
assert.isFalse(valueDisplay.hidden)
})
it('should decrement value when decrement button clicked', async () => {
const el = document.getElementById('test2')
const decrementBtn = el.querySelector('.decrement')
const valueDisplay = el.querySelector('.value')
// Start at 5
assert.equal(el.value, 5)
decrementBtn.click()
assert.equal(el.value, 4)
assert.equal(valueDisplay.textContent, '4')
})
it('should show/hide elements based on value state', async () => {
const el = document.getElementById('test1')
const incrementBtn = el.querySelector('.increment')
const decrementBtn = el.querySelector('.decrement')
const valueDisplay = el.querySelector('.value')
// At zero
assert.equal(el.value, 0)
assert.isTrue(decrementBtn.hidden)
assert.isTrue(valueDisplay.hidden)
assert.equal(incrementBtn.textContent, 'Add to Cart')
// Increment to 1
incrementBtn.click()
assert.equal(el.value, 1)
assert.isFalse(decrementBtn.hidden)
assert.isFalse(valueDisplay.hidden)
assert.equal(incrementBtn.textContent, '+')
// Decrement back to 0
decrementBtn.click()
assert.equal(el.value, 0)
assert.isTrue(decrementBtn.hidden)
assert.isTrue(valueDisplay.hidden)
assert.equal(incrementBtn.textContent, 'Add to Cart')
})
it('should handle keyboard navigation', async () => {
const el = document.getElementById('test2')
const incrementBtn = el.querySelector('.increment')
const initialValue = el.value
// ArrowUp should increment
incrementBtn.focus()
simulateKeyEvent(incrementBtn, 'keydown', 'ArrowUp')
assert.equal(el.value, initialValue + 1)
// ArrowDown should decrement
simulateKeyEvent(incrementBtn, 'keydown', 'ArrowDown')
assert.equal(el.value, initialValue)
// + key should increment
simulateKeyEvent(incrementBtn, 'keydown', '+')
assert.equal(el.value, initialValue + 1)
// - key should decrement
simulateKeyEvent(incrementBtn, 'keydown', '-')
assert.equal(el.value, initialValue)
})
it('should respect max value constraint', async () => {
const el = document.getElementById('test3')
const incrementBtn = el.querySelector('.increment')
// Start at 0, max is 3
assert.equal(el.value, 0)
assert.isFalse(incrementBtn.disabled)
// Increment to max
incrementBtn.click()
incrementBtn.click()
incrementBtn.click()
assert.equal(el.value, 3)
assert.isTrue(incrementBtn.disabled)
// Try to increment beyond max
incrementBtn.click()
assert.equal(el.value, 3) // Should remain at max
})
it('should handle rapid button clicks', async () => {
const el = document.getElementById('test1')
const incrementBtn = el.querySelector('.increment')
// Click multiple times rapidly
incrementBtn.click()
incrementBtn.click()
incrementBtn.click()
assert.equal(el.value, 3)
})
it('should update aria-label appropriately', async () => {
const el = document.getElementById('test1')
const incrementBtn = el.querySelector('.increment')
// At zero, should show zero-label
assert.equal(
incrementBtn.getAttribute('aria-label'),
'Add to Cart',
)
// After increment, should show increment-label (or default)
incrementBtn.click()
assert.equal(
incrementBtn.getAttribute('aria-label'),
'Increment',
)
})
it('should handle missing attributes gracefully', async () => {
const el = document.getElementById('test2')
const incrementBtn = el.querySelector('.increment')
// Should use default labels when attributes not provided
assert.isNotNull(
incrementBtn.getAttribute('aria-label'),
)
})
it('should not allow programmatic value updates', async () => {
const el = document.getElementById('test2')
const valueDisplay = el.querySelector('.value')
const originalValue = el.value
// Should throw TypeError when trying to set value
assert.throws(() => {
el.value = 100
}, TypeError)
// Value should remain unchanged after failed assignment
assert.equal(el.value, originalValue)
assert.equal(
valueDisplay.textContent,
String(originalValue),
)
// Should throw for different value types
assert.throws(() => {
el.value = 'invalid'
}, TypeError)
assert.throws(() => {
el.value = null
}, TypeError)
assert.throws(() => {
el.value = undefined
}, TypeError)
// Verify value is still the original value
assert.equal(el.value, originalValue)
})
it('should maintain value constraints across operations', async () => {
const el = document.getElementById('test4')
const decrementBtn = el.querySelector('.decrement')
const incrementBtn = el.querySelector('.increment')
// Start at max (9)
assert.equal(el.value, 9)
assert.isTrue(incrementBtn.disabled)
// Decrement should enable increment
decrementBtn.click()
assert.equal(el.value, 8)
assert.isFalse(incrementBtn.disabled)
// Increment back to max
incrementBtn.click()
assert.equal(el.value, 9)
assert.isTrue(incrementBtn.disabled)
})
it('should handle keyboard events only on navigation keys', async () => {
const el = document.getElementById('test2')
const incrementBtn = el.querySelector('.increment')
const initialValue = el.value
// Non-navigation keys should not affect value
simulateKeyEvent(incrementBtn, 'keydown', 'Enter')
simulateKeyEvent(incrementBtn, 'keydown', 'Space')
simulateKeyEvent(incrementBtn, 'keydown', 'a')
assert.equal(el.value, initialValue)
})
})
})
</script>
</body>
</html>