UNPKG

super-hands

Version:

All-in-one natural hand controller interaction component for A-Frame.

596 lines (590 loc) 26.1 kB
/* global assert, process, setup, suite, test, sinon */ const helpers = require('../helpers') const entityFactory = helpers.entityFactory suite('super-hands lifecycle', function () { setup(function (done) { const el = this.el = entityFactory() el.setAttribute('super-hands', '') el.addEventListener('loaded', function () { done() }) }) test('component attaches without errors', function () { assert.isOk(this.el.components['super-hands'].data) }) test('component removes without errors', function (done) { const el = this.el el.removeAttribute('super-hands') process.nextTick(function () { assert.notOk(el.components['super-hands']) done() }) }) }) suite('super-hands hit processing & event emission', function () { setup(function (done) { this.target1 = entityFactory() this.target2 = document.createElement('a-entity') this.target1.parentNode.appendChild(this.target2) this.hand1 = helpers.controllerFactory({ 'super-hands': '' }) this.hand2 = helpers.controllerFactory({ 'vive-controls': 'hand: left', 'super-hands': '' }, true) this.hand1.parentNode.addEventListener('loaded', () => { this.sh1 = this.hand1.components['super-hands'] this.sh2 = this.hand2.components['super-hands'] done() }) }) test('onHit finds the target entity in event details', function () { this.hand1.emit('hit', { el: this.target1 }) assert.includeMembers(this.sh1.hoverEls, [this.target1]) // change to cursor config this.hand1.setAttribute('super-hands', { colliderEvent: 'mouseenter', colliderEventProperty: 'intersectedEl' }) this.hand1.emit('mouseenter', { intersectedEl: this.target2 }) assert.includeMembers(this.sh1.hoverEls, [this.target2]) }) test('hover event', function (done) { this.target1.addEventListener('hover-start', evt => { assert.strictEqual(evt.detail.hand, this.hand1) assert.includeMembers(this.sh1.hoverEls, [this.target1]) done() }) this.sh1.onHit({ detail: { el: this.target1 } }) }) test('hover accepted', function () { this.target1.addEventListener('hover-start', e => e.preventDefault()) this.sh1.onHit({ detail: { el: this.target1 } }) assert.strictEqual(this.sh1.state.get(this.sh1.HOVER_EVENT), this.target1) }) test('hover rejected', function () { this.sh1.onHit({ detail: { el: this.target1 } }) assert.notOk(this.sh1.lastHover) }) test('unhover event', function (done) { this.target1.addEventListener('hover-start', e => e.preventDefault()) this.sh1.onHit({ detail: { el: this.target1 } }) this.target1.addEventListener('hover-end', evt => { assert.strictEqual(evt.detail.hand, this.hand1) assert.strictEqual(this.sh1.hoverEls.indexOf(this.target1), -1) done() }) helpers.simCollisionEnd(this.hand1, this.target1) }) test('stacking hovered entities', function () { this.sh1.onHit({ detail: { el: this.target1 } }) this.sh1.onHit({ detail: { el: this.target2 } }) assert.equal(this.sh1.hoverEls.length, 2) assert.strictEqual(this.sh1.hoverEls[0], this.target1) helpers.simCollisionEnd(this.hand1, this.target1) assert.equal(this.sh1.hoverEls.length, 1) assert.strictEqual(this.sh1.hoverEls[0], this.target2) helpers.simCollisionEnd(this.hand1, this.target2) assert.equal(this.sh1.hoverEls.length, 0) }) test('finding targets in the stack', function () { this.sh1.onHit({ detail: { el: this.target2 } }) this.sh1.onHit({ detail: { el: this.target1 } }) this.target2.addEventListener('grab-start', e => e.preventDefault()) this.target2.addEventListener('stretch-start', e => e.preventDefault()) this.sh1.onGrabStartButton() this.sh1.onStretchStartButton() assert.strictEqual(this.sh1.state.get(this.sh1.GRAB_EVENT), this.target2) assert.strictEqual(this.sh1.state.get(this.sh1.STRETCH_EVENT), this.target2) }) test('grab event', function (done) { this.target1.addEventListener('grab-start', evt => { assert.strictEqual(evt.detail.hand, this.hand1) assert.strictEqual(evt.detail.target, this.target1) done() }) this.sh1.onHit({ detail: { el: this.target1 } }) this.sh1.onGrabStartButton() }) test('grab accepted', function () { this.target1.addEventListener('grab-start', evt => { evt.preventDefault() }) this.sh1.onHit({ detail: { el: this.target1 } }) this.sh1.onGrabStartButton() assert.strictEqual(this.sh1.state.get(this.sh1.GRAB_EVENT), this.target1) }) test('grab rejected', function () { this.sh1.onHit({ detail: { el: this.target1 } }) this.sh1.onGrabStartButton() assert.notOk(this.sh1.state.get(this.sh1.GRAB_EVENT)) }) test('ungrab event', function (done) { this.target1 .addEventListener('grab-start', evt => evt.preventDefault()) this.target1.addEventListener('grab-end', evt => { assert.strictEqual(evt.detail.hand, this.hand1) process.nextTick(() => { assert.isNotOk(this.sh1.state.get(this.sh1.GRAB_EVENT)) done() }) evt.preventDefault() }) this.sh1.onHit({ detail: { el: this.target1 } }) this.sh1.onGrabStartButton() this.sh1.onGrabEndButton({}) }) test('finds other controller', function () { assert.isOk(this.sh1.otherSuperHand) assert.isOk(this.sh2.otherSuperHand) assert.strictEqual(this.sh1.otherSuperHand, this.sh2) assert.strictEqual(this.sh2.otherSuperHand, this.sh1) }) test('stretch event', function () { const stretchSpy = this.sinon.spy(this.sh2, 'emitCancelable') .withArgs(this.target1, 'stretch-start') const unStretchSpy = this.sinon.spy() this.target1.addEventListener('stretch-start', e => e.preventDefault()) this.target1.addEventListener('stretch-end', e => e.preventDefault()) this.target1.addEventListener('stretch-end', unStretchSpy) this.sh1.onHit({ detail: { el: this.target1 } }) this.sh1.onStretchStartButton() assert.strictEqual( this.sh1.state.get(this.sh1.STRETCH_EVENT), this.target1 ) assert.isFalse(stretchSpy.called) this.sh2.onHit({ detail: { el: this.target1 } }) this.sh2.onStretchStartButton() assert .strictEqual(this.sh2.state.get(this.sh2.STRETCH_EVENT), this.target1) assert.isTrue(stretchSpy.called) assert.isFalse(unStretchSpy.called) this.sh1.onStretchEndButton() assert.isTrue(unStretchSpy.called) assert .strictEqual(this.sh2.state.get(this.sh2.STRETCH_EVENT), this.target1) this.sh2.onStretchEndButton() assert.isTrue(unStretchSpy.calledTwice) }) test('stretch rejected', function () { this.sh1.onHit({ detail: { el: this.target1 } }) this.sh1.onStretchStartButton() this.sh2.onHit({ detail: { el: this.target1 } }) this.sh2.onStretchStartButton() assert.notOk(this.sh1.state.get(this.sh2.STRETCH_EVENT)) assert.notOk(this.sh2.state.get(this.sh2.STRETCH_EVENT)) }) test('drag accepted', function () { this.target1.addEventListener('drag-start', evt => { evt.preventDefault() }) this.sh1.onHit({ detail: { el: this.target1 } }) this.sh1.onDragDropStartButton() assert.strictEqual(this.sh1.state.get(this.sh1.DRAG_EVENT), this.target1) }) test('drag rejected', function () { this.sh1.onHit({ detail: { el: this.target1 } }) this.sh1.onDragDropStartButton() assert.notOk(this.sh1.state.get(this.sh1.DRAG_EVENT)) }) test('undrag event -- no drop target', function () { const dragEndSpy = this.sinon.spy() this.target1 .addEventListener('drag-start', evt => evt.preventDefault()) this.target1.addEventListener('drag-end', evt => evt.preventDefault()) this.target1.addEventListener('drag-end', dragEndSpy) this.sh1.onHit({ detail: { el: this.target1 } }) this.sh1.onDragDropStartButton() this.sh1.onDragDropEndButton({}) assert.isTrue(dragEndSpy.calledWithMatch({ detail: { hand: this.hand1 } })) assert.isNotOk(this.sh1.state.get(this.sh1.DRAG_EVENT)) }) test('drag events', function () { const emitSpy1 = this.sinon.spy(this.sh1, 'emitCancelable') const dragOverSpy1 = emitSpy1.withArgs(this.target1, 'dragover-start') const unDragOverSpy1 = emitSpy1.withArgs(this.target1, 'dragover-end') const dragDropSpy1 = emitSpy1.withArgs(this.target1, 'drag-drop') const dragOverSpy2 = emitSpy1.withArgs(this.target2, 'dragover-start') const unDragOverSpy2 = emitSpy1.withArgs(this.target2, 'dragover-end') const dragDropSpy2 = emitSpy1.withArgs(this.target2, 'drag-drop') this.target1.addEventListener('drag-start', e => e.preventDefault()) this.target1.addEventListener('dragover-start', e => e.preventDefault()) this.target2.addEventListener('dragover-start', e => e.preventDefault()) this.target2.addEventListener('drag-drop', e => e.preventDefault()) this.sh1.onHit({ detail: { el: this.target1 } }) this.sh1.onDragDropStartButton() assert.strictEqual(this.sh1.state.get(this.sh1.DRAG_EVENT), this.target1) assert.isFalse(dragOverSpy1.called, 'dragover-start not emitted with no droptarget') assert.isFalse(dragOverSpy2.called, 'dragover-start not emitted with no droptarget') this.sh1.onHit({ detail: { el: this.target2 } }) assert.notEqual(this.sh1.hoverEls.indexOf(this.target2), -1, 'droptaret added to hoverEls') assert.isTrue(dragOverSpy1.called, 'dragover-start emitted from held') assert.isTrue(dragOverSpy2.called, 'dragover-start emitted from hovered') helpers.simCollisionEnd(this.hand1, this.target2) assert.isTrue(unDragOverSpy1.called, 'drag-over ends with unhover: held') assert.isTrue(unDragOverSpy2.called, 'drag-over ends with unhover: hovered') this.sh1.onHit({ detail: { el: this.target2 } }) assert.isFalse(dragDropSpy1.called, 'drag-drop not yet emitted from held') assert.isFalse(dragDropSpy2.called, 'drag-drop not yet emitted form hovered') this.sh1.onDragDropEndButton() assert.isTrue(unDragOverSpy1.calledTwice, 'drag-over ends with drag-drop: held') assert.isTrue(unDragOverSpy2.calledTwice, 'drag-over ends with drag-drop: hovered') assert.isTrue(dragDropSpy1.called, 'drag-drop emitted from held') assert.isTrue(dragDropSpy2.called, 'drag-drop emitted form hovered') }) test('dragover rejected', function () { const dragoverSpy = this.sinon.spy(this.sh1, 'emitCancelable') this.sh1.onHit({ detail: { el: this.target1 } }) this.sh1.onHit({ detail: { el: this.target2 } }) this.sh1.onDragDropStartButton() assert.isFalse(dragoverSpy.calledWith(this.target1, 'dragover-start')) }) test('drag-drop rejected', function () { const dragoverSpy = this.sinon.spy(this.sh1, 'emitCancelable') this.sh1.onHit({ detail: { el: this.target1 } }) this.sh1.onHit({ detail: { el: this.target2 } }) this.sh1.onDragDropStartButton() this.sh1.onDragDropEndButton() assert.isFalse(dragoverSpy.calledWith(this.target1, 'drag-drop')) }) test('all gestures at once capture same entity', function () { this.target1.addEventListener('grab-start', e => e.preventDefault()) this.target1.addEventListener('stretch-start', e => e.preventDefault()) this.target1.addEventListener('drag-start', e => e.preventDefault()) this.sh1.onHit({ detail: { el: this.target1 } }) this.sh1.onGrabStartButton() this.sh1.onDragDropStartButton() this.sh1.onStretchStartButton() assert.equal(this.sh1.hoverEls.length, 1) assert.notOk(this.sh1.lastHover) assert.strictEqual( this.sh1.state.get(this.sh1.GRAB_EVENT), this.sh1.state.get(this.sh1.DRAG_EVENT) ) assert.strictEqual( this.sh1.state.get(this.sh1.DRAG_EVENT), this.sh1.state.get(this.sh1.STRETCH_EVENT) ) }) test('drag after grab uses carried entity', function () { this.target3 = document.createElement('a-entity') this.target1.sceneEl.appendChild(this.target3) this.target1.addEventListener('grab-start', e => e.preventDefault()) this.target1.addEventListener('drag-start', e => e.preventDefault()) this.target3.addEventListener('drag-start', e => e.preventDefault()) this.sh1.onHit({ detail: { el: this.target3 } }) this.sh1.onHit({ detail: { el: this.target1 } }) this.sh1.onGrabStartButton() this.sh1.onDragDropStartButton() this.sh1.onHit({ detail: { el: this.target2 } }) assert.strictEqual( this.sh1.state.get(this.sh1.GRAB_EVENT), this.sh1.state.get(this.sh1.DRAG_EVENT) ) assert.sameMembers( this.sh1.hoverEls, [this.target2, this.target1, this.target3] ) }) test('pressing button after collision', function () { const grabSpy = sinon.spy() const stretchSpy = sinon.spy() const dragSpy = sinon.spy() this.target1.addEventListener('grab-start', grabSpy) this.target1.addEventListener('stretch-start', stretchSpy) this.target1.addEventListener('drag-start', dragSpy) this.sh1.onHit({ detail: { el: this.target1 } }) assert.isFalse(grabSpy.called, 'before grab button') this.sh1.onGrabStartButton() assert.isTrue(grabSpy.called, 'after grab button') assert.isFalse(stretchSpy.called, 'before stretch button') this.sh1.onStretchStartButton() assert.isTrue(stretchSpy.called, 'after stretch button') assert.isFalse(dragSpy.called, 'before drag button') this.sh1.onDragDropStartButton() assert.isTrue(dragSpy.called, 'after drag button') }) test('collision after button push doesnt trigger', function () { this.sh1.onGrabStartButton() this.sh1.onHit({ detail: { el: this.target1 } }) this.target1.addEventListener('grab-start', evt => { evt.preventDefault() }) assert.equal(this.sh1.state.get(this.sh1.GRAB_EVENT), undefined) }) test('button events pass through', function () { const testSpy = this.sinon.spy(assert, 'equal') this.hand1.setAttribute('super-hands', { grabStartButtons: ['triggerdown'], grabEndButtons: ['triggerup'], stretchStartButtons: ['trackpaddown'], stretchEndButtons: ['trackpadup'], dragDropStartButtons: ['gripdown'], dragDropEndButtons: ['gripup'] }) this.target1.addEventListener('grab-start', e => { assert.equal(e.detail.buttonEvent.type, 'triggerdown', 'grab start') e.preventDefault() }) this.target1.addEventListener('stretch-start', e => { assert.equal(e.detail.buttonEvent.type, 'trackpaddown', 'stretch start') e.preventDefault() }) this.target1.addEventListener('drag-start', e => { assert.equal(e.detail.buttonEvent.type, 'gripdown', 'drag start') e.preventDefault() }) this.target1.addEventListener('grab-end', e => { assert.equal(e.detail.buttonEvent.type, 'triggerup', 'grab end') }, { once: true }) // avoid running this test during cleanup this.target1.addEventListener('stretch-end', e => { assert.equal(e.detail.buttonEvent.type, 'trackpadup', 'stretch end') }, { once: true }) this.target1.addEventListener('drag-end', e => { assert.equal(e.detail.buttonEvent.type, 'gripup', 'drag end') }, { once: true }) this.target2.addEventListener('drag-drop', e => { assert.equal(e.detail.buttonEvent.type, 'gripup', 'dragdrop') e.preventDefault() }) this.sh1.onHit({ detail: { el: this.target1 } }) this.sh1.onHit({ detail: { el: this.target2 } }) this.hand1.emit('triggerdown', {}) this.hand1.emit('trackpaddown', {}) this.hand1.emit('gripdown', {}) this.hand1.emit('triggerup', {}) this.hand1.emit('trackpadup', {}) this.hand1.emit('gripup', {}) // simpler than async test in this case - just count the tests assert.strictEqual(testSpy.callCount, 7) }) test('end event rejection', function () { this.target1.addEventListener('grab-start', e => e.preventDefault()) this.target1.addEventListener('stretch-start', e => e.preventDefault()) this.target1.addEventListener('drag-start', e => e.preventDefault()) this.sh1.onHit({ detail: { el: this.target1 } }) assert.notStrictEqual(this.sh1.state.get('grab-start'), this.target1) assert.notStrictEqual(this.sh1.state.get('drag-start'), this.target1) assert.notStrictEqual(this.sh1.state.get('stretch-start'), this.target1) this.hand1.emit('triggerdown', {}) assert.strictEqual(this.sh1.state.get('grab-start'), this.target1) assert.strictEqual(this.sh1.state.get('drag-start'), this.target1) assert.strictEqual(this.sh1.state.get('stretch-start'), this.target1) this.hand1.emit('triggerup', {}) assert.strictEqual(this.sh1.state.get('grab-start'), this.target1) assert.strictEqual(this.sh1.state.get('drag-start'), this.target1) assert.strictEqual(this.sh1.state.get('stretch-start'), this.target1) }) test('removal cleanup: hover', function (done) { const hoverEndSpy = this.sinon.spy() this.target1.addEventListener('hover-end', hoverEndSpy) this.target1.addEventListener('hover-start', e => e.preventDefault()) this.sh1.onHit({ detail: { el: this.target1 } }) assert.strictEqual(this.sh1.state.get(this.sh1.HOVER_EVENT), this.target1) assert.isFalse(hoverEndSpy.called) this.hand1.removeAttribute('super-hands') process.nextTick(() => { assert.isNotOk(this.hand1.getAttribute('super-hands')) assert.isTrue(hoverEndSpy.called) done() }) }) test('removal cleanup: grab', function (done) { const grabEndSpy = this.sinon.spy() this.target1.addEventListener('grab-end', grabEndSpy) this.target1.addEventListener('grab-start', e => e.preventDefault()) this.sh1.onHit({ detail: { el: this.target1 } }) this.sh1.onGrabStartButton() assert.strictEqual(this.sh1.state.get(this.sh1.GRAB_EVENT), this.target1) assert.isFalse(grabEndSpy.called) this.hand1.removeAttribute('super-hands') process.nextTick(() => { assert.isNotOk(this.hand1.getAttribute('super-hands')) assert.isTrue(grabEndSpy.called) done() }) }) }) suite('custom button mapping', function () { setup(function (done) { this.target1 = entityFactory() this.hand1 = helpers.controllerFactory({ 'super-hands': '' }) this.hand1.parentNode.addEventListener('loaded', () => { this.sh1 = this.hand1.components['super-hands'] done() }) }) test('default button mapping', function () { const grabSpy = this.sinon.spy() const stretchSpy = this.sinon.spy() const dragSpy = this.sinon.spy() this.target1.addEventListener('grab-start', grabSpy) this.target1.addEventListener('stretch-start', stretchSpy) this.target1.addEventListener('drag-start', dragSpy) this.sh1.onHit({ detail: { el: this.target1 } }) assert.isFalse(grabSpy.called) assert.isFalse(stretchSpy.called) assert.isFalse(dragSpy.called) this.hand1.emit('triggerdown', {}) assert.isTrue(grabSpy.called) assert.isTrue(stretchSpy.called) assert.isTrue(dragSpy.called) }) test('button mapping can be changed after init', function () { const grabSpy = this.sinon.spy() const stretchSpy = this.sinon.spy() const dragSpy = this.sinon.spy() this.target1.addEventListener('grab-start', grabSpy) this.target1.addEventListener('stretch-start', stretchSpy) this.target1.addEventListener('drag-start', dragSpy) this.hand1.setAttribute( 'super-hands', 'grabStartButtons: trackpaddown; grabEndButtons: trackpadup' ) this.sh1.onHit({ detail: { el: this.target1 } }) this.hand1.emit('triggerdown', {}) assert.isFalse(grabSpy.called) assert.isTrue(stretchSpy.called) assert.isTrue(dragSpy.called) }) }) suite('state tracking', function () { setup(function (done) { this.target1 = entityFactory() this.target1.id = 'target1' this.target2 = entityFactory() this.target2.id = 'target2' this.target3 = entityFactory() this.target3.id = 'target3' this.hand1 = helpers.controllerFactory({ 'super-hands': '' }) this.hand1.parentNode.addEventListener('loaded', () => { this.sh1 = this.hand1.components['super-hands'] done() }) }) test('hover els collected', function () { const targets = [this.target1, this.target2, this.target3] this.sh1.onHit({ detail: { el: this.target1 } }) this.sh1.onHit({ detail: { el: this.target2 } }) this.sh1.onHit({ detail: { el: this.target3 } }) assert.sameMembers(this.sh1.hoverEls, targets) for (let i = 0; i < targets.length; i++) { assert.strictEqual(this.sh1.hoverEls[i], targets[i], 'member ' + i) } }) test('hover els lifo for actions', function () { this.target1.addEventListener('grab-start', e => e.preventDefault()) this.target2.addEventListener('grab-start', e => e.preventDefault()) this.target3.addEventListener('grab-start', e => e.preventDefault()) this.sh1.onHit({ detail: { el: this.target1 } }) this.sh1.onHit({ detail: { el: this.target2 } }) this.sh1.onHit({ detail: { el: this.target3 } }) this.sh1.onGrabStartButton() assert.strictEqual(this.sh1.state.get(this.sh1.GRAB_EVENT), this.target3) }) test('hover els nearest-first for actions', function () { this.hand1.setAttribute('super-hands', { colliderEventProperty: 'els' }) this.target1.addEventListener('hover-start', e => e.preventDefault()) this.target2.addEventListener('hover-start', e => e.preventDefault()) this.target3.addEventListener('hover-start', e => e.preventDefault()) this.sh1.onHit({ detail: { els: [this.target1, this.target2], intersections: [ { distance: 1, object: { el: this.target1 } }, { distance: 2, object: { el: this.target2 } } ] } }) assert.strictEqual(this.sh1.state.get(this.sh1.HOVER_EVENT).id, 'target1') this.sh1.onHit({ detail: { els: [this.target3], intersections: [{ distance: 3 }] } }) assert.strictEqual(this.sh1.state.get(this.sh1.HOVER_EVENT).id, 'target1') this.sh1.unWatch({ detail: { el: this.target1 } }) this.sh1.unHover({ detail: { el: this.target1 } }) assert.strictEqual(this.sh1.state.get(this.sh1.HOVER_EVENT).id, 'target2') }) test('hover targeting updates as distances change', function () { let time = 10000 this.hand1.setAttribute('super-hands', { colliderEventProperty: 'els' }) this.target1.addEventListener('hover-start', e => e.preventDefault()) this.target2.addEventListener('hover-start', e => e.preventDefault()) this.target3.addEventListener('hover-start', e => e.preventDefault()) const target1Sect = { distance: 1, object: { el: this.target1 } } const target2Sect = { distance: 2, object: { el: this.target2 } } this.sh1.onHit({ detail: { els: [this.target1, this.target2], intersections: [target1Sect, target2Sect] } }) assert.strictEqual(this.sh1.state.get(this.sh1.HOVER_EVENT).id, 'target1') target2Sect.distance = 0.5 this.sh1.tick(time) assert.strictEqual(this.sh1.state.get(this.sh1.HOVER_EVENT).id, 'target2', 'closer') assert.sameOrderedMembers(this.sh1.hoverEls, [this.target1, this.target2], 'closer') target2Sect.distance = 3 target1Sect.distance = 2 time += 10000 this.sh1.tick(time) assert.strictEqual(this.sh1.state.get(this.sh1.HOVER_EVENT).id, 'target1', 'further') assert.sameOrderedMembers(this.sh1.hoverEls, [this.target2, this.target1], 'further') }) test('released el placed back at top of stack', function () { this.target1.addEventListener('grab-start', e => e.preventDefault()) this.target2.addEventListener('grab-start', e => e.preventDefault()) this.target1.addEventListener('grab-end', e => e.preventDefault()) this.target2.addEventListener('grab-end', e => e.preventDefault()) // this.target3.addEventListener('grab-start', e => e.preventDefault()); this.sh1.onHit({ detail: { el: this.target1 } }) this.sh1.onHit({ detail: { el: this.target2 } }) this.sh1.onHit({ detail: { el: this.target3 } }) this.sh1.onGrabStartButton() this.sh1.onGrabEndButton() const targets = [this.target1, this.target3, this.target2] assert.sameMembers(this.sh1.hoverEls, targets) for (let i = 0; i < targets.length; i++) { assert.strictEqual(this.sh1.hoverEls[i], targets[i], 'member ' + i) } this.sh1.onGrabStartButton() assert.strictEqual(this.sh1.state.get(this.sh1.GRAB_EVENT), this.target2) }) test('hover switches to new target', function () { const hoverSpy1 = this.sinon.spy(e => e.preventDefault()) const hoverEndSpy1 = this.sinon.spy() const hoverSpy2 = this.sinon.spy(e => e.preventDefault()) this.target1.addEventListener('hover-start', hoverSpy1) this.target1.addEventListener('hover-end', hoverEndSpy1) this.target2.addEventListener('hover-start', hoverSpy2) this.sh1.onHit({ detail: { el: this.target1 } }) assert.isTrue(hoverSpy1.called, '1st hover start') assert.isFalse(hoverEndSpy1.called) assert.isFalse(hoverSpy2.called) this.sh1.onHit({ detail: { el: this.target2 } }) assert.isTrue(hoverSpy2.called, '2nd hover start') assert.isTrue(hoverEndSpy1.called, '1st hover end') }) test('multiple collision start/end handling', function () { this.hand1.setAttribute('super-hands', { colliderEvent: 'collisions', colliderEventProperty: 'els', colliderEndEvent: 'collisions', colliderEndEventProperty: 'clearedEls' }) this.hand1.emit('collisions', { els: [this.target1, this.target2, this.target3] }) assert.sameMembers(this.sh1.hoverEls, [this.target1, this.target2, this.target3]) this.hand1.emit('collisions', { clearedEls: [this.target1, this.target3] }) assert.sameMembers(this.sh1.hoverEls, [this.target2]) this.hand1.emit('collisions', { els: [this.target1], clearedEls: [this.target2] }) assert.sameMembers(this.sh1.hoverEls, [this.target1]) }) })