diff --git a/src/discussions/common/ActionsDropdown.test.jsx b/src/discussions/common/ActionsDropdown.test.jsx index b6d69fa1..dd4711d7 100644 --- a/src/discussions/common/ActionsDropdown.test.jsx +++ b/src/discussions/common/ActionsDropdown.test.jsx @@ -1,16 +1,24 @@ import { fireEvent, render, screen, waitFor, } from '@testing-library/react'; +import MockAdapter from 'axios-mock-adapter'; import { act } from 'react-dom/test-utils'; import { IntlProvider } from 'react-intl'; import { Factory } from 'rosie'; import { camelCaseObject, initializeMockApp, snakeCaseObject } from '@edx/frontend-platform'; +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { AppProvider } from '@edx/frontend-platform/react'; import { ContentActions } from '../../data/constants'; import { initializeStore } from '../../store'; +import { executeThunk } from '../../test-utils'; import messages from '../messages'; +import { getCommentsApiUrl } from '../post-comments/data/api'; +import { addComment, fetchThreadComments } from '../post-comments/data/thunks'; +import { PostCommentsContext } from '../post-comments/postCommentsContext'; +import { getThreadsApiUrl } from '../posts/data/api'; +import { fetchThread } from '../posts/data/thunks'; import { ACTIONS_LIST } from '../utils'; import ActionsDropdown from './ActionsDropdown'; @@ -18,107 +26,151 @@ import '../post-comments/data/__factories__'; import '../posts/data/__factories__'; let store; +let axiosMock; +const commentsApiUrl = getCommentsApiUrl(); +const threadsApiUrl = getThreadsApiUrl(); +const discussionThreadId = 'thread-1'; +const questionThreadId = 'thread-2'; +const commentContent = 'This is a comment for thread-1'; +let discussionThread; +let questionThread; +let comment; -function buildTestContent(buildParams, testMeta) { +const buildTestContent = (buildParams, testMeta) => { const buildParamsSnakeCase = snakeCaseObject(buildParams); - return [ - { - testFor: 'comments', - ...camelCaseObject(Factory.build('comment', { ...buildParamsSnakeCase }, null)), - ...testMeta, - }, - { - testFor: 'question threads', - ...camelCaseObject(Factory.build('thread', { ...buildParamsSnakeCase, type: 'question' }, null)), - ...testMeta, - }, - { + discussionThread = Factory.build('thread', { ...buildParamsSnakeCase, id: discussionThreadId }, null); + questionThread = Factory.build('thread', { ...buildParamsSnakeCase, id: questionThreadId }, null); + comment = Factory.build('comment', { ...buildParamsSnakeCase, thread_id: discussionThreadId }, null); + + return { + discussion: { testFor: 'discussion threads', - ...camelCaseObject(Factory.build('thread', { ...buildParamsSnakeCase, type: 'discussion' }, null)), + contentType: 'POST', + ...camelCaseObject(discussionThread), ...testMeta, }, - ]; -} + question: { + testFor: 'question threads', + contentType: 'POST', + ...camelCaseObject(questionThread), + ...testMeta, + }, + comment: { + testFor: 'comments', + contentType: 'COMMENT', + type: 'discussion', + ...camelCaseObject(comment), + ...testMeta, + }, + }; +}; -const canPerformActionTestData = ACTIONS_LIST - .map(({ action, conditions, label: { defaultMessage } }) => { - const buildParams = { +const mockThreadAndComment = async (response) => { + axiosMock.onGet(`${threadsApiUrl}${discussionThreadId}/`).reply(200, response); + axiosMock.onGet(`${threadsApiUrl}${questionThreadId}/`).reply(200, response); + axiosMock.onGet(commentsApiUrl).reply(200, Factory.build('commentsResult')); + axiosMock.onPost(commentsApiUrl).reply(200, response); + + await executeThunk(fetchThread(discussionThreadId), store.dispatch, store.getState); + await executeThunk(fetchThread(questionThreadId), store.dispatch, store.getState); + await executeThunk(fetchThreadComments(discussionThreadId), store.dispatch, store.getState); + await executeThunk(addComment(commentContent, discussionThreadId, null), store.dispatch, store.getState); +}; + +const canPerformActionTestData = ACTIONS_LIST.flatMap(({ + id, action, conditions, label: { defaultMessage }, +}) => { + const buildParams = { editable_fields: [action] }; + + if (conditions) { + Object.entries(conditions).forEach(([conditionKey, conditionValue]) => { + buildParams[conditionKey] = conditionValue; + }); + } + + const testContent = buildTestContent(buildParams, { label: defaultMessage, action }); + + switch (id) { + case 'answer': + case 'unanswer': + return [testContent.question]; + case 'endorse': + case 'unendorse': + return [testContent.comment, testContent.discussion]; + default: + return [testContent.discussion, testContent.question, testContent.comment]; + } +}); + +const canNotPerformActionTestData = ACTIONS_LIST.flatMap(({ action, conditions, label: { defaultMessage } }) => { + const label = defaultMessage; + + if (!conditions) { + const content = buildTestContent({ editable_fields: [] }, { reason: 'field is not editable', label: defaultMessage }); + return [content.discussion, content.question, content.comment]; + } + + const reversedConditions = Object.fromEntries(Object.entries(conditions).map(([key, value]) => [key, !value])); + + const content = { + // can edit field, but doesn't pass conditions + ...buildTestContent({ editable_fields: [action], - }; - if (conditions) { - Object.entries(conditions) - .forEach(([conditionKey, conditionValue]) => { - buildParams[conditionKey] = conditionValue; - }); - } - return buildTestContent(buildParams, { label: defaultMessage, action }); - }) - .flat(); + ...reversedConditions, + }, { reason: 'field is editable but does not pass condition', label, action }), -const canNotPerformActionTestData = ACTIONS_LIST - .map(({ action, conditions, label: { defaultMessage } }) => { - const label = defaultMessage; - let content; - if (!conditions) { - content = buildTestContent({ editable_fields: [] }, { reason: 'field is not editable', label: defaultMessage }); - } else { - const reversedConditions = Object.keys(conditions) - .reduce( - (results, key) => ({ - ...results, - [key]: !conditions[key], - }), - {}, - ); + // passes conditions, but can't edit field + ...(action === ContentActions.DELETE ? {} : buildTestContent({ + editable_fields: [], + ...conditions, + }, { reason: 'passes conditions but field is not editable', label, action })), - content = [ - // can edit field, but doesn't pass conditions - ...buildTestContent({ - editable_fields: [action], - ...reversedConditions, - }, { reason: 'field is editable but does not pass condition', label, action }), - // passes conditions, but can't edit field - ...(action === ContentActions.DELETE - ? [] - : buildTestContent({ - editable_fields: [], - ...conditions, - }, { reason: 'passes conditions but field is not editable', label, action }) - ), - // can't edit field, and doesn't pass conditions - ...buildTestContent({ - editable_fields: [], - ...reversedConditions, - }, { reason: 'can not edit field and does not match conditions', label, action }), - ]; - } - return content; - }) - .flat(); + // can't edit field, and doesn't pass conditions + ...buildTestContent({ + editable_fields: [], + ...reversedConditions, + }, { reason: 'can not edit field and does not match conditions', label, action }), + }; -function renderComponent( - commentOrPost, - { disabled = false, actionHandlers = {} } = {}, -) { + return [content.discussion, content.question, content.comment]; +}); + +const renderComponent = ({ + id = '', + contentType = 'POST', + closed = false, + type = 'discussion', + postId = '', + disabled = false, + actionHandlers = {}, +} = {}) => { render( - + + + , ); -} +}; const findOpenActionsDropdownButton = async () => ( screen.findByRole('button', { name: messages.actionsAlt.defaultMessage }) ); describe('ActionsDropdown', () => { - beforeEach(async () => { + beforeEach(() => { initializeMockApp({ authenticatedUser: { userId: 3, @@ -128,10 +180,13 @@ describe('ActionsDropdown', () => { }, }); store = initializeStore(); + Factory.resetAll(); + axiosMock = new MockAdapter(getAuthenticatedHttpClient()); }); - it.each(buildTestContent())('can open drop down if enabled', async (commentOrPost) => { - renderComponent(commentOrPost, { disabled: false }); + it.each(Object.values(buildTestContent()))('can open drop down if enabled', async (commentOrPost) => { + await mockThreadAndComment(commentOrPost); + renderComponent({ ...commentOrPost }); const openButton = await findOpenActionsDropdownButton(); await act(async () => { @@ -141,8 +196,9 @@ describe('ActionsDropdown', () => { await waitFor(() => expect(screen.queryByTestId('actions-dropdown-modal-popup')).toBeInTheDocument()); }); - it.each(buildTestContent())('can not open drop down if disabled', async (commentOrPost) => { - renderComponent(commentOrPost, { disabled: true }); + it.each(Object.values(buildTestContent()))('can not open drop down if disabled', async (commentOrPost) => { + await mockThreadAndComment(commentOrPost); + renderComponent({ ...commentOrPost, disabled: true }); const openButton = await findOpenActionsDropdownButton(); await act(async () => { @@ -153,11 +209,9 @@ describe('ActionsDropdown', () => { }); it('copy link action should be visible on posts', async () => { - const commentOrPost = { - testFor: 'thread', - ...camelCaseObject(Factory.build('thread', { editable_fields: ['copy_link'] }, null)), - }; - renderComponent(commentOrPost, { disabled: false }); + const discussionObject = buildTestContent({ editable_fields: ['copy_link'] }).discussion; + await mockThreadAndComment(discussionObject); + renderComponent({ ...camelCaseObject(discussionObject) }); const openButton = await findOpenActionsDropdownButton(); await act(async () => { @@ -168,11 +222,9 @@ describe('ActionsDropdown', () => { }); it('copy link action should not be visible on a comment', async () => { - const commentOrPost = { - testFor: 'comments', - ...camelCaseObject(Factory.build('comment', {}, null)), - }; - renderComponent(commentOrPost, { disabled: false }); + const commentObject = buildTestContent().comment; + await mockThreadAndComment(commentObject); + renderComponent({ ...camelCaseObject(commentObject) }); const openButton = await findOpenActionsDropdownButton(); await act(async () => { @@ -183,15 +235,13 @@ describe('ActionsDropdown', () => { }); describe.each(canPerformActionTestData)('Actions', ({ - testFor, action, label, reason, ...commentOrPost + testFor, action, label, ...commentOrPost }) => { describe(`for ${testFor}`, () => { it(`can "${label}" when allowed`, async () => { + await mockThreadAndComment(commentOrPost); const mockHandler = jest.fn(); - renderComponent( - commentOrPost, - { actionHandlers: { [action]: mockHandler } }, - ); + renderComponent({ ...commentOrPost, actionHandlers: { [action]: mockHandler } }); const openButton = await findOpenActionsDropdownButton(); await act(async () => { @@ -214,7 +264,8 @@ describe('ActionsDropdown', () => { }) => { describe(`for ${testFor}`, () => { it(`can't "${label}" when ${reason}`, async () => { - renderComponent(commentOrPost); + await mockThreadAndComment(commentOrPost); + renderComponent({ ...commentOrPost }); const openButton = await findOpenActionsDropdownButton(); await act(async () => { diff --git a/src/discussions/common/AlertBanner.test.jsx b/src/discussions/common/AlertBanner.test.jsx index 9d6bba7f..bed58ea9 100644 --- a/src/discussions/common/AlertBanner.test.jsx +++ b/src/discussions/common/AlertBanner.test.jsx @@ -31,7 +31,14 @@ function renderComponent( value={{ courseId: 'course-v1:edX+TestX+Test_Course' }} > diff --git a/src/discussions/common/EndorsedAlertBanner.test.jsx b/src/discussions/common/EndorsedAlertBanner.test.jsx index 77bdaa44..98af41fb 100644 --- a/src/discussions/common/EndorsedAlertBanner.test.jsx +++ b/src/discussions/common/EndorsedAlertBanner.test.jsx @@ -8,6 +8,7 @@ import { AppProvider } from '@edx/frontend-platform/react'; import { ThreadType } from '../../data/constants'; import { initializeStore } from '../../store'; import messages from '../post-comments/messages'; +import { PostCommentsContext } from '../post-comments/postCommentsContext'; import { DiscussionContext } from './context'; import EndorsedAlertBanner from './EndorsedAlertBanner'; @@ -30,10 +31,18 @@ function renderComponent( - + + + + , diff --git a/src/discussions/learners/LearnerPostsView.test.jsx b/src/discussions/learners/LearnerPostsView.test.jsx index 6a4083b3..aa0f6db1 100644 --- a/src/discussions/learners/LearnerPostsView.test.jsx +++ b/src/discussions/learners/LearnerPostsView.test.jsx @@ -82,6 +82,10 @@ describe('Learner Posts View', () => { await executeThunk(fetchUserPosts(courseId), store.dispatch, store.getState); }); + afterEach(() => { + axiosMock.reset(); + }); + test('Reported icon is visible to moderator for post with reported comment', async () => { await setUpPrivilages(axiosMock, store, true); await waitFor(() => { renderComponent(); }); @@ -112,7 +116,9 @@ describe('Learner Posts View', () => { const backButton = screen.getByLabelText('Back'); - await act(() => fireEvent.click(backButton)); + await act(() => { + fireEvent.click(backButton); + }); await waitFor(() => { expect(lastLocation.pathname.endsWith('/learners')).toBeTruthy(); }); @@ -128,15 +134,13 @@ describe('Learner Posts View', () => { expect(recentActivity).toBeInTheDocument(); }); - it(`should display a list of the interactive posts of a selected learner and the posts count - should be equal to the API response count.`, async () => { + it('should display a list of the interactive posts of a selected learner', async () => { await waitFor(() => { renderComponent(); }); const posts = await container.querySelectorAll('.discussion-post'); expect(posts).toHaveLength(2); - expect(posts).toHaveLength(Object.values(store.getState().threads.threadsById).length); }); it.each([ @@ -162,9 +166,9 @@ describe('Learner Posts View', () => { await act(async () => { fireEvent.click(activity); }); + await waitFor(() => { const learners = container.querySelectorAll('.discussion-post'); - expect(learners).toHaveLength(result); }); }); @@ -175,8 +179,7 @@ describe('Learner Posts View', () => { { searchBy: 'cohort-1', result: 2 }, ])('successfully display learners by %s.', async ({ searchBy, result }) => { await setUpPrivilages(axiosMock, store, true); - axiosMock.onGet(getCohortsApiUrl(courseId)) - .reply(200, Factory.buildList('cohort', 3)); + axiosMock.onGet(getCohortsApiUrl(courseId)).reply(200, Factory.buildList('cohort', 3)); await executeThunk(fetchCourseCohorts(courseId), store.dispatch, store.getState); await renderComponent(); @@ -191,9 +194,9 @@ describe('Learner Posts View', () => { await act(async () => { fireEvent.click(cohort); }); + await waitFor(() => { const learners = container.querySelectorAll('.discussion-post'); - expect(learners).toHaveLength(result); }); }); @@ -211,6 +214,6 @@ describe('Learner Posts View', () => { }); expect(loadMoreButton).not.toBeInTheDocument(); - expect(container.querySelectorAll('.discussion-post')).toHaveLength(2); + expect(container.querySelectorAll('.discussion-post')).toHaveLength(4); }); }); diff --git a/src/discussions/learners/LearnersView.test.jsx b/src/discussions/learners/LearnersView.test.jsx index 45631178..0e39f927 100644 --- a/src/discussions/learners/LearnersView.test.jsx +++ b/src/discussions/learners/LearnersView.test.jsx @@ -37,6 +37,7 @@ function renderComponent() { diff --git a/src/discussions/learners/data/__factories__/learners.factory.js b/src/discussions/learners/data/__factories__/learners.factory.js index f49a548f..f2badf63 100644 --- a/src/discussions/learners/data/__factories__/learners.factory.js +++ b/src/discussions/learners/data/__factories__/learners.factory.js @@ -1,5 +1,7 @@ import { Factory } from 'rosie'; +import '../../../posts/data/__factories__'; + Factory.define('learner') .sequence('id') .attr('username', ['id'], (id) => `learner-${id}`) @@ -91,50 +93,8 @@ Factory.define('learnerPosts') 'results', ['abuseFlaggedCount', 'courseId'], (abuseFlaggedCount, courseId) => { - const threads = []; - for (let i = 0; i < 2; i++) { - threads.push({ - created_at: new Date().toISOString(), - updated_at: new Date().toISOString(), - editable_fields: [ - 'abuse_flagged', - 'following', - 'group_id', - 'raw_body', - 'closed', - 'read', - 'title', - 'topic_id', - 'type', - 'voted', - 'pinned', - ], - id: `post_id${i}`, - author: 'test_user', - author_label: 'Staff', - abuse_flagged: false, - can_delete: true, - voted: false, - vote_count: 1, - title: `Title ${i}`, - raw_body: `

body ${i}

`, - preview_body: `

body ${i}

`, - course_id: courseId, - group_id: null, - group_name: null, - abuse_flagged_count: abuseFlaggedCount, - following: false, - comment_count: 8, - unread_comment_count: 0, - endorsed_comment_list_url: null, - non_endorsed_comment_list_url: null, - read: false, - has_endorsed: false, - pinned: false, - topic_id: 'topic', - }); - } - return threads; + const attrs = { course_id: courseId, abuse_flagged_count: abuseFlaggedCount }; + return Factory.buildList('thread', 2, attrs); }, ) .attr('pagination', [], () => ({ diff --git a/src/discussions/learners/learner/LearnerFooter.jsx b/src/discussions/learners/learner/LearnerFooter.jsx index 2ee5bd5b..4d4c6704 100644 --- a/src/discussions/learners/learner/LearnerFooter.jsx +++ b/src/discussions/learners/learner/LearnerFooter.jsx @@ -23,8 +23,9 @@ const LearnerFooter = ({
+
{intl.formatMessage(messages.allActivity)}
@@ -38,8 +39,9 @@ const LearnerFooter = ({
+
{intl.formatMessage(messages.posts)}
@@ -54,8 +56,9 @@ const LearnerFooter = ({ {Boolean(canSeeLearnerReportedStats) && ( +
{Boolean(activeFlags) && ( diff --git a/src/discussions/learners/learner/LearnerFooter.test.jsx b/src/discussions/learners/learner/LearnerFooter.test.jsx index 61d16747..f748f8f4 100644 --- a/src/discussions/learners/learner/LearnerFooter.test.jsx +++ b/src/discussions/learners/learner/LearnerFooter.test.jsx @@ -16,7 +16,15 @@ function renderComponent(learner) { return render( - + , ); diff --git a/src/discussions/posts/data/__factories__/threads.factory.js b/src/discussions/posts/data/__factories__/threads.factory.js index ed74d293..fbd9aab3 100644 --- a/src/discussions/posts/data/__factories__/threads.factory.js +++ b/src/discussions/posts/data/__factories__/threads.factory.js @@ -5,6 +5,7 @@ Factory.define('thread') .sequence('title', ['topic_id'], (idx, topicId) => `This is Thread-${idx} in topic ${topicId}`) .sequence('raw_body', (idx) => `Some contents for **thread number ${idx}**.`) .sequence('rendered_body', (idx) => `Some contents for thread number ${idx}.`) + .sequence('preview_body', (idx) => `Some contents for thread number ${idx}.`) .sequence('type', (idx) => (idx % 2 === 1 ? 'discussion' : 'question')) .sequence('pinned', idx => (idx < 3)) .sequence('topic_id', idx => `test-topic-${(idx % 3)}`) diff --git a/src/discussions/posts/post-editor/PostEditor.test.jsx b/src/discussions/posts/post-editor/PostEditor.test.jsx index 9d5cee12..6c8851b5 100644 --- a/src/discussions/posts/post-editor/PostEditor.test.jsx +++ b/src/discussions/posts/post-editor/PostEditor.test.jsx @@ -71,13 +71,12 @@ describe('PostEditor', () => { axiosMock = new MockAdapter(getAuthenticatedHttpClient()); const cwtopics = Factory.buildList('category', 2); Factory.reset('topic'); - axiosMock - .onGet(topicsApiUrl) - .reply(200, { - courseware_topics: cwtopics, - non_courseware_topics: Factory.buildList('topic', 3, {}, { topicPrefix: 'ncw-' }), - }); + axiosMock.onGet(topicsApiUrl).reply(200, { + courseware_topics: cwtopics, + non_courseware_topics: Factory.buildList('topic', 3, {}, { topicPrefix: 'ncw-' }), + }); }); + describe.each([ { allowAnonymous: false, @@ -110,50 +109,35 @@ describe('PostEditor', () => { }); await executeThunk(fetchCourseTopics(courseId), store.dispatch, store.getState); }); - test( - `new post when anonymous posts are ${allowAnonymous ? '' : 'not '}allowed and anonymous posts to peers are ${allowAnonymousToPeers ? '' : 'not '}allowed`, - async () => { - await renderComponent(); - expect(screen.queryByRole('heading')) - .toHaveTextContent('Add a post'); - expect(screen.queryAllByRole('radio')) - .toHaveLength(2); - // 2 categories with 4 subcategories each - expect(screen.queryAllByText(/category-\d-topic \d/)) - .toHaveLength(8); - // 3 non courseare topics - expect(screen.queryAllByText(/ncw-topic \d/)) - .toHaveLength(3); + test(`new post when anonymous posts are ${allowAnonymous ? '' : 'not'} allowed and anonymous posts to peers are ${ + allowAnonymousToPeers ? '' : 'not'} allowed`, async () => { + await renderComponent(); + + expect(screen.queryByRole('heading')).toHaveTextContent('Add a post'); + expect(screen.queryAllByRole('radio')).toHaveLength(2); + // 2 categories with 4 subcategories each + expect(screen.queryAllByText(/category-\d-topic \d/)).toHaveLength(8); + // 3 non courseare topics + expect(screen.queryAllByText(/ncw-topic \d/)).toHaveLength(3); + expect(screen.queryByText('cohort', { exact: false })).not.toBeInTheDocument(); + expect(screen.queryByText('Post anonymously')).not.toBeInTheDocument(); + + if (allowAnonymousToPeers) { + expect(screen.queryByText('Post anonymously to peers')).toBeInTheDocument(); + } else { + expect(screen.queryByText('Post anonymously to peers')).not.toBeInTheDocument(); + } + }); - expect(screen.queryByText('cohort', { exact: false })) - .not.toBeInTheDocument(); - if (allowAnonymous) { - expect(screen.queryByText('Post anonymously')) - .not.toBeInTheDocument(); - } else { - expect(screen.queryByText('Post anonymously')) - .not.toBeInTheDocument(); - } - if (allowAnonymousToPeers) { - expect(screen.queryByText('Post anonymously to peers')) - .toBeInTheDocument(); - } else { - expect(screen.queryByText('Post anonymously to peers')) - .not - .toBeInTheDocument(); - } - }, - ); test('selectThread is not called while creating a new post', async () => { const mockSelectThread = jest.fn(); jest.mock('../data/selectors', () => ({ selectThread: mockSelectThread, })); await renderComponent(); - expect(mockSelectThread) - .not - .toHaveBeenCalled(); + + expect(mockSelectThread).not.toHaveBeenCalled(); }); }); @@ -162,8 +146,7 @@ describe('PostEditor', () => { const dividedcw = ['category-1-topic-2', 'category-2-topic-1', 'category-2-topic-2']; beforeEach(async () => { - axiosMock.onGet(getCohortsApiUrl(courseId)) - .reply(200, Factory.buildList('cohort', 3)); + axiosMock.onGet(getCohortsApiUrl(courseId)).reply(200, Factory.buildList('cohort', 3)); }); async function setupData(config = {}, settings = {}) { @@ -187,69 +170,46 @@ describe('PostEditor', () => { await setupData(); await renderComponent(); // Initially the user can't select a cohort - expect(screen.queryByRole('combobox', { - name: /cohort visibility/i, - })) - .not - .toBeInTheDocument(); + expect(screen.queryByRole('combobox', { name: /cohort visibility/i })).not.toBeInTheDocument(); // If a cohorted topic is selected, the cohort visibility selector is displayed - ['ncw-topic 2', 'category-1-topic 2', 'category-2-topic 1', 'category-2-topic 2'].forEach((topicName) => { + ['ncw-topic 2', 'category-1-topic 2', 'category-2-topic 1', 'category-2-topic 2'].forEach(async (topicName) => { act(() => { userEvent.selectOptions( - screen.getByRole('combobox', { - name: /topic area/i, - }), + screen.getByRole('combobox', { name: /topic area/i }), screen.getByRole('option', { name: topicName }), ); }); - expect(screen.queryByRole('combobox', { - name: /cohort visibility/i, - })) - .toBeInTheDocument(); + expect(screen.queryByRole('combobox', { name: /cohort visibility/i })).toBeInTheDocument(); }); // Now if a non-cohorted topic is selected, the cohort visibility selector is hidden - ['ncw-topic 1', 'category-1-topic 1', 'category-2-topic 4'].forEach((topicName) => { + ['ncw-topic 1', 'category-1-topic 1', 'category-2-topic 4'].forEach(async (topicName) => { act(() => { userEvent.selectOptions( - screen.getByRole('combobox', { - name: /topic area/i, - }), + screen.getByRole('combobox', { name: /topic area/i }), screen.getByRole('option', { name: topicName }), ); }); - expect(screen.queryByRole('combobox', { - name: /cohort visibility/i, - })) - .not - .toBeInTheDocument(); + expect(screen.queryByRole('combobox', { name: /cohort visibility/i })).not.toBeInTheDocument(); }); }); + test('test always divided inline', async () => { await setupData({}, { alwaysDivideInlineDiscussions: true }); await renderComponent(); // Initially the user can't select a cohort - expect(screen.queryByRole('combobox', { - name: /cohort visibility/i, - })) - .not - .toBeInTheDocument(); + expect(screen.queryByRole('combobox', { name: /cohort visibility/i })).not.toBeInTheDocument(); // All coursweare topics are divided [1, 2].forEach(catId => { [1, 2, 3, 4].forEach((topicId) => { act(() => { userEvent.selectOptions( - screen.getByRole('combobox', { - name: /topic area/i, - }), + screen.getByRole('combobox', { name: /topic area/i }), screen.getByRole('option', { name: `category-${catId}-topic ${topicId}` }), ); }); - expect(screen.queryByRole('combobox', { - name: /cohort visibility/i, - })) - .toBeInTheDocument(); + expect(screen.queryByRole('combobox', { name: /cohort visibility/i })).toBeInTheDocument(); }); }); @@ -257,46 +217,38 @@ describe('PostEditor', () => { ['ncw-topic 1', 'ncw-topic 3'].forEach((topicName) => { act(() => { userEvent.selectOptions( - screen.getByRole('combobox', { - name: /topic area/i, - }), + screen.getByRole('combobox', { name: /topic area/i }), screen.getByRole('option', { name: topicName }), ); }); - expect(screen.queryByRole('combobox', { - name: /cohort visibility/i, - })) - .not - .toBeInTheDocument(); + expect(screen.queryByRole('combobox', { name: /cohort visibility/i })).not.toBeInTheDocument(); }); }); + test('test unprivileged user', async () => { await setupData({ hasModerationPrivileges: false }); await renderComponent(); + ['ncw-topic 1', 'ncw-topic 2', 'category-1-topic 1', 'category-2-topic 1'].forEach((topicName) => { act(() => { userEvent.selectOptions( - screen.getByRole('combobox', { - name: /topic area/i, - }), + screen.getByRole('combobox', { name: /topic area/i }), screen.getByRole('option', { name: topicName }), ); }); // If a cohorted topic is selected, the cohort visibility selector is displayed - expect(screen.queryByRole('combobox', { - name: /cohort visibility/i, - })) - .not - .toBeInTheDocument(); + expect(screen.queryByRole('combobox', { name: /cohort visibility/i })).not.toBeInTheDocument(); }); }); + test('edit existing post should not show cohort selector to unprivileged users', async () => { const threadId = 'thread-1'; await setupData({ hasModerationPrivileges: false }); - axiosMock.onGet(`${threadsApiUrl}${threadId}/`) - .reply(200, Factory.build('thread')); - await executeThunk(fetchThread(threadId), store.dispatch, store.getState); - await renderComponent(true, `/${courseId}/posts/${threadId}/edit`); + await act(async () => { + axiosMock.onGet(`${threadsApiUrl}${threadId}/`).reply(200, Factory.build('thread')); + await executeThunk(fetchThread(threadId), store.dispatch, store.getState); + await renderComponent(true, `/${courseId}/posts/${threadId}/edit`); + }); ['ncw-topic 1', 'ncw-topic 2', 'category-1-topic 1', 'category-2-topic 1'].forEach((topicName) => { act(() => { @@ -308,20 +260,18 @@ describe('PostEditor', () => { ); }); // If a cohorted topic is selected, the cohort visibility selector is displayed - expect(screen.queryByRole('combobox', { - name: /cohort visibility/i, - })) - .not - .toBeInTheDocument(); + expect(screen.queryByRole('combobox', { name: /cohort visibility/i })).not.toBeInTheDocument(); }); }); + test('cancel posting of existing post', async () => { const threadId = 'thread-1'; await setupData(); - axiosMock.onGet(`${threadsApiUrl}${threadId}/`) - .reply(200, Factory.build('thread')); - await executeThunk(fetchThread(threadId), store.dispatch, store.getState); - await renderComponent(true, `/${courseId}/posts/${threadId}/edit`); + await act(async () => { + axiosMock.onGet(`${threadsApiUrl}${threadId}/`).reply(200, Factory.build('thread')); + await executeThunk(fetchThread(threadId), store.dispatch, store.getState); + await renderComponent(true, `/${courseId}/posts/${threadId}/edit`); + }); const cancelButton = screen.getByRole('button', { name: /cancel/i }); await act(async () => { @@ -333,6 +283,7 @@ describe('PostEditor', () => { describe('Edit codes', () => { const threadId = 'thread-1'; + beforeEach(async () => { const dividedncw = ['ncw-topic-2']; const dividedcw = ['category-1-topic-2', 'category-2-topic-1', 'category-2-topic-2']; @@ -359,26 +310,26 @@ describe('PostEditor', () => { }, }); await executeThunk(fetchCourseTopics(courseId), store.dispatch, store.getState); - axiosMock.onGet(`${threadsApiUrl}${threadId}/`) - .reply(200, Factory.build('thread')); + axiosMock.onGet(`${threadsApiUrl}${threadId}/`).reply(200, Factory.build('thread')); await executeThunk(fetchThread(threadId), store.dispatch, store.getState); }); - test('Edit post and see reasons', async () => { - await renderComponent(true, `/${courseId}/posts/${threadId}/edit`); - expect(screen.queryByRole('combobox', { - name: /reason for editing/i, - })) - .toBeInTheDocument(); - expect(screen.getAllByRole('option', { - name: /reason \d/i, - })).toHaveLength(2); + test('Edit post and see reasons', async () => { + await act(async () => { + await renderComponent(true, `/${courseId}/posts/${threadId}/edit`); + }); + + expect(screen.queryByRole('combobox', { name: /reason for editing/i })).toBeInTheDocument(); + expect(screen.getAllByRole('option', { name: /reason \d/i })).toHaveLength(2); }); it('should show Preview Panel', async () => { await renderComponent(true, `/${courseId}/posts/${threadId}/edit`); - await act(() => fireEvent.click(container.querySelector('[data-testid="show-preview-button"]'))); + await act(async () => { + fireEvent.click(container.querySelector('[data-testid="show-preview-button"]')); + }); + await waitFor(() => { expect(container.querySelector('[data-testid="hide-preview-button"]')).toBeInTheDocument(); expect(container.querySelector('[data-testid="show-preview-button"]')).not.toBeInTheDocument(); @@ -388,12 +339,18 @@ describe('PostEditor', () => { it('should hide Preview Panel', async () => { await renderComponent(true, `/${courseId}/posts/${threadId}/edit`); - await act(() => fireEvent.click(container.querySelector('[data-testid="show-preview-button"]'))); + await act(async () => { + fireEvent.click(container.querySelector('[data-testid="show-preview-button"]')); + }); + await waitFor(() => { expect(container.querySelector('[data-testid="hide-preview-button"]')).toBeInTheDocument(); }); - await act(() => fireEvent.click(container.querySelector('[data-testid="hide-preview-button"]'))); + await act(async () => { + fireEvent.click(container.querySelector('[data-testid="hide-preview-button"]')); + }); + await waitFor(() => { expect(container.querySelector('[data-testid="show-preview-button"]')).toBeInTheDocument(); expect(container.querySelector('[data-testid="hide-preview-button"]')).not.toBeInTheDocument(); diff --git a/src/discussions/posts/post/PostFooter.test.jsx b/src/discussions/posts/post/PostFooter.test.jsx index 2dcc7ae3..56cca348 100644 --- a/src/discussions/posts/post/PostFooter.test.jsx +++ b/src/discussions/posts/post/PostFooter.test.jsx @@ -18,7 +18,17 @@ function renderComponent(post, userHasModerationPrivileges = false) { return render( - + , ); @@ -88,7 +98,7 @@ describe('PostFooter', () => { expect(store.getState().threads.status === RequestStatus.IN_PROGRESS).toBeTruthy(); }); - it('test follow button when following=false', async () => { + it('test follow button when following = false', async () => { renderComponent({ ...mockPost, following: false }); expect(screen.queryByRole('button', { name: /follow/i })).not.toBeInTheDocument(); }); diff --git a/src/discussions/posts/post/PostLink.test.jsx b/src/discussions/posts/post/PostLink.test.jsx index 03bbbda2..d3729367 100644 --- a/src/discussions/posts/post/PostLink.test.jsx +++ b/src/discussions/posts/post/PostLink.test.jsx @@ -3,6 +3,7 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; import MockAdapter from 'axios-mock-adapter'; import { IntlProvider } from 'react-intl'; +import { Factory } from 'rosie'; import { initializeMockApp } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; @@ -13,48 +14,52 @@ import { executeThunk } from '../../../test-utils'; import { DiscussionContext } from '../../common/context'; import { getCourseConfigApiUrl } from '../../data/api'; import { fetchCourseConfig } from '../../data/thunks'; +import { getThreadsApiUrl } from '../data/api'; +import { fetchThread } from '../data/thunks'; import PostLink from './PostLink'; +import '../data/__factories__'; + const courseId = 'course-v1:edX+DemoX+Demo_Course'; const courseConfigApiUrl = getCourseConfigApiUrl(); +const threadsApiUrl = getThreadsApiUrl(); +const postId = 'thread-1'; let store; let axiosMock; -function renderComponent(post) { +const mockThread = async (id, abuseFlagged) => { + store = initializeStore(); + Factory.resetAll(); + + axiosMock = new MockAdapter(getAuthenticatedHttpClient()); + axiosMock.onGet(`${courseConfigApiUrl}${courseId}/settings`).reply(200, {}); + axiosMock.onGet(`${courseConfigApiUrl}${courseId}/`).reply(200, { + learners_tab_enabled: true, + has_moderation_privileges: true, + }); + axiosMock.onGet(`${threadsApiUrl}${id}/`).reply(200, Factory.build('thread', { + id, abuse_flagged: abuseFlagged, + })); + + await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState); + await executeThunk(fetchThread(id), store.dispatch, store.getState); +}; + +function renderComponent(id) { return render( - true} /> + , ); } -const mockPost = { - abuseFlagged: false, - author: 'test-user', - commentCount: 5, - courseId: 'course-v1:edX+DemoX+Demo_Course', - following: false, - id: 'test-id', - pinned: false, - rawBody: '

a test post

', - hasEndorsed: false, - voted: false, - voteCount: 10, - previewBody: '

a test post

', - read: false, - title: 'test post', - topicId: 'i4x-edx-eiorguegnru-course-foobarbaz', - unreadCommentCount: 2, - groupName: null, - groupId: null, - createdAt: '2022-02-25T09:17:17Z', - closed: false, -}; - describe('PostFooter', () => { beforeEach(async () => { initializeMockApp({ @@ -65,21 +70,17 @@ describe('PostFooter', () => { roles: [], }, }); - store = initializeStore(); - axiosMock = new MockAdapter(getAuthenticatedHttpClient()); - axiosMock.onGet(`${courseConfigApiUrl}${courseId}/`).reply(200, { - has_moderation_privileges: true, - }); - axiosMock.onGet(`${courseConfigApiUrl}${courseId}/settings`).reply(200, {}); - await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState); }); - it('has reported text only when abuseFlagged is true', async () => { - renderComponent(mockPost); - expect(screen.queryByTestId('reported-post')).toBeFalsy(); + it.each([true, false])('has reported text only when abuseFlagged is %s', async (abuseFlagged) => { + await mockThread(postId, abuseFlagged); + renderComponent(postId); - renderComponent({ ...mockPost, abuseFlagged: true }); - expect(screen.getByTestId('reported-post')).toBeTruthy(); + if (abuseFlagged) { + expect(screen.getByText('Reported')).toBeTruthy(); + } else { + expect(screen.queryByTestId('reported-post')).toBeFalsy(); + } }); }); @@ -93,21 +94,15 @@ describe('Post username', () => { roles: [], }, }); - store = initializeStore(); - axiosMock = new MockAdapter(getAuthenticatedHttpClient()); - axiosMock.onGet(`${courseConfigApiUrl}${courseId}/`).reply(200, { - learners_tab_enabled: true, - has_moderation_privileges: true, - }); - axiosMock.onGet(`${courseConfigApiUrl}${courseId}/settings`).reply(200, {}); - await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState); }); it.each([ 'anonymous', 'test-user', - ])('is not clickable for %s user', async (user) => { - renderComponent({ ...mockPost, author: user }); + ])('is not clickable for %s user', async () => { + await mockThread(postId, false); + renderComponent(postId); + expect(screen.queryByTestId('learner-posts-link')).not.toBeInTheDocument(); }); });