mattermost-mobile/app/utils/post_list/index.test.ts
Harshil Sharma 2fb2e9d899
Burn On Read Posts (#9307)
* Added scaffolding for unrevealed BoR post

* Displayed reveal UI

* Displayed expiry timer

* WIP

* Displayed own post indicator and blur text

* restored button

* Ungrouped BoR posts

* WIP

* WIP

* DIsplayed error message on revealing

* Added last run check on mobile app cleanup job

* Cleanup

* lint fix

* i18n-fix

* Added tests

* test: add test suite for revealBoRPost function

* test: add unrevealed burn on read post test file

* feat: add tests for UnrevealedBurnOnReadPost component

* test: update UnrevealedBurnOnReadPost test with PostModel type

* test: replace toBeTruthy with toBeVisible for component visibility assertions

* test: add initial test file for expiry timer component

* test: add comprehensive tests for ExpiryCountdown component

* refactor: clean up test formatting and remove redundant test case

* fix: adjust test timing for ExpiryCountdown onExpiry callback

* test: fix timer test by advancing timers in smaller increments

* Added tests

* Updated tests

* fixed accidental change

* restored package.resolved

* WIP review fixes

* Review fixes

* Review fixes

* Fixed tests

* restored package.resolved

* WIP

* test: add test for skipping BoR post cleanup within 15 minutes

* test: add comprehensive test cases for expiredBoRPostCleanup

* WIP

* WIP

* test: add bor.test.ts placeholder file

* test: add comprehensive tests for BoR utility functions

* test: add comprehensive tests for formatTime function

* WIP

* test: add comprehensive tests for getLastBoRPostCleanupRun function

* Added tests

* removed a commented code

* post list optimization

* test: add test case for updating unrevealed burn-on-read post

* fix: handle error logging in expiredBoRPostCleanup test

* test: add test case for updateLastBoRCleanupRun error handling

* review fixes

* lint fixes

* Added WS event handling (#9320)

* Added WS event handling

* test: add burn on read websocket action test

* test: add tests for handleBoRPostRevealedEvent in burn_on_read

* Added tests

* test: add comprehensive error handling test cases for burn on read

* Added tests and error handling

* BoR post - restricted actions (#9315)

* Restricted post actions for BoR post type

* Prevent opening thread fr nBoR post

* WIP

* fix: remove redundant observable wrapping in post options

* fixed a change

* removed broken

* restored package.resolved

* Added tests

* Awaiting for last run to be set

* Added tests for validating expiry timer behaviour (#9338)

* Added tests for validating expiry timer behaviour

* No need of use event setup

* Bor ux fixes (#9336)

* WIP

* UI and delete fixes

* lint fixes

* fixed tests

* Improved mocking

* Improved test
2025-12-11 09:42:16 +05:30

849 lines
37 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Post} from '@constants';
import {PostTypes} from '@constants/post';
import TestHelper from '@test/test_helper';
import {
selectOrderedPostsWithPrevAndNext,
selectOrderedPosts,
generateCombinedPost,
getDateForDateLine,
getPostIdsForCombinedUserActivityPost,
preparePostList,
shouldFilterJoinLeavePost,
} from '.';
import type {PostListItem} from '@typings/components/post_list';
import type PostModel from '@typings/database/models/servers/post';
const mockPostModel = (overrides: Partial<PostModel> = {}): PostModel => ({
id: 'post-id',
channelId: 'channel-id',
createAt: Date.now(),
deleteAt: 0,
type: 'custom_post_type',
userId: 'user-id',
...overrides,
} as PostModel);
describe('selectOrderedPostsWithPrevAndNext', () => {
const lastViewedAt = Date.now() - 1000; // 1 second ago
const currentUserId = 'current-user-id';
const currentUsername = 'current-username';
const showJoinLeave = true;
const currentTimezone = 'America/New_York';
const isThreadScreen = false;
const savedPostIds = new Set<string>();
it('should return an empty array when posts array is empty', () => {
const result = selectOrderedPostsWithPrevAndNext([], lastViewedAt, true, currentUserId, currentUsername, showJoinLeave, currentTimezone, isThreadScreen, savedPostIds);
expect(result).toEqual([]);
});
it('should return ordered posts with previous and next posts', () => {
const posts = [
mockPostModel({id: 'post1', createAt: Date.now() - 5000}), // 5 seconds ago
mockPostModel({id: 'post2', createAt: Date.now() - 10000}), // 10 seconds ago
mockPostModel({id: 'post3', createAt: Date.now() - 15000}), // 15 seconds ago
];
const result = selectOrderedPostsWithPrevAndNext(posts, lastViewedAt, true, currentUserId, currentUsername, showJoinLeave, currentTimezone, isThreadScreen, savedPostIds);
// The number of posts returned should match the input
expect(result.length).toBe(4);
// Ensure the next and previous posts are set correctly
expect(typeof result[0].value === 'object' && result[0].value.nextPost).toBeUndefined(); // First post shouldn't have nextPost
expect(typeof result[0].value === 'object' && result[0].value.previousPost).toBe(posts[1]);
expect(typeof result[1].value === 'object' && result[1].value.nextPost).toBe(posts[0]);
expect(typeof result[1].value === 'object' && result[1].value.previousPost).toBe(posts[2]);
expect(typeof result[2].value === 'object' && result[2].value.nextPost).toBe(posts[1]);
expect(typeof result[2].value === 'object' && result[2].value.previousPost).toBeUndefined(); // Last post shouldn't have nextPost
});
it('should include date headers between posts from different days', () => {
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1); // 1 day ago
const posts = [
mockPostModel({id: 'post1', createAt: yesterday.getTime()}), // Yesterday
mockPostModel({id: 'post2', createAt: Date.now()}), // Today
];
const result = selectOrderedPostsWithPrevAndNext(posts, lastViewedAt, true, currentUserId, currentUsername, showJoinLeave, currentTimezone, isThreadScreen, savedPostIds);
// There should be 4 items: 2 posts + 2 date headers
expect(result.length).toBe(5);
// Ensure the first item is a date header
expect(result[1].type).toBe('date');
expect(result[4].type).toBe('date');
});
it('should add "start-of-new-messages" indicator when there are new posts', () => {
const newPostTime = Date.now() + 1000; // Post in the future (newer than `lastViewedAt`)
const posts = [
mockPostModel({id: 'post1', createAt: newPostTime, userId: 'another-user-id'}), // New post by another user
mockPostModel({id: 'post2', createAt: Date.now() - 5000}), // Older post
];
const result = selectOrderedPostsWithPrevAndNext(posts, lastViewedAt, true, currentUserId, currentUsername, showJoinLeave, currentTimezone, isThreadScreen, savedPostIds);
// There should be 3 items: 1 "start-of-new-messages" + 2 posts
expect(result.length).toBe(4);
// Ensure the "start-of-new-messages" indicator is added
expect(result[1].type).toBe('start-of-new-messages');
});
it('should not add "start-of-new-messages" indicator if there are no new posts', () => {
const oldPostTime = Date.now() - 10000; // Older than `lastViewedAt`
const posts = [
mockPostModel({id: 'post1', createAt: oldPostTime, userId: 'another-user-id'}), // Old post by another user
];
const result = selectOrderedPostsWithPrevAndNext(posts, lastViewedAt, true, currentUserId, currentUsername, showJoinLeave, currentTimezone, isThreadScreen, savedPostIds);
// There should be only 1 post one date and no "start-of-new-messages"
expect(result.length).toBe(2);
expect(result[0].type).toBe('post');
});
it('should add thread overview when isThreadScreen is true', () => {
const posts = [
mockPostModel({id: 'post1', createAt: Date.now()}),
];
const result = selectOrderedPostsWithPrevAndNext(posts, lastViewedAt, true, currentUserId, currentUsername, showJoinLeave, currentTimezone, true, savedPostIds);
// There should be 4 items: 1 post + 1 thread overview + 1 date + 1 new messages
expect(result.length).toBe(4);
// Ensure the last item is a thread overview
expect(result[0].type).toBe('thread-overview');
});
it('should mark saved posts as isSaved', () => {
const savedPosts = new Set<string>(['post1']);
const posts = [
mockPostModel({id: 'post1', createAt: Date.now()}),
mockPostModel({id: 'post2', createAt: Date.now()}),
];
const result = selectOrderedPostsWithPrevAndNext(posts, lastViewedAt, true, currentUserId, currentUsername, showJoinLeave, currentTimezone, isThreadScreen, savedPosts);
// The first post should be marked as saved
expect(typeof result[0].value === 'object' && result[0].value.isSaved).toBe(true);
// The second post should not be marked as saved
expect(typeof result[1].value === 'object' && result[1].value.isSaved).toBe(false);
});
});
describe('selectOrderedPosts', () => {
const lastViewedAt = Date.now() - 1000; // 1 second ago
const currentUserId = 'current-user-id';
const currentUsername = 'current-username';
const showJoinLeave = true;
const currentTimezone = 'America/New_York';
const isThreadScreen = false;
const savedPostIds = new Set<string>();
it('should return an empty array when posts array is empty', () => {
const result = selectOrderedPosts([], lastViewedAt, true, currentUserId, currentUsername, showJoinLeave, currentTimezone, isThreadScreen, savedPostIds);
expect(result).toEqual([]);
});
it('should order posts with date headers and new message indicators', () => {
const posts = [
mockPostModel({id: 'post1', createAt: Date.now() - 5000}), // 5 seconds ago
mockPostModel({id: 'post2', createAt: Date.now() - 10000}), // 10 seconds ago
];
const result = selectOrderedPosts(posts, lastViewedAt, true, currentUserId, currentUsername, showJoinLeave, currentTimezone, isThreadScreen, savedPostIds);
// There should be 3 items: 2 posts and 1 date header
expect(result.length).toBe(3);
// Ensure the first item is a date header
expect(result[0].type).toBe('post');
// Ensure the rest are posts
expect(result[1].type).toBe('post');
expect(result[2].type).toBe('date');
});
it('should add the "start-of-new-messages" indicator when there are new messages', () => {
const newPostTime = Date.now() + 1000; // Future post (newer than `lastViewedAt`)
const posts = [
mockPostModel({id: 'post1', createAt: newPostTime, userId: 'another-user-id'}), // New post from another user
mockPostModel({id: 'post2', createAt: Date.now() - 5000}), // Older post
];
const result = selectOrderedPosts(posts, lastViewedAt, true, currentUserId, currentUsername, showJoinLeave, currentTimezone, isThreadScreen, savedPostIds);
// There should be 4 items: 1 "start-of-new-messages" + 2 posts + 1 date header
expect(result.length).toBe(4);
// Ensure the "start-of-new-messages" indicator is added
expect(result[1].type).toBe('start-of-new-messages');
});
it('should not add "start-of-new-messages" if there are no new posts', () => {
const oldPostTime = Date.now() - 2000; // Older than `lastViewedAt`
const posts = [
mockPostModel({id: 'post1', createAt: oldPostTime, userId: 'another-user-id'}), // Old post by another user
];
const result = selectOrderedPosts(posts, lastViewedAt, true, currentUserId, currentUsername, showJoinLeave, currentTimezone, isThreadScreen, savedPostIds);
// There should be 2 items: 1 post + 1 date header, no "start-of-new-messages"
expect(result.length).toBe(2);
expect(result[1].type).toBe('date');
expect(result[0].type).toBe('post');
});
it('should handle timezone differences correctly', () => {
const postTime = Date.now() - 5000; // 5 seconds ago
const postInDifferentTimezone = mockPostModel({createAt: postTime});
const result = selectOrderedPosts([postInDifferentTimezone], lastViewedAt, true, currentUserId, currentUsername, showJoinLeave, 'Europe/London', isThreadScreen, savedPostIds);
// There should be 2 items: 1 date header + 1 post
expect(result.length).toBe(2);
expect(result[0].type).toBe('post');
expect(result[1].type).toBe('date');
});
it('should not filter out join/leave posts if showJoinLeave is false and the username is the current user', () => {
const joinLeavePost = mockPostModel({type: Post.POST_TYPES.LEAVE_CHANNEL, props: {username: currentUsername}});
const result = selectOrderedPosts([joinLeavePost], lastViewedAt, true, currentUserId, currentUsername, false, currentTimezone, isThreadScreen, savedPostIds);
expect(result.length).toBe(3);
});
it('should include join/leave posts if showJoinLeave is true', () => {
const joinLeavePost = mockPostModel({type: Post.POST_TYPES.LEAVE_CHANNEL, props: {username: currentUsername}});
const result = selectOrderedPosts([joinLeavePost], lastViewedAt, true, currentUserId, currentUsername, true, currentTimezone, isThreadScreen, savedPostIds);
// There should be 2 items: 1 date header + 1 join/leave post + new message
expect(result.length).toBe(3);
expect(result[0].type).toBe('post');
});
it('should add thread overview post when isThreadScreen is true', () => {
const posts = [
mockPostModel({id: 'post1', createAt: Date.now()}),
];
const result = selectOrderedPosts(posts, lastViewedAt, true, currentUserId, currentUsername, showJoinLeave, currentTimezone, true, savedPostIds);
// There should be 3 items: 1 post + 1 thread overview + 1 date header
expect(result.length).toBe(4);
// Ensure the last item is a thread overview
expect(result[0].type).toBe('thread-overview');
});
it('should mark saved posts as isSaved', () => {
const savedPosts = new Set<string>(['post1']);
const posts = [
mockPostModel({id: 'post1', createAt: Date.now()}),
mockPostModel({id: 'post2', createAt: Date.now()}),
];
const result = selectOrderedPosts(posts, lastViewedAt, true, currentUserId, currentUsername, showJoinLeave, currentTimezone, isThreadScreen, savedPosts);
// The first post should be marked as saved
expect(typeof result[0].value === 'object' && result[0].value.isSaved).toBe(true);
// The second post should not be marked as saved
expect(typeof result[1].value === 'object' && result[1].value.isSaved).toBe(false);
});
it('should filter out expired BoR posts', () => {
const now = Date.now();
const borPostExpiredForAll = TestHelper.fakePostModel({
id: 'postid1',
type: PostTypes.BURN_ON_READ,
props: {expire_at: now - 10000},
});
const borPostExpiredForMe = TestHelper.fakePostModel({
id: 'postid2',
type: PostTypes.BURN_ON_READ,
props: {expire_at: now + 100000},
metadata: {expire_at: now - 10000},
});
const unrevealedBoRPost = TestHelper.fakePostModel({
id: 'postid3',
type: PostTypes.BURN_ON_READ,
props: {expire_at: now + 10000},
});
const result = selectOrderedPosts([borPostExpiredForAll, borPostExpiredForMe, unrevealedBoRPost], lastViewedAt, true, currentUserId, currentUsername, showJoinLeave, currentTimezone, isThreadScreen, savedPostIds);
// Both BoR posts should be filtered out
expect(result.length).toBe(2);
expect(result[0].type).toBe('post');
expect(result[1].type).toBe('date');
expect((result[0] as PostListItem).value.currentPost.id).toBe(unrevealedBoRPost.id);
});
});
describe('generateCombinedPost', () => {
it('should generate a combined post with the correct id and channel_id', () => {
const systemPosts = [
mockPostModel({id: 'post1', channelId: 'channel1', createAt: Date.now() - 5000, message: 'Message 1'}),
mockPostModel({id: 'post2', channelId: 'channel1', createAt: Date.now() - 10000, message: 'Message 2'}),
];
const combinedId = 'combined-post-id';
const result = generateCombinedPost(combinedId, systemPosts);
// Ensure the combined post has the correct id and channel_id
expect(result.id).toBe(combinedId);
expect(result.channel_id).toBe('channel1');
});
it('should combine messages from all system posts into a single message', () => {
const systemPosts = [
mockPostModel({message: 'Message 1'}),
mockPostModel({message: 'Message 2'}),
];
const result = generateCombinedPost('combined-post-id', systemPosts);
// The combined message should be a concatenation of all system post messages
expect(result.message).toBe('Message 1\nMessage 2');
});
it('should use the createAt timestamp from the last post in the system posts array', () => {
const systemPosts = [
mockPostModel({createAt: Date.now() - 20000}),
mockPostModel({createAt: Date.now() - 10000}),
mockPostModel({createAt: Date.now() - 5000}),
];
const result = generateCombinedPost('combined-post-id', systemPosts);
// The createAt timestamp should be from the last post in the array
expect(result.create_at).toBe(systemPosts[systemPosts.length - 1].createAt);
});
it('should include all system_post_ids in the props object', () => {
const systemPosts = [
mockPostModel({id: 'post1'}),
mockPostModel({id: 'post2'}),
mockPostModel({id: 'post3'}),
];
const result = generateCombinedPost('combined-post-id', systemPosts);
// Ensure all post ids are included in the system_post_ids prop
expect(result.props?.system_post_ids).toEqual(['post1', 'post2', 'post3']);
});
it('should include combined messages in the props object', () => {
const systemPosts = [
mockPostModel({message: 'Message 1'}),
mockPostModel({message: 'Message 2'}),
];
const result = generateCombinedPost('combined-post-id', systemPosts);
// Ensure the messages prop includes all individual messages
expect(result.props?.messages).toEqual(['Message 1', 'Message 2']);
});
it('should set the post type to COMBINED_USER_ACTIVITY', () => {
const systemPosts = [
mockPostModel({id: 'post1'}),
mockPostModel({id: 'post2'}),
];
const result = generateCombinedPost('combined-post-id', systemPosts);
// Ensure the post type is COMBINED_USER_ACTIVITY
expect(result.type).toBe('system_combined_user_activity');
});
it('should handle an empty systemPosts array gracefully', () => {
const systemPosts: PostModel[] = [];
// Test that an empty array throws an error or handles the case
expect(() => generateCombinedPost('combined-post-id', systemPosts)).toThrow();
});
it('should correctly sort system posts based on post type priority', () => {
const systemPosts: PostModel[] = [
mockPostModel({
id: 'post1',
type: Post.POST_TYPES.ADD_TO_TEAM,
props: {
addedUserId: 'user1',
addedUsername: 'username1',
},
}),
mockPostModel({
id: 'post2',
type: Post.POST_TYPES.REMOVE_FROM_TEAM,
props: {
removedUserId: 'user2',
removedUsername: 'username2',
},
}),
mockPostModel({
id: 'post3',
type: Post.POST_TYPES.JOIN_TEAM,
props: {},
}),
mockPostModel({
id: 'post4',
type: Post.POST_TYPES.LEAVE_TEAM,
props: {},
}),
];
const combinedPostId = 'combined-post-id';
const result = generateCombinedPost(combinedPostId, systemPosts);
// Extract the post types from the sorted messages in the combined post props
const sortedPostTypes = (result.props?.user_activity_posts as any).map((post: PostModel) => post.type);
// Expect the post types to be sorted based on their priorities in comparePostTypes
expect(sortedPostTypes).toEqual([
Post.POST_TYPES.ADD_TO_TEAM, // Next priority
Post.POST_TYPES.REMOVE_FROM_TEAM, // Lowest priority
Post.POST_TYPES.JOIN_TEAM, // Highest priority
Post.POST_TYPES.LEAVE_TEAM, // Next priority
]);
});
it('should combine user activity for ADD_TO_TEAM posts correctly', () => {
const systemPosts: PostModel[] = [
mockPostModel({
id: 'post1',
type: Post.POST_TYPES.ADD_TO_TEAM,
props: {
addedUserId: 'user1',
addedUsername: 'username1',
},
}),
mockPostModel({
id: 'post2',
type: Post.POST_TYPES.ADD_TO_TEAM,
props: {
addedUserId: 'user2',
addedUsername: 'username2',
},
}),
];
const combinedPostId = 'combined-post-id';
const result = generateCombinedPost(combinedPostId, systemPosts);
// Ensure user activities are combined for ADD_TO_TEAM
expect((result.props?.user_activity_posts as any).length).toBe(2);
expect((result.props?.user_activity_posts as any)[0].props.addedUserId).toBe('user1');
expect((result.props?.user_activity_posts as any)[1].props.addedUserId).toBe('user2');
});
it('should combine user activity for REMOVE_FROM_CHANNEL posts correctly', () => {
const systemPosts: PostModel[] = [
mockPostModel({
id: 'post1',
type: Post.POST_TYPES.REMOVE_FROM_CHANNEL,
props: {
removedUserId: 'user3',
removedUsername: 'username3',
},
}),
mockPostModel({
id: 'post2',
type: Post.POST_TYPES.REMOVE_FROM_CHANNEL,
props: {
removedUserId: 'user4',
removedUsername: 'username4',
},
}),
];
const combinedPostId = 'combined-post-id';
const result = generateCombinedPost(combinedPostId, systemPosts);
// Ensure user activities are combined for REMOVE_FROM_CHANNEL
expect((result.props?.user_activity_posts as any).length).toBe(2);
expect((result.props?.user_activity_posts as any)[0].props.removedUserId).toBe('user3');
expect((result.props?.user_activity_posts as any)[1].props.removedUserId).toBe('user4');
});
it('should handle empty systemPosts gracefully', () => {
const systemPosts: PostModel[] = [];
const combinedPostId = 'combined-post-id';
// Expect an error due to empty systemPosts
expect(() => generateCombinedPost(combinedPostId, systemPosts)).toThrow();
});
});
describe('getDateForDateLine', () => {
it('should extract the correct timestamp from a valid date line', () => {
const dateLine = 'date-1626098400000'; // July 12, 2021
const result = getDateForDateLine(dateLine);
// Expect the result to be the correct timestamp
expect(result).toBe(1626098400000);
});
it('should handle invalid date lines and return NaN', () => {
const invalidDateLine = 'invalid-prefix-12345';
// The function should throw an error if the prefix is incorrect
expect(getDateForDateLine(invalidDateLine)).toBeNaN();
});
it('should return NaN if the string is not a valid date line format', () => {
const incompleteDateLine = 'date-'; // Missing the timestamp part
// The function should throw an error if no valid timestamp is present
expect(getDateForDateLine(incompleteDateLine)).toBeNaN();
});
it('should return NaN if the timestamp part is not a number', () => {
const invalidTimestampDateLine = 'date-not_a_number';
// The result should be NaN when the timestamp part is not a valid number
const result = getDateForDateLine(invalidTimestampDateLine);
expect(result).toBeNaN();
});
it('should extract timestamp correctly when there are extra characters after the timestamp', () => {
const dateLineWithExtra = 'date-1626098400000-extra';
const result = getDateForDateLine(dateLineWithExtra);
// Expect the result to correctly extract the timestamp and ignore the extra characters
expect(result).toBe(1626098400000);
});
});
describe('getPostIdsForCombinedUserActivityPost', () => {
it('should extract post IDs from a valid combined user activity post string', () => {
const combinedPostId = 'user-activity-post1_post2_post3';
const result = getPostIdsForCombinedUserActivityPost(combinedPostId);
// Expect the result to be an array of post IDs
expect(result).toEqual(['post1', 'post2', 'post3']);
});
it('should return an array with an empty value if there are no post IDs after the prefix', () => {
const combinedPostId = 'user-activity-';
const result = getPostIdsForCombinedUserActivityPost(combinedPostId);
// Expect an array with empty value since there are no post IDs after the prefix
expect(result).toEqual(['']);
});
it('should return a single post ID if there is only one post ID', () => {
const combinedPostId = 'user-activity-post1';
const result = getPostIdsForCombinedUserActivityPost(combinedPostId);
// Expect an array with one post ID
expect(result).toEqual(['post1']);
});
it('should return an empty array if the string does not start with the correct prefix', () => {
const invalidPrefix = 'wrong-prefix-post1_post2';
expect(() => getPostIdsForCombinedUserActivityPost(invalidPrefix)).toThrow('Invalid prefix');
});
it('should handle post IDs with special characters correctly', () => {
const combinedPostId = 'user-activity-post1_post@_special#id_3';
const result = getPostIdsForCombinedUserActivityPost(combinedPostId);
// Expect the result to correctly extract the post IDs, including special characters
expect(result).toEqual(['post1', 'post@', 'special#id', '3']);
});
});
describe('preparePostList', () => {
const lastViewedAt = Date.now() - 1000; // 1 second ago
const currentUserId = 'current-user-id';
const currentUsername = 'current-username';
const showJoinLeave = true;
const currentTimezone = 'America/New_York';
const isThreadScreen = false;
const savedPostIds = new Set<string>();
it('should return an empty array when posts array is empty', () => {
const result = preparePostList([], lastViewedAt, true, currentUserId, currentUsername, showJoinLeave, currentTimezone, isThreadScreen, savedPostIds);
expect(result).toEqual([]);
});
it('should process posts and add new messages indicator if necessary', () => {
const newPostTime = Date.now() + 1000; // Future post (newer than `lastViewedAt`)
const posts = [
mockPostModel({id: 'post1', createAt: newPostTime, userId: 'another-user-id'}),
mockPostModel({id: 'post2', createAt: Date.now() - 5000}),
];
const result = preparePostList(posts, lastViewedAt, true, currentUserId, currentUsername, showJoinLeave, currentTimezone, isThreadScreen, savedPostIds);
// There should be 4 items: 1 "start-of-new-messages" + 2 posts + 1 date header
expect(result.length).toBe(4);
expect(result[0].type).toBe('post');
expect(result[1].type).toBe('start-of-new-messages');
expect(result[2].type).toBe('post');
expect(result[3].type).toBe('date');
});
it('should handle posts without a new message indicator', () => {
const oldPostTime = Date.now() - 2000; // Older than `lastViewedAt`
const posts = [
mockPostModel({id: 'post1', createAt: oldPostTime, userId: 'another-user-id'}),
];
const result = preparePostList(posts, lastViewedAt, true, currentUserId, currentUsername, showJoinLeave, currentTimezone, isThreadScreen, savedPostIds);
// There should be 2 items: 1 post + 1 date header
expect(result.length).toBe(2);
expect(result[0].type).toBe('post');
expect(result[1].type).toBe('date');
});
it('should combine user activity posts correctly', () => {
const posts = [
mockPostModel({id: 'post1', type: 'system_combined_user_activity', props: {user_activity_posts: []}}),
mockPostModel({id: 'post2', type: 'system_combined_user_activity', props: {user_activity_posts: []}}),
];
const result = preparePostList(posts, lastViewedAt, true, currentUserId, currentUsername, showJoinLeave, currentTimezone, isThreadScreen, savedPostIds);
// Ensure that user activity posts are combined correctly
// Since it's calling `combineUserActivityPosts`, it should handle those posts.
expect(result.length).toBe(4); // 2 posts + new messages + 1 date header
expect(result[0].type).toBe('post');
expect(result[1].type).toBe('post');
expect(result[2].type).toBe('start-of-new-messages');
expect(result[3].type).toBe('date');
});
it('should handle posts with deleteAt property in preparePostList', () => {
const deletedPost: PostModel = mockPostModel({
id: 'deleted-post',
deleteAt: Date.now(), // Set deleteAt to a non-zero value
createAt: Date.now(),
type: '',
userId: 'user-id',
message: 'This is a deleted post',
props: {},
});
const posts = [
deletedPost, // The post that should trigger the deleteAt condition
];
const result = preparePostList(posts, lastViewedAt, true, currentUserId, currentUsername, showJoinLeave, currentTimezone, isThreadScreen, savedPostIds);
// Ensure the deleted post is included in the result
expect(result.length).toBe(3); // 1 date header + 1 deleted post + new messages
expect(typeof result[0].value === 'object' && result[0].value.currentPost.id).toBe('deleted-post');
});
it('should combine consecutive user activity posts when postIsUserActivity is true', () => {
// Mock consecutive user activity posts
const userActivityPost1: PostModel = mockPostModel({
id: 'user-activity-post1',
type: Post.POST_TYPES.LEAVE_CHANNEL,
createAt: Date.now() - 5000,
userId: 'user-id-1',
message: 'User activity post 1',
props: {},
});
const userActivityPost2: PostModel = mockPostModel({
id: 'user-activity-post2',
type: Post.POST_TYPES.JOIN_CHANNEL,
createAt: Date.now() - 4000,
userId: 'user-id-2',
message: 'User activity post 2',
props: {},
});
const posts: PostModel[] = [
userActivityPost1,
userActivityPost2, // These two should be combined
];
const result = preparePostList(posts, lastViewedAt, true, currentUserId, currentUsername, showJoinLeave, currentTimezone, isThreadScreen, savedPostIds);
// We expect the two user activity posts to be combined
expect(result.length).toBe(2); // 1 date header + 1 combined user activity post
expect(result[0].type).toBe('user-activity');
});
it('should filter out join/leave posts if showJoinLeave is false for other users', () => {
const joinLeavePost = mockPostModel({type: Post.POST_TYPES.LEAVE_CHANNEL, props: {username: 'other-user'}});
const result = preparePostList([joinLeavePost], lastViewedAt, true, currentUserId, currentUsername, false, currentTimezone, isThreadScreen, savedPostIds);
// No posts should be returned because showJoinLeave is false
expect(result.length).toBe(0);
});
it('should include join/leave posts if showJoinLeave is true', () => {
const joinLeavePost = mockPostModel({type: Post.POST_TYPES.JOIN_CHANNEL, props: {username: currentUsername}});
const result = preparePostList([joinLeavePost], lastViewedAt, true, currentUserId, currentUsername, true, currentTimezone, isThreadScreen, savedPostIds);
// There should be 2 items: 1 post + new message + 1 date header
expect(result.length).toBe(3);
expect(result[0].type).toBe('user-activity');
});
it('should handle saved posts correctly', () => {
const savedPosts = new Set<string>(['post1']);
const posts = [
mockPostModel({id: 'post1', createAt: Date.now()}),
mockPostModel({id: 'post2', createAt: Date.now()}),
];
const result = preparePostList(posts, lastViewedAt, true, currentUserId, currentUsername, showJoinLeave, currentTimezone, isThreadScreen, savedPosts);
// The first post should be marked as saved
expect(typeof result[0].value === 'object' && result[0].value.isSaved).toBe(true);
// The second post should not be marked as saved
expect(typeof result[1].value === 'object' && result[1].value.isSaved).toBe(false);
});
it('should handle thread overview when isThreadScreen is true', () => {
const posts = [
mockPostModel({id: 'post1', createAt: Date.now()}),
];
const result = preparePostList(posts, lastViewedAt, true, currentUserId, currentUsername, showJoinLeave, currentTimezone, true, savedPostIds);
// There should be 3 items: 1 post + 1 thread overview + 1 date header + new line
expect(result.length).toBe(4);
// Ensure the last item is a thread overview
expect(result[0].type).toBe('thread-overview');
});
it('should return false when post has no props in isJoinLeavePostForUsername', () => {
const joinLeavePost = mockPostModel({type: Post.POST_TYPES.LEAVE_CHANNEL, props: undefined}); // No props
const result = preparePostList([joinLeavePost], lastViewedAt, true, currentUserId, currentUsername, true, currentTimezone, isThreadScreen, savedPostIds);
// Post should be included because showJoinLeave is true, but it shouldn't be treated as a join/leave post for the current user
expect(result.length).toBe(3); // 1 post + 1 date header + new messages
expect(result[0].type).toBe('user-activity');
});
it('should return false when currentUsername is undefined in isJoinLeavePostForUsername', () => {
const joinLeavePost = mockPostModel({type: Post.POST_TYPES.LEAVE_CHANNEL, props: {username: 'other-username'}});
const result = preparePostList([joinLeavePost], lastViewedAt, true, currentUserId, '', true, currentTimezone, isThreadScreen, savedPostIds);
// Post should be included because showJoinLeave is true, but it shouldn't be treated as a join/leave post for the current user since currentUsername is undefined
expect(result.length).toBe(3); // 1 post + 1 date header
expect(result[0].type).toBe('user-activity');
});
});
describe('shouldFilterJoinLeavePost', () => {
const currentUsername = 'current-username';
it('should not filter non-join/leave posts', () => {
const post = mockPostModel({type: 'custom_post_type' as PostType});
const result = shouldFilterJoinLeavePost(post, false, currentUsername);
// Non-join/leave posts should not be filtered regardless of showJoinLeave value
expect(result).toBe(false);
});
it('should not filter join/leave posts if showJoinLeave is true', () => {
const post = mockPostModel({type: Post.POST_TYPES.LEAVE_CHANNEL, props: {username: 'another-user'}});
const result = shouldFilterJoinLeavePost(post, true, currentUsername);
// Join/leave posts should not be filtered if showJoinLeave is true
expect(result).toBe(false);
});
it('should not filter join/leave posts related to the current user even if showJoinLeave is false', () => {
const post = mockPostModel({type: Post.POST_TYPES.JOIN_CHANNEL, props: {username: currentUsername}});
const result = shouldFilterJoinLeavePost(post, false, currentUsername);
// Join/leave posts for the current user should not be filtered even if showJoinLeave is false
expect(result).toBe(false);
});
it('should filter join/leave posts unrelated to the current user if showJoinLeave is false', () => {
const post = mockPostModel({type: Post.POST_TYPES.LEAVE_CHANNEL, props: {username: 'another-user'}});
const result = shouldFilterJoinLeavePost(post, false, currentUsername);
// Join/leave posts unrelated to the current user should be filtered if showJoinLeave is false
expect(result).toBe(true);
});
it('should return true if post has no props', () => {
const post = mockPostModel({type: Post.POST_TYPES.LEAVE_CHANNEL});
const result = shouldFilterJoinLeavePost(post, false, currentUsername);
// The post should be filtered because we can't determine if it's a join/leave post for the current user
expect(result).toBe(true);
});
it('should return true if currentUsername is undefined', () => {
const post = mockPostModel({type: Post.POST_TYPES.LEAVE_CHANNEL, props: {username: 'another-user'}});
const result = shouldFilterJoinLeavePost(post, false, '');
// The post should be filtered because we can't determine if it's a join/leave post for the current user
expect(result).toBe(true);
});
it('should iterate over child posts in user_activity_posts and not filter if one matches current user', () => {
const childPost1 = mockPostModel({id: 'child1', props: {username: 'another-user'}});
const childPost2 = mockPostModel({id: 'child2', props: {username: currentUsername}});
const post = mockPostModel({
type: Post.POST_TYPES.LEAVE_CHANNEL,
props: {
user_activity_posts: [childPost1, childPost2], // Add child posts here
},
});
const result = shouldFilterJoinLeavePost(post, false, currentUsername);
// It should not filter because one of the child posts has the currentUsername
expect(result).toBe(false);
});
it('should iterate over child posts in user_activity_posts and filter if none match current user', () => {
const childPost1 = mockPostModel({id: 'child1', props: {username: 'another-user'}});
const childPost2 = mockPostModel({id: 'child2', props: {username: 'yet-another-user'}});
const post = mockPostModel({
type: Post.POST_TYPES.LEAVE_CHANNEL,
props: {
user_activity_posts: [childPost1, childPost2], // Add child posts here
},
});
const result = shouldFilterJoinLeavePost(post, false, currentUsername);
// It should filter because none of the child posts match currentUsername
expect(result).toBe(true);
});
});