Skip to content

Commit

Permalink
fix: reflect the the true initial state of a permission (#33)
Browse files Browse the repository at this point in the history
Permissions now have a `pending` state while waiting for the initial query to succeed
  • Loading branch information
TonySpegel authored Aug 3, 2024
1 parent fd1a039 commit 0c9cdd1
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 6 deletions.
7 changes: 7 additions & 0 deletions docs/permissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ 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.

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`.

Initially, while querying the browser for a state, the state will be set to
`pending`.

## Options

N/A
17 changes: 13 additions & 4 deletions src/controllers/permissions.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
import type {ReactiveController, ReactiveControllerHost} from 'lit';

/**
* The permission state can be either 'denied', 'granted' or 'prompt'.
* While querying the browser for the underlying state, the async permission
* state will be set to `pending`.
*/
export type AsyncPermissionState = 'pending' | PermissionState;

/**
* Tracks the status of a given browser permission
*/
export class PermissionsController {
/**
* Gets the current permission state
* @return {PermissionState}
* Gets the current async permission state
* @return {AsyncPermissionState}
*/
public get state(): PermissionState {
return this.__status?.state ?? 'prompt';
public get state(): AsyncPermissionState {
return this.__status?.state ?? 'pending';
}

private __host: ReactiveControllerHost;
Expand All @@ -35,6 +42,8 @@ export class PermissionsController {
protected async __initialisePermissions(name: PermissionName): Promise<void> {
this.__status = await navigator.permissions.query({name});
this.__status.addEventListener('change', this.__onPermissionChanged);
// Request an update to reflect the initial state
this.__host.requestUpdate();
}

protected __onPermissionChanged: () => void = (): void => {
Expand Down
49 changes: 47 additions & 2 deletions src/test/controllers/permissions_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof navigator.permissions.query>;
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(() => {
return new Promise<PermissionStatus>((res) => {
permissionResolver = res;
});
});
element = document.createElement('test-element') as TestElement;
controller = new PermissionsController(element, 'geolocation');
element.controllers.push(controller as ReactiveController);
Expand All @@ -20,12 +41,36 @@ suite('PermissionsController', () => {

teardown(() => {
element.remove();
hanbi.restore();
});

test('initialises to prompt', () => {
test('initialises to pending', () => {
assert.equal(controller.state, 'pending');
assert.equal(element.shadowRoot!.textContent, 'pending');
});

test('changes from pending to prompt', async () => {
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 changes to permission');
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');
});
});

0 comments on commit 0c9cdd1

Please sign in to comment.