瀏覽代碼

Add vitest and configuration

GitOrigin-RevId: 1262f9f32a0db6a29d3feedd8158b8dd04e48b6a
Andrew Rumble 1 年之前
父節點
當前提交
0d3025b8cf

+ 743 - 6
package-lock.json

@@ -44850,6 +44850,7 @@
         "@uppy/react": "^3.2.1",
         "@uppy/utils": "^5.7.0",
         "@uppy/xhr-upload": "^3.6.0",
+        "@vitest/eslint-plugin": "1.1.44",
         "5to6-codemod": "^1.8.0",
         "abort-controller": "^3.0.0",
         "acorn": "^7.1.1",
@@ -44956,6 +44957,7 @@
         "tty-browserify": "^0.0.1",
         "typescript": "^5.0.4",
         "uuid": "^9.0.1",
+        "vitest": "^3.1.2",
         "w3c-keyname": "^2.2.8",
         "webpack": "^5.98.0",
         "webpack-assets-manifest": "^5.2.1",
@@ -44965,6 +44967,26 @@
         "yup": "^0.32.11"
       }
     },
+    "services/web/node_modules/@eslint-community/eslint-utils": {
+      "version": "4.7.0",
+      "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
+      "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==",
+      "dev": true,
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "eslint-visitor-keys": "^3.4.3"
+      },
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      },
+      "peerDependencies": {
+        "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+      }
+    },
     "services/web/node_modules/@google-cloud/bigquery": {
       "version": "6.0.3",
       "resolved": "https://registry.npmjs.org/@google-cloud/bigquery/-/bigquery-6.0.3.tgz",
@@ -45161,6 +45183,18 @@
       "dev": true,
       "license": "MIT"
     },
+    "services/web/node_modules/@types/debug": {
+      "version": "4.1.12",
+      "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
+      "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "peer": true,
+      "dependencies": {
+        "@types/ms": "*"
+      }
+    },
     "services/web/node_modules/@types/express": {
       "version": "4.17.21",
       "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
@@ -45179,6 +45213,143 @@
       "integrity": "sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw==",
       "dev": true
     },
+    "services/web/node_modules/@typescript-eslint/scope-manager": {
+      "version": "8.32.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz",
+      "integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==",
+      "dev": true,
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@typescript-eslint/types": "8.32.1",
+        "@typescript-eslint/visitor-keys": "8.32.1"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      }
+    },
+    "services/web/node_modules/@typescript-eslint/types": {
+      "version": "8.32.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz",
+      "integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==",
+      "dev": true,
+      "license": "MIT",
+      "peer": true,
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      }
+    },
+    "services/web/node_modules/@typescript-eslint/typescript-estree": {
+      "version": "8.32.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz",
+      "integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==",
+      "dev": true,
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@typescript-eslint/types": "8.32.1",
+        "@typescript-eslint/visitor-keys": "8.32.1",
+        "debug": "^4.3.4",
+        "fast-glob": "^3.3.2",
+        "is-glob": "^4.0.3",
+        "minimatch": "^9.0.4",
+        "semver": "^7.6.0",
+        "ts-api-utils": "^2.1.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.8.4 <5.9.0"
+      }
+    },
+    "services/web/node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
+      "version": "9.0.5",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+      "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+      "dev": true,
+      "license": "ISC",
+      "peer": true,
+      "dependencies": {
+        "brace-expansion": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "services/web/node_modules/@typescript-eslint/utils": {
+      "version": "8.32.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.1.tgz",
+      "integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==",
+      "dev": true,
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@eslint-community/eslint-utils": "^4.7.0",
+        "@typescript-eslint/scope-manager": "8.32.1",
+        "@typescript-eslint/types": "8.32.1",
+        "@typescript-eslint/typescript-estree": "8.32.1"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "eslint": "^8.57.0 || ^9.0.0",
+        "typescript": ">=4.8.4 <5.9.0"
+      }
+    },
+    "services/web/node_modules/@typescript-eslint/visitor-keys": {
+      "version": "8.32.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz",
+      "integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==",
+      "dev": true,
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@typescript-eslint/types": "8.32.1",
+        "eslint-visitor-keys": "^4.2.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      }
+    },
+    "services/web/node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
+      "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "peer": true,
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
     "services/web/node_modules/@uppy/core": {
       "version": "3.8.0",
       "resolved": "https://registry.npmjs.org/@uppy/core/-/core-3.8.0.tgz",
@@ -45332,6 +45503,130 @@
         "@uppy/core": "^3.8.0"
       }
     },
