pdf-synctex-controls.test.js 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  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.showNewPdfPreview = true
  88. window.metaAttributesCache = new Map()
  89. fetchMock.restore()
  90. mockCompile()
  91. mockSynctex()
  92. mockBuildFile()
  93. })
  94. afterEach(function () {
  95. window.showNewPdfPreview = undefined
  96. window.metaAttributesCache = new Map()
  97. fetchMock.restore()
  98. })
  99. it('handles clicks on sync buttons', async function () {
  100. const Inner = () => {
  101. const { setPosition } = useCompileContext()
  102. // mock PDF scroll position update
  103. useEffect(() => {
  104. setPosition({
  105. page: 1,
  106. offset: { top: 10, left: 10 },
  107. pageSize: { height: 500, width: 500 },
  108. })
  109. }, [setPosition])
  110. return null
  111. }
  112. const { container } = renderWithEditorContext(
  113. <>
  114. <Inner />
  115. <PdfSynctexControls />
  116. </>,
  117. { scope }
  118. )
  119. const syncToPdfButton = await screen.findByRole('button', {
  120. name: 'Go to code location in PDF',
  121. })
  122. const syncToCodeButton = await screen.findByRole('button', {
  123. name: /Go to PDF location in code/,
  124. })
  125. expect(container.querySelectorAll('.synctex-control-icon').length).to.equal(
  126. 2
  127. )
  128. // mock editor cursor position update
  129. fireEvent(
  130. window,
  131. new CustomEvent('cursor:editor:update', {
  132. detail: { row: 100, column: 10 },
  133. })
  134. )
  135. fireEvent.click(syncToPdfButton)
  136. expect(syncToPdfButton.disabled).to.be.true
  137. await waitFor(() => {
  138. expect(fetchMock.called('express:/project/:projectId/sync/code')).to.be
  139. .true
  140. })
  141. fireEvent.click(syncToCodeButton)
  142. expect(syncToCodeButton.disabled).to.be.true
  143. await waitFor(() => {
  144. expect(fetchMock.called('express:/project/:projectId/sync/pdf')).to.be
  145. .true
  146. })
  147. })
  148. describe('with detacher role', async function () {
  149. beforeEach(function () {
  150. window.metaAttributesCache.set('ol-detachRole', 'detacher')
  151. })
  152. it('does not have go to PDF location button nor arrow icon', async function () {
  153. const { container } = renderWithEditorContext(<PdfSynctexControls />, {
  154. scope,
  155. })
  156. expect(
  157. await screen.queryByRole('button', {
  158. name: 'Go to PDF location in code',
  159. })
  160. ).to.not.exist
  161. expect(container.querySelector('.synctex-control-icon')).to.not.exist
  162. })
  163. it('send go to PDF location action', async function () {
  164. renderWithEditorContext(<PdfSynctexControls />, { scope })
  165. sysendTestHelper.resetHistory()
  166. const syncToPdfButton = await screen.findByRole('button', {
  167. name: 'Go to code location in PDF',
  168. })
  169. // mock editor cursor position update
  170. fireEvent(
  171. window,
  172. new CustomEvent('cursor:editor:update', {
  173. detail: { row: 100, column: 10 },
  174. })
  175. )
  176. fireEvent.click(syncToPdfButton)
  177. // the button is only disabled when the state is updated via sysend
  178. expect(syncToPdfButton.disabled).to.be.false
  179. expect(sysendTestHelper.getLastBroacastMessage()).to.deep.equal({
  180. role: 'detacher',
  181. event: 'action-go-to-pdf-location',
  182. data: { args: ['file=&line=101&column=10'] },
  183. })
  184. })
  185. it('update inflight state', async function () {
  186. const { container } = renderWithEditorContext(<PdfSynctexControls />, {
  187. scope,
  188. })
  189. sysendTestHelper.resetHistory()
  190. const syncToPdfButton = await screen.findByRole('button', {
  191. name: 'Go to code location in PDF',
  192. })
  193. // mock editor cursor position update
  194. fireEvent(
  195. window,
  196. new CustomEvent('cursor:editor:update', {
  197. detail: { row: 100, column: 10 },
  198. })
  199. )
  200. sysendTestHelper.receiveMessage({
  201. role: 'detached',
  202. event: 'state-sync-to-pdf-inflight',
  203. data: { value: true },
  204. })
  205. expect(syncToPdfButton.disabled).to.be.true
  206. expect(container.querySelectorAll('.synctex-spin-icon').length).to.equal(
  207. 1
  208. )
  209. sysendTestHelper.receiveMessage({
  210. role: 'detached',
  211. event: 'state-sync-to-pdf-inflight',
  212. data: { value: false },
  213. })
  214. expect(syncToPdfButton.disabled).to.be.false
  215. expect(container.querySelectorAll('.synctex-spin-icon').length).to.equal(
  216. 0
  217. )
  218. })
  219. })
  220. describe('with detached role', async function () {
  221. beforeEach(function () {
  222. window.metaAttributesCache.set('ol-detachRole', 'detached')
  223. })
  224. it('does not have go to code location button nor arrow icon', async function () {
  225. const { container } = renderWithEditorContext(<PdfSynctexControls />, {
  226. scope,
  227. })
  228. expect(
  229. await screen.queryByRole('button', {
  230. name: 'Go to code location in PDF',
  231. })
  232. ).to.not.exist
  233. expect(container.querySelector('.synctex-control-icon')).to.not.exist
  234. })
  235. it('send go to code line action and update inflight state', async function () {
  236. const { container } = renderWithEditorContext(<PdfSynctexControls />, {
  237. scope,
  238. })
  239. sysendTestHelper.resetHistory()
  240. const syncToCodeButton = await screen.findByRole('button', {
  241. name: /Go to PDF location in code/,
  242. })
  243. sysendTestHelper.resetHistory()
  244. expect(syncToCodeButton.disabled).to.be.false
  245. expect(container.querySelectorAll('.synctex-spin-icon').length).to.equal(
  246. 0
  247. )
  248. fireEvent.click(syncToCodeButton)
  249. expect(syncToCodeButton.disabled).to.be.true
  250. expect(container.querySelectorAll('.synctex-spin-icon').length).to.equal(
  251. 1
  252. )
  253. await waitFor(() => {
  254. expect(fetchMock.called('express:/project/:projectId/sync/pdf')).to.be
  255. .true
  256. })
  257. expect(sysendTestHelper.getLastBroacastMessage()).to.deep.equal({
  258. role: 'detached',
  259. event: 'action-go-to-code-line',
  260. data: { args: ['main.tex', 100] },
  261. })
  262. })
  263. it('sends PDF exists state', async function () {
  264. renderWithEditorContext(<PdfSynctexControls />, { scope })
  265. sysendTestHelper.resetHistory()
  266. await waitFor(() => {
  267. expect(fetchMock.called('express:/project/:projectId/compile')).to.be
  268. .true
  269. })
  270. expect(sysendTestHelper.getLastBroacastMessage()).to.deep.equal({
  271. role: 'detached',
  272. event: 'state-pdf-exists',
  273. data: { value: true },
  274. })
  275. })
  276. it('reacts to go to PDF location action', async function () {
  277. renderWithEditorContext(<PdfSynctexControls />, { scope })
  278. sysendTestHelper.resetHistory()
  279. await waitFor(() => {
  280. expect(fetchMock.called('express:/project/:projectId/compile')).to.be
  281. .true
  282. })
  283. sysendTestHelper.spy.broadcast.resetHistory()
  284. sysendTestHelper.receiveMessage({
  285. role: 'detacher',
  286. event: 'action-go-to-pdf-location',
  287. data: { args: ['file=&line=101&column=10'] },
  288. })
  289. await waitFor(() => {
  290. expect(fetchMock.called('express:/project/:projectId/sync/code')).to.be
  291. .true
  292. })
  293. expect(sysendTestHelper.getLastBroacastMessage()).to.deep.equal({
  294. role: 'detached',
  295. event: 'state-sync-to-pdf-inflight',
  296. data: { value: false },
  297. })
  298. })
  299. })
  300. })