editor-providers.jsx 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. // Disable prop type checks for test harnesses
  2. /* eslint-disable react/prop-types */
  3. import sinon from 'sinon'
  4. import { get, merge } from 'lodash'
  5. import { SocketIOMock } from '@/ide/connection/SocketIoShim'
  6. import { IdeContext } from '@/shared/context/ide-context'
  7. import React, { useEffect, useState } from 'react'
  8. import {
  9. createReactScopeValueStore,
  10. IdeReactContext,
  11. } from '@/features/ide-react/context/ide-react-context'
  12. import { IdeEventEmitter } from '@/features/ide-react/create-ide-event-emitter'
  13. import { ReactScopeEventEmitter } from '@/features/ide-react/scope-event-emitter/react-scope-event-emitter'
  14. import { ConnectionContext } from '@/features/ide-react/context/connection-context'
  15. import { ReactContextRoot } from '@/features/ide-react/context/react-context-root'
  16. // these constants can be imported in tests instead of
  17. // using magic strings
  18. export const PROJECT_ID = 'project123'
  19. export const PROJECT_NAME = 'project-name'
  20. export const USER_ID = '123abd'
  21. export const USER_EMAIL = 'testuser@example.com'
  22. const defaultUserSettings = {
  23. pdfViewer: 'pdfjs',
  24. fontSize: 12,
  25. fontFamily: 'monaco',
  26. lineHeight: 'normal',
  27. editorTheme: 'textmate',
  28. overallTheme: '',
  29. mode: 'default',
  30. autoComplete: true,
  31. autoPairDelimiters: true,
  32. trackChanges: true,
  33. syntaxValidation: false,
  34. mathPreview: true,
  35. }
  36. export function EditorProviders({
  37. user = { id: USER_ID, email: USER_EMAIL },
  38. projectId = PROJECT_ID,
  39. projectOwner = {
  40. _id: '124abd',
  41. email: 'owner@example.com',
  42. },
  43. rootDocId = '_root_doc_id',
  44. imageName = 'texlive-full:2024.1',
  45. compiler = 'pdflatex',
  46. socket = new SocketIOMock(),
  47. isRestrictedTokenMember = false,
  48. clsiServerId = '1234',
  49. scope = {},
  50. features = {
  51. referencesSearch: true,
  52. },
  53. projectFeatures = features,
  54. permissionsLevel = 'owner',
  55. children,
  56. rootFolder = [
  57. {
  58. _id: 'root-folder-id',
  59. name: 'rootFolder',
  60. docs: [
  61. {
  62. _id: '_root_doc_id',
  63. name: 'main.tex',
  64. },
  65. ],
  66. folders: [],
  67. fileRefs: [],
  68. },
  69. ],
  70. ui = { view: 'editor', pdfLayout: 'sideBySide', chatOpen: true },
  71. fileTreeManager = {
  72. findEntityById: () => null,
  73. findEntityByPath: () => null,
  74. getEntityPath: () => '',
  75. getRootDocDirname: () => '',
  76. getPreviewByPath: path => ({ url: path, extension: 'png' }),
  77. },
  78. editorManager = {
  79. getCurrentDocumentId: () => 'foo',
  80. getCurrentDocValue: () => {},
  81. openDoc: sinon.stub(),
  82. },
  83. userSettings = {},
  84. providers = {},
  85. }) {
  86. window.metaAttributesCache.set(
  87. 'ol-gitBridgePublicBaseUrl',
  88. 'https://git.overleaf.test'
  89. )
  90. window.metaAttributesCache.set(
  91. 'ol-isRestrictedTokenMember',
  92. isRestrictedTokenMember
  93. )
  94. window.metaAttributesCache.set(
  95. 'ol-userSettings',
  96. merge({}, defaultUserSettings, userSettings)
  97. )
  98. const $scope = merge(
  99. {
  100. user,
  101. editor: {
  102. sharejs_doc: {
  103. doc_id: 'test-doc',
  104. getSnapshot: () => 'some doc content',
  105. hasBufferedOps: () => false,
  106. on: () => {},
  107. off: () => {},
  108. leaveAndCleanUpPromise: async () => {},
  109. },
  110. },
  111. project: {
  112. _id: projectId,
  113. name: PROJECT_NAME,
  114. owner: projectOwner,
  115. features: projectFeatures,
  116. rootDoc_id: rootDocId,
  117. rootFolder,
  118. imageName,
  119. compiler,
  120. },
  121. ui,
  122. $watch: (path, callback) => {
  123. callback(get($scope, path))
  124. return () => null
  125. },
  126. $on: sinon.stub(),
  127. $applyAsync: sinon.stub(),
  128. permissionsLevel,
  129. },
  130. scope
  131. )
  132. window._ide = {
  133. $scope,
  134. socket,
  135. clsiServerId,
  136. editorManager,
  137. fileTreeManager,
  138. }
  139. // Add details for useUserContext
  140. window.metaAttributesCache.set('ol-user', { ...user, features })
  141. window.metaAttributesCache.set('ol-project_id', projectId)
  142. return (
  143. <ReactContextRoot
  144. providers={{
  145. ConnectionProvider,
  146. IdeReactProvider,
  147. ...providers,
  148. }}
  149. >
  150. {children}
  151. </ReactContextRoot>
  152. )
  153. }
  154. const ConnectionProvider = ({ children }) => {
  155. const [value] = useState(() => ({
  156. socket: window._ide.socket,
  157. connectionState: {
  158. readyState: WebSocket.OPEN,
  159. forceDisconnected: false,
  160. inactiveDisconnect: false,
  161. reconnectAt: null,
  162. forcedDisconnectDelay: 0,
  163. lastConnectionAttempt: 0,
  164. error: '',
  165. },
  166. isConnected: true,
  167. isStillReconnecting: false,
  168. secondsUntilReconnect: () => 0,
  169. tryReconnectNow: () => {},
  170. registerUserActivity: () => {},
  171. disconnect: () => {},
  172. }))
  173. return (
  174. <ConnectionContext.Provider value={value}>
  175. {children}
  176. </ConnectionContext.Provider>
  177. )
  178. }
  179. const IdeReactProvider = ({ children }) => {
  180. const [startedFreeTrial, setStartedFreeTrial] = useState(false)
  181. const [ideReactContextValue] = useState(() => ({
  182. projectId: PROJECT_ID,
  183. eventEmitter: new IdeEventEmitter(),
  184. startedFreeTrial,
  185. setStartedFreeTrial,
  186. reportError: () => {},
  187. projectJoined: true,
  188. }))
  189. const [ideContextValue] = useState(() => {
  190. const ide = window._ide
  191. const scopeStore = createReactScopeValueStore(PROJECT_ID)
  192. for (const [key, value] of Object.entries(ide.$scope)) {
  193. // TODO: path for nested entries
  194. scopeStore.set(key, value)
  195. }
  196. scopeStore.set('editor.sharejs_doc', ide.$scope.editor.sharejs_doc)
  197. scopeStore.set('ui.chatOpen', ide.$scope.ui.chatOpen)
  198. const scopeEventEmitter = new ReactScopeEventEmitter(new IdeEventEmitter())
  199. return {
  200. ...ide,
  201. scopeStore,
  202. scopeEventEmitter,
  203. }
  204. })
  205. useEffect(() => {
  206. window.overleaf = {
  207. ...window.overleaf,
  208. unstable: {
  209. ...window.overleaf?.unstable,
  210. store: ideContextValue.scopeStore,
  211. },
  212. }
  213. }, [ideContextValue.scopeStore])
  214. return (
  215. <IdeReactContext.Provider value={ideReactContextValue}>
  216. <IdeContext.Provider value={ideContextValue}>
  217. {children}
  218. </IdeContext.Provider>
  219. </IdeReactContext.Provider>
  220. )
  221. }