فهرست منبع

Merge pull request #27790 from overleaf/mj-chat-typescript

[web] Convert remaining chat frontend and tests to typescript

GitOrigin-RevId: 6b2b485433e0a4530f00496e7ecdd49d9eb450af
Mathias Jakobsen 1 سال پیش
والد
کامیت
05bf74cf94

+ 7 - 3
services/web/frontend/js/features/chat/context/chat-context.tsx

@@ -32,6 +32,10 @@ export type Message = {
   user?: User
 }
 
+export type ServerMessageEntry = Omit<Message, 'contents'> & {
+  content: string
+}
+
 type State = {
   status: 'idle' | 'pending' | 'error'
   messages: Message[]
@@ -52,16 +56,16 @@ type Action =
     }
   | {
       type: 'FETCH_MESSAGES_SUCCESS'
-      messages: Message[]
+      messages: ServerMessageEntry[]
     }
   | {
       type: 'SEND_MESSAGE'
       user: any
-      content: any
+      content: string
     }
   | {
       type: 'RECEIVE_MESSAGE'
-      message: any
+      message: ServerMessageEntry
     }
   | {
       type: 'MARK_MESSAGES_AS_READ'

+ 12 - 2
services/web/frontend/js/features/chat/utils/message-list-appender.js → services/web/frontend/js/features/chat/utils/message-list-appender.ts

@@ -1,6 +1,12 @@
+import { Message, ServerMessageEntry } from '../context/chat-context'
+
 const TIMESTAMP_GROUP_SIZE = 5 * 60 * 1000 // 5 minutes
 
-export function appendMessage(messageList, message, uniqueMessageIds) {
+export function appendMessage(
+  messageList: Message[],
+  message: ServerMessageEntry,
+  uniqueMessageIds: string[]
+) {
   if (uniqueMessageIds.includes(message.id)) {
     return { messages: messageList, uniqueMessageIds }
   }
@@ -41,7 +47,11 @@ export function appendMessage(messageList, message, uniqueMessageIds) {
   return { messages: messageList, uniqueMessageIds }
 }
 
-export function prependMessages(messageList, messages, uniqueMessageIds) {
+export function prependMessages(
+  messageList: Message[],
+  messages: ServerMessageEntry[],
+  uniqueMessageIds: string[]
+) {
   const listCopy = messageList.slice(0)
 
   uniqueMessageIds = uniqueMessageIds.slice(0)

+ 0 - 0
services/web/test/frontend/features/chat/components/chat-pane.test.jsx → services/web/test/frontend/features/chat/components/chat-pane.test.tsx


+ 2 - 1
services/web/test/frontend/features/chat/components/message-input.test.jsx → services/web/test/frontend/features/chat/components/message-input.test.tsx

@@ -5,7 +5,8 @@ import { screen, render, fireEvent } from '@testing-library/react'
 import MessageInput from '../../../../../frontend/js/features/chat/components/message-input'
 
 describe('<MessageInput />', function () {
-  let resetUnreadMessages, sendMessage
+  let resetUnreadMessages: () => void
+  let sendMessage: (content: string) => void
 
   beforeEach(function () {
     resetUnreadMessages = sinon.stub()

+ 8 - 16
services/web/test/frontend/features/chat/components/message-list.test.jsx → services/web/test/frontend/features/chat/components/message-list.test.tsx

@@ -5,15 +5,17 @@ import { screen, render, fireEvent } from '@testing-library/react'
 import MessageList from '../../../../../frontend/js/features/chat/components/message-list'
 import { stubMathJax, tearDownMathJaxStubs } from './stubs'
 import { UserProvider } from '@/shared/context/user-context'
+import { User, UserId } from '@ol-types/user'
+import { Message } from '@/features/chat/context/chat-context'
 
 describe('<MessageList />', function () {
-  const currentUser = {
-    id: 'fake_user',
+  const currentUser: User = {
+    id: 'fake_user' as UserId,
     first_name: 'fake_user_first_name',
     email: 'fake@example.com',
   }
 
-  function createMessages() {
+  function createMessages(): Message[] {
     return [
       {
         id: '1',
@@ -38,7 +40,7 @@ describe('<MessageList />', function () {
     tearDownMathJaxStubs()
   })
 
-  let olUser
+  let olUser: User
   beforeEach(function () {
     olUser = window.metaAttributesCache.get('ol-user')
     window.metaAttributesCache.set('ol-user', currentUser)
@@ -52,7 +54,6 @@ describe('<MessageList />', function () {
     render(
       <UserProvider>
         <MessageList
-          userId={currentUser.id}
           messages={createMessages()}
           resetUnreadMessages={() => {}}
         />
@@ -70,11 +71,7 @@ describe('<MessageList />', function () {
 
     render(
       <UserProvider>
-        <MessageList
-          userId={currentUser.id}
-          messages={msgs}
-          resetUnreadMessages={() => {}}
-        />
+        <MessageList messages={msgs} resetUnreadMessages={() => {}} />
       </UserProvider>
     )
 
@@ -89,11 +86,7 @@ describe('<MessageList />', function () {
 
     render(
       <UserProvider>
-        <MessageList
-          userId={currentUser.id}
-          messages={msgs}
-          resetUnreadMessages={() => {}}
-        />
+        <MessageList messages={msgs} resetUnreadMessages={() => {}} />
       </UserProvider>
     )
 
@@ -106,7 +99,6 @@ describe('<MessageList />', function () {
     render(
       <UserProvider>
         <MessageList
-          userId={currentUser.id}
           messages={createMessages()}
           resetUnreadMessages={resetUnreadMessages}
         />

+ 31 - 16
services/web/test/frontend/features/chat/components/message.test.jsx → services/web/test/frontend/features/chat/components/message.test.tsx

@@ -3,10 +3,12 @@ import { render, screen } from '@testing-library/react'
 
 import Message from '../../../../../frontend/js/features/chat/components/message'
 import { stubMathJax, tearDownMathJaxStubs } from './stubs'
+import { User, UserId } from '@ol-types/user'
+import { Message as MessageType } from '@/features/chat/context/chat-context'
 
 describe('<Message />', function () {
-  const currentUser = {
-    id: 'fake_user',
+  const currentUser: User = {
+    id: 'fake_user' as UserId,
     first_name: 'fake_user_first_name',
     email: 'fake@example.com',
   }
@@ -21,9 +23,11 @@ describe('<Message />', function () {
   })
 
   it('renders a basic message', function () {
-    const message = {
+    const message: MessageType = {
       contents: ['a message'],
       user: currentUser,
+      id: 'msg_1',
+      timestamp: new Date('2025-01-01T00:00:00.000Z').getTime(),
     }
 
     render(<Message message={message} fromSelf />)
@@ -32,9 +36,11 @@ describe('<Message />', function () {
   })
 
   it('renders a message with multiple contents', function () {
-    const message = {
+    const message: MessageType = {
       contents: ['a message', 'another message'],
       user: currentUser,
+      id: 'msg_1',
+      timestamp: new Date('2025-01-01T00:00:00.000Z').getTime(),
     }
 
     render(<Message message={message} fromSelf />)
@@ -44,11 +50,13 @@ describe('<Message />', function () {
   })
 
   it('renders HTML links within messages', function () {
-    const message = {
+    const message: MessageType = {
       contents: [
         'a message with a <a href="https://overleaf.com">link to Overleaf</a>',
       ],
       user: currentUser,
+      id: 'msg_1',
+      timestamp: new Date('2025-01-01T00:00:00.000Z').getTime(),
     }
 
     render(<Message message={message} fromSelf />)
@@ -57,49 +65,56 @@ describe('<Message />', function () {
   })
 
   describe('when the message is from the user themselves', function () {
-    const message = {
+    const message: MessageType = {
       contents: ['a message'],
       user: currentUser,
+      id: 'msg_1',
+      timestamp: new Date('2025-01-01T00:00:00.000Z').getTime(),
     }
 
     it('does not render the user name nor the email', function () {
       render(<Message message={message} fromSelf />)
 
-      expect(screen.queryByText(currentUser.first_name)).to.not.exist
+      expect(screen.queryByText(currentUser.first_name!)).to.not.exist
       expect(screen.queryByText(currentUser.email)).to.not.exist
     })
   })
 
   describe('when the message is from other user', function () {
-    const otherUser = {
-      id: 'other_user',
+    const otherUser: User = {
+      id: 'other_user' as UserId,
       first_name: 'other_user_first_name',
+      email: 'other@example.com',
     }
 
-    const message = {
+    const message: MessageType = {
       contents: ['a message'],
       user: otherUser,
+      id: 'msg_1',
+      timestamp: new Date('2025-01-01T00:00:00.000Z').getTime(),
     }
 
     it('should render the other user name', function () {
-      render(<Message message={message} />)
+      render(<Message message={message} fromSelf={false} />)
 
-      screen.getByText(otherUser.first_name)
+      screen.getByText(otherUser.first_name!)
     })
 
     it('should render the other user email when their name is not available', function () {
-      const msg = {
+      const msg: MessageType = {
         contents: message.contents,
         user: {
           id: otherUser.id,
           email: 'other@example.com',
         },
+        id: 'msg_1',
+        timestamp: new Date('2025-01-01T00:00:00.000Z').getTime(),
       }
 
-      render(<Message message={msg} />)
+      render(<Message message={msg} fromSelf={false} />)
 
-      expect(screen.queryByText(otherUser.first_name)).to.not.exist
-      screen.getByText(msg.user.email)
+      expect(screen.queryByText(otherUser.first_name!)).to.not.exist
+      screen.getByText(msg.user!.email)
     })
   })
 })

+ 1 - 0
services/web/test/frontend/features/chat/components/stubs.js → services/web/test/frontend/features/chat/components/stubs.ts

@@ -10,5 +10,6 @@ export function stubMathJax() {
 }
 
 export function tearDownMathJaxStubs() {
+  // @ts-expect-error - this is a stub that we're setting ourselves per test
   delete window.MathJax
 }

+ 55 - 24
services/web/test/frontend/features/chat/context/chat-context.test.jsx → services/web/test/frontend/features/chat/context/chat-context.test.tsx

@@ -8,14 +8,20 @@ import fetchMock from 'fetch-mock'
 import {
   useChatContext,
   chatClientIdGenerator,
+  ServerMessageEntry,
 } from '@/features/chat/context/chat-context'
 import { stubMathJax, tearDownMathJaxStubs } from '../components/stubs'
 import { SocketIOMock } from '@/ide/connection/SocketIoShim'
-import { EditorProviders } from '../../../helpers/editor-providers'
+import {
+  EditorProviders,
+  EditorProvidersProps,
+} from '../../../helpers/editor-providers'
+import { User, UserId } from '@ol-types/user'
+import { Socket } from '@/features/ide-react/connection/types/socket'
 
 describe('ChatContext', function () {
-  const user = {
-    id: 'fake_user',
+  const user: User = {
+    id: 'fake_user' as UserId,
     first_name: 'fake_user_first_name',
     email: 'fake@example.com',
   }
@@ -55,13 +61,17 @@ describe('ChatContext', function () {
 
     it('subscribes when mounted', function () {
       const socket = new SocketIOMock()
-      renderChatContextHook({ socket })
+      renderChatContextHook({
+        socket: socket as any as Socket,
+      })
       expect(socket.countEventListeners('new-chat-message')).to.equal(1)
     })
 
     it('unsubscribes when unmounted', function () {
       const socket = new SocketIOMock()
-      const { unmount } = renderChatContextHook({ socket })
+      const { unmount } = renderChatContextHook({
+        socket: socket as any as Socket,
+      })
 
       unmount()
 
@@ -72,7 +82,7 @@ describe('ChatContext', function () {
       // Mock socket: we only need to emit events, not mock actual connections
       const socket = new SocketIOMock()
       const { result } = renderChatContextHook({
-        socket,
+        socket: socket as any as Socket,
       })
 
       // Wait until initial messages have loaded
@@ -107,7 +117,7 @@ describe('ChatContext', function () {
       // Mock socket: we only need to emit events, not mock actual connections
       const socket = new SocketIOMock()
       const { result } = renderChatContextHook({
-        socket,
+        socket: socket as any as Socket,
       })
 
       fetchMock.modifyRoute('fetchMessages', {
@@ -158,7 +168,7 @@ describe('ChatContext', function () {
       // Mock socket: we only need to emit events, not mock actual connections
       const socket = new SocketIOMock()
       const { result } = renderChatContextHook({
-        socket,
+        socket: socket as any as Socket,
       })
 
       fetchMock.modifyRoute('fetchMessages', {
@@ -205,7 +215,7 @@ describe('ChatContext', function () {
     it("doesn't add received messages from the current user if a message was just sent", async function () {
       const socket = new SocketIOMock()
       const { result } = renderChatContextHook({
-        socket,
+        socket: socket as any as Socket,
       })
 
       // Wait until initial messages have loaded
@@ -239,7 +249,7 @@ describe('ChatContext', function () {
     it('adds the new message from the current user if another message was received after sending', async function () {
       const socket = new SocketIOMock()
       const { result } = renderChatContextHook({
-        socket,
+        socket: socket as any as Socket,
       })
 
       // Wait until initial messages have loaded
@@ -411,7 +421,9 @@ describe('ChatContext', function () {
 
       // The before query param for the second request matches the timestamp
       // of the first message
-      const beforeParam = parseInt(getLastFetchMockQueryParam('before'), 10)
+      const beforeParamRaw = getLastFetchMockQueryParam('before')
+      expect(beforeParamRaw).to.not.be.null
+      const beforeParam = parseInt(beforeParamRaw!, 10)
       expect(beforeParam).to.equal(new Date('2021-03-04T10:00:00').getTime())
     })
 
@@ -442,7 +454,7 @@ describe('ChatContext', function () {
 
     it('handles socket messages while loading', async function () {
       // Mock GET messages so that we can control when the promise is resolved
-      let resolveLoadingMessages
+      let resolveLoadingMessages: (messages: ServerMessageEntry[]) => void
       fetchMock.get(
         'express:/project/:projectId/messages',
         new Promise(resolve => {
@@ -452,7 +464,7 @@ describe('ChatContext', function () {
 
       const socket = new SocketIOMock()
       const { result } = renderChatContextHook({
-        socket,
+        socket: socket as any as Socket,
       })
 
       // Start loading messages
@@ -472,7 +484,7 @@ describe('ChatContext', function () {
       })
 
       // Resolve messages being loaded
-      resolveLoadingMessages([
+      resolveLoadingMessages!([
         {
           id: 'fetched_msg',
           content: 'loaded message',
@@ -531,12 +543,19 @@ describe('ChatContext', function () {
 
       result.current.sendMessage('sent message')
 
+      const calls = fetchMock.callHistory.calls(
+        'express:/project/:projectId/messages',
+        { method: 'POST' }
+      )
+      expect(calls.length).to.be.greaterThanOrEqual(1)
+
       const {
         options: { body },
-      } = fetchMock.callHistory
-        .calls('express:/project/:projectId/messages', { method: 'POST' })
-        .at(-1)
-      expect(JSON.parse(body)).to.deep.include({ content: 'sent message' })
+      } = calls.at(-1)!
+      expect(body).to.not.be.undefined
+      expect(JSON.parse(body!.toString())).to.deep.include({
+        content: 'sent message',
+      })
     })
 
     it("doesn't send if the content is empty", function () {
@@ -574,7 +593,9 @@ describe('ChatContext', function () {
 
     it('increments unreadMessageCount when a new message is received', function () {
       const socket = new SocketIOMock()
-      const { result } = renderChatContextHook({ socket })
+      const { result } = renderChatContextHook({
+        socket: socket as any as Socket,
+      })
 
       // Receive a new message from the socket
       act(() => {
@@ -591,7 +612,9 @@ describe('ChatContext', function () {
 
     it('resets unreadMessageCount when markMessagesAsRead is called', function () {
       const socket = new SocketIOMock()
-      const { result } = renderChatContextHook({ socket })
+      const { result } = renderChatContextHook({
+        socket: socket as any as Socket,
+      })
 
       // Receive a new message from the socket, incrementing unreadMessageCount
       // by 1
@@ -609,7 +632,7 @@ describe('ChatContext', function () {
   })
 })
 
-function renderChatContextHook(props) {
+function renderChatContextHook(props: EditorProvidersProps) {
   return renderHook(() => useChatContext(), {
     // Wrap with ChatContext.Provider (and the other editor context providers)
     // eslint-disable-next-line react/display-name
@@ -619,7 +642,11 @@ function renderChatContextHook(props) {
   })
 }
 
-function createMessages(number, user, timestamp = Date.now()) {
+function createMessages(
+  number: number,
+  user: User,
+  timestamp = Date.now()
+): ServerMessageEntry[] {
   return Array.from({ length: number }, (_m, idx) => ({
     id: `msg_${idx + 1}`,
     content: `message ${idx + 1}`,
@@ -631,8 +658,12 @@ function createMessages(number, user, timestamp = Date.now()) {
 /*
  * Get query param by key from the last fetchMock response
  */
-function getLastFetchMockQueryParam(key) {
-  const { url } = fetchMock.callHistory.calls().at(-1)
+function getLastFetchMockQueryParam(key: string) {
+  const calls = fetchMock.callHistory.calls()
+  if (calls.length === 0) {
+    throw new Error('No fetchMock calls found')
+  }
+  const { url } = calls.at(-1)!
   const { searchParams } = new URL(url, 'https://www.overleaf.com')
   return searchParams.get(key)
 }

+ 20 - 13
services/web/test/frontend/features/chat/util/message-list-appender.test.js → services/web/test/frontend/features/chat/util/message-list-appender.test.tsx

@@ -3,16 +3,23 @@ import {
   appendMessage,
   prependMessages,
 } from '../../../../../frontend/js/features/chat/utils/message-list-appender'
+import { User, UserId } from '@ol-types/user'
+import {
+  Message,
+  ServerMessageEntry,
+} from '@/features/chat/context/chat-context'
 
-const testUser = {
-  id: '123abc',
+const testUser: User = {
+  id: '123abc' as UserId,
+  email: 'test-user@example.com',
 }
 
-const otherUser = {
-  id: '234other',
+const otherUser: User = {
+  id: '234other' as UserId,
+  email: 'other-user@example.com',
 }
 
-function createTestMessageList() {
+function createTestMessageList(): Message[] {
   return [
     {
       id: 'msg_1',
@@ -30,7 +37,7 @@ function createTestMessageList() {
 }
 
 describe('prependMessages()', function () {
-  function createTestMessages() {
+  function createTestMessages(): ServerMessageEntry[] {
     const message1 = {
       id: 'prepended_message',
       content: 'hello',
@@ -43,7 +50,7 @@ describe('prependMessages()', function () {
 
   it('to an empty list', function () {
     const messages = createTestMessages()
-    const uniqueMessageIds = []
+    const uniqueMessageIds: string[] = []
 
     expect(
       prependMessages([], messages, uniqueMessageIds).messages
@@ -58,7 +65,7 @@ describe('prependMessages()', function () {
   })
 
   describe('when the messages to prepend are from the same user', function () {
-    let list, messages, uniqueMessageIds
+    let list, messages: ServerMessageEntry[], uniqueMessageIds: string[]
 
     beforeEach(function () {
       list = createTestMessageList()
@@ -106,7 +113,7 @@ describe('prependMessages()', function () {
   })
 
   describe('when the messages to prepend are from different users', function () {
-    let list, messages, uniqueMessageIds
+    let list, messages: ServerMessageEntry[], uniqueMessageIds: string[]
 
     beforeEach(function () {
       list = createTestMessageList()
@@ -141,7 +148,7 @@ describe('prependMessages()', function () {
     const list = createTestMessageList()
     const messages = createTestMessages()
     messages[0].user = messages[1].user = list[0].user
-    const uniqueMessageIds = []
+    const uniqueMessageIds: string[] = []
 
     const result = prependMessages(
       createTestMessageList(),
@@ -170,7 +177,7 @@ describe('appendMessage()', function () {
 
   it('to an empty list', function () {
     const testMessage = createTestMessage()
-    const uniqueMessageIds = []
+    const uniqueMessageIds: string[] = []
 
     expect(
       appendMessage([], testMessage, uniqueMessageIds).messages
@@ -185,7 +192,7 @@ describe('appendMessage()', function () {
   })
 
   describe('messages appended shortly after the last message on the list', function () {
-    let list, message, uniqueMessageIds
+    let list: Message[], message: ServerMessageEntry, uniqueMessageIds: string[]
 
     beforeEach(function () {
       list = createTestMessageList()
@@ -228,7 +235,7 @@ describe('appendMessage()', function () {
   })
 
   describe('messages appended later after the last message on the list', function () {
-    let list, message, uniqueMessageIds
+    let list: Message[], message: ServerMessageEntry, uniqueMessageIds: string[]
 
     beforeEach(function () {
       list = createTestMessageList()