Skip to content

Commit

Permalink
feat(useMediaDevices): Adding the function useMediaDevices
Browse files Browse the repository at this point in the history
  • Loading branch information
microcipcip committed Feb 22, 2020
1 parent 9185917 commit 1717c6a
Show file tree
Hide file tree
Showing 8 changed files with 315 additions and 0 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ Vue.use(VueCompositionAPI);
- [`useMedia`](./src/components/useMedia/stories/useMedia.md) — tracks state of a CSS media query.
[![Demo](https://img.shields.io/badge/demo-馃殌-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/sensors-usemedia--demo)
[![Demo](https://img.shields.io/badge/advanced_demo-馃殌-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/sensors-usemedia--advanced-demo)
- [`useMediaDevices`](./src/components/useMediaDevices/stories/useMediaDevices.md) — tracks connected hardware devices.
[![Demo](https://img.shields.io/badge/demo-馃殌-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/sensors-usemediadevices--demo)
- [`useMouse`](./src/components/useMouse/stories/useMouse.md) — tracks the mouse position.
[![Demo](https://img.shields.io/badge/demo-馃殌-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/sensors-usemouse--demo)
[![Demo](https://img.shields.io/badge/advanced_demo-馃殌-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/sensors-usemouse--advanced-demo)
Expand Down
1 change: 1 addition & 0 deletions src/components/useMediaDevices/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useMediaDevices'
51 changes: 51 additions & 0 deletions src/components/useMediaDevices/stories/UseMediaDevicesDemo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<template>
<table class="table is-fullwidth">
<thead>
<tr>
<th>Prop</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>devicesState</td>
<td>
<pre>{{ JSON.stringify(devicesState, null, 2) }}</pre>
</td>
</tr>
<tr>
<td>isTracked</td>
<td>{{ isTracked }}</td>
</tr>
<tr>
<td colspan="2">
<button class="button is-primary" @click="start" v-if="!isTracking">
Start tracking media devices
</button>
<button class="button is-danger" @click="stop" v-else>
Stop tracking media devices
</button>
</td>
</tr>
</tbody>
</table>
</template>

<script lang="ts">
import Vue from 'vue'
import { useMediaDevices } from '@src/vue-use-kit'
export default Vue.extend({
name: 'UseMediaDevicesDemo',
setup() {
const {
devicesState,
isTracking,
isTracked,
start,
stop
} = useMediaDevices(false)
return { devicesState, isTracking, isTracked, start, stop }
}
})
</script>
71 changes: 71 additions & 0 deletions src/components/useMediaDevices/stories/useMediaDevices.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# useMediaDevices

Vue function that tracks connected hardware devices.

## Reference

```typescript
interface UseMediaDevicesState {
deviceId: string
groupId: string
kind: string
label: string
}

function useMediaDevices(
runOnMount?: boolean
): {
devicesState: Ref<UseMediaDevicesState[]>
isTracking: Ref<boolean>
isTracked: Ref<boolean>
start: () => void
stop: () => void
}
```

### Parameters

- `runOnMount: boolean` whether to run the connected media devices tracking on mount, `true` by default

### Returns

- `devicesState: Ref<UseMediaDevicesState[]>` the list of connected media devices
- `isTracking: Ref<boolean>` whether the function is tracking the connected media devices or not
- `isTracked: Ref<boolean>` whether the connected devices have been successfully tracked
- `start: Function` the function used to start tracking the connected media devices
- `stop: Function` the function used to stop tracking the connected media devices

## Usage

```html
<template>
<div>
<div>
devicesState:
<pre>{{ JSON.stringify(devicesState, null, 2) }}</pre>
</div>
<div>
isTracked: {{ isTracked }}
</div>
<div>
<button @click="start" v-if="!isTracking">
Start tracking media devices
</button>
<button @click="stop" v-else>Stop tracking media devices</button>
</div>
</div>
</template>

<script lang="ts">
import Vue from 'vue'
import { useMediaDevices } from 'vue-use-kit'
export default Vue.extend({
name: 'UseMediaDevicesDemo',
setup() {
const { devicesState, isTracked, start, stop } = useMediaDevices(false)
return { devicesState, isTracked, start, stop }
}
})
</script>
```
28 changes: 28 additions & 0 deletions src/components/useMediaDevices/stories/useMediaDevices.story.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { storiesOf } from '@storybook/vue'
import path from 'path'
import StoryTitle from '@src/helpers/StoryTitle.vue'
import UseMediaDevicesDemo from './UseMediaDevicesDemo.vue'

const functionName = 'useMediaDevices'
const functionPath = path.resolve(__dirname, '..')
const notes = require(`./${functionName}.md`).default

const basicDemo = () => ({
components: { StoryTitle, demo: UseMediaDevicesDemo },
template: `
<div class="container">
<story-title
function-path="${functionPath}"
source-name="${functionName}"
demo-name="UseMediaDevicesDemo.vue"
>
<template v-slot:title></template>
<template v-slot:intro></template>
</story-title>
<demo />
</div>`
})

storiesOf('sensors|useMediaDevices', module)
.addParameters({ notes })
.add('Demo', basicDemo)
101 changes: 101 additions & 0 deletions src/components/useMediaDevices/useMediaDevices.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { mount } from '@src/helpers/test'
import { useMediaDevices } from '@src/vue-use-kit'

const mediaDeviceInfo = {
deviceId: 'string',
groupId: 'string',
kind: 'string',
label: 'string'
}
const mediaDevices = [mediaDeviceInfo, mediaDeviceInfo]
let enumerateDevices: any
beforeEach(() => {
enumerateDevices = () => Promise.resolve(mediaDevices)
;(navigator as any).mediaDevices = {
enumerateDevices,
addEventListener: jest.fn(),
removeEventListener: jest.fn()
}
})

afterEach(() => {
jest.clearAllMocks()
})

const testComponent = (onMount = true) => ({
template: `
<div>
<div id="isTracking" v-if="isTracking"></div>
<div id="devicesState">{{JSON.stringify(devicesState)}}</div>
<button id="start" @click="start"></button>
<button id="stop" @click="stop"></button>
</div>
`,
setup() {
const { devicesState, isTracking, start, stop } = useMediaDevices(onMount)
return { devicesState, isTracking, start, stop }
}
})

describe('useMediaDevices', () => {
const event = 'devicechange'
it('should call devicechange onMounted', async () => {
const addEventListenerSpy = jest.spyOn(
navigator.mediaDevices,
'addEventListener'
)
const removeEventListenerSpy = jest.spyOn(
navigator.mediaDevices,
'removeEventListener'
)
const wrapper = mount(testComponent())
await wrapper.vm.$nextTick()
expect(addEventListenerSpy).toHaveBeenCalledTimes(1)
expect(addEventListenerSpy).toBeCalledWith(event, expect.any(Function))

// Destroy instance to check if the remove event listener is being called
wrapper.destroy()
expect(removeEventListenerSpy).toHaveBeenCalledTimes(1)
expect(removeEventListenerSpy).toBeCalledWith(event, expect.any(Function))
})

it('should call document.addEventListener again when start is called', async () => {
const addEventListenerSpy = jest.spyOn(
navigator.mediaDevices,
'addEventListener'
)
const wrapper = mount(testComponent())
expect(addEventListenerSpy).toHaveBeenCalledTimes(1)
wrapper.find('#stop').trigger('click')

// Wait for Vue to append #start in the DOM
await wrapper.vm.$nextTick()
wrapper.find('#start').trigger('click')
expect(addEventListenerSpy).toHaveBeenCalledTimes(1 * 2)
})

it('should call document.removeEventListener when stop is called', async () => {
const removeEventListenerSpy = jest.spyOn(
navigator.mediaDevices,
'removeEventListener'
)
const wrapper = mount(testComponent())
wrapper.find('#stop').trigger('click')

// Wait for Vue to append #start in the DOM
await wrapper.vm.$nextTick()
expect(removeEventListenerSpy).toHaveBeenCalledTimes(1)
})

it('should show #isTracking when onMount is true', async () => {
const wrapper = mount(testComponent(true))
await wrapper.vm.$nextTick()
expect(wrapper.find('#isTracking').exists()).toBe(true)
})

it('should not show #isTracking when onMount is false', async () => {
const wrapper = mount(testComponent(false))
await wrapper.vm.$nextTick()
expect(wrapper.find('#isTracking').exists()).toBe(false)
})
})
60 changes: 60 additions & 0 deletions src/components/useMediaDevices/useMediaDevices.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { ref, onMounted, onUnmounted, Ref } from '@vue/composition-api'

export interface UseMediaDevicesState {
deviceId: string
groupId: string
kind: string
label: string
}

export function useMediaDevices(runOnMount = true) {
const devicesState: Ref<UseMediaDevicesState[]> = ref([])
const isTracking = ref(false)
const isTracked = ref(false)

const deviceMap = ({
deviceId,
groupId,
kind,
label
}: UseMediaDevicesState) => ({
deviceId,
groupId,
kind,
label
})

const handleDeviceChange = () => {
navigator.mediaDevices
.enumerateDevices()
.then(deviceList => {
if (!isTracking.value) return
isTracked.value = true
devicesState.value = deviceList.map(deviceMap)
})
.catch(() => {
isTracked.value = false
})
}

const start = () => {
if (isTracking.value) return
handleDeviceChange()
navigator.mediaDevices.addEventListener('devicechange', handleDeviceChange)
isTracking.value = true
}

const stop = () => {
if (!isTracking.value) return
navigator.mediaDevices.removeEventListener(
'devicechange',
handleDeviceChange
)
isTracking.value = false
}

onMounted(() => runOnMount && start())
onUnmounted(stop)

return { devicesState, isTracking, isTracked, start, stop }
}
1 change: 1 addition & 0 deletions src/vue-use-kit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export * from './components/useIdle'
export * from './components/useIntersection'
export * from './components/useLocation'
export * from './components/useMedia'
export * from './components/useMediaDevices'
export * from './components/useMouse'
export * from './components/useMouseElement'
export * from './components/useSearchParams'
Expand Down

0 comments on commit 1717c6a

Please sign in to comment.