Răsfoiți Sursa

[saas-e2e] initial revision of SaaS E2E tests running in the dev-env (#24311)

* [saas-e2e] initial revision of SaaS E2E tests running in the dev-env

* [v1] make rubocop happy

* [v1] make rubocop happy

* [saas-e2e] more retries for webpack startup check

Co-authored-by: Alf Eaton <alf.eaton@overleaf.com>

* [web] restrict e2e_test_setup.mjs to running in the dev-env

Co-authored-by: Mathias Jakobsen <mathias.jakobsen@overleaf.com>

* [saas-e2e] import latest split-tests from production

---------

Co-authored-by: Alf Eaton <alf.eaton@overleaf.com>
Co-authored-by: Mathias Jakobsen <mathias.jakobsen@overleaf.com>
GitOrigin-RevId: 034343ee56b8d059090d8a5de74eaac24429b527
Jakob Ackermann 1 an în urmă
părinte
comite
7b69d61540

+ 313 - 4
package-lock.json

@@ -30,7 +30,8 @@
         "services/third-party-datastore",
         "services/third-party-references",
         "services/tpdsworker",
-        "services/web"
+        "services/web",
+        "tools/saas-e2e"
       ],
       "dependencies": {
         "patch-package": "^8.0.0"
@@ -6441,6 +6442,29 @@
         "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
       }
     },
+    "node_modules/@isomorphic-git/idb-keyval": {
+      "version": "3.3.2",
+      "resolved": "https://registry.npmjs.org/@isomorphic-git/idb-keyval/-/idb-keyval-3.3.2.tgz",
+      "integrity": "sha512-r8/AdpiS0/WJCNR/t/gsgL+M8NMVj/ek7s60uz3LmpCaTF2mEVlZJlB01ZzalgYzRLXwSPC92o+pdzjM7PN/pA==",
+      "dev": true,
+      "license": "Apache-2.0"
+    },
+    "node_modules/@isomorphic-git/lightning-fs": {
+      "version": "4.6.0",
+      "resolved": "https://registry.npmjs.org/@isomorphic-git/lightning-fs/-/lightning-fs-4.6.0.tgz",
+      "integrity": "sha512-tfon8f1h6LawjFI/d8lZPWRPTxmdvyTMbkT/j5yo6dB0hALhKw5D9JsdCcUu/D1pAcMMiU7GZFDsDGqylerr7g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@isomorphic-git/idb-keyval": "3.3.2",
+        "isomorphic-textencoder": "1.0.1",
+        "just-debounce-it": "1.1.0",
+        "just-once": "1.1.0"
+      },
+      "bin": {
+        "superblocktxt": "src/superblocktxt.js"
+      }
+    },
     "node_modules/@istanbuljs/schema": {
       "version": "0.1.3",
       "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
@@ -8810,6 +8834,10 @@
       "resolved": "services/references",
       "link": true
     },
+    "node_modules/@overleaf/saas-e2e": {
+      "resolved": "tools/saas-e2e",
+      "link": true
+    },
     "node_modules/@overleaf/settings": {
       "resolved": "libraries/settings",
       "link": true
@@ -11901,6 +11929,16 @@
         "@types/node": "*"
       }
     },
+    "node_modules/@types/adm-zip": {
+      "version": "0.5.7",
+      "resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.5.7.tgz",
+      "integrity": "sha512-DNEs/QvmyRLurdQPChqq0Md4zGvPwHerAJYWk9l2jCbD1VPpnzRJorOdiq4zsw09NFbYnhfsoEhWtxIzXpn2yw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
     "node_modules/@types/aria-query": {
       "version": "4.2.2",
       "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.2.tgz",
@@ -12483,6 +12521,13 @@
       "integrity": "sha512-ZkC5IUqqIFPXx3ASTTybTzmQdwHwe2C0u3eL75ldQ6T9E9IWFJodn6hIfbZGab73DfyiHN4Xw15gNxUq2FbvBA==",
       "dev": true
     },
+    "node_modules/@types/pdf-parse": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/@types/pdf-parse/-/pdf-parse-1.1.4.tgz",
+      "integrity": "sha512-+gbBHbNCVGGYw1S9lAIIvrHW47UYOhMIFUsJcMkMrzy1Jf0vulBN3XQIjPgnoOXveMuHnF3b57fXROnY/Or7eg==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/@types/pg": {
       "version": "8.6.1",
       "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.6.1.tgz",
@@ -14573,6 +14618,16 @@
         "node": ">=8.9"
       }
     },
