diff --git a/src/discussions/topics/TopicsView.jsx b/src/discussions/topics/TopicsView.jsx
index d71bed21..50f969a4 100644
--- a/src/discussions/topics/TopicsView.jsx
+++ b/src/discussions/topics/TopicsView.jsx
@@ -38,7 +38,7 @@ function TopicsView() {
),
);
- if (nonCoursewareTopics && category === undefined) {
+ if (nonCoursewareTopics?.length > 1 && category === undefined) {
topicElements.unshift();
}
diff --git a/src/discussions/topics/TopicsView.test.jsx b/src/discussions/topics/TopicsView.test.jsx
new file mode 100644
index 00000000..9798afbe
--- /dev/null
+++ b/src/discussions/topics/TopicsView.test.jsx
@@ -0,0 +1,186 @@
+import {
+ fireEvent, queryByText, render, screen, waitFor,
+} from '@testing-library/react';
+import MockAdapter from 'axios-mock-adapter';
+import { IntlProvider } from 'react-intl';
+import { MemoryRouter, Route } from 'react-router';
+import { Factory } from 'rosie';
+
+import { initializeMockApp } from '@edx/frontend-platform';
+import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
+import { AppProvider } from '@edx/frontend-platform/react';
+
+import { API_BASE_URL } from '../../data/constants';
+import { initializeStore } from '../../store';
+import TopicsView from './TopicsView';
+
+import './data/__factories__';
+
+const courseId = 'course-v1:edX+TestX+Test_Course';
+const topicsApiUrl = `${API_BASE_URL}/api/discussion/v1/course_topics/${courseId}`;
+let store;
+let axiosMock;
+let lastLocation;
+
+function renderComponent() {
+ render(
+
+
+
+
+
+
+
+
+
+ {
+ lastLocation = location;
+ return null;
+ }}
+ />
+
+
+ ,
+ );
+}
+
+describe('TopicsView', () => {
+ beforeEach(() => {
+ initializeMockApp({
+ authenticatedUser: {
+ userId: 3,
+ username: 'abc123',
+ administrator: true,
+ roles: [],
+ },
+ });
+
+ store = initializeStore();
+ Factory.resetAll();
+ axiosMock = new MockAdapter(getAuthenticatedHttpClient());
+
+ lastLocation = undefined;
+ });
+
+ it('displays non-courseware topics', async () => {
+ axiosMock
+ .onGet(topicsApiUrl)
+ .reply(200, {
+ courseware_topics: [],
+ non_courseware_topics: Factory.buildList('topic', 3),
+ });
+ renderComponent();
+
+ await screen.findByText('topic 1');
+ expect(screen.queryByText('topic 2')).toBeInTheDocument();
+ expect(screen.queryByText('topic 3')).toBeInTheDocument();
+ });
+
+ it('displays non-courseware in one topic group', async () => {
+ axiosMock
+ .onGet(topicsApiUrl)
+ .reply(200, {
+ courseware_topics: [],
+ non_courseware_topics: Factory.buildList('topic', 3),
+ });
+ renderComponent();
+
+ await screen.findByText('topic 1');
+ const topicGroups = screen.queryAllByTestId('topic-group');
+ expect(topicGroups).toHaveLength(1);
+ });
+
+ it('displays courseware topics', async () => {
+ axiosMock
+ .onGet(topicsApiUrl)
+ .reply(200, {
+ courseware_topics: Factory.buildList('topic', 3),
+ non_courseware_topics: [],
+ });
+ renderComponent();
+
+ await screen.findByText('topic 1');
+ expect(screen.queryByText('topic 2')).toBeInTheDocument();
+ expect(screen.queryByText('topic 3')).toBeInTheDocument();
+ });
+
+ it('displays courseware topics in individual topic groups', async () => {
+ axiosMock
+ .onGet(topicsApiUrl)
+ .reply(200, {
+ courseware_topics: Factory.buildList('topic', 3),
+ non_courseware_topics: [],
+ });
+ renderComponent();
+
+ await screen.findByText('topic 1');
+ const topicGroups = screen.queryAllByTestId('topic-group');
+ expect(topicGroups).toHaveLength(3);
+ });
+
+ it('displays non-courseware topics before courseware topics', async () => {
+ axiosMock
+ .onGet(topicsApiUrl)
+ .reply(200, {
+ courseware_topics: Factory.buildList('topic', 3),
+ non_courseware_topics: Factory.buildList('topic', 3),
+ });
+ renderComponent();
+
+ await screen.findByText('topic 1');
+ const topicGroups = screen.queryAllByTestId('topic-group');
+ expect(queryByText(topicGroups[0], 'topic 4')).toBeInTheDocument();
+ expect(queryByText(topicGroups[0], 'topic 5')).toBeInTheDocument();
+ expect(queryByText(topicGroups[0], 'topic 6')).toBeInTheDocument();
+ expect(queryByText(topicGroups[1], 'topic 1')).toBeInTheDocument();
+ expect(queryByText(topicGroups[2], 'topic 2')).toBeInTheDocument();
+ expect(queryByText(topicGroups[3], 'topic 3')).toBeInTheDocument();
+ });
+
+ it('clicking on courseware topic (category) takes to category page', async () => {
+ axiosMock
+ .onGet(topicsApiUrl)
+ .reply(200, {
+ courseware_topics: Factory.buildList('topic', 3),
+ non_courseware_topics: Factory.buildList('topic', 3),
+ });
+ renderComponent();
+
+ const topic = await screen.findByText('topic 1');
+ fireEvent.click(topic);
+
+ await waitFor(() => expect(screen.queryByText('topic 2')).not.toBeInTheDocument());
+ expect(lastLocation.pathname.endsWith('/category/topic 1')).toBeTruthy();
+ });
+
+ it('on category page only selected category and it\'s children are displayed', async () => {
+ const category = Factory.build('topic');
+ category.children = Factory.buildList('topic', 3);
+ axiosMock
+ .onGet(topicsApiUrl)
+ .reply(200, {
+ courseware_topics: [
+ category,
+ Factory.build('topic'),
+ ],
+ non_courseware_topics: Factory.buildList('topic', 3),
+ });
+ renderComponent();
+
+ const topic = await screen.findByText('topic 1');
+ fireEvent.click(topic);
+
+ await waitFor(() => expect(screen.queryByText('topic 6')).not.toBeInTheDocument());
+ // children
+ expect(screen.queryByText('topic 2')).toBeInTheDocument();
+ expect(screen.queryByText('topic 3')).toBeInTheDocument();
+ expect(screen.queryByText('topic 4')).toBeInTheDocument();
+
+ // other courseware topics (categories) and non-courseware topics
+ expect(screen.queryByText('topic 5')).not.toBeInTheDocument();
+ expect(screen.queryByText('topic 6')).not.toBeInTheDocument();
+ expect(screen.queryByText('topic 7')).not.toBeInTheDocument();
+ expect(screen.queryByText('topic 8')).not.toBeInTheDocument();
+ });
+});
diff --git a/src/discussions/topics/data/__factories__/index.js b/src/discussions/topics/data/__factories__/index.js
new file mode 100644
index 00000000..4a487958
--- /dev/null
+++ b/src/discussions/topics/data/__factories__/index.js
@@ -0,0 +1 @@
+import './topics.factory';
diff --git a/src/discussions/topics/data/__factories__/topics.factory.js b/src/discussions/topics/data/__factories__/topics.factory.js
new file mode 100644
index 00000000..68a8eaaa
--- /dev/null
+++ b/src/discussions/topics/data/__factories__/topics.factory.js
@@ -0,0 +1,6 @@
+import { Factory } from 'rosie';
+
+Factory.define('topic')
+ .sequence('id', (idx) => `topic-${idx}`)
+ .sequence('name', (idx) => `topic ${idx}`)
+ .attr('children', []);
diff --git a/src/discussions/topics/topic-group/TopicGroup.jsx b/src/discussions/topics/topic-group/TopicGroup.jsx
index fd5e61f6..8f86bfb5 100644
--- a/src/discussions/topics/topic-group/TopicGroup.jsx
+++ b/src/discussions/topics/topic-group/TopicGroup.jsx
@@ -42,7 +42,11 @@ function TopicGroup({
}
return (
-