* feat: ai rewrite * feat: allow crating content apart from editing it * feat: feature parity with webapp * feat: feature parity with webapp * chore: fixed padding * map ux to webapp * refactored ai rewrite logic to separate package * chore: tests and lint * Update app/products/ai/rewrite/screens/options/options.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * rewrite post editor animation * i18n * ui feedback, centered icon and less top padding * chore: lint * Consolidate @ai product into @agents Move all rewrite functionality from app/products/ai/ into app/products/agents/ to unify under a single namespace. - Add rewrite types, store, hooks, components, and screens to agents - Merge AI client methods into agents client - Update screen constants (AI_* -> AGENTS_*) - Update all consumer imports - Remove AI state from EphemeralStore - Remove @ai path alias from config - Delete app/products/ai/ directory * refactor: load screens from product package * refactor: move detection logic to the agents pacakge * refactor: remove "backwards compatibility" * refactor: move hooks to proper package * refactor: styles * refactor: remove unneedd position attribute * refactor: remove unneeded cancel animation calls * refactor: "backwards compat" * refactor: optimize renderContent with useCallback for performance * refactor: rename variable to avoid confusion * refactor: update handleRewrite to use async/await for better error handling * refactor: simplify handleRewrite by always dismissing keyboard * refactor: use hook, always all keyboard.dismiss * chore: enhance AgentSelector component with FlatList support * feat: add rewriteMessage function for AI message rewriting and integrate it into useRewrite hook * refactor: simplify message length calculation in useHandleSendMessage hook * refactor: consolidate agent screen constants and integrate with existing screens * fix: update dependency array in useMemo for isUnrevealedPost to include post expiration metadata * refactor: remove unused variable to clean up Typing component * refactor: simplify logic for atDisabled and slashDisabled flags in QuickActions component * feat: add AIRewriteAction component for AI message rewriting functionality * revert: thread.ts changes manually * refactor: integrate useSafeAreaInsets * refactor: cleanup unused methods * chore: add comment to clarify casting * refactor: use_agents * chore: lint and test * fix: trim --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
279 lines
9.4 KiB
TypeScript
279 lines
9.4 KiB
TypeScript
// 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'),
|
|
withServerUrl: (Component: React.ComponentType<any>) => (props: any) => (
|
|
<Component
|
|
{...props}
|
|
serverUrl={'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);
|
|
});
|
|
|
|
it('renders BoR indicator for BoR scheduled post', () => {
|
|
const props = {
|
|
...baseProps,
|
|
draftType: DRAFT_TYPE_SCHEDULED,
|
|
post: TestHelper.fakeScheduledPostModel({
|
|
id: 'post_id_1',
|
|
rootId: '',
|
|
updateAt: 1234567890,
|
|
metadata: {},
|
|
files: [] as FileInfo[],
|
|
scheduledAt: 1234567890,
|
|
errorCode: '',
|
|
type: 'burn_on_read',
|
|
}),
|
|
borUserTimeLimit: 100000,
|
|
};
|
|
renderWithIntlAndTheme(<DraftAndScheduledPost {...props}/>);
|
|
expect(screen.getByTestId('post_id_1_bor_label')).toBeVisible();
|
|
});
|
|
|
|
it('renders BoR indicator with post priority if set', () => {
|
|
const props = {
|
|
...baseProps,
|
|
draftType: DRAFT_TYPE_SCHEDULED,
|
|
isPostPriorityEnabled: true,
|
|
post: TestHelper.fakeScheduledPostModel({
|
|
id: 'post_id_1',
|
|
rootId: '',
|
|
updateAt: 1234567890,
|
|
metadata: {
|
|
priority: {
|
|
priority: 'urgent',
|
|
requested_ack: true,
|
|
persistent_notifications: true,
|
|
},
|
|
},
|
|
files: [] as FileInfo[],
|
|
scheduledAt: 1234567890,
|
|
errorCode: '',
|
|
type: 'burn_on_read',
|
|
}),
|
|
borUserTimeLimit: 100000,
|
|
};
|
|
renderWithIntlAndTheme(<DraftAndScheduledPost {...props}/>);
|
|
|
|
expect(screen.getByTestId('post_id_1_bor_label')).toBeVisible();
|
|
|
|
expect(screen.getByTestId('draft_post.priority')).toBeVisible();
|
|
expect(screen.getByTestId('urgent_post_priority_label')).toBeVisible();
|
|
expect(screen.getByTestId('drafts.requested_ack.icon')).toBeVisible();
|
|
expect(screen.getByTestId('drafts.persistent_notifications.icon')).toBeVisible();
|
|
});
|
|
});
|