+    "node_modules/adm-zip": {
+      "version": "0.5.16",
+      "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz",
+      "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.0"
+      }
+    },
     "node_modules/agent-base": {
       "version": "6.0.2",
       "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
@@ -15225,6 +15280,13 @@
       "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz",
       "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg=="
     },
+    "node_modules/async-lock": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz",
+      "integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/async-retry": {
       "version": "1.3.3",
       "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz",
@@ -17215,6 +17277,13 @@
         "node": ">= 10.0"
       }
     },
+    "node_modules/clean-git-ref": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/clean-git-ref/-/clean-git-ref-2.0.1.tgz",
+      "integrity": "sha512-bLSptAy2P0s6hU4PzuIMKmMJJSE6gLXGH1cntDu7bWJUksvuM+7ReOK61mozULErYvP6a15rnYl0zFDef+pyPw==",
+      "dev": true,
+      "license": "Apache-2.0"
+    },
     "node_modules/clean-regexp": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/clean-regexp/-/clean-regexp-1.0.0.tgz",
@@ -19300,6 +19369,22 @@
         "node": ">=0.10"
       }
     },
+    "node_modules/decompress-response": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
+      "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "mimic-response": "^3.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/dedent": {
       "version": "0.7.0",
       "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz",
@@ -19605,6 +19690,13 @@
       "integrity": "sha512-UfwfKGxT/Wm2KaxFSZsp7/+YOnAgIzxQXs86zu1IFMLU/+3ouxnEvXqPDn3yxSxlsO4r1B+I2GGQHsIthp6/7Q==",
       "license": "Apache-2.0"
     },
+    "node_modules/diff3": {
+      "version": "0.0.3",
+      "resolved": "https://registry.npmjs.org/diff3/-/diff3-0.0.3.tgz",
+      "integrity": "sha512-iSq8ngPOt0K53A6eVr4d5Kn6GNrM2nQZtC740pzIriHtn4pOQ2lyzEXQMBeVcWERN0ye7fhBsk9PbLLQOnUx/g==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/dijkstrajs": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.2.tgz",
@@ -26461,6 +26553,79 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/isomorphic-git": {
+      "version": "1.29.0",
+      "resolved": "https://registry.npmjs.org/isomorphic-git/-/isomorphic-git-1.29.0.tgz",
+      "integrity": "sha512-zWGqk8901cicvVEhVpN76AwKrS/TzHak2NQCtNXIAavpMIy/yqh+d/JtC9A8AUKZAauUdOyEWKI29tuCLAL+Zg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "async-lock": "^1.4.1",
+        "clean-git-ref": "^2.0.1",
+        "crc-32": "^1.2.0",
+        "diff3": "0.0.3",
+        "ignore": "^5.1.4",
+        "minimisted": "^2.0.0",
+        "pako": "^1.0.10",
+        "path-browserify": "^1.0.1",
+        "pify": "^4.0.1",
+        "readable-stream": "^3.4.0",
+        "sha.js": "^2.4.9",
+        "simple-get": "^4.0.1"
+      },
+      "bin": {
+        "isogit": "cli.cjs"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/isomorphic-git/node_modules/pify": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
+      "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/isomorphic-git/node_modules/simple-get": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
+      "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "decompress-response": "^6.0.0",
+        "once": "^1.3.1",
+        "simple-concat": "^1.0.0"
+      }
+    },
+    "node_modules/isomorphic-textencoder": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/isomorphic-textencoder/-/isomorphic-textencoder-1.0.1.tgz",
+      "integrity": "sha512-676hESgHullDdHDsj469hr+7t3i/neBKU9J7q1T4RHaWwLAsaQnywC0D1dIUId0YZ+JtVrShzuBk1soo0+GVcQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fast-text-encoding": "^1.0.0"
+      }
+    },
     "node_modules/isstream": {
       "version": "0.1.2",
       "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
@@ -27367,12 +27532,26 @@
         "node": ">=4.0"
       }
     },
