Newer
Older
pixi.js / test / interaction / InteractionManager.js
@Matt Karl Matt Karl on 12 Apr 2017 49 KB 4.5.0
'use strict';

const MockPointer = require('./MockPointer');

describe('PIXI.interaction.InteractionManager', function ()
{
    afterEach(function ()
    {
        // if we made a MockPointer for the test, clean it up
        if (this.pointer)
        {
            this.pointer.cleanUp();
            this.pointer = null;
        }
    });

    describe('event basics', function ()
    {
        it('should call mousedown handler', function ()
        {
            const stage = new PIXI.Container();
            const graphics = new PIXI.Graphics();
            const eventSpy = sinon.spy();
            const pointer = this.pointer = new MockPointer(stage);

            stage.addChild(graphics);
            graphics.beginFill(0xFFFFFF);
            graphics.drawRect(0, 0, 50, 50);
            graphics.interactive = true;
            graphics.on('mousedown', eventSpy);

            pointer.mousedown(10, 10);

            expect(eventSpy).to.have.been.calledOnce;
        });

        it('should call mouseup handler', function ()
        {
            const stage = new PIXI.Container();
            const graphics = new PIXI.Graphics();
            const eventSpy = sinon.spy();
            const pointer = this.pointer = new MockPointer(stage);

            stage.addChild(graphics);
            graphics.beginFill(0xFFFFFF);
            graphics.drawRect(0, 0, 50, 50);
            graphics.interactive = true;
            graphics.on('mouseup', eventSpy);

            pointer.click(10, 10);

            expect(eventSpy).to.have.been.called;
        });

        it('should call mouseupoutside handler', function ()
        {
            const stage = new PIXI.Container();
            const graphics = new PIXI.Graphics();
            const eventSpy = sinon.spy();
            const pointer = this.pointer = new MockPointer(stage);

            stage.addChild(graphics);
            graphics.beginFill(0xFFFFFF);
            graphics.drawRect(0, 0, 50, 50);
            graphics.interactive = true;
            graphics.on('mouseupoutside', eventSpy);

            pointer.mousedown(10, 10);
            pointer.mouseup(60, 60);

            expect(eventSpy).to.have.been.called;
        });

        it('should call mouseover handler', function ()
        {
            const stage = new PIXI.Container();
            const graphics = new PIXI.Graphics();
            const eventSpy = sinon.spy();
            const pointer = this.pointer = new MockPointer(stage);

            stage.addChild(graphics);
            graphics.beginFill(0xFFFFFF);
            graphics.drawRect(0, 0, 50, 50);
            graphics.interactive = true;
            graphics.on('mouseover', eventSpy);

            pointer.mousemove(10, 10);

            expect(eventSpy).to.have.been.called;
        });

        it('should call mouseout handler', function ()
        {
            const stage = new PIXI.Container();
            const graphics = new PIXI.Graphics();
            const eventSpy = sinon.spy();
            const pointer = this.pointer = new MockPointer(stage);

            stage.addChild(graphics);
            graphics.beginFill(0xFFFFFF);
            graphics.drawRect(0, 0, 50, 50);
            graphics.interactive = true;
            graphics.on('mouseout', eventSpy);

            pointer.mousemove(10, 10);
            pointer.mousemove(60, 60);

            expect(eventSpy).to.have.been.called;
        });
    });

    describe('add/remove events', function ()
    {
        let stub;

        before(function ()
        {
            stub = sinon.stub(PIXI.interaction.InteractionManager.prototype, 'setTargetElement');
        });

        after(function ()
        {
            stub.restore();
        });

        it('should add and remove pointer events to document', function ()
        {
            const manager = new PIXI.interaction.InteractionManager(sinon.stub());
            const addSpy = sinon.spy(window.document, 'addEventListener');
            const removeSpy = sinon.spy(window.document, 'removeEventListener');

            manager.interactionDOMElement = { style: {}, addEventListener: sinon.stub(), removeEventListener: sinon.stub() };
            manager.supportsPointerEvents = true;

            manager.addEvents();

            expect(addSpy).to.have.been.calledOnce;
            expect(addSpy).to.have.been.calledWith('pointermove');

            manager.removeEvents();

            expect(removeSpy).to.have.been.calledOnce;
            expect(removeSpy).to.have.been.calledWith('pointermove');

            addSpy.restore();
            removeSpy.restore();
        });

        it('should add and remove pointer events to window', function ()
        {
            const manager = new PIXI.interaction.InteractionManager(sinon.stub());
            const addSpy = sinon.spy(window, 'addEventListener');
            const removeSpy = sinon.spy(window, 'removeEventListener');

            manager.interactionDOMElement = { style: {}, addEventListener: sinon.stub(), removeEventListener: sinon.stub() };
            manager.supportsPointerEvents = true;

            manager.addEvents();

            expect(addSpy).to.have.been.calledTwice;
            expect(addSpy).to.have.been.calledWith('pointercancel');
            expect(addSpy).to.have.been.calledWith('pointerup');

            manager.removeEvents();

            expect(removeSpy).to.have.been.calledTwice;
            expect(removeSpy).to.have.been.calledWith('pointercancel');
            expect(removeSpy).to.have.been.calledWith('pointerup');

            addSpy.restore();
            removeSpy.restore();
        });

        it('should add and remove pointer events to element', function ()
        {
            const manager = new PIXI.interaction.InteractionManager(sinon.stub());
            const element = { style: {}, addEventListener: sinon.stub(), removeEventListener: sinon.stub() };

            manager.interactionDOMElement = element;
            manager.supportsPointerEvents = true;

            manager.addEvents();

            expect(element.addEventListener).to.have.been.calledThrice;
            expect(element.addEventListener).to.have.been.calledWith('pointerdown');
            expect(element.addEventListener).to.have.been.calledWith('pointerleave');
            expect(element.addEventListener).to.have.been.calledWith('pointerover');

            manager.removeEvents();

            expect(element.removeEventListener).to.have.been.calledThrice;
            expect(element.removeEventListener).to.have.been.calledWith('pointerdown');
            expect(element.removeEventListener).to.have.been.calledWith('pointerleave');
            expect(element.removeEventListener).to.have.been.calledWith('pointerover');
        });

        it('should add and remove mouse events to document', function ()
        {
            const manager = new PIXI.interaction.InteractionManager(sinon.stub());
            const addSpy = sinon.spy(window.document, 'addEventListener');
            const removeSpy = sinon.spy(window.document, 'removeEventListener');

            manager.interactionDOMElement = { style: {}, addEventListener: sinon.stub(), removeEventListener: sinon.stub() };
            manager.supportsPointerEvents = false;

            manager.addEvents();

            expect(addSpy).to.have.been.calledOnce;
            expect(addSpy).to.have.been.calledWith('mousemove');

            manager.removeEvents();

            expect(removeSpy).to.have.been.calledOnce;
            expect(removeSpy).to.have.been.calledWith('mousemove');

            addSpy.restore();
            removeSpy.restore();
        });

        it('should add and remove mouse events to window', function ()
        {
            const manager = new PIXI.interaction.InteractionManager(sinon.stub());
            const addSpy = sinon.spy(window, 'addEventListener');
            const removeSpy = sinon.spy(window, 'removeEventListener');

            manager.interactionDOMElement = { style: {}, addEventListener: sinon.stub(), removeEventListener: sinon.stub() };
            manager.supportsPointerEvents = false;

            manager.addEvents();

            expect(addSpy).to.have.been.calledOnce;
            expect(addSpy).to.have.been.calledWith('mouseup');

            manager.removeEvents();

            expect(removeSpy).to.have.been.calledOnce;
            expect(removeSpy).to.have.been.calledWith('mouseup');

            addSpy.restore();
            removeSpy.restore();
        });

        it('should add and remove mouse events to element', function ()
        {
            const manager = new PIXI.interaction.InteractionManager(sinon.stub());
            const element = { style: {}, addEventListener: sinon.stub(), removeEventListener: sinon.stub() };

            manager.interactionDOMElement = element;
            manager.supportsPointerEvents = false;
            manager.supportsTouchEvents = false;

            manager.addEvents();

            expect(element.addEventListener).to.have.been.calledThrice;
            expect(element.addEventListener).to.have.been.calledWith('mousedown');
            expect(element.addEventListener).to.have.been.calledWith('mouseout');
            expect(element.addEventListener).to.have.been.calledWith('mouseover');

            manager.removeEvents();

            expect(element.removeEventListener).to.have.been.calledThrice;
            expect(element.removeEventListener).to.have.been.calledWith('mousedown');
            expect(element.removeEventListener).to.have.been.calledWith('mouseout');
            expect(element.removeEventListener).to.have.been.calledWith('mouseover');
        });

        it('should add and remove touch events to element', function ()
        {
            const manager = new PIXI.interaction.InteractionManager(sinon.stub());
            const element = { style: {}, addEventListener: sinon.stub(), removeEventListener: sinon.stub() };

            manager.interactionDOMElement = element;
            manager.supportsPointerEvents = false;
            manager.supportsTouchEvents = true;

            manager.addEvents();

            expect(element.addEventListener).to.have.been.calledWith('touchstart');
            expect(element.addEventListener).to.have.been.calledWith('touchcancel');
            expect(element.addEventListener).to.have.been.calledWith('touchend');
            expect(element.addEventListener).to.have.been.calledWith('touchmove');

            manager.removeEvents();

            expect(element.removeEventListener).to.have.been.calledWith('touchstart');
            expect(element.removeEventListener).to.have.been.calledWith('touchcancel');
            expect(element.removeEventListener).to.have.been.calledWith('touchend');
            expect(element.removeEventListener).to.have.been.calledWith('touchmove');
        });
    });

    describe('onClick', function ()
    {
        it('should call handler when inside', function ()
        {
            const stage = new PIXI.Container();
            const graphics = new PIXI.Graphics();
            const clickSpy = sinon.spy();
            const pointer = this.pointer = new MockPointer(stage);

            stage.addChild(graphics);
            graphics.beginFill(0xFFFFFF);
            graphics.drawRect(0, 0, 50, 50);
            graphics.interactive = true;
            graphics.on('click', clickSpy);

            pointer.click(10, 10);

            expect(clickSpy).to.have.been.calledOnce;
        });

        it('should not call handler when outside', function ()
        {
            const stage = new PIXI.Container();
            const graphics = new PIXI.Graphics();
            const clickSpy = sinon.spy();
            const pointer = this.pointer = new MockPointer(stage);

            stage.addChild(graphics);
            graphics.beginFill(0xFFFFFF);
            graphics.drawRect(0, 0, 50, 50);
            graphics.interactive = true;
            graphics.on('click', clickSpy);

            pointer.click(60, 60);

            expect(clickSpy).to.not.have.been.called;
        });

        it('should not call handler when mousedown not received', function ()
        {
            const stage = new PIXI.Container();
            const graphics = new PIXI.Graphics();
            const clickSpy = sinon.spy();
            const pointer = new MockPointer(stage);

            stage.addChild(graphics);
            graphics.beginFill(0xFFFFFF);
            graphics.drawRect(0, 0, 50, 50);
            graphics.interactive = true;
            graphics.on('click', clickSpy);

            pointer.mouseup(10, 10);

            expect(clickSpy, 'click should not happen on first mouseup').to.not.have.been.called;

            // test again, just because it was a bug that was reported
            pointer.mouseup(20, 20);

            expect(clickSpy, 'click should not happen on second mouseup').to.not.have.been.called;
        });
    });

    describe('onTap', function ()
    {
        it('should call handler when inside', function ()
        {
            const stage = new PIXI.Container();
            const graphics = new PIXI.Graphics();
            const clickSpy = sinon.spy();
            const pointer = this.pointer = new MockPointer(stage);

            stage.addChild(graphics);
            graphics.beginFill(0xFFFFFF);
            graphics.drawRect(0, 0, 50, 50);
            graphics.interactive = true;
            graphics.on('tap', clickSpy);

            pointer.tap(10, 10);

            expect(clickSpy).to.have.been.calledOnce;
        });

        it('should not call handler when outside', function ()
        {
            const stage = new PIXI.Container();
            const graphics = new PIXI.Graphics();
            const clickSpy = sinon.spy();
            const pointer = this.pointer = new MockPointer(stage);

            stage.addChild(graphics);
            graphics.beginFill(0xFFFFFF);
            graphics.drawRect(0, 0, 50, 50);
            graphics.interactive = true;
            graphics.on('tap', clickSpy);

            pointer.tap(60, 60);

            expect(clickSpy).to.not.have.been.called;
        });
    });

    describe('overlapping children', function ()
    {
        function getScene(callbackEventName, splitParents)
        {
            const behindChild = new PIXI.Graphics();
            const frontChild = new PIXI.Graphics();
            const parent = new PIXI.Container();
            const behindChildCallback = sinon.spy(function behindSpy() { /* no op*/ });
            const frontChildCallback = sinon.spy(function frontSpy() { /* no op*/ });
            const parentCallback = sinon.spy(function parentSpy() { /* no op*/ });
            let behindParent;
            let frontParent;
            let behindParentCallback;
            let frontParentCallback;

            behindChild.beginFill(0xFF);
            behindChild.drawRect(0, 0, 50, 50);
            behindChild.on(callbackEventName, behindChildCallback);
            behindChild.name = 'behind';

            frontChild.beginFill(0x00FF);
            frontChild.drawRect(0, 0, 50, 50);
            frontChild.on(callbackEventName, frontChildCallback);
            frontChild.name = 'front';

            if (splitParents)
            {
                behindParent = new PIXI.Container();
                behindParent.name = 'behindParent';
                frontParent = new PIXI.Container();
                frontParent.name = 'frontParent';
                behindParentCallback = sinon.spy(function behindParentSpy() { /* no op*/ });
                frontParentCallback = sinon.spy(function frontParentSpy() { /* no op*/ });
                behindParent.on(callbackEventName, behindParentCallback);
                frontParent.on(callbackEventName, frontParentCallback);

                parent.addChild(behindParent, frontParent);
                behindParent.addChild(behindChild);
                frontParent.addChild(frontChild);

                parent.name = 'parent';
            }
            else
            {
                parent.addChild(behindChild, frontChild);
            }
            parent.on(callbackEventName, parentCallback);

            return {
                behindChild,
                frontChild,
                behindParent,
                frontParent,
                parent,
                behindChildCallback,
                frontChildCallback,
                behindParentCallback,
                frontParentCallback,
                parentCallback,
            };
        }

        describe('when parent is non-interactive', function ()
        {
            describe('when both children are interactive', function ()
            {
                it('should callback front child when clicking front child', function ()
                {
                    const stage = new PIXI.Container();
                    const pointer = this.pointer = new MockPointer(stage);
                    const scene = getScene('click');

                    scene.behindChild.interactive = true;
                    scene.behindChild.x = 25;
                    scene.frontChild.interactive = true;

                    stage.addChild(scene.parent);
                    pointer.click(10, 10);

                    expect(scene.behindChildCallback).to.not.have.been.called;
                    expect(scene.frontChildCallback).to.have.been.calledOnce;
                    expect(scene.parentCallback).to.not.have.been.called;
                });

                it('should callback front child when clicking overlap', function ()
                {
                    const stage = new PIXI.Container();
                    const pointer = this.pointer = new MockPointer(stage);
                    const scene = getScene('click');

                    scene.behindChild.interactive = true;
                    scene.behindChild.x = 25;
                    scene.frontChild.interactive = true;

                    stage.addChild(scene.parent);
                    pointer.click(40, 10);

                    expect(scene.behindChildCallback).to.not.have.been.called;
                    expect(scene.frontChildCallback).to.have.been.calledOnce;
                    expect(scene.parentCallback).to.not.have.been.called;
                });

                it('should callback behind child when clicking behind child', function ()
                {
                    const stage = new PIXI.Container();
                    const pointer = this.pointer = new MockPointer(stage);
                    const scene = getScene('click');

                    scene.behindChild.interactive = true;
                    scene.behindChild.x = 25;
                    scene.frontChild.interactive = true;

                    stage.addChild(scene.parent);
                    pointer.click(60, 10);

                    expect(scene.frontChildCallback).to.not.have.been.called;
                    expect(scene.behindChildCallback).to.have.been.calledOnce;
                    expect(scene.parentCallback).to.not.have.been.called;
                });

                it('should callback front child of different non-interactive parents when clicking overlap', function ()
                {
                    const stage = new PIXI.Container();
                    const pointer = this.pointer = new MockPointer(stage);
                    const scene = getScene('click', true);

                    scene.behindChild.interactive = true;
                    scene.behindChild.x = 25;
                    scene.frontChild.interactive = true;

                    stage.addChild(scene.parent);
                    pointer.click(40, 10);

                    expect(scene.behindChildCallback).to.not.have.been.called;
                    expect(scene.frontChildCallback).to.have.been.calledOnce;
                    expect(scene.parentCallback).to.not.have.been.called;
                    expect(scene.behindParentCallback).to.not.have.been.called;
                    expect(scene.frontParentCallback).to.not.have.been.called;
                });

                it('should callback front child of different interactive parents when clicking overlap', function ()
                {
                    const stage = new PIXI.Container();
                    const pointer = this.pointer = new MockPointer(stage);
                    const scene = getScene('click', true);

                    scene.behindChild.interactive = true;
                    scene.behindChild.x = 25;
                    scene.frontChild.interactive = true;
                    scene.behindParent.interactive = true;
                    scene.frontParent.interactive = true;

                    stage.addChild(scene.parent);
                    pointer.click(40, 10);

                    expect(scene.behindChildCallback).to.not.have.been.called;
                    expect(scene.frontChildCallback).to.have.been.calledOnce;
                    expect(scene.parentCallback).to.not.have.been.called;
                    expect(scene.behindParentCallback).to.not.have.been.called;
                    expect(scene.frontParentCallback).to.have.been.calledOnce;
                });
            });

            describe('when front child is non-interactive', function ()
            {
                it('should not callback when clicking front child', function ()
                {
                    const stage = new PIXI.Container();
                    const pointer = this.pointer = new MockPointer(stage);
                    const scene = getScene('click');

                    scene.behindChild.interactive = true;
                    scene.behindChild.x = 25;

                    stage.addChild(scene.parent);
                    pointer.click(10, 10);

                    expect(scene.behindChildCallback).to.not.have.been.called;
                    expect(scene.frontChildCallback).to.not.have.been.called;
                    expect(scene.parentCallback).to.not.have.been.called;
                });

                it('should callback behind child when clicking overlap', function ()
                {
                    const stage = new PIXI.Container();
                    const pointer = this.pointer = new MockPointer(stage);
                    const scene = getScene('click');

                    scene.behindChild.interactive = true;
                    scene.behindChild.x = 25;

                    stage.addChild(scene.parent);
                    pointer.click(40, 10);

                    expect(scene.behindChildCallback).to.have.been.calledOnce;
                    expect(scene.frontChildCallback).to.not.have.been.called;
                    expect(scene.parentCallback).to.not.have.been.called;
                });

                it('should callback behind child when clicking behind child', function ()
                {
                    const stage = new PIXI.Container();
                    const pointer = this.pointer = new MockPointer(stage);
                    const scene = getScene('click');

                    scene.behindChild.interactive = true;
                    scene.behindChild.x = 25;

                    stage.addChild(scene.parent);
                    pointer.click(60, 10);

                    expect(scene.frontChildCallback).to.not.have.been.called;
                    expect(scene.behindChildCallback).to.have.been.calledOnce;
                    expect(scene.parentCallback).to.not.have.been.called;
                });
            });

            describe('when behind child is non-interactive', function ()
            {
                it('should callback front child when clicking front child', function ()
                {
                    const stage = new PIXI.Container();
                    const pointer = this.pointer = new MockPointer(stage);
                    const scene = getScene('click');

                    scene.behindChild.x = 25;
                    scene.frontChild.interactive = true;

                    stage.addChild(scene.parent);
                    pointer.click(10, 10);

                    expect(scene.behindChildCallback).to.not.have.been.called;
                    expect(scene.frontChildCallback).to.have.been.calledOnce;
                    expect(scene.parentCallback).to.not.have.been.called;
                });

                it('should callback front child when clicking overlap', function ()
                {
                    const stage = new PIXI.Container();
                    const pointer = this.pointer = new MockPointer(stage);
                    const scene = getScene('click');

                    scene.behindChild.x = 25;
                    scene.frontChild.interactive = true;

                    stage.addChild(scene.parent);
                    pointer.click(40, 10);

                    expect(scene.behindChildCallback).to.not.have.been.called;
                    expect(scene.frontChildCallback).to.have.been.calledOnce;
                    expect(scene.parentCallback).to.not.have.been.called;
                });

                it('should not callback when clicking behind child', function ()
                {
                    const stage = new PIXI.Container();
                    const pointer = this.pointer = new MockPointer(stage);
                    const scene = getScene('click');

                    scene.behindChild.x = 25;
                    scene.frontChild.interactive = true;

                    stage.addChild(scene.parent);
                    pointer.click(60, 10);

                    expect(scene.frontChildCallback).to.not.have.been.called;
                    expect(scene.behindChildCallback).to.not.have.been.called;
                    expect(scene.parentCallback).to.not.have.been.called;
                });
            });
        });

        describe('when parent is interactive', function ()
        {
            describe('when both children are interactive', function ()
            {
                it('should callback parent and front child when clicking front child', function ()
                {
                    const stage = new PIXI.Container();
                    const pointer = this.pointer = new MockPointer(stage);
                    const scene = getScene('click');

                    scene.behindChild.interactive = true;
                    scene.behindChild.x = 25;
                    scene.frontChild.interactive = true;
                    scene.parent.interactive = true;

                    stage.addChild(scene.parent);
                    pointer.click(10, 10);

                    expect(scene.behindChildCallback).to.not.have.been.called;
                    expect(scene.frontChildCallback).to.have.been.calledOnce;
                    expect(scene.parentCallback).to.have.been.calledOnce;
                });

                it('should callback parent and front child when clicking overlap', function ()
                {
                    const stage = new PIXI.Container();
                    const pointer = this.pointer = new MockPointer(stage);
                    const scene = getScene('click');

                    scene.behindChild.interactive = true;
                    scene.behindChild.x = 25;
                    scene.frontChild.interactive = true;
                    scene.parent.interactive = true;

                    stage.addChild(scene.parent);
                    pointer.click(40, 10);

                    expect(scene.behindChildCallback).to.not.have.been.called;
                    expect(scene.frontChildCallback).to.have.been.calledOnce;
                    expect(scene.parentCallback).to.have.been.calledOnce;
                });

                it('should callback parent and behind child when clicking behind child', function ()
                {
                    const stage = new PIXI.Container();
                    const pointer = this.pointer = new MockPointer(stage);
                    const scene = getScene('click');

                    scene.behindChild.interactive = true;
                    scene.behindChild.x = 25;
                    scene.frontChild.interactive = true;
                    scene.parent.interactive = true;

                    stage.addChild(scene.parent);
                    pointer.click(60, 10);

                    expect(scene.frontChildCallback).to.not.have.been.called;
                    expect(scene.behindChildCallback).to.have.been.calledOnce;
                    expect(scene.parentCallback).to.have.been.calledOnce;
                });

                it('should callback front child of different non-interactive parents when clicking overlap', function ()
                {
                    const stage = new PIXI.Container();
                    const pointer = this.pointer = new MockPointer(stage);
                    const scene = getScene('click', true);

                    scene.behindChild.interactive = true;
                    scene.behindChild.x = 25;
                    scene.frontChild.interactive = true;
                    scene.parent.interactive = true;

                    stage.addChild(scene.parent);
                    pointer.click(40, 10);

                    expect(scene.behindChildCallback).to.not.have.been.called;
                    expect(scene.frontChildCallback).to.have.been.calledOnce;
                    expect(scene.parentCallback).to.have.been.calledOnce;
                    expect(scene.behindParentCallback).to.not.have.been.called;
                    expect(scene.frontParentCallback).to.not.have.been.called;
                });

                it('should callback front child of different interactive parents when clicking overlap', function ()
                {
                    const stage = new PIXI.Container();
                    const pointer = this.pointer = new MockPointer(stage);
                    const scene = getScene('click', true);

                    scene.behindChild.interactive = true;
                    scene.behindChild.x = 25;
                    scene.frontChild.interactive = true;
                    scene.parent.interactive = true;
                    scene.behindParent.interactive = true;
                    scene.frontParent.interactive = true;

                    stage.addChild(scene.parent);
                    pointer.click(40, 10);

                    expect(scene.behindChildCallback).to.not.have.been.called;
                    expect(scene.frontChildCallback).to.have.been.calledOnce;
                    expect(scene.parentCallback).to.have.been.calledOnce;
                    expect(scene.behindParentCallback).to.not.have.been.called;
                    expect(scene.frontParentCallback).to.have.been.calledOnce;
                });
            });

            describe('when front child is non-interactive', function ()
            {
                it('should callback parent when clicking front child', function ()
                {
                    const stage = new PIXI.Container();
                    const pointer = this.pointer = new MockPointer(stage);
                    const scene = getScene('click');

                    scene.behindChild.interactive = true;
                    scene.behindChild.x = 25;
                    scene.parent.interactive = true;

                    stage.addChild(scene.parent);
                    pointer.click(10, 10);

                    expect(scene.behindChildCallback).to.not.have.been.called;
                    expect(scene.frontChildCallback).to.not.have.been.called;
                    expect(scene.parentCallback).to.have.been.calledOnce;
                });

                it('should callback parent and behind child when clicking overlap', function ()
                {
                    const stage = new PIXI.Container();
                    const pointer = this.pointer = new MockPointer(stage);
                    const scene = getScene('click');

                    scene.behindChild.interactive = true;
                    scene.behindChild.x = 25;
                    scene.parent.interactive = true;

                    stage.addChild(scene.parent);
                    pointer.click(40, 10);

                    expect(scene.behindChildCallback).to.have.been.calledOnce;
                    expect(scene.frontChildCallback).to.not.have.been.called;
                    expect(scene.parentCallback).to.have.been.calledOnce;
                });

                it('should callback parent and behind child when clicking behind child', function ()
                {
                    const stage = new PIXI.Container();
                    const pointer = this.pointer = new MockPointer(stage);
                    const scene = getScene('click');

                    scene.behindChild.interactive = true;
                    scene.behindChild.x = 25;
                    scene.parent.interactive = true;

                    stage.addChild(scene.parent);
                    pointer.click(60, 10);

                    expect(scene.frontChildCallback).to.not.have.been.called;
                    expect(scene.behindChildCallback).to.have.been.calledOnce;
                    expect(scene.parentCallback).to.have.been.calledOnce;
                });
            });

            describe('when behind child is non-interactive', function ()
            {
                it('should callback parent and front child when clicking front child', function ()
                {
                    const stage = new PIXI.Container();
                    const pointer = this.pointer = new MockPointer(stage);
                    const scene = getScene('click');

                    scene.behindChild.x = 25;
                    scene.frontChild.interactive = true;
                    scene.parent.interactive = true;

                    stage.addChild(scene.parent);
                    pointer.click(10, 10);

                    expect(scene.behindChildCallback).to.not.have.been.called;
                    expect(scene.frontChildCallback).to.have.been.calledOnce;
                    expect(scene.parentCallback).to.have.been.calledOnce;
                });

                it('should callback parent and front child when clicking overlap', function ()
                {
                    const stage = new PIXI.Container();
                    const pointer = this.pointer = new MockPointer(stage);
                    const scene = getScene('click');

                    scene.behindChild.x = 25;
                    scene.frontChild.interactive = true;
                    scene.parent.interactive = true;

                    stage.addChild(scene.parent);
                    pointer.click(40, 10);

                    expect(scene.behindChildCallback).to.not.have.been.called;
                    expect(scene.frontChildCallback).to.have.been.calledOnce;
                    expect(scene.parentCallback).to.have.been.calledOnce;
                });

                it('should callback parent when clicking behind child', function ()
                {
                    const stage = new PIXI.Container();
                    const pointer = this.pointer = new MockPointer(stage);
                    const scene = getScene('click');

                    scene.behindChild.x = 25;
                    scene.frontChild.interactive = true;
                    scene.parent.interactive = true;

                    stage.addChild(scene.parent);
                    pointer.click(60, 10);

                    expect(scene.frontChildCallback).to.not.have.been.called;
                    expect(scene.behindChildCallback).to.not.have.been.called;
                    expect(scene.parentCallback).to.have.been.calledOnce;
                });
            });
        });

        it('Semi-complicated nesting with overlap, should not call behind callback', function ()
        {
            const stage = new PIXI.Container();
            const frontParent = new PIXI.Container();
            const frontChild = new PIXI.Graphics();
            const behindParent = new PIXI.Container();
            const subParent = new PIXI.Container();
            const behindChild = new PIXI.Graphics();
            const behindCallback = sinon.spy(function behindSpy() { /* no op*/ });
            const frontCallback = sinon.spy(function frontSpy() { /* no op*/ });

            behindChild.beginFill(0xFF);
            behindChild.drawRect(0, 0, 50, 50);
            subParent.on('click', behindCallback);

            frontChild.beginFill(0x00FF);
            frontChild.drawRect(0, 0, 50, 50);
            frontParent.on('click', frontCallback);
            const pointer = this.pointer = new MockPointer(stage);

            behindParent.x = 25;
            subParent.interactive = true;
            frontParent.interactive = true;

            behindParent.addChild(subParent);
            subParent.addChild(behindChild);
            stage.addChild(behindParent);
            frontParent.addChild(frontChild);
            stage.addChild(frontParent);

            pointer.click(40, 10);

            expect(behindCallback).to.not.have.been.called;
            expect(frontCallback).to.have.been.calledOnce;
        });
    });

    describe('cursor changes', function ()
    {
        it('cursor should be the cursor of interactive item', function ()
        {
            const stage = new PIXI.Container();
            const graphics = new PIXI.Graphics();
            const pointer = this.pointer = new MockPointer(stage);

            stage.addChild(graphics);
            graphics.beginFill(0xFFFFFF);
            graphics.drawRect(0, 0, 50, 50);
            graphics.interactive = true;
            graphics.cursor = 'help';
            pointer.interaction.cursorStyles.help = 'help';

            pointer.mousemove(10, 10);

            expect(pointer.renderer.view.style.cursor).to.equal('help');
        });

        it('should return cursor to default on mouseout', function ()
        {
            const stage = new PIXI.Container();
            const graphics = new PIXI.Graphics();
            const pointer = this.pointer = new MockPointer(stage);

            stage.addChild(graphics);
            graphics.beginFill(0xFFFFFF);
            graphics.drawRect(0, 0, 50, 50);
            graphics.interactive = true;
            graphics.cursor = 'help';
            pointer.interaction.cursorStyles.help = 'help';

            pointer.mousemove(10, 10);
            pointer.mousemove(60, 60);

            expect(pointer.renderer.view.style.cursor).to.equal(pointer.interaction.cursorStyles.default);
        });

        it('should still be the over cursor after a click', function ()
        {
            const stage = new PIXI.Container();
            const graphics = new PIXI.Graphics();
            const pointer = this.pointer = new MockPointer(stage);

            stage.addChild(graphics);
            graphics.beginFill(0xFFFFFF);
            graphics.drawRect(0, 0, 50, 50);
            graphics.interactive = true;
            graphics.cursor = 'help';
            pointer.interaction.cursorStyles.help = 'help';

            pointer.mousemove(10, 10);
            pointer.click(10, 10);

            expect(pointer.renderer.view.style.cursor).to.equal('help');
        });

        it('should return cursor to default when mouse leaves renderer', function ()
        {
            const stage = new PIXI.Container();
            const graphics = new PIXI.Graphics();
            const pointer = this.pointer = new MockPointer(stage);

            stage.addChild(graphics);
            graphics.beginFill(0xFFFFFF);
            graphics.drawRect(0, 0, 50, 50);
            graphics.interactive = true;
            graphics.cursor = 'help';
            pointer.interaction.cursorStyles.help = 'help';

            pointer.mousemove(10, 10);
            pointer.mousemove(-10, 60);

            expect(pointer.renderer.view.style.cursor).to.equal(pointer.interaction.cursorStyles.default);
        });

        it('cursor callback should be called', function ()
        {
            const stage = new PIXI.Container();
            const graphics = new PIXI.Graphics();
            const overSpy = sinon.spy();
            const defaultSpy = sinon.spy();
            const pointer = this.pointer = new MockPointer(stage);

            stage.addChild(graphics);
            graphics.beginFill(0xFFFFFF);
            graphics.drawRect(0, 0, 50, 50);
            graphics.interactive = true;
            graphics.cursor = 'help';
            pointer.interaction.cursorStyles.help = overSpy;
            pointer.interaction.cursorStyles.default = defaultSpy;

            pointer.mousemove(10, 10);
            pointer.mousemove(60, 60);

            expect(overSpy).to.have.been.called;
            expect(defaultSpy).to.have.been.called;
        });

        it('cursor callback should only be called if the cursor actually changed', function ()
        {
            const stage = new PIXI.Container();
            const graphics = new PIXI.Graphics();
            const defaultSpy = sinon.spy();
            const pointer = this.pointer = new MockPointer(stage);

            stage.addChild(graphics);
            graphics.beginFill(0xFFFFFF);
            graphics.drawRect(0, 0, 50, 50);
            graphics.interactive = true;
            graphics.cursor = null;
            pointer.interaction.cursorStyles.default = defaultSpy;

            pointer.mousemove(10, 10);
            pointer.mousemove(20, 20);

            expect(defaultSpy).to.have.been.calledOnce;
        });

        it('cursor style object should be fully applied', function ()
        {
            const stage = new PIXI.Container();
            const graphics = new PIXI.Graphics();
            const pointer = this.pointer = new MockPointer(stage);

            stage.addChild(graphics);
            graphics.beginFill(0xFFFFFF);
            graphics.drawRect(0, 0, 50, 50);
            graphics.interactive = true;
            graphics.cursor = 'help';
            pointer.interaction.cursorStyles.help = {
                cursor: 'none',
                display: 'none',
            };

            pointer.mousemove(10, 10);

            expect(pointer.renderer.view.style.cursor).to.equal('none');
            expect(pointer.renderer.view.style.display).to.equal('none');
        });

        it('should not change cursor style if no cursor style provided', function ()
        {
            const stage = new PIXI.Container();
            const graphics = new PIXI.Graphics();
            const pointer = this.pointer = new MockPointer(stage);

            stage.addChild(graphics);
            graphics.beginFill(0xFFFFFF);
            graphics.drawRect(0, 0, 50, 50);
            graphics.interactive = true;
            graphics.cursor = 'pointer';
            pointer.interaction.cursorStyles.pointer = null;
            pointer.interaction.cursorStyles.default = null;

            pointer.mousemove(10, 10);
            expect(pointer.renderer.view.style.cursor).to.equal('');

            pointer.mousemove(60, 60);
            expect(pointer.renderer.view.style.cursor).to.equal('');
        });
    });

    describe('recursive hitTesting', function ()
    {
        function getScene()
        {
            const stage = new PIXI.Container();
            const behindChild = new PIXI.Graphics();
            const middleChild = new PIXI.Graphics();
            const frontChild = new PIXI.Graphics();

            behindChild.beginFill(0xFF);
            behindChild.drawRect(0, 0, 50, 50);

            middleChild.beginFill(0xFF00);
            middleChild.drawRect(0, 0, 50, 50);

            frontChild.beginFill(0xFF0000);
            frontChild.drawRect(0, 0, 50, 50);

            stage.addChild(behindChild, middleChild, frontChild);

            return {
                behindChild,
                middleChild,
                frontChild,
                stage,
            };
        }

        describe('when frontChild is interactive', function ()
        {
            it('should stop hitTesting after first hit', function ()
            {
                const scene = getScene();
                const pointer = this.pointer = new MockPointer(scene.stage);
                const frontHitTest = sinon.spy(scene.frontChild, 'containsPoint');
                const middleHitTest = sinon.spy(scene.middleChild, 'containsPoint');
                const behindHitTest = sinon.spy(scene.behindChild, 'containsPoint');

                scene.frontChild.interactive = true;
                scene.middleChild.interactive = true;
                scene.behindChild.interactive = true;

                pointer.mousedown(25, 25);

                expect(frontHitTest).to.have.been.calledOnce;
                expect(middleHitTest).to.not.have.been.called;
                expect(behindHitTest).to.not.have.been.called;
            });
        });

        describe('when frontChild is not interactive', function ()
        {
            it('should stop hitTesting after first hit', function ()
            {
                const scene = getScene();
                const pointer = this.pointer = new MockPointer(scene.stage);
                const frontHitTest = sinon.spy(scene.frontChild, 'containsPoint');
                const middleHitTest = sinon.spy(scene.middleChild, 'containsPoint');
                const behindHitTest = sinon.spy(scene.behindChild, 'containsPoint');

                scene.frontChild.interactive = false;
                scene.middleChild.interactive = true;
                scene.behindChild.interactive = true;

                pointer.mousedown(25, 25);

                expect(frontHitTest).to.not.have.been.called;
                expect(middleHitTest).to.have.been.calledOnce;
                expect(behindHitTest).to.not.have.been.called;
            });
        });
    });

    describe('pointer handling', function ()
    {
        it('pointer event from mouse should use single mouse data', function ()
        {
            const stage = new PIXI.Container();
            const graphics = new PIXI.Graphics();
            const pointer = this.pointer = new MockPointer(stage, 100, 100, true);

            stage.addChild(graphics);
            graphics.beginFill(0xFFFFFF);
            graphics.drawRect(0, 0, 50, 50);
            graphics.interactive = true;

            pointer.mousemove(20, 10, true);

            expect(pointer.interaction.mouse.global.x).to.equal(20);
            expect(pointer.interaction.mouse.global.y).to.equal(10);
        });
    });

    describe('data cleanup', function ()
    {
        it('touchleave after touchout should not orphan data', function ()
        {
            const stage = new PIXI.Container();
            const graphics = new PIXI.Graphics();
            const pointer = this.pointer = new MockPointer(stage);

            stage.addChild(graphics);
            graphics.beginFill(0xFFFFFF);
            graphics.drawRect(0, 0, 50, 50);
            graphics.interactive = true;

            pointer.touchstart(10, 10, 42);
            expect(pointer.interaction.activeInteractionData[42]).to.exist;
            pointer.touchend(10, 10, 42);
            expect(pointer.interaction.activeInteractionData[42]).to.be.undefined;
            pointer.touchleave(10, 10, 42);
            expect(pointer.interaction.activeInteractionData[42]).to.be.undefined;
        });
    });

    describe('hitTest()', function ()
    {
        it('should return hit', function ()
        {
            const stage = new PIXI.Container();
            const graphics = new PIXI.Graphics();
            const pointer = this.pointer = new MockPointer(stage);

            stage.addChild(graphics);
            graphics.beginFill(0xFFFFFF);
            graphics.drawRect(0, 0, 50, 50);
            graphics.interactive = true;

            pointer.render();
            const hit = pointer.interaction.hitTest(new PIXI.Point(10, 10));

            expect(hit).to.equal(graphics);
        });

        it('should return null if not hit', function ()
        {
            const stage = new PIXI.Container();
            const graphics = new PIXI.Graphics();
            const pointer = this.pointer = new MockPointer(stage);

            stage.addChild(graphics);
            graphics.beginFill(0xFFFFFF);
            graphics.drawRect(0, 0, 50, 50);
            graphics.interactive = true;

            pointer.render();
            const hit = pointer.interaction.hitTest(new PIXI.Point(60, 60));

            expect(hit).to.be.null;
        });

        it('should return top thing that was hit', function ()
        {
            const stage = new PIXI.Container();
            const graphics = new PIXI.Graphics();
            const behind = new PIXI.Graphics();
            const pointer = this.pointer = new MockPointer(stage);

            stage.addChild(behind);
            stage.addChild(graphics);
            graphics.beginFill(0xFFFFFF);
            graphics.drawRect(0, 0, 50, 50);
            graphics.interactive = true;
            behind.beginFill(0xFFFFFF);
            behind.drawRect(0, 0, 50, 50);
            behind.interactive = true;

            pointer.render();
            const hit = pointer.interaction.hitTest(new PIXI.Point(10, 10));

            expect(hit).to.equal(graphics);
        });

        it('should return hit when passing in root', function ()
        {
            const stage = new PIXI.Container();
            const graphics = new PIXI.Graphics();
            const behind = new PIXI.Graphics();
            const pointer = this.pointer = new MockPointer(stage);

            stage.addChild(behind);
            stage.addChild(graphics);
            graphics.beginFill(0xFFFFFF);
            graphics.drawRect(0, 0, 50, 50);
            graphics.interactive = true;
            behind.beginFill(0xFFFFFF);
            behind.drawRect(0, 0, 50, 50);
            behind.interactive = true;

            pointer.render();
            const hit = pointer.interaction.hitTest(new PIXI.Point(10, 10), behind);

            expect(hit).to.equal(behind);
        });
    });
});