+    "services/web/node_modules/@vitest/eslint-plugin": {
+      "version": "1.1.44",
+      "resolved": "https://registry.npmjs.org/@vitest/eslint-plugin/-/eslint-plugin-1.1.44.tgz",
+      "integrity": "sha512-m4XeohMT+Dj2RZfxnbiFR+Cv5dEC0H7C6TlxRQT7GK2556solm99kxgzJp/trKrZvanZcOFyw7aABykUTfWyrg==",
+      "dev": true,
+      "license": "MIT",
+      "peerDependencies": {
+        "@typescript-eslint/utils": ">= 8.24.0",
+        "eslint": ">= 8.57.0",
+        "typescript": ">= 5.0.0",
+        "vitest": "*"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        },
+        "vitest": {
+          "optional": true
+        }
+      }
+    },
+    "services/web/node_modules/@vitest/expect": {
+      "version": "3.1.4",
+      "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.4.tgz",
+      "integrity": "sha512-xkD/ljeliyaClDYqHPNCiJ0plY5YIcM0OlRiZizLhlPmpXWpxnGMyTZXOHFhFeG7w9P5PBeL4IdtJ/HeQwTbQA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/spy": "3.1.4",
+        "@vitest/utils": "3.1.4",
+        "chai": "^5.2.0",
+        "tinyrainbow": "^2.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "services/web/node_modules/@vitest/expect/node_modules/chai": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz",
+      "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "assertion-error": "^2.0.1",
+        "check-error": "^2.1.1",
+        "deep-eql": "^5.0.1",
+        "loupe": "^3.1.0",
+        "pathval": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "services/web/node_modules/@vitest/pretty-format": {
+      "version": "3.1.4",
+      "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.4.tgz",
+      "integrity": "sha512-cqv9H9GvAEoTaoq+cYqUTCGscUjKqlJZC7PRwY5FMySVj5J+xOm1KQcCiYHJOEzOKRUhLH4R2pTwvFlWCEScsg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "tinyrainbow": "^2.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "services/web/node_modules/@vitest/runner": {
+      "version": "3.1.4",
+      "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.4.tgz",
+      "integrity": "sha512-djTeF1/vt985I/wpKVFBMWUlk/I7mb5hmD5oP8K9ACRmVXgKTae3TUOtXAEBfslNKPzUQvnKhNd34nnRSYgLNQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/utils": "3.1.4",
+        "pathe": "^2.0.3"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "services/web/node_modules/@vitest/snapshot": {
+      "version": "3.1.4",
+      "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.4.tgz",
+      "integrity": "sha512-JPHf68DvuO7vilmvwdPr9TS0SuuIzHvxeaCkxYcCD4jTk67XwL45ZhEHFKIuCm8CYstgI6LZ4XbwD6ANrwMpFg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/pretty-format": "3.1.4",
+        "magic-string": "^0.30.17",
+        "pathe": "^2.0.3"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "services/web/node_modules/@vitest/spy": {
+      "version": "3.1.4",
+      "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.4.tgz",
+      "integrity": "sha512-Xg1bXhu+vtPXIodYN369M86K8shGLouNjoVI78g8iAq2rFoHFdajNvJJ5A/9bPMFcfQqdaCpOgWKEoMQg/s0Yg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "tinyspy": "^3.0.2"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "services/web/node_modules/@vitest/utils": {
+      "version": "3.1.4",
+      "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.4.tgz",
+      "integrity": "sha512-yriMuO1cfFhmiGc8ataN51+9ooHRuURdfAZfwFd3usWynjzpLslZdYnRegTv32qdgtJTsj15FoeZe2g15fY1gg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/pretty-format": "3.1.4",
+        "loupe": "^3.1.3",
+        "tinyrainbow": "^2.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
     "services/web/node_modules/agent-base": {
       "version": "7.1.3",
       "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
@@ -45368,6 +45663,16 @@
         "ajv": "^8.8.2"
       }
     },
+    "services/web/node_modules/assertion-error": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+      "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      }
+    },
     "services/web/node_modules/base-x": {
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.1.tgz",
@@ -45405,6 +45710,16 @@
         "ieee754": "^1.2.1"
       }
     },
+    "services/web/node_modules/check-error": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
+      "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 16"
+      }
+    },
     "services/web/node_modules/csv": {
       "version": "6.2.5",
       "resolved": "https://registry.npmjs.org/csv/-/csv-6.2.5.tgz",
@@ -45451,6 +45766,16 @@
         }
       }
     },
