mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-23 17:19:37 +02:00
Merge pull request #27790 from overleaf/mj-chat-typescript
[web] Convert remaining chat frontend and tests to typescript GitOrigin-RevId: 6b2b485433e0a4530f00496e7ecdd49d9eb450af
This commit is contained in:
committed by
Copybot
parent
9f3fb78904
commit
ed91d043c1
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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}
|
||||
/>
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user