feat: Add support for Frigate reviews / detections [initial PR] (#2315)
- Add support for Frigate reviews / detections. - Add support for GenAI metadata. - Significant internal refactor to more flexible "UnifiedQuery" to allow mixing cameras with simple metadata and review metadata (e.g. a timeline view of a Frigate camera with reviews, and a Reolink camera with simple metadata). - Add support for folder media as camera media. There are a few more PRs to commit prior to this going live, but commiting this for now due to the scale of the change. BREAKING CHANGE: `media_type` and `events_type` are retired under `live`, `viewer` and `timeline` configuration sections, instead media type is associated (once) with the camera under `media`.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { assert, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { ViewFolder, ViewItem } from '../../src/view/item';
|
||||
import { QueryResults } from '../../src/view/query-results';
|
||||
import { createFolder, generateViewMediaArray } from '../test-utils';
|
||||
import { createFolder, generateViewMediaArray, TestViewMedia } from '../test-utils';
|
||||
|
||||
describe('dispatchViewContextChangeEvent', () => {
|
||||
beforeEach(() => {
|
||||
@@ -281,7 +281,7 @@ describe('dispatchViewContextChangeEvent', () => {
|
||||
});
|
||||
|
||||
it('should keep selected folder in main slice but not camera slices', () => {
|
||||
const folder = new ViewFolder(createFolder());
|
||||
const folder = new ViewFolder(createFolder(), []);
|
||||
const results = new QueryResults({
|
||||
results: [
|
||||
folder,
|
||||
@@ -295,4 +295,148 @@ describe('dispatchViewContextChangeEvent', () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user