From 7b4b71f74d748864ad773e8928b31f8d4ee223e1 Mon Sep 17 00:00:00 2001 From: Tim Rourke Date: Sat, 28 Oct 2017 21:32:50 -0500 Subject: [PATCH] Export default options Add tests for constructor Add test for attaching event listeners on init Remove linebreak Add tests for attachig plugins Add tests for attaching native sensors Add tests for attaching non-native sensors Add test for triggering DraggableInitializedEvent on init Add tests for sensors Add tests for destroy Simplify instance creation Add test for removing event listeners on destroy Add test for on Add tests for off and fix indentation Add tests for classnames Fix eslint errors --- src/Draggable/Draggable.js | 2 +- src/Draggable/tests/Draggable.test.js | 710 +++++++++++++++++++++++++- 2 files changed, 705 insertions(+), 7 deletions(-) diff --git a/src/Draggable/Draggable.js b/src/Draggable/Draggable.js index 84385bf9..6f45fc30 100644 --- a/src/Draggable/Draggable.js +++ b/src/Draggable/Draggable.js @@ -36,7 +36,7 @@ const onDragStop = Symbol('onDragStop'); const onDragPressure = Symbol('onDragPressure'); const getAppendableContainer = Symbol('getAppendableContainer'); -const defaults = { +export const defaultOptions = { draggable: '.draggable-source', handle: null, delay: 100, diff --git a/src/Draggable/tests/Draggable.test.js b/src/Draggable/tests/Draggable.test.js index a5330a30..87bdf251 100644 --- a/src/Draggable/tests/Draggable.test.js +++ b/src/Draggable/tests/Draggable.test.js @@ -1,10 +1,29 @@ import { createSandbox, - triggerEvent, } from 'helper'; -import Draggable from './..'; -import {DragStartEvent} from './../DragEvent'; +import Draggable, { + defaultOptions, +} from './../Draggable'; + +import { + DragStartEvent, + DragMoveEvent, + DragStopEvent, +} from './../DragEvent'; + +import { + DraggableInitializedEvent, + DraggableDestroyEvent, +} from './../DraggableEvent'; + +import {Accessibility, Mirror} from './../Plugins'; + +import { + DragSensor, + MouseSensor, + TouchSensor, +} from './../Sensors'; const sampleMarkup = ` `; +/** + * A stub of the Plugin class + * + * @class PluginStub + */ +class PluginStub { + + /** + * Constructor. + * + * @param {Draggable} draggable + */ + constructor(draggable) { + this.draggable = draggable; + this.numTimesAttachCalled = 0; + this.numTimesDetachCalled = 0; + } + + /** + * Set a testable property when `attach` is called + */ + attach() { + this.attachWasCalled = true; + this.numTimesAttachCalled++; + } + + /** + * Set a testable property when `detach` is called + */ + detach() { + this.detachWasCalled = true; + this.numTimesDetachCalled++; + } +} + describe('Draggable', () => { let sandbox; let draggable; @@ -31,18 +85,662 @@ describe('Draggable', () => { sandbox.parentNode.removeChild(sandbox); }); + describe('#constructor', () => { + test('should be an instance of Draggable', () => { + expect(draggable).toBeInstanceOf(Draggable); + }); + + test('should initialize with default options', () => { + const containers = sandbox.querySelectorAll('ul'); + const newInstance = new Draggable(containers); + + for (const key in defaultOptions) { + if (Object.prototype.hasOwnProperty.call(defaultOptions, key)) { + expect(newInstance.options[key]) + .toBe(defaultOptions[key]); + } + } + }); + + test('should add event listeners to containers', () => { + const containers = sandbox.querySelectorAll('ul'); + + containers.forEach((container) => { + container.addEventListener = jest.fn(); + }); + + const newInstance = new Draggable(containers); + + containers.forEach((container) => { + expect(container.addEventListener.mock.calls[0]) + .toMatchObject(['drag:start', newInstance.dragStart, true]); + + expect(container.addEventListener.mock.calls[1]) + .toMatchObject(['drag:move', newInstance.dragMove, true]); + + expect(container.addEventListener.mock.calls[2]) + .toMatchObject(['drag:stop', newInstance.dragStop, true]); + + expect(container.addEventListener.mock.calls[3]) + .toMatchObject(['drag:pressure', newInstance.dragPressure, true]); + }); + }); + + test('should attach default plugins', () => { + const newInstance = new Draggable(); + + expect(newInstance.activePlugins.length) + .toBe(2); + + expect(newInstance.activePlugins[0]) + .toBeInstanceOf(Mirror); + + expect(newInstance.activePlugins[1]) + .toBeInstanceOf(Accessibility); + }); + + test('should attach custom plugins', () => { + const newInstance = new Draggable([], { + plugins: [ + PluginStub, + ], + }); + + expect(newInstance.activePlugins.length) + .toBe(3); + + const customPlugin = newInstance.activePlugins[2]; + + expect(customPlugin.draggable).toBe(newInstance); + + expect(customPlugin.attachWasCalled) + .toBe(true); + }); + + test('should attach sensors for native option', () => { + const newInstance = new Draggable([], { + native: true, + }); + + expect(newInstance.activeSensors.length) + .toBe(2); + + expect(newInstance.activeSensors[0]) + .toBeInstanceOf(TouchSensor); + + expect(newInstance.activeSensors[1]) + .toBeInstanceOf(DragSensor); + }); + + test('should attach sensors for non-native option', () => { + const newInstance = new Draggable([], { + native: false, + }); + + expect(newInstance.activeSensors.length) + .toBe(2); + + expect(newInstance.activeSensors[0]) + .toBeInstanceOf(TouchSensor); + + expect(newInstance.activeSensors[1]) + .toBeInstanceOf(MouseSensor); + }); + + test('should trigger DraggableInitializedEvent on init', () => { + const spy = jest.spyOn(Draggable.prototype, 'triggerEvent'); + const newInstance = new Draggable(); + + expect(spy.mock.calls.length) + .toBe(1); + + expect(spy.mock.calls[0][0]) + .toBeInstanceOf(DraggableInitializedEvent); + + expect(spy.mock.calls[0][0].draggable) + .toBe(newInstance); + + spy.mockReset(); + spy.mockRestore(); + }); + }); + + describe('#destroy', () => { + test('should remove event listeners from containers', () => { + const containers = sandbox.querySelectorAll('ul'); + + containers.forEach((container) => { + container.removeEventListener = jest.fn(); + }); + + const newInstance = new Draggable(containers); + + newInstance.destroy(); + + containers.forEach((container) => { + expect(container.removeEventListener.mock.calls[0]) + .toMatchObject(['drag:start', newInstance.dragStart, true]); + + expect(container.removeEventListener.mock.calls[1]) + .toMatchObject(['drag:move', newInstance.dragMove, true]); + + expect(container.removeEventListener.mock.calls[2]) + .toMatchObject(['drag:stop', newInstance.dragStop, true]); + + expect(container.removeEventListener.mock.calls[3]) + .toMatchObject(['drag:pressure', newInstance.dragPressure, true]); + }); + }); + + test('triggers `draggable:destroy` event on destroy', () => { + const newInstance = new Draggable(); + const callback = jest.fn(); + + newInstance.on('draggable:destroy', callback); + + newInstance.destroy(); + + const call = callback.mock.calls[0][0]; + + expect(call.type) + .toBe('draggable:destroy'); + + expect(call) + .toBeInstanceOf(DraggableDestroyEvent); + + expect(call.draggable) + .toBe(newInstance); + }); + + test('should call Plugin#detach once on each of provided plugins', () => { + const newInstance = new Draggable([], { + plugins: [PluginStub, PluginStub, PluginStub], + }); + + newInstance.destroy(); + + expect(newInstance.activePlugins[2].detachWasCalled) + .toBe(true); + + expect(newInstance.activePlugins[2].numTimesDetachCalled) + .toBe(1); + + expect(newInstance.activePlugins[3].detachWasCalled) + .toBe(true); + + expect(newInstance.activePlugins[3].numTimesDetachCalled) + .toBe(1); + + expect(newInstance.activePlugins[4].detachWasCalled) + .toBe(true); + + expect(newInstance.activePlugins[4].numTimesDetachCalled) + .toBe(1); + }); + }); + + describe('#on', () => { + test('should add an event handler to the list of callbacks', () => { + const newInstance = new Draggable(); + function stubHandler() { /* do nothing */ } + + expect('my:event' in newInstance.callbacks) + .toBe(false); + + newInstance.on('my:event', stubHandler); + + expect('my:event' in newInstance.callbacks) + .toBe(true); + + expect(newInstance.callbacks['my:event']) + .toMatchObject([stubHandler]); + }); + + test('should return draggable instance', () => { + const newInstance = new Draggable(); + function stubHandler() { /* do nothing */ } + + expect('my:event' in newInstance.callbacks) + .toBe(false); + + const returnValue = newInstance.on('my:event', stubHandler); + + expect(returnValue) + .toBe(newInstance); + }); + }); + + describe('#off', () => { + test('should return null if event was not bound', () => { + const newInstance = new Draggable(); + function stubHandler() { /* do nothing */ } + + expect('my:event' in newInstance.callbacks) + .toBe(false); + + const returnValue = newInstance.off('my:event', stubHandler); + + expect(returnValue).toBe(null); + }); + + test('should remove event handler from the list of callbacks', () => { + const newInstance = new Draggable(); + function stubHandler() { /* do nothing */ } + + newInstance.on('my:event', stubHandler); + + expect('my:event' in newInstance.callbacks) + .toBe(true); + + newInstance.off('my:event', stubHandler); + + expect('my:event' in newInstance.callbacks) + .toBe(true); + + expect(newInstance.callbacks['my:event']) + .toMatchObject([]); + }); + + test('should return draggable instance', () => { + const newInstance = new Draggable(); + function stubHandler() { /* do nothing */ } + + newInstance.on('my:event', stubHandler); + + expect('my:event' in newInstance.callbacks) + .toBe(true); + + const returnValue = newInstance.off('my:event', stubHandler); + + expect(returnValue) + .toBe(newInstance); + }); + }); + + describe('#sensors', () => { + test('should return default sensors for non-native option', () => { + const containers = sandbox.querySelectorAll('ul'); + const newInstance = new Draggable(containers, { + native: false, + }); + const sensors = newInstance.sensors(); + + expect(sensors.length) + .toBe(2); + + expect(new sensors[0]()) + .toBeInstanceOf(TouchSensor); + + expect(new sensors[1]()) + .toBeInstanceOf(MouseSensor); + }); + + test('should return native sensors for native option', () => { + const containers = sandbox.querySelectorAll('ul'); + const newInstance = new Draggable(containers, { + native: true, + }); + const sensors = newInstance.sensors(); + + expect(sensors.length) + .toBe(2); + + expect(new sensors[0]()) + .toBeInstanceOf(TouchSensor); + + expect(new sensors[1]()) + .toBeInstanceOf(DragSensor); + }); + }); + + + describe('#trigger', () => { + test('should invoke bound event', () => { + const handler = jest.fn(); + + draggable.on('my:event', handler); + + draggable.trigger('my:event', 'expectedArg', 'expectedArg2'); + + expect(handler.mock.calls.length) + .toBe(1); + + expect(handler.mock.calls[0].length) + .toBe(2); + + expect(handler.mock.calls[0][0]) + .toBe('expectedArg'); + + expect(handler.mock.calls[0][1]) + .toBe('expectedArg2'); + }); + }); + + describe('#triggerEvent', () => { + test('should invoke bound event by its type', () => { + const handler = jest.fn(); + const event = { + type: 'my:event', + value: 'expectedValue', + }; + + draggable.on('my:event', handler); + + draggable.triggerEvent(event); + + expect(handler.mock.calls.length) + .toBe(1); + + expect(handler.mock.calls[0].length) + .toBe(1); + + expect(handler.mock.calls[0][0]) + .toMatchObject(event); + }); + }); + test('triggers `drag:start` drag event on mousedown', () => { const draggableElement = sandbox.querySelector('li'); document.elementFromPoint = () => draggableElement; const callback = jest.fn(); draggable.on('drag:start', callback); - triggerEvent(draggableElement, 'mousedown', {button: 0}); + + const event = new MouseEvent('mousedown'); + + draggableElement.dispatchEvent(event); + + // Wait for delay + jest.runTimersToTime(100); + + const call = callback.mock.calls[0][0]; + + expect(call.type) + .toBe('drag:start'); + + expect(call) + .toBeInstanceOf(DragStartEvent); + }); + + test('should trigger `drag:start` drag event on dragstart', () => { + const draggableElement = sandbox.querySelector('li'); + + const callback = jest.fn(); + draggable.on('drag:start', callback); + + const event = new MouseEvent('mousedown'); + + draggableElement.dispatchEvent(event); + + // Wait for delay + jest.runTimersToTime(100); + + draggableElement.dispatchEvent(new MouseEvent('dragstart')); + + const call = callback.mock.calls[0][0]; + + expect(call.type) + .toBe('drag:start'); + + expect(call) + .toBeInstanceOf(DragStartEvent); + }); + + test('triggers `drag:move` drag event on mousedown', () => { + const draggableElement = sandbox.querySelector('li'); + const expectedClientX = 39; + const expectedClientY = 82; + + draggableElement.dispatchEvent(new MouseEvent('mousedown')); + + // Wait for delay + jest.runTimersToTime(100); + + const callback = jest.fn(); + draggable.on('drag:move', callback); + + const event = new MouseEvent('mousemove', { + clientX: expectedClientX, + clientY: expectedClientY, + }); + + document.dispatchEvent(event); + + const call = callback.mock.calls[0][0]; + const sensorEvent = call.data.sensorEvent; + + expect(call.type) + .toBe('drag:move'); + + expect(call) + .toBeInstanceOf(DragMoveEvent); + + expect(sensorEvent.clientX) + .toBe(39); + + expect(sensorEvent.clientY) + .toBe(82); + }); + + test('triggers `drag:stop` drag event on mouseup', () => { + const draggableElement = sandbox.querySelector('li'); + document.elementFromPoint = () => draggableElement; + + draggableElement.dispatchEvent(new MouseEvent('mousedown')); + + // Wait for delay + jest.runTimersToTime(100); + + const callback = jest.fn(); + draggable.on('drag:stop', callback); + + document.dispatchEvent(new MouseEvent('mouseup')); + + const call = callback.mock.calls[0][0]; + + expect(call.type) + .toBe('drag:stop'); + + expect(call) + .toBeInstanceOf(DragStopEvent); + }); + + test('adds `source:dragging` classname to draggable element on mousedown', () => { + const draggableElement = sandbox.querySelector('li'); + + draggableElement.dispatchEvent(new MouseEvent('mousedown')); + + // Wait for delay + jest.runTimersToTime(100); + + expect(draggable.source.classList) + .toContain('draggable-source--is-dragging'); + }); + + test('removes `source:dragging` classname from draggable element on mouseup', () => { + const draggableElement = sandbox.querySelector('li'); + document.elementFromPoint = () => draggableElement; + + draggableElement.dispatchEvent(new MouseEvent('mousedown')); + + // Wait for delay + jest.runTimersToTime(100); + + const source = draggable.source; + + expect(source.classList) + .toContain('draggable-source--is-dragging'); + + document.dispatchEvent(new MouseEvent('mouseup')); + + expect(source.classList) + .not + .toContain('draggable-source--is-dragging'); + }); + + test('removes `source:dragging` classname from draggable element on dragEvent.cancel()', () => { + const draggableElement = sandbox.querySelector('li'); + document.elementFromPoint = () => draggableElement; + + draggable.on('drag:start', (event) => { + expect(draggable.source.classList) + .toContain('draggable-source--is-dragging'); + + event.cancel(); + }); + + draggableElement.dispatchEvent(new MouseEvent('mousedown')); + + // Wait for delay + jest.runTimersToTime(100); + + const source = draggable.source; + + expect(source.classList) + .not + .toContain('draggable-source--is-dragging'); + }); + + test('adds `body:dragging` classname to body on mousedown', () => { + const draggableElement = sandbox.querySelector('li'); + document.elementFromPoint = () => draggableElement; + + draggableElement.dispatchEvent(new MouseEvent('mousedown')); + + // Wait for delay + jest.runTimersToTime(100); + + expect(document.body.classList) + .toContain('draggable--is-dragging'); + }); + + test('removes `body:dragging` classname from body on mouseup', () => { + const draggableElement = sandbox.querySelector('li'); + document.elementFromPoint = () => draggableElement; + + draggableElement.dispatchEvent(new MouseEvent('mousedown')); + + // Wait for delay + jest.runTimersToTime(100); + + expect(document.body.classList) + .toContain('draggable--is-dragging'); + + document.dispatchEvent(new MouseEvent('mouseup')); + + expect(document.body.classList) + .not + .toContain('draggable--is-dragging'); + }); + + test('removes `body:dragging` classname from body on dragEvent.cancel()', () => { + const draggableElement = sandbox.querySelector('li'); + document.elementFromPoint = () => draggableElement; + + draggable.on('drag:start', (event) => { + expect(document.body.classList) + .toContain('draggable--is-dragging'); + + event.cancel(); + }); + + draggableElement.dispatchEvent(new MouseEvent('mousedown')); + + // Wait for delay + jest.runTimersToTime(100); + + expect(document.body.classList) + .not + .toContain('draggable--is-dragging'); + }); + + test('adds `container:placed` classname to draggable container element on mouseup', () => { + const draggableElement = sandbox.querySelector('li'); + const container = sandbox.querySelector('ul'); + + draggableElement.dispatchEvent(new MouseEvent('mousedown')); + + // Wait for delay + jest.runTimersToTime(100); + + document.dispatchEvent(new MouseEvent('mouseup')); + + expect(container.classList) + .toContain('draggable-container--placed'); + }); + + test('removes `container:placed` classname from draggable container element on mouseup after delay', () => { + const draggableElement = sandbox.querySelector('li'); + const container = sandbox.querySelector('ul'); + + draggableElement.dispatchEvent(new MouseEvent('mousedown')); + + // Wait for delay + jest.runTimersToTime(100); + + document.dispatchEvent(new MouseEvent('mouseup')); + + expect(container.classList) + .toContain('draggable-container--placed'); + + // Wait for default draggable.options.placedTimeout delay + jest.runTimersToTime(800); + + expect(container.classList) + .not + .toContain('draggable-container--placed'); + }); + + test('adds `container:dragging` classname to draggable container element on mousedown', () => { + const draggableElement = sandbox.querySelector('li'); + const container = sandbox.querySelector('ul'); + + draggableElement.dispatchEvent(new MouseEvent('mousedown')); + + // Wait for delay + jest.runTimersToTime(100); + + expect(container.classList) + .toContain('draggable-container--is-dragging'); + }); + + test('removes `container:dragging` classname from draggable container element on mouseup', () => { + const draggableElement = sandbox.querySelector('li'); + const container = sandbox.querySelector('ul'); + + draggableElement.dispatchEvent(new MouseEvent('mousedown')); + + // Wait for delay + jest.runTimersToTime(100); + + expect(container.classList) + .toContain('draggable-container--is-dragging'); + + document.dispatchEvent(new MouseEvent('mouseup')); + + expect(container.classList) + .not + .toContain('draggable-container--is-dragging'); + }); + + test('removes `container:dragging` classname from draggable container element on dragEvent.cancel()', () => { + const draggableElement = sandbox.querySelector('li'); + const container = sandbox.querySelector('ul'); + + draggable.on('drag:start', (event) => { + expect(container.classList) + .toContain('draggable-container--is-dragging'); + + event.cancel(); + }); + + draggableElement.dispatchEvent(new MouseEvent('mousedown')); // Wait for delay jest.runTimersToTime(100); - expect(callback.mock.calls[0][0].type).toBe('drag:start'); - expect(callback.mock.calls[0][0]).toBeInstanceOf(DragStartEvent); + expect(container.classList) + .not + .toContain('draggable-container--is-dragging'); }); });