From f64690335bfe7b43f275c1a9ca0078090cd5bc50 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Thu, 11 Dec 2025 05:33:48 -0800 Subject: [PATCH] test: Improve timezone resilience / cleanliness of tests (#2279) - Related: https://github.com/dermotduffy/advanced-camera-card/pull/2278 - Reported by @0x464e --- .../reolink/engine-reolink.test.ts | 20 +++++++++++-------- .../folders/ha/metadata-generator.test.ts | 18 +++++++++++------ .../card-controller/view/item-manager.test.ts | 9 +++++++-- .../thumbnail/details-controller.test.ts | 16 +++++++++++---- .../thumbnail/feature/controller.test.ts | 9 +++++++-- tests/ha/browse-media/item.test.ts | 17 ++++++++++++---- 6 files changed, 63 insertions(+), 26 deletions(-) diff --git a/tests/camera-manager/reolink/engine-reolink.test.ts b/tests/camera-manager/reolink/engine-reolink.test.ts index c8cf1d07..6d9e02ef 100644 --- a/tests/camera-manager/reolink/engine-reolink.test.ts +++ b/tests/camera-manager/reolink/engine-reolink.test.ts @@ -381,8 +381,9 @@ describe('ReolinkCameraManagerEngine', () => { { _metadata: { cameraID: 'office', - startDate: new Date('2024-11-04T21:37:00.000Z'), - endDate: new Date('2024-11-04T21:37:00.000Z'), + // Date parsed from directory '2024/11/4' + time '21:37:00' in local time + startDate: new Date(2024, 10, 4, 21, 37, 0), + endDate: new Date(2024, 10, 4, 21, 37, 0), }, can_expand: false, can_play: true, @@ -397,8 +398,9 @@ describe('ReolinkCameraManagerEngine', () => { { _metadata: { cameraID: 'office', - endDate: new Date('2024-11-04T21:35:46.000Z'), - startDate: new Date('2024-11-04T21:35:05.000Z'), + // Date parsed from directory '2024/11/4' + time '21:35:05' with duration in local time + endDate: new Date(2024, 10, 4, 21, 35, 46), + startDate: new Date(2024, 10, 4, 21, 35, 5), what: ['person', 'vehicle', 'zebra'], }, can_expand: false, @@ -414,8 +416,9 @@ describe('ReolinkCameraManagerEngine', () => { { _metadata: { cameraID: 'office', - endDate: new Date('2024-11-04T21:29:46'), - startDate: new Date('2024-11-04T21:29:05'), + // Date parsed from directory '2024/11/4' + time '21:29:05' with duration in local time + endDate: new Date(2024, 10, 4, 21, 29, 46), + startDate: new Date(2024, 10, 4, 21, 29, 5), }, can_expand: false, can_play: true, @@ -430,8 +433,9 @@ describe('ReolinkCameraManagerEngine', () => { { _metadata: { cameraID: 'office', - endDate: new Date('2024-11-04T21:24:27'), - startDate: new Date('2024-11-04T21:23:53'), + // Date parsed from directory '2024/11/4' + time '21:23:53' with duration in local time + endDate: new Date(2024, 10, 4, 21, 24, 27), + startDate: new Date(2024, 10, 4, 21, 23, 53), }, can_expand: false, can_play: true, diff --git a/tests/card-controller/folders/ha/metadata-generator.test.ts b/tests/card-controller/folders/ha/metadata-generator.test.ts index 83c41db9..538097fd 100644 --- a/tests/card-controller/folders/ha/metadata-generator.test.ts +++ b/tests/card-controller/folders/ha/metadata-generator.test.ts @@ -7,7 +7,9 @@ describe('MetadataGenerator', () => { const browseMedia = createBrowseMedia({ title: 'Test Media 2025-05-26 18:18', }); - const expectedDate = new Date('2025-05-26T18:18:00.000Z'); + // Create expected date using local time, since the date parser interprets + // the date string in local time (not UTC) + const expectedDate = new Date(2025, 4, 26, 18, 18); const formatlessDateParser: Parser = { type: 'startdate', }; @@ -129,10 +131,11 @@ describe('MetadataGenerator', () => { }); it('should incorporate parent metadata without a date format', async () => { + // Parent has a start date at local midnight on 2025-05-26 const parentBrowseMedia = createRichBrowseMedia({ title: '2025-05-26', _metadata: { - startDate: new Date('2025-05-26T00:00:00.000Z'), + startDate: new Date(2025, 4, 26, 0, 0), }, }); const childBrowseMedia = createBrowseMedia({ @@ -145,7 +148,7 @@ describe('MetadataGenerator', () => { generator.generate(childBrowseMedia, parentBrowseMedia, [ formatlessDateParser, ])?.startDate, - ).toEqual(new Date('2025-05-26T22:42:00.000Z')); + ).toEqual(new Date(2025, 4, 26, 22, 42)); }); }); @@ -195,16 +198,19 @@ describe('MetadataGenerator', () => { const generator = new MetadataGenerator(); await generator.prepare(parsers); + // The parsed date combines 20250507 (May 7, 2025) and 171758 (17:17:58) + // interpreted in local time expect(generator.generate(browseMedia, undefined, parsers)?.startDate).toEqual( - new Date('2025-05-07T17:17:58.000Z'), + new Date(2025, 4, 7, 17, 17, 58), ); }); it('should incorporate parent metadata with a date format', async () => { + // Parent has a start date at local midnight on 2025-05-26 const parentBrowseMedia = createRichBrowseMedia({ title: '2025-05-26', _metadata: { - startDate: new Date('2025-05-26T00:00:00.000Z'), + startDate: new Date(2025, 4, 26, 0, 0), }, }); const childBrowseMedia = createBrowseMedia({ @@ -219,7 +225,7 @@ describe('MetadataGenerator', () => { expect( generator.generate(childBrowseMedia, parentBrowseMedia, [parser])?.startDate, - ).toEqual(new Date('2025-05-26T22:42:00.000Z')); + ).toEqual(new Date(2025, 4, 26, 22, 42)); }); }); }); diff --git a/tests/card-controller/view/item-manager.test.ts b/tests/card-controller/view/item-manager.test.ts index e0d540fd..53a80120 100644 --- a/tests/card-controller/view/item-manager.test.ts +++ b/tests/card-controller/view/item-manager.test.ts @@ -1,3 +1,4 @@ +import { format } from 'date-fns'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { ViewItemManager } from '../../../src/card-controller/view/item-manager'; import { homeAssistantSignPath } from '../../../src/ha/sign-path.js'; @@ -189,7 +190,8 @@ describe('ViewItemManager', () => { vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS()); const manager = new ViewItemManager(api); - const item = new TestViewMedia({ startTime: new Date('2025-05-03T17:41:00Z') }); + const startTime = new Date('2025-05-03T17:41:00Z'); + const item = new TestViewMedia({ startTime }); vi.mocked(api.getCameraManager().getMediaDownloadPath).mockResolvedValue({ sign: false, @@ -197,7 +199,10 @@ describe('ViewItemManager', () => { }); expect(await manager.download(item)).toBe(true); - expect(downloadURL).toBeCalledWith('foo', 'camera_id_2025-05-03-17-41-00.mp4'); + + // Use format() to generate expected filename timestamp (formats in local time) + const expectedFilename = `camera_id_${format(startTime, 'yyyy-MM-dd-HH-mm-ss')}.mp4`; + expect(downloadURL).toBeCalledWith('foo', expectedFilename); }); it('should generate filename for snapshot', async () => { diff --git a/tests/components-lib/thumbnail/details-controller.test.ts b/tests/components-lib/thumbnail/details-controller.test.ts index e2ddb055..07f5afa2 100644 --- a/tests/components-lib/thumbnail/details-controller.test.ts +++ b/tests/components-lib/thumbnail/details-controller.test.ts @@ -1,7 +1,9 @@ +import { format } from 'date-fns'; import { describe, expect, it } from 'vitest'; import { mock } from 'vitest-mock-extended'; import { CameraManager } from '../../../src/camera-manager/manager'; import { ThumbnailDetailsController } from '../../../src/components-lib/thumbnail/details-controller'; +import { formatDateAndTime } from '../../../src/utils/basic'; import { ViewFolder, ViewMediaType } from '../../../src/view/item'; import { createFolder, TestViewMedia } from '../../test-utils'; @@ -135,14 +137,17 @@ describe('ThumbnailDetailsController', () => { }); it('should have start time in details', () => { + const startTime = new Date('2025-05-18T17:03:00Z'); const item = new TestViewMedia({ - startTime: new Date('2025-05-18T17:03:00Z'), + startTime, }); const controller = new ThumbnailDetailsController(); controller.calculate(null, item); + + // Use formatDateAndTime to generate expected value (formats in local time with seconds) expect(controller.getDetails()).toContainEqual({ - title: '2025-05-18 17:03:00', + title: formatDateAndTime(startTime, true), hint: 'Start', icon: { icon: 'mdi:calendar-clock-outline' }, }); @@ -249,11 +254,14 @@ describe('ThumbnailDetailsController', () => { it('should have seek in details', () => { const item = new TestViewMedia(); + const seekTime = new Date('2025-05-20T07:14:57Z'); const controller = new ThumbnailDetailsController(); - controller.calculate(null, item, new Date('2025-05-20T07:14:57Z')); + controller.calculate(null, item, seekTime); + + // Use format() to generate expected value (formats in local time) expect(controller.getDetails()).toContainEqual({ - title: '07:14:57', + title: format(seekTime, 'HH:mm:ss'), hint: 'Seek', icon: { icon: 'mdi:clock-fast' }, }); diff --git a/tests/components-lib/thumbnail/feature/controller.test.ts b/tests/components-lib/thumbnail/feature/controller.test.ts index c3178cf5..f6be0c0a 100644 --- a/tests/components-lib/thumbnail/feature/controller.test.ts +++ b/tests/components-lib/thumbnail/feature/controller.test.ts @@ -1,3 +1,4 @@ +import { format } from 'date-fns'; import { describe, expect, it } from 'vitest'; import { mock } from 'vitest-mock-extended'; import { CameraManager } from '../../../../src/camera-manager/manager'; @@ -18,7 +19,9 @@ describe('ThumbnailFeatureController', () => { controller.calculate(null, itemWithTime, false); - expect(controller.getTitle()).toBe('17:03'); + // Use format() to generate expected time in local timezone + const expectedTime = format(itemWithTime.getStartTime()!, 'HH:mm'); + expect(controller.getTitle()).toBe(expectedTime); }); it('should not set title when details are shown ', () => { @@ -50,7 +53,9 @@ describe('ThumbnailFeatureController', () => { controller.calculate(null, itemWithTime, false); - expect(controller.getSubtitles()).toContain('May 18th'); + // Use format() to generate expected date string (formats in local time) + const expectedDate = format(itemWithTime.getStartTime()!, 'MMM do'); + expect(controller.getSubtitles()).toContain(expectedDate); }); it('should set subtitle with source from item title ', () => { diff --git a/tests/ha/browse-media/item.test.ts b/tests/ha/browse-media/item.test.ts index 2fcfc986..9fdc45a1 100644 --- a/tests/ha/browse-media/item.test.ts +++ b/tests/ha/browse-media/item.test.ts @@ -1,8 +1,10 @@ +import { format } from 'date-fns'; import { describe, expect, it } from 'vitest'; import { BrowseMediaEventViewMedia, BrowseMediaViewFolder, } from '../../../src/ha/browse-media/item'; +import { formatDateAndTime } from '../../../src/utils/basic'; import { VideoContentType, ViewMediaType } from '../../../src/view/item'; import { createBrowseMedia, @@ -45,17 +47,21 @@ describe('BrowseMediaEventViewMedia', () => { describe('should set ID', () => { it('should set id from metadata', () => { + const startDate = new Date('2025-05-05T07:46:00Z'); const browseMedia = createRichBrowseMedia({ media_content_id: 'media_content_id', _metadata: { - startDate: new Date('2025-05-05T07:46:00Z'), + startDate, 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'); + + // The ID is formatted using local time, so we use format() to generate expected value + const expectedId = `camera.office/${format(startDate, 'yyyy-MM-dd HH:mm:ss')}`; + expect(viewMedia.getID()).toBe(expectedId); }); it('should set id from media_content_id from the options', () => { @@ -125,16 +131,19 @@ describe('BrowseMediaEventViewMedia', () => { describe('should get title', () => { it('should get title from metadata start time', () => { + const startDate = new Date('2025-05-05T07:46:00Z'); const browseMedia = createRichBrowseMedia({ _metadata: { - startDate: new Date('2025-05-05T07:46:00Z'), + startDate: startDate, 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'); + + // The title is formatted using local time via formatDateAndTime + expect(viewMedia.getTitle()).toEqual(formatDateAndTime(startDate)); }); it('should get title without metadata', () => {