+
-
-
-
-
-
-
-
+
+
@@ -22,43 +17,41 @@ import { mapState } from 'vuex'
import NcCounterBubble from '@nextcloud/vue/dist/Components/NcCounterBubble.js'
-import VirtualScroll from './VirtualScroll.vue'
-import FeedItemComponent from './FeedItem.vue'
+import FeedItemDisplayList from './FeedItemDisplayList.vue'
import { FeedItem } from '../types/FeedItem'
+import { ACTIONS, MUTATIONS } from '../store'
export default Vue.extend({
components: {
NcCounterBubble,
- VirtualScroll,
- FeedItemComponent,
- },
- data() {
- return {
- mounted: false,
- }
+ FeedItemDisplayList,
},
computed: {
...mapState(['items']),
+
starred(): FeedItem[] {
return this.$store.getters.starred
},
- reachedEnd(): boolean {
- return this.mounted && this.$store.state.items.starredLoaded
- },
},
- mounted() {
- this.mounted = true
+ created() {
+ this.$store.commit(MUTATIONS.SET_SELECTED_ITEM, { id: undefined })
},
methods: {
async fetchMore() {
- // TODO: fetch more starred
+ if (!this.$store.state.items.fetchingItems.starred) {
+ this.$store.dispatch(ACTIONS.FETCH_STARRED, { start: this.$store.getters.starred[this.$store.getters.starred?.length - 1]?.id })
+ }
},
},
})
diff --git a/src/components/VirtualScroll.vue b/src/components/VirtualScroll.vue
index 7758559695..c6bb471bd9 100644
--- a/src/components/VirtualScroll.vue
+++ b/src/components/VirtualScroll.vue
@@ -19,6 +19,10 @@ export default Vue.extend({
type: Boolean,
required: true,
},
+ fetchKey: {
+ type: String,
+ required: true,
+ },
},
data() {
return {
@@ -31,7 +35,7 @@ export default Vue.extend({
},
computed: {
fetching() {
- return this.$store.state.items.fetchingItems
+ return this.$store.state.items.fetchingItems[this.key]
},
},
watch: {
diff --git a/src/dataservices/item.service.ts b/src/dataservices/item.service.ts
new file mode 100644
index 0000000000..cddd1e9781
--- /dev/null
+++ b/src/dataservices/item.service.ts
@@ -0,0 +1,80 @@
+import _ from 'lodash'
+import { AxiosResponse } from 'axios'
+import axios from '@nextcloud/axios'
+
+import { API_ROUTES } from '../types/ApiRoutes'
+import { FeedItem } from '../types/FeedItem'
+
+export const ITEM_TYPES = {
+ STARRED: 2,
+ UNREAD: 6,
+}
+
+export class ItemService {
+
+ static debounceFetchStarred = _.debounce(ItemService.fetchStarred, 400, { leading: true })
+ static debounceFetchUnread = _.debounce(ItemService.fetchUnread, 400, { leading: true })
+
+ /**
+ * Makes backend call to retrieve starred items
+ *
+ * @param start (id of last starred item loaded)
+ * @return {AxiosResponse} response object containing backend request response
+ */
+ static async fetchStarred(start: number): Promise
{
+ return await axios.get(API_ROUTES.ITEMS, {
+ params: {
+ limit: 40,
+ oldestFirst: false,
+ search: '',
+ showAll: false,
+ type: ITEM_TYPES.STARRED,
+ offset: start,
+ },
+ })
+ }
+
+ /**
+ * Makes backend call to retrieve unread items
+ *
+ * @param start (id of last unread item loaded)
+ * @return {AxiosResponse} response object containing backend request response
+ */
+ static async fetchUnread(start: number): Promise {
+ return await axios.get(API_ROUTES.ITEMS, {
+ params: {
+ limit: 40,
+ oldestFirst: false,
+ search: '',
+ showAll: false,
+ type: ITEM_TYPES.UNREAD,
+ offset: start,
+ },
+ })
+ }
+
+ /**
+ * Makes backend call to mark item as read/unread in DB
+ *
+ * @param {FeedItem} item FeedItem (containing id) that wil be marked as read/unread
+ * @param {boolean} read if read or not
+ */
+ static async markRead(item: FeedItem, read: boolean): Promise {
+ axios.post(API_ROUTES.ITEMS + `/${item.id}/read`, {
+ isRead: read,
+ })
+ }
+
+ /**
+ * Makes backend call to mark item as starred/unstarred in DB
+ *
+ * @param {FeedItem} item FeedItem (containing id) that wil be marked as starred/unstarred
+ * @param {boolean} read if starred or not
+ */
+ static async markStarred(item: FeedItem, read: boolean): Promise {
+ axios.post(API_ROUTES.ITEMS + `/${item.feedId}/${item.guidHash}/star`, {
+ isStarred: read,
+ })
+ }
+
+}
diff --git a/src/routes/index.ts b/src/routes/index.ts
index f54823d608..b572742c8f 100644
--- a/src/routes/index.ts
+++ b/src/routes/index.ts
@@ -2,10 +2,12 @@ import VueRouter from 'vue-router'
import ExplorePanel from '../components/Explore.vue'
import StarredPanel from '../components/Starred.vue'
+import UnreadPanel from '../components/Unread.vue'
export const ROUTES = {
EXPLORE: 'explore',
STARRED: 'starred',
+ UNREAD: 'unread',
}
const getInitialRoute = function() {
@@ -33,6 +35,12 @@ const routes = [
component: StarredPanel,
props: true,
},
+ {
+ name: ROUTES.UNREAD,
+ path: '/unread',
+ component: UnreadPanel,
+ props: true,
+ },
]
export default new VueRouter({
diff --git a/src/store/feed.ts b/src/store/feed.ts
index 9a0da520b3..d866042661 100644
--- a/src/store/feed.ts
+++ b/src/store/feed.ts
@@ -3,7 +3,7 @@ import axios from '@nextcloud/axios'
import { ActionParams, AppState } from '../store'
import { Feed } from '../types/Feed'
import { API_ROUTES } from '../types/ApiRoutes'
-import { FOLDER_MUTATION_TYPES, FEED_MUTATION_TYPES } from '../types/MutationTypes'
+import { FOLDER_MUTATION_TYPES, FEED_MUTATION_TYPES, FEED_ITEM_MUTATION_TYPES } from '../types/MutationTypes'
export const FEED_ACTION_TYPES = {
ADD_FEED: 'ADD_FEED',
@@ -25,6 +25,9 @@ export const actions = {
const feeds = await axios.get(API_ROUTES.FEED)
commit(FEED_MUTATION_TYPES.SET_FEEDS, feeds.data.feeds)
+ commit(FEED_ITEM_MUTATION_TYPES.SET_UNREAD_COUNT, (feeds.data.feeds.reduce((total: number, feed: Feed) => {
+ return total + feed.unreadCount
+ }, 0)))
},
async [FEED_ACTION_TYPES.ADD_FEED](
{ commit }: ActionParams,
diff --git a/src/store/item.ts b/src/store/item.ts
index dbe343cb55..982e759790 100644
--- a/src/store/item.ts
+++ b/src/store/item.ts
@@ -1,12 +1,12 @@
-import axios from '@nextcloud/axios'
-
import { ActionParams } from '../store'
import { FEED_ITEM_MUTATION_TYPES } from '../types/MutationTypes'
-import { API_ROUTES } from '../types/ApiRoutes'
+
import { FeedItem } from '../types/FeedItem'
+import { ItemService } from '../dataservices/item.service'
export const FEED_ITEM_ACTION_TYPES = {
FETCH_STARRED: 'FETCH_STARRED',
+ FETCH_UNREAD: 'FETCH_UNREAD',
MARK_READ: 'MARK_READ',
MARK_UNREAD: 'MARK_UNREAD',
STAR_ITEM: 'STAR_ITEM',
@@ -14,77 +14,97 @@ export const FEED_ITEM_ACTION_TYPES = {
}
export type ItemState = {
- fetchingItems: boolean;
+ fetchingItems: { [key: string]: boolean };
+ allItemsLoaded: { [key: string]: boolean };
starredLoaded: boolean;
starredCount: number;
+ unreadCount: number;
allItems: FeedItem[];
+
+ selectedId?: string;
}
const state: ItemState = {
- fetchingItems: false,
+ fetchingItems: {},
+ allItemsLoaded: {},
starredLoaded: false,
starredCount: 0,
+ unreadCount: 0,
allItems: [],
+ selectedId: undefined,
}
const getters = {
starred(state: ItemState) {
return state.allItems.filter((item) => item.starred)
},
+ unread(state: ItemState) {
+ return state.allItems.filter((item) => item.unread)
+ },
+ selected(state: ItemState) {
+ return state.allItems.find((item: FeedItem) => item.id === state.selectedId)
+ },
}
export const actions = {
- async [FEED_ITEM_ACTION_TYPES.FETCH_STARRED]({ commit }: ActionParams) {
- state.fetchingItems = true
- const response = await axios.get(API_ROUTES.ITEMS, {
- params: {
- limit: 40,
- oldestFirst: false,
- search: '',
- showAll: false,
- type: 2,
- offset: 0,
- },
- })
-
- commit(FEED_ITEM_MUTATION_TYPES.SET_ITEMS, response.data.items)
- commit(FEED_ITEM_MUTATION_TYPES.SET_STARRED_COUNT, response.data.starred)
-
- if (response.data.items.length < 40) {
- state.starredLoaded = true
+ async [FEED_ITEM_ACTION_TYPES.FETCH_UNREAD]({ commit }: ActionParams, { start }: { start: number } = { start: 0 }) {
+ commit(FEED_ITEM_MUTATION_TYPES.SET_FETCHING, { key: 'unread', fetching: true })
+
+ const response = await ItemService.debounceFetchUnread(start)
+
+ commit(FEED_ITEM_MUTATION_TYPES.SET_ITEMS, response?.data.items)
+
+ if (response?.data.items.length < 40) {
+ commit(FEED_ITEM_MUTATION_TYPES.SET_ALL_LOADED, { key: 'unread', loaded: true })
+ }
+ commit(FEED_ITEM_MUTATION_TYPES.SET_FETCHING, { key: 'unread', fetching: false })
+ },
+ async [FEED_ITEM_ACTION_TYPES.FETCH_STARRED]({ commit }: ActionParams, { start }: { start: number } = { start: 0 }) {
+ commit(FEED_ITEM_MUTATION_TYPES.SET_FETCHING, { key: 'starred', fetching: true })
+ const response = await ItemService.debounceFetchStarred(start)
+
+ commit(FEED_ITEM_MUTATION_TYPES.SET_ITEMS, response?.data.items)
+ if (response?.data.starred) {
+ commit(FEED_ITEM_MUTATION_TYPES.SET_STARRED_COUNT, response?.data.starred)
+ }
+
+ if (response?.data.items.length < 40) {
+ commit(FEED_ITEM_MUTATION_TYPES.SET_ALL_LOADED, { key: 'starred', loaded: true })
}
- state.fetchingItems = false
+ commit(FEED_ITEM_MUTATION_TYPES.SET_FETCHING, { key: 'starred', fetching: false })
},
[FEED_ITEM_ACTION_TYPES.MARK_READ]({ commit }: ActionParams, { item }: { item: FeedItem}) {
- axios.post(API_ROUTES.ITEMS + `/${item.id}/read`, {
- isRead: true,
- })
+ ItemService.markRead(item, true)
+
+ if (item.unread) {
+ commit(FEED_ITEM_MUTATION_TYPES.SET_UNREAD_COUNT, state.unreadCount - 1)
+ }
item.unread = false
commit(FEED_ITEM_MUTATION_TYPES.UPDATE_ITEM, { item })
},
[FEED_ITEM_ACTION_TYPES.MARK_UNREAD]({ commit }: ActionParams, { item }: { item: FeedItem}) {
- axios.post(API_ROUTES.ITEMS + `/${item.id}/read`, {
- isRead: false,
- })
+ ItemService.markRead(item, false)
+
+ if (!item.unread) {
+ commit(FEED_ITEM_MUTATION_TYPES.SET_UNREAD_COUNT, state.unreadCount + 1)
+ }
item.unread = true
commit(FEED_ITEM_MUTATION_TYPES.UPDATE_ITEM, { item })
},
[FEED_ITEM_ACTION_TYPES.STAR_ITEM]({ commit }: ActionParams, { item }: { item: FeedItem}) {
- axios.post(API_ROUTES.ITEMS + `/${item.feedId}/${item.guidHash}/star`, {
- isStarred: true,
- })
+ ItemService.markStarred(item, true)
+
item.starred = true
commit(FEED_ITEM_MUTATION_TYPES.UPDATE_ITEM, { item })
commit(FEED_ITEM_MUTATION_TYPES.SET_STARRED_COUNT, state.starredCount + 1)
},
[FEED_ITEM_ACTION_TYPES.UNSTAR_ITEM]({ commit }: ActionParams, { item }: { item: FeedItem}) {
- axios.post(API_ROUTES.ITEMS + `/${item.feedId}/${item.guidHash}/star`, {
- isStarred: false,
- })
+ ItemService.markStarred(item, false)
+
item.starred = false
commit(FEED_ITEM_MUTATION_TYPES.UPDATE_ITEM, { item })
commit(FEED_ITEM_MUTATION_TYPES.SET_STARRED_COUNT, state.starredCount - 1)
@@ -92,18 +112,34 @@ export const actions = {
}
export const mutations = {
+ [FEED_ITEM_MUTATION_TYPES.SET_SELECTED_ITEM](state: ItemState, { id }: { id: string }) {
+ state.selectedId = id
+ },
[FEED_ITEM_MUTATION_TYPES.SET_ITEMS](state: ItemState, items: FeedItem[]) {
- items.forEach(it => {
- state.allItems.push(it)
- })
+ if (items) {
+ items.forEach(it => {
+ if (state.allItems.find((existing: FeedItem) => existing.id === it.id) === undefined) {
+ state.allItems.push(it)
+ }
+ })
+ }
},
[FEED_ITEM_MUTATION_TYPES.SET_STARRED_COUNT](state: ItemState, count: number) {
state.starredCount = count
},
+ [FEED_ITEM_MUTATION_TYPES.SET_UNREAD_COUNT](state: ItemState, count: number) {
+ state.unreadCount = count
+ },
[FEED_ITEM_MUTATION_TYPES.UPDATE_ITEM](state: ItemState, { item }: { item: FeedItem }) {
const idx = state.allItems.findIndex((it) => it.id === item.id)
state.allItems.splice(idx, 1, item)
},
+ [FEED_ITEM_MUTATION_TYPES.SET_FETCHING](state: ItemState, { fetching, key }: { fetching: boolean; key: string; }) {
+ state.fetchingItems[key] = fetching
+ },
+ [FEED_ITEM_MUTATION_TYPES.SET_ALL_LOADED](state: ItemState, { loaded, key }: { loaded: boolean; key: string; }) {
+ state.allItemsLoaded[key] = loaded
+ },
}
export default {
diff --git a/src/types/MutationTypes.ts b/src/types/MutationTypes.ts
index 8c00e714b4..9d725c0c39 100644
--- a/src/types/MutationTypes.ts
+++ b/src/types/MutationTypes.ts
@@ -10,6 +10,12 @@ export const FOLDER_MUTATION_TYPES = {
export const FEED_ITEM_MUTATION_TYPES = {
SET_ITEMS: 'SET_ITEMS',
- SET_STARRED_COUNT: 'SET_STARRED_COUNT',
UPDATE_ITEM: 'UPDATE_ITEM',
+ SET_SELECTED_ITEM: 'SET_SELECTED_ITEM',
+
+ SET_STARRED_COUNT: 'SET_STARRED_COUNT',
+ SET_UNREAD_COUNT: 'SET_UNREAD_COUNT',
+
+ SET_FETCHING: 'SET_FETCHING',
+ SET_ALL_LOADED: 'SET_ALL_LOADED',
}
diff --git a/tests/javascript/unit/components/AddFeed.spec.ts b/tests/javascript/unit/components/AddFeed.spec.ts
index daf50329e6..169d9b5a21 100644
--- a/tests/javascript/unit/components/AddFeed.spec.ts
+++ b/tests/javascript/unit/components/AddFeed.spec.ts
@@ -47,13 +47,13 @@ describe('AddFeed.vue', () => {
expect(response).toBeFalsy()
- wrapper.vm.$data.feedUrl = 'http://test.com'
+ wrapper.vm.$data.feedUrl = 'http://example.com'
response = wrapper.vm.feedUrlExists()
expect(response).toBeFalsy()
- wrapper.vm.$data.feedUrl = 'http://test.com'
- wrapper.vm.$store.state.feeds.feeds = [{ url: 'http://test.com' }]
+ wrapper.vm.$data.feedUrl = 'http://example.com'
+ wrapper.vm.$store.state.feeds.feeds = [{ url: 'http://example.com' }]
response = wrapper.vm.feedUrlExists()
expect(response).toBeTruthy()
diff --git a/tests/javascript/unit/components/FeedItemDisplay.spec.ts b/tests/javascript/unit/components/FeedItemDisplay.spec.ts
new file mode 100644
index 0000000000..594ccf9c15
--- /dev/null
+++ b/tests/javascript/unit/components/FeedItemDisplay.spec.ts
@@ -0,0 +1,92 @@
+import { shallowMount, createLocalVue, Wrapper } from '@vue/test-utils'
+
+import FeedItemDisplay from '../../../../src/components/FeedItemDisplay.vue'
+import { ACTIONS, MUTATIONS } from '../../../../src/store'
+
+describe('FeedItemDisplay.vue', () => {
+ 'use strict'
+ const localVue = createLocalVue()
+ let wrapper: Wrapper
+
+ const mockItem = {
+ feedId: 1,
+ title: 'feed item',
+ pubDate: Date.now() / 1000,
+ }
+ const mockFeed = {
+ id: 1,
+ }
+
+ const dispatchStub = jest.fn()
+ const commitStub = jest.fn()
+ beforeAll(() => {
+ wrapper = shallowMount(FeedItemDisplay, {
+ propsData: {
+ item: mockItem,
+ },
+ localVue,
+ mocks: {
+ $store: {
+ getters: {
+ feeds: [mockFeed],
+ },
+ state: {
+ feeds: [],
+ folders: [],
+ },
+ dispatch: dispatchStub,
+ commit: commitStub,
+ },
+ },
+ })
+ })
+
+ beforeEach(() => {
+ dispatchStub.mockReset()
+ commitStub.mockReset()
+ })
+
+ it('should send SET_SELECTED_ITEM with undefined id', () => {
+ (wrapper.vm as any).clearSelected()
+
+ expect(commitStub).toBeCalledWith(MUTATIONS.SET_SELECTED_ITEM, { id: undefined })
+ })
+
+ it('should format date to match locale', () => {
+ const epoch = Date.now() // Provide an epoch timestamp
+ const formattedDate = (wrapper.vm as any).formatDate(epoch)
+
+ expect(formattedDate).toEqual(new Date(epoch).toLocaleString())
+ })
+
+ it('should format datetime to match international standard', () => {
+ const epoch = Date.now() // Provide an epoch timestamp
+ const formattedDate = (wrapper.vm as any).formatDatetime(epoch)
+
+ expect(formattedDate).toEqual(new Date(epoch).toISOString())
+ })
+
+ it('should retrieve feed by ID', () => {
+ const feed = (wrapper.vm as any).getFeed(mockFeed.id)
+
+ expect(feed).toEqual(mockFeed)
+ })
+
+ it('toggles starred state', () => {
+ wrapper.vm.$props.item.starred = true;
+
+ (wrapper.vm as any).toggleStarred(wrapper.vm.$props.item)
+ expect(dispatchStub).toHaveBeenCalledWith(ACTIONS.UNSTAR_ITEM, {
+ item: wrapper.vm.$props.item,
+ })
+
+ wrapper.vm.$props.item.starred = false;
+
+ (wrapper.vm as any).toggleStarred(wrapper.vm.$props.item)
+ expect(dispatchStub).toHaveBeenCalledWith(ACTIONS.STAR_ITEM, {
+ item: wrapper.vm.$props.item,
+ })
+ })
+
+ // TODO: Audio/Video tests
+})
diff --git a/tests/javascript/unit/components/FeedItemDisplayList.spec.ts b/tests/javascript/unit/components/FeedItemDisplayList.spec.ts
new file mode 100644
index 0000000000..2b1d7e4551
--- /dev/null
+++ b/tests/javascript/unit/components/FeedItemDisplayList.spec.ts
@@ -0,0 +1,66 @@
+import Vuex, { Store } from 'vuex'
+import { shallowMount, createLocalVue, Wrapper } from '@vue/test-utils'
+
+import FeedItemDisplayList from '../../../../src/components/FeedItemDisplayList.vue'
+import VirtualScroll from '../../../../src/components/VirtualScroll.vue'
+import FeedItemRow from '../../../../src/components/FeedItemRow.vue'
+
+jest.mock('@nextcloud/axios')
+
+describe('FeedItemDisplayList.vue', () => {
+ 'use strict'
+ const localVue = createLocalVue()
+ localVue.use(Vuex)
+ let wrapper: Wrapper
+
+ const mockItem = {
+ feedId: 1,
+ title: 'feed item',
+ pubDate: Date.now() / 1000,
+ }
+
+ let store: Store
+ beforeAll(() => {
+ store = new Vuex.Store({
+ state: {
+ items: {
+ allItemsLoaded: {
+ unread: false,
+ },
+ },
+ },
+ actions: {
+ },
+ getters: {
+ unread: () => [mockItem, mockItem],
+ },
+ })
+
+ store.dispatch = jest.fn()
+ store.commit = jest.fn()
+
+ wrapper = shallowMount(FeedItemDisplayList, {
+ propsData: {
+ items: [mockItem],
+ fetchKey: 'unread',
+ },
+ localVue,
+ store,
+ })
+ })
+
+ it('should create FeedItemRow items from input', () => {
+ expect((wrapper.findComponent(VirtualScroll)).findAllComponents(FeedItemRow).length).toEqual(1)
+
+ wrapper = shallowMount(FeedItemDisplayList, {
+ propsData: {
+ items: [mockItem, mockItem],
+ fetchKey: 'unread',
+ },
+ localVue,
+ store,
+ })
+ expect((wrapper.findComponent(VirtualScroll)).findAllComponents(FeedItemRow).length).toEqual(2)
+ })
+
+})
diff --git a/tests/javascript/unit/components/FeedItem.spec.ts b/tests/javascript/unit/components/FeedItemRow.spec.ts
similarity index 80%
rename from tests/javascript/unit/components/FeedItem.spec.ts
rename to tests/javascript/unit/components/FeedItemRow.spec.ts
index 88991201fb..dde0106a95 100644
--- a/tests/javascript/unit/components/FeedItem.spec.ts
+++ b/tests/javascript/unit/components/FeedItemRow.spec.ts
@@ -1,12 +1,12 @@
import { shallowMount, createLocalVue, Wrapper } from '@vue/test-utils'
-import FeedItem from '../../../../src/components/FeedItem.vue'
+import FeedItemRow from '../../../../src/components/FeedItemRow.vue'
import { ACTIONS } from '../../../../src/store'
-describe('FeedItem.vue', () => {
+describe('FeedItemRow.vue', () => {
'use strict'
const localVue = createLocalVue()
- let wrapper: Wrapper
+ let wrapper: Wrapper
const mockItem = {
feedId: 1,
@@ -19,7 +19,7 @@ describe('FeedItem.vue', () => {
const dispatchStub = jest.fn()
beforeAll(() => {
- wrapper = shallowMount(FeedItem, {
+ wrapper = shallowMount(FeedItemRow, {
propsData: {
item: mockItem,
},
@@ -34,6 +34,7 @@ describe('FeedItem.vue', () => {
folders: [],
},
dispatch: dispatchStub,
+ commit: jest.fn(),
},
},
})
@@ -44,24 +45,17 @@ describe('FeedItem.vue', () => {
})
it('should initialize without expanded and without keepUnread', () => {
- expect(wrapper.vm.$data.expanded).toBeFalsy()
expect(wrapper.vm.$data.keepUnread).toBeFalsy()
})
- it('should expand when clicked', async () => {
- await wrapper.find('.feed-item-row').trigger('click')
-
- expect(wrapper.vm.$data.expanded).toBe(true)
- })
-
- it('should format date correctly', () => {
+ it('should format date to match locale', () => {
const epoch = Date.now() // Provide an epoch timestamp
const formattedDate = (wrapper.vm as any).formatDate(epoch)
expect(formattedDate).toEqual(new Date(epoch).toLocaleString())
})
- it('should format datetime correctly', () => {
+ it('should format datetime to match international standard', () => {
const epoch = Date.now() // Provide an epoch timestamp
const formattedDate = (wrapper.vm as any).formatDatetime(epoch)
@@ -130,12 +124,4 @@ describe('FeedItem.vue', () => {
item: wrapper.vm.$props.item,
})
})
-
- xit('TODO test: getMediaType(mime: any): audio | video | false', () => {
- // TODO: finish tests after audio/video playback is supported
- })
-
- xit('TODO test: play(item: any): void', () => {
- // TODO: finish tests after audio/video playback is supported
- })
})
diff --git a/tests/javascript/unit/components/Starred.spec.ts b/tests/javascript/unit/components/Starred.spec.ts
index 212c69f6e1..4f09f4e3ce 100644
--- a/tests/javascript/unit/components/Starred.spec.ts
+++ b/tests/javascript/unit/components/Starred.spec.ts
@@ -2,12 +2,11 @@ import Vuex, { Store } from 'vuex'
import { shallowMount, createLocalVue, Wrapper } from '@vue/test-utils'
import Starred from '../../../../src/components/Starred.vue'
-import VirtualScroll from '../../../../src/components/VirtualScroll.vue'
-import FeedItem from '../../../../src/components/FeedItem.vue'
+import FeedItemDisplayList from '../../../../src/components/FeedItemDisplayList.vue'
jest.mock('@nextcloud/axios')
-describe('Explore.vue', () => {
+describe('Starred.vue', () => {
'use strict'
const localVue = createLocalVue()
localVue.use(Vuex)
@@ -24,7 +23,9 @@ describe('Explore.vue', () => {
store = new Vuex.Store({
state: {
items: {
- starredLoaded: false,
+ fetchingItems: {
+ starred: false,
+ },
},
},
actions: {
@@ -33,6 +34,10 @@ describe('Explore.vue', () => {
starred: () => [mockItem],
},
})
+
+ store.dispatch = jest.fn()
+ store.commit = jest.fn()
+
wrapper = shallowMount(Starred, {
propsData: {
item: mockItem,
@@ -42,34 +47,12 @@ describe('Explore.vue', () => {
})
})
- it('should initialize with mounted flag set', () => {
- expect(wrapper.vm.$data.mounted).toBeTruthy()
- })
-
it('should get starred items from state', () => {
- expect((wrapper.findAllComponents(FeedItem).length)).toEqual(1)
+ expect((wrapper.findComponent(FeedItemDisplayList)).props().items.length).toEqual(1)
})
- it('should check starredLoaded and mounted to determine if the virtual scroll has reached end ', () => {
- wrapper.vm.$store.state.items.starredLoaded = false
- expect((wrapper.findComponent(VirtualScroll)).props().reachedEnd).toEqual(false)
-
- wrapper.vm.$store.state.items.starredLoaded = true
- store.state.items.starredLoaded = true
-
- wrapper = shallowMount(Starred, {
- propsData: {
- item: mockItem,
- },
- data: () => {
- return {
- mounted: true,
- }
- },
- localVue,
- store,
- })
-
- expect((wrapper.findComponent(VirtualScroll)).props().reachedEnd).toEqual(true)
+ it('should dispatch FETCH_STARRED action if not fetchingItems.starred', () => {
+ (wrapper.vm as any).fetchMore()
+ expect(store.dispatch).toBeCalled()
})
})
diff --git a/tests/javascript/unit/components/Unread.spec.ts b/tests/javascript/unit/components/Unread.spec.ts
new file mode 100644
index 0000000000..6de4d85651
--- /dev/null
+++ b/tests/javascript/unit/components/Unread.spec.ts
@@ -0,0 +1,65 @@
+import Vuex, { Store } from 'vuex'
+import { shallowMount, createLocalVue, Wrapper } from '@vue/test-utils'
+
+import Unread from '../../../../src/components/Unread.vue'
+import FeedItemDisplayList from '../../../../src/components/FeedItemDisplayList.vue'
+
+jest.mock('@nextcloud/axios')
+
+describe('Unread.vue', () => {
+ 'use strict'
+ const localVue = createLocalVue()
+ localVue.use(Vuex)
+ let wrapper: Wrapper
+
+ const mockItem = {
+ feedId: 1,
+ title: 'feed item',
+ pubDate: Date.now() / 1000,
+ }
+
+ let store: Store
+ beforeAll(() => {
+ store = new Vuex.Store({
+ state: {
+ items: {
+ fetchingItems: {
+ unread: false,
+ },
+ },
+ },
+ actions: {
+ },
+ getters: {
+ unread: () => [mockItem, mockItem],
+ },
+ })
+
+ store.dispatch = jest.fn()
+ store.commit = jest.fn()
+
+ wrapper = shallowMount(Unread, {
+ propsData: {
+ item: mockItem,
+ },
+ localVue,
+ store,
+ })
+ })
+
+ it('should get unread items from state', () => {
+ expect((wrapper.findComponent(FeedItemDisplayList)).props().items.length).toEqual(2)
+ })
+
+ it('should dispatch FETCH_UNREAD action if not fetchingItems.unread', () => {
+ (wrapper.vm as any).$store.state.items.fetchingItems.unread = true;
+
+ (wrapper.vm as any).fetchMore()
+ expect(store.dispatch).not.toBeCalled();
+
+ (wrapper.vm as any).$store.state.items.fetchingItems.unread = false;
+
+ (wrapper.vm as any).fetchMore()
+ expect(store.dispatch).toBeCalled()
+ })
+})
diff --git a/tests/javascript/unit/services/item.service.spec.ts b/tests/javascript/unit/services/item.service.spec.ts
new file mode 100644
index 0000000000..29de8f1a30
--- /dev/null
+++ b/tests/javascript/unit/services/item.service.spec.ts
@@ -0,0 +1,66 @@
+import { ITEM_TYPES, ItemService } from '../../../../src/dataservices/item.service'
+import axios from '@nextcloud/axios'
+
+jest.mock('@nextcloud/axios')
+
+describe('item.service.ts', () => {
+ 'use strict'
+
+ beforeEach(() => {
+ (axios.get as any).mockReset();
+ (axios.post as any).mockReset()
+ })
+
+ describe('fetchStarred', () => {
+ it('should call GET with offset set to start param', async () => {
+ (axios as any).get.mockResolvedValue({ data: { feeds: [] } })
+
+ await ItemService.fetchStarred(0)
+
+ expect(axios.get).toBeCalled()
+ const queryParams = (axios.get as any).mock.calls[0][1].params
+
+ expect(queryParams.offset).toEqual(0)
+ expect(queryParams.type).toEqual(ITEM_TYPES.STARRED)
+ })
+ })
+
+ describe('fetchUnread', () => {
+ it('should call GET with offset set to start param', async () => {
+ (axios as any).get.mockResolvedValue({ data: { feeds: [] } })
+
+ await ItemService.fetchUnread(2)
+
+ expect(axios.get).toBeCalled()
+ const queryParams = (axios.get as any).mock.calls[0][1].params
+
+ expect(queryParams.offset).toEqual(2)
+ expect(queryParams.type).toEqual(ITEM_TYPES.UNREAD)
+ })
+ })
+
+ describe('markRead', () => {
+ it('should call POST with item id in URL and read param', async () => {
+ await ItemService.markRead({ id: 123 } as any, true)
+
+ expect(axios.post).toBeCalled()
+ const args = (axios.post as any).mock.calls[0]
+
+ expect(args[0]).toContain('123')
+ expect(args[1].isRead).toEqual(true)
+ })
+ })
+
+ describe('markStarred', () => {
+ it('should call POST with item feedId and guidHash in URL and read param', async () => {
+ await ItemService.markStarred({ feedId: 1, guidHash: 'abc' } as any, false)
+
+ expect(axios.post).toBeCalled()
+ const args = (axios.post as any).mock.calls[0]
+
+ expect(args[0]).toContain('1')
+ expect(args[0]).toContain('abc')
+ expect(args[1].isStarred).toEqual(false)
+ })
+ })
+})
diff --git a/tests/javascript/unit/store/feed.spec.ts b/tests/javascript/unit/store/feed.spec.ts
index 922c79f3fa..3a7521f892 100644
--- a/tests/javascript/unit/store/feed.spec.ts
+++ b/tests/javascript/unit/store/feed.spec.ts
@@ -3,7 +3,7 @@ import { Feed } from '../../../../src/types/Feed'
import { AppState } from '../../../../src/store'
import { FEED_ACTION_TYPES, mutations, actions } from '../../../../src/store/feed'
-import { FEED_MUTATION_TYPES } from '../../../../src/types/MutationTypes'
+import { FEED_ITEM_MUTATION_TYPES, FEED_MUTATION_TYPES } from '../../../../src/types/MutationTypes'
jest.mock('@nextcloud/axios')
@@ -11,6 +11,17 @@ describe('feed.ts', () => {
'use strict'
describe('actions', () => {
+ describe('FETCH_FEEDS', () => {
+ it('should call GET and commit returned feeds to state', async () => {
+ (axios as any).get.mockResolvedValue({ data: { feeds: [] } })
+ const commit = jest.fn()
+ await (actions[FEED_ACTION_TYPES.FETCH_FEEDS] as any)({ commit })
+ expect(axios.get).toBeCalled()
+ expect(commit).toBeCalledWith(FEED_MUTATION_TYPES.SET_FEEDS, [])
+ expect(commit).toBeCalledWith(FEED_ITEM_MUTATION_TYPES.SET_UNREAD_COUNT, 0)
+ })
+ })
+
describe('ADD_FEED', () => {
it('should call POST and commit feed to state', async () => {
(axios as any).post.mockResolvedValue({ data: { feeds: [] } })
@@ -30,13 +41,6 @@ describe('feed.ts', () => {
})
})
- it('FETCH_FEEDS should call GET and commit returned feeds to state', async () => {
- (axios as any).get.mockResolvedValue({ data: { feeds: [] } })
- const commit = jest.fn()
- await (actions[FEED_ACTION_TYPES.FETCH_FEEDS] as any)({ commit })
- expect(axios.get).toBeCalled()
- expect(commit).toBeCalled()
- })
})
describe('mutations', () => {
diff --git a/tests/javascript/unit/store/item.spec.ts b/tests/javascript/unit/store/item.spec.ts
index 614e47c1da..c393a03f4a 100644
--- a/tests/javascript/unit/store/item.spec.ts
+++ b/tests/javascript/unit/store/item.spec.ts
@@ -1,22 +1,37 @@
-import axios from '@nextcloud/axios'
import { AppState } from '../../../../src/store'
import { FEED_ITEM_ACTION_TYPES, mutations, actions } from '../../../../src/store/item'
import { FEED_ITEM_MUTATION_TYPES } from '../../../../src/types/MutationTypes'
-import { FeedItem } from '../../../../src/types/FeedItem'
+import { ItemService } from '../../../../src/dataservices/item.service'
-jest.mock('@nextcloud/axios')
-
-describe('feed.ts', () => {
+describe('item.ts', () => {
'use strict'
describe('actions', () => {
+ describe('FETCH_UNREAD', () => {
+ it('should call ItemService and commit items to state', async () => {
+ const fetchMock = jest.fn()
+ fetchMock.mockResolvedValue({ data: { items: [] } })
+ ItemService.debounceFetchUnread = fetchMock as any
+ const commit = jest.fn()
+
+ await (actions[FEED_ITEM_ACTION_TYPES.FETCH_UNREAD] as any)({ commit })
+
+ expect(fetchMock).toBeCalled()
+ expect(commit).toBeCalledWith(FEED_ITEM_MUTATION_TYPES.SET_ITEMS, [])
+ })
+ })
+
describe('FETCH_STARRED', () => {
- it('should call GET and commit items and starred count to state', async () => {
- (axios as any).get.mockResolvedValue({ data: { items: [], starred: 3 } })
+ it('should call ItemService and commit items and starred count to state', async () => {
+ const fetchMock = jest.fn()
+ fetchMock.mockResolvedValue({ data: { items: [], starred: 3 } })
+ ItemService.debounceFetchStarred = fetchMock as any
const commit = jest.fn()
+
await (actions[FEED_ITEM_ACTION_TYPES.FETCH_STARRED] as any)({ commit })
- expect(axios.get).toBeCalled()
+
+ expect(fetchMock).toBeCalled()
expect(commit).toBeCalledWith(FEED_ITEM_MUTATION_TYPES.SET_ITEMS, [])
expect(commit).toBeCalledWith(FEED_ITEM_MUTATION_TYPES.SET_STARRED_COUNT, 3)
})
@@ -25,52 +40,95 @@ describe('feed.ts', () => {
it('MARK_READ should call GET and commit returned feeds to state', async () => {
const item = { id: 1 }
const commit = jest.fn()
+ const serviceMock = jest.fn()
+ ItemService.markRead = serviceMock
+
await (actions[FEED_ITEM_ACTION_TYPES.MARK_READ] as any)({ commit }, { item })
- expect(axios.post).toBeCalled()
+
+ expect(serviceMock).toBeCalledWith(item, true)
expect(commit).toBeCalled()
})
it('MARK_UNREAD should call GET and commit returned feeds to state', async () => {
const item = { id: 1 }
const commit = jest.fn()
+ const serviceMock = jest.fn()
+ ItemService.markRead = serviceMock
+
await (actions[FEED_ITEM_ACTION_TYPES.MARK_UNREAD] as any)({ commit }, { item })
- expect(axios.post).toBeCalled()
+
+ expect(serviceMock).toBeCalledWith(item, false)
expect(commit).toBeCalledWith(FEED_ITEM_MUTATION_TYPES.UPDATE_ITEM, { item })
})
it('STAR_ITEM should call GET and commit returned feeds to state', async () => {
- const item = { id: 1 };
- (axios as any).get.mockResolvedValue({ data: { feeds: [] } })
+ const item = { id: 1 }
const commit = jest.fn()
+ const serviceMock = jest.fn()
+ ItemService.markStarred = serviceMock
+
await (actions[FEED_ITEM_ACTION_TYPES.STAR_ITEM] as any)({ commit }, { item })
- expect(axios.post).toBeCalled()
+
+ expect(serviceMock).toBeCalledWith(item, true)
expect(commit).toBeCalled()
})
it('UNSTAR_ITEM should call GET and commit returned feeds to state', async () => {
- const item = { id: 1 };
- (axios as any).get.mockResolvedValue({ data: { feeds: [] } })
+ const item = { id: 1 }
const commit = jest.fn()
+ const serviceMock = jest.fn()
+ ItemService.markStarred = serviceMock
+
await (actions[FEED_ITEM_ACTION_TYPES.UNSTAR_ITEM] as any)({ commit }, { item })
- expect(axios.post).toBeCalled()
+
+ expect(serviceMock).toBeCalledWith(item, false)
expect(commit).toBeCalled()
})
})
describe('mutations', () => {
+ describe('SET_SELECTED_ITEM', () => {
+ it('should update selectedId on state', async () => {
+ const state = { selectedId: undefined } as any
+ const item = { id: 123 } as any
+ mutations[FEED_ITEM_MUTATION_TYPES.SET_SELECTED_ITEM](state, item as any)
+ expect(state.selectedId).toEqual(123)
+ })
+ })
describe('SET_ITEMS', () => {
it('should add feeds to state', () => {
- const state = { allItems: [] as any } as AppState
+ const state = { allItems: [] as any } as any
let items = [] as any
mutations[FEED_ITEM_MUTATION_TYPES.SET_ITEMS](state, items)
expect(state.allItems.length).toEqual(0)
- items = [{ title: 'test' }] as FeedItem[]
+ items = [{ title: 'test', id: 123 }]
+
+ mutations[FEED_ITEM_MUTATION_TYPES.SET_ITEMS](state, items)
+ expect(state.allItems.length).toEqual(1)
+ expect(state.allItems[0]).toEqual(items[0])
+
+ items = [{ title: 'test2', id: 234 }]
+ mutations[FEED_ITEM_MUTATION_TYPES.SET_ITEMS](state, items)
+ expect(state.allItems.length).toEqual(2)
+ })
+
+ it('should not add duplicates', () => {
+ const state = { allItems: [] as any } as any
+ let items = [{ title: 'test', id: 123 }] as any
mutations[FEED_ITEM_MUTATION_TYPES.SET_ITEMS](state, items)
expect(state.allItems.length).toEqual(1)
expect(state.allItems[0]).toEqual(items[0])
+
+ mutations[FEED_ITEM_MUTATION_TYPES.SET_ITEMS](state, items)
+ expect(state.allItems.length).toEqual(1)
+ expect(state.allItems[0]).toEqual(items[0])
+
+ items = [{ title: 'test2', id: 234 }]
+ mutations[FEED_ITEM_MUTATION_TYPES.SET_ITEMS](state, items)
+ expect(state.allItems.length).toEqual(2)
})
})
@@ -83,6 +141,15 @@ describe('feed.ts', () => {
})
})
+ describe('SET_UNREAD_COUNT', () => {
+ it('should set unreadCount with value passed in', () => {
+ const state = { unreadCount: 0 } as AppState
+
+ (mutations[FEED_ITEM_MUTATION_TYPES.SET_UNREAD_COUNT] as any)(state, 123)
+ expect(state.unreadCount).toEqual(123)
+ })
+ })
+
describe('UPDATE_ITEM', () => {
it('should add a single feed to state', () => {
const state = { allItems: [{ id: 1, title: 'abc' }] as any } as AppState
@@ -92,5 +159,29 @@ describe('feed.ts', () => {
expect(state.allItems[0]).toEqual(item)
})
})
+
+ describe('SET_FETCHING', () => {
+ it('should set fetchingItems value with key passed in', () => {
+ const state = { fetchingItems: {} } as AppState
+
+ (mutations[FEED_ITEM_MUTATION_TYPES.SET_FETCHING] as any)(state, { fetching: true, key: 'starred' })
+ expect(state.fetchingItems.starred).toEqual(true);
+
+ (mutations[FEED_ITEM_MUTATION_TYPES.SET_FETCHING] as any)(state, { fetching: false, key: 'starred' })
+ expect(state.fetchingItems.starred).toEqual(false)
+ })
+ })
+
+ describe('SET_ALL_LOADED', () => {
+ it('should set allItemsLoaded value with key passed in', () => {
+ const state = { allItemsLoaded: {} } as AppState
+
+ (mutations[FEED_ITEM_MUTATION_TYPES.SET_ALL_LOADED] as any)(state, { loaded: true, key: 'starred' })
+ expect(state.allItemsLoaded.starred).toEqual(true);
+
+ (mutations[FEED_ITEM_MUTATION_TYPES.SET_ALL_LOADED] as any)(state, { loaded: false, key: 'starred' })
+ expect(state.allItemsLoaded.starred).toEqual(false)
+ })
+ })
})
})