+    "services/web/node_modules/deep-eql": {
+      "version": "5.0.2",
+      "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
+      "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "services/web/node_modules/duplexify": {
       "version": "4.1.2",
       "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz",
@@ -45462,12 +45787,26 @@
         "stream-shift": "^1.0.0"
       }
     },
-    "services/web/node_modules/esmock": {
-      "version": "2.6.7",
-      "resolved": "https://registry.npmjs.org/esmock/-/esmock-2.6.7.tgz",
-      "integrity": "sha512-4DmjZ0qQIG+NQV1njHvWrua/cZEuJq56A3pSELT2BjNuol1aads7BluofCbLErdO41Ic1XCd2UMepVLpjL64YQ==",
-      "dev": true,
-      "engines": {
+    "services/web/node_modules/eslint-visitor-keys": {
+      "version": "3.4.3",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+      "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "peer": true,
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "services/web/node_modules/esmock": {
+      "version": "2.6.7",
+      "resolved": "https://registry.npmjs.org/esmock/-/esmock-2.6.7.tgz",
+      "integrity": "sha512-4DmjZ0qQIG+NQV1njHvWrua/cZEuJq56A3pSELT2BjNuol1aads7BluofCbLErdO41Ic1XCd2UMepVLpjL64YQ==",
+      "dev": true,
+      "engines": {
         "node": ">=14.16.0"
       }
     },
@@ -45479,6 +45818,21 @@
         "node": ">=0.8.x"
       }
     },
+    "services/web/node_modules/fdir": {
+      "version": "6.4.4",
+      "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
+      "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
+      "dev": true,
+      "license": "MIT",
+      "peerDependencies": {
+        "picomatch": "^3 || ^4"
+      },
+      "peerDependenciesMeta": {
+        "picomatch": {
+          "optional": true
+        }
+      }
+    },
     "services/web/node_modules/fetch-mock": {
       "version": "12.5.2",
       "resolved": "https://registry.npmjs.org/fetch-mock/-/fetch-mock-12.5.2.tgz",
@@ -45598,6 +45952,18 @@
       "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==",
       "dev": true
     },
+    "services/web/node_modules/jiti": {
+      "version": "2.4.2",
+      "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz",
+      "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "peer": true,
+      "bin": {
+        "jiti": "lib/jiti-cli.mjs"
+      }
+    },
     "services/web/node_modules/json-schema-traverse": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
@@ -45609,6 +45975,13 @@
       "integrity": "sha512-gKO5uExCXvSm6zbF562EvM+rd1kQDnB9AZBbiQVzf1ZmdDpxUSvpnAaVOP83N/31mRK8Ml8/VE8DMvsAZQ+7wg==",
       "dev": true
     },
+    "services/web/node_modules/loupe": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz",
+      "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==",
+      "dev": true,
+      "license": "MIT"
+    },
     "services/web/node_modules/lru-cache": {
       "version": "7.10.1",
       "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.10.1.tgz",
@@ -45735,6 +46108,29 @@
         "isarray": "0.0.1"
       }
     },
+    "services/web/node_modules/pathval": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz",
+      "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 14.16"
+      }
+    },
+    "services/web/node_modules/picomatch": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
+      "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
     "services/web/node_modules/retry-request": {
       "version": "5.0.2",
       "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-5.0.2.tgz",
@@ -45778,6 +46174,20 @@
         "url": "https://opencollective.com/webpack"
       }
     },
+    "services/web/node_modules/semver": {
+      "version": "7.7.2",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+      "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+      "dev": true,
+      "license": "ISC",
+      "peer": true,
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
     "services/web/node_modules/sinon": {
       "version": "7.5.0",
       "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.5.0.tgz",
@@ -45941,6 +46351,30 @@
         }
       }
     },
+    "services/web/node_modules/tinyrainbow": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz",
+      "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
+    "services/web/node_modules/ts-api-utils": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
+      "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
+      "dev": true,
+      "license": "MIT",
+      "peer": true,
+      "engines": {
+        "node": ">=18.12"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.8.4"
+      }
+    },
     "services/web/node_modules/uuid": {
       "version": "9.0.1",
       "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
@@ -45953,6 +46387,294 @@
         "uuid": "dist/bin/uuid"
       }
     },
