@zeix/ui-element
Version:
UIElement - a HTML-first library for reactive Web Components
693 lines (556 loc) ⢠19.8 kB
HTML
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Input Textbox Component Tests</title>
</head>
<body>
<!-- Test fixtures -->
<form-textbox id="test1">
<label for="name-input">Name</label>
<div class="input">
<input
type="text"
id="name-input"
name="name"
autocomplete="name"
required
/>
</div>
<p class="error" aria-live="assertive" id="name-error"></p>
<p class="description" aria-live="polite" id="name-description">
Tell us how you want us to call you in our communications.
</p>
</form-textbox>
<form-textbox id="test2" clearable>
<label for="query-input">Search terms</label>
<div class="input">
<input
type="text"
id="query-input"
name="query"
autocomplete="off"
placeholder="apple banana"
required
/>
<button
type="button"
class="clear"
aria-label="Clear input"
hidden
>
â
</button>
</div>
<p class="error" aria-live="assertive" id="query-error"></p>
</form-textbox>
<form-textbox id="test3">
<label for="comment-input">Comment</label>
<div class="input">
<textarea
id="comment-input"
name="comment"
autocomplete="off"
maxlength="500"
></textarea>
</div>
<p class="error" aria-live="assertive" id="comment-error"></p>
<p
class="description"
aria-live="polite"
id="comment-description"
data-remaining="${n} characters remaining"
></p>
</form-textbox>
<form-textbox id="test4">
<label for="email-input">Email</label>
<div class="input">
<input
type="email"
id="email-input"
name="email"
autocomplete="email"
required
/>
</div>
<p class="error" aria-live="assertive" id="email-error"></p>
</form-textbox>
<form-textbox id="test5">
<label for="no-desc-input">No Description</label>
<div class="input">
<input type="text" id="no-desc-input" name="no-desc" />
</div>
<p class="error" aria-live="assertive" id="no-desc-error"></p>
</form-textbox>
<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 reset input component state
const resetInput = async el => {
const input = el.querySelector('input, textarea')
if (input) {
input.value = ''
// Reset component state directly to avoid triggering validation
el.value = ''
el.length = 0
el.error = ''
await tick()
}
}
// Helper to simulate typing in input
const typeInInput = async (input, text) => {
input.value = text
input.dispatchEvent(new Event('input', { bubbles: true }))
await tick()
await animationFrame() // Wait for all component updates
}
// Helper to simulate committing input value
const commitInput = async input => {
input.dispatchEvent(new Event('change', { bubbles: true }))
await tick()
await animationFrame() // Wait for all component updates
}
runTests(() => {
describe('Input Textbox Component', () => {
beforeEach(async () => {
// Reset all test components before each test
const testIds = [
'test1',
'test2',
'test3',
'test4',
'test5',
]
for (const id of testIds) {
const el = document.getElementById(id)
if (el) await resetInput(el)
}
// Wait for all resets to complete
await animationFrame()
await animationFrame()
})
it('should verify component exists and has expected structure', () => {
const el = document.getElementById('test1')
assert.isNotNull(el)
assert.equal(el.tagName.toLowerCase(), 'form-textbox')
// Check for required elements
const input = el.querySelector('input')
const error = el.querySelector('.error')
const description = el.querySelector('.description')
assert.isNotNull(input)
assert.isNotNull(error)
assert.isNotNull(description)
// Check initial properties exist
assert.isDefined(el.value)
assert.isDefined(el.length)
assert.isDefined(el.error)
assert.isDefined(el.description)
})
it('should initialize with correct default state', async () => {
const el = document.getElementById('test1')
const input = el.querySelector('input')
await tick()
assert.equal(el.value, '')
assert.equal(el.length, 0)
// Untouched fields should never show validation error, even if required
// el.error should only be non-empty if .error element contains server-side error
assert.equal(el.error, '')
assert.equal(input.value, '')
})
it('should update length when user types', async () => {
const el = document.getElementById('test1')
const input = el.querySelector('input')
await typeInInput(input, 'Hello')
assert.equal(el.length, 5)
assert.equal(input.value, 'Hello')
})
it('should update value when input changes', async () => {
const el = document.getElementById('test1')
const input = el.querySelector('input')
await typeInInput(input, 'Hello World')
await commitInput(input)
assert.equal(el.value, 'Hello World')
assert.equal(el.length, 11)
})
it('should handle validation errors', async () => {
const el = document.getElementById('test4')
const input = el.querySelector('input')
const errorEl = el.querySelector('.error')
// Type invalid email
await typeInInput(input, 'invalid-email')
await commitInput(input)
// Should have validation error
assert.isNotEmpty(el.error)
assert.equal(errorEl.textContent, el.error)
assert.equal(input.getAttribute('aria-invalid'), 'true')
})
it('should clear validation errors when input becomes valid', async () => {
const el = document.getElementById('test4')
const input = el.querySelector('input')
const errorEl = el.querySelector('.error')
// Type invalid email first
await typeInInput(input, 'invalid')
await commitInput(input)
assert.isNotEmpty(el.error)
// Type valid email
await typeInInput(input, 'test@example.com')
await commitInput(input)
assert.equal(el.error, '')
assert.equal(errorEl.textContent, '')
assert.equal(
input.getAttribute('aria-invalid'),
'false',
)
})
it('should handle required field validation', async () => {
const el = document.getElementById('test1')
const input = el.querySelector('input')
// Required field should show error when empty and committed
await typeInInput(input, '')
await commitInput(input)
assert.isNotEmpty(el.error)
assert.equal(input.getAttribute('aria-invalid'), 'true')
})
it('should set ARIA attributes correctly', async () => {
const el = document.getElementById('test1')
const input = el.querySelector('input')
const errorEl = el.querySelector('.error')
const descriptionEl = el.querySelector('.description')
await animationFrame()
// Should have aria-describedby pointing to description
assert.equal(
input.getAttribute('aria-describedby'),
descriptionEl.id,
)
// When error occurs, should have aria-errormessage
await typeInInput(input, '')
await commitInput(input)
if (el.error) {
assert.equal(
input.getAttribute('aria-errormessage'),
errorEl.id,
)
}
})
it('should work with textarea element', async () => {
const el = document.getElementById('test3')
const textarea = el.querySelector('textarea')
assert.isNotNull(textarea)
assert.equal(textarea.tagName.toLowerCase(), 'textarea')
await typeInInput(textarea, 'This is a comment')
assert.equal(el.length, 17)
await commitInput(textarea)
assert.equal(el.value, 'This is a comment')
})
it('should handle character count with remaining calculation', async () => {
const el = document.getElementById('test3')
const textarea = el.querySelector('textarea')
const descriptionEl = el.querySelector('.description')
await tick()
// Should show initial remaining count
assert.include(
descriptionEl.textContent,
'500 characters remaining',
)
// Type some text and check remaining count updates
await typeInInput(textarea, 'Hello')
await tick()
assert.include(
descriptionEl.textContent,
'495 characters remaining',
)
})
it('should show/hide clear button based on input content', async () => {
const el = document.getElementById('test2')
const input = el.querySelector('input')
const clearBtn = el.querySelector('.clear')
await tick()
// Initially hidden when no content
assert.isTrue(clearBtn.hidden)
// Should show when content is added
await typeInInput(input, 'test')
await tick() // Wait for DOM update to reflect el.length change
assert.isFalse(clearBtn.hidden)
// Should hide when content is cleared
await typeInInput(input, '')
await tick() // Wait for DOM update to reflect el.length change
assert.isTrue(clearBtn.hidden)
})
it('should clear input when clear button is clicked', async () => {
const el = document.getElementById('test2')
const input = el.querySelector('input')
const clearBtn = el.querySelector('.clear')
// Add some content
input.focus()
await typeInInput(input, 'test content')
await commitInput(input)
assert.equal(el.value, 'test content')
assert.equal(el.length, 12)
// Click clear button (functionality should work regardless of visibility)
clearBtn.click()
await tick()
assert.equal(el.value, '')
assert.equal(el.length, 0)
assert.equal(input.value, '')
})
it('should handle whitespace-only content correctly', async () => {
const el = document.getElementById('test2')
const input = el.querySelector('input')
const clearBtn = el.querySelector('.clear')
// Add only whitespace
await typeInInput(input, ' ')
await tick() // Wait for DOM update to reflect el.length change
// Component should handle whitespace correctly in logic
assert.equal(el.length, 3)
// Clear button should show for whitespace (el.length > 0)
assert.isFalse(clearBtn.hidden)
// Add actual content
await typeInInput(input, ' test ')
await tick() // Wait for DOM update to reflect el.length change
assert.equal(el.length, 8)
// Clear button should still show
assert.isFalse(clearBtn.hidden)
// Clear content completely
await typeInInput(input, '')
await tick() // Wait for DOM update to reflect el.length change
// Clear button should hide when empty
assert.isTrue(clearBtn.hidden)
})
it('should work without description element', async () => {
const el = document.getElementById('test5')
const input = el.querySelector('input')
assert.isNull(el.querySelector('.description'))
// Should still work normally
await typeInInput(input, 'test')
await commitInput(input)
assert.equal(el.value, 'test')
assert.equal(el.length, 4)
// ARIA attributes should handle missing description gracefully
assert.isNull(input.getAttribute('aria-describedby'))
})
it('should handle rapid input changes correctly', async () => {
const el = document.getElementById('test1')
const input = el.querySelector('input')
// Rapid typing simulation
for (let i = 0; i < 5; i++) {
await typeInInput(input, 'a'.repeat(i + 1))
}
await commitInput(input)
assert.equal(el.value, 'aaaaa')
assert.equal(el.length, 5)
})
it('should maintain maxlength constraint', async () => {
const el = document.getElementById('test3')
const textarea = el.querySelector('textarea')
const longText = 'a'.repeat(600) // More than maxlength of 500
// Browser should enforce maxlength, but let's test our handling
textarea.value = longText.substring(0, 500) // Simulate browser constraint
textarea.dispatchEvent(
new Event('input', { bubbles: true }),
)
await tick()
assert.equal(el.length, 500)
})
it('should handle component without error element gracefully', () => {
// Create a minimal component structure for edge case testing
const tempDiv = document.createElement('div')
tempDiv.innerHTML = `
<form-textbox>
<input type="text" />
</form-textbox>
`
document.body.appendChild(tempDiv)
const tempEl = tempDiv.querySelector('form-textbox')
// Component should initialize without throwing
assert.isNotNull(tempEl)
// Cleanup
document.body.removeChild(tempDiv)
})
it('should update error validation message correctly', async () => {
const el = document.getElementById('test4')
const input = el.querySelector('input')
// Test validation message updates
await typeInInput(input, 'invalid-email')
await commitInput(input)
const firstError = el.error
assert.isNotEmpty(firstError)
// Change to a different invalid format
await typeInInput(input, '@invalid')
await commitInput(input)
// Error should update (might be different message)
assert.isNotEmpty(el.error)
// Fix the email
await typeInInput(input, 'valid@email.com')
await commitInput(input)
assert.equal(el.error, '')
})
it('should handle programmatic error setting', async () => {
const el = document.getElementById('test1')
const errorEl = el.querySelector('.error')
el.error = 'Custom error message'
await tick()
assert.equal(
errorEl.textContent,
'Custom error message',
)
})
it('should handle programmatic description setting', async () => {
const el = document.getElementById('test1')
const descriptionEl = el.querySelector('.description')
el.description = 'New description'
await tick()
assert.equal(
descriptionEl.textContent,
'New description',
)
})
it('should have clear() method available', () => {
const el = document.getElementById('test1')
assert.isFunction(
el.clear,
'clear method should be a function',
)
})
it('should clear all state when clear() method is called', async () => {
const el = document.getElementById('test1')
const input = el.querySelector('input')
// Add content first
await typeInInput(input, 'test content')
await commitInput(input)
assert.equal(el.value, 'test content')
assert.equal(el.length, 12)
// Clear using component method
el.clear()
await tick()
assert.equal(el.value, '')
assert.equal(el.length, 0)
assert.equal(input.value, '')
})
it('should clear validation error when clear() is called', async () => {
const el = document.getElementById('test5')
const input = el.querySelector('input')
// Set a custom validation error
input.setCustomValidity('Custom error message')
await commitInput(input)
assert.isNotEmpty(el.error)
// Clear should reset custom error (test5 is not required)
el.clear()
await tick()
assert.equal(el.error, '')
assert.equal(el.value, '')
assert.equal(input.value, '')
})
it('should work with textarea when clear() is called', async () => {
const el = document.getElementById('test3')
const textarea = el.querySelector('textarea')
// Add content to textarea
await typeInInput(
textarea,
'Some long textarea content',
)
await commitInput(textarea)
assert.equal(el.value, 'Some long textarea content')
assert.equal(el.length, 26)
// Clear should work
el.clear()
await tick()
assert.equal(el.value, '')
assert.equal(el.length, 0)
assert.equal(textarea.value, '')
})
it('should hide clear button after clear() is called', async () => {
const el = document.getElementById('test2')
const input = el.querySelector('input')
const clearBtn = el.querySelector('.clear')
// Add content to show clear button
await typeInInput(input, 'test content')
await tick()
assert.isFalse(
clearBtn.hidden,
'Clear button should be visible with content',
)
// Call clear method
el.clear()
await tick()
assert.isTrue(
clearBtn.hidden,
'Clear button should be hidden after clear()',
)
})
it('should update character count after clear() on textarea with maxlength', async () => {
const el = document.getElementById('test3')
const textarea = el.querySelector('textarea')
const descriptionEl = el.querySelector('.description')
// Add some content
await typeInInput(textarea, 'Test content')
await tick()
assert.include(
descriptionEl.textContent,
'488 characters remaining',
)
// Clear and check count is reset
el.clear()
await tick()
assert.include(
descriptionEl.textContent,
'500 characters remaining',
)
})
it('should be safe to call clear() multiple times', async () => {
const el = document.getElementById('test1')
const input = el.querySelector('input')
// Add content
await typeInInput(input, 'test')
await commitInput(input)
// Multiple clears should not cause errors
el.clear()
await tick()
el.clear()
await tick()
el.clear()
await tick()
assert.equal(el.value, '')
assert.equal(el.length, 0)
assert.equal(input.value, '')
})
it('should be safe to call clear() on empty input', async () => {
const el = document.getElementById('test1')
const input = el.querySelector('input')
// Ensure it's empty
await resetInput(el)
// Clear on empty should not cause errors
el.clear()
await tick()
assert.equal(el.value, '')
assert.equal(el.length, 0)
assert.equal(input.value, '')
})
it('should handle clear() with required field validation', async () => {
const el = document.getElementById('test1')
const input = el.querySelector('input')
// Add valid content first
await typeInInput(input, 'valid content')
await commitInput(input)
// Should be valid
assert.equal(el.error, '')
// Clear should trigger required field validation
el.clear()
await tick()
// Should show required field error since input is required and now empty
if (input.required) {
assert.isNotEmpty(el.error)
}
})
})
})
</script>
</body>
</html>