@flighthq/permissions 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/permission.d.ts +7 -0
- package/dist/permission.d.ts.map +1 -0
- package/dist/permission.js +190 -0
- package/dist/permission.js.map +1 -0
- package/package.json +37 -0
- package/src/permission.test.ts +200 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { PermissionBackend, PermissionName, PermissionState } from '@flighthq/types';
|
|
2
|
+
export declare function createWebPermissionBackend(): PermissionBackend;
|
|
3
|
+
export declare function getPermissionBackend(): PermissionBackend;
|
|
4
|
+
export declare function getPermissionState(name: PermissionName): Promise<PermissionState>;
|
|
5
|
+
export declare function requestPermission(name: PermissionName): Promise<PermissionState>;
|
|
6
|
+
export declare function setPermissionBackend(backend: PermissionBackend | null): void;
|
|
7
|
+
//# sourceMappingURL=permission.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permission.d.ts","sourceRoot":"","sources":["../src/permission.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAO1F,wBAAgB,0BAA0B,IAAI,iBAAiB,CAS9D;AAGD,wBAAgB,oBAAoB,IAAI,iBAAiB,CAGxD;AAID,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,eAAe,CAAC,CAEjF;AAKD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,eAAe,CAAC,CAEhF;AAGD,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,iBAAiB,GAAG,IAAI,GAAG,IAAI,CAE5E"}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// Builds the default web backend. Query goes through navigator.permissions.query where supported;
|
|
2
|
+
// request routes each name to its concrete web trigger (Notification.requestPermission, getUserMedia,
|
|
3
|
+
// a one-shot geolocation read, navigator.storage.persist). Created lazily by getPermissionBackend —
|
|
4
|
+
// nothing binds to the DOM at import time, so importing the package has no side effect. An absent
|
|
5
|
+
// API, an unqueryable name, or a denied request resolves to a three-state sentinel, never a throw.
|
|
6
|
+
export function createWebPermissionBackend() {
|
|
7
|
+
return {
|
|
8
|
+
getState(name) {
|
|
9
|
+
return readWebPermissionState(name);
|
|
10
|
+
},
|
|
11
|
+
request(name) {
|
|
12
|
+
return requestWebPermission(name);
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
// The active permission backend, lazily defaulting to the web backend. There is always a backend.
|
|
17
|
+
export function getPermissionBackend() {
|
|
18
|
+
if (_backend === null)
|
|
19
|
+
_backend = createWebPermissionBackend();
|
|
20
|
+
return _backend;
|
|
21
|
+
}
|
|
22
|
+
// Resolves the current state of a named permission without prompting. Returns 'granted', 'denied',
|
|
23
|
+
// or 'prompt' (not yet decided); 'prompt' is also the sentinel for an unqueryable name.
|
|
24
|
+
export function getPermissionState(name) {
|
|
25
|
+
return getPermissionBackend().getState(name);
|
|
26
|
+
}
|
|
27
|
+
// Requests a named permission, triggering the OS prompt where the platform supports it. Resolves to
|
|
28
|
+
// the resulting state; a name with no request path falls back to a plain state query, and a missing
|
|
29
|
+
// API resolves to a sentinel rather than throwing.
|
|
30
|
+
export function requestPermission(name) {
|
|
31
|
+
return getPermissionBackend().request(name);
|
|
32
|
+
}
|
|
33
|
+
// Installs a native host permission backend; pass null to fall back to a fresh lazy web default.
|
|
34
|
+
export function setPermissionBackend(backend) {
|
|
35
|
+
_backend = backend;
|
|
36
|
+
}
|
|
37
|
+
let _backend = null;
|
|
38
|
+
// Maps a PermissionName to its Permissions-API query descriptor name. The built-in names are already
|
|
39
|
+
// valid descriptor names, so this is identity for them; an unlisted name is queried under itself.
|
|
40
|
+
function getPermissionQueryDescriptorName(name) {
|
|
41
|
+
return _permissionQueryDescriptors[name] ?? name;
|
|
42
|
+
}
|
|
43
|
+
// The per-name fallback state when the Permissions API is absent or the name is unqueryable.
|
|
44
|
+
// notifications is still readable synchronously via Notification.permission ('default' → 'prompt');
|
|
45
|
+
// every other name resolves to the 'prompt' sentinel.
|
|
46
|
+
function readWebFallbackPermissionState(name) {
|
|
47
|
+
if (name === 'notifications') {
|
|
48
|
+
const permission = getWebNotificationPermission();
|
|
49
|
+
if (permission !== null)
|
|
50
|
+
return permission === 'default' ? 'prompt' : permission;
|
|
51
|
+
}
|
|
52
|
+
return 'prompt';
|
|
53
|
+
}
|
|
54
|
+
async function readWebPermissionState(name) {
|
|
55
|
+
const permissions = getWebPermissions();
|
|
56
|
+
if (permissions !== null) {
|
|
57
|
+
try {
|
|
58
|
+
const status = await permissions.query({
|
|
59
|
+
name: getPermissionQueryDescriptorName(name),
|
|
60
|
+
});
|
|
61
|
+
return status.state;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// Unqueryable name or a rejected query — fall through to the per-name fallback.
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return readWebFallbackPermissionState(name);
|
|
68
|
+
}
|
|
69
|
+
// Drives a one-shot geolocation read purely to observe grant/denial; the position itself is discarded.
|
|
70
|
+
function requestWebGeolocationPermission() {
|
|
71
|
+
return new Promise((resolve) => {
|
|
72
|
+
const geolocation = getWebGeolocation();
|
|
73
|
+
if (geolocation === null || typeof geolocation.getCurrentPosition !== 'function') {
|
|
74
|
+
resolve('prompt');
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
geolocation.getCurrentPosition(() => resolve('granted'), () => resolve('denied'));
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
resolve('prompt');
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
// Prompts for camera ('video') or microphone ('audio') via getUserMedia, then stops the granted
|
|
86
|
+
// tracks immediately — the prompt is the only thing wanted, not an open capture stream.
|
|
87
|
+
async function requestWebMediaPermission(kind) {
|
|
88
|
+
const mediaDevices = getWebMediaDevices();
|
|
89
|
+
if (mediaDevices === null || typeof mediaDevices.getUserMedia !== 'function')
|
|
90
|
+
return 'prompt';
|
|
91
|
+
try {
|
|
92
|
+
const stream = await mediaDevices.getUserMedia(kind === 'video' ? { video: true } : { audio: true });
|
|
93
|
+
stopMediaStreamTracks(stream);
|
|
94
|
+
return 'granted';
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return 'denied';
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async function requestWebNotificationPermission() {
|
|
101
|
+
const notification = getWebNotification();
|
|
102
|
+
if (notification === null || typeof notification.requestPermission !== 'function')
|
|
103
|
+
return 'prompt';
|
|
104
|
+
try {
|
|
105
|
+
const result = await notification.requestPermission();
|
|
106
|
+
return result === 'default' ? 'prompt' : result;
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return 'prompt';
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async function requestWebPersistentStoragePermission() {
|
|
113
|
+
const storage = getWebStorageManager();
|
|
114
|
+
if (storage === null || typeof storage.persist !== 'function')
|
|
115
|
+
return 'prompt';
|
|
116
|
+
try {
|
|
117
|
+
return (await storage.persist()) ? 'granted' : 'prompt';
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return 'prompt';
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async function requestWebPermission(name) {
|
|
124
|
+
const router = _permissionRequestRouters[name];
|
|
125
|
+
if (router !== undefined)
|
|
126
|
+
return await router();
|
|
127
|
+
return await readWebPermissionState(name);
|
|
128
|
+
}
|
|
129
|
+
// Stops every track of a granted media stream, releasing the device. Guards a missing getTracks so a
|
|
130
|
+
// stubbed or partial stream never throws.
|
|
131
|
+
function stopMediaStreamTracks(stream) {
|
|
132
|
+
if (typeof stream.getTracks !== 'function')
|
|
133
|
+
return;
|
|
134
|
+
for (const track of stream.getTracks()) {
|
|
135
|
+
if (typeof track.stop === 'function')
|
|
136
|
+
track.stop();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function getWebGeolocation() {
|
|
140
|
+
if (typeof navigator === 'undefined')
|
|
141
|
+
return null;
|
|
142
|
+
return navigator.geolocation ?? null;
|
|
143
|
+
}
|
|
144
|
+
function getWebMediaDevices() {
|
|
145
|
+
if (typeof navigator === 'undefined')
|
|
146
|
+
return null;
|
|
147
|
+
return navigator.mediaDevices ?? null;
|
|
148
|
+
}
|
|
149
|
+
function getWebNotification() {
|
|
150
|
+
return typeof Notification !== 'undefined' ? Notification : null;
|
|
151
|
+
}
|
|
152
|
+
function getWebNotificationPermission() {
|
|
153
|
+
const notification = getWebNotification();
|
|
154
|
+
if (notification === null)
|
|
155
|
+
return null;
|
|
156
|
+
return notification.permission ?? null;
|
|
157
|
+
}
|
|
158
|
+
function getWebPermissions() {
|
|
159
|
+
if (typeof navigator === 'undefined')
|
|
160
|
+
return null;
|
|
161
|
+
const permissions = navigator.permissions ?? null;
|
|
162
|
+
if (permissions === null || typeof permissions.query !== 'function')
|
|
163
|
+
return null;
|
|
164
|
+
return permissions;
|
|
165
|
+
}
|
|
166
|
+
function getWebStorageManager() {
|
|
167
|
+
if (typeof navigator === 'undefined')
|
|
168
|
+
return null;
|
|
169
|
+
return navigator.storage ?? null;
|
|
170
|
+
}
|
|
171
|
+
const _permissionQueryDescriptors = {
|
|
172
|
+
camera: 'camera',
|
|
173
|
+
microphone: 'microphone',
|
|
174
|
+
geolocation: 'geolocation',
|
|
175
|
+
notifications: 'notifications',
|
|
176
|
+
'clipboard-read': 'clipboard-read',
|
|
177
|
+
'clipboard-write': 'clipboard-write',
|
|
178
|
+
'persistent-storage': 'persistent-storage',
|
|
179
|
+
push: 'push',
|
|
180
|
+
midi: 'midi',
|
|
181
|
+
'screen-wake-lock': 'screen-wake-lock',
|
|
182
|
+
};
|
|
183
|
+
const _permissionRequestRouters = {
|
|
184
|
+
camera: () => requestWebMediaPermission('video'),
|
|
185
|
+
microphone: () => requestWebMediaPermission('audio'),
|
|
186
|
+
geolocation: requestWebGeolocationPermission,
|
|
187
|
+
notifications: requestWebNotificationPermission,
|
|
188
|
+
'persistent-storage': requestWebPersistentStoragePermission,
|
|
189
|
+
};
|
|
190
|
+
//# sourceMappingURL=permission.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permission.js","sourceRoot":"","sources":["../src/permission.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flighthq/permissions",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"src/**/*.test.ts",
|
|
16
|
+
"!dist/**/*.test.js",
|
|
17
|
+
"!dist/**/*.test.d.ts",
|
|
18
|
+
"!dist/**/*.test.js.map",
|
|
19
|
+
"!dist/**/*.test.d.ts.map"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc -b",
|
|
23
|
+
"clean": "tsc -b --clean",
|
|
24
|
+
"test": "vitest run --config vitest.config.ts",
|
|
25
|
+
"test:watch": "vitest --watch --config vitest.config.ts",
|
|
26
|
+
"prepack": "npm run clean && npm run clean:dist && npm run build",
|
|
27
|
+
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@flighthq/types": "0.1.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"typescript": "^5.3.0"
|
|
34
|
+
},
|
|
35
|
+
"description": "OS-permission query/request over a swappable web/native backend (Permissions API + per-permission web triggers by default)",
|
|
36
|
+
"sideEffects": false
|
|
37
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import type { PermissionBackend, PermissionName, PermissionState } from '@flighthq/types';
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
createWebPermissionBackend,
|
|
6
|
+
getPermissionBackend,
|
|
7
|
+
getPermissionState,
|
|
8
|
+
requestPermission,
|
|
9
|
+
setPermissionBackend,
|
|
10
|
+
} from './permission';
|
|
11
|
+
|
|
12
|
+
// A recording mock backend: relays a fixed state and captures the last name each method saw.
|
|
13
|
+
function fakeBackend(
|
|
14
|
+
state: PermissionState,
|
|
15
|
+
): PermissionBackend & { lastGetState: PermissionName | null; lastRequest: PermissionName | null } {
|
|
16
|
+
const backend = {
|
|
17
|
+
lastGetState: null as PermissionName | null,
|
|
18
|
+
lastRequest: null as PermissionName | null,
|
|
19
|
+
async getState(name: PermissionName) {
|
|
20
|
+
backend.lastGetState = name;
|
|
21
|
+
return state;
|
|
22
|
+
},
|
|
23
|
+
async request(name: PermissionName) {
|
|
24
|
+
backend.lastRequest = name;
|
|
25
|
+
return state;
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
return backend;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function fakeStream(trackCount = 1): { stream: MediaStream; stopped: () => number } {
|
|
32
|
+
let stopped = 0;
|
|
33
|
+
const tracks = Array.from({ length: trackCount }, () => ({ stop: () => (stopped += 1) }));
|
|
34
|
+
const stream = { getTracks: () => tracks } as unknown as MediaStream;
|
|
35
|
+
return { stream, stopped: () => stopped };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
setPermissionBackend(null);
|
|
40
|
+
vi.unstubAllGlobals();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('createWebPermissionBackend', () => {
|
|
44
|
+
it('returns a backend with getState and request methods', () => {
|
|
45
|
+
const backend = createWebPermissionBackend();
|
|
46
|
+
expect(typeof backend.getState).toBe('function');
|
|
47
|
+
expect(typeof backend.request).toBe('function');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('getState resolves the queried Permissions API state', async () => {
|
|
51
|
+
const query = vi.fn(async () => ({ state: 'granted' }));
|
|
52
|
+
vi.stubGlobal('navigator', { permissions: { query } });
|
|
53
|
+
const backend = createWebPermissionBackend();
|
|
54
|
+
expect(await backend.getState('camera')).toBe('granted');
|
|
55
|
+
expect(query).toHaveBeenCalledWith({ name: 'camera' });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('getState maps clipboard names to their clipboard descriptor', async () => {
|
|
59
|
+
const query = vi.fn(async () => ({ state: 'prompt' }));
|
|
60
|
+
vi.stubGlobal('navigator', { permissions: { query } });
|
|
61
|
+
await createWebPermissionBackend().getState('clipboard-read');
|
|
62
|
+
expect(query).toHaveBeenCalledWith({ name: 'clipboard-read' });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('getState falls back to Notification.permission when the Permissions API is absent', async () => {
|
|
66
|
+
vi.stubGlobal('navigator', {});
|
|
67
|
+
vi.stubGlobal('Notification', { permission: 'granted' });
|
|
68
|
+
expect(await createWebPermissionBackend().getState('notifications')).toBe('granted');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('getState maps a default Notification permission to the prompt sentinel', async () => {
|
|
72
|
+
vi.stubGlobal('navigator', {});
|
|
73
|
+
vi.stubGlobal('Notification', { permission: 'default' });
|
|
74
|
+
expect(await createWebPermissionBackend().getState('notifications')).toBe('prompt');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('getState returns the prompt sentinel for a non-notification name with no queryable state', async () => {
|
|
78
|
+
vi.stubGlobal('navigator', {});
|
|
79
|
+
expect(await createWebPermissionBackend().getState('geolocation')).toBe('prompt');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('getState falls back when the query rejects', async () => {
|
|
83
|
+
const query = vi.fn(async () => {
|
|
84
|
+
throw new TypeError('unsupported');
|
|
85
|
+
});
|
|
86
|
+
vi.stubGlobal('navigator', { permissions: { query } });
|
|
87
|
+
vi.stubGlobal('Notification', { permission: 'denied' });
|
|
88
|
+
expect(await createWebPermissionBackend().getState('notifications')).toBe('denied');
|
|
89
|
+
expect(await createWebPermissionBackend().getState('midi')).toBe('prompt');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('request(notifications) maps a granted result through', async () => {
|
|
93
|
+
vi.stubGlobal('Notification', { requestPermission: async () => 'granted' });
|
|
94
|
+
expect(await createWebPermissionBackend().request('notifications')).toBe('granted');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('request(notifications) maps a default result to the prompt sentinel', async () => {
|
|
98
|
+
vi.stubGlobal('Notification', { requestPermission: async () => 'default' });
|
|
99
|
+
expect(await createWebPermissionBackend().request('notifications')).toBe('prompt');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('request(camera) grants and stops the granted tracks immediately', async () => {
|
|
103
|
+
const { stream, stopped } = fakeStream(2);
|
|
104
|
+
const getUserMedia = vi.fn(async () => stream);
|
|
105
|
+
vi.stubGlobal('navigator', { mediaDevices: { getUserMedia } });
|
|
106
|
+
expect(await createWebPermissionBackend().request('camera')).toBe('granted');
|
|
107
|
+
expect(getUserMedia).toHaveBeenCalledWith({ video: true });
|
|
108
|
+
expect(stopped()).toBe(2);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('request(microphone) requests an audio stream', async () => {
|
|
112
|
+
const { stream } = fakeStream();
|
|
113
|
+
const getUserMedia = vi.fn(async () => stream);
|
|
114
|
+
vi.stubGlobal('navigator', { mediaDevices: { getUserMedia } });
|
|
115
|
+
expect(await createWebPermissionBackend().request('microphone')).toBe('granted');
|
|
116
|
+
expect(getUserMedia).toHaveBeenCalledWith({ audio: true });
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('request(camera) denies when getUserMedia rejects', async () => {
|
|
120
|
+
const getUserMedia = vi.fn(async () => {
|
|
121
|
+
throw new DOMException('denied', 'NotAllowedError');
|
|
122
|
+
});
|
|
123
|
+
vi.stubGlobal('navigator', { mediaDevices: { getUserMedia } });
|
|
124
|
+
expect(await createWebPermissionBackend().request('camera')).toBe('denied');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('request(camera) returns the prompt sentinel with no throw when the API is absent', async () => {
|
|
128
|
+
vi.stubGlobal('navigator', {});
|
|
129
|
+
expect(await createWebPermissionBackend().request('camera')).toBe('prompt');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('request(geolocation) grants on a successful one-shot position read', async () => {
|
|
133
|
+
const getCurrentPosition = (success: () => void) => success();
|
|
134
|
+
vi.stubGlobal('navigator', { geolocation: { getCurrentPosition } });
|
|
135
|
+
expect(await createWebPermissionBackend().request('geolocation')).toBe('granted');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('request(geolocation) denies when the position read errors', async () => {
|
|
139
|
+
const getCurrentPosition = (_success: () => void, error: () => void) => error();
|
|
140
|
+
vi.stubGlobal('navigator', { geolocation: { getCurrentPosition } });
|
|
141
|
+
expect(await createWebPermissionBackend().request('geolocation')).toBe('denied');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('request(persistent-storage) grants when persist resolves true, prompts when false', async () => {
|
|
145
|
+
vi.stubGlobal('navigator', { storage: { persist: async () => true } });
|
|
146
|
+
expect(await createWebPermissionBackend().request('persistent-storage')).toBe('granted');
|
|
147
|
+
vi.stubGlobal('navigator', { storage: { persist: async () => false } });
|
|
148
|
+
expect(await createWebPermissionBackend().request('persistent-storage')).toBe('prompt');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('request falls back to a state query for a name with no request path', async () => {
|
|
152
|
+
const query = vi.fn(async () => ({ state: 'granted' }));
|
|
153
|
+
vi.stubGlobal('navigator', { permissions: { query } });
|
|
154
|
+
expect(await createWebPermissionBackend().request('midi')).toBe('granted');
|
|
155
|
+
expect(query).toHaveBeenCalledWith({ name: 'midi' });
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe('getPermissionBackend', () => {
|
|
160
|
+
it('returns a lazily-created web default', () => {
|
|
161
|
+
expect(getPermissionBackend()).not.toBeNull();
|
|
162
|
+
expect(getPermissionBackend()).toBe(getPermissionBackend());
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('returns an installed backend', () => {
|
|
166
|
+
const backend = fakeBackend('granted');
|
|
167
|
+
setPermissionBackend(backend);
|
|
168
|
+
expect(getPermissionBackend()).toBe(backend);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe('getPermissionState', () => {
|
|
173
|
+
it('dispatches to the active backend and relays the resolved state', async () => {
|
|
174
|
+
const backend = fakeBackend('denied');
|
|
175
|
+
setPermissionBackend(backend);
|
|
176
|
+
expect(await getPermissionState('camera')).toBe('denied');
|
|
177
|
+
expect(backend.lastGetState).toBe('camera');
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe('requestPermission', () => {
|
|
182
|
+
it('dispatches to the active backend and relays the resolved state', async () => {
|
|
183
|
+
const backend = fakeBackend('granted');
|
|
184
|
+
setPermissionBackend(backend);
|
|
185
|
+
expect(await requestPermission('microphone')).toBe('granted');
|
|
186
|
+
expect(backend.lastRequest).toBe('microphone');
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe('setPermissionBackend', () => {
|
|
191
|
+
it('installs a backend and restores a fresh distinct web default on null', () => {
|
|
192
|
+
const backend = fakeBackend('prompt');
|
|
193
|
+
setPermissionBackend(backend);
|
|
194
|
+
expect(getPermissionBackend()).toBe(backend);
|
|
195
|
+
setPermissionBackend(null);
|
|
196
|
+
const restored = getPermissionBackend();
|
|
197
|
+
expect(restored).not.toBe(backend);
|
|
198
|
+
expect(restored.getState).toBeInstanceOf(Function);
|
|
199
|
+
});
|
|
200
|
+
});
|