Compare commits
3 Commits
dependabot
...
sundas/INF
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1005541e8d | ||
|
|
562db39c24 | ||
|
|
d6a38ae6d7 |
@@ -72,6 +72,7 @@ function Search({ intl }) {
|
|||||||
<Icon
|
<Icon
|
||||||
src={SearchIcon}
|
src={SearchIcon}
|
||||||
onClick={() => onSubmit(searchValue)}
|
onClick={() => onSubmit(searchValue)}
|
||||||
|
data-testid="search-icon"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</SearchField.Advanced>
|
</SearchField.Advanced>
|
||||||
|
|||||||
@@ -8,14 +8,13 @@ ensureConfig([
|
|||||||
], 'Posts API service');
|
], 'Posts API service');
|
||||||
|
|
||||||
export const getCourseConfigApiUrl = () => `${getConfig().LMS_BASE_URL}/api/discussion/v1/courses/`;
|
export const getCourseConfigApiUrl = () => `${getConfig().LMS_BASE_URL}/api/discussion/v1/courses/`;
|
||||||
|
export const getDiscussionsConfigUrl = (courseId) => `${getCourseConfigApiUrl()}${courseId}/`;
|
||||||
/**
|
/**
|
||||||
* Get discussions course config
|
* Get discussions course config
|
||||||
* @param {string} courseId
|
* @param {string} courseId
|
||||||
*/
|
*/
|
||||||
export async function getDiscussionsConfig(courseId) {
|
export async function getDiscussionsConfig(courseId) {
|
||||||
const url = `${getCourseConfigApiUrl()}${courseId}/`;
|
const { data } = await getAuthenticatedHttpClient().get(getDiscussionsConfigUrl(courseId));
|
||||||
const { data } = await getAuthenticatedHttpClient().get(url);
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,7 +23,7 @@ export async function getDiscussionsConfig(courseId) {
|
|||||||
* @param {string} courseId
|
* @param {string} courseId
|
||||||
*/
|
*/
|
||||||
export async function getDiscussionsSettings(courseId) {
|
export async function getDiscussionsSettings(courseId) {
|
||||||
const url = `${getCourseConfigApiUrl()}${courseId}/settings`;
|
const url = `${getDiscussionsConfigUrl(courseId)}settings`;
|
||||||
const { data } = await getAuthenticatedHttpClient().get(url);
|
const { data } = await getAuthenticatedHttpClient().get(url);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import React from 'react';
|
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 MockAdapter from 'axios-mock-adapter';
|
||||||
import { act } from 'react-dom/test-utils';
|
import { act } from 'react-dom/test-utils';
|
||||||
import { IntlProvider } from 'react-intl';
|
import { IntlProvider } from 'react-intl';
|
||||||
@@ -11,11 +13,13 @@ import { initializeMockApp } from '@edx/frontend-platform';
|
|||||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||||
import { AppProvider } from '@edx/frontend-platform/react';
|
import { AppProvider } from '@edx/frontend-platform/react';
|
||||||
|
|
||||||
|
import { PostActionsBar } from '../../components';
|
||||||
import { initializeStore } from '../../store';
|
import { initializeStore } from '../../store';
|
||||||
import { executeThunk } from '../../test-utils';
|
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 { fetchCourseConfig } from '../data/thunks';
|
||||||
import { getCoursesApiUrl, getUserProfileApiUrl } from './data/api';
|
import { getUserProfileApiUrl, learnersApiUrl } from './data/api';
|
||||||
import { fetchLearners } from './data/thunks';
|
import { fetchLearners } from './data/thunks';
|
||||||
import LearnersView from './LearnersView';
|
import LearnersView from './LearnersView';
|
||||||
|
|
||||||
@@ -23,27 +27,31 @@ import './data/__factories__';
|
|||||||
|
|
||||||
let store;
|
let store;
|
||||||
let axiosMock;
|
let axiosMock;
|
||||||
const coursesApiUrl = getCoursesApiUrl();
|
|
||||||
const courseConfigApiUrl = getCourseConfigApiUrl();
|
|
||||||
const userProfileApiUrl = getUserProfileApiUrl();
|
|
||||||
const courseId = 'course-v1:edX+TestX+Test_Course';
|
const courseId = 'course-v1:edX+TestX+Test_Course';
|
||||||
|
let container;
|
||||||
|
|
||||||
function renderComponent() {
|
function renderComponent() {
|
||||||
return render(
|
const wrapper = render(
|
||||||
<IntlProvider locale="en">
|
<IntlProvider locale="en">
|
||||||
<AppProvider store={store}>
|
<AppProvider store={store}>
|
||||||
<MemoryRouter initialEntries={[`/${courseId}/`]}>
|
<DiscussionContext.Provider value={{
|
||||||
<Route path="/:courseId/">
|
page: 'learners',
|
||||||
<LearnersView />
|
}}
|
||||||
</Route>
|
>
|
||||||
</MemoryRouter>
|
<MemoryRouter initialEntries={[`/${courseId}/`]}>
|
||||||
|
<Route path="/:courseId/">
|
||||||
|
<PostActionsBar />
|
||||||
|
<LearnersView />
|
||||||
|
</Route>
|
||||||
|
</MemoryRouter>
|
||||||
|
</DiscussionContext.Provider>
|
||||||
</AppProvider>
|
</AppProvider>
|
||||||
</IntlProvider>,
|
</IntlProvider>,
|
||||||
);
|
);
|
||||||
|
container = wrapper.container;
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('LearnersView', () => {
|
describe('LearnersView', () => {
|
||||||
const learnerCount = 3;
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
initializeMockApp({
|
initializeMockApp({
|
||||||
authenticatedUser: {
|
authenticatedUser: {
|
||||||
@@ -53,41 +61,190 @@ describe('LearnersView', () => {
|
|||||||
roles: [],
|
roles: [],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||||
store = initializeStore();
|
store = initializeStore();
|
||||||
Factory.resetAll();
|
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', () => {
|
async function setUpLearnerMockResponse(
|
||||||
test('Learners tab is disabled by default', async () => {
|
count = 3,
|
||||||
await act(async () => {
|
pageSize = 6,
|
||||||
await renderComponent();
|
page = 1,
|
||||||
});
|
username = ['learner-1', 'learner-2', 'learner-3'],
|
||||||
expect(screen.queryByText(/Last active/i)).toBeFalsy();
|
searchText,
|
||||||
|
) {
|
||||||
|
Factory.resetAll();
|
||||||
|
const learnersData = Factory.build('learnersResult', {}, {
|
||||||
|
count,
|
||||||
|
pageSize,
|
||||||
|
page,
|
||||||
});
|
});
|
||||||
test('Learners tab is enabled', async () => {
|
axiosMock.onGet(learnersApiUrl(courseId))
|
||||||
axiosMock.onGet(`${courseConfigApiUrl}${courseId}/`).reply(200, {
|
.reply(() => [200, learnersData]);
|
||||||
learners_tab_enabled: true,
|
|
||||||
user_is_privileged: true,
|
axiosMock.onGet(`${getUserProfileApiUrl()}?username=${username.join()}`)
|
||||||
});
|
.reply(() => [200, Factory.build('learnersProfile', {}, {
|
||||||
axiosMock.onGet(`${courseConfigApiUrl}${courseId}/settings`).reply(200, {});
|
username,
|
||||||
await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState);
|
}).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 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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ Factory.define('learner')
|
|||||||
});
|
});
|
||||||
|
|
||||||
Factory.define('learnersResult')
|
Factory.define('learnersResult')
|
||||||
.option('count', null, 3)
|
.option('count', null)
|
||||||
.option('page', null, 1)
|
.option('page', null)
|
||||||
.option('pageSize', null, 5)
|
.option('pageSize', null)
|
||||||
.option('courseId', null, 'course-v1:Test+TestX+Test_Course')
|
.option('courseId', null, 'course-v1:Test+TestX+Test_Course')
|
||||||
.option('activeFlags', null, 0)
|
.option('activeFlags', null, 0)
|
||||||
.attr(
|
.attr(
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const ActionItem = ({
|
|||||||
<label
|
<label
|
||||||
htmlFor={id}
|
htmlFor={id}
|
||||||
className="focus border-bottom-0 d-flex align-items-center w-100 py-2 m-0 font-weight-500 filter-menu"
|
className="focus border-bottom-0 d-flex align-items-center w-100 py-2 m-0 font-weight-500 filter-menu"
|
||||||
data-testid={value === selected ? 'selected' : null}
|
data-testid={value === selected ? `${value} selected` : null}
|
||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: 'pointer' }}
|
||||||
aria-checked={value === selected}
|
aria-checked={value === selected}
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user