pdf-synctex-controls.spec.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  1. import PdfSynctexControls from '../../../../frontend/js/features/pdf-preview/components/pdf-synctex-controls'
  2. import { cloneDeep } from 'lodash'
  3. import { useDetachCompileContext as useCompileContext } from '../../../../frontend/js/shared/context/detach-compile-context'
  4. import { useFileTreeData } from '../../../../frontend/js/shared/context/file-tree-data-context'
  5. import { useEffect } from 'react'
  6. import { EditorProviders } from '../../helpers/editor-providers'
  7. import { mockScope } from './scope'
  8. import { detachChannel, testDetachChannel } from '../../helpers/detach-channel'
  9. const mockHighlights = [
  10. {
  11. page: 1,
  12. h: 85.03936,
  13. v: 509.999878,
  14. width: 441.921265,
  15. height: 8.855677,
  16. },
  17. {
  18. page: 1,
  19. h: 85.03936,
  20. v: 486.089539,
  21. width: 441.921265,
  22. height: 8.855677,
  23. },
  24. ]
  25. type Position = {
  26. page: number
  27. offset: { top: number; left: number }
  28. pageSize: { height: number; width: number }
  29. }
  30. const mockPosition: Position = {
  31. page: 1,
  32. offset: { top: 10, left: 10 },
  33. pageSize: { height: 500, width: 500 },
  34. }
  35. type Entity = {
  36. type: string
  37. }
  38. const mockSelectedEntities: Entity[] = [{ type: 'doc' }]
  39. const WithPosition = ({ mockPosition }: { mockPosition: Position }) => {
  40. const { setPosition } = useCompileContext()
  41. // mock PDF scroll position update
  42. useEffect(() => {
  43. setPosition(mockPosition)
  44. }, [mockPosition, setPosition])
  45. return null
  46. }
  47. // mock PDF scroll position update
  48. const setDetachedPosition = (mockPosition: Position) => {
  49. testDetachChannel.postMessage({
  50. role: 'detacher',
  51. event: 'state-position',
  52. data: { value: mockPosition },
  53. })
  54. }
  55. const WithSelectedEntities = ({
  56. mockSelectedEntities = [],
  57. }: {
  58. mockSelectedEntities: Entity[]
  59. }) => {
  60. const { setSelectedEntities } = useFileTreeData()
  61. useEffect(() => {
  62. setSelectedEntities(mockSelectedEntities)
  63. }, [mockSelectedEntities, setSelectedEntities])
  64. return null
  65. }
  66. describe('<PdfSynctexControls/>', function () {
  67. beforeEach(function () {
  68. window.metaAttributesCache.set('ol-project_id', 'test-project')
  69. window.metaAttributesCache.set('ol-preventCompileOnLoad', false)
  70. cy.interceptEvents()
  71. })
  72. // eslint-disable-next-line mocha/no-skipped-tests
  73. it.skip('handles clicks on sync buttons', function () {
  74. cy.interceptCompile()
  75. const scope = mockScope()
  76. cy.mount(
  77. <EditorProviders scope={scope}>
  78. <WithPosition mockPosition={mockPosition} />
  79. <WithSelectedEntities mockSelectedEntities={mockSelectedEntities} />
  80. <PdfSynctexControls />
  81. </EditorProviders>
  82. )
  83. cy.waitForCompile()
  84. cy.get('.synctex-control-icon').should('have.length', 2)
  85. // mock editor cursor position update
  86. cy.window().then(win => {
  87. win.dispatchEvent(
  88. new CustomEvent('cursor:editor:update', {
  89. detail: { row: 100, column: 10 },
  90. })
  91. )
  92. })
  93. cy.wrap(null).then(() => {
  94. setDetachedPosition(mockPosition)
  95. })
  96. cy.interceptAsync({ pathname: '/project/*/sync/code' }, 'sync-code').then(
  97. syncCodeResponse => {
  98. cy.findByRole('button', { name: 'Go to code location in PDF' }).click()
  99. cy.findByRole('button', { name: 'Go to code location in PDF' })
  100. .should('be.disabled')
  101. .then(() => {
  102. syncCodeResponse.resolve({
  103. body: { pdf: cloneDeep(mockHighlights) },
  104. })
  105. })
  106. cy.wait('@sync-code')
  107. }
  108. )
  109. cy.interceptAsync({ pathname: '/project/*/sync/pdf' }, 'sync-pdf').then(
  110. syncPdfResponse => {
  111. cy.findByRole('button', { name: /^Go to PDF location in code/ }).click()
  112. cy.findByRole('button', { name: /^Go to PDF location in code/ })
  113. .should('be.disabled')
  114. .then(() => {
  115. syncPdfResponse.resolve({
  116. body: { code: [{ file: 'main.tex', line: 100 }] },
  117. })
  118. })
  119. cy.wait('@sync-pdf')
  120. }
  121. )
  122. })
  123. it('disables button when multiple entities are selected', function () {
  124. cy.interceptCompile()
  125. const scope = mockScope()
  126. cy.mount(
  127. <EditorProviders scope={scope}>
  128. <WithPosition mockPosition={mockPosition} />
  129. <WithSelectedEntities
  130. mockSelectedEntities={[{ type: 'doc' }, { type: 'doc' }]}
  131. />
  132. <PdfSynctexControls />
  133. </EditorProviders>
  134. )
  135. cy.waitForCompile()
  136. cy.findByRole('button', { name: 'Go to code location in PDF' }).should(
  137. 'be.disabled'
  138. )
  139. })
  140. it('disables button when a file is selected', function () {
  141. cy.interceptCompile()
  142. const scope = mockScope()
  143. cy.mount(
  144. <EditorProviders scope={scope}>
  145. <WithPosition mockPosition={mockPosition} />
  146. <WithSelectedEntities mockSelectedEntities={[{ type: 'file' }]} />
  147. <PdfSynctexControls />
  148. </EditorProviders>
  149. )
  150. cy.waitForCompile()
  151. cy.findByRole('button', { name: 'Go to code location in PDF' }).should(
  152. 'be.disabled'
  153. )
  154. })
  155. describe('with detacher role', function () {
  156. beforeEach(function () {
  157. window.metaAttributesCache.set('ol-detachRole', 'detacher')
  158. })
  159. it('does not have go to PDF location button nor arrow icon', function () {
  160. cy.interceptCompile()
  161. const scope = mockScope()
  162. cy.mount(
  163. <EditorProviders scope={scope}>
  164. <WithPosition mockPosition={mockPosition} />
  165. <WithSelectedEntities mockSelectedEntities={mockSelectedEntities} />
  166. <PdfSynctexControls />
  167. </EditorProviders>
  168. )
  169. cy.waitForCompile()
  170. cy.findByRole('button', { name: /^Go to PDF location in code/ }).should(
  171. 'not.exist'
  172. )
  173. cy.get('.synctex-control-icon').should('not.exist')
  174. })
  175. // eslint-disable-next-line mocha/no-skipped-tests
  176. it.skip('send set highlights action', function () {
  177. cy.interceptCompile()
  178. const scope = mockScope()
  179. cy.mount(
  180. <EditorProviders scope={scope}>
  181. <WithPosition mockPosition={mockPosition} />
  182. <WithSelectedEntities mockSelectedEntities={mockSelectedEntities} />
  183. <PdfSynctexControls />
  184. </EditorProviders>
  185. )
  186. cy.waitForCompile()
  187. // mock editor cursor position update
  188. cy.window().then(win => {
  189. win.dispatchEvent(
  190. new CustomEvent('cursor:editor:update', {
  191. detail: { row: 100, column: 10 },
  192. })
  193. )
  194. })
  195. cy.spy(detachChannel, 'postMessage').as('postDetachMessage')
  196. cy.interceptAsync({ pathname: '/project/*/sync/code' }, 'sync-code').then(
  197. syncCodeResponse => {
  198. cy.findByRole('button', {
  199. name: 'Go to code location in PDF',
  200. })
  201. .should('not.be.disabled')
  202. .click()
  203. cy.findByRole('button', {
  204. name: 'Go to code location in PDF',
  205. })
  206. .should('be.disabled')
  207. .then(() => {
  208. syncCodeResponse.resolve({
  209. body: { pdf: cloneDeep(mockHighlights) },
  210. })
  211. })
  212. cy.wait('@sync-code')
  213. }
  214. )
  215. cy.findByRole('button', {
  216. name: 'Go to code location in PDF',
  217. }).should('not.be.disabled')
  218. // synctex is called locally and the result are broadcast for the detached tab
  219. // NOTE: can't use `.to.deep.include({…})` as it doesn't match the nested array
  220. cy.get('@postDetachMessage').should('have.been.calledWith', {
  221. role: 'detacher',
  222. event: 'action-setHighlights',
  223. data: { args: [mockHighlights] },
  224. })
  225. })
  226. it('reacts to sync to code action', function () {
  227. cy.interceptCompile()
  228. const scope = mockScope()
  229. cy.mount(
  230. <EditorProviders scope={scope}>
  231. <WithPosition mockPosition={mockPosition} />
  232. <WithSelectedEntities mockSelectedEntities={mockSelectedEntities} />
  233. <PdfSynctexControls />
  234. </EditorProviders>
  235. )
  236. cy.waitForCompile()
  237. cy.interceptAsync({ pathname: '/project/*/sync/pdf' }, 'sync-pdf')
  238. .then(syncPdfResponse => {
  239. syncPdfResponse.resolve({
  240. body: { code: [{ file: 'main.tex', line: 100 }] },
  241. })
  242. testDetachChannel.postMessage({
  243. role: 'detached',
  244. event: 'action-sync-to-code',
  245. data: {
  246. args: [mockPosition],
  247. },
  248. })
  249. })
  250. .wait('@sync-pdf')
  251. })
  252. })
  253. describe('with detached role', function () {
  254. beforeEach(function () {
  255. window.metaAttributesCache.set('ol-detachRole', 'detached')
  256. })
  257. it('does not have go to code location button nor arrow icon', function () {
  258. cy.interceptCompile()
  259. const scope = mockScope()
  260. cy.mount(
  261. <EditorProviders scope={scope}>
  262. <WithPosition mockPosition={mockPosition} />
  263. <PdfSynctexControls />
  264. </EditorProviders>
  265. )
  266. cy.waitForCompile()
  267. cy.findByRole('button', {
  268. name: 'Go to code location in PDF',
  269. }).should('not.exist')
  270. cy.get('.synctex-control-icon').should('not.exist')
  271. })
  272. it('send go to code line action', function () {
  273. cy.interceptCompile()
  274. const scope = mockScope()
  275. cy.mount(
  276. <EditorProviders scope={scope}>
  277. <PdfSynctexControls />
  278. </EditorProviders>
  279. )
  280. cy.waitForCompile().then(() => {
  281. testDetachChannel.postMessage({
  282. role: 'detacher',
  283. event: `state-position`,
  284. data: { value: mockPosition },
  285. })
  286. })
  287. cy.findByRole('button', {
  288. name: /^Go to PDF location in code/,
  289. })
  290. cy.findByRole('button', { name: /^Go to PDF location in code/ }).should(
  291. 'not.be.disabled'
  292. )
  293. cy.spy(detachChannel, 'postMessage').as('postDetachMessage')
  294. cy.findByRole('button', { name: /^Go to PDF location in code/ }).click()
  295. // the button is only disabled when the state is updated
  296. cy.findByRole('button', { name: /^Go to PDF location in code/ }).should(
  297. 'not.be.disabled'
  298. )
  299. cy.get('.synctex-spin-icon').should('not.exist')
  300. cy.get('@postDetachMessage').should('have.been.calledWith', {
  301. role: 'detached',
  302. event: 'action-sync-to-code',
  303. data: {
  304. args: [mockPosition, 72],
  305. },
  306. })
  307. })
  308. it('update inflight state', function () {
  309. cy.interceptCompile()
  310. const scope = mockScope()
  311. cy.mount(
  312. <EditorProviders scope={scope}>
  313. <WithPosition mockPosition={mockPosition} />
  314. <PdfSynctexControls />
  315. </EditorProviders>
  316. )
  317. cy.waitForCompile().then(() => {
  318. testDetachChannel.postMessage({
  319. role: 'detacher',
  320. event: `state-position`,
  321. data: { value: mockPosition },
  322. })
  323. })
  324. cy.findByRole('button', { name: /^Go to PDF location in code/ }).should(
  325. 'not.be.disabled'
  326. )
  327. cy.get('.synctex-spin-icon').should('not.exist')
  328. cy.wrap(null).then(() => {
  329. testDetachChannel.postMessage({
  330. role: 'detacher',
  331. event: 'state-sync-to-code-inflight',
  332. data: { value: true },
  333. })
  334. })
  335. cy.findByRole('button', { name: /^Go to PDF location in code/ }).should(
  336. 'be.disabled'
  337. )
  338. cy.get('.synctex-spin-icon').should('have.length', 1)
  339. cy.wrap(null).then(() => {
  340. testDetachChannel.postMessage({
  341. role: 'detacher',
  342. event: 'state-sync-to-code-inflight',
  343. data: { value: false },
  344. })
  345. })
  346. cy.findByRole('button', { name: /^Go to PDF location in code/ }).should(
  347. 'not.be.disabled'
  348. )
  349. cy.get('.synctex-spin-icon').should('not.exist')
  350. })
  351. })
  352. })