feat: Add hardened error handling and retries (#2451)
- Closes #1830 - Closes #2099
This commit is contained in:
committed by
dermotduffy
parent
4bc787e2b7
commit
47bcce93d3
@@ -360,7 +360,7 @@ describe('createSetReviewAction', () => {
|
||||
|
||||
describe('createNotificationAction', () => {
|
||||
it('should create notification action', () => {
|
||||
const notification = { text: 'test' };
|
||||
const notification = { body: { text: 'test' } };
|
||||
expect(createNotificationAction(notification)).toEqual({
|
||||
action: 'fire-dom-event',
|
||||
advanced_camera_card_action: 'notification',
|
||||
@@ -369,7 +369,7 @@ describe('createNotificationAction', () => {
|
||||
});
|
||||
|
||||
it('should create notification action with cardID', () => {
|
||||
const notification = { text: 'test' };
|
||||
const notification = { body: { text: 'test' } };
|
||||
expect(createNotificationAction(notification, { cardID: 'card_id' })).toEqual({
|
||||
action: 'fire-dom-event',
|
||||
advanced_camera_card_action: 'notification',
|
||||
|
||||
@@ -104,6 +104,7 @@ describe('getDiagnostics', () => {
|
||||
lang: 'en',
|
||||
ha_version: '2023.9.0',
|
||||
timezone: expect.anything(),
|
||||
issues: [],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -148,26 +149,33 @@ describe('getDiagnostics', () => {
|
||||
date: now,
|
||||
lang: 'en',
|
||||
timezone: expect.anything(),
|
||||
issues: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should include problems in diagnostics', async () => {
|
||||
it('should include issues in diagnostics', async () => {
|
||||
const deviceRegistryManager = mock<DeviceRegistryManager>();
|
||||
deviceRegistryManager.getMatchingDevices.mockResolvedValue([]);
|
||||
|
||||
const problems = {
|
||||
config_upgrade: true,
|
||||
legacy_resource: false,
|
||||
};
|
||||
const issues = new Map([
|
||||
[
|
||||
'config_upgrade' as const,
|
||||
{
|
||||
icon: 'mdi:update',
|
||||
severity: 'medium' as const,
|
||||
notification: { body: { text: 'test' } },
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
const result = await getDiagnostics(
|
||||
hass,
|
||||
deviceRegistryManager,
|
||||
{ cameras: [{ camera_entity: 'camera.office' }] },
|
||||
problems,
|
||||
issues,
|
||||
);
|
||||
|
||||
expect(result.problems).toEqual(problems);
|
||||
expect(result.issues).toEqual(['config_upgrade']);
|
||||
});
|
||||
|
||||
it('should fetch diagnostics without device model', async () => {
|
||||
@@ -194,6 +202,7 @@ describe('getDiagnostics', () => {
|
||||
date: now,
|
||||
lang: 'en',
|
||||
timezone: expect.anything(),
|
||||
issues: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { AdvancedCameraCardError } from '../../src/types';
|
||||
import { getContextFromError } from '../../src/utils/error-context';
|
||||
|
||||
describe('getContextFromError', () => {
|
||||
it('should return the context for an AdvancedCameraCardError with an object context', () => {
|
||||
const error = new AdvancedCameraCardError('boom', { foo: 'bar' });
|
||||
expect(getContextFromError(error)).toEqual({ foo: 'bar' });
|
||||
});
|
||||
|
||||
it('should return null for an AdvancedCameraCardError without context', () => {
|
||||
const error = new AdvancedCameraCardError('boom');
|
||||
expect(getContextFromError(error)).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for an AdvancedCameraCardError with null context', () => {
|
||||
const error = new AdvancedCameraCardError('boom', null);
|
||||
expect(getContextFromError(error)).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for an AdvancedCameraCardError with non-object context', () => {
|
||||
const error = new AdvancedCameraCardError('boom', 'string context');
|
||||
expect(getContextFromError(error)).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for a plain Error', () => {
|
||||
expect(getContextFromError(new Error('plain'))).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for non-error values', () => {
|
||||
expect(getContextFromError('string')).toBeNull();
|
||||
expect(getContextFromError(42)).toBeNull();
|
||||
expect(getContextFromError(null)).toBeNull();
|
||||
expect(getContextFromError(undefined)).toBeNull();
|
||||
expect(getContextFromError({})).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -6,9 +6,8 @@ describe('Initializer', () => {
|
||||
const initializer = new Initializer();
|
||||
|
||||
expect(initializer.isInitialized('foo')).toBeFalsy();
|
||||
expect(
|
||||
await initializer.initializeIfNecessary('foo', async () => true),
|
||||
).toBeTruthy();
|
||||
|
||||
await initializer.initializeIfNecessary('foo', async () => {});
|
||||
expect(initializer.isInitialized('foo')).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -16,9 +15,7 @@ describe('Initializer', () => {
|
||||
const initializer = new Initializer();
|
||||
|
||||
expect(initializer.isInitialized('foo')).toBeFalsy();
|
||||
expect(
|
||||
await initializer.initializeIfNecessary('foo', async () => true),
|
||||
).toBeTruthy();
|
||||
await initializer.initializeIfNecessary('foo');
|
||||
expect(initializer.isInitialized('foo')).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -26,12 +23,8 @@ describe('Initializer', () => {
|
||||
const initializer = new Initializer();
|
||||
|
||||
expect(initializer.isInitialized('foo')).toBeFalsy();
|
||||
expect(
|
||||
await initializer.initializeIfNecessary('foo', async () => true),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
await initializer.initializeIfNecessary('foo', async () => true),
|
||||
).toBeTruthy();
|
||||
await initializer.initializeIfNecessary('foo', async () => {});
|
||||
await initializer.initializeIfNecessary('foo', async () => {});
|
||||
expect(initializer.isInitialized('foo')).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -39,9 +32,11 @@ describe('Initializer', () => {
|
||||
const initializer = new Initializer();
|
||||
|
||||
expect(initializer.isInitialized('foo')).toBeFalsy();
|
||||
expect(
|
||||
await initializer.initializeIfNecessary('foo', async () => false),
|
||||
).toBeFalsy();
|
||||
await expect(
|
||||
initializer.initializeIfNecessary('foo', async () => {
|
||||
throw new Error('test');
|
||||
}),
|
||||
).rejects.toThrow('test');
|
||||
expect(initializer.isInitialized('foo')).toBeFalsy();
|
||||
});
|
||||
|
||||
@@ -49,20 +44,20 @@ describe('Initializer', () => {
|
||||
const initializer = new Initializer();
|
||||
|
||||
expect(initializer.isInitializedMultiple(['foo', 'bar'])).toBeFalsy();
|
||||
expect(
|
||||
await initializer.initializeMultipleIfNecessary({
|
||||
foo: async () => true,
|
||||
bar: async () => false,
|
||||
await expect(
|
||||
initializer.initializeMultipleIfNecessary({
|
||||
foo: async () => {},
|
||||
bar: async () => {
|
||||
throw new Error('test');
|
||||
},
|
||||
}),
|
||||
).toBeFalsy();
|
||||
).rejects.toThrow('test');
|
||||
|
||||
expect(initializer.isInitializedMultiple(['foo', 'bar'])).toBeFalsy();
|
||||
|
||||
expect(
|
||||
await initializer.initializeMultipleIfNecessary({
|
||||
bar: async () => true,
|
||||
}),
|
||||
).toBeTruthy();
|
||||
await initializer.initializeMultipleIfNecessary({
|
||||
bar: async () => {},
|
||||
});
|
||||
|
||||
expect(initializer.isInitializedMultiple(['foo', 'bar'])).toBeTruthy();
|
||||
});
|
||||
|
||||
+28
-15
@@ -1,7 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { Capabilities } from '../../src/camera-manager/capabilities';
|
||||
import { AdvancedCameraCardView } from '../../src/config/schema/common/const';
|
||||
import { IMAGE_VIEW_ZOOM_TARGET_SENTINEL } from '../../src/const';
|
||||
import { PTZMovementType } from '../../src/types';
|
||||
import {
|
||||
getPTZTarget,
|
||||
@@ -9,6 +8,8 @@ import {
|
||||
ptzActionToCapabilityKey,
|
||||
} from '../../src/utils/ptz';
|
||||
import { QueryResults } from '../../src/view/query-results';
|
||||
import { IMAGE_VIEW_TARGET_ID_SENTINEL } from '../../src/view/target-id';
|
||||
import * as targetId from '../../src/view/target-id';
|
||||
import {
|
||||
TestViewMedia,
|
||||
createCameraManager,
|
||||
@@ -18,7 +19,7 @@ import {
|
||||
|
||||
describe('getPTZTarget', () => {
|
||||
describe('in a viewer view', () => {
|
||||
it('with media', () => {
|
||||
it('should return target with media', () => {
|
||||
const media = [new TestViewMedia({ id: 'media-id' })];
|
||||
const view = createView({
|
||||
view: 'media',
|
||||
@@ -30,14 +31,14 @@ describe('getPTZTarget', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('without media', () => {
|
||||
it('should return null without media', () => {
|
||||
const view = createView({
|
||||
view: 'media',
|
||||
});
|
||||
expect(getPTZTarget(view, { cameraManager: createCameraManager() })).toBeNull();
|
||||
});
|
||||
|
||||
it('with true PTZ restriction', () => {
|
||||
it('should return null with true PTZ restriction', () => {
|
||||
const media = [new TestViewMedia({ id: 'media-id' })];
|
||||
const view = createView({
|
||||
view: 'media',
|
||||
@@ -49,15 +50,15 @@ describe('getPTZTarget', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('in image view', () => {
|
||||
it('should return target in image view', () => {
|
||||
expect(getPTZTarget(createView({ view: 'image' }))).toEqual({
|
||||
targetID: IMAGE_VIEW_ZOOM_TARGET_SENTINEL,
|
||||
targetID: IMAGE_VIEW_TARGET_ID_SENTINEL,
|
||||
type: 'digital',
|
||||
});
|
||||
});
|
||||
|
||||
describe('in live view', () => {
|
||||
it('without restriction with true PTZ capability', () => {
|
||||
it('should return PTZ target without restriction with true PTZ capability', () => {
|
||||
const view = createView({
|
||||
view: 'live',
|
||||
camera: 'camera-1',
|
||||
@@ -75,7 +76,7 @@ describe('getPTZTarget', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('without restriction without true PTZ capability', () => {
|
||||
it('should return digital target without restriction without true PTZ capability', () => {
|
||||
const view = createView({
|
||||
view: 'live',
|
||||
camera: 'camera-1',
|
||||
@@ -87,7 +88,7 @@ describe('getPTZTarget', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('with truePTZ restriction without true PTZ capability', () => {
|
||||
it('should return null with truePTZ restriction without true PTZ capability', () => {
|
||||
const view = createView({
|
||||
view: 'live',
|
||||
camera: 'camera-1',
|
||||
@@ -98,7 +99,7 @@ describe('getPTZTarget', () => {
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('with digitalPTZ restriction with true PTZ capability', () => {
|
||||
it('should return digital target with digitalPTZ restriction with true PTZ capability', () => {
|
||||
const view = createView({
|
||||
view: 'live',
|
||||
camera: 'camera-1',
|
||||
@@ -121,7 +122,7 @@ describe('getPTZTarget', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('without cameraID', () => {
|
||||
it('should return null without cameraID', () => {
|
||||
const view = createView({
|
||||
view: 'live',
|
||||
camera: null,
|
||||
@@ -141,10 +142,22 @@ describe('getPTZTarget', () => {
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should return null for a view with a targetID that is not viewer, image, or live', () => {
|
||||
// Force getViewTargetID to return a non-null value so that the early-return
|
||||
// guard passes, then present a view that is none of viewer/image/live to
|
||||
// exercise the final return null branch at the end of getPTZTarget.
|
||||
vi.spyOn(targetId, 'getViewTargetID').mockReturnValue('some-target');
|
||||
const view = createView({ view: 'timeline' });
|
||||
|
||||
expect(getPTZTarget(view)).toBeNull();
|
||||
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasCameraTruePTZ', () => {
|
||||
it('with true PTZ', () => {
|
||||
it('should return true with true PTZ', () => {
|
||||
const store = createStore([
|
||||
{
|
||||
cameraID: 'camera-1',
|
||||
@@ -155,12 +168,12 @@ describe('hasCameraTruePTZ', () => {
|
||||
expect(hasCameraTruePTZ(createCameraManager(store), 'camera-1')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('without true PTZ', () => {
|
||||
it('should return false without true PTZ', () => {
|
||||
expect(hasCameraTruePTZ(createCameraManager(createStore()), 'camera-1')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
it('ptzActionToCapabilityKey', () => {
|
||||
it('should map ptzActionToCapabilityKey correctly', () => {
|
||||
expect(ptzActionToCapabilityKey('left')).toBe('left');
|
||||
expect(ptzActionToCapabilityKey('right')).toBe('right');
|
||||
expect(ptzActionToCapabilityKey('up')).toBe('up');
|
||||
|
||||
Reference in New Issue
Block a user