pdf-synctex-controls.test.js 12 KB

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