diff --git a/kolibri/core/assets/src/composables/__mocks__/useSnackbar.js b/kolibri/core/assets/src/composables/__mocks__/useSnackbar.js new file mode 100644 index 00000000000..c12bd0c70d6 --- /dev/null +++ b/kolibri/core/assets/src/composables/__mocks__/useSnackbar.js @@ -0,0 +1,85 @@ +/** + * `useSnackbar` composable function mock. + * + * If default values are sufficient for tests, + * you only need call `jest.mock('')` + * at the top of a test file. + * + * If you need to override some default values for some tests, + * or if you need to inspect the state of the refs during tests, + * you can import a helper function `useSnackbarMock` that accepts + * an object with values to be overriden and use it together + * with `mockImplementation` as follows: + * + * ``` + * // eslint-disable-next-line import/named + * import useSnackbar, { useSnackbarMock } from ''; + * + * jest.mock('') + * describe('describe test', function () { + * let snackbar = { snackbarIsVisible: ref(false) } + * + * beforeAll(() => { + * useSnackbar.mockImplementation(() => useSnackbarMock(snackbar) + * }) + * + * it('the test', () => { + * expect(get(snackbar.snackbarIsVisible)).toEqual(false); + * ) + * }) + * ``` + */ +import { ref } from 'kolibri.lib.vueCompositionApi'; +import { get, set } from '@vueuse/core'; + +const MOCK_DEFAULTS = { + snackbarIsVisible: ref(false), + snackbarOptions: ref({ + text: '', + autoDismiss: true, + }), +}; + +export function useSnackbarMock(overrides = {}) { + const mocks = { + ...MOCK_DEFAULTS, + ...overrides, + }; + + const createSnackbar = (options = {}) => { + // reset + set(mocks.snackbarIsVisible, false); + set(mocks.snackbarOptions, {}); + + // set new options + set(mocks.snackbarIsVisible, true); + + // options include text, autoDismiss, duration, actionText, actionCallback, + // hideCallback, bottomPosition + // if the options are a string, set it as the snackbar text + // and default autoDismiss to true + if (typeof options === 'string') { + set(mocks.snackbarOptions, { text: options, autoDismiss: true }); + } else { + set(mocks.snackbarOptions, options); + } + }; + + const clearSnackbar = () => { + set(mocks.snackbarIsVisible, false); + set(mocks.snackbarOptions, {}); + }; + + const setSnackbarText = text => { + set(mocks.snackbarOptions, { ...get(mocks.snackbarOptions), text }); + }; + + return { + createSnackbar, + clearSnackbar, + setSnackbarText, + ...mocks, + }; +} + +export default jest.fn(() => useSnackbarMock()); diff --git a/kolibri/core/assets/src/composables/useSnackbar.js b/kolibri/core/assets/src/composables/useSnackbar.js new file mode 100644 index 00000000000..640a496fc02 --- /dev/null +++ b/kolibri/core/assets/src/composables/useSnackbar.js @@ -0,0 +1,49 @@ +import { ref } from 'kolibri.lib.vueCompositionApi'; +import { get, set } from '@vueuse/core'; + +const snackbarIsVisible = ref(false); +const snackbarOptions = ref({ + text: '', + autoDismiss: true, +}); + +export default function useSnackbar() { + const createSnackbar = (options = {}) => { + // reset + set(snackbarIsVisible, false); + set(snackbarOptions, {}); + + // set new options + set(snackbarIsVisible, true); + + // options include text, autoDismiss, duration, actionText, actionCallback, + // hideCallback, bottomPosition + // if the options are a string, set it as the snackbar text + // and default autoDismiss to true + if (typeof options === 'string') { + set(snackbarOptions, { text: options, autoDismiss: true }); + } else { + set(snackbarOptions, options); + } + }; + + const clearSnackbar = () => { + set(snackbarIsVisible, false); + set(snackbarOptions, {}); + }; + + const setSnackbarText = text => { + set(snackbarOptions, { ...get(snackbarOptions), text }); + }; + + return { + // state + snackbarIsVisible, + snackbarOptions, + + // mutators + createSnackbar, + clearSnackbar, + setSnackbarText, + }; +} diff --git a/kolibri/core/assets/src/core-app/apiSpec.js b/kolibri/core/assets/src/core-app/apiSpec.js index c3cbfb29f37..07d4a82d6d2 100644 --- a/kolibri/core/assets/src/core-app/apiSpec.js +++ b/kolibri/core/assets/src/core-app/apiSpec.js @@ -91,6 +91,7 @@ import NotificationsRoot from '../views/NotificationsRoot'; import useMinimumKolibriVersion from '../composables/useMinimumKolibriVersion'; import useUserSyncStatus from '../composables/useUserSyncStatus'; import useUser from '../composables/useUser'; +import useSnackbar from '../composables/useSnackbar'; import { registerNavItem } from '../composables/useNav'; import useNow from '../composables/useNow'; @@ -202,6 +203,7 @@ export default { useNow, useUser, useUserSyncStatus, + useSnackbar, }, }, resources, diff --git a/kolibri/core/assets/src/disconnection.js b/kolibri/core/assets/src/disconnection.js index df53e9de210..9eb2ebb47d8 100644 --- a/kolibri/core/assets/src/disconnection.js +++ b/kolibri/core/assets/src/disconnection.js @@ -1,5 +1,6 @@ import { createTranslator } from 'kolibri.utils.i18n'; import { get } from '@vueuse/core'; +import useSnackbar from 'kolibri.coreVue.composables.useSnackbar'; import useConnection from './composables/useConnection'; export const trs = createTranslator('DisconnectionSnackbars', { @@ -24,8 +25,9 @@ export const trs = createTranslator('DisconnectionSnackbars', { }, }); -export function createTryingToReconnectSnackbar(store) { - store.commit('CORE_CREATE_SNACKBAR', { +export function createTryingToReconnectSnackbar() { + const { createSnackbar } = useSnackbar(); + createSnackbar({ text: trs.$tr('tryingToReconnect'), backdrop: true, autoDismiss: false, @@ -42,7 +44,8 @@ export function createDisconnectedSnackbar(store, beatCallback) { const { reconnectTime } = useConnection(); setDynamicReconnectTime(get(reconnectTime)); // create snackbar - store.commit('CORE_CREATE_SNACKBAR', { + const { createSnackbar, setSnackbarText } = useSnackbar(); + createSnackbar({ text: generateDisconnectedSnackbarText(), actionText: trs.$tr('tryNow'), actionCallback: beatCallback, @@ -53,7 +56,7 @@ export function createDisconnectedSnackbar(store, beatCallback) { // start timeout timer = setInterval(() => { setDynamicReconnectTime(dynamicReconnectTime - 1); - store.commit('CORE_SET_SNACKBAR_TEXT', generateDisconnectedSnackbarText()); + setSnackbarText(generateDisconnectedSnackbarText()); }, 1000); } @@ -73,7 +76,8 @@ function clearTimer() { } } -export function createReconnectedSnackbar(store) { +export function createReconnectedSnackbar() { clearTimer(); - store.dispatch('createSnackbar', trs.$tr('successfullyReconnected')); + const { createSnackbar } = useSnackbar(); + createSnackbar(trs.$tr('successfullyReconnected')); } diff --git a/kolibri/core/assets/src/heartbeat.js b/kolibri/core/assets/src/heartbeat.js index 800957d6445..b10282cb845 100644 --- a/kolibri/core/assets/src/heartbeat.js +++ b/kolibri/core/assets/src/heartbeat.js @@ -251,7 +251,7 @@ export class HeartBeat { _setConnected() { set(this._connection.connected, true); set(this._connection.reconnectTime, null); - createReconnectedSnackbar(store); + createReconnectedSnackbar(); if (get(this._connection.reloadOnReconnect)) { // If we were disconnected while loading, we need to reload the page // to ensure that we are in a consistent state. diff --git a/kolibri/core/assets/src/mixins/commonCoreStrings.js b/kolibri/core/assets/src/mixins/commonCoreStrings.js index a05091a5154..67bdae9bfb5 100644 --- a/kolibri/core/assets/src/mixins/commonCoreStrings.js +++ b/kolibri/core/assets/src/mixins/commonCoreStrings.js @@ -3,6 +3,7 @@ import camelCase from 'lodash/camelCase'; import get from 'lodash/get'; import invert from 'lodash/invert'; import * as METADATA from 'kolibri.coreVue.vuex.constants'; +import useSnackbar from 'kolibri.coreVue.composables.useSnackbar'; import notificationStrings from './notificationStrings'; export const coreStrings = createTranslator('CommonCoreStrings', { @@ -1609,14 +1610,15 @@ export default { * `CORE_CREATE_SNACKBAR` mutation. */ showSnackbarNotification(key, args, coreCreateSnackbarArgs) { + const { createSnackbar } = useSnackbar(); const text = notificationStrings.$tr(key, args || {}); if (coreCreateSnackbarArgs) { - this.$store.commit('CORE_CREATE_SNACKBAR', { + createSnackbar({ ...coreCreateSnackbarArgs, text, }); } else { - this.$store.dispatch('createSnackbar', text); + createSnackbar(text); } }, }, diff --git a/kolibri/core/assets/src/state/modules/core/actions.js b/kolibri/core/assets/src/state/modules/core/actions.js index 80321be670c..643aa911d36 100644 --- a/kolibri/core/assets/src/state/modules/core/actions.js +++ b/kolibri/core/assets/src/state/modules/core/actions.js @@ -276,15 +276,6 @@ export function fetchPoints(store) { } } -// Creates a snackbar that automatically dismisses and has no action buttons. -export function createSnackbar(store, text) { - store.commit('CORE_CREATE_SNACKBAR', { text, autoDismiss: true }); -} - -export function clearSnackbar(store) { - store.commit('CORE_CLEAR_SNACKBAR'); -} - export function loading(store) { return new Promise(resolve => { store.commit('CORE_SET_PAGE_LOADING', true); diff --git a/kolibri/core/assets/src/state/modules/core/index.js b/kolibri/core/assets/src/state/modules/core/index.js index 87c56477b58..8a597501e83 100644 --- a/kolibri/core/assets/src/state/modules/core/index.js +++ b/kolibri/core/assets/src/state/modules/core/index.js @@ -1,5 +1,4 @@ import sessionModule from '../session'; -import snackbarModule from '../snackbar'; import * as getters from './getters'; import * as actions from './actions'; import mutations from './mutations'; @@ -29,6 +28,5 @@ export default { mutations, modules: { session: sessionModule, - snackbar: snackbarModule, }, }; diff --git a/kolibri/core/assets/src/state/modules/snackbar.js b/kolibri/core/assets/src/state/modules/snackbar.js deleted file mode 100644 index 9a853d985f3..00000000000 --- a/kolibri/core/assets/src/state/modules/snackbar.js +++ /dev/null @@ -1,36 +0,0 @@ -export default { - state: { - isVisible: false, - options: { - text: '', - autoDismiss: true, - }, - }, - getters: { - snackbarIsVisible(state) { - return state.isVisible; - }, - snackbarOptions(state) { - return state.options; - }, - }, - mutations: { - CORE_CREATE_SNACKBAR(state, snackbarOptions = {}) { - // reset - state.isVisible = false; - state.options = {}; - // set new options - state.isVisible = true; - // options include text, autoDismiss, duration, actionText, actionCallback, - // hideCallback, bottomPosition - state.options = snackbarOptions; - }, - CORE_CLEAR_SNACKBAR(state) { - state.isVisible = false; - state.options = {}; - }, - CORE_SET_SNACKBAR_TEXT(state, text) { - state.options.text = text; - }, - }, -}; diff --git a/kolibri/core/assets/src/views/CoreSnackbar/index.vue b/kolibri/core/assets/src/views/CoreSnackbar/index.vue index 6daa93d4fd5..f6ef459fe63 100644 --- a/kolibri/core/assets/src/views/CoreSnackbar/index.vue +++ b/kolibri/core/assets/src/views/CoreSnackbar/index.vue @@ -40,9 +40,9 @@