Skip to content

Commit

Permalink
test: add change handler tests and simplify docs
Browse files Browse the repository at this point in the history
  • Loading branch information
43081j committed Aug 3, 2024
1 parent 9850aec commit e2936df
Show file tree
Hide file tree
Showing 3 changed files with 51 additions and 29 deletions.
28 changes: 5 additions & 23 deletions docs/permissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 4 additions & 4 deletions src/controllers/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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 {
Expand Down Expand Up @@ -42,7 +42,7 @@ export class PermissionsController {
protected async __initialisePermissions(name: PermissionName): Promise<void> {
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();
}

Expand Down
44 changes: 42 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(({name}) => {

Check failure on line 29 in src/test/controllers/permissions_test.ts

View workflow job for this annotation

GitHub Actions / test (18.x)

'name' is declared but its value is never read.

Check failure on line 29 in src/test/controllers/permissions_test.ts

View workflow job for this annotation

GitHub Actions / test (19.x)

'name' is declared but its value is never read.
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,6 +41,7 @@ suite('PermissionsController', () => {

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

test('initialises to pending', () => {
Expand All @@ -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');
});
});

0 comments on commit e2936df

Please sign in to comment.