Bladeren bron

Merge pull request #29038 from overleaf/td-eslint-e2e-tests

Enable ESLint for all end-to-end tests

GitOrigin-RevId: 5d085f52fabcc794b0457edbbb551500477d4110
Tim Down 8 maanden geleden
bovenliggende
commit
41d120d8f1

+ 67 - 78
package-lock.json

@@ -13174,32 +13174,35 @@
       ]
     },
     "node_modules/@napi-rs/canvas": {
-      "version": "0.1.68",
-      "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.68.tgz",
-      "integrity": "sha512-LQESrePLEBLvhuFkXx9jjBXRC2ClYsO5mqQ1m/puth5z9SOuM3N/B3vDuqnC3RJFktDktyK9khGvo7dTkqO9uQ==",
+      "version": "0.1.80",
+      "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.80.tgz",
+      "integrity": "sha512-DxuT1ClnIPts1kQx8FBmkk4BQDTfI5kIzywAaMjQSXfNnra5UFU9PwurXrl+Je3bJ6BGsp/zmshVVFbCmyI+ww==",
       "dev": true,
       "license": "MIT",
       "optional": true,
+      "workspaces": [
+        "e2e/*"
+      ],
       "engines": {
         "node": ">= 10"
       },
       "optionalDependencies": {
-        "@napi-rs/canvas-android-arm64": "0.1.68",
-        "@napi-rs/canvas-darwin-arm64": "0.1.68",
-        "@napi-rs/canvas-darwin-x64": "0.1.68",
-        "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.68",
-        "@napi-rs/canvas-linux-arm64-gnu": "0.1.68",
-        "@napi-rs/canvas-linux-arm64-musl": "0.1.68",
-        "@napi-rs/canvas-linux-riscv64-gnu": "0.1.68",
-        "@napi-rs/canvas-linux-x64-gnu": "0.1.68",
-        "@napi-rs/canvas-linux-x64-musl": "0.1.68",
-        "@napi-rs/canvas-win32-x64-msvc": "0.1.68"
+        "@napi-rs/canvas-android-arm64": "0.1.80",
+        "@napi-rs/canvas-darwin-arm64": "0.1.80",
+        "@napi-rs/canvas-darwin-x64": "0.1.80",
+        "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.80",
+        "@napi-rs/canvas-linux-arm64-gnu": "0.1.80",
+        "@napi-rs/canvas-linux-arm64-musl": "0.1.80",
+        "@napi-rs/canvas-linux-riscv64-gnu": "0.1.80",
+        "@napi-rs/canvas-linux-x64-gnu": "0.1.80",
+        "@napi-rs/canvas-linux-x64-musl": "0.1.80",
+        "@napi-rs/canvas-win32-x64-msvc": "0.1.80"
       }
     },
     "node_modules/@napi-rs/canvas-android-arm64": {
-      "version": "0.1.68",
-      "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.68.tgz",
-      "integrity": "sha512-h1KcSR4LKLfRfzeBH65xMxbWOGa1OtMFQbCMVlxPCkN1Zr+2gK+70pXO5ktojIYcUrP6KDcOwoc8clho5ccM/w==",
+      "version": "0.1.80",
+      "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.80.tgz",
+      "integrity": "sha512-sk7xhN/MoXeuExlggf91pNziBxLPVUqF2CAVnB57KLG/pz7+U5TKG8eXdc3pm0d7Od0WreB6ZKLj37sX9muGOQ==",
       "cpu": [
         "arm64"
       ],
@@ -13214,9 +13217,9 @@
       }
     },
     "node_modules/@napi-rs/canvas-darwin-arm64": {
-      "version": "0.1.68",
-      "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.68.tgz",
-      "integrity": "sha512-/VURlrAD4gDoxW1GT/b0nP3fRz/fhxmHI/xznTq2FTwkQLPOlLkDLCvTmQ7v6LtGKdc2Ed6rvYpRan+JXThInQ==",
+      "version": "0.1.80",
+      "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.80.tgz",
+      "integrity": "sha512-O64APRTXRUiAz0P8gErkfEr3lipLJgM6pjATwavZ22ebhjYl/SUbpgM0xcWPQBNMP1n29afAC/Us5PX1vg+JNQ==",
       "cpu": [
         "arm64"
       ],
@@ -13231,9 +13234,9 @@
       }
     },
     "node_modules/@napi-rs/canvas-darwin-x64": {
-      "version": "0.1.68",
-      "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.68.tgz",
-      "integrity": "sha512-tEpvGR6vCLTo1Tx9wmDnoOKROpw57wiCWwCpDOuVlj/7rqEJOUYr9ixW4aRJgmeGBrZHgevI0EURys2ER6whmg==",
+      "version": "0.1.80",
+      "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.80.tgz",
+      "integrity": "sha512-FqqSU7qFce0Cp3pwnTjVkKjjOtxMqRe6lmINxpIZYaZNnVI0H5FtsaraZJ36SiTHNjZlUB69/HhxNDT1Aaa9vA==",
       "cpu": [
         "x64"
       ],
@@ -13248,9 +13251,9 @@
       }
     },
     "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": {
-      "version": "0.1.68",
-      "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.68.tgz",
-      "integrity": "sha512-U9xbJsumPOiAYeAFZMlHf62b9dGs2HJ6Q5xt7xTB0uEyPeurwhgYBWGgabdsEidyj38YuzI/c3LGBbSQB3vagw==",
+      "version": "0.1.80",
+      "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.80.tgz",
+      "integrity": "sha512-eyWz0ddBDQc7/JbAtY4OtZ5SpK8tR4JsCYEZjCE3dI8pqoWUC8oMwYSBGCYfsx2w47cQgQCgMVRVTFiiO38hHQ==",
       "cpu": [
         "arm"
       ],
@@ -13265,9 +13268,9 @@
       }
     },
     "node_modules/@napi-rs/canvas-linux-arm64-gnu": {
-      "version": "0.1.68",
-      "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.68.tgz",
-      "integrity": "sha512-KFkn8wEm3mPnWD4l8+OUUkxylSJuN5q9PnJRZJgv15RtCA1bgxIwTkBhI/+xuyVMcHqON9sXq7cDkEJtHm35dg==",
+      "version": "0.1.80",
+      "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.80.tgz",
+      "integrity": "sha512-qwA63t8A86bnxhuA/GwOkK3jvb+XTQaTiVML0vAWoHyoZYTjNs7BzoOONDgTnNtr8/yHrq64XXzUoLqDzU+Uuw==",
       "cpu": [
         "arm64"
       ],
@@ -13282,9 +13285,9 @@
       }
     },
     "node_modules/@napi-rs/canvas-linux-arm64-musl": {
-      "version": "0.1.68",
-      "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.68.tgz",
-      "integrity": "sha512-IQzts91rCdOALXBWQxLZRCEDrfFTGDtNRJMNu+2SKZ1uT8cmPQkPwVk5rycvFpvgAcmiFiOSCp1aRrlfU8KPpQ==",
+      "version": "0.1.80",
+      "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.80.tgz",
+      "integrity": "sha512-1XbCOz/ymhj24lFaIXtWnwv/6eFHXDrjP0jYkc6iHQ9q8oXKzUX1Lc6bu+wuGiLhGh2GS/2JlfORC5ZcXimRcg==",
       "cpu": [
         "arm64"
       ],
@@ -13299,9 +13302,9 @@
       }
     },
     "node_modules/@napi-rs/canvas-linux-riscv64-gnu": {
-      "version": "0.1.68",
-      "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.68.tgz",
-      "integrity": "sha512-e9AS5UttoIKqXSmBzKZdd3NErSVyOEYzJfNOCGtafGk1//gibTwQXGlSXmAKuErqMp09pyk9aqQRSYzm1AQfBw==",
+      "version": "0.1.80",
+      "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.80.tgz",
+      "integrity": "sha512-XTzR125w5ZMs0lJcxRlS1K3P5RaZ9RmUsPtd1uGt+EfDyYMu4c6SEROYsxyatbbu/2+lPe7MPHOO/0a0x7L/gw==",
       "cpu": [
         "riscv64"
       ],
@@ -13316,9 +13319,9 @@
       }
     },
     "node_modules/@napi-rs/canvas-linux-x64-gnu": {
-      "version": "0.1.68",
-      "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.68.tgz",
-      "integrity": "sha512-Pa/I36VE3j57I3Obhrr+J48KGFfkZk2cJN/2NmW/vCgmoF7kCP6aTVq5n+cGdGWLd/cN9CJ9JvNwEoMRDghu0g==",
+      "version": "0.1.80",
+      "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.80.tgz",
+      "integrity": "sha512-BeXAmhKg1kX3UCrJsYbdQd3hIMDH/K6HnP/pG2LuITaXhXBiNdh//TVVVVCBbJzVQaV5gK/4ZOCMrQW9mvuTqA==",
       "cpu": [
         "x64"
       ],
@@ -13333,9 +13336,9 @@
       }
     },
     "node_modules/@napi-rs/canvas-linux-x64-musl": {
-      "version": "0.1.68",
-      "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.68.tgz",
-      "integrity": "sha512-9c6rkc5195wNxuUHJdf4/mmnq433OQey9TNvQ9LspJazvHbfSkTij8wtKjASVQsJyPDva4fkWOeV/OQ7cLw0GQ==",
+      "version": "0.1.80",
+      "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.80.tgz",
+      "integrity": "sha512-x0XvZWdHbkgdgucJsRxprX/4o4sEed7qo9rCQA9ugiS9qE2QvP0RIiEugtZhfLH3cyI+jIRFJHV4Fuz+1BHHMg==",
       "cpu": [
         "x64"
       ],
@@ -13350,9 +13353,9 @@
       }
     },
     "node_modules/@napi-rs/canvas-win32-x64-msvc": {
-      "version": "0.1.68",
-      "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.68.tgz",
-      "integrity": "sha512-Fc5Dez23u0FoSATurT6/w1oMytiRnKWEinHivdMvXpge6nG4YvhrASrtqMk8dGJMVQpHr8QJYF45rOrx2YU2Aw==",
+      "version": "0.1.80",
+      "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.80.tgz",
+      "integrity": "sha512-Z8jPsM6df5V8B1HrCHB05+bDiCxjE9QA//3YrkKIdVDEwn5RKaqOxCJDRJkl48cJbylcrJbW4HxZbTte8juuPg==",
       "cpu": [
         "x64"
       ],
@@ -20010,16 +20013,6 @@
       "dev": true,
       "license": "MIT"
     },
