Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dev to Main Sync #1321

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import { Provider } from 'react-redux';
import { store } from '@/app/store';
import { renderWithRouter } from '@/test_utils/createMockRouter';
import { fireEvent, screen } from '@testing-library/react';
import TooltipAutoPlacement from '@/components/common/TooltipAutoPlacement/Tooltip';

describe('Tooltip Auto Placement Component', () => {
beforeEach(() => {
global.innerWidth = 1024;
global.innerHeight = 768;
});

it('should render the tooltip on hover', () => {
renderWithRouter(
<Provider store={store()}>
<TooltipAutoPlacement content="This is tooltip">
<div data-testid="content">This is great</div>
</TooltipAutoPlacement>
</Provider>
);
const content = screen.getByTestId('content');
fireEvent.mouseOver(content);
const classList = screen.getByTestId('tooltip').classList;
expect(classList).toContain('fade-in');
});
it('should not have fade-in or fade-out classes on initial render', () => {
renderWithRouter(
<Provider store={store()}>
<TooltipAutoPlacement content="This is tooltip">
<div data-testid="content">This is great</div>
</TooltipAutoPlacement>
</Provider>
);
const classList = screen.getByTestId('tooltip').classList;
expect(classList).not.toContain('fade-in');
expect(classList).not.toContain('fade-out');
expect(classList).toContain('tooltip');
});
it('should not have fade-in class after hover and exit', () => {
renderWithRouter(
<Provider store={store()}>
<TooltipAutoPlacement content="This is tooltip">
<div data-testid="content">This is great</div>
</TooltipAutoPlacement>
</Provider>
);
const content = screen.getByTestId('content');
fireEvent.mouseOver(content);
fireEvent.mouseLeave(content);
const classList = screen.getByTestId('tooltip').classList;
expect(classList).not.toContain('fade-in');
});
it('should display the correct content in the tooltip', () => {
const tooltipContent = 'Expected Tooltip Content';

renderWithRouter(
<Provider store={store()}>
<TooltipAutoPlacement content={tooltipContent}>
<div data-testid="trigger">Trigger</div>
</TooltipAutoPlacement>
</Provider>
);

fireEvent.mouseOver(screen.getByTestId('trigger'));
const tooltip = screen.getByTestId('tooltip');

expect(tooltip).toBeVisible();
expect(tooltip.textContent).toBe(tooltipContent);
});
it('should handle small viewport sizes', () => {
global.innerWidth = 320;
global.innerHeight = 480;

renderWithRouter(
<Provider store={store()}>
<TooltipAutoPlacement content="Tooltip content">
<div
data-testid="trigger"
style={{ position: 'absolute', bottom: 0 }}
>
Trigger
</div>
</TooltipAutoPlacement>
</Provider>
);

fireEvent.mouseOver(screen.getByTestId('trigger'));

const tooltip = screen.getByTestId('tooltip');
const styles = window.getComputedStyle(tooltip);

expect(tooltip).toBeVisible();

const left = parseInt(styles.left, 10);
const top = parseInt(styles.top, 10);

expect(left).toBeGreaterThanOrEqual(0);
expect(top).toBeGreaterThanOrEqual(0);
expect(left).toBeLessThanOrEqual(global.innerWidth);
expect(top).toBeLessThanOrEqual(global.innerHeight);
expect(tooltip.textContent).toBe('Tooltip content');
});

it('should handle long content correctly', () => {
const longContent = 'A'.repeat(200);

renderWithRouter(
<Provider store={store()}>
<TooltipAutoPlacement content={longContent}>
<div data-testid="trigger">Trigger</div>
</TooltipAutoPlacement>
</Provider>
);

fireEvent.mouseOver(screen.getByTestId('trigger'));
const tooltip = screen.getByTestId('tooltip');

expect(tooltip).toBeVisible();
expect(tooltip.textContent).toBe(longContent);
});

it('should handle tooltip with complex content', () => {
const complexContent = (
<div>
<h3>Complex Title</h3>
<p>Multiple paragraphs</p>
<ul>
<li>List item 1</li>
<li>List item 2</li>
</ul>
</div>
);

renderWithRouter(
<Provider store={store()}>
<TooltipAutoPlacement content={complexContent}>
<div data-testid="trigger">Trigger</div>
</TooltipAutoPlacement>
</Provider>
);

fireEvent.mouseOver(screen.getByTestId('trigger'));
const tooltip = screen.getByTestId('tooltip');

expect(tooltip).toBeVisible();
expect(tooltip.querySelector('h3')).toBeInTheDocument();
expect(tooltip.querySelectorAll('li')).toHaveLength(2);
});

it('should reposition tooltip on viewport resize', () => {
renderWithRouter(
<Provider store={store()}>
<TooltipAutoPlacement content="Tooltip content">
<div data-testid="trigger">Trigger</div>
</TooltipAutoPlacement>
</Provider>
);

fireEvent.mouseOver(screen.getByTestId('trigger'));
global.innerWidth = 500;
global.innerHeight = 300;
window.dispatchEvent(new Event('resize'));

const tooltip = screen.getByTestId('tooltip');
const tooltipRect = tooltip.getBoundingClientRect();
expect(tooltipRect.right).toBeLessThanOrEqual(window.innerWidth);
expect(tooltipRect.bottom).toBeLessThanOrEqual(window.innerHeight);
});

it('should handle unusual placement (top-left corner)', async () => {
renderWithRouter(
<Provider store={store()}>
<TooltipAutoPlacement content="Tooltip content">
<div
data-testid="trigger"
style={{ position: 'absolute', top: 0, left: 0 }}
>
Trigger
</div>
</TooltipAutoPlacement>
</Provider>
);

fireEvent.mouseMove(screen.getByTestId('trigger'));
const tooltip = screen.getByTestId('tooltip');
const tooltipRect = tooltip.getBoundingClientRect();
expect(tooltipRect.left).toBeGreaterThanOrEqual(0);
expect(tooltipRect.top).toBeGreaterThanOrEqual(0);
});
});
18 changes: 1 addition & 17 deletions __tests__/Unit/pages/Tasks/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ describe('Tasks', () => {

render(
<Provider store={store()}>
<Tasks dev={true} />
<Tasks />
</Provider>
);
const skeletonContainer = screen.getByTestId('task-skeleton-container');
Expand All @@ -47,22 +47,6 @@ describe('Tasks', () => {
expect(shimmerCards).toHaveLength(5);
});

it('should display loading state when isLoading is true', () => {
(useGetAllTasksQuery as jest.Mock).mockReturnValue({
data: { tasks: [], next: '' },
isError: false,
isLoading: true,
isFetching: false,
});

render(
<Provider store={store()}>
<Tasks />
</Provider>
);
expect(screen.getByText('Loading...')).toBeInTheDocument();
});

it('should render the Tasks component', () => {
render(
<Provider store={store()}>
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"author": "",
"license": "ISC",
"dependencies": {
"@floating-ui/react-dom": "^2.1.2",
"@reduxjs/toolkit": "^1.9.1",
"@testing-library/user-event": "^14.5.2",
"axios": "^1.2.2",
Expand Down
137 changes: 137 additions & 0 deletions src/components/common/TooltipAutoPlacement/Tooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import {
cloneElement,
isValidElement,
ReactElement,
ReactNode,
useId,
useRef,
} from 'react';
import styles from './tooltip.module.scss';
import {
arrow,
autoUpdate,
flip,
offset,
shift,
useFloating,
} from '@floating-ui/react-dom';

type TooltipProps = {
children: ReactNode;
content: ReactNode;
};

/**
* TooltipAutoPlacement displays a tooltip with automatic positioning and arrow.
* Default position is top but can flip to bottom, right, or left based on viewport.
* Shows on hover with fade-in/out animations.
*
* @param content Content to display in tooltip
* @param children Element that triggers the tooltip
*/
const TooltipAutoPlacement = ({ content, children }: TooltipProps) => {
const arrowRef = useRef(null);
const tooltipId = useId();
const {
refs,
floatingStyles,
placement: position,
middlewareData: { arrow: { x: arrowX, y: arrowY } = {} },
} = useFloating({
placement: 'top',
transform: false,
middleware: [
offset(12),
flip({
fallbackPlacements: ['bottom', 'right', 'left'],
}),
shift(),
arrow({
element: arrowRef,
}),
],
whileElementsMounted: autoUpdate,
});

const tooltip = refs.floating.current;

const showTooltip = () => {
if (!tooltip) return;

tooltip.classList.remove(styles['fade-out']);
tooltip.classList.add(styles['fade-in']);
};

const hideTooltip = () => {
if (tooltip) {
tooltip.classList.remove(styles['fade-in']);
tooltip.classList.add(styles['fade-out']);
}
};

const tooltipEventHandlers = {
onMouseEnter: showTooltip,
onMouseMove: showTooltip,
onMouseLeave: hideTooltip,
};

const renderTriggerElement = () => {
if (isValidElement(children)) {
// If it's a React element, clone it and add the props
return cloneElement(children as ReactElement<any>, {
ref: refs.setReference,
className: `${children.props.className || ''} ${
styles['tooltip-container']
}`,
'aria-describedby': tooltipId,
...tooltipEventHandlers,
});
}

// If it's text or any other content, wrap it in a span
return (
<span
ref={refs.setReference}
className={styles['tooltip-container']}
aria-describedby={tooltipId}
{...tooltipEventHandlers}
>
{children}
</span>
);
};

const staticSide = {
top: 'bottom',
right: 'left',
bottom: 'top',
left: 'right',
}[position.split('-')[0] as 'top' | 'right' | 'bottom' | 'left'];

return (
<>
{renderTriggerElement()}
<div
ref={refs.setFloating}
style={floatingStyles}
className={styles.tooltip}
data-testid="tooltip"
role="tooltip"
id={tooltipId}
>
{content}
<div
ref={arrowRef}
className={styles.arrow}
style={{
left: arrowX != null ? `${arrowX}px` : '',
top: arrowY != null ? `${arrowY}px` : '',
[staticSide]: '-7px',
}}
/>
</div>
</>
);
};

export default TooltipAutoPlacement;
Loading
Loading