[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-17 08:05:34 +00:00
committed by Copybot
parent a9a28a13f5
commit a13fca15d3
10 changed files with 337 additions and 242 deletions
@@ -4,7 +4,7 @@ import {
openProjectViaInviteNotification, openProjectViaInviteNotification,
} from './helpers/project' } from './helpers/project'
import { isExcludedBySharding, startWith } from './helpers/config' import { isExcludedBySharding, startWith } from './helpers/config'
import { throttledRecompile } from './helpers/compile' import { prepareWaitForNextCompileSlot } from './helpers/compile'
const USER = 'user@example.com' const USER = 'user@example.com'
const COLLABORATOR = 'collaborator@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 () { it('users can create project and compile it', function () {
login(USER) login(USER)
createProject('test-project') const { recompile, waitForCompile } = prepareWaitForNextCompileSlot()
const recompile = throttledRecompile() waitForCompile(() => {
createProject('test-project')
})
cy.findByRole('textbox', { name: 'Source Editor editing' }).within(() => { cy.findByRole('textbox', { name: 'Source Editor editing' }).within(() => {
cy.findByText('\\maketitle').parent().click() cy.findByText('\\maketitle').parent().click()
cy.findByText('\\maketitle').parent().type('\n\\section{{}Test Section}') 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' }) cy.findByRole('navigation', { name: 'Project files and outline' })
.findByRole('button', { name: 'New file' }) .findByRole('button', { name: 'New file' })
.click() .click()
cy.findByRole('dialog').within(() => { cy.findByRole('dialog').within(() => {
cy.findByRole('button', { name: 'From another project' }).click() cy.findByRole('button', { name: 'From another project' }).click()
cy.findByLabelText('Select a Project').select(sourceProjectName) cy.findByLabelText('Select a Project').select(sourceProjectName)
+3 -4
View File
@@ -22,20 +22,19 @@ describe('editor', () => {
let projectName: string let projectName: string
let projectId: string let projectId: string
let recompile: () => void let recompile: () => void
let waitForCompileRateLimitCoolOff: (fn: () => void) => void let waitForCompile: (fn: () => void) => void
beforeWithReRunOnTestRetry(function () { beforeWithReRunOnTestRetry(function () {
projectName = `project-${uuid()}` projectName = `project-${uuid()}`
login(USER) login(USER)
createProject(projectName, { type: 'Example project', open: false }).then( createProject(projectName, { type: 'Example project', open: false }).then(
id => (projectId = id) id => (projectId = id)
) )
;({ recompile, waitForCompileRateLimitCoolOff } = ;({ recompile, waitForCompile } = prepareWaitForNextCompileSlot())
prepareWaitForNextCompileSlot())
}) })
beforeEach(() => { beforeEach(() => {
login(USER) login(USER)
waitForCompileRateLimitCoolOff(() => { waitForCompile(() => {
openProjectById(projectId) openProjectById(projectId)
}) })
}) })
+6 -7
View File
@@ -39,7 +39,7 @@ describe('filestore migration', function () {
const projectName = `project-${uuid()}` const projectName = `project-${uuid()}`
let defaultImage: string let defaultImage: string
let projectId: string let projectId: string
let waitForCompileRateLimitCoolOff: (fn: () => void) => void let waitForCompile: (fn: () => void) => void
const previousBinaryFiles: (() => void)[] = [] const previousBinaryFiles: (() => void)[] = []
function avoid502() { function avoid502() {
@@ -55,7 +55,7 @@ describe('filestore migration', function () {
) { ) {
before(function () { before(function () {
login(email) login(email)
waitForCompileRateLimitCoolOff(() => { waitForCompile(() => {
cy.visit(`/project/${projectId}`) cy.visit(`/project/${projectId}`)
}) })
previousBinaryFiles.push(prepareFileUploadTest(true)) previousBinaryFiles.push(prepareFileUploadTest(true))
@@ -112,8 +112,7 @@ describe('filestore migration', function () {
.should('match', /\/project\/[a-fA-F0-9]{24}/) .should('match', /\/project\/[a-fA-F0-9]{24}/)
.then(url => (projectId = url.split('/').pop()!)) .then(url => (projectId = url.split('/').pop()!))
let queueReset let queueReset
;({ waitForCompileRateLimitCoolOff, queueReset } = ;({ waitForCompile, queueReset } = prepareWaitForNextCompileSlot())
prepareWaitForNextCompileSlot())
queueReset() queueReset()
// Create a new binary file // Create a new binary file
@@ -265,7 +264,7 @@ describe('filestore migration', function () {
createProject(projectName, { type: 'Example project', open: false }).then( createProject(projectName, { type: 'Example project', open: false }).then(
id => (projectId = id) id => (projectId = id)
) )
;({ waitForCompileRateLimitCoolOff } = prepareWaitForNextCompileSlot()) ;({ waitForCompile } = prepareWaitForNextCompileSlot())
}) })
} }
addNewBinaryFileAndCheckPrevious() addNewBinaryFileAndCheckPrevious()
@@ -298,7 +297,7 @@ describe('filestore migration', function () {
// filestore-migration // filestore-migration
beforeEach(() => { beforeEach(() => {
login(email) login(email)
waitForCompileRateLimitCoolOff(() => { waitForCompile(() => {
openProjectById(projectId) openProjectById(projectId)
}) })
ensureStopOnFirstErrorIsActive() ensureStopOnFirstErrorIsActive()
@@ -327,7 +326,7 @@ describe('filestore migration', function () {
.parent() .parent()
.type(`\n\\section{{}Test Section ${id}}`) .type(`\n\\section{{}Test Section ${id}}`)
waitForCompileRateLimitCoolOff(() => { waitForCompile(() => {
cy.findByRole('button', { name: 'Toggle compile options menu' }).click() cy.findByRole('button', { name: 'Toggle compile options menu' }).click()
cy.findByRole('menuitem', { cy.findByRole('menuitem', {
+211 -179
View File
@@ -12,7 +12,7 @@ import {
import git from 'isomorphic-git' import git from 'isomorphic-git'
import http from 'isomorphic-git/http/web' import http from 'isomorphic-git/http/web'
import LightningFS from '@isomorphic-git/lightning-fs' import LightningFS from '@isomorphic-git/lightning-fs'
import { throttledRecompile } from './helpers/compile' import { prepareWaitForNextCompileSlot } from './helpers/compile'
const USER = 'user@example.com' const USER = 'user@example.com'
@@ -52,13 +52,13 @@ describe('git-bridge', function () {
function maybeClearAllTokens() { function maybeClearAllTokens() {
cy.visit('/user/settings') cy.visit('/user/settings')
cy.findByRole('heading', { name: 'Git integration' }) cy.findByRole('heading', { name: 'Git integration' })
cy.get('button') cy.findByRole('button', {
.contains(/Generate token|Add another token/) name: /Generate token|Add another token/i,
.then(btn => { }).then(btn => {
if (btn.text() === 'Add another token') { if (btn.text() === 'Add another token') {
clearAllTokens() clearAllTokens()
} }
}) })
} }
beforeEach(function () { beforeEach(function () {
@@ -130,9 +130,12 @@ describe('git-bridge', function () {
cy.findByTestId('left-menu').within(() => { cy.findByTestId('left-menu').within(() => {
cy.findByRole('button', { name: 'Git' }).click() cy.findByRole('button', { name: 'Git' }).click()
}) })
cy.findByTestId('git-bridge-modal').within(() => { cy.findByTestId('git-bridge-modal').within(() => {
cy.get('@projectId').then(id => { 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', { cy.findByRole('button', {
name: 'Generate token', name: 'Generate token',
@@ -151,14 +154,19 @@ describe('git-bridge', function () {
ensureUserExists({ email: 'collaborator-link-ro@example.com' }) ensureUserExists({ email: 'collaborator-link-ro@example.com' })
let projectName: string let projectName: string
let recompile: () => void
let waitForCompile: (triggerCompile: () => void) => void
beforeEach(() => { beforeEach(() => {
projectName = uuid() projectName = uuid()
createProject(projectName, { open: false }).as('projectId') createProject(projectName, { open: false }).as('projectId')
;({ recompile, waitForCompile } = prepareWaitForNextCompileSlot())
}) })
it('should expose r/w interface to owner', () => { it('should expose r/w interface to owner', () => {
maybeClearAllTokens() maybeClearAllTokens()
openProjectByName(projectName) waitForCompile(() => {
openProjectByName(projectName)
})
checkGitAccess('readAndWrite') checkGitAccess('readAndWrite')
}) })
@@ -169,7 +177,9 @@ describe('git-bridge', function () {
'Editor' 'Editor'
) )
maybeClearAllTokens() maybeClearAllTokens()
openProjectByName(projectName) waitForCompile(() => {
openProjectByName(projectName)
})
checkGitAccess('readAndWrite') checkGitAccess('readAndWrite')
}) })
@@ -180,7 +190,9 @@ describe('git-bridge', function () {
'Viewer' 'Viewer'
) )
maybeClearAllTokens() maybeClearAllTokens()
openProjectByName(projectName) waitForCompile(() => {
openProjectByName(projectName)
})
checkGitAccess('readOnly') checkGitAccess('readOnly')
}) })
@@ -190,212 +202,232 @@ describe('git-bridge', function () {
const email = 'collaborator-link-rw@example.com' const email = 'collaborator-link-rw@example.com'
login(email) login(email)
maybeClearAllTokens() maybeClearAllTokens()
openProjectViaLinkSharingAsUser( waitForCompile(() => {
linkSharingReadAndWrite, openProjectViaLinkSharingAsUser(
projectName, linkSharingReadAndWrite,
email projectName,
) email
)
})
checkGitAccess('readAndWrite') checkGitAccess('readAndWrite')
}) })
}) })
it('should expose r/o interface to link-sharing r/o collaborator', () => { it('should expose r/o interface to link-sharing r/o collaborator', () => {
openProjectByName(projectName) waitForCompile(() => {
openProjectByName(projectName)
})
enableLinkSharing().then(({ linkSharingReadOnly }) => { enableLinkSharing().then(({ linkSharingReadOnly }) => {
const email = 'collaborator-link-ro@example.com' const email = 'collaborator-link-ro@example.com'
login(email) login(email)
maybeClearAllTokens() maybeClearAllTokens()
openProjectViaLinkSharingAsUser( waitForCompile(() => {
linkSharingReadOnly, openProjectViaLinkSharingAsUser(
projectName, linkSharingReadOnly,
email projectName,
) email
)
})
checkGitAccess('readOnly') checkGitAccess('readOnly')
}) })
}) })
})
function checkGitAccess(access: 'readOnly' | 'readAndWrite') { function checkGitAccess(access: 'readOnly' | 'readAndWrite') {
const recompile = throttledRecompile() cy.findByRole('navigation', {
name: 'Project actions',
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())}`
)
}) })
cy.findByRole('heading', { name: 'Clone with Git' }) .findByRole('button', { name: 'Menu' })
cy.findByRole('button', { .click()
name: 'Generate token', cy.findByTestId('left-menu').within(() => {
}).click() cy.findByRole('heading', { name: 'Sync' })
cy.get('code') cy.findByRole('button', { name: 'Git' }).click()
.contains(/olp_[a-zA-Z0-9]{16}/) })
.then(async tokenEl => { cy.get('@projectId').then(projectId => {
const token = tokenEl.text() 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 // close Git modal
cy.findAllByText('Close').last().click() cy.get('body').type('{esc}')
// close editor menu cy.findByTestId('git-bridge-modal').should('not.exist')
cy.get('.left-menu-modal-backdrop').click() // 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') async function readFile(path: string): Promise<string> {
const dir = `/${projectId}` 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> { async function writeFile(path: string, data: string) {
return new Promise((resolve, reject) => { return new Promise<void>((resolve, reject) => {
fs.readFile(path, { encoding: 'utf8' }, (err, blob) => { fs.writeFile(path, data, undefined, err => {
if (err) return reject(err) if (err) return reject(err)
resolve(blob as string) 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,
}) })
}) })
} cy.findByText(/\\documentclass/)
.parent()
async function writeFile(path: string, data: string) { .parent()
return new Promise<void>((resolve, reject) => { .then(async editor => {
fs.writeFile(path, data, undefined, err => { const onDisk = await readFile(mainTex)
if (err) return reject(err) expect(onDisk.replaceAll('\n', '')).to.equal(editor.text())
resolve()
}) })
}) const 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 = `
\\documentclass{article} \\documentclass{article}
\\begin{document} \\begin{document}
Hello world Hello world
\\end{document} \\end{document}
` `
// Make a change // 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()
cy.then(async () => { cy.then(async () => {
await git.push({ await writeFile(mainTex, text)
await git.add({
...commonOptions, ...commonOptions,
...httpOptions, filepath: 'main.tex',
})
await git.commit({
...commonOptions,
...authorOptions,
message: 'Swap main.tex',
}) })
}) })
} else {
cy.then(async () => { if (access === 'readAndWrite') {
try { // 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({ await git.push({
...commonOptions, ...commonOptions,
...httpOptions, ...httpOptions,
}) })
expect.fail('push should have failed') })
} catch (err) { } else {
expect(err).to.match(/branches were not updated/) cy.then(async () => {
expect(err).to.match(/forbidden/) 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 history
} cy.findByRole('navigation', {
name: 'Project actions',
// 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,
}) })
.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() { function checkDisabled() {
+6 -4
View File
@@ -6,7 +6,7 @@ import {
} from './helpers/config' } from './helpers/config'
import { dockerCompose, getRedisKeys } from './helpers/hostAdminClient' import { dockerCompose, getRedisKeys } from './helpers/hostAdminClient'
import { createProject } from './helpers/project' import { createProject } from './helpers/project'
import { throttledRecompile } from './helpers/compile' import { prepareWaitForNextCompileSlot } from './helpers/compile'
const USER = 'user@example.com' const USER = 'user@example.com'
const PROJECT_NAME = 'Old Project' const PROJECT_NAME = 'Old Project'
@@ -31,10 +31,12 @@ describe('GracefulShutdown', function () {
it('should display banner and flush changes out of redis', () => { it('should display banner and flush changes out of redis', () => {
bringServerProBackUp() bringServerProBackUp()
login(USER) login(USER)
createProject(PROJECT_NAME).then(id => { const { recompile, waitForCompile } = prepareWaitForNextCompileSlot()
projectId = id waitForCompile(() => {
createProject(PROJECT_NAME).then(id => {
projectId = id
})
}) })
const recompile = throttledRecompile()
cy.log('add additional content') cy.log('add additional content')
cy.findByText('\\maketitle').parent().click() cy.findByText('\\maketitle').parent().click()
+29 -8
View File
@@ -3,11 +3,6 @@
* The naive approach is waiting a fixed a mount of time (3s) just before clicking the button. * 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. * 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 } = {}) { export function stopCompile(options: { delay?: number } = {}) {
const { delay = 0 } = options const { delay = 0 } = options
@@ -24,25 +19,51 @@ export function prepareWaitForNextCompileSlot() {
lastCompile = Date.now() lastCompile = Date.now()
}) })
} }
function waitForCompileRateLimitCoolOff(triggerCompile: () => void) { function waitForCompileRateLimitCoolOff() {
cy.then(() => { cy.then(() => {
cy.log('Wait for recompile rate-limit to cool off') cy.log('Wait for recompile rate-limit to cool off')
const msSinceLastCompile = Date.now() - lastCompile const msSinceLastCompile = Date.now() - lastCompile
cy.wait(Math.max(0, 1_000 - msSinceLastCompile)) cy.wait(Math.max(0, 1_000 - msSinceLastCompile))
queueReset() 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() triggerCompile()
cy.log('Wait for compile to finish') 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') cy.findByRole('button', { name: 'Recompile' }).should('be.visible')
}) })
} }
function recompile() { function recompile() {
waitForCompileRateLimitCoolOff(() => { waitForCompile(() => {
cy.findByText('Recompile').click() cy.findByRole('button', { name: 'Recompile' }).click()
}) })
} }
return { return {
queueReset, queueReset,
waitForCompileRateLimitCoolOff, waitForCompileRateLimitCoolOff,
waitForCompile,
recompile, recompile,
} }
} }
+5 -3
View File
@@ -1,5 +1,5 @@
import { createProject } from './helpers/project' import { createProject } from './helpers/project'
import { throttledRecompile } from './helpers/compile' import { prepareWaitForNextCompileSlot } from './helpers/compile'
import { ensureUserExists, login } from './helpers/login' import { ensureUserExists, login } from './helpers/login'
import { isExcludedBySharding, startWith } from './helpers/config' import { isExcludedBySharding, startWith } from './helpers/config'
@@ -62,8 +62,10 @@ describe('History', function () {
const CLASS_DELETION = 'ol-cm-deletion-marker' const CLASS_DELETION = 'ol-cm-deletion-marker'
it('should support labels, comparison and download', () => { it('should support labels, comparison and download', () => {
createProject('labels') const { recompile, waitForCompile } = prepareWaitForNextCompileSlot()
const recompile = throttledRecompile() waitForCompile(() => {
createProject('labels')
})
cy.log('add content, including a line that will get removed soon') cy.log('add content, including a line that will get removed soon')
cy.findByRole('textbox', { name: 'Source Editor editing' }).within(() => { cy.findByRole('textbox', { name: 'Source Editor editing' }).within(() => {
+32 -11
View File
@@ -16,7 +16,7 @@ import {
shareProjectByEmailAndAcceptInviteViaDash, shareProjectByEmailAndAcceptInviteViaDash,
shareProjectByEmailAndAcceptInviteViaEmail, shareProjectByEmailAndAcceptInviteViaEmail,
} from './helpers/project' } from './helpers/project'
import { throttledRecompile } from './helpers/compile' import { prepareWaitForNextCompileSlot } from './helpers/compile'
import { beforeWithReRunOnTestRetry } from './helpers/beforeWithReRunOnTestRetry' import { beforeWithReRunOnTestRetry } from './helpers/beforeWithReRunOnTestRetry'
describe('Project Sharing', function () { describe('Project Sharing', function () {
@@ -25,8 +25,11 @@ describe('Project Sharing', function () {
startWith({ withDataDir: true, pro: true }) startWith({ withDataDir: true, pro: true })
let projectName: string let projectName: string
let recompile: () => void
let waitForCompile: (triggerCompile: () => void) => void
beforeWithReRunOnTestRetry(function () { beforeWithReRunOnTestRetry(function () {
projectName = getSpamSafeProjectName() projectName = getSpamSafeProjectName()
;({ recompile, waitForCompile } = prepareWaitForNextCompileSlot())
setupTestProject() setupTestProject()
}) })
@@ -40,7 +43,9 @@ describe('Project Sharing', function () {
function setupTestProject() { function setupTestProject() {
login('user@example.com') login('user@example.com')
createProject(projectName) waitForCompile(() => {
createProject(projectName)
})
// Add chat message // Add chat message
cy.findByRole('button', { name: 'Chat' }).click() cy.findByRole('button', { name: 'Chat' }).click()
@@ -75,7 +80,6 @@ describe('Project Sharing', function () {
function expectContentWriteAccess() { function expectContentWriteAccess() {
const section = `Test Section ${uuid()}` const section = `Test Section ${uuid()}`
cy.url().should('match', /\/project\/[a-fA-F0-9]{24}/) cy.url().should('match', /\/project\/[a-fA-F0-9]{24}/)
const recompile = throttledRecompile()
// wait for the editor to finish loading // wait for the editor to finish loading
cy.findByRole('textbox', { name: 'Source Editor editing' }).should( cy.findByRole('textbox', { name: 'Source Editor editing' }).should(
'contain.text', 'contain.text',
@@ -87,8 +91,10 @@ describe('Project Sharing', function () {
'contenteditable', 'contenteditable',
'true' 'true'
) )
cy.findByText('\\maketitle').parent().click() cy.findByRole('textbox', { name: 'Source Editor editing' }).within(() => {
cy.findByText('\\maketitle').parent().type(`\n\\section{{}${section}}`) cy.findByText('\\maketitle').parent().click()
cy.findByText('\\maketitle').parent().type(`\n\\section{{}${section}}`)
})
// should have written // should have written
cy.findByRole('textbox', { name: 'Source Editor editing' }).should( cy.findByRole('textbox', { name: 'Source Editor editing' }).should(
'contain.text', 'contain.text',
@@ -128,13 +134,28 @@ describe('Project Sharing', function () {
} }
function expectHistoryAccess() { function expectHistoryAccess() {
cy.findByText('History').click() cy.findByRole('button', { name: 'History' }).click()
cy.findByText('Labels') // 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.findByText(/\\begin\{document}/)
cy.findAllByTestId('history-version-metadata-users') cy.findByRole('complementary', {
.last() name: 'Project history and labels',
.should('have.text', 'user') }).within(() => {
cy.findByText('Back to editor').click() cy.findAllByTestId('history-version-metadata-users')
.last()
.should('have.text', 'user')
})
cy.findByRole('button', { name: 'Back to editor' }).click()
} }
function expectNoChatAccess() { function expectNoChatAccess() {
+24 -13
View File
@@ -1,10 +1,9 @@
import { ensureUserExists, login } from './helpers/login' import { ensureUserExists, login } from './helpers/login'
import { createProject } from './helpers/project' import { createProject } from './helpers/project'
import { isExcludedBySharding, startWith } from './helpers/config' 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 { v4 as uuid } from 'uuid'
import { waitUntilScrollingFinished } from './helpers/waitUntilScrollingFinished' import { waitUntilScrollingFinished } from './helpers/waitUntilScrollingFinished'
import { beforeWithReRunOnTestRetry } from './helpers/beforeWithReRunOnTestRetry'
const LABEL_TEX_LIVE_VERSION = 'TeX Live version' 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 () { it('should offer TexLive images and switch the compiler', function () {
createProject('sandboxed') const { recompile, waitForCompile } = prepareWaitForNextCompileSlot()
const recompile = throttledRecompile() waitForCompile(() => {
createProject('sandboxed')
})
cy.log('wait for compile') cy.log('wait for compile')
cy.findByRole('region', { name: 'PDF preview and logs' }).should( cy.findByRole('region', { name: 'PDF preview and logs' }).should(
'contain.text', 'contain.text',
@@ -53,7 +54,6 @@ describe('SandboxedCompiles', function () {
}) })
cy.get('body').type('{esc}') cy.get('body').type('{esc}')
cy.findByRole('dialog').should('not.exist') cy.findByRole('dialog').should('not.exist')
cy.log('Trigger compile with other TeX Live version') cy.log('Trigger compile with other TeX Live version')
recompile() recompile()
@@ -71,13 +71,18 @@ describe('SandboxedCompiles', function () {
function checkStopCompile() { function checkStopCompile() {
it('users can stop a running compile', function () { it('users can stop a running compile', function () {
login('user@example.com') login('user@example.com')
createProject('test-project') const { recompile, waitForCompile, waitForCompileRateLimitCoolOff } =
prepareWaitForNextCompileSlot()
waitForCompile(() => {
createProject('test-project')
})
// create an infinite loop in the main document // create an infinite loop in the main document
// this will cause the compile to run indefinitely // this will cause the compile to run indefinitely
cy.findByText('\\maketitle').parent().click() cy.findByText('\\maketitle').parent().click()
cy.findByText('\\maketitle') cy.findByText('\\maketitle')
.parent() .parent()
.type('\n\\def\\x{{}Hello!\\par\\x}\\x') .type('\n\\def\\x{{}Hello!\\par\\x}\\x')
waitForCompileRateLimitCoolOff()
cy.log('Start compile') cy.log('Start compile')
// We need to start the compile manually because we do not want to wait for it to finish // We need to start the compile manually because we do not want to wait for it to finish
cy.findByText('Recompile').click() cy.findByText('Recompile').click()
@@ -90,7 +95,7 @@ describe('SandboxedCompiles', function () {
// disabling the infinite loop and recompiling // disabling the infinite loop and recompiling
cy.findByText('\\def').parent().click() cy.findByText('\\def').parent().click()
cy.findByText('\\def').parent().type('{home}disabled loop% ') cy.findByText('\\def').parent().type('{home}disabled loop% ')
cy.findByText('Recompile').click() recompile()
cy.get('.pdf-viewer').should('contain.text', 'disabled loop') cy.get('.pdf-viewer').should('contain.text', 'disabled loop')
cy.get('.logs-pane').should( cy.get('.logs-pane').should(
'not.contain.text', 'not.contain.text',
@@ -104,8 +109,10 @@ describe('SandboxedCompiles', function () {
let projectName: string let projectName: string
beforeEach(function () { beforeEach(function () {
projectName = `Project ${uuid()}` projectName = `Project ${uuid()}`
createProject(projectName) const { recompile, waitForCompile } = prepareWaitForNextCompileSlot()
const recompile = throttledRecompile() waitForCompile(() => {
createProject(projectName)
})
cy.findByRole('textbox', { name: 'Source Editor editing' }).within( cy.findByRole('textbox', { name: 'Source Editor editing' }).within(
() => { () => {
cy.findByText('\\maketitle').parent().click() cy.findByText('\\maketitle').parent().click()
@@ -205,8 +212,10 @@ describe('SandboxedCompiles', function () {
function checkRecompilesAfterErrors() { function checkRecompilesAfterErrors() {
it('recompiles even if there are Latex errors', function () { it('recompiles even if there are Latex errors', function () {
login('user@example.com') login('user@example.com')
createProject('test-project') const { recompile, waitForCompile } = prepareWaitForNextCompileSlot()
const recompile = throttledRecompile() waitForCompile(() => {
createProject('test-project')
})
cy.findByRole('textbox', { name: 'Source Editor editing' }).within(() => { cy.findByRole('textbox', { name: 'Source Editor editing' }).within(() => {
cy.findByText('\\maketitle').parent().click() cy.findByText('\\maketitle').parent().click()
cy.findByText('\\maketitle') cy.findByText('\\maketitle')
@@ -224,8 +233,10 @@ describe('SandboxedCompiles', function () {
function checkXeTeX() { function checkXeTeX() {
it('should be able to use XeLaTeX', function () { it('should be able to use XeLaTeX', function () {
createProject('XeLaTeX') const { recompile, waitForCompile } = prepareWaitForNextCompileSlot()
const recompile = throttledRecompile() waitForCompile(() => {
createProject('XeLaTeX')
})
cy.log('wait for compile') cy.log('wait for compile')
cy.findByRole('region', { name: 'PDF preview and logs' }).should( cy.findByRole('region', { name: 'PDF preview and logs' }).should(
'contain.text', 'contain.text',
+16 -9
View File
@@ -2,7 +2,7 @@ import { ensureUserExists, login } from './helpers/login'
import { isExcludedBySharding, startWith } from './helpers/config' import { isExcludedBySharding, startWith } from './helpers/config'
import { dockerCompose, runScript } from './helpers/hostAdminClient' import { dockerCompose, runScript } from './helpers/hostAdminClient'
import { createProject, openProjectByName } from './helpers/project' import { createProject, openProjectByName } from './helpers/project'
import { throttledRecompile } from './helpers/compile' import { prepareWaitForNextCompileSlot } from './helpers/compile'
import { v4 as uuid } from 'uuid' import { v4 as uuid } from 'uuid'
const USER = 'user@example.com' const USER = 'user@example.com'
@@ -11,6 +11,9 @@ const PROJECT_NAME = 'Old Project'
describe('Upgrading', function () { describe('Upgrading', function () {
if (isExcludedBySharding('PRO_CUSTOM_3')) return if (isExcludedBySharding('PRO_CUSTOM_3')) return
let recompile: () => void
let waitForCompile: (triggerCompile: () => void) => void
function testUpgrade( function testUpgrade(
steps: { steps: {
version: string version: string
@@ -38,10 +41,13 @@ describe('Upgrading', function () {
before(() => { before(() => {
cy.log('Populate old instance') cy.log('Populate old instance')
login(USER) login(USER)
createProject(PROJECT_NAME, { ;({ recompile, waitForCompile } = prepareWaitForNextCompileSlot())
newProjectButtonMatcher: startOptions.newProjectButtonMatcher, waitForCompile(() => {
createProject(PROJECT_NAME, {
newProjectButtonMatcher: startOptions.newProjectButtonMatcher,
})
}) })
const recompile = throttledRecompile()
cy.log('Wait for successful compile') cy.log('Wait for successful compile')
cy.get('.pdf-viewer').should('contain.text', PROJECT_NAME) cy.get('.pdf-viewer').should('contain.text', PROJECT_NAME)
@@ -117,7 +123,9 @@ describe('Upgrading', function () {
}) })
it('should open the old project', () => { it('should open the old project', () => {
openProjectByName(PROJECT_NAME) waitForCompile(() => {
openProjectByName(PROJECT_NAME)
})
cy.url().should('match', /\/project\/[a-fA-F0-9]{24}/) cy.url().should('match', /\/project\/[a-fA-F0-9]{24}/)
cy.findByRole('navigation', { cy.findByRole('navigation', {
@@ -125,7 +133,6 @@ describe('Upgrading', function () {
}).within(() => { }).within(() => {
cy.findByText(PROJECT_NAME) cy.findByText(PROJECT_NAME)
}) })
const recompile = throttledRecompile()
cy.log('wait for successful compile') cy.log('wait for successful compile')
cy.get('.pdf-viewer').should('contain.text', PROJECT_NAME) cy.get('.pdf-viewer').should('contain.text', PROJECT_NAME)
@@ -189,9 +196,9 @@ describe('Upgrading', function () {
hook() { hook() {
before(function () { before(function () {
login(USER) login(USER)
cy.visit('/') waitForCompile(() => {
cy.findByText(PROJECT_NAME).click() openProjectByName(PROJECT_NAME)
const recompile = throttledRecompile() })
cy.log('Make a change') cy.log('Make a change')
cy.findByText('\\maketitle').parent().click() cy.findByText('\\maketitle').parent().click()