Files
sky_phone/frontend/src/stores/notifications.test.ts
T
Leon.Schmidt 1b453e3456 ENH - add muted vibration alerts
Play the supplied notification vibration asset for muted push banners and loop the supplied call vibration asset for muted incoming calls. Remove the synthesized vibration tone, add playback coverage, and provide a development preview for the muted notification path.
2026-08-17 15:30:53 +02:00

302 lines
8.0 KiB
TypeScript

import { createPinia, setActivePinia } from 'pinia'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import {
MAX_LOCK_SCREEN_NOTIFICATIONS,
useNotificationsStore,
type PhoneNotificationDevice,
} from '@/stores/notifications'
import { usePhoneStore } from '@/stores/phone'
import { nuiCall } from '@/utils/nui'
import {
DEFAULT_PHONE_PREFERENCES,
type PhonePreferencesV1,
} from '@/utils/preferences'
import { playPhoneVibration } from '@/utils/tones'
vi.mock('@/utils/tones', () => ({
playPhoneTone: vi.fn(() => vi.fn()),
playPhoneVibration: vi.fn(() => vi.fn()),
}))
vi.mock('@/utils/nui', () => ({
nuiCall: vi.fn(async () => ({ success: true, data: { revision: 1 } })),
}))
function device(
imei: string,
configure?: (preferences: PhonePreferencesV1) => void,
): PhoneNotificationDevice {
const preferences = structuredClone(DEFAULT_PHONE_PREFERENCES)
configure?.(preferences)
return { imei, name: `Phone ${imei}`, preferences }
}
function openPhone(imei: string): void {
usePhoneStore().open({
device: {
data: {},
imei,
name: `Phone ${imei}`,
sim: null,
},
})
}
describe('notifications store', () => {
beforeEach(() => {
vi.useFakeTimers()
vi.stubGlobal('window', {
matchMedia: vi.fn(() => ({ matches: false })),
})
setActivePinia(createPinia())
vi.mocked(nuiCall).mockClear()
})
afterEach(() => {
vi.clearAllTimers()
vi.useRealTimers()
vi.unstubAllGlobals()
})
it('shows one simultaneous preview per notifying phone', () => {
const notifications = useNotificationsStore()
notifications.show({
appId: 'mail',
device: device('111'),
text: 'First phone',
title: 'Mail',
})
notifications.show({
appId: 'mail',
device: device('222'),
text: 'Second phone',
title: 'Mail',
})
expect(
notifications.devicePreviews.map(
(notification) => notification.device?.imei,
),
).toEqual(['111', '222'])
})
it('queues mail independently for each phone', () => {
const notifications = useNotificationsStore()
const target = device('111')
const firstId = notifications.show({
appId: 'mail',
device: target,
text: 'First message',
title: 'Mail',
})
notifications.show({
appId: 'mail',
device: target,
text: 'Second message',
title: 'Mail',
})
expect(notifications.devicePreviews).toHaveLength(1)
expect(notifications.devicePreviews[0].text).toBe('First message')
notifications.dismiss(firstId!)
expect(notifications.devicePreviews[0].text).toBe('Second message')
})
it('uses the target phone notification preferences', () => {
const notifications = useNotificationsStore()
const muted = device('111', (preferences) => {
preferences.settings.notifications.mail.enabled = false
})
const id = notifications.show({
appId: 'mail',
device: muted,
text: 'Hidden message',
title: 'Mail',
})
expect(id).toBeNull()
expect(notifications.devicePreviews).toEqual([])
})
it('plays a vibration alert for push notifications while globally muted', () => {
const notifications = useNotificationsStore()
const muted = device('111', (preferences) => {
preferences.settings.notificationVolume = 0
preferences.settings.ringtoneVolume = 0
})
notifications.show({
appId: 'messages',
device: muted,
text: 'Muted message',
title: 'Messages',
})
expect(playPhoneVibration).toHaveBeenCalledWith('notification', false)
})
it('suppresses normal notifications during Focus but keeps critical alerts', () => {
const phone = usePhoneStore()
const notifications = useNotificationsStore()
phone.preferences.settings.focusMode = true
const normalId = notifications.show({
appId: 'mail',
text: 'Quiet message',
title: 'Mail',
})
const criticalId = notifications.show({
appId: 'clock',
critical: true,
text: 'Timer finished',
title: 'Clock',
})
expect(normalId).toBeNull()
expect(criticalId).not.toBeNull()
expect(notifications.current?.text).toBe('Timer finished')
})
it('persists a notification immediately while the phone is closed', async () => {
const notifications = useNotificationsStore()
notifications.show({
appId: 'companies',
device: device('111'),
route: '/apps/companies?requestId=request-1&area=customer',
text: 'Store while closed',
title: 'Companies',
})
await Promise.resolve()
await Promise.resolve()
expect(nuiCall).toHaveBeenCalledWith('notifications:save', {
imei: '111',
payload: {
items: [
expect.objectContaining({
appId: 'companies',
route: '/apps/companies?requestId=request-1&area=customer',
text: 'Store while closed',
title: 'Companies',
}),
],
version: 1,
},
})
})
it('keeps notifications on the lock screen after the banner expires', () => {
openPhone('111')
const notifications = useNotificationsStore()
notifications.show({
appId: 'mail',
text: 'Persistent lock screen message',
title: 'Mail',
})
vi.advanceTimersByTime(
DEFAULT_PHONE_PREFERENCES.settings.notificationDurationSeconds * 1000,
)
expect(notifications.current).toBeNull()
expect(notifications.lockScreenNotifications[0].text).toBe(
'Persistent lock screen message',
)
})
it('hides the matching device preview when that phone opens', () => {
const notifications = useNotificationsStore()
notifications.show({
appId: 'mail',
device: device('111'),
text: 'Move to lock screen',
title: 'Mail',
})
openPhone('111')
notifications.hideDevicePreview('111')
expect(notifications.devicePreviews).toEqual([])
expect(notifications.lockScreenNotifications[0].text).toBe(
'Move to lock screen',
)
})
it('dismisses a lock screen notification everywhere', () => {
openPhone('111')
const notifications = useNotificationsStore()
const id = notifications.show({
appId: 'mail',
text: 'Dismiss me',
title: 'Mail',
})
notifications.dismissFromLockScreen(id!)
expect(notifications.current).toBeNull()
expect(notifications.lockScreenNotifications).toEqual([])
})
it('hydrates persisted notifications and only removes them explicitly', () => {
const phone = usePhoneStore()
phone.open({
device: {
data: {
notifications: {
payload: {
items: [
{
appId: 'mail',
id: 'saved-notification',
text: 'Saved message',
title: 'Mail',
},
],
version: 1,
},
revision: 3,
},
},
imei: '111',
name: 'Phone 111',
sim: null,
},
})
const notifications = useNotificationsStore()
notifications.hydrate(phone.device?.data.notifications?.payload, '111')
vi.advanceTimersByTime(60_000)
expect(notifications.lockScreenNotifications[0].text).toBe('Saved message')
notifications.clearLockScreen()
expect(notifications.lockScreenNotifications).toEqual([])
})
it('bounds persisted lock screen history to the newest notifications', () => {
openPhone('111')
const notifications = useNotificationsStore()
const items = Array.from(
{ length: MAX_LOCK_SCREEN_NOTIFICATIONS + 10 },
(_, index) => ({
appId: 'mail' as const,
id: `saved-${index}`,
text: `Message ${index}`,
title: 'Mail',
}),
)
notifications.hydrate({ items, version: 1 }, '111')
expect(notifications.lockScreenNotifications).toHaveLength(
MAX_LOCK_SCREEN_NOTIFICATIONS,
)
expect(notifications.lockScreenNotifications[0]?.id).toBe('saved-59')
expect(notifications.lockScreenNotifications.at(-1)?.id).toBe('saved-10')
})
})