mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-23 09:09:36 +02:00
Merge pull request #30910 from overleaf/mj-tear-down-old-e2e
[e2e] Remove old editor E2E tests GitOrigin-RevId: 960c5cd5f17c2a5bae225ecb53fa0eed76938939
This commit is contained in:
committed by
Copybot
parent
9223515705
commit
9e0de43638
@@ -1,150 +0,0 @@
|
|||||||
import { ensureUserExists, login } from './helpers/login'
|
|
||||||
import {
|
|
||||||
createProject,
|
|
||||||
openProjectViaInviteNotification,
|
|
||||||
} from './helpers/project'
|
|
||||||
import { isExcludedBySharding, startWith } from './helpers/config'
|
|
||||||
import { prepareWaitForNextCompileSlot } from './helpers/compile'
|
|
||||||
|
|
||||||
const USER = 'user@example.com'
|
|
||||||
const COLLABORATOR = 'collaborator@example.com'
|
|
||||||
|
|
||||||
describe('Project creation and compilation', function () {
|
|
||||||
if (isExcludedBySharding('CE_DEFAULT')) return
|
|
||||||
startWith({})
|
|
||||||
ensureUserExists({ email: USER })
|
|
||||||
ensureUserExists({ email: COLLABORATOR })
|
|
||||||
|
|
||||||
it('users can create project and compile it', function () {
|
|
||||||
login(USER)
|
|
||||||
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}')
|
|
||||||
})
|
|
||||||
recompile()
|
|
||||||
cy.findByRole('region', { name: 'PDF preview and logs' }).within(() => {
|
|
||||||
cy.findByLabelText(/Page.*1/i).should('be.visible')
|
|
||||||
cy.findByText('Test Section').should('be.visible')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('create and edit markdown file', function () {
|
|
||||||
const fileName = `test-${Date.now()}.md`
|
|
||||||
const markdownContent = '# Markdown title'
|
|
||||||
login(USER)
|
|
||||||
createProject('test-project')
|
|
||||||
|
|
||||||
cy.findByRole('navigation', { name: 'Project files and outline' })
|
|
||||||
.findByRole('button', { name: 'New file' })
|
|
||||||
.click()
|
|
||||||
cy.findByRole('dialog').within(() => {
|
|
||||||
cy.findByLabelText('File Name').as('filename').clear()
|
|
||||||
cy.get('@filename').type(fileName)
|
|
||||||
cy.findByRole('button', { name: 'Create' }).click()
|
|
||||||
})
|
|
||||||
cy.findByRole('button', { name: fileName }).click()
|
|
||||||
// wait until we've switched to the newly created empty file
|
|
||||||
cy.findByRole('textbox', { name: 'Source Editor editing' }).should(
|
|
||||||
'have.length',
|
|
||||||
1
|
|
||||||
)
|
|
||||||
cy.findByRole('textbox', { name: 'Source Editor editing' }).type(
|
|
||||||
markdownContent
|
|
||||||
)
|
|
||||||
cy.findByRole('button', { name: 'main.tex' }).click()
|
|
||||||
cy.findByRole('textbox', { name: 'Source Editor editing' }).should(
|
|
||||||
'contain.text',
|
|
||||||
'\\maketitle'
|
|
||||||
)
|
|
||||||
cy.findByRole('button', { name: fileName }).click()
|
|
||||||
cy.findByRole('textbox', { name: 'Source Editor editing' }).should(
|
|
||||||
'contain.text',
|
|
||||||
markdownContent
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('can link and display linked image from other project', function () {
|
|
||||||
const sourceProjectName = `test-project-${Date.now()}`
|
|
||||||
const targetProjectName = `${sourceProjectName}-target`
|
|
||||||
login(USER)
|
|
||||||
|
|
||||||
createProject(sourceProjectName, {
|
|
||||||
type: 'Example project',
|
|
||||||
open: false,
|
|
||||||
}).as('sourceProjectId')
|
|
||||||
createProject(targetProjectName)
|
|
||||||
|
|
||||||
// link the image from `projectName` into this project
|
|
||||||
cy.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)
|
|
||||||
cy.findByLabelText('Select a File').select('frog.jpg')
|
|
||||||
cy.findByRole('button', { name: 'Create' }).click()
|
|
||||||
})
|
|
||||||
cy.findByRole('navigation', { name: 'Project files and outline' })
|
|
||||||
.findByRole('treeitem', { name: 'frog.jpg' })
|
|
||||||
.click()
|
|
||||||
cy.findByRole('link', { name: 'Another project' })
|
|
||||||
.should('have.attr', 'href')
|
|
||||||
.then(href => {
|
|
||||||
cy.get('@sourceProjectId').then(sourceProjectId => {
|
|
||||||
expect(href).to.equal(`/project/${sourceProjectId}`)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('can refresh linked files as collaborator', function () {
|
|
||||||
const sourceProjectName = `test-project-${Date.now()}`
|
|
||||||
const targetProjectName = `${sourceProjectName}-target`
|
|
||||||
login(USER)
|
|
||||||
createProject(sourceProjectName, {
|
|
||||||
type: 'Example project',
|
|
||||||
open: false,
|
|
||||||
}).as('sourceProjectId')
|
|
||||||
createProject(targetProjectName).as('targetProjectId')
|
|
||||||
|
|
||||||
// link the image from `projectName` into this project
|
|
||||||
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)
|
|
||||||
cy.findByLabelText('Select a File').select('frog.jpg')
|
|
||||||
cy.findByRole('button', { name: 'Create' }).click()
|
|
||||||
})
|
|
||||||
|
|
||||||
cy.findByRole('navigation', { name: 'Project actions' }).within(() => {
|
|
||||||
cy.findByRole('button', { name: 'Share' }).click()
|
|
||||||
})
|
|
||||||
cy.findByRole('dialog').within(() => {
|
|
||||||
cy.findByRole('combobox', { name: 'Add email address' }).type(
|
|
||||||
COLLABORATOR + ','
|
|
||||||
)
|
|
||||||
cy.findByRole('button', { name: 'Invite' }).click()
|
|
||||||
cy.findByText('Invite not yet accepted.')
|
|
||||||
})
|
|
||||||
|
|
||||||
login(COLLABORATOR)
|
|
||||||
openProjectViaInviteNotification(targetProjectName)
|
|
||||||
cy.get('@targetProjectId').then(targetProjectId => {
|
|
||||||
cy.url().should('include', targetProjectId)
|
|
||||||
})
|
|
||||||
|
|
||||||
cy.findByRole('navigation', { name: 'Project files and outline' })
|
|
||||||
.findByRole('treeitem', { name: 'frog.jpg' })
|
|
||||||
.click()
|
|
||||||
cy.findByRole('link', { name: 'Another project' })
|
|
||||||
.should('have.attr', 'href')
|
|
||||||
.then(href => {
|
|
||||||
cy.get('@sourceProjectId').then(sourceProjectId => {
|
|
||||||
expect(href).to.equal(`/project/${sourceProjectId}`)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,338 +0,0 @@
|
|||||||
import {
|
|
||||||
createNewFile,
|
|
||||||
createProject,
|
|
||||||
openProjectById,
|
|
||||||
testNewFileUpload,
|
|
||||||
} from './helpers/project'
|
|
||||||
import { isExcludedBySharding, startWith } from './helpers/config'
|
|
||||||
import { ensureUserExists, login } from './helpers/login'
|
|
||||||
import { v4 as uuid } from 'uuid'
|
|
||||||
import { beforeWithReRunOnTestRetry } from './helpers/beforeWithReRunOnTestRetry'
|
|
||||||
import { prepareWaitForNextCompileSlot } from './helpers/compile'
|
|
||||||
|
|
||||||
const USER = 'user@example.com'
|
|
||||||
const COLLABORATOR = 'collaborator@example.com'
|
|
||||||
|
|
||||||
describe('editor', function () {
|
|
||||||
if (isExcludedBySharding('PRO_DEFAULT_1')) return
|
|
||||||
startWith({ pro: true })
|
|
||||||
ensureUserExists({ email: USER })
|
|
||||||
ensureUserExists({ email: COLLABORATOR })
|
|
||||||
|
|
||||||
let projectName: string
|
|
||||||
let projectId: string
|
|
||||||
let recompile: () => void
|
|
||||||
let waitForCompile: (fn: () => void) => void
|
|
||||||
beforeWithReRunOnTestRetry(() => {
|
|
||||||
projectName = `project-${uuid()}`
|
|
||||||
login(USER)
|
|
||||||
createProject(projectName, { type: 'Example project', open: false }).then(
|
|
||||||
id => (projectId = id)
|
|
||||||
)
|
|
||||||
;({ recompile, waitForCompile } = prepareWaitForNextCompileSlot())
|
|
||||||
})
|
|
||||||
|
|
||||||
beforeEach(function () {
|
|
||||||
login(USER)
|
|
||||||
waitForCompile(() => {
|
|
||||||
openProjectById(projectId)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('spelling', function () {
|
|
||||||
function changeSpellCheckLanguageTo(lng: string) {
|
|
||||||
cy.log(`change project language to '${lng}'`)
|
|
||||||
cy.findByRole('navigation', {
|
|
||||||
name: 'Project actions',
|
|
||||||
})
|
|
||||||
.findByRole('button', { name: 'Menu' })
|
|
||||||
.click()
|
|
||||||
|
|
||||||
cy.findByRole('dialog').within(() => {
|
|
||||||
cy.findByLabelText('Spell check').select(lng)
|
|
||||||
})
|
|
||||||
cy.get('body').type('{esc}')
|
|
||||||
}
|
|
||||||
|
|
||||||
afterEach(function () {
|
|
||||||
changeSpellCheckLanguageTo('Off')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('word dictionary and spelling', function () {
|
|
||||||
changeSpellCheckLanguageTo('English (American)')
|
|
||||||
createNewFile()
|
|
||||||
const word = createRandomLetterString()
|
|
||||||
|
|
||||||
cy.log('edit project file')
|
|
||||||
cy.get('.cm-line').type(word)
|
|
||||||
cy.findByText(word).should('have.class', 'ol-cm-spelling-error')
|
|
||||||
|
|
||||||
changeSpellCheckLanguageTo('Off')
|
|
||||||
cy.findByText(word).should('not.have.class', 'ol-cm-spelling-error')
|
|
||||||
|
|
||||||
changeSpellCheckLanguageTo('Spanish')
|
|
||||||
cy.findByText(word).should('have.class', 'ol-cm-spelling-error')
|
|
||||||
|
|
||||||
cy.log('add word to dictionary')
|
|
||||||
cy.findByText(word).rightclick()
|
|
||||||
cy.findByRole('menuitem', { name: 'Add to dictionary' }).click()
|
|
||||||
cy.findByText(word).should('not.have.class', 'ol-cm-spelling-error')
|
|
||||||
|
|
||||||
cy.log('remove word from dictionary')
|
|
||||||
cy.findByRole('navigation', {
|
|
||||||
name: 'Project actions',
|
|
||||||
})
|
|
||||||
.findByRole('button', { name: 'Menu' })
|
|
||||||
.click()
|
|
||||||
cy.findByRole('dialog').within(() => {
|
|
||||||
cy.findByLabelText('Dictionary').click()
|
|
||||||
})
|
|
||||||
cy.findByTestId('dictionary-modal').within(() => {
|
|
||||||
cy.findByText(word)
|
|
||||||
.parent()
|
|
||||||
.within(() =>
|
|
||||||
cy.findByRole('button', { name: 'Remove from dictionary' }).click()
|
|
||||||
)
|
|
||||||
|
|
||||||
// the modal has 2 close buttons, this ensures the one with the visible label is
|
|
||||||
// clicked, otherwise it would need `force: true`
|
|
||||||
cy.contains('button', /close/i).click()
|
|
||||||
})
|
|
||||||
|
|
||||||
cy.log('close left panel')
|
|
||||||
cy.findByTestId('left-menu').type('{esc}')
|
|
||||||
|
|
||||||
cy.log('rewrite word to force spelling error')
|
|
||||||
cy.get('.cm-line').type('{selectAll}{del}' + word + '{enter}')
|
|
||||||
|
|
||||||
cy.get('.ol-cm-spelling-error').should('contain.text', word)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('editor', function () {
|
|
||||||
it('renders jpg', function () {
|
|
||||||
cy.findByRole('navigation', {
|
|
||||||
name: 'Project files and outline',
|
|
||||||
})
|
|
||||||
.findByRole('treeitem', { name: 'frog.jpg' })
|
|
||||||
.click()
|
|
||||||
cy.get('[alt="frog.jpg"]')
|
|
||||||
.should('be.visible')
|
|
||||||
.and('have.prop', 'naturalWidth')
|
|
||||||
.should('be.greaterThan', 0)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('symbol palette', function () {
|
|
||||||
createNewFile()
|
|
||||||
|
|
||||||
cy.get('button[aria-label="Insert symbol"]').click({
|
|
||||||
force: true,
|
|
||||||
})
|
|
||||||
cy.get('button').contains('𝜉').click()
|
|
||||||
cy.findByRole('textbox', { name: 'Source Editor editing' }).should(
|
|
||||||
'contain.text',
|
|
||||||
'\\xi'
|
|
||||||
)
|
|
||||||
|
|
||||||
cy.log('recompile to force flush and avoid "unsaved changes" prompt')
|
|
||||||
recompile()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('add new file to project', function () {
|
|
||||||
beforeEach(function () {
|
|
||||||
cy.findByRole('button', { name: 'New file' }).click()
|
|
||||||
})
|
|
||||||
|
|
||||||
testNewFileUpload()
|
|
||||||
|
|
||||||
it('should not display import from URL', function () {
|
|
||||||
cy.findByRole('button', { name: 'From external URL' }).should('not.exist')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('left menu', function () {
|
|
||||||
beforeEach(function () {
|
|
||||||
cy.findByRole('navigation', {
|
|
||||||
name: 'Project actions',
|
|
||||||
})
|
|
||||||
.findByRole('button', { name: 'Menu' })
|
|
||||||
.click()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('can download project sources', function () {
|
|
||||||
cy.findByRole('link', { name: 'Source' }).click()
|
|
||||||
const zipName = projectName.replaceAll('-', '_')
|
|
||||||
cy.task('readFileInZip', {
|
|
||||||
pathToZip: `cypress/downloads/${zipName}.zip`,
|
|
||||||
fileToRead: 'main.tex',
|
|
||||||
}).should('contain', 'Your introduction goes here')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('can download project PDF', function () {
|
|
||||||
cy.log('ensure project is compiled')
|
|
||||||
cy.findByRole('region', { name: 'PDF preview and logs' }).should(
|
|
||||||
'contain.text',
|
|
||||||
'Your Paper'
|
|
||||||
)
|
|
||||||
cy.findByRole('dialog').within(() => {
|
|
||||||
cy.findByRole('link', { name: 'PDF' }).click()
|
|
||||||
const pdfName = projectName.replaceAll('-', '_')
|
|
||||||
cy.task('readPdf', `cypress/downloads/${pdfName}.pdf`).should(
|
|
||||||
'contain',
|
|
||||||
'Your introduction goes here'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('word count', function () {
|
|
||||||
cy.log('ensure project is compiled')
|
|
||||||
cy.findByRole('region', { name: 'PDF preview and logs' }).should(
|
|
||||||
'contain.text',
|
|
||||||
'Your Paper'
|
|
||||||
)
|
|
||||||
cy.findByRole('button', { name: 'Word Count' }).click()
|
|
||||||
|
|
||||||
cy.findByTestId('word-count-modal').within(() => {
|
|
||||||
cy.findByText('Total Words:')
|
|
||||||
cy.findByText('607')
|
|
||||||
cy.findByText('Headers:')
|
|
||||||
cy.findByText('14')
|
|
||||||
cy.findByText('Math Inline:')
|
|
||||||
cy.findByText('6')
|
|
||||||
cy.findByText('Math Display:')
|
|
||||||
cy.findByText('1')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('cite key search', function () {
|
|
||||||
it('can insert citation from cite key', function () {
|
|
||||||
createNewFile()
|
|
||||||
cy.get('.cm-line').type('\\cite{{}gre')
|
|
||||||
cy.findByRole('listbox').within(() => {
|
|
||||||
cy.findByRole('option').should('contain.text', 'greenwade93').click()
|
|
||||||
})
|
|
||||||
cy.get('.cm-line').should('have.text', '\\cite{greenwade93}')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('updates citation search when bib file is changed', function () {
|
|
||||||
createNewFile()
|
|
||||||
cy.get('.cm-line').type('\\cite{{}new')
|
|
||||||
// Wait a reasonable time to ensure the autocomplete would've appeared if there were any matches
|
|
||||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
|
||||||
cy.wait(200)
|
|
||||||
cy.findByRole('listbox').should('not.exist')
|
|
||||||
cy.findByRole('treeitem', { name: 'sample.bib' }).click()
|
|
||||||
cy.get('.cm-line')
|
|
||||||
.last()
|
|
||||||
.type(
|
|
||||||
'\n@article{{}newkey2024,\n author = {{}Doe, John},\n title = {{}A New Article},\n journal = {{}Journal of Testing},\n year = 2024\n}\n'
|
|
||||||
)
|
|
||||||
createNewFile()
|
|
||||||
cy.get('.cm-line').type('\\cite{{}new')
|
|
||||||
cy.findByRole('listbox').within(() => {
|
|
||||||
cy.findByRole('option').should('contain.text', 'newkey2024').click()
|
|
||||||
})
|
|
||||||
cy.get('.cm-line').should('have.text', '\\cite{newkey2024}')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('layout selector', function () {
|
|
||||||
it('show editor only and switch between editor and pdf', function () {
|
|
||||||
cy.findByRole('region', { name: 'PDF preview and logs' }).should(
|
|
||||||
'be.visible'
|
|
||||||
)
|
|
||||||
cy.get('.cm-editor').should('be.visible')
|
|
||||||
|
|
||||||
cy.findByRole('button', { name: 'Layout' }).click()
|
|
||||||
cy.findByRole('menu').within(() => {
|
|
||||||
cy.findByRole('menuitem', { name: /Editor only/ }).click()
|
|
||||||
})
|
|
||||||
|
|
||||||
cy.findByRole('region', { name: 'PDF preview and logs' }).should(
|
|
||||||
'not.be.visible'
|
|
||||||
)
|
|
||||||
cy.get('.cm-editor').should('be.visible')
|
|
||||||
|
|
||||||
cy.findByRole('button', { name: 'Switch to PDF' }).click()
|
|
||||||
|
|
||||||
cy.findByRole('region', { name: 'PDF preview and logs' }).should(
|
|
||||||
'be.visible'
|
|
||||||
)
|
|
||||||
cy.get('.cm-editor').should('not.be.visible')
|
|
||||||
|
|
||||||
cy.findByRole('button', { name: 'Switch to editor' }).click()
|
|
||||||
|
|
||||||
cy.findByRole('region', { name: 'PDF preview and logs' }).should(
|
|
||||||
'not.be.visible'
|
|
||||||
)
|
|
||||||
cy.get('.cm-editor').should('be.visible')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('show PDF only and go back to Editor & PDF', function () {
|
|
||||||
cy.findByRole('region', { name: 'PDF preview and logs' }).should(
|
|
||||||
'be.visible'
|
|
||||||
)
|
|
||||||
cy.get('.cm-editor').should('be.visible')
|
|
||||||
|
|
||||||
cy.findByRole('button', { name: 'Layout' }).click()
|
|
||||||
cy.findByRole('menu').within(() => {
|
|
||||||
cy.findByRole('menuitem', { name: /PDF only/ }).click()
|
|
||||||
})
|
|
||||||
|
|
||||||
cy.findByRole('region', { name: 'PDF preview and logs' }).should(
|
|
||||||
'be.visible'
|
|
||||||
)
|
|
||||||
cy.get('.cm-editor').should('not.be.visible')
|
|
||||||
|
|
||||||
cy.findByRole('button', { name: 'Layout' }).click()
|
|
||||||
cy.findByRole('menu').within(() => {
|
|
||||||
cy.findByRole('menuitem', { name: 'Editor & PDF' }).click()
|
|
||||||
})
|
|
||||||
|
|
||||||
cy.findByRole('region', { name: 'PDF preview and logs' }).should(
|
|
||||||
'be.visible'
|
|
||||||
)
|
|
||||||
cy.get('.cm-editor').should('be.visible')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('PDF in a separate tab (tests editor only)', function () {
|
|
||||||
cy.findByTestId('pdf-viewer').should('be.visible')
|
|
||||||
cy.get('.cm-editor').should('be.visible')
|
|
||||||
|
|
||||||
cy.findByRole('button', { name: 'Layout' }).click()
|
|
||||||
cy.findByRole('menu').within(() => {
|
|
||||||
cy.findByRole('menuitem', { name: 'PDF in separate tab' }).click()
|
|
||||||
})
|
|
||||||
|
|
||||||
cy.findByTestId('pdf-viewer').should('not.exist')
|
|
||||||
cy.get('.cm-editor').should('be.visible')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('full project search', function () {
|
|
||||||
it('can search for text in project files', function () {
|
|
||||||
cy.get('button').contains('Search').click({ force: true })
|
|
||||||
cy.findByRole('searchbox', { name: 'Search' })
|
|
||||||
.should('be.visible')
|
|
||||||
.type('Some examples to get started')
|
|
||||||
cy.get('button').contains('Search').click()
|
|
||||||
|
|
||||||
cy.findByRole('listbox').within(() => {
|
|
||||||
cy.findByRole('option', {
|
|
||||||
name: /Some examples to get started/,
|
|
||||||
}).should('be.visible')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
function createRandomLetterString() {
|
|
||||||
const chars = 'abcdefghijklmnopqrstuvwxyz'
|
|
||||||
let result = ''
|
|
||||||
for (let i = 0; i < 12; i++) {
|
|
||||||
result += chars.charAt(Math.floor(Math.random() * chars.length))
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
@@ -1,492 +0,0 @@
|
|||||||
import { v4 as uuid } from 'uuid'
|
|
||||||
import { isExcludedBySharding, startWith } from './helpers/config'
|
|
||||||
import { ensureUserExists, login } from './helpers/login'
|
|
||||||
import {
|
|
||||||
createProject,
|
|
||||||
enableLinkSharing,
|
|
||||||
openProjectByName,
|
|
||||||
openProjectViaLinkSharingAsUser,
|
|
||||||
shareProjectByEmailAndAcceptInviteViaDash,
|
|
||||||
} from './helpers/project'
|
|
||||||
|
|
||||||
import git from 'isomorphic-git'
|
|
||||||
import http from 'isomorphic-git/http/web'
|
|
||||||
import LightningFS from '@isomorphic-git/lightning-fs'
|
|
||||||
import { prepareWaitForNextCompileSlot } from './helpers/compile'
|
|
||||||
|
|
||||||
const USER = 'user@example.com'
|
|
||||||
|
|
||||||
describe('git-bridge', function () {
|
|
||||||
const ENABLED_VARS = {
|
|
||||||
GIT_BRIDGE_ENABLED: 'true',
|
|
||||||
GIT_BRIDGE_HOST: 'git-bridge',
|
|
||||||
GIT_BRIDGE_PORT: '8000',
|
|
||||||
V1_HISTORY_URL: 'http://sharelatex:3100/api',
|
|
||||||
}
|
|
||||||
|
|
||||||
function gitURL(projectId: string) {
|
|
||||||
const url = new URL(Cypress.config().baseUrl!)
|
|
||||||
url.username = 'git'
|
|
||||||
url.pathname = `/git/${projectId}`
|
|
||||||
return url
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('enabled in Server Pro', function () {
|
|
||||||
if (isExcludedBySharding('PRO_CUSTOM_1')) return
|
|
||||||
startWith({
|
|
||||||
pro: true,
|
|
||||||
vars: ENABLED_VARS,
|
|
||||||
})
|
|
||||||
ensureUserExists({ email: USER })
|
|
||||||
|
|
||||||
function clearAllTokens() {
|
|
||||||
cy.findAllByRole('button', { name: 'Remove' })
|
|
||||||
.not('[disabled]')
|
|
||||||
.each($button => {
|
|
||||||
cy.wrap($button).click()
|
|
||||||
cy.findByRole('button', { name: 'Delete token' }).click()
|
|
||||||
})
|
|
||||||
cy.findByRole('dialog').should('not.exist')
|
|
||||||
}
|
|
||||||
|
|
||||||
function maybeClearAllTokens() {
|
|
||||||
cy.visit('/user/settings')
|
|
||||||
cy.findByRole('heading', { name: 'Git integration' })
|
|
||||||
cy.findByRole('button', {
|
|
||||||
name: /Generate token|Add another token/i,
|
|
||||||
}).then(btn => {
|
|
||||||
if (btn.text() === 'Add another token') {
|
|
||||||
clearAllTokens()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeEach(function () {
|
|
||||||
login(USER)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should render the git-bridge UI in the settings', function () {
|
|
||||||
maybeClearAllTokens()
|
|
||||||
cy.visit('/user/settings')
|
|
||||||
cy.findByRole('heading', { name: 'Git integration' })
|
|
||||||
cy.findByRole('button', {
|
|
||||||
name: 'Git integration Generate token',
|
|
||||||
}).click()
|
|
||||||
cy.findByRole('dialog').within(() => {
|
|
||||||
cy.findByLabelText('Git authentication token')
|
|
||||||
.contains(/olp_[a-zA-Z0-9]{16}/)
|
|
||||||
.then(el => el.text())
|
|
||||||
.as('newToken')
|
|
||||||
cy.findByRole('button', { name: 'Close dialog' }).click()
|
|
||||||
})
|
|
||||||
cy.get('@newToken').then(token => {
|
|
||||||
// There can be more than one token with the same prefix when retrying
|
|
||||||
cy.findAllByText(
|
|
||||||
`${token.slice(0, 'olp_1234'.length)}${'*'.repeat(12)}`
|
|
||||||
).should('have.length.at.least', 1)
|
|
||||||
})
|
|
||||||
cy.findByRole('button', {
|
|
||||||
name: 'Git integration Generate token',
|
|
||||||
}).should('not.exist')
|
|
||||||
cy.findByRole('button', { name: 'Add another token' }).should('exist')
|
|
||||||
clearAllTokens()
|
|
||||||
cy.findByRole('button', {
|
|
||||||
name: 'Git integration Generate token',
|
|
||||||
}).should('exist')
|
|
||||||
cy.findByRole('button', { name: 'Add another token' }).should('not.exist')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should render the git-bridge UI in the editor', function () {
|
|
||||||
maybeClearAllTokens()
|
|
||||||
createProject('git').as('projectId')
|
|
||||||
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.findByTestId('git-bridge-modal').within(() => {
|
|
||||||
cy.get('@projectId').then(id => {
|
|
||||||
cy.findByLabelText('Git clone project command').contains(
|
|
||||||
`git clone ${gitURL(id.toString())}`
|
|
||||||
)
|
|
||||||
})
|
|
||||||
cy.findByRole('button', {
|
|
||||||
name: 'Generate token',
|
|
||||||
}).click()
|
|
||||||
cy.findByLabelText('Git authentication token').contains(
|
|
||||||
/olp_[a-zA-Z0-9]{16}/
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Re-open
|
|
||||||
cy.url().then(url => cy.visit(url))
|
|
||||||
cy.findByRole('navigation', {
|
|
||||||
name: 'Project actions',
|
|
||||||
})
|
|
||||||
.findByRole('button', { name: 'Menu' })
|
|
||||||
.click()
|
|
||||||
cy.findByTestId('left-menu').within(() => {
|
|
||||||
cy.findByRole('button', { name: 'Git' }).click()
|
|
||||||
})
|
|
||||||
|
|
||||||
cy.findByTestId('git-bridge-modal').within(() => {
|
|
||||||
cy.get('@projectId').then(id => {
|
|
||||||
cy.findByLabelText('Git clone project command').contains(
|
|
||||||
`git clone ${gitURL(id.toString())}`
|
|
||||||
)
|
|
||||||
})
|
|
||||||
cy.findByRole('button', {
|
|
||||||
name: 'Generate token',
|
|
||||||
}).should('not.exist')
|
|
||||||
cy.findByText(/generate a new one in Account settings/)
|
|
||||||
cy.findByRole('link', { name: 'Go to settings' })
|
|
||||||
.should('have.attr', 'target', '_blank')
|
|
||||||
.and('have.attr', 'href', '/user/settings')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('git access', function () {
|
|
||||||
ensureUserExists({ email: 'collaborator-rw@example.com' })
|
|
||||||
ensureUserExists({ email: 'collaborator-ro@example.com' })
|
|
||||||
ensureUserExists({ email: 'collaborator-link-rw@example.com' })
|
|
||||||
ensureUserExists({ email: 'collaborator-link-ro@example.com' })
|
|
||||||
|
|
||||||
let projectName: string
|
|
||||||
let recompile: () => void
|
|
||||||
let waitForCompile: (triggerCompile: () => void) => void
|
|
||||||
beforeEach(function () {
|
|
||||||
projectName = uuid()
|
|
||||||
createProject(projectName, { open: false }).as('projectId')
|
|
||||||
;({ recompile, waitForCompile } = prepareWaitForNextCompileSlot())
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should expose r/w interface to owner', function () {
|
|
||||||
maybeClearAllTokens()
|
|
||||||
waitForCompile(() => {
|
|
||||||
openProjectByName(projectName)
|
|
||||||
})
|
|
||||||
checkGitAccess('readAndWrite')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should expose r/w interface to invited r/w collaborator', function () {
|
|
||||||
shareProjectByEmailAndAcceptInviteViaDash(
|
|
||||||
projectName,
|
|
||||||
'collaborator-rw@example.com',
|
|
||||||
'Editor'
|
|
||||||
)
|
|
||||||
maybeClearAllTokens()
|
|
||||||
waitForCompile(() => {
|
|
||||||
openProjectByName(projectName)
|
|
||||||
})
|
|
||||||
checkGitAccess('readAndWrite')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should expose r/o interface to invited r/o collaborator', function () {
|
|
||||||
shareProjectByEmailAndAcceptInviteViaDash(
|
|
||||||
projectName,
|
|
||||||
'collaborator-ro@example.com',
|
|
||||||
'Viewer'
|
|
||||||
)
|
|
||||||
maybeClearAllTokens()
|
|
||||||
waitForCompile(() => {
|
|
||||||
openProjectByName(projectName)
|
|
||||||
})
|
|
||||||
checkGitAccess('readOnly')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should expose r/w interface to link-sharing r/w collaborator', function () {
|
|
||||||
openProjectByName(projectName)
|
|
||||||
enableLinkSharing().then(({ linkSharingReadAndWrite }) => {
|
|
||||||
const email = 'collaborator-link-rw@example.com'
|
|
||||||
login(email)
|
|
||||||
maybeClearAllTokens()
|
|
||||||
waitForCompile(() => {
|
|
||||||
openProjectViaLinkSharingAsUser(
|
|
||||||
linkSharingReadAndWrite,
|
|
||||||
projectName,
|
|
||||||
email
|
|
||||||
)
|
|
||||||
})
|
|
||||||
checkGitAccess('readAndWrite')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should expose r/o interface to link-sharing r/o collaborator', function () {
|
|
||||||
waitForCompile(() => {
|
|
||||||
openProjectByName(projectName)
|
|
||||||
})
|
|
||||||
enableLinkSharing().then(({ linkSharingReadOnly }) => {
|
|
||||||
const email = 'collaborator-link-ro@example.com'
|
|
||||||
login(email)
|
|
||||||
maybeClearAllTokens()
|
|
||||||
waitForCompile(() => {
|
|
||||||
openProjectViaLinkSharingAsUser(
|
|
||||||
linkSharingReadOnly,
|
|
||||||
projectName,
|
|
||||||
email
|
|
||||||
)
|
|
||||||
})
|
|
||||||
checkGitAccess('readOnly')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
function checkGitAccess(access: 'readOnly' | 'readAndWrite') {
|
|
||||||
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' })
|
|
||||||
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.findByRole('button', { name: 'Close dialog' }).click()
|
|
||||||
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}`
|
|
||||||
|
|
||||||
async function readFile(path: string): Promise<string> {
|
|
||||||
return await 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 await 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,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
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}
|
|
||||||
\\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.findByRole('navigation', {
|
|
||||||
name: 'Project actions',
|
|
||||||
})
|
|
||||||
.findByRole('button', { name: 'History' })
|
|
||||||
.click()
|
|
||||||
cy.findByRole('complementary', {
|
|
||||||
name: 'Project history and labels',
|
|
||||||
}).within(() => {
|
|
||||||
cy.findByText('(via Git)').should('not.exist')
|
|
||||||
})
|
|
||||||
cy.findByRole('navigation', {
|
|
||||||
name: 'Project actions',
|
|
||||||
})
|
|
||||||
.findByRole('button', { name: 'Back to editor' })
|
|
||||||
.click()
|
|
||||||
cy.then(async () => {
|
|
||||||
await git.push({
|
|
||||||
...commonOptions,
|
|
||||||
...httpOptions,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
} 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',
|
|
||||||
}).within(() => {
|
|
||||||
cy.findByRole('button', { name: 'History' }).click()
|
|
||||||
cy.findByRole('button', { name: 'Back to editor' }).click()
|
|
||||||
})
|
|
||||||
|
|
||||||
// check push in history
|
|
||||||
cy.findByRole('navigation', {
|
|
||||||
name: 'Project actions',
|
|
||||||
})
|
|
||||||
.findByRole('button', { name: 'History' })
|
|
||||||
.click()
|
|
||||||
cy.findByText(/Hello world/)
|
|
||||||
cy.findByRole('complementary', {
|
|
||||||
name: 'Project history and labels',
|
|
||||||
}).within(() => {
|
|
||||||
cy.findByText('(via Git)').should('exist')
|
|
||||||
})
|
|
||||||
|
|
||||||
// Back to the editor
|
|
||||||
cy.findByRole('navigation', {
|
|
||||||
name: 'Project actions',
|
|
||||||
})
|
|
||||||
.findByRole('button', { name: 'Back to editor' })
|
|
||||||
.click()
|
|
||||||
cy.findByText(/\\documentclass/)
|
|
||||||
.parent()
|
|
||||||
.parent()
|
|
||||||
.as('documentclass')
|
|
||||||
.click()
|
|
||||||
cy.get('@documentclass').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() {
|
|
||||||
ensureUserExists({ email: USER })
|
|
||||||
|
|
||||||
it('should not render the git-bridge UI in the settings', function () {
|
|
||||||
login(USER)
|
|
||||||
cy.visit('/user/settings')
|
|
||||||
cy.findByRole('heading', { name: 'Git integration' }).should('not.exist')
|
|
||||||
})
|
|
||||||
it('should not render the git-bridge UI in the editor', function () {
|
|
||||||
login(USER)
|
|
||||||
createProject('maybe git')
|
|
||||||
cy.findByRole('navigation', {
|
|
||||||
name: 'Project actions',
|
|
||||||
})
|
|
||||||
.findByRole('button', { name: 'Menu' })
|
|
||||||
.click()
|
|
||||||
cy.findByTestId('left-menu').within(() => {
|
|
||||||
cy.findByRole('button', { name: 'Word Count' }) // wait for lazy loading
|
|
||||||
cy.findByRole('heading', { name: 'Sync' }).should('not.exist')
|
|
||||||
cy.findByRole('button', { name: 'Git' }).should('not.exist')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('disabled in Server Pro', function () {
|
|
||||||
if (isExcludedBySharding('PRO_DEFAULT_1')) return
|
|
||||||
startWith({
|
|
||||||
pro: true,
|
|
||||||
})
|
|
||||||
checkDisabled()
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('unavailable in CE', function () {
|
|
||||||
if (isExcludedBySharding('CE_CUSTOM_1')) return
|
|
||||||
startWith({
|
|
||||||
pro: false,
|
|
||||||
vars: ENABLED_VARS,
|
|
||||||
})
|
|
||||||
checkDisabled()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
import { ensureUserExists, login } from './helpers/login'
|
|
||||||
import {
|
|
||||||
isExcludedBySharding,
|
|
||||||
STARTUP_TIMEOUT,
|
|
||||||
startWith,
|
|
||||||
} from './helpers/config'
|
|
||||||
import { dockerCompose, getRedisKeys } from './helpers/hostAdminClient'
|
|
||||||
import { createProject } from './helpers/project'
|
|
||||||
import { prepareWaitForNextCompileSlot } from './helpers/compile'
|
|
||||||
|
|
||||||
const USER = 'user@example.com'
|
|
||||||
const PROJECT_NAME = 'Old Project'
|
|
||||||
|
|
||||||
function bringServerProBackUp() {
|
|
||||||
cy.log('bring server pro back up')
|
|
||||||
cy.then({ timeout: STARTUP_TIMEOUT }, async () => {
|
|
||||||
await dockerCompose('up', '--detach', '--wait', 'sharelatex')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('GracefulShutdown', function () {
|
|
||||||
if (isExcludedBySharding('PRO_CUSTOM_1')) return
|
|
||||||
startWith({
|
|
||||||
pro: true,
|
|
||||||
withDataDir: true,
|
|
||||||
resetData: true,
|
|
||||||
})
|
|
||||||
ensureUserExists({ email: USER })
|
|
||||||
|
|
||||||
let projectId: string
|
|
||||||
it('should display banner and flush changes out of redis', function () {
|
|
||||||
bringServerProBackUp()
|
|
||||||
login(USER)
|
|
||||||
const { recompile, waitForCompile } = prepareWaitForNextCompileSlot()
|
|
||||||
waitForCompile(() => {
|
|
||||||
createProject(PROJECT_NAME).then(id => {
|
|
||||||
projectId = id
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
cy.log('add additional content')
|
|
||||||
cy.findByRole('region', { name: 'Editor' }).within(() => {
|
|
||||||
cy.findByText('\\maketitle').parent().click()
|
|
||||||
cy.findByText('\\maketitle').parent().type(`\n\\section{{}New Section}`)
|
|
||||||
})
|
|
||||||
recompile()
|
|
||||||
|
|
||||||
cy.log(
|
|
||||||
'check flush from frontend to backend: should include new section in PDF'
|
|
||||||
)
|
|
||||||
cy.findByRole('region', { name: 'PDF preview and logs' }).should(
|
|
||||||
'contain.text',
|
|
||||||
'New Section'
|
|
||||||
)
|
|
||||||
|
|
||||||
cy.log('should have unflushed content in redis before shutdown')
|
|
||||||
cy.then(async () => {
|
|
||||||
const keys = await getRedisKeys()
|
|
||||||
expect(keys).to.contain(`DocsIn:${projectId}`)
|
|
||||||
expect(keys).to.contain(`ProjectHistory:Ops:{${projectId}}`)
|
|
||||||
})
|
|
||||||
|
|
||||||
cy.log('trigger graceful shutdown')
|
|
||||||
let pendingShutdown: Promise<any>
|
|
||||||
cy.then(() => {
|
|
||||||
pendingShutdown = dockerCompose('stop', '--timeout=60', 'sharelatex')
|
|
||||||
})
|
|
||||||
|
|
||||||
cy.log('wait for banner')
|
|
||||||
cy.findByRole('dialog').findByText(/performing maintenance/)
|
|
||||||
cy.log('wait for page reload')
|
|
||||||
cy.findByRole('heading', { name: 'Maintenance' })
|
|
||||||
cy.findByText(/is currently down for maintenance/)
|
|
||||||
|
|
||||||
cy.log('wait for shutdown to complete')
|
|
||||||
cy.then({ timeout: 60 * 1000 }, async () => {
|
|
||||||
await pendingShutdown
|
|
||||||
})
|
|
||||||
|
|
||||||
cy.log('should not have any unflushed content in redis after shutdown')
|
|
||||||
cy.then(async () => {
|
|
||||||
const keys = await getRedisKeys()
|
|
||||||
expect(keys).to.not.contain(`DocsIn:${projectId}`)
|
|
||||||
expect(keys).to.not.contain(`ProjectHistory:Ops:{${projectId}}`)
|
|
||||||
})
|
|
||||||
|
|
||||||
bringServerProBackUp()
|
|
||||||
|
|
||||||
cy.then(() => {
|
|
||||||
cy.visit(
|
|
||||||
`/project/${projectId}?trick-cypress-into-page-reload=true&old-editor-override=true`
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
cy.log('check loading doc from mongo')
|
|
||||||
cy.findByRole('region', { name: 'Editor' }).findByText('New Section')
|
|
||||||
|
|
||||||
cy.log('check PDF')
|
|
||||||
cy.findByRole('region', { name: 'PDF preview and logs' }).should(
|
|
||||||
'contain.text',
|
|
||||||
'New Section'
|
|
||||||
)
|
|
||||||
cy.log('check history')
|
|
||||||
cy.findByRole('navigation', {
|
|
||||||
name: 'Project actions',
|
|
||||||
})
|
|
||||||
.findByRole('button', { name: 'History' })
|
|
||||||
.click()
|
|
||||||
cy.findByText(/\\section\{New Section}/)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
import { createProject } from './helpers/project'
|
|
||||||
import { prepareWaitForNextCompileSlot } from './helpers/compile'
|
|
||||||
import { ensureUserExists, login } from './helpers/login'
|
|
||||||
import { isExcludedBySharding, startWith } from './helpers/config'
|
|
||||||
|
|
||||||
describe('History', function () {
|
|
||||||
if (isExcludedBySharding('CE_DEFAULT')) return
|
|
||||||
startWith({})
|
|
||||||
ensureUserExists({ email: 'user@example.com' })
|
|
||||||
beforeEach(function () {
|
|
||||||
login('user@example.com')
|
|
||||||
})
|
|
||||||
|
|
||||||
function addLabel(name: string) {
|
|
||||||
cy.log(`add label ${JSON.stringify(name)}`)
|
|
||||||
// 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('Labels').click()
|
|
||||||
})
|
|
||||||
cy.findByRole('radio', { name: 'Labels' }).should('be.checked')
|
|
||||||
cy.findByRole('radio', { name: 'All history' }).should('not.be.checked')
|
|
||||||
cy.findAllByTestId('history-version-details')
|
|
||||||
.first()
|
|
||||||
.within(() => {
|
|
||||||
cy.findByRole('button', { name: 'More actions' }).click()
|
|
||||||
cy.findByRole('menuitem', { name: 'Label this version' }).click()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
cy.findByRole('dialog').within(() => {
|
|
||||||
cy.findByLabelText('New label name').type(`${name}{enter}`)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function downloadVersion(name: string) {
|
|
||||||
cy.log(`download version ${JSON.stringify(name)}`)
|
|
||||||
// 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('Labels').click()
|
|
||||||
})
|
|
||||||
cy.findByRole('radio', { name: 'Labels' }).should('be.checked')
|
|
||||||
cy.findByRole('radio', { name: 'All history' }).should('not.be.checked')
|
|
||||||
cy.findByText(name)
|
|
||||||
.closest('[data-testid="history-version-details"]')
|
|
||||||
.within(() => {
|
|
||||||
cy.findByRole('button', { name: 'More actions' }).click()
|
|
||||||
cy.findByRole('menuitem', { name: 'Download this version' }).click()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const CLASS_ADDITION = 'ol-cm-addition-marker'
|
|
||||||
const CLASS_DELETION = 'ol-cm-deletion-marker'
|
|
||||||
|
|
||||||
it('should support labels, comparison and download', function () {
|
|
||||||
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(() => {
|
|
||||||
cy.findByText('\\maketitle').parent().click()
|
|
||||||
cy.findByText('\\maketitle').parent().type('\n% added')
|
|
||||||
cy.findByText('\\maketitle').parent().type('\n% to be removed')
|
|
||||||
})
|
|
||||||
recompile()
|
|
||||||
cy.findByRole('button', { name: 'History' }).click()
|
|
||||||
|
|
||||||
cy.log('expect to see additions in history')
|
|
||||||
cy.get('.document-diff-container').within(() => {
|
|
||||||
cy.findByText('% to be removed').should('have.class', CLASS_ADDITION)
|
|
||||||
cy.findByText('% added').should('have.class', CLASS_ADDITION)
|
|
||||||
})
|
|
||||||
|
|
||||||
addLabel('Before removal')
|
|
||||||
|
|
||||||
cy.log('remove content')
|
|
||||||
cy.findByRole('button', { name: 'Back to editor' }).click()
|
|
||||||
cy.findByText('% to be removed').parent().type('{end}{shift}{upArrow}{del}')
|
|
||||||
recompile()
|
|
||||||
cy.findByRole('button', { name: 'History' }).click()
|
|
||||||
|
|
||||||
cy.log('expect to see annotation for newly removed content in history')
|
|
||||||
cy.get('.document-diff-container').within(() => {
|
|
||||||
cy.findByText('% to be removed').should('have.class', CLASS_DELETION)
|
|
||||||
cy.findByText('% added').should('not.have.class', CLASS_ADDITION)
|
|
||||||
})
|
|
||||||
|
|
||||||
addLabel('After removal')
|
|
||||||
|
|
||||||
cy.log('add more content after labeling')
|
|
||||||
cy.findByRole('button', { name: 'Back to editor' }).click()
|
|
||||||
cy.findByRole('textbox', { name: 'Source Editor editing' }).within(() => {
|
|
||||||
cy.findByText('\\maketitle').parent().click()
|
|
||||||
cy.findByText('\\maketitle').parent().type('\n% more')
|
|
||||||
})
|
|
||||||
recompile()
|
|
||||||
|
|
||||||
cy.log('compare non current versions')
|
|
||||||
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('Labels').click()
|
|
||||||
})
|
|
||||||
cy.findByRole('radio', { name: 'Labels' }).should('be.checked')
|
|
||||||
cy.findByRole('radio', { name: 'All history' }).should('not.be.checked')
|
|
||||||
cy.findAllByTestId('compare-icon-version').last().click()
|
|
||||||
cy.findAllByTestId('compare-icon-version').filter(':visible').click()
|
|
||||||
cy.findByRole('menuitem', {
|
|
||||||
name: 'Compare up to this version',
|
|
||||||
}).click()
|
|
||||||
})
|
|
||||||
cy.log(
|
|
||||||
'expect to see annotation for removed content between the two versions'
|
|
||||||
)
|
|
||||||
cy.get('.document-diff-container').within(() => {
|
|
||||||
cy.findByText('% to be removed').should('have.class', CLASS_DELETION)
|
|
||||||
cy.findByText('% added').should('not.have.class', CLASS_ADDITION)
|
|
||||||
cy.findByText('% more').should('not.exist')
|
|
||||||
})
|
|
||||||
|
|
||||||
downloadVersion('Before removal')
|
|
||||||
cy.task('readFileInZip', {
|
|
||||||
pathToZip: `cypress/downloads/labels (Version 2).zip`,
|
|
||||||
fileToRead: 'main.tex',
|
|
||||||
})
|
|
||||||
.should('contain', '% added')
|
|
||||||
.should('contain', '% to be removed')
|
|
||||||
.should('not.contain', '% more')
|
|
||||||
|
|
||||||
downloadVersion('After removal')
|
|
||||||
cy.task('readFileInZip', {
|
|
||||||
pathToZip: `cypress/downloads/labels (Version 3).zip`,
|
|
||||||
fileToRead: 'main.tex',
|
|
||||||
})
|
|
||||||
.should('contain', '% added')
|
|
||||||
.should('not.contain', '% to be removed')
|
|
||||||
.should('not.contain', '% more')
|
|
||||||
|
|
||||||
downloadVersion('Current state')
|
|
||||||
cy.task('readFileInZip', {
|
|
||||||
pathToZip: `cypress/downloads/labels (Version 4).zip`,
|
|
||||||
fileToRead: 'main.tex',
|
|
||||||
})
|
|
||||||
.should('contain', '% added')
|
|
||||||
.should('not.contain', '% to be removed')
|
|
||||||
.should('contain', '% more')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,518 +0,0 @@
|
|||||||
import { v4 as uuid } from 'uuid'
|
|
||||||
import {
|
|
||||||
isExcludedBySharding,
|
|
||||||
startWith,
|
|
||||||
reloadWith,
|
|
||||||
STARTUP_TIMEOUT,
|
|
||||||
} from './helpers/config'
|
|
||||||
import { ensureUserExists, login } from './helpers/login'
|
|
||||||
import {
|
|
||||||
createProject,
|
|
||||||
enableLinkSharing,
|
|
||||||
getSpamSafeProjectName,
|
|
||||||
openProjectByName,
|
|
||||||
openProjectViaLinkSharingAsAnon,
|
|
||||||
openProjectViaLinkSharingAsUser,
|
|
||||||
shareProjectByEmailAndAcceptInviteViaDash,
|
|
||||||
shareProjectByEmailAndAcceptInviteViaEmail,
|
|
||||||
} from './helpers/project'
|
|
||||||
import { prepareWaitForNextCompileSlot } from './helpers/compile'
|
|
||||||
import { beforeWithReRunOnTestRetry } from './helpers/beforeWithReRunOnTestRetry'
|
|
||||||
|
|
||||||
describe('Project Sharing', function () {
|
|
||||||
if (isExcludedBySharding('PRO_CUSTOM_4')) return
|
|
||||||
ensureUserExists({ email: 'user@example.com' })
|
|
||||||
startWith({ withDataDir: true, pro: true })
|
|
||||||
|
|
||||||
let projectName: string
|
|
||||||
let recompile: () => void
|
|
||||||
let waitForCompile: (triggerCompile: () => void) => void
|
|
||||||
beforeWithReRunOnTestRetry(() => {
|
|
||||||
projectName = getSpamSafeProjectName()
|
|
||||||
;({ recompile, waitForCompile } = prepareWaitForNextCompileSlot())
|
|
||||||
setupTestProject()
|
|
||||||
})
|
|
||||||
|
|
||||||
beforeEach(function () {
|
|
||||||
// Always start with a fresh session
|
|
||||||
cy.session([uuid()], () => {})
|
|
||||||
})
|
|
||||||
|
|
||||||
let linkSharingReadOnly: string
|
|
||||||
let linkSharingReadAndWrite: string
|
|
||||||
|
|
||||||
function setupTestProject() {
|
|
||||||
login('user@example.com')
|
|
||||||
waitForCompile(() => {
|
|
||||||
createProject(projectName)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Add chat message
|
|
||||||
cy.findByRole('button', { name: 'Chat' }).click()
|
|
||||||
// wait for lazy loading of the chat pane
|
|
||||||
cy.findByRole('complementary', { name: 'Chat' }).findByText(
|
|
||||||
'Send your first message to your collaborators'
|
|
||||||
)
|
|
||||||
cy.findByLabelText('Send a message to your collaborators…').type(
|
|
||||||
'New Chat Message{enter}'
|
|
||||||
)
|
|
||||||
|
|
||||||
// Get link sharing links
|
|
||||||
enableLinkSharing().then(
|
|
||||||
({ linkSharingReadOnly: ro, linkSharingReadAndWrite: rw }) => {
|
|
||||||
linkSharingReadAndWrite = rw
|
|
||||||
linkSharingReadOnly = ro
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function expectContentReadOnlyAccess() {
|
|
||||||
cy.url().should('match', /\/project\/[a-fA-F0-9]{24}/)
|
|
||||||
cy.findByRole('textbox', { name: 'Source Editor editing' }).should(
|
|
||||||
'contain.text',
|
|
||||||
'\\maketitle'
|
|
||||||
)
|
|
||||||
cy.findByRole('textbox', { name: 'Source Editor editing' }).should(
|
|
||||||
'have.attr',
|
|
||||||
'contenteditable',
|
|
||||||
'false'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function expectContentWriteAccess() {
|
|
||||||
const section = `Test Section ${uuid()}`
|
|
||||||
cy.url().should('match', /\/project\/[a-fA-F0-9]{24}/)
|
|
||||||
// wait for the editor to finish loading
|
|
||||||
cy.findByRole('textbox', { name: 'Source Editor editing' }).should(
|
|
||||||
'contain.text',
|
|
||||||
'\\maketitle'
|
|
||||||
)
|
|
||||||
// the editor should be writable
|
|
||||||
cy.findByRole('textbox', { name: 'Source Editor editing' }).should(
|
|
||||||
'have.attr',
|
|
||||||
'contenteditable',
|
|
||||||
'true'
|
|
||||||
)
|
|
||||||
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',
|
|
||||||
`\\section{${section}}`
|
|
||||||
)
|
|
||||||
// check PDF
|
|
||||||
recompile()
|
|
||||||
cy.findByRole('region', { name: 'PDF preview and logs' }).within(() => {
|
|
||||||
cy.findByLabelText(/Page.*1/i).should('be.visible')
|
|
||||||
cy.findByText(projectName).should('be.visible')
|
|
||||||
})
|
|
||||||
cy.findByRole('region', { name: 'PDF preview and logs' }).within(() => {
|
|
||||||
cy.findByLabelText(/Page.*1/i).should('be.visible')
|
|
||||||
cy.contains(section)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function expectNoAccess() {
|
|
||||||
// try read only access link
|
|
||||||
cy.visit(linkSharingReadOnly)
|
|
||||||
cy.url().should('match', /\/login/)
|
|
||||||
|
|
||||||
// Cypress bugs: cypress resolves the link-sharing link outside the browser, and it carries over the hash of the link-sharing link to the login page redirect (bug 1).
|
|
||||||
// Effectively, cypress then instructs the browser to change the page from /login#read-only-hash to /login#read-and-write-hash.
|
|
||||||
// This is turn does not trigger a "page load", but rather just "scrolling", which in turn trips up the "page loaded" detection in cypress (bug 2).
|
|
||||||
// Work around this by navigating away from the /login page in between checks.
|
|
||||||
cy.visit('/user/password/reset')
|
|
||||||
|
|
||||||
// try read and write access link
|
|
||||||
cy.visit(linkSharingReadAndWrite)
|
|
||||||
cy.url().should('match', /\/login/)
|
|
||||||
}
|
|
||||||
|
|
||||||
function expectChatAccess() {
|
|
||||||
cy.findByRole('button', { name: 'Chat' }).click()
|
|
||||||
cy.findByRole('complementary', { name: 'Chat' }).findByText(
|
|
||||||
'New Chat Message'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function expectHistoryAccess() {
|
|
||||||
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.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() {
|
|
||||||
cy.findByRole('button', { name: 'Layout' }) // wait for lazy loading
|
|
||||||
cy.findByRole('button', { name: 'Chat' }).should('not.exist')
|
|
||||||
}
|
|
||||||
|
|
||||||
function expectNoHistoryAccess() {
|
|
||||||
cy.findByRole('button', { name: 'Layout' }) // wait for lazy loading
|
|
||||||
cy.findByRole('button', { name: 'History' }).should('not.exist')
|
|
||||||
}
|
|
||||||
|
|
||||||
function expectCommentAccess() {
|
|
||||||
cy.findByRole('textbox', { name: 'Source Editor editing' }).should(
|
|
||||||
'contain.text',
|
|
||||||
'\\maketitle'
|
|
||||||
)
|
|
||||||
|
|
||||||
cy.findByText('\\maketitle').parent().dblclick()
|
|
||||||
|
|
||||||
cy.findByRole('button', { name: 'Add comment' }).should('be.visible')
|
|
||||||
|
|
||||||
cy.findByRole('textbox', { name: 'Source Editor editing' }).click()
|
|
||||||
}
|
|
||||||
|
|
||||||
function expectNoCommentAccess() {
|
|
||||||
cy.findByRole('textbox', { name: 'Source Editor editing' }).should(
|
|
||||||
'contain.text',
|
|
||||||
'\\maketitle'
|
|
||||||
)
|
|
||||||
|
|
||||||
cy.findByText('\\maketitle').parent().dblclick()
|
|
||||||
|
|
||||||
cy.findByRole('button', { name: 'Add comment' }).should('not.exist')
|
|
||||||
cy.findByRole('textbox', { name: 'Source Editor editing' }).click()
|
|
||||||
}
|
|
||||||
|
|
||||||
function expectFullReadOnlyAccess() {
|
|
||||||
expectContentReadOnlyAccess()
|
|
||||||
expectChatAccess()
|
|
||||||
expectHistoryAccess()
|
|
||||||
expectNoCommentAccess()
|
|
||||||
}
|
|
||||||
|
|
||||||
function expectRestrictedReadOnlyAccess() {
|
|
||||||
expectContentReadOnlyAccess()
|
|
||||||
expectNoChatAccess()
|
|
||||||
expectNoHistoryAccess()
|
|
||||||
expectNoCommentAccess()
|
|
||||||
}
|
|
||||||
|
|
||||||
function expectFullReadAndWriteAccess() {
|
|
||||||
expectContentWriteAccess()
|
|
||||||
expectChatAccess()
|
|
||||||
expectHistoryAccess()
|
|
||||||
expectCommentAccess()
|
|
||||||
}
|
|
||||||
|
|
||||||
function expectAnonymousReadAndWriteAccess() {
|
|
||||||
expectContentWriteAccess()
|
|
||||||
expectChatAccess()
|
|
||||||
expectHistoryAccess()
|
|
||||||
expectNoCommentAccess()
|
|
||||||
}
|
|
||||||
|
|
||||||
function expectProjectDashboardEntry() {
|
|
||||||
cy.visit('/project')
|
|
||||||
cy.findByText(projectName)
|
|
||||||
}
|
|
||||||
|
|
||||||
function expectEditAuthoredAs(author: string) {
|
|
||||||
cy.findByRole('button', { name: 'History' }).click()
|
|
||||||
cy.findByRole('complementary', {
|
|
||||||
name: 'Project history and labels',
|
|
||||||
}).within(() => {
|
|
||||||
cy.findAllByTestId('history-version-metadata-users')
|
|
||||||
.first()
|
|
||||||
.should('contain.text', author) // might have other edits in the same group
|
|
||||||
})
|
|
||||||
}
|
|
||||||
describe('via email', function () {
|
|
||||||
const email = 'collaborator-email@example.com'
|
|
||||||
ensureUserExists({ email })
|
|
||||||
|
|
||||||
beforeEach(function () {
|
|
||||||
login('user@example.com')
|
|
||||||
shareProjectByEmailAndAcceptInviteViaEmail(projectName, email, 'Viewer')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should grant the collaborator read access', function () {
|
|
||||||
expectFullReadOnlyAccess()
|
|
||||||
expectProjectDashboardEntry()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('read only', function () {
|
|
||||||
const email = 'collaborator-ro@example.com'
|
|
||||||
ensureUserExists({ email })
|
|
||||||
|
|
||||||
beforeWithReRunOnTestRetry(() => {
|
|
||||||
login('user@example.com')
|
|
||||||
shareProjectByEmailAndAcceptInviteViaDash(projectName, email, 'Viewer')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should grant the collaborator read access', function () {
|
|
||||||
login(email)
|
|
||||||
openProjectByName(projectName)
|
|
||||||
expectFullReadOnlyAccess()
|
|
||||||
expectProjectDashboardEntry()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('read and write', function () {
|
|
||||||
const email = 'collaborator-rw@example.com'
|
|
||||||
ensureUserExists({ email })
|
|
||||||
|
|
||||||
beforeWithReRunOnTestRetry(() => {
|
|
||||||
login('user@example.com')
|
|
||||||
shareProjectByEmailAndAcceptInviteViaDash(projectName, email, 'Editor')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should grant the collaborator write access', function () {
|
|
||||||
login(email)
|
|
||||||
openProjectByName(projectName)
|
|
||||||
expectFullReadAndWriteAccess()
|
|
||||||
expectEditAuthoredAs('You')
|
|
||||||
expectProjectDashboardEntry()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('token access', function () {
|
|
||||||
describe('logged in', function () {
|
|
||||||
describe('read only', function () {
|
|
||||||
const email = 'collaborator-link-ro@example.com'
|
|
||||||
ensureUserExists({ email })
|
|
||||||
|
|
||||||
it('should grant restricted read access', function () {
|
|
||||||
login(email)
|
|
||||||
openProjectViaLinkSharingAsUser(
|
|
||||||
linkSharingReadOnly,
|
|
||||||
projectName,
|
|
||||||
email
|
|
||||||
)
|
|
||||||
expectRestrictedReadOnlyAccess()
|
|
||||||
expectProjectDashboardEntry()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('read and write', function () {
|
|
||||||
const email = 'collaborator-link-rw@example.com'
|
|
||||||
ensureUserExists({ email })
|
|
||||||
|
|
||||||
it('should grant full write access', function () {
|
|
||||||
login(email)
|
|
||||||
openProjectViaLinkSharingAsUser(
|
|
||||||
linkSharingReadAndWrite,
|
|
||||||
projectName,
|
|
||||||
email
|
|
||||||
)
|
|
||||||
expectFullReadAndWriteAccess()
|
|
||||||
expectEditAuthoredAs('You')
|
|
||||||
expectProjectDashboardEntry()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('with OVERLEAF_ALLOW_PUBLIC_ACCESS=false', function () {
|
|
||||||
describe('wrap startup', function () {
|
|
||||||
startWith({
|
|
||||||
pro: true,
|
|
||||||
vars: {
|
|
||||||
OVERLEAF_ALLOW_PUBLIC_ACCESS: 'false',
|
|
||||||
},
|
|
||||||
withDataDir: true,
|
|
||||||
})
|
|
||||||
it('should block access', function () {
|
|
||||||
expectNoAccess()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('with OVERLEAF_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING=true', function () {
|
|
||||||
startWith({
|
|
||||||
pro: true,
|
|
||||||
vars: {
|
|
||||||
OVERLEAF_ALLOW_PUBLIC_ACCESS: 'false',
|
|
||||||
OVERLEAF_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING: 'true',
|
|
||||||
},
|
|
||||||
withDataDir: true,
|
|
||||||
})
|
|
||||||
it('should block access', function () {
|
|
||||||
expectNoAccess()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('with OVERLEAF_ALLOW_PUBLIC_ACCESS=true', function () {
|
|
||||||
describe('wrap startup', function () {
|
|
||||||
startWith({
|
|
||||||
pro: true,
|
|
||||||
vars: {
|
|
||||||
OVERLEAF_ALLOW_PUBLIC_ACCESS: 'true',
|
|
||||||
},
|
|
||||||
withDataDir: true,
|
|
||||||
})
|
|
||||||
it('should grant read access with read link', function () {
|
|
||||||
openProjectViaLinkSharingAsAnon(linkSharingReadOnly)
|
|
||||||
expectRestrictedReadOnlyAccess()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should prompt for login with write link', function () {
|
|
||||||
cy.visit(linkSharingReadAndWrite)
|
|
||||||
cy.url().should('match', /\/login/)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('with OVERLEAF_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING=true', function () {
|
|
||||||
startWith({
|
|
||||||
pro: true,
|
|
||||||
vars: {
|
|
||||||
OVERLEAF_ALLOW_PUBLIC_ACCESS: 'true',
|
|
||||||
OVERLEAF_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING: 'true',
|
|
||||||
},
|
|
||||||
withDataDir: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should grant read access with read link', function () {
|
|
||||||
openProjectViaLinkSharingAsAnon(linkSharingReadOnly)
|
|
||||||
expectRestrictedReadOnlyAccess()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should grant write access with write link', function () {
|
|
||||||
openProjectViaLinkSharingAsAnon(linkSharingReadAndWrite)
|
|
||||||
expectAnonymousReadAndWriteAccess()
|
|
||||||
expectEditAuthoredAs('Anonymous')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('with OVERLEAF_DISABLE_LINK_SHARING=true', function () {
|
|
||||||
const email = 'collaborator-email@example.com'
|
|
||||||
ensureUserExists({ email })
|
|
||||||
|
|
||||||
const invitedEmail = 'invited-email@example.com'
|
|
||||||
ensureUserExists({ email: invitedEmail })
|
|
||||||
|
|
||||||
const retainedViewerEmail = 'collaborator-retained-viewer@example.com'
|
|
||||||
ensureUserExists({ email: retainedViewerEmail })
|
|
||||||
|
|
||||||
const retainedEditorEmail = 'collaborator-retained-editor@example.com'
|
|
||||||
ensureUserExists({ email: retainedEditorEmail })
|
|
||||||
|
|
||||||
// Link-sharing urls have to be created before disabling link sharing.
|
|
||||||
// We use the `beforeEach` hook to reload the server with link sharing
|
|
||||||
// disabled **after** the initial setup which happens in the `before`
|
|
||||||
// block. The `before` hook always runs prior to the `beforeEach` hook.
|
|
||||||
|
|
||||||
// Set up retained access before disabling link sharing
|
|
||||||
before(function () {
|
|
||||||
// Set up retained viewer access
|
|
||||||
login(retainedViewerEmail)
|
|
||||||
openProjectViaLinkSharingAsUser(
|
|
||||||
linkSharingReadOnly,
|
|
||||||
projectName,
|
|
||||||
retainedViewerEmail
|
|
||||||
)
|
|
||||||
|
|
||||||
// Set up retained editor access
|
|
||||||
login(retainedEditorEmail)
|
|
||||||
openProjectViaLinkSharingAsUser(
|
|
||||||
linkSharingReadAndWrite,
|
|
||||||
projectName,
|
|
||||||
retainedEditorEmail
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
beforeEach(function () {
|
|
||||||
this.timeout(STARTUP_TIMEOUT) // Increase timeout for server reload
|
|
||||||
|
|
||||||
return cy.wrap(
|
|
||||||
reloadWith({
|
|
||||||
pro: true,
|
|
||||||
vars: {
|
|
||||||
OVERLEAF_ALLOW_PUBLIC_ACCESS: 'true',
|
|
||||||
OVERLEAF_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING: 'true',
|
|
||||||
OVERLEAF_DISABLE_LINK_SHARING: 'true',
|
|
||||||
},
|
|
||||||
withDataDir: true,
|
|
||||||
}),
|
|
||||||
{ timeout: STARTUP_TIMEOUT }
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should not display link sharing in the sharing modal', function () {
|
|
||||||
login('user@example.com')
|
|
||||||
openProjectByName(projectName)
|
|
||||||
cy.findByRole('navigation', {
|
|
||||||
name: 'Project actions',
|
|
||||||
})
|
|
||||||
.findByRole('button', { name: 'Share' })
|
|
||||||
.click()
|
|
||||||
cy.findByRole('button', { name: 'Turn on link sharing' }).should(
|
|
||||||
'not.exist'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should block new access to read-only link shared projects', function () {
|
|
||||||
login(email)
|
|
||||||
|
|
||||||
// Test read-only link returns 404
|
|
||||||
cy.request({
|
|
||||||
url: linkSharingReadOnly,
|
|
||||||
failOnStatusCode: false,
|
|
||||||
}).then(response => {
|
|
||||||
expect(response.status).to.eq(404)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should block new access to read-write link shared projects', function () {
|
|
||||||
login(email)
|
|
||||||
|
|
||||||
// Test read-write link returns 404
|
|
||||||
cy.request({
|
|
||||||
url: linkSharingReadAndWrite,
|
|
||||||
failOnStatusCode: false,
|
|
||||||
}).then(response => {
|
|
||||||
expect(response.status).to.eq(404)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should continue to allow email sharing', function () {
|
|
||||||
login('user@example.com')
|
|
||||||
shareProjectByEmailAndAcceptInviteViaEmail(
|
|
||||||
projectName,
|
|
||||||
invitedEmail,
|
|
||||||
'Viewer'
|
|
||||||
)
|
|
||||||
expectFullReadOnlyAccess()
|
|
||||||
expectProjectDashboardEntry()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should retain read-only access when project was joined via link before link sharing was turned off', function () {
|
|
||||||
login(retainedViewerEmail)
|
|
||||||
openProjectByName(projectName)
|
|
||||||
expectRestrictedReadOnlyAccess()
|
|
||||||
expectProjectDashboardEntry()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should retain read-write access when project was joined via link before link sharing was turned off', function () {
|
|
||||||
login(retainedEditorEmail)
|
|
||||||
openProjectByName(projectName)
|
|
||||||
expectFullReadAndWriteAccess()
|
|
||||||
expectProjectDashboardEntry()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,352 +0,0 @@
|
|||||||
import { ensureUserExists, login } from './helpers/login'
|
|
||||||
import { createProject } from './helpers/project'
|
|
||||||
import { isExcludedBySharding, startWith } from './helpers/config'
|
|
||||||
import { prepareWaitForNextCompileSlot, stopCompile } from './helpers/compile'
|
|
||||||
import { v4 as uuid } from 'uuid'
|
|
||||||
import { waitUntilScrollingFinished } from './helpers/waitUntilScrollingFinished'
|
|
||||||
|
|
||||||
const LABEL_TEX_LIVE_VERSION = 'TeX Live version'
|
|
||||||
|
|
||||||
describe('SandboxedCompiles', function () {
|
|
||||||
const enabledVars = {
|
|
||||||
SANDBOXED_COMPILES: 'true',
|
|
||||||
ALL_TEX_LIVE_DOCKER_IMAGE_NAMES: '2023,2022',
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('enabled in Server Pro', function () {
|
|
||||||
if (isExcludedBySharding('PRO_CUSTOM_2')) return
|
|
||||||
startWith({
|
|
||||||
pro: true,
|
|
||||||
vars: enabledVars,
|
|
||||||
resetData: true,
|
|
||||||
})
|
|
||||||
ensureUserExists({ email: 'user@example.com' })
|
|
||||||
beforeEach(function () {
|
|
||||||
login('user@example.com')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should offer TexLive images and switch the compiler', function () {
|
|
||||||
const { recompile, waitForCompile } = prepareWaitForNextCompileSlot()
|
|
||||||
waitForCompile(() => {
|
|
||||||
createProject('sandboxed')
|
|
||||||
})
|
|
||||||
cy.log('wait for compile')
|
|
||||||
cy.findByRole('region', { name: 'PDF preview and logs' }).should(
|
|
||||||
'contain.text',
|
|
||||||
'sandboxed'
|
|
||||||
)
|
|
||||||
|
|
||||||
cy.log('Check which compiler version was used, expect 2023')
|
|
||||||
cy.findByRole('button', { name: 'View logs' }).click()
|
|
||||||
cy.findByLabelText('Raw logs from the LaTeX compiler').findByText(
|
|
||||||
/This is pdfTeX, Version .+ \(TeX Live 2023\) /
|
|
||||||
)
|
|
||||||
|
|
||||||
cy.log('Switch TeXLive version from 2023 to 2022')
|
|
||||||
cy.findByRole('navigation', {
|
|
||||||
name: 'Project actions',
|
|
||||||
})
|
|
||||||
.findByRole('button', { name: 'Menu' })
|
|
||||||
.click()
|
|
||||||
cy.findByRole('dialog').within(() => {
|
|
||||||
cy.findByRole('option', { name: '2023' }).should('be.selected')
|
|
||||||
cy.findByRole('combobox', { name: LABEL_TEX_LIVE_VERSION }).select(
|
|
||||||
'2022'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
cy.get('body').type('{esc}')
|
|
||||||
cy.findByRole('dialog').should('not.exist')
|
|
||||||
cy.log('Trigger compile with other TeX Live version')
|
|
||||||
recompile()
|
|
||||||
|
|
||||||
cy.log('Check which compiler version was used, expect 2022')
|
|
||||||
cy.findByRole('button', { name: 'View logs' }).click()
|
|
||||||
cy.findByLabelText('Raw logs from the LaTeX compiler').findByText(
|
|
||||||
/This is pdfTeX, Version .+ \(TeX Live 2022\) /
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
checkSyncTeX()
|
|
||||||
checkXeTeX()
|
|
||||||
checkRecompilesAfterErrors()
|
|
||||||
checkStopCompile()
|
|
||||||
})
|
|
||||||
|
|
||||||
function checkStopCompile() {
|
|
||||||
it('users can stop a running compile', function () {
|
|
||||||
login('user@example.com')
|
|
||||||
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.findByRole('button', { name: 'Recompile' }).click()
|
|
||||||
// Now stop the compile and kill the latex process
|
|
||||||
stopCompile({ delay: 1000 })
|
|
||||||
cy.findByRole('region', { name: 'PDF preview and logs' })
|
|
||||||
.invoke('text')
|
|
||||||
.should('match', /PDF Rendering Error|Compilation cancelled/)
|
|
||||||
// Check that the previous compile is not running in the background by
|
|
||||||
// disabling the infinite loop and recompiling
|
|
||||||
cy.findByText('\\def').parent().click()
|
|
||||||
cy.findByText('\\def').parent().type('{home}disabled loop% ')
|
|
||||||
recompile()
|
|
||||||
cy.findByRole('region', { name: 'PDF preview and logs' })
|
|
||||||
.should('contain.text', 'disabled loop')
|
|
||||||
.should('not.contain.text', 'A previous compile is still running')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkSyncTeX() {
|
|
||||||
describe('SyncTeX', function () {
|
|
||||||
let projectName: string
|
|
||||||
beforeEach(function () {
|
|
||||||
projectName = `Project ${uuid()}`
|
|
||||||
const { recompile, waitForCompile } = prepareWaitForNextCompileSlot()
|
|
||||||
waitForCompile(() => {
|
|
||||||
createProject(projectName)
|
|
||||||
})
|
|
||||||
cy.findByRole('textbox', { name: 'Source Editor editing' }).within(
|
|
||||||
() => {
|
|
||||||
cy.findByText('\\maketitle').parent().click()
|
|
||||||
cy.findByText('\\maketitle')
|
|
||||||
.parent()
|
|
||||||
.type(
|
|
||||||
`\n\\pagebreak\n\\section{{}Section A}\n\\pagebreak\n\\section{{}Section B}\n\\pagebreak`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
recompile()
|
|
||||||
cy.log('wait for pdf-rendering')
|
|
||||||
cy.findByRole('region', { name: 'PDF preview and logs' }).findByText(
|
|
||||||
projectName
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should sync to code', function () {
|
|
||||||
cy.log('navigate to \\maketitle using double click in PDF')
|
|
||||||
cy.findByRole('region', { name: 'PDF preview and logs' })
|
|
||||||
.findByText(projectName)
|
|
||||||
.dblclick()
|
|
||||||
cy.get('.cm-activeLine').should('have.text', '\\maketitle')
|
|
||||||
|
|
||||||
cy.log('navigate to Section A using double click in PDF')
|
|
||||||
cy.findByRole('region', { name: 'PDF preview and logs' })
|
|
||||||
.findByText('Section A')
|
|
||||||
.dblclick()
|
|
||||||
cy.get('.cm-activeLine').should('have.text', '\\section{Section A}')
|
|
||||||
|
|
||||||
cy.log('navigate to Section B using arrow button')
|
|
||||||
cy.findByTestId('pdfjs-viewer-inner')
|
|
||||||
.should('have.prop', 'scrollTop')
|
|
||||||
.as('start')
|
|
||||||
cy.findByRole('region', { name: 'PDF preview and logs' })
|
|
||||||
.findByText('Section B')
|
|
||||||
.scrollIntoView()
|
|
||||||
cy.get('@start').then((start: any) => {
|
|
||||||
waitUntilScrollingFinished(
|
|
||||||
'[data-testid="pdfjs-viewer-inner"]',
|
|
||||||
start
|
|
||||||
)
|
|
||||||
})
|
|
||||||
// The sync button is swapped as the position in the PDF changes.
|
|
||||||
// Cypress appears to click on a button that references a stale position.
|
|
||||||
// Adding a cy.wait() statement is the most reliable "fix" so far :/
|
|
||||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
|
||||||
cy.wait(1000)
|
|
||||||
cy.findByRole('button', {
|
|
||||||
name: 'Go to PDF location in code (Tip: double click on the PDF for best results)',
|
|
||||||
}).click()
|
|
||||||
cy.get('.cm-activeLine').should('have.text', '\\section{Section B}')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should sync to pdf', function () {
|
|
||||||
cy.log('zoom in')
|
|
||||||
cy.findByRole('button', { name: 'PDF zoom level' }).click()
|
|
||||||
cy.findByRole('menuitem', { name: '400%' }).click()
|
|
||||||
cy.log('scroll to top')
|
|
||||||
cy.findByTestId('pdfjs-viewer-inner').scrollTo('top')
|
|
||||||
waitUntilScrollingFinished('[data-testid="pdfjs-viewer-inner"]', -1).as(
|
|
||||||
'start'
|
|
||||||
)
|
|
||||||
|
|
||||||
cy.log('navigate to title')
|
|
||||||
cy.findByRole('textbox', { name: 'Source Editor editing' }).within(
|
|
||||||
() => {
|
|
||||||
cy.findByText('\\maketitle').parent().click()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
cy.findByRole('button', { name: 'Go to code location in PDF' }).click()
|
|
||||||
cy.get('@start').then((start: any) => {
|
|
||||||
waitUntilScrollingFinished(
|
|
||||||
'[data-testid="pdfjs-viewer-inner"]',
|
|
||||||
start
|
|
||||||
)
|
|
||||||
.as('title')
|
|
||||||
.should('be.greaterThan', start)
|
|
||||||
})
|
|
||||||
|
|
||||||
cy.log('navigate to Section A')
|
|
||||||
cy.findByRole('textbox', { name: 'Source Editor editing' }).within(() =>
|
|
||||||
cy.findByText('Section A').click()
|
|
||||||
)
|
|
||||||
cy.findByRole('button', { name: 'Go to code location in PDF' }).click()
|
|
||||||
cy.get('@title').then((title: any) => {
|
|
||||||
waitUntilScrollingFinished(
|
|
||||||
'[data-testid="pdfjs-viewer-inner"]',
|
|
||||||
title
|
|
||||||
)
|
|
||||||
.as('sectionA')
|
|
||||||
.should('be.greaterThan', title)
|
|
||||||
})
|
|
||||||
|
|
||||||
cy.log('navigate to Section B')
|
|
||||||
cy.findByRole('textbox', { name: 'Source Editor editing' }).within(() =>
|
|
||||||
cy.findByText('Section B').click()
|
|
||||||
)
|
|
||||||
cy.findByRole('button', { name: 'Go to code location in PDF' }).click()
|
|
||||||
cy.get('@sectionA').then((title: any) => {
|
|
||||||
waitUntilScrollingFinished(
|
|
||||||
'[data-testid="pdfjs-viewer-inner"]',
|
|
||||||
title
|
|
||||||
)
|
|
||||||
.as('sectionB')
|
|
||||||
.should('be.greaterThan', title)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkRecompilesAfterErrors() {
|
|
||||||
it('recompiles even if there are Latex errors', function () {
|
|
||||||
login('user@example.com')
|
|
||||||
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\\fakeCommand{} \n\\section{{}Test Section}')
|
|
||||||
})
|
|
||||||
recompile()
|
|
||||||
recompile()
|
|
||||||
cy.findByRole('region', { name: 'PDF preview and logs' })
|
|
||||||
.findByText('Test Section')
|
|
||||||
.should('not.contain.text', 'No PDF')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkXeTeX() {
|
|
||||||
it('should be able to use XeLaTeX', function () {
|
|
||||||
const { recompile, waitForCompile } = prepareWaitForNextCompileSlot()
|
|
||||||
waitForCompile(() => {
|
|
||||||
createProject('XeLaTeX')
|
|
||||||
})
|
|
||||||
cy.log('wait for compile')
|
|
||||||
cy.findByRole('region', { name: 'PDF preview and logs' }).findByText(
|
|
||||||
'XeLaTeX'
|
|
||||||
)
|
|
||||||
cy.log('Check which compiler was used, expect pdfLaTeX')
|
|
||||||
cy.findByRole('button', { name: 'View logs' }).click()
|
|
||||||
cy.findByLabelText('Raw logs from the LaTeX compiler').findByText(
|
|
||||||
/This is pdfTeX/
|
|
||||||
)
|
|
||||||
|
|
||||||
cy.log('Switch compiler to from pdfLaTeX to XeLaTeX')
|
|
||||||
cy.findByRole('navigation', {
|
|
||||||
name: 'Project actions',
|
|
||||||
})
|
|
||||||
.findByRole('button', { name: 'Menu' })
|
|
||||||
.click()
|
|
||||||
cy.findByRole('dialog').within(() => {
|
|
||||||
cy.findByRole('option', { name: 'pdfLaTeX' }).should('be.selected')
|
|
||||||
cy.findByRole('combobox', { name: 'Compiler' }).select('XeLaTeX')
|
|
||||||
})
|
|
||||||
cy.get('body').type('{esc}')
|
|
||||||
cy.findByRole('dialog').should('not.exist')
|
|
||||||
|
|
||||||
cy.log('Trigger compile with other compiler')
|
|
||||||
recompile()
|
|
||||||
|
|
||||||
cy.log('Check which compiler was used, expect XeLaTeX')
|
|
||||||
cy.findByRole('button', { name: 'View logs' }).click()
|
|
||||||
cy.findByLabelText('Raw logs from the LaTeX compiler').findByText(
|
|
||||||
/This is XeTeX/
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkUsesDefaultCompiler() {
|
|
||||||
beforeEach(function () {
|
|
||||||
login('user@example.com')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should not offer TexLive images and use default compiler', function () {
|
|
||||||
createProject('sandboxed')
|
|
||||||
cy.log('wait for compile')
|
|
||||||
cy.findByRole('region', { name: 'PDF preview and logs' }).findByText(
|
|
||||||
'sandboxed'
|
|
||||||
)
|
|
||||||
|
|
||||||
cy.log('Check which compiler version was used, expect 2025')
|
|
||||||
cy.findByRole('button', { name: 'View logs' }).click()
|
|
||||||
cy.findByLabelText('Raw logs from the LaTeX compiler').findByText(
|
|
||||||
/This is pdfTeX, Version .+ \(TeX Live 2025\) /
|
|
||||||
)
|
|
||||||
|
|
||||||
cy.log('Check that there is no TeX Live version toggle')
|
|
||||||
cy.findByRole('navigation', {
|
|
||||||
name: 'Project actions',
|
|
||||||
})
|
|
||||||
.findByRole('button', { name: 'Menu' })
|
|
||||||
.click()
|
|
||||||
cy.findByTestId('left-menu').within(() => {
|
|
||||||
cy.findByRole('button', { name: 'Word Count' }) // wait for lazy loading
|
|
||||||
cy.findByText(LABEL_TEX_LIVE_VERSION).should('not.exist')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('disabled in Server Pro', function () {
|
|
||||||
if (isExcludedBySharding('PRO_DEFAULT_2')) return
|
|
||||||
startWith({ pro: true })
|
|
||||||
ensureUserExists({ email: 'user@example.com' })
|
|
||||||
beforeEach(function () {
|
|
||||||
login('user@example.com')
|
|
||||||
})
|
|
||||||
|
|
||||||
checkUsesDefaultCompiler()
|
|
||||||
checkSyncTeX()
|
|
||||||
checkXeTeX()
|
|
||||||
checkRecompilesAfterErrors()
|
|
||||||
checkStopCompile()
|
|
||||||
})
|
|
||||||
|
|
||||||
// https://github.com/overleaf/internal/issues/20216
|
|
||||||
// eslint-disable-next-line mocha/no-skipped-tests
|
|
||||||
describe.skip('unavailable in CE', function () {
|
|
||||||
if (isExcludedBySharding('CE_CUSTOM_1')) return
|
|
||||||
startWith({ pro: false, vars: enabledVars, resetData: true })
|
|
||||||
ensureUserExists({ email: 'user@example.com' })
|
|
||||||
beforeEach(function () {
|
|
||||||
login('user@example.com')
|
|
||||||
})
|
|
||||||
|
|
||||||
checkUsesDefaultCompiler()
|
|
||||||
checkSyncTeX()
|
|
||||||
checkXeTeX()
|
|
||||||
checkRecompilesAfterErrors()
|
|
||||||
checkStopCompile()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,280 +0,0 @@
|
|||||||
import { isExcludedBySharding, startWith } from './helpers/config'
|
|
||||||
import { ensureUserExists, login } from './helpers/login'
|
|
||||||
import {
|
|
||||||
createProject,
|
|
||||||
NEW_PROJECT_BUTTON_MATCHER,
|
|
||||||
redirectEditorUrlWithQueryParams,
|
|
||||||
} from './helpers/project'
|
|
||||||
|
|
||||||
const WITHOUT_PROJECTS_USER = 'user-without-projects@example.com'
|
|
||||||
const ADMIN_USER = 'admin@example.com'
|
|
||||||
const REGULAR_USER = 'user@example.com'
|
|
||||||
const TEMPLATES_USER = 'templates@example.com'
|
|
||||||
|
|
||||||
// Re-use value for "exists" and "does not exist" tests
|
|
||||||
const LABEL_BROWSE_TEMPLATES = 'Browse templates'
|
|
||||||
|
|
||||||
describe('Templates', function () {
|
|
||||||
ensureUserExists({ email: TEMPLATES_USER })
|
|
||||||
ensureUserExists({ email: WITHOUT_PROJECTS_USER })
|
|
||||||
|
|
||||||
let OVERLEAF_TEMPLATES_USER_ID: string
|
|
||||||
before(function () {
|
|
||||||
login(TEMPLATES_USER)
|
|
||||||
cy.visit('/')
|
|
||||||
cy.get('meta[name="ol-user_id"]').then(el => {
|
|
||||||
OVERLEAF_TEMPLATES_USER_ID = el.attr('content')!
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
function varsFn() {
|
|
||||||
return {
|
|
||||||
OVERLEAF_TEMPLATES_USER_ID,
|
|
||||||
OVERLEAF_NEW_PROJECT_TEMPLATE_LINKS:
|
|
||||||
'[{"name":"All Templates","url":"/templates/all"}]',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('enabled in Server Pro', function () {
|
|
||||||
if (isExcludedBySharding('PRO_CUSTOM_2')) return
|
|
||||||
startWith({
|
|
||||||
pro: true,
|
|
||||||
varsFn,
|
|
||||||
})
|
|
||||||
ensureUserExists({ email: REGULAR_USER })
|
|
||||||
ensureUserExists({ email: ADMIN_USER, isAdmin: true })
|
|
||||||
|
|
||||||
it('should show templates link on welcome page', function () {
|
|
||||||
login(WITHOUT_PROJECTS_USER)
|
|
||||||
cy.visit('/')
|
|
||||||
cy.findByRole('link', { name: LABEL_BROWSE_TEMPLATES }).click()
|
|
||||||
cy.url().should('match', /\/templates$/)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should have templates feature', function () {
|
|
||||||
login(TEMPLATES_USER)
|
|
||||||
const name = `Template ${Date.now()}`
|
|
||||||
const description = `Template Description ${Date.now()}`
|
|
||||||
|
|
||||||
cy.visit('/')
|
|
||||||
createProject(name, { type: 'Example project' }).as('templateProjectId')
|
|
||||||
|
|
||||||
cy.findByRole('navigation', {
|
|
||||||
name: 'Project actions',
|
|
||||||
})
|
|
||||||
.findByRole('button', { name: 'Menu' })
|
|
||||||
.click()
|
|
||||||
cy.findByRole('button', { name: 'Manage Template' }).click()
|
|
||||||
|
|
||||||
cy.findByLabelText('Template Description').type(description)
|
|
||||||
cy.findByRole('button', { name: 'Publish' }).click()
|
|
||||||
cy.findByRole('button', { name: 'Publishing…' }).should('be.disabled')
|
|
||||||
cy.findByRole('button', { name: 'Publish' }).should('not.exist')
|
|
||||||
cy.findByRole('button', { name: 'Unpublish', timeout: 60_000 })
|
|
||||||
cy.findByRole('button', { name: 'Republish' })
|
|
||||||
|
|
||||||
cy.findByRole('link', { name: 'View it in the template gallery' }).click()
|
|
||||||
cy.url()
|
|
||||||
.should('match', /\/templates\/[a-f0-9]{24}$/)
|
|
||||||
.as('templateURL')
|
|
||||||
|
|
||||||
cy.findByRole('heading', { level: 2 }).findByText(name)
|
|
||||||
cy.findByText(description)
|
|
||||||
cy.findByRole('link', { name: 'Open as Template' })
|
|
||||||
cy.findByRole('button', { name: 'Unpublish' })
|
|
||||||
cy.findByRole('button', { name: 'Republish' })
|
|
||||||
cy.get('img')
|
|
||||||
.should('have.attr', 'src')
|
|
||||||
.and('match', /\/v\/0\//)
|
|
||||||
cy.findByRole('button', { name: 'Republish' }).click()
|
|
||||||
cy.findByRole('button', { name: 'Publishing…' }).should('be.disabled')
|
|
||||||
cy.findByRole('button', { name: 'Republish', timeout: 60_000 })
|
|
||||||
cy.get('img', { timeout: 60_000 })
|
|
||||||
.should('have.attr', 'src')
|
|
||||||
.and('match', /\/v\/1\//)
|
|
||||||
|
|
||||||
// custom tag
|
|
||||||
const tagName = `${Date.now()}`
|
|
||||||
cy.visit('/')
|
|
||||||
cy.findByRole('checkbox', { name: `Select ${name}` }).check()
|
|
||||||
cy.findByRole('navigation', { name: 'Project categories and tags' })
|
|
||||||
.findByRole('button', { name: 'New tag' })
|
|
||||||
.click()
|
|
||||||
cy.focused().type(tagName)
|
|
||||||
cy.findByRole('button', { name: 'Create' }).click()
|
|
||||||
cy.findByRole('navigation', {
|
|
||||||
name: 'Project categories and tags',
|
|
||||||
}).should('contain', `${tagName} (1)`)
|
|
||||||
|
|
||||||
// Check listing
|
|
||||||
cy.visit('/templates')
|
|
||||||
cy.findByRole('link', { name: tagName })
|
|
||||||
cy.visit('/templates/all')
|
|
||||||
cy.findByRole('heading', { name })
|
|
||||||
cy.visit(`/templates/${tagName}`)
|
|
||||||
cy.findByRole('heading', { name })
|
|
||||||
|
|
||||||
// Unpublish via template page
|
|
||||||
cy.get('@templateURL').then(url => cy.visit(`${url}`))
|
|
||||||
cy.findByRole('button', { name: 'Unpublish' }).click()
|
|
||||||
cy.url().should('match', /\/templates$/)
|
|
||||||
cy.get('@templateURL').then(url =>
|
|
||||||
cy.visit(`${url}`, {
|
|
||||||
failOnStatusCode: false,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
cy.findByRole('heading', { name: 'Not found' })
|
|
||||||
cy.visit('/templates/all')
|
|
||||||
cy.findByRole('heading', { name }).should('not.exist')
|
|
||||||
cy.visit(`/templates/${tagName}`)
|
|
||||||
cy.findByRole('heading', { name }).should('not.exist')
|
|
||||||
|
|
||||||
// Publish again
|
|
||||||
redirectEditorUrlWithQueryParams(false)
|
|
||||||
cy.get('@templateProjectId').then(projectId =>
|
|
||||||
cy.visit(`/project/${projectId}`)
|
|
||||||
)
|
|
||||||
cy.findByRole('navigation', {
|
|
||||||
name: 'Project actions',
|
|
||||||
})
|
|
||||||
.findByRole('button', { name: 'Menu' })
|
|
||||||
.click()
|
|
||||||
cy.findByRole('button', { name: 'Manage Template' }).click()
|
|
||||||
cy.findByRole('button', { name: 'Publish' }).click()
|
|
||||||
cy.findByRole('button', { name: 'Unpublish', timeout: 60_000 })
|
|
||||||
|
|
||||||
// Should assign a new template id
|
|
||||||
cy.findByRole('link', { name: 'View it in the template gallery' }).click()
|
|
||||||
cy.url()
|
|
||||||
.should('match', /\/templates\/[a-f0-9]{24}$/)
|
|
||||||
.as('newTemplateURL')
|
|
||||||
cy.get('@newTemplateURL').then(newURL => {
|
|
||||||
cy.get('@templateURL').then(prevURL => {
|
|
||||||
expect(newURL).to.match(/\/templates\/[a-f0-9]{24}$/)
|
|
||||||
expect(prevURL).to.not.equal(newURL)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// Open project from template
|
|
||||||
login(REGULAR_USER)
|
|
||||||
cy.visit('/templates')
|
|
||||||
cy.findByRole('link', { name: tagName }).click()
|
|
||||||
cy.findByRole('link', { name }).click()
|
|
||||||
redirectEditorUrlWithQueryParams(false)
|
|
||||||
cy.findByRole('link', { name: 'Open as Template' }).click()
|
|
||||||
cy.findByRole('navigation', { name: 'Project actions' }).findByText(
|
|
||||||
/Your Paper/i
|
|
||||||
) // might have (1) suffix
|
|
||||||
cy.findByRole('navigation', {
|
|
||||||
name: 'Project actions',
|
|
||||||
})
|
|
||||||
.findByRole('button', { name: 'Menu' })
|
|
||||||
.click()
|
|
||||||
cy.findByRole('button', { name: 'Word Count' }).click() // wait for lazy loading
|
|
||||||
cy.findByRole('button', { name: 'Manage Template' }).should('not.exist')
|
|
||||||
|
|
||||||
// Check management as regular user
|
|
||||||
cy.get('@newTemplateURL').then(url => cy.visit(`${url}`))
|
|
||||||
cy.findByRole('link', { name: 'Open as Template' })
|
|
||||||
cy.findByRole('button', { name: 'Unpublish' }).should('not.exist')
|
|
||||||
cy.findByRole('button', { name: 'Republish' }).should('not.exist')
|
|
||||||
|
|
||||||
// Check management as admin user
|
|
||||||
login(ADMIN_USER)
|
|
||||||
cy.get('@newTemplateURL').then(url => cy.visit(`${url}`))
|
|
||||||
cy.findByRole('link', { name: 'Open as Template' })
|
|
||||||
cy.findByRole('button', { name: 'Unpublish' })
|
|
||||||
cy.findByRole('button', { name: 'Republish' })
|
|
||||||
redirectEditorUrlWithQueryParams(false)
|
|
||||||
cy.get('@templateProjectId').then(projectId =>
|
|
||||||
cy.visit(`/project/${projectId}`)
|
|
||||||
)
|
|
||||||
cy.findByRole('navigation', {
|
|
||||||
name: 'Project actions',
|
|
||||||
})
|
|
||||||
.findByRole('button', { name: 'Menu' })
|
|
||||||
.click()
|
|
||||||
cy.findByRole('button', { name: 'Manage Template' }).click()
|
|
||||||
cy.findByRole('button', { name: 'Unpublish' })
|
|
||||||
|
|
||||||
// Back to templates user
|
|
||||||
login(TEMPLATES_USER)
|
|
||||||
|
|
||||||
// Unpublish via editor
|
|
||||||
redirectEditorUrlWithQueryParams(false)
|
|
||||||
cy.get('@templateProjectId').then(projectId =>
|
|
||||||
cy.visit(`/project/${projectId}`)
|
|
||||||
)
|
|
||||||
cy.findByRole('navigation', {
|
|
||||||
name: 'Project actions',
|
|
||||||
})
|
|
||||||
.findByRole('button', { name: 'Menu' })
|
|
||||||
.click()
|
|
||||||
cy.findByRole('button', { name: 'Manage Template' }).click()
|
|
||||||
cy.findByRole('button', { name: 'Unpublish' }).click()
|
|
||||||
cy.findByRole('button', { name: 'Publish' })
|
|
||||||
cy.visit('/templates/all')
|
|
||||||
cy.findByRole('link', { name }).should('not.exist')
|
|
||||||
|
|
||||||
// check for template links, after creating the first project
|
|
||||||
cy.visit('/')
|
|
||||||
cy.findAllByRole('button', { name: NEW_PROJECT_BUTTON_MATCHER }).click()
|
|
||||||
cy.findByRole('menuitem', { name: /All Templates/ }).should(
|
|
||||||
'have.attr',
|
|
||||||
'href',
|
|
||||||
'/templates/all'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
function checkDisabled() {
|
|
||||||
it('should not have templates feature', function () {
|
|
||||||
login(TEMPLATES_USER)
|
|
||||||
|
|
||||||
cy.visit('/')
|
|
||||||
createProject('maybe templates')
|
|
||||||
|
|
||||||
cy.findByRole('navigation', {
|
|
||||||
name: 'Project actions',
|
|
||||||
})
|
|
||||||
.findByRole('button', { name: 'Menu' })
|
|
||||||
.click()
|
|
||||||
cy.findByRole('button', { name: 'Word Count' }) // wait for lazy loading
|
|
||||||
cy.findByRole('button', { name: 'Manage Template' }).should('not.exist')
|
|
||||||
|
|
||||||
cy.visit('/templates', { failOnStatusCode: false })
|
|
||||||
cy.findByRole('heading', { name: 'Not found' })
|
|
||||||
cy.visit('/templates/all', { failOnStatusCode: false })
|
|
||||||
cy.findByRole('heading', { name: 'Not found' })
|
|
||||||
|
|
||||||
// check for template links, after creating the first project
|
|
||||||
cy.visit('/')
|
|
||||||
cy.findAllByRole('button', { name: NEW_PROJECT_BUTTON_MATCHER }).click()
|
|
||||||
cy.findByRole('menuitem', { name: /All Templates/ }).should('not.exist')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should not show templates link on welcome page', function () {
|
|
||||||
login(WITHOUT_PROJECTS_USER)
|
|
||||||
cy.visit('/')
|
|
||||||
cy.findByRole('button', { name: NEW_PROJECT_BUTTON_MATCHER }) // wait for lazy loading
|
|
||||||
cy.findByRole('link', { name: LABEL_BROWSE_TEMPLATES }).should(
|
|
||||||
'not.exist'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('disabled Server Pro', function () {
|
|
||||||
if (isExcludedBySharding('PRO_DEFAULT_2')) return
|
|
||||||
startWith({ pro: true })
|
|
||||||
checkDisabled()
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('unavailable in CE', function () {
|
|
||||||
if (isExcludedBySharding('CE_CUSTOM_1')) return
|
|
||||||
startWith({
|
|
||||||
pro: false,
|
|
||||||
varsFn,
|
|
||||||
})
|
|
||||||
checkDisabled()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,12 +1,7 @@
|
|||||||
import { useUserSettingsContext } from '@/shared/context/user-settings-context'
|
import { useUserSettingsContext } from '@/shared/context/user-settings-context'
|
||||||
|
|
||||||
// For e2e tests purposes, allow overriding to old editor
|
|
||||||
export const oldEditorOverride =
|
|
||||||
new URLSearchParams(window.location.search).get('old-editor-override') ===
|
|
||||||
'true'
|
|
||||||
|
|
||||||
export const canUseNewEditor = () => {
|
export const canUseNewEditor = () => {
|
||||||
return !oldEditorOverride
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useIsNewEditorEnabled = () => {
|
export const useIsNewEditorEnabled = () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user