import React from 'react';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { render, fireEvent, act } from '@testing-library/react';
import { PixelTooltip } from '../../overlays/PixelTooltip';
describe('PixelTooltip — floating-ui upgrade', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('renders into document.body via portal (escapes ancestor container)', () => {
const { container } = render(
,
);
const tip = document.querySelector('[role="tooltip"]');
expect(tip).toBeTruthy();
// Portal escape: tip must NOT live inside the inline parent.
const inline = container.querySelector('[data-testid="inline-parent"]');
expect(inline?.contains(tip!)).toBe(false);
expect(document.body.contains(tip!)).toBe(true);
});
it('positions according to placement prop (uses floating-ui inline styles)', () => {
render(
,
);
const tip = document.querySelector('[role="tooltip"]') as HTMLElement;
expect(tip).toBeTruthy();
// floating-ui writes position via inline style — at minimum `position: absolute`.
expect(tip.style.position).toBe('absolute');
// floating-ui also writes top/left as px strings.
expect(typeof tip.style.top).toBe('string');
expect(typeof tip.style.left).toBe('string');
});
it('click trigger toggles open on click', () => {
render(
,
);
// Closed initially.
expect(document.querySelector('[role="tooltip"]')).toBeNull();
// Click trigger wrapper to open.
const wrapper = document.querySelector('[data-testid="trg"]')!.parentElement!;
act(() => { fireEvent.click(wrapper); });
expect(document.querySelector('[role="tooltip"]')).toBeTruthy();
// Click again to close.
act(() => { fireEvent.click(wrapper); });
expect(document.querySelector('[role="tooltip"]')).toBeNull();
});
it('focus trigger toggles open on focus + blur', () => {
render(
,
);
const wrapper = document.querySelector('[data-testid="trg"]')!.parentElement!;
act(() => { fireEvent.focus(wrapper); });
expect(document.querySelector('[role="tooltip"]')).toBeTruthy();
act(() => { fireEvent.blur(wrapper); });
expect(document.querySelector('[role="tooltip"]')).toBeNull();
});
it('honors delay.open before showing on hover', () => {
render(
,
);
const wrapper = document.querySelector('[data-testid="trg"]')!.parentElement!;
act(() => { fireEvent.mouseEnter(wrapper); });
// Still hidden right after enter.
expect(document.querySelector('[role="tooltip"]')).toBeNull();
// Advance < delay: still hidden.
act(() => { vi.advanceTimersByTime(400); });
expect(document.querySelector('[role="tooltip"]')).toBeNull();
// Advance past delay: now visible.
act(() => { vi.advanceTimersByTime(150); });
expect(document.querySelector('[role="tooltip"]')).toBeTruthy();
});
/* ──────────────────────────────────────────────────────────────────────
Backwards-compat — existing API surface must keep working.
────────────────────────────────────────────────────────────────────── */
it('backwards-compat: accepts `content` as a string (existing API)', () => {
render(
,
);
const tip = document.querySelector('[role="tooltip"]');
expect(tip).toBeTruthy();
expect(tip!.textContent).toBe('legacy string');
});
it('backwards-compat: accepts ReactNode content (new API)', () => {
render(
rich node} defaultOpen>
,
);
expect(document.querySelector('[data-testid="rich"]')).toBeTruthy();
});
it('backwards-compat: hover trigger by default (no trigger prop)', () => {
render(
,
);
const wrapper = document.querySelector('[data-testid="trg"]')!.parentElement!;
act(() => { fireEvent.mouseEnter(wrapper); });
expect(document.querySelector('[role="tooltip"]')).toBeTruthy();
act(() => { fireEvent.mouseLeave(wrapper); });
expect(document.querySelector('[role="tooltip"]')).toBeNull();
});
it('backwards-compat: numeric `delay` is treated as open-delay', () => {
render(
,
);
const wrapper = document.querySelector('[data-testid="trg"]')!.parentElement!;
act(() => { fireEvent.mouseEnter(wrapper); });
expect(document.querySelector('[role="tooltip"]')).toBeNull();
act(() => { vi.advanceTimersByTime(350); });
expect(document.querySelector('[role="tooltip"]')).toBeTruthy();
});
it('backwards-compat: aria-describedby is wired to tooltip id when open', () => {
render(
,
);
const wrapper = document.querySelector('[data-testid="trg"]')!.parentElement!;
const tip = document.querySelector('[role="tooltip"]')!;
expect(wrapper.getAttribute('aria-describedby')).toBe(tip.id);
});
/* ──────────────────────────────────────────────────────────────────────
Controlled / uncontrolled behaviour.
────────────────────────────────────────────────────────────────────── */
it('respects controlled `open` prop', () => {
const { rerender } = render(
,
);
expect(document.querySelector('[role="tooltip"]')).toBeNull();
rerender(
,
);
expect(document.querySelector('[role="tooltip"]')).toBeTruthy();
});
it('fires onOpenChange when click trigger toggles', () => {
const onOpenChange = vi.fn();
render(
,
);
const wrapper = document.querySelector('[data-testid="trg"]')!.parentElement!;
act(() => { fireEvent.click(wrapper); });
expect(onOpenChange).toHaveBeenCalledWith(true);
act(() => { fireEvent.click(wrapper); });
expect(onOpenChange).toHaveBeenLastCalledWith(false);
});
});