This commit addresses failures in end-to-end tests that occurred due to
`ResizeObserver` loop limit exceptions.
These errors were triggered by Vue dependency upgrades in the commit
aae5434451.
The errors had the following message:
> `ResizeObserver loop completed with undelivered notifications`
This error happens when there are too many observations and the observer
is not able to deliver all observations within a single animation frame.
See: WICG/resize-observer#38
his commit resolves the issue by controlling how many observations are
delivered per animation frame and limiting it to only one.
It improves performance by reducing layout trashing, improving frame
rates, and managing resources more effectively.
Changes:
- Introduce an animation frame control to manage observations more
efficiently.
- Centralized `ResizeObserver` management within the `UseResizeObserver`
hook to improve consistency and reuse across the application.
270 lines
9.9 KiB
TypeScript
270 lines
9.9 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { ref, type Ref } from 'vue';
|
|
import { useDragHandler, type DragDomModifier } from '@/presentation/components/Scripts/Slider/UseDragHandler';
|
|
import { ThrottleStub } from '@tests/unit/shared/Stubs/ThrottleStub';
|
|
import { type ThrottleFunction } from '@/application/Common/Timing/Throttle';
|
|
import type { ConstructorArguments } from '@/TypeHelpers';
|
|
|
|
describe('useDragHandler', () => {
|
|
describe('initially', () => {
|
|
it('sets displacement X to 0', () => {
|
|
// arrange
|
|
const expectedValue = 0;
|
|
|
|
// act
|
|
const { displacementX } = initializeDragHandlerWithMocks();
|
|
|
|
// assert
|
|
expect(displacementX.value).to.equal(expectedValue);
|
|
});
|
|
it('sets dragging state to false', () => {
|
|
// arrange
|
|
const expectedValue = false;
|
|
|
|
// act
|
|
const { isDragging } = initializeDragHandlerWithMocks();
|
|
|
|
// assert
|
|
expect(isDragging.value).to.equal(expectedValue);
|
|
});
|
|
it('disables element touch action', () => {
|
|
// arrange
|
|
const expectedTouchAction = 'none';
|
|
const mockElement = document.createElement('div');
|
|
|
|
// act
|
|
initializeDragHandlerWithMocks({
|
|
draggableElementRef: ref(mockElement),
|
|
});
|
|
|
|
// assert
|
|
const actualTouchAction = mockElement.style.touchAction;
|
|
expect(actualTouchAction).to.equal(expectedTouchAction);
|
|
});
|
|
it('attaches event listener for drag start', () => {
|
|
// arrange
|
|
const expectedEventName: keyof HTMLElementEventMap = 'pointerdown';
|
|
let actualEventName: keyof HTMLElementEventMap | undefined;
|
|
const mockElement = document.createElement('div');
|
|
mockElement.addEventListener = (eventName: keyof HTMLElementEventMap) => {
|
|
actualEventName = eventName;
|
|
};
|
|
|
|
// act
|
|
initializeDragHandlerWithMocks({
|
|
draggableElementRef: ref(mockElement),
|
|
});
|
|
|
|
// assert
|
|
expect(expectedEventName).to.equal(actualEventName);
|
|
});
|
|
});
|
|
describe('on drag', () => {
|
|
it('activates dragging on start of drag', () => {
|
|
// arrange
|
|
const expectedDragState = true;
|
|
const mockElement = document.createElement('div');
|
|
|
|
// act
|
|
const { isDragging } = initializeDragHandlerWithMocks({
|
|
draggableElementRef: ref(mockElement),
|
|
});
|
|
mockElement.dispatchEvent(createMockPointerEvent('pointerdown', { clientX: 100 }));
|
|
|
|
// assert
|
|
expect(isDragging.value).to.equal(expectedDragState);
|
|
});
|
|
it('updates displacement during dragging', () => {
|
|
// arrange
|
|
const initialDragX = 100;
|
|
const finalDragX = 150;
|
|
const expectedDisplacementX = finalDragX - initialDragX;
|
|
const mockElement = document.createElement('div');
|
|
const dragDomModifierMock = new DragDomModifierMock();
|
|
|
|
// act
|
|
const { displacementX } = initializeDragHandlerWithMocks({
|
|
draggableElementRef: ref(mockElement),
|
|
dragDomModifier: dragDomModifierMock,
|
|
});
|
|
mockElement.dispatchEvent(createMockPointerEvent('pointerdown', { clientX: 100 }));
|
|
dragDomModifierMock.simulateEvent('pointermove', createMockPointerEvent('pointermove', { clientX: finalDragX }));
|
|
|
|
// assert
|
|
expect(displacementX.value).to.equal(expectedDisplacementX);
|
|
});
|
|
it('attaches event listeners', () => {
|
|
// arrange
|
|
const expectedEventNames: ReadonlyArray<keyof HTMLElementEventMap> = [
|
|
'pointerup',
|
|
'pointermove',
|
|
];
|
|
const mockElement = document.createElement('div');
|
|
const dragDomModifierMock = new DragDomModifierMock();
|
|
|
|
// act
|
|
initializeDragHandlerWithMocks({
|
|
draggableElementRef: ref(mockElement),
|
|
dragDomModifier: dragDomModifierMock,
|
|
});
|
|
mockElement.dispatchEvent(createMockPointerEvent('pointerdown', { clientX: 100 }));
|
|
|
|
// assert
|
|
const actualEventNames = [...dragDomModifierMock.events].map(([eventName]) => eventName);
|
|
expect(expectedEventNames).to.have.lengthOf(actualEventNames.length);
|
|
expect(expectedEventNames).to.have.members(actualEventNames);
|
|
});
|
|
describe('throttling', () => {
|
|
it('initializes event throttling', () => {
|
|
// arrange
|
|
const throttleStub = new ThrottleStub()
|
|
.withImmediateExecution(false);
|
|
const mockElement = document.createElement('div');
|
|
|
|
// act
|
|
initializeDragHandlerWithMocks({
|
|
draggableElementRef: ref(mockElement),
|
|
throttler: throttleStub.func,
|
|
});
|
|
|
|
// assert
|
|
expect(throttleStub.throttleInitializationCallArgs.length).to.equal(1);
|
|
});
|
|
it('sets a specific throttle time interval', () => {
|
|
// arrange
|
|
const expectedThrottleInMs = 15;
|
|
const throttleStub = new ThrottleStub()
|
|
.withImmediateExecution(false);
|
|
const mockElement = document.createElement('div');
|
|
|
|
// act
|
|
initializeDragHandlerWithMocks({
|
|
draggableElementRef: ref(mockElement),
|
|
throttler: throttleStub.func,
|
|
});
|
|
|
|
// assert
|
|
expect(throttleStub.throttleInitializationCallArgs.length).to.equal(1);
|
|
const [, actualThrottleInMs] = throttleStub.throttleInitializationCallArgs[0];
|
|
expect(expectedThrottleInMs).to.equal(actualThrottleInMs);
|
|
});
|
|
it('limits frequency of drag movement updates', () => {
|
|
// arrange
|
|
const expectedTotalThrottledEvents = 3;
|
|
const throttleStub = new ThrottleStub();
|
|
const mockElement = document.createElement('div');
|
|
const dragDomModifierMock = new DragDomModifierMock();
|
|
|
|
// act
|
|
initializeDragHandlerWithMocks({
|
|
draggableElementRef: ref(mockElement),
|
|
throttler: throttleStub.func,
|
|
dragDomModifier: dragDomModifierMock,
|
|
});
|
|
mockElement.dispatchEvent(createMockPointerEvent('pointerdown', { clientX: 100 }));
|
|
for (let i = 0; i < expectedTotalThrottledEvents; i++) {
|
|
dragDomModifierMock.simulateEvent('pointermove', createMockPointerEvent('pointermove', { clientX: 110 }));
|
|
}
|
|
|
|
// assert
|
|
const actualTotalThrottledEvents = throttleStub.throttledFunctionCallArgs.length;
|
|
expect(actualTotalThrottledEvents).to.equal(expectedTotalThrottledEvents);
|
|
});
|
|
it('calculates displacement considering throttling', () => {
|
|
// arrange
|
|
const throttleStub = new ThrottleStub().withImmediateExecution(true);
|
|
const mockElement = document.createElement('div');
|
|
const initialDragX = 100;
|
|
const firstDisplacementX = 10;
|
|
const dragDomModifierMock = new DragDomModifierMock();
|
|
|
|
// act
|
|
const { displacementX } = initializeDragHandlerWithMocks({
|
|
draggableElementRef: ref(mockElement),
|
|
throttler: throttleStub.func,
|
|
dragDomModifier: dragDomModifierMock,
|
|
});
|
|
mockElement.dispatchEvent(createMockPointerEvent('pointerdown', { clientX: initialDragX }));
|
|
dragDomModifierMock.simulateEvent('pointermove', createMockPointerEvent('pointermove', { clientX: initialDragX - firstDisplacementX }));
|
|
dragDomModifierMock.simulateEvent('pointermove', createMockPointerEvent('pointermove', { clientX: initialDragX - firstDisplacementX * 2 }));
|
|
throttleStub.executeFirst();
|
|
|
|
// assert
|
|
expect(displacementX.value).to.equal(firstDisplacementX);
|
|
});
|
|
});
|
|
});
|
|
describe('on drag end', () => {
|
|
it('deactivates dragging on drag end', () => {
|
|
// arrange
|
|
const expectedDraggingState = false;
|
|
const mockElement = document.createElement('div');
|
|
const dragDomModifierMock = new DragDomModifierMock();
|
|
|
|
// act
|
|
const { isDragging } = initializeDragHandlerWithMocks({
|
|
draggableElementRef: ref(mockElement),
|
|
dragDomModifier: dragDomModifierMock,
|
|
});
|
|
mockElement.dispatchEvent(createMockPointerEvent('pointerdown', { clientX: 100 }));
|
|
dragDomModifierMock.simulateEvent('pointerup', createMockPointerEvent('pointerup'));
|
|
|
|
// assert
|
|
expect(isDragging.value).to.equal(expectedDraggingState);
|
|
});
|
|
it('removes event listeners', () => {
|
|
// arrange
|
|
const mockElement = document.createElement('div');
|
|
const dragDomModifierMock = new DragDomModifierMock();
|
|
|
|
// act
|
|
initializeDragHandlerWithMocks({
|
|
draggableElementRef: ref(mockElement),
|
|
dragDomModifier: dragDomModifierMock,
|
|
});
|
|
mockElement.dispatchEvent(createMockPointerEvent('pointerdown', { clientX: 100 }));
|
|
dragDomModifierMock.simulateEvent('pointerup', createMockPointerEvent('pointerup'));
|
|
|
|
// assert
|
|
const actualEvents = [...dragDomModifierMock.events];
|
|
expect(actualEvents).to.have.lengthOf(0);
|
|
});
|
|
});
|
|
});
|
|
|
|
function initializeDragHandlerWithMocks(mocks?: {
|
|
readonly dragDomModifier?: DragDomModifier;
|
|
readonly draggableElementRef?: Ref<HTMLElement>;
|
|
readonly throttler?: ThrottleFunction,
|
|
}) {
|
|
return useDragHandler(
|
|
mocks?.draggableElementRef ?? ref(document.createElement('div')),
|
|
mocks?.dragDomModifier ?? new DragDomModifierMock(),
|
|
mocks?.throttler ?? new ThrottleStub().withImmediateExecution(true).func,
|
|
);
|
|
}
|
|
|
|
function createMockPointerEvent(...args: ConstructorArguments<typeof PointerEvent>): PointerEvent {
|
|
return new MouseEvent(...args) as PointerEvent; // jsdom does not support `PointerEvent` constructor, https://github.com/jsdom/jsdom/issues/2527
|
|
}
|
|
|
|
class DragDomModifierMock implements DragDomModifier {
|
|
public events = new Map<keyof DocumentEventMap, EventListener>();
|
|
|
|
public addEventListenerToDocument(type: keyof DocumentEventMap, handler: EventListener): void {
|
|
this.events.set(type, handler);
|
|
}
|
|
|
|
public removeEventListenerFromDocument(type: keyof DocumentEventMap): void {
|
|
this.events.delete(type);
|
|
}
|
|
|
|
public simulateEvent(type: keyof DocumentEventMap, event: Event) {
|
|
const handler = this.events.get(type);
|
|
if (!handler) {
|
|
throw new Error(`No event handler registered for: ${type}`);
|
|
}
|
|
handler(event);
|
|
}
|
|
}
|