import { assert, beforeEach, describe, expect, it, vi } from 'vitest'; import { ViewFolder, type ViewItem } from '../../src/view/item'; import { QueryResults } from '../../src/view/query-results'; import { createFolder } from '../test-utils'; import { generateViewMediaArray, TestViewMedia } from './test-utils'; describe('dispatchViewContextChangeEvent', () => { beforeEach(() => { vi.useRealTimers(); }); it('should function with empty results', () => { const fakeNow = new Date('2023-08-07T20:44'); vi.useFakeTimers(); vi.setSystemTime(fakeNow); const results = new QueryResults(); expect(results.isSupersetOf(results)).toBeFalsy(); expect(results.getCameraIDs()).toEqual(new Set()); expect(results.getResults()).toEqual([]); expect(results.getResultsCount()).toEqual(0); expect(results.hasResults()).toBeFalsy(); expect(results.getResult(0)).toBeNull(); expect(results.getSelectedIndex()).toBeNull(); expect(results.getSelectedResult()).toBeNull(); expect(results.hasSelectedResult()).toBeFalsy(); expect(results.resetSelectedResult()).toBe(results); expect(results.getResultsTimestamp()).toEqual(fakeNow); expect(results.selectIndex(0)).toEqual(results); expect(results.getSelectedResult()).toBeNull(); // eslint-disable-next-line @typescript-eslint/no-unused-vars expect(results.selectResultIfFound((_item: ViewItem) => true)).toEqual(results); expect(results.getSelectedResult()).toBeNull(); // eslint-disable-next-line @typescript-eslint/no-unused-vars expect(results.selectBestResult((_item: ViewItem[]) => null)).toEqual(results); expect(results.getSelectedResult()).toBeNull(); expect(results.getMultipleSelectedResults()).toEqual([]); }); it('should function with basic results', () => { const testResults = generateViewMediaArray(); const results = new QueryResults({ results: testResults }); expect(results.isSupersetOf(results)).toBeTruthy(); expect(results.getCameraIDs()).toEqual(new Set(['kitchen', 'office'])); expect(results.getResults()).toEqual(testResults); expect(results.getResultsCount()).toEqual(200); expect(results.hasResults()).toBeTruthy(); expect(results.getResult(0)).not.toBeNull(); expect(results.getSelectedIndex()).toBe(199); expect(results.getSelectedResult()).not.toBeNull(); expect(results.hasSelectedResult()).toBeTruthy(); expect(results.resetSelectedResult()).toBe(results); expect(results.getSelectedResult()).toBeNull(); expect(results.selectIndex(100)).toEqual(results); expect(results.getSelectedIndex()).toBe(100); expect( results.selectResultIfFound((item: ViewItem) => item.getID() === 'id-kitchen-42'), ).toEqual(results); expect(results.getSelectedResult()?.getID()).toBe('id-kitchen-42'); expect( results.selectBestResult((itemArray: ViewItem[]) => itemArray.findIndex((item) => item.getID() === 'id-kitchen-43'), ), ).toEqual(results); expect(results.getSelectedResult()?.getID()).toBe('id-kitchen-43'); }); it('should function with camera slice', () => { const testResults = generateViewMediaArray(); const results = new QueryResults({ results: testResults }); const slice = results.getSlice('office'); expect(slice).not.toBeNull(); assert(slice); expect(slice.getResults()).toEqual( testResults.filter((item) => item.getCameraID() === 'office'), ); expect(slice.getResultsCount()).toEqual(100); expect(slice.hasResults()).toBeTruthy(); expect(slice.getResult(0)).not.toBeNull(); expect(slice.getResult()).toBeNull(); expect(slice.getSelectedIndex()).toBe(99); expect(slice.getSelectedResult()?.getID()).toEqual('id-office-99'); expect(slice.hasSelectedResult()).toBeTruthy(); expect(slice.resetSelectedResult()); expect(slice.getSelectedResult()).toBeNull(); expect(slice.selectIndex(10)); expect(slice.getSelectedIndex()).toBe(10); expect(slice.selectIndex(10000)); expect(slice.getSelectedIndex()).toBe(10); expect(slice.selectIndex(-10000)); expect(slice.getSelectedIndex()).toBe(10); slice.selectResultIfFound((item: ViewItem) => item.getID() === 'id-office-42'); expect(slice.getSelectedResult()?.getID()).toBe('id-office-42'); slice.selectBestResult((itemArray: ViewItem[]) => itemArray.findIndex((item) => item.getID() === 'id-office-43'), ); expect(slice.getSelectedResult()?.getID()).toBe('id-office-43'); }); describe('should respect select approach during construction', () => { it.each([ ['first' as const, 0], ['last' as const, 199], ])('%s', async (selectApproach, expectedIndex) => { const results = new QueryResults({ results: generateViewMediaArray(), selectApproach: selectApproach, }); expect(results.getSelectedIndex()).toBe(expectedIndex); }); }); it('should respect selectIndex during construction', () => { const results = new QueryResults({ results: generateViewMediaArray(), selectedIndex: 42, }); expect(results.getSelectedIndex()).toBe(42); }); it('should correctly clone a slice', () => { const results = new QueryResults({ results: generateViewMediaArray(), }); const slice = results.getSlice('office'); const clone = slice?.clone(); expect(clone?.getResults()).toBe(slice?.getResults()); expect(clone?.getSelectedIndex()).toBe(slice?.getSelectedIndex()); }); it('should not get slice for non-existent camera', () => { const results = new QueryResults({ results: generateViewMediaArray(), }); expect(results.getSlice('not-a-camera')).toBeNull(); }); it('should get main slice', () => { const results = new QueryResults({ results: generateViewMediaArray(), }); expect(results.getSlice()?.getResults()).toBe(results.getResults()); }); it('should correctly clone', () => { const results = new QueryResults({ results: generateViewMediaArray(), }); const clone = results.clone(); expect(results.getResultsTimestamp()).toBe(clone.getResultsTimestamp()); expect(results.getResults()).toBe(clone.getResults()); for (const cameraID of results.getCameraIDs()) { expect(results.getSlice(cameraID)?.getResults()).toBe( clone.getSlice(cameraID)?.getResults(), ); } }); it('should not getResults on invalid slice', () => { const results = new QueryResults({ results: generateViewMediaArray(), }); expect(results.getResults('not-a-camera')).toBeNull(); expect(results.getResultsCount('not-a-camera')).toBe(0); expect(results.hasSelectedResult('not-a-camera')).toBeFalsy(); }); it('should always demote main selection', () => { const results = new QueryResults({ results: generateViewMediaArray(), }); results .getSlice('office') ?.selectResultIfFound((item) => item.getID() === 'id-office-42'); // Verify main and office selections are as expected. expect(results.getSelectedIndex()).toBe(199); expect(results.getSelectedResult('office')?.getID()).toBe('id-office-42'); // Select a different main result... results?.selectResultIfFound((item) => item.getID() === 'id-office-80'); // ... and ensure that selection has been demoted into the camera slice. expect(results.getSelectedResult('office')?.getID()).toBe('id-office-80'); }); it('should promote camera selection', () => { const results = new QueryResults({ results: generateViewMediaArray(), }); results .getSlice('office') ?.selectResultIfFound((item) => item.getID() === 'id-office-42'); expect(results.getSelectedIndex()).toBe(199); results.promoteCameraSelectionToMainSelection('office'); expect(results.getSelectedIndex()).not.toBe(199); expect(results.getSelectedResult()?.getID()).toBe('id-office-42'); }); it('should selectBestResult via advanced selection criteria', () => { const results = new QueryResults({ results: generateViewMediaArray(), }); results.selectBestResult( (itemArray: ViewItem[]) => { const index = itemArray.findIndex((item) => item.getID()?.endsWith('-42')); return index < 0 ? null : index; }, { allCameras: true }, ); expect(results.getSelectedResult('office')?.getID()).toBe('id-office-42'); expect(results.getSelectedResult('kitchen')?.getID()).toBe('id-kitchen-42'); }); it('should get multiple selected results', () => { const results = new QueryResults({ results: generateViewMediaArray(), }); expect( results .getMultipleSelectedResults({ main: true, allCameras: true }) .map((item) => item.getID()), ).toEqual(['id-office-99', 'id-kitchen-99', 'id-office-99']); }); it('should get multiple selected results without main', () => { const results = new QueryResults({ results: generateViewMediaArray(), }); expect( results .getMultipleSelectedResults({ main: false, allCameras: true }) .map((item) => item.getID()), ).toEqual(['id-kitchen-99', 'id-office-99']); }); it('should get no results with invalid camera ID without main', () => { const results = new QueryResults({ results: generateViewMediaArray(), }); expect( results .getMultipleSelectedResults({ main: false, cameraID: 'not-a-real-camera' }) .map((item) => item.getID()), ).toEqual([]); }); it('should not demote main selection when selecting from a specific camera', () => { const results = new QueryResults({ results: generateViewMediaArray(), }); results.selectIndex(42); results.selectIndex(24, 'office'); expect(results.getSelectedIndex()).toBe(42); expect(results.getSelectedIndex('office')).toBe(24); }); it('should keep selected folder in main slice but not camera slices', () => { const folder = new ViewFolder(createFolder(), []); const results = new QueryResults({ results: [ folder, ...generateViewMediaArray({ cameraIDs: ['camera.office', 'camera.kitchen'], }), ], selectedIndex: 0, }); expect(results.getSelectedResult()).toBe(folder); expect(results.getSelectedResult('camera.office')).not.toBe(folder); }); describe('removeItem', () => { it('should remove item from results', () => { const testResults = generateViewMediaArray(); const results = new QueryResults({ results: testResults }); const itemToRemove = testResults[50]; expect(results.getResults()?.includes(itemToRemove)).toBeTruthy(); expect(results.getResultsCount()).toBe(200); results.removeItem(itemToRemove); expect(results.getResults()?.includes(itemToRemove)).toBeFalsy(); expect(results.getResultsCount()).toBe(199); }); it('should not remove item that is not in results', () => { const testResults = generateViewMediaArray(); const results = new QueryResults({ results: testResults }); const outsideItem = generateViewMediaArray({ cameraIDs: ['other'] })[0]; const countBefore = results.getResultsCount(); results.removeItem(outsideItem); expect(results.getResultsCount()).toBe(countBefore); }); it('should adjust selection when removing selected item', () => { const testResults = generateViewMediaArray(); const results = new QueryResults({ results: testResults, selectedIndex: 50 }); const itemToRemove = testResults[50]; results.removeItem(itemToRemove); // After removing selected item at index 50, Math.min(50, 199-1) = 50 // which is still valid in the new 199-element array expect(results.getSelectedIndex()).toBe(50); }); it('should adjust selection when removing item before selected', () => { const testResults = generateViewMediaArray(); const results = new QueryResults({ results: testResults, selectedIndex: 50 }); const itemToRemove = testResults[10]; results.removeItem(itemToRemove); // Selection should decrement expect(results.getSelectedIndex()).toBe(49); }); it('should not change selection when removing item after selected', () => { const testResults = generateViewMediaArray(); const results = new QueryResults({ results: testResults, selectedIndex: 50 }); const itemToRemove = testResults[100]; results.removeItem(itemToRemove); // Selection should stay the same expect(results.getSelectedIndex()).toBe(50); }); it('should set selection to null when removing last remaining selected item', () => { const testResults = generateViewMediaArray({ cameraIDs: ['camera'], count: 1 }); const results = new QueryResults({ results: testResults, selectedIndex: 0 }); results.removeItem(testResults[0]); // Selection should be null when no items remain expect(results.getSelectedIndex()).toBeNull(); }); it('should handle removing folder item (non-media)', () => { const folder = new ViewFolder(createFolder(), []); const testResults = generateViewMediaArray(); const results = new QueryResults({ results: [folder, ...testResults] }); const countBefore = results.getResultsCount(); results.removeItem(folder); expect(results.getResultsCount()).toBe(countBefore - 1); }); }); describe('replaceItem', () => { it('should replace item in slice', () => { const testResults = generateViewMediaArray(); const results = new QueryResults({ results: testResults }); const slice = results.getSlice('office'); assert(slice); const oldItem = slice.getResults()[42]; const newItem = oldItem.clone(); expect(slice?.replaceItem(oldItem, newItem)).toBeTruthy(); expect(slice?.getResults()[42]).toBe(newItem); }); it('should fail to replace item not in camera slice', () => { const results = new QueryResults({ results: generateViewMediaArray() }); const slice = results.getSlice('office'); assert(slice); const foreignItem = new TestViewMedia({ cameraID: 'other', }); const newItem = foreignItem.clone(); expect(slice.replaceItem(foreignItem, newItem)).toBeFalsy(); }); it('should replace item in main slice', () => { const testResults = generateViewMediaArray(); const results = new QueryResults({ results: testResults }); const oldItem = testResults[42]; const newItem = oldItem.clone(); expect(results.replaceItem(oldItem, newItem)).toBe(results); expect(results.getResults()?.[42]).toBe(newItem); expect(results.getSlice('kitchen')?.getResults().includes(newItem)).toBeTruthy(); expect(results.getSlice('kitchen')?.getResults().includes(oldItem)).toBeFalsy(); }); it('should fail to replace item not in main slice', () => { const results = new QueryResults({ results: generateViewMediaArray() }); const outsideItem = generateViewMediaArray({ cameraIDs: ['other'] })[0]; const newItem = outsideItem.clone(); expect(results.replaceItem(outsideItem, newItem)).toBe(results); }); it('should replace folder in main slice', () => { const folder = new ViewFolder(createFolder(), []); const results = new QueryResults({ results: [folder] }); const newFolder = folder.clone(); expect(results.replaceItem(folder, newFolder)).toBe(results); expect(results.getResults()?.[0]).toBe(newFolder); }); }); });