@@ -0,0 +1,71 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
brandsUrl,
|
||||
extractDomainFromBrandUrl,
|
||||
isBrandUrl,
|
||||
} from '../../src/ha/brands-url';
|
||||
|
||||
describe('brandsUrl', () => {
|
||||
it('generates a basic icon url', () => {
|
||||
expect(brandsUrl({ domain: 'amcrest', type: 'icon' })).toBe(
|
||||
'https://brands.home-assistant.io/amcrest/icon.png',
|
||||
);
|
||||
});
|
||||
|
||||
it('generates a logo url with brand', () => {
|
||||
expect(brandsUrl({ domain: 'hikvision', type: 'logo', brand: true })).toBe(
|
||||
'https://brands.home-assistant.io/brands/hikvision/logo.png',
|
||||
);
|
||||
});
|
||||
|
||||
it('generates a dark optimized icon@2x url with fallback', () => {
|
||||
expect(
|
||||
brandsUrl({
|
||||
domain: 'unifi',
|
||||
type: 'icon@2x',
|
||||
useFallback: true,
|
||||
darkOptimized: true,
|
||||
}),
|
||||
).toBe('https://brands.home-assistant.io/_/unifi/dark_icon@2x.png');
|
||||
});
|
||||
|
||||
it('generates a logo@2x url with all options', () => {
|
||||
expect(
|
||||
brandsUrl({
|
||||
domain: 'dahua',
|
||||
type: 'logo@2x',
|
||||
useFallback: true,
|
||||
darkOptimized: true,
|
||||
brand: true,
|
||||
}),
|
||||
).toBe('https://brands.home-assistant.io/brands/_/dahua/dark_logo@2x.png');
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractDomainFromBrandUrl', () => {
|
||||
it('extracts domain from a brands url', () => {
|
||||
expect(
|
||||
extractDomainFromBrandUrl(
|
||||
'https://brands.home-assistant.io/brands/hikvision/logo.png',
|
||||
),
|
||||
).toBe('hikvision');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isBrandUrl', () => {
|
||||
it('returns true for a valid brands url', () => {
|
||||
expect(isBrandUrl('https://brands.home-assistant.io/amcrest/icon.png')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for a non-brands url', () => {
|
||||
expect(isBrandUrl('https://example.com/amcrest/icon.png')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for undefined', () => {
|
||||
expect(isBrandUrl(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for null', () => {
|
||||
expect(isBrandUrl(null)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { getViewMediaFromBrowseMediaArray } from '../../../src/ha/browse-media/browse-media-to-view-media';
|
||||
import {
|
||||
BrowseMediaMetadata,
|
||||
MEDIA_CLASS_IMAGE,
|
||||
MEDIA_CLASS_VIDEO,
|
||||
RichBrowseMedia,
|
||||
} from '../../../src/ha/browse-media/types';
|
||||
import { createBrowseMedia, createRichBrowseMedia } from '../../test-utils';
|
||||
|
||||
const createBrowseMediaChildren = (
|
||||
overrides: Partial<RichBrowseMedia<BrowseMediaMetadata>>[],
|
||||
) => {
|
||||
return overrides.map((override) => createRichBrowseMedia(override));
|
||||
};
|
||||
|
||||
describe('getViewMediaFromBrowseMediaArray', () => {
|
||||
it('should not ignore absent metadata', () => {
|
||||
const children = [createBrowseMedia()];
|
||||
const viewMedia = getViewMediaFromBrowseMediaArray(children, {
|
||||
cameraID: 'camera.office',
|
||||
});
|
||||
expect(viewMedia).toHaveLength(1);
|
||||
expect(viewMedia?.[0].getMediaType()).toBe('clip');
|
||||
expect(viewMedia?.[0].getID()).toBe('content_id');
|
||||
});
|
||||
|
||||
it('should ignore unknown media class', () => {
|
||||
const children = createBrowseMediaChildren([{ media_class: 'UNKNOWN' }]);
|
||||
expect(
|
||||
getViewMediaFromBrowseMediaArray(children, { cameraID: 'camera.office' }),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it('should generate clip view media', () => {
|
||||
const children = createBrowseMediaChildren([{ media_class: MEDIA_CLASS_VIDEO }]);
|
||||
const viewMedia = getViewMediaFromBrowseMediaArray(children, {
|
||||
cameraID: 'camera.office',
|
||||
});
|
||||
expect(viewMedia).toHaveLength(1);
|
||||
expect(viewMedia?.[0].getMediaType()).toBe('clip');
|
||||
expect(viewMedia?.[0].getID()).toBe('camera.office/2024-11-19 07:23:00');
|
||||
});
|
||||
|
||||
it('should generate snapshot view media', () => {
|
||||
const children = createBrowseMediaChildren([{ media_class: MEDIA_CLASS_IMAGE }]);
|
||||
const viewMedia = getViewMediaFromBrowseMediaArray(children, {
|
||||
cameraID: 'camera.office',
|
||||
});
|
||||
expect(viewMedia).toHaveLength(1);
|
||||
expect(viewMedia?.[0].getMediaType()).toBe('snapshot');
|
||||
expect(viewMedia?.[0].getID()).toBe('camera.office/2024-11-19 07:23:00');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
BrowseMediaEventViewMedia,
|
||||
BrowseMediaViewFolder,
|
||||
} from '../../../src/ha/browse-media/item';
|
||||
import { BrowseMediaViewItemFactory } from '../../../src/ha/browse-media/item-factory';
|
||||
import {
|
||||
MEDIA_CLASS_IMAGE,
|
||||
MEDIA_CLASS_VIDEO,
|
||||
} from '../../../src/ha/browse-media/types';
|
||||
import { ViewMediaType } from '../../../src/view/item';
|
||||
import { createBrowseMedia, createFolder } from '../../test-utils';
|
||||
|
||||
describe('BrowseMediaViewItemFactory', () => {
|
||||
it('returns null if can_expand is true without options.folder', () => {
|
||||
const browseMedia = createBrowseMedia({ can_expand: true });
|
||||
expect(BrowseMediaViewItemFactory.create(browseMedia)).toBeNull();
|
||||
});
|
||||
|
||||
it('returns a folder if can_expand is true with options.folder', () => {
|
||||
const browseMedia = createBrowseMedia({ can_expand: true });
|
||||
const result = BrowseMediaViewItemFactory.create(browseMedia, {
|
||||
folder: createFolder(),
|
||||
});
|
||||
expect(result).toBeInstanceOf(BrowseMediaViewFolder);
|
||||
});
|
||||
|
||||
it('returns BrowseMediaEventViewMedia for MEDIA_CLASS_VIDEO', () => {
|
||||
const browseMedia = createBrowseMedia({ media_class: MEDIA_CLASS_VIDEO });
|
||||
const result = BrowseMediaViewItemFactory.create(browseMedia);
|
||||
expect(result).toBeInstanceOf(BrowseMediaEventViewMedia);
|
||||
if (result instanceof BrowseMediaEventViewMedia) {
|
||||
expect(result?.getMediaType()).toBe(ViewMediaType.Clip);
|
||||
}
|
||||
});
|
||||
|
||||
it('returns BrowseMediaEventViewMedia for MEDIA_CLASS_VIDEO', () => {
|
||||
const browseMedia = createBrowseMedia({ media_class: MEDIA_CLASS_IMAGE });
|
||||
const result = BrowseMediaViewItemFactory.create(browseMedia);
|
||||
expect(result).toBeInstanceOf(BrowseMediaEventViewMedia);
|
||||
if (result instanceof BrowseMediaEventViewMedia) {
|
||||
expect(result?.getMediaType()).toBe(ViewMediaType.Snapshot);
|
||||
}
|
||||
});
|
||||
|
||||
it('returns null for unknown media_class', () => {
|
||||
const browseMedia = createBrowseMedia({ media_class: 'UNKNOWN' });
|
||||
expect(BrowseMediaViewItemFactory.create(browseMedia)).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,321 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
BrowseMediaEventViewMedia,
|
||||
BrowseMediaViewFolder,
|
||||
} from '../../../src/ha/browse-media/item';
|
||||
import { VideoContentType, ViewMediaType } from '../../../src/view/item';
|
||||
import {
|
||||
createBrowseMedia,
|
||||
createFolder,
|
||||
createRichBrowseMedia,
|
||||
} from '../../test-utils';
|
||||
|
||||
describe('BrowseMediaEventViewMedia', () => {
|
||||
describe('should set cameraID', () => {
|
||||
it('should set cameraID from the metadata', () => {
|
||||
const browseMedia = createRichBrowseMedia({
|
||||
media_content_id: 'media_content_id',
|
||||
_metadata: {
|
||||
startDate: new Date('2025-05-05T07:46:00Z'),
|
||||
endDate: new Date('2025-05-05T07:48:00Z'),
|
||||
cameraID: 'camera.office',
|
||||
},
|
||||
});
|
||||
|
||||
const viewMedia = new BrowseMediaEventViewMedia(ViewMediaType.Clip, browseMedia);
|
||||
expect(viewMedia.getCameraID()).toBe('camera.office');
|
||||
});
|
||||
|
||||
it('should set cameraID from the options', () => {
|
||||
const browseMedia = createRichBrowseMedia({
|
||||
media_content_id: 'media_content_id',
|
||||
_metadata: {
|
||||
startDate: new Date('2025-05-05T07:46:00Z'),
|
||||
endDate: new Date('2025-05-05T07:48:00Z'),
|
||||
cameraID: 'camera.office',
|
||||
},
|
||||
});
|
||||
|
||||
const viewMedia = new BrowseMediaEventViewMedia(ViewMediaType.Clip, browseMedia, {
|
||||
cameraID: 'camera.kitchen',
|
||||
});
|
||||
expect(viewMedia.getCameraID()).toBe('camera.kitchen');
|
||||
});
|
||||
});
|
||||
|
||||
describe('should set ID', () => {
|
||||
it('should set id from metadata', () => {
|
||||
const browseMedia = createRichBrowseMedia({
|
||||
media_content_id: 'media_content_id',
|
||||
_metadata: {
|
||||
startDate: new Date('2025-05-05T07:46:00Z'),
|
||||
endDate: new Date('2025-05-05T07:48:00Z'),
|
||||
cameraID: 'camera.office',
|
||||
},
|
||||
});
|
||||
|
||||
const viewMedia = new BrowseMediaEventViewMedia(ViewMediaType.Clip, browseMedia);
|
||||
expect(viewMedia.getID()).toBe('camera.office/2025-05-05 07:46:00');
|
||||
});
|
||||
|
||||
it('should set id from media_content_id from the options', () => {
|
||||
const browseMedia = createBrowseMedia({
|
||||
media_content_id: 'media_content_id',
|
||||
});
|
||||
|
||||
const viewMedia = new BrowseMediaEventViewMedia(ViewMediaType.Clip, browseMedia, {
|
||||
cameraID: 'camera.kitchen',
|
||||
});
|
||||
expect(viewMedia.getID()).toBe('media_content_id');
|
||||
});
|
||||
});
|
||||
|
||||
describe('should get start and end time', () => {
|
||||
it('should get start and end time from metadata', () => {
|
||||
const browseMedia = createRichBrowseMedia({
|
||||
media_content_id: 'media_content_id',
|
||||
_metadata: {
|
||||
startDate: new Date('2025-05-05T07:46:00Z'),
|
||||
endDate: new Date('2025-05-05T07:48:00Z'),
|
||||
cameraID: 'camera.office',
|
||||
},
|
||||
});
|
||||
|
||||
const viewMedia = new BrowseMediaEventViewMedia(ViewMediaType.Clip, browseMedia);
|
||||
expect(viewMedia.getStartTime()).toEqual(new Date('2025-05-05T07:46:00Z'));
|
||||
expect(viewMedia.getEndTime()).toEqual(new Date('2025-05-05T07:48:00Z'));
|
||||
});
|
||||
|
||||
it('should return null start and end time without metadata', () => {
|
||||
const browseMedia = createBrowseMedia({
|
||||
media_content_id: 'media_content_id',
|
||||
});
|
||||
|
||||
const viewMedia = new BrowseMediaEventViewMedia(ViewMediaType.Clip, browseMedia);
|
||||
expect(viewMedia.getStartTime()).toBeNull();
|
||||
expect(viewMedia.getEndTime()).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('should get video content type', () => {
|
||||
it('should get video content type for clip', () => {
|
||||
const browseMedia = createRichBrowseMedia();
|
||||
const viewMedia = new BrowseMediaEventViewMedia(ViewMediaType.Clip, browseMedia);
|
||||
expect(viewMedia.getVideoContentType()).toBe(VideoContentType.MP4);
|
||||
});
|
||||
|
||||
it('should get null for snapshot', () => {
|
||||
const browseMedia = createBrowseMedia();
|
||||
const viewMedia = new BrowseMediaEventViewMedia(
|
||||
ViewMediaType.Snapshot,
|
||||
browseMedia,
|
||||
);
|
||||
expect(viewMedia.getVideoContentType()).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it('should get content ID', () => {
|
||||
const browseMedia = createBrowseMedia({
|
||||
media_content_id: 'media_content_id',
|
||||
});
|
||||
|
||||
const viewMedia = new BrowseMediaEventViewMedia(ViewMediaType.Clip, browseMedia);
|
||||
expect(viewMedia.getContentID()).toEqual('media_content_id');
|
||||
});
|
||||
|
||||
describe('should get title', () => {
|
||||
it('should get title from metadata start time', () => {
|
||||
const browseMedia = createRichBrowseMedia({
|
||||
_metadata: {
|
||||
startDate: new Date('2025-05-05T07:46:00Z'),
|
||||
endDate: new Date('2025-05-05T07:48:00Z'),
|
||||
cameraID: 'camera.office',
|
||||
},
|
||||
});
|
||||
|
||||
const viewMedia = new BrowseMediaEventViewMedia(ViewMediaType.Clip, browseMedia);
|
||||
expect(viewMedia.getTitle()).toEqual('2025-05-05 07:46');
|
||||
});
|
||||
|
||||
it('should get title without metadata', () => {
|
||||
const browseMedia = createBrowseMedia({
|
||||
title: 'Test Title',
|
||||
});
|
||||
|
||||
const viewMedia = new BrowseMediaEventViewMedia(ViewMediaType.Clip, browseMedia);
|
||||
expect(viewMedia.getTitle()).toEqual('Test Title');
|
||||
});
|
||||
});
|
||||
|
||||
it('should get thumbnail', () => {
|
||||
const browseMedia = createRichBrowseMedia({
|
||||
thumbnail: 'thumbnail.jpg',
|
||||
});
|
||||
|
||||
const viewMedia = new BrowseMediaEventViewMedia(ViewMediaType.Clip, browseMedia);
|
||||
expect(viewMedia.getThumbnail()).toEqual('thumbnail.jpg');
|
||||
});
|
||||
|
||||
describe('should get what', () => {
|
||||
it('should get what from metadata', () => {
|
||||
const browseMedia = createRichBrowseMedia({
|
||||
_metadata: {
|
||||
startDate: new Date('2025-05-05T07:46:00Z'),
|
||||
endDate: new Date('2025-05-05T07:48:00Z'),
|
||||
cameraID: 'camera.office',
|
||||
what: ['car'],
|
||||
},
|
||||
});
|
||||
|
||||
const viewMedia = new BrowseMediaEventViewMedia(ViewMediaType.Clip, browseMedia);
|
||||
expect(viewMedia.getWhat()).toEqual(['car']);
|
||||
});
|
||||
|
||||
it('should return null without metadata', () => {
|
||||
const browseMedia = createBrowseMedia();
|
||||
|
||||
const viewMedia = new BrowseMediaEventViewMedia(ViewMediaType.Clip, browseMedia);
|
||||
expect(viewMedia.getWhat()).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return null for score', () => {
|
||||
const browseMedia = createBrowseMedia();
|
||||
const viewMedia = new BrowseMediaEventViewMedia(ViewMediaType.Clip, browseMedia);
|
||||
expect(viewMedia.getScore()).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for tags', () => {
|
||||
const browseMedia = createBrowseMedia();
|
||||
const viewMedia = new BrowseMediaEventViewMedia(ViewMediaType.Clip, browseMedia);
|
||||
expect(viewMedia.getTags()).toBeNull();
|
||||
});
|
||||
|
||||
describe('should determine what is groupable', () => {
|
||||
it('should return true when groupable', () => {
|
||||
const browseMedia_1 = createRichBrowseMedia({
|
||||
_metadata: {
|
||||
startDate: new Date('2025-05-05T07:46:00Z'),
|
||||
endDate: new Date('2025-05-05T07:48:00Z'),
|
||||
cameraID: 'camera.office',
|
||||
what: ['car'],
|
||||
},
|
||||
});
|
||||
|
||||
const browseMedia_2 = createRichBrowseMedia({
|
||||
_metadata: {
|
||||
startDate: new Date('2025-05-05T07:48:00Z'),
|
||||
endDate: new Date('2025-05-05T07:50:00Z'),
|
||||
cameraID: 'camera.office',
|
||||
what: ['car'],
|
||||
},
|
||||
});
|
||||
|
||||
const viewMedia_1 = new BrowseMediaEventViewMedia(
|
||||
ViewMediaType.Clip,
|
||||
browseMedia_1,
|
||||
);
|
||||
const viewMedia_2 = new BrowseMediaEventViewMedia(
|
||||
ViewMediaType.Clip,
|
||||
browseMedia_2,
|
||||
);
|
||||
expect(viewMedia_1.isGroupableWith(viewMedia_2)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when media types are different', () => {
|
||||
const browseMedia_1 = createRichBrowseMedia({
|
||||
_metadata: {
|
||||
startDate: new Date('2025-05-05T07:46:00Z'),
|
||||
endDate: new Date('2025-05-05T07:48:00Z'),
|
||||
cameraID: 'camera.office',
|
||||
what: ['car'],
|
||||
},
|
||||
});
|
||||
|
||||
const browseMedia_2 = createRichBrowseMedia({
|
||||
_metadata: {
|
||||
startDate: new Date('2025-05-05T07:48:00Z'),
|
||||
endDate: new Date('2025-05-05T07:50:00Z'),
|
||||
cameraID: 'camera.office',
|
||||
what: ['car'],
|
||||
},
|
||||
});
|
||||
|
||||
const viewMedia_1 = new BrowseMediaEventViewMedia(
|
||||
ViewMediaType.Clip,
|
||||
browseMedia_1,
|
||||
);
|
||||
const viewMedia_2 = new BrowseMediaEventViewMedia(
|
||||
ViewMediaType.Snapshot,
|
||||
browseMedia_2,
|
||||
);
|
||||
expect(viewMedia_1.isGroupableWith(viewMedia_2)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('should set icon', () => {
|
||||
it('should set icon from known media class', () => {
|
||||
const browseMedia = createBrowseMedia({
|
||||
media_class: 'channel',
|
||||
});
|
||||
|
||||
const media = new BrowseMediaEventViewMedia(ViewMediaType.Snapshot, browseMedia);
|
||||
|
||||
expect(media.getIcon()).toBe('mdi:television-classic');
|
||||
});
|
||||
|
||||
it('should set null icon from unknown media class', () => {
|
||||
const browseMedia = createBrowseMedia({
|
||||
media_class: 'unknown',
|
||||
});
|
||||
|
||||
const viewMedia = new BrowseMediaEventViewMedia(
|
||||
ViewMediaType.Snapshot,
|
||||
browseMedia,
|
||||
);
|
||||
expect(viewMedia.getIcon()).toBeNull();
|
||||
});
|
||||
|
||||
it('should set null icon from null media class', () => {
|
||||
const browseMedia = createBrowseMedia({
|
||||
media_class: undefined,
|
||||
});
|
||||
|
||||
const viewMedia = new BrowseMediaEventViewMedia(
|
||||
ViewMediaType.Snapshot,
|
||||
browseMedia,
|
||||
);
|
||||
expect(viewMedia.getIcon()).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('BrowseMediaViewFolder', () => {
|
||||
it('should set folder', () => {
|
||||
const folder = createFolder();
|
||||
const browseMedia = createBrowseMedia();
|
||||
|
||||
const viewMedia = new BrowseMediaViewFolder(folder, browseMedia);
|
||||
expect(viewMedia.getFolder()).toEqual(folder);
|
||||
});
|
||||
|
||||
describe('should set icon', () => {
|
||||
it('should set icon from known children media class', () => {
|
||||
const browseMedia = createBrowseMedia({
|
||||
children_media_class: 'album',
|
||||
});
|
||||
|
||||
const viewMedia = new BrowseMediaViewFolder(createFolder(), browseMedia);
|
||||
expect(viewMedia.getIcon()).toBe('mdi:album');
|
||||
});
|
||||
|
||||
it('should set null icon from unknown children media class', () => {
|
||||
const browseMedia = createBrowseMedia({
|
||||
children_media_class: 'unknown',
|
||||
});
|
||||
|
||||
const viewMedia = new BrowseMediaViewFolder(createFolder(), browseMedia);
|
||||
expect(viewMedia.getIcon()).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
import { expect, it } from 'vitest';
|
||||
import { browseMediaSchema } from '../../../src/ha/browse-media/types';
|
||||
import { createBrowseMedia } from '../../test-utils';
|
||||
|
||||
it('should lazy evaluate lazy recursive browse media schema', () => {
|
||||
expect(browseMediaSchema.parse(createBrowseMedia())).toEqual(createBrowseMedia());
|
||||
});
|
||||
@@ -0,0 +1,470 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
BrowseMedia,
|
||||
BrowseMediaCache,
|
||||
RichBrowseMedia,
|
||||
browseMediaSchema,
|
||||
} from '../../../src/ha/browse-media/types';
|
||||
import { BrowseMediaStep, BrowseMediaWalker } from '../../../src/ha/browse-media/walker';
|
||||
import { homeAssistantWSRequest } from '../../../src/ha/ws-request';
|
||||
import { createBrowseMedia, createHASS } from '../../test-utils';
|
||||
|
||||
vi.mock('../../../src/ha/ws-request');
|
||||
|
||||
describe('BrowseMediaWalker', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return an empty array if steps are null or empty', async () => {
|
||||
const walker = new BrowseMediaWalker();
|
||||
const hass = createHASS();
|
||||
expect(await walker.walk(hass, null)).toEqual([]);
|
||||
expect(await walker.walk(hass, [])).toEqual([]);
|
||||
});
|
||||
|
||||
it('should perform a simple walk with one step', async () => {
|
||||
const child = createBrowseMedia();
|
||||
const parent = createBrowseMedia({
|
||||
media_content_id: 'media/parent',
|
||||
children: [child],
|
||||
});
|
||||
|
||||
vi.mocked(homeAssistantWSRequest).mockResolvedValue(parent);
|
||||
|
||||
const hass = createHASS();
|
||||
const walker = new BrowseMediaWalker();
|
||||
const result = await walker.walk(hass, [
|
||||
{
|
||||
targets: ['media/parent'],
|
||||
},
|
||||
]);
|
||||
|
||||
expect(homeAssistantWSRequest).toBeCalledWith(hass, browseMediaSchema, {
|
||||
type: 'media_source/browse_media',
|
||||
media_content_id: 'media/parent',
|
||||
});
|
||||
expect(result).toEqual([child]);
|
||||
});
|
||||
|
||||
it('should filter media using a matcher', async () => {
|
||||
const childToKeep = createBrowseMedia({
|
||||
media_content_id: 'media/child-keep',
|
||||
});
|
||||
const childToFilter = createBrowseMedia({
|
||||
media_content_id: 'media/child-filter',
|
||||
});
|
||||
|
||||
const parent = createBrowseMedia({
|
||||
media_content_id: 'media/parent',
|
||||
media_content_type: 'directory',
|
||||
title: 'Parent',
|
||||
children: [childToKeep, childToFilter],
|
||||
});
|
||||
|
||||
vi.mocked(homeAssistantWSRequest).mockResolvedValue(parent);
|
||||
|
||||
const hass = createHASS();
|
||||
const walker = new BrowseMediaWalker();
|
||||
|
||||
const steps: BrowseMediaStep<undefined>[] = [
|
||||
{
|
||||
targets: ['media/parent'],
|
||||
matcher: (media) => media.media_content_id === 'media/child-keep',
|
||||
},
|
||||
];
|
||||
|
||||
const result = await walker.walk(hass, steps);
|
||||
|
||||
expect(homeAssistantWSRequest).toBeCalledWith(hass, browseMediaSchema, {
|
||||
type: 'media_source/browse_media',
|
||||
media_content_id: 'media/parent',
|
||||
});
|
||||
expect(result).toEqual([childToKeep]);
|
||||
});
|
||||
|
||||
interface TestMetadata {
|
||||
custom: string;
|
||||
}
|
||||
|
||||
describe('should generate metadata', async () => {
|
||||
it('should generate simple metadata', async () => {
|
||||
const child = createBrowseMedia({
|
||||
media_content_id: 'media/child',
|
||||
});
|
||||
|
||||
const parent = createBrowseMedia({
|
||||
media_content_id: 'media/parent',
|
||||
children: [child],
|
||||
});
|
||||
|
||||
const metadataGenerator = (
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_media: BrowseMedia,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_parentTarget?: RichBrowseMedia<TestMetadata>,
|
||||
) => ({
|
||||
custom: 'foo',
|
||||
});
|
||||
|
||||
vi.mocked(homeAssistantWSRequest).mockResolvedValue(parent);
|
||||
|
||||
const steps: BrowseMediaStep<TestMetadata>[] = [
|
||||
{
|
||||
targets: ['media/parent'],
|
||||
metadataGenerator,
|
||||
},
|
||||
];
|
||||
|
||||
const walker = new BrowseMediaWalker();
|
||||
const result = await walker.walk(createHASS(), steps);
|
||||
|
||||
expect(result.length).toBe(1);
|
||||
expect(result[0]?._metadata).toEqual({
|
||||
custom: 'foo',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle parents without children', async () => {
|
||||
const parent = createBrowseMedia({
|
||||
media_content_id: 'media/parent-with-free-time',
|
||||
children: null,
|
||||
});
|
||||
|
||||
const metadataGenerator = (
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_media: BrowseMedia,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_parentTarget?: RichBrowseMedia<TestMetadata>,
|
||||
) => ({
|
||||
custom: 'foo',
|
||||
});
|
||||
|
||||
vi.mocked(homeAssistantWSRequest).mockResolvedValue(parent);
|
||||
|
||||
const steps: BrowseMediaStep<TestMetadata>[] = [
|
||||
{
|
||||
targets: ['media/parent'],
|
||||
metadataGenerator,
|
||||
},
|
||||
];
|
||||
|
||||
const walker = new BrowseMediaWalker();
|
||||
const result = await walker.walk(createHASS(), steps);
|
||||
|
||||
expect(result.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle metadata generator that returns null', async () => {
|
||||
const child = createBrowseMedia({
|
||||
media_content_id: 'media/child',
|
||||
});
|
||||
const parent = createBrowseMedia({
|
||||
media_content_id: 'media/parent',
|
||||
children: [child],
|
||||
});
|
||||
|
||||
const metadataGenerator = (
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_media: BrowseMedia,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_parentTarget?: RichBrowseMedia<TestMetadata>,
|
||||
) => null;
|
||||
|
||||
vi.mocked(homeAssistantWSRequest).mockResolvedValue(parent);
|
||||
|
||||
const steps: BrowseMediaStep<TestMetadata>[] = [
|
||||
{
|
||||
targets: ['media/parent'],
|
||||
metadataGenerator,
|
||||
},
|
||||
];
|
||||
|
||||
const walker = new BrowseMediaWalker();
|
||||
const result = await walker.walk(createHASS(), steps);
|
||||
|
||||
expect(result).toEqual([child]);
|
||||
expect(result[0]?._metadata).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle recurisve metadata', async () => {
|
||||
const grandchild = createBrowseMedia({
|
||||
media_content_id: 'media/grandchild',
|
||||
can_expand: false,
|
||||
can_play: true,
|
||||
});
|
||||
const subparent = createBrowseMedia({
|
||||
media_content_id: 'media/subparent',
|
||||
can_expand: true,
|
||||
children: [grandchild],
|
||||
});
|
||||
const parent = createBrowseMedia({
|
||||
media_content_id: 'media/parent',
|
||||
can_expand: true,
|
||||
children: [subparent],
|
||||
});
|
||||
|
||||
const metadataGenerator = (
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_media: BrowseMedia,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_parentTarget?: RichBrowseMedia<TestMetadata>,
|
||||
) => ({
|
||||
custom: 'foo',
|
||||
});
|
||||
|
||||
vi.mocked(homeAssistantWSRequest).mockImplementation(
|
||||
async (_hass, _schema, request) => {
|
||||
if (request.media_content_id === 'media/parent') {
|
||||
return parent;
|
||||
}
|
||||
if (request.media_content_id === 'media/subparent') {
|
||||
return subparent;
|
||||
}
|
||||
throw new Error(
|
||||
`Unexpected media_content_id in mock: ${request.media_content_id}`,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const steps: BrowseMediaStep<TestMetadata>[] = [
|
||||
{
|
||||
targets: ['media/parent'],
|
||||
metadataGenerator,
|
||||
advance: (matchedDirs) => {
|
||||
return matchedDirs.map((dir) => ({
|
||||
targets: [dir],
|
||||
metadataGenerator,
|
||||
}));
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const hass = createHASS();
|
||||
const walker = new BrowseMediaWalker();
|
||||
const result = await walker.walk(hass, steps);
|
||||
|
||||
expect(result.length).toBe(1);
|
||||
expect(result[0].media_content_id).toBe('media/grandchild');
|
||||
expect(result[0]?._metadata).toEqual({
|
||||
custom: 'foo',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should sort media using a sorter', async () => {
|
||||
const child_1 = createBrowseMedia({
|
||||
media_content_id: 'media/child-1',
|
||||
});
|
||||
const child_2 = createBrowseMedia({
|
||||
media_content_id: 'media/child-2',
|
||||
});
|
||||
const parent = createBrowseMedia({
|
||||
children: [child_2, child_1],
|
||||
});
|
||||
|
||||
vi.mocked(homeAssistantWSRequest).mockResolvedValue(parent);
|
||||
|
||||
const steps: BrowseMediaStep<undefined>[] = [
|
||||
{
|
||||
targets: ['media/parent'],
|
||||
sorter: (media) =>
|
||||
[...media].sort((a, b) =>
|
||||
a.media_content_id.localeCompare(b.media_content_id),
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const walker = new BrowseMediaWalker();
|
||||
const result = await walker.walk(createHASS(), steps);
|
||||
expect(result.map((m) => m.media_content_id)).toEqual([
|
||||
'media/child-1',
|
||||
'media/child-2',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should exit early when earlyExit returns true', async () => {
|
||||
const child_1 = createBrowseMedia({
|
||||
media_content_id: 'media/child-1',
|
||||
});
|
||||
const child_2 = createBrowseMedia({
|
||||
media_content_id: 'media/child-2',
|
||||
});
|
||||
const parent_1 = createBrowseMedia({
|
||||
media_content_id: 'media/parent-1',
|
||||
children: [child_1],
|
||||
});
|
||||
const parent_2 = createBrowseMedia({
|
||||
media_content_id: 'media/parent-2',
|
||||
children: [child_2],
|
||||
});
|
||||
|
||||
vi.mocked(homeAssistantWSRequest).mockResolvedValueOnce(parent_1);
|
||||
|
||||
vi.mocked(homeAssistantWSRequest).mockImplementation(
|
||||
async (_hass, _schema, request) => {
|
||||
if (request.media_content_id === 'media/parent-1') {
|
||||
return parent_1;
|
||||
}
|
||||
if (request.media_content_id === 'media/parent-2') {
|
||||
return parent_2;
|
||||
}
|
||||
throw new Error('Unexpected request');
|
||||
},
|
||||
);
|
||||
|
||||
const steps: BrowseMediaStep<undefined>[] = [
|
||||
{
|
||||
targets: ['media/parent-1', 'media/parent-2'],
|
||||
concurrency: 1,
|
||||
earlyExit: (media) => media.length >= 1,
|
||||
},
|
||||
];
|
||||
|
||||
const hass = createHASS();
|
||||
const walker = new BrowseMediaWalker();
|
||||
const result = await walker.walk(hass, steps);
|
||||
|
||||
expect(result).toEqual([child_1]);
|
||||
expect(homeAssistantWSRequest).toBeCalledTimes(1);
|
||||
expect(homeAssistantWSRequest).toBeCalledWith(hass, browseMediaSchema, {
|
||||
type: 'media_source/browse_media',
|
||||
media_content_id: 'media/parent-1',
|
||||
});
|
||||
});
|
||||
|
||||
it('should advance to next steps for recursive walking', async () => {
|
||||
const grandchild = createBrowseMedia({
|
||||
media_content_id: 'media/grandchild',
|
||||
can_expand: false,
|
||||
can_play: true,
|
||||
});
|
||||
const subparent = createBrowseMedia({
|
||||
media_content_id: 'media/subparent',
|
||||
can_expand: true,
|
||||
children: [grandchild],
|
||||
});
|
||||
const parent = createBrowseMedia({
|
||||
media_content_id: 'media/parent',
|
||||
can_expand: true,
|
||||
children: [subparent],
|
||||
});
|
||||
|
||||
vi.mocked(homeAssistantWSRequest).mockImplementation(
|
||||
async (_hass, _schema, request) => {
|
||||
if (request.media_content_id === 'media/parent') {
|
||||
return parent;
|
||||
}
|
||||
if (request.media_content_id === 'media/subparent') {
|
||||
return subparent;
|
||||
}
|
||||
throw new Error(
|
||||
`Unexpected media_content_id in mock: ${request.media_content_id}`,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const steps: BrowseMediaStep<undefined>[] = [
|
||||
{
|
||||
targets: ['media/parent'],
|
||||
matcher: (media) => media.can_expand === true,
|
||||
advance: (matchedDirs) => {
|
||||
return matchedDirs.map((dir) => ({
|
||||
targets: [dir],
|
||||
matcher: (childMedia) => childMedia.can_play === true,
|
||||
}));
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const hass = createHASS();
|
||||
const walker = new BrowseMediaWalker();
|
||||
const result = await walker.walk(hass, steps);
|
||||
|
||||
expect(result.length).toBe(1);
|
||||
expect(result[0].media_content_id).toBe('media/grandchild');
|
||||
|
||||
expect(homeAssistantWSRequest).toHaveBeenCalledTimes(2);
|
||||
expect(homeAssistantWSRequest).toHaveBeenCalledWith(hass, browseMediaSchema, {
|
||||
type: 'media_source/browse_media',
|
||||
media_content_id: 'media/parent',
|
||||
});
|
||||
expect(homeAssistantWSRequest).toHaveBeenCalledWith(hass, browseMediaSchema, {
|
||||
type: 'media_source/browse_media',
|
||||
media_content_id: 'media/subparent',
|
||||
});
|
||||
});
|
||||
|
||||
it('should use cache when available', async () => {
|
||||
const child = createBrowseMedia({
|
||||
media_content_id: 'media/child',
|
||||
});
|
||||
const parent = createBrowseMedia({
|
||||
media_content_id: 'media/parent',
|
||||
children: [child],
|
||||
});
|
||||
|
||||
vi.mocked(homeAssistantWSRequest).mockResolvedValue(parent);
|
||||
|
||||
const cache = new BrowseMediaCache();
|
||||
const hass = createHASS();
|
||||
const walker = new BrowseMediaWalker();
|
||||
|
||||
expect(await walker.walk(hass, [{ targets: ['media/parent'] }], { cache })).toEqual([
|
||||
child,
|
||||
]);
|
||||
|
||||
expect(homeAssistantWSRequest).toBeCalledTimes(1);
|
||||
expect(cache.has('media/parent')).toBe(true);
|
||||
expect(cache.get('media/parent')).toEqual(parent);
|
||||
|
||||
expect(await walker.walk(hass, [{ targets: ['media/parent'] }], { cache })).toEqual([
|
||||
child,
|
||||
]);
|
||||
expect(homeAssistantWSRequest).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should process multiple targets and combine their children', async () => {
|
||||
const child_1 = createBrowseMedia({
|
||||
media_content_id: 'media/child-1',
|
||||
});
|
||||
const parent_1 = createBrowseMedia({
|
||||
media_content_id: 'media/parent-1',
|
||||
children: [child_1],
|
||||
});
|
||||
const child_2 = createBrowseMedia({
|
||||
media_content_id: 'media/child-2',
|
||||
});
|
||||
const parent_2 = createBrowseMedia({
|
||||
media_content_id: 'media/parent-2',
|
||||
children: [child_2],
|
||||
});
|
||||
|
||||
vi.mocked(homeAssistantWSRequest).mockImplementation(
|
||||
async (_hass, _schema, request) => {
|
||||
if (request.media_content_id === 'media/parent-1') {
|
||||
return parent_1;
|
||||
}
|
||||
if (request.media_content_id === 'media/parent-2') {
|
||||
return parent_2;
|
||||
}
|
||||
throw new Error('Unexpected request');
|
||||
},
|
||||
);
|
||||
|
||||
const steps: BrowseMediaStep<undefined>[] = [
|
||||
{
|
||||
targets: ['media/parent-1', 'media/parent-2'],
|
||||
},
|
||||
];
|
||||
|
||||
const hass = createHASS();
|
||||
const walker = new BrowseMediaWalker();
|
||||
const result = await walker.walk(hass, steps);
|
||||
|
||||
expect(homeAssistantWSRequest).toHaveBeenCalledTimes(2);
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result).toContainEqual(child_1);
|
||||
expect(result).toContainEqual(child_2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import { sub } from 'date-fns';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { isMediaWithinDates } from '../../../src/ha/browse-media/within-dates';
|
||||
import { createBrowseMedia, createRichBrowseMedia } from '../../test-utils';
|
||||
|
||||
describe('isMediaWithinDates', () => {
|
||||
const rangeStart = new Date('2024-11-19T07:00:00');
|
||||
const rangeEnd = new Date('2024-11-19T08:00:00');
|
||||
|
||||
it('should never match media without metadata', () => {
|
||||
const media = createBrowseMedia();
|
||||
expect(isMediaWithinDates(media, rangeStart, rangeEnd)).toBe(false);
|
||||
});
|
||||
|
||||
it('should always match media without start or end date', () => {
|
||||
expect(isMediaWithinDates(createRichBrowseMedia(), undefined, undefined)).toBe(true);
|
||||
});
|
||||
|
||||
it('should match without a start date in the range', () => {
|
||||
expect(isMediaWithinDates(createRichBrowseMedia(), undefined, rangeEnd)).toBe(true);
|
||||
});
|
||||
|
||||
it('should match without an end date in the range', () => {
|
||||
expect(isMediaWithinDates(createRichBrowseMedia(), rangeStart, undefined)).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('should match when ranges overlap', () => {
|
||||
expect(isMediaWithinDates(createRichBrowseMedia(), rangeStart, rangeEnd)).toBe(true);
|
||||
});
|
||||
|
||||
it('should not match when ranges do not overlap', () => {
|
||||
expect(
|
||||
isMediaWithinDates(
|
||||
createRichBrowseMedia(),
|
||||
sub(rangeStart, { days: 1 }),
|
||||
sub(rangeEnd, { days: 1 }),
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { canonicalizeHAURL } from '../../src/ha/canonical-url';
|
||||
import { createHASS } from '../test-utils';
|
||||
|
||||
describe('canonicalizeHAURL', () => {
|
||||
it('returns canonicalized URL for HA relative URL', () => {
|
||||
const hass = createHASS();
|
||||
hass.hassUrl = vi.fn((url) => 'hass:' + url);
|
||||
|
||||
const url = '/media/local/file.mp4';
|
||||
expect(canonicalizeHAURL(hass, url)).toBe('hass:/media/local/file.mp4');
|
||||
});
|
||||
|
||||
it('returns original URL for absolute URL', () => {
|
||||
const url = 'https://card.camera/file.mp4';
|
||||
expect(canonicalizeHAURL(createHASS(), url)).toBe(url);
|
||||
});
|
||||
|
||||
it('returns null if url is null', () => {
|
||||
expect(canonicalizeHAURL(createHASS())).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { getMediaDownloadPath } from '../../src/ha/download';
|
||||
import { ResolvedMediaCache, resolveMedia } from '../../src/ha/resolved-media.js';
|
||||
import { createHASS } from '../test-utils';
|
||||
|
||||
vi.mock('../../src/ha/canonical-url.js', () => ({
|
||||
canonicalizeHAURL: vi.fn((_hass, url) => `canonicalized:${url}`),
|
||||
}));
|
||||
vi.mock('../../src/ha/resolved-media.js', () => ({
|
||||
resolveMedia: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('getMediaDownloadPath', () => {
|
||||
it('returns null if contentID is undefined', async () => {
|
||||
expect(
|
||||
await getMediaDownloadPath(createHASS(), undefined, mock<ResolvedMediaCache>()),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null if contentID is null', async () => {
|
||||
expect(
|
||||
await getMediaDownloadPath(createHASS(), null, mock<ResolvedMediaCache>()),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null if resolveMedia returns null', async () => {
|
||||
const hass = createHASS();
|
||||
const resolvedMediaCache = mock<ResolvedMediaCache>();
|
||||
vi.mocked(resolveMedia).mockResolvedValueOnce(null);
|
||||
|
||||
expect(await getMediaDownloadPath(hass, 'id-1', resolvedMediaCache)).toBeNull();
|
||||
expect(resolveMedia).toHaveBeenCalledWith(hass, 'id-1', resolvedMediaCache);
|
||||
});
|
||||
|
||||
it('returns endpoint if resolveMedia returns a url', async () => {
|
||||
const hass = createHASS();
|
||||
const resolvedMediaCache = mock<ResolvedMediaCache>();
|
||||
vi.mocked(resolveMedia).mockResolvedValueOnce({
|
||||
url: '/media/path.mp4',
|
||||
mime_type: 'video/mp4',
|
||||
});
|
||||
|
||||
expect(await getMediaDownloadPath(hass, 'id-1', resolvedMediaCache)).toEqual({
|
||||
endpoint: 'canonicalized:/media/path.mp4',
|
||||
});
|
||||
expect(resolveMedia).toBeCalledWith(hass, 'id-1', resolvedMediaCache);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { getEntityStateTranslation } from '../../src/ha/entity-state-translation';
|
||||
import { createHASS, createRegistryEntity, createStateEntity } from '../test-utils';
|
||||
|
||||
describe('getEntityStateTranslation', () => {
|
||||
it('returns null if state is missing', () => {
|
||||
expect(getEntityStateTranslation(createHASS(), 'light.office')).toBeNull();
|
||||
});
|
||||
|
||||
it('returns translation_key translation if available', () => {
|
||||
const entity = createRegistryEntity({
|
||||
entity_id: 'light.office',
|
||||
translation_key: 'translation_key',
|
||||
});
|
||||
const hass = createHASS({
|
||||
'light.office': createStateEntity({
|
||||
state: 'on',
|
||||
attributes: {},
|
||||
}),
|
||||
});
|
||||
vi.mocked(hass.localize).mockReturnValue('Translated State');
|
||||
|
||||
expect(getEntityStateTranslation(hass, 'light.office', { entity })).toBe(
|
||||
'Translated State',
|
||||
);
|
||||
});
|
||||
|
||||
it('returns device_class translation if translation_key not available', () => {
|
||||
const entity = createRegistryEntity({
|
||||
entity_id: 'light.office',
|
||||
});
|
||||
const hass = createHASS({
|
||||
'light.office': createStateEntity({
|
||||
state: 'on',
|
||||
attributes: {
|
||||
device_class: 'light',
|
||||
},
|
||||
}),
|
||||
});
|
||||
vi.mocked(hass.localize).mockReturnValue('Translated State');
|
||||
|
||||
expect(getEntityStateTranslation(hass, 'light.office', { entity })).toBe(
|
||||
'Translated State',
|
||||
);
|
||||
});
|
||||
|
||||
it('returns default translation if translation_key not available', () => {
|
||||
const entity = createRegistryEntity({
|
||||
entity_id: 'light.office',
|
||||
});
|
||||
const hass = createHASS({
|
||||
'light.office': createStateEntity({
|
||||
state: 'on',
|
||||
}),
|
||||
});
|
||||
vi.mocked(hass.localize).mockReturnValue('Translated State');
|
||||
|
||||
expect(getEntityStateTranslation(hass, 'light.office', { entity })).toBe(
|
||||
'Translated State',
|
||||
);
|
||||
});
|
||||
|
||||
it('returns raw state if no translation found', () => {
|
||||
const entity = createRegistryEntity({
|
||||
entity_id: 'light.office',
|
||||
});
|
||||
const hass = createHASS({
|
||||
'light.office': createStateEntity({
|
||||
state: 'on',
|
||||
}),
|
||||
});
|
||||
vi.mocked(hass.localize).mockReturnValue('');
|
||||
|
||||
expect(getEntityStateTranslation(hass, 'light.office', { entity })).toBe('on');
|
||||
});
|
||||
|
||||
it('uses passed in state', () => {
|
||||
const entity = createRegistryEntity({
|
||||
entity_id: 'light.office',
|
||||
});
|
||||
const hass = createHASS();
|
||||
vi.mocked(hass.localize).mockReturnValue('');
|
||||
|
||||
expect(
|
||||
getEntityStateTranslation(hass, 'light.office', { entity, state: 'off' }),
|
||||
).toBe('off');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { fireHASSEvent } from '../../src/ha/fire-hass-event';
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('fireHASSEvent', () => {
|
||||
it('should fire an event with specified type and detail', () => {
|
||||
const target = document.createElement('div');
|
||||
const handler = vi.fn();
|
||||
|
||||
const type = 'll-custom';
|
||||
const detail = { action: 'test' };
|
||||
|
||||
target.addEventListener(type, handler);
|
||||
|
||||
fireHASSEvent(target, type, detail);
|
||||
|
||||
expect(handler).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
detail,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { getEntitiesFromHASS } from '../../src/ha/get-entities';
|
||||
import { createHASS, createStateEntity } from '../test-utils';
|
||||
|
||||
describe('getEntitiesFromHASS', () => {
|
||||
const hass = createHASS({
|
||||
'camera.front_door': createStateEntity(),
|
||||
'light.kitchen': createStateEntity(),
|
||||
'light.living_room': createStateEntity(),
|
||||
'sensor.temperature': createStateEntity(),
|
||||
'switch.garage': createStateEntity(),
|
||||
});
|
||||
|
||||
it('returns all entity ids when no domain is specified', () => {
|
||||
expect(getEntitiesFromHASS(hass)).toEqual([
|
||||
'camera.front_door',
|
||||
'light.kitchen',
|
||||
'light.living_room',
|
||||
'sensor.temperature',
|
||||
'switch.garage',
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns only entities of the specified domain', () => {
|
||||
expect(getEntitiesFromHASS(hass, 'light')).toEqual([
|
||||
'light.kitchen',
|
||||
'light.living_room',
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns an empty array if no entities match the domain', () => {
|
||||
const result = getEntitiesFromHASS(hass, 'binary_sensor');
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { getEntityTitle } from '../../src/ha/get-entity-title';
|
||||
import type { HomeAssistant } from '../../src/ha/types';
|
||||
import { createHASS, createStateEntity } from '../test-utils';
|
||||
|
||||
describe('getEntityTitle', () => {
|
||||
const hass: HomeAssistant = createHASS({
|
||||
'sensor.temperature': createStateEntity({
|
||||
attributes: {
|
||||
friendly_name: 'Temperature Sensor',
|
||||
},
|
||||
}),
|
||||
'light.living_room': createStateEntity({
|
||||
attributes: {
|
||||
friendly_name: 'Living Room Light',
|
||||
},
|
||||
}),
|
||||
'switch.no_friendly': createStateEntity({
|
||||
attributes: {},
|
||||
}),
|
||||
});
|
||||
|
||||
it('returns the friendly_name for a valid entity', () => {
|
||||
expect(getEntityTitle(hass, 'sensor.temperature')).toBe('Temperature Sensor');
|
||||
expect(getEntityTitle(hass, 'light.living_room')).toBe('Living Room Light');
|
||||
});
|
||||
|
||||
it('returns null if the entity does not exist', () => {
|
||||
expect(getEntityTitle(hass, 'sensor.unknown')).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null if hass is undefined', () => {
|
||||
expect(getEntityTitle(undefined, 'sensor.temperature')).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null if entity is undefined', () => {
|
||||
expect(getEntityTitle(hass)).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null if friendly_name attribute is missing', () => {
|
||||
expect(getEntityTitle(hass, 'switch.no_friendly')).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null if both hass and entity are undefined', () => {
|
||||
expect(getEntityTitle()).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,160 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { getHassDifferences } from '../../src/ha/get-hass-differences';
|
||||
import { createHASS, createStateEntity } from '../test-utils';
|
||||
|
||||
describe('getHassDifferences', () => {
|
||||
const newHass = createHASS({
|
||||
'light.office': createStateEntity({
|
||||
entity_id: 'light.office',
|
||||
state: 'on',
|
||||
}),
|
||||
'light.kitchen': createStateEntity({
|
||||
entity_id: 'light.kitchen',
|
||||
state: 'on',
|
||||
}),
|
||||
'light.attributes_only': createStateEntity({
|
||||
entity_id: 'light.attributes_only',
|
||||
state: 'on',
|
||||
attributes: {
|
||||
friendly_name: 'Attributes Only After',
|
||||
},
|
||||
}),
|
||||
});
|
||||
const oldHass = createHASS({
|
||||
'light.office': createStateEntity({
|
||||
entity_id: 'light.office',
|
||||
state: 'off',
|
||||
}),
|
||||
'light.kitchen': createStateEntity({
|
||||
entity_id: 'light.kitchen',
|
||||
state: 'off',
|
||||
}),
|
||||
'light.attributes_only': createStateEntity({
|
||||
entity_id: 'light.attributes_only',
|
||||
state: 'on',
|
||||
attributes: {
|
||||
friendly_name: 'Attributes Only Before',
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
it('should return empty list without difference', () => {
|
||||
expect(getHassDifferences(newHass, newHass, ['light.office'])).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return differences', () => {
|
||||
expect(
|
||||
getHassDifferences(newHass, oldHass, [
|
||||
'light.office',
|
||||
'light.kitchen',
|
||||
'light.attributes_only',
|
||||
]),
|
||||
).toEqual([
|
||||
{
|
||||
entityID: 'light.office',
|
||||
oldState: expect.objectContaining({
|
||||
entity_id: 'light.office',
|
||||
state: 'off',
|
||||
}),
|
||||
newState: expect.objectContaining({
|
||||
entity_id: 'light.office',
|
||||
state: 'on',
|
||||
}),
|
||||
},
|
||||
{
|
||||
entityID: 'light.kitchen',
|
||||
oldState: expect.objectContaining({
|
||||
entity_id: 'light.kitchen',
|
||||
state: 'off',
|
||||
}),
|
||||
newState: expect.objectContaining({
|
||||
entity_id: 'light.kitchen',
|
||||
state: 'on',
|
||||
}),
|
||||
},
|
||||
{
|
||||
entityID: 'light.attributes_only',
|
||||
oldState: expect.objectContaining({
|
||||
entity_id: 'light.attributes_only',
|
||||
state: 'on',
|
||||
attributes: {
|
||||
friendly_name: 'Attributes Only Before',
|
||||
},
|
||||
}),
|
||||
newState: expect.objectContaining({
|
||||
entity_id: 'light.attributes_only',
|
||||
state: 'on',
|
||||
attributes: {
|
||||
friendly_name: 'Attributes Only After',
|
||||
},
|
||||
}),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return single difference', () => {
|
||||
expect(
|
||||
getHassDifferences(newHass, oldHass, ['light.office', 'light.kitchen'], {
|
||||
firstOnly: true,
|
||||
}),
|
||||
).toEqual([
|
||||
{
|
||||
entityID: 'light.office',
|
||||
oldState: expect.objectContaining({
|
||||
entity_id: 'light.office',
|
||||
state: 'off',
|
||||
}),
|
||||
newState: expect.objectContaining({
|
||||
entity_id: 'light.office',
|
||||
state: 'on',
|
||||
}),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return only state differences', () => {
|
||||
expect(
|
||||
getHassDifferences(
|
||||
newHass,
|
||||
oldHass,
|
||||
['light.office', 'light.kitchen', 'light.attributes_only'],
|
||||
{
|
||||
stateOnly: true,
|
||||
},
|
||||
),
|
||||
).toEqual([
|
||||
{
|
||||
entityID: 'light.office',
|
||||
oldState: expect.objectContaining({
|
||||
entity_id: 'light.office',
|
||||
state: 'off',
|
||||
}),
|
||||
newState: expect.objectContaining({
|
||||
entity_id: 'light.office',
|
||||
state: 'on',
|
||||
}),
|
||||
},
|
||||
{
|
||||
entityID: 'light.kitchen',
|
||||
oldState: expect.objectContaining({
|
||||
entity_id: 'light.kitchen',
|
||||
state: 'off',
|
||||
}),
|
||||
newState: expect.objectContaining({
|
||||
entity_id: 'light.kitchen',
|
||||
state: 'on',
|
||||
}),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
describe('should return empty list with empty values', () => {
|
||||
it('should return empty list for empty new hass', () => {
|
||||
expect(getHassDifferences(null, oldHass, ['light.office'])).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return empty list for empty entity list', () => {
|
||||
expect(getHassDifferences(newHass, oldHass, [])).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { hasHAConnectionStateChanged } from '../../src/ha/has-hass-connection-changed';
|
||||
import { createHASS } from '../test-utils';
|
||||
|
||||
describe('hasHAConnectionStateChanged', () => {
|
||||
it('returns false if both oldHass and newHass are undefined', () => {
|
||||
expect(hasHAConnectionStateChanged()).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false if both oldHass and newHass are null', () => {
|
||||
expect(hasHAConnectionStateChanged(null, null)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false if both oldHass and newHass are the same object', () => {
|
||||
const hass = createHASS();
|
||||
expect(hasHAConnectionStateChanged(hass, hass)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false if both oldHass.connected and newHass.connected are the same', () => {
|
||||
const oldHass = createHASS();
|
||||
const newHass = createHASS();
|
||||
oldHass.connected = true;
|
||||
newHass.connected = true;
|
||||
expect(hasHAConnectionStateChanged(oldHass, newHass)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true if oldHass.connected and newHass.connected are different', () => {
|
||||
const oldHass = createHASS();
|
||||
const newHass = createHASS();
|
||||
oldHass.connected = true;
|
||||
newHass.connected = false;
|
||||
expect(hasHAConnectionStateChanged(oldHass, newHass)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { getIntegrationManifest } from '../../../src/ha/integration';
|
||||
import { integrationManifestSchema } from '../../../src/ha/integration/types';
|
||||
import { homeAssistantWSRequest } from '../../../src/ha/ws-request.js';
|
||||
import { createHASS } from '../../test-utils';
|
||||
|
||||
vi.mock('../../../src/ha/ws-request.js');
|
||||
|
||||
describe('getIntegrationManifest', () => {
|
||||
it('should get integration manifest', async () => {
|
||||
const hass = createHASS();
|
||||
await getIntegrationManifest(hass, 'INTEGRATION');
|
||||
expect(homeAssistantWSRequest).toHaveBeenCalledWith(
|
||||
hass,
|
||||
integrationManifestSchema,
|
||||
{ type: 'manifest/get', integration: 'INTEGRATION' },
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { isHARelativeURL } from '../../src/ha/is-ha-relative-url';
|
||||
|
||||
describe('isHARelativeURL', () => {
|
||||
it('returns true for a simple relative URL', () => {
|
||||
expect(isHARelativeURL('/image.jpg')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true for root path', () => {
|
||||
expect(isHARelativeURL('/')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for undefined', () => {
|
||||
expect(isHARelativeURL()).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for empty string', () => {
|
||||
expect(isHARelativeURL('')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for absolute URL', () => {
|
||||
expect(isHARelativeURL('https://card.camera/image.jpg')).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { getHassDifferences } from '../../src/ha/get-hass-differences';
|
||||
import { isHassDifferent } from '../../src/ha/is-hass-different';
|
||||
import { createHASS, createStateEntity } from '../test-utils';
|
||||
|
||||
vi.mock('../../src/ha/get-hass-differences');
|
||||
|
||||
describe('isHassDifferent', () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return true with difference', async () => {
|
||||
vi.mocked(getHassDifferences).mockReturnValue([
|
||||
{ entityID: 'light.office', newState: createStateEntity() },
|
||||
]);
|
||||
|
||||
const newHass = createHASS();
|
||||
const oldHass = createHASS();
|
||||
const entities = ['light.office'];
|
||||
|
||||
expect(isHassDifferent(newHass, oldHass, entities)).toBe(true);
|
||||
expect(getHassDifferences).toBeCalledWith(newHass, oldHass, entities, {
|
||||
firstOnly: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return false without difference', async () => {
|
||||
vi.mocked(getHassDifferences).mockReturnValue([]);
|
||||
expect(isHassDifferent(createHASS(), createHASS(), ['light.office'])).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { isTriggeredState } from '../../src/ha/is-triggered-state';
|
||||
|
||||
describe('isTriggeredState', () => {
|
||||
it('returns true for a state included in STATES_ON', () => {
|
||||
expect(isTriggeredState('on')).toBe(true);
|
||||
expect(isTriggeredState('open')).toBe(true);
|
||||
expect(isTriggeredState('unlocked')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for a state not included in STATES_ON', () => {
|
||||
expect(isTriggeredState('off')).toBe(false);
|
||||
expect(isTriggeredState('closed')).toBe(false);
|
||||
expect(isTriggeredState('locked')).toBe(false);
|
||||
expect(isTriggeredState('other')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for undefined or empty state', () => {
|
||||
expect(isTriggeredState()).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { isCardInPanel } from '../../src/ha/panel';
|
||||
import { createLitElement } from '../test-utils';
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('isCardInPanel', () => {
|
||||
it('returns true if card is in a ShadowRoot with correct tag name"', () => {
|
||||
const card = createLitElement();
|
||||
|
||||
const parent = document.createElement('HUI-PANEL-VIEW');
|
||||
parent.attachShadow({ mode: 'open' });
|
||||
parent.shadowRoot?.append(card);
|
||||
|
||||
expect(isCardInPanel(card)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false if card is in a ShadowRoot with incorrect tag name"', () => {
|
||||
const card = createLitElement();
|
||||
|
||||
const parent = document.createElement('ANOTHER-VIEW');
|
||||
parent.attachShadow({ mode: 'open' });
|
||||
parent.shadowRoot?.append(card);
|
||||
|
||||
expect(isCardInPanel(card)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { DeviceRegistryManager } from '../../../../src/ha/registry/device';
|
||||
import { DeviceCache } from '../../../../src/ha/registry/device/types';
|
||||
import { homeAssistantWSRequest } from '../../../../src/ha/ws-request';
|
||||
import { createHASS, createRegistryDevice } from '../../../test-utils.js';
|
||||
|
||||
vi.mock('../../../../src/ha/ws-request');
|
||||
vi.spyOn(global.console, 'warn').mockImplementation(() => true);
|
||||
|
||||
describe('DeviceRegistryManager', () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('getDevice', () => {
|
||||
it('should not fetch when cached', async () => {
|
||||
const cache = new DeviceCache();
|
||||
const testDevice = createRegistryDevice({ id: 'test' });
|
||||
|
||||
cache.set('test', testDevice);
|
||||
|
||||
const manager = new DeviceRegistryManager(cache);
|
||||
expect(await manager.getDevice(createHASS(), 'test')).toEqual(testDevice);
|
||||
|
||||
expect(homeAssistantWSRequest).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should fetch and cache when not cached', async () => {
|
||||
const testDevice = createRegistryDevice({ id: 'test' });
|
||||
|
||||
const manager = new DeviceRegistryManager(new DeviceCache());
|
||||
vi.mocked(homeAssistantWSRequest).mockResolvedValueOnce([testDevice]);
|
||||
|
||||
expect(await manager.getDevice(createHASS(), 'test')).toEqual(testDevice);
|
||||
expect(homeAssistantWSRequest).toBeCalledTimes(1);
|
||||
|
||||
expect(await manager.getDevice(createHASS(), 'test')).toEqual(testDevice);
|
||||
expect(homeAssistantWSRequest).toBeCalledTimes(1);
|
||||
|
||||
expect(await manager.getDevice(createHASS(), 'missing')).toBeNull();
|
||||
|
||||
// The fetch call is called exactly once.
|
||||
expect(homeAssistantWSRequest).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should return null when fetch fails', async () => {
|
||||
vi.mocked(homeAssistantWSRequest).mockRejectedValueOnce(new Error('Fetch error'));
|
||||
|
||||
const manager = new DeviceRegistryManager(new DeviceCache());
|
||||
expect(await manager.getDevice(createHASS(), 'test')).toBeNull();
|
||||
|
||||
vi.mocked(expect(console.warn)).toBeCalledWith('Fetch error');
|
||||
});
|
||||
});
|
||||
|
||||
it('getMatchingDevices', async () => {
|
||||
const matchingDevice = createRegistryDevice({ id: 'matching' });
|
||||
const notMatchingDevice = createRegistryDevice({ id: 'not-matching' });
|
||||
const hass = createHASS();
|
||||
|
||||
vi.mocked(homeAssistantWSRequest).mockResolvedValueOnce([
|
||||
matchingDevice,
|
||||
notMatchingDevice,
|
||||
]);
|
||||
|
||||
const manager = new DeviceRegistryManager(new DeviceCache());
|
||||
expect(
|
||||
await manager.getMatchingDevices(hass, (entity) => entity.id == 'matching'),
|
||||
).toEqual([matchingDevice]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,130 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { EntityRegistryManagerLive } from '../../../../src/ha/registry/entity';
|
||||
import { EntityCache } from '../../../../src/ha/registry/entity/types';
|
||||
import { homeAssistantWSRequest } from '../../../../src/ha/ws-request';
|
||||
import { createHASS, createRegistryEntity } from '../../../test-utils.js';
|
||||
|
||||
vi.mock('../../../../src/ha/ws-request');
|
||||
vi.spyOn(global.console, 'warn').mockImplementation(() => true);
|
||||
|
||||
describe('EntityRegistryManager', () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('getEntity', () => {
|
||||
it('should not fetch when cached', async () => {
|
||||
const cache = new EntityCache();
|
||||
const testEntity = createRegistryEntity({ entity_id: 'test' });
|
||||
|
||||
cache.set('test', testEntity);
|
||||
|
||||
const manager = new EntityRegistryManagerLive(cache);
|
||||
expect(await manager.getEntity(createHASS(), 'test')).toEqual(testEntity);
|
||||
|
||||
expect(homeAssistantWSRequest).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should fetch and cache when not cached', async () => {
|
||||
const testEntity = createRegistryEntity({ entity_id: 'test' });
|
||||
|
||||
const manager = new EntityRegistryManagerLive(new EntityCache());
|
||||
vi.mocked(homeAssistantWSRequest).mockResolvedValueOnce(testEntity);
|
||||
|
||||
expect(await manager.getEntity(createHASS(), 'test')).toEqual(testEntity);
|
||||
expect(homeAssistantWSRequest).toBeCalledTimes(1);
|
||||
|
||||
expect(await manager.getEntity(createHASS(), 'test')).toEqual(testEntity);
|
||||
expect(homeAssistantWSRequest).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should return null when entity does not exist', async () => {
|
||||
vi.mocked(homeAssistantWSRequest).mockRejectedValueOnce(new Error('Not found'));
|
||||
|
||||
const manager = new EntityRegistryManagerLive(new EntityCache());
|
||||
expect(await manager.getEntity(createHASS(), 'missing')).toBeNull();
|
||||
|
||||
vi.mocked(expect(console.warn)).toBeCalledWith('Not found');
|
||||
});
|
||||
});
|
||||
|
||||
it('getEntities', async () => {
|
||||
const cachedEntity = createRegistryEntity({ entity_id: 'cached' });
|
||||
const notCachedEntity = createRegistryEntity({ entity_id: 'not-cached' });
|
||||
|
||||
const cache = new EntityCache();
|
||||
cache.set('cached', cachedEntity);
|
||||
|
||||
const manager = new EntityRegistryManagerLive(cache);
|
||||
vi.mocked(homeAssistantWSRequest).mockResolvedValueOnce(notCachedEntity);
|
||||
vi.mocked(homeAssistantWSRequest).mockRejectedValueOnce(new Error('Not found'));
|
||||
|
||||
expect(
|
||||
await manager.getEntities(createHASS(), ['cached', 'not-cached', 'missing']),
|
||||
).toEqual(
|
||||
new Map([
|
||||
['cached', cachedEntity],
|
||||
['not-cached', notCachedEntity],
|
||||
]),
|
||||
);
|
||||
|
||||
vi.mocked(expect(console.warn)).toBeCalledWith('Not found');
|
||||
});
|
||||
|
||||
describe('fetchEntityList', async () => {
|
||||
it('should fetch entire entity list once', async () => {
|
||||
const hass = createHASS();
|
||||
const entity = createRegistryEntity({ entity_id: 'cached' });
|
||||
vi.mocked(homeAssistantWSRequest).mockResolvedValueOnce([entity]);
|
||||
|
||||
const manager = new EntityRegistryManagerLive(new EntityCache());
|
||||
|
||||
await manager.fetchEntityList(hass);
|
||||
|
||||
expect(homeAssistantWSRequest).toBeCalledTimes(1);
|
||||
expect(homeAssistantWSRequest).toBeCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
{
|
||||
type: 'config/entity_registry/list',
|
||||
},
|
||||
);
|
||||
|
||||
expect(await manager.getEntity(hass, 'cached')).toEqual(entity);
|
||||
expect(homeAssistantWSRequest).toBeCalledTimes(1);
|
||||
|
||||
await manager.fetchEntityList(hass);
|
||||
expect(homeAssistantWSRequest).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should log to console on error', async () => {
|
||||
const hass = createHASS();
|
||||
vi.mocked(homeAssistantWSRequest).mockRejectedValueOnce(new Error('Fetch error'));
|
||||
|
||||
const manager = new EntityRegistryManagerLive(new EntityCache());
|
||||
|
||||
await manager.fetchEntityList(hass);
|
||||
|
||||
vi.mocked(expect(console.warn)).toBeCalledWith('Fetch error');
|
||||
});
|
||||
});
|
||||
|
||||
it('getMatchingEntities', async () => {
|
||||
const matchingEntity = createRegistryEntity({ entity_id: 'matching' });
|
||||
const notMatchingEntity = createRegistryEntity({ entity_id: 'not-matching' });
|
||||
const hass = createHASS();
|
||||
|
||||
vi.mocked(homeAssistantWSRequest).mockResolvedValueOnce([
|
||||
matchingEntity,
|
||||
notMatchingEntity,
|
||||
]);
|
||||
|
||||
const manager = new EntityRegistryManagerLive(new EntityCache());
|
||||
expect(
|
||||
await manager.getMatchingEntities(
|
||||
hass,
|
||||
(entity) => entity.entity_id == 'matching',
|
||||
),
|
||||
).toEqual([matchingEntity]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
import {
|
||||
Entity,
|
||||
EntityCache,
|
||||
EntityRegistryManager,
|
||||
} from '../../../../src/ha/registry/entity/types';
|
||||
import { HomeAssistant } from '../../../../src/ha/types';
|
||||
|
||||
export class EntityRegistryManagerMock implements EntityRegistryManager {
|
||||
protected _cache = new EntityCache();
|
||||
protected _fetchedEntityList = false;
|
||||
|
||||
constructor(data?: Entity[]) {
|
||||
data?.forEach((entity) => {
|
||||
this._cache.set(entity.entity_id, entity);
|
||||
});
|
||||
}
|
||||
|
||||
public async getEntity(
|
||||
_hass: HomeAssistant,
|
||||
entityID: string,
|
||||
): Promise<Entity | null> {
|
||||
return this._cache.get(entityID);
|
||||
}
|
||||
|
||||
public async getMatchingEntities(
|
||||
_hass: HomeAssistant,
|
||||
func: (arg: Entity) => boolean,
|
||||
): Promise<Entity[]> {
|
||||
return this._cache.getMatches(func);
|
||||
}
|
||||
|
||||
public async getEntities(
|
||||
hass: HomeAssistant,
|
||||
entityIDs: string[],
|
||||
): Promise<Map<string, Entity>> {
|
||||
const output: Map<string, Entity> = new Map();
|
||||
for (const entityID of entityIDs) {
|
||||
const entityData = await this.getEntity(hass, entityID);
|
||||
if (entityData) {
|
||||
output.set(entityID, entityData);
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
public async fetchEntityList(_hass: HomeAssistant): Promise<void> {}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { ResolvedMediaCache, resolveMedia } from '../../src/ha/resolved-media';
|
||||
import { ResolvedMedia, resolvedMediaSchema } from '../../src/ha/types';
|
||||
import { homeAssistantWSRequest } from '../../src/ha/ws-request';
|
||||
import { errorToConsole } from '../../src/utils/basic';
|
||||
import { createHASS } from '../test-utils';
|
||||
|
||||
vi.mock('../../src/ha/ws-request', () => ({
|
||||
homeAssistantWSRequest: vi.fn(),
|
||||
}));
|
||||
vi.mock('../../src/utils/basic', () => ({
|
||||
errorToConsole: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('ResolvedMediaCache', () => {
|
||||
it('should store and retrieve values', () => {
|
||||
const cache = new ResolvedMediaCache();
|
||||
const key = 'media-id';
|
||||
const resolvedMedia = {
|
||||
id: key,
|
||||
title: 'Test Media',
|
||||
mime_type: 'video/mp4',
|
||||
url: 'http://media',
|
||||
media_content_type: 'video',
|
||||
media_content_id: key,
|
||||
};
|
||||
cache.set(key, resolvedMedia);
|
||||
expect(cache.get(key)).toBe(resolvedMedia);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveMedia', () => {
|
||||
const mediaContentID = 'media-123';
|
||||
const resolvedMedia: ResolvedMedia = {
|
||||
mime_type: 'video/mp4',
|
||||
url: 'http://media',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('returns cached value if present', async () => {
|
||||
const cache = new ResolvedMediaCache();
|
||||
cache.set(mediaContentID, resolvedMedia);
|
||||
const result = await resolveMedia(createHASS(), mediaContentID, cache);
|
||||
expect(result).toBe(resolvedMedia);
|
||||
expect(homeAssistantWSRequest).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('fetches and caches value if not present in cache', async () => {
|
||||
vi.mocked(homeAssistantWSRequest).mockResolvedValueOnce(resolvedMedia);
|
||||
|
||||
const hass = createHASS();
|
||||
const cache = new ResolvedMediaCache();
|
||||
const result = await resolveMedia(hass, mediaContentID, cache);
|
||||
|
||||
expect(homeAssistantWSRequest).toBeCalledWith(
|
||||
hass,
|
||||
resolvedMediaSchema,
|
||||
expect.objectContaining({
|
||||
type: 'media_source/resolve_media',
|
||||
media_content_id: mediaContentID,
|
||||
}),
|
||||
);
|
||||
expect(result).toEqual(resolvedMedia);
|
||||
expect(cache.get(mediaContentID)).toEqual(resolvedMedia);
|
||||
});
|
||||
|
||||
it('returns null and logs error if request fails', async () => {
|
||||
const error = new Error('fail');
|
||||
vi.mocked(homeAssistantWSRequest).mockRejectedValueOnce(error);
|
||||
|
||||
const cache = new ResolvedMediaCache();
|
||||
const result = await resolveMedia(createHASS(), mediaContentID, cache);
|
||||
expect(result).toBeNull();
|
||||
expect(errorToConsole).toBeCalledWith(error);
|
||||
});
|
||||
|
||||
it('does not cache null results', async () => {
|
||||
vi.mocked(homeAssistantWSRequest).mockResolvedValueOnce(null);
|
||||
const cache = new ResolvedMediaCache();
|
||||
const result = await resolveMedia(createHASS(), mediaContentID, cache);
|
||||
expect(result).toBeNull();
|
||||
expect(cache.get(mediaContentID)).toBeNull();
|
||||
});
|
||||
|
||||
it('works without cache argument', async () => {
|
||||
vi.mocked(homeAssistantWSRequest).mockResolvedValueOnce(resolvedMedia);
|
||||
const result = await resolveMedia(createHASS(), mediaContentID);
|
||||
expect(result).toEqual(resolvedMedia);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
import { LitElement } from 'lit';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { sideLoadHomeAssistantElements } from '../../src/ha/side-load-ha-elements';
|
||||
|
||||
describe('sideLoadHomeAssistantElements', () => {
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('customElements', {
|
||||
get: vi.fn(),
|
||||
whenDefined: vi.fn(),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('returns true if all elements already registered', async () => {
|
||||
vi.mocked(customElements.get).mockResolvedValue(LitElement);
|
||||
expect(await sideLoadHomeAssistantElements()).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when the picture glance card cannot be found', async () => {
|
||||
vi.mocked(customElements.get).mockReturnValue(undefined);
|
||||
|
||||
const createCardElement = vi.fn();
|
||||
vi.stubGlobal('window', {
|
||||
loadCardHelpers: vi.fn().mockReturnValue({
|
||||
createCardElement,
|
||||
}),
|
||||
});
|
||||
|
||||
expect(await sideLoadHomeAssistantElements()).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true when elements are loaded', async () => {
|
||||
vi.mocked(customElements.get).mockImplementation((name: string) => {
|
||||
if (name === 'hui-picture-glance-card') {
|
||||
const result = LitElement;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(result as any).getConfigElement = vi.fn();
|
||||
return LitElement;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const createCardElement = vi.fn();
|
||||
vi.stubGlobal('window', {
|
||||
loadCardHelpers: vi.fn().mockReturnValue({
|
||||
createCardElement,
|
||||
}),
|
||||
});
|
||||
|
||||
expect(await sideLoadHomeAssistantElements()).toBe(true);
|
||||
|
||||
expect(customElements.whenDefined).toHaveBeenCalledWith('hui-picture-glance-card');
|
||||
expect(createCardElement).toHaveBeenCalledWith({
|
||||
type: 'picture-glance',
|
||||
entities: [],
|
||||
camera_image: 'dummy-to-load-editor-components',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { homeAssistantSignPath } from '../../src/ha/sign-path';
|
||||
import { homeAssistantWSRequest } from '../../src/ha/ws-request.js';
|
||||
import { signedPathSchema } from '../../src/types';
|
||||
import { createHASS } from '../test-utils';
|
||||
|
||||
vi.mock('../../src/ha/ws-request.js');
|
||||
|
||||
describe('homeAssistantSignPath', () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should sign path', async () => {
|
||||
const hass = createHASS();
|
||||
const unsignedPath = 'unsigned/path';
|
||||
const expires = 42;
|
||||
|
||||
vi.mocked(homeAssistantWSRequest).mockResolvedValue({
|
||||
path: 'signed/path',
|
||||
});
|
||||
vi.mocked(hass.hassUrl).mockImplementation((url) => 'hass:' + url);
|
||||
|
||||
expect(await homeAssistantSignPath(hass, unsignedPath, expires)).toEqual(
|
||||
'hass:signed/path',
|
||||
);
|
||||
expect(homeAssistantWSRequest).toBeCalledWith(hass, signedPathSchema, {
|
||||
type: 'auth/sign_path',
|
||||
path: unsignedPath,
|
||||
expires,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return null for null response', async () => {
|
||||
vi.mocked(homeAssistantWSRequest).mockResolvedValue(null);
|
||||
expect(await homeAssistantSignPath(createHASS(), 'unsigned/path', 42)).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { sortMediaByStartDate } from '../../src/ha/browse-media/sort-browse-media-by-start-date';
|
||||
import { BrowseMediaMetadata, RichBrowseMedia } from '../../src/ha/browse-media/types';
|
||||
import { createBrowseMedia, createRichBrowseMedia } from '../test-utils';
|
||||
|
||||
const createMetadata = (
|
||||
metadata: Partial<BrowseMediaMetadata>,
|
||||
): BrowseMediaMetadata => ({
|
||||
cameraID: 'camera.office',
|
||||
startDate: new Date('2025-05-10T20:22:00Z'),
|
||||
endDate: new Date('2025-05-10T20:22:10Z'),
|
||||
...metadata,
|
||||
});
|
||||
|
||||
describe('sortMediaByStartDate', () => {
|
||||
it('should return an empty array when given an empty array', () => {
|
||||
const media: RichBrowseMedia<BrowseMediaMetadata>[] = [];
|
||||
expect(sortMediaByStartDate(media)).toEqual([]);
|
||||
});
|
||||
|
||||
it('should sort media by startDate in descending order', () => {
|
||||
const media: RichBrowseMedia<BrowseMediaMetadata>[] = [
|
||||
createRichBrowseMedia({
|
||||
title: 'Media 1',
|
||||
_metadata: createMetadata({ startDate: new Date('2025-05-10T20:29:00.000Z') }),
|
||||
}),
|
||||
createRichBrowseMedia({
|
||||
title: 'Media 2',
|
||||
_metadata: createMetadata({ startDate: new Date('2025-05-12T20:29:00.000Z') }),
|
||||
}),
|
||||
createRichBrowseMedia({
|
||||
title: 'Media 3',
|
||||
_metadata: createMetadata({ startDate: new Date('2025-05-11T20:29:00.000Z') }),
|
||||
}),
|
||||
];
|
||||
const expected: RichBrowseMedia<BrowseMediaMetadata>[] = [
|
||||
createRichBrowseMedia({
|
||||
title: 'Media 2',
|
||||
_metadata: createMetadata({ startDate: new Date('2025-05-12T20:29:00.000Z') }),
|
||||
}),
|
||||
createRichBrowseMedia({
|
||||
title: 'Media 3',
|
||||
_metadata: createMetadata({ startDate: new Date('2025-05-11T20:29:00.000Z') }),
|
||||
}),
|
||||
createRichBrowseMedia({
|
||||
title: 'Media 1',
|
||||
_metadata: createMetadata({ startDate: new Date('2025-05-10T20:29:00.000Z') }),
|
||||
}),
|
||||
];
|
||||
expect(sortMediaByStartDate(media)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should handle items with undefined startDate by placing them at the end', () => {
|
||||
const media: RichBrowseMedia<BrowseMediaMetadata>[] = [
|
||||
createRichBrowseMedia({
|
||||
title: 'Media 1',
|
||||
_metadata: createMetadata({ startDate: new Date('2025-05-10T20:29:00.000Z') }),
|
||||
}),
|
||||
createBrowseMedia({
|
||||
title: 'Media 2',
|
||||
}),
|
||||
createRichBrowseMedia({
|
||||
title: 'Media 3',
|
||||
_metadata: createMetadata({ startDate: new Date('2025-05-12T20:29:00.000Z') }),
|
||||
}),
|
||||
createBrowseMedia({
|
||||
title: 'Media 4',
|
||||
}),
|
||||
];
|
||||
const expected: RichBrowseMedia<BrowseMediaMetadata>[] = [
|
||||
createBrowseMedia({
|
||||
title: 'Media 2',
|
||||
}),
|
||||
createBrowseMedia({
|
||||
title: 'Media 4',
|
||||
}),
|
||||
createRichBrowseMedia({
|
||||
title: 'Media 3',
|
||||
_metadata: createMetadata({ startDate: new Date('2025-05-12T20:29:00.000Z') }),
|
||||
}),
|
||||
createRichBrowseMedia({
|
||||
title: 'Media 1',
|
||||
_metadata: createMetadata({ startDate: new Date('2025-05-10T20:29:00.000Z') }),
|
||||
}),
|
||||
];
|
||||
|
||||
expect(sortMediaByStartDate(media)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { supportsFeature } from '../../src/ha/supports-feature';
|
||||
import { createStateEntity } from '../test-utils';
|
||||
|
||||
describe('supportsFeature', () => {
|
||||
it('returns true if the feature bit is set', () => {
|
||||
const stateObj = createStateEntity({
|
||||
attributes: { supported_features: 4 },
|
||||
});
|
||||
expect(supportsFeature(stateObj, 4)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false if the feature bit is not set', () => {
|
||||
const stateObj = createStateEntity({
|
||||
attributes: { supported_features: 2 },
|
||||
});
|
||||
expect(supportsFeature(stateObj, 4)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false if supported_features is undefined', () => {
|
||||
const stateObj = createStateEntity({
|
||||
attributes: {},
|
||||
});
|
||||
expect(supportsFeature(stateObj, 1)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true if multiple feature bits are set and one matches', () => {
|
||||
const stateObj = createStateEntity({
|
||||
attributes: { supported_features: 6 }, // 2 + 4
|
||||
});
|
||||
expect(supportsFeature(stateObj, 2)).toBe(true);
|
||||
expect(supportsFeature(stateObj, 4)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false if feature is 0', () => {
|
||||
const stateObj = createStateEntity({
|
||||
attributes: { supported_features: 7 },
|
||||
});
|
||||
expect(supportsFeature(stateObj, 0)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { CameraProxyConfig } from '../../src/camera-manager/types.js';
|
||||
import {
|
||||
addDynamicProxyURL,
|
||||
getWebProxiedURL,
|
||||
shouldUseWebProxy,
|
||||
} from '../../src/ha/web-proxy.js';
|
||||
import { createHASS } from '../test-utils.js';
|
||||
|
||||
describe('getWebProxiedURL', () => {
|
||||
it('should return proxied URL with v != 0', () => {
|
||||
expect(getWebProxiedURL('http://example.com', 2)).toBe(
|
||||
'/api/hass_web_proxy/v2/?url=http%3A%2F%2Fexample.com',
|
||||
);
|
||||
});
|
||||
|
||||
it('should return proxied URL with default v', () => {
|
||||
expect(getWebProxiedURL('http://example.com')).toBe(
|
||||
'/api/hass_web_proxy/v0/?url=http%3A%2F%2Fexample.com',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldUseWebProxy', () => {
|
||||
const createProxyConfig = (
|
||||
config: Partial<CameraProxyConfig> = {},
|
||||
): CameraProxyConfig => ({
|
||||
media: true,
|
||||
ssl_verification: true,
|
||||
ssl_ciphers: 'default',
|
||||
dynamic: true,
|
||||
...config,
|
||||
});
|
||||
|
||||
it('should return false without a the proxy installed', () => {
|
||||
const hass = createHASS();
|
||||
hass.config.components = [];
|
||||
|
||||
expect(shouldUseWebProxy(hass, createProxyConfig())).toBe(false);
|
||||
});
|
||||
|
||||
it('should return when proxy config does not want proxying', () => {
|
||||
const hass = createHASS();
|
||||
hass.config.components = ['hass_web_proxy'];
|
||||
|
||||
const proxyConfig = createProxyConfig({ media: false });
|
||||
expect(shouldUseWebProxy(hass, proxyConfig, 'media')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return when proxy config does want proxying', () => {
|
||||
const hass = createHASS();
|
||||
hass.config.components = ['hass_web_proxy'];
|
||||
|
||||
const proxyConfig = createProxyConfig({ media: true });
|
||||
expect(shouldUseWebProxy(hass, proxyConfig, 'media')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addDynamicProxyURL', () => {
|
||||
it('should add dynamic proxy URL', async () => {
|
||||
const hass = createHASS();
|
||||
|
||||
await addDynamicProxyURL(hass, 'http://example.com', {
|
||||
urlID: 'id',
|
||||
sslVerification: true,
|
||||
sslCiphers: 'modern',
|
||||
openLimit: 5,
|
||||
ttl: 60,
|
||||
allowUnauthenticated: false,
|
||||
});
|
||||
|
||||
expect(hass.callService).toHaveBeenCalledWith(
|
||||
'hass_web_proxy',
|
||||
'create_proxied_url',
|
||||
{
|
||||
url_pattern: 'http://example.com',
|
||||
url_id: 'id',
|
||||
ssl_verification: true,
|
||||
ssl_ciphers: 'modern',
|
||||
open_limit: 5,
|
||||
ttl: 60,
|
||||
allow_unauthenticated: false,
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { ResolvedMedia, resolvedMediaSchema } from '../../src/ha/types';
|
||||
import { homeAssistantWSRequest } from '../../src/ha/ws-request';
|
||||
import { createHASS } from '../test-utils';
|
||||
|
||||
describe('homeAssistantWSRequest', () => {
|
||||
const request = {
|
||||
type: 'foo',
|
||||
};
|
||||
const response: ResolvedMedia = {
|
||||
url: 'https://example.com/media.mp4',
|
||||
mime_type: 'video/mp4',
|
||||
};
|
||||
|
||||
it('should return parsed data on successful call', async () => {
|
||||
const hass = createHASS();
|
||||
vi.mocked(hass.callWS).mockResolvedValueOnce(response);
|
||||
|
||||
expect(await homeAssistantWSRequest(hass, resolvedMediaSchema, request)).toEqual(
|
||||
response,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return parsed data on successful call with passthrough', async () => {
|
||||
const hass = createHASS();
|
||||
vi.mocked(hass.callWS).mockResolvedValueOnce(JSON.stringify(response));
|
||||
|
||||
expect(
|
||||
await homeAssistantWSRequest(hass, resolvedMediaSchema, request, true),
|
||||
).toEqual(response);
|
||||
});
|
||||
|
||||
it('should throw on error', async () => {
|
||||
const error = new Error('WS call failed');
|
||||
const hass = createHASS();
|
||||
vi.mocked(hass.callWS).mockRejectedValueOnce(error);
|
||||
|
||||
await expect(
|
||||
homeAssistantWSRequest(hass, resolvedMediaSchema, request),
|
||||
).rejects.toThrowError(/Failed to receive response/);
|
||||
});
|
||||
|
||||
it('should throw on empty response', async () => {
|
||||
const hass = createHASS();
|
||||
vi.mocked(hass.callWS).mockResolvedValueOnce(null);
|
||||
|
||||
await expect(
|
||||
homeAssistantWSRequest(hass, resolvedMediaSchema, request),
|
||||
).rejects.toThrowError(/Received empty response/);
|
||||
});
|
||||
|
||||
it('should throw error on parse failure', async () => {
|
||||
const hass = createHASS();
|
||||
vi.mocked(hass.callWS).mockResolvedValueOnce({});
|
||||
|
||||
await expect(
|
||||
homeAssistantWSRequest(hass, resolvedMediaSchema, request),
|
||||
).rejects.toThrowError(/Received invalid response/);
|
||||
});
|
||||
|
||||
it('should throw on JSON parse failure', async () => {
|
||||
const malformedJSONResponse = "{ foo: 'test', bar: 123 "; // Malformed JSON
|
||||
|
||||
const hass = createHASS();
|
||||
vi.mocked(hass.callWS).mockResolvedValueOnce(malformedJSONResponse);
|
||||
|
||||
await expect(
|
||||
homeAssistantWSRequest(hass, resolvedMediaSchema, request, true),
|
||||
).rejects.toThrowError(/Received invalid response/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user