From 1717c6a83159ae9ec4c6f56665d13d36149b12aa Mon Sep 17 00:00:00 2001 From: Salvatore Tedde Date: Sat, 22 Feb 2020 12:20:50 +0000 Subject: [PATCH] feat(useMediaDevices): Adding the function useMediaDevices --- README.md | 2 + src/components/useMediaDevices/index.ts | 1 + .../stories/UseMediaDevicesDemo.vue | 51 +++++++++ .../stories/useMediaDevices.md | 71 ++++++++++++ .../stories/useMediaDevices.story.ts | 28 +++++ .../useMediaDevices/useMediaDevices.spec.ts | 101 ++++++++++++++++++ .../useMediaDevices/useMediaDevices.ts | 60 +++++++++++ src/vue-use-kit.ts | 1 + 8 files changed, 315 insertions(+) create mode 100755 src/components/useMediaDevices/index.ts create mode 100755 src/components/useMediaDevices/stories/UseMediaDevicesDemo.vue create mode 100755 src/components/useMediaDevices/stories/useMediaDevices.md create mode 100755 src/components/useMediaDevices/stories/useMediaDevices.story.ts create mode 100755 src/components/useMediaDevices/useMediaDevices.spec.ts create mode 100755 src/components/useMediaDevices/useMediaDevices.ts diff --git a/README.md b/README.md index 1ab8d90..98a0567 100755 --- a/README.md +++ b/README.md @@ -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) diff --git a/src/components/useMediaDevices/index.ts b/src/components/useMediaDevices/index.ts new file mode 100755 index 0000000..349f955 --- /dev/null +++ b/src/components/useMediaDevices/index.ts @@ -0,0 +1 @@ +export * from './useMediaDevices' diff --git a/src/components/useMediaDevices/stories/UseMediaDevicesDemo.vue b/src/components/useMediaDevices/stories/UseMediaDevicesDemo.vue new file mode 100755 index 0000000..ab8ab0d --- /dev/null +++ b/src/components/useMediaDevices/stories/UseMediaDevicesDemo.vue @@ -0,0 +1,51 @@ + + + diff --git a/src/components/useMediaDevices/stories/useMediaDevices.md b/src/components/useMediaDevices/stories/useMediaDevices.md new file mode 100755 index 0000000..2bc5146 --- /dev/null +++ b/src/components/useMediaDevices/stories/useMediaDevices.md @@ -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 + isTracking: Ref + isTracked: Ref + start: () => void + stop: () => void +} +``` + +### Parameters + +- `runOnMount: boolean` whether to run the connected media devices tracking on mount, `true` by default + +### Returns + +- `devicesState: Ref` the list of connected media devices +- `isTracking: Ref` whether the function is tracking the connected media devices or not +- `isTracked: Ref` 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 + + + +``` diff --git a/src/components/useMediaDevices/stories/useMediaDevices.story.ts b/src/components/useMediaDevices/stories/useMediaDevices.story.ts new file mode 100755 index 0000000..685dbc6 --- /dev/null +++ b/src/components/useMediaDevices/stories/useMediaDevices.story.ts @@ -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: ` +
+ + + + + +
` +}) + +storiesOf('sensors|useMediaDevices', module) + .addParameters({ notes }) + .add('Demo', basicDemo) diff --git a/src/components/useMediaDevices/useMediaDevices.spec.ts b/src/components/useMediaDevices/useMediaDevices.spec.ts new file mode 100755 index 0000000..75b4d29 --- /dev/null +++ b/src/components/useMediaDevices/useMediaDevices.spec.ts @@ -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: ` +
+
+
{{JSON.stringify(devicesState)}}
+ + +
+ `, + 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) + }) +}) diff --git a/src/components/useMediaDevices/useMediaDevices.ts b/src/components/useMediaDevices/useMediaDevices.ts new file mode 100755 index 0000000..3b2cf76 --- /dev/null +++ b/src/components/useMediaDevices/useMediaDevices.ts @@ -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 = 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 } +} diff --git a/src/vue-use-kit.ts b/src/vue-use-kit.ts index 3aab9d3..d1e2836 100755 --- a/src/vue-use-kit.ts +++ b/src/vue-use-kit.ts @@ -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'