Skip to content

Commit

Permalink
creating useSnackbar composable and replacing existing logic with it
Browse files Browse the repository at this point in the history
  • Loading branch information
nathanaelg16 committed Aug 22, 2024
1 parent 6e61470 commit 24627da
Show file tree
Hide file tree
Showing 20 changed files with 248 additions and 54 deletions.
79 changes: 79 additions & 0 deletions kolibri/core/assets/src/composables/__mocks__/useSnackbar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* `useSnackbar` composable function mock.
*
* If default values are sufficient for tests,
* you only need call `jest.mock('<useSnackbar file path>')`
* 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 '<useSnackbar file path>';
*
* jest.mock('<useSnackbar file path>')
* 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
set(mocks.snackbarOptions, options);
};

const clearSnackbar = () => {
set(mocks.snackbarIsVisible, false);
set(mocks.snackbarOptions, {});
};

const setSnackbarText = text => {
set(mocks.snackbarOptions, { ...get(mocks.snackbarOptions), text });
};

return {
...mocks,
createSnackbar,
clearSnackbar,
setSnackbarText,
};
}

export default jest.fn(() => useSnackbarMock());
43 changes: 43 additions & 0 deletions kolibri/core/assets/src/composables/useSnackbar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
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
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,
};
}
2 changes: 2 additions & 0 deletions kolibri/core/assets/src/core-app/apiSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,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';

Expand Down Expand Up @@ -206,6 +207,7 @@ export default {
useNow,
useUser,
useUserSyncStatus,
useSnackbar,
},
},
resources,
Expand Down
11 changes: 7 additions & 4 deletions kolibri/core/assets/src/disconnection.js
Original file line number Diff line number Diff line change
@@ -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', {
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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);
}

Expand Down
4 changes: 3 additions & 1 deletion kolibri/core/assets/src/mixins/commonCoreStrings.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', {
Expand Down Expand Up @@ -1609,9 +1610,10 @@ 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,
});
Expand Down
8 changes: 3 additions & 5 deletions kolibri/core/assets/src/state/modules/core/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import CatchErrors from 'kolibri.utils.CatchErrors';
import Vue from 'kolibri.lib.vue';
import Lockr from 'lockr';
import { set } from '@vueuse/core';
import useSnackbar from 'kolibri.coreVue.composables.useSnackbar';
import { baseSessionState } from '../session';
import { LoginErrors, ERROR_CONSTANTS, UPDATE_MODAL_DISMISSED } from '../../../constants';
import { browser, os } from '../../../utils/browserInfo';
Expand Down Expand Up @@ -278,11 +279,8 @@ 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');
const snackbar = useSnackbar();
snackbar.createSnackbar({ text, autoDismiss: true });
}

