Merge pull request #3267 from overleaf/msm-react-chat-tests

React chat tests

GitOrigin-RevId: e3b4d5b7cb2657d9aad7e1006c18db4e6c0d8a3f
This commit is contained in:
Miguel Serrano 2020-10-27 11:52:40 +01:00 committed by Copybot
parent c504f2a64c
commit df37668180
10 changed files with 457 additions and 5 deletions

View file

@ -58,7 +58,7 @@ function LoadingSpinner() {
return (
<div className="loading">
<Icon type="fw" modifier="refresh" spin />
{` ${t('loading')}...`}
{` ${t('loading')}`}
</div>
)
}

View file

@ -23,10 +23,10 @@ function MessageContent({ content }) {
useEffect(
() => {
// adds attributes to all the links generated by <Linkify/>, required due to https://github.com/tasti/react-linkify/issues/99
root.current.getElementsByTagName('a').forEach(a => {
for (let a of root.current.getElementsByTagName('a')) {
a.setAttribute('target', '_blank')
a.setAttribute('rel', 'noreferrer noopener')
})
}
// MathJax typesetting
const MJHub = window.MathJax.Hub

View file

@ -16,7 +16,7 @@ function MessageInput({ resetUnreadMessages, sendMessage }) {
return (
<div className="new-message">
<textarea
placeholder={`${t('your_message')}...`}
placeholder={`${t('your_message')}`}
onKeyDown={handleKeyDown}
onClick={resetUnreadMessages}
/>

View file

@ -17,7 +17,7 @@
"test:unit:run_dir": "mocha --recursive --timeout 25000 --exit --grep=$MOCHA_GREP --file test/unit/bootstrap.js",
"test:unit:app": "npm run test:unit:run_dir -- test/unit/src",
"test:unit:app:parallel": "parallel --plain --keep-order --halt now,fail=1 npm run test:unit:run_dir -- {} ::: test/unit/src/*",
"test:frontend": "NODE_ENV=test mocha --recursive --exit --grep=$MOCHA_GREP --require test/frontend/bootstrap.js test/frontend modules/*/test/frontend",
"test:frontend": "NODE_ENV=test TZ=GMT mocha --recursive --exit --grep=$MOCHA_GREP --require test/frontend/bootstrap.js test/frontend modules/*/test/frontend",
"test:frontend:coverage": "c8 --all --include 'frontend/js' --include 'modules/*/frontend/js' --exclude 'frontend/js/vendor' --reporter=lcov --reporter=text-summary npm run test:frontend",
"test:karma": "karma start",
"test:karma:single": "karma start --single-run",

View file

@ -11,3 +11,15 @@ chai.use(require('sinon-chai'))
window.i18n = { currentLangCode: 'en' }
require('../../frontend/js/i18n')
const moment = require('moment')
moment.updateLocale('en', {
calendar: {
lastDay: '[Yesterday]',
sameDay: '[Today]',
nextDay: '[Tomorrow]',
lastWeek: 'ddd, Do MMM YY',
nextWeek: 'ddd, Do MMM YY',
sameElse: 'ddd, Do MMM YY'
}
})

View file

@ -0,0 +1,123 @@
import React from 'react'
import { expect } from 'chai'
import { screen, render } from '@testing-library/react'
import ChatPane from '../../../../../frontend/js/features/chat/components/chat-pane'
import {
stubGlobalUser,
stubMathJax,
stubUIConfig,
tearDownGlobalUserStub,
tearDownMathJaxStubs,
tearDownUIConfigStubs
} from './stubs'
describe('<ChatPane />', function() {
const currentUser = {
id: 'fake_user',
first_name: 'fake_user_first_name',
email: 'fake@example.com'
}
function createMessages() {
return [
{
contents: ['a message'],
user: currentUser,
timestamp: new Date()
},
{
contents: ['another message'],
user: currentUser,
timestamp: new Date()
}
]
}
before(function() {
stubGlobalUser(currentUser) // required by ColorManager
stubUIConfig()
stubMathJax()
})
after(function() {
tearDownGlobalUserStub()
tearDownUIConfigStubs()
tearDownMathJaxStubs()
})
it('renders multiple messages', function() {
render(
<ChatPane
loadMoreMessages={() => {}}
sendMessage={() => {}}
userId={currentUser.id}
messages={createMessages()}
resetUnreadMessages={() => {}}
/>
)
screen.getByText('a message')
screen.getByText('another message')
})
describe('loading spinner', function() {
it('is rendered while the messages is loading', function() {
render(
<ChatPane
loading
loadMoreMessages={() => {}}
sendMessage={() => {}}
userId={currentUser.id}
messages={createMessages()}
resetUnreadMessages={() => {}}
/>
)
screen.getByText('Loading…')
})
it('is not rendered when the messages are not loading', function() {
render(
<ChatPane
loading={false}
loadMoreMessages={() => {}}
sendMessage={() => {}}
userId={currentUser.id}
messages={createMessages()}
resetUnreadMessages={() => {}}
/>
)
})
expect(screen.queryByText('Loading…')).to.not.exist
})
describe('"send your first message" placeholder', function() {
it('is rendered when there are no messages ', function() {
render(
<ChatPane
loadMoreMessages={() => {}}
sendMessage={() => {}}
userId={currentUser.id}
messages={[]}
resetUnreadMessages={() => {}}
/>
)
screen.getByText('Send your first message to your collaborators')
})
it('is not rendered when there are some messages', function() {
render(
<ChatPane
loading={false}
loadMoreMessages={() => {}}
sendMessage={() => {}}
userId={currentUser.id}
messages={createMessages()}
resetUnreadMessages={() => {}}
/>
)
})
expect(screen.queryByText('Send your first message to your collaborators'))
.to.not.exist
})
})

View file

@ -0,0 +1,56 @@
import { expect } from 'chai'
import React from 'react'
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.getByPlaceholderText('Your Message…')
})
it('sends a message after typing and hitting enter', function() {
render(
<MessageInput
sendMessage={sendMessage}
resetUnreadMessages={resetUnreadMessages}
/>
)
const input = screen.getByPlaceholderText('Your Message…')
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('Your Message…')
fireEvent.click(input)
expect(resetUnreadMessages).to.be.calledOnce
})
})

View file

@ -0,0 +1,110 @@
import React from 'react'
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 {
stubGlobalUser,
stubMathJax,
stubUIConfig,
tearDownGlobalUserStub,
tearDownMathJaxStubs,
tearDownUIConfigStubs
} from './stubs'
describe('<MessageList />', function() {
const currentUser = {
id: 'fake_user',
first_name: 'fake_user_first_name',
email: 'fake@example.com'
}
function createMessages() {
return [
{
contents: ['a message'],
user: currentUser,
timestamp: new Date()
},
{
contents: ['another message'],
user: currentUser,
timestamp: new Date()
}
]
}
before(function() {
stubGlobalUser(currentUser) // required by ColorManager
stubUIConfig()
stubMathJax()
})
after(function() {
tearDownGlobalUserStub()
tearDownUIConfigStubs()
tearDownMathJaxStubs()
})
it('renders multiple messages', function() {
render(
<MessageList
userId={currentUser.id}
messages={createMessages()}
resetUnreadMessages={() => {}}
/>
)
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)
msgs[1].timestamp = new Date(2019, 6, 3, 4, 27)
render(
<MessageList
userId={currentUser.id}
messages={msgs}
resetUnreadMessages={() => {}}
/>
)
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)
msgs[1].timestamp = new Date(2019, 6, 3, 4, 31)
render(
<MessageList
userId={currentUser.id}
messages={msgs}
resetUnreadMessages={() => {}}
/>
)
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(
<MessageList
userId={currentUser.id}
messages={createMessages()}
resetUnreadMessages={resetUnreadMessages}
/>
)
fireEvent.click(screen.getByRole('list'))
expect(resetUnreadMessages).to.be.calledOnce
})
})

