File size: 4,878 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
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import { defineComponent, h } from 'vue'
import { mount } from '@vue/test-utils'
import { useWakeLock } from './useWakeLock'

// useWakeLock() calls onUnmounted() internally, which requires a genuine
// component instance (an effectScope alone is not enough). Mounting a tiny
// host component — and unmounting it after each test — is what actually
// exercises (and cleans up after) the composable's lifecycle hook, and
// avoids leaking `document` listeners between tests.
function mountWakeLock() {
  let exposed
  const Host = defineComponent({
    setup() {
      exposed = useWakeLock()
      return () => h('div')
    },
  })
  const wrapper = mount(Host)
  return { wrapper, ...exposed }
}

describe('useWakeLock', () => {
  let originalWakeLock
  let wrapper

  beforeEach(() => {
    originalWakeLock = navigator.wakeLock
  })

  afterEach(() => {
    wrapper?.unmount()
    wrapper = undefined
    if (originalWakeLock === undefined) delete navigator.wakeLock
    else Object.defineProperty(navigator, 'wakeLock', { value: originalWakeLock, configurable: true })
    vi.restoreAllMocks()
  })

  it('does nothing when the Wake Lock API is not supported', async () => {
    delete navigator.wakeLock
    const result = mountWakeLock()
    wrapper = result.wrapper
    await expect(result.acquireWakeLock()).resolves.toBeUndefined()
    await expect(result.releaseWakeLock()).resolves.toBeUndefined()
  })

  it('acquires and releases a wake lock when supported', async () => {
    const release = vi.fn().mockResolvedValue(undefined)
    const mockLock = { addEventListener: vi.fn(), release }
    Object.defineProperty(navigator, 'wakeLock', {
      value: { request: vi.fn().mockResolvedValue(mockLock) },
      configurable: true,
    })

    const result = mountWakeLock()
    wrapper = result.wrapper
    await result.acquireWakeLock()
    expect(navigator.wakeLock.request).toHaveBeenCalledWith('screen')

    await result.releaseWakeLock()
    expect(release).toHaveBeenCalled()
  })

  it('does not request a second lock while one is already held', async () => {
    const mockLock = { addEventListener: vi.fn(), release: vi.fn().mockResolvedValue(undefined) }
    const request = vi.fn().mockResolvedValue(mockLock)
    Object.defineProperty(navigator, 'wakeLock', { value: { request }, configurable: true })

    const result = mountWakeLock()
    wrapper = result.wrapper
    await result.acquireWakeLock()
    await result.acquireWakeLock()
    expect(request).toHaveBeenCalledTimes(1)
    await result.releaseWakeLock()
  })

  it('silently ignores a rejected wake lock request', async () => {
    Object.defineProperty(navigator, 'wakeLock', {
      value: { request: vi.fn().mockRejectedValue(new Error('denied')) },
      configurable: true,
    })
    const result = mountWakeLock()
    wrapper = result.wrapper
    await expect(result.acquireWakeLock()).resolves.toBeUndefined()
    await result.releaseWakeLock()
  })

  it('releaseWakeLock is a no-op when no lock is held', async () => {
    delete navigator.wakeLock
    const result = mountWakeLock()
    wrapper = result.wrapper
    await expect(result.releaseWakeLock()).resolves.toBeUndefined()
  })

  it('re-acquires the lock when the tab becomes visible again after wanting one', async () => {
    const mockLock = { addEventListener: vi.fn(), release: vi.fn().mockResolvedValue(undefined) }
    const request = vi.fn().mockResolvedValue(mockLock)
    Object.defineProperty(navigator, 'wakeLock', { value: { request }, configurable: true })

    const result = mountWakeLock()
    wrapper = result.wrapper
    await result.acquireWakeLock()
    // Simulate the browser auto-releasing the lock on tab-hide.
    const releaseCallback = mockLock.addEventListener.mock.calls[0][1]
    releaseCallback()

    Object.defineProperty(document, 'visibilityState', { value: 'visible', configurable: true })
    document.dispatchEvent(new Event('visibilitychange'))
    // Allow the async re-acquire to run.
    await Promise.resolve()
    await Promise.resolve()
    expect(request).toHaveBeenCalledTimes(2)
    await result.releaseWakeLock()
  })

  it('cleans up the visibility listener and releases the lock on unmount', async () => {
    const release = vi.fn().mockResolvedValue(undefined)
    const mockLock = { addEventListener: vi.fn(), release }
    Object.defineProperty(navigator, 'wakeLock', {
      value: { request: vi.fn().mockResolvedValue(mockLock) },
      configurable: true,
    })
    const removeSpy = vi.spyOn(document, 'removeEventListener')

    const result = mountWakeLock()
    await result.acquireWakeLock()
    result.wrapper.unmount()
    wrapper = undefined

    expect(removeSpy).toHaveBeenCalledWith('visibilitychange', expect.any(Function))
    expect(release).toHaveBeenCalled()
  })
})