pdf-synctex-controls.test.js 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. import PdfSynctexControls from '../../../../../frontend/js/features/pdf-preview/components/pdf-synctex-controls'
  2. import { renderWithEditorContext } from '../../../helpers/render-with-context'
  3. import sysendTestHelper from '../../../helpers/sysend'
  4. import { cloneDeep } from 'lodash'
  5. import fetchMock from 'fetch-mock'
  6. import { fireEvent, screen, waitFor } from '@testing-library/react'
  7. import fs from 'fs'
  8. import path from 'path'
  9. import { expect } from 'chai'
  10. import { useCompileContext } from '../../../../../frontend/js/shared/context/compile-context'
  11. import { useEffect } from 'react'
  12. const examplePDF = path.join(__dirname, '../fixtures/test-example.pdf')
  13. const scope = {
  14. settings: {
  15. syntaxValidation: false,
  16. pdfViewer: 'pdfjs',
  17. },
  18. editor: {
  19. sharejs_doc: {
  20. doc_id: 'test-doc',
  21. getSnapshot: () => 'some doc content',
  22. },
  23. },
  24. }
  25. const outputFiles = [
  26. {
  27. path: 'output.pdf',
  28. build: '123',
  29. url: '/build/output.pdf',
  30. type: 'pdf',
  31. },
  32. {
  33. path: 'output.log',
  34. build: '123',
  35. url: '/build/output.log',
  36. type: 'log',
  37. },
  38. ]
  39. const mockCompile = () =>
  40. fetchMock.post('express:/project/:projectId/compile', {
  41. body: {
  42. status: 'success',
  43. clsiServerId: 'foo',
  44. compileGroup: 'standard',
  45. pdfDownloadDomain: 'https://clsi.test-overleaf.com',
  46. outputFiles: cloneDeep(outputFiles),
  47. },
  48. })
  49. const fileResponses = {
  50. '/build/output.pdf': () => fs.createReadStream(examplePDF),
  51. '/build/output.log': '',
  52. }
  53. const mockBuildFile = () =>
  54. fetchMock.get('begin:https://clsi.test-overleaf.com/', _url => {
  55. const url = new URL(_url, 'https://clsi.test-overleaf.com')
  56. if (url.pathname in fileResponses) {
  57. return fileResponses[url.pathname]
  58. }
  59. return 404
  60. })
  61. const mockHighlights = [
  62. {
  63. page: 1,
  64. h: 85.03936,
  65. v: 509.999878,
  66. width: 441.921265,
  67. height: 8.855677,
  68. },
  69. {
  70. page: 1,
  71. h: 85.03936,
  72. v: 486.089539,
  73. width: 441.921265,
  74. height: 8.855677,
  75. },
  76. ]
  77. const mockSynctex = () =>
  78. fetchMock
  79. .get('express:/project/:projectId/sync/code', () => {
  80. return { pdf: cloneDeep(mockHighlights) }
  81. })
  82. .get('express:/project/:projectId/sync/pdf', () => {
  83. return { code: [{ file: 'main.tex', line: 100 }] }
  84. })
  85. describe('<PdfSynctexControls/>', function () {
  86. beforeEach(function () {
  87. window.metaAttributesCache = new Map()
  88. fetchMock.restore()
  89. mockCompile()
  90. mockSynctex()
  91. mockBuildFile()
  92. })
  93. afterEach(function () {
  94. window.metaAttributesCache = new Map()
  95. fetchMock.restore()
  96. })
  97. it('handles clicks on sync buttons', async function () {
  98. const Inner = () => {
  99. const { setPosition } = useCompileContext()
  100. // mock PDF scroll position update
  101. useEffect(() => {
  102. setPosition({
  103. page: 1,
  104. offset: { top: 10, left: 10 },
  105. pageSize: { height: 500, width: 500 },
  106. })
  107. }, [setPosition])
  108. return null
  109. }
  110. const { container } = renderWithEditorContext(
  111. <>
  112. <Inner />
  113. <PdfSynctexControls />
  114. </>,
  115. { scope }
  116. )
  117. const syncToPdfButton = await screen.findByRole('button', {
  118. name: 'Go to code location in PDF',
  119. })
  120. const syncToCodeButton = await screen.findByRole('button', {
  121. name: /Go to PDF location in code/,
  122. })
  123. expect(container.querySelectorAll('.synctex-control-icon').length).to.equal(
  124. 2
  125. )
  126. // mock editor cursor position update
  127. fireEvent(
  128. window,
  129. new CustomEvent('cursor:editor:update', {
  130. detail: { row: 100, column: 10 },
  131. })
  132. )
  133. fireEvent.click(syncToPdfButton)
  134. expect(syncToPdfButton.disabled).to.be.true
  135. await waitFor(() => {
  136. expect(fetchMock.called('express:/project/:projectId/sync/code')).to.be
  137. .true
  138. })
  139. fireEvent.click(syncToCodeButton)
  140. expect(syncToCodeButton.disabled).to.be.true
  141. await waitFor(() => {
  142. expect(fetchMock.called('express:/project/:projectId/sync/pdf')).to.be
  143. .true
  144. })
  145. })
  146. describe('with detacher role', async function () {
  147. beforeEach(function () {
  148. window.metaAttributesCache.set('ol-detachRole', 'detacher')
  149. })
  150. it('does not have go to PDF location button nor arrow icon', async function () {
  151. const { container } = renderWithEditorContext(<PdfSynctexControls />, {
  152. scope,
  153. })
  154. expect(
  155. await screen.queryByRole('button', {
  156. name: 'Go to PDF location in code',
  157. })
  158. ).to.not.exist
  159. expect(container.querySelector('.synctex-control-icon')).to.not.exist
  160. })
  161. it('send go to PDF location action', async function () {
  162. renderWithEditorContext(<PdfSynctexControls />, { scope })
  163. sysendTestHelper.resetHistory()
  164. const syncToPdfButton = await screen.findByRole('button', {
  165. name: 'Go to code location in PDF',
  166. })
  167. // mock editor cursor position update
  168. fireEvent(
  169. window,
  170. new CustomEvent('cursor:editor:update', {
  171. detail: { row: 100, column: 10 },
  172. })
  173. )
  174. fireEvent.click(syncToPdfButton)
  175. // the button is only disabled when the state is updated via sysend
  176. expect(syncToPdfButton.disabled).to.be.false
  177. expect(sysendTestHelper.getLastBroacastMessage()).to.deep.equal({
  178. role: 'detacher',
  179. event: 'action-go-to-pdf-location',
  180. data: { args: ['file=&line=101&column=10'] },
  181. })
  182. })
  183. it('update inflight state', async function () {
  184. const { container } = renderWithEditorContext(<PdfSynctexControls />, {
  185. scope,
  186. })
  187. sysendTestHelper.resetHistory()
  188. const syncToPdfButton = await screen.findByRole('button', {
  189. name: 'Go to code location in PDF',
  190. })
  191. // mock editor cursor position update
  192. fireEvent(
  193. window,
  194. new CustomEvent('cursor:editor:update', {
  195. detail: { row: 100, column: 10 },
  196. })
  197. )
  198. sysendTestHelper.receiveMessage({
  199. role: 'detached',
  200. event: 'state-sync-to-pdf-inflight',
  201. data: { value: true },
  202. })
  203. expect(syncToPdfButton.disabled).to.be.true
  204. expect(container.querySelectorAll('.synctex-spin-icon').length).to.equal(
  205. 1
  206. )
  207. sysendTestHelper.receiveMessage({
  208. role: 'detached',
  209. event: 'state-sync-to-pdf-inflight',
  210. data: { value: false },
  211. })
  212. expect(syncToPdfButton.disabled).to.be.false
  213. expect(container.querySelectorAll('.synctex-spin-icon').length).to.equal(
  214. 0
  215. )
  216. })
  217. })
  218. describe('with detached role', async function () {
  219. beforeEach(function () {
  220. window.metaAttributesCache.set('ol-detachRole', 'detached')
  221. })
  222. it('does not have go to code location button nor arrow icon', async function () {
  223. const { container } = renderWithEditorContext(<PdfSynctexControls />, {
  224. scope,
  225. })
  226. expect(
  227. await screen.queryByRole('button', {
  228. name: 'Go to code location in PDF',
  229. })
  230. ).to.not.exist
  231. expect(container.querySelector('.synctex-control-icon')).to.not.exist
  232. })
  233. it('send go to code line action and update inflight state', async function () {
  234. const { container } = renderWithEditorContext(<PdfSynctexControls />, {
  235. scope,
  236. })
  237. sysendTestHelper.resetHistory()
  238. const syncToCodeButton = await screen.findByRole('button', {
  239. name: /Go to PDF location in code/,
  240. })
  241. sysendTestHelper.resetHistory()
  242. expect(syncToCodeButton.disabled).to.be.false
  243. expect(container.querySelectorAll('.synctex-spin-icon').length).to.equal(
  244. 0
  245. )
  246. fireEvent.click(syncToCodeButton)
  247. expect(syncToCodeButton.disabled).to.be.true
  248. expect(container.querySelectorAll('.synctex-spin-icon').length).to.equal(
  249. 1
  250. )
  251. await waitFor(() => {
  252. expect(fetchMock.called('express:/project/:projectId/sync/pdf')).to.be
  253. .true
  254. })
  255. expect(sysendTestHelper.getLastBroacastMessage()).to.deep.equal({
  256. role: 'detached',
  257. event: 'action-go-to-code-line',
  258. data: { args: ['main.tex', 100] },
  259. })
  260. })
  261. it('sends PDF exists state', async function () {
  262. renderWithEditorContext(<PdfSynctexControls />, { scope })
  263. sysendTestHelper.resetHistory()
  264. await waitFor(() => {
  265. expect(fetchMock.called('express:/project/:projectId/compile')).to.be
  266. .true
  267. })
  268. expect(sysendTestHelper.getLastBroacastMessage()).to.deep.equal({
  269. role: 'detached',
  270. event: 'state-pdf-exists',
  271. data: { value: true },
  272. })
  273. })
  274. it('reacts to go to PDF location action', async function () {
  275. renderWithEditorContext(<PdfSynctexControls />, { scope })
  276. sysendTestHelper.resetHistory()
  277. await waitFor(() => {
  278. expect(fetchMock.called('express:/project/:projectId/compile')).to.be
  279. .true
  280. })
  281. sysendTestHelper.spy.broadcast.resetHistory()
  282. sysendTestHelper.receiveMessage({
  283. role: 'detacher',
  284. event: 'action-go-to-pdf-location',
  285. data: { args: ['file=&line=101&column=10'] },
  286. })
  287. await waitFor(() => {
  288. expect(fetchMock.called('express:/project/:projectId/sync/code')).to.be
  289. .true
  290. })
  291. expect(sysendTestHelper.getLastBroacastMessage()).to.deep.equal({
  292. role: 'detached',
  293. event: 'state-sync-to-pdf-inflight',
  294. data: { value: false },
  295. })
  296. })
  297. })
  298. })