@@ -66,7 +66,7 @@ const makeDevice = (overrides: Partial<Device> = {}): Device => ({
name : 'Living Room' ,
orientation : 'landscape' ,
rotationIntervalMinutes : 60 ,
wakeHour : null ,
wakeTimes : [ ] ,
timezone : 'America/Chicago' ,
uniquenessWindow : 30 ,
linkedAt : '2026-01-01T00:00:00Z' ,
@@ -253,7 +253,7 @@ describe('HomeView', () => {
// HV-05: edit opens the settings sheet pre-filled from the device record
it ( 'edit emits open the settings sheet pre-populated from the device' , async ( ) = > {
const devicesStore = useDevicesStore ( )
devicesStore . devices = [ makeDevice ( { id : 9 , name : 'Den' , wakeHour : 22 , timezone : 'America/Chicago' } ) ]
devicesStore . devices = [ makeDevice ( { id : 9 , name : 'Den' , wakeTimes : [ 22 * 60 ] , timezone : 'America/Chicago' } ) ]
vi . spyOn ( devicesStore , 'fetchDevices' ) . mockResolvedValue ( )
const wrapper = mountView ( )
@@ -287,7 +287,7 @@ describe('HomeView', () => {
// HV-06: saving the sheet calls updateDevice and closes it
it ( 'saving the settings sheet PATCHes via the store and closes' , async ( ) = > {
const devicesStore = useDevicesStore ( )
devicesStore . devices = [ makeDevice ( { id : 5 , name : 'Old' , wakeHour : 4 , timezone : 'UTC' } ) ]
devicesStore . devices = [ makeDevice ( { id : 5 , name : 'Old' , wakeTimes : [ 4 * 60 ] , timezone : 'UTC' } ) ]
vi . spyOn ( devicesStore , 'fetchDevices' ) . mockResolvedValue ( )
const updateSpy = vi . spyOn ( devicesStore , 'updateDevice' ) . mockResolvedValue ( makeDevice ( { id : 5 } ) )
@@ -304,7 +304,7 @@ describe('HomeView', () => {
expect ( updateSpy ) . toHaveBeenCalledWith ( 5 , expect . objectContaining ( {
orientation : 'landscape' ,
wakeHour : 4 ,
wakeTimes : [ 4 * 60 ] ,
timezone : 'UTC' ,
} ) )
const sheet = wrapper . findComponent ( { name : 'BaseBottomSheet' } )
@@ -347,13 +347,13 @@ describe('HomeView', () => {
expect ( wrapper . findComponent ( { name : 'FrameCard' } ) . props ( 'status' ) ) . toBe ( 'sync-fail' )
} )
it ( 'uses a 24h window for devices configured with a daily wakeHour ' , async ( ) = > {
it ( 'uses a 24h window for devices configured with explicit wake times ' , async ( ) = > {
const devicesStore = useDevicesStore ( )
// wakeHour set, last seen 30h ago — between 1× 24h and 2× 24h → sync-fail
// wakeTimes set, last seen 30h ago — between 1× 24h and 2× 24h → sync-fail
devicesStore . devices = [ makeDevice ( {
id : 1 ,
wakeHour : 4 ,
rotationIntervalMinutes : 5 , // ignored when wakeHour is set
wakeTimes : [ 4 * 60 ] ,
rotationIntervalMinutes : 5 , // ignored when wakeTimes is non-empty
lastSeenAt : new Date ( Date . now ( ) - 1000 * 60 * 60 * 30 ) . toISOString ( ) ,
} ) ]
vi . spyOn ( devicesStore , 'fetchDevices' ) . mockResolvedValue ( )
@@ -377,11 +377,11 @@ describe('HomeView', () => {
expect ( props . nextSync ) . toMatch ( /next sync in/ )
} )
it ( 'passes a wakeHour -based nextSync label when the device wakes daily ' , async ( ) = > {
it ( 'passes a wakeTimes -based nextSync label when the device has explicit wake times ' , async ( ) = > {
const devicesStore = useDevicesStore ( )
devicesStore . devices = [ makeDevice ( {
id : 1 ,
wakeHour : 4 ,
wakeTimes : [ 4 * 60 ] ,
timezone : 'UTC' ,
lastSeenAt : new Date ( Date . now ( ) - 1000 * 60 * 60 ) . toISOString ( ) ,
} ) ]
@@ -432,24 +432,29 @@ describe('HomeView', () => {
expect ( wrapper . findComponent ( { name : 'FrameCard' } ) . props ( 'nextSync' ) ) . toBeNull ( )
} )
it ( 'formats wakeHour 12 PM, 12 AM, and 8 P M correctly' , async ( ) = > {
it ( 'formats wake times 12 PM, 12 AM, 8 PM, and 6:30 A M correctly' , async ( ) = > {
const devicesStore = useDevicesStore ( )
vi . spyOn ( devicesStore , 'fetchDevices' ) . mockResolvedValue ( )
devicesStore . devices = [ makeDevice ( { id : 1 , wakeHour : 12 , timezone : 'UTC' } ) ]
devicesStore . devices = [ makeDevice ( { id : 1 , wakeTimes : [ 12 * 60 ] , timezone : 'UTC' } ) ]
let wrapper = mountView ( )
await flushPromises ( )
expect ( wrapper . findComponent ( { name : 'FrameCard' } ) . props ( 'nextSync' ) ) . toMatch ( /12 PM/ )
devicesStore . devices = [ makeDevice ( { id : 1 , wakeHour : 0 , timezone : 'UTC' } ) ]
devicesStore . devices = [ makeDevice ( { id : 1 , wakeTimes : [ 0 ] , timezone : 'UTC' } ) ]
wrapper = mountView ( )
await flushPromises ( )
expect ( wrapper . findComponent ( { name : 'FrameCard' } ) . props ( 'nextSync' ) ) . toMatch ( /12 AM/ )
devicesStore . devices = [ makeDevice ( { id : 1 , wakeHour : 20 , timezone : 'UTC' } ) ]
devicesStore . devices = [ makeDevice ( { id : 1 , wakeTimes : [ 20 * 60 ] , timezone : 'UTC' } ) ]
wrapper = mountView ( )
await flushPromises ( )
expect ( wrapper . findComponent ( { name : 'FrameCard' } ) . props ( 'nextSync' ) ) . toMatch ( /8 PM/ )
devicesStore . devices = [ makeDevice ( { id : 1 , wakeTimes : [ 6 * 60 + 30 ] , timezone : 'UTC' } ) ]
wrapper = mountView ( )
await flushPromises ( )
expect ( wrapper . findComponent ( { name : 'FrameCard' } ) . props ( 'nextSync' ) ) . toMatch ( /6:30 AM/ )
} )
it ( 'returns null lastSync when the device has no recorded last-seen time' , async ( ) = > {
@@ -493,9 +498,9 @@ describe('HomeView', () => {
expect ( wrapper . findComponent ( { name : 'FrameCard' } ) . props ( 'thumbnailUrl' ) ) . toBeUndefined ( )
} )
it ( 'updates editWakeHour when the user picks a different hour chip ' , async ( ) = > {
it ( '+ Add time appends a new wake time and saves it ' , async ( ) = > {
const devicesStore = useDevicesStore ( )
devicesStore . devices = [ makeDevice ( { id : 5 , wakeHour : 4 } ) ]
devicesStore . devices = [ makeDevice ( { id : 5 , wakeTimes : [ 4 * 60 ] } ) ]
vi . spyOn ( devicesStore , 'fetchDevices' ) . mockResolvedValue ( )
vi . spyOn ( devicesStore , 'updateDevice' ) . mockResolvedValue ( makeDevice ( { id : 5 } ) )
@@ -504,16 +509,95 @@ describe('HomeView', () => {
await wrapper . findComponent ( { name : 'FrameCard' } ) . vm . $emit ( 'edit' , 5 )
await flushPromises ( )
const chips = wrapper . findAll ( '.home-view__interval-chip' )
const chip8pm = chips . find ( c = > c . text ( ) === '8 PM' ) !
await chip8pm . trigger ( 'click' )
expect ( chip8pm . classes ( ) ) . toContain ( 'home-view__interval-chip--on ' )
// Sheet opens in 'times' mode (because device.wakeTimes is non-empty).
// Click the + Add time button — it should add 9:00 AM (first default
// candidate not already in the list).
const addBtn = wrapper . find ( '. home-view__time-add ' )
await addBtn . trigger ( 'click' )
await flushPromises ( )
expect ( wrapper . findAll ( '.home-view__time-row' ) ) . toHaveLength ( 2 )
const saveBtn = wrapper . findAllComponents ( { name : 'BaseButton' } )
. find ( b = > b . text ( ) . toLowerCase ( ) . includes ( 'sav' ) ) !
await saveBtn . trigger ( 'click' )
await flushPromises ( )
expect ( devicesStore . updateDevice ) . toHaveBeenCalledWith ( 5 , expect . objectContaining ( { wakeHour : 20 } ) )
expect ( devicesStore . updateDevice ) . toHaveBeenCalledWith ( 5 , expect . objectContaining ( {
wakeTimes : [ 4 * 60 , 9 * 60 ] ,
} ) )
} )
it ( 'trash button removes a wake time from the list' , async ( ) = > {
const devicesStore = useDevicesStore ( )
devicesStore . devices = [ makeDevice ( { id : 5 , wakeTimes : [ 6 * 60 , 18 * 60 ] } ) ]
vi . spyOn ( devicesStore , 'fetchDevices' ) . mockResolvedValue ( )
vi . spyOn ( devicesStore , 'updateDevice' ) . mockResolvedValue ( makeDevice ( { id : 5 } ) )
const wrapper = mountView ( )
await flushPromises ( )
await wrapper . findComponent ( { name : 'FrameCard' } ) . vm . $emit ( 'edit' , 5 )
await flushPromises ( )
const removeButtons = wrapper . findAll ( '.home-view__time-remove' )
expect ( removeButtons ) . toHaveLength ( 2 )
// Remove the first row (6 AM)
await removeButtons [ 0 ] . trigger ( 'click' )
await flushPromises ( )
expect ( wrapper . findAll ( '.home-view__time-row' ) ) . toHaveLength ( 1 )
const saveBtn = wrapper . findAllComponents ( { name : 'BaseButton' } )
. find ( b = > b . text ( ) . toLowerCase ( ) . includes ( 'sav' ) ) !
await saveBtn . trigger ( 'click' )
await flushPromises ( )
expect ( devicesStore . updateDevice ) . toHaveBeenCalledWith ( 5 , expect . objectContaining ( {
wakeTimes : [ 18 * 60 ] ,
} ) )
} )
it ( 'switching the mode dropdown to interval saves rotationIntervalMinutes and clears wakeTimes' , async ( ) = > {
const devicesStore = useDevicesStore ( )
devicesStore . devices = [ makeDevice ( {
id : 5 ,
wakeTimes : [ 4 * 60 ] ,
rotationIntervalMinutes : 60 ,
} ) ]
vi . spyOn ( devicesStore , 'fetchDevices' ) . mockResolvedValue ( )
vi . spyOn ( devicesStore , 'updateDevice' ) . mockResolvedValue ( makeDevice ( { id : 5 } ) )
const wrapper = mountView ( )
await flushPromises ( )
await wrapper . findComponent ( { name : 'FrameCard' } ) . vm . $emit ( 'edit' , 5 )
await flushPromises ( )
const modeSelect = wrapper . find ( '.home-view__mode-select' )
; ( modeSelect . element as HTMLSelectElement ) . value = 'interval'
await modeSelect . trigger ( 'change' )
const intervalInput = wrapper . find ( '.home-view__interval-input' )
; ( intervalInput . element as HTMLInputElement ) . value = '15'
await intervalInput . trigger ( 'input' )
const saveBtn = wrapper . findAllComponents ( { name : 'BaseButton' } )
. find ( b = > b . text ( ) . toLowerCase ( ) . includes ( 'sav' ) ) !
await saveBtn . trigger ( 'click' )
await flushPromises ( )
expect ( devicesStore . updateDevice ) . toHaveBeenCalledWith ( 5 , expect . objectContaining ( {
wakeTimes : [ ] ,
rotationIntervalMinutes : 15 ,
} ) )
} )
it ( 'shows the propagation note in the settings sheet' , async ( ) = > {
const devicesStore = useDevicesStore ( )
devicesStore . devices = [ makeDevice ( { id : 5 } ) ]
vi . spyOn ( devicesStore , 'fetchDevices' ) . mockResolvedValue ( )
const wrapper = mountView ( )
await flushPromises ( )
await wrapper . findComponent ( { name : 'FrameCard' } ) . vm . $emit ( 'edit' , 5 )
await flushPromises ( )
expect ( wrapper . find ( '.home-view__propagation-note' ) . text ( ) )
. toMatch ( /take effect at the next device update/i )
} )
it ( 'saving while no device is being edited is a no-op (defensive guard)' , async ( ) = > {
@@ -536,7 +620,7 @@ describe('HomeView', () => {
it ( 'updates editName/orientation/timezone when their components emit changes' , async ( ) = > {
const devicesStore = useDevicesStore ( )
devicesStore . devices = [ makeDevice ( { id : 5 , name : 'Original' , wakeHour : 4 , timezone : 'UTC' } ) ]
devicesStore . devices = [ makeDevice ( { id : 5 , name : 'Original' , wakeTimes : [ 4 * 60 ] , timezone : 'UTC' } ) ]
vi . spyOn ( devicesStore , 'fetchDevices' ) . mockResolvedValue ( )
const updateSpy = vi . spyOn ( devicesStore , 'updateDevice' ) . mockResolvedValue ( makeDevice ( { id : 5 } ) )
@@ -562,12 +646,13 @@ describe('HomeView', () => {
} ) )
} )
it ( 'edit defaults wakeHour to 4 and timezone to UTC when the device has neither ' , async ( ) = > {
it ( 'opens in interval mode and defaults timezone to UTC when device has empty wakeTimes ' , async ( ) = > {
const devicesStore = useDevicesStore ( )
devicesStore . devices = [ makeDevice ( {
id : 5 ,
name : 'Den' ,
wakeHour : null ,
wakeTimes : [ ] ,
rotationIntervalMinutes : 60 ,
timezone : null as any ,
} ) ]
vi . spyOn ( devicesStore , 'fetchDevices' ) . mockResolvedValue ( )
@@ -578,12 +663,17 @@ describe('HomeView', () => {
await wrapper . findComponent ( { name : 'FrameCard' } ) . vm . $emit ( 'edit' , 5 )
await flushPromises ( )
// Sheet opens in interval mode — interval input is shown, time-list is not.
expect ( wrapper . find ( '.home-view__interval-input' ) . exists ( ) ) . toBe ( true )
expect ( wrapper . find ( '.home-view__time-add' ) . exists ( ) ) . toBe ( false )
const saveBtn = wrapper . findAllComponents ( { name : 'BaseButton' } )
. find ( b = > b . text ( ) . toLowerCase ( ) . includes ( 'sav' ) ) !
await saveBtn . trigger ( 'click' )
await flushPromises ( )
expect ( updateSpy ) . toHaveBeenCalledWith ( 5 , expect . objectContaining ( {
wakeHour : 4 ,
wakeTimes : [ ] ,
rotationIntervalMinutes : 60 ,
timezone : 'UTC' ,
} ) )
} )