@fakes/media-devices
Version:
A interactive fake implementation of MediaDevices interface in the browser for testing
213 lines (200 loc) • 6.43 kB
text/typescript
export type MediaStreamCheckResult = { success: boolean; messages?: Array<string> }
type MediaStreamPredicate = (mediaStream: MediaStream) => MediaStreamCheckResult
type ErrorPredicate = (error: Error) => MediaStreamCheckResult
type MediaStreamPromisePredicate = (mediaStream: Promise<MediaStream>) => Promise<MediaStreamCheckResult>
const mediaStream: (input: MediaStreamPredicate) => MediaStreamPromisePredicate = (input: MediaStreamPredicate) => {
return async (promise: Promise<MediaStream>) => input(await promise)
}
const error: (input: ErrorPredicate) => MediaStreamPromisePredicate = (input: ErrorPredicate) => {
return async (promise: Promise<MediaStream>) => {
try {
await promise
return { success: false, messages: ['expected a rejected promise'] }
} catch (e) {
return input(e)
}
}
}
type MediaStreamCheck = {
what: string
predicate: MediaStreamPromisePredicate
}
interface Expected {
description: string
checks: MediaStreamCheck[]
}
type Matrix = Record<PermissionState, Expected | undefined>
export interface Scenario {
summary: string
description: string
constraints?: MediaStreamConstraints
expected: Matrix
}
export const passUndefined: Scenario = {
summary: 'undefined constraints',
description: 'pass undefined as constraints',
constraints: undefined,
expected: {
prompt: undefined,
denied: undefined,
granted: {
description: 'reject and communicate that at least one constrain has to be present',
checks: [
{
what: 'TypeError',
predicate: error((err) => {
const success = err instanceof TypeError
const messages = [`got: ${err.toString()}`]
return { success, messages }
}),
},
{
what: 'error message',
predicate: error((err) => {
const expected = `Failed to execute 'getUserMedia' on 'MediaDevices': At least one of audio and video must be requested`
const success = err.message === expected
const messages = [`expected: ${expected}`, `got: '${err.message}'`]
return { success, messages }
}),
},
],
},
},
}
export const requestedDeviceTypeNotAttached: Scenario = {
summary: 'requested device type not attached',
description: 'Requesting a camera but none is attached',
constraints: { video: true },
expected: {
prompt: undefined,
denied: undefined,
granted: {
description: 'reject and communicate that the requested device was not found',
checks: [
{
what: 'DOMException',
predicate: error((err) => {
const success = err instanceof DOMException
const messages = [`got: ${err.toString()}`]
return { success, messages }
}),
},
{
what: 'error message',
predicate: error((err) => {
const expected = `Requested device not found`
const success = err.message === expected
const messages = [`expected: ${expected}`, `got: '${err.message}'`]
return { success, messages }
}),
},
],
},
},
}
export const allConstraintsFalse: Scenario = {
summary: 'all constraints false',
description: 'pass false to the video and audio constraint',
constraints: {
audio: false,
video: false,
},
expected: {
prompt: undefined,
denied: undefined,
granted: {
description: 'reject and communicate that at least one constrain has to be set to true',
checks: [
{
what: 'TypeError',
predicate: error((err) => {
const success = err instanceof TypeError
const messages = [`got: ${err.toString()}`]
return { success, messages }
}),
},
{
what: 'error message',
predicate: error((err) => {
const expected = `Failed to execute 'getUserMedia' on 'MediaDevices': At least one of audio and video must be requested`
const success = err.message === expected
const messages = [`expected: ${expected}`, `got: '${err.message}'`]
return { success, messages }
}),
},
],
},
},
}
export const noDeviceWithDeviceId: Scenario = {
summary: 'bogus device id',
description: 'the constraint contains a deviceId that no device has',
constraints: { video: { deviceId: 'bogus' } },
expected: {
prompt: undefined,
denied: undefined,
granted: {
description: 'fallback to any other audio device',
checks: [
{
what: 'stream is active',
predicate: mediaStream((stream) => {
const success = stream.active
return { success }
}),
},
{
what: 'stream has an id',
predicate: mediaStream((stream) => {
const success = stream.id.length > 0
return { success }
}),
},
],
},
},
}
export const existingDevice: Scenario = {
summary: 'existing device',
description: 'any camera device without any other constraints',
constraints: { video: true },
expected: {
prompt: undefined,
denied: {
description: 'should be rejected',
checks: [
{
what: 'DOMException',
predicate: error((error) => {
const success = error instanceof DOMException
const messages = [`got: ${error.constructor.name}`]
return { success, messages }
}),
},
{
what: 'NotAllowedError',
predicate: error((error) => {
const actual = error.name
const success = actual === 'NotAllowedError'
const messages = [`got: ${actual}`]
return { success, messages }
}),
},
],
},
granted: {
description: 'tbd',
checks: [],
},
},
}
const collectScenarios = () => {
const result = new Map<string, Scenario>()
result.set(existingDevice.summary, existingDevice)
result.set(noDeviceWithDeviceId.summary, noDeviceWithDeviceId)
result.set(passUndefined.summary, passUndefined)
result.set(allConstraintsFalse.summary, allConstraintsFalse)
result.set(requestedDeviceTypeNotAttached.summary, requestedDeviceTypeNotAttached)
return result
}
export const scenarios = collectScenarios()