File size: 6,287 Bytes
8a428e5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
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)
    })
  })
})