Skip to content

Commit

Permalink
creating useTotalProgress composable and migrating code to use it
Browse files Browse the repository at this point in the history
  • Loading branch information
nathanaelg16 committed Aug 30, 2024
1 parent 65db937 commit 4515dc2
Show file tree
Hide file tree
Showing 14 changed files with 162 additions and 85 deletions.
62 changes: 62 additions & 0 deletions kolibri/core/assets/src/composables/__mocks__/useTotalProgress.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* `useTotalProgress` composable function mock.
*
* If default values are sufficient for tests,
* you only need call `jest.mock('<useTotalProgress 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 `useTotalProgressMock` that accepts
* an object with values to be overriden and use it together
* with `mockImplementation` as follows:
*
* ```
* // eslint-disable-next-line import/named
* import useTotalProgress, { useTotalProgressMock } from '<useTotalProgress file path>';
*
* jest.mock('<useTotalProgress file path>')
* describe('describe test', function () {
* let totalProgressMock = { totalProgress: ref(null) }
*
* beforeAll(() => {
* useTotalProgress.mockImplementation(() => useTotalProgressMock(totalProgressMock)
* })
*
* it('the test', () => {
* expect(get(totalProgressMock.totalProgress)).toEqual(null);
* )
* })
* ```
*/
import { ref, computed } from 'kolibri.lib.vueCompositionApi';
import { get, set } from '@vueuse/core';
import { MaxPointsPerContent } from '../../constants';

const MOCK_DEFAULTS = {
totalProgress: ref(null),
};

export function useTotalProgressMock(overrides = {}) {
const mocks = {
...MOCK_DEFAULTS,
...overrides,
};

const totalPoints = computed(() => mocks.totalProgress.value * MaxPointsPerContent);

const fetchPoints = jest.fn();

const incrementTotalProgress = progress => {
set(mocks.totalProgress, get(mocks.totalProgress) + progress);
};

return {
totalPoints,
fetchPoints,
incrementTotalProgress,
...mocks,
};
}

export default jest.fn(() => useTotalProgressMock());
31 changes: 31 additions & 0 deletions kolibri/core/assets/src/composables/useTotalProgress.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { ref, computed } from 'kolibri.lib.vueCompositionApi';
import { get, set } from '@vueuse/core';
import useUser from 'kolibri.coreVue.composables.useUser';
import { UserProgressResource } from 'kolibri.resources';
import { MaxPointsPerContent } from '../constants';

const totalProgress = ref(null);

export default function useTotalProgress() {
const totalPoints = computed(() => totalProgress.value * MaxPointsPerContent);

const fetchPoints = () => {
const { isUserLoggedIn, currentUserId } = useUser();
if (get(isUserLoggedIn) && get(totalProgress) === null) {
UserProgressResource.fetchModel({ id: get(currentUserId) }).then(progress => {
set(totalProgress, progress.progress);
});
}
};

const incrementTotalProgress = progress => {
set(totalProgress, get(totalProgress) + progress);
};

return {
totalProgress,
totalPoints,
fetchPoints,
incrementTotalProgress,
};
}
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 @@ -92,6 +92,7 @@ import useMinimumKolibriVersion from '../composables/useMinimumKolibriVersion';
import useUserSyncStatus from '../composables/useUserSyncStatus';
import useUser from '../composables/useUser';
import useSnackbar from '../composables/useSnackbar';
import useTotalProgress from '../composables/useTotalProgress';
import { registerNavItem } from '../composables/useNav';
import useNow from '../composables/useNow';

Expand Down Expand Up @@ -204,6 +205,7 @@ export default {
useUser,
useUserSyncStatus,
useSnackbar,
useTotalProgress,
},
},
resources,
Expand Down
10 changes: 0 additions & 10 deletions kolibri/core/assets/src/state/modules/core/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import logger from 'kolibri.lib.logging';
import {
FacilityResource,
FacilityDatasetResource,
UserProgressResource,
UserSyncStatusResource,
PingbackNotificationResource,
PingbackNotificationDismissedResource,
Expand Down Expand Up @@ -246,15 +245,6 @@ export function getFacilityConfig(store, facilityId) {
});
}

export function fetchPoints(store) {
const { isUserLoggedIn, currentUserId } = store.getters;
if (isUserLoggedIn && store.state.totalProgress === null) {
UserProgressResource.fetchModel({ id: currentUserId }).then(progress => {
store.commit('SET_TOTAL_PROGRESS', progress.progress);
});
}
}

export function loading(store) {
return new Promise(resolve => {
store.commit('CORE_SET_PAGE_LOADING', true);
Expand Down
5 changes: 0 additions & 5 deletions kolibri/core/assets/src/state/modules/core/getters.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import useUser from 'kolibri.coreVue.composables.useUser';
import { get } from '@vueuse/core';
import { MaxPointsPerContent } from '../../../constants';

export function facilityConfig(state) {
return state.facilityConfig;
Expand All @@ -10,10 +9,6 @@ export function facilities(state) {
return state.facilities;
}

export function totalPoints(state) {
return state.totalProgress * MaxPointsPerContent;
}

export function pageSessionId(state) {
return state.pageSessionId;
}
Expand Down
1 change: 0 additions & 1 deletion kolibri/core/assets/src/state/modules/core/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ export default {
error: '',
loading: true,
pageSessionId: 0,
totalProgress: null,
notifications: [],
allowRemoteAccess: plugin_data.allowRemoteAccess,
// facility
Expand Down
6 changes: 0 additions & 6 deletions kolibri/core/assets/src/state/modules/core/mutations.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,6 @@ export default {
CORE_SET_ERROR(state, error) {
state.error = error;
},
SET_TOTAL_PROGRESS(state, progress) {
state.totalProgress = progress;
},
INCREMENT_TOTAL_PROGRESS(state, progress) {
state.totalProgress += progress;
},
CORE_SET_NOTIFICATIONS(state, notifications) {
state.notifications = notifications;
},
Expand Down
7 changes: 4 additions & 3 deletions kolibri/core/assets/src/views/AppBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -127,14 +127,14 @@

<script>
import { mapActions, mapGetters } from 'vuex';
import { get } from '@vueuse/core';
import { computed, getCurrentInstance } from 'kolibri.lib.vueCompositionApi';
import commonCoreStrings from 'kolibri.coreVue.mixins.commonCoreStrings';
import UiToolbar from 'kolibri.coreVue.components.UiToolbar';
import KIconButton from 'kolibri-design-system/lib/buttons-and-links/KIconButton';
import themeConfig from 'kolibri.themeConfig';
import useKResponsiveWindow from 'kolibri-design-system/lib/composables/useKResponsiveWindow';
import useTotalProgress from 'kolibri.coreVue.composables.useTotalProgress';
import useNav from '../composables/useNav';
import useUser from '../composables/useUser';
import SkipNavigationLink from './SkipNavigationLink';
Expand All @@ -157,6 +157,7 @@
const { windowIsLarge, windowIsSmall } = useKResponsiveWindow();
const { topBarHeight, navItems } = useNav();
const { isLearner, isUserLoggedIn, username, full_name } = useUser();
const { totalPoints, fetchPoints } = useTotalProgress();
const links = computed(() => {
const currentItem = navItems.find(nc => nc.url === window.location.pathname);
if (!currentItem || !currentItem.routes) {
Expand All @@ -179,6 +180,8 @@
isLearner,
username,
fullName: full_name,
totalPoints,
fetchPoints,
};
},
props: {
Expand All @@ -201,7 +204,6 @@
};
},
computed: {
...mapGetters(['totalPoints']),
// temp hack for the VF plugin
usernameForDisplay() {
return !hashedValuePattern.test(this.username) ? this.username : this.fullName;
Expand All @@ -219,7 +221,6 @@
window.removeEventListener('keydown', this.handlePopoverByKeyboard, true);
},
methods: {
...mapActions(['fetchPoints']),
handleWindowClick(event) {
if (this.$refs.pointsButton && this.$refs.pointsButton.$el) {
if (!this.$refs.pointsButton.$el.contains(event.target) && this.pointsDisplayed) {
Expand Down
16 changes: 8 additions & 8 deletions kolibri/core/assets/src/views/TotalPoints.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,20 @@

<script>
import { mapGetters, mapActions } from 'vuex';
import useUser from 'kolibri.coreVue.composables.useUser';
import useTotalProgress from 'kolibri.coreVue.composables.useTotalProgress';
export default {
name: 'TotalPoints',
setup() {
const { currentUserId, isUserLoggedIn } = useUser();
return { currentUserId, isUserLoggedIn };
},
computed: {
...mapGetters(['totalPoints']),
const { fetchPoints, totalPoints } = useTotalProgress();
return {
currentUserId,
isUserLoggedIn,
fetchPoints,
totalPoints,
};
},
watch: {
currentUserId() {
Expand All @@ -50,9 +53,6 @@
created() {
this.fetchPoints();
},
methods: {
...mapActions(['fetchPoints']),
},
$trs: {
pointsTooltip: {
message: 'You earned { points, number } points',
Expand Down
65 changes: 23 additions & 42 deletions kolibri/core/assets/src/views/__tests__/TotalPoints.spec.js
Original file line number Diff line number Diff line change
@@ -1,77 +1,58 @@
import { render, screen, fireEvent } from '@testing-library/vue';
import useUser, { useUserMock } from 'kolibri.coreVue.composables.useUser';
import TotalPoints from '../TotalPoints.vue';
import useTotalProgress, {
useTotalProgressMock,
} from 'kolibri.coreVue.composables.useTotalProgress';
import '@testing-library/jest-dom';

let store, storeActions;
import { ref } from 'kolibri.lib.vueCompositionApi';
import { get, set } from '@vueuse/core';
import TotalPoints from '../TotalPoints.vue';

jest.mock('kolibri.coreVue.composables.useUser');

// Create a mock Vuex store with the required getters and actions
// This is a helper function to avoid create a new store for each test and not reuse the same object
const getMockStore = () => {
return {
getters: {
totalPoints: () => store.totalPoints,
},
actions: {
fetchPoints: storeActions.fetchPoints,
},
};
};

// Helper function to render the component with Vuex store
const renderComponent = store => {
return render(TotalPoints, {
store,
});
};
jest.mock('kolibri.coreVue.composables.useTotalProgress');

describe('TotalPoints', () => {
let totalPointsMock;
beforeEach(() => {
jest.clearAllMocks();
useUser.mockImplementation(() => useUserMock());
store = {
totalPoints: 0,
};

storeActions = {
fetchPoints: jest.fn(),
};
totalPointsMock = { totalPoints: ref(0), fetchPoints: jest.fn() };
useTotalProgress.mockImplementation(() => useTotalProgressMock(totalPointsMock));
});

test('renders when user is logged in', async () => {
useUser.mockImplementation(() => useUserMock({ currentUserId: 1, isUserLoggedIn: true }));
store.totalPoints = 100;
renderComponent(getMockStore());
set(totalPointsMock.totalPoints, 100);
render(TotalPoints);

expect(screen.getByRole('presentation')).toBeInTheDocument();
expect(screen.getByText(store.totalPoints)).toBeInTheDocument();
expect(screen.getByText(get(totalPointsMock.totalPoints))).toBeInTheDocument();
});

test('does not render when user is not logged in', async () => {
useUser.mockImplementation(() => useUserMock({ currentUserId: 1, isUserLoggedIn: false }));
store.totalPoints = 100;
renderComponent(getMockStore());
set(totalPointsMock.totalPoints, 100);
render(TotalPoints);

expect(screen.queryByRole('presentation')).not.toBeInTheDocument();
expect(screen.queryByText(store.totalPoints)).not.toBeInTheDocument();
expect(screen.queryByText(get(totalPointsMock.totalPoints))).not.toBeInTheDocument();
});

test('fetchPoints method is called on created', async () => {
useUser.mockImplementation(() => useUserMock({ currentUserId: 1, isUserLoggedIn: true }));
const mockedStore = getMockStore();
renderComponent(mockedStore);
render(TotalPoints);

expect(mockedStore.actions.fetchPoints).toHaveBeenCalledTimes(1);
expect(totalPointsMock.fetchPoints).toHaveBeenCalledTimes(1);
});

test('tooltip message is displayed correctly when the mouse hovers over the icon', async () => {
useUser.mockImplementation(() => useUserMock({ currentUserId: 1, isUserLoggedIn: true }));
store.totalPoints = 100;
renderComponent(getMockStore());
set(totalPointsMock.totalPoints, 100);
render(TotalPoints);

await fireEvent.mouseOver(screen.getByRole('presentation'));
expect(screen.getByText(`You earned ${store.totalPoints} points`)).toBeInTheDocument();
expect(
screen.getByText(`You earned ${get(totalPointsMock.totalPoints)} points`),
).toBeInTheDocument();
});
});
Loading

0 comments on commit 4515dc2

Please sign in to comment.