import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { ref } from 'vue'; import { useGamemasterFiles } from '@/composables/useGamemasterFiles.js'; describe('useGamemasterFiles', () => { let mockClient; let composable; beforeEach(() => { // Mock GamemasterClient mockClient = { getStatus: vi.fn(() => Promise.resolve({ available: [ { filename: 'pokemon.json', size: 5000 }, { filename: 'moves.json', size: 3000 }, { filename: 'allForms.json', size: 8000 }, { filename: 'raw.json', size: 20000 } ] }) ), getPokemon: vi.fn(() => Promise.resolve({ pokemon: [{ name: 'pikachu', id: 25 }] }) ), getMoves: vi.fn(() => Promise.resolve({ moves: [{ name: 'thunderbolt', id: 24 }] }) ), getAllForms: vi.fn(() => Promise.resolve({ forms: [{ name: 'pikachu-gmax' }] }) ), getRaw: vi.fn(() => Promise.resolve({ raw: [{ data: 'raw content' }] }) ) }; composable = useGamemasterFiles(mockClient); }); afterEach(() => { vi.clearAllMocks(); }); describe('initialization', () => { it('should initialize with empty state', () => { expect(composable.selectedFile.value).toBe(''); expect(composable.fileContent.value).toBe(''); expect(composable.fileLines.value).toEqual([]); expect(composable.displayLines.value).toEqual([]); expect(composable.isLoading.value).toBe(false); expect(composable.fileError.value).toBeNull(); }); it('should have default preferences', () => { expect(composable.preferences.value).toBeDefined(); expect(composable.preferences.value.darkMode).toBe(false); expect(composable.preferences.value.showLineNumbers).toBe(true); }); }); describe('loadStatus', () => { it('should fetch file list from server', async () => { await composable.loadStatus(); expect(mockClient.getStatus).toHaveBeenCalled(); expect(composable.status.value.available).toHaveLength(4); }); it('should set error on fetch failure', async () => { mockClient.getStatus.mockRejectedValueOnce(new Error('Network error')); await composable.loadStatus(); expect(composable.fileError.value).toBe('Network error'); }); it('should auto-load last file if preference set', async () => { composable.preferences.value.lastFile = 'pokemon'; mockClient.getPokemon.mockResolvedValueOnce({ pokemon: [] }); await composable.loadStatus(); expect(composable.selectedFile.value).toBe('pokemon'); expect(mockClient.getPokemon).toHaveBeenCalled(); }); it('should set loading state during fetch', async () => { const loadingStates = []; const originalSetTimeout = setTimeout; await composable.loadStatus(); expect(composable.isLoading.value).toBe(false); }); }); describe('loadFile', () => { beforeEach(async () => { await composable.loadStatus(); }); it('should return early if no file selected', async () => { composable.selectedFile.value = ''; await composable.loadFile(); expect(mockClient.getPokemon).not.toHaveBeenCalled(); }); it('should load pokemon file', async () => { composable.selectedFile.value = 'pokemon'; await composable.loadFile(); expect(mockClient.getPokemon).toHaveBeenCalled(); expect(composable.fileContent.value).toContain('pikachu'); expect(composable.fileLines.value.length).toBeGreaterThan(0); }); it('should load moves file', async () => { composable.selectedFile.value = 'moves'; await composable.loadFile(); expect(mockClient.getMoves).toHaveBeenCalled(); expect(composable.fileContent.value).toContain('thunderbolt'); }); it('should load allForms file', async () => { composable.selectedFile.value = 'allForms'; await composable.loadFile(); expect(mockClient.getAllForms).toHaveBeenCalled(); expect(composable.fileContent.value).toContain('pikachu-gmax'); }); it('should load raw file', async () => { composable.selectedFile.value = 'raw'; await composable.loadFile(); expect(mockClient.getRaw).toHaveBeenCalled(); expect(composable.fileContent.value).toContain('raw content'); }); it('should reject raw file if too large', async () => { composable.status.value.available = [ { filename: 'raw.json', size: 100 * 1024 * 1024 // 100MB } ]; composable.selectedFile.value = 'raw'; await composable.loadFile(); expect(composable.fileError.value).toContain('very large'); expect(composable.fileContent.value).toBe(''); }); it('should set display lines for small files', async () => { composable.selectedFile.value = 'pokemon'; await composable.loadFile(); expect(composable.displayLines.value.length).toBeGreaterThan(0); expect(composable.displayLines.value[0]).toHaveProperty('lineNumber'); expect(composable.displayLines.value[0]).toHaveProperty('content'); expect(composable.displayLines.value[0]).toHaveProperty('hasMatch'); }); it('should limit displayed lines to 10000', async () => { composable.selectedFile.value = 'pokemon'; await composable.loadFile(); expect(composable.displayLines.value.length).toBeLessThanOrEqual( composable.LINES_TO_DISPLAY ); }); it('should save last file preference', async () => { await composable.loadStatus(); // Load status first composable.selectedFile.value = 'moves'; await composable.loadFile(); expect(composable.preferences.value.lastFile).toBe('moves'); }); it('should handle unknown file type', async () => { composable.selectedFile.value = 'unknown'; await composable.loadFile(); expect(composable.fileError.value).toContain('Unknown file type'); }); it('should set error on load failure', async () => { // First, set up status so we have available files mockClient.getStatus.mockResolvedValueOnce({ available: [{ filename: 'moves.json', size: 3000 }] }); await composable.loadStatus(); // Now set up the getMoves to fail mockClient.getMoves.mockRejectedValueOnce(new Error('Load failed')); composable.selectedFile.value = 'moves'; await composable.loadFile(); expect(composable.fileError.value).toBe('Load failed'); }); }); describe('clearFileSelection', () => { beforeEach(async () => { composable.selectedFile.value = 'pokemon'; composable.fileContent.value = 'test content'; composable.fileLines.value = ['line1', 'line2']; composable.jsonPaths.value = ['/path1']; composable.fileError.value = 'Some error'; }); it('should clear all file state', () => { composable.clearFileSelection(); expect(composable.selectedFile.value).toBe(''); expect(composable.fileContent.value).toBe(''); expect(composable.fileLines.value).toEqual([]); expect(composable.displayLines.value).toEqual([]); expect(composable.jsonPaths.value).toEqual([]); expect(composable.fileError.value).toBeNull(); }); }); describe('formatSize', () => { it('should format bytes correctly', () => { expect(composable.formatSize(0)).toBe('0 B'); expect(composable.formatSize(1024)).toContain('KB'); expect(composable.formatSize(1024 * 1024)).toContain('MB'); }); it('should handle null/undefined', () => { expect(composable.formatSize(null)).toBe('0 B'); expect(composable.formatSize(undefined)).toBe('0 B'); }); }); describe('getFileType', () => { it('should identify pokemon file', () => { expect(composable.getFileType('pokemon.json')).toBe('pokemon'); }); it('should identify moves file', () => { expect(composable.getFileType('moves.json')).toBe('moves'); }); it('should identify allForms file', () => { expect(composable.getFileType('allForms.json')).toBe('allForms'); expect(composable.getFileType('AllForms.json')).toBe('allForms'); }); it('should identify raw file', () => { expect(composable.getFileType('raw.json')).toBe('raw'); }); it('should return empty string for unknown type', () => { expect(composable.getFileType('unknown.json')).toBe(''); }); }); describe('formatFileName', () => { it('should format pokemon file name', () => { expect(composable.formatFileName('pokemon.json')).toBe('Pokemon'); }); it('should format moves file name', () => { expect(composable.formatFileName('moves.json')).toBe('Moves'); }); it('should format allForms file name', () => { expect(composable.formatFileName('allForms.json')).toBe( 'Pokemon All Forms' ); }); it('should format raw file name', () => { expect(composable.formatFileName('raw.json')).toBe('Raw Gamemaster'); }); }); describe('computed properties', () => { describe('availableFiles', () => { it('should return files from status', async () => { await composable.loadStatus(); expect(composable.availableFiles.value).toHaveLength(4); }); it('should return empty array initially', () => { expect(composable.availableFiles.value).toEqual([]); }); }); describe('uniqueFiles', () => { it('should return unique files sorted by type', async () => { await composable.loadStatus(); expect(composable.uniqueFiles.value.length).toBeGreaterThan(0); }); it('should sort files by type order', async () => { await composable.loadStatus(); const types = composable.uniqueFiles.value.map(f => composable.getFileType(f.filename) ); expect(types.indexOf('pokemon')).toBeLessThan( types.indexOf('allForms') ); }); }); describe('hasFiles', () => { it('should be false initially', () => { expect(composable.hasFiles.value).toBe(false); }); it('should be true after loading status', async () => { await composable.loadStatus(); expect(composable.hasFiles.value).toBe(true); }); }); describe('fileTooLarge', () => { it('should be false for small files', async () => { composable.selectedFile.value = 'pokemon'; await composable.loadFile(); expect(composable.fileTooLarge.value).toBe(false); }); it('should be true for large files', async () => { // Create large array that when JSON stringified will have many lines const largeData = { items: Array(12000).fill({ id: 1, data: 'test' }) }; mockClient.getPokemon.mockResolvedValueOnce(largeData); composable.selectedFile.value = 'pokemon'; await composable.loadFile(); // Verify we have many lines (JSON.stringify adds newlines for pretty-print) expect(composable.fileLines.value.length).toBeGreaterThan(10000); expect(composable.fileTooLarge.value).toBe(true); }); }); }); describe('updateDisplayLines', () => { beforeEach(async () => { composable.selectedFile.value = 'pokemon'; await composable.loadFile(); }); it('should update display lines range', () => { const originalCount = composable.displayLines.value.length; composable.updateDisplayLines(0, 100); expect(composable.displayLines.value.length).toBeLessThanOrEqual(100); }); it('should set correct line numbers', () => { // Create enough lines for this test composable.fileLines.value = Array(200).fill('test line'); composable.updateDisplayLines(50, 150); expect(composable.displayLines.value.length).toBeGreaterThan(0); if (composable.displayLines.value.length > 0) { expect(composable.displayLines.value[0].lineNumber).toBe(51); } }); }); describe('expandDisplayLinesToInclude', () => { beforeEach(async () => { // Create large file with many items that will create many lines const largeData = { items: Array(15000).fill({ id: 1, data: 'test' }) }; mockClient.getPokemon.mockResolvedValueOnce(largeData); composable.selectedFile.value = 'pokemon'; await composable.loadFile(); }); it('should not expand if line is visible', () => { const originalLength = composable.displayLines.value.length; composable.expandDisplayLinesToInclude(100); expect(composable.displayLines.value.length).toBe(originalLength); }); it('should expand to include line number', () => { if (composable.fileLines.value.length > 15000) { composable.expandDisplayLinesToInclude(15000); expect(composable.displayLines.value.length).toBeGreaterThan( composable.LINES_TO_DISPLAY ); } }); it('should not exceed total file lines', () => { composable.expandDisplayLinesToInclude(999999); expect(composable.displayLines.value.length).toBeLessThanOrEqual( composable.fileLines.value.length ); }); }); describe('file selection watch', () => { it('should load file when selectedFile changes', async () => { composable.selectedFile.value = 'pokemon'; await new Promise(r => setTimeout(r, 50)); expect(mockClient.getPokemon).toHaveBeenCalled(); }); it('should not load when selectedFile is empty', async () => { composable.selectedFile.value = ''; await new Promise(r => setTimeout(r, 50)); expect(mockClient.getPokemon).not.toHaveBeenCalled(); }); }); describe('constants', () => { it('should have correct constants', () => { expect(composable.LINES_TO_DISPLAY).toBe(10000); expect(composable.MAX_RAW_FILE_SIZE).toBe(50 * 1024 * 1024); }); }); });