overleaf/services/web/test/frontend/features/pdf-preview/components/pdf-preview.test.js

383 lines
12 KiB
JavaScript
Raw Normal View History

import { expect } from 'chai'
import sinon from 'sinon'
import fetchMock from 'fetch-mock'
import { screen, fireEvent, waitFor } from '@testing-library/react'
import PdfPreview from '../../../../../frontend/js/features/pdf-preview/components/pdf-preview'
import { renderWithEditorContext } from '../../../helpers/render-with-context'
const outputFiles = [
{
path: 'output.pdf',
build: '123',
// url: 'about:blank', // TODO: PDF URL to render
type: 'pdf',
},
{
path: 'output.bbl',
build: '123',
url: '/build/output.bbl',
type: 'bbl',
},
{
path: 'output.bib',
build: '123',
url: '/build/output.bib',
type: 'bib',
},
{
path: 'example.txt',
build: '123',
url: '/build/example.txt',
type: 'txt',
},
{
path: 'output.log',
build: '123',
url: '/build/output.log',
type: 'log',
},
{
path: 'output.blg',
build: '123',
url: '/build/output.blg',
type: 'blg',
},
]
const mockCompile = () =>
fetchMock.post('express:/project/:projectId/compile', {
body: {
status: 'success',
clsiServerId: 'foo',
compileGroup: 'priority',
pdfDownloadDomain: '',
outputFiles,
},
})
const mockCompileError = status =>
fetchMock.post('express:/project/:projectId/compile', {
body: {
status,
clsiServerId: 'foo',
compileGroup: 'priority',
},
})
const mockValidationProblems = validationProblems =>
fetchMock.post('express:/project/:projectId/compile', {
body: {
status: 'validation-problems',
validationProblems,
clsiServerId: 'foo',
compileGroup: 'priority',
},
})
const mockClearCache = () =>
fetchMock.delete('express:/project/:projectId/output', 204)
const defaultFileResponses = {
'/build/output.blg': 'This is BibTeX, Version 4.0', // FIXME
'/build/output.log': `
The LaTeX compiler output
* With a lot of details
Wrapped in an HTML <pre> element with
preformatted text which is to be presented exactly
as written in the HTML file
(whitespace included)
The text is typically rendered using a non-proportional ("monospace") font.
LaTeX Font Info: External font \`cmex10' loaded for size
(Font) <7> on input line 18.
LaTeX Font Info: External font \`cmex10' loaded for size
(Font) <5> on input line 18.
! Undefined control sequence.
<recently read> \\Zlpha
main.tex, line 23
`,
}
const mockBuildFile = (responses = defaultFileResponses) =>
fetchMock.get('express:/build/:file', (_url, options, request) => {
const url = new URL(_url, 'https://example.com')
if (url.pathname in responses) {
return responses[url.pathname]
}
return 404
})
const storeAndFireEvent = (key, value) => {
localStorage.setItem(key, value)
fireEvent(window, new StorageEvent('storage', { key }))
}
describe('<PdfPreview/>', function () {
var clock
beforeEach(function () {
clock = sinon.useFakeTimers({
shouldAdvanceTime: true,
now: Date.now(),
})
// xhrMock.setup()
})
afterEach(function () {
clock.runAll()
clock.restore()
// xhrMock.teardown()
fetchMock.reset()
localStorage.clear()
})
it('renders the PDF preview', function () {
renderWithEditorContext(<PdfPreview />)
screen.getByRole('button', { name: 'Recompile' })
})
it('runs a compile only on the first project:joined event', async function () {
mockCompile()
mockBuildFile()
renderWithEditorContext(<PdfPreview />)
// fire a project:joined event => compile
screen.getByRole('button', { name: 'Recompile' })
fireEvent(window, new CustomEvent('project:joined'))
screen.getByRole('button', { name: 'Compiling…' })
await screen.findByRole('button', { name: 'Recompile' })
// fire another project:joined event => no compile
fireEvent(window, new CustomEvent('project:joined'))
screen.getByRole('button', { name: 'Recompile' })
expect(fetchMock.calls()).to.have.length(3)
expect(fetchMock.called('express:/project/:projectId/compile')).to.be.true // TODO: auto_compile query param
expect(fetchMock.called('express:/build/:file')).to.be.true // TODO: actual path
})
it('runs a compile when the Recompile button is pressed', async function () {
mockCompile()
mockBuildFile()
renderWithEditorContext(<PdfPreview />)
// fire a project:joined event => compile
screen.getByRole('button', { name: 'Recompile' })
fireEvent(window, new CustomEvent('project:joined'))
screen.getByRole('button', { name: 'Compiling…' })
await screen.findByRole('button', { name: 'Recompile' })
// press the Recompile button => compile
const button = screen.getByRole('button', { name: 'Recompile' })
button.click()
screen.getByRole('button', { name: 'Compiling…' })
await screen.findByRole('button', { name: 'Recompile' })
expect(fetchMock.calls()).to.have.length(6)
})
it('runs a compile on doc change if autocompile is enabled', async function () {
mockCompile()
mockBuildFile()
renderWithEditorContext(<PdfPreview />)
// fire a project:joined event => compile
screen.getByRole('button', { name: 'Recompile' })
fireEvent(window, new CustomEvent('project:joined'))
screen.getByRole('button', { name: 'Compiling…' })
await screen.findByRole('button', { name: 'Recompile' })
// switch on auto compile
storeAndFireEvent('autocompile_enabled:project123', true)
// fire a doc:changed event => compile
fireEvent(window, new CustomEvent('doc:changed'))
clock.tick(2000) // AUTO_COMPILE_DEBOUNCE
await screen.findByText('Compiling…')
await screen.findByRole('button', { name: 'Recompile' })
expect(fetchMock.calls()).to.have.length(6)
})
it('does not run a compile on doc change if autocompile is disabled', async function () {
mockCompile()
mockBuildFile()
renderWithEditorContext(<PdfPreview />)
// fire a project:joined event => compile
screen.getByRole('button', { name: 'Recompile' })
fireEvent(window, new CustomEvent('project:joined'))
screen.getByRole('button', { name: 'Compiling…' })
await screen.findByRole('button', { name: 'Recompile' })
// make sure auto compile is switched off
storeAndFireEvent('autocompile_enabled:project123', false)
screen.getByRole('button', { name: 'Recompile' })
// fire a doc:changed event => no compile
fireEvent(window, new CustomEvent('doc:changed'))
clock.tick(2000) // AUTO_COMPILE_DEBOUNCE
screen.getByRole('button', { name: 'Recompile' })
expect(fetchMock.calls()).to.have.length(3)
})
it('does not run a compile on doc change if autocompile is blocked by syntax check', async function () {
mockCompile()
mockBuildFile()
renderWithEditorContext(<PdfPreview />, {
scope: {
'settings.syntaxValidation': true, // enable linting in the editor
hasLintingError: true, // mock a linting error
},
})
// fire a project:joined event => compile
screen.getByRole('button', { name: 'Recompile' })
fireEvent(window, new CustomEvent('project:joined'))
screen.getByRole('button', { name: 'Compiling…' })
await screen.findByRole('button', { name: 'Recompile' })
// switch on auto compile and syntax checking
storeAndFireEvent('autocompile_enabled:project123', true)
storeAndFireEvent('stop_on_validation_error:project123', true)
screen.getByRole('button', { name: 'Recompile' })
// fire a doc:changed event => no compile
fireEvent(window, new CustomEvent('doc:changed'))
clock.tick(2000) // AUTO_COMPILE_DEBOUNCE
screen.getByRole('button', { name: 'Recompile' })
await screen.findByText('Code check failed')
expect(fetchMock.calls()).to.have.length(3)
})
it('displays an error message if there was a compile error', async function () {
mockCompileError('compile-in-progress')
renderWithEditorContext(<PdfPreview />)
// fire a project:joined event => compile
screen.getByRole('button', { name: 'Recompile' })
fireEvent(window, new CustomEvent('project:joined'))
screen.getByRole('button', { name: 'Compiling…' })
await screen.findByRole('button', { name: 'Recompile' })
screen.getByText(
'Please wait for your other compile to finish before trying again.'
)
expect(fetchMock.called('express:/project/:projectId/compile')).to.be.true // TODO: auto_compile query param
expect(fetchMock.called('express:/build/:file')).to.be.false // TODO: actual path
})
it('disables the view logs button if there is a compile error', async function () {
mockCompileError()
mockBuildFile()
renderWithEditorContext(<PdfPreview />)
const logsButton = screen.getByRole('button', { name: 'View logs' })
expect(logsButton.hasAttribute('disabled')).to.be.false
// fire a project:joined event => compile
screen.getByRole('button', { name: 'Recompile' })
fireEvent(window, new CustomEvent('project:joined'))
screen.getByRole('button', { name: 'Compiling…' })
expect(logsButton.hasAttribute('disabled')).to.be.false
await screen.findByRole('button', { name: 'Recompile' })
expect(logsButton.hasAttribute('disabled')).to.be.true
expect(fetchMock.called('express:/project/:projectId/compile')).to.be.true // TODO: auto_compile query param
expect(fetchMock.called('express:/build/:file')).to.be.false // TODO: actual path
})
it('displays error messages if there were validation problems', async function () {
const validationProblems = {
sizeCheck: {
resources: [
{ path: 'foo/bar', kbSize: 76221 },
{ path: 'bar/baz', kbSize: 2342 },
],
},
mainFile: true,
conflictedPaths: [
{
path: 'foo/bar',
},
{
path: 'foo/baz',
},
],
}
mockValidationProblems(validationProblems)
renderWithEditorContext(<PdfPreview />)
// fire a project:joined event => compile
screen.getByRole('button', { name: 'Recompile' })
fireEvent(window, new CustomEvent('project:joined'))
screen.getByRole('button', { name: 'Compiling…' })
await screen.findByRole('button', { name: 'Recompile' })
screen.getByText('Project too large')
screen.getByText('Unknown main document')
screen.getByText('Conflicting Paths Found')
expect(fetchMock.called('express:/project/:projectId/compile')).to.be.true // TODO: auto_compile query param
expect(fetchMock.called('express:/build/:file')).to.be.false // TODO: actual path
})
it('sends a clear cache request when the button is pressed', async function () {
mockCompile()
mockBuildFile()
mockClearCache()
renderWithEditorContext(<PdfPreview />)
const logsButton = screen.getByRole('button', { name: 'View logs' })
logsButton.click()
let clearCacheButton = screen.getByRole('button', {
name: 'Clear cached files',
})
expect(clearCacheButton.hasAttribute('disabled')).to.be.false
// fire a project:joined event => compile
screen.getByRole('button', { name: 'Recompile' })
fireEvent(window, new CustomEvent('project:joined'))
screen.getByRole('button', { name: 'Compiling…' })
expect(clearCacheButton.hasAttribute('disabled')).to.be.true
await screen.findByRole('button', { name: 'Recompile' })
logsButton.click()
clearCacheButton = await screen.findByRole('button', {
name: 'Clear cached files',
})
expect(clearCacheButton.hasAttribute('disabled')).to.be.false
// click the button
clearCacheButton.click()
expect(clearCacheButton.hasAttribute('disabled')).to.be.true
await waitFor(() => {
expect(clearCacheButton.hasAttribute('disabled')).to.be.false
})
expect(fetchMock.called('express:/project/:projectId/compile')).to.be.true // TODO: auto_compile query param
expect(fetchMock.called('express:/build/:file')).to.be.true // TODO: actual path
})
})