diff --git a/src/components/Search.jsx b/src/components/Search.jsx index 1911d762..8f415960 100644 --- a/src/components/Search.jsx +++ b/src/components/Search.jsx @@ -72,6 +72,7 @@ function Search({ intl }) { onSubmit(searchValue)} + data-testid="search-icon" /> diff --git a/src/discussions/data/api.js b/src/discussions/data/api.js index 045ac180..3f197194 100644 --- a/src/discussions/data/api.js +++ b/src/discussions/data/api.js @@ -8,14 +8,13 @@ ensureConfig([ ], 'Posts API service'); export const getCourseConfigApiUrl = () => `${getConfig().LMS_BASE_URL}/api/discussion/v1/courses/`; - +export const getDiscussionsConfigUrl = (courseId) => `${getCourseConfigApiUrl()}${courseId}/`; /** * Get discussions course config * @param {string} courseId */ export async function getDiscussionsConfig(courseId) { - const url = `${getCourseConfigApiUrl()}${courseId}/`; - const { data } = await getAuthenticatedHttpClient().get(url); + const { data } = await getAuthenticatedHttpClient().get(getDiscussionsConfigUrl(courseId)); return data; } @@ -24,7 +23,7 @@ export async function getDiscussionsConfig(courseId) { * @param {string} courseId */ export async function getDiscussionsSettings(courseId) { - const url = `${getCourseConfigApiUrl()}${courseId}/settings`; + const url = `${getDiscussionsConfigUrl(courseId)}settings`; const { data } = await getAuthenticatedHttpClient().get(url); return data; } diff --git a/src/discussions/learners/LearnersView.test.jsx b/src/discussions/learners/LearnersView.test.jsx index e364b8ec..9e05e7b5 100644 --- a/src/discussions/learners/LearnersView.test.jsx +++ b/src/discussions/learners/LearnersView.test.jsx @@ -1,6 +1,8 @@ import React from 'react'; -import { render, screen } from '@testing-library/react'; +import { + fireEvent, render, screen, waitFor, within, +} from '@testing-library/react'; import MockAdapter from 'axios-mock-adapter'; import { act } from 'react-dom/test-utils'; import { IntlProvider } from 'react-intl'; @@ -11,11 +13,13 @@ import { initializeMockApp } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { AppProvider } from '@edx/frontend-platform/react'; +import { PostActionsBar } from '../../components'; import { initializeStore } from '../../store'; import { executeThunk } from '../../test-utils'; -import { getCourseConfigApiUrl } from '../data/api'; +import { DiscussionContext } from '../common/context'; +import { getDiscussionsConfigUrl } from '../data/api'; import { fetchCourseConfig } from '../data/thunks'; -import { getCoursesApiUrl, getUserProfileApiUrl } from './data/api'; +import { getUserProfileApiUrl, learnersApiUrl } from './data/api'; import { fetchLearners } from './data/thunks'; import LearnersView from './LearnersView'; @@ -23,27 +27,31 @@ import './data/__factories__'; let store; let axiosMock; -const coursesApiUrl = getCoursesApiUrl(); -const courseConfigApiUrl = getCourseConfigApiUrl(); -const userProfileApiUrl = getUserProfileApiUrl(); const courseId = 'course-v1:edX+TestX+Test_Course'; +let container; function renderComponent() { - return render( + const wrapper = render( - - - - - + + + + + + + + , ); + container = wrapper.container; } describe('LearnersView', () => { - const learnerCount = 3; beforeEach(async () => { initializeMockApp({ authenticatedUser: { @@ -53,41 +61,190 @@ describe('LearnersView', () => { roles: [], }, }); - + axiosMock = new MockAdapter(getAuthenticatedHttpClient()); store = initializeStore(); Factory.resetAll(); - const learnersData = Factory.build('learnersResult', {}, { - count: learnerCount, - pageSize: 6, - }); - axiosMock = new MockAdapter(getAuthenticatedHttpClient()); - axiosMock.onGet(`${coursesApiUrl}${courseId}/activity_stats/`) - .reply(() => [200, learnersData]); - const learnersProfile = Factory.build('learnersProfile', {}, { - username: ['learner-1', 'learner-2', 'learner-3'], - }); - axiosMock.onGet(`${userProfileApiUrl}?username=learner-1,learner-2,learner-3`) - .reply(() => [200, learnersProfile.profiles]); - await executeThunk(fetchLearners(courseId), store.dispatch, store.getState); }); - describe('Basic', () => { - test('Learners tab is disabled by default', async () => { - await act(async () => { - await renderComponent(); - }); - expect(screen.queryByText(/Last active/i)).toBeFalsy(); + async function setUpLearnerMockResponse( + count = 3, + pageSize = 6, + page = 1, + username = ['learner-1', 'learner-2', 'learner-3'], + searchText, + ) { + Factory.resetAll(); + const learnersData = Factory.build('learnersResult', {}, { + count, + pageSize, + page, }); - test('Learners tab is enabled', async () => { - axiosMock.onGet(`${courseConfigApiUrl}${courseId}/`).reply(200, { - learners_tab_enabled: true, - user_is_privileged: true, - }); - axiosMock.onGet(`${courseConfigApiUrl}${courseId}/settings`).reply(200, {}); - await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState); + axiosMock.onGet(learnersApiUrl(courseId)) + .reply(() => [200, learnersData]); + + axiosMock.onGet(`${getUserProfileApiUrl()}?username=${username.join()}`) + .reply(() => [200, Factory.build('learnersProfile', {}, { + username, + }).profiles]); + await executeThunk(fetchLearners(courseId, { usernameSearch: searchText }), store.dispatch, store.getState); + } + + async function assignPrivilages() { + axiosMock.onGet(getDiscussionsConfigUrl(courseId)).reply(200, { + learners_tab_enabled: true, + user_is_privileged: true, + }); + + await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState); + } + + test('Learners tab is disabled by default', async () => { + await setUpLearnerMockResponse(); + await renderComponent(); + + expect(screen.queryByText('learner-1')).not.toBeInTheDocument(); + }); + + test('Learners tab is enabled', async () => { + await setUpLearnerMockResponse(); + await assignPrivilages(); + await waitFor(() => { + renderComponent(); + }); + + expect(screen.queryByText('learner-1')).toBeInTheDocument(); + }); + + test('Most activity should be selected by default for the non-moderator role.', async () => { + await setUpLearnerMockResponse(); + await renderComponent(); + + const filterBar = container.querySelector('.collapsible-trigger'); + + await act(async () => { + fireEvent.click(filterBar); + }); + + await waitFor(() => { + const mostActivity = screen.getByTestId('activity selected'); + + expect(mostActivity).toBeInTheDocument(); + }); + }); + + it.each([ + { searchBy: 'sort-recency', result: 0 }, + { searchBy: 'sort-activity', result: 3 }, + ])('successfully display learners by %s.', async ({ searchBy, result }) => { + await setUpLearnerMockResponse(); + await assignPrivilages(); + await renderComponent(); + + const filterBar = container.querySelector('.collapsible-trigger'); + await act(async () => { + fireEvent.click(filterBar); + }); + + await waitFor(async () => { + const activity = container.querySelector(`#${searchBy}`); + await act(async () => { - await renderComponent(); + fireEvent.click(activity); + }); + await waitFor(() => { + const learners = container.querySelectorAll('.discussion-post'); + + expect(learners).toHaveLength(result); }); }); }); + + it('should display a learner\'s list.', async () => { + await setUpLearnerMockResponse(); + await assignPrivilages(); + await waitFor(() => { + renderComponent(); + }); + + const learners = await container.querySelectorAll('.discussion-post'); + const firstLearner = learners.item(0); + const learnerAvatar = firstLearner.querySelector('[alt=learner-1]'); + const learnerTitle = within(firstLearner).queryByText('learner-1'); + const stats = firstLearner.querySelectorAll('.icon-size'); + + expect(learners).toHaveLength(3); + expect(learnerAvatar).toBeInTheDocument(); + expect(learnerTitle).toBeInTheDocument(); + expect(stats).toHaveLength(2); + }); + + it.each([ + { + searchText: 'hello world', + output: 'Showing 0 results for', + learnersCount: 0, + username: [], + }, + { + searchText: 'learner', + output: 'Showing 2 results for', + learnersCount: 2, + username: + ['learner-1', 'learner-2'], + }, + ])('should have a search bar with a clear button and \'$output\' results found text.', + async ({ + searchText, output, learnersCount, username, + }) => { + await setUpLearnerMockResponse(); + await assignPrivilages(); + await renderComponent(); + + const searchField = within(container).getByPlaceholderText('Search learners'); + const searchButton = within(container).getByTestId('search-icon'); + + await fireEvent.change(searchField, { target: { value: searchText } }); + await act(async () => { + fireEvent.click(searchButton); + setUpLearnerMockResponse(learnersCount, learnersCount, 1, username, searchText); + }); + + await waitFor(() => { + const clearButton = within(container).queryByText('Clear results'); + const searchMessage = within(container).queryByText(`${output} "${searchText}"`); + const leaners = container.querySelectorAll('.discussion-post') ?? []; + + expect(searchMessage).toBeInTheDocument(); + expect(clearButton).toBeInTheDocument(); + expect(leaners).toHaveLength(learnersCount); + }); + }); + + test('When click on the clear button it should move to a list of all learners.', async () => { + await setUpLearnerMockResponse(); + await assignPrivilages(); + await renderComponent(); + + const searchField = within(container).getByPlaceholderText('Search learners'); + const searchButton = within(container).getByTestId('search-icon'); + let clearButton; + + await fireEvent.change(searchField, { target: { value: 'learner' } }); + await act(async () => { + fireEvent.click(searchButton); + setUpLearnerMockResponse(2, 2, 1, ['learner-1', 'learner-2'], 'learner'); + }); + + await waitFor(() => { + clearButton = within(container).queryByText('Clear results'); + }); + await act(async () => fireEvent.click(clearButton)); + await waitFor(() => { + setUpLearnerMockResponse(); + }); + + const learners = container.querySelectorAll('.discussion-post'); + + expect(learners).toHaveLength(3); + }); }); diff --git a/src/discussions/learners/data/__factories__/learners.factory.js b/src/discussions/learners/data/__factories__/learners.factory.js index 88a9920f..283aa371 100644 --- a/src/discussions/learners/data/__factories__/learners.factory.js +++ b/src/discussions/learners/data/__factories__/learners.factory.js @@ -13,9 +13,9 @@ Factory.define('learner') }); Factory.define('learnersResult') - .option('count', null, 3) - .option('page', null, 1) - .option('pageSize', null, 5) + .option('count', null) + .option('page', null) + .option('pageSize', null) .option('courseId', null, 'course-v1:Test+TestX+Test_Course') .option('activeFlags', null, 0) .attr( diff --git a/src/discussions/learners/learner/LearnerFilterBar.jsx b/src/discussions/learners/learner/LearnerFilterBar.jsx index 610c1504..d9b10c26 100644 --- a/src/discussions/learners/learner/LearnerFilterBar.jsx +++ b/src/discussions/learners/learner/LearnerFilterBar.jsx @@ -24,7 +24,7 @@ const ActionItem = ({