box-ui-elements
Version:
Box UI Elements
1,356 lines (1,220 loc) • 89.5 kB
JavaScript
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