Remaining test for draft and Scheduled post (#8887)

* Added test for draft_scheduled_post and header component

* Added test for drafts_button/index.ts

* Added test for send_button/index.ts

* Added test for servers/scheduled_post.ts queries

* Added test for global_scheduled_post_list/index.ts

* Added test for rescheduled draft index file and minor update

* Added test for core option and index

* Added test for scheduled post options

* Added test for send_draft index file

* updated test for draft_scheduled_post and draft_scheduled_post_header

* Updated test for drafts_button index

* Updated test for send_button index

* Updated test for server/scheduled_post

* Updated test for global_scheduled_post/index

* removed the unnecessary config and team data to populate in db for test

* Update app/components/draft_scheduled_post/draft_scheduled_post.test.tsx

Co-authored-by: Daniel Espino García <larkox@gmail.com>

* linter fixes

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
Co-authored-by: Daniel Espino García <larkox@gmail.com>
This commit is contained in:
Rajat Dabade 2025-07-01 13:47:02 +05:30 committed by GitHub
parent d804f70d08
commit 6bdcf2be49
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 1739 additions and 12 deletions

View file

@ -0,0 +1,220 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {fireEvent, screen} from '@testing-library/react-native';
import React from 'react';
import {switchToThread} from '@actions/local/thread';
import {switchToChannelById} from '@actions/remote/channel';
import {DRAFT_TYPE_DRAFT, DRAFT_TYPE_SCHEDULED} from '@constants/draft';
import {openAsBottomSheet} from '@screens/navigation';
import {renderWithIntlAndTheme} from '@test/intl-test-helper';
import TestHelper from '@test/test_helper';
import DraftAndScheduledPost from './draft_scheduled_post';
import type {AvailableScreens} from '@typings/screens/navigation';
jest.mock('@actions/local/thread', () => ({
switchToThread: jest.fn(),
}));
jest.mock('@actions/remote/channel', () => ({
switchToChannelById: jest.fn(),
}));
jest.mock('@screens/navigation', () => ({
openAsBottomSheet: jest.fn(),
}));
jest.mock('@context/server', () => ({
useServerUrl: jest.fn().mockReturnValue('https://server.com'),
}));
jest.mock('@components/draft_scheduled_post_header', () => () => null);
jest.mock('./draft_scheduled_post_container', () => () => null);
describe('DraftAndScheduledPost', () => {
const baseProps: Parameters<typeof DraftAndScheduledPost>[0] = {
channel: TestHelper.fakeChannelModel({
id: 'channel-id',
teamId: 'team-id',
}),
location: 'location' as AvailableScreens,
post: TestHelper.fakeDraftModel({
rootId: '',
updateAt: 1234567890,
metadata: {},
files: [] as FileInfo[],
}),
layoutWidth: 300,
isPostPriorityEnabled: false,
draftType: DRAFT_TYPE_DRAFT,
};
beforeEach(() => {
jest.clearAllMocks();
});
it('renders correctly for draft', () => {
renderWithIntlAndTheme(<DraftAndScheduledPost {...baseProps}/>);
expect(screen.getByTestId('draft_post')).toBeTruthy();
});
it('renders error line for scheduled post with error', () => {
const props = {
...baseProps,
draftType: DRAFT_TYPE_SCHEDULED,
post: TestHelper.fakeScheduledPostModel({
rootId: '',
updateAt: 1234567890,
metadata: {},
files: [] as FileInfo[],
scheduledAt: 1234567890,
errorCode: 'some_error',
}),
};
renderWithIntlAndTheme(<DraftAndScheduledPost {...props}/>);
expect(screen.getByTestId('draft_post.error_line')).toBeTruthy();
});
it('does not render error line for scheduled post without error', () => {
const props = {
...baseProps,
draftType: DRAFT_TYPE_SCHEDULED,
post: TestHelper.fakeScheduledPostModel({
rootId: '',
updateAt: 1234567890,
metadata: {},
files: [] as FileInfo[],
scheduledAt: 1234567890,
errorCode: '',
}),
};
renderWithIntlAndTheme(<DraftAndScheduledPost {...props}/>);
expect(screen.queryByTestId('draft_post.error_line')).toBeNull();
});
it('does not render error line for draft posts', () => {
renderWithIntlAndTheme(<DraftAndScheduledPost {...baseProps}/>);
expect(screen.queryByTestId('draft_post.error_line')).toBeNull();
});
it('renders post priority when enabled and present', () => {
const props = {
...baseProps,
isPostPriorityEnabled: true,
post: TestHelper.fakeDraftModel({
rootId: '',
updateAt: 1234567890,
files: [] as FileInfo[],
metadata: {
priority: {
priority: 'important',
},
},
}),
};
renderWithIntlAndTheme(<DraftAndScheduledPost {...props}/>);
expect(screen.getByTestId('draft_post.priority')).toBeTruthy();
});
it('does not render post priority when disabled', () => {
const props = {
...baseProps,
isPostPriorityEnabled: false,
post: TestHelper.fakeDraftModel({
rootId: '',
updateAt: 1234567890,
files: [] as FileInfo[],
metadata: {
priority: {
priority: 'important',
},
},
}),
};
renderWithIntlAndTheme(<DraftAndScheduledPost {...props}/>);
expect(screen.queryByTestId('draft_post.priority')).toBeNull();
});
it('does not render post priority when not present in metadata', () => {
const props = {
...baseProps,
isPostPriorityEnabled: true,
post: TestHelper.fakeDraftModel({
rootId: '',
updateAt: 1234567890,
files: [] as FileInfo[],
metadata: {},
}),
};
renderWithIntlAndTheme(<DraftAndScheduledPost {...props}/>);
expect(screen.queryByTestId('draft_post.priority')).toBeNull();
});
it('navigates to thread when post has rootId', () => {
const props = {
...baseProps,
post: TestHelper.fakeDraftModel({
rootId: 'root-id',
updateAt: 1234567890,
metadata: {},
files: [] as FileInfo[],
}),
};
renderWithIntlAndTheme(<DraftAndScheduledPost {...props}/>);
fireEvent.press(screen.getByTestId('draft_post'));
expect(switchToThread).toHaveBeenCalledWith('https://server.com', 'root-id', false);
expect(switchToChannelById).not.toHaveBeenCalled();
});
it('navigates to channel when post has no rootId', () => {
renderWithIntlAndTheme(<DraftAndScheduledPost {...baseProps}/>);
fireEvent.press(screen.getByTestId('draft_post'));
expect(switchToChannelById).toHaveBeenCalledWith('https://server.com', 'channel-id', 'team-id', false);
expect(switchToThread).not.toHaveBeenCalled();
});
it('opens options on long press for draft', () => {
const mockOpenAsBottonSheet = jest.mocked(openAsBottomSheet);
renderWithIntlAndTheme(<DraftAndScheduledPost {...baseProps}/>);
fireEvent(screen.getByTestId('draft_post'), 'longPress');
expect(mockOpenAsBottonSheet).toHaveBeenCalled();
expect(mockOpenAsBottonSheet.mock.calls[0][0].props?.draftType).toBe(DRAFT_TYPE_DRAFT);
});
it('opens options on long press for scheduled post', () => {
const mockOpenAsBottonSheet = jest.mocked(openAsBottomSheet);
const props = {
...baseProps,
draftType: DRAFT_TYPE_SCHEDULED,
post: TestHelper.fakeScheduledPostModel({
rootId: '',
updateAt: 1234567890,
metadata: {},
files: [] as FileInfo[],
scheduledAt: 1234567890,
}),
};
renderWithIntlAndTheme(<DraftAndScheduledPost {...props}/>);
fireEvent(screen.getByTestId('draft_post'), 'longPress');
expect(mockOpenAsBottonSheet).toHaveBeenCalled();
expect(mockOpenAsBottonSheet.mock.calls[0][0].props?.draftType).toBe(DRAFT_TYPE_SCHEDULED);
});
});

View file

@ -118,7 +118,10 @@ const DraftAndScheduledPost: React.FC<Props> = ({
>
<View style={style.container}>
{draftType === DRAFT_TYPE_SCHEDULED && (post as ScheduledPostModel).errorCode !== '' &&
<View style={style.errorLine}/>
<View
style={style.errorLine}
testID='draft_post.error_line'
/>
}
<View
style={[style.postContainer, firstItem ? null : style.postContainerBorderTop]}
@ -134,7 +137,10 @@ const DraftAndScheduledPost: React.FC<Props> = ({
scheduledPostErrorCode={'errorCode' in post ? post.errorCode : undefined}
/>
{showPostPriority && post.metadata?.priority &&
<View style={style.postPriority}>
<View
style={style.postPriority}
testID='draft_post.priority'
>
<Header
noMentionsError={false}
postPriority={post.metadata?.priority}

View file

@ -0,0 +1,179 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {General, Screens} from '@constants';
import {SYSTEM_IDENTIFIERS} from '@constants/database';
import permissions from '@constants/permissions';
import {PostPriorityType} from '@constants/post';
import DatabaseManager from '@database/manager/__mocks__';
import {renderWithEverything} from '@test/intl-test-helper';
import TestHelper from '@test/test_helper';
import SendDraft from './send_draft';
import EnhancedSendDraft from './index';
import type ServerDataOperator from '@database/operator/server_data_operator';
import type Database from '@nozbe/watermelondb/Database';
jest.mock('./send_draft', () => ({
__esModule: true,
default: jest.fn(),
}));
jest.mocked(SendDraft).mockImplementation((props) => React.createElement('SendDraft', {...props, testID: 'send-draft'}));
describe('EnhancedSendDraft', () => {
const serverUrl = 'server-1';
const teamId = 'team1';
let database: Database;
let operator: ServerDataOperator;
const defaultProps = {
channelId: 'channel1',
rootId: 'root1',
channelType: General.DM_CHANNEL,
currentUserId: 'user1',
channelName: 'channel1',
maxMessageLength: 1000,
customEmojis: [],
value: '',
files: [],
useChannelMentions: false,
userIsOutOfOffice: false,
persistentNotificationInterval: 0,
persistentNotificationMaxRecipients: 0,
postPriority: {
priority: PostPriorityType.STANDARD,
requested_ack: false,
persistent_notifications: false,
},
bottomSheetId: Screens.SCHEDULED_POST_OPTIONS,
channelDisplayName: 'channel1',
draftReceiverUserName: '',
};
beforeEach(async () => {
await DatabaseManager.init([serverUrl]);
const serverDatabaseAndOperator = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
database = serverDatabaseAndOperator.database;
operator = serverDatabaseAndOperator.operator;
await operator.handleUsers({users: [TestHelper.fakeUser({
id: 'user1',
})],
prepareRecordsOnly: false});
await operator.handleTeam({teams: [TestHelper.fakeTeam({id: teamId})], prepareRecordsOnly: false});
await operator.handleSystem(
{
systems: [
{
id: SYSTEM_IDENTIFIERS.CURRENT_TEAM_ID,
value: teamId,
},
{
id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID,
value: 'user1',
},
{
id: SYSTEM_IDENTIFIERS.CURRENT_CHANNEL_ID,
value: 'channel1',
},
],
prepareRecordsOnly: false,
});
await operator.handleConfigs({
configs: [
{id: 'Version', value: '7.6.0'},
],
configsToDelete: [],
prepareRecordsOnly: false,
});
await operator.handleMyTeam({
myTeams: [
{
id: teamId,
roles: 'role-name',
},
],
prepareRecordsOnly: false,
});
await operator.handleRole({roles: [TestHelper.fakeRole({
id: 'role-id',
name: 'role-name',
permissions: [permissions.CREATE_POST],
})],
prepareRecordsOnly: false});
});
afterEach(async () => {
await DatabaseManager.destroyServerDatabase(serverUrl);
});
it('should return the correct values', async () => {
await operator.handleChannel({channels: [TestHelper.fakeChannel({id: 'channel1', team_id: teamId})], prepareRecordsOnly: false});
const {getByTestId} = renderWithEverything(
<EnhancedSendDraft {...defaultProps}/>,
{database},
);
expect(getByTestId('send-draft')).toBeTruthy();
expect(getByTestId('send-draft').props.canPost).toBe(true);
expect(getByTestId('send-draft').props.channelIsArchived).toBe(false);
expect(getByTestId('send-draft').props.channelIsReadOnly).toBe(false);
expect(getByTestId('send-draft').props.deactivatedChannel).toBe(false);
});
it('should return the correct values when the channel is archived', async () => {
await operator.handleChannel({channels: [TestHelper.fakeChannel({id: 'channel1', delete_at: 1})], prepareRecordsOnly: false});
const {getByTestId} = renderWithEverything(
<EnhancedSendDraft {...defaultProps}/>,
{database},
);
expect(getByTestId('send-draft')).toBeTruthy();
expect(getByTestId('send-draft').props.channelIsArchived).toBe(true);
});
it('should return the correct values when the channel is read only', async () => {
await operator.handleChannel({channels: [TestHelper.fakeChannel({id: 'channel1', name: General.DEFAULT_CHANNEL, delete_at: 0})], prepareRecordsOnly: false});
await operator.handleConfigs({
configs: [
{id: 'ExperimentalTownSquareIsReadOnly', value: 'true'},
],
configsToDelete: [],
prepareRecordsOnly: false,
});
const {getByTestId} = renderWithEverything(
<EnhancedSendDraft {...defaultProps}/>,
{database},
);
expect(getByTestId('send-draft')).toBeTruthy();
expect(getByTestId('send-draft').props.channelIsReadOnly).toBe(true);
});
it('should return the correct values when the channel is deactivated', async () => {
await operator.handleChannel({channels: [TestHelper.fakeChannel({id: 'channel1', name: 'user2__channel1', type: General.DM_CHANNEL, delete_at: 0})], prepareRecordsOnly: false});
await operator.handleUsers({
users: [
TestHelper.fakeUser({
id: 'user2',
delete_at: 1,
}),
],
prepareRecordsOnly: false,
});
const {getByTestId} = renderWithEverything(
<EnhancedSendDraft {...defaultProps}/>,
{database},
);
expect(getByTestId('send-draft')).toBeTruthy();
expect(getByTestId('send-draft').props.deactivatedChannel).toBe(true);
});
});

View file

@ -0,0 +1,130 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {screen} from '@testing-library/react-native';
import React from 'react';
import {General} from '@constants';
import {DRAFT_TYPE_DRAFT, DRAFT_TYPE_SCHEDULED} from '@constants/draft';
import {renderWithIntlAndTheme} from '@test/intl-test-helper';
import TestHelper from '@test/test_helper';
import DraftAndScheduledPostHeader from './draft_scheduled_post_header';
// Mock the datetime util to provide both getReadableTimestamp and toMilliseconds
jest.mock('@utils/datetime', () => ({
getReadableTimestamp: jest.fn(() => 'May 5, 2025, 10:00 AM'),
toMilliseconds: jest.fn(({minutes}) => minutes * 60 * 1000),
}));
jest.mock('@utils/user', () => ({
getUserTimezone: jest.fn(() => ({
useAutomaticTimezone: true,
automaticTimezone: 'America/New_York',
manualTimezone: '',
})),
}));
jest.mock('@utils/scheduled_post', () => ({
getErrorStringFromCode: jest.fn(() => 'Error message'),
}));
jest.mock('@components/draft_scheduled_post_header/profile_avatar', () => {
const MockProfileAvatar = () => null;
return MockProfileAvatar;
});
describe('DraftAndScheduledPostHeader', () => {
const baseProps: Parameters<typeof DraftAndScheduledPostHeader>[0] = {
channel: TestHelper.fakeChannelModel({
id: 'channel-id',
type: General.OPEN_CHANNEL,
displayName: 'Test Channel',
}),
updateAt: 1620000000000,
draftType: DRAFT_TYPE_DRAFT,
testID: 'draft_header',
isMilitaryTime: false,
currentUser: TestHelper.fakeUserModel({
id: 'user-id',
username: 'testuser',
locale: 'en',
}),
};
it('renders draft header correctly', () => {
renderWithIntlAndTheme(<DraftAndScheduledPostHeader {...baseProps}/>);
expect(screen.getByText('In:')).toBeTruthy();
expect(screen.getByText('Test Channel')).toBeTruthy();
});
it('renders DM draft header correctly', () => {
const dmProps: Parameters<typeof DraftAndScheduledPostHeader>[0] = {
...baseProps,
channel: TestHelper.fakeChannelModel({
id: 'channel-id',
type: General.DM_CHANNEL,
displayName: 'Test Channel',
}),
postReceiverUser: TestHelper.fakeUserModel({
id: 'receiver-id',
username: 'receiver',
}),
};
renderWithIntlAndTheme(<DraftAndScheduledPostHeader {...dmProps}/>);
expect(screen.getByText('To:')).toBeTruthy();
});
it('renders thread draft header correctly', () => {
const threadProps = {
...baseProps,
rootId: 'root-post-id',
};
renderWithIntlAndTheme(<DraftAndScheduledPostHeader {...threadProps}/>);
expect(screen.getByText('Thread in:')).toBeTruthy();
});
it('renders scheduled post header correctly', () => {
const scheduledProps = {
...baseProps,
draftType: DRAFT_TYPE_SCHEDULED,
postScheduledAt: 1620100000000,
};
renderWithIntlAndTheme(<DraftAndScheduledPostHeader {...scheduledProps}/>);
expect(screen.getByTestId('scheduled_post_header.scheduled_at')).toBeTruthy();
expect(screen.getByText('Send on May 5, 2025, 10:00 AM')).toBeTruthy();
});
it('renders scheduled post with error correctly', () => {
const errorProps = {
...baseProps,
draftType: DRAFT_TYPE_SCHEDULED,
postScheduledAt: 1620100000000,
scheduledPostErrorCode: 'channel_not_found',
};
renderWithIntlAndTheme(<DraftAndScheduledPostHeader {...errorProps}/>);
expect(screen.getByText('Error message')).toBeTruthy();
});
it('renders sent scheduled post correctly', () => {
const sentProps = {
...baseProps,
draftType: DRAFT_TYPE_SCHEDULED,
postScheduledAt: 1620100000000,
scheduledPostErrorCode: 'post_send_success_delete_failed',
};
renderWithIntlAndTheme(<DraftAndScheduledPostHeader {...sentProps}/>);
expect(screen.getByText('Sent')).toBeTruthy();
});
});

View file

@ -0,0 +1,99 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {Screens} from '@constants';
import {SYSTEM_IDENTIFIERS} from '@constants/database';
import DatabaseManager from '@database/manager';
import {renderWithEverything} from '@test/intl-test-helper';
import TestHelper from '@test/test_helper';
import DraftsButton from './drafts_button';
import EnhancedDraftButton from './index';
import type ServerDataOperator from '@database/operator/server_data_operator';
import type {Database} from '@nozbe/watermelondb';
jest.mock('./drafts_button', () => ({
__esModule: true,
default: jest.fn(),
}));
jest.mocked(DraftsButton).mockImplementation((props) => React.createElement('DraftsButton', {...props, testID: 'drafts-button'}));
describe('DraftsButton', () => {
const serverUrl = 'server-1';
const teamId = 'team1';
let database: Database;
let operator: ServerDataOperator;
beforeEach(async () => {
await DatabaseManager.init([serverUrl]);
const serverDatabaseAndOperator = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
database = serverDatabaseAndOperator.database;
operator = serverDatabaseAndOperator.operator;
await operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.CURRENT_TEAM_ID, value: teamId}], prepareRecordsOnly: false});
await operator.handleTeam({teams: [TestHelper.fakeTeam({id: teamId})], prepareRecordsOnly: false});
});
afterEach(async () => {
await DatabaseManager.destroyServerDatabase(serverUrl);
});
it('should correctly show isActiveTab if the last channel id Global_DRAFTS', async () => {
await operator.handleTeamChannelHistory({
teamChannelHistories: [
{channel_ids: [Screens.GLOBAL_DRAFTS], id: teamId},
],
prepareRecordsOnly: false,
});
const {getByTestId} = renderWithEverything(
<EnhancedDraftButton
shouldHighlightActive={false}
draftsCount={0}
scheduledPostCount={0}
scheduledPostHasError={false}
/>, {database});
const draftsButton = getByTestId('drafts-button');
expect(draftsButton.props.isActiveTab).toBe(true);
});
it('should correctly show isActiveTab if the last channel id is not Global_DRAFTS', async () => {
await operator.handleTeamChannelHistory({
teamChannelHistories: [
{channel_ids: ['channel1'], id: teamId},
],
prepareRecordsOnly: false,
});
const {getByTestId} = renderWithEverything(
<EnhancedDraftButton
shouldHighlightActive={false}
draftsCount={0}
scheduledPostCount={0}
scheduledPostHasError={false}
/>, {database});
const draftsButton = getByTestId('drafts-button');
expect(draftsButton.props.isActiveTab).toBe(false);
});
it('if shouldHightlightActive is true and no last channel id is present, then isActiveTab should be true', async () => {
const {getByTestId} = renderWithEverything(
<EnhancedDraftButton
shouldHighlightActive={true}
draftsCount={0}
scheduledPostCount={0}
scheduledPostHasError={false}
/>, {database});
const draftsButton = getByTestId('drafts-button');
expect(draftsButton.props.isActiveTab).toBe(true);
});
});

View file

@ -13,7 +13,11 @@ import DraftsButton from './drafts_button';
import type {WithDatabaseArgs} from '@typings/database/database';
const enhanced = withObservables(['shouldHighlightActive'], ({database, shouldHighlightActive}: {shouldHighlightActive: boolean} & WithDatabaseArgs) => {
type Props = WithDatabaseArgs & {
shouldHighlightActive: boolean;
}
const enhanced = withObservables(['shouldHighlightActive'], ({database, shouldHighlightActive}: Props) => {
const currentTeamId = observeCurrentTeamId(database);
const isActiveTab = currentTeamId.pipe(
switchMap(

View file

@ -0,0 +1,68 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {storeGlobal} from '@actions/app/global';
import {Tutorial} from '@constants';
import {SYSTEM_IDENTIFIERS} from '@constants/database';
import DatabaseManager from '@database/manager';
import {renderWithEverything, waitFor} from '@test/intl-test-helper';
import SendButton from './send_button';
import EnhancedSendButton from './index';
import type ServerDataOperator from '@database/operator/server_data_operator';
import type {Database} from '@nozbe/watermelondb';
jest.mock('./send_button', () => ({
__esModule: true,
default: jest.fn(),
}));
jest.mocked(SendButton).mockImplementation((props) => React.createElement('SendButton', {...props, testID: 'send-button'}));
describe('SendButton', () => {
const serverUrl = 'server-1';
const teamId = 'team1';
let database: Database;
let operator: ServerDataOperator;
beforeEach(async () => {
await DatabaseManager.init([serverUrl]);
const serverDatabaseAndOperator = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
database = serverDatabaseAndOperator.database;
operator = serverDatabaseAndOperator.operator;
await operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.CURRENT_TEAM_ID, value: teamId}], prepareRecordsOnly: false});
});
afterEach(async () => {
await DatabaseManager.destroyServerDatabase(serverUrl);
await storeGlobal(Tutorial.SCHEDULED_POST, null, false);
});
const defaultProps: Parameters<typeof EnhancedSendButton>[0] = {
testID: 'send-button',
disabled: false,
sendMessage: jest.fn(),
showScheduledPostOptions: jest.fn(),
scheduledPostEnabled: true,
};
it('should return false if the scheduled post tutorial is not watched', async () => {
const {getByTestId} = renderWithEverything(<EnhancedSendButton {...defaultProps}/>, {database});
const sendButton = getByTestId('send-button');
expect(sendButton.props.scheduledPostFeatureTooltipWatched).toBe(false);
});
it('should return true if the scheduled post tutorial is watched', async () => {
await storeGlobal(Tutorial.SCHEDULED_POST, 'true', false);
await waitFor(() => {
const {getByTestId} = renderWithEverything(<EnhancedSendButton {...defaultProps}/>, {database});
const sendButton = getByTestId('send-button');
expect(sendButton.props.scheduledPostFeatureTooltipWatched).toBe(true);
});
});
});

View file

@ -3,7 +3,7 @@
import {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import {SendButton} from '@components/post_draft/send_button/send_button';
import SendButton from '@components/post_draft/send_button/send_button';
import {Tutorial} from '@constants';
import {observeTutorialWatched} from '@queries/app/global';

View file

@ -5,7 +5,7 @@ import {act} from '@testing-library/react-hooks';
import React from 'react';
import {InteractionManager} from 'react-native';
import {SendButton} from '@components/post_draft/send_button/send_button';
import SendButton from '@components/post_draft/send_button/send_button';
import {fireEvent, renderWithIntl} from '@test/intl-test-helper';
describe('components/post_draft/send_button', () => {

View file

@ -51,14 +51,14 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
};
});
export function SendButton({
const SendButton: React.FC<Props> = ({
testID,
disabled,
sendMessage,
showScheduledPostOptions,
scheduledPostFeatureTooltipWatched,
scheduledPostEnabled,
}: Props) {
}: Props) => {
const theme = useTheme();
const sendButtonTestID = `${testID}.send.button` + (disabled ? '.disabled' : '');
const style = getStyleSheet(theme);
@ -116,4 +116,6 @@ export function SendButton({
</Tooltip>
</TouchableWithFeedback>
);
}
};
export default SendButton;

View file

@ -0,0 +1,287 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Database} from '@nozbe/watermelondb';
import {ActionType} from '@constants';
import DatabaseManager from '@database/manager';
import ServerDataOperator from '@database/operator/server_data_operator';
import TestHelper from '@test/test_helper';
import {
observeFirstScheduledPost,
observeScheduledPostCount,
observeScheduledPostCountForChannel,
observeScheduledPostCountForThread,
observeScheduledPostsForTeam,
queryScheduledPost,
queryScheduledPostsForTeam,
} from './scheduled_post';
describe('Scheduled Post Queries', () => {
const serverUrl = 'scheduledpost.test.com';
let database: Database;
let operator: ServerDataOperator;
beforeEach(async () => {
await DatabaseManager.init([serverUrl]);
const serverDatabaseAndOperator = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
database = serverDatabaseAndOperator.database;
operator = serverDatabaseAndOperator.operator;
});
afterEach(async () => {
await DatabaseManager.destroyServerDatabase(serverUrl);
});
describe('queryScheduledPostsForTeam', () => {
it('should return posts for specific team', async () => {
const channel = TestHelper.fakeChannel({id: 'ch1', team_id: 'team1', type: 'O'});
const post = TestHelper.fakeScheduledPost({id: 'post1', channel_id: 'ch1', message: 'Test'});
const models = (await Promise.all([
operator.handleChannel({channels: [channel], prepareRecordsOnly: true}),
operator.handleScheduledPosts({
actionType: ActionType.SCHEDULED_POSTS.CREATE_OR_UPDATED_SCHEDULED_POST,
scheduledPosts: [post],
prepareRecordsOnly: true,
}),
])).flat();
await operator.batchRecords(models, 'test');
const result = await queryScheduledPostsForTeam(database, 'team1').fetch();
expect(result).toHaveLength(1);
expect(result[0].id).toBe('post1');
});
it('should return empty for non-existent team', async () => {
const result = await queryScheduledPostsForTeam(database, 'nonexistent').fetch();
expect(result).toHaveLength(0);
});
it('should include direct channels when specified', async () => {
const dmChannel = TestHelper.fakeChannel({id: 'dm1', team_id: '', type: 'D'});
const post = TestHelper.fakeScheduledPost({id: 'post1', channel_id: 'dm1', message: 'DM'});
const models = (await Promise.all([
operator.handleChannel({channels: [dmChannel], prepareRecordsOnly: true}),
operator.handleScheduledPosts({
actionType: ActionType.SCHEDULED_POSTS.CREATE_OR_UPDATED_SCHEDULED_POST,
scheduledPosts: [post],
prepareRecordsOnly: true,
}),
])).flat();
await operator.batchRecords(models, 'test');
const result = await queryScheduledPostsForTeam(database, 'team1', true).fetch();
expect(result).toHaveLength(1);
expect(result[0].id).toBe('post1');
});
});
describe('queryScheduledPost', () => {
it('should return posts for specific channel and root', async () => {
const channel = TestHelper.fakeChannel({id: 'ch1', team_id: 'team1'});
const posts = [
TestHelper.fakeScheduledPost({id: 'post1', channel_id: 'ch1', root_id: 'root1', message: 'Reply1'}),
TestHelper.fakeScheduledPost({id: 'post2', channel_id: 'ch1', root_id: 'root1', message: 'Reply2'}),
TestHelper.fakeScheduledPost({id: 'post3', channel_id: 'ch1', root_id: 'root2', message: 'Other'}),
];
const models = (await Promise.all([
operator.handleChannel({channels: [channel], prepareRecordsOnly: true}),
operator.handleScheduledPosts({
actionType: ActionType.SCHEDULED_POSTS.CREATE_OR_UPDATED_SCHEDULED_POST,
scheduledPosts: posts,
prepareRecordsOnly: true,
}),
])).flat();
await operator.batchRecords(models, 'test');
const result = await queryScheduledPost(database, 'ch1', 'root1').fetch();
expect(result).toHaveLength(2);
expect(result.map((p) => p.id).sort()).toEqual(['post1', 'post2']);
});
});
describe('observeScheduledPostsForTeam', () => {
it('should observe team posts', (done) => {
const channel = TestHelper.fakeChannel({id: 'ch1', team_id: 'team1', type: 'O'});
const post = TestHelper.fakeScheduledPost({id: 'post1', channel_id: 'ch1', message: 'Test'});
Promise.all([
operator.handleChannel({channels: [channel], prepareRecordsOnly: false}),
operator.handleScheduledPosts({
actionType: ActionType.SCHEDULED_POSTS.CREATE_OR_UPDATED_SCHEDULED_POST,
scheduledPosts: [post],
prepareRecordsOnly: false,
}),
]).then(() => {
observeScheduledPostsForTeam(database, 'team1').subscribe((posts) => {
if (posts.length === 1 && posts[0].id === 'post1') {
done();
}
});
});
}, 1500);
});
describe('observeScheduledPostCount', () => {
it('should count team posts correctly', (done) => {
const channels = [
TestHelper.fakeChannel({id: 'ch1', team_id: 'team1', type: 'O'}),
TestHelper.fakeChannel({id: 'ch2', team_id: 'team1', type: 'P'}),
];
const posts = [
TestHelper.fakeScheduledPost({id: 'post1', channel_id: 'ch1', message: 'Test1'}),
TestHelper.fakeScheduledPost({id: 'post2', channel_id: 'ch2', message: 'Test2'}),
];
Promise.all([
operator.handleChannel({channels, prepareRecordsOnly: false}),
operator.handleScheduledPosts({
actionType: ActionType.SCHEDULED_POSTS.CREATE_OR_UPDATED_SCHEDULED_POST,
scheduledPosts: posts,
prepareRecordsOnly: false,
}),
]).then(() => {
observeScheduledPostCount(database, 'team1', false).subscribe((count) => {
if (count === 2) {
done();
}
});
});
}, 1500);
it('should include direct channels when specified', (done) => {
const channels = [
TestHelper.fakeChannel({id: 'ch1', team_id: 'team1', type: 'O'}),
TestHelper.fakeChannel({id: 'dm1', team_id: '', type: 'D'}),
];
const posts = [
TestHelper.fakeScheduledPost({id: 'post1', channel_id: 'ch1', message: 'Team'}),
TestHelper.fakeScheduledPost({id: 'post2', channel_id: 'dm1', message: 'DM'}),
];
Promise.all([
operator.handleChannel({channels, prepareRecordsOnly: false}),
operator.handleScheduledPosts({
actionType: ActionType.SCHEDULED_POSTS.CREATE_OR_UPDATED_SCHEDULED_POST,
scheduledPosts: posts,
prepareRecordsOnly: false,
}),
]).then(() => {
observeScheduledPostCount(database, 'team1', true).subscribe((count) => {
if (count === 2) {
done();
}
});
});
}, 1500);
});
describe('observeScheduledPostCountForChannel', () => {
it('should count channel posts without errors', (done) => {
const channel = TestHelper.fakeChannel({id: 'ch1', team_id: 'team1'});
const posts = [
TestHelper.fakeScheduledPost({id: 'post1', channel_id: 'ch1', error_code: '', message: 'Success1'}),
TestHelper.fakeScheduledPost({id: 'post2', channel_id: 'ch1', error_code: '', message: 'Success2'}),
TestHelper.fakeScheduledPost({id: 'post3', channel_id: 'ch1', error_code: 'failed', message: 'Error'}),
];
Promise.all([
operator.handleChannel({channels: [channel], prepareRecordsOnly: false}),
operator.handleScheduledPosts({
actionType: ActionType.SCHEDULED_POSTS.CREATE_OR_UPDATED_SCHEDULED_POST,
scheduledPosts: posts,
prepareRecordsOnly: false,
}),
]).then(() => {
observeScheduledPostCountForChannel(database, 'ch1', false).subscribe((count) => {
if (count === 2) { // Only posts without errors
done();
}
});
});
}, 1500);
it('should handle CRT filtering', (done) => {
const channel = TestHelper.fakeChannel({id: 'ch1', team_id: 'team1'});
const posts = [
TestHelper.fakeScheduledPost({id: 'post1', channel_id: 'ch1', root_id: '', error_code: '', message: 'Root1'}),
TestHelper.fakeScheduledPost({id: 'post2', channel_id: 'ch1', root_id: '', error_code: '', message: 'Root2'}),
TestHelper.fakeScheduledPost({id: 'post3', channel_id: 'ch1', root_id: 'thread1', error_code: '', message: 'Reply'}),
];
Promise.all([
operator.handleChannel({channels: [channel], prepareRecordsOnly: false}),
operator.handleScheduledPosts({
actionType: ActionType.SCHEDULED_POSTS.CREATE_OR_UPDATED_SCHEDULED_POST,
scheduledPosts: posts,
prepareRecordsOnly: false,
}),
]).then(() => {
observeScheduledPostCountForChannel(database, 'ch1', true).subscribe((count) => {
if (count === 2) { // Only root posts when CRT enabled
done();
}
});
});
}, 1500);
});
describe('observeScheduledPostCountForThread', () => {
it('should count thread replies only', (done) => {
const channel = TestHelper.fakeChannel({id: 'ch1', team_id: 'team1'});
const posts = [
TestHelper.fakeScheduledPost({id: 'post1', channel_id: 'ch1', root_id: 'thread1', error_code: '', message: 'Reply1'}),
TestHelper.fakeScheduledPost({id: 'post2', channel_id: 'ch1', root_id: 'thread1', error_code: '', message: 'Reply2'}),
TestHelper.fakeScheduledPost({id: 'post3', channel_id: 'ch1', root_id: 'thread2', error_code: '', message: 'Other'}),
TestHelper.fakeScheduledPost({id: 'post4', channel_id: 'ch1', root_id: '', error_code: '', message: 'Root'}),
];
Promise.all([
operator.handleChannel({channels: [channel], prepareRecordsOnly: false}),
operator.handleScheduledPosts({
actionType: ActionType.SCHEDULED_POSTS.CREATE_OR_UPDATED_SCHEDULED_POST,
scheduledPosts: posts,
prepareRecordsOnly: false,
}),
]).then(() => {
observeScheduledPostCountForThread(database, 'thread1').subscribe((count) => {
if (count === 2) { // Only replies to thread1
done();
}
});
});
}, 1500);
});
describe('observeFirstScheduledPost', () => {
it('should return undefined for empty array', (done) => {
observeFirstScheduledPost([]).subscribe((result) => {
if (result === undefined) {
done();
}
});
});
it('should return first post when available', (done) => {
const mockPost = TestHelper.fakeScheduledPostModel({
id: 'first_post',
observe: jest.fn().mockReturnValue({
subscribe: (callback: (result: any) => void) => {
callback({id: 'first_post', message: 'First post'});
return {unsubscribe: jest.fn()};
},
}),
});
observeFirstScheduledPost([mockPost]).subscribe((result) => {
if (result?.id === 'first_post') {
done();
}
});
});
});
});

View file

@ -13,7 +13,7 @@ import type ScheduledPostModel from '@typings/database/models/servers/scheduled_
const {SERVER: {CHANNEL, SCHEDULED_POST}} = MM_TABLES;
export const queryScheduledPostsForTeam = (database: Database, teamId: string, includeDirectChannelPosts?: boolean) => {
return database.collections.get<ScheduledPostModel>(SCHEDULED_POST).query(
return database.get<ScheduledPostModel>(SCHEDULED_POST).query(
Q.on(CHANNEL,
Q.or(
Q.where('team_id', teamId),
@ -27,7 +27,7 @@ export const queryScheduledPostsForTeam = (database: Database, teamId: string, i
};
export const queryScheduledPost = (database: Database, channelId: string, rootId = '') => {
return database.collections.get<ScheduledPostModel>(SCHEDULED_POST).query(
return database.get<ScheduledPostModel>(SCHEDULED_POST).query(
Q.and(
Q.where('channel_id', channelId),
Q.where('root_id', rootId),
@ -67,7 +67,7 @@ export const observeScheduledPostCountForChannel = (
};
export const observeScheduledPostCountForThread = (database: Database, rootId: string) => {
return database.collections.get<ScheduledPostModel>(SCHEDULED_POST).query(
return database.get<ScheduledPostModel>(SCHEDULED_POST).query(
Q.where('root_id', rootId),
).observeCount();
};

View file

@ -0,0 +1,234 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {storeGlobal} from '@actions/app/global';
import {ActionType, Tutorial, Screens} from '@constants';
import {SYSTEM_IDENTIFIERS} from '@constants/database';
import DatabaseManager from '@database/manager';
import {act, renderWithEverything, waitFor} from '@test/intl-test-helper';
import TestHelper from '@test/test_helper';
import GlobalScheduledPostList from './global_scheduled_post_list';
import EnhancedGlobalScheduledPostList from './index';
import type ServerDataOperator from '@database/operator/server_data_operator';
import type {Database} from '@nozbe/watermelondb';
import type ScheduledPostModel from '@typings/database/models/servers/scheduled_post';
jest.mock('./global_scheduled_post_list', () => ({
__esModule: true,
default: jest.fn(),
}));
jest.mocked(GlobalScheduledPostList).mockImplementation((props) => React.createElement('GlobalScheduledPostList', {...props, testID: 'global-scheduled-post-list'}));
describe('GlobalScheduledPostList', () => {
const serverUrl = 'server-1';
const teamId = 'team1';
let database: Database;
let operator: ServerDataOperator;
beforeEach(async () => {
await DatabaseManager.init([serverUrl]);
const serverDatabaseAndOperator = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
database = serverDatabaseAndOperator.database;
operator = serverDatabaseAndOperator.operator;
await operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.CURRENT_TEAM_ID, value: teamId}], prepareRecordsOnly: false});
await operator.handleTeam({teams: [TestHelper.fakeTeam({id: teamId})], prepareRecordsOnly: false});
await operator.handleConfigs({
configs: [
{id: 'Version', value: '7.6.0'},
],
configsToDelete: [],
prepareRecordsOnly: false,
});
});
afterEach(async () => {
await DatabaseManager.destroyServerDatabase(serverUrl);
});
it('should return empty array is there is no scheduled post', async () => {
const {getByTestId} = renderWithEverything(
<EnhancedGlobalScheduledPostList
location={Screens.GLOBAL_DRAFTS}
/>,
{database},
);
const globalScheduledPostList = getByTestId('global-scheduled-post-list');
expect(globalScheduledPostList.props.allScheduledPosts).toStrictEqual([]);
});
it('should show scheduled post when one exists', async () => {
const {getByTestId} = renderWithEverything(
<EnhancedGlobalScheduledPostList
location={Screens.GLOBAL_DRAFTS}
/>,
{database},
);
const globalScheduledPostList = getByTestId('global-scheduled-post-list');
expect(globalScheduledPostList.props.allScheduledPosts).toStrictEqual([]);
const channelId = 'channel1';
await operator.handleChannel({
channels: [TestHelper.fakeChannel({id: channelId, team_id: teamId})],
prepareRecordsOnly: false,
});
const scheduledPosts = [TestHelper.fakeScheduledPost({
message: 'test message',
files: [],
channel_id: channelId,
})];
await act(async () => {
await operator.handleScheduledPosts({
actionType: ActionType.SCHEDULED_POSTS.CREATE_OR_UPDATED_SCHEDULED_POST,
scheduledPosts,
includeDirectChannelPosts: false,
prepareRecordsOnly: false,
});
});
await waitFor(() => {
const updatedGlobalScheduledPostList = getByTestId('global-scheduled-post-list');
expect(updatedGlobalScheduledPostList.props.allScheduledPosts).toHaveLength(1);
expect(updatedGlobalScheduledPostList.props.allScheduledPosts[0].id).toStrictEqual(scheduledPosts[0].id);
});
});
it('should correctly handle tutorial watched state', async () => {
const {getByTestId} = renderWithEverything(
<EnhancedGlobalScheduledPostList
location={Screens.GLOBAL_DRAFTS}
/>,
{database},
);
const globalScheduledPostList = getByTestId('global-scheduled-post-list');
expect(globalScheduledPostList.props.tutorialWatched).toBe(false);
await storeGlobal(Tutorial.SCHEDULED_POSTS_LIST, 'true', false);
await waitFor(() => {
const updatedGlobalScheduledPostList = getByTestId('global-scheduled-post-list');
expect(updatedGlobalScheduledPostList.props.tutorialWatched).toBe(true);
});
});
it('should only show scheduled posts for current team', async () => {
const {getByTestId} = renderWithEverything(
<EnhancedGlobalScheduledPostList
location={Screens.GLOBAL_DRAFTS}
/>,
{database},
);
const team1ChannelId = 'team1_channel';
const team2ChannelId = 'team2_channel';
await operator.handleChannel({
channels: [
TestHelper.fakeChannel({id: team1ChannelId, team_id: teamId, type: 'O'}),
TestHelper.fakeChannel({id: team2ChannelId, team_id: 'team2', type: 'O'}),
],
prepareRecordsOnly: false,
});
const scheduledPosts = [
TestHelper.fakeScheduledPost({
id: 'post1',
message: 'Team1 message',
channel_id: team1ChannelId,
}),
TestHelper.fakeScheduledPost({
id: 'post2',
message: 'Team2 message',
channel_id: team2ChannelId,
}),
];
await act(async () => {
await operator.handleScheduledPosts({
actionType: ActionType.SCHEDULED_POSTS.CREATE_OR_UPDATED_SCHEDULED_POST,
scheduledPosts,
includeDirectChannelPosts: false,
prepareRecordsOnly: false,
});
});
await waitFor(() => {
const globalScheduledPostList = getByTestId('global-scheduled-post-list');
// Should only show the post from current team (team1)
expect(globalScheduledPostList.props.allScheduledPosts).toHaveLength(1);
expect(globalScheduledPostList.props.allScheduledPosts[0].id).toBe('post1');
});
});
it('should include DMs and GMs in scheduled posts', async () => {
const {getByTestId} = renderWithEverything(
<EnhancedGlobalScheduledPostList
location={Screens.GLOBAL_DRAFTS}
/>,
{database},
);
const teamChannelId = 'team_channel';
const dmChannelId = 'dm_channel';
const gmChannelId = 'gm_channel';
await operator.handleChannel({
channels: [
TestHelper.fakeChannel({id: teamChannelId, team_id: teamId, type: 'O'}), // Team channel
TestHelper.fakeChannel({id: dmChannelId, team_id: '', type: 'D'}), // Direct message
TestHelper.fakeChannel({id: gmChannelId, team_id: '', type: 'G'}), // Group message
],
prepareRecordsOnly: false,
});
const scheduledPosts = [
TestHelper.fakeScheduledPost({
id: 'team_post',
message: 'Team message',
channel_id: teamChannelId,
}),
TestHelper.fakeScheduledPost({
id: 'dm_post',
message: 'DM message',
channel_id: dmChannelId,
}),
TestHelper.fakeScheduledPost({
id: 'gm_post',
message: 'GM message',
channel_id: gmChannelId,
}),
];
await act(async () => {
await operator.handleScheduledPosts({
actionType: ActionType.SCHEDULED_POSTS.CREATE_OR_UPDATED_SCHEDULED_POST,
scheduledPosts,
includeDirectChannelPosts: true,
prepareRecordsOnly: false,
});
});
await waitFor(() => {
const globalScheduledPostList = getByTestId('global-scheduled-post-list');
expect(globalScheduledPostList.props.allScheduledPosts).toHaveLength(3);
const postIds = globalScheduledPostList.props.allScheduledPosts.map((post: ScheduledPostModel) => post.id);
expect(postIds).toContain('team_post');
expect(postIds).toContain('dm_post');
expect(postIds).toContain('gm_post');
});
});
});

View file

@ -0,0 +1,81 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {Screens} from '@constants';
import {SYSTEM_IDENTIFIERS} from '@constants/database';
import DatabaseManager from '@database/manager/__mocks__';
import {renderWithEverything} from '@test/intl-test-helper';
import TestHelper from '@test/test_helper';
import RescheduledDraft from './reschedule_draft';
import EnhancedRescheduledDraft from './index';
import type ServerDataOperator from '@database/operator/server_data_operator';
import type {Database} from '@nozbe/watermelondb';
jest.mock('./reschedule_draft', () => ({
__esModule: true,
default: jest.fn(),
}));
jest.mocked(RescheduledDraft).mockImplementation((props) => React.createElement('RescheduledDraft', {...props, testID: 'reschedule-draft'}));
describe('EnhancedRescheduledDraft', () => {
const serverUrl = 'server-1';
const teamId = 'team1';
let database: Database;
let operator: ServerDataOperator;
beforeEach(async () => {
await DatabaseManager.init([serverUrl]);
const serverDatabaseAndOperator = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
database = serverDatabaseAndOperator.database;
operator = serverDatabaseAndOperator.operator;
await operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.CURRENT_TEAM_ID, value: teamId}, {id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID, value: 'user1'}], prepareRecordsOnly: false});
});
afterEach(async () => {
await DatabaseManager.destroyServerDatabase(serverUrl);
});
it('should correctly return the current user timezone', async () => {
await operator.handleUsers({users: [TestHelper.fakeUser({id: 'user1', timezone: {useAutomaticTimezone: false, manualTimezone: 'America/New_York', automaticTimezone: 'America/New_York'}})], prepareRecordsOnly: false});
const {getByTestId} = renderWithEverything(
<EnhancedRescheduledDraft
componentId={Screens.RESCHEDULE_DRAFT}
closeButtonId={'close-button'}
draft={TestHelper.fakeScheduledPostModel({id: 'draft1'})}
/>,
{database},
);
expect(getByTestId('reschedule-draft')).toBeTruthy();
expect(getByTestId('reschedule-draft').props.currentUserTimezone).toStrictEqual({
useAutomaticTimezone: false,
manualTimezone: 'America/New_York',
automaticTimezone: 'America/New_York',
});
});
it('should return undefined if the current user timezone is not set', async () => {
await operator.handleUsers({users: [TestHelper.fakeUser({id: 'user1', timezone: undefined})], prepareRecordsOnly: false});
const {getByTestId} = renderWithEverything(
<EnhancedRescheduledDraft
componentId={Screens.RESCHEDULE_DRAFT}
closeButtonId={'close-button'}
draft={TestHelper.fakeScheduledPostModel({id: 'draft1'})}
/>,
{database},
);
expect(getByTestId('reschedule-draft')).toBeTruthy();
expect(getByTestId('reschedule-draft').props.currentUserTimezone).toBeUndefined();
});
});

View file

@ -0,0 +1,204 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {fireEvent} from '@testing-library/react-native';
import moment from 'moment-timezone';
import React from 'react';
import {renderWithEverything} from '@test/intl-test-helper';
import TestHelper from '@test/test_helper';
import {ScheduledPostCoreOptions} from './core_options';
import type Database from '@nozbe/watermelondb/Database';
jest.mock('@utils/time', () => ({
getFormattedTime: jest.fn().mockReturnValue('9:00 AM'),
}));
describe('ScheduledPostCoreOptions', () => {
let database: Database;
const baseProps = {
userTimezone: 'America/New_York',
isMilitaryTime: false,
onSelectOption: jest.fn(),
onCustomTimeSelected: jest.fn(),
};
beforeAll(async () => {
const server = await TestHelper.setupServerDatabase();
database = server.database;
});
beforeEach(() => {
jest.clearAllMocks();
});
it('renders correctly for Sunday', () => {
// Sunday is weekday 7 in moment
jest.spyOn(moment.prototype, 'weekday').mockReturnValue(7);
const {getByText, queryByText} = renderWithEverything(
<ScheduledPostCoreOptions {...baseProps}/>,
{database},
);
// Should show Tomorrow option
expect(getByText('Tomorrow at 9:00 AM')).toBeTruthy();
// Should not show Monday or Next Monday options
expect(queryByText('Monday at 9:00 AM')).toBeNull();
expect(queryByText('Next Monday at 9:00 AM')).toBeNull();
// Should always show Custom Time option
expect(getByText('Custom Time')).toBeTruthy();
});
it('renders correctly for Monday', () => {
// Monday is weekday 1 in moment
jest.spyOn(moment.prototype, 'weekday').mockReturnValue(1);
const {getByText, queryByText} = renderWithEverything(
<ScheduledPostCoreOptions {...baseProps}/>,
{database},
);
// Should show Tomorrow and Next Monday options
expect(getByText('Tomorrow at 9:00 AM')).toBeTruthy();
expect(getByText('Next Monday at 9:00 AM')).toBeTruthy();
// Should not show Monday option
expect(queryByText('Monday at 9:00 AM')).toBeNull();
// Should always show Custom Time option
expect(getByText('Custom Time')).toBeTruthy();
});
it('renders correctly for Friday', () => {
// Friday is weekday 5 in moment
jest.spyOn(moment.prototype, 'weekday').mockReturnValue(5);
const {getByText, queryByText} = renderWithEverything(
<ScheduledPostCoreOptions {...baseProps}/>,
{database},
);
// Should show Monday option
expect(getByText('Monday at 9:00 AM')).toBeTruthy();
// Should not show Tomorrow or Next Monday options
expect(queryByText('Tomorrow at 9:00 AM')).toBeNull();
expect(queryByText('Next Monday at 9:00 AM')).toBeNull();
// Should always show Custom Time option
expect(getByText('Custom Time')).toBeTruthy();
});
it('renders correctly for Saturday', () => {
// Saturday is weekday 6 in moment
jest.spyOn(moment.prototype, 'weekday').mockReturnValue(6);
const {getByText, queryByText} = renderWithEverything(
<ScheduledPostCoreOptions {...baseProps}/>,
{database},
);
// Should show Monday option
expect(getByText('Monday at 9:00 AM')).toBeTruthy();
// Should not show Tomorrow or Next Monday options
expect(queryByText('Tomorrow at 9:00 AM')).toBeNull();
expect(queryByText('Next Monday at 9:00 AM')).toBeNull();
// Should always show Custom Time option
expect(getByText('Custom Time')).toBeTruthy();
});
it('calls onSelectOption with correct timestamp when Tomorrow is selected', () => {
const mockDate = moment.tz('2023-05-15 12:00', 'America/New_York');
jest.spyOn(moment, 'tz').mockReturnValue(mockDate);
jest.spyOn(moment.prototype, 'weekday').mockReturnValue(1);
jest.spyOn(moment.prototype, 'clone').mockReturnValue(mockDate);
jest.spyOn(moment.prototype, 'valueOf').mockReturnValue(1684242000000);
const {getByText} = renderWithEverything(
<ScheduledPostCoreOptions {...baseProps}/>,
{database},
);
fireEvent.press(getByText('Tomorrow at 9:00 AM'));
expect(baseProps.onSelectOption).toHaveBeenCalledWith('1684242000000');
expect(baseProps.onCustomTimeSelected).toHaveBeenCalledWith(false);
});
it('calls onSelectOption with correct timestamp when Monday is selected', () => {
const mockDate = moment.tz('2023-05-11 12:00', 'America/New_York');
jest.spyOn(moment, 'tz').mockReturnValue(mockDate);
jest.spyOn(moment.prototype, 'weekday').mockReturnValue(4);
const {getByText} = renderWithEverything(
<ScheduledPostCoreOptions {...baseProps}/>,
{database},
);
fireEvent.press(getByText('Monday at 9:00 AM'));
const expectedTime = mockDate.clone().isoWeekday(1).add(1, 'week').startOf('day').hour(9).minute(0);
expect(baseProps.onSelectOption).toHaveBeenCalledWith(expectedTime.valueOf().toString());
expect(baseProps.onCustomTimeSelected).toHaveBeenCalledWith(false);
});
it('calls onSelectOption with correct timestamp when Next Monday is selected', () => {
const mockDate = moment.tz('2023-05-15 12:00', 'America/New_York');
jest.spyOn(moment, 'tz').mockReturnValue(mockDate);
jest.spyOn(moment.prototype, 'weekday').mockReturnValue(1);
const {getByText} = renderWithEverything(
<ScheduledPostCoreOptions {...baseProps}/>,
{database},
);
fireEvent.press(getByText('Next Monday at 9:00 AM'));
const expectedTime = mockDate.clone().isoWeekday(1).add(1, 'week').startOf('day').hour(9).minute(0);
expect(baseProps.onSelectOption).toHaveBeenCalledWith(expectedTime.valueOf().toString());
expect(baseProps.onCustomTimeSelected).toHaveBeenCalledWith(false);
});
it('shows DateTimeSelector when Custom Time is selected', () => {
const {getByText, queryByTestId} = renderWithEverything(
<ScheduledPostCoreOptions {...baseProps}/>,
{database},
);
// Initially, DateTimeSelector should not be visible
expect(queryByTestId('custom_date_time_picker')).toBeNull();
// Select Custom Time option
fireEvent.press(getByText('Custom Time'));
// DateTimeSelector should now be visible
expect(queryByTestId('custom_date_time_picker')).not.toBeNull();
// Should call onCustomTimeSelected with true
expect(baseProps.onCustomTimeSelected).toHaveBeenCalledWith(true);
});
it('calls handleCustomTimeChange when a custom time is selected', () => {
// This test would need to mock the DateTimeSelector component's behavior
// Since we can't directly test the internal callback without exposing it,
// we'll verify that the component is rendered and the callback is set up
const {getByText} = renderWithEverything(
<ScheduledPostCoreOptions {...baseProps}/>,
{database},
);
// Select Custom Time option
fireEvent.press(getByText('Custom Time'));
// Verify onCustomTimeSelected was called
expect(baseProps.onCustomTimeSelected).toHaveBeenCalledWith(true);
});
});

View file

@ -0,0 +1,97 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {Preferences} from '@constants';
import {SYSTEM_IDENTIFIERS} from '@constants/database';
import DatabaseManager from '@database/manager/__mocks__';
import {renderWithEverything} from '@test/intl-test-helper';
import TestHelper from '@test/test_helper';
import {getTimezone} from '@utils/user';
import {ScheduledPostCoreOptions} from './core_options';
import EnhancedScheduledPostCoreOptions from './index';
import type ServerDataOperator from '@database/operator/server_data_operator';
import type {Database} from '@nozbe/watermelondb';
jest.mock('./core_options', () => ({
__esModule: true,
ScheduledPostCoreOptions: jest.fn(),
}));
jest.mocked(ScheduledPostCoreOptions).mockImplementation((props) => React.createElement('ScheduledPostCoreOptions', {...props, testID: 'scheduled-post-core-options'}));
describe('EnhancedScheduledPostCoreOptions', () => {
const serverUrl = 'server-1';
const teamId = 'team1';
let database: Database;
let operator: ServerDataOperator;
beforeEach(async () => {
await DatabaseManager.init([serverUrl]);
const serverDatabaseAndOperator = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
database = serverDatabaseAndOperator.database;
operator = serverDatabaseAndOperator.operator;
await operator.handleUsers({users: [TestHelper.fakeUser({id: 'user1', timezone: {useAutomaticTimezone: false, manualTimezone: 'America/New_York', automaticTimezone: 'America/New_York'}})], prepareRecordsOnly: false});
await operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.CURRENT_TEAM_ID, value: teamId}, {id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID, value: 'user1'}], prepareRecordsOnly: false});
});
afterEach(async () => {
await DatabaseManager.destroyServerDatabase(serverUrl);
});
it('should correctly return the isMilitaryTime preference', async () => {
await operator.handlePreferences({
preferences: [{
category: Preferences.CATEGORIES.DISPLAY_SETTINGS,
name: 'use_military_time',
user_id: 'user1',
value: 'true',
}],
prepareRecordsOnly: false,
});
const {getByTestId} = renderWithEverything(
<EnhancedScheduledPostCoreOptions
userTimezone={getTimezone({
useAutomaticTimezone: false,
manualTimezone: 'America/New_York',
automaticTimezone: 'America/New_York',
})}
onSelectOption={jest.fn()}
onCustomTimeSelected={jest.fn()}
/>,
{database},
);
expect(getByTestId('scheduled-post-core-options')).toBeTruthy();
expect(getByTestId('scheduled-post-core-options').props.isMilitaryTime).toBe(true);
});
it('should return false if the isMilitaryTime preference is not set', async () => {
await operator.handlePreferences({
preferences: [],
prepareRecordsOnly: false,
});
const {getByTestId} = renderWithEverything(
<EnhancedScheduledPostCoreOptions
userTimezone={getTimezone({
useAutomaticTimezone: false,
manualTimezone: 'America/New_York',
automaticTimezone: 'America/New_York',
})}
onSelectOption={jest.fn()}
onCustomTimeSelected={jest.fn()}
/>,
{database},
);
expect(getByTestId('scheduled-post-core-options')).toBeTruthy();
expect(getByTestId('scheduled-post-core-options').props.isMilitaryTime).toBe(false);
});
});

View file

@ -0,0 +1,78 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {SYSTEM_IDENTIFIERS} from '@constants/database';
import DatabaseManager from '@database/manager/__mocks__';
import {ScheduledPostOptions} from '@screens/scheduled_post_options/scheduled_post_picker';
import {renderWithEverything} from '@test/intl-test-helper';
import TestHelper from '@test/test_helper';
import EnhancedScheduledPostPicker from './index';
import type ServerDataOperator from '@database/operator/server_data_operator';
import type {Database} from '@nozbe/watermelondb';
jest.mock('./scheduled_post_picker', () => ({
__esModule: true,
ScheduledPostOptions: jest.fn(),
}));
jest.mocked(ScheduledPostOptions).mockImplementation((props) => React.createElement('ScheduledPostOptions', {...props, testID: 'scheduled-post-options'}));
describe('EnhancedRescheduledDraft', () => {
const serverUrl = 'server-1';
const teamId = 'team1';
let database: Database;
let operator: ServerDataOperator;
beforeEach(async () => {
await DatabaseManager.init([serverUrl]);
const serverDatabaseAndOperator = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
database = serverDatabaseAndOperator.database;
operator = serverDatabaseAndOperator.operator;
await operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.CURRENT_TEAM_ID, value: teamId}, {id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID, value: 'user1'}], prepareRecordsOnly: false});
});
afterEach(async () => {
await DatabaseManager.destroyServerDatabase(serverUrl);
});
it('should correctly return the current user timezone', async () => {
await operator.handleUsers({users: [TestHelper.fakeUser({id: 'user1', timezone: {useAutomaticTimezone: false, manualTimezone: 'America/New_York', automaticTimezone: 'America/New_York'}})], prepareRecordsOnly: false});
const {getByTestId} = renderWithEverything(
<EnhancedScheduledPostPicker
onSchedule={jest.fn()}
/>,
{database},
);
expect(getByTestId('scheduled-post-options')).toBeTruthy();
expect(getByTestId('scheduled-post-options').props.currentUserTimezone).toStrictEqual({
useAutomaticTimezone: false,
manualTimezone: 'America/New_York',
automaticTimezone: 'America/New_York',
});
});
it('should return undefined if the current user timezone is not set', async () => {
await operator.handleUsers({users: [TestHelper.fakeUser({id: 'user1', timezone: undefined})], prepareRecordsOnly: false});
const {getByTestId} = renderWithEverything(
<EnhancedScheduledPostPicker
onSchedule={jest.fn()}
/>,
{database},
);
expect(getByTestId('scheduled-post-options')).toBeTruthy();
expect(getByTestId('scheduled-post-options').props.currentUserTimezone).toBeUndefined();
});
});

