From 7888ccc869d4c3560a517a7de33e878c3df43a43 Mon Sep 17 00:00:00 2001 From: Devlin Junker Date: Thu, 28 Sep 2023 14:16:39 -0700 Subject: [PATCH 1/2] tested with audio and video feeds Signed-off-by: Devlin Junker --- src/App.vue | 86 +++++++++++++++++-- .../feed-display/FeedItemDisplay.vue | 65 ++++++++++---- src/store/item.ts | 6 ++ src/types/MutationTypes.ts | 2 + 4 files changed, 137 insertions(+), 22 deletions(-) diff --git a/src/App.vue b/src/App.vue index 79a14d71e..f07f5810a 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,9 +1,29 @@ @@ -13,7 +33,7 @@ import Vue from 'vue' import NcContent from '@nextcloud/vue/dist/Components/NcContent.js' import NcAppContent from '@nextcloud/vue/dist/Components/NcAppContent.js' import Sidebar from './components/Sidebar.vue' -import { ACTIONS } from './store' +import { ACTIONS, MUTATIONS } from './store' export default Vue.extend({ components: { @@ -21,6 +41,11 @@ export default Vue.extend({ Sidebar, NcAppContent, }, + computed: { + playingItem() { + return this.$store.state.items.playingItem + }, + }, async created() { // fetch folders and feeds to build side bar await this.$store.dispatch(ACTIONS.FETCH_FOLDERS) @@ -28,6 +53,18 @@ export default Vue.extend({ // fetch starred to get starred count await this.$store.dispatch(ACTIONS.FETCH_STARRED) }, + methods: { + stopPlaying() { + this.$store.commit(MUTATIONS.SET_PLAYING_ITEM, { item: undefined }) + }, + stopVideo() { + const videoElements = document.getElementsByTagName('video') + + for (let i = 0; i < videoElements.length; i++) { + videoElements[i].pause() + } + }, + }, }) @@ -35,4 +72,43 @@ export default Vue.extend({ .material-design-icon { color: var(--color-text-lighter) } + + #news-app { + display: flex; + flex-direction: column; + width: 100%; + } + + #content-display { + display: flex; + flex-direction: row; + } + + #content-display.playing { + height: calc(100vh - 98px) + } + + .podcast { + position: absolute; + bottom: 0px; + height: 40px; + display: flex; + background-color: #474747; + width: 100%; + } + + .podcast audio { + flex-grow: 1; + background-color: rgba(0,0,0,0); + height: 40px; + } + + .podcast .podcast-download { + padding: 4px 10px; + margin: 2px 6px; + } + + .podcast .podcast-close { + margin: 2px 6px; + } diff --git a/src/components/feed-display/FeedItemDisplay.vue b/src/components/feed-display/FeedItemDisplay.vue index 8d97e856e..67417b39e 100644 --- a/src/components/feed-display/FeedItemDisplay.vue +++ b/src/components/feed-display/FeedItemDisplay.vue @@ -53,34 +53,39 @@ - -
- {{ t('news', 'Download audio') }}
-
+
-
- +
+ +
@@ -172,11 +177,22 @@ export default Vue.extend({ }, getMediaType(mime: string): 'audio' | 'video' | false { - // TODO: figure out how to check media type + if (mime && mime.indexOf('audio') === 0) { + return 'audio' + } else if (mime && mime.indexOf('video') === 0) { + return 'video' + } return false }, - play(item: FeedItem) { - // TODO: implement play audio/video + playAudio(item: FeedItem) { + this.$store.commit(MUTATIONS.SET_PLAYING_ITEM, { item }) + }, + stopAudio() { + const audioElements = document.getElementsByTagName('audio') + + for (let i = 0; i < audioElements.length; i++) { + audioElements[i].pause() + } }, }, }) @@ -197,6 +213,21 @@ export default Vue.extend({ height: 100%; } + .article video { + width: 100%; + background-size: cover; + } + + .article .enclosure.video { + display: flex; + flex-direction: column; + } + + .article .enclosure.video .download { + justify-content: center; + display: flex; + } + .article .body { color: var(--color-main-text); font-size: 15px; diff --git a/src/store/item.ts b/src/store/item.ts index 14ae20658..2ff49f4d1 100644 --- a/src/store/item.ts +++ b/src/store/item.ts @@ -29,6 +29,7 @@ export type ItemState = { allItems: FeedItem[]; selectedId?: string; + playingItem?: FeedItem } const state: ItemState = { @@ -40,7 +41,9 @@ const state: ItemState = { unreadCount: 0, allItems: [], + selectedId: undefined, + playingItem: undefined, } const getters = { @@ -290,6 +293,9 @@ export const mutations = { [FEED_ITEM_MUTATION_TYPES.SET_SELECTED_ITEM](state: ItemState, { id }: { id: string }) { state.selectedId = id }, + [FEED_ITEM_MUTATION_TYPES.SET_PLAYING_ITEM](state: ItemState, { item }: { item?: FeedItem }) { + state.playingItem = item + }, [FEED_ITEM_MUTATION_TYPES.SET_ITEMS](state: ItemState, items: FeedItem[]) { if (items) { diff --git a/src/types/MutationTypes.ts b/src/types/MutationTypes.ts index 6da393c6a..b427821cd 100644 --- a/src/types/MutationTypes.ts +++ b/src/types/MutationTypes.ts @@ -20,7 +20,9 @@ export const FOLDER_MUTATION_TYPES = { export const FEED_ITEM_MUTATION_TYPES = { SET_ITEMS: 'SET_ITEMS', UPDATE_ITEM: 'UPDATE_ITEM', + SET_SELECTED_ITEM: 'SET_SELECTED_ITEM', + SET_PLAYING_ITEM: 'SET_PLAYING_ITEM', SET_STARRED_COUNT: 'SET_STARRED_COUNT', SET_UNREAD_COUNT: 'SET_UNREAD_COUNT', From a04a017d96bb1979f15aed1d5d54c75cb8182271 Mon Sep 17 00:00:00 2001 From: Devlin Junker Date: Thu, 28 Sep 2023 15:46:35 -0700 Subject: [PATCH 2/2] add unit tests Signed-off-by: Devlin Junker --- src/App.vue | 3 +- .../feed-display/FeedItemDisplay.vue | 2 +- src/store/item.ts | 2 +- tests/javascript/unit/components/App.spec.ts | 49 +++++++++++++++++++ .../feed-display/FeedItemDisplay.spec.ts | 18 ++++++- tests/javascript/unit/store/item.spec.ts | 10 ++++ 6 files changed, 79 insertions(+), 5 deletions(-) create mode 100644 tests/javascript/unit/components/App.spec.ts diff --git a/src/App.vue b/src/App.vue index f07f5810a..2179ae8e6 100644 --- a/src/App.vue +++ b/src/App.vue @@ -55,7 +55,7 @@ export default Vue.extend({ }, methods: { stopPlaying() { - this.$store.commit(MUTATIONS.SET_PLAYING_ITEM, { item: undefined }) + this.$store.commit(MUTATIONS.SET_PLAYING_ITEM, undefined) }, stopVideo() { const videoElements = document.getElementsByTagName('video') @@ -82,6 +82,7 @@ export default Vue.extend({ #content-display { display: flex; flex-direction: row; + height: 100%; } #content-display.playing { diff --git a/src/components/feed-display/FeedItemDisplay.vue b/src/components/feed-display/FeedItemDisplay.vue index 67417b39e..904a0e51e 100644 --- a/src/components/feed-display/FeedItemDisplay.vue +++ b/src/components/feed-display/FeedItemDisplay.vue @@ -185,7 +185,7 @@ export default Vue.extend({ return false }, playAudio(item: FeedItem) { - this.$store.commit(MUTATIONS.SET_PLAYING_ITEM, { item }) + this.$store.commit(MUTATIONS.SET_PLAYING_ITEM, item) }, stopAudio() { const audioElements = document.getElementsByTagName('audio') diff --git a/src/store/item.ts b/src/store/item.ts index 2ff49f4d1..508b5c919 100644 --- a/src/store/item.ts +++ b/src/store/item.ts @@ -293,7 +293,7 @@ export const mutations = { [FEED_ITEM_MUTATION_TYPES.SET_SELECTED_ITEM](state: ItemState, { id }: { id: string }) { state.selectedId = id }, - [FEED_ITEM_MUTATION_TYPES.SET_PLAYING_ITEM](state: ItemState, { item }: { item?: FeedItem }) { + [FEED_ITEM_MUTATION_TYPES.SET_PLAYING_ITEM](state: ItemState, item?: FeedItem) { state.playingItem = item }, diff --git a/tests/javascript/unit/components/App.spec.ts b/tests/javascript/unit/components/App.spec.ts new file mode 100644 index 000000000..135b41d40 --- /dev/null +++ b/tests/javascript/unit/components/App.spec.ts @@ -0,0 +1,49 @@ +import { shallowMount, createLocalVue, Wrapper } from '@vue/test-utils' + +import App from '../../../../src/App.vue' +import { MUTATIONS } from '../../../../src/store' + +describe('FeedItemDisplay.vue', () => { + 'use strict' + const localVue = createLocalVue() + let wrapper: Wrapper + + const dispatchStub = jest.fn() + const commitStub = jest.fn() + beforeAll(() => { + wrapper = shallowMount(App, { + localVue, + mocks: { + $store: { + state: { + items: { + playingItem: undefined, + }, + }, + dispatch: dispatchStub, + commit: commitStub, + }, + }, + }) + }) + + beforeEach(() => { + dispatchStub.mockReset() + commitStub.mockReset() + }) + + it('should send SET_PLAYING_ITEM with undefined to stop playback', () => { + (wrapper.vm as any).stopPlaying() + + expect(commitStub).toBeCalledWith(MUTATIONS.SET_PLAYING_ITEM, undefined) + }) + + it('should stop all video elements in page when playing video', () => { + const pauseStub = jest.fn() + document.getElementsByTagName = jest.fn().mockReturnValue([{ pause: pauseStub }]); + + (wrapper.vm as any).stopVideo() + + expect(pauseStub).toBeCalled() + }) +}) diff --git a/tests/javascript/unit/components/feed-display/FeedItemDisplay.spec.ts b/tests/javascript/unit/components/feed-display/FeedItemDisplay.spec.ts index a75119414..7d60d4a01 100644 --- a/tests/javascript/unit/components/feed-display/FeedItemDisplay.spec.ts +++ b/tests/javascript/unit/components/feed-display/FeedItemDisplay.spec.ts @@ -72,7 +72,7 @@ describe('FeedItemDisplay.vue', () => { expect(feed).toEqual(mockFeed) }) - it('toggles starred state', () => { + it('should toggle starred state', () => { wrapper.vm.$props.item.starred = true; (wrapper.vm as any).toggleStarred(wrapper.vm.$props.item) @@ -88,5 +88,19 @@ describe('FeedItemDisplay.vue', () => { }) }) - // TODO: Audio/Video tests + it('should send SET_PLAYING_ITEM with item', () => { + const item = { id: 123 }; + (wrapper.vm as any).playAudio(item) + + expect(commitStub).toBeCalledWith(MUTATIONS.SET_PLAYING_ITEM, item) + }) + + it('should stop all audio elements in page when playing video', () => { + const pauseStub = jest.fn() + document.getElementsByTagName = jest.fn().mockReturnValue([{ pause: pauseStub }]); + + (wrapper.vm as any).stopAudio() + + expect(pauseStub).toBeCalled() + }) }) diff --git a/tests/javascript/unit/store/item.spec.ts b/tests/javascript/unit/store/item.spec.ts index b0c5fa412..f194ad223 100644 --- a/tests/javascript/unit/store/item.spec.ts +++ b/tests/javascript/unit/store/item.spec.ts @@ -129,6 +129,16 @@ describe('item.ts', () => { expect(state.selectedId).toEqual(123) }) }) + + describe('SET_PLAYING_ITEM', () => { + it('should update selectedId on state', async () => { + const state = { playingItem: undefined } as any + const item = { id: 123 } as any + mutations[FEED_ITEM_MUTATION_TYPES.SET_PLAYING_ITEM](state, item as any) + expect(state.playingItem).toEqual(item) + }) + }) + describe('SET_ITEMS', () => { it('should add feeds to state', () => { const state = { allItems: [] as any } as any