mirror of
https://github.com/philomena-dev/philomena.git
synced 2024-12-18 23:17:59 +01:00
306 lines
10 KiB
TypeScript
306 lines
10 KiB
TypeScript
import { clearDragSource, initDraggables } from '../draggable';
|
|
import { fireEvent } from '@testing-library/dom';
|
|
import { getRandomArrayItem } from '../../../test/randomness';
|
|
import { MockInstance } from 'vitest';
|
|
|
|
describe('Draggable Utilities', () => {
|
|
// jsdom lacks proper support for window.DragEvent so this is an attempt at a minimal recreation
|
|
const createDragEvent = (name: string, init?: DragEventInit): DragEvent => {
|
|
const mockEvent = new Event(name, { bubbles: true, cancelable: true }) as unknown as DragEvent;
|
|
let dataTransfer = init?.dataTransfer;
|
|
if (!dataTransfer) {
|
|
const items: Pick<DataTransferItem, 'type' | 'getAsString'>[] = [];
|
|
dataTransfer = {
|
|
items: items as unknown as DataTransferItemList,
|
|
setData(format: string, data: string) {
|
|
items.push({ type: format, getAsString: (callback: FunctionStringCallback) => callback(data) });
|
|
},
|
|
} as unknown as DataTransfer;
|
|
}
|
|
Object.assign(mockEvent, { dataTransfer });
|
|
return mockEvent;
|
|
};
|
|
|
|
const createDraggableElement = (): HTMLDivElement => {
|
|
const el = document.createElement('div');
|
|
el.setAttribute('draggable', 'true');
|
|
return el;
|
|
};
|
|
|
|
describe('initDraggables', () => {
|
|
const draggingClass = 'dragging';
|
|
const dragContainerClass = 'drag-container';
|
|
const dragOverClass = 'over';
|
|
let documentEventListenerSpy: MockInstance;
|
|
|
|
let mockDragContainer: HTMLDivElement;
|
|
let mockDraggable: HTMLDivElement;
|
|
|
|
beforeEach(() => {
|
|
mockDragContainer = document.createElement('div');
|
|
mockDragContainer.classList.add(dragContainerClass);
|
|
document.body.appendChild(mockDragContainer);
|
|
|
|
mockDraggable = createDraggableElement();
|
|
mockDragContainer.appendChild(mockDraggable);
|
|
|
|
// Redirect all document event listeners to this element for easier cleanup
|
|
documentEventListenerSpy = vi.spyOn(document, 'addEventListener').mockImplementation((...params) => {
|
|
mockDragContainer.addEventListener(...params);
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
document.body.removeChild(mockDragContainer);
|
|
documentEventListenerSpy.mockRestore();
|
|
});
|
|
|
|
describe('dragStart', () => {
|
|
it('should add the dragging class to the element that starts moving', () => {
|
|
initDraggables();
|
|
|
|
const mockEvent = createDragEvent('dragstart');
|
|
|
|
fireEvent(mockDraggable, mockEvent);
|
|
|
|
expect(mockDraggable).toHaveClass(draggingClass);
|
|
});
|
|
|
|
it("should add dummy data to the dragstart event if it's empty", () => {
|
|
initDraggables();
|
|
|
|
const mockEvent = createDragEvent('dragstart');
|
|
expect(mockEvent.dataTransfer?.items).toHaveLength(0);
|
|
|
|
fireEvent(mockDraggable, mockEvent);
|
|
|
|
expect(mockEvent.dataTransfer?.items).toHaveLength(1);
|
|
|
|
const dataTransferItem = (mockEvent.dataTransfer as DataTransfer).items[0];
|
|
expect(dataTransferItem.type).toEqual('text/plain');
|
|
|
|
let stringValue: string | undefined;
|
|
dataTransferItem.getAsString(value => {
|
|
stringValue = value;
|
|
});
|
|
expect(stringValue).toEqual('');
|
|
});
|
|
|
|
it("should keep data in the dragstart event if it's present", () => {
|
|
initDraggables();
|
|
|
|
const mockTransferItemType = getRandomArrayItem(['text/javascript', 'image/jpg', 'application/json']);
|
|
const mockDataTransferItem: DataTransferItem = {
|
|
type: mockTransferItemType,
|
|
} as unknown as DataTransferItem;
|
|
|
|
const mockEvent = createDragEvent('dragstart', {
|
|
dataTransfer: { items: [mockDataTransferItem] as unknown as DataTransferItemList },
|
|
} as DragEventInit);
|
|
expect(mockEvent.dataTransfer?.items).toHaveLength(1);
|
|
|
|
fireEvent(mockDraggable, mockEvent);
|
|
|
|
expect(mockEvent.dataTransfer?.items).toHaveLength(1);
|
|
|
|
const dataTransferItem = (mockEvent.dataTransfer as DataTransfer).items[0];
|
|
expect(dataTransferItem.type).toEqual(mockTransferItemType);
|
|
});
|
|
|
|
it('should set the allowed effect to move on the data transfer', () => {
|
|
initDraggables();
|
|
|
|
const mockEvent = createDragEvent('dragstart');
|
|
expect(mockEvent.dataTransfer?.effectAllowed).toBeFalsy();
|
|
|
|
fireEvent(mockDraggable, mockEvent);
|
|
|
|
expect(mockEvent.dataTransfer?.effectAllowed).toEqual('move');
|
|
});
|
|
|
|
it('should not throw if the event has no dataTransfer property', () => {
|
|
initDraggables();
|
|
|
|
const mockEvent = createDragEvent('dragstart');
|
|
delete (mockEvent as Record<keyof typeof mockEvent, unknown>).dataTransfer;
|
|
expect(() => fireEvent(mockDraggable, mockEvent)).not.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('dragOver', () => {
|
|
it('should cancel event and set the drop effect to move on the data transfer', () => {
|
|
initDraggables();
|
|
|
|
const mockEvent = createDragEvent('dragover');
|
|
|
|
fireEvent(mockDraggable, mockEvent);
|
|
|
|
expect(mockEvent.defaultPrevented).toBe(true);
|
|
expect(mockEvent.dataTransfer?.dropEffect).toEqual('move');
|
|
});
|
|
});
|
|
|
|
describe('dragEnter', () => {
|
|
it('should add the over class to the target', () => {
|
|
initDraggables();
|
|
|
|
const mockEvent = createDragEvent('dragenter');
|
|
|
|
fireEvent(mockDraggable, mockEvent);
|
|
|
|
expect(mockDraggable).toHaveClass(dragOverClass);
|
|
});
|
|
});
|
|
|
|
describe('dragLeave', () => {
|
|
it('should remove the over class from the target', () => {
|
|
initDraggables();
|
|
|
|
mockDraggable.classList.add(dragOverClass);
|
|
const mockEvent = createDragEvent('dragleave');
|
|
|
|
fireEvent(mockDraggable, mockEvent);
|
|
|
|
expect(mockDraggable).not.toHaveClass(dragOverClass);
|
|
});
|
|
});
|
|
|
|
describe('drop', () => {
|
|
it('should cancel the event and remove dragging class if dropped on same element', () => {
|
|
initDraggables();
|
|
|
|
const mockStartEvent = createDragEvent('dragstart');
|
|
fireEvent(mockDraggable, mockStartEvent);
|
|
|
|
expect(mockDraggable).toHaveClass(draggingClass);
|
|
|
|
const mockDropEvent = createDragEvent('drop');
|
|
fireEvent(mockDraggable, mockDropEvent);
|
|
|
|
expect(mockDropEvent.defaultPrevented).toBe(true);
|
|
expect(mockDraggable).not.toHaveClass(draggingClass);
|
|
});
|
|
|
|
it('should cancel the event and insert source before target if dropped on left side', () => {
|
|
initDraggables();
|
|
|
|
const mockSecondDraggable = createDraggableElement();
|
|
mockDragContainer.appendChild(mockSecondDraggable);
|
|
|
|
const mockStartEvent = createDragEvent('dragstart');
|
|
fireEvent(mockSecondDraggable, mockStartEvent);
|
|
|
|
expect(mockSecondDraggable).toHaveClass(draggingClass);
|
|
|
|
const mockDropEvent = createDragEvent('drop');
|
|
Object.assign(mockDropEvent, { clientX: 124 });
|
|
const boundingBoxSpy = vi.spyOn(mockDraggable, 'getBoundingClientRect').mockReturnValue({
|
|
left: 100,
|
|
width: 50,
|
|
} as unknown as DOMRect);
|
|
fireEvent(mockDraggable, mockDropEvent);
|
|
|
|
try {
|
|
expect(mockDropEvent.defaultPrevented).toBe(true);
|
|
expect(mockSecondDraggable).not.toHaveClass(draggingClass);
|
|
expect(mockSecondDraggable.nextElementSibling).toBe(mockDraggable);
|
|
} finally {
|
|
boundingBoxSpy.mockRestore();
|
|
}
|
|
});
|
|
|
|
it('should cancel the event and insert source after target if dropped on right side', () => {
|
|
initDraggables();
|
|
|
|
const mockSecondDraggable = createDraggableElement();
|
|
mockDragContainer.appendChild(mockSecondDraggable);
|
|
|
|
const mockStartEvent = createDragEvent('dragstart');
|
|
fireEvent(mockSecondDraggable, mockStartEvent);
|
|
|
|
expect(mockSecondDraggable).toHaveClass(draggingClass);
|
|
|
|
const mockDropEvent = createDragEvent('drop');
|
|
Object.assign(mockDropEvent, { clientX: 125 });
|
|
const boundingBoxSpy = vi.spyOn(mockDraggable, 'getBoundingClientRect').mockReturnValue({
|
|
left: 100,
|
|
width: 50,
|
|
} as unknown as DOMRect);
|
|
fireEvent(mockDraggable, mockDropEvent);
|
|
|
|
try {
|
|
expect(mockDropEvent.defaultPrevented).toBe(true);
|
|
expect(mockSecondDraggable).not.toHaveClass(draggingClass);
|
|
expect(mockDraggable.nextElementSibling).toBe(mockSecondDraggable);
|
|
} finally {
|
|
boundingBoxSpy.mockRestore();
|
|
}
|
|
});
|
|
|
|
it('should not throw if drag source element is not set', () => {
|
|
clearDragSource();
|
|
initDraggables();
|
|
|
|
const mockSecondDraggable = createDraggableElement();
|
|
mockDragContainer.appendChild(mockSecondDraggable);
|
|
|
|
const mockDropEvent = createDragEvent('drop');
|
|
fireEvent(mockDraggable, mockDropEvent);
|
|
|
|
expect(() => fireEvent(mockDraggable, mockDropEvent)).not.toThrow();
|
|
expect(mockDropEvent.defaultPrevented).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('dragEnd', () => {
|
|
it("should remove dragging class from source and over class from target's descendants", () => {
|
|
initDraggables();
|
|
|
|
const mockStartEvent = createDragEvent('dragstart');
|
|
fireEvent(mockDraggable, mockStartEvent);
|
|
|
|
expect(mockDraggable).toHaveClass(draggingClass);
|
|
|
|
const mockOverElement = createDraggableElement();
|
|
mockOverElement.classList.add(dragOverClass);
|
|
mockDraggable.parentNode?.appendChild(mockOverElement);
|
|
const mockOverEvent = createDragEvent('dragend');
|
|
fireEvent(mockOverElement, mockOverEvent);
|
|
|
|
const mockDropEvent = createDragEvent('dragend');
|
|
fireEvent(mockDraggable, mockDropEvent);
|
|
|
|
expect(mockDraggable).not.toHaveClass(draggingClass);
|
|
expect(mockOverElement).not.toHaveClass(dragOverClass);
|
|
});
|
|
});
|
|
|
|
describe('wrapper', () => {
|
|
it('should do nothing when event target has no closest method', () => {
|
|
initDraggables();
|
|
|
|
const mockEvent = createDragEvent('dragstart');
|
|
Object.assign(mockDraggable, { closest: undefined });
|
|
|
|
fireEvent(mockDraggable, mockEvent);
|
|
|
|
expect(mockEvent.dataTransfer?.effectAllowed).toBeFalsy();
|
|
});
|
|
|
|
it('should do nothing when event target does not have a parent matching the predefined selector', () => {
|
|
initDraggables();
|
|
|
|
const mockEvent = createDragEvent('dragstart');
|
|
const draggableClosestSpy = vi.spyOn(mockDraggable, 'closest').mockReturnValue(null);
|
|
|
|
try {
|
|
fireEvent(mockDraggable, mockEvent);
|
|
|
|
expect(mockEvent.dataTransfer?.effectAllowed).toBeFalsy();
|
|
} finally {
|
|
draggableClosestSpy.mockRestore();
|
|
}
|
|
});
|
|
});
|
|
});
|
|
});
|