+    "node_modules/just-debounce-it": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/just-debounce-it/-/just-debounce-it-1.1.0.tgz",
+      "integrity": "sha512-87Nnc0qZKgBZuhFZjYVjSraic0x7zwjhaTMrCKlj0QYKH6lh0KbFzVnfu6LHan03NO7J8ygjeBeD0epejn5Zcg==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/just-extend": {
       "version": "4.2.1",
       "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz",
       "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==",
       "dev": true
     },
+    "node_modules/just-once": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/just-once/-/just-once-1.1.0.tgz",
+      "integrity": "sha512-+rZVpl+6VyTilK7vB/svlMPil4pxqIJZkbnN7DKZTOzyXfun6ZiFeq2Pk4EtCEHZ0VU4EkdFzG8ZK5F3PErcDw==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/jwa": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz",
@@ -28517,6 +28696,32 @@
         "@jridgewell/sourcemap-codec": "^1.5.0"
       }
     },
+    "node_modules/mailtrap": {
+      "version": "3.4.0",
+      "resolved": "https://registry.npmjs.org/mailtrap/-/mailtrap-3.4.0.tgz",
+      "integrity": "sha512-gegg90/gMY8hvfxB+WMtE8RRZyhQr90jUw00QOLApIAomItumqFBCpZv5IfG51EUKThu9+p7X4QdNA4buryenw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "axios": ">=0.27"
+      },
+      "engines": {
+        "node": ">=16.20.1",
+        "yarn": ">=1.22.17"
+      },
+      "peerDependencies": {
+        "@types/nodemailer": "^6.4.9",
+        "nodemailer": "^6.9.4"
+      },
+      "peerDependenciesMeta": {
+        "@types/nodemailer": {
+          "optional": true
+        },
+        "nodemailer": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/make-dir": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
@@ -29316,6 +29521,19 @@
         "node": ">=6"
       }
     },
+    "node_modules/mimic-response": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
+      "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/min-document": {
       "version": "2.19.0",
       "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz",
@@ -29431,6 +29649,16 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/minimisted": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/minimisted/-/minimisted-2.0.1.tgz",
+      "integrity": "sha512-1oPjfuLQa2caorJUM8HV8lGgWCc0qqAO1MNv/k05G4qslmsndV/5WdNZrqCiyqiz3wohia2Ij2B7w2Dr7/IyrA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "minimist": "^1.2.5"
+      }
+    },
     "node_modules/minipass": {
       "version": "3.1.6",
       "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.6.tgz",
@@ -30242,6 +30470,13 @@
         "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",
@@ -31191,6 +31426,13 @@
       "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz",
       "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ=="
     },
+    "node_modules/pako": {
+      "version": "1.0.11",
+      "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
+      "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
+      "dev": true,
+      "license": "(MIT AND Zlib)"
+    },
     "node_modules/param-case": {
       "version": "3.0.4",
       "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz",
@@ -31660,6 +31902,30 @@
       "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
       "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==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^3.1.0",
+        "node-ensure": "^0.0.0"
+      },
+      "engines": {
+        "node": ">=6.8.1"
+      }
+    },
+    "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==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.1"
+      }
+    },
     "node_modules/pdfjs-dist": {
       "version": "4.10.38",
       "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-4.10.38.tgz",
@@ -36376,6 +36642,20 @@
       "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
       "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
     },
+    "node_modules/sha.js": {
+      "version": "2.4.11",
+      "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
+      "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
+      "dev": true,
+      "license": "(MIT AND BSD-3-Clause)",
+      "dependencies": {
+        "inherits": "^2.0.1",
+        "safe-buffer": "^5.0.1"
+      },
+      "bin": {
+        "sha.js": "bin.js"
+      }
+    },
     "node_modules/shallow-clone": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz",
@@ -36478,9 +36758,7 @@
           "type": "consulting",
           "url": "https://feross.org/support"
         }
