UNPKG

@fakes/media-devices

Version:

A interactive fake implementation of MediaDevices interface in the browser for testing

189 lines (167 loc) 6.55 kB
import { Context } from './context' import { Deferred } from './Deferred' import { PermissionStatusFake } from './permissions/PermissionStatusFake' export enum RequestedMediaInput { Microphone = 'Microphone', Camera = 'Camera', } export type PermissionPromptAction = 'dismiss' | 'allow' | 'block' export interface PermissionPrompt { requestedPermissions(): RequestedMediaInput[] takeAction(action: PermissionPromptAction): void } export interface PermissionRequest { deviceKind: MediaDeviceKind granted: () => void blocked: () => void } export type UserConsent = { camera: PermissionState microphone: PermissionState } const resultingPermissionStateFor = (context: Context, action: PermissionPromptAction): PermissionState => { if (action === 'allow') { return 'granted' } if (action === 'block') { return 'denied' } context.notImplemented.call(`resultingPermissionStateFor() action: ${action}`) } export class UserConsentTracker { private readonly _trackedPermissionStatus: Record<keyof UserConsent, PermissionStatusFake[]> = { camera: [], microphone: [], } private _pendingPermissionRequest: void | PermissionRequest = undefined constructor(private readonly _context: Context, private readonly _userConsent: UserConsent) {} permissionStatusFor(kind: keyof UserConsent) { const permissionState = this.permissionStateFor(kind) const permissionStatus = new PermissionStatusFake(permissionState) this._trackedPermissionStatus[kind].push(permissionStatus) return permissionStatus } setPermissionFor(kind: keyof UserConsent, state: PermissionState) { this._userConsent[kind] = state this._trackedPermissionStatus[kind].forEach((permissionStatus) => permissionStatus.updateTo(state)) } requestPermissionFor(permissionRequest: PermissionRequest) { if (this._pendingPermissionRequest) { this._context.notImplemented.call('There is already a pending permission request, not sure if this can happen') } if (this.permissionGrantedFor(permissionRequest.deviceKind)) { permissionRequest.granted() return } if (this.permissionBlockedFor(permissionRequest.deviceKind)) { permissionRequest.blocked() return } this._pendingPermissionRequest = permissionRequest } private permissionStateFor(kind: 'camera' | 'microphone') { return this._userConsent[kind] } private permissionGrantedFor(deviceKind: MediaDeviceKind) { if (deviceKind === 'videoinput') { return this._userConsent.camera === 'granted' } if (deviceKind === 'audioinput') { return this._userConsent.microphone === 'granted' } this._context.notImplemented.call(`permissionGrantedFor '${deviceKind}'`) } private permissionBlockedFor(deviceKind: MediaDeviceKind) { if (deviceKind === 'videoinput') { return this._userConsent.camera === 'denied' } if (deviceKind === 'audioinput') { return this._userConsent.microphone === 'denied' } this._context.notImplemented.call(`permissionGrantedFor '${deviceKind}'`) } //todo add an override for the wait time and poll interval async deviceAccessPrompt(): Promise<PermissionPrompt> { const deferred = new Deferred<PermissionPrompt>() const maximumWaitTime = 1000 const pollInterval = 100 let timeWaited = 0 let pollForPendingPermissionRequest = () => { if (this._pendingPermissionRequest) { const complete = (action: PermissionPromptAction): void => { if (this._pendingPermissionRequest === undefined) { throw new Error('there is no pending permission request') } const updatedPermission = resultingPermissionStateFor(this._context, action) if (this._pendingPermissionRequest.deviceKind === 'audioinput') { this._userConsent.microphone = updatedPermission this._trackedPermissionStatus.microphone.forEach((fake) => fake.updateTo(updatedPermission)) } if (this._pendingPermissionRequest.deviceKind === 'videoinput') { this._userConsent.camera = updatedPermission this._trackedPermissionStatus.camera.forEach((fake) => fake.updateTo(updatedPermission)) } this._pendingPermissionRequest = undefined } const value = this.permissionPromptFor(this._context, this._pendingPermissionRequest, complete) deferred.resolve(value) return } if (timeWaited >= maximumWaitTime) { deferred.reject( new Error(`After waiting for ${maximumWaitTime} ms there still is no pending permission request`), ) return } timeWaited += pollInterval // TODO add scheduler abstraction to encapsulate window access window.setTimeout(pollForPendingPermissionRequest, pollInterval) } pollForPendingPermissionRequest() // check with the Permission Manager if permissions where already rejected // check if there was a request for media return deferred.promise } private permissionPromptFor( context: Context, permissionRequest: PermissionRequest, complete: (action: PermissionPromptAction) => void, ) { const requestedPermissions: RequestedMediaInput[] = [] if (permissionRequest.deviceKind === 'videoinput' && !this.permissionGrantedFor('videoinput')) { requestedPermissions.push(RequestedMediaInput.Camera) } if (permissionRequest.deviceKind === 'audioinput' && !this.permissionGrantedFor('audioinput')) { requestedPermissions.push(RequestedMediaInput.Microphone) } return new (class implements PermissionPrompt { requestedPermissions(): RequestedMediaInput[] { return requestedPermissions } takeAction(action: PermissionPromptAction): void { complete(action) if (action === 'allow') { permissionRequest.granted() return } if (action === 'block') { permissionRequest.blocked() return } context.notImplemented.call(`takeAction '${action}'`) } })() } accessAllowedFor(kind: MediaDeviceKind): boolean { if (kind === 'audioinput') { return this._userConsent.microphone === 'granted' } if (kind === 'videoinput') { return this._userConsent.camera === 'granted' } if (kind === 'audiooutput') { return this._userConsent.microphone === 'granted' } this._context.notImplemented.call(`not sure how to implement this for ${kind}`) } }