Skip to content

Commit 3e9a442

Browse files
authored
Add support for screen.lock/unlock (#875)
1 parent 012b392 commit 3e9a442

File tree

2 files changed

+231
-1
lines changed

2 files changed

+231
-1
lines changed

integration-test/test-web-compat.js

+162-1
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ describe('Ensure safari interface is injected', () => {
8080
})
8181
})
8282

83-
describe('Ensure Notification and Permissions interface is injected', () => {
83+
describe('Ensure Notification interface is injected', () => {
8484
let browser
8585
let server
8686
let teardown
@@ -300,6 +300,167 @@ describe('Permissions API', () => {
300300
})
301301
})
302302

303+
describe('ScreenOrientation API', () => {
304+
let browser
305+
let server
306+
let teardown
307+
let setupServer
308+
let gotoAndWait
309+
let port
310+
let page
311+
beforeAll(async () => {
312+
({ browser, setupServer, teardown, gotoAndWait } = await setup({ withExtension: true }))
313+
server = setupServer()
314+
})
315+
afterAll(async () => {
316+
await server?.close()
317+
await teardown()
318+
})
319+
320+
describe('disabled feature', () => {
321+
async function checkLockThrows (orientation) {
322+
const payload = `screen.orientation.lock(${JSON.stringify(orientation)})`
323+
const result = await page.evaluate(payload).catch((e) => {
324+
return { threw: e }
325+
})
326+
return result
327+
}
328+
329+
it(' should not fix screenOrientation API', async () => {
330+
port = server.address().port
331+
page = await browser.newPage()
332+
333+
// no screenLock setting
334+
await gotoAndWait(page, `http://localhost:${port}/blank.html`, { site: { enabledFeatures: ['webCompat'] } })
335+
const result = await checkLockThrows('landscape')
336+
expect(result.threw).not.toBeUndefined()
337+
expect(result.threw.message).toEqual('screen.orientation.lock() is not available on this device.')
338+
})
339+
})
340+
341+
describe('enabled feature', () => {
342+
let port
343+
let page
344+
345+
beforeAll(async () => {
346+
port = server.address().port
347+
page = await browser.newPage()
348+
await gotoAndWait(page, `http://localhost:${port}/blank.html`, {
349+
site: {
350+
enabledFeatures: ['webCompat']
351+
},
352+
featureSettings: {
353+
webCompat: {
354+
screenLock: 'enabled'
355+
}
356+
}
357+
})
358+
})
359+
360+
async function checkLock (orientation) {
361+
const payload = `screen.orientation.lock(${JSON.stringify(orientation)})`
362+
const result = await page.evaluate(payload).catch((e) => {
363+
return { threw: e }
364+
})
365+
const message = await page.evaluate(() => {
366+
return globalThis.lockReq
367+
})
368+
return { result, message }
369+
}
370+
371+
async function checkUnlock () {
372+
const payload = 'screen.orientation.unlock()'
373+
const result = await page.evaluate(payload).catch((e) => {
374+
return { threw: e }
375+
})
376+
const message = await page.evaluate(() => {
377+
return globalThis.lockReq
378+
})
379+
return { result, message }
380+
}
381+
382+
it('should err out when orientation not provided', async () => {
383+
const { result } = await checkLock()
384+
expect(result.threw).not.toBeUndefined()
385+
expect(result.threw.message).toContain("Failed to execute 'lock' on 'ScreenOrientation': 1 argument required, but only 0 present")
386+
})
387+
388+
it('should err out when orientation of unexpected type', async () => {
389+
const { result } = await checkLock({})
390+
expect(result.threw).not.toBeUndefined()
391+
expect(result.threw.message).toContain('not a valid enum value of type OrientationLockType')
392+
})
393+
394+
it('should err out when orientation of unexpected value', async () => {
395+
const { result } = await checkLock('xxx')
396+
expect(result.threw).not.toBeUndefined()
397+
expect(result.threw.message).toContain('not a valid enum value of type OrientationLockType')
398+
})
399+
400+
it('should propagate native TypeError', async () => {
401+
await page.evaluate(() => {
402+
globalThis.cssMessaging.impl.request = () => {
403+
return Promise.resolve({ failure: { name: 'TypeError', message: 'some error message' } })
404+
}
405+
})
406+
407+
const { result } = await checkLock('landscape')
408+
expect(result.threw).not.toBeUndefined()
409+
expect(result.threw.message).toContain('some error message')
410+
})
411+
412+
it('should propagate native InvalidStateError', async () => {
413+
await page.evaluate(() => {
414+
globalThis.cssMessaging.impl.request = () => {
415+
return Promise.resolve({ failure: { name: 'InvalidStateError', message: 'some error message' } })
416+
}
417+
})
418+
419+
const { result } = await checkLock('landscape')
420+
expect(result.threw).not.toBeUndefined()
421+
expect(result.threw.message).toContain('some error message')
422+
})
423+
424+
it('should propagate native default error', async () => {
425+
await page.evaluate(() => {
426+
globalThis.cssMessaging.impl.request = () => {
427+
return Promise.resolve({ failure: { name: 'xxx', message: 'some error message' } })
428+
}
429+
})
430+
431+
const { result } = await checkLock('landscape')
432+
expect(result.threw).not.toBeUndefined()
433+
expect(result.threw.message).toContain('some error message')
434+
})
435+
436+
it('should fix screenOrientation API', async () => {
437+
await page.evaluate(() => {
438+
globalThis.cssMessaging.impl.request = (req) => {
439+
globalThis.lockReq = req
440+
return Promise.resolve({})
441+
}
442+
})
443+
444+
const { result, message } = await checkLock('landscape')
445+
expect(result).toBeUndefined()
446+
expect(message).toEqual(jasmine.objectContaining({ featureName: 'webCompat', method: 'screenLock', params: { orientation: 'landscape' } }))
447+
})
448+
449+
it('should send message on unlock', async () => {
450+
await page.evaluate(() => {
451+
globalThis.cssMessaging.impl.request = (req) => {
452+
globalThis.lockReq = req
453+
return Promise.resolve({})
454+
}
455+
})
456+
457+
const { result, message } = await checkUnlock()
458+
expect(result).toBeUndefined()
459+
expect(message).toEqual(jasmine.objectContaining({ featureName: 'webCompat', method: 'screenUnlock' }))
460+
})
461+
})
462+
})
463+
303464
describe('Web Share API', () => {
304465
let browser
305466
let server

src/features/web-compat.js

+69
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ function windowSizingFix () {
1414

1515
const MSG_WEB_SHARE = 'webShare'
1616
const MSG_PERMISSIONS_QUERY = 'permissionsQuery'
17+
const MSG_SCREEN_LOCK = 'screenLock'
18+
const MSG_SCREEN_UNLOCK = 'screenUnlock'
1719

1820
function canShare (data) {
1921
if (typeof data !== 'object') return false
@@ -69,6 +71,9 @@ export default class WebCompat extends ContentFeature {
6971
/** @type {Promise<any> | null} */
7072
#activeShareRequest = null
7173

74+
/** @type {Promise<any> | null} */
75+
#activeScreenLockRequest = null
76+
7277
init () {
7378
if (this.getFeatureSettingEnabled('windowSizing')) {
7479
windowSizingFix()
@@ -108,6 +113,10 @@ export default class WebCompat extends ContentFeature {
108113
if (this.getFeatureSettingEnabled('viewportWidth')) {
109114
this.viewportWidthFix()
110115
}
116+
117+
if (this.getFeatureSettingEnabled('screenLock')) {
118+
this.screenLockFix()
119+
}
111120
}
112121

113122
/** Shim Web Share API in Android WebView */
@@ -283,6 +292,66 @@ export default class WebCompat extends ContentFeature {
283292
window.navigator.permissions = permissions
284293
}
285294

295+
/**
296+
* Fixes screen lock/unlock APIs for Android WebView.
297+
*/
298+
screenLockFix () {
299+
const validOrientations = [
300+
'any',
301+
'natural',
302+
'landscape',
303+
'portrait',
304+
'portrait-primary',
305+
'portrait-secondary',
306+
'landscape-primary',
307+
'landscape-secondary',
308+
'unsupported'
309+
]
310+
311+
this.wrapProperty(globalThis.ScreenOrientation.prototype, 'lock', {
312+
value: async (requestedOrientation) => {
313+
if (!requestedOrientation) {
314+
return Promise.reject(new TypeError("Failed to execute 'lock' on 'ScreenOrientation': 1 argument required, but only 0 present."))
315+
}
316+
if (!validOrientations.includes(requestedOrientation)) {
317+
return Promise.reject(new TypeError(`Failed to execute 'lock' on 'ScreenOrientation': The provided value '${requestedOrientation}' is not a valid enum value of type OrientationLockType.`))
318+
}
319+
if (this.#activeScreenLockRequest) {
320+
return Promise.reject(new DOMException('Screen lock already in progress', 'AbortError'))
321+
}
322+
323+
this.#activeScreenLockRequest = this.messaging.request(MSG_SCREEN_LOCK, { orientation: requestedOrientation })
324+
let resp
325+
try {
326+
resp = await this.#activeScreenLockRequest
327+
} catch (err) {
328+
throw new DOMException(err.message, 'DataError')
329+
} finally {
330+
this.#activeScreenLockRequest = null
331+
}
332+
333+
if (resp.failure) {
334+
switch (resp.failure.name) {
335+
case 'TypeError':
336+
return Promise.reject(new TypeError(resp.failure.message))
337+
case 'InvalidStateError':
338+
return Promise.reject(new DOMException(resp.failure.message, resp.failure.name))
339+
default:
340+
return Promise.reject(new DOMException(resp.failure.message, 'DataError'))
341+
}
342+
}
343+
344+
return Promise.resolve()
345+
}
346+
})
347+
348+
this.wrapProperty(globalThis.ScreenOrientation.prototype, 'unlock', {
349+
value: () => {
350+
this.messaging.request(MSG_SCREEN_UNLOCK, {})
351+
}
352+
})
353+
}
354+
286355
/**
287356
* Add missing navigator.credentials API
288357
*/

0 commit comments

Comments
 (0)