UNPKG

@zeix/ui-element

Version:

UIElement - a HTML-first library for reactive Web Components

561 lines (458 loc) 16 kB
<!doctype html> <html> <head> <title>fromSelector Tests</title> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> </head> <body> <div class="test-element" id="original">Content</div> <script type="module"> import { runTests } from '@web/test-runner-mocha' import { assert } from '@esm-bundle/chai' import { fromSelector, computed, effect } from '../../index.dev.js' const wait = ms => new Promise(resolve => setTimeout(resolve, ms)) const animationFrame = async () => new Promise(requestAnimationFrame) const microtask = async () => new Promise(queueMicrotask) runTests(() => { describe('fromSelector()', () => { it('should create a signal returning an empty array for no matching elements', () => { const signal = fromSelector('.nonexistent')(document) assert.deepEqual(signal.get(), []) }) it('should return an array of elements matching the selector', () => { const signal = fromSelector('.test-element')(document) const elements = Array.from( document.querySelectorAll('.test-element'), ) assert.deepEqual(signal.get(), elements) }) it('should update the signal when elements are added or removed', () => { const signal = fromSelector('.test-element')(document) const newElement = document.createElement('div') newElement.classList.add('test-element') document.body.appendChild(newElement) let elements = Array.from( document.querySelectorAll('.test-element'), ) assert.deepEqual(signal.get(), elements) newElement.remove() elements = Array.from( document.querySelectorAll('.test-element'), ) assert.deepEqual(signal.get(), elements) }) it('should update the signal when matching class is added or removed', () => { const signal = fromSelector('.test-element')(document) const newElement = document.createElement('div') document.body.appendChild(newElement) let elements = Array.from( document.querySelectorAll('.test-element'), ) assert.deepEqual(signal.get(), elements) newElement.classList.add('test-element') elements = Array.from( document.querySelectorAll('.test-element'), ) assert.deepEqual(signal.get(), elements) newElement.classList.remove('test-element') elements = Array.from( document.querySelectorAll('.test-element'), ) assert.deepEqual(signal.get(), elements) newElement.remove() }) it('should update the signal when matching id is added or removed', () => { const signal = fromSelector('#test-element')(document) const newElement = document.createElement('div') document.body.appendChild(newElement) let elements = Array.from( document.querySelectorAll('#test-element'), ) assert.deepEqual(signal.get(), elements) newElement.id = 'test-element' elements = Array.from( document.querySelectorAll('#test-element'), ) assert.deepEqual(signal.get(), elements) newElement.removeAttribute('id') elements = Array.from( document.querySelectorAll('#test-element'), ) assert.deepEqual(signal.get(), elements) newElement.remove() }) it('should update the computed signal watching the element selection when elements are added or removed', async () => { const signal = fromSelector('.test-element')(document) const contents = computed(elements => signal.get().map(element => element.textContent), ) // Wait for initial setup await microtask() assert.deepEqual(contents.get(), ['Content']) const newElement = document.createElement('div') newElement.textContent = 'New Content' newElement.classList.add('test-element') document.body.appendChild(newElement) await microtask() assert.deepEqual(contents.get(), [ 'Content', 'New Content', ]) newElement.remove() await microtask() assert.deepEqual(contents.get(), ['Content']) }) it('should apply the effect to an updated array of elements when elements are added or removed', async () => { const signal = fromSelector('.test-element')(document) document .querySelectorAll('.test-element') .forEach(element => { element.hidden = false }) const cleanup = effect({ signals: [signal], ok: elements => elements .filter(element => !element.hidden) .map(element => { element.hidden = true }), }) // Wait for initial effect to run await microtask() const newElement = document.createElement('div') newElement.classList.add('test-element') newElement.classList.add('test-element') document.body.appendChild(newElement) await microtask() let expected = Array.from( document.querySelectorAll('.test-element'), ).filter(element => element.hidden === true) assert.equal(expected.length, 2) document .querySelectorAll('.test-element') .forEach(element => { element.hidden = false }) newElement.remove() await microtask() expected = Array.from( document.querySelectorAll('.test-element'), ).filter(element => element.hidden === true) assert.equal(expected.length, 1) document .querySelectorAll('.test-element') .forEach(element => { element.hidden = false }) cleanup() }) it('should handle complex selectors with multiple attributes', async () => { const container = document.createElement('div') document.body.appendChild(container) try { const signal = fromSelector( 'div.active[role="button"][data-test="value"]', )(container) // Initially empty assert.deepEqual(signal.get(), []) // Add element with only class const partial = document.createElement('div') partial.classList.add('active') container.appendChild(partial) await microtask() assert.deepEqual(signal.get(), []) // Add role attribute partial.setAttribute('role', 'button') await microtask() assert.deepEqual(signal.get(), []) // Add data attribute to complete match partial.setAttribute('data-test', 'value') await microtask() assert.deepEqual(signal.get(), [partial]) // Remove role to break match partial.removeAttribute('role') await microtask() assert.deepEqual(signal.get(), []) } finally { container.remove() } }) it('should handle attribute selectors with different operators', async () => { const container = document.createElement('div') document.body.appendChild(container) try { const signal = fromSelector( '[data-prefix^="test"]', )(container) const element1 = document.createElement('div') element1.setAttribute('data-prefix', 'testing') container.appendChild(element1) await microtask() assert.deepEqual(signal.get(), [element1]) const element2 = document.createElement('div') element2.setAttribute('data-prefix', 'other') container.appendChild(element2) await microtask() assert.deepEqual(signal.get(), [element1]) element2.setAttribute('data-prefix', 'test-value') await microtask() assert.deepEqual(signal.get(), [element1, element2]) } finally { container.remove() } }) it('should properly disconnect observer when no watchers exist', () => { const container = document.createElement('div') document.body.appendChild(container) try { const signal = fromSelector('.test')(container) // Get value to create observer signal.get() // Add element to trigger mutation const element = document.createElement('div') element.classList.add('test') container.appendChild(element) // Observer should disconnect automatically since no watchers assert.deepEqual(signal.get(), [element]) } finally { container.remove() } }) it('should handle rapid DOM mutations efficiently', async () => { const container = document.createElement('div') document.body.appendChild(container) try { const signal = fromSelector('.rapid-test')(container) let updateCount = 0 const cleanup = effect({ signals: [signal], ok: () => { updateCount++ }, }) // Rapid mutations for (let i = 0; i < 10; i++) { const element = document.createElement('div') element.classList.add('rapid-test') container.appendChild(element) } // Should handle rapid mutations without excessive updates assert.isAtLeast(updateCount, 1) assert.isAtMost(updateCount, 15) // Allow some batching variance cleanup() } finally { container.remove() } }) it('should detect circular mutations and throw CircularMutationError', async () => { const container = document.createElement('div') document.body.appendChild(container) try { const signal = fromSelector('.circular')(container) let errorThrown = false const cleanup = effect({ signals: [signal], ok: elements => { // Create circular mutation by adding element in response to signal change if (elements.length < 3) { const newElement = document.createElement('div') newElement.classList.add('circular') container.appendChild(newElement) } }, error: error => { errorThrown = true assert.instanceOf(error, Error) assert.include(error.message, 'circular') }, }) // Trigger the circular mutation const initialElement = document.createElement('div') initialElement.classList.add('circular') container.appendChild(initialElement) // Wait for effects to run await animationFrame() // Either error should be caught or effect should have run multiple times assert.isTrue( errorThrown || signal.get().length >= 3, 'Should either throw circular mutation error or handle rapid mutations', ) cleanup() } finally { container.remove() } }) it('should create a signal producer that selects elements', () => { const container = document.createElement('div') container.innerHTML = ` <div class="item" data-value="1">Item 1</div> <div class="item" data-value="2">Item 2</div> <p>Not an item</p> ` document.body.appendChild(container) try { const signal = fromSelector('.item')(container) const items = signal.get() assert.equal(items.length, 2) assert.equal(items[0].dataset.value, '1') assert.equal(items[1].dataset.value, '2') } finally { container.remove() } }) it('should update when matching elements change', async () => { const container = document.createElement('div') container.innerHTML = `<div class="item">Original</div>` document.body.appendChild(container) try { const signal = fromSelector('.item')(container) assert.equal(signal.get().length, 1) // Add element const newItem = document.createElement('div') newItem.className = 'item' newItem.textContent = 'New' container.appendChild(newItem) assert.equal(signal.get().length, 2) // Remove element newItem.remove() assert.equal(signal.get().length, 1) } finally { container.remove() } }) it('should work with complex selectors', () => { const container = document.createElement('div') container.innerHTML = ` <form class="user-form"> <input type="email" required name="email" /> <input type="text" name="name" /> <button type="submit">Submit</button> </form> ` document.body.appendChild(container) try { const signal = fromSelector( 'form.user-form input[required]', )(container) const requiredInputs = signal.get() assert.equal(requiredInputs.length, 1) assert.equal(requiredInputs[0].type, 'email') assert.equal(requiredInputs[0].name, 'email') } finally { container.remove() } }) it('should handle selection with rapidly changing DOM', async () => { const container = document.createElement('div') document.body.appendChild(container) try { const signal = fromSelector('.dynamic')(container) let lastCount = 0 const cleanup = effect({ signals: [signal], ok: elements => { lastCount = elements.length }, }) // Add elements rapidly const elements = [] for (let i = 0; i < 5; i++) { const element = document.createElement('div') element.className = 'dynamic' element.textContent = `Item ${i}` container.appendChild(element) elements.push(element) } await animationFrame() assert.equal(lastCount, 5) // Remove elements elements.forEach(el => el.remove()) await animationFrame() assert.equal(lastCount, 0) cleanup() } finally { container.remove() } }) it('should handle events during DOM mutations', async () => { const container = document.createElement('div') document.body.appendChild(container) try { const signal = fromSelector('.event-test')(container) let eventCount = 0 const cleanup = effect({ signals: [signal], ok: elements => { elements.forEach(element => { element.addEventListener( 'click', () => { eventCount++ }, ) }) }, }) // Add element and trigger event const element = document.createElement('div') element.className = 'event-test' container.appendChild(element) await animationFrame() element.click() assert.equal(eventCount, 1) // Add another element const element2 = document.createElement('div') element2.className = 'event-test' container.appendChild(element2) await animationFrame() element2.click() assert.equal(eventCount, 2) cleanup() } finally { container.remove() } }) it('should handle memory cleanup properly', async () => { const container = document.createElement('div') document.body.appendChild(container) try { const signal = fromSelector('.cleanup-test')(container) let effectRuns = 0 const cleanup = effect({ signals: [signal], ok: () => { effectRuns++ }, }) // Add and remove elements for (let i = 0; i < 3; i++) { const element = document.createElement('div') element.className = 'cleanup-test' container.appendChild(element) await microtask() element.remove() await microtask() } const initialRuns = effectRuns // Cleanup effect cleanup() // Add more elements - should not trigger effects const element = document.createElement('div') element.className = 'cleanup-test' container.appendChild(element) assert.equal(effectRuns, initialRuns) } finally { container.remove() } }) }) }) </script> </body> </html>