pdf-preview.spec.tsx 19 KB

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