import { describe, it, expect, beforeEach, vi } from 'vitest'; import { ref } from 'vue'; import { useLineSelection } from '@/composables/useLineSelection.js'; describe('useLineSelection', () => { let displayLines; let fileContent; let selectedFile; let composable; beforeEach(() => { displayLines = ref([ { lineNumber: 1, content: 'line 1' }, { lineNumber: 2, content: 'line 2' }, { lineNumber: 3, content: 'line 3' }, { lineNumber: 4, content: 'line 4' }, { lineNumber: 5, content: 'line 5' } ]); fileContent = ref('line 1\nline 2\nline 3\nline 4\nline 5'); selectedFile = ref('test-file'); composable = useLineSelection(displayLines, fileContent, selectedFile); // Mock clipboard global.navigator = { clipboard: { writeText: vi.fn() } }; // Mock URL methods global.URL.createObjectURL = vi.fn(() => 'blob:test'); global.URL.revokeObjectURL = vi.fn(); // Mock document methods document.body.appendChild = vi.fn(); document.body.removeChild = vi.fn(); }); describe('initialization', () => { it('should initialize with empty selection', () => { expect(composable.selectedLines.value.size).toBe(0); expect(composable.hasSelection.value).toBe(false); expect(composable.selectionCount.value).toBe(0); }); }); describe('toggleLineSelection', () => { it('should select single line with no modifiers', () => { const event = { shiftKey: false, ctrlKey: false, metaKey: false }; composable.toggleLineSelection(1, event); expect(composable.selectedLines.value.has(1)).toBe(true); expect(composable.selectedLines.value.size).toBe(1); }); it('should clear previous selection on single click', () => { const event = { shiftKey: false, ctrlKey: false, metaKey: false }; composable.toggleLineSelection(1, event); composable.toggleLineSelection(3, event); expect(composable.selectedLines.value.has(1)).toBe(false); expect(composable.selectedLines.value.has(3)).toBe(true); expect(composable.selectedLines.value.size).toBe(1); }); it('should toggle line with Ctrl modifier', () => { const ctrlEvent = { shiftKey: false, ctrlKey: true, metaKey: false }; composable.toggleLineSelection(1, ctrlEvent); expect(composable.selectedLines.value.has(1)).toBe(true); composable.toggleLineSelection(1, ctrlEvent); expect(composable.selectedLines.value.has(1)).toBe(false); }); it('should toggle line with Cmd modifier (Mac)', () => { const cmdEvent = { shiftKey: false, ctrlKey: false, metaKey: true }; composable.toggleLineSelection(1, cmdEvent); expect(composable.selectedLines.value.has(1)).toBe(true); composable.toggleLineSelection(1, cmdEvent); expect(composable.selectedLines.value.has(1)).toBe(false); }); it('should add multiple lines with Ctrl', () => { const ctrlEvent = { shiftKey: false, ctrlKey: true, metaKey: false }; composable.toggleLineSelection(1, ctrlEvent); composable.toggleLineSelection(3, ctrlEvent); composable.toggleLineSelection(5, ctrlEvent); expect(composable.selectedLines.value.size).toBe(3); expect(composable.selectedLines.value.has(1)).toBe(true); expect(composable.selectedLines.value.has(3)).toBe(true); expect(composable.selectedLines.value.has(5)).toBe(true); }); it('should select range with Shift modifier', () => { const event = { shiftKey: false, ctrlKey: false, metaKey: false }; const shiftEvent = { shiftKey: true, ctrlKey: false, metaKey: false }; composable.toggleLineSelection(2, event); // Set starting point composable.toggleLineSelection(4, shiftEvent); // Select range expect(composable.selectedLines.value.has(2)).toBe(true); expect(composable.selectedLines.value.has(3)).toBe(true); expect(composable.selectedLines.value.has(4)).toBe(true); expect(composable.selectedLines.value.size).toBe(3); }); it('should select range in reverse order with Shift', () => { const event = { shiftKey: false, ctrlKey: false, metaKey: false }; const shiftEvent = { shiftKey: true, ctrlKey: false, metaKey: false }; composable.toggleLineSelection(4, event); // Set starting point composable.toggleLineSelection(2, shiftEvent); // Select range backwards expect(composable.selectedLines.value.has(2)).toBe(true); expect(composable.selectedLines.value.has(3)).toBe(true); expect(composable.selectedLines.value.has(4)).toBe(true); expect(composable.selectedLines.value.size).toBe(3); }); }); describe('clearSelection', () => { it('should clear all selections', () => { const event = { shiftKey: false, ctrlKey: false, metaKey: false }; composable.toggleLineSelection(1, event); composable.toggleLineSelection(3, event); composable.clearSelection(); expect(composable.selectedLines.value.size).toBe(0); expect(composable.hasSelection.value).toBe(false); }); }); describe('getSelectedContent', () => { it('should return empty string when no selection', () => { expect(composable.getSelectedContent()).toBe(''); }); it('should return selected lines content', () => { const event = { shiftKey: false, ctrlKey: false, metaKey: false }; const ctrlEvent = { shiftKey: false, ctrlKey: true, metaKey: false }; composable.toggleLineSelection(1, event); composable.toggleLineSelection(3, ctrlEvent); const content = composable.getSelectedContent(); expect(content).toContain('line 1'); expect(content).toContain('line 3'); expect(content).not.toContain('line 2'); }); it('should maintain line order in output', () => { const ctrlEvent = { shiftKey: false, ctrlKey: true, metaKey: false }; composable.toggleLineSelection(5, ctrlEvent); composable.toggleLineSelection(2, ctrlEvent); composable.toggleLineSelection(4, ctrlEvent); const content = composable.getSelectedContent(); const lines = content.split('\n'); expect(lines[0]).toBe('line 2'); expect(lines[1]).toBe('line 4'); expect(lines[2]).toBe('line 5'); }); }); describe('computed properties', () => { it('should update hasSelection', () => { expect(composable.hasSelection.value).toBe(false); const event = { shiftKey: false, ctrlKey: false, metaKey: false }; composable.toggleLineSelection(1, event); expect(composable.hasSelection.value).toBe(true); }); it('should update selectionCount', () => { const ctrlEvent = { shiftKey: false, ctrlKey: true, metaKey: false }; expect(composable.selectionCount.value).toBe(0); composable.toggleLineSelection(1, ctrlEvent); expect(composable.selectionCount.value).toBe(1); composable.toggleLineSelection(3, ctrlEvent); expect(composable.selectionCount.value).toBe(2); }); it('should return sorted selected line numbers', () => { const ctrlEvent = { shiftKey: false, ctrlKey: true, metaKey: false }; composable.toggleLineSelection(5, ctrlEvent); composable.toggleLineSelection(2, ctrlEvent); composable.toggleLineSelection(4, ctrlEvent); expect(composable.selectedLineNumbers.value).toEqual([2, 4, 5]); }); }); describe('selectAll', () => { it('should select all available lines', () => { composable.selectAll(); expect(composable.selectedLines.value.size).toBe(5); expect(composable.hasSelection.value).toBe(true); for (let i = 1; i <= 5; i++) { expect(composable.selectedLines.value.has(i)).toBe(true); } }); }); describe('invertSelection', () => { it('should invert selection', () => { const ctrlEvent = { shiftKey: false, ctrlKey: true, metaKey: false }; composable.toggleLineSelection(2, ctrlEvent); composable.toggleLineSelection(4, ctrlEvent); composable.invertSelection(); expect(composable.selectedLines.value.has(1)).toBe(true); expect(composable.selectedLines.value.has(2)).toBe(false); expect(composable.selectedLines.value.has(3)).toBe(true); expect(composable.selectedLines.value.has(4)).toBe(false); expect(composable.selectedLines.value.has(5)).toBe(true); }); it('should invert empty selection to select all', () => { composable.invertSelection(); expect(composable.selectedLines.value.size).toBe(5); }); }); describe('copySelected', () => { it('should not copy if no selection', async () => { await composable.copySelected(); expect(global.navigator.clipboard.writeText).not.toHaveBeenCalled(); }); it('should copy selected lines', async () => { const event = { shiftKey: false, ctrlKey: false, metaKey: false }; composable.toggleLineSelection(1, event); await composable.copySelected(); expect(global.navigator.clipboard.writeText).toHaveBeenCalledWith( 'line 1' ); }); }); describe('copyAll', () => { it('should copy all content', async () => { await composable.copyAll(); expect(global.navigator.clipboard.writeText).toHaveBeenCalledWith( fileContent.value ); }); }); describe('exportSelected', () => { it('should not export if no selection', () => { const createElementSpy = vi.spyOn(document, 'createElement'); composable.exportSelected(); expect(createElementSpy).not.toHaveBeenCalledWith('a'); }); it('should export selected lines as file', () => { const event = { shiftKey: false, ctrlKey: false, metaKey: false }; const ctrlEvent = { shiftKey: false, ctrlKey: true, metaKey: false }; composable.toggleLineSelection(1, event); composable.toggleLineSelection(3, ctrlEvent); const createElementSpy = vi.spyOn(document, 'createElement'); composable.exportSelected(); const linkElement = createElementSpy.mock.results.find( r => r.value?.click )?.value; expect(linkElement?.download).toContain('test-file-selected'); }); }); describe('exportAll', () => { it('should export all content as file', () => { const createElementSpy = vi.spyOn(document, 'createElement'); composable.exportAll(); const linkElement = createElementSpy.mock.results.find( r => r.value?.click )?.value; expect(linkElement?.download).toContain('test-file'); expect(linkElement?.download).not.toContain('selected'); }); }); describe('shareUrl', () => { it('should copy current URL to clipboard', async () => { await composable.shareUrl(); expect(global.navigator.clipboard.writeText).toHaveBeenCalled(); const call = global.navigator.clipboard.writeText.mock.calls[0][0]; expect(call).toContain(globalThis.location.href || ''); }); }); });