UNPKG

@zeix/ui-element

Version:

UIElement - a HTML-first library for reactive Web Components

693 lines (556 loc) • 19.8 kB
<!doctype 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>