summaryrefslogtreecommitdiffstats
path: root/js/tests/unit/dom
diff options
context:
space:
mode:
Diffstat (limited to 'js/tests/unit/dom')
-rw-r--r--js/tests/unit/dom/data.spec.js106
-rw-r--r--js/tests/unit/dom/event-handler.spec.js480
-rw-r--r--js/tests/unit/dom/manipulator.spec.js135
-rw-r--r--js/tests/unit/dom/selector-engine.spec.js236
4 files changed, 957 insertions, 0 deletions
diff --git a/js/tests/unit/dom/data.spec.js b/js/tests/unit/dom/data.spec.js
new file mode 100644
index 0000000..e898cbb
--- /dev/null
+++ b/js/tests/unit/dom/data.spec.js
@@ -0,0 +1,106 @@
+import Data from '../../../src/dom/data'
+import { getFixture, clearFixture } from '../../helpers/fixture'
+
+describe('Data', () => {
+ const TEST_KEY = 'bs.test'
+ const UNKNOWN_KEY = 'bs.unknown'
+ const TEST_DATA = {
+ test: 'bsData'
+ }
+
+ let fixtureEl
+ let div
+
+ beforeAll(() => {
+ fixtureEl = getFixture()
+ })
+
+ beforeEach(() => {
+ fixtureEl.innerHTML = '<div></div>'
+ div = fixtureEl.querySelector('div')
+ })
+
+ afterEach(() => {
+ Data.remove(div, TEST_KEY)
+ clearFixture()
+ })
+
+ it('should return null for unknown elements', () => {
+ const data = { ...TEST_DATA }
+
+ Data.set(div, TEST_KEY, data)
+
+ expect(Data.get(null)).toBeNull()
+ expect(Data.get(undefined)).toBeNull()
+ expect(Data.get(document.createElement('div'), TEST_KEY)).toBeNull()
+ })
+
+ it('should return null for unknown keys', () => {
+ const data = { ...TEST_DATA }
+
+ Data.set(div, TEST_KEY, data)
+
+ expect(Data.get(div, null)).toBeNull()
+ expect(Data.get(div, undefined)).toBeNull()
+ expect(Data.get(div, UNKNOWN_KEY)).toBeNull()
+ })
+
+ it('should store data for an element with a given key and return it', () => {
+ const data = { ...TEST_DATA }
+
+ Data.set(div, TEST_KEY, data)
+
+ expect(Data.get(div, TEST_KEY)).toEqual(data)
+ })
+
+ it('should overwrite data if something is already stored', () => {
+ const data = { ...TEST_DATA }
+ const copy = { ...data }
+
+ Data.set(div, TEST_KEY, data)
+ Data.set(div, TEST_KEY, copy)
+
+ // Using `toBe` since spread creates a shallow copy
+ expect(Data.get(div, TEST_KEY)).not.toBe(data)
+ expect(Data.get(div, TEST_KEY)).toBe(copy)
+ })
+
+ it('should do nothing when an element has nothing stored', () => {
+ Data.remove(div, TEST_KEY)
+
+ expect().nothing()
+ })
+
+ it('should remove nothing for an unknown key', () => {
+ const data = { ...TEST_DATA }
+
+ Data.set(div, TEST_KEY, data)
+ Data.remove(div, UNKNOWN_KEY)
+
+ expect(Data.get(div, TEST_KEY)).toEqual(data)
+ })
+
+ it('should remove data for a given key', () => {
+ const data = { ...TEST_DATA }
+
+ Data.set(div, TEST_KEY, data)
+ Data.remove(div, TEST_KEY)
+
+ expect(Data.get(div, TEST_KEY)).toBeNull()
+ })
+
+ /* eslint-disable no-console */
+ it('should console.error a message if called with multiple keys', () => {
+ console.error = jasmine.createSpy('console.error')
+
+ const data = { ...TEST_DATA }
+ const copy = { ...data }
+
+ Data.set(div, TEST_KEY, data)
+ Data.set(div, UNKNOWN_KEY, copy)
+
+ expect(console.error).toHaveBeenCalled()
+ expect(Data.get(div, UNKNOWN_KEY)).toBeNull()
+ })
+ /* eslint-enable no-console */
+})
diff --git a/js/tests/unit/dom/event-handler.spec.js b/js/tests/unit/dom/event-handler.spec.js
new file mode 100644
index 0000000..623b9c1
--- /dev/null
+++ b/js/tests/unit/dom/event-handler.spec.js
@@ -0,0 +1,480 @@
+import EventHandler from '../../../src/dom/event-handler'
+import { clearFixture, getFixture } from '../../helpers/fixture'
+import { noop } from '../../../src/util'
+
+describe('EventHandler', () => {
+ let fixtureEl
+
+ beforeAll(() => {
+ fixtureEl = getFixture()
+ })
+
+ afterEach(() => {
+ clearFixture()
+ })
+
+ describe('on', () => {
+ it('should not add event listener if the event is not a string', () => {
+ fixtureEl.innerHTML = '<div></div>'
+
+ const div = fixtureEl.querySelector('div')
+
+ EventHandler.on(div, null, noop)
+ EventHandler.on(null, 'click', noop)
+
+ expect().nothing()
+ })
+
+ it('should add event listener', () => {
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = '<div></div>'
+
+ const div = fixtureEl.querySelector('div')
+
+ EventHandler.on(div, 'click', () => {
+ expect().nothing()
+ resolve()
+ })
+
+ div.click()
+ })
+ })
+
+ it('should add namespaced event listener', () => {
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = '<div></div>'
+
+ const div = fixtureEl.querySelector('div')
+
+ EventHandler.on(div, 'bs.namespace', () => {
+ expect().nothing()
+ resolve()
+ })
+
+ EventHandler.trigger(div, 'bs.namespace')
+ })
+ })
+
+ it('should add native namespaced event listener', () => {
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = '<div></div>'
+
+ const div = fixtureEl.querySelector('div')
+
+ EventHandler.on(div, 'click.namespace', () => {
+ expect().nothing()
+ resolve()
+ })
+
+ EventHandler.trigger(div, 'click')
+ })
+ })
+
+ it('should handle event delegation', () => {
+ return new Promise(resolve => {
+ EventHandler.on(document, 'click', '.test', () => {
+ expect().nothing()
+ resolve()
+ })
+
+ fixtureEl.innerHTML = '<div class="test"></div>'
+
+ const div = fixtureEl.querySelector('div')
+
+ div.click()
+ })
+ })
+
+ it('should handle mouseenter/mouseleave like the native counterpart', () => {
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = [
+ '<div class="outer">',
+ '<div class="inner">',
+ '<div class="nested">',
+ '<div class="deep"></div>',
+ '</div>',
+ '</div>',
+ '<div class="sibling"></div>',
+ '</div>'
+ ].join('')
+
+ const outer = fixtureEl.querySelector('.outer')
+ const inner = fixtureEl.querySelector('.inner')
+ const nested = fixtureEl.querySelector('.nested')
+ const deep = fixtureEl.querySelector('.deep')
+ const sibling = fixtureEl.querySelector('.sibling')
+
+ const enterSpy = jasmine.createSpy('mouseenter')
+ const leaveSpy = jasmine.createSpy('mouseleave')
+ const delegateEnterSpy = jasmine.createSpy('mouseenter')
+ const delegateLeaveSpy = jasmine.createSpy('mouseleave')
+
+ EventHandler.on(inner, 'mouseenter', enterSpy)
+ EventHandler.on(inner, 'mouseleave', leaveSpy)
+ EventHandler.on(outer, 'mouseenter', '.inner', delegateEnterSpy)
+ EventHandler.on(outer, 'mouseleave', '.inner', delegateLeaveSpy)
+
+ EventHandler.on(sibling, 'mouseenter', () => {
+ expect(enterSpy.calls.count()).toEqual(2)
+ expect(leaveSpy.calls.count()).toEqual(2)
+ expect(delegateEnterSpy.calls.count()).toEqual(2)
+ expect(delegateLeaveSpy.calls.count()).toEqual(2)
+ resolve()
+ })
+
+ const moveMouse = (from, to) => {
+ from.dispatchEvent(new MouseEvent('mouseout', {
+ bubbles: true,
+ relatedTarget: to
+ }))
+
+ to.dispatchEvent(new MouseEvent('mouseover', {
+ bubbles: true,
+ relatedTarget: from
+ }))
+ }
+
+ // from outer to deep and back to outer (nested)
+ moveMouse(outer, inner)
+ moveMouse(inner, nested)
+ moveMouse(nested, deep)
+ moveMouse(deep, nested)
+ moveMouse(nested, inner)
+ moveMouse(inner, outer)
+
+ setTimeout(() => {
+ expect(enterSpy.calls.count()).toEqual(1)
+ expect(leaveSpy.calls.count()).toEqual(1)
+ expect(delegateEnterSpy.calls.count()).toEqual(1)
+ expect(delegateLeaveSpy.calls.count()).toEqual(1)
+
+ // from outer to inner to sibling (adjacent)
+ moveMouse(outer, inner)
+ moveMouse(inner, sibling)
+ }, 20)
+ })
+ })
+ })
+
+ describe('one', () => {
+ it('should call listener just once', () => {
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = '<div></div>'
+
+ let called = 0
+ const div = fixtureEl.querySelector('div')
+ const obj = {
+ oneListener() {
+ called++
+ }
+ }
+
+ EventHandler.one(div, 'bootstrap', obj.oneListener)
+
+ EventHandler.trigger(div, 'bootstrap')
+ EventHandler.trigger(div, 'bootstrap')
+
+ setTimeout(() => {
+ expect(called).toEqual(1)
+ resolve()
+ }, 20)
+ })
+ })
+
+ it('should call delegated listener just once', () => {
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = '<div></div>'
+
+ let called = 0
+ const div = fixtureEl.querySelector('div')
+ const obj = {
+ oneListener() {
+ called++
+ }
+ }
+
+ EventHandler.one(fixtureEl, 'bootstrap', 'div', obj.oneListener)
+
+ EventHandler.trigger(div, 'bootstrap')
+ EventHandler.trigger(div, 'bootstrap')
+
+ setTimeout(() => {
+ expect(called).toEqual(1)
+ resolve()
+ }, 20)
+ })
+ })
+ })
+
+ describe('off', () => {
+ it('should not remove a listener', () => {
+ fixtureEl.innerHTML = '<div></div>'
+ const div = fixtureEl.querySelector('div')
+
+ EventHandler.off(div, null, noop)
+ EventHandler.off(null, 'click', noop)
+ expect().nothing()
+ })
+
+ it('should remove a listener', () => {
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = '<div></div>'
+ const div = fixtureEl.querySelector('div')
+
+ let called = 0
+ const handler = () => {
+ called++
+ }
+
+ EventHandler.on(div, 'foobar', handler)
+ EventHandler.trigger(div, 'foobar')
+
+ EventHandler.off(div, 'foobar', handler)
+ EventHandler.trigger(div, 'foobar')
+
+ setTimeout(() => {
+ expect(called).toEqual(1)
+ resolve()
+ }, 20)
+ })
+ })
+
+ it('should remove all the events', () => {
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = '<div></div>'
+ const div = fixtureEl.querySelector('div')
+
+ let called = 0
+
+ EventHandler.on(div, 'foobar', () => {
+ called++
+ })
+ EventHandler.on(div, 'foobar', () => {
+ called++
+ })
+ EventHandler.trigger(div, 'foobar')
+
+ EventHandler.off(div, 'foobar')
+ EventHandler.trigger(div, 'foobar')
+
+ setTimeout(() => {
+ expect(called).toEqual(2)
+ resolve()
+ }, 20)
+ })
+ })
+
+ it('should remove all the namespaced listeners if namespace is passed', () => {
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = '<div></div>'
+ const div = fixtureEl.querySelector('div')
+
+ let called = 0
+
+ EventHandler.on(div, 'foobar.namespace', () => {
+ called++
+ })
+ EventHandler.on(div, 'foofoo.namespace', () => {
+ called++
+ })
+ EventHandler.trigger(div, 'foobar.namespace')
+ EventHandler.trigger(div, 'foofoo.namespace')
+
+ EventHandler.off(div, '.namespace')
+ EventHandler.trigger(div, 'foobar.namespace')
+ EventHandler.trigger(div, 'foofoo.namespace')
+
+ setTimeout(() => {
+ expect(called).toEqual(2)
+ resolve()
+ }, 20)
+ })
+ })
+
+ it('should remove the namespaced listeners', () => {
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = '<div></div>'
+ const div = fixtureEl.querySelector('div')
+
+ let calledCallback1 = 0
+ let calledCallback2 = 0
+
+ EventHandler.on(div, 'foobar.namespace', () => {
+ calledCallback1++
+ })
+ EventHandler.on(div, 'foofoo.namespace', () => {
+ calledCallback2++
+ })
+
+ EventHandler.trigger(div, 'foobar.namespace')
+ EventHandler.off(div, 'foobar.namespace')
+ EventHandler.trigger(div, 'foobar.namespace')
+
+ EventHandler.trigger(div, 'foofoo.namespace')
+
+ setTimeout(() => {
+ expect(calledCallback1).toEqual(1)
+ expect(calledCallback2).toEqual(1)
+ resolve()
+ }, 20)
+ })
+ })
+
+ it('should remove the all the namespaced listeners for native events', () => {
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = '<div></div>'
+ const div = fixtureEl.querySelector('div')
+
+ let called = 0
+
+ EventHandler.on(div, 'click.namespace', () => {
+ called++
+ })
+ EventHandler.on(div, 'click.namespace2', () => {
+ called++
+ })
+
+ EventHandler.trigger(div, 'click')
+ EventHandler.off(div, 'click')
+ EventHandler.trigger(div, 'click')
+
+ setTimeout(() => {
+ expect(called).toEqual(2)
+ resolve()
+ }, 20)
+ })
+ })
+
+ it('should remove the specified namespaced listeners for native events', () => {
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = '<div></div>'
+ const div = fixtureEl.querySelector('div')
+
+ let called1 = 0
+ let called2 = 0
+
+ EventHandler.on(div, 'click.namespace', () => {
+ called1++
+ })
+ EventHandler.on(div, 'click.namespace2', () => {
+ called2++
+ })
+ EventHandler.trigger(div, 'click')
+
+ EventHandler.off(div, 'click.namespace')
+ EventHandler.trigger(div, 'click')
+
+ setTimeout(() => {
+ expect(called1).toEqual(1)
+ expect(called2).toEqual(2)
+ resolve()
+ }, 20)
+ })
+ })
+
+ it('should remove a listener registered by .one', () => {
+ return new Promise((resolve, reject) => {
+ fixtureEl.innerHTML = '<div></div>'
+
+ const div = fixtureEl.querySelector('div')
+ const handler = () => {
+ reject(new Error('called'))
+ }
+
+ EventHandler.one(div, 'foobar', handler)
+ EventHandler.off(div, 'foobar', handler)
+
+ EventHandler.trigger(div, 'foobar')
+ setTimeout(() => {
+ expect().nothing()
+ resolve()
+ }, 20)
+ })
+ })
+
+ it('should remove the correct delegated event listener', () => {
+ const element = document.createElement('div')
+ const subelement = document.createElement('span')
+ element.append(subelement)
+
+ const anchor = document.createElement('a')
+ element.append(anchor)
+
+ let i = 0
+ const handler = () => {
+ i++
+ }
+
+ EventHandler.on(element, 'click', 'a', handler)
+ EventHandler.on(element, 'click', 'span', handler)
+
+ fixtureEl.append(element)
+
+ EventHandler.trigger(anchor, 'click')
+ EventHandler.trigger(subelement, 'click')
+
+ // first listeners called
+ expect(i).toEqual(2)
+
+ EventHandler.off(element, 'click', 'span', handler)
+ EventHandler.trigger(subelement, 'click')
+
+ // removed listener not called
+ expect(i).toEqual(2)
+
+ EventHandler.trigger(anchor, 'click')
+
+ // not removed listener called
+ expect(i).toEqual(3)
+
+ EventHandler.on(element, 'click', 'span', handler)
+ EventHandler.trigger(anchor, 'click')
+ EventHandler.trigger(subelement, 'click')
+
+ // listener re-registered
+ expect(i).toEqual(5)
+
+ EventHandler.off(element, 'click', 'span')
+ EventHandler.trigger(subelement, 'click')
+
+ // listener removed again
+ expect(i).toEqual(5)
+ })
+ })
+
+ describe('general functionality', () => {
+ it('should hydrate properties, and make them configurable', () => {
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = [
+ '<div id="div1">',
+ ' <div id="div2"></div>',
+ ' <div id="div3"></div>',
+ '</div>'
+ ].join('')
+
+ const div1 = fixtureEl.querySelector('#div1')
+ const div2 = fixtureEl.querySelector('#div2')
+
+ EventHandler.on(div1, 'click', event => {
+ expect(event.currentTarget).toBe(div2)
+ expect(event.delegateTarget).toBe(div1)
+ expect(event.originalTarget).toBeNull()
+
+ Object.defineProperty(event, 'currentTarget', {
+ configurable: true,
+ get() {
+ return div1
+ }
+ })
+
+ expect(event.currentTarget).toBe(div1)
+ resolve()
+ })
+
+ expect(() => {
+ EventHandler.trigger(div1, 'click', { originalTarget: null, currentTarget: div2 })
+ }).not.toThrowError(TypeError)
+ })
+ })
+ })
+})
diff --git a/js/tests/unit/dom/manipulator.spec.js b/js/tests/unit/dom/manipulator.spec.js
new file mode 100644
index 0000000..4561e2e
--- /dev/null
+++ b/js/tests/unit/dom/manipulator.spec.js
@@ -0,0 +1,135 @@
+import Manipulator from '../../../src/dom/manipulator'
+import { clearFixture, getFixture } from '../../helpers/fixture'
+
+describe('Manipulator', () => {
+ let fixtureEl
+
+ beforeAll(() => {
+ fixtureEl = getFixture()
+ })
+
+ afterEach(() => {
+ clearFixture()
+ })
+
+ describe('setDataAttribute', () => {
+ it('should set data attribute prefixed with bs', () => {
+ fixtureEl.innerHTML = '<div></div>'
+
+ const div = fixtureEl.querySelector('div')
+
+ Manipulator.setDataAttribute(div, 'key', 'value')
+ expect(div.getAttribute('data-bs-key')).toEqual('value')
+ })
+
+ it('should set data attribute in kebab case', () => {
+ fixtureEl.innerHTML = '<div></div>'
+
+ const div = fixtureEl.querySelector('div')
+
+ Manipulator.setDataAttribute(div, 'testKey', 'value')
+ expect(div.getAttribute('data-bs-test-key')).toEqual('value')
+ })
+ })
+
+ describe('removeDataAttribute', () => {
+ it('should only remove bs-prefixed data attribute', () => {
+ fixtureEl.innerHTML = '<div data-bs-key="value" data-key-bs="postfixed" data-key="value"></div>'
+
+ const div = fixtureEl.querySelector('div')
+
+ Manipulator.removeDataAttribute(div, 'key')
+ expect(div.getAttribute('data-bs-key')).toBeNull()
+ expect(div.getAttribute('data-key-bs')).toEqual('postfixed')
+ expect(div.getAttribute('data-key')).toEqual('value')
+ })
+
+ it('should remove data attribute in kebab case', () => {
+ fixtureEl.innerHTML = '<div data-bs-test-key="value"></div>'
+
+ const div = fixtureEl.querySelector('div')
+
+ Manipulator.removeDataAttribute(div, 'testKey')
+ expect(div.getAttribute('data-bs-test-key')).toBeNull()
+ })
+ })
+
+ describe('getDataAttributes', () => {
+ it('should return an empty object for null', () => {
+ expect(Manipulator.getDataAttributes(null)).toEqual({})
+ expect().nothing()
+ })
+
+ it('should get only bs-prefixed data attributes without bs namespace', () => {
+ fixtureEl.innerHTML = '<div data-bs-toggle="tabs" data-bs-target="#element" data-another="value" data-target-bs="#element" data-in-bs-out="in-between"></div>'
+
+ const div = fixtureEl.querySelector('div')
+
+ expect(Manipulator.getDataAttributes(div)).toEqual({
+ toggle: 'tabs',
+ target: '#element'
+ })
+ })
+
+ it('should omit `bs-config` data attribute', () => {
+ fixtureEl.innerHTML = '<div data-bs-toggle="tabs" data-bs-target="#element" data-bs-config=\'{"testBool":false}\'></div>'
+
+ const div = fixtureEl.querySelector('div')
+
+ expect(Manipulator.getDataAttributes(div)).toEqual({
+ toggle: 'tabs',
+ target: '#element'
+ })
+ })
+ })
+
+ describe('getDataAttribute', () => {
+ it('should only get bs-prefixed data attribute', () => {
+ fixtureEl.innerHTML = '<div data-bs-key="value" data-test-bs="postFixed" data-toggle="tab"></div>'
+
+ const div = fixtureEl.querySelector('div')
+
+ expect(Manipulator.getDataAttribute(div, 'key')).toEqual('value')
+ expect(Manipulator.getDataAttribute(div, 'test')).toBeNull()
+ expect(Manipulator.getDataAttribute(div, 'toggle')).toBeNull()
+ })
+
+ it('should get data attribute in kebab case', () => {
+ fixtureEl.innerHTML = '<div data-bs-test-key="value" ></div>'
+
+ const div = fixtureEl.querySelector('div')
+
+ expect(Manipulator.getDataAttribute(div, 'testKey')).toEqual('value')
+ })
+
+ it('should normalize data', () => {
+ fixtureEl.innerHTML = '<div data-bs-test="false" ></div>'
+
+ const div = fixtureEl.querySelector('div')
+
+ expect(Manipulator.getDataAttribute(div, 'test')).toBeFalse()
+
+ div.setAttribute('data-bs-test', 'true')
+ expect(Manipulator.getDataAttribute(div, 'test')).toBeTrue()
+
+ div.setAttribute('data-bs-test', '1')
+ expect(Manipulator.getDataAttribute(div, 'test')).toEqual(1)
+ })
+
+ it('should normalize json data', () => {
+ fixtureEl.innerHTML = '<div data-bs-test=\'{"delay":{"show":100,"hide":10}}\'></div>'
+
+ const div = fixtureEl.querySelector('div')
+
+ expect(Manipulator.getDataAttribute(div, 'test')).toEqual({ delay: { show: 100, hide: 10 } })
+
+ const objectData = { 'Super Hero': ['Iron Man', 'Super Man'], testNum: 90, url: 'http://localhost:8080/test?foo=bar' }
+ const dataStr = JSON.stringify(objectData)
+ div.setAttribute('data-bs-test', encodeURIComponent(dataStr))
+ expect(Manipulator.getDataAttribute(div, 'test')).toEqual(objectData)
+
+ div.setAttribute('data-bs-test', dataStr)
+ expect(Manipulator.getDataAttribute(div, 'test')).toEqual(objectData)
+ })
+ })
+})
diff --git a/js/tests/unit/dom/selector-engine.spec.js b/js/tests/unit/dom/selector-engine.spec.js
new file mode 100644
index 0000000..0245896
--- /dev/null
+++ b/js/tests/unit/dom/selector-engine.spec.js
@@ -0,0 +1,236 @@
+import SelectorEngine from '../../../src/dom/selector-engine'
+import { getFixture, clearFixture } from '../../helpers/fixture'
+
+describe('SelectorEngine', () => {
+ let fixtureEl
+
+ beforeAll(() => {
+ fixtureEl = getFixture()
+ })
+
+ afterEach(() => {
+ clearFixture()
+ })
+
+ describe('find', () => {
+ it('should find elements', () => {
+ fixtureEl.innerHTML = '<div></div>'
+
+ const div = fixtureEl.querySelector('div')
+
+ expect(SelectorEngine.find('div', fixtureEl)).toEqual([div])
+ })
+
+ it('should find elements globally', () => {
+ fixtureEl.innerHTML = '<div id="test"></div>'
+
+ const div = fixtureEl.querySelector('#test')
+
+ expect(SelectorEngine.find('#test')).toEqual([div])
+ })
+
+ it('should handle :scope selectors', () => {
+ fixtureEl.innerHTML = [
+ '<ul>',
+ ' <li></li>',
+ ' <li>',
+ ' <a href="#" class="active">link</a>',
+ ' </li>',
+ ' <li></li>',
+ '</ul>'
+ ].join('')
+
+ const listEl = fixtureEl.querySelector('ul')
+ const aActive = fixtureEl.querySelector('.active')
+
+ expect(SelectorEngine.find(':scope > li > .active', listEl)).toEqual([aActive])
+ })
+ })
+
+ describe('findOne', () => {
+ it('should return one element', () => {
+ fixtureEl.innerHTML = '<div id="test"></div>'
+
+ const div = fixtureEl.querySelector('#test')
+
+ expect(SelectorEngine.findOne('#test')).toEqual(div)
+ })
+ })
+
+ describe('children', () => {
+ it('should find children', () => {
+ fixtureEl.innerHTML = [
+ '<ul>',
+ ' <li></li>',
+ ' <li></li>',
+ ' <li></li>',
+ '</ul>'
+ ].join('')
+
+ const list = fixtureEl.querySelector('ul')
+ const liList = [].concat(...fixtureEl.querySelectorAll('li'))
+ const result = SelectorEngine.children(list, 'li')
+
+ expect(result).toEqual(liList)
+ })
+ })
+
+ describe('parents', () => {
+ it('should return parents', () => {
+ expect(SelectorEngine.parents(fixtureEl, 'body')).toHaveSize(1)
+ })
+ })
+
+ describe('prev', () => {
+ it('should return previous element', () => {
+ fixtureEl.innerHTML = '<div class="test"></div><button class="btn"></button>'
+
+ const btn = fixtureEl.querySelector('.btn')
+ const divTest = fixtureEl.querySelector('.test')
+
+ expect(SelectorEngine.prev(btn, '.test')).toEqual([divTest])
+ })
+
+ it('should return previous element with an extra element between', () => {
+ fixtureEl.innerHTML = [
+ '<div class="test"></div>',
+ '<span></span>',
+ '<button class="btn"></button>'
+ ].join('')
+
+ const btn = fixtureEl.querySelector('.btn')
+ const divTest = fixtureEl.querySelector('.test')
+
+ expect(SelectorEngine.prev(btn, '.test')).toEqual([divTest])
+ })
+
+ it('should return previous element with comments or text nodes between', () => {
+ fixtureEl.innerHTML = [
+ '<div class="test"></div>',
+ '<div class="test"></div>',
+ '<!-- Comment-->',
+ 'Text',
+ '<button class="btn"></button>'
+ ].join('')
+
+ const btn = fixtureEl.querySelector('.btn')
+ const divTest = fixtureEl.querySelectorAll('.test')[1]
+
+ expect(SelectorEngine.prev(btn, '.test')).toEqual([divTest])
+ })
+ })
+
+ describe('next', () => {
+ it('should return next element', () => {
+ fixtureEl.innerHTML = '<div class="test"></div><button class="btn"></button>'
+
+ const btn = fixtureEl.querySelector('.btn')
+ const divTest = fixtureEl.querySelector('.test')
+
+ expect(SelectorEngine.next(divTest, '.btn')).toEqual([btn])
+ })
+
+ it('should return next element with an extra element between', () => {
+ fixtureEl.innerHTML = [
+ '<div class="test"></div>',
+ '<span></span>',
+ '<button class="btn"></button>'
+ ].join('')
+
+ const btn = fixtureEl.querySelector('.btn')
+ const divTest = fixtureEl.querySelector('.test')
+
+ expect(SelectorEngine.next(divTest, '.btn')).toEqual([btn])
+ })
+
+ it('should return next element with comments or text nodes between', () => {
+ fixtureEl.innerHTML = [
+ '<div class="test"></div>',
+ '<!-- Comment-->',
+ 'Text',
+ '<button class="btn"></button>',
+ '<button class="btn"></button>'
+ ].join('')
+
+ const btn = fixtureEl.querySelector('.btn')
+ const divTest = fixtureEl.querySelector('.test')
+
+ expect(SelectorEngine.next(divTest, '.btn')).toEqual([btn])
+ })
+ })
+
+ describe('focusableChildren', () => {
+ it('should return only elements with specific tag names', () => {
+ fixtureEl.innerHTML = [
+ '<div>lorem</div>',
+ '<span>lorem</span>',
+ '<a>lorem</a>',
+ '<button>lorem</button>',
+ '<input>',
+ '<textarea></textarea>',
+ '<select></select>',
+ '<details>lorem</details>'
+ ].join('')
+
+ const expectedElements = [
+ fixtureEl.querySelector('a'),
+ fixtureEl.querySelector('button'),
+ fixtureEl.querySelector('input'),
+ fixtureEl.querySelector('textarea'),
+ fixtureEl.querySelector('select'),
+ fixtureEl.querySelector('details')
+ ]
+
+ expect(SelectorEngine.focusableChildren(fixtureEl)).toEqual(expectedElements)
+ })
+
+ it('should return any element with non negative tab index', () => {
+ fixtureEl.innerHTML = [
+ '<div tabindex>lorem</div>',
+ '<div tabindex="0">lorem</div>',
+ '<div tabindex="10">lorem</div>'
+ ].join('')
+
+ const expectedElements = [
+ fixtureEl.querySelector('[tabindex]'),
+ fixtureEl.querySelector('[tabindex="0"]'),
+ fixtureEl.querySelector('[tabindex="10"]')
+ ]
+
+ expect(SelectorEngine.focusableChildren(fixtureEl)).toEqual(expectedElements)
+ })
+
+ it('should return not return elements with negative tab index', () => {
+ fixtureEl.innerHTML = '<button tabindex="-1">lorem</button>'
+
+ const expectedElements = []
+
+ expect(SelectorEngine.focusableChildren(fixtureEl)).toEqual(expectedElements)
+ })
+
+ it('should return contenteditable elements', () => {
+ fixtureEl.innerHTML = '<div contenteditable="true">lorem</div>'
+
+ const expectedElements = [fixtureEl.querySelector('[contenteditable="true"]')]
+
+ expect(SelectorEngine.focusableChildren(fixtureEl)).toEqual(expectedElements)
+ })
+
+ it('should not return disabled elements', () => {
+ fixtureEl.innerHTML = '<button disabled="true">lorem</button>'
+
+ const expectedElements = []
+
+ expect(SelectorEngine.focusableChildren(fixtureEl)).toEqual(expectedElements)
+ })
+
+ it('should not return invisible elements', () => {
+ fixtureEl.innerHTML = '<button style="display:none;">lorem</button>'
+
+ const expectedElements = []
+
+ expect(SelectorEngine.focusableChildren(fixtureEl)).toEqual(expectedElements)
+ })
+ })
+})
+