pdf-preview.spec.tsx 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912
  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, PropsWithChildren, ReactElement, useEffect } from 'react'
  11. import { useLocalCompileContext } from '@/shared/context/local-compile-context'
  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. const PdfViewer = ({ children }: { children: ReactElement }) => {
  27. return (
  28. // TODO: ide-redesign-cleanup: Remove the .ide-redesign-main wrapper when
  29. // the styles are no longer nested in that.
  30. <div className="ide-redesign-main">
  31. <div className="pdf-viewer">{children}</div>
  32. </div>
  33. )
  34. }
  35. describe('<PdfPreview/>', function () {
  36. let projectId: string
  37. beforeEach(function () {
  38. /**
  39. * There are time sensitive tests in this test suite. They need to wait for a Promise before resolving a request.
  40. *
  41. * Using a promise across the test-env (browser) vs stub-env (server) causes additional latency.
  42. *
  43. * This latency seems to stack up when adding more intercepts for the same path. Using static responses for some of these intercepts does not help.
  44. *
  45. * All of that seems like a bug in Cypress. For now just work around it by using a unique projectId for each intercept.
  46. */
  47. projectId = Math.random().toString().slice(2)
  48. window.metaAttributesCache.set('ol-preventCompileOnLoad', true)
  49. window.metaAttributesCache.set(
  50. 'ol-compilesUserContentDomain',
  51. 'https://compiles-user.dev-overleaf.com'
  52. )
  53. window.metaAttributesCache.set('ol-canUseClsiCache', true)
  54. window.metaAttributesCache.set('ol-compileSettings', {
  55. compileTimeout: 240,
  56. })
  57. cy.interceptEvents()
  58. })
  59. it('renders the PDF preview', function () {
  60. window.metaAttributesCache.set('ol-preventCompileOnLoad', false)
  61. cy.interceptCompile()
  62. const scope = mockScope()
  63. cy.mount(
  64. <EditorProviders scope={scope}>
  65. <PdfViewer>
  66. <PdfPreview />
  67. </PdfViewer>
  68. </EditorProviders>
  69. )
  70. // wait for "compile on load" to finish
  71. cy.waitForCompile({ pdf: true })
  72. cy.findByRole('button', { name: 'Recompile' })
  73. })
  74. it('uses the cache when available', function () {
  75. cy.interceptCompile({
  76. prefix: 'compile',
  77. times: 1,
  78. cached: true,
  79. regular: false,
  80. })
  81. const scope = mockScope()
  82. cy.mount(
  83. <EditorProviders scope={scope}>
  84. <PdfViewer>
  85. <PdfPreview />
  86. </PdfViewer>
  87. </EditorProviders>
  88. )
  89. // wait for "compile from cache on load" to finish
  90. cy.waitForCompile({ pdf: true, cached: true, regular: false })
  91. cy.contains('Your Paper')
  92. })
  93. it('uses the cache when available then compiles', function () {
  94. cy.interceptCompile({
  95. prefix: 'compile',
  96. times: 1,
  97. cached: true,
  98. regular: false,
  99. })
  100. const scope = mockScope()
  101. cy.mount(
  102. <EditorProviders scope={scope}>
  103. <PdfViewer>
  104. <PdfPreview />
  105. </PdfViewer>
  106. </EditorProviders>
  107. )
  108. // wait for "compile from cache on load" to finish
  109. cy.waitForCompile({ pdf: true, cached: true, regular: false })
  110. cy.contains('Your Paper')
  111. // Then trigger a new compile
  112. cy.interceptCompile({
  113. prefix: 'recompile',
  114. times: 1,
  115. cached: false,
  116. outputPDFFixture: 'output-2.pdf',
  117. })
  118. // press the Recompile button => compile
  119. cy.findByRole('button', { name: 'Recompile' }).click()
  120. // wait for compile to finish
  121. cy.waitForCompile({ prefix: 'recompile', pdf: true })
  122. cy.contains('Modern Authoring Tools for Science')
  123. })
  124. describe('racing compile from cache and regular compile trigger', function () {
  125. for (const [timing] of ['before rendering', 'after rendering']) {
  126. it(`replaces the compile from cache with a regular compile - ${timing}`, function () {
  127. const requestedOnce = new Set()
  128. ;['log', 'pdf', 'blg'].forEach(ext => {
  129. cy.intercept({ pathname: `/build/*/output.${ext}` }, req => {
  130. if (requestedOnce.has(ext)) {
  131. throw new Error(
  132. `compile from cache triggered extra ${ext} request: ${req.url}`
  133. )
  134. }
  135. requestedOnce.add(ext)
  136. req.reply({ fixture: `build/output.${ext},null` })
  137. }).as(`compile-${ext}`)
  138. })
  139. const { promise, resolve } = Promise.withResolvers<void>()
  140. cy.interceptCompileFromCacheRequest({
  141. promise,
  142. times: 1,
  143. }).as('cached-compile')
  144. cy.interceptCompileRequest().as('compile')
  145. const scope = mockScope()
  146. cy.mount(
  147. <EditorProviders scope={scope} projectId={projectId}>
  148. <PdfViewer>
  149. <PdfPreview />
  150. </PdfViewer>
  151. </EditorProviders>
  152. )
  153. // press the Recompile button => compile
  154. cy.findByRole('button', { name: 'Recompile' }).click()
  155. if (timing === 'before rendering') {
  156. cy.then(() => resolve())
  157. cy.wait('@cached-compile')
  158. }
  159. // wait for rendering to finish
  160. cy.waitForCompile({ pdf: true, cached: false })
  161. if (timing === 'after rendering') {
  162. cy.then(() => resolve())
  163. cy.wait('@cached-compile')
  164. }
  165. cy.contains('Your Paper')
  166. cy.then(() => Array.from(requestedOnce).sort().join(',')).should(
  167. 'equal',
  168. 'blg,log,pdf'
  169. )
  170. })
  171. }
  172. })
  173. describe('clsi-cache project settings validation', function () {
  174. const cases = {
  175. // Flaky, skip for now
  176. 'uses compile from cache when nothing changed': {
  177. cached: true,
  178. setup: () => {},
  179. props: {},
  180. },
  181. 'ignores the compile from cache when draft mode changed': {
  182. cached: false,
  183. setup: () => {
  184. cy.window().then(w =>
  185. w.localStorage.setItem(`draft:${projectId}`, 'true')
  186. )
  187. },
  188. props: {},
  189. },
  190. 'ignores the compile from cache when stopOnFirstError mode changed': {
  191. cached: false,
  192. setup: () => {
  193. cy.window().then(w =>
  194. w.localStorage.setItem(`stop_on_first_error:${projectId}`, 'true')
  195. )
  196. },
  197. props: {},
  198. },
  199. 'ignores the compile from cache when rootDoc changed': {
  200. cached: false,
  201. setup: () => {},
  202. props: {
  203. rootDocId: 'new-root-doc-id',
  204. rootFolder: [
  205. {
  206. _id: 'root-folder-id',
  207. name: 'rootFolder',
  208. docs: [
  209. {
  210. _id: '_root_doc_id',
  211. name: 'main.tex',
  212. },
  213. {
  214. _id: 'new-root-doc-id',
  215. name: 'new-main.tex',
  216. },
  217. ],
  218. folders: [],
  219. fileRefs: [],
  220. },
  221. ],
  222. },
  223. },
  224. }
  225. Object.entries(cases).forEach(([name, { cached, setup, props }]) => {
  226. it(name, function () {
  227. cy.interceptCompile({
  228. cached: true,
  229. regular: !cached,
  230. })
  231. const scope = mockScope()
  232. window.metaAttributesCache.set('ol-preventCompileOnLoad', false)
  233. setup()
  234. cy.mount(
  235. <EditorProviders scope={scope} projectId={projectId} {...props}>
  236. <PdfViewer>
  237. <PdfPreview />
  238. </PdfViewer>
  239. </EditorProviders>
  240. )
  241. // wait for compile to finish
  242. cy.waitForCompile({ pdf: true, cached, regular: !cached })
  243. cy.contains('Your Paper')
  244. })
  245. })
  246. })
  247. it('runs a compile when the Recompile button is pressed', function () {
  248. cy.interceptCompile()
  249. const scope = mockScope()
  250. cy.mount(
  251. <EditorProviders scope={scope}>
  252. <PdfViewer>
  253. <PdfPreview />
  254. </PdfViewer>
  255. </EditorProviders>
  256. )
  257. // press the Recompile button => compile
  258. cy.findByRole('button', { name: 'Recompile' }).click()
  259. // wait for compile to finish
  260. cy.waitForCompile({ pdf: true })
  261. cy.contains('Your Paper')
  262. })
  263. it('runs a compile on `pdf:recompile` event', function () {
  264. cy.interceptCompile()
  265. const scope = mockScope()
  266. cy.mount(
  267. <EditorProviders scope={scope}>
  268. <PdfViewer>
  269. <PdfPreview />
  270. </PdfViewer>
  271. </EditorProviders>
  272. )
  273. cy.window().then(win => {
  274. win.dispatchEvent(new CustomEvent('pdf:recompile'))
  275. })
  276. // wait for compile to finish
  277. cy.waitForCompile({ pdf: true })
  278. cy.contains('Your Paper')
  279. })
  280. it('does not compile while compiling', function () {
  281. let counter = 0
  282. cy.interceptDeferredCompile(() => counter++).then(
  283. resolveDeferredCompile => {
  284. const scope = mockScope()
  285. cy.mount(
  286. <EditorProviders scope={scope}>
  287. <PdfViewer>
  288. <PdfPreview />
  289. </PdfViewer>
  290. </EditorProviders>
  291. )
  292. // start compiling
  293. cy.findByRole('button', { name: 'Recompile' }).click()
  294. cy.findByRole('button', { name: 'Compiling…' }).then(() => {
  295. // trigger a recompile
  296. cy.window().then(win => {
  297. win.dispatchEvent(new CustomEvent('pdf:recompile'))
  298. })
  299. // finish the original compile
  300. resolveDeferredCompile()
  301. // wait for the original compile to finish
  302. cy.waitForCompile().then(() => {
  303. // NOTE: difficult to assert that a second request won't be sent, at some point
  304. expect(counter).to.equal(1)
  305. })
  306. })
  307. }
  308. )
  309. })
  310. it('disables compile button while compile is running', function () {
  311. cy.interceptDeferredCompile().then(resolveDeferredCompile => {
  312. const scope = mockScope()
  313. cy.mount(
  314. <EditorProviders scope={scope}>
  315. <PdfViewer>
  316. <PdfPreview />
  317. </PdfViewer>
  318. </EditorProviders>
  319. )
  320. cy.findByRole('button', { name: 'Recompile' }).click()
  321. cy.findByRole('button', { name: 'Compiling…' })
  322. .should('be.disabled')
  323. .then(() => resolveDeferredCompile())
  324. cy.waitForCompile()
  325. cy.findByRole('button', { name: 'Recompile' }).should('not.be.disabled')
  326. })
  327. })
  328. it('runs a compile on doc change if autocompile is enabled', function () {
  329. cy.interceptCompile()
  330. const scope = mockScope()
  331. cy.mount(
  332. <EditorProviders scope={scope}>
  333. <PdfViewer>
  334. <PdfPreview />
  335. </PdfViewer>
  336. </EditorProviders>
  337. )
  338. cy.window().then(win => {
  339. cy.clock()
  340. // switch on auto compile
  341. storeAndFireEvent(win, 'autocompile_enabled:project123', true)
  342. // fire a doc:changed event => compile
  343. win.dispatchEvent(new CustomEvent('doc:changed'))
  344. // wait enough time for the compile to start
  345. cy.tick(6000) // > AUTO_COMPILE_DEBOUNCE
  346. cy.clock().invoke('restore')
  347. })
  348. // wait for compile to finish
  349. cy.waitForCompile({ pdf: true })
  350. cy.findByRole('button', { name: 'Recompile' })
  351. })
  352. it('does not run a compile on doc change if autocompile is disabled', function () {
  353. cy.interceptCompile()
  354. const scope = mockScope()
  355. cy.mount(
  356. <EditorProviders scope={scope}>
  357. <PdfViewer>
  358. <PdfPreview />
  359. </PdfViewer>
  360. </EditorProviders>
  361. )
  362. cy.window().then(win => {
  363. cy.clock()
  364. // make sure auto compile is switched off
  365. storeAndFireEvent(win, 'autocompile_enabled:project123', false)
  366. // fire a doc:changed event => no compile
  367. win.dispatchEvent(new CustomEvent('doc:changed'))
  368. // wait enough time for the compile to start
  369. cy.tick(6000) // AUTO_COMPILE_DEBOUNCE
  370. cy.clock().invoke('restore')
  371. })
  372. // NOTE: difficult to assert that a request hasn't been sent
  373. cy.findByRole('button', { name: 'Recompile' })
  374. })
  375. it('does not run a compile on doc change if autocompile is blocked by syntax check', function () {
  376. cy.interceptCompile()
  377. const scope = mockScope()
  378. // enable linting in the editor
  379. const userSettings = { syntaxValidation: true }
  380. const WithLintingErrors: FC<PropsWithChildren> = ({ children }) => {
  381. const { setHasLintingError } = useLocalCompileContext()
  382. useEffect(() => setHasLintingError(true), [setHasLintingError])
  383. return children
  384. }
  385. cy.mount(
  386. <EditorProviders scope={scope} userSettings={userSettings}>
  387. <WithLintingErrors>
  388. <PdfViewer>
  389. <PdfPreview />
  390. </PdfViewer>
  391. </WithLintingErrors>
  392. </EditorProviders>
  393. )
  394. cy.window().then(win => {
  395. cy.clock()
  396. // switch on auto compile
  397. storeAndFireEvent(win, 'autocompile_enabled:project123', true)
  398. // switch on syntax checking
  399. storeAndFireEvent(win, 'stop_on_validation_error', true)
  400. // fire a doc:changed event => no compile
  401. win.dispatchEvent(new CustomEvent('doc:changed'))
  402. // wait enough time for the compile to start
  403. cy.tick(6000) // AUTO_COMPILE_DEBOUNCE
  404. cy.clock().invoke('restore')
  405. })
  406. // NOTE: difficult to assert that a request hasn't been sent
  407. cy.findByRole('button', { name: 'Recompile' })
  408. cy.findByText(
  409. 'Your code has errors that need to be fixed before the auto-compile can run'
  410. )
  411. })
  412. it('does not run a compile on doc change if the PDF preview is not open', function () {
  413. cy.interceptCompile()
  414. const scope = mockScope()
  415. cy.mount(
  416. <EditorProviders scope={scope}>
  417. <Layout layout="flat" view="editor" />
  418. <PdfViewer>
  419. <PdfPreview />
  420. </PdfViewer>
  421. </EditorProviders>
  422. )
  423. cy.window().then(win => {
  424. cy.clock()
  425. // switch on auto compile
  426. storeAndFireEvent(win, 'autocompile_enabled:project123', true)
  427. // fire a doc:changed event => compile
  428. win.dispatchEvent(new CustomEvent('doc:changed'))
  429. // wait enough time for the compile to start
  430. cy.tick(6000) // > AUTO_COMPILE_DEBOUNCE
  431. cy.clock().invoke('restore')
  432. })
  433. // NOTE: difficult to assert that a request hasn't been sent
  434. cy.findByRole('button', { name: 'Recompile' })
  435. })
  436. describe('error messages', function () {
  437. const compileErrorStatuses = {
  438. 'clear-cache':
  439. 'Sorry, something went wrong and your project could not be compiled. Please try again in a few moments.',
  440. 'clsi-maintenance':
  441. 'The compile servers are down for maintenance, and will be back shortly.',
  442. 'compile-in-progress':
  443. 'A previous compile is still running. Please wait a minute and try compiling again.',
  444. exited: 'Server Error',
  445. failure: 'No PDF',
  446. generic: 'Server Error',
  447. 'project-too-large': 'Project too large',
  448. 'rate-limited': 'Compile rate limit hit',
  449. terminated: 'Compilation cancelled',
  450. timedout: 'Timed out',
  451. 'too-recently-compiled':
  452. 'This project was compiled very recently, so this compile has been skipped.',
  453. unavailable:
  454. 'Sorry, the compile server for your project was temporarily unavailable. Please try again in a few moments.',
  455. foo: 'Sorry, something went wrong and your project could not be compiled. Please try again in a few moments.',
  456. }
  457. for (const [status, message] of Object.entries(compileErrorStatuses)) {
  458. it(`displays error message for '${status}' status`, function () {
  459. cy.intercept('POST', '/project/*/compile*', {
  460. body: {
  461. status,
  462. clsiServerId: 'foo',
  463. compileGroup: 'priority',
  464. },
  465. }).as('compile')
  466. const scope = mockScope()
  467. cy.mount(
  468. <EditorProviders scope={scope}>
  469. <PdfViewer>
  470. <PdfPreview />
  471. </PdfViewer>
  472. </EditorProviders>
  473. )
  474. cy.findByRole('button', { name: 'Recompile' }).click()
  475. cy.wait('@compile')
  476. cy.findByText(message)
  477. })
  478. }
  479. it('displays error messages expanded by default', function () {
  480. cy.intercept('POST', '/project/*/compile*', {
  481. body: {
  482. status: 'failure',
  483. clsiServerId: 'foo',
  484. compileGroup: 'priority',
  485. },
  486. }).as('compile')
  487. const scope = mockScope()
  488. cy.mount(
  489. <EditorProviders scope={scope}>
  490. <PdfViewer>
  491. <PdfPreview />
  492. </PdfViewer>
  493. </EditorProviders>
  494. )
  495. cy.findByRole('button', { name: 'Recompile' }).click()
  496. cy.wait('@compile')
  497. // The error message should be visible (expanded by default)
  498. cy.findByText('No PDF')
  499. cy.findByText(/This compile didn’t produce a PDF/)
  500. // The collapse button should be available (meaning it's expanded)
  501. cy.findByLabelText(
  502. 'An error which prevented this project from compiling'
  503. ).within(() => {
  504. cy.findByRole('button', { name: 'Collapse' })
  505. })
  506. })
  507. })
  508. it('displays expandable raw logs', function () {
  509. cy.interceptCompile()
  510. const scope = mockScope()
  511. cy.mount(
  512. <EditorProviders scope={scope}>
  513. <PdfViewer>
  514. <PdfPreview />
  515. </PdfViewer>
  516. </EditorProviders>
  517. )
  518. cy.findByRole('button', { name: 'Recompile' }).click()
  519. cy.waitForCompile({ pdf: true })
  520. cy.findByRole('button', { name: 'View logs' }).click()
  521. cy.findByRole('button', { name: 'Back to PDF' })
  522. cy.findByLabelText('Raw logs from the LaTeX compiler').within(() => {
  523. cy.findByRole('button', { name: 'Expand' }).click()
  524. cy.findByRole('button', { name: 'Collapse' }).click()
  525. })
  526. })
  527. it('displays error messages if there were validation problems', function () {
  528. const validationProblems = {
  529. sizeCheck: {
  530. resources: [
  531. { path: 'foo/bar', kbSize: 76221 },
  532. { path: 'bar/baz', kbSize: 2342 },
  533. ],
  534. },
  535. mainFile: true,
  536. conflictedPaths: [
  537. {
  538. path: 'foo/bar',
  539. },
  540. {
  541. path: 'foo/baz',
  542. },
  543. ],
  544. }
  545. cy.intercept('POST', '/project/*/compile*', {
  546. body: {
  547. status: 'validation-problems',
  548. validationProblems,
  549. clsiServerId: 'foo',
  550. compileGroup: 'priority',
  551. },
  552. }).as('compile')
  553. const scope = mockScope()
  554. cy.mount(
  555. <EditorProviders scope={scope}>
  556. <PdfViewer>
  557. <PdfPreview />
  558. </PdfViewer>
  559. </EditorProviders>
  560. )
  561. cy.findByRole('button', { name: 'Recompile' }).click()
  562. cy.wait('@compile')
  563. cy.findByText('Project too large')
  564. cy.findByText('Unknown main document')
  565. cy.findByText('Conflicting Paths Found')
  566. })
  567. describe('clear cache', function () {
  568. it('sends a clear cache request when the button is pressed', function () {
  569. cy.interceptCompile()
  570. const scope = mockScope()
  571. cy.mount(
  572. <EditorProviders scope={scope}>
  573. <PdfViewer>
  574. <PdfPreview />
  575. </PdfViewer>
  576. </EditorProviders>
  577. )
  578. cy.findByRole('button', { name: 'Recompile' }).click()
  579. cy.waitForCompile({ pdf: true })
  580. cy.findByRole('button', { name: 'View logs' }).click()
  581. cy.findByRole('button', { name: 'Clear cached files' }).should(
  582. 'not.be.disabled'
  583. )
  584. const { promise, resolve } = Promise.withResolvers<void>()
  585. cy.intercept('DELETE', '/project/*/output*', req => {
  586. return promise
  587. .then(() => Cypress.Promise.delay(100))
  588. .then(() => {
  589. req.reply({ statusCode: 204 })
  590. })
  591. }).as('clear-cache')
  592. // click the button
  593. cy.findByRole('button', { name: 'Clear cached files' }).click()
  594. cy.findByRole('button', { name: 'Clear cached files' }).should(
  595. 'be.disabled'
  596. )
  597. cy.then(() => {
  598. resolve()
  599. })
  600. cy.wait('@clear-cache')
  601. cy.findByRole('button', { name: 'Clear cached files' }).should(
  602. 'not.be.disabled'
  603. )
  604. })
  605. it('handle "recompile from scratch"', function () {
  606. cy.interceptCompile()
  607. const scope = mockScope()
  608. cy.mount(
  609. <EditorProviders scope={scope}>
  610. <PdfViewer>
  611. <PdfPreview />
  612. </PdfViewer>
  613. </EditorProviders>
  614. )
  615. cy.findByRole('button', { name: 'Recompile' }).click()
  616. cy.waitForCompile({ pdf: true })
  617. cy.interceptCompile({ prefix: 'recompile' })
  618. cy.intercept('DELETE', '/project/*/output*', {
  619. statusCode: 204,
  620. delay: 100,
  621. }).as('clear-cache')
  622. // show the logs UI
  623. cy.findByRole('button', { name: 'View logs' }).click()
  624. cy.findByRole('button', { name: 'Clear cached files' }).should(
  625. 'not.be.disabled'
  626. )
  627. cy.interceptDeferredCompile().then(resolveDeferredCompile => {
  628. cy.findByRole('button', { name: 'Toggle compile options menu' }).click()
  629. cy.findByRole('menuitem', {
  630. name: 'Recompile from scratch',
  631. }).trigger('click')
  632. cy.findByRole('button', { name: 'Clear cached files' }).should(
  633. 'be.disabled'
  634. )
  635. cy.wait('@clear-cache')
  636. cy.findByRole('button', { name: 'Compiling…' }).then(() =>
  637. resolveDeferredCompile()
  638. )
  639. // wait for recompile from scratch to finish
  640. cy.waitForCompile({ pdf: true })
  641. cy.findByRole('button', { name: 'Recompile' })
  642. })
  643. })
  644. })
  645. describe('invalid URLs and broken PDFs', function () {
  646. it('shows an error for an invalid URL', function () {
  647. cy.interceptCompile()
  648. cy.intercept('/build/*/output.pdf*', {
  649. statusCode: 500,
  650. body: {
  651. message: 'something awful happened',
  652. code: 'AWFUL_ERROR',
  653. },
  654. }).as('compile-pdf-error')
  655. const scope = mockScope()
  656. cy.mount(
  657. <EditorProviders scope={scope}>
  658. <PdfViewer>
  659. <PdfPreview />
  660. </PdfViewer>
  661. </EditorProviders>
  662. )
  663. cy.findByRole('button', { name: 'Recompile' }).click()
  664. cy.waitForCompile()
  665. cy.wait('@compile-pdf-error')
  666. cy.contains('Something went wrong while rendering this PDF.')
  667. cy.contains(
  668. 'Please try recompiling the project from scratch, and if that doesn’t help, follow our troubleshooting guide.'
  669. )
  670. cy.findByLabelText('Page 1').should('not.exist')
  671. })
  672. it('shows an error for a corrupt PDF', function () {
  673. cy.interceptCompile()
  674. cy.intercept('/build/*/output.pdf*', {
  675. fixture: 'build/output-corrupt.pdf,null',
  676. }).as('compile-pdf-corrupt')
  677. const scope = mockScope()
  678. cy.mount(
  679. <EditorProviders scope={scope}>
  680. <PdfViewer>
  681. <PdfPreview />
  682. </PdfViewer>
  683. </EditorProviders>
  684. )
  685. cy.findByRole('button', { name: 'Recompile' }).click()
  686. cy.waitForCompile()
  687. cy.wait('@compile-pdf-corrupt')
  688. cy.contains('Something went wrong while rendering this PDF.')
  689. cy.contains(
  690. 'Please try recompiling the project from scratch, and if that doesn’t help, follow our troubleshooting guide.'
  691. )
  692. cy.findByLabelText('Page 1').should('not.exist')
  693. })
  694. })
  695. describe('human readable logs', function () {
  696. it('shows human readable hint for undefined reference errors', function () {
  697. cy.interceptCompile()
  698. cy.intercept('/build/*/output.log*', {
  699. fixture: 'build/output-human-readable.log',
  700. }).as('compile-log')
  701. const scope = mockScope()
  702. cy.mount(
  703. <EditorProviders scope={scope}>
  704. <PdfViewer>
  705. <PdfPreview />
  706. </PdfViewer>
  707. </EditorProviders>
  708. )
  709. cy.findByRole('button', { name: 'Recompile' }).click()
  710. cy.waitForCompile()
  711. cy.findByRole('button', { name: 'View logs' }).click()
  712. cy.findByText(
  713. "Reference `intorduction' on page 1 undefined on input line 11."
  714. )
  715. cy.findByText(
  716. "Reference `section1' on page 1 undefined on input line 13."
  717. )
  718. cy.findByText('There were undefined references.')
  719. cy.findAllByText(
  720. /You have referenced something which has not yet been labelled/
  721. ).should('have.length', 3)
  722. })
  723. it('does not show human readable hint when no undefined reference errors', function () {
  724. cy.interceptCompile()
  725. cy.intercept('/build/*/output.log?*', {
  726. fixture: 'build/output-undefined-references.log',
  727. }).as('compile-log')
  728. const scope = mockScope()
  729. cy.mount(
  730. <EditorProviders scope={scope}>
  731. <PdfViewer>
  732. <PdfPreview />
  733. </PdfViewer>
  734. </EditorProviders>
  735. )
  736. cy.findByRole('button', { name: 'Recompile' }).click()
  737. cy.waitForCompile()
  738. cy.findByRole('button', { name: 'View logs' }).click()
  739. cy.findByText(
  740. "Package rerunfilecheck Warning: File `output.brf' has changed. Rerun to get bibliographical references right."
  741. )
  742. cy.findByText(
  743. /You have referenced something which has not yet been labelled/
  744. ).should('not.exist')
  745. })
  746. })
  747. })