first commit

This commit is contained in:
2025-04-24 13:11:28 +08:00
commit ff9c54d5e4
5960 changed files with 834111 additions and 0 deletions

View File

@@ -0,0 +1,123 @@
import { expect } from 'chai'
import {
fireEvent,
screen,
waitForElementToBeRemoved,
} from '@testing-library/react'
import fetchMock from 'fetch-mock'
import ChatPane from '../../../../../frontend/js/features/chat/components/chat-pane'
import {
cleanUpContext,
renderWithEditorContext,
} from '../../../helpers/render-with-context'
import { stubMathJax, tearDownMathJaxStubs } from './stubs'
describe('<ChatPane />', function () {
const user = {
id: 'fake_user',
first_name: 'fake_user_first_name',
email: 'fake@example.com',
}
beforeEach(function () {
window.metaAttributesCache.set('ol-user', user)
window.metaAttributesCache.set('ol-chatEnabled', true)
})
afterEach(function () {
fetchMock.removeRoutes().clearHistory()
})
const testMessages = [
{
id: 'msg_1',
content: 'a message',
user,
timestamp: new Date().getTime(),
},
{
id: 'msg_2',
content: 'another message',
user,
timestamp: new Date().getTime(),
},
]
beforeEach(function () {
fetchMock.removeRoutes().clearHistory()
cleanUpContext()
stubMathJax()
})
afterEach(function () {
tearDownMathJaxStubs()
})
it('renders multiple messages', async function () {
fetchMock.get(/messages/, testMessages)
renderWithEditorContext(<ChatPane />, { user })
await screen.findByText('a message')
await screen.findByText('another message')
})
it('provides error message with reload button on FetchError', async function () {
fetchMock.get(/messages/, 500)
renderWithEditorContext(<ChatPane />, { user })
// should have hit a FetchError and will prompt user to reconnect
await screen.findByText('Try again')
// bring chat back up
fetchMock.removeRoutes().clearHistory()
fetchMock.get(/messages/, [])
const reconnectButton = screen.getByRole('button', {
name: 'Try again',
})
expect(reconnectButton).to.exist
// should now reconnect with placeholder message
fireEvent.click(reconnectButton)
await screen.findByText('Send your first message to your collaborators')
})
it('a loading spinner is rendered while the messages are loading, then disappears', async function () {
fetchMock.get(/messages/, [], { delay: 1000 })
renderWithEditorContext(<ChatPane />, { user })
// not displayed initially
expect(screen.queryByText('Loading…')).to.not.exist
// eventually displayed
await screen.findByText('Loading…')
// eventually removed when the fetch call returns
await waitForElementToBeRemoved(() => screen.getByText('Loading…'))
})
describe('"send your first message" placeholder', function () {
it('is rendered when there are no messages ', async function () {
fetchMock.get(/messages/, [])
renderWithEditorContext(<ChatPane />, { user })
await screen.findByText('Send your first message to your collaborators')
})
it('is not rendered when messages are displayed', function () {
fetchMock.get(/messages/, testMessages)
renderWithEditorContext(<ChatPane />, { user })
expect(
screen.queryByText('Send your first message to your collaborators')
).to.not.exist
})
})
})

View File

@@ -0,0 +1,57 @@
import { expect } from 'chai'
import sinon from 'sinon'
import { screen, render, fireEvent } from '@testing-library/react'
import MessageInput from '../../../../../frontend/js/features/chat/components/message-input'
describe('<MessageInput />', function () {
let resetUnreadMessages, sendMessage
beforeEach(function () {
resetUnreadMessages = sinon.stub()
sendMessage = sinon.stub()
})
it('renders successfully', function () {
render(
<MessageInput
sendMessage={sendMessage}
resetUnreadMessages={resetUnreadMessages}
/>
)
screen.getByLabelText('Send a message to your collaborators')
})
it('sends a message after typing and hitting enter', function () {
render(
<MessageInput
sendMessage={sendMessage}
resetUnreadMessages={resetUnreadMessages}
/>
)
const input = screen.getByRole('textbox')
fireEvent.change(input, { target: { value: 'hello world' } })
fireEvent.keyDown(input, { key: 'Enter' })
expect(sendMessage).to.be.calledOnce
expect(sendMessage).to.be.calledWith('hello world')
})
it('resets the number of unread messages after clicking on the input', function () {
render(
<MessageInput
sendMessage={sendMessage}
resetUnreadMessages={resetUnreadMessages}
/>
)
const input = screen.getByPlaceholderText(
'Send a message to your collaborators…'
)
fireEvent.click(input)
expect(resetUnreadMessages).to.be.calledOnce
})
})

View File

