test(webapp): add unit coverage for state and PWA flows

This commit is contained in:
2026-04-14 18:15:00 +01:00
parent 5f3daf7922
commit 2a8ce5c5e9
5 changed files with 256 additions and 17 deletions

View File

@@ -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);
});
});

View 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);
});
});