test(webapp): add unit coverage for state and PWA flows
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
clampScore,
|
||||
applyMotionStateMachine,
|
||||
computeChangedPixelRatio,
|
||||
computeLuma,
|
||||
@@ -8,6 +9,13 @@ import {
|
||||
} from './motion-detector';
|
||||
|
||||
describe('motion-detector helpers', () => {
|
||||
it('clamps invalid and out-of-range scores', () => {
|
||||
expect(clampScore(Number.NaN)).toBe(0);
|
||||
expect(clampScore(-1)).toBe(0);
|
||||
expect(clampScore(0.35)).toBe(0.35);
|
||||
expect(clampScore(4)).toBe(1);
|
||||
});
|
||||
|
||||
it('computes grayscale luminance for RGBA image data', () => {
|
||||
const luminance = computeLuma({
|
||||
data: new Uint8ClampedArray([
|
||||
@@ -28,6 +36,10 @@ describe('motion-detector helpers', () => {
|
||||
expect(computeChangedPixelRatio(previousFrame, currentFrame, 18)).toBeCloseTo(0.5, 5);
|
||||
expect(computeChangedPixelRatio(previousFrame, currentFrame, 40)).toBe(0);
|
||||
});
|
||||
|
||||
it('returns zero when frame sizes do not match', () => {
|
||||
expect(computeChangedPixelRatio(new Float32Array([0, 10]), new Float32Array([0]), 5)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('motion-detector state machine', () => {
|
||||
@@ -58,6 +70,15 @@ describe('motion-detector state machine', () => {
|
||||
expect(result.snapshot.activeSince).toBe(1600);
|
||||
});
|
||||
|
||||
it('keeps the detector in warming up state before a first stable frame', () => {
|
||||
const snapshot = createMotionMachineSnapshot();
|
||||
const result = applyMotionStateMachine(snapshot, { state: 'warming_up', score: 0.9 }, config, 1000);
|
||||
|
||||
expect(result.output.state).toBe('warming_up');
|
||||
expect(result.output.activeMotion).toBe(false);
|
||||
expect(result.snapshot.consecutiveHighFrames).toBe(0);
|
||||
});
|
||||
|
||||
it('holds motion until minimum duration and quiet cooldown complete', () => {
|
||||
let snapshot = createMotionMachineSnapshot({
|
||||
activeMotion: true,
|
||||
@@ -76,4 +97,16 @@ describe('motion-detector state machine', () => {
|
||||
expect(result.snapshot.activeSince).toBeNull();
|
||||
expect(result.snapshot.quietSince).toBeNull();
|
||||
});
|
||||
|
||||
it('applies snapshot overrides when creating a machine snapshot', () => {
|
||||
const snapshot = createMotionMachineSnapshot({
|
||||
activeMotion: true,
|
||||
activeSince: 1234,
|
||||
consecutiveHighFrames: 2
|
||||
});
|
||||
|
||||
expect(snapshot.activeMotion).toBe(true);
|
||||
expect(snapshot.activeSince).toBe(1234);
|
||||
expect(snapshot.consecutiveHighFrames).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
73
WebApp/src/lib/app/store.spec.ts
Normal file
73
WebApp/src/lib/app/store.spec.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { get } from 'svelte/store';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
appState,
|
||||
createInitialState,
|
||||
getAppState,
|
||||
patchAppState,
|
||||
resetAppState,
|
||||
setAppState,
|
||||
unreadNotificationsCount
|
||||
} from './store';
|
||||
|
||||
afterEach(() => {
|
||||
resetAppState();
|
||||
});
|
||||
|
||||
describe('app store', () => {
|
||||
it('creates the expected initial state shape', () => {
|
||||
const state = createInitialState();
|
||||
|
||||
expect(state.page).toBe('auth');
|
||||
expect(state.motionDetection.profile).toBe('balanced');
|
||||
expect(state.clientPlaceholderText).toBe('Select a camera to view');
|
||||
expect(state.streamDiagnostics).toEqual({});
|
||||
});
|
||||
|
||||
it('setAppState merges top-level properties into the current store', () => {
|
||||
setAppState({ page: 'client', socketConnected: true });
|
||||
|
||||
const state = getAppState();
|
||||
expect(state.page).toBe('client');
|
||||
expect(state.socketConnected).toBe(true);
|
||||
expect(state.deviceToken).toBeNull();
|
||||
});
|
||||
|
||||
it('patchAppState derives the next state from the previous state', () => {
|
||||
patchAppState((state) => ({
|
||||
authForm: {
|
||||
...state.authForm,
|
||||
email: 'test@example.com'
|
||||
},
|
||||
toasts: [{ id: 'toast-1', title: 'Saved' }]
|
||||
}));
|
||||
|
||||
const state = getAppState();
|
||||
expect(state.authForm.email).toBe('test@example.com');
|
||||
expect(state.toasts).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('resetAppState restores defaults while keeping explicit overrides', () => {
|
||||
setAppState({ page: 'client', socketConnected: true });
|
||||
resetAppState({ page: 'settings' });
|
||||
|
||||
const state = getAppState();
|
||||
expect(state.page).toBe('settings');
|
||||
expect(state.socketConnected).toBe(false);
|
||||
expect(state.linkedCameras).toEqual([]);
|
||||
});
|
||||
|
||||
it('counts only unread motion notifications', () => {
|
||||
appState.set({
|
||||
...createInitialState(),
|
||||
motionNotifications: [
|
||||
{ id: 'n1', isRead: false },
|
||||
{ id: 'n2', isRead: true },
|
||||
{ id: 'n3', isRead: false }
|
||||
]
|
||||
});
|
||||
|
||||
expect(get(unreadNotificationsCount)).toBe(2);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user