UNPKG

box-ui-elements

Version:
1,356 lines (1,220 loc) • 89.5 kB
import cloneDeep from 'lodash/cloneDeep'; import commonMessages from '../../elements/common/messages'; import messages from '../messages'; import * as sorter from '../../utils/sorter'; import * as error from '../../utils/error'; import { FEED_FILE_VERSIONS_FIELDS_TO_FETCH } from '../../utils/fields'; import { FEED_ITEM_TYPE_APP_ACTIVITY, FEED_ITEM_TYPE_ANNOTATION, FEED_ITEM_TYPE_COMMENT, FEED_ITEM_TYPE_VERSION, FILE_ACTIVITY_TYPE_ANNOTATION, FILE_ACTIVITY_TYPE_APP_ACTIVITY, FILE_ACTIVITY_TYPE_COMMENT, FILE_ACTIVITY_TYPE_TASK, FILE_ACTIVITY_TYPE_VERSION, IS_ERROR_DISPLAYED, TASK_MAX_GROUP_ASSIGNEES, } from '../../constants'; import AnnotationsAPI from '../Annotations'; import Feed, { getParsedFileActivitiesResponse } from '../Feed'; import ThreadedCommentsAPI from '../ThreadedComments'; import { annotation as mockAnnotation } from '../../__mocks__/annotations'; import { fileActivities as mockFileActivities, task as mockTask, threadedComments as mockThreadedComments, threadedCommentsFormatted, annotationsWithFormattedReplies as mockFormattedAnnotations, fileActivitiesVersion, promotedFileActivitiesVersion, } from '../fixtures'; const mockErrors = [{ code: 'error_code_0' }, { code: 'error_code_1' }]; const mockFirstVersion = { action: 'upload', type: FEED_ITEM_TYPE_VERSION, id: 123, created_at: 'Thu Sep 20 33658 19:45:39 GMT-0600 (CST)', trashed_at: 1234567891, modified_at: 1234567891, modified_by: { name: 'Akon', id: 11 }, }; const mockCurrentVersion = { action: 'restore', type: FEED_ITEM_TYPE_VERSION, id: '123', }; const deleted_version = { action: 'delete', type: FEED_ITEM_TYPE_VERSION, id: 234, created_at: 'Thu Sep 20 33658 19:45:39 GMT-0600 (CST)', trashed_at: 1234567891, modified_at: 1234567891, modified_by: { name: 'Akon', id: 11 }, }; const versions = { total_count: 1, entries: [mockFirstVersion, deleted_version], }; const versionsWithCurrent = { total_count: 3, entries: [mockCurrentVersion, mockFirstVersion, deleted_version], }; const annotations = { entries: [mockAnnotation], limit: 1000, next_marker: null, }; const threadedComments = { entries: mockThreadedComments, limit: 1000, next_marker: null, }; const mockReplies = mockThreadedComments[0].replies; jest.mock('lodash/uniqueId', () => () => 'uniqueId'); const mockCreateTaskWithDeps = jest.fn().mockImplementation(({ successCallback }) => { successCallback(); }); jest.mock('../tasks/TasksNew', () => { return jest.fn().mockImplementation(() => ({ createTaskWithDeps: mockCreateTaskWithDeps, updateTaskWithDeps: jest.fn().mockImplementation(({ successCallback }) => { successCallback(); }), deleteTask: jest.fn().mockImplementation(({ successCallback }) => { successCallback(); }), getTasksForFile: jest.fn().mockReturnValue({ entries: [mockTask], next_marker: null, limit: 1000 }), getTask: jest.fn(({ successCallback }) => { successCallback(mockTask); }), })); }); jest.mock('../tasks/TaskCollaborators', () => jest.fn().mockImplementation(() => ({ createTaskCollaborator: jest.fn().mockImplementation(({ successCallback }) => { successCallback([ { type: 'task_collaborator', id: '1', status: 'NOT_STARTED', role: 'ASSIGNEE', target: { type: 'user', id: '00000001', name: 'Box One', login: 'boxOne@box.com', }, permissions: { can_update: true, can_delete: true, }, }, ]); }), createTaskCollaboratorsforGroup: jest.fn().mockImplementation(({ successCallback }) => { successCallback({ type: 'task_collaborator', id: '2', status: 'NOT_STARTED', role: 'ASSIGNEE', target: { type: 'user', id: '00000002', name: 'Box two', login: 'boxTwo@box.com', }, permissions: { can_update: true, can_delete: true, }, }); }), updateTaskCollaborator: jest.fn().mockImplementation(({ successCallback }) => { successCallback(); }), deleteTaskCollaborator: jest.fn().mockImplementation(({ successCallback }) => { successCallback(); }), getTaskCollaborators: jest.fn().mockReturnValue({ entries: [ { id: '1', target: { name: 'Beyonce', id: '2', avatar_url: '', type: 'user' }, status: 'NOT_STARTED', permissions: { can_delete: false, can_update: false, }, role: 'ASSIGNEE', type: 'task_collaborator', }, ], next_marker: null, limit: 1000, }), })), ); jest.mock('../tasks/TaskLinks', () => jest.fn().mockImplementation(() => ({ createTaskLink: jest.fn().mockImplementation(({ successCallback }) => { successCallback(); }), })), ); const mockGetGroupCount = jest.fn(); jest.mock('../Groups', () => { const GroupAPI = jest.fn().mockImplementation(() => ({ getGroupCount: mockGetGroupCount, })); return GroupAPI; }); jest.mock('../Comments', () => jest.fn().mockImplementation(() => ({ getComments: jest.fn().mockReturnValue({ total_count: 1, entries: [ { type: 'comment', id: '123', created_at: 'Thu Sep 26 33658 19:46:39 GMT-0600 (CST)', tagged_message: 'test @[123:Jeezy] @[10:Kanye West]', created_by: { name: 'Akon', id: 11 }, }, ], }), deleteComment: jest.fn().mockImplementation(({ successCallback }) => { successCallback(); }), updateComment: jest.fn().mockImplementation(({ successCallback }) => { successCallback(); }), createComment: jest.fn().mockImplementation(({ successCallback }) => { successCallback(); }), })), ); jest.mock('../ThreadedComments', () => jest.fn().mockImplementation(() => ({ getComment: jest.fn().mockResolvedValue(mockThreadedComments[1]), getComments: jest.fn().mockReturnValue({ entries: mockThreadedComments, limit: 1000, next_marker: null, }), getCommentReplies: jest.fn().mockImplementation(({ successCallback }) => { successCallback({ entries: mockReplies, limit: 1000, next_marker: null, }); }), deleteComment: jest.fn().mockImplementation(({ successCallback }) => { successCallback(); }), updateComment: jest.fn().mockImplementation(({ successCallback }) => { successCallback({}); }), createComment: jest.fn().mockImplementation(({ successCallback }) => { successCallback(); }), createCommentReply: jest.fn().mockImplementation(({ successCallback }) => { successCallback(); }), })), ); jest.mock('../Versions', () => { return jest.fn().mockImplementation(() => ({ getVersions: jest.fn(() => mockFirstVersion), getVersion: jest.fn(() => mockCurrentVersion), })); }); jest.mock('../Annotations', () => jest.fn().mockImplementation(() => ({ deleteAnnotation: jest.fn().mockImplementation((file, id, permissions, successCallback) => { successCallback(); }), updateAnnotation: jest.fn().mockImplementation((file, id, payload, permissions, successCallback) => { successCallback({}); }), getAnnotations: jest.fn(), getAnnotationReplies: jest.fn().mockImplementation((fileId, annotationId, permissions, successCallback) => { successCallback({ entries: mockReplies, limit: 1000, next_marker: null, }); }), createAnnotationReply: jest .fn() .mockImplementation((fileId, annotationId, permissions, message, successCallback) => { successCallback(); }), })), ); const MOCK_APP_ACTIVITY_ITEM = { activity_template: { id: '1', type: 'activity_template', }, app: { icon_url: 'https://some.cdn.com/12345.png', id: '123456', name: 'App activities test', type: 'app', }, created_by: { id: '1234556789876', login: 'some-account@box.com', name: 'John Doe', type: 'user', }, id: '3782', occurred_at: '2019-02-21T04:00:00Z', rendered_text: 'You shared this file in <a href="https://some-app.com" rel="noreferrer noopener">Some App</a>', type: FEED_ITEM_TYPE_APP_ACTIVITY, }; jest.mock('../AppActivity', () => jest.fn().mockImplementation(() => ({ getAppActivity: jest.fn().mockReturnValue({ total_count: 1, entries: [MOCK_APP_ACTIVITY_ITEM], }), deleteAppActivity: jest.fn().mockImplementation(({ successCallback }) => { successCallback(); }), })), ); jest.mock('../FileActivities', () => { return jest.fn().mockImplementation(() => ({ getActivities: jest.fn().mockReturnValue({ entries: mockFileActivities, }), })); }); describe('api/Feed', () => { let feed; const comments = { total_count: 1, entries: [ { type: FEED_ITEM_TYPE_COMMENT, id: '123', created_at: 'Thu Sep 26 33658 19:46:39 GMT-0600 (CST)', tagged_message: 'test @[123:Jeezy] @[10:Kanye West]', created_by: { name: 'Akon', id: 11 }, }, ], }; const tasks = { entries: [mockTask], limit: 1000, next_marker: null, }; const appActivities = { total_count: 1, entries: [MOCK_APP_ACTIVITY_ITEM], }; const feedItems = [...comments.entries, ...tasks.entries, ...versions.entries, ...appActivities.entries]; const file = { id: '12345', permissions: { can_comment: true, can_create_annotations: true, can_view_annotations: true, }, modified_at: 1234567891, file_version: { id: 987, }, restored_from: { id: mockFirstVersion.id, type: mockFirstVersion.type, }, }; const user = { id: 'user1' }; const fileError = 'Bad box item!'; const errorCode = 'foo'; beforeEach(() => { feed = new Feed({}); jest.spyOn(global.console, 'error').mockImplementation(); }); afterEach(() => { feed.errors = []; jest.restoreAllMocks(); }); describe('getCacheKey()', () => { test('should return the cache key', () => { expect(feed.getCacheKey('1')).toBe('feedItems_1'); }); }); describe('getCachedItems()', () => { beforeEach(() => { feed.getCache = jest.fn().mockReturnValue({ set: jest.fn(), get: jest.fn().mockReturnValue(feedItems), }); feed.getCacheKey = jest.fn().mockReturnValue('baz'); }); test('should get the cached items', () => { const id = '1'; const result = feed.getCachedItems(id); expect(feed.getCacheKey).toHaveBeenCalledWith(id); expect(result).toEqual(feedItems); }); }); describe('setCachedItems()', () => { let cache; const cacheKey = 'baz'; beforeEach(() => { cache = { get: jest.fn().mockRejectedValue(feedItems), set: jest.fn(), }; feed.getCache = jest.fn().mockReturnValue(cache); feed.getCacheKey = jest.fn().mockReturnValue(cacheKey); }); test('should set the cached items', () => { const id = '1'; feed.setCachedItems(id, feedItems); expect(feed.getCacheKey).toHaveBeenCalledWith(id); expect(cache.set).toHaveBeenCalledWith(cacheKey, { errors: [], items: feedItems, }); }); }); describe('feedItems()', () => { const sortedItems = [ ...versionsWithCurrent.entries, ...tasks.entries, ...comments.entries, ...appActivities.entries, ...annotations.entries, ]; let successCb; let errorCb; beforeEach(() => { feed.fetchVersions = jest.fn().mockResolvedValue(versions); feed.fetchCurrentVersion = jest.fn().mockResolvedValue(mockCurrentVersion); feed.fetchTasksNew = jest.fn().mockResolvedValue(tasks); feed.fetchFileActivities = jest.fn().mockResolvedValue({ entries: mockFileActivities }); feed.fetchComments = jest.fn().mockResolvedValue(comments); feed.fetchThreadedComments = jest.fn().mockResolvedValue(threadedComments); feed.fetchAppActivity = jest.fn().mockReturnValue(appActivities); feed.fetchAnnotations = jest.fn().mockReturnValue(annotations); feed.setCachedItems = jest.fn(); feed.versionsAPI = { getVersion: jest.fn().mockReturnValue(versions), addCurrentVersion: jest.fn().mockReturnValue(versionsWithCurrent), }; successCb = jest.fn(); errorCb = jest.fn(); feed.isDestroyed = jest.fn().mockReturnValue(false); jest.spyOn(sorter, 'sortFeedItems').mockReturnValue(sortedItems); }); afterEach(() => { jest.restoreAllMocks(); }); test('should get feed items, sort, save to cache, and call the success callback', done => { feed.feedItems(file, false, successCb, errorCb, jest.fn(), { shouldShowAnnotations: true, shouldShowAppActivity: true, }); setImmediate(() => { expect(feed.versionsAPI.addCurrentVersion).toHaveBeenCalledWith(mockCurrentVersion, versions, file); expect(sorter.sortFeedItems).toHaveBeenCalledWith( versionsWithCurrent, comments, tasks, appActivities, annotations, ); expect(feed.setCachedItems).toHaveBeenCalledWith(file.id, sortedItems); expect(successCb).toHaveBeenCalledWith(sortedItems); done(); }); }); test('should get feed items, sort, save to cache, and call the error callback', done => { feed.fetchVersions = function fetchVersionsWithError() { this.errors = mockErrors; return []; }; feed.feedItems(file, false, successCb, errorCb); setImmediate(() => { expect(errorCb).toHaveBeenCalledWith(sortedItems, mockErrors); done(); }); }); test('should use the app activity api if the { shouldShowAppActivity } option in the last argument is true', done => { feed.feedItems(file, false, successCb, errorCb, errorCb, { shouldShowAppActivity: true }); setImmediate(() => { expect(feed.fetchAppActivity).toHaveBeenCalled(); done(); }); }); test('should not use the app activity api if the { shouldShowAppActivity } option in the last argument is false', done => { feed.feedItems(file, false, successCb, errorCb, errorCb, { shouldShowAppActivity: false }); setImmediate(() => { expect(feed.fetchAppActivity).not.toHaveBeenCalled(); done(); }); }); test('should use the annotations api if shouldShowannotations is true', done => { feed.feedItems(file, false, successCb, errorCb, errorCb, { shouldShowAnnotations: true }); setImmediate(() => { expect(feed.fetchAnnotations).toHaveBeenCalled(); done(); }); }); test.each` shouldShowReplies | expected ${undefined} | ${false} ${false} | ${false} ${true} | ${true} `( 'should pass $expected as shouldShowReplies when calling fetchAnnotations when $shouldShowReplies is given as shouldShowReplies', ({ shouldShowReplies, expected }) => { feed.feedItems(file, false, successCb, errorCb, errorCb, { shouldShowAnnotations: true, shouldShowReplies, }); expect(feed.fetchAnnotations).toBeCalledWith(expect.anything(), expected); }, ); test('should not use the annotations api if shouldShowannotations is false', done => { feed.feedItems(file, false, successCb, errorCb, errorCb, { shouldShowAnnotations: false }); setImmediate(() => { expect(feed.fetchAnnotations).not.toHaveBeenCalled(); done(); }); }); test('should use the threaded comments api if shouldShowReplies is true', done => { feed.feedItems(file, false, successCb, errorCb, errorCb, { shouldShowReplies: true }); setImmediate(() => { expect(feed.fetchThreadedComments).toBeCalledWith(file.permissions); done(); }); }); test('should not call success or error callback if it is destroyed', done => { feed.isDestroyed = jest.fn().mockReturnValue(true); feed.feedItems(file, false, successCb, errorCb); setImmediate(() => { expect(successCb).not.toHaveBeenCalled(); expect(errorCb).not.toHaveBeenCalled(); done(); }); }); test('should return the cached items', () => { feed.getCachedItems = jest.fn().mockReturnValue({ errors: [], items: feedItems, }); feed.feedItems(file, false, successCb, errorCb); expect(feed.getCachedItems).toHaveBeenCalledWith(file.id); expect(successCb).toHaveBeenCalledWith(feedItems); }); test('should refresh the cache after returning the cached items', done => { feed.getCachedItems = jest.fn().mockReturnValue({ errors: [], items: feedItems, }); feed.feedItems(file, true, successCb, errorCb); expect(feed.getCachedItems).toHaveBeenCalledWith(file.id); expect(successCb).toHaveBeenCalledTimes(1); expect(successCb).toHaveBeenCalledWith(feedItems); // refresh cache setImmediate(() => { expect(successCb).toHaveBeenCalledTimes(2); done(); }); }); test('should not include versions in feed items if shouldShowVersions is false', done => { feed.feedItems(file, false, successCb, errorCb, errorCb, { shouldShowVersions: false }); setImmediate(() => { expect(feed.versionsAPI.addCurrentVersion).not.toBeCalled(); expect(sorter.sortFeedItems).toBeCalledWith(undefined, comments, tasks, undefined, undefined); done(); }); }); test('should not fetch tasks and include them in feed items if shouldShowTasks is false', done => { feed.feedItems(file, false, successCb, errorCb, errorCb, { shouldShowTasks: false }); setImmediate(() => { expect(feed.fetchTasksNew).not.toBeCalled(); expect(sorter.sortFeedItems).toBeCalledWith( versionsWithCurrent, comments, undefined, undefined, undefined, ); done(); }); }); test('should use the file activities api if shouldUseUAA is true and shadow call v2 APIs', done => { feed.feedItems(file, false, successCb, errorCb, errorCb, { shouldUseUAA: true, shouldShowAnnotations: true, shouldShowAppActivity: true, shouldShowTasks: true, shouldShowReplies: true, shouldShowVersions: true, }); setImmediate(() => { expect(feed.fetchFileActivities).toBeCalledWith( file.permissions, [ FILE_ACTIVITY_TYPE_ANNOTATION, FILE_ACTIVITY_TYPE_APP_ACTIVITY, FILE_ACTIVITY_TYPE_COMMENT, FILE_ACTIVITY_TYPE_TASK, FILE_ACTIVITY_TYPE_VERSION, ], true, ); expect(feed.fetchThreadedComments).toBeCalled(); done(); }); }); }); describe('fetchAnnotations()', () => { beforeEach(() => { feed.file = file; feed.fetchFeedItemErrorCallback = jest.fn(); }); test('should return a promise and call the annotations api', () => { const annotationItems = feed.fetchAnnotations(); expect(annotationItems instanceof Promise).toBeTruthy(); expect(feed.annotationsAPI.getAnnotations).toBeCalled(); }); test('when called with shouldFetchReplies = true, should return a promise and call the annotations api with shouldFetchReplies param as true', () => { const annotationItems = feed.fetchAnnotations({ can_edit: true }, true); expect(annotationItems instanceof Promise).toBeTruthy(); expect(feed.annotationsAPI.getAnnotations).toBeCalledWith( feed.file.id, undefined, { can_edit: true }, expect.any(Function), expect.any(Function), undefined, undefined, true, ); }); }); describe('fetchComments()', () => { beforeEach(() => { feed.file = file; feed.fetchFeedItemErrorCallback = jest.fn(); }); test('should return a promise and call the comments api', () => { const commentItems = feed.fetchComments(); expect(commentItems instanceof Promise).toBeTruthy(); expect(feed.commentsAPI.getComments).toBeCalled(); }); }); describe('fetchThreadedComment()', () => { test('should throw if no file id', () => { expect(() => feed.fetchThreadedComment({})).toThrow(fileError); }); test('should throw if no file permissions', () => { expect(() => feed.fetchReplies({ id: '1234' })).toThrow(fileError); }); test('should call the threaded comments api', () => { const commentId = '123'; const successCallback = jest.fn(); const errorCallback = jest.fn(); const boundFetchThreadedCommentSuccessCallback = jest.fn(); feed.fetchThreadedCommentSuccessCallback = jest.fn(); feed.fetchThreadedCommentSuccessCallback.bind = jest.fn(() => boundFetchThreadedCommentSuccessCallback); feed.fetchThreadedComment(file, commentId, successCallback, errorCallback); expect(feed.threadedCommentsAPI.getComment).toBeCalledWith({ commentId, errorCallback, fileId: file.id, permissions: file.permissions, successCallback: boundFetchThreadedCommentSuccessCallback, }); expect(feed.fetchThreadedCommentSuccessCallback.bind).toBeCalledWith( feed, expect.any(Function), successCallback, ); }); }); describe('fetchThreadedCommentSuccessCallback()', () => { test('should call successCallback with given comment and call resolve function', () => { const comment = { id: '123' }; const successCallback = jest.fn(); const resolve = jest.fn(); feed.fetchThreadedCommentSuccessCallback(resolve, successCallback, comment); expect(successCallback).toBeCalledWith(comment); expect(resolve).toBeCalledWith(); }); }); describe('fetchThreadedComments()', () => { beforeEach(() => { feed.file = file; feed.fetchFeedItemErrorCallback = jest.fn(); }); test('should return a promise and call the threaded comments api', () => { const permissions = { can_edit: true, can_delete: true, can_resolve: true }; const commentItems = feed.fetchThreadedComments(permissions); expect(commentItems instanceof Promise).toBeTruthy(); expect(feed.threadedCommentsAPI.getComments).toBeCalledWith({ errorCallback: expect.any(Function), fileId: feed.file.id, permissions, successCallback: expect.any(Function), }); }); }); describe('fetchReplies()', () => { beforeEach(() => { feed.file = file; feed.updateFeedItem = jest.fn(); }); test('should throw if no file id', () => { expect(() => feed.fetchReplies({ permissions: { can_comment: true } })).toThrow(fileError); }); test('should throw if no file permissions', () => { expect(() => feed.fetchReplies({ id: '1234' })).toThrow(fileError); }); test('should call the threaded comments api and call passed successCallback if itemType is comment', () => { const commentId = '123'; const successCallback = jest.fn(); const errorCallback = jest.fn(); feed.fetchReplies(file, commentId, FEED_ITEM_TYPE_COMMENT, successCallback, errorCallback); expect(feed.updateFeedItem).toBeCalledWith({ isRepliesLoading: true }, commentId); expect(feed.threadedCommentsAPI.getCommentReplies).toBeCalledWith({ fileId: feed.file.id, commentId, permissions: feed.file.permissions, successCallback: expect.any(Function), errorCallback: expect.any(Function), }); expect(successCallback).toBeCalled(); }); test('should call the annotations api and call passed successCallback if itemType is annotation', () => { const annotationId = '1234'; const successCallback = jest.fn(); const errorCallback = jest.fn(); feed.fetchReplies(file, annotationId, FEED_ITEM_TYPE_ANNOTATION, successCallback, errorCallback); expect(feed.annotationsAPI.getAnnotationReplies).toBeCalledWith( feed.file.id, annotationId, feed.file.permissions, expect.any(Function), expect.any(Function), ); expect(feed.updateFeedItem).toHaveBeenNthCalledWith(1, { isRepliesLoading: true }, annotationId); expect(feed.updateFeedItem).toHaveBeenNthCalledWith( 2, { isRepliesLoading: false, replies: mockReplies, total_reply_count: mockReplies.length, }, annotationId, ); expect(successCallback).toBeCalled(); }); }); describe('Fetching Base Items', () => { beforeEach(() => { feed.file = file; feed.fetchFeedItemErrorCallback = jest.fn(); }); describe('fetchVersions()', () => { test('should return a promise and call the versions api', () => { const versionItems = feed.fetchVersions(); expect(versionItems instanceof Promise).toBeTruthy(); expect(feed.versionsAPI.getVersions).toBeCalled(); }); test('should call the versions api with the correct fields', () => { feed.fetchVersions(); expect(feed.versionsAPI.getVersions).toBeCalledWith( file.id, expect.any(Function), expect.any(Function), undefined, undefined, FEED_FILE_VERSIONS_FIELDS_TO_FETCH, ); }); }); describe('fetchCurrentVersion()', () => { test('should return a promise and call the versions api', () => { const currentVersion = feed.fetchCurrentVersion(); expect(currentVersion instanceof Promise).toBeTruthy(); expect(feed.versionsAPI.getVersion).toBeCalled(); }); }); describe('fetchAppActivity()', () => { test('should return a promise and call the app activity api', () => { const activityItems = feed.fetchAppActivity(); expect(activityItems instanceof Promise).toBeTruthy(); expect(feed.appActivityAPI.getAppActivity).toBeCalled(); }); }); describe('fetchTasksNew()', () => { test('should return a promise and call the tasks api', () => { const taskItems = feed.fetchTasksNew(); expect(taskItems instanceof Promise).toBeTruthy(); expect(feed.tasksNewAPI.getTasksForFile).toBeCalled(); }); }); describe('fetchFileActivities()', () => { beforeEach(() => { feed.file = file; feed.fetchFeedItemErrorCallback = jest.fn(); }); test('should return a promise and call the file activities api', () => { const permissions = { can_edit: true, can_delete: true, can_resolve: true, can_view_annotations: true }; const fileActivityItems = feed.fetchFileActivities(permissions, [ FILE_ACTIVITY_TYPE_ANNOTATION, FILE_ACTIVITY_TYPE_APP_ACTIVITY, FILE_ACTIVITY_TYPE_COMMENT, FILE_ACTIVITY_TYPE_TASK, ]); expect(fileActivityItems instanceof Promise).toBeTruthy(); expect(feed.fileActivitiesAPI.getActivities).toBeCalledWith({ activityTypes: [ FILE_ACTIVITY_TYPE_ANNOTATION, FILE_ACTIVITY_TYPE_APP_ACTIVITY, FILE_ACTIVITY_TYPE_COMMENT, FILE_ACTIVITY_TYPE_TASK, ], errorCallback: expect.any(Function), fileID: feed.file.id, permissions, shouldShowReplies: false, successCallback: expect.any(Function), }); expect(fileActivityItems).resolves.toEqual({ entries: mockFileActivities }); }); }); }); describe('updateTaskCollaborator()', () => { beforeEach(() => { feed.updateFeedItem = jest.fn(); feed.updateTaskCollaboratorSuccessCallback = jest.fn(); }); test('should throw if no file id', () => { expect(() => feed.updateTaskCollaborator({})).toThrow(fileError); }); test('should call the tasks collaborators api and if successful, the success callback', () => { feed.updateTaskCollaborator(file); expect(feed.taskCollaboratorsAPI.length).toBe(1); expect(feed.taskCollaboratorsAPI.pop().updateTaskCollaborator).toBeCalled(); expect(feed.updateTaskCollaboratorSuccessCallback).toBeCalled(); }); }); describe('updateTaskCollaboratorSuccessCallback()', () => { beforeEach(() => { feed.getCachedItems = jest.fn().mockReturnValue({ errors: [], items: feedItems, }); feed.updateFeedItem = jest.fn(); }); test('should refresh the task from the server and call the success callback', () => { const updatedStatus = 'COMPLETED'; const successCb = jest.fn(); const taskId = mockTask.id; feed.updateTaskCollaboratorSuccessCallback( taskId, file, { ...tasks.entries[0].assigned_to.entries[0], status: updatedStatus, }, successCb, ); expect(feed.tasksNewAPI.getTask).toBeCalled(); expect(feed.updateFeedItem).toBeCalledWith({ ...mockTask, isPending: false }, taskId); expect(successCb).toBeCalled(); }); }); describe('createTaskNew()', () => { const currentUser = { id: 'bar', }; const message = 'hi'; const assignees = [ { id: '3086276240', type: 'group', name: 'Test Group', item: { id: '3086276240', name: 'Test User', type: 'group', }, }, ]; const taskType = 'GENERAL'; const taskCompletionRule = 'ALL_ASSIGNEES'; const dueAt = null; const code = 'group_exceeds_limit'; const hasError = false; beforeEach(() => { feed.feedErrorCallback = jest.fn(); }); test('should check group size by calling groups endpoint', async () => { const mockSuccessCallback = jest.fn(); const mockErrorCallback = jest.fn(); mockGetGroupCount.mockResolvedValueOnce({ total_count: TASK_MAX_GROUP_ASSIGNEES - 1 }); feed.createTaskNew( file, currentUser, message, assignees, taskType, dueAt, taskCompletionRule, mockSuccessCallback, mockErrorCallback, ); expect(feed.file.id).toBe(file.id); await new Promise(r => setTimeout(r, 0)); expect(mockGetGroupCount).toBeCalled(); expect(mockCreateTaskWithDeps).toBeCalled(); }); test('should call error handling when group size exceeds limit', async () => { const mockSuccessCallback = jest.fn(); const mockErrorCallback = jest.fn(); mockGetGroupCount.mockResolvedValueOnce({ total_count: TASK_MAX_GROUP_ASSIGNEES + 1 }); await feed.createTaskNew( file, currentUser, message, assignees, taskType, dueAt, taskCompletionRule, mockSuccessCallback, mockErrorCallback, ); await new Promise(r => setTimeout(r, 0)); expect(feed.file.id).toBe(file.id); expect(mockGetGroupCount).toBeCalled(); expect(mockCreateTaskWithDeps).not.toBeCalled(); expect(feed.feedErrorCallback).toBeCalledWith( hasError, { code: 'group_exceeds_limit', type: 'warning' }, code, ); }); }); describe('updateTaskNew()', () => { const code = 'group_exceeds_limit'; const hasError = false; beforeEach(() => { feed.updateFeedItem = jest.fn(); }); test('should throw if no file id', async () => { expect.assertions(1); const updatedTask = feed.updateTaskNew({}); await expect(updatedTask).rejects.toEqual(Error(fileError)); }); test('should check group size by calling groups endpoint', async () => { const mockSuccessCallback = jest.fn(); const mockErrorCallback = jest.fn(); mockGetGroupCount.mockResolvedValueOnce({ total_count: TASK_MAX_GROUP_ASSIGNEES - 1 }); const task = { id: '1', description: 'updated description', addedAssignees: [ { type: 'task_collaborator', id: '19283765', item: { type: 'group', id: '19283765', name: 'adding a group', login: '' }, role: 'ASSIGNEE', permissions: { can_delete: true, can_update: true, }, status: 'incomplete', }, ], }; feed.updateTaskNew(file, task, mockSuccessCallback, mockErrorCallback); expect(feed.file.id).toBe(file.id); await new Promise(r => setTimeout(r, 0)); expect(mockGetGroupCount).toBeCalled(); }); test('should call the feed error handling when group size exceeds limit on update', async () => { const mockSuccessCallback = jest.fn(); const mockErrorCallback = jest.fn(); feed.feedErrorCallback = jest.fn(); feed.createTaskCollaborator = jest.fn(); feed.createTaskCollaboratorsforGroup = jest.fn(); mockGetGroupCount.mockResolvedValueOnce({ total_count: TASK_MAX_GROUP_ASSIGNEES + 1 }); const task = { id: '1', description: 'updated description', addedAssignees: [ { type: 'task_collaborator', id: '19283765', item: { type: 'group', id: '19283765', name: 'adding a group', login: '' }, role: 'ASSIGNEE', permissions: { can_delete: true, can_update: true, }, status: 'incomplete', }, ], }; feed.updateTaskNew(file, task, mockSuccessCallback, mockErrorCallback); expect(feed.file.id).toBe(file.id); await new Promise(r => setTimeout(r, 0)); expect(mockGetGroupCount).toBeCalled(); expect(feed.feedErrorCallback).toBeCalledWith( hasError, { code: 'group_exceeds_limit', type: 'warning' }, code, ); expect(feed.tasksNewAPI.updateTaskWithDeps).not.toBeCalled(); }); test('should call the new task api and if successful, the success callback', async () => { const successCallback = jest.fn(); const task = { id: '1', description: 'updated description', addedAssignees: [], removedAssignees: [], }; feed.updateTaskNew(file, task, successCallback, jest.fn()); expect(feed.file.id).toBe(file.id); // push a new promise to trigger the promises in updateTaskNew await new Promise(r => setTimeout(r, 0)); expect(feed.tasksNewAPI.updateTaskWithDeps).toBeCalled(); expect(feed.tasksNewAPI.getTask).toBeCalled(); expect(feed.updateFeedItem).toBeCalledTimes(2); expect(successCallback).toBeCalled(); }); test('should call the appropriate new task APIs when adding new assignees', async () => { const successCallback = jest.fn(); const task = { id: '1', description: 'updated description', addedAssignees: [ { type: 'user', id: '3086276240', name: 'Test User', login: 'testuser@foo.com', }, { type: 'group', id: '3086276240', name: 'Test User', login: 'testuser@foo.com', }, ], removedAssignees: [ { type: 'task_collaborator', id: '19283765', target: { type: 'user', id: '19283765', name: 'remove Test User', login: 'testuser@foo.com' }, role: 'ASSIGNEE', permissions: { can_delete: true, can_update: true, }, status: 'incomplete', }, ], }; feed.updateTaskNew(file, task, successCallback, jest.fn()); await new Promise(r => setTimeout(r, 0)); expect(feed.tasksNewAPI.updateTaskWithDeps).toBeCalled(); expect(feed.updateFeedItem).toBeCalled(); expect(successCallback).toBeCalled(); }); }); describe('updateComment()', () => { beforeEach(() => { feed.updateFeedItem = jest.fn(); }); test('should throw if no file id', () => { expect(() => feed.updateComment({})).toThrow(fileError); }); test('should call the comments api and update the feed items', () => { const successCallback = jest.fn(); const comment = { id: '1', tagged_message: 'updated message', message: 'update message', permissions: { can_edit: true }, }; feed.updateComment(file, comment.id, comment.text, true, comment.permmissions, successCallback, jest.fn()); expect(feed.updateFeedItem).toBeCalled(); expect(successCallback).toBeCalled(); }); }); describe('updateThreadedComment()', () => { beforeEach(() => { feed.updateFeedItem = jest.fn(); }); test('should throw if no file id', () => { expect(() => feed.updateThreadedComment({})).toThrow(fileError); }); test('should throw if no text or status', () => { expect(() => feed.updateThreadedComment( file, '123', undefined, undefined, { can_delete: true }, undefined, jest.fn(), jest.fn(), ), ).toThrowError(); }); describe('should call the threaded comments api and update the feed items', () => { test.each` testText | testStatus | expected ${'hello'} | ${undefined} | ${{ message: 'hello' }} ${undefined} | ${'open'} | ${{ status: 'open' }} ${'hello'} | ${'open'} | ${{ message: 'hello', status: 'open' }} `('given text=$testText and status=$testStatus', ({ testText, testStatus, expected }) => { const successCallback = jest.fn(); const errorCallback = jest.fn(); const comment = { id: '1', permissions: { can_edit: true }, }; feed.updateThreadedComment( file, comment.id, testText, testStatus, comment.permissions, successCallback, errorCallback, ); expect(feed.threadedCommentsAPI.updateComment).toBeCalledWith({ fileId: file.id, commentId: comment.id, permissions: comment.permissions, ...expected, successCallback: expect.any(Function), errorCallback: expect.any(Function), }); expect(feed.updateFeedItem).toBeCalled(); expect(successCallback).toBeCalled(); }); }); test('should call updateFeedItem without the replies and total_reply_count properties', () => { const commentId = '1'; const text = 'abc'; const permissions = { can_edit: true }; const updatedCommentResult = { id: commentId, permissions, replies: [], tagged_message: text, total_reply_count: 0, }; ThreadedCommentsAPI.mockReturnValueOnce({ updateComment: jest.fn().mockImplementation(({ successCallback }) => { successCallback(updatedCommentResult); }), }); feed.updateThreadedComment(file, commentId, text, undefined, permissions, jest.fn(), jest.fn()); const expectedUpdateFeedItemCommentData = { id: commentId, isPending: false, permissions, tagged_message: text, }; expect(feed.updateFeedItem).toHaveBeenNthCalledWith( 1, { tagged_message: text, isPending: true }, commentId, ); expect(feed.updateFeedItem).toHaveBeenNthCalledWith(2, expectedUpdateFeedItemCommentData, commentId); }); }); describe('updateReply()', () => { test('should throw if no file id', () => { expect(() => feed.updateReply({})).toThrow(fileError); }); test('should call the threaded comments api and update the feed items', () => { feed.updateReplyItem = jest.fn(); const successCallback = jest.fn(); const errorCallback = jest.fn(); const parentId = '123'; const text = 'abc'; const reply = { id: '1', permissions: { can_edit: true }, }; feed.updateReply(file, reply.id, parentId, text, reply.permissions, successCallback, errorCallback); expect(feed.threadedCommentsAPI.updateComment).toBeCalledWith({ fileId: file.id, commentId: reply.id, permissions: reply.permissions, message: text, status: undefined, successCallback: expect.any(Function), errorCallback: expect.any(Function), }); expect(feed.updateReplyItem).toBeCalled(); expect(successCallback).toBeCalled(); }); }); describe('deleteComment()', () => { beforeEach(() => { feed.updateFeedItem = jest.fn(); feed.deleteFeedItem = jest.fn(); }); test('should throw if no file id', () => { expect(() => feed.deleteComment({})).toThrow(fileError); }); test('should call the comments api and if successful, the success callback', () => { const successCallback = jest.fn(); const errorCallback = jest.fn(); const comment = { id: '1', permissions: { can_edit: true }, }; feed.deleteComment(file, comment.id, comment.permissions, successCallback, errorCallback); expect(feed.commentsAPI.deleteComment).toBeCalledWith({ file, commentId: comment.id, permissions: comment.permissions, successCallback: expect.any(Function), errorCallback: expect.any(Function), }); expect(feed.deleteFeedItem).toBeCalled(); }); }); describe('deleteThreadedComment()', () => { beforeEach(() => { feed.updateFeedItem = jest.fn(); feed.deleteFeedItem = jest.fn(); }); test('should throw if no file id', () => { expect(() => feed.deleteComment({})).toThrow(fileError); }); test('should call the threaded comments api and if successful, the success callback', () => { const successCallback = jest.fn(); const errorCallback = jest.fn(); const comment = { id: '1', permissions: { can_edit: true }, }; feed.deleteThreadedComment(file, comment.id, comment.permissions, true, successCallback, errorCallback); expect(feed.threadedCommentsAPI.deleteComment).toBeCalledWith({ fileId: file.id, commentId: comment.id, perm