diff --git a/code/websites/pokedex.online/tests/unit/composables/useGamemasterFiles.test.js b/code/websites/pokedex.online/tests/unit/composables/useGamemasterFiles.test.js new file mode 100644 index 0000000..c8a2e13 --- /dev/null +++ b/code/websites/pokedex.online/tests/unit/composables/useGamemasterFiles.test.js @@ -0,0 +1,430 @@ +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 () => { + // Create large content + const largeData = { + items: Array(15000).fill({ id: 1, name: 'test' }) + }; + mockClient.getPokemon.mockResolvedValueOnce(largeData); + composable.selectedFile.value = 'pokemon'; + + await composable.loadFile(); + + expect(composable.displayLines.value.length).toBeLessThanOrEqual( + composable.LINES_TO_DISPLAY + ); + expect(composable.fileLines.value.length).toBeGreaterThan(10000); + }); + + it('should save last file preference', async () => { + 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 () => { + mockClient.getPokemon.mockRejectedValueOnce( + new Error('Load failed') + ); + composable.selectedFile.value = 'pokemon'; + + 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 () => { + const largeData = { + items: Array(15000).fill({ id: 1 }) + }; + mockClient.getPokemon.mockResolvedValueOnce(largeData); + composable.selectedFile.value = 'pokemon'; + + await composable.loadFile(); + + 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', () => { + composable.updateDisplayLines(50, 150); + + expect(composable.displayLines.value[0].lineNumber).toBe(51); + expect(composable.displayLines.value[1].lineNumber).toBe(52); + }); + }); + + describe('expandDisplayLinesToInclude', () => { + beforeEach(async () => { + // Create large file + const largeData = { + items: Array(20000).fill({ id: 1 }) + }; + 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', () => { + composable.expandDisplayLinesToInclude(15000); + + expect(composable.displayLines.value.length).toBeGreaterThan(10000); + }); + + 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); + }); + }); +});