View file

@ -37,6 +37,7 @@ import type PostModel from '@typings/database/models/servers/post';
import type PostsInChannelModel from '@typings/database/models/servers/posts_in_channel';
import type PreferenceModel from '@typings/database/models/servers/preference';
import type RoleModel from '@typings/database/models/servers/role';
import type ScheduledPostModel from '@typings/database/models/servers/scheduled_post';
import type TeamModel from '@typings/database/models/servers/team';
import type ThreadModel from '@typings/database/models/servers/thread';
import type UserModel from '@typings/database/models/servers/user';
@ -165,7 +166,7 @@ class TestHelperSingleton {
if (c === 'x') {
v = r;
} else {
// eslint-disable-next-line no-mixed-operators
v = (r & 0x3) | 0x8;
}
@ -855,6 +856,43 @@ class TestHelperSingleton {
};
};
fakeScheduledPostModel = (overwrite?: Partial<ScheduledPostModel>): ScheduledPostModel => {
return {
...this.fakeModel(),
id: this.generateId(),
createAt: 0,
updateAt: 0,
channelId: this.generateId(),
message: '',
rootId: '',
files: [],
metadata: {},
scheduledAt: 0,
processedAt: 0,
errorCode: '',
toApi: jest.fn(),
...overwrite,
};
};
fakeScheduledPost = (overwrite?: Partial<ScheduledPost>): ScheduledPost => {
return {
id: this.generateId(),
channel_id: this.generateId(),
message: '',
root_id: '',
files: [],
metadata: {},
create_at: Date.now(),
update_at: Date.now(),
scheduled_at: Date.now(),
processed_at: 0,
error_code: '',
user_id: this.generateId(),
...overwrite,
};
};
fakePreferenceModel = (overwrite?: Partial<PreferenceModel>): PreferenceModel => {
return {
...this.fakeModel(),