Migrate worker tests to Cypress (#7359)

GitOrigin-RevId: f373f4215e5f25d14256008cf5f6582eb3124431
This commit is contained in:
Alf Eaton 2022-04-06 11:14:43 +01:00 committed by Copybot
parent 69a2283984
commit 5e9af2c15c
48 changed files with 1812 additions and 2052 deletions

726
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -104,7 +104,7 @@
}, },
{ {
// Cypress specific rules // Cypress specific rules
"files": ["cypress/**/*.js", "**/test/frontend/**/*.spec.js"], "files": ["cypress/**/*.{js,ts,tsx}", "**/test/frontend/**/*.spec.{js,ts,tsx}"],
"extends": [ "extends": [
"plugin:cypress/recommended" "plugin:cypress/recommended"
] ]

View file

@ -91,3 +91,5 @@ cypress/downloads/
# Ace themes for conversion # Ace themes for conversion
modules/source-editor/frontend/js/themes/ace/ modules/source-editor/frontend/js/themes/ace/
!**/fixtures/**/*.log

View file

@ -1,11 +1,10 @@
{ {
"component": { "component": {
"componentFolder": ".", "componentFolder": ".",
"testFiles": "./{test,modules/**/test}/frontend/components/**/*.spec.js", "testFiles": "./{test,modules/**/test}/frontend/components/**/*.spec.{js,ts,tsx}",
"supportFile": "cypress/support/ct/index.js" "supportFile": "cypress/support/ct/index.ts"
}, },
"experimentalFetchPolyfill": true, "fixturesFolder": "cypress/fixtures",
"fixturesFolder": false,
"video": false, "video": false,
"viewportHeight": 800, "viewportHeight": 800,
"viewportWidth": 800 "viewportWidth": 800

View file

@ -0,0 +1,21 @@
log This is pdfTeX, Version 3.14159265-2.6-1.40.21 (TeX Live 2020) (preloaded format=pdflatex 2020.9.10) 8 FEB 2022 16:27
entering extended mode
\write18 enabled.
%&-line parsing enabled.
**main.tex
(./main.tex
LaTeX2e <2020-02-02> patch level 5
LaTeX Warning: Reference `intorduction' on page 1 undefined on input line 11.
LaTeX Warning: Reference `section1' on page 1 undefined on input line 13.
[1
{/usr/local/texlive/2020/texmf-var/fonts/map/pdftex/updmap/pdftex.map}] (/compi
le/output.aux)
LaTeX Warning: There were undefined references.
)

View file

@ -0,0 +1,10 @@
Package rerunfilecheck Info: File `output.out' has not changed.
(rerunfilecheck) Checksum: 339DB29951BB30436898BC39909EA4FA;11265.
Package rerunfilecheck Warning: File `output.brf' has changed.
(rerunfilecheck) Rerun to get bibliographical references right.
Package rerunfilecheck Info: Checksums for `output.brf':
(rerunfilecheck) Before: D41D8CD98F00B204E9800998ECF8427E;0
(rerunfilecheck) After: DF3260FAD3828D54C5E4E9337E97F7AF;4841.
)

View file

@ -0,0 +1 @@
This is BibTeX, Version 4.0

View file

@ -0,0 +1,19 @@
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

View file

@ -3,17 +3,40 @@ module.exports = (on, config) => {
const { startDevServer } = require('@cypress/webpack-dev-server') const { startDevServer } = require('@cypress/webpack-dev-server')
const { merge } = require('webpack-merge') const { merge } = require('webpack-merge')
const path = require('path') const path = require('path')
const webpack = require('webpack')
const devConfig = require('../../webpack.config.dev') const devConfig = require('../../webpack.config.dev')
const webpackConfig = merge(devConfig, { const webpackConfig = merge(devConfig, {
devServer: { devServer: {
static: path.join(__dirname, '../../../../public'), static: path.join(__dirname, '../../public'),
}, },
stats: 'none', stats: 'none',
plugins: [
new webpack.EnvironmentPlugin({
CYPRESS: true,
}),
],
}) })
delete webpackConfig.devServer.client delete webpackConfig.devServer.client
webpackConfig.entry = {}
const addWorker = (name, importPath) => {
webpackConfig.entry[name] = require.resolve(importPath)
}
// add entrypoint under '/' for latex-linter worker
addWorker(
'latex-linter-worker',
'../../modules/source-editor/frontend/js/languages/latex/linter/latex-linter.worker.js'
)
// add entrypoints under '/' for pdfjs workers
const pdfjsVersions = ['pdfjs-dist210', 'pdfjs-dist213']
for (const name of pdfjsVersions) {
addWorker(name, `${name}/legacy/build/pdf.worker.js`)
}
on('dev-server:start', options => { on('dev-server:start', options => {
return startDevServer({ options, webpackConfig }) return startDevServer({ options, webpackConfig })
}) })

View file

@ -1,3 +0,0 @@
require('../../../frontend/stylesheets/style.less')
require('../shared/exceptions')
require('./i18n')

View file

@ -0,0 +1,5 @@
import '../../../frontend/stylesheets/style.less'
import './window' // needs to be before i18n
import '../../../frontend/js/i18n'
import '../shared/commands'
import '../shared/exceptions'

View file

@ -1,4 +1,2 @@
window.i18n = { currentLangCode: 'en' } window.i18n = { currentLangCode: 'en' }
window.ExposedSettings = { appName: 'Overleaf' } window.ExposedSettings = { appName: 'Overleaf' }
require('../../../frontend/js/i18n')

View file

@ -0,0 +1,3 @@
import '@testing-library/cypress/add-commands'
import './compile'
import './events'

View file

@ -0,0 +1,68 @@
import { v4 as uuid } from 'uuid'
const outputFiles = () => {
const build = uuid()
return [
{
path: 'output.pdf',
build,
url: `/build/${build}/output.pdf`,
type: 'pdf',
},
{
path: 'output.bbl',
build,
url: `/build/${build}/output.bbl`,
type: 'bbl',
},
{
path: 'output.bib',
build,
url: `/build/${build}/output.bib`,
type: 'bib',
},
{
path: 'example.txt',
build,
url: `/build/${build}/example.txt`,
type: 'txt',
},
{
path: 'output.log',
build,
url: `/build/${build}/output.log`,
type: 'log',
},
{
path: 'output.blg',
build,
url: `/build/${build}/output.blg`,
type: 'blg',
},
]
}
Cypress.Commands.add('interceptCompile', (prefix = 'compile') => {
cy.intercept('POST', '/project/*/compile*', {
body: {
status: 'success',
clsiServerId: 'foo',
compileGroup: 'priority',
pdfDownloadDomain: 'https://clsi.test-overleaf.com',
outputFiles: outputFiles(),
},
}).as(`${prefix}`)
cy.intercept('/build/*/output.pdf*', {
fixture: 'build/output.pdf,null',
}).as(`${prefix}-pdf`)
cy.intercept('/build/*/output.log*', {
fixture: 'build/output.log',
}).as(`${prefix}-log`)
cy.intercept('/build/*/output.blg*', {
fixture: 'build/output.blg',
}).as(`${prefix}-blg`)
})

View file

@ -0,0 +1,5 @@
Cypress.Commands.add('interceptEvents', () => {
cy.intercept('POST', '/event/*', {
statusCode: 204,
})
})

View file

@ -0,0 +1,8 @@
// eslint-disable-next-line no-unused-vars
declare namespace Cypress {
// eslint-disable-next-line no-unused-vars
interface Chainable {
interceptCompile(prefix?: string): void
interceptEvents(): void
}
}

View file

@ -25,7 +25,7 @@ export default function BetaBadge({ tooltip, url = '/beta/participate' }) {
} }
BetaBadge.propTypes = { BetaBadge.propTypes = {
tooltip: PropTypes.exact({ tooltip: PropTypes.shape({
id: PropTypes.string.isRequired, id: PropTypes.string.isRequired,
text: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired, text: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired,
placement: PropTypes.string, placement: PropTypes.string,

View file

@ -1,4 +1,7 @@
export const createWorker = callback => { export const createWorker = callback => {
if (process.env.CYPRESS) {
return callback()
}
const webpackPublicPath = __webpack_public_path__ const webpackPublicPath = __webpack_public_path__
__webpack_public_path__ = '/' __webpack_public_path__ = '/'
callback() callback()

View file

@ -116,6 +116,7 @@
"daterangepicker": "https://github.com/overleaf/daterangepicker/archive/e496d2d44ca53e208c930e4cb4bcf29bcefa4550.tar.gz", "daterangepicker": "https://github.com/overleaf/daterangepicker/archive/e496d2d44ca53e208c930e4cb4bcf29bcefa4550.tar.gz",
"downshift": "^6.1.0", "downshift": "^6.1.0",
"east": "^2.0.2", "east": "^2.0.2",
"events": "^3.3.0",
"express": "4.17.1", "express": "4.17.1",
"express-bearer-token": "^2.4.0", "express-bearer-token": "^2.4.0",
"express-http-proxy": "^1.6.0", "express-http-proxy": "^1.6.0",
@ -218,6 +219,7 @@
"@testing-library/react": "^11.2.7", "@testing-library/react": "^11.2.7",
"@testing-library/react-hooks": "^7.0.0", "@testing-library/react-hooks": "^7.0.0",
"@types/chai": "^4.3.0", "@types/chai": "^4.3.0",
"@types/events": "^3.0.0",
"@types/mocha": "^9.1.0", "@types/mocha": "^9.1.0",
"@types/react": "^17.0.40", "@types/react": "^17.0.40",
"@types/react-bootstrap": "^0.32.29", "@types/react-bootstrap": "^0.32.29",
@ -264,6 +266,7 @@
"fetch-mock": "^9.10.2", "fetch-mock": "^9.10.2",
"glob": "^7.1.6", "glob": "^7.1.6",
"handlebars-loader": "^1.7.1", "handlebars-loader": "^1.7.1",
"html-webpack-plugin": "^5.5.0",
"i18next-scanner": "^3.0.0", "i18next-scanner": "^3.0.0",
"jsdom": "^19.0.0", "jsdom": "^19.0.0",
"jsdom-global": "^3.0.2", "jsdom-global": "^3.0.2",

View file

@ -0,0 +1,75 @@
import { mount } from '@cypress/react'
import sysendTestHelper from '../../helpers/sysend'
import { EditorProviders } from '../../helpers/editor-providers'
import DetachCompileButton from '../../../../frontend/js/features/pdf-preview/components/detach-compile-button'
import { mockScope } from './scope'
describe('<DetachCompileButton/>', function () {
beforeEach(function () {
cy.interceptCompile()
cy.interceptEvents()
})
afterEach(function () {
window.metaAttributesCache = new Map()
sysendTestHelper.resetHistory()
})
it('detacher mode and not linked: does not show button ', function () {
cy.window().then(win => {
win.metaAttributesCache = new Map([['ol-detachRole', 'detacher']])
})
const scope = mockScope()
mount(
<EditorProviders scope={scope}>
<DetachCompileButton />
</EditorProviders>
)
cy.findByRole('button', { name: 'Recompile' }).should('not.exist')
})
it('detacher mode and linked: show button ', function () {
cy.window().then(win => {
win.metaAttributesCache = new Map([['ol-detachRole', 'detacher']])
})
const scope = mockScope()
mount(
<EditorProviders scope={scope}>
<DetachCompileButton />
</EditorProviders>
).then(() => {
sysendTestHelper.receiveMessage({
role: 'detached',
event: 'connected',
})
})
cy.findByRole('button', { name: 'Recompile' })
})
it('not detacher mode and linked: does not show button ', function () {
cy.window().then(win => {
win.metaAttributesCache = new Map([['ol-detachRole', 'detached']])
})
const scope = mockScope()
mount(
<EditorProviders scope={scope}>
<DetachCompileButton />
</EditorProviders>
).then(() => {
sysendTestHelper.receiveMessage({
role: 'detacher',
event: 'connected',
})
})
cy.findByRole('button', { name: 'Recompile' }).should('not.exist')
})
})

View file

@ -0,0 +1,74 @@
import { mount, unmount } from '@cypress/react'
import { EditorProviders } from '../../helpers/editor-providers'
import PdfJsViewer from '../../../../frontend/js/features/pdf-preview/components/pdf-js-viewer'
import { mockScope } from './scope'
describe('<PdfJSViewer/>', function () {
beforeEach(function () {
cy.interceptCompile()
cy.interceptEvents()
})
it('loads all PDF pages', function () {
const scope = mockScope()
mount(
<EditorProviders scope={scope}>
<div className="pdf-viewer">
<PdfJsViewer url="/build/123/output.pdf" />
</div>
</EditorProviders>
)
cy.findByLabelText('Page 1')
cy.findByLabelText('Page 2')
cy.findByLabelText('Page 3')
cy.findByLabelText('Page 4').should('not.exist')
cy.contains('Your Paper')
})
it('renders pages in a "loading" state', function () {
const scope = mockScope()
mount(
<EditorProviders scope={scope}>
<div className="pdf-viewer">
<PdfJsViewer url="/build/123/output.pdf" />
</div>
</EditorProviders>
)
cy.findByLabelText('Loading…')
})
it('can be unmounted while loading a document', function () {
const scope = mockScope()
mount(
<EditorProviders scope={scope}>
<div className="pdf-viewer">
<PdfJsViewer url="/build/123/output.pdf" />
</div>
</EditorProviders>
)
unmount()
})
it('can be unmounted after loading a document', function () {
const scope = mockScope()
mount(
<EditorProviders scope={scope}>
<div className="pdf-viewer">
<PdfJsViewer url="/build/123/output.pdf" />
</div>
</EditorProviders>
)
cy.findByLabelText('Page 1')
unmount()
})
})

View file

@ -0,0 +1,143 @@
import { mount } from '@cypress/react'
import sysendTestHelper from '../../helpers/sysend'
import { EditorProviders } from '../../helpers/editor-providers'
import PdfLogsEntries from '../../../../frontend/js/features/pdf-preview/components/pdf-logs-entries'
window.metaAttributesCache = new Map([['ol-debugPdfDetach', true]])
describe('<PdfLogsEntries/>', function () {
const fakeEntity = { type: 'doc' }
const logEntries = [
{
file: 'main.tex',
line: 9,
column: 8,
level: 'error',
message: 'LaTeX Error',
content: 'See the LaTeX manual',
raw: '',
ruleId: 'hint_misplaced_alignment_tab_character',
key: '',
},
]
let props
beforeEach(function () {
props = {
fileTreeManager: {
findEntityByPath: cy.stub().as('findEntityByPath').returns(fakeEntity),
},
editorManager: {
openDoc: cy.stub().as('openDoc'),
},
}
cy.interceptCompile()
cy.interceptEvents()
})
afterEach(function () {
window.metaAttributesCache = new Map()
sysendTestHelper.resetHistory()
})
it('displays human readable hint', function () {
mount(
<EditorProviders {...props}>
<PdfLogsEntries entries={logEntries} />
</EditorProviders>
)
cy.contains('You have placed an alignment tab character')
})
it('opens doc on click', function () {
mount(
<EditorProviders {...props}>
<PdfLogsEntries entries={logEntries} />
</EditorProviders>
)
cy.findByRole('button', {
name: 'Navigate to log position in source code: main.tex, 9',
})
.click()
.then(() => {
expect(props.fileTreeManager.findEntityByPath).to.be.calledOnce
expect(props.editorManager.openDoc).to.be.calledOnce
expect(props.editorManager.openDoc).to.be.calledWith(fakeEntity, {
gotoLine: 9,
gotoColumn: 8,
})
})
})
it('opens doc via detached action', function () {
cy.window().then(win => {
win.metaAttributesCache = new Map([['ol-detachRole', 'detacher']])
})
mount(
<EditorProviders {...props}>
<PdfLogsEntries entries={logEntries} />
</EditorProviders>
).then(() => {
sysendTestHelper.receiveMessage({
role: 'detached',
event: 'action-sync-to-entry',
data: {
args: [
{
file: 'main.tex',
line: 7,
column: 6,
},
],
},
})
expect(props.fileTreeManager.findEntityByPath).to.be.calledOnce
expect(props.editorManager.openDoc).to.be.calledOnce
expect(props.editorManager.openDoc).to.be.calledWith(fakeEntity, {
gotoLine: 7,
gotoColumn: 6,
})
})
})
it('sends open doc clicks via detached action', function () {
cy.window().then(win => {
win.metaAttributesCache = new Map([['ol-detachRole', 'detached']])
})
mount(
<EditorProviders {...props}>
<PdfLogsEntries entries={logEntries} />
</EditorProviders>
)
cy.findByRole('button', {
name: 'Navigate to log position in source code: main.tex, 9',
})
.click()
.then(() => {
expect(props.fileTreeManager.findEntityByPath).not.to.be.called
expect(props.editorManager.openDoc).not.to.be.called
expect(sysendTestHelper.getLastBroacastMessage()).to.deep.equal({
role: 'detached',
event: 'action-sync-to-entry',
data: {
args: [
{
file: 'main.tex',
line: 9,
column: 8,
},
],
},
})
})
})
})

View file

@ -0,0 +1,75 @@
import { mount } from '@cypress/react'
import sysendTestHelper from '../../helpers/sysend'
import PdfPreviewDetachedRoot from '../../../../frontend/js/features/pdf-preview/components/pdf-preview-detached-root'
describe('<PdfPreviewDetachedRoot/>', function () {
beforeEach(function () {
window.user = { id: 'user1' }
window.metaAttributesCache = new Map<string, unknown>([
['ol-user', window.user],
['ol-project_id', 'project1'],
['ol-detachRole', 'detached'],
['ol-projectName', 'Project Name'],
])
cy.interceptCompile()
cy.interceptEvents()
})
afterEach(function () {
window.metaAttributesCache = new Map()
sysendTestHelper.resetHistory()
})
it('syncs compiling state', function () {
mount(<PdfPreviewDetachedRoot />).then(() => {
sysendTestHelper.receiveMessage({
role: 'detacher',
event: 'connected',
})
sysendTestHelper.receiveMessage({
role: 'detacher',
event: 'state-compiling',
data: { value: true },
})
})
cy.findByRole('button', { name: 'Compiling…' })
cy.findByRole('button', { name: 'Recompile' })
.should('not.exist')
.then(() => {
sysendTestHelper.receiveMessage({
role: 'detacher',
event: 'state-compiling',
data: { value: false },
})
})
cy.findByRole('button', { name: 'Recompile' })
cy.findByRole('button', { name: 'Compiling…' }).should('not.exist')
})
it('sends a clear cache request when the button is pressed', function () {
mount(<PdfPreviewDetachedRoot />).then(() => {
sysendTestHelper.receiveMessage({
role: 'detacher',
event: 'state-showLogs',
data: { value: true },
})
})
cy.findByRole('button', { name: 'Clear cached files' })
.should('not.be.disabled')
.click()
.then(() => {
expect(sysendTestHelper.getLastBroacastMessage()).to.deep.equal({
role: 'detached',
event: 'action-clearCache',
data: {
args: [],
},
})
})
})
})

View file

@ -0,0 +1,102 @@
import { mount } from '@cypress/react'
import sysendTestHelper from '../../helpers/sysend'
import { EditorProviders } from '../../helpers/editor-providers'
import PdfPreviewHybridToolbar from '../../../../frontend/js/features/pdf-preview/components/pdf-preview-hybrid-toolbar'
describe('<PdfPreviewHybridToolbar/>', function () {
beforeEach(function () {
cy.interceptCompile()
cy.interceptEvents()
})
afterEach(function () {
window.metaAttributesCache = new Map()
sysendTestHelper.resetHistory()
})
it('shows normal mode', function () {
mount(
<EditorProviders>
<PdfPreviewHybridToolbar />
</EditorProviders>
)
cy.findByRole('button', { name: 'Recompile' })
})
describe('orphan mode', function () {
it('shows connecting message on load', function () {
cy.window().then(win => {
win.metaAttributesCache = new Map([['ol-detachRole', 'detached']])
})
mount(
<EditorProviders>
<PdfPreviewHybridToolbar />
</EditorProviders>
)
cy.contains('Connecting with the editor')
})
it('shows compile UI when connected', function () {
cy.window().then(win => {
win.metaAttributesCache = new Map([['ol-detachRole', 'detached']])
})
mount(
<EditorProviders>
<PdfPreviewHybridToolbar />
</EditorProviders>
).then(() => {
sysendTestHelper.receiveMessage({
role: 'detacher',
event: 'connected',
})
})
cy.findByRole('button', { name: 'Recompile' })
})
it('shows connecting message when disconnected', function () {
cy.window().then(win => {
win.metaAttributesCache = new Map([['ol-detachRole', 'detached']])
})
mount(
<EditorProviders>
<PdfPreviewHybridToolbar />
</EditorProviders>
).then(() => {
sysendTestHelper.receiveMessage({
role: 'detacher',
event: 'connected',
})
sysendTestHelper.receiveMessage({
role: 'detacher',
event: 'closed',
})
})
cy.contains('Connecting with the editor')
})
it('shows redirect button after timeout', function () {
cy.window().then(win => {
win.metaAttributesCache = new Map([['ol-detachRole', 'detached']])
})
cy.clock()
mount(
<EditorProviders>
<PdfPreviewHybridToolbar />
</EditorProviders>
)
cy.tick(6000)
cy.findByRole('button', { name: 'Redirect to editor' })
})
})
})

View file

@ -0,0 +1,599 @@
import { mount } from '@cypress/react'
import localStorage from '../../../../frontend/js/infrastructure/local-storage'
import PdfPreview from '../../../../frontend/js/features/pdf-preview/components/pdf-preview'
import { EditorProviders } from '../../helpers/editor-providers'
import { mockScope } from './scope'
const storeAndFireEvent = (win, key, value) => {
localStorage.setItem(key, value)
win.dispatchEvent(new StorageEvent('storage', { key }))
}
describe('<PdfPreview/>', function () {
beforeEach(function () {
cy.interceptCompile()
cy.interceptEvents()
})
it('renders the PDF preview', function () {
const scope = mockScope()
mount(
<EditorProviders scope={scope}>
<div className="pdf-viewer">
<PdfPreview />
</div>
</EditorProviders>
)
// wait for "compile on load" to finish
cy.findByRole('button', { name: 'Compiling…' })
cy.wait('@compile')
cy.findByRole('button', { name: 'Recompile' })
cy.wait('@compile-pdf')
})
it('runs a compile when the Recompile button is pressed', function () {
const scope = mockScope()
mount(
<EditorProviders scope={scope}>
<div className="pdf-viewer">
<PdfPreview />
</div>
</EditorProviders>
)
// wait for "compile on load" to finish
cy.findByRole('button', { name: 'Compiling…' })
cy.wait('@compile')
cy.wait('@compile-pdf')
cy.interceptCompile('recompile')
// press the Recompile button => compile
cy.findByRole('button', { name: 'Recompile' }).click()
// wait for "recompile" to finish
// cy.findByRole('button', { name: 'Compiling…' })
cy.wait('@recompile-pdf')
cy.wait('@recompile-log')
cy.wait('@recompile-blg')
cy.findByRole('button', { name: 'Recompile' })
cy.contains('Your Paper')
})
it('runs a compile on `pdf:recompile` event', function () {
const scope = mockScope()
mount(
<EditorProviders scope={scope}>
<div className="pdf-viewer">
<PdfPreview />
</div>
</EditorProviders>
)
// wait for "compile on load" to finish
cy.findByRole('button', { name: 'Compiling…' })
cy.wait('@compile')
cy.wait('@compile-pdf')
cy.interceptCompile('recompile')
cy.window().then(win => {
win.dispatchEvent(new CustomEvent('pdf:recompile'))
})
// wait for "recompile" to finish
// cy.findByRole('button', { name: 'Compiling…' })
cy.wait('@recompile')
cy.findByRole('button', { name: 'Recompile' })
cy.wait('@recompile-pdf')
cy.contains('Your Paper')
})
it('does not compile while compiling', function () {
let compileResolve
let counter = 0
const promise = new Promise(resolve => {
compileResolve = resolve
})
cy.intercept(
'POST',
'/project/project123/compile?auto_compile=true',
req => {
counter++
promise.then(() => {
req.reply({
body: {
status: 'success',
clsiServerId: 'foo',
compileGroup: 'priority',
pdfDownloadDomain: 'https://clsi.test-overleaf.com',
outputFiles: [
{
path: 'output.pdf',
build: '123',
url: '/build/123/output.pdf',
type: 'pdf',
},
{
path: 'output.log',
build: '123',
url: '/build/123/output.log',
type: 'log',
},
],
},
})
})
return promise
}
).as('compile')
const scope = mockScope()
mount(
<EditorProviders scope={scope}>
<div className="pdf-viewer">
<PdfPreview />
</div>
</EditorProviders>
).then(() => {
cy.findByRole('button', { name: 'Compiling…' })
cy.window().then(win => {
win.dispatchEvent(new CustomEvent('pdf:recompile'))
})
compileResolve()
cy.findByRole('button', { name: 'Recompile' })
cy.contains('Your Paper').should(() => {
expect(counter).to.equal(1)
})
})
})
it('disables compile button while compile is running', function () {
const scope = mockScope()
mount(
<EditorProviders scope={scope}>
<div className="pdf-viewer">
<PdfPreview />
</div>
</EditorProviders>
)
cy.findByRole('button', { name: 'Compiling…' }).should('be.disabled')
cy.findByRole('button', { name: 'Recompile' }).should('not.be.disabled')
})
it('runs a compile on doc change if autocompile is enabled', function () {
const scope = mockScope()
mount(
<EditorProviders scope={scope}>
<div className="pdf-viewer">
<PdfPreview />
</div>
</EditorProviders>
)
// wait for "compile on load" to finish
cy.findByRole('button', { name: 'Compiling…' })
cy.wait('@compile')
cy.findByRole('button', { name: 'Recompile' })
cy.window().then(win => {
cy.clock()
// switch on auto compile
storeAndFireEvent(win, 'autocompile_enabled:project123', true)
// fire a doc:changed event => compile
win.dispatchEvent(new CustomEvent('doc:changed'))
cy.tick(5000) // AUTO_COMPILE_DEBOUNCE
cy.clock().invoke('restore')
})
cy.findByRole('button', { name: 'Compiling…' })
cy.findByRole('button', { name: 'Recompile' })
})
it('does not run a compile on doc change if autocompile is disabled', function () {
const scope = mockScope()
mount(
<EditorProviders scope={scope}>
<div className="pdf-viewer">
<PdfPreview />
</div>
</EditorProviders>
)
// wait for "compile on load" to finish
cy.findByRole('button', { name: 'Compiling…' })
cy.findByRole('button', { name: 'Recompile' })
cy.window().then(win => {
cy.clock()
// make sure auto compile is switched off
storeAndFireEvent(win, 'autocompile_enabled:project123', false)
// fire a doc:changed event => no compile
win.dispatchEvent(new CustomEvent('doc:changed'))
cy.tick(5000) // AUTO_COMPILE_DEBOUNCE
cy.clock().invoke('restore')
})
cy.findByRole('button', { name: 'Recompile' })
})
it('does not run a compile on doc change if autocompile is blocked by syntax check', function () {
const scope = mockScope()
// enable linting in the editor
scope.settings.syntaxValidation = true
// mock a linting error
scope.hasLintingError = true
mount(
<EditorProviders scope={scope}>
<div className="pdf-viewer">
<PdfPreview />
</div>
</EditorProviders>
)
// wait for "compile on load" to finish
cy.findByRole('button', { name: 'Compiling…' })
cy.findByRole('button', { name: 'Recompile' })
cy.window().then(win => {
cy.clock()
// switch on auto compile
storeAndFireEvent(win, 'autocompile_enabled:project123', true)
// switch on syntax checking
storeAndFireEvent(win, 'stop_on_validation_error', true)
// fire a doc:changed event => no compile
win.dispatchEvent(new CustomEvent('doc:changed'))
cy.tick(5000) // AUTO_COMPILE_DEBOUNCE
cy.clock().invoke('restore')
})
cy.findByRole('button', { name: 'Recompile' })
cy.findByText('Code check failed')
})
describe('displays error messages', function () {
const compileErrorStatuses = {
'clear-cache':
'Sorry, something went wrong and your project could not be compiled. Please try again in a few moments.',
'clsi-maintenance':
'The compile servers are down for maintenance, and will be back shortly.',
'compile-in-progress':
'A previous compile is still running. Please wait a minute and try compiling again.',
exited: 'Server Error',
failure: 'No PDF',
generic: 'Server Error',
'project-too-large': 'Project too large',
'rate-limited': 'Compile rate limit hit',
terminated: 'Compilation cancelled',
timedout: 'Timed out',
'too-recently-compiled':
'This project was compiled very recently, so this compile has been skipped.',
unavailable:
'Sorry, the compile server for your project was temporarily unavailable. Please try again in a few moments.',
foo: 'Sorry, something went wrong and your project could not be compiled. Please try again in a few moments.',
}
for (const [status, message] of Object.entries(compileErrorStatuses)) {
it(`displays error message for '${status}' status`, function () {
cy.intercept('POST', '/project/*/compile?*', {
body: {
status,
clsiServerId: 'foo',
compileGroup: 'priority',
},
}).as('compile')
const scope = mockScope()
mount(
<EditorProviders scope={scope}>
<div className="pdf-viewer">
<PdfPreview />
</div>
</EditorProviders>
)
// wait for "compile on load" to finish
cy.findByRole('button', { name: 'Compiling…' })
cy.findByRole('button', { name: 'Recompile' })
cy.findByText(message)
})
}
it('displays expandable raw logs', function () {
const scope = mockScope()
mount(
<EditorProviders scope={scope}>
<div className="pdf-viewer">
<PdfPreview />
</div>
</EditorProviders>
)
// wait for "compile on load" to finish
cy.findByRole('button', { name: 'Compiling…' })
cy.findByRole('button', { name: 'Recompile' })
cy.findByRole('button', { name: 'View logs' }).click()
cy.findByRole('button', { name: 'View PDF' })
cy.findByRole('button', { name: 'Expand' }).click()
cy.findByRole('button', { name: 'Collapse' }).click()
})
it('displays error messages if there were validation problems', function () {
const validationProblems = {
sizeCheck: {
resources: [
{ path: 'foo/bar', kbSize: 76221 },
{ path: 'bar/baz', kbSize: 2342 },
],
},
mainFile: true,
conflictedPaths: [
{
path: 'foo/bar',
},
{
path: 'foo/baz',
},
],
}
cy.intercept('POST', '/project/*/compile?*', {
body: {
status: 'validation-problems',
validationProblems,
clsiServerId: 'foo',
compileGroup: 'priority',
},
}).as('compile')
const scope = mockScope()
mount(
<EditorProviders scope={scope}>
<div className="pdf-viewer">
<PdfPreview />
</div>
</EditorProviders>
)
// wait for "compile on load" to finish
cy.findByRole('button', { name: 'Compiling…' })
cy.findByRole('button', { name: 'Recompile' })
cy.wait('@compile')
cy.findByText('Project too large')
cy.findByText('Unknown main document')
cy.findByText('Conflicting Paths Found')
})
it('sends a clear cache request when the button is pressed', function () {
const scope = mockScope()
mount(
<EditorProviders scope={scope}>
<div className="pdf-viewer">
<PdfPreview />
</div>
</EditorProviders>
)
// wait for "compile on load" to finish
cy.findByRole('button', { name: 'Compiling…' })
cy.findByRole('button', { name: 'Recompile' })
cy.findByRole('button', { name: 'View logs' }).click()
cy.findByRole('button', { name: 'Clear cached files' }).should(
'not.be.disabled'
)
cy.intercept('DELETE', 'project/*/output?*', {
statusCode: 204,
delay: 100,
}).as('clear-cache')
// click the button
cy.findByRole('button', { name: 'Clear cached files' }).click()
cy.findByRole('button', { name: 'Clear cached files' }).should(
'be.disabled'
)
cy.wait('@clear-cache')
cy.findByRole('button', { name: 'Clear cached files' }).should(
'not.be.disabled'
)
})
it('handle "recompile from scratch"', function () {
const scope = mockScope()
mount(
<EditorProviders scope={scope}>
<div className="pdf-viewer">
<PdfPreview />
</div>
</EditorProviders>
)
// wait for "compile on load" to finish
cy.findByRole('button', { name: 'Compiling…' })
cy.findByRole('button', { name: 'Recompile' })
// show the logs UI
cy.findByRole('button', { name: 'View logs' }).click()
cy.findByRole('button', { name: 'Clear cached files' }).should(
'not.be.disabled'
)
cy.interceptCompile()
cy.intercept('DELETE', 'project/*/output?*', {
statusCode: 204,
delay: 100,
}).as('clear-cache')
// TODO: open the menu?
cy.findByRole('menuitem', {
name: 'Recompile from scratch',
hidden: true,
}).trigger('click', { force: true })
cy.findByRole('button', { name: 'Clear cached files' }).should(
'be.disabled'
)
cy.findByRole('button', { name: 'Compiling…' })
cy.wait('@clear-cache')
cy.findByRole('button', { name: 'Recompile' })
cy.wait('@compile')
cy.wait('@compile-pdf')
})
it('shows an error for an invalid URL', function () {
cy.intercept('/build/*/output.pdf?*', {
statusCode: 500,
body: {
message: 'something awful happened',
code: 'AWFUL_ERROR',
},
}).as('compile-pdf-error')
const scope = mockScope()
mount(
<EditorProviders scope={scope}>
<div className="pdf-viewer">
<PdfPreview />
</div>
</EditorProviders>
)
cy.wait('@compile-pdf-error')
cy.findByText('Something went wrong while rendering this PDF.')
cy.findByLabelText('Page 1').should('not.exist')
})
it('shows an error for a corrupt PDF', function () {
cy.intercept('/build/*/output.pdf?*', {
fixture: 'build/output-corrupt.pdf,null',
}).as('compile-pdf-corrupt')
const scope = mockScope()
mount(
<EditorProviders scope={scope}>
<div className="pdf-viewer">
<PdfPreview />
</div>
</EditorProviders>
)
cy.wait('@compile-pdf-corrupt')
cy.findByText('Something went wrong while rendering this PDF.')
cy.findByLabelText('Page 1').should('not.exist')
})
})
describe('human readable logs', function () {
it('shows human readable hint for undefined reference errors', function () {
cy.intercept('/build/*/output.log?*', {
fixture: 'build/output-human-readable.log',
}).as('log')
const scope = mockScope()
mount(
<EditorProviders scope={scope}>
<div className="pdf-viewer">
<PdfPreview />
</div>
</EditorProviders>
)
cy.wait('@log')
cy.findByRole('button', { name: 'View logs' }).click()
cy.findByText(
"Reference `intorduction' on page 1 undefined on input line 11."
)
cy.findByText(
"Reference `section1' on page 1 undefined on input line 13."
)
cy.findByText('There were undefined references.')
cy.findAllByText(
/You have referenced something which has not yet been labelled/
).should('have.length', 3)
})
it('does not show human readable hint when no undefined reference errors', function () {
cy.intercept('/build/*/output.log?*', {
fixture: 'build/output-undefined-references.log',
}).as('log')
const scope = mockScope()
mount(
<EditorProviders scope={scope}>
<div className="pdf-viewer">
<PdfPreview />
</div>
</EditorProviders>
)
cy.wait('@log')
cy.findByRole('button', { name: 'View logs' }).click()
cy.findByText(
"Package rerunfilecheck Warning: File `output.brf' has changed. Rerun to get bibliographical references right."
)
cy.findByText(
/You have referenced something which has not yet been labelled/
).should('not.exist')
})
})
})

