pdf-preview.spec.tsx 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627
  1. import localStorage from '../../../../frontend/js/infrastructure/local-storage'
  2. import PdfPreview from '../../../../frontend/js/features/pdf-preview/components/pdf-preview'
  3. import { EditorProviders } from '../../helpers/editor-providers'
  4. import { mockScope } from './scope'
  5. import { useLayoutContext } from '../../../../frontend/js/shared/context/layout-context'
  6. import { FC, useEffect } from 'react'
  7. const storeAndFireEvent = (win: typeof window, key: string, value: unknown) => {
  8. localStorage.setItem(key, value)
  9. win.dispatchEvent(new StorageEvent('storage', { key }))
  10. }
  11. const Layout: FC<{ layout: string; view?: string }> = ({ layout, view }) => {
  12. const { changeLayout } = useLayoutContext()
  13. useEffect(() => {
  14. changeLayout(layout, view)
  15. }, [changeLayout, layout, view])
  16. return null
  17. }
  18. describe('<PdfPreview/>', function () {
  19. beforeEach(function () {
  20. window.metaAttributesCache.set('ol-preventCompileOnLoad', true)
  21. window.metaAttributesCache.set(
  22. 'ol-compilesUserContentDomain',
  23. 'https://compiles-user.dev-overleaf.com'
  24. )
  25. cy.interceptEvents()
  26. })
  27. it('renders the PDF preview', function () {
  28. window.metaAttributesCache.set('ol-preventCompileOnLoad', false)
  29. cy.interceptCompile('compile')
  30. const scope = mockScope()
  31. cy.mount(
  32. <EditorProviders scope={scope}>
  33. <div className="pdf-viewer">
  34. <PdfPreview />
  35. </div>
  36. </EditorProviders>
  37. )
  38. // wait for "compile on load" to finish
  39. cy.waitForCompile({ pdf: true })
  40. cy.findByRole('button', { name: 'Recompile' })
  41. })
  42. it('runs a compile when the Recompile button is pressed', function () {
  43. cy.interceptCompile()
  44. const scope = mockScope()
  45. cy.mount(
  46. <EditorProviders scope={scope}>
  47. <div className="pdf-viewer">
  48. <PdfPreview />
  49. </div>
  50. </EditorProviders>
  51. )
  52. // press the Recompile button => compile
  53. cy.findByRole('button', { name: 'Recompile' }).click()
  54. // wait for compile to finish
  55. cy.waitForCompile({ pdf: true })
  56. cy.contains('Your Paper')
  57. })
  58. it('runs a compile on `pdf:recompile` event', function () {
  59. cy.interceptCompile()
  60. const scope = mockScope()
  61. cy.mount(
  62. <EditorProviders scope={scope}>
  63. <div className="pdf-viewer">
  64. <PdfPreview />
  65. </div>
  66. </EditorProviders>
  67. )
  68. cy.window().then(win => {
  69. win.dispatchEvent(new CustomEvent('pdf:recompile'))
  70. })
  71. // wait for compile to finish
  72. cy.waitForCompile({ pdf: true })
  73. cy.contains('Your Paper')
  74. })
  75. it('does not compile while compiling', function () {
  76. let counter = 0
  77. cy.interceptDeferredCompile(() => counter++).then(
  78. resolveDeferredCompile => {
  79. const scope = mockScope()
  80. cy.mount(
  81. <EditorProviders scope={scope}>
  82. <div className="pdf-viewer">
  83. <PdfPreview />
  84. </div>
  85. </EditorProviders>
  86. )
  87. // start compiling
  88. cy.findByRole('button', { name: 'Recompile' }).click()
  89. cy.findByRole('button', { name: 'Compiling…' }).then(() => {
  90. // trigger a recompile
  91. cy.window().then(win => {
  92. win.dispatchEvent(new CustomEvent('pdf:recompile'))
  93. })
  94. // finish the original compile
  95. resolveDeferredCompile()
  96. // wait for the original compile to finish
  97. cy.waitForCompile().then(() => {
  98. // NOTE: difficult to assert that a second request won't be sent, at some point
  99. expect(counter).to.equal(1)
  100. })
  101. })
  102. }
  103. )
  104. })
  105. // eslint-disable-next-line mocha/no-skipped-tests
  106. it.skip('disables compile button while compile is running', function () {
  107. cy.interceptDeferredCompile().then(resolveDeferredCompile => {
  108. const scope = mockScope()
  109. cy.mount(
  110. <EditorProviders scope={scope}>
  111. <div className="pdf-viewer">
  112. <PdfPreview />
  113. </div>
  114. </EditorProviders>
  115. )
  116. cy.findByRole('button', { name: 'Recompile' }).click()
  117. cy.findByRole('button', { name: 'Compiling…' })
  118. .should('be.disabled')
  119. .then(resolveDeferredCompile)
  120. cy.waitForCompile()
  121. cy.findByRole('button', { name: 'Recompile' }).should('not.be.disabled')
  122. })
  123. })
  124. it('runs a compile on doc change if autocompile is enabled', function () {
  125. cy.interceptCompile()
  126. const scope = mockScope()
  127. cy.mount(
  128. <EditorProviders scope={scope}>
  129. <div className="pdf-viewer">
  130. <PdfPreview />
  131. </div>
  132. </EditorProviders>
  133. )
  134. cy.window().then(win => {
  135. cy.clock()
  136. // switch on auto compile
  137. storeAndFireEvent(win, 'autocompile_enabled:project123', true)
  138. // fire a doc:changed event => compile
  139. win.dispatchEvent(new CustomEvent('doc:changed'))
  140. // wait enough time for the compile to start
  141. cy.tick(6000) // > AUTO_COMPILE_DEBOUNCE
  142. cy.clock().invoke('restore')
  143. })
  144. // wait for compile to finish
  145. cy.waitForCompile({ pdf: true })
  146. cy.findByRole('button', { name: 'Recompile' })
  147. })
  148. it('does not run a compile on doc change if autocompile is disabled', function () {
  149. cy.interceptCompile()
  150. const scope = mockScope()
  151. cy.mount(
  152. <EditorProviders scope={scope}>
  153. <div className="pdf-viewer">
  154. <PdfPreview />
  155. </div>
  156. </EditorProviders>
  157. )
  158. cy.window().then(win => {
  159. cy.clock()
  160. // make sure auto compile is switched off
  161. storeAndFireEvent(win, 'autocompile_enabled:project123', false)
  162. // fire a doc:changed event => no compile
  163. win.dispatchEvent(new CustomEvent('doc:changed'))
  164. // wait enough time for the compile to start
  165. cy.tick(6000) // AUTO_COMPILE_DEBOUNCE
  166. cy.clock().invoke('restore')
  167. })
  168. // NOTE: difficult to assert that a request hasn't been sent
  169. cy.findByRole('button', { name: 'Recompile' })
  170. })
  171. it('does not run a compile on doc change if autocompile is blocked by syntax check', function () {
  172. cy.interceptCompile()
  173. const scope = mockScope()
  174. // enable linting in the editor
  175. scope.settings.syntaxValidation = true
  176. // mock a linting error
  177. scope.hasLintingError = true
  178. cy.mount(
  179. <EditorProviders scope={scope}>
  180. <div className="pdf-viewer">
  181. <PdfPreview />
  182. </div>
  183. </EditorProviders>
  184. )
  185. cy.window().then(win => {
  186. cy.clock()
  187. // switch on auto compile
  188. storeAndFireEvent(win, 'autocompile_enabled:project123', true)
  189. // switch on syntax checking
  190. storeAndFireEvent(win, 'stop_on_validation_error', true)
  191. // fire a doc:changed event => no compile
  192. win.dispatchEvent(new CustomEvent('doc:changed'))
  193. // wait enough time for the compile to start
  194. cy.tick(6000) // AUTO_COMPILE_DEBOUNCE
  195. cy.clock().invoke('restore')
  196. })
  197. // NOTE: difficult to assert that a request hasn't been sent
  198. cy.findByRole('button', { name: 'Recompile' })
  199. cy.findByText('Code check failed')
  200. })
  201. it('does not run a compile on doc change if the PDF preview is not open', function () {
  202. cy.interceptCompile()
  203. const scope = mockScope()
  204. cy.mount(
  205. <EditorProviders scope={scope}>
  206. <Layout layout="flat" view="editor" />
  207. <div className="pdf-viewer">
  208. <PdfPreview />
  209. </div>
  210. </EditorProviders>
  211. )
  212. cy.window().then(win => {
  213. cy.clock()
  214. // switch on auto compile
  215. storeAndFireEvent(win, 'autocompile_enabled:project123', true)
  216. // fire a doc:changed event => compile
  217. win.dispatchEvent(new CustomEvent('doc:changed'))
  218. // wait enough time for the compile to start
  219. cy.tick(6000) // > AUTO_COMPILE_DEBOUNCE
  220. cy.clock().invoke('restore')
  221. })
  222. // NOTE: difficult to assert that a request hasn't been sent
  223. cy.findByRole('button', { name: 'Recompile' })
  224. })
  225. describe('error messages', function () {
  226. const compileErrorStatuses = {
  227. 'clear-cache':
  228. 'Sorry, something went wrong and your project could not be compiled. Please try again in a few moments.',
  229. 'clsi-maintenance':
  230. 'The compile servers are down for maintenance, and will be back shortly.',
  231. 'compile-in-progress':
  232. 'A previous compile is still running. Please wait a minute and try compiling again.',
  233. exited: 'Server Error',
  234. failure: 'No PDF',
  235. generic: 'Server Error',
  236. 'project-too-large': 'Project too large',
  237. 'rate-limited': 'Compile rate limit hit',
  238. terminated: 'Compilation cancelled',
  239. timedout: 'Timed out',
  240. 'too-recently-compiled':
  241. 'This project was compiled very recently, so this compile has been skipped.',
  242. unavailable:
  243. 'Sorry, the compile server for your project was temporarily unavailable. Please try again in a few moments.',
  244. foo: 'Sorry, something went wrong and your project could not be compiled. Please try again in a few moments.',
  245. }
  246. for (const [status, message] of Object.entries(compileErrorStatuses)) {
  247. it(`displays error message for '${status}' status`, function () {
  248. cy.intercept('POST', '/project/*/compile*', {
  249. body: {
  250. status,
  251. clsiServerId: 'foo',
  252. compileGroup: 'priority',
  253. },
  254. }).as('compile')
  255. const scope = mockScope()
  256. cy.mount(
  257. <EditorProviders scope={scope}>
  258. <div className="pdf-viewer">
  259. <PdfPreview />
  260. </div>
  261. </EditorProviders>
  262. )
  263. cy.findByRole('button', { name: 'Recompile' }).click()
  264. cy.wait('@compile')
  265. cy.findByText(message)
  266. })
  267. }
  268. })
  269. it('displays expandable raw logs', function () {
  270. cy.interceptCompile()
  271. const scope = mockScope()
  272. cy.mount(
  273. <EditorProviders scope={scope}>
  274. <div className="pdf-viewer">
  275. <PdfPreview />
  276. </div>
  277. </EditorProviders>
  278. )
  279. cy.findByRole('button', { name: 'Recompile' }).click()
  280. cy.waitForCompile({ pdf: true })
  281. cy.findByRole('button', { name: 'View logs' }).click()
  282. cy.findByRole('button', { name: 'View PDF' })
  283. cy.findByRole('button', { name: 'Expand' }).click()
  284. cy.findByRole('button', { name: 'Collapse' }).click()
  285. })
  286. it('displays error messages if there were validation problems', function () {
  287. const validationProblems = {
  288. sizeCheck: {
  289. resources: [
  290. { path: 'foo/bar', kbSize: 76221 },
  291. { path: 'bar/baz', kbSize: 2342 },
  292. ],
  293. },
  294. mainFile: true,
  295. conflictedPaths: [
  296. {
  297. path: 'foo/bar',
  298. },
  299. {
  300. path: 'foo/baz',
  301. },
  302. ],
  303. }
  304. cy.intercept('POST', '/project/*/compile*', {
  305. body: {
  306. status: 'validation-problems',
  307. validationProblems,
  308. clsiServerId: 'foo',
  309. compileGroup: 'priority',
  310. },
  311. }).as('compile')
  312. const scope = mockScope()
  313. cy.mount(
  314. <EditorProviders scope={scope}>
  315. <div className="pdf-viewer">
  316. <PdfPreview />
  317. </div>
  318. </EditorProviders>
  319. )
  320. cy.findByRole('button', { name: 'Recompile' }).click()
  321. cy.wait('@compile')
  322. cy.findByText('Project too large')
  323. cy.findByText('Unknown main document')
  324. cy.findByText('Conflicting Paths Found')
  325. })
  326. describe('clear cache', function () {
  327. it('sends a clear cache request when the button is pressed', function () {
  328. cy.interceptCompile()
  329. const scope = mockScope()
  330. cy.mount(
  331. <EditorProviders scope={scope}>
  332. <div className="pdf-viewer">
  333. <PdfPreview />
  334. </div>
  335. </EditorProviders>
  336. )
  337. cy.findByRole('button', { name: 'Recompile' }).click()
  338. cy.waitForCompile({ pdf: true })
  339. cy.findByRole('button', { name: 'View logs' }).click()
  340. cy.findByRole('button', { name: 'Clear cached files' }).should(
  341. 'not.be.disabled'
  342. )
  343. cy.intercept('DELETE', '/project/*/output*', {
  344. statusCode: 204,
  345. delay: 100,
  346. }).as('clear-cache')
  347. // click the button
  348. cy.findByRole('button', { name: 'Clear cached files' }).click()
  349. cy.findByRole('button', { name: 'Clear cached files' }).should(
  350. 'be.disabled'
  351. )
  352. cy.wait('@clear-cache')
  353. cy.findByRole('button', { name: 'Clear cached files' }).should(
  354. 'not.be.disabled'
  355. )
  356. })
  357. it('handle "recompile from scratch"', function () {
  358. cy.interceptCompile()
  359. const scope = mockScope()
  360. cy.mount(
  361. <EditorProviders scope={scope}>
  362. <div className="pdf-viewer">
  363. <PdfPreview />
  364. </div>
  365. </EditorProviders>
  366. )
  367. cy.findByRole('button', { name: 'Recompile' }).click()
  368. cy.waitForCompile({ pdf: true })
  369. cy.interceptCompile('recompile')
  370. cy.intercept('DELETE', '/project/*/output*', {
  371. statusCode: 204,
  372. delay: 100,
  373. }).as('clear-cache')
  374. // show the logs UI
  375. cy.findByRole('button', { name: 'View logs' }).click()
  376. cy.findByRole('button', { name: 'Clear cached files' }).should(
  377. 'not.be.disabled'
  378. )
  379. // TODO: open the menu?
  380. cy.findByRole('menuitem', {
  381. name: 'Recompile from scratch',
  382. hidden: true,
  383. }).trigger('click', { force: true })
  384. cy.findByRole('button', { name: 'Clear cached files' }).should(
  385. 'be.disabled'
  386. )
  387. cy.findByRole('button', { name: 'Compiling…' })
  388. cy.wait('@clear-cache')
  389. // wait for recompile from scratch to finish
  390. cy.waitForCompile({ pdf: true, prefix: 'recompile' })
  391. cy.findByRole('button', { name: 'Recompile' })
  392. })
  393. })
  394. describe('invalid URLs and broken PDFs', function () {
  395. it('shows an error for an invalid URL', function () {
  396. cy.interceptCompile()
  397. cy.intercept('/build/*/output.pdf*', {
  398. statusCode: 500,
  399. body: {
  400. message: 'something awful happened',
  401. code: 'AWFUL_ERROR',
  402. },
  403. }).as('compile-pdf-error')
  404. const scope = mockScope()
  405. cy.mount(
  406. <EditorProviders scope={scope}>
  407. <div className="pdf-viewer">
  408. <PdfPreview />
  409. </div>
  410. </EditorProviders>
  411. )
  412. cy.findByRole('button', { name: 'Recompile' }).click()
  413. cy.waitForCompile()
  414. cy.wait('@compile-pdf-error')
  415. cy.contains('Something went wrong while rendering this PDF.')
  416. cy.contains(
  417. 'Please try recompiling the project from scratch, and if that doesn’t help, follow our troubleshooting guide.'
  418. )
  419. cy.findByLabelText('Page 1').should('not.exist')
  420. })
  421. it('shows an error for a corrupt PDF', function () {
  422. cy.interceptCompile()
  423. cy.intercept('/build/*/output.pdf*', {
  424. fixture: 'build/output-corrupt.pdf,null',
  425. }).as('compile-pdf-corrupt')
  426. const scope = mockScope()
  427. cy.mount(
  428. <EditorProviders scope={scope}>
  429. <div className="pdf-viewer">
  430. <PdfPreview />
  431. </div>
  432. </EditorProviders>
  433. )
  434. cy.findByRole('button', { name: 'Recompile' }).click()
  435. cy.waitForCompile()
  436. cy.wait('@compile-pdf-corrupt')
  437. cy.contains('Something went wrong while rendering this PDF.')
  438. cy.contains(
  439. 'Please try recompiling the project from scratch, and if that doesn’t help, follow our troubleshooting guide.'
  440. )
  441. cy.findByLabelText('Page 1').should('not.exist')
  442. })
  443. })
  444. describe('human readable logs', function () {
  445. it('shows human readable hint for undefined reference errors', function () {
  446. cy.interceptCompile()
  447. cy.intercept('/build/*/output.log*', {
  448. fixture: 'build/output-human-readable.log',
  449. }).as('compile-log')
  450. const scope = mockScope()
  451. cy.mount(
  452. <EditorProviders scope={scope}>
  453. <div className="pdf-viewer">
  454. <PdfPreview />
  455. </div>
  456. </EditorProviders>
  457. )
  458. cy.findByRole('button', { name: 'Recompile' }).click()
  459. cy.waitForCompile()
  460. cy.findByRole('button', { name: 'View logs' }).click()
  461. cy.findByText(
  462. "Reference `intorduction' on page 1 undefined on input line 11."
  463. )
  464. cy.findByText(
  465. "Reference `section1' on page 1 undefined on input line 13."
  466. )
  467. cy.findByText('There were undefined references.')
  468. cy.findAllByText(
  469. /You have referenced something which has not yet been labelled/
  470. ).should('have.length', 3)
  471. })
  472. it('does not show human readable hint when no undefined reference errors', function () {
  473. cy.interceptCompile()
  474. cy.intercept('/build/*/output.log?*', {
  475. fixture: 'build/output-undefined-references.log',
  476. }).as('compile-log')
  477. const scope = mockScope()
  478. cy.mount(
  479. <EditorProviders scope={scope}>
  480. <div className="pdf-viewer">
  481. <PdfPreview />
  482. </div>
  483. </EditorProviders>
  484. )
  485. cy.findByRole('button', { name: 'Recompile' }).click()
  486. cy.waitForCompile()
  487. cy.findByRole('button', { name: 'View logs' }).click()
  488. cy.findByText(
  489. "Package rerunfilecheck Warning: File `output.brf' has changed. Rerun to get bibliographical references right."
  490. )
  491. cy.findByText(
  492. /You have referenced something which has not yet been labelled/
  493. ).should('not.exist')
  494. })
  495. })
  496. })