-      ],
-      "optional": true,
-      "peer": true
+      ]
     },
     "node_modules/simple-get": {
       "version": "3.1.1",
@@ -45445,6 +45723,37 @@
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
       "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+    },
+    "tools/saas-e2e": {
+      "name": "@overleaf/saas-e2e",
+      "devDependencies": {
+        "@isomorphic-git/lightning-fs": "^4.6.0",
+        "@testing-library/cypress": "^10.0.1",
+        "@types/adm-zip": "^0.5.5",
+        "@types/pdf-parse": "^1.1.4",
+        "@types/uuid": "^9.0.8",
+        "adm-zip": "^0.5.12",
+        "cypress": "13.13.2",
+        "isomorphic-git": "^1.25.10",
+        "mailtrap": "^3.4.0",
+        "pdf-parse": "^1.1.1",
+        "typescript": "^5.0.4",
+        "uuid": "^9.0.1"
+      }
+    },
+    "tools/saas-e2e/node_modules/uuid": {
+      "version": "9.0.1",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+      "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+      "dev": true,
+      "funding": [
+        "https://github.com/sponsors/broofa",
+        "https://github.com/sponsors/ctavan"
+      ],
+      "license": "MIT",
+      "bin": {
+        "uuid": "dist/bin/uuid"
+      }
     }
   }
 }

+ 2 - 1
package.json

@@ -73,6 +73,7 @@
     "services/third-party-datastore",
     "services/third-party-references",
     "services/tpdsworker",
-    "services/web"
+    "services/web",
+    "tools/saas-e2e"
   ]
 }

+ 5 - 1
services/web/frontend/js/features/form-helpers/hydrate-form.js

