pdf-preview.spec.tsx 17 KB

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