From 4d4a0002268ed06e07d7443dfafdfd1e0097112a Mon Sep 17 00:00:00 2001 From: Eshaan Aggarwal <96648934+EshaanAgg@users.noreply.github.com> Date: Wed, 21 Feb 2024 01:59:51 +0530 Subject: [PATCH 1/6] tests: add tests for TotalPoints --- .../src/views/__tests__/TotalPoints.spec.js | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 kolibri/core/assets/src/views/__tests__/TotalPoints.spec.js diff --git a/kolibri/core/assets/src/views/__tests__/TotalPoints.spec.js b/kolibri/core/assets/src/views/__tests__/TotalPoints.spec.js new file mode 100644 index 00000000000..f035244bc60 --- /dev/null +++ b/kolibri/core/assets/src/views/__tests__/TotalPoints.spec.js @@ -0,0 +1,61 @@ +import VueRouter from 'vue-router'; +import { render, screen, fireEvent } from '@testing-library/vue'; +import TotalPoints from '../TotalPoints.vue'; +import '@testing-library/jest-dom'; + +// 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: () => 100, + currentUserId: () => 'user123', + isUserLoggedIn: () => true, + }, + actions: { + fetchPoints: jest.fn(), + }, + }; +}; + +// Helper function to render the component with Vuex store +const renderComponent = store => { + return render(TotalPoints, { + store, + routes: new VueRouter(), + }); +}; + +describe('TotalPoints', () => { + test('renders when user is logged in', async () => { + const store = getMockStore(); + renderComponent(store); + + expect(screen.getByRole('presentation')).toBeInTheDocument(); + expect(screen.getByText('100')).toBeInTheDocument(); + }); + + test('does not render when user is not logged in', async () => { + const store = getMockStore(); + store.getters.isUserLoggedIn = () => false; + renderComponent(store); + + expect(screen.queryByRole('presentation')).not.toBeInTheDocument(); + expect(screen.queryByText('100')).not.toBeInTheDocument(); + }); + + test('fetchPoints method is called on created', async () => { + const store = getMockStore(); + renderComponent(store); + + expect(store.actions.fetchPoints).toHaveBeenCalledTimes(1); + }); + + test('tooltip message is displayed correctly when the mouse hovers over the icon', async () => { + const store = getMockStore(); + renderComponent(store); + + await fireEvent.mouseOver(screen.getByRole('presentation')); + expect(screen.getByText('You earned 100 points')).toBeInTheDocument(); + }); +}); From cb0f81387b59afeb3cde73536a2c4cb5ca3f095b Mon Sep 17 00:00:00 2001 From: Eshaan Aggarwal <96648934+EshaanAgg@users.noreply.github.com> Date: Wed, 21 Feb 2024 11:32:42 +0530 Subject: [PATCH 2/6] test: add more tests --- .../__tests__/GenderDisplayText.spec.js | 56 +++++++++++++++++++ .../__tests__/GenderSelect.spec.js | 21 +++++++ 2 files changed, 77 insertions(+) create mode 100644 kolibri/core/assets/src/views/userAccounts/__tests__/GenderDisplayText.spec.js create mode 100644 kolibri/core/assets/src/views/userAccounts/__tests__/GenderSelect.spec.js diff --git a/kolibri/core/assets/src/views/userAccounts/__tests__/GenderDisplayText.spec.js b/kolibri/core/assets/src/views/userAccounts/__tests__/GenderDisplayText.spec.js new file mode 100644 index 00000000000..685189c7b5b --- /dev/null +++ b/kolibri/core/assets/src/views/userAccounts/__tests__/GenderDisplayText.spec.js @@ -0,0 +1,56 @@ +import VueRouter from 'vue-router'; +import { render, screen } from '@testing-library/vue'; +import { FacilityUserGender } from 'kolibri.coreVue.vuex.constants'; +import GenderDisplayText from '../GenderDisplayText.vue'; +import '@testing-library/jest-dom'; + +const renderComponent = gender => { + return render(GenderDisplayText, { + routes: new VueRouter(), + props: { + gender, + }, + }); +}; + +describe('GenderDisplayText', () => { + const testCases = [ + { + name: 'does not render when gender is not specified', + gender: FacilityUserGender.NOT_SPECIFIED, + shouldContain: [], + shouldNotContain: ['Male', 'Female'], + }, + { + name: 'does not render when gender is deffered', + gender: FacilityUserGender.DEFERRED, + shouldContain: [], + shouldNotContain: ['Male', 'Female'], + }, + { + name: 'renders when the gender is male', + gender: FacilityUserGender.MALE, + shouldContain: ['Male'], + shouldNotContain: ['Female'], + }, + { + name: 'renders when the gender is female', + gender: FacilityUserGender.FEMALE, + shouldContain: ['Female'], + shouldNotContain: ['Male'], + }, + ]; + + testCases.forEach(testCase => { + it(testCase.name, () => { + renderComponent(testCase.gender); + + testCase.shouldContain.forEach(text => { + expect(screen.getByText(text)).toBeInTheDocument(); + }); + testCase.shouldNotContain.forEach(text => { + expect(screen.queryByText(text)).not.toBeInTheDocument(); + }); + }); + }); +}); diff --git a/kolibri/core/assets/src/views/userAccounts/__tests__/GenderSelect.spec.js b/kolibri/core/assets/src/views/userAccounts/__tests__/GenderSelect.spec.js new file mode 100644 index 00000000000..bc8cc348ae5 --- /dev/null +++ b/kolibri/core/assets/src/views/userAccounts/__tests__/GenderSelect.spec.js @@ -0,0 +1,21 @@ +import { render, screen } from '@testing-library/vue'; +import VueRouter from 'vue-router'; +import GenderSelect from '../GenderSelect.vue'; +import '@testing-library/jest-dom'; + +const renderComponent = () => { + return render(GenderSelect, { + routes: new VueRouter(), + }); +}; + +describe('GenderSelect', () => { + test('renders correctly with label placeholder and options', async () => { + renderComponent(); + + expect(screen.getByText('Gender')).toBeInTheDocument(); + expect(screen.getByText('Male')).toBeInTheDocument(); + expect(screen.getByText('Female')).toBeInTheDocument(); + expect(screen.getByText('Not specified')).toBeInTheDocument(); + }); +}); From ee096c0316058020ceefa38dd9f4c59402892366 Mon Sep 17 00:00:00 2001 From: Eshaan Aggarwal <96648934+EshaanAgg@users.noreply.github.com> Date: Thu, 29 Feb 2024 15:15:32 +0530 Subject: [PATCH 3/6] feat: migrate the test to new pattern --- .../src/views/__tests__/TotalPoints.spec.js | 50 ++++++++++++------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/kolibri/core/assets/src/views/__tests__/TotalPoints.spec.js b/kolibri/core/assets/src/views/__tests__/TotalPoints.spec.js index f035244bc60..13041c388a8 100644 --- a/kolibri/core/assets/src/views/__tests__/TotalPoints.spec.js +++ b/kolibri/core/assets/src/views/__tests__/TotalPoints.spec.js @@ -3,17 +3,19 @@ import { render, screen, fireEvent } from '@testing-library/vue'; import TotalPoints from '../TotalPoints.vue'; import '@testing-library/jest-dom'; +let store, storeActions; + // 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: () => 100, - currentUserId: () => 'user123', - isUserLoggedIn: () => true, + totalPoints: () => store.totalPoints, + currentUserId: () => store.currentUserId, + isUserLoggedIn: () => store.isUserLoggedIn, }, actions: { - fetchPoints: jest.fn(), + fetchPoints: storeActions.fetchPoints, }, }; }; @@ -27,35 +29,49 @@ const renderComponent = store => { }; describe('TotalPoints', () => { + beforeEach(() => { + store = { + totalPoints: 0, + currentUserId: 1, + isUserLoggedIn: false, + }; + storeActions = { + fetchPoints: jest.fn(), + }; + }); + test('renders when user is logged in', async () => { - const store = getMockStore(); - renderComponent(store); + store.isUserLoggedIn = true; + store.totalPoints = 100; + renderComponent(getMockStore()); expect(screen.getByRole('presentation')).toBeInTheDocument(); - expect(screen.getByText('100')).toBeInTheDocument(); + expect(screen.getByText(store.totalPoints)).toBeInTheDocument(); }); test('does not render when user is not logged in', async () => { - const store = getMockStore(); - store.getters.isUserLoggedIn = () => false; - renderComponent(store); + store.isUserLoggedIn = false; + store.totalPoints = 100; + renderComponent(getMockStore()); expect(screen.queryByRole('presentation')).not.toBeInTheDocument(); - expect(screen.queryByText('100')).not.toBeInTheDocument(); + expect(screen.queryByText(store.totalPoints)).not.toBeInTheDocument(); }); test('fetchPoints method is called on created', async () => { - const store = getMockStore(); - renderComponent(store); + store.isUserLoggedIn = true; + const mockedStore = getMockStore(); + renderComponent(mockedStore); - expect(store.actions.fetchPoints).toHaveBeenCalledTimes(1); + expect(mockedStore.actions.fetchPoints).toHaveBeenCalledTimes(1); }); test('tooltip message is displayed correctly when the mouse hovers over the icon', async () => { - const store = getMockStore(); - renderComponent(store); + store.isUserLoggedIn = true; + store.totalPoints = 100; + renderComponent(getMockStore()); await fireEvent.mouseOver(screen.getByRole('presentation')); - expect(screen.getByText('You earned 100 points')).toBeInTheDocument(); + expect(screen.getByText(`You earned ${store.totalPoints} points`)).toBeInTheDocument(); }); }); From cbd49e3baa0dce789bf563c2c25ea40a62bb000f Mon Sep 17 00:00:00 2001 From: Eshaan Aggarwal <96648934+EshaanAgg@users.noreply.github.com> Date: Thu, 29 Feb 2024 15:27:24 +0530 Subject: [PATCH 4/6] test: add testcases to check for emitted events --- .../__tests__/GenderSelect.spec.js | 38 +++++++++++++++++-- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/kolibri/core/assets/src/views/userAccounts/__tests__/GenderSelect.spec.js b/kolibri/core/assets/src/views/userAccounts/__tests__/GenderSelect.spec.js index bc8cc348ae5..b0d34b31abe 100644 --- a/kolibri/core/assets/src/views/userAccounts/__tests__/GenderSelect.spec.js +++ b/kolibri/core/assets/src/views/userAccounts/__tests__/GenderSelect.spec.js @@ -1,4 +1,4 @@ -import { render, screen } from '@testing-library/vue'; +import { render, screen, fireEvent } from '@testing-library/vue'; import VueRouter from 'vue-router'; import GenderSelect from '../GenderSelect.vue'; import '@testing-library/jest-dom'; @@ -10,12 +10,42 @@ const renderComponent = () => { }; describe('GenderSelect', () => { + const labelOptions = ['Male', 'Female', 'Not specified']; + test('renders correctly with label placeholder and options', async () => { renderComponent(); expect(screen.getByText('Gender')).toBeInTheDocument(); - expect(screen.getByText('Male')).toBeInTheDocument(); - expect(screen.getByText('Female')).toBeInTheDocument(); - expect(screen.getByText('Not specified')).toBeInTheDocument(); + labelOptions.forEach(option => { + expect(screen.getByText(option)).toBeInTheDocument(); + }); + }); + + test("emits 'update:value' event when an option is selected", async () => { + const { emitted } = renderComponent(); + + const selectedOption = labelOptions[0]; + await fireEvent.click(screen.getByText(selectedOption)); + + const emittedEvents = emitted(); + expect(emittedEvents).toHaveProperty('update:value'); + expect(emittedEvents['update:value'][0]).toEqual([selectedOption.toUpperCase()]); + }); + + test("the value of 'update:value' event is changed when a different option is selected", async () => { + const { emitted } = renderComponent(); + + const selectedOption = labelOptions[0]; + await fireEvent.click(screen.getByText(selectedOption)); + const newSelectedOption = labelOptions[1]; + await fireEvent.click(screen.getByText(newSelectedOption)); + + const emittedEvents = emitted(); + expect(emittedEvents).toHaveProperty('update:value'); + expect(emittedEvents['update:value']).toHaveLength(2); // As the event is emitted twice + expect(emittedEvents['update:value']).toEqual([ + [selectedOption.toUpperCase()], + [newSelectedOption.toUpperCase()], + ]); }); }); From d6c473e295f1e0bf5e76c8b58bcd533b453174f5 Mon Sep 17 00:00:00 2001 From: Eshaan Aggarwal <96648934+EshaanAgg@users.noreply.github.com> Date: Fri, 1 Mar 2024 18:45:47 +0530 Subject: [PATCH 5/6] fix: address review comments --- .../core/assets/src/views/ExamReport/TriesOverview.vue | 8 +++++--- .../src/views/ExamReport/__tests__/TriesOverview.spec.js | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/kolibri/core/assets/src/views/ExamReport/TriesOverview.vue b/kolibri/core/assets/src/views/ExamReport/TriesOverview.vue index 83a3b85a4ff..55c8b4d3aed 100644 --- a/kolibri/core/assets/src/views/ExamReport/TriesOverview.vue +++ b/kolibri/core/assets/src/views/ExamReport/TriesOverview.vue @@ -126,9 +126,11 @@ return this.pastTries.length ? Math.max(...this.pastTries.map(t => t.correct)) : null; }, bestScore() { - return this.maxQuestionsCorrect !== null - ? this.maxQuestionsCorrect / this.totalQuestions - : null; + const bestScoreAttempt = this.pastTries.find(t => t.correct === this.maxQuestionsCorrect); + if (!bestScoreAttempt) { + return null; + } + return bestScoreAttempt.time_spent; }, suggestedTimeAnnotation() { if (!this.suggestedTime || this.bestTimeSpent === null) { diff --git a/kolibri/core/assets/src/views/ExamReport/__tests__/TriesOverview.spec.js b/kolibri/core/assets/src/views/ExamReport/__tests__/TriesOverview.spec.js index 05acce0b770..83066bc85b9 100644 --- a/kolibri/core/assets/src/views/ExamReport/__tests__/TriesOverview.spec.js +++ b/kolibri/core/assets/src/views/ExamReport/__tests__/TriesOverview.spec.js @@ -59,7 +59,7 @@ describe('TriesOverview', () => { }); test('renders progress icon and not started label when there are no past tries', () => { - renderComponent(); + renderComponent({ pastTries: [] }); expect(screen.getByTestId('progress-icon-0')).toBeInTheDocument(); expect(screen.getByText('notStartedLabel')).toBeInTheDocument(); From 67af17e8164d79e19a5ba3907b124d9f46bcee69 Mon Sep 17 00:00:00 2001 From: Eshaan Aggarwal <96648934+EshaanAgg@users.noreply.github.com> Date: Fri, 1 Mar 2024 18:49:53 +0530 Subject: [PATCH 6/6] Revert "fix: address review comments" as it was for a different branch This reverts commit d6c473e295f1e0bf5e76c8b58bcd533b453174f5. --- .../core/assets/src/views/ExamReport/TriesOverview.vue | 8 +++----- .../src/views/ExamReport/__tests__/TriesOverview.spec.js | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/kolibri/core/assets/src/views/ExamReport/TriesOverview.vue b/kolibri/core/assets/src/views/ExamReport/TriesOverview.vue index 55c8b4d3aed..83a3b85a4ff 100644 --- a/kolibri/core/assets/src/views/ExamReport/TriesOverview.vue +++ b/kolibri/core/assets/src/views/ExamReport/TriesOverview.vue @@ -126,11 +126,9 @@ return this.pastTries.length ? Math.max(...this.pastTries.map(t => t.correct)) : null; }, bestScore() { - const bestScoreAttempt = this.pastTries.find(t => t.correct === this.maxQuestionsCorrect); - if (!bestScoreAttempt) { - return null; - } - return bestScoreAttempt.time_spent; + return this.maxQuestionsCorrect !== null + ? this.maxQuestionsCorrect / this.totalQuestions + : null; }, suggestedTimeAnnotation() { if (!this.suggestedTime || this.bestTimeSpent === null) { diff --git a/kolibri/core/assets/src/views/ExamReport/__tests__/TriesOverview.spec.js b/kolibri/core/assets/src/views/ExamReport/__tests__/TriesOverview.spec.js index 83066bc85b9..05acce0b770 100644 --- a/kolibri/core/assets/src/views/ExamReport/__tests__/TriesOverview.spec.js +++ b/kolibri/core/assets/src/views/ExamReport/__tests__/TriesOverview.spec.js @@ -59,7 +59,7 @@ describe('TriesOverview', () => { }); test('renders progress icon and not started label when there are no past tries', () => { - renderComponent({ pastTries: [] }); + renderComponent(); expect(screen.getByTestId('progress-icon-0')).toBeInTheDocument(); expect(screen.getByText('notStartedLabel')).toBeInTheDocument();