pdf-preview.spec.tsx 18 KB

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