diff --git a/public/index.html b/public/index.html index 7ef8a8a4..fbb12a47 100644 --- a/public/index.html +++ b/public/index.html @@ -9,9 +9,10 @@ href="<%=htmlWebpackPlugin.options.FAVICON_URL%>" type="image/x-icon" /> - + + - -
diff --git a/src/components/HTMLLoader.jsx b/src/components/HTMLLoader.jsx index 18bff087..584917ab 100644 --- a/src/components/HTMLLoader.jsx +++ b/src/components/HTMLLoader.jsx @@ -1,9 +1,31 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import DOMPurify from 'dompurify'; -import { logError } from '@edx/frontend-platform/logging'; +const baseConfig = { + showMathMenu: true, + tex2jax: { + inlineMath: [ + ['$', '$'], + ['\\\\(', '\\\\)'], + ['\\(', '\\)'], + ['[mathjaxinline]', '[/mathjaxinline]'], + ['\\begin{math}', '\\end{math}'], + ], + displayMath: [ + ['[mathjax]', '[/mathjax]'], + ['$$', '$$'], + ['\\\\[', '\\\\]'], + ['\\[', '\\]'], + ['\\begin{displaymath}', '\\end{displaymath}'], + ['\\begin{equation}', '\\end{equation}'], + ], + processEscapes: true, + processEnvironments: true, + }, + skipStartupTypeset: false, +}; const defaultSanitizeOptions = { USE_PROFILES: { html: true }, @@ -11,28 +33,50 @@ const defaultSanitizeOptions = { }; function HTMLLoader({ htmlNode, componentId, cssClassName }) { + const [loadingState, setLoadingState] = useState(window.MathJax ? 'loaded' : 'loading'); const sanitizedMath = DOMPurify.sanitize(htmlNode, { ...defaultSanitizeOptions }); + const mathjaxScript = 'https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.7/MathJax.js?config=TeX-MML-AM_SVG'; useEffect(() => { - let promise = Promise.resolve(); // Used to hold chain of typesetting calls - function typeset(code) { - promise = promise.then(() => window.MathJax.typesetPromise(code())) - .catch((err) => logError(`Typeset failed: ${err.message}`)); - return promise; - } + let mathjaxScriptTag = document.querySelector(`script[src="${mathjaxScript}"]`); + if (!mathjaxScriptTag) { + mathjaxScriptTag = document.createElement('script'); + mathjaxScriptTag.async = true; + mathjaxScriptTag.src = mathjaxScript; - typeset(() => { - const math = document.querySelector('#tex2jax_process'); - math.innerHTML = sanitizedMath; - return [math]; - }); - }, [sanitizedMath]); + const node = document.head || document.getElementsByTagName('head')[0]; + node.appendChild(mathjaxScriptTag); + } + const onloadHandler = () => { + setLoadingState('loaded'); + window.MathJax.Hub.Config({ ...baseConfig }); + }; + const onerrorHandler = () => { + setLoadingState('failed'); + }; + + return () => { + mathjaxScriptTag.removeEventListener('load', onloadHandler); + mathjaxScriptTag.removeEventListener('error', onerrorHandler); + }; + }, [setLoadingState, baseConfig]); + + useEffect(() => { + if (loadingState !== 'loaded') { + return; + } + window.MathJax.Hub.Queue([ + 'Typeset', + window.MathJax.Hub, + 'mathjax-code', + ]); + }, [sanitizedMath, loadingState]); return ( -
-
+
+ {/* eslint-disable-next-line react/no-danger */} +
- ); } diff --git a/src/discussions/comments/CommentsView.test.jsx b/src/discussions/comments/CommentsView.test.jsx index 90874cef..9f8b7910 100644 --- a/src/discussions/comments/CommentsView.test.jsx +++ b/src/discussions/comments/CommentsView.test.jsx @@ -1,5 +1,5 @@ import { - act, fireEvent, render, screen, waitFor, /* within, */ + act, fireEvent, render, screen, waitFor, within, } from '@testing-library/react'; import MockAdapter from 'axios-mock-adapter'; import { IntlProvider } from 'react-intl'; @@ -32,7 +32,7 @@ const closedPostId = 'thread-2'; const courseId = 'course-v1:edX+TestX+Test_Course'; let store; let axiosMock; -// let testLocation; +let testLocation; function mockAxiosReturnPagedComments() { [null, false, true].forEach(endorsed => { @@ -92,7 +92,10 @@ function renderComponent(postId) { null} + render={({ location }) => { + testLocation = location; + return null; + }} /> @@ -157,43 +160,43 @@ describe('CommentsView', () => { expect(JSON.parse(axiosMock.history.patch[axiosMock.history.patch.length - 1].data)).toMatchObject(data); } - // it('should show and hide the editor', async () => { - // renderComponent(discussionPostId); - // await waitFor(() => screen.findByText('comment number 1', { exact: false })); - // const addResponseButtons = screen.getAllByRole('button', { name: /add a response/i }); - // await act(async () => { - // fireEvent.click( - // addResponseButtons[0], - // ); - // }); - // expect(screen.queryByTestId('tinymce-editor')).toBeInTheDocument(); - // await act(async () => { - // fireEvent.click(screen.getByRole('button', { name: /cancel/i })); - // }); - // expect(screen.queryByTestId('tinymce-editor')).not.toBeInTheDocument(); - // }); + it('should show and hide the editor', async () => { + renderComponent(discussionPostId); + await waitFor(() => screen.findByText('comment number 1', { exact: false })); + const addResponseButtons = screen.getAllByRole('button', { name: /add a response/i }); + await act(async () => { + fireEvent.click( + addResponseButtons[0], + ); + }); + expect(screen.queryByTestId('tinymce-editor')).toBeInTheDocument(); + await act(async () => { + fireEvent.click(screen.getByRole('button', { name: /cancel/i })); + }); + expect(screen.queryByTestId('tinymce-editor')).not.toBeInTheDocument(); + }); - // it('should allow posting a response', async () => { - // renderComponent(discussionPostId); - // await waitFor(() => screen.findByText('comment number 1', { exact: false })); - // const responseButtons = screen.getAllByRole('button', { name: /add a response/i }); - // await act(async () => { - // fireEvent.click( - // responseButtons[0], - // ); - // }); - // await act(() => { - // fireEvent.change(screen.getByTestId('tinymce-editor'), { target: { value: 'testing123' } }); - // }); - // - // await act(async () => { - // fireEvent.click( - // screen.getByText(/submit/i), - // ); - // }); - // expect(screen.queryByTestId('tinymce-editor')).not.toBeInTheDocument(); - // await waitFor(async () => expect(await screen.findByText('testing123', { exact: false })).toBeInTheDocument()); - // }); + it('should allow posting a response', async () => { + renderComponent(discussionPostId); + await waitFor(() => screen.findByText('comment number 1', { exact: false })); + const responseButtons = screen.getAllByRole('button', { name: /add a response/i }); + await act(async () => { + fireEvent.click( + responseButtons[0], + ); + }); + await act(() => { + fireEvent.change(screen.getByTestId('tinymce-editor'), { target: { value: 'testing123' } }); + }); + + await act(async () => { + fireEvent.click( + screen.getByText(/submit/i), + ); + }); + expect(screen.queryByTestId('tinymce-editor')).not.toBeInTheDocument(); + await waitFor(async () => expect(await screen.findByText('testing123', { exact: false })).toBeInTheDocument()); + }); it('should not allow posting a response on a closed post', async () => { renderComponent(closedPostId); @@ -201,26 +204,26 @@ describe('CommentsView', () => { expect(screen.queryByRole('button', { name: /add a response/i })).not.toBeInTheDocument(); }); - // it('should allow posting a comment', async () => { - // renderComponent(discussionPostId); - // await waitFor(() => screen.findByText('comment number 1', { exact: false })); - // await act(async () => { - // fireEvent.click( - // screen.getAllByRole('button', { name: /add a comment/i })[0], - // ); - // }); - // act(() => { - // fireEvent.change(screen.getByTestId('tinymce-editor'), { target: { value: 'testing123' } }); - // }); - // - // await act(async () => { - // fireEvent.click( - // screen.getByText(/submit/i), - // ); - // }); - // expect(screen.queryByTestId('tinymce-editor')).not.toBeInTheDocument(); - // await waitFor(async () => expect(await screen.findByText('testing123', { exact: false })).toBeInTheDocument()); - // }); + it('should allow posting a comment', async () => { + renderComponent(discussionPostId); + await waitFor(() => screen.findByText('comment number 1', { exact: false })); + await act(async () => { + fireEvent.click( + screen.getAllByRole('button', { name: /add a comment/i })[0], + ); + }); + act(() => { + fireEvent.change(screen.getByTestId('tinymce-editor'), { target: { value: 'testing123' } }); + }); + + await act(async () => { + fireEvent.click( + screen.getByText(/submit/i), + ); + }); + expect(screen.queryByTestId('tinymce-editor')).not.toBeInTheDocument(); + await waitFor(async () => expect(await screen.findByText('testing123', { exact: false })).toBeInTheDocument()); + }); it('should not allow posting a comment on a closed post', async () => { renderComponent(closedPostId); @@ -232,29 +235,29 @@ describe('CommentsView', () => { }); }); - // it('should allow editing an existing comment', async () => { - // renderComponent(discussionPostId); - // await waitFor(() => screen.findByText('comment number 1', { exact: false })); - // await act(async () => { - // fireEvent.click( - // // The first edit menu is for the post, the second will be for the first comment. - // screen.getAllByRole('button', { name: /actions menu/i })[1], - // ); - // }); - // await act(async () => { - // fireEvent.click(screen.getByRole('button', { name: /edit/i })); - // }); - // act(() => { - // fireEvent.change(screen.getByTestId('tinymce-editor'), { target: { value: 'testing123' } }); - // }); - // await act(async () => { - // fireEvent.click(screen.getByRole('button', { name: /submit/i })); - // }); - // await waitFor(async () => { - // expect(await screen.findByText('testing123', { exact: false })).toBeInTheDocument(); - // }); - // }); - // + it('should allow editing an existing comment', async () => { + renderComponent(discussionPostId); + await waitFor(() => screen.findByText('comment number 1', { exact: false })); + await act(async () => { + fireEvent.click( + // The first edit menu is for the post, the second will be for the first comment. + screen.getAllByRole('button', { name: /actions menu/i })[1], + ); + }); + await act(async () => { + fireEvent.click(screen.getByRole('button', { name: /edit/i })); + }); + act(() => { + fireEvent.change(screen.getByTestId('tinymce-editor'), { target: { value: 'testing123' } }); + }); + await act(async () => { + fireEvent.click(screen.getByRole('button', { name: /submit/i })); + }); + await waitFor(async () => { + expect(await screen.findByText('testing123', { exact: false })).toBeInTheDocument(); + }); + }); + async function setupCourseConfig(reasonCodesEnabled = true) { axiosMock.onGet(`${courseConfigApiUrl}${courseId}/`).reply(200, { has_moderation_privileges: true, @@ -271,83 +274,81 @@ describe('CommentsView', () => { axiosMock.onGet(`${courseConfigApiUrl}${courseId}/settings`).reply(200, {}); await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState); } - // - // it('should show reason codes when editing an existing comment', async () => { - // setupCourseConfig(); - // renderComponent(discussionPostId); - // await waitFor(() => screen.findByText('comment number 1', { exact: false })); - // await act(async () => { - // fireEvent.click( - // // The first edit menu is for the post, the second will be for the first comment. - // screen.getAllByRole('button', { name: /actions menu/i })[1], - // ); - // }); - // await act(async () => { - // fireEvent.click(screen.getByRole('button', { name: /edit/i })); - // }); - // expect(screen.queryByRole('combobox', { name: /reason for editing/i })).toBeInTheDocument(); - // expect(screen.getAllByRole('option', { name: /reason \d/i })).toHaveLength(2); - // await act(async () => { - // fireEvent.change(screen.queryByRole('combobox', { name: /reason for editing/i }), - // { target: { value: null } }); - // }); - // await act(async () => { - // fireEvent.change(screen.queryByRole('combobox', - // { name: /reason for editing/i }), { target: { value: 'reason-1' } }); - // }); - // await act(async () => { - // fireEvent.change(screen.getByTestId('tinymce-editor'), { target: { value: 'testing123' } }); - // }); - // await act(async () => { - // fireEvent.click(screen.getByRole('button', { name: /submit/i })); - // }); - // assertLastUpdateData({ edit_reason_code: 'reason-1' }); - // }); - // - // it('should show reason codes when closing a post', async () => { - // setupCourseConfig(); - // renderComponent(discussionPostId); - // await act(async () => { - // fireEvent.click( - // // The first edit menu is for the post - // screen.getAllByRole('button', { - // name: /actions menu/i, - // })[0], - // ); - // }); - // expect(screen.queryByRole('dialog', { name: /close post/i })).not.toBeInTheDocument(); - // await act(async () => { - // fireEvent.click(screen.getByRole('button', { name: /close/i })); - // }); - // expect(screen.queryByRole('dialog', { name: /close post/i })).toBeInTheDocument(); - // expect(screen.queryByRole('combobox', { name: /reason/i })).toBeInTheDocument(); - // expect(screen.getAllByRole('option', { name: /reason \d/i })).toHaveLength(2); - // await act(async () => { - // fireEvent.change(screen.queryByRole('combobox', { name: /reason/i }), { target: { value: 'reason-1' } }); - // }); - // await act(async () => { - // fireEvent.click(screen.getByRole('button', { name: /close post/i })); - // }); - // expect(screen.queryByRole('dialog', { name: /close post/i })).not.toBeInTheDocument(); - // assertLastUpdateData({ closed: true, close_reason_code: 'reason-1' }); - // }); - // it('should close the post directly if reason codes are not enabled', async () => { - // setupCourseConfig(false); - // renderComponent(discussionPostId); - // await act(async () => { - // fireEvent.click( - // // The first edit menu is for the post - // screen.getAllByRole('button', { name: /actions menu/i })[0], - // ); - // }); - // expect(screen.queryByRole('dialog', { name: /close post/i })).not.toBeInTheDocument(); - // await act(async () => { - // fireEvent.click(screen.getByRole('button', { name: /close/i })); - // }); - // expect(screen.queryByRole('dialog', { name: /close post/i })).not.toBeInTheDocument(); - // assertLastUpdateData({ closed: true }); - // }); + it('should show reason codes when editing an existing comment', async () => { + setupCourseConfig(); + renderComponent(discussionPostId); + await waitFor(() => screen.findByText('comment number 1', { exact: false })); + await act(async () => { + fireEvent.click( + // The first edit menu is for the post, the second will be for the first comment. + screen.getAllByRole('button', { name: /actions menu/i })[1], + ); + }); + await act(async () => { + fireEvent.click(screen.getByRole('button', { name: /edit/i })); + }); + expect(screen.queryByRole('combobox', { name: /reason for editing/i })).toBeInTheDocument(); + expect(screen.getAllByRole('option', { name: /reason \d/i })).toHaveLength(2); + await act(async () => { + fireEvent.change(screen.queryByRole('combobox', { name: /reason for editing/i }), { target: { value: null } }); + }); + await act(async () => { + fireEvent.change(screen.queryByRole('combobox', { name: /reason for editing/i }), { target: { value: 'reason-1' } }); + }); + await act(async () => { + fireEvent.change(screen.getByTestId('tinymce-editor'), { target: { value: 'testing123' } }); + }); + await act(async () => { + fireEvent.click(screen.getByRole('button', { name: /submit/i })); + }); + assertLastUpdateData({ edit_reason_code: 'reason-1' }); + }); + + it('should show reason codes when closing a post', async () => { + setupCourseConfig(); + renderComponent(discussionPostId); + await act(async () => { + fireEvent.click( + // The first edit menu is for the post + screen.getAllByRole('button', { + name: /actions menu/i, + })[0], + ); + }); + expect(screen.queryByRole('dialog', { name: /close post/i })).not.toBeInTheDocument(); + await act(async () => { + fireEvent.click(screen.getByRole('button', { name: /close/i })); + }); + expect(screen.queryByRole('dialog', { name: /close post/i })).toBeInTheDocument(); + expect(screen.queryByRole('combobox', { name: /reason/i })).toBeInTheDocument(); + expect(screen.getAllByRole('option', { name: /reason \d/i })).toHaveLength(2); + await act(async () => { + fireEvent.change(screen.queryByRole('combobox', { name: /reason/i }), { target: { value: 'reason-1' } }); + }); + await act(async () => { + fireEvent.click(screen.getByRole('button', { name: /close post/i })); + }); + expect(screen.queryByRole('dialog', { name: /close post/i })).not.toBeInTheDocument(); + assertLastUpdateData({ closed: true, close_reason_code: 'reason-1' }); + }); + + it('should close the post directly if reason codes are not enabled', async () => { + setupCourseConfig(false); + renderComponent(discussionPostId); + await act(async () => { + fireEvent.click( + // The first edit menu is for the post + screen.getAllByRole('button', { name: /actions menu/i })[0], + ); + }); + expect(screen.queryByRole('dialog', { name: /close post/i })).not.toBeInTheDocument(); + await act(async () => { + fireEvent.click(screen.getByRole('button', { name: /close/i })); + }); + expect(screen.queryByRole('dialog', { name: /close post/i })).not.toBeInTheDocument(); + assertLastUpdateData({ closed: true }); + }); it.each([true, false])( 'should reopen the post directly when reason codes enabled=%s', @@ -369,20 +370,20 @@ describe('CommentsView', () => { }, ); - // it('should show the editor if the post is edited', async () => { - // setupCourseConfig(false); - // renderComponent(discussionPostId); - // await act(async () => { - // fireEvent.click( - // // The first edit menu is for the post - // screen.getAllByRole('button', { name: /actions menu/i })[0], - // ); - // }); - // await act(async () => { - // fireEvent.click(screen.getByRole('button', { name: /edit/i })); - // }); - // expect(testLocation.pathname).toBe(`/${courseId}/posts/${discussionPostId}/edit`); - // }); + it('should show the editor if the post is edited', async () => { + setupCourseConfig(false); + renderComponent(discussionPostId); + await act(async () => { + fireEvent.click( + // The first edit menu is for the post + screen.getAllByRole('button', { name: /actions menu/i })[0], + ); + }); + await act(async () => { + fireEvent.click(screen.getByRole('button', { name: /edit/i })); + }); + expect(testLocation.pathname).toBe(`/${courseId}/posts/${discussionPostId}/edit`); + }); it('should allow pinning the post', async () => { renderComponent(discussionPostId); @@ -417,314 +418,314 @@ describe('CommentsView', () => { assertLastUpdateData({ abuse_flagged: true }); }); - // it('handles liking a comment', async () => { - // renderComponent(discussionPostId); - // - // // Wait for the content to load - // await screen.findByText('comment number 7', { exact: false }); - // const view = screen.getByTestId('comment-comment-1'); - // - // const likeButton = within(view).getByRole('button', { name: /like/i }); - // await act(async () => { - // fireEvent.click(likeButton); - // }); - // expect(axiosMock.history.patch).toHaveLength(2); - // expect(JSON.parse(axiosMock.history.patch[1].data)).toMatchObject({ voted: true }); - // }); - // - // it('handles endorsing comments', async () => { - // renderComponent(discussionPostId); - // // Wait for the content to load - // await screen.findByText('comment number 7', { exact: false }); - // - // // There should be three buttons, one for the post, the second for the - // // comment and the third for a response to that comment - // const actionButtons = screen.queryAllByRole('button', { name: /actions menu/i }); - // await act(async () => { - // fireEvent.click(actionButtons[1]); - // }); - // - // await act(async () => { - // fireEvent.click(screen.getByRole('button', { name: /Endorse/i })); - // }); - // expect(axiosMock.history.patch).toHaveLength(2); - // expect(JSON.parse(axiosMock.history.patch[1].data)).toMatchObject({ endorsed: true }); - // }); - // - // it('handles reporting comments', async () => { - // renderComponent(discussionPostId); - // // Wait for the content to load - // await screen.findByText('comment number 7', { exact: false }); - // - // // There should be three buttons, one for the post, the second for the - // // comment and the third for a response to that comment - // const actionButtons = screen.queryAllByRole('button', { name: /actions menu/i }); - // await act(async () => { - // fireEvent.click(actionButtons[1]); - // }); - // - // await act(async () => { - // fireEvent.click(screen.getByRole('button', { name: /Report/i })); - // }); - // expect(screen.queryByRole('dialog', { name: /Report \w+/i, exact: false })).toBeInTheDocument(); - // await act(async () => { - // fireEvent.click(screen.queryByRole('button', { name: /Confirm/i })); - // }); - // expect(screen.queryByRole('dialog', { name: /Report \w+/i, exact: false })).not.toBeInTheDocument(); - // expect(axiosMock.history.patch).toHaveLength(2); - // expect(JSON.parse(axiosMock.history.patch[1].data)).toMatchObject({ abuse_flagged: true }); - // }); - // }); - // - // describe('for discussion thread', () => { - // const findLoadMoreCommentsButton = () => screen.findByTestId('load-more-comments'); - // - // it('shown post not found when post id does not belong to course', async () => { - // renderComponent('unloaded-id'); - // expect(await screen.findByText('Thread not found', { exact: true })) - // .toBeInTheDocument(); - // }); - // - // it('initially loads only the first page', async () => { - // renderComponent(discussionPostId); - // expect(await screen.findByText('comment number 1', { exact: false })) - // .toBeInTheDocument(); - // expect(screen.queryByText('comment number 2', { exact: false })) - // .not - // .toBeInTheDocument(); - // }); - // - // it('pressing load more button will load next page of comments', async () => { - // renderComponent(discussionPostId); - // - // const loadMoreButton = await findLoadMoreCommentsButton(); - // fireEvent.click(loadMoreButton); - // - // await screen.findByText('comment number 1', { exact: false }); - // await screen.findByText('comment number 2', { exact: false }); - // }); - // - // it('newly loaded comments are appended to the old ones', async () => { - // renderComponent(discussionPostId); - // - // const loadMoreButton = await findLoadMoreCommentsButton(); - // fireEvent.click(loadMoreButton); - // - // await screen.findByText('comment number 1', { exact: false }); - // // check that comments from the first page are also displayed - // expect(screen.queryByText('comment number 2', { exact: false })) - // .toBeInTheDocument(); - // }); - // - // it('load more button is hidden when no more comments pages to load', async () => { - // const totalPages = 2; - // renderComponent(discussionPostId); - // - // const loadMoreButton = await findLoadMoreCommentsButton(); - // for (let page = 1; page < totalPages; page++) { - // fireEvent.click(loadMoreButton); - // } - // - // await screen.findByText('comment number 2', { exact: false }); - // await expect(findLoadMoreCommentsButton()) - // .rejects - // .toThrow(); - // }); - // }); - // - // describe('for question thread', () => { - // const findLoadMoreCommentsButtons = () => screen.findAllByTestId('load-more-comments'); - // - // it('initially loads only the first page', async () => { - // act(() => renderComponent(questionPostId)); - // expect(await screen.findByText('comment number 3', { exact: false })) - // .toBeInTheDocument(); - // expect(await screen.findByText('endorsed comment number 5', { exact: false })) - // .toBeInTheDocument(); - // expect(screen.queryByText('comment number 4', { exact: false })) - // .not - // .toBeInTheDocument(); - // }); - // - // it('pressing load more button will load next page of comments', async () => { - // act(() => { - // renderComponent(questionPostId); - // }); - // - // const [loadMoreButtonEndorsed, loadMoreButtonUnendorsed] = await findLoadMoreCommentsButtons(); - // // Both load more buttons should show - // expect(await findLoadMoreCommentsButtons()).toHaveLength(2); - // expect(await screen.findByText('unendorsed comment number 3', { exact: false })) - // .toBeInTheDocument(); - // expect(await screen.findByText('endorsed comment number 5', { exact: false })) - // .toBeInTheDocument(); - // // Comments from next page should not be loaded yet. - // expect(await screen.queryByText('endorsed comment number 6', { exact: false })) - // .not - // .toBeInTheDocument(); - // expect(await screen.queryByText('unendorsed comment number 4', { exact: false })) - // .not - // .toBeInTheDocument(); - // - // await act(async () => { - // fireEvent.click(loadMoreButtonEndorsed); - // }); - // // Endorsed comment from next page should be loaded now. - // await waitFor(() => expect(screen.queryByText('endorsed comment number 6', { exact: false })) - // .toBeInTheDocument()); - // // Unendorsed comment from next page should not be loaded yet. - // expect(await screen.queryByText('unendorsed comment number 4', { exact: false })) - // .not - // .toBeInTheDocument(); - // // Now only one load more buttons should show, for unendorsed comments - // expect(await findLoadMoreCommentsButtons()).toHaveLength(1); - // await act(async () => { - // fireEvent.click(loadMoreButtonUnendorsed); - // }); - // // Unendorsed comment from next page should be loaded now. - // await waitFor(() => expect(screen.queryByText('unendorsed comment number 4', { exact: false })) - // .toBeInTheDocument()); - // await expect(findLoadMoreCommentsButtons()).rejects.toThrow(); - // }); - // }); - // - // describe('comments responses', () => { - // const findLoadMoreCommentsResponsesButton = () => screen.findByTestId('load-more-comments-responses'); - // - // it('initially loads only the first page', async () => { - // renderComponent(discussionPostId); - // - // await waitFor(() => screen.findByText('comment number 7', { exact: false })); - // expect(screen.queryByText('comment number 8', { exact: false })).not.toBeInTheDocument(); - // }); - // - // it('pressing load more button will load next page of responses', async () => { - // renderComponent(discussionPostId); - // - // const loadMoreButton = await findLoadMoreCommentsResponsesButton(); - // await act(async () => { - // fireEvent.click(loadMoreButton); - // }); - // - // await screen.findByText('comment number 8', { exact: false }); - // }); - // - // it('newly loaded responses are appended to the old ones', async () => { - // renderComponent(discussionPostId); - // - // const loadMoreButton = await findLoadMoreCommentsResponsesButton(); - // await act(async () => { - // fireEvent.click(loadMoreButton); - // }); - // - // await screen.findByText('comment number 8', { exact: false }); - // // check that comments from the first page are also displayed - // expect(screen.queryByText('comment number 7', { exact: false })).toBeInTheDocument(); - // }); - // - // it('load more button is hidden when no more responses pages to load', async () => { - // const totalPages = 2; - // renderComponent(discussionPostId); - // - // const loadMoreButton = await findLoadMoreCommentsResponsesButton(); - // for (let page = 1; page < totalPages; page++) { - // act(() => { - // fireEvent.click(loadMoreButton); - // }); - // } - // - // await screen.findByText('comment number 8', { exact: false }); - // await expect(findLoadMoreCommentsResponsesButton()) - // .rejects - // .toThrow(); - // }); - // - // it('handles liking a comment', async () => { - // renderComponent(discussionPostId); - // - // // Wait for the content to load - // await screen.findByText('comment number 7', { exact: false }); - // const view = screen.getByTestId('comment-comment-1'); - // - // const likeButton = within(view).getByRole('button', { name: /like/i }); - // await act(async () => { - // fireEvent.click(likeButton); - // }); - // expect(axiosMock.history.patch).toHaveLength(2); - // expect(JSON.parse(axiosMock.history.patch[1].data)).toMatchObject({ voted: true }); - // }); - // - // it('handles endorsing comments', async () => { - // renderComponent(discussionPostId); - // // Wait for the content to load - // await screen.findByText('comment number 7', { exact: false }); - // - // // There should be three buttons, one for the post, the second for the - // // comment and the third for a response to that comment - // const actionButtons = screen.queryAllByRole('button', { name: /actions menu/i }); - // await act(async () => { - // fireEvent.click(actionButtons[1]); - // }); - // - // await act(async () => { - // fireEvent.click(screen.getByRole('button', { name: /Endorse/i })); - // }); - // expect(axiosMock.history.patch).toHaveLength(2); - // expect(JSON.parse(axiosMock.history.patch[1].data)).toMatchObject({ endorsed: true }); - // }); - // - // it('handles reporting comments', async () => { - // renderComponent(discussionPostId); - // // Wait for the content to load - // await screen.findByText('comment number 7', { exact: false }); - // - // // There should be three buttons, one for the post, the second for the - // // comment and the third for a response to that comment - // const actionButtons = screen.queryAllByRole('button', { name: /actions menu/i }); - // await act(async () => { - // fireEvent.click(actionButtons[1]); - // }); - // - // await act(async () => { - // fireEvent.click(screen.getByRole('button', { name: /Report/i })); - // }); - // expect(screen.queryByRole('dialog', { name: /Report \w+/i, exact: false })).toBeInTheDocument(); - // await act(async () => { - // fireEvent.click(screen.queryByRole('button', { name: /Confirm/i })); - // }); - // expect(screen.queryByRole('dialog', { name: /Report \w+/i, exact: false })).not.toBeInTheDocument(); - // expect(axiosMock.history.patch).toHaveLength(2); - // expect(JSON.parse(axiosMock.history.patch[1].data)).toMatchObject({ abuse_flagged: true }); - // }); - // }); - // - // describe.each([ - // { component: 'post', testId: 'post-thread-1' }, - // { component: 'comment', testId: 'comment-comment-1' }, - // { component: 'reply', testId: 'reply-comment-7' }, - // ])('delete confirmation modal', ({ - // component, - // testId, - // }) => { - // test(`for ${component}`, async () => { - // renderComponent(discussionPostId); - // // Wait for the content to load - // await waitFor(() => expect(screen.queryByText('comment number 7', { exact: false })).toBeInTheDocument()); - // const content = screen.getByTestId(testId); - // const actionsButton = within(content).getAllByRole('button', { name: /actions menu/i })[0]; - // await act(async () => { - // fireEvent.click(actionsButton); - // }); - // expect(screen.queryByRole('dialog', { name: /delete \w+/i, exact: false })).not.toBeInTheDocument(); - // const deleteButton = within(content).queryByRole('button', { name: /delete/i }); - // await act(async () => { - // fireEvent.click(deleteButton); - // }); - // expect(screen.queryByRole('dialog', { name: /delete \w+/i, exact: false })).toBeInTheDocument(); - // await act(async () => { - // fireEvent.click(screen.queryByRole('button', { name: /delete/i })); - // }); - // expect(screen.queryByRole('dialog', { name: /delete \w+/i, exact: false })).not.toBeInTheDocument(); - // }); + it('handles liking a comment', async () => { + renderComponent(discussionPostId); + + // Wait for the content to load + await screen.findByText('comment number 7', { exact: false }); + const view = screen.getByTestId('comment-comment-1'); + + const likeButton = within(view).getByRole('button', { name: /like/i }); + await act(async () => { + fireEvent.click(likeButton); + }); + expect(axiosMock.history.patch).toHaveLength(2); + expect(JSON.parse(axiosMock.history.patch[1].data)).toMatchObject({ voted: true }); + }); + + it('handles endorsing comments', async () => { + renderComponent(discussionPostId); + // Wait for the content to load + await screen.findByText('comment number 7', { exact: false }); + + // There should be three buttons, one for the post, the second for the + // comment and the third for a response to that comment + const actionButtons = screen.queryAllByRole('button', { name: /actions menu/i }); + await act(async () => { + fireEvent.click(actionButtons[1]); + }); + + await act(async () => { + fireEvent.click(screen.getByRole('button', { name: /Endorse/i })); + }); + expect(axiosMock.history.patch).toHaveLength(2); + expect(JSON.parse(axiosMock.history.patch[1].data)).toMatchObject({ endorsed: true }); + }); + + it('handles reporting comments', async () => { + renderComponent(discussionPostId); + // Wait for the content to load + await screen.findByText('comment number 7', { exact: false }); + + // There should be three buttons, one for the post, the second for the + // comment and the third for a response to that comment + const actionButtons = screen.queryAllByRole('button', { name: /actions menu/i }); + await act(async () => { + fireEvent.click(actionButtons[1]); + }); + + await act(async () => { + fireEvent.click(screen.getByRole('button', { name: /Report/i })); + }); + expect(screen.queryByRole('dialog', { name: /Report \w+/i, exact: false })).toBeInTheDocument(); + await act(async () => { + fireEvent.click(screen.queryByRole('button', { name: /Confirm/i })); + }); + expect(screen.queryByRole('dialog', { name: /Report \w+/i, exact: false })).not.toBeInTheDocument(); + expect(axiosMock.history.patch).toHaveLength(2); + expect(JSON.parse(axiosMock.history.patch[1].data)).toMatchObject({ abuse_flagged: true }); + }); + }); + + describe('for discussion thread', () => { + const findLoadMoreCommentsButton = () => screen.findByTestId('load-more-comments'); + + it('shown post not found when post id does not belong to course', async () => { + renderComponent('unloaded-id'); + expect(await screen.findByText('Thread not found', { exact: true })) + .toBeInTheDocument(); + }); + + it('initially loads only the first page', async () => { + renderComponent(discussionPostId); + expect(await screen.findByText('comment number 1', { exact: false })) + .toBeInTheDocument(); + expect(screen.queryByText('comment number 2', { exact: false })) + .not + .toBeInTheDocument(); + }); + + it('pressing load more button will load next page of comments', async () => { + renderComponent(discussionPostId); + + const loadMoreButton = await findLoadMoreCommentsButton(); + fireEvent.click(loadMoreButton); + + await screen.findByText('comment number 1', { exact: false }); + await screen.findByText('comment number 2', { exact: false }); + }); + + it('newly loaded comments are appended to the old ones', async () => { + renderComponent(discussionPostId); + + const loadMoreButton = await findLoadMoreCommentsButton(); + fireEvent.click(loadMoreButton); + + await screen.findByText('comment number 1', { exact: false }); + // check that comments from the first page are also displayed + expect(screen.queryByText('comment number 2', { exact: false })) + .toBeInTheDocument(); + }); + + it('load more button is hidden when no more comments pages to load', async () => { + const totalPages = 2; + renderComponent(discussionPostId); + + const loadMoreButton = await findLoadMoreCommentsButton(); + for (let page = 1; page < totalPages; page++) { + fireEvent.click(loadMoreButton); + } + + await screen.findByText('comment number 2', { exact: false }); + await expect(findLoadMoreCommentsButton()) + .rejects + .toThrow(); + }); + }); + + describe('for question thread', () => { + const findLoadMoreCommentsButtons = () => screen.findAllByTestId('load-more-comments'); + + it('initially loads only the first page', async () => { + act(() => renderComponent(questionPostId)); + expect(await screen.findByText('comment number 3', { exact: false })) + .toBeInTheDocument(); + expect(await screen.findByText('endorsed comment number 5', { exact: false })) + .toBeInTheDocument(); + expect(screen.queryByText('comment number 4', { exact: false })) + .not + .toBeInTheDocument(); + }); + + it('pressing load more button will load next page of comments', async () => { + act(() => { + renderComponent(questionPostId); + }); + + const [loadMoreButtonEndorsed, loadMoreButtonUnendorsed] = await findLoadMoreCommentsButtons(); + // Both load more buttons should show + expect(await findLoadMoreCommentsButtons()).toHaveLength(2); + expect(await screen.findByText('unendorsed comment number 3', { exact: false })) + .toBeInTheDocument(); + expect(await screen.findByText('endorsed comment number 5', { exact: false })) + .toBeInTheDocument(); + // Comments from next page should not be loaded yet. + expect(await screen.queryByText('endorsed comment number 6', { exact: false })) + .not + .toBeInTheDocument(); + expect(await screen.queryByText('unendorsed comment number 4', { exact: false })) + .not + .toBeInTheDocument(); + + await act(async () => { + fireEvent.click(loadMoreButtonEndorsed); + }); + // Endorsed comment from next page should be loaded now. + await waitFor(() => expect(screen.queryByText('endorsed comment number 6', { exact: false })) + .toBeInTheDocument()); + // Unendorsed comment from next page should not be loaded yet. + expect(await screen.queryByText('unendorsed comment number 4', { exact: false })) + .not + .toBeInTheDocument(); + // Now only one load more buttons should show, for unendorsed comments + expect(await findLoadMoreCommentsButtons()).toHaveLength(1); + await act(async () => { + fireEvent.click(loadMoreButtonUnendorsed); + }); + // Unendorsed comment from next page should be loaded now. + await waitFor(() => expect(screen.queryByText('unendorsed comment number 4', { exact: false })) + .toBeInTheDocument()); + await expect(findLoadMoreCommentsButtons()).rejects.toThrow(); + }); + }); + + describe('comments responses', () => { + const findLoadMoreCommentsResponsesButton = () => screen.findByTestId('load-more-comments-responses'); + + it('initially loads only the first page', async () => { + renderComponent(discussionPostId); + + await waitFor(() => screen.findByText('comment number 7', { exact: false })); + expect(screen.queryByText('comment number 8', { exact: false })).not.toBeInTheDocument(); + }); + + it('pressing load more button will load next page of responses', async () => { + renderComponent(discussionPostId); + + const loadMoreButton = await findLoadMoreCommentsResponsesButton(); + await act(async () => { + fireEvent.click(loadMoreButton); + }); + + await screen.findByText('comment number 8', { exact: false }); + }); + + it('newly loaded responses are appended to the old ones', async () => { + renderComponent(discussionPostId); + + const loadMoreButton = await findLoadMoreCommentsResponsesButton(); + await act(async () => { + fireEvent.click(loadMoreButton); + }); + + await screen.findByText('comment number 8', { exact: false }); + // check that comments from the first page are also displayed + expect(screen.queryByText('comment number 7', { exact: false })).toBeInTheDocument(); + }); + + it('load more button is hidden when no more responses pages to load', async () => { + const totalPages = 2; + renderComponent(discussionPostId); + + const loadMoreButton = await findLoadMoreCommentsResponsesButton(); + for (let page = 1; page < totalPages; page++) { + act(() => { + fireEvent.click(loadMoreButton); + }); + } + + await screen.findByText('comment number 8', { exact: false }); + await expect(findLoadMoreCommentsResponsesButton()) + .rejects + .toThrow(); + }); + + it('handles liking a comment', async () => { + renderComponent(discussionPostId); + + // Wait for the content to load + await screen.findByText('comment number 7', { exact: false }); + const view = screen.getByTestId('comment-comment-1'); + + const likeButton = within(view).getByRole('button', { name: /like/i }); + await act(async () => { + fireEvent.click(likeButton); + }); + expect(axiosMock.history.patch).toHaveLength(2); + expect(JSON.parse(axiosMock.history.patch[1].data)).toMatchObject({ voted: true }); + }); + + it('handles endorsing comments', async () => { + renderComponent(discussionPostId); + // Wait for the content to load + await screen.findByText('comment number 7', { exact: false }); + + // There should be three buttons, one for the post, the second for the + // comment and the third for a response to that comment + const actionButtons = screen.queryAllByRole('button', { name: /actions menu/i }); + await act(async () => { + fireEvent.click(actionButtons[1]); + }); + + await act(async () => { + fireEvent.click(screen.getByRole('button', { name: /Endorse/i })); + }); + expect(axiosMock.history.patch).toHaveLength(2); + expect(JSON.parse(axiosMock.history.patch[1].data)).toMatchObject({ endorsed: true }); + }); + + it('handles reporting comments', async () => { + renderComponent(discussionPostId); + // Wait for the content to load + await screen.findByText('comment number 7', { exact: false }); + + // There should be three buttons, one for the post, the second for the + // comment and the third for a response to that comment + const actionButtons = screen.queryAllByRole('button', { name: /actions menu/i }); + await act(async () => { + fireEvent.click(actionButtons[1]); + }); + + await act(async () => { + fireEvent.click(screen.getByRole('button', { name: /Report/i })); + }); + expect(screen.queryByRole('dialog', { name: /Report \w+/i, exact: false })).toBeInTheDocument(); + await act(async () => { + fireEvent.click(screen.queryByRole('button', { name: /Confirm/i })); + }); + expect(screen.queryByRole('dialog', { name: /Report \w+/i, exact: false })).not.toBeInTheDocument(); + expect(axiosMock.history.patch).toHaveLength(2); + expect(JSON.parse(axiosMock.history.patch[1].data)).toMatchObject({ abuse_flagged: true }); + }); + }); + + describe.each([ + { component: 'post', testId: 'post-thread-1' }, + { component: 'comment', testId: 'comment-comment-1' }, + { component: 'reply', testId: 'reply-comment-7' }, + ])('delete confirmation modal', ({ + component, + testId, + }) => { + test(`for ${component}`, async () => { + renderComponent(discussionPostId); + // Wait for the content to load + await waitFor(() => expect(screen.queryByText('comment number 7', { exact: false })).toBeInTheDocument()); + const content = screen.getByTestId(testId); + const actionsButton = within(content).getAllByRole('button', { name: /actions menu/i })[0]; + await act(async () => { + fireEvent.click(actionsButton); + }); + expect(screen.queryByRole('dialog', { name: /delete \w+/i, exact: false })).not.toBeInTheDocument(); + const deleteButton = within(content).queryByRole('button', { name: /delete/i }); + await act(async () => { + fireEvent.click(deleteButton); + }); + expect(screen.queryByRole('dialog', { name: /delete \w+/i, exact: false })).toBeInTheDocument(); + await act(async () => { + fireEvent.click(screen.queryByRole('button', { name: /delete/i })); + }); + expect(screen.queryByRole('dialog', { name: /delete \w+/i, exact: false })).not.toBeInTheDocument(); + }); }); });