11import React from 'react' ;
2- import { SearchController } from 'stream-chat' ;
2+ import type {
3+ Channel as ChannelType ,
4+ CommandResponse ,
5+ CooldownTimerState ,
6+ LinkPreview ,
7+ LinkPreviewsManagerState ,
8+ LocalAttachment ,
9+ LocalMessage ,
10+ SendFileAPIResponse ,
11+ StreamChat ,
12+ UserResponse ,
13+ } from 'stream-chat' ;
14+ import { LinkPreviewStatus , SearchController } from 'stream-chat' ;
315import {
416 act ,
517 cleanup ,
@@ -13,16 +25,18 @@ import { axe } from '../../../../axe-helper';
1325import { nanoid } from 'nanoid' ;
1426import { fromPartial } from '@total-typescript/shoehorn' ;
1527
28+ import type { MessageComposerProps } from '../MessageComposer' ;
1629import { MessageComposer } from '../MessageComposer' ;
1730import { Channel } from '../../Channel/Channel' ;
31+ import type { MessageActionsProps } from '../../MessageActions' ;
1832import { MessageActions } from '../../MessageActions' ;
1933
20- import { DialogManagerProvider , MessageProvider , WithComponents } from '../../../context' ;
2134import type {
2235 ChatContextValue ,
2336 ComponentContextValue ,
2437 MessageContextValue ,
2538} from '../../../context' ;
39+ import { DialogManagerProvider , MessageProvider , WithComponents } from '../../../context' ;
2640import { ChatProvider } from '../../../context/ChatContext' ;
2741import {
2842 dispatchMessageDeletedEvent ,
@@ -36,21 +50,7 @@ import {
3650 initClientWithChannels ,
3751} from '../../../mock-builders' ;
3852import { QuotedMessagePreview } from '../QuotedMessagePreview' ;
39- import type {
40- Attachment ,
41- Channel as ChannelType ,
42- CommandResponse ,
43- CooldownTimerState ,
44- LinkPreviewsManagerState ,
45- LocalAttachment ,
46- LocalMessage ,
47- SendFileAPIResponse ,
48- StreamChat ,
49- UserResponse ,
50- } from 'stream-chat' ;
5153import type { ChannelProps } from '../../Channel' ;
52- import type { MessageComposerProps } from '../MessageComposer' ;
53- import type { MessageActionsProps } from '../../MessageActions' ;
5454import type { GenerateChannelOptions } from '../../../mock-builders/generator/channel' ;
5555
5656vi . mock ( '../../ChatView' , async ( importOriginal ) => {
@@ -102,24 +102,25 @@ const mockedChannelData = generateChannel(
102102 } ,
103103 members : [ generateMember ( { user } ) , generateMember ( { user : mentionUser } ) ] ,
104104 messages : [ mainListMessage ] ,
105- threads : [ threadMessage ] ,
105+ threads : [ threadMessage ] as unknown as GenerateChannelOptions [ 'threads' ] ,
106106 } ) ,
107107) ;
108108
109- const defaultChatContext = {
109+ const defaultChatContext = fromPartial < ChatContextValue > ( {
110110 channelsQueryState : { queryInProgress : 'uninitialized' } ,
111111 getAppSettings : vi . fn ( ) ,
112112 latestMessageDatesByChannels : { } ,
113113 mutes : [ ] ,
114114 searchController : new SearchController ( ) ,
115- } ;
115+ } ) ;
116116
117117const cooldown = 30 ;
118118const filename = 'some.txt' ;
119119const fileUploadUrl = 'http://www.getstream.io' ; // real url, because ImageAttachmentPreview will try to load the image
120120
121121const getImage = ( ) => new File ( [ 'content' ] , filename , { type : 'image/png' } ) ;
122- const getFile = ( name = filename ) => new File ( [ 'content' ] , name , { type : 'text/plain' } ) ;
122+ const getFile = ( name = filename ) : File =>
123+ new File ( [ 'content' ] , name , { type : 'text/plain' } ) ;
123124
124125// Polyfill DOMRect for jsdom
125126if ( typeof globalThis . DOMRect === 'undefined' ) {
@@ -174,7 +175,7 @@ vi.mock('../../Channel/utils', async (importOriginal) => ({
174175 makeAddNotifications : ( ) => mockAddNotification ,
175176} ) ) ;
176177
177- const defaultMessageContextValue = {
178+ const defaultMessageContextValue = fromPartial < MessageContextValue > ( {
178179 getMessageActions : ( ) => [ 'delete' , 'edit' , 'quote' ] ,
179180 handleDelete : ( ) => { } ,
180181 handleFlag : ( ) => { } ,
@@ -183,7 +184,7 @@ const defaultMessageContextValue = {
183184 handlePin : ( ) => { } ,
184185 isMyMessage : ( ) => true ,
185186 message : mainListMessage ,
186- } ;
187+ } ) ;
187188
188189function dropFile ( file : File , formElement : Element ) {
189190 fireEvent . drop ( formElement , {
@@ -350,16 +351,26 @@ const setupUploadRejected = async (error: unknown) => {
350351} ;
351352
352353/** `channel.sendImage` / `channel.sendFile` pass upload options (e.g. `onUploadProgress`) after the file. */
353- const expectChannelUploadCall = ( spy , expectedFile ) => {
354- expect ( spy ) . toHaveBeenCalled ( ) ;
354+ type UploadSpy = {
355+ mock : {
356+ calls : [ unknown , ...unknown [ ] ] [ ] ;
357+ } ;
358+ } ;
359+
360+ const expectChannelUploadCall = ( spy : UploadSpy , expectedFile : File ) => {
361+ expect ( spy . mock . calls . length ) . toBeGreaterThan ( 0 ) ;
355362 const callArgs = spy . mock . calls [ 0 ] ;
356363 expect ( callArgs [ 0 ] ) . toBe ( expectedFile ) ;
357364 expect ( callArgs [ callArgs . length - 1 ] ) . toEqual (
358365 expect . objectContaining ( { onUploadProgress : expect . any ( Function ) } ) ,
359366 ) ;
360367} ;
361368
362- const renderWithActiveCooldown = async ( { messageInputProps = { } } = { } ) => {
369+ const renderWithActiveCooldown = async ( {
370+ messageInputProps = { } ,
371+ } : {
372+ messageInputProps ?: Partial < MessageComposerProps > ;
373+ } = { } ) => {
363374 const {
364375 channels : [ channel ] ,
365376 client,
@@ -530,9 +541,9 @@ describe(`MessageInputFlat`, () => {
530541 // Manually trigger link preview state so the previews section renders
531542 await act ( ( ) => {
532543 const scrapedData = generateScrapedDataAttachment (
533- fromPartial < Attachment > ( {
544+ fromPartial < LinkPreview > ( {
534545 og_scrape_url : 'http://getstream.io' ,
535- status : 'loaded' ,
546+ status : LinkPreviewStatus . LOADED ,
536547 title : 'http://getstream.io' ,
537548 } ) ,
538549 ) ;
@@ -1321,6 +1332,87 @@ describe(`MessageInputFlat`, () => {
13211332 expect ( results ) . toHaveNoViolations ( ) ;
13221333 } ) ;
13231334
1335+ describe ( 'Command dismissal' , ( ) => {
1336+ it ( 'clears the active command when Escape is pressed in the textarea' , async ( ) => {
1337+ const { channel } = await renderComponent ( ) ;
1338+ const input = await screen . findByPlaceholderText ( inputPlaceholder ) ;
1339+ const command = fromPartial < CommandResponse > ( {
1340+ args : 'giphy-command-args' ,
1341+ description : 'giphy-command-description' ,
1342+ name : 'giphy' ,
1343+ } ) ;
1344+
1345+ await act ( ( ) => {
1346+ channel . messageComposer . textComposer . setCommand ( command ) ;
1347+ } ) ;
1348+
1349+ expect ( screen . getByText ( 'giphy' ) ) . toBeInTheDocument ( ) ;
1350+
1351+ await act ( ( ) => {
1352+ fireEvent . keyDown ( input , { key : 'Escape' } ) ;
1353+ } ) ;
1354+
1355+ await waitFor ( ( ) => {
1356+ expect ( screen . queryByText ( 'giphy' ) ) . not . toBeInTheDocument ( ) ;
1357+ } ) ;
1358+ } ) ;
1359+
1360+ it ( 'clears the active command when Backspace is pressed in an empty textarea' , async ( ) => {
1361+ const { channel } = await renderComponent ( ) ;
1362+ const input = await screen . findByPlaceholderText ( inputPlaceholder ) ;
1363+ const command = fromPartial < CommandResponse > ( {
1364+ args : 'giphy-command-args' ,
1365+ description : 'giphy-command-description' ,
1366+ name : 'giphy' ,
1367+ } ) ;
1368+
1369+ await act ( ( ) => {
1370+ channel . messageComposer . textComposer . setCommand ( command ) ;
1371+ } ) ;
1372+
1373+ expect ( screen . getByText ( 'giphy' ) ) . toBeInTheDocument ( ) ;
1374+ expect ( input ) . toHaveValue ( '' ) ;
1375+
1376+ await act ( ( ) => {
1377+ fireEvent . keyDown ( input , { key : 'Backspace' } ) ;
1378+ } ) ;
1379+
1380+ await waitFor ( ( ) => {
1381+ expect ( screen . queryByText ( 'giphy' ) ) . not . toBeInTheDocument ( ) ;
1382+ } ) ;
1383+ } ) ;
1384+
1385+ it ( 'clears the active command when the textarea DOM is empty even if composer text lags behind' , async ( ) => {
1386+ const { channel } = await renderComponent ( ) ;
1387+ const input = ( await screen . findByPlaceholderText (
1388+ inputPlaceholder ,
1389+ ) ) as HTMLTextAreaElement ;
1390+ const command = fromPartial < CommandResponse > ( {
1391+ args : 'giphy-command-args' ,
1392+ description : 'giphy-command-description' ,
1393+ name : 'giphy' ,
1394+ } ) ;
1395+
1396+ await act ( ( ) => {
1397+ channel . messageComposer . textComposer . setCommand ( command ) ;
1398+ channel . messageComposer . textComposer . state . next ( {
1399+ ...channel . messageComposer . textComposer . state . getLatestValue ( ) ,
1400+ text : 'a' ,
1401+ } ) ;
1402+ } ) ;
1403+
1404+ input . value = '' ;
1405+
1406+ await act ( ( ) => {
1407+ fireEvent . keyDown ( input , { key : 'Backspace' } ) ;
1408+ } ) ;
1409+
1410+ await waitFor ( ( ) => {
1411+ expect ( screen . queryByText ( 'giphy' ) ) . not . toBeInTheDocument ( ) ;
1412+ } ) ;
1413+ } ) ;
1414+ } ) ;
1415+
13241416 describe ( 'QuotedMessagePreview' , ( ) => {
13251417 it ( 'is displayed on quote action click' , async ( ) => {
13261418 await renderComponent ( ) ;
0 commit comments