View file

@ -0,0 +1,380 @@
import PdfSynctexControls from '../../../../frontend/js/features/pdf-preview/components/pdf-synctex-controls'
import sysendTestHelper from '../../helpers/sysend'
import { cloneDeep } from 'lodash'
import { useDetachCompileContext as useCompileContext } from '../../../../frontend/js/shared/context/detach-compile-context'
import { useFileTreeData } from '../../../../frontend/js/shared/context/file-tree-data-context'
import { useEffect } from 'react'
import { mount } from '@cypress/react'
import { EditorProviders } from '../../helpers/editor-providers'
import { mockScope } from './scope'
const mockHighlights = [
{
page: 1,
h: 85.03936,
v: 509.999878,
width: 441.921265,
height: 8.855677,
},
{
page: 1,
h: 85.03936,
v: 486.089539,
width: 441.921265,
height: 8.855677,
},
]
const mockPosition = {
page: 1,
offset: { top: 10, left: 10 },
pageSize: { height: 500, width: 500 },
}
const mockSelectedEntities = [{ type: 'doc' }]
const WithPosition = ({ mockPosition }) => {
const { setPosition } = useCompileContext()
// mock PDF scroll position update
useEffect(() => {
setPosition(mockPosition)
}, [mockPosition, setPosition])
return null
}
const WithSelectedEntities = ({ mockSelectedEntities = [] }) => {
const { setSelectedEntities } = useFileTreeData()
useEffect(() => {
setSelectedEntities(mockSelectedEntities)
}, [mockSelectedEntities, setSelectedEntities])
return null
}
describe('<PdfSynctexControls/>', function () {
beforeEach(function () {
window.metaAttributesCache = new Map()
cy.interceptCompile()
cy.interceptEvents()
cy.intercept('/project/*/sync/code?*', {
body: { pdf: cloneDeep(mockHighlights) },
delay: 100,
}).as('sync-code')
cy.intercept('/project/*/sync/pdf?*', {
body: { code: [{ file: 'main.tex', line: 100 }] },
delay: 100,
}).as('sync-pdf')
})
afterEach(function () {
window.metaAttributesCache = new Map()
})
it('handles clicks on sync buttons', function () {
const scope = mockScope()
mount(
<EditorProviders scope={scope}>
<WithPosition mockPosition={mockPosition} />
<WithSelectedEntities mockSelectedEntities={mockSelectedEntities} />
<PdfSynctexControls />
</EditorProviders>
)
cy.get('.synctex-control-icon').should('have.length', 2)
// mock editor cursor position update
cy.window().then(win => {
win.dispatchEvent(
new CustomEvent('cursor:editor:update', {
detail: { row: 100, column: 10 },
})
)
})
cy.get('body')
.findByRole('button', { name: 'Go to code location in PDF' })
.click()
cy.get('body')
.findByRole('button', { name: 'Go to code location in PDF' })
.should('be.disabled')
cy.wait('@sync-code')
cy.get('body')
.findByRole('button', { name: /^Go to PDF location in code/ })
.click()
cy.get('body')
.findByRole('button', { name: /^Go to PDF location in code/ })
.should('be.disabled')
cy.wait('@sync-pdf')
})
it('disables button when multiple entities are selected', function () {
const scope = mockScope()
mount(
<EditorProviders scope={scope}>
<WithPosition mockPosition={mockPosition} />
<WithSelectedEntities
mockSelectedEntities={[{ type: 'doc' }, { type: 'doc' }]}
/>
<PdfSynctexControls />
</EditorProviders>
)
cy.get('body')
.findByRole('button', { name: 'Go to code location in PDF' })
.should('be.disabled')
cy.get('body')
.findByRole('button', { name: /^Go to PDF location in code/ })
.should('be.disabled')
})
it('disables button when a file is selected', function () {
const scope = mockScope()
mount(
<EditorProviders scope={scope}>
<WithPosition mockPosition={mockPosition} />
<WithSelectedEntities mockSelectedEntities={[{ type: 'file' }]} />
<PdfSynctexControls />
</EditorProviders>
)
cy.get('body')
.findByRole('button', { name: 'Go to code location in PDF' })
.should('be.disabled')
cy.get('body')
.findByRole('button', { name: /^Go to PDF location in code/ })
.should('be.disabled')
})
describe('with detacher role', function () {
beforeEach(function () {
window.metaAttributesCache.set('ol-detachRole', 'detacher')
})
it('does not have go to PDF location button nor arrow icon', function () {
const scope = mockScope()
mount(
<EditorProviders scope={scope}>
<WithPosition mockPosition={mockPosition} />
<WithSelectedEntities mockSelectedEntities={mockSelectedEntities} />
<PdfSynctexControls />
</EditorProviders>
)
cy.get('body')
.findByRole('button', { name: /^Go to PDF location in code/ })
.should('not.exist')
cy.get('.synctex-control-icon').should('not.exist')
})
it('send set highlights action', function () {
const scope = mockScope()
mount(
<EditorProviders scope={scope}>
<WithPosition mockPosition={mockPosition} />
<WithSelectedEntities mockSelectedEntities={mockSelectedEntities} />
<PdfSynctexControls />
</EditorProviders>
).then(() => {
sysendTestHelper.resetHistory()
})
cy.wait('@compile')
// mock editor cursor position update
cy.window().then(win => {
win.dispatchEvent(
new CustomEvent('cursor:editor:update', {
detail: { row: 100, column: 10 },
})
)
})
cy.findByRole('button', {
name: 'Go to code location in PDF',
})
.should('not.be.disabled')
.click()
cy.findByRole('button', {
name: 'Go to code location in PDF',
}).should('be.disabled')
cy.wait('@sync-code').then(() => {
// synctex is called locally and the result are broadcast for the detached tab
expect(sysendTestHelper.getLastBroacastMessage()).to.deep.equal({
role: 'detacher',
event: 'action-setHighlights',
data: { args: [mockHighlights] },
})
})
})
it('reacts to sync to code action', function () {
const scope = mockScope()
mount(
<EditorProviders scope={scope}>
<WithPosition mockPosition={mockPosition} />
<WithSelectedEntities mockSelectedEntities={mockSelectedEntities} />
<PdfSynctexControls />
</EditorProviders>
)
cy.wait('@compile').then(() => {
sysendTestHelper.receiveMessage({
role: 'detached',
event: 'action-sync-to-code',
data: {
args: [mockPosition],
},
})
})
cy.wait('@sync-pdf')
})
})
describe('with detached role', function () {
beforeEach(function () {
window.metaAttributesCache.set('ol-detachRole', 'detached')
})
it('does not have go to code location button nor arrow icon', function () {
const scope = mockScope()
mount(
<EditorProviders scope={scope}>
<WithPosition mockPosition={mockPosition} />
<PdfSynctexControls />
</EditorProviders>
)
cy.findByRole('button', {
name: 'Go to code location in PDF',
}).should('not.exist')
cy.get('.synctex-control-icon').should('not.exist')
})
// eslint-disable-next-line mocha/no-skipped-tests
it.skip('send go to code line action', function () {
const scope = mockScope()
mount(
<EditorProviders scope={scope}>
<WithPosition mockPosition={mockPosition} />
<PdfSynctexControls />
</EditorProviders>
)
cy.wait('@compile')
cy.get('body')
.findByRole('button', { name: /^Go to PDF location in code/ })
.should('be.disabled')
.then(() => {
sysendTestHelper.receiveMessage({
role: 'detached',
event: 'state-has-single-selected-doc',
data: { value: true },
})
})
cy.get('body')
.findByRole('button', { name: /^Go to PDF location in code/ })
.should('not.be.disabled')
.then(() => {
sysendTestHelper.resetHistory()
})
cy.get('body')
.findByRole('button', { name: /^Go to PDF location in code/ })
.click()
// the button is only disabled when the state is updated via sysend
cy.get('body')
.findByRole('button', { name: /^Go to PDF location in code/ })
.should('not.be.disabled')
cy.get('.synctex-spin-icon')
.should('not.exist')
.then(() => {
expect(sysendTestHelper.getLastBroacastMessage()).to.deep.equal({
role: 'detached',
event: 'action-sync-to-code',
data: {
args: [mockPosition, 72],
},
})
})
})
// eslint-disable-next-line mocha/no-skipped-tests
it.skip('update inflight state', function () {
const scope = mockScope()
mount(
<EditorProviders scope={scope}>
<WithPosition mockPosition={mockPosition} />
<PdfSynctexControls />
</EditorProviders>
).then(() => {
sysendTestHelper.receiveMessage({
role: 'detached',
event: 'state-has-single-selected-doc',
data: { value: true },
})
})
cy.get('body')
.findByRole('button', { name: /^Go to PDF location in code/ })
.should('be.disabled')
cy.get('.synctex-spin-icon')
.should('not.exist')
.then(() => {
sysendTestHelper.receiveMessage({
role: 'detacher',
event: 'state-sync-to-code-inflight',
data: { value: true },
})
})
cy.get('body')
.findByRole('button', { name: /^Go to PDF location in code/ })
.should('be.disabled')
cy.get('.synctex-spin-icon')
.should('have.length', 1)
.then(() => {
sysendTestHelper.receiveMessage({
role: 'detacher',
event: 'state-sync-to-code-inflight',
data: { value: false },
})
})
cy.get('body')
.findByRole('button', { name: /^Go to PDF location in code/ })
.should('not.be.disabled')
cy.get('.synctex-spin-icon').should('not.exist')
})
})
})

