pdf-preview.spec.tsx 24 KB

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