| 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) |
| }) |
| }) |
| }) |
|
|