View file

@ -0,0 +1,116 @@
import { expect } from 'chai'
import React from 'react'
import { screen, render } from '@testing-library/react'
import Message from '../../../../../frontend/js/features/chat/components/message'
import {
stubGlobalUser,
stubMathJax,
stubUIConfig,
tearDownGlobalUserStub,
tearDownMathJaxStubs,
tearDownUIConfigStubs
} from './stubs'
describe('<Message />', function() {
const currentUser = {
id: 'fake_user',
first_name: 'fake_user_first_name',
email: 'fake@example.com'
}
before(function() {
stubGlobalUser(currentUser) // required by ColorManager
stubUIConfig()
stubMathJax()
})
after(function() {
tearDownGlobalUserStub()
tearDownUIConfigStubs()
tearDownMathJaxStubs()
})
it('renders a basic message', function() {
const message = {
contents: ['a message'],
user: currentUser
}
render(<Message userId={currentUser.id} message={message} />)
screen.getByText('a message')
})
it('renders a message with multiple contents', function() {
const message = {
contents: ['a message', 'another message'],
user: currentUser
}
render(<Message userId={currentUser.id} message={message} />)
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 userId={currentUser.id} message={message} />)
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 userId={currentUser.id} message={message} />)
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 userId={currentUser.id} 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 userId={currentUser.id} message={msg} />)
expect(screen.queryByText(otherUser.first_name)).to.not.exist
screen.getByText(msg.user.email)
})
})
})

View file

@ -0,0 +1,35 @@
import sinon from 'sinon'
export function stubUIConfig() {
window.uiConfig = {
chatMessageBorderSaturation: '85%',
chatMessageBorderLightness: '40%',
chatMessageBgSaturation: '85%',
chatMessageBgLightness: '40%'
}
}
export function tearDownUIConfigStubs() {
delete window.uiConfig
}
export function stubMathJax() {
window.MathJax = {
Hub: {
Queue: sinon.stub(),
config: { tex2jax: { inlineMath: [['$', '$']] } }
}
}
}
export function tearDownMathJaxStubs() {
delete window.MathJax
}
export function stubGlobalUser(user) {
window.user = user
}
export function tearDownGlobalUserStub() {
delete window.user
}