Keep screen on via Wake Lock (Feature 15) #151
@@ -117,7 +117,14 @@ function currentStep() {
|
||||
}
|
||||
|
||||
// FakeSentinel is a WakeLockSentinel as far as the screen uses one.
|
||||
type FakeSentinel = { released: boolean; release: Mock<() => Promise<void>> }
|
||||
type FakeSentinel = EventTarget & { released: boolean; release: Mock<() => Promise<void>> }
|
||||
|
||||
// letGo releases a lock as the browser does, telling its listeners so.
|
||||
function letGo(sentinel: FakeSentinel) {
|
||||
if (sentinel.released) return
|
||||
sentinel.released = true
|
||||
sentinel.dispatchEvent(new Event('release'))
|
||||
}
|
||||
|
||||
// stubWakeLock gives jsdom, which has none, a Screen Wake Lock. Each request
|
||||
// is granted at once, or, given hold, only when the test calls grant.
|
||||
@@ -126,13 +133,13 @@ function stubWakeLock({ hold = false } = {}) {
|
||||
const pending: (() => void)[] = []
|
||||
const request = vi.fn((type: string) => {
|
||||
expect(type).toBe('screen')
|
||||
const sentinel: FakeSentinel = {
|
||||
const sentinel: FakeSentinel = Object.assign(new EventTarget(), {
|
||||
released: false,
|
||||
release: vi.fn(() => {
|
||||
sentinel.released = true
|
||||
letGo(sentinel)
|
||||
return Promise.resolve()
|
||||
}),
|
||||
}
|
||||
})
|
||||
sentinels.push(sentinel)
|
||||
if (!hold) return Promise.resolve(sentinel)
|
||||
return new Promise<FakeSentinel>((resolve) => pending.push(() => resolve(sentinel)))
|
||||
@@ -471,8 +478,8 @@ describe('Kitchen reading mode', () => {
|
||||
await waitFor(() => expect(wakeLock.request).toHaveBeenCalledTimes(1))
|
||||
|
||||
// Hidden, the browser lets the lock go; nothing is asked while hidden.
|
||||
wakeLock.sentinels[0]!.released = true
|
||||
act(() => setVisibility('hidden'))
|
||||
act(() => letGo(wakeLock.sentinels[0]!))
|
||||
expect(wakeLock.request).toHaveBeenCalledTimes(1)
|
||||
|
||||
act(() => setVisibility('visible'))
|
||||
@@ -489,6 +496,49 @@ describe('Kitchen reading mode', () => {
|
||||
expect(wakeLock.request).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('asks for the lock again when the browser lets it go while the page is visible', async () => {
|
||||
const wakeLock = stubWakeLock()
|
||||
serve()
|
||||
await renderPage()
|
||||
const keepOn = screen.getByRole('switch', { name: 'Keep screen on' })
|
||||
fireEvent.click(keepOn)
|
||||
await waitFor(() => expect(wakeLock.request).toHaveBeenCalledTimes(1))
|
||||
|
||||
act(() => letGo(wakeLock.sentinels[0]!))
|
||||
await waitFor(() => expect(wakeLock.request).toHaveBeenCalledTimes(2))
|
||||
await act(async () => {})
|
||||
expect(keepOn).toHaveAttribute('aria-checked', 'true')
|
||||
|
||||
// The fresh lock is watched too.
|
||||
act(() => letGo(wakeLock.sentinels[1]!))
|
||||
await waitFor(() => expect(wakeLock.request).toHaveBeenCalledTimes(3))
|
||||
expect(wakeLock.sentinels[2]!.released).toBe(false)
|
||||
expect(keepOn).toHaveAttribute('aria-checked', 'true')
|
||||
})
|
||||
|
||||
it('asks for the lock again when the page was hidden and shown while a request was pending', async () => {
|
||||
const wakeLock = stubWakeLock({ hold: true })
|
||||
serve()
|
||||
await renderPage()
|
||||
const keepOn = screen.getByRole('switch', { name: 'Keep screen on' })
|
||||
fireEvent.click(keepOn)
|
||||
expect(wakeLock.request).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Hidden and shown again before the first lock is granted, which the
|
||||
// browser has let go by the time it is.
|
||||
act(() => setVisibility('hidden'))
|
||||
act(() => setVisibility('visible'))
|
||||
expect(wakeLock.request).toHaveBeenCalledTimes(1)
|
||||
letGo(wakeLock.sentinels[0]!)
|
||||
await act(async () => wakeLock.grant())
|
||||
expect(wakeLock.request).toHaveBeenCalledTimes(2)
|
||||
|
||||
await act(async () => wakeLock.grant())
|
||||
expect(wakeLock.sentinels[1]!.released).toBe(false)
|
||||
expect(wakeLock.request).toHaveBeenCalledTimes(2)
|
||||
expect(keepOn).toHaveAttribute('aria-checked', 'true')
|
||||
})
|
||||
|
||||
it('reports a lock the browser refuses and turns the switch back off', async () => {
|
||||
const wakeLock = stubWakeLock()
|
||||
wakeLock.request.mockRejectedValueOnce(new DOMException('Blocked by permissions policy', 'NotAllowedError'))
|
||||
|
||||
@@ -32,6 +32,15 @@ export function useKeepScreenOn(): KeepScreenOn {
|
||||
let requesting = false
|
||||
let left = false
|
||||
|
||||
// The browser may let a lock go while the switch is on: always when the
|
||||
// page is hidden, and, the specification allows, while it is still
|
||||
// visible. Either way the lock is asked for again at once if the page is
|
||||
// visible, or when it is visible again if not, so the switch never claims
|
||||
// a lock that is not held.
|
||||
function onRelease() {
|
||||
if (!left) acquire()
|
||||
}
|
||||
|
||||
function acquire() {
|
||||
if (document.visibilityState !== 'visible' || requesting) return
|
||||
if (sentinel !== null && !sentinel.released) return
|
||||
@@ -39,8 +48,16 @@ export function useKeepScreenOn(): KeepScreenOn {
|
||||
wakeLock.request('screen').then(
|
||||
(granted) => {
|
||||
requesting = false
|
||||
if (left) void granted.release()
|
||||
else sentinel = granted
|
||||
if (left) {
|
||||
void granted.release()
|
||||
return
|
||||
}
|
||||
sentinel = granted
|
||||
granted.addEventListener('release', onRelease)
|
||||
// While the request was pending the page may have been hidden and
|
||||
// shown again (that visibilitychange was ignored) and the lock let
|
||||
// go before it could be listened for, so check again now.
|
||||
acquire()
|
||||
},
|
||||
(err: unknown) => {
|
||||
requesting = false
|
||||
|
||||
Reference in New Issue
Block a user