mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-23 17:19:37 +02:00
[clsi-cache] check compiler settings before using compile from cache (#24845)
* [web] provide an actual rootFolder from EditorProviders in tests - Fixup SocketIOMock and ShareJS mocks to provide the complete interface - Extend SocketIOMock interface to count event listeners - Fixup test that did not expect to find a working rootDoc * [web] expose imageName from ProjectContext * [clsi-cache] check compiler settings before using compile from cache * [web] avoid fetching initial compile from clsi-cache in PDF detach tab GitOrigin-RevId: e3c754a7ceca55f03a317e1bc8ae45ed12cc2f02
This commit is contained in:
@@ -28,6 +28,7 @@ const MAX_ENTRIES_IN_OUTPUT_TAR = 100
|
||||
* @param {string} editorId
|
||||
* @param {[{path: string}]} outputFiles
|
||||
* @param {string} compileGroup
|
||||
* @param {Record<string, any>} options
|
||||
*/
|
||||
function notifyCLSICacheAboutBuild({
|
||||
projectId,
|
||||
@@ -36,6 +37,7 @@ function notifyCLSICacheAboutBuild({
|
||||
editorId,
|
||||
outputFiles,
|
||||
compileGroup,
|
||||
options,
|
||||
}) {
|
||||
if (!Settings.apis.clsiCache.enabled) return
|
||||
|
||||
@@ -55,6 +57,7 @@ function notifyCLSICacheAboutBuild({
|
||||
downloadHost: Settings.apis.clsi.downloadHost,
|
||||
clsiServerId: Settings.apis.clsi.clsiServerId,
|
||||
compileGroup,
|
||||
options,
|
||||
},
|
||||
signal: AbortSignal.timeout(15_000),
|
||||
}).catch(err => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
const Path = require('node:path')
|
||||
const RequestParser = require('./RequestParser')
|
||||
const CompileManager = require('./CompileManager')
|
||||
const Settings = require('@overleaf/settings')
|
||||
@@ -123,6 +124,15 @@ function compile(req, res, next) {
|
||||
editorId: request.editorId,
|
||||
outputFiles,
|
||||
compileGroup: request.compileGroup,
|
||||
options: {
|
||||
compiler: request.compiler,
|
||||
draft: request.draft,
|
||||
imageName: request.imageName
|
||||
? Path.basename(request.imageName)
|
||||
: undefined,
|
||||
rootResourcePath: request.rootResourcePath,
|
||||
stopOnFirstError: request.stopOnFirstError,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -133,7 +133,8 @@ async function getLatestBuildFromCache(req, res) {
|
||||
baseURL += `/user/${userId}`
|
||||
}
|
||||
|
||||
const { ranges, contentId, clsiServerId, compileGroup, size } = meta
|
||||
const { ranges, contentId, clsiServerId, compileGroup, size, options } =
|
||||
meta
|
||||
|
||||
const outputFiles = allFiles
|
||||
.filter(
|
||||
@@ -175,6 +176,7 @@ async function getLatestBuildFromCache(req, res) {
|
||||
clsiServerId,
|
||||
pdfDownloadDomain,
|
||||
pdfCachingMinChunkSize,
|
||||
options,
|
||||
})
|
||||
} catch (err) {
|
||||
if (err instanceof NotFoundError) {
|
||||
|
||||
@@ -43,26 +43,77 @@ const outputFiles = () => {
|
||||
]
|
||||
}
|
||||
|
||||
const compileFromCacheResponse = () => {
|
||||
return {
|
||||
fromCache: true,
|
||||
status: 'success',
|
||||
clsiServerId: 'foo',
|
||||
compileGroup: 'priority',
|
||||
pdfDownloadDomain: 'https://clsi.test-overleaf.com',
|
||||
outputFiles: outputFiles(),
|
||||
options: {
|
||||
rootResourcePath: 'main.tex',
|
||||
imageName: 'texlive-full:2024.1',
|
||||
compiler: 'pdflatex',
|
||||
stopOnFirstError: false,
|
||||
draft: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const interceptCompileFromCacheRequest = ({
|
||||
times,
|
||||
promise,
|
||||
}: {
|
||||
times: number
|
||||
promise: Promise<void>
|
||||
}) => {
|
||||
return cy.intercept(
|
||||
{ path: '/project/*/output/cached/output.overleaf.json', times },
|
||||
async req => {
|
||||
await promise
|
||||
req.reply({ body: compileFromCacheResponse() })
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export const interceptCompileRequest = ({ times = 1 } = {}) => {
|
||||
return cy.intercept(
|
||||
{ method: 'POST', pathname: '/project/*/compile', times },
|
||||
{
|
||||
body: {
|
||||
status: 'success',
|
||||
clsiServerId: 'foo',
|
||||
compileGroup: 'priority',
|
||||
pdfDownloadDomain: 'https://clsi.test-overleaf.com',
|
||||
outputFiles: outputFiles(),
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export const interceptCompile = ({
|
||||
prefix = 'compile',
|
||||
times = 1,
|
||||
cached = false,
|
||||
regular = true,
|
||||
outputPDFFixture = 'output.pdf',
|
||||
} = {}) => {
|
||||
if (cached) {
|
||||
cy.intercept(
|
||||
{ pathname: '/project/*/output/cached/output.overleaf.json', times },
|
||||
{
|
||||
body: {
|
||||
fromCache: true,
|
||||
status: 'success',
|
||||
clsiServerId: 'foo',
|
||||
compileGroup: 'priority',
|
||||
pdfDownloadDomain: 'https://clsi.test-overleaf.com',
|
||||
outputFiles: outputFiles(),
|
||||
},
|
||||
}
|
||||
{ path: '/project/*/output/cached/output.overleaf.json', times },
|
||||
{ body: compileFromCacheResponse() }
|
||||
).as(`${prefix}-cached`)
|
||||
} else {
|
||||
cy.intercept(
|
||||
{ pathname: '/project/*/output/cached/output.overleaf.json', times },
|
||||
{ statusCode: 404 }
|
||||
).as(`${prefix}-cached`)
|
||||
}
|
||||
|
||||
if (regular) {
|
||||
interceptCompileRequest({ times }).as(`${prefix}`)
|
||||
} else {
|
||||
cy.intercept(
|
||||
{ method: 'POST', pathname: '/project/*/compile', times },
|
||||
{
|
||||
@@ -75,23 +126,6 @@ export const interceptCompile = ({
|
||||
},
|
||||
}
|
||||
).as(`${prefix}`)
|
||||
} else {
|
||||
cy.intercept(
|
||||
{ pathname: '/project/*/output/cached/output.overleaf.json', times },
|
||||
{ statusCode: 404 }
|
||||
).as(`${prefix}-cached`)
|
||||
cy.intercept(
|
||||
{ method: 'POST', pathname: '/project/*/compile', times },
|
||||
{
|
||||
body: {
|
||||
status: 'success',
|
||||
clsiServerId: 'foo',
|
||||
compileGroup: 'priority',
|
||||
pdfDownloadDomain: 'https://clsi.test-overleaf.com',
|
||||
outputFiles: outputFiles(),
|
||||
},
|
||||
}
|
||||
).as(`${prefix}`)
|
||||
}
|
||||
|
||||
cy.intercept(
|
||||
@@ -114,12 +148,22 @@ export const waitForCompile = ({
|
||||
prefix = 'compile',
|
||||
pdf = false,
|
||||
cached = false,
|
||||
regular = true,
|
||||
} = {}) => {
|
||||
if (cached) {
|
||||
cy.wait(`@${prefix}-cached`)
|
||||
} else {
|
||||
}
|
||||
if (regular) {
|
||||
cy.wait(`@${prefix}`)
|
||||
}
|
||||
return waitForCompileOutput({ prefix, pdf, cached })
|
||||
}
|
||||
|
||||
export const waitForCompileOutput = ({
|
||||
prefix = 'compile',
|
||||
pdf = false,
|
||||
cached = false,
|
||||
} = {}) => {
|
||||
cy.wait(`@${prefix}-log`)
|
||||
.its('request.query.clsiserverid')
|
||||
.should('eq', cached ? 'cache' : 'foo') // straight from cache if cached
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import '@testing-library/cypress/add-commands'
|
||||
import {
|
||||
interceptCompile,
|
||||
interceptCompileFromCacheRequest,
|
||||
waitForCompile,
|
||||
interceptDeferredCompile,
|
||||
interceptCompileRequest,
|
||||
} from './compile'
|
||||
import { interceptEvents } from './events'
|
||||
import { interceptAsync } from './intercept-async'
|
||||
@@ -21,6 +23,8 @@ declare global {
|
||||
interface Chainable {
|
||||
interceptAsync: typeof interceptAsync
|
||||
interceptCompile: typeof interceptCompile
|
||||
interceptCompileRequest: typeof interceptCompileRequest
|
||||
interceptCompileFromCacheRequest: typeof interceptCompileFromCacheRequest
|
||||
interceptEvents: typeof interceptEvents
|
||||
interceptMetadata: typeof interceptMetadata
|
||||
waitForCompile: typeof waitForCompile
|
||||
@@ -36,6 +40,11 @@ declare global {
|
||||
|
||||
Cypress.Commands.add('interceptAsync', interceptAsync)
|
||||
Cypress.Commands.add('interceptCompile', interceptCompile)
|
||||
Cypress.Commands.add('interceptCompileRequest', interceptCompileRequest)
|
||||
Cypress.Commands.add(
|
||||
'interceptCompileFromCacheRequest',
|
||||
interceptCompileFromCacheRequest
|
||||
)
|
||||
Cypress.Commands.add('interceptEvents', interceptEvents)
|
||||
Cypress.Commands.add('interceptMetadata', interceptMetadata)
|
||||
Cypress.Commands.add('waitForCompile', waitForCompile)
|
||||
|
||||
@@ -277,13 +277,34 @@ if (typeof io === 'undefined' || !io) {
|
||||
current = SocketShimV2
|
||||
}
|
||||
|
||||
export class SocketIOMock extends EventEmitter {
|
||||
export class SocketIOMock extends SocketShimBase {
|
||||
constructor() {
|
||||
super(new EventEmitter())
|
||||
this.socket = {
|
||||
get connected() {
|
||||
return false
|
||||
},
|
||||
get sessionid() {
|
||||
return undefined
|
||||
},
|
||||
get transport() {
|
||||
return {}
|
||||
},
|
||||
get transports() {
|
||||
return []
|
||||
},
|
||||
|
||||
connect() {},
|
||||
disconnect(reason) {},
|
||||
}
|
||||
}
|
||||
|
||||
addListener(event, listener) {
|
||||
this.on(event, listener)
|
||||
this._socket.on(event, listener)
|
||||
}
|
||||
|
||||
removeListener(event, listener) {
|
||||
this.off(event, listener)
|
||||
this._socket.off(event, listener)
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
@@ -294,6 +315,10 @@ export class SocketIOMock extends EventEmitter {
|
||||
// Round-trip through JSON.parse/stringify to simulate (de-)serializing on network layer.
|
||||
this.emit(...JSON.parse(JSON.stringify(args)))
|
||||
}
|
||||
|
||||
countEventListeners(event) {
|
||||
return this._socket.events[event].length
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
|
||||
@@ -34,6 +34,7 @@ import { buildFileList } from '../../features/pdf-preview/util/file-list'
|
||||
import { useLayoutContext } from './layout-context'
|
||||
import { useUserContext } from './user-context'
|
||||
import { useFileTreeData } from '@/shared/context/file-tree-data-context'
|
||||
import { useDetachContext } from '@/shared/context/detach-context'
|
||||
import { useFileTreePathContext } from '@/features/file-tree/contexts/file-tree-path'
|
||||
import { useUserSettingsContext } from '@/shared/context/user-settings-context'
|
||||
import { useFeatureFlag } from '@/shared/context/split-test-context'
|
||||
@@ -46,6 +47,8 @@ import {
|
||||
} from '@/shared/hooks/use-pdf-scroll-position'
|
||||
import { PdfFileDataList } from '@/features/pdf-preview/util/types'
|
||||
import { isSplitTestEnabled } from '@/utils/splitTestUtils'
|
||||
import { captureException } from '@/infrastructure/error-reporter'
|
||||
import OError from '@overleaf/o-error'
|
||||
|
||||
type PdfFile = Record<string, any>
|
||||
|
||||
@@ -117,8 +120,15 @@ export const LocalCompileContext = createContext<CompileContext | undefined>(
|
||||
export const LocalCompileProvider: FC = ({ children }) => {
|
||||
const { hasPremiumCompile, isProjectOwner } = useEditorContext()
|
||||
const { openDocWithId, openDocs, currentDocument } = useEditorManagerContext()
|
||||
const { role } = useDetachContext()
|
||||
|
||||
const { _id: projectId, rootDocId, joinedOnce } = useProjectContext()
|
||||
const {
|
||||
_id: projectId,
|
||||
rootDocId,
|
||||
joinedOnce,
|
||||
imageName,
|
||||
compiler: compilerName,
|
||||
} = useProjectContext()
|
||||
|
||||
const { pdfPreviewOpen } = useLayoutContext()
|
||||
|
||||
@@ -190,10 +200,15 @@ export const LocalCompileProvider: FC = ({ children }) => {
|
||||
const [compiledOnce, setCompiledOnce] = useState(false)
|
||||
// fetch initial compile response from cache
|
||||
const [initialCompileFromCache, setInitialCompileFromCache] = useState(
|
||||
isSplitTestEnabled('initial-compile-from-clsi-cache')
|
||||
isSplitTestEnabled('initial-compile-from-clsi-cache') &&
|
||||
// Avoid fetching the initial compile from cache in PDF detach tab
|
||||
role !== 'detached'
|
||||
)
|
||||
// Compile triggered while fetching the initial compile from cache
|
||||
const upgradeInitialCompileFromCacheRef = useRef(false)
|
||||
// fetch of initial compile from cache is pending
|
||||
const [pendingInitialCompileFromCache, setPendingInitialCompileFromCache] =
|
||||
useState(false)
|
||||
// Raw data from clsi-cache, will need post-processing and check settings
|
||||
const [dataFromCache, setDataFromCache] = useState<CompileResponseData>()
|
||||
|
||||
// whether the cache is being cleared
|
||||
const [clearingCache, setClearingCache] = useState(false)
|
||||
@@ -337,41 +352,88 @@ export const LocalCompileProvider: FC = ({ children }) => {
|
||||
|
||||
// try to fetch the last compile result after opening the project, potentially before joining the project.
|
||||
useEffect(() => {
|
||||
if (initialCompileFromCache) {
|
||||
setInitialCompileFromCache(false)
|
||||
setCompiling(true)
|
||||
setCompiledOnce(true)
|
||||
if (initialCompileFromCache && !pendingInitialCompileFromCache) {
|
||||
setPendingInitialCompileFromCache(true)
|
||||
getJSON(`/project/${projectId}/output/cached/output.overleaf.json`)
|
||||
.then((data: any) => {
|
||||
setCompiling(false)
|
||||
setData({
|
||||
...data,
|
||||
options: compiler.defaultOptions,
|
||||
})
|
||||
if (upgradeInitialCompileFromCacheRef.current) {
|
||||
compilingRef.current = false
|
||||
compiler.compile() // trigger regular compile
|
||||
}
|
||||
// Hand data over to next effect, it will wait for project/doc loading.
|
||||
setDataFromCache(data)
|
||||
})
|
||||
.catch(() => {
|
||||
setCompiling(false)
|
||||
if (upgradeInitialCompileFromCacheRef.current) {
|
||||
compilingRef.current = false
|
||||
compiler.compile() // trigger regular compile
|
||||
} else {
|
||||
setCompiledOnce(false) // trigger auto compile
|
||||
}
|
||||
// Let the isAutoCompileOnLoad effect take over
|
||||
setInitialCompileFromCache(false)
|
||||
setPendingInitialCompileFromCache(false)
|
||||
})
|
||||
}
|
||||
}, [projectId, initialCompileFromCache, compiler])
|
||||
}, [projectId, initialCompileFromCache, pendingInitialCompileFromCache])
|
||||
|
||||
// Maybe adopt the compile from cache
|
||||
useEffect(() => {
|
||||
if (!dataFromCache) return // no compile from cache available
|
||||
if (!joinedOnce) return // wait for joinProject, it populates the file-tree.
|
||||
if (!currentDocument) return // wait for current doc to load, it affects the rootDoc override
|
||||
if (compiledOnce) return // regular compile triggered
|
||||
|
||||
// Gracefully access file-tree and getRootDocOverride
|
||||
let settingsUpToDate = false
|
||||
try {
|
||||
dataFromCache.rootDocId = findEntityByPath(
|
||||
dataFromCache.options?.rootResourcePath || ''
|
||||
)?.entity?._id
|
||||
const rootDocOverride = compiler.getRootDocOverrideId() || rootDocId
|
||||
settingsUpToDate =
|
||||
rootDocOverride === dataFromCache.rootDocId &&
|
||||
dataFromCache.options.imageName === imageName &&
|
||||
dataFromCache.options.compiler === compilerName &&
|
||||
dataFromCache.options.stopOnFirstError === stopOnFirstError &&
|
||||
dataFromCache.options.draft === draft
|
||||
} catch (err) {
|
||||
captureException(
|
||||
OError.tag(err as unknown as Error, 'validate compile options', {
|
||||
options: dataFromCache.options,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (settingsUpToDate) {
|
||||
setData(dataFromCache)
|
||||
setCompiledOnce(true)
|
||||
}
|
||||
setDataFromCache(undefined)
|
||||
setInitialCompileFromCache(false)
|
||||
setPendingInitialCompileFromCache(false)
|
||||
}, [
|
||||
dataFromCache,
|
||||
joinedOnce,
|
||||
currentDocument,
|
||||
compiledOnce,
|
||||
rootDocId,
|
||||
findEntityByPath,
|
||||
compiler,
|
||||
compilerName,
|
||||
imageName,
|
||||
stopOnFirstError,
|
||||
draft,
|
||||
])
|
||||
|
||||
// always compile the PDF once after opening the project, after the doc has loaded
|
||||
useEffect(() => {
|
||||
if (!compiledOnce && currentDocument && !initialCompileFromCache) {
|
||||
if (
|
||||
!compiledOnce &&
|
||||
currentDocument &&
|
||||
!initialCompileFromCache &&
|
||||
!pendingInitialCompileFromCache
|
||||
) {
|
||||
setCompiledOnce(true)
|
||||
compiler.compile({ isAutoCompileOnLoad: true })
|
||||
}
|
||||
}, [compiledOnce, currentDocument, initialCompileFromCache, compiler])
|
||||
}, [
|
||||
compiledOnce,
|
||||
currentDocument,
|
||||
initialCompileFromCache,
|
||||
pendingInitialCompileFromCache,
|
||||
compiler,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
setHasShortCompileTimeout(
|
||||
@@ -619,10 +681,10 @@ export const LocalCompileProvider: FC = ({ children }) => {
|
||||
// start a compile manually
|
||||
const startCompile = useCallback(
|
||||
options => {
|
||||
upgradeInitialCompileFromCacheRef.current = true
|
||||
setCompiledOnce(true)
|
||||
compiler.compile(options)
|
||||
},
|
||||
[compiler, upgradeInitialCompileFromCacheRef]
|
||||
[compiler, setCompiledOnce]
|
||||
)
|
||||
|
||||
// stop a compile manually
|
||||
|
||||
@@ -34,6 +34,7 @@ export const ProjectProvider: FC = ({ children }) => {
|
||||
const {
|
||||
_id,
|
||||
compiler,
|
||||
imageName,
|
||||
name,
|
||||
rootDoc_id: rootDocId,
|
||||
members,
|
||||
@@ -59,6 +60,7 @@ export const ProjectProvider: FC = ({ children }) => {
|
||||
return {
|
||||
_id,
|
||||
compiler,
|
||||
imageName,
|
||||
name,
|
||||
rootDocId,
|
||||
members,
|
||||
@@ -75,6 +77,7 @@ export const ProjectProvider: FC = ({ children }) => {
|
||||
}, [
|
||||
_id,
|
||||
compiler,
|
||||
imageName,
|
||||
name,
|
||||
rootDocId,
|
||||
members,
|
||||
|
||||
@@ -18,6 +18,7 @@ export type ProjectContextValue = {
|
||||
rootDocId?: string
|
||||
mainBibliographyDocId?: string
|
||||
compiler: string
|
||||
imageName: string
|
||||
members: ProjectContextMember[]
|
||||
invites: ProjectContextMember[]
|
||||
features: {
|
||||
|
||||
@@ -89,6 +89,7 @@ const initialize = () => {
|
||||
sharejs_doc: {
|
||||
doc_id: 'test-doc',
|
||||
getSnapshot: () => 'some doc content',
|
||||
hasBufferedOps: () => false,
|
||||
},
|
||||
open_doc_name: 'testfile.tex',
|
||||
},
|
||||
|
||||
@@ -2,6 +2,7 @@ export function mockDocument(text: string) {
|
||||
return {
|
||||
doc_id: 'story-doc',
|
||||
getSnapshot: () => text,
|
||||
hasBufferedOps: () => false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -211,6 +211,7 @@ const mockDoc = (content: string, changes: Array<Record<string, any>> = []) => {
|
||||
return null
|
||||
},
|
||||
ranges: new RangesTracker(changes, []),
|
||||
hasBufferedOps: () => false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,19 @@ const Layout: FC<{ layout: IdeLayout; view?: IdeView }> = ({
|
||||
}
|
||||
|
||||
describe('<PdfPreview/>', function () {
|
||||
let projectId: string
|
||||
beforeEach(function () {
|
||||
/**
|
||||
* There are time sensitive tests in this test suite. They need to wait for a Promise before resolving a request.
|
||||
*
|
||||
* Using a promise across the test-env (browser) vs stub-env (server) causes additional latency.
|
||||
*
|
||||
* This latency seems to stack up when adding more intercepts for the same path. Using static responses for some of these intercepts does not help.
|
||||
*
|
||||
* All of that seems like a bug in Cypress. For now just work around it by using a unique projectId for each intercept.
|
||||
*/
|
||||
projectId = Math.random().toString().slice(2)
|
||||
|
||||
window.metaAttributesCache.set('ol-preventCompileOnLoad', true)
|
||||
window.metaAttributesCache.set(
|
||||
'ol-compilesUserContentDomain',
|
||||
@@ -61,7 +73,12 @@ describe('<PdfPreview/>', function () {
|
||||
})
|
||||
|
||||
it('uses the cache when available', function () {
|
||||
cy.interceptCompile({ prefix: 'compile', times: 1, cached: true })
|
||||
cy.interceptCompile({
|
||||
prefix: 'compile',
|
||||
times: 1,
|
||||
cached: true,
|
||||
regular: false,
|
||||
})
|
||||
|
||||
const scope = mockScope()
|
||||
|
||||
@@ -74,13 +91,18 @@ describe('<PdfPreview/>', function () {
|
||||
)
|
||||
|
||||
// wait for "compile from cache on load" to finish
|
||||
cy.waitForCompile({ pdf: true, cached: true })
|
||||
cy.waitForCompile({ pdf: true, cached: true, regular: false })
|
||||
|
||||
cy.contains('Your Paper')
|
||||
})
|
||||
|
||||
it('uses the cache when available then compiles', function () {
|
||||
cy.interceptCompile({ prefix: 'compile', times: 1, cached: true })
|
||||
cy.interceptCompile({
|
||||
prefix: 'compile',
|
||||
times: 1,
|
||||
cached: true,
|
||||
regular: false,
|
||||
})
|
||||
|
||||
const scope = mockScope()
|
||||
|
||||
@@ -93,7 +115,7 @@ describe('<PdfPreview/>', function () {
|
||||
)
|
||||
|
||||
// wait for "compile from cache on load" to finish
|
||||
cy.waitForCompile({ pdf: true, cached: true })
|
||||
cy.waitForCompile({ pdf: true, cached: true, regular: false })
|
||||
cy.contains('Your Paper')
|
||||
|
||||
// Then trigger a new compile
|
||||
@@ -112,6 +134,154 @@ describe('<PdfPreview/>', function () {
|
||||
cy.contains('Modern Authoring Tools for Science')
|
||||
})
|
||||
|
||||
describe('racing compile from cache and regular compile trigger', function () {
|
||||
for (const [timing] of ['before rendering', 'after rendering']) {
|
||||
it(`replaces the compile from cache with a regular compile - ${timing}`, function () {
|
||||
const requestedOnce = new Set()
|
||||
;['log', 'pdf', 'blg'].forEach(ext => {
|
||||
cy.intercept({ pathname: `/build/*/output.${ext}` }, req => {
|
||||
if (requestedOnce.has(ext)) {
|
||||
throw new Error(
|
||||
`compile from cache triggered extra ${ext} request: ${req.url}`
|
||||
)
|
||||
}
|
||||
requestedOnce.add(ext)
|
||||
req.reply({ fixture: `build/output.${ext},null` })
|
||||
}).as(`compile-${ext}`)
|
||||
})
|
||||
const { promise, resolve } = Promise.withResolvers<void>()
|
||||
cy.interceptCompileFromCacheRequest({
|
||||
promise,
|
||||
times: 1,
|
||||
}).as('cached-compile')
|
||||
cy.interceptCompileRequest().as('compile')
|
||||
|
||||
const scope = mockScope()
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope} projectId={projectId}>
|
||||
<div className="pdf-viewer">
|
||||
<PdfPreview />
|
||||
</div>
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
// press the Recompile button => compile
|
||||
cy.findByRole('button', { name: 'Recompile' }).click()
|
||||
|
||||
if (timing === 'before rendering') {
|
||||
cy.then(() => resolve())
|
||||
cy.wait('@cached-compile')
|
||||
}
|
||||
|
||||
// wait for rendering to finish
|
||||
cy.waitForCompile({ pdf: true, cached: false })
|
||||
|
||||
if (timing === 'after rendering') {
|
||||
cy.then(() => resolve())
|
||||
cy.wait('@cached-compile')
|
||||
}
|
||||
|
||||
cy.contains('Your Paper')
|
||||
cy.then(() => Array.from(requestedOnce).sort().join(',')).should(
|
||||
'equal',
|
||||
'blg,log,pdf'
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
describe('clsi-cache project settings validation', function () {
|
||||
const cases = {
|
||||
// Flaky, skip for now
|
||||
'uses compile from cache when nothing changed': {
|
||||
cached: true,
|
||||
setup: () => {},
|
||||
props: {},
|
||||
},
|
||||
'ignores the compile from cache when imageName changed': {
|
||||
cached: false,
|
||||
setup: () => {},
|
||||
props: {
|
||||
imageName: 'texlive-full:2025.1',
|
||||
},
|
||||
},
|
||||
'ignores the compile from cache when compiler changed': {
|
||||
cached: false,
|
||||
setup: () => {},
|
||||
props: {
|
||||
compiler: 'lualatex',
|
||||
},
|
||||
},
|
||||
'ignores the compile from cache when draft mode changed': {
|
||||
cached: false,
|
||||
setup: () => {
|
||||
cy.window().then(w =>
|
||||
w.localStorage.setItem(`draft:${projectId}`, 'true')
|
||||
)
|
||||
},
|
||||
props: {},
|
||||
},
|
||||
'ignores the compile from cache when stopOnFirstError mode changed': {
|
||||
cached: false,
|
||||
setup: () => {
|
||||
cy.window().then(w =>
|
||||
w.localStorage.setItem(`stop_on_first_error:${projectId}`, 'true')
|
||||
)
|
||||
},
|
||||
props: {},
|
||||
},
|
||||
'ignores the compile from cache when rootDoc changed': {
|
||||
cached: false,
|
||||
setup: () => {},
|
||||
props: {
|
||||
rootDocId: 'new-root-doc-id',
|
||||
rootFolder: [
|
||||
{
|
||||
_id: 'root-folder-id',
|
||||
name: 'rootFolder',
|
||||
docs: [
|
||||
{
|
||||
_id: '_root_doc_id',
|
||||
name: 'main.tex',
|
||||
},
|
||||
{
|
||||
_id: 'new-root-doc-id',
|
||||
name: 'new-main.tex',
|
||||
},
|
||||
],
|
||||
folders: [],
|
||||
fileRefs: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
Object.entries(cases).forEach(([name, { cached, setup, props }]) => {
|
||||
it(name, function () {
|
||||
cy.interceptCompile({
|
||||
cached: true,
|
||||
regular: !cached,
|
||||
})
|
||||
|
||||
const scope = mockScope()
|
||||
window.metaAttributesCache.set('ol-preventCompileOnLoad', false)
|
||||
setup()
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope} projectId={projectId} {...props}>
|
||||
<div className="pdf-viewer">
|
||||
<PdfPreview />
|
||||
</div>
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
// wait for compile to finish
|
||||
cy.waitForCompile({ pdf: true, cached, regular: !cached })
|
||||
cy.contains('Your Paper')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('runs a compile when the Recompile button is pressed', function () {
|
||||
cy.interceptCompile()
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ export const mockScope = () => ({
|
||||
sharejs_doc: {
|
||||
doc_id: 'test-doc',
|
||||
getSnapshot: () => 'some doc content',
|
||||
hasBufferedOps: () => false,
|
||||
},
|
||||
view: new EditorView({
|
||||
doc: '\\documentclass{article}',
|
||||
|
||||
@@ -56,8 +56,7 @@ describe('ChatContext', function () {
|
||||
it('subscribes when mounted', function () {
|
||||
const socket = new SocketIOMock()
|
||||
renderChatContextHook({ socket })
|
||||
// Assert that there is 1 listener
|
||||
expect(socket.events['new-chat-message']).to.have.length(1)
|
||||
expect(socket.countEventListeners('new-chat-message')).to.equal(1)
|
||||
})
|
||||
|
||||
it('unsubscribes when unmounted', function () {
|
||||
@@ -66,8 +65,7 @@ describe('ChatContext', function () {
|
||||
|
||||
unmount()
|
||||
|
||||
// Assert that there is 0 listeners
|
||||
expect(socket.events['new-chat-message'].length).to.equal(0)
|
||||
expect(socket.countEventListeners('new-chat-message')).to.equal(0)
|
||||
})
|
||||
|
||||
it('adds received messages to the list', async function () {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { FileTreeProvider } from '../helpers/file-tree-provider'
|
||||
describe('<FileTreeToolbar/>', function () {
|
||||
it('without selected files', function () {
|
||||
cy.mount(
|
||||
<EditorProviders>
|
||||
<EditorProviders rootDocId="">
|
||||
<FileTreeProvider>
|
||||
<FileTreeToolbar />
|
||||
</FileTreeProvider>
|
||||
|
||||
@@ -44,6 +44,8 @@ export function EditorProviders({
|
||||
email: 'owner@example.com',
|
||||
},
|
||||
rootDocId = '_root_doc_id',
|
||||
imageName = 'texlive-full:2024.1',
|
||||
compiler = 'pdflatex',
|
||||
socket = new SocketIOMock(),
|
||||
isRestrictedTokenMember = false,
|
||||
clsiServerId = '1234',
|
||||
@@ -58,7 +60,12 @@ export function EditorProviders({
|
||||
{
|
||||
_id: 'root-folder-id',
|
||||
name: 'rootFolder',
|
||||
docs: [],
|
||||
docs: [
|
||||
{
|
||||
_id: '_root_doc_id',
|
||||
name: 'main.tex',
|
||||
},
|
||||
],
|
||||
folders: [],
|
||||
fileRefs: [],
|
||||
},
|
||||
@@ -99,6 +106,10 @@ export function EditorProviders({
|
||||
sharejs_doc: {
|
||||
doc_id: 'test-doc',
|
||||
getSnapshot: () => 'some doc content',
|
||||
hasBufferedOps: () => false,
|
||||
on: () => {},
|
||||
off: () => {},
|
||||
leaveAndCleanUpPromise: async () => {},
|
||||
},
|
||||
},
|
||||
project: {
|
||||
@@ -108,6 +119,8 @@ export function EditorProviders({
|
||||
features: projectFeatures,
|
||||
rootDoc_id: rootDocId,
|
||||
rootFolder,
|
||||
imageName,
|
||||
compiler,
|
||||
},
|
||||
ui,
|
||||
$watch: (path, callback) => {
|
||||
|
||||
Reference in New Issue
Block a user