+    "services/web/node_modules/vite-node": {
+      "version": "3.1.4",
+      "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.4.tgz",
+      "integrity": "sha512-6enNwYnpyDo4hEgytbmc6mYWHXDHYEn0D1/rw4Q+tnHUGtKTJsn8T1YkX6Q18wI5LCrS8CTYlBaiCqxOy2kvUA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "cac": "^6.7.14",
+        "debug": "^4.4.0",
+        "es-module-lexer": "^1.7.0",
+        "pathe": "^2.0.3",
+        "vite": "^5.0.0 || ^6.0.0"
+      },
+      "bin": {
+        "vite-node": "vite-node.mjs"
+      },
+      "engines": {
+        "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "services/web/node_modules/vite-node/node_modules/vite": {
+      "version": "6.3.5",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
+      "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "esbuild": "^0.25.0",
+        "fdir": "^6.4.4",
+        "picomatch": "^4.0.2",
+        "postcss": "^8.5.3",
+        "rollup": "^4.34.9",
+        "tinyglobby": "^0.2.13"
+      },
+      "bin": {
+        "vite": "bin/vite.js"
+      },
+      "engines": {
+        "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/vitejs/vite?sponsor=1"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.3"
+      },
+      "peerDependencies": {
+        "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+        "jiti": ">=1.21.0",
+        "less": "*",
+        "lightningcss": "^1.21.0",
+        "sass": "*",
+        "sass-embedded": "*",
+        "stylus": "*",
+        "sugarss": "*",
+        "terser": "^5.16.0",
+        "tsx": "^4.8.1",
+        "yaml": "^2.4.2"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        },
+        "jiti": {
+          "optional": true
+        },
+        "less": {
+          "optional": true
+        },
+        "lightningcss": {
+          "optional": true
+        },
+        "sass": {
+          "optional": true
+        },
+        "sass-embedded": {
+          "optional": true
+        },
+        "stylus": {
+          "optional": true
+        },
+        "sugarss": {
+          "optional": true
+        },
+        "terser": {
+          "optional": true
+        },
+        "tsx": {
+          "optional": true
+        },
+        "yaml": {
+          "optional": true
+        }
+      }
+    },
+    "services/web/node_modules/vitest": {
+      "version": "3.1.4",
+      "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.4.tgz",
+      "integrity": "sha512-Ta56rT7uWxCSJXlBtKgIlApJnT6e6IGmTYxYcmxjJ4ujuZDI59GUQgVDObXXJujOmPDBYXHK1qmaGtneu6TNIQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/expect": "3.1.4",
+        "@vitest/mocker": "3.1.4",
+        "@vitest/pretty-format": "^3.1.4",
+        "@vitest/runner": "3.1.4",
+        "@vitest/snapshot": "3.1.4",
+        "@vitest/spy": "3.1.4",
+        "@vitest/utils": "3.1.4",
+        "chai": "^5.2.0",
+        "debug": "^4.4.0",
+        "expect-type": "^1.2.1",
+        "magic-string": "^0.30.17",
+        "pathe": "^2.0.3",
+        "std-env": "^3.9.0",
+        "tinybench": "^2.9.0",
+        "tinyexec": "^0.3.2",
+        "tinyglobby": "^0.2.13",
+        "tinypool": "^1.0.2",
+        "tinyrainbow": "^2.0.0",
+        "vite": "^5.0.0 || ^6.0.0",
+        "vite-node": "3.1.4",
+        "why-is-node-running": "^2.3.0"
+      },
+      "bin": {
+        "vitest": "vitest.mjs"
+      },
+      "engines": {
+        "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      },
+      "peerDependencies": {
+        "@edge-runtime/vm": "*",
+        "@types/debug": "^4.1.12",
+        "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+        "@vitest/browser": "3.1.4",
+        "@vitest/ui": "3.1.4",
+        "happy-dom": "*",
+        "jsdom": "*"
+      },
+      "peerDependenciesMeta": {
+        "@edge-runtime/vm": {
+          "optional": true
+        },
+        "@types/debug": {
+          "optional": true
+        },
+        "@types/node": {
+          "optional": true
+        },
+        "@vitest/browser": {
+          "optional": true
+        },
+        "@vitest/ui": {
+          "optional": true
+        },
+        "happy-dom": {
+          "optional": true
+        },
+        "jsdom": {
+          "optional": true
+        }
+      }
+    },
+    "services/web/node_modules/vitest/node_modules/@vitest/mocker": {
+      "version": "3.1.4",
+      "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.4.tgz",
+      "integrity": "sha512-8IJ3CvwtSw/EFXqWFL8aCMu+YyYXG2WUSrQbViOZkWTKTVicVwZ/YiEZDSqD00kX+v/+W+OnxhNWoeVKorHygA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/spy": "3.1.4",
+        "estree-walker": "^3.0.3",
+        "magic-string": "^0.30.17"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      },
+      "peerDependencies": {
+        "msw": "^2.4.9",
+        "vite": "^5.0.0 || ^6.0.0"
+      },
+      "peerDependenciesMeta": {
+        "msw": {
+          "optional": true
+        },
+        "vite": {
+          "optional": true
+        }
+      }
+    },
+    "services/web/node_modules/vitest/node_modules/chai": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz",
+      "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "assertion-error": "^2.0.1",
+        "check-error": "^2.1.1",
+        "deep-eql": "^5.0.1",
+        "loupe": "^3.1.0",
+        "pathval": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "services/web/node_modules/vitest/node_modules/vite": {
+      "version": "6.3.5",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
+      "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "esbuild": "^0.25.0",
+        "fdir": "^6.4.4",
+        "picomatch": "^4.0.2",
+        "postcss": "^8.5.3",
+        "rollup": "^4.34.9",
+        "tinyglobby": "^0.2.13"
+      },
+      "bin": {
+        "vite": "bin/vite.js"
+      },
+      "engines": {
+        "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/vitejs/vite?sponsor=1"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.3"
+      },
+      "peerDependencies": {
+        "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+        "jiti": ">=1.21.0",
+        "less": "*",
+        "lightningcss": "^1.21.0",
+        "sass": "*",
+        "sass-embedded": "*",
+        "stylus": "*",
+        "sugarss": "*",
+        "terser": "^5.16.0",
+        "tsx": "^4.8.1",
+        "yaml": "^2.4.2"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        },
+        "jiti": {
+          "optional": true
+        },
+        "less": {
+          "optional": true
+        },
+        "lightningcss": {
+          "optional": true
+        },
+        "sass": {
+          "optional": true
+        },
+        "sass-embedded": {
+          "optional": true
+        },
+        "stylus": {
+          "optional": true
+        },
+        "sugarss": {
+          "optional": true
+        },
+        "terser": {
+          "optional": true
+        },
+        "tsx": {
+          "optional": true
+        },
+        "yaml": {
+          "optional": true
+        }
+      }
+    },
     "services/web/node_modules/xml-crypto": {
       "version": "2.1.6",
       "resolved": "https://registry.npmjs.org/xml-crypto/-/xml-crypto-2.1.6.tgz",
@@ -45980,6 +46702,21 @@
       "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
       "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
     },
