meridian / frontend /src /components /PushNotificationButton.spec.js
GitHub Actions
Deploy to HuggingFace Spaces
8a428e5
Raw
History Blame Contribute Delete
6.29 kB
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import { flushPromises, mount } from '@vue/test-utils'
import { setActivePinia, createPinia } from 'pinia'
import PushNotificationButton from './PushNotificationButton.vue'
vi.mock('../api.js', () => ({
default: { get: vi.fn(), post: vi.fn(), delete: vi.fn() },
}))
import api from '../api.js'
describe('PushNotificationButton', () => {
let originalServiceWorker
let originalPushManager
let originalNotification
beforeEach(() => {
localStorage.clear()
setActivePinia(createPinia())
vi.clearAllMocks()
originalServiceWorker = navigator.serviceWorker
originalPushManager = window.PushManager
originalNotification = window.Notification
})
afterEach(() => {
if (originalServiceWorker === undefined) delete navigator.serviceWorker
else Object.defineProperty(navigator, 'serviceWorker', { value: originalServiceWorker, configurable: true })
window.PushManager = originalPushManager
window.Notification = originalNotification
})
it('renders nothing when the browser does not support push', async () => {
delete navigator.serviceWorker
delete window.PushManager
const wrapper = mount(PushNotificationButton)
await flushPromises()
expect(wrapper.find('button').exists()).toBe(false)
})
describe('when supported', () => {
let registration
beforeEach(() => {
window.PushManager = function () {}
registration = {
pushManager: {
subscribe: vi.fn(),
getSubscription: vi.fn(),
},
}
Object.defineProperty(navigator, 'serviceWorker', {
value: { ready: Promise.resolve(registration) },
configurable: true,
})
window.Notification = { requestPermission: vi.fn() }
})
it('renders the bell-off icon when not subscribed', async () => {
const wrapper = mount(PushNotificationButton)
await flushPromises()
expect(wrapper.find('button').exists()).toBe(true)
expect(wrapper.attributes('aria-pressed')).not.toBe('true')
})
it('shows subscribed state from localStorage on mount', async () => {
localStorage.setItem('push_subscribed', '1')
const wrapper = mount(PushNotificationButton)
await flushPromises()
expect(wrapper.find('button').attributes('aria-pressed')).toBe('true')
})
it('subscribes on click when permission is granted', async () => {
window.Notification.requestPermission.mockResolvedValue('granted')
api.get.mockResolvedValue({ data: { publicKey: 'QUJD' } })
registration.pushManager.subscribe.mockResolvedValue({
toJSON: () => ({ endpoint: 'https://push.example/1' }),
})
api.post.mockResolvedValue({})
const wrapper = mount(PushNotificationButton)
await flushPromises()
await wrapper.find('button').trigger('click')
await flushPromises()
expect(api.post).toHaveBeenCalledWith('/push/subscribe', { endpoint: 'https://push.example/1' })
expect(localStorage.getItem('push_subscribed')).toBe('1')
expect(wrapper.find('button').attributes('aria-pressed')).toBe('true')
})
it('does not subscribe when permission is denied', async () => {
window.Notification.requestPermission.mockResolvedValue('denied')
const wrapper = mount(PushNotificationButton)
await flushPromises()
await wrapper.find('button').trigger('click')
await flushPromises()
expect(api.get).not.toHaveBeenCalled()
expect(wrapper.find('button').attributes('aria-pressed')).not.toBe('true')
})
it('does nothing when the server has no VAPID key configured', async () => {
window.Notification.requestPermission.mockResolvedValue('granted')
api.get.mockResolvedValue({ data: {} })
const wrapper = mount(PushNotificationButton)
await flushPromises()
await wrapper.find('button').trigger('click')
await flushPromises()
expect(registration.pushManager.subscribe).not.toHaveBeenCalled()
})
it('unsubscribes on click when already subscribed', async () => {
localStorage.setItem('push_subscribed', '1')
const sub = { endpoint: 'https://push.example/1', unsubscribe: vi.fn().mockResolvedValue(true) }
registration.pushManager.getSubscription.mockResolvedValue(sub)
api.delete.mockResolvedValue({})
const wrapper = mount(PushNotificationButton)
await flushPromises()
await wrapper.find('button').trigger('click')
await flushPromises()
expect(api.delete).toHaveBeenCalledWith('/push/unsubscribe', { data: { endpoint: sub.endpoint } })
expect(sub.unsubscribe).toHaveBeenCalled()
expect(localStorage.getItem('push_subscribed')).toBeNull()
})
it('handles unsubscribe when there is no active subscription', async () => {
localStorage.setItem('push_subscribed', '1')
registration.pushManager.getSubscription.mockResolvedValue(null)
const wrapper = mount(PushNotificationButton)
await flushPromises()
await wrapper.find('button').trigger('click')
await flushPromises()
expect(localStorage.getItem('push_subscribed')).toBeNull()
})
it('logs and recovers when the toggle throws', async () => {
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
window.Notification.requestPermission.mockRejectedValue(new Error('boom'))
const wrapper = mount(PushNotificationButton)
await flushPromises()
await wrapper.find('button').trigger('click')
await flushPromises()
expect(consoleSpy).toHaveBeenCalled()
consoleSpy.mockRestore()
})
it('ignores a second click while a toggle is already in flight', async () => {
let resolvePermission
window.Notification.requestPermission.mockReturnValue(
new Promise((resolve) => { resolvePermission = resolve }),
)
const wrapper = mount(PushNotificationButton)
await flushPromises()
wrapper.find('button').trigger('click')
await wrapper.vm.$nextTick()
wrapper.find('button').trigger('click')
resolvePermission('denied')
await flushPromises()
expect(window.Notification.requestPermission).toHaveBeenCalledTimes(1)
})
})
})