-    "node_modules/@types/pdf-parse": {
-      "version": "1.1.5",
-      "resolved": "https://registry.npmjs.org/@types/pdf-parse/-/pdf-parse-1.1.5.tgz",
-      "integrity": "sha512-kBfrSXsloMnUJOKi25s3+hRmkycHfLK6A09eRGqF/N8BkQoPUmaCr+q8Cli5FnfohEz/rsv82zAiPz/LXtOGhA==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "@types/node": "*"
-      }
-    },
     "node_modules/@types/pg": {
       "version": "8.6.1",
       "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.6.1.tgz",
@@ -39620,13 +39613,6 @@
         "node": ">=10.5.0"
       }
     },
-    "node_modules/node-ensure": {
-      "version": "0.0.0",
-      "resolved": "https://registry.npmjs.org/node-ensure/-/node-ensure-0.0.0.tgz",
-      "integrity": "sha512-DRI60hzo2oKN1ma0ckc6nQWlHU69RH6xN0sjQTjMpChPfTYvKZdcQFfdYK2RWbJcKyUizSIy/l8OTGxMAM1QDw==",
-      "dev": true,
-      "license": "MIT"
-    },
     "node_modules/node-fetch": {
       "version": "2.7.0",
       "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
@@ -41147,27 +41133,32 @@
       "integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10="
     },
     "node_modules/pdf-parse": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/pdf-parse/-/pdf-parse-1.1.1.tgz",
-      "integrity": "sha512-v6ZJ/efsBpGrGGknjtq9J/oC8tZWq0KWL5vQrk2GlzLEQPUDB1ex+13Rmidl1neNN358Jn9EHZw5y07FFtaC7A==",
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/pdf-parse/-/pdf-parse-2.3.0.tgz",
+      "integrity": "sha512-VRKvhqdZ694CjdR1vusZ7VIA7ZuMN/GQ7eKz+e3z9ujCQdCQMOEG9x6cHfq8ddS7XspXVrruWuKmXm8g0hFlSQ==",
       "dev": true,
-      "license": "MIT",
+      "license": "Apache-2.0",
       "dependencies": {
-        "debug": "^3.1.0",
-        "node-ensure": "^0.0.0"
+        "pdfjs-dist": "^5.4.296"
       },
       "engines": {
-        "node": ">=6.8.1"
+        "node": ">=20.16.0 <21 || >=22.3.0"
+      },
+      "optionalDependencies": {
+        "@napi-rs/canvas": "^0.1.80"
       }
     },
-    "node_modules/pdf-parse/node_modules/debug": {
-      "version": "3.2.7",
-      "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
-      "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+    "node_modules/pdf-parse/node_modules/pdfjs-dist": {
+      "version": "5.4.296",
+      "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.296.tgz",
+      "integrity": "sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q==",
       "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "ms": "^2.1.1"
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=20.16.0 || >=22.3.0"
+      },
+      "optionalDependencies": {
+        "@napi-rs/canvas": "^0.1.80"
       }
     },
     "node_modules/pdfjs-dist": {
@@ -53269,7 +53260,6 @@
         "@overleaf/validation-tools": "*",
         "@testing-library/cypress": "^10.0.3",
         "@types/adm-zip": "^0.5.7",
-        "@types/pdf-parse": "^1.1.5",
         "@types/uuid": "^9.0.8",
         "adm-zip": "^0.5.12",
         "body-parser": "^1.20.3",
@@ -53279,7 +53269,7 @@
         "isomorphic-git": "^1.33.1",
         "js-yaml": "^4.1.1",
         "mocha-junit-reporter": "^2.2.1",
-        "pdf-parse": "^1.1.1",
+        "pdf-parse": "^2.3.0",
         "uuid": "^9.0.1",
         "zod-validation-error": "^4.0.1"
       }
@@ -56646,7 +56636,6 @@
         "@isomorphic-git/lightning-fs": "^4.6.0",
         "@testing-library/cypress": "^10.0.3",
         "@types/adm-zip": "^0.5.7",
-        "@types/pdf-parse": "^1.1.5",
         "@types/uuid": "^9.0.8",
         "adm-zip": "^0.5.12",
         "cypress": "13.13.2",
@@ -56654,7 +56643,7 @@
         "isomorphic-git": "^1.33.1",
         "mailtrap": "^4.3.0",
         "mocha-junit-reporter": "^2.2.1",
-        "pdf-parse": "^1.1.1",
+        "pdf-parse": "^2.3.0",
         "typescript": "^5.0.4",
         "uuid": "^9.0.1"
       }

+ 4 - 0
server-ce/.eslintrc

@@ -21,5 +21,9 @@
   "overrides": [
     // Extra rules for Cypress tests
     { "files": ["**/*.spec.ts"], "extends": ["plugin:cypress/recommended"] }
+  ],
+  "ignorePatterns": [
+    "hotfix/",
+    "develop/"
   ]
 }

+ 3 - 3
server-ce/config/settings.js

@@ -14,7 +14,7 @@
  */
 let redisConfig, siteUrl
 let e
-const Path = require('path')
+const Path = require('node:path')
 
 // These credentials are used for authenticating api requests
 // between services that may need to go over public channels
@@ -491,9 +491,9 @@ if (
 // With lots of incoming and outgoing HTTP connections to different services,
 // sometimes long running, it is a good idea to increase the default number
 // of sockets that Node will hold open.
-const http = require('http')
+const http = require('node:http')
 http.globalAgent.maxSockets = 300
-const https = require('https')
+const https = require('node:https')
 https.globalAgent.maxSockets = 300
 
 module.exports = settings

+ 19 - 1
server-ce/test/Jenkinsfile

@@ -75,6 +75,24 @@ pipeline {
             sh 'bin/run -w /overleaf/server-ce/test monorepo npm run format'
           }
         }
