[saas-e2e] harden recompiling (#28514)

* [saas-e2e] harden recompiling

* [server-ce] import changes from SaaS e2e tests

GitOrigin-RevId: 9b86ed864457b068b8dded94a7e12658a047aac2
This commit is contained in:
Jakob Ackermann
2025-09-16 16:22:36 +02:00
committed by Copybot
parent a9a28a13f5
commit a13fca15d3
10 changed files with 337 additions and 242 deletions

View File

@@ -4,7 +4,7 @@ import {
openProjectViaInviteNotification,
} from './helpers/project'
import { isExcludedBySharding, startWith } from './helpers/config'
import { throttledRecompile } from './helpers/compile'
import { prepareWaitForNextCompileSlot } from './helpers/compile'
const USER = 'user@example.com'
const COLLABORATOR = 'collaborator@example.com'
@@ -17,8 +17,10 @@ describe('Project creation and compilation', function () {
it('users can create project and compile it', function () {
login(USER)
createProject('test-project')
const recompile = throttledRecompile()
const { recompile, waitForCompile } = prepareWaitForNextCompileSlot()
waitForCompile(() => {
createProject('test-project')
})
cy.findByRole('textbox', { name: 'Source Editor editing' }).within(() => {
cy.findByText('\\maketitle').parent().click()
cy.findByText('\\maketitle').parent().type('\n\\section{{}Test Section}')
@@ -109,7 +111,6 @@ describe('Project creation and compilation', function () {
cy.findByRole('navigation', { name: 'Project files and outline' })
.findByRole('button', { name: 'New file' })
.click()
cy.findByRole('dialog').within(() => {
cy.findByRole('button', { name: 'From another project' }).click()
cy.findByLabelText('Select a Project').select(sourceProjectName)

View File

@@ -22,20 +22,19 @@ describe('editor', () => {
let projectName: string
let projectId: string
let recompile: () => void
let waitForCompileRateLimitCoolOff: (fn: () => void) => void
let waitForCompile: (fn: () => void) => void
beforeWithReRunOnTestRetry(function () {
projectName = `project-${uuid()}`
login(USER)
createProject(projectName, { type: 'Example project', open: false }).then(
id => (projectId = id)
)
;({ recompile, waitForCompileRateLimitCoolOff } =
prepareWaitForNextCompileSlot())
;({ recompile, waitForCompile } = prepareWaitForNextCompileSlot())
})
beforeEach(() => {
login(USER)
waitForCompileRateLimitCoolOff(() => {
waitForCompile(() => {
openProjectById(projectId)
})
})

View File

@@ -39,7 +39,7 @@ describe('filestore migration', function () {
const projectName = `project-${uuid()}`
let defaultImage: string
let projectId: string
let waitForCompileRateLimitCoolOff: (fn: () => void) => void
let waitForCompile: (fn: () => void) => void
const previousBinaryFiles: (() => void)[] = []
function avoid502() {
@@ -55,7 +55,7 @@ describe('filestore migration', function () {
) {
before(function () {
login(email)
waitForCompileRateLimitCoolOff(() => {
waitForCompile(() => {
cy.visit(`/project/${projectId}`)
})
previousBinaryFiles.push(prepareFileUploadTest(true))
@@ -112,8 +112,7 @@ describe('filestore migration', function () {
.should('match', /\/project\/[a-fA-F0-9]{24}/)
.then(url => (projectId = url.split('/').pop()!))
let queueReset
;({ waitForCompileRateLimitCoolOff, queueReset } =
prepareWaitForNextCompileSlot())
;({ waitForCompile, queueReset } = prepareWaitForNextCompileSlot())
queueReset()
// Create a new binary file
@@ -265,7 +264,7 @@ describe('filestore migration', function () {
createProject(projectName, { type: 'Example project', open: false }).then(
id => (projectId = id)
)
;({ waitForCompileRateLimitCoolOff } = prepareWaitForNextCompileSlot())
;({ waitForCompile } = prepareWaitForNextCompileSlot())
})
}
addNewBinaryFileAndCheckPrevious()
@@ -298,7 +297,7 @@ describe('filestore migration', function () {
// filestore-migration
beforeEach(() => {
login(email)
waitForCompileRateLimitCoolOff(() => {
waitForCompile(() => {
openProjectById(projectId)
})
ensureStopOnFirstErrorIsActive()
@@ -327,7 +326,7 @@ describe('filestore migration', function () {
.parent()
.type(`\n\\section{{}Test Section ${id}}`)
waitForCompileRateLimitCoolOff(() => {
waitForCompile(() => {
cy.findByRole('button', { name: 'Toggle compile options menu' }).click()
cy.findByRole('menuitem', {

View File

@@ -12,7 +12,7 @@ import {
import git from 'isomorphic-git'
import http from 'isomorphic-git/http/web'
import LightningFS from '@isomorphic-git/lightning-fs'
import { throttledRecompile } from './helpers/compile'
import { prepareWaitForNextCompileSlot } from './helpers/compile'
const USER = 'user@example.com'
@@ -52,13 +52,13 @@ describe('git-bridge', function () {
function maybeClearAllTokens() {
cy.visit('/user/settings')
cy.findByRole('heading', { name: 'Git integration' })
cy.get('button')
.contains(/Generate token|Add another token/)
.then(btn => {
if (btn.text() === 'Add another token') {
clearAllTokens()
}
})
cy.findByRole('button', {
name: /Generate token|Add another token/i,
}).then(btn => {
if (btn.text() === 'Add another token') {
clearAllTokens()
}
})
}
beforeEach(function () {
@@ -130,9 +130,12 @@ describe('git-bridge', function () {
cy.findByTestId('left-menu').within(() => {
cy.findByRole('button', { name: 'Git' }).click()
})
cy.findByTestId('git-bridge-modal').within(() => {
cy.get('@projectId').then(id => {
cy.get('code').contains(`git clone ${gitURL(id.toString())}`)
cy.findByLabelText('Git clone project command').contains(
`git clone ${gitURL(id.toString())}`
)
})
cy.findByRole('button', {
name: 'Generate token',
@@ -151,14 +154,19 @@ describe('git-bridge', function () {
ensureUserExists({ email: 'collaborator-link-ro@example.com' })
let projectName: string
let recompile: () => void
let waitForCompile: (triggerCompile: () => void) => void
beforeEach(() => {
projectName = uuid()
createProject(projectName, { open: false }).as('projectId')
;({ recompile, waitForCompile } = prepareWaitForNextCompileSlot())
})
it('should expose r/w interface to owner', () => {
maybeClearAllTokens()
openProjectByName(projectName)
waitForCompile(() => {
openProjectByName(projectName)
})
checkGitAccess('readAndWrite')
})
@@ -169,7 +177,9 @@ describe('git-bridge', function () {
'Editor'
)
maybeClearAllTokens()
openProjectByName(projectName)
waitForCompile(() => {
openProjectByName(projectName)
})
checkGitAccess('readAndWrite')
})
@@ -180,7 +190,9 @@ describe('git-bridge', function () {
'Viewer'
)
maybeClearAllTokens()
openProjectByName(projectName)
waitForCompile(() => {
openProjectByName(projectName)
})
checkGitAccess('readOnly')
})
@@ -190,212 +202,232 @@ describe('git-bridge', function () {
const email = 'collaborator-link-rw@example.com'
login(email)
maybeClearAllTokens()
openProjectViaLinkSharingAsUser(
linkSharingReadAndWrite,
projectName,
email
)
waitForCompile(() => {
openProjectViaLinkSharingAsUser(
linkSharingReadAndWrite,
projectName,
email
)
})
checkGitAccess('readAndWrite')
})
})
it('should expose r/o interface to link-sharing r/o collaborator', () => {
openProjectByName(projectName)
waitForCompile(() => {
openProjectByName(projectName)
})
enableLinkSharing().then(({ linkSharingReadOnly }) => {
const email = 'collaborator-link-ro@example.com'
login(email)
maybeClearAllTokens()
openProjectViaLinkSharingAsUser(
linkSharingReadOnly,
projectName,
email
)
waitForCompile(() => {
openProjectViaLinkSharingAsUser(
linkSharingReadOnly,
projectName,
email
)
})
checkGitAccess('readOnly')
})
})
})
function checkGitAccess(access: 'readOnly' | 'readAndWrite') {
const recompile = throttledRecompile()
cy.findByRole('navigation', {
name: 'Project actions',
})
.findByRole('button', { name: 'Menu' })
.click()
cy.findByTestId('left-menu').within(() => {
cy.findByRole('heading', { name: 'Sync' })
cy.findByRole('button', { name: 'Git' }).click()
})
cy.get('@projectId').then(projectId => {
cy.findByTestId('git-bridge-modal').within(() => {
cy.findByLabelText('Git clone project command').contains(
`git clone ${gitURL(projectId.toString())}`
)
function checkGitAccess(access: 'readOnly' | 'readAndWrite') {
cy.findByRole('navigation', {
name: 'Project actions',
})
cy.findByRole('heading', { name: 'Clone with Git' })
cy.findByRole('button', {
name: 'Generate token',
}).click()
cy.get('code')
.contains(/olp_[a-zA-Z0-9]{16}/)
.then(async tokenEl => {
const token = tokenEl.text()
.findByRole('button', { name: 'Menu' })
.click()
cy.findByTestId('left-menu').within(() => {
cy.findByRole('heading', { name: 'Sync' })
cy.findByRole('button', { name: 'Git' }).click()
})
cy.get('@projectId').then(projectId => {
cy.findByTestId('git-bridge-modal').within(() => {
cy.findByLabelText('Git clone project command').contains(
`git clone ${gitURL(projectId.toString())}`
)
cy.findByRole('heading', { name: 'Clone with Git' })
cy.findByRole('button', {
name: 'Generate token',
}).click()
})
cy.findByLabelText('Git authentication token')
.contains(/olp_[a-zA-Z0-9]{16}/)
.then(async tokenEl => {
const token = tokenEl.text()
// close Git modal
cy.findAllByText('Close').last().click()
// close editor menu
cy.get('.left-menu-modal-backdrop').click()
// close Git modal
cy.get('body').type('{esc}')
cy.findByTestId('git-bridge-modal').should('not.exist')
// close the modal
cy.get('body').type('{esc}')
cy.findByTestId('left-menu').should('not.exist')
const fs = new LightningFS('fs')
const dir = `/${projectId}`
const fs = new LightningFS('fs')
const dir = `/${projectId}`
async function readFile(path: string): Promise<string> {
return new Promise((resolve, reject) => {
fs.readFile(path, { encoding: 'utf8' }, (err, blob) => {
if (err) return reject(err)
resolve(blob as string)
})
})
}
async function readFile(path: string): Promise<string> {
return new Promise((resolve, reject) => {
fs.readFile(path, { encoding: 'utf8' }, (err, blob) => {
if (err) return reject(err)
resolve(blob as string)
async function writeFile(path: string, data: string) {
return new Promise<void>((resolve, reject) => {
fs.writeFile(path, data, undefined, err => {
if (err) return reject(err)
resolve()
})
})
}
const commonOptions = {
dir,
fs,
}
const url = gitURL(projectId.toString())
url.username = '' // basic auth is specified separately.
const httpOptions = {
http,
url: url.toString(),
headers: {
Authorization: `Basic ${Buffer.from(`git:${token}`).toString('base64')}`,
},
}
const authorOptions = {
author: { name: 'user', email: USER },
committer: { name: 'user', email: USER },
}
const mainTex = `${dir}/main.tex`
// Clone
cy.then({ timeout: 10_000 }, async () => {
await git.clone({
...commonOptions,
...httpOptions,
})
})
}
async function writeFile(path: string, data: string) {
return new Promise<void>((resolve, reject) => {
fs.writeFile(path, data, undefined, err => {
if (err) return reject(err)
resolve()
cy.findByText(/\\documentclass/)
.parent()
.parent()
.then(async editor => {
const onDisk = await readFile(mainTex)
expect(onDisk.replaceAll('\n', '')).to.equal(editor.text())
})
})
}
const commonOptions = {
dir,
fs,
}
const url = gitURL(projectId.toString())
url.username = '' // basic auth is specified separately.
const httpOptions = {
http,
url: url.toString(),
headers: {
Authorization: `Basic ${Buffer.from(`git:${token}`).toString('base64')}`,
},
}
const authorOptions = {
author: { name: 'user', email: USER },
committer: { name: 'user', email: USER },
}
const mainTex = `${dir}/main.tex`
// Clone
cy.then({ timeout: 10_000 }, async () => {
await git.clone({
...commonOptions,
...httpOptions,
})
})
cy.findByText(/\\documentclass/)
.parent()
.parent()
.then(async editor => {
const onDisk = await readFile(mainTex)
expect(onDisk.replaceAll('\n', '')).to.equal(editor.text())
})
const text = `
const text = `
\\documentclass{article}
\\begin{document}
Hello world
\\end{document}
`
// Make a change
cy.then(async () => {
await writeFile(mainTex, text)
await git.add({
...commonOptions,
filepath: 'main.tex',
})
await git.commit({
...commonOptions,
...authorOptions,
message: 'Swap main.tex',
})
})
if (access === 'readAndWrite') {
// check history before push
cy.findAllByText('History').last().click()
cy.findByText('(via Git)').should('not.exist')
cy.findAllByText('Back to editor').last().click()
// Make a change
cy.then(async () => {
await git.push({
await writeFile(mainTex, text)
await git.add({
...commonOptions,
...httpOptions,
filepath: 'main.tex',
})
await git.commit({
...commonOptions,
...authorOptions,
message: 'Swap main.tex',
})
})
} else {
cy.then(async () => {
try {
if (access === 'readAndWrite') {
// check history before push
cy.findByRole('navigation', {
name: 'Project actions',
})
.findByRole('button', { name: 'History' })
.click()
cy.findByText('(via Git)').should('not.exist')
cy.findAllByText('Back to editor').last().click()
cy.then(async () => {
await git.push({
...commonOptions,
...httpOptions,
})
expect.fail('push should have failed')
} catch (err) {
expect(err).to.match(/branches were not updated/)
expect(err).to.match(/forbidden/)
}
})
} else {
cy.then(async () => {
try {
await git.push({
...commonOptions,
...httpOptions,
})
expect.fail('push should have failed')
} catch (err) {
expect(err).to.match(/branches were not updated/)
expect(err).to.match(/forbidden/)
}
})
return // return early, below are write access bits
}
// check push in editor
cy.findByText(/\\documentclass/)
.parent()
.parent()
.should('have.text', text.replaceAll('\n', ''))
// Wait for history sync - trigger flush by toggling the UI
cy.findByRole('navigation', {
name: 'Project actions',
})
.findByRole('button', { name: 'History' })
.click()
cy.findAllByText('Back to editor').last().click()
return // return early, below are write access bits
}
// check push in editor
cy.findByText(/\\documentclass/)
.parent()
.parent()
.should('have.text', text.replaceAll('\n', ''))
// Wait for history sync - trigger flush by toggling the UI
cy.findAllByText('History').last().click()
cy.findAllByText('Back to editor').last().click()
// check push in history
cy.findAllByText('History').last().click()
cy.findByText(/Hello world/)
cy.findByText('(via Git)').should('exist')
// Back to the editor
cy.findAllByText('Back to editor').last().click()
cy.findByText(/\\documentclass/)
.parent()
.parent()
.click()
.type('% via editor{enter}')
// Trigger flush via compile
recompile()
// Back into the history, check what we just added
cy.findAllByText('History').last().click()
cy.findByText(/% via editor/)
// Pull the change
cy.then(async () => {
await git.pull({
...commonOptions,
...httpOptions,
...authorOptions,
// check push in history
cy.findByRole('navigation', {
name: 'Project actions',
})
.findByRole('button', { name: 'History' })
.click()
cy.findByText(/Hello world/)
cy.findByText('(via Git)').should('exist')
expect(await readFile(mainTex)).to.equal(text + '% via editor\n')
// Back to the editor
cy.findAllByText('Back to editor').last().click()
cy.findByText(/\\documentclass/)
.parent()
.parent()
.click()
.type('% via editor{enter}')
// Trigger flush via compile
recompile()
// Back into the history, check what we just added
cy.findByRole('navigation', {
name: 'Project actions',
})
.findByRole('button', { name: 'History' })
.click()
cy.findByText(/% via editor/)
// Pull the change
cy.then(async () => {
await git.pull({
...commonOptions,
...httpOptions,
...authorOptions,
})
expect(await readFile(mainTex)).to.equal(
text + '% via editor\n'
)
})
})
})
})
}
})
}
})
})
function checkDisabled() {

View File

@@ -6,7 +6,7 @@ import {
} from './helpers/config'
import { dockerCompose, getRedisKeys } from './helpers/hostAdminClient'
import { createProject } from './helpers/project'
import { throttledRecompile } from './helpers/compile'
import { prepareWaitForNextCompileSlot } from './helpers/compile'
const USER = 'user@example.com'
const PROJECT_NAME = 'Old Project'
@@ -31,10 +31,12 @@ describe('GracefulShutdown', function () {
it('should display banner and flush changes out of redis', () => {
bringServerProBackUp()
login(USER)
createProject(PROJECT_NAME).then(id => {
projectId = id
const { recompile, waitForCompile } = prepareWaitForNextCompileSlot()
waitForCompile(() => {
createProject(PROJECT_NAME).then(id => {
projectId = id
})
})
const recompile = throttledRecompile()
cy.log('add additional content')
cy.findByText('\\maketitle').parent().click()

View File

@@ -3,11 +3,6 @@
* The naive approach is waiting a fixed a mount of time (3s) just before clicking the button.
* This helper takes into account that other UI interactions take time. We can deduce that latency from the fixed delay (3s minus other latency). This can bring down the effective waiting time to 0s.
*/
export function throttledRecompile() {
const { queueReset, recompile } = prepareWaitForNextCompileSlot()
queueReset()
return recompile
}
export function stopCompile(options: { delay?: number } = {}) {
const { delay = 0 } = options
@@ -24,25 +19,51 @@ export function prepareWaitForNextCompileSlot() {
lastCompile = Date.now()
})
}
function waitForCompileRateLimitCoolOff(triggerCompile: () => void) {
function waitForCompileRateLimitCoolOff() {
cy.then(() => {
cy.log('Wait for recompile rate-limit to cool off')
const msSinceLastCompile = Date.now() - lastCompile
cy.wait(Math.max(0, 1_000 - msSinceLastCompile))
queueReset()
})
}
function waitForCompile(triggerCompile: () => void) {
waitForCompileRateLimitCoolOff()
cy.then(() => {
let compilingVisible: () => void
const waitForCompilingVisible = new Promise<void>(resolve => {
compilingVisible = resolve
})
cy.intercept(
{
method: 'POST',
pathname: /\/project\/[a-fA-F0-9]{24}\/compile$/,
times: 1,
},
async req => {
await waitForCompilingVisible
req.continue()
}
).as('recompile')
triggerCompile()
cy.log('Wait for compile to finish')
cy.findByRole('button', { name: 'Compiling…' }).then(() =>
compilingVisible()
)
cy.wait('@recompile')
cy.findByRole('button', { name: 'Compiling…' }).should('not.exist')
cy.findByRole('button', { name: 'Recompile' }).should('be.visible')
})
}
function recompile() {
waitForCompileRateLimitCoolOff(() => {
cy.findByText('Recompile').click()
waitForCompile(() => {
cy.findByRole('button', { name: 'Recompile' }).click()
})
}
return {
queueReset,
waitForCompileRateLimitCoolOff,
waitForCompile,
recompile,
}
}

View File

@@ -1,5 +1,5 @@
import { createProject } from './helpers/project'
import { throttledRecompile } from './helpers/compile'
import { prepareWaitForNextCompileSlot } from './helpers/compile'
import { ensureUserExists, login } from './helpers/login'
import { isExcludedBySharding, startWith } from './helpers/config'
@@ -62,8 +62,10 @@ describe('History', function () {
const CLASS_DELETION = 'ol-cm-deletion-marker'
it('should support labels, comparison and download', () => {
createProject('labels')
const recompile = throttledRecompile()
const { recompile, waitForCompile } = prepareWaitForNextCompileSlot()
waitForCompile(() => {
createProject('labels')
})
cy.log('add content, including a line that will get removed soon')
cy.findByRole('textbox', { name: 'Source Editor editing' }).within(() => {

View File

@@ -16,7 +16,7 @@ import {
shareProjectByEmailAndAcceptInviteViaDash,
shareProjectByEmailAndAcceptInviteViaEmail,
} from './helpers/project'
import { throttledRecompile } from './helpers/compile'
import { prepareWaitForNextCompileSlot } from './helpers/compile'
import { beforeWithReRunOnTestRetry } from './helpers/beforeWithReRunOnTestRetry'
describe('Project Sharing', function () {
@@ -25,8 +25,11 @@ describe('Project Sharing', function () {
startWith({ withDataDir: true, pro: true })
let projectName: string
let recompile: () => void
let waitForCompile: (triggerCompile: () => void) => void
beforeWithReRunOnTestRetry(function () {
projectName = getSpamSafeProjectName()
;({ recompile, waitForCompile } = prepareWaitForNextCompileSlot())
setupTestProject()
})
@@ -40,7 +43,9 @@ describe('Project Sharing', function () {
function setupTestProject() {
login('user@example.com')
createProject(projectName)
waitForCompile(() => {
createProject(projectName)
})
// Add chat message
cy.findByRole('button', { name: 'Chat' }).click()
@@ -75,7 +80,6 @@ describe('Project Sharing', function () {
function expectContentWriteAccess() {
const section = `Test Section ${uuid()}`
cy.url().should('match', /\/project\/[a-fA-F0-9]{24}/)
const recompile = throttledRecompile()
// wait for the editor to finish loading
cy.findByRole('textbox', { name: 'Source Editor editing' }).should(
'contain.text',
@@ -87,8 +91,10 @@ describe('Project Sharing', function () {
'contenteditable',
'true'
)
cy.findByText('\\maketitle').parent().click()
cy.findByText('\\maketitle').parent().type(`\n\\section{{}${section}}`)
cy.findByRole('textbox', { name: 'Source Editor editing' }).within(() => {
cy.findByText('\\maketitle').parent().click()
cy.findByText('\\maketitle').parent().type(`\n\\section{{}${section}}`)
})
// should have written
cy.findByRole('textbox', { name: 'Source Editor editing' }).should(
'contain.text',
@@ -128,13 +134,28 @@ describe('Project Sharing', function () {
}
function expectHistoryAccess() {
cy.findByText('History').click()
cy.findByText('Labels')
cy.findByRole('button', { name: 'History' }).click()
// The input is not clickable due to being visually hidden, click its label instead
cy.findByRole('complementary', {
name: 'Project history and labels',
}).within(() => {
cy.findByRole('group', {
name: 'Show all of the project history or only labelled versions.',
}).within(() => {
cy.findByText('All history').click()
})
cy.findByRole('radio', { name: 'Labels' }).should('not.be.checked')
cy.findByRole('radio', { name: 'All history' }).should('be.checked')
})
cy.findByText(/\\begin\{document}/)
cy.findAllByTestId('history-version-metadata-users')
.last()
.should('have.text', 'user')
cy.findByText('Back to editor').click()
cy.findByRole('complementary', {
name: 'Project history and labels',
}).within(() => {
cy.findAllByTestId('history-version-metadata-users')
.last()
.should('have.text', 'user')
})
cy.findByRole('button', { name: 'Back to editor' }).click()
}
function expectNoChatAccess() {

View File

@@ -1,10 +1,9 @@
import { ensureUserExists, login } from './helpers/login'
import { createProject } from './helpers/project'
import { isExcludedBySharding, startWith } from './helpers/config'
import { throttledRecompile, stopCompile } from './helpers/compile'
import { prepareWaitForNextCompileSlot, stopCompile } from './helpers/compile'
import { v4 as uuid } from 'uuid'
import { waitUntilScrollingFinished } from './helpers/waitUntilScrollingFinished'
import { beforeWithReRunOnTestRetry } from './helpers/beforeWithReRunOnTestRetry'
const LABEL_TEX_LIVE_VERSION = 'TeX Live version'
@@ -27,8 +26,10 @@ describe('SandboxedCompiles', function () {
})
it('should offer TexLive images and switch the compiler', function () {
createProject('sandboxed')
const recompile = throttledRecompile()
const { recompile, waitForCompile } = prepareWaitForNextCompileSlot()
waitForCompile(() => {
createProject('sandboxed')
})
cy.log('wait for compile')
cy.findByRole('region', { name: 'PDF preview and logs' }).should(
'contain.text',
@@ -53,7 +54,6 @@ describe('SandboxedCompiles', function () {
})
cy.get('body').type('{esc}')
cy.findByRole('dialog').should('not.exist')
cy.log('Trigger compile with other TeX Live version')
recompile()
@@ -71,13 +71,18 @@ describe('SandboxedCompiles', function () {
function checkStopCompile() {
it('users can stop a running compile', function () {
login('user@example.com')
createProject('test-project')
const { recompile, waitForCompile, waitForCompileRateLimitCoolOff } =
prepareWaitForNextCompileSlot()
waitForCompile(() => {
createProject('test-project')
})
// create an infinite loop in the main document
// this will cause the compile to run indefinitely
cy.findByText('\\maketitle').parent().click()
cy.findByText('\\maketitle')
.parent()
.type('\n\\def\\x{{}Hello!\\par\\x}\\x')
waitForCompileRateLimitCoolOff()
cy.log('Start compile')
// We need to start the compile manually because we do not want to wait for it to finish
cy.findByText('Recompile').click()
@@ -90,7 +95,7 @@ describe('SandboxedCompiles', function () {
// disabling the infinite loop and recompiling
cy.findByText('\\def').parent().click()
cy.findByText('\\def').parent().type('{home}disabled loop% ')
cy.findByText('Recompile').click()
recompile()
cy.get('.pdf-viewer').should('contain.text', 'disabled loop')
cy.get('.logs-pane').should(
'not.contain.text',
@@ -104,8 +109,10 @@ describe('SandboxedCompiles', function () {
let projectName: string
beforeEach(function () {
projectName = `Project ${uuid()}`
createProject(projectName)
const recompile = throttledRecompile()
const { recompile, waitForCompile } = prepareWaitForNextCompileSlot()
waitForCompile(() => {
createProject(projectName)
})
cy.findByRole('textbox', { name: 'Source Editor editing' }).within(
() => {
cy.findByText('\\maketitle').parent().click()
@@ -205,8 +212,10 @@ describe('SandboxedCompiles', function () {
function checkRecompilesAfterErrors() {
it('recompiles even if there are Latex errors', function () {
login('user@example.com')
createProject('test-project')
const recompile = throttledRecompile()
const { recompile, waitForCompile } = prepareWaitForNextCompileSlot()
waitForCompile(() => {
createProject('test-project')
})
cy.findByRole('textbox', { name: 'Source Editor editing' }).within(() => {
cy.findByText('\\maketitle').parent().click()
cy.findByText('\\maketitle')
@@ -224,8 +233,10 @@ describe('SandboxedCompiles', function () {
function checkXeTeX() {
it('should be able to use XeLaTeX', function () {
createProject('XeLaTeX')
const recompile = throttledRecompile()
const { recompile, waitForCompile } = prepareWaitForNextCompileSlot()
waitForCompile(() => {
createProject('XeLaTeX')
})
cy.log('wait for compile')
cy.findByRole('region', { name: 'PDF preview and logs' }).should(
'contain.text',

View File

@@ -2,7 +2,7 @@ import { ensureUserExists, login } from './helpers/login'
import { isExcludedBySharding, startWith } from './helpers/config'
import { dockerCompose, runScript } from './helpers/hostAdminClient'
import { createProject, openProjectByName } from './helpers/project'
import { throttledRecompile } from './helpers/compile'
import { prepareWaitForNextCompileSlot } from './helpers/compile'
import { v4 as uuid } from 'uuid'
const USER = 'user@example.com'
@@ -11,6 +11,9 @@ const PROJECT_NAME = 'Old Project'
describe('Upgrading', function () {
if (isExcludedBySharding('PRO_CUSTOM_3')) return
let recompile: () => void
let waitForCompile: (triggerCompile: () => void) => void
function testUpgrade(
steps: {
version: string
@@ -38,10 +41,13 @@ describe('Upgrading', function () {
before(() => {
cy.log('Populate old instance')
login(USER)
createProject(PROJECT_NAME, {
newProjectButtonMatcher: startOptions.newProjectButtonMatcher,
;({ recompile, waitForCompile } = prepareWaitForNextCompileSlot())
waitForCompile(() => {
createProject(PROJECT_NAME, {
newProjectButtonMatcher: startOptions.newProjectButtonMatcher,
})
})
const recompile = throttledRecompile()
cy.log('Wait for successful compile')
cy.get('.pdf-viewer').should('contain.text', PROJECT_NAME)
@@ -117,7 +123,9 @@ describe('Upgrading', function () {
})
it('should open the old project', () => {
openProjectByName(PROJECT_NAME)
waitForCompile(() => {
openProjectByName(PROJECT_NAME)
})
cy.url().should('match', /\/project\/[a-fA-F0-9]{24}/)
cy.findByRole('navigation', {
@@ -125,7 +133,6 @@ describe('Upgrading', function () {
}).within(() => {
cy.findByText(PROJECT_NAME)
})
const recompile = throttledRecompile()
cy.log('wait for successful compile')
cy.get('.pdf-viewer').should('contain.text', PROJECT_NAME)
@@ -189,9 +196,9 @@ describe('Upgrading', function () {
hook() {
before(function () {
login(USER)
cy.visit('/')
cy.findByText(PROJECT_NAME).click()
const recompile = throttledRecompile()
waitForCompile(() => {
openProjectByName(PROJECT_NAME)
})
cy.log('Make a change')
cy.findByText('\\maketitle').parent().click()