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.
|
// 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
|
// 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.
|
// 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 pending: (() => void)[] = []
|
||||||
const request = vi.fn((type: string) => {
|
const request = vi.fn((type: string) => {
|
||||||
expect(type).toBe('screen')
|
expect(type).toBe('screen')
|
||||||
const sentinel: FakeSentinel = {
|
const sentinel: FakeSentinel = Object.assign(new EventTarget(), {
|
||||||
released: false,
|
released: false,
|
||||||
release: vi.fn(() => {
|
release: vi.fn(() => {
|
||||||
sentinel.released = true
|
letGo(sentinel)
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}),
|
}),
|
||||||
}
|
})
|
||||||
sentinels.push(sentinel)
|
sentinels.push(sentinel)
|
||||||
if (!hold) return Promise.resolve(sentinel)
|
if (!hold) return Promise.resolve(sentinel)
|
||||||
return new Promise<FakeSentinel>((resolve) => pending.push(() => 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))
|
await waitFor(() => expect(wakeLock.request).toHaveBeenCalledTimes(1))
|
||||||
|
|
||||||
// Hidden, the browser lets the lock go; nothing is asked while hidden.
|
// Hidden, the browser lets the lock go; nothing is asked while hidden.
|
||||||
wakeLock.sentinels[0]!.released = true
|
|
||||||
act(() => setVisibility('hidden'))
|
act(() => setVisibility('hidden'))
|
||||||
|
act(() => letGo(wakeLock.sentinels[0]!))
|
||||||
expect(wakeLock.request).toHaveBeenCalledTimes(1)
|
expect(wakeLock.request).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
act(() => setVisibility('visible'))
|
act(() => setVisibility('visible'))
|
||||||
@@ -489,6 +496,49 @@ describe('Kitchen reading mode', () => {
|
|||||||
expect(wakeLock.request).toHaveBeenCalledTimes(2)
|
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 () => {
|
it('reports a lock the browser refuses and turns the switch back off', async () => {
|
||||||
const wakeLock = stubWakeLock()
|
const wakeLock = stubWakeLock()
|
||||||
wakeLock.request.mockRejectedValueOnce(new DOMException('Blocked by permissions policy', 'NotAllowedError'))
|
wakeLock.request.mockRejectedValueOnce(new DOMException('Blocked by permissions policy', 'NotAllowedError'))
|
||||||
|
|||||||
@@ -32,6 +32,15 @@ export function useKeepScreenOn(): KeepScreenOn {
|
|||||||
let requesting = false
|
let requesting = false
|
||||||
let left = 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() {
|
function acquire() {
|
||||||
if (document.visibilityState !== 'visible' || requesting) return
|
if (document.visibilityState !== 'visible' || requesting) return
|
||||||
if (sentinel !== null && !sentinel.released) return
|
if (sentinel !== null && !sentinel.released) return
|
||||||
@@ -39,8 +48,16 @@ export function useKeepScreenOn(): KeepScreenOn {
|
|||||||
wakeLock.request('screen').then(
|
wakeLock.request('screen').then(
|
||||||
(granted) => {
|
(granted) => {
|
||||||
requesting = false
|
requesting = false
|
||||||
if (left) void granted.release()
|
if (left) {
|
||||||
else sentinel = granted
|
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) => {
|
(err: unknown) => {
|
||||||
requesting = false
|
requesting = false
|
||||||
|
|||||||
Reference in New Issue
Block a user