diff --git a/src/discussions/common/ActionsDropdown.jsx b/src/discussions/common/ActionsDropdown.jsx
index 8607a5ec..c88aad28 100644
--- a/src/discussions/common/ActionsDropdown.jsx
+++ b/src/discussions/common/ActionsDropdown.jsx
@@ -48,7 +48,10 @@ function ActionsDropdown({
isOpen={isOpen}
placement="auto-start"
>
-
+
{actions.map(action => (
{action.action === ContentActions.DELETE
diff --git a/src/discussions/common/ActionsDropdown.test.jsx b/src/discussions/common/ActionsDropdown.test.jsx
new file mode 100644
index 00000000..dd9111ba
--- /dev/null
+++ b/src/discussions/common/ActionsDropdown.test.jsx
@@ -0,0 +1,185 @@
+import {
+ fireEvent, render, screen, waitFor,
+} from '@testing-library/react';
+import { IntlProvider } from 'react-intl';
+import { Factory } from 'rosie';
+
+import { camelCaseObject, initializeMockApp, snakeCaseObject } from '@edx/frontend-platform';
+import { AppProvider } from '@edx/frontend-platform/react';
+
+import { ContentActions } from '../../data/constants';
+import messages from '../messages';
+import { ACTIONS_LIST } from '../utils';
+import ActionsDropdown from './ActionsDropdown';
+
+import '../posts/data/__factories__';
+import '../comments/data/__factories__';
+
+let store;
+
+function buildTestContent(buildParams) {
+ const buildParamsSnakeCase = snakeCaseObject(buildParams);
+ return [
+ Factory.build('comment', { ...buildParamsSnakeCase }, null),
+ // question
+ Factory.build('thread', { ...buildParamsSnakeCase }, null),
+ // thread
+ Factory.build('thread', { ...buildParamsSnakeCase }, null),
+ ].map(content => camelCaseObject(content));
+}
+
+const canPerformActionTestData = ACTIONS_LIST
+ .map(({ action, conditions, label: { defaultMessage } }) => {
+ const buildParams = {
+ editable_fields: [action],
+ };
+ if (conditions) {
+ Object.entries(conditions)
+ .forEach(([conditionKey, conditionValue]) => {
+ buildParams[conditionKey] = conditionValue;
+ });
+ }
+ return buildTestContent(buildParams)
+ .map(commentOrPost => ([defaultMessage, commentOrPost, action]));
+ })
+ .flat();
+
+const canNotPerformActionTestData = ACTIONS_LIST
+ .map(({ action, conditions, label: { defaultMessage } }) => {
+ let content;
+ if (!conditions) {
+ content = buildTestContent({
+ editable_fields: [],
+ });
+ } 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,
+ }),
+ // passes conditions, but can't edit field
+ ...(action === ContentActions.DELETE
+ ? []
+ : buildTestContent({
+ editable_fields: [],
+ ...conditions,
+ })
+ ),
+ // can't edit field, and doesn't pass conditions
+ ...buildTestContent({
+ editable_fields: [],
+ ...reversedConditions,
+ }),
+ ];
+ }
+ return content.map(commentOrPost => ([defaultMessage, commentOrPost]));
+ })
+ .flat();
+
+function renderComponent(
+ commentOrPost,
+ { disabled = false, actionHandlers = {} } = {},
+) {
+ render(
+
+
+
+
+ ,
+ );
+}
+
+const findOpenActionsDropdownButton = async () => (
+ screen.findByRole('button', { name: messages.actionsAlt.defaultMessage })
+);
+
+describe('ActionsDropdown', () => {
+ beforeEach(async () => {
+ initializeMockApp({
+ authenticatedUser: {
+ userId: 3,
+ username: 'abc123',
+ administrator: false,
+ roles: [],
+ },
+ });
+ });
+
+ it.each(buildTestContent())(
+ 'can open drop down if enabled',
+ async (commentOrPost) => {
+ renderComponent(commentOrPost, { disabled: false });
+
+ const openButton = await findOpenActionsDropdownButton();
+ fireEvent.click(openButton);
+
+ 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 });
+
+ const openButton = await findOpenActionsDropdownButton();
+ fireEvent.click(openButton);
+
+ await waitFor(() => expect(screen.queryByTestId('actions-dropdown-modal-popup'))
+ .not
+ .toBeInTheDocument());
+ },
+ );
+
+ it.each(canPerformActionTestData)(
+ // not using jest $variable notation because it's not working (probably a bug)
+ 'can perform action %s',
+ async (defaultMessage, commentOrPost, action) => {
+ const mockHandler = jest.fn();
+ renderComponent(
+ commentOrPost,
+ { actionHandlers: { [action]: mockHandler } },
+ );
+
+ const openButton = await findOpenActionsDropdownButton();
+ fireEvent.click(openButton);
+
+ await waitFor(() => expect(screen.queryByText(defaultMessage))
+ .toBeInTheDocument());
+
+ fireEvent.click(screen.queryByText(defaultMessage));
+
+ expect(mockHandler).toHaveBeenCalled();
+ },
+ );
+
+ it.each(canNotPerformActionTestData)(
+ // not using jest $variable notation because it's not working (probably a bug)
+ 'can not perform action %s',
+ async (defaultMessage, commentOrPost) => {
+ renderComponent(commentOrPost);
+
+ const openButton = await findOpenActionsDropdownButton();
+ fireEvent.click(openButton);
+
+ await waitFor(() => expect(screen.queryByText(defaultMessage))
+ .not
+ .toBeInTheDocument());
+ },
+ );
+});
diff --git a/src/discussions/utils.js b/src/discussions/utils.js
index 0224549d..8278225b 100644
--- a/src/discussions/utils.js
+++ b/src/discussions/utils.js
@@ -80,7 +80,7 @@ export function checkPermissions(content, action) {
* in the content/post.
* e.g. for {pinned:false} the action will show up if the content/post has post.pinned==false
*/
-const ACTIONS_LIST = [
+export const ACTIONS_LIST = [
{
id: 'edit',
action: ContentActions.EDIT_CONTENT,