mirror of
https://github.com/overleaf/overleaf.git
synced 2025-12-05 01:10:29 +00:00
* Initial server-side delete of chat message plus dropdown * Update chat pane after deleting message * Chat message dropdown styling * Add confirmation dialog for deleting a message * Refactor chat message grouping to allow deletion of individual messages * Delete other user's deleted message from chat pane * Implement message editing * Styling * Make the dropdown appear overlap with the button slightly so that the menu stays visible when the user moves their cursor into the menu when the menu is positioned above the button * Submit edit with Enter key * Add edited indicator to edited chat messages * Add animation to chat message deletion * Tidying, edit chat message textarea improvements * Add types to message-list-utils * update dependencies * edit/delete for ide-redesign * fix type errors in tests * filter deleted messages from group * promisify ChatController * fix tests and translations * add new tests * chat-context tests * fix message-list-appender tests * add new tests for message-list-utils * Update services/web/test/frontend/features/chat/context/chat-context.test.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * preserve original content when canceling edits * update delete message translation * hide dropdown only if not already shown * remove delete animation * fix lint error * fix chat.yaml * hide under feature flag --------- Co-authored-by: Tim Down <158919+timdown@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> GitOrigin-RevId: 12521886a1a59ccd564851df19e5d46c70d328f5
744 lines
23 KiB
TypeScript
744 lines
23 KiB
TypeScript
// Disable prop type checks for test harnesses
|
|
/* eslint-disable react/prop-types */
|
|
|
|
import { renderHook, act, waitFor } from '@testing-library/react'
|
|
import { expect } from 'chai'
|
|
import sinon from 'sinon'
|
|
import fetchMock from 'fetch-mock'
|
|
import clientIdGenerator from '@/utils/client-id'
|
|
import {
|
|
useChatContext,
|
|
ServerMessageEntry,
|
|
} from '@/features/chat/context/chat-context'
|
|
import { stubMathJax, tearDownMathJaxStubs } from '../components/stubs'
|
|
import { SocketIOMock } from '@/ide/connection/SocketIoShim'
|
|
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: User = {
|
|
id: 'fake_user' as UserId,
|
|
first_name: 'fake_user_first_name',
|
|
email: 'fake@example.com',
|
|
}
|
|
const uuidValue = '00000000-0000-0000-0000-000000000000'
|
|
|
|
beforeEach(function () {
|
|
fetchMock.removeRoutes().clearHistory()
|
|
|
|
stubMathJax()
|
|
|
|
window.metaAttributesCache.set('ol-user', user)
|
|
window.metaAttributesCache.set('ol-preventCompileOnLoad', true)
|
|
|
|
this.stub = sinon.stub(clientIdGenerator, 'get').returns(uuidValue)
|
|
})
|
|
|
|
afterEach(function () {
|
|
tearDownMathJaxStubs()
|
|
|
|
this.stub.restore()
|
|
})
|
|
|
|
describe('socket connection', function () {
|
|
beforeEach(function () {
|
|
// Mock GET messages to return no messages
|
|
fetchMock.get('express:/project/:projectId/messages', [], {
|
|
name: 'fetchMessages',
|
|
})
|
|
|
|
// Mock POST new message to return 200
|
|
fetchMock.post('express:/project/:projectId/messages', 200)
|
|
})
|
|
|
|
afterEach(function () {
|
|
fetchMock.removeRoutes().clearHistory()
|
|
})
|
|
|
|
it('subscribes when mounted', function () {
|
|
const socket = new SocketIOMock()
|
|
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: socket as any as Socket,
|
|
})
|
|
|
|
unmount()
|
|
|
|
expect(socket.countEventListeners('new-chat-message')).to.equal(0)
|
|
})
|
|
|
|
it('adds received messages to the list', async function () {
|
|
// Mock socket: we only need to emit events, not mock actual connections
|
|
const socket = new SocketIOMock()
|
|
const { result } = renderChatContextHook({
|
|
socket: socket as any as Socket,
|
|
})
|
|
|
|
// Wait until initial messages have loaded
|
|
result.current.loadInitialMessages()
|
|
await waitFor(
|
|
() => expect(result.current.initialMessagesLoaded).to.be.true
|
|
)
|
|
|
|
// No messages shown at first
|
|
expect(result.current.messages).to.deep.equal([])
|
|
|
|
// Mock message being received from another user
|
|
socket.emitToClient('new-chat-message', {
|
|
id: 'msg_1',
|
|
content: 'new message',
|
|
timestamp: Date.now(),
|
|
user: {
|
|
id: 'another_fake_user',
|
|
first_name: 'another_fake_user_first_name',
|
|
email: 'another_fake@example.com',
|
|
},
|
|
})
|
|
|
|
await waitFor(() => {
|
|
const message = result.current.messages[0]
|
|
expect(message.id).to.equal('msg_1')
|
|
expect(message.content).to.deep.equal('new message')
|
|
})
|
|
})
|
|
|
|
it('deduplicate messages from preloading', async function () {
|
|
// Mock socket: we only need to emit events, not mock actual connections
|
|
const socket = new SocketIOMock()
|
|
const { result } = renderChatContextHook({
|
|
socket: socket as any as Socket,
|
|
})
|
|
|
|
fetchMock.modifyRoute('fetchMessages', {
|
|
response: () => [
|
|
{
|
|
id: 'msg_1',
|
|
content: 'new message',
|
|
timestamp: Date.now(),
|
|
user: {
|
|
id: 'another_fake_user',
|
|
first_name: 'another_fake_user_first_name',
|
|
email: 'another_fake@example.com',
|
|
},
|
|
},
|
|
],
|
|
})
|
|
|
|
// Mock message being received from another user
|
|
socket.emitToClient('new-chat-message', {
|
|
id: 'msg_1',
|
|
content: 'new message',
|
|
timestamp: Date.now(),
|
|
user: {
|
|
id: 'another_fake_user',
|
|
first_name: 'another_fake_user_first_name',
|
|
email: 'another_fake@example.com',
|
|
},
|
|
})
|
|
|
|
// Check if received the message ID
|
|
await waitFor(() => expect(result.current.messages).to.have.length(1))
|
|
|
|
// Wait until initial messages have loaded
|
|
result.current.loadInitialMessages()
|
|
await waitFor(
|
|
() => expect(result.current.initialMessagesLoaded).to.be.true
|
|
)
|
|
|
|
// Check if there are no message duplication
|
|
expect(result.current.messages).to.have.length(1)
|
|
|
|
const message = result.current.messages[0]
|
|
expect(message.id).to.equal('msg_1')
|
|
expect(message.content).to.deep.equal('new message')
|
|
})
|
|
|
|
it('deduplicate messages from websocket', async function () {
|
|
// Mock socket: we only need to emit events, not mock actual connections
|
|
const socket = new SocketIOMock()
|
|
const { result } = renderChatContextHook({
|
|
socket: socket as any as Socket,
|
|
})
|
|
|
|
fetchMock.modifyRoute('fetchMessages', {
|
|
response: [
|
|
{
|
|
id: 'msg_1',
|
|
content: 'new message',
|
|
timestamp: Date.now(),
|
|
user: {
|
|
id: 'another_fake_user',
|
|
first_name: 'another_fake_user_first_name',
|
|
email: 'another_fake@example.com',
|
|
},
|
|
},
|
|
],
|
|
})
|
|
|
|
// Wait until initial messages have loaded
|
|
result.current.loadInitialMessages()
|
|
|
|
// Check if received the message ID
|
|
await waitFor(() => expect(result.current.messages).to.have.length(1))
|
|
|
|
// Mock message being received from another user
|
|
socket.emitToClient('new-chat-message', {
|
|
id: 'msg_1',
|
|
content: 'new message',
|
|
timestamp: Date.now(),
|
|
user: {
|
|
id: 'another_fake_user',
|
|
first_name: 'another_fake_user_first_name',
|
|
email: 'another_fake@example.com',
|
|
},
|
|
})
|
|
|
|
// Check if there are no message duplication
|
|
expect(result.current.messages).to.have.length(1)
|
|
|
|
const message = result.current.messages[0]
|
|
expect(message.id).to.equal('msg_1')
|
|
expect(message.content).to.deep.equal('new message')
|
|
})
|
|
|
|
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 as any as Socket,
|
|
})
|
|
|
|
// Wait until initial messages have loaded
|
|
result.current.loadInitialMessages()
|
|
await waitFor(
|
|
() => expect(result.current.initialMessagesLoaded).to.be.true
|
|
)
|
|
|
|
// Send a message from the current user
|
|
const content = 'sent message'
|
|
result.current.sendMessage(content)
|
|
|
|
act(() => {
|
|
// Receive a message from the current user
|
|
socket.emitToClient('new-chat-message', {
|
|
id: 'msg_1',
|
|
content,
|
|
timestamp: Date.now(),
|
|
user,
|
|
clientId: uuidValue,
|
|
})
|
|
})
|
|
|
|
await waitFor(() => expect(result.current.messages).to.have.length(1))
|
|
|
|
const [message] = result.current.messages
|
|
|
|
expect(message.content).to.deep.equal(content)
|
|
})
|
|
|
|
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 as any as Socket,
|
|
})
|
|
|
|
// Wait until initial messages have loaded
|
|
result.current.loadInitialMessages()
|
|
await waitFor(
|
|
() => expect(result.current.initialMessagesLoaded).to.be.true
|
|
)
|
|
|
|
// Send a message from the current user
|
|
const content = 'sent message from current user'
|
|
act(() => {
|
|
result.current.sendMessage(content)
|
|
})
|
|
|
|
const [sentMessageFromCurrentUser] = result.current.messages
|
|
expect(sentMessageFromCurrentUser.content).to.deep.equal(content)
|
|
|
|
const otherMsg = 'new message from other user'
|
|
|
|
act(() => {
|
|
// Receive a message from another user.
|
|
socket.emitToClient('new-chat-message', {
|
|
id: 'msg_1',
|
|
content: otherMsg,
|
|
timestamp: Date.now(),
|
|
user: {
|
|
id: 'another_fake_user',
|
|
first_name: 'another_fake_user_first_name',
|
|
email: 'another_fake@example.com',
|
|
},
|
|
clientId: '123',
|
|
})
|
|
})
|
|
|
|
const [, messageFromOtherUser] = result.current.messages
|
|
expect(messageFromOtherUser.content).to.deep.equal(otherMsg)
|
|
|
|
const receivedMessageTimestamp = Date.now()
|
|
act(() => {
|
|
// Receive a message from the current user
|
|
socket.emitToClient('new-chat-message', {
|
|
id: 'msg_2',
|
|
content,
|
|
timestamp: receivedMessageTimestamp,
|
|
user,
|
|
clientId: uuidValue,
|
|
})
|
|
})
|
|
|
|
// Since this message has the same clientId, it should update the pending message
|
|
const updatedSentMessage = {
|
|
...sentMessageFromCurrentUser,
|
|
id: 'msg_2',
|
|
content,
|
|
pending: false,
|
|
user,
|
|
timestamp: receivedMessageTimestamp,
|
|
}
|
|
expect(result.current.messages).to.deep.equal([
|
|
updatedSentMessage,
|
|
messageFromOtherUser,
|
|
])
|
|
})
|
|
|
|
it('handles multiple pending messages correctly when confirmed out of order', async function () {
|
|
const socket = new SocketIOMock()
|
|
const { result } = renderChatContextHook({
|
|
socket: socket as any as Socket,
|
|
})
|
|
|
|
// Wait until initial messages have loaded
|
|
result.current.loadInitialMessages()
|
|
await waitFor(
|
|
() => expect(result.current.initialMessagesLoaded).to.be.true
|
|
)
|
|
|
|
// Send first message
|
|
act(() => {
|
|
result.current.sendMessage('first message')
|
|
})
|
|
|
|
// Send second message quickly
|
|
act(() => {
|
|
result.current.sendMessage('second message')
|
|
})
|
|
|
|
// At this point we should have 2 pending messages
|
|
expect(result.current.messages).to.have.length(2)
|
|
expect(result.current.messages[0].content).to.equal('first message')
|
|
expect(result.current.messages[0].pending).to.be.true
|
|
expect(result.current.messages[1].content).to.equal('second message')
|
|
expect(result.current.messages[1].pending).to.be.true
|
|
|
|
// Server confirms the second message first
|
|
const secondMessageTimestamp = Date.now()
|
|
act(() => {
|
|
socket.emitToClient('new-chat-message', {
|
|
id: 'server-id-2',
|
|
content: 'second message',
|
|
user,
|
|
timestamp: secondMessageTimestamp,
|
|
clientId: uuidValue,
|
|
})
|
|
})
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.messages[0].content).to.equal('first message')
|
|
expect(result.current.messages[0].pending).to.be.true // Still pending
|
|
expect(result.current.messages[1].content).to.equal('second message')
|
|
expect(result.current.messages[1].pending).to.be.false // Confirmed
|
|
expect(result.current.messages[1].id).to.equal('server-id-2')
|
|
})
|
|
|
|
// Server confirms the first message
|
|
const firstMessageTimestamp = Date.now()
|
|
act(() => {
|
|
socket.emitToClient('new-chat-message', {
|
|
id: 'server-id-1',
|
|
content: 'first message',
|
|
user,
|
|
timestamp: firstMessageTimestamp,
|
|
clientId: uuidValue,
|
|
})
|
|
})
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.messages[0].content).to.equal('first message')
|
|
expect(result.current.messages[0].pending).to.be.false // Now confirmed
|
|
expect(result.current.messages[0].id).to.equal('server-id-1')
|
|
expect(result.current.messages[1].content).to.equal('second message')
|
|
expect(result.current.messages[1].pending).to.be.false // Still confirmed
|
|
expect(result.current.messages[1].id).to.equal('server-id-2')
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('loadInitialMessages', function () {
|
|
beforeEach(function () {
|
|
fetchMock.get('express:/project/:projectId/messages', [
|
|
{
|
|
id: 'msg_1',
|
|
content: 'a message',
|
|
user,
|
|
timestamp: Date.now(),
|
|
},
|
|
])
|
|
})
|
|
|
|
it('adds messages to the list', async function () {
|
|
const { result } = renderChatContextHook({})
|
|
|
|
result.current.loadInitialMessages()
|
|
await waitFor(() =>
|
|
expect(result.current.messages[0].content).to.deep.equal('a message')
|
|
)
|
|
})
|
|
|
|
it("won't load messages a second time", async function () {
|
|
const { result } = renderChatContextHook({})
|
|
|
|
result.current.loadInitialMessages()
|
|
|
|
await waitFor(() =>
|
|
expect(result.current.initialMessagesLoaded).to.equal(true)
|
|
)
|
|
|
|
// Calling a second time won't do anything
|
|
result.current.loadInitialMessages()
|
|
await waitFor(
|
|
() => expect(result.current.initialMessagesLoaded).to.be.true
|
|
)
|
|
|
|
expect(
|
|
fetchMock.callHistory.calls('express:/project/:projectId/messages')
|
|
).to.have.lengthOf(1)
|
|
})
|
|
|
|
it('provides an error on failure', async function () {
|
|
fetchMock.removeRoutes().clearHistory()
|
|
fetchMock.get('express:/project/:projectId/messages', 500)
|
|
const { result } = renderChatContextHook({})
|
|
|
|
result.current.loadInitialMessages()
|
|
await waitFor(() => expect(result.current.error).to.exist)
|
|
expect(result.current.status).to.equal('error')
|
|
})
|
|
})
|
|
|
|
describe('loadMoreMessages', function () {
|
|
it('adds messages to the list', async function () {
|
|
// Mock a GET request for an initial message
|
|
fetchMock.getOnce('express:/project/:projectId/messages', [
|
|
{
|
|
id: 'msg_1',
|
|
content: 'first message',
|
|
user,
|
|
timestamp: new Date('2021-03-04T10:00:00').getTime(),
|
|
},
|
|
])
|
|
|
|
const { result } = renderChatContextHook({})
|
|
|
|
result.current.loadMoreMessages()
|
|
await waitFor(() =>
|
|
expect(result.current.messages[0].content).to.deep.equal(
|
|
'first message'
|
|
)
|
|
)
|
|
|
|
// The before query param is not set
|
|
expect(getLastFetchMockQueryParam('before')).to.be.null
|
|
})
|
|
|
|
it('adds more messages if called a second time', async function () {
|
|
// Mock 2 GET requests, with different content
|
|
fetchMock
|
|
.getOnce(
|
|
'express:/project/:projectId/messages',
|
|
// Resolve a full "page" of messages (50)
|
|
createMessages(50, user, new Date('2021-03-04T10:00:00').getTime())
|
|
)
|
|
.getOnce('express:/project/:projectId/messages', [
|
|
{
|
|
id: 'msg_51',
|
|
content: 'message from second page',
|
|
user,
|
|
timestamp: new Date('2021-03-04T11:00:00').getTime(),
|
|
},
|
|
])
|
|
|
|
const { result } = renderChatContextHook({})
|
|
|
|
result.current.loadMoreMessages()
|
|
await waitFor(() => expect(result.current.messages).to.have.length(50))
|
|
|
|
// Call a second time
|
|
result.current.loadMoreMessages()
|
|
|
|
// The second request is added to the list
|
|
// Since both messages from the same user, they are collapsed into the
|
|
// same "message"
|
|
await waitFor(() =>
|
|
expect(result.current.messages[0].content).to.include(
|
|
'message from second page'
|
|
)
|
|
)
|
|
|
|
// The before query param for the second request matches the timestamp
|
|
// of the first message
|
|
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())
|
|
})
|
|
|
|
it("won't load more messages if there are no more messages", async function () {
|
|
// Mock a GET request for 49 messages. This is less than the full page
|
|
// size (50 messages), meaning that there are no further messages to be
|
|
// loaded
|
|
fetchMock.getOnce(
|
|
'express:/project/:projectId/messages',
|
|
createMessages(49, user)
|
|
)
|
|
|
|
const { result } = renderChatContextHook({})
|
|
|
|
result.current.loadMoreMessages()
|
|
await waitFor(() => expect(result.current.messages).to.have.length(49))
|
|
|
|
result.current.loadMoreMessages()
|
|
|
|
await waitFor(() => expect(result.current.atEnd).to.be.true)
|
|
|
|
expect(
|
|
fetchMock.callHistory.calls('express:/project/:projectId/messages')
|
|
).to.have.lengthOf(1)
|
|
})
|
|
|
|
it('handles socket messages while loading', async function () {
|
|
// Mock GET messages so that we can control when the promise is resolved
|
|
let resolveLoadingMessages: (messages: ServerMessageEntry[]) => void
|
|
fetchMock.get(
|
|
'express:/project/:projectId/messages',
|
|
new Promise(resolve => {
|
|
resolveLoadingMessages = resolve
|
|
})
|
|
)
|
|
|
|
const socket = new SocketIOMock()
|
|
const { result } = renderChatContextHook({
|
|
socket: socket as any as Socket,
|
|
})
|
|
|
|
// Start loading messages
|
|
result.current.loadMoreMessages()
|
|
|
|
// Mock message being received from the socket while the request is in
|
|
// flight
|
|
socket.emitToClient('new-chat-message', {
|
|
id: 'socket_msg',
|
|
content: 'socket message',
|
|
timestamp: Date.now(),
|
|
user: {
|
|
id: 'another_fake_user',
|
|
first_name: 'another_fake_user_first_name',
|
|
email: 'another_fake@example.com',
|
|
},
|
|
})
|
|
|
|
// Resolve messages being loaded
|
|
resolveLoadingMessages!([
|
|
{
|
|
id: 'fetched_msg',
|
|
content: 'loaded message',
|
|
user,
|
|
timestamp: Date.now(),
|
|
},
|
|
])
|
|
|
|
await waitFor(() => {
|
|
// Although the loaded message was resolved last, it appears first (since
|
|
// requested messages must have come first)
|
|
const messageContents = result.current.messages.map(
|
|
({ content }) => content
|
|
)
|
|
expect(messageContents).to.deep.equal([
|
|
'loaded message',
|
|
'socket message',
|
|
])
|
|
})
|
|
})
|
|
|
|
it('provides an error on failures', async function () {
|
|
fetchMock.removeRoutes().clearHistory()
|
|
fetchMock.get('express:/project/:projectId/messages', 500)
|
|
const { result } = renderChatContextHook({})
|
|
|
|
result.current.loadMoreMessages()
|
|
await waitFor(() => expect(result.current.error).to.exist)
|
|
expect(result.current.status).to.equal('error')
|
|
})
|
|
})
|
|
|
|
describe('sendMessage', function () {
|
|
beforeEach(function () {
|
|
// Mock GET messages to return no messages and POST new message to be
|
|
// successful
|
|
fetchMock
|
|
.get('express:/project/:projectId/messages', [])
|
|
.postOnce('express:/project/:projectId/messages', 200)
|
|
})
|
|
|
|
it('optimistically adds the message to the list', async function () {
|
|
const { result } = renderChatContextHook({})
|
|
|
|
result.current.sendMessage('sent message')
|
|
|
|
await waitFor(() =>
|
|
expect(result.current.messages[0].content).to.deep.equal('sent message')
|
|
)
|
|
})
|
|
|
|
it('POSTs the message to the backend', function () {
|
|
const { result } = renderChatContextHook({})
|
|
|
|
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 },
|
|
} = 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 () {
|
|
const { result } = renderChatContextHook({})
|
|
|
|
result.current.sendMessage('')
|
|
|
|
expect(result.current.messages).to.be.empty
|
|
expect(
|
|
fetchMock.callHistory.called('express:/project/:projectId/messages', {
|
|
method: 'post',
|
|
})
|
|
).to.be.false
|
|
})
|
|
|
|
it('provides an error on failure', async function () {
|
|
fetchMock.removeRoutes().clearHistory()
|
|
fetchMock
|
|
.get('express:/project/:projectId/messages', [])
|
|
.postOnce('express:/project/:projectId/messages', 500)
|
|
const { result } = renderChatContextHook({})
|
|
|
|
result.current.sendMessage('sent message')
|
|
await waitFor(() => expect(result.current.error).to.exist)
|
|
|
|
expect(result.current.status).to.equal('error')
|
|
})
|
|
})
|
|
|
|
describe('unread messages', function () {
|
|
beforeEach(function () {
|
|
// Mock GET messages to return no messages
|
|
fetchMock.get('express:/project/:projectId/messages', [])
|
|
})
|
|
|
|
it('increments unreadMessageCount when a new message is received', function () {
|
|
const socket = new SocketIOMock()
|
|
const { result } = renderChatContextHook({
|
|
socket: socket as any as Socket,
|
|
})
|
|
|
|
// Receive a new message from the socket
|
|
act(() => {
|
|
socket.emitToClient('new-chat-message', {
|
|
id: 'msg_1',
|
|
content: 'new message',
|
|
timestamp: Date.now(),
|
|
user,
|
|
})
|
|
})
|
|
|
|
expect(result.current.unreadMessageCount).to.equal(1)
|
|
})
|
|
|
|
it('resets unreadMessageCount when markMessagesAsRead is called', function () {
|
|
const socket = new SocketIOMock()
|
|
const { result } = renderChatContextHook({
|
|
socket: socket as any as Socket,
|
|
})
|
|
|
|
// Receive a new message from the socket, incrementing unreadMessageCount
|
|
// by 1
|
|
socket.emitToClient('new-chat-message', {
|
|
id: 'msg_1',
|
|
content: 'new message',
|
|
timestamp: Date.now(),
|
|
user,
|
|
})
|
|
|
|
result.current.markMessagesAsRead()
|
|
|
|
expect(result.current.unreadMessageCount).to.equal(0)
|
|
})
|
|
})
|
|
})
|
|
|
|
function renderChatContextHook(props: EditorProvidersProps) {
|
|
return renderHook(() => useChatContext(), {
|
|
// Wrap with ChatContext.Provider (and the other editor context providers)
|
|
// eslint-disable-next-line react/display-name
|
|
wrapper: ({ children }) => (
|
|
<EditorProviders {...props}>{children}</EditorProviders>
|
|
),
|
|
})
|
|
}
|
|
|
|
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}`,
|
|
user,
|
|
timestamp,
|
|
}))
|
|
}
|
|
|
|
/*
|
|
* Get query param by key from the last fetchMock response
|
|
*/
|
|
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)
|
|
}
|