pdf-synctex-controls.spec.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  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. const interceptSyncCodeAsync = () => {
  67. const output: { resolve: () => void } = {
  68. resolve: () => {
  69. // do nothing
  70. },
  71. }
  72. cy.intercept('/project/*/sync/code?*', req => {
  73. return new Promise(resolve => {
  74. output.resolve = () => {
  75. req.reply({
  76. body: { pdf: cloneDeep(mockHighlights) },
  77. })
  78. resolve()
  79. }
  80. })
  81. }).as('sync-code')
  82. return output
  83. }
  84. const interceptSyncPdfAsync = () => {
  85. const output: { resolve: () => void } = {
  86. resolve: () => {
  87. // do nothing
  88. },
  89. }
  90. cy.intercept('/project/*/sync/pdf?*', req => {
  91. return new Promise(resolve => {
  92. output.resolve = () => {
  93. req.reply({
  94. body: { code: [{ file: 'main.tex', line: 100 }] },
  95. delay: 1,
  96. })
  97. resolve()
  98. }
  99. })
  100. }).as('sync-pdf')
  101. return output
  102. }
  103. const interceptSyncPdf = () => {
  104. cy.intercept('/project/*/sync/pdf?*', req => {
  105. req.reply({
  106. body: { code: [{ file: 'main.tex', line: 100 }] },
  107. })
  108. }).as('sync-pdf')
  109. }
  110. // eslint-disable-next-line mocha/no-skipped-tests
  111. describe.skip('<PdfSynctexControls/>', function () {
  112. beforeEach(function () {
  113. window.metaAttributesCache = new Map()
  114. cy.interceptCompile()
  115. cy.interceptEvents()
  116. })
  117. afterEach(function () {
  118. window.metaAttributesCache = new Map()
  119. })
  120. it('handles clicks on sync buttons', function () {
  121. const scope = mockScope()
  122. cy.mount(
  123. <EditorProviders scope={scope}>
  124. <WithPosition mockPosition={mockPosition} />
  125. <WithSelectedEntities mockSelectedEntities={mockSelectedEntities} />
  126. <PdfSynctexControls />
  127. </EditorProviders>
  128. )
  129. cy.get('.synctex-control-icon').should('have.length', 2)
  130. // mock editor cursor position update
  131. cy.window().then(win => {
  132. win.dispatchEvent(
  133. new CustomEvent('cursor:editor:update', {
  134. detail: { row: 100, column: 10 },
  135. })
  136. )
  137. })
  138. cy.wait('@compile').then(() => {
  139. setDetachedPosition(mockPosition)
  140. })
  141. const syncCode = interceptSyncCodeAsync()
  142. cy.findByRole('button', { name: 'Go to code location in PDF' }).click()
  143. cy.findByRole('button', { name: 'Go to code location in PDF' })
  144. .should('be.disabled')
  145. .then(() => {
  146. syncCode.resolve()
  147. })
  148. cy.wait('@sync-code')
  149. const syncPdf = interceptSyncPdfAsync()
  150. cy.findByRole('button', { name: /^Go to PDF location in code/ }).click()
  151. cy.findByRole('button', { name: /^Go to PDF location in code/ })
  152. .should('be.disabled')
  153. .then(() => {
  154. syncPdf.resolve()
  155. })
  156. cy.wait('@sync-pdf')
  157. })
  158. it('disables button when multiple entities are selected', function () {
  159. const scope = mockScope()
  160. cy.mount(
  161. <EditorProviders scope={scope}>
  162. <WithPosition mockPosition={mockPosition} />
  163. <WithSelectedEntities
  164. mockSelectedEntities={[{ type: 'doc' }, { type: 'doc' }]}
  165. />
  166. <PdfSynctexControls />
  167. </EditorProviders>
  168. )
  169. cy.findByRole('button', { name: 'Go to code location in PDF' }).should(
  170. 'be.disabled'
  171. )
  172. })
  173. it('disables button when a file is selected', function () {
  174. const scope = mockScope()
  175. cy.mount(
  176. <EditorProviders scope={scope}>
  177. <WithPosition mockPosition={mockPosition} />
  178. <WithSelectedEntities mockSelectedEntities={[{ type: 'file' }]} />
  179. <PdfSynctexControls />
  180. </EditorProviders>
  181. )
  182. cy.findByRole('button', { name: 'Go to code location in PDF' }).should(
  183. 'be.disabled'
  184. )
  185. })
  186. describe('with detacher role', function () {
  187. beforeEach(function () {
  188. window.metaAttributesCache.set('ol-detachRole', 'detacher')
  189. })
  190. it('does not have go to PDF location button nor arrow icon', function () {
  191. const scope = mockScope()
  192. cy.mount(
  193. <EditorProviders scope={scope}>
  194. <WithPosition mockPosition={mockPosition} />
  195. <WithSelectedEntities mockSelectedEntities={mockSelectedEntities} />
  196. <PdfSynctexControls />
  197. </EditorProviders>
  198. )
  199. cy.findByRole('button', { name: /^Go to PDF location in code/ }).should(
  200. 'not.exist'
  201. )
  202. cy.get('.synctex-control-icon').should('not.exist')
  203. })
  204. it('send set highlights action', function () {
  205. const scope = mockScope()
  206. cy.mount(
  207. <EditorProviders scope={scope}>
  208. <WithPosition mockPosition={mockPosition} />
  209. <WithSelectedEntities mockSelectedEntities={mockSelectedEntities} />
  210. <PdfSynctexControls />
  211. </EditorProviders>
  212. )
  213. cy.wait('@compile')
  214. // mock editor cursor position update
  215. cy.window().then(win => {
  216. win.dispatchEvent(
  217. new CustomEvent('cursor:editor:update', {
  218. detail: { row: 100, column: 10 },
  219. })
  220. )
  221. })
  222. cy.spy(detachChannel, 'postMessage').as('postDetachMessage')
  223. const syncing = interceptSyncCodeAsync()
  224. cy.findByRole('button', {
  225. name: 'Go to code location in PDF',
  226. })
  227. .should('not.be.disabled')
  228. .click()
  229. cy.findByRole('button', {
  230. name: 'Go to code location in PDF',
  231. })
  232. .should('be.disabled')
  233. .then(() => {
  234. syncing.resolve()
  235. })
  236. cy.wait('@sync-code')
  237. cy.findByRole('button', {
  238. name: 'Go to code location in PDF',
  239. }).should('not.be.disabled')
  240. // synctex is called locally and the result are broadcast for the detached tab
  241. // NOTE: can't use `.to.deep.include({…})` as it doesn't match the nested array
  242. cy.get('@postDetachMessage').should('be.calledWith', {
  243. role: 'detacher',
  244. event: 'action-setHighlights',
  245. data: { args: [mockHighlights] },
  246. })
  247. })
  248. it('reacts to sync to code action', function () {
  249. interceptSyncPdf()
  250. const scope = mockScope()
  251. cy.mount(
  252. <EditorProviders scope={scope}>
  253. <WithPosition mockPosition={mockPosition} />
  254. <WithSelectedEntities mockSelectedEntities={mockSelectedEntities} />
  255. <PdfSynctexControls />
  256. </EditorProviders>
  257. ).then(() => {
  258. testDetachChannel.postMessage({
  259. role: 'detached',
  260. event: 'action-sync-to-code',
  261. data: {
  262. args: [mockPosition],
  263. },
  264. })
  265. })
  266. cy.wait('@sync-pdf')
  267. })
  268. })
  269. describe('with detached role', function () {
  270. beforeEach(function () {
  271. window.metaAttributesCache.set('ol-detachRole', 'detached')
  272. })
  273. it('does not have go to code location button nor arrow icon', function () {
  274. const scope = mockScope()
  275. cy.mount(
  276. <EditorProviders scope={scope}>
  277. <WithPosition mockPosition={mockPosition} />
  278. <PdfSynctexControls />
  279. </EditorProviders>
  280. )
  281. cy.findByRole('button', {
  282. name: 'Go to code location in PDF',
  283. }).should('not.exist')
  284. cy.get('.synctex-control-icon').should('not.exist')
  285. })
  286. it('send go to code line action', function () {
  287. const scope = mockScope()
  288. cy.mount(
  289. <EditorProviders scope={scope}>
  290. <PdfSynctexControls />
  291. </EditorProviders>
  292. )
  293. cy.wait('@compile').then(() => {
  294. testDetachChannel.postMessage({
  295. role: 'detacher',
  296. event: `state-position`,
  297. data: { value: mockPosition },
  298. })
  299. })
  300. cy.findByRole('button', {
  301. name: /^Go to PDF location in code/,
  302. })
  303. cy.findByRole('button', { name: /^Go to PDF location in code/ }).should(
  304. 'not.be.disabled'
  305. )
  306. cy.spy(detachChannel, 'postMessage').as('postDetachMessage')
  307. cy.findByRole('button', { name: /^Go to PDF location in code/ }).click()
  308. // the button is only disabled when the state is updated
  309. cy.findByRole('button', { name: /^Go to PDF location in code/ }).should(
  310. 'not.be.disabled'
  311. )
  312. cy.get('.synctex-spin-icon').should('not.exist')
  313. cy.get('@postDetachMessage').should('be.calledWith', {
  314. role: 'detached',
  315. event: 'action-sync-to-code',
  316. data: {
  317. args: [mockPosition, 72],
  318. },
  319. })
  320. })
  321. it('update inflight state', function () {
  322. const scope = mockScope()
  323. cy.mount(
  324. <EditorProviders scope={scope}>
  325. <WithPosition mockPosition={mockPosition} />
  326. <PdfSynctexControls />
  327. </EditorProviders>
  328. )
  329. cy.wrap(null).then(() => {
  330. testDetachChannel.postMessage({
  331. role: 'detacher',
  332. event: `state-position`,
  333. data: { value: mockPosition },
  334. })
  335. })
  336. cy.findByRole('button', { name: /^Go to PDF location in code/ }).should(
  337. 'not.be.disabled'
  338. )
  339. cy.get('.synctex-spin-icon').should('not.exist')
  340. cy.wrap(null).then(() => {
  341. testDetachChannel.postMessage({
  342. role: 'detacher',
  343. event: 'state-sync-to-code-inflight',
  344. data: { value: true },
  345. })
  346. })
  347. cy.findByRole('button', { name: /^Go to PDF location in code/ }).should(
  348. 'be.disabled'
  349. )
  350. cy.get('.synctex-spin-icon').should('have.length', 1)
  351. cy.wrap(null).then(() => {
  352. testDetachChannel.postMessage({
  353. role: 'detacher',
  354. event: 'state-sync-to-code-inflight',
  355. data: { value: false },
  356. })
  357. })
  358. cy.findByRole('button', { name: /^Go to PDF location in code/ }).should(
  359. 'not.be.disabled'
  360. )
  361. cy.get('.synctex-spin-icon').should('not.exist')
  362. })
  363. })
  364. })