diff --git a/docs/permissions.md b/docs/permissions.md index b1bc5fb..8cdca1e 100644 --- a/docs/permissions.md +++ b/docs/permissions.md @@ -26,30 +26,12 @@ class MyElement extends LitElement { The required argument is a [permission name](https://developer.mozilla.org/en-US/docs/Web/API/Permissions/query#name) which varies across browsers in some cases. -There is a brief moment until the controller status is resolved to either `prompt`, `granted` or `denied`. -To intercept this brief undefined state, a new state has been introduced. -The `AsyncPermissionState` type extends `PermissionState` by `pending` as the initial state of the controller. +The controller will expose a `state` property which is either a valid +[PermissionState](https://developer.mozilla.org/en-US/docs/Web/API/PermissionStatus/state) +or the string `pending`. -```ts -class MyElement extends LitElement { - constructor() { - super(); - - this._permissionsCtrl = new PermissionsController(this, 'geolocation'); - } - - render() { - const {state} = this._permissionsCtrl; - - return html` - ${ - 'geolocation' in navigator && state !== 'pending' - ? html`Geolocation permission is ${state}` - : null - } - `; -} -``` +Initially, while querying the browser for a state, the state will be set to +`pending`. ## Options diff --git a/src/controllers/permissions.ts b/src/controllers/permissions.ts index 3e211e6..43ece2a 100644 --- a/src/controllers/permissions.ts +++ b/src/controllers/permissions.ts @@ -2,8 +2,8 @@ import type {ReactiveController, ReactiveControllerHost} from 'lit'; /** * The permission state can be either 'denied', 'granted' or 'prompt'. - * There is a brief moment until the state is resolved as the query for this state is asynchronous. - * To represent this initial state, 'pending' is added to PermissionState. + * While querying the browser for the underlying state, the async permission + * state will be set to `pending`. */ export type AsyncPermissionState = 'pending' | PermissionState; @@ -12,7 +12,7 @@ export type AsyncPermissionState = 'pending' | PermissionState; */ export class PermissionsController { /** - * Gets the current permission state or 'pending' + * Gets the current async permission state * @return {AsyncPermissionState} */ public get state(): AsyncPermissionState { @@ -42,7 +42,7 @@ export class PermissionsController { protected async __initialisePermissions(name: PermissionName): Promise { this.__status = await navigator.permissions.query({name}); this.__status.addEventListener('change', this.__onPermissionChanged); - // Implicitly request for an update to reflect the initial state + // Request an update to reflect the initial state this.__host.requestUpdate(); } diff --git a/src/test/controllers/permissions_test.ts b/src/test/controllers/permissions_test.ts index eca8a66..8f2000c 100644 --- a/src/test/controllers/permissions_test.ts +++ b/src/test/controllers/permissions_test.ts @@ -2,14 +2,35 @@ import '../util.js'; import {html, ReactiveController} from 'lit'; import * as assert from 'uvu/assert'; +import * as hanbi from 'hanbi'; import {PermissionsController} from '../../main.js'; import type {TestElement} from '../util.js'; suite('PermissionsController', () => { let element: TestElement; let controller: PermissionsController; + let permissionStub: hanbi.Stub; + let eventSpy: hanbi.Stub<(name: string, handler: unknown) => void>; + let mockStatus: PermissionStatus; + let mockState: PermissionState; + let permissionResolver: (state: PermissionStatus) => void; setup(async () => { + eventSpy = hanbi.spy(); + mockState = 'prompt'; + mockStatus = { + name: 'geolocation', + addEventListener: eventSpy.handler, + get state() { + return mockState; + } + } as PermissionStatus; + permissionStub = hanbi.stubMethod(navigator.permissions, 'query'); + permissionStub.callsFake(({name}) => { + return new Promise((res) => { + permissionResolver = res; + }); + }); element = document.createElement('test-element') as TestElement; controller = new PermissionsController(element, 'geolocation'); element.controllers.push(controller as ReactiveController); @@ -20,6 +41,7 @@ suite('PermissionsController', () => { teardown(() => { element.remove(); + hanbi.restore(); }); test('initialises to pending', () => { @@ -28,9 +50,27 @@ suite('PermissionsController', () => { }); test('changes from pending to prompt', async () => { - await new Promise((resolve) => setTimeout(resolve, 0)); - + permissionResolver(mockStatus); + // TODO (43081j): be sure why two renders happen here + await element.updateComplete; + await element.updateComplete; assert.equal(controller.state, 'prompt'); assert.equal(element.shadowRoot!.textContent, 'prompt'); }); + + test('observes permission changes', async () => { + permissionResolver(mockStatus); + await element.updateComplete; + + const changeHandler = [...eventSpy.calls].find( + (c) => c.args[0] === 'change' + )!.args[1]; + + mockState = 'granted'; + (changeHandler as () => void)(); + + await element.updateComplete; + assert.equal(controller.state, 'granted'); + assert.equal(element.shadowRoot!.textContent, 'granted'); + }); });