445 lines
15 KiB
TypeScript
445 lines
15 KiB
TypeScript
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, 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);
|
|
});
|
|
});
|
|
});
|