Newer
Older
reroad-test / 2020-ryusei / aframe-master / tests / components / cursor.test.js
@ryusei ryusei on 22 Oct 2020 16 KB パノラマ表示
/* global assert, process, setup, suite, test, CustomEvent */
var entityFactory = require('../helpers').entityFactory;
var once = require('../helpers').once;

suite('cursor', function () {
  var cameraEl;
  var component;
  var el;
  var intersection;
  var intersectedEl;
  var prevIntersection;
  var prevIntersectedEl;

  setup(function (done) {
    cameraEl = entityFactory();
    el = document.createElement('a-entity');
    intersection = {distance: 10.5};
    intersectedEl = document.createElement('a-entity');
    prevIntersection = {distance: 12.5};
    prevIntersectedEl = document.createElement('a-entity');
    cameraEl.setAttribute('camera', 'active: true');
    el.setAttribute('cursor', '');
    // Wait for elements to load.
    el.addEventListener('componentinitialized', function (evt) {
      if (evt.detail.name !== 'cursor') { return; }
      component = el.components.cursor;
      done();
    });
    cameraEl.appendChild(el);
  });

  suite('init', function () {
    test('initializes raycaster as a dependency', function () {
      assert.ok(el.components.raycaster);
    });
  });

  suite('remove', function () {
    test('removes hover state', function (done) {
      el.emit('raycaster-intersection', {
        intersections: [intersection],
        els: [intersectedEl]
      });
      assert.ok(el.is('cursor-hovering'));
      assert.ok(intersectedEl.is('cursor-hovered'));
      el.removeAttribute('cursor');
      process.nextTick(function () {
        assert.notOk(el.is('cursor-hovering'));
        assert.notOk(intersectedEl.is('cursor-hovered'));
        done();
      });
    });

    test('removes fuse state', function (done) {
      el.setAttribute('cursor', {fuse: true});
      el.emit('raycaster-intersection', {
        intersections: [intersection],
        els: [intersectedEl]
      });
      assert.ok(el.is('cursor-fusing'));
      el.removeAttribute('cursor');
      process.nextTick(function () {
        assert.notOk(el.is('cursor-fusing'));
        done();
      });
    });

    test('removes intersection listener', function (done) {
      el.removeAttribute('cursor');
      process.nextTick(function () {
        el.emit('raycaster-intersection', {
          intersections: [intersection],
          els: [intersectedEl]
        });
        assert.notOk(el.is('cursor-hovering'));
        done();
      });
    });

    suite('update', function () {
      test('update mousemove event listeners when rayOrigin is the mouse', function () {
        var updateSpy = this.sinon.spy(el.components.cursor, 'update');
        var updateMouseEventListenersSpy = this.sinon.spy(el.components.cursor, 'updateMouseEventListeners');
        el.setAttribute('cursor', 'rayOrigin', 'mouse');
        assert.ok(updateSpy.called);
        assert.ok(updateMouseEventListenersSpy.called);
      });
    });
  });

  suite('onCursorDown', function () {
    test('emits mousedown event on el', function (done) {
      component.intersection = intersection;
      component.intersectedEl = intersectedEl;
      once(el, 'mousedown', function () {
        done();
      });
      component.onCursorDown({});
    });

    test('emits mousedown event on intersectedEl', function (done) {
      component.intersection = intersection;
      component.intersectedEl = intersectedEl;
      once(intersectedEl, 'mousedown', function () {
        done();
      });
      component.onCursorDown({});
    });

    test('sets cursorDownEl', function () {
      component.intersection = intersection;
      component.intersectedEl = intersectedEl;
      assert.notOk(component.cursorDownEl);
      component.onCursorDown({});
      assert.equal(component.cursorDownEl, intersectedEl);
    });
  });

  suite('onCursorUp', function () {
    test('emits mouseup event on el', function (done) {
      component.intersection = intersection;
      component.intersectedEl = intersectedEl;
      once(el, 'mouseup', function () {
        done();
      });
      component.isCursorDown = true;
      component.onCursorUp();
    });

    test('emits mouseup event on intersectedEl', function (done) {
      component.intersection = intersection;
      component.intersectedEl = intersectedEl;
      component.mouseDownEl = document.createElement('a-entity');
      once(intersectedEl, 'mouseup', function () {
        done();
      });
      component.isCursorDown = true;
      component.onCursorUp();
    });

    test('emits click event on el', function (done) {
      component.intersection = intersection;
      component.intersectedEl = intersectedEl;
      component.cursorDownEl = intersectedEl;
      once(el, 'click', function () {
        done();
      });
      component.isCursorDown = true;
      component.onCursorUp();
    });

    test('emits click event on intersectedEl', function (done) {
      component.intersection = intersection;
      component.intersectedEl = intersectedEl;
      component.cursorDownEl = intersectedEl;
      once(intersectedEl, 'click', function () {
        done();
      });
      component.isCursorDown = true;
      component.onCursorUp();
    });

    test('emits click event on intersectedEl when fuse and mouse cursor enabled', function (done) {
      el.setAttribute('cursor', 'fuse', true);
      el.setAttribute('cursor', 'rayOrigin', 'mouse');
      component.intersection = intersection;
      component.intersectedEl = intersectedEl;
      component.cursorDownEl = intersectedEl;
      once(intersectedEl, 'click', function () { done(); });
      component.isCursorDown = true;
      component.onCursorUp({type: 'touchend', preventDefault: function () {}});
    });
  });

  suite('onIntersection', function () {
    test('does not do anything if already intersecting', function () {
      el.emit('raycaster-intersection', {
        intersections: [intersection],
        els: [el]
      });
      assert.notOk(intersectedEl.is('cursor-hovered'));
    });

    test('does not do anything if only the cursor is intersecting', function () {
      el.emit('raycaster-intersection', {
        intersections: [intersection],
        els: [el]
      });
      assert.notOk(el.is('cursor-hovered'));
    });

    test('sets cursor-hovered state on intersectedEl', function () {
      el.emit('raycaster-intersection', {
        intersections: [intersection],
        els: [intersectedEl]
      });
      assert.ok(intersectedEl.is('cursor-hovered'));
    });

    test('emits mouseenter event on el', function (done) {
      once(el, 'mouseenter', function (evt) {
        assert.equal(evt.detail.intersectedEl, intersectedEl);
        done();
      });
      el.emit('raycaster-intersection', {
        intersections: [intersection],
        els: [intersectedEl]
      });
    });

    test('emits mouseenter event on intersectedEl', function (done) {
      once(intersectedEl, 'mouseenter', function (evt) {
        assert.equal(evt.detail.cursorEl, el);
        done();
      });
      el.emit('raycaster-intersection', {
        intersections: [intersection],
        els: [intersectedEl]
      });
    });

    test('emits mousenter event on intersectedEl, ignoring el intersection', function (done) {
      once(intersectedEl, 'mouseenter', function (evt) {
        assert.equal(evt.detail.cursorEl, el);
        done();
      });
      el.emit('raycaster-intersection', {
        intersections: [intersection, intersection],
        els: [el, intersectedEl]
      });
    });

    test('updates intersected entity when nearer intersection occurs', function (done, fail) {
      var intersection1 = {distance: 10.5};
      var intersection2 = {distance: 9.0};
      var intersection3 = {distance: 12.0};
      var nearerIntersectedEl = document.createElement('a-entity');
      var furtherIntersectedEl = document.createElement('a-entity');

      this.sinon.stub(el.components.raycaster, 'getIntersection', function (el) {
        switch (el) {
          case intersectedEl: return intersection1;
          case nearerIntersectedEl: return intersection2;
          case furtherIntersectedEl: return intersection3;
        }
      });

      intersectedEl.addEventListener('mouseenter', function onMouseenter (evt) {
        assert.shallowDeepEqual(evt.detail.intersection, intersection1);

        intersectedEl.removeEventListener('mouseenter', onMouseenter);
        intersectedEl.addEventListener('mouseenter', fail);
        el.addEventListener('mouseenter', fail);

        el.emit('raycaster-intersection', {
          intersections: [intersection2],
          els: [nearerIntersectedEl]
        });

        el.emit('raycaster-intersection', {
          intersections: [intersection3],
          els: [furtherIntersectedEl]
        });

        process.nextTick(function () {
          assert.equal(el.components.cursor.intersectedEl, nearerIntersectedEl);
          done();
        });
      });

      el.emit('raycaster-intersection', {
        intersections: [intersection],
        els: [intersectedEl]
      });
      assert.ok(el.is('cursor-hovering'));
    });

    test('emits a mouseleave event on the prevIntersectedEl', function (done) {
      once(prevIntersectedEl, 'mouseleave', function (evt) {
        done();
      });
      this.sinon.stub(el.components.raycaster, 'getIntersection', function (el) {
        return el === intersectedEl ? intersection : prevIntersection;
      });
      el.emit('raycaster-intersection', {
        intersections: [prevIntersection],
        els: [prevIntersectedEl]
      });
      el.emit('raycaster-intersection', {
        intersections: [intersection],
        els: [intersectedEl]
      });
    });

    test('does not set cursor-fusing state on cursor if not fuse', function () {
      el.emit('raycaster-intersection', {
        intersections: [intersection],
        els: [intersectedEl]
      });
      assert.notOk(el.is('cursor-fusing'));
    });

    test('sets cursor-fusing state on cursor if fuse', function () {
      el.setAttribute('cursor', 'fuse', true);
      el.emit('raycaster-intersection', {
        intersections: [intersection],
        els: [intersectedEl]
      });
      assert.ok(el.is('cursor-fusing'));
    });

    test('removes fuse state and emits event on fuse click', function (done) {
      el.setAttribute('cursor', {fuse: true, fuseTimeout: 1});
      el.emit('raycaster-intersection', {
        intersections: [intersection],
        els: [intersectedEl]
      });
      once(el, 'click', function () {
        assert.notOk(el.is('cursor-fusing'));
        done();
      });
    });

    test('emits event on intersectedEl on fuse click', function (done) {
      el.setAttribute('cursor', {fuse: true, fuseTimeout: 1});
      el.emit('raycaster-intersection', {
        intersections: [intersection],
        els: [intersectedEl]
      });
      once(intersectedEl, 'click', function () {
        done();
      });
    });
  });

  suite('onIntersectionCleared', function () {
    test('does not do anything if not intersecting', function () {
      el.emit('raycaster-intersection-cleared', {clearedEls: [intersectedEl]});
    });

    test('does not do anything if only the cursor is intersecting', function () {
      component.intersectedEl = intersectedEl;
      el.emit('raycaster-intersection-cleared', {clearedEls: [el]});
      assert.ok(component.intersectedEl);
    });

    test('unsets intersectedEl', function () {
      component.intersectedEl = intersectedEl;
      el.emit('raycaster-intersection-cleared', {clearedEls: [intersectedEl]});
      assert.notOk(component.intersectedEl);
    });

    test('removes cursor-hovered state on intersectedEl', function () {
      component.intersectedEl = intersectedEl;
      intersectedEl.addState('cursor-hovered');
      el.emit('raycaster-intersection-cleared', {clearedEls: [intersectedEl]});
      assert.notOk(intersectedEl.is('cursor-hovered'));
    });

    test('emits mouseleave event on el', function (done) {
      component.intersectedEl = intersectedEl;
      once(el, 'mouseleave', function (evt) {
        assert.equal(evt.detail.intersectedEl, intersectedEl);
        done();
      });
      el.emit('raycaster-intersection-cleared', {clearedEls: [intersectedEl]});
    });

    test('emits mouseleave event on intersectedEl', function (done) {
      component.intersectedEl = intersectedEl;
      once(intersectedEl, 'mouseleave', function (evt) {
        assert.equal(evt.detail.cursorEl, el);
        done();
      });
      el.emit('raycaster-intersection-cleared', {clearedEls: [intersectedEl]});
    });

    test('removes cursor-hovering and cursor-fusing states on cursor', function () {
      component.intersectedEl = intersectedEl;
      el.addState('cursor-fusing');
      el.addState('cursor-hovering');
      el.emit('raycaster-intersection-cleared', {clearedEls: [intersectedEl]});
      assert.notOk(el.is('cursor-fusing'));
      assert.notOk(el.is('cursor-hovering'));
    });

    test('sets another intersected element if any', function () {
      var dummyEl = document.createElement('a-entity');
      var dummyIntersection = {object: {el: dummyEl}};
      component.intersectedEl = intersectedEl;
      el.addState('cursor-fusing');
      el.addState('cursor-hovering');
      el.components.raycaster.intersections = [dummyIntersection];
      el.emit('raycaster-intersection-cleared', {clearedEls: [intersectedEl]});
      assert.notOk(el.is('cursor-fusing'));
      assert.ok(el.is('cursor-hovering'));
      assert.equal(dummyEl, el.components.cursor.intersectedEl);
    });
  });

  suite('onMouseMove', function () {
    test('update raycaster based on mouse coordinates', function (done) {
      var event = new CustomEvent('mousemove');
      event.clientX = 5;
      event.clientY = 5;
      el.setAttribute('cursor', 'rayOrigin', 'mouse');
      el.sceneEl.canvas.dispatchEvent(event);
      process.nextTick(function () {
        var raycaster = el.getAttribute('raycaster');
        assert.notEqual(raycaster.direction.x, 0);
        done();
      });
    });

    test('update raycaster based on touch coordinates', function (done) {
      var event = new CustomEvent('touchstart');
      event.touches = {item: function () { return {clientX: 5, clientY: 5}; }};
      el.setAttribute('cursor', 'rayOrigin', 'mouse');
      el.sceneEl.canvas.dispatchEvent(event);
      process.nextTick(function () {
        var raycaster = el.getAttribute('raycaster');
        assert.notEqual(raycaster.direction.x, 0);
        done();
      });
    });

    test('casts ray at current touch location', function (done) {
      var event = new CustomEvent('touchstart');
      var target = el.sceneEl.appendChild(document.createElement('a-entity'));
      var mouseDownSpy = this.sinon.spy();
      el.addEventListener('mousedown', mouseDownSpy);
      el.setAttribute('cursor', 'rayOrigin', 'mouse');
      target.setAttribute('geometry', '');
      target.setAttribute('position', '0 0 -5');
      target.addEventListener('loaded', function () {
        target.object3D.updateMatrixWorld();
        el.components.raycaster.refreshObjects();
        el.components.raycaster.tick();
        assert.strictEqual(component.intersectedEl, target);
        event.touches = {item: function () { return {clientX: 5, clientY: 5}; }};
        el.sceneEl.canvas.dispatchEvent(event);
        assert.isFalse(mouseDownSpy.calledWithMatch({detail: {intersectedEl: target}}));
        done();
      });
    });
  });

  suite('canvas events', function () {
    test('cursor responds to mouse events on canvas', function () {
      // Cannot spy on onCursorDown/Up directly due to binding.
      var cursorEmitSpy = this.sinon.spy(component, 'twoWayEmit');
      var downEvt = new CustomEvent('mousedown');
      var upEvt = new CustomEvent('mouseup');
      assert.isFalse(cursorEmitSpy.calledWith('mousedown'));
      el.sceneEl.canvas.dispatchEvent(downEvt);
      assert.isTrue(cursorEmitSpy.calledWith('mousedown'));
      assert.isFalse(cursorEmitSpy.calledWith('mouseup'));
      el.sceneEl.canvas.dispatchEvent(upEvt);
      assert.isTrue(cursorEmitSpy.calledWith('mouseup'));
    });

    test('cursor responds to touch events on canvas', function () {
      // Cannot spy on onCursorDown/Up directly due to binding.
      var cursorEmitSpy = this.sinon.spy(component, 'twoWayEmit');
      var downEvt = new CustomEvent('touchstart');
      downEvt.touches = [];
      var upEvt = new CustomEvent('touchend');
      upEvt.touches = [];
      assert.isFalse(cursorEmitSpy.calledWith('mousedown'));
      el.sceneEl.canvas.dispatchEvent(downEvt);
      assert.isTrue(cursorEmitSpy.calledWith('mousedown'));
      assert.isFalse(cursorEmitSpy.calledWith('mouseup'));
      el.sceneEl.canvas.dispatchEvent(upEvt);
      assert.isTrue(cursorEmitSpy.calledWith('mouseup'));
    });
  });
});

suite('cursor + raycaster', function () {
  test('can use HTML-configured raycaster', function (done) {
    var parentEl = entityFactory();
    parentEl.addEventListener('child-attached', function (evt) {
      var el = evt.detail.el;
      el.addEventListener('loaded', function () {
        assert.equal(el.components.raycaster.data.objects, '.clickable');
        done();
      });
    });
    parentEl.innerHTML = '<a-entity cursor raycaster="objects: .clickable"></a-entity>';
  });
});