Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
510 changes: 510 additions & 0 deletions uui-components/src/navigation/__tests__/ScrollSpy.test.tsx

Large diffs are not rendered by default.

79 changes: 77 additions & 2 deletions uui-core/src/services/__tests__/ModalContext.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { ModalContext } from '../ModalContext';
import { ModalContext, ModalOperationCancelled } from '../ModalContext';
import { LayoutContext } from '../LayoutContext';

describe('ModalContext', () => {
Expand Down Expand Up @@ -46,6 +46,25 @@ describe('ModalContext', () => {
expect(context.getOperations().length).toBe(0);
});

it('should reject with ModalOperationCancelled when abort is called without argument', async () => {
const p = context.show(() => <div />);
expect(context.getOperations().length).toBe(1);
context.getOperations()[0].props.abort();

await expect(p).rejects.toBeInstanceOf(ModalOperationCancelled);
expect(context.getOperations().length).toBe(0);
});

it('should reject with provided value when abort is called with argument', async () => {
const error = new Error('Custom abort error');
const p = context.show(() => <div />);
expect(context.getOperations().length).toBe(1);
context.getOperations()[0].props.abort(error);

await expect(p).rejects.toBe(error);
expect(context.getOperations().length).toBe(0);
});

it('should have correct arguments', () => {
context = new ModalContext(new LayoutContext());

Expand All @@ -61,7 +80,7 @@ describe('ModalContext', () => {

context.show(() => <div />);
operation = context.getOperations()[1];
expect(operation.props.key).toBe((parseInt(key, 10) + 1).toString());
expect(operation.props.key).toBe((parseInt(key) + 1).toString());
expect(operation.props.depth).toBe(depth + 1);
expect(operation.props.zIndex).toBe(zIndex + 100);
});
Expand All @@ -77,4 +96,60 @@ describe('ModalContext', () => {
expect(context.getOperations().length).toBe(0);
expect(context.isModalOperationActive()).toBe(false);
});

it('should destroy context and close all operations', () => {
const render = () => <div />;
const handler = jest.fn();

context.show(render);
context.show(render);
expect(context.getOperations().length).toBe(2);

context.subscribe(handler);
expect(handler).toHaveBeenCalledTimes(0);

context.destroyContext();
expect(context.getOperations().length).toBe(0);
expect(context.isModalOperationActive()).toBe(false);

// Verify that handlers are cleared (BaseContext behavior)
context.update({});
expect(handler).toHaveBeenCalledTimes(0);
});

it('should create ModalAdapter component that wraps render function', () => {
const render = jest.fn(() => <div data-testid="modal" />);
const testParameters = { name: 'test' };

context.show(render, testParameters);
expect(context.getOperations().length).toBe(1);

const operation = context.getOperations()[0];
expect(operation.component).toBeDefined();
expect(typeof operation.component).toBe('function');

// Verify that ModalAdapter is a React component class
const ModalAdapter = operation.component!;
expect(ModalAdapter.prototype).toBeDefined();
expect(ModalAdapter.prototype.render).toBeDefined();
});

it('should pass parameters through show method', () => {
const render = jest.fn(() => <div />);
const testParameters = { userId: 123, action: 'delete' };

context.show(render, testParameters);
expect(context.getOperations().length).toBe(1);

const operation = context.getOperations()[0];
expect(operation.props.parameters).toBe(testParameters);
});

it('should return a promise from show method', () => {
const render = () => <div />;
const promise = context.show(render);

expect(promise).toBeInstanceOf(Promise);
expect(context.getOperations().length).toBe(1);
});
});
269 changes: 269 additions & 0 deletions uui-core/src/services/dnd/__tests__/DndActor.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
import * as React from 'react';
import { screen, fireEvent, waitFor, act } from '@epam/uui-test-utils';
import { DndActor, DndActorProps } from '../DndActor';
import { DndContext } from '../DndContext';
import { UuiContext } from '../../UuiContext';
import { UuiContexts } from '../../../types';
import { renderWithContextAsync } from '@epam/uui-test-utils';

jest.mock('../../../helpers/events', () => ({
isEventTargetInsideDraggable: jest.fn(() => false),
isEventTargetInsideInput: jest.fn(() => false),
releasePointerCaptureOnEventTarget: jest.fn(),
}));

jest.mock('../../../helpers/ssr', () => ({
isClientSide: true,
}));

jest.mock('../../../helpers/getOffset', () => ({
getOffset: jest.fn(() => ({
left: 100,
top: 200,
})),
}));

jest.mock('../../../helpers/events', () => ({
getScrollParentOfEventTarget: jest.fn(() => null),
isEventTargetInsideDraggable: jest.fn(() => false),
isEventTargetInsideInput: jest.fn(() => false),
releasePointerCaptureOnEventTarget: jest.fn(),
}));

describe('DndActor', () => {
let mockDndContext: DndContext;
let mockUuiContext: UuiContexts;
let mockRender: jest.Mock;
let mockCanAcceptDrop: jest.Mock;
let mockOnDrop: jest.Mock;

beforeEach(() => {
jest.clearAllMocks();
mockDndContext = new DndContext();
mockDndContext.init();
mockUuiContext = { uuiDnD: mockDndContext } as unknown as UuiContexts;

mockRender = jest.fn((params) => (
<div
data-testid="dnd-actor"
ref={ params.ref }
{ ...params.eventHandlers }
className={ params.classNames?.join(' ') }
>
{ params.isDraggable && 'Draggable' }
{ params.isDraggedOver && 'DraggedOver' }
{ params.isDropAccepted && 'DropAccepted' }
{ params.isDragGhost && 'DragGhost' }
</div>
));

mockCanAcceptDrop = jest.fn(() => ({
top: true,
bottom: true,
left: true,
right: true,
inside: true,
}));

mockOnDrop = jest.fn();
});

afterEach(() => {
mockDndContext.destroyContext();
});

const createDndActor = async (props: Partial<DndActorProps<any, any>> = {}) => {
const defaultProps: DndActorProps<any, any> = {
render: mockRender,
...props,
};

const wrapper = ({ children }: { children?: React.ReactNode }) => (
<UuiContext.Provider value={ mockUuiContext as any }>
{children}
</UuiContext.Provider>
);

const result = await renderWithContextAsync(
<DndActor { ...defaultProps } />,
{ wrapper },
);

return { result, mockRender, mockCanAcceptDrop, mockOnDrop };
};

describe('rendering', () => {
it('should render with draggable state when srcData is provided', async () => {
await createDndActor({ srcData: { id: 1 } });

expect(mockRender).toHaveBeenCalled();
const renderParams = mockRender.mock.calls[0][0];
expect(renderParams.isDraggable).toBe(true);
expect(renderParams.isDraggedOut).toBe(false);
expect(renderParams.isDraggedOver).toBe(false);
});

it('should render without draggable state when srcData is not provided', async () => {
await createDndActor({ srcData: null });

const renderParams = mockRender.mock.calls[0][0];
expect(renderParams.isDraggable).toBe(false);
});

it('should render with event handlers when srcData is provided', async () => {
await createDndActor({ srcData: { id: 1 } });

const renderParams = mockRender.mock.calls[0][0];
expect(renderParams.eventHandlers.onPointerDown).toBeDefined();
});

it('should render with drop handlers when canAcceptDrop is provided', async () => {
await createDndActor({
canAcceptDrop: mockCanAcceptDrop,
dstData: { id: 2 },
});

const renderParams = mockRender.mock.calls[0][0];
expect(renderParams.eventHandlers.onPointerEnter).toBeDefined();
expect(renderParams.eventHandlers.onPointerMove).toBeDefined();
expect(renderParams.eventHandlers.onPointerLeave).toBeDefined();
});
});

describe('drag start', () => {
it('should not start drag if movement is below threshold', async () => {
await createDndActor({ srcData: { id: 1 } });
const element = screen.getByTestId('dnd-actor');

fireEvent.pointerDown(element, {
clientX: 100,
clientY: 200,
button: 0,
});

jest.spyOn(mockDndContext, 'getMouseCoords').mockReturnValue({
mousePageX: 102,
mousePageY: 202,
mouseDx: 2,
mouseDy: 2,
mouseDxSmooth: 2,
mouseDySmooth: 2,
mouseDownPageX: 100,
mouseDownPageY: 200,
buttons: 1,
});

const moveEvent = new MouseEvent('pointermove', {
bubbles: true,
cancelable: true,
clientX: 102,
clientY: 202,
buttons: 1,
});
window.dispatchEvent(moveEvent);

await waitFor(() => {
expect(mockDndContext.isDragging).toBe(false);
}, { timeout: 100 });
});
});

describe('drop handling', () => {
it('should calculate position correctly', async () => {
await createDndActor({
canAcceptDrop: mockCanAcceptDrop,
dstData: { id: 2 },
});
const element = screen.getByTestId('dnd-actor');

await waitFor(() => {
expect(mockRender).toHaveBeenCalled();
});

jest.spyOn(element, 'getBoundingClientRect').mockReturnValue({
left: 100,
top: 200,
width: 200,
height: 200,
right: 300,
bottom: 400,
x: 100,
y: 200,
toJSON: jest.fn(),
} as DOMRect);

await act(async () => {
mockDndContext.startDrag(element, { id: 1 }, () => <div>Ghost</div>);
});

await waitFor(() => {
expect(mockDndContext.isDragging).toBe(true);
});

await waitFor(() => {
const renderParams = mockRender.mock.calls[mockRender.mock.calls.length - 1][0];
expect(renderParams.isDndInProgress).toBe(true);
}, { timeout: 1000 });
});
});

describe('getPosition', () => {
it.skip('should return inside when mouse is in center', async () => {
await createDndActor({
canAcceptDrop: mockCanAcceptDrop,
dstData: { id: 2 },
});
const element = screen.getByTestId('dnd-actor');

jest.spyOn(element, 'getBoundingClientRect').mockReturnValue({
left: 100,
top: 200,
width: 200,
height: 200,
right: 300,
bottom: 400,
x: 100,
y: 200,
toJSON: jest.fn(),
} as DOMRect);

mockDndContext.startDrag(element, { id: 1 }, () => <div>Ghost</div>);

await waitFor(() => {
expect(mockDndContext.isDragging).toBe(true);
});

fireEvent.pointerEnter(element, {
clientX: 200,
clientY: 300,
});

await waitFor(() => expect(mockCanAcceptDrop).toHaveBeenCalled());
await waitFor(() => {
const latestRenderParams = mockRender.mock.calls[mockRender.mock.calls.length - 1][0];
expect(latestRenderParams.position).toBe('inside');
}, { timeout: 1000 });
});
});

describe('cleanup', () => {
it('should cleanup on unmount', async () => {
const subscribeSpy = jest.spyOn(mockDndContext, 'subscribe');
const unsubscribeSpy = jest.spyOn(mockDndContext, 'unsubscribe');
const removeEventListenerSpy = jest.spyOn(window, 'removeEventListener');

const { result } = await createDndActor({ srcData: { id: 1 } });

await waitFor(() => {
expect(mockRender).toHaveBeenCalled();
});

expect(subscribeSpy).toHaveBeenCalled();

result.unmount();

await waitFor(() => expect(unsubscribeSpy).toHaveBeenCalled());
expect(removeEventListenerSpy).toHaveBeenCalled();
});
});
});
Loading