@@ -90,7 +90,11 @@ function formSubmitHelper(formEl) {
 
 async function validateCaptcha(formEl) {
   let captchaResponse
-  if (formEl.hasAttribute('captcha')) {
+  if (
+    formEl.hasAttribute('captcha') &&
+    // Disable captcha for E2E tests in dev-env.
+    !(process.env.NODE_ENV === 'development' && window.Cypress)
+  ) {
     if (
       formEl.getAttribute('action') === '/login' &&
       (await canSkipCaptcha(new FormData(formEl).get('email')))

+ 3 - 0
services/web/frontend/js/shared/components/recaptcha-2.tsx

@@ -21,6 +21,9 @@ export function ReCaptcha2({
   if (site && recaptchaDisabled[site]) {
     return null
   }
+  if (process.env.NODE_ENV === 'development' && window.Cypress) {
+    return null // Disable captcha for E2E tests in dev-env.
+  }
   return (
     <ReCAPTCHA
       ref={recaptchaRef}

+ 169 - 0
services/web/scripts/e2e_test_setup.mjs

@@ -0,0 +1,169 @@
+import fs from 'node:fs'
+import Path from 'node:path'
+import { fileURLToPath } from 'node:url'
+import { promiseMapWithLimit } from '@overleaf/promise-utils'
+import Settings from '@overleaf/settings'
+import { db } from '../app/src/infrastructure/mongodb.js'
+import GracefulShutdown from '../app/src/infrastructure/GracefulShutdown.js'
+import ProjectDeleter from '../app/src/Features/Project/ProjectDeleter.js'
+import SplitTestManager from '../app/src/Features/SplitTests/SplitTestManager.js'
+import UserDeleter from '../app/src/Features/User/UserDeleter.js'
+import UserRegistrationHandler from '../app/src/Features/User/UserRegistrationHandler.js'
+
+const MONOREPO = Path.dirname(
+  Path.dirname(Path.dirname(Path.dirname(fileURLToPath(import.meta.url))))
+)
+
+/**
+ * @param {string} email
+ * @return {Promise<void>}
+ */
+async function createUser(email) {
+  const user = await UserRegistrationHandler.promises.registerNewUser({
+    email,
+    password: process.env.CYPRESS_DEFAULT_PASSWORD,
+  })
+  const features = email.startsWith('free@')
+    ? Settings.defaultFeatures
+    : Settings.features.professional
+  await db.users.updateOne(
+    { _id: user._id },
+    {
+      $set: {
+        // Set admin flag.
+        isAdmin: email.startsWith('admin@'),
+        // Disable spell-checking for performance and flakiness reasons.
+        'ace.spellCheckLanguage': '',
+        // Override features.
+        features,
+        featuresOverrides: [{ features }],
+      },
+    }
+  )
+}
+
+/**
+ * @param {string} email
+ * @return {Promise<void>}
+ */
+async function deleteUser(email) {
+  const user = await db.users.findOne({ email })
+  if (!user) return
+  // Soft delete the user.
+  await UserDeleter.promises.deleteUser(user._id, {
+    force: true,
+    ipAddress: '0.0.0.0',
+  })
+  // Hard-delete the users projects.
+  const projects = await db.deletedProjects
+    .find(
+      { deletedProjectOwnerId: user._id },
+      { projection: { deletedProjectId: 1 } }
+    )
+    .toArray()
+  await promiseMapWithLimit(
+    10,
+    projects.map(p => p.deletedProjectId),
+    ProjectDeleter.promises.expireDeletedProject
+  )
+  // Hard-delete the user.
+  await UserDeleter.promises.expireDeletedUser(user._id)
+}
+
+/**
+ * @param {string} email
+ * @return {Promise<void>}
+ */
+async function provisionUser(email) {
+  await deleteUser(email)
+  await createUser(email)
+}
+
+async function provisionUsers() {
+  const emails = Settings.recaptcha.trustedUsers
+  console.log(`> Provisioning ${emails.length} E2E users.`)
+  await promiseMapWithLimit(3, emails, provisionUser)
+}
+
+async function purgeNewUsers() {
+  const users = await db.users
+    .find(
+      { email: Settings.recaptcha.trustedUsersRegex },
+      { projection: { email: 1 } }
+    )
+    .toArray()
+  console.log(`> Deleting ${users.length} newly created E2E users.`)
+  await promiseMapWithLimit(
+    3,
+    users.map(user => user.email),
+    deleteUser
+  )
+}
+
+const SPLIT_TEST_OVERRIDES = [
+  // disable writefull, oauth registration does not work in dev-env and their banners hide our buttons.
+  {
+    name: 'writefull-auto-account-creation',
+    versions: [
+      {
+        versionNumber: 1,
+        phase: 'release',
+        active: true,
+        analyticsEnabled: false,
+        variants: [{ name: 'enabled', rolloutPercent: 0, rolloutStripes: [] }],
+        createdAt: new Date(),
+      },
+    ],
+  },
+]
+
+async function provisionSplitTests() {
+  const backup = Path.join(
+    MONOREPO,
+    'backup',
+    'split-tests',
+    new Date().toISOString() + '.json'
+  )
+  console.log(
+    `> Backing up previous split-tests into ${backup}. You can import them again on https://www.dev-overleaf.com/admin/split-test via the [Import] button.`
+  )
+  const splitTests = await SplitTestManager.getRuntimeTests()
+  await fs.promises.mkdir(Path.dirname(backup), { recursive: true })
+  await fs.promises.writeFile(
+    backup,
+    JSON.stringify(splitTests.sort((a, b) => (a.name > b.name ? 1 : -1)))
+  )
+
+  // Imported from production via https://www.overleaf.com/admin/split-test -> "Copy all split tests" -> "Copy for E2E test setup"
+  const SPLIT_TESTS = JSON.parse(
+    await fs.promises.readFile(
+      Path.join(MONOREPO, 'tools/saas-e2e/split-tests.json')
+    )
+  )
+  console.log(`> Importing ${SPLIT_TESTS.length} split-tests from production.`)
+  await SplitTestManager.replaceSplitTests(SPLIT_TESTS)
+  console.log(
+    `> Importing ${SPLIT_TEST_OVERRIDES.length} split-tests for test compatibility.`
+  )
+  await SplitTestManager.mergeSplitTests(SPLIT_TEST_OVERRIDES, true)
+}
+
+async function main() {
+  if (process.env.NODE_ENV !== 'development') {
+    throw new Error('only available in dev-env')
+  }
+
+  await purgeNewUsers()
+  await provisionUsers()
+  await provisionSplitTests()
+}
+
+await main()
+await GracefulShutdown.gracefulShutdown(
+  {
+    close(cb) {
+      cb()
+    },
+  },
+  'SIGTERM'
+)