* Initial setup for new keyboard
* fix the offset calculation onMove by adding isKeyboardFullyOpened
* Done with the keyboard handling implementation
* Handled keyboard focus and blured state using context
* Added default height for input container
* Android support
* Tablet state handling
* Fix for refreshing offset in list
* Created a default context for mention post list
* Fix linter errors
* Fix tests
* Minor
* Fix the height issue for tablet view
* Review comments
* Dependency fix
* Reveiw comment
* keyboard animation only enabled with screen on top navigation
* added tests
* Added extra keyboard component for emoji picker (#9328)
* Added extra keyboard component
* handled swipe geature for extra keyboard
* scroll to bottom visible on emoji picker
* Check for stale event for keyboard geature area
* fix keyboard stale event for mid-gesture change swipe direction
* Closing emoji picker when message priority is opened
* changing to thread view closes emoji picker
* Close emoij picker and keyboard when attachment icons are clicked
* handle android emoji picker behaviour
* Remove emoji picker code
* fix tests
* Address reviev comments
* Test fixes
* usePreventDoubleTab for race condition and corrected comment on isInputAccessoryViewMode flag
* File attachments option in bottom sheet quick action (#9331)
* Added extra keyboard component
* handled swipe geature for extra keyboard
* File attachments option in bottom sheet quick action
* Updated tests
* i18n
* fix test
* Added tests
* Review comment
* fix tests
* Integrated Emoji picker to Extra keyboard component. (#9339)
* Added extra keyboard component
* handled swipe geature for extra keyboard
* scroll to bottom visible on emoji picker
* Fix the post input container height change issue
* Added emoji picker with search functionality
* keyboard height not recorded fallback to default height, set search to false closing emoji picker
* fix search funcitonality in android
* scroll to end bottom dismissed with pressed
* Fixed the scroll issue for android
* fix the flickering post input issue
* Wired up the emoji picker
* Added keyboard and spaceback in emoji picker
* intl extract
* initial cursor position and simple calculation review comment
* separated grapheme to utils file
* Review comments
* nitpick
* Fix the bottom safe area and navigation stack restore fix
* Fix test
* disabled emoji picker in edit post screen
* change the name of the variable to name it clarier
* fix the tab gap issue in andriod
* disabled the emoji skin tone change on andriod
* fix the input container jump issue
* fix the failing test
* UX review
* added white space after the attachment icon
* When @ or / is press open keyboard on andriod also fix the scroll issue when keyboard is open
* back bottom closes emoji picker and opening keyboard jump fix
* reverted code
* fixed the flickering issue of input and scroll position fix
* Fix the gap issue in thread
* fix the warning reaminated issue when opening a channel
* Fix the extra space issue between input and emoij picker
* Minor fix for extra space between input and emoji picker
* Fix thread view bottom safe area and gap between keyboard and searched emojis
* Fix the keyboard issue in tablet
* Fix the warning issue react-native-keyboard-controller
* Fix tests
* Fix the tablet search hight issue
* Replaced the ExtraKeyboardProvider with KeyboardProvider from rnkc for permalink
* Bottom sheet jump issue resolved
* Search do not close after selecting emoji in search list
* Added the bottom inset height in search emoji for android
* fix buid issue
* Fix the cancel state to emoji picker
* use portals for autocomplete
* fix permalink extra space in post list
* Portal only for android
---------
(cherry picked from commit e39b27be7e)
Co-authored-by: Rajat Dabade <rajatdabade1997@gmail.com>
Co-authored-by: Rahim Rahman <rahim.rahman@mattermost.com>
Co-authored-by: Harshil Sharma <harshilsharma63@gmail.com>
Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
326 lines
12 KiB
TypeScript
326 lines
12 KiB
TypeScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
import {act, waitFor} from '@testing-library/react-native';
|
|
import React from 'react';
|
|
import {Alert} from 'react-native';
|
|
|
|
import DraftEditPostUploadManager from '@managers/draft_upload_manager';
|
|
import * as Navigation from '@screens/navigation';
|
|
import {fireEvent, renderWithEverything} from '@test/intl-test-helper';
|
|
import TestHelper from '@test/test_helper';
|
|
import PickerUtil from '@utils/file/file_picker';
|
|
|
|
import EditPost from './edit_post';
|
|
|
|
import type {Database} from '@nozbe/watermelondb';
|
|
import type PostModel from '@typings/database/models/servers/post';
|
|
import type {IntlShape} from 'react-intl';
|
|
|
|
jest.mock('@utils/file/file_picker');
|
|
jest.mock('@managers/draft_upload_manager', () => ({
|
|
prepareUpload: jest.fn(),
|
|
isUploading: jest.fn(() => false),
|
|
registerProgressHandler: jest.fn(() => jest.fn()),
|
|
registerErrorHandler: jest.fn(() => jest.fn()),
|
|
cancel: jest.fn(),
|
|
}));
|
|
jest.mock('@screens/navigation', () => ({
|
|
openAttachmentOptions: jest.fn(),
|
|
buildNavigationButton: jest.fn((id: string, testID: string) => ({id, testID})),
|
|
dismissBottomSheet: jest.fn(() => Promise.resolve()),
|
|
dismissModal: jest.fn(),
|
|
setButtons: jest.fn(),
|
|
}));
|
|
|
|
const TEST_CONFIG = {
|
|
serverUrl: 'baseHandler.test.com',
|
|
maxPostSize: 1000,
|
|
maxFileCount: 10,
|
|
maxFileSize: 1000,
|
|
} as const;
|
|
|
|
const TEST_FILES = {
|
|
existingFile1: {
|
|
id: 'file-1',
|
|
name: 'test-1',
|
|
extension: 'png',
|
|
has_preview_image: true,
|
|
height: 100,
|
|
mime_type: 'test',
|
|
size: 100,
|
|
user_id: '1',
|
|
width: 100,
|
|
},
|
|
existingFile2: {
|
|
id: 'file-2',
|
|
name: 'test-2',
|
|
extension: 'pdf',
|
|
has_preview_image: false,
|
|
height: 100,
|
|
mime_type: 'test',
|
|
size: 100,
|
|
user_id: '1',
|
|
width: 100,
|
|
},
|
|
newFile: {
|
|
clientId: 'file-3',
|
|
id: 'file-3',
|
|
name: 'test-3',
|
|
extension: 'txt',
|
|
mime_type: 'text/plain',
|
|
size: 999,
|
|
},
|
|
smallFile: {
|
|
name: 'test.txt',
|
|
mime_type: 'text/plain',
|
|
},
|
|
largeFile: {
|
|
name: 'test.txt',
|
|
mime_type: 'text/plain',
|
|
size: 1001,
|
|
},
|
|
} as const satisfies Record<string, Partial<ExtractedFileInfo>>;
|
|
|
|
const ERROR_MESSAGES = {
|
|
uploadsDisabled: 'File uploads from mobile are disabled.',
|
|
maxFilesReached: 'Uploads limited to 1 files maximum.',
|
|
fileTooLarge: 'Files must be less than 1000 B',
|
|
confirmDelete: 'Delete attachment',
|
|
confirmDeleteMessage: 'Are you sure you want to remove test-3?',
|
|
} as const;
|
|
|
|
describe('Edit Post', () => {
|
|
let database: Database;
|
|
|
|
const baseProps: Parameters<typeof EditPost>[0] = {
|
|
componentId: 'EditPost',
|
|
closeButtonId: 'edit-post',
|
|
post: {
|
|
id: '1',
|
|
channelId: '1',
|
|
message: 'test',
|
|
messageSource: 'test',
|
|
userId: '1',
|
|
rootId: '1',
|
|
metadata: {},
|
|
} as PostModel,
|
|
maxPostSize: TEST_CONFIG.maxPostSize,
|
|
|
|
canDelete: true,
|
|
files: [TEST_FILES.existingFile1, TEST_FILES.existingFile2],
|
|
maxFileCount: TEST_CONFIG.maxFileCount,
|
|
maxFileSize: TEST_CONFIG.maxFileSize,
|
|
canUploadFiles: true,
|
|
};
|
|
|
|
const setupPickerMock = (file: Partial<ExtractedFileInfo>) => {
|
|
jest.mocked(PickerUtil).mockImplementation((intl, onUploadFiles) => ({
|
|
attachFileFromFiles: jest.fn(() => {
|
|
onUploadFiles?.([file as ExtractedFileInfo]);
|
|
return Promise.resolve({error: undefined});
|
|
}),
|
|
attachFileFromPhotoGallery: jest.fn(() => {
|
|
onUploadFiles?.([file as ExtractedFileInfo]);
|
|
return Promise.resolve({error: undefined});
|
|
}),
|
|
attachFileFromCamera: jest.fn(() => {
|
|
onUploadFiles?.([file as ExtractedFileInfo]);
|
|
return Promise.resolve({error: undefined});
|
|
}),
|
|
}) as unknown as PickerUtil);
|
|
};
|
|
|
|
const renderEditPost = (props = baseProps) => {
|
|
return renderWithEverything(<EditPost {...props}/>, {
|
|
database,
|
|
serverUrl: TEST_CONFIG.serverUrl,
|
|
});
|
|
};
|
|
|
|
const triggerFileUpload = async (screen: ReturnType<typeof renderEditPost>) => {
|
|
let onUploadFilesCallback: ((files: ExtractedFileInfo[]) => void) | undefined;
|
|
jest.mocked(Navigation.openAttachmentOptions).mockImplementation((intl, theme, props) => {
|
|
onUploadFilesCallback = props?.onUploadFiles;
|
|
return undefined;
|
|
});
|
|
|
|
await act(async () => {
|
|
fireEvent.press(screen.getByTestId('edit_post.quick_actions.attachment_action'));
|
|
});
|
|
|
|
if (onUploadFilesCallback) {
|
|
await act(async () => {
|
|
const mockIntl = {formatMessage: jest.fn()} as unknown as IntlShape;
|
|
const mockPicker = new PickerUtil(mockIntl, onUploadFilesCallback as (files: ExtractedFileInfo[]) => void);
|
|
await mockPicker.attachFileFromFiles(undefined, true);
|
|
});
|
|
}
|
|
};
|
|
|
|
const triggerFileRemoval = async (screen: ReturnType<typeof renderEditPost>, fileId: string) => {
|
|
await act(async () => {
|
|
fireEvent.press(screen.getByTestId(`remove-button-${fileId}`));
|
|
});
|
|
};
|
|
|
|
beforeAll(async () => {
|
|
const server = await TestHelper.setupServerDatabase(TEST_CONFIG.serverUrl);
|
|
database = server.database;
|
|
const operator = server.operator;
|
|
|
|
operator.handleConfigs({
|
|
configs: [
|
|
{id: 'Version', value: '10.5.0'},
|
|
{id: 'EnableFileAttachments', value: 'true'},
|
|
{id: 'EnableMobileFileUpload', value: 'true'},
|
|
],
|
|
configsToDelete: [],
|
|
prepareRecordsOnly: false,
|
|
});
|
|
});
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
describe('Rendering', () => {
|
|
it('should render existing attachments in edit mode', () => {
|
|
const screen = renderEditPost();
|
|
|
|
expect(screen.getByTestId('uploads')).toBeVisible();
|
|
expect(screen.getByTestId('file-1')).toBeVisible();
|
|
expect(screen.getByTestId('file-2')).toBeVisible();
|
|
});
|
|
});
|
|
|
|
describe('File Upload Validation', () => {
|
|
it('should show error when file uploads are disabled', async () => {
|
|
setupPickerMock(TEST_FILES.smallFile);
|
|
const props = {...baseProps, canUploadFiles: false};
|
|
const screen = renderEditPost(props);
|
|
await triggerFileUpload(screen);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText(ERROR_MESSAGES.uploadsDisabled)).toBeVisible();
|
|
});
|
|
});
|
|
|
|
it('should show error when maximum file count is reached', async () => {
|
|
setupPickerMock(TEST_FILES.smallFile);
|
|
const props = {...baseProps, maxFileCount: 1};
|
|
const screen = renderEditPost(props);
|
|
await triggerFileUpload(screen);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText(ERROR_MESSAGES.maxFilesReached)).toBeVisible();
|
|
});
|
|
});
|
|
|
|
it('should show error when file size exceeds limit', async () => {
|
|
setupPickerMock(TEST_FILES.largeFile);
|
|
const props = {...baseProps, maxFileSize: 1000};
|
|
const screen = renderEditPost(props);
|
|
await triggerFileUpload(screen);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText(ERROR_MESSAGES.fileTooLarge)).toBeVisible();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('File Upload Integration', () => {
|
|
it('should integrate with DraftEditPostUploadManager for successful uploads', async () => {
|
|
setupPickerMock(TEST_FILES.newFile);
|
|
const screen = renderEditPost();
|
|
await triggerFileUpload(screen);
|
|
|
|
await waitFor(() => {
|
|
expect(DraftEditPostUploadManager.prepareUpload).toHaveBeenCalledWith(
|
|
TEST_CONFIG.serverUrl,
|
|
TEST_FILES.newFile,
|
|
baseProps.post.channelId,
|
|
baseProps.post.rootId,
|
|
0,
|
|
true,
|
|
expect.any(Function),
|
|
);
|
|
expect(DraftEditPostUploadManager.registerErrorHandler).toHaveBeenCalledWith(
|
|
TEST_FILES.newFile.clientId,
|
|
expect.any(Function),
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('File Removal', () => {
|
|
beforeEach(() => {
|
|
jest.mocked(DraftEditPostUploadManager.isUploading).mockReturnValue(true);
|
|
setupPickerMock(TEST_FILES.newFile);
|
|
});
|
|
|
|
it('should remove newly uploaded files without confirmation', async () => {
|
|
const screen = renderEditPost();
|
|
await triggerFileUpload(screen);
|
|
await triggerFileRemoval(screen, TEST_FILES.newFile.id);
|
|
expect(Alert.alert).not.toHaveBeenCalled();
|
|
await waitFor(() => {
|
|
expect(DraftEditPostUploadManager.cancel).toHaveBeenCalledWith(TEST_FILES.newFile.clientId);
|
|
});
|
|
});
|
|
|
|
it('should show confirmation dialog for existing files', async () => {
|
|
const screen = renderEditPost();
|
|
await triggerFileRemoval(screen, TEST_FILES.existingFile1.id);
|
|
await waitFor(() => {
|
|
expect(Alert.alert).toHaveBeenCalledWith(
|
|
ERROR_MESSAGES.confirmDelete,
|
|
'Are you sure you want to remove test-1?',
|
|
expect.any(Array),
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Error Display', () => {
|
|
it('should display upload error in PostError component when file upload fails', async () => {
|
|
setupPickerMock(TEST_FILES.largeFile);
|
|
const props = {...baseProps, maxFileSize: 1000};
|
|
const screen = renderEditPost(props);
|
|
|
|
// Trigger file upload that will cause size error
|
|
await triggerFileUpload(screen);
|
|
|
|
// Verify that the PostError component is displayed with the error
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('edit_post.message.input.error')).toBeVisible();
|
|
expect(screen.getByText(ERROR_MESSAGES.fileTooLarge)).toBeVisible();
|
|
});
|
|
});
|
|
|
|
it('should display upload error with divider when error is present', async () => {
|
|
setupPickerMock(TEST_FILES.smallFile);
|
|
const props = {...baseProps, canUploadFiles: false};
|
|
const screen = renderEditPost(props);
|
|
|
|
// Trigger file upload that will cause upload disabled error
|
|
await triggerFileUpload(screen);
|
|
|
|
// Verify that the error is displayed
|
|
await waitFor(() => {
|
|
expect(screen.getByText(ERROR_MESSAGES.uploadsDisabled)).toBeVisible();
|
|
});
|
|
|
|
// Verify that the PostError component has the hasError styling applied
|
|
const editPostInput = screen.getByTestId('edit_post.message.input');
|
|
expect(editPostInput).toBeVisible();
|
|
});
|
|
|
|
it('should not display PostError component when no upload error exists', () => {
|
|
const screen = renderEditPost();
|
|
|
|
// Verify that no error message is displayed
|
|
expect(screen.queryByTestId('edit_post.message.input.error')).toBeNull();
|
|
});
|
|
});
|
|
});
|