mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #15547 from overleaf/mj-community-edition-tests
[server-ce] Add e2e test for CE GitOrigin-RevId: f76ee4d19680c57a3a0854bc89175b3fb352ca41
This commit is contained in:
parent
21c61be543
commit
732cbf0c26
14 changed files with 3023 additions and 0 deletions
8
server-ce/test/Makefile
Normal file
8
server-ce/test/Makefile
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
all: test-e2e
|
||||||
|
|
||||||
|
test-e2e:
|
||||||
|
docker-compose down -v -t 0
|
||||||
|
docker-compose -f docker-compose.yml run --rm e2e
|
||||||
|
docker-compose down -v -t 0
|
||||||
|
|
||||||
|
.PHONY: test-e2e
|
10
server-ce/test/accounts.spec.ts
Normal file
10
server-ce/test/accounts.spec.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { login } from './helpers/login'
|
||||||
|
|
||||||
|
describe('Accounts', function () {
|
||||||
|
it('can log in and out', function () {
|
||||||
|
login('user@example.com')
|
||||||
|
cy.visit('/project')
|
||||||
|
cy.findByText('Account').click()
|
||||||
|
cy.findByText('Log Out').click()
|
||||||
|
})
|
||||||
|
})
|
136
server-ce/test/create-and-compile-project.spec.ts
Normal file
136
server-ce/test/create-and-compile-project.spec.ts
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
import { login } from './helpers/login'
|
||||||
|
import { createProject } from './helpers/project'
|
||||||
|
|
||||||
|
describe('Project creation and compilation', function () {
|
||||||
|
it('users can create project and compile it', function () {
|
||||||
|
login('user@example.com')
|
||||||
|
cy.visit('/project')
|
||||||
|
createProject('test-project')
|
||||||
|
cy.url().should('match', /\/project\/[a-fA-F0-9]{24}/)
|
||||||
|
cy.findByText('\\maketitle')
|
||||||
|
.parent()
|
||||||
|
.click()
|
||||||
|
.type('\n\\section{{}Test Section}')
|
||||||
|
// Wait for the PDF compilation throttling
|
||||||
|
cy.wait(3000)
|
||||||
|
cy.findByText('Recompile').click()
|
||||||
|
cy.get('.pdf-viewer').should('contain.text', 'Test Section')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('create and edit markdown file', function () {
|
||||||
|
const fileName = `test-${Date.now()}.md`
|
||||||
|
const markdownContent = '# Markdown title'
|
||||||
|
login('user@example.com')
|
||||||
|
cy.visit('/project')
|
||||||
|
createProject('test-project')
|
||||||
|
// FIXME: Add aria-label maybe? or at least data-test-id
|
||||||
|
cy.findByText('New File').click({ force: true })
|
||||||
|
cy.findByRole('dialog').within(() => {
|
||||||
|
cy.get('input').clear()
|
||||||
|
cy.get('input').type(fileName)
|
||||||
|
cy.findByText('Create').click()
|
||||||
|
})
|
||||||
|
cy.findByText(fileName).click()
|
||||||
|
// wait until we've switched to the newly created empty file
|
||||||
|
cy.get('.cm-line').should('have.length', 1)
|
||||||
|
cy.get('.cm-line').type(markdownContent)
|
||||||
|
cy.findByText('main.tex').click()
|
||||||
|
cy.get('.cm-content').should('contain.text', '\\maketitle')
|
||||||
|
cy.findByText(fileName).click()
|
||||||
|
cy.get('.cm-content').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@example.com')
|
||||||
|
|
||||||
|
cy.visit('/project')
|
||||||
|
createProject(sourceProjectName, { type: 'Example Project' }).as(
|
||||||
|
'sourceProjectId'
|
||||||
|
)
|
||||||
|
|
||||||
|
cy.visit('/project')
|
||||||
|
createProject(targetProjectName)
|
||||||
|
|
||||||
|
// link the image from `projectName` into this project
|
||||||
|
cy.findByText('New File').click({ force: true })
|
||||||
|
cy.findByRole('dialog').within(() => {
|
||||||
|
cy.findByText('From Another Project').click()
|
||||||
|
cy.findByLabelText('Select a Project').select(sourceProjectName)
|
||||||
|
cy.findByLabelText('Select a File').select('frog.jpg')
|
||||||
|
cy.findByText('Create').click()
|
||||||
|
})
|
||||||
|
// FIXME: should be aria-labeled or data-test-id
|
||||||
|
cy.get('.file-tree').within(() => {
|
||||||
|
cy.findByText('frog.jpg').click()
|
||||||
|
})
|
||||||
|
cy.findByText('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@example.com')
|
||||||
|
|
||||||
|
cy.visit('/project')
|
||||||
|
createProject(sourceProjectName, { type: 'Example Project' }).as(
|
||||||
|
'sourceProjectId'
|
||||||
|
)
|
||||||
|
|
||||||
|
cy.visit('/project')
|
||||||
|
createProject(targetProjectName).as('targetProjectId')
|
||||||
|
|
||||||
|
// link the image from `projectName` into this project
|
||||||
|
cy.findByText('New File').click({ force: true })
|
||||||
|
cy.findByRole('dialog').within(() => {
|
||||||
|
cy.findByText('From Another Project').click()
|
||||||
|
cy.findByLabelText('Select a Project').select(sourceProjectName)
|
||||||
|
cy.findByLabelText('Select a File').select('frog.jpg')
|
||||||
|
cy.findByText('Create').click()
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.findByText('Share').click()
|
||||||
|
cy.findByRole('dialog').within(() => {
|
||||||
|
cy.get('input').type('collaborator@example.com,')
|
||||||
|
// FIXME: Open an issue for this.
|
||||||
|
cy.get('button[type="submit"]').click({ force: true })
|
||||||
|
cy.get('button[type="submit"]').click({ force: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.visit('/project')
|
||||||
|
cy.findByText('Account').click()
|
||||||
|
cy.findByText('Log Out').click()
|
||||||
|
|
||||||
|
login('collaborator@example.com')
|
||||||
|
cy.visit('/project')
|
||||||
|
// FIXME: Should have data-test-id
|
||||||
|
cy.findByText(targetProjectName)
|
||||||
|
.parent()
|
||||||
|
.parent()
|
||||||
|
.find('button.btn-info')
|
||||||
|
.click()
|
||||||
|
cy.findByText('Open Project').click()
|
||||||
|
cy.url().should('match', /\/project\/[a-fA-F0-9]{24}/)
|
||||||
|
cy.get('@targetProjectId').then(targetProjectId => {
|
||||||
|
cy.url().should('include', targetProjectId)
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.get('.file-tree').within(() => {
|
||||||
|
cy.findByText('frog.jpg').click()
|
||||||
|
})
|
||||||
|
cy.findByText('Another project')
|
||||||
|
.should('have.attr', 'href')
|
||||||
|
.then(href => {
|
||||||
|
cy.get('@sourceProjectId').then(sourceProjectId => {
|
||||||
|
expect(href).to.equal(`/project/${sourceProjectId}`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
23
server-ce/test/cypress.config.js
Normal file
23
server-ce/test/cypress.config.js
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
const { defineConfig } = require('cypress')
|
||||||
|
|
||||||
|
const specPattern = process.env.SPEC_PATTERN || './**/*.spec.{js,ts,tsx}'
|
||||||
|
|
||||||
|
module.exports = defineConfig({
|
||||||
|
fixturesFolder: 'cypress/fixtures',
|
||||||
|
video: !!process.env.CI,
|
||||||
|
screenshotsFolder: 'cypress/results',
|
||||||
|
videosFolder: 'cypress/results',
|
||||||
|
videoUploadOnPasses: false,
|
||||||
|
viewportHeight: 768,
|
||||||
|
viewportWidth: 1024,
|
||||||
|
e2e: {
|
||||||
|
baseUrl: 'http://localhost',
|
||||||
|
setupNodeEvents(on, config) {
|
||||||
|
// implement node event listeners here
|
||||||
|
},
|
||||||
|
specPattern,
|
||||||
|
},
|
||||||
|
retries: {
|
||||||
|
runMode: 1,
|
||||||
|
},
|
||||||
|
})
|
1
server-ce/test/cypress/.gitignore
vendored
Normal file
1
server-ce/test/cypress/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
results/
|
1
server-ce/test/cypress/support/e2e.js
Normal file
1
server-ce/test/cypress/support/e2e.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
import '@testing-library/cypress/add-commands'
|
64
server-ce/test/docker-compose.yml
Normal file
64
server-ce/test/docker-compose.yml
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
version: '2.2'
|
||||||
|
services:
|
||||||
|
sharelatex:
|
||||||
|
image: ${IMAGE_TAG:-sharelatex/sharelatex:latest}
|
||||||
|
container_name: sharelatex
|
||||||
|
depends_on:
|
||||||
|
mongo:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_started
|
||||||
|
ports:
|
||||||
|
- 80:80
|
||||||
|
links:
|
||||||
|
- mongo
|
||||||
|
- redis
|
||||||
|
environment:
|
||||||
|
SHARELATEX_APP_NAME: Overleaf Community Edition
|
||||||
|
SHARELATEX_MONGO_URL: mongodb://mongo/sharelatex?directConnection=true
|
||||||
|
SHARELATEX_REDIS_HOST: redis
|
||||||
|
REDIS_HOST: redis
|
||||||
|
ENABLED_LINKED_FILE_TYPES: 'project_file,project_output_file'
|
||||||
|
ENABLE_CONVERSIONS: 'true'
|
||||||
|
EMAIL_CONFIRMATION_DISABLED: 'true'
|
||||||
|
healthcheck:
|
||||||
|
test: curl --fail http://localhost:3000/status || exit 1
|
||||||
|
interval: 10s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 10
|
||||||
|
volumes:
|
||||||
|
- ./util/seed-mongo.sh:/etc/my_init.d/99_seed-mongo.sh
|
||||||
|
- ./util/seed-mongo.js:/overleaf/services/web/modules/server-ce-scripts/scripts/seed-mongo.js
|
||||||
|
|
||||||
|
mongo:
|
||||||
|
image: mongo:5.0.17
|
||||||
|
container_name: mongo
|
||||||
|
command: '--replSet overleaf'
|
||||||
|
expose:
|
||||||
|
- 27017
|
||||||
|
healthcheck:
|
||||||
|
# FIXME: silly hack to make sure replicaset is initialized
|
||||||
|
test: 'echo ''rs.initiate({ _id: "overleaf", members: [{ _id: 0, host: "mongo:27017" }] })'' | mongo localhost:27017/test --quiet'
|
||||||
|
interval: 10s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7.2.1
|
||||||
|
container_name: redis
|
||||||
|
expose:
|
||||||
|
- 6379
|
||||||
|
|
||||||
|
e2e:
|
||||||
|
image: cypress/included:13.3.0
|
||||||
|
links:
|
||||||
|
- sharelatex
|
||||||
|
working_dir: /e2e
|
||||||
|
volumes:
|
||||||
|
- ./:/e2e
|
||||||
|
environment:
|
||||||
|
CYPRESS_BASE_URL: http://sharelatex
|
||||||
|
SPEC_PATTERN: '**/*.spec.{js,jsx,ts,tsx}'
|
||||||
|
depends_on:
|
||||||
|
sharelatex:
|
||||||
|
condition: service_healthy
|
9
server-ce/test/helpers/login.ts
Normal file
9
server-ce/test/helpers/login.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
export function login(username: string, password = 'Passw0rd!') {
|
||||||
|
cy.session([username, password, new Date()], () => {
|
||||||
|
cy.visit('/login')
|
||||||
|
cy.get('input[name="email"]').type(username)
|
||||||
|
cy.get('input[name="password"]').type(password)
|
||||||
|
cy.findByRole('button', { name: 'Login' }).click()
|
||||||
|
cy.url().should('contain', '/project')
|
||||||
|
})
|
||||||
|
}
|
19
server-ce/test/helpers/project.ts
Normal file
19
server-ce/test/helpers/project.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
export function createProject(
|
||||||
|
name: string,
|
||||||
|
{
|
||||||
|
type = 'Blank Project',
|
||||||
|
}: { type?: 'Blank Project' | 'Example Project' } = {}
|
||||||
|
): Cypress.Chainable<string> {
|
||||||
|
// FIXME: This should be be a data-test-id shared between the welcome page and project list
|
||||||
|
cy.get('.new-project-button').first().click()
|
||||||
|
// FIXME: This should only look in the left menu
|
||||||
|
cy.findAllByText(type).first().click()
|
||||||
|
cy.findByRole('dialog').within(() => {
|
||||||
|
cy.get('input').type(name)
|
||||||
|
cy.findByText('Create').click()
|
||||||
|
})
|
||||||
|
return cy
|
||||||
|
.url()
|
||||||
|
.should('match', /\/project\/[a-fA-F0-9]{24}/)
|
||||||
|
.then(url => url.split('/').pop())
|
||||||
|
}
|
2590
server-ce/test/package-lock.json
generated
Normal file
2590
server-ce/test/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
16
server-ce/test/package.json
Normal file
16
server-ce/test/package.json
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"name": "@overleaf/server-ce/test",
|
||||||
|
"description": "e2e tests for Overleaf Community Edition",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"cypress:open": "cypress open",
|
||||||
|
"cypress:run": "cypress run",
|
||||||
|
"format": "prettier --list-different $PWD/'**/*.{js,mjs,ts,tsx,json}'",
|
||||||
|
"format:fix": "prettier --write $PWD/'**/*.{js,mjs,ts,tsx,json}'"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@testing-library/cypress": "^10.0.1",
|
||||||
|
"cypress": "13.3.0",
|
||||||
|
"typescript": "^5.0.4"
|
||||||
|
}
|
||||||
|
}
|
17
server-ce/test/tsconfig.json
Normal file
17
server-ce/test/tsconfig.json
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "esnext" /* Specify ECMAScript target version */,
|
||||||
|
"module": "es2020" /* Specify module code generation */,
|
||||||
|
"allowJs": true /* Allow JavaScript files to be compiled. */,
|
||||||
|
// "checkJs": true /* Report errors in .js files. */,
|
||||||
|
"jsx": "preserve" /* Specify JSX code generation */,
|
||||||
|
"noEmit": true /* Do not emit outputs. */,
|
||||||
|
"strict": true /* Enable all strict type-checking options. */,
|
||||||
|
"moduleResolution": "node" /* Specify module resolution strategy */,
|
||||||
|
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
|
||||||
|
"skipLibCheck": true /* Skip type checking of declaration files. */,
|
||||||
|
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */,
|
||||||
|
"types": ["cypress", "node", "@testing-library/cypress"]
|
||||||
|
},
|
||||||
|
"include": ["**/*.ts", "**/*.tsx"]
|
||||||
|
}
|
121
server-ce/test/util/seed-mongo.js
Normal file
121
server-ce/test/util/seed-mongo.js
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
const { ObjectId } = require('mongodb')
|
||||||
|
const { waitForDb, db } = require('../../../app/src/infrastructure/mongodb')
|
||||||
|
|
||||||
|
waitForDb()
|
||||||
|
.then(async () => {
|
||||||
|
await seedUsers()
|
||||||
|
process.exit(0)
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
const DEFAULT_USER_PROPERTIES = {
|
||||||
|
staffAccess: {
|
||||||
|
publisherMetrics: false,
|
||||||
|
publisherManagement: false,
|
||||||
|
institutionMetrics: false,
|
||||||
|
institutionManagement: false,
|
||||||
|
groupMetrics: false,
|
||||||
|
groupManagement: false,
|
||||||
|
adminMetrics: false,
|
||||||
|
splitTestMetrics: false,
|
||||||
|
splitTestManagement: false,
|
||||||
|
},
|
||||||
|
ace: {
|
||||||
|
mode: 'none',
|
||||||
|
theme: 'textmate',
|
||||||
|
overallTheme: '',
|
||||||
|
fontSize: 12,
|
||||||
|
autoComplete: true,
|
||||||
|
autoPairDelimiters: true,
|
||||||
|
spellCheckLanguage: 'en',
|
||||||
|
pdfViewer: 'pdfjs',
|
||||||
|
syntaxValidation: true,
|
||||||
|
},
|
||||||
|
features: {
|
||||||
|
collaborators: -1,
|
||||||
|
versioning: true,
|
||||||
|
dropbox: true,
|
||||||
|
github: true,
|
||||||
|
gitBridge: true,
|
||||||
|
compileTimeout: 180,
|
||||||
|
compileGroup: 'standard',
|
||||||
|
templates: true,
|
||||||
|
references: true,
|
||||||
|
trackChanges: true,
|
||||||
|
},
|
||||||
|
first_name: 'user',
|
||||||
|
role: '',
|
||||||
|
institution: '',
|
||||||
|
isAdmin: false,
|
||||||
|
lastLoginIp: '',
|
||||||
|
loginCount: 0,
|
||||||
|
holdingAccount: false,
|
||||||
|
must_reconfirm: false,
|
||||||
|
refered_users: [],
|
||||||
|
refered_user_count: 0,
|
||||||
|
alphaProgram: false,
|
||||||
|
betaProgram: false,
|
||||||
|
labsProgram: false,
|
||||||
|
awareOfV2: false,
|
||||||
|
samlIdentifiers: [],
|
||||||
|
thirdPartyIdentifiers: [],
|
||||||
|
|
||||||
|
signUpDate: new Date('2023-11-02T11:36:40.151Z'),
|
||||||
|
featuresOverrides: [],
|
||||||
|
referal_id: 'scTS4kjjJENbfbjG',
|
||||||
|
__v: 0,
|
||||||
|
hashedPassword:
|
||||||
|
'$2a$12$nRvTj6U896uUnE.RFhnGKOyi/CvqBpfxezlqwyIPpezRa2xXLW7MO',
|
||||||
|
}
|
||||||
|
|
||||||
|
async function seedUsers() {
|
||||||
|
const adminUser = {
|
||||||
|
...DEFAULT_USER_PROPERTIES,
|
||||||
|
email: 'admin@example.com',
|
||||||
|
first_name: 'admin',
|
||||||
|
isAdmin: true,
|
||||||
|
emails: [
|
||||||
|
{
|
||||||
|
email: 'admin@example.com',
|
||||||
|
reversedHostname: '',
|
||||||
|
_id: ObjectId('646ca54806d54400b74e77c6'),
|
||||||
|
createdAt: new Date('2023-05-23T11:36:40.494Z'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = {
|
||||||
|
...DEFAULT_USER_PROPERTIES,
|
||||||
|
_id: ObjectId('6543cf90bbe1368944db04d7'),
|
||||||
|
emails: [
|
||||||
|
{
|
||||||
|
email: 'user@example.com',
|
||||||
|
reversedHostname: '',
|
||||||
|
_id: ObjectId('6543c614d58b090b461f3549'),
|
||||||
|
createdAt: new Date('2023-11-21T11:36:40.494Z'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
email: 'user@example.com',
|
||||||
|
}
|
||||||
|
|
||||||
|
const collaborator = {
|
||||||
|
...DEFAULT_USER_PROPERTIES,
|
||||||
|
_id: ObjectId('6544e78c9b6e937424976b64'),
|
||||||
|
emails: [
|
||||||
|
{
|
||||||
|
email: 'collaborator@example.com',
|
||||||
|
reversedHostname: '',
|
||||||
|
_id: ObjectId('6543c614d58b090b461f354a'),
|
||||||
|
createdAt: new Date('2023-11-21T11:36:40.494Z'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
email: 'collaborator@example.com',
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.users.insertOne(adminUser)
|
||||||
|
await db.users.insertOne(user)
|
||||||
|
await db.users.insertOne(collaborator)
|
||||||
|
}
|
8
server-ce/test/util/seed-mongo.sh
Executable file
8
server-ce/test/util/seed-mongo.sh
Executable file
|
@ -0,0 +1,8 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "Seeding mongo for e2e tests"
|
||||||
|
cd /overleaf/services/web
|
||||||
|
node modules/server-ce-scripts/scripts/seed-mongo
|
||||||
|
node modules/server-ce-scripts/scripts/check-redis
|
||||||
|
echo "mongo seeding complete"
|
Loading…
Reference in a new issue