[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:
Jakob Ackermann
2025-04-16 08:05:35 +00:00
committed by Copybot
parent ef958f97a1
commit 39110d9da9
17 changed files with 416 additions and 72 deletions
@@ -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) => {