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,