pdf-preview.spec.tsx 24 KB

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