@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user