diff --git a/services/clsi/app/js/CLSICacheHandler.js b/services/clsi/app/js/CLSICacheHandler.js index 38a04d81ac..de6f512987 100644 --- a/services/clsi/app/js/CLSICacheHandler.js +++ b/services/clsi/app/js/CLSICacheHandler.js @@ -28,6 +28,7 @@ const MAX_ENTRIES_IN_OUTPUT_TAR = 100 * @param {string} editorId * @param {[{path: string}]} outputFiles * @param {string} compileGroup + * @param {Record} 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 => { diff --git a/services/clsi/app/js/CompileController.js b/services/clsi/app/js/CompileController.js index 20cac73c7a..87a7db6ec2 100644 --- a/services/clsi/app/js/CompileController.js +++ b/services/clsi/app/js/CompileController.js @@ -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, + }, }) } diff --git a/services/web/app/src/Features/Compile/ClsiCacheController.js b/services/web/app/src/Features/Compile/ClsiCacheController.js index a8c99a830b..9795fd3ef2 100644 --- a/services/web/app/src/Features/Compile/ClsiCacheController.js +++ b/services/web/app/src/Features/Compile/ClsiCacheController.js @@ -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) { diff --git a/services/web/cypress/support/shared/commands/compile.ts b/services/web/cypress/support/shared/commands/compile.ts index e6ea7f70dd..9f7273c403 100644 --- a/services/web/cypress/support/shared/commands/compile.ts +++ b/services/web/cypress/support/shared/commands/compile.ts @@ -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 +}) => { + 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 diff --git a/services/web/cypress/support/shared/commands/index.ts b/services/web/cypress/support/shared/commands/index.ts index cd39f8f5d1..bb55fdddac 100644 --- a/services/web/cypress/support/shared/commands/index.ts +++ b/services/web/cypress/support/shared/commands/index.ts @@ -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) diff --git a/services/web/frontend/js/ide/connection/SocketIoShim.js b/services/web/frontend/js/ide/connection/SocketIoShim.js index 40ada0485a..9fb57ef1f1 100644 --- a/services/web/frontend/js/ide/connection/SocketIoShim.js +++ b/services/web/frontend/js/ide/connection/SocketIoShim.js @@ -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 { diff --git a/services/web/frontend/js/shared/context/local-compile-context.tsx b/services/web/frontend/js/shared/context/local-compile-context.tsx index 4ecb465382..7b237a441d 100644 --- a/services/web/frontend/js/shared/context/local-compile-context.tsx +++ b/services/web/frontend/js/shared/context/local-compile-context.tsx @@ -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 @@ -117,8 +120,15 @@ export const LocalCompileContext = createContext( 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() // 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 diff --git a/services/web/frontend/js/shared/context/project-context.tsx b/services/web/frontend/js/shared/context/project-context.tsx index 48532e821c..b97db3d252 100644 --- a/services/web/frontend/js/shared/context/project-context.tsx +++ b/services/web/frontend/js/shared/context/project-context.tsx @@ -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, diff --git a/services/web/frontend/js/shared/context/types/project-context.tsx b/services/web/frontend/js/shared/context/types/project-context.tsx index aa045d693f..18eb42010c 100644 --- a/services/web/frontend/js/shared/context/types/project-context.tsx +++ b/services/web/frontend/js/shared/context/types/project-context.tsx @@ -18,6 +18,7 @@ export type ProjectContextValue = { rootDocId?: string mainBibliographyDocId?: string compiler: string + imageName: string members: ProjectContextMember[] invites: ProjectContextMember[] features: { diff --git a/services/web/frontend/stories/decorators/scope.tsx b/services/web/frontend/stories/decorators/scope.tsx index 473aa037de..0e26551f32 100644 --- a/services/web/frontend/stories/decorators/scope.tsx +++ b/services/web/frontend/stories/decorators/scope.tsx @@ -89,6 +89,7 @@ const initialize = () => { sharejs_doc: { doc_id: 'test-doc', getSnapshot: () => 'some doc content', + hasBufferedOps: () => false, }, open_doc_name: 'testfile.tex', }, diff --git a/services/web/frontend/stories/fixtures/document.ts b/services/web/frontend/stories/fixtures/document.ts index 2ff0466a72..ad2995a4dc 100644 --- a/services/web/frontend/stories/fixtures/document.ts +++ b/services/web/frontend/stories/fixtures/document.ts @@ -2,6 +2,7 @@ export function mockDocument(text: string) { return { doc_id: 'story-doc', getSnapshot: () => text, + hasBufferedOps: () => false, } } diff --git a/services/web/frontend/stories/source-editor/source-editor.stories.tsx b/services/web/frontend/stories/source-editor/source-editor.stories.tsx index 53cf2b7eea..ac011bc5a3 100644 --- a/services/web/frontend/stories/source-editor/source-editor.stories.tsx +++ b/services/web/frontend/stories/source-editor/source-editor.stories.tsx @@ -211,6 +211,7 @@ const mockDoc = (content: string, changes: Array> = []) => { return null }, ranges: new RangesTracker(changes, []), + hasBufferedOps: () => false, } } diff --git a/services/web/test/frontend/components/pdf-preview/pdf-preview.spec.tsx b/services/web/test/frontend/components/pdf-preview/pdf-preview.spec.tsx index 3f5222f0e0..a1cbed1fe3 100644 --- a/services/web/test/frontend/components/pdf-preview/pdf-preview.spec.tsx +++ b/services/web/test/frontend/components/pdf-preview/pdf-preview.spec.tsx @@ -28,7 +28,19 @@ const Layout: FC<{ layout: IdeLayout; view?: IdeView }> = ({ } describe('', 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('', 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('', 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('', 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('', 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() + cy.interceptCompileFromCacheRequest({ + promise, + times: 1, + }).as('cached-compile') + cy.interceptCompileRequest().as('compile') + + const scope = mockScope() + cy.mount( + +
+ +
+
+ ) + + // 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( + +
+ +
+
+ ) + + // 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() diff --git a/services/web/test/frontend/components/pdf-preview/scope.tsx b/services/web/test/frontend/components/pdf-preview/scope.tsx index 8e61102108..bb4e4d9d1d 100644 --- a/services/web/test/frontend/components/pdf-preview/scope.tsx +++ b/services/web/test/frontend/components/pdf-preview/scope.tsx @@ -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}', diff --git a/services/web/test/frontend/features/chat/context/chat-context.test.jsx b/services/web/test/frontend/features/chat/context/chat-context.test.jsx index 76410a65fd..3d14a11355 100644 --- a/services/web/test/frontend/features/chat/context/chat-context.test.jsx +++ b/services/web/test/frontend/features/chat/context/chat-context.test.jsx @@ -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 () { diff --git a/services/web/test/frontend/features/file-tree/components/file-tree-toolbar.spec.tsx b/services/web/test/frontend/features/file-tree/components/file-tree-toolbar.spec.tsx index 70dad331a2..e663541049 100644 --- a/services/web/test/frontend/features/file-tree/components/file-tree-toolbar.spec.tsx +++ b/services/web/test/frontend/features/file-tree/components/file-tree-toolbar.spec.tsx @@ -5,7 +5,7 @@ import { FileTreeProvider } from '../helpers/file-tree-provider' describe('', function () { it('without selected files', function () { cy.mount( - + diff --git a/services/web/test/frontend/helpers/editor-providers.jsx b/services/web/test/frontend/helpers/editor-providers.jsx index cd21b92456..a6bc9c32c6 100644 --- a/services/web/test/frontend/helpers/editor-providers.jsx +++ b/services/web/test/frontend/helpers/editor-providers.jsx @@ -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) => {