export function loading(store) {
Expand Down
7 changes: 5 additions & 2 deletions kolibri/core/assets/src/views/CoreSnackbar/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@

<script>
import { mapActions } from 'vuex';
import UiSnackbar from 'kolibri-design-system/lib/keen/UiSnackbar.vue';
import Backdrop from 'kolibri.coreVue.components.Backdrop';
import useSnackbar from 'kolibri.coreVue.composables.useSnackbar';
/* Snackbars are used to display notification. */
export default {
Expand All @@ -51,6 +51,10 @@
Backdrop,
UiSnackbar,
},
setup() {
const { clearSnackbar } = useSnackbar();
return { clearSnackbar };
},
props: {
/* Text of notification to be displayed */
text: {
Expand Down Expand Up @@ -119,7 +123,6 @@
}
},
methods: {
...mapActions(['clearSnackbar']),
hideSnackbar() {
this.isVisible = false;
this.$emit('hide');
Expand Down
46 changes: 33 additions & 13 deletions kolibri/core/assets/test/heartbeat.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import coreStore from 'kolibri.coreVue.vuex.store';
import redirectBrowser from 'kolibri.utils.redirectBrowser';
import * as serverClock from 'kolibri.utils.serverClock';
import { get, set } from '@vueuse/core';
import useSnackbar, { useSnackbarMock } from 'kolibri.coreVue.composables.useSnackbar';
import { ref } from 'kolibri.lib.vueCompositionApi';
import { HeartBeat } from '../src/heartbeat.js';
import disconnectionErrorCodes from '../src/disconnectionErrorCodes';
import { trs } from '../src/disconnection';
Expand All @@ -13,6 +15,7 @@ jest.mock('kolibri.lib.logging');
jest.mock('kolibri.utils.redirectBrowser');
jest.mock('kolibri.urls');
jest.mock('lockr');
jest.mock('kolibri.coreVue.composables.useSnackbar');

coreStore.registerModule('core', coreModule);

Expand Down Expand Up @@ -161,7 +164,17 @@ describe('HeartBeat', function () {
});
});
describe('monitorDisconnect method', function () {
let heartBeat;
let heartBeat, snackbar;
beforeAll(function () {
snackbar = {
snackbarIsVisible: ref(false),
snackbarOptions: ref({
text: '',
autoDismiss: true,
}),
};
useSnackbar.mockImplementation(() => useSnackbarMock(snackbar));
});
beforeEach(function () {
heartBeat = new HeartBeat();
jest.spyOn(heartBeat, '_wait').mockImplementation(() => {});
Expand All @@ -174,9 +187,9 @@ describe('HeartBeat', function () {
expect(get(heartBeat._connection.reconnectTime)).not.toEqual(null);
});
it('should set current snackbar to disconnected', function () {
expect(coreStore.getters.snackbarIsVisible).toEqual(true);
expect(get(snackbar.snackbarIsVisible)).toEqual(true);
expect(
coreStore.getters.snackbarOptions.text.startsWith(
get(snackbar.snackbarOptions).text.startsWith(
'Disconnected from server. Will try to reconnect in',
),
).toEqual(true);
Expand Down Expand Up @@ -267,13 +280,22 @@ describe('HeartBeat', function () {
});
});
describe('when not connected', function () {
let snackbar;
beforeEach(function () {
snackbar = {
snackbarIsVisible: ref(false),
snackbarOptions: ref({
text: '',
autoDismiss: true,
}),
};
useSnackbar.mockImplementation(() => useSnackbarMock(snackbar));
heartBeat.monitorDisconnect();
});
it('should set snackbar to trying to reconnect', function () {
heartBeat._checkSession();
expect(coreStore.getters.snackbarIsVisible).toEqual(true);
expect(coreStore.getters.snackbarOptions.text).toEqual(trs.$tr('tryingToReconnect'));
expect(get(snackbar.snackbarIsVisible)).toEqual(true);
expect(get(snackbar.snackbarOptions).text).toEqual(trs.$tr('tryingToReconnect'));
});
disconnectionErrorCodes
.filter(code => code !== 0)
Expand All @@ -286,9 +308,9 @@ describe('HeartBeat', function () {
});
heartBeat._wait = jest.fn();
return heartBeat._checkSession().finally(() => {
expect(coreStore.getters.snackbarIsVisible).toEqual(true);
expect(get(snackbar.snackbarIsVisible)).toEqual(true);
expect(
coreStore.getters.snackbarOptions.text.startsWith(
get(snackbar.snackbarOptions).text.startsWith(
'Disconnected from server. Will try to reconnect in',
),
).toEqual(true);
Expand All @@ -299,9 +321,9 @@ describe('HeartBeat', function () {
jest.spyOn(heartBeat, 'monitorDisconnect');
mock.put(/.*/, () => Promise.reject(new Error()));
return heartBeat._checkSession().finally(() => {
expect(coreStore.getters.snackbarIsVisible).toEqual(true);
expect(get(snackbar.snackbarIsVisible)).toEqual(true);
expect(
coreStore.getters.snackbarOptions.text.startsWith(
get(snackbar.snackbarOptions).text.startsWith(
'Disconnected from server. Will try to reconnect in',
),
).toEqual(true);
Expand All @@ -326,10 +348,8 @@ describe('HeartBeat', function () {
});
it('should set snackbar to reconnected', function () {
return heartBeat._checkSession().finally(() => {
expect(coreStore.getters.snackbarIsVisible).toEqual(true);
expect(coreStore.getters.snackbarOptions.text).toEqual(
trs.$tr('successfullyReconnected'),
);
expect(get(snackbar.snackbarIsVisible)).toEqual(true);
expect(get(snackbar.snackbarOptions).text).toEqual(trs.$tr('successfullyReconnected'));
});
});
it('should set connected to true', function () {
Expand Down
6 changes: 4 additions & 2 deletions kolibri/plugins/coach/assets/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import redirectBrowser from 'kolibri.utils.redirectBrowser';
import { setChannelInfo } from 'kolibri.coreVue.vuex.actions';
import router from 'kolibri.coreVue.router';
import KolibriApp from 'kolibri_app';
import useSnackbar from 'kolibri.coreVue.composables.useSnackbar';
import { PageNames } from './constants';
import routes from './routes';
import pluginModule from './modules/pluginModule';
Expand All @@ -26,6 +27,7 @@ class CoachToolsModule extends KolibriApp {
return pluginModule;
}
ready() {
const { snackbarIsVisibile, clearSnackbar } = useSnackbar();
const { isLearnerOnlyImport } = useUser();
router.beforeEach((to, from, next) => {
if (get(isLearnerOnlyImport)) {
Expand Down Expand Up @@ -54,8 +56,8 @@ class CoachToolsModule extends KolibriApp {

// Clear the snackbar at every navigation to prevent it from re-appearing
// when the next page component mounts.
if (this.store.state.core.snackbar.isVisible && !skipLoading.includes(to.name)) {
this.store.dispatch('clearSnackbar');
if (get(snackbarIsVisibile) && !skipLoading.includes(to.name)) {
clearSnackbar();
}

this.store.commit('SET_PAGE_NAME', to.name);
Expand Down
Loading

0 comments on commit 24627da

Please sign in to comment.