From 89e09857af5cbcdd514e89fb3b10512d2adb9865 Mon Sep 17 00:00:00 2001 From: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com> Date: Thu, 27 Jul 2023 02:38:27 +0530 Subject: [PATCH] feat: Add store and API to support articles in widget (#7616) --- app/javascript/widget/api/article.js | 7 + app/javascript/widget/api/endPoints.js | 8 ++ app/javascript/widget/store/index.js | 2 + .../widget/store/modules/articles.js | 56 ++++++++ .../modules/specs/article/actions.spec.js | 127 ++++++++++++++++++ 5 files changed, 200 insertions(+) create mode 100644 app/javascript/widget/api/article.js create mode 100644 app/javascript/widget/store/modules/articles.js create mode 100644 app/javascript/widget/store/modules/specs/article/actions.spec.js diff --git a/app/javascript/widget/api/article.js b/app/javascript/widget/api/article.js new file mode 100644 index 000000000..0c9d6675a --- /dev/null +++ b/app/javascript/widget/api/article.js @@ -0,0 +1,7 @@ +import endPoints from 'widget/api/endPoints'; +import { API } from 'widget/helpers/axios'; + +export const getMostReadArticles = async (slug, locale) => { + const urlData = endPoints.getMostReadArticles(slug, locale); + return API.get(urlData.url, { params: urlData.params }); +}; diff --git a/app/javascript/widget/api/endPoints.js b/app/javascript/widget/api/endPoints.js index 3839a8e82..e8193ee9e 100755 --- a/app/javascript/widget/api/endPoints.js +++ b/app/javascript/widget/api/endPoints.js @@ -93,6 +93,13 @@ const triggerCampaign = ({ websiteToken, campaignId, customAttributes }) => ({ }, }); +const getMostReadArticles = (slug, locale) => ({ + url: `/hc/${slug}/${locale}/articles.json`, + params: { + page: 1, + }, +}); + export default { createConversation, sendMessage, @@ -102,4 +109,5 @@ export default { getAvailableAgents, getCampaigns, triggerCampaign, + getMostReadArticles, }; diff --git a/app/javascript/widget/store/index.js b/app/javascript/widget/store/index.js index f2ab1e3ae..f4cb0f90a 100755 --- a/app/javascript/widget/store/index.js +++ b/app/javascript/widget/store/index.js @@ -10,6 +10,7 @@ import events from 'widget/store/modules/events'; import globalConfig from 'shared/store/globalConfig'; import message from 'widget/store/modules/message'; import campaign from 'widget/store/modules/campaign'; +import article from 'widget/store/modules/articles'; Vue.use(Vuex); export default new Vuex.Store({ @@ -24,5 +25,6 @@ export default new Vuex.Store({ globalConfig, message, campaign, + article, }, }); diff --git a/app/javascript/widget/store/modules/articles.js b/app/javascript/widget/store/modules/articles.js new file mode 100644 index 000000000..8a22ed26f --- /dev/null +++ b/app/javascript/widget/store/modules/articles.js @@ -0,0 +1,56 @@ +import Vue from 'vue'; +import { getMostReadArticles } from 'widget/api/article'; + +const state = { + records: [], + uiFlags: { + isError: false, + hasFetched: false, + isFetching: false, + }, +}; + +export const getters = { + uiFlags: $state => $state.uiFlags, + popularArticles: $state => $state.records, +}; + +export const actions = { + fetch: async ({ commit }, { slug, locale }) => { + commit('setIsFetching', true); + commit('setError', false); + + try { + const { data } = await getMostReadArticles(slug, locale); + const { payload = [] } = data; + + if (payload.length) { + commit('setArticles', payload); + } + } catch (error) { + commit('setError', true); + } finally { + commit('setIsFetching', false); + } + }, +}; + +export const mutations = { + setArticles($state, data) { + Vue.set($state, 'records', data); + }, + setError($state, value) { + Vue.set($state.uiFlags, 'isError', value); + }, + setIsFetching($state, value) { + Vue.set($state.uiFlags, 'isFetching', value); + }, +}; + +export default { + namespaced: true, + state, + getters, + actions, + mutations, +}; diff --git a/app/javascript/widget/store/modules/specs/article/actions.spec.js b/app/javascript/widget/store/modules/specs/article/actions.spec.js new file mode 100644 index 000000000..bcf043085 --- /dev/null +++ b/app/javascript/widget/store/modules/specs/article/actions.spec.js @@ -0,0 +1,127 @@ +import { mutations, actions, getters } from '../../articles'; // update this import path to your actual module location +import { getMostReadArticles } from 'widget/api/article'; + +jest.mock('widget/api/article'); + +describe('Vuex Articles Module', () => { + let state; + + beforeEach(() => { + state = { + records: [], + uiFlags: { + isError: false, + hasFetched: false, + isFetching: false, + }, + }; + }); + + describe('Mutations', () => { + it('sets articles correctly', () => { + const articles = [{ id: 1 }, { id: 2 }]; + mutations.setArticles(state, articles); + expect(state.records).toEqual(articles); + }); + + it('sets error flag correctly', () => { + mutations.setError(state, true); + expect(state.uiFlags.isError).toBe(true); + }); + + it('sets fetching state correctly', () => { + mutations.setIsFetching(state, true); + expect(state.uiFlags.isFetching).toBe(true); + }); + + it('does not mutate records when no articles are provided', () => { + const previousState = { ...state }; + mutations.setArticles(state, []); + expect(state.records).toEqual(previousState.records); + }); + + it('toggles the error state correctly', () => { + mutations.setError(state, true); + expect(state.uiFlags.isError).toBe(true); + mutations.setError(state, false); + expect(state.uiFlags.isError).toBe(false); + }); + + it('toggles the fetching state correctly', () => { + mutations.setIsFetching(state, true); + expect(state.uiFlags.isFetching).toBe(true); + mutations.setIsFetching(state, false); + expect(state.uiFlags.isFetching).toBe(false); + }); + }); + + describe('Actions', () => { + it('fetches articles correctly', async () => { + const commit = jest.fn(); + const articles = [{ id: 1 }, { id: 2 }]; + getMostReadArticles.mockResolvedValueOnce({ + data: { payload: articles }, + }); + + await actions.fetch({ commit }, { slug: 'slug', locale: 'en' }); + + expect(commit).toHaveBeenCalledWith('setIsFetching', true); + expect(commit).toHaveBeenCalledWith('setError', false); + expect(commit).toHaveBeenCalledWith('setArticles', articles); + expect(commit).toHaveBeenCalledWith('setIsFetching', false); + }); + + it('handles fetch error correctly', async () => { + const commit = jest.fn(); + getMostReadArticles.mockRejectedValueOnce(new Error('Error message')); + + await actions.fetch( + { commit }, + { websiteToken: 'token', slug: 'slug', locale: 'en' } + ); + + expect(commit).toHaveBeenCalledWith('setIsFetching', true); + expect(commit).toHaveBeenCalledWith('setError', true); + expect(commit).toHaveBeenCalledWith('setIsFetching', false); + }); + + it('does not mutate state when fetching returns an empty payload', async () => { + const commit = jest.fn(); + getMostReadArticles.mockResolvedValueOnce({ data: { payload: [] } }); + + await actions.fetch({ commit }, { slug: 'slug', locale: 'en' }); + + expect(commit).toHaveBeenCalledWith('setIsFetching', true); + expect(commit).toHaveBeenCalledWith('setError', false); + expect(commit).not.toHaveBeenCalledWith('setArticles', expect.any(Array)); + expect(commit).toHaveBeenCalledWith('setIsFetching', false); + }); + + it('sets error state when fetching fails', async () => { + const commit = jest.fn(); + getMostReadArticles.mockRejectedValueOnce(new Error('Network error')); + + await actions.fetch( + { commit }, + { websiteToken: 'token', slug: 'slug', locale: 'en' } + ); + + expect(commit).toHaveBeenCalledWith('setIsFetching', true); + expect(commit).toHaveBeenCalledWith('setError', true); + expect(commit).not.toHaveBeenCalledWith('setArticles', expect.any(Array)); + expect(commit).toHaveBeenCalledWith('setIsFetching', false); + }); + }); + + describe('Getters', () => { + it('returns uiFlags correctly', () => { + const result = getters.uiFlags(state); + expect(result).toEqual(state.uiFlags); + }); + + it('returns popularArticles correctly', () => { + const result = getters.popularArticles(state); + expect(result).toEqual(state.records); + }); + }); +});