Skip to content

Commit

Permalink
Merge branch master into bilalqamar95/packages-upgrade
Browse files Browse the repository at this point in the history
  • Loading branch information
BilalQamar95 committed May 23, 2023
2 parents ec42179 + 8228549 commit 21f55a9
Show file tree
Hide file tree
Showing 24 changed files with 610 additions and 809 deletions.
48 changes: 48 additions & 0 deletions src/assets/Empty.jsx

Large diffs are not rendered by default.

44 changes: 0 additions & 44 deletions src/assets/empty.svg

This file was deleted.

2 changes: 1 addition & 1 deletion src/components/Spinner.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React from 'react';
import { Spinner as ParagonSpinner } from '@edx/paragon';

const Spinner = () => (
<div className="spinner-container">
<div className="spinner-container" data-testid="spinner">
<ParagonSpinner animation="border" variant="primary" size="lg" />
</div>
);
Expand Down
9 changes: 6 additions & 3 deletions src/components/TopicStats.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@ const TopicStats = ({
return (
<div className="d-flex align-items-center mt-2.5" style={{ marginBottom: '2px' }}>
<OverlayTrigger
id="discussion-topic-stats"
placement="right"
overlay={(
<Tooltip>
<Tooltip id="discussion-topic-stats">
<div className="d-flex flex-column align-items-start">
{intl.formatMessage(messages.discussions, {
count: threadCounts?.discussion || 0,
Expand All @@ -44,9 +45,10 @@ const TopicStats = ({
</div>
</OverlayTrigger>
<OverlayTrigger
id="question-topic-stats"
placement="right"
overlay={(
<Tooltip>
<Tooltip id="question-topic-stats">
<div className="d-flex flex-column align-items-start">
{intl.formatMessage(messages.questions, {
count: threadCounts?.question || 0,
Expand All @@ -62,9 +64,10 @@ const TopicStats = ({
</OverlayTrigger>
{Boolean(canSeeReportedStats) && (
<OverlayTrigger
id="reported-topic-stats"
placement="right"
overlay={(
<Tooltip>
<Tooltip id="reported-topic-stats">
<div className="d-flex flex-column align-items-start">
{Boolean(activeFlags) && (
<span>
Expand Down
2 changes: 1 addition & 1 deletion src/data/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ export const DiscussionProvider = {
OPEN_EDX: 'openedx',
};

const BASE_PATH = '/:courseId';
const BASE_PATH = `${getConfig().PUBLIC_PATH}:courseId`;

export const Routes = {
DISCUSSIONS: {
Expand Down
247 changes: 149 additions & 98 deletions src/discussions/common/ActionsDropdown.test.jsx
Original file line number Diff line number Diff line change
@@ -1,124 +1,176 @@
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';

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)),
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',
contentType: 'POST',
...camelCaseObject(discussionThread),
...testMeta,
},
{
question: {
testFor: 'question threads',
...camelCaseObject(Factory.build('thread', { ...buildParamsSnakeCase, type: 'question' }, null)),
contentType: 'POST',
...camelCaseObject(questionThread),
...testMeta,
},
{
testFor: 'discussion threads',
...camelCaseObject(Factory.build('thread', { ...buildParamsSnakeCase, type: 'discussion' }, null)),
comment: {
testFor: 'comments',
contentType: 'COMMENT',
type: 'discussion',
...camelCaseObject(comment),
...testMeta,
},
];
}
};
};

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 canPerformActionTestData = ACTIONS_LIST
.map(({ action, conditions, label: { defaultMessage } }) => {
const buildParams = {
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();

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],
}),
{},
);

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();

function renderComponent(
commentOrPost,
{ disabled = false, actionHandlers = {} } = {},
) {
...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.discussion, content.question, content.comment];
});

const renderComponent = ({
id = '',
contentType = 'POST',
closed = false,
type = 'discussion',
postId = '',
disabled = false,
actionHandlers = {},
} = {}) => {
render(
<IntlProvider locale="en">
<AppProvider store={store}>
<ActionsDropdown
commentOrPost={commentOrPost}
disabled={disabled}
actionHandlers={actionHandlers}
/>
<PostCommentsContext.Provider value={{
isClosed: closed,
postType: type,
postId,
}}
>
<ActionsDropdown
id={id}
disabled={disabled}
actionHandlers={actionHandlers}
contentType={contentType}
/>
</PostCommentsContext.Provider>
</AppProvider>
</IntlProvider>,
);
}
};

const findOpenActionsDropdownButton = async () => (
screen.findByRole('button', { name: messages.actionsAlt.defaultMessage })
);

describe('ActionsDropdown', () => {
beforeEach(async () => {
beforeEach(() => {
initializeMockApp({
authenticatedUser: {
userId: 3,
Expand All @@ -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 () => {
Expand All @@ -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 () => {
Expand All @@ -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 () => {
Expand All @@ -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 () => {
Expand All @@ -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 () => {
Expand All @@ -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 () => {
Expand Down
Loading

0 comments on commit 21f55a9

Please sign in to comment.