@@ -0,0 +1,119 @@
import sinon from 'sinon'
import { expect } from 'chai'
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'
describe('<MessageList />', function () {
const currentUser = {
id: 'fake_user',
first_name: 'fake_user_first_name',
email: 'fake@example.com',
}
function createMessages() {
return [
{
id: '1',
contents: ['a message'],
user: currentUser,
timestamp: new Date().getTime(),
},
{
id: '2',
contents: ['another message'],
user: currentUser,
timestamp: new Date().getTime(),
},
]
}
before(function () {
stubMathJax()
})
after(function () {
tearDownMathJaxStubs()
})
let olUser
beforeEach(function () {
olUser = window.metaAttributesCache.get('ol-user')
window.metaAttributesCache.set('ol-user', currentUser)
})
afterEach(function () {
window.metaAttributesCache.set('ol-user', olUser)
})
it('renders multiple messages', function () {
render(
<UserProvider>
<MessageList
userId={currentUser.id}
messages={createMessages()}
resetUnreadMessages={() => {}}
/>
</UserProvider>
)
screen.getByText('a message')
screen.getByText('another message')
})
it('renders a single timestamp for all messages within 5 minutes', function () {
const msgs = createMessages()
msgs[0].timestamp = new Date(2019, 6, 3, 4, 23).getTime()
msgs[1].timestamp = new Date(2019, 6, 3, 4, 27).getTime()
render(
<UserProvider>
<MessageList
userId={currentUser.id}
messages={msgs}
resetUnreadMessages={() => {}}
/>
</UserProvider>
)
screen.getByText('4:23 am Wed, 3rd Jul 19')
expect(screen.queryByText('4:27 am Wed, 3rd Jul 19')).to.not.exist
})
it('renders a timestamp for each messages separated for more than 5 minutes', function () {
const msgs = createMessages()
msgs[0].timestamp = new Date(2019, 6, 3, 4, 23).getTime()
msgs[1].timestamp = new Date(2019, 6, 3, 4, 31).getTime()
render(
<UserProvider>
<MessageList
userId={currentUser.id}
messages={msgs}
resetUnreadMessages={() => {}}
/>
</UserProvider>
)
screen.getByText('4:23 am Wed, 3rd Jul 19')
screen.getByText('4:31 am Wed, 3rd Jul 19')
})
it('resets the number of unread messages after clicking on the input', function () {
const resetUnreadMessages = sinon.stub()
render(
<UserProvider>
<MessageList
userId={currentUser.id}
messages={createMessages()}
resetUnreadMessages={resetUnreadMessages}
/>
</UserProvider>
)
fireEvent.click(screen.getByRole('list'))
expect(resetUnreadMessages).to.be.calledOnce
})
})

View File

@@ -0,0 +1,105 @@
import { expect } from 'chai'
import { render, screen } from '@testing-library/react'
import Message from '../../../../../frontend/js/features/chat/components/message'
import { stubMathJax, tearDownMathJaxStubs } from './stubs'
describe('<Message />', function () {
const currentUser = {
id: 'fake_user',
first_name: 'fake_user_first_name',
email: 'fake@example.com',
}
beforeEach(function () {
window.metaAttributesCache.set('ol-user', currentUser)
stubMathJax()
})
afterEach(function () {
tearDownMathJaxStubs()
})
it('renders a basic message', function () {
const message = {
contents: ['a message'],
user: currentUser,
}
render(<Message message={message} fromSelf />)
screen.getByText('a message')
})
it('renders a message with multiple contents', function () {
const message = {
contents: ['a message', 'another message'],
user: currentUser,
}
render(<Message message={message} fromSelf />)
screen.getByText('a message')
screen.getByText('another message')
})
it('renders HTML links within messages', function () {
const message = {
contents: [
'a message with a <a href="https://overleaf.com">link to Overleaf</a>',
],
user: currentUser,
}
render(<Message message={message} fromSelf />)
screen.getByRole('link', { name: 'https://overleaf.com' })
})
describe('when the message is from the user themselves', function () {
const message = {
contents: ['a message'],
user: currentUser,
}
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.email)).to.not.exist
})
})
describe('when the message is from other user', function () {
const otherUser = {
id: 'other_user',
first_name: 'other_user_first_name',
}
const message = {
contents: ['a message'],
user: otherUser,
}
it('should render the other user name', function () {
render(<Message message={message} />)
screen.getByText(otherUser.first_name)
})
it('should render the other user email when their name is not available', function () {
const msg = {
contents: message.contents,
user: {
id: otherUser.id,
email: 'other@example.com',
},
}
render(<Message message={msg} />)
expect(screen.queryByText(otherUser.first_name)).to.not.exist
screen.getByText(msg.user.email)
})
})
})

View File

@@ -0,0 +1,14 @@
import sinon from 'sinon'
export function stubMathJax() {
window.MathJax = {
Hub: {
Queue: sinon.stub(),
config: { tex2jax: { inlineMath: [['$', '$']] } },
},
}
}
export function tearDownMathJaxStubs() {
delete window.MathJax
}