217 lines
6.2 KiB
JavaScript
217 lines
6.2 KiB
JavaScript
import { ref, nextTick } from 'vue';
|
|
import { useDropdownPosition } from '../useDropdownPosition';
|
|
|
|
vi.mock('@vueuse/core', () => ({
|
|
useElementBounding: vi.fn(elementRef => {
|
|
const top = ref(0);
|
|
const right = ref(0);
|
|
const bottom = ref(100);
|
|
const left = ref(0);
|
|
const width = ref(200);
|
|
const height = ref(50);
|
|
|
|
const update = vi.fn(() => {
|
|
if (elementRef && elementRef.value?.getBoundingClientRect) {
|
|
const rect = elementRef.value.getBoundingClientRect();
|
|
top.value = rect.top;
|
|
right.value = rect.right;
|
|
bottom.value = rect.bottom;
|
|
left.value = rect.left;
|
|
width.value = rect.width;
|
|
height.value = rect.height;
|
|
}
|
|
});
|
|
|
|
update();
|
|
|
|
return { top, right, bottom, left, width, height, x: left, y: top, update };
|
|
}),
|
|
useWindowSize: vi.fn(() => ({
|
|
width: ref(1024),
|
|
height: ref(768),
|
|
})),
|
|
}));
|
|
|
|
describe('useDropdownPosition', () => {
|
|
let triggerRef;
|
|
let dropdownRef;
|
|
let enabled;
|
|
let querySelectorSpy;
|
|
|
|
beforeEach(() => {
|
|
triggerRef = ref({
|
|
getBoundingClientRect: () => ({
|
|
top: 100,
|
|
right: 250,
|
|
bottom: 150,
|
|
left: 50,
|
|
width: 200,
|
|
height: 50,
|
|
}),
|
|
});
|
|
|
|
dropdownRef = ref({
|
|
getBoundingClientRect: () => ({
|
|
top: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
left: 0,
|
|
width: 250,
|
|
height: 200,
|
|
}),
|
|
});
|
|
|
|
enabled = ref(true);
|
|
|
|
querySelectorSpy = vi.spyOn(document, 'querySelector').mockReturnValue({
|
|
getAttribute: () => 'ltr',
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
querySelectorSpy.mockRestore();
|
|
});
|
|
|
|
it('should return default position when disabled', () => {
|
|
enabled.value = false;
|
|
|
|
const { position } = useDropdownPosition(triggerRef, dropdownRef, enabled);
|
|
|
|
expect(position.value.class).toBe('top-full mt-2');
|
|
expect(position.value.style).toEqual({});
|
|
});
|
|
|
|
it('should return default position when refs are null', () => {
|
|
triggerRef.value = null;
|
|
dropdownRef.value = null;
|
|
|
|
const { position } = useDropdownPosition(triggerRef, dropdownRef, enabled);
|
|
|
|
expect(position.value.class).toBe('top-full mt-2');
|
|
expect(position.value.style).toEqual({ left: '0px' });
|
|
});
|
|
|
|
it('should calculate position when enabled and refs exist', () => {
|
|
const { position } = useDropdownPosition(triggerRef, dropdownRef, enabled);
|
|
|
|
// Should return some position classes (not the default)
|
|
expect(position.value.class).toBeTruthy();
|
|
expect(typeof position.value.class).toBe('string');
|
|
});
|
|
|
|
it('should position dropdown below trigger when enough space', () => {
|
|
// Window height: 768, Trigger bottom: 150, Dropdown height: 200, Margin: 16
|
|
// Space below: 768 - 150 = 618px
|
|
// Required: 200 + 16 = 216px
|
|
// 618 >= 216, so position below
|
|
const { position } = useDropdownPosition(triggerRef, dropdownRef, enabled);
|
|
|
|
expect(position.value.class).toContain('top-full');
|
|
expect(position.value.class).not.toContain('bottom-full');
|
|
});
|
|
|
|
it('should position dropdown above trigger when insufficient space below', () => {
|
|
// Window height: 768, Trigger bottom: 750, Dropdown height: 200, Margin: 16
|
|
// Space below: 768 - 750 = 18px
|
|
// Required: 200 + 16 = 216px
|
|
// 18 < 216, so position above
|
|
triggerRef.value.getBoundingClientRect = () => ({
|
|
top: 700,
|
|
right: 250,
|
|
bottom: 750,
|
|
left: 50,
|
|
width: 200,
|
|
height: 50,
|
|
});
|
|
|
|
const { position } = useDropdownPosition(triggerRef, dropdownRef, enabled);
|
|
|
|
expect(position.value.class).toContain('bottom-full');
|
|
expect(position.value.class).not.toContain('top-full');
|
|
});
|
|
|
|
it('should align dropdown to left by default (LTR)', () => {
|
|
// Window width: 1024, Trigger left: 50, Dropdown width: 250
|
|
// Available right: 1024 - 50 = 974px
|
|
// Overflow: 250 - 974 = -724 (no overflow)
|
|
// Result: left: 0px
|
|
const { position } = useDropdownPosition(triggerRef, dropdownRef, enabled);
|
|
|
|
expect(position.value.style).toEqual({ left: '0px' });
|
|
});
|
|
|
|
it('should shift left when dropdown would overflow right edge (LTR)', () => {
|
|
// Window width: 1024, Trigger left: 800, Dropdown width: 250
|
|
// Available right: 1024 - 800 = 224px
|
|
// Overflow: 250 - 224 = 26px
|
|
// Result: left: -26px
|
|
triggerRef.value.getBoundingClientRect = () => ({
|
|
top: 100,
|
|
right: 1000,
|
|
bottom: 150,
|
|
left: 800,
|
|
width: 200,
|
|
height: 50,
|
|
});
|
|
|
|
const { position } = useDropdownPosition(triggerRef, dropdownRef, enabled);
|
|
|
|
expect(position.value.style).toEqual({ left: '-26px' });
|
|
});
|
|
|
|
it('should handle RTL layout', () => {
|
|
querySelectorSpy.mockReturnValue({
|
|
getAttribute: () => 'rtl',
|
|
});
|
|
|
|
const { position } = useDropdownPosition(triggerRef, dropdownRef, enabled);
|
|
|
|
expect(position.value.style).toHaveProperty('right');
|
|
});
|
|
|
|
it('should reactively update when enabled changes', async () => {
|
|
const { position } = useDropdownPosition(triggerRef, dropdownRef, enabled);
|
|
|
|
// Initially enabled should calculate position
|
|
const initialValue = position.value.class;
|
|
expect(initialValue).toBeTruthy();
|
|
expect(typeof initialValue).toBe('string');
|
|
|
|
// Disable - should return default
|
|
enabled.value = false;
|
|
await nextTick();
|
|
|
|
expect(position.value.class).toBe('top-full mt-2');
|
|
expect(position.value.style).toEqual({});
|
|
|
|
// Re-enable - should calculate again
|
|
enabled.value = true;
|
|
await nextTick();
|
|
|
|
expect(position.value.class).toBeTruthy();
|
|
expect(typeof position.value.class).toBe('string');
|
|
});
|
|
|
|
it('should provide updatePosition function', () => {
|
|
const { updatePosition } = useDropdownPosition(
|
|
triggerRef,
|
|
dropdownRef,
|
|
enabled
|
|
);
|
|
|
|
expect(updatePosition).toBeDefined();
|
|
expect(typeof updatePosition).toBe('function');
|
|
|
|
// Should not throw when called
|
|
expect(() => updatePosition()).not.toThrow();
|
|
});
|
|
|
|
it('should handle optional enabled parameter', () => {
|
|
// Should work without enabled parameter (backward compatibility)
|
|
const { position } = useDropdownPosition(triggerRef, dropdownRef);
|
|
|
|
// Should default to enabled (not return default classes)
|
|
expect(position.value.class).toBeTruthy();
|
|
});
|
|
});
|