+    "services/web/node_modules/yaml": {
+      "version": "2.8.0",
+      "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz",
+      "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==",
+      "dev": true,
+      "license": "ISC",
+      "optional": true,
+      "peer": true,
+      "bin": {
+        "yaml": "bin.mjs"
+      },
+      "engines": {
+        "node": ">= 14.6"
+      }
+    },
     "tools/saas-e2e": {
       "name": "@overleaf/saas-e2e",
       "devDependencies": {

+ 24 - 0
services/web/.eslintrc.js

@@ -64,6 +64,10 @@ module.exports = {
     {
       // Test specific rules
       files: ['**/test/**/*.*'],
+      excludedFiles: [
+        '**/test/unit/src/**/*.test.mjs',
+        'test/unit/vitest_bootstrap.mjs',
+      ], // exclude vitest files
       plugins: ['mocha', 'chai-expect', 'chai-friendly'],
       env: {
         mocha: true,
@@ -95,6 +99,26 @@ module.exports = {
         '@typescript-eslint/no-unused-expressions': 'off',
       },
     },
+    {
+      files: [
+        '**/test/unit/src/**/*.test.mjs',
+        'test/unit/vitest_bootstrap.mjs',
+      ],
+      env: {
+        jest: true, // best match for vitest API etc.
+      },
+      plugins: ['@vitest', 'chai-expect', 'chai-friendly'], // still using chai for now
+      rules: {
+        // Swap the no-unused-expressions rule with a more chai-friendly one
+        'no-unused-expressions': 'off',
+        'chai-friendly/no-unused-expressions': 'error',
+
+        // chai-specific rules
+        'chai-expect/missing-assertion': 'error',
+        'chai-expect/terminating-properties': 'error',
+        '@typescript-eslint/no-unused-expressions': 'off',
+      },
+    },
     {
       // ES specific rules
       files: [

+ 10 - 0
services/web/Makefile

@@ -83,6 +83,16 @@ test_unit_app:
 	$(DOCKER_COMPOSE) run --name unit_test_$(BUILD_DIR_NAME) --rm test_unit
 	$(DOCKER_COMPOSE) down -v -t 0
 
+test_unit_esm: export COMPOSE_PROJECT_NAME=unit_test_esm_$(BUILD_DIR_NAME)
+test_unit_esm:
+	$(DOCKER_COMPOSE) run --rm test_unit npm run test:unit:esm
+	$(DOCKER_COMPOSE) down -v -t 0
+
+test_unit_esm_watch: export COMPOSE_PROJECT_NAME=unit_test_esm_watch_$(BUILD_DIR_NAME)
+test_unit_esm_watch:
+	$(DOCKER_COMPOSE) run --rm test_unit npm run test:unit:esm:watch
+	$(DOCKER_COMPOSE) down -v -t 0
+
 TEST_SUITES = $(sort $(filter-out \
 	$(wildcard test/unit/src/helpers/*), \
 	$(wildcard test/unit/src/*/*)))

+ 45 - 0
services/web/bin/test_unit_run_dir

@@ -0,0 +1,45 @@
+#!/bin/bash
+
+TARGET_DIR=$1
+
+
+declare -a vitest_args=("$TARGET_DIR")
+
+if [[ -n "$MOCHA_GREP" ]]; then
+  vitest_args+=("--testNamePattern" "$MOCHA_GREP")
+fi
+
+if [[ -n "$VITEST_NO_CACHE" ]]; then
+  echo "Disabling cache for vitest."
+  vitest_args+=("--no-cache")
+fi
+
+echo "Running unit tests in directory: $TARGET_DIR"
+
+npm run test:unit:esm -- "${vitest_args[@]}"
+
+vitest_status=$?
+
+if find "$TARGET_DIR" -type f -name "*.js" -print -quit | grep -q '.'; then
+  mocha --recursive --timeout 25000 --exit --grep="$MOCHA_GREP" --require test/unit/bootstrap.js --extension=js "$TARGET_DIR"
+  mocha_status=$?
+else
+  echo "No mocha tests found in $TARGET_DIR, skipping mocha step."
+  mocha_status=0
+fi
+
+if [ $mocha_status -eq 0 ] && [ $vitest_status -eq 0 ]; then
+  exit 0
+fi
+
+# Report status briefly at the end for failures
+
+if [ $mocha_status -ne 0 ]; then
+  echo "Mocha tests failed with status: $mocha_status"
+fi
+
+if [ $vitest_status -ne 0 ]; then
+  echo "Vitest tests failed with status: $vitest_status"
+fi
+
+exit 1

+ 1 - 0
services/web/docker-compose.ci.yml

@@ -21,6 +21,7 @@ services:
       OVERLEAF_CONFIG:
       NODE_ENV: test
       NODE_OPTIONS: "--unhandled-rejections=strict"
+      VITEST_NO_CACHE: true
     depends_on:
       - mongo
 

+ 5 - 1
services/web/package.json

@@ -9,10 +9,12 @@
   "scripts": {
     "test:acceptance:run_dir": "mocha --recursive --timeout 25000 --grep=$MOCHA_GREP --require test/acceptance/bootstrap.js",
     "test:acceptance:app": "npm run test:acceptance:run_dir -- test/acceptance/src",
-    "test:unit:run_dir": "mocha --recursive --timeout 25000 --exit --grep=$MOCHA_GREP --require test/unit/bootstrap.js",
+    "test:unit:run_dir": "bin/test_unit_run_dir",
     "test:unit:all": "npm run test:unit:run_dir -- test/unit/src modules/*/test/unit/src",
     "test:unit:all:silent": "npm run test:unit:all -- --reporter dot",
     "test:unit:app": "npm run test:unit:run_dir -- test/unit/src",
+    "test:unit:esm": "vitest run",
+    "test:unit:esm:watch": "vitest",
     "test:frontend": "NODE_ENV=test TZ=GMT mocha --recursive --timeout 5000 --exit --extension js,jsx,mjs,ts,tsx --grep=$MOCHA_GREP --require test/frontend/bootstrap.js --ignore '**/*.spec.{js,jsx,ts,tsx}' --ignore '**/helpers/**/*.{js,jsx,ts,tsx}' test/frontend modules/*/test/frontend",
     "test:frontend:coverage": "c8 --all --include 'frontend/js' --include 'modules/*/frontend/js' --exclude 'frontend/js/vendor' --reporter=lcov --reporter=text-summary npm run test:frontend",
     "start": "node app.mjs",
@@ -250,6 +252,7 @@
     "@uppy/react": "^3.2.1",
     "@uppy/utils": "^5.7.0",
     "@uppy/xhr-upload": "^3.6.0",
+    "@vitest/eslint-plugin": "1.1.44",
     "5to6-codemod": "^1.8.0",
     "abort-controller": "^3.0.0",
     "acorn": "^7.1.1",
@@ -356,6 +359,7 @@
     "tty-browserify": "^0.0.1",
     "typescript": "^5.0.4",
     "uuid": "^9.0.1",
+    "vitest": "^3.1.2",
     "w3c-keyname": "^2.2.8",
     "webpack": "^5.98.0",
     "webpack-assets-manifest": "^5.2.1",

+ 1 - 24
services/web/test/unit/bootstrap.js

@@ -1,29 +1,6 @@
 const Path = require('path')
-const chai = require('chai')
 const sinon = require('sinon')
-
-/*
- * Chai configuration
- */
-
-// add chai.should()
-chai.should()
-
-// Load sinon-chai assertions so expect(stubFn).to.have.been.calledWith('abc')
-// has a nicer failure messages
-chai.use(require('sinon-chai'))
-
-// Load promise support for chai
-chai.use(require('chai-as-promised'))
-
-// Do not truncate assertion errors
-chai.config.truncateThreshold = 0
-
-// add support for mongoose in sinon
-require('sinon-mongoose')
-
-// ensure every ObjectId has the id string as a property for correct comparisons
-require('mongodb-legacy').ObjectId.cacheHexString = true
+require('./common_bootstrap')
 
 /*
  * Global stubs

+ 24 - 0
services/web/test/unit/common_bootstrap.js

@@ -0,0 +1,24 @@
+const chai = require('chai')
+
+/*
+ * Chai configuration
+ */
+
+// add chai.should()
+chai.should()
+
+// Load sinon-chai assertions so expect(stubFn).to.have.been.calledWith('abc')
+// has a nicer failure messages
+chai.use(require('sinon-chai'))
+
+// Load promise support for chai
+chai.use(require('chai-as-promised'))
+
+// Do not truncate assertion errors
+chai.config.truncateThreshold = 0
+
+// add support for mongoose in sinon
+require('sinon-mongoose')
+
+// ensure every ObjectId has the id string as a property for correct comparisons
+require('mongodb-legacy').ObjectId.cacheHexString = true

+ 29 - 0
services/web/test/unit/vitest_bootstrap.mjs

@@ -0,0 +1,29 @@
+import { vi } from 'vitest'
+import './common_bootstrap.js'
+import sinon from 'sinon'
+import logger from '@overleaf/logger'
+
+vi.mock('@overleaf/logger', async () => {
+  const sinon = (await import('sinon')).default
+  return {
+    default: {
+      debug: sinon.stub(),
+      info: sinon.stub(),
+      log: sinon.stub(),
+      warn: sinon.stub(),
+      err: sinon.stub(),
+      error: sinon.stub(),
+      fatal: sinon.stub(),
+    },
+  }
+})
+
+beforeEach(ctx => {
+  ctx.logger = logger
+})
+
+afterEach(() => {
+  vi.restoreAllMocks()
+  vi.resetModules()
+  sinon.restore()
+})

+ 12 - 0
services/web/vitest.config.js

@@ -0,0 +1,12 @@
+const { defineConfig } = require('vitest/config')
+
+module.exports = defineConfig({
+  test: {
+    include: [
+      'modules/*/test/unit/**/*.test.mjs',
+      'test/unit/src/**/*.test.mjs',
+    ],
+    setupFiles: ['./test/unit/vitest_bootstrap.mjs'],
+    globals: true,
+  },
+})