mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #3436 from overleaf/msm-react-shared-context
React shared context GitOrigin-RevId: ebc6fa90dd8c65ddf803fd457c99a30f0e8e3c9c
This commit is contained in:
parent
c446f7712c
commit
1fcf94c3b9
18 changed files with 232 additions and 65 deletions
|
@ -7,6 +7,8 @@ block vars
|
||||||
|
|
||||||
block content
|
block content
|
||||||
.editor(ng-controller="IdeController").full-size
|
.editor(ng-controller="IdeController").full-size
|
||||||
|
//- required by react2angular-shared-context, must be rendered as a top level component
|
||||||
|
shared-context-react()
|
||||||
.loading-screen(ng-if="state.loading")
|
.loading-screen(ng-if="state.loading")
|
||||||
.loading-screen-brand-container
|
.loading-screen-brand-container
|
||||||
.loading-screen-brand(
|
.loading-screen-brand(
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import App from '../../../base'
|
import App from '../../../base'
|
||||||
import { react2angular } from 'react2angular'
|
import { react2angular } from 'react2angular'
|
||||||
|
import { rootContext } from '../../../shared/context/root-context'
|
||||||
|
|
||||||
import ChatPane from '../components/chat-pane'
|
import ChatPane from '../components/chat-pane'
|
||||||
|
|
||||||
|
@ -8,4 +9,7 @@ App.controller('ReactChatController', function($scope, ide) {
|
||||||
ide.$scope.$broadcast('chat:resetUnreadMessages')
|
ide.$scope.$broadcast('chat:resetUnreadMessages')
|
||||||
})
|
})
|
||||||
|
|
||||||
App.component('chat', react2angular(ChatPane))
|
App.component(
|
||||||
|
'chat',
|
||||||
|
react2angular(rootContext.use(ChatPane), ['resetUnreadMessages'])
|
||||||
|
)
|
||||||
|
|
|
@ -1,37 +1,38 @@
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect, useRef } from 'react'
|
||||||
import { ChatStore } from './chat-store'
|
import { ChatStore } from './chat-store'
|
||||||
|
import { useApplicationContext } from '../../../shared/context/application-context'
|
||||||
let chatStore
|
import { useEditorContext } from '../../../shared/context/editor-context'
|
||||||
|
|
||||||
export function resetChatStore() {
|
|
||||||
chatStore = undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useChatStore() {
|
export function useChatStore() {
|
||||||
if (!chatStore) {
|
const { user } = useApplicationContext()
|
||||||
chatStore = new ChatStore()
|
const { projectId } = useEditorContext()
|
||||||
|
|
||||||
|
const chatStoreRef = useRef(new ChatStore(user, projectId))
|
||||||
|
|
||||||
|
const [atEnd, setAtEnd] = useState(chatStoreRef.current.atEnd)
|
||||||
|
const [loading, setLoading] = useState(chatStoreRef.current.loading)
|
||||||
|
const [messages, setMessages] = useState(chatStoreRef.current.messages)
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => {
|
||||||
|
const chatStore = chatStoreRef.current
|
||||||
|
function handleStoreUpdated() {
|
||||||
|
setAtEnd(chatStore.atEnd)
|
||||||
|
setLoading(chatStore.loading)
|
||||||
|
setMessages(chatStore.messages)
|
||||||
|
}
|
||||||
|
chatStore.on('updated', handleStoreUpdated)
|
||||||
|
return () => chatStore.destroy()
|
||||||
|
},
|
||||||
|
[chatStoreRef]
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
userId: user.id,
|
||||||
|
atEnd,
|
||||||
|
loading,
|
||||||
|
messages,
|
||||||
|
loadMoreMessages: () => chatStoreRef.current.loadMoreMessages(),
|
||||||
|
sendMessage: message => chatStoreRef.current.sendMessage(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStateFromStore() {
|
|
||||||
return {
|
|
||||||
userId: window.user.id,
|
|
||||||
atEnd: chatStore.atEnd,
|
|
||||||
loading: chatStore.loading,
|
|
||||||
messages: chatStore.messages,
|
|
||||||
loadMoreMessages: () => chatStore.loadMoreMessages(),
|
|
||||||
sendMessage: message => chatStore.sendMessage(message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const [storeState, setStoreState] = useState(getStateFromStore())
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
function handleStoreUpdated() {
|
|
||||||
setStoreState(getStateFromStore())
|
|
||||||
}
|
|
||||||
chatStore.on('updated', handleStoreUpdated)
|
|
||||||
return () => chatStore.off('updated', handleStoreUpdated)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return storeState
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,19 +5,21 @@ import { getJSON, postJSON } from '../../../infrastructure/fetch-json'
|
||||||
export const MESSAGE_LIMIT = 50
|
export const MESSAGE_LIMIT = 50
|
||||||
|
|
||||||
export class ChatStore {
|
export class ChatStore {
|
||||||
constructor() {
|
constructor(user, projectId) {
|
||||||
this.messages = []
|
this.messages = []
|
||||||
this.loading = false
|
this.loading = false
|
||||||
this.atEnd = false
|
this.atEnd = false
|
||||||
|
|
||||||
|
this._user = user
|
||||||
|
this._projectId = projectId
|
||||||
this._nextBeforeTimestamp = null
|
this._nextBeforeTimestamp = null
|
||||||
this._justSent = false
|
this._justSent = false
|
||||||
|
|
||||||
this._emitter = new EventEmitter()
|
this._emitter = new EventEmitter()
|
||||||
|
|
||||||
window._ide.socket.on('new-chat-message', message => {
|
this._onNewChatMessage = message => {
|
||||||
const messageIsFromSelf =
|
const messageIsFromSelf =
|
||||||
message && message.user && message.user.id === window.user.id
|
message && message.user && message.user.id === this._user.id
|
||||||
if (!messageIsFromSelf || !this._justSent) {
|
if (!messageIsFromSelf || !this._justSent) {
|
||||||
this.messages = appendMessage(this.messages, message)
|
this.messages = appendMessage(this.messages, message)
|
||||||
this._emitter.emit('updated')
|
this._emitter.emit('updated')
|
||||||
|
@ -27,7 +29,14 @@ export class ChatStore {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
this._justSent = false
|
this._justSent = false
|
||||||
})
|
}
|
||||||
|
|
||||||
|
window._ide.socket.on('new-chat-message', this._onNewChatMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
window._ide.socket.off('new-chat-message', this._onNewChatMessage)
|
||||||
|
this._emitter.off() // removes all listeners
|
||||||
}
|
}
|
||||||
|
|
||||||
on(event, fn) {
|
on(event, fn) {
|
||||||
|
@ -76,11 +85,11 @@ export class ChatStore {
|
||||||
}
|
}
|
||||||
this._justSent = true
|
this._justSent = true
|
||||||
this.messages = appendMessage(this.messages, {
|
this.messages = appendMessage(this.messages, {
|
||||||
user: window.user,
|
user: this._user,
|
||||||
content: message,
|
content: message,
|
||||||
timestamp: Date.now()
|
timestamp: Date.now()
|
||||||
})
|
})
|
||||||
const url = `/project/${window.project_id}/messages`
|
const url = `/project/${this._projectId}/messages`
|
||||||
this._emitter.emit('updated')
|
this._emitter.emit('updated')
|
||||||
return postJSON(url, { body })
|
return postJSON(url, { body })
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,11 +7,11 @@ import OutlineRoot from './outline-root'
|
||||||
import Icon from '../../../shared/components/icon'
|
import Icon from '../../../shared/components/icon'
|
||||||
import localStorage from '../../../infrastructure/local-storage'
|
import localStorage from '../../../infrastructure/local-storage'
|
||||||
import withErrorBoundary from '../../../infrastructure/error-boundary'
|
import withErrorBoundary from '../../../infrastructure/error-boundary'
|
||||||
|
import { useEditorContext } from '../../../shared/context/editor-context'
|
||||||
|
|
||||||
function OutlinePane({
|
function OutlinePane({
|
||||||
isTexFile,
|
isTexFile,
|
||||||
outline,
|
outline,
|
||||||
projectId,
|
|
||||||
jumpToLine,
|
jumpToLine,
|
||||||
onToggle,
|
onToggle,
|
||||||
eventTracking,
|
eventTracking,
|
||||||
|
@ -19,6 +19,8 @@ function OutlinePane({
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const { projectId } = useEditorContext()
|
||||||
|
|
||||||
const storageKey = `file_outline.expanded.${projectId}`
|
const storageKey = `file_outline.expanded.${projectId}`
|
||||||
const [expanded, setExpanded] = useState(() => {
|
const [expanded, setExpanded] = useState(() => {
|
||||||
const storedExpandedState = localStorage.getItem(storageKey) !== false
|
const storedExpandedState = localStorage.getItem(storageKey) !== false
|
||||||
|
@ -77,7 +79,6 @@ function OutlinePane({
|
||||||
OutlinePane.propTypes = {
|
OutlinePane.propTypes = {
|
||||||
isTexFile: PropTypes.bool.isRequired,
|
isTexFile: PropTypes.bool.isRequired,
|
||||||
outline: PropTypes.array.isRequired,
|
outline: PropTypes.array.isRequired,
|
||||||
projectId: PropTypes.string.isRequired,
|
|
||||||
jumpToLine: PropTypes.func.isRequired,
|
jumpToLine: PropTypes.func.isRequired,
|
||||||
onToggle: PropTypes.func.isRequired,
|
onToggle: PropTypes.func.isRequired,
|
||||||
eventTracking: PropTypes.object.isRequired,
|
eventTracking: PropTypes.object.isRequired,
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import App from '../../../base'
|
import App from '../../../base'
|
||||||
import OutlinePane from '../components/outline-pane'
|
import OutlinePane from '../components/outline-pane'
|
||||||
import { react2angular } from 'react2angular'
|
import { react2angular } from 'react2angular'
|
||||||
|
import { rootContext } from '../../../shared/context/root-context'
|
||||||
|
|
||||||
App.controller('OutlineController', function($scope, ide, eventTracking) {
|
App.controller('OutlineController', function($scope, ide, eventTracking) {
|
||||||
$scope.isTexFile = false
|
$scope.isTexFile = false
|
||||||
|
@ -30,4 +31,14 @@ App.controller('OutlineController', function($scope, ide, eventTracking) {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Wrap React component as Angular component. Only needed for "top-level" component
|
// Wrap React component as Angular component. Only needed for "top-level" component
|
||||||
App.component('outlinePane', react2angular(OutlinePane))
|
App.component(
|
||||||
|
'outlinePane',
|
||||||
|
react2angular(rootContext.use(OutlinePane), [
|
||||||
|
'outline',
|
||||||
|
'jumpToLine',
|
||||||
|
'highlightedLine',
|
||||||
|
'eventTracking',
|
||||||
|
'onToggle',
|
||||||
|
'isTexFile'
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
|
@ -61,6 +61,10 @@ import './main/account-upgrade'
|
||||||
import './main/exposed-settings'
|
import './main/exposed-settings'
|
||||||
import './main/system-messages'
|
import './main/system-messages'
|
||||||
import '../../modules/modules-ide.js'
|
import '../../modules/modules-ide.js'
|
||||||
|
|
||||||
|
import { react2angular } from 'react2angular'
|
||||||
|
import { rootContext } from './shared/context/root-context'
|
||||||
|
|
||||||
App.controller('IdeController', function(
|
App.controller('IdeController', function(
|
||||||
$scope,
|
$scope,
|
||||||
$timeout,
|
$timeout,
|
||||||
|
@ -349,6 +353,10 @@ If the project has been renamed please look in your project list for a new proje
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// required by react2angular-shared-context, maps the shared context instance to an angular component
|
||||||
|
// that must be rendered in the app
|
||||||
|
App.component('sharedContextReact', react2angular(rootContext.component))
|
||||||
|
|
||||||
export default angular.bootstrap(document.body, ['SharelatexApp'])
|
export default angular.bootstrap(document.body, ['SharelatexApp'])
|
||||||
|
|
||||||
function __guard__(value, transform) {
|
function __guard__(value, transform) {
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
import React, { createContext, useContext } from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
export const ApplicationContext = createContext()
|
||||||
|
|
||||||
|
export function ApplicationProvider({ children }) {
|
||||||
|
return (
|
||||||
|
<ApplicationContext.Provider
|
||||||
|
value={{
|
||||||
|
user: window.user
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ApplicationContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplicationProvider.propTypes = {
|
||||||
|
children: PropTypes.any
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useApplicationContext() {
|
||||||
|
const { user } = useContext(ApplicationContext)
|
||||||
|
return {
|
||||||
|
user
|
||||||
|
}
|
||||||
|
}
|
27
services/web/frontend/js/shared/context/editor-context.js
Normal file
27
services/web/frontend/js/shared/context/editor-context.js
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import React, { createContext, useContext } from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
export const EditorContext = createContext()
|
||||||
|
|
||||||
|
export function EditorProvider({ children }) {
|
||||||
|
return (
|
||||||
|
<EditorContext.Provider
|
||||||
|
value={{
|
||||||
|
projectId: window.project_id
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</EditorContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorProvider.propTypes = {
|
||||||
|
children: PropTypes.any
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useEditorContext() {
|
||||||
|
const { projectId } = useContext(EditorContext)
|
||||||
|
return {
|
||||||
|
projectId
|
||||||
|
}
|
||||||
|
}
|
15
services/web/frontend/js/shared/context/root-context.js
Normal file
15
services/web/frontend/js/shared/context/root-context.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { ApplicationProvider } from './application-context'
|
||||||
|
import { EditorProvider } from './editor-context'
|
||||||
|
import createSharedContext from 'react2angular-shared-context'
|
||||||
|
|
||||||
|
// eslint-disable-next-line react/prop-types
|
||||||
|
export function ContextRoot({ children }) {
|
||||||
|
return (
|
||||||
|
<ApplicationProvider>
|
||||||
|
<EditorProvider>{children}</EditorProvider>
|
||||||
|
</ApplicationProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const rootContext = createSharedContext(ContextRoot)
|
|
@ -1,6 +1,9 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import OutlinePane from '../js/features/outline/components/outline-pane'
|
import OutlinePane from '../js/features/outline/components/outline-pane'
|
||||||
|
import { ContextRoot } from '../js/shared/context/root-context'
|
||||||
|
|
||||||
|
window.project_id = '1234'
|
||||||
|
|
||||||
export const Basic = args => <OutlinePane {...args} />
|
export const Basic = args => <OutlinePane {...args} />
|
||||||
Basic.args = {
|
Basic.args = {
|
||||||
|
@ -47,11 +50,17 @@ export default {
|
||||||
jumpToLine: { action: 'jumpToLine' }
|
jumpToLine: { action: 'jumpToLine' }
|
||||||
},
|
},
|
||||||
args: {
|
args: {
|
||||||
projectId: '1234',
|
|
||||||
eventTracking: { sendMB: () => {} },
|
eventTracking: { sendMB: () => {} },
|
||||||
isTexFile: true,
|
isTexFile: true,
|
||||||
outline: [],
|
outline: [],
|
||||||
jumpToLine: () => {},
|
jumpToLine: () => {},
|
||||||
onToggle: () => {}
|
onToggle: () => {}
|
||||||
}
|
},
|
||||||
|
decorators: [
|
||||||
|
Story => (
|
||||||
|
<ContextRoot>
|
||||||
|
<Story />
|
||||||
|
</ContextRoot>
|
||||||
|
)
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
17
services/web/package-lock.json
generated
17
services/web/package-lock.json
generated
|
@ -23924,7 +23924,7 @@
|
||||||
},
|
},
|
||||||
"mkdirp": {
|
"mkdirp": {
|
||||||
"version": "0.5.1",
|
"version": "0.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
|
"resolved": false,
|
||||||
"integrity": "sha512-SknJC52obPfGQPnjIkXbmA6+5H15E+fR+E4iR2oQ3zzCLbd7/ONua69R/Gw7AgkTLsRG+r5fzksYwWe1AgTyWA==",
|
"integrity": "sha512-SknJC52obPfGQPnjIkXbmA6+5H15E+fR+E4iR2oQ3zzCLbd7/ONua69R/Gw7AgkTLsRG+r5fzksYwWe1AgTyWA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"minimist": "0.0.8"
|
"minimist": "0.0.8"
|
||||||
|
@ -30003,6 +30003,21 @@
|
||||||
"ngcomponent": "^4.1.0"
|
"ngcomponent": "^4.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"react2angular-shared-context": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react2angular-shared-context/-/react2angular-shared-context-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-8es9/tSf0XxbJ6f/58OPVrNUKjzY370WJXhMo5e4klxaEW0obLK2MRidIT8aY1ZZAf/x9EAGlBFJlvrxhu0H0Q==",
|
||||||
|
"requires": {
|
||||||
|
"uuid": "^7.0.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"uuid": {
|
||||||
|
"version": "7.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz",
|
||||||
|
"integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"reactcss": {
|
"reactcss": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz",
|
||||||
|
|
|
@ -133,6 +133,7 @@
|
||||||
"react-i18next": "^11.7.1",
|
"react-i18next": "^11.7.1",
|
||||||
"react-linkify": "^1.0.0-alpha",
|
"react-linkify": "^1.0.0-alpha",
|
||||||
"react2angular": "^4.0.6",
|
"react2angular": "^4.0.6",
|
||||||
|
"react2angular-shared-context": "^1.1.0",
|
||||||
"request": "^2.88.2",
|
"request": "^2.88.2",
|
||||||
"request-promise-native": "^1.0.8",
|
"request-promise-native": "^1.0.8",
|
||||||
"requestretry": "^1.13.0",
|
"requestretry": "^1.13.0",
|
||||||
|
|
|
@ -1,13 +1,10 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import {
|
import { screen, waitForElementToBeRemoved } from '@testing-library/react'
|
||||||
render,
|
|
||||||
screen,
|
|
||||||
waitForElementToBeRemoved
|
|
||||||
} from '@testing-library/react'
|
|
||||||
import fetchMock from 'fetch-mock'
|
import fetchMock from 'fetch-mock'
|
||||||
|
|
||||||
import ChatPane from '../../../../../frontend/js/features/chat/components/chat-pane'
|
import ChatPane from '../../../../../frontend/js/features/chat/components/chat-pane'
|
||||||
|
import { renderWithEditorContext } from '../../../helpers/render-with-context'
|
||||||
import {
|
import {
|
||||||
stubChatStore,
|
stubChatStore,
|
||||||
stubMathJax,
|
stubMathJax,
|
||||||
|
@ -53,31 +50,44 @@ describe('<ChatPane />', function() {
|
||||||
|
|
||||||
it('renders multiple messages', async function() {
|
it('renders multiple messages', async function() {
|
||||||
fetchMock.get(/messages/, testMessages)
|
fetchMock.get(/messages/, testMessages)
|
||||||
render(<ChatPane resetUnreadMessages={() => {}} />)
|
// unmounting before `beforeEach` block is executed is required to prevent cleanup errors
|
||||||
|
const { unmount } = renderWithEditorContext(
|
||||||
|
<ChatPane resetUnreadMessages={() => {}} />
|
||||||
|
)
|
||||||
|
|
||||||
await screen.findByText('a message')
|
await screen.findByText('a message')
|
||||||
await screen.findByText('another message')
|
await screen.findByText('another message')
|
||||||
|
unmount()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('A loading spinner is rendered while the messages are loading, then disappears', async function() {
|
it('A loading spinner is rendered while the messages are loading, then disappears', async function() {
|
||||||
fetchMock.get(/messages/, [])
|
fetchMock.get(/messages/, [])
|
||||||
render(<ChatPane resetUnreadMessages={() => {}} />)
|
const { unmount } = renderWithEditorContext(
|
||||||
|
<ChatPane resetUnreadMessages={() => {}} />
|
||||||
|
)
|
||||||
await waitForElementToBeRemoved(() => screen.getByText('Loading…'))
|
await waitForElementToBeRemoved(() => screen.getByText('Loading…'))
|
||||||
|
unmount()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('"send your first message" placeholder', function() {
|
describe('"send your first message" placeholder', function() {
|
||||||
it('is rendered when there are no messages ', async function() {
|
it('is rendered when there are no messages ', async function() {
|
||||||
fetchMock.get(/messages/, [])
|
fetchMock.get(/messages/, [])
|
||||||
render(<ChatPane resetUnreadMessages={() => {}} />)
|
const { unmount } = renderWithEditorContext(
|
||||||
|
<ChatPane resetUnreadMessages={() => {}} />
|
||||||
|
)
|
||||||
await screen.findByText('Send your first message to your collaborators')
|
await screen.findByText('Send your first message to your collaborators')
|
||||||
|
unmount()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('is not rendered when messages are displayed', function() {
|
it('is not rendered when messages are displayed', function() {
|
||||||
fetchMock.get(/messages/, testMessages)
|
fetchMock.get(/messages/, testMessages)
|
||||||
render(<ChatPane resetUnreadMessages={() => {}} />)
|
const { unmount } = renderWithEditorContext(
|
||||||
|
<ChatPane resetUnreadMessages={() => {}} />
|
||||||
|
)
|
||||||
expect(
|
expect(
|
||||||
screen.queryByText('Send your first message to your collaborators')
|
screen.queryByText('Send your first message to your collaborators')
|
||||||
).to.not.exist
|
).to.not.exist
|
||||||
|
unmount()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import sinon from 'sinon'
|
import sinon from 'sinon'
|
||||||
import { resetChatStore } from '../../../../../frontend/js/features/chat/store/chat-store-effect'
|
|
||||||
|
|
||||||
export function stubUIConfig() {
|
export function stubUIConfig() {
|
||||||
window.uiConfig = {
|
window.uiConfig = {
|
||||||
|
@ -30,7 +29,6 @@ export function tearDownMathJaxStubs() {
|
||||||
export function stubChatStore({ user }) {
|
export function stubChatStore({ user }) {
|
||||||
window._ide = { socket: { on: sinon.stub(), off: sinon.stub() } }
|
window._ide = { socket: { on: sinon.stub(), off: sinon.stub() } }
|
||||||
window.user = user
|
window.user = user
|
||||||
resetChatStore()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function tearDownChatStore() {
|
export function tearDownChatStore() {
|
||||||
|
|
|
@ -13,6 +13,8 @@ describe('ChatStore', function() {
|
||||||
id: '123abc'
|
id: '123abc'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const testProjectId = 'project-123'
|
||||||
|
|
||||||
const testMessage = {
|
const testMessage = {
|
||||||
content: 'hello',
|
content: 'hello',
|
||||||
timestamp: new Date().getTime(),
|
timestamp: new Date().getTime(),
|
||||||
|
@ -22,15 +24,13 @@ describe('ChatStore', function() {
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
fetchMock.reset()
|
fetchMock.reset()
|
||||||
|
|
||||||
window.user = user
|
|
||||||
window.project_id = 'project-123'
|
|
||||||
window.csrfToken = 'csrf_tok'
|
window.csrfToken = 'csrf_tok'
|
||||||
|
|
||||||
socket = { on: sinon.stub() }
|
socket = { on: sinon.stub(), off: sinon.stub() }
|
||||||
window._ide = { socket }
|
window._ide = { socket }
|
||||||
mockSocketMessage = message => socket.on.getCall(0).args[1](message)
|
mockSocketMessage = message => socket.on.getCall(0).args[1](message)
|
||||||
|
|
||||||
store = new ChatStore()
|
store = new ChatStore(user, testProjectId)
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(function() {
|
afterEach(function() {
|
||||||
|
@ -216,4 +216,18 @@ describe('ChatStore', function() {
|
||||||
expect(subscriber).not.to.be.called
|
expect(subscriber).not.to.be.called
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('destroy', function() {
|
||||||
|
beforeEach(function() {
|
||||||
|
fetchMock.post(/messages/, 204)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('removes event listeners', async function() {
|
||||||
|
const subscriber = sinon.stub()
|
||||||
|
store.on('updated', subscriber)
|
||||||
|
store.destroy()
|
||||||
|
await store.sendMessage('a message')
|
||||||
|
expect(subscriber).not.to.be.called
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,16 +1,20 @@
|
||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import sinon from 'sinon'
|
import sinon from 'sinon'
|
||||||
import { screen, render, fireEvent } from '@testing-library/react'
|
import { screen, fireEvent } from '@testing-library/react'
|
||||||
|
|
||||||
import OutlinePane from '../../../../../frontend/js/features/outline/components/outline-pane'
|
import OutlinePane from '../../../../../frontend/js/features/outline/components/outline-pane'
|
||||||
|
import { renderWithEditorContext } from '../../../helpers/render-with-context'
|
||||||
|
|
||||||
describe('<OutlinePane />', function() {
|
describe('<OutlinePane />', function() {
|
||||||
const jumpToLine = () => {}
|
const jumpToLine = () => {}
|
||||||
const projectId = '123abc'
|
|
||||||
const onToggle = sinon.stub()
|
const onToggle = sinon.stub()
|
||||||
const eventTracking = { sendMB: sinon.stub() }
|
const eventTracking = { sendMB: sinon.stub() }
|
||||||
|
|
||||||
|
function render(children) {
|
||||||
|
renderWithEditorContext(children, { projectId: '123abc' })
|
||||||
|
}
|
||||||
|
|
||||||
before(function() {
|
before(function() {
|
||||||
global.localStorage = {
|
global.localStorage = {
|
||||||
getItem: sinon.stub().returns(null),
|
getItem: sinon.stub().returns(null),
|
||||||
|
@ -41,7 +45,6 @@ describe('<OutlinePane />', function() {
|
||||||
<OutlinePane
|
<OutlinePane
|
||||||
isTexFile
|
isTexFile
|
||||||
outline={outline}
|
outline={outline}
|
||||||
projectId={projectId}
|
|
||||||
jumpToLine={jumpToLine}
|
jumpToLine={jumpToLine}
|
||||||
onToggle={onToggle}
|
onToggle={onToggle}
|
||||||
eventTracking={eventTracking}
|
eventTracking={eventTracking}
|
||||||
|
@ -57,7 +60,6 @@ describe('<OutlinePane />', function() {
|
||||||
<OutlinePane
|
<OutlinePane
|
||||||
isTexFile={false}
|
isTexFile={false}
|
||||||
outline={outline}
|
outline={outline}
|
||||||
projectId={projectId}
|
|
||||||
jumpToLine={jumpToLine}
|
jumpToLine={jumpToLine}
|
||||||
onToggle={onToggle}
|
onToggle={onToggle}
|
||||||
eventTracking={eventTracking}
|
eventTracking={eventTracking}
|
||||||
|
@ -80,7 +82,6 @@ describe('<OutlinePane />', function() {
|
||||||
<OutlinePane
|
<OutlinePane
|
||||||
isTexFile
|
isTexFile
|
||||||
outline={outline}
|
outline={outline}
|
||||||
projectId={projectId}
|
|
||||||
jumpToLine={jumpToLine}
|
jumpToLine={jumpToLine}
|
||||||
onToggle={onToggle}
|
onToggle={onToggle}
|
||||||
eventTracking={eventTracking}
|
eventTracking={eventTracking}
|
||||||
|
|
14
services/web/test/frontend/helpers/render-with-context.js
Normal file
14
services/web/test/frontend/helpers/render-with-context.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { render } from '@testing-library/react'
|
||||||
|
import { ApplicationProvider } from '../../../frontend/js/shared/context/application-context'
|
||||||
|
import { EditorProvider } from '../../../frontend/js/shared/context/editor-context'
|
||||||
|
|
||||||
|
export function renderWithEditorContext(children, { user, projectId } = {}) {
|
||||||
|
window.user = user || window.user
|
||||||
|
window.project_id = projectId != null ? projectId : window.project_id
|
||||||
|
return render(
|
||||||
|
<ApplicationProvider>
|
||||||
|
<EditorProvider>{children}</EditorProvider>
|
||||||
|
</ApplicationProvider>
|
||||||
|
)
|
||||||
|
}
|
Loading…
Reference in a new issue