From 9f762c78503f9e34a2c1e119f2dcae1c679773d5 Mon Sep 17 00:00:00 2001 From: Rijk van Zanten Date: Tue, 26 Aug 2025 17:47:01 -0400 Subject: [PATCH 1/9] Add tests for use-grid-template --- .../src/composables/use-grid-template.test.ts | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 packages/vue-split-panel/src/composables/use-grid-template.test.ts diff --git a/packages/vue-split-panel/src/composables/use-grid-template.test.ts b/packages/vue-split-panel/src/composables/use-grid-template.test.ts new file mode 100644 index 0000000..3a7a10c --- /dev/null +++ b/packages/vue-split-panel/src/composables/use-grid-template.test.ts @@ -0,0 +1,88 @@ +import type { ComputedRef } from 'vue'; +import type { UseGridTemplateOptions } from './use-grid-template'; +import { describe, expect, it } from 'vitest'; +import { computed, ref } from 'vue'; +import { useGridTemplate } from './use-grid-template'; + +describe('useGridTemplate', () => { + const createOptions = (overrides = {}): UseGridTemplateOptions => ({ + collapsed: ref(false), + minSizePercentage: computed(() => {}) as ComputedRef, + maxSizePercentage: computed(() => {}) as ComputedRef, + sizePercentage: computed(() => 50), + dividerSize: computed(() => 4), + primary: 'start', + direction: 'ltr', + orientation: 'horizontal', + ...overrides, + }); + + it('returns collapsed state when collapsed is true', () => { + const options = createOptions({ collapsed: ref(true) }); + const { gridTemplate } = useGridTemplate(options); + + expect(gridTemplate.value).toBe('0 4px auto'); + }); + + it('returns basic clamp template when no min/max constraints', () => { + const options = createOptions(); + const { gridTemplate } = useGridTemplate(options); + + expect(gridTemplate.value).toBe('clamp(0%, 50%, calc(100% - 4px)) 4px auto'); + }); + + it('returns complex clamp template with min/max constraints', () => { + const options = createOptions({ + minSizePercentage: computed(() => 20), + maxSizePercentage: computed(() => 80), + }); + const { gridTemplate } = useGridTemplate(options); + + expect(gridTemplate.value).toBe('clamp(0%, clamp(20%, 50%, 80%), calc(100% - 4px)) 4px auto'); + }); + + it('reverses order when primary is end and direction is ltr', () => { + const options = createOptions({ primary: 'end' }); + const { gridTemplate } = useGridTemplate(options); + + expect(gridTemplate.value).toBe('auto 4px clamp(0%, 50%, calc(100% - 4px))'); + }); + + it('reverses order when direction is rtl and primary is start', () => { + const options = createOptions({ direction: 'rtl' }); + const { gridTemplate } = useGridTemplate(options); + + expect(gridTemplate.value).toBe('auto 4px clamp(0%, 50%, calc(100% - 4px))'); + }); + + it('handles vertical orientation correctly', () => { + const options = createOptions({ orientation: 'vertical' }); + const { gridTemplate } = useGridTemplate(options); + + expect(gridTemplate.value).toBe('clamp(0%, 50%, calc(100% - 4px)) 4px auto'); + }); + + it('handles vertical orientation with end primary', () => { + const options = createOptions({ + orientation: 'vertical', + primary: 'end', + }); + const { gridTemplate } = useGridTemplate(options); + + expect(gridTemplate.value).toBe('auto 4px clamp(0%, 50%, calc(100% - 4px))'); + }); + + it('uses custom divider size', () => { + const options = createOptions({ dividerSize: computed(() => 8) }); + const { gridTemplate } = useGridTemplate(options); + + expect(gridTemplate.value).toBe('clamp(0%, 50%, calc(100% - 8px)) 8px auto'); + }); + + it('handles undefined primary as start', () => { + const options = createOptions({ primary: undefined }); + const { gridTemplate } = useGridTemplate(options); + + expect(gridTemplate.value).toBe('clamp(0%, 50%, calc(100% - 4px)) 4px auto'); + }); +}); From a567ca10f2c7c808064f31b7ae96bb26842dc558 Mon Sep 17 00:00:00 2001 From: Rijk van Zanten Date: Tue, 26 Aug 2025 17:59:15 -0400 Subject: [PATCH 2/9] Add tests for use keyboard --- .../src/composables/use-keyboard.test.ts | 312 ++++++++++++++++++ 1 file changed, 312 insertions(+) create mode 100644 packages/vue-split-panel/src/composables/use-keyboard.test.ts diff --git a/packages/vue-split-panel/src/composables/use-keyboard.test.ts b/packages/vue-split-panel/src/composables/use-keyboard.test.ts new file mode 100644 index 0000000..f7a0944 --- /dev/null +++ b/packages/vue-split-panel/src/composables/use-keyboard.test.ts @@ -0,0 +1,312 @@ +import { describe, expect, it, vi } from 'vitest'; +import { ref } from 'vue'; +import { useKeyboard } from './use-keyboard'; + +describe('useKeyboard', () => { + const createMockKeyboardEvent = (key: string, shiftKey = false): KeyboardEvent => { + const event = new KeyboardEvent('keydown', { key, shiftKey }); + vi.spyOn(event, 'preventDefault'); + return event; + }; + + it('should return handleKeydown function', () => { + const sizePercentage = ref(50); + const collapsed = ref(false); + const options = { + disabled: false, + collapsible: true, + primary: 'start' as const, + orientation: 'horizontal' as const, + }; + + const { handleKeydown } = useKeyboard(sizePercentage, collapsed, options); + + expect(typeof handleKeydown).toBe('function'); + }); + + it('should do nothing when disabled', () => { + const sizePercentage = ref(50); + const collapsed = ref(false); + const options = { + disabled: true, + collapsible: true, + primary: 'start' as const, + orientation: 'horizontal' as const, + }; + + const { handleKeydown } = useKeyboard(sizePercentage, collapsed, options); + const event = createMockKeyboardEvent('ArrowRight'); + + handleKeydown(event); + + expect(sizePercentage.value).toBe(50); + expect(event.preventDefault).not.toHaveBeenCalled(); + }); + + describe('horizontal orientation', () => { + it('should decrease size on ArrowLeft when primary is start', () => { + const sizePercentage = ref(50); + const collapsed = ref(false); + const options = { + disabled: false, + collapsible: true, + primary: 'start' as const, + orientation: 'horizontal' as const, + }; + + const { handleKeydown } = useKeyboard(sizePercentage, collapsed, options); + const event = createMockKeyboardEvent('ArrowLeft'); + + handleKeydown(event); + + expect(sizePercentage.value).toBe(49); + expect(event.preventDefault).toHaveBeenCalled(); + }); + + it('should increase size on ArrowRight when primary is start', () => { + const sizePercentage = ref(50); + const collapsed = ref(false); + const options = { + disabled: false, + collapsible: true, + primary: 'start' as const, + orientation: 'horizontal' as const, + }; + + const { handleKeydown } = useKeyboard(sizePercentage, collapsed, options); + const event = createMockKeyboardEvent('ArrowRight'); + + handleKeydown(event); + + expect(sizePercentage.value).toBe(51); + expect(event.preventDefault).toHaveBeenCalled(); + }); + + it('should increase size on ArrowLeft when primary is end', () => { + const sizePercentage = ref(50); + const collapsed = ref(false); + const options = { + disabled: false, + collapsible: true, + primary: 'end' as const, + orientation: 'horizontal' as const, + }; + + const { handleKeydown } = useKeyboard(sizePercentage, collapsed, options); + const event = createMockKeyboardEvent('ArrowLeft'); + + handleKeydown(event); + + expect(sizePercentage.value).toBe(51); + }); + }); + + describe('vertical orientation', () => { + it('should decrease size on ArrowUp when primary is start', () => { + const sizePercentage = ref(50); + const collapsed = ref(false); + const options = { + disabled: false, + collapsible: true, + primary: 'start' as const, + orientation: 'vertical' as const, + }; + + const { handleKeydown } = useKeyboard(sizePercentage, collapsed, options); + const event = createMockKeyboardEvent('ArrowUp'); + + handleKeydown(event); + + expect(sizePercentage.value).toBe(49); + }); + + it('should increase size on ArrowDown when primary is start', () => { + const sizePercentage = ref(50); + const collapsed = ref(false); + const options = { + disabled: false, + collapsible: true, + primary: 'start' as const, + orientation: 'vertical' as const, + }; + + const { handleKeydown } = useKeyboard(sizePercentage, collapsed, options); + const event = createMockKeyboardEvent('ArrowDown'); + + handleKeydown(event); + + expect(sizePercentage.value).toBe(51); + }); + }); + + describe('shift key modifier', () => { + it('should change by 10 when shift key is pressed', () => { + const sizePercentage = ref(50); + const collapsed = ref(false); + const options = { + disabled: false, + collapsible: true, + primary: 'start' as const, + orientation: 'horizontal' as const, + }; + + const { handleKeydown } = useKeyboard(sizePercentage, collapsed, options); + const event = createMockKeyboardEvent('ArrowRight', true); + + handleKeydown(event); + + expect(sizePercentage.value).toBe(60); + }); + }); + + describe('Home and End keys', () => { + it('should set to 0 on Home when primary is start', () => { + const sizePercentage = ref(50); + const collapsed = ref(false); + const options = { + disabled: false, + collapsible: true, + primary: 'start' as const, + orientation: 'horizontal' as const, + }; + + const { handleKeydown } = useKeyboard(sizePercentage, collapsed, options); + const event = createMockKeyboardEvent('Home'); + + handleKeydown(event); + + expect(sizePercentage.value).toBe(0); + }); + + it('should set to 100 on End when primary is start', () => { + const sizePercentage = ref(50); + const collapsed = ref(false); + const options = { + disabled: false, + collapsible: true, + primary: 'start' as const, + orientation: 'horizontal' as const, + }; + + const { handleKeydown } = useKeyboard(sizePercentage, collapsed, options); + const event = createMockKeyboardEvent('End'); + + handleKeydown(event); + + expect(sizePercentage.value).toBe(100); + }); + + it('should set to 100 on Home when primary is end', () => { + const sizePercentage = ref(50); + const collapsed = ref(false); + const options = { + disabled: false, + collapsible: true, + primary: 'end' as const, + orientation: 'horizontal' as const, + }; + + const { handleKeydown } = useKeyboard(sizePercentage, collapsed, options); + const event = createMockKeyboardEvent('Home'); + + handleKeydown(event); + + expect(sizePercentage.value).toBe(100); + }); + }); + + describe('Enter key and collapsible', () => { + it('should toggle collapsed state on Enter when collapsible is true', () => { + const sizePercentage = ref(50); + const collapsed = ref(false); + const options = { + disabled: false, + collapsible: true, + primary: 'start' as const, + orientation: 'horizontal' as const, + }; + + const { handleKeydown } = useKeyboard(sizePercentage, collapsed, options); + const event = createMockKeyboardEvent('Enter'); + + handleKeydown(event); + + expect(collapsed.value).toBe(true); + }); + + it('should not toggle collapsed state on Enter when collapsible is false', () => { + const sizePercentage = ref(50); + const collapsed = ref(false); + const options = { + disabled: false, + collapsible: false, + primary: 'start' as const, + orientation: 'horizontal' as const, + }; + + const { handleKeydown } = useKeyboard(sizePercentage, collapsed, options); + const event = createMockKeyboardEvent('Enter'); + + handleKeydown(event); + + expect(collapsed.value).toBe(false); + }); + }); + + describe('clamping values', () => { + it('should clamp size to 0 minimum', () => { + const sizePercentage = ref(2); + const collapsed = ref(false); + const options = { + disabled: false, + collapsible: true, + primary: 'start' as const, + orientation: 'horizontal' as const, + }; + + const { handleKeydown } = useKeyboard(sizePercentage, collapsed, options); + const event = createMockKeyboardEvent('ArrowLeft', true); + + handleKeydown(event); + + expect(sizePercentage.value).toBe(0); + }); + + it('should clamp size to 100 maximum', () => { + const sizePercentage = ref(98); + const collapsed = ref(false); + const options = { + disabled: false, + collapsible: true, + primary: 'start' as const, + orientation: 'horizontal' as const, + }; + + const { handleKeydown } = useKeyboard(sizePercentage, collapsed, options); + const event = createMockKeyboardEvent('ArrowRight', true); + + handleKeydown(event); + + expect(sizePercentage.value).toBe(100); + }); + }); + + it('should ignore non-handled keys', () => { + const sizePercentage = ref(50); + const collapsed = ref(false); + const options = { + disabled: false, + collapsible: true, + primary: 'start' as const, + orientation: 'horizontal' as const, + }; + + const { handleKeydown } = useKeyboard(sizePercentage, collapsed, options); + const event = createMockKeyboardEvent('KeyA'); + + handleKeydown(event); + + expect(sizePercentage.value).toBe(50); + expect(event.preventDefault).not.toHaveBeenCalled(); + }); +}); From 0444806fa052bb3b345a3e6acb2f2266bae1ec62 Mon Sep 17 00:00:00 2001 From: Rijk van Zanten Date: Tue, 26 Aug 2025 18:04:21 -0400 Subject: [PATCH 3/9] Add tests for use-pointer --- .../src/composables/use-pointer.test.ts | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 packages/vue-split-panel/src/composables/use-pointer.test.ts diff --git a/packages/vue-split-panel/src/composables/use-pointer.test.ts b/packages/vue-split-panel/src/composables/use-pointer.test.ts new file mode 100644 index 0000000..84137b2 --- /dev/null +++ b/packages/vue-split-panel/src/composables/use-pointer.test.ts @@ -0,0 +1,104 @@ +import type { UsePointerOptions } from './use-pointer'; +import { beforeEach, describe, expect, it } from 'vitest'; +import { computed, ref } from 'vue'; +import { usePointer } from './use-pointer'; + +describe('usePointer', () => { + let collapsed: any; + let sizePercentage: any; + let sizePixels: any; + let options: UsePointerOptions; + let dividerEl: any; + let panelEl: any; + + beforeEach(() => { + collapsed = ref(false); + sizePercentage = ref(50); + sizePixels = ref(200); + + // Create mock DOM elements + dividerEl = ref({ + getBoundingClientRect: () => ({ left: 0, top: 0, width: 10, height: 10 }), + style: {}, + addEventListener: () => {}, + removeEventListener: () => {}, + }); + + panelEl = ref({ + getBoundingClientRect: () => ({ left: 0, top: 0, width: 400, height: 400 }), + style: {}, + }); + + options = { + disabled: ref(false), + collapsible: ref(true), + primary: ref('start'), + orientation: ref('horizontal'), + direction: ref('ltr'), + collapseThreshold: ref(10), + snapThreshold: ref(5), + dividerEl, + panelEl, + componentSize: computed(() => 400), + minSizePixels: computed(() => 50), + snapPixels: computed(() => [100, 200, 300]), + }; + }); + + it('should return handleDblClick and isDragging', () => { + const result = usePointer(collapsed, sizePercentage, sizePixels, options); + + expect(result).toHaveProperty('handleDblClick'); + expect(result).toHaveProperty('isDragging'); + expect(typeof result.handleDblClick).toBe('function'); + expect(typeof result.isDragging.value).toBe('boolean'); + }); + + it('should handle double click to snap to closest point', () => { + const { handleDblClick } = usePointer(collapsed, sizePercentage, sizePixels, options); + + sizePixels.value = 195; // Close to 200 + handleDblClick(); + + expect(sizePixels.value).toBe(200); + }); + + it('should remain collapsed on handle double click when collapsed', () => { + collapsed.value = true; + const { handleDblClick } = usePointer(collapsed, sizePercentage, sizePixels, options); + + handleDblClick(); + + expect(collapsed.value).toBe(true); + }); + + it('should not snap on double click when disabled', () => { + options.disabled = ref(true); + const originalSize = sizePixels.value; + const { handleDblClick } = usePointer(collapsed, sizePercentage, sizePixels, options); + + handleDblClick(); + + expect(sizePixels.value).toBe(originalSize); + }); + + it('should not snap on double click when no snap points exist', () => { + options.snapPixels = computed(() => []); + const originalSize = sizePixels.value; + const { handleDblClick } = usePointer(collapsed, sizePercentage, sizePixels, options); + + handleDblClick(); + + expect(sizePixels.value).toBe(originalSize); + }); + + it('should handle when collapsible is false', () => { + options.collapsible = ref(false); + collapsed.value = false; + const { handleDblClick } = usePointer(collapsed, sizePercentage, sizePixels, options); + + handleDblClick(); + + expect(collapsed.value).toBe(false); // Should remain visible when not collapsible + }); +}); From d28efe9a055e62bfe42dd31a66eeef6ee3138eec Mon Sep 17 00:00:00 2001 From: Rijk van Zanten Date: Tue, 26 Aug 2025 18:26:18 -0400 Subject: [PATCH 4/9] Add tests for use resize --- .../src/composables/use-resize.test.ts | 80 +++++++++++++++++++ .../src/composables/use-resize.ts | 9 ++- 2 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 packages/vue-split-panel/src/composables/use-resize.test.ts diff --git a/packages/vue-split-panel/src/composables/use-resize.test.ts b/packages/vue-split-panel/src/composables/use-resize.test.ts new file mode 100644 index 0000000..857d52e --- /dev/null +++ b/packages/vue-split-panel/src/composables/use-resize.test.ts @@ -0,0 +1,80 @@ +import { mount } from '@vue/test-utils'; +import { beforeEach, describe, expect, it } from 'vitest'; +import { computed, defineComponent, ref } from 'vue'; +import { useResize } from './use-resize'; + +describe('useResize', () => { + let mockObserver: ResizeObserver; + + beforeEach(() => { + mockObserver = new ResizeObserver(() => {}); + }); + + it('should not reset sizePercentage when no primary has been set', () => { + const sizePercentage = ref(100); + + const wrapper = mount(defineComponent({ + template: '
', + setup() { + return useResize(sizePercentage, { + sizePixels: computed(() => 50), + panelEl: document.createElement('div'), + orientation: ref('horizontal'), + primary: ref(undefined), + }); + }, + })); + + const entry = { contentRect: { width: 75, height: 75 } } as ResizeObserverEntry; + + wrapper.vm.onResize([entry], mockObserver); + + expect(sizePercentage.value).toBe(100); + }); + + it('should set the sizePercentage based on the primary size width', () => { + const sizePercentage = ref(50); + + const wrapper = mount(defineComponent({ + template: '
', + setup() { + return useResize(sizePercentage, { + sizePixels: computed(() => 250), + panelEl: document.createElement('div'), + orientation: ref('horizontal'), + primary: ref('start'), + }); + }, + })); + + const entry = { contentRect: { width: 400, height: 150 } } as ResizeObserverEntry; + + wrapper.vm.onResize([entry], mockObserver); + + // Pixel size of 250 on a total width of 400 = 62.5% + expect(sizePercentage.value).toBe(62.5); + }); + + it('should use the height when orientation is vertical', () => { + const sizePercentage = ref(50); + + const wrapper = mount(defineComponent({ + template: '
', + setup() { + return useResize(sizePercentage, { + sizePixels: computed(() => 250), + panelEl: document.createElement('div'), + orientation: ref('vertical'), + primary: ref('start'), + }); + }, + })); + + const entry = { contentRect: { width: 150, height: 400 } } as ResizeObserverEntry; + + wrapper.vm.onResize([entry], mockObserver); + + // Pixel size of 250 on a total width of 400 = 62.5% + expect(sizePercentage.value).toBe(62.5); + }); +}); diff --git a/packages/vue-split-panel/src/composables/use-resize.ts b/packages/vue-split-panel/src/composables/use-resize.ts index 922848f..b7b191d 100644 --- a/packages/vue-split-panel/src/composables/use-resize.ts +++ b/packages/vue-split-panel/src/composables/use-resize.ts @@ -1,3 +1,4 @@ +import type { ResizeObserverCallback } from '@vueuse/core'; import type { ComputedRef, MaybeRefOrGetter, Ref } from 'vue'; import type { Orientation, Primary } from '../types'; import { useResizeObserver } from '@vueuse/core'; @@ -23,7 +24,7 @@ export const useResize = (sizePercentage: Ref, options: UseResizeOptions cachedSizePixels = newPixels; }); - useResizeObserver(options.panelEl, (entries) => { + const onResize: ResizeObserverCallback = (entries) => { const entry = entries[0]; const { width, height } = entry.contentRect; const size = toValue(options.orientation) === 'horizontal' ? width : height; @@ -31,5 +32,9 @@ export const useResize = (sizePercentage: Ref, options: UseResizeOptions if (toValue(options.primary)) { sizePercentage.value = pixelsToPercentage(size, cachedSizePixels); } - }); + }; + + useResizeObserver(options.panelEl, onResize); + + return { onResize }; }; From 3fac853838e43e2f395f01ee6e80373c4145ebe7 Mon Sep 17 00:00:00 2001 From: Rijk van Zanten Date: Tue, 26 Aug 2025 18:32:40 -0400 Subject: [PATCH 5/9] Improve testing --- packages/vue-split-panel/src/SplitPanel.vue | 13 +- .../src/composables/use-sizes.test.ts | 188 ++++++++++++++++++ .../vue-split-panel/tests/collapse.test.ts | 12 +- .../vue-split-panel/tests/mounting.test.ts | 6 +- 4 files changed, 207 insertions(+), 12 deletions(-) create mode 100644 packages/vue-split-panel/src/composables/use-sizes.test.ts diff --git a/packages/vue-split-panel/src/SplitPanel.vue b/packages/vue-split-panel/src/SplitPanel.vue index d4d305e..0d4d742 100644 --- a/packages/vue-split-panel/src/SplitPanel.vue +++ b/packages/vue-split-panel/src/SplitPanel.vue @@ -126,8 +126,14 @@ defineExpose({ collapse, expand, toggle });