+        stage('Lint') {
+          steps {
+            script {
+              waitUntil {
+                return job_npm_install_done
+              }
+            }
+            sh 'bin/run -w /overleaf/server-ce/test monorepo npm run lint -- --format json --output-file reports/eslint.json'
+          }
+          post {
+            always {
+              sh """
+                sed -i 's_"filePath":"/overleaf_"filePath":"/workspace_g' server-ce/test/reports/eslint.json
+              """
+              recordIssues checksAnnotationScope: 'ALL', enabledForFailure: true, failOnError: true, id: 'server-pro-e2e-tests-eslint', name: 'Server-Pro-E2E-Tests eslint', qualityGates: [[integerThreshold: 1, threshold: 1.0, type: 'TOTAL']], sourceCodeRetention: 'LAST_BUILD', tools: [esLint(pattern: 'server-ce/test/reports/eslint.json')]
+            }
+          }
+        }
         stage('Copybara') {
           steps {
             sh 'copybara/bin/sync'
@@ -337,7 +355,7 @@ pipeline {
   post {
     // Collect junit test results for both success and failure case.
     always {
-      junit checksName: 'Server Pro E2E test results', testResults: 'server-ce/test/reports/junit-*.xml'
+      junit checksName: 'Server-Pro-E2E-Tests results', testResults: 'server-ce/test/reports/junit-*.xml'
     }
     // Ensure tear down of test containers, remove CE docker images, then run general Jenkins VM cleanup.
     cleanup {

+ 1 - 1
server-ce/test/accounts.spec.ts

@@ -16,7 +16,7 @@ describe('Accounts', function () {
     cy.url().should('include', '/login')
   })
 
-  it('should render the email on the user activate screen', () => {
+  it('should render the email on the user activate screen', function () {
     const email = 'not-activated-user@example.com'
     cy.then(async () => {
       const { url } = await createMongoUser({ email })

+ 33 - 33
server-ce/test/admin.spec.ts

@@ -12,7 +12,7 @@ import { openEmail } from './helpers/email'
 
 describe('admin panel', function () {
   function registrationTests() {
-    it('via GUI and opening URL manually', () => {
+    it('via GUI and opening URL manually', function () {
       const user = `${uuid()}@example.com`
       cy.findByLabelText('Emails to register new users').type(user + '{enter}')
 
@@ -24,7 +24,7 @@ describe('admin panel', function () {
         })
     })
 
-    it('via GUI and email', () => {
+    it('via GUI and email', function () {
       const user = `${uuid()}@example.com`
       cy.findByLabelText('Emails to register new users').type(user + '{enter}')
 
@@ -49,7 +49,7 @@ describe('admin panel', function () {
         activateUser(url)
       })
     })
-    it('via script and opening URL manually', () => {
+    it('via script and opening URL manually', function () {
       const user = `${uuid()}@example.com`
       let url: string
       cy.then(async () => {
@@ -59,7 +59,7 @@ describe('admin panel', function () {
         activateUser(url)
       })
     })
-    it('via script and email', () => {
+    it('via script and email', function () {
       const user = `${uuid()}@example.com`
       let url: string
       cy.then(async () => {
@@ -81,7 +81,7 @@ describe('admin panel', function () {
     })
   }
 
-  describe('in CE', () => {
+  describe('in CE', function () {
     if (isExcludedBySharding('CE_DEFAULT')) return
     startWith({ pro: false, version: 'latest' })
     const admin = 'admin@example.com'
@@ -89,8 +89,8 @@ describe('admin panel', function () {
     ensureUserExists({ email: admin, isAdmin: true })
     ensureUserExists({ email: user })
 
-    describe('create users', () => {
-      beforeEach(() => {
+    describe('create users', function () {
+      beforeEach(function () {
         login(admin)
         cy.visit('/project')
         cy.findByRole('menuitem', { name: 'Admin' }).click()
@@ -100,7 +100,7 @@ describe('admin panel', function () {
     })
   })
 
-  describe('in server pro', () => {
+  describe('in server pro', function () {
     const admin = 'admin@example.com'
     const user1 = 'user@example.com'
     const user2 = 'user2@example.com'
@@ -135,13 +135,13 @@ describe('admin panel', function () {
       )
     })
 
-    describe('admin menu items', () => {
-      beforeEach(() => {
+    describe('admin menu items', function () {
+      beforeEach(function () {
         login(admin)
         cy.visit('/project')
       })
 
-      it('displays expected admin menu items', () => {
+      it('displays expected admin menu items', function () {
         const menuitems = ['Manage Site', 'Manage Users', 'Project URL Lookup']
         menuitems.forEach(name => {
           cy.findByRole('menuitem', { name: 'Admin' }).click()
@@ -153,15 +153,15 @@ describe('admin panel', function () {
       })
     })
 
-    describe('manage site', () => {
-      beforeEach(() => {
+    describe('manage site', function () {
+      beforeEach(function () {
         login(admin)
         cy.visit('/project')
         cy.findByRole('menuitem', { name: 'Admin' }).click()
         cy.findByRole('menuitem', { name: 'Manage Site' }).click()
       })
 
-      it('publish and clear admin messages', () => {
+      it('publish and clear admin messages', function () {
         const message = 'Admin Message ' + uuid()
 
         cy.log('create system message')
@@ -189,15 +189,15 @@ describe('admin panel', function () {
       })
     })
 
-    describe('manage users', () => {
-      beforeEach(() => {
+    describe('manage users', function () {
+      beforeEach(function () {
         login(admin)
         cy.visit('/project')
         cy.findByRole('menuitem', { name: 'Admin' }).click()
         cy.findByRole('menuitem', { name: 'Manage Users' }).click()
       })
 
-      it('displays expected tabs', () => {
+      it('displays expected tabs', function () {
         const tabs = ['Users', 'License Usage']
         cy.findAllByRole('tab').should('have.length', tabs.length)
         tabs.forEach(tabName => {
@@ -205,21 +205,21 @@ describe('admin panel', function () {
         })
       })
 
-      it('license usage tab', () => {
+      it('license usage tab', function () {
         cy.get('a').contains('License Usage').click()
         cy.findByText(
           'An active user is one who has opened a project in this Server Pro instance in the last 12 months.'
         )
       })
 
-      describe('create users', () => {
-        beforeEach(() => {
+      describe('create users', function () {
+        beforeEach(function () {
           cy.get('a').contains('New User').click()
         })
         registrationTests()
       })
 
-      it('user list RegExp search', () => {
+      it('user list RegExp search', function () {
         cy.findByLabelText('RegExp').click()
         cy.findByPlaceholderText('Search users by email or id…').type(
           'user[0-9]{enter}'
@@ -229,8 +229,8 @@ describe('admin panel', function () {
       })
     })
 
-    describe('user page', () => {
-      beforeEach(() => {
+    describe('user page', function () {
+      beforeEach(function () {
         login(admin)
         cy.visit('/project')
         cy.findByRole('menuitem', { name: 'Admin' }).click()
@@ -242,7 +242,7 @@ describe('admin panel', function () {
         cy.url().should('match', /\/admin\/user\/[a-fA-F0-9]{24}/)
       })
 
-      it('displays expected tabs', () => {
+      it('displays expected tabs', function () {
         const tabs = [
           'User Info',
           'Projects',
@@ -256,18 +256,18 @@ describe('admin panel', function () {
         })
       })
 
-      describe('user info tab', () => {
-        beforeEach(() => {
+      describe('user info tab', function () {
+        beforeEach(function () {
           cy.findByRole('tab', { name: 'User Info' }).click()
         })
 
-        it('displays required sections', () => {
+        it('displays required sections', function () {
           // not exhaustive list, checks the tab content is rendered
           cy.findByText('Profile')
           cy.findByText('Editor Settings')
         })
 
-        it('should not display SaaS-only sections', () => {
+        it('should not display SaaS-only sections', function () {
           cy.findByLabelText('Referred User Count').should('not.exist')
           cy.findByRole('heading', { name: /Split Test Assignments/ }).should(
             'not.exist'
@@ -285,7 +285,7 @@ describe('admin panel', function () {
         })
       })
 
-      it('transfer project ownership', () => {
+      it('transfer project ownership', function () {
         cy.log("access project admin through owners' project list")
         cy.findByRole('tablist').within(() => {
           cy.findByRole('tab', { name: 'Projects' }).click()
@@ -326,13 +326,13 @@ describe('admin panel', function () {
       })
     })
 
-    describe('project page', () => {
-      beforeEach(() => {
+    describe('project page', function () {
+      beforeEach(function () {
         login(admin)
         cy.visit(`/admin/project/${testProjectId}`)
       })
 
-      it('displays expected tabs', () => {
+      it('displays expected tabs', function () {
         const tabs = ['Project Info', 'Deleted Docs', 'Audit Log']
         cy.findAllByRole('tab').should('have.length', tabs.length)
         tabs.forEach(tabName => {
@@ -341,7 +341,7 @@ describe('admin panel', function () {
       })
     })
 
-    it('restore deleted projects', () => {
+    it('restore deleted projects', function () {
       login(user1)
       cy.visit('/project')
 

+ 2 - 1
server-ce/test/create-and-compile-project.spec.ts

@@ -42,7 +42,8 @@ describe('Project creation and compilation', function () {
       .findByRole('button', { name: 'New file' })
       .click()
     cy.findByRole('dialog').within(() => {
-      cy.findByLabelText('File Name').clear().type(fileName)
+      cy.findByLabelText('File Name').as('filename').clear()
+      cy.get('@filename').type(fileName)
       cy.findByRole('button', { name: 'Create' }).click()
     })
     cy.findByRole('button', { name: fileName }).click()

+ 7 - 7
server-ce/test/customization.spec.ts

@@ -1,18 +1,18 @@
 import { isExcludedBySharding, startWith } from './helpers/config'
 
-describe('Customization', () => {
+describe('Customization', function () {
   if (isExcludedBySharding('CE_CUSTOM_1')) return
 
-  describe('default settings', () => {
+  describe('default settings', function () {
     startWith({})
 
-    it('should display the default right footer', () => {
+    it('should display the default right footer', function () {
       cy.visit('/')
       cy.get('footer').findByRole('link', { name: 'Fork on GitHub!' })
     })
   })
 
-  describe('custom settings', () => {
+  describe('custom settings', function () {
     startWith({
       vars: {
         OVERLEAF_APP_NAME: 'CUSTOM APP NAME',
@@ -23,16 +23,16 @@ describe('Customization', () => {
       },
     })
 
-    it('should display custom name', () => {
+    it('should display custom name', function () {
       cy.visit('/')
       cy.get('nav').findByText('CUSTOM APP NAME')
     })
 
-    it('should display custom left footer', () => {
+    it('should display custom left footer', function () {
       cy.visit('/')
       cy.get('footer').findByText('CUSTOM LEFT FOOTER')
     })
-    it('should display custom right footer', () => {
+    it('should display custom right footer', function () {
       cy.visit('/')
       cy.get('footer').findByText('CUSTOM RIGHT FOOTER')
     })

+ 1 - 1
server-ce/test/cypress.config.ts

@@ -53,7 +53,7 @@ export default defineConfig({
   viewportWidth: 1024,
   e2e: {
     baseUrl: 'http://localhost',
-    setupNodeEvents(on, config) {
+    setupNodeEvents(on) {
       on('task', {
         readPdf,
         readFileInZip,

+ 0 - 0
server-ce/test/cypress/support/e2e.js → server-ce/test/cypress/support/e2e.ts


+ 19 - 19
server-ce/test/editor.spec.ts

@@ -13,7 +13,7 @@ import { prepareWaitForNextCompileSlot } from './helpers/compile'
 const USER = 'user@example.com'
 const COLLABORATOR = 'collaborator@example.com'
 
-describe('editor', () => {
+describe('editor', function () {
   if (isExcludedBySharding('PRO_DEFAULT_1')) return
   startWith({ pro: true })
   ensureUserExists({ email: USER })
@@ -23,7 +23,7 @@ describe('editor', () => {
   let projectId: string
   let recompile: () => void
   let waitForCompile: (fn: () => void) => void
-  beforeWithReRunOnTestRetry(function () {
+  beforeWithReRunOnTestRetry(() => {
     projectName = `project-${uuid()}`
     login(USER)
     createProject(projectName, { type: 'Example project', open: false }).then(
@@ -32,7 +32,7 @@ describe('editor', () => {
     ;({ recompile, waitForCompile } = prepareWaitForNextCompileSlot())
   })
 
-  beforeEach(() => {
+  beforeEach(function () {
     login(USER)
     waitForCompile(() => {
       openProjectById(projectId)
@@ -58,7 +58,7 @@ describe('editor', () => {
       changeSpellCheckLanguageTo('Off')
     })
 
-    it('word dictionary and spelling', () => {
+    it('word dictionary and spelling', function () {
       changeSpellCheckLanguageTo('English (American)')
       createNewFile()
       const word = createRandomLetterString()
@@ -109,8 +109,8 @@ describe('editor', () => {
     })
   })
 
-  describe('editor', () => {
-    it('renders jpg', () => {
+  describe('editor', function () {
+    it('renders jpg', function () {
       cy.findByRole('navigation', {
         name: 'Project files and outline',
       })
@@ -122,7 +122,7 @@ describe('editor', () => {
         .should('be.greaterThan', 0)
     })
 
-    it('symbol palette', () => {
+    it('symbol palette', function () {
       createNewFile()
 
       cy.get('button[aria-label="Insert symbol"]').click({
@@ -139,20 +139,20 @@ describe('editor', () => {
     })
   })
 
-  describe('add new file to project', () => {
-    beforeEach(() => {
+  describe('add new file to project', function () {
+    beforeEach(function () {
       cy.findByRole('button', { name: 'New file' }).click()
     })
 
     testNewFileUpload()
 
-    it('should not display import from URL', () => {
+    it('should not display import from URL', function () {
       cy.findByRole('button', { name: 'From external URL' }).should('not.exist')
     })
   })
 
-  describe('left menu', () => {
-    beforeEach(() => {
+  describe('left menu', function () {
+    beforeEach(function () {
       cy.findByRole('navigation', {
         name: 'Project actions',
       })
@@ -160,7 +160,7 @@ describe('editor', () => {
         .click()
     })
 
-    it('can download project sources', () => {
+    it('can download project sources', function () {
       cy.findByRole('link', { name: 'Source' }).click()
       const zipName = projectName.replaceAll('-', '_')
       cy.task('readFileInZip', {
@@ -169,7 +169,7 @@ describe('editor', () => {
       }).should('contain', 'Your introduction goes here')
     })
 
-    it('can download project PDF', () => {
+    it('can download project PDF', function () {
       cy.log('ensure project is compiled')
       cy.findByRole('region', { name: 'PDF preview and logs' }).should(
         'contain.text',
@@ -185,7 +185,7 @@ describe('editor', () => {
       })
     })
 
-    it('word count', () => {
+    it('word count', function () {
       cy.log('ensure project is compiled')
       cy.findByRole('region', { name: 'PDF preview and logs' }).should(
         'contain.text',
@@ -206,8 +206,8 @@ describe('editor', () => {
     })
   })
 
-  describe('layout selector', () => {
-    it('show editor only and switch between editor and pdf', () => {
+  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'
       )
@@ -238,7 +238,7 @@ describe('editor', () => {
       cy.get('.cm-editor').should('be.visible')
     })
 
-    it('show PDF only and go back to Editor & PDF', () => {
+    it('show PDF only and go back to Editor & PDF', function () {
       cy.findByRole('region', { name: 'PDF preview and logs' }).should(
         'be.visible'
       )
@@ -265,7 +265,7 @@ describe('editor', () => {
       cy.get('.cm-editor').should('be.visible')
     })
 
-    it('PDF in a separate tab (tests editor only)', () => {
+    it('PDF in a separate tab (tests editor only)', function () {
       cy.findByTestId('pdf-viewer').should('be.visible')
       cy.get('.cm-editor').should('be.visible')
 

+ 4 - 4
server-ce/test/external-auth.spec.ts

@@ -1,7 +1,7 @@
 import { isExcludedBySharding, startWith } from './helpers/config'
 import { createProject } from './helpers/project'
 
-describe('SAML', () => {
+describe('SAML', function () {
   if (isExcludedBySharding('PRO_CUSTOM_1')) return
   const samlURL = Cypress.env('SAML_URL') || 'http://saml'
 
@@ -22,7 +22,7 @@ describe('SAML', () => {
     },
   })
 
-  it('login', () => {
+  it('login', function () {
     cy.visit('/')
     cy.findByText('Log in with SAML Test Server').click()
 
@@ -39,7 +39,7 @@ describe('SAML', () => {
   })
 })
 
-describe('LDAP', () => {
+describe('LDAP', function () {
   if (isExcludedBySharding('PRO_CUSTOM_1')) return
   startWith({
     pro: true,
@@ -57,7 +57,7 @@ describe('LDAP', () => {
     },
   })
 
-  it('login', () => {
+  it('login', function () {
     cy.visit('/')
     cy.findByText('Log in LDAP')
 

+ 2 - 2
server-ce/test/filestore-migration.spec.ts

@@ -295,7 +295,7 @@ describe('filestore migration', function () {
 
   // -------------------
   // filestore-migration
-  beforeEach(() => {
+  beforeEach(function () {
     login(email)
     waitForCompile(() => {
       openProjectById(projectId)
@@ -311,7 +311,7 @@ describe('filestore migration', function () {
       }
     })
 
-    it('renders image of example project', () => {
+    it('renders image of example project', function () {
       cy.findByTestId('file-tree').findByText(defaultImage).click()
       cy.get(`[alt="${defaultImage}"]`)
         .should('be.visible')

+ 15 - 14
server-ce/test/git-bridge.spec.ts

@@ -65,7 +65,7 @@ describe('git-bridge', function () {
       login(USER)
     })
 
-    it('should render the git-bridge UI in the settings', () => {
+    it('should render the git-bridge UI in the settings', function () {
       maybeClearAllTokens()
       cy.visit('/user/settings')
       cy.findByRole('heading', { name: 'Git integration' })
@@ -147,7 +147,7 @@ describe('git-bridge', function () {
       })
     })
 
-    describe('git access', () => {
+    describe('git access', function () {
       ensureUserExists({ email: 'collaborator-rw@example.com' })
       ensureUserExists({ email: 'collaborator-ro@example.com' })
       ensureUserExists({ email: 'collaborator-link-rw@example.com' })
@@ -156,13 +156,13 @@ describe('git-bridge', function () {
       let projectName: string
       let recompile: () => void
       let waitForCompile: (triggerCompile: () => void) => void
-      beforeEach(() => {
+      beforeEach(function () {
         projectName = uuid()
         createProject(projectName, { open: false }).as('projectId')
         ;({ recompile, waitForCompile } = prepareWaitForNextCompileSlot())
       })
 
-      it('should expose r/w interface to owner', () => {
+      it('should expose r/w interface to owner', function () {
         maybeClearAllTokens()
         waitForCompile(() => {
           openProjectByName(projectName)
@@ -170,7 +170,7 @@ describe('git-bridge', function () {
         checkGitAccess('readAndWrite')
       })
 
-      it('should expose r/w interface to invited r/w collaborator', () => {
+      it('should expose r/w interface to invited r/w collaborator', function () {
         shareProjectByEmailAndAcceptInviteViaDash(
           projectName,
           'collaborator-rw@example.com',
@@ -183,7 +183,7 @@ describe('git-bridge', function () {
         checkGitAccess('readAndWrite')
       })
 
-      it('should expose r/o interface to invited r/o collaborator', () => {
+      it('should expose r/o interface to invited r/o collaborator', function () {
         shareProjectByEmailAndAcceptInviteViaDash(
           projectName,
           'collaborator-ro@example.com',
@@ -196,7 +196,7 @@ describe('git-bridge', function () {
         checkGitAccess('readOnly')
       })
 
-      it('should expose r/w interface to link-sharing r/w collaborator', () => {
+      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'
@@ -213,7 +213,7 @@ describe('git-bridge', function () {
         })
       })
 
-      it('should expose r/o interface to link-sharing r/o collaborator', () => {
+      it('should expose r/o interface to link-sharing r/o collaborator', function () {
         waitForCompile(() => {
           openProjectByName(projectName)
         })
@@ -267,7 +267,7 @@ describe('git-bridge', function () {
               const dir = `/${projectId}`
 
               async function readFile(path: string): Promise<string> {
-                return new Promise((resolve, reject) => {
+                return await new Promise((resolve, reject) => {
                   fs.readFile(path, { encoding: 'utf8' }, (err, blob) => {
                     if (err) return reject(err)
                     resolve(blob as string)
@@ -276,7 +276,7 @@ describe('git-bridge', function () {
               }
 
               async function writeFile(path: string, data: string) {
-                return new Promise<void>((resolve, reject) => {
+                return await new Promise<void>((resolve, reject) => {
                   fs.writeFile(path, data, undefined, err => {
                     if (err) return reject(err)
                     resolve()
@@ -398,8 +398,9 @@ Hello world
               cy.findByText(/\\documentclass/)
                 .parent()
                 .parent()
+                .as('documentclass')
                 .click()
-                .type('% via editor{enter}')
+              cy.get('@documentclass').type('% via editor{enter}')
 
               // Trigger flush via compile
               recompile()
@@ -433,7 +434,7 @@ Hello world
   function checkDisabled() {
     ensureUserExists({ email: USER })
 
-    it('should not render the git-bridge UI in the settings', () => {
+    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')
@@ -454,7 +455,7 @@ Hello world
     })
   }
 
-  describe('disabled in Server Pro', () => {
+  describe('disabled in Server Pro', function () {
     if (isExcludedBySharding('PRO_DEFAULT_1')) return
     startWith({
       pro: true,
@@ -462,7 +463,7 @@ Hello world
     checkDisabled()
   })
 
-  describe('unavailable in CE', () => {
+  describe('unavailable in CE', function () {
     if (isExcludedBySharding('CE_CUSTOM_1')) return
     startWith({
       pro: false,

+ 1 - 1
server-ce/test/graceful-shutdown.spec.ts

@@ -28,7 +28,7 @@ describe('GracefulShutdown', function () {
   ensureUserExists({ email: USER })
 
   let projectId: string
-  it('should display banner and flush changes out of redis', () => {
+  it('should display banner and flush changes out of redis', function () {
     bringServerProBackUp()
     login(USER)
     const { recompile, waitForCompile } = prepareWaitForNextCompileSlot()

+ 1 - 1
server-ce/test/helpers/beforeWithReRunOnTestRetry.ts

@@ -1,6 +1,6 @@
 export function beforeWithReRunOnTestRetry(fn: () => void | Promise<any>) {
   let ranOnce = false
-  beforeEach(() => {
+  beforeEach(function () {
     if (ranOnce && Cypress.currentRetry === 0) return
     ranOnce = true
     return fn()

+ 6 - 1
server-ce/test/helpers/email.ts

@@ -7,7 +7,10 @@
  */
 export function openEmail<T>(
   subject: string | RegExp,
-  runner: (frame: Cypress.Chainable<JQuery<any>>, args: T) => void,
+  runner: (
+    frame: Cypress.Chainable<Cypress.JQueryWithSelector<any>>,
+    args: T
+  ) => void,
   args?: T
 ) {
   const runnerS = runner.toString()
@@ -28,9 +31,11 @@ export function openEmail<T>(
       // Use force as the subject is partially hidden
       cy.contains(subject).click({ force: true })
       cy.log('wait for iframe loading')
+      // eslint-disable-next-line cypress/no-unnecessary-waiting
       cy.wait(1000)
       cy.get('iframe[id="messagecontframe"]').then(frame => {
         // runnerS='(frame, args) => { runner body }'. Extract the runnable function.
+        // eslint-disable-next-line no-new-func
         const runner = new Function('return ' + runnerS)()
         runner(cy.wrap(frame.prop('contentWindow').document.body), args)
       })

+ 1 - 1
server-ce/test/helpers/hostAdminClient.ts

@@ -130,7 +130,7 @@ export async function purgeFilestoreData() {
 }
 
 async function sleep(ms: number) {
-  return new Promise(resolve => {
+  return await new Promise(resolve => {
     setTimeout(resolve, ms)
   })
 }

+ 6 - 7
server-ce/test/helpers/project.ts

@@ -114,11 +114,10 @@ function shareProjectByEmail(
     cy.findByLabelText('Add email address', { selector: 'input' })
       .parents('form')
       .within(() => {
-        cy.findByTestId('add-collaborator-select')
-          .click()
-          .then(() => {
-            cy.findByRole('option', { name: level }).click()
-          })
+        cy.findByTestId('add-collaborator-select').as('select').click()
+        cy.get('@select').then(() => {
+          cy.findByRole('option', { name: level }).click()
+        })
       })
     cy.findByRole('button', { name: 'Invite' }).click()
     cy.findByText('Invite not yet accepted.')
@@ -275,12 +274,12 @@ export function prepareFileUploadTest(binary = false) {
 }
 
 export function testNewFileUpload() {
-  it('can upload text file', () => {
+  it('can upload text file', function () {
     const check = prepareFileUploadTest(false)
     check()
   })
 
-  it('can upload binary file', () => {
+  it('can upload binary file', function () {
     const check = prepareFileUploadTest(true)
     check()
   })

+ 14 - 8
server-ce/test/helpers/read-file.ts

@@ -1,9 +1,8 @@
-import fs from 'fs'
-import path from 'path'
-// @ts-ignore broken package entrypoint
-import pdf from 'pdf-parse/lib/pdf-parse.js'
+import fs from 'node:fs'
+import path from 'node:path'
+import { PDFParse } from 'pdf-parse'
 import AdmZip from 'adm-zip'
-import { setTimeout } from 'timers/promises'
+import { setTimeout } from 'node:timers/promises'
 
 const MAX_ATTEMPTS = 15
 const POLL_INTERVAL = 500
@@ -23,7 +22,7 @@ export async function readFileInZip({
       const zip = new AdmZip(path.resolve(pathToZip))
       const entry = zip
         .getEntries()
-        .find(entry => entry.entryName == fileToRead)
+        .find(entry => entry.entryName === fileToRead)
       if (entry) {
         return entry.getData().toString('utf8')
       } else {
@@ -41,8 +40,15 @@ export async function readPdf(file: string) {
   while (attempt < MAX_ATTEMPTS) {
     if (fs.existsSync(file)) {
       const dataBuffer = fs.readFileSync(path.resolve(file))
-      const { text } = await pdf(dataBuffer)
-      return text
+      const parser = new PDFParse({ data: dataBuffer })
+      try {
+        const result = await parser.getText()
+        return result.text
+      } catch (error) {
+        console.error('PDF parsing failed:', error)
+      } finally {
+        await parser.destroy()
+      }
     }
     await setTimeout(POLL_INTERVAL)
     attempt++

+ 1 - 1
server-ce/test/history.spec.ts

@@ -61,7 +61,7 @@ describe('History', function () {
   const CLASS_ADDITION = 'ol-cm-addition-marker'
   const CLASS_DELETION = 'ol-cm-deletion-marker'
 
-  it('should support labels, comparison and download', () => {
+  it('should support labels, comparison and download', function () {
     const { recompile, waitForCompile } = prepareWaitForNextCompileSlot()
     waitForCompile(() => {
       createProject('labels')

+ 9 - 10
server-ce/test/learn-wiki.spec.ts

@@ -1,6 +1,5 @@
 import { isExcludedBySharding, startWith } from './helpers/config'
 import { ensureUserExists, login } from './helpers/login'
-import { v4 as uuid } from 'uuid'
 
 describe('LearnWiki', function () {
   const COPYING_A_PROJECT_URL = '/learn/how-to/Copying_a_project'
@@ -15,7 +14,7 @@ describe('LearnWiki', function () {
   ensureUserExists({ email: WITHOUT_PROJECTS_USER })
   ensureUserExists({ email: REGULAR_USER })
 
-  describe('enabled in Pro', () => {
+  describe('enabled in Pro', function () {
     if (isExcludedBySharding('PRO_CUSTOM_2')) return
     startWith({
       pro: true,
@@ -24,7 +23,7 @@ describe('LearnWiki', function () {
       },
     })
 
-    it('should add a documentation entry to the nav bar', () => {
+    it('should add a documentation entry to the nav bar', function () {
       login(REGULAR_USER)
       cy.visit('/project')
       cy.findByRole('menuitem', { name: 'Documentation' }).should(
@@ -34,7 +33,7 @@ describe('LearnWiki', function () {
       )
     })
 
-    it('should display a tutorial link in the welcome page', () => {
+    it('should display a tutorial link in the welcome page', function () {
       login(WITHOUT_PROJECTS_USER)
       cy.visit('/project')
       cy.findByRole('link', { name: LABEL_LEARN_LATEX })
@@ -45,7 +44,7 @@ describe('LearnWiki', function () {
         })
     })
 
-    it('should render wiki page', () => {
+    it('should render wiki page', function () {
       login(REGULAR_USER)
 
       cy.visit(UPLOADING_A_PROJECT_URL)
@@ -81,13 +80,13 @@ describe('LearnWiki', function () {
     })
   })
 
-  describe('disabled in Pro', () => {
+  describe('disabled in Pro', function () {
     if (isExcludedBySharding('PRO_DEFAULT_1')) return
     startWith({ pro: true })
     checkDisabled()
   })
 
-  describe('unavailable in CE', () => {
+  describe('unavailable in CE', function () {
     if (isExcludedBySharding('CE_CUSTOM_1')) return
     startWith({
       pro: false,
@@ -100,13 +99,13 @@ describe('LearnWiki', function () {
   })
 
   function checkDisabled() {
-    it('should not add a documentation entry to the nav bar', () => {
+    it('should not add a documentation entry to the nav bar', function () {
       login(REGULAR_USER)
       cy.visit('/project')
       cy.findByText('Documentation').should('not.exist')
     })
 
-    it('should not render wiki page', () => {
+    it('should not render wiki page', function () {
       login(REGULAR_USER)
       cy.visit(COPYING_A_PROJECT_URL, {
         failOnStatusCode: false,
@@ -114,7 +113,7 @@ describe('LearnWiki', function () {
       cy.findByText('Not found')
     })
 
-    it('should not display a tutorial link in the welcome page', () => {
+    it('should not display a tutorial link in the welcome page', function () {
       login(WITHOUT_PROJECTS_USER)
       cy.visit('/project')
       cy.findByText(LABEL_LEARN_LATEX).should('not.exist')

+ 4 - 3
server-ce/test/package.json

@@ -7,14 +7,15 @@
     "cypress:open": "cypress open --e2e --browser chrome",
     "cypress:run": "cypress run --e2e --browser chrome",
     "format": "prettier --list-different $PWD/'**/*.{js,mjs,ts,tsx}'",
-    "format:fix": "prettier --write $PWD/'**/*.{js,mjs,ts,tsx}'"
+    "format:fix": "prettier --write $PWD/'**/*.{js,mjs,ts,tsx}'",
+    "lint": "eslint --max-warnings 0 --format unix --ext .js,.jsx,.mjs,.ts,.tsx .",
+    "lint:fix": "eslint --fix --ext .js,.jsx,.mjs,.ts,.tsx ."
   },
   "devDependencies": {
     "@isomorphic-git/lightning-fs": "^4.6.0",
     "@overleaf/validation-tools": "*",
     "@testing-library/cypress": "^10.0.3",
     "@types/adm-zip": "^0.5.7",
-    "@types/pdf-parse": "^1.1.5",
     "@types/uuid": "^9.0.8",
     "adm-zip": "^0.5.12",
     "body-parser": "^1.20.3",
@@ -24,7 +25,7 @@
     "isomorphic-git": "^1.33.1",
     "js-yaml": "^4.1.1",
     "mocha-junit-reporter": "^2.2.1",
-    "pdf-parse": "^1.1.1",
+    "pdf-parse": "^2.3.0",
     "uuid": "^9.0.1",
     "zod-validation-error": "^4.0.1"
   }

+ 9 - 9
server-ce/test/project-list.spec.ts

@@ -6,7 +6,7 @@ import { v4 as uuid } from 'uuid'
 const WITHOUT_PROJECTS_USER = 'user-without-projects@example.com'
 const REGULAR_USER = 'user@example.com'
 
-describe('Project List', () => {
+describe('Project List', function () {
   if (isExcludedBySharding('PRO_DEFAULT_2')) return
   startWith({ pro: true })
 
@@ -15,10 +15,10 @@ describe('Project List', () => {
     return cy.findByText(projectName).parent().parent()
   }
 
-  describe('user with no projects', () => {
+  describe('user with no projects', function () {
     ensureUserExists({ email: WITHOUT_PROJECTS_USER })
 
-    it("'Import from GitHub' is not displayed in the welcome page", () => {
+    it("'Import from GitHub' is not displayed in the welcome page", function () {
       login(WITHOUT_PROJECTS_USER)
       cy.visit('/project')
       cy.findByRole('button', { name: 'Create a new project' }).click()
@@ -28,11 +28,11 @@ describe('Project List', () => {
     })
   })
 
-  describe('user with projects', () => {
+  describe('user with projects', function () {
     const projectName = `test-project-${uuid()}`
     ensureUserExists({ email: REGULAR_USER })
 
-    before(() => {
+    before(function () {
       login(REGULAR_USER)
       createProject(projectName, { type: 'Example project', open: false })
     })
@@ -41,7 +41,7 @@ describe('Project List', () => {
       cy.visit('/project')
     })
 
-    it('Can download project sources', () => {
+    it('Can download project sources', function () {
       findProjectRow(projectName).within(() =>
         cy.findByRole('button', { name: 'Download .zip file' }).click()
       )
@@ -53,7 +53,7 @@ describe('Project List', () => {
       }).should('contain', 'Your introduction goes here')
     })
 
-    it('Can download project PDF', () => {
+    it('Can download project PDF', function () {
       findProjectRow(projectName).within(() =>
         cy.findByRole('button', { name: 'Download PDF' }).click()
       )
@@ -65,7 +65,7 @@ describe('Project List', () => {
       )
     })
 
-    it('can assign and remove tags to projects', () => {
+    it('can assign and remove tags to projects', function () {
       const tagName = uuid().slice(0, 7) // long tag names are truncated in the UI, which affects selectors
       cy.log('select project')
       cy.findByRole('checkbox', { name: `Select ${projectName}` }).check()
@@ -88,7 +88,7 @@ describe('Project List', () => {
       )
     })
 
-    it('can filter by tag', () => {
+    it('can filter by tag', function () {
       cy.log('create a separate project to filter')
       const nonTaggedProjectName = `project-${uuid()}`
       createProject(nonTaggedProjectName, { open: false })

+ 34 - 34
server-ce/test/project-sharing.spec.ts

@@ -27,13 +27,13 @@ describe('Project Sharing', function () {
   let projectName: string
   let recompile: () => void
   let waitForCompile: (triggerCompile: () => void) => void
-  beforeWithReRunOnTestRetry(function () {
+  beforeWithReRunOnTestRetry(() => {
     projectName = getSpamSafeProjectName()
     ;({ recompile, waitForCompile } = prepareWaitForNextCompileSlot())
     setupTestProject()
   })
 
-  beforeEach(() => {
+  beforeEach(function () {
     // Always start with a fresh session
     cy.session([uuid()], () => {})
   })
@@ -245,22 +245,22 @@ describe('Project Sharing', function () {
       shareProjectByEmailAndAcceptInviteViaEmail(projectName, email, 'Viewer')
     })
 
-    it('should grant the collaborator read access', () => {
+    it('should grant the collaborator read access', function () {
       expectFullReadOnlyAccess()
       expectProjectDashboardEntry()
     })
   })
 
-  describe('read only', () => {
+  describe('read only', function () {
     const email = 'collaborator-ro@example.com'
     ensureUserExists({ email })
 
-    beforeWithReRunOnTestRetry(function () {
+    beforeWithReRunOnTestRetry(() => {
       login('user@example.com')
       shareProjectByEmailAndAcceptInviteViaDash(projectName, email, 'Viewer')
     })
 
-    it('should grant the collaborator read access', () => {
+    it('should grant the collaborator read access', function () {
       login(email)
       openProjectByName(projectName)
       expectFullReadOnlyAccess()
@@ -268,16 +268,16 @@ describe('Project Sharing', function () {
     })
   })
 
-  describe('read and write', () => {
+  describe('read and write', function () {
     const email = 'collaborator-rw@example.com'
     ensureUserExists({ email })
 
-    beforeWithReRunOnTestRetry(function () {
+    beforeWithReRunOnTestRetry(() => {
       login('user@example.com')
       shareProjectByEmailAndAcceptInviteViaDash(projectName, email, 'Editor')
     })
 
-    it('should grant the collaborator write access', () => {
+    it('should grant the collaborator write access', function () {
       login(email)
       openProjectByName(projectName)
       expectFullReadAndWriteAccess()
@@ -286,13 +286,13 @@ describe('Project Sharing', function () {
     })
   })
 
-  describe('token access', () => {
-    describe('logged in', () => {
-      describe('read only', () => {
+  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', () => {
+        it('should grant restricted read access', function () {
           login(email)
           openProjectViaLinkSharingAsUser(
             linkSharingReadOnly,
@@ -304,11 +304,11 @@ describe('Project Sharing', function () {
         })
       })
 
-      describe('read and write', () => {
+      describe('read and write', function () {
         const email = 'collaborator-link-rw@example.com'
         ensureUserExists({ email })
 
-        it('should grant full write access', () => {
+        it('should grant full write access', function () {
           login(email)
           openProjectViaLinkSharingAsUser(
             linkSharingReadAndWrite,
@@ -322,8 +322,8 @@ describe('Project Sharing', function () {
       })
     })
 
-    describe('with OVERLEAF_ALLOW_PUBLIC_ACCESS=false', () => {
-      describe('wrap startup', () => {
+    describe('with OVERLEAF_ALLOW_PUBLIC_ACCESS=false', function () {
+      describe('wrap startup', function () {
         startWith({
           pro: true,
           vars: {
@@ -331,12 +331,12 @@ describe('Project Sharing', function () {
           },
           withDataDir: true,
         })
-        it('should block access', () => {
+        it('should block access', function () {
           expectNoAccess()
         })
       })
 
-      describe('with OVERLEAF_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING=true', () => {
+      describe('with OVERLEAF_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING=true', function () {
         startWith({
           pro: true,
           vars: {
@@ -345,14 +345,14 @@ describe('Project Sharing', function () {
           },
           withDataDir: true,
         })
-        it('should block access', () => {
+        it('should block access', function () {
           expectNoAccess()
         })
       })
     })
 
-    describe('with OVERLEAF_ALLOW_PUBLIC_ACCESS=true', () => {
-      describe('wrap startup', () => {
+    describe('with OVERLEAF_ALLOW_PUBLIC_ACCESS=true', function () {
+      describe('wrap startup', function () {
         startWith({
           pro: true,
           vars: {
@@ -360,18 +360,18 @@ describe('Project Sharing', function () {
           },
           withDataDir: true,
         })
-        it('should grant read access with read link', () => {
+        it('should grant read access with read link', function () {
           openProjectViaLinkSharingAsAnon(linkSharingReadOnly)
           expectRestrictedReadOnlyAccess()
         })
 
-        it('should prompt for login with write link', () => {
+        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', () => {
+      describe('with OVERLEAF_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING=true', function () {
         startWith({
           pro: true,
           vars: {
@@ -381,12 +381,12 @@ describe('Project Sharing', function () {
           withDataDir: true,
         })
 
-        it('should grant read access with read link', () => {
+        it('should grant read access with read link', function () {
           openProjectViaLinkSharingAsAnon(linkSharingReadOnly)
           expectRestrictedReadOnlyAccess()
         })
 
-        it('should grant write access with write link', () => {
+        it('should grant write access with write link', function () {
           openProjectViaLinkSharingAsAnon(linkSharingReadAndWrite)
           expectAnonymousReadAndWriteAccess()
           expectEditAuthoredAs('Anonymous')
@@ -394,7 +394,7 @@ describe('Project Sharing', function () {
       })
     })
 
-    describe('with OVERLEAF_DISABLE_LINK_SHARING=true', () => {
+    describe('with OVERLEAF_DISABLE_LINK_SHARING=true', function () {
       const email = 'collaborator-email@example.com'
       ensureUserExists({ email })
 
@@ -448,14 +448,14 @@ describe('Project Sharing', function () {
         )
       })
 
-      it('should not display link sharing in the sharing modal', () => {
+      it('should not display link sharing in the sharing modal', function () {
         login('user@example.com')
         openProjectByName(projectName)
         cy.findByText('Share').click()
         cy.findByText('Turn on link sharing').should('not.exist')
       })
 
-      it('should block new access to read-only link shared projects', () => {
+      it('should block new access to read-only link shared projects', function () {
         login(email)
 
         // Test read-only link returns 404
@@ -467,7 +467,7 @@ describe('Project Sharing', function () {
         })
       })
 
-      it('should block new access to read-write link shared projects', () => {
+      it('should block new access to read-write link shared projects', function () {
         login(email)
 
         // Test read-write link returns 404
@@ -479,7 +479,7 @@ describe('Project Sharing', function () {
         })
       })
 
-      it('should continue to allow email sharing', () => {
+      it('should continue to allow email sharing', function () {
         login('user@example.com')
         shareProjectByEmailAndAcceptInviteViaEmail(
           projectName,
@@ -490,14 +490,14 @@ describe('Project Sharing', function () {
         expectProjectDashboardEntry()
       })
 
-      it('should retain read-only access when project was joined via link before link sharing was turned off', () => {
+      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', () => {
+      it('should retain read-write access when project was joined via link before link sharing was turned off', function () {
         login(retainedEditorEmail)
         openProjectByName(projectName)
         expectFullReadAndWriteAccess()

+ 2 - 0
server-ce/test/sandboxed-compiles.spec.ts

@@ -156,6 +156,7 @@ describe('SandboxedCompiles', function () {
         // 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)',
@@ -309,6 +310,7 @@ describe('SandboxedCompiles', function () {
   })
 
   // 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 })

+ 10 - 13
server-ce/test/templates.spec.ts

@@ -10,7 +10,7 @@ const TEMPLATES_USER = 'templates@example.com'
 // Re-use value for "exists" and "does not exist" tests
 const LABEL_BROWSE_TEMPLATES = 'Browse templates'
 
-describe('Templates', () => {
+describe('Templates', function () {
   ensureUserExists({ email: TEMPLATES_USER })
   ensureUserExists({ email: WITHOUT_PROJECTS_USER })
 
@@ -31,7 +31,7 @@ describe('Templates', () => {
     }
   }
 
-  describe('enabled in Server Pro', () => {
+  describe('enabled in Server Pro', function () {
     if (isExcludedBySharding('PRO_CUSTOM_2')) return
     startWith({
       pro: true,
@@ -40,7 +40,7 @@ describe('Templates', () => {
     ensureUserExists({ email: REGULAR_USER })
     ensureUserExists({ email: ADMIN_USER, isAdmin: true })
 
-    it('should show templates link on welcome page', () => {
+    it('should show templates link on welcome page', function () {
       login(WITHOUT_PROJECTS_USER)
       cy.visit('/')
       cy.findByRole('link', { name: LABEL_BROWSE_TEMPLATES })
@@ -49,7 +49,7 @@ describe('Templates', () => {
       cy.url().should('match', /\/templates$/)
     })
 
-    it('should have templates feature', () => {
+    it('should have templates feature', function () {
       login(TEMPLATES_USER)
       const name = `Template ${Date.now()}`
       const description = `Template Description ${Date.now()}`
@@ -64,11 +64,8 @@ describe('Templates', () => {
         .click()
       cy.findByText('Manage Template').click()
 
-      cy.findByText('Template Description')
-        .click()
-        .parent()
-        .get('textarea')
-        .type(description)
+      cy.findByText('Template Description').as('description').click()
+      cy.get('@description').parent().get('textarea').type(description)
       cy.findByText('Publish').click()
       cy.findByText('Publishing…').parent().should('be.disabled')
       cy.findByText('Publish').should('not.exist')
@@ -229,7 +226,7 @@ describe('Templates', () => {
   })
 
   function checkDisabled() {
-    it('should not have templates feature', () => {
+    it('should not have templates feature', function () {
       login(TEMPLATES_USER)
 
       cy.visit('/')
@@ -254,7 +251,7 @@ describe('Templates', () => {
       cy.findAllByText('All Templates').should('not.exist')
     })
 
-    it('should not show templates link on welcome page', () => {
+    it('should not show templates link on welcome page', function () {
       login(WITHOUT_PROJECTS_USER)
       cy.visit('/')
       cy.findByText(NEW_PROJECT_BUTTON_MATCHER) // wait for lazy loading
@@ -262,13 +259,13 @@ describe('Templates', () => {
     })
   }
 
-  describe('disabled Server Pro', () => {
+  describe('disabled Server Pro', function () {
     if (isExcludedBySharding('PRO_DEFAULT_2')) return
     startWith({ pro: true })
     checkDisabled()
   })
 
-  describe('unavailable in CE', () => {
+  describe('unavailable in CE', function () {
     if (isExcludedBySharding('CE_CUSTOM_1')) return
     startWith({
       pro: false,

+ 3 - 2
server-ce/test/tsconfig.json

@@ -11,7 +11,8 @@
     "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"]
+    "types": ["cypress", "node", "@testing-library/cypress"],
+    "allowImportingTsExtensions": true
   },
-  "include": ["**/*.ts", "**/*.tsx"]
+  "include": ["**/*.ts", "**/*.js"]
 }

+ 10 - 10
server-ce/test/upgrading.spec.ts

@@ -24,7 +24,7 @@ describe('Upgrading', function () {
   ) {
     const startOptions = steps.shift()!
 
-    before(async () => {
+    before(async function () {
       cy.log('Create old instance')
     })
     startWith({
@@ -38,7 +38,7 @@ describe('Upgrading', function () {
       cy.log('Create initial user after deleting it')
     })
     ensureUserExists({ email: USER })
-    before(() => {
+    before(function () {
       cy.log('Populate old instance')
       login(USER)
       ;({ recompile, waitForCompile } = prepareWaitForNextCompileSlot())
@@ -76,12 +76,12 @@ describe('Upgrading', function () {
       }
       cy.findByText('History').click()
       for (let i = 0; i < 3; i++) {
-        cy.findByText(new RegExp(`\\\\section\{Old Section ${i}}`))
+        cy.findByText(new RegExp(`\\\\section{Old Section ${i}}`))
       }
     })
 
     for (const step of steps) {
-      before(() => {
+      before(function () {
         cy.log(`Upgrade to version ${step.version}`)
 
         // Navigate way from editor to avoid redirect to /login when the next instance comes up (which slows down tests)
@@ -113,16 +113,16 @@ describe('Upgrading', function () {
 
       step.hook?.()
     }
-    beforeEach(() => {
+    beforeEach(function () {
       login(USER)
     })
 
-    it('should list the old project', () => {
+    it('should list the old project', function () {
       cy.visit('/project')
       cy.findByText(PROJECT_NAME)
     })
 
-    it('should open the old project', () => {
+    it('should open the old project', function () {
       waitForCompile(() => {
         openProjectByName(PROJECT_NAME)
       })
@@ -174,21 +174,21 @@ describe('Upgrading', function () {
       })
     },
   }
-  describe('from 4.2 to latest', () => {
+  describe('from 4.2 to latest', function () {
     testUpgrade([
       optionsFourDotTwo,
       optionsBinaryFilesMigration,
       { version: 'latest' },
     ])
   })
-  describe('from 5.0 to latest', () => {
+  describe('from 5.0 to latest', function () {
     testUpgrade([
       { version: '5.0' },
       optionsBinaryFilesMigration,
       { version: 'latest' },
     ])
   })
-  describe('doc version recovery', () => {
+  describe('doc version recovery', function () {
     testUpgrade([
       optionsFourDotTwo,
       {