chat-store.test.js 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. import { expect } from 'chai'
  2. import sinon from 'sinon'
  3. import fetchMock from 'fetch-mock'
  4. import {
  5. ChatStore,
  6. MESSAGE_LIMIT
  7. } from '../../../../../frontend/js/features/chat/store/chat-store'
  8. describe('ChatStore', function() {
  9. let store, socket, mockSocketMessage
  10. const user = {
  11. id: '123abc'
  12. }
  13. const testProjectId = 'project-123'
  14. const testMessage = {
  15. id: 'msg_1',
  16. content: 'hello',
  17. timestamp: new Date().getTime(),
  18. user
  19. }
  20. beforeEach(function() {
  21. fetchMock.reset()
  22. window.csrfToken = 'csrf_tok'
  23. socket = { on: sinon.stub(), removeListener: sinon.stub() }
  24. window._ide = { socket }
  25. mockSocketMessage = message => socket.on.getCall(0).args[1](message)
  26. store = new ChatStore(user, testProjectId)
  27. })
  28. afterEach(function() {
  29. fetchMock.restore()
  30. delete window._ide
  31. delete window.csrfToken
  32. delete window.user
  33. delete window.project_id
  34. })
  35. describe('new message events', function() {
  36. it('subscribes to the socket for new message events', function() {
  37. expect(socket.on).to.be.calledWith('new-chat-message')
  38. })
  39. it('notifies an update event after new messages are received', function() {
  40. const subscriber = sinon.stub()
  41. store.on('updated', subscriber)
  42. mockSocketMessage(testMessage)
  43. expect(subscriber).to.be.calledOnce
  44. })
  45. it('can unsubscribe from events', function() {
  46. const subscriber = sinon.stub()
  47. store.on('updated', subscriber)
  48. store.off('updated', subscriber)
  49. mockSocketMessage(testMessage)
  50. expect(subscriber).not.to.be.called
  51. })
  52. it('when the message is from other user, it is added to the messages list', function() {
  53. mockSocketMessage({ ...testMessage, id: 'other_user_msg' })
  54. expect(store.messages[store.messages.length - 1]).to.deep.equal({
  55. id: 'other_user_msg',
  56. user: testMessage.user,
  57. timestamp: testMessage.timestamp,
  58. contents: [testMessage.content]
  59. })
  60. })
  61. describe('messages sent by the user', function() {
  62. beforeEach(function() {
  63. fetchMock.post(/messages/, 204)
  64. })
  65. it('are not added to the message list', async function() {
  66. await store.sendMessage(testMessage.content)
  67. const originalMessageList = store.messages.slice(0)
  68. mockSocketMessage(testMessage)
  69. expect(originalMessageList).to.deep.equal(store.messages)
  70. // next message by a different user is added normally
  71. const otherMessage = {
  72. ...testMessage,
  73. id: 'other_user_msg',
  74. user: { id: 'other_user' },
  75. content: 'other'
  76. }
  77. mockSocketMessage(otherMessage)
  78. expect(store.messages.length).to.equal(originalMessageList.length + 1)
  79. expect(store.messages[store.messages.length - 1]).to.deep.equal({
  80. id: otherMessage.id,
  81. user: otherMessage.user,
  82. timestamp: otherMessage.timestamp,
  83. contents: [otherMessage.content]
  84. })
  85. })
  86. it("don't notify an update event after new messages are received", async function() {
  87. await store.sendMessage(testMessage.content)
  88. const subscriber = sinon.stub()
  89. store.on('updated', subscriber)
  90. mockSocketMessage(testMessage)
  91. expect(subscriber).not.to.be.called
  92. })
  93. it("have an 'id' property", async function() {
  94. await store.sendMessage(testMessage.content)
  95. expect(typeof store.messages[0].id).to.equal('string')
  96. })
  97. })
  98. })
  99. describe('loadMoreMessages()', function() {
  100. it('aborts the request when the entire message list is loaded', async function() {
  101. store.atEnd = true
  102. await store.loadMoreMessages()
  103. expect(fetchMock.calls().length).to.equal(0)
  104. expect(store.loading).to.equal(false)
  105. })
  106. it('updates the list of messages', async function() {
  107. const originalMessageList = store.messages.slice(0)
  108. fetchMock.get(/messages/, [testMessage])
  109. await store.loadMoreMessages()
  110. expect(store.messages.length).to.equal(originalMessageList.length + 1)
  111. expect(store.messages[store.messages.length - 1]).to.deep.equal({
  112. id: testMessage.id,
  113. user: testMessage.user,
  114. timestamp: testMessage.timestamp,
  115. contents: [testMessage.content]
  116. })
  117. })
  118. it('notifies an update event for when the loading starts, and a second one once data is available', async function() {
  119. const subscriber = sinon.stub()
  120. store.on('updated', subscriber)
  121. fetchMock.get(/messages/, [testMessage])
  122. await store.loadMoreMessages()
  123. expect(subscriber).to.be.calledTwice
  124. })
  125. it('marks `atEnd` flag to true when there are no more messages to retrieve', async function() {
  126. expect(store.atEnd).to.equal(false)
  127. fetchMock.get(/messages/, [testMessage])
  128. await store.loadMoreMessages()
  129. expect(store.atEnd).to.equal(true)
  130. })
  131. it('marks `atEnd` flag to false when there are still messages to retrieve', async function() {
  132. const messages = []
  133. for (let i = 0; i < MESSAGE_LIMIT; i++) {
  134. messages.push({ ...testMessage, content: `message #${i}` })
  135. }
  136. expect(store.atEnd).to.equal(false)
  137. fetchMock.get(/messages/, messages)
  138. await store.loadMoreMessages()
  139. expect(store.atEnd).to.equal(false)
  140. })
  141. it('subsequent requests for new messages start at the timestamp of the latest message', async function() {
  142. const messages = []
  143. for (let i = 0; i < MESSAGE_LIMIT - 1; i++) {
  144. // sending enough messages so it doesn't mark `atEnd === true`
  145. messages.push({ ...testMessage, content: `message #${i}` })
  146. }
  147. const timestamp = new Date().getTime()
  148. messages.push({ ...testMessage, timestamp })
  149. fetchMock.get(/messages/, messages)
  150. await store.loadMoreMessages()
  151. fetchMock.get(/messages/, [])
  152. await store.loadMoreMessages()
  153. expect(fetchMock.calls().length).to.equal(2)
  154. const url = fetchMock.lastCall()[0]
  155. expect(url).to.match(new RegExp(`&before=${timestamp}`))
  156. })
  157. })
  158. describe('sendMessage()', function() {
  159. beforeEach(function() {
  160. fetchMock.post(/messages/, 204)
  161. })
  162. it('appends the message to the list', async function() {
  163. const originalMessageList = store.messages.slice(0)
  164. await store.sendMessage('a message')
  165. expect(store.messages.length).to.equal(originalMessageList.length + 1)
  166. const lastMessage = store.messages[store.messages.length - 1]
  167. expect(lastMessage.contents).to.deep.equal(['a message'])
  168. expect(lastMessage.user).to.deep.equal(user)
  169. expect(lastMessage.timestamp).to.be.greaterThan(0)
  170. })
  171. it('notifies an update event', async function() {
  172. const subscriber = sinon.stub()
  173. store.on('updated', subscriber)
  174. await store.sendMessage('a message')
  175. expect(subscriber).to.be.calledOnce
  176. })
  177. it('sends an http POST request to the server', async function() {
  178. await store.sendMessage('a message')
  179. expect(fetchMock.calls().length).to.equal(1)
  180. const body = fetchMock.lastCall()[1].body
  181. expect(JSON.parse(body)).to.deep.equal({
  182. content: 'a message',
  183. _csrf: 'csrf_tok'
  184. })
  185. })
  186. it('ignores empty messages', async function() {
  187. const subscriber = sinon.stub()
  188. store.on('updated', subscriber)
  189. await store.sendMessage('')
  190. await store.sendMessage(null)
  191. expect(subscriber).not.to.be.called
  192. })
  193. })
  194. describe('destroy', function() {
  195. beforeEach(function() {
  196. fetchMock.post(/messages/, 204)
  197. })
  198. it('removes event listeners', async function() {
  199. const subscriber = sinon.stub()
  200. store.on('updated', subscriber)
  201. store.destroy()
  202. await store.sendMessage('a message')
  203. expect(subscriber).not.to.be.called
  204. })
  205. })
  206. })