View file

@ -0,0 +1,13 @@
export const mockScope = () => ({
settings: {
syntaxValidation: false,
pdfViewer: 'pdfjs',
},
editor: {
sharejs_doc: {
doc_id: 'test-doc',
getSnapshot: () => 'some doc content',
},
},
hasLintingError: false,
})

View file

@ -9,13 +9,8 @@ import FileTreeContextMenu from '../../../../../../frontend/js/features/file-tre
describe('<FileTreeitemInner />', function () { describe('<FileTreeitemInner />', function () {
const setContextMenuCoords = sinon.stub() const setContextMenuCoords = sinon.stub()
beforeEach(function () {
global.requestAnimationFrame = sinon.stub()
})
afterEach(function () { afterEach(function () {
setContextMenuCoords.reset() setContextMenuCoords.reset()
delete global.requestAnimationFrame
}) })
describe('menu', function () { describe('menu', function () {

View file

@ -14,13 +14,11 @@ describe('<FileTreeRoot/>', function () {
const onInit = sinon.stub() const onInit = sinon.stub()
beforeEach(function () { beforeEach(function () {
global.requestAnimationFrame = sinon.stub()
window.metaAttributesCache = new Map() window.metaAttributesCache = new Map()
window.metaAttributesCache.set('ol-user', { id: 'user1' }) window.metaAttributesCache.set('ol-user', { id: 'user1' })
}) })
afterEach(function () { afterEach(function () {
delete global.requestAnimationFrame
fetchMock.restore() fetchMock.restore()
onSelect.reset() onSelect.reset()
onInit.reset() onInit.reset()

View file

@ -15,13 +15,11 @@ describe('FileTree Create Folder Flow', function () {
const onInit = sinon.stub() const onInit = sinon.stub()
beforeEach(function () { beforeEach(function () {
global.requestAnimationFrame = sinon.stub()
window.metaAttributesCache = new Map() window.metaAttributesCache = new Map()
window.metaAttributesCache.set('ol-user', { id: 'user1' }) window.metaAttributesCache.set('ol-user', { id: 'user1' })
}) })
afterEach(function () { afterEach(function () {
delete global.requestAnimationFrame
fetchMock.restore() fetchMock.restore()
onSelect.reset() onSelect.reset()
onInit.reset() onInit.reset()

View file

@ -15,13 +15,11 @@ describe('FileTree Rename Entity Flow', function () {
const onInit = sinon.stub() const onInit = sinon.stub()
beforeEach(function () { beforeEach(function () {
global.requestAnimationFrame = sinon.stub()
window.metaAttributesCache = new Map() window.metaAttributesCache = new Map()
window.metaAttributesCache.set('ol-user', { id: 'user1' }) window.metaAttributesCache.set('ol-user', { id: 'user1' })
}) })
afterEach(function () { afterEach(function () {
delete global.requestAnimationFrame
fetchMock.restore() fetchMock.restore()
onSelect.reset() onSelect.reset()
onInit.reset() onInit.reset()

View file

@ -1,51 +0,0 @@
import DetachCompileButton from '../../../../../frontend/js/features/pdf-preview/components/detach-compile-button'
import { renderWithEditorContext } from '../../../helpers/render-with-context'
import { screen } from '@testing-library/react'
import sysendTestHelper from '../../../helpers/sysend'
import { expect } from 'chai'
describe('<DetachCompileButton/>', function () {
afterEach(function () {
window.metaAttributesCache = new Map()
sysendTestHelper.resetHistory()
})
it('detacher mode and linked: show button ', async function () {
window.metaAttributesCache.set('ol-detachRole', 'detacher')
renderWithEditorContext(<DetachCompileButton />)
sysendTestHelper.receiveMessage({
role: 'detached',
event: 'connected',
})
await screen.getByRole('button', {
name: 'Recompile',
})
})
it('detacher mode and not linked: does not show button ', async function () {
window.metaAttributesCache.set('ol-detachRole', 'detacher')
renderWithEditorContext(<DetachCompileButton />)
expect(
await screen.queryByRole('button', {
name: 'Recompile',
})
).to.not.exist
})
it('not detacher mode and linked: does not show button ', async function () {
window.metaAttributesCache.set('ol-detachRole', 'detached')
renderWithEditorContext(<DetachCompileButton />)
sysendTestHelper.receiveMessage({
role: 'detacher',
event: 'connected',
})
expect(
await screen.queryByRole('button', {
name: 'Recompile',
})
).to.not.exist
})
})

View file

@ -1,37 +0,0 @@
import { expect } from 'chai'
import { screen } from '@testing-library/react'
import path from 'path'
import { renderWithEditorContext } from '../../../helpers/render-with-context'
import { pathToFileURL } from 'url'
import PdfJsViewer from '../../../../../frontend/js/features/pdf-preview/components/pdf-js-viewer'
const example = pathToFileURL(
path.join(__dirname, '../fixtures/test-example.pdf')
).toString()
describe('<PdfJSViewer/>', function () {
it('loads all PDF pages', async function () {
renderWithEditorContext(<PdfJsViewer url={example} />)
await screen.findByLabelText('Page 1')
await screen.findByLabelText('Page 2')
await screen.findByLabelText('Page 3')
expect(screen.queryByLabelText('Page 4')).to.not.exist
})
it('renders pages in a "loading" state', async function () {
renderWithEditorContext(<PdfJsViewer url={example} />)
await screen.findByLabelText('Loading…')
})
it('can be unmounted while loading a document', async function () {
const { unmount } = renderWithEditorContext(<PdfJsViewer url={example} />)
unmount()
})
it('can be unmounted after loading a document', async function () {
const { unmount } = renderWithEditorContext(<PdfJsViewer url={example} />)
await screen.findByLabelText('Page 1')
unmount()
})
})

View file

@ -1,120 +0,0 @@
import PdfLogsEntries from '../../../../../frontend/js/features/pdf-preview/components/pdf-logs-entries'
import { renderWithEditorContext } from '../../../helpers/render-with-context'
import { screen, fireEvent } from '@testing-library/react'
import sysendTestHelper from '../../../helpers/sysend'
import { expect } from 'chai'
import sinon from 'sinon'
describe('<PdfLogsEntries/>', function () {
const fileTreeManager = {}
const editorManager = {}
const logEntries = [
{
file: 'main.tex',
line: 9,
column: 8,
level: 'error',
message: 'LaTeX Error',
content: 'See the LaTeX manual',
raw: '',
ruleId: 'hint_misplaced_alignment_tab_character',
key: '',
},
]
const fakeEntity = { type: 'doc' }
beforeEach(function () {
fileTreeManager.findEntityByPath = sinon.stub().returns(fakeEntity)
editorManager.openDoc = sinon.stub()
})
afterEach(function () {
window.metaAttributesCache = new Map()
sysendTestHelper.resetHistory()
fileTreeManager.findEntityByPath.resetHistory()
})
it('displays human readable hint', async function () {
renderWithEditorContext(<PdfLogsEntries entries={logEntries} />, {
fileTreeManager,
editorManager,
})
screen.getByText(/You have placed an alignment tab character/)
})
it('opens doc on click', async function () {
renderWithEditorContext(<PdfLogsEntries entries={logEntries} />, {
fileTreeManager,
editorManager,
})
const button = await screen.getByRole('button', {
name: 'Navigate to log position in source code: main.tex, 9',
})
fireEvent.click(button)
sinon.assert.calledOnce(fileTreeManager.findEntityByPath)
sinon.assert.calledOnce(editorManager.openDoc)
sinon.assert.calledWith(editorManager.openDoc, fakeEntity, {
gotoLine: 9,
gotoColumn: 8,
})
})
it('opens doc via detached action', async function () {
window.metaAttributesCache.set('ol-detachRole', 'detacher')
renderWithEditorContext(<PdfLogsEntries entries={logEntries} />, {
fileTreeManager,
editorManager,
})
sysendTestHelper.receiveMessage({
role: 'detached',
event: 'action-sync-to-entry',
data: {
args: [
{
file: 'main.tex',
line: 7,
column: 6,
},
],
},
})
sinon.assert.calledOnce(fileTreeManager.findEntityByPath)
sinon.assert.calledOnce(editorManager.openDoc)
sinon.assert.calledWith(editorManager.openDoc, fakeEntity, {
gotoLine: 7,
gotoColumn: 6,
})
})
it('sends open doc clicks via detached action', async function () {
window.metaAttributesCache.set('ol-detachRole', 'detached')
renderWithEditorContext(<PdfLogsEntries entries={logEntries} />, {
fileTreeManager,
editorManager,
})
const button = await screen.getByRole('button', {
name: 'Navigate to log position in source code: main.tex, 9',
})
fireEvent.click(button)
sinon.assert.notCalled(fileTreeManager.findEntityByPath)
sinon.assert.notCalled(editorManager.openDoc)
expect(sysendTestHelper.getLastBroacastMessage()).to.deep.equal({
role: 'detached',
event: 'action-sync-to-entry',
data: {
args: [
{
file: 'main.tex',
line: 9,
column: 8,
},
],
},
})
})
})

View file

@ -1,70 +0,0 @@
import { expect } from 'chai'
import { render, screen, fireEvent } from '@testing-library/react'
import sysendTestHelper from '../../../helpers/sysend'
import PdfPreviewDetachedRoot from '../../../../../frontend/js/features/pdf-preview/components/pdf-preview-detached-root'
describe('<PdfPreviewDetachedRoot/>', function () {
beforeEach(function () {
const user = { id: 'user1' }
window.user = user
window.metaAttributesCache = new Map()
window.metaAttributesCache.set('ol-user', user)
window.metaAttributesCache.set('ol-project_id', 'project1')
window.metaAttributesCache.set('ol-detachRole', 'detached')
window.metaAttributesCache.set('ol-projectName', 'Project Name')
})
afterEach(function () {
window.metaAttributesCache = new Map()
})
it('syncs compiling state', async function () {
render(<PdfPreviewDetachedRoot />)
sysendTestHelper.receiveMessage({
role: 'detacher',
event: 'connected',
})
sysendTestHelper.receiveMessage({
role: 'detacher',
event: 'state-compiling',
data: { value: true },
})
await screen.findByRole('button', { name: 'Compiling…' })
expect(screen.queryByRole('button', { name: 'Recompile' })).to.not.exist
sysendTestHelper.receiveMessage({
role: 'detacher',
event: 'state-compiling',
data: { value: false },
})
await screen.findByRole('button', { name: 'Recompile' })
expect(screen.queryByRole('button', { name: 'Compiling…' })).to.not.exist
})
it('sends a clear cache request when the button is pressed', async function () {
render(<PdfPreviewDetachedRoot />)
sysendTestHelper.receiveMessage({
role: 'detacher',
event: 'state-showLogs',
data: { value: true },
})
const clearCacheButton = await screen.findByRole('button', {
name: 'Clear cached files',
})
expect(clearCacheButton.hasAttribute('disabled')).to.be.false
fireEvent.click(clearCacheButton)
expect(sysendTestHelper.getLastBroacastMessage()).to.deep.equal({
role: 'detached',
event: 'action-clearCache',
data: {
args: [],
},
})
})
})

View file

@ -1,74 +0,0 @@
import sinon from 'sinon'
import PdfPreviewHybridToolbar from '../../../../../frontend/js/features/pdf-preview/components/pdf-preview-hybrid-toolbar'
import { renderWithEditorContext } from '../../../helpers/render-with-context'
import { screen } from '@testing-library/react'
import sysendTestHelper from '../../../helpers/sysend'
describe('<PdfPreviewHybridToolbar/>', function () {
let clock
beforeEach(function () {
clock = sinon.useFakeTimers()
})
afterEach(function () {
window.metaAttributesCache = new Map()
sysendTestHelper.resetHistory()
clock.runAll()
clock.restore()
})
it('shows normal mode', async function () {
renderWithEditorContext(<PdfPreviewHybridToolbar />)
await screen.getByRole('button', {
name: 'Recompile',
})
})
describe('orphan mode', async function () {
it('shows connecting message on load', async function () {
window.metaAttributesCache.set('ol-detachRole', 'detached')
renderWithEditorContext(<PdfPreviewHybridToolbar />)
await screen.getByText(/Connecting with the editor/)
})
it('shows compile UI when connected', async function () {
window.metaAttributesCache.set('ol-detachRole', 'detached')
renderWithEditorContext(<PdfPreviewHybridToolbar />)
sysendTestHelper.receiveMessage({
role: 'detacher',
event: 'connected',
})
await screen.getByRole('button', {
name: 'Recompile',
})
})
it('shows connecting message when disconnected', async function () {
window.metaAttributesCache.set('ol-detachRole', 'detached')
renderWithEditorContext(<PdfPreviewHybridToolbar />)
sysendTestHelper.receiveMessage({
role: 'detacher',
event: 'connected',
})
sysendTestHelper.receiveMessage({
role: 'detacher',
event: 'closed',
})
await screen.getByText(/Connecting with the editor/)
})
it('shows redirect button after timeout', async function () {
window.metaAttributesCache.set('ol-detachRole', 'detached')
renderWithEditorContext(<PdfPreviewHybridToolbar />)
clock.tick(6000)
await screen.getByRole('button', {
name: 'Redirect to editor',
})
})
})
})

View file

@ -1,523 +0,0 @@
import { expect } from 'chai'
import sinon from 'sinon'
import fetchMock from 'fetch-mock'
import { screen, fireEvent, waitFor, cleanup } from '@testing-library/react'
import PdfPreview from '../../../../../frontend/js/features/pdf-preview/components/pdf-preview'
import { renderWithEditorContext } from '../../../helpers/render-with-context'
import nock from 'nock'
import {
corruptPDF,
defaultFileResponses,
mockBuildFile,
mockClearCache,
mockCompile,
mockCompileError,
mockValidationProblems,
mockValidPdf,
} from '../utils/mock-compile'
const mockDelayed = fn => {
let _resolve = null
const delayPromise = new Promise((resolve, reject) => {
_resolve = resolve
})
fn(delayPromise)
return _resolve
}
const storeAndFireEvent = (key, value) => {
localStorage.setItem(key, value)
fireEvent(window, new StorageEvent('storage', { key }))
}
const scope = {
settings: {
syntaxValidation: false,
},
editor: {
sharejs_doc: {
doc_id: 'test-doc',
getSnapshot: () => 'some doc content',
},
},
}
describe('<PdfPreview/>', function () {
let clock
beforeEach(function () {
clock = sinon.useFakeTimers({
shouldAdvanceTime: true,
now: Date.now(),
})
nock.cleanAll()
})
afterEach(function () {
clock.runAll()
clock.restore()
fetchMock.reset()
localStorage.clear()
sinon.restore()
})
it('renders the PDF preview', async function () {
mockCompile()
mockBuildFile()
mockValidPdf()
renderWithEditorContext(<PdfPreview />, { scope })
// wait for "compile on load" to finish
await screen.findByRole('button', { name: 'Compiling…' })
await screen.findByRole('button', { name: 'Recompile' })
})
it('runs a compile when the Recompile button is pressed', async function () {
mockCompile()
mockBuildFile()
mockValidPdf()
renderWithEditorContext(<PdfPreview />, { scope })
// wait for "compile on load" to finish
await screen.findByRole('button', { name: 'Compiling…' })
await screen.findByRole('button', { name: 'Recompile' })
mockValidPdf()
// press the Recompile button => compile
const button = screen.getByRole('button', { name: 'Recompile' })
button.click()
await screen.findByRole('button', { name: 'Compiling…' })
await screen.findByRole('button', { name: 'Recompile' })
expect(fetchMock.calls()).to.have.length(6)
})
it('runs a compile on `pdf:recompile` event', async function () {
mockCompile()
mockBuildFile()
mockValidPdf()
renderWithEditorContext(<PdfPreview />, { scope })
// wait for "compile on load" to finish
await screen.findByRole('button', { name: 'Compiling…' })
await screen.findByRole('button', { name: 'Recompile' })
mockValidPdf()
fireEvent(window, new CustomEvent('pdf:recompile'))
await screen.findByRole('button', { name: 'Compiling…' })
await screen.findByRole('button', { name: 'Recompile' })
expect(fetchMock.calls()).to.have.length(6)
})
it('does not compile while compiling', async function () {
mockDelayed(mockCompile)
renderWithEditorContext(<PdfPreview />, { scope })
// trigger compiles while "compile on load" is running
await screen.findByRole('button', { name: 'Compiling…' })
fireEvent(window, new CustomEvent('pdf:recompile'))
expect(fetchMock.calls()).to.have.length(1)
})
it('disables compile button while compile is running', async function () {
mockCompile()
mockBuildFile()
mockValidPdf()
renderWithEditorContext(<PdfPreview />, { scope })
let button = screen.getByRole('button', { name: 'Compiling…' })
expect(button.hasAttribute('disabled')).to.be.true
button = await screen.findByRole('button', { name: 'Recompile' })
expect(button.hasAttribute('disabled')).to.be.false
})
it('runs a compile on doc change if autocompile is enabled', async function () {
mockCompile()
mockBuildFile()
mockValidPdf()
renderWithEditorContext(<PdfPreview />, { scope })
// wait for "compile on load" to finish
await screen.findByRole('button', { name: 'Compiling…' })
await screen.findByRole('button', { name: 'Recompile' })
// switch on auto compile
storeAndFireEvent('autocompile_enabled:project123', true)
mockValidPdf()
// fire a doc:changed event => compile
fireEvent(window, new CustomEvent('doc:changed'))
clock.tick(2000) // AUTO_COMPILE_DEBOUNCE
await screen.findByRole('button', { name: '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()
mockValidPdf()
renderWithEditorContext(<PdfPreview />, { scope })
// wait for "compile on load" to finish
await screen.findByRole('button', { name: 'Compiling…' })
await screen.findByRole('button', { name: 'Recompile' })
// make sure auto compile is switched off
storeAndFireEvent('autocompile_enabled:project123', false)
// 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()
mockValidPdf()
renderWithEditorContext(<PdfPreview />, {
scope: {
...scope,
'settings.syntaxValidation': true, // enable linting in the editor
hasLintingError: true, // mock a linting error
},
})
// wait for "compile on load" to finish
await screen.findByRole('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)
// 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)
})
describe('displays error messages', function () {
const compileErrorStatuses = {
'clear-cache':
'Sorry, something went wrong and your project could not be compiled. Please try again in a few moments.',
'clsi-maintenance':
'The compile servers are down for maintenance, and will be back shortly.',
'compile-in-progress':
'A previous compile is still running. Please wait a minute and try compiling again.',
exited: 'Server Error',
failure: 'No PDF',
generic: 'Server Error',
'project-too-large': 'Project too large',
'rate-limited': 'Compile rate limit hit',
terminated: 'Compilation cancelled',
timedout: 'Timed out',
'too-recently-compiled':
'This project was compiled very recently, so this compile has been skipped.',
unavailable:
'Sorry, the compile server for your project was temporarily unavailable. Please try again in a few moments.',
foo: 'Sorry, something went wrong and your project could not be compiled. Please try again in a few moments.',
}
for (const [status, message] of Object.entries(compileErrorStatuses)) {
it(`displays error message for '${status}' status`, async function () {
cleanup()
fetchMock.restore()
mockCompileError(status)
renderWithEditorContext(<PdfPreview />, { scope })
// wait for "compile on load" to finish
await screen.findByRole('button', { name: 'Compiling…' })
await screen.findByRole('button', { name: 'Recompile' })
screen.getByText(message)
})
}
})
it('displays expandable raw logs', async function () {
mockCompile()
mockBuildFile()
mockValidPdf()
// pretend that the content is large enough to trigger a "collapse"
// (in jsdom these values are always zero)
sinon.stub(HTMLElement.prototype, 'scrollHeight').value(500)
sinon.stub(HTMLElement.prototype, 'scrollWidth').value(500)
renderWithEditorContext(<PdfPreview />, { scope })
// wait for "compile on load" to finish
await screen.findByRole('button', { name: 'Compiling…' })
await screen.findByRole('button', { name: 'Recompile' })
const logsButton = screen.getByRole('button', { name: 'View logs' })
logsButton.click()
await screen.findByRole('button', { name: 'View PDF' })
// expand the log
const [expandButton] = screen.getAllByRole('button', { name: 'Expand' })
expandButton.click()
// collapse the log
const [collapseButton] = screen.getAllByRole('button', { name: 'Collapse' })
collapseButton.click()
})
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 />, { scope })
// wait for "compile on load" to finish
await screen.findByRole('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('begin:https://clsi.test-overleaf.com/')).to.be
.false // TODO: actual path
})
it('sends a clear cache request when the button is pressed', async function () {
mockCompile()
mockBuildFile()
mockValidPdf()
renderWithEditorContext(<PdfPreview />, { scope })
// wait for "compile on load" to finish
await screen.findByRole('button', { name: 'Compiling…' })
await screen.findByRole('button', { name: 'Recompile' })
const logsButton = screen.getByRole('button', {
name: 'View logs',
})
logsButton.click()
const clearCacheButton = await screen.findByRole('button', {
name: 'Clear cached files',
})
expect(clearCacheButton.hasAttribute('disabled')).to.be.false
mockClearCache()
// click the button
clearCacheButton.click()
await waitFor(() => {
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('begin:https://clsi.test-overleaf.com/')).to.be.true // TODO: actual path
})
it('handle "recompile from scratch"', async function () {
mockCompile()
mockBuildFile()
mockValidPdf()
renderWithEditorContext(<PdfPreview />, { scope })
// wait for "compile on load" to finish
await screen.findByRole('button', { name: 'Compiling…' })
await screen.findByRole('button', { name: 'Recompile' })
// show the logs UI
const logsButton = screen.getByRole('button', {
name: 'View logs',
})
logsButton.click()
const clearCacheButton = await screen.findByRole('button', {
name: 'Clear cached files',
})
expect(clearCacheButton.hasAttribute('disabled')).to.be.false
mockValidPdf()
const finishClearCache = mockDelayed(mockClearCache)
const recompileFromScratch = screen.getByRole('menuitem', {
name: 'Recompile from scratch',
hidden: true,
})
recompileFromScratch.click()
await waitFor(() => {
expect(clearCacheButton.hasAttribute('disabled')).to.be.true
})
finishClearCache()
// wait for compile to finish
await screen.findByRole('button', { name: 'Compiling…' })
await screen.findByRole('button', { name: 'Recompile' })
expect(fetchMock.called('express:/project/:projectId/compile')).to.be.true // TODO: auto_compile query param
expect(fetchMock.called('express:/project/:projectId/output')).to.be.true
expect(fetchMock.called('begin:https://clsi.test-overleaf.com/')).to.be.true // TODO: actual path
})
it('shows an error for an invalid URL', async function () {
mockCompile()
mockBuildFile()
nock('https://clsi.test-overleaf.com')
.get(/^\/build\/output.pdf/)
.replyWithError({
message: 'something awful happened',
code: 'AWFUL_ERROR',
})
renderWithEditorContext(<PdfPreview />, { scope })
await screen.findByText('Something went wrong while rendering this PDF.')
expect(screen.queryByLabelText('Page 1')).to.not.exist
expect(nock.isDone()).to.be.true
})
it('shows an error for a corrupt PDF', async function () {
mockCompile()
mockBuildFile()
nock('https://clsi.test-overleaf.com')
.get(/^\/build\/output.pdf/)
.replyWithFile(200, corruptPDF)
renderWithEditorContext(<PdfPreview />, { scope })
await screen.findByText('Something went wrong while rendering this PDF.')
expect(screen.queryByLabelText('Page 1')).to.not.exist
expect(nock.isDone()).to.be.true
})
describe('human readable logs', function () {
it('shows human readable hint for undefined reference errors', async function () {
mockCompile()
mockBuildFile({
...defaultFileResponses,
'/build/output.log': `
log This is pdfTeX, Version 3.14159265-2.6-1.40.21 (TeX Live 2020) (preloaded format=pdflatex 2020.9.10) 8 FEB 2022 16:27
entering extended mode
\\write18 enabled.
%&-line parsing enabled.
**main.tex
(./main.tex
LaTeX2e <2020-02-02> patch level 5
LaTeX Warning: Reference \`intorduction' on page 1 undefined on input line 11.
LaTeX Warning: Reference \`section1' on page 1 undefined on input line 13.
[1
{/usr/local/texlive/2020/texmf-var/fonts/map/pdftex/updmap/pdftex.map}] (/compi
le/output.aux)
LaTeX Warning: There were undefined references.
)
`,
})
mockValidPdf()
renderWithEditorContext(<PdfPreview />, { scope })
await screen.findByText(
"Reference `intorduction' on page 1 undefined on input line 11."
)
await screen.findByText(
"Reference `section1' on page 1 undefined on input line 13."
)
await screen.findByText('There were undefined references.')
const hints = await screen.findAllByText(
/You have referenced something which has not yet been labelled/
)
expect(hints.length).to.equal(3)
})
it('idoes not show human readable hint for undefined reference errors', async function () {
mockCompile()
mockBuildFile({
...defaultFileResponses,
'/build/output.log': `
Package rerunfilecheck Info: File \`output.out' has not changed.
(rerunfilecheck) Checksum: 339DB29951BB30436898BC39909EA4FA;11265.
Package rerunfilecheck Warning: File \`output.brf' has changed.
(rerunfilecheck) Rerun to get bibliographical references right.
Package rerunfilecheck Info: Checksums for \`output.brf':
(rerunfilecheck) Before: D41D8CD98F00B204E9800998ECF8427E;0
(rerunfilecheck) After: DF3260FAD3828D54C5E4E9337E97F7AF;4841.
)
`,
})
mockValidPdf()
renderWithEditorContext(<PdfPreview />, { scope })
await screen.findByText(
/Package rerunfilecheck Warning: File `output.brf' has changed. Rerun to get bibliographical references right./
)
expect(
screen.queryByText(
/You have referenced something which has not yet been labelled/
)
).to.not.exist
})
})
})

View file

@ -1,437 +0,0 @@
import PdfSynctexControls from '../../../../../frontend/js/features/pdf-preview/components/pdf-synctex-controls'
import { renderWithEditorContext } from '../../../helpers/render-with-context'
import sysendTestHelper from '../../../helpers/sysend'
import { cloneDeep } from 'lodash'
import fetchMock from 'fetch-mock'
import { fireEvent, screen, waitFor } from '@testing-library/react'
import fs from 'fs'
import path from 'path'
import { expect } from 'chai'
import { useDetachCompileContext as useCompileContext } from '../../../../../frontend/js/shared/context/detach-compile-context'
import { useFileTreeData } from '../../../../../frontend/js/shared/context/file-tree-data-context'
import { useEffect } from 'react'
const examplePDF = path.join(__dirname, '../fixtures/test-example.pdf')
const scope = {
settings: {
syntaxValidation: false,
pdfViewer: 'pdfjs',
},
editor: {
sharejs_doc: {
doc_id: 'test-doc',
getSnapshot: () => 'some doc content',
},
},
}
const outputFiles = [
{
path: 'output.pdf',
build: '123',
url: '/build/output.pdf',
type: 'pdf',
},
{
path: 'output.log',
build: '123',
url: '/build/output.log',
type: 'log',
},
]
const mockCompile = () =>
fetchMock.post('express:/project/:projectId/compile', {
body: {
status: 'success',
clsiServerId: 'foo',
compileGroup: 'standard',
pdfDownloadDomain: 'https://clsi.test-overleaf.com',
outputFiles: cloneDeep(outputFiles),
},
})
const fileResponses = {
'/build/output.pdf': () => fs.createReadStream(examplePDF),
'/build/output.log': '',
}
const mockBuildFile = () =>
fetchMock.get('begin:https://clsi.test-overleaf.com/', _url => {
const url = new URL(_url, 'https://clsi.test-overleaf.com')
if (url.pathname in fileResponses) {
return fileResponses[url.pathname]
}
return 404
})
const mockHighlights = [
{
page: 1,
h: 85.03936,
v: 509.999878,
width: 441.921265,
height: 8.855677,
},
{
page: 1,
h: 85.03936,
v: 486.089539,
width: 441.921265,
height: 8.855677,
},
]
const mockPosition = {
page: 1,
offset: { top: 10, left: 10 },
pageSize: { height: 500, width: 500 },
}
const mockSelectedEntities = [{ type: 'doc' }]
const mockSynctex = () =>
fetchMock
.get('express:/project/:projectId/sync/code', () => {
return { pdf: cloneDeep(mockHighlights) }
})
.get('express:/project/:projectId/sync/pdf', () => {
return { code: [{ file: 'main.tex', line: 100 }] }
})
const WithPosition = ({ mockPosition }) => {
const { setPosition } = useCompileContext()
// mock PDF scroll position update
useEffect(() => {
setPosition(mockPosition)
}, [mockPosition, setPosition])
return null
}
const WithSelectedEntities = ({ mockSelectedEntities = [] }) => {
const { setSelectedEntities } = useFileTreeData()
useEffect(() => {
setSelectedEntities(mockSelectedEntities)
}, [mockSelectedEntities, setSelectedEntities])
return null
}
describe('<PdfSynctexControls/>', function () {
beforeEach(function () {
window.metaAttributesCache = new Map()
fetchMock.restore()
mockCompile()
mockSynctex()
mockBuildFile()
})
afterEach(function () {
window.metaAttributesCache = new Map()
fetchMock.restore()
})
it('handles clicks on sync buttons', async function () {
const { container } = renderWithEditorContext(
<>
<WithPosition mockPosition={mockPosition} />
<WithSelectedEntities mockSelectedEntities={mockSelectedEntities} />
<PdfSynctexControls />
</>,
{ scope }
)
const syncToPdfButton = await screen.findByRole('button', {
name: 'Go to code location in PDF',
})
const syncToCodeButton = await screen.findByRole('button', {
name: /Go to PDF location in code/,
})
expect(container.querySelectorAll('.synctex-control-icon').length).to.equal(
2
)
// mock editor cursor position update
fireEvent(
window,
new CustomEvent('cursor:editor:update', {
detail: { row: 100, column: 10 },
})
)
fireEvent.click(syncToPdfButton)
expect(syncToPdfButton.disabled).to.be.true
await waitFor(() => {
expect(fetchMock.called('express:/project/:projectId/sync/code')).to.be
.true
})
fireEvent.click(syncToCodeButton)
expect(syncToCodeButton.disabled).to.be.true
await waitFor(() => {
expect(fetchMock.called('express:/project/:projectId/sync/pdf')).to.be
.true
})
})
it('disables button when multiple entities are selected', async function () {
renderWithEditorContext(
<>
<WithPosition mockPosition={mockPosition} />
<WithSelectedEntities
mockSelectedEntities={[{ type: 'doc' }, { type: 'doc' }]}
/>
<PdfSynctexControls />
</>,
{ scope }
)
const syncToPdfButton = await screen.findByRole('button', {
name: 'Go to code location in PDF',
})
expect(syncToPdfButton.disabled).to.be.true
const syncToCodeButton = await screen.findByRole('button', {
name: /Go to PDF location in code/,
})
expect(syncToCodeButton.disabled).to.be.true
})
it('disables button when a file is selected', async function () {
renderWithEditorContext(
<>
<WithPosition mockPosition={mockPosition} />
<WithSelectedEntities mockSelectedEntities={[{ type: 'file' }]} />
<PdfSynctexControls />
</>,
{ scope }
)
const syncToPdfButton = await screen.findByRole('button', {
name: 'Go to code location in PDF',
})
expect(syncToPdfButton.disabled).to.be.true
const syncToCodeButton = await screen.findByRole('button', {
name: /Go to PDF location in code/,
})
expect(syncToCodeButton.disabled).to.be.true
})
describe('with detacher role', async function () {
beforeEach(function () {
window.metaAttributesCache.set('ol-detachRole', 'detacher')
})
it('does not have go to PDF location button nor arrow icon', async function () {
const { container } = renderWithEditorContext(
<>
<WithPosition mockPosition={mockPosition} />
<WithSelectedEntities mockSelectedEntities={mockSelectedEntities} />
<PdfSynctexControls />
</>,
{ scope }
)
expect(
await screen.queryByRole('button', {
name: 'Go to PDF location in code',
})
).to.not.exist
expect(container.querySelector('.synctex-control-icon')).to.not.exist
})
it('send set highlights action', async function () {
renderWithEditorContext(
<>
<WithPosition mockPosition={mockPosition} />
<WithSelectedEntities mockSelectedEntities={mockSelectedEntities} />
<PdfSynctexControls />
</>,
{ scope }
)
sysendTestHelper.resetHistory()
const syncToPdfButton = await screen.findByRole('button', {
name: 'Go to code location in PDF',
})
// mock editor cursor position update
fireEvent(
window,
new CustomEvent('cursor:editor:update', {
detail: { row: 100, column: 10 },
})
)
expect(syncToPdfButton.disabled).to.be.false
fireEvent.click(syncToPdfButton)
expect(syncToPdfButton.disabled).to.be.true
await waitFor(() => {
expect(fetchMock.called('express:/project/:projectId/sync/code')).to.be
.true
})
// synctex is called locally and the result are broadcast for the detached
// tab
expect(sysendTestHelper.getLastBroacastMessage()).to.deep.equal({
role: 'detacher',
event: 'action-setHighlights',
data: { args: [mockHighlights] },
})
})
it('reacts to sync to code action', async function () {
renderWithEditorContext(
<>
<WithPosition mockPosition={mockPosition} />
<WithSelectedEntities mockSelectedEntities={mockSelectedEntities} />
<PdfSynctexControls />
</>,
{ scope }
)
await waitFor(() => {
expect(fetchMock.called('express:/project/:projectId/compile')).to.be
.true
})
sysendTestHelper.receiveMessage({
role: 'detached',
event: 'action-sync-to-code',
data: {
args: [mockPosition],
},
})
await waitFor(() => {
expect(fetchMock.called('express:/project/:projectId/sync/pdf')).to.be
.true
})
})
})
describe('with detached role', async function () {
beforeEach(function () {
window.metaAttributesCache.set('ol-detachRole', 'detached')
})
it('does not have go to code location button nor arrow icon', async function () {
const { container } = renderWithEditorContext(
<>
<WithPosition mockPosition={mockPosition} />
<PdfSynctexControls />
</>,
{ scope }
)
expect(
await screen.queryByRole('button', {
name: 'Go to code location in PDF',
})
).to.not.exist
expect(container.querySelector('.synctex-control-icon')).to.not.exist
})
it('send go to code line action', async function () {
const { container } = renderWithEditorContext(
<>
<WithPosition mockPosition={mockPosition} />
<PdfSynctexControls />
</>,
{ scope }
)
const syncToCodeButton = await screen.findByRole('button', {
name: /Go to PDF location in code/,
})
expect(syncToCodeButton.disabled).to.be.true
sysendTestHelper.receiveMessage({
role: 'detached',
event: 'state-has-single-selected-doc',
data: { value: true },
})
expect(syncToCodeButton.disabled).to.be.false
sysendTestHelper.resetHistory()
fireEvent.click(syncToCodeButton)
// the button is only disabled when the state is updated via sysend
expect(syncToCodeButton.disabled).to.be.false
expect(container.querySelectorAll('.synctex-spin-icon').length).to.equal(
0
)
expect(sysendTestHelper.getLastBroacastMessage()).to.deep.equal({
role: 'detached',
event: 'action-sync-to-code',
data: {
args: [mockPosition, 72],
},
})
})
it('update inflight state', async function () {
const { container } = renderWithEditorContext(
<>
<WithPosition mockPosition={mockPosition} />
<PdfSynctexControls />
</>,
{ scope }
)
sysendTestHelper.receiveMessage({
role: 'detached',
event: 'state-has-single-selected-doc',
data: { value: true },
})
const syncToCodeButton = await screen.findByRole('button', {
name: /Go to PDF location in code/,
})
expect(syncToCodeButton.disabled).to.be.false
expect(container.querySelectorAll('.synctex-spin-icon').length).to.equal(
0
)
sysendTestHelper.receiveMessage({
role: 'detacher',
event: 'state-sync-to-code-inflight',
data: { value: true },
})
expect(syncToCodeButton.disabled).to.be.true
expect(container.querySelectorAll('.synctex-spin-icon').length).to.equal(
1
)
sysendTestHelper.receiveMessage({
role: 'detacher',
event: 'state-sync-to-code-inflight',
data: { value: false },
})
expect(syncToCodeButton.disabled).to.be.false
expect(container.querySelectorAll('.synctex-spin-icon').length).to.equal(
0
)
})
})
})

View file

@ -1,148 +0,0 @@
import fetchMock from 'fetch-mock'
import { cloneDeep } from 'lodash'
import nock from 'nock'
import fs from 'fs'
import path from 'path'
export const examplePDF = path.join(__dirname, '../fixtures/test-example.pdf')
export const corruptPDF = path.join(
__dirname,
'../fixtures/test-example-corrupt.pdf'
)
const outputFiles = [
{
path: 'output.pdf',
build: '123',
url: '/build/output.pdf',
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',
},
]
export const mockCompile = (delayPromise = Promise.resolve()) =>
fetchMock.post(
'express:/project/:projectId/compile',
delayPromise.then(() => ({
body: {
status: 'success',
clsiServerId: 'foo',
compileGroup: 'priority',
pdfDownloadDomain: 'https://clsi.test-overleaf.com',
outputFiles: cloneDeep(outputFiles),
},
}))
)
export const mockCompileError = status =>
fetchMock.post('express:/project/:projectId/compile', {
body: {
status,
clsiServerId: 'foo',
compileGroup: 'priority',
},
})
export const mockValidationProblems = validationProblems =>
fetchMock.post('express:/project/:projectId/compile', {
body: {
status: 'validation-problems',
validationProblems,
clsiServerId: 'foo',
compileGroup: 'priority',
},
})
export const mockClearCache = (delayPromise = Promise.resolve()) =>
fetchMock.delete(
'express:/project/:projectId/output',
delayPromise.then(() => ({
body: {
status: 204,
},
}))
)
export const mockValidPdf = () => {
nock('https://clsi.test-overleaf.com')
.get(/^\/build\/output\.pdf/)
.replyWithFile(200, examplePDF)
}
export const defaultFileResponses = {
'/build/output.pdf': () => fs.createReadStream(examplePDF),
'/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
`,
}
export const mockBuildFile = (responses = defaultFileResponses) => {
fetchMock.get('begin:https://clsi.test-overleaf.com/', _url => {
const url = new URL(_url, 'https://clsi.test-overleaf.com')
if (url.pathname in responses) {
return responses[url.pathname]
}
return 404
})
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
})
}

View file

@ -28,7 +28,7 @@ export function EditorProviders({
}, },
isRestrictedTokenMember = false, isRestrictedTokenMember = false,
clsiServerId = '1234', clsiServerId = '1234',
scope, scope = {},
features = { features = {
referencesSearch: true, referencesSearch: true,
}, },

View file

@ -10,13 +10,15 @@
"moduleResolution": "node" /* Specify module resolution strategy */, "moduleResolution": "node" /* Specify module resolution strategy */,
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
"skipLibCheck": true /* Skip type checking of declaration files. */, "skipLibCheck": true /* Skip type checking of declaration files. */,
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */,
"types": ["cypress", "@testing-library/cypress"]
}, },
"include": [ "include": [
"frontend/js/**/*.*", "frontend/js/**/*.*",
"modules/**/frontend/js/**/*.*", "modules/**/frontend/js/**/*.*",
"test/frontend/**/*.*", "test/frontend/**/*.*",
"modules/**/test/frontend/**/*.*", "modules/**/test/frontend/**/*.*",
"cypress",
"types" "types"
] ]
} }

View file

@ -5,6 +5,11 @@ declare global {
user: { user: {
id: string id: string
} }
metaAttributesCache: Map<string, unknown>
i18n: {
currentLangCode: string
}
ExposedSettings: Record<string, unknown>
} }
} }
export {} // pretend this is a module export {} // pretend this is a module

View file

@ -234,10 +234,12 @@ module.exports = {
output: 'manifest.json', output: 'manifest.json',
}), }),
// Ensure that process.env.RESET_APP_DATA_TIMER is defined, to avoid an error.
// https://github.com/algolia/algoliasearch-client-javascript/issues/756
new webpack.EnvironmentPlugin({ new webpack.EnvironmentPlugin({
// Ensure that process.env.RESET_APP_DATA_TIMER is defined, to avoid an error.
// https://github.com/algolia/algoliasearch-client-javascript/issues/756
RESET_APP_DATA_TIMER: '120000', RESET_APP_DATA_TIMER: '120000',
// Ensure that process.env.CYPRESS is defined (see utils/worker.js)
CYPRESS: false,
}), }),
// Prevent moment from loading (very large) locale files that aren't used // Prevent moment from loading (very large) locale files that aren't used