diff --git a/src/state/auto-scroller/jump-scroller.js b/src/state/auto-scroller/jump-scroller.js index e470988a99..07fb7c01e0 100644 --- a/src/state/auto-scroller/jump-scroller.js +++ b/src/state/auto-scroller/jump-scroller.js @@ -118,6 +118,8 @@ export default ({ return; } + console.warn('manual keyboard move', windowRemainder); + // The entire scroll could not be absorbed by the droppable and window // so we manually move whatever is left moveByOffset(state, windowRemainder); diff --git a/src/state/move-to-next-index/in-home-list.js b/src/state/move-to-next-index/in-home-list.js index b83e613a7d..34f5d88a51 100644 --- a/src/state/move-to-next-index/in-home-list.js +++ b/src/state/move-to-next-index/in-home-list.js @@ -87,6 +87,9 @@ export default ({ viewport: viewport.frame, }); + console.log('new page center', newPageBorderBoxCenter, 'is visible in frame', viewport.frame, isVisibleInNewLocation); + + const displaced: Displacement[] = (() => { if (isMovingTowardStart) { return withFirstRemoved({ @@ -131,6 +134,8 @@ export default ({ const distance: Position = subtract(newPageBorderBoxCenter, previousPageBorderBoxCenter); const distanceWithScroll: Position = withDroppableDisplacement(droppable, distance); + console.log('request jump scroll', distanceWithScroll); + return { pageBorderBoxCenter: previousPageBorderBoxCenter, impact: newImpact, diff --git a/src/state/reducer.js b/src/state/reducer.js index b8aed1e94b..4ff343266c 100644 --- a/src/state/reducer.js +++ b/src/state/reducer.js @@ -285,11 +285,17 @@ export default (state: State = idle, action: Action): State => { return state; } + invariant(isMovementAllowed(state), `MOVE not permitted in phase ${state.phase}`); + const { client, shouldAnimate } = action.payload; + // If we are jump scrolling - manual movements should not update the impact + const impact: ?DragImpact = state.autoScrollMode === 'JUMP' ? state.impact : null; + return moveWithPositionUpdates({ state, clientSelection: client, + impact, shouldAnimate, }); } @@ -425,9 +431,10 @@ export default (state: State = idle, action: Action): State => { const isJumpScrolling: boolean = state.autoScrollMode === 'JUMP'; const impact: ?DragImpact = isJumpScrolling ? state.impact : null; - console.log('scrolling viewport in state'); const viewport: Viewport = scrollViewport(state.viewport, newScroll); + console.log('MOVE_BY_WINDOW_SCROLL: updating viewport to', viewport.frame); + return moveWithPositionUpdates({ state, clientSelection: state.current.client.selection, @@ -443,6 +450,8 @@ export default (state: State = idle, action: Action): State => { return state; } + console.warn(action.type); + invariant(state.phase === 'DRAGGING', `${action.type} received while not in DRAGGING phase`); const { droppable, isMainAxisMovementAllowed } = (() => { @@ -491,6 +500,7 @@ export default (state: State = idle, action: Action): State => { if (!result) { return state; } + const impact: DragImpact = result.impact; const pageBorderBoxCenter: Position = result.pageBorderBoxCenter; // TODO: not sure if this is correct diff --git a/src/state/scroll-viewport.js b/src/state/scroll-viewport.js index ca747778fc..69f170d697 100644 --- a/src/state/scroll-viewport.js +++ b/src/state/scroll-viewport.js @@ -9,13 +9,13 @@ export default (viewport: Viewport, newScroll: Position): Viewport => { const diff: Position = subtract(newScroll, viewport.scroll.initial); const displacement: Position = negate(diff); + // We need to update the frame so that it is always a live value + // The top / left of the frame should always match the newScroll position + const change: Position = subtract(diff, viewport.scroll.current); const frame: Rect = getRect( - offsetByPosition(viewport.frame, diff) + offsetByPosition(viewport.frame, change) ); - console.log('scroll change recorded:', diff); - console.log('window scroll is now', newScroll) - const updated: Viewport = { frame, scroll: { diff --git a/src/view/drag-handle/sensor/create-keyboard-sensor.js b/src/view/drag-handle/sensor/create-keyboard-sensor.js index 9f5fbf6a22..ad3deef83f 100644 --- a/src/view/drag-handle/sensor/create-keyboard-sensor.js +++ b/src/view/drag-handle/sensor/create-keyboard-sensor.js @@ -71,6 +71,8 @@ export default ({ // cannot lift at this time if (!canStartCapturing(event)) { + // need to block to prevent default browser behaviour + event.preventDefault(); return; } diff --git a/src/view/drag-handle/sensor/create-touch-sensor.js b/src/view/drag-handle/sensor/create-touch-sensor.js index 58fa8fea35..dc007eb4dd 100644 --- a/src/view/drag-handle/sensor/create-touch-sensor.js +++ b/src/view/drag-handle/sensor/create-touch-sensor.js @@ -368,6 +368,7 @@ export default ({ return; } + // TODO: call prevent default here? if (!canStartCapturing(event)) { return; } diff --git a/test/unit/state/action-creators.spec.js b/test/unit/state/action-creators.spec.js deleted file mode 100644 index 3b3589b1da..0000000000 --- a/test/unit/state/action-creators.spec.js +++ /dev/null @@ -1,224 +0,0 @@ -// @flow -import { type Position } from 'css-box-model'; -import { - cancel, - clean, - lift, - completeDrop, - prepare, - completeLift, - requestDimensions, - publishDraggableDimension, - publishDroppableDimension, -} from '../../../src/state/action-creators'; -import createStore from '../../../src/state/create-store'; -import { getPreset } from '../../utils/dimension'; -import getViewport from '../../../src/view/window/get-viewport'; -import getStatePreset from '../../utils/get-simple-state-preset'; -import type { - State, - DraggableId, - Store, - InitialDragPositions, - LiftRequest, - Viewport, -} from '../../../src/types'; - -const preset = getPreset(); -const origin: Position = { x: 0, y: 0 }; -const noWhere: InitialDragPositions = { - selection: origin, - borderBoxCenter: origin, -}; - -type LiftFnArgs = {| - id: DraggableId, - client: InitialDragPositions, - viewport: Viewport, - autoScrollMode: 'FLUID' | 'JUMP', -|} - -const liftDefaults: LiftFnArgs = { - id: preset.inHome1.descriptor.id, - viewport: getViewport(), - client: noWhere, - autoScrollMode: 'FLUID', -}; - -const state = getStatePreset(); - -const liftWithDefaults = (args?: LiftFnArgs = liftDefaults) => { - const { id, client, viewport, autoScrollMode } = args; - return lift(id, client, viewport, autoScrollMode); -}; - -describe('action creators', () => { - describe('lift', () => { - beforeEach(() => { - jest.useFakeTimers(); - jest.spyOn(console, 'error').mockImplementation(() => { }); - }); - - afterEach(() => { - jest.useRealTimers(); - console.error.mockRestore(); - }); - - // This test is more a baseline for the others - // to ensure that the happy path works correctly - it('should perform a multi phased lift', () => { - const store: Store = createStore(); - jest.spyOn(store, 'dispatch'); - - liftWithDefaults()(store.dispatch, store.getState); - - // Phase 1: flush any existing animations - expect(store.dispatch).toHaveBeenCalledWith(prepare()); - expect(store.dispatch).toHaveBeenCalledTimes(1); - - // Phase 2: request dimensions after flushing animations - jest.runOnlyPendingTimers(); - - const request: LiftRequest = { - draggableId: preset.inHome1.descriptor.id, - scrollOptions: { - shouldPublishImmediately: false, - }, - }; - expect(store.dispatch).toHaveBeenCalledWith(requestDimensions(request)); - expect(store.dispatch).toHaveBeenCalledTimes(2); - - // publishing some fake dimensions - store.dispatch(publishDroppableDimension(preset.home)); - store.dispatch(publishDraggableDimension(preset.inHome1)); - // now called four times - expect(store.dispatch).toHaveBeenCalledTimes(4); - - // Phase 3: after dimensions are collected complete the lift - jest.runOnlyPendingTimers(); - - expect(store.dispatch).toHaveBeenCalledWith(completeLift( - liftDefaults.id, - liftDefaults.client, - liftDefaults.viewport, - liftDefaults.autoScrollMode, - )); - expect(store.dispatch).toHaveBeenCalledTimes(5); - }); - - describe('flushing previous drop animations', () => { - it('should flush any existing drop animation', () => { - const current: State = state.dropAnimating(); - const dispatch: Function = jest.fn(); - const getState: Function = jest.fn(() => current); - - liftWithDefaults()(dispatch, getState); - - // $ExpectError - not checking for null in state shape - expect(dispatch).toHaveBeenCalledWith(completeDrop(current.drop.pending.result)); - expect(dispatch).toHaveBeenCalledWith(prepare()); - expect(console.error).not.toHaveBeenCalled(); - }); - - it('should clean the state and log an error if there is an invalid drop animating state', () => { - const current: State = { - ...state.idle, - // hacking the phase - phase: 'DROP_ANIMATING', - }; - const dispatch: Function = jest.fn(); - const getState: Function = jest.fn(() => current); - - liftWithDefaults()(dispatch, getState); - - expect(dispatch).toHaveBeenCalledWith(clean()); - expect(console.error).toHaveBeenCalled(); - }); - - it('should not begin a lift if the drag is cancelled while the animations are flushing', () => { - const store: Store = createStore(); - jest.spyOn(store, 'dispatch'); - - liftWithDefaults()(store.dispatch, store.getState); - // flushing - expect(store.dispatch).toHaveBeenCalledWith(prepare()); - - // need to wait for setTimeout to flush - expect(store.dispatch).toHaveBeenCalledTimes(1); - - // while waiting a cancel occurs - cancel()(store.dispatch, store.getState); - - // because a drag was not occurring the state is cleaned - expect(store.dispatch).toHaveBeenCalledWith(clean()); - // now called two times - expect(store.dispatch).toHaveBeenCalledTimes(2); - - // now ticking the setTimeout - jest.runOnlyPendingTimers(); - - // normally would start requesting dimensions - expect(store.dispatch).not.toHaveBeenCalledWith( - completeLift( - liftDefaults.id, - liftDefaults.client, - liftDefaults.viewport, - liftDefaults.autoScrollMode, - ) - ); - - // dispatch not called since previous clean - expect(store.dispatch).toHaveBeenCalledTimes(2); - }); - }); - - describe('dimensions collected and drag not started', () => { - it('should not continue to lift if cancelled', () => { - const store: Store = createStore(); - jest.spyOn(store, 'dispatch'); - - liftWithDefaults()(store.dispatch, store.getState); - - // Phase 1: flush any existing animations - expect(store.dispatch).toHaveBeenCalledWith(prepare()); - expect(store.dispatch).toHaveBeenCalledTimes(1); - - // Phase 2: request dimensions after flushing animations - jest.runOnlyPendingTimers(); - - expect(store.dispatch).toHaveBeenCalledWith( - requestDimensions({ - draggableId: preset.inHome1.descriptor.id, - scrollOptions: { - shouldPublishImmediately: false, - }, - }) - ); - expect(store.dispatch).toHaveBeenCalledTimes(2); - - // drag is now cancelled before all dimensions are published - cancel()(store.dispatch, store.getState); - expect(store.dispatch).toHaveBeenCalledTimes(3); - - // This would usually start phase three: lift - jest.runOnlyPendingTimers(); - - // no increase in the amount of times called - expect(store.dispatch).toHaveBeenCalledTimes(3); - expect(store.dispatch).not.toHaveBeenCalledWith(completeLift( - liftDefaults.id, - liftDefaults.client, - liftDefaults.viewport, - liftDefaults.autoScrollMode, - )); - - // being super careful - jest.runAllTimers(); - expect(store.dispatch).toHaveBeenCalledTimes(3); - - // should be in the idle state - expect(store.getState()).toEqual(state.idle); - }); - }); - }); -}); diff --git a/test/unit/state/auto-scroll/can-scroll.spec.js b/test/unit/state/auto-scroll/can-scroll.spec.js index 459c062a90..fadd403b8f 100644 --- a/test/unit/state/auto-scroll/can-scroll.spec.js +++ b/test/unit/state/auto-scroll/can-scroll.spec.js @@ -53,7 +53,7 @@ const scrollable: DroppableDimension = getDroppableDimension({ }); const customViewport: Viewport = createViewport({ - subject: getRect({ + frame: getRect({ top: 0, left: 0, right: 100, @@ -414,7 +414,7 @@ describe('can scroll', () => { describe('can scroll window', () => { it('should return true if the window is able to be scrolled', () => { const viewport: Viewport = createViewport({ - subject: customViewport.subject, + frame: customViewport.frame, scrollHeight: 200, scrollWidth: 100, scroll: origin, @@ -427,7 +427,7 @@ describe('can scroll', () => { it('should return false if the window is not able to be scrolled', () => { const viewport: Viewport = createViewport({ - subject: customViewport.subject, + frame: customViewport.frame, scrollHeight: 200, scrollWidth: 100, // already at the max scroll @@ -500,7 +500,7 @@ describe('can scroll', () => { describe('get window overlap', () => { it('should return null if the window cannot be scrolled', () => { const viewport: Viewport = createViewport({ - subject: customViewport.subject, + frame: customViewport.frame, scrollHeight: 200, scrollWidth: 100, // already at the max scroll @@ -518,7 +518,7 @@ describe('can scroll', () => { // tested in get remainder it('should return the overlap', () => { const viewport: Viewport = createViewport({ - subject: customViewport.subject, + frame: customViewport.frame, scrollHeight: 200, scrollWidth: 200, scroll: { @@ -531,8 +531,8 @@ describe('can scroll', () => { const maxScroll: Position = getMaxScroll({ scrollHeight: 200, scrollWidth: 200, - height: viewport.subject.height, - width: viewport.subject.width, + height: viewport.frame.height, + width: viewport.frame.width, }); expect(maxScroll).toEqual({ x: 100, y: 100 }); @@ -551,7 +551,7 @@ describe('can scroll', () => { it('should return null if there is no overlap', () => { const viewport: Viewport = createViewport({ - subject: customViewport.subject, + frame: customViewport.frame, scrollHeight: 200, scrollWidth: 200, scroll: origin, diff --git a/test/unit/state/get-drag-impact.spec.js b/test/unit/state/get-drag-impact.spec.js index 58b85c098a..0da017825f 100644 --- a/test/unit/state/get-drag-impact.spec.js +++ b/test/unit/state/get-drag-impact.spec.js @@ -503,7 +503,7 @@ describe('get drag impact', () => { [axis.crossAxisStart]: 0, [axis.crossAxisEnd]: 100, [axis.start]: 0, - [axis.end]: viewport.subject[axis.end] + 100, + [axis.end]: viewport.frame[axis.end] + 100, }, }); const visible: DraggableDimension = getDraggableDimension({ @@ -516,7 +516,7 @@ describe('get drag impact', () => { [axis.crossAxisStart]: 0, [axis.crossAxisEnd]: 100, [axis.start]: 0, - [axis.end]: viewport.subject[axis.end], + [axis.end]: viewport.frame[axis.end], }, }); const notVisible1: DraggableDimension = getDraggableDimension({ @@ -529,8 +529,8 @@ describe('get drag impact', () => { [axis.crossAxisStart]: 0, [axis.crossAxisEnd]: 100, // inside the droppable, but not in the visible area - [axis.start]: viewport.subject[axis.end] + 10, - [axis.end]: viewport.subject[axis.end] + 20, + [axis.start]: viewport.frame[axis.end] + 10, + [axis.end]: viewport.frame[axis.end] + 20, }, }); const notVisible2: DraggableDimension = getDraggableDimension({ @@ -543,8 +543,8 @@ describe('get drag impact', () => { [axis.crossAxisStart]: 0, [axis.crossAxisEnd]: 100, // inside the droppable, but not in the visible area - [axis.start]: viewport.subject[axis.end] + 30, - [axis.end]: viewport.subject[axis.end] + 40, + [axis.start]: viewport.frame[axis.end] + 30, + [axis.end]: viewport.frame[axis.end] + 40, }, }); const customDraggables: DraggableDimensionMap = { @@ -1070,7 +1070,7 @@ describe('get drag impact', () => { [axis.crossAxisStart]: foreignCrossAxisStart, [axis.crossAxisEnd]: foreignCrossAxisEnd, [axis.start]: 0, - [axis.end]: viewport.subject[axis.end], + [axis.end]: viewport.frame[axis.end], }, }); const notVisible: DraggableDimension = getDraggableDimension({ @@ -1180,7 +1180,7 @@ describe('get drag impact', () => { [axis.crossAxisEnd]: foreignCrossAxisEnd, [axis.start]: 0, // stretches longer than viewport - [axis.end]: viewport.subject[axis.end] + 100, + [axis.end]: viewport.frame[axis.end] + 100, }, }); const visible: DraggableDimension = getDraggableDimension({ @@ -1193,7 +1193,7 @@ describe('get drag impact', () => { [axis.crossAxisStart]: foreignCrossAxisStart, [axis.crossAxisEnd]: foreignCrossAxisEnd, [axis.start]: 0, - [axis.end]: viewport.subject[axis.end], + [axis.end]: viewport.frame[axis.end], }, }); const notVisible: DraggableDimension = getDraggableDimension({ @@ -1206,8 +1206,8 @@ describe('get drag impact', () => { [axis.crossAxisStart]: foreignCrossAxisStart, [axis.crossAxisEnd]: foreignCrossAxisEnd, // inside the droppable, but not in the visible area - [axis.start]: viewport.subject[axis.end] + 10, - [axis.end]: viewport.subject[axis.end] + 20, + [axis.start]: viewport.frame[axis.end] + 10, + [axis.end]: viewport.frame[axis.end] + 20, }, }); diff --git a/test/unit/state/hook-caller.spec.js b/test/unit/state/hook-caller.spec.js deleted file mode 100644 index ac83239f50..0000000000 --- a/test/unit/state/hook-caller.spec.js +++ /dev/null @@ -1,1259 +0,0 @@ -// @flow -import createHookCaller from '../../../src/state/hooks/hook-caller'; -import messagePreset from '../../../src/state/hooks/message-preset'; -import type { HookCaller } from '../../../src/state/hooks/hooks-types'; -import getStatePreset from '../../utils/get-simple-state-preset'; -import { getPreset } from '../../utils/dimension'; -import noImpact, { noMovement } from '../../../src/state/no-impact'; -import type { - Announce, - Hooks, - HookProvided, - DropResult, - State, - DimensionState, - DraggableLocation, - DraggableDescriptor, - DroppableDimension, - DragStart, - DragUpdate, - DragImpact, -} from '../../../src/types'; - -const preset = getPreset(); -const state = getStatePreset(); - -const noDimensions: DimensionState = { - request: null, - draggable: {}, - droppable: {}, -}; - -describe('fire hooks', () => { - let hooks: Hooks; - let caller: HookCaller; - let announceMock: Announce; - - beforeEach(() => { - jest.useFakeTimers(); - announceMock = jest.fn(); - caller = createHookCaller(announceMock); - hooks = { - onDragStart: jest.fn(), - onDragUpdate: jest.fn(), - onDragEnd: jest.fn(), - }; - jest.spyOn(console, 'error').mockImplementation(() => { }); - jest.spyOn(console, 'warn').mockImplementation(() => { }); - }); - - afterEach(() => { - console.error.mockRestore(); - console.warn.mockRestore(); - jest.useRealTimers(); - }); - - describe('drag start', () => { - it('should call the onDragStart hook when a drag starts', () => { - const expected: DragStart = { - draggableId: preset.inHome1.descriptor.id, - type: preset.home.descriptor.type, - source: { - droppableId: preset.inHome1.descriptor.droppableId, - index: preset.inHome1.descriptor.index, - }, - }; - - caller.onStateChange(hooks, state.requesting(), state.dragging()); - - expect(hooks.onDragStart).toHaveBeenCalledWith(expected, { - announce: expect.any(Function), - }); - }); - - it('should log an error and not call the callback if there is no current drag', () => { - const invalid: State = { - ...state.dragging(), - drag: null, - }; - - caller.onStateChange(hooks, state.requesting(), invalid); - - expect(console.error).toHaveBeenCalled(); - }); - - it('should not call if only collecting dimensions (not dragging yet)', () => { - caller.onStateChange(hooks, state.idle, state.preparing); - caller.onStateChange(hooks, state.preparing, state.requesting()); - - expect(hooks.onDragStart).not.toHaveBeenCalled(); - }); - - describe('announcements', () => { - const getDragStart = (appState: State): ?DragStart => { - if (!appState.drag) { - return null; - } - - const descriptor: DraggableDescriptor = appState.drag.initial.descriptor; - const home: ?DroppableDimension = appState.dimension.droppable[descriptor.droppableId]; - - if (!home) { - return null; - } - - const source: DraggableLocation = { - index: descriptor.index, - droppableId: descriptor.droppableId, - }; - - const start: DragStart = { - draggableId: descriptor.id, - type: home.descriptor.type, - source, - }; - - return start; - }; - - const dragStart: ?DragStart = getDragStart(state.dragging()); - if (!dragStart) { - throw new Error('Invalid test setup'); - } - - it('should announce with the default lift message if no message is provided', () => { - caller.onStateChange(hooks, state.requesting(), state.dragging()); - - expect(announceMock).toHaveBeenCalledWith(messagePreset.onDragStart(dragStart)); - }); - - it('should announce with the default lift message if no onDragStart hook is provided', () => { - const customHooks: Hooks = { - onDragEnd: jest.fn(), - }; - - caller.onStateChange(customHooks, state.requesting(), state.dragging()); - - expect(announceMock).toHaveBeenCalledWith(messagePreset.onDragStart(dragStart)); - }); - - it('should announce with a provided message', () => { - const customHooks: Hooks = { - onDragStart: (start: DragStart, provided: HookProvided) => provided.announce('test'), - onDragEnd: jest.fn(), - }; - - caller.onStateChange(customHooks, state.requesting(), state.dragging()); - - expect(announceMock).toHaveBeenCalledTimes(1); - expect(announceMock).toHaveBeenCalledWith('test'); - }); - - it('should prevent double announcing', () => { - let myAnnounce: ?Announce; - const customHooks: Hooks = { - onDragStart: (start: DragStart, provided: HookProvided) => { - myAnnounce = provided.announce; - myAnnounce('test'); - }, - onDragEnd: jest.fn(), - }; - - caller.onStateChange(customHooks, state.requesting(), state.dragging()); - expect(announceMock).toHaveBeenCalledWith('test'); - expect(announceMock).toHaveBeenCalledTimes(1); - expect(console.warn).not.toHaveBeenCalled(); - - if (!myAnnounce) { - throw new Error('Invalid test setup'); - } - - myAnnounce('second'); - - expect(announceMock).toHaveBeenCalledTimes(1); - expect(console.warn).toHaveBeenCalled(); - }); - - it('should prevent async announcing', () => { - const customHooks: Hooks = { - onDragStart: (start: DragStart, provided: HookProvided) => { - setTimeout(() => { - // boom - provided.announce('too late'); - }); - }, - onDragEnd: jest.fn(), - }; - - caller.onStateChange(customHooks, state.requesting(), state.dragging()); - expect(announceMock).toHaveBeenCalledWith(messagePreset.onDragStart(dragStart)); - expect(console.warn).not.toHaveBeenCalled(); - - // not releasing the async message - jest.runOnlyPendingTimers(); - - expect(announceMock).toHaveBeenCalledTimes(1); - expect(console.warn).toHaveBeenCalled(); - }); - }); - }); - - describe('drag update', () => { - const withImpact = (current: State, impact: DragImpact) => { - if (!current.drag) { - throw new Error('invalid state'); - } - return { - ...current, - drag: { - ...current.drag, - impact, - }, - }; - }; - - const start: DragStart = { - draggableId: preset.inHome1.descriptor.id, - type: preset.home.descriptor.type, - source: { - index: preset.inHome1.descriptor.index, - droppableId: preset.inHome1.descriptor.droppableId, - }, - }; - - const inHomeImpact: DragImpact = { - movement: noMovement, - direction: preset.home.axis.direction, - destination: start.source, - }; - - describe('has not moved from home location', () => { - beforeEach(() => { - // start a drag - caller.onStateChange( - hooks, - state.requesting(), - withImpact(state.dragging(), inHomeImpact), - ); - expect(hooks.onDragUpdate).not.toHaveBeenCalled(); - }); - - it('should not provide an update if the location has not changed since the last drag', () => { - // drag to the same spot - caller.onStateChange( - hooks, - withImpact(state.dragging(), inHomeImpact), - withImpact(state.dragging(), inHomeImpact), - ); - - expect(hooks.onDragUpdate).not.toHaveBeenCalled(); - }); - - it('should provide an update if the index changes', () => { - const destination: DraggableLocation = { - index: preset.inHome1.descriptor.index + 1, - droppableId: preset.inHome1.descriptor.droppableId, - }; - const impact: DragImpact = { - movement: noMovement, - direction: preset.home.axis.direction, - destination, - }; - const expected: DragUpdate = { - draggableId: start.draggableId, - type: start.type, - source: start.source, - destination, - }; - - // drag to the same spot - caller.onStateChange( - hooks, - withImpact(state.dragging(), inHomeImpact), - withImpact(state.dragging(), impact), - ); - - expect(hooks.onDragUpdate).toHaveBeenCalledWith(expected, { - announce: expect.any(Function), - }); - }); - - it('should provide an update if the droppable changes', () => { - const destination: DraggableLocation = { - // same index - index: preset.inHome1.descriptor.index, - // different droppable - droppableId: preset.foreign.descriptor.id, - }; - const impact: DragImpact = { - movement: noMovement, - direction: preset.home.axis.direction, - destination, - }; - const expected: DragUpdate = { - draggableId: start.draggableId, - type: start.type, - source: start.source, - destination, - }; - - // drag to the same spot - caller.onStateChange( - hooks, - withImpact(state.dragging(), inHomeImpact), - withImpact(state.dragging(), impact), - ); - - expect(hooks.onDragUpdate).toHaveBeenCalledWith(expected, { - announce: expect.any(Function), - }); - }); - - it('should provide an update if moving from a droppable to nothing', () => { - const expected: DragUpdate = { - draggableId: start.draggableId, - type: start.type, - source: start.source, - destination: null, - }; - - // drag to the same spot - caller.onStateChange( - hooks, - withImpact(state.dragging(), inHomeImpact), - withImpact(state.dragging(), noImpact), - ); - - expect(hooks.onDragUpdate).toHaveBeenCalledWith(expected, { - announce: expect.any(Function), - }); - }); - - describe('announcements', () => { - const destination: DraggableLocation = { - // new index - index: preset.inHome1.descriptor.index + 1, - // different droppable - droppableId: preset.inHome1.descriptor.droppableId, - }; - const updateImpact: DragImpact = { - movement: noMovement, - direction: preset.home.axis.direction, - destination, - }; - const dragUpdate: DragUpdate = { - draggableId: start.draggableId, - type: start.type, - source: start.source, - destination, - }; - const inHome = withImpact(state.dragging(), inHomeImpact); - const withUpdate = withImpact(state.dragging(), updateImpact); - - const perform = (myHooks: Hooks) => { - caller.onStateChange(myHooks, inHome, withUpdate); - }; - - beforeEach(() => { - // from the lift - expect(announceMock).toHaveBeenCalledTimes(1); - // clear its state - announceMock.mockReset(); - }); - - it('should announce with the default update message if no message is provided', () => { - caller.onStateChange(hooks, inHome, withUpdate); - - expect(announceMock).toHaveBeenCalledWith(messagePreset.onDragUpdate(dragUpdate)); - }); - - it('should announce with the default update message if no onDragUpdate hook is provided', () => { - const customHooks: Hooks = { - onDragEnd: jest.fn(), - }; - - perform(customHooks); - - expect(announceMock).toHaveBeenCalledWith(messagePreset.onDragUpdate(dragUpdate)); - }); - - it('should announce with a provided message', () => { - const customHooks: Hooks = { - onDragUpdate: (update: DragUpdate, provided: HookProvided) => provided.announce('test'), - onDragEnd: jest.fn(), - }; - - perform(customHooks); - - expect(announceMock).toHaveBeenCalledTimes(1); - expect(announceMock).toHaveBeenCalledWith('test'); - }); - - it('should prevent double announcing', () => { - let myAnnounce: ?Announce; - const customHooks: Hooks = { - onDragUpdate: (update: DragUpdate, provided: HookProvided) => { - myAnnounce = provided.announce; - myAnnounce('test'); - }, - onDragEnd: jest.fn(), - }; - - perform(customHooks); - - expect(announceMock).toHaveBeenCalledWith('test'); - expect(announceMock).toHaveBeenCalledTimes(1); - expect(console.warn).not.toHaveBeenCalled(); - - if (!myAnnounce) { - throw new Error('Invalid test setup'); - } - - myAnnounce('second'); - - expect(announceMock).toHaveBeenCalledTimes(1); - expect(console.warn).toHaveBeenCalled(); - }); - - it('should prevent async announcing', () => { - const customHooks: Hooks = { - onDragUpdate: (update: DragUpdate, provided: HookProvided) => { - setTimeout(() => { - // boom - provided.announce('too late'); - }); - }, - onDragEnd: jest.fn(), - }; - - perform(customHooks); - - expect(announceMock).toHaveBeenCalledWith(messagePreset.onDragUpdate(dragUpdate)); - expect(console.warn).not.toHaveBeenCalled(); - - // not releasing the async message - jest.runOnlyPendingTimers(); - - expect(announceMock).toHaveBeenCalledTimes(1); - expect(console.warn).toHaveBeenCalled(); - }); - }); - }); - - describe('no longer in home location', () => { - const firstImpact: DragImpact = { - movement: noMovement, - direction: preset.home.axis.direction, - // moved into the second index - destination: { - index: preset.inHome1.descriptor.index + 1, - droppableId: preset.inHome1.descriptor.droppableId, - }, - }; - - beforeEach(() => { - // initial lift - caller.onStateChange( - hooks, - state.requesting(), - withImpact(state.dragging(), inHomeImpact), - ); - // checking everything is well - expect(hooks.onDragStart).toHaveBeenCalled(); - expect(hooks.onDragUpdate).not.toHaveBeenCalled(); - - // first move into new location - caller.onStateChange( - hooks, - withImpact(state.dragging(), inHomeImpact), - withImpact(state.dragging(), firstImpact), - ); - - expect(hooks.onDragUpdate).toHaveBeenCalled(); - // cleaning the hook - // $ExpectError - no mock reset property - hooks.onDragUpdate.mockReset(); - }); - - it('should not provide an update if the location has not changed since the last drag', () => { - // drag to the same spot - caller.onStateChange( - hooks, - withImpact(state.dragging(), firstImpact), - withImpact(state.dragging(), firstImpact), - ); - - expect(hooks.onDragUpdate).not.toHaveBeenCalled(); - }); - - it('should provide an update if the index changes', () => { - const destination: DraggableLocation = { - index: preset.inHome1.descriptor.index + 2, - droppableId: preset.inHome1.descriptor.droppableId, - }; - const secondImpact: DragImpact = { - movement: noMovement, - direction: preset.home.axis.direction, - destination, - }; - const expected: DragUpdate = { - draggableId: start.draggableId, - type: start.type, - source: start.source, - destination, - }; - - // drag to the same spot - caller.onStateChange( - hooks, - withImpact(state.dragging(), firstImpact), - withImpact(state.dragging(), secondImpact), - ); - - expect(hooks.onDragUpdate).toHaveBeenCalledWith(expected, { - announce: expect.any(Function), - }); - }); - - it('should provide an update if the droppable changes', () => { - const destination: DraggableLocation = { - index: preset.inHome1.descriptor.index + 1, - droppableId: preset.foreign.descriptor.id, - }; - const secondImpact: DragImpact = { - movement: noMovement, - direction: preset.home.axis.direction, - destination, - }; - const expected: DragUpdate = { - draggableId: start.draggableId, - type: start.type, - source: start.source, - destination, - }; - - // drag to the same spot - caller.onStateChange( - hooks, - withImpact(state.dragging(), firstImpact), - withImpact(state.dragging(), secondImpact), - ); - - expect(hooks.onDragUpdate).toHaveBeenCalledWith(expected, { - announce: expect.any(Function), - }); - }); - - it('should provide an update if moving from a droppable to nothing', () => { - const secondImpact: DragImpact = { - movement: noMovement, - direction: null, - destination: null, - }; - const expected: DragUpdate = { - draggableId: start.draggableId, - type: start.type, - source: start.source, - destination: null, - }; - - // drag to the same spot - caller.onStateChange( - hooks, - withImpact(state.dragging(), firstImpact), - withImpact(state.dragging(), secondImpact), - ); - - expect(hooks.onDragUpdate).toHaveBeenCalledWith(expected, { - announce: expect.any(Function), - }); - }); - - it('should provide an update if moving back to the home location', () => { - const impact: DragImpact = { - movement: noMovement, - direction: preset.home.axis.direction, - destination: null, - }; - - // drag to nowhere - caller.onStateChange( - hooks, - withImpact(state.dragging(), inHomeImpact), - withImpact(state.dragging(), impact), - ); - const first: DragUpdate = { - draggableId: start.draggableId, - type: start.type, - source: start.source, - destination: null, - }; - - expect(hooks.onDragUpdate).toHaveBeenCalledWith(first, { - announce: expect.any(Function), - }); - - // drag back to home - caller.onStateChange( - hooks, - withImpact(state.dragging(), impact), - withImpact(state.dragging(), inHomeImpact), - ); - const second: DragUpdate = { - draggableId: start.draggableId, - type: start.type, - source: start.source, - destination: start.source, - }; - expect(hooks.onDragUpdate).toHaveBeenCalledWith(second, { - announce: expect.any(Function), - }); - }); - - describe('announcements', () => { - const destination: DraggableLocation = { - // new index - index: preset.inHome1.descriptor.index + 2, - // different droppable - droppableId: preset.inHome1.descriptor.droppableId, - }; - const secondImpact: DragImpact = { - movement: noMovement, - direction: preset.home.axis.direction, - destination, - }; - const secondUpdate: DragUpdate = { - draggableId: start.draggableId, - type: start.type, - source: start.source, - destination, - }; - const withFirstUpdate = withImpact(state.dragging(), firstImpact); - const withSecondUpdate = withImpact(state.dragging(), secondImpact); - - const perform = (myHooks: Hooks) => { - caller.onStateChange(myHooks, withFirstUpdate, withSecondUpdate); - }; - - beforeEach(() => { - // clear its state from previous updates - announceMock.mockReset(); - }); - - it('should announce with the default update message if no message is provided', () => { - caller.onStateChange(hooks, withFirstUpdate, withSecondUpdate); - - expect(announceMock).toHaveBeenCalledWith(messagePreset.onDragUpdate(secondUpdate)); - }); - - it('should announce with the default update message if no onDragUpdate hook is provided', () => { - const customHooks: Hooks = { - onDragEnd: jest.fn(), - }; - - perform(customHooks); - - expect(announceMock).toHaveBeenCalledWith(messagePreset.onDragUpdate(secondUpdate)); - }); - - it('should announce with a provided message', () => { - const customHooks: Hooks = { - onDragUpdate: (update: DragUpdate, provided: HookProvided) => provided.announce('test'), - onDragEnd: jest.fn(), - }; - - perform(customHooks); - - expect(announceMock).toHaveBeenCalledTimes(1); - expect(announceMock).toHaveBeenCalledWith('test'); - }); - - it('should prevent double announcing', () => { - let myAnnounce: ?Announce; - const customHooks: Hooks = { - onDragUpdate: (update: DragUpdate, provided: HookProvided) => { - myAnnounce = provided.announce; - myAnnounce('test'); - }, - onDragEnd: jest.fn(), - }; - - perform(customHooks); - - expect(announceMock).toHaveBeenCalledWith('test'); - expect(announceMock).toHaveBeenCalledTimes(1); - expect(console.warn).not.toHaveBeenCalled(); - - if (!myAnnounce) { - throw new Error('Invalid test setup'); - } - - myAnnounce('second'); - - expect(announceMock).toHaveBeenCalledTimes(1); - expect(console.warn).toHaveBeenCalled(); - }); - - it('should prevent async announcing', () => { - const customHooks: Hooks = { - onDragUpdate: (update: DragUpdate, provided: HookProvided) => { - setTimeout(() => { - // boom - provided.announce('too late'); - }); - }, - onDragEnd: jest.fn(), - }; - - perform(customHooks); - - expect(announceMock).toHaveBeenCalledWith(messagePreset.onDragUpdate(secondUpdate)); - expect(console.warn).not.toHaveBeenCalled(); - - // not releasing the async message - jest.runOnlyPendingTimers(); - - expect(announceMock).toHaveBeenCalledTimes(1); - expect(console.warn).toHaveBeenCalled(); - }); - }); - }); - - describe('multiple updates', () => { - it('should correctly update across multiple updates', () => { - // initial lift - caller.onStateChange( - hooks, - state.requesting(), - withImpact(state.dragging(), inHomeImpact), - ); - // checking everything is well - expect(hooks.onDragStart).toHaveBeenCalled(); - expect(hooks.onDragUpdate).not.toHaveBeenCalled(); - - // first move into new location - const firstImpact: DragImpact = { - movement: noMovement, - direction: preset.home.axis.direction, - // moved into the second index - destination: { - index: preset.inHome1.descriptor.index + 1, - droppableId: preset.inHome1.descriptor.droppableId, - }, - }; - caller.onStateChange( - hooks, - withImpact(state.dragging(), inHomeImpact), - withImpact(state.dragging(), firstImpact), - ); - - expect(hooks.onDragUpdate).toHaveBeenCalledTimes(1); - expect(hooks.onDragUpdate).toHaveBeenCalledWith({ - draggableId: start.draggableId, - type: start.type, - source: start.source, - destination: firstImpact.destination, - }, { announce: expect.any(Function) }); - - // second move into new location - const secondImpact: DragImpact = { - movement: noMovement, - direction: preset.home.axis.direction, - // moved into the second index - destination: { - index: preset.inHome1.descriptor.index + 2, - droppableId: preset.inHome1.descriptor.droppableId, - }, - }; - caller.onStateChange( - hooks, - withImpact(state.dragging(), firstImpact), - withImpact(state.dragging(), secondImpact), - ); - - expect(hooks.onDragUpdate).toHaveBeenCalledTimes(2); - expect(hooks.onDragUpdate).toHaveBeenCalledWith({ - draggableId: start.draggableId, - type: start.type, - source: start.source, - destination: secondImpact.destination, - }, { announce: expect.any(Function) }); - }); - - it('should update correctly across multiple drags', () => { - // initial lift - caller.onStateChange( - hooks, - state.requesting(), - withImpact(state.dragging(), inHomeImpact), - ); - // checking everything is well - expect(hooks.onDragStart).toHaveBeenCalled(); - expect(hooks.onDragUpdate).not.toHaveBeenCalled(); - - // first move into new location - const firstImpact: DragImpact = { - movement: noMovement, - direction: preset.home.axis.direction, - // moved into the second index - destination: { - index: preset.inHome1.descriptor.index + 1, - droppableId: preset.inHome1.descriptor.droppableId, - }, - }; - caller.onStateChange( - hooks, - withImpact(state.dragging(), inHomeImpact), - withImpact(state.dragging(), firstImpact), - ); - expect(hooks.onDragUpdate).toHaveBeenCalledTimes(1); - expect(hooks.onDragUpdate).toHaveBeenCalledWith({ - draggableId: start.draggableId, - type: start.type, - source: start.source, - destination: firstImpact.destination, - }, { announce: expect.any(Function) }); - // resetting the mock - // $ExpectError - resetting mock - hooks.onDragUpdate.mockReset(); - - // drop - caller.onStateChange( - hooks, - withImpact(state.dragging(), firstImpact), - state.idle, - ); - - expect(hooks.onDragUpdate).not.toHaveBeenCalled(); - - // a new lift! - caller.onStateChange( - hooks, - state.requesting(), - withImpact(state.dragging(), inHomeImpact), - ); - // checking everything is well - expect(hooks.onDragStart).toHaveBeenCalled(); - expect(hooks.onDragUpdate).not.toHaveBeenCalled(); - - // first move into new location - caller.onStateChange( - hooks, - withImpact(state.dragging(), inHomeImpact), - withImpact(state.dragging(), firstImpact), - ); - expect(hooks.onDragUpdate).toHaveBeenCalledTimes(1); - expect(hooks.onDragUpdate).toHaveBeenCalledWith({ - draggableId: start.draggableId, - type: start.type, - source: start.source, - destination: firstImpact.destination, - }, { announce: expect.any(Function) }); - }); - }); - }); - - describe('drag end', () => { - // it is possible to complete a drag from a DRAGGING or DROP_ANIMATING (drop or cancel) - const preEndStates: State[] = [ - state.dragging(), - state.dropAnimating(), - state.userCancel(), - ]; - - preEndStates.forEach((previous: State, index: number): void => { - describe(`end state ${index}`, () => { - it('should call onDragEnd with the drop result', () => { - const result: DropResult = { - draggableId: preset.inHome1.descriptor.id, - type: preset.home.descriptor.type, - source: { - droppableId: preset.inHome1.descriptor.droppableId, - index: preset.inHome1.descriptor.index, - }, - destination: { - droppableId: preset.inHome1.descriptor.droppableId, - index: preset.inHome1.descriptor.index + 1, - }, - reason: 'DROP', - }; - const current: State = { - phase: 'DROP_COMPLETE', - drop: { - pending: null, - result, - }, - drag: null, - dimension: noDimensions, - }; - - caller.onStateChange(hooks, previous, current); - - if (!current.drop || !current.drop.result) { - throw new Error('invalid state'); - } - - const provided: DropResult = current.drop.result; - expect(hooks.onDragEnd).toHaveBeenCalledWith(provided, { - announce: expect.any(Function), - }); - }); - - it('should log an error and not call the callback if there is no drop result', () => { - const invalid: State = { - ...state.dropComplete(), - drop: null, - }; - - caller.onStateChange(hooks, previous, invalid); - - expect(hooks.onDragEnd).not.toHaveBeenCalled(); - expect(console.error).toHaveBeenCalled(); - }); - - it('should call onDragEnd with null as the destination if there is no destination', () => { - const result: DropResult = { - draggableId: preset.inHome1.descriptor.id, - type: preset.home.descriptor.type, - source: { - droppableId: preset.inHome1.descriptor.droppableId, - index: preset.inHome1.descriptor.index, - }, - destination: null, - reason: 'DROP', - }; - const current: State = { - phase: 'DROP_COMPLETE', - drop: { - pending: null, - result, - }, - drag: null, - dimension: noDimensions, - }; - - caller.onStateChange(hooks, previous, current); - - expect(hooks.onDragEnd).toHaveBeenCalledWith(result, { - announce: expect.any(Function), - }); - }); - - it('should call onDragEnd with original source if the item did not move', () => { - const source: DraggableLocation = { - droppableId: preset.inHome1.descriptor.droppableId, - index: preset.inHome1.descriptor.index, - }; - const result: DropResult = { - draggableId: preset.inHome1.descriptor.id, - type: preset.home.descriptor.type, - source, - destination: source, - reason: 'DROP', - }; - const current: State = { - phase: 'DROP_COMPLETE', - drop: { - pending: null, - result, - }, - drag: null, - dimension: noDimensions, - }; - const expected: DropResult = { - draggableId: result.draggableId, - type: result.type, - source: result.source, - // destination has been cleared - destination: source, - reason: 'DROP', - }; - - caller.onStateChange(hooks, previous, current); - - expect(hooks.onDragEnd).toHaveBeenCalledWith(expected, { - announce: expect.any(Function), - }); - }); - - describe('announcements', () => { - const result: DropResult = { - draggableId: preset.inHome1.descriptor.id, - type: preset.home.descriptor.type, - source: { - droppableId: preset.inHome1.descriptor.droppableId, - index: preset.inHome1.descriptor.index, - }, - destination: { - droppableId: preset.inHome1.descriptor.droppableId, - index: preset.inHome1.descriptor.index + 1, - }, - reason: 'DROP', - }; - const current: State = { - phase: 'DROP_COMPLETE', - drop: { - pending: null, - result, - }, - drag: null, - dimension: noDimensions, - }; - - const perform = (myHooks: Hooks) => { - caller.onStateChange(myHooks, previous, current); - }; - - beforeEach(() => { - // clear its state from previous updates - announceMock.mockReset(); - }); - - it('should announce with the default update message if no message is provided', () => { - perform(hooks); - - expect(announceMock).toHaveBeenCalledWith(messagePreset.onDragEnd(result)); - }); - - it('should announce with the default update message if no onDragEnd hook is provided', () => { - const customHooks: Hooks = { - onDragEnd: jest.fn(), - }; - - perform(customHooks); - - expect(announceMock).toHaveBeenCalledWith(messagePreset.onDragEnd(result)); - }); - - it('should announce with a provided message', () => { - const customHooks: Hooks = { - onDragEnd: (drop: DropResult, provided: HookProvided) => provided.announce('the end'), - }; - - perform(customHooks); - - expect(announceMock).toHaveBeenCalledTimes(1); - expect(announceMock).toHaveBeenCalledWith('the end'); - }); - - it('should prevent double announcing', () => { - let myAnnounce: ?Announce; - const customHooks: Hooks = { - onDragEnd: (drop: DropResult, provided: HookProvided) => { - myAnnounce = provided.announce; - myAnnounce('test'); - }, - }; - - perform(customHooks); - - expect(announceMock).toHaveBeenCalledWith('test'); - expect(announceMock).toHaveBeenCalledTimes(1); - expect(console.warn).not.toHaveBeenCalled(); - - if (!myAnnounce) { - throw new Error('Invalid test setup'); - } - - myAnnounce('second'); - - expect(announceMock).toHaveBeenCalledTimes(1); - expect(console.warn).toHaveBeenCalled(); - }); - - it('should prevent async announcing', () => { - const customHooks: Hooks = { - onDragEnd: (drop: DropResult, provided: HookProvided) => { - setTimeout(() => { - // boom - provided.announce('too late'); - }); - }, - }; - - perform(customHooks); - - expect(announceMock).toHaveBeenCalledWith(messagePreset.onDragEnd(result)); - expect(console.warn).not.toHaveBeenCalled(); - - // not releasing the async message - jest.runOnlyPendingTimers(); - - expect(announceMock).toHaveBeenCalledTimes(1); - expect(console.warn).toHaveBeenCalled(); - }); - }); - }); - }); - }); - - describe('drag cleared', () => { - describe('cleared while dragging', () => { - const drop: DropResult = { - draggableId: preset.inHome1.descriptor.id, - type: preset.home.descriptor.type, - // $ExpectError - not checking for null - source: { - index: preset.inHome1.descriptor.index, - droppableId: preset.inHome1.descriptor.droppableId, - }, - destination: null, - reason: 'CANCEL', - }; - it('should return a result with a null destination', () => { - caller.onStateChange(hooks, state.dragging(), state.idle); - - expect(hooks.onDragEnd).toHaveBeenCalledWith(drop, { - announce: expect.any(Function), - }); - }); - - it('should log an error and do nothing if it cannot find a previous drag to publish', () => { - const invalid: State = { - phase: 'DRAGGING', - drag: null, - drop: null, - dimension: noDimensions, - }; - - caller.onStateChange(hooks, state.idle, invalid); - - expect(hooks.onDragEnd).not.toHaveBeenCalled(); - expect(console.error).toHaveBeenCalled(); - }); - - describe('announcements', () => { - const perform = (myHooks: Hooks) => { - caller.onStateChange(myHooks, state.dragging(), state.idle); - }; - - beforeEach(() => { - // clear its state from previous updates - announceMock.mockReset(); - }); - - it('should announce with the default update message if no message is provided', () => { - perform(hooks); - - expect(announceMock).toHaveBeenCalledWith(messagePreset.onDragEnd(drop)); - }); - - it('should announce with the default update message if no onDragEnd hook is provided', () => { - const customHooks: Hooks = { - onDragEnd: jest.fn(), - }; - - perform(customHooks); - - expect(announceMock).toHaveBeenCalledWith(messagePreset.onDragEnd(drop)); - }); - - it('should announce with a provided message', () => { - const customHooks: Hooks = { - onDragEnd: (dropResult: DropResult, provided: HookProvided) => provided.announce('the end'), - }; - - perform(customHooks); - - expect(announceMock).toHaveBeenCalledTimes(1); - expect(announceMock).toHaveBeenCalledWith('the end'); - }); - - it('should prevent double announcing', () => { - let myAnnounce: ?Announce; - const customHooks: Hooks = { - onDragEnd: (dropResult: DropResult, provided: HookProvided) => { - myAnnounce = provided.announce; - myAnnounce('test'); - }, - }; - - perform(customHooks); - - expect(announceMock).toHaveBeenCalledWith('test'); - expect(announceMock).toHaveBeenCalledTimes(1); - expect(console.warn).not.toHaveBeenCalled(); - - if (!myAnnounce) { - throw new Error('Invalid test setup'); - } - - myAnnounce('second'); - - expect(announceMock).toHaveBeenCalledTimes(1); - expect(console.warn).toHaveBeenCalled(); - }); - - it('should prevent async announcing', () => { - const customHooks: Hooks = { - onDragEnd: (dropResult: DropResult, provided: HookProvided) => { - setTimeout(() => { - // boom - provided.announce('too late'); - }); - }, - }; - - perform(customHooks); - - expect(announceMock).toHaveBeenCalledWith(messagePreset.onDragEnd(drop)); - expect(console.warn).not.toHaveBeenCalled(); - - // not releasing the async message - jest.runOnlyPendingTimers(); - - expect(announceMock).toHaveBeenCalledTimes(1); - expect(console.warn).toHaveBeenCalled(); - }); - }); - }); - - // this should never really happen - but just being safe - describe('cleared while drop animating', () => { - it('should return a result with a null destination', () => { - const expected: DropResult = { - draggableId: preset.inHome1.descriptor.id, - type: preset.home.descriptor.type, - source: { - index: preset.inHome1.descriptor.index, - droppableId: preset.inHome1.descriptor.droppableId, - }, - destination: null, - reason: 'CANCEL', - }; - - caller.onStateChange(hooks, state.dropAnimating(), state.idle); - - expect(hooks.onDragEnd).toHaveBeenCalledWith(expected, { - announce: expect.any(Function), - }); - }); - - it('should log an error and do nothing if it cannot find a previous drag to publish', () => { - const invalid: State = { - ...state.dropAnimating(), - drop: null, - }; - - caller.onStateChange(hooks, invalid, state.idle); - - expect(hooks.onDragEnd).not.toHaveBeenCalled(); - expect(console.error).toHaveBeenCalled(); - }); - }); - }); - - describe('phase unchanged', () => { - it('should not do anything if the previous and next phase are the same', () => { - Object.keys(state).forEach((key: string) => { - const current: State = state[key]; - - caller.onStateChange(hooks, current, current); - - expect(hooks.onDragStart).not.toHaveBeenCalled(); - expect(hooks.onDragUpdate).not.toHaveBeenCalled(); - expect(hooks.onDragEnd).not.toHaveBeenCalled(); - }); - }); - }); -}); diff --git a/test/unit/state/middleware/util/preset-action-args.js b/test/unit/state/middleware/util/preset-action-args.js index bc8324f30c..cf57669ddb 100644 --- a/test/unit/state/middleware/util/preset-action-args.js +++ b/test/unit/state/middleware/util/preset-action-args.js @@ -1,4 +1,5 @@ // @flow +import type { Position } from 'css-box-model'; import { getPreset } from '../../../../utils/dimension'; import getViewport from '../../../../../src/view/window/get-viewport'; import type { @@ -14,10 +15,20 @@ import type { } from '../../../../../src/state/action-creators'; const preset = getPreset(); +const origin: Position = { x: 0, y: 0 }; +const base: Viewport = getViewport(); export const viewport: Viewport = { - ...getViewport(), - scroll: preset.windowScroll, + frame: base.frame, + scroll: { + initial: preset.windowScroll, + current: preset.windowScroll, + max: base.scroll.max, + diff: { + value: origin, + displacement: origin, + }, + }, }; export const initialPublishArgs: InitialPublishArgs = { diff --git a/test/unit/state/move-to-next-index.spec.js b/test/unit/state/move-to-next-index.spec.js index 8d1033009c..dcdd9a6e18 100644 --- a/test/unit/state/move-to-next-index.spec.js +++ b/test/unit/state/move-to-next-index.spec.js @@ -9,6 +9,7 @@ import noImpact, { noMovement } from '../../../src/state/no-impact'; import { patch, subtract } from '../../../src/state/position'; import { vertical, horizontal } from '../../../src/state/axis'; import { isPartiallyVisible } from '../../../src/state/visibility/is-visible'; +import { createViewport } from '../../utils/viewport'; import type { Viewport, Axis, @@ -21,16 +22,17 @@ import type { const origin: Position = { x: 0, y: 0 }; -const customViewport: Viewport = { - subject: getRect({ +const customViewport: Viewport = createViewport({ + frame: getRect({ top: 0, left: 0, bottom: 1000, right: 1000, }), scroll: origin, - maxScroll: origin, -}; + scrollHeight: 1000, + scrollWidth: 1000, +}); describe('move to next index', () => { beforeEach(() => { @@ -620,28 +622,28 @@ describe('move to next index', () => { expect(isPartiallyVisible({ target: inHome6.page.marginBox, destination: scrolled, - viewport: customViewport.subject, + viewport: customViewport.frame, })).toBe(true); expect(isPartiallyVisible({ target: inHome5.page.marginBox, destination: scrolled, - viewport: customViewport.subject, + viewport: customViewport.frame, })).toBe(true); expect(isPartiallyVisible({ target: inHome4.page.marginBox, destination: scrolled, - viewport: customViewport.subject, + viewport: customViewport.frame, })).toBe(false); expect(isPartiallyVisible({ target: inHome3.page.marginBox, destination: scrolled, - viewport: customViewport.subject, + viewport: customViewport.frame, })).toBe(false); // this one will remain invisible expect(isPartiallyVisible({ target: inHome2.page.marginBox, destination: scrolled, - viewport: customViewport.subject, + viewport: customViewport.frame, })).toBe(false); const expected: DragImpact = { @@ -1034,7 +1036,7 @@ describe('move to next index', () => { index: 0, droppableId: droppable.descriptor.id, }, - borderBox: customViewport.subject, + borderBox: customViewport.frame, }); const outsideViewport: DraggableDimension = getDraggableDimension({ descriptor: { @@ -1044,10 +1046,10 @@ describe('move to next index', () => { }, borderBox: { // is bottom left of the viewport - top: customViewport.subject.bottom + 1, - right: customViewport.subject.right + 100, - left: customViewport.subject.right + 1, - bottom: customViewport.subject.bottom + 100, + top: customViewport.frame.bottom + 1, + right: customViewport.frame.right + 100, + left: customViewport.frame.right + 1, + bottom: customViewport.frame.bottom + 100, }, }); // inViewport is in its original position @@ -1124,8 +1126,8 @@ describe('move to next index', () => { borderBox: { top: 0, left: 0, - right: customViewport.subject.right - 100, - bottom: customViewport.subject.bottom - 100, + right: customViewport.frame.right - 100, + bottom: customViewport.frame.bottom - 100, }, }); const invisible: DraggableDimension = getDraggableDimension({ @@ -1135,10 +1137,10 @@ describe('move to next index', () => { droppableId: droppable.descriptor.id, }, borderBox: { - top: customViewport.subject.bottom + 1, - left: customViewport.subject.right + 1, - bottom: customViewport.subject.bottom + 100, - right: customViewport.subject.right + 100, + top: customViewport.frame.bottom + 1, + left: customViewport.frame.right + 1, + bottom: customViewport.frame.bottom + 100, + right: customViewport.frame.right + 100, }, }); // inViewport is in its original position diff --git a/test/unit/state/scroll-viewport.spec.js b/test/unit/state/scroll-viewport.spec.js new file mode 100644 index 0000000000..c1e077b698 --- /dev/null +++ b/test/unit/state/scroll-viewport.spec.js @@ -0,0 +1,100 @@ +// @flow +import { getRect, type Position, type Rect } from 'css-box-model'; +import type { Viewport } from '../../../src/types'; +import { add, negate } from '../../../src/state/position'; +import scrollViewport from '../../../src/state/scroll-viewport'; +import { offsetByPosition } from '../../../src/state/spacing'; + +const origin: Position = { x: 0, y: 0 }; + +it('should update the window details scroll', () => { + const original: Viewport = { + frame: getRect({ + top: 0, + left: 0, + right: 100, + bottom: 100, + }), + scroll: { + initial: origin, + current: origin, + max: { x: 1000, y: 1000 }, + diff: { + value: origin, + displacement: origin, + }, + }, + }; + const newScroll: Position = { x: 100, y: 50 }; + const expected: Viewport = { + frame: getRect({ + // shifted 50 + top: 50, + bottom: 150, + // shifted 100 + left: 100, + right: 200, + }), + scroll: { + initial: origin, + current: newScroll, + max: { x: 1000, y: 1000 }, + diff: { + value: newScroll, + displacement: negate(newScroll), + }, + }, + }; + + const updated: Viewport = scrollViewport(original, newScroll); + + expect(updated).toEqual(expected); +}); + +it('should correctly update scroll across multiple movements', () => { + const original: Rect = getRect({ + top: 0, + left: 0, + right: 100, + bottom: 100, + }); + + let lastViewport: Viewport = { + frame: original, + scroll: { + initial: origin, + current: origin, + max: { x: 1000, y: 1000 }, + diff: { + value: origin, + displacement: origin, + }, + }, + }; + + let lastScroll: Position = origin; + + Array.from({ length: 5 }).forEach(() => { + const newScroll: Position = add(lastScroll, { x: 10, y: 20 }); + const updated: Viewport = scrollViewport(lastViewport, newScroll); + + const expected: Viewport = { + frame: getRect(offsetByPosition(original, newScroll)), + scroll: { + initial: origin, + current: newScroll, + max: { x: 1000, y: 1000 }, + diff: { + value: newScroll, + displacement: negate(newScroll), + }, + }, + }; + expect(updated).toEqual(expected); + expect(updated.frame.top).toEqual(newScroll.y); + expect(updated.frame.left).toEqual(newScroll.x); + + lastScroll = newScroll; + lastViewport = updated; + }); +}); diff --git a/test/unit/state/scroll-window-details.spec.js b/test/unit/state/scroll-window-details.spec.js deleted file mode 100644 index 6f1ef204cc..0000000000 --- a/test/unit/state/scroll-window-details.spec.js +++ /dev/null @@ -1,42 +0,0 @@ -// @flow -import type { Position } from 'css-box-model'; -import type { WindowDetails, Viewport } from '../../../src/types'; -import { negate } from '../../../src/state/position'; -import scrollWindowDetails from '../../../src/state/scroll-window-details'; -import getViewport from '../../../src/view/window/get-viewport'; - -const origin: Position = { x: 0, y: 0 }; - -it('should update the window details scroll', () => { - const viewport: Viewport = getViewport(); - const original: WindowDetails = { - viewport, - scroll: { - initial: origin, - current: origin, - diff: { - value: origin, - displacement: origin, - }, - }, - }; - const newScroll: Position = { x: 100, y: 50 }; - const expected: WindowDetails = { - viewport: { - ...viewport, - scroll: newScroll, - }, - scroll: { - initial: origin, - current: newScroll, - diff: { - value: newScroll, - displacement: negate(newScroll), - }, - }, - }; - - const updated: WindowDetails = scrollWindowDetails(original, newScroll); - - expect(updated).toEqual(expected); -}); diff --git a/test/utils/get-simple-state-preset.js b/test/utils/get-simple-state-preset.js index 8204cddfaa..7d236229be 100644 --- a/test/utils/get-simple-state-preset.js +++ b/test/utils/get-simple-state-preset.js @@ -23,7 +23,6 @@ import type { ItemPositions, DragPositions, DraggingState, - WindowDetails, } from '../../src/types'; const scheduled: ScrollOptions = {