pdf-preview.spec.tsx 25 KB

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