diff --git a/src/components/form-elements/draft-js-mention-selector/__tests__/utils.test.js b/src/components/form-elements/draft-js-mention-selector/__tests__/utils.test.js index dcfe057adc..a7f7dae0e1 100644 --- a/src/components/form-elements/draft-js-mention-selector/__tests__/utils.test.js +++ b/src/components/form-elements/draft-js-mention-selector/__tests__/utils.test.js @@ -236,5 +236,19 @@ describe('components/form-elements/draft-js-mention-selector/utils', () => { expect(getFormattedCommentText(dummyEditorState)).toEqual(expected); }); + + test.each` + input | expected + ${' hello '} | ${'hello'} + ${'\n\nhello\n\n'} | ${'hello'} + ${'\thello\t'} | ${'hello'} + ${' \t\nhello\n '} | ${'hello'} + ${'hello world'} | ${'hello world'} + ${' hello world '} | ${'hello world'} + `('should trim leading and trailing whitespace from "$input"', ({ input, expected }) => { + const dummyEditorState = EditorState.createWithContent(ContentState.createFromText(input)); + + expect(getFormattedCommentText(dummyEditorState)).toEqual({ text: expected, hasMention: false }); + }); }); }); diff --git a/src/components/form-elements/draft-js-mention-selector/utils.js b/src/components/form-elements/draft-js-mention-selector/utils.js index d404eb6eb0..af2a808232 100644 --- a/src/components/form-elements/draft-js-mention-selector/utils.js +++ b/src/components/form-elements/draft-js-mention-selector/utils.js @@ -163,7 +163,7 @@ function getFormattedCommentText(editorState: EditorState): { hasMention: boolea // Concatenate the array of block strings with newlines // (Each block represents a paragraph) - return { text: resultStringArr.join('\n'), hasMention }; + return { text: resultStringArr.join('\n').trim(), hasMention }; } export { diff --git a/src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.scss b/src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.scss index 3df463eb02..c2ceaec619 100644 --- a/src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.scss +++ b/src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.scss @@ -20,6 +20,11 @@ padding-left: var(--bp-space-040); } + &-editor > div { + padding-right: var(--bp-space-040); + padding-left: var(--bp-space-040); + } + // Forms: BUIE sets width, padding, border, box-shadow, border-radius on // contenteditable and inputs via _forms.scss inside .bcs scope. div[contenteditable='true'], @@ -53,4 +58,8 @@ button svg { pointer-events: auto; } + + &-mentionEmpty { + padding: var(--bp-space-030) var(--bp-space-040); + } } diff --git a/src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.tsx b/src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.tsx index 1f93b1675e..ca8b14f21b 100644 --- a/src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.tsx +++ b/src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.tsx @@ -8,14 +8,14 @@ import * as React from 'react'; import noop from 'lodash/noop'; -import { useIntl } from 'react-intl'; +import { FormattedMessage, useIntl } from 'react-intl'; import { ActivityFeed, useActivityFeedScroll } from '@box/activity-feed'; -import { serializeMentionMarkup } from '@box/threaded-annotations'; import TaskModal from '../TaskModal'; import FeedItemRow from './FeedItemRow'; +import { serializeEditorContent } from './helpers'; import { transformFeedItem } from './transformers'; import type { ActivityFeedV2Props, TransformedFeedItem, UserContact } from './types'; @@ -24,6 +24,7 @@ import type { TaskAssigneeCollection, TaskNew } from '../../../common/types/task import { TASK_COMPLETION_RULE_ALL, TASK_EDIT_MODE_EDIT, TASK_TYPE_APPROVAL } from '../../../constants'; import commonMessages from '../../common/messages'; +import draftJsMentionSelectorMessages from '../../../components/form-elements/draft-js-mention-selector/messages'; import messages from '../messages'; import './ActivityFeedV2.scss'; @@ -68,14 +69,16 @@ const ActivityFeedV2 = ({ const scrolledEntryIdRef = React.useRef(null); const hasScrolledToEndRef = React.useRef(false); + const knownIdsBeforePostRef = React.useRef | null>(null); const fetchUsers = React.useCallback( async (inputValue: string): Promise => { - if (!getMentionAsync) { + const trimmed = inputValue.trim(); + if (!trimmed || !getMentionAsync) { return []; } try { - const entries = await getMentionAsync(inputValue); + const entries = await getMentionAsync(trimmed); return entries.map((c: Record) => ({ email: (c.email as string) ?? (c.login as string) ?? '', id: Number(c.id) || 0, @@ -114,10 +117,20 @@ const ActivityFeedV2 = ({ const userSelectorProps = React.useMemo( () => ({ + allowEmptyQuery: true, ariaRoleDescription: intl.formatMessage(messages.mentionUserSelectorRoleDescription), fetchAvatarUrls, fetchUsers, loadingAriaLabel: intl.formatMessage(messages.mentionUserSelectorLoading), + renderEmpty: (value: string) => ( +
+ +
+ ), }), [fetchAvatarUrls, fetchUsers, intl], ); @@ -247,19 +260,38 @@ const ActivityFeedV2 = ({ } }, [activeFeedEntryId, filteredItems, scrollHandle]); + // Scroll only to comments/annotations the current user authored after the post + // snapshot, so a concurrent push from another user doesn't hijack the viewport. + React.useEffect(() => { + const knownIds = knownIdsBeforePostRef.current; + if (!knownIds || !scrollHandle || !currentUserId) return; + const newItem = filteredItems.find(item => { + if (knownIds.has(item.id)) return false; + if (item.type !== 'comment' && item.type !== 'annotation') return false; + const author = item.messages[0]?.author; + return author ? String(author.id) === currentUserId : false; + }); + if (!newItem) return; + if (scrollHandle.scrollTo(newItem.id)) { + knownIdsBeforePostRef.current = null; + } + }, [currentUserId, filteredItems, scrollHandle]); + const handleCommentPost = React.useCallback( async (content: unknown) => { if (!onCommentCreate) return; - let serialized; + const serialized = serializeEditorContent(content); + if (!serialized || !serialized.text) return; try { - serialized = serializeMentionMarkup(content as Parameters[0]); - } catch { - return; + const snapshot = new Set(filteredItems.map(item => item.id)); + await onCommentCreate(serialized.text, serialized.hasMention); + knownIdsBeforePostRef.current = snapshot; + } catch (error) { + // eslint-disable-next-line no-console + console.error('ActivityFeedV2: failed to post comment', error); } - if (!serialized.text.trim()) return; - onCommentCreate(serialized.text, serialized.hasMention); }, - [onCommentCreate], + [filteredItems, onCommentCreate], ); return ( @@ -319,11 +351,13 @@ const ActivityFeedV2 = ({ )} - +
+ +
; + +const mockSerializeMentionMarkup = jest.fn((doc: unknown) => ({ hasMention: false, text: JSON.stringify(doc) })); + jest.mock('@box/threaded-annotations', () => ({ AnnotationBadgeType: { Drawing: 'drawing', @@ -12,7 +18,7 @@ jest.mock('@box/threaded-annotations', () => ({ Point: 'point', Region: 'region', }, - serializeMentionMarkup: (doc: unknown) => ({ hasMention: false, text: JSON.stringify(doc) }), + serializeMentionMarkup: (doc: unknown) => mockSerializeMentionMarkup(doc), })); const mockScrollTo = jest.fn(() => true); @@ -20,6 +26,7 @@ const mockScrollTo = jest.fn(() => true); type FilterOptionProps = { checked?: boolean; onCheckedChange?: (checked: boolean) => void }; let lastShowResolvedOptionProps: FilterOptionProps = {}; let lastMentionMeOptionProps: FilterOptionProps = {}; +let lastEditorProps: Partial = {}; jest.mock('@box/activity-feed', () => { const actual = jest.requireActual('@box/activity-feed'); @@ -37,7 +44,10 @@ jest.mock('@box/activity-feed', () => {
ThreadedAnnotation
); ActivityFeedList.Version = (props: { id: string }) =>
Version
; - const ActivityFeedEditor = () =>
Editor
; + const ActivityFeedEditor = (props: Partial) => { + lastEditorProps = props; + return
Editor
; + }; const ActivityFeedHeader = ({ children }: { children: React.ReactNode }) => (
{children}
); @@ -153,6 +163,11 @@ describe('elements/content-sidebar/activity-feed-v2/ActivityFeedV2', () => { mockScrollTo.mockReturnValue(true); lastShowResolvedOptionProps = {}; lastMentionMeOptionProps = {}; + lastEditorProps = {}; + mockSerializeMentionMarkup.mockImplementation((doc: unknown) => ({ + hasMention: false, + text: JSON.stringify(doc), + })); }); afterEach(() => { @@ -323,6 +338,129 @@ describe('elements/content-sidebar/activity-feed-v2/ActivityFeedV2', () => { expect(screen.getByTestId('activity-feed-root')).toBeVisible(); }); + describe('mention popover behavior', () => { + test('should pass allowEmptyQuery=true so the popover opens on @ before any character is typed', () => { + render(); + + expect(lastEditorProps.userSelectorProps?.allowEmptyQuery).toBe(true); + }); + + test('should skip the API call when fetchUsers is invoked with an empty query', async () => { + const getMentionAsync = jest.fn().mockResolvedValue([{ id: '1', name: 'foo' }]); + render( + , + ); + + const result = await lastEditorProps.userSelectorProps?.fetchUsers?.(''); + + expect(getMentionAsync).not.toHaveBeenCalled(); + expect(result).toEqual([]); + }); + + test('should skip the API call when fetchUsers is invoked with a whitespace-only query', async () => { + const getMentionAsync = jest.fn().mockResolvedValue([{ id: '1', name: 'foo' }]); + render( + , + ); + + const result = await lastEditorProps.userSelectorProps?.fetchUsers?.(' '); + + expect(getMentionAsync).not.toHaveBeenCalled(); + expect(result).toEqual([]); + }); + + test('should call getMentionAsync with the trimmed value and shape results for a non-empty query', async () => { + const getMentionAsync = jest.fn().mockResolvedValue([ + { email: 'a@b.com', id: '7', name: 'Alice' }, + { id: '8', login: 'bob@b.com', name: 'Bob' }, + ]); + render( + , + ); + + const result = await lastEditorProps.userSelectorProps?.fetchUsers?.(' al '); + + expect(getMentionAsync).toHaveBeenCalledWith('al'); + expect(result).toEqual([ + { email: 'a@b.com', id: 7, name: 'Alice', value: '7' }, + { email: 'bob@b.com', id: 8, name: 'Bob', value: '8' }, + ]); + }); + + test('should render the V1-style start prompt via renderEmpty when value is empty', () => { + render(); + + const empty = lastEditorProps.userSelectorProps?.renderEmpty?.(''); + render(<>{empty}); + + expect(screen.getByText('Mention someone to notify them')).toBeVisible(); + }); + + test('should render the V1-style start prompt via renderEmpty when value is whitespace-only', () => { + render(); + + const empty = lastEditorProps.userSelectorProps?.renderEmpty?.(' '); + render(<>{empty}); + + expect(screen.getByText('Mention someone to notify them')).toBeVisible(); + }); + + test('should render the no-users-found message via renderEmpty when value is non-empty', () => { + render(); + + const empty = lastEditorProps.userSelectorProps?.renderEmpty?.('xyz'); + render(<>{empty}); + + expect(screen.getByText('No users found')).toBeVisible(); + }); + }); + + describe('comment posting', () => { + test('should call onCommentCreate with trimmed text when the editor posts content', async () => { + mockSerializeMentionMarkup.mockReturnValue({ hasMention: true, text: ' hello world ' }); + const onCommentCreate = jest.fn(); + render( + , + ); + + await lastEditorProps.onPost?.({ type: 'doc', content: [] }); + + expect(onCommentCreate).toHaveBeenCalledWith('hello world', true); + }); + + test('should skip onCommentCreate when the trimmed text is empty', async () => { + mockSerializeMentionMarkup.mockReturnValue({ hasMention: false, text: ' \n\t ' }); + const onCommentCreate = jest.fn(); + render( + , + ); + + await lastEditorProps.onPost?.({ type: 'doc', content: [] }); + + expect(onCommentCreate).not.toHaveBeenCalled(); + }); + }); + describe('filter controls', () => { test('should default showResolved and showOnlyMentionsMe to false in the filter menu', () => { render(); @@ -522,4 +660,199 @@ describe('elements/content-sidebar/activity-feed-v2/ActivityFeedV2', () => { expect(mockScrollTo).toHaveBeenCalledTimes(1); }); }); + + describe('scroll to user post', () => { + const numericCurrentUser: ActivityFeedV2Props['currentUser'] = { id: '10', name: 'Current User', type: 'user' }; + const userComment = { + ...mockComment, + created_by: { id: '10', name: 'Current User', type: 'user' }, + id: 'new-comment', + tagged_message: 'fresh post', + }; + const strangerComment = { + ...mockComment, + created_by: { id: '99', name: 'Stranger', type: 'user' }, + id: 'stranger-comment', + tagged_message: 'unrelated', + }; + + test('should scroll to the new user item once feedItems updates after a post', async () => { + const onCommentCreate = jest.fn(); + const { rerender } = render( + , + ); + mockScrollTo.mockClear(); + + await lastEditorProps.onPost?.({ type: 'doc', content: [] }); + expect(onCommentCreate).toHaveBeenCalled(); + expect(mockScrollTo).not.toHaveBeenCalled(); + + rerender( + , + ); + + expect(mockScrollTo).toHaveBeenLastCalledWith('new-comment'); + }); + + test('should not scroll when onCommentCreate rejects (post failed)', async () => { + const consoleError = jest.spyOn(console, 'error').mockImplementation(() => undefined); + const onCommentCreate = jest.fn().mockRejectedValue(new Error('network error')); + const { rerender } = render( + , + ); + mockScrollTo.mockClear(); + + await lastEditorProps.onPost?.({ type: 'doc', content: [] }); + rerender( + , + ); + + expect(consoleError).toHaveBeenCalledWith('ActivityFeedV2: failed to post comment', expect.any(Error)); + expect(mockScrollTo).not.toHaveBeenCalled(); + consoleError.mockRestore(); + }); + + test('should not scroll to a concurrent push that arrives without a user post', async () => { + const { rerender } = render( + , + ); + mockScrollTo.mockClear(); + + rerender( + , + ); + + expect(mockScrollTo).not.toHaveBeenCalled(); + }); + + test('should scroll past a stranger insert and target the user-authored item only', async () => { + const onCommentCreate = jest.fn(); + const { rerender } = render( + , + ); + mockScrollTo.mockClear(); + + await lastEditorProps.onPost?.({ type: 'doc', content: [] }); + rerender( + , + ); + + expect(mockScrollTo).toHaveBeenCalledWith('new-comment'); + expect(mockScrollTo).not.toHaveBeenCalledWith('stranger-comment'); + }); + + test('should not scroll when only a stranger insert lands after a user post', async () => { + const onCommentCreate = jest.fn(); + const { rerender } = render( + , + ); + mockScrollTo.mockClear(); + + await lastEditorProps.onPost?.({ type: 'doc', content: [] }); + rerender( + , + ); + + expect(mockScrollTo).not.toHaveBeenCalled(); + }); + + test('should not scroll when serializeEditorContent yields no text', async () => { + mockSerializeMentionMarkup.mockReturnValue({ hasMention: false, text: ' ' }); + const onCommentCreate = jest.fn(); + const { rerender } = render( + , + ); + mockScrollTo.mockClear(); + + await lastEditorProps.onPost?.({ type: 'doc', content: [] }); + rerender( + , + ); + + expect(onCommentCreate).not.toHaveBeenCalled(); + expect(mockScrollTo).not.toHaveBeenCalled(); + }); + + test('should retry the scroll on the next feedItems change when scrollTo returns false', async () => { + mockScrollTo.mockReturnValue(false); + const onCommentCreate = jest.fn(); + const { rerender } = render( + , + ); + mockScrollTo.mockClear(); + mockScrollTo.mockReturnValue(false); + + await lastEditorProps.onPost?.({ type: 'doc', content: [] }); + rerender( + , + ); + expect(mockScrollTo).toHaveBeenLastCalledWith('new-comment'); + + mockScrollTo.mockClear(); + mockScrollTo.mockReturnValue(true); + rerender( + , + ); + + expect(mockScrollTo).toHaveBeenLastCalledWith('new-comment'); + }); + }); }); diff --git a/src/elements/content-sidebar/activity-feed-v2/__tests__/helpers.test.ts b/src/elements/content-sidebar/activity-feed-v2/__tests__/helpers.test.ts index a591d24cb3..3763b2cf09 100644 --- a/src/elements/content-sidebar/activity-feed-v2/__tests__/helpers.test.ts +++ b/src/elements/content-sidebar/activity-feed-v2/__tests__/helpers.test.ts @@ -43,6 +43,19 @@ describe('elements/content-sidebar/activity-feed-v2/helpers', () => { expect(mockedSerialize).toHaveBeenCalledWith(content); }); + test.each` + input | expected + ${' hello '} | ${'hello'} + ${'\n\nhello world\n\n'} | ${'hello world'} + ${'\thello\t'} | ${'hello'} + ${' \t\nhello\n '} | ${'hello'} + ${' leading and inner '} | ${'leading and inner'} + `('should trim leading and trailing whitespace from "$input"', ({ input, expected }) => { + mockedSerialize.mockReturnValue({ hasMention: true, text: input }); + + expect(serializeEditorContent({})).toEqual({ hasMention: true, text: expected }); + }); + test('should log via console.error and return null when serialize throws', () => { const consoleError = jest.spyOn(console, 'error').mockImplementation(() => undefined); mockedSerialize.mockImplementation(() => { diff --git a/src/elements/content-sidebar/activity-feed-v2/helpers.ts b/src/elements/content-sidebar/activity-feed-v2/helpers.ts index 4547b83f2e..daf7b22496 100644 --- a/src/elements/content-sidebar/activity-feed-v2/helpers.ts +++ b/src/elements/content-sidebar/activity-feed-v2/helpers.ts @@ -13,7 +13,8 @@ type EditorContent = Parameters[0]; export const serializeEditorContent = (content: unknown): ReturnType | null => { try { - return serializeMentionMarkup(content as EditorContent); + const serialized = serializeMentionMarkup(content as EditorContent); + return { ...serialized, text: serialized.text.trim() }; } catch (error) { // eslint-disable-next-line no-console console.error('ActivityFeedV2: failed to serialize editor content', error); diff --git a/src/elements/content-sidebar/activity-feed/comment-form/CommentForm.js b/src/elements/content-sidebar/activity-feed/comment-form/CommentForm.js index fb1f1bf336..186a4bfa6a 100644 --- a/src/elements/content-sidebar/activity-feed/comment-form/CommentForm.js +++ b/src/elements/content-sidebar/activity-feed/comment-form/CommentForm.js @@ -147,6 +147,7 @@ class CommentForm extends React.Component { const allowVideoTimestamps = isVideo && istimestampedCommentsEnabled; const timestampLabel = allowVideoTimestamps ? formatMessage(messages.commentTimestampLabel) : undefined; const { commentEditorState } = this.state; + const isPostDisabled = !commentEditorState.getCurrentContent().getPlainText().trim(); const inputContainerClassNames = classNames('bcs-CommentForm', className, { 'bcs-is-open': isOpen, }); @@ -186,7 +187,7 @@ class CommentForm extends React.Component { )} - {isOpen && } + {isOpen && } diff --git a/src/elements/content-sidebar/activity-feed/comment-form/CommentFormControls.js b/src/elements/content-sidebar/activity-feed/comment-form/CommentFormControls.js index 2fa86fc65c..c0e23db1e0 100644 --- a/src/elements/content-sidebar/activity-feed/comment-form/CommentFormControls.js +++ b/src/elements/content-sidebar/activity-feed/comment-form/CommentFormControls.js @@ -13,15 +13,16 @@ import messages from './messages'; import { ACTIVITY_TARGETS } from '../../../common/interactionTargets'; type Props = { + isDisabled?: boolean, onCancel: Function, }; -const CommentInputControls = ({ onCancel }: Props): React.Node => ( +const CommentInputControls = ({ isDisabled, onCancel }: Props): React.Node => (
- +
diff --git a/src/elements/content-sidebar/activity-feed/comment-form/__tests__/CommentForm.test.js b/src/elements/content-sidebar/activity-feed/comment-form/__tests__/CommentForm.test.js index 07a6dde59a..d0b3147be1 100644 --- a/src/elements/content-sidebar/activity-feed/comment-form/__tests__/CommentForm.test.js +++ b/src/elements/content-sidebar/activity-feed/comment-form/__tests__/CommentForm.test.js @@ -130,20 +130,32 @@ describe('elements/content-sidebar/ActivityFeed/comment-form/CommentForm', () => expect(wrapper.find('DraftJSMentionSelector').at(0).prop('placeholder')).toEqual('Your comment goes here'); }); - test('should not focus on textbox when shouldFocusOnOpen is false', () => { - const mockFocusFunc = jest.fn(); - EditorState.moveFocusToEnd = mockFocusFunc; + describe('moveFocusToEnd', () => { + let originalMoveFocusToEnd; - getWrapperRTL(); - expect(mockFocusFunc).not.toHaveBeenCalled(); - }); + beforeEach(() => { + originalMoveFocusToEnd = EditorState.moveFocusToEnd; + }); + + afterEach(() => { + EditorState.moveFocusToEnd = originalMoveFocusToEnd; + }); + + test('should not focus on textbox when shouldFocusOnOpen is false', () => { + const mockFocusFunc = jest.fn(); + EditorState.moveFocusToEnd = mockFocusFunc; - test('should focus on textbox when shouldFocusOnOpen is true', () => { - const mockFocusFunc = jest.fn(); - EditorState.moveFocusToEnd = mockFocusFunc; + getWrapperRTL(); + expect(mockFocusFunc).not.toHaveBeenCalled(); + }); + + test('should focus on textbox when shouldFocusOnOpen is true', () => { + const mockFocusFunc = jest.fn(state => state); + EditorState.moveFocusToEnd = mockFocusFunc; - getWrapperRTL({ shouldFocusOnOpen: true }); - expect(mockFocusFunc).toHaveBeenCalled(); + getWrapperRTL({ shouldFocusOnOpen: true }); + expect(mockFocusFunc).toHaveBeenCalled(); + }); }); test('should enable timestamp when file is a video and timestampedComments is enabled', () => { @@ -169,4 +181,26 @@ describe('elements/content-sidebar/ActivityFeed/comment-form/CommentForm', () => }); expect(wrapper.find('DraftJSMentionSelector').at(0).prop('timestampLabel')).toBeUndefined(); }); + + describe('post button disabled state', () => { + const findControls = wrapper => wrapper.find('CommentInputControls'); + + test('should pass isDisabled=true to the controls when the editor is empty', () => { + const wrapper = getWrapper({ isOpen: true }); + + expect(findControls(wrapper).prop('isDisabled')).toBe(true); + }); + + test('should pass isDisabled=true to the controls when the editor has only whitespace', () => { + const wrapper = getWrapper({ isOpen: true, tagged_message: ' \n\t ' }); + + expect(findControls(wrapper).prop('isDisabled')).toBe(true); + }); + + test('should pass isDisabled=false to the controls when the editor has non-whitespace content', () => { + const wrapper = getWrapper({ isOpen: true, tagged_message: 'hello' }); + + expect(findControls(wrapper).prop('isDisabled')).toBe(false); + }); + }); }); diff --git a/src/elements/content-sidebar/activity-feed/comment-form/__tests__/CommentFormControls.test.js b/src/elements/content-sidebar/activity-feed/comment-form/__tests__/CommentFormControls.test.js new file mode 100644 index 0000000000..eb0e972328 --- /dev/null +++ b/src/elements/content-sidebar/activity-feed/comment-form/__tests__/CommentFormControls.test.js @@ -0,0 +1,39 @@ +import * as React from 'react'; +import { render, screen } from '@testing-library/react'; +import { IntlProvider } from 'react-intl'; + +import CommentFormControls from '../CommentFormControls'; + +jest.mock('react-intl', () => ({ + ...jest.requireActual('react-intl'), + FormattedMessage: ({ defaultMessage }) => {defaultMessage}, +})); + +const renderControls = (props = {}) => + render( + + {}} {...props} /> + , + ); + +describe('elements/content-sidebar/ActivityFeed/comment-form/CommentFormControls', () => { + test('should render Post button enabled by default', () => { + renderControls(); + const post = screen.getByRole('button', { name: 'Post' }); + expect(post).not.toHaveAttribute('aria-disabled'); + expect(post).not.toHaveClass('is-disabled'); + }); + + test('should disable Post button when isDisabled is true', () => { + renderControls({ isDisabled: true }); + const post = screen.getByRole('button', { name: 'Post' }); + expect(post).toHaveAttribute('aria-disabled', 'true'); + expect(post).toHaveClass('is-disabled'); + }); + + test('should leave Cancel button enabled even when Post is disabled', () => { + renderControls({ isDisabled: true }); + const cancel = screen.getByRole('button', { name: 'Cancel' }); + expect(cancel).not.toHaveAttribute('aria-disabled'); + }); +}); diff --git a/src/elements/content-sidebar/activity-feed/comment/__tests__/BaseComment.test.js b/src/elements/content-sidebar/activity-feed/comment/__tests__/BaseComment.test.js index 27842de4ac..81716684e2 100644 --- a/src/elements/content-sidebar/activity-feed/comment/__tests__/BaseComment.test.js +++ b/src/elements/content-sidebar/activity-feed/comment/__tests__/BaseComment.test.js @@ -405,7 +405,7 @@ describe('elements/content-sidebar/ActivityFeed/comment/BaseComment', () => { }); test('should focus on the edit CommentForm when it is opened', () => { - const mockFocusFunc = jest.fn(); + const mockFocusFunc = jest.fn(editor => editor); EditorState.moveFocusToEnd = mockFocusFunc; getWrapper({ canEdit: true }); diff --git a/src/elements/content-sidebar/activity-feed/comment/__tests__/CreateReply.test.js b/src/elements/content-sidebar/activity-feed/comment/__tests__/CreateReply.test.js index 7e78b5b202..4156bd043c 100644 --- a/src/elements/content-sidebar/activity-feed/comment/__tests__/CreateReply.test.js +++ b/src/elements/content-sidebar/activity-feed/comment/__tests__/CreateReply.test.js @@ -130,7 +130,7 @@ describe('elements/content-sidebar/ActivityFeed/comment/CreateReply', () => { }); test('reply form should be focused when opened', () => { - const mockFocusFunc = jest.fn(); + const mockFocusFunc = jest.fn(editor => editor); EditorState.moveFocusToEnd = mockFocusFunc; getWrapper({ showReplyForm: true });