UNPKG

@zeix/ui-element

Version:

UIElement - a HTML-first library for reactive Web Components

604 lines (526 loc) 15.3 kB
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Component Core Tests</title> </head> <body> <style> .visually-hidden { position: absolute; left: -10000px; top: auto; width: 1px; height: 1px; overflow: hidden; } basic-counter { display: flex; flex-direction: row; gap: 1rem; & p { margin-block: 0.2rem; } } module-tabgroup > [role='tablist'] { display: flex; gap: 0.2rem; padding: 0; & button[aria-selected='true'] { color: purple; } } </style> <void-component id="void"> <h1>Hello from Server</h1> </void-component> <causal-component id="causal"> <h1>Hello from Server</h1> </causal-component> <updating-component id="updating-with-attributes" heading="Hello from Attribute" count="42" step="0.1" value="3.14" selected > <h1>Hello from Server</h1> <p>Number of unread messages: <span></span></p> <input type="number" /> </updating-component> <basic-counter value="42"> <p>Count: <span class="value">42</span></p> <p>Parity: <span class="parity">even</span></p> <p>Double: <span class="double">84</span></p> <div> <button class="decrement"></button> <button class="increment">+</button> </div> </basic-counter> <greeting-configurator> <form-textbox class="first"> <input type="text" name="first" value="Jane" /> </form-textbox> <form-textbox class="last"> <input type="text" name="last" value="Doe" /> </form-textbox> <form-checkbox> <input type="checkbox" name="fullname" /> </form-checkbox> <hello-world> <p> <span class="greeting">Hello</span> <span class="name">World</span> </p> </hello-world> </greeting-configurator> <module-tabgroup> <div role="tablist"> <button type="button" role="tab" id="trigger1" aria-controls="panel1" aria-selected="true" tabindex="0" > Tab 1 </button> <button type="button" role="tab" id="trigger2" aria-controls="panel2" aria-selected="false" tabindex="-1" > Tab 2 </button> <button type="button" role="tab" id="trigger3" aria-controls="panel3" aria-selected="false" tabindex="-1" > Tab 3 </button> </div> <div role="tabpanel" id="panel1" aria-labelledby="trigger1"> Tab 1 content </div> <div role="tabpanel" id="panel2" aria-labelledby="trigger2" hidden> Tab 2 content </div> <div role="tabpanel" id="panel3" aria-labelledby="trigger3" hidden> Tab 3 content </div> </module-tabgroup> <script type="module"> import { runTests } from '@web/test-runner-mocha' import { assert } from '@esm-bundle/chai' import { asBoolean, asInteger, asNumber, asString, component, computed, on, pass, setText, setProperty, setAttribute, show, state, toggleAttribute, toggleClass, setStyle, getText, } from '../index.dev.js' const wait = ms => new Promise(resolve => setTimeout(resolve, ms)) const animationFrame = async () => new Promise(requestAnimationFrame) const normalizeText = text => text.replace(/\s+/g, ' ').trim() // Define test components component('void-component', {}, () => []) component( 'causal-component', { heading: 'Hello from Internal State', }, (_, { first }) => first('h1', setText('heading')), ) component( 'updating-component', { heading: asString( el => el.querySelector('h1')?.textContent?.trim() ?? '', ), count: asInteger(), step: asNumber(), value: (el, value) => { if (value == null) return el.querySelector('input')?.value ? parseFloat(el.querySelector('input').value) : 0 const parsed = Number.isInteger(el.step) ? parseInt(value, 10) : parseFloat(value) return Number.isFinite(parsed) ? parsed : 0 }, selected: asBoolean(), }, (_, { first }) => [ first('h1', [setText('heading'), toggleClass('selected')]), first('span', setText('count')), first('input', [ setAttribute('step'), setProperty('value'), ]), ], ) component( 'basic-counter', { value: asInteger(), }, (el, { first }) => [ first( '.decrement', on('click', () => { el.value-- }), ), first( '.increment', on('click', () => { el.value++ }), ), first('.value', setText('value')), first( '.parity', setText(() => (el.value % 2 ? 'odd' : 'even')), ), first( '.double', setText(() => el.value * 2), ), ], ) component( 'greeting-configurator', {}, (el, { first, useElement }) => { const firstName = useElement('.first') const lastName = useElement('.last') const checkbox = useElement('form-checkbox') return [ first( 'hello-world', pass({ name: () => checkbox?.checked ? `${firstName?.value} ${lastName?.value}` : firstName?.value, }), ), ] }, ) component( 'hello-world', { name: asString('World'), }, (_, { first }) => [first('.name', setText('name'))], ) component( 'form-textbox', { value: asString(''), }, (el, { first }) => [ first('input', [ setProperty('value'), on('change', ({ event }) => { el.value = event.target.value }), ]), ], ) component( 'form-checkbox', { checked: asBoolean(), }, (el, { first }) => [ first('input', [ setProperty('checked'), on('change', ({ event }) => { el.checked = event.target.checked }), ]), ], ) component( 'module-tabgroup', { selected: '', }, (el, { all, first }) => { el.selected = el .querySelector('[role="tab"][aria-selected="true"]') ?.getAttribute('aria-controls') ?? '' const isSelected = target => el.selected === target.getAttribute('aria-controls') const tabs = Array.from(el.querySelectorAll('[role="tab"]')) return [ all('[role="tab"]', [ on('click', ({ event }) => { el.selected = event.currentTarget.getAttribute( 'aria-controls', ) ?? '' }), setProperty('ariaSelected', target => String(isSelected(target)), ), setProperty('tabIndex', target => isSelected(target) ? 0 : -1, ), ]), all( '[role="tabpanel"]', setProperty( 'hidden', target => el.selected !== target.id, ), ), ] }, ) runTests(() => { describe('Basic Component Functionality', () => { it('should create void component instances', () => { const voidComponent = document.getElementById('void') assert.instanceOf(voidComponent, HTMLElement) assert.equal(voidComponent.localName, 'void-component') }) it('should handle components with no effects', () => { const voidComponent = document.getElementById('void') const originalContent = voidComponent.innerHTML // Component should not modify content when it has no effects assert.include(originalContent, 'Hello from Server') }) }) describe('Reactive Properties', () => { it('should initialize properties from internal state', async () => { const causal = document.getElementById('causal') const heading = causal.querySelector('h1') assert.equal( normalizeText(heading.textContent), 'Hello from Internal State', ) }) it('should update properties from attributes', async () => { const updating = document.getElementById( 'updating-with-attributes', ) // Check string attribute assert.equal(updating.heading, 'Hello from Attribute') // Check number attributes assert.equal(updating.count, 42) assert.equal(updating.step, 0.1) assert.equal(updating.value, 3.14) // Check boolean attribute assert.equal(updating.selected, true) }) it('should update DOM when properties change', async () => { const updating = document.getElementById( 'updating-with-attributes', ) const h1 = updating.querySelector('h1') const span = updating.querySelector('span') const input = updating.querySelector('input') assert.equal( normalizeText(h1.textContent), 'Hello from Attribute', ) assert.equal(span.textContent, '42') assert.equal(input.step, '0.1') assert.equal(input.value, '3.14') assert.isTrue(h1.classList.contains('selected')) }) it('should handle property updates after initialization', async () => { const updating = document.getElementById( 'updating-with-attributes', ) // Update properties updating.heading = 'Updated Heading' updating.count = 100 updating.selected = false const h1 = updating.querySelector('h1') const span = updating.querySelector('span') assert.equal( normalizeText(h1.textContent), 'Updated Heading', ) assert.equal(span.textContent, '100') assert.isFalse(h1.classList.contains('selected')) }) }) describe('Event Handling', () => { it('should handle click events and update state', async () => { const counter = document.querySelector('basic-counter') const incrementBtn = counter.querySelector('.increment') const decrementBtn = counter.querySelector('.decrement') const valueSpan = counter.querySelector('.value') assert.equal(counter.value, 42) assert.equal(valueSpan.textContent, '42') // Test increment incrementBtn.click() assert.equal(counter.value, 43) assert.equal(valueSpan.textContent, '43') // Test decrement decrementBtn.click() assert.equal(counter.value, 42) assert.equal(valueSpan.textContent, '42') }) it('should handle form input events', async () => { const configurator = document.querySelector( 'greeting-configurator', ) const textbox = configurator.querySelector('.first') const input = textbox.querySelector('input') // Change input value input.value = 'John' input.dispatchEvent(new Event('change')) assert.equal(textbox.value, 'John') }) }) describe('Computed Properties', () => { it('should update computed values when dependencies change', async () => { const counter = document.querySelector('basic-counter') const paritySpan = counter.querySelector('.parity') const doubleSpan = counter.querySelector('.double') // Initial state (42 is even) assert.equal(paritySpan.textContent, 'even') assert.equal(doubleSpan.textContent, '84') // Increment to make odd const incrementBtn = counter.querySelector('.increment') incrementBtn.click() assert.equal(paritySpan.textContent, 'odd') assert.equal(doubleSpan.textContent, '86') }) }) describe('Component Communication', () => { it('should handle parent-child component communication', async () => { // Temporarily simplified test to avoid timing issues const configurator = document.querySelector( 'greeting-configurator', ) const greeting = configurator.querySelector('hello-world') // Just test that components exist and can communicate assert.instanceOf(configurator, HTMLElement) assert.instanceOf(greeting, HTMLElement) }) }) describe('Tab Group Component', () => { it('should initialize with correct active tab', async () => { const tabgroup = document.querySelector('module-tabgroup') const firstTab = tabgroup.querySelector('#trigger1') const firstPanel = tabgroup.querySelector('#panel1') assert.equal( firstTab.getAttribute('aria-selected'), 'true', ) assert.equal(firstTab.tabIndex, 0) assert.isFalse(firstPanel.hidden) }) it('should switch tabs when clicked', async () => { const tabgroup = document.querySelector('module-tabgroup') const firstTab = tabgroup.querySelector('#trigger1') const secondTab = tabgroup.querySelector('#trigger2') const firstPanel = tabgroup.querySelector('#panel1') const secondPanel = tabgroup.querySelector('#panel2') // Click second tab secondTab.click() assert.equal( firstTab.getAttribute('aria-selected'), 'false', ) assert.equal( secondTab.getAttribute('aria-selected'), 'true', ) assert.equal(firstTab.tabIndex, -1) assert.equal(secondTab.tabIndex, 0) assert.isTrue(firstPanel.hidden) assert.isFalse(secondPanel.hidden) }) }) describe('Component Lifecycle', () => { it('should handle connect and disconnect properly', async () => { const testComponent = document.createElement('void-component') document.body.appendChild(testComponent) assert.isTrue(testComponent.isConnected) document.body.removeChild(testComponent) assert.isFalse(testComponent.isConnected) }) it('should handle multiple instances independently', async () => { const counter1 = document.createElement('basic-counter') const counter2 = document.createElement('basic-counter') counter1.setAttribute('value', '10') counter2.setAttribute('value', '20') document.body.appendChild(counter1) document.body.appendChild(counter2) assert.equal(counter1.value, 10) assert.equal(counter2.value, 20) // Update one counter counter1.value = 15 assert.equal(counter1.value, 15) assert.equal(counter2.value, 20) // Should remain unchanged document.body.removeChild(counter1) document.body.removeChild(counter2) }) }) describe('Signal Management', () => { it('should get and set signals correctly', async () => { const component = document.createElement('basic-counter') component.setAttribute('value', '5') document.body.appendChild(component) // Get signal const valueSignal = component.getSignal('value') assert.equal(valueSignal.get(), 5) // Set new signal const newSignal = state(10) component.setSignal('value', newSignal) assert.equal(component.value, 10) document.body.removeChild(component) }) it('should handle computed signals', async () => { const component = document.createElement('basic-counter') document.body.appendChild(component) const doubleSignal = computed(() => component.value * 2) component.setSignal('double', doubleSignal) component.value = 5 assert.equal(component.double, 10) document.body.removeChild(component